1use crate::text;
2use crate::widget::{Action, Dock, Units, Widget, WidgetKind};
3
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub enum FlexDir {
6 Row,
7 Column,
8}
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum Align {
11 Start,
12 Center,
13 End,
14}
15
16#[derive(Debug, Clone, Copy, Default)]
17pub struct Rect {
18 pub x: f32,
19 pub y: f32,
20 pub w: f32,
21 pub h: f32,
22}
23
24pub const SCROLLBAR_W: f32 = 6.0;
27pub const MIN_KNOB_H: f32 = 16.0;
29
30impl Rect {
31 pub fn contains(&self, px: f32, py: f32) -> bool {
33 px >= self.x && py >= self.y && px <= self.x + self.w && py <= self.y + self.h
34 }
35
36 pub fn intersect(&self, o: &Rect) -> Option<Rect> {
38 let x0 = self.x.max(o.x);
39 let y0 = self.y.max(o.y);
40 let x1 = (self.x + self.w).min(o.x + o.w);
41 let y1 = (self.y + self.h).min(o.y + o.h);
42 (x1 > x0 && y1 > y0).then_some(Rect {
43 x: x0,
44 y: y0,
45 w: x1 - x0,
46 h: y1 - y0,
47 })
48 }
49}
50
51#[derive(Debug, Clone)]
52pub struct LayoutNode {
53 pub rect: Rect,
54 pub id: Option<String>,
55 pub on_click: Option<String>,
56 pub children: Vec<LayoutNode>,
57 pub enabled: bool,
58 pub focused: bool,
59 pub content_h: f32,
62 pub scroll_y: f32,
66 pub clip: bool,
68 pub scrollbar: bool,
70 pub action: Option<Action>,
72 pub visible: bool,
75}
76
77impl LayoutNode {
78 pub fn max_scroll(&self) -> f32 {
80 (self.content_h - self.rect.h).max(0.0)
81 }
82
83 pub fn scrollbar_rects(&self) -> Option<(Rect, Rect)> {
86 let max = self.max_scroll();
87 if !self.scrollbar || max <= 0.0 {
88 return None;
89 }
90 let r = self.rect;
91 let track = Rect {
92 x: r.x + r.w - SCROLLBAR_W,
93 y: r.y,
94 w: SCROLLBAR_W,
95 h: r.h,
96 };
97 let knob_h = (r.h * r.h / self.content_h).max(MIN_KNOB_H).min(r.h);
98 let knob = Rect {
99 y: r.y + (r.h - knob_h) * (self.scroll_y / max),
100 h: knob_h,
101 ..track
102 };
103 Some((track, knob))
104 }
105
106 fn translate_y(&mut self, dy: f32) {
107 self.rect.y += dy;
108 for c in &mut self.children {
109 c.translate_y(dy);
110 }
111 }
112}
113
114impl Default for LayoutNode {
115 fn default() -> Self {
116 Self {
117 rect: Rect::default(),
118 id: None,
119 on_click: None,
120 children: Vec::new(),
121 enabled: true,
122 focused: false,
123 content_h: 0.0,
124 scroll_y: 0.0,
125 clip: false,
126 scrollbar: false,
127 action: None,
128 visible: true,
129 }
130 }
131}
132
133pub fn compute(widget: &Widget, avail_w: f32, avail_h: f32) -> LayoutNode {
150 let mut node = new_node(widget);
151 if widget.visible {
152 layout_widget(widget, &mut node, 0.0, 0.0, avail_w, avail_h, avail_w, avail_h);
153 }
154 node
155}
156
157const UNBOUNDED: f32 = f32::MAX;
160
161fn bounded(v: f32) -> bool {
162 v < UNBOUNDED / 2.0
163}
164
165fn resolve_size(u: Units, basis: f32) -> Option<f32> {
167 match u {
168 Units::Percent(_) if !bounded(basis) => None,
169 _ => u.resolve(basis),
170 }
171}
172
173fn new_node(w: &Widget) -> LayoutNode {
174 LayoutNode {
175 id: w.id.clone(),
176 on_click: w.on_click.clone(),
177 enabled: w.enabled,
178 focused: w.focused,
179 action: w.action,
180 visible: w.visible,
181 ..Default::default()
182 }
183}
184
185#[allow(clippy::too_many_arguments)]
189fn layout_widget(
190 w: &Widget,
191 node: &mut LayoutNode,
192 x: f32,
193 y: f32,
194 max_w: f32,
195 max_h: f32,
196 basis_w: f32,
197 basis_h: f32,
198) {
199 let s = &w.style;
200 let has_children = !w.children.is_empty();
201 let explicit_w = resolve_size(s.w, basis_w).map(|v| v.min(max_w));
202 let explicit_h = resolve_size(s.h, basis_h).map(|v| v.min(max_h));
203
204 let mut ww = explicit_w.unwrap_or(max_w);
206 let mut hh = explicit_h.unwrap_or(max_h);
207
208 if !has_children {
209 match &w.kind {
211 WidgetKind::Label(t) | WidgetKind::Button(t) => {
212 let box_w = explicit_w.unwrap_or(max_w);
214 let avail_w = (box_w - s.pad[1] - s.pad[3]).max(0.0);
215 if avail_w < 4096.0 && !s.no_wrap {
218 ww = (avail_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
219 hh = (text::text_height_in(t, avail_w, s.font_scale, &s.font)
220 + s.pad[0]
221 + s.pad[2])
222 .max(s.min_h)
223 .min(max_h);
224 } else {
225 let tw = text::str_width_in(t, s.font_scale, &s.font);
226 ww = (tw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
227 hh = (text::LINE_H * s.font_scale + s.pad[0] + s.pad[2])
228 .max(s.min_h)
229 .min(max_h);
230 }
231 }
232 WidgetKind::ItemSlot(_) | WidgetKind::InvSlot { .. } => {
233 ww = (18.0 + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
234 hh = (18.0 + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
235 }
236 WidgetKind::Spacer => {
237 ww = s.min_w.max(1.0).min(max_w);
238 hh = s.min_h.max(1.0).min(max_h);
239 }
240 WidgetKind::Panel(_) => {
243 if !bounded(ww) {
244 ww = s.min_w;
245 }
246 if !bounded(hh) {
247 hh = s.min_h;
248 }
249 }
250 WidgetKind::TextBox(_) => {
253 if !bounded(ww) {
254 ww = 100.0;
255 }
256 ww = ww.max(s.min_w).min(max_w);
257 hh = (text::LINE_H * s.font_scale + 7.0 + s.pad[0] + s.pad[2])
258 .max(s.min_h)
259 .min(max_h);
260 }
261 WidgetKind::Image(image) => {
262 let (nw, nh) = crate::image::natural_size(image);
263 ww = (nw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
264 hh = (nh + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
265 }
266 }
267 if let Some(v) = explicit_w {
269 ww = v;
270 }
271 if let Some(v) = explicit_h {
272 hh = v;
273 }
274 }
275
276 node.rect = Rect { x, y, w: ww, h: hh };
277 node.content_h = hh;
278 node.scroll_y = 0.0;
279 node.clip = s.clip;
280 node.scrollbar = s.scrollbar;
281
282 if has_children {
283 let shrink = Shrink {
284 w: explicit_w.is_none(),
285 h: explicit_h.is_none(),
286 max_w,
287 max_h,
288 };
289 layout_children(w, node, Some(shrink));
290 }
291}
292
293fn arrange(w: &Widget, node: &mut LayoutNode, x: f32, y: f32) {
296 node.rect.x = x;
297 node.rect.y = y;
298 node.content_h = node.rect.h;
299 if !w.children.is_empty() {
300 layout_children(w, node, None);
301 }
302}
303
304struct Shrink {
307 w: bool,
308 h: bool,
309 max_w: f32,
310 max_h: f32,
311}
312
313fn layout_children(w: &Widget, node: &mut LayoutNode, shrink: Option<Shrink>) {
316 let s = &w.style;
317 let Rect {
318 x,
319 y,
320 w: ww,
321 h: hh,
322 } = node.rect;
323
324 let dir = if matches!(w.kind, WidgetKind::Panel(_)) && w.flex_dir == FlexDir::Row {
325 FlexDir::Row
326 } else {
327 FlexDir::Column
328 };
329 let bar_w = if s.scrollbar { SCROLLBAR_W } else { 0.0 };
330 let content_w = if bounded(ww) {
331 (ww - s.pad[1] - s.pad[3] - bar_w).max(0.0)
332 } else {
333 UNBOUNDED
334 };
335 let content_h = if bounded(hh) {
336 (hh - s.pad[0] - s.pad[2]).max(0.0)
337 } else {
338 UNBOUNDED
339 };
340 let (main_bounded, cross_bounded) = match dir {
341 FlexDir::Row => (bounded(content_w), bounded(content_h)),
342 FlexDir::Column => (bounded(content_h), bounded(content_w)),
343 };
344 let cross_shrinks = shrink.as_ref().is_some_and(|f| match dir {
347 FlexDir::Row => f.h,
348 FlexDir::Column => f.w,
349 });
350 let cross_offset = |content: f32, size: f32| -> f32 {
351 if !cross_bounded || cross_shrinks {
352 return 0.0;
353 }
354 match s.align {
355 Align::Center => (content - size) / 2.0,
356 Align::End => content - size,
357 Align::Start => 0.0,
358 }
359 };
360
361 let effective_flex = |child: &Widget| -> f32 {
364 if child.style.dock == Dock::Fill {
365 child.style.flex.max(1.0)
366 } else {
367 child.style.flex
368 }
369 };
370 let in_flow = |child: &Widget| -> bool {
372 child.visible
373 && child.style.pos.is_none()
374 && !matches!(
375 (dir, child.style.dock),
376 (FlexDir::Row, Dock::Right) | (FlexDir::Column, Dock::Bottom)
377 )
378 };
379 let stretches = |dock: Dock| -> bool {
380 cross_bounded
381 && match dir {
382 FlexDir::Row => matches!(dock, Dock::Fill | Dock::Left | Dock::Right),
383 FlexDir::Column => matches!(dock, Dock::Fill | Dock::Top | Dock::Bottom),
384 }
385 };
386
387 let mut child_nodes: Vec<LayoutNode> = Vec::with_capacity(w.children.len());
389 let mut total_flex: f32 = 0.0;
390 let mut used_main: f32 = 0.0;
391
392 for child in &w.children {
393 let mut cn = new_node(child);
394 if !child.visible {
396 cn.rect = Rect {
397 x: x + s.pad[3],
398 y: y + s.pad[0],
399 w: 0.0,
400 h: 0.0,
401 };
402 child_nodes.push(cn);
403 continue;
404 }
405 if let Some((px, py)) = child.style.pos {
409 let mw = if bounded(content_w) {
410 (content_w - px).max(0.0)
411 } else {
412 UNBOUNDED
413 };
414 let mh = if bounded(content_h) {
415 (content_h - py).max(0.0)
416 } else {
417 UNBOUNDED
418 };
419 layout_widget(child, &mut cn, 0.0, 0.0, mw, mh, content_w, content_h);
420 child_nodes.push(cn);
421 continue;
422 }
423 let (cmw, cmh) = match (dir, child.style.dock) {
426 (_, Dock::Fill) => (content_w, content_h),
427 (FlexDir::Row, _) => (UNBOUNDED, content_h),
428 (FlexDir::Column, _) => (content_w, UNBOUNDED),
429 };
430 layout_widget(child, &mut cn, 0.0, 0.0, cmw, cmh, content_w, content_h);
431 let ef = effective_flex(child);
432 if ef > 0.0 && main_bounded && in_flow(child) {
437 match dir {
438 FlexDir::Row if child.style.w.is_auto() => cn.rect.w = child.style.min_w,
439 FlexDir::Column if child.style.h.is_auto() => cn.rect.h = child.style.min_h,
440 _ => {}
441 }
442 }
443 used_main += match dir {
446 FlexDir::Row => cn.rect.w,
447 FlexDir::Column => cn.rect.h,
448 };
449 if in_flow(child) {
450 total_flex += ef;
451 }
452 child_nodes.push(cn);
453 }
454 let placed = w
455 .children
456 .iter()
457 .filter(|c| c.visible && c.style.pos.is_none())
458 .count();
459 used_main += s.gap * (placed.saturating_sub(1) as f32);
460
461 let available = if main_bounded {
463 (match dir {
464 FlexDir::Row => content_w,
465 FlexDir::Column => content_h,
466 }) - used_main
467 } else {
468 0.0
469 };
470 let mut pos = match dir {
471 FlexDir::Row => s.pad[3],
472 FlexDir::Column => s.pad[0],
473 };
474
475 for (i, child) in w.children.iter().enumerate() {
477 if !in_flow(child) {
478 continue;
479 }
480 let cn = &mut child_nodes[i];
481 let ef = effective_flex(child);
482 let grow = if ef > 0.0 && total_flex > 0.0 && available > 0.0 {
483 available * ef / total_flex
484 } else {
485 0.0
486 };
487 let stretch = stretches(child.style.dock);
488 let (cx, cy) = if dir == FlexDir::Row {
489 cn.rect.w += grow;
490 if stretch {
491 cn.rect.h = content_h;
492 }
493 let p = (x + pos, y + s.pad[0] + cross_offset(content_h, cn.rect.h));
494 pos += cn.rect.w + s.gap;
495 p
496 } else {
497 cn.rect.h += grow;
498 if stretch {
499 cn.rect.w = content_w;
500 }
501 let p = (x + s.pad[3] + cross_offset(content_w, cn.rect.w), y + pos);
502 pos += cn.rect.h + s.gap;
503 p
504 };
505 arrange(child, cn, cx, cy);
506 }
507
508 let mut rpos = if main_bounded {
512 match dir {
513 FlexDir::Row => x + s.pad[3] + content_w,
514 FlexDir::Column => y + s.pad[0] + content_h,
515 }
516 } else {
517 let docked: Vec<f32> = w
518 .children
519 .iter()
520 .zip(&child_nodes)
521 .filter(|(c, _)| c.visible && !in_flow(c) && c.style.pos.is_none())
522 .map(|(_, n)| match dir {
523 FlexDir::Row => n.rect.w,
524 FlexDir::Column => n.rect.h,
525 })
526 .collect();
527 let span: f32 =
528 docked.iter().sum::<f32>() + s.gap * (docked.len().saturating_sub(1) as f32);
529 (match dir {
530 FlexDir::Row => x,
531 FlexDir::Column => y,
532 }) + pos
533 + span
534 };
535 for (i, child) in w.children.iter().enumerate() {
536 if !child.visible || in_flow(child) || child.style.pos.is_some() {
537 continue;
538 }
539 let cn = &mut child_nodes[i];
540 let stretch = stretches(child.style.dock);
541 let (cx, cy) = if dir == FlexDir::Row {
542 if stretch {
543 cn.rect.h = content_h;
544 }
545 rpos -= cn.rect.w;
546 (rpos, y + s.pad[0] + cross_offset(content_h, cn.rect.h))
547 } else {
548 if stretch {
549 cn.rect.w = content_w;
550 }
551 rpos -= cn.rect.h;
552 (x + s.pad[3] + cross_offset(content_w, cn.rect.w), rpos)
553 };
554 rpos -= s.gap;
555 arrange(child, cn, cx, cy);
556 }
557
558 for (i, child) in w.children.iter().enumerate() {
560 let Some((px, py)) = child.style.pos else {
561 continue;
562 };
563 if !child.visible {
564 continue;
565 }
566 let cn = &mut child_nodes[i];
567 arrange(child, cn, x + s.pad[3] + px, y + s.pad[0] + py);
568 }
569
570 let ch: f32 = child_nodes
572 .iter()
573 .map(|c| c.rect.y - y + c.rect.h)
574 .fold(0.0f32, f32::max);
575 if let Some(fit) = shrink {
576 if fit.w {
577 let cw: f32 = child_nodes
578 .iter()
579 .map(|c| c.rect.x - x + c.rect.w)
580 .fold(0.0f32, f32::max);
581 node.rect.w = (cw + s.pad[1] + bar_w).max(s.min_w).min(fit.max_w);
582 }
583 if fit.h {
584 node.rect.h = (ch + s.pad[2]).max(s.min_h).min(fit.max_h);
585 }
586 }
587
588 node.content_h = ch + s.pad[2];
591 node.scroll_y = s.scroll_y.clamp(0.0, node.max_scroll());
592 if node.scroll_y > 0.0 {
593 for c in &mut child_nodes {
594 c.translate_y(-node.scroll_y);
595 }
596 }
597
598 node.children = child_nodes;
599}
600
601pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
605 if !node.visible || !node.rect.contains(mx, my) {
606 return None;
607 }
608 for child in node.children.iter().rev() {
609 if let Some(hit) = hit_test(child, mx, my) {
610 return Some(hit);
611 }
612 }
613 if node.is_clickable() {
614 Some(node)
615 } else {
616 None
617 }
618}
619
620pub fn hit_path(node: &LayoutNode, mx: f32, my: f32) -> Option<Vec<u16>> {
623 fn walk(node: &LayoutNode, mx: f32, my: f32, path: &mut Vec<u16>) -> bool {
624 if !node.visible || !node.rect.contains(mx, my) {
625 return false;
626 }
627 for (i, child) in node.children.iter().enumerate().rev() {
628 path.push(i as u16);
629 if walk(child, mx, my, path) {
630 return true;
631 }
632 path.pop();
633 }
634 node.is_clickable()
635 }
636 let mut path = Vec::new();
637 walk(node, mx, my, &mut path).then_some(path)
638}
639
640pub fn deepest_path(node: &LayoutNode, mx: f32, my: f32) -> Option<Vec<u16>> {
643 if !node.visible || !node.rect.contains(mx, my) {
644 return None;
645 }
646 let mut path = Vec::new();
647 let mut n = node;
648 'outer: loop {
649 for (i, child) in n.children.iter().enumerate().rev() {
650 if child.visible && child.rect.contains(mx, my) {
651 path.push(i as u16);
652 n = child;
653 continue 'outer;
654 }
655 }
656 return Some(path);
657 }
658}
659
660pub fn node_at<'a>(node: &'a LayoutNode, path: &[u16]) -> Option<&'a LayoutNode> {
662 let mut n = node;
663 for &i in path {
664 n = n.children.get(i as usize)?;
665 }
666 Some(n)
667}
668
669pub fn node_at_mut<'a>(node: &'a mut LayoutNode, path: &[u16]) -> Option<&'a mut LayoutNode> {
671 let mut n = node;
672 for &i in path {
673 n = n.children.get_mut(i as usize)?;
674 }
675 Some(n)
676}
677
678impl LayoutNode {
679 pub fn is_clickable(&self) -> bool {
682 self.visible && self.enabled && (self.action.is_some() || self.on_click.is_some())
683 }
684}
685
686pub fn find_by_id(node: &LayoutNode, id: &str) -> Option<Rect> {
691 if node.id.as_deref() == Some(id) {
692 return Some(node.rect);
693 }
694 node.children.iter().find_map(|c| find_by_id(c, id))
695}
696
697pub fn find_node<'a>(node: &'a LayoutNode, id: &str) -> Option<&'a LayoutNode> {
700 if node.id.as_deref() == Some(id) {
701 return Some(node);
702 }
703 node.children.iter().find_map(|c| find_node(c, id))
704}
705
706pub fn set_focus(node: &mut LayoutNode, focused_id: Option<&str>) {
708 node.focused = focused_id.is_some() && node.id.as_deref() == focused_id;
709 for child in &mut node.children {
710 set_focus(child, focused_id);
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717 use crate::widget::{self, Units};
718
719 #[test]
720 fn positioned_children_leave_the_flow() {
721 let root = widget::panel(FlexDir::Column)
722 .w(200.0)
723 .h(100.0)
724 .padding(4.0, 4.0, 4.0, 4.0)
725 .child(widget::spacer().min_w(10.0).min_h(10.0))
726 .child(widget::button("Hi").w(40.0).h(20.0).at(50.0, 30.0))
727 .child(widget::spacer().min_w(10.0).min_h(10.0));
728 let node = compute(&root, 400.0, 400.0);
729 let r = node.children[1].rect;
730 assert_eq!((r.x, r.y, r.w, r.h), (54.0, 34.0, 40.0, 20.0));
731 assert_eq!(node.children[0].rect.y, 4.0);
733 assert_eq!(node.children[2].rect.y, 4.0 + 10.0 + 2.0);
734 }
735
736 #[test]
737 fn positioned_container_lays_out_its_children_in_place() {
738 let root = widget::panel(FlexDir::Column)
739 .w(Units::Percent(100.0))
740 .h(Units::Percent(100.0))
741 .child(
742 widget::panel(FlexDir::Row)
743 .w(100.0)
744 .h(20.0)
745 .gap(0.0)
746 .at(10.0, 20.0)
747 .child(widget::spacer().min_w(30.0).min_h(5.0))
748 .child(widget::spacer().min_w(30.0).min_h(5.0)),
749 );
750 let node = compute(&root, 320.0, 240.0);
751 let panel = &node.children[0];
752 assert_eq!((panel.rect.x, panel.rect.y), (10.0, 20.0));
753 assert_eq!(panel.children[1].rect.x, 40.0);
754 assert_eq!(panel.children[1].rect.y, 20.0);
755 }
756
757 #[test]
758 fn auto_sized_parent_grows_to_fit_positioned_children() {
759 let root = widget::panel(FlexDir::Column)
760 .child(widget::spacer().min_w(10.0).min_h(10.0).at(30.0, 40.0));
761 let node = compute(&root, 300.0, 300.0);
762 assert_eq!((node.rect.w, node.rect.h), (40.0, 50.0));
763 }
764
765 fn pct(v: f32) -> Units {
766 Units::Percent(v)
767 }
768
769 fn r(n: &LayoutNode) -> (f32, f32, f32, f32) {
770 (n.rect.x, n.rect.y, n.rect.w, n.rect.h)
771 }
772
773 #[test]
774 fn percent_width_in_a_row_resolves_once_against_the_content_box() {
775 let root = widget::panel(FlexDir::Row)
776 .w(400.0)
777 .h(100.0)
778 .padding(5.0, 10.0, 5.0, 10.0)
779 .child(
780 widget::panel(FlexDir::Column)
781 .w(pct(50.0))
782 .child(widget::spacer().min_w(10.0).min_h(10.0)),
783 )
784 .child(widget::spacer().w(pct(25.0)).h(pct(50.0)));
785 let node = compute(&root, 800.0, 600.0);
786 assert_eq!(r(&node.children[0]).2, 190.0);
788 assert_eq!(r(&node.children[1]), (10.0 + 190.0 + 2.0, 5.0, 95.0, 45.0));
789 }
790
791 #[test]
792 fn percent_sizes_in_a_column() {
793 let root = widget::panel(FlexDir::Column)
794 .w(200.0)
795 .h(300.0)
796 .padding(10.0, 0.0, 10.0, 0.0)
797 .gap(0.0)
798 .child(
799 widget::panel(FlexDir::Row)
800 .w(pct(50.0))
801 .h(pct(50.0))
802 .child(widget::spacer()),
803 )
804 .child(widget::spacer().w(pct(100.0)).h(pct(10.0)));
805 let node = compute(&root, 800.0, 600.0);
806 assert_eq!(r(&node.children[0]), (0.0, 10.0, 100.0, 140.0));
807 assert_eq!(r(&node.children[1]), (0.0, 150.0, 200.0, 28.0));
808 }
809
810 #[test]
811 fn nested_percents_compound_per_level_only() {
812 let root = widget::panel(FlexDir::Row).w(400.0).h(200.0).child(
813 widget::panel(FlexDir::Column)
814 .w(pct(50.0))
815 .h(pct(100.0))
816 .child(
817 widget::panel(FlexDir::Row)
818 .w(pct(50.0))
819 .h(pct(50.0))
820 .child(widget::spacer().w(pct(50.0)).h(pct(50.0))),
821 ),
822 );
823 let node = compute(&root, 400.0, 200.0);
824 let a = &node.children[0];
825 let b = &a.children[0];
826 let c = &b.children[0];
827 assert_eq!(r(a), (0.0, 0.0, 200.0, 200.0));
828 assert_eq!(r(b), (0.0, 0.0, 100.0, 100.0));
829 assert_eq!(r(c), (0.0, 0.0, 50.0, 50.0));
830 }
831
832 #[test]
833 fn percent_resolves_against_the_scrollbar_free_box() {
834 let root = widget::scroll_view()
835 .w(106.0)
836 .h(50.0)
837 .child(widget::spacer().w(pct(50.0)).h(10.0));
838 let node = compute(&root, 400.0, 400.0);
839 assert_eq!(node.children[0].rect.w, 50.0);
840 }
841
842 #[test]
843 fn grow_shares_free_space_with_percent_siblings() {
844 let root = widget::panel(FlexDir::Row)
845 .w(300.0)
846 .h(50.0)
847 .gap(4.0)
848 .child(widget::spacer().w(pct(50.0)))
849 .child(widget::panel(FlexDir::Column).flex(1.0).child(widget::spacer()))
850 .child(widget::spacer().w(pct(10.0)));
851 let node = compute(&root, 300.0, 50.0);
852 assert_eq!(r(&node.children[0]).2, 150.0);
854 assert_eq!(r(&node.children[1]), (154.0, 0.0, 112.0, 4.0));
855 assert_eq!(r(&node.children[2]), (270.0, 0.0, 30.0, 4.0));
856 }
857
858 #[test]
859 fn grown_panel_keeps_its_size_and_lays_children_out_against_it() {
860 let root = widget::panel(FlexDir::Column)
861 .w(200.0)
862 .h(200.0)
863 .gap(0.0)
864 .child(widget::spacer().min_h(20.0))
865 .child(
866 widget::panel(FlexDir::Row)
867 .flex(1.0)
868 .w(pct(100.0))
869 .gap(0.0)
870 .child(widget::spacer().w(pct(25.0)).h(pct(50.0)))
871 .child(widget::panel(FlexDir::Column).flex(1.0).child(widget::spacer()))
872 .child(widget::spacer().dock(Dock::Right).min_w(10.0)),
873 );
874 let node = compute(&root, 400.0, 400.0);
875 let grown = &node.children[1];
876 assert_eq!(r(grown), (0.0, 20.0, 200.0, 180.0));
877 assert_eq!(r(&grown.children[0]), (0.0, 20.0, 50.0, 90.0));
878 assert_eq!(r(&grown.children[1]).2, 200.0 - 50.0 - 10.0);
880 assert_eq!(r(&grown.children[2]), (190.0, 20.0, 10.0, 180.0));
882 }
883
884 #[test]
885 fn long_list_deep_inside_a_grown_row_does_not_push_siblings_out() {
886 let mut list = widget::scroll_view().flex(1.0).w(pct(100.0));
889 for _ in 0..60 {
890 list = list.child(widget::spacer().h(25.0));
891 }
892 let root = widget::panel(FlexDir::Column)
893 .w(400.0)
894 .h(252.0)
895 .gap(0.0)
896 .child(widget::spacer().h(20.0))
897 .child(
898 widget::panel(FlexDir::Row)
899 .flex(1.0)
900 .w(pct(100.0))
901 .child(
902 widget::panel(FlexDir::Column)
903 .w(pct(50.0))
904 .h(pct(100.0))
905 .child(widget::spacer().h(16.0))
906 .child(list),
907 ),
908 )
909 .child(widget::spacer().h(26.0));
910 let node = compute(&root, 400.0, 252.0);
911 assert_eq!(r(&node.children[1]), (0.0, 20.0, 400.0, 206.0));
912 let footer = r(&node.children[2]);
913 assert_eq!((footer.1, footer.3), (226.0, 26.0));
914 let column = &node.children[1].children[0];
915 assert_eq!(column.rect.h, 206.0);
916 let list = &column.children[1];
917 assert!(list.rect.y + list.rect.h <= 226.0, "{:?}", list.rect);
918 assert!(list.content_h > list.rect.h);
919 }
920
921 #[test]
922 fn grown_column_panel_fills_its_parent_and_children_grow_too() {
923 let root = widget::panel(FlexDir::Column)
924 .w(100.0)
925 .h(100.0)
926 .gap(0.0)
927 .child(
928 widget::panel(FlexDir::Column)
929 .flex(1.0)
930 .gap(0.0)
931 .child(widget::spacer().flex(1.0)),
932 );
933 let node = compute(&root, 100.0, 100.0);
934 assert_eq!(node.children[0].rect.h, 100.0);
935 assert_eq!(node.children[0].children[0].rect.h, 100.0);
936 }
937
938 #[test]
939 fn empty_auto_panel_in_a_row_takes_its_min_width() {
940 let root = widget::panel(FlexDir::Row)
941 .w(100.0)
942 .h(20.0)
943 .child(widget::panel(FlexDir::Column).min_w(7.0))
944 .child(widget::spacer());
945 let node = compute(&root, 100.0, 20.0);
946 assert_eq!(node.children[0].rect.w, 7.0);
947 assert_eq!(node.children[1].rect.x, 9.0);
948 }
949
950 #[test]
951 fn padding_counts_once_in_auto_size() {
952 let root = widget::panel(FlexDir::Column)
953 .padding(3.0, 4.0, 5.0, 6.0)
954 .child(widget::spacer().min_w(10.0).min_h(10.0));
955 let node = compute(&root, 300.0, 300.0);
956 assert_eq!((node.rect.w, node.rect.h), (20.0, 18.0));
957 assert_eq!((node.children[0].rect.x, node.children[0].rect.y), (6.0, 3.0));
958 }
959
960 #[test]
961 fn auto_row_in_a_column_measures_to_content() {
962 let root = widget::panel(FlexDir::Column).w(200.0).h(200.0).child(
964 widget::panel(FlexDir::Row)
965 .align(Align::Center)
966 .gap(0.0)
967 .child(widget::spacer().min_w(10.0).min_h(10.0))
968 .child(widget::spacer().min_w(10.0).min_h(20.0))
969 .child(widget::spacer().min_w(5.0).dock(Dock::Right)),
970 );
971 let node = compute(&root, 200.0, 200.0);
972 let row = &node.children[0];
973 assert_eq!(row.rect.h, 20.0);
974 assert_eq!(row.children[0].rect.y, 5.0);
975 assert_eq!(row.children[1].rect.y, 0.0);
976 }
977
978 #[test]
979 fn positioned_auto_row_measures_to_content() {
980 let pill = widget::panel(FlexDir::Row).padding(1.0, 1.0, 1.0, 1.0).child(
983 widget::panel(FlexDir::Row)
984 .align(Align::Center)
985 .child(widget::spacer().min_w(10.0).min_h(9.0)),
986 );
987 let root = widget::panel(FlexDir::Column).w(500.0).h(500.0).child(pill.at(3.0, 4.0));
988 let node = compute(&root, 500.0, 500.0);
989 let r = node.children[0].rect;
990 assert_eq!((r.x, r.y, r.w, r.h), (3.0, 4.0, 12.0, 11.0));
991 assert_eq!(node.children[0].children[0].children[0].rect.y, 5.0);
992 }
993
994 #[test]
995 fn row_with_right_dock_measured_on_unbounded_axis() {
996 let root = widget::panel(FlexDir::Row).w(300.0).h(20.0).child(
999 widget::panel(FlexDir::Row)
1000 .gap(1.0)
1001 .child(widget::spacer().min_w(10.0))
1002 .child(widget::spacer().min_w(5.0).dock(Dock::Right))
1003 .child(widget::spacer().min_w(7.0).dock(Dock::Right)),
1004 );
1005 let node = compute(&root, 300.0, 20.0);
1006 let inner = &node.children[0];
1007 assert_eq!(inner.rect.w, 10.0 + 1.0 + 7.0 + 1.0 + 5.0);
1008 assert_eq!(inner.children[1].rect.x, 19.0);
1009 assert_eq!(inner.children[2].rect.x, 11.0);
1010 }
1011
1012 #[test]
1013 fn align_center_and_end_in_both_directions() {
1014 let col = widget::panel(FlexDir::Column)
1015 .w(100.0)
1016 .h(50.0)
1017 .align(Align::Center)
1018 .child(widget::spacer().min_w(20.0).min_h(5.0));
1019 assert_eq!(compute(&col, 100.0, 50.0).children[0].rect.x, 40.0);
1020 let row = widget::panel(FlexDir::Row)
1021 .w(100.0)
1022 .h(50.0)
1023 .padding(10.0, 0.0, 0.0, 0.0)
1024 .align(Align::End)
1025 .child(widget::spacer().min_w(20.0).h(pct(20.0)));
1026 assert_eq!(compute(&row, 100.0, 50.0).children[0].rect.y, 10.0 + 32.0);
1028 }
1029
1030 #[test]
1031 fn positioned_children_with_percent_sizes() {
1032 let root = widget::panel(FlexDir::Column)
1033 .w(200.0)
1034 .h(100.0)
1035 .padding(10.0, 10.0, 10.0, 10.0)
1036 .child(widget::spacer().w(pct(50.0)).h(pct(50.0)).at(20.0, 10.0))
1037 .child(
1038 widget::panel(FlexDir::Row)
1039 .w(pct(100.0))
1040 .h(pct(25.0))
1041 .at(0.0, 60.0)
1042 .gap(0.0)
1043 .child(widget::spacer().w(pct(50.0)))
1044 .child(widget::spacer().flex(1.0)),
1045 );
1046 let node = compute(&root, 400.0, 400.0);
1047 assert_eq!(r(&node.children[0]), (30.0, 20.0, 90.0, 40.0));
1049 let p = &node.children[1];
1052 assert_eq!(r(p), (10.0, 70.0, 180.0, 20.0));
1053 assert_eq!(r(&p.children[0]), (10.0, 70.0, 90.0, 4.0));
1054 assert_eq!(r(&p.children[1]), (100.0, 70.0, 90.0, 4.0));
1055 }
1056
1057 #[test]
1058 fn growing_scroll_view_fills_free_space_and_scrolls_the_rest() {
1059 let list = widget::scroll_view()
1060 .id("list")
1061 .flex(1.0)
1062 .w(pct(100.0))
1063 .gap(0.0)
1064 .padding(2.0, 2.0, 2.0, 2.0)
1065 .scroll_y(1000.0)
1066 .children((0..10).map(|_| widget::spacer().w(pct(100.0)).min_h(20.0)));
1067 let root = widget::panel(FlexDir::Column)
1068 .w(100.0)
1069 .h(120.0)
1070 .gap(0.0)
1071 .child(widget::spacer().min_h(20.0))
1072 .child(list);
1073 let node = compute(&root, 100.0, 120.0);
1074 let v = find_node(&node, "list").unwrap();
1075 assert_eq!(r(v), (0.0, 20.0, 100.0, 100.0));
1076 assert_eq!(v.content_h, 204.0);
1077 assert_eq!(v.scroll_y, 104.0);
1078 assert_eq!(v.children[0].rect.w, 100.0 - 4.0 - SCROLLBAR_W);
1080 let last = v.children[9].rect;
1082 assert_eq!(last.y + last.h, 20.0 + 100.0 - 2.0);
1083 assert!(v.scrollbar_rects().is_some());
1084 }
1085
1086 #[test]
1087 fn label_wraps_at_its_explicit_width() {
1088 let root = widget::panel(FlexDir::Column)
1089 .w(300.0)
1090 .child(widget::label("aaaa bbbb cccc").w(40.0));
1091 let node = compute(&root, 300.0, 300.0);
1092 let l = node.children[0].rect;
1093 assert_eq!(l.w, 40.0);
1094 assert!(l.h > text::LINE_H, "wrapped onto several lines: {l:?}");
1095 }
1096
1097 #[test]
1098 fn percent_caps_at_the_cross_axis_and_ignores_unbounded_basis() {
1099 let root = widget::panel(FlexDir::Column)
1101 .w(100.0)
1102 .h(100.0)
1103 .child(widget::spacer().w(pct(150.0)));
1104 assert_eq!(compute(&root, 100.0, 100.0).children[0].rect.w, 100.0);
1105 let root = widget::panel(FlexDir::Row).w(200.0).h(20.0).child(
1108 widget::panel(FlexDir::Row)
1109 .gap(0.0)
1110 .child(widget::spacer().min_w(40.0))
1111 .child(widget::spacer().w(pct(50.0))),
1112 );
1113 let node = compute(&root, 200.0, 20.0);
1114 let inner = &node.children[0];
1115 assert_eq!(inner.rect.w, 44.0);
1116 assert_eq!(inner.children[1].rect.w, 22.0);
1117 }
1118
1119 #[test]
1120 fn find_mut_and_set_text() {
1121 let mut root = widget::panel(FlexDir::Column).child(widget::label("a").id("title"));
1122 assert!(root.find_mut("title").unwrap().set_text("b"));
1123 match &root.children[0].kind {
1124 crate::widget::WidgetKind::Label(t) => assert_eq!(t, "b"),
1125 _ => unreachable!(),
1126 }
1127 assert!(root.find_mut("missing").is_none());
1128 }
1129}