diff --git a/core/src/orientation/pos.rs b/core/src/orientation/pos.rs index 466022e..423de52 100644 --- a/core/src/orientation/pos.rs +++ b/core/src/orientation/pos.rs @@ -56,13 +56,6 @@ impl UiVec2 { } } - pub const fn outside(&self, region: &UiRegion) -> UiVec2 { - UiVec2 { - x: self.x.outside(®ion.x), - y: self.y.outside(®ion.y), - } - } - pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar { match axis { Axis::X => &mut self.x, @@ -209,10 +202,12 @@ impl UiScalar { } } - pub const fn outside(&self, span: &UiSpan) -> Self { - let rel = self.rel.lerp_inv(span.start.rel, span.end.rel); + /// Undoes `within`, and `None` where the span has a fixed length: every + /// fraction of it lands on the same `rel`, so none can be told apart. + pub fn outside(&self, span: &UiSpan) -> Option { + let rel = self.rel.lerp_inv(span.start.rel, span.end.rel)?; let abs = self.abs - rel.lerp(span.start.abs, span.end.abs); - Self { rel, abs } + Some(Self { rel, abs }) } pub fn within_len(&self, len: UiScalar) -> Self { @@ -283,11 +278,11 @@ impl UiSpan { } } - pub const fn outside(&self, parent: &Self) -> Self { - Self { - start: self.start.outside(parent), - end: self.end.outside(parent), - } + pub fn outside(&self, parent: &Self) -> Option { + Some(Self { + start: self.start.outside(parent)?, + end: self.end.outside(parent)?, + }) } pub const fn len(&self) -> UiScalar { @@ -324,14 +319,7 @@ impl UiRegion { y: self.y.within(&parent.y), } } - pub const fn outside(&self, parent: &Self) -> Self { - Self { - x: self.x.outside(&parent.x), - y: self.y.outside(&parent.y), - } - } - - pub const fn axis(&mut self, axis: Axis) -> &UiSpan { + pub const fn axis(&self, axis: Axis) -> &UiSpan { match axis { Axis::X => &self.x, Axis::Y => &self.y, @@ -409,6 +397,50 @@ impl UiRegion { } } +/// Taking a drawing out of one box and putting it in another, checked once +/// for a whole subtree so that applying it cannot fail. +/// +/// A box of a fixed length holds each part as an offset from its start rather +/// than as a fraction of it, so those parts can be carried to a box of the +/// same length but never stretched to a different one. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Remap { + from: UiRegion, + to: UiRegion, +} + +impl Remap { + pub fn new(from: UiRegion, to: UiRegion) -> Option { + [Axis::X, Axis::Y] + .into_iter() + .all(|axis| { + let (from, to) = (from.axis(axis), to.axis(axis)); + from.start.rel != from.end.rel || from.len() == to.len() + }) + .then_some(Self { from, to }) + } + + pub fn apply(&self, region: UiRegion) -> UiRegion { + UiRegion { + x: Self::span(region.x, self.from.x, self.to.x), + y: Self::span(region.y, self.from.y, self.to.y), + } + } + + fn span(span: UiSpan, from: UiSpan, to: UiSpan) -> UiSpan { + match span.outside(&from) { + Some(out) => out.within(&to), + // `new` admits this only where the two are the same length, so + // the difference between their starts is the whole move. + None => { + let mut span = span; + span.shift(to.start - from.start); + span + } + } + } +} + impl Display for UiRegion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index b2c6ec9..daa6084 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -1,14 +1,20 @@ -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, + /// Whether it read the output's size, and so is wrong when that changes. + pub reads_output: bool, 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..8d229c8 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) reads_output: bool, pub layer: usize, pub(super) id: WidgetId, } @@ -68,19 +71,31 @@ impl<'a> Painter<'a> { } /// Draws a widget within this widget's region. - pub fn widget(&mut self, id: &StrongWidget) { - self.widget_at(id, self.region); + pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget) -> DrawResult<'s, 'a, W> { + 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)); + /// Draws a widget somewhere within this one. Drawing one a second time + /// gives it a new box, keeping the drawing it already has where it can. + pub fn widget_within<'s, W: ?Sized>( + &'s mut self, + id: &'s StrongWidget, + region: UiRegion, + ) -> DrawResult<'s, 'a, W> { + let region = region.within(&self.region); + self.widget_at(id, region) } - fn widget_at(&mut self, id: &StrongWidget, region: UiRegion) { - self.children.push(id.id()); - self.state.draw_inner( + fn widget_at<'s, W: ?Sized>( + &'s mut self, + id: &'s StrongWidget, + region: UiRegion, + ) -> DrawResult<'s, 'a, W> { + // A child listed twice would be moved twice. + if !self.children.contains(&id.id()) { + self.children.push(id.id()); + } + let size = self.state.draw_inner( self.layer, id.id(), region, @@ -89,6 +104,25 @@ impl<'a> Painter<'a> { None, self.rsc, ); + DrawResult { + child: id, + painter: self, + size, + } + } + + /// What a child says its length is without being drawn, if it can say. + /// Asking counts as reading its size. + pub fn size_hint(&mut self, id: &StrongWidget, axis: Axis) -> Option { + let hint = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis)?; + self.depend_on_size(id); + Some(hint) + } + + fn depend_on_size(&mut self, child: &StrongWidget) { + if !self.size_deps.contains(&child.id()) { + self.size_deps.push(child.id()); + } } pub fn render_text( @@ -129,22 +163,17 @@ 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 { + /// The output's size in pixels. A widget that reads it draws again when + /// the output changes, since nothing else can put that right. + pub fn output_size(&mut self) -> Vec2 { + self.reads_output = true; self.state.output_size } + /// This widget's box in pixels. Resolved against the output's size, so a + /// widget that reads it draws again when the output changes. pub fn px_size(&mut self) -> Vec2 { + self.reads_output = true; self.region.size().to_abs(self.state.output_size) } @@ -167,9 +196,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, W: ?Sized> { + painter: &'p mut Painter<'a>, + child: &'p StrongWidget, + size: Size, +} + +impl DrawResult<'_, '_, W> { + pub fn size(self) -> Size { + self.painter.depend_on_size(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..e40a937 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, OnResize, Painter, PixelRegion, Remap, Size, + 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, @@ -56,11 +53,21 @@ impl UiRenderState { ); } let root = root.into(); - if self.needs_full_redraw(root) { + if self.root_changed(root) { self.redraw_all(root, rsc); self.old_root = root.map(|r| r.id()); - self.resized = false; - } else if rsc.widgets().has_updates() { + } else if self.resized { + // A region is a fraction of the output plus an offset, resolved + // against the window in the shader, so a resize moves the whole + // drawing on its own. Only a widget that read pixels can be wrong. + for (&id, active) in &self.active { + if active.reads_output { + rsc.widgets_mut().needs_redraw.insert(id); + } + } + } + self.resized = false; + if rsc.widgets().has_updates() { self.redraw_updates(rsc); } } @@ -84,19 +91,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(); @@ -104,6 +103,7 @@ impl UiRenderState { } // draw widget + rsc.widgets_mut().needs_redraw.remove(&id); self.draw_started.insert(id); let mut painter = Painter { @@ -115,11 +115,13 @@ impl UiRenderState { textures: Vec::new(), primitives: Vec::new(), children: Vec::new(), + size_deps: Vec::new(), + reads_output: false, rsc, }; let mut widget = painter.rsc.widgets().get_dyn_dynamic(id); - widget.draw(&mut painter); + let size = widget.draw(&mut painter); drop(widget); let Painter { @@ -130,18 +132,29 @@ impl UiRenderState { textures, primitives, children, + size_deps, + reads_output, layer, id, } = painter; + 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, + reads_output, mask, layer, }; @@ -155,19 +168,77 @@ impl UiRenderState { rsc.on_draw(&active); self.active.insert(id, active); + size } - fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) { + /// 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, old) = (active.size, active.region); + if old == region { + return Some(size); + } + // TODO: epsilon? + if old.size() != region.size() && !self.reusable(id, region, rsc) { + return None; + } + // Its drawing stands, if the new box can be reached from the old one. + self.mov(id, &Remap::new(old, region)?); + Some(size) + } + + /// 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, 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; + }; + [Axis::X, Axis::Y].into_iter().all(|axis| { + let offered = region.axis(axis).len(); + let had = active.region.axis(axis).len(); + match widget.on_resize(axis) { + OnResize::Scale => true, + // `Translate` is not acted on yet, and cannot be until a + // drawing can sit somewhere other than its box. `region` is + // both the box a widget was given and the box its primitives + // are in, and `mov` remaps from it -- so carrying a drawing at + // its old size while the box grows makes the next move stretch + // it. The offset chain is what separates the two. + OnResize::Translate | OnResize::Redraw => 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, remap: &Remap) { let active = self.active.get_mut(&id).unwrap(); for h in &active.primitives { let region = self.layers[h.layer].region_mut(h); - *region = region.outside(&from).within(&to); + *region = remap.apply(*region); } - active.region = active.region.outside(&from).within(&to); + active.region = remap.apply(active.region); // SAFETY: children cannot be recursive let children = unsafe { forget_ref(&active.children) }; for child in children { - self.mov(*child, from, to); + self.mov(*child, remap); } } @@ -191,7 +262,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 +275,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(); @@ -222,17 +291,12 @@ impl UiRenderState { root.into().map(|r| r.id()) != self.old_root } - // Scheduling and drawing must use the same full-redraw predicate. - fn needs_full_redraw<'a>(&self, root: impl Into>) -> bool { - self.root_changed(root) || self.resized - } - pub fn needs_redraw<'a>( &self, root: impl Into>, widgets: &Widgets, ) -> bool { - self.needs_full_redraw(root) || widgets.has_updates() + self.root_changed(root) || self.resized || widgets.has_updates() } pub fn active_widgets(&self) -> usize { @@ -265,22 +329,19 @@ impl UiRenderState { /// redraws a widget that's currently active (drawn) 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. Everything between the two is marked + // as well: their own boxes have not changed, so the mark is the only + // thing stopping the draw reusing its way past this widget. + if let Some(top) = self.mark_readers(id, rsc) { + self.redraw(top, rsc); + // Cleared by that draw if it reached here; if it did not, this is + // no longer drawn and asking again would not end. + rsc.widgets_mut().needs_redraw.remove(&id); + return; } + rsc.widgets_mut().needs_redraw.remove(&id); if self.draw_started.contains(&id) { return; @@ -301,22 +362,23 @@ 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, marking everything below it on the way. + fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> 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)) + { + rsc.widgets_mut().needs_redraw.insert(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/util/math.rs b/core/src/util/math.rs index 4bf4f3a..cf4187c 100644 --- a/core/src/util/math.rs +++ b/core/src/util/math.rs @@ -1,33 +1,21 @@ -use std::ops::*; - -pub const trait LerpUtil { +pub const trait LerpUtil: Sized { fn lerp(self, from: Self, to: Self) -> Self; - fn lerp_inv(self, from: Self, to: Self) -> Self; + fn lerp_inv(self, from: Self, to: Self) -> Option; } -pub const trait DivOr { - fn div_or(self, rhs: Self, other: Self) -> Self; -} - -const impl DivOr for f32 { - fn div_or(self, rhs: Self, other: Self) -> Self { - let res = self / rhs; - if res.is_nan() { other } else { res } - } -} - -const impl< - T: const Add + const Sub + const Mul + const DivOr + Copy, -> LerpUtil for T -{ +const impl LerpUtil for f32 { /// linear interpolation /// from * (1.0 - self) + to * self fn lerp(self, from: Self, to: Self) -> Self { from + (to - from) * self } - /// inverse of lerp - fn lerp_inv(self, from: Self, to: Self) -> Self { - (self - from).div_or(to - from, from) + /// inverse of lerp, and `None` where `from` and `to` are the same point: + /// every input lerps to it, so there is no one answer to come back to. + fn lerp_inv(self, from: Self, to: Self) -> Option { + match to == from { + true => None, + false => Some((self - from) / (to - from)), + } } } diff --git a/core/src/util/vec2.rs b/core/src/util/vec2.rs index ca67c68..a678216 100644 --- a/core/src/util/vec2.rs +++ b/core/src/util/vec2.rs @@ -1,4 +1,4 @@ -use crate::util::{DivOr, impl_op}; +use crate::util::impl_op; use std::{hash::Hash, ops::*}; #[repr(C)] @@ -67,15 +67,6 @@ impl_op!(Vec2 Sub sub; x y); impl_op!(Vec2 Mul mul; x y); impl_op!(Vec2 Div div; x y); -const impl DivOr for Vec2 { - fn div_or(self, rhs: Self, other: Self) -> Self { - Self { - x: self.x.div_or(rhs.x, other.x), - y: self.y.div_or(rhs.y, other.y), - } - } -} - impl Neg for Vec2 { type Output = Self; diff --git a/core/src/widget/mod.rs b/core/src/widget/mod.rs index a0f084c..a702f47 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,45 @@ pub use tag::*; pub use view::*; pub use widgets::*; +/// What may be done to a widget's drawing when the box it was given changes +/// on this axis, instead of drawing it again. Asked per axis, because wrapped +/// text reads the width it is offered and not the height. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum OnResize { + Scale, + Translate, + #[default] + Redraw, +} + pub trait Widget: Any { - 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; -} + /// Draws the widget, and returns what it used of the box it was given. + fn draw(&mut self, painter: &mut Painter) -> Size; -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 on_resize(&self, _axis: Axis) -> OnResize { + OnResize::default() } } impl Widget for () { - fn draw(&mut self, _: &mut Painter) {} - fn desired_width(&mut self, _: &mut SizeCtx) -> Len { - Len::ZERO + /// A gap: nothing drawn, at the default length, so a span gives it a share. + fn draw(&mut self, _: &mut Painter) -> Size { + Size::default() } - fn desired_height(&mut self, _: &mut SizeCtx) -> Len { - Len::ZERO + + fn size_hint(&self, _axis: Axis) -> Option { + Some(Len::default()) + } + + fn on_resize(&self, _axis: Axis) -> OnResize { + OnResize::Scale } } 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..cc7d1bc 100644 --- a/src/widget/image.rs +++ b/src/widget/image.rs @@ -6,16 +6,17 @@ pub struct Image { } impl Widget for Image { - fn draw(&mut self, painter: &mut Painter) { + fn draw(&mut self, painter: &mut Painter) -> Size { painter.primitive(&self.handle); + 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 on_resize(&self, _: Axis) -> OnResize { + OnResize::Scale } } diff --git a/src/widget/mask.rs b/src/widget/mask.rs index cc075e9..d5a2e24 100644 --- a/src/widget/mask.rs +++ b/src/widget/mask.rs @@ -5,16 +5,13 @@ pub struct Masked { } impl Widget for Masked { - fn draw(&mut self, painter: &mut Painter) { + fn draw(&mut self, painter: &mut Painter) -> Size { painter.set_mask(painter.region()); - painter.widget(&self.inner); + painter.widget(&self.inner).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 on_resize(&self, _: Axis) -> OnResize { + OnResize::Redraw } } diff --git a/src/widget/position/align.rs b/src/widget/position/align.rs index 1a6d6a5..d63ec30 100644 --- a/src/widget/position/align.rs +++ b/src/widget/position/align.rs @@ -6,30 +6,17 @@ pub struct Aligned { } impl Widget for Aligned { - fn draw(&mut self, painter: &mut Painter) { + fn draw(&mut self, painter: &mut Painter) -> Size { + // 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) + size } } diff --git a/src/widget/position/layer.rs b/src/widget/position/layer.rs index fb2ced3..6b111a4 100644 --- a/src/widget/position/layer.rs +++ b/src/widget/position/layer.rs @@ -6,18 +6,10 @@ pub struct LayerOffset { } impl Widget for LayerOffset { - fn draw(&mut self, painter: &mut Painter) { + fn draw(&mut self, painter: &mut Painter) -> Size { 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) + painter.widget(&self.inner).size() } } diff --git a/src/widget/position/max_size.rs b/src/widget/position/max_size.rs index 1a9aa39..714e534 100644 --- a/src/widget/position/max_size.rs +++ b/src/widget/position/max_size.rs @@ -6,43 +6,20 @@ 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 Widget for MaxSize { - fn draw(&mut self, painter: &mut Painter) { - painter.widget(&self.inner); - } - - 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 draw(&mut self, painter: &mut Painter) -> Size { + let child = painter.widget(&self.inner).size(); + let output = painter.output_size(); + Size { + x: capped(child.x, self.x, output.x), + y: capped(child.y, self.y, output.y), } } } + +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..5f490b6 100644 --- a/src/widget/position/offset.rs +++ b/src/widget/position/offset.rs @@ -6,16 +6,8 @@ pub struct Offset { } impl Widget for Offset { - fn draw(&mut self, painter: &mut Painter) { + fn draw(&mut self, painter: &mut Painter) -> Size { 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) + painter.widget_within(&self.inner, region).size() } } diff --git a/src/widget/position/pad.rs b/src/widget/position/pad.rs index 5619d0f..7dbd6a7 100644 --- a/src/widget/position/pad.rs +++ b/src/widget/position/pad.rs @@ -6,28 +6,20 @@ 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 + fn draw(&mut self, painter: &mut Painter) -> Size { + let inner = painter + .widget_within(&self.inner, self.padding.region()) + .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..41e0801 100644 --- a/src/widget/position/scroll.rs +++ b/src/widget/position/scroll.rs @@ -10,11 +10,14 @@ pub struct Scroll { } impl Widget for Scroll { - fn draw(&mut self, painter: &mut Painter) { + fn draw(&mut self, painter: &mut Painter) -> Size { 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); @@ -29,14 +32,7 @@ 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) + child } } diff --git a/src/widget/position/set_size.rs b/src/widget/position/set_size.rs index b39d6e6..2632198 100644 --- a/src/widget/position/set_size.rs +++ b/src/widget/position/set_size.rs @@ -6,29 +6,21 @@ 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 Widget for SetSize { - fn draw(&mut self, painter: &mut Painter) { - painter.widget(&self.inner); + fn draw(&mut self, painter: &mut Painter) -> Size { + let child = painter.widget(&self.inner).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..0e7c724 100644 --- a/src/widget/position/span.rs +++ b/src/widget/position/span.rs @@ -8,13 +8,27 @@ pub struct Span { } impl Widget for Span { - fn draw(&mut self, painter: &mut Painter) { - let total = self.len_sum(&mut painter.size_ctx()); + fn draw(&mut self, painter: &mut Painter) -> Size { + 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.widget_within(child, region).size().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(), + }; + 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..36d1db3 100644 --- a/src/widget/position/stack.rs +++ b/src/widget/position/stack.rs @@ -8,30 +8,25 @@ 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]), + fn draw(&mut self, painter: &mut Painter) -> Size { + 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(); + } } + size } } diff --git a/src/widget/ptr.rs b/src/widget/ptr.rs index b3387b2..d25312f 100644 --- a/src/widget/ptr.rs +++ b/src/widget/ptr.rs @@ -6,25 +6,10 @@ 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 + fn draw(&mut self, painter: &mut Painter) -> Size { + match &self.inner { + Some(id) => painter.widget(id).size(), + None => Size::default(), } } } diff --git a/src/widget/rect.rs b/src/widget/rect.rs index f72820e..886fc89 100644 --- a/src/widget/rect.rs +++ b/src/widget/rect.rs @@ -28,21 +28,23 @@ impl Rect { } impl Widget for Rect { - fn draw(&mut self, painter: &mut Painter) { + fn draw(&mut self, painter: &mut Painter) -> Size { painter.primitive(RectPrimitive { color: self.color, radius: self.radius, thickness: self.thickness, inner_radius: self.inner_radius, }); + 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 on_resize(&self, _: Axis) -> OnResize { + OnResize::Scale } } diff --git a/src/widget/text/edit.rs b/src/widget/text/edit.rs index 608ad00..2391cab 100644 --- a/src/widget/text/edit.rs +++ b/src/widget/text/edit.rs @@ -55,44 +55,47 @@ impl TextEdit { } impl Widget for TextEdit { - fn draw(&mut self, painter: &mut Painter) { + fn draw(&mut self, painter: &mut Painter) -> Size { let base = painter.layer; painter.child_layer(); - self.view.draw(painter); + let (_, size) = self.view.draw(painter); painter.layer = base; let region = self.region(); let Some(selection) = self.selection else { - return; + return size; }; let layout = self.view.buf.layout(); // parley reports selection as boxes in layout space, so bidi and // wrapped lines come out right without this code knowing about either. for (rect, _) in selection.geometry(layout) { - let size = vec2(rect.width() as f32, rect.height() as f32); + let rect_size = vec2(rect.width() as f32, rect.height() as f32); let top_left = vec2(rect.x0 as f32, rect.y0 as f32); painter.primitive_within( RectPrimitive::color(Color::SKY), - size.align(Align::TOP_LEFT).offset(top_left).within(®ion), + rect_size + .align(Align::TOP_LEFT) + .offset(top_left) + .within(®ion), ); } let caret = selection.focus().geometry(layout, CARET_WIDTH); - let size = vec2(caret.width() as f32, caret.height() as f32); + let caret_size = vec2(caret.width() as f32, caret.height() as f32); let top_left = vec2(caret.x0 as f32, caret.y0 as f32); painter.primitive_within( RectPrimitive::color(Color::WHITE), - size.align(Align::TOP_LEFT).offset(top_left).within(®ion), + caret_size + .align(Align::TOP_LEFT) + .offset(top_left) + .within(®ion), ); + size } - 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 on_resize(&self, axis: Axis) -> OnResize { + self.view.on_resize(axis) } } diff --git a/src/widget/text/mod.rs b/src/widget/text/mod.rs index 1aec47e..0601b24 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,40 @@ 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 and + /// a taller one does not. Alignment matters too, and separately: glyphs + /// anchored to the start of an axis stay put when that extent changes, + /// but centred or end-aligned ones move even though the shaping stands. + pub fn on_resize(&self, axis: Axis) -> OnResize { + let reshapes = axis == Axis::X && self.attrs.wrap; + let anchored = match axis { + Axis::X => self.align.x, + Axis::Y => self.align.y, + } == AxisAlign::Neg; + match reshapes || !anchored { + true => OnResize::Redraw, + false => OnResize::Translate, + } } pub fn content(&self) -> String { @@ -117,7 +118,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()); @@ -126,19 +127,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); + fn draw(&mut self, painter: &mut Painter) -> Size { + self.update_buf(); + self.view.draw(painter).1 } - 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 on_resize(&self, axis: Axis) -> OnResize { + self.view.on_resize(axis) } } diff --git a/tests/layout.rs b/tests/layout.rs index 940a5aa..85ee631 100644 --- a/tests/layout.rs +++ b/tests/layout.rs @@ -32,3 +32,80 @@ fn resizing_relays_out_against_the_new_output() { assert_corners!(h, left, (0, 0), (100, 100)); assert_corners!(h, right, (100, 0), (800, 100)); } + +#[test] +fn an_empty_widget_takes_a_share_of_a_span() { + let mut h = Harness::new((400, 200)); + let gap = ().add(&mut h.rsc); + let right = rect(Color::BLUE).width(100).add(&mut h.rsc); + h.set_root((gap, right).span(Dir::RIGHT)); + + assert_corners!(h, gap, (0, 0), (300, 200)); + assert_corners!(h, right, (300, 0), (400, 200)); +} + +#[test] +fn a_child_drawn_twice_moves_once() { + let mut h = Harness::new((400, 200)); + // `Aligned` draws its child twice; listing it twice would move it twice. + let inner = rect(Color::BLUE).add(&mut h.rsc); + let centered = inner.center().width(200).add(&mut h.rsc); + let left = rect(Color::RED).width(100).add(&mut h.rsc); + h.set_root((left, centered).span(Dir::RIGHT)); + assert_corners!(h, inner, (100, 0), (300, 200)); + + h.rsc[left].x = Some(Len::abs(150)); + h.frame(); + + assert_corners!(h, inner, (150, 0), (350, 200)); +} + +#[test] +fn a_resize_lands_where_a_cold_start_would() { + let build = |h: &mut Harness| { + let para = wtext( + "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.", + ) + .size(20) + .wrap(true) + .pad(16) + .add(&mut h.rsc); + let below = rect(Color::RED).add(&mut h.rsc); + let root = (para, below).span(Dir::DOWN).pad(12); + h.set_root(root); + (para, below) + }; + + let mut cold = Harness::new((900, 1200)); + let (cold_para, cold_below) = build(&mut cold); + + let mut resized = Harness::new((1920, 1200)); + let (para, below) = build(&mut resized); + resized.resize((900, 1200)); + resized.frame(); + + assert_eq!(resized.region(¶), cold.region(&cold_para), "paragraph"); + assert_eq!(resized.region(&below), cold.region(&cold_below), "below"); +} + +#[test] +fn a_fixed_box_is_drawn_again_rather_than_stretched() { + let mut h = Harness::new((400, 400)); + // The panel fills a stack sized by its sibling, so it is drawn in the + // whole box and then placed in the shorter one. Reusing it in that fixed + // box afterwards would leave it whatever height it happened to have. + let panel = rect(Color::BLUE).add(&mut h.rsc); + let leaf = rect(Color::RED).height(100).add(&mut h.rsc); + let stack = (panel, leaf) + .stack() + .size(StackSize::Child(1)) + .add(&mut h.rsc); + h.set_root(stack.align(Align::TOP)); + assert_corners!(h, panel, (0, 0), (400, 100)); + + h.rsc[leaf].y = Some(Len::abs(250)); + h.frame(); + + assert_corners!(h, panel, (0, 0), (400, 250)); +} diff --git a/tests/remap.rs b/tests/remap.rs new file mode 100644 index 0000000..67cae07 --- /dev/null +++ b/tests/remap.rs @@ -0,0 +1,27 @@ +//! What a drawing can be taken out of, and what it cannot. + +use iris::core::{Remap, UiRegion, UiScalar, UiSpan}; + +/// A box `size` tall whose top is `rel` of the way down the window. +fn fixed(rel: f32, size: f32) -> UiRegion { + UiRegion::new( + UiSpan::FULL, + UiSpan::new(UiScalar { rel, abs: 0.0 }, UiScalar { rel, abs: size }), + ) +} + +#[test] +fn a_fixed_box_can_be_carried_but_not_stretched() { + let from = fixed(0.0, 164.0); + assert!(Remap::new(from, UiRegion::FULL).is_none()); + assert!(Remap::new(from, fixed(0.5, 164.0)).is_some()); + assert!(Remap::new(from, fixed(0.0, 98.0)).is_none()); +} + +#[test] +fn a_relative_box_can_be_stretched_to_any_other() { + let remap = Remap::new(UiRegion::FULL, fixed(0.0, 98.0)).expect("relative boxes remap"); + // A part that filled the window keeps filling what replaced it, which is + // exactly what `outside` could not say for a box of a fixed length. + assert_eq!(remap.apply(UiRegion::FULL), fixed(0.0, 98.0)); +} diff --git a/tests/retained.rs b/tests/retained.rs new file mode 100644 index 0000000..6941a48 --- /dev/null +++ b/tests/retained.rs @@ -0,0 +1,252 @@ +//! 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: OnResize, +} + +impl Widget for Counted { + fn draw(&mut self, _: &mut Painter) -> Size { + self.draws.set(self.draws.get() + 1); + self.size + } + + fn on_resize(&self, _: Axis) -> OnResize { + self.dependence + } +} + +struct Counts(Rc>); + +impl Counts { + fn get(&self) -> usize { + self.0.get() + } +} + +fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (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: OnResize) -> (WeakWidget, Counts, WidgetId) { + let (first, _) = counted(h, Size::from((100, 200)), OnResize::Translate); + 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, OnResize::Scale); + 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, OnResize::Redraw); + 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)), OnResize::Translate); + let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate); + // 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, OnResize::Translate); + + 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)); +} + +/// Lays its child out from the hint alone, never reading what it drew. +struct FromHint { + inner: StrongWidget, +} + +impl Widget for FromHint { + fn draw(&mut self, painter: &mut Painter) -> Size { + let len = painter.size_hint(&self.inner, Axis::Y).unwrap(); + let mut region = UiRegion::FULL; + region.y.end = region.y.start.offset(len.abs); + painter.widget_within(&self.inner, region); + Size::REST + } +} + +#[test] +fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() { + let mut h = Harness::new((400, 200)); + let inner = rect(Color::RED).height(80).add(&mut h.rsc); + let parent = FromHint { + inner: inner.add_strong(&mut h.rsc), + } + .add(&mut h.rsc); + h.set_root(parent); + assert_corners!(h, inner, (0, 0), (400, 80)); + + h.rsc[inner].y = Some(Len::abs(120)); + h.frame(); + + assert_corners!(h, inner, (0, 0), (400, 120)); +} + +/// Reads the output's size, which nothing but its own draw can put right. +struct ReadsOutput { + draws: Rc>, +} + +impl Widget for ReadsOutput { + fn draw(&mut self, painter: &mut Painter) -> Size { + self.draws.set(self.draws.get() + 1); + Size::abs(painter.output_size() / 4.0) + } +} + +#[test] +fn a_resize_does_not_redraw_what_the_shader_can_move() { + let mut h = Harness::new((400, 200)); + let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw); + h.set_root(leaf); + let settled = draws.get(); + + h.resize((800, 100)); + assert!(h.needs_redraw()); + h.frame(); + + assert_eq!( + draws.get(), + settled, + "its box is the same fraction of a different output" + ); + assert_corners!(h, leaf, (0, 0), (800, 100)); +} + +#[test] +fn a_resize_redraws_what_read_the_output() { + let mut h = Harness::new((400, 200)); + let draws = Rc::new(Cell::new(0)); + let leaf = ReadsOutput { + draws: draws.clone(), + } + .add(&mut h.rsc); + h.set_root(leaf); + let settled = draws.get(); + + h.resize((800, 100)); + h.frame(); + + assert_eq!(draws.get(), settled + 1); +} + +#[test] +fn narrowing_the_output_reflows_text_and_relays_out_around_it() { + let mut h = Harness::new((600, 400)); + let para = wtext( + "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.", + ) + .size(20) + .wrap(true) + .add(&mut h.rsc); + let below = rect(Color::RED).add(&mut h.rsc); + h.set_root((para, below).span(Dir::DOWN)); + let top = h.region(&below).expect("drew nothing").top_left.y; + + h.resize((300, 400)); + h.frame(); + + let lower = h.region(&below).expect("drew nothing").top_left.y; + assert!(lower > top, "same words, half the width: {top} -> {lower}"); +} + +#[test] +fn a_change_two_levels_under_its_reader_still_reaches_it() { + let mut h = Harness::new((400, 400)); + // Every wrapper up to the outer pad read the size below it, so the outer + // pad is what draws again -- and the span it hands the box to is the same + // size as before, which is what lets a draw reuse its way past the leaf. + let (leaf, _) = counted(&mut h, Size::abs((100, 100).into()), OnResize::Redraw); + let padded = leaf.pad(10).add(&mut h.rsc); + let below = rect(Color::RED).add(&mut h.rsc); + h.set_root((padded, below).span(Dir::DOWN).pad(12)); + assert_corners!(h, below, (12, 132), (388, 388)); + + h.rsc[leaf].size = Size::abs((100, 200).into()); + h.frame(); + + assert_corners!(h, below, (12, 232), (388, 388)); +}