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<PlaceDesc>`, 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`.
This commit is contained in:
iris-ai committed 2026-09-19 17:59:26 -04:00
1 parent c55be21761
commit 58ce74dd7d
12 files changed
+367 -271

No files matched your search

+57 -100
View File
@@ -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<W>) -> 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<W>,
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<W>,
place: [Place; 2],
narrow_rel_base: impl Into<Option<[Option<Len>; 2]>>,
place: impl Into<PlaceDesc>,
) -> 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<W>,
place: [Place; 2],
narrow_rel_base: impl Into<Option<[Option<Len>; 2]>>,
place: impl Into<PlaceDesc>,
) -> 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<Len>; 2],
place: PlaceDesc,
declared: [Option<LayoutLen>; 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<LayoutLen>; 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<Len>; 2],
place: PlaceDesc,
declared: [Option<LayoutLen>; 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
}