1use crate::text;
2use crate::widget::{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}
71
72impl LayoutNode {
73 pub fn max_scroll(&self) -> f32 {
75 (self.content_h - self.rect.h).max(0.0)
76 }
77
78 pub fn scrollbar_rects(&self) -> Option<(Rect, Rect)> {
81 let max = self.max_scroll();
82 if !self.scrollbar || max <= 0.0 {
83 return None;
84 }
85 let r = self.rect;
86 let track = Rect {
87 x: r.x + r.w - SCROLLBAR_W,
88 y: r.y,
89 w: SCROLLBAR_W,
90 h: r.h,
91 };
92 let knob_h = (r.h * r.h / self.content_h).max(MIN_KNOB_H).min(r.h);
93 let knob = Rect {
94 y: r.y + (r.h - knob_h) * (self.scroll_y / max),
95 h: knob_h,
96 ..track
97 };
98 Some((track, knob))
99 }
100
101 fn translate_y(&mut self, dy: f32) {
102 self.rect.y += dy;
103 for c in &mut self.children {
104 c.translate_y(dy);
105 }
106 }
107}
108
109impl Default for LayoutNode {
110 fn default() -> Self {
111 Self {
112 rect: Rect::default(),
113 id: None,
114 on_click: None,
115 children: Vec::new(),
116 enabled: true,
117 focused: false,
118 content_h: 0.0,
119 scroll_y: 0.0,
120 clip: false,
121 scrollbar: false,
122 }
123 }
124}
125
126pub fn compute(widget: &Widget, avail_w: f32, avail_h: f32) -> LayoutNode {
143 let mut node = LayoutNode {
144 id: widget.id.clone(),
145 on_click: widget.on_click.clone(),
146 enabled: widget.enabled,
147 focused: widget.focused,
148 ..Default::default()
149 };
150 layout_widget(widget, &mut node, 0.0, 0.0, avail_w, avail_h, avail_w, avail_h);
151 node
152}
153
154const UNBOUNDED: f32 = f32::MAX;
157
158fn bounded(v: f32) -> bool {
159 v < UNBOUNDED / 2.0
160}
161
162fn resolve_size(u: Units, basis: f32) -> Option<f32> {
164 match u {
165 Units::Percent(_) if !bounded(basis) => None,
166 _ => u.resolve(basis),
167 }
168}
169
170fn new_node(w: &Widget) -> LayoutNode {
171 LayoutNode {
172 id: w.id.clone(),
173 on_click: w.on_click.clone(),
174 enabled: w.enabled,
175 focused: w.focused,
176 ..Default::default()
177 }
178}
179
180#[allow(clippy::too_many_arguments)]
184fn layout_widget(
185 w: &Widget,
186 node: &mut LayoutNode,
187 x: f32,
188 y: f32,
189 max_w: f32,
190 max_h: f32,
191 basis_w: f32,
192 basis_h: f32,
193) {
194 let s = &w.style;
195 let has_children = !w.children.is_empty();
196 let explicit_w = resolve_size(s.w, basis_w).map(|v| v.min(max_w));
197 let explicit_h = resolve_size(s.h, basis_h).map(|v| v.min(max_h));
198
199 let mut ww = explicit_w.unwrap_or(max_w);
201 let mut hh = explicit_h.unwrap_or(max_h);
202
203 if !has_children {
204 match &w.kind {
206 WidgetKind::Label(t) | WidgetKind::Button(t) => {
207 let box_w = explicit_w.unwrap_or(max_w);
209 let avail_w = (box_w - s.pad[1] - s.pad[3]).max(0.0);
210 if avail_w < 4096.0 && !s.no_wrap {
213 ww = (avail_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
214 hh = (text::text_height_in(t, avail_w, s.font_scale, &s.font)
215 + s.pad[0]
216 + s.pad[2])
217 .max(s.min_h)
218 .min(max_h);
219 } else {
220 let tw = text::str_width_in(t, s.font_scale, &s.font);
221 ww = (tw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
222 hh = (text::LINE_H * s.font_scale + s.pad[0] + s.pad[2])
223 .max(s.min_h)
224 .min(max_h);
225 }
226 }
227 WidgetKind::ItemSlot(_) | WidgetKind::InvSlot { .. } => {
228 ww = (18.0 + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
229 hh = (18.0 + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
230 }
231 WidgetKind::Spacer => {
232 ww = s.min_w.max(1.0).min(max_w);
233 hh = s.min_h.max(1.0).min(max_h);
234 }
235 WidgetKind::Panel(_) => {
238 if !bounded(ww) {
239 ww = s.min_w;
240 }
241 if !bounded(hh) {
242 hh = s.min_h;
243 }
244 }
245 WidgetKind::TextBox(_) => {
248 if !bounded(ww) {
249 ww = 100.0;
250 }
251 ww = ww.max(s.min_w).min(max_w);
252 hh = (text::LINE_H * s.font_scale + 7.0 + s.pad[0] + s.pad[2])
253 .max(s.min_h)
254 .min(max_h);
255 }
256 WidgetKind::Image(image) => {
257 let (nw, nh) = crate::image::natural_size(image);
258 ww = (nw + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
259 hh = (nh + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
260 }
261 WidgetKind::McImage { img_w, img_h, .. } => {
262 ww = (*img_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
263 hh = (*img_h + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
264 }
265 }
266 if let Some(v) = explicit_w {
268 ww = v;
269 }
270 if let Some(v) = explicit_h {
271 hh = v;
272 }
273 }
274
275 node.rect = Rect { x, y, w: ww, h: hh };
276 node.content_h = hh;
277 node.scroll_y = 0.0;
278 node.clip = s.clip;
279 node.scrollbar = s.scrollbar;
280
281 if has_children {
282 let shrink = Shrink {
283 w: explicit_w.is_none(),
284 h: explicit_h.is_none(),
285 max_w,
286 max_h,
287 };
288 layout_children(w, node, Some(shrink));
289 }
290}
291
292fn arrange(w: &Widget, node: &mut LayoutNode, x: f32, y: f32) {
295 node.rect.x = x;
296 node.rect.y = y;
297 node.content_h = node.rect.h;
298 if !w.children.is_empty() {
299 layout_children(w, node, None);
300 }
301}
302
303struct Shrink {
306 w: bool,
307 h: bool,
308 max_w: f32,
309 max_h: f32,
310}
311
312fn layout_children(w: &Widget, node: &mut LayoutNode, shrink: Option<Shrink>) {
315 let s = &w.style;
316 let Rect {
317 x,
318 y,
319 w: ww,
320 h: hh,
321 } = node.rect;
322
323 let dir = if matches!(w.kind, WidgetKind::Panel(_)) && w.flex_dir == FlexDir::Row {
324 FlexDir::Row
325 } else {
326 FlexDir::Column
327 };
328 let bar_w = if s.scrollbar { SCROLLBAR_W } else { 0.0 };
329 let content_w = if bounded(ww) {
330 (ww - s.pad[1] - s.pad[3] - bar_w).max(0.0)
331 } else {
332 UNBOUNDED
333 };
334 let content_h = if bounded(hh) {
335 (hh - s.pad[0] - s.pad[2]).max(0.0)
336 } else {
337 UNBOUNDED
338 };
339 let (main_bounded, cross_bounded) = match dir {
340 FlexDir::Row => (bounded(content_w), bounded(content_h)),
341 FlexDir::Column => (bounded(content_h), bounded(content_w)),
342 };
343 let cross_offset = |content: f32, size: f32| -> f32 {
344 if !cross_bounded {
345 return 0.0;
346 }
347 match s.align {
348 Align::Center => (content - size) / 2.0,
349 Align::End => content - size,
350 Align::Start => 0.0,
351 }
352 };
353
354 let effective_flex = |child: &Widget| -> f32 {
357 if child.style.dock == Dock::Fill {
358 child.style.flex.max(1.0)
359 } else {
360 child.style.flex
361 }
362 };
363 let in_flow = |child: &Widget| -> bool {
365 child.style.pos.is_none()
366 && !matches!(
367 (dir, child.style.dock),
368 (FlexDir::Row, Dock::Right) | (FlexDir::Column, Dock::Bottom)
369 )
370 };
371 let stretches = |dock: Dock| -> bool {
372 cross_bounded
373 && match dir {
374 FlexDir::Row => matches!(dock, Dock::Fill | Dock::Left | Dock::Right),
375 FlexDir::Column => matches!(dock, Dock::Fill | Dock::Top | Dock::Bottom),
376 }
377 };
378
379 let mut child_nodes: Vec<LayoutNode> = Vec::with_capacity(w.children.len());
381 let mut total_flex: f32 = 0.0;
382 let mut used_main: f32 = 0.0;
383
384 for child in &w.children {
385 let mut cn = new_node(child);
386 if let Some((px, py)) = child.style.pos {
390 let mw = if bounded(content_w) {
391 (content_w - px).max(0.0)
392 } else {
393 UNBOUNDED
394 };
395 let mh = if bounded(content_h) {
396 (content_h - py).max(0.0)
397 } else {
398 UNBOUNDED
399 };
400 layout_widget(child, &mut cn, 0.0, 0.0, mw, mh, content_w, content_h);
401 child_nodes.push(cn);
402 continue;
403 }
404 let (cmw, cmh) = match (dir, child.style.dock) {
407 (_, Dock::Fill) => (content_w, content_h),
408 (FlexDir::Row, _) => (UNBOUNDED, content_h),
409 (FlexDir::Column, _) => (content_w, UNBOUNDED),
410 };
411 layout_widget(child, &mut cn, 0.0, 0.0, cmw, cmh, content_w, content_h);
412 let ef = effective_flex(child);
413 if ef > 0.0 && child.style.clip && main_bounded && in_flow(child) {
415 match dir {
416 FlexDir::Row if child.style.w.is_auto() => cn.rect.w = child.style.min_w,
417 FlexDir::Column if child.style.h.is_auto() => cn.rect.h = child.style.min_h,
418 _ => {}
419 }
420 }
421 used_main += match dir {
424 FlexDir::Row => cn.rect.w,
425 FlexDir::Column => cn.rect.h,
426 };
427 if in_flow(child) {
428 total_flex += ef;
429 }
430 child_nodes.push(cn);
431 }
432 let placed = w.children.iter().filter(|c| c.style.pos.is_none()).count();
433 used_main += s.gap * (placed.saturating_sub(1) as f32);
434
435 let available = if main_bounded {
437 (match dir {
438 FlexDir::Row => content_w,
439 FlexDir::Column => content_h,
440 }) - used_main
441 } else {
442 0.0
443 };
444 let mut pos = match dir {
445 FlexDir::Row => s.pad[3],
446 FlexDir::Column => s.pad[0],
447 };
448
449 for (i, child) in w.children.iter().enumerate() {
451 if !in_flow(child) {
452 continue;
453 }
454 let cn = &mut child_nodes[i];
455 let ef = effective_flex(child);
456 let grow = if ef > 0.0 && total_flex > 0.0 && available > 0.0 {
457 available * ef / total_flex
458 } else {
459 0.0
460 };
461 let stretch = stretches(child.style.dock);
462 let (cx, cy) = if dir == FlexDir::Row {
463 cn.rect.w += grow;
464 if stretch {
465 cn.rect.h = content_h;
466 }
467 let p = (x + pos, y + s.pad[0] + cross_offset(content_h, cn.rect.h));
468 pos += cn.rect.w + s.gap;
469 p
470 } else {
471 cn.rect.h += grow;
472 if stretch {
473 cn.rect.w = content_w;
474 }
475 let p = (x + s.pad[3] + cross_offset(content_w, cn.rect.w), y + pos);
476 pos += cn.rect.h + s.gap;
477 p
478 };
479 arrange(child, cn, cx, cy);
480 }
481
482 let mut rpos = if main_bounded {
486 match dir {
487 FlexDir::Row => x + s.pad[3] + content_w,
488 FlexDir::Column => y + s.pad[0] + content_h,
489 }
490 } else {
491 let docked: Vec<f32> = w
492 .children
493 .iter()
494 .zip(&child_nodes)
495 .filter(|(c, _)| !in_flow(c) && c.style.pos.is_none())
496 .map(|(_, n)| match dir {
497 FlexDir::Row => n.rect.w,
498 FlexDir::Column => n.rect.h,
499 })
500 .collect();
501 let span: f32 =
502 docked.iter().sum::<f32>() + s.gap * (docked.len().saturating_sub(1) as f32);
503 (match dir {
504 FlexDir::Row => x,
505 FlexDir::Column => y,
506 }) + pos
507 + span
508 };
509 for (i, child) in w.children.iter().enumerate() {
510 if in_flow(child) || child.style.pos.is_some() {
511 continue;
512 }
513 let cn = &mut child_nodes[i];
514 let stretch = stretches(child.style.dock);
515 let (cx, cy) = if dir == FlexDir::Row {
516 if stretch {
517 cn.rect.h = content_h;
518 }
519 rpos -= cn.rect.w;
520 (rpos, y + s.pad[0] + cross_offset(content_h, cn.rect.h))
521 } else {
522 if stretch {
523 cn.rect.w = content_w;
524 }
525 rpos -= cn.rect.h;
526 (x + s.pad[3] + cross_offset(content_w, cn.rect.w), rpos)
527 };
528 rpos -= s.gap;
529 arrange(child, cn, cx, cy);
530 }
531
532 for (i, child) in w.children.iter().enumerate() {
534 let Some((px, py)) = child.style.pos else {
535 continue;
536 };
537 let cn = &mut child_nodes[i];
538 arrange(child, cn, x + s.pad[3] + px, y + s.pad[0] + py);
539 }
540
541 let ch: f32 = child_nodes
543 .iter()
544 .map(|c| c.rect.y - y + c.rect.h)
545 .fold(0.0f32, f32::max);
546 if let Some(fit) = shrink {
547 if fit.w {
548 let cw: f32 = child_nodes
549 .iter()
550 .map(|c| c.rect.x - x + c.rect.w)
551 .fold(0.0f32, f32::max);
552 node.rect.w = (cw + s.pad[1] + bar_w).max(s.min_w).min(fit.max_w);
553 }
554 if fit.h {
555 node.rect.h = (ch + s.pad[2]).max(s.min_h).min(fit.max_h);
556 }
557 }
558
559 node.content_h = ch + s.pad[2];
562 node.scroll_y = s.scroll_y.clamp(0.0, node.max_scroll());
563 if node.scroll_y > 0.0 {
564 for c in &mut child_nodes {
565 c.translate_y(-node.scroll_y);
566 }
567 }
568
569 node.children = child_nodes;
570}
571
572pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
576 if !node.rect.contains(mx, my) {
577 return None;
578 }
579 for child in node.children.iter().rev() {
580 if let Some(hit) = hit_test(child, mx, my) {
581 return Some(hit);
582 }
583 }
584 if node.on_click.is_some() && node.enabled {
585 Some(node)
586 } else {
587 None
588 }
589}
590
591pub fn find_by_id(node: &LayoutNode, id: &str) -> Option<Rect> {
596 if node.id.as_deref() == Some(id) {
597 return Some(node.rect);
598 }
599 node.children.iter().find_map(|c| find_by_id(c, id))
600}
601
602pub fn find_node<'a>(node: &'a LayoutNode, id: &str) -> Option<&'a LayoutNode> {
605 if node.id.as_deref() == Some(id) {
606 return Some(node);
607 }
608 node.children.iter().find_map(|c| find_node(c, id))
609}
610
611pub fn set_focus(node: &mut LayoutNode, focused_id: Option<&str>) {
613 node.focused = focused_id.is_some() && node.id.as_deref() == focused_id;
614 for child in &mut node.children {
615 set_focus(child, focused_id);
616 }
617}
618
619#[cfg(test)]
620mod tests {
621 use super::*;
622 use crate::widget::{self, Units};
623
624 #[test]
625 fn positioned_children_leave_the_flow() {
626 let root = widget::panel(FlexDir::Column)
627 .w(200.0)
628 .h(100.0)
629 .padding(4.0, 4.0, 4.0, 4.0)
630 .child(widget::spacer().min_w(10.0).min_h(10.0))
631 .child(widget::button("Hi").w(40.0).h(20.0).at(50.0, 30.0))
632 .child(widget::spacer().min_w(10.0).min_h(10.0));
633 let node = compute(&root, 400.0, 400.0);
634 let r = node.children[1].rect;
635 assert_eq!((r.x, r.y, r.w, r.h), (54.0, 34.0, 40.0, 20.0));
636 assert_eq!(node.children[0].rect.y, 4.0);
638 assert_eq!(node.children[2].rect.y, 4.0 + 10.0 + 2.0);
639 }
640
641 #[test]
642 fn positioned_container_lays_out_its_children_in_place() {
643 let root = widget::panel(FlexDir::Column)
644 .w(Units::Percent(100.0))
645 .h(Units::Percent(100.0))
646 .child(
647 widget::panel(FlexDir::Row)
648 .w(100.0)
649 .h(20.0)
650 .gap(0.0)
651 .at(10.0, 20.0)
652 .child(widget::spacer().min_w(30.0).min_h(5.0))
653 .child(widget::spacer().min_w(30.0).min_h(5.0)),
654 );
655 let node = compute(&root, 320.0, 240.0);
656 let panel = &node.children[0];
657 assert_eq!((panel.rect.x, panel.rect.y), (10.0, 20.0));
658 assert_eq!(panel.children[1].rect.x, 40.0);
659 assert_eq!(panel.children[1].rect.y, 20.0);
660 }
661
662 #[test]
663 fn auto_sized_parent_grows_to_fit_positioned_children() {
664 let root = widget::panel(FlexDir::Column)
665 .child(widget::spacer().min_w(10.0).min_h(10.0).at(30.0, 40.0));
666 let node = compute(&root, 300.0, 300.0);
667 assert_eq!((node.rect.w, node.rect.h), (40.0, 50.0));
668 }
669
670 fn pct(v: f32) -> Units {
671 Units::Percent(v)
672 }
673
674 fn r(n: &LayoutNode) -> (f32, f32, f32, f32) {
675 (n.rect.x, n.rect.y, n.rect.w, n.rect.h)
676 }
677
678 #[test]
679 fn percent_width_in_a_row_resolves_once_against_the_content_box() {
680 let root = widget::panel(FlexDir::Row)
681 .w(400.0)
682 .h(100.0)
683 .padding(5.0, 10.0, 5.0, 10.0)
684 .child(
685 widget::panel(FlexDir::Column)
686 .w(pct(50.0))
687 .child(widget::spacer().min_w(10.0).min_h(10.0)),
688 )
689 .child(widget::spacer().w(pct(25.0)).h(pct(50.0)));
690 let node = compute(&root, 800.0, 600.0);
691 assert_eq!(r(&node.children[0]).2, 190.0);
693 assert_eq!(r(&node.children[1]), (10.0 + 190.0 + 2.0, 5.0, 95.0, 45.0));
694 }
695
696 #[test]
697 fn percent_sizes_in_a_column() {
698 let root = widget::panel(FlexDir::Column)
699 .w(200.0)
700 .h(300.0)
701 .padding(10.0, 0.0, 10.0, 0.0)
702 .gap(0.0)
703 .child(
704 widget::panel(FlexDir::Row)
705 .w(pct(50.0))
706 .h(pct(50.0))
707 .child(widget::spacer()),
708 )
709 .child(widget::spacer().w(pct(100.0)).h(pct(10.0)));
710 let node = compute(&root, 800.0, 600.0);
711 assert_eq!(r(&node.children[0]), (0.0, 10.0, 100.0, 140.0));
712 assert_eq!(r(&node.children[1]), (0.0, 150.0, 200.0, 28.0));
713 }
714
715 #[test]
716 fn nested_percents_compound_per_level_only() {
717 let root = widget::panel(FlexDir::Row).w(400.0).h(200.0).child(
718 widget::panel(FlexDir::Column)
719 .w(pct(50.0))
720 .h(pct(100.0))
721 .child(
722 widget::panel(FlexDir::Row)
723 .w(pct(50.0))
724 .h(pct(50.0))
725 .child(widget::spacer().w(pct(50.0)).h(pct(50.0))),
726 ),
727 );
728 let node = compute(&root, 400.0, 200.0);
729 let a = &node.children[0];
730 let b = &a.children[0];
731 let c = &b.children[0];
732 assert_eq!(r(a), (0.0, 0.0, 200.0, 200.0));
733 assert_eq!(r(b), (0.0, 0.0, 100.0, 100.0));
734 assert_eq!(r(c), (0.0, 0.0, 50.0, 50.0));
735 }
736
737 #[test]
738 fn percent_resolves_against_the_scrollbar_free_box() {
739 let root = widget::scroll_view()
740 .w(106.0)
741 .h(50.0)
742 .child(widget::spacer().w(pct(50.0)).h(10.0));
743 let node = compute(&root, 400.0, 400.0);
744 assert_eq!(node.children[0].rect.w, 50.0);
745 }
746
747 #[test]
748 fn grow_shares_free_space_with_percent_siblings() {
749 let root = widget::panel(FlexDir::Row)
750 .w(300.0)
751 .h(50.0)
752 .gap(4.0)
753 .child(widget::spacer().w(pct(50.0)))
754 .child(widget::panel(FlexDir::Column).flex(1.0).child(widget::spacer()))
755 .child(widget::spacer().w(pct(10.0)));
756 let node = compute(&root, 300.0, 50.0);
757 assert_eq!(r(&node.children[0]).2, 150.0);
759 assert_eq!(r(&node.children[1]), (154.0, 0.0, 112.0, 4.0));
760 assert_eq!(r(&node.children[2]), (270.0, 0.0, 30.0, 4.0));
761 }
762
763 #[test]
764 fn grown_panel_keeps_its_size_and_lays_children_out_against_it() {
765 let root = widget::panel(FlexDir::Column)
766 .w(200.0)
767 .h(200.0)
768 .gap(0.0)
769 .child(widget::spacer().min_h(20.0))
770 .child(
771 widget::panel(FlexDir::Row)
772 .flex(1.0)
773 .w(pct(100.0))
774 .gap(0.0)
775 .child(widget::spacer().w(pct(25.0)).h(pct(50.0)))
776 .child(widget::panel(FlexDir::Column).flex(1.0).child(widget::spacer()))
777 .child(widget::spacer().dock(Dock::Right).min_w(10.0)),
778 );
779 let node = compute(&root, 400.0, 400.0);
780 let grown = &node.children[1];
781 assert_eq!(r(grown), (0.0, 20.0, 200.0, 180.0));
782 assert_eq!(r(&grown.children[0]), (0.0, 20.0, 50.0, 90.0));
783 assert_eq!(r(&grown.children[1]).2, 200.0 - 50.0 - 10.0);
785 assert_eq!(r(&grown.children[2]), (190.0, 20.0, 10.0, 180.0));
787 }
788
789 #[test]
790 fn grown_column_panel_fills_its_parent_and_children_grow_too() {
791 let root = widget::panel(FlexDir::Column)
792 .w(100.0)
793 .h(100.0)
794 .gap(0.0)
795 .child(
796 widget::panel(FlexDir::Column)
797 .flex(1.0)
798 .gap(0.0)
799 .child(widget::spacer().flex(1.0)),
800 );
801 let node = compute(&root, 100.0, 100.0);
802 assert_eq!(node.children[0].rect.h, 100.0);
803 assert_eq!(node.children[0].children[0].rect.h, 100.0);
804 }
805
806 #[test]
807 fn empty_auto_panel_in_a_row_takes_its_min_width() {
808 let root = widget::panel(FlexDir::Row)
809 .w(100.0)
810 .h(20.0)
811 .child(widget::panel(FlexDir::Column).min_w(7.0))
812 .child(widget::spacer());
813 let node = compute(&root, 100.0, 20.0);
814 assert_eq!(node.children[0].rect.w, 7.0);
815 assert_eq!(node.children[1].rect.x, 9.0);
816 }
817
818 #[test]
819 fn padding_counts_once_in_auto_size() {
820 let root = widget::panel(FlexDir::Column)
821 .padding(3.0, 4.0, 5.0, 6.0)
822 .child(widget::spacer().min_w(10.0).min_h(10.0));
823 let node = compute(&root, 300.0, 300.0);
824 assert_eq!((node.rect.w, node.rect.h), (20.0, 18.0));
825 assert_eq!((node.children[0].rect.x, node.children[0].rect.y), (6.0, 3.0));
826 }
827
828 #[test]
829 fn auto_row_in_a_column_measures_to_content() {
830 let root = widget::panel(FlexDir::Column).w(200.0).h(200.0).child(
832 widget::panel(FlexDir::Row)
833 .align(Align::Center)
834 .gap(0.0)
835 .child(widget::spacer().min_w(10.0).min_h(10.0))
836 .child(widget::spacer().min_w(10.0).min_h(20.0))
837 .child(widget::spacer().min_w(5.0).dock(Dock::Right)),
838 );
839 let node = compute(&root, 200.0, 200.0);
840 let row = &node.children[0];
841 assert_eq!(row.rect.h, 20.0);
842 assert_eq!(row.children[0].rect.y, 5.0);
843 assert_eq!(row.children[1].rect.y, 0.0);
844 }
845
846 #[test]
847 fn row_with_right_dock_measured_on_unbounded_axis() {
848 let root = widget::panel(FlexDir::Row).w(300.0).h(20.0).child(
851 widget::panel(FlexDir::Row)
852 .gap(1.0)
853 .child(widget::spacer().min_w(10.0))
854 .child(widget::spacer().min_w(5.0).dock(Dock::Right))
855 .child(widget::spacer().min_w(7.0).dock(Dock::Right)),
856 );
857 let node = compute(&root, 300.0, 20.0);
858 let inner = &node.children[0];
859 assert_eq!(inner.rect.w, 10.0 + 1.0 + 7.0 + 1.0 + 5.0);
860 assert_eq!(inner.children[1].rect.x, 19.0);
861 assert_eq!(inner.children[2].rect.x, 11.0);
862 }
863
864 #[test]
865 fn align_center_and_end_in_both_directions() {
866 let col = widget::panel(FlexDir::Column)
867 .w(100.0)
868 .h(50.0)
869 .align(Align::Center)
870 .child(widget::spacer().min_w(20.0).min_h(5.0));
871 assert_eq!(compute(&col, 100.0, 50.0).children[0].rect.x, 40.0);
872 let row = widget::panel(FlexDir::Row)
873 .w(100.0)
874 .h(50.0)
875 .padding(10.0, 0.0, 0.0, 0.0)
876 .align(Align::End)
877 .child(widget::spacer().min_w(20.0).h(pct(20.0)));
878 assert_eq!(compute(&row, 100.0, 50.0).children[0].rect.y, 10.0 + 32.0);
880 }
881
882 #[test]
883 fn positioned_children_with_percent_sizes() {
884 let root = widget::panel(FlexDir::Column)
885 .w(200.0)
886 .h(100.0)
887 .padding(10.0, 10.0, 10.0, 10.0)
888 .child(widget::spacer().w(pct(50.0)).h(pct(50.0)).at(20.0, 10.0))
889 .child(
890 widget::panel(FlexDir::Row)
891 .w(pct(100.0))
892 .h(pct(25.0))
893 .at(0.0, 60.0)
894 .gap(0.0)
895 .child(widget::spacer().w(pct(50.0)))
896 .child(widget::spacer().flex(1.0)),
897 );
898 let node = compute(&root, 400.0, 400.0);
899 assert_eq!(r(&node.children[0]), (30.0, 20.0, 90.0, 40.0));
901 let p = &node.children[1];
904 assert_eq!(r(p), (10.0, 70.0, 180.0, 20.0));
905 assert_eq!(r(&p.children[0]), (10.0, 70.0, 90.0, 4.0));
906 assert_eq!(r(&p.children[1]), (100.0, 70.0, 90.0, 4.0));
907 }
908
909 #[test]
910 fn growing_scroll_view_fills_free_space_and_scrolls_the_rest() {
911 let list = widget::scroll_view()
912 .id("list")
913 .flex(1.0)
914 .w(pct(100.0))
915 .gap(0.0)
916 .padding(2.0, 2.0, 2.0, 2.0)
917 .scroll_y(1000.0)
918 .children((0..10).map(|_| widget::spacer().w(pct(100.0)).min_h(20.0)));
919 let root = widget::panel(FlexDir::Column)
920 .w(100.0)
921 .h(120.0)
922 .gap(0.0)
923 .child(widget::spacer().min_h(20.0))
924 .child(list);
925 let node = compute(&root, 100.0, 120.0);
926 let v = find_node(&node, "list").unwrap();
927 assert_eq!(r(v), (0.0, 20.0, 100.0, 100.0));
928 assert_eq!(v.content_h, 204.0);
929 assert_eq!(v.scroll_y, 104.0);
930 assert_eq!(v.children[0].rect.w, 100.0 - 4.0 - SCROLLBAR_W);
932 let last = v.children[9].rect;
934 assert_eq!(last.y + last.h, 20.0 + 100.0 - 2.0);
935 assert!(v.scrollbar_rects().is_some());
936 }
937
938 #[test]
939 fn label_wraps_at_its_explicit_width() {
940 let root = widget::panel(FlexDir::Column)
941 .w(300.0)
942 .child(widget::label("aaaa bbbb cccc").w(40.0));
943 let node = compute(&root, 300.0, 300.0);
944 let l = node.children[0].rect;
945 assert_eq!(l.w, 40.0);
946 assert!(l.h > text::LINE_H, "wrapped onto several lines: {l:?}");
947 }
948
949 #[test]
950 fn percent_caps_at_the_cross_axis_and_ignores_unbounded_basis() {
951 let root = widget::panel(FlexDir::Column)
953 .w(100.0)
954 .h(100.0)
955 .child(widget::spacer().w(pct(150.0)));
956 assert_eq!(compute(&root, 100.0, 100.0).children[0].rect.w, 100.0);
957 let root = widget::panel(FlexDir::Row).w(200.0).h(20.0).child(
960 widget::panel(FlexDir::Row)
961 .gap(0.0)
962 .child(widget::spacer().min_w(40.0))
963 .child(widget::spacer().w(pct(50.0))),
964 );
965 let node = compute(&root, 200.0, 20.0);
966 let inner = &node.children[0];
967 assert_eq!(inner.rect.w, 44.0);
968 assert_eq!(inner.children[1].rect.w, 22.0);
969 }
970
971 #[test]
972 fn find_mut_and_set_text() {
973 let mut root = widget::panel(FlexDir::Column).child(widget::label("a").id("title"));
974 assert!(root.find_mut("title").unwrap().set_text("b"));
975 match &root.children[0].kind {
976 crate::widget::WidgetKind::Label(t) => assert_eq!(t, "b"),
977 _ => unreachable!(),
978 }
979 assert!(root.find_mut("missing").is_none());
980 }
981}