A local redraw drew a dirty widget in the box it was placed in. When a reader decided that box from the widget's own answer -- an aligned span sized to its children, a text at the tail of a row, a scroll's content -- the old answer is a fixed point of measuring there whatever the content now says, so the layout had two stable answers and which one it reached depended on the tree's history. `tests/unsettled.rs` has the two shrunk cases: the four-widget aligned span, and a scroll placing a pass-through `SetSize` in a box the content decided, where the span under it was placed once and nothing at its own edge said so. `ActiveData::offered_px` keeps the pixel size of the box the parent first asked about the child in, whether through `known_len` or a first `place`, beside `px`, the box it drew against. A dirty widget whose size reads an axis on which some reader up its chain gave what it read a box other than the one it asked in is not drawn locally: the chain is marked and the parent of the highest such placement draws, since above it every box is a constraint rather than an answer. The walk goes up the whole reader chain because a pass-through hands a derived box down unchanged. `Scroll` read its box's length for the clamp through `px_len`, which records the reported size as depending on it, and it does not: its size is its content's. That made every scroll tick a size question asked in a derived box, at 34x the instructions. `Painter::px_len_for_draw` is the read that records nothing. Instructions per frame on the depth-8 rig against the previous head: `many` at 32 dirty 0.66M to 0.74M, at 130 dirty 27.7M to 26.5M, `resize` 15.8M to 15.0M, `scroll`, `repaint` and `size` unchanged. The shrinking fuzzer passes 200 trees at depth 7 in all four cases, the hundred-seed sweep passes, and the five reference renders and the resize render are byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
416 lines
14 KiB
Rust
416 lines
14 KiB
Rust
#[cfg(feature = "layout-diagnostics")]
|
|
use crate::layout_diagnostics::{self as diag, Counter};
|
|
use crate::{
|
|
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
|
|
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
|
|
render::{
|
|
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
|
PrimitiveKind, TexturePrimitive,
|
|
},
|
|
util::Vec2,
|
|
};
|
|
|
|
/// makes your surfaces look pretty
|
|
pub struct Painter<'a> {
|
|
pub(super) state: &'a mut UiRenderState,
|
|
pub(super) rsc: &'a mut dyn UiRsc,
|
|
|
|
/// This widget's box, in the coordinates of `move_idx`.
|
|
pub(super) region: UiRegion,
|
|
pub(super) mask: MaskIdx,
|
|
pub(super) textures: Vec<TextureHandle>,
|
|
pub(super) primitives: Vec<PrimitiveHandle>,
|
|
pub(super) children: Vec<WidgetId>,
|
|
/// The children asked about so far, so the first box each was asked
|
|
/// about is the one recorded as its offer.
|
|
pub(super) offered: Vec<WidgetId>,
|
|
/// The children whose size this widget read while drawing.
|
|
pub(super) size_deps: Vec<WidgetId>,
|
|
/// Offered pixel axes which can affect the size this draw reports.
|
|
pub(super) size_box_inputs: [bool; 2],
|
|
pub(super) size_output_inputs: [bool; 2],
|
|
/// The slot this widget's primitives are positioned through: its own if
|
|
/// its parent placed it, otherwise the nearest ancestor that has one.
|
|
pub(super) move_idx: MoveIdx,
|
|
pub layer: usize,
|
|
pub(super) depth: usize,
|
|
pub(super) id: WidgetId,
|
|
}
|
|
|
|
impl<'a> Painter<'a> {
|
|
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
|
let kind = self.rsc.ui_mut().primitives.kind::<P>();
|
|
self.write(kind, primitive, region);
|
|
}
|
|
|
|
/// Takes the kind, for a caller writing many of one primitive.
|
|
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::PrimitiveWrites);
|
|
let h = self.state.layers.write(
|
|
self.layer,
|
|
PrimitiveInst {
|
|
kind,
|
|
id: self.id,
|
|
primitive,
|
|
region,
|
|
mask_idx: self.mask,
|
|
move_idx: self.move_idx,
|
|
},
|
|
);
|
|
self.push_primitive(h);
|
|
}
|
|
|
|
fn push_primitive(&mut self, h: PrimitiveHandle) {
|
|
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);
|
|
}
|
|
self.primitives.push(h);
|
|
}
|
|
|
|
/// Writes a primitive to be rendered
|
|
pub fn primitive(&mut self, primitive: impl PrimitiveLike) {
|
|
let primitive = primitive.into_primitive(self);
|
|
self.primitive_at(primitive, self.region)
|
|
}
|
|
|
|
pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) {
|
|
let primitive = primitive.into_primitive(self);
|
|
self.primitive_at(primitive, region.within(&self.region));
|
|
}
|
|
|
|
pub fn set_mask(&mut self, region: UiRegion) {
|
|
assert!(self.mask == MaskIdx::NONE);
|
|
self.mask = self.rsc.ui_mut().masks.push(Mask {
|
|
region,
|
|
move_idx: self.move_idx,
|
|
});
|
|
}
|
|
|
|
/// Draws a widget within this widget's region.
|
|
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
|
self.widget_at(id, self.region, false)
|
|
}
|
|
|
|
/// Draws a widget somewhere within this one.
|
|
pub fn widget_within<'s, W: ?Sized>(
|
|
&'s mut self,
|
|
id: &'s StrongWidget<W>,
|
|
region: UiRegion,
|
|
) -> DrawResult<'s, 'a, W> {
|
|
let region = region.within(&self.region);
|
|
self.widget_at(id, region, false)
|
|
}
|
|
|
|
/// Draws a child this widget decides the box of, and may decide again
|
|
/// once it knows what the child came to. The child gets a slot of its
|
|
/// own, so placing it a second time writes one entry however much it
|
|
/// drew -- moved or resized alike, since everything under the slot is
|
|
/// held as a fraction of its box. A child drawn any other way has no slot
|
|
/// and can only be given a different box by drawing again.
|
|
pub fn place<'s, W: ?Sized>(
|
|
&'s mut self,
|
|
id: &'s StrongWidget<W>,
|
|
region: UiRegion,
|
|
) -> DrawResult<'s, 'a, W> {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::PlaceCalls);
|
|
let region = region.within(&self.region);
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::placed(id.id(), self.id, region);
|
|
self.widget_at(id, region, true)
|
|
}
|
|
|
|
fn widget_at<'s, W: ?Sized>(
|
|
&'s mut self,
|
|
id: &'s StrongWidget<W>,
|
|
region: UiRegion,
|
|
slotted: bool,
|
|
) -> 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,
|
|
Some(self.id),
|
|
self.depth + 1,
|
|
self.move_idx,
|
|
slotted,
|
|
self.mask,
|
|
None,
|
|
self.rsc,
|
|
);
|
|
self.offer(id.id(), region);
|
|
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<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
|
|
let hint = self
|
|
.rsc
|
|
.widgets()
|
|
.get_dyn(id.id())
|
|
.and_then(|widget| widget.size_hint(axis));
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::hint_read(id.id(), self.id, axis, hint);
|
|
match hint {
|
|
Some(hint) => {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::HintHits);
|
|
self.depend_on_hint(id);
|
|
Some(hint)
|
|
}
|
|
None => {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::HintMisses);
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A retained child length valid under the region it is about to be
|
|
/// offered. Unlike a hint, this is contextual: it is kept only when none
|
|
/// of the offered pixel axes which produced it changed.
|
|
pub fn known_len<W: ?Sized>(
|
|
&mut self,
|
|
child: &StrongWidget<W>,
|
|
axis: Axis,
|
|
region: UiRegion,
|
|
) -> Option<Len> {
|
|
let region = region.within(&self.region);
|
|
self.offer(child.id(), region);
|
|
if let Some(hint) = self.size_hint(child, axis) {
|
|
return Some(hint);
|
|
}
|
|
self.retained_size(child, region)
|
|
.map(|size| size.axis(axis))
|
|
}
|
|
|
|
/// `region` in this widget's own coordinates.
|
|
fn retained_size<W: ?Sized>(
|
|
&mut self,
|
|
child: &StrongWidget<W>,
|
|
region: UiRegion,
|
|
) -> Option<Size> {
|
|
let (size, box_inputs, output_inputs) =
|
|
self.state
|
|
.retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?;
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::RetainedSizeHits);
|
|
self.depend_on_size_inputs(child, box_inputs, output_inputs);
|
|
Some(size)
|
|
}
|
|
|
|
/// Records the box a child was first asked about in this draw. Any later
|
|
/// box this draw gives it was decided knowing its answer, so a size the
|
|
/// child measures there is not an answer to this widget's question.
|
|
fn offer(&mut self, child: WidgetId, region: UiRegion) {
|
|
if self.offered.contains(&child) {
|
|
return;
|
|
}
|
|
self.offered.push(child);
|
|
let px = self.state.px_of(self.move_idx, region);
|
|
if let Some(active) = self.state.active.get_mut(&child) {
|
|
active.offered_px = px;
|
|
}
|
|
}
|
|
|
|
/// Depends on a length the child gave without being drawn. A hint is
|
|
/// context-free, so this depends on the child but on no pixel axis.
|
|
fn depend_on_hint<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
|
self.depend_on_size_inputs(child, [false; 2], [false; 2]);
|
|
}
|
|
|
|
/// Depends on a size the child produced by drawing, which carries
|
|
/// whatever the child read to produce it.
|
|
fn depend_on_drawn_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
|
let (box_inputs, output_inputs) = self
|
|
.state
|
|
.active
|
|
.get(&child.id())
|
|
.map_or(([false; 2], [false; 2]), |active| {
|
|
(active.size_box_inputs, active.size_output_inputs)
|
|
});
|
|
self.depend_on_size_inputs(child, box_inputs, output_inputs);
|
|
}
|
|
|
|
fn depend_on_size_inputs<W: ?Sized>(
|
|
&mut self,
|
|
child: &StrongWidget<W>,
|
|
box_inputs: [bool; 2],
|
|
output_inputs: [bool; 2],
|
|
) {
|
|
if !self.size_deps.contains(&child.id()) {
|
|
self.size_deps.push(child.id());
|
|
}
|
|
for (own, child) in self.size_box_inputs.iter_mut().zip(box_inputs) {
|
|
*own |= child;
|
|
}
|
|
for (own, child) in self.size_output_inputs.iter_mut().zip(output_inputs) {
|
|
*own |= child;
|
|
}
|
|
}
|
|
|
|
pub fn render_text<'b>(
|
|
&mut self,
|
|
buffer: &'b mut TextBuffer,
|
|
attrs: &TextAttrs,
|
|
width: Option<f32>,
|
|
) -> &'b RenderedText {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::render_text(self.id, self.rsc.widgets().label(self.id), width);
|
|
let ui = self.rsc.ui_mut();
|
|
ui.text.render(buffer, attrs, width)
|
|
}
|
|
|
|
// TODO: merge the text methods into the primitive ones.
|
|
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
|
let kind = self.rsc.ui_mut().primitives.kind::<GlyphPrimitive>();
|
|
for glyph in text.glyphs.iter() {
|
|
let mut region = origin;
|
|
region.x.end = region.x.start;
|
|
region.y.end = region.y.start;
|
|
let mut region = region.offset(UiVec2::px(glyph.offset));
|
|
region.x.end = region.x.start + UiScalar::px(glyph.entry.width as f32);
|
|
region.y.end = region.y.start + UiScalar::px(glyph.entry.height as f32);
|
|
self.write(
|
|
kind,
|
|
GlyphPrimitive {
|
|
uv_min: glyph.entry.uv_min,
|
|
uv_max: glyph.entry.uv_max,
|
|
layer: glyph.entry.layer,
|
|
color: text.color,
|
|
flags: glyph.entry.flags(),
|
|
},
|
|
region,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// This widget's box, in the coordinates its own primitives are written
|
|
/// in -- so a region composed `within` it may be drawn directly.
|
|
pub fn region(&self) -> UiRegion {
|
|
self.region
|
|
}
|
|
|
|
/// 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.size_output_inputs = [true; 2];
|
|
self.state.output_size
|
|
}
|
|
|
|
/// One axis of the output in pixels. Prefer this to [`Self::output_size`]
|
|
/// when the other axis cannot affect the size this widget reports.
|
|
pub fn output_len(&mut self, axis: Axis) -> f32 {
|
|
self.size_output_inputs[axis as usize] = true;
|
|
self.state.output_size.axis(axis)
|
|
}
|
|
|
|
/// This widget's box in pixels. Resolved against the output's size and
|
|
/// the boxes it sits within, so a widget that reads it draws again when
|
|
/// the output changes.
|
|
pub fn px_size(&mut self) -> Vec2 {
|
|
self.size_box_inputs = [true; 2];
|
|
let region = self.state.moves.resolve(self.move_idx, self.region);
|
|
region.size().to_px(self.state.output_size)
|
|
}
|
|
|
|
/// One axis of this widget's box in pixels. Prefer this to
|
|
/// [`Self::px_size`] when the other axis cannot affect the reported size.
|
|
pub fn px_len(&mut self, axis: Axis) -> f32 {
|
|
self.size_box_inputs[axis as usize] = true;
|
|
self.px_len_for_draw(axis)
|
|
}
|
|
|
|
/// One axis of this widget's box in pixels, for a draw whose reported
|
|
/// size does not follow from it -- a clamp or a position. Nothing records
|
|
/// the read, so a size that does depend on it would go stale.
|
|
pub fn px_len_for_draw(&self, axis: Axis) -> f32 {
|
|
let region = self.state.moves.resolve(self.move_idx, self.region);
|
|
region
|
|
.size()
|
|
.axis(axis)
|
|
.to_px(self.state.output_size.axis(axis))
|
|
}
|
|
|
|
pub fn text_data(&mut self) -> &mut TextData {
|
|
&mut self.rsc.ui_mut().text
|
|
}
|
|
|
|
pub fn child_layer(&mut self) {
|
|
self.layer = self.state.layers.child(self.layer);
|
|
}
|
|
|
|
pub fn next_layer(&mut self) {
|
|
self.layer = self.state.layers.next(self.layer);
|
|
}
|
|
|
|
pub fn label(&self) -> &str {
|
|
&self.rsc.widgets().data(self.id).unwrap().label
|
|
}
|
|
|
|
pub fn id(&self) -> &WidgetId {
|
|
&self.id
|
|
}
|
|
}
|
|
|
|
/// 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<W>,
|
|
size: Size,
|
|
}
|
|
|
|
impl<W: ?Sized> DrawResult<'_, '_, W> {
|
|
pub fn size(self) -> Size {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
diag::bump(Counter::SizeReads);
|
|
diag::size_read(self.child.id(), self.painter.id, self.size);
|
|
}
|
|
self.painter.depend_on_drawn_size(self.child);
|
|
self.size
|
|
}
|
|
|
|
pub fn len(self, axis: Axis) -> Len {
|
|
self.size().axis(axis)
|
|
}
|
|
}
|
|
|
|
/// What `Painter::primitive` takes: a primitive, or something that yields one
|
|
/// and does whatever else drawing it needs.
|
|
pub trait PrimitiveLike {
|
|
type Primitive: Primitive;
|
|
fn into_primitive(self, painter: &mut Painter) -> Self::Primitive;
|
|
}
|
|
|
|
impl<P: Primitive> PrimitiveLike for P {
|
|
type Primitive = P;
|
|
fn into_primitive(self, _: &mut Painter) -> P {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl PrimitiveLike for &TextureHandle {
|
|
type Primitive = TexturePrimitive;
|
|
|
|
/// Retains a share of the handle, so the slot the primitive names cannot
|
|
/// be freed and reused while it is still drawn.
|
|
fn into_primitive(self, painter: &mut Painter) -> TexturePrimitive {
|
|
painter.textures.push(self.clone());
|
|
self.into()
|
|
}
|
|
}
|