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::McImage { img_w, img_h, .. } => {
257 ww = (*img_w + s.pad[1] + s.pad[3]).max(s.min_w).min(max_w);
258 hh = (*img_h + s.pad[0] + s.pad[2]).max(s.min_h).min(max_h);
259 }
260 }
261 if let Some(v) = explicit_w {
263 ww = v;
264 }
265 if let Some(v) = explicit_h {
266 hh = v;
267 }
268 }
269
270 node.rect = Rect { x, y, w: ww, h: hh };
271 node.content_h = hh;
272 node.scroll_y = 0.0;
273 node.clip = s.clip;
274 node.scrollbar = s.scrollbar;
275
276 if has_children {
277 let shrink = Shrink {
278 w: explicit_w.is_none(),
279 h: explicit_h.is_none(),
280 max_w,
281 max_h,
282 };
283 layout_children(w, node, Some(shrink));
284 }
285}
286
287fn arrange(w: &Widget, node: &mut LayoutNode, x: f32, y: f32) {
290 node.rect.x = x;
291 node.rect.y = y;
292 node.content_h = node.rect.h;
293 if !w.children.is_empty() {
294 layout_children(w, node, None);
295 }
296}
297
298struct Shrink {
301 w: bool,
302 h: bool,
303 max_w: f32,
304 max_h: f32,
305}
306
307fn layout_children(w: &Widget, node: &mut LayoutNode, shrink: Option<Shrink>) {
310 let s = &w.style;
311 let Rect {
312 x,
313 y,
314 w: ww,
315 h: hh,
316 } = node.rect;
317
318 let dir = if matches!(w.kind, WidgetKind::Panel(_)) && w.flex_dir == FlexDir::Row {
319 FlexDir::Row
320 } else {
321 FlexDir::Column
322 };
323 let bar_w = if s.scrollbar { SCROLLBAR_W } else { 0.0 };
324 let content_w = if bounded(ww) {
325 (ww - s.pad[1] - s.pad[3] - bar_w).max(0.0)
326 } else {
327 UNBOUNDED
328 };
329 let content_h = if bounded(hh) {
330 (hh - s.pad[0] - s.pad[2]).max(0.0)
331 } else {
332 UNBOUNDED
333 };
334 let (main_bounded, cross_bounded) = match dir {
335 FlexDir::Row => (bounded(content_w), bounded(content_h)),
336 FlexDir::Column => (bounded(content_h), bounded(content_w)),
337 };
338 let cross_offset = |content: f32, size: f32| -> f32 {
339 if !cross_bounded {
340 return 0.0;
341 }
342 match s.align {
343 Align::Center => (content - size) / 2.0,
344 Align::End => content - size,
345 Align::Start => 0.0,
346 }
347 };
348
349 let effective_flex = |child: &Widget| -> f32 {
352 if child.style.dock == Dock::Fill {
353 child.style.flex.max(1.0)
354 } else {
355 child.style.flex
356 }
357 };
358 let in_flow = |child: &Widget| -> bool {
360 child.style.pos.is_none()
361 && !matches!(
362 (dir, child.style.dock),
363 (FlexDir::Row, Dock::Right) | (FlexDir::Column, Dock::Bottom)
364 )
365 };
366 let stretches = |dock: Dock| -> bool {
367 cross_bounded
368 && match dir {
369 FlexDir::Row => matches!(dock, Dock::Fill | Dock::Left | Dock::Right),
370 FlexDir::Column => matches!(dock, Dock::Fill | Dock::Top | Dock::Bottom),
371 }
372 };
373
374 let mut child_nodes: Vec<LayoutNode> = Vec::with_capacity(w.children.len());
376 let mut total_flex: f32 = 0.0;
377 let mut used_main: f32 = 0.0;
378
379 for child in &w.children {
380 let mut cn = new_node(child);
381 if let Some((px, py)) = child.style.pos {
385 let mw = if bounded(content_w) {
386 (content_w - px).max(0.0)
387 } else {
388 UNBOUNDED
389 };
390 let mh = if bounded(content_h) {
391 (content_h - py).max(0.0)
392 } else {
393 UNBOUNDED
394 };
395 layout_widget(child, &mut cn, 0.0, 0.0, mw, mh, content_w, content_h);
396 child_nodes.push(cn);
397 continue;
398 }
399 let (cmw, cmh) = match (dir, child.style.dock) {
402 (_, Dock::Fill) => (content_w, content_h),
403 (FlexDir::Row, _) => (UNBOUNDED, content_h),
404 (FlexDir::Column, _) => (content_w, UNBOUNDED),
405 };
406 layout_widget(child, &mut cn, 0.0, 0.0, cmw, cmh, content_w, content_h);
407 let ef = effective_flex(child);
408 if ef > 0.0 && child.style.clip && main_bounded && in_flow(child) {
410 match dir {
411 FlexDir::Row if child.style.w.is_auto() => cn.rect.w = child.style.min_w,
412 FlexDir::Column if child.style.h.is_auto() => cn.rect.h = child.style.min_h,
413 _ => {}
414 }
415 }
416 used_main += match dir {
419 FlexDir::Row => cn.rect.w,
420 FlexDir::Column => cn.rect.h,
421 };
422 if in_flow(child) {
423 total_flex += ef;
424 }
425 child_nodes.push(cn);
426 }
427 let placed = w.children.iter().filter(|c| c.style.pos.is_none()).count();
428 used_main += s.gap * (placed.saturating_sub(1) as f32);
429
430 let available = if main_bounded {
432 (match dir {
433 FlexDir::Row => content_w,
434 FlexDir::Column => content_h,
435 }) - used_main
436 } else {
437 0.0
438 };
439 let mut pos = match dir {
440 FlexDir::Row => s.pad[3],
441 FlexDir::Column => s.pad[0],
442 };
443
444 for (i, child) in w.children.iter().enumerate() {
446 if !in_flow(child) {
447 continue;
448 }
449 let cn = &mut child_nodes[i];
450 let ef = effective_flex(child);
451 let grow = if ef > 0.0 && total_flex > 0.0 && available > 0.0 {
452 available * ef / total_flex
453 } else {
454 0.0
455 };
456 let stretch = stretches(child.style.dock);
457 let (cx, cy) = if dir == FlexDir::Row {
458 cn.rect.w += grow;
459 if stretch {
460 cn.rect.h = content_h;
461 }
462 let p = (x + pos, y + s.pad[0] + cross_offset(content_h, cn.rect.h));
463 pos += cn.rect.w + s.gap;
464 p
465 } else {
466 cn.rect.h += grow;
467 if stretch {
468 cn.rect.w = content_w;
469 }
470 let p = (x + s.pad[3] + cross_offset(content_w, cn.rect.w), y + pos);
471 pos += cn.rect.h + s.gap;
472 p
473 };
474 arrange(child, cn, cx, cy);
475 }
476
477 let mut rpos = if main_bounded {
481 match dir {
482 FlexDir::Row => x + s.pad[3] + content_w,
483 FlexDir::Column => y + s.pad[0] + content_h,
484 }
485 } else {
486 let docked: Vec<f32> = w
487 .children
488 .iter()
489 .zip(&child_nodes)
490 .filter(|(c, _)| !in_flow(c) && c.style.pos.is_none())
491 .map(|(_, n)| match dir {
492 FlexDir::Row => n.rect.w,
493 FlexDir::Column => n.rect.h,
494 })
495 .collect();
496 let span: f32 =
497 docked.iter().sum::<f32>() + s.gap * (docked.len().saturating_sub(1) as f32);
498 (match dir {
499 FlexDir::Row => x,
500 FlexDir::Column => y,
501 }) + pos
502 + span
503 };
504 for (i, child) in w.children.iter().enumerate() {
505 if in_flow(child) || child.style.pos.is_some() {
506 continue;
507 }
508 let cn = &mut child_nodes[i];
509 let stretch = stretches(child.style.dock);
510 let (cx, cy) = if dir == FlexDir::Row {
511 if stretch {
512 cn.rect.h = content_h;
513 }
514 rpos -= cn.rect.w;
515 (rpos, y + s.pad[0] + cross_offset(content_h, cn.rect.h))
516 } else {
517 if stretch {
518 cn.rect.w = content_w;
519 }
520 rpos -= cn.rect.h;
521 (x + s.pad[3] + cross_offset(content_w, cn.rect.w), rpos)
522 };
523 rpos -= s.gap;
524 arrange(child, cn, cx, cy);
525 }
526
527 for (i, child) in w.children.iter().enumerate() {
529 let Some((px, py)) = child.style.pos else {
530 continue;
531 };
532 let cn = &mut child_nodes[i];
533 arrange(child, cn, x + s.pad[3] + px, y + s.pad[0] + py);
534 }
535
536 let ch: f32 = child_nodes
538 .iter()
539 .map(|c| c.rect.y - y + c.rect.h)
540 .fold(0.0f32, f32::max);
541 if let Some(fit) = shrink {
542 if fit.w {
543 let cw: f32 = child_nodes
544 .iter()
545 .map(|c| c.rect.x - x + c.rect.w)
546 .fold(0.0f32, f32::max);
547 node.rect.w = (cw + s.pad[1] + bar_w).max(s.min_w).min(fit.max_w);
548 }
549 if fit.h {
550 node.rect.h = (ch + s.pad[2]).max(s.min_h).min(fit.max_h);
551 }
552 }
553
554 node.content_h = ch + s.pad[2];
557 node.scroll_y = s.scroll_y.clamp(0.0, node.max_scroll());
558 if node.scroll_y > 0.0 {
559 for c in &mut child_nodes {
560 c.translate_y(-node.scroll_y);
561 }
562 }
563
564 node.children = child_nodes;
565}
566
567pub fn hit_test(node: &LayoutNode, mx: f32, my: f32) -> Option<&LayoutNode> {
571 if !node.rect.contains(mx, my) {
572 return None;
573 }
574 for child in node.children.iter().rev() {
575 if let Some(hit) = hit_test(child, mx, my) {
576 return Some(hit);
577 }
578 }
579 if node.on_click.is_some() && node.enabled {
580 Some(node)
581 } else {
582 None
583 }
584}
585
586pub fn find_by_id(node: &LayoutNode, id: &str) -> Option<Rect> {
591 if node.id.as_deref() == Some(id) {
592 return Some(node.rect);
593 }
594 node.children.iter().find_map(|c| find_by_id(c, id))
595}
596
597pub fn find_node<'a>(node: &'a LayoutNode, id: &str) -> Option<&'a LayoutNode> {
600 if node.id.as_deref() == Some(id) {
601 return Some(node);
602 }
603 node.children.iter().find_map(|c| find_node(c, id))
604}
605
606pub fn set_focus(node: &mut LayoutNode, focused_id: Option<&str>) {
608 node.focused = focused_id.is_some() && node.id.as_deref() == focused_id;
609 for child in &mut node.children {
610 set_focus(child, focused_id);
611 }
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617 use crate::widget::{self, Units};
618
619 #[test]
620 fn positioned_children_leave_the_flow() {
621 let root = widget::panel(FlexDir::Column)
622 .w(200.0)
623 .h(100.0)
624 .padding(4.0, 4.0, 4.0, 4.0)
625 .child(widget::spacer().min_w(10.0).min_h(10.0))
626 .child(widget::button("Hi").w(40.0).h(20.0).at(50.0, 30.0))
627 .child(widget::spacer().min_w(10.0).min_h(10.0));
628 let node = compute(&root, 400.0, 400.0);
629 let r = node.children[1].rect;
630 assert_eq!((r.x, r.y, r.w, r.h), (54.0, 34.0, 40.0, 20.0));
631 assert_eq!(node.children[0].rect.y, 4.0);
633 assert_eq!(node.children[2].rect.y, 4.0 + 10.0 + 2.0);
634 }
635
636 #[test]
637 fn positioned_container_lays_out_its_children_in_place() {
638 let root = widget::panel(FlexDir::Column)
639 .w(Units::Percent(100.0))
640 .h(Units::Percent(100.0))
641 .child(
642 widget::panel(FlexDir::Row)
643 .w(100.0)
644 .h(20.0)
645 .gap(0.0)
646 .at(10.0, 20.0)
647 .child(widget::spacer().min_w(30.0).min_h(5.0))
648 .child(widget::spacer().min_w(30.0).min_h(5.0)),
649 );
650 let node = compute(&root, 320.0, 240.0);
651 let panel = &node.children[0];
652 assert_eq!((panel.rect.x, panel.rect.y), (10.0, 20.0));
653 assert_eq!(panel.children[1].rect.x, 40.0);
654 assert_eq!(panel.children[1].rect.y, 20.0);
655 }
656
657 #[test]
658 fn auto_sized_parent_grows_to_fit_positioned_children() {
659 let root = widget::panel(FlexDir::Column)
660 .child(widget::spacer().min_w(10.0).min_h(10.0).at(30.0, 40.0));
661 let node = compute(&root, 300.0, 300.0);
662 assert_eq!((node.rect.w, node.rect.h), (40.0, 50.0));
663 }
664
665 fn pct(v: f32) -> Units {
666 Units::Percent(v)
667 }
668
669 fn r(n: &LayoutNode) -> (f32, f32, f32, f32) {
670 (n.rect.x, n.rect.y, n.rect.w, n.rect.h)
671 }
672
673 #[test]
674 fn percent_width_in_a_row_resolves_once_against_the_content_box() {
675 let root = widget::panel(FlexDir::Row)
676 .w(400.0)
677 .h(100.0)
678 .padding(5.0, 10.0, 5.0, 10.0)
679 .child(
680 widget::panel(FlexDir::Column)
681 .w(pct(50.0))
682 .child(widget::spacer().min_w(10.0).min_h(10.0)),
683 )
684 .child(widget::spacer().w(pct(25.0)).h(pct(50.0)));
685 let node = compute(&root, 800.0, 600.0);
686 assert_eq!(r(&node.children[0]).2, 190.0);
688 assert_eq!(r(&node.children[1]), (10.0 + 190.0 + 2.0, 5.0, 95.0, 45.0));
689 }
690
691 #[test]
692 fn percent_sizes_in_a_column() {
693 let root = widget::panel(FlexDir::Column)
694 .w(200.0)
695 .h(300.0)
696 .padding(10.0, 0.0, 10.0, 0.0)
697 .gap(0.0)
698 .child(
699 widget::panel(FlexDir::Row)
700 .w(pct(50.0))
701 .h(pct(50.0))
702 .child(widget::spacer()),
703 )
704 .child(widget::spacer().w(pct(100.0)).h(pct(10.0)));
705 let node = compute(&root, 800.0, 600.0);
706 assert_eq!(r(&node.children[0]), (0.0, 10.0, 100.0, 140.0));
707 assert_eq!(r(&node.children[1]), (0.0, 150.0, 200.0, 28.0));
708 }
709
710 #[test]
711 fn nested_percents_compound_per_level_only() {
712 let root = widget::panel(FlexDir::Row).w(400.0).h(200.0).child(
713 widget::panel(FlexDir::Column)
714 .w(pct(50.0))
715 .h(pct(100.0))
716 .child(
717 widget::panel(FlexDir::Row)
718 .w(pct(50.0))
719 .h(pct(50.0))
720 .child(widget::spacer().w(pct(50.0)).h(pct(50.0))),
721 ),
722 );
723 let node = compute(&root, 400.0, 200.0);
724 let a = &node.children[0];
725 let b = &a.children[0];
726 let c = &b.children[0];
727 assert_eq!(r(a), (0.0, 0.0, 200.0, 200.0));
728 assert_eq!(r(b), (0.0, 0.0, 100.0, 100.0));
729 assert_eq!(r(c), (0.0, 0.0, 50.0, 50.0));
730 }
731
732 #[test]
733 fn percent_resolves_against_the_scrollbar_free_box() {
734 let root = widget::scroll_view()
735 .w(106.0)
736 .h(50.0)
737 .child(widget::spacer().w(pct(50.0)).h(10.0));
738 let node = compute(&root, 400.0, 400.0);
739 assert_eq!(node.children[0].rect.w, 50.0);
740 }
741
742 #[test]
743 fn grow_shares_free_space_with_percent_siblings() {
744 let root = widget::panel(FlexDir::Row)
745 .w(300.0)
746 .h(50.0)
747 .gap(4.0)
748 .child(widget::spacer().w(pct(50.0)))
749 .child(widget::panel(FlexDir::Column).flex(1.0).child(widget::spacer()))
750 .child(widget::spacer().w(pct(10.0)));
751 let node = compute(&root, 300.0, 50.0);
752 assert_eq!(r(&node.children[0]).2, 150.0);
754 assert_eq!(r(&node.children[1]), (154.0, 0.0, 112.0, 4.0));
755 assert_eq!(r(&node.children[2]), (270.0, 0.0, 30.0, 4.0));
756 }
757
758 #[test]
759 fn grown_panel_keeps_its_size_and_lays_children_out_against_it() {
760 let root = widget::panel(FlexDir::Column)
761 .w(200.0)
762 .h(200.0)
763 .gap(0.0)
764 .child(widget::spacer().min_h(20.0))
765 .child(
766 widget::panel(FlexDir::Row)
767 .flex(1.0)
768 .w(pct(100.0))
769 .gap(0.0)
770 .child(widget::spacer().w(pct(25.0)).h(pct(50.0)))
771 .child(widget::panel(FlexDir::Column).flex(1.0).child(widget::spacer()))
772 .child(widget::spacer().dock(Dock::Right).min_w(10.0)),
773 );
774 let node = compute(&root, 400.0, 400.0);
775 let grown = &node.children[1];
776 assert_eq!(r(grown), (0.0, 20.0, 200.0, 180.0));
777 assert_eq!(r(&grown.children[0]), (0.0, 20.0, 50.0, 90.0));
778 assert_eq!(r(&grown.children[1]).2, 200.0 - 50.0 - 10.0);
780 assert_eq!(r(&grown.children[2]), (190.0, 20.0, 10.0, 180.0));
782 }
783
784 #[test]
785 fn grown_column_panel_fills_its_parent_and_children_grow_too() {
786 let root = widget::panel(FlexDir::Column)
787 .w(100.0)
788 .h(100.0)
789 .gap(0.0)
790 .child(
791 widget::panel(FlexDir::Column)
792 .flex(1.0)
793 .gap(0.0)
794 .child(widget::spacer().flex(1.0)),
795 );
796 let node = compute(&root, 100.0, 100.0);
797 assert_eq!(node.children[0].rect.h, 100.0);
798 assert_eq!(node.children[0].children[0].rect.h, 100.0);
799 }
800
801 #[test]
802 fn empty_auto_panel_in_a_row_takes_its_min_width() {
803 let root = widget::panel(FlexDir::Row)
804 .w(100.0)
805 .h(20.0)
806 .child(widget::panel(FlexDir::Column).min_w(7.0))
807 .child(widget::spacer());
808 let node = compute(&root, 100.0, 20.0);
809 assert_eq!(node.children[0].rect.w, 7.0);
810 assert_eq!(node.children[1].rect.x, 9.0);
811 }
812
813 #[test]
814 fn padding_counts_once_in_auto_size() {
815 let root = widget::panel(FlexDir::Column)
816 .padding(3.0, 4.0, 5.0, 6.0)
817 .child(widget::spacer().min_w(10.0).min_h(10.0));
818 let node = compute(&root, 300.0, 300.0);
819 assert_eq!((node.rect.w, node.rect.h), (20.0, 18.0));
820 assert_eq!((node.children[0].rect.x, node.children[0].rect.y), (6.0, 3.0));
821 }
822
823 #[test]
824 fn auto_row_in_a_column_measures_to_content() {
825 let root = widget::panel(FlexDir::Column).w(200.0).h(200.0).child(
827 widget::panel(FlexDir::Row)
828 .align(Align::Center)
829 .gap(0.0)
830 .child(widget::spacer().min_w(10.0).min_h(10.0))
831 .child(widget::spacer().min_w(10.0).min_h(20.0))
832 .child(widget::spacer().min_w(5.0).dock(Dock::Right)),
833 );
834 let node = compute(&root, 200.0, 200.0);
835 let row = &node.children[0];
836 assert_eq!(row.rect.h, 20.0);
837 assert_eq!(row.children[0].rect.y, 5.0);
838 assert_eq!(row.children[1].rect.y, 0.0);
839 }
840
841 #[test]
842 fn row_with_right_dock_measured_on_unbounded_axis() {
843 let root = widget::panel(FlexDir::Row).w(300.0).h(20.0).child(
846 widget::panel(FlexDir::Row)
847 .gap(1.0)
848 .child(widget::spacer().min_w(10.0))
849 .child(widget::spacer().min_w(5.0).dock(Dock::Right))
850 .child(widget::spacer().min_w(7.0).dock(Dock::Right)),
851 );
852 let node = compute(&root, 300.0, 20.0);
853 let inner = &node.children[0];
854 assert_eq!(inner.rect.w, 10.0 + 1.0 + 7.0 + 1.0 + 5.0);
855 assert_eq!(inner.children[1].rect.x, 19.0);
856 assert_eq!(inner.children[2].rect.x, 11.0);
857 }
858
859 #[test]
860 fn align_center_and_end_in_both_directions() {
861 let col = widget::panel(FlexDir::Column)
862 .w(100.0)
863 .h(50.0)
864 .align(Align::Center)
865 .child(widget::spacer().min_w(20.0).min_h(5.0));
866 assert_eq!(compute(&col, 100.0, 50.0).children[0].rect.x, 40.0);
867 let row = widget::panel(FlexDir::Row)
868 .w(100.0)
869 .h(50.0)
870 .padding(10.0, 0.0, 0.0, 0.0)
871 .align(Align::End)
872 .child(widget::spacer().min_w(20.0).h(pct(20.0)));
873 assert_eq!(compute(&row, 100.0, 50.0).children[0].rect.y, 10.0 + 32.0);
875 }
876
877 #[test]
878 fn positioned_children_with_percent_sizes() {
879 let root = widget::panel(FlexDir::Column)
880 .w(200.0)
881 .h(100.0)
882 .padding(10.0, 10.0, 10.0, 10.0)
883 .child(widget::spacer().w(pct(50.0)).h(pct(50.0)).at(20.0, 10.0))
884 .child(
885 widget::panel(FlexDir::Row)
886 .w(pct(100.0))
887 .h(pct(25.0))
888 .at(0.0, 60.0)
889 .gap(0.0)
890 .child(widget::spacer().w(pct(50.0)))
891 .child(widget::spacer().flex(1.0)),
892 );
893 let node = compute(&root, 400.0, 400.0);
894 assert_eq!(r(&node.children[0]), (30.0, 20.0, 90.0, 40.0));
896 let p = &node.children[1];
899 assert_eq!(r(p), (10.0, 70.0, 180.0, 20.0));
900 assert_eq!(r(&p.children[0]), (10.0, 70.0, 90.0, 4.0));
901 assert_eq!(r(&p.children[1]), (100.0, 70.0, 90.0, 4.0));
902 }
903
904 #[test]
905 fn growing_scroll_view_fills_free_space_and_scrolls_the_rest() {
906 let list = widget::scroll_view()
907 .id("list")
908 .flex(1.0)
909 .w(pct(100.0))
910 .gap(0.0)
911 .padding(2.0, 2.0, 2.0, 2.0)
912 .scroll_y(1000.0)
913 .children((0..10).map(|_| widget::spacer().w(pct(100.0)).min_h(20.0)));
914 let root = widget::panel(FlexDir::Column)
915 .w(100.0)
916 .h(120.0)
917 .gap(0.0)
918 .child(widget::spacer().min_h(20.0))
919 .child(list);
920 let node = compute(&root, 100.0, 120.0);
921 let v = find_node(&node, "list").unwrap();
922 assert_eq!(r(v), (0.0, 20.0, 100.0, 100.0));
923 assert_eq!(v.content_h, 204.0);
924 assert_eq!(v.scroll_y, 104.0);
925 assert_eq!(v.children[0].rect.w, 100.0 - 4.0 - SCROLLBAR_W);
927 let last = v.children[9].rect;
929 assert_eq!(last.y + last.h, 20.0 + 100.0 - 2.0);
930 assert!(v.scrollbar_rects().is_some());
931 }
932
933 #[test]
934 fn label_wraps_at_its_explicit_width() {
935 let root = widget::panel(FlexDir::Column)
936 .w(300.0)
937 .child(widget::label("aaaa bbbb cccc").w(40.0));
938 let node = compute(&root, 300.0, 300.0);
939 let l = node.children[0].rect;
940 assert_eq!(l.w, 40.0);
941 assert!(l.h > text::LINE_H, "wrapped onto several lines: {l:?}");
942 }
943
944 #[test]
945 fn percent_caps_at_the_cross_axis_and_ignores_unbounded_basis() {
946 let root = widget::panel(FlexDir::Column)
948 .w(100.0)
949 .h(100.0)
950 .child(widget::spacer().w(pct(150.0)));
951 assert_eq!(compute(&root, 100.0, 100.0).children[0].rect.w, 100.0);
952 let root = widget::panel(FlexDir::Row).w(200.0).h(20.0).child(
955 widget::panel(FlexDir::Row)
956 .gap(0.0)
957 .child(widget::spacer().min_w(40.0))
958 .child(widget::spacer().w(pct(50.0))),
959 );
960 let node = compute(&root, 200.0, 20.0);
961 let inner = &node.children[0];
962 assert_eq!(inner.rect.w, 44.0);
963 assert_eq!(inner.children[1].rect.w, 22.0);
964 }
965
966 #[test]
967 fn find_mut_and_set_text() {
968 let mut root = widget::panel(FlexDir::Column).child(widget::label("a").id("title"));
969 assert!(root.find_mut("title").unwrap().set_text("b"));
970 match &root.children[0].kind {
971 crate::widget::WidgetKind::Label(t) => assert_eq!(t, "b"),
972 _ => unreachable!(),
973 }
974 assert!(root.find_mut("missing").is_none());
975 }
976}