Add SizeRule::{Min, Max, Clamp}, which the oracle refuses
`MaxSize` on the app's pin narrows the box it asks its child in and cuts the
answer to the cap; nothing on this branch does either, so the capability is
missing rather than merely unported. This is that capability as a rule beside
the widget, the way `Exact` already is: `Min(Len)`, `Max(Len)` and
`Clamp { min, max }`, resolved against the rel base a declared length is a
fraction of, and never carrying `leftover` -- a cap containing a share admits
several self-sizing fixed points (`docs/LAYOUT.md`, failed hypotheses).
Where it stands: every hand-written test passes, including the capability the
app actually used -- `a_capped_scroll_takes_its_viewport_from_the_cap` puts
400 px of content under a 100 px cap and gets a 100 px viewport with 300 to
scroll, which is what `MaxSize` gave. The 400-seed depth-5 scan does not
pass, and the reason is a design question rather than a slip, so this sits on
its own branch instead of in #19.
What the scan finds: a bound is the first rule whose effect depends on the
box its parent gives it, and the retained machinery hands a widget a box by
paths that never ask it again -- `place_in` from a re-placing parent, and
`reposition` after a parent's box moved. A decision made when the box was one
length therefore survives into a box of another, so warm and cold disagree
about a tree they agree on structurally. Four readings were measured over 400
seeds at depth 5:
- deciding at every ask and keeping it: seeds 291, 1, 120, 178, 64 differ.
- the same, re-decided at `place_in` too: seeds 1, 362, 188, 254, 156 differ,
because that path's box is the one the answer chose rather than the one the
widget was asked in.
- skipping a place its parent decided outright, which is the rule the share
follows: worse -- the same widget then gets two decisions by two paths.
- the bound as an answer rule only, leaving the box alone: seeds 4 and 196,
and those are the closest to passing by a wide margin.
The share is the one existing rule of this kind and it is stable because
`place_at` re-asks a child whose rel base it narrows, and because its
decision is baked into the retained place as a `Sized` length. Neither
protection generalises: a bound that binds is a length of the rel base, and
`Sized` cannot say "this slot, narrowed" for a `Within` place.
Also here, because a bound needed them: `Len::longer_than` and
`Bound::outside` share one comparison with the span; a rule that is a
fraction now pins its rel base whether the fraction is a length or a bound,
which was a real gap for `Exact` too; `widget_trait!` passes attributes
through, so the methods it defines can carry doc comments (none could);
`From<N> for Len`, so a bound reads `max_width(300)`; and `random.rs` grows
all three variants, with `describe` printing them so a failure can be written
out by hand.
Format, clippy with and without layout-diagnostics, and the suite (142 + 19 +
13 + 4) are clean. The fast ten-seed oracle passes; the long scans do not.
Neutering the bounds in the generator while leaving its draws in place puts
the same shapes back to green, so the divergence is the bounds and not the
new trees.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d0326769c
commit
76aaf06c0b
14 files changed
+604
-73
No files matched your search
+48
-12
@@ -1,9 +1,9 @@
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
use crate::layout_diagnostics::{self as diag, Counter};
|
||||
use crate::{
|
||||
Axis, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel,
|
||||
RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextureHandle,
|
||||
UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
|
||||
Axis, Bound, Bounds, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2,
|
||||
RegionAlign, Rel, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer,
|
||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
|
||||
render::{
|
||||
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
|
||||
TexturePrimitive,
|
||||
@@ -193,6 +193,7 @@ impl<'a> Painter<'a> {
|
||||
region,
|
||||
place,
|
||||
declared,
|
||||
bounds,
|
||||
holds: ask_holds,
|
||||
} = self
|
||||
.placing()
|
||||
@@ -221,6 +222,8 @@ impl<'a> Painter<'a> {
|
||||
region,
|
||||
placed: place,
|
||||
asked: offer,
|
||||
declared,
|
||||
bounds,
|
||||
ask_holds,
|
||||
re_asked,
|
||||
},
|
||||
@@ -269,7 +272,13 @@ impl<'a> Painter<'a> {
|
||||
let states_rel_base = Axis::BOTH
|
||||
.iter()
|
||||
.any(|&axis| matches!(place[axis].rel_base, RelBase::Len(_)));
|
||||
if states_rel_base || !self.children.contains(&id.id()) {
|
||||
// A bound is decided against the box the widget is given, so a box
|
||||
// decided here is a question rather than a move: putting the drawing
|
||||
// in it would keep a decision made about the box it was measured in.
|
||||
let bounded = Axis::BOTH
|
||||
.iter()
|
||||
.any(|&axis| self.rsc.widgets().size_rules(id.id())[axis].bound() != Bound::ANY);
|
||||
if states_rel_base || bounded || !self.children.contains(&id.id()) {
|
||||
return self.widget_at(id, place);
|
||||
}
|
||||
let at = self.placing();
|
||||
@@ -689,7 +698,14 @@ pub(super) struct Ask {
|
||||
/// The place the ask came to, which a rule of the widget's own can take
|
||||
/// past the box its parent offered.
|
||||
pub place: PlaceDesc,
|
||||
/// What the widget's box is on each axis where something says so
|
||||
/// outright: its rule or its hint, or a bound of its own that the box it
|
||||
/// was offered falls outside -- a bound that binds is a declaration, and
|
||||
/// the same one the widget answers with.
|
||||
pub declared: Declared,
|
||||
/// Its bounds, resolved against the rel base its rules were resolved
|
||||
/// against, for the answer to be held to where the box was not.
|
||||
pub bounds: Bounds,
|
||||
/// What the ask itself holds for, kept on the widget asked about: a rule
|
||||
/// compared against the offer in pixels holds only for the windows on its
|
||||
/// side of the crossing, and that range reaches whoever asked through the
|
||||
@@ -714,21 +730,40 @@ impl Placing {
|
||||
mut place: PlaceDesc,
|
||||
) -> Ask {
|
||||
let align = widgets.alignment(id);
|
||||
let rules = widgets.size_rules(id);
|
||||
let mut holds = LayoutHolds::ANY;
|
||||
// A share fills what the pixels and fraction beside it leave of the
|
||||
// box and overflows where they are longer, which is the rule a span
|
||||
// follows with one child. Only the overflow is a box of the child's
|
||||
// own: a share that fits is the box it was given, which is what this
|
||||
// place already says.
|
||||
let mut declared = widgets.declared_lens(id);
|
||||
let mut bounds = Bounds::ANY;
|
||||
for axis in Axis::BOTH {
|
||||
let (len, kept) =
|
||||
let base = place.base(axis, self.rel_base);
|
||||
// A share fills what the pixels and fraction beside it leave of
|
||||
// the box and overflows where they are longer, which is the rule
|
||||
// a span follows with one child. Only the overflow is a box of
|
||||
// the widget's own: a share that fits is the box it was given,
|
||||
// which is what this place already says.
|
||||
let (share, kept) =
|
||||
self.share_past_the_offer(widgets, window[axis], id, place, align, axis);
|
||||
holds[axis].window = holds[axis].window.and(kept);
|
||||
if let Some(len) = len {
|
||||
if let Some(len) = share {
|
||||
place[axis] = len.as_desc().fills();
|
||||
}
|
||||
// A bound the box falls outside is what the widget's length is
|
||||
// instead, which is a declaration: the box comes to the bound,
|
||||
// and its own answer is held to the same bound where it drew
|
||||
// past that. Asked of the box it would otherwise have -- what it
|
||||
// declares of the place, or what the place gives it.
|
||||
let bound = rules[axis].bound();
|
||||
bounds[axis] = bound.within_len(base);
|
||||
let offered = declared[axis].map_or_else(
|
||||
|| place.of(self.region, align)[axis].len(),
|
||||
|len| len.within_len(base),
|
||||
);
|
||||
let (outside, kept) = bounds[axis].outside(offered, window[axis]);
|
||||
holds[axis].window = holds[axis].window.and(kept);
|
||||
if let Some(outside) = outside {
|
||||
declared[axis] = Some(bound.at(outside));
|
||||
}
|
||||
}
|
||||
let declared = widgets.declared_lens(id);
|
||||
let (rel_base, region) =
|
||||
place.rel_base_and_region(self.region, self.rel_base, declared, align);
|
||||
Ask {
|
||||
@@ -736,6 +771,7 @@ impl Placing {
|
||||
region,
|
||||
place,
|
||||
declared,
|
||||
bounds,
|
||||
holds,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user