Return the size from draw, and fold placing back into drawing

Review response.

`Painter::set_size` is gone: `Widget::draw` returns the `Size` instead, so
a widget that does not say what it used cannot compile rather than
panicking at the widget that forgot. That also settles setting it twice --
a branch that learns something late just returns a different value.

`Painter::place` and `UiRenderState::place` are gone too. `draw_inner`
already tried to reuse an active widget's drawing before redrawing it, so
`place` was `widget_within` with its own bookkeeping bolted on; drawing a
child a second time now *is* how a parent puts it where it belongs, and a
child is deduplicated in `children` because listing one twice would move
it twice. The unification also drops `place`'s use of `ActiveData::layer`,
which is the layer a widget's own `child_layer()` left the painter on
rather than the layer it was drawn into.

What `place` did unconditionally and `widget_within` did not is record the
size dependency, so `Painter::size_hint` now records one: reading a
child's length to lay out around it is reading its size, whether it came
from a draw or from a hint. `tests/retained.rs` has a parent that only
ever reads the hint, which is the case no existing widget exercises.

`()` sizes itself `Size::default()` -- rest -- rather than zero, so it is
a gap that takes an even share of a span; `WidgetPtr` with nothing in it
does the same, since it is the same situation. `Widget::on_resize`'s
default body said `Translate` while the enum's `#[default]` said `Redraw`;
it now defers to the enum.

`was` is `old` throughout, the `OnResize` variant comments are gone, and
so are two empty `impl` blocks.

Checked: fmt, clippy and 27 tests across the workspace; `tabs` on each of
its five tabs, and `tabs` with a replay that adds two images, all
byte-identical to `upstream/main`; `view` and `minimal` likewise; and the
`text` example rendered at 1920x1200 and 900x1200 to see the paragraph
reflow and its container follow.
This commit is contained in:
iris-ai committed 2026-09-14 00:39:39 -04:00
1 parent b108645240
commit 9520996623
20 files changed
+143 -151

No files matched your search

+18 -45
View File
@@ -20,7 +20,6 @@ pub struct Painter<'a> {
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,
}
@@ -70,24 +69,13 @@ 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>) -> DrawResult<'_, 'a> {
self.widget_at(id, self.region)
}
/// Draws a widget somewhere within this one.
/// Useful for drawing child widgets in select areas.
/// 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<W: ?Sized>(
&mut self,
id: &StrongWidget<W>,
@@ -102,10 +90,14 @@ impl<'a> Painter<'a> {
id: &StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'_, 'a> {
self.children.push(id.id());
let child = id.id();
// A child listed twice would be moved twice.
if !self.children.contains(&child) {
self.children.push(child);
}
let size = self.state.draw_inner(
self.layer,
id.id(),
child,
region,
Some(self.id),
self.mask,
@@ -113,43 +105,24 @@ impl<'a> Painter<'a> {
self.rsc,
);
DrawResult {
child: id.id(),
child,
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)
/// Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
let hint = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis)?;
self.depend_on_size(id.id());
Some(hint)
}
/// 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);
fn depend_on_size(&mut self, child: WidgetId) {
if !self.size_deps.contains(&child) {
self.size_deps.push(child);
}
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(
@@ -230,7 +203,7 @@ pub struct DrawResult<'p, 'a> {
impl DrawResult<'_, '_> {
pub fn size(self) -> Size {
self.painter.size_deps.push(self.child);
self.painter.depend_on_size(self.child);
self.size
}
+8 -36
View File
@@ -106,12 +106,11 @@ impl UiRenderState {
primitives: Vec::new(),
children: Vec::new(),
size_deps: Vec::new(),
size: None,
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 {
@@ -123,17 +122,10 @@ impl UiRenderState {
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",
@@ -166,26 +158,6 @@ impl UiRenderState {
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> {
@@ -193,18 +165,18 @@ impl UiRenderState {
return None;
}
let active = self.active.get(&id)?;
let (size, was) = (active.size, active.region);
if was == region {
let (size, old) = (active.size, active.region);
if old == region {
return Some(size);
}
// TODO: epsilon?
if was.size() == region.size() {
self.mov(id, was, region);
if old.size() == region.size() {
self.mov(id, old, region);
return Some(size);
}
if self.reusable(id, region, rsc) {
// Its drawing stands; the new box is remapped into the primitives.
self.mov(id, was, region);
self.mov(id, old, region);
return Some(size);
}
None
@@ -220,10 +192,10 @@ impl UiRenderState {
let Some(widget) = rsc.widgets().get_dyn(id) else {
return false;
};
let mut was = active.region;
let mut old = active.region;
[Axis::X, Axis::Y].into_iter().all(|axis| {
let offered = region.axis_mut(axis).len();
let had = was.axis_mut(axis).len();
let had = old.axis_mut(axis).len();
match widget.on_resize(axis) {
OnResize::Scale => true,
// `Translate` is not acted on yet, and cannot be until a
+7 -16
View File
@@ -20,25 +20,15 @@ pub use widgets::*;
/// text reads the width it is offered and not the height.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OnResize {
/// Stretched to the new box, which is all its drawing ever was.
Scale,
/// Carried to the new box at the size it drew, which it keeps.
Translate,
/// Nothing doing: it draws again.
///
/// The default, because it is the only answer that is right without
/// knowing anything about the widget. The other two are claims that the
/// drawing does not change when the box does, and a widget that inherited
/// such a claim by accident would be quietly wrong -- `SetSize` reports
/// one size and hands its child the whole box, so its pixels very much do
/// change.
#[default]
Redraw,
}
pub trait Widget: Any {
/// Draws the widget, and states what it used with `Painter::set_size`.
fn draw(&mut self, painter: &mut Painter);
/// Draws the widget, and returns what it used of the box it was given.
fn draw(&mut self, painter: &mut Painter) -> Size;
/// 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
@@ -48,17 +38,18 @@ pub trait Widget: Any {
}
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::Translate
OnResize::default()
}
}
impl Widget for () {
fn draw(&mut self, painter: &mut Painter) {
painter.set_size(Size::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 size_hint(&self, _axis: Axis) -> Option<Len> {
Some(Len::ZERO)
Some(Len::default())
}
fn on_resize(&self, _axis: Axis) -> OnResize {