Resolve deferred size comparisons before allocating span slots

This commit is contained in:
iris-ai committed 2026-09-20 16:50:18 -04:00
1 parent de1eb7e406
commit 8780b40bb7
23 files changed
+1471 -130

No files matched your search

+4 -4
View File
@@ -292,7 +292,7 @@ impl Plan {
align: None,
..self.clone()
}),
self.size.map(|_| Plan {
self.size.as_ref().map(|_| Plan {
size: None,
..self.clone()
}),
@@ -373,7 +373,7 @@ impl Plan {
}
if plan.size.is_some() {
if let Some(lens) = edits.sizes.get(&sized) {
plan.size = Some(*lens);
plan.size = Some(lens.clone());
}
sized += 1;
}
@@ -716,7 +716,7 @@ impl Sow<'_> {
}
let idx = self.sized;
self.sized += 1;
inner.size = Some(self.edits.sizes.get(&idx).copied().unwrap_or(lens));
inner.size = Some(self.edits.sizes.get(&idx).cloned().unwrap_or(lens));
}
/// An alignment over some of the tree, kept where a test can change it.
@@ -852,7 +852,7 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
fn node(&mut self, plan: &Plan) -> StrongWidget {
let built = self.kind(&plan.kind);
let id = built.id();
if let Some(lens) = plan.size {
if let Some(lens) = plan.size.clone() {
self.rsc.ui_mut().widgets.set_size_rules(id, lens.x, lens.y);
self.tree.sized.push(id);
}
+4
View File
@@ -6,6 +6,10 @@ pub struct LayerOffset {
}
impl Widget for LayerOffset {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
requests.widget(&self.inner, axis)
}
fn draw(&mut self, painter: &mut Painter) -> Size {
for _ in 0..self.offset {
painter.next_layer();
+8
View File
@@ -30,6 +30,14 @@ impl MaxSize {
}
impl Widget for MaxSize {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
let inner = requests.widget(&self.inner, axis)?;
Some(match self.max(axis) {
Some(max) => requests.min(inner, max.into()),
None => inner,
})
}
fn draw(&mut self, painter: &mut Painter) -> Size {
let align = painter.alignment();
let mut region = UiRegion::FULL;
+4
View File
@@ -6,6 +6,10 @@ pub struct Offset {
}
impl Widget for Offset {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
requests.widget(&self.inner, axis)
}
fn draw(&mut self, painter: &mut Painter) -> Size {
painter
.widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
+8
View File
@@ -6,6 +6,14 @@ pub struct Pad {
}
impl Widget for Pad {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
let padding = match axis {
Axis::X => self.padding.left + self.padding.right,
Axis::Y => self.padding.top + self.padding.bottom,
};
requests.inset(&self.inner, axis, padding)
}
fn draw(&mut self, painter: &mut Painter) -> Size {
// The inner's own alignment, not the near edge. This reports the
// inner's size plus the padding, so where the box is that answer the
+109 -49
View File
@@ -8,57 +8,79 @@ pub struct Span {
}
impl Widget for Span {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
if axis != self.dir.axis {
// A share can be hidden when the other axis has no room. Its
// cross-axis length then contributes nothing to the drawn answer.
return None;
}
let mut total = RequestedLen::from(Len::from_parts(
Rel::ZERO,
self.gap
.mul_int(self.children.len().saturating_sub(1) as i32),
));
for child in &self.children {
let child = requests.widget(child, axis)?;
total = requests.sum(total, child);
}
Some(total)
}
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.with_requests(|painter, lens, values| self.layout(painter, lens, values))
}
}
impl Span {
fn layout(
&self,
painter: &mut Painter,
lens: &mut Vec<RequestedLen>,
values: &mut Vec<Px>,
) -> Size {
let axis = self.dir.axis;
// The row this span lays its children out along, as a length of the
// rel base they are laid out against. Where it starts is nothing's
// business -- a slot is a length from there -- so what this reads is
// the length alone.
let row = painter.region_len(axis);
// 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
// given whatever else is in it and wherever this child sits among
// them; what a drawn child is asked in is the room left from the
// cursor, because a text has to wrap at the width actually there.
let mut cursor = Len::ZERO;
let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children {
let len = match painter.size_hint(child, axis) {
Some(len) => len,
None => {
// 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 room = self.slot(row, cursor, row).shifted_desc().on_axis(axis);
painter.widget_at(child, room).len(axis)
}
};
cursor += len.without_leftover();
cursor.px += self.gap;
lens.push(len);
}
self.collect(painter, row, lens, true);
let gaps = self
.gap
.mul_int(self.children.len().saturating_sub(1) as i32);
let total = lens.iter().fold(
LayoutLen {
px: gaps,
let fixed = lens
.iter()
.try_fold(Len::from_parts(Rel::ZERO, gaps), |sum, len| {
Some(sum + len.linear()?.without_leftover())
});
if let Some(fixed) = fixed
&& lens.iter().any(|len| len.has_leftover())
&& !painter.longer_than(row, fixed, axis)
{
// With no share to assign, intrinsic drawings keep the remaining
// offer, including overflow. Their answer is only moved into a slot.
self.collect(painter, row, lens, false);
}
let nonlinear = lens.iter().any(|len| len.linear().is_none());
if nonlinear {
painter.allocate(lens, row - Len::from_parts(Rel::ZERO, gaps), axis, values);
}
let allocated = nonlinear.then_some(&values);
let total = match &allocated {
Some(allocated) => LayoutLen {
px: allocated.iter().fold(gaps, |sum, len| sum + *len),
..LayoutLen::ZERO
},
|sum, len| sum + *len,
);
// What is left for the shares to divide: the row less everything
// fixed, as a length of the rel base rather than a number of pixels.
None => lens.iter().fold(
LayoutLen {
px: gaps,
..LayoutLen::ZERO
},
|sum, len| sum + len.linear().unwrap(),
),
};
let all_fixed = total.without_leftover();
let room = row - all_fixed;
// The three cases a rounded division needed -- the fixed parts
// growing slower than the box, faster, or exactly with it -- are the
// sign of `room.rel`, which the range `longer_than` keeps already
// reads. What the generated oracle checks is the consequence, since
// which children exist at all turns on this.
let any_leftover = total.leftover > Weight::ZERO;
let has_room = any_leftover && painter.longer_than(row, all_fixed, axis);
@@ -83,17 +105,30 @@ impl Widget for Span {
false => fixed,
true => fixed + room.scale(Rel::ratio(taken, total.leftover)),
};
for (child, &len) in self.children.iter().zip(&lens) {
for (index, (child, request)) in self.children.iter().zip(lens.iter()).enumerate() {
let len = match &allocated {
Some(allocated) => LayoutLen {
px: allocated[index],
..LayoutLen::ZERO
},
None => request.linear().unwrap(),
};
let shares = match &allocated {
Some(_) => request.has_leftover(),
None => len.leftover > Weight::ZERO && has_room,
};
// A child asking for nothing but a part of what is left over,
// when nothing is, is not drawn at all. One that also asked for
// pixels or a fraction keeps those and overflows.
if len.is_only_leftover() && !has_room {
if (len.is_only_leftover() && !has_room)
|| (allocated.is_some() && shares && len.px == Px::ZERO)
{
painter.undraw(child);
fixed.px += self.gap;
continue;
}
let from = reached(fixed, taken);
if len.leftover > Weight::ZERO && has_room {
if shares {
taken += len.leftover;
}
fixed += len.without_leftover();
@@ -106,8 +141,8 @@ 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 = self.slot(row, from, to);
let mut place = slot.shifted_desc().fills().on_axis(axis);
if len.leftover > Weight::ZERO && has_room {
let mut place = slot.shifted_desc().allocated().on_axis(axis);
if shares {
place = place.rel_base(axis, slot.len());
}
let used = painter.place_at(child, place).len(!axis);
@@ -125,13 +160,8 @@ impl Widget for Span {
fixed.px += self.gap;
}
// Carried whole rather than collapsed to one share: a span that sizes
// from its children does not resolve `leftover`, it passes the weight up,
// so nesting spans divides the same space rather than re-dividing a
// share of it. Four `leftover(1)` children under two spans under one span
// get a quarter each, which collapsing to `leftover(1)` per level does
// not give. Resolution happens at the nearest ancestor with a length,
// and the root always has one.
// Discovery carries nested requests to the allocating ancestor. The
// draw still returns an ordinary Size for callers measuring content.
let ortho = match shrinks {
true => ortho,
false => LayoutLen::rel(1.0),
@@ -141,6 +171,36 @@ impl Widget for Span {
}
impl Span {
fn collect(
&self,
painter: &mut Painter,
row: Len,
lens: &mut Vec<RequestedLen>,
discover: bool,
) {
let axis = self.dir.axis;
let mut cursor = Len::ZERO;
lens.clear();
for child in &self.children {
let request = if discover {
painter.size_request(child, axis)
} else {
painter.size_hint(child, axis).map(Into::into)
};
let len = match request {
Some(len) => len,
None => {
let room = self.slot(row, cursor, row).shifted_desc().on_axis(axis);
let len = painter.widget_at(child, room).len(axis);
painter.measured_request(child, axis, len)
}
};
cursor += painter.minimum_request(&len, axis);
cursor.px += self.gap;
lens.push(len);
}
}
/// The stretch of the row between two distances from where this span
/// starts laying children out, as a span of its own box. A negative
/// direction lays out from the far end, so the same two distances mirror
+10
View File
@@ -8,6 +8,16 @@ pub struct Stack {
}
impl Widget for Stack {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
match self.size {
StackSize::Default => Some(LayoutLen::LEFTOVER.into()),
StackSize::Child(i) => match self.children.get(i) {
Some(child) => requests.widget(child, axis),
None => Some(LayoutLen::LEFTOVER.into()),
},
}
}
fn draw(&mut self, painter: &mut Painter) -> Size {
let sizing = match self.size {
StackSize::Default => None,
+4 -4
View File
@@ -59,14 +59,14 @@ widget_trait! {
}
}
fn width(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> {
fn width(self, len: impl Into<SizeRequest>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into();
move |state| {
let id = self.add(state);
state
.ui_mut()
.widgets
.set_size_rule(id, Axis::X, SizeRule::Exact(len));
.set_size_rule(id, Axis::X, SizeRule::from(len));
id
}
}
@@ -115,14 +115,14 @@ widget_trait! {
}
}
fn height(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> {
fn height(self, len: impl Into<SizeRequest>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into();
move |state| {
let id = self.add(state);
state
.ui_mut()
.widgets
.set_size_rule(id, Axis::Y, SizeRule::Exact(len));
.set_size_rule(id, Axis::Y, SizeRule::from(len));
id
}
}