From 1f15125992db09455d4850a3d2da3402973adc9a Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sat, 12 Sep 2026 19:35:18 -0400 Subject: [PATCH] iris: separate layout allocation from lengths --- benches/message_list.rs | 8 +- core/src/orientation/len.rs | 234 +++++++++++++++++++++++++------ core/src/ui/painter.rs | 10 +- core/src/ui/render_state.rs | 6 +- core/src/widget/mod.rs | 8 +- src/layout_tests.rs | 99 +++++++++---- src/rsc/sense_tests.rs | 12 +- src/widget/image.rs | 4 +- src/widget/layout/lazy_span.rs | 14 +- src/widget/layout/max_size.rs | 8 +- src/widget/layout/pad.rs | 112 ++++++++++----- src/widget/layout/scroll_area.rs | 2 +- src/widget/layout/sized.rs | 6 +- src/widget/layout/span.rs | 24 ++-- src/widget/layout/stack.rs | 4 +- src/widget/rect.rs | 4 +- src/widget/trait_fns.rs | 8 +- 17 files changed, 395 insertions(+), 168 deletions(-) diff --git a/benches/message_list.rs b/benches/message_list.rs index 92e76eb..b675081 100644 --- a/benches/message_list.rs +++ b/benches/message_list.rs @@ -140,7 +140,7 @@ fn bench_input_grows(n: usize, lines: usize) { let input_area = rsc.ui.widgets.add_strong(Sized { inner: input_rect.any(), x: None, - y: Some(abs(line_height)), + y: Some(abs(line_height).into()), }); let input_area_weak = input_area.weak(); @@ -162,7 +162,7 @@ fn bench_input_grows(n: usize, lines: usize) { let mut total_moves = 0u64; for line in 1..=lines { rsc.ui.widgets.get_mut(&input_area_weak).unwrap().y = - Some(abs(line_height * (line + 1) as f32)); + Some(abs(line_height * (line + 1) as f32).into()); let start = Instant::now(); render.update(&root, &mut rsc); total += start.elapsed(); @@ -253,7 +253,7 @@ fn bench_expand_holds_edge(n: usize, growths: usize) { let sized = rsc.ui.widgets.add_strong(Sized { inner: rect.any(), x: None, - y: Some(abs(40.0)), + y: Some(abs(40.0).into()), }); growable = Some(sized.weak()); list.push_back(LazyItem::new(i as u64, sized.any())); @@ -287,7 +287,7 @@ fn bench_expand_holds_edge(n: usize, growths: usize) { .unwrap() .note_tap(top + 1.0); } - rsc.ui.widgets.get_mut(&growable).unwrap().y = Some(abs(height)); + rsc.ui.widgets.get_mut(&growable).unwrap().y = Some(abs(height).into()); let start = Instant::now(); render.update(&root, &mut rsc); total += start.elapsed(); diff --git a/core/src/orientation/len.rs b/core/src/orientation/len.rs index d426edd..0d081ec 100644 --- a/core/src/orientation/len.rs +++ b/core/src/orientation/len.rs @@ -3,10 +3,13 @@ use crate::{UiNum, util::impl_op}; #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct Size { - pub x: Len, - pub y: Len, + pub x: LayoutLen, + pub y: LayoutLen, } +/// A length resolved from physical pixels, density-independent pixels, and a +/// fraction of a reference length. Unlike [`LayoutLen`], it carries no claim +/// on space left over by a layout container. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Len { /// Physical pixels -- a raw device pixel, unaffected by the display's @@ -17,6 +20,15 @@ pub struct Len { pub abs: f32, pub dp: f32, pub rel: f32, +} + +/// A widget length plus its proportional claim on the space left after fixed +/// and relative lengths have been allocated. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct LayoutLen { + pub abs: f32, + pub dp: f32, + pub rel: f32, pub rest: f32, } @@ -26,8 +38,25 @@ impl From for Len { } } -impl From<(Nx, Ny)> for Size { - fn from((x, y): (Nx, Ny)) -> Self { +impl From for LayoutLen { + fn from(value: N) -> Self { + Self::abs(value.to_f32()) + } +} + +impl From for LayoutLen { + fn from(value: Len) -> Self { + Self { + abs: value.abs, + dp: value.dp, + rel: value.rel, + rest: 0.0, + } + } +} + +impl, Y: Into> From<(X, Y)> for Size { + fn from((x, y): (X, Y)) -> Self { Self { x: x.into(), y: y.into(), @@ -35,41 +64,47 @@ impl From<(Nx, Ny)> for Size { } } +impl From for Size { + fn from(value: LayoutLen) -> Self { + Self { x: value, y: value } + } +} + impl From for Size { fn from(value: Len) -> Self { - Self { x: value, y: value } + Self::from(LayoutLen::from(value)) } } impl Size { pub const ZERO: Self = Self { - x: Len::ZERO, - y: Len::ZERO, + x: LayoutLen::ZERO, + y: LayoutLen::ZERO, }; pub const REST: Self = Self { - x: Len::REST, - y: Len::REST, + x: LayoutLen::REST, + y: LayoutLen::REST, }; pub fn abs(v: Vec2) -> Self { Self { - x: Len::abs(v.x), - y: Len::abs(v.y), + x: LayoutLen::abs(v.x), + y: LayoutLen::abs(v.y), } } pub fn rel(v: Vec2) -> Self { Self { - x: Len::rel(v.x), - y: Len::rel(v.y), + x: LayoutLen::rel(v.x), + y: LayoutLen::rel(v.y), } } pub fn rest(v: Vec2) -> Self { Self { - x: Len::rest(v.x), - y: Len::rest(v.y), + x: LayoutLen::rest(v.x), + y: LayoutLen::rest(v.y), } } @@ -80,7 +115,7 @@ impl Size { } } - pub fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self { + pub fn from_axis(axis: Axis, aligned: LayoutLen, ortho: LayoutLen) -> Self { match axis { Axis::X => Self { x: aligned, @@ -93,7 +128,7 @@ impl Size { } } - pub fn axis(&self, axis: Axis) -> Len { + pub fn axis(&self, axis: Axis) -> LayoutLen { match axis { Axis::X => self.x, Axis::Y => self.y, @@ -101,7 +136,7 @@ impl Size { } } -impl Len { +impl LayoutLen { pub const ZERO: Self = Self { abs: 0.0, dp: 0.0, @@ -119,7 +154,7 @@ impl Len { /// Resolves to a `UiScalar`, folding `dp` into `abs` pixels against /// `density` (physical pixels per dp -- 1.0 on a desktop or an /// unscaled display, `content_scale` on Android; see `dp`'s field - /// doc). Every other component of `Len` is already resolution- + /// doc). Every other component of `LayoutLen` is already resolution- /// independent (`rel` is a fraction of the parent; `rest` becomes a /// fraction too, below), so `density` only ever touches this one term. pub fn apply_rest(&self, density: f32) -> UiScalar { @@ -129,11 +164,11 @@ impl Len { } } - /// The same fold as [`Self::apply_rest`] but staying a `Len`, so + /// The same fold as [`Self::apply_rest`] but staying a `LayoutLen`, so /// `rest` survives: `dp` becomes physical pixels and every other /// component is left alone. /// - /// **A `Len` a widget *reports* must have been through this.** `dp` is + /// **A `LayoutLen` a widget *reports* must have been through this.** `dp` is /// an input unit -- a number the widget author wrote -- and the /// containers that consume a reported length read `abs`/`rel`/`rest` /// directly (`Span::draw`'s placement arithmetic, `Pad`'s addition), @@ -186,35 +221,68 @@ impl Len { } } +impl Len { + pub const ZERO: Self = Self { + abs: 0.0, + dp: 0.0, + rel: 0.0, + }; + + pub fn abs(abs: impl UiNum) -> Self { + Self { + abs: abs.to_f32(), + dp: 0.0, + rel: 0.0, + } + } + + pub fn dp(dp: impl UiNum) -> Self { + Self { + abs: 0.0, + dp: dp.to_f32(), + rel: 0.0, + } + } + + pub fn rel(rel: impl UiNum) -> Self { + Self { + abs: 0.0, + dp: 0.0, + rel: rel.to_f32(), + } + } + + pub const fn fold_dp(self, density: f32) -> Self { + Self { + abs: self.abs + self.dp * density, + dp: 0.0, + rel: self.rel, + } + } + + pub const fn resolve(self, density: f32) -> UiScalar { + let folded = self.fold_dp(density); + UiScalar { + rel: folded.rel, + abs: folded.abs, + } + } +} + pub mod len_fns { use super::*; pub fn abs(abs: impl UiNum) -> Len { - Len { - abs: abs.to_f32(), - dp: 0.0, - rel: 0.0, - rest: 0.0, - } + Len::abs(abs) } pub fn dp(dp: impl UiNum) -> Len { - Len { - abs: 0.0, - dp: dp.to_f32(), - rel: 0.0, - rest: 0.0, - } + Len::dp(dp) } pub fn rel(rel: impl UiNum) -> Len { - Len { - abs: 0.0, - dp: 0.0, - rel: rel.to_f32(), - rest: 0.0, - } + Len::rel(rel) } - pub fn rest(ratio: impl UiNum) -> Len { - Len { + pub fn rest(ratio: impl UiNum) -> LayoutLen { + LayoutLen { abs: 0.0, dp: 0.0, rel: 0.0, @@ -223,25 +291,49 @@ pub mod len_fns { } } -impl_op!(Len Add add; abs dp rel rest); -impl_op!(Len Sub sub; abs dp rel rest); +impl_op!(LayoutLen Add add; abs dp rel rest); +impl_op!(LayoutLen Sub sub; abs dp rel rest); +impl_op!(Len Add add; abs dp rel); +impl_op!(Len Sub sub; abs dp rel); + +impl std::ops::Add for LayoutLen { + type Output = Self; + + fn add(self, rhs: Len) -> Self::Output { + self + Self::from(rhs) + } +} + +impl std::ops::Sub for LayoutLen { + type Output = Self; + + fn sub(self, rhs: Len) -> Self::Output { + self - Self::from(rhs) + } +} impl_op!(Size Add add; x y); impl_op!(Size Sub sub; x y); -impl Default for Len { +impl Default for LayoutLen { fn default() -> Self { Self::rest(1.0) } } +impl Default for Len { + fn default() -> Self { + Self::ZERO + } +} + impl std::fmt::Display for Size { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "({}, {})", self.x, self.y) } } -impl std::fmt::Display for Len { +impl std::fmt::Display for LayoutLen { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if self.abs != 0.0 { write!(f, "{} abs;", self.abs)?; @@ -258,3 +350,55 @@ impl std::fmt::Display for Len { Ok(()) } } + +impl std::fmt::Display for Len { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.abs != 0.0 { + write!(f, "{} abs;", self.abs)?; + } + if self.dp != 0.0 { + write!(f, "{} dp;", self.dp)?; + } + if self.rel != 0.0 { + write!(f, "{} rel;", self.rel)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn an_ordinary_length_enters_layout_without_claiming_rest() { + let layout = LayoutLen::from(Len { + abs: 3.0, + dp: 4.0, + rel: 0.5, + }); + assert_eq!(layout.abs, 3.0); + assert_eq!(layout.dp, 4.0); + assert_eq!(layout.rel, 0.5); + assert_eq!(layout.rest, 0.0); + } + + #[test] + fn ordinary_and_layout_lengths_keep_their_own_defaults() { + assert_eq!(Len::default(), Len::ZERO); + assert_eq!(LayoutLen::default(), LayoutLen::REST); + } + + #[test] + fn adding_an_ordinary_length_preserves_a_layout_claim() { + assert_eq!( + LayoutLen::rest(2) + Len::dp(8), + LayoutLen { + abs: 0.0, + dp: 8.0, + rel: 0.0, + rest: 2.0, + } + ); + } +} diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 9d2989f..0d74380 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -1,7 +1,7 @@ use crate::{ - Axis, Len, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, TextHandle, - TextResources, TextureHandle, UiData, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, - WidgetId, + Axis, LayoutLen, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget, + TextHandle, TextResources, TextureHandle, UiData, UiRegion, UiRenderState, UiRsc, UiScalar, + UiVec2, WidgetId, render::{ Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive, PrimitiveHandle, PrimitiveInst, RectPrimitive, @@ -253,7 +253,7 @@ impl<'a> Painter<'a> { } } - pub fn known_len(&mut self, id: &StrongWidget, axis: Axis) -> Option { + pub fn known_len(&mut self, id: &StrongWidget, axis: Axis) -> Option { let len = if let Some(len) = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) { Some(len.fold_dp(self.density())) } else if !self.reuse_child_sizes[match axis { @@ -480,7 +480,7 @@ impl<'a> Painter<'a> { } /// Physical pixels per `dp` -- see `UiRenderState::density`'s field - /// doc. What `Len::dp`'s `apply_rest` call resolves against. + /// doc. What `LayoutLen::dp`'s `apply_rest` call resolves against. pub fn density(&self) -> f32 { self.render_state.density } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 4f047b2..3a20068 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -31,7 +31,7 @@ pub struct UiRenderState { pub primitives: Primitives, pub layers: PrimitiveLayers, pub(super) output_size: Vec2, - /// Physical pixels per `dp` -- see `Len::dp`'s field doc. `1.0` (an + /// Physical pixels per `dp` -- see `LayoutLen::dp`'s field doc. `1.0` (an /// unscaled display) until a backend that knows its own density calls /// `set_density` (Android's `content_scale`, read at `surface_changed` /// time); the winit backend has no analogous per-monitor value wired up @@ -207,7 +207,7 @@ impl UiRenderState { self.resized = true; } - /// Sets the physical-pixels-per-dp ratio every `Len::dp` in the tree + /// Sets the physical-pixels-per-dp ratio every `LayoutLen::dp` in the tree /// resolves against from the next layout pass on -- see `density`'s /// field doc. Not folded into `resize` because the two change on /// different triggers (a surface resize on every rotation or keyboard @@ -485,7 +485,7 @@ impl UiRenderState { debug_assert!( size.x.dp == 0.0 && size.y.dp == 0.0, "widget {id:?} reported an unresolved `dp` size ({size:?}); \ - report `Len::fold_dp(painter.density())` instead" + report `LayoutLen::fold_dp(painter.density())` instead" ); for (axis, hint) in [Axis::X, Axis::Y] .into_iter() diff --git a/core/src/widget/mod.rs b/core/src/widget/mod.rs index 0e4acd8..425a175 100644 --- a/core/src/widget/mod.rs +++ b/core/src/widget/mod.rs @@ -1,4 +1,4 @@ -use crate::{Axis, Len, Painter, Size}; +use crate::{Axis, LayoutLen, Painter, Size}; use std::any::Any; mod data; @@ -28,7 +28,7 @@ pub enum ChildOrder { pub trait Widget: Any { fn draw(&mut self, painter: &mut Painter); - fn size_hint(&self, _axis: Axis) -> Option { + fn size_hint(&self, _axis: Axis) -> Option { None } @@ -63,8 +63,8 @@ impl Widget for () { true } - fn size_hint(&self, _axis: Axis) -> Option { - Some(Len::ZERO) + fn size_hint(&self, _axis: Axis) -> Option { + Some(LayoutLen::ZERO) } } diff --git a/src/layout_tests.rs b/src/layout_tests.rs index 3cdfae4..d9849f7 100644 --- a/src/layout_tests.rs +++ b/src/layout_tests.rs @@ -18,7 +18,7 @@ struct FixedRect(f32); impl Widget for FixedRect { fn draw(&mut self, painter: &mut Painter) { - let size = Size::from_axis(Axis::Y, Len::abs(self.0), Len::REST); + let size = Size::from_axis(Axis::Y, LayoutLen::abs(self.0), LayoutLen::REST); let paint = painter.paint(&PaintId::WHITE); painter.primitive_within( RectPrimitive::color(paint), @@ -50,7 +50,11 @@ struct TracedLeaf { impl Widget for TracedLeaf { fn draw(&mut self, painter: &mut Painter) { self.trace.borrow_mut().push("child"); - painter.set_size(Size::from_axis(Axis::Y, Len::abs(self.height), Len::REST)); + painter.set_size(Size::from_axis( + Axis::Y, + LayoutLen::abs(self.height), + LayoutLen::REST, + )); } } @@ -68,7 +72,11 @@ struct CountedLeaf { impl Widget for CountedLeaf { fn draw(&mut self, painter: &mut Painter) { self.draws.set(self.draws.get() + 1); - painter.set_size(Size::from_axis(Axis::Y, Len::abs(self.height), Len::REST)); + painter.set_size(Size::from_axis( + Axis::Y, + LayoutLen::abs(self.height), + LayoutLen::REST, + )); } } @@ -146,7 +154,10 @@ fn a_size_dependent_parent_is_invalidated_before_layout_runs_downward() { render.update(&root, &mut rsc); assert_eq!(&*trace.borrow(), &["parent", "child"]); - assert_eq!(render.active[&parent_weak.id()].size.y, Len::abs(40.0)); + assert_eq!( + render.active[&parent_weak.id()].size.y, + LayoutLen::abs(40.0) + ); } #[test] @@ -192,7 +203,7 @@ fn a_span_reuses_unchanged_sibling_sizes_when_only_its_along_extent_changes() { let span = rsc.ui.widgets.add_strong(Span { children: vec![changed.any(), sibling.any()], dir: Dir::DOWN, - gap: Len::ZERO, + gap: LayoutLen::ZERO, }); let root = rsc .ui @@ -275,7 +286,7 @@ fn a_hinted_rest_draws_once_and_only_moves_the_fixed_child_after_it() { let fill = rsc.ui.widgets.add_strong(Sized { inner: fill.any(), x: None, - y: Some(Len::REST), + y: Some(LayoutLen::REST), }); let last = rsc.ui.widgets.add_strong(FixedRect(40.0)); let last_w = last.weak(); @@ -308,7 +319,7 @@ fn scrolled_rects( let row = rsc.ui.widgets.add_strong(Sized { inner: rect.any(), x: None, - y: Some(Len::abs(10.0)), + y: Some(LayoutLen::abs(10.0)), }); span.push(row.any()); } @@ -531,7 +542,7 @@ fn a_scroll_measures_the_box_it_was_offered_not_the_window() { let tall = rsc.ui.widgets.add_strong(Sized { inner: rect.any(), x: None, - y: Some(Len::abs(1000.0)), + y: Some(LayoutLen::abs(1000.0)), }); let scroll = rsc .ui @@ -542,7 +553,7 @@ fn a_scroll_measures_the_box_it_was_offered_not_the_window() { let capped = rsc.ui.widgets.add_strong(MaxSize { inner: scroll.any(), x: None, - y: Some(Len::abs(100.0)), + y: Some(LayoutLen::abs(100.0)), }); let capped_id = capped.id(); let root = capped.any(); @@ -555,11 +566,11 @@ fn a_scroll_measures_the_box_it_was_offered_not_the_window() { assert_eq!( render.active.get(&scroll_id).unwrap().size.y, - Len::abs(1000.0) + LayoutLen::abs(1000.0) ); assert_eq!( render.active.get(&capped_id).unwrap().size.y, - Len::abs(100.0), + LayoutLen::abs(100.0), "the cap, not the content and not the window" ); @@ -579,7 +590,7 @@ fn a_panned_widgets_own_hit_box_moves_exactly_once() { let tall = rsc.ui.widgets.add_strong(Sized { inner: rect.any(), x: None, - y: Some(Len::abs(1000.0)), + y: Some(LayoutLen::abs(1000.0)), }); let tall_w = tall.weak(); let scroll = rsc @@ -624,7 +635,7 @@ fn a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region() { let capped = rsc.ui.widgets.add_strong(MaxSize { inner: masked.any(), x: None, - y: Some(Len::abs(60.0)), + y: Some(LayoutLen::abs(60.0)), }); let mut span = Span::empty(Dir::DOWN); span.push(filler.any()); @@ -658,12 +669,12 @@ fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() { let tall = rsc.ui.widgets.add_strong(Sized { inner: rect.any(), x: None, - y: Some(Len::abs(1000.0)), + y: Some(LayoutLen::abs(1000.0)), }); let capped = rsc.ui.widgets.add_strong(MaxSize { inner: tall.any(), x: None, - y: Some(Len::dp(100.0)), + y: Some(LayoutLen::dp(100.0)), }); let capped_w = capped.weak(); let filler = rsc.ui.widgets.add_strong(Rect::new(PaintId::BLACK)); @@ -698,7 +709,7 @@ fn a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at( let spacer = rsc.ui.widgets.add_strong(Sized { inner: top.any(), x: None, - y: Some(Len::abs(100.0)), + y: Some(LayoutLen::abs(100.0)), }); let spacer_w = spacer.weak(); let below = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)); @@ -718,7 +729,7 @@ fn a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at( first.top_left ); - rsc.ui.widgets.get_mut(&spacer_w).unwrap().y = Some(Len::abs(250.0)); + rsc.ui.widgets.get_mut(&spacer_w).unwrap().y = Some(LayoutLen::abs(250.0)); render.update(&root, &mut rsc); let after = render.window_region(&below_w, &rsc).unwrap(); assert!( @@ -763,7 +774,7 @@ fn a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement let child = rsc.ui.widgets.add_strong(Sized { inner: rect.any(), x: None, - y: Some(Len::abs(40.0)), + y: Some(LayoutLen::abs(40.0)), }); let child_w = child.weak(); let parent = rsc.ui.widgets.add_strong(MoveThenPlace { @@ -998,7 +1009,7 @@ fn a_scroll_area_opens_at_the_start_of_content_it_has_not_measured_yet() { let tall = rsc.ui.widgets.add_strong(Sized { inner: fill, x: None, - y: Some(Len::abs(5000.0)), + y: Some(LayoutLen::abs(5000.0)), }); let scroll = rsc .ui @@ -1032,7 +1043,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() { let header = rsc.ui.widgets.add_strong(Sized { inner: header_fill, x: None, - y: Some(Len::abs(HEADER)), + y: Some(LayoutLen::abs(HEADER)), }); let mut inner = Span::empty(Dir::DOWN); let mut rects = Vec::new(); @@ -1042,7 +1053,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() { let sized = rsc.ui.widgets.add_strong(Sized { inner: rect.any(), x: None, - y: Some(Len::abs(ROW)), + y: Some(LayoutLen::abs(ROW)), }); let padded = rsc.ui.widgets.add_strong(Pad { padding: Padding::uniform(PAD), @@ -1056,7 +1067,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() { }); let wide = rsc.ui.widgets.add_strong(Sized { inner: card.any(), - x: Some(Len::rest(1.0)), + x: Some(LayoutLen::rest(1.0)), y: None, }); inner.push(wide.any()); @@ -1065,7 +1076,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() { let outer = rsc.ui.widgets.add_strong(Span { children: vec![header.any(), inner.any()], dir: Dir::DOWN, - gap: Len::ZERO, + gap: LayoutLen::ZERO, }); let mut list = LazySpan::new(Dir::DOWN, Pin::End); list.push_back(LazyItem::new(0, outer.any())); @@ -1101,6 +1112,40 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() { } } +#[test] +fn rest_padding_claims_leftover_space_around_fixed_content() { + let mut rsc = TestRsc { ui: Ui::default() }; + let content = rect(PaintId::WHITE).sized((20, 10)).add_strong(&mut rsc); + let content_id = content.weak(); + let padded = rsc.ui.widgets.add_strong(Pad { + inner: content.any(), + padding: Padding { + left: LayoutLen::rest(0.5), + right: LayoutLen::rest(0.5), + ..Padding::ZERO + }, + exact_region: false, + }); + let root = rsc + .ui + .widgets + .add_strong(Sized { + inner: padded.any(), + x: Some(LayoutLen::abs(100)), + y: None, + }) + .any(); + let mut render = UiRenderState::new(); + render.resize((100.0, 40.0)); + + render.update(&root, &mut rsc); + render.update(&root, &mut rsc); + + let region = render.window_region(&content_id, &rsc).unwrap(); + assert_eq!(region.top_left.x, 40.0); + assert_eq!(region.bot_right.x, 60.0); +} + #[test] fn a_new_child_in_a_growing_lazy_row_uses_its_final_box_immediately() { const FIRST: f32 = 30.0; @@ -1113,15 +1158,15 @@ fn a_new_child_in_a_growing_lazy_row_uses_its_final_box_immediately() { let first = rsc.ui.widgets.add_strong(Sized { inner: first.any(), x: None, - y: Some(Len::abs(FIRST)), + y: Some(LayoutLen::abs(FIRST)), }); - let mut contents = Span::empty(Dir::DOWN).gap(Len::abs(GAP)); + let mut contents = Span::empty(Dir::DOWN).gap(LayoutLen::abs(GAP)); contents.push(first.any()); let contents = rsc.ui.widgets.add_strong(contents); let contents_w = contents.weak(); let row = rsc.ui.widgets.add_strong(Sized { inner: contents.any(), - x: Some(Len::rest(1.0)), + x: Some(LayoutLen::rest(1.0)), y: None, }); let mut list = LazySpan::new(Dir::DOWN, Pin::End); @@ -1137,7 +1182,7 @@ fn a_new_child_in_a_growing_lazy_row_uses_its_final_box_immediately() { let second = rsc.ui.widgets.add_strong(Sized { inner: second.any(), x: None, - y: Some(Len::abs(SECOND)), + y: Some(LayoutLen::abs(SECOND)), }); rsc.ui .widgets diff --git a/src/rsc/sense_tests.rs b/src/rsc/sense_tests.rs index 5e281d7..21f7750 100644 --- a/src/rsc/sense_tests.rs +++ b/src/rsc/sense_tests.rs @@ -277,7 +277,7 @@ fn a_finger_drag_over_a_scroll_area_pans_it() { }; let scroll_strong = rect(PaintId::WHITE) - .height(Len::abs(1000.0)) + .height(LayoutLen::abs(1000.0)) .scrollable(Axis::Y, Pin::Start) .add_strong(&mut rsc); let scroll = scroll_strong.weak(); @@ -367,7 +367,7 @@ fn a_scroll_area_that_captured_the_pointer_learns_its_gesture_ended() { events: EventManager::default(), }; let scroll_strong = rect(PaintId::WHITE) - .height(Len::abs(1000.0)) + .height(LayoutLen::abs(1000.0)) .scrollable(Axis::Y, Pin::Start) .add_strong(&mut rsc); let scroll = scroll_strong.weak(); @@ -522,8 +522,8 @@ fn a_drag_pans_whichever_nested_scroll_area_owns_its_axis() { let seen = Rc::new(Cell::new(None)); let record = seen.clone(); let outer_strong = rect(PaintId::WHITE) - .width(Len::abs(1000.0)) - .height(Len::abs(1000.0)) + .width(LayoutLen::abs(1000.0)) + .height(LayoutLen::abs(1000.0)) .scrollable(Axis::X, Pin::Start) .with_id(move |_rsc, id| { record.set(Some(id)); @@ -581,13 +581,13 @@ fn a_press_does_not_reach_a_widget_the_pointer_has_just_left() { let half = |slot: &Rc>>>| { let record = slot.clone(); rect(PaintId::WHITE) - .height(Len::abs(1000.0)) + .height(LayoutLen::abs(1000.0)) .scrollable(Axis::Y, Pin::Start) .with_id(move |_rsc, id| { record.set(Some(id)); id }) - .height(Len::rel(0.5)) + .height(LayoutLen::rel(0.5)) }; let root = (half(&seen[0]), half(&seen[1])) .span(Dir::DOWN) diff --git a/src/widget/image.rs b/src/widget/image.rs index 64fa4be..77fe446 100644 --- a/src/widget/image.rs +++ b/src/widget/image.rs @@ -12,8 +12,8 @@ impl Widget for Image { painter.set_size(Size::abs(size)); } - fn size_hint(&self, axis: Axis) -> Option { - Some(Len::abs(self.handle.size().axis(axis))) + fn size_hint(&self, axis: Axis) -> Option { + Some(LayoutLen::abs(self.handle.size().axis(axis))) } fn is_size_independent(&self) -> bool { diff --git a/src/widget/layout/lazy_span.rs b/src/widget/layout/lazy_span.rs index 7897edb..37a6e54 100644 --- a/src/widget/layout/lazy_span.rs +++ b/src/widget/layout/lazy_span.rs @@ -811,8 +811,8 @@ impl Widget for LazySpan { painter.set_size(Size::REST); } - fn size_hint(&self, _axis: Axis) -> Option { - Some(Len::REST) + fn size_hint(&self, _axis: Axis) -> Option { + Some(LayoutLen::REST) } } @@ -850,7 +850,7 @@ mod tests { let sized = rsc.ui.widgets.add_strong(Sized { inner: rect.any(), x: None, - y: Some(Len::abs(height)), + y: Some(LayoutLen::abs(height)), }); (sized.weak(), sized.any()) } @@ -1054,7 +1054,7 @@ mod tests { let fg = rsc.ui.widgets.add_strong(Sized { inner: fg_rect.any(), x: None, - y: Some(Len::abs(height)), + y: Some(LayoutLen::abs(height)), }); let fg_weak = fg.weak(); let stack = Stack { @@ -1084,7 +1084,7 @@ mod tests { render.update(&root, &mut rsc); let (bg_id, fg) = rows[key_to_change as usize]; - rsc.ui.widgets.get_mut(&fg).unwrap().y = Some(Len::abs(new_height)); + rsc.ui.widgets.get_mut(&fg).unwrap().y = Some(LayoutLen::abs(new_height)); render.update(&root, &mut rsc); let px = render @@ -1190,7 +1190,7 @@ mod tests { } rsc.ui.widgets.get_mut(&list_weak).unwrap().note_tap(41.0); - rsc.ui.widgets.get_mut(&row2).unwrap().y = Some(Len::abs(50.0)); + rsc.ui.widgets.get_mut(&row2).unwrap().y = Some(LayoutLen::abs(50.0)); render.update(&root, &mut rsc); @@ -1227,7 +1227,7 @@ mod tests { let row2 = rows[2]; rsc.ui.widgets.get_mut(&list_weak).unwrap().note_tap(59.0); - rsc.ui.widgets.get_mut(&row2).unwrap().y = Some(Len::abs(50.0)); + rsc.ui.widgets.get_mut(&row2).unwrap().y = Some(LayoutLen::abs(50.0)); render.update(&root, &mut rsc); let list_ref = rsc.ui.widgets.get(&list_weak).unwrap(); diff --git a/src/widget/layout/max_size.rs b/src/widget/layout/max_size.rs index f9d243e..fae22e3 100644 --- a/src/widget/layout/max_size.rs +++ b/src/widget/layout/max_size.rs @@ -2,12 +2,12 @@ use crate::prelude::*; pub struct MaxSize { pub inner: StrongWidget, - pub x: Option, - pub y: Option, + pub x: Option, + pub y: Option, } impl MaxSize { - fn clamp(len: Len, max: Option, output: f32, density: f32) -> Len { + fn clamp(len: LayoutLen, max: Option, output: f32, density: f32) -> LayoutLen { let Some(max) = max else { return len; }; @@ -20,7 +20,7 @@ impl MaxSize { } } - fn clamp_region(offered_px: f32, max: Option, output: f32, density: f32) -> UiSpan { + fn clamp_region(offered_px: f32, max: Option, output: f32, density: f32) -> UiSpan { let Some(max) = max else { return UiSpan::FULL; }; diff --git a/src/widget/layout/pad.rs b/src/widget/layout/pad.rs index 7cf690a..e90fa6c 100644 --- a/src/widget/layout/pad.rs +++ b/src/widget/layout/pad.rs @@ -10,16 +10,14 @@ impl Widget for Pad { fn draw(&mut self, painter: &mut Painter) { let density = painter.density(); let offered = painter.px_size(); - let region = self.padding.region(density); - let used = painter.widget_within(&self.inner, region).size(); + let used = painter + .widget_within(&self.inner, self.padding.fixed_region(density)) + .size(); + let region = self.padding.region_for(used, density); painter.place_used(&self.inner, used, region); - let width = - self.padding.left.apply_rest(density).abs + self.padding.right.apply_rest(density).abs; - let height = - self.padding.top.apply_rest(density).abs + self.padding.bottom.apply_rest(density).abs; let size = Size { - x: used.x + Len::abs(width), - y: used.y + Len::abs(height), + x: self.padding.left.fold_dp(density) + used.x + self.padding.right.fold_dp(density), + y: self.padding.top.fold_dp(density) + used.y + self.padding.bottom.fold_dp(density), }; let needed = size.to_uivec2(density).to_abs(painter.output_size()); if needed.x <= offered.x + 0.01 && needed.y <= offered.y + 0.01 { @@ -34,21 +32,21 @@ impl Widget for Pad { } pub struct Padding { - pub left: Len, - pub right: Len, - pub top: Len, - pub bottom: Len, + pub left: LayoutLen, + pub right: LayoutLen, + pub top: LayoutLen, + pub bottom: LayoutLen, } impl Padding { pub const ZERO: Self = Self { - left: Len::ZERO, - right: Len::ZERO, - top: Len::ZERO, - bottom: Len::ZERO, + left: LayoutLen::ZERO, + right: LayoutLen::ZERO, + top: LayoutLen::ZERO, + bottom: LayoutLen::ZERO, }; - pub fn uniform(amt: impl Into) -> Self { + pub fn uniform(amt: impl Into) -> Self { let amt = amt.into(); Self { left: amt, @@ -57,79 +55,119 @@ impl Padding { bottom: amt, } } - pub fn region(&self, density: f32) -> UiRegion { + fn fixed_region(&self, density: f32) -> UiRegion { let mut region = UiRegion::FULL; - region.x.start.abs += self.left.apply_rest(density).abs; - region.y.start.abs += self.top.apply_rest(density).abs; - region.x.end.abs -= self.right.apply_rest(density).abs; - region.y.end.abs -= self.bottom.apply_rest(density).abs; + let left = self.left.fold_dp(density); + let right = self.right.fold_dp(density); + let top = self.top.fold_dp(density); + let bottom = self.bottom.fold_dp(density); + region.x.start += UiScalar::new(left.rel, left.abs); + region.x.end -= UiScalar::new(right.rel, right.abs); + region.y.start += UiScalar::new(top.rel, top.abs); + region.y.end -= UiScalar::new(bottom.rel, bottom.abs); region } - pub fn x(amt: impl Into) -> Self { + + fn region_for(&self, inner: Size, density: f32) -> UiRegion { + UiRegion { + x: padded_axis(self.left, inner.x, self.right, density), + y: padded_axis(self.top, inner.y, self.bottom, density), + } + } + pub fn x(amt: impl Into) -> Self { let amt = amt.into(); Self { left: amt, right: amt, - top: Len::ZERO, - bottom: Len::ZERO, + top: LayoutLen::ZERO, + bottom: LayoutLen::ZERO, } } - pub fn y(amt: impl Into) -> Self { + pub fn y(amt: impl Into) -> Self { let amt = amt.into(); Self { - left: Len::ZERO, - right: Len::ZERO, + left: LayoutLen::ZERO, + right: LayoutLen::ZERO, top: amt, bottom: amt, } } - pub fn top(amt: impl Into) -> Self { + pub fn top(amt: impl Into) -> Self { let mut s = Self::ZERO; s.top = amt.into(); s } - pub fn bottom(amt: impl Into) -> Self { + pub fn bottom(amt: impl Into) -> Self { let mut s = Self::ZERO; s.bottom = amt.into(); s } - pub fn left(amt: impl Into) -> Self { + pub fn left(amt: impl Into) -> Self { let mut s = Self::ZERO; s.left = amt.into(); s } - pub fn right(amt: impl Into) -> Self { + pub fn right(amt: impl Into) -> Self { let mut s = Self::ZERO; s.right = amt.into(); s } - pub fn with_top(mut self, amt: impl Into) -> Self { + pub fn with_top(mut self, amt: impl Into) -> Self { self.top = amt.into(); self } - pub fn with_bottom(mut self, amt: impl Into) -> Self { + pub fn with_bottom(mut self, amt: impl Into) -> Self { self.bottom = amt.into(); self } - pub fn with_left(mut self, amt: impl Into) -> Self { + pub fn with_left(mut self, amt: impl Into) -> Self { self.left = amt.into(); self } - pub fn with_right(mut self, amt: impl Into) -> Self { + pub fn with_right(mut self, amt: impl Into) -> Self { self.right = amt.into(); self } } -impl> From for Padding { +fn padded_axis(before: LayoutLen, inner: LayoutLen, after: LayoutLen, density: f32) -> UiSpan { + let lengths = [ + before.fold_dp(density), + inner.fold_dp(density), + after.fold_dp(density), + ]; + let total = lengths + .iter() + .copied() + .fold(LayoutLen::ZERO, |sum, len| sum + len); + let fixed = UiScalar::new(total.rel, total.abs); + let mut cursor = UiScalar::ZERO; + let mut inner_span = UiSpan::FULL; + for (index, len) in lengths.into_iter().enumerate() { + let start = cursor; + if len.rest > 0.0 { + let rest_end = UiScalar::rel(len.rest / total.rest); + let available_end = (UiScalar::FULL + cursor) - fixed; + cursor = rest_end.within(&cursor.to(available_end)); + } + cursor.rel += len.rel; + cursor.abs += len.abs; + if index == 1 { + inner_span = start.to(cursor); + } + } + inner_span +} + +impl> From for Padding { fn from(amt: T) -> Self { Self::uniform(amt.into()) } diff --git a/src/widget/layout/scroll_area.rs b/src/widget/layout/scroll_area.rs index beafd14..9318657 100644 --- a/src/widget/layout/scroll_area.rs +++ b/src/widget/layout/scroll_area.rs @@ -97,7 +97,7 @@ mod tests { let mut rsc = TestRsc { ui: Ui::default() }; let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any(); let id = fill.id(); - let long = Some(Len::abs(1000.0)); + let long = Some(LayoutLen::abs(1000.0)); let tall = rsc.ui.widgets.add_strong(Sized { inner: fill, x: (axis == Axis::X).then_some(long).flatten(), diff --git a/src/widget/layout/sized.rs b/src/widget/layout/sized.rs index c648a2a..92a6ded 100644 --- a/src/widget/layout/sized.rs +++ b/src/widget/layout/sized.rs @@ -2,8 +2,8 @@ use crate::prelude::*; pub struct Sized { pub inner: StrongWidget, - pub x: Option, - pub y: Option, + pub x: Option, + pub y: Option, } impl Widget for Sized { @@ -25,7 +25,7 @@ impl Widget for Sized { painter.set_size(size); } - fn size_hint(&self, axis: Axis) -> Option { + fn size_hint(&self, axis: Axis) -> Option { match axis { Axis::X => self.x, Axis::Y => self.y, diff --git a/src/widget/layout/span.rs b/src/widget/layout/span.rs index e805fc4..f83606c 100644 --- a/src/widget/layout/span.rs +++ b/src/widget/layout/span.rs @@ -4,7 +4,7 @@ use std::marker::PhantomData; pub struct Span { pub children: Vec, pub dir: Dir, - pub gap: Len, + pub gap: LayoutLen, } impl Widget for Span { @@ -16,7 +16,7 @@ impl Widget for Span { let axis = self.dir.axis; let gap = self.gap.apply_rest(painter.density()).abs; - let mut lens: Vec> = self + let mut lens: Vec> = self .children .iter() .map(|child| painter.known_len(child, axis)) @@ -43,13 +43,13 @@ impl Widget for Span { cursor.rel += len.rel; } - let lens: Vec = lens.into_iter().map(Option::unwrap).collect(); + let lens: Vec = lens.into_iter().map(Option::unwrap).collect(); let gap_total = gap * self.children.len().saturating_sub(1) as f32; - let total = lens.iter().fold(Len::abs(gap_total), |s, &l| s + l); + let total = lens.iter().fold(LayoutLen::abs(gap_total), |s, &l| s + l); let mut start = UiScalar::rel_min(); - let mut ortho_len = Len::ZERO; + let mut ortho_len = LayoutLen::ZERO; let mut ortho_mixed = false; let mut placed = Vec::with_capacity(self.children.len()); for (i, (child, &len)) in self.children.iter().zip(&lens).enumerate() { @@ -84,7 +84,7 @@ impl Widget for Span { } } if ortho_mixed { - ortho_len = Len::default(); + ortho_len = LayoutLen::default(); } else { let ortho = ortho_len .apply_rest(painter.density()) @@ -98,7 +98,7 @@ impl Widget for Span { let along = if total.rest == 0.0 && total.rel == 0.0 { total } else { - Len::default() + LayoutLen::default() }; painter.set_size(Size::from_axis(axis, along, ortho_len)); @@ -110,11 +110,11 @@ impl Span { Self { children: Vec::new(), dir, - gap: Len::ZERO, + gap: LayoutLen::ZERO, } } - pub fn gap(mut self, gap: impl Into) -> Self { + pub fn gap(mut self, gap: impl Into) -> Self { self.gap = gap.into(); self } @@ -131,7 +131,7 @@ impl Span { pub struct SpanBuilder, Tag> { pub children: Wa, pub dir: Dir, - pub gap: Len, + pub gap: LayoutLen, _pd: PhantomData<(State, Tag)>, } @@ -157,12 +157,12 @@ impl, Tag> Self { children, dir, - gap: Len::ZERO, + gap: LayoutLen::ZERO, _pd: PhantomData, } } - pub fn gap(mut self, gap: impl Into) -> Self { + pub fn gap(mut self, gap: impl Into) -> Self { self.gap = gap.into(); self } diff --git a/src/widget/layout/stack.rs b/src/widget/layout/stack.rs index 90fe230..807fdd2 100644 --- a/src/widget/layout/stack.rs +++ b/src/widget/layout/stack.rs @@ -53,9 +53,9 @@ impl Widget for Stack { painter.set_size(size); } - fn size_hint(&self, _axis: Axis) -> Option { + fn size_hint(&self, _axis: Axis) -> Option { match self.size { - StackSize::Default => Some(Len::REST), + StackSize::Default => Some(LayoutLen::REST), StackSize::Child(_) => None, } } diff --git a/src/widget/rect.rs b/src/widget/rect.rs index 0b89ba9..8125eff 100644 --- a/src/widget/rect.rs +++ b/src/widget/rect.rs @@ -49,8 +49,8 @@ impl Widget for Rect { painter.set_size(Size::REST); } - fn size_hint(&self, _axis: Axis) -> Option { - Some(Len::REST) + fn size_hint(&self, _axis: Axis) -> Option { + Some(LayoutLen::REST) } } diff --git a/src/widget/trait_fns.rs b/src/widget/trait_fns.rs index f4be048..089f73e 100644 --- a/src/widget/trait_fns.rs +++ b/src/widget/trait_fns.rs @@ -41,7 +41,7 @@ widget_trait! { } } - fn max_width(self, len: impl Into) -> impl WidgetFn { + fn max_width(self, len: impl Into) -> impl WidgetFn { let len = len.into(); move |state| MaxSize { inner: self.add_strong(state), @@ -50,7 +50,7 @@ widget_trait! { } } - fn max_height(self, len: impl Into) -> impl WidgetFn { + fn max_height(self, len: impl Into) -> impl WidgetFn { let len = len.into(); move |state| MaxSize { inner: self.add_strong(state), @@ -59,7 +59,7 @@ widget_trait! { } } - fn width(self, len: impl Into) -> impl WidgetFn { + fn width(self, len: impl Into) -> impl WidgetFn { let len = len.into(); move |state| Sized { inner: self.add_strong(state), @@ -68,7 +68,7 @@ widget_trait! { } } - fn height(self, len: impl Into) -> impl WidgetFn { + fn height(self, len: impl Into) -> impl WidgetFn { let len = len.into(); move |state| Sized { inner: self.add_strong(state),