Read a leftover as a minimum where nothing divides it
A share under a parent that divides nothing is still a share: the pixels and
fraction beside it are taken first, the share fills whatever the box has left,
and where those are already longer than the box they overflow it exactly as
they would without the share. So the length is `max(box, px + rel*box)`, a
minimum the share imposes rather than an addition to what was asked for
(Bryan, 2026-09-20, generalising the same `max` he gave for `Scroll`'s content
length two days earlier).
A span does that. Measured at `77ed7a2`, a probe recording the box it is asked
in, in a 400 px window, under `.wrapper()` against a one-child span:
rule nothing divides a span divides
leftover(1) 400 400
px(50) + leftover(1) 400 400
px(500) + leftover(1) 400 500
rel(0.5) + leftover(1) 400 400
px(500), no leftover 500 500
One row disagreed, and the same length without the share overflows fine
(drawn -50..450, its alignment centring it), so what swallowed the overflow was
the share. `LayoutLen::declared` refuses to answer for anything carrying
leftover weight, so the non-dividing path never learned the fixed part and fell
back to the offer.
Said as the place the parent gives rather than as a declaration, because that
is what the retained record already keeps: where the fixed part is the longer,
`widget_at` hands the child `fixed.as_desc().fills()` -- a box of that length,
placed by the child's alignment, its own rel base -- which is what a declared
length already comes to, and `active.placed` stores it, so a recomposed subtree
reads the same box without resolving anything again. A place that is already
the child's placement is skipped: a parent that divides has given the share
whatever it was owed, and re-placing a span's slot moved its child.
Which of two lengths is longer is a question in pixels, so it is one operation
with the crossing kept as a window range, and both callers now share it.
`Painter::longer_than` is that operation -- the span's room for the shares it
divides, and a share past the box it was given -- and it narrows this widget's
range where the span replaced it, since a comparison the framework makes on an
arbitrary parent's behalf is one more reason its drawing holds, not the only
one. A `SizeRule::Min` of `rel(1.0)` is the same operation again, which is what
this is (Bryan, 2026-09-20); when that lands it belongs on this path.
`a_share_is_a_minimum_wherever_nothing_divides_it` walks the table above and
holds the two parents to the same length; the crossing case is checked from
both sides, by a window that crosses it and by the rule itself crossing while
the window holds still. Both fail at `77ed7a2` with 400 where 500 is wanted. A
change of rule needs nothing to escalate it: the reported size is the rule
resolved, so the answer changes and the parent refuses its own drawing --
verified by writing the escalation, finding the tests pass without it, and
dropping it.
Format, clippy with and without layout-diagnostics, and the suite (134 + 19 +
13 + 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
`77ed7a2` across all 34,488 boxes, since no generated tree carries a share with
pixels beside it -- which the next commit changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c2b8bf83de
commit
b295c8b97a
4 files changed
+164
-24
No files matched your search
+86
-6
@@ -187,10 +187,20 @@ impl<'a> Painter<'a> {
|
||||
id: &'s StrongWidget<W>,
|
||||
place: impl Into<PlaceDesc>,
|
||||
) -> DrawResult<'s, 'a, W> {
|
||||
let place = self.resolve_rel_base(place.into());
|
||||
let mut place = self.resolve_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());
|
||||
// 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")]
|
||||
@@ -299,6 +309,45 @@ impl<'a> Painter<'a> {
|
||||
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<Len> {
|
||||
// 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
|
||||
@@ -475,6 +524,40 @@ 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.
|
||||
///
|
||||
/// 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);
|
||||
debug_assert!(
|
||||
holds.contains(window),
|
||||
"'{}' ({:?}) compared two lengths and kept a range without this window",
|
||||
self.label(),
|
||||
self.id
|
||||
);
|
||||
self.own[axis].window = self.own[axis].window.and(holds);
|
||||
longer
|
||||
}
|
||||
|
||||
/// The windows this drawing holds for, stated rather than taken: a
|
||||
/// container that branched on a length in pixels says which side of the
|
||||
/// boundary it was on, which is wider than the one window reading that
|
||||
@@ -725,10 +808,7 @@ impl PlaceDesc {
|
||||
let mut rel_base = parent_rel_base;
|
||||
let mut region = given;
|
||||
for axis in Axis::BOTH {
|
||||
let base = match self[axis].rel_base {
|
||||
RelBase::Len(len) => len,
|
||||
RelBase::Inherit | RelBase::WithRegion => parent_rel_base[axis],
|
||||
};
|
||||
let base = self.base(axis, parent_rel_base);
|
||||
let len = declared[axis]
|
||||
.map(|len| len.within_len(base))
|
||||
.unwrap_or(base);
|
||||
|
||||
+11
-1
@@ -1,5 +1,5 @@
|
||||
use crate::util::impl_axis_index;
|
||||
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan};
|
||||
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan, UiVec2};
|
||||
|
||||
/// How a child's region along one axis comes from the region of the widget
|
||||
/// asking, and what its fractions are of.
|
||||
@@ -123,6 +123,16 @@ impl PlaceDesc {
|
||||
self
|
||||
}
|
||||
|
||||
/// What a child's fractions on one axis are of, as a length of the
|
||||
/// window: a length this place names, or the rel base of the widget
|
||||
/// giving it, which is `parent_rel_base`.
|
||||
pub(super) fn base(&self, axis: Axis, parent_rel_base: UiVec2) -> Len {
|
||||
match self[axis].rel_base {
|
||||
RelBase::Len(len) => len,
|
||||
RelBase::Inherit | RelBase::WithRegion => parent_rel_base[axis],
|
||||
}
|
||||
}
|
||||
|
||||
/// The box each axis names, in the coordinates `own` is in.
|
||||
pub fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion {
|
||||
UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y))
|
||||
|
||||
Reference in new issue
Block a user