Let a rule beat a hint, and name marking a widget for redraw

A ninth sweep, over the part no earlier round named -- the widget vocabulary and
the builder methods, `Widgets`, the examples, the `util` additions and the
manifests -- and once more over `77ed7a2`, the eighth sweep's own commit and so
itself unreviewed.

A hint overrode a rule. `declared_lens` asked `rules[axis].declared()` first and
fell through to the widget's own `size_hint` whenever that answered `None` --
which it does for a share, since a share is not a declaration. So a widget
carrying `width(leftover(1))` and hinting a pixel length of its own was handed a
box of the hint, against the rule and against the comment inside the function:
"a hint still narrows the box where no rule does". `Painter::size_hint` spells
the same rule-else-hint step three hundred lines up and gets it right, with the
reason written on it; both read `Widgets::exact_len` now, and `declared_lens` is
the part of its answer that needs nobody to divide it. `Image` is the only
widget here whose hint is a declared length, and neither the tests nor the
generator builds one, so nothing in this repository could reach the difference
-- which is why the dump is unchanged and why the test builds a widget of its
own. It records the box it was asked in: 400 with the rule and 50 without, and
50 either way before this.

Marking a widget for redraw had no name. Twenty-one sites under `tests/` said it
as `widgets_mut().get_dyn_mut(id);` with the widget thrown away, five with a
`let _ =` in front, one with a comment explaining what the line was for, and one
wrapped in a local function called `mark`. `Widgets::mark_for_redraw` says it.
`revision_cost.rs` keeps the long spelling and now says why in place: it is
deliberately in the API subset an old worktree also has.

`assert_same_regions` could not see the defect the eighth sweep had just fixed.
It zips the warm and cold id lists, so a list naming one widget twice -- which
is what `width`, `sized` and `align` giving back their own argument produces --
compares fewer boxes than it lists and says nothing about it. It now rejects a
repeated id and two lists of different lengths, which also checks the nine
fixtures that round left alone: all eighteen cases pass.

Bare pairs where the framework has named ones. `random.rs`'s `Lens` and `Aligns`
were `[Option<LayoutLen>; 2]` and `[Option<AxisAlign>; 2]`, read as `[0]`/`[1]`
and zipped against a hand-written `[Axis::X, Axis::Y]`. They are `SizeRules` and
`Align`; `Align` took the `Index<Axis>` every other per-axis pair on this branch
has, and `RegionAlign::from` does the "an axis left out is centred" step two
rigs were spelling per axis. The three sites that wrote the axis pair out say
`Axis::BOTH`, which is what the rest of the layout code says.

`BothAxis<T>`, `AxisT`, `XAxis` and `YAxis` -- 45 lines with a const trait, two
marker types and three accessors -- have no user anywhere in the workspace. They
are the mechanism `impl_axis_index!` replaced, in the file this branch took
`Vec2::axis`/`axis_mut` out of. Deleted, which is a drive-by in a block the
branch was already rewriting; drop it if the scope matters more.

Smaller things, each in its own place: `Wrapper` arrived beside core's
`WidgetWrapper`, one word for a widget that wraps a child and for a dynamic
borrow guard, so the alias is gone and its two uses name `DynBorrower` -- which
is what they are. `Wrapper::new`, `Wrapper::empty` and its `Default` were three
names for one value, two of them unused. `Arena::get_mut` was the only
`pub(crate)` among `pub` siblings on a public type. `Selector` rounded the
pointer onto the pixel grid to do arithmetic on two values already there, losing
the precision the platform gave it for nothing; the step between the regions is
taken on the grid instead. And the two `debug` profile settings carry their
reason where the next reader looks rather than only in the commit that made
them, one of which was about renaming `rest`.

Format, clippy with and without layout-diagnostics, and the suite (132 + 19 + 13
+ 4) are clean. The cold dump over 400 depth-5 trees is byte-identical to
`77ed7a2` across all 34,488 boxes, and all three seed scans pass: 400 at depth 5
in 63.27s, 1,000 at depth 6 in 160.45s, 2,000 at depth 4 in 302.52s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-20 03:56:01 -04:00
1 parent 77ed7a24c0
commit c2b8bf83de
21 files changed
+190 -152

No files matched your search

+4 -3
View File
@@ -15,9 +15,10 @@ where
let region = ctx.data.render.window_region(&id).unwrap();
let id_pos = region.top_left;
let container_pos = ctx.data.render.window_region(&container).unwrap().top_left;
// The pointer arrives from the platform in floats; everything
// it is compared against is on the grid.
let pos = (PxVec2::from_f32(ctx.data.pos) + container_pos - id_pos).to_f32();
// The two regions are on the grid and the pointer is not, so the
// step between them is taken there and the pointer keeps the
// precision the platform gave it.
let pos = ctx.data.pos + (container_pos - id_pos).to_f32();
let size = region.size().to_f32();
select(
rsc,
+21 -22
View File
@@ -8,23 +8,16 @@
use crate::prelude::*;
use std::collections::HashMap;
/// The declared lengths of one widget carrying a size rule, by axis.
pub type Lens = [Option<LayoutLen>; 2];
/// Where one widget carrying an alignment sits, by axis. `None` uses the
/// centered default.
pub type Aligns = [Option<AxisAlign>; 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 rules were put on.
pub sizes: HashMap<usize, Lens>,
pub sizes: HashMap<usize, SizeRules>,
/// Which children a span has, by the order the spans were made.
pub spans: HashMap<usize, SpanEdit>,
/// Alignments, by the order they were put on.
pub aligns: HashMap<usize, Aligns>,
pub aligns: HashMap<usize, Align>,
/// Which widgets own a movable region, by the order they were offered
/// one. Region nodes change what a move writes and how deep a primitive's
/// chain is, so a tree that never grows one leaves both untested.
@@ -176,9 +169,11 @@ pub struct Plan {
/// it one and the offer is taken or declined; a second offer to the same
/// widget is dropped, because two rules on one widget would settle in the
/// order they were applied rather than in grow order.
pub size: Option<Lens>,
/// The alignment it carries, under the same one-offer rule.
pub align: Option<Aligns>,
pub size: Option<SizeRules>,
/// The alignment it carries, under the same one-offer rule. An axis left
/// out takes the centered default, which is what [`RegionAlign`] reads it
/// as.
pub align: Option<Align>,
/// Whether it was offered a movable region of its own and what it
/// answered. `Some(false)` is an offer declined, which still uses up the
/// one offer, where `None` is an offer never made.
@@ -651,7 +646,7 @@ impl Sow<'_> {
}
}
fn align(&mut self) -> Aligns {
fn align(&mut self) -> Align {
let axis = |s: &mut Self| match s.rng.below(4) {
0 => None,
1 => Some(AxisAlign::NEG),
@@ -661,15 +656,21 @@ impl Sow<'_> {
let (x, y) = (axis(self), axis(self));
// Aligning on neither axis leaves the branch unexercised.
match x.is_none() && y.is_none() {
true => [Some(AxisAlign::CENTER), y],
false => [x, y],
true => Align {
x: Some(AxisAlign::CENTER),
y,
},
false => Align { x, y },
}
}
/// A declared size over half the tree, kept where a test can change it.
fn sized(&mut self, inner: &mut Plan) {
let take = self.rng.chance();
let lens = [self.len(), self.len()];
let lens = SizeRules {
x: self.len().into(),
y: self.len().into(),
};
if !take || inner.size.is_some() {
return;
}
@@ -807,16 +808,14 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
let built = self.kind(&plan.kind);
let id = built.id();
if let Some(lens) = plan.size {
self.rsc
.ui_mut()
.widgets
.set_size_rules(id, lens[0], lens[1]);
self.rsc.ui_mut().widgets.set_size_rules(id, lens.x, lens.y);
self.tree.sized.push(id);
}
if let Some(align) = plan.align {
let resolved = RegionAlign::from(align);
let widgets = &mut self.rsc.ui_mut().widgets;
for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) {
widgets.set_alignment(id, axis, align.unwrap_or_default());
for axis in Axis::BOTH {
widgets.set_alignment(id, axis, resolved[axis]);
}
self.tree.aligned.push(id);
}
+2 -2
View File
@@ -19,8 +19,8 @@ widget_trait! {
move |state| {
let id = self.add(state);
let widgets = &mut state.ui_mut().widgets;
for (axis, align) in [(Axis::X, align.x), (Axis::Y, align.y)] {
if let Some(align) = align {
for axis in Axis::BOTH {
if let Some(align) = align[axis] {
widgets.set_alignment(id, axis, align);
}
}
+2 -11
View File
@@ -9,6 +9,7 @@ use std::marker::Unsize;
///
/// Its child is optional so it can also be the swappable slot a tab bar
/// needs, which is what it was written for.
#[derive(Default)]
pub struct Wrapper {
pub inner: Option<StrongWidget>,
}
@@ -26,11 +27,7 @@ impl Wrapper {
pub fn new() -> Self {
Self::default()
}
pub fn empty() -> Self {
Self {
inner: Default::default(),
}
}
pub fn set<W: ?Sized + Unsize<dyn Widget>>(&mut self, to: StrongWidget<W>) {
self.inner = Some(to)
}
@@ -42,9 +39,3 @@ impl Wrapper {
self.inner.replace(to)
}
}
impl Default for Wrapper {
fn default() -> Self {
Self::empty()
}
}