Compare commits

...
Author SHA1 Message Date
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
26 changed files with 1695 additions and 189 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.
pub mask_region: Option<UiRegion>,
pub children: Vec<WidgetId>,
pub request_deps: Vec<WidgetId>,
pub(crate) scratch: DrawScratch,
/// The movable region its primitives are positioned through: its own when
/// opted in, otherwise the nearest ancestor's.
pub move_idx: MoveIdx,
@@ -101,3 +103,12 @@ pub struct Answer {
pub size: Size,
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;
/// The lengths of a box, in pixels, that one drawing of a widget holds for:
@@ -40,40 +40,39 @@ impl Len {
}
impl Bound {
/// Which end of this bound `len` falls outside, and the windows that
/// answer holds for. Nothing where it is inside, which is the answer
/// wherever there is no bound at all.
/// The end of this bound `len` falls outside, which is the length it
/// gets instead of its own, and the windows that answer holds for.
/// 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
/// 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
/// 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.
pub fn outside(&self, len: Len, window: Px) -> (Option<Outside>, Holds) {
let mut outside = None;
pub fn outside(&self, len: Len, window: Px) -> (Option<Len>, Holds) {
let mut held = None;
let mut holds = Holds::ANY;
let mut held = len;
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);
if shorter {
outside = Some(Outside::Shorter);
held = min;
held = Some(min);
}
}
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);
if longer {
debug_assert!(
outside.is_none(),
held.is_none(),
"a floor of {:?} over a cap of {max:?} bounds nothing",
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 layout_holds::*;
pub use painter::{Painter, PrimitiveLike};
pub use place::{PlaceDesc, PlaceDescAxis, RetainedPrimitive};
pub use place::{PlaceDesc, PlaceDescAxis, PlaceFit, RetainedPrimitive};
pub use render_state::*;
#[derive(Default)]
+189 -13
View File
@@ -1,9 +1,10 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{
Axis, Bounds, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign,
Rel, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextureHandle,
UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
Axis, Bound, Bounds, Declared, DrawScratch, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc,
PlaceFit, Px, PxVec2, RegionAlign, Rel, RenderedText, RequestArena, RequestedLen,
RetainedPrimitive, Size, SizeRequests, SizeRule, StrongWidget, TextAttrs, TextBuffer,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets,
render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind,
TexturePrimitive,
@@ -41,6 +42,8 @@ pub struct Painter<'a> {
pub(super) children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
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
/// 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
@@ -62,6 +65,134 @@ pub struct 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 self.rsc.widgets().size_rules(child.id())[axis].bound() == Bound::ANY
&& 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()
|| matches!(
self.rsc.widgets().size_rules(child.id())[axis],
SizeRule::Request(_)
)
});
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) {
let kind = self.rsc.ui_mut().primitives.kind::<P>();
self.write(kind, primitive, region);
@@ -195,9 +326,15 @@ impl<'a> Painter<'a> {
declared,
bounds,
holds: ask_holds,
} = self
.placing()
.ask(self.rsc.widgets(), self.window, id.id(), offer);
inputs,
} = self.placing().ask(
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());
#[cfg(feature = "layout-diagnostics")]
if region_node {
@@ -422,9 +559,10 @@ impl<'a> Painter<'a> {
/// worth anything, since reading one is also what makes its own size
/// depend on it.
pub fn has_exact_size(&self, axis: Axis) -> bool {
self.rsc.widgets().size_rules(self.id)[axis]
.exact()
.is_some()
matches!(
self.rsc.widgets().size_rules(self.id)[axis],
SizeRule::Exact(_) | SizeRule::Request(_)
)
}
/// This widget's own box in pixels. Reading it makes the drawing one
@@ -667,6 +805,13 @@ impl Widgets {
/// share included, since a share is a length only to whoever divides one,
/// and that is the parent rather than this widget.
fn exact_len(&self, id: WidgetId, axis: Axis) -> Option<LayoutLen> {
// 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.
if matches!(self.size_rules(id)[axis], SizeRule::Request(_)) {
return None;
}
self.size_rules(id)[axis].exact().or_else(|| {
// 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
@@ -706,6 +851,9 @@ pub(super) struct Ask {
/// drawing it is part of. Kept on the widget asked about rather than on
/// the asker because the root has no asker.
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 {
@@ -719,6 +867,7 @@ impl Placing {
pub(super) fn ask(
&self,
widgets: &Widgets,
requests: &mut RequestArena,
window: PxVec2,
id: WidgetId,
mut place: PlaceDesc,
@@ -726,10 +875,32 @@ impl Placing {
let align = widgets.alignment(id);
let rules = widgets.size_rules(id);
let mut holds = LayoutHolds::ANY;
let declared = widgets.declared_lens(id);
let mut inputs = LayoutHolds::ANY;
let mut declared = widgets.declared_lens(id);
let mut bounds = Bounds::ANY;
for axis in Axis::BOTH {
let base = place.base(axis, self.rel_base);
if let SizeRule::Request(request) = &rules[axis] {
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);
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
// 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
@@ -748,7 +919,11 @@ impl Placing {
// 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 widget because a widget is drawn again when its box changes.
bounds[axis] = rules[axis].bound().within_len(base);
let bound = rules[axis].bound();
if bound.has_fraction() {
inputs[axis].rel_base = Some(self.rel_base[axis]);
}
bounds[axis] = bound.within_len(base);
}
let (rel_base, region) =
place.rel_base_and_region(self.region, self.rel_base, declared, align);
@@ -759,6 +934,7 @@ impl Placing {
declared,
bounds,
holds,
inputs,
}
}
@@ -785,7 +961,7 @@ impl Placing {
// 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
// to be placed inside it -- is a box a share reads.
if place[axis].fills {
if place[axis].fit.fills() {
return (None, Holds::ANY);
}
// A share with nothing beside it is the box whatever the box is, so
@@ -838,7 +1014,7 @@ impl PlaceDesc {
let mut placed = region;
for axis in Axis::BOTH {
let reported = size[axis];
if reported.fills(declared[axis], self[axis].fills) {
if reported.fills(declared[axis], self[axis].fit.fills()) {
continue;
}
placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]);
+25 -5
View File
@@ -12,10 +12,24 @@ use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlaceDescAxis {
pub span: PlaceSpan,
pub fills: bool,
pub fit: PlaceFit,
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)]
pub enum PlaceSpan {
Within(UiSpan),
@@ -44,7 +58,13 @@ impl PlaceDescAxis {
/// 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.
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
}
@@ -153,7 +173,7 @@ impl UiSpan {
pub const fn within_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Within(self),
fills: false,
fit: PlaceFit::Align,
rel_base: RelBase::WithRegion,
}
}
@@ -171,7 +191,7 @@ impl UiSpan {
pub const fn shifted_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Shifted(self),
fills: false,
fit: PlaceFit::Align,
rel_base: RelBase::Inherit,
}
}
@@ -185,7 +205,7 @@ impl Len {
pub const fn as_desc(self) -> PlaceDescAxis {
PlaceDescAxis {
span: PlaceSpan::Sized(self),
fills: false,
fit: PlaceFit::Align,
rel_base: RelBase::Len(self),
}
}
+107 -28
View File
@@ -94,7 +94,10 @@ pub struct UiRenderState {
deferred: crate::util::HashSet<WidgetId>,
/// 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.
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,
}
@@ -108,6 +111,9 @@ impl UiRenderState {
slots: Default::default(),
deferred: Default::default(),
pending: Default::default(),
requests: Default::default(),
changed: Vec::new(),
request_readers: Default::default(),
moves: Default::default(),
resized: false,
}
@@ -194,6 +200,7 @@ impl UiRenderState {
weak widgets: {all:#?}"
);
}
self.requests.reset();
let root = root.into();
if self.root_changed(root) {
self.redraw_all(root, rsc);
@@ -211,8 +218,13 @@ impl UiRenderState {
let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc);
if let Some(id) = root {
let ask =
Placing::WINDOW.ask(rsc.widgets(), self.output_size, id.id(), PlaceDesc::WHOLE);
let ask = Placing::WINDOW.ask(
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()));
self.draw_inner(id.id(), info, None, rsc);
}
@@ -327,7 +339,27 @@ impl UiRenderState {
let mask_slot = old
.as_ref()
.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);
let window = self.output_size;
let mut painter = Painter {
@@ -339,17 +371,19 @@ impl UiRenderState {
layer: info.layer,
own_layer: info.layer,
id,
textures: Vec::new(),
primitives: Vec::new(),
textures,
primitives,
mask_region: None,
mask_slot,
children: Vec::new(),
size_deps: Vec::new(),
children,
size_deps,
request_deps,
scratch,
// What the ask holds for is part of what the drawing holds for:
// a box the widget's own rule took past the offer was decided in
// this window, and at the root nobody else keeps that range.
own: info.ask_holds,
under: Vec::new(),
under,
answer_under: LayoutHolds::ANY,
depth: info.depth,
move_idx,
@@ -381,8 +415,10 @@ impl UiRenderState {
own,
answer_under,
children,
size_deps,
under,
mut size_deps,
request_deps,
mut scratch,
mut under,
move_idx,
layer,
own_layer: _,
@@ -401,8 +437,12 @@ impl UiRenderState {
// rel base is the answer wherever the ask declared a length: it was
// resolved into the rel base when the widget was asked, and resolving
// it again here would take the fraction of a fraction.
let rules = rsc.widgets().size_rules(id);
let ruled = |axis: Axis, reported: LayoutLen| match rules[axis].exact() {
let rules = rsc.widgets().size_rules(id).clone();
let ruled = |axis: Axis, reported: LayoutLen| {
if matches!(rules[axis], crate::SizeRule::Request(_)) {
return info.rel_base[axis].into();
}
match rules[axis].exact() {
None => reported,
Some(len) if len.leftover == Weight::ZERO => LayoutLen {
rel: info.rel_base[axis].rel,
@@ -410,6 +450,7 @@ impl UiRenderState {
leftover: Weight::ZERO,
},
Some(len) => len.within_len(info.rel_base[axis]),
}
};
let mut size = Size {
x: ruled(Axis::X, size.x),
@@ -435,11 +476,10 @@ impl UiRenderState {
if answer.leftover != Weight::ZERO {
continue;
}
let (outside, kept) =
info.bounds[axis].outside(answer.without_leftover(), window[axis]);
let (held, kept) = info.bounds[axis].outside(answer.without_leftover(), window[axis]);
bounded[axis].window = kept;
if let Some(outside) = outside {
size[axis] = info.bounds[axis].at(outside).into();
if let Some(held) = held {
size[axis] = held.into();
}
}
// A widget that clipped its contents to its box drew nothing outside
@@ -483,8 +523,8 @@ impl UiRenderState {
}
let answer_holds = own_holds.and(answer_under);
let holds = under
.into_iter()
.fold(answer_holds, |holds, (_, child)| holds.and(child));
.iter()
.fold(answer_holds, |holds, (_, child)| holds.and(*child));
debug_assert!(
holds.contains(self.output_size, info.rel_base, region),
"'{}' ({id:?}) drew in {}, outside the ranges it reported: {holds:?}",
@@ -520,6 +560,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 {
id,
placement: region,
@@ -539,6 +588,8 @@ impl UiRenderState {
primitives,
mask_region,
children,
request_deps,
scratch,
declared: info.declared,
bounds: info.bounds,
own_align: rsc.widgets().alignment(id),
@@ -585,6 +636,7 @@ impl UiRenderState {
if !active.drawn
|| active.is_region_node() != info.region_node
|| active.parent_move != info.parent_move
|| active.bounds != info.bounds
{
return None;
}
@@ -841,6 +893,11 @@ impl UiRenderState {
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
let mut active = self.active.remove(&id);
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 {
let mask = self.layers.free(&primitive.handle);
if mask != MaskIdx::NONE {
@@ -914,6 +971,8 @@ impl UiRenderState {
primitives: Vec::new(),
mask_region: None,
children: Vec::new(),
request_deps: Vec::new(),
scratch: Default::default(),
move_idx: info.parent_move,
declared: Declared::NONE,
bounds: Bounds::ANY,
@@ -927,6 +986,7 @@ impl UiRenderState {
}
fn clear(&mut self, rsc: &mut dyn UiRsc) {
self.request_readers.clear();
for (_, active) in self.active.drain() {
if active.drawn {
rsc.on_undraw(&active);
@@ -947,6 +1007,7 @@ impl UiRenderState {
rsc.on_remove(id);
self.remove(id, true, rsc);
self.drop_slot(id);
self.request_readers.remove(&id);
}
rsc.ui_mut().textures.free();
}
@@ -954,6 +1015,18 @@ impl UiRenderState {
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")]
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
// is defers to its parent rather than drawing the parent from
// inside itself. It marks the parent, stays marked, and waits here
@@ -965,21 +1038,21 @@ impl UiRenderState {
// something below is about to change it -- which is the whole class
// of defect where a widget settles inside its parent's draw, clears
// its mark there, and tells nobody its answer moved.
// The queue is that set, ordered: a mark made while the walk runs
// queues itself through `mark`. What ends the walk is still the set
// being spent, not the queue, so a mark that reached it another way
// cannot be left for the next frame.
// A mark made while the walk runs queues itself through `mark`. What
// ends the walk is the marks being spent rather than the queue being
// empty, so a mark that reached the queue twice, or that was settled
// another way, costs a pop and nothing else.
loop {
for &id in rsc.widgets().needs_redraw.iter() {
if !self.deferred.contains(&id) {
let depth = self.depth(id);
self.pending.insert((depth, id));
self.pending.push((depth, id));
}
}
if self.pending.is_empty() {
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,
// since the mark that queued it.
if self.deferred.contains(&id) || !rsc.widgets().needs_redraw.contains(&id) {
@@ -990,7 +1063,7 @@ impl UiRenderState {
// depth it had under the parent it left.
let now = self.depth(id);
if now != depth {
self.pending.insert((now, id));
self.pending.push((now, id));
continue;
}
#[cfg(feature = "layout-diagnostics")]
@@ -1007,7 +1080,7 @@ impl UiRenderState {
fn mark(&mut self, id: WidgetId, widgets: &mut Widgets) {
if widgets.needs_redraw.insert(id) && !self.deferred.contains(&id) {
let depth = self.depth(id);
self.pending.insert((depth, id));
self.pending.push((depth, id));
}
}
@@ -1114,7 +1187,13 @@ impl UiRenderState {
Some(parent) => self.placing_of(parent, self.active[&parent].region),
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 declared_changed = ask.declared != active.declared;
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
+8
View File
@@ -4,6 +4,7 @@ use std::any::Any;
mod data;
mod handle;
mod like;
mod request;
mod size_rule;
mod tag;
mod view;
@@ -12,6 +13,7 @@ mod widgets;
pub use data::*;
pub use handle::*;
pub use like::*;
pub use request::*;
pub use size_rule::*;
pub use tag::*;
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.
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.
/// Optional, and saves a draw rather than changing one: a hint that
/// disagrees with the eventual draw fails a debug assertion.
+449
View File
@@ -0,0 +1,449 @@
use crate::{
ActiveData, Axis, Bound, LayoutLen, Len, Px, Rel, SizeRule, StrongWidget, UiNum, Weight,
WidgetId, Widgets, util::HashMap,
};
use std::{cmp::Ordering, sync::Arc};
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)
}
}
/// A size request before a container has divided its leftover space.
/// Comparisons keep both operands until the share is known.
#[derive(Clone, Debug, PartialEq)]
pub enum SizeRequest {
Linear(LayoutLen),
Sum(Arc<(Self, Self)>),
Min(Arc<(Self, Self)>),
Max(Arc<(Self, Self)>),
}
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 SizeRequest {
pub fn min(self, other: impl Into<Self>) -> Self {
let other = other.into();
if let (Self::Linear(a), Self::Linear(b)) = (&self, &other)
&& let Some(order) = independent_order(*a, *b)
{
return if !order.is_gt() { self } else { other };
}
if self == other {
self
} else {
Self::Min(Arc::new((self, other)))
}
}
pub fn max(self, other: impl Into<Self>) -> Self {
let other = other.into();
if let (Self::Linear(a), Self::Linear(b)) = (&self, &other)
&& let Some(order) = independent_order(*a, *b)
{
return if !order.is_lt() { self } else { other };
}
if self == other {
self
} else {
Self::Max(Arc::new((self, other)))
}
}
pub fn clamp(self, min: impl Into<Self>, max: impl Into<Self>) -> Self {
self.max(min).min(max)
}
}
impl std::ops::Add for SizeRequest {
type Output = Self;
fn add(self, other: Self) -> Self {
match (self, other) {
(Self::Linear(a), Self::Linear(b)) => Self::Linear(a + b),
(a, b) => Self::Sum(Arc::new((a, b))),
}
}
}
/// 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: usize,
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(Clone, Copy)]
enum Op {
Sum,
Min,
Max,
}
struct Node {
op: Op,
a: RequestedLen,
b: RequestedLen,
}
#[derive(Default)]
pub(crate) struct RequestArena {
nodes: Vec<Node>,
epoch: u64,
}
impl RequestArena {
pub(crate) fn reset(&mut self) {
self.nodes.clear();
self.epoch = self
.epoch
.checked_add(1)
.expect("layout generation exhausted");
}
pub(crate) fn import(&mut self, request: &SizeRequest, base: Len) -> RequestedLen {
let (op, pair) = match request {
SizeRequest::Linear(len) => return len.within_len(base).into(),
SizeRequest::Sum(pair) => (Op::Sum, pair),
SizeRequest::Min(pair) => (Op::Min, pair),
SizeRequest::Max(pair) => (Op::Max, pair),
};
let a = self.import(&pair.0, base);
let b = self.import(&pair.1, base);
self.combine(op, a, b)
}
fn combine(&mut self, op: Op, a: RequestedLen, b: RequestedLen) -> RequestedLen {
if let (Some(x), Some(y)) = (a.linear(), b.linear()) {
if matches!(op, Op::Sum) {
return (x + y).into();
}
let order = independent_order(x, y);
if let Some(order) = order {
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 = self.nodes.len();
self.nodes.push(Node { op, a, b });
RequestedLen(RequestValue::Deferred {
index,
epoch: self.epoch,
leftover: a.has_leftover() || b.has_leftover(),
})
}
pub(crate) fn minimum(&self, request: RequestedLen, window: Px) -> Px {
Px::from_raw(self.segment(request, Ratio::ZERO, window).fixed as i32)
}
fn segment(&self, request: RequestedLen, at: Ratio, window: Px) -> Segment {
match request.0 {
RequestValue::Linear(len) => {
debug_assert!(
len.leftover >= Weight::ZERO,
"a leftover weight cannot be negative"
);
Segment {
fixed: i64::from(len.without_leftover().to_px(window).raw()),
weight: i64::from(len.leftover.raw()),
end: None,
}
}
RequestValue::Deferred { index, epoch, .. } => {
assert_eq!(epoch, self.epoch, "request retained beyond its layout pass");
let Node { op, a, b } = self.nodes[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(*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(*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];
if let SizeRule::Request(request) = rule {
return Some(self.arena.import(request, self.rel_base));
}
if let Some(exact) = rule.exact() {
return Some(exact.within_len(self.rel_base).into());
}
let widget = self.widgets.get_dyn(child.id())?;
let request = 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 {
let bound = bound.within_len(self.rel_base);
let request = match bound.min {
Some(min) => self.max(request, min.into()),
None => request,
};
match bound.max {
Some(max) => self.min(request, max.into()),
None => request,
}
}
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
}
}
+35 -43
View File
@@ -1,5 +1,6 @@
use crate::util::impl_axis_index;
use crate::{Axis, LayoutLen, Len, Rel};
use crate::{Axis, LayoutLen, Len, Rel, SizeRequest};
use std::sync::Arc;
/// What a widget's length on one axis is, as a rule its parent applies where
/// it draws it rather than an answer the widget gives about itself.
@@ -10,16 +11,17 @@ use crate::{Axis, LayoutLen, Len, Rel};
/// 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)]
/// Exact expressions can bound a share before allocation. Bounds on an
/// intrinsic answer are applied after that answer becomes known.
#[derive(Debug, Clone, PartialEq, Default)]
pub enum SizeRule {
/// Whatever the widget reports from drawing.
#[default]
Free,
/// This length, whatever the widget reports.
Exact(LayoutLen),
/// An exact request whose comparisons await the parent's allocation.
Request(Arc<SizeRequest>),
/// At least this long, and otherwise whatever the box gives it.
Min(Len),
/// At most this long.
@@ -33,7 +35,7 @@ impl SizeRule {
/// outright.
pub fn bound(&self) -> Bound {
match *self {
Self::Free | Self::Exact(_) => Bound::ANY,
Self::Free | Self::Exact(_) | Self::Request(_) => Bound::ANY,
Self::Min(min) => Bound {
min: Some(min),
max: None,
@@ -52,12 +54,7 @@ impl SizeRule {
/// 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 {
let bound = self.bound();
self.exact().is_some_and(|len| len.rel != Rel::ZERO)
|| [bound.min, bound.max]
.iter()
.flatten()
.any(|len| len.rel != Rel::ZERO)
self.exact().is_some_and(|len| len.rel != Rel::ZERO) || self.bound().has_fraction()
}
/// This rule with a floor under it, which is the whole of it where there
@@ -66,10 +63,8 @@ impl SizeRule {
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)
}
Self::Request(ref request) => request.as_ref().clone().max(min).into(),
Self::Exact(len) => len.max(min).into(),
}
}
@@ -79,10 +74,8 @@ impl SizeRule {
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)
}
Self::Request(ref request) => request.as_ref().clone().min(max).into(),
Self::Exact(len) => len.min(max).into(),
}
}
@@ -100,33 +93,24 @@ impl SizeRule {
pub fn exact(&self) -> Option<LayoutLen> {
match self {
Self::Exact(len) => Some(*len),
Self::Free | Self::Min(_) | Self::Max(_) | Self::Clamp { .. } => None,
Self::Free | Self::Request(_) | Self::Min(_) | Self::Max(_) | Self::Clamp { .. } => {
None
}
}
}
}
/// What a rule allows a length to be where it does not give one outright: a
/// floor, a cap, or both. Each is a length of the rel base the widget is
/// asked with, which is the base a declared length is a fraction of too, and
/// a bound that binds is a declaration -- the box comes to what it says.
/// asked with, which is the base a declared length is a fraction of too.
///
/// A bound is a [`Len`] and never a share. Which of a fixed and a relative
/// 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.
/// A bound is a [`Len`]. Comparisons involving shares are [`SizeRequest`]s.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Bound {
pub min: 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 {
/// Every length.
pub const ANY: Self = Self {
@@ -134,14 +118,13 @@ impl Bound {
max: None,
};
/// The end [`Outside`] names, which is the length a widget outside it
/// gets instead of its own.
pub fn at(&self, outside: Outside) -> Len {
let end = match outside {
Outside::Shorter => self.min,
Outside::Longer => self.max,
};
end.expect("an end nothing is outside of")
/// Whether either end is a fraction of the rel base, so that the same
/// bound against a different one binds at a different length.
pub fn has_fraction(&self) -> bool {
[self.min, self.max]
.into_iter()
.flatten()
.any(|len| len.rel != Rel::ZERO)
}
/// This bound as lengths of the window, from lengths of a rel base that
@@ -183,6 +166,15 @@ impl From<LayoutLen> for SizeRule {
}
}
impl From<SizeRequest> for SizeRule {
fn from(request: SizeRequest) -> Self {
match request {
SizeRequest::Linear(len) => Self::Exact(len),
request => Self::Request(Arc::new(request)),
}
}
}
impl From<Option<LayoutLen>> for SizeRule {
fn from(len: Option<LayoutLen>) -> Self {
len.map_or(Self::Free, Self::Exact)
@@ -191,7 +183,7 @@ impl From<Option<LayoutLen>> for SizeRule {
/// One rule per axis, which is how a widget carries a length on one axis and
/// leaves the other to whatever it draws.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct SizeRules {
pub x: SizeRule,
pub y: SizeRule,
+2 -2
View File
@@ -128,8 +128,8 @@ impl Widgets {
}
/// The length rules whoever draws this widget applies to its box.
pub fn size_rules(&self, id: impl IdLike) -> SizeRules {
self.data(id).unwrap().size
pub fn size_rules(&self, id: impl IdLike) -> &SizeRules {
&self.data(id).unwrap().size
}
/// Sets one axis's rule. The widget is marked rather than its parent
+19 -4
View File
@@ -292,7 +292,7 @@ impl Plan {
align: None,
..self.clone()
}),
self.size.map(|_| Plan {
self.size.as_ref().map(|_| Plan {
size: None,
..self.clone()
}),
@@ -342,6 +342,21 @@ impl Plan {
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 {
if rules[axis].bound() != Bound::ANY {
rules[axis] = SizeRule::Free;
}
}
});
}
/// The same tree with `edits` applied, by the indices the generator would
/// have used for them.
///
@@ -373,7 +388,7 @@ impl Plan {
}
if plan.size.is_some() {
if let Some(lens) = edits.sizes.get(&sized) {
plan.size = Some(*lens);
plan.size = Some(lens.clone());
}
sized += 1;
}
@@ -716,7 +731,7 @@ impl Sow<'_> {
}
let idx = self.sized;
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.
@@ -852,7 +867,7 @@ impl<Rsc: UiRsc + 'static> Build<'_, Rsc> {
fn node(&mut self, plan: &Plan) -> StrongWidget {
let built = self.kind(&plan.kind);
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.tree.sized.push(id);
}
+4
View File
@@ -6,6 +6,10 @@ pub struct 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 {
for _ in 0..self.offset {
painter.next_layer();
+8
View File
@@ -30,6 +30,14 @@ impl MaxSize {
}
impl Widget for MaxSize {
fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option<RequestedLen> {
let inner = requests.widget(&self.inner, axis)?;
Some(match self.max(axis) {
Some(max) => requests.min(inner, max.into()),
None => inner,
})
}
fn draw(&mut self, painter: &mut Painter) -> Size {
let align = painter.alignment();
let mut region = UiRegion::FULL;
+4
View File
@@ -6,6 +6,10 @@ pub struct 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 {
painter
.widget_at(&self.inner, UiRegion::FULL.offset(self.amt))
+16 -2
View File
@@ -6,6 +6,10 @@ pub struct 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 {
// 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
@@ -22,11 +26,11 @@ impl Widget for Pad {
let inner = painter.widget_at(&self.inner, self.padding.region()).size();
Size {
x: LayoutLen {
px: inner.x.px + self.padding.left + self.padding.right,
px: inner.x.px + self.padding.along(Axis::X),
..inner.x
},
y: LayoutLen {
px: inner.y.px + self.padding.top + self.padding.bottom,
px: inner.y.px + self.padding.along(Axis::Y),
..inner.y
},
}
@@ -57,6 +61,16 @@ impl Padding {
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.
pub fn region_of(&self, mut region: UiRegion) -> UiRegion {
region.x.start.px += self.left;
+123 -51
View File
@@ -8,57 +8,73 @@ pub struct 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 {
painter.with_requests(|painter, lens, values| self.layout(painter, lens, values))
}
}
impl Span {
fn layout(
&self,
painter: &mut Painter,
lens: &mut Vec<RequestedLen>,
values: &mut Vec<Px>,
) -> Size {
let axis = self.dir.axis;
// The row this span lays its children out along, as a length of the
// rel base they are laid out against. Where it starts is nothing's
// business -- a slot is a length from there -- so what this reads is
// the length alone.
let row = painter.region_len(axis);
// A length for every child before their final slots are chosen: from
// a hint where one says, and from drawing otherwise. The rel base passes
// through unchanged, so `rel(0.5)` is half the area this span was
// given whatever else is in it and wherever this child sits among
// them; what a drawn child is asked in is the room left from the
// cursor, because a text has to wrap at the width actually there.
let mut cursor = Len::ZERO;
let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children {
let len = match painter.size_hint(child, axis) {
Some(len) => len,
None => {
// Across itself the child sits where its own alignment
// says, in the whole of the row: a span is what contains
// 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)
self.collect(painter, row, lens, true);
let gaps = self.gaps();
let fixed = lens
.iter()
.try_fold(Len::from_parts(Rel::ZERO, gaps), |sum, len| {
Some(sum + len.linear()?.without_leftover())
});
if let Some(fixed) = fixed
&& lens.iter().any(|len| len.has_leftover())
&& !painter.longer_than(row, fixed, axis)
{
// With no share to assign, intrinsic drawings keep the remaining
// offer, including overflow. Their answer is only moved into a slot.
self.collect(painter, row, lens, false);
}
};
cursor += len.without_leftover();
cursor.px += self.gap;
lens.push(len);
let nonlinear = lens.iter().any(|len| len.linear().is_none());
if nonlinear {
painter.allocate(lens, row - Len::from_parts(Rel::ZERO, gaps), axis, values);
}
let gaps = self
.gap
.mul_int(self.children.len().saturating_sub(1) as i32);
let total = lens.iter().fold(
let allocated = nonlinear.then(|| &values[..]);
let total = match allocated {
Some(allocated) => LayoutLen {
px: allocated.iter().fold(gaps, |sum, len| sum + *len),
..LayoutLen::ZERO
},
None => lens.iter().fold(
LayoutLen {
px: gaps,
..LayoutLen::ZERO
},
|sum, len| sum + *len,
);
// 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.
|sum, len| sum + len.linear().unwrap(),
),
};
let all_fixed = total.without_leftover();
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 has_room = any_leftover && painter.longer_than(row, all_fixed, axis);
@@ -83,17 +99,34 @@ impl Widget for Span {
false => fixed,
true => fixed + room.scale(Rel::ratio(taken, total.leftover)),
};
for (child, &len) in self.children.iter().zip(&lens) {
// A child asking for nothing but a part of what is left over,
// when nothing is, is not drawn at all. One that also asked for
// pixels or a fraction keeps those and overflows.
if len.is_only_leftover() && !has_room {
for (index, (child, request)) in self.children.iter().zip(lens.iter()).enumerate() {
// An allocated row already has a length for every child; without
// one the request is the length and the room is divided here.
// Either way a child asking for nothing but a part of what is
// left over, when nothing is, is not drawn at all -- one that
// also asked for pixels or a fraction keeps those and overflows.
let (len, shares, nothing_left) = match allocated {
Some(allocated) => {
let len = LayoutLen {
px: allocated[index],
..LayoutLen::ZERO
};
let shares = request.has_leftover();
(len, shares, shares && len.px == Px::ZERO)
}
None => {
let len = request.linear().unwrap();
let shares = len.leftover > Weight::ZERO && has_room;
(len, shares, len.is_only_leftover() && !has_room)
}
};
if nothing_left {
painter.undraw(child);
fixed.px += self.gap;
continue;
}
let from = reached(fixed, taken);
if len.leftover > Weight::ZERO && has_room {
if shares {
taken += len.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
// room is put there as it is, and one not made yet is made here.
let slot = self.slot(row, from, to);
let mut place = slot.shifted_desc().fills().on_axis(axis);
if len.leftover > Weight::ZERO && has_room {
let mut place = slot.shifted_desc().allocated().on_axis(axis);
if shares {
place = place.rel_base(axis, slot.len());
}
let used = painter.place_at(child, place).len(!axis);
@@ -125,13 +158,15 @@ impl Widget for Span {
fixed.px += self.gap;
}
// Carried whole rather than collapsed to one share: a span that sizes
// from its children does not resolve `leftover`, it passes the weight up,
// so nesting spans divides the same space rather than re-dividing a
// share of it. Four `leftover(1)` children under two spans under one span
// get a quarter each, which collapsing to `leftover(1)` per level does
// not give. Resolution happens at the nearest ancestor with a length,
// and the root always has one.
// Where nothing was allocated the weight is carried whole rather
// than collapsed to one share, so nesting spans divides the same
// space rather than re-dividing a share of it: four `leftover(1)`
// children under two spans under one span get a quarter each, which
// one share per level does not give. Resolution happens at the
// nearest ancestor with a length, and the root always has one --
// or, where a comparison deferred the row, at the ancestor whose
// allocation discovery carried these requests to, and `total` is
// pixels by the time it gets here.
let ortho = match shrinks {
true => ortho,
false => LayoutLen::rel(1.0),
@@ -141,6 +176,43 @@ impl Widget for 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
/// 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
+13
View File
@@ -8,6 +8,19 @@ pub struct 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 {
let sizing = match self.size {
StackSize::Default => None,
+4 -4
View File
@@ -59,14 +59,14 @@ widget_trait! {
}
}
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();
move |state| {
let id = self.add(state);
state
.ui_mut()
.widgets
.set_size_rule(id, Axis::X, SizeRule::Exact(len));
.set_size_rule(id, Axis::X, SizeRule::from(len));
id
}
}
@@ -115,14 +115,14 @@ widget_trait! {
}
}
fn height(self, len: impl Into<LayoutLen>) -> impl WidgetIdFn<Rsc, WL::Widget> {
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_size_rule(id, Axis::Y, SizeRule::Exact(len));
.set_size_rule(id, Axis::Y, SizeRule::from(len));
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 deferred in [false, true] {
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);
if deferred {
h.rsc
.widgets_mut()
.set_size_rule(a, Axis::X, leftover(1).clamp(20, 80).into());
}
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!("deferred={deferred}: {allocations} allocations over 100 resize frames");
assert_eq!(allocations, 0);
}
}
+391
View File
@@ -0,0 +1,391 @@
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.ui_mut().widgets.get_mut(&a).unwrap().x = Some(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 {
min: Len::rel(0.25),
max: 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 {
min: Len::rel(0.25),
max: 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));
}
+67
View File
@@ -0,0 +1,67 @@
#[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 = scenario::env("IRIS_DEFERRED_SEEDS", 20_u64);
let depth = scenario::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 {
rules[axis] = match rules[axis] {
SizeRule::Min(_) => SizeRule::Min(Len::rel(0.25)),
SizeRule::Max(_) => SizeRule::Max(Len::rel(0.75)),
SizeRule::Clamp { .. } => SizeRule::Clamp {
min: Len::rel(0.25),
max: Len::rel(0.75),
},
ref rule => rule.clone(),
};
}
}
});
});
}
+29 -4
View File
@@ -13,11 +13,12 @@
//!
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
//! `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.
use iris::harness::Harness;
use iris::prelude::*;
use iris::random::{Edits, Tree, grow};
use iris::random::{Edits, Tree, build, plan};
use std::time::Instant;
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
@@ -125,9 +126,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) {
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.frame();
println!(
@@ -209,7 +218,7 @@ fn layout_cost() {
if selected("cold") {
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);
println!(
"fixture: seed {seed}, depth {depth}, {} widgets",
@@ -276,3 +285,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());
}
+8 -3
View File
@@ -8,10 +8,11 @@
//!
//! 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
//! 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.
use iris::harness::Harness;
use iris::random::{Edits, grow};
use iris::random::{Edits, build, plan};
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
@@ -28,7 +29,11 @@ fn every_cold_layout_is_printed() {
let mut out = String::new();
for seed in 1..=seeds {
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.frame();
for (index, id) in tree.ids.iter().enumerate() {
+82 -12
View File
@@ -1,19 +1,22 @@
//! What a resize frame costs and what it holds, on a tree the revision before
//! #16 also builds.
//!
//! Deliberately written in the API subset `43ce8c7` and this branch share, so
//! 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.
//! Text-layout workloads with stable paragraphs for comparisons across revisions.
//! 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
//! provide the same workload across the full PR.
//!
//! ROWS=40 FRAMES=500 cargo test --release --test revision_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 \
//! -- --ignored --nocapture text_memory
//!
//! Wall time on this machine varies with CPU frequency; take the number from
//! `perf stat -e instructions:u` on the test binary directly.
//! `text_updates_cost` selects idle, repaint, edit, or scroll with `PHASE`.
//! 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.
use iris::harness::Harness;
use iris::prelude::*;
@@ -137,9 +140,10 @@ fn resize_cost() {
println!("paragraph {at}: {:?}", h.region(id));
}
// Two widths in turn is the friendly case for anything that remembers an
// answer, so `SWEEP=1` never repeats one -- a drag rather than a toggle.
// The sweep cycles 256 widths, avoiding the two-width cache-friendly case.
let sweep = env("SWEEP", 0_usize) != 0;
#[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take();
let mut elapsed = Vec::with_capacity(frames);
for frame in 0..frames {
let narrower = match sweep {
@@ -151,6 +155,11 @@ fn resize_cost() {
h.frame();
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());
println!(
"resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
@@ -203,3 +212,64 @@ fn text_memory() {
}
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)
);
}
+4 -3
View File
@@ -213,7 +213,7 @@ fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Siz
};
warm.rsc
.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
}
@@ -356,12 +356,13 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu
/// out by hand. A failure is a lead; the fast test that replaces it has to be
/// buildable from what the failure printed.
fn describe(id: WidgetId, h: &Harness) -> String {
let rules = h.rsc.widgets().size_rules(id);
let rules = h.rsc.widgets().size_rules(id).clone();
// A bound prints as itself: a failure is reproduced from what it printed,
// and a rule shown as "no rule" cannot be written out again.
let rule = |r: SizeRule| match r {
SizeRule::Free => "-".into(),
SizeRule::Exact(len) => format!("{len}"),
SizeRule::Request(request) => format!("{request:?}"),
SizeRule::Min(min) => format!(">{}", LayoutLen::from(min)),
SizeRule::Max(max) => format!("<{}", LayoutLen::from(max)),
SizeRule::Clamp { min, max } => {
@@ -383,7 +384,7 @@ fn describe(id: WidgetId, h: &Harness) -> String {
// A rule and an alignment are properties of whatever carries them, so
// they print with that widget rather than as widgets of their own.
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));
}
if align != RegionAlign::default() {
+3
View File
@@ -32,3 +32,6 @@ mod tasks;
mod text_edit;
#[path = "cases/unsettled.rs"]
mod unsettled;
#[path = "cases/deferred.rs"]
mod deferred;