Files
iris/src/widget/position/span.rs
T
iris-ai 4cb6f6882a Hold a bound's length where it is decided, and say each thing once
A quality sweep over the deferred request system, which no earlier round
has reviewed.

`Bound::outside` said which end a length fell outside and left the caller
to look that end up through `Bound::at`, which `expect`s an end the value
it is given does not promise: only the pairing of the two calls kept
`at(Shorter)` off a bound with no floor. It already had the length in
hand, so it returns that, and `Outside` and `at` go with the state that
could panic.

`measured_request` pinned the rel base for any bound at all, so a measured
share under a cap in pixels was invalidated by a change to a base its
answer cannot depend on. That question is `Bound::has_fraction` now, which
is also the one `Placing::ask` and `SizeRule::has_fraction` were each
writing out over a bare array.

The rest is one name where there were several spellings: `Span::gaps`,
`Padding::along`, `Plan::drop_bounds` behind one `IRIS_UNBOUNDED` in both
rigs that had grown their own, and `Stack::size_request` resolving its
sizing child the way its draw already does. `Span`'s placement loop asked
three times whether the row was allocated, twice to decide one child's
length; one match answers all three, so the allocated and plain rules are
read side by side.

The buffers `draw_at` now reuses for their capacity are empty only because
every path to it drains them in `remove`; a `debug_assert` says so, since
a drawing over primitives left in one would record them twice.

Comments: `with_requests` named discovery as the hazard where it is a
child drawn mid-row, `Painter::allocate` documented the window it holds
for rather than what it does, `minimum_request` had none, and the note
saying a span carries its children's weight whole -- which is still what
the unallocated path does, and still the surprising part -- had been
replaced by one about the other path.
2026-09-20 18:24:37 -04:00

302 lines
11 KiB
Rust

use crate::prelude::*;
use std::marker::PhantomData;
pub struct Span {
pub children: Vec<StrongWidget>,
pub dir: Dir,
pub gap: Px,
}
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.gaps()));
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);
self.collect(painter, row, lens, true);
let gaps = self.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(|| &values[..]);
let total = match allocated {
Some(allocated) => LayoutLen {
px: allocated.iter().fold(gaps, |sum, len| sum + *len),
..LayoutLen::ZERO
},
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;
let any_leftover = total.leftover > Weight::ZERO;
let has_room = any_leftover && painter.longer_than(row, all_fixed, axis);
// Across itself a span is as long as its longest child -- unless a
// rule beside it gives that length outright, and then reading them
// answers nothing and makes its size depend on theirs for it. A rule
// that only bounds the length does not count: the answer is still
// this span's to give.
let shrinks = !painter.has_exact_size(!axis);
// What the fixed parts and the gaps before here take, which is a sum
// of lengths and exact, and how much of the leftover weight is
// spoken for. Both ends of a slot are read from those two rather
// than stepped from the last child: the share of the room is
// rounded, and taking each end from the one before it would carry
// every rounding along the row.
let mut fixed = Len::ZERO;
let mut taken = Weight::ZERO;
let mut ortho = LayoutLen::ZERO;
// Nothing divides the room where no child asked for any of it, and a
// ratio of a whole of nothing has no answer.
let reached = |fixed: Len, taken: Weight| match any_leftover {
false => fixed,
true => fixed + room.scale(Rel::ratio(taken, total.leftover)),
};
for (index, (child, request)) in self.children.iter().zip(lens.iter()).enumerate() {
// An allocated row already has a length for every child; without
// one the request is the length and the room is divided here.
// Either way 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.
let (len, shares, nothing_left) = match allocated {
Some(allocated) => {
let len = LayoutLen {
px: allocated[index],
..LayoutLen::ZERO
};
let shares = request.has_leftover();
(len, shares, shares && len.px == Px::ZERO)
}
None => {
let len = request.linear().unwrap();
let shares = len.leftover > Weight::ZERO && has_room;
(len, shares, len.is_only_leftover() && !has_room)
}
};
if nothing_left {
painter.undraw(child);
fixed.px += self.gap;
continue;
}
let from = reached(fixed, taken);
if shares {
taken += len.leftover;
}
fixed += len.without_leftover();
let to = reached(fixed, taken);
// Along the row the span says where the child goes, and that slot
// is the child's box outright rather than something to place an
// answer inside again. A share is decided here and nowhere
// else: its slot narrows its rel base, and the child is asked in
// it, since a text wraps at the width it is actually given. A
// 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().allocated().on_axis(axis);
if 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.
// A scalable child therefore makes the span scalable too;
// only fixed children are compared with one another.
if !used.is_px() {
ortho = LayoutLen::LEFTOVER;
} else if ortho.leftover == Weight::ZERO {
ortho.px = ortho.px.max(used.px);
}
}
fixed.px += self.gap;
}
// Where nothing was allocated the weight is carried whole rather
// than collapsed to one share, 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
// one share per level does not give. Resolution happens at the
// nearest ancestor with a length, and the root always has one --
// or, where a comparison deferred the row, at the ancestor whose
// allocation discovery carried these requests to, and `total` is
// pixels by the time it gets here.
let ortho = match shrinks {
true => ortho,
false => LayoutLen::rel(1.0),
};
Size::from_axis(axis, total, ortho)
}
}
impl Span {
/// What the gaps between this span's children take, which is a length of
/// the row before anything is divided.
fn gaps(&self) -> Px {
self.gap
.mul_int(self.children.len().saturating_sub(1) as i32)
}
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
/// in a row `row` long.
fn slot(&self, row: Len, from: Len, to: Len) -> UiSpan {
match self.dir.sign {
Sign::Pos => from.to(to),
Sign::Neg => (row - to).to(row - from),
}
}
pub fn empty(dir: Dir) -> Self {
Self {
children: Vec::new(),
dir,
gap: Px::ZERO,
}
}
pub fn gap(mut self, gap: impl UiNum) -> Self {
self.gap = Px::from_num(gap);
self
}
pub fn push(&mut self, w: StrongWidget) {
self.children.push(w);
}
pub fn pop(&mut self) -> Option<StrongWidget> {
self.children.pop()
}
}
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
pub children: Wa,
pub dir: Dir,
pub gap: Px,
_pd: PhantomData<(State, Tag)>,
}
impl<Rsc, const LEN: usize, Wa: WidgetArrLike<Rsc, LEN, Tag>, Tag> WidgetFnTrait<Rsc>
for SpanBuilder<Rsc, LEN, Wa, Tag>
{
type Widget = Span;
#[track_caller]
fn run(self, rsc: &mut Rsc) -> Self::Widget {
Span {
children: self.children.add(rsc).arr.into_iter().collect(),
dir: self.dir,
gap: self.gap,
}
}
}
impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
SpanBuilder<State, LEN, Wa, Tag>
{
pub fn new(children: Wa, dir: Dir) -> Self {
Self {
children,
dir,
gap: Px::ZERO,
_pd: PhantomData,
}
}
pub fn gap(mut self, gap: impl UiNum) -> Self {
self.gap = Px::from_num(gap);
self
}
}
impl std::ops::Deref for Span {
type Target = Vec<StrongWidget>;
fn deref(&self) -> &Self::Target {
&self.children
}
}
impl std::ops::DerefMut for Span {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.children
}
}