Size a widget while drawing it, not in a pass of its own

`desired_width`/`desired_height`, `WidgetAxisFns`, `SizeCtx` and the size
cache are gone. A widget states what it used with `Painter::set_size`
while it draws, and `Painter::widget` hands back a `DrawResult` whose
`size()` both reads the child and records that this widget's size depends
on it. Reading nothing keeps the parent independent of what the child came
to.

`Span` is what the change is for. It takes each child's `size_hint` where
there is one, draws only the children that cannot answer, allocates the
flexible space, then places everything -- which deletes `desired_ortho`,
whose own comment said it "literally copies draw ... which makes this slow
and not cool".

Invalidation follows the dependency edges the draw recorded: a widget that
needs redrawing hands off to the highest ancestor that read its size,
instead of re-running a measurement to find out whether anything changed.

`Widget::size_dependence(axis)` says how much of its box a widget's
drawing depends on -- none of it, its own extent, or the whole box -- so
the retained path can keep a drawing and write a new box into it. Asked
per axis, because wrapped text depends on the width it is offered and not
on the height. `Internal` does not yet buy more than `External`: keeping a
drawing when only the room around it changed is a translation, which waits
for the move chain.

`tests/retained.rs` covers the second frame rather than the first, which
is where the bugs were: a placed child that was not recorded as one got
pruned as departed on the next draw.

`examples/text.rs` is new, since wrapping was the one thing here with no
way to see it on its own.
This commit is contained in:
iris committed 2026-09-13 22:51:05 -04:00
1 parent 43ce8c7d02
commit f192f75b25
24 files changed
+610 -523

No files matched your search

+5 -1
View File
@@ -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<WidgetId>,
pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>,
pub children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub size_deps: Vec<WidgetId>,
pub mask: MaskIdx,
pub layer: LayerId,
}
-18
View File
@@ -1,18 +0,0 @@
use crate::{BothAxis, Len, UiVec2, WidgetId, util::HashMap};
#[derive(Default)]
pub struct Cache {
pub size: BothAxis<HashMap<WidgetId, (UiVec2, Len)>>,
}
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();
}
}
-3
View File
@@ -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 {
+87 -21
View File
@@ -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<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>,
pub(super) children: Vec<WidgetId>,
/// The children whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>,
pub(super) size: Option<Size>,
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<Size>) {
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<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
self.widget_at(id, self.region);
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) -> 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<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
self.widget_at(id, region.within(&self.region));
pub fn widget_within<W: ?Sized>(
&mut self,
id: &StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'_, 'a> {
let region = region.within(&self.region);
self.widget_at(id, region)
}
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
fn widget_at<W: ?Sized>(
&mut self,
id: &StrongWidget<W>,
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<W: ?Sized>(&self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
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<W: ?Sized>(&mut self, id: &StrongWidget<W>, 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<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
self.size_ctx().size(id)
}
pub fn len_axis<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>, 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)
}
}
+124 -47
View File
@@ -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<WidgetId, ActiveData>,
pub layers: DrawLayers,
pub(super) output_size: Vec2,
pub cache: Cache,
old_root: Option<WidgetId>,
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<Vec<WidgetId>>,
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<Size> {
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<ActiveData> {
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<WidgetId> {
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
}
}
-89
View File
@@ -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<A: const AxisT>(&mut self, id: WidgetId) -> Len {
if let Some((_, len)) = self.cache.size.axis::<A>().get(&id) {
return *len;
}
let len = self
.widgets
.get_dyn_dynamic(id)
.desired_len::<A>(&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::<A>().insert(id, (self.outer, len));
len
}
pub fn width(&mut self, id: impl IdLike) -> Len {
self.len_inner::<XAxis>(id.id())
}
pub fn height(&mut self, id: impl IdLike) -> Len {
self.len_inner::<YAxis>(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<f32>,
) -> RenderedText {
self.text.render(buffer, attrs, width)
}
pub fn label(&self, id: WidgetId) -> &String {
self.widgets.label(id)
}
}
+35 -18
View File
@@ -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<A: AxisT>(&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<Len> {
None
}
impl<W: Widget + ?Sized> WidgetAxisFns for W {
fn desired_len<A: AxisT>(&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<Len> {
Some(Len::ZERO)
}
fn size_dependence(&self, _axis: Axis) -> SizeDependence {
SizeDependence::None
}
}