1#![allow(
13 dead_code,
14 reason = "P1 delivers the vocabulary before P2-P4 consume it; the allow \
15 goes when the first surface migrates"
16)]
17
18use egui_taffy::taffy;
19use egui_taffy::taffy::prelude::{auto, length, percent};
20use egui_taffy::{TuiBuilderLogic, TuiWidget, tui};
21
22use crate::bounded::{Bounded, Bounds};
23
24pub use egui_taffy::Tui;
25
26pub const MAX_PASSES: core::num::NonZeroUsize = core::num::NonZeroUsize::MIN.saturating_add(2);
32
33pub struct PointBounds;
34
35impl Bounds for PointBounds {
36 const MIN: f32 = 0.0;
37 const MAX: f32 = f32::INFINITY;
38}
39
40pub type Points = Bounded<PointBounds>;
44
45impl Points {
46 pub const ZERO: Self = Self::new(0.0);
47}
48
49#[derive(Clone, Copy, PartialEq, Eq, Debug)]
53pub enum Room {
54 Width,
55 Height,
56 Both,
57}
58
59impl Room {
60 fn spans_width(self) -> bool {
61 matches!(self, Room::Width | Room::Both)
62 }
63
64 fn spans_height(self) -> bool {
65 matches!(self, Room::Height | Room::Both)
66 }
67}
68
69#[derive(Clone, Debug, Default, PartialEq)]
74pub struct Flex(taffy::Style);
75
76pub fn row(gap: Points) -> Flex {
79 line(taffy::FlexDirection::Row, taffy::AlignItems::Center, gap)
80}
81
82pub fn row_start(gap: Points) -> Flex {
85 line(taffy::FlexDirection::Row, taffy::AlignItems::FlexStart, gap)
86}
87
88pub fn stack(gap: Points) -> Flex {
90 line(
91 taffy::FlexDirection::Column,
92 taffy::AlignItems::Stretch,
93 gap,
94 )
95}
96
97pub fn wrap(gap: Points) -> Flex {
100 let mut flex = line(taffy::FlexDirection::Row, taffy::AlignItems::FlexStart, gap);
101 flex.0.flex_wrap = taffy::FlexWrap::Wrap;
102 flex
103}
104
105pub fn grow() -> Flex {
108 Flex::default().grow()
109}
110
111pub fn fixed() -> Flex {
113 Flex::default().fixed()
114}
115
116pub fn centre(gap: Points) -> Flex {
120 row(gap).grow().centred()
121}
122
123pub fn cell(side: Points) -> Flex {
125 row(Points::ZERO).fixed().square(side).centred()
126}
127
128pub fn trailing(gap: Points) -> Flex {
130 let mut flex = stack(gap).fixed();
131 flex.0.align_items = Some(taffy::AlignItems::FlexEnd);
132 flex
133}
134
135pub fn scrolling() -> Flex {
139 stack(Points::ZERO).grow().scrolls()
140}
141
142fn line(direction: taffy::FlexDirection, align: taffy::AlignItems, gap: Points) -> Flex {
143 Flex(taffy::Style {
144 display: taffy::Display::Flex,
145 flex_direction: direction,
146 align_items: Some(align),
147 gap: length(f32::from(gap)),
148 ..Default::default()
149 })
150}
151
152impl Flex {
153 pub fn grow(mut self) -> Self {
155 self.0.flex_grow = 1.0;
156 self.0.flex_shrink = 1.0;
157 self.0.flex_basis = length(0.0);
158 self.0.min_size.width = length(0.0);
159 self
160 }
161
162 pub fn fixed(mut self) -> Self {
164 self.0.flex_grow = 0.0;
165 self.0.flex_shrink = 0.0;
166 self.0.flex_basis = auto();
167 self
168 }
169
170 pub fn centred(mut self) -> Self {
173 self.0.justify_content = Some(taffy::JustifyContent::Center);
174 self.0.align_items = Some(taffy::AlignItems::Center);
175 self
176 }
177
178 pub fn min_height(mut self, points: Points) -> Self {
179 self.0.min_size.height = length(f32::from(points));
180 self
181 }
182
183 pub fn width(mut self, points: Points) -> Self {
184 self.0.size.width = length(f32::from(points));
185 self
186 }
187
188 pub fn height(mut self, points: Points) -> Self {
189 self.0.size.height = length(f32::from(points));
190 self
191 }
192
193 pub fn square(self, side: Points) -> Self {
194 self.width(side).height(side)
195 }
196
197 pub fn pad(mut self, points: Points) -> Self {
198 self.0.padding = length(f32::from(points));
199 self
200 }
201
202 pub fn scrolls(mut self) -> Self {
206 self.0.overflow = taffy::Point {
207 x: taffy::Overflow::Clip,
208 y: taffy::Overflow::Scroll,
209 };
210 self
211 }
212
213 fn spanning(mut self, room: Room) -> Self {
214 if room.spans_width() {
215 self.0.size.width = percent(1.0);
216 }
217 if room.spans_height() {
218 self.0.size.height = percent(1.0);
219 }
220 self
221 }
222
223 pub fn style(&self) -> &taffy::Style {
224 &self.0
225 }
226}
227
228impl From<Flex> for taffy::Style {
229 fn from(flex: Flex) -> taffy::Style {
230 flex.0
231 }
232}
233
234pub fn surface<T>(
241 ui: &mut egui::Ui,
242 name: &'static str,
243 room: Room,
244 style: Flex,
245 add: impl FnOnce(&mut Tui) -> T,
246) -> T {
247 let _span = tracing::info_span!("taffy_layout", surface = name).entered();
248 let root = tui(ui, egui::Id::new(name));
249 let root = match room {
250 Room::Width => root.reserve_available_width(),
251 Room::Height => root.reserve_available_height(),
252 Room::Both => root.reserve_available_space(),
253 };
254 root.style(style.spanning(room).into())
255 .show(|tui| recorded(name, tui, add))
256}
257
258pub trait Named<'r>: TuiBuilderLogic<'r> {
261 fn node<T>(self, name: &'static str, style: Flex, add: impl FnOnce(&mut Tui) -> T) -> T {
263 self.style(style.into()).add(|tui| recorded(name, tui, add))
264 }
265
266 fn pressable<T>(
269 self,
270 name: &'static str,
271 style: Flex,
272 add: impl FnOnce(&mut Tui) -> T,
273 ) -> egui_taffy::TuiInnerResponse<T> {
274 self.style(style.into())
275 .clickable(|tui| recorded(name, tui, add))
276 }
277
278 fn leaf<T>(self, name: &'static str, style: Flex, draw: impl FnOnce(&mut egui::Ui) -> T) -> T {
285 self.style(style.into()).ui(|ui| {
286 record(ui.ctx(), name, ui.max_rect());
287 draw(ui)
288 })
289 }
290
291 fn widget<W>(self, name: &'static str, style: Flex, widget: W) -> egui::Response
295 where
296 W: TuiWidget<Response = egui::Response>,
297 {
298 let response = self.style(style.into()).ui_add(widget);
299 record(&response.ctx, name, response.rect);
300 response
301 }
302
303 fn text(
310 self,
311 name: &'static str,
312 style: Flex,
313 text: impl Into<egui::WidgetText>,
314 ) -> egui::Response {
315 self.widget(name, style, egui::Label::new(text).wrap())
316 }
317}
318
319impl<'r, B: TuiBuilderLogic<'r>> Named<'r> for B {}
320
321fn recorded<T>(name: &'static str, tui: &mut Tui, add: impl FnOnce(&mut Tui) -> T) -> T {
325 let ctx = tui.egui_ctx().clone();
326 enter(&ctx, name, tui.taffy_container().full_container());
327 note_overflow(
328 &ctx,
329 name,
330 tui.taffy_container(),
331 tui.current_style().overflow,
332 );
333 let inner = add(tui);
334 leave(&ctx);
335 inner
336}
337
338#[derive(Clone, Copy, Debug, PartialEq)]
340pub struct Node {
341 pub name: &'static str,
342 pub depth: usize,
344 pub rect: egui::Rect,
345}
346
347pub fn nodes(ctx: &egui::Context) -> Vec<Node> {
349 read(ctx, |recording| recording.nodes.clone())
350}
351
352pub fn dump(ctx: &egui::Context) -> String {
356 use std::fmt::Write as _;
357
358 let (nodes, overflowing) = read(ctx, |recording| {
359 (recording.nodes.clone(), recording.overflowing.clone())
360 });
361 let mut out = String::new();
362 for node in nodes {
363 let rect = node.rect;
364 let flag = if overflowing.contains(node.name) {
365 " OVERFLOWS"
366 } else {
367 ""
368 };
369 let _ = writeln!(
370 out,
371 "{:indent$}{} at ({:.0}, {:.0}) {:.0}x{:.0}{flag}",
372 "",
373 node.name,
374 rect.left(),
375 rect.top(),
376 rect.width(),
377 rect.height(),
378 indent = node.depth * 2,
379 );
380 }
381 out
382}
383
384pub fn overflowing(ctx: &egui::Context) -> Vec<&'static str> {
386 read(ctx, |recording| {
387 recording.overflowing.iter().copied().collect()
388 })
389}
390
391#[cfg(feature = "ui_debug")]
394pub fn inspect(ctx: &egui::Context, theme: &crate::theme::Theme) {
395 use crate::theme::Role;
396
397 const BY_DEPTH: [Role; 3] = [Role::LayoutBox0, Role::LayoutBox1, Role::LayoutBox2];
398 let dump_chord = egui::KeyboardShortcut::new(
399 egui::Modifiers::COMMAND | egui::Modifiers::SHIFT,
400 egui::Key::L,
401 );
402 if ctx.input_mut(|input| input.consume_shortcut(&dump_chord)) {
403 println!("{}", dump(ctx));
404 }
405
406 let painter = ctx.layer_painter(egui::LayerId::new(
407 egui::Order::Debug,
408 egui::Id::new("flex_inspector"),
409 ));
410 for node in nodes(ctx) {
411 if !node.rect.is_positive() {
412 continue;
413 }
414 let tint = theme.resolve(BY_DEPTH[node.depth % BY_DEPTH.len()]);
415 painter.rect_stroke(
416 node.rect,
417 egui::CornerRadius::ZERO,
418 egui::Stroke::new(1.0, tint),
419 egui::StrokeKind::Inside,
420 );
421 painter.text(
422 node.rect.left_top(),
423 egui::Align2::LEFT_TOP,
424 node.name,
425 egui::FontId::monospace(7.0),
426 theme.resolve(Role::LayoutName),
427 );
428 }
429}
430
431fn buffer() -> egui::Id {
433 egui::Id::new("blockworx::shell::flex")
434}
435
436#[derive(Clone, Default)]
437struct Recording {
438 pass: u64,
443 depth: usize,
444 nodes: Vec<Node>,
445 overflowing: std::collections::BTreeSet<&'static str>,
448}
449
450fn read<T>(ctx: &egui::Context, act: impl FnOnce(&Recording) -> T) -> T {
454 ctx.data_mut(|data| act(data.get_temp_mut_or_default::<Recording>(buffer())))
455}
456
457fn write<T>(ctx: &egui::Context, act: impl FnOnce(&mut Recording) -> T) -> T {
460 let pass = ctx.cumulative_pass_nr();
461 ctx.data_mut(|data| {
462 let recording = data.get_temp_mut_or_default::<Recording>(buffer());
463 if recording.pass != pass {
464 recording.pass = pass;
465 recording.depth = 0;
466 recording.nodes.clear();
467 }
468 act(recording)
469 })
470}
471
472fn placed(rect: egui::Rect) -> egui::Rect {
475 if rect.any_nan() {
476 egui::Rect::ZERO
477 } else {
478 rect
479 }
480}
481
482fn record(ctx: &egui::Context, name: &'static str, rect: egui::Rect) {
483 write(ctx, |recording| {
484 recording.nodes.push(Node {
485 name,
486 depth: recording.depth,
487 rect: placed(rect),
488 });
489 });
490}
491
492fn enter(ctx: &egui::Context, name: &'static str, rect: egui::Rect) {
493 record(ctx, name, rect);
494 write(ctx, |recording| recording.depth += 1);
495}
496
497fn leave(ctx: &egui::Context) {
498 write(ctx, |recording| {
499 recording.depth = recording.depth.saturating_sub(1);
500 });
501}
502
503#[cfg(debug_assertions)]
507fn note_overflow(
508 ctx: &egui::Context,
509 name: &'static str,
510 container: &egui_taffy::TaffyContainerUi,
511 overflow: taffy::Point<taffy::Overflow>,
512) {
513 const SLACK: f32 = 1.0;
515
516 let layout = container.layout();
517 let (box_size, content) = (layout.size, layout.content_size);
518 if box_size.width <= 0.0 || box_size.height <= 0.0 {
519 return;
520 }
521 let over_x = overflow.x == taffy::Overflow::Visible && content.width > box_size.width + SLACK;
522 let over_y = overflow.y == taffy::Overflow::Visible && content.height > box_size.height + SLACK;
523 if !(over_x || over_y) {
524 return;
525 }
526 let first = write(ctx, |recording| recording.overflowing.insert(name));
527 if first {
528 tracing::warn!(
529 node = name,
530 box_width = box_size.width,
531 box_height = box_size.height,
532 content_width = content.width,
533 content_height = content.height,
534 "layout overflow: the content does not fit the box it was given",
535 );
536 }
537}
538
539#[cfg(not(debug_assertions))]
540fn note_overflow(
541 _ctx: &egui::Context,
542 _name: &'static str,
543 _container: &egui_taffy::TaffyContainerUi,
544 _overflow: taffy::Point<taffy::Overflow>,
545) {
546}
547
548#[cfg(test)]
549mod tests {
550 use super::{
551 Flex, MAX_PASSES, Named as _, Node, Points, Room, Tui, cell, centre, dump, fixed, grow,
552 nodes, overflowing, row, row_start, scrolling, stack, surface, trailing, wrap,
553 };
554 use egui_taffy::taffy;
555
556 const ROOM: f32 = 300.0;
559
560 fn points(value: f32) -> Points {
561 Points::new(value)
562 }
563
564 fn laid_out(mut show: impl FnMut(&mut egui::Ui)) -> crate::tools::painted::Chrome {
567 let mut chrome = crate::tools::painted::Chrome::new(egui::Rect::from_min_size(
568 egui::pos2(0.0, 0.0),
569 egui::vec2(ROOM + 40.0, 600.0),
570 ));
571 chrome.ctx().options_mut(|options| {
572 options.max_passes = MAX_PASSES;
573 });
574 chrome.settle(|ui| {
575 let mut room = ui.new_child(
576 egui::UiBuilder::new()
577 .id(egui::Id::new("flex_room"))
578 .max_rect(egui::Rect::from_min_size(
579 egui::pos2(10.0, 10.0),
580 egui::vec2(ROOM, 560.0),
581 ))
582 .layout(egui::Layout::top_down(egui::Align::Min)),
583 );
584 show(&mut room);
585 });
586 chrome
587 }
588
589 fn named(chrome: &crate::tools::painted::Chrome, name: &str) -> Node {
590 nodes(chrome.ctx())
591 .into_iter()
592 .find(|node| node.name == name)
593 .unwrap_or_else(|| {
594 panic!(
595 "no node named {name:?}: {:?}",
596 nodes(chrome.ctx())
597 .iter()
598 .map(|node| node.name)
599 .collect::<Vec<_>>()
600 )
601 })
602 }
603
604 fn a_row(ui: &mut egui::Ui) {
607 surface(
608 ui,
609 "row",
610 Room::Width,
611 row(points(10.0)),
612 |tui: &mut Tui| {
613 tui.leaf("lead", fixed(), |ui| {
614 ui.label("LEAD");
615 });
616 tui.node("body", grow(), |tui: &mut Tui| {
617 tui.leaf("body_text", fixed(), |ui| {
618 ui.label("BODY");
619 });
620 });
621 tui.leaf("trail", fixed(), |ui| {
622 ui.label("TRAIL");
623 });
624 },
625 );
626 }
627
628 #[test]
629 fn the_growing_middle_takes_the_room_the_fixed_sides_leave() {
630 let chrome = laid_out(a_row);
631 let (lead, body, trail) = (
632 named(&chrome, "lead"),
633 named(&chrome, "body"),
634 named(&chrome, "trail"),
635 );
636 assert!(
637 lead.rect.is_positive() && trail.rect.is_positive(),
638 "the sides did not lay out at all: {lead:?} {trail:?}",
639 );
640 assert!(
641 body.rect.left() >= lead.rect.right(),
642 "the middle overlaps the lead: {body:?} vs {lead:?}",
643 );
644 assert!(
645 trail.rect.left() >= body.rect.right(),
646 "the trailing cluster did not go to the end: {trail:?} vs {body:?}",
647 );
648 assert!(
649 body.rect.width() > lead.rect.width() + trail.rect.width(),
650 "the middle did not grow: {body:?}",
651 );
652 assert!(
653 trail.rect.right() <= 10.0 + ROOM + 1.0,
654 "the row overran its room: {trail:?}",
655 );
656 }
657
658 #[test]
659 fn every_node_carries_its_name_and_its_nesting_depth() {
660 let chrome = laid_out(a_row);
661 let seen: Vec<(&str, usize)> = nodes(chrome.ctx())
662 .iter()
663 .map(|node| (node.name, node.depth))
664 .collect();
665 assert_eq!(
666 seen,
667 vec![
668 ("row", 0),
669 ("lead", 1),
670 ("body", 1),
671 ("body_text", 2),
672 ("trail", 1),
673 ],
674 );
675 }
676
677 #[test]
680 fn the_buffer_holds_one_pass_and_not_every_pass() {
681 let chrome = laid_out(a_row);
682 assert_eq!(nodes(chrome.ctx()).len(), 5);
683 }
684
685 #[test]
688 fn wrapping_text_uses_the_width_its_node_was_given() {
689 let long = "Base plate seventy eight to eighty four millimetres and then some more";
690 let chrome = laid_out(|ui| {
691 surface(
692 ui,
693 "wrapping",
694 Room::Width,
695 row_start(points(10.0)),
696 |tui: &mut Tui| {
697 tui.leaf("av", cell(points(26.0)), |ui| {
698 ui.label("AV");
699 });
700 tui.text("description", grow(), long);
701 },
702 );
703 });
704 let text = named(&chrome, "description");
705 assert!(
706 text.rect.width() > ROOM / 2.0,
707 "the label collapsed instead of using its node: {text:?} of {ROOM}",
708 );
709 }
710
711 #[test]
713 fn a_cell_keeps_its_side_whatever_it_holds() {
714 let chrome = laid_out(|ui| {
715 surface(
716 ui,
717 "cells",
718 Room::Width,
719 row(points(4.0)),
720 |tui: &mut Tui| {
721 tui.node("tool", cell(points(52.0)), |tui: &mut Tui| {
722 tui.leaf("glyph", fixed(), |ui| {
723 ui.label("T");
724 });
725 });
726 },
727 );
728 });
729 let tool = named(&chrome, "tool");
730 assert_eq!((tool.rect.width(), tool.rect.height()), (52.0, 52.0));
731 let glyph = named(&chrome, "glyph");
732 assert!(
733 tool.rect.contains_rect(glyph.rect),
734 "the glyph left its cell: {glyph:?} in {tool:?}",
735 );
736 }
737
738 #[test]
741 fn a_node_whose_content_outgrows_its_box_is_named() {
742 let chrome = laid_out(|ui| {
743 surface(
744 ui,
745 "cramped",
746 Room::Width,
747 stack(points(0.0)),
748 |tui: &mut Tui| {
749 tui.node("too_small", fixed().width(points(20.0)), |tui: &mut Tui| {
750 tui.widget(
751 "wide",
752 fixed(),
753 egui::Label::new("a great deal wider than twenty points").extend(),
754 );
755 });
756 },
757 );
758 });
759 assert!(
760 named(&chrome, "wide").rect.width() > 20.0,
761 "the test's own premise failed: the content fits the box",
762 );
763 assert!(
764 overflowing(chrome.ctx()).contains(&"too_small"),
765 "the overflow went unreported: {:?}",
766 overflowing(chrome.ctx()),
767 );
768 }
769
770 #[test]
772 fn a_scrolling_node_is_not_an_overflow() {
773 let chrome = laid_out(|ui| {
774 surface(
775 ui,
776 "sheet",
777 Room::Both,
778 stack(points(0.0)).height(points(120.0)),
779 |tui: &mut Tui| {
780 tui.node("body", scrolling(), |tui: &mut Tui| {
781 for _ in 0..20 {
782 tui.leaf("line", fixed(), |ui| {
783 ui.label("a row of the list");
784 });
785 }
786 });
787 },
788 );
789 });
790 assert!(
791 !overflowing(chrome.ctx()).contains(&"body"),
792 "a scroll container was reported as overflowing",
793 );
794 }
795
796 #[test]
799 fn a_pressable_row_answers_a_click_anywhere_in_its_box() {
800 let mut chrome = crate::tools::painted::Chrome::new(egui::Rect::from_min_size(
801 egui::pos2(0.0, 0.0),
802 egui::vec2(ROOM + 40.0, 600.0),
803 ));
804 chrome.ctx().options_mut(|options| {
805 options.max_passes = MAX_PASSES;
806 });
807 let clicked = std::cell::Cell::new(false);
808 let show = |ui: &mut egui::Ui| {
809 surface(
810 ui,
811 "list",
812 Room::Width,
813 stack(points(0.0)),
814 |tui: &mut Tui| {
815 let pressed = tui.pressable("rev", row(points(11.0)), |tui: &mut Tui| {
816 tui.leaf("lead", cell(points(28.0)), |ui| {
817 ui.label("SB");
818 });
819 tui.text("what_changed", grow(), "Moved the base plate");
820 });
821 if pressed.clicked() {
822 clicked.set(true);
823 }
824 },
825 );
826 };
827 chrome.settle(show);
828 let row_rect = named(&chrome, "rev").rect;
829 assert!(
830 row_rect.is_positive(),
831 "the row did not lay out: {row_rect:?}"
832 );
833 let empty = egui::pos2(row_rect.right() - 4.0, row_rect.center().y);
834 assert!(
835 chrome
836 .texts_inside(egui::Rect::from_center_size(empty, egui::vec2(6.0, 6.0)))
837 .is_empty(),
838 "the point pressed is not empty, so the row is not what answered",
839 );
840 chrome.click_at(empty, show);
841 assert!(clicked.get(), "the row ignored a press inside its own box");
842 }
843
844 #[test]
845 fn the_dump_indents_by_depth() {
846 let chrome = laid_out(a_row);
847 let dumped = dump(chrome.ctx());
848 let lines: Vec<&str> = dumped.lines().collect();
849 assert!(lines[0].starts_with("row at "), "{dumped}");
850 assert!(lines[1].starts_with(" lead at "), "{dumped}");
851 assert!(lines[3].starts_with(" body_text at "), "{dumped}");
852 }
853
854 #[test]
855 fn the_dump_says_which_node_overflowed() {
856 let chrome = laid_out(|ui| {
857 surface(
858 ui,
859 "cramped",
860 Room::Width,
861 stack(points(0.0)),
862 |tui: &mut Tui| {
863 tui.node("too_small", fixed().width(points(20.0)), |tui: &mut Tui| {
864 tui.widget(
865 "wide",
866 fixed(),
867 egui::Label::new("a great deal wider than twenty points").extend(),
868 );
869 });
870 },
871 );
872 });
873 assert!(dump(chrome.ctx()).contains("too_small at "));
874 assert!(dump(chrome.ctx()).contains("OVERFLOWS"));
875 }
876
877 #[test]
878 fn the_growing_middle_may_shrink_below_its_content() {
879 let style: taffy::Style = grow().into();
880 assert_eq!(style.flex_grow, 1.0);
881 assert_eq!(style.flex_shrink, 1.0);
882 assert_eq!(style.min_size.width, taffy::prelude::length(0.0));
883 }
884
885 #[test]
886 fn a_fixed_box_neither_grows_nor_shrinks() {
887 let style: taffy::Style = fixed().into();
888 assert_eq!(style.flex_grow, 0.0);
889 assert_eq!(style.flex_shrink, 0.0);
890 }
891
892 #[test]
895 fn a_reversion_row_starts_where_an_ordinary_row_centres() {
896 let centred: taffy::Style = row(points(11.0)).into();
897 let started: taffy::Style = row_start(points(10.0)).into();
898 assert_eq!(centred.align_items, Some(taffy::AlignItems::Center));
899 assert_eq!(started.align_items, Some(taffy::AlignItems::FlexStart));
900 assert_eq!(centred.gap.width, taffy::prelude::length(11.0));
901 }
902
903 #[test]
904 fn the_centred_middle_grows_and_centres() {
905 let style: taffy::Style = centre(points(8.0)).into();
906 assert_eq!(style.flex_grow, 1.0);
907 assert_eq!(style.justify_content, Some(taffy::JustifyContent::Center));
908 assert_eq!(style.min_size.width, taffy::prelude::length(0.0));
909 }
910
911 #[test]
912 fn chip_flows_wrap_and_meta_columns_do_not() {
913 let chips: taffy::Style = wrap(points(4.0)).into();
914 let meta: taffy::Style = trailing(points(3.0)).into();
915 assert_eq!(chips.flex_wrap, taffy::FlexWrap::Wrap);
916 assert_eq!(meta.flex_direction, taffy::FlexDirection::Column);
917 assert_eq!(meta.align_items, Some(taffy::AlignItems::FlexEnd));
918 assert_eq!(meta.flex_grow, 0.0);
919 }
920
921 #[test]
922 fn a_scrolling_body_scrolls_one_way_and_clips_the_other() {
923 let style: taffy::Style = scrolling().into();
924 assert_eq!(style.overflow.y, taffy::Overflow::Scroll);
925 assert_eq!(style.overflow.x, taffy::Overflow::Clip);
926 assert_eq!(style.flex_grow, 1.0);
927 }
928
929 #[test]
930 fn a_gap_below_zero_is_not_a_gap() {
931 assert_eq!(f32::from(points(-4.0)), 0.0);
932 assert_eq!(f32::from(Points::new(f32::NAN)), 0.0);
933 }
934
935 #[test]
936 fn the_engine_needs_more_than_one_pass() {
937 assert_eq!(MAX_PASSES.get(), 3);
938 }
939
940 #[cfg(feature = "ui_debug")]
943 #[test]
944 fn the_overlay_draws_one_outline_per_node_with_its_name() {
945 let theme = crate::theme::Theme::from_embedded();
946 let mut chrome = crate::tools::painted::Chrome::new(egui::Rect::from_min_size(
947 egui::pos2(0.0, 0.0),
948 egui::vec2(ROOM + 40.0, 600.0),
949 ));
950 chrome.ctx().options_mut(|options| {
951 options.max_passes = MAX_PASSES;
952 });
953 chrome.settle(|ui| {
954 a_row(ui);
955 super::inspect(ui.ctx(), &theme);
956 });
957 assert_eq!(nodes(chrome.ctx()).len(), 5, "the tree did not lay out");
958 for name in ["row", "lead", "body", "body_text", "trail"] {
959 assert!(chrome.shows(name), "the overlay did not name {name}");
960 }
961 assert!(
962 chrome.outlines().len() >= 5,
963 "fewer outlines than nodes: {:?}",
964 chrome.outlines(),
965 );
966 }
967
968 #[test]
969 fn a_root_that_fills_says_so_in_its_style() {
970 let both: taffy::Style = Flex::default().spanning(Room::Both).into();
971 let wide: taffy::Style = Flex::default().spanning(Room::Width).into();
972 assert_eq!(both.size.width, taffy::prelude::percent(1.0));
973 assert_eq!(both.size.height, taffy::prelude::percent(1.0));
974 assert_eq!(wide.size.height, taffy::prelude::auto());
975 }
976}