GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/widget/display/node.rs (9.9K)
1 //! Narrow-trait `Node` (Phase 5k) — a network-editor node box: draggable with grid snap
2 //! (self-moving, via [`Input::drag_reposition`]), a geometry-visibility toggle sub-zone, and
3 //! two controller capabilities ([`ParamController`] + [`GeomController`]) re-exposed through
4 //! the `Input` hooks for the legacy `WidgetHost::as_*_controller` downcasts.
5
6 use crate::colors;
7 use crate::scene::layout::Rect;
8 use crate::scene::paint::PaintCtx;
9 use crate::widget::{
10 Adapted, ElementState, Event, EventCtx, GeomController, Input, Layout, MouseButton, Paint,
11 ParamController,
12 };
13
14 #[derive(Debug, Clone)]
15 pub struct Node {
16 hovered: bool,
17 selected: bool,
18 dragging: bool,
19 drag_ox: f32,
20 drag_oy: f32,
21 bounds: Option<(f32, f32, f32, f32)>,
22 grid_snap_x: f32,
23 grid_snap_y: f32,
24 grid_origin_x: f32,
25 grid_origin_y: f32,
26 name: String,
27 pub parameters: Vec<(String, String, String)>,
28 geom_visible: bool,
29 geom_toggled: bool,
30 pub(crate) toggle_hovered: bool,
31 }
32
33 impl Node {
34 pub fn new(x: f32, y: f32, w: f32, h: f32, name: &str) -> Adapted<Node> {
35 let mut node = Adapted::new(Node {
36 hovered: false,
37 selected: false,
38 dragging: false,
39 drag_ox: 0.0,
40 drag_oy: 0.0,
41 bounds: None,
42 grid_snap_x: 0.0,
43 grid_snap_y: 0.0,
44 grid_origin_x: 0.0,
45 grid_origin_y: 0.0,
46 name: name.to_string(),
47 parameters: Vec::new(),
48 geom_visible: true,
49 geom_toggled: false,
50 toggle_hovered: false,
51 });
52 crate::widget::WidgetHost::set_rect(&mut node, x, y, w, h);
53 node
54 }
55
56 pub(crate) fn toggle_rect(rect: Rect) -> (f32, f32, f32, f32) {
57 (rect.x + rect.width - 30.0, rect.y + (rect.height - 18.0) / 2.0, 18.0, 18.0)
58 }
59
60 fn in_toggle(rect: Rect, px: f32, py: f32) -> bool {
61 let (tx, ty, tw, th) = Self::toggle_rect(rect);
62 px >= tx && px < tx + tw && py >= ty && py < ty + th
63 }
64
65 pub fn set_grid_snap(&mut self, gx: f32, gy: f32) {
66 self.grid_snap_x = gx;
67 self.grid_snap_y = gy;
68 }
69 pub fn set_grid_origin(&mut self, ox: f32, oy: f32) {
70 self.grid_origin_x = ox;
71 self.grid_origin_y = oy;
72 }
73 pub fn set_node_name(&mut self, name: &str) {
74 self.name = name.to_string();
75 }
76 }
77
78 impl Adapted<Node> {
79 pub fn with_params(mut self, params: &[(&str, &str)]) -> Self {
80 self.parameters =
81 params.iter().map(|(k, v)| (k.to_string(), v.to_string(), "string".to_string())).collect();
82 self
83 }
84
85 pub fn with_grid_snap(mut self, gx: f32, gy: f32) -> Self {
86 self.set_grid_snap(gx, gy);
87 self
88 }
89 }
90
91 impl Layout for Node {}
92
93 impl Paint for Node {
94 fn color(&self) -> [f32; 4] {
95 if self.dragging {
96 colors::node_drag_color()
97 } else if self.selected {
98 colors::node_selected_color()
99 } else {
100 colors::node_color()
101 }
102 }
103
104 fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
105 ctx.quad(rect, Paint::color(self));
106
107 let (tx, ty, tw, th) = Self::toggle_rect(rect);
108 let bg_color = if self.toggle_hovered { colors::TOGGLE_HOVER } else { colors::TOGGLE_OFF };
109 ctx.quad(Rect { x: tx, y: ty, width: tw, height: th }, bg_color);
110 if self.geom_visible {
111 let inset = 3.0;
112 ctx.quad(
113 Rect { x: tx + inset, y: ty + inset, width: tw - inset * 2.0, height: th - inset * 2.0 },
114 colors::TOGGLE_ON,
115 );
116 }
117
118 // Deliberately UNBOUNDED, and the only text in the toolkit that is.
119 // The name is drawn in the gutter to the right of the node box, so a
120 // clip to `rect` would erase every node name on the canvas — and the
121 // node has no idea how much gutter it has, because only the canvas
122 // placing it knows where the next node or the viewport edge is. The
123 // bound for this one belongs to the HOST: clip the node layer, not the
124 // node. Do not fix this by clipping to rect.
125 ctx.text(
126 self.name.clone(),
127 rect.x + rect.width + 8.0,
128 crate::layout::align_text_y(rect.y, rect.height, 14.0, 0.0),
129 14.0,
130 [0xcc, 0xcc, 0xd4],
131 );
132 }
133 }
134
135 impl Input for Node {
136 fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
137 match event {
138 Event::PointerMove { x: px, y: py, .. } => {
139 let r = ectx.rect;
140 let was_hovered = self.hovered;
141 self.hovered =
142 *px >= r.x && *px <= r.x + r.width && *py >= r.y && *py <= r.y + r.height;
143 let was_toggle = self.toggle_hovered;
144 self.toggle_hovered = Self::in_toggle(r, *px, *py);
145 was_hovered != self.hovered || was_toggle != self.toggle_hovered
146 }
147 Event::MouseButton {
148 button: MouseButton::Left,
149 state: ElementState::Pressed,
150 x: px,
151 y: py,
152 ..
153 } => {
154 if Self::in_toggle(ectx.rect, *px, *py) {
155 self.geom_visible = !self.geom_visible;
156 self.geom_toggled = true;
157 } else {
158 self.drag_begin(*px, *py, ectx.rect);
159 }
160 true
161 }
162 Event::MouseButton { button: MouseButton::Left, state: ElementState::Released, .. } => {
163 if self.dragging {
164 self.drag_end();
165 true
166 } else {
167 false
168 }
169 }
170 // Legacy `focus()`/`unfocus()` toggled selection; hosts reach them through the
171 // adapter's focus forwards, which arrive here as focus events.
172 Event::FocusIn => {
173 self.selected = true;
174 true
175 }
176 Event::FocusOut => {
177 self.selected = false;
178 true
179 }
180 _ => false,
181 }
182 }
183
184 fn draggable(&self, _rect: Rect) -> bool {
185 !self.toggle_hovered
186 }
187 fn is_dragging(&self) -> bool {
188 self.dragging
189 }
190
191 fn drag_begin(&mut self, px: f32, py: f32, rect: Rect) {
192 self.dragging = true;
193 self.drag_ox = px - rect.x;
194 self.drag_oy = py - rect.y;
195 }
196
197 fn drag_reposition(&mut self, px: f32, py: f32, rect: Rect) -> Option<(f32, f32)> {
198 let nx = px - self.drag_ox;
199 let ny = py - self.drag_oy;
200 let (nx, ny) = if let Some((bx, by, bw, bh)) = self.bounds {
201 (nx.clamp(bx, bx + bw - rect.width), ny.clamp(by, by + bh - rect.height))
202 } else {
203 (nx, ny)
204 };
205 let nx = if self.grid_snap_x > 0.0 {
206 let relative = nx - self.grid_origin_x;
207 (relative / self.grid_snap_x).round() * self.grid_snap_x + self.grid_origin_x
208 } else {
209 nx
210 };
211 let ny = if self.grid_snap_y > 0.0 {
212 let relative = ny - self.grid_origin_y;
213 (relative / self.grid_snap_y).round() * self.grid_snap_y + self.grid_origin_y
214 } else {
215 ny
216 };
217 if (nx - rect.x).abs() > 0.01 || (ny - rect.y).abs() > 0.01 {
218 Some((nx, ny))
219 } else {
220 None
221 }
222 }
223
224 fn drag_end(&mut self) {
225 self.dragging = false;
226 }
227
228 fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
229 self.bounds = Some((bx, by, bw, bh));
230 }
231
232 }
233
234 impl ParamController for Node {
235 fn node_params(&self) -> Vec<(String, String, String)> {
236 self.parameters.clone()
237 }
238 fn set_display_params(&mut self, params: &[(String, String, String)]) {
239 self.parameters = params.to_vec();
240 }
241 }
242
243 impl GeomController for Node {
244 fn set_geom_visible(&mut self, visible: bool) {
245 self.geom_visible = visible;
246 }
247 fn geom_visible(&self) -> bool {
248 self.geom_visible
249 }
250 fn take_geom_toggle(&mut self) -> bool {
251 std::mem::take(&mut self.geom_toggled)
252 }
253 }
254
255 #[cfg(test)]
256 mod tests {
257 use super::*;
258 use crate::context::UiContext;
259 use crate::widget::WidgetHost;
260
261 #[test]
262 fn toggle_click_flips_geom_and_press_starts_drag() {
263 let mut ctx = UiContext::new();
264 let mut node = Node::new(100.0, 100.0, 120.0, 40.0, "geo1");
265 let (id, ptr) = (node.id(), node.as_ptr_mut());
266 ctx.register_widget(id, ptr);
267
268 // Toggle zone: (100+120-30, 100+11) => 18x18 at (190, 111).
269 assert!(node.mouse_input(MouseButton::Left, ElementState::Pressed, 195.0, 115.0, &mut ctx));
270 let geom: &mut dyn GeomController = &mut *node;
271 assert!(!geom.geom_visible(), "toggle click hides geometry");
272 assert!(geom.take_geom_toggle(), "toggle flag set once");
273 assert!(!geom.take_geom_toggle(), "…and drained");
274
275 // A press outside the toggle starts a drag; reposition snaps to the drag origin.
276 assert!(node.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 110.0, &mut ctx));
277 assert!(node.is_dragging());
278 assert!(node.drag_update(150.0, 130.0));
279 assert_eq!(WidgetHost::rect(&node), (140.0, 120.0, 120.0, 40.0), "moved by the pointer delta");
280 assert!(node.mouse_input(MouseButton::Left, ElementState::Released, 150.0, 130.0, &mut ctx));
281 assert!(!node.is_dragging());
282 }
283
284 #[test]
285 fn param_controller_roundtrips_through_element() {
286 let mut node = Node::new(0.0, 0.0, 10.0, 10.0, "n").with_params(&[("k", "v")]);
287 let params = ParamController::node_params(&*node);
288 assert_eq!(params, vec![("k".to_string(), "v".to_string(), "string".to_string())]);
289 ParamController::set_display_params(&mut *node, &[("a".to_string(), "b".to_string(), "int".to_string())]);
290 assert_eq!(node.parameters.len(), 1);
291 assert_eq!(node.parameters[0].2, "int");
292 }
293 }