1use taffy::geometry::Point;
2use taffy::prelude::*;
3use winit::event::WindowEvent;
4
5use crate::framework::{DrawContext, EventContext, Widget};
6use crate::renderer::Renderer;
7
8const SCROLLBAR_WIDTH: f32 = 8.0;
9const SCROLLBAR_MARGIN: f32 = 2.0;
10const SCROLLBAR_MIN_THUMB: f32 = 20.0;
11
12pub struct WidgetNode {
13 pub(crate) widget: Box<dyn Widget>,
14 pub(crate) children: Vec<WidgetNode>,
15 pub(crate) node: Option<NodeId>,
16 pub(crate) scroll_y: f32,
17 pub(crate) scrollbar_dragging: bool,
19 pub(crate) scrollbar_drag_start_y: f32,
20 pub(crate) scrollbar_drag_start_scroll: f32,
21}
22
23impl WidgetNode {
24 pub fn new(widget: impl Widget + 'static, children: Vec<WidgetNode>) -> Self {
25 Self {
26 widget: Box::new(widget),
27 children,
28 node: None,
29 scroll_y: 0.0,
30 scrollbar_dragging: false,
31 scrollbar_drag_start_y: 0.0,
32 scrollbar_drag_start_scroll: 0.0,
33 }
34 }
35}
36
37pub fn build_taffy(node: &mut WidgetNode, taffy: &mut TaffyTree) -> NodeId {
38 let child_nodes = node
39 .children
40 .iter_mut()
41 .map(|child| build_taffy(child, taffy))
42 .collect::<Vec<_>>();
43
44 let style = node.widget.style();
45 let node_id = if child_nodes.is_empty() {
46 taffy.new_leaf(style).expect("create leaf")
47 } else {
48 taffy
49 .new_with_children(style, &child_nodes)
50 .expect("create node")
51 };
52
53 node.node = Some(node_id);
54 node_id
55}
56
57pub fn sync_styles(node: &mut WidgetNode, taffy: &mut TaffyTree, width: f32, height: f32, is_root: bool) {
58 let Some(node_id) = node.node else {
59 return;
60 };
61
62 let mut style = node.widget.style();
63 if is_root {
64 style.size = Size {
65 width: Dimension::Length(width),
66 height: Dimension::Length(height),
67 };
68 }
69
70 taffy.set_style(node_id, style).expect("set style");
71
72 for child in &mut node.children {
73 sync_styles(child, taffy, width, height, false);
74 }
75}
76
77pub fn collect_focus_paths(node: &WidgetNode, path: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
78 if node.widget.is_focusable() {
79 out.push(path.clone());
80 }
81
82 for (index, child) in node.children.iter().enumerate() {
83 path.push(index);
84 collect_focus_paths(child, path, out);
85 path.pop();
86 }
87}
88
89pub fn widget_mut_at_path<'a>(node: &'a mut WidgetNode, path: &[usize]) -> Option<&'a mut dyn Widget> {
90 if path.is_empty() {
91 return Some(node.widget.as_mut());
92 }
93
94 let idx = path[0];
95 if idx >= node.children.len() {
96 return None;
97 }
98
99 widget_mut_at_path(&mut node.children[idx], &path[1..])
100}
101
102pub fn draw_widgets(node: &WidgetNode, taffy: &TaffyTree, renderer: &mut Renderer) {
103 draw_widgets_offset(node, taffy, renderer, 0.0, 0.0);
104}
105
106fn draw_widgets_offset(node: &WidgetNode, taffy: &TaffyTree, renderer: &mut Renderer, parent_x: f32, parent_y: f32) {
107 let Some(node_id) = node.node else {
108 return;
109 };
110
111 let layout = taffy.layout(node_id).expect("layout");
112 let abs_x = parent_x + layout.location.x;
113 let abs_y = parent_y + layout.location.y;
114
115 let mut absolute_layout = *layout;
116 absolute_layout.location = Point { x: abs_x, y: abs_y };
117
118 let mut ctx = DrawContext {
119 renderer,
120 layout: &absolute_layout,
121 };
122 node.widget.draw(&mut ctx);
123
124 let is_scroll = node.widget.is_scrollable();
125 if is_scroll {
126 renderer.push_clip((abs_x, abs_y, layout.size.width, layout.size.height));
127 }
128
129 let child_y = abs_y - node.scroll_y;
130 for child in &node.children {
131 draw_widgets_offset(child, taffy, renderer, abs_x, child_y);
132 }
133
134 if is_scroll {
135 renderer.pop_clip();
136
137 let container_h = layout.size.height;
139 let content_h = content_height(node, taffy);
140 if content_h > container_h {
141 draw_scrollbar(renderer, abs_x, abs_y, layout.size.width, container_h, content_h, node.scroll_y);
142 }
143 }
144}
145
146fn content_height(node: &WidgetNode, taffy: &TaffyTree) -> f32 {
147 let mut h: f32 = 0.0;
148 for child in &node.children {
149 if let Some(child_id) = child.node {
150 let cl = taffy.layout(child_id).expect("child layout");
151 let bottom = cl.location.y + cl.size.height;
152 h = h.max(bottom);
153 }
154 }
155 h
156}
157
158fn draw_scrollbar(
159 renderer: &mut Renderer,
160 container_x: f32,
161 container_y: f32,
162 container_w: f32,
163 container_h: f32,
164 content_h: f32,
165 scroll_y: f32,
166) {
167 let track_x = container_x + container_w - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN;
168 let track_y = container_y + SCROLLBAR_MARGIN;
169 let track_h = container_h - SCROLLBAR_MARGIN * 2.0;
170
171 renderer.fill_rect_rounded(
173 (track_x, track_y, SCROLLBAR_WIDTH, track_h),
174 [0.3, 0.3, 0.3, 0.15],
175 SCROLLBAR_WIDTH / 2.0,
176 );
177
178 let ratio = container_h / content_h;
180 let thumb_h = (ratio * track_h).max(SCROLLBAR_MIN_THUMB);
181 let max_scroll = (content_h - container_h).max(0.0);
182 let scroll_ratio = if max_scroll > 0.0 { scroll_y / max_scroll } else { 0.0 };
183 let thumb_y = track_y + scroll_ratio * (track_h - thumb_h);
184
185 renderer.fill_rect_rounded(
186 (track_x, thumb_y, SCROLLBAR_WIDTH, thumb_h),
187 [0.6, 0.6, 0.6, 0.5],
188 SCROLLBAR_WIDTH / 2.0,
189 );
190}
191
192pub fn dispatch_event(
193 node: &mut WidgetNode,
194 taffy: &TaffyTree,
195 event: &WindowEvent,
196 path: &mut Vec<usize>,
197) -> Option<Vec<usize>> {
198 dispatch_event_offset(node, taffy, event, path, 0.0, 0.0)
199}
200
201fn dispatch_event_offset(
202 node: &mut WidgetNode,
203 taffy: &TaffyTree,
204 event: &WindowEvent,
205 path: &mut Vec<usize>,
206 parent_x: f32,
207 parent_y: f32,
208) -> Option<Vec<usize>> {
209 let Some(node_id) = node.node else {
210 return None;
211 };
212 let layout = taffy.layout(node_id).expect("layout");
213 let abs_x = parent_x + layout.location.x;
214 let abs_y = parent_y + layout.location.y;
215
216 let child_y = abs_y - node.scroll_y;
217 for (index, child) in node.children.iter_mut().enumerate() {
218 path.push(index);
219 if let Some(found) = dispatch_event_offset(child, taffy, event, path, abs_x, child_y) {
220 return Some(found);
221 }
222 path.pop();
223 }
224
225 let mut absolute_layout = *layout;
226 absolute_layout.location = Point { x: abs_x, y: abs_y };
227
228 let mut ctx = EventContext {
229 event,
230 layout: &absolute_layout,
231 };
232 if node.widget.handle_event(&mut ctx) {
233 return Some(path.clone());
234 }
235
236 None
237}
238
239pub fn dispatch_scroll(node: &mut WidgetNode, delta_y: f32, cursor_x: f32, cursor_y: f32, taffy: &TaffyTree) {
242 if !dispatch_scroll_offset(node, delta_y, cursor_x, cursor_y, taffy, 0.0, 0.0) {
243 scroll_node(node, delta_y, taffy);
245 }
246}
247
248fn dispatch_scroll_offset(
249 node: &mut WidgetNode,
250 delta_y: f32,
251 cx: f32,
252 cy: f32,
253 taffy: &TaffyTree,
254 parent_x: f32,
255 parent_y: f32,
256) -> bool {
257 let Some(node_id) = node.node else { return false; };
258 let layout = taffy.layout(node_id).expect("layout");
259 let abs_x = parent_x + layout.location.x;
260 let abs_y = parent_y + layout.location.y;
261
262 let inside = cx >= abs_x
264 && cx <= abs_x + layout.size.width
265 && cy >= abs_y
266 && cy <= abs_y + layout.size.height;
267
268 if !inside {
269 return false;
270 }
271
272 let child_y = abs_y - node.scroll_y;
274 for child in &mut node.children {
275 if dispatch_scroll_offset(child, delta_y, cx, cy, taffy, abs_x, child_y) {
276 return true;
277 }
278 }
279
280 if node.widget.is_scrollable() {
282 scroll_node(node, delta_y, taffy);
283 return true;
284 }
285
286 false
287}
288
289fn scroll_node(node: &mut WidgetNode, delta_y: f32, taffy: &TaffyTree) {
290 let Some(node_id) = node.node else { return; };
291 let layout = taffy.layout(node_id).expect("layout");
292 let container_h = layout.size.height;
293
294 let mut content_h: f32 = 0.0;
296 for child in &node.children {
297 if let Some(child_id) = child.node {
298 let cl = taffy.layout(child_id).expect("child layout");
299 let bottom = cl.location.y + cl.size.height;
300 content_h = content_h.max(bottom);
301 }
302 }
303
304 let max_scroll = (content_h - container_h).max(0.0);
305 node.scroll_y = (node.scroll_y - delta_y).clamp(0.0, max_scroll);
306}
307
308pub fn scroll_root(node: &mut WidgetNode, delta_y: f32, viewport_h: f32, taffy: &TaffyTree) {
310 let _ = viewport_h;
311 scroll_node(node, delta_y, taffy);
312}
313
314pub fn update_widget_measures(node: &mut WidgetNode, measures: &[Vec<f32>]) {
315 node.widget.update_measures(measures);
316 for child in &mut node.children {
317 update_widget_measures(child, measures);
318 }
319}
320
321pub fn handle_scrollbar_event(
323 node: &mut WidgetNode,
324 taffy: &TaffyTree,
325 event: &WindowEvent,
326) -> bool {
327 handle_scrollbar_event_offset(node, taffy, event, 0.0, 0.0)
328}
329
330fn handle_scrollbar_event_offset(
331 node: &mut WidgetNode,
332 taffy: &TaffyTree,
333 event: &WindowEvent,
334 parent_x: f32,
335 parent_y: f32,
336) -> bool {
337 let Some(node_id) = node.node else { return false; };
338 let layout = taffy.layout(node_id).expect("layout");
339 let abs_x = parent_x + layout.location.x;
340 let abs_y = parent_y + layout.location.y;
341
342 let child_y = abs_y - node.scroll_y;
344 for child in &mut node.children {
345 if handle_scrollbar_event_offset(child, taffy, event, abs_x, child_y) {
346 return true;
347 }
348 }
349
350 if !node.widget.is_scrollable() {
351 return false;
352 }
353
354 let container_h = layout.size.height;
355 let content_h = content_height(node, taffy);
356 if content_h <= container_h {
357 return false;
358 }
359
360 let _track_y = abs_y + SCROLLBAR_MARGIN;
361 let track_h = container_h - SCROLLBAR_MARGIN * 2.0;
362 let max_scroll = (content_h - container_h).max(0.0);
363 let ratio = container_h / content_h;
364 let thumb_h = (ratio * track_h).max(SCROLLBAR_MIN_THUMB);
365
366 match event {
367 WindowEvent::CursorMoved { position, .. } => {
368 let _cx = position.x as f32;
369 let cy = position.y as f32;
370
371 if node.scrollbar_dragging {
372 let delta_y = cy - node.scrollbar_drag_start_y;
374 let scroll_per_pixel = max_scroll / (track_h - thumb_h);
375 node.scroll_y = (node.scrollbar_drag_start_scroll + delta_y * scroll_per_pixel)
376 .clamp(0.0, max_scroll);
377 return true;
378 }
379 false
380 }
381 _ => false,
382 }
383}
384
385pub fn try_start_scrollbar_drag(
388 node: &mut WidgetNode,
389 taffy: &TaffyTree,
390 cx: f32,
391 cy: f32,
392) -> bool {
393 try_start_scrollbar_drag_offset(node, taffy, cx, cy, 0.0, 0.0)
394}
395
396fn try_start_scrollbar_drag_offset(
397 node: &mut WidgetNode,
398 taffy: &TaffyTree,
399 cx: f32,
400 cy: f32,
401 parent_x: f32,
402 parent_y: f32,
403) -> bool {
404 let Some(node_id) = node.node else { return false; };
405 let layout = taffy.layout(node_id).expect("layout");
406 let abs_x = parent_x + layout.location.x;
407 let abs_y = parent_y + layout.location.y;
408
409 let child_y = abs_y - node.scroll_y;
410 for child in &mut node.children {
411 if try_start_scrollbar_drag_offset(child, taffy, cx, cy, abs_x, child_y) {
412 return true;
413 }
414 }
415
416 if !node.widget.is_scrollable() {
417 return false;
418 }
419
420 let container_h = layout.size.height;
421 let content_h = content_height(node, taffy);
422 if content_h <= container_h {
423 return false;
424 }
425
426 let track_x = abs_x + layout.size.width - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN;
427 let track_y = abs_y + SCROLLBAR_MARGIN;
428 let track_h = container_h - SCROLLBAR_MARGIN * 2.0;
429
430 let in_scrollbar = cx >= track_x
432 && cx <= track_x + SCROLLBAR_WIDTH + SCROLLBAR_MARGIN
433 && cy >= abs_y
434 && cy <= abs_y + container_h;
435
436 if !in_scrollbar {
437 return false;
438 }
439
440 let max_scroll = (content_h - container_h).max(0.0);
441 let ratio = container_h / content_h;
442 let thumb_h = (ratio * track_h).max(SCROLLBAR_MIN_THUMB);
443 let scroll_ratio = if max_scroll > 0.0 { node.scroll_y / max_scroll } else { 0.0 };
444 let thumb_y = track_y + scroll_ratio * (track_h - thumb_h);
445
446 if cy >= thumb_y && cy <= thumb_y + thumb_h {
448 node.scrollbar_dragging = true;
450 node.scrollbar_drag_start_y = cy;
451 node.scrollbar_drag_start_scroll = node.scroll_y;
452 } else {
453 let click_ratio = (cy - track_y) / track_h;
455 node.scroll_y = (click_ratio * max_scroll).clamp(0.0, max_scroll);
456 }
457
458 true
459}
460
461pub fn release_scrollbar_drag(node: &mut WidgetNode) {
463 node.scrollbar_dragging = false;
464 for child in &mut node.children {
465 release_scrollbar_drag(child);
466 }
467}
468
469pub fn clear_active_widgets(node: &mut WidgetNode) {
470 node.widget.clear_active();
471 for child in &mut node.children {
472 clear_active_widgets(child);
473 }
474}