Carry a length as a rule beside a widget, not a widget around it

`.width()` built a `SetSize` whose whole job was to answer `size_hint`, so
every declared length cost a widget, an `ActiveData` and a link of chain to
say one number. It is now a `SizeRule` per axis on `WidgetData`, beside
`region_node`, resolved by `Painter` where the widget is drawn. `SetSize`
and `MaxSize` are gone; `MaxSize` had no caller but its own builders.

That settles which of two answers is the size. A rule wins on the axis it
names and the `Size` returned by `draw` answers the rest, applied once in
`draw_inner` rather than by each widget that could carry one -- so the
widget under a rule never learns of it. `Painter::size_hint` reads the rule
first for the same reason: a rule that beats what a widget would draw has
to beat what it says about itself.

`declared_lens` still falls back to a non-leftover `size_hint`, which is
how an image or a gap gets its own pixel size rather than the whole offer.
That is the offer's business rather than a declaration's, and it falls away
when a widget occupies its reported size inside the box it was offered.

`known` and `declared` are separate because a share is a length to whoever
divides one and not to whoever composes a box: `.width(leftover(3))` is
known without drawing but cannot narrow anything.

Checked: fmt, clippy, 85 tests, and 100 generated seeds agreeing warm
against cold in 67.6 s. `minimal`, `text` and `view` render byte-identical
at 1920x1200; `tabs` differs only in the widget count it prints about
itself, which is two wrapper types smaller.
This commit is contained in:
iris-ai committed 2026-09-15 19:44:21 -04:00
1 parent 0283c9d6c7
commit 8220a78d4a
21 files changed
+252 -214

No files matched your search

+7
View File
@@ -158,6 +158,13 @@ impl Harness {
self.render.resize(size);
}
/// Changes a length rule after the fact, the way `.width()` sets one.
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<Len>) {
self.rsc
.widgets_mut()
.set_size_rule(id, axis, SizeRule::Exact(len.into()));
}
/// Sets the root and lays it out, so a pointer event has something to hit.
pub fn set_root<T>(&mut self, widget: impl WidgetLike<DefaultRsc<HarnessState>, T>) {
widget.set_root(&mut self.rsc, &mut self.state);
+9 -11
View File
@@ -8,14 +8,14 @@
use crate::prelude::*;
use std::collections::HashMap;
/// The declared lengths of one `SetSize`, by axis.
/// The declared lengths of one widget carrying a size rule, by axis.
pub type Lens = [Option<Len>; 2];
/// What a test changes between two trees grown from the same seed, so the
/// warm one can be mutated and the cold one grown that way to begin with.
#[derive(Default)]
pub struct Edits {
/// Declared sizes, by the order the `SetSize` wrappers were made.
/// Declared sizes, by the order the rules were put on.
pub sizes: HashMap<usize, Lens>,
/// Which children a span has, by the order the spans were made.
pub spans: HashMap<usize, SpanEdit>,
@@ -75,7 +75,7 @@ const WORDS: &str = "Wrapping shapes one source into as many lines as the box \
#[derive(Default)]
pub struct Tree {
pub ids: Vec<WidgetId>,
pub sized: Vec<WeakWidget<SetSize>>,
pub sized: Vec<WidgetId>,
pub spans: Vec<Spanned>,
pub scrolls: Vec<WeakWidget<Scroll>>,
/// Children a `SpanEdit` took out, held so that dropping the last share
@@ -201,15 +201,13 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
let idx = self.tree.sized.len();
let lens = [self.len(), self.len()];
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
let id = SetSize {
inner,
x: lens[0],
y: lens[1],
}
.add(self.rsc);
let id = inner.id();
self.rsc
.ui_mut()
.widgets
.set_size_rules(id, lens[0], lens[1]);
self.tree.sized.push(id);
self.tree.ids.push(id.id());
id.add_strong(self.rsc)
inner
}
fn node(&mut self, depth: usize) -> StrongWidget {
-25
View File
@@ -1,25 +0,0 @@
use crate::prelude::*;
pub struct MaxSize {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Widget for MaxSize {
fn draw(&mut self, painter: &mut Painter) -> Size {
let child = painter.widget(&self.inner).size();
let own = painter.px_size();
Size {
x: capped(child.x, self.x, own.x),
y: capped(child.y, self.y, own.y),
}
}
}
fn capped(len: Len, max: Option<Len>, own: f32) -> Len {
match max {
Some(max) if len.apply_leftover().to_px(own) > max.apply_leftover().to_px(own) => max,
_ => len,
}
}
-4
View File
@@ -1,19 +1,15 @@
mod align;
mod layer;
mod max_size;
mod offset;
mod pad;
mod scroll;
mod set_size;
mod span;
mod stack;
pub use align::*;
pub use layer::*;
pub use max_size::*;
pub use offset::*;
pub use pad::*;
pub use scroll::*;
pub use set_size::*;
pub use span::*;
pub use stack::*;
-30
View File
@@ -1,30 +0,0 @@
use crate::prelude::*;
pub struct SetSize {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl Widget for SetSize {
fn draw(&mut self, painter: &mut Painter) -> Size {
// Nothing to apply: a declared length is taken where this widget is
// drawn, so the box it has already is that length, and `leftover` is a
// share only whoever divides a length can work out. Both reach them
// through `size_hint`.
let child = painter.widget(&self.inner).size();
Size {
x: self.x.unwrap_or(child.x),
y: self.y.unwrap_or(child.y),
}
}
/// A declared axis is known without looking at the child, which is what
/// lets a span lay out around `.height(leftover(1))` without drawing it.
fn size_hint(&self, axis: Axis) -> Option<Len> {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
}
+23 -33
View File
@@ -39,48 +39,38 @@ widget_trait! {
}
}
fn sized(self, size: impl Into<Size>) -> impl WidgetFn<Rsc, SetSize> {
fn sized(self, size: impl Into<Size>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let size = size.into();
move |state| SetSize {
inner: self.add_strong(state),
x: Some(size.x),
y: Some(size.y),
move |state| {
let id = self.add(state);
let widgets = &mut state.ui_mut().widgets;
widgets.set_size_rule(id, Axis::X, SizeRule::Exact(size.x));
widgets.set_size_rule(id, Axis::Y, SizeRule::Exact(size.y));
id
}
}
fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
fn width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into();
move |state| MaxSize {
inner: self.add_strong(state),
x: Some(len),
y: None,
move |state| {
let id = self.add(state);
state
.ui_mut()
.widgets
.set_size_rule(id, Axis::X, SizeRule::Exact(len));
id
}
}
fn max_height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
fn height(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into();
move |state| MaxSize {
inner: self.add_strong(state),
x: None,
y: Some(len),
}
}
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
let len = len.into();
move |state| SetSize {
inner: self.add_strong(state),
x: Some(len),
y: None,
}
}
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, SetSize> {
let len = len.into();
move |state| SetSize {
inner: self.add_strong(state),
x: None,
y: Some(len),
move |state| {
let id = self.add(state);
state
.ui_mut()
.widgets
.set_size_rule(id, Axis::Y, SizeRule::Exact(len));
id
}
}