diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index a485232..ba56da4 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -3,8 +3,8 @@ use crate::layout_diagnostics::{self as diag, Counter}; use crate::{ 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, + RetainedPrimitive, Size, SizeRequests, StrongWidget, TextAttrs, TextBuffer, TextureHandle, + UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets, render::{ GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind, TexturePrimitive, @@ -88,9 +88,7 @@ impl<'a> Painter<'a> { child: &StrongWidget, axis: Axis, ) -> Option { - if self.rsc.widgets().size_rules(child.id())[axis].bound() == Bound::ANY - && let Some(len) = self.size_hint(child, axis) - { + if let Some(len) = self.size_hint(child, axis) { self.request_deps.push(child.id()); return Some(len.into()); } @@ -106,10 +104,9 @@ impl<'a> Painter<'a> { // 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(_) - ) + || self.rsc.widgets().size_rules(child.id())[axis] + .request + .is_some() }); if request.is_some() { self.rel_base(axis); @@ -130,7 +127,7 @@ impl<'a> Painter<'a> { len: LayoutLen, ) -> RequestedLen { let start = self.request_deps.len(); - let bound = self.rsc.widgets().size_rules(child.id())[axis].bound(); + 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), @@ -442,6 +439,9 @@ impl<'a> Painter<'a> { /// against this widget's rel base, which is the rel base a child asked with /// nothing narrowed gets. Asking counts as reading its size. pub fn size_hint(&mut self, id: &StrongWidget, axis: Axis) -> Option { + if self.rsc.widgets().size_rules(id.id())[axis].bound != Bound::ANY { + return None; + } let hint = self.rsc.widgets().exact_len(id.id(), axis); let rel_base = self.rel_base[axis]; let resolved = hint.map(|hint| hint.within_len(rel_base)); @@ -559,10 +559,9 @@ 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 { - matches!( - self.rsc.widgets().size_rules(self.id)[axis], - SizeRule::Exact(_) | SizeRule::Request(_) - ) + self.rsc.widgets().size_rules(self.id)[axis] + .request + .is_some() } /// This widget's own box in pixels. Reading it makes the drawing one @@ -809,10 +808,11 @@ impl Widgets { // 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(_)) { + let rule = &self.size_rules(id)[axis]; + if rule.deferred().is_some() { return None; } - self.size_rules(id)[axis].exact().or_else(|| { + rule.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 // size rather than the whole offer. That is the offer's business @@ -821,12 +821,6 @@ impl Widgets { self.get_dyn(id)?.size_hint(axis) }) } - - /// What a widget's box is where a rule or its own hint gives one outright, - /// rather than a share for whoever draws it to divide. - pub(super) fn declared_lens(&self, id: WidgetId) -> Declared { - Declared::from_axes(|axis| self.exact_len(id, axis)?.declared()) - } } /// One ask of a widget: the box it draws in, what its fractions are of, and @@ -838,9 +832,8 @@ pub(super) struct Ask { /// past the box its parent offered. pub place: PlaceDesc, /// What the widget's box is on each axis where something says so - /// outright: its rule or its hint, or a bound of its own that the box it - /// was offered falls outside -- a bound that binds is a declaration, and - /// the same one the widget answers with. + /// outright: its rule, its hint, or a bound the offer fell outside. + /// An intrinsic answer can still occupy less than a capped box. pub declared: Declared, /// Its bounds, resolved against the rel base its rules were resolved /// against, for the answer to be held to where the box was not. @@ -876,11 +869,18 @@ impl Placing { let rules = widgets.size_rules(id); let mut holds = LayoutHolds::ANY; let mut inputs = LayoutHolds::ANY; - let mut declared = widgets.declared_lens(id); + let mut declared = Declared::NONE; 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] { + let stated = widgets.exact_len(id, axis); + if let Some(len) = stated.and_then(|len| len.declared()) { + if len.rel != Rel::ZERO { + inputs[axis].rel_base = Some(self.rel_base[axis]); + } + declared[axis] = Some(len.within_len(base)); + } + if let Some(request) = rules[axis].deferred() { inputs[axis].rel_base = Some(self.rel_base[axis]); inputs[axis].region_len = Some(self.region[axis].len()); inputs[axis].window = Holds::at(window[axis]); @@ -889,6 +889,7 @@ impl Placing { offer.to_px(window[axis]) } else { let request = requests.import(request, base); + let request = requests.bounded(request, rules[axis].bound.within_len(base)); holds[axis].window = Holds::at(window[axis]); requests .allocate(&[request], offer.to_px(window[axis]), window[axis]) @@ -906,24 +907,38 @@ impl Placing { // a span follows with one child. Only the overflow is a box of // the widget's own: a share that fits is the box it was given, // which is what this place already says. - let (share, kept) = - self.share_past_the_offer(widgets, window[axis], id, place, align, axis); + let (share, kept) = self.share_past_the_offer(stated, window[axis], place, align, axis); holds[axis].window = holds[axis].window.and(kept); if let Some(len) = share { place[axis] = len.as_desc().fills(); } - // A bound holds what the widget answers, not the box it is asked - // in: the box it is given is whoever asked's to decide, and a - // rule that read it would be decided again by every path that - // hands the widget a box -- including the ones that never ask it - // 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. - let bound = rules[axis].bound(); - if bound.has_fraction() { - inputs[axis].rel_base = Some(self.rel_base[axis]); + let bound = rules[axis].bound; + if bound != Bound::ANY { + if bound.has_fraction() { + inputs[axis].rel_base = Some(self.rel_base[axis]); + } + // A solved slot may narrow the widget's rel base, but the + // allocator evaluated its bounds against this parent's base. + let bound_base = if place[axis].fit == PlaceFit::Allocated { + self.rel_base[axis] + } else { + base + }; + bounds[axis] = bound.within_len(bound_base); + let offer = + declared[axis].unwrap_or_else(|| place.of(self.region, align)[axis].len()); + let (held, kept) = bounds[axis].outside(offer, window[axis]); + holds[axis].window = holds[axis].window.and(kept); + // The comparison reads the incoming box, before a bound + // replaces it. Placement keeps that decision; only an ask + // may compare a new offer. + if declared[axis].is_none() { + inputs[axis].region_len = Some(self.region[axis].len()); + } + if let Some(len) = held { + declared[axis] = Some(len); + } } - bounds[axis] = bound.within_len(base); } let (rel_base, region) = place.rel_base_and_region(self.region, self.rel_base, declared, align); @@ -950,9 +965,8 @@ impl Placing { /// crossing between them is a question in pixels. fn share_past_the_offer( &self, - widgets: &Widgets, + stated: Option, window: Px, - id: WidgetId, place: PlaceDesc, align: RegionAlign, axis: Axis, @@ -966,7 +980,7 @@ impl Placing { } // A share with nothing beside it is the box whatever the box is, so // there is no comparison to make and no range to keep for one. - let Some(stated) = widgets.exact_len(id, axis) else { + let Some(stated) = stated else { return (None, Holds::ANY); }; if stated.leftover == Weight::ZERO || stated.is_only_leftover() { @@ -985,12 +999,10 @@ impl LayoutLen { /// Whether what a widget reported along an axis is the whole of the box /// it is in rather than a part to be placed inside it. A share fills, /// because a share is a length only to whoever divides one, and whoever - /// did is the one that handed down this box. A declared axis does too: - /// the rule already gave the region its length, and the rule's length is - /// what the widget reports there. And an axis the parent decided from - /// the answer is the answer already. - pub(super) fn fills(&self, declared: Option, decided: bool) -> bool { - self.leftover != Weight::ZERO || declared.is_some() || decided + /// did is the one that handed down this box. An axis the parent decided + /// from the answer is the answer already. + pub(super) fn fills(&self, decided: bool) -> bool { + self.leftover != Weight::ZERO || decided } } @@ -1004,17 +1016,11 @@ impl PlaceDesc { /// part. That is what makes a fraction the same fraction wherever the part /// it is placed in sits and however long it is -- the fraction is resolved /// once, here, against the rel base it was reported of. - pub(super) fn placement( - self, - region: UiRegion, - size: Size, - declared: Declared, - align: RegionAlign, - ) -> UiRegion { + pub(super) fn placement(self, region: UiRegion, size: Size, align: RegionAlign) -> UiRegion { let mut placed = region; for axis in Axis::BOTH { let reported = size[axis]; - if reported.fills(declared[axis], self[axis].fit.fills()) { + if reported.fills(self[axis].fit.fills()) { continue; } placed[axis] = placed[axis].place(reported.without_leftover(), align[axis]); @@ -1027,11 +1033,8 @@ impl PlaceDesc { /// /// `own` is that widget's own box, and `place` what of it the child is /// given, including any rel base it states -- a row's slot, or padding's rel - /// base less its pixels. That is a window length, like every other length - /// here, since a slot of a row is not a fraction of anything the row can - /// name. The child's declaration is a fraction of whichever reached it, and - /// is the only one that also places the box: a box the caller decided is - /// what `place` names. + /// base less its pixels. The ask has already resolved declarations into + /// window lengths, so placement only aligns them inside the given box. pub(super) fn rel_base_and_region( self, own: UiRegion, @@ -1044,9 +1047,7 @@ impl PlaceDesc { let mut region = given; for axis in Axis::BOTH { let base = self.base(axis, parent_rel_base); - let len = declared[axis] - .map(|len| len.within_len(base)) - .unwrap_or(base); + let len = declared[axis].unwrap_or(base); rel_base[axis] = len; if declared[axis].is_some() { region[axis] = given[axis].place(len, align[axis]); diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 767573e..38f8290 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -254,7 +254,6 @@ impl UiRenderState { ); } let align = rsc.widgets().alignment(id); - let declared = info.declared; // Nothing this widget measured can be dirty while it draws: layout is // one bottom-up walk, so anything deeper has settled or deferred to // its own parent, and a deferred one leaves that parent marked. @@ -268,7 +267,7 @@ impl UiRenderState { .then(|| self.retained_answer(id, region, info)) .flatten() .and_then(|answer| { - let placed = info.placed.placement(region, answer.size, declared, align); + let placed = info.placed.placement(region, answer.size, align); self.try_reuse(id, region, placed, info, rsc) .then_some(answer) }); @@ -280,7 +279,7 @@ impl UiRenderState { // Where the drawing goes: the part its parent gave it, with the // answer placed inside that part on any axis the parent left // open. - let placed = info.placed.placement(region, answer.size, declared, align); + let placed = info.placed.placement(region, answer.size, align); if placed != region { self.relocate(id, placed, info, rsc); } @@ -379,10 +378,7 @@ impl UiRenderState { 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, + own: LayoutHolds::ANY, under, answer_under: LayoutHolds::ANY, depth: info.depth, @@ -437,9 +433,9 @@ 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).clone(); + let rules = rsc.widgets().size_rules(id); let ruled = |axis: Axis, reported: LayoutLen| { - if matches!(rules[axis], crate::SizeRule::Request(_)) { + if rules[axis].deferred().is_some() { return info.rel_base[axis].into(); } match rules[axis].exact() { @@ -470,18 +466,30 @@ impl UiRenderState { // it is a length only to whoever divides one, and the box that // divider gives is a box this widget is asked in, where the bound is // applied to it. - let mut bounded = LayoutHolds::ANY; + // Widgets may widen their own read ranges, but not the ask's constraints. + let mut own_holds = own.and(info.ask_holds); for axis in Axis::BOTH { let answer = size[axis]; if answer.leftover != Weight::ZERO { continue; } let (held, kept) = info.bounds[axis].outside(answer.without_leftover(), window[axis]); - bounded[axis].window = kept; + own_holds[axis].window = own_holds[axis].window.and(kept); if let Some(held) = held { size[axis] = held.into(); } } + // A rule that is a fraction of the rel base is answered with the + // rel base's own length, so the answer is that rel base's and not just + // that many pixels of this window -- the same pin a widget that read + // its rel base took for its drawing. A bound counts: which side of it + // the box fell was decided against this rel base, and the same box of + // a different one can fall on the other. + for axis in Axis::BOTH { + if rules[axis].has_fraction() { + own_holds[axis].rel_base = Some(info.rel_base[axis]); + } + } // A widget that clipped its contents to its box drew nothing outside // it, so reporting more than the box asks to be placed at a length it // does not occupy -- and its parent would place the part it cut off. @@ -509,18 +517,6 @@ impl UiRenderState { if let Some(idx) = retired_move { self.moves.remove(idx); } - // A rule that is a fraction of the rel base is answered with the - // rel base's own length, so the answer is that rel base's and not just - // that many pixels of this window -- the same pin a widget that read - // its rel base took for its drawing. A bound counts: which side of it - // the box fell was decided against this rel base, and the same box of - // a different one can fall on the other. - let mut own_holds = own.and(bounded); - for axis in Axis::BOTH { - if rules[axis].has_fraction() { - own_holds[axis].rel_base = Some(info.rel_base[axis]); - } - } let answer_holds = own_holds.and(answer_under); let holds = under .iter() @@ -798,7 +794,6 @@ impl UiRenderState { let placed = place.placement( region, active.measured().unwrap_or(active.size), - active.declared, active.own_align, ); let info = DrawInfo { @@ -823,8 +818,8 @@ impl UiRenderState { /// The rel base and the box a widget already drawn is given at `place` of /// the box its parent is being taken as. What narrowed its rel base and what - /// it declared are its own record's, so both are resolved against that - /// parent's rel base again exactly as the first ask resolved them. + /// it declared are its own record's. Declared lengths stay as the ask + /// resolved them; the destination only decides where they sit. fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) { place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align) } @@ -1195,10 +1190,11 @@ impl UiRenderState { active.asked, ); let active = &self.active[&id]; - let declared_changed = ask.declared != active.declared; + // Even an inactive bound changes what the parent must track about its offer. + let constraints_changed = ask.declared != active.declared || ask.bounds != active.bounds; let alignment_changed = rsc.widgets().alignment(id) != active.own_align; if let Some(parent) = active.parent - && (declared_changed + && (constraints_changed || alignment_changed || active.re_asked || !active.drawn diff --git a/core/src/widget/request.rs b/core/src/widget/request.rs index 53e3f6d..ec9c96a 100644 --- a/core/src/widget/request.rs +++ b/core/src/widget/request.rs @@ -1,6 +1,6 @@ use crate::{ - ActiveData, Axis, Bound, LayoutLen, Len, Px, Rel, SizeRule, StrongWidget, UiNum, Weight, - WidgetId, Widgets, util::HashMap, + ActiveData, Axis, Bound, LayoutLen, Len, Px, Rel, StrongWidget, UiNum, Weight, WidgetId, + Widgets, util::HashMap, }; use std::{cmp::Ordering, sync::Arc}; @@ -169,6 +169,17 @@ impl RequestArena { let b = self.import(&pair.1, base); self.combine(op, a, b) } + pub(crate) fn bounded(&mut self, request: RequestedLen, bound: Bound) -> RequestedLen { + let request = match bound.min { + Some(min) => self.combine(Op::Max, request, min.into()), + None => request, + }; + match bound.max { + Some(max) => self.combine(Op::Min, request, max.into()), + None => request, + } + } + fn combine(&mut self, op: Op, a: RequestedLen, b: RequestedLen) -> RequestedLen { if let (Some(x), Some(y)) = (a.linear(), b.linear()) { if matches!(op, Op::Sum) { @@ -391,32 +402,23 @@ impl SizeRequests<'_> { 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())) + let request = match &rule.request { + Some(request) => self.arena.import(request, self.rel_base), + None => { + let widget = self.widgets.get_dyn(child.id())?; + widget.size_request(self, axis).or_else(|| { + self.measured? + .get(&child.id())? + .measured() + .map(|size| size[axis].into()) + })? + } + }; + Some(self.bounded(request, rule.bound)) } pub(crate) fn bounded(&mut self, request: RequestedLen, bound: Bound) -> RequestedLen { - 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, - } + self.arena.bounded(request, bound.within_len(self.rel_base)) } pub fn length(&self, len: LayoutLen) -> RequestedLen { diff --git a/core/src/widget/size_rule.rs b/core/src/widget/size_rule.rs index e5dea47..c156933 100644 --- a/core/src/widget/size_rule.rs +++ b/core/src/widget/size_rule.rs @@ -1,108 +1,80 @@ use crate::util::impl_axis_index; 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. -/// -/// A rule and a drawn size are not two opinions to reconcile: a rule wins on -/// the axis it names, and the `Size` returned by `draw` answers only the axes -/// with no rule. That is what lets a span divide its space around a length -/// 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. -/// -/// Exact expressions can bound a share before allocation. Bounds on an -/// intrinsic answer are applied after that answer becomes known. +/// A preferred length and independent bounds on one axis. Without a +/// request, the widget's drawing supplies the preferred length. #[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), - /// At least this long, and otherwise whatever the box gives it. - Min(Len), - /// At most this long. - Max(Len), - /// Between the two. - Clamp { min: Len, max: Len }, +pub struct SizeRule { + pub request: Option, + pub bound: Bound, } impl SizeRule { - /// What this rule allows the length to be where it does not give one - /// outright. - pub fn bound(&self) -> Bound { - match *self { - Self::Free | Self::Exact(_) | Self::Request(_) => Bound::ANY, - Self::Min(min) => Bound { - min: Some(min), - max: None, - }, - Self::Max(max) => Bound { - min: None, - max: Some(max), - }, - Self::Clamp { min, max } => Bound { - min: Some(min), - max: Some(max), - }, - } + pub const FREE: Self = Self { + request: None, + bound: Bound::ANY, + }; + + pub fn min(min: Len) -> Self { + Self::FREE.at_least(min) + } + + pub fn max(max: Len) -> Self { + Self::FREE.at_most(max) + } + + pub fn clamp(min: Len, max: Len) -> Self { + Self::min(min).at_most(max) } - /// 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 { - self.exact().is_some_and(|len| len.rel != Rel::ZERO) || self.bound().has_fraction() + 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 - /// was no rule. + /// Replaces the floor while preserving the request and cap. pub fn at_least(&self, min: Len) -> Self { - match *self { - Self::Free | Self::Min(_) => Self::Min(min), - Self::Max(max) | Self::Clamp { max, .. } => Self::Clamp { min, max }, - Self::Request(ref request) => request.as_ref().clone().max(min).into(), - Self::Exact(len) => len.max(min).into(), - } + let mut rule = self.clone(); + rule.bound.min = Some(min); + rule } - /// This rule with a cap over it, which is the whole of it where there was - /// no rule. + /// Replaces the cap while preserving the request and floor. pub fn at_most(&self, max: Len) -> Self { - match *self { - Self::Free | Self::Max(_) => Self::Max(max), - Self::Min(min) | Self::Clamp { min, .. } => Self::Clamp { min, max }, - Self::Request(ref request) => request.as_ref().clone().min(max).into(), - Self::Exact(len) => len.min(max).into(), - } + let mut rule = self.clone(); + rule.bound.max = Some(max); + rule } - /// The length this rule gives without the widget being drawn, if it can - /// give one. pub fn declared(&self) -> Option { self.exact().and_then(|len| len.declared()) } - /// The length this rule gives outright, whatever the widget reports -- - /// which makes the widget's answer on that axis moot. A share counts: it - /// is a length the widget's parent still has to divide, so it is exact - /// here and resolved there, unlike `declared`, which is only the ones - /// that give a box directly. + /// A linear preferred length, before applying the independent bounds. pub fn exact(&self) -> Option { - match self { - Self::Exact(len) => Some(*len), - Self::Free | Self::Request(_) | Self::Min(_) | Self::Max(_) | Self::Clamp { .. } => { + match self.request { + Some(SizeRequest::Linear(len)) => Some(len), + _ => None, + } + } + + /// Requests whose final length needs allocation, including a linear + /// share constrained by an independent bound. + pub(crate) fn deferred(&self) -> Option<&SizeRequest> { + let request = self.request.as_ref()?; + match request { + SizeRequest::Linear(len) + if len.leftover == crate::Weight::ZERO || self.bound == Bound::ANY => + { None } + _ => Some(request), } } } -/// What a rule allows a length to be where it does not give one outright: a -/// 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. +/// A floor, a cap, or both, independent of the preferred length. +/// Fractions use the incoming rel base. An allocated slot keeps the +/// allocator's base for bounds, even when the slot narrows the widget's own. /// /// A bound is a [`Len`]. Comparisons involving shares are [`SizeRequest`]s. #[derive(Debug, Clone, Copy, PartialEq, Default)] @@ -162,22 +134,22 @@ impl_axis_index!(Bounds => Bound); impl From for SizeRule { fn from(len: LayoutLen) -> Self { - Self::Exact(len) + SizeRequest::from(len).into() } } impl From for SizeRule { fn from(request: SizeRequest) -> Self { - match request { - SizeRequest::Linear(len) => Self::Exact(len), - request => Self::Request(Arc::new(request)), + Self { + request: Some(request), + bound: Bound::ANY, } } } impl From> for SizeRule { fn from(len: Option) -> Self { - len.map_or(Self::Free, Self::Exact) + len.map_or(Self::FREE, Self::from) } } @@ -191,12 +163,10 @@ pub struct SizeRules { impl_axis_index!(SizeRules => SizeRule); -/// What a widget's box is on each axis where something says so outright, -/// before it is drawn: a rule beside it, or a hint it gives about itself. -/// Whoever draws the widget resolves these against its rel base. -/// -/// A [`Len`] rather than a [`LayoutLen`], because a share can never be one -/// -- see [`LayoutLen::declared`]. +/// Box lengths chosen by the ask: a declaration, a natural-size hint, or +/// a binding bound. These are window lengths, already resolved against the +/// incoming rel base. Moving a drawing preserves them instead of resolving +/// a fraction against its destination a second time. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Declared { pub x: Option, diff --git a/core/src/widget/widgets.rs b/core/src/widget/widgets.rs index fd465ac..5ed6524 100644 --- a/core/src/widget/widgets.rs +++ b/core/src/widget/widgets.rs @@ -145,6 +145,16 @@ impl Widgets { self.needs_redraw.insert(id); } + /// Changes the preferred length without changing its bounds. + pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into) { + let id = id.id(); + let rule = SizeRule { + request: Some(len.into()), + bound: self.size_rules(id)[axis].bound, + }; + self.set_size_rule(id, axis, rule); + } + /// Puts a floor under this widget's length on one axis, keeping a cap it /// already had. See [`SizeRule::at_least`]. pub fn set_min_len(&mut self, id: impl IdLike, axis: Axis, min: Len) { diff --git a/src/harness.rs b/src/harness.rs index d5e00fe..8354a34 100644 --- a/src/harness.rs +++ b/src/harness.rs @@ -168,7 +168,7 @@ impl Harness { pub fn set_len(&mut self, id: impl IdLike, axis: Axis, len: impl Into) { self.rsc .widgets_mut() - .set_size_rule(id, axis, SizeRule::Exact(len.into())); + .set_size_rule(id, axis, SizeRule::from(len.into())); } /// Sets the root and lays it out, so a pointer event has something to hit. diff --git a/src/random.rs b/src/random.rs index f5416bd..99e1812 100644 --- a/src/random.rs +++ b/src/random.rs @@ -350,9 +350,7 @@ impl Plan { 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; - } + rules[axis].bound = Bound::ANY; } }); } @@ -682,8 +680,8 @@ impl Sow<'_> { match self.rng.below(8) { 0 | 1 => self.len().into(), 2 => LayoutLen::LEFTOVER.into(), - 3 => SizeRule::Min(self.bound()), - 4 => SizeRule::Max(self.bound()), + 3 => SizeRule::min(self.bound()), + 4 => SizeRule::max(self.bound()), // Both in pixels, so one can be put under the other: a floor and // a cap that change sides with the window bound nothing, which // is a caller's bug rather than a tree to grow. @@ -692,12 +690,9 @@ impl Sow<'_> { Px::from_f32(20.0 + self.rng.below(180) as f32), Px::from_f32(20.0 + self.rng.below(180) as f32), ); - SizeRule::Clamp { - min: Len::px(a.min(b).to_f32()), - max: Len::px(a.max(b).to_f32()), - } + SizeRule::clamp(Len::px(a.min(b).to_f32()), Len::px(a.max(b).to_f32())) } - _ => SizeRule::Free, + _ => SizeRule::FREE, } } diff --git a/src/widget/position/max_size.rs b/src/widget/position/max_size.rs deleted file mode 100644 index aa56d0e..0000000 --- a/src/widget/position/max_size.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::prelude::*; - -/// Asks its child in the shorter of a cap and the box this widget was given, -/// and answers what the child used, held to the same cap. -/// -/// A cap on the box is a widget rather than a [`SizeRule`] because a box is -/// whoever asked's to decide: a rule that read the box it was given would be -/// decided again by every path that hands a widget one, including the ones -/// that re-place a drawing without asking it anything, and the decision would -/// then depend on which path arrived last. A widget is drawn again whenever -/// its own box changes, so the comparison is made where the answer can be -/// kept -- `longer_than` narrows the windows this drawing holds for, and -/// `holds` says the box lengths. -/// -/// The box is what a text wraps at and what a scroll takes its viewport from, -/// which is why capping the answer alone is not the same thing. -pub struct MaxSize { - pub inner: StrongWidget, - pub x: Option, - pub y: Option, -} - -impl MaxSize { - fn max(&self, axis: Axis) -> Option { - match axis { - Axis::X => self.x, - Axis::Y => self.y, - } - } -} - -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; - for axis in Axis::BOTH { - let Some(max) = self.max(axis) else { - continue; - }; - let own = painter.region_len(axis); - if painter.longer_than(own, max, axis) { - region[axis] = max.align(align[axis]); - } - } - let mut size = painter.widget_at(&self.inner, region).size(); - for axis in Axis::BOTH { - // The child may draw past the box it was given -- a text too tall - // for it -- and the cap is a promise about the length as well. A - // share passes through: it is a length only to whoever divides - // one, and that is this widget's parent rather than this widget, - // which has already given the share the box the cap allows. - if let Some(max) = self.max(axis) - && painter.longer_than(size[axis].without_leftover(), max, axis) - { - size[axis] = max.into(); - } - } - size - } -} diff --git a/src/widget/position/mod.rs b/src/widget/position/mod.rs index ce50abe..a86979b 100644 --- a/src/widget/position/mod.rs +++ b/src/widget/position/mod.rs @@ -1,5 +1,4 @@ mod layer; -mod max_size; mod offset; mod pad; mod scroll; @@ -7,7 +6,6 @@ mod span; mod stack; pub use layer::*; -pub use max_size::*; pub use offset::*; pub use pad::*; pub use scroll::*; diff --git a/src/widget/trait_fns.rs b/src/widget/trait_fns.rs index ab42dea..44d1bc0 100644 --- a/src/widget/trait_fns.rs +++ b/src/widget/trait_fns.rs @@ -53,8 +53,8 @@ widget_trait! { move |state| { let id = self.add(state); let widgets = &mut state.ui_mut().widgets; - widgets.set_size_rule(id, Axis::X, SizeRule::Exact(size.x)); - widgets.set_size_rule(id, Axis::Y, SizeRule::Exact(size.y)); + widgets.set_len(id, Axis::X, size.x); + widgets.set_len(id, Axis::Y, size.y); id } } @@ -66,15 +66,12 @@ widget_trait! { state .ui_mut() .widgets - .set_size_rule(id, Axis::X, SizeRule::from(len)); + .set_len(id, Axis::X, len); id } } - /// Answers at least this wide, whatever it drew: a rule beside the - /// widget, so what a row gives it is at least this even where the widget - /// itself wanted less. The box it draws in is untouched -- for that, see - /// [`MaxSize`]. + /// Sets a floor on this widget's offered width and reported width. fn min_width(self, len: impl Into) -> impl WidgetIdFn { let len = len.into(); move |state| { @@ -93,25 +90,22 @@ widget_trait! { } } - /// Puts this in a [`MaxSize`]: it is asked in the shorter of the cap and - /// the box that widget was given, and is as long as it used, held to the - /// cap. A widget rather than a rule because the box is whoever asked's to - /// decide -- see [`MaxSize`]. - fn max_width(self, len: impl Into) -> impl WidgetFn { + /// Caps this widget's offered width and reported width. + fn max_width(self, len: impl Into) -> impl WidgetIdFn { let len = len.into(); - move |state| MaxSize { - inner: self.add_strong(state), - x: Some(len), - y: None, + move |state| { + let id = self.add(state); + state.ui_mut().widgets.set_max_len(id, Axis::X, len); + id } } - fn max_height(self, len: impl Into) -> impl WidgetFn { + fn max_height(self, len: impl Into) -> impl WidgetIdFn { let len = len.into(); - move |state| MaxSize { - inner: self.add_strong(state), - x: None, - y: Some(len), + move |state| { + let id = self.add(state); + state.ui_mut().widgets.set_max_len(id, Axis::Y, len); + id } } @@ -122,7 +116,7 @@ widget_trait! { state .ui_mut() .widgets - .set_size_rule(id, Axis::Y, SizeRule::from(len)); + .set_len(id, Axis::Y, len); id } } diff --git a/tests/allocation_cost.rs b/tests/allocation_cost.rs index 73e5ea6..929e6f1 100644 --- a/tests/allocation_cost.rs +++ b/tests/allocation_cost.rs @@ -35,16 +35,16 @@ static ALLOCATOR: Counting = Counting; #[test] fn unchanged_tree_reuses_layout_storage() { - for deferred in [false, true] { + for rule in [ + SizeRule::FREE, + leftover(1).clamp(20, 80).into(), + SizeRule::clamp(20.into(), 80.into()), + ] { let mut h = Harness::new((600, 200)); let mut children: Vec = 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()); - } + h.rsc.widgets_mut().set_size_rule(a, Axis::X, rule.clone()); let row = (a, rect(Color::BLUE)) .span(Dir::RIGHT) .add_strong(&mut h.rsc); @@ -74,7 +74,7 @@ fn unchanged_tree_reuses_layout_storage() { h.frame(); } let allocations = COUNT.replace(None).unwrap(); - println!("deferred={deferred}: {allocations} allocations over 100 resize frames"); + println!("rule={rule:?}: {allocations} allocations over 100 resize frames"); assert_eq!(allocations, 0); } } diff --git a/tests/bounds_cost.rs b/tests/bounds_cost.rs new file mode 100644 index 0000000..d6437d6 --- /dev/null +++ b/tests/bounds_cost.rs @@ -0,0 +1,60 @@ +//! CPU comparison of bounds attributes and the former wrapper, using the +//! same builder calls and geometry. Run the release executable under perf; +//! process totals include the cold frame. MODE=plain|exact|cap, REDRAW=0|1. +use iris::{harness::Harness, prelude::*}; + +#[test] +#[ignore = "instruction-count measurement"] +fn bounds_cost() { + let mode = std::env::var("MODE").unwrap_or_else(|_| "cap".into()); + let redraw = std::env::var("REDRAW").is_ok_and(|value| value == "1"); + let frames = std::env::var("FRAMES") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(2000); + let mut h = Harness::new((300, 512)); + let mut column = Span::empty(Dir::DOWN); + let mut leaves = Vec::new(); + for _ in 0..128 { + let first = match mode.as_str() { + "plain" => rect(Color::RED).add_strong(&mut h.rsc).any(), + "exact" => rect(Color::RED).width(40).add_strong(&mut h.rsc).any(), + "cap" => rect(Color::RED).max_width(80).add_strong(&mut h.rsc).any(), + _ => panic!("unknown MODE {mode}"), + }; + leaves.push(first.id()); + let second = rect(Color::BLUE).add_strong(&mut h.rsc); + let row = Span { + children: vec![first, second], + dir: Dir::RIGHT, + gap: Px::ZERO, + }; + column.push(row.height(4).add_strong(&mut h.rsc)); + } + h.set_root(column); + let ids: Vec<_> = h.render.active.keys().copied().collect(); + println!( + "mode={mode}, widgets={}, rule_bytes={}", + ids.len(), + std::mem::size_of::() + ); + for frame in 0..frames { + if redraw { + for &id in &ids { + h.rsc.widgets_mut().mark_for_redraw(id); + } + } + let width = if frame % 2 == 0 { 100 } else { 300 }; + h.resize((width, 512)); + h.frame(); + let expected = match mode.as_str() { + "plain" => width / 2, + "exact" => 40, + "cap" => (width / 2).min(80), + _ => unreachable!(), + }; + for id in &leaves { + assert_eq!(h.region(id).unwrap().size().x, Px::from_int(expected)); + } + } +} diff --git a/tests/cases/deferred.rs b/tests/cases/deferred.rs index b1bebdf..faecdb1 100644 --- a/tests/cases/deferred.rs +++ b/tests/cases/deferred.rs @@ -84,7 +84,7 @@ fn request_edits_in_a_nested_child_reach_the_allocator() { 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.rsc.widgets_mut().set_max_len(a, Axis::X, Len::px(40.0)); h.frame(); assert_corners!(h, a, (0, 0), (40, 100)); assert_corners!(h, tail, (40, 0), (300, 100)); @@ -109,7 +109,7 @@ fn adding_a_bound_to_a_previously_unbounded_share_reallocates_the_row() { h.frame(); h.rsc .widgets_mut() - .set_size_rule(id, Axis::X, SizeRule::Max(Len::px(80.0))); + .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)); @@ -209,7 +209,7 @@ fn bounds_also_apply_to_shares_discovered_by_drawing() { 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))); + .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(); @@ -245,7 +245,7 @@ fn a_measured_nested_share_keeps_its_comparison_for_the_outer_span() { let a_id = a.id(); h.rsc .widgets_mut() - .set_size_rule(a_id, Axis::X, SizeRule::Max(Len::px(80.0))); + .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 { @@ -289,14 +289,11 @@ fn relative_bounds_on_a_hinted_child_track_the_offer_before_its_declared_size() let id = natural.id(); h.rsc .widgets_mut() - .set_size_rule(id, Axis::X, SizeRule::Max(Len::rel(0.75))); + .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), - }, + SizeRule::clamp(Len::rel(0.25), Len::rel(0.75)), ); let inner = h.rsc.widgets_mut().add_strong(Stack { children: vec![natural], @@ -313,10 +310,7 @@ fn relative_bounds_on_a_hinted_child_track_the_offer_before_its_declared_size() h.rsc.widgets_mut().set_size_rule( bounded.id(), Axis::X, - SizeRule::Clamp { - min: Len::rel(0.25), - max: Len::rel(0.75), - }, + SizeRule::clamp(Len::rel(0.25), Len::rel(0.75)), ); let fixed = wtext("one line, overflowing whatever it is given") .size(16) @@ -389,3 +383,18 @@ fn moving_scroll_content_preserves_its_resolved_expression_size() { h.frame(); assert_corners!(h, leaf_id, (0, 240), (300, 360)); } + +#[test] +fn an_intrinsic_share_cap_uses_the_allocators_fractional_base() { + let mut h = Harness::new((300, 100)); + let head = rect(Color::BLUE).width(30).add(&mut h.rsc); + let bounded = rect(Color::RED).max_width(Len::rel(0.25)).add(&mut h.rsc); + let tail = rect(Color::GREEN).add(&mut h.rsc); + h.set_root((head, bounded, tail).span(Dir::RIGHT)); + assert_corners!(h, bounded, (30, 0), (105, 100)); + assert_corners!(h, tail, (105, 0), (300, 100)); + h.resize((400, 100)); + h.frame(); + assert_corners!(h, bounded, (30, 0), (130, 100)); + assert_corners!(h, tail, (130, 0), (400, 100)); +} diff --git a/tests/cases/layout.rs b/tests/cases/layout.rs index d1d421b..c749801 100644 --- a/tests/cases/layout.rs +++ b/tests/cases/layout.rs @@ -824,7 +824,7 @@ fn row_under_fractions(kid: Option, gap: f32, box_w: f32) -> (Vec if let Some(len) = kid { h.rsc .widgets_mut() - .set_size_rule(r.id(), Axis::X, SizeRule::Exact(len)); + .set_size_rule(r.id(), Axis::X, SizeRule::from(len)); } ids.push(r.id()); kids.push(r.add_strong(&mut h.rsc)); @@ -1005,10 +1005,6 @@ fn a_region_node_root_is_a_region_node() { assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900)); } -/// A bound is a rule about what a widget answers: it holds the length that -/// reaches whoever asked and leaves the box alone. Here the content is 400 -/// wide in a 250 window, so a cap cuts what the row reports and a floor -/// raises it, while the rects inside stay where the 250 box put them. #[test] fn a_bound_holds_what_a_widget_answers() { let row = |rule: SizeRule| { @@ -1023,51 +1019,37 @@ fn a_bound_holds_what_a_widget_answers() { h.region(&left).unwrap().size().x, ) }; - let (capped, left) = row(SizeRule::Max(Len::px(300.0))); + let (capped, left) = row(SizeRule::max(Len::px(300.0))); assert_eq!(capped, Px::from_int(300), "the cap, not the 400 drawn"); assert_eq!(left, Px::from_int(200), "the box the children were given"); - let (floored, _) = row(SizeRule::Min(Len::px(600.0))); + let (floored, _) = row(SizeRule::min(Len::px(600.0))); assert_eq!(floored, Px::from_int(600), "the floor, not the 400 drawn"); - let (free, _) = row(SizeRule::Free); + let (free, _) = row(SizeRule::FREE); assert_eq!(free, Px::from_int(400), "what it drew"); } -/// A cap on the box is `MaxSize`, which asks its child in the shorter of the -/// cap and its own box. That is the box a text wraps at and a scroll takes -/// its viewport from, so it cannot be had by holding the answer. #[test] -fn a_cap_widget_asks_its_child_in_the_shorter_box() { +fn a_cap_attribute_narrows_the_widgets_box() { let mut h = Harness::new((400, 200)); - // A fraction of its box, so it says what box it was asked in. let fills = rect(Color::RED).width(rel(1.0)).add(&mut h.rsc); let capped = fills.max_width(300).add(&mut h.rsc); + assert_eq!(fills.id(), capped.id()); h.set_root(capped); assert_eq!(h.region(&fills).unwrap().size().x, Px::from_int(300)); - assert_eq!( - h.region(&capped).unwrap().size().x, - Px::from_int(300), - "as long as its child used" - ); - // A child that asked for a share takes the box the cap allows, and the - // share itself passes up: whoever divides one is this widget's parent. let mut h = Harness::new((400, 200)); let share = rect(Color::RED).add(&mut h.rsc); let capped = share.max_width(300).add(&mut h.rsc); h.set_root(capped); assert_eq!(h.region(&share).unwrap().size().x, Px::from_int(300)); - assert_eq!(h.region(&capped).unwrap().size().x, Px::from_int(400)); } -/// Which of the cap and the box is shorter is a question in pixels, so it is -/// asked again wherever the answer can change -- and the widget asking it is -/// drawn again whenever its own box is, which is what keeps the two in step. #[test] -fn a_cap_widget_is_decided_again_on_either_side_of_the_crossing() { +fn a_cap_attribute_is_decided_again_on_either_side_of_the_crossing() { let mut h = Harness::new((400, 200)); let probe = rect(Color::RED).add(&mut h.rsc); h.set_root(probe.max_width(300)); @@ -1116,3 +1098,222 @@ fn a_cap_holds_an_answer_that_overflowed_its_box() { // row draws 400 of it. Its answer is the cap, and the window centres it. assert_corners!(h, row, (-25, 0), (275, 200)); } + +struct Offered { + seen: Rc>, + answer: Size, +} + +impl Widget for Offered { + fn draw(&mut self, painter: &mut Painter) -> Size { + self.seen.set(painter.px_size()); + painter.primitive(RectPrimitive::color(Color::RED)); + self.answer + } +} + +#[test] +fn bounds_constrain_the_offer_without_replacing_an_intrinsic_answer() { + for axis in Axis::BOTH { + for node in [false, true] { + let mut h = Harness::new((400, 400)); + let seen = Rc::new(Cell::new(PxVec2::ZERO)); + let probe = h.rsc.widgets_mut().add_strong(Offered { + seen: seen.clone(), + answer: Size::px(Vec2::new(40.0, 40.0)), + }); + let id = probe.id(); + h.rsc.widgets_mut().set_region_node(id, node); + h.rsc.widgets_mut().set_max_len(id, axis, 100.into()); + h.state.root = Some(probe); + h.frame(); + assert_eq!(seen.get()[axis], Px::from_int(100)); + assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(40)); + h.rsc.widgets_mut().set_min_len(id, axis, 60.into()); + h.frame(); + assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(60)); + h.resize((50, 50)); + h.frame(); + assert_eq!(seen.get()[axis], Px::from_int(60)); + h.rsc.widgets_mut().set_size_rule(id, axis, SizeRule::FREE); + h.frame(); + assert_eq!(seen.get()[axis], Px::from_int(50)); + assert_eq!(h.region(&id).unwrap().size()[axis], Px::from_int(40)); + } + } +} + +#[test] +fn a_cap_attribute_is_the_scroll_viewport() { + for node in [false, true] { + let mut h = Harness::new((400, 400)); + let content = rect(Color::RED).height(400).add(&mut h.rsc); + let inner = content.add_strong(&mut h.rsc); + let scroll = Scroll::new(inner, Axis::Y).max_height(100).add(&mut h.rsc); + h.rsc.widgets_mut().set_region_node(scroll, node); + h.set_root(scroll); + assert_corners!(h, scroll, (0, 150), (400, 250)); + assert_corners!(h, content, (0, -150), (400, 250)); + h.rsc.widgets_mut().get_mut(&scroll).unwrap().scroll(1000.0); + h.frame(); + assert_corners!(h, content, (0, 150), (400, 550)); + h.resize((400, 80)); + h.frame(); + assert_corners!(h, scroll, (0, 0), (400, 80)); + assert_corners!(h, content, (0, 0), (400, 400)); + } +} + +#[test] +fn a_cap_attribute_wraps_text_before_it_answers() { + let mut h = Harness::new((400, 500)); + let text = wtext("one two three four five six seven eight nine ten") + .size(16) + .wrap(true) + .max_width(80) + .align(Align::TOP_LEFT) + .add(&mut h.rsc); + h.set_root(text); + let capped = h.region(&text).unwrap().size(); + assert!(capped.x <= Px::from_int(80)); + assert!(capped.y > Px::from_int(30)); + h.rsc + .widgets_mut() + .set_size_rule(text, Axis::X, SizeRule::FREE); + h.frame(); + let free = h.region(&text).unwrap().size(); + assert!(free.x > capped.x); + assert!(free.y < capped.y); +} + +#[test] +fn dimensions_and_bounds_are_independent_attributes_in_either_order() { + for bounds_first in [false, true] { + let mut h = Harness::new((400, 400)); + let probe = rect(Color::RED).add(&mut h.rsc); + let bounded = if bounds_first { + probe + .max_width(80) + .min_height(60) + .width(120) + .height(40) + .add(&mut h.rsc) + } else { + probe + .width(120) + .height(40) + .max_width(80) + .min_height(60) + .add(&mut h.rsc) + }; + assert_eq!(probe.id(), bounded.id()); + h.set_root(bounded); + assert_eq!( + h.region(&probe).unwrap().size(), + PxVec2::from_f32((80, 60).into()) + ); + h.rsc.widgets_mut().set_len(probe, Axis::X, 50); + h.rsc.widgets_mut().set_len(probe, Axis::Y, 100); + h.frame(); + assert_eq!( + h.region(&probe).unwrap().size(), + PxVec2::from_f32((50, 100).into()) + ); + } +} + +#[test] +fn changing_a_share_cap_replaces_it_without_losing_the_share() { + let mut h = Harness::new((300, 100)); + let capped = rect(Color::RED) + .max_width(80) + .width(leftover(1)) + .add(&mut h.rsc); + let sibling = rect(Color::BLUE).add(&mut h.rsc); + h.set_root((capped, sibling).span(Dir::RIGHT)); + assert_corners!(h, capped, (0, 0), (80, 100)); + assert_corners!(h, sibling, (80, 0), (300, 100)); + h.rsc.widgets_mut().set_max_len(capped, Axis::X, 160.into()); + h.frame(); + assert_corners!(h, capped, (0, 0), (150, 100)); + assert_corners!(h, sibling, (150, 0), (300, 100)); +} + +// Reduced from seed 104 at depth 5: a widget widening its own window +// contract must not erase the bound's crossing at a quarter-window of 173. +#[test] +fn a_widgets_window_contract_cannot_widen_its_bounds_contract() { + fn tree(h: &mut Harness) -> WidgetId { + let content = rect(Color::RED) + .width(137) + .min_height(194) + .add_strong(&mut h.rsc); + let scroll = Scroll::new(content, Axis::X).add_strong(&mut h.rsc); + let probe = rect(Color::RED).add_strong(&mut h.rsc); + let id = probe.id(); + let wide = rect(Color::GREEN).add_strong(&mut h.rsc); + let narrow = rect(Color::BLUE).add_strong(&mut h.rsc); + let branch = iris::random::Branch { + probe, + wide, + narrow, + threshold: 459.0, + } + .min_width(94) + .max_width(173) + .add_strong(&mut h.rsc); + let stack = Stack { + children: vec![scroll, branch], + size: StackSize::Child(0), + } + .add_strong(&mut h.rsc); + let mut children: Vec = (0..3) + .map(|_| rect(Color::RED).add_strong(&mut h.rsc).any()) + .collect(); + children.push(stack); + h.set_root( + Span { + children, + dir: Dir::RIGHT, + gap: Px::ZERO, + } + .height(rel(1)), + ); + id + } + let mut warm = Harness::new((1920, 1200)); + let probe = tree(&mut warm); + warm.resize((640, 900)); + warm.frame(); + assert_corners!(warm, probe, (480, 0), (640, 40)); + let mut cold = Harness::new((640, 900)); + let other = tree(&mut cold); + assert_eq!(warm.region(&probe), cold.region(&other)); +} + +#[test] +fn a_fixed_declaration_keeps_its_cap_when_the_row_has_no_leftover() { + let mut h = Harness::new((100, 100)); + let fixed = rect(Color::RED).width(200).max_width(100).add(&mut h.rsc); + let share = rect(Color::BLUE).add(&mut h.rsc); + h.set_root((fixed, share).span(Dir::RIGHT)); + assert_corners!(h, fixed, (0, 0), (100, 100)); +} + +#[test] +fn a_new_bound_reaches_the_parent_even_when_the_current_answer_is_unchanged() { + let mut h = Harness::new((400, 100)); + let leaf = rect(Color::RED).add(&mut h.rsc); + let inner = leaf.add_strong(&mut h.rsc); + let root = Scroll::new(inner, Axis::Y) + .pad(Padding::uniform(0)) + .width(154) + .height(100) + .add(&mut h.rsc); + h.set_root(root); + assert_eq!(h.region(&leaf).unwrap().size().x, Px::from_int(154)); + h.rsc.widgets_mut().set_min_len(leaf, Axis::X, 140.into()); + h.rsc.widgets_mut().set_len(root, Axis::X, 78); + h.frame(); + assert_corners!(h, leaf, (130, 0), (270, 100)); +} diff --git a/tests/cases/plan.rs b/tests/cases/plan.rs index a9a398c..b3ef1de 100644 --- a/tests/cases/plan.rs +++ b/tests/cases/plan.rs @@ -31,8 +31,8 @@ fn some_edits(seed: u64, of: &Plan) -> Edits { ( i, SizeRules { - x: SizeRule::Exact(LayoutLen::LEFTOVER), - y: SizeRule::Free, + x: SizeRule::from(LayoutLen::LEFTOVER), + y: SizeRule::FREE, }, ) }) diff --git a/tests/deferred_generated.rs b/tests/deferred_generated.rs index 961d64c..f8a1f95 100644 --- a/tests/deferred_generated.rs +++ b/tests/deferred_generated.rs @@ -51,15 +51,33 @@ fn relative_intrinsic_bounds_agree_warm_and_cold() { 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(), - }; + let bound = &mut rules[axis].bound; + if bound.min.is_some() { + bound.min = Some(Len::rel(0.25)); + } + if bound.max.is_some() { + bound.max = Some(Len::rel(0.75)); + } + } + } + }); + }); +} + +#[test] +fn preferred_requests_with_independent_bounds_agree_warm_and_cold() { + check_requests(|grown| { + let mut index = 0; + grown.walk_mut(&mut |node| { + if let Some(rules) = &mut node.size { + for axis in Axis::BOTH { + index += 1; + rules[axis].request = Some(match index % 4 { + 0 => leftover(1).into(), + 1 => rel(0.5).into(), + 2 => px(80).into(), + _ => (leftover(1) + px(30)).min(leftover(2)), + }); } } }); diff --git a/tests/layout_diagnostics.rs b/tests/layout_diagnostics.rs index 84f3500..11eb245 100644 --- a/tests/layout_diagnostics.rs +++ b/tests/layout_diagnostics.rs @@ -264,7 +264,7 @@ fn layout_cost() { harness .rsc .widgets_mut() - .set_size_rule(sized, Axis::X, SizeRule::Exact(len)); + .set_size_rule(sized, Axis::X, SizeRule::from(len)); }); } diff --git a/tests/scenario/mod.rs b/tests/scenario/mod.rs index 9e797b3..4a229e8 100644 --- a/tests/scenario/mod.rs +++ b/tests/scenario/mod.rs @@ -200,8 +200,8 @@ fn mark(warm: &mut Harness, tree: &Tree, step: usize) { fn a_rule(rng: &mut Rng) -> SizeRule { let len = Len::px(20.0 + rng.below(180) as f32); match rng.below(4) { - 0 => SizeRule::Max(len), - 1 => SizeRule::Min(len), + 0 => SizeRule::max(len), + 1 => SizeRule::min(len), _ => LayoutLen::from(len).into(), } } @@ -357,18 +357,7 @@ fn change(case: Case, warm: &mut Harness, tree: &mut Tree, plan: &Plan, rng: &mu /// buildable from what the failure printed. fn describe(id: WidgetId, h: &Harness) -> String { 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 } => { - format!(">{}<{}", LayoutLen::from(min), LayoutLen::from(max)) - } - }; + let rule = |r: SizeRule| format!("{r:?}"); let align = h.rsc.widgets().alignment(id); let side = |a: AxisAlign| { if a == AxisAlign::NEG {