1use crate::text;
2use crate::widget::{Action, Dock, HAlign, SLOT_SIZE, Units, VAlign, Widget, WidgetKind};
3
4mod grid;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum FlexDir {
8 Row,
9 Column,
10}
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum Align {
13 Start,
14 Center,
15 End,
16}
17
18#[derive(Debug, Clone, Copy, Default)]
19pub struct Rect {
20 pub x: f32,
21 pub y: f32,
22 pub w: f32,
23 pub h: f32,
24}
25
26pub const SCROLLBAR_W: f32 = 6.0;
29pub const MIN_KNOB_H: f32 = 16.0;
31
32impl Rect {
33 pub fn inset(&self, pad: [f32; 4]) -> Rect {
35 Rect {
36 x: self.x + pad[3],
37 y: self.y + pad[0],
38 w: (self.w - pad[1] - pad[3]).max(0.0),
39 h: (self.h - pad[0] - pad[2]).max(0.0),
40 }
41 }
42
43 pub fn contains(&self, px: f32, py: f32) -> bool {
45 px >= self.x && py >= self.y && px <= self.x + self.w && py <= self.y + self.h
46 }
47
48 pub fn intersect(&self, o: &Rect) -> Option<Rect> {
50 let x0 = self.x.max(o.x);
51 let y0 = self.y.max(o.y);
52 let x1 = (self.x + self.w).min(o.x + o.w);
53 let y1 = (self.y + self.h).min(o.y + o.h);
54 (x1 > x0 && y1 > y0).then_some(Rect {
55 x: x0,
56 y: y0,
57 w: x1 - x0,
58 h: y1 - y0,
59 })
60 }
61}
62
63#[derive(Debug, Clone)]
64pub struct LayoutNode {
65 pub rect: Rect,
66 pub id: Option<String>,
67 pub on_click: Option<String>,
68 pub children: Vec<LayoutNode>,
69 pub enabled: bool,
70 pub focused: bool,
71 pub content_h: f32,
74 pub scroll_y: f32,
78 pub clip: bool,
80 pub scrollbar: bool,
82 pub action: Option<Action>,
84 pub visible: bool,
87 pub grid: Option<Box<GridLines>>,
89}
90
91#[derive(Debug, Clone, Default, PartialEq)]
94pub struct GridLines {
95 pub rows: Vec<(f32, f32)>,
97 pub cols: Vec<(f32, f32)>,
99 pub(crate) floor_rows: Vec<f32>,
103 pub(crate) floor_cols: Vec<f32>,
104}
105
106impl LayoutNode {
107 pub fn max_scroll(&self) -> f32 {
109 (self.content_h - self.rect.h).max(0.0)
110 }
111
112 pub fn scrollbar_rects(&self) -> Option<(Rect, Rect)> {
115 let max = self.max_scroll();
116 if !self.scrollbar || max <= 0.0 {
117 return None;
118 }
119 let r = self.rect;
120 let track = Rect {
121 x: r.x + r.w - SCROLLBAR_W,
122 y: r.y,
123 w: SCROLLBAR_W,
124 h: r.h,
125 };
126 let knob_h = (r.h * r.h / self.content_h).max(MIN_KNOB_H).min(r.h);
127 let knob = Rect {
128 y: r.y + (r.h - knob_h) * (self.scroll_y / max),
129 h: knob_h,
130 ..track
131 };
132 Some((track, knob))
133 }
134
135 fn translate_y(&mut self, dy: f32) {
136 self.rect.y += dy;
137 if let Some(g) = &mut self.grid {
138 for r in &mut g.rows {
139 r.0 += dy;
140 }
141 }
142 for c in &mut self.children {
143 c.translate_y(dy);
144 }
145 }
146}
147
148impl Default for LayoutNode {
149 fn default() -> Self {
150 Self {
151 rect: Rect::default(),
152 id: None,
153 on_click: None,
154 children: Vec::new(),
155 enabled: true,
156 focused: false,
157 content_h: 0.0,
158 scroll_y: 0.0,
159 clip: false,
160 scrollbar: false,
161 action: None,
162 visible: true,
163 grid: None,
164 }
165 }
166}
167
168pub fn compute(widget: &Widget, avail_w: f32, avail_h: f32) -> LayoutNode {
185 let mut node = new_node(widget);
186 if widget.visible {
187 layout_widget(
188 widget,
189 &mut node,
190 0.0,
191 0.0,
192 avail_w,
193 avail_h,
194 avail_w,
195 avail_h,
196 Depth::Full,
197 );
198 }
199 node
200}
201
202const UNBOUNDED: f32 = f32::MAX;
205
206const NO_WRAP_W: f32 = 4096.0;
209const TEXT_BOX_W: f32 = 100.0;
211const TEXT_BOX_CHROME_H: f32 = 7.0;
213
214fn bounded(v: f32) -> bool {
215 v < UNBOUNDED / 2.0
216}
217
218fn resolve_size(u: Units, basis: f32) -> Option<f32> {
220 match u {
221 Units::Percent(_) if !bounded(basis) => None,
222 _ => u.resolve(basis),
223 }
224}
225
226fn new_node(w: &Widget) -> LayoutNode {
227 LayoutNode {
228 id: w.id.clone(),
229 on_click: w.on_click.clone(),
230 enabled: w.enabled,
231 focused: w.focused,
232 action: w.action,
233 visible: w.visible,
234 ..Default::default()
235 }
236}
237
238#[derive(Clone, Copy, PartialEq)]
240enum Depth {
241 Size,
246 Full,
248}
249
250fn measure(w: &Widget, node: &mut LayoutNode, max_w: f32, max_h: f32, basis_w: f32, basis_h: f32) {
254 layout_widget(
255 w,
256 node,
257 0.0,
258 0.0,
259 max_w,
260 max_h,
261 basis_w,
262 basis_h,
263 Depth::Size,
264 );
265}
266
267#[allow(clippy::too_many_arguments)]
268fn layout_widget(
269 w: &Widget,
270 node: &mut LayoutNode,
271 x: f32,
272 y: f32,
273 max_w: f32,
274 max_h: f32,
275 basis_w: f32,
276 basis_h: f32,
277 depth: Depth,
278) {
279 let s = &w.style;
280 let has_children = is_container(w);
281 let explicit_w = resolve_size(s.w, basis_w).map(|v| v.min(max_w));
282 let explicit_h = resolve_size(s.h, basis_h).map(|v| v.min(max_h));
283
284 let mut ww = explicit_w.unwrap_or(max_w);
286 let mut hh = explicit_h.unwrap_or(max_h);
287
288 if !has_children {
289 let fit_w = |content: f32| (content + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
292 let fit_h = |content: f32| (content + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
293 match &w.kind {
294 WidgetKind::Label(t) | WidgetKind::Button(t) => {
295 let avail_w = (ww - s.pad[1] - s.pad[3]).max(0.0);
297 if avail_w < NO_WRAP_W && !s.no_wrap {
300 ww = fit_w(avail_w);
301 hh = fit_h(text::text_height_in(t, avail_w, s.font_scale, &s.font));
302 } else {
303 ww = fit_w(text::str_width_in(t, s.font_scale, &s.font));
304 hh = fit_h(text::LINE_H * s.font_scale);
305 }
306 }
307 WidgetKind::ItemSlot(_) | WidgetKind::InvSlot { .. } => {
308 ww = fit_w(SLOT_SIZE);
309 hh = fit_h(SLOT_SIZE);
310 }
311 WidgetKind::Spacer | WidgetKind::Surface(_) => {
312 ww = s.min_w.max(1.0).min(max_w);
313 hh = s.min_h.max(1.0).min(max_h);
314 }
315 WidgetKind::Panel(_) | WidgetKind::Grid(_) => {
318 if !bounded(ww) {
319 ww = s.min_w;
320 }
321 if !bounded(hh) {
322 hh = s.min_h;
323 }
324 }
325 WidgetKind::TextBox(_) => {
328 if !bounded(ww) {
329 ww = TEXT_BOX_W;
330 }
331 ww = ww.max(s.min_w).min(max_w);
332 hh = fit_h(text::LINE_H * s.font_scale + TEXT_BOX_CHROME_H);
333 }
334 WidgetKind::Image(image) => {
335 let (nw, nh) = crate::image::natural_size(image);
336 ww = fit_w(nw);
337 hh = fit_h(nh);
338 }
339 }
340 if let Some(v) = explicit_w {
342 ww = v;
343 }
344 if let Some(v) = explicit_h {
345 hh = v;
346 }
347 }
348
349 node.rect = Rect { x, y, w: ww, h: hh };
350 node.grid = None;
351 node.content_h = hh;
352 node.scroll_y = 0.0;
353 node.clip = s.clip;
354 node.scrollbar = s.scrollbar;
355
356 if has_children {
357 let shrink = Shrink {
358 w: explicit_w.is_none(),
359 h: explicit_h.is_none(),
360 max_w,
361 max_h,
362 };
363 layout_children(w, node, Some(shrink), depth);
364 }
365}
366
367fn arrange(w: &Widget, node: &mut LayoutNode, x: f32, y: f32) {
370 node.rect.x = x;
371 node.rect.y = y;
372 node.content_h = node.rect.h;
373 if is_container(w) {
374 layout_children(w, node, None, Depth::Full);
375 }
376}
377
378fn is_container(w: &Widget) -> bool {
380 !w.children.is_empty() || matches!(w.kind, WidgetKind::Grid(_))
381}
382
383struct Shrink {
386 w: bool,
387 h: bool,
388 max_w: f32,
389 max_h: f32,
390}
391
392fn place(child: &Widget, cn: &mut LayoutNode, x: f32, y: f32, depth: Depth) {
395 match depth {
396 Depth::Full => arrange(child, cn, x, y),
397 Depth::Size => {
398 cn.rect.x = x;
399 cn.rect.y = y;
400 }
401 }
402}
403
404fn layout_children(w: &Widget, node: &mut LayoutNode, shrink: Option<Shrink>, depth: Depth) {
407 if let WidgetKind::Grid(spec) = &w.kind {
408 return grid::layout(w, spec, node, shrink, depth);
409 }
410 let s = &w.style;
411 let Rect { x, y, w: ww, h: hh } = node.rect;
412
413 let dir = if matches!(w.kind, WidgetKind::Panel(_)) && w.flex_dir == FlexDir::Row {
414 FlexDir::Row
415 } else {
416 FlexDir::Column
417 };
418 let bar_w = if s.scrollbar { SCROLLBAR_W } else { 0.0 };
419 let content_w = if bounded(ww) {
420 (ww - s.pad[1] - s.pad[3] - bar_w).max(0.0)
421 } else {
422 UNBOUNDED
423 };
424 let content_h = if bounded(hh) {
425 (hh - s.pad[0] - s.pad[2]).max(0.0)
426 } else {
427 UNBOUNDED
428 };
429 let (main_bounded, cross_bounded) = match dir {
430 FlexDir::Row => (bounded(content_w), bounded(content_h)),
431 FlexDir::Column => (bounded(content_h), bounded(content_w)),
432 };
433 let cross_shrinks = shrink.as_ref().is_some_and(|f| match dir {
436 FlexDir::Row => f.h,
437 FlexDir::Column => f.w,
438 });
439 let self_place = |child: &Widget| -> Option<Option<Align>> {
443 match dir {
444 FlexDir::Column => child.style.halign.map(|a| match a {
445 HAlign::Left => Some(Align::Start),
446 HAlign::Center => Some(Align::Center),
447 HAlign::Right => Some(Align::End),
448 HAlign::Stretch => None,
449 }),
450 FlexDir::Row => child.style.valign.map(|a| match a {
451 VAlign::Top => Some(Align::Start),
452 VAlign::Center => Some(Align::Center),
453 VAlign::Bottom => Some(Align::End),
454 VAlign::Stretch => None,
455 }),
456 }
457 };
458 let cross_offset = |child: &Widget, content: f32, size: f32| -> f32 {
460 if !cross_bounded || cross_shrinks {
461 return 0.0;
462 }
463 match self_place(child).flatten().unwrap_or(s.align) {
464 Align::Center => (content - size) / 2.0,
465 Align::End => content - size,
466 Align::Start => 0.0,
467 }
468 };
469 let main_m = |c: &Widget| -> (f32, f32) {
471 let m = c.style.margin;
472 match dir {
473 FlexDir::Row => (m[3], m[1] + m[3]),
474 FlexDir::Column => (m[0], m[0] + m[2]),
475 }
476 };
477 let cross_m = |c: &Widget| -> (f32, f32) {
478 let m = c.style.margin;
479 match dir {
480 FlexDir::Row => (m[0], m[0] + m[2]),
481 FlexDir::Column => (m[3], m[1] + m[3]),
482 }
483 };
484
485 let effective_flex = |child: &Widget| -> f32 {
488 if child.style.dock == Dock::Fill {
489 child.style.flex.max(1.0)
490 } else {
491 child.style.flex
492 }
493 };
494 let in_flow = |child: &Widget| -> bool {
496 child.visible
497 && child.style.pos.is_none()
498 && !matches!(
499 (dir, child.style.dock),
500 (FlexDir::Row, Dock::Right) | (FlexDir::Column, Dock::Bottom)
501 )
502 };
503 let stretches = |child: &Widget| -> bool {
504 let dock = child.style.dock;
505 let auto_cross = match dir {
506 FlexDir::Row => child.style.h.is_auto(),
507 FlexDir::Column => child.style.w.is_auto(),
508 };
509 cross_bounded
510 && (match dir {
511 FlexDir::Row => matches!(dock, Dock::Fill | Dock::Left | Dock::Right),
512 FlexDir::Column => matches!(dock, Dock::Fill | Dock::Top | Dock::Bottom),
513 } || (auto_cross && self_place(child) == Some(None)))
514 };
515
516 let mut child_nodes: Vec<LayoutNode> = Vec::with_capacity(w.children.len());
518 let mut total_flex: f32 = 0.0;
519 let mut used_main: f32 = 0.0;
520
521 for child in &w.children {
522 let mut cn = new_node(child);
523 if !child.visible {
525 cn.rect = Rect {
526 x: x + s.pad[3],
527 y: y + s.pad[0],
528 w: 0.0,
529 h: 0.0,
530 };
531 child_nodes.push(cn);
532 continue;
533 }
534 let m = child.style.margin;
535 if let Some((px, py)) = child.style.pos {
541 let fixed = |u: Units| matches!(u, Units::Px(_));
542 let mw = if fixed(child.style.w) { UNBOUNDED } else { less(content_w, px + m[1] + m[3]) };
543 let mh = if fixed(child.style.h) { UNBOUNDED } else { less(content_h, py + m[0] + m[2]) };
544 measure(child, &mut cn, mw, mh, content_w, content_h);
545 child_nodes.push(cn);
546 continue;
547 }
548 let (cmw, cmh) = match (dir, child.style.dock) {
551 (_, Dock::Fill) => (content_w, content_h),
552 (FlexDir::Row, _) => (UNBOUNDED, content_h),
553 (FlexDir::Column, _) => (content_w, UNBOUNDED),
554 };
555 let (cmw, cmh) = (less(cmw, m[1] + m[3]), less(cmh, m[0] + m[2]));
556 if dir == FlexDir::Column
557 && child.style.dock != Dock::Fill
558 && matches!(self_place(child), Some(Some(_)))
559 {
560 measure_aligned(child, &mut cn, cmw, cmh, content_w, content_h);
561 } else {
562 measure(child, &mut cn, cmw, cmh, content_w, content_h);
563 }
564 let ef = effective_flex(child);
565 if ef > 0.0 && main_bounded && in_flow(child) {
570 match dir {
571 FlexDir::Row if child.style.w.is_auto() => cn.rect.w = child.style.min_w,
572 FlexDir::Column if child.style.h.is_auto() => cn.rect.h = child.style.min_h,
573 _ => {}
574 }
575 }
576 used_main += main_m(child).1
579 + match dir {
580 FlexDir::Row => cn.rect.w,
581 FlexDir::Column => cn.rect.h,
582 };
583 if in_flow(child) {
584 total_flex += ef;
585 }
586 child_nodes.push(cn);
587 }
588 let placed = w
589 .children
590 .iter()
591 .filter(|c| c.visible && c.style.pos.is_none())
592 .count();
593 used_main += s.gap * (placed.saturating_sub(1) as f32);
594
595 let available = if main_bounded {
597 (match dir {
598 FlexDir::Row => content_w,
599 FlexDir::Column => content_h,
600 }) - used_main
601 } else {
602 0.0
603 };
604 let mut pos = match dir {
605 FlexDir::Row => s.pad[3],
606 FlexDir::Column => s.pad[0],
607 };
608
609 for (i, child) in w.children.iter().enumerate() {
611 if !in_flow(child) {
612 continue;
613 }
614 let cn = &mut child_nodes[i];
615 let ef = effective_flex(child);
616 let grow = if ef > 0.0 && total_flex > 0.0 && available > 0.0 {
617 available * ef / total_flex
618 } else {
619 0.0
620 };
621 let stretch = stretches(child);
622 let (ml, mt) = main_m(child);
623 let (cl, ct) = cross_m(child);
624 let (cx, cy) = if dir == FlexDir::Row {
625 cn.rect.w += grow;
626 if stretch {
627 cn.rect.h = (content_h - ct).max(0.0);
628 }
629 let p = (
630 x + pos + ml,
631 y + s.pad[0] + cl + cross_offset(child, content_h, cn.rect.h + ct),
632 );
633 pos += cn.rect.w + mt + s.gap;
634 p
635 } else {
636 cn.rect.h += grow;
637 if stretch {
638 cn.rect.w = (content_w - ct).max(0.0);
639 }
640 let p = (
641 x + s.pad[3] + cl + cross_offset(child, content_w, cn.rect.w + ct),
642 y + pos + ml,
643 );
644 pos += cn.rect.h + mt + s.gap;
645 p
646 };
647 place(child, cn, cx, cy, depth);
648 }
649
650 let mut rpos = if main_bounded {
654 match dir {
655 FlexDir::Row => x + s.pad[3] + content_w,
656 FlexDir::Column => y + s.pad[0] + content_h,
657 }
658 } else {
659 let docked: Vec<f32> = w
660 .children
661 .iter()
662 .zip(&child_nodes)
663 .filter(|(c, _)| c.visible && !in_flow(c) && c.style.pos.is_none())
664 .map(|(c, n)| {
665 main_m(c).1
666 + match dir {
667 FlexDir::Row => n.rect.w,
668 FlexDir::Column => n.rect.h,
669 }
670 })
671 .collect();
672 let span: f32 =
673 docked.iter().sum::<f32>() + s.gap * (docked.len().saturating_sub(1) as f32);
674 (match dir {
675 FlexDir::Row => x,
676 FlexDir::Column => y,
677 }) + pos
678 + span
679 };
680 for (i, child) in w.children.iter().enumerate() {
681 if !child.visible || in_flow(child) || child.style.pos.is_some() {
682 continue;
683 }
684 let cn = &mut child_nodes[i];
685 let stretch = stretches(child);
686 let (ml, mt) = main_m(child);
687 let (cl, ct) = cross_m(child);
688 rpos -= mt - ml;
690 let (cx, cy) = if dir == FlexDir::Row {
691 if stretch {
692 cn.rect.h = (content_h - ct).max(0.0);
693 }
694 rpos -= cn.rect.w;
695 (
696 rpos,
697 y + s.pad[0] + cl + cross_offset(child, content_h, cn.rect.h + ct),
698 )
699 } else {
700 if stretch {
701 cn.rect.w = (content_w - ct).max(0.0);
702 }
703 rpos -= cn.rect.h;
704 (
705 x + s.pad[3] + cl + cross_offset(child, content_w, cn.rect.w + ct),
706 rpos,
707 )
708 };
709 rpos -= ml + s.gap;
710 place(child, cn, cx, cy, depth);
711 }
712
713 for (i, child) in w.children.iter().enumerate() {
715 let Some((px, py)) = child.style.pos else {
716 continue;
717 };
718 if !child.visible {
719 continue;
720 }
721 let m = child.style.margin;
722 let cn = &mut child_nodes[i];
723 place(
724 child,
725 cn,
726 x + s.pad[3] + px + m[3],
727 y + s.pad[0] + py + m[0],
728 depth,
729 );
730 }
731
732 finish(w, node, child_nodes, shrink, bar_w);
733}
734
735fn measure_aligned(
739 w: &Widget,
740 node: &mut LayoutNode,
741 max_w: f32,
742 max_h: f32,
743 basis_w: f32,
744 basis_h: f32,
745) {
746 if bounded(max_w) && w.style.w.is_auto() {
747 measure(w, node, UNBOUNDED, max_h, basis_w, basis_h);
748 if node.rect.w <= max_w {
749 return;
750 }
751 }
752 measure(w, node, max_w, max_h, basis_w, basis_h);
753}
754
755fn vis_margin(c: &Widget, side: usize) -> f32 {
757 if c.visible { c.style.margin[side] } else { 0.0 }
758}
759
760fn less(v: f32, by: f32) -> f32 {
762 if bounded(v) {
763 (v - by).max(0.0)
764 } else {
765 UNBOUNDED
766 }
767}
768
769fn finish(
772 w: &Widget,
773 node: &mut LayoutNode,
774 mut child_nodes: Vec<LayoutNode>,
775 shrink: Option<Shrink>,
776 bar_w: f32,
777) {
778 let s = &w.style;
779 let Rect { x, y, .. } = node.rect;
780 let extent = |f: &dyn Fn(&Widget, &LayoutNode) -> f32| -> f32 {
781 w.children
782 .iter()
783 .zip(&child_nodes)
784 .map(|(c, n)| f(c, n))
785 .fold(0.0f32, f32::max)
786 };
787 let ch = extent(&|c, n| n.rect.y - y + n.rect.h + vis_margin(c, 2));
788 if let Some(fit) = shrink {
789 if fit.w {
790 let cw = extent(&|c, n| n.rect.x - x + n.rect.w + vis_margin(c, 1));
791 node.rect.w = (cw + s.pad[1] + bar_w).max(s.min_w).min(fit.max_w);
792 }
793 if fit.h {
794 node.rect.h = (ch + s.pad[2]).max(s.min_h).min(fit.max_h);
795 }
796 }
797
798 node.content_h = ch + s.pad[2];
801 node.scroll_y = s.scroll_y.clamp(0.0, node.max_scroll());
802 if node.scroll_y > 0.0 {
803 for c in &mut child_nodes {
804 c.translate_y(-node.scroll_y);
805 }
806 }
807
808 node.children = child_nodes;
809}
810
811pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
815 if !node.visible || !node.rect.contains(mx, my) {
816 return None;
817 }
818 for child in node.children.iter().rev() {
819 if let Some(hit) = hit_test(child, mx, my) {
820 return Some(hit);
821 }
822 }
823 if node.is_clickable() {
824 Some(node)
825 } else {
826 None
827 }
828}
829
830pub fn hit_path(node: &LayoutNode, mx: f32, my: f32) -> Option<Vec<u16>> {
833 fn walk(node: &LayoutNode, mx: f32, my: f32, path: &mut Vec<u16>) -> bool {
834 if !node.visible || !node.rect.contains(mx, my) {
835 return false;
836 }
837 for (i, child) in node.children.iter().enumerate().rev() {
838 path.push(i as u16);
839 if walk(child, mx, my, path) {
840 return true;
841 }
842 path.pop();
843 }
844 node.is_clickable()
845 }
846 let mut path = Vec::new();
847 walk(node, mx, my, &mut path).then_some(path)
848}
849
850pub fn deepest_path(node: &LayoutNode, mx: f32, my: f32) -> Option<Vec<u16>> {
853 if !node.visible || !node.rect.contains(mx, my) {
854 return None;
855 }
856 let mut path = Vec::new();
857 let mut n = node;
858 'outer: loop {
859 for (i, child) in n.children.iter().enumerate().rev() {
860 if child.visible && child.rect.contains(mx, my) {
861 path.push(i as u16);
862 n = child;
863 continue 'outer;
864 }
865 }
866 return Some(path);
867 }
868}
869
870pub fn node_at<'a>(node: &'a LayoutNode, path: &[u16]) -> Option<&'a LayoutNode> {
872 let mut n = node;
873 for &i in path {
874 n = n.children.get(i as usize)?;
875 }
876 Some(n)
877}
878
879pub fn node_at_mut<'a>(node: &'a mut LayoutNode, path: &[u16]) -> Option<&'a mut LayoutNode> {
881 let mut n = node;
882 for &i in path {
883 n = n.children.get_mut(i as usize)?;
884 }
885 Some(n)
886}
887
888impl LayoutNode {
889 pub fn is_clickable(&self) -> bool {
892 self.visible && self.enabled && (self.action.is_some() || self.on_click.is_some())
893 }
894}
895
896pub fn find_by_id(node: &LayoutNode, id: &str) -> Option<Rect> {
901 if node.id.as_deref() == Some(id) {
902 return Some(node.rect);
903 }
904 node.children.iter().find_map(|c| find_by_id(c, id))
905}
906
907pub fn find_node<'a>(node: &'a LayoutNode, id: &str) -> Option<&'a LayoutNode> {
910 if node.id.as_deref() == Some(id) {
911 return Some(node);
912 }
913 node.children.iter().find_map(|c| find_node(c, id))
914}
915
916pub fn set_focus(node: &mut LayoutNode, focused_id: Option<&str>) {
918 node.focused = focused_id.is_some() && node.id.as_deref() == focused_id;
919 for child in &mut node.children {
920 set_focus(child, focused_id);
921 }
922}
923
924#[cfg(test)]
925mod tests {
926 use super::*;
927 use crate::widget::{self, Units};
928
929 #[test]
930 fn positioned_children_leave_the_flow() {
931 let root = widget::panel(FlexDir::Column)
932 .w(200.0)
933 .h(100.0)
934 .padding(4.0, 4.0, 4.0, 4.0)
935 .child(widget::spacer().min_w(10.0).min_h(10.0))
936 .child(widget::button("Hi").w(40.0).h(20.0).at(50.0, 30.0))
937 .child(widget::spacer().min_w(10.0).min_h(10.0));
938 let node = compute(&root, 400.0, 400.0);
939 let r = node.children[1].rect;
940 assert_eq!((r.x, r.y, r.w, r.h), (54.0, 34.0, 40.0, 20.0));
941 assert_eq!(node.children[0].rect.y, 4.0);
943 assert_eq!(node.children[2].rect.y, 4.0 + 10.0 + 2.0);
944 }
945
946 #[test]
947 fn positioned_container_lays_out_its_children_in_place() {
948 let root = widget::panel(FlexDir::Column)
949 .w(Units::Percent(100.0))
950 .h(Units::Percent(100.0))
951 .child(
952 widget::panel(FlexDir::Row)
953 .w(100.0)
954 .h(20.0)
955 .gap(0.0)
956 .at(10.0, 20.0)
957 .child(widget::spacer().min_w(30.0).min_h(5.0))
958 .child(widget::spacer().min_w(30.0).min_h(5.0)),
959 );
960 let node = compute(&root, 320.0, 240.0);
961 let panel = &node.children[0];
962 assert_eq!((panel.rect.x, panel.rect.y), (10.0, 20.0));
963 assert_eq!(panel.children[1].rect.x, 40.0);
964 assert_eq!(panel.children[1].rect.y, 20.0);
965 }
966
967 #[test]
968 fn auto_sized_parent_grows_to_fit_positioned_children() {
969 let root = widget::panel(FlexDir::Column)
970 .child(widget::spacer().min_w(10.0).min_h(10.0).at(30.0, 40.0));
971 let node = compute(&root, 300.0, 300.0);
972 assert_eq!((node.rect.w, node.rect.h), (40.0, 50.0));
973 }
974
975 fn pct(v: f32) -> Units {
976 Units::Percent(v)
977 }
978
979 fn r(n: &LayoutNode) -> (f32, f32, f32, f32) {
980 (n.rect.x, n.rect.y, n.rect.w, n.rect.h)
981 }
982
983 #[test]
984 fn percent_width_in_a_row_resolves_once_against_the_content_box() {
985 let root = widget::panel(FlexDir::Row)
986 .w(400.0)
987 .h(100.0)
988 .padding(5.0, 10.0, 5.0, 10.0)
989 .child(
990 widget::panel(FlexDir::Column)
991 .w(pct(50.0))
992 .child(widget::spacer().min_w(10.0).min_h(10.0)),
993 )
994 .child(widget::spacer().w(pct(25.0)).h(pct(50.0)));
995 let node = compute(&root, 800.0, 600.0);
996 assert_eq!(r(&node.children[0]).2, 190.0);
998 assert_eq!(r(&node.children[1]), (10.0 + 190.0 + 2.0, 5.0, 95.0, 45.0));
999 }
1000
1001 #[test]
1002 fn percent_sizes_in_a_column() {
1003 let root = widget::panel(FlexDir::Column)
1004 .w(200.0)
1005 .h(300.0)
1006 .padding(10.0, 0.0, 10.0, 0.0)
1007 .gap(0.0)
1008 .child(
1009 widget::panel(FlexDir::Row)
1010 .w(pct(50.0))
1011 .h(pct(50.0))
1012 .child(widget::spacer()),
1013 )
1014 .child(widget::spacer().w(pct(100.0)).h(pct(10.0)));
1015 let node = compute(&root, 800.0, 600.0);
1016 assert_eq!(r(&node.children[0]), (0.0, 10.0, 100.0, 140.0));
1017 assert_eq!(r(&node.children[1]), (0.0, 150.0, 200.0, 28.0));
1018 }
1019
1020 #[test]
1021 fn nested_percents_compound_per_level_only() {
1022 let root = widget::panel(FlexDir::Row).w(400.0).h(200.0).child(
1023 widget::panel(FlexDir::Column)
1024 .w(pct(50.0))
1025 .h(pct(100.0))
1026 .child(
1027 widget::panel(FlexDir::Row)
1028 .w(pct(50.0))
1029 .h(pct(50.0))
1030 .child(widget::spacer().w(pct(50.0)).h(pct(50.0))),
1031 ),
1032 );
1033 let node = compute(&root, 400.0, 200.0);
1034 let a = &node.children[0];
1035 let b = &a.children[0];
1036 let c = &b.children[0];
1037 assert_eq!(r(a), (0.0, 0.0, 200.0, 200.0));
1038 assert_eq!(r(b), (0.0, 0.0, 100.0, 100.0));
1039 assert_eq!(r(c), (0.0, 0.0, 50.0, 50.0));
1040 }
1041
1042 #[test]
1043 fn percent_resolves_against_the_scrollbar_free_box() {
1044 let root = widget::scroll_view()
1045 .w(106.0)
1046 .h(50.0)
1047 .child(widget::spacer().w(pct(50.0)).h(10.0));
1048 let node = compute(&root, 400.0, 400.0);
1049 assert_eq!(node.children[0].rect.w, 50.0);
1050 }
1051
1052 #[test]
1053 fn grow_shares_free_space_with_percent_siblings() {
1054 let root = widget::panel(FlexDir::Row)
1055 .w(300.0)
1056 .h(50.0)
1057 .gap(4.0)
1058 .child(widget::spacer().w(pct(50.0)))
1059 .child(
1060 widget::panel(FlexDir::Column)
1061 .flex(1.0)
1062 .child(widget::spacer()),
1063 )
1064 .child(widget::spacer().w(pct(10.0)));
1065 let node = compute(&root, 300.0, 50.0);
1066 assert_eq!(r(&node.children[0]).2, 150.0);
1068 assert_eq!(r(&node.children[1]), (154.0, 0.0, 112.0, 4.0));
1069 assert_eq!(r(&node.children[2]), (270.0, 0.0, 30.0, 4.0));
1070 }
1071
1072 #[test]
1073 fn grown_panel_keeps_its_size_and_lays_children_out_against_it() {
1074 let root = widget::panel(FlexDir::Column)
1075 .w(200.0)
1076 .h(200.0)
1077 .gap(0.0)
1078 .child(widget::spacer().min_h(20.0))
1079 .child(
1080 widget::panel(FlexDir::Row)
1081 .flex(1.0)
1082 .w(pct(100.0))
1083 .gap(0.0)
1084 .child(widget::spacer().w(pct(25.0)).h(pct(50.0)))
1085 .child(
1086 widget::panel(FlexDir::Column)
1087 .flex(1.0)
1088 .child(widget::spacer()),
1089 )
1090 .child(widget::spacer().dock(Dock::Right).min_w(10.0)),
1091 );
1092 let node = compute(&root, 400.0, 400.0);
1093 let grown = &node.children[1];
1094 assert_eq!(r(grown), (0.0, 20.0, 200.0, 180.0));
1095 assert_eq!(r(&grown.children[0]), (0.0, 20.0, 50.0, 90.0));
1096 assert_eq!(r(&grown.children[1]).2, 200.0 - 50.0 - 10.0);
1098 assert_eq!(r(&grown.children[2]), (190.0, 20.0, 10.0, 180.0));
1100 }
1101
1102 #[test]
1103 fn long_list_deep_inside_a_grown_row_does_not_push_siblings_out() {
1104 let mut list = widget::scroll_view().flex(1.0).w(pct(100.0));
1107 for _ in 0..60 {
1108 list = list.child(widget::spacer().h(25.0));
1109 }
1110 let root = widget::panel(FlexDir::Column)
1111 .w(400.0)
1112 .h(252.0)
1113 .gap(0.0)
1114 .child(widget::spacer().h(20.0))
1115 .child(
1116 widget::panel(FlexDir::Row).flex(1.0).w(pct(100.0)).child(
1117 widget::panel(FlexDir::Column)
1118 .w(pct(50.0))
1119 .h(pct(100.0))
1120 .child(widget::spacer().h(16.0))
1121 .child(list),
1122 ),
1123 )
1124 .child(widget::spacer().h(26.0));
1125 let node = compute(&root, 400.0, 252.0);
1126 assert_eq!(r(&node.children[1]), (0.0, 20.0, 400.0, 206.0));
1127 let footer = r(&node.children[2]);
1128 assert_eq!((footer.1, footer.3), (226.0, 26.0));
1129 let column = &node.children[1].children[0];
1130 assert_eq!(column.rect.h, 206.0);
1131 let list = &column.children[1];
1132 assert!(list.rect.y + list.rect.h <= 226.0, "{:?}", list.rect);
1133 assert!(list.content_h > list.rect.h);
1134 }
1135
1136 #[test]
1137 fn grown_column_panel_fills_its_parent_and_children_grow_too() {
1138 let root = widget::panel(FlexDir::Column)
1139 .w(100.0)
1140 .h(100.0)
1141 .gap(0.0)
1142 .child(
1143 widget::panel(FlexDir::Column)
1144 .flex(1.0)
1145 .gap(0.0)
1146 .child(widget::spacer().flex(1.0)),
1147 );
1148 let node = compute(&root, 100.0, 100.0);
1149 assert_eq!(node.children[0].rect.h, 100.0);
1150 assert_eq!(node.children[0].children[0].rect.h, 100.0);
1151 }
1152
1153 #[test]
1154 fn empty_auto_panel_in_a_row_takes_its_min_width() {
1155 let root = widget::panel(FlexDir::Row)
1156 .w(100.0)
1157 .h(20.0)
1158 .child(widget::panel(FlexDir::Column).min_w(7.0))
1159 .child(widget::spacer());
1160 let node = compute(&root, 100.0, 20.0);
1161 assert_eq!(node.children[0].rect.w, 7.0);
1162 assert_eq!(node.children[1].rect.x, 9.0);
1163 }
1164
1165 #[test]
1166 fn padding_counts_once_in_auto_size() {
1167 let root = widget::panel(FlexDir::Column)
1168 .padding(3.0, 4.0, 5.0, 6.0)
1169 .child(widget::spacer().min_w(10.0).min_h(10.0));
1170 let node = compute(&root, 300.0, 300.0);
1171 assert_eq!((node.rect.w, node.rect.h), (20.0, 18.0));
1172 assert_eq!(
1173 (node.children[0].rect.x, node.children[0].rect.y),
1174 (6.0, 3.0)
1175 );
1176 }
1177
1178 #[test]
1179 fn auto_row_in_a_column_measures_to_content() {
1180 let root = widget::panel(FlexDir::Column).w(200.0).h(200.0).child(
1182 widget::panel(FlexDir::Row)
1183 .align(Align::Center)
1184 .gap(0.0)
1185 .child(widget::spacer().min_w(10.0).min_h(10.0))
1186 .child(widget::spacer().min_w(10.0).min_h(20.0))
1187 .child(widget::spacer().min_w(5.0).dock(Dock::Right)),
1188 );
1189 let node = compute(&root, 200.0, 200.0);
1190 let row = &node.children[0];
1191 assert_eq!(row.rect.h, 20.0);
1192 assert_eq!(row.children[0].rect.y, 5.0);
1193 assert_eq!(row.children[1].rect.y, 0.0);
1194 }
1195
1196 #[test]
1197 fn positioned_auto_row_measures_to_content() {
1198 let pill = widget::panel(FlexDir::Row)
1201 .padding(1.0, 1.0, 1.0, 1.0)
1202 .child(
1203 widget::panel(FlexDir::Row)
1204 .align(Align::Center)
1205 .child(widget::spacer().min_w(10.0).min_h(9.0)),
1206 );
1207 let root = widget::panel(FlexDir::Column)
1208 .w(500.0)
1209 .h(500.0)
1210 .child(pill.at(3.0, 4.0));
1211 let node = compute(&root, 500.0, 500.0);
1212 let r = node.children[0].rect;
1213 assert_eq!((r.x, r.y, r.w, r.h), (3.0, 4.0, 12.0, 11.0));
1214 assert_eq!(node.children[0].children[0].children[0].rect.y, 5.0);
1215 }
1216
1217 #[test]
1218 fn row_with_right_dock_measured_on_unbounded_axis() {
1219 let root = widget::panel(FlexDir::Row).w(300.0).h(20.0).child(
1222 widget::panel(FlexDir::Row)
1223 .gap(1.0)
1224 .child(widget::spacer().min_w(10.0))
1225 .child(widget::spacer().min_w(5.0).dock(Dock::Right))
1226 .child(widget::spacer().min_w(7.0).dock(Dock::Right)),
1227 );
1228 let node = compute(&root, 300.0, 20.0);
1229 let inner = &node.children[0];
1230 assert_eq!(inner.rect.w, 10.0 + 1.0 + 7.0 + 1.0 + 5.0);
1231 assert_eq!(inner.children[1].rect.x, 19.0);
1232 assert_eq!(inner.children[2].rect.x, 11.0);
1233 }
1234
1235 #[test]
1236 fn align_center_and_end_in_both_directions() {
1237 let col = widget::panel(FlexDir::Column)
1238 .w(100.0)
1239 .h(50.0)
1240 .align(Align::Center)
1241 .child(widget::spacer().min_w(20.0).min_h(5.0));
1242 assert_eq!(compute(&col, 100.0, 50.0).children[0].rect.x, 40.0);
1243 let row = widget::panel(FlexDir::Row)
1244 .w(100.0)
1245 .h(50.0)
1246 .padding(10.0, 0.0, 0.0, 0.0)
1247 .align(Align::End)
1248 .child(widget::spacer().min_w(20.0).h(pct(20.0)));
1249 assert_eq!(compute(&row, 100.0, 50.0).children[0].rect.y, 10.0 + 32.0);
1251 }
1252
1253 #[test]
1254 fn positioned_children_with_percent_sizes() {
1255 let root = widget::panel(FlexDir::Column)
1256 .w(200.0)
1257 .h(100.0)
1258 .padding(10.0, 10.0, 10.0, 10.0)
1259 .child(widget::spacer().w(pct(50.0)).h(pct(50.0)).at(20.0, 10.0))
1260 .child(
1261 widget::panel(FlexDir::Row)
1262 .w(pct(100.0))
1263 .h(pct(25.0))
1264 .at(0.0, 60.0)
1265 .gap(0.0)
1266 .child(widget::spacer().w(pct(50.0)))
1267 .child(widget::spacer().flex(1.0)),
1268 );
1269 let node = compute(&root, 400.0, 400.0);
1270 assert_eq!(r(&node.children[0]), (30.0, 20.0, 90.0, 40.0));
1272 let p = &node.children[1];
1275 assert_eq!(r(p), (10.0, 70.0, 180.0, 20.0));
1276 assert_eq!(r(&p.children[0]), (10.0, 70.0, 90.0, 4.0));
1277 assert_eq!(r(&p.children[1]), (100.0, 70.0, 90.0, 4.0));
1278 }
1279
1280 #[test]
1281 fn growing_scroll_view_fills_free_space_and_scrolls_the_rest() {
1282 let list = widget::scroll_view()
1283 .id("list")
1284 .flex(1.0)
1285 .w(pct(100.0))
1286 .gap(0.0)
1287 .padding(2.0, 2.0, 2.0, 2.0)
1288 .scroll_y(1000.0)
1289 .children((0..10).map(|_| widget::spacer().w(pct(100.0)).min_h(20.0)));
1290 let root = widget::panel(FlexDir::Column)
1291 .w(100.0)
1292 .h(120.0)
1293 .gap(0.0)
1294 .child(widget::spacer().min_h(20.0))
1295 .child(list);
1296 let node = compute(&root, 100.0, 120.0);
1297 let v = find_node(&node, "list").unwrap();
1298 assert_eq!(r(v), (0.0, 20.0, 100.0, 100.0));
1299 assert_eq!(v.content_h, 204.0);
1300 assert_eq!(v.scroll_y, 104.0);
1301 assert_eq!(v.children[0].rect.w, 100.0 - 4.0 - SCROLLBAR_W);
1303 let last = v.children[9].rect;
1305 assert_eq!(last.y + last.h, 20.0 + 100.0 - 2.0);
1306 assert!(v.scrollbar_rects().is_some());
1307 }
1308
1309 #[test]
1310 fn label_wraps_at_its_explicit_width() {
1311 let root = widget::panel(FlexDir::Column)
1312 .w(300.0)
1313 .child(widget::label("aaaa bbbb cccc").w(40.0));
1314 let node = compute(&root, 300.0, 300.0);
1315 let l = node.children[0].rect;
1316 assert_eq!(l.w, 40.0);
1317 assert!(l.h > text::LINE_H, "wrapped onto several lines: {l:?}");
1318 }
1319
1320 #[test]
1321 fn percent_caps_at_the_cross_axis_and_ignores_unbounded_basis() {
1322 let root = widget::panel(FlexDir::Column)
1324 .w(100.0)
1325 .h(100.0)
1326 .child(widget::spacer().w(pct(150.0)));
1327 assert_eq!(compute(&root, 100.0, 100.0).children[0].rect.w, 100.0);
1328 let root = widget::panel(FlexDir::Row).w(200.0).h(20.0).child(
1331 widget::panel(FlexDir::Row)
1332 .gap(0.0)
1333 .child(widget::spacer().min_w(40.0))
1334 .child(widget::spacer().w(pct(50.0))),
1335 );
1336 let node = compute(&root, 200.0, 20.0);
1337 let inner = &node.children[0];
1338 assert_eq!(inner.rect.w, 44.0);
1339 assert_eq!(inner.children[1].rect.w, 22.0);
1340 }
1341
1342 #[test]
1343 fn find_mut_and_set_text() {
1344 let mut root = widget::panel(FlexDir::Column).child(widget::label("a").id("title"));
1345 assert!(root.find_mut("title").unwrap().set_text("b"));
1346 match &root.children[0].kind {
1347 crate::widget::WidgetKind::Label(t) => assert_eq!(t, "b"),
1348 _ => unreachable!(),
1349 }
1350 assert!(root.find_mut("missing").is_none());
1351 }
1352}
1353#[cfg(test)]
1354mod bigger_than_the_window {
1355 use crate::{FlexDir, Units, widget};
1356
1357 #[test]
1358 fn a_screen_bigger_than_the_window_keeps_its_size_and_is_cut() {
1359 let content = widget::panel(FlexDir::Column)
1360 .w(Units::Px(300.0))
1361 .h(Units::Px(200.0))
1362 .child(widget::spacer().w(Units::Percent(100.0)).h(Units::Px(40.0)))
1363 .at(-50.0, -25.0);
1364 let screen = widget::panel(FlexDir::Column).w(Units::Px(200.0)).h(Units::Px(150.0)).child(content);
1365 let l = super::compute(&screen, 200.0, 150.0);
1366 let c = &l.children[0];
1367 assert_eq!((c.rect.x, c.rect.y, c.rect.w, c.rect.h), (-50.0, -25.0, 300.0, 200.0));
1368 assert_eq!(c.children[0].rect.w, 300.0, "percent of its own size, not the window's");
1369 }
1370}