diff --git a/core/src/ui/holds.rs b/core/src/ui/holds.rs index 2845aaa..35d852c 100644 --- a/core/src/ui/holds.rs +++ b/core/src/ui/holds.rs @@ -19,6 +19,26 @@ pub struct Holds { pub hi: Px, } +impl Len { + /// Whether this is longer than `than` in a window this wide, and the + /// windows that answer holds for. + /// + /// Which is longer is a question in pixels -- `rel(0.5)` is longer than + /// 300 px at a box of 600 and shorter at 400 -- and it is asked of the + /// difference and answered back through that same difference, so the + /// boundary is the comparison's own rather than a second way of finding + /// it. + pub fn longer_than(&self, than: Len, window: Px) -> (bool, Holds) { + let over = *self - than; + let longer = over.to_px(window) > Px::ZERO; + let side = match longer { + true => Px::STEP..=Px::MAX, + false => Px::MIN..=Px::ZERO, + }; + (longer, Holds::from(side).through(over)) + } +} + impl Holds { pub const ANY: Self = Self { lo: Px::MIN, diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index c579879..4a24065 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -187,22 +187,17 @@ impl<'a> Painter<'a> { id: &'s StrongWidget, place: impl Into, ) -> DrawResult<'s, 'a, W> { - let mut place = self.resolve_rel_base(place.into()); + let offer = self.resolve_rel_base(place.into()); + let Ask { + rel_base, + region, + place, + declared, + holds: ask_holds, + } = self + .placing() + .ask(self.rsc.widgets(), self.window, id.id(), offer); let region_node = self.rsc.widgets().is_region_node(id.id()); - let align = self.rsc.widgets().alignment(id.id()); - // A share fills what the pixels and fraction beside it leave of the - // box and overflows where they are longer, which is the rule a span - // follows with one child. Only the overflow is a box of the child's - // own: a share that fits is the box it was given, which is what this - // place already says. - for axis in Axis::BOTH { - if let Some(len) = self.share_past_the_offer(id.id(), place, align, axis) { - place[axis] = len.as_desc().fills(); - } - } - let declared = self.declared_lens(id); - let (rel_base, region) = - place.rel_base_and_region(self.region, self.rel_base, declared, align); #[cfg(feature = "layout-diagnostics")] if region_node { diag::bump(Counter::RegionNodeDraws); @@ -225,7 +220,8 @@ impl<'a> Painter<'a> { rel_base, region, placed: place, - asked: place, + asked: offer, + ask_holds, re_asked, }, None, @@ -292,7 +288,7 @@ impl<'a> Painter<'a> { /// This widget as the thing its children are placed within. fn placing(&self) -> Placing { Placing { - id: self.id, + id: Some(self.id), region: self.region, rel_base: self.rel_base, depth: self.depth, @@ -301,53 +297,6 @@ impl<'a> Painter<'a> { } } - /// What a rule or a hint declares a widget's lengths to be, which whoever - /// draws it resolves into its rel base. Reading them depends on nothing -- the box - /// that comes of them is kept on the child, and `redraw` compares it - /// there. - fn declared_lens(&self, id: &StrongWidget) -> Declared { - self.rsc.widgets().declared_lens(id.id()) - } - - /// The box a child's own share asks for where that is longer than the box - /// `place` gives it, and nothing where the share fits. - /// - /// A share is a length only to whoever divides one, and nothing divides a - /// box handed to one child: what is left of it after the pixels and the - /// fraction beside the share is what the share takes, so the length comes - /// to the whole box until those are longer than it and to them once they - /// are. Only that second case is a box this widget did not give, and the - /// crossing between them is a question in pixels, so this widget's drawing - /// holds for the windows on one side of it. Narrowed rather than stated, - /// because this widget may have read its own box as well, and a range it - /// pinned for that still holds. - fn share_past_the_offer( - &mut self, - id: WidgetId, - place: PlaceDesc, - align: RegionAlign, - axis: Axis, - ) -> Option { - // A place that is the child's placement outright is a box its parent - // decided, and a parent that divides one has already given the share - // whatever it was owed. Only an offer -- a box with the answer still - // to be placed inside it -- is a box a share reads. - if place[axis].fills { - return None; - } - // A share with nothing beside it is the box whatever the box is, so - // there is no comparison to make and no range to keep for one. - let stated = self.rsc.widgets().exact_len(id, axis)?; - if stated.leftover == Weight::ZERO || stated.is_only_leftover() { - return None; - } - let fixed = stated - .without_leftover() - .within_len(place.base(axis, self.rel_base)); - let offer = place.of(self.region, align)[axis].len(); - self.longer_than(fixed, offer, axis).then_some(fixed) - } - /// What a child says its length is without being drawn, if it can say, /// as the length its draw would report: a fraction in it is resolved /// against this widget's rel base, which is the rel base a child asked with @@ -524,30 +473,16 @@ impl<'a> Painter<'a> { len.to_px(window) } - /// Whether `len` is longer than `than`, kept as the windows that comparison - /// comes out the same way on: a drawing that took one of two lengths holds - /// where the same one is the longer, and nowhere else. + /// [`Len::longer_than`], asked on this widget's behalf: the windows the + /// comparison comes out the same way on are windows its drawing holds + /// for, and nowhere else does it. What a container has left for the + /// shares it divides is the one thing that asks. /// - /// Which is longer is a question in pixels -- `rel(0.5)` is longer than 300 - /// px at a box of 600 and shorter at 400 -- and it is asked of the - /// difference and answered back through that same difference, so the - /// boundary is the drawing's own rather than a second way of finding it. /// Narrowed rather than stated, because whatever else this widget read /// about the window is a reason its drawing holds where it does too. - /// - /// This is the one operation a length that is the longer of two needs: the - /// room a container has left for the shares it divides, and a share that - /// overflows the box it was given because the pixels beside it are longer - /// than the box. pub fn longer_than(&mut self, len: Len, than: Len, axis: Axis) -> bool { - let over = len - than; let window = self.window[axis]; - let longer = over.to_px(window) > Px::ZERO; - let side = match longer { - true => Px::STEP..=Px::MAX, - false => Px::MIN..=Px::ZERO, - }; - let holds = Holds::from(side).through(over); + let (longer, holds) = len.longer_than(than, window); debug_assert!( holds.contains(window), "'{}' ({:?}) compared two lengths and kept a range without this window", @@ -746,6 +681,108 @@ impl Widgets { } } +/// One ask of a widget: the box it draws in, what its fractions are of, and +/// what deciding those read. +pub(super) struct Ask { + pub rel_base: UiVec2, + pub region: UiRegion, + /// The place the ask came to, which a rule of the widget's own can take + /// past the box its parent offered. + pub place: PlaceDesc, + pub declared: Declared, + /// What the ask itself holds for, kept on the widget asked about: a rule + /// compared against the offer in pixels holds only for the windows on its + /// side of the crossing, and that range reaches whoever asked through the + /// drawing it is part of. Kept on the widget asked about rather than on + /// the asker because the root has no asker. + pub holds: LayoutHolds, +} + +impl Placing { + /// Asks about a widget at `place` of this box, with the widget's own + /// rules applied to what the place offers it. `place` is resolved: what + /// a rel base of the asker's is a fraction of, the asker worked out. + /// + /// Every ask is this one, the root's included -- there the box is the + /// window and nothing above narrowed it, which is what [`Self::WINDOW`] + /// says. + pub(super) fn ask( + &self, + widgets: &Widgets, + window: PxVec2, + id: WidgetId, + mut place: PlaceDesc, + ) -> Ask { + let align = widgets.alignment(id); + let mut holds = LayoutHolds::ANY; + // A share fills what the pixels and fraction beside it leave of the + // box and overflows where they are longer, which is the rule a span + // follows with one child. Only the overflow is a box of the child's + // own: a share that fits is the box it was given, which is what this + // place already says. + for axis in Axis::BOTH { + let (len, kept) = + self.share_past_the_offer(widgets, window[axis], id, place, align, axis); + holds[axis].window = holds[axis].window.and(kept); + if let Some(len) = len { + place[axis] = len.as_desc().fills(); + } + } + let declared = widgets.declared_lens(id); + let (rel_base, region) = + place.rel_base_and_region(self.region, self.rel_base, declared, align); + Ask { + rel_base, + region, + place, + declared, + holds, + } + } + + /// The box a widget's own share asks for where that is longer than the + /// box `place` gives it, and nothing where the share fits -- with the + /// windows that answer holds for, which is a range either way. + /// + /// A share is a length only to whoever divides one, and nothing divides a + /// box handed to one child: what is left of it after the pixels and the + /// fraction beside the share is what the share takes, so the length comes + /// to the whole box until those are longer than it and to them once they + /// are. Only that second case is a box its parent did not give, and the + /// crossing between them is a question in pixels. + fn share_past_the_offer( + &self, + widgets: &Widgets, + window: Px, + id: WidgetId, + place: PlaceDesc, + align: RegionAlign, + axis: Axis, + ) -> (Option, Holds) { + // A place that is the widget's placement outright is a box its parent + // decided, and a parent that divides one has already given the share + // whatever it was owed. Only an offer -- a box with the answer still + // to be placed inside it -- is a box a share reads. + if place[axis].fills { + return (None, Holds::ANY); + } + // A share with nothing beside it is the box whatever the box is, so + // there is no comparison to make and no range to keep for one. + let Some(stated) = widgets.exact_len(id, axis) else { + return (None, Holds::ANY); + }; + if stated.leftover == Weight::ZERO || stated.is_only_leftover() { + return (None, Holds::ANY); + } + let fixed = stated + .without_leftover() + .within_len(place.base(axis, self.rel_base)); + let offer = place.of(self.region, align)[axis].len(); + let (longer, holds) = fixed.longer_than(offer, window); + (longer.then_some(fixed), holds) + } +} + impl LayoutLen { /// Whether what a widget reported along an axis is the whole of the box /// it is in rather than a part to be placed inside it. A share fills, diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index bd8eab2..8f18d99 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -4,6 +4,7 @@ use crate::{ ActiveData, Answer, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, + ui::painter::Ask, util::{HashMap, Vec2}, }; @@ -23,11 +24,14 @@ pub(super) struct DrawInfo { /// The box the widget is asked in, in its parent region node's /// coordinates. pub region: UiRegion, - /// Where the widget is put, and where it was asked, as parts of the - /// parent's box. See [`PlaceDesc`]. The two are one ask's place until the - /// parent puts the answer somewhere else. + /// Where the widget is put, and what its parent offered it, as parts of + /// the parent's box. See [`PlaceDesc`]. The two are one place until a + /// rule of the widget's own takes it past the offer, or the parent puts + /// the answer somewhere else. pub placed: PlaceDesc, pub asked: PlaceDesc, + /// What the ask that gave it those two holds for. See [`Ask::holds`]. + pub ask_holds: LayoutHolds, /// Whether the parent already asked about this widget in this draw. pub re_asked: bool, } @@ -43,7 +47,8 @@ pub(super) struct Drawn { /// What a widget's children are placed in: its own box, the coordinates its /// drawing is in, and what else one ask of a child is decided from. pub(super) struct Placing { - pub id: WidgetId, + /// The widget whose box this is, and nothing for the window. + pub id: Option, pub region: UiRegion, pub rel_base: UiVec2, pub depth: usize, @@ -51,6 +56,21 @@ pub(super) struct Placing { pub mask: MaskIdx, } +impl Placing { + /// The window, which is what the root is placed within. Nothing above the + /// root narrowed a box or chose where it goes, so it is asked in the whole + /// output and its fractions are of the whole output -- an ordinary ask, + /// from the one box nobody drew. + pub const WINDOW: Self = Self { + id: None, + region: UiRegion::FULL, + rel_base: UiVec2::FULL_SIZE, + depth: 0, + move_idx: MoveIdx::NONE, + mask: MaskIdx::NONE, + }; +} + pub struct UiRenderState { pub active: HashMap, pub layers: DrawLayers, @@ -124,20 +144,21 @@ impl UiRenderState { } } - /// The root is asked about in the output. Its own rules narrow both its - /// rel base and box; nothing above it chose a different one. - fn root_info(&self, rel_base: UiVec2, region: UiRegion) -> DrawInfo { + /// The root's first draw: the ask [`Placing::WINDOW`] answered, with the + /// bookkeeping a widget with no parent carries. + fn root_info(&self, ask: &Ask, region_node: bool) -> DrawInfo { DrawInfo { layer: 0, parent: None, - depth: 1, + depth: Placing::WINDOW.depth + 1, parent_move: MoveIdx::NONE, - region_node: false, + region_node, mask: MaskIdx::NONE, - rel_base, - region, - placed: PlaceDesc::WHOLE, + rel_base: ask.rel_base, + region: ask.region, + placed: ask.place, asked: PlaceDesc::WHOLE, + ask_holds: ask.holds, re_asked: false, } } @@ -184,24 +205,13 @@ impl UiRenderState { let _layout = diag::timer(TimerKind::FullLayout); self.clear(rsc); if let Some(id) = root { - let (rel_base, region) = Self::root_layout(id.id(), rsc.widgets()); - let info = self.root_info(rel_base, region); + let ask = + Placing::WINDOW.ask(rsc.widgets(), self.output_size, id.id(), PlaceDesc::WHOLE); + let info = self.root_info(&ask, rsc.widgets().is_region_node(id.id())); self.draw_inner(id.id(), info, None, rsc); } } - /// The root's rel base and box: the window, taken in by the root's own - /// rules. Nothing above it narrowed anything or chose where it goes, so - /// its declaration is the whole of what decides either. - fn root_layout(id: WidgetId, widgets: &Widgets) -> (UiVec2, UiRegion) { - PlaceDesc::WHOLE.rel_base_and_region( - UiRegion::FULL, - UiVec2::FULL_SIZE, - widgets.declared_lens(id), - widgets.alignment(id), - ) - } - pub(super) fn draw_inner( &mut self, id: WidgetId, @@ -329,7 +339,10 @@ impl UiRenderState { mask_slot, children: Vec::new(), size_deps: Vec::new(), - own: LayoutHolds::ANY, + // What the ask holds for is part of what the drawing holds for: + // a box the widget's own rule took past the offer was decided in + // this window, and at the root nobody else keeps that range. + own: info.ask_holds, under: Vec::new(), answer_under: LayoutHolds::ANY, depth: info.depth, @@ -462,6 +475,7 @@ impl UiRenderState { region: UiRegion::FULL, placed: PlaceDesc::WHOLE, asked: PlaceDesc::WHOLE, + ask_holds: LayoutHolds::ANY, re_asked: false, }, rsc, @@ -697,7 +711,7 @@ impl UiRenderState { ); let info = DrawInfo { layer: active.layer, - parent: Some(at.id), + parent: at.id, depth: at.depth + 1, parent_move: at.move_idx, region_node: active.is_region_node(), @@ -706,6 +720,8 @@ impl UiRenderState { region, placed: place, asked: active.asked, + // Placing decides no box: this is the one the ask already gave. + ask_holds: LayoutHolds::ANY, re_asked: active.re_asked, }; self.relocate(child, placed, info, rsc); @@ -734,7 +750,7 @@ impl UiRenderState { rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&placed); } let at = Placing { - id, + id: Some(id), region: placed, rel_base: info.rel_base, depth: info.depth, @@ -1066,29 +1082,19 @@ impl UiRenderState { if !active.drawn { return true; } - // Nothing above the root resolved its rules or its alignment, so its - // box is its own to work out again against the output. Every other - // widget was given one. - let Some(parent) = active.parent else { - let (rel_base, region) = Self::root_layout(id, rsc.widgets()); - let info = DrawInfo { - mask: active.parent_mask, - ..self.root_info(rel_base, region) - }; - #[cfg(feature = "layout-diagnostics")] - diag::bump(Counter::LocalRedraws); - let old = self.remove(id, false, rsc); - self.draw_inner(id, info, old, rsc); - return true; - }; let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.placed); // The question its parent asked, asked again: the same place of the // box the parent was asked in, which is the box the parent's own // draw ran in and what its children's parts are of. Where the // parent's answer put its own drawing is not a question anybody - // asked, and nothing is asked in it here either. - let parent_at = self.placing_of(parent, self.active[&parent].region); - let (rel_base, region) = Self::ask_again(active, &parent_at, active.asked); + // asked, and nothing is asked in it here either. The root's parent is + // the window, which no draw made and no answer can move. + let at = match active.parent { + Some(parent) => self.placing_of(parent, self.active[&parent].region), + None => Placing::WINDOW, + }; + let ask = at.ask(rsc.widgets(), self.output_size, id, active.asked); + let (rel_base, region) = (ask.rel_base, ask.region); let info = DrawInfo { layer: active.layer, parent: active.parent, @@ -1098,8 +1104,9 @@ impl UiRenderState { mask: active.parent_mask, rel_base, region, - placed: active.asked, + placed: ask.place, asked: active.asked, + ask_holds: ask.holds, re_asked: false, }; #[cfg(feature = "layout-diagnostics")] @@ -1127,21 +1134,31 @@ impl UiRenderState { if active.holds.covers(was_holds) && was_holds.contains(window, rel_base, region) { active.holds = was_holds; } - if active.answer != was_answer || active.holds != was_holds { - // The parent retains both the answer and the drawing's validity; - // even an unchanged size can narrow the range safe for a resize. - #[cfg(feature = "layout-diagnostics")] - { - diag::bump(Counter::SizeChanges); - diag::bump(Counter::ReaderEdges); + let changed = active.answer != was_answer || active.holds != was_holds; + // Nothing above the root retained either, so there is nobody to tell + // and nowhere else the drawing has to go back to. + if let Some(parent) = active.parent { + match changed { + // The parent retains both the answer and the drawing's + // validity; even an unchanged size can narrow the range safe + // for a resize. + true => { + #[cfg(feature = "layout-diagnostics")] + { + diag::bump(Counter::SizeChanges); + diag::bump(Counter::ReaderEdges); + } + self.mark(parent, rsc.widgets_mut()); + } + // The answer stands, so where the parent put it stands: the + // fresh drawing goes back there -- the same place, of the box + // the parent's answer chose rather than the one it was asked + // in. + false => { + let at = self.placing_of(parent, self.active[&parent].placement); + self.place_in(id, &at, was_place, rsc); + } } - self.mark(parent, rsc.widgets_mut()); - } else { - // The answer stands, so where the parent put it stands: the - // fresh drawing goes back there -- the same place, of the box - // the parent's answer chose rather than the one it was asked in. - let at = self.placing_of(parent, self.active[&parent].placement); - self.place_in(id, &at, was_place, rsc); } true } @@ -1152,7 +1169,7 @@ impl UiRenderState { fn placing_of(&self, id: WidgetId, region: UiRegion) -> Placing { let active = &self.active[&id]; Placing { - id, + id: Some(id), region, rel_base: active.rel_base, depth: active.depth, diff --git a/tests/cases/layout.rs b/tests/cases/layout.rs index 1e1b8d5..916b629 100644 --- a/tests/cases/layout.rs +++ b/tests/cases/layout.rs @@ -260,32 +260,52 @@ fn a_share_rule_beats_the_widgets_own_pixel_size() { assert_eq!(asked.get(), 400.0, "the share is all of the box"); } -/// A share with pixels or a fraction beside it is the longer of the two: it -/// fills what they leave of the box and overflows the box where they are -/// longer than it. A parent that divides nothing gives the same length as a -/// span with one child, because in both there is nobody else to divide with. -#[test] -fn a_share_is_a_minimum_wherever_nothing_divides_it() { - let asked = |rule: LayoutLen, in_a_span: bool| { +/// Every box a widget is given comes of one ask, and the window is one of +/// them: the root is asked in it exactly as a child is asked in its parent's +/// box, so a rule of its own reads the same way at either place. +#[derive(Clone, Copy, Debug)] +enum Asked { + Root, + Wrapped, + InASpan, +} + +impl Asked { + const ALL: [Self; 3] = [Self::Root, Self::Wrapped, Self::InASpan]; + + /// The width the probe is given under this parent, in a 400 px window. + fn width(&self, rule: LayoutLen) -> Px { let mut h = Harness::new((400, 200)); let probe = rect(Color::RED).add(&mut h.rsc); h.set_len(probe, Axis::X, rule); - match in_a_span { - true => h.set_root((probe,).span(Dir::RIGHT)), - false => h.set_root(probe.wrapper()), + match self { + Self::Root => h.set_root(probe), + Self::Wrapped => h.set_root(probe.wrapper()), + Self::InASpan => h.set_root((probe,).span(Dir::RIGHT)), } h.region(&probe).unwrap().size().x - }; + } +} + +/// A share with pixels or a fraction beside it is the longer of the two: it +/// fills what they leave of the box and overflows the box where they are +/// longer than it. A parent that divides nothing gives the same length as a +/// span with one child, because in both there is nobody else to divide with -- +/// and so does the window, which divides nothing either. +#[test] +fn a_share_is_a_minimum_wherever_nothing_divides_it() { for (rule, want) in [ (LayoutLen::LEFTOVER, 400), (LayoutLen::px(50) + LayoutLen::LEFTOVER, 400), (LayoutLen::px(500) + LayoutLen::LEFTOVER, 500), (LayoutLen::rel(0.5) + LayoutLen::LEFTOVER, 400), + (LayoutLen::rel(2.0) + LayoutLen::LEFTOVER, 800), (LayoutLen::px(500), 500), ] { let want = Px::from_int(want); - assert_eq!(asked(rule, false), want, "{rule:?} where nothing divides"); - assert_eq!(asked(rule, true), want, "{rule:?} in a span"); + for asked in Asked::ALL { + assert_eq!(asked.width(rule), want, "{rule:?} asked {asked:?}"); + } } } @@ -297,27 +317,36 @@ fn a_share_is_a_minimum_wherever_nothing_divides_it() { /// way, so it reaches the parent as a length only the parent can resolve. #[test] fn a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing() { - let mut h = Harness::new((400, 200)); - let probe = rect(Color::RED).add(&mut h.rsc); - h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER); - h.set_root(probe.wrapper()); - assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(500)); + // At the root as well as under a parent: the comparison is the same one, + // and nothing above the root will make it again on its behalf, so the + // range it holds for is the root's own. + for wrapped in [false, true] { + let mut h = Harness::new((400, 200)); + let probe = rect(Color::RED).add(&mut h.rsc); + h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER); + match wrapped { + true => h.set_root(probe.wrapper()), + false => h.set_root(probe), + } + let width = |h: &Harness| h.region(&probe).unwrap().size().x; + assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}"); - h.resize((900, 200)); - h.frame(); - assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900)); + h.resize((900, 200)); + h.frame(); + assert_eq!(width(&h), Px::from_int(900), "wrapped: {wrapped}"); - h.resize((400, 200)); - h.frame(); - assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(500)); + h.resize((400, 200)); + h.frame(); + assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}"); - h.set_len(probe, Axis::X, LayoutLen::px(50) + LayoutLen::LEFTOVER); - h.frame(); - assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(400)); + h.set_len(probe, Axis::X, LayoutLen::px(50) + LayoutLen::LEFTOVER); + h.frame(); + assert_eq!(width(&h), Px::from_int(400), "wrapped: {wrapped}"); - h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER); - h.frame(); - assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(500)); + h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER); + h.frame(); + assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}"); + } } #[test] @@ -959,3 +988,19 @@ fn a_collapsed_share_keeps_the_gaps_before_the_next_slot() { } } } + +/// The root is asked the way any child is, so what it says about itself is +/// read there too: a root that opted into a region node gets one, where the +/// path it used to have ignored the flag. +#[test] +fn a_region_node_root_is_a_region_node() { + let mut h = Harness::new((400, 200)); + let probe = rect(Color::RED).add(&mut h.rsc); + let root = (probe,).span(Dir::RIGHT).region_node().add(&mut h.rsc); + h.set_root(root); + assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(400)); + + h.resize((900, 200)); + h.frame(); + assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900)); +}