git.lucas.co / cce-ui
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 }