Redesign span layout around retained placement

This commit is contained in:
iris committed 2026-09-09 15:11:57 -04:00
1 parent 4b69f3cc6b
commit 992482414f
23 files changed
+426 -1005

No files matched your search

+2 -16
View File
@@ -264,22 +264,8 @@ impl Primitives {
slot as u32
}
/// Rewrites a slot this widget already owns, instead of freeing it
/// and allocating another -- the recycle path
/// (`Painter::write_primitive`).
///
/// **Why a redraw must be able to do this.** Freed slots do not
/// become reusable until the end of the frame (`freed`), so a widget
/// that frees its primitives and immediately draws again takes fresh
/// slots every time. Since a container learns a child's size by
/// drawing it (`Painter::measure`, and the real draw that follows)
/// and containers nest, that made the arena's
/// high-water the *transient* push count rather than the live one:
/// measured over the bench fixture's 401 streamed deltas
/// (`scripts/rigs/ui-profile`'s `arena_churn`) at 17 million pushes
/// and 127,443 slots for 11,569 live primitives, growing linearly
/// with the transcript.
///
/// Rewrites a slot this widget already owns. Freed slots are not reusable
/// until the end of a frame, so nested provisional redraws must recycle.
/// The caller has already checked that `h` is the same kind of
/// primitive in the same layer, which is what makes the slot, its
/// entry in the per-primitive data, and its position in the layer's
+5 -49
View File
@@ -11,59 +11,15 @@ pub struct ActiveData {
pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>,
pub children: Vec<WidgetId>,
/// The mask this widget was drawn **under** (its parent's), not the
/// one it set for itself -- see `own_mask` for that.
/// The inherited mask, not `own_mask`.
pub mask: MaskIdx,
/// The mask slot this widget allocated for *itself* with
/// `Painter::set_mask`, or `MaskIdx::NONE`. Kept across redraws and
/// rewritten in place, the way `move_slot` is: a `Masked` that pushed
/// a fresh slot each draw left every already-drawn descendant --
/// which `draw_inner`'s unchanged-region fast path does not revisit --
/// clipping to the *old* slot's region, so a composer whose bar had
/// since been placed at the bottom of the screen was still being
/// clipped to a box at the top of it and drew nothing (measured
/// 2026-09-06: four mask entries live, none of them the widget's
/// current region). Its path out is the `undraw` branch of
/// `UiRenderState::remove`, which drops the self-ownership ref taken
/// when the slot was allocated.
/// The widget's retained mask slot, or `MaskIdx::NONE`.
pub own_mask: MaskIdx,
pub layer: LayerId,
/// What `Widget::draw` returned the last time this widget was actually
/// drawn -- read by a parent placing this widget again without
/// redrawing it, replacing `Cache.size`'s old role. See LAYOUT.md
/// section 5.
/// The last `Widget::draw` result.
pub size: Size,
/// This widget's slot in `UiData::move_offsets`, assigned on its first
/// draw and kept for the rest of its life (redraws reuse it in place
/// so a retained child's `parent` link never goes stale). See
/// LAYOUT.md section 2.
/// Retained so descendants' parent links stay valid across redraws.
pub move_slot: MoveIdx,
/// How much of this widget's own `move_slot` delta is already folded
/// into `region` above, in window pixels. The two mechanisms that
/// write that slot disagree about this and cannot be told apart from
/// the slot alone: `UiRenderState::mov` shifts `region` and the delta
/// together (the *offered* region genuinely moved), while
/// `Painter::reposition` writes only the delta (`region` stays the
/// offered box and the delta says where inside it the content was
/// placed). So anything that wants the widget's real position --
/// `resolved_region`, and through it every hit test -- must subtract
/// this from the chain sum. Without it a panned widget's own hit box
/// sits at twice the pan while its descendants' are correct, which is
/// how it went unnoticed: the composer's field became untappable
/// after a finger pan (2026-09-06). Reset to zero whenever the widget
/// is really redrawn, since `draw_inner` zeroes the slot then too.
/// The part of this widget's move delta already folded into `region`.
pub move_applied: Vec2,
/// The offset the last `Painter::reposition` placed this widget's
/// content at *within* `region`, in window pixels. The move slot has
/// exactly one owner and one meaning:
/// `move_offsets[move_slot] == move_applied + repositioned`. `mov`
/// adds to the first, `reposition` overwrites the second (it
/// recomputes `from` afresh every call, so repeating it must land on
/// the same answer rather than drifting), and both then rewrite the
/// slot from the sum -- which is what lets a parent both move a child
/// with its own layout and place it inside that moved region in one
/// frame. `LazySpan::place`'s Bottom-known branch does exactly that once a
/// row's blocks wrap. Reset to zero on a real redraw, with
/// `move_applied` and the slot itself.
pub repositioned: Vec2,
}
+68 -106
View File
@@ -1,6 +1,6 @@
use crate::{
Color, DrawMode, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData,
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
Axis, Color, Len, RegionAlign, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer,
TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
render::{
Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive,
PrimitiveHandle, PrimitiveInst, RectPrimitive,
@@ -9,7 +9,6 @@ use crate::{
util::Vec2,
};
/// makes your surfaces look pretty
pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc,
@@ -17,43 +16,19 @@ pub struct Painter<'a> {
pub(super) region: UiRegion,
pub(super) mask: MaskIdx,
pub(super) move_slot: MoveIdx,
/// This widget's own mask slot, reused across redraws -- see
/// `ActiveData::own_mask`. `MaskIdx::NONE` until `set_mask` is called
/// for the first time in this widget's life.
/// This widget's retained mask slot.
pub(super) own_mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>,
/// The handles this widget owned before *this* draw, offered back to
/// it in the order it wrote them last time -- see
/// [`Self::take_recycled`]. Empty for a widget being drawn for the
/// first time. Whatever is left when the draw ends is genuinely gone,
/// and `UiRenderState::draw_inner` frees the remainder.
///
/// An iterator rather than a vec and a cursor because a
/// `PrimitiveHandle` is an ownership token and deliberately not
/// `Clone`: `peek` asks whether the next one fits without taking it,
/// `next` takes it, and what is left is exactly what nothing claimed.
/// Previous handles, consumed in draw order and freed if left over.
pub(super) recycle: std::iter::Peekable<std::vec::IntoIter<PrimitiveHandle>>,
pub(super) children: Vec<WidgetId>,
pub(super) reuse_child_sizes: bool,
pub layer: usize,
pub(super) id: WidgetId,
/// Whether this draw produces what goes on screen or only a size --
/// see [`crate::DrawMode`]. Inherited by every child this widget
/// draws, so one `measure` at the top makes the whole subtree
/// write-free.
pub(super) mode: DrawMode,
}
impl<'a> Painter<'a> {
/// True while this draw is only being asked how big the widget would
/// be. **Every method here that writes anything must return early on
/// it** -- a widget's own `draw` never has to check, which is the
/// point: measuring is a property of the painter, not something each
/// widget re-implements.
pub fn measuring(&self) -> bool {
self.mode == DrawMode::Measure
}
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
self.write_primitive(primitive, region, Drawn::Yes);
}
@@ -74,8 +49,6 @@ impl<'a> Painter<'a> {
let h = self.recycle.peek()?;
let drawn_matches = (h.pos == NOT_DRAWN) == (drawn == Drawn::No);
if h.binding != binding || h.layer != self.layer || !drawn_matches {
// Left un-taken deliberately: this handle and everything after
// it is freed together when the draw ends.
return None;
}
self.recycle.next()
@@ -89,9 +62,6 @@ impl<'a> Painter<'a> {
region: UiRegion,
drawn: Drawn,
) -> u32 {
if self.measuring() {
return u32::MAX;
}
let inst = PrimitiveInst {
id: self.id,
primitive,
@@ -107,7 +77,6 @@ impl<'a> Painter<'a> {
None => self.state.write_primitive(self.layer, drawn, inst),
};
if self.mask != MaskIdx::NONE {
// TODO: I have no clue if this works at all :joy:
self.rsc.ui_mut().masks.push_ref(self.mask);
}
let slot = h.slot;
@@ -157,13 +126,6 @@ impl<'a> Painter<'a> {
/// so keeps pointing at whichever slot it was drawn under. See
/// `ActiveData::own_mask` for what pushing a fresh one cost.
pub fn set_mask(&mut self, region: UiRegion) {
// Clipping changes no widget's reported size, so a measurement
// skips it whole -- not just the shape primitive, but the mask
// slot and its refs, which would otherwise be a leaked slot per
// masked widget per measured frame.
if self.measuring() {
return;
}
let shape = self.write_primitive(RectPrimitive::color(Color::NONE), region, Drawn::No);
self.set_mask_to(shape);
}
@@ -176,12 +138,6 @@ impl<'a> Painter<'a> {
/// with no radius argument anywhere that could fall out of step with
/// the one being drawn.
pub fn set_mask_to_widget<W: ?Sized>(&mut self, shape: &StrongWidget<W>) {
// Same as `set_mask`, and doubly so: a measurement leaves nothing
// in `active`, so the shape widget has drawn no primitive to
// point at and this would panic on its own message.
if self.measuring() {
return;
}
let slot = self.state.first_primitive(shape.id()).unwrap_or_else(|| {
panic!(
"'{}' was given as a mask's shape but drew no primitive, so there is nothing to \
@@ -261,6 +217,16 @@ impl<'a> Painter<'a> {
self.widget_at(id, region.within(&self.region))
}
pub fn known_len<W: ?Sized>(&self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
if let Some(len) = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) {
return Some(len.fold_dp(self.density()));
}
if !self.reuse_child_sizes || self.rsc.widgets().needs_redraw.contains(&id.id()) {
return None;
}
self.state.active.get(&id.id()).map(|a| a.size.axis(axis))
}
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size {
self.children.push(id.id());
// Passed directly rather than looked up from `self.active`: this
@@ -277,66 +243,65 @@ impl<'a> Painter<'a> {
Some(self.id),
self.move_slot.idx() as u32,
self.mask,
self.mode,
Retained::default(),
self.rsc,
)
}
/// Ask `widget` how big it would be in `region`, **writing nothing**
/// -- see [`DrawMode::Measure`]. For the container that cannot choose
/// what to offer a child without already knowing the child's size:
/// measure, work out the real region, then draw it for real.
///
/// ```ignore
/// let used = painter.measure(&child, generous);
/// painter.widget_within(&child, self.box_for(used));
/// ```
///
/// This replaced a `draw_twice(child, first, |used| second)`, which
/// made the same two draws but had the caller express the second
/// region as a closure returning it -- so the interesting arithmetic
/// happened inside a callback and anything it wanted to keep had to
/// be written out through a captured `&mut`. Two statements say the
/// same thing in the order it happens (CODE_RULES' "compose
/// linearly"), and the measurement costs no arena slot now rather
/// than allocating one and freeing it.
///
/// The measured widget is left exactly as it was -- not in `active`
/// if it was not there before, and untouched if it was -- so the draw
/// that follows is an ordinary one and cannot be short-circuited by
/// the measurement having "already drawn" it at that region.
pub fn measure<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size {
self.state.draw_inner(
self.layer,
id.id(),
region.within(&self.region),
Some(self.id),
self.move_slot.idx() as u32,
self.mask,
DrawMode::Measure,
Retained::default(),
self.rsc,
)
}
/// Move an already-drawn child from wherever it currently sits to
/// `region` (resolved against this widget's own region, matching
/// `widget_within`) without a second draw -- an O(1) offset write via
/// `UiRenderState::mov`. For a container that draws a child
/// provisionally to learn its size (e.g. `Aligned`) and then places it
/// for real. Only valid when the target keeps the child's drawn size;
/// if the shape actually changes, the normal `widget_within` dispatch
/// (which detects that from the stored region) does the right thing
/// instead.
pub fn reposition<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
// Moves an *already-drawn* child, of which a measurement has
// none.
if self.measuring() {
return;
}
/// Place an already-drawn child's used area, redrawing only if its size changes.
pub fn place<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) -> Size {
let region = region.within(&self.region);
self.state.reposition(id.id(), region, self.rsc);
let retained = self
.state
.active
.get(&id.id())
.map(|active| (active.layer, active.mask));
if let Some(size) = self.state.place(id.id(), region, self.rsc) {
size
} else if let Some((layer, mask)) = retained {
self.children.push(id.id());
self.rsc.widgets_mut().needs_redraw.insert(id.id());
self.state.draw_inner(
layer,
id.id(),
region,
Some(self.id),
self.move_slot.idx() as u32,
mask,
Retained::default(),
self.rsc,
)
} else {
self.widget_at(id, region)
}
}
pub fn place_used<W: ?Sized>(
&mut self,
id: &StrongWidget<W>,
used: Size,
within: UiRegion,
) -> Size {
let region = self.fit_region(used, within);
self.place(id, region)
}
pub fn fit_region(&mut self, used: Size, mut within: UiRegion) -> UiRegion {
let mut region = used
.to_uivec2(self.density())
.align(RegionAlign::TOP_LEFT)
.within(&within);
let output = self.output_size();
for axis in [Axis::X, Axis::Y] {
let mut actual = region.within(&self.region);
let mut available = within.within(&self.region);
if actual.axis(axis).len().to_abs(output.axis(axis))
> available.axis(axis).len().to_abs(output.axis(axis))
{
*region.axis_mut(axis) = *within.axis(axis);
}
}
region
}
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
@@ -358,9 +323,6 @@ impl<'a> Painter<'a> {
/// the layer's one instanced draw, so it goes through
/// `Primitives::write_image` instead of `primitive_at`/`Primitive::vec`.
fn write_image(&mut self, texture_idx: u32, region: UiRegion) {
if self.measuring() {
return;
}
let h = match self.take_recycled(IMAGE_BINDING, Drawn::Yes) {
Some(h) => {
self.state.primitives.recycle_image(
+104 -211
View File
@@ -2,8 +2,8 @@ use std::sync::Mutex;
use std::time::{Duration, Instant};
use crate::{
ActiveData, IdLike, MaskIdx, MoveIdx, Painter, PixelRegion, PrimitiveLayers, RegionAlign, Size,
StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
ActiveData, Axis, IdLike, MaskIdx, MoveIdx, Painter, PixelRegion, PrimitiveLayers, RegionAlign,
Size, StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
render::{
Drawn, MoveOffset, NOT_DRAWN, Primitive, PrimitiveHandle, PrimitiveInst, Primitives,
RectPrimitive, rounded_rect_coverage,
@@ -94,65 +94,19 @@ pub struct UiRenderState {
last_input_at: Mutex<Option<Instant>>,
}
/// Whether a draw is producing what goes on screen, or only asking a
/// widget how big it would be.
///
/// **There is no size query without a draw** (LAYOUT.md section 5):
/// `Widget::draw` reports the size it used, and nothing else can answer
/// it. A container that cannot choose what to offer a child without
/// already knowing the child's size therefore has to draw it -- so
/// [`Self::Measure`] is that draw with everything it *writes* switched
/// off. It allocates no arena slot, no mask, no move slot, leaves nothing
/// in `active` and marks nothing dirty; the widget is walked and its text
/// is shaped (which is memoized, and is the expensive half anyway), and
/// only the returned `Size` survives.
///
/// Because it leaves no trace, the real draw that follows is an ordinary
/// first draw or redraw and cannot be short-circuited by the measurement
/// having "already drawn" the widget at that region -- which is the trap
/// the discarded-draw approach it replaced had to work around.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DrawMode {
/// Write primitives, keep the result in `active`.
Draw,
/// Report a size and write nothing.
Measure,
}
impl DrawMode {
fn measuring(self) -> bool {
self == Self::Measure
}
}
/// What a widget being redrawn keeps from the draw it is replacing.
///
/// These four always travel together -- they are read off one
/// `ActiveData` that was just taken out of `active` and handed straight
/// to the draw that replaces it -- and they were four positional
/// parameters of [`UiRenderState::draw_inner`] until 2026-09-09, next to
/// six others. [`Default`] is the "nothing to keep" case: a widget drawn
/// for the first time, and the root of a full relayout.
/// State retained while replacing one draw with another.
pub(crate) struct Retained {
/// So children this draw does not draw again can be retired.
pub region: Option<UiRegion>,
pub children: Vec<WidgetId>,
/// Reused in place with its delta reset, never reallocated: a
/// descendant that is not itself redrawn still points at it. See
/// LAYOUT.md section 2.
pub move_slot: Option<MoveIdx>,
pub own_mask: MaskIdx,
/// Slots the draw may write into instead of allocating -- see
/// `Painter::take_recycled`. Anything it does not claim is freed when
/// the draw ends.
pub primitives: Vec<PrimitiveHandle>,
}
impl Default for Retained {
/// Nothing kept: no children to retire, no move slot to reuse, no
/// mask of its own yet, nothing to recycle. Hand-written because
/// `MaskIdx`'s zero is a real slot rather than "none".
fn default() -> Self {
Self {
region: None,
children: Vec::new(),
move_slot: None,
own_mask: MaskIdx::NONE,
@@ -453,7 +407,6 @@ impl UiRenderState {
None,
MoveOffset::NONE_PARENT,
MaskIdx::NONE,
DrawMode::Draw,
Retained::default(),
rsc,
);
@@ -477,7 +430,6 @@ impl UiRenderState {
.unwrap_or(MoveOffset::NONE_PARENT)
}
// TODO: should prolly make a DrawInfo struct or smth for everything other than rsc
#[allow(clippy::too_many_arguments)]
pub(super) fn draw_inner(
&mut self,
@@ -487,120 +439,83 @@ impl UiRenderState {
parent: Option<WidgetId>,
parent_move_slot: u32,
mask: MaskIdx,
mode: DrawMode,
retained: Retained,
rsc: &mut dyn UiRsc,
) -> Size {
let Retained {
region: mut old_region,
children: mut old_children,
move_slot: mut old_move_slot,
mut own_mask,
primitives: mut recycle,
} = retained;
// Consumed here, not merely read: this call *is* the redraw the mark
// asked for, and leaving the mark set is what stranded a widget's
// primitives. A measure-then-draw reaches this twice for the same id
// in one frame (`LazySpan::place`'s measurement pass), and on the second
// call the still-set mark took the whole `if let` below -- including
// the `remove` that frees the first draw's primitives -- out of play,
// so `active.insert` at the end overwrote the only handles that could
// ever have freed them. The result is a full second copy of the row,
// drawn every frame from then on at the oversized measurement region
// and, with `LazySpan` setting no mask, outside the list's own bounds:
// the doubled `Compacted:` row in docs/bench/iris-phone-v2-2026-09-06.md.
// The same shape reaches any dirty widget an ancestor redraws first.
// A measurement **peeks** at the mark rather than consuming it: it
// is not the redraw the mark asked for, and swallowing it would
// leave the widget stale until something else marked it again.
let dirty = if mode.measuring() {
rsc.widgets().needs_redraw.contains(&id)
} else {
rsc.widgets_mut().needs_redraw.remove(&id)
};
let dirty = rsc.widgets_mut().needs_redraw.remove(&id);
let requires_exact_region = rsc
.widgets()
.get_dyn(id)
.is_some_and(|widget| widget.requires_exact_region());
// What a measurement can answer without drawing at all.
//
// A widget's reported size is a function of its own state and the
// size it was offered -- not of where it was offered. So an
// undirtied widget already drawn at a region of this size has
// *already answered this question*, and `active.size` is that
// answer. This is the same assumption `mov` below already makes
// (same offered size, therefore identical output, therefore a
// translation rather than a redraw); it is only stated here as a
// size rather than acted on as a move.
//
// Without it a measurement costs a full recursive walk of the
// subtree, and since the containers that measure nest, that walk
// is what made one streamed frame 1,083 `Widget::draw` calls over
// 113 distinct widgets.
if mode.measuring() {
if let Some(active) = self.active.get(&id)
&& !dirty
&& active.region.size() == region.size()
{
return active.size;
}
} else if let Some(active) = self.active.get_mut(&id)
if let Some(active) = self.active.get_mut(&id)
&& !dirty
&& active.layer == layer
&& active.mask == mask
{
// check to see if we can skip drawing first
if active.region == region {
return active.size;
} else if active.region.size() == region.size() {
// TODO: epsilon?
} else if Self::same_size(active.region, region, self.output_size) {
let from = active.region;
let size = active.size;
self.mov(id, from, region, rsc);
return size;
} else if !requires_exact_region
&& Self::same_size(
active
.size
.to_uivec2(self.density)
.align(RegionAlign::TOP_LEFT)
.within(&active.region),
region,
self.output_size,
)
{
return self.place(id, region, rsc).unwrap();
} else if rsc
.widgets()
.get_dyn(id)
.map(|w| w.is_size_independent())
.unwrap_or(false)
{
// The offered region changed shape, but this widget's own
// drawn output does not depend on it (a fixed-size leaf) --
// rewrite its own primitives' regions in place (O(primitives
// owned directly by this widget, which for a leaf is O(1))
// instead of redrawing. See LAYOUT.md section 3.
let from = active.region;
for h in &active.primitives {
let r = self.primitives.region_mut(h);
*r = r.outside(&from).within(&region);
self.region_mut_count += 1;
}
// `move_applied` is deliberately **not** touched here,
// unlike in `mov`: it counts the part of this widget's own
// move-slot delta that `region` has already absorbed, and
// this branch writes no delta at all -- the primitives were
// moved directly. Counting one would make
// `resolved_region` subtract a distance the chain never
// held, putting the hit box short of the drawing by
// exactly this step. See `ActiveData::move_applied`, and
// `a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at`.
active.region = region;
return active.size;
}
// if not, then maintain resize and track old children to remove unneeded
let active = self.remove(id, false, true, rsc).unwrap();
old_region = Some(active.region);
old_children = active.children;
old_move_slot = Some(active.move_slot);
own_mask = active.own_mask;
recycle = active.primitives;
} else if dirty && self.active.contains_key(&id) {
debug_assert!(!mode.measuring());
// Dirty and already drawn: none of the fast paths above may be
// taken (the widget's own content changed, so its old primitives
// say nothing about its new ones), but they are also the only
// thing that frees them. Same two lines, reached the other way.
} else if self.active.contains_key(&id) {
let layer_changed = self
.active
.get(&id)
.is_some_and(|active| active.layer != layer);
let active = self.remove(id, false, true, rsc).unwrap();
if layer_changed {
rsc.on_undraw(&active);
}
old_region = Some(active.region);
old_children = active.children;
old_move_slot = Some(active.move_slot);
own_mask = active.own_mask;
recycle = active.primitives;
}
// draw widget
let reentrant = !self.draw_started.insert(id);
debug_assert!(
!reentrant,
@@ -608,23 +523,11 @@ impl UiRenderState {
the second draw's primitives would orphan the first's"
);
// A measurement writes no primitive, so nothing ever reads this
// -- and allocating one would leak a slot per measured widget per
// frame, since `move_offsets` only frees on a widget's removal.
let move_slot = match mode {
DrawMode::Measure => Id::preset(MoveOffset::NONE_PARENT),
DrawMode::Draw => Self::move_slot_for(old_move_slot, parent_move_slot, rsc),
};
let move_slot = Self::move_slot_for(old_move_slot, parent_move_slot, rsc);
// The mask this widget was drawn *under*, kept aside because
// `Painter::set_mask` overwrites `painter.mask` with the widget's
// own new one -- and `ActiveData::mask`'s only consumer is
// `redraw`, which feeds it back in as the *inherited* mask. Storing
// the set one instead handed a `Masked` its own mask on every
// targeted redraw -- an abort the first time the composer's scroll
// area was redrawn on the emulator, and now (masks nest) a mask
// whose parent is itself, which `set_mask`'s own assert names.
let inherited_mask = mask;
let reuse_child_sizes =
old_region.is_some_and(|old| Self::same_size(old, region, self.output_size));
let mut painter = Painter {
state: self,
region,
@@ -637,23 +540,34 @@ impl UiRenderState {
primitives: Vec::new(),
recycle: recycle.into_iter().peekable(),
children: Vec::new(),
mode,
reuse_child_sizes,
rsc,
};
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
let density = painter.density();
let hints = [
widget.size_hint(Axis::X).map(|len| len.fold_dp(density)),
widget.size_hint(Axis::Y).map(|len| len.fold_dp(density)),
];
painter.state.draw_count += 1;
let size = widget.draw(&mut painter);
// A reported length is consumed by containers that read `abs`,
// `rel` and `rest` straight off it (`Span`'s placement, `Pad`'s
// addition), so an unresolved `dp` in one is silently worth zero
// -- see `Len::fold_dp`, which is what a widget reporting a
// caller-declared size has to put it through.
debug_assert!(
size.x.dp == 0.0 && size.y.dp == 0.0,
"widget {id:?} reported an unresolved `dp` size ({size:?}); \
report `Len::fold_dp(painter.density())` instead"
);
for (axis, hint) in [Axis::X, Axis::Y]
.into_iter()
.zip(hints)
.filter_map(|(axis, hint)| hint.map(|hint| (axis, hint)))
{
debug_assert_eq!(
size.axis(axis),
hint,
"widget {id:?}'s {axis:?} size hint differs from its draw result"
);
}
drop(widget);
painter.state.draw_started.remove(&id);
@@ -668,29 +582,11 @@ impl UiRenderState {
primitives,
recycle,
children,
reuse_child_sizes: _,
layer,
id,
mode: _,
} = painter;
if mode.measuring() {
// Nothing to unwind: a measurement allocates no slot, no
// mask, no move offset and no `ActiveData`, so the size is
// the whole of what it produced. Asserted rather than
// assumed, because a `Painter` method that forgot to check
// the mode would otherwise leak silently -- one primitive per
// measured widget per frame, which a screen redrawn every
// frame turns into an arena that grows without bound.
debug_assert!(
primitives.is_empty() && textures.is_empty(),
"measuring {id:?} wrote {} primitive(s) and {} texture(s); \
every `Painter` write must check `Painter::measuring`",
primitives.len(),
textures.len(),
);
return size;
}
// Whatever the draw did not claim is genuinely gone: this draw
// wrote fewer primitives than the last one, or stopped matching
// part way. Freeing it here rather than in `remove` is what lets
@@ -714,7 +610,6 @@ impl UiRenderState {
move_slot,
own_mask,
move_applied: Vec2::ZERO,
repositioned: Vec2::ZERO,
};
// remove old children that weren't kept
@@ -782,67 +677,64 @@ impl UiRenderState {
self.mov_count += 1;
}
/// Move an already-active widget to `to`. Used by `Painter::reposition`,
/// for a parent that drew a child provisionally (at the whole region it
/// was offered) and now knows where the child actually belongs.
///
/// Unlike `mov` (called by `draw_inner`'s own dispatch, where the
/// *offered* region really did move and `active.region` already tracks
/// it), the child here was not offered a smaller region -- it was
/// offered everything and chose, on its own, to occupy only
/// `active.size` of it. By convention every widget in this crate that
/// does that anchors its own content at the top-left of whatever it
/// was given (`Rect`/`Image`/`Sized`/`MaxSize` -- see their `draw`
/// bodies), so that is where this assumes the child was actually
/// painted, not `active.region` itself (which is the *offered* box,
/// usually bigger). A nested `Aligned` whose own child is not top-left
/// anchored -- i.e. `Aligned` wrapping `Aligned` -- is the one shape
/// this does not cover; none of iris's widgets or examples build that
/// today. See LAYOUT.md's "Rejected, and why" / deviations for the
/// full reasoning.
///
/// The delta is overwritten, not accumulated like `mov`'s: `from` is
/// recomputed fresh from `active.size`/`active.region` every call, so
/// repeating the same `reposition` (e.g. an unrelated redraw elsewhere
/// re-running this widget's parent without its own layout changing)
/// must land on the same answer, not drift further each time.
pub(super) fn reposition(&mut self, id: WidgetId, to: UiRegion, rsc: &mut dyn UiRsc) {
let Some(active) = self.active.get(&id) else {
return;
};
let move_applied = active.move_applied;
let repositioned = active.repositioned;
fn same_size(a: UiRegion, b: UiRegion, output: Vec2) -> bool {
let a = a.size().to_abs(output);
let b = b.size().to_abs(output);
(a.x - b.x).abs() < 0.01 && (a.y - b.y).abs() < 0.01
}
pub(super) fn place(
&mut self,
id: WidgetId,
to: UiRegion,
rsc: &mut dyn UiRsc,
) -> Option<Size> {
let active = self.active.get(&id)?;
if rsc
.widgets()
.get_dyn(id)
.is_some_and(|widget| widget.requires_exact_region())
&& !Self::same_size(active.region, to, self.output_size)
{
return None;
}
let from = active
.size
.to_uivec2(self.density)
.align(RegionAlign::TOP_LEFT)
.within(&active.region);
if !Self::same_size(from, to, self.output_size) {
return None;
}
let size = active.size;
if active.region == to {
return Some(size);
}
if Self::same_size(active.region, to, self.output_size) {
let region = active.region;
self.mov(id, region, to, rsc);
return Some(size);
}
let move_applied = active.move_applied;
let slot = active.move_slot;
let from_px = from.top_left().to_abs(self.output_size);
let to_px = to.top_left().to_abs(self.output_size);
let delta = to_px - from_px;
// Not `delta` alone: a parent may have `mov`ed this widget to a
// region that itself moved earlier in the same frame, and that
// part of the slot is `move_applied`'s, not this call's. Writing
// `delta` on its own dropped it and put the content back at the
// pre-move position. `from` is computed against `active.region`,
// which `mov` already updated, so `delta` is purely the placement
// inside the region and the two summands never overlap.
if delta.x.abs() < 0.01 && delta.y.abs() < 0.01 {
if let Some(active) = self.active.get_mut(&id) {
active.region = to;
}
return Some(size);
}
let entry = rsc.ui_mut().move_offsets.get_mut(slot);
debug_assert_eq!(
entry.delta,
[
move_applied.x + repositioned.x,
move_applied.y + repositioned.y
],
"widget {id:?}'s move slot was written by something other than `mov`/`reposition`; \
the slot is theirs and means `move_applied + repositioned` -- see `ActiveData`"
);
entry.delta = [move_applied.x + delta.x, move_applied.y + delta.y];
if let Some(active) = self.active.get_mut(&id) {
active.repositioned = delta;
active.region = to;
active.move_applied += delta;
}
self.mov_count += 1;
Some(size)
}
/// Retires `id`'s primitives (unless `keep_primitives`, in which case
@@ -1073,6 +965,7 @@ impl UiRenderState {
/// against [`Self::orphaned_primitives`]'s O(primitives), which on a
/// transcript is tens of thousands and made a debug build on a phone
/// too slow to finish a benchmark run.
#[cfg(debug_assertions)]
fn primitive_counts_agree(&self) -> bool {
let live: usize = self.primitives.live_count();
let owned: usize = self.active.values().map(|a| a.primitives.len()).sum();
@@ -1316,8 +1209,8 @@ impl UiRenderState {
parent,
parent_move_slot,
active.mask,
DrawMode::Draw,
Retained {
region: Some(active.region),
children: active.children,
move_slot: Some(active.move_slot),
own_mask: active.own_mask,
+17 -31
View File
@@ -1,4 +1,4 @@
use crate::{Painter, Size};
use crate::{Axis, Len, Painter, Size};
use std::any::Any;
mod data;
@@ -16,46 +16,28 @@ pub use view::*;
pub use widgets::*;
pub trait Widget: Any {
/// Draw within `painter.region()` (the space the parent offered) and
/// report how much of it was actually used, per axis.
fn draw(&mut self, painter: &mut Painter) -> Size;
/// True if `draw`'s output (both the primitives it writes and the
/// `Size` it returns) is the same for any `painter.region()` of the
/// same *content* -- an icon, a fixed-size rect, an already-decoded
/// image at its natural size. Default `false` (redraw on any change to
/// the offered region) because assuming independence wrongly produces
/// a stale draw; a widget must opt in. See LAYOUT.md.
/// An exact, context-free length known without drawing or inspecting children.
fn size_hint(&self, _axis: Axis) -> Option<Len> {
None
}
/// Whether the draw result is independent of the offered region.
fn is_size_independent(&self) -> bool {
false
}
/// What kind of control this is, for the AccessKit tree `ui::access`
/// builds (RUST.md's I4). Only consulted for a widget that also has an
/// explicit `.label()` -- an unnamed widget is never visited by that
/// tree at all, named or not, so the default here costs nothing except
/// at the handful of call sites that opt in. Default `Unknown` (a
/// generic control with no more specific semantics); a widget with a
/// real platform equivalent -- `TextEdit`'s `MultilineTextInput` --
/// overrides it.
fn requires_exact_region(&self) -> bool {
false
}
/// The AccessKit role for a labelled widget.
fn access_role(&self) -> accesskit::Role {
accesskit::Role::Unknown
}
/// Advance whatever this widget is animating to `now`, and say whether
/// it is still animating afterwards. Default: nothing is, so a widget
/// opts in by overriding this *and* by something calling
/// [`crate::UiData::animate`] with its id when the animation starts --
/// which is that animation's path out, since the driver
/// ([`crate::UiData::tick_animations`]) drops every id whose `tick`
/// answers `false`.
///
/// Called once per frame, before the frame's draw, by whichever
/// backend owns the surface; a `true` answer is what makes that
/// backend ask for another frame. So this is the only thing in iris
/// that moves without an input event, and a widget that animates
/// without registering simply never moves -- which is exactly how a
/// finger fling looked on Iris's phone before this existed.
/// Advance an animation and report whether it needs another frame.
#[allow(unused_variables)]
fn tick(&mut self, now: std::time::Instant) -> bool {
false
@@ -70,6 +52,10 @@ impl Widget for () {
fn is_size_independent(&self) -> bool {
true
}
fn size_hint(&self, _axis: Axis) -> Option<Len> {
Some(Len::ZERO)
}
}
impl dyn Widget {