Compare commits

...
Author SHA1 Message Date
iris-ai 05e6ced31d Hold a request in the arena its nodes are allocated in
A size request was a second expression shape beside the one the layout
pass already has. `SizeRequest` held `Sum`/`Min`/`Max` over `Arc` pairs;
`RequestArena` held the same three operators as `Node { op, a, b }` in a
`Vec`, with the same fold over `independent_order` written a second time,
and `import` walked the first rebuilding it as the second.

There is one node type now. An expression is the pass's nodes in an arena
of its own that lasts as long as the rule holding it, and `import` grafts
those nodes into the pass's arena, resolving fractions as they land. The
fold is `Nodes::combine`, which both the builder and the importer call.

So the `Arc` goes, and no refcount replaces it: nothing shares a request
and nothing outside widget code holds one. A plain length stays inline,
so `size_of::<SizeRule>()` is 40 either way and only an actual expression
allocates. A node's operand is a number within its own arena, and
`RequestedLen` -- the only form that leaves one -- is that plus the epoch
saying which pass numbered it, so the epoch is now checked once where a
handle comes back in rather than at every level of the walk it starts.

`SizeRule::at_least`/`at_most` were the only clones of a request in the
framework, and both read a rule out, moved one end of its bound, and
wrote it back into the slot it came from. `Widgets::edit_bound` does it
where it sits, so nothing copies an expression to cap it.

`SizeRequest` grew a `Display`, since the shrinker prints one and a
derived `Debug` of an arena is not something a tree can be rebuilt from:
`min(30 px;1 leftover;, 2 leftover;)<0.5 rel;`.

Measured, medians of three release runs under `perf stat -e
instructions:u`, each set within 0.005% of its median: bounds_cost
MODE=cap FRAMES=2000 is 5.665B against 5.743B (-1.34%), and
revision_cost resize ROWS=40 FRAMES=500 is 4.855B against 4.893B
(-0.79%).

Format, workspace clippy under -D warnings with and without
layout-diagnostics, 206 ordinary and 210 diagnostic tests, the cold dump
byte-identical to 2ac0843 across all 34,986 boxes, 400 depth-5 trees in
64.24s, 1,000 depth-6 in 160.35s, 2,000 depth-4 in 298.82s, and 400
depth-5 trees in each of the three deferred-request corpora in 205.42s.
2026-09-20 21:07:19 -04:00
iris-ai ea1f836bf9 Say the environment once, and stop a hint read going uncounted
The eleventh sweep, over the built-in bounds work in 2ac0843.

`Painter::size_hint` refused to answer for a bounded widget by returning
above the diagnostics, so that read was neither a hint hit nor a miss and
`hint_read` recorded nothing. It is a miss now, with the reason on it.

The two `for axis in Axis::BOTH` loops that `draw_widget` grew, both
writing `own_holds`, are one loop, and the comment about combining the
ask's holds no longer sits between a comment and the code it describes.

`Declared::from_axes` lost its only caller with `Widgets::declared_lens`;
`Bounds::from_axes` and `SizeRule::declared` never had one.

The scenario shrinker printed a rule with derived `Debug`, which is 130
characters an axis in a line that carries every ancestor, in the one
function whose job is output a tree can be rebuilt from. It prints its
parts again.

`bounds_cost` invented three environment-reading spellings where four
copies of one `env` helper already existed; there is now one, in
`tests/rig`, and the four copies are gone. It also verified 128 regions
inside its measured loop, which the other rigs deliberately do before
theirs; that measured 0.65% of the total, and none of it is layout.

The 250-window row with a 300 cap was built by two tests, and the one
that still explained itself tested less; they are one. The half of
`a_cap_attribute_narrows_the_widgets_box` that the wrapper's removal left
without its deciding assertion is the allocator's path instead, which
nothing at the root covered.

Format, workspace clippy under -D warnings with and without
layout-diagnostics, 206 ordinary and 210 diagnostic tests, 400 depth-5
trees warm against cold in 64.19s, and the cold dump byte-identical to
2ac0843 across all 34,986 boxes.
2026-09-20 20:13:57 -04:00
iris-ai 2ac0843cb2 Constrain offered boxes with independent widget size bounds 2026-09-20 19:47:32 -04:00
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
iris-ai 0e838e9dd1 Retain request dependencies only when discovery supplies the answer 2026-09-20 17:31:53 -04:00
iris-ai 8780b40bb7 Resolve deferred size comparisons before allocating span slots 2026-09-20 16:50:18 -04:00
34 changed files with 2436 additions and 563 deletions

No files matched your search

+11
View File
@@ -51,6 +51,8 @@ pub struct ActiveData {
/// An owned mask holds one reference independently of its primitives. /// An owned mask holds one reference independently of its primitives.
pub mask_region: Option<UiRegion>, pub mask_region: Option<UiRegion>,
pub children: Vec<WidgetId>, pub children: Vec<WidgetId>,
pub request_deps: Vec<WidgetId>,
pub(crate) scratch: DrawScratch,
/// The movable region its primitives are positioned through: its own when /// The movable region its primitives are positioned through: its own when
/// opted in, otherwise the nearest ancestor's. /// opted in, otherwise the nearest ancestor's.
pub move_idx: MoveIdx, pub move_idx: MoveIdx,
@@ -101,3 +103,12 @@ pub struct Answer {
pub size: Size, pub size: Size,
pub holds: LayoutHolds, pub holds: LayoutHolds,
} }
#[derive(Debug, Default)]
pub(crate) struct DrawScratch {
pub children: Vec<WidgetId>,
pub size_deps: Vec<WidgetId>,
pub under: Vec<(WidgetId, LayoutHolds)>,
pub requests: Vec<crate::RequestedLen>,
pub lengths: Vec<crate::Px>,
}
+13 -14
View File
@@ -1,4 +1,4 @@
use crate::{Bound, Len, Outside, Px, REL_SHIFT, fixed::div_toward, fixed::narrow}; use crate::{Bound, Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
/// The lengths of a box, in pixels, that one drawing of a widget holds for: /// The lengths of a box, in pixels, that one drawing of a widget holds for:
@@ -40,40 +40,39 @@ impl Len {
} }
impl Bound { impl Bound {
/// Which end of this bound `len` falls outside, and the windows that /// The end of this bound `len` falls outside, which is the length it
/// answer holds for. Nothing where it is inside, which is the answer /// gets instead of its own, and the windows that answer holds for.
/// wherever there is no bound at all. /// Nothing where it is inside, which is the answer wherever there is no
/// bound at all.
/// ///
/// `len` and this bound are lengths of the same thing, whichever that /// `len` and this bound are lengths of the same thing, whichever that
/// is: a box in window lengths wants the bound resolved, and a length a /// is: a box in window lengths wants the bound resolved, and a length a
/// widget declares of its rel base wants it as the rule wrote it. Both /// widget declares of its rel base wants it as the rule wrote it. Both
/// comparisons are in pixels, so each is a question about this window, /// comparisons are in pixels, so each is a question about this window,
/// and the box is decided again on the other side of a crossing. /// and the box is decided again on the other side of a crossing.
pub fn outside(&self, len: Len, window: Px) -> (Option<Outside>, Holds) { pub fn outside(&self, len: Len, window: Px) -> (Option<Len>, Holds) {
let mut outside = None; let mut held = None;
let mut holds = Holds::ANY; let mut holds = Holds::ANY;
let mut held = len;
if let Some(min) = self.min { if let Some(min) = self.min {
let (shorter, kept) = min.longer_than(held, window); let (shorter, kept) = min.longer_than(len, window);
holds = holds.and(kept); holds = holds.and(kept);
if shorter { if shorter {
outside = Some(Outside::Shorter); held = Some(min);
held = min;
} }
} }
if let Some(max) = self.max { if let Some(max) = self.max {
let (longer, kept) = held.longer_than(max, window); let (longer, kept) = held.unwrap_or(len).longer_than(max, window);
holds = holds.and(kept); holds = holds.and(kept);
if longer { if longer {
debug_assert!( debug_assert!(
outside.is_none(), held.is_none(),
"a floor of {:?} over a cap of {max:?} bounds nothing", "a floor of {:?} over a cap of {max:?} bounds nothing",
self.min, self.min,
); );
outside = Some(Outside::Longer); held = Some(max);
} }
} }
(outside, holds) (held, holds)
} }
} }
+1 -1
View File
@@ -20,7 +20,7 @@ pub use active::*;
pub use holds::*; pub use holds::*;
pub use layout_holds::*; pub use layout_holds::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use place::{PlaceDesc, PlaceDescAxis, RetainedPrimitive}; pub use place::{PlaceDesc, PlaceDescAxis, PlaceFit, RetainedPrimitive};
pub use render_state::*; pub use render_state::*;
#[derive(Default)] #[derive(Default)]
+235 -54
View File
@@ -1,8 +1,9 @@
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter}; use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, Bounds, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Axis, Bound, Bounds, Declared, DrawScratch, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc,
Rel, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextureHandle, PlaceFit, Px, PxVec2, RegionAlign, Rel, RenderedText, RequestArena, RequestedLen,
RetainedPrimitive, Size, SizeRequests, StrongWidget, TextAttrs, TextBuffer, TextureHandle,
UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
@@ -41,6 +42,8 @@ pub struct Painter<'a> {
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children whose size this widget read while drawing. /// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>, pub(super) size_deps: Vec<WidgetId>,
pub(super) request_deps: Vec<WidgetId>,
pub(super) scratch: DrawScratch,
/// What this draw itself reads, as against what its children's drawings /// What this draw itself reads, as against what its children's drawings
/// hold for: every window and every length of its own region until it /// hold for: every window and every length of its own region until it
/// reads one, then that one unless it says otherwise, and the rel base or /// reads one, then that one unless it says otherwise, and the rel base or
@@ -62,6 +65,131 @@ pub struct Painter<'a> {
} }
impl<'a> Painter<'a> { impl<'a> Painter<'a> {
/// Reuses this widget's allocation buffers across draws. A child drawn
/// part way through gets a painter of its own, with buffers of its own,
/// so nothing it does while this one is mid-row can reach these.
pub fn with_requests<T>(
&mut self,
f: impl FnOnce(&mut Self, &mut Vec<RequestedLen>, &mut Vec<Px>) -> T,
) -> T {
let mut requests = std::mem::take(&mut self.scratch.requests);
let mut lengths = std::mem::take(&mut self.scratch.lengths);
requests.clear();
lengths.clear();
let result = f(self, &mut requests, &mut lengths);
self.scratch.requests = requests;
self.scratch.lengths = lengths;
result
}
/// Discovers a composable request without painting a provisional box.
pub fn size_request<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
) -> Option<RequestedLen> {
if let Some(len) = self.size_hint(child, axis) {
self.request_deps.push(child.id());
return Some(len.into());
}
let start = self.request_deps.len();
let mut requests = SizeRequests {
arena: &mut self.state.requests,
measured: None,
widgets: self.rsc.widgets(),
dependencies: &mut self.request_deps,
rel_base: self.rel_base[axis],
};
// Intrinsic fixed content must keep its offered box for wrapping;
// only a declaration or a share chooses the box it is drawn in.
let request = requests.widget(child, axis).filter(|request| {
request.has_leftover()
|| self.rsc.widgets().size_rules(child.id())[axis]
.request
.is_some()
});
if request.is_some() {
self.rel_base(axis);
} else {
// A discarded request contributes no dependency: the measured
// draw below records the size and box it actually used instead.
self.request_deps.truncate(start);
}
request
}
/// Completes discovery after a child was measured. Only this call may use
/// drawn answers: before the ask they could belong to an obsolete box.
pub fn measured_request<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
len: LayoutLen,
) -> RequestedLen {
let start = self.request_deps.len();
let bound = self.rsc.widgets().size_rules(child.id())[axis].bound;
let mut requests = SizeRequests {
arena: &mut self.state.requests,
measured: Some(&self.state.active),
widgets: self.rsc.widgets(),
dependencies: &mut self.request_deps,
rel_base: self.rel_base[axis],
};
if let Some(request) = requests.widget(child, axis)
&& request.linear().is_none()
&& request.has_leftover()
{
self.rel_base(axis);
return request;
}
let shares = len.leftover > Weight::ZERO;
let request = match shares {
true => requests.bounded(len.into(), bound),
false => len.into(),
};
self.request_deps.truncate(start);
// Only a bound that is a fraction was read against the rel base; one
// in pixels binds at the same length under any of them.
if shares && bound.has_fraction() {
self.rel_base(axis);
}
request
}
/// Divides `room` between `requests`, one length per request in
/// `output`. A deferred comparison is decided here, against this window:
/// which side of a crossing the solution falls is a question in pixels,
/// so the drawing holds only for the window that answered it.
pub fn allocate(
&mut self,
requests: &[RequestedLen],
room: Len,
axis: Axis,
output: &mut Vec<Px>,
) {
let window = self.window[axis];
self.own[axis].window = self.own[axis].window.and(Holds::at(window));
output.clear();
output.extend(
self.state
.requests
.allocate(requests, room.to_px(window), window),
)
}
/// The least a request can come to, which is what it takes of the row
/// before anything is divided. A comparison is read at no share at all.
pub fn minimum_request(&mut self, request: &RequestedLen, axis: Axis) -> Len {
match request.linear() {
Some(len) => len.without_leftover(),
None => {
let window = self.window[axis];
self.own[axis].window = self.own[axis].window.and(Holds::at(window));
Len::from_parts(Rel::ZERO, self.state.requests.minimum(*request, window))
}
}
}
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) { fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
let kind = self.rsc.ui_mut().primitives.kind::<P>(); let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region); self.write(kind, primitive, region);
@@ -195,9 +323,15 @@ impl<'a> Painter<'a> {
declared, declared,
bounds, bounds,
holds: ask_holds, holds: ask_holds,
} = self inputs,
.placing() } = self.placing().ask(
.ask(self.rsc.widgets(), self.window, id.id(), offer); self.rsc.widgets(),
&mut self.state.requests,
self.window,
id.id(),
offer,
);
self.own = self.own.and(inputs);
let region_node = self.rsc.widgets().is_region_node(id.id()); let region_node = self.rsc.widgets().is_region_node(id.id());
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
if region_node { if region_node {
@@ -305,7 +439,14 @@ impl<'a> Painter<'a> {
/// against this widget's rel base, which is the rel base a child asked with /// against this widget's rel base, which is the rel base a child asked with
/// nothing narrowed gets. Asking counts as reading its size. /// nothing narrowed gets. Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> { pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
let hint = self.rsc.widgets().exact_len(id.id(), axis); // A bound is composed into a request rather than applied to a hint,
// so a bounded widget cannot say its length without being asked: what
// it comes to is a comparison only the ask or the allocator makes.
// A miss rather than no read at all, so the counters see it.
let bounded = self.rsc.widgets().size_rules(id.id())[axis].bound != Bound::ANY;
let hint = (!bounded)
.then(|| self.rsc.widgets().exact_len(id.id(), axis))
.flatten();
let rel_base = self.rel_base[axis]; let rel_base = self.rel_base[axis];
let resolved = hint.map(|hint| hint.within_len(rel_base)); let resolved = hint.map(|hint| hint.within_len(rel_base));
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -423,7 +564,7 @@ impl<'a> Painter<'a> {
/// depend on it. /// depend on it.
pub fn has_exact_size(&self, axis: Axis) -> bool { pub fn has_exact_size(&self, axis: Axis) -> bool {
self.rsc.widgets().size_rules(self.id)[axis] self.rsc.widgets().size_rules(self.id)[axis]
.exact() .request
.is_some() .is_some()
} }
@@ -667,7 +808,15 @@ impl Widgets {
/// share included, since a share is a length only to whoever divides one, /// share included, since a share is a length only to whoever divides one,
/// and that is the parent rather than this widget. /// and that is the parent rather than this widget.
fn exact_len(&self, id: WidgetId, axis: Axis) -> Option<LayoutLen> { fn exact_len(&self, id: WidgetId, axis: Axis) -> Option<LayoutLen> {
self.size_rules(id)[axis].exact().or_else(|| { // A request is a length the rule gives, and the hint below must not
// narrow the box in its place: what the request comes to is not known
// until the parent allocates, and it is the parent's answer, not this
// widget's.
let rule = &self.size_rules(id)[axis];
if rule.deferred().is_some() {
return None;
}
rule.exact().or_else(|| {
// A hint still narrows the box where no rule does, which is how a // A hint still narrows the box where no rule does, which is how a
// widget with a natural pixel size -- an image, a gap -- gets that // widget with a natural pixel size -- an image, a gap -- gets that
// size rather than the whole offer. That is the offer's business // size rather than the whole offer. That is the offer's business
@@ -676,12 +825,6 @@ impl Widgets {
self.get_dyn(id)?.size_hint(axis) self.get_dyn(id)?.size_hint(axis)
}) })
} }
/// What a widget's box is where a rule or its own hint gives one outright,
/// rather than a share for whoever draws it to divide.
pub(super) fn declared_lens(&self, id: WidgetId) -> Declared {
Declared::from_axes(|axis| self.exact_len(id, axis)?.declared())
}
} }
/// One ask of a widget: the box it draws in, what its fractions are of, and /// One ask of a widget: the box it draws in, what its fractions are of, and
@@ -693,9 +836,8 @@ pub(super) struct Ask {
/// past the box its parent offered. /// past the box its parent offered.
pub place: PlaceDesc, pub place: PlaceDesc,
/// What the widget's box is on each axis where something says so /// 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 /// outright: its rule, its hint, or a bound the offer fell outside.
/// was offered falls outside -- a bound that binds is a declaration, and /// An intrinsic answer can still occupy less than a capped box.
/// the same one the widget answers with.
pub declared: Declared, pub declared: Declared,
/// Its bounds, resolved against the rel base its rules were resolved /// Its bounds, resolved against the rel base its rules were resolved
/// against, for the answer to be held to where the box was not. /// against, for the answer to be held to where the box was not.
@@ -706,6 +848,9 @@ pub(super) struct Ask {
/// drawing it is part of. Kept on the widget asked about rather than on /// drawing it is part of. Kept on the widget asked about rather than on
/// the asker because the root has no asker. /// the asker because the root has no asker.
pub holds: LayoutHolds, pub holds: LayoutHolds,
/// Inputs read against the parent before declarations choose a new base.
/// These belong to the asker; the widget's own holds describe its output box.
pub inputs: LayoutHolds,
} }
impl Placing { impl Placing {
@@ -719,6 +864,7 @@ impl Placing {
pub(super) fn ask( pub(super) fn ask(
&self, &self,
widgets: &Widgets, widgets: &Widgets,
requests: &mut RequestArena,
window: PxVec2, window: PxVec2,
id: WidgetId, id: WidgetId,
mut place: PlaceDesc, mut place: PlaceDesc,
@@ -726,29 +872,77 @@ impl Placing {
let align = widgets.alignment(id); let align = widgets.alignment(id);
let rules = widgets.size_rules(id); let rules = widgets.size_rules(id);
let mut holds = LayoutHolds::ANY; let mut holds = LayoutHolds::ANY;
let declared = widgets.declared_lens(id); let mut inputs = LayoutHolds::ANY;
let mut declared = Declared::NONE;
let mut bounds = Bounds::ANY; let mut bounds = Bounds::ANY;
for axis in Axis::BOTH { for axis in Axis::BOTH {
let base = place.base(axis, self.rel_base); let base = place.base(axis, self.rel_base);
let stated = widgets.exact_len(id, axis);
if let Some(len) = stated.and_then(|len| len.declared()) {
if len.rel != Rel::ZERO {
inputs[axis].rel_base = Some(self.rel_base[axis]);
}
declared[axis] = Some(len.within_len(base));
}
if let Some(request) = rules[axis].deferred() {
inputs[axis].rel_base = Some(self.rel_base[axis]);
inputs[axis].region_len = Some(self.region[axis].len());
inputs[axis].window = Holds::at(window[axis]);
let offer = place.of(self.region, align)[axis].len();
let px = if place[axis].fit == PlaceFit::Allocated {
offer.to_px(window[axis])
} else {
let request = requests.import(request, base);
let request = requests.bounded(request, rules[axis].bound.within_len(base));
holds[axis].window = Holds::at(window[axis]);
requests
.allocate(&[request], offer.to_px(window[axis]), window[axis])
.next()
.unwrap()
};
let len = Len::from_parts(Rel::ZERO, px);
place[axis].rel_base = RelBase::Len(len);
declared[axis] = Some(len);
holds[axis].rel_base = Some(len);
continue;
}
// A share fills what the pixels and fraction beside it leave of // A share fills what the pixels and fraction beside it leave of
// the box and overflows where they are longer, which is the rule // 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 // 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, // the widget's own: a share that fits is the box it was given,
// which is what this place already says. // which is what this place already says.
let (share, kept) = let (share, kept) = self.share_past_the_offer(stated, window[axis], place, align, axis);
self.share_past_the_offer(widgets, window[axis], id, place, align, axis);
holds[axis].window = holds[axis].window.and(kept); holds[axis].window = holds[axis].window.and(kept);
if let Some(len) = share { if let Some(len) = share {
place[axis] = len.as_desc().fills(); place[axis] = len.as_desc().fills();
} }
// A bound holds what the widget answers, not the box it is asked let bound = rules[axis].bound;
// in: the box it is given is whoever asked's to decide, and a if bound != Bound::ANY {
// rule that read it would be decided again by every path that if bound.has_fraction() {
// hands the widget a box -- including the ones that never ask it inputs[axis].rel_base = Some(self.rel_base[axis]);
// anything. Resolved here because only the ask knows the rel base }
// a fraction in it is of. `MaxSize` is the box version, and it is // A solved slot may narrow the widget's rel base, but the
// a widget because a widget is drawn again when its box changes. // allocator evaluated its bounds against this parent's base.
bounds[axis] = rules[axis].bound().within_len(base); let bound_base = if place[axis].fit == PlaceFit::Allocated {
self.rel_base[axis]
} else {
base
};
bounds[axis] = bound.within_len(bound_base);
let offer =
declared[axis].unwrap_or_else(|| place.of(self.region, align)[axis].len());
let (held, kept) = bounds[axis].outside(offer, window[axis]);
holds[axis].window = holds[axis].window.and(kept);
// The comparison reads the incoming box, before a bound
// replaces it. Placement keeps that decision; only an ask
// may compare a new offer.
if declared[axis].is_none() {
inputs[axis].region_len = Some(self.region[axis].len());
}
if let Some(len) = held {
declared[axis] = Some(len);
}
}
} }
let (rel_base, region) = let (rel_base, region) =
place.rel_base_and_region(self.region, self.rel_base, declared, align); place.rel_base_and_region(self.region, self.rel_base, declared, align);
@@ -759,6 +953,7 @@ impl Placing {
declared, declared,
bounds, bounds,
holds, holds,
inputs,
} }
} }
@@ -774,9 +969,8 @@ impl Placing {
/// crossing between them is a question in pixels. /// crossing between them is a question in pixels.
fn share_past_the_offer( fn share_past_the_offer(
&self, &self,
widgets: &Widgets, stated: Option<LayoutLen>,
window: Px, window: Px,
id: WidgetId,
place: PlaceDesc, place: PlaceDesc,
align: RegionAlign, align: RegionAlign,
axis: Axis, axis: Axis,
@@ -785,12 +979,12 @@ impl Placing {
// decided, and a parent that divides one has already given the share // decided, and a parent that divides one has already given the share
// whatever it was owed. Only an offer -- a box with the answer still // whatever it was owed. Only an offer -- a box with the answer still
// to be placed inside it -- is a box a share reads. // to be placed inside it -- is a box a share reads.
if place[axis].fills { if place[axis].fit.fills() {
return (None, Holds::ANY); return (None, Holds::ANY);
} }
// A share with nothing beside it is the box whatever the box is, so // A share with nothing beside it is the box whatever the box is, so
// there is no comparison to make and no range to keep for one. // there is no comparison to make and no range to keep for one.
let Some(stated) = widgets.exact_len(id, axis) else { let Some(stated) = stated else {
return (None, Holds::ANY); return (None, Holds::ANY);
}; };
if stated.leftover == Weight::ZERO || stated.is_only_leftover() { if stated.leftover == Weight::ZERO || stated.is_only_leftover() {
@@ -809,12 +1003,10 @@ impl LayoutLen {
/// Whether what a widget reported along an axis is the whole of the box /// Whether what a widget reported along an axis is the whole of the box
/// it is in rather than a part to be placed inside it. A share fills, /// it is in rather than a part to be placed inside it. A share fills,
/// because a share is a length only to whoever divides one, and whoever /// because a share is a length only to whoever divides one, and whoever
/// did is the one that handed down this box. A declared axis does too: /// did is the one that handed down this box. An axis the parent decided
/// the rule already gave the region its length, and the rule's length is /// from the answer is the answer already.
/// what the widget reports there. And an axis the parent decided from pub(super) fn fills(&self, decided: bool) -> bool {
/// the answer is the answer already. self.leftover != Weight::ZERO || decided
pub(super) fn fills(&self, declared: Option<Len>, decided: bool) -> bool {
self.leftover != Weight::ZERO || declared.is_some() || decided
} }
} }
@@ -828,17 +1020,11 @@ impl PlaceDesc {
/// part. That is what makes a fraction the same fraction wherever the part /// part. That is what makes a fraction the same fraction wherever the part
/// it is placed in sits and however long it is -- the fraction is resolved /// it is placed in sits and however long it is -- the fraction is resolved
/// once, here, against the rel base it was reported of. /// once, here, against the rel base it was reported of.
pub(super) fn placement( pub(super) fn placement(self, region: UiRegion, size: Size, align: RegionAlign) -> UiRegion {
self,
region: UiRegion,
size: Size,
declared: Declared,
align: RegionAlign,
) -> UiRegion {
let mut placed = region; let mut placed = region;
for axis in Axis::BOTH { for axis in Axis::BOTH {
let reported = size[axis]; let reported = size[axis];
if reported.fills(declared[axis], self[axis].fills) { if reported.fills(self[axis].fit.fills()) {
continue; continue;
} }
placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]); placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
@@ -851,11 +1037,8 @@ impl PlaceDesc {
/// ///
/// `own` is that widget's own box, and `place` what of it the child is /// `own` is that widget's own box, and `place` what of it the child is
/// given, including any rel base it states -- a row's slot, or padding's rel /// given, including any rel base it states -- a row's slot, or padding's rel
/// base less its pixels. That is a window length, like every other length /// base less its pixels. The ask has already resolved declarations into
/// here, since a slot of a row is not a fraction of anything the row can /// window lengths, so placement only aligns them inside the given box.
/// name. The child's declaration is a fraction of whichever reached it, and
/// is the only one that also places the box: a box the caller decided is
/// what `place` names.
pub(super) fn rel_base_and_region( pub(super) fn rel_base_and_region(
self, self,
own: UiRegion, own: UiRegion,
@@ -868,9 +1051,7 @@ impl PlaceDesc {
let mut region = given; let mut region = given;
for axis in Axis::BOTH { for axis in Axis::BOTH {
let base = self.base(axis, parent_rel_base); let base = self.base(axis, parent_rel_base);
let len = declared[axis] let len = declared[axis].unwrap_or(base);
.map(|len| len.within_len(base))
.unwrap_or(base);
rel_base[axis] = len; rel_base[axis] = len;
if declared[axis].is_some() { if declared[axis].is_some() {
region[axis] = given[axis].place(len, align[axis]); region[axis] = given[axis].place(len, align[axis]);
+25 -5
View File
@@ -12,10 +12,24 @@ use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlaceDescAxis { pub struct PlaceDescAxis {
pub span: PlaceSpan, pub span: PlaceSpan,
pub fills: bool, pub fit: PlaceFit,
pub rel_base: RelBase, pub rel_base: RelBase,
} }
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PlaceFit {
Align,
Fill,
/// The parent has already evaluated the child's size request.
Allocated,
}
impl PlaceFit {
pub fn fills(&self) -> bool {
!matches!(self, Self::Align)
}
}
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum PlaceSpan { pub enum PlaceSpan {
Within(UiSpan), Within(UiSpan),
@@ -44,7 +58,13 @@ impl PlaceDescAxis {
/// it again. A container uses it where it hands back exactly what the /// it again. A container uses it where it hands back exactly what the
/// child asked for -- a row placing a child at the length it reported. /// child asked for -- a row placing a child at the length it reported.
pub const fn fills(mut self) -> Self { pub const fn fills(mut self) -> Self {
self.fills = true; self.fit = PlaceFit::Fill;
self
}
/// A final allocation, including any comparisons in the child's request.
pub const fn allocated(mut self) -> Self {
self.fit = PlaceFit::Allocated;
self self
} }
@@ -153,7 +173,7 @@ impl UiSpan {
pub const fn within_desc(self) -> PlaceDescAxis { pub const fn within_desc(self) -> PlaceDescAxis {
PlaceDescAxis { PlaceDescAxis {
span: PlaceSpan::Within(self), span: PlaceSpan::Within(self),
fills: false, fit: PlaceFit::Align,
rel_base: RelBase::WithRegion, rel_base: RelBase::WithRegion,
} }
} }
@@ -171,7 +191,7 @@ impl UiSpan {
pub const fn shifted_desc(self) -> PlaceDescAxis { pub const fn shifted_desc(self) -> PlaceDescAxis {
PlaceDescAxis { PlaceDescAxis {
span: PlaceSpan::Shifted(self), span: PlaceSpan::Shifted(self),
fills: false, fit: PlaceFit::Align,
rel_base: RelBase::Inherit, rel_base: RelBase::Inherit,
} }
} }
@@ -185,7 +205,7 @@ impl Len {
pub const fn as_desc(self) -> PlaceDescAxis { pub const fn as_desc(self) -> PlaceDescAxis {
PlaceDescAxis { PlaceDescAxis {
span: PlaceSpan::Sized(self), span: PlaceSpan::Sized(self),
fills: false, fit: PlaceFit::Align,
rel_base: RelBase::Len(self), rel_base: RelBase::Len(self),
} }
} }
+137 -62
View File
@@ -94,7 +94,10 @@ pub struct UiRenderState {
deferred: crate::util::HashSet<WidgetId>, deferred: crate::util::HashSet<WidgetId>,
/// What the walk has left to settle, deepest last. Ordered rather than /// What the walk has left to settle, deepest last. Ordered rather than
/// searched for, so finding the next one is not a pass over the marks. /// searched for, so finding the next one is not a pass over the marks.
pending: std::collections::BTreeSet<(usize, WidgetId)>, pending: std::collections::BinaryHeap<(usize, WidgetId)>,
pub(super) requests: crate::RequestArena,
changed: Vec<WidgetId>,
request_readers: HashMap<WidgetId, crate::util::HashSet<WidgetId>>,
pub moves: Moves, pub moves: Moves,
} }
@@ -108,6 +111,9 @@ impl UiRenderState {
slots: Default::default(), slots: Default::default(),
deferred: Default::default(), deferred: Default::default(),
pending: Default::default(), pending: Default::default(),
requests: Default::default(),
changed: Vec::new(),
request_readers: Default::default(),
moves: Default::default(), moves: Default::default(),
resized: false, resized: false,
} }
@@ -194,6 +200,7 @@ impl UiRenderState {
weak widgets: {all:#?}" weak widgets: {all:#?}"
); );
} }
self.requests.reset();
let root = root.into(); let root = root.into();
if self.root_changed(root) { if self.root_changed(root) {
self.redraw_all(root, rsc); self.redraw_all(root, rsc);
@@ -211,8 +218,13 @@ impl UiRenderState {
let _layout = diag::timer(TimerKind::FullLayout); let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc); self.clear(rsc);
if let Some(id) = root { if let Some(id) = root {
let ask = let ask = Placing::WINDOW.ask(
Placing::WINDOW.ask(rsc.widgets(), self.output_size, id.id(), PlaceDesc::WHOLE); rsc.widgets(),
&mut self.requests,
self.output_size,
id.id(),
PlaceDesc::WHOLE,
);
let info = self.root_info(&ask, rsc.widgets().is_region_node(id.id())); let info = self.root_info(&ask, rsc.widgets().is_region_node(id.id()));
self.draw_inner(id.id(), info, None, rsc); self.draw_inner(id.id(), info, None, rsc);
} }
@@ -242,7 +254,6 @@ impl UiRenderState {
); );
} }
let align = rsc.widgets().alignment(id); let align = rsc.widgets().alignment(id);
let declared = info.declared;
// Nothing this widget measured can be dirty while it draws: layout is // Nothing this widget measured can be dirty while it draws: layout is
// one bottom-up walk, so anything deeper has settled or deferred to // one bottom-up walk, so anything deeper has settled or deferred to
// its own parent, and a deferred one leaves that parent marked. // its own parent, and a deferred one leaves that parent marked.
@@ -256,7 +267,7 @@ impl UiRenderState {
.then(|| self.retained_answer(id, region, info)) .then(|| self.retained_answer(id, region, info))
.flatten() .flatten()
.and_then(|answer| { .and_then(|answer| {
let placed = info.placed.placement(region, answer.size, declared, align); let placed = info.placed.placement(region, answer.size, align);
self.try_reuse(id, region, placed, info, rsc) self.try_reuse(id, region, placed, info, rsc)
.then_some(answer) .then_some(answer)
}); });
@@ -268,7 +279,7 @@ impl UiRenderState {
// Where the drawing goes: the part its parent gave it, with the // Where the drawing goes: the part its parent gave it, with the
// answer placed inside that part on any axis the parent left // answer placed inside that part on any axis the parent left
// open. // open.
let placed = info.placed.placement(region, answer.size, declared, align); let placed = info.placed.placement(region, answer.size, align);
if placed != region { if placed != region {
self.relocate(id, placed, info, rsc); self.relocate(id, placed, info, rsc);
} }
@@ -327,7 +338,27 @@ impl UiRenderState {
let mask_slot = old let mask_slot = old
.as_ref() .as_ref()
.and_then(|old| old.mask_region.map(|_| old.mask)); .and_then(|old| old.mask_region.map(|_| old.mask));
let old_children = old.map_or_else(Vec::new, |old| old.children); let (mut old_children, textures, primitives, request_deps, mut scratch) = match old {
Some(old) => (
old.children,
old.textures,
old.primitives,
old.request_deps,
old.scratch,
),
None => Default::default(),
};
// Every one of these is a buffer this widget's last draw filled and
// `remove` emptied, kept for its capacity alone. A drawing whose
// primitives were still in it would record them twice.
debug_assert!(
textures.is_empty() && primitives.is_empty() && request_deps.is_empty(),
"'{}' ({id:?}) was drawn again over what its last draw left",
rsc.widgets().label(id)
);
let children = std::mem::take(&mut scratch.children);
let size_deps = std::mem::take(&mut scratch.size_deps);
let under = std::mem::take(&mut scratch.under);
rsc.widgets_mut().needs_redraw.remove(&id); rsc.widgets_mut().needs_redraw.remove(&id);
let window = self.output_size; let window = self.output_size;
let mut painter = Painter { let mut painter = Painter {
@@ -339,17 +370,16 @@ impl UiRenderState {
layer: info.layer, layer: info.layer,
own_layer: info.layer, own_layer: info.layer,
id, id,
textures: Vec::new(), textures,
primitives: Vec::new(), primitives,
mask_region: None, mask_region: None,
mask_slot, mask_slot,
children: Vec::new(), children,
size_deps: Vec::new(), size_deps,
// What the ask holds for is part of what the drawing holds for: request_deps,
// a box the widget's own rule took past the offer was decided in scratch,
// this window, and at the root nobody else keeps that range. own: LayoutHolds::ANY,
own: info.ask_holds, under,
under: Vec::new(),
answer_under: LayoutHolds::ANY, answer_under: LayoutHolds::ANY,
depth: info.depth, depth: info.depth,
move_idx, move_idx,
@@ -381,8 +411,10 @@ impl UiRenderState {
own, own,
answer_under, answer_under,
children, children,
size_deps, mut size_deps,
under, request_deps,
mut scratch,
mut under,
move_idx, move_idx,
layer, layer,
own_layer: _, own_layer: _,
@@ -402,7 +434,11 @@ impl UiRenderState {
// resolved into the rel base when the widget was asked, and resolving // resolved into the rel base when the widget was asked, and resolving
// it again here would take the fraction of a fraction. // it again here would take the fraction of a fraction.
let rules = rsc.widgets().size_rules(id); let rules = rsc.widgets().size_rules(id);
let ruled = |axis: Axis, reported: LayoutLen| match rules[axis].exact() { let ruled = |axis: Axis, reported: LayoutLen| {
if rules[axis].deferred().is_some() {
return info.rel_base[axis].into();
}
match rules[axis].exact() {
None => reported, None => reported,
Some(len) if len.leftover == Weight::ZERO => LayoutLen { Some(len) if len.leftover == Weight::ZERO => LayoutLen {
rel: info.rel_base[axis].rel, rel: info.rel_base[axis].rel,
@@ -410,36 +446,48 @@ impl UiRenderState {
leftover: Weight::ZERO, leftover: Weight::ZERO,
}, },
Some(len) => len.within_len(info.rel_base[axis]), Some(len) => len.within_len(info.rel_base[axis]),
}
}; };
let mut size = Size { let mut size = Size {
x: ruled(Axis::X, size.x), x: ruled(Axis::X, size.x),
y: ruled(Axis::Y, size.y), y: ruled(Axis::Y, size.y),
}; };
// A bound is a promise about the length as well as about the box: a // What the drawing read is combined with what the ask decided rather
// widget that drew past the box it was given -- a text too tall for // than replacing it: a widget may widen its own ranges, and cannot
// it, an image at its own size under a cap -- is still held to what // widen the ask's.
// its rule allows. let mut own_holds = own.and(info.ask_holds);
for axis in Axis::BOTH {
// A rule that is a fraction of the rel base is answered with the
// rel base's own length, so the answer is that rel base's and not
// just that many pixels of this window -- the same pin a widget
// that read its rel base took for its drawing. A bound counts:
// which side of it the box fell was decided against this rel
// base, and the same box of a different one can fall on the other.
if rules[axis].has_fraction() {
own_holds[axis].rel_base = Some(info.rel_base[axis]);
}
// A bound is a promise about the length as well as about the box:
// a widget that drew past the box it was given -- a text too tall
// for it, an image at its own size under a cap -- is still held to
// what its rule allows.
// //
// Held here rather than taken from the box, even where the bound // Held here rather than taken from the box, even where the bound
// decided that box. What a widget answers is its own, and a bound // decided that box. What a widget answers is its own, and a bound
// that replaced the answer would make a share into a fixed length // that replaced the answer would make a share into a fixed length
// the moment a box was long enough -- which is a length the span // the moment a box was long enough -- which is a length the span
// dividing that box decided from this answer, so the two would // dividing that box decided from this answer, so the two would
// choose each other. A share is left alone here for the same reason: // choose each other. A share is left alone here for the same
// it is a length only to whoever divides one, and the box that // reason: it is a length only to whoever divides one, and the box
// divider gives is a box this widget is asked in, where the bound is // that divider gives is a box this widget is asked in, where the
// applied to it. // bound is applied to it.
let mut bounded = LayoutHolds::ANY;
for axis in Axis::BOTH {
let answer = size[axis]; let answer = size[axis];
if answer.leftover != Weight::ZERO { if answer.leftover != Weight::ZERO {
continue; continue;
} }
let (outside, kept) = let (held, kept) = info.bounds[axis].outside(answer.without_leftover(), window[axis]);
info.bounds[axis].outside(answer.without_leftover(), window[axis]); own_holds[axis].window = own_holds[axis].window.and(kept);
bounded[axis].window = kept; if let Some(held) = held {
if let Some(outside) = outside { size[axis] = held.into();
size[axis] = info.bounds[axis].at(outside).into();
} }
} }
// A widget that clipped its contents to its box drew nothing outside // A widget that clipped its contents to its box drew nothing outside
@@ -469,22 +517,10 @@ impl UiRenderState {
if let Some(idx) = retired_move { if let Some(idx) = retired_move {
self.moves.remove(idx); self.moves.remove(idx);
} }
// A rule that is a fraction of the rel base is answered with the
// rel base's own length, so the answer is that rel base's and not just
// that many pixels of this window -- the same pin a widget that read
// its rel base took for its drawing. A bound counts: which side of it
// the box fell was decided against this rel base, and the same box of
// a different one can fall on the other.
let mut own_holds = own.and(bounded);
for axis in Axis::BOTH {
if rules[axis].has_fraction() {
own_holds[axis].rel_base = Some(info.rel_base[axis]);
}
}
let answer_holds = own_holds.and(answer_under); let answer_holds = own_holds.and(answer_under);
let holds = under let holds = under
.into_iter() .iter()
.fold(answer_holds, |holds, (_, child)| holds.and(child)); .fold(answer_holds, |holds, (_, child)| holds.and(*child));
debug_assert!( debug_assert!(
holds.contains(self.output_size, info.rel_base, region), holds.contains(self.output_size, info.rel_base, region),
"'{}' ({id:?}) drew in {}, outside the ranges it reported: {holds:?}", "'{}' ({id:?}) drew in {}, outside the ranges it reported: {holds:?}",
@@ -520,6 +556,15 @@ impl UiRenderState {
} }
} }
for &dep in &request_deps {
self.request_readers.entry(dep).or_default().insert(id);
}
old_children.clear();
size_deps.clear();
under.clear();
scratch.children = old_children;
scratch.size_deps = size_deps;
scratch.under = under;
let active = ActiveData { let active = ActiveData {
id, id,
placement: region, placement: region,
@@ -539,6 +584,8 @@ impl UiRenderState {
primitives, primitives,
mask_region, mask_region,
children, children,
request_deps,
scratch,
declared: info.declared, declared: info.declared,
bounds: info.bounds, bounds: info.bounds,
own_align: rsc.widgets().alignment(id), own_align: rsc.widgets().alignment(id),
@@ -585,6 +632,7 @@ impl UiRenderState {
if !active.drawn if !active.drawn
|| active.is_region_node() != info.region_node || active.is_region_node() != info.region_node
|| active.parent_move != info.parent_move || active.parent_move != info.parent_move
|| active.bounds != info.bounds
{ {
return None; return None;
} }
@@ -746,7 +794,6 @@ impl UiRenderState {
let placed = place.placement( let placed = place.placement(
region, region,
active.measured().unwrap_or(active.size), active.measured().unwrap_or(active.size),
active.declared,
active.own_align, active.own_align,
); );
let info = DrawInfo { let info = DrawInfo {
@@ -771,8 +818,8 @@ impl UiRenderState {
/// The rel base and the box a widget already drawn is given at `place` of /// The rel base and the box a widget already drawn is given at `place` of
/// the box its parent is being taken as. What narrowed its rel base and what /// the box its parent is being taken as. What narrowed its rel base and what
/// it declared are its own record's, so both are resolved against that /// it declared are its own record's. Declared lengths stay as the ask
/// parent's rel base again exactly as the first ask resolved them. /// resolved them; the destination only decides where they sit.
fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) { fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) {
place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align) place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align)
} }
@@ -841,6 +888,11 @@ impl UiRenderState {
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> { fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
let mut active = self.active.remove(&id); let mut active = self.active.remove(&id);
if let Some(active) = &mut active { if let Some(active) = &mut active {
for dep in active.request_deps.drain(..) {
if let Some(readers) = self.request_readers.get_mut(&dep) {
readers.remove(&id);
}
}
for primitive in &active.primitives { for primitive in &active.primitives {
let mask = self.layers.free(&primitive.handle); let mask = self.layers.free(&primitive.handle);
if mask != MaskIdx::NONE { if mask != MaskIdx::NONE {
@@ -914,6 +966,8 @@ impl UiRenderState {
primitives: Vec::new(), primitives: Vec::new(),
mask_region: None, mask_region: None,
children: Vec::new(), children: Vec::new(),
request_deps: Vec::new(),
scratch: Default::default(),
move_idx: info.parent_move, move_idx: info.parent_move,
declared: Declared::NONE, declared: Declared::NONE,
bounds: Bounds::ANY, bounds: Bounds::ANY,
@@ -927,6 +981,7 @@ impl UiRenderState {
} }
fn clear(&mut self, rsc: &mut dyn UiRsc) { fn clear(&mut self, rsc: &mut dyn UiRsc) {
self.request_readers.clear();
for (_, active) in self.active.drain() { for (_, active) in self.active.drain() {
if active.drawn { if active.drawn {
rsc.on_undraw(&active); rsc.on_undraw(&active);
@@ -947,6 +1002,7 @@ impl UiRenderState {
rsc.on_remove(id); rsc.on_remove(id);
self.remove(id, true, rsc); self.remove(id, true, rsc);
self.drop_slot(id); self.drop_slot(id);
self.request_readers.remove(&id);
} }
rsc.ui_mut().textures.free(); rsc.ui_mut().textures.free();
} }
@@ -954,6 +1010,18 @@ impl UiRenderState {
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) { pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
let _layout = diag::timer(TimerKind::IncrementalLayout); let _layout = diag::timer(TimerKind::IncrementalLayout);
self.changed.clear();
self.changed
.extend(rsc.widgets().needs_redraw.iter().copied());
while let Some(id) = self.changed.pop() {
if let Some(readers) = self.request_readers.get(&id) {
for &reader in readers {
if rsc.widgets_mut().needs_redraw.insert(reader) {
self.changed.push(reader);
}
}
}
}
// Deepest first, and strictly: a widget that cannot settle where it // Deepest first, and strictly: a widget that cannot settle where it
// is defers to its parent rather than drawing the parent from // is defers to its parent rather than drawing the parent from
// inside itself. It marks the parent, stays marked, and waits here // inside itself. It marks the parent, stays marked, and waits here
@@ -965,21 +1033,21 @@ impl UiRenderState {
// something below is about to change it -- which is the whole class // something below is about to change it -- which is the whole class
// of defect where a widget settles inside its parent's draw, clears // of defect where a widget settles inside its parent's draw, clears
// its mark there, and tells nobody its answer moved. // its mark there, and tells nobody its answer moved.
// The queue is that set, ordered: a mark made while the walk runs // A mark made while the walk runs queues itself through `mark`. What
// queues itself through `mark`. What ends the walk is still the set // ends the walk is the marks being spent rather than the queue being
// being spent, not the queue, so a mark that reached it another way // empty, so a mark that reached the queue twice, or that was settled
// cannot be left for the next frame. // another way, costs a pop and nothing else.
loop { loop {
for &id in rsc.widgets().needs_redraw.iter() { for &id in rsc.widgets().needs_redraw.iter() {
if !self.deferred.contains(&id) { if !self.deferred.contains(&id) {
let depth = self.depth(id); let depth = self.depth(id);
self.pending.insert((depth, id)); self.pending.push((depth, id));
} }
} }
if self.pending.is_empty() { if self.pending.is_empty() {
break; break;
} }
while let Some((depth, id)) = self.pending.pop_last() { while let Some((depth, id)) = self.pending.pop() {
// Settled inside an ancestor's draw, or deferred to one, // Settled inside an ancestor's draw, or deferred to one,
// since the mark that queued it. // since the mark that queued it.
if self.deferred.contains(&id) || !rsc.widgets().needs_redraw.contains(&id) { if self.deferred.contains(&id) || !rsc.widgets().needs_redraw.contains(&id) {
@@ -990,7 +1058,7 @@ impl UiRenderState {
// depth it had under the parent it left. // depth it had under the parent it left.
let now = self.depth(id); let now = self.depth(id);
if now != depth { if now != depth {
self.pending.insert((now, id)); self.pending.push((now, id));
continue; continue;
} }
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -1007,7 +1075,7 @@ impl UiRenderState {
fn mark(&mut self, id: WidgetId, widgets: &mut Widgets) { fn mark(&mut self, id: WidgetId, widgets: &mut Widgets) {
if widgets.needs_redraw.insert(id) && !self.deferred.contains(&id) { if widgets.needs_redraw.insert(id) && !self.deferred.contains(&id) {
let depth = self.depth(id); let depth = self.depth(id);
self.pending.insert((depth, id)); self.pending.push((depth, id));
} }
} }
@@ -1114,12 +1182,19 @@ impl UiRenderState {
Some(parent) => self.placing_of(parent, self.active[&parent].region), Some(parent) => self.placing_of(parent, self.active[&parent].region),
None => Placing::WINDOW, None => Placing::WINDOW,
}; };
let ask = at.ask(rsc.widgets(), self.output_size, id, active.asked); let ask = at.ask(
rsc.widgets(),
&mut self.requests,
self.output_size,
id,
active.asked,
);
let active = &self.active[&id]; let active = &self.active[&id];
let declared_changed = ask.declared != active.declared; // Even an inactive bound changes what the parent must track about its offer.
let constraints_changed = ask.declared != active.declared || ask.bounds != active.bounds;
let alignment_changed = rsc.widgets().alignment(id) != active.own_align; let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
if let Some(parent) = active.parent if let Some(parent) = active.parent
&& (declared_changed && (constraints_changed
|| alignment_changed || alignment_changed
|| active.re_asked || active.re_asked
|| !active.drawn || !active.drawn
+8
View File
@@ -4,6 +4,7 @@ use std::any::Any;
mod data; mod data;
mod handle; mod handle;
mod like; mod like;
mod request;
mod size_rule; mod size_rule;
mod tag; mod tag;
mod view; mod view;
@@ -12,6 +13,7 @@ mod widgets;
pub use data::*; pub use data::*;
pub use handle::*; pub use handle::*;
pub use like::*; pub use like::*;
pub use request::*;
pub use size_rule::*; pub use size_rule::*;
pub use tag::*; pub use tag::*;
pub use view::*; pub use view::*;
@@ -21,6 +23,12 @@ pub trait Widget: Any {
/// Draws the widget, and returns what it used of the box it was given. /// Draws the widget, and returns what it used of the box it was given.
fn draw(&mut self, painter: &mut Painter) -> Size; fn draw(&mut self, painter: &mut Painter) -> Size;
/// Describes an axis before painting. Return `None` when discovering it
/// needs a concrete box or work performed by `draw`.
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
self.size_hint(axis).map(|len| requests.length(len))
}
/// An exact length the widget can give without a painter or its children. /// An exact length the widget can give without a painter or its children.
/// Optional, and saves a draw rather than changing one: a hint that /// Optional, and saves a draw rather than changing one: a hint that
/// disagrees with the eventual draw fails a debug assertion. /// disagrees with the eventual draw fails a debug assertion.
+599
View File
@@ -0,0 +1,599 @@
use crate::{
ActiveData, Axis, Bound, LayoutLen, Len, Px, Rel, StrongWidget, UiNum, Weight, WidgetId,
Widgets, util::HashMap,
};
use std::cmp::Ordering;
impl<N: UiNum> From<N> for SizeRequest {
fn from(value: N) -> Self {
LayoutLen::px(value).into()
}
}
impl LayoutLen {
pub fn min(self, other: impl Into<SizeRequest>) -> SizeRequest {
SizeRequest::from(self).min(other)
}
pub fn max(self, other: impl Into<SizeRequest>) -> SizeRequest {
SizeRequest::from(self).max(other)
}
pub fn clamp(self, min: impl Into<SizeRequest>, max: impl Into<SizeRequest>) -> SizeRequest {
SizeRequest::from(self).clamp(min, max)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Op {
Sum,
Min,
Max,
}
/// One operand of a [`Node`]: a length, or another node. Node numbers are an
/// arena's own, so an operand says nothing about which arena it came from --
/// [`RequestedLen`] is the form that does, and the only one that leaves one.
#[derive(Clone, Copy, Debug, PartialEq)]
enum Operand {
Linear(LayoutLen),
Node(u32),
}
/// One sum or comparison, with its two operands. Whether anything under it
/// divides leftover space is carried on the node rather than walked for,
/// because every caller of one asks.
#[derive(Clone, Copy, Debug, PartialEq)]
struct Node {
op: Op,
a: Operand,
b: Operand,
leftover: bool,
}
/// The nodes of one expression, numbered from zero, and the folding that
/// happens as each is added. There are two kinds of owner and one kind of
/// arena: a rule's [`SizeRequest`] holds a small one for as long as the rule
/// lasts, and [`RequestArena`] holds the layout pass's.
#[derive(Clone, Debug, Default, PartialEq)]
struct Nodes(Vec<Node>);
impl Nodes {
fn node(&self, index: u32) -> Node {
self.0[index as usize]
}
/// The length itself, where no comparison is waiting on an allocation.
fn linear(&self, at: Operand) -> Option<LayoutLen> {
match at {
Operand::Linear(len) => Some(len),
Operand::Node(_) => None,
}
}
fn leftover(&self, at: Operand) -> bool {
match at {
Operand::Linear(len) => len.leftover > Weight::ZERO,
Operand::Node(index) => self.node(index).leftover,
}
}
/// `a op b`, which is a node only where the answer needs one. Two
/// lengths that keep their order whatever the room comes to are already
/// decided, and so are two operands that are the same thing.
fn combine(&mut self, op: Op, a: Operand, b: Operand) -> Operand {
if let (Some(x), Some(y)) = (self.linear(a), self.linear(b)) {
if matches!(op, Op::Sum) {
return Operand::Linear(x + y);
}
if let Some(order) = independent_order(x, y) {
let take_a = match op {
Op::Min => !order.is_gt(),
_ => !order.is_lt(),
};
return if take_a { a } else { b };
}
}
if a == b && !matches!(op, Op::Sum) {
return a;
}
let index = u32::try_from(self.0.len()).expect("more nodes than one arena can number");
let leftover = self.leftover(a) || self.leftover(b);
self.0.push(Node { op, a, b, leftover });
Operand::Node(index)
}
/// Copies `at` and everything under it out of `from`, with every length
/// it holds passed through `resolve`. Folded again on the way in, since
/// resolving a fraction can settle a comparison that was open before it.
fn graft(
&mut self,
from: &Self,
at: Operand,
resolve: impl Copy + Fn(LayoutLen) -> LayoutLen,
) -> Operand {
let index = match at {
Operand::Linear(len) => return Operand::Linear(resolve(len)),
Operand::Node(index) => index,
};
let Node { op, a, b, .. } = from.node(index);
let a = self.graft(from, a, resolve);
let b = self.graft(from, b, resolve);
self.combine(op, a, b)
}
fn write(&self, f: &mut std::fmt::Formatter<'_>, at: Operand) -> std::fmt::Result {
let index = match at {
Operand::Linear(len) => return write!(f, "{len}"),
Operand::Node(index) => index,
};
let Node { op, a, b, .. } = self.node(index);
write!(
f,
"{}(",
match op {
Op::Sum => "sum",
Op::Min => "min",
Op::Max => "max",
}
)?;
self.write(f, a)?;
write!(f, ", ")?;
self.write(f, b)?;
write!(f, ")")
}
}
/// A size request before a container has divided its leftover space.
/// Comparisons keep both operands until the share is known.
///
/// An expression is the same nodes the layout pass allocates, in an arena of
/// its own: importing one copies those nodes into the pass's arena, so there
/// is no second shape to keep in step and one place where folding is decided.
#[derive(Clone, Debug, PartialEq)]
pub enum SizeRequest {
Linear(LayoutLen),
/// Behind a pointer, because a plain length is what nearly every rule
/// holds and an expression should cost those rules nothing.
Expr(Box<Expr>),
}
/// An expression's own arena, and which of its nodes is the whole of it.
/// `root` is a node number rather than an operand, so an expression that
/// folded all the way down to a length cannot be written as one: that is a
/// [`SizeRequest::Linear`].
#[derive(Clone, Debug, PartialEq)]
pub struct Expr {
nodes: Nodes,
root: u32,
}
impl SizeRequest {
pub fn min(self, other: impl Into<Self>) -> Self {
self.join(Op::Min, other.into())
}
pub fn max(self, other: impl Into<Self>) -> Self {
self.join(Op::Max, other.into())
}
pub fn clamp(self, min: impl Into<Self>, max: impl Into<Self>) -> Self {
self.max(min).min(max)
}
/// `self op other`, keeping `self`'s arena and copying `other`'s into
/// it, so the two sets of node numbers become one. Two requests that are
/// the same request compare to the same thing whatever the room is, which
/// is a comparison worth not building -- but adding something to itself
/// is twice it.
fn join(self, op: Op, other: Self) -> Self {
if self == other && !matches!(op, Op::Sum) {
return self;
}
let (mut nodes, a) = match self {
Self::Linear(len) => (Nodes::default(), Operand::Linear(len)),
Self::Expr(expr) => {
let Expr { nodes, root } = *expr;
(nodes, Operand::Node(root))
}
};
let b = match other {
Self::Linear(len) => Operand::Linear(len),
Self::Expr(expr) => nodes.graft(&expr.nodes, Operand::Node(expr.root), |len| len),
};
match nodes.combine(op, a, b) {
Operand::Linear(len) => Self::Linear(len),
Operand::Node(root) => Self::Expr(Box::new(Expr { nodes, root })),
}
}
}
impl From<LayoutLen> for SizeRequest {
fn from(len: LayoutLen) -> Self {
Self::Linear(len)
}
}
impl From<Len> for SizeRequest {
fn from(len: Len) -> Self {
LayoutLen::from(len).into()
}
}
impl std::ops::Add for SizeRequest {
type Output = Self;
fn add(self, other: Self) -> Self {
self.join(Op::Sum, other)
}
}
impl std::fmt::Display for SizeRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Linear(len) => write!(f, "{len}"),
Self::Expr(expr) => expr.nodes.write(f, Operand::Node(expr.root)),
}
}
}
/// A discovered length. Deferred values belong to the current layout pass;
/// widgets must not retain them. Ordinary requests remain inline lengths.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RequestedLen(RequestValue);
#[derive(Clone, Copy, Debug, PartialEq)]
enum RequestValue {
Linear(LayoutLen),
Deferred {
index: u32,
epoch: u64,
leftover: bool,
},
}
impl From<LayoutLen> for RequestedLen {
fn from(len: LayoutLen) -> Self {
Self(RequestValue::Linear(len))
}
}
impl From<Len> for RequestedLen {
fn from(len: Len) -> Self {
LayoutLen::from(len).into()
}
}
impl RequestedLen {
/// The length itself, where no comparison is waiting on an allocation.
pub fn linear(&self) -> Option<LayoutLen> {
match self.0 {
RequestValue::Linear(len) => Some(len),
_ => None,
}
}
pub fn has_leftover(&self) -> bool {
match self.0 {
RequestValue::Linear(len) => len.leftover > Weight::ZERO,
RequestValue::Deferred { leftover, .. } => leftover,
}
}
}
#[derive(Default)]
pub(crate) struct RequestArena {
nodes: Nodes,
epoch: u64,
}
impl RequestArena {
pub(crate) fn reset(&mut self) {
self.nodes.0.clear();
self.epoch = self
.epoch
.checked_add(1)
.expect("layout generation exhausted");
}
/// An operand of this pass's arena as the handle that leaves it. The
/// epoch says which pass numbered the node, so a handle a widget kept
/// past its pass is caught rather than answering about whatever node
/// took its place.
fn handle(&self, at: Operand) -> RequestedLen {
RequestedLen(match at {
Operand::Linear(len) => RequestValue::Linear(len),
Operand::Node(index) => RequestValue::Deferred {
index,
epoch: self.epoch,
leftover: self.nodes.leftover(at),
},
})
}
/// The other direction, checked once where a handle comes back in rather
/// than again at every level of the walk it starts.
fn operand(&self, request: RequestedLen) -> Operand {
match request.0 {
RequestValue::Linear(len) => Operand::Linear(len),
RequestValue::Deferred { index, epoch, .. } => {
assert_eq!(epoch, self.epoch, "request retained beyond its layout pass");
Operand::Node(index)
}
}
}
/// A rule's expression in this pass's arena, with its fractions resolved
/// against the rel base the widget is being asked with.
pub(crate) fn import(&mut self, request: &SizeRequest, base: Len) -> RequestedLen {
let at = match request {
SizeRequest::Linear(len) => Operand::Linear(len.within_len(base)),
SizeRequest::Expr(expr) => {
self.nodes
.graft(&expr.nodes, Operand::Node(expr.root), |len| {
len.within_len(base)
})
}
};
self.handle(at)
}
pub(crate) fn bounded(&mut self, request: RequestedLen, bound: Bound) -> RequestedLen {
let request = match bound.min {
Some(min) => self.combine(Op::Max, request, min.into()),
None => request,
};
match bound.max {
Some(max) => self.combine(Op::Min, request, max.into()),
None => request,
}
}
fn combine(&mut self, op: Op, a: RequestedLen, b: RequestedLen) -> RequestedLen {
let (a, b) = (self.operand(a), self.operand(b));
let at = self.nodes.combine(op, a, b);
self.handle(at)
}
pub(crate) fn minimum(&self, request: RequestedLen, window: Px) -> Px {
let at = self.operand(request);
Px::from_raw(self.segment(at, Ratio::ZERO, window).fixed as i32)
}
fn segment(&self, of: Operand, at: Ratio, window: Px) -> Segment {
let index = match of {
Operand::Linear(len) => {
debug_assert!(
len.leftover >= Weight::ZERO,
"a leftover weight cannot be negative"
);
return Segment {
fixed: i64::from(len.without_leftover().to_px(window).raw()),
weight: i64::from(len.leftover.raw()),
end: None,
};
}
Operand::Node(index) => index,
};
let Node { op, a, b, .. } = self.nodes.node(index);
let a = self.segment(a, at, window);
let b = self.segment(b, at, window);
if matches!(op, Op::Sum) {
return a + b;
}
// At a crossing choose the branch to its right, so the next
// iteration advances rather than selecting that crossing again.
let order = a.value(at).cmp(&b.value(at)).then(a.weight.cmp(&b.weight));
let take_a = match op {
Op::Min => !order.is_gt(),
_ => !order.is_lt(),
};
let mut selected = if take_a { a } else { b };
selected.end = first(a.end, b.end);
if a.weight != b.weight {
let crossing = Ratio::new(b.fixed - a.fixed, a.weight - b.weight);
if crossing > at {
selected.end = first(selected.end, Some(crossing));
}
}
selected
}
/// Divides `room` between requests whose weights are nonnegative, one
/// length per request. A floor can overflow the room and a cap can leave
/// part of it unused, so the lengths need not come to `room`. Each edge
/// is rounded from the running total rather than from the length before
/// it, so two neighbouring slots meet exactly.
pub(crate) fn allocate<'a>(
&'a self,
requests: &'a [RequestedLen],
room: Px,
window: Px,
) -> impl Iterator<Item = Px> + 'a {
let mut at = Ratio::ZERO;
loop {
let total = requests.iter().fold(Segment::ZERO, |total, request| {
total + self.segment(self.operand(*request), at, window)
});
if total.value(at) >= i128::from(room.raw()) * i128::from(at.den) {
break;
}
if total.weight != 0 {
let solution = Ratio::new(i64::from(room.raw()) - total.fixed, total.weight);
if total.end.is_none_or(|end| solution <= end) {
at = solution;
break;
}
}
match total.end {
Some(end) => at = end,
None => break,
}
}
let mut prefix = 0_i128;
let mut previous = 0_i128;
requests.iter().map(move |request| {
prefix += self.segment(self.operand(*request), at, window).value(at);
let den = i128::from(at.den);
// Half away from zero, which is what `Fixed` rounds a division
// to: the two decide the same edge, and a change to one of them
// is a change to the other.
let edge = prefix.signum() * ((prefix.abs() + den / 2) / den);
let len = Px::from_raw((edge - previous) as i32);
previous = edge;
len
})
}
}
#[derive(Clone, Copy, Debug, Eq)]
struct Ratio {
num: i64,
den: i64,
}
impl PartialEq for Ratio {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Ratio {
const ZERO: Self = Self { num: 0, den: 1 };
fn new(num: i64, den: i64) -> Self {
debug_assert_ne!(den, 0, "a ratio of nothing");
if den < 0 {
Self {
num: -num,
den: -den,
}
} else {
Self { num, den }
}
}
}
impl Ord for Ratio {
fn cmp(&self, other: &Self) -> Ordering {
(i128::from(self.num) * i128::from(other.den))
.cmp(&(i128::from(other.num) * i128::from(self.den)))
}
}
impl PartialOrd for Ratio {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Copy)]
struct Segment {
fixed: i64,
weight: i64,
end: Option<Ratio>,
}
impl Segment {
const ZERO: Self = Self {
fixed: 0,
weight: 0,
end: None,
};
fn value(self, at: Ratio) -> i128 {
i128::from(self.fixed) * i128::from(at.den) + i128::from(self.weight) * i128::from(at.num)
}
}
impl std::ops::Add for Segment {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
fixed: self.fixed + other.fixed,
weight: self.weight + other.weight,
end: first(self.end, other.end),
}
}
}
fn first(a: Option<Ratio>, b: Option<Ratio>) -> Option<Ratio> {
match (a, b) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
}
}
/// Read-only discovery of requests through a widget's children. A request is
/// expressed in window lengths; `rel_base` supplies the base for declarations.
pub struct SizeRequests<'a> {
pub(crate) arena: &'a mut RequestArena,
pub(crate) measured: Option<&'a HashMap<WidgetId, ActiveData>>,
pub(crate) widgets: &'a Widgets,
pub(crate) dependencies: &'a mut Vec<WidgetId>,
pub(crate) rel_base: Len,
}
impl SizeRequests<'_> {
pub fn sum(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
self.arena.combine(Op::Sum, a, b)
}
pub fn min(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
self.arena.combine(Op::Min, a, b)
}
pub fn max(&mut self, a: RequestedLen, b: RequestedLen) -> RequestedLen {
self.arena.combine(Op::Max, a, b)
}
pub fn widget<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
) -> Option<RequestedLen> {
self.dependencies.push(child.id());
let rules = self.widgets.size_rules(child.id());
let rule = &rules[axis];
let request = match &rule.request {
Some(request) => self.arena.import(request, self.rel_base),
None => {
let widget = self.widgets.get_dyn(child.id())?;
widget.size_request(self, axis).or_else(|| {
self.measured?
.get(&child.id())?
.measured()
.map(|size| size[axis].into())
})?
}
};
Some(self.bounded(request, rule.bound))
}
pub(crate) fn bounded(&mut self, request: RequestedLen, bound: Bound) -> RequestedLen {
self.arena.bounded(request, bound.within_len(self.rel_base))
}
pub fn length(&self, len: LayoutLen) -> RequestedLen {
len.within_len(self.rel_base).into()
}
pub fn inset<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
padding: Px,
) -> Option<RequestedLen> {
let base = self.rel_base;
self.rel_base.px -= padding;
let request = self.widget(child, axis);
self.rel_base = base;
request.map(|request| self.sum(request, Len::from_parts(Rel::ZERO, padding).into()))
}
}
// Equal fractions keep this valid even when padding makes a rel base negative.
fn independent_order(a: LayoutLen, b: LayoutLen) -> Option<Ordering> {
if a.rel == b.rel && a.leftover == b.leftover {
Some(a.px.cmp(&b.px))
} else if a.px == b.px && a.rel == b.rel {
Some(a.leftover.cmp(&b.leftover))
} else {
None
}
}
+79 -131
View File
@@ -1,132 +1,88 @@
use crate::util::impl_axis_index; use crate::util::impl_axis_index;
use crate::{Axis, LayoutLen, Len, Rel}; use crate::{LayoutLen, Len, Rel, SizeRequest, Weight};
/// What a widget's length on one axis is, as a rule its parent applies where /// A preferred length and independent bounds on one axis. Without a
/// it draws it rather than an answer the widget gives about itself. /// request, the widget's drawing supplies the preferred length.
/// #[derive(Debug, Clone, PartialEq, Default)]
/// A rule and a drawn size are not two opinions to reconcile: a rule wins on pub struct SizeRule {
/// the axis it names, and the `Size` returned by `draw` answers only the axes pub request: Option<SizeRequest>,
/// with no rule. That is what lets a span divide its space around a length pub bound: Bound,
/// nobody has drawn yet, and it is why a rule lives beside the widget rather
/// than inside it -- the widget under the rule never has to know about it.
///
/// A rule gives a length or bounds one, never both: a share that is also
/// capped wants two widgets, one taking the share and one capping what is
/// inside it.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum SizeRule {
/// Whatever the widget reports from drawing.
#[default]
Free,
/// This length, whatever the widget reports.
Exact(LayoutLen),
/// At least this long, and otherwise whatever the box gives it.
Min(Len),
/// At most this long.
Max(Len),
/// Between the two.
Clamp { min: Len, max: Len },
} }
impl SizeRule { impl SizeRule {
/// What this rule allows the length to be where it does not give one pub const FREE: Self = Self {
/// outright. request: None,
pub fn bound(&self) -> Bound { bound: Bound::ANY,
match *self { };
Self::Free | Self::Exact(_) => Bound::ANY,
Self::Min(min) => Bound { pub fn min(min: Len) -> Self {
Self::bounded(Bound {
min: Some(min), min: Some(min),
max: None, max: None,
}, })
Self::Max(max) => Bound { }
pub fn max(max: Len) -> Self {
Self::bounded(Bound {
min: None, min: None,
max: Some(max), max: Some(max),
}, })
Self::Clamp { min, max } => Bound { }
pub fn clamp(min: Len, max: Len) -> Self {
Self::bounded(Bound {
min: Some(min), min: Some(min),
max: Some(max), max: Some(max),
}, })
}
/// A bound and no preferred length, so whatever the widget draws is held
/// to it.
fn bounded(bound: Bound) -> Self {
Self {
request: None,
bound,
} }
} }
/// Whether what this rule says is a fraction of the rel base, so that
/// the same rule against a different one is a different length.
pub fn has_fraction(&self) -> bool { pub fn has_fraction(&self) -> bool {
let bound = self.bound(); self.exact().is_some_and(|len| len.rel != Rel::ZERO) || self.bound.has_fraction()
self.exact().is_some_and(|len| len.rel != Rel::ZERO)
|| [bound.min, bound.max]
.iter()
.flatten()
.any(|len| len.rel != Rel::ZERO)
} }
/// This rule with a floor under it, which is the whole of it where there /// A linear preferred length, before applying the independent bounds.
/// was no rule.
pub fn at_least(&self, min: Len) -> Self {
match *self {
Self::Free | Self::Min(_) => Self::Min(min),
Self::Max(max) | Self::Clamp { max, .. } => Self::Clamp { min, max },
Self::Exact(len) => {
debug_assert!(false, "{len:?} is a length, so bounding it says nothing");
Self::Min(min)
}
}
}
/// This rule with a cap over it, which is the whole of it where there was
/// no rule.
pub fn at_most(&self, max: Len) -> Self {
match *self {
Self::Free | Self::Max(_) => Self::Max(max),
Self::Min(min) | Self::Clamp { min, .. } => Self::Clamp { min, max },
Self::Exact(len) => {
debug_assert!(false, "{len:?} is a length, so bounding it says nothing");
Self::Max(max)
}
}
}
/// The length this rule gives without the widget being drawn, if it can
/// give one.
pub fn declared(&self) -> Option<Len> {
self.exact().and_then(|len| len.declared())
}
/// The length this rule gives outright, whatever the widget reports --
/// which makes the widget's answer on that axis moot. A share counts: it
/// is a length the widget's parent still has to divide, so it is exact
/// here and resolved there, unlike `declared`, which is only the ones
/// that give a box directly.
pub fn exact(&self) -> Option<LayoutLen> { pub fn exact(&self) -> Option<LayoutLen> {
match self { match self.request {
Self::Exact(len) => Some(*len), Some(SizeRequest::Linear(len)) => Some(len),
Self::Free | Self::Min(_) | Self::Max(_) | Self::Clamp { .. } => None, _ => None,
}
}
/// Requests whose final length needs allocation, including a linear
/// share constrained by an independent bound.
pub(crate) fn deferred(&self) -> Option<&SizeRequest> {
let request = self.request.as_ref()?;
match request {
SizeRequest::Linear(len)
if len.leftover == Weight::ZERO || self.bound == Bound::ANY =>
{
None
}
_ => Some(request),
} }
} }
} }
/// What a rule allows a length to be where it does not give one outright: a /// A floor, a cap, or both, independent of the preferred length.
/// floor, a cap, or both. Each is a length of the rel base the widget is /// Fractions use the incoming rel base. An allocated slot keeps the
/// asked with, which is the base a declared length is a fraction of too, and /// allocator's base for bounds, even when the slot narrows the widget's own.
/// a bound that binds is a declaration -- the box comes to what it says.
/// ///
/// A bound is a [`Len`] and never a share. Which of a fixed and a relative /// A bound is a [`Len`]. Comparisons involving shares are [`SizeRequest`]s.
/// child is longer, asked at the length the cap is itself deciding, admits
/// several self-sizing fixed points, so a cap containing `leftover` has no
/// one answer: see `docs/LAYOUT.md` under the failed hypotheses.
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Bound { pub struct Bound {
pub min: Option<Len>, pub min: Option<Len>,
pub max: Option<Len>, pub max: Option<Len>,
} }
/// Which end of a bound a length fell outside.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outside {
Shorter,
Longer,
}
impl Bound { impl Bound {
/// Every length. /// Every length.
pub const ANY: Self = Self { pub const ANY: Self = Self {
@@ -134,14 +90,13 @@ impl Bound {
max: None, max: None,
}; };
/// The end [`Outside`] names, which is the length a widget outside it /// Whether either end is a fraction of the rel base, so that the same
/// gets instead of its own. /// bound against a different one binds at a different length.
pub fn at(&self, outside: Outside) -> Len { pub fn has_fraction(&self) -> bool {
let end = match outside { [self.min, self.max]
Outside::Shorter => self.min, .into_iter()
Outside::Longer => self.max, .flatten()
}; .any(|len| len.rel != Rel::ZERO)
end.expect("an end nothing is outside of")
} }
/// This bound as lengths of the window, from lengths of a rel base that /// This bound as lengths of the window, from lengths of a rel base that
@@ -166,32 +121,34 @@ impl Bounds {
x: Bound::ANY, x: Bound::ANY,
y: Bound::ANY, y: Bound::ANY,
}; };
pub fn from_axes(f: impl Fn(Axis) -> Bound) -> Self {
Self {
x: f(Axis::X),
y: f(Axis::Y),
}
}
} }
impl_axis_index!(Bounds => Bound); impl_axis_index!(Bounds => Bound);
impl From<LayoutLen> for SizeRule { impl From<LayoutLen> for SizeRule {
fn from(len: LayoutLen) -> Self { fn from(len: LayoutLen) -> Self {
Self::Exact(len) SizeRequest::from(len).into()
}
}
impl From<SizeRequest> for SizeRule {
fn from(request: SizeRequest) -> Self {
Self {
request: Some(request),
bound: Bound::ANY,
}
} }
} }
impl From<Option<LayoutLen>> for SizeRule { impl From<Option<LayoutLen>> for SizeRule {
fn from(len: Option<LayoutLen>) -> Self { fn from(len: Option<LayoutLen>) -> Self {
len.map_or(Self::Free, Self::Exact) len.map_or(Self::FREE, Self::from)
} }
} }
/// One rule per axis, which is how a widget carries a length on one axis and /// One rule per axis, which is how a widget carries a length on one axis and
/// leaves the other to whatever it draws. /// leaves the other to whatever it draws.
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct SizeRules { pub struct SizeRules {
pub x: SizeRule, pub x: SizeRule,
pub y: SizeRule, pub y: SizeRule,
@@ -199,12 +156,10 @@ pub struct SizeRules {
impl_axis_index!(SizeRules => SizeRule); impl_axis_index!(SizeRules => SizeRule);
/// What a widget's box is on each axis where something says so outright, /// Box lengths chosen by the ask: a declaration, a natural-size hint, or
/// before it is drawn: a rule beside it, or a hint it gives about itself. /// a binding bound. These are window lengths, already resolved against the
/// Whoever draws the widget resolves these against its rel base. /// incoming rel base. Moving a drawing preserves them instead of resolving
/// /// a fraction against its destination a second time.
/// A [`Len`] rather than a [`LayoutLen`], because a share can never be one
/// -- see [`LayoutLen::declared`].
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Declared { pub struct Declared {
pub x: Option<Len>, pub x: Option<Len>,
@@ -213,13 +168,6 @@ pub struct Declared {
impl Declared { impl Declared {
pub const NONE: Self = Self { x: None, y: None }; pub const NONE: Self = Self { x: None, y: None };
pub fn from_axes(f: impl Fn(Axis) -> Option<Len>) -> Self {
Self {
x: f(Axis::X),
y: f(Axis::Y),
}
}
} }
impl_axis_index!(Declared => Option<Len>); impl_axis_index!(Declared => Option<Len>);
+34 -14
View File
@@ -1,8 +1,8 @@
use std::sync::mpsc::{Receiver, Sender, channel}; use std::sync::mpsc::{Receiver, Sender, channel};
use crate::{ use crate::{
Axis, AxisAlign, IdLike, Len, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, Axis, AxisAlign, Bound, IdLike, Len, RegionAlign, SizeRequest, SizeRule, SizeRules,
Widget, WidgetData, WidgetId, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut}, util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
}; };
@@ -128,8 +128,8 @@ impl Widgets {
} }
/// The length rules whoever draws this widget applies to its box. /// The length rules whoever draws this widget applies to its box.
pub fn size_rules(&self, id: impl IdLike) -> SizeRules { pub fn size_rules(&self, id: impl IdLike) -> &SizeRules {
self.data(id).unwrap().size &self.data(id).unwrap().size
} }
/// Sets one axis's rule. The widget is marked rather than its parent /// Sets one axis's rule. The widget is marked rather than its parent
@@ -145,20 +145,40 @@ impl Widgets {
self.needs_redraw.insert(id); self.needs_redraw.insert(id);
} }
/// Puts a floor under this widget's length on one axis, keeping a cap it /// Changes the preferred length, leaving the bounds beside it alone.
/// already had. See [`SizeRule::at_least`]. pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<SizeRequest>) {
pub fn set_min_len(&mut self, id: impl IdLike, axis: Axis, min: Len) {
let id = id.id(); let id = id.id();
let rule = self.size_rules(id)[axis].at_least(min); let request = Some(len.into());
self.set_size_rule(id, axis, rule); let rule = &mut self.data_mut(id).unwrap().size[axis];
if rule.request == request {
return;
}
rule.request = request;
self.needs_redraw.insert(id);
} }
/// Puts a cap over it, keeping a floor it already had. See /// Puts a floor under this widget's length on one axis, keeping a cap it
/// [`SizeRule::at_most`]. /// already had and the preferred length beside it.
pub fn set_min_len(&mut self, id: impl IdLike, axis: Axis, min: Len) {
self.edit_bound(id.id(), axis, |bound| bound.min = Some(min));
}
/// Puts a cap over it, keeping a floor it already had.
pub fn set_max_len(&mut self, id: impl IdLike, axis: Axis, max: Len) { pub fn set_max_len(&mut self, id: impl IdLike, axis: Axis, max: Len) {
let id = id.id(); self.edit_bound(id.id(), axis, |bound| bound.max = Some(max));
let rule = self.size_rules(id)[axis].at_most(max); }
self.set_size_rule(id, axis, rule);
/// Edits one axis's bound where it sits, rather than reading the whole
/// rule out and writing it back: an expression beside the bound is not
/// this edit's business, and copying it to move one end would be the
/// only thing here that ever copies one.
fn edit_bound(&mut self, id: WidgetId, axis: Axis, edit: impl FnOnce(&mut Bound)) {
let bound = &mut self.data_mut(id).unwrap().size[axis].bound;
let before = *bound;
edit(bound);
if *bound != before {
self.needs_redraw.insert(id);
}
} }
/// Where this widget sits in a box longer than the length it takes. /// Where this widget sits in a box longer than the length it takes.
+1 -1
View File
@@ -168,7 +168,7 @@ impl Harness {
pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<LayoutLen>) { pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into<LayoutLen>) {
self.rsc self.rsc
.widgets_mut() .widgets_mut()
.set_size_rule(id, axis, SizeRule::Exact(len.into())); .set_size_rule(id, axis, SizeRule::from(len.into()));
} }
/// Sets the root and lays it out, so a pointer event has something to hit. /// Sets the root and lays it out, so a pointer event has something to hit.
+21 -11
View File
@@ -292,7 +292,7 @@ impl Plan {
align: None, align: None,
..self.clone() ..self.clone()
}), }),
self.size.map(|_| Plan { self.size.as_ref().map(|_| Plan {
size: None, size: None,
..self.clone() ..self.clone()
}), }),
@@ -342,6 +342,19 @@ impl Plan {
at(self); at(self);
} }
/// Drops every intrinsic bound from this tree, leaving the rest of it
/// -- and the generator's draws -- exactly as they were. That isolates
/// the ordinary path from the deferred one over the same shapes, which
/// is what says whether a difference is the bounds or the trees.
pub fn drop_bounds(&mut self) {
self.walk_mut(&mut |node| {
let Some(rules) = &mut node.size else { return };
for axis in Axis::BOTH {
rules[axis].bound = Bound::ANY;
}
});
}
/// The same tree with `edits` applied, by the indices the generator would /// The same tree with `edits` applied, by the indices the generator would
/// have used for them. /// have used for them.
/// ///
@@ -373,7 +386,7 @@ impl Plan {
} }
if plan.size.is_some() { if plan.size.is_some() {
if let Some(lens) = edits.sizes.get(&sized) { if let Some(lens) = edits.sizes.get(&sized) {
plan.size = Some(*lens); plan.size = Some(lens.clone());
} }
sized += 1; sized += 1;
} }
@@ -667,8 +680,8 @@ impl Sow<'_> {
match self.rng.below(8) { match self.rng.below(8) {
0 | 1 => self.len().into(), 0 | 1 => self.len().into(),
2 => LayoutLen::LEFTOVER.into(), 2 => LayoutLen::LEFTOVER.into(),
3 => SizeRule::Min(self.bound()), 3 => SizeRule::min(self.bound()),
4 => SizeRule::Max(self.bound()), 4 => SizeRule::max(self.bound()),
// Both in pixels, so one can be put under the other: a floor and // Both in pixels, so one can be put under the other: a floor and
// a cap that change sides with the window bound nothing, which // a cap that change sides with the window bound nothing, which
// is a caller's bug rather than a tree to grow. // is a caller's bug rather than a tree to grow.
@@ -677,12 +690,9 @@ impl Sow<'_> {
Px::from_f32(20.0 + self.rng.below(180) as f32), Px::from_f32(20.0 + self.rng.below(180) as f32),
Px::from_f32(20.0 + self.rng.below(180) as f32), Px::from_f32(20.0 + self.rng.below(180) as f32),
); );
SizeRule::Clamp { SizeRule::clamp(Len::px(a.min(b).to_f32()), Len::px(a.max(b).to_f32()))
min: Len::px(a.min(b).to_f32()),
max: Len::px(a.max(b).to_f32()),
} }
} _ => SizeRule::FREE,
_ => SizeRule::Free,
} }
} }
@@ -716,7 +726,7 @@ impl Sow<'_> {
} }
let idx = self.sized; let idx = self.sized;
self.sized += 1; 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. /// An alignment over some of the tree, kept where a test can change it.
@@ -852,7 +862,7 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
fn node(&mut self, plan: &Plan) -> StrongWidget { fn node(&mut self, plan: &Plan) -> StrongWidget {
let built = self.kind(&plan.kind); let built = self.kind(&plan.kind);
let id = built.id(); 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.rsc.ui_mut().widgets.set_size_rules(id, lens.x, lens.y);
self.tree.sized.push(id); self.tree.sized.push(id);
} }
+4
View File
@@ -6,6 +6,10 @@ pub struct LayerOffset {
} }
impl Widget for 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 { fn draw(&mut self, painter: &mut Painter) -> Size {
for _ in 0..self.offset { for _ in 0..self.offset {
painter.next_layer(); painter.next_layer();
-60
View File
@@ -1,60 +0,0 @@
use crate::prelude::*;
/// Asks its child in the shorter of a cap and the box this widget was given,
/// and answers what the child used, held to the same cap.
///
/// A cap on the box is a widget rather than a [`SizeRule`] because a box is
/// whoever asked's to decide: a rule that read the box it was given would be
/// decided again by every path that hands a widget one, including the ones
/// that re-place a drawing without asking it anything, and the decision would
/// then depend on which path arrived last. A widget is drawn again whenever
/// its own box changes, so the comparison is made where the answer can be
/// kept -- `longer_than` narrows the windows this drawing holds for, and
/// `holds` says the box lengths.
///
/// The box is what a text wraps at and what a scroll takes its viewport from,
/// which is why capping the answer alone is not the same thing.
pub struct MaxSize {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
}
impl MaxSize {
fn max(&self, axis: Axis) -> Option<Len> {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
}
}
impl Widget for MaxSize {
fn draw(&mut self, painter: &mut Painter) -> Size {
let align = painter.alignment();
let mut region = UiRegion::FULL;
for axis in Axis::BOTH {
let Some(max) = self.max(axis) else {
continue;
};
let own = painter.region_len(axis);
if painter.longer_than(own, max, axis) {
region[axis] = max.align(align[axis]);
}
}
let mut size = painter.widget_at(&self.inner, region).size();
for axis in Axis::BOTH {
// The child may draw past the box it was given -- a text too tall
// for it -- and the cap is a promise about the length as well. A
// share passes through: it is a length only to whoever divides
// one, and that is this widget's parent rather than this widget,
// which has already given the share the box the cap allows.
if let Some(max) = self.max(axis)
&& painter.longer_than(size[axis].without_leftover(), max, axis)
{
size[axis] = max.into();
}
}
size
}
}
-2
View File
@@ -1,5 +1,4 @@
mod layer; mod layer;
mod max_size;
mod offset; mod offset;
mod pad; mod pad;
mod scroll; mod scroll;
@@ -7,7 +6,6 @@ mod span;
mod stack; mod stack;
pub use layer::*; pub use layer::*;
pub use max_size::*;
pub use offset::*; pub use offset::*;
pub use pad::*; pub use pad::*;
pub use scroll::*; pub use scroll::*;
+4
View File
@@ -6,6 +6,10 @@ pub struct Offset {
} }
impl Widget for 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 { fn draw(&mut self, painter: &mut Painter) -> Size {
painter painter
.widget_at(&self.inner, UiRegion::FULL.offset(self.amt)) .widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
+16 -2
View File
@@ -6,6 +6,10 @@ pub struct Pad {
} }
impl Widget for Pad { impl Widget for Pad {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
requests.inset(&self.inner, axis, self.padding.along(axis))
}
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
// The inner's own alignment, not the near edge. This reports the // 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 // inner's size plus the padding, so where the box is that answer the
@@ -22,11 +26,11 @@ impl Widget for Pad {
let inner = painter.widget_at(&self.inner, self.padding.region()).size(); let inner = painter.widget_at(&self.inner, self.padding.region()).size();
Size { Size {
x: LayoutLen { x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right, px: inner.x.px + self.padding.along(Axis::X),
..inner.x ..inner.x
}, },
y: LayoutLen { y: LayoutLen {
px: inner.y.px + self.padding.top + self.padding.bottom, px: inner.y.px + self.padding.along(Axis::Y),
..inner.y ..inner.y
}, },
} }
@@ -57,6 +61,16 @@ impl Padding {
bottom: amt, bottom: amt,
} }
} }
/// Both sides of one axis together, which is what this padding takes
/// of a length along it.
pub fn along(&self, axis: Axis) -> Px {
match axis {
Axis::X => self.left + self.right,
Axis::Y => self.top + self.bottom,
}
}
/// `region` less this padding on each side. /// `region` less this padding on each side.
pub fn region_of(&self, mut region: UiRegion) -> UiRegion { pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
region.x.start.px += self.left; region.x.start.px += self.left;
+123 -51
View File
@@ -8,57 +8,73 @@ pub struct Span {
} }
impl Widget for 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.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 { 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; let axis = self.dir.axis;
// The row this span lays its children out along, as a length of the // 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 // 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 // business -- a slot is a length from there -- so what this reads is
// the length alone. // the length alone.
let row = painter.region_len(axis); let row = painter.region_len(axis);
// A length for every child before their final slots are chosen: from self.collect(painter, row, lens, true);
// a hint where one says, and from drawing otherwise. The rel base passes let gaps = self.gaps();
// through unchanged, so `rel(0.5)` is half the area this span was let fixed = lens
// given whatever else is in it and wherever this child sits among .iter()
// them; what a drawn child is asked in is the room left from the .try_fold(Len::from_parts(Rel::ZERO, gaps), |sum, len| {
// cursor, because a text has to wrap at the width actually there. Some(sum + len.linear()?.without_leftover())
let mut cursor = Len::ZERO; });
let mut lens = Vec::with_capacity(self.children.len()); if let Some(fixed) = fixed
for child in &self.children { && lens.iter().any(|len| len.has_leftover())
let len = match painter.size_hint(child, axis) { && !painter.longer_than(row, fixed, axis)
Some(len) => len, {
None => { // With no share to assign, intrinsic drawings keep the remaining
// Across itself the child sits where its own alignment // offer, including overflow. Their answer is only moved into a slot.
// says, in the whole of the row: a span is what contains self.collect(painter, row, lens, false);
// 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)
} }
}; let nonlinear = lens.iter().any(|len| len.linear().is_none());
cursor += len.without_leftover(); if nonlinear {
cursor.px += self.gap; painter.allocate(lens, row - Len::from_parts(Rel::ZERO, gaps), axis, values);
lens.push(len);
} }
let allocated = nonlinear.then(|| &values[..]);
let gaps = self let total = match allocated {
.gap Some(allocated) => LayoutLen {
.mul_int(self.children.len().saturating_sub(1) as i32); px: allocated.iter().fold(gaps, |sum, len| sum + *len),
let total = lens.iter().fold( ..LayoutLen::ZERO
},
None => lens.iter().fold(
LayoutLen { LayoutLen {
px: gaps, px: gaps,
..LayoutLen::ZERO ..LayoutLen::ZERO
}, },
|sum, len| sum + *len, |sum, len| sum + len.linear().unwrap(),
); ),
};
// 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.
let all_fixed = total.without_leftover(); let all_fixed = total.without_leftover();
let room = row - all_fixed; 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 any_leftover = total.leftover > Weight::ZERO;
let has_room = any_leftover && painter.longer_than(row, all_fixed, axis); let has_room = any_leftover && painter.longer_than(row, all_fixed, axis);
@@ -83,17 +99,34 @@ impl Widget for Span {
false => fixed, false => fixed,
true => fixed + room.scale(Rel::ratio(taken, total.leftover)), 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() {
// A child asking for nothing but a part of what is left over, // An allocated row already has a length for every child; without
// when nothing is, is not drawn at all. One that also asked for // one the request is the length and the room is divided here.
// pixels or a fraction keeps those and overflows. // Either way a child asking for nothing but a part of what is
if len.is_only_leftover() && !has_room { // 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); painter.undraw(child);
fixed.px += self.gap; fixed.px += self.gap;
continue; continue;
} }
let from = reached(fixed, taken); let from = reached(fixed, taken);
if len.leftover > Weight::ZERO && has_room { if shares {
taken += len.leftover; taken += len.leftover;
} }
fixed += len.without_leftover(); fixed += len.without_leftover();
@@ -106,8 +139,8 @@ impl Widget for Span {
// fixed child's slot is its own answer, so a drawing made in the // 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. // room is put there as it is, and one not made yet is made here.
let slot = self.slot(row, from, to); let slot = self.slot(row, from, to);
let mut place = slot.shifted_desc().fills().on_axis(axis); let mut place = slot.shifted_desc().allocated().on_axis(axis);
if len.leftover > Weight::ZERO && has_room { if shares {
place = place.rel_base(axis, slot.len()); place = place.rel_base(axis, slot.len());
} }
let used = painter.place_at(child, place).len(!axis); let used = painter.place_at(child, place).len(!axis);
@@ -125,13 +158,15 @@ impl Widget for Span {
fixed.px += self.gap; fixed.px += self.gap;
} }
// Carried whole rather than collapsed to one share: a span that sizes // Where nothing was allocated the weight is carried whole rather
// from its children does not resolve `leftover`, it passes the weight up, // than collapsed to one share, so nesting spans divides the same
// so nesting spans divides the same space rather than re-dividing a // space rather than re-dividing a share of it: four `leftover(1)`
// share of it. Four `leftover(1)` children under two spans under one span // children under two spans under one span get a quarter each, which
// get a quarter each, which collapsing to `leftover(1)` per level does // one share per level does not give. Resolution happens at the
// not give. Resolution happens at the nearest ancestor with a length, // nearest ancestor with a length, and the root always has one --
// 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 { let ortho = match shrinks {
true => ortho, true => ortho,
false => LayoutLen::rel(1.0), false => LayoutLen::rel(1.0),
@@ -141,6 +176,43 @@ impl Widget for Span {
} }
impl Span { 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 /// 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 /// 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 /// direction lays out from the far end, so the same two distances mirror
+13
View File
@@ -8,6 +8,19 @@ pub struct Stack {
} }
impl Widget for Stack { impl Widget for Stack {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
let sizing = match self.size {
StackSize::Default => None,
StackSize::Child(i) => self.children.get(i),
};
// With nothing sizing it a stack is a share of the box it is given,
// which is what its draw answers too.
match sizing {
Some(child) => requests.widget(child, axis),
None => Some(LayoutLen::LEFTOVER.into()),
}
}
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let sizing = match self.size { let sizing = match self.size {
StackSize::Default => None, StackSize::Default => None,
+26 -38
View File
@@ -53,28 +53,22 @@ widget_trait! {
move |state| { move |state| {
let id = self.add(state); let id = self.add(state);
let widgets = &mut state.ui_mut().widgets; let widgets = &mut state.ui_mut().widgets;
widgets.set_size_rule(id, Axis::X, SizeRule::Exact(size.x)); widgets.set_len(id, Axis::X, size.x);
widgets.set_size_rule(id, Axis::Y, SizeRule::Exact(size.y)); widgets.set_len(id, Axis::Y, size.y);
id id
} }
} }
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(); let len = len.into();
move |state| { move |state| {
let id = self.add(state); let id = self.add(state);
state state.ui_mut().widgets.set_len(id, Axis::X, len);
.ui_mut()
.widgets
.set_size_rule(id, Axis::X, SizeRule::Exact(len));
id id
} }
} }
/// Answers at least this wide, whatever it drew: a rule beside the /// Sets a floor on this widget's offered width and reported width.
/// widget, so what a row gives it is at least this even where the widget
/// itself wanted less. The box it draws in is untouched -- for that, see
/// [`MaxSize`].
fn min_width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> { fn min_width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into(); let len = len.into();
move |state| { move |state| {
@@ -93,36 +87,30 @@ widget_trait! {
} }
} }
/// Puts this in a [`MaxSize`]: it is asked in the shorter of the cap and /// Caps this widget's offered width and reported width.
/// the box that widget was given, and is as long as it used, held to the fn max_width(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
/// cap. A widget rather than a rule because the box is whoever asked's to
/// decide -- see [`MaxSize`].
fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
let len = len.into();
move |state| MaxSize {
inner: self.add_strong(state),
x: Some(len),
y: None,
}
}
fn max_height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
let len = len.into();
move |state| MaxSize {
inner: self.add_strong(state),
x: None,
y: Some(len),
}
}
fn height(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into(); let len = len.into();
move |state| { move |state| {
let id = self.add(state); let id = self.add(state);
state state.ui_mut().widgets.set_max_len(id, Axis::X, len);
.ui_mut() id
.widgets }
.set_size_rule(id, Axis::Y, SizeRule::Exact(len)); }
fn max_height(self, len: impl Into<Len>) -> impl WidgetIdFn<Rsc, WL::Widget> {
let len = len.into();
move |state| {
let id = self.add(state);
state.ui_mut().widgets.set_max_len(id, Axis::Y, len);
id
}
}
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_len(id, Axis::Y, len);
id id
} }
} }
+80
View File
@@ -0,0 +1,80 @@
use iris::{harness::Harness, prelude::*};
use std::{
alloc::{GlobalAlloc, Layout, System},
cell::Cell,
};
struct Counting;
thread_local! {
static COUNT: Cell<Option<usize>> = const { Cell::new(None) };
}
fn count() {
COUNT.with(|count| {
if let Some(n) = count.get() {
count.set(Some(n + 1));
}
});
}
// The wrapper preserves System's allocation and deallocation contracts;
// observing calls here also counts allocations hidden inside layout helpers.
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
count();
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 {
count();
unsafe { System.realloc(ptr, layout, size) }
}
}
#[global_allocator]
static ALLOCATOR: Counting = Counting;
#[test]
fn unchanged_tree_reuses_layout_storage() {
for rule in [
SizeRule::FREE,
leftover(1).clamp(20, 80).into(),
SizeRule::clamp(20.into(), 80.into()),
] {
let mut h = Harness::new((600, 200));
let mut children: Vec<StrongWidget> = Vec::new();
for _ in 0..8 {
let a = rect(Color::RED).add(&mut h.rsc);
h.rsc.widgets_mut().set_size_rule(a, Axis::X, rule.clone());
let row = (a, rect(Color::BLUE))
.span(Dir::RIGHT)
.add_strong(&mut h.rsc);
children.push(row);
}
let root = h.rsc.widgets_mut().add_strong(Span {
children,
dir: Dir::DOWN,
gap: Px::ZERO,
});
h.state.root = Some(root);
h.frame();
let ids: Vec<_> = h.render.active.keys().copied().collect();
for frame in 0..8 {
for &id in &ids {
h.rsc.widgets_mut().mark_for_redraw(id);
}
h.resize((600 + frame % 2, 200));
h.frame();
}
COUNT.set(Some(0));
for frame in 0..100 {
for &id in &ids {
h.rsc.widgets_mut().mark_for_redraw(id);
}
h.resize((600 + frame % 2, 200));
h.frame();
}
let allocations = COUNT.replace(None).unwrap();
println!("rule={rule:?}: {allocations} allocations over 100 resize frames");
assert_eq!(allocations, 0);
}
}
+88
View File
@@ -0,0 +1,88 @@
//! CPU comparison of a bounds attribute against the `MaxSize` wrapper it
//! replaced, using the same builder calls and the same geometry. The wrapper
//! is gone from this tree, so its side of the comparison is run by checking
//! out a commit that still has it: the fixture is written to build the same
//! way at both.
//!
//! MODE=cap FRAMES=2000 cargo test --release --test bounds_cost \
//! -- --ignored --nocapture
//!
//! `MODE` is `plain`, `exact` or `cap`; `REDRAW=1` marks every widget for
//! redraw each frame; `FRAMES` is how many resize frames to measure. Use
//! repeated `perf stat -e instructions:u` runs on the executable directly.
//! Process totals include the cold frame, so compare identical modes and
//! frame counts. Wall time on this machine is not a stable comparison.
mod rig;
use iris::{harness::Harness, prelude::*};
use rig::env;
/// The two widths the loop alternates. The cap of 80 binds at 300 and does
/// not at 100, so the measured frames cross it in both directions.
const WIDTHS: [i32; 2] = [100, 300];
#[test]
#[ignore = "instruction-count measurement"]
fn bounds_cost() {
let mode = env("MODE", "cap".to_string());
let redraw = env("REDRAW", 0_u8) != 0;
let frames = env("FRAMES", 2000_usize);
let mut h = Harness::new((300, 512));
let mut column = Span::empty(Dir::DOWN);
let mut leaves = Vec::new();
for _ in 0..128 {
let first = match mode.as_str() {
"plain" => rect(Color::RED).add_strong(&mut h.rsc).any(),
"exact" => rect(Color::RED).width(40).add_strong(&mut h.rsc).any(),
"cap" => rect(Color::RED).max_width(80).add_strong(&mut h.rsc).any(),
_ => panic!("unknown MODE {mode}"),
};
leaves.push(first.id());
let second = rect(Color::BLUE).add_strong(&mut h.rsc);
let row = Span {
children: vec![first, second],
dir: Dir::RIGHT,
gap: Px::ZERO,
};
column.push(row.height(4).add_strong(&mut h.rsc));
}
h.set_root(column);
let ids: Vec<_> = h.render.active.keys().copied().collect();
println!(
"mode={mode}, widgets={}, rule_bytes={}",
ids.len(),
std::mem::size_of::<SizeRule>()
);
// That the fixture measures what it says is checked on both sides of the
// crossing here rather than inside the measured loop, which is what the
// other rigs do. Per frame it measured only 0.65% of the total (5.780B
// against 5.743B instructions at MODE=cap, FRAMES=2000), but none of it
// is the layout the number is about.
for width in WIDTHS {
h.resize((width, 512));
h.frame();
let expected = match mode.as_str() {
"plain" => width / 2,
"exact" => 40,
"cap" => (width / 2).min(80),
_ => unreachable!("the mode was checked while building"),
};
for id in &leaves {
assert_eq!(h.region(id).unwrap().size().x, Px::from_int(expected));
}
println!(
"width {width}: first leaf {}",
h.region(&leaves[0]).unwrap()
);
}
for frame in 0..frames {
if redraw {
for &id in &ids {
h.rsc.widgets_mut().mark_for_redraw(id);
}
}
h.resize((WIDTHS[frame % WIDTHS.len()], 512));
h.frame();
}
}
+400
View File
@@ -0,0 +1,400 @@
use std::{cell::Cell, rc::Rc};
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
struct Counted {
draws: Rc<Cell<usize>>,
}
impl Widget for Counted {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.px_size();
painter.primitive(RectPrimitive::color(Color::RED));
Size::LEFTOVER
}
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::LEFTOVER)
}
}
#[test]
fn a_capped_share_returns_room_to_its_sibling() {
let mut h = Harness::new((300, 100));
let first = rect(Color::RED).max_width(80).add(&mut h.rsc);
let second = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((first, second).span(Dir::RIGHT));
assert_corners!(h, first, (0, 0), (80, 100));
assert_corners!(h, second, (80, 0), (300, 100));
h.resize((100, 100));
h.frame();
assert_corners!(h, first, (0, 0), (50, 100));
assert_corners!(h, second, (50, 0), (100, 100));
h.resize((300, 100));
h.frame();
assert_corners!(h, first, (0, 0), (80, 100));
assert_corners!(h, second, (80, 0), (300, 100));
}
#[test]
fn nested_shares_are_discovered_without_provisional_paint() {
let mut h = Harness::new((400, 100));
let draws = Rc::new(Cell::new(0));
let leaf = h.rsc.ui_mut().widgets.add_strong(Counted {
draws: draws.clone(),
});
let leaf_id = leaf.id();
let mut inner: StrongWidget = leaf;
for _ in 0..8 {
let sibling = rect(Color::BLUE).add_strong(&mut h.rsc);
inner = h.rsc.ui_mut().widgets.add_strong(Span {
children: vec![inner, sibling],
dir: Dir::RIGHT,
gap: Px::ZERO,
});
}
h.state.root = Some(inner);
h.frame();
assert_eq!(draws.get(), 1);
assert!(h.region(&leaf_id).is_some());
h.resize((800, 100));
h.frame();
assert_eq!(draws.get(), 2);
}
#[test]
fn nested_bounds_are_resolved_in_the_outer_allocation() {
let mut h = Harness::new((300, 100));
let a = rect(Color::RED).max_width(40).add(&mut h.rsc);
let b = rect(Color::GREEN).max_width(60).add(&mut h.rsc);
let inner = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
let tail = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((inner, tail).span(Dir::RIGHT));
assert_corners!(h, a, (0, 0), (40, 100));
assert_corners!(h, b, (40, 0), (100, 100));
assert_corners!(h, tail, (100, 0), (300, 100));
}
#[test]
fn request_edits_in_a_nested_child_reach_the_allocator() {
let mut h = Harness::new((300, 100));
let a = rect(Color::RED).max_width(80).add(&mut h.rsc);
let inner = (a,).span(Dir::RIGHT).add(&mut h.rsc);
let tail = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((inner, tail).span(Dir::RIGHT));
h.rsc.widgets_mut().set_max_len(a, Axis::X, Len::px(40.0));
h.frame();
assert_corners!(h, a, (0, 0), (40, 100));
assert_corners!(h, tail, (40, 0), (300, 100));
}
#[test]
fn adding_a_bound_to_a_previously_unbounded_share_reallocates_the_row() {
for hinted in [true, false] {
let mut h = Harness::new((300, 100));
let a = if hinted {
rect(Color::RED).add_strong(&mut h.rsc).any()
} else {
h.rsc.widgets_mut().add_strong(Unhinted).any()
};
let id = a.id();
let b = rect(Color::BLUE).add(&mut h.rsc);
let mut row = Span::empty(Dir::RIGHT);
row.push(a);
row.push(b.add_strong(&mut h.rsc));
h.set_root(row);
h.resize((400, 100));
h.frame();
h.rsc
.widgets_mut()
.set_size_rule(id, Axis::X, SizeRule::max(Len::px(80.0)));
h.frame();
assert_corners!(h, id, (0, 0), (80, 100));
assert_corners!(h, b, (80, 0), (400, 100));
}
}
#[test]
fn a_deferred_comparison_can_compare_two_different_weights() {
let a = SizeRequest::from(leftover(1.0) + px(30.0)).min(leftover(2.0));
let b = SizeRequest::from(leftover(1.0)).clamp(px(20.0), px(100.0));
let mut h = Harness::new((60, 100));
let a = rect(Color::RED).width(a).add(&mut h.rsc);
let b = rect(Color::BLUE).width(b).add(&mut h.rsc);
h.set_root((a, b).span(Dir::RIGHT));
assert_corners!(h, a, (0, 0), (40, 100));
assert_corners!(h, b, (40, 0), (60, 100));
h.resize((300, 100));
h.frame();
assert_corners!(h, a, (0, 0), (200, 100));
assert_corners!(h, b, (200, 0), (300, 100));
}
#[test]
fn a_length_expression_is_resolved_before_wrapping_text() {
let mut h = Harness::new((300, 500));
let text = wtext("one two three four five six seven eight nine ten")
.size(16)
.wrap(true)
.width(leftover(1).clamp(40, 80))
.add(&mut h.rsc);
let other = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((text, other).span(Dir::RIGHT));
let box_ = h.region(&text).unwrap();
assert_eq!(box_.top_left.x, Px::ZERO);
assert_eq!(box_.bot_right.x, Px::from_int(80));
assert!(box_.bot_right.y - box_.top_left.y > Px::from_int(30));
assert_corners!(h, other, (80, 0), (300, 500));
}
#[test]
fn relative_bounds_keep_the_allocators_base() {
let mut h = Harness::new((300, 100));
let head = rect(Color::BLUE).width(30).add(&mut h.rsc);
let bounded = rect(Color::RED)
.width(leftover(1).min(rel(0.25)))
.add(&mut h.rsc);
let tail = rect(Color::GREEN).add(&mut h.rsc);
h.set_root((head, bounded, tail).span(Dir::RIGHT));
assert_corners!(h, bounded, (30, 0), (105, 100));
assert_corners!(h, tail, (105, 0), (300, 100));
h.resize((400, 100));
h.frame();
assert_corners!(h, bounded, (30, 0), (130, 100));
assert_corners!(h, tail, (130, 0), (400, 100));
}
#[test]
fn the_root_resolves_a_deferred_request_again_after_resize() {
let mut h = Harness::new((300, 100));
let bounded = rect(Color::RED)
.width(leftover(1).min(rel(0.25)))
.add(&mut h.rsc);
h.set_root(bounded);
assert_corners!(h, bounded, (112.5, 0), (187.5, 100));
h.resize((400, 100));
h.frame();
assert_corners!(h, bounded, (150, 0), (250, 100));
}
#[test]
fn filling_a_stack_does_not_mean_its_sizing_child_was_already_allocated() {
let mut h = Harness::new((300, 100));
let child = rect(Color::RED).width(leftover(1).min(80)).add(&mut h.rsc);
let overlay = rect(Color::BLUE).add(&mut h.rsc);
let children: Vec<StrongWidget> =
vec![child.add_strong(&mut h.rsc), overlay.add_strong(&mut h.rsc)];
h.set_root(Stack {
children,
size: StackSize::Child(0),
});
assert_corners!(h, child, (110, 0), (190, 100));
assert_corners!(h, overlay, (110, 0), (190, 100));
}
struct Unhinted;
impl Widget for Unhinted {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.px_size();
painter.primitive(RectPrimitive::color(Color::RED));
Size::LEFTOVER
}
}
#[test]
fn bounds_also_apply_to_shares_discovered_by_drawing() {
let mut h = Harness::new((300, 100));
let a = h.rsc.widgets_mut().add_strong(Unhinted);
h.rsc
.widgets_mut()
.set_size_rule(a.id(), Axis::X, SizeRule::max(Len::px(80.0)));
let id = a.id();
let b = rect(Color::BLUE).add_strong(&mut h.rsc);
let b_id = b.id();
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
children: vec![a, b],
dir: Dir::RIGHT,
gap: Px::ZERO,
}));
h.frame();
assert_corners!(h, id, (0, 0), (80, 100));
assert_corners!(h, b_id, (80, 0), (300, 100));
h.resize((100, 100));
h.frame();
assert_corners!(h, id, (0, 0), (50, 100));
assert_corners!(h, b_id, (50, 0), (100, 100));
}
#[test]
fn comparisons_with_a_known_order_remain_plain_lengths() {
assert_eq!(leftover(2).max(leftover(5)), SizeRequest::from(leftover(5)));
assert_eq!(leftover(2).min(leftover(5)), SizeRequest::from(leftover(2)));
assert_eq!(
(px(10) + rel(0.5)).max(px(30) + rel(0.5)),
SizeRequest::from(px(30) + rel(0.5))
);
assert_eq!(LayoutLen::px(10).clamp(20, 80), SizeRequest::from(20));
}
#[test]
fn a_measured_nested_share_keeps_its_comparison_for_the_outer_span() {
let mut h = Harness::new((300, 100));
let a = h.rsc.widgets_mut().add_strong(Unhinted);
let a_id = a.id();
h.rsc
.widgets_mut()
.set_size_rule(a_id, Axis::X, SizeRule::max(Len::px(80.0)));
let b = rect(Color::BLUE).add_strong(&mut h.rsc);
let b_id = b.id();
let inner = h.rsc.widgets_mut().add_strong(Span {
children: vec![a, b],
dir: Dir::RIGHT,
gap: Px::ZERO,
});
let c = rect(Color::GREEN).add_strong(&mut h.rsc);
let c_id = c.id();
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
children: vec![inner, c],
dir: Dir::RIGHT,
gap: Px::ZERO,
}));
h.frame();
assert_corners!(h, a_id, (0, 0), (80, 100));
assert_corners!(h, b_id, (80, 0), (190, 100));
assert_corners!(h, c_id, (190, 0), (300, 100));
h.rsc
.widgets_mut()
.mark_for_redraw(h.state.root.as_ref().unwrap().id());
h.frame();
assert_corners!(h, c_id, (190, 0), (300, 100));
}
struct Natural;
impl Widget for Natural {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.primitive(RectPrimitive::color(Color::RED));
Size::from_axis(Axis::X, LayoutLen::px(64), LayoutLen::px(64))
}
fn size_hint(&self, _: Axis) -> Option<LayoutLen> {
Some(LayoutLen::px(64))
}
}
#[test]
fn relative_bounds_on_a_hinted_child_track_the_offer_before_its_declared_size() {
fn tree(h: &mut Harness) -> WidgetId {
let natural = h.rsc.widgets_mut().add_strong(Natural);
let id = natural.id();
h.rsc
.widgets_mut()
.set_size_rule(id, Axis::X, SizeRule::max(Len::rel(0.75)));
h.rsc.widgets_mut().set_size_rule(
id,
Axis::Y,
SizeRule::clamp(Len::rel(0.25), Len::rel(0.75)),
);
let inner = h.rsc.widgets_mut().add_strong(Stack {
children: vec![natural],
size: StackSize::Child(0),
});
let inner_id = inner.id();
let fill = rect(Color::BLUE).add_strong(&mut h.rsc);
let overlay = h.rsc.widgets_mut().add_strong(Stack {
children: vec![fill, inner],
size: StackSize::Child(0),
});
let share = rect(Color::GREEN).add_strong(&mut h.rsc);
let bounded = rect(Color::GREEN).add_strong(&mut h.rsc);
h.rsc.widgets_mut().set_size_rule(
bounded.id(),
Axis::X,
SizeRule::clamp(Len::rel(0.25), Len::rel(0.75)),
);
let fixed = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add_strong(&mut h.rsc);
h.state.root = Some(h.rsc.widgets_mut().add_strong(Span {
children: vec![share, bounded, fixed, overlay],
dir: Dir::LEFT,
gap: Px::from_int(8),
}));
h.rsc.widgets_mut().set_size_rule(
h.state.root.as_ref().unwrap().id(),
Axis::Y,
LayoutLen::rel(1).into(),
);
h.frame();
inner_id
}
let mut warm = Harness::new((1920, 1200));
let a = tree(&mut warm);
warm.resize((640, 900));
warm.frame();
let mut cold = Harness::new((640, 900));
let b = tree(&mut cold);
assert_eq!(warm.region(&a), cold.region(&b));
}
#[test]
fn a_bound_can_extend_an_explicit_share_request() {
let mut h = Harness::new((300, 100));
let a = rect(Color::RED)
.width(leftover(1))
.min_width(100)
.add(&mut h.rsc);
let b = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((a, b).span(Dir::RIGHT));
assert_corners!(h, a, (0, 0), (150, 100));
h.resize((120, 100));
h.frame();
assert_corners!(h, a, (0, 0), (100, 100));
assert_corners!(h, b, (100, 0), (120, 100));
}
#[test]
fn moving_scroll_content_preserves_its_resolved_expression_size() {
let mut h = Harness::new((300, 300));
let leaf = rect(Color::BLUE).add_strong(&mut h.rsc);
let leaf_id = leaf.id();
let content = h.rsc.widgets_mut().add_strong(Stack {
children: vec![leaf],
size: StackSize::Child(0),
});
let content_id = content.id();
h.rsc
.widgets_mut()
.set_size_rule(content_id, Axis::Y, leftover(1).min(120).into());
let scroll = h
.rsc
.widgets_mut()
.add_strong(Scroll::new(content, Axis::Y));
let scroll_id = scroll.id();
h.state.root = Some(scroll);
for _ in 0..3 {
h.rsc.widgets_mut().mark_for_redraw(scroll_id);
h.frame();
assert_corners!(h, content_id, (0, 90), (300, 210));
assert_corners!(h, leaf_id, (0, 90), (300, 210));
}
h.resize((300, 600));
h.frame();
assert_corners!(h, leaf_id, (0, 240), (300, 360));
}
#[test]
fn an_intrinsic_share_cap_uses_the_allocators_fractional_base() {
let mut h = Harness::new((300, 100));
let head = rect(Color::BLUE).width(30).add(&mut h.rsc);
let bounded = rect(Color::RED).max_width(Len::rel(0.25)).add(&mut h.rsc);
let tail = rect(Color::GREEN).add(&mut h.rsc);
h.set_root((head, bounded, tail).span(Dir::RIGHT));
assert_corners!(h, bounded, (30, 0), (105, 100));
assert_corners!(h, tail, (105, 0), (300, 100));
h.resize((400, 100));
h.frame();
assert_corners!(h, bounded, (30, 0), (130, 100));
assert_corners!(h, tail, (130, 0), (400, 100));
}
+261 -51
View File
@@ -824,7 +824,7 @@ fn row_under_fractions(kid: Option<LayoutLen>, gap: f32, box_w: f32) -> (Vec<Px>
if let Some(len) = kid { if let Some(len) = kid {
h.rsc h.rsc
.widgets_mut() .widgets_mut()
.set_size_rule(r.id(), Axis::X, SizeRule::Exact(len)); .set_size_rule(r.id(), Axis::X, SizeRule::from(len));
} }
ids.push(r.id()); ids.push(r.id());
kids.push(r.add_strong(&mut h.rsc)); kids.push(r.add_strong(&mut h.rsc));
@@ -1005,69 +1005,77 @@ fn a_region_node_root_is_a_region_node() {
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900)); assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900));
} }
/// A bound is a rule about what a widget answers: it holds the length that /// A bound holds the length a widget reports as well as narrowing the box it
/// reaches whoever asked and leaves the box alone. Here the content is 400 /// is offered, and the two are not the same question. Here two 200-wide rects
/// wide in a 250 window, so a cap cuts what the row reports and a floor /// fill a row in a 250 window, so a cap of 300 leaves the box alone and only
/// raises it, while the rects inside stay where the 250 box put them. /// cuts what the row reports -- which the window then centres, past both its
/// edges -- while a floor raises the report and the rects stay where the 250
/// box put them.
#[test] #[test]
fn a_bound_holds_what_a_widget_answers() { fn a_bound_holds_what_a_widget_answers() {
let row = |rule: SizeRule| { let bounded_row = |rule: SizeRule| {
let mut h = Harness::new((250, 200)); let mut h = Harness::new((250, 200));
let left = rect(Color::RED).width(200).add(&mut h.rsc); let left = rect(Color::RED).width(200).add(&mut h.rsc);
let right = rect(Color::BLUE).width(200).add(&mut h.rsc); let right = rect(Color::BLUE).width(200).add(&mut h.rsc);
let row = (left, right).span(Dir::RIGHT).add(&mut h.rsc); let row = (left, right).span(Dir::RIGHT).add(&mut h.rsc);
h.rsc.widgets_mut().set_size_rule(row, Axis::X, rule); h.rsc.widgets_mut().set_size_rule(row, Axis::X, rule);
h.set_root(row); h.set_root(row);
( (h, row, left)
h.region(&row).unwrap().size().x,
h.region(&left).unwrap().size().x,
)
}; };
let (capped, left) = row(SizeRule::Max(Len::px(300.0))); let (h, row, left) = bounded_row(SizeRule::max(Len::px(300.0)));
assert_eq!(capped, Px::from_int(300), "the cap, not the 400 drawn"); assert_eq!(
assert_eq!(left, Px::from_int(200), "the box the children were given"); h.region(&row).unwrap().size().x,
Px::from_int(300),
"the cap, not the 400 drawn"
);
assert_eq!(
h.region(&left).unwrap().size().x,
Px::from_int(200),
"the box the children were given"
);
assert_corners!(h, row, (-25, 0), (275, 200));
let (floored, _) = row(SizeRule::Min(Len::px(600.0))); let (h, row, _) = bounded_row(SizeRule::min(Len::px(600.0)));
assert_eq!(floored, Px::from_int(600), "the floor, not the 400 drawn"); assert_eq!(
h.region(&row).unwrap().size().x,
Px::from_int(600),
"the floor, not the 400 drawn"
);
let (free, _) = row(SizeRule::Free); let (h, row, _) = bounded_row(SizeRule::FREE);
assert_eq!(free, Px::from_int(400), "what it drew"); assert_eq!(
h.region(&row).unwrap().size().x,
Px::from_int(400),
"what it drew"
);
} }
/// A cap on the box is `MaxSize`, which asks its child in the shorter of the /// A cap narrows the box the widget is asked in, whether a declaration of its
/// cap and its own box. That is the box a text wraps at and a scroll takes /// own decides that box or the allocator divides a share into it. The cap is
/// its viewport from, so it cannot be had by holding the answer. /// an attribute of the widget rather than something wrapped around it, which
/// is what the id assertions say.
#[test] #[test]
fn a_cap_widget_asks_its_child_in_the_shorter_box() { fn a_cap_attribute_narrows_the_widgets_box() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
// A fraction of its box, so it says what box it was asked in. // A fraction of its box, so it says what box it was asked in.
let fills = rect(Color::RED).width(rel(1.0)).add(&mut h.rsc); let fills = rect(Color::RED).width(rel(1.0)).add(&mut h.rsc);
let capped = fills.max_width(300).add(&mut h.rsc); let capped = fills.max_width(300).add(&mut h.rsc);
assert_eq!(fills.id(), capped.id());
h.set_root(capped); h.set_root(capped);
assert_eq!(h.region(&fills).unwrap().size().x, Px::from_int(300)); assert_eq!(h.region(&fills).unwrap().size().x, Px::from_int(300));
assert_eq!(
h.region(&capped).unwrap().size().x,
Px::from_int(300),
"as long as its child used"
);
// A child that asked for a share takes the box the cap allows, and the // A share with nothing beside it: the cap is composed into the request
// share itself passes up: whoever divides one is this widget's parent. // and the allocator answers with it rather than the whole 400.
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let share = rect(Color::RED).add(&mut h.rsc); let share = rect(Color::RED).width(leftover(1)).add(&mut h.rsc);
let capped = share.max_width(300).add(&mut h.rsc); let capped = share.max_width(300).add(&mut h.rsc);
assert_eq!(share.id(), capped.id());
h.set_root(capped); h.set_root(capped);
assert_eq!(h.region(&share).unwrap().size().x, Px::from_int(300)); assert_eq!(h.region(&share).unwrap().size().x, Px::from_int(300));
assert_eq!(h.region(&capped).unwrap().size().x, Px::from_int(400));
} }
/// Which of the cap and the box is shorter is a question in pixels, so it is
/// asked again wherever the answer can change -- and the widget asking it is
/// drawn again whenever its own box is, which is what keeps the two in step.
#[test] #[test]
fn a_cap_widget_is_decided_again_on_either_side_of_the_crossing() { fn a_cap_attribute_is_decided_again_on_either_side_of_the_crossing() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
let probe = rect(Color::RED).add(&mut h.rsc); let probe = rect(Color::RED).add(&mut h.rsc);
h.set_root(probe.max_width(300)); h.set_root(probe.max_width(300));
@@ -1100,19 +1108,221 @@ fn a_cap_is_a_fraction_of_the_box_it_was_given() {
assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(150)); assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(150));
} }
/// A cap is a promise about the length as well as the box: a widget whose struct Offered {
/// content is longer than the box it was given reports what it drew, and the seen: Rc<Cell<PxVec2>>,
/// cap holds that down even though it never decided the box. answer: Size,
#[test] }
fn a_cap_holds_an_answer_that_overflowed_its_box() {
let mut h = Harness::new((250, 200)); impl Widget for Offered {
let left = rect(Color::RED).width(200).add(&mut h.rsc); fn draw(&mut self, painter: &mut Painter) -> Size {
let right = rect(Color::BLUE).width(200).add(&mut h.rsc); self.seen.set(painter.px_size());
let row = (left, right).span(Dir::RIGHT).add(&mut h.rsc); painter.primitive(RectPrimitive::color(Color::RED));
h.rsc.widgets_mut().set_max_len(row, Axis::X, 300.into()); self.answer
h.set_root(row); }
}
// The box is the 250 window, which the cap of 300 leaves alone, and the
// row draws 400 of it. Its answer is the cap, and the window centres it. #[test]
assert_corners!(h, row, (-25, 0), (275, 200)); fn bounds_constrain_the_offer_without_replacing_an_intrinsic_answer() {
for axis in Axis::BOTH {
for node in [false, true] {
let mut h = Harness::new((400, 400));
let seen = Rc::new(Cell::new(PxVec2::ZERO));
let probe = h.rsc.widgets_mut().add_strong(Offered {
seen: seen.clone(),
answer: Size::px(Vec2::new(40.0, 40.0)),
});
let id = probe.id();
h.rsc.widgets_mut().set_region_node(id, node);
h.rsc.widgets_mut().set_max_len(id, axis, 100.into());
h.state.root = Some(probe);
h.frame();
assert_eq!(seen.get()[axis], Px::from_int(100));
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(40));
h.rsc.widgets_mut().set_min_len(id, axis, 60.into());
h.frame();
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(60));
h.resize((50, 50));
h.frame();
assert_eq!(seen.get()[axis], Px::from_int(60));
h.rsc.widgets_mut().set_size_rule(id, axis, SizeRule::FREE);
h.frame();
assert_eq!(seen.get()[axis], Px::from_int(50));
assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(40));
}
}
}
#[test]
fn a_cap_attribute_is_the_scroll_viewport() {
for node in [false, true] {
let mut h = Harness::new((400, 400));
let content = rect(Color::RED).height(400).add(&mut h.rsc);
let inner = content.add_strong(&mut h.rsc);
let scroll = Scroll::new(inner, Axis::Y).max_height(100).add(&mut h.rsc);
h.rsc.widgets_mut().set_region_node(scroll, node);
h.set_root(scroll);
assert_corners!(h, scroll, (0, 150), (400, 250));
assert_corners!(h, content, (0, -150), (400, 250));
h.rsc.widgets_mut().get_mut(&scroll).unwrap().scroll(1000.0);
h.frame();
assert_corners!(h, content, (0, 150), (400, 550));
h.resize((400, 80));
h.frame();
assert_corners!(h, scroll, (0, 0), (400, 80));
assert_corners!(h, content, (0, 0), (400, 400));
}
}
#[test]
fn a_cap_attribute_wraps_text_before_it_answers() {
let mut h = Harness::new((400, 500));
let text = wtext("one two three four five six seven eight nine ten")
.size(16)
.wrap(true)
.max_width(80)
.align(Align::TOP_LEFT)
.add(&mut h.rsc);
h.set_root(text);
let capped = h.region(&text).unwrap().size();
assert!(capped.x <= Px::from_int(80));
assert!(capped.y > Px::from_int(30));
h.rsc
.widgets_mut()
.set_size_rule(text, Axis::X, SizeRule::FREE);
h.frame();
let free = h.region(&text).unwrap().size();
assert!(free.x > capped.x);
assert!(free.y < capped.y);
}
#[test]
fn dimensions_and_bounds_are_independent_attributes_in_either_order() {
for bounds_first in [false, true] {
let mut h = Harness::new((400, 400));
let probe = rect(Color::RED).add(&mut h.rsc);
let bounded = if bounds_first {
probe
.max_width(80)
.min_height(60)
.width(120)
.height(40)
.add(&mut h.rsc)
} else {
probe
.width(120)
.height(40)
.max_width(80)
.min_height(60)
.add(&mut h.rsc)
};
assert_eq!(probe.id(), bounded.id());
h.set_root(bounded);
assert_eq!(
h.region(&probe).unwrap().size(),
PxVec2::from_f32((80, 60).into())
);
h.rsc.widgets_mut().set_len(probe, Axis::X, 50);
h.rsc.widgets_mut().set_len(probe, Axis::Y, 100);
h.frame();
assert_eq!(
h.region(&probe).unwrap().size(),
PxVec2::from_f32((50, 100).into())
);
}
}
#[test]
fn changing_a_share_cap_replaces_it_without_losing_the_share() {
let mut h = Harness::new((300, 100));
let capped = rect(Color::RED)
.max_width(80)
.width(leftover(1))
.add(&mut h.rsc);
let sibling = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((capped, sibling).span(Dir::RIGHT));
assert_corners!(h, capped, (0, 0), (80, 100));
assert_corners!(h, sibling, (80, 0), (300, 100));
h.rsc.widgets_mut().set_max_len(capped, Axis::X, 160.into());
h.frame();
assert_corners!(h, capped, (0, 0), (150, 100));
assert_corners!(h, sibling, (150, 0), (300, 100));
}
// Reduced from seed 104 at depth 5: a widget widening its own window
// contract must not erase the bound's crossing at a quarter-window of 173.
#[test]
fn a_widgets_window_contract_cannot_widen_its_bounds_contract() {
fn tree(h: &mut Harness) -> WidgetId {
let content = rect(Color::RED)
.width(137)
.min_height(194)
.add_strong(&mut h.rsc);
let scroll = Scroll::new(content, Axis::X).add_strong(&mut h.rsc);
let probe = rect(Color::RED).add_strong(&mut h.rsc);
let id = probe.id();
let wide = rect(Color::GREEN).add_strong(&mut h.rsc);
let narrow = rect(Color::BLUE).add_strong(&mut h.rsc);
let branch = iris::random::Branch {
probe,
wide,
narrow,
threshold: 459.0,
}
.min_width(94)
.max_width(173)
.add_strong(&mut h.rsc);
let stack = Stack {
children: vec![scroll, branch],
size: StackSize::Child(0),
}
.add_strong(&mut h.rsc);
let mut children: Vec<StrongWidget> = (0..3)
.map(|_| rect(Color::RED).add_strong(&mut h.rsc).any())
.collect();
children.push(stack);
h.set_root(
Span {
children,
dir: Dir::RIGHT,
gap: Px::ZERO,
}
.height(rel(1)),
);
id
}
let mut warm = Harness::new((1920, 1200));
let probe = tree(&mut warm);
warm.resize((640, 900));
warm.frame();
assert_corners!(warm, probe, (480, 0), (640, 40));
let mut cold = Harness::new((640, 900));
let other = tree(&mut cold);
assert_eq!(warm.region(&probe), cold.region(&other));
}
#[test]
fn a_fixed_declaration_keeps_its_cap_when_the_row_has_no_leftover() {
let mut h = Harness::new((100, 100));
let fixed = rect(Color::RED).width(200).max_width(100).add(&mut h.rsc);
let share = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((fixed, share).span(Dir::RIGHT));
assert_corners!(h, fixed, (0, 0), (100, 100));
}
#[test]
fn a_new_bound_reaches_the_parent_even_when_the_current_answer_is_unchanged() {
let mut h = Harness::new((400, 100));
let leaf = rect(Color::RED).add(&mut h.rsc);
let inner = leaf.add_strong(&mut h.rsc);
let root = Scroll::new(inner, Axis::Y)
.pad(Padding::uniform(0))
.width(154)
.height(100)
.add(&mut h.rsc);
h.set_root(root);
assert_eq!(h.region(&leaf).unwrap().size().x, Px::from_int(154));
h.rsc.widgets_mut().set_min_len(leaf, Axis::X, 140.into());
h.rsc.widgets_mut().set_len(root, Axis::X, 78);
h.frame();
assert_corners!(h, leaf, (130, 0), (270, 100));
} }
+2 -2
View File
@@ -31,8 +31,8 @@ fn some_edits(seed: u64, of: &Plan) -> Edits {
( (
i, i,
SizeRules { SizeRules {
x: SizeRule::Exact(LayoutLen::LEFTOVER), x: SizeRule::from(LayoutLen::LEFTOVER),
y: SizeRule::Free, y: SizeRule::FREE,
}, },
) )
}) })
+86
View File
@@ -0,0 +1,86 @@
mod rig;
#[path = "scenario/mod.rs"]
mod scenario;
use iris::prelude::*;
use iris::random::{Edits, Plan, plan};
fn check_requests(edit: impl Fn(&mut Plan) + Sync) {
let count = rig::env("IRIS_DEFERRED_SEEDS", 20_u64);
let depth = rig::env("IRIS_DEFERRED_DEPTH", 4_usize);
let seeds = std::env::var("IRIS_DEFERRED_SEED")
.ok()
.and_then(|seed| seed.parse().ok())
.map_or_else(|| (1..=count).collect(), |seed| vec![seed]);
scenario::over_seeds(seeds, |seed| {
let mut grown = plan(seed, depth, &Edits::default());
edit(&mut grown);
for case in scenario::ALL {
if let Some(how) = scenario::diverges(&grown, case, seed) {
panic!(
"request seed {seed} depth {depth} after {}: {how}",
case.name()
);
}
}
});
}
#[test]
fn deferred_requests_agree_warm_and_cold() {
check_requests(|grown| {
let mut index = 0;
grown.walk_mut(&mut |node| {
if let Some(rules) = &mut node.size {
for axis in Axis::BOTH {
index += 1;
rules[axis] = match index % 7 {
0 => leftover(1).clamp(20, 120).into(),
1 => leftover(1).min(rel(0.5)).into(),
2 => (leftover(1) + px(30)).min(leftover(2)).into(),
_ => rules[axis].clone(),
};
}
}
});
});
}
#[test]
fn relative_intrinsic_bounds_agree_warm_and_cold() {
check_requests(|grown| {
grown.walk_mut(&mut |node| {
if let Some(rules) = &mut node.size {
for axis in Axis::BOTH {
let bound = &mut rules[axis].bound;
if bound.min.is_some() {
bound.min = Some(Len::rel(0.25));
}
if bound.max.is_some() {
bound.max = Some(Len::rel(0.75));
}
}
}
});
});
}
#[test]
fn preferred_requests_with_independent_bounds_agree_warm_and_cold() {
check_requests(|grown| {
let mut index = 0;
grown.walk_mut(&mut |node| {
if let Some(rules) = &mut node.size {
for axis in Axis::BOTH {
index += 1;
rules[axis].request = Some(match index % 4 {
0 => leftover(1).into(),
1 => rel(0.5).into(),
2 => px(80).into(),
_ => (leftover(1) + px(30)).min(leftover(2)),
});
}
}
});
});
}
+3 -1
View File
@@ -11,11 +11,13 @@
//! `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS` and `IRIS_GENERATED_DEPTH` //! `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS` and `IRIS_GENERATED_DEPTH`
//! select what the long run covers. //! select what the long run covers.
mod rig;
#[path = "scenario/mod.rs"] #[path = "scenario/mod.rs"]
mod scenario; mod scenario;
use iris::random::{Edits, Plan, plan}; use iris::random::{Edits, Plan, plan};
use scenario::{ALL, Case, diverges, env, over_seeds}; use rig::env;
use scenario::{ALL, Case, diverges, over_seeds};
/// How deep the generator branches. The generator widens two to four ways per /// How deep the generator branches. The generator widens two to four ways per
/// level, so depth is exponential in width and a deep narrow tree is not /// level, so depth is exponential in width and a deep narrow tree is not
+33 -12
View File
@@ -13,11 +13,15 @@
//! //!
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or //! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and //! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
//! `IRIS_DIRTY` how many widgets `many` marks at once. //! `IRIS_DIRTY` how many widgets `many` marks at once. `IRIS_UNBOUNDED=1`
//! removes intrinsic bounds while preserving the rest of the generated tree.
mod rig;
use iris::harness::Harness; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use iris::random::{Edits, Tree, grow}; use iris::random::{Edits, Tree, build, plan};
use rig::env;
use std::time::Instant; use std::time::Instant;
const OUTPUT: (f32, f32) = (1920.0, 1200.0); const OUTPUT: (f32, f32) = (1920.0, 1200.0);
@@ -93,13 +97,6 @@ fn a_selected_widget_retains_its_layout_events() {
diagnostics::clear_traced_widgets(); diagnostics::clear_traced_widgets();
} }
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(fallback)
}
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
fn trace_selected(tree: &Tree) { fn trace_selected(tree: &Tree) {
let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else { let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else {
@@ -125,9 +122,17 @@ fn rig_edits() -> Edits {
} }
} }
fn fixture(harness: &mut Harness, seed: u64, depth: usize) -> (StrongWidget, Tree) {
let mut plan = plan(seed, depth, &rig_edits());
if env("IRIS_UNBOUNDED", 0_u8) != 0 {
plan.drop_bounds();
}
build(&mut harness.rsc, &plan)
}
fn warm(seed: u64, depth: usize) -> (Harness, Tree) { fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
let mut harness = Harness::new(OUTPUT); let mut harness = Harness::new(OUTPUT);
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits()); let (root, tree) = fixture(&mut harness, seed, depth);
harness.state.root = Some(root); harness.state.root = Some(root);
harness.frame(); harness.frame();
println!( println!(
@@ -209,7 +214,7 @@ fn layout_cost() {
if selected("cold") { if selected("cold") {
let mut harness = Harness::new(OUTPUT); let mut harness = Harness::new(OUTPUT);
let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits()); let (root, tree) = fixture(&mut harness, seed, depth);
harness.state.root = Some(root); harness.state.root = Some(root);
println!( println!(
"fixture: seed {seed}, depth {depth}, {} widgets", "fixture: seed {seed}, depth {depth}, {} widgets",
@@ -255,7 +260,7 @@ fn layout_cost() {
harness harness
.rsc .rsc
.widgets_mut() .widgets_mut()
.set_size_rule(sized, Axis::X, SizeRule::Exact(len)); .set_size_rule(sized, Axis::X, SizeRule::from(len));
}); });
} }
@@ -276,3 +281,19 @@ fn layout_cost() {
}); });
} }
} }
#[cfg(feature = "layout-diagnostics")]
#[test]
fn repainting_measured_text_does_not_invalidate_its_span() {
use iris::core::layout_diagnostics as diag;
let mut h = Harness::new((400, 200));
let text = wtext("a paragraph that fits").wrap(true).add(&mut h.rsc);
h.set_root((text, wtext("another paragraph")).span(Dir::DOWN));
let _ = diag::take();
h.rsc.widgets_mut().mark_for_redraw(text);
h.frame();
let report = diag::take();
assert_eq!(report.distinct_widgets(), 1);
assert_eq!(report.hot_widgets()[0].id, text.id());
}
+11 -10
View File
@@ -8,17 +8,14 @@
//! //!
//! then the same after, and `diff` the two. A line is one widget: the seed, //! then the same after, and `diff` the two. A line is one widget: the seed,
//! its index in creation order, and its box in window pixels, or `-` where //! its index in creation order, and its box in window pixels, or `-` where
//! it is not drawn. //! it is not drawn. `IRIS_UNBOUNDED=1` drops the trees' intrinsic bounds, as
//! in the diagnostics rig, which compares the two paths over the same shapes.
mod rig;
use iris::harness::Harness; use iris::harness::Harness;
use iris::random::{Edits, grow}; use iris::random::{Edits, build, plan};
use rig::env;
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(fallback)
}
#[test] #[test]
#[ignore = "a dump to diff across commits, not a check"] #[ignore = "a dump to diff across commits, not a check"]
@@ -28,7 +25,11 @@ fn every_cold_layout_is_printed() {
let mut out = String::new(); let mut out = String::new();
for seed in 1..=seeds { for seed in 1..=seeds {
let mut harness = Harness::new((1920.0, 1200.0)); let mut harness = Harness::new((1920.0, 1200.0));
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default()); let mut plan = plan(seed, depth, &Edits::default());
if env("IRIS_UNBOUNDED", 0_u8) != 0 {
plan.drop_bounds();
}
let (root, tree) = build(&mut harness.rsc, &plan);
harness.state.root = Some(root); harness.state.root = Some(root);
harness.frame(); harness.frame();
for (index, id) in tree.ids.iter().enumerate() { for (index, id) in tree.ids.iter().enumerate() {
+85 -19
View File
@@ -1,22 +1,28 @@
//! What a resize frame costs and what it holds, on a tree the revision before //! Text-layout workloads with stable paragraphs for comparisons across revisions.
//! #16 also builds. //! PR #19's base uses the older spelling of the fixed 40-pixel width and has
//! //! no diagnostics. The random generator changed with layout, so it cannot
//! Deliberately written in the API subset `43ce8c7` and this branch share, so //! provide the same workload across the full PR.
//! the same source can be dropped into an old worktree and measured there:
//! that is the only like-for-like comparison with the code the retained
//! layout replaced. The random tree cannot carry one, because the generator
//! itself changed with the work.
//! //!
//! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \ //! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \
//! -- --ignored --nocapture resize_cost //! -- --ignored --nocapture resize_cost
//! PHASE=edit ROWS=40 FRAMES=2000 cargo test --release --test revision_cost \
//! -- --ignored --nocapture text_updates_cost
//! ROWS=2000 cargo test --release --test revision_cost \ //! ROWS=2000 cargo test --release --test revision_cost \
//! -- --ignored --nocapture text_memory //! -- --ignored --nocapture text_memory
//! //!
//! Wall time on this machine varies with CPU frequency; take the number from //! `text_updates_cost` selects idle, repaint, edit, or scroll with `PHASE`.
//! `perf stat -e instructions:u` on the test binary directly. //! It alternates a short suffix for edits so later frames do not get a longer
//! paragraph than earlier ones. These are CPU fixtures, with no GPU submission.
//!
//! Use repeated `perf stat -e instructions:u` runs on the executable directly;
//! process totals include font loading and the cold frame, so compare identical
//! row and frame counts. Wall time on this machine is not a stable comparison.
mod rig;
use iris::harness::Harness; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
use rig::env;
use std::time::Instant; use std::time::Instant;
/// xorshift64, so one seed is one set of paragraphs on any machine. /// xorshift64, so one seed is one set of paragraphs on any machine.
@@ -78,13 +84,6 @@ fn words(rng: &mut Rng, least: usize, most: usize) -> String {
const OUTPUT: (f32, f32) = (900.0, 1200.0); const OUTPUT: (f32, f32) = (900.0, 1200.0);
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(fallback)
}
/// A row of a fixed-width rect beside a column of one wrapping and one /// A row of a fixed-width rect beside a column of one wrapping and one
/// overflowing text: the shape that makes a container measure a child in a /// overflowing text: the shape that makes a container measure a child in a
/// box it will not keep. /// box it will not keep.
@@ -137,9 +136,10 @@ fn resize_cost() {
println!("paragraph {at}: {:?}", h.region(id)); println!("paragraph {at}: {:?}", h.region(id));
} }
// Two widths in turn is the friendly case for anything that remembers an // The sweep cycles 256 widths, avoiding the two-width cache-friendly case.
// answer, so `SWEEP=1` never repeats one -- a drag rather than a toggle.
let sweep = env("SWEEP", 0_usize) != 0; let sweep = env("SWEEP", 0_usize) != 0;
#[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take();
let mut elapsed = Vec::with_capacity(frames); let mut elapsed = Vec::with_capacity(frames);
for frame in 0..frames { for frame in 0..frames {
let narrower = match sweep { let narrower = match sweep {
@@ -151,6 +151,11 @@ fn resize_cost() {
h.frame(); h.frame();
elapsed.push(start.elapsed().as_secs_f64() * 1000.0); elapsed.push(start.elapsed().as_secs_f64() * 1000.0);
} }
#[cfg(feature = "layout-diagnostics")]
print!(
"{}",
iris::core::layout_diagnostics::take().per_frame(frames)
);
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap()); elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
println!( println!(
"resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \ "resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
@@ -203,3 +208,64 @@ fn text_memory() {
} }
report("after settling"); report("after settling");
} }
#[test]
#[ignore = "measurement, not a check"]
fn text_updates_cost() {
let rows = env("ROWS", 40_usize);
let frames = env("FRAMES", 1000_usize);
let phase = env("PHASE", String::from("edit"));
assert!(rows > 0 && frames > 0);
assert!(["idle", "repaint", "edit", "scroll"].contains(&phase.as_str()));
let mut h = Harness::new(OUTPUT);
let mut rng = Rng(1);
let mut col = Span::empty(Dir::DOWN);
let first = wtext(words(&mut rng, 12, 52))
.size(16)
.wrap(true)
.add(&mut h.rsc);
col.push(first.add_strong(&mut h.rsc));
for _ in 1..rows {
col.push(
wtext(words(&mut rng, 12, 52))
.size(16)
.wrap(true)
.add_strong(&mut h.rsc),
);
}
let root = col.scrollable().add(&mut h.rsc);
h.set_root(root);
#[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take();
let original = h.rsc[first].content.to_string();
let alternate = format!("{original} another word");
let start = Instant::now();
for frame in 0..frames {
match phase.as_str() {
"idle" => {}
"repaint" => {
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
}
"edit" => {
h.rsc[first].content.clear();
h.rsc[first].content.push_str(if frame % 2 == 0 {
&alternate
} else {
&original
});
}
"scroll" => h.rsc[root].scroll(if frame % 2 == 0 { -12.0 } else { 12.0 }),
_ => unreachable!(),
}
h.frame();
}
println!(
"{phase}: {rows} rows, {frames} frames, {:.1} ms",
start.elapsed().as_secs_f64() * 1000.0
);
#[cfg(feature = "layout-diagnostics")]
print!(
"{}",
iris::core::layout_diagnostics::take().per_frame(frames)
);
}
+12
View File
@@ -0,0 +1,12 @@
//! What the rigs need and none of them should spell its own way. Every
//! fuzzer and measurement here is run by hand with its parameters in the
//! environment, so one reader is shared rather than copied into each target.
/// A rig's parameter from the environment, or its default. A switch is
/// `env("NAME", 0_u8) != 0`, so `NAME=1` turns it on.
pub fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(fallback)
}
+19 -21
View File
@@ -30,13 +30,6 @@ pub fn over_seeds(seeds: Vec<u64>, run: impl Fn(u64) + Sync) {
}); });
} }
pub fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(fallback)
}
/// The window a tree is grown in, and the one a resize takes it to. /// The window a tree is grown in, and the one a resize takes it to.
const OUTER: (f32, f32) = (1920.0, 1200.0); const OUTER: (f32, f32) = (1920.0, 1200.0);
const INNER: (f32, f32) = (640.0, 900.0); const INNER: (f32, f32) = (640.0, 900.0);
@@ -200,8 +193,8 @@ fn mark(warm: &mut Harness, tree: &Tree, step: usize) {
fn a_rule(rng: &mut Rng) -> SizeRule { fn a_rule(rng: &mut Rng) -> SizeRule {
let len = Len::px(20.0 + rng.below(180) as f32); let len = Len::px(20.0 + rng.below(180) as f32);
match rng.below(4) { match rng.below(4) {
0 => SizeRule::Max(len), 0 => SizeRule::max(len),
1 => SizeRule::Min(len), 1 => SizeRule::min(len),
_ => LayoutLen::from(len).into(), _ => LayoutLen::from(len).into(),
} }
} }
@@ -213,7 +206,7 @@ fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Siz
}; };
warm.rsc warm.rsc
.widgets_mut() .widgets_mut()
.set_size_rules(tree.sized[idx], lens.x, lens.y); .set_size_rules(tree.sized[idx], lens.x.clone(), lens.y.clone());
lens lens
} }
@@ -357,16 +350,21 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu
/// buildable from what the failure printed. /// buildable from what the failure printed.
fn describe(id: WidgetId, h: &Harness) -> String { fn describe(id: WidgetId, h: &Harness) -> String {
let rules = h.rsc.widgets().size_rules(id); let rules = h.rsc.widgets().size_rules(id);
// A bound prints as itself: a failure is reproduced from what it printed, // The tree a failure names is written out again from what it printed, so
// and a rule shown as "no rule" cannot be written out again. // every part of a rule prints: the preferred length and the bounds are
let rule = |r: SizeRule| match r { // independent, and a bound lumped into "no rule" could not be rebuilt.
SizeRule::Free => "-".into(), let rule = |r: &SizeRule| {
SizeRule::Exact(len) => format!("{len}"), let mut out = r
SizeRule::Min(min) => format!(">{}", LayoutLen::from(min)), .request
SizeRule::Max(max) => format!("<{}", LayoutLen::from(max)), .as_ref()
SizeRule::Clamp { min, max } => { .map_or_else(String::new, |r| format!("{r}"));
format!(">{}<{}", LayoutLen::from(min), LayoutLen::from(max)) if let Some(min) = r.bound.min {
out += &format!(">{}", LayoutLen::from(min));
} }
if let Some(max) = r.bound.max {
out += &format!("<{}", LayoutLen::from(max));
}
if out.is_empty() { "-".into() } else { out }
}; };
let align = h.rsc.widgets().alignment(id); let align = h.rsc.widgets().alignment(id);
let side = |a: AxisAlign| { let side = |a: AxisAlign| {
@@ -383,8 +381,8 @@ fn describe(id: WidgetId, h: &Harness) -> String {
// A rule and an alignment are properties of whatever carries them, so // A rule and an alignment are properties of whatever carries them, so
// they print with that widget rather than as widgets of their own. // they print with that widget rather than as widgets of their own.
let mut out = describe_widget(id, h); let mut out = describe_widget(id, h);
if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) { if *rules != SizeRules::default() {
out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y)); out += &format!("[x:{},y:{}]", rule(&rules.x), rule(&rules.y));
} }
if align != RegionAlign::default() { if align != RegionAlign::default() {
out += &format!("@{},{}", side(align.x), side(align.y)); out += &format!("@{},{}", side(align.x), side(align.y));
+3 -1
View File
@@ -17,11 +17,13 @@
//! It is a fuzzer: run it once the ordinary tests pass, and turn what it //! It is a fuzzer: run it once the ordinary tests pass, and turn what it
//! finds into a test of its own rather than leaving a seed as the record. //! finds into a test of its own rather than leaving a seed as the record.
mod rig;
#[path = "scenario/mod.rs"] #[path = "scenario/mod.rs"]
mod scenario; mod scenario;
use iris::random::{Edits, Plan, plan}; use iris::random::{Edits, Plan, plan};
use scenario::{ALL, Case, diverges, env, over_seeds}; use rig::env;
use scenario::{ALL, Case, diverges, over_seeds};
/// Takes the first simplification that still fails, until none does. The /// Takes the first simplification that still fails, until none does. The
/// simplifications come biggest first, so this walks down rather than /// simplifications come biggest first, so this walks down rather than
+3
View File
@@ -32,3 +32,6 @@ mod tasks;
mod text_edit; mod text_edit;
#[path = "cases/unsettled.rs"] #[path = "cases/unsettled.rs"]
mod unsettled; mod unsettled;
#[path = "cases/deferred.rs"]
mod deferred;