diff --git a/core/src/layout_diagnostics.rs b/core/src/layout_diagnostics.rs index 1cdf04d..4aef559 100644 --- a/core/src/layout_diagnostics.rs +++ b/core/src/layout_diagnostics.rs @@ -40,6 +40,7 @@ pub(crate) enum Counter { ReuseWrongParent, ReuseRemapped, ReuseOutside, + PlaceRedraws, QueuePops, DepthReads, LocalRedraws, @@ -72,6 +73,7 @@ impl Counter { "reuse: wrong parent", "reuse remapped", "reuse: outside what it holds for", + "placed by redrawing", "redraw queue pops", "depth reads", "local redraws", diff --git a/core/src/orientation/align.rs b/core/src/orientation/align.rs index ff79dbf..7bc309d 100644 --- a/core/src/orientation/align.rs +++ b/core/src/orientation/align.rs @@ -2,7 +2,7 @@ use crate::vec2; use super::*; -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq)] pub struct Align { pub x: Option, pub y: Option, @@ -30,20 +30,30 @@ impl Align { } } -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum AxisAlign { - Neg, - Center, - Pos, -} +/// Where a widget sits in a box longer than it is. The default is the middle, +/// because the two edges are the ones that assume a direction: which of them +/// is the near one depends on the writing system and on which way a container +/// runs, and the middle is the same either way. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct AxisAlign(f32); impl AxisAlign { + pub const NEG: Self = Self::new(0.0); + pub const CENTER: Self = Self::new(0.5); + pub const POS: Self = Self::new(1.0); + + pub const fn new(rel: f32) -> Self { + Self(rel) + } + pub const fn rel(&self) -> f32 { - match self { - Self::Neg => 0.0, - Self::Center => 0.5, - Self::Pos => 1.0, - } + self.0 + } +} + +impl Default for AxisAlign { + fn default() -> Self { + Self::CENTER } } @@ -53,34 +63,57 @@ pub struct CardinalAlign { } impl CardinalAlign { - pub const LEFT: Self = Self::new(Axis::X, AxisAlign::Neg); - pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::Center); - pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::Pos); - pub const TOP: Self = Self::new(Axis::Y, AxisAlign::Neg); - pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::Center); - pub const BOT: Self = Self::new(Axis::Y, AxisAlign::Pos); + pub const LEFT: Self = Self::new(Axis::X, AxisAlign::NEG); + pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::CENTER); + pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::POS); + pub const TOP: Self = Self::new(Axis::Y, AxisAlign::NEG); + pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::CENTER); + pub const BOT: Self = Self::new(Axis::Y, AxisAlign::POS); pub const fn new(axis: Axis, align: AxisAlign) -> Self { Self { axis, align } } } -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct RegionAlign { pub x: AxisAlign, pub y: AxisAlign, } impl RegionAlign { - pub const TOP_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Neg); - pub const TOP_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Neg); - pub const TOP_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Neg); - pub const CENTER_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Center); - pub const CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Center); - pub const CENTER_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Center); - pub const BOT_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Pos); - pub const BOT_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Pos); - pub const BOT_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Pos); + /// Both axes at the near edge. What a container passes as an override for + /// a child it is going to position itself. + pub const NEAR: Self = Self { + x: AxisAlign::NEG, + y: AxisAlign::NEG, + }; + + pub fn axis(&self, axis: Axis) -> AxisAlign { + match axis { + Axis::X => self.x, + Axis::Y => self.y, + } + } + + pub fn axis_mut(&mut self, axis: Axis) -> &mut AxisAlign { + match axis { + Axis::X => &mut self.x, + Axis::Y => &mut self.y, + } + } +} + +impl RegionAlign { + pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG); + pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG); + pub const TOP_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::NEG); + pub const CENTER_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::CENTER); + pub const CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::CENTER); + pub const CENTER_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::CENTER); + pub const BOT_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::POS); + pub const BOT_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::POS); + pub const BOT_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::POS); pub const fn new(x: AxisAlign, y: AxisAlign) -> Self { Self { x, y } @@ -165,8 +198,8 @@ impl From for Align { impl From for RegionAlign { fn from(align: Align) -> Self { Self { - x: align.x.unwrap_or(AxisAlign::Center), - y: align.y.unwrap_or(AxisAlign::Center), + x: align.x.unwrap_or(AxisAlign::CENTER), + y: align.y.unwrap_or(AxisAlign::CENTER), } } } diff --git a/core/src/orientation/pos.rs b/core/src/orientation/pos.rs index 6a31276..d271ad9 100644 --- a/core/src/orientation/pos.rs +++ b/core/src/orientation/pos.rs @@ -188,6 +188,15 @@ impl UiScalar { } } + /// Both channels by the same factor, which is what a fraction of a + /// length means when the length is part pixels and part a share. + pub const fn scale(&self, by: f32) -> Self { + Self { + rel: self.rel * by, + px: self.px * by, + } + } + pub const fn offset(mut self, amt: f32) -> Self { self.px += amt; self diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index d60527d..29257a1 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -1,5 +1,6 @@ use crate::{ - Holds, LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId, + Holds, LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, RegionAlign, Size, TextureHandle, + UiRegion, WidgetId, }; /// What is kept of a widget its parent has asked about. `drawn` says whether @@ -8,6 +9,7 @@ use crate::{ #[derive(Debug)] pub struct ActiveData { pub id: WidgetId, + /// The box its drawing is in, in `parent_move`'s coordinates. pub region: UiRegion, /// The box its parent first asked about it in, as a part of the box the /// parent was itself asked in. Any later box it was given was decided @@ -18,7 +20,7 @@ pub struct ActiveData { pub answer: (Size, [Holds; 2]), /// What the widget said it used of its box, the last time it drew. pub size: Size, - /// The pixel lengths of its box, per axis, that its drawing and `size` + /// The pixel lengths of `region`, per axis, that its drawing and `size` /// hold for. pub holds: [Holds; 2], pub drawn: bool, @@ -39,6 +41,15 @@ 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: [Option; 2], + /// The alignment its parent asked it with. A local redraw repeats that + /// question, including an override chosen by a container. + pub align: RegionAlign, + /// Whether that alignment was the parent's override rather than the + /// widget's own property. + pub align_override: bool, + /// Its own alignment when it was last drawn. A change to the property is + /// found against this even when its parent overrode the alignment. + pub own_align: RegionAlign, /// The movable region whose coordinates `region` uses. pub parent_move: MoveIdx, pub mask: MaskIdx, diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 4269f5f..f37a673 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -1,8 +1,8 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter}; use crate::{ - Axis, Holds, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, - TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, Widgets, + Axis, Holds, Len, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, + TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, Widgets, render::{ GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind, TexturePrimitive, @@ -99,16 +99,7 @@ impl<'a> Painter<'a> { /// Draws a widget within this widget's region. pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget) -> DrawResult<'s, 'a, W> { - self.widget_at(id, UiRegion::FULL) - } - - /// Draws a widget somewhere within this one. - pub fn widget_within<'s, W: ?Sized>( - &'s mut self, - id: &'s StrongWidget, - region: UiRegion, - ) -> DrawResult<'s, 'a, W> { - self.widget_at(id, region) + self.widget_within(id, UiRegion::FULL) } /// What a widget's rules declare its lengths to be, which whoever draws @@ -127,19 +118,45 @@ impl<'a> Painter<'a> { self.state.undraw_rec(id.id(), self.rsc); } - /// `region` in this widget's own coordinates, and with the child's - /// declared lengths still to be taken. - fn widget_at<'s, W: ?Sized>( + /// Draws a widget somewhere within this one. `region` is in this widget's + /// own coordinates, and the child's declared lengths are still to be + /// taken from it. Where the child's drawing sits inside what it is given + /// is the child's alignment, applied where the child is drawn, so a + /// container positions a child either by handing it a box of exactly its + /// length or by leaving it room and letting its alignment decide. + pub fn widget_within<'s, W: ?Sized>( &'s mut self, id: &'s StrongWidget, region: UiRegion, + ) -> DrawResult<'s, 'a, W> { + self.widget_at(id, region, None) + } + + /// Draws a widget with an alignment chosen by its container rather than + /// the widget's property. Containers use this when the box they hand down + /// already expresses the size they report around the child. + pub fn widget_aligned<'s, W: ?Sized>( + &'s mut self, + id: &'s StrongWidget, + region: UiRegion, + align: RegionAlign, + ) -> DrawResult<'s, 'a, W> { + self.widget_at(id, region, Some(align)) + } + + fn widget_at<'s, W: ?Sized>( + &'s mut self, + id: &'s StrongWidget, + region: UiRegion, + align_override: Option, ) -> DrawResult<'s, 'a, W> { let region_node = self.rsc.widgets().is_region_node(id.id()); let declared = self.declared_lens(id); + let align = align_override.unwrap_or_else(|| self.rsc.widgets().alignment(id.id())); // Composing `FULL` through a box is not quite the identity in f32, // so a child with nothing declared keeps the box it would have had. let local = match declared.iter().any(Option::is_some) { - true => declared_box(region, declared), + true => declared_box(region, declared, align), false => region, }; let within = match local == UiRegion::FULL { @@ -161,7 +178,10 @@ impl<'a> Painter<'a> { false => self.state.active.get(&id.id()).map_or(local, |a| a.offer), }; let answers_offer = self.at_offer && local == offer; - let size = self.state.draw_inner( + // The answer and what it holds for, both about the box asked in. The + // child's record may say something else once its drawing has been + // placed: a drawing made again in its placed box holds for that box. + let (size, holds) = self.state.draw_inner( id.id(), within, DrawInfo { @@ -173,19 +193,18 @@ impl<'a> Painter<'a> { mask: self.mask, offer, offered_px: self.px_within_offer(offer), + align: align_override, }, None, self.rsc, ); - let active = self.state.active.get_mut(&id.id()).unwrap(); - active.declared = declared; if answers_offer { - active.answer = (active.size, active.holds); + self.state.active.get_mut(&id.id()).unwrap().answer = (size, holds); } - // Whatever the child's drawing holds for keeps this one to the boxes + // Whatever the child's answer holds for keeps this one to the boxes // that give the child a length inside it. for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { - *under = under.and(active.holds[axis as usize].through(local.axis(axis).len())); + *under = under.and(holds[axis as usize].through(local.axis(axis).len())); } DrawResult { child: id, @@ -232,7 +251,8 @@ impl<'a> Painter<'a> { region: UiRegion, ) -> Option { let declared = self.declared_lens(child); - let local = declared_box(region, declared); + let align = self.rsc.widgets().alignment(child.id()); + let local = declared_box(region, declared, align); let within = local.within(&self.region); let first_ask = self.offer(child.id()); if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) { @@ -326,6 +346,20 @@ impl<'a> Painter<'a> { self.region } + /// Where this widget sits in a box longer than the length it takes. A + /// widget that positions its own content reads it to place that content + /// the way the box around it would have placed the widget. + pub fn alignment(&self) -> RegionAlign { + self.rsc.widgets().alignment(self.id) + } + + /// The part of this widget's box that something of `size` takes, at the + /// near edge. A container that reports one child's size gives every child + /// this, so what it draws is inside what it says it occupies. + pub fn box_of(&self, size: Size) -> UiRegion { + placed_box(UiRegion::FULL, size, RegionAlign::NEAR, [None; 2]) + } + /// This widget's box in pixels. Reading it makes the drawing one that /// holds for this box only, until `holds` says how far it goes. pub fn px_size(&mut self) -> Vec2 { @@ -455,15 +489,52 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option; 2] }) } +/// The box a drawing occupies: the size the widget reported, on the side of +/// the box it was asked in that its alignment says. An axis reported as a +/// share fills, because a share is a length only to whoever divides one, and +/// whoever did is the one that handed down this box. A declared axis is +/// left alone too: `declared_box` already placed it, in the parent's box, +/// and the rule's length is what the widget reports there. +/// +/// A reported fraction is a fraction of the box the widget drew in, where a +/// declared one is a fraction of the box its parent handed down -- a span +/// reporting `rel(1.0)` means all of what it was given, whatever that was a +/// fraction of. So this scales by the box rather than composing into it. +pub(crate) fn placed_box( + region: UiRegion, + size: Size, + align: RegionAlign, + declared: [Option; 2], +) -> UiRegion { + let mut placed = region; + for (axis, declared) in AXES.into_iter().zip(declared) { + let reported = size.axis(axis); + if reported.leftover != 0.0 || declared.is_some() { + continue; + } + let span = placed.axis_mut(axis); + let len = span.len().scale(reported.rel) + UiScalar::px(reported.px); + span.start += (span.len() - len).scale(align.axis(axis).rel()); + span.end = span.start + len; + } + placed +} + /// Takes a widget's declared lengths in the box `region` is given in, since a -/// fraction of a length means a fraction of that one. A caller that already -/// reserved the space hands back the same length, so this is the identity -/// for it. -fn declared_box(mut region: UiRegion, declared: [Option; 2]) -> UiRegion { +/// fraction of a length means a fraction of that one, and puts what is left +/// over on the side its alignment says. A caller that already reserved the +/// space hands back the same length, so this is the identity for it. +pub(crate) fn declared_box( + mut region: UiRegion, + declared: [Option; 2], + align: RegionAlign, +) -> UiRegion { for (axis, len) in AXES.into_iter().zip(declared) { let Some(len) = len else { continue }; let span = region.axis_mut(axis); - span.end = span.start + UiScalar::new(len.rel, len.px); + let len = UiScalar::new(len.rel, len.px); + span.start += (span.len() - len).scale(align.axis(axis).rel()); + span.end = span.start + len; } region } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 709c2e0..69540c5 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1,9 +1,10 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; -use crate::ui::painter::declared_lens; +use crate::ui::painter::{declared_box, declared_lens, placed_box}; use crate::{ ActiveData, Axis, DrawLayers, Holds, IdLike, Len, MaskIdx, MoveIdx, Moves, Painter, - PixelRegion, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId, Widgets, + PixelRegion, RegionAlign, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId, + Widgets, util::{HashMap, Vec2}, }; @@ -23,6 +24,9 @@ pub(super) struct DrawInfo { /// that box in pixels. pub offer: UiRegion, pub offered_px: Vec2, + /// A container's answer for where the widget sits. `None` uses the + /// widget's own property. + pub align: Option, } pub struct UiRenderState { @@ -39,6 +43,13 @@ pub struct UiRenderState { /// A widget's move slot, which outlives any one `ActiveData`: a redraw /// replaces that while its children go on pointing at the slot. slots: HashMap, + /// Answers invalidated by a declared-length change below them. These are + /// replaced even when retained placement means the redraw is not at the + /// old offer. + answer_invalid: crate::util::HashSet, + /// Whether this frame contains a declared-length change, so any dirty + /// dependent replaces its answer too. + replace_answers: bool, pub moves: Moves, } @@ -50,6 +61,8 @@ impl UiRenderState { output_size: Vec2::ZERO, old_root: None, slots: Default::default(), + answer_invalid: Default::default(), + replace_answers: false, moves: Default::default(), root_move: MoveIdx::NONE, resized: false, @@ -91,6 +104,7 @@ impl UiRenderState { mask: MaskIdx::NONE, offer: UiRegion::FULL, offered_px: self.output_size, + align: None, } } @@ -131,12 +145,15 @@ impl UiRenderState { // anything dirty settles, so that whatever a new output draws // again is drawn once, in the box it will have. let info = self.root_info(); - self.draw_inner(root.id(), UiRegion::FULL, info, None, rsc); + let region = Self::root_region(root.id(), rsc.widgets()); + let answer = self.draw_inner(root.id(), region, info, None, rsc); + self.active.get_mut(&root.id()).unwrap().answer = answer; } self.resized = false; if rsc.widgets().has_updates() { self.redraw_updates(rsc); } + self.replace_answers = false; self.free(rsc); } @@ -148,10 +165,19 @@ impl UiRenderState { self.write_root(); if let Some(id) = root { let info = self.root_info(); - self.draw_inner(id.id(), UiRegion::FULL, info, None, rsc); + let region = Self::root_region(id.id(), rsc.widgets()); + self.draw_inner(id.id(), region, info, None, rsc); } } + fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion { + declared_box( + UiRegion::FULL, + declared_lens(widgets, id), + widgets.alignment(id), + ) + } + pub(super) fn draw_inner( &mut self, id: WidgetId, @@ -159,7 +185,7 @@ impl UiRenderState { info: DrawInfo, mut old: Option, rsc: &mut dyn UiRsc, - ) -> Size { + ) -> (Size, [Holds; 2]) { #[cfg(feature = "layout-diagnostics")] { diag::bump(Counter::DrawRequests); @@ -171,15 +197,82 @@ impl UiRenderState { info.region_node, ); } - if self.active.contains_key(&id) { - if let Some(size) = self.try_reuse(id, region, info, rsc) { - return size; + let own_align = rsc.widgets().alignment(id); + let align = info.align.unwrap_or(own_align); + let replace_answer = self.answer_invalid.remove(&id) + || (self.replace_answers + && (rsc.widgets().needs_redraw.contains(&id) + || self.dirty_size_under(id, rsc.widgets()))); + let retained = match replace_answer { + true => None, + false => self + .retained_answer(id, region, info, rsc.widgets()) + .or_else(|| self.try_reuse(id, region, info, rsc)), + }; + let answer = retained.unwrap_or_else(|| { + if old.is_none() { + old = self.remove(id, false, rsc); } - // if not, then maintain resize and track old children to remove unneeded - old = self.remove(id, false, rsc); + self.draw_at(id, region, info, align, old.take(), rsc) + }); + + let declared = declared_lens(rsc.widgets(), id); + // A near-edge override means the caller already chose this box from + // the child's answer. Applying the answer again would compound the + // placement; it is also how the second, final ask terminates. + let placed = match info.align == Some(RegionAlign::NEAR) { + true => region, + false => placed_box(region, answer.0, align, declared), + }; + let placed_info = DrawInfo { + align: Some(RegionAlign::NEAR), + ..info + }; + // The symbolic box can be unchanged while its parent slot changed + // pixel size. Reuse checks the resolved box even in that case. + if self.try_reuse(id, placed, placed_info, rsc).is_none() { + #[cfg(feature = "layout-diagnostics")] + diag::bump(Counter::PlaceRedraws); + let old = self.remove(id, false, rsc); + self.draw_at(id, placed, placed_info, RegionAlign::NEAR, old, rsc); } - // draw widget + // The answer is only reusable while both parts of the operation are: + // what the widget reported in the offered box, and what it drew in + // the box its report selected. Express the latter's contract back in + // terms of the offered box before handing it to the parent. + let drawing_holds = self.active[&id].holds; + let mut settled = answer; + for axis in AXES { + let reported = answer.0.axis(axis); + let placed_len = match reported.leftover != 0.0 || declared[axis as usize].is_some() { + true => UiScalar::FULL, + false => UiScalar::new(reported.rel, reported.px), + }; + settled.1[axis as usize] = + settled.1[axis as usize].and(drawing_holds[axis as usize].through(placed_len)); + } + + let active = self.active.get_mut(&id).unwrap(); + active.offer = info.offer; + active.answer = settled; + active.align = align; + active.align_override = info.align.is_some(); + active.own_align = own_align; + active.depth = info.depth; + settled + } + + /// Calls a widget's `draw` and keeps what it drew in `region`. + fn draw_at( + &mut self, + id: WidgetId, + region: UiRegion, + info: DrawInfo, + align: RegionAlign, + old: Option, + rsc: &mut dyn UiRsc, + ) -> (Size, [Holds; 2]) { let (move_idx, local, retired_move) = match info.region_node { // Its box becomes its movable region, so it draws in that // region's coordinates and its box is one entry to rewrite. @@ -295,6 +388,7 @@ impl UiRenderState { mask, offer: UiRegion::FULL, offered_px: px, + align: None, }, rsc, ); @@ -302,7 +396,6 @@ impl UiRenderState { } } - // add to active let active = ActiveData { id, region, @@ -318,8 +411,10 @@ impl UiRenderState { primitives, children, size_deps, - // Written by whoever draws it, which is what resolves them. - declared: [None; 2], + declared: declared_lens(rsc.widgets(), id), + align, + align_override: info.align.is_some(), + own_align: rsc.widgets().alignment(id), move_idx, parent_move: info.parent_move, mask, @@ -327,7 +422,7 @@ impl UiRenderState { }; rsc.on_draw(&active); self.active.insert(id, active); - size + (size, holds) } /// Keeps a region node's entry across redraws because descendants retain @@ -358,9 +453,9 @@ impl UiRenderState { .to_px(self.output_size) } - /// A clean, drawn widget's retained size, if its drawing holds for a box - /// of `px`. This observes the answer only; it does not move or otherwise - /// reuse the widget's drawing. + /// A clean widget's retained answer, if that answer holds for a box of + /// `px`. This does not move its drawing, which may already be in the box + /// that answer placed it in. pub(super) fn retained_size( &self, id: WidgetId, @@ -372,8 +467,38 @@ impl UiRenderState { return None; } let active = self.active.get(&id)?; - let valid = active.drawn && active.parent_move == parent_move && active.holds_at(px); - valid.then_some((active.size, active.holds)) + let (size, holds) = active.answer; + let valid = active.drawn + && active.parent_move == parent_move + && holds[0].contains(px.x) + && holds[1].contains(px.y); + valid.then_some((size, holds)) + } + + /// The answer to an ask can be retained independently of where its + /// drawing ended up. Alignment is exactly that case: the first box is the + /// question and the smaller placed box holds the drawing. + fn retained_answer( + &self, + id: WidgetId, + region: UiRegion, + info: DrawInfo, + widgets: &Widgets, + ) -> Option<(Size, [Holds; 2])> { + if widgets.needs_redraw.contains(&id) || self.dirty_size_under(id, widgets) { + return None; + } + let active = self.active.get(&id)?; + let has_region_node = active.move_idx != active.parent_move; + if !active.drawn + || has_region_node != info.region_node + || active.parent_move != info.parent_move + { + return None; + } + let px = self.px_of(info.parent_move, region); + let (size, holds) = active.answer; + (holds[0].contains(px.x) && holds[1].contains(px.y)).then_some((size, holds)) } /// Whether anything whose size this widget's own size was read from is @@ -388,29 +513,42 @@ impl UiRenderState { }) } - /// The pixel size of the box a widget was first asked about in, composed - /// through the boxes its ancestors were asked in. - fn offered_px(&self, id: WidgetId) -> Vec2 { - let Some(active) = self.active.get(&id) else { - return self.output_size; + /// The first box a widget was asked about, re-expressed in the coordinate + /// space its drawing uses. Keeping the relative box and composing it + /// again avoids rebuilding a shifted box from rounded pixel lengths. + fn offered_region(&self, id: WidgetId) -> UiRegion { + let active = &self.active[&id]; + let parent_region = match active.parent.and_then(|id| self.active.get(&id)) { + Some(parent) if parent.move_idx == active.parent_move => { + if parent.move_idx == parent.parent_move { + self.offered_region(parent.id) + } else { + UiRegion::FULL + } + } + _ => UiRegion::FULL, }; - let parent = match active.parent { - Some(parent) => self.offered_px(parent), - None => self.output_size, + let mut offered = match active.offer == UiRegion::FULL { + true => parent_region, + false => active.offer.within(&parent_region), }; - let size = active.offer.size(); - Vec2::new(size.x.to_px(parent.x), size.y.to_px(parent.y)) + for axis in AXES { + if active.declared[axis as usize].is_some() { + *offered.axis_mut(axis) = *active.region.axis(axis); + } + } + offered } - /// The drawing a widget already has, kept for a new box if it holds for - /// that box. + /// Reuses the actual drawing in a new box if its retained contract holds + /// there. Answers retained from a different ask are handled separately. fn try_reuse( &mut self, id: WidgetId, region: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc, - ) -> Option { + ) -> Option<(Size, [Holds; 2])> { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::ReuseAttempts); if rsc.widgets().needs_redraw.contains(&id) { @@ -453,8 +591,12 @@ impl UiRenderState { return None; } let moved = active.region != region; - let (size, old_region, slot, mask) = - (active.size, active.region, active.move_idx, info.mask); + let (answer, old_region, slot, mask) = ( + (active.size, active.holds), + active.region, + active.move_idx, + info.mask, + ); if moved { if has_region_node { self.moves.set(slot, region); @@ -487,7 +629,7 @@ impl UiRenderState { }, ); } - Some(size) + Some(answer) } /// Re-expresses an ordinary retained subtree in a new parent region. @@ -610,6 +752,9 @@ impl UiRenderState { size_deps: Vec::new(), move_idx: info.parent_move, declared: [None; 2], + align: RegionAlign::default(), + align_override: false, + own_align: rsc.widgets().alignment(id), parent_move: info.parent_move, mask: info.mask, layer: info.layer, @@ -624,6 +769,8 @@ impl UiRenderState { } } self.slots.clear(); + self.answer_invalid.clear(); + self.replace_answers = false; self.moves.clear(); self.root_move = MoveIdx::NONE; self.layers.clear(); @@ -638,6 +785,7 @@ impl UiRenderState { rsc.on_remove(id); self.remove(id, true, rsc); self.drop_slot(id); + self.answer_invalid.remove(&id); } rsc.ui_mut().textures.free(); } @@ -748,9 +896,19 @@ impl UiRenderState { // to draw -- with the mark left on, so the parent draws it rather // than keeping it. let declared_changed = declared_lens(rsc.widgets(), id) != active.declared; + let alignment_changed = rsc.widgets().alignment(id) != active.own_align; if let Some(parent) = active.parent - && (declared_changed || !active.drawn) + && (declared_changed || alignment_changed || !active.drawn) { + if declared_changed { + self.replace_answers = true; + let mut at = Some(id); + while let Some(next) = at { + self.answer_invalid.insert(next); + rsc.widgets_mut().needs_redraw.insert(next); + at = self.active[&next].parent; + } + } rsc.widgets_mut().needs_redraw.insert(id); self.redraw(parent, rsc); // Whatever the parent did not draw again is nothing it holds now. @@ -762,15 +920,27 @@ impl UiRenderState { } let region = active.region; let region_node = active.parent.is_some() && rsc.widgets().is_region_node(id); - let offered_px = self.offered_px(id); + let asked_in = match active.parent { + Some(_) => self.offered_region(id), + None => Self::root_region(id, rsc.widgets()), + }; + let offered_px = self.px_of(active.parent_move, asked_in); let at_offer = same_px(self.px_of(active.parent_move, region), offered_px); - // Asked again where its parent asked: a box decided from its own - // answer gives that answer back whatever the content now says. Only - // a region node can be redrawn away from its current box without - // first involving the parent that chose that box. + let parent_must_place = active.parent.is_some() + && (!region_node || active.align_override) + && !same_pixel_region( + self.moves + .resolve(active.parent_move, region) + .to_px(self.output_size), + self.moves + .resolve(active.parent_move, asked_in) + .to_px(self.output_size), + ); + // An independently positioned region node can redraw at its offer + // and move its slot to its own placement. Every other widget needs + // its parent to reproduce a different final position. if let Some(parent) = active.parent - && !region_node - && !at_offer + && parent_must_place { rsc.widgets_mut().needs_redraw.insert(id); self.redraw(parent, rsc); @@ -786,23 +956,15 @@ impl UiRenderState { mask: active.mask, offer: active.offer, offered_px, + align: active.align_override.then_some(active.align), }; let (was_answer, was) = (active.answer, (active.size, active.holds)); #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::LocalRedraws); - let mut asked_in = region; - if !at_offer { - for axis in AXES { - let span = asked_in.axis_mut(axis); - span.end = span.start + UiScalar::px(offered_px.axis(axis)); - } - } let old = self.remove(id, false, rsc); - let size = self.draw_inner(id, asked_in, info, old, rsc); - let active = self.active.get_mut(&id).unwrap(); - let answer = (size, active.holds); - active.answer = answer; + let answer = self.draw_inner(id, asked_in, info, old, rsc); + self.active.get_mut(&id).unwrap().answer = answer; let Some(parent) = info.parent else { return; }; @@ -834,6 +996,10 @@ fn same_px(a: Vec2, b: Vec2) -> bool { Holds::at(a.x).contains(b.x) && Holds::at(a.y).contains(b.y) } +fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool { + same_px(a.top_left, b.top_left) && same_px(a.bot_right, b.bot_right) +} + /// A retained region rewritten from one parent box into another. A fixed /// source extent can be translated but cannot recover fractions for a resize. #[derive(Clone, Copy)] diff --git a/core/src/widget/data.rs b/core/src/widget/data.rs index 59d557a..4ed824f 100644 --- a/core/src/widget/data.rs +++ b/core/src/widget/data.rs @@ -1,10 +1,11 @@ -use crate::{SizeRules, Widget}; +use crate::{RegionAlign, SizeRules, Widget}; pub struct WidgetData { pub widget: Box, pub label: String, pub(super) region_node: bool, pub(super) size: SizeRules, + pub(super) align: RegionAlign, /// dynamic borrow checking pub borrowed: bool, } @@ -20,6 +21,7 @@ impl WidgetData { label, region_node: false, size: SizeRules::default(), + align: RegionAlign::default(), borrowed: false, } } diff --git a/core/src/widget/widgets.rs b/core/src/widget/widgets.rs index 474f393..6856d24 100644 --- a/core/src/widget/widgets.rs +++ b/core/src/widget/widgets.rs @@ -1,7 +1,8 @@ use std::sync::mpsc::{Receiver, Sender, channel}; use crate::{ - Axis, IdLike, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId, + Axis, AxisAlign, IdLike, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget, + WidgetData, WidgetId, util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut}, }; @@ -136,6 +137,23 @@ impl Widgets { self.needs_redraw.insert(id); } + /// 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 + } + + /// Sets one axis's alignment. Which box a widget ends up in is its + /// parent's to decide, so this is escalated the way a length rule is. + pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) { + let id = id.id(); + let data = self.data_mut(id).unwrap(); + if *data.align.axis_mut(axis) == align { + return; + } + *data.align.axis_mut(axis) = align; + self.needs_redraw.insert(id); + } + /// Both axes at once, for a caller holding a pair. pub fn set_size_rules( &mut self, diff --git a/src/random.rs b/src/random.rs index 30e9258..eb131f6 100644 --- a/src/random.rs +++ b/src/random.rs @@ -11,6 +11,10 @@ use std::collections::HashMap; /// The declared lengths of one widget carrying a size rule, by axis. pub type Lens = [Option; 2]; +/// Where one widget carrying an alignment sits, by axis. `None` uses the +/// centered default. +pub type Aligns = [Option; 2]; + /// What a test changes between two trees grown from the same seed, so the /// warm one can be mutated and the cold one grown that way to begin with. #[derive(Default)] @@ -19,6 +23,12 @@ pub struct Edits { pub sizes: HashMap, /// Which children a span has, by the order the spans were made. pub spans: HashMap, + /// Alignments, by the order they were put on. + pub aligns: HashMap, + /// Which widgets own a movable region, by the order they were offered + /// one. Region nodes change what a move writes and how deep a primitive's + /// chain is, so a tree that never grows one leaves both untested. + pub nodes: HashMap, } #[derive(Default, Clone)] @@ -76,6 +86,8 @@ const WORDS: &str = "Wrapping shapes one source into as many lines as the box \ pub struct Tree { pub ids: Vec, pub sized: Vec, + pub aligned: Vec, + pub nodes: Vec, pub spans: Vec, pub scrolls: Vec>, /// Children a `SpanEdit` took out, held so that dropping the last share @@ -180,26 +192,32 @@ impl Grow<'_, Rsc> { fn align(&mut self) -> Align { let mut axis = || match self.rng.below(4) { 0 => None, - 1 => Some(AxisAlign::Neg), - 2 => Some(AxisAlign::Center), - _ => Some(AxisAlign::Pos), + 1 => Some(AxisAlign::NEG), + 2 => Some(AxisAlign::CENTER), + _ => Some(AxisAlign::POS), }; let (mut x, y) = (axis(), axis()); - // Aligning on neither axis is just another transparent wrapper and - // would leave this branch unexercised. + // Aligning on neither axis leaves the branch unexercised. if x.is_none() && y.is_none() { - x = Some(AxisAlign::Center); + x = Some(AxisAlign::CENTER); } Align { x, y } } /// A declared size over half the tree, kept where a test can change it. fn sized(&mut self, inner: StrongWidget) -> StrongWidget { - if !self.rng.chance() { + // A rule is a property now, so a node already carrying one would take + // a second entry in `sized` -- and two edits naming one widget settle + // in the order they are applied, which is grow order cold and edit + // order warm. One entry per widget instead. Both draws are taken + // whatever is decided, and the decision is grow order alone, so the + // two trees consume the same random stream. + let take = self.rng.chance(); + let lens = [self.len(), self.len()]; + if !take || self.tree.sized.contains(&inner.id()) { return inner; } let idx = self.tree.sized.len(); - let lens = [self.len(), self.len()]; let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens); let id = inner.id(); self.rsc @@ -210,6 +228,41 @@ impl Grow<'_, Rsc> { inner } + /// An alignment over some of the tree, kept where a test can change it. + /// One entry per widget for the reason `sized` gives. + fn aligned(&mut self, inner: StrongWidget) -> StrongWidget { + let align = self.align(); + let align = [align.x, align.y]; + if self.tree.aligned.contains(&inner.id()) { + return inner; + } + let idx = self.tree.aligned.len(); + let align = self.edits.aligns.get(&idx).copied().unwrap_or(align); + let id = inner.id(); + let widgets = &mut self.rsc.ui_mut().widgets; + for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(align) { + widgets.set_alignment(id, axis, align.unwrap_or_default()); + } + self.tree.aligned.push(id); + inner + } + + /// A movable region of its own over some of the tree. What it changes is + /// how a move is written and how long a primitive's chain is, neither of + /// which any other branch here varies. + fn noded(&mut self, inner: StrongWidget) -> StrongWidget { + let take = self.rng.below(4) == 0; + if self.tree.nodes.contains(&inner.id()) { + return inner; + } + let idx = self.tree.nodes.len(); + let take = self.edits.nodes.get(&idx).copied().unwrap_or(take); + let id = inner.id(); + self.rsc.ui_mut().widgets.set_region_node(id, take); + self.tree.nodes.push(id); + inner + } + fn node(&mut self, depth: usize) -> StrongWidget { if depth == 0 { return self.leaf(); @@ -220,6 +273,7 @@ impl Grow<'_, Rsc> { // else here does, and gives its child a box longer than its own. let inner = self.node(depth - 1); let inner = self.sized(inner); + let inner = self.noded(inner); let axis = if self.rng.chance() { Axis::X } else { Axis::Y }; let id = Scroll::new(inner, axis).add(self.rsc); self.tree.scrolls.push(id); @@ -246,17 +300,13 @@ impl Grow<'_, Rsc> { if positioned == 1 { let inner = self.node(depth - 1); let inner = self.sized(inner); - let id = Aligned { - inner, - align: self.align(), - } - .add_strong(self.rsc); - self.tree.ids.push(id.id()); - return id; + let inner = self.noded(inner); + return self.aligned(inner); } if self.rng.below(4) == 0 { let inner = self.node(depth - 1); let inner = self.sized(inner); + let inner = self.noded(inner); // Each side its own, since a padding that is the same all round // hides anything that treats one edge differently from another. let mut side = || self.rng.below(24) as f32; @@ -274,7 +324,9 @@ impl Grow<'_, Rsc> { let mut children = Vec::with_capacity(grown); for _ in 0..grown { let child = self.node(depth - 1); - children.push(self.sized(child)); + let child = self.sized(child); + let child = self.noded(child); + children.push(child); } if self.rng.chance() { let id = Stack { diff --git a/src/widget/position/align.rs b/src/widget/position/align.rs deleted file mode 100644 index 239859e..0000000 --- a/src/widget/position/align.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::prelude::*; - -pub struct Aligned { - pub inner: StrongWidget, - pub align: Align, -} - -impl Widget for Aligned { - fn draw(&mut self, painter: &mut Painter) -> Size { - let known = match self.align.tuple() { - (Some(_), Some(_)) => painter - .known_len(&self.inner, Axis::X, UiRegion::FULL) - .zip(painter.known_len(&self.inner, Axis::Y, UiRegion::FULL)) - .map(|(x, y)| Size { x, y }), - (Some(_), None) => painter - .known_len(&self.inner, Axis::X, UiRegion::FULL) - .map(|x| Size { - x, - y: Len::LEFTOVER, - }), - (None, Some(_)) => painter - .known_len(&self.inner, Axis::Y, UiRegion::FULL) - .map(|y| Size { - x: Len::LEFTOVER, - y, - }), - (None, None) => Some(Size::LEFTOVER), - }; - // Drawn where it may be too big only when the aligned axes are not - // already known, then given its aligned box once its size is known. - let had_size = known.is_some(); - let size = - known.unwrap_or_else(|| painter.widget_within(&self.inner, UiRegion::FULL).size()); - let region = match self.align.tuple() { - (Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }), - (Some(x), None) => UiRegion::new(size.x.apply_leftover().align(x), UiSpan::FULL), - (None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_leftover().align(y)), - (None, None) => UiRegion::FULL, - }; - let placed = painter.widget_within(&self.inner, region).size(); - if had_size { placed } else { size } - } -} diff --git a/src/widget/position/mod.rs b/src/widget/position/mod.rs index 9957d3c..a86979b 100644 --- a/src/widget/position/mod.rs +++ b/src/widget/position/mod.rs @@ -1,4 +1,3 @@ -mod align; mod layer; mod offset; mod pad; @@ -6,7 +5,6 @@ mod scroll; mod span; mod stack; -pub use align::*; pub use layer::*; pub use offset::*; pub use pad::*; diff --git a/src/widget/position/pad.rs b/src/widget/position/pad.rs index 97604be..d16beb5 100644 --- a/src/widget/position/pad.rs +++ b/src/widget/position/pad.rs @@ -8,7 +8,7 @@ pub struct Pad { impl Widget for Pad { fn draw(&mut self, painter: &mut Painter) -> Size { let inner = painter - .widget_within(&self.inner, self.padding.region()) + .widget_aligned(&self.inner, self.padding.region(), RegionAlign::NEAR) .size(); Size { x: Len { diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index 3c6f9d9..91be954 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -14,13 +14,9 @@ impl Widget for Scroll { let container_len = painter.px_len(self.axis); // Draw in the whole container only when its scrolling-axis length is // not already known, then draw it at the scrolled offset. - let (answer_len, measured) = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) - { - Some(len) => (len, None), - None => { - let size = painter.widget_within(&self.inner, UiRegion::FULL).size(); - (size.axis(self.axis), Some(size)) - } + let answer_len = match painter.known_len(&self.inner, self.axis, UiRegion::FULL) { + Some(len) => len, + None => painter.widget(&self.inner).size().axis(self.axis), }; let content = answer_len.apply_leftover(); self.container_len = container_len; @@ -30,21 +26,33 @@ impl Widget for Scroll { self.amt = self.content_len - self.container_len; } self.update_amt(); - // Content of a fixed length that fits sits at the start of any box - // it fits in; one scrolled part way sits where it is until the box - // shrinks past what is left of it. Kept to the end, it moves with - // every length. - if content.rel == 0.0 && self.content_len <= self.container_len { + let align = painter.alignment().axis(self.axis); + // Content of a fixed length that fits sits at the start of any box it + // fits in -- but only anchored there. Anywhere else it is a part of + // the room left over, so it moves with every length the box takes and + // the drawing holds for that length alone. One scrolled part way sits + // where it is until the box shrinks past what is left of it. Kept to + // the end, it moves with every length. + if content.rel == 0.0 && self.content_len <= self.container_len && align == AxisAlign::NEG { painter.holds(self.axis, self.content_len..=f32::INFINITY); } else if content.rel == 0.0 && !self.snap_end { let left = self.content_len - self.amt; painter.holds(self.axis, f32::NEG_INFINITY..=left); } - let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0)); + // Content shorter than the viewport has room to sit in, and where it + // sits is this widget's own alignment -- the same property that would + // have placed the whole scroll in a box longer than it. + let slack = (self.container_len - self.content_len).max(0.0); + let anchor = slack * align.rel(); + let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, anchor - self.amt, 0.0)); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); - let placed = painter.widget_within(&self.inner, region).size(); - measured.unwrap_or_else(|| Size::from_axis(self.axis, answer_len, placed.axis(!self.axis))) + painter.widget_aligned(&self.inner, region, RegionAlign::NEAR); + // What it occupies is its box, on both axes: it clips its content to + // that box, so it can neither take less of one nor honestly ask for + // more. The content's length is what it scrolls through, not what it + // is. + Size::LEFTOVER } } diff --git a/src/widget/position/stack.rs b/src/widget/position/stack.rs index 36d1db3..82b86f6 100644 --- a/src/widget/position/stack.rs +++ b/src/widget/position/stack.rs @@ -13,18 +13,20 @@ impl Widget for Stack { StackSize::Default => None, StackSize::Child(i) => Some(i), }; - let mut size = Size::default(); + // Whichever child sizes the stack decides the box every child gets. + // The stack reports that size, so a child given a longer box would + // draw outside what the stack says it occupies. + let size = match sizing.and_then(|i| self.children.get(i)) { + Some(child) => painter.widget(child).size(), + None => Size::LEFTOVER, + }; + let region = painter.box_of(size); for (i, child) in self.children.iter().enumerate() { match i { 0 => painter.child_layer(), _ => painter.next_layer(), } - let drawn = painter.widget(child); - // Only the child that sizes the stack is read, so the others - // changing size does not redraw it. - if sizing == Some(i) { - size = drawn.size(); - } + painter.widget_aligned(child, region, RegionAlign::NEAR); } size } diff --git a/src/widget/trait_fns.rs b/src/widget/trait_fns.rs index 13988e3..4ec85fb 100644 --- a/src/widget/trait_fns.rs +++ b/src/widget/trait_fns.rs @@ -12,14 +12,23 @@ widget_trait! { } } - fn align(self, align: impl Into) -> impl WidgetFn { - move |state| Aligned { - inner: self.add_strong(state), - align: align.into(), + fn align(self, align: impl Into) -> impl WidgetIdFn { + // An axis left out keeps whatever it had, which is centered unless + // something else set it. + let align = align.into(); + move |state| { + let id = self.add(state); + let widgets = &mut state.ui_mut().widgets; + for (axis, align) in [(Axis::X, align.x), (Axis::Y, align.y)] { + if let Some(align) = align { + widgets.set_alignment(id, axis, align); + } + } + id } } - fn center(self) -> impl WidgetFn { + fn center(self) -> impl WidgetIdFn { self.align(Align::CENTER) } diff --git a/tests/generated.rs b/tests/generated.rs index 0569b8a..26b1358 100644 --- a/tests/generated.rs +++ b/tests/generated.rs @@ -13,7 +13,7 @@ use std::collections::HashMap; use iris::harness::Harness; use iris::prelude::*; -use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow}; +use iris::random::{Aligns, Edits, Lens, Rng, SpanEdit, Tree, grow}; /// How deep the generator branches. The generator widens two to four ways per /// level, so depth is exponential in width and a deep narrow tree is not @@ -179,12 +179,28 @@ fn describe(id: WidgetId, h: &Harness) -> String { Some(len) => format!("{len}"), None => "-".into(), }; - // A size rule is a property of whatever carries it, so it prints with - // that widget rather than as one of its own. - match (rules.x, rules.y) { - (SizeRule::Free, SizeRule::Free) => describe_widget(id, h), - (x, y) => format!("{}[x:{},y:{}]", describe_widget(id, h), rule(x), rule(y)), + let align = h.rsc.widgets().alignment(id); + let side = |a: AxisAlign| { + if a == AxisAlign::NEG { + "neg".into() + } else if a == AxisAlign::CENTER { + "mid".into() + } else if a == AxisAlign::POS { + "pos".into() + } else { + format!("{:.2}", a.rel()) + } + }; + // A rule and an alignment are properties of whatever carries them, so + // they print with that widget rather than as widgets of their own. + let mut out = describe_widget(id, h); + if (rules.x, rules.y) != (SizeRule::Free, SizeRule::Free) { + out += &format!("[x:{},y:{}]", rule(rules.x), rule(rules.y)); } + if align != RegionAlign::default() { + out += &format!("@{},{}", side(align.x), side(align.y)); + } + out } fn describe_widget(id: WidgetId, h: &Harness) -> String { @@ -210,15 +226,6 @@ fn describe_widget(id: WidgetId, h: &Harness) -> String { p.left, p.right, p.top, p.bottom ); } - if let Some(w) = any.downcast_ref::() { - let a = |v: Option| match v { - None => "-", - Some(AxisAlign::Neg) => "neg", - Some(AxisAlign::Center) => "mid", - Some(AxisAlign::Pos) => "pos", - }; - return format!("Aligned{{x:{},y:{}}}", a(w.align.x), a(w.align.y)); - } if let Some(w) = any.downcast_ref::() { return format!("Stack{{n:{}}}", w.children.len()); } @@ -290,6 +297,87 @@ fn changed_size(seed: u64) { assert_same(seed, "a size change", (&warm, &grown), (&cold, &same)); } +/// Moves one widget to a different corner of the box it is given. +fn realign_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Aligns { + let mut side = || match rng.below(4) { + 0 => None, + 1 => Some(AxisAlign::NEG), + 2 => Some(AxisAlign::CENTER), + _ => Some(AxisAlign::POS), + }; + let aligns = [side(), side()]; + for (axis, align) in [Axis::X, Axis::Y].into_iter().zip(aligns) { + h.rsc + .widgets_mut() + .set_alignment(tree.aligned[idx], axis, align.unwrap_or_default()); + } + aligns +} + +fn changed_alignment(seed: u64) { + let mut warm = Harness::new((900, 1200)); + let grown = plant(&mut warm, seed, &Edits::default()); + if grown.aligned.is_empty() { + return; + } + + let mut rng = Rng::new(seed ^ 0xa11); + let aligns = (0..grown.aligned.len()) + .step_by(3) + .map(|idx| (idx, realign_one(&mut warm, &grown, idx, &mut rng))) + .collect(); + warm.frame(); + + let mut cold = Harness::new((900, 1200)); + let same = plant( + &mut cold, + seed, + &Edits { + aligns, + ..Default::default() + }, + ); + assert_same(seed, "an alignment change", (&warm, &grown), (&cold, &same)); +} + +/// Giving a widget a movable region of its own, or taking it away, is a +/// structural change: every primitive under it changes which chain resolves +/// it. A cold tree built that way is what says the rebuild was complete. +fn changed_region_node(seed: u64) { + let mut warm = Harness::new((900, 1200)); + let grown = plant(&mut warm, seed, &Edits::default()); + if grown.nodes.is_empty() { + return; + } + + let nodes: HashMap = (0..grown.nodes.len()) + .step_by(2) + .map(|idx| { + let id = grown.nodes[idx]; + let was = warm.rsc.widgets().is_region_node(id); + warm.rsc.widgets_mut().set_region_node(id, !was); + (idx, !was) + }) + .collect(); + warm.frame(); + + let mut cold = Harness::new((900, 1200)); + let same = plant( + &mut cold, + seed, + &Edits { + nodes, + ..Default::default() + }, + ); + assert_same( + seed, + "a region-node change", + (&warm, &grown), + (&cold, &same), + ); +} + fn reshuffled(seed: u64, shuffle: Shuffle) { let mut warm = Harness::new((900, 1200)); let mut grown = plant(&mut warm, seed, &Edits::default()); @@ -411,6 +499,16 @@ fn a_changed_size_lands_where_growing_it_that_way_would() { SEEDS.into_iter().for_each(changed_size); } +#[test] +fn a_changed_alignment_lands_where_growing_it_that_way_would() { + SEEDS.into_iter().for_each(changed_alignment); +} + +#[test] +fn a_toggled_region_node_lands_where_growing_it_that_way_would() { + SEEDS.into_iter().for_each(changed_region_node); +} + #[test] fn every_size_changing_at_once_lands_where_growing_it_that_way_would() { SEEDS.into_iter().for_each(changed_every_size); diff --git a/tests/layout.rs b/tests/layout.rs index 9e91161..3e3ca21 100644 --- a/tests/layout.rs +++ b/tests/layout.rs @@ -74,17 +74,39 @@ fn an_empty_widget_takes_a_share_of_a_span() { #[test] fn a_child_drawn_twice_moves_once() { let mut h = Harness::new((400, 200)); - // `Aligned` draws its child twice; listing it twice would move it twice. + // The span measures a child and then places it; listing it twice would + // move it twice. The span's own fixed total is shorter than the window, + // so the span is centred in it and everything under it carries that. let inner = rect(Color::BLUE).add(&mut h.rsc); let centered = inner.center().width(200).add(&mut h.rsc); let left = rect(Color::RED).width(100).add(&mut h.rsc); h.set_root((left, centered).span(Dir::RIGHT)); - assert_corners!(h, inner, (100, 0), (300, 200)); + assert_corners!(h, inner, (150, 0), (350, 200)); h.set_len(left, Axis::X, 150); h.frame(); - assert_corners!(h, inner, (150, 0), (350, 200)); + assert_corners!(h, inner, (175, 0), (375, 200)); +} + +#[test] +fn alignment_accepts_an_arbitrary_fraction_and_changes_at_runtime() { + let mut h = Harness::new((400, 200)); + let fixed = rect(Color::BLUE).sized((100, 100)).add(&mut h.rsc); + h.rsc + .widgets_mut() + .set_alignment(fixed, Axis::X, AxisAlign::new(0.25)); + h.rsc + .widgets_mut() + .set_alignment(fixed, Axis::Y, AxisAlign::NEG); + h.set_root(fixed); + assert_corners!(h, fixed, (75, 0), (175, 100)); + + h.rsc + .widgets_mut() + .set_alignment(fixed, Axis::X, AxisAlign::new(0.75)); + h.frame(); + assert_corners!(h, fixed, (225, 0), (325, 100)); } #[test] @@ -143,15 +165,17 @@ fn a_moved_subtree_takes_its_children_with_it() { let first = rect(Color::RED).height(40).add(&mut h.rsc); let inner = rect(Color::BLUE).add(&mut h.rsc); let row = inner.pad(10).height(40).region_node().add(&mut h.rsc); + // 80 of fixed rows in a 400 window, so the span takes 80 and sits in the + // middle of what it was given. h.set_root((first, row).span(Dir::DOWN)); - assert_corners!(h, inner, (10, 50), (390, 70)); + assert_corners!(h, inner, (10, 210), (390, 230)); h.set_len(first, Axis::Y, 80); h.frame(); // The row opted into one movable region, so its descendants follow one // entry rather than having their primitive regions rewritten. - assert_corners!(h, inner, (10, 90), (390, 110)); + assert_corners!(h, inner, (10, 230), (390, 250)); } #[test] diff --git a/tests/retained.rs b/tests/retained.rs index fe69e12..0494e54 100644 --- a/tests/retained.rs +++ b/tests/retained.rs @@ -159,10 +159,15 @@ fn a_span_child_that_declares_its_length_is_drawn_once() { h.set_root((hinted, asked).span(Dir::RIGHT)); assert_eq!(told_draws.get(), 1); + // Reading its box makes its drawing hold for the measuring box alone, + // and it reports less than that box: so it is drawn again in the box its + // answer places it in, and once more in the final box the span chooses. + // A widget that says what it holds for, as text does, skips the middle + // one. assert_eq!( asked_draws.get(), - 2, - "drawn to be measured, then again in its final box" + 3, + "drawn to be measured, in its placed box, then in its final box" ); } @@ -256,6 +261,11 @@ impl Widget for ReadsBox { /// Reads its box across one axis only, so its drawing holds for a taller /// box on its own and only a wider one is worth a draw. +/// +/// Both of these report a quarter of what they read, without saying that the +/// drawing holds there too, so each length they are asked at costs two draws: +/// one to answer, and one in the quarter-sized box that answer places them +/// in. The counts below are in those pairs. struct ReadsWidth { draws: Rc>, } @@ -336,7 +346,7 @@ fn a_resize_redraws_what_read_its_box() { h.resize((800, 100)); h.frame(); - assert_eq!(draws.get(), settled + 1); + assert_eq!(draws.get(), settled + 2); } #[test] @@ -356,7 +366,7 @@ fn a_resize_only_redraws_read_axes() { h.resize((800, 300)); h.frame(); - assert_eq!(draws.get(), settled + 1, "width changes its answer"); + assert_eq!(draws.get(), settled + 2, "width changes its answer"); } #[test] @@ -378,7 +388,7 @@ fn subpixel_resize_changes_accumulate_from_the_last_layout() { h.resize((400.06, 200.0)); h.frame(); - assert_eq!(draws.get(), settled + 1); + assert_eq!(draws.get(), settled + 2); } #[test] diff --git a/tests/shrink.rs b/tests/shrink.rs index 2a37f50..a16e901 100644 --- a/tests/shrink.rs +++ b/tests/shrink.rs @@ -76,9 +76,9 @@ enum Node { fn axis_align(v: u8) -> Option { match v % 4 { 0 => None, - 1 => Some(AxisAlign::Neg), - 2 => Some(AxisAlign::Center), - _ => Some(AxisAlign::Pos), + 1 => Some(AxisAlign::NEG), + 2 => Some(AxisAlign::CENTER), + _ => Some(AxisAlign::POS), } } @@ -94,6 +94,7 @@ impl Node { h: &mut Harness, out: &mut Vec, spans: &mut Vec>, + sized: &mut Vec, ) -> StrongWidget { let id: StrongWidget = match self { Node::Text(words, wrap) => { @@ -106,7 +107,10 @@ impl Node { Node::OneLine => wtext(ONE_LINE).size(16).wrap(false).add_strong(&mut h.rsc), Node::Rect => rect(Color::RED).add_strong(&mut h.rsc), Node::Span(down, gap, kids, order) => { - let mut built: Vec<_> = kids.iter().map(|k| Some(k.build(h, out, spans))).collect(); + let mut built: Vec<_> = kids + .iter() + .map(|k| Some(k.build(h, out, spans, sized))) + .collect(); // `order` is a permutation, so each is taken exactly once. let children = order .iter() @@ -126,7 +130,7 @@ impl Node { handle.add_strong(&mut h.rsc) } Node::Stack(kids) => { - let children = kids.iter().map(|k| k.build(h, out, spans)).collect(); + let children = kids.iter().map(|k| k.build(h, out, spans, sized)).collect(); Stack { children, size: StackSize::Child(0), @@ -134,7 +138,7 @@ impl Node { .add_strong(&mut h.rsc) } Node::Pad(p, kid) => { - let inner = kid.build(h, out, spans); + let inner = kid.build(h, out, spans, sized); Pad { padding: Padding { left: *p, @@ -147,30 +151,29 @@ impl Node { .add_strong(&mut h.rsc) } Node::Aligned(x, y, kid) => { - let inner = kid.build(h, out, spans); - Aligned { - inner, - align: Align { - x: axis_align(*x), - y: axis_align(*y), - }, + let inner = kid.build(h, out, spans, sized); + for (axis, align) in [(Axis::X, axis_align(*x)), (Axis::Y, axis_align(*y))] { + if let Some(align) = align { + h.rsc.widgets_mut().set_alignment(&inner, axis, align); + } } - .add_strong(&mut h.rsc) + inner } Node::Sized(x, y, kid) => { - let inner = kid.build(h, out, spans); + let inner = kid.build(h, out, spans, sized); h.rsc.widgets_mut().set_size_rules(&inner, *x, *y); + sized.push(inner.id()); inner } Node::Scroll(down, kid) => { - let inner = kid.build(h, out, spans); + let inner = kid.build(h, out, spans, sized); let axis = if *down { Axis::Y } else { Axis::X }; Scroll::new(inner, axis).add_strong(&mut h.rsc) } Node::Branch(probe, a, b, at) => { - let probe = probe.build(h, out, spans); - let wide = a.build(h, out, spans); - let narrow = b.build(h, out, spans); + let probe = probe.build(h, out, spans, sized); + let wide = a.build(h, out, spans, sized); + let narrow = b.build(h, out, spans, sized); Branch { probe, wide, @@ -184,6 +187,27 @@ impl Node { id } + /// The lengths every `Sized` node would carry after `resized`, in the + /// order `build` pushes them. + fn sized_lens(&self, out: &mut Vec<(Option, Option)>) { + match self { + Node::Text(..) | Node::OneLine | Node::Rect => {} + Node::Span(_, _, kids, _) | Node::Stack(kids) => { + kids.iter().for_each(|k| k.sized_lens(out)); + } + Node::Pad(_, k) | Node::Aligned(_, _, k) | Node::Scroll(_, k) => k.sized_lens(out), + Node::Sized(x, y, k) => { + k.sized_lens(out); + out.push((resized_len(*x), resized_len(*y))); + } + Node::Branch(p, a, b, _) => { + p.sized_lens(out); + a.sized_lens(out); + b.sized_lens(out); + } + } + } + fn size(&self) -> usize { 1 + match self { Node::Text(..) | Node::OneLine | Node::Rect => 0, @@ -377,6 +401,41 @@ enum Case { Repaint, ResizeRepaint, Reorder, + SizeChange, +} + +/// A different declared length, kept the same kind so the change is to the +/// value alone. +fn resized_len(len: Option) -> Option { + len.map(|len| Len { + px: len.px * 0.5 + 13.0, + rel: len.rel * 0.5, + leftover: len.leftover, + }) +} + +/// Every declared size changed, as a tree rather than as a change. +fn resized(node: &Node) -> Node { + match node { + Node::Span(down, gap, kids, order) => Node::Span( + *down, + *gap, + kids.iter().map(resized).collect(), + order.clone(), + ), + Node::Stack(kids) => Node::Stack(kids.iter().map(resized).collect()), + Node::Pad(p, k) => Node::Pad(*p, Box::new(resized(k))), + Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(resized(k))), + Node::Sized(x, y, k) => Node::Sized(resized_len(*x), resized_len(*y), Box::new(resized(k))), + Node::Scroll(d, k) => Node::Scroll(*d, Box::new(resized(k))), + Node::Branch(p, a, b, at) => Node::Branch( + Box::new(resized(p)), + Box::new(resized(a)), + Box::new(resized(b)), + *at, + ), + leaf => leaf.clone(), + } } /// Every span's children rotated by one, as a tree rather than as a change: @@ -413,7 +472,8 @@ fn diverges(node: &Node, case: Case) -> Option { let mut warm = Harness::new(start); let mut warm_ids = Vec::new(); let mut warm_spans = Vec::new(); - let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans); + let mut warm_sized = Vec::new(); + let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans, &mut warm_sized); warm.state.root = Some(root); // The frame that makes it warm: without it there is nothing retained and // the comparison is two cold starts agreeing with each other. @@ -434,16 +494,26 @@ fn diverges(node: &Node, case: Case) -> Option { } warm.frame(); } + if case == Case::SizeChange { + let mut lens = Vec::new(); + node.sized_lens(&mut lens); + for (id, (x, y)) in warm_sized.iter().zip(lens) { + warm.rsc.widgets_mut().set_size_rules(*id, x, y); + } + warm.frame(); + } // What the warm tree was moved into, grown that way from the start. let want = match case { Case::Reorder => reordered(node), + Case::SizeChange => resized(node), _ => node.clone(), }; let mut cold = Harness::new(INNER); let mut cold_ids = Vec::new(); let mut cold_spans = Vec::new(); - let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans); + let mut cold_sized = Vec::new(); + let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans, &mut cold_sized); cold.state.root = Some(root); cold.frame(); @@ -497,6 +567,7 @@ fn no_grown_tree_lays_out_differently_warm_than_cold() { "repaint" => Case::Repaint, "resize-repaint" => Case::ResizeRepaint, "reorder" => Case::Reorder, + "size-change" => Case::SizeChange, _ => Case::Resize, }; diff --git a/tests/trace_unsettled.rs b/tests/trace_unsettled.rs index b9e81ac..2d441be 100644 --- a/tests/trace_unsettled.rs +++ b/tests/trace_unsettled.rs @@ -11,14 +11,13 @@ fn plant(h: &mut Harness) -> Vec { let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc); let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc); let sized = wrapped.width(76).add(&mut h.rsc); - let aligned = Aligned { - inner: sized.add_strong(&mut h.rsc), - align: Align { - x: Some(AxisAlign::Pos), - y: Some(AxisAlign::Pos), - }, - } - .add(&mut h.rsc); + let aligned = sized; + h.rsc + .widgets_mut() + .set_alignment(sized, Axis::X, AxisAlign::POS); + h.rsc + .widgets_mut() + .set_alignment(sized, Axis::Y, AxisAlign::POS); let stack = Stack { children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)], size: StackSize::Child(0), @@ -94,14 +93,10 @@ fn what_box_the_text_is_drawn_in() { fn plant_fixed(h: &mut Harness) -> Vec { let words = "Wrapping shapes one source into as many lines as the box leaves"; let text = wtext(words).size(16).wrap(true).add(&mut h.rsc); - let aligned = Aligned { - inner: text.add_strong(&mut h.rsc), - align: Align { - x: Some(AxisAlign::Neg), - y: None, - }, - } - .add(&mut h.rsc); + let aligned = text; + h.rsc + .widgets_mut() + .set_alignment(text, Axis::X, AxisAlign::NEG); let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc); let sized = inner.sized((189, 176)).add(&mut h.rsc); let filler = rect(Color::RED).add(&mut h.rsc); diff --git a/tests/unsettled.rs b/tests/unsettled.rs index 3a5f735..046c644 100644 --- a/tests/unsettled.rs +++ b/tests/unsettled.rs @@ -16,14 +16,13 @@ fn plant(h: &mut Harness) -> Vec { let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc); let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc); let sized = wrapped.width(76).add(&mut h.rsc); - let aligned = Aligned { - inner: sized.add_strong(&mut h.rsc), - align: Align { - x: Some(AxisAlign::Pos), - y: Some(AxisAlign::Pos), - }, - } - .add(&mut h.rsc); + let aligned = sized; + h.rsc + .widgets_mut() + .set_alignment(sized, Axis::X, AxisAlign::POS); + h.rsc + .widgets_mut() + .set_alignment(sized, Axis::Y, AxisAlign::POS); let stack = Stack { children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)], size: StackSize::Child(0), @@ -96,14 +95,10 @@ fn repainting_everything_moves_nothing() { fn plant_fixed(h: &mut Harness) -> Vec { let words = "Wrapping shapes one source into as many lines as the box leaves"; let text = wtext(words).size(16).wrap(true).add(&mut h.rsc); - let aligned = Aligned { - inner: text.add_strong(&mut h.rsc), - align: Align { - x: Some(AxisAlign::Neg), - y: None, - }, - } - .add(&mut h.rsc); + let aligned = text; + h.rsc + .widgets_mut() + .set_alignment(text, Axis::X, AxisAlign::NEG); let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc); let sized = inner.sized((189, 176)).add(&mut h.rsc); let filler = rect(Color::RED).add(&mut h.rsc); @@ -167,14 +162,10 @@ fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec, WeakWidget (WeakWidget, WidgetId) { let content = Wider { extra }.add(&mut h.rsc); let scroll = Scroll::new(content.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc); - let root = Aligned { - inner: scroll.add_strong(&mut h.rsc), - align: Align { - x: Some(AxisAlign::Neg), - y: None, - }, - } - .add(&mut h.rsc); + let root = scroll; + h.rsc + .widgets_mut() + .set_alignment(scroll, Axis::X, AxisAlign::NEG); h.set_root(root); (content, scroll.id()) }