diff --git a/core/src/orientation/axis.rs b/core/src/orientation/axis.rs index 17b5cc9..279484d 100644 --- a/core/src/orientation/axis.rs +++ b/core/src/orientation/axis.rs @@ -7,6 +7,17 @@ pub enum Axis { Y, } +impl Axis { + /// A per-axis pair with `aligned` on this axis and `ortho` on the other, + /// which is what `from_axis` does for a vector. + pub fn pair(self, aligned: T, ortho: T) -> [T; 2] { + match self { + Self::X => [aligned, ortho], + Self::Y => [ortho, aligned], + } + } +} + impl std::ops::Not for Axis { type Output = Self; diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index f6e9710..4a2d0b8 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -9,12 +9,16 @@ use crate::{ #[derive(Debug)] pub struct ActiveData { pub id: WidgetId, - /// The box its drawing is in, in `parent_move`'s coordinates. + /// The box its parent gave it, in `parent_move`'s coordinates: what it + /// was asked about, and what a fraction under it is a fraction of. A + /// local redraw asks here. pub region: UiRegion, - /// The box its parent gave it, in the same coordinates: what it was - /// asked about, before its own answer placed its drawing inside it. - /// `region` is that placement, and a local redraw asks here. - pub given: UiRegion, + /// Where its drawing sits inside that box, in the box's own coordinates. + pub placement: UiRegion, + /// Whether its drawing read that placement, which is what says whether + /// moving it within the region is a redraw or only a different claim on + /// the same drawing. + pub reads_placement: bool, /// The same box as lengths of its parent's box, which is the one route /// to a box in pixels: a draw threads these down a level at a time, and /// [`crate::UiRenderState::redraw`] takes the same steps back up. diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 6d19417..76133b4 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -2,8 +2,8 @@ use crate::layout_diagnostics::{self as diag, Counter}; use crate::{ Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, RenderedText, Size, StrongWidget, - TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, - WidgetId, Widgets, + TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiSpan, UiVec2, + Weight, WidgetId, Widgets, render::{ GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind, TexturePrimitive, @@ -17,8 +17,20 @@ pub struct Painter<'a> { pub(super) state: &'a mut UiRenderState, pub(super) rsc: &'a mut dyn UiRsc, - /// This widget's box, in the coordinates of `move_idx`. + /// The box its parent gave it, in the coordinates of `move_idx`: what a + /// fraction of this widget's area is a fraction of, and what every region + /// it writes composes within. The same box on the ask that measures and + /// the ask that places, which is what keeps a fraction under it from + /// being resolved twice. pub(super) region: UiRegion, + /// Where this widget's drawing sits inside that box, in the box's own + /// coordinates: `FULL` while its answer is not yet known, and the box + /// its answer or its parent chose once one of them has. + pub(super) placement: UiRegion, + /// Whether this draw read its placement, which makes the drawing one + /// that holds for that placement alone -- the way reading a length in + /// pixels makes it hold for that length. + pub(super) reads_placement: bool, /// That box in pixels, which its children's are a length of: threaded /// down from the box this widget was given rather than composed back up /// the chain, so every length in layout is one multiply from its @@ -88,28 +100,37 @@ impl<'a> Painter<'a> { self.primitives.push(h); } - /// Writes a primitive to be rendered + /// Writes a primitive over the whole of this widget's own box. pub fn primitive(&mut self, primitive: impl PrimitiveLike) { + let at = self.placed(); let primitive = primitive.into_primitive(self); - self.primitive_at(primitive, self.region) + self.primitive_at(primitive, at) } + /// Writes one somewhere in this widget's region. A widget that wants its + /// own box rather than the region it was given composes through + /// [`Self::placement`] first. pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) { let primitive = primitive.into_primitive(self); self.primitive_at(primitive, region.within(&self.region)); } + /// `region` is in this widget's own region, as every region it writes is. pub fn set_mask(&mut self, region: UiRegion) { assert!(self.mask == MaskIdx::NONE); self.mask = self.rsc.ui_mut().masks.push(Mask { - region, + region: region.within(&self.region), move_idx: self.move_idx, }); } - /// Draws a widget within this widget's region. + /// Draws a widget in the whole of this widget's own box: it gets the + /// same region -- the same area for its fractions to be of -- and is put + /// where this widget was put. What a container that is only a wrapper + /// around one child wants, since its box is the child's. pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget) -> DrawResult<'s, 'a, W> { - self.widget_within(id, UiRegion::FULL) + let own = self.placement(); + self.widget_at(id, UiRegion::FULL, [Some(own.x), Some(own.y)]) } /// What a widget's rules declare its lengths to be, which whoever draws @@ -129,51 +150,41 @@ impl<'a> Painter<'a> { } /// 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. + /// own region, and is the child's own region: what its declared lengths + /// and its report are fractions of. Where its drawing sits inside that is + /// its own answer placed by its alignment. pub fn widget_within<'s, W: ?Sized>( &'s mut self, id: &'s StrongWidget, region: UiRegion, ) -> DrawResult<'s, 'a, W> { - self.widget_at(id, region, region.size(), [false; 2]) + self.widget_at(id, region, [None; 2]) } - /// Draws a widget in `region`, saying what the answer means. + /// Draws a widget in `region`, saying where in it the drawing goes. /// - /// `reports_of` is what a fraction the child reports is a fraction of, as - /// lengths of this widget's own box. It is the box the child was given - /// wherever that box is the child's whole area -- a pad's inset, a stack - /// child, a scroll's content -- and a span passes its own extent along - /// the row instead: it offers each child the room left from its cursor, - /// because a text has to wrap at the width actually there, while - /// `rel(0.5)` still means half the span wherever the child sits in it. + /// `region` is the child's own area: what a fraction it declares or + /// reports is a fraction of, and the coordinates the regions it writes + /// compose within. It is the same box on the ask that measures and the + /// ask that places, which is what stops a fraction under it being + /// resolved twice. /// - /// A `decided` axis is one where this box was chosen from the widget's - /// own answer. On those the answer is not placed inside the box again: it - /// already is the box, and a fraction taken of it a second time would - /// shrink it twice. A container uses that where it hands back exactly - /// what a child asked for -- a span placing a child at the length it - /// reported, a scroll giving its content the content's own length. + /// `placement` is what of that region the child's drawing takes, per + /// axis, wherever this widget is choosing. `None` leaves the axis to the + /// child's own answer and alignment, which is what + /// [`Self::widget_within`] passes. A span passes the whole row as the + /// region, so `rel(0.5)` is half the row wherever the child sits in it, + /// and places the child by passing the slot along its axis. pub fn widget_at<'s, W: ?Sized>( &'s mut self, id: &'s StrongWidget, region: UiRegion, - reports_of: UiVec2, - decided: [bool; 2], + placement: [Option; 2], ) -> DrawResult<'s, 'a, W> { let region_node = self.rsc.widgets().is_region_node(id.id()); let declared = self.declared_lens(id); let align = 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, align), - false => region, - }; + let (local, placement) = ask_box(region, declared, align, placement); let within = match local == UiRegion::FULL { true => self.region, false => local.within(&self.region), @@ -219,7 +230,7 @@ impl<'a> Painter<'a> { offer_len, px, offered_px, - decided, + placement, }, None, self.rsc, @@ -235,7 +246,7 @@ impl<'a> Painter<'a> { DrawResult { child: id, painter: self, - size: in_parent_frame(size, reports_of, declared), + size: in_parent_frame(size, local.size(), declared), } } @@ -267,20 +278,18 @@ impl<'a> Painter<'a> { } } - /// A child's length in the box it is about to be offered, if it can be - /// had without drawing it: from its hint, or from a drawing it already - /// has that holds for that box. `reports_of` is what a fraction in the - /// answer is a fraction of, as it is for [`Self::widget_at`]. + /// A child's length in the region it is about to be offered, if it can + /// be had without drawing it: from its hint, or from a drawing it already + /// has that holds for that box. pub fn known_len( &mut self, child: &StrongWidget, axis: Axis, region: UiRegion, - reports_of: UiVec2, ) -> Option { let declared = self.declared_lens(child); let align = self.rsc.widgets().alignment(child.id()); - let local = declared_box(region, declared, align); + let (local, _) = ask_box(region, declared, align, [None; 2]); let first_ask = self.offer(child.id()); if first_ask && let Some(active) = self.state.active.get_mut(&child.id()) { active.offer_len = local.size(); @@ -302,7 +311,7 @@ impl<'a> Painter<'a> { for (axis, under) in AXES.into_iter().zip(self.under.iter_mut()) { *under = under.and(holds[axis as usize].through(local.axis(axis).len())); } - Some(in_parent_frame(size, reports_of, declared).axis(axis)) + Some(in_parent_frame(size, local.size(), declared).axis(axis)) } /// Whether this is the first box a child is asked about in during a draw @@ -334,8 +343,10 @@ impl<'a> Painter<'a> { ui.text.render(buffer, attrs, width) } + /// `origin` is in this widget's own region, as every region it writes is. // TODO: merge the text methods into the primitive ones. pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { + let origin = origin.within(&self.region); let kind = self.rsc.ui_mut().primitives.kind::(); for glyph in text.glyphs.iter() { let mut region = origin; @@ -362,12 +373,35 @@ impl<'a> Painter<'a> { } } - /// This widget's box, in the coordinates its own primitives are written - /// in -- so a region composed `within` it may be drawn directly. + /// The box this widget's parent gave it, in the coordinates its own + /// primitives are written in -- so a region composed `within` it may be + /// drawn directly. Its own box is [`Self::placement`] of this one. pub fn region(&self) -> UiRegion { self.region } + /// Where this widget's drawing goes inside the box it was given, in that + /// box's coordinates: what its own answer took of it, or what its parent + /// chose for it. `FULL` on the ask that measures, since nothing has been + /// placed yet. + /// + /// Reading it is what says the drawing depends on it, so a widget that + /// positions its own content reads it and is drawn again once its box is + /// known, and one that fills whatever it is given never is. + pub fn placement(&mut self) -> UiRegion { + self.reads_placement = true; + self.placement + } + + /// This widget's own box in the coordinates its primitives are written + /// in: its placement composed through the region it was given. + fn placed(&mut self) -> UiRegion { + match self.placement() == UiRegion::FULL { + true => self.region, + false => self.placement.within(&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. @@ -401,20 +435,47 @@ impl<'a> Painter<'a> { placed_box(UiRegion::FULL, lens, RegionAlign::NEAR) } - /// 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. + /// This widget's own 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) -> PxVec2 { - for (own, len) in self.own.iter_mut().zip([self.px.x, self.px.y]) { - if *own == Holds::ANY { - *own = Holds::at(len); - } - } - self.px + PxVec2::new(self.px_len(Axis::X), self.px_len(Axis::Y)) } - /// One axis of this widget's box in pixels. Prefer this to + /// One axis of this widget's own box in pixels. Prefer this to /// [`Self::px_size`] when the other axis cannot affect the drawing. pub fn px_len(&mut self, axis: Axis) -> Px { + let part = self.placement().axis(axis).len(); + let len = part.to_px(self.px.axis(axis)); + let own = &mut self.own[axis as usize]; + if *own == Holds::ANY { + *own = Holds::at(len).through(part); + } + len + } + + /// The lengths of this widget's own box on `axis` that what it is drawing + /// holds for -- the same primitives, in the same fractions and offsets + /// of the box, and the same reported size. A widget that read its length + /// in pixels holds for that one alone until it says otherwise. + pub fn holds(&mut self, axis: Axis, holds: impl Into) { + let part = self.placement().axis(axis).len(); + let holds = holds.into(); + debug_assert!( + holds.contains(part.to_px(self.px.axis(axis))), + "'{}' ({:?}) says its drawing holds for lengths that leave out its own box", + self.label(), + self.id + ); + // Kept as a range of the region's lengths, which is the one variable + // every range here is about: its own box is a part of that box, and + // `through` is the exact preimage of taking the part. + self.own[axis as usize] = holds.through(part); + } + + /// One axis of the box this widget's parent gave it, in pixels -- what a + /// fraction of its area resolves against, and so what a container divides + /// among its children. Its own box is a part of this one. + pub fn region_px_len(&mut self, axis: Axis) -> Px { let len = self.px.axis(axis); let own = &mut self.own[axis as usize]; if *own == Holds::ANY { @@ -423,15 +484,14 @@ impl<'a> Painter<'a> { len } - /// The lengths of this widget's box on `axis` that what it is drawing - /// holds for -- the same primitives, in the same fractions and offsets - /// of the box, and the same reported size. A widget that read its - /// length in pixels holds for that one alone until it says otherwise. - pub fn holds(&mut self, axis: Axis, holds: impl Into) { + /// [`Self::holds`] stated about the region rather than about this + /// widget's own box, for a container whose drawing turns on the box it + /// was given rather than on the part of it it took. + pub fn region_holds(&mut self, axis: Axis, holds: impl Into) { let holds = holds.into(); debug_assert!( holds.contains(self.px.axis(axis)), - "'{}' ({:?}) says its drawing holds for lengths that leave out its own box", + "'{}' ({:?}) says its drawing holds for lengths that leave out its region", self.label(), self.id ); @@ -521,19 +581,16 @@ impl PrimitiveLike for &TextureHandle { } } -/// A child's answer as lengths of the parent's own box. A widget reports a -/// fraction, and `reports_of` is the length that fraction is of: the box the -/// child was given wherever that is the child's whole area, and the parent's -/// own extent wherever the box is a positional remainder, as a span's is -/// after an earlier child. Pixels come through untouched either way, being -/// that many pixels wherever they end up. A declared axis is already the -/// parent's: it resolved the rule in its own box, and the rule is what the -/// report says. -fn in_parent_frame(size: Size, reports_of: UiVec2, declared: [Option; 2]) -> Size { +/// A child's answer as lengths of the parent's own region. A widget reports +/// a fraction of its own region, and `of` is that region as a length of this +/// one. Pixels come through untouched, being that many pixels wherever they +/// end up. A declared axis is already the parent's: it resolved the rule in +/// its own region, and the rule is what the report says. +fn in_parent_frame(size: Size, of: UiVec2, declared: [Option; 2]) -> Size { let mut size = size; for (axis, declared) in AXES.into_iter().zip(declared) { if declared.is_none() { - *size.axis_mut(axis) = size.axis(axis).within_len(reports_of.axis(axis)); + *size.axis_mut(axis) = size.axis(axis).within_len(of.axis(axis)); } } size @@ -562,9 +619,9 @@ pub(crate) fn declared_lens(widgets: &Widgets, id: WidgetId) -> [Option, decided: bool) -> bool { reported.leftover != Weight::ZERO || declared.is_some() || decided @@ -612,21 +669,35 @@ pub(crate) fn placed_box(region: UiRegion, lens: UiVec2, align: RegionAlign) -> 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, 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( +/// A child's own region and where in it its drawing goes, from the box it is +/// offered, the lengths its rules declare, and what its parent chose. +/// +/// A rule gives the region its length outright -- that is what makes a rule +/// win, and it is why a widget under one never learns of it -- and the region +/// then sits where its parent placed it, or where its alignment says if its +/// parent left the axis open. With no rule the region is the whole of what +/// was offered, since that is the area a fraction under it is a fraction of, +/// and what the parent chose is where in it the drawing goes. So the two +/// coordinate spaces are the same one wherever a placement survives. +pub(crate) fn ask_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 }; + placement: [Option; 2], +) -> (UiRegion, [Option; 2]) { + let mut placed = [None; 2]; + for (axis, (len, chosen)) in AXES.into_iter().zip(declared.into_iter().zip(placement)) { + let Some(len) = len else { + placed[axis as usize] = chosen; + continue; + }; let span = region.axis_mut(axis); let len = Len::from_parts(len.rel, len.px); - span.start += (span.len() - len).scale(align.axis(axis).rel()); + span.start = match chosen { + Some(chosen) => chosen.start, + None => span.start + (span.len() - len).scale(align.axis(axis).rel()), + }; span.end = span.start + len; } - region + (region, placed) } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index cce4a39..206c6f3 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1,6 +1,6 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; -use crate::ui::painter::{declared_box, declared_lens, placed_box, placed_lens}; +use crate::ui::painter::{ask_box, declared_lens, placed_box, placed_lens}; use crate::{ ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter, PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight, @@ -30,10 +30,27 @@ pub(super) struct DrawInfo { /// parent's own, which is where every pixel length in layout comes from. pub px: PxVec2, pub offered_px: PxVec2, - /// The axes along which the parent chose this box from the widget's own - /// answer, so the answer is not placed inside it again. See - /// [`Painter::widget_at`]. - pub decided: [bool; 2], + /// What of that region the parent chose to put the drawing in, per axis. + /// `None` leaves the axis to the widget's own answer and its alignment. + /// See [`Painter::widget_at`]. + pub placement: [Option; 2], +} + +impl DrawInfo { + /// The axes the parent chose the placement on, which are the axes the + /// answer is not placed inside its region again. + fn decided(&self) -> [bool; 2] { + self.placement.map(|span| span.is_some()) + } + + /// The placement to draw in before the answer is known: what the parent + /// chose, and the whole region on any axis it left open. + fn offered_placement(&self) -> UiRegion { + UiRegion { + x: self.placement[0].unwrap_or(UiSpan::FULL), + y: self.placement[1].unwrap_or(UiSpan::FULL), + } + } } pub struct UiRenderState { @@ -127,7 +144,7 @@ impl UiRenderState { offer_len: UiVec2::FULL_SIZE, px, offered_px: px, - decided: [false; 2], + placement: [None; 2], } } @@ -181,11 +198,13 @@ impl UiRenderState { } fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion { - declared_box( + ask_box( UiRegion::FULL, declared_lens(widgets, id), widgets.alignment(id), + [None; 2], ) + .0 } pub(super) fn draw_inner( @@ -217,41 +236,41 @@ impl UiRenderState { if old.is_none() { old = self.remove(id, false, rsc); } - self.draw_at(id, region, info, old.take(), rsc) + self.draw_at(id, region, info.offered_placement(), info, old.take(), rsc) }); let declared = declared_lens(rsc.widgets(), id); - // The second, final ask is in a box chosen from the answer on both - // axes, which is also what makes it terminate. - let lens = placed_lens(answer.0, declared, info.decided); - let placed = placed_box(region, lens, align); - let placed_info = DrawInfo { - px: lens.to_px(info.px), - decided: [true; 2], - ..info + // Where the drawing goes, in the region's own coordinates: what the + // parent chose, and on any axis it left open, what the answer took of + // the region placed by the widget's alignment. The region itself does + // not change, so nothing under it resolves a fraction a second time. + let lens = placed_lens(answer.0, declared, info.decided()); + let own = placed_box(UiRegion::FULL, lens, align); + let placement = UiRegion { + x: info.placement[0].unwrap_or(own.x), + y: info.placement[1].unwrap_or(own.y), }; - self.place(id, placed, placed_info, rsc); + self.place(id, region, placement, info, rsc); // The answer is only reusable while both parts of the operation are: - // what the widget reported in the box it was asked in, and what it - // drew in the box its report selected. Express the latter's contract - // back in terms of the box asked in before handing it to the parent. + // what the widget reported, and what it drew once placed. Both are + // ranges of the region's own lengths, since that is the box neither + // ask changes. let drawing_holds = self.active[&id].holds; let mut settled = answer; for axis in AXES { - settled.1[axis as usize] = - settled.1[axis as usize].and(drawing_holds[axis as usize].through(lens.axis(axis))); + settled.1[axis as usize] = settled.1[axis as usize].and(drawing_holds[axis as usize]); } let active = self.active.get_mut(&id).unwrap(); // Whoever asked owns how the box was reached: the box it stated, and // what of that box the answer then took. A local redraw asks the // same question again from these. - active.given = region; + active.region = region; active.given_len = info.given_len; active.offer_len = info.offer_len; active.answer = settled; - active.decided = info.decided; + active.decided = info.decided(); active.own_align = align; // A subtree can be reused whole under a different parent -- same box, // same layer, same region node -- and nothing in the drawing says it @@ -268,17 +287,33 @@ impl UiRenderState { settled } - /// Draws a widget in the final box its answer chose, reusing the drawing - /// already there where its retained contract holds for that box. The - /// symbolic box can be unchanged while the box it sits in changed pixel - /// length, so what reuse checks is the box in pixels. - fn place(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) { - if self.try_reuse(id, 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, info, old, rsc); + /// Puts the drawing where the answer says it goes. A drawing that never + /// read its placement is the same drawing wherever it is put, so all that + /// changes is what box the widget is recorded as occupying; one that read + /// it is drawn again, and only where the placement it read has moved. + fn place( + &mut self, + id: WidgetId, + region: UiRegion, + placement: UiRegion, + info: DrawInfo, + rsc: &mut dyn UiRsc, + ) { + let active = &self.active[&id]; + // A drawing that never read its placement is the same drawing + // wherever it is put, so only what the widget is recorded as + // occupying changes; one that read it stands only for the placement + // it read. Either way the drawing still has to be where the region + // now is, which is what a retained answer on its own does not do. + let stands = !active.reads_placement || active.placement == placement; + if stands && self.try_reuse(id, region, info, rsc).is_some() { + self.active.get_mut(&id).unwrap().placement = placement; + return; } + #[cfg(feature = "layout-diagnostics")] + diag::bump(Counter::PlaceRedraws); + let old = self.remove(id, false, rsc); + self.draw_at(id, region, placement, info, old, rsc); } /// Calls a widget's `draw` and keeps what it drew in `region`. @@ -286,6 +321,7 @@ impl UiRenderState { &mut self, id: WidgetId, region: UiRegion, + placement: UiRegion, info: DrawInfo, old: Option, rsc: &mut dyn UiRsc, @@ -317,6 +353,8 @@ impl UiRenderState { let mut painter = Painter { state: self, region: local, + placement, + reads_placement: false, px, mask: info.mask, layer: info.layer, @@ -351,6 +389,8 @@ impl UiRenderState { state: _, rsc: _, region: _, + placement: _, + reads_placement, px: _, mask, textures, @@ -425,7 +465,7 @@ impl UiRenderState { offer_len: UiVec2::FULL_SIZE, px, offered_px: px, - decided: [false; 2], + placement: [None; 2], }, rsc, ); @@ -436,10 +476,8 @@ impl UiRenderState { let active = ActiveData { id, region, - // The box a placing ask draws in is a part of the one its parent - // gave, which `draw_inner` writes back over these once the - // placement is done. - given: region, + placement, + reads_placement, given_len: info.given_len, offer_len: info.offer_len, // Whoever asked writes the answer, if this was the asking. @@ -454,7 +492,7 @@ impl UiRenderState { children, size_deps, declared: declared_lens(rsc.widgets(), id), - decided: info.decided, + decided: info.decided(), own_align: rsc.widgets().alignment(id), move_idx, parent_move: info.parent_move, @@ -540,11 +578,7 @@ impl UiRenderState { // Nothing above the root: the window is where a fraction becomes // pixels, which is also the whole of the box the root is given. let (parent_px, parent_offer) = match active.parent.and_then(|p| self.active.get(&p)) { - Some(parent) => { - let (given, offer) = self.asked_px(parent.id); - let lens = placed_lens(parent.answer.0, parent.declared, parent.decided); - (lens.to_px(given), offer) - } + Some(parent) => self.asked_px(parent.id), None => (self.output_size, self.output_size), }; let px = active.given_len.to_px(parent_px); @@ -639,7 +673,6 @@ impl UiRenderState { self.redepth(id, info.depth); let active = self.active.get_mut(&id).unwrap(); active.region = region; - active.given = region; active.given_len = info.given_len; active.offer_len = info.offer_len; #[cfg(feature = "layout-diagnostics")] @@ -694,7 +727,6 @@ impl UiRenderState { rsc: &mut dyn UiRsc, ) { let active = self.active.get_mut(&id).unwrap(); - active.given = remap.apply(active.given); if active.move_idx != parent_move { let region = remap.apply(active.region); active.region = region; @@ -792,7 +824,8 @@ impl UiRenderState { ActiveData { id, region: UiRegion::FULL, - given: UiRegion::FULL, + placement: UiRegion::FULL, + reads_placement: false, given_len: UiVec2::FULL_SIZE, offer_len: UiVec2::FULL_SIZE, answer: (size, [Holds::ANY; 2]), @@ -951,8 +984,9 @@ impl UiRenderState { pub fn window_region(&self, id: &impl IdLike) -> Option { let active = self.active.get(&id.id())?; active.drawn.then(|| { + let placed = active.placement.within(&active.region); self.moves - .resolve(active.parent_move, active.region) + .resolve(active.parent_move, placed) .to_px(self.output_size) }) } @@ -1031,9 +1065,12 @@ impl UiRenderState { offer_len: active.offer_len, px: given_px, offered_px, - decided: active.decided, + // The same question its parent asked: the axes its parent chose + // the placement on, put back where they were. + placement: AXES + .map(|axis| active.decided[axis as usize].then(|| *active.placement.axis(axis))), }; - let (given, was_answer) = (active.given, active.answer); + let (given, was_answer) = (active.region, active.answer); #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::LocalRedraws); diff --git a/src/widget/mask.rs b/src/widget/mask.rs index 45fd608..db58eb2 100644 --- a/src/widget/mask.rs +++ b/src/widget/mask.rs @@ -6,7 +6,8 @@ pub struct Masked { impl Widget for Masked { fn draw(&mut self, painter: &mut Painter) -> Size { - painter.set_mask(painter.region()); + let own = painter.placement(); + painter.set_mask(own); painter.widget(&self.inner); // What it occupies is its box, on both axes, for the reason `Scroll` // reports the same: it clips what is inside to that box, so it can diff --git a/src/widget/position/pad.rs b/src/widget/position/pad.rs index 7141a4d..1257c26 100644 --- a/src/widget/position/pad.rs +++ b/src/widget/position/pad.rs @@ -13,9 +13,8 @@ impl Widget for Pad { // it; where the box is bigger -- a share of a row, a rule over this // widget -- the slack is the inner's to sit in, and forcing the near // edge pinned it to a corner it had not asked for. - let inner = painter - .widget_within(&self.inner, self.padding.region()) - .size(); + let inside = self.padding.region_of(painter.placement()); + let inner = painter.widget_within(&self.inner, inside).size(); Size { x: LayoutLen { px: inner.x.px + self.padding.left + self.padding.right, @@ -53,14 +52,18 @@ impl Padding { bottom: amt, } } - pub fn region(&self) -> UiRegion { - let mut region = UiRegion::FULL; + /// `region` less this padding on each side. + pub fn region_of(&self, mut region: UiRegion) -> UiRegion { region.x.start.px += self.left; region.y.start.px += self.top; region.x.end.px -= self.right; region.y.end.px -= self.bottom; region } + + pub fn region(&self) -> UiRegion { + self.region_of(UiRegion::FULL) + } pub fn x(amt: impl UiNum) -> Self { let amt = Px::from_num(amt); Self { diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index 199ff70..4981b64 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -15,7 +15,7 @@ impl Widget for Scroll { // Draw in the whole container only when its scrolling-axis length is // not already known, then draw it at the scrolled offset. let whole = UiRegion::FULL; - let answer_len = match painter.known_len(&self.inner, self.axis, whole, whole.size()) { + let answer_len = match painter.known_len(&self.inner, self.axis, whole) { Some(len) => len, None => painter.widget(&self.inner).size().axis(self.axis), }; @@ -64,7 +64,11 @@ impl Widget for Scroll { region = region.offset(offset); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); } - painter.widget_at(&self.inner, region, region.size(), [true; 2]); + // The viewport is the inner's region, so a fraction it declares or + // reports is a fraction of what is on screen rather than of the + // content box its own answer decided. Where it is put is the content + // box, scrolled. + painter.widget_at(&self.inner, whole, [Some(region.x), Some(region.y)]); // 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 diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index 139afa3..d6cb611 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -10,25 +10,36 @@ pub struct Span { impl Widget for Span { fn draw(&mut self, painter: &mut Painter) -> Size { let axis = self.dir.axis; + // The row: this span's own box, as a span of the region it was given. + // Its children are laid out along it, and what they declare or report + // is a fraction of the region -- the area this span was told it has, + // which it passes on unchanged. + let own = painter.placement(); + let row = *own.axis(axis); + // Across itself the span's own box is the child's region: a span is + // what contains its children there, and nothing divides that axis. + // Along it the whole region is, so a fraction means the same thing + // for every child however much of the row is left when it is asked. + let region = UiRegion::from_axis(axis, UiSpan::FULL, *own.axis(!axis)); + let along = |from: Len, to: Len| match self.dir.sign { + Sign::Pos => UiSpan::new(row.start + from, row.start + to), + Sign::Neg => UiSpan::new(row.end - to, row.end - from), + }; + let far = row.len(); // A length for every child before their final boxes are chosen: from // a hint where one exists, and from drawing otherwise. let mut cursor = Len::rel_min(); let mut lens = Vec::with_capacity(self.children.len()); for child in &self.children { - let mut span = UiSpan::new(cursor, Len::rel_max()); - if self.dir.sign == Sign::Neg { - span.flip(); - } - let region = UiRegion::from_axis(axis, span, UiSpan::FULL); - // Offered the room left from the cursor, because a text has to - // wrap at the width actually there, but reporting a fraction of - // the whole row: `rel(0.5)` is half the span whatever else is in - // it and wherever this child sits among them. - let len = match painter.known_len(child, axis, region, UiVec2::FULL_SIZE) { + // The whole region is the child's, so `rel(0.5)` is half the area + // this span was given whatever else is in it and wherever this + // child sits among them. What it is placed in is the room left + // from the cursor, because a text has to wrap at the width + // actually there. + let room = axis.pair(Some(along(cursor, far)), None); + let len = match painter.known_len(child, axis, region) { Some(len) => len, - None => painter - .widget_at(child, region, UiVec2::FULL_SIZE, [false; 2]) - .len(axis), + None => painter.widget_at(child, region, room).len(axis), }; cursor.px += len.px + self.gap; cursor.rel += len.rel; @@ -46,9 +57,9 @@ impl Widget for Span { |sum, len| sum + *len, ); - // What is left for the shares to divide: the box less everything - // fixed, as a length of the box rather than a number of pixels. - let room = Len::rel_max() - Len::from_parts(total.rel, total.px); + // What is left for the shares to divide: the row less everything + // fixed, as a length of the region rather than a number of pixels. + let room = far - Len::from_parts(total.rel, total.px); // Whether anything is left over is a question in pixels: `rel(0.5)` // beside 300 px is full at 600 and overfull at 400. Asked of `room` // itself, and answered back through the same expression, so the @@ -60,12 +71,12 @@ impl Widget for Span { // exist at all turns on this. let mut shares = false; if total.leftover > Weight::ZERO { - shares = room.to_px(painter.px_len(axis)) > Px::ZERO; + shares = room.to_px(painter.region_px_len(axis)) > Px::ZERO; let holds = match shares { true => Holds::from(Px::STEP..=Px::MAX), false => Holds::from(Px::MIN..=Px::ZERO), }; - painter.holds(axis, holds.through(room)); + painter.region_holds(axis, holds.through(room)); } // Across itself a span is as long as its longest child -- unless a @@ -94,28 +105,19 @@ impl Widget for Span { fixed.px += self.gap; continue; } - let mut span = UiSpan::FULL; - span.start = start; + let from = start; if len.leftover > Weight::ZERO && shares { taken += len.leftover; } fixed.px += len.px; fixed.rel += len.rel; start = shared(fixed, taken, total.leftover, room); - span.end = start; - let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL); - if self.dir.sign == Sign::Neg { - region.flip(axis); - } - // Along the row this box is the child's own answer, so the answer - // is not placed in it again; across it the child sits where its - // alignment says. - let placed = painter.widget_at( - child, - region, - UiVec2::FULL_SIZE, - [axis == Axis::X, axis == Axis::Y], - ); + // Along the row the span says where the child goes; across it the + // child sits where its own alignment says. Its region is the + // whole of what this span was given either way, which is what its + // fractions are of. + let placed = + painter.widget_at(child, region, axis.pair(Some(along(from, start)), None)); if shrinks { let used = placed.len(!axis); // Choosing between a fixed and a relative length from the diff --git a/src/widget/position/stack.rs b/src/widget/position/stack.rs index 2fd06e5..ef19b47 100644 --- a/src/widget/position/stack.rs +++ b/src/widget/position/stack.rs @@ -13,31 +13,37 @@ impl Widget for Stack { StackSize::Default => None, StackSize::Child(i) => Some(i), }; - // 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. + // This stack's own box, which is `FULL` until its answer is known. + let placement = painter.placement(); + // Whichever child sizes the stack keeps the stack's whole region as + // its own -- the stack is the length that child asked for, so taking + // the fraction of the stack's box again would take it twice -- and is + // put where the stack itself is put. let size = match sizing.and_then(|i| self.children.get(i).map(|c| (i, c))) { // On the layer that child ends up on, so the ask below is a reuse // rather than a second drawing of it somewhere else: a retained // drawing belongs to the layer it was made on. Some((i, child)) => { painter.child_layer_at(i); - painter.widget(child).size() + painter + .widget_at( + child, + UiRegion::FULL, + [Some(placement.x), Some(placement.y)], + ) + .size() } None => Size::LEFTOVER, }; - let region = painter.box_of(size); for (i, child) in self.children.iter().enumerate() { + if sizing == Some(i) { + continue; + } painter.child_layer_at(i); - // The sizing child placed its own content in the box its answer - // decided, and this box was derived from that answer, so applying - // its alignment again here would place it twice. Every other - // child is handed a box that owes nothing to its own answer, and - // where it sits in one bigger than itself is its own business. - match sizing == Some(i) { - true => painter.widget_at(child, region, region.size(), [true; 2]), - false => painter.widget_within(child, region), - }; + // Every other child has the stack's own box for its region, since + // the stack is what contains it, and where it sits in one bigger + // than itself is its own business. + painter.widget_within(child, placement); } size } diff --git a/src/widget/text/mod.rs b/src/widget/text/mod.rs index eb776a3..eea7d1e 100644 --- a/src/widget/text/mod.rs +++ b/src/widget/text/mod.rs @@ -89,7 +89,7 @@ impl TextView { // hair under that line, and the break made in it is not the break a // cold layout makes there. let size = Size::from_px(PxVec2::ceil_from_f32(tex.size)); - let within = region.within(&painter.region()); + let within = region.within(&painter.placement()); painter.glyphs(tex, within); (region, size) } diff --git a/tests/cases/layout.rs b/tests/cases/layout.rs index acd7e31..d7a4d33 100644 --- a/tests/cases/layout.rs +++ b/tests/cases/layout.rs @@ -20,11 +20,12 @@ fn a_span_gives_each_child_the_width_it_asked_for() { assert_corners!(h, right, (100, 0), (400, 200)); } -/// A span offers each child the room left after the one before, because a -/// text has to wrap at the width actually there, but reads what the child -/// reports as a fraction of the whole row. So two children asking for half -/// each take the whole row between them, however much of it was left when -/// each was asked, and a third overflows. +/// A span places each child in the room left after the one before, because a +/// text has to wrap at the width actually there, but the child's region is +/// the whole row. So two children asking for half each take the whole row +/// between them, however much of it was left when each was asked, and a third +/// overflows -- and a span passes its own region on unchanged, so a child of +/// a nested span asking for half asks for half of the same row. #[test] fn a_span_reads_a_child_report_as_a_fraction_of_the_row() { let mut h = Harness::new((400, 100)); @@ -34,10 +35,10 @@ fn a_span_reads_a_child_report_as_a_fraction_of_the_row() { let tail = rect(Color::BLUE).width(100).add(&mut h.rsc); h.set_root((half, nested, tail).span(Dir::RIGHT).width(rel(1.0))); - // The nested span is placed at the length it reported and drawn there - // once more; half of that final box is what its own child takes. + // The nested span is placed at the length it reported, and its own child + // asks for half of the row rather than half of that placement. assert_corners!(h, nested, (200, 0), (400, 100)); - assert_corners!(h, inner, (200, 0), (300, 100)); + assert_corners!(h, inner, (200, 0), (400, 100)); assert_corners!(h, tail, (400, 0), (500, 100)); }