Constrain offered boxes with independent widget size bounds

This commit is contained in:
iris-ai committed 2026-09-20 19:47:32 -04:00
1 parent 4cb6f6882a
commit 2ac0843cb2
18 files changed
+554 -379

No files matched your search

+24 -28
View File
@@ -254,7 +254,6 @@ impl UiRenderState {
);
}
let align = rsc.widgets().alignment(id);
let declared = info.declared;
// Nothing this widget measured can be dirty while it draws: layout is
// one bottom-up walk, so anything deeper has settled or deferred to
// its own parent, and a deferred one leaves that parent marked.
@@ -268,7 +267,7 @@ impl UiRenderState {
.then(|| self.retained_answer(id, region, info))
.flatten()
.and_then(|answer| {
let placed = info.placed.placement(region, answer.size, declared, align);
let placed = info.placed.placement(region, answer.size, align);
self.try_reuse(id, region, placed, info, rsc)
.then_some(answer)
});
@@ -280,7 +279,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 = info.placed.placement(region, answer.size, declared, align);
let placed = info.placed.placement(region, answer.size, align);
if placed != region {
self.relocate(id, placed, info, rsc);
}
@@ -379,10 +378,7 @@ impl UiRenderState {
size_deps,
request_deps,
scratch,
// 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,
own: LayoutHolds::ANY,
under,
answer_under: LayoutHolds::ANY,
depth: info.depth,
@@ -437,9 +433,9 @@ impl UiRenderState {
// rel base is the answer wherever the ask declared a length: it was
// resolved into the rel base when the widget was asked, and resolving
// it again here would take the fraction of a fraction.
let rules = rsc.widgets().size_rules(id).clone();
let rules = rsc.widgets().size_rules(id);
let ruled = |axis: Axis, reported: LayoutLen| {
if matches!(rules[axis], crate::SizeRule::Request(_)) {
if rules[axis].deferred().is_some() {
return info.rel_base[axis].into();
}
match rules[axis].exact() {
@@ -470,18 +466,30 @@ impl UiRenderState {
// it is a length only to whoever divides one, and the box that
// divider gives is a box this widget is asked in, where the bound is
// applied to it.
let mut bounded = LayoutHolds::ANY;
// Widgets may widen their own read ranges, but not the ask's constraints.
let mut own_holds = own.and(info.ask_holds);
for axis in Axis::BOTH {
let answer = size[axis];
if answer.leftover != Weight::ZERO {
continue;
}
let (held, kept) = info.bounds[axis].outside(answer.without_leftover(), window[axis]);
bounded[axis].window = kept;
own_holds[axis].window = own_holds[axis].window.and(kept);
if let Some(held) = held {
size[axis] = held.into();
}
}
// A rule that is a fraction of the rel base is answered with the
// rel base's own length, so the answer is that rel base's and not just
// that many pixels of this window -- the same pin a widget that read
// its rel base took for its drawing. A bound counts: which side of it
// the box fell was decided against this rel base, and the same box of
// a different one can fall on the other.
for axis in Axis::BOTH {
if rules[axis].has_fraction() {
own_holds[axis].rel_base = Some(info.rel_base[axis]);
}
}
// A widget that clipped its contents to its box drew nothing outside
// it, so reporting more than the box asks to be placed at a length it
// does not occupy -- and its parent would place the part it cut off.
@@ -509,18 +517,6 @@ impl UiRenderState {
if let Some(idx) = retired_move {
self.moves.remove(idx);
}
// A rule that is a fraction of the rel base is answered with the
// rel base's own length, so the answer is that rel base's and not just
// that many pixels of this window -- the same pin a widget that read
// its rel base took for its drawing. A bound counts: which side of it
// the box fell was decided against this rel base, and the same box of
// a different one can fall on the other.
let mut own_holds = own.and(bounded);
for axis in Axis::BOTH {
if rules[axis].has_fraction() {
own_holds[axis].rel_base = Some(info.rel_base[axis]);
}
}
let answer_holds = own_holds.and(answer_under);
let holds = under
.iter()
@@ -798,7 +794,6 @@ impl UiRenderState {
let placed = place.placement(
region,
active.measured().unwrap_or(active.size),
active.declared,
active.own_align,
);
let info = DrawInfo {
@@ -823,8 +818,8 @@ impl UiRenderState {
/// The rel base and the box a widget already drawn is given at `place` of
/// 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.
/// it declared are its own record's. Declared lengths stay as the ask
/// resolved them; the destination only decides where they sit.
fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) {
place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align)
}
@@ -1195,10 +1190,11 @@ impl UiRenderState {
active.asked,
);
let active = &self.active[&id];
let declared_changed = ask.declared != active.declared;
// Even an inactive bound changes what the parent must track about its offer.
let constraints_changed = ask.declared != active.declared || ask.bounds != active.bounds;
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
if let Some(parent) = active.parent
&& (declared_changed
&& (constraints_changed
|| alignment_changed
|| active.re_asked
|| !active.drawn