From 8780b40bb79a47271ed509455faf62557399af3e Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Sun, 20 Sep 2026 16:50:18 -0400 Subject: [PATCH] Resolve deferred size comparisons before allocating span slots --- core/src/ui/active.rs | 11 + core/src/ui/mod.rs | 2 +- core/src/ui/painter.rs | 173 ++++++++++++- core/src/ui/place.rs | 30 ++- core/src/ui/render_state.rs | 128 ++++++++-- core/src/widget/mod.rs | 8 + core/src/widget/request.rs | 438 ++++++++++++++++++++++++++++++++ core/src/widget/size_rule.rs | 48 ++-- core/src/widget/widgets.rs | 2 +- src/random.rs | 8 +- src/widget/position/layer.rs | 4 + src/widget/position/max_size.rs | 8 + src/widget/position/offset.rs | 4 + src/widget/position/pad.rs | 8 + src/widget/position/span.rs | 158 ++++++++---- src/widget/position/stack.rs | 10 + src/widget/trait_fns.rs | 8 +- tests/allocation_cost.rs | 80 ++++++ tests/cases/deferred.rs | 381 +++++++++++++++++++++++++++ tests/deferred_generated.rs | 67 +++++ tests/layout_dump.rs | 17 +- tests/scenario/mod.rs | 5 +- tests/suite.rs | 3 + 23 files changed, 1471 insertions(+), 130 deletions(-) create mode 100644 core/src/widget/request.rs create mode 100644 tests/allocation_cost.rs create mode 100644 tests/cases/deferred.rs create mode 100644 tests/deferred_generated.rs diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 3f6f572..af2babf 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -51,6 +51,8 @@ pub struct ActiveData { /// An owned mask holds one reference independently of its primitives. pub mask_region: Option, pub children: Vec, + pub request_deps: Vec, + 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, + pub size_deps: Vec, + pub under: Vec<(WidgetId, LayoutHolds)>, + pub requests: Vec, + pub lengths: Vec, +} diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index d6767da..e45003c 100644 --- a/core/src/ui/mod.rs +++ b/core/src/ui/mod.rs @@ -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)] diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 51de0d3..9efaad5 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -2,8 +2,9 @@ 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, + Rel, RenderedText, RequestArena, RequestedLen, RetainedPrimitive, Size, SizeRequests, + 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, /// The children whose size this widget read while drawing. pub(super) size_deps: Vec, + pub(super) request_deps: Vec, + pub(super) scratch: crate::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,106 @@ pub struct Painter<'a> { } impl<'a> Painter<'a> { + /// Reuses this widget's allocation buffers across draws. Nested painters + /// have independent buffers, so discovering a child cannot overwrite them. + pub fn with_requests( + &mut self, + f: impl FnOnce(&mut Self, &mut Vec, &mut Vec) -> 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( + &mut self, + child: &StrongWidget, + axis: Axis, + ) -> Option { + self.request_deps.push(child.id()); + if self.rsc.widgets().size_rules(child.id())[axis].bound() == crate::Bound::ANY + && let Some(len) = self.size_hint(child, axis) + { + return Some(len.into()); + } + let rel_base = self.rel_base(axis); + let mut requests = SizeRequests { + arena: &mut self.state.requests, + measured: None, + widgets: self.rsc.widgets(), + dependencies: &mut self.request_deps, + rel_base, + }; + let request = requests.widget(child, axis)?; + // An intrinsic fixed answer still draws in the offered room and is + // moved afterwards. Only a declaration or a share chooses its ask box. + (request.has_leftover() + || matches!( + self.rsc.widgets().size_rules(child.id())[axis], + crate::SizeRule::Request(_) + )) + .then_some(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( + &mut self, + child: &StrongWidget, + axis: Axis, + len: LayoutLen, + ) -> RequestedLen { + let rel_base = self.rel_base(axis); + 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, + }; + match requests.widget(child, axis) { + Some(request) if request.linear().is_none() && request.has_leftover() => request, + _ if len.leftover > Weight::ZERO => requests.bounded(len.into(), bound), + _ => len.into(), + } + } + + /// A deferred comparison reads this window when the allocation is solved. + pub fn allocate( + &mut self, + requests: &[RequestedLen], + room: Len, + axis: Axis, + output: &mut Vec, + ) { + 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), + ) + } + + 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(&mut self, primitive: P, region: UiRegion) { let kind = self.rsc.ui_mut().primitives.kind::

(); self.write(kind, primitive, region); @@ -195,9 +298,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 +531,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], + crate::SizeRule::Exact(_) | crate::SizeRule::Request(_) + ) } /// This widget's own box in pixels. Reading it makes the drawing one @@ -667,6 +777,9 @@ 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 { + if matches!(self.size_rules(id)[axis], crate::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 +819,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 +835,7 @@ impl Placing { pub(super) fn ask( &self, widgets: &Widgets, + requests: &mut RequestArena, window: PxVec2, id: WidgetId, mut place: PlaceDesc, @@ -726,10 +843,33 @@ 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 crate::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 len = if place[axis].fit == crate::PlaceFit::Allocated { + offer + } else { + let request = requests.import(request, base); + let len = requests + .allocate(&[request], offer.to_px(window[axis]), window[axis]) + .next() + .unwrap(); + holds[axis].window = Holds::at(window[axis]); + Len::from_parts(Rel::ZERO, len) + }; + let len = Len::from_parts(Rel::ZERO, len.to_px(window[axis])); + 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 +888,15 @@ 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.min, bound.max] + .into_iter() + .flatten() + .any(|len| len.rel != Rel::ZERO) + { + 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 +907,7 @@ impl Placing { declared, bounds, holds, + inputs, } } @@ -785,7 +934,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 +987,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]); diff --git a/core/src/ui/place.rs b/core/src/ui/place.rs index 465e386..2502ae2 100644 --- a/core/src/ui/place.rs +++ b/core/src/ui/place.rs @@ -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 { + 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), } } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 8558cb1..f3abcdd 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -94,7 +94,10 @@ pub struct UiRenderState { deferred: crate::util::HashSet, /// 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, + request_readers: HashMap>, 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,19 @@ 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(), + }; + 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 +363,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 +407,10 @@ impl UiRenderState { own, answer_under, children, - size_deps, - under, + mut size_deps, + request_deps, + mut scratch, + mut under, move_idx, layer, own_layer: _, @@ -402,14 +430,19 @@ impl UiRenderState { // 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() { - None => reported, - Some(len) if len.leftover == Weight::ZERO => LayoutLen { - rel: info.rel_base[axis].rel, - px: info.rel_base[axis].px, - leftover: Weight::ZERO, - }, - Some(len) => len.within_len(info.rel_base[axis]), + 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, + px: info.rel_base[axis].px, + leftover: Weight::ZERO, + }, + Some(len) => len.within_len(info.rel_base[axis]), + } }; let mut size = Size { x: ruled(Axis::X, size.x), @@ -483,8 +516,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 +553,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 +581,8 @@ impl UiRenderState { primitives, mask_region, children, + request_deps, + scratch, declared: info.declared, bounds: info.bounds, own_align: rsc.widgets().alignment(id), @@ -585,6 +629,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 +886,11 @@ impl UiRenderState { fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option { 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 +964,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 +979,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 +1000,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 +1008,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 +1031,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 + // 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. 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 +1056,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 +1073,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 +1180,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; diff --git a/core/src/widget/mod.rs b/core/src/widget/mod.rs index 8d4637e..a84be80 100644 --- a/core/src/widget/mod.rs +++ b/core/src/widget/mod.rs @@ -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 { + 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. diff --git a/core/src/widget/request.rs b/core/src/widget/request.rs new file mode 100644 index 0000000..397e196 --- /dev/null +++ b/core/src/widget/request.rs @@ -0,0 +1,438 @@ +use crate::{Axis, LayoutLen, Len, Px, StrongWidget, Weight, WidgetId, Widgets}; + +impl From for SizeRequest { + fn from(value: N) -> Self { + LayoutLen::px(value).into() + } +} + +impl LayoutLen { + pub fn min(self, other: impl Into) -> SizeRequest { + SizeRequest::from(self).min(other) + } + + pub fn max(self, other: impl Into) -> SizeRequest { + SizeRequest::from(self).max(other) + } + + pub fn clamp(self, min: impl Into, max: impl Into) -> 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(std::sync::Arc<(Self, Self)>), + Min(std::sync::Arc<(Self, Self)>), + Max(std::sync::Arc<(Self, Self)>), +} + +impl From for SizeRequest { + fn from(len: LayoutLen) -> Self { + Self::Linear(len) + } +} + +impl From for SizeRequest { + fn from(len: Len) -> Self { + LayoutLen::from(len).into() + } +} + +impl SizeRequest { + pub fn min(self, other: impl Into) -> 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(std::sync::Arc::new((self, other))) + } + } + + pub fn max(self, other: impl Into) -> 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(std::sync::Arc::new((self, other))) + } + } + + pub fn clamp(self, min: impl Into, max: impl Into) -> 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(std::sync::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 for RequestedLen { + fn from(len: LayoutLen) -> Self { + Self(RequestValue::Linear(len)) + } +} +impl From for RequestedLen { + fn from(len: Len) -> Self { + LayoutLen::from(len).into() + } +} +impl RequestedLen { + pub fn linear(self) -> Option { + 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, + 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) => { + 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 + } + } + } + /// Allocates one scope of nonnegative shares. Floors can overflow; caps + /// can leave unused room. Prefix rounding keeps adjacent slot edges equal. + pub(crate) fn allocate<'a>( + &'a self, + requests: &'a [RequestedLen], + room: Px, + window: Px, + ) -> impl Iterator + '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); + 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 { + assert_ne!(den, 0); + if den < 0 { + Self { + num: -num, + den: -den, + } + } else { + Self { num, den } + } + } +} + +impl Ord for Ratio { + fn cmp(&self, other: &Self) -> std::cmp::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 { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Copy)] +struct Segment { + fixed: i64, + weight: i64, + end: Option, +} + +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, b: Option) -> Option { + 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 crate::util::HashMap>, + pub(crate) widgets: &'a Widgets, + pub(crate) dependencies: &'a mut Vec, + 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( + &mut self, + child: &StrongWidget, + axis: Axis, + ) -> Option { + self.dependencies.push(child.id()); + let rules = self.widgets.size_rules(child.id()); + let rule = &rules[axis]; + if let crate::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: crate::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( + &mut self, + child: &StrongWidget, + axis: Axis, + padding: Px, + ) -> Option { + 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(crate::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 { + 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 + } +} diff --git a/core/src/widget/size_rule.rs b/core/src/widget/size_rule.rs index 0caab28..f4d1534 100644 --- a/core/src/widget/size_rule.rs +++ b/core/src/widget/size_rule.rs @@ -1,5 +1,5 @@ use crate::util::impl_axis_index; -use crate::{Axis, LayoutLen, Len, Rel}; +use crate::{Axis, LayoutLen, Len, Rel, SizeRequest}; /// 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 +10,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(std::sync::Arc), /// At least this long, and otherwise whatever the box gives it. Min(Len), /// At most this long. @@ -33,7 +34,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, @@ -66,10 +67,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 +78,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,20 +97,18 @@ impl SizeRule { pub fn exact(&self) -> Option { 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, @@ -183,6 +178,15 @@ impl From for SizeRule { } } +impl From for SizeRule { + fn from(request: SizeRequest) -> Self { + match request { + SizeRequest::Linear(len) => Self::Exact(len), + request => Self::Request(std::sync::Arc::new(request)), + } + } +} + impl From> for SizeRule { fn from(len: Option) -> Self { len.map_or(Self::Free, Self::Exact) @@ -191,7 +195,7 @@ impl From> 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, diff --git a/core/src/widget/widgets.rs b/core/src/widget/widgets.rs index 9ee6bf2..b5b8916 100644 --- a/core/src/widget/widgets.rs +++ b/core/src/widget/widgets.rs @@ -129,7 +129,7 @@ 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 + self.data(id).unwrap().size.clone() } /// Sets one axis's rule. The widget is marked rather than its parent diff --git a/src/random.rs b/src/random.rs index 7b52123..88f9852 100644 --- a/src/random.rs +++ b/src/random.rs @@ -292,7 +292,7 @@ impl Plan { align: None, ..self.clone() }), - self.size.map(|_| Plan { + self.size.as_ref().map(|_| Plan { size: None, ..self.clone() }), @@ -373,7 +373,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 +716,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 +852,7 @@ impl 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); } diff --git a/src/widget/position/layer.rs b/src/widget/position/layer.rs index 6b111a4..af8b9fd 100644 --- a/src/widget/position/layer.rs +++ b/src/widget/position/layer.rs @@ -6,6 +6,10 @@ pub struct LayerOffset { } impl Widget for LayerOffset { + fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option { + requests.widget(&self.inner, axis) + } + fn draw(&mut self, painter: &mut Painter) -> Size { for _ in 0..self.offset { painter.next_layer(); diff --git a/src/widget/position/max_size.rs b/src/widget/position/max_size.rs index 07250bf..aa56d0e 100644 --- a/src/widget/position/max_size.rs +++ b/src/widget/position/max_size.rs @@ -30,6 +30,14 @@ impl MaxSize { } impl Widget for MaxSize { + fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option { + 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; diff --git a/src/widget/position/offset.rs b/src/widget/position/offset.rs index c899ec1..1a01f99 100644 --- a/src/widget/position/offset.rs +++ b/src/widget/position/offset.rs @@ -6,6 +6,10 @@ pub struct Offset { } impl Widget for Offset { + fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option { + requests.widget(&self.inner, axis) + } + fn draw(&mut self, painter: &mut Painter) -> Size { painter .widget_at(&self.inner, UiRegion::FULL.offset(self.amt)) diff --git a/src/widget/position/pad.rs b/src/widget/position/pad.rs index d180e99..376f00b 100644 --- a/src/widget/position/pad.rs +++ b/src/widget/position/pad.rs @@ -6,6 +6,14 @@ pub struct Pad { } impl Widget for Pad { + fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option { + let padding = match axis { + Axis::X => self.padding.left + self.padding.right, + Axis::Y => self.padding.top + self.padding.bottom, + }; + requests.inset(&self.inner, axis, padding) + } + 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 diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 727caec..db0771f 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -8,57 +8,79 @@ pub struct Span { } impl Widget for Span { + fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option { + 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.gap + .mul_int(self.children.len().saturating_sub(1) as i32), + )); + 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, + values: &mut Vec, + ) -> 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) - } - }; - cursor += len.without_leftover(); - cursor.px += self.gap; - lens.push(len); - } - + self.collect(painter, row, lens, true); let gaps = self .gap .mul_int(self.children.len().saturating_sub(1) as i32); - let total = lens.iter().fold( - LayoutLen { - px: 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); + } + 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 allocated = nonlinear.then_some(&values); + let total = match &allocated { + Some(allocated) => LayoutLen { + px: allocated.iter().fold(gaps, |sum, len| sum + *len), ..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. + None => lens.iter().fold( + LayoutLen { + px: gaps, + ..LayoutLen::ZERO + }, + |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 +105,30 @@ impl Widget for Span { false => fixed, 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() { + let len = match &allocated { + Some(allocated) => LayoutLen { + px: allocated[index], + ..LayoutLen::ZERO + }, + None => request.linear().unwrap(), + }; + let shares = match &allocated { + Some(_) => request.has_leftover(), + None => len.leftover > Weight::ZERO && has_room, + }; // 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 { + if (len.is_only_leftover() && !has_room) + || (allocated.is_some() && shares && len.px == Px::ZERO) + { 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 +141,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 +160,8 @@ 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. + // Discovery carries nested requests to the allocating ancestor. The + // draw still returns an ordinary Size for callers measuring content. let ortho = match shrinks { true => ortho, false => LayoutLen::rel(1.0), @@ -141,6 +171,36 @@ impl Widget for Span { } impl Span { + fn collect( + &self, + painter: &mut Painter, + row: Len, + lens: &mut Vec, + 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 diff --git a/src/widget/position/stack.rs b/src/widget/position/stack.rs index 4e5639f..bf918ee 100644 --- a/src/widget/position/stack.rs +++ b/src/widget/position/stack.rs @@ -8,6 +8,16 @@ pub struct Stack { } impl Widget for Stack { + fn size_request(&self, requests: &mut SizeRequests, axis: Axis) -> Option { + match self.size { + StackSize::Default => Some(LayoutLen::LEFTOVER.into()), + StackSize::Child(i) => match self.children.get(i) { + 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, diff --git a/src/widget/trait_fns.rs b/src/widget/trait_fns.rs index 3b741f5..ab42dea 100644 --- a/src/widget/trait_fns.rs +++ b/src/widget/trait_fns.rs @@ -59,14 +59,14 @@ widget_trait! { } } - fn width(self, len: impl Into) -> impl WidgetIdFn { + fn width(self, len: impl Into) -> impl WidgetIdFn { 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) -> impl WidgetIdFn { + fn height(self, len: impl Into) -> impl WidgetIdFn { 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 } } diff --git a/tests/allocation_cost.rs b/tests/allocation_cost.rs new file mode 100644 index 0000000..73e5ea6 --- /dev/null +++ b/tests/allocation_cost.rs @@ -0,0 +1,80 @@ +use iris::{harness::Harness, prelude::*}; +use std::{ + alloc::{GlobalAlloc, Layout, System}, + cell::Cell, +}; + +struct Counting; +thread_local! { + static COUNT: Cell> = 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 = 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); + } +} diff --git a/tests/cases/deferred.rs b/tests/cases/deferred.rs new file mode 100644 index 0000000..89ec25b --- /dev/null +++ b/tests/cases/deferred.rs @@ -0,0 +1,381 @@ +use std::{cell::Cell, rc::Rc}; + +use iris::harness::{Harness, assert_corners}; +use iris::prelude::*; + +struct Counted { + draws: Rc>, +} + +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 { + 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() { + let mut h = Harness::new((300, 100)); + let a = rect(Color::RED).add(&mut h.rsc); + let b = rect(Color::BLUE).add(&mut h.rsc); + h.set_root((a, b).span(Dir::RIGHT)); + h.resize((400, 100)); + h.frame(); + h.rsc + .widgets_mut() + .set_size_rule(a, Axis::X, SizeRule::Max(Len::px(80.0))); + h.frame(); + assert_corners!(h, a, (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 = + 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 { + 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)); +} diff --git a/tests/deferred_generated.rs b/tests/deferred_generated.rs new file mode 100644 index 0000000..961d64c --- /dev/null +++ b/tests/deferred_generated.rs @@ -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(), + }; + } + } + }); + }); +} diff --git a/tests/layout_dump.rs b/tests/layout_dump.rs index 3671afe..fc9a01b 100644 --- a/tests/layout_dump.rs +++ b/tests/layout_dump.rs @@ -11,7 +11,8 @@ //! it is not drawn. use iris::harness::Harness; -use iris::random::{Edits, grow}; +use iris::prelude::{Axis, Bound, SizeRule}; +use iris::random::{Edits, build, plan}; fn env(name: &str, fallback: T) -> T { std::env::var(name) @@ -28,7 +29,19 @@ 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 std::env::var_os("IRIS_DUMP_UNBOUNDED").is_some() { + plan.walk_mut(&mut |node| { + if let Some(rules) = &mut node.size { + for axis in Axis::BOTH { + if rules[axis].bound() != Bound::ANY { + rules[axis] = SizeRule::Free; + } + } + } + }); + } + let (root, tree) = build(&mut harness.rsc, &plan); harness.state.root = Some(root); harness.frame(); for (index, id) in tree.ids.iter().enumerate() { diff --git a/tests/scenario/mod.rs b/tests/scenario/mod.rs index 5c51a14..549d192 100644 --- a/tests/scenario/mod.rs +++ b/tests/scenario/mod.rs @@ -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 } @@ -362,6 +362,7 @@ fn describe(id: WidgetId, h: &Harness) -> String { 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() { diff --git a/tests/suite.rs b/tests/suite.rs index d98c6d4..2be9395 100644 --- a/tests/suite.rs +++ b/tests/suite.rs @@ -32,3 +32,6 @@ mod tasks; mod text_edit; #[path = "cases/unsettled.rs"] mod unsettled; + +#[path = "cases/deferred.rs"] +mod deferred;