diff --git a/core/src/orientation/len.rs b/core/src/orientation/len.rs index 0ef6bdc..81205e0 100644 --- a/core/src/orientation/len.rs +++ b/core/src/orientation/len.rs @@ -23,6 +23,14 @@ pub struct LayoutLen { pub leftover: Weight, } +/// A bare number is pixels, which is the one length that needs no box to be +/// read in. +impl From for Len { + fn from(value: N) -> Self { + Len::px(value.to_f32()) + } +} + impl From for LayoutLen { fn from(value: N) -> Self { LayoutLen::px(value.to_f32()) diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index ee2f620..3f6f572 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -1,6 +1,6 @@ use crate::{ - Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive, - Size, TextureHandle, UiRegion, UiVec2, WidgetId, + Bounds, Declared, LayerId, LayoutHolds, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, + RetainedPrimitive, Size, TextureHandle, UiRegion, UiVec2, WidgetId, }; /// What is kept of a widget its parent has asked about. `drawn` says whether @@ -58,6 +58,10 @@ pub struct ActiveData { /// A change to one moves a box this widget cannot fix by drawing again, /// and comparing them is what says so. pub declared: Declared, + /// Its bounds, resolved the same way. The answer is held to these where + /// the box was not, so a change to one changes what it answers even + /// where its declared lengths stand. + pub bounds: Bounds, /// Its alignment when it was last drawn, which a change to the property /// is found against. pub own_align: RegionAlign, diff --git a/core/src/ui/holds.rs b/core/src/ui/holds.rs index 35d852c..2460669 100644 --- a/core/src/ui/holds.rs +++ b/core/src/ui/holds.rs @@ -1,4 +1,4 @@ -use crate::{Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow}; +use crate::{Bound, Len, Outside, Px, REL_SHIFT, fixed::div_toward, fixed::narrow}; use std::ops::RangeInclusive; /// The lengths of a box, in pixels, that one drawing of a widget holds for: @@ -39,6 +39,44 @@ impl Len { } } +impl Bound { + /// Which end of this bound `len` falls outside, and the windows that + /// answer holds for. Nothing where it is inside, which is the answer + /// wherever there is no bound at all. + /// + /// `len` and this bound are lengths of the same thing, whichever that + /// is: a box in window lengths wants the bound resolved, and a length a + /// widget declares of its rel base wants it as the rule wrote it. Both + /// comparisons are in pixels, so each is a question about this window, + /// and the box is decided again on the other side of a crossing. + pub fn outside(&self, len: Len, window: Px) -> (Option, Holds) { + let mut outside = None; + let mut holds = Holds::ANY; + let mut held = len; + if let Some(min) = self.min { + let (shorter, kept) = min.longer_than(held, window); + holds = holds.and(kept); + if shorter { + outside = Some(Outside::Shorter); + held = min; + } + } + if let Some(max) = self.max { + let (longer, kept) = held.longer_than(max, window); + holds = holds.and(kept); + if longer { + debug_assert!( + outside.is_none(), + "a floor of {:?} over a cap of {max:?} bounds nothing", + self.min, + ); + outside = Some(Outside::Longer); + } + } + (outside, holds) + } +} + impl Holds { pub const ANY: Self = Self { lo: Px::MIN, diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 4a24065..c9ea38b 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -1,9 +1,9 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter}; use crate::{ - Axis, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, RegionAlign, Rel, - RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, TextureHandle, - UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets, + Axis, Bound, Bounds, Declared, Holds, LayoutHolds, LayoutLen, Len, PlaceDesc, Px, PxVec2, + RegionAlign, Rel, RenderedText, RetainedPrimitive, Size, StrongWidget, TextAttrs, TextBuffer, + TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, WidgetId, Widgets, render::{ GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveInst, PrimitiveKind, TexturePrimitive, @@ -193,6 +193,7 @@ impl<'a> Painter<'a> { region, place, declared, + bounds, holds: ask_holds, } = self .placing() @@ -221,6 +222,8 @@ impl<'a> Painter<'a> { region, placed: place, asked: offer, + declared, + bounds, ask_holds, re_asked, }, @@ -269,7 +272,13 @@ impl<'a> Painter<'a> { let states_rel_base = Axis::BOTH .iter() .any(|&axis| matches!(place[axis].rel_base, RelBase::Len(_))); - if states_rel_base || !self.children.contains(&id.id()) { + // A bound is decided against the box the widget is given, so a box + // decided here is a question rather than a move: putting the drawing + // in it would keep a decision made about the box it was measured in. + let bounded = Axis::BOTH + .iter() + .any(|&axis| self.rsc.widgets().size_rules(id.id())[axis].bound() != Bound::ANY); + if states_rel_base || bounded || !self.children.contains(&id.id()) { return self.widget_at(id, place); } let at = self.placing(); @@ -689,7 +698,14 @@ pub(super) struct Ask { /// The place the ask came to, which a rule of the widget's own can take /// past the box its parent offered. pub place: PlaceDesc, + /// What the widget's box is on each axis where something says so + /// outright: its rule or its hint, or a bound of its own that the box it + /// was offered falls outside -- a bound that binds is a declaration, and + /// the same one the widget answers with. pub declared: Declared, + /// Its bounds, resolved against the rel base its rules were resolved + /// against, for the answer to be held to where the box was not. + pub bounds: Bounds, /// What the ask itself holds for, kept on the widget asked about: a rule /// compared against the offer in pixels holds only for the windows on its /// side of the crossing, and that range reaches whoever asked through the @@ -714,21 +730,40 @@ impl Placing { mut place: PlaceDesc, ) -> Ask { let align = widgets.alignment(id); + let rules = widgets.size_rules(id); let mut holds = LayoutHolds::ANY; - // A share fills what the pixels and fraction beside it leave of the - // box and overflows where they are longer, which is the rule a span - // follows with one child. Only the overflow is a box of the child's - // own: a share that fits is the box it was given, which is what this - // place already says. + let mut declared = widgets.declared_lens(id); + let mut bounds = Bounds::ANY; for axis in Axis::BOTH { - let (len, kept) = + let base = place.base(axis, self.rel_base); + // A share fills what the pixels and fraction beside it leave of + // the box and overflows where they are longer, which is the rule + // a span follows with one child. Only the overflow is a box of + // the widget's own: a share that fits is the box it was given, + // which is what this place already says. + let (share, kept) = self.share_past_the_offer(widgets, window[axis], id, place, align, axis); holds[axis].window = holds[axis].window.and(kept); - if let Some(len) = len { + if let Some(len) = share { place[axis] = len.as_desc().fills(); } + // A bound the box falls outside is what the widget's length is + // instead, which is a declaration: the box comes to the bound, + // and its own answer is held to the same bound where it drew + // past that. Asked of the box it would otherwise have -- what it + // declares of the place, or what the place gives it. + let bound = rules[axis].bound(); + bounds[axis] = bound.within_len(base); + let offered = declared[axis].map_or_else( + || place.of(self.region, align)[axis].len(), + |len| len.within_len(base), + ); + let (outside, kept) = bounds[axis].outside(offered, window[axis]); + holds[axis].window = holds[axis].window.and(kept); + if let Some(outside) = outside { + declared[axis] = Some(bound.at(outside)); + } } - let declared = widgets.declared_lens(id); let (rel_base, region) = place.rel_base_and_region(self.region, self.rel_base, declared, align); Ask { @@ -736,6 +771,7 @@ impl Placing { region, place, declared, + bounds, holds, } } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 8f18d99..8558cb1 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1,8 +1,8 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; use crate::{ - ActiveData, Answer, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, - MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion, + ActiveData, Answer, Axis, Bounds, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, + MaskIdx, MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets, ui::painter::Ask, util::{HashMap, Vec2}, @@ -30,6 +30,10 @@ pub(super) struct DrawInfo { /// the answer somewhere else. pub placed: PlaceDesc, pub asked: PlaceDesc, + /// What the ask made of the widget's own rules. See [`Ask::declared`] + /// and [`Ask::bounds`]. + pub declared: Declared, + pub bounds: Bounds, /// What the ask that gave it those two holds for. See [`Ask::holds`]. pub ask_holds: LayoutHolds, /// Whether the parent already asked about this widget in this draw. @@ -158,6 +162,8 @@ impl UiRenderState { region: ask.region, placed: ask.place, asked: PlaceDesc::WHOLE, + declared: ask.declared, + bounds: ask.bounds, ask_holds: ask.holds, re_asked: false, } @@ -236,7 +242,7 @@ impl UiRenderState { ); } let align = rsc.widgets().alignment(id); - let declared = rsc.widgets().declared_lens(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. @@ -392,9 +398,9 @@ impl UiRenderState { // A rule wins on the axis it names, and the draw answers the rest. // Applied here so it is one place rather than every widget that could // carry one, and so the widget under a rule never learns of it. The - // rel base is the answer where the rule gave a length outright: it was - // resolved into the rel base when the child was asked, and resolving it - // again here would take the fraction of a fraction. + // rel base is the answer wherever the ask declared a length: it was + // resolved into the rel base when the widget was asked, and resolving + // it again here would take the fraction of a fraction. let rules = rsc.widgets().size_rules(id); let ruled = |axis: Axis, reported: LayoutLen| match rules[axis].exact() { None => reported, @@ -405,10 +411,37 @@ impl UiRenderState { }, Some(len) => len.within_len(info.rel_base[axis]), }; - let size = Size { + let mut size = Size { x: ruled(Axis::X, size.x), y: ruled(Axis::Y, size.y), }; + // A bound is a promise about the length as well as about the box: a + // widget that drew past the box it was given -- a text too tall for + // it, an image at its own size under a cap -- is still held to what + // its rule allows. + // + // Held here rather than taken from the box, even where the bound + // decided that box. What a widget answers is its own, and a bound + // that replaced the answer would make a share into a fixed length + // the moment a box was long enough -- which is a length the span + // dividing that box decided from this answer, so the two would + // choose each other. A share is left alone here for the same reason: + // 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; + for axis in Axis::BOTH { + let answer = size[axis]; + if answer.leftover != Weight::ZERO { + continue; + } + let (outside, kept) = + info.bounds[axis].outside(answer.without_leftover(), window[axis]); + bounded[axis].window = kept; + if let Some(outside) = outside { + size[axis] = info.bounds[axis].at(outside).into(); + } + } // 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. @@ -439,11 +472,12 @@ impl UiRenderState { // 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. - let mut own_holds = own; + // 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 { - let fraction = rules[axis].exact().is_some_and(|len| len.rel != Rel::ZERO); - if fraction { + if rules[axis].has_fraction() { own_holds[axis].rel_base = Some(info.rel_base[axis]); } } @@ -475,6 +509,8 @@ impl UiRenderState { region: UiRegion::FULL, placed: PlaceDesc::WHOLE, asked: PlaceDesc::WHOLE, + declared: Declared::NONE, + bounds: Bounds::ANY, ask_holds: LayoutHolds::ANY, re_asked: false, }, @@ -503,7 +539,8 @@ impl UiRenderState { primitives, mask_region, children, - declared: rsc.widgets().declared_lens(id), + declared: info.declared, + bounds: info.bounds, own_align: rsc.widgets().alignment(id), move_idx, parent_move: info.parent_move, @@ -673,6 +710,9 @@ impl UiRenderState { let active = self.active.get_mut(&id).unwrap(); active.rel_base = info.rel_base; active.placed = info.placed; + // What the ask made of its rules, which a re-place decides again. + active.declared = info.declared; + active.bounds = info.bounds; #[cfg(feature = "layout-diagnostics")] { let (counter, outcome) = match (moved, is_region_node) { @@ -720,6 +760,8 @@ impl UiRenderState { region, placed: place, asked: active.asked, + declared: active.declared, + bounds: active.bounds, // Placing decides no box: this is the one the ask already gave. ask_holds: LayoutHolds::ANY, re_asked: active.re_asked, @@ -874,6 +916,7 @@ impl UiRenderState { children: Vec::new(), move_idx: info.parent_move, declared: Declared::NONE, + bounds: Bounds::ANY, own_align: rsc.widgets().alignment(id), parent_move: info.parent_move, mask: info.mask, @@ -1058,12 +1101,22 @@ impl UiRenderState { let Some(active) = self.active.get(&id) else { return true; }; - // Its parent resolved its declared lengths into its box and decided - // whether to draw it at all, so a change to either is the parent's - // to draw -- with the mark left on, so the parent draws it rather - // than keeping it. So is a widget the parent asked twice: its - // layout rests on an answer this widget cannot give again alone. - let declared_changed = rsc.widgets().declared_lens(id) != active.declared; + // Asked where its parent asked it, which is what says whether the + // question is still this widget's own: its parent resolved its + // declared lengths into its box -- a bound of its own that the box + // falls outside is one of them -- and decided whether to draw it at + // all, so a change to either is the parent's to draw, with the mark + // left on so the parent draws it rather than keeping it. So is a + // widget the parent asked twice: its layout rests on an answer this + // widget cannot give again alone. The root's parent is the window, + // which no draw made and no answer can move. + let at = match active.parent { + 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 active = &self.active[&id]; + let declared_changed = ask.declared != active.declared; let alignment_changed = rsc.widgets().alignment(id) != active.own_align; if let Some(parent) = active.parent && (declared_changed @@ -1083,17 +1136,11 @@ impl UiRenderState { return true; } let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.placed); - // The question its parent asked, asked again: the same place of the - // box the parent was asked in, which is the box the parent's own - // draw ran in and what its children's parts are of. Where the - // parent's answer put its own drawing is not a question anybody - // asked, and nothing is asked in it here either. The root's parent is - // the window, which no draw made and no answer can move. - let at = match active.parent { - 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); + // The place the ask above came to: the same place of the box the + // parent was asked in, which is the box the parent's own draw ran in + // and what its children's parts are of. Where the parent's answer put + // its own drawing is not a question anybody asked, and nothing is + // asked in it here either. let (rel_base, region) = (ask.rel_base, ask.region); let info = DrawInfo { layer: active.layer, @@ -1106,6 +1153,8 @@ impl UiRenderState { region, placed: ask.place, asked: active.asked, + declared: ask.declared, + bounds: ask.bounds, ask_holds: ask.holds, re_asked: false, }; diff --git a/core/src/widget/size_rule.rs b/core/src/widget/size_rule.rs index 1d59154..0caab28 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}; +use crate::{Axis, LayoutLen, Len, Rel}; /// 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. @@ -9,6 +9,10 @@ use crate::{Axis, LayoutLen, Len}; /// 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. +/// +/// A rule gives a length or bounds one, never both: a share that is also +/// capped wants two widgets, one taking the share and one capping what is +/// inside it. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum SizeRule { /// Whatever the widget reports from drawing. @@ -16,9 +20,72 @@ pub enum SizeRule { Free, /// This length, whatever the widget reports. Exact(LayoutLen), + /// At least this long, and otherwise whatever the box gives it. + Min(Len), + /// At most this long. + Max(Len), + /// Between the two. + Clamp { min: Len, max: Len }, } impl SizeRule { + /// 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(_) => 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), + }, + } + } + + /// Whether what this rule says is a fraction of the rel base, so that + /// the same rule against a different one is a different length. + pub fn has_fraction(&self) -> bool { + let bound = self.bound(); + self.exact().is_some_and(|len| len.rel != Rel::ZERO) + || [bound.min, bound.max] + .iter() + .flatten() + .any(|len| len.rel != Rel::ZERO) + } + + /// This rule with a floor under it, which is the whole of it where there + /// was no rule. + pub fn at_least(&self, min: Len) -> Self { + match *self { + Self::Free | Self::Min(_) => Self::Min(min), + Self::Max(max) | Self::Clamp { max, .. } => Self::Clamp { min, max }, + Self::Exact(len) => { + debug_assert!(false, "{len:?} is a length, so bounding it says nothing"); + Self::Min(min) + } + } + } + + /// This rule with a cap over it, which is the whole of it where there was + /// no rule. + pub fn at_most(&self, max: Len) -> Self { + match *self { + Self::Free | Self::Max(_) => Self::Max(max), + Self::Min(min) | Self::Clamp { min, .. } => Self::Clamp { min, max }, + Self::Exact(len) => { + debug_assert!(false, "{len:?} is a length, so bounding it says nothing"); + Self::Max(max) + } + } + } + /// The length this rule gives without the widget being drawn, if it can /// give one. pub fn declared(&self) -> Option { @@ -32,12 +99,84 @@ impl SizeRule { /// that give a box directly. pub fn exact(&self) -> Option { match self { - Self::Free => None, Self::Exact(len) => Some(*len), + Self::Free | 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. +/// +/// 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. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Bound { + pub min: Option, + pub max: Option, +} + +/// Which end of a bound a length fell outside. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Outside { + Shorter, + Longer, +} + +impl Bound { + /// Every length. + pub const ANY: Self = Self { + min: None, + max: None, + }; + + /// The end [`Outside`] names, which is the length a widget outside it + /// gets instead of its own. + pub fn at(&self, outside: Outside) -> Len { + let end = match outside { + Outside::Shorter => self.min, + Outside::Longer => self.max, + }; + end.expect("an end nothing is outside of") + } + + /// This bound as lengths of the window, from lengths of a rel base that + /// long. + pub fn within_len(&self, len: Len) -> Self { + Self { + min: self.min.map(|min| min.within_len(len)), + max: self.max.map(|max| max.within_len(len)), + } + } +} + +/// One bound per axis, as [`SizeRules`] is one rule per axis. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct Bounds { + pub x: Bound, + pub y: Bound, +} + +impl Bounds { + pub const ANY: Self = Self { + x: Bound::ANY, + y: Bound::ANY, + }; + + pub fn from_axes(f: impl Fn(Axis) -> Bound) -> Self { + Self { + x: f(Axis::X), + y: f(Axis::Y), + } + } +} + +impl_axis_index!(Bounds => Bound); + impl From for SizeRule { fn from(len: LayoutLen) -> Self { Self::Exact(len) diff --git a/core/src/widget/widgets.rs b/core/src/widget/widgets.rs index 3433689..9ee6bf2 100644 --- a/core/src/widget/widgets.rs +++ b/core/src/widget/widgets.rs @@ -1,8 +1,8 @@ use std::sync::mpsc::{Receiver, Sender, channel}; use crate::{ - Axis, AxisAlign, IdLike, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget, - WidgetData, WidgetId, + Axis, AxisAlign, IdLike, Len, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, + Widget, WidgetData, WidgetId, util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut}, }; @@ -145,6 +145,22 @@ impl Widgets { self.needs_redraw.insert(id); } + /// 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) { + let id = id.id(); + let rule = self.size_rules(id)[axis].at_least(min); + self.set_size_rule(id, axis, rule); + } + + /// Puts a cap over it, keeping a floor it already had. See + /// [`SizeRule::at_most`]. + pub fn set_max_len(&mut self, id: impl IdLike, axis: Axis, max: Len) { + let id = id.id(); + let rule = self.size_rules(id)[axis].at_most(max); + self.set_size_rule(id, axis, rule); + } + /// Where this widget sits in a box longer than the length it takes. pub fn alignment(&self, id: impl IdLike) -> RegionAlign { self.data(id).unwrap().align diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 98b13cf..2a61bc2 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -18,6 +18,7 @@ struct Input { } struct InputFn { + attrs: Vec, sig: Signature, body: Block, } @@ -32,9 +33,10 @@ impl Parse for Input { input.parse::()?; let mut fns = Vec::new(); while !input.is_empty() { + let attrs = input.call(Attribute::parse_outer)?; let sig = input.parse()?; let body = input.parse()?; - fns.push(InputFn { sig, body }) + fns.push(InputFn { attrs, sig, body }) } if !input.is_empty() { input.error("function expected"); @@ -59,10 +61,15 @@ pub fn widget_trait(input: TokenStream) -> TokenStream { fns, } = parse_macro_input!(input as Input); - let sigs: Vec<_> = fns.iter().map(|f| f.sig.clone()).collect(); + // What a method says about itself belongs on the trait, where a reader + // looks it up; the implementation is the same text and says it again. + let sigs: Vec<_> = fns + .iter() + .map(|InputFn { attrs, sig, .. }| quote! { #(#attrs)* #sig }) + .collect(); let impls: Vec<_> = fns .iter() - .map(|InputFn { sig, body }| quote! { #sig #body }) + .map(|InputFn { attrs, sig, body }| quote! { #(#attrs)* #sig #body }) .collect(); let Some(GenericParam::Type(state)) = generics.params.first() else { diff --git a/src/random.rs b/src/random.rs index d61d24a..5c6a256 100644 --- a/src/random.rs +++ b/src/random.rs @@ -644,11 +644,40 @@ impl Sow<'_> { }) } - fn len(&mut self) -> Option { - match self.rng.below(4) { - 0 => Some(LayoutLen::px(20.0 + self.rng.below(180) as f32)), - 1 => Some(LayoutLen::LEFTOVER), - _ => None, + fn len(&mut self) -> LayoutLen { + LayoutLen::px(20.0 + self.rng.below(180) as f32) + } + + /// A length of a box rather than a length of the window, which is what a + /// bound is. Both kinds, since which of a fraction and a box is longer + /// turns on the window and a bound in pixels never changes sides. + fn bound(&mut self) -> Len { + match self.rng.chance() { + true => Len::px(20.0 + self.rng.below(180) as f32), + false => Len::rel(0.2 + self.rng.below(12) as f32 / 10.0), + } + } + + fn rule(&mut self) -> SizeRule { + match self.rng.below(8) { + 0 | 1 => self.len().into(), + 2 => LayoutLen::LEFTOVER.into(), + 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. + 5 => { + let (a, b) = ( + 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::Free, } } @@ -674,8 +703,8 @@ impl Sow<'_> { fn sized(&mut self, inner: &mut Plan) { let take = self.rng.chance(); let lens = SizeRules { - x: self.len().into(), - y: self.len().into(), + x: self.rule(), + y: self.rule(), }; if !take || inner.size.is_some() { return; diff --git a/src/widget/image.rs b/src/widget/image.rs index 6145f33..02a67b9 100644 --- a/src/widget/image.rs +++ b/src/widget/image.rs @@ -17,7 +17,7 @@ impl Widget for Image { } impl Image { - /// One texture already uploaded, for a caller holding its handle: [`image`] + /// One texture already uploaded, for a caller holding its handle: [`image()`] /// uploads what it is given, and several widgets showing one picture want /// one upload and one slot between them. pub fn new(handle: TextureHandle) -> Self { diff --git a/src/widget/trait_fns.rs b/src/widget/trait_fns.rs index 620b78e..5b71855 100644 --- a/src/widget/trait_fns.rs +++ b/src/widget/trait_fns.rs @@ -71,6 +71,47 @@ widget_trait! { } } + /// At least this wide, and otherwise as wide as its box makes it. A + /// cap set beside it stands: the two make one rule. + fn min_width(self, len: impl Into) -> impl WidgetIdFn { + let len = len.into(); + move |state| { + let id = self.add(state); + state.ui_mut().widgets.set_min_len(id, Axis::X, len); + id + } + } + + fn min_height(self, len: impl Into) -> impl WidgetIdFn { + let len = len.into(); + move |state| { + let id = self.add(state); + state.ui_mut().widgets.set_min_len(id, Axis::Y, len); + id + } + } + + /// At most this wide: the widget is asked in the shorter of the cap and + /// the box it would have had, and answers no more than the cap even + /// where it drew past it. + fn max_width(self, len: impl Into) -> impl WidgetIdFn { + let len = len.into(); + 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 WidgetIdFn { + let len = len.into(); + move |state| { + let id = self.add(state); + state.ui_mut().widgets.set_max_len(id, Axis::Y, len); + id + } + } + fn height(self, len: impl Into) -> impl WidgetIdFn { let len = len.into(); move |state| { diff --git a/tests/cases/layout.rs b/tests/cases/layout.rs index 916b629..a7fb37f 100644 --- a/tests/cases/layout.rs +++ b/tests/cases/layout.rs @@ -296,11 +296,11 @@ impl Asked { fn a_share_is_a_minimum_wherever_nothing_divides_it() { for (rule, want) in [ (LayoutLen::LEFTOVER, 400), - (LayoutLen::px(50) + LayoutLen::LEFTOVER, 400), - (LayoutLen::px(500) + LayoutLen::LEFTOVER, 500), + (LayoutLen::px(50.0) + LayoutLen::LEFTOVER, 400), + (LayoutLen::px(500.0) + LayoutLen::LEFTOVER, 500), (LayoutLen::rel(0.5) + LayoutLen::LEFTOVER, 400), (LayoutLen::rel(2.0) + LayoutLen::LEFTOVER, 800), - (LayoutLen::px(500), 500), + (LayoutLen::px(500.0), 500), ] { let want = Px::from_int(want); for asked in Asked::ALL { @@ -323,7 +323,7 @@ fn a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing() { for wrapped in [false, true] { let mut h = Harness::new((400, 200)); let probe = rect(Color::RED).add(&mut h.rsc); - h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER); + h.set_len(probe, Axis::X, LayoutLen::px(500.0) + LayoutLen::LEFTOVER); match wrapped { true => h.set_root(probe.wrapper()), false => h.set_root(probe), @@ -339,11 +339,11 @@ fn a_share_past_the_box_is_decided_again_on_either_side_of_the_crossing() { h.frame(); assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}"); - h.set_len(probe, Axis::X, LayoutLen::px(50) + LayoutLen::LEFTOVER); + h.set_len(probe, Axis::X, LayoutLen::px(50.0) + LayoutLen::LEFTOVER); h.frame(); assert_eq!(width(&h), Px::from_int(400), "wrapped: {wrapped}"); - h.set_len(probe, Axis::X, LayoutLen::px(500) + LayoutLen::LEFTOVER); + h.set_len(probe, Axis::X, LayoutLen::px(500.0) + LayoutLen::LEFTOVER); h.frame(); assert_eq!(width(&h), Px::from_int(500), "wrapped: {wrapped}"); } @@ -756,7 +756,7 @@ fn only_a_pure_leftover_child_disappears_when_nothing_is_left() { let mut h = Harness::new((100, 20)); let fixed = rect(Color::RED).width(100).add(&mut h.rsc); let mixed = rect(Color::BLUE) - .width(LayoutLen::px(20) + LayoutLen::LEFTOVER) + .width(LayoutLen::px(20.0) + LayoutLen::LEFTOVER) .add(&mut h.rsc); h.set_root((fixed, mixed).span(Dir::RIGHT)); @@ -1004,3 +1004,129 @@ fn a_region_node_root_is_a_region_node() { h.frame(); assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(900)); } + +/// A cap is the shorter of itself and the box the widget would have had, and +/// a floor the longer of itself and that box. Asked at the root, under a +/// parent that divides nothing, and in a span, since the box comes of one ask +/// wherever the widget is. +#[test] +fn a_bound_decides_the_box_against_the_one_offered() { + let width = |rule: SizeRule, asked: Asked| { + let mut h = Harness::new((400, 200)); + let probe = rect(Color::RED).add(&mut h.rsc); + h.rsc.widgets_mut().set_size_rule(probe, Axis::X, rule); + match asked { + Asked::Root => h.set_root(probe), + Asked::Wrapped => h.set_root(probe.wrapper()), + Asked::InASpan => h.set_root((probe,).span(Dir::RIGHT)), + } + h.region(&probe).unwrap().size().x + }; + for (rule, want) in [ + // Shorter than the 400 box, so the cap decides it. + (SizeRule::Max(Len::px(300.0)), 300), + // Longer than it, so the box stands. + (SizeRule::Max(Len::px(500.0)), 400), + // Longer than the box, so the floor decides it and it overflows. + (SizeRule::Min(Len::px(500.0)), 500), + (SizeRule::Min(Len::px(300.0)), 400), + // Both at once are one rule, and the cap is the shorter here. + ( + SizeRule::Clamp { + min: Len::px(100.0), + max: Len::px(300.0), + }, + 300, + ), + ] { + for asked in Asked::ALL { + assert_eq!(width(rule, asked), Px::from_int(want), "asked {asked:?}"); + } + } +} + +/// A floor and a cap set one after the other are one rule, which is what lets +/// a caller say both without knowing about the third variant. +#[test] +fn a_floor_and_a_cap_set_apart_make_one_rule() { + let mut h = Harness::new((400, 200)); + let probe = rect(Color::RED) + .min_width(100) + .max_width(300) + .add(&mut h.rsc); + assert_eq!( + h.rsc.widgets().size_rules(probe)[Axis::X], + SizeRule::Clamp { + min: Len::px(100.0), + max: Len::px(300.0), + } + ); + + h.set_root(probe); + assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(300)); +} + +/// Which of the cap and the box is shorter is a question in pixels, so the +/// box is decided again wherever the answer can change -- at the root as much +/// as under a parent, since nothing above the root will ask again for it. +#[test] +fn a_bound_is_decided_again_on_either_side_of_the_crossing() { + for wrapped in [false, true] { + let mut h = Harness::new((400, 200)); + let probe = rect(Color::RED).add(&mut h.rsc); + h.rsc.widgets_mut().set_max_len(probe, Axis::X, 300.into()); + h.rsc.widgets_mut().set_min_len(probe, Axis::X, 200.into()); + match wrapped { + true => h.set_root(probe.wrapper()), + false => h.set_root(probe), + } + let width = |h: &Harness| h.region(&probe).unwrap().size().x; + assert_eq!(width(&h), Px::from_int(300), "wrapped: {wrapped}"); + + h.resize((250, 200)); + h.frame(); + assert_eq!(width(&h), Px::from_int(250), "wrapped: {wrapped}"); + + h.resize((100, 200)); + h.frame(); + assert_eq!(width(&h), Px::from_int(200), "wrapped: {wrapped}"); + + h.resize((400, 200)); + h.frame(); + assert_eq!(width(&h), Px::from_int(300), "wrapped: {wrapped}"); + } +} + +/// A fraction in a bound is a fraction of the same box a declared length +/// would be: the rel base the widget was asked with, and not the box the +/// bound itself decided. +#[test] +fn a_bound_is_a_fraction_of_the_box_the_widget_was_asked_in() { + let mut h = Harness::new((400, 200)); + let probe = rect(Color::RED).add(&mut h.rsc); + h.rsc + .widgets_mut() + .set_max_len(probe, Axis::X, Len::rel(0.5)); + h.set_root(probe.pad(Padding::uniform(50))); + + // Half of the 300 left by the padding, not half of the window and not + // half of itself. + assert_eq!(h.region(&probe).unwrap().size().x, Px::from_int(150)); +} + +/// A cap is a promise about the length as well as the box: a widget whose +/// content is longer than the box it was given reports what it drew, and the +/// cap holds that down even though it never decided the box. +#[test] +fn a_cap_holds_an_answer_that_overflowed_its_box() { + let mut h = Harness::new((250, 200)); + let left = rect(Color::RED).width(200).add(&mut h.rsc); + let right = rect(Color::BLUE).width(200).add(&mut h.rsc); + let row = (left, right).span(Dir::RIGHT).add(&mut h.rsc); + h.rsc.widgets_mut().set_max_len(row, Axis::X, 300.into()); + h.set_root(row); + + // The box is the 250 window, which the cap of 300 leaves alone, and the + // row draws 400 of it. Its answer is the cap, and the window centres it. + assert_corners!(h, row, (-25, 0), (275, 200)); +} diff --git a/tests/cases/scroll.rs b/tests/cases/scroll.rs index 0b7b962..576b59f 100644 --- a/tests/cases/scroll.rs +++ b/tests/cases/scroll.rs @@ -161,3 +161,26 @@ fn content_that_fits_is_placed_in_the_viewport_and_not_in_the_window() { assert_corners!(h, scroll, (0, 100), (400, 400)); assert_corners!(h, inner, (0, 225), (400, 275)); } + +/// A cap narrows the box the widget is asked in, which is what a scroll +/// measures its viewport from: the content scrolls within the cap rather than +/// within the room the cap was cut from. +#[test] +fn a_capped_scroll_takes_its_viewport_from_the_cap() { + let mut h = Harness::new((400, 200)); + let top = rect(Color::RED).height(200).add(&mut h.rsc); + let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc); + let scroll = (top, bottom).span(Dir::DOWN).scrollable().add(&mut h.rsc); + h.rsc.widgets_mut().set_max_len(scroll, Axis::Y, 100.into()); + h.set_root(scroll); + h.move_to((200, 50)); + + // 400 of content in a viewport of 100, so 300 to scroll and the end + // showing: the top is 300 above the box, which the window centres. + assert_eq!(h.region(&scroll).unwrap().size().y, Px::from_int(100)); + assert_corners!(h, top, (0, -250), (400, -50)); + + h.scroll((0, 1)); + h.frame(); + assert_corners!(h, top, (0, -200), (400, 0)); +} diff --git a/tests/scenario/mod.rs b/tests/scenario/mod.rs index 0b7b745..5c51a14 100644 --- a/tests/scenario/mod.rs +++ b/tests/scenario/mod.rs @@ -194,14 +194,22 @@ fn mark(warm: &mut Harness, tree: &Tree, step: usize) { } } -fn a_len(rng: &mut Rng) -> Option { - Some(LayoutLen::px(20.0 + rng.below(180) as f32)) +/// A length in pixels, or a cap over one: a rule that reads the box it is +/// given is the one a resize can change the effect of without changing the +/// rule, so a tree that never grows one leaves that unexercised. +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), + _ => LayoutLen::from(len).into(), + } } fn resize_one(warm: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> SizeRules { let lens = SizeRules { - x: a_len(rng).into(), - y: a_len(rng).into(), + x: a_rule(rng), + y: a_rule(rng), }; warm.rsc .widgets_mut() @@ -349,9 +357,16 @@ 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); - let rule = |r: SizeRule| match r.exact() { - Some(len) => format!("{len}"), - None => "-".into(), + // 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::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 align = h.rsc.widgets().alignment(id); let side = |a: AxisAlign| {