From 58ce74dd7da772c0c20ddb011e157914ac0ee5eb Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Sat, 19 Sep 2026 17:59:26 -0400 Subject: [PATCH] One argument says where a child goes and what its fractions are of `Place` was a product written as a sum -- a `Part` and a fill flag -- and `Part` named three operations the geometry already had, under words that did not match them. `Of` was `UiSpan::within`, `From` was `UiSpan::shift`, and `Sized` was `placement`'s own body with the length given rather than reported. Both enums are gone. `PlaceDescAxis` says one axis, named after the operation it performs: `within`, `shifted`, `sized`, and `WHOLE`. What is optional is a builder -- `fills` and `rel_base` -- so a caller writes only what it decided, and the rel base it does not write follows the constructor: a span composed into the caller's box narrows it, a span along a cursor does not, a decided length is it. That was the one rule a caller could get wrong with nothing failing. `PlaceDesc` says both axes with named fields, so `axis`, `axis_mut` and `from_axis` work the way they do on every other pair here, and the joint work -- resolving a region, reading the fill flags -- is written once rather than per axis. `widget_at` and `place_at` take `impl Into`, so a wrapper passes a `UiRegion` and says nothing else. `widget_within` and `ActiveData::narrow_rel_base` are deleted; `asked` and `placed` carry the rel base their ask stated. Cold layout is byte-identical to `84dad21`. --- core/src/ui/active.rs | 21 +-- core/src/ui/painter.rs | 157 +++++++------------- core/src/ui/place.rs | 272 +++++++++++++++++++++++++++------- core/src/ui/render_state.rs | 56 +++---- src/random.rs | 12 +- src/widget/position/offset.rs | 2 +- src/widget/position/pad.rs | 4 +- src/widget/position/scroll.rs | 10 +- src/widget/position/span.rs | 16 +- src/widget/position/stack.rs | 15 +- tests/cases/determinism.rs | 12 +- tests/cases/retained.rs | 61 ++++---- 12 files changed, 367 insertions(+), 271 deletions(-) diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 63318d7..0f943bf 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -1,5 +1,5 @@ use crate::{ - LayerId, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Place, RegionAlign, RetainedPrimitive, + LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive, Size, TextureHandle, UiRegion, UiVec2, WidgetId, }; @@ -14,18 +14,13 @@ pub struct ActiveData { /// What a fraction declared or reported under this widget is a fraction /// of, as a length of the window. pub rel_base: UiVec2, - /// A rel base its parent decided for it on each axis -- a row's slot, or - /// padding's rel base less its pixels -- as a length of the window. `None` - /// forwards the parent's rel base. What it declared is kept separately in - /// `declared` and is a fraction of whichever of the two reached it. - pub narrow_rel_base: [Option; 2], - /// Where its drawing was put, and where it was asked, each as a part of - /// its parent's box. The two differ where a container asks in one place - /// and puts the answer in another -- a row measures from its cursor and - /// puts the child in its slot. A part is a length from the box's start, - /// so a box that moved re-places every child by re-adding that start. - pub placed: [Place; 2], - pub asked: [Place; 2], + /// Where its drawing was put, and where it was asked. The two differ + /// where a container asks in one place and puts the answer in another -- + /// a row measures from its cursor and puts the child in its slot. Each + /// carries the rel base that ask stated, so asking again from either is + /// the same question it was. + pub placed: PlaceDesc, + pub asked: PlaceDesc, /// The box it was asked in, in the parent's region-node coordinates: the /// box its drawing was made in and the one its contract is about. Its /// drawing is placed elsewhere by re-expression, never by asking again. diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 4360e84..489ff39 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -1,7 +1,7 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter}; use crate::{ - Axis, Holds, LayoutHolds, LayoutLen, Len, Part, Place, Px, PxVec2, RegionAlign, Rel, + Axis, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, render::{ @@ -144,68 +144,49 @@ impl<'a> Painter<'a> { }; } - /// Draws a widget in the whole of this widget's own box, with the rel base - /// forwarded unchanged: what a container that is only a wrapper around - /// one child wants, and what every transparent container passes for the - /// rel base. + /// Draws a widget in the whole of this widget's own box, with the rel + /// base forwarded unchanged: what a container that is only a wrapper + /// around one child wants. pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget) -> DrawResult<'s, 'a, W> { - self.widget_within(id, UiRegion::FULL) + self.widget_at(id, UiRegion::FULL) } - /// Draws a widget in `region` of this widget's own box, in that box's own - /// coordinates -- an inset, or an offset. - /// - /// The child's rel base is this widget's narrowed the same way the box is, - /// so padding takes its pixels off both and an offset, which changes the - /// box's length by nothing, changes neither. An axis the region leaves - /// whole is not read at all, so a wrapper that only moves its child does - /// not pin the drawing to a rel base. - pub fn widget_within<'s, W: ?Sized>( - &'s mut self, - id: &'s StrongWidget, - region: UiRegion, - ) -> DrawResult<'s, 'a, W> { - let narrow = AXES.map(|axis| { - let len = region.axis(axis).len(); - (len != Len::FULL).then(|| len.within_len(self.rel_base(axis))) - }); - self.widget_at(id, region_places(region), narrow) + /// Resolves what the place says about the child's rel base into a length, + /// where that is this widget's own narrowed the way the region is. An + /// axis the region leaves whole is not read at all, so a wrapper that + /// only moves its child does not pin its drawing to a rel base. + fn state_rel_base(&mut self, mut place: PlaceDesc) -> PlaceDesc { + for axis in AXES { + if let Some(span) = place.axis(axis).narrows_rel_base() { + let len = span.len(); + let stated = (len != Len::FULL).then(|| len.within_len(self.rel_base(axis))); + *place.axis_mut(axis) = place.axis(axis).with_rel_base(stated); + } + } + place } /// Asks a child, saying what its fractions are of and where it is asked. /// - /// `place` is the child's region, per axis, said as a part of this - /// widget's own: see [`Place`]. The child draws once, in that region, - /// and its answer is placed inside it by re-expressing the drawing. - /// Nothing is drawn again in a box an answer chose; a container that - /// puts the answer somewhere else says so with [`Self::place_at`]. + /// `place` says where the child goes and what its fractions are of: + /// see [`PlaceDesc`]. A `UiRegion` converts into the common case, which + /// is a box of this widget's own with the answer placed inside it. /// - /// `narrow_rel_base` is the child's rel base, per axis, as a length of - /// the window: a resolved share, or a box a sibling's answer decided. - /// `None`, whole or per axis, forwards this widget's own -- which is - /// what a container that only divides room passes, so a fraction under - /// it means the same wherever it sits and however deeply it is nested. - /// It only ever narrows: a length the child declares narrows it again - /// here whatever the caller says, and what comes of it also narrows the - /// region, placed in the part by the child's alignment. + /// The child draws once, in the region that comes of it, and its answer + /// is placed inside that region by re-expressing the drawing. Nothing is + /// drawn again in a box an answer chose; a container that puts the + /// answer somewhere else says so with [`Self::place_at`]. pub fn widget_at<'s, W: ?Sized>( &'s mut self, id: &'s StrongWidget, - place: [Place; 2], - narrow_rel_base: impl Into; 2]>>, + place: impl Into, ) -> DrawResult<'s, 'a, W> { - let narrow_rel_base = narrow_rel_base.into().unwrap_or([None; 2]); + let place = self.state_rel_base(place.into()); let region_node = self.rsc.widgets().is_region_node(id.id()); let declared = self.declared_lens(id); let align = self.rsc.widgets().alignment(id.id()); - let (rel_base, region) = rel_base_and_region( - self.region, - self.rel_base, - place, - narrow_rel_base, - declared, - align, - ); + let (rel_base, region) = + rel_base_and_region(self.region, self.rel_base, place, declared, align); #[cfg(feature = "layout-diagnostics")] if region_node { diag::bump(Counter::RegionNodeDraws); @@ -230,15 +211,14 @@ impl<'a> Painter<'a> { region, placed: place, asked: place, - narrow_rel_base, re_asked, px, }, None, self.rsc, ); - let holds = self.in_parent(holds, region, place, narrow_rel_base, declared); - let answer_holds = self.in_parent(answer_holds, region, place, narrow_rel_base, declared); + let holds = self.in_parent(holds, region, place, declared); + let answer_holds = self.in_parent(answer_holds, region, place, declared); match self.under.iter_mut().find(|(child, _)| *child == id.id()) { Some((_, kept)) => *kept = holds, None => self.under.push((id.id(), holds)), @@ -273,12 +253,14 @@ impl<'a> Painter<'a> { pub fn place_at<'s, W: ?Sized>( &'s mut self, id: &'s StrongWidget, - place: [Place; 2], - narrow_rel_base: impl Into; 2]>>, + place: impl Into, ) -> DrawResult<'s, 'a, W> { - let narrow_rel_base = narrow_rel_base.into().unwrap_or([None; 2]); - if narrow_rel_base.iter().any(Option::is_some) || !self.children.contains(&id.id()) { - return self.widget_at(id, place, narrow_rel_base); + let place = self.state_rel_base(place.into()); + let states_rel_base = AXES + .iter() + .any(|&axis| place.axis(axis).stated_rel_base().is_some()); + if states_rel_base || !self.children.contains(&id.id()) { + return self.widget_at(id, place); } let at = self.placing(); self.state.place_in(id.id(), &at, place, self.rsc); @@ -619,8 +601,7 @@ impl Painter<'_> { &self, holds: LayoutHolds, region: UiRegion, - place: [Place; 2], - narrow_rel_base: [Option; 2], + place: PlaceDesc, declared: [Option; 2], ) -> LayoutHolds { let mut result = LayoutHolds::ANY; @@ -629,11 +610,12 @@ impl Painter<'_> { // Every read became pixels against the window, so a range on // it is already in this widget's terms. result.window[n] = holds.window[n]; - let reaches = narrow_rel_base[n].is_none() - && !matches!(place[n].part(), Part::Sized(_)) + let at = *place.axis(axis); + let reaches = at.stated_rel_base().is_none() + && !at.is_sized() && declared[n].is_none_or(|len| len.rel != Rel::ZERO); result.rel_base[n] = holds.rel_base[n].and(reaches.then(|| self.rel_base.axis(axis))); - match (place[n].part(), declared[n].is_some()) { + match (at.within_span(), declared[n].is_some()) { // Its box is a part of this widget's own box, in that box's // own lengths, so what it holds for maps back through that // part into a range on this widget's box. A length it pinned @@ -641,7 +623,7 @@ impl Painter<'_> { // part is the whole of the box less pixels, which is the one // shape that inverts exactly; any other part pins this // widget's own length. - (Part::Of(span), false) => { + (Some(span), false) => { let part_len = span.len(); result.region[n] = holds.region[n].through(part_len); result.region_len[n] = holds.region_len[n].map(|pinned| match part_len.rel { @@ -663,14 +645,6 @@ impl Painter<'_> { } } -/// A box of a widget's own, per axis, with the answer placed inside it. -fn region_places(region: UiRegion) -> [Place; 2] { - [ - Place::Within(Part::Of(region.x)), - Place::Within(Part::Of(region.y)), - ] -} - /// What a widget declares a length of its box to be. `leftover` is not one: a /// share of what is left over is only a length to the widget dividing one, /// so it passes up in the size instead. @@ -715,14 +689,14 @@ pub(crate) fn placement( region: UiRegion, size: Size, declared: [Option; 2], - fill: [bool; 2], + place: PlaceDesc, align: RegionAlign, ) -> UiRegion { let mut placed = region; for axis in AXES { let n = axis as usize; let reported = size.axis(axis); - if fills(reported, declared[n], fill[n]) { + if fills(reported, declared[n], place.axis(axis).does_fill()) { continue; } let len = Len::from_parts(reported.rel, reported.px); @@ -737,32 +711,27 @@ pub(crate) fn placement( /// widget asking draws in. /// /// `own` is that widget's own box, and `place` what of it the child is -/// given. `narrow_rel_base` is a rel base the container decided for the child -- a row's -/// slot, or padding's rel base less its pixels -- and [`Part::Sized`] one a -/// sibling's answer decided; both are window lengths, like every other -/// length here, since a slot of a row is not a fraction of anything the row -/// can name. The child's declaration is a fraction of whichever reached it, -/// and is the only one of the three that also places the box: a box the -/// caller decided is what `place` names. +/// given, including any rel base it states -- a row's slot, or padding's rel +/// base less its pixels. That is a window length, like every other length +/// here, since a slot of a row is not a fraction of anything the row can +/// name. The child's declaration is a fraction of whichever reached it, and +/// is the only one that also places the box: a box the caller decided is +/// what `place` names. pub(crate) fn rel_base_and_region( own: UiRegion, parent_rel_base: UiVec2, - place: [Place; 2], - narrow_rel_base: [Option; 2], + place: PlaceDesc, declared: [Option; 2], align: RegionAlign, ) -> (UiVec2, UiRegion) { - let given = region_of(own, place, align); + let given = place.of(own, align); let mut rel_base = parent_rel_base; let mut region = given; for axis in AXES { let n = axis as usize; - let sized = match place[n].part() { - Part::Sized(len) => Some(len), - _ => None, - }; - let base = sized - .or(narrow_rel_base[n]) + let base = place + .axis(axis) + .stated_rel_base() .unwrap_or_else(|| parent_rel_base.axis(axis)); let len = declared[n] .map(|len| Len::from_parts(len.rel, len.px).within_len(base)) @@ -776,15 +745,3 @@ pub(crate) fn rel_base_and_region( } (rel_base, region) } - -/// The part of a widget's own box a `place` names, in the coordinates that -/// box is in. -fn region_of(own: UiRegion, place: [Place; 2], align: RegionAlign) -> UiRegion { - let mut region = own; - for axis in AXES { - *region.axis_mut(axis) = place[axis as usize] - .part() - .of(*own.axis(axis), align.axis(axis)); - } - region -} diff --git a/core/src/ui/place.rs b/core/src/ui/place.rs index 6a24659..7e61d7b 100644 --- a/core/src/ui/place.rs +++ b/core/src/ui/place.rs @@ -1,70 +1,242 @@ -use crate::{AxisAlign, Len, PrimitiveHandle, UiRegion, UiSpan}; +use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan}; -/// A child's region along one axis, said as a part of the region the widget -/// saying it was given. +/// How a child's region along one axis comes from the region of the widget +/// asking, and what its fractions are of. +/// +/// The three ways of saying a region are the three the geometry already has: +/// a span composed into the caller's box, a span shifted to where that box +/// starts, and a length placed in it by alignment. Which one is meant cannot +/// be read off the numbers, since two of them take the same span and apply +/// it differently, so it is said here. #[derive(Clone, Copy, Debug, PartialEq)] -pub enum Part { - /// Window lengths from where the box starts, which is what a container - /// dividing room among its children speaks: a child's report is a window - /// length, so the cursor that sums those reports is one too. A moved box - /// re-places every child by re-adding its start, exactly. A fraction - /// here is a fraction of the window and not of the box -- the whole of a - /// box is [`Self::WHOLE`], not a `rel(1.0)` span. - From(UiSpan), - /// A part of the box in its own coordinates, which is what a container - /// that insets one speaks: taking eleven pixels off the end needs no - /// length, where saying the same thing in window lengths would make the - /// container read its own box -- and a box chosen from its own answer - /// then feeds back into the answer. - Of(UiSpan), - /// A box of this length, wherever in the parent's box the child's own - /// alignment puts it, and that same length as its rel base. Unlike `From`, - /// it is a length decided from above rather than a place along a - /// container's cursor -- what a stack's sizing child decides for the - /// rest. +pub struct PlaceDescAxis { + span: PlaceSpan, + fills: bool, + rel_base: RelBase, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +enum PlaceSpan { + Within(UiSpan), + Shifted(UiSpan), Sized(Len), } -impl Part { - /// The whole of the box. Not a variant of its own: it composes and - /// inverts through the same expressions every other `Of` does. - pub const WHOLE: Self = Self::Of(UiSpan::FULL); +/// What a child's fractions are of, where the caller has not named a length. +#[derive(Clone, Copy, Debug, PartialEq)] +enum RelBase { + /// The caller's own, unchanged. + Inherit, + /// The caller's own, narrowed the way the region is. + WithRegion, + /// This length of the window. + Len(Len), +} + +impl PlaceDescAxis { + /// The whole of the caller's box. + pub const WHOLE: Self = Self::within(UiSpan::FULL); + + /// `span` composed into the caller's own box, so it moves and scales + /// with it: [`UiSpan::within`], which is what a container that insets + /// one speaks. Taking eleven pixels off the end needs no length, where + /// saying the same thing in window lengths would make the container read + /// its own box -- and a box chosen from its own answer then feeds back + /// into the answer. + /// + /// The child's rel base is narrowed the same way, so padding takes its + /// pixels off both and `rel(1)` under it fills the caller rather than + /// overflowing it. + pub const fn within(span: UiSpan) -> Self { + Self { + span: PlaceSpan::Within(span), + fills: false, + rel_base: RelBase::WithRegion, + } + } + + /// `span` shifted to where the caller's own box starts: window lengths + /// along a cursor, which is what a container dividing room among its + /// children speaks. A child's report is a window length, so the cursor + /// that sums those reports is one too, and a moved box re-places every + /// child by re-adding its start, exactly. + /// + /// The child's rel base passes through: how far along the cursor a child + /// sits says nothing about what a fraction under it is of. + pub const fn shifted(span: UiSpan) -> Self { + Self { + span: PlaceSpan::Shifted(span), + fills: false, + rel_base: RelBase::Inherit, + } + } + + /// A box this long, placed in the caller's own by the child's alignment: + /// the rule that places an answer, with the length given from above + /// rather than reported. What a stack's sizing child decides for the + /// rest. It is the child's rel base too. + pub const fn sized(len: Len) -> Self { + Self { + span: PlaceSpan::Sized(len), + fills: false, + rel_base: RelBase::Len(len), + } + } + + /// This region is the child's placement: its answer is not placed inside + /// it again. A container uses it where it hands back exactly what the + /// child asked for -- a row placing a child at the length it reported. + pub const fn fills(mut self) -> Self { + self.fills = true; + self + } + + /// What the child's fractions are of, as a length of the window: a + /// resolved share, or a box a sibling's answer decided. + pub const fn rel_base(mut self, len: Len) -> Self { + self.rel_base = RelBase::Len(len); + self + } + + /// Whether the region is the placement outright, rather than a box the + /// answer is placed inside. + pub(crate) const fn does_fill(self) -> bool { + self.fills + } /// Where it lands in the coordinates `own` is in. pub(crate) fn of(self, own: UiSpan, align: AxisAlign) -> UiSpan { - match self { - Self::From(span) => UiSpan::new(own.start + span.start, own.start + span.end), - Self::Of(span) => span.within(&own), - Self::Sized(len) => { + match self.span { + PlaceSpan::Within(span) => span.within(&own), + PlaceSpan::Shifted(mut span) => { + span.shift(own.start); + span + } + PlaceSpan::Sized(len) => { let start = own.start + (own.len() - len).scale(align.rel()); UiSpan::new(start, start + len) } } } -} -/// A child's region along one axis, and what becomes of its placement in -/// that region once it has answered. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Place { - /// The placement is the child's answer, aligned inside the region by the - /// child's alignment. - Within(Part), - /// The region is the placement; the answer is not placed inside it again. - Fill(Part), -} - -impl Place { - pub(crate) fn part(self) -> Part { - match self { - Self::Within(part) | Self::Fill(part) => part, + /// The child's rel base, where this says one outright. `None` forwards + /// the caller's own, and [`RelBase::WithRegion`] is resolved by whoever + /// can read that rel base, so it does not reach here. + pub(crate) const fn stated_rel_base(self) -> Option { + match self.rel_base { + RelBase::Len(len) => Some(len), + _ => None, } } - /// Whether the region is the placement outright, rather than a box the - /// answer is placed inside. - pub(crate) fn fills(self) -> bool { - matches!(self, Self::Fill(_)) + /// The length this narrows the caller's rel base by, where it does. + /// `None` leaves that rel base alone, and reading it is then a + /// dependency the caller does not take. + pub(crate) const fn narrows_rel_base(self) -> Option { + match (self.rel_base, self.span) { + (RelBase::WithRegion, PlaceSpan::Within(span)) => Some(span), + _ => None, + } + } + + /// The span it composes into the caller's box, where that is what it + /// does: the one case whose validity maps back through the part. + pub(crate) const fn within_span(self) -> Option { + match self.span { + PlaceSpan::Within(span) => Some(span), + _ => None, + } + } + + /// Whether the caller decided this length rather than a place along its + /// own box, which is what stops its length reaching the child at all. + pub(crate) const fn is_sized(self) -> bool { + matches!(self.span, PlaceSpan::Sized(_)) + } + + /// The same, with its rel base stated outright. + pub(crate) const fn with_rel_base(mut self, len: Option) -> Self { + self.rel_base = match len { + Some(len) => RelBase::Len(len), + None => RelBase::Inherit, + }; + self + } +} + +/// Where a child is asked, on both axes. A [`UiRegion`] converts into the +/// common case: that box of the caller's own, the answer placed inside it. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct PlaceDesc { + pub x: PlaceDescAxis, + pub y: PlaceDescAxis, +} + +impl PlaceDesc { + /// The whole of the caller's box, on both axes. + pub const WHOLE: Self = Self::splat(PlaceDescAxis::WHOLE); + + pub const fn new(x: PlaceDescAxis, y: PlaceDescAxis) -> Self { + Self { x, y } + } + + /// The same on both axes. + pub const fn splat(place: PlaceDescAxis) -> Self { + Self { x: place, y: place } + } + + /// `aligned` on `axis` and `ortho` on the other, which is how a + /// container that divides one axis says what it is doing. + pub fn from_axis(axis: Axis, aligned: PlaceDescAxis, ortho: PlaceDescAxis) -> Self { + match axis { + Axis::X => Self::new(aligned, ortho), + Axis::Y => Self::new(ortho, aligned), + } + } + + pub const fn axis(&self, axis: Axis) -> &PlaceDescAxis { + match axis { + Axis::X => &self.x, + Axis::Y => &self.y, + } + } + + pub const fn axis_mut(&mut self, axis: Axis) -> &mut PlaceDescAxis { + match axis { + Axis::X => &mut self.x, + Axis::Y => &mut self.y, + } + } + + /// Both regions are the child's placement. See [`PlaceDescAxis::fills`]. + pub const fn fills(self) -> Self { + Self::new(self.x.fills(), self.y.fills()) + } + + /// The child's rel base on one axis. See [`PlaceDescAxis::rel_base`]. + pub const fn rel_base(mut self, axis: Axis, len: Len) -> Self { + *self.axis_mut(axis) = self.axis(axis).rel_base(len); + self + } + + /// The box each axis names, in the coordinates `own` is in. + pub(crate) fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion { + UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y)) + } +} + +impl From for PlaceDesc { + fn from(region: UiRegion) -> Self { + Self::new( + PlaceDescAxis::within(region.x), + PlaceDescAxis::within(region.y), + ) + } +} + +impl From for PlaceDesc { + fn from(place: PlaceDescAxis) -> Self { + Self::splat(place) } } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index b847c33..9c2160f 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -3,7 +3,7 @@ use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::ui::painter::{declared_lens, placement, rel_base_and_region}; use crate::{ ActiveData, Axis, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx, Moves, - Painter, Part, PixelRegion, Place, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, + Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, util::{HashMap, Vec2}, }; @@ -27,27 +27,16 @@ pub(super) struct DrawInfo { /// coordinates. pub region: UiRegion, /// Where the widget is put, and where it was asked, as parts of the - /// parent's box. See [`Place`]. The two are one ask's place until the + /// parent's box. See [`PlaceDesc`]. The two are one ask's place until the /// parent puts the answer somewhere else. - pub placed: [Place; 2], - pub asked: [Place; 2], - /// A rel base the parent decided for it on each axis, as a length of the - /// window, which the widget's own declaration is a fraction of. - pub narrow_rel_base: [Option; 2], + pub placed: PlaceDesc, + pub asked: PlaceDesc, /// Whether the parent already asked about this widget in this draw. pub re_asked: bool, /// The rel base in pixels, resolved once against the window. pub px: PxVec2, } -impl DrawInfo { - /// The axes where the part is the drawing's box outright, which are the - /// axes the answer is not placed inside it again. - fn fill(&self) -> [bool; 2] { - self.placed.map(Place::fills) - } -} - /// 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 { @@ -133,7 +122,7 @@ impl UiRenderState { } } - /// The root is asked about in the output. Its own rules narrow_rel_base both its + /// 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 { let px = rel_base.to_px(self.output_size); @@ -146,9 +135,8 @@ impl UiRenderState { mask: MaskIdx::NONE, rel_base, region, - placed: [Place::Within(Part::WHOLE); 2], - asked: [Place::Within(Part::WHOLE); 2], - narrow_rel_base: [None; 2], + placed: PlaceDesc::WHOLE, + asked: PlaceDesc::WHOLE, re_asked: false, px, } @@ -209,8 +197,7 @@ impl UiRenderState { rel_base_and_region( UiRegion::FULL, UiVec2::FULL_SIZE, - [Place::Within(Part::WHOLE); 2], - [None; 2], + PlaceDesc::WHOLE, declared_lens(widgets, id), widgets.alignment(id), ) @@ -248,7 +235,7 @@ impl UiRenderState { .then(|| self.retained_answer(id, region, info)) .flatten() .and_then(|answer| { - let placed = placement(region, answer.0, declared, info.fill(), align); + let placed = placement(region, answer.0, declared, info.placed, align); self.try_reuse(id, region, placed, info, rsc) .map(|()| answer) }); @@ -260,7 +247,7 @@ impl UiRenderState { // Where the drawing goes: the part its parent gave it, with the // answer placed inside that part on any axis the parent left // open. - let placed = placement(region, answer.0, declared, info.fill(), align); + let placed = placement(region, answer.0, declared, info.placed, align); if placed != region { self.relocate(id, placed, info, rsc); } @@ -273,7 +260,6 @@ impl UiRenderState { // and what of its own box it asked in. A local redraw asks the same // question again from these. active.rel_base = info.rel_base; - active.narrow_rel_base = info.narrow_rel_base; active.re_asked = info.re_asked; active.answer = Some(answer); active.asked = info.asked; @@ -468,9 +454,8 @@ impl UiRenderState { mask, rel_base: UiVec2::FULL_SIZE, region: UiRegion::FULL, - placed: [Place::Within(Part::WHOLE); 2], - asked: [Place::Within(Part::WHOLE); 2], - narrow_rel_base: [None; 2], + placed: PlaceDesc::WHOLE, + asked: PlaceDesc::WHOLE, re_asked: false, px, }, @@ -484,7 +469,6 @@ impl UiRenderState { id, placement: region, rel_base: info.rel_base, - narrow_rel_base: info.narrow_rel_base, placed: info.placed, asked: info.asked, region, @@ -718,7 +702,7 @@ impl UiRenderState { &mut self, child: WidgetId, at: &Placing, - place: [Place; 2], + place: PlaceDesc, rsc: &mut dyn UiRsc, ) { let active = &self.active[&child]; @@ -727,7 +711,7 @@ impl UiRenderState { region, active.measured().unwrap_or(active.size), active.declared, - place.map(Place::fills), + place, active.own_align, ); let info = DrawInfo { @@ -741,7 +725,6 @@ impl UiRenderState { region, placed: place, asked: active.asked, - narrow_rel_base: active.narrow_rel_base, re_asked: active.re_asked, px: rel_base.to_px(at.window), }; @@ -752,12 +735,11 @@ impl UiRenderState { /// the box its parent is being taken as. What narrowed its rel base and what /// it declared are its own record's, so both are resolved against that /// parent's rel base again exactly as the first ask resolved them. - fn ask_again(active: &ActiveData, at: &Placing, place: [Place; 2]) -> (UiVec2, UiRegion) { + fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) { rel_base_and_region( at.region, at.rel_base, place, - active.narrow_rel_base, active.declared, active.own_align, ) @@ -886,9 +868,8 @@ impl UiRenderState { id, placement: UiRegion::FULL, rel_base: UiVec2::FULL_SIZE, - narrow_rel_base: [None; 2], - placed: [Place::Within(Part::WHOLE); 2], - asked: [Place::Within(Part::WHOLE); 2], + placed: PlaceDesc::WHOLE, + asked: PlaceDesc::WHOLE, region: UiRegion::FULL, answer: None, re_asked: false, @@ -1146,7 +1127,6 @@ impl UiRenderState { region, placed: active.asked, asked: active.asked, - narrow_rel_base: active.narrow_rel_base, re_asked: false, px: rel_base.to_px(self.output_size), }; @@ -1171,7 +1151,7 @@ impl UiRenderState { } 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_rel_base the range safe for a resize. + // even an unchanged size can narrow the range safe for a resize. #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::SizeChanges); diff --git a/src/random.rs b/src/random.rs index 7dd9531..5c49e30 100644 --- a/src/random.rs +++ b/src/random.rs @@ -118,9 +118,9 @@ pub struct Branch { impl Widget for Branch { fn draw(&mut self, painter: &mut Painter) -> Size { let cut = Len::from_parts(Rel::ZERO, Px::from_int(40)); - let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut))); + let top = PlaceDescAxis::shifted(UiSpan::new(Len::ZERO, cut)); let measured = painter - .widget_at(&self.probe, [Place::Within(Part::WHOLE), top], None) + .widget_at(&self.probe, PlaceDesc::new(PlaceDescAxis::WHOLE, top)) .len(Axis::X); let len = measured.apply_leftover(); let px = painter.to_px(len, Axis::X); @@ -134,11 +134,11 @@ impl Widget for Branch { }; painter.window_holds(Axis::X, holds.through(len)); - let below = Place::Within(Part::From(UiSpan::new(cut, painter.region_len(Axis::Y)))); - let place = [Place::Within(Part::WHOLE), below]; + let below = PlaceDescAxis::shifted(UiSpan::new(cut, painter.region_len(Axis::Y))); + let place = PlaceDesc::new(PlaceDescAxis::WHOLE, below); match px > threshold { - true => painter.widget_at(&self.wide, place, None), - false => painter.widget_at(&self.narrow, place, None), + true => painter.widget_at(&self.wide, place), + false => painter.widget_at(&self.narrow, place), }; Size::LEFTOVER } diff --git a/src/widget/position/offset.rs b/src/widget/position/offset.rs index 572f5f5..c899ec1 100644 --- a/src/widget/position/offset.rs +++ b/src/widget/position/offset.rs @@ -8,7 +8,7 @@ pub struct Offset { impl Widget for Offset { fn draw(&mut self, painter: &mut Painter) -> Size { painter - .widget_within(&self.inner, UiRegion::FULL.offset(self.amt)) + .widget_at(&self.inner, UiRegion::FULL.offset(self.amt)) .size() } } diff --git a/src/widget/position/pad.rs b/src/widget/position/pad.rs index 7d001ae..d180e99 100644 --- a/src/widget/position/pad.rs +++ b/src/widget/position/pad.rs @@ -19,9 +19,7 @@ impl Widget for Pad { // padding, and it comes off the box, so what is drawn sits inside. // The two stay distinct -- the box can be narrower still, where a row // asked this widget in the room left, and a text wraps at that. - let inner = painter - .widget_within(&self.inner, self.padding.region()) - .size(); + let inner = painter.widget_at(&self.inner, self.padding.region()).size(); Size { x: LayoutLen { px: inner.x.px + self.padding.left + self.padding.right, diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index 84c8fe7..ab369b0 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -14,7 +14,7 @@ impl Widget for Scroll { let container_len = painter.px_len(self.axis); // Asked in the whole viewport, then put at the scrolled offset. let answer_len = painter - .widget_at(&self.inner, [Place::Fill(Part::WHOLE); 2], None) + .widget_at(&self.inner, PlaceDesc::WHOLE.fills()) .len(self.axis); let fixed = painter.to_px(Len::from_parts(answer_len.rel, answer_len.px), self.axis); self.container_len = container_len; @@ -54,9 +54,9 @@ impl Widget for Scroll { let content = match moved || self.content_len != self.container_len { true => { let start = Len::from_parts(Rel::ZERO, anchor - self.amt); - Part::From(UiSpan::new(start, start.offset(self.content_len))) + PlaceDescAxis::shifted(UiSpan::new(start, start.offset(self.content_len))) } - false => Part::WHOLE, + false => PlaceDescAxis::WHOLE, }; // The viewport is the inner's rel base, so a fraction it declares or // reports is a fraction of what is on screen rather than of the @@ -64,9 +64,7 @@ impl Widget for Scroll { // box, scrolled: its drawing moved there, not made again there. painter.place_at( &self.inner, - self.axis - .pair(Place::Fill(content), Place::Fill(Part::WHOLE)), - None, + PlaceDesc::from_axis(self.axis, content.fills(), PlaceDescAxis::WHOLE.fills()), ); // What it occupies is its box, on both axes: it clips its content to // that box, so it can neither take less of one nor honestly ask for diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 9acdc35..435ccf2 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -21,7 +21,7 @@ impl Widget for Span { // Across itself the child sits where its own alignment says, in the // whole of the row: a span is what contains its children there, and // nothing divides that axis. - let across = Place::Within(Part::WHOLE); + let across = PlaceDescAxis::WHOLE; // A length for every child before their final slots are chosen: from // a hint where one says, and from drawing otherwise. The rel base passes // through unchanged, so `rel(0.5)` is half the area this span was @@ -34,9 +34,9 @@ impl Widget for Span { let len = match painter.size_hint(child, axis) { Some(len) => len, None => { - let room = Place::Within(Part::From(along(cursor, far))); + let room = PlaceDescAxis::shifted(along(cursor, far)); painter - .widget_at(child, axis.pair(room, across), None) + .widget_at(child, PlaceDesc::from_axis(axis, room, across)) .len(axis) } }; @@ -120,10 +120,12 @@ impl Widget for Span { // fixed child's slot is its own answer, so a drawing made in the // room is put there as it is, and one not made yet is made here. let slot = along(from, start); - let place = axis.pair(Place::Fill(Part::From(slot)), across); - let narrow = - (len.leftover > Weight::ZERO && shares).then(|| axis.pair(Some(slot.len()), None)); - let used = painter.place_at(child, place, narrow).len(!axis); + let slot_place = PlaceDescAxis::shifted(slot).fills(); + let mut place = PlaceDesc::from_axis(axis, slot_place, across); + if len.leftover > Weight::ZERO && shares { + place = place.rel_base(axis, slot.len()); + } + let used = painter.place_at(child, place).len(!axis); if shrinks { // Choosing between a fixed and a relative length from the // span's own eventual width admits multiple fixed points. diff --git a/src/widget/position/stack.rs b/src/widget/position/stack.rs index 874e199..17d15ff 100644 --- a/src/widget/position/stack.rs +++ b/src/widget/position/stack.rs @@ -22,9 +22,7 @@ impl Widget for Stack { // drawing belongs to the layer it was made on. Some((i, child)) => { painter.child_layer_at(i); - painter - .widget_at(child, [Place::Fill(Part::WHOLE); 2], None) - .size() + painter.widget_at(child, PlaceDesc::WHOLE.fills()).size() } None => Size::LEFTOVER, }; @@ -33,19 +31,20 @@ impl Widget for Stack { // fraction under them is a fraction of it. A share leaves the axis // to whoever gave the stack its box. Where a child sits in a box // bigger than itself is its own business. - let place = [Axis::X, Axis::Y].map(|axis| { + let on = |axis| { let len = size.axis(axis); match len.leftover == Weight::ZERO { - true => Place::Fill(Part::Sized(Len::from_parts(len.rel, len.px))), - false => Place::Within(Part::WHOLE), + true => PlaceDescAxis::sized(Len::from_parts(len.rel, len.px)).fills(), + false => PlaceDescAxis::WHOLE, } - }); + }; + let place = PlaceDesc::new(on(Axis::X), on(Axis::Y)); for (i, child) in self.children.iter().enumerate() { if sizing == Some(i) { continue; } painter.child_layer_at(i); - painter.widget_at(child, place, None); + painter.widget_at(child, place); } size } diff --git a/tests/cases/determinism.rs b/tests/cases/determinism.rs index eacaf26..3c78c16 100644 --- a/tests/cases/determinism.rs +++ b/tests/cases/determinism.rs @@ -22,17 +22,17 @@ struct BranchesOnMeasurement { impl Widget for BranchesOnMeasurement { fn draw(&mut self, painter: &mut Painter) -> Size { let cut = Len::from_parts(Rel::ZERO, Px::from_int(40)); - let top = Place::Within(Part::From(UiSpan::new(Len::ZERO, cut))); + let top = PlaceDescAxis::shifted(UiSpan::new(Len::ZERO, cut)); let measured = painter - .widget_at(&self.probe, [Place::Within(Part::WHOLE), top], None) + .widget_at(&self.probe, PlaceDesc::new(PlaceDescAxis::WHOLE, top)) .len(Axis::X); let px = painter.to_px(measured.apply_leftover(), Axis::X); - let below = Place::Within(Part::From(UiSpan::new(cut, painter.region_len(Axis::Y)))); - let place = [Place::Within(Part::WHOLE), below]; + let below = PlaceDescAxis::shifted(UiSpan::new(cut, painter.region_len(Axis::Y))); + let place = PlaceDesc::new(PlaceDescAxis::WHOLE, below); match px > Px::from_f32(self.threshold) { - true => painter.widget_at(&self.wide, place, None), - false => painter.widget_at(&self.narrow, place, None), + true => painter.widget_at(&self.wide, place), + false => painter.widget_at(&self.narrow, place), }; Size::LEFTOVER } diff --git a/tests/cases/retained.rs b/tests/cases/retained.rs index 42229fa..a9254fc 100644 --- a/tests/cases/retained.rs +++ b/tests/cases/retained.rs @@ -216,8 +216,7 @@ impl Widget for FromHint { let top = UiSpan::new(Len::ZERO, Len::from_parts(Rel::ZERO, len.px)); painter.widget_at( &self.inner, - [Place::Within(Part::WHOLE), Place::Within(Part::From(top))], - None, + PlaceDesc::new(PlaceDescAxis::WHOLE, PlaceDescAxis::shifted(top)), ); Size::LEFTOVER } @@ -877,11 +876,10 @@ fn resizing_a_fixed_frame_recomposes_its_contents_without_drawing_them() { fn draw(&mut self, painter: &mut Painter) -> Size { painter.widget_at( &self.child, - [ - Place::Within(Part::From(self.region.x)), - Place::Within(Part::From(self.region.y)), - ], - None, + PlaceDesc::new( + PlaceDescAxis::shifted(self.region.x), + PlaceDescAxis::shifted(self.region.y), + ), ); Size::LEFTOVER } @@ -966,11 +964,11 @@ fn glyph_origins_compose_identically_when_drawn_and_when_retained() { fn draw(&mut self, painter: &mut Painter) -> Size { painter.widget_at( &self.child, - [ - Place::Fill(Part::From(self.region.x)), - Place::Fill(Part::From(self.region.y)), - ], - [Some(self.frame.x.len()), None], + PlaceDesc::new( + PlaceDescAxis::shifted(self.region.x).fills(), + PlaceDescAxis::shifted(self.region.y).fills(), + ) + .rel_base(Axis::X, self.frame.x.len()), ); Size::LEFTOVER } @@ -1136,11 +1134,10 @@ fn padding_and_stack_boxes_follow_the_region_without_drawing_again() { fn draw(&mut self, painter: &mut Painter) -> Size { painter.widget_at( &self.child, - [ - Place::Fill(Part::From(self.region.x)), - Place::Fill(Part::From(self.region.y)), - ], - None, + PlaceDesc::new( + PlaceDescAxis::shifted(self.region.x).fills(), + PlaceDescAxis::shifted(self.region.y).fills(), + ), ); Size::LEFTOVER } @@ -1224,14 +1221,14 @@ fn moving_a_childs_region_preserves_the_slot_chosen_from_its_measurement() { fn draw(&mut self, painter: &mut Painter) -> Size { painter.widget_at( &self.child, - [ - Place::Fill(Part::From(UiSpan::new( + PlaceDesc::new( + PlaceDescAxis::shifted(UiSpan::new( Len::px(self.start), Len::px(self.start + 200.0), - ))), - Place::Fill(Part::From(UiSpan::FULL)), - ], - None, + )) + .fills(), + PlaceDescAxis::shifted(UiSpan::FULL).fills(), + ), ); Size::LEFTOVER } @@ -1266,11 +1263,10 @@ fn changing_regions_keep_fractional_reports_and_numeric_dependencies_valid() { painter .widget_at( &self.child, - [ - Place::Within(Part::From(self.region.x)), - Place::Within(Part::From(self.region.y)), - ], - None, + PlaceDesc::new( + PlaceDescAxis::shifted(self.region.x), + PlaceDescAxis::shifted(self.region.y), + ), ) .size() } @@ -1286,11 +1282,10 @@ fn changing_regions_keep_fractional_reports_and_numeric_dependencies_valid() { painter .widget_at( &self.child, - [ - Place::Fill(Part::From(self.region.x)), - Place::Fill(Part::From(self.region.y)), - ], - None, + PlaceDesc::new( + PlaceDescAxis::shifted(self.region.x).fills(), + PlaceDescAxis::shifted(self.region.y).fills(), + ), ) .size(), );