diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index 7b84148..cff380a 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -1,5 +1,6 @@ use crate::{ - LayerId, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId, util::Vec2, + LayerId, Len, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId, + util::Vec2, }; /// important non rendering data for retained drawing @@ -38,6 +39,10 @@ pub struct ActiveData { /// The slot its primitives are positioned through: its own if its parent /// placed it, otherwise the nearest ancestor that has one. pub move_idx: MoveIdx, + /// The declared lengths whoever drew this widget resolved into its box. + /// 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 slot `region` is given in, which is whatever its parent drew in. pub parent_move: MoveIdx, pub mask: MaskIdx, diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index afdaaa2..c8f801d 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -2,7 +2,7 @@ use crate::layout_diagnostics::{self as diag, Counter}; use crate::{ Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, - UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, + UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, render::{ GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind, TexturePrimitive, @@ -90,7 +90,14 @@ 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, self.region, false) + let declared = self.declared_lens(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 region = match declared.iter().any(Option::is_some) { + true => declared_box(UiRegion::FULL, declared).within(&self.region), + false => self.region, + }; + self.widget_at(id, region, false, declared) } /// Draws a widget somewhere within this one. @@ -99,8 +106,21 @@ impl<'a> Painter<'a> { id: &'s StrongWidget, region: UiRegion, ) -> DrawResult<'s, 'a, W> { - let region = region.within(&self.region); - self.widget_at(id, region, false) + let declared = self.declared_lens(id); + let region = declared_box(region, declared).within(&self.region); + self.widget_at(id, region, false, declared) + } + + /// What a widget declares its lengths to be, which whoever draws it + /// resolves into its box. `rest` is not among them: a share of what is + /// left over is only a length to the widget dividing one, so it passes + /// up in the size instead. Reading it depends on nothing -- the box that + /// comes of it is kept on the child, and `redraw` compares it there. + fn declared_lens(&self, id: &StrongWidget) -> [Option; 2] { + let Some(widget) = self.rsc.widgets().get_dyn(id.id()) else { + return [None; 2]; + }; + [Axis::X, Axis::Y].map(|axis| declared_len(widget, axis)) } /// Draws a child this widget decides the box of, and may decide again @@ -116,10 +136,11 @@ impl<'a> Painter<'a> { ) -> DrawResult<'s, 'a, W> { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::PlaceCalls); - let region = region.within(&self.region); + let declared = self.declared_lens(id); + let region = declared_box(region, declared).within(&self.region); #[cfg(feature = "layout-diagnostics")] diag::placed(id.id(), self.id, region); - self.widget_at(id, region, true) + self.widget_at(id, region, true, declared) } fn widget_at<'s, W: ?Sized>( @@ -127,6 +148,7 @@ impl<'a> Painter<'a> { id: &'s StrongWidget, region: UiRegion, slotted: bool, + declared: [Option; 2], ) -> DrawResult<'s, 'a, W> { // A child listed twice would be moved twice. if !self.children.contains(&id.id()) { @@ -145,6 +167,9 @@ impl<'a> Painter<'a> { self.rsc, ); self.offer(id.id(), region); + if let Some(active) = self.state.active.get_mut(&id.id()) { + active.declared = declared; + } DrawResult { child: id, painter: self, @@ -413,3 +438,23 @@ impl PrimitiveLike for &TextureHandle { self.into() } } + +/// What a widget declares a length of its box to be. `rest` is not one: a +/// share of what is left over is only a length to the widget dividing one, +/// so it passes up in the size instead. +pub(crate) fn declared_len(widget: &dyn Widget, axis: Axis) -> Option { + widget.size_hint(axis).filter(|len| len.rest == 0.0) +} + +/// 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 { + for (axis, len) in [Axis::X, Axis::Y].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); + } + region +} diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index faa5094..ae48250 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1,5 +1,6 @@ #[cfg(feature = "layout-diagnostics")] use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind}; +use crate::ui::painter::declared_len; use crate::{ ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion, Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId, Widgets, @@ -307,6 +308,8 @@ impl UiRenderState { size_deps, size_box_inputs, size_output_inputs, + // Written by whoever draws it, which is what resolves them. + declared: [None; 2], output_px: self.output_size, move_idx, parent_move, @@ -753,11 +756,28 @@ impl UiRenderState { AXES.into_iter() .any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis))) }); + // A declared length is resolved into this widget's box by whoever + // drew it, so a change to one moves a box this widget cannot fix by + // drawing again, however its own size comes out. Compared rather + // than assumed: a widget dirtied for any other reason declares what + // it declared before, and redrawing its parent for that costs 17%. + let declared_changed = self.active.get(&id).is_some_and(|active| { + rsc.widgets().get_dyn(id).is_some_and(|widget| { + AXES.into_iter() + .zip(active.declared) + .any(|(axis, was)| declared_len(widget, axis) != was) + }) + }); let top = match box_changed { true => self.top_reader(id), false => None, } - .or_else(|| self.derived_box_reader(id)); + .or_else(|| self.derived_box_reader(id)) + .or_else(|| { + declared_changed + .then(|| self.active.get(&id).and_then(|active| active.parent)) + .flatten() + }); if let Some(top) = top { #[cfg(feature = "layout-diagnostics")] diag::bump(Counter::EagerReaderRedraws); diff --git a/src/widget/position/set_size.rs b/src/widget/position/set_size.rs index 8ec989e..3541cc9 100644 --- a/src/widget/position/set_size.rs +++ b/src/widget/position/set_size.rs @@ -8,20 +8,11 @@ pub struct SetSize { impl Widget for SetSize { fn draw(&mut self, painter: &mut Painter) -> Size { - // A declared length is what the child gets, whatever box this widget - // was offered before its parent knew that. Measuring it anywhere else - // asks about a box it will not have, and the answer on the other axis - // is taken under that: a wrapping text measured in the whole width - // reports one line, and nothing revisits it once the real width - // arrives. - let mut region = UiRegion::FULL; - for (axis, len) in [(Axis::X, self.x), (Axis::Y, self.y)] { - if let Some(len) = len { - let span = region.axis_mut(axis); - span.end = span.start + len.apply_rest(); - } - } - let child = painter.widget_within(&self.inner, region).size(); + // Nothing to apply: a declared length is taken where this widget is + // drawn, so the box it has already is that length, and `rest` is a + // share only whoever divides a length can work out. Both reach them + // through `size_hint`. + let child = painter.widget(&self.inner).size(); Size { x: self.x.unwrap_or(child.x), y: self.y.unwrap_or(child.y),