diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index b2c6ec9..33f208b 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -1,14 +1,18 @@ -use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId}; +use crate::{LayerId, MaskIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId}; /// important non rendering data for retained drawing #[derive(Debug)] pub struct ActiveData { pub id: WidgetId, pub region: UiRegion, + /// What the widget said it used of `region`, the last time it drew. + pub size: Size, pub parent: Option, pub textures: Vec, pub primitives: Vec, pub children: Vec, + /// The children whose size this widget read while drawing. + pub size_deps: Vec, pub mask: MaskIdx, pub layer: LayerId, } diff --git a/core/src/ui/cache.rs b/core/src/ui/cache.rs deleted file mode 100644 index 10565ee..0000000 --- a/core/src/ui/cache.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{BothAxis, Len, UiVec2, WidgetId, util::HashMap}; - -#[derive(Default)] -pub struct Cache { - pub size: BothAxis>, -} - -impl Cache { - pub fn remove(&mut self, id: WidgetId) { - self.size.x.remove(&id); - self.size.y.remove(&id); - } - - pub fn clear(&mut self) { - self.size.x.clear(); - self.size.y.clear(); - } -} diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index 607dd82..5593164 100644 --- a/core/src/ui/mod.rs +++ b/core/src/ui/mod.rs @@ -3,15 +3,12 @@ use crate::{ }; mod active; -mod cache; mod painter; mod render_state; -mod size; pub use active::*; pub use painter::{Painter, PrimitiveLike}; pub use render_state::*; -pub use size::*; #[derive(Default)] pub struct UiData { diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 5ec130b..ec0d8a9 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -1,6 +1,6 @@ use crate::{ - Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData, - TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, + Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, + UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, render::{ GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind, TexturePrimitive, @@ -18,6 +18,9 @@ pub struct Painter<'a> { pub(super) textures: Vec, pub(super) primitives: Vec, pub(super) children: Vec, + /// The children whose size this widget read while drawing. + pub(super) size_deps: Vec, + pub(super) size: Option, pub layer: usize, pub(super) id: WidgetId, } @@ -67,20 +70,40 @@ impl<'a> Painter<'a> { self.mask = self.rsc.ui_mut().masks.push(Mask { region }); } + /// States what this widget uses of the box it was handed, once per draw. + /// This is the size a parent reads. + pub fn set_size(&mut self, size: impl Into) { + debug_assert!( + self.size.is_none(), + "{} set its size twice in one draw", + self.label(), + ); + self.size = Some(size.into()); + } + /// Draws a widget within this widget's region. - pub fn widget(&mut self, id: &StrongWidget) { - self.widget_at(id, self.region); + pub fn widget(&mut self, id: &StrongWidget) -> DrawResult<'_, 'a> { + self.widget_at(id, self.region) } /// Draws a widget somewhere within this one. /// Useful for drawing child widgets in select areas. - pub fn widget_within(&mut self, id: &StrongWidget, region: UiRegion) { - self.widget_at(id, region.within(&self.region)); + pub fn widget_within( + &mut self, + id: &StrongWidget, + region: UiRegion, + ) -> DrawResult<'_, 'a> { + let region = region.within(&self.region); + self.widget_at(id, region) } - fn widget_at(&mut self, id: &StrongWidget, region: UiRegion) { + fn widget_at( + &mut self, + id: &StrongWidget, + region: UiRegion, + ) -> DrawResult<'_, 'a> { self.children.push(id.id()); - self.state.draw_inner( + let size = self.state.draw_inner( self.layer, id.id(), region, @@ -89,6 +112,44 @@ impl<'a> Painter<'a> { None, self.rsc, ); + DrawResult { + child: id.id(), + painter: self, + size, + } + } + + /// What a child says its length is without being drawn, if it can say. + pub fn size_hint(&self, id: &StrongWidget, axis: Axis) -> Option { + self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) + } + + /// Gives a child its final box once this widget knows what that is, + /// keeping the drawing it already has where it can. + pub fn place(&mut self, id: &StrongWidget, region: UiRegion) -> Size { + let region = region.within(&self.region); + let id = id.id(); + // Choosing a child's box depends on its size, from a draw or a hint. + // Claiming it is separate: one left out of `children` is removed. + if !self.size_deps.contains(&id) { + self.size_deps.push(id); + } + if !self.children.contains(&id) { + self.children.push(id); + } + if self.state.active.contains_key(&id) { + return self.state.place(id, region, self.rsc); + } + // Not drawn yet: its length came from a hint, so this is its one draw. + self.state.draw_inner( + self.layer, + id, + region, + Some(self.id), + self.mask, + None, + self.rsc, + ) } pub fn render_text( @@ -129,17 +190,6 @@ impl<'a> Painter<'a> { self.region } - pub fn size(&mut self, id: &StrongWidget) -> Size { - self.size_ctx().size(id) - } - - pub fn len_axis(&mut self, id: &StrongWidget, axis: Axis) -> Len { - match axis { - Axis::X => self.size_ctx().width(id), - Axis::Y => self.size_ctx().height(id), - } - } - pub fn output_size(&self) -> Vec2 { self.state.output_size } @@ -167,9 +217,25 @@ impl<'a> Painter<'a> { pub fn id(&self) -> &WidgetId { &self.id } +} - pub fn size_ctx(&mut self) -> SizeCtx<'_> { - self.state.size_ctx(self.id, self.region.size(), self.rsc) +/// A child that has just been drawn. Reading its size records that this +/// widget's own size depends on it; dropping it without reading draws the +/// child and leaves the parent independent of what it came to. +pub struct DrawResult<'p, 'a> { + painter: &'p mut Painter<'a>, + child: WidgetId, + size: Size, +} + +impl DrawResult<'_, '_> { + pub fn size(self) -> Size { + self.painter.size_deps.push(self.child); + self.size + } + + pub fn len(self, axis: Axis) -> Len { + self.size().axis(axis) } } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 476dc78..7ae756c 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1,7 +1,6 @@ use crate::{ - ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget, - UiRegion, UiRsc, UiVec2, WidgetId, Widgets, - ui::cache::Cache, + ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, Size, SizeDependence, + StrongWidget, UiRegion, UiRsc, WidgetId, Widgets, util::{HashMap, HashSet, Vec2, forget_ref}, }; @@ -9,7 +8,6 @@ pub struct UiRenderState { pub active: HashMap, pub layers: DrawLayers, pub(super) output_size: Vec2, - pub cache: Cache, old_root: Option, resized: bool, @@ -21,7 +19,6 @@ impl UiRenderState { Self { active: Default::default(), layers: Default::default(), - cache: Default::default(), output_size: Vec2::ZERO, old_root: None, resized: false, @@ -84,19 +81,11 @@ impl UiRenderState { mask: MaskIdx, old_children: Option>, rsc: &mut dyn UiRsc, - ) { + ) -> Size { let mut old_children = old_children.unwrap_or_default(); - if let Some(active) = self.active.get_mut(&id) - && !rsc.widgets().needs_redraw.contains(&id) - { - // check to see if we can skip drawing first - if active.region == region { - return; - } else if active.region.size() == region.size() { - // TODO: epsilon? - let from = active.region; - self.mov(id, from, region); - return; + if self.active.contains_key(&id) { + if let Some(size) = self.try_reuse(id, region, rsc) { + return size; } // if not, then maintain resize and track old children to remove unneeded let active = self.remove(id, false, rsc).unwrap(); @@ -115,6 +104,8 @@ impl UiRenderState { textures: Vec::new(), primitives: Vec::new(), children: Vec::new(), + size_deps: Vec::new(), + size: None, rsc, }; @@ -130,18 +121,34 @@ impl UiRenderState { textures, primitives, children, + size_deps, + size, layer, id, } = painter; + let size = size.unwrap_or_else(|| { + panic!( + "'{}' ({id:?}) drew without a size; every widget calls Painter::set_size", + rsc.widgets().label(id) + ) + }); + debug_assert!( + Self::hints_agree(id, size, rsc), + "'{}' ({id:?}) drew a size its size_hint disagrees with", + rsc.widgets().label(id) + ); + // add to active let active = ActiveData { id, region, + size, parent, textures, primitives, children, + size_deps, mask, layer, }; @@ -155,6 +162,86 @@ impl UiRenderState { rsc.on_draw(&active); self.active.insert(id, active); + size + } + + /// Gives an already drawn widget a new box, keeping its drawing if it can + /// and drawing it again if it cannot. + pub(super) fn place(&mut self, id: WidgetId, region: UiRegion, rsc: &mut dyn UiRsc) -> Size { + if let Some(size) = self.try_reuse(id, region, rsc) { + return size; + } + let Some(active) = self.remove(id, false, rsc) else { + return Size::ZERO; + }; + self.draw_inner( + active.layer, + id, + region, + active.parent, + active.mask, + Some(active.children), + rsc, + ) + } + + /// The drawing a widget already has, kept for a new box if the box has not + /// changed in a way it depends on. + fn try_reuse(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option { + if rsc.widgets().needs_redraw.contains(&id) { + return None; + } + let active = self.active.get(&id)?; + let (size, was) = (active.size, active.region); + if was == region { + return Some(size); + } + // TODO: epsilon? + if was.size() == region.size() { + self.mov(id, was, region); + return Some(size); + } + if self.reusable(id, region, rsc) { + // Its drawing stands; the box is written into the primitives. + self.mov(id, was, region); + return Some(size); + } + None + } + + /// Whether the widget can keep the drawing it has and be given `region` + /// instead, asked one axis at a time: a change on an axis it does not + /// depend on costs nothing, whatever it depends on elsewhere. + fn reusable(&self, id: WidgetId, mut region: UiRegion, rsc: &dyn UiRsc) -> bool { + let Some(active) = self.active.get(&id) else { + return false; + }; + let Some(widget) = rsc.widgets().get_dyn(id) else { + return false; + }; + let mut was = active.region; + [Axis::X, Axis::Y].into_iter().all(|axis| { + let offered = region.axis_mut(axis).len(); + let had = was.axis_mut(axis).len(); + match widget.size_dependence(axis) { + SizeDependence::None => true, + // `Internal` could also keep its drawing when only the room + // around it changed, but that is a translation rather than a + // remap, so it waits for the move chain. + SizeDependence::Internal | SizeDependence::External => offered == had, + } + }) + } + + fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool { + let Some(widget) = rsc.widgets().get_dyn(id) else { + return true; + }; + [Axis::X, Axis::Y].into_iter().all(|axis| { + widget + .size_hint(axis) + .is_none_or(|hint| hint == size.axis(axis)) + }) } fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) { @@ -191,7 +278,6 @@ impl UiRenderState { } fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option { - self.cache.remove(id); let inst = self.remove(id, true, rsc); if let Some(inst) = &inst { for c in &inst.children { @@ -205,7 +291,6 @@ impl UiRenderState { for (_, active) in self.active.drain() { rsc.on_undraw(&active); } - self.cache.clear(); self.layers.clear(); rsc.widgets_mut().needs_redraw.clear(); rsc.free(); @@ -267,19 +352,11 @@ impl UiRenderState { pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) { rsc.widgets_mut().needs_redraw.remove(&id); self.draw_started.remove(&id); - // check if parent depends on the desired size of this, if so then redraw it first - for axis in [Axis::X, Axis::Y] { - if let Some(&(outer, old)) = self.cache.size.axis_dyn(axis).get(&id) - && let Some(current) = self.active.get(&id) - && let Some(pid) = current.parent - { - self.cache.size.axis_dyn(axis).remove(&id); - let new = self.size_ctx(id, outer, rsc).len_axis(id, axis); - self.cache.size.axis_dyn(axis).insert(id, (outer, new)); - if new != old { - self.redraw(pid, rsc); - } - } + // Whoever read this widget's size may be a different size now, so the + // highest reader is what draws; it reaches this one on the way down. + if let Some(top) = self.highest_reader(id) { + self.redraw(top, rsc); + return; } if self.draw_started.contains(&id) { @@ -301,22 +378,22 @@ impl UiRenderState { ); } - pub(super) fn size_ctx<'b>( - &'b mut self, - source: WidgetId, - outer: UiVec2, - rsc: &'b mut dyn UiRsc, - ) -> SizeCtx<'b> { - let ui = rsc.ui_mut(); - SizeCtx { - source, - cache: &mut self.cache, - text: &mut ui.text, - widgets: &ui.widgets, - outer, - output_size: self.output_size, - id: source, + /// The furthest ancestor that read this widget's size, directly or through + /// widgets that did the same. + fn highest_reader(&self, id: WidgetId) -> Option { + let mut top = None; + let mut at = id; + while let Some(active) = self.active.get(&at) + && let Some(parent) = active.parent + && self + .active + .get(&parent) + .is_some_and(|p| p.size_deps.contains(&at)) + { + top = Some(parent); + at = parent; } + top } } diff --git a/core/src/ui/size.rs b/core/src/ui/size.rs deleted file mode 100644 index 931a1b2..0000000 --- a/core/src/ui/size.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::{ - Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2, - WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2, -}; - -pub struct SizeCtx<'a> { - pub text: &'a mut TextData, - pub(super) source: WidgetId, - pub(super) widgets: &'a Widgets, - pub(super) cache: &'a mut Cache, - /// TODO: should this be pub? rn used for sized - pub outer: UiVec2, - pub(super) output_size: Vec2, - pub(super) id: WidgetId, -} - -impl SizeCtx<'_> { - pub fn id(&self) -> &WidgetId { - &self.id - } - - pub fn source(&self) -> &WidgetId { - &self.source - } - - pub(super) fn len_inner(&mut self, id: WidgetId) -> Len { - if let Some((_, len)) = self.cache.size.axis::().get(&id) { - return *len; - } - let len = self - .widgets - .get_dyn_dynamic(id) - .desired_len::(&mut SizeCtx { - text: self.text, - source: self.source, - widgets: self.widgets, - cache: self.cache, - outer: self.outer, - output_size: self.output_size, - id, - }); - self.cache.size.axis::().insert(id, (self.outer, len)); - len - } - - pub fn width(&mut self, id: impl IdLike) -> Len { - self.len_inner::(id.id()) - } - - pub fn height(&mut self, id: impl IdLike) -> Len { - self.len_inner::(id.id()) - } - - pub fn len_axis(&mut self, id: impl IdLike, axis: Axis) -> Len { - match axis { - Axis::X => self.width(id), - Axis::Y => self.height(id), - } - } - - pub fn size(&mut self, id: impl IdLike) -> Size { - let id = id.id(); - Size { - x: self.width(id), - y: self.height(id), - } - } - - pub fn px_size(&mut self) -> Vec2 { - self.outer.to_abs(self.output_size) - } - - pub fn output_size(&mut self) -> Vec2 { - self.output_size - } - - pub fn draw_text( - &mut self, - buffer: &mut TextBuffer, - attrs: &TextAttrs, - width: Option, - ) -> RenderedText { - self.text.render(buffer, attrs, width) - } - - pub fn label(&self, id: WidgetId) -> &String { - self.widgets.label(id) - } -} diff --git a/core/src/widget/mod.rs b/core/src/widget/mod.rs index a0f084c..f7b72aa 100644 --- a/core/src/widget/mod.rs +++ b/core/src/widget/mod.rs @@ -1,4 +1,4 @@ -use crate::{Axis, AxisT, Len, Painter, SizeCtx}; +use crate::{Axis, Len, Painter, Size}; use std::any::Any; mod data; @@ -15,32 +15,49 @@ pub use tag::*; pub use view::*; pub use widgets::*; +/// How much of the box a widget was handed its drawing depends on, and so +/// what has to change before it must be drawn again. Asked per axis, because +/// wrapped text depends on the width it is offered and not on the height. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum SizeDependence { + /// None of it: the box only says where the primitives go, so a new one is + /// written into them instead of drawn. + None, + /// Its own extent, whatever box that sits in. Reusable in any box that + /// leaves that extent unchanged, including a larger one it does not fill. + #[default] + Internal, + /// The extent of the box itself, used or not. + External, +} + pub trait Widget: Any { + /// Draws the widget, and states what it used with `Painter::set_size`. fn draw(&mut self, painter: &mut Painter); - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len; - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len; -} -pub trait WidgetAxisFns { - fn desired_len(&mut self, ctx: &mut SizeCtx) -> Len; -} + /// An exact length the widget can give without a painter or its children. + /// Optional, and saves a draw rather than changing one: a hint that + /// disagrees with the eventual draw fails a debug assertion. + fn size_hint(&self, _axis: Axis) -> Option { + None + } -impl WidgetAxisFns for W { - fn desired_len(&mut self, ctx: &mut SizeCtx) -> Len { - match A::get() { - Axis::X => self.desired_width(ctx), - Axis::Y => self.desired_height(ctx), - } + fn size_dependence(&self, _axis: Axis) -> SizeDependence { + SizeDependence::Internal } } impl Widget for () { - fn draw(&mut self, _: &mut Painter) {} - fn desired_width(&mut self, _: &mut SizeCtx) -> Len { - Len::ZERO + fn draw(&mut self, painter: &mut Painter) { + painter.set_size(Size::ZERO); } - fn desired_height(&mut self, _: &mut SizeCtx) -> Len { - Len::ZERO + + fn size_hint(&self, _axis: Axis) -> Option { + Some(Len::ZERO) + } + + fn size_dependence(&self, _axis: Axis) -> SizeDependence { + SizeDependence::None } } diff --git a/examples/text.rs b/examples/text.rs new file mode 100644 index 0000000..6955880 --- /dev/null +++ b/examples/text.rs @@ -0,0 +1,63 @@ +//! Text sizing: wrapped text reads the width it is offered, fixed text does +//! not, and both report a height their container lays out around. + +use iris::prelude::*; + +fn main() { + DefaultApp::::run(); +} + +#[derive(DefaultUiState)] +struct State { + ui_state: DefaultUiState, +} + +const SAMPLE: &str = "Wrapping shapes one source into as many lines as its container \ + leaves room for, so the height of a paragraph is an answer rather than a setting, and \ + the same words in a narrower box come back taller. Resize the window and watch the \ + text below reflow into a different number of lines while nothing about it changes."; + +impl DefaultAppState for State { + fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc, _: Proxy) -> Self { + let panel = || rect(Color::WHITE.darker(0.85)); + + let wrapped = wtext(SAMPLE) + .size(28) + .wrap(true) + .text_align(Align::LEFT) + .pad(16) + .background(panel()); + + let aligned = ( + wtext("left").size(24).text_align(Align::LEFT), + wtext("centred").size(24).text_align(Align::CENTER), + wtext("right").size(24).text_align(Align::RIGHT), + ) + .span(Dir::DOWN) + .gap(8) + .pad(16) + .background(panel()); + + // The same words in half the width, which is a different number of + // lines and so a different height. A declared width only holds along + // a span's own axis, hence the row. + let narrow = ( + wtext(SAMPLE) + .size(20) + .wrap(true) + .pad(16) + .background(panel()) + .align(Align::TOP) + .width(rel(0.5)), + rect(Color::WHITE.darker(0.95)), + ) + .span(Dir::RIGHT); + + (wrapped, aligned, narrow) + .span(Dir::DOWN) + .gap(12) + .pad(12) + .set_root(rsc, &mut ui_state); + Self { ui_state } + } +} diff --git a/src/widget/image.rs b/src/widget/image.rs index 86d2027..84620e8 100644 --- a/src/widget/image.rs +++ b/src/widget/image.rs @@ -8,14 +8,15 @@ pub struct Image { impl Widget for Image { fn draw(&mut self, painter: &mut Painter) { painter.primitive(&self.handle); + painter.set_size(Size::abs(self.handle.size())); } - fn desired_width(&mut self, _: &mut SizeCtx) -> Len { - Len::abs(self.handle.size().x) + fn size_hint(&self, axis: Axis) -> Option { + Some(Len::abs(self.handle.size().axis(axis))) } - fn desired_height(&mut self, _: &mut SizeCtx) -> Len { - Len::abs(self.handle.size().y) + fn size_dependence(&self, _: Axis) -> SizeDependence { + SizeDependence::None } } diff --git a/src/widget/mask.rs b/src/widget/mask.rs index cc075e9..27f014b 100644 --- a/src/widget/mask.rs +++ b/src/widget/mask.rs @@ -7,14 +7,12 @@ pub struct Masked { impl Widget for Masked { fn draw(&mut self, painter: &mut Painter) { painter.set_mask(painter.region()); - painter.widget(&self.inner); + let size = painter.widget(&self.inner).size(); + painter.set_size(size); } - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - ctx.width(&self.inner) - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - ctx.height(&self.inner) + /// It clips to the box it was given, not to the part its child used. + fn size_dependence(&self, _: Axis) -> SizeDependence { + SizeDependence::External } } diff --git a/src/widget/position/align.rs b/src/widget/position/align.rs index 1a6d6a5..d335fbb 100644 --- a/src/widget/position/align.rs +++ b/src/widget/position/align.rs @@ -7,29 +7,16 @@ pub struct Aligned { impl Widget for Aligned { fn draw(&mut self, painter: &mut Painter) { + // Drawn where it may be too big, then given its aligned box once its + // size is known. + let size = painter.widget(&self.inner).size(); let region = match self.align.tuple() { - (Some(x), Some(y)) => painter - .size(&self.inner) - .to_uivec2() - .align(RegionAlign { x, y }), - (Some(x), None) => { - let x = painter.size_ctx().width(&self.inner).apply_rest().align(x); - UiRegion::new(x, UiSpan::FULL) - } - (None, Some(y)) => { - let y = painter.size_ctx().height(&self.inner).apply_rest().align(y); - UiRegion::new(UiSpan::FULL, y) - } + (Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }), + (Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL), + (None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)), (None, None) => UiRegion::FULL, }; - painter.widget_within(&self.inner, region); - } - - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - ctx.width(&self.inner) - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - ctx.height(&self.inner) + painter.place(&self.inner, region); + painter.set_size(size); } } diff --git a/src/widget/position/layer.rs b/src/widget/position/layer.rs index fb2ced3..f1fa596 100644 --- a/src/widget/position/layer.rs +++ b/src/widget/position/layer.rs @@ -10,14 +10,7 @@ impl Widget for LayerOffset { for _ in 0..self.offset { painter.next_layer(); } - painter.widget(&self.inner); - } - - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - ctx.width(&self.inner) - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - ctx.height(&self.inner) + let size = painter.widget(&self.inner).size(); + painter.set_size(size); } } diff --git a/src/widget/position/max_size.rs b/src/widget/position/max_size.rs index 1a9aa39..28b2dff 100644 --- a/src/widget/position/max_size.rs +++ b/src/widget/position/max_size.rs @@ -6,43 +6,22 @@ pub struct MaxSize { pub y: Option, } -impl MaxSize { - fn apply_to_outer(&self, ctx: &mut SizeCtx) { - if let Some(x) = self.x { - ctx.outer.x.select_len(x.apply_rest()); - } - if let Some(y) = self.y { - ctx.outer.y.select_len(y.apply_rest()); - } - } -} +impl MaxSize {} impl Widget for MaxSize { fn draw(&mut self, painter: &mut Painter) { - painter.widget(&self.inner); + let child = painter.widget(&self.inner).size(); + let output = painter.output_size(); + painter.set_size(Size { + x: capped(child.x, self.x, output.x), + y: capped(child.y, self.y, output.y), + }); } +} - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - self.apply_to_outer(ctx); - let width = ctx.width(&self.inner); - if let Some(x) = self.x { - let width_px = width.apply_rest().to_abs(ctx.output_size().x); - let x_px = x.apply_rest().to_abs(ctx.output_size().x); - if width_px > x_px { x } else { width } - } else { - width - } - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - self.apply_to_outer(ctx); - let height = ctx.height(&self.inner); - if let Some(y) = self.y { - let height_px = height.apply_rest().to_abs(ctx.output_size().y); - let y_px = y.apply_rest().to_abs(ctx.output_size().y); - if height_px > y_px { y } else { height } - } else { - height - } +fn capped(len: Len, max: Option, output: f32) -> Len { + match max { + Some(max) if len.apply_rest().to_abs(output) > max.apply_rest().to_abs(output) => max, + _ => len, } } diff --git a/src/widget/position/offset.rs b/src/widget/position/offset.rs index da54f69..a878119 100644 --- a/src/widget/position/offset.rs +++ b/src/widget/position/offset.rs @@ -8,14 +8,7 @@ pub struct Offset { impl Widget for Offset { fn draw(&mut self, painter: &mut Painter) { let region = UiRegion::FULL.offset(self.amt); - painter.widget_within(&self.inner, region); - } - - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - ctx.width(&self.inner) - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - ctx.height(&self.inner) + let size = painter.widget_within(&self.inner, region).size(); + painter.set_size(size); } } diff --git a/src/widget/position/pad.rs b/src/widget/position/pad.rs index 5619d0f..4f45205 100644 --- a/src/widget/position/pad.rs +++ b/src/widget/position/pad.rs @@ -7,27 +7,19 @@ pub struct Pad { impl Widget for Pad { fn draw(&mut self, painter: &mut Painter) { - painter.widget_within(&self.inner, self.padding.region()); - } - - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - let width = self.padding.left + self.padding.right; - let height = self.padding.top + self.padding.bottom; - ctx.outer.x.abs -= width; - ctx.outer.y.abs -= height; - let mut size = ctx.width(&self.inner); - size.abs += width; - size - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - let width = self.padding.left + self.padding.right; - let height = self.padding.top + self.padding.bottom; - ctx.outer.x.abs -= width; - ctx.outer.y.abs -= height; - let mut size = ctx.height(&self.inner); - size.abs += height; - size + let inner = painter + .widget_within(&self.inner, self.padding.region()) + .size(); + painter.set_size(Size { + x: Len { + abs: inner.x.abs + self.padding.left + self.padding.right, + ..inner.x + }, + y: Len { + abs: inner.y.abs + self.padding.top + self.padding.bottom, + ..inner.y + }, + }); } } diff --git a/src/widget/position/scroll.rs b/src/widget/position/scroll.rs index c789acc..b19a6a5 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -13,8 +13,11 @@ impl Widget for Scroll { fn draw(&mut self, painter: &mut Painter) { let output_len = painter.output_size().axis(self.axis); let container_len = painter.region().axis(self.axis).len(); - let content_len = painter - .len_axis(&self.inner, self.axis) + // Drawn in the whole container to learn its length, then placed at + // the scrolled offset. + let child = painter.widget(&self.inner).size(); + let content_len = child + .axis(self.axis) .apply_rest() .within_len(container_len) .to_abs(output_len); @@ -28,15 +31,8 @@ impl Widget for Scroll { let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0)); region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len); - painter.widget_within(&self.inner, region); - } - - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - ctx.width(&self.inner) - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - ctx.height(&self.inner) + painter.place(&self.inner, region); + painter.set_size(child); } } diff --git a/src/widget/position/set_size.rs b/src/widget/position/set_size.rs index b39d6e6..cd28017 100644 --- a/src/widget/position/set_size.rs +++ b/src/widget/position/set_size.rs @@ -6,29 +6,23 @@ pub struct SetSize { pub y: Option, } -impl SetSize { - fn apply_to_outer(&self, ctx: &mut SizeCtx) { - if let Some(x) = self.x { - ctx.outer.x.select_len(x.apply_rest()); - } - if let Some(y) = self.y { - ctx.outer.y.select_len(y.apply_rest()); - } - } -} +impl SetSize {} impl Widget for SetSize { fn draw(&mut self, painter: &mut Painter) { - painter.widget(&self.inner); + let child = painter.widget(&self.inner).size(); + painter.set_size(Size { + x: self.x.unwrap_or(child.x), + y: self.y.unwrap_or(child.y), + }); } - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - self.apply_to_outer(ctx); - self.x.unwrap_or_else(|| ctx.width(&self.inner)) - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - self.apply_to_outer(ctx); - self.y.unwrap_or_else(|| ctx.height(&self.inner)) + /// A declared axis is known without looking at the child, which is what + /// lets a span lay out around `.height(rest(1))` without drawing it. + fn size_hint(&self, axis: Axis) -> Option { + match axis { + Axis::X => self.x, + Axis::Y => self.y, + } } } diff --git a/src/widget/position/span.rs b/src/widget/position/span.rs index ad4f932..84f41d1 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -9,12 +9,26 @@ pub struct Span { impl Widget for Span { fn draw(&mut self, painter: &mut Painter) { - let total = self.len_sum(&mut painter.size_ctx()); + let axis = self.dir.axis; + // A length for every child before any is placed: from its own hint + // where it has one, and from drawing it where it does not. + let lens: Vec = self + .children + .iter() + .map(|child| match painter.size_hint(child, axis) { + Some(len) => len, + None => painter.widget(child).len(axis), + }) + .collect(); + + let gap = self.gap * self.children.len().saturating_sub(1) as f32; + let total = lens.iter().fold(Len::abs(gap), |sum, len| sum + *len); + let mut start = UiScalar::rel_min(); - for child in &self.children { + let mut ortho = Len::ZERO; + for (child, len) in self.children.iter().zip(&lens) { let mut span = UiSpan::FULL; span.start = start; - let len = painter.len_axis(child, self.dir.axis); if len.rest > 0.0 { let offset = UiScalar::new(total.rel, total.abs); let rel_end = UiScalar::rel(len.rest / total.rest); @@ -24,27 +38,25 @@ impl Widget for Span { start.abs += len.abs; start.rel += len.rel; span.end = start; - let mut child_region = UiRegion::from_axis(self.dir.axis, span, UiSpan::FULL); + let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL); if self.dir.sign == Sign::Neg { - child_region.flip(self.dir.axis); + region.flip(axis); + } + let used = painter.place(child, region).axis(!axis); + // TODO: rel shouldn't do this, but no easy way before actually calculating pixels + if used.rel > 0.0 || used.rest > 0.0 { + ortho = Len::REST; + } else if ortho.rest == 0.0 { + ortho.abs = ortho.abs.max(used.abs); } - painter.widget_within(child, child_region); start.abs += self.gap; } - } - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - match self.dir.axis { - Axis::X => self.desired_len(ctx), - Axis::Y => self.desired_ortho(ctx), - } - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - match self.dir.axis { - Axis::X => self.desired_ortho(ctx), - Axis::Y => self.desired_len(ctx), - } + let along = match total.rest == 0.0 && total.rel == 0.0 { + true => total, + false => Len::default(), + }; + painter.set_size(Size::from_axis(axis, along, ortho)); } } @@ -69,87 +81,6 @@ impl Span { pub fn pop(&mut self) -> Option { self.children.pop() } - - fn len_sum(&mut self, ctx: &mut SizeCtx) -> Len { - let gap = self.gap * self.children.len().saturating_sub(1) as f32; - self.children.iter().fold(Len::abs(gap), |mut s, id| { - // it's tempting to subtract the abs & rel from the ctx outer, - // but that would create inconsistent sizing if you put - // a rest first vs last & only speed up in one direction. - // I think this is only solvable by restricting how you can - // compute size, bc currently you need child to define parent's - // sectioning and you need parent's sectioning to define child. - // Fortunately, that doesn't matter in most cases - let len = ctx.len_axis(id, self.dir.axis); - s += len; - s - }) - } - - fn desired_len(&mut self, ctx: &mut SizeCtx) -> Len { - let len = self.len_sum(ctx); - if len.rest == 0.0 && len.rel == 0.0 { - len - } else { - Len::default() - } - } - - fn desired_ortho(&mut self, ctx: &mut SizeCtx) -> Len { - // this is a weird hack to get text wrapping to work properly when in a downward span - // the correct solution here is to add a function to widget that lets them - // request that ctx.outer has an axis "resolved" before checking the other, - // and panicking or warning if two request opposite axis (unsolvable in that case) - let outer = ctx.outer.axis(self.dir.axis); - if self.dir.axis == Axis::X { - // so....... this literally copies draw so that the lengths are correctly set in the - // context, which makes this slow and not cool - let total = self.len_sum(ctx); - let mut start = UiScalar::rel_min(); - let mut ortho_len = Len::ZERO; - for child in &self.children { - let mut span = UiSpan::FULL; - span.start = start; - let len = ctx.len_axis(child, self.dir.axis); - if len.rest > 0.0 { - let offset = UiScalar::new(total.rel, total.abs); - let rel_end = UiScalar::rel(len.rest / total.rest); - let end = (UiScalar::rel_max() + start) - offset; - start = rel_end.within(&start.to(end)); - } - start.abs += len.abs; - start.rel += len.rel; - span.end = start; - - let scalar = span.len(); - *ctx.outer.axis_mut(self.dir.axis) = outer.select_len(scalar); - let ortho = ctx.len_axis(child, !self.dir.axis); - // TODO: rel shouldn't do this, but no easy way before actually calculating pixels - if ortho.rel > 0.0 || ortho.rest > 0.0 { - ortho_len.rest = 1.0; - ortho_len.abs = 0.0; - break; - } - ortho_len.abs = ortho_len.abs.max(ortho.abs); - start.abs += self.gap; - } - ortho_len - } else { - let mut ortho_len = Len::ZERO; - let ortho = !self.dir.axis; - for child in &self.children { - let len = ctx.len_axis(child, ortho); - // TODO: rel shouldn't do this, but no easy way before actually calculating pixels - if len.rel > 0.0 || len.rest > 0.0 { - ortho_len.rest = 1.0; - ortho_len.abs = 0.0; - break; - } - ortho_len.abs = ortho_len.abs.max(len.abs); - } - ortho_len - } - } } pub struct SpanBuilder, Tag> { diff --git a/src/widget/position/stack.rs b/src/widget/position/stack.rs index fb4a591..0918359 100644 --- a/src/widget/position/stack.rs +++ b/src/widget/position/stack.rs @@ -9,29 +9,24 @@ pub struct Stack { impl Widget for Stack { fn draw(&mut self, painter: &mut Painter) { - let mut iter = self.children.iter(); - if let Some(child) = iter.next() { - painter.child_layer(); - painter.widget(child); - } - for child in iter { - painter.next_layer(); - painter.widget(child); - } - } - - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - match self.size { - StackSize::Default => Len::default(), - StackSize::Child(i) => ctx.width(&self.children[i]), - } - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - match self.size { - StackSize::Default => Len::default(), - StackSize::Child(i) => ctx.height(&self.children[i]), + let sizing = match self.size { + StackSize::Default => None, + StackSize::Child(i) => Some(i), + }; + let mut size = Size::default(); + 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.set_size(size); } } diff --git a/src/widget/ptr.rs b/src/widget/ptr.rs index b3387b2..365a1fa 100644 --- a/src/widget/ptr.rs +++ b/src/widget/ptr.rs @@ -7,25 +7,11 @@ pub struct WidgetPtr { impl Widget for WidgetPtr { fn draw(&mut self, painter: &mut Painter) { - if let Some(id) = &self.inner { - painter.widget(id); - } - } - - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - if let Some(id) = &self.inner { - ctx.width(id) - } else { - Len::ZERO - } - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - if let Some(id) = &self.inner { - ctx.height(id) - } else { - Len::ZERO - } + let size = match &self.inner { + Some(id) => painter.widget(id).size(), + None => Size::ZERO, + }; + painter.set_size(size); } } diff --git a/src/widget/rect.rs b/src/widget/rect.rs index f72820e..f139ae8 100644 --- a/src/widget/rect.rs +++ b/src/widget/rect.rs @@ -35,14 +35,16 @@ impl Widget for Rect { thickness: self.thickness, inner_radius: self.inner_radius, }); + painter.set_size(Size::REST); } - fn desired_width(&mut self, _: &mut SizeCtx) -> Len { - Len::rest(1) + fn size_hint(&self, _: Axis) -> Option { + Some(Len::REST) } - fn desired_height(&mut self, _: &mut SizeCtx) -> Len { - Len::rest(1) + /// Its box is its primitive's own region, so a new one is written there. + fn size_dependence(&self, _: Axis) -> SizeDependence { + SizeDependence::None } } diff --git a/src/widget/text/edit.rs b/src/widget/text/edit.rs index 608ad00..55f9599 100644 --- a/src/widget/text/edit.rs +++ b/src/widget/text/edit.rs @@ -58,7 +58,8 @@ impl Widget for TextEdit { fn draw(&mut self, painter: &mut Painter) { let base = painter.layer; painter.child_layer(); - self.view.draw(painter); + let (_, size) = self.view.draw(painter); + painter.set_size(size); painter.layer = base; let region = self.region(); @@ -87,12 +88,8 @@ impl Widget for TextEdit { ); } - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - self.view.desired_width(ctx) - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - self.view.desired_height(ctx) + fn size_dependence(&self, axis: Axis) -> SizeDependence { + self.view.size_dependence(axis) } } diff --git a/src/widget/text/mod.rs b/src/widget/text/mod.rs index 1aec47e..30de829 100644 --- a/src/widget/text/mod.rs +++ b/src/widget/text/mod.rs @@ -52,15 +52,15 @@ impl TextView { .align(self.align) } - fn render(&mut self, ctx: &mut SizeCtx) -> &RenderedText { + fn render(&mut self, painter: &mut Painter) -> &RenderedText { let width = if self.attrs.wrap { - Some(ctx.px_size().x) + Some(painter.px_size().x) } else { None }; if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed { self.width = width; - self.tex = Some(ctx.draw_text(&mut self.buf, &self.attrs, width)); + self.tex = Some(painter.render_text(&mut self.buf, &self.attrs, width)); self.attrs.changed = false; self.buf.changed = false; } @@ -69,39 +69,33 @@ impl TextView { pub fn tex(&self) -> Option<&RenderedText> { self.tex.as_ref() } - pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - if self.is_empty() - && let Some(hint) = &self.hint - { - ctx.width(hint) - } else { - Len::abs(self.render(ctx).size.x) - } - } - pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - if self.is_empty() - && let Some(hint) = &self.hint - { - ctx.height(hint) - } else { - Len::abs(self.render(ctx).size.y) - } - } - pub fn draw(&mut self, painter: &mut Painter) -> UiRegion { + /// Draws the text, and says where the glyphs went and what they use. + pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) { let align = self.align; if self.is_empty() && self.hint.is_some() { - let region = self.render(&mut painter.size_ctx()).size.align(align); - if let Some(hint) = &self.hint { - painter.widget(hint); - } - return region; + let region = self.render(painter).size.align(align); + let size = match &self.hint { + Some(hint) => painter.widget(hint).size(), + None => Size::ZERO, + }; + return (region, size); } - let tex = self.render(&mut painter.size_ctx()); + let tex = self.render(painter); let region = tex.size.align(align); + let size = Size::abs(tex.size); let within = region.within(&painter.region()); painter.glyphs(tex, within); - region + (region, size) + } + + /// Wrapping reads the width it is offered, so a wider box reshapes it; a + /// taller one never does. + pub fn size_dependence(&self, axis: Axis) -> SizeDependence { + match axis == Axis::X && self.attrs.wrap { + true => SizeDependence::External, + false => SizeDependence::Internal, + } } pub fn content(&self) -> String { @@ -117,7 +111,7 @@ impl Text { content: content.into(), } } - fn update_buf(&mut self, _ctx: &mut SizeCtx) { + fn update_buf(&mut self) { if self.content.changed { self.content.changed = false; self.view.buf.set_text(self.content.as_str()); @@ -127,18 +121,13 @@ impl Text { impl Widget for Text { fn draw(&mut self, painter: &mut Painter) { - self.update_buf(&mut painter.size_ctx()); - self.view.draw(painter); + self.update_buf(); + let (_, size) = self.view.draw(painter); + painter.set_size(size); } - fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { - self.update_buf(ctx); - self.view.desired_width(ctx) - } - - fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { - self.update_buf(ctx); - self.view.desired_height(ctx) + fn size_dependence(&self, axis: Axis) -> SizeDependence { + self.view.size_dependence(axis) } } diff --git a/tests/retained.rs b/tests/retained.rs new file mode 100644 index 0000000..3f306df --- /dev/null +++ b/tests/retained.rs @@ -0,0 +1,137 @@ +//! What a second frame draws again, and what it keeps. + +use std::{cell::Cell, rc::Rc}; + +use iris::harness::{Harness, assert_corners}; +use iris::prelude::*; + +/// A leaf that counts its draws and reports whatever size it is given, so a +/// test can see what the retained path skipped. +struct Counted { + draws: Rc>, + size: Size, + dependence: SizeDependence, +} + +impl Widget for Counted { + fn draw(&mut self, painter: &mut Painter) { + self.draws.set(self.draws.get() + 1); + painter.set_size(self.size); + } + + fn size_dependence(&self, _: Axis) -> SizeDependence { + self.dependence + } +} + +struct Counts(Rc>); + +impl Counts { + fn get(&self) -> usize { + self.0.get() + } +} + +fn counted( + h: &mut Harness, + size: Size, + dependence: SizeDependence, +) -> (WeakWidget, Counts) { + let draws = Rc::new(Cell::new(0)); + let id = Counted { + draws: draws.clone(), + size, + dependence, + } + .add(&mut h.rsc); + (id, Counts(draws)) +} + +/// A fixed-width leaf beside one that takes the rest, so changing the first +/// hands the second a different box without the output changing. +fn pair(h: &mut Harness, rest: SizeDependence) -> (WeakWidget, Counts, WidgetId) { + let (first, _) = counted(h, Size::from((100, 200)), SizeDependence::Internal); + let (second, draws) = counted(h, Size::REST, rest); + h.set_root((first, second).span(Dir::RIGHT)); + (first, draws, second.id()) +} + +#[test] +fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() { + let mut h = Harness::new((400, 200)); + let (first, draws, second) = pair(&mut h, SizeDependence::None); + let settled = draws.get(); + assert_corners!(h, second, (100, 0), (400, 200)); + + h.rsc[first].size = Size::from((150, 200)); + h.frame(); + + assert_eq!( + draws.get(), + settled, + "its box is a field to write, not a reason to draw" + ); + assert_corners!(h, second, (150, 0), (400, 200)); +} + +#[test] +fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() { + let mut h = Harness::new((400, 200)); + let (first, draws, second) = pair(&mut h, SizeDependence::External); + let settled = draws.get(); + + h.rsc[first].size = Size::from((150, 200)); + h.frame(); + + // Twice: once for the span to measure it, once for its real box. A child + // that can hint its length is spared the first, and a smaller number here + // means someone has made that cheaper rather than broken it. + assert_eq!(draws.get(), settled + 2); + assert_corners!(h, second, (150, 0), (400, 200)); +} + +#[test] +fn a_span_child_that_declares_its_length_is_drawn_once() { + let mut h = Harness::new((400, 200)); + let (told, told_draws) = counted(&mut h, Size::from((100, 200)), SizeDependence::Internal); + let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), SizeDependence::Internal); + // The span takes one child's length from its hint and has to draw the + // other to find out, so only the second is drawn before it is placed. + let hinted = told.width(100).add(&mut h.rsc); + h.set_root((hinted, asked).span(Dir::RIGHT)); + + assert_eq!(told_draws.get(), 1); + assert_eq!( + asked_draws.get(), + 2, + "drawn to be measured, then again to be placed" + ); +} + +#[test] +fn a_span_relays_out_when_a_child_it_measured_changes() { + let mut h = Harness::new((400, 200)); + let (first, _, second) = pair(&mut h, SizeDependence::Internal); + + h.rsc[first].size = Size::from((250, 200)); + h.frame(); + + assert_corners!(h, first, (0, 0), (250, 200)); + assert_corners!(h, second, (250, 0), (400, 200)); +} + +#[test] +fn a_placed_child_survives_the_next_frame() { + let mut h = Harness::new((400, 200)); + // Both children declare a length, so the span places them from their hints + // rather than drawing them to find out. + let top = rect(Color::RED).height(80).add(&mut h.rsc); + let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc); + h.set_root((top, bottom).span(Dir::DOWN)); + + h.rsc.widgets_mut().get_dyn_mut(top.id()); + h.frame(); + + assert_corners!(h, top, (0, 0), (400, 80)); + assert_corners!(h, bottom, (0, 80), (400, 200)); +}