iris: separate layout allocation from lengths

This commit is contained in:
iris committed 2026-09-12 19:35:18 -04:00
1 parent 2b9d8c49a0
commit 1f15125992
17 files changed
+395 -168

No files matched your search

+4 -4
View File
@@ -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();
+189 -45
View File
@@ -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<N: UiNum> From<N> for Len {
}
}
impl<Nx: UiNum, Ny: UiNum> From<(Nx, Ny)> for Size {
fn from((x, y): (Nx, Ny)) -> Self {
impl<N: UiNum> From<N> for LayoutLen {
fn from(value: N) -> Self {
Self::abs(value.to_f32())
}
}
impl From<Len> for LayoutLen {
fn from(value: Len) -> Self {
Self {
abs: value.abs,
dp: value.dp,
rel: value.rel,
rest: 0.0,
}
}
}
impl<X: Into<LayoutLen>, Y: Into<LayoutLen>> From<(X, Y)> for Size {
fn from((x, y): (X, Y)) -> Self {
Self {
x: x.into(),
y: y.into(),
@@ -35,41 +64,47 @@ impl<Nx: UiNum, Ny: UiNum> From<(Nx, Ny)> for Size {
}
}
impl From<LayoutLen> for Size {
fn from(value: LayoutLen) -> Self {
Self { x: value, y: value }
}
}
impl From<Len> 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<Len> for LayoutLen {
type Output = Self;
fn add(self, rhs: Len) -> Self::Output {
self + Self::from(rhs)
}
}
impl std::ops::Sub<Len> 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,
}
);
}
}
+5 -5
View File
@@ -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<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
pub fn known_len<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
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
}
+3 -3
View File
@@ -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()
+4 -4
View File
@@ -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<Len> {
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
None
}
@@ -63,8 +63,8 @@ impl Widget for () {
true
}
fn size_hint(&self, _axis: Axis) -> Option<Len> {
Some(Len::ZERO)
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
Some(LayoutLen::ZERO)
}
}
+72 -27
View File
@@ -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
+6 -6
View File
@@ -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<Cell<Option<WeakWidget<ScrollArea>>>>| {
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)
+2 -2
View File
@@ -12,8 +12,8 @@ impl Widget for Image {
painter.set_size(Size::abs(size));
}
fn size_hint(&self, axis: Axis) -> Option<Len> {
Some(Len::abs(self.handle.size().axis(axis)))
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
Some(LayoutLen::abs(self.handle.size().axis(axis)))
}
fn is_size_independent(&self) -> bool {
+7 -7
View File
@@ -811,8 +811,8 @@ impl Widget for LazySpan {
painter.set_size(Size::REST);
}
fn size_hint(&self, _axis: Axis) -> Option<Len> {
Some(Len::REST)
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
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();
+4 -4
View File
@@ -2,12 +2,12 @@ use crate::prelude::*;
pub struct MaxSize {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
pub x: Option<LayoutLen>,
pub y: Option<LayoutLen>,
}
impl MaxSize {
fn clamp(len: Len, max: Option<Len>, output: f32, density: f32) -> Len {
fn clamp(len: LayoutLen, max: Option<LayoutLen>, 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<Len>, output: f32, density: f32) -> UiSpan {
fn clamp_region(offered_px: f32, max: Option<LayoutLen>, output: f32, density: f32) -> UiSpan {
let Some(max) = max else {
return UiSpan::FULL;
};
+75 -37
View File
@@ -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<Len>) -> Self {
pub fn uniform(amt: impl Into<LayoutLen>) -> 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<Len>) -> 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<LayoutLen>) -> 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<Len>) -> Self {
pub fn y(amt: impl Into<LayoutLen>) -> 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<Len>) -> Self {
pub fn top(amt: impl Into<LayoutLen>) -> Self {
let mut s = Self::ZERO;
s.top = amt.into();
s
}
pub fn bottom(amt: impl Into<Len>) -> Self {
pub fn bottom(amt: impl Into<LayoutLen>) -> Self {
let mut s = Self::ZERO;
s.bottom = amt.into();
s
}
pub fn left(amt: impl Into<Len>) -> Self {
pub fn left(amt: impl Into<LayoutLen>) -> Self {
let mut s = Self::ZERO;
s.left = amt.into();
s
}
pub fn right(amt: impl Into<Len>) -> Self {
pub fn right(amt: impl Into<LayoutLen>) -> Self {
let mut s = Self::ZERO;
s.right = amt.into();
s
}
pub fn with_top(mut self, amt: impl Into<Len>) -> Self {
pub fn with_top(mut self, amt: impl Into<LayoutLen>) -> Self {
self.top = amt.into();
self
}
pub fn with_bottom(mut self, amt: impl Into<Len>) -> Self {
pub fn with_bottom(mut self, amt: impl Into<LayoutLen>) -> Self {
self.bottom = amt.into();
self
}
pub fn with_left(mut self, amt: impl Into<Len>) -> Self {
pub fn with_left(mut self, amt: impl Into<LayoutLen>) -> Self {
self.left = amt.into();
self
}
pub fn with_right(mut self, amt: impl Into<Len>) -> Self {
pub fn with_right(mut self, amt: impl Into<LayoutLen>) -> Self {
self.right = amt.into();
self
}
}
impl<T: Into<Len>> From<T> 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<T: Into<LayoutLen>> From<T> for Padding {
fn from(amt: T) -> Self {
Self::uniform(amt.into())
}
+1 -1
View File
@@ -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(),
+3 -3
View File
@@ -2,8 +2,8 @@ use crate::prelude::*;
pub struct Sized {
pub inner: StrongWidget,
pub x: Option<Len>,
pub y: Option<Len>,
pub x: Option<LayoutLen>,
pub y: Option<LayoutLen>,
}
impl Widget for Sized {
@@ -25,7 +25,7 @@ impl Widget for Sized {
painter.set_size(size);
}
fn size_hint(&self, axis: Axis) -> Option<Len> {
fn size_hint(&self, axis: Axis) -> Option<LayoutLen> {
match axis {
Axis::X => self.x,
Axis::Y => self.y,
+12 -12
View File
@@ -4,7 +4,7 @@ use std::marker::PhantomData;
pub struct Span {
pub children: Vec<StrongWidget>,
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<Option<Len>> = self
let mut lens: Vec<Option<LayoutLen>> = 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<Len> = lens.into_iter().map(Option::unwrap).collect();
let lens: Vec<LayoutLen> = 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<Len>) -> Self {
pub fn gap(mut self, gap: impl Into<LayoutLen>) -> Self {
self.gap = gap.into();
self
}
@@ -131,7 +131,7 @@ impl Span {
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
pub children: Wa,
pub dir: Dir,
pub gap: Len,
pub gap: LayoutLen,
_pd: PhantomData<(State, Tag)>,
}
@@ -157,12 +157,12 @@ impl<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag>
Self {
children,
dir,
gap: Len::ZERO,
gap: LayoutLen::ZERO,
_pd: PhantomData,
}
}
pub fn gap(mut self, gap: impl Into<Len>) -> Self {
pub fn gap(mut self, gap: impl Into<LayoutLen>) -> Self {
self.gap = gap.into();
self
}
+2 -2
View File
@@ -53,9 +53,9 @@ impl Widget for Stack {
painter.set_size(size);
}
fn size_hint(&self, _axis: Axis) -> Option<Len> {
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
match self.size {
StackSize::Default => Some(Len::REST),
StackSize::Default => Some(LayoutLen::REST),
StackSize::Child(_) => None,
}
}
+2 -2
View File
@@ -49,8 +49,8 @@ impl Widget for Rect {
painter.set_size(Size::REST);
}
fn size_hint(&self, _axis: Axis) -> Option<Len> {
Some(Len::REST)
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
Some(LayoutLen::REST)
}
}
+4 -4
View File
@@ -41,7 +41,7 @@ widget_trait! {
}
}
fn max_width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, MaxSize> {
fn max_width(self, len: impl Into<LayoutLen>) -> impl WidgetFn<Rsc, MaxSize> {
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<Len>) -> impl WidgetFn<Rsc, MaxSize> {
fn max_height(self, len: impl Into<LayoutLen>) -> impl WidgetFn<Rsc, MaxSize> {
let len = len.into();
move |state| MaxSize {
inner: self.add_strong(state),
@@ -59,7 +59,7 @@ widget_trait! {
}
}
fn width(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
fn width(self, len: impl Into<LayoutLen>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into();
move |state| Sized {
inner: self.add_strong(state),
@@ -68,7 +68,7 @@ widget_trait! {
}
}
fn height(self, len: impl Into<Len>) -> impl WidgetFn<Rsc, Sized> {
fn height(self, len: impl Into<LayoutLen>) -> impl WidgetFn<Rsc, Sized> {
let len = len.into();
move |state| Sized {
inner: self.add_strong(state),