Three of the eight rejections in `try_reuse` were invisible or half-visible to the diagnostics: a changed inherited mask counted nothing and traced nothing, an undrawn record traced without counting, and a changed region-node choice counted without tracing. The mask one is the rejection this branch's repair was about, so "why did that redraw?" was exactly the question the rig could not answer. Adding a counter meant editing a variant list and a name list at the same index, which renames every total after a slip and says nothing. The two lists are one declaration now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1201 lines
46 KiB
Rust
1201 lines
46 KiB
Rust
#[cfg(feature = "layout-diagnostics")]
|
|
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
|
use crate::{
|
|
ActiveData, Answer, Axis, Declared, DrawLayers, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx,
|
|
MoveIdx, Moves, Painter, PixelRegion, PlaceDesc, PxVec2, Rel, Size, StrongWidget, UiRegion,
|
|
UiRsc, UiSpan, UiVec2, Weight, WidgetId, Widgets,
|
|
util::{HashMap, Vec2},
|
|
};
|
|
|
|
/// Where a widget is drawn: what its parent decides about the draw besides
|
|
/// the boxes themselves.
|
|
#[derive(Clone, Copy)]
|
|
pub(super) struct DrawInfo {
|
|
pub layer: usize,
|
|
pub parent: Option<WidgetId>,
|
|
pub depth: usize,
|
|
pub parent_move: MoveIdx,
|
|
pub region_node: bool,
|
|
pub mask: MaskIdx,
|
|
/// What a fraction declared or reported under this widget is a fraction
|
|
/// of, as a length of the window.
|
|
pub rel_base: UiVec2,
|
|
/// The box the widget is asked in, in its parent region node's
|
|
/// coordinates.
|
|
pub region: UiRegion,
|
|
/// Where the widget is put, and where it was asked, as parts of the
|
|
/// parent's box. See [`PlaceDesc`]. The two are one ask's place until the
|
|
/// parent puts the answer somewhere else.
|
|
pub placed: PlaceDesc,
|
|
pub asked: PlaceDesc,
|
|
/// Whether the parent already asked about this widget in this draw.
|
|
pub re_asked: bool,
|
|
}
|
|
|
|
/// What one draw of a widget came to: the answer it gave, and the boxes and
|
|
/// windows the drawing that gave it holds for. The two are separate ranges --
|
|
/// a drawing can be invalid where its answer still stands.
|
|
pub(super) struct Drawn {
|
|
pub answer: Answer,
|
|
pub drawing_holds: LayoutHolds,
|
|
}
|
|
|
|
/// What a widget's children are placed in: its own box, the coordinates its
|
|
/// drawing is in, and what else one ask of a child is decided from.
|
|
pub(super) struct Placing {
|
|
pub id: WidgetId,
|
|
pub region: UiRegion,
|
|
pub rel_base: UiVec2,
|
|
pub depth: usize,
|
|
pub move_idx: MoveIdx,
|
|
pub mask: MaskIdx,
|
|
}
|
|
|
|
pub struct UiRenderState {
|
|
pub active: HashMap<WidgetId, ActiveData>,
|
|
pub layers: DrawLayers,
|
|
pub(super) output_size: PxVec2,
|
|
|
|
old_root: Option<WidgetId>,
|
|
/// Whether the output has changed since the last update. A frame is
|
|
/// owed for that whether or not anything has to be drawn again: every
|
|
/// fraction becomes pixels against the output, in the shader's uniform
|
|
/// as well as here.
|
|
resized: bool,
|
|
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
|
|
/// replaces that while its children go on pointing at the slot.
|
|
slots: HashMap<WidgetId, MoveIdx>,
|
|
/// Widgets waiting for an ancestor to draw them, so the walk down the
|
|
/// depths does not pick one up again at its own depth.
|
|
deferred: crate::util::HashSet<WidgetId>,
|
|
/// What the walk has left to settle, deepest last. Ordered rather than
|
|
/// searched for, so finding the next one is not a pass over the marks.
|
|
pending: std::collections::BTreeSet<(usize, WidgetId)>,
|
|
pub moves: Moves,
|
|
}
|
|
|
|
impl UiRenderState {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
active: Default::default(),
|
|
layers: Default::default(),
|
|
output_size: PxVec2::ZERO,
|
|
old_root: None,
|
|
slots: Default::default(),
|
|
deferred: Default::default(),
|
|
pending: Default::default(),
|
|
moves: Default::default(),
|
|
resized: false,
|
|
}
|
|
}
|
|
|
|
/// The window, in whatever the platform measures it in, onto the grid
|
|
/// everything below it is decided on. No move entry holds it: a chain
|
|
/// bottoms out in `MoveIdx::NONE`, which is the window, and the window's
|
|
/// size is applied where a fraction becomes pixels -- here in `to_px`,
|
|
/// and in the shader by its uniform. A resize therefore rewrites no
|
|
/// retained entry at all.
|
|
///
|
|
/// The root is the only widget a resize marks, and only where the new
|
|
/// output invalidates its answer or its drawing. The latter includes
|
|
/// children whose size it never read. Where either fails, the ordinary walk
|
|
/// draws the root, and each widget's own range decides how far down the
|
|
/// new length reaches.
|
|
pub fn resize(&mut self, size: impl Into<Vec2>, widgets: &mut Widgets) {
|
|
let size = PxVec2::from_f32(size.into());
|
|
if size == self.output_size {
|
|
return;
|
|
}
|
|
self.output_size = size;
|
|
self.resized = true;
|
|
let Some(root) = self.old_root else { return };
|
|
let stands = self.active.get(&root).is_some_and(|active| {
|
|
// Nothing above the root chose anything, so the box it was first
|
|
// asked about is the whole of its rel base. Both its answer and its
|
|
// drawing have to stand in the new window, since nothing above
|
|
// it will ask either again.
|
|
let answer = active
|
|
.answer
|
|
.is_some_and(|answer| answer.holds.contains(size, active.rel_base, active.region));
|
|
answer && active.holds.contains(size, active.rel_base, active.region)
|
|
});
|
|
if !stands {
|
|
widgets.needs_redraw.insert(root);
|
|
}
|
|
}
|
|
|
|
/// The root is asked about in the output. Its own rules narrow both its
|
|
/// rel base and box; nothing above it chose a different one.
|
|
fn root_info(&self, rel_base: UiVec2, region: UiRegion) -> DrawInfo {
|
|
DrawInfo {
|
|
layer: 0,
|
|
parent: None,
|
|
depth: 1,
|
|
parent_move: MoveIdx::NONE,
|
|
region_node: false,
|
|
mask: MaskIdx::NONE,
|
|
rel_base,
|
|
region,
|
|
placed: PlaceDesc::WHOLE,
|
|
asked: PlaceDesc::WHOLE,
|
|
re_asked: false,
|
|
}
|
|
}
|
|
|
|
pub fn output_size(&self) -> PxVec2 {
|
|
self.output_size
|
|
}
|
|
|
|
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::Updates);
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
let _update = diag::timer(TimerKind::Update);
|
|
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
|
// decide whether to panic or not
|
|
if !rsc.widgets().waiting.is_empty() {
|
|
let widgets = rsc.widgets();
|
|
let len = widgets.waiting.len();
|
|
let all: Vec<_> = widgets
|
|
.waiting
|
|
.iter()
|
|
.map(|&w| format!("'{}' ({w:?})", widgets.label(w)))
|
|
.collect();
|
|
panic!(
|
|
"{len} widget(s) were never upgraded\n\
|
|
this is likely a memory leak; consider upgrading to strong if you plan on using it later\n\
|
|
weak widgets: {all:#?}"
|
|
);
|
|
}
|
|
let root = root.into();
|
|
if self.root_changed(root) {
|
|
self.redraw_all(root, rsc);
|
|
self.old_root = root.map(|r| r.id());
|
|
}
|
|
self.resized = false;
|
|
if rsc.widgets().has_updates() {
|
|
self.redraw_updates(rsc);
|
|
}
|
|
self.free(rsc);
|
|
}
|
|
|
|
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
let _layout = diag::timer(TimerKind::FullLayout);
|
|
self.clear(rsc);
|
|
if let Some(id) = root {
|
|
let (rel_base, region) = Self::root_layout(id.id(), rsc.widgets());
|
|
let info = self.root_info(rel_base, region);
|
|
self.draw_inner(id.id(), info, None, rsc);
|
|
}
|
|
}
|
|
|
|
/// The root's rel base and box: the window, taken in by the root's own
|
|
/// rules. Nothing above it narrowed anything or chose where it goes, so
|
|
/// its declaration is the whole of what decides either.
|
|
fn root_layout(id: WidgetId, widgets: &Widgets) -> (UiVec2, UiRegion) {
|
|
PlaceDesc::WHOLE.rel_base_and_region(
|
|
UiRegion::FULL,
|
|
UiVec2::FULL_SIZE,
|
|
widgets.declared_lens(id),
|
|
widgets.alignment(id),
|
|
)
|
|
}
|
|
|
|
pub(super) fn draw_inner(
|
|
&mut self,
|
|
id: WidgetId,
|
|
info: DrawInfo,
|
|
mut old: Option<ActiveData>,
|
|
rsc: &mut dyn UiRsc,
|
|
) -> Drawn {
|
|
let old_parent = old
|
|
.as_ref()
|
|
.or_else(|| self.active.get(&id))
|
|
.and_then(|a| a.parent);
|
|
let region = info.region;
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
diag::bump(Counter::DrawRequests);
|
|
diag::draw_request(
|
|
id,
|
|
info.parent,
|
|
region,
|
|
region.to_px(self.output_size).size(),
|
|
info.region_node,
|
|
);
|
|
}
|
|
let align = rsc.widgets().alignment(id);
|
|
let declared = rsc.widgets().declared_lens(id);
|
|
// Nothing this widget measured can be dirty while it draws: layout is
|
|
// one bottom-up walk, so anything deeper has settled or deferred to
|
|
// its own parent, and a deferred one leaves that parent marked.
|
|
let stale = rsc.widgets().needs_redraw.contains(&id);
|
|
// The widget draws once, in the box it is asked in, and its answer
|
|
// is placed inside that box by re-expressing the drawing. The box the
|
|
// answer chose is never a question: nothing is drawn again in it, so
|
|
// an answer is kept only with the drawing that gave it, and both
|
|
// have to hold for the box asked about.
|
|
let reused = (!stale)
|
|
.then(|| self.retained_answer(id, region, info))
|
|
.flatten()
|
|
.and_then(|answer| {
|
|
let placed = info.placed.placement(region, answer.size, declared, align);
|
|
self.try_reuse(id, region, placed, info, rsc)
|
|
.then_some(answer)
|
|
});
|
|
let answer = reused.unwrap_or_else(|| {
|
|
if old.is_none() {
|
|
old = self.remove(id, false, rsc);
|
|
}
|
|
let answer = self.draw_at(id, region, info, old.take(), rsc);
|
|
// Where the drawing goes: the part its parent gave it, with the
|
|
// answer placed inside that part on any axis the parent left
|
|
// open.
|
|
let placed = info.placed.placement(region, answer.size, declared, align);
|
|
if placed != region {
|
|
self.relocate(id, placed, info, rsc);
|
|
}
|
|
answer
|
|
});
|
|
|
|
let drawing_holds = self.active[&id].holds;
|
|
let active = self.active.get_mut(&id).unwrap();
|
|
// Whoever asked owns how the boxes were reached: the rel base it stated,
|
|
// and what of its own box it asked in. A local redraw asks the same
|
|
// question again from these.
|
|
active.rel_base = info.rel_base;
|
|
active.re_asked = info.re_asked;
|
|
active.answer = Some(answer);
|
|
active.asked = info.asked;
|
|
active.region = region;
|
|
active.placed = info.placed;
|
|
active.own_align = align;
|
|
// The previous parent must stop owning the subtree before it can
|
|
// undraw it, whether changing hands reused the drawing or replaced it.
|
|
active.parent = info.parent;
|
|
if old_parent != info.parent
|
|
&& let Some(old_parent) = old_parent
|
|
&& let Some(old_parent) = self.active.get_mut(&old_parent)
|
|
{
|
|
old_parent.children.retain(|child| *child != id);
|
|
}
|
|
Drawn {
|
|
answer,
|
|
drawing_holds,
|
|
}
|
|
}
|
|
|
|
/// Calls a widget's `draw` and keeps what it drew in `region`.
|
|
fn draw_at(
|
|
&mut self,
|
|
id: WidgetId,
|
|
region: UiRegion,
|
|
info: DrawInfo,
|
|
old: Option<ActiveData>,
|
|
rsc: &mut dyn UiRsc,
|
|
) -> Answer {
|
|
let rel_base = info.rel_base;
|
|
let (move_idx, region, retired_move) = match info.region_node {
|
|
// A node entry is only a translation. Its local box keeps the
|
|
// same window-unit length as the box in its parent's node.
|
|
true => (
|
|
self.move_slot(id, info.parent_move, region.as_translation()),
|
|
region.at_origin(),
|
|
None,
|
|
),
|
|
// Keep the old entry alive until every descendant has migrated.
|
|
// Reusing its index sooner could make an old parent look current.
|
|
false => (info.parent_move, region, self.slots.remove(&id)),
|
|
};
|
|
let mask_slot = old
|
|
.as_ref()
|
|
.and_then(|old| old.mask_region.map(|_| old.mask));
|
|
let old_children = old.map_or_else(Vec::new, |old| old.children);
|
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
|
let window = self.output_size;
|
|
let mut painter = Painter {
|
|
state: self,
|
|
rel_base,
|
|
region,
|
|
window,
|
|
mask: info.mask,
|
|
layer: info.layer,
|
|
own_layer: info.layer,
|
|
id,
|
|
textures: Vec::new(),
|
|
primitives: Vec::new(),
|
|
mask_region: None,
|
|
mask_slot,
|
|
children: Vec::new(),
|
|
size_deps: Vec::new(),
|
|
own: LayoutHolds::ANY,
|
|
under: Vec::new(),
|
|
answer_under: LayoutHolds::ANY,
|
|
depth: info.depth,
|
|
move_idx,
|
|
rsc,
|
|
};
|
|
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
diag::bump(Counter::WidgetDraws);
|
|
diag::draw_widget(id, painter.rsc.widgets().label(id));
|
|
}
|
|
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
|
let size = widget.draw(&mut painter);
|
|
drop(widget);
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::size_reported(id, size);
|
|
|
|
let Painter {
|
|
state: _,
|
|
rsc: _,
|
|
rel_base: _,
|
|
region: _,
|
|
window: _,
|
|
mask,
|
|
textures,
|
|
primitives,
|
|
mask_region,
|
|
mask_slot,
|
|
own,
|
|
answer_under,
|
|
children,
|
|
size_deps,
|
|
under,
|
|
move_idx,
|
|
layer,
|
|
own_layer: _,
|
|
depth: _,
|
|
id,
|
|
} = painter;
|
|
|
|
debug_assert!(
|
|
Self::hints_agree(id, size, rsc),
|
|
"'{}' ({id:?}) drew a size its size_hint disagrees with",
|
|
rsc.widgets().label(id)
|
|
);
|
|
// A rule wins on the axis it names, and the draw answers the rest.
|
|
// Applied here so it is one place rather than every widget that could
|
|
// carry one, and so the widget under a rule never learns of it. The
|
|
// rel base is the answer where the rule gave a length outright: it was
|
|
// resolved into the rel base when the child was asked, and resolving it
|
|
// again here would take the fraction of a fraction.
|
|
let rules = rsc.widgets().size_rules(id);
|
|
let ruled = |axis: Axis, reported: LayoutLen| match rules[axis].exact() {
|
|
None => reported,
|
|
Some(len) if len.leftover == Weight::ZERO => LayoutLen {
|
|
rel: info.rel_base[axis].rel,
|
|
px: info.rel_base[axis].px,
|
|
leftover: Weight::ZERO,
|
|
},
|
|
Some(len) => len.within_len(info.rel_base[axis]),
|
|
};
|
|
let size = Size {
|
|
x: ruled(Axis::X, size.x),
|
|
y: ruled(Axis::Y, size.y),
|
|
};
|
|
// A widget that clipped its contents to its box drew nothing outside
|
|
// it, so reporting more than the box asks to be placed at a length it
|
|
// does not occupy -- and its parent would place the part it cut off.
|
|
// Overflowing is otherwise ordinary: a text too tall for the box it
|
|
// was offered reports the height it needs.
|
|
debug_assert!(
|
|
mask == info.mask
|
|
|| Axis::BOTH.into_iter().all(|axis| size.within_box(
|
|
region,
|
|
self.output_size,
|
|
axis
|
|
)),
|
|
"'{}' ({id:?}) clips to {} and reports {size}",
|
|
rsc.widgets().label(id),
|
|
region.to_px(window),
|
|
);
|
|
for c in &old_children {
|
|
if !children.contains(c) {
|
|
self.undraw_rec(*c, rsc);
|
|
}
|
|
}
|
|
if let Some(idx) = mask_slot {
|
|
rsc.ui_mut().masks.remove(idx);
|
|
}
|
|
if let Some(idx) = retired_move {
|
|
self.moves.remove(idx);
|
|
}
|
|
// A rule that is a fraction of the rel base is answered with the
|
|
// rel base's own length, so the answer is that rel base's and not just
|
|
// that many pixels of this window -- the same pin a widget that read
|
|
// its rel base took for its drawing.
|
|
let mut own_holds = own;
|
|
for axis in Axis::BOTH {
|
|
let fraction = rules[axis].exact().is_some_and(|len| len.rel != Rel::ZERO);
|
|
if fraction {
|
|
own_holds[axis].rel_base = Some(info.rel_base[axis]);
|
|
}
|
|
}
|
|
let answer_holds = own_holds.and(answer_under);
|
|
let holds = under
|
|
.into_iter()
|
|
.fold(answer_holds, |holds, (_, child)| holds.and(child));
|
|
debug_assert!(
|
|
holds.contains(self.output_size, info.rel_base, region),
|
|
"'{}' ({id:?}) drew in {}, outside the ranges it reported: {holds:?}",
|
|
rsc.widgets().label(id),
|
|
region.to_px(window),
|
|
);
|
|
// What it asked about and did not draw is still something it asked,
|
|
// and a change there has to reach it. Asking answered whatever mark
|
|
// it had: a hint is read live, and a drawing is not kept past one.
|
|
for &dep in &size_deps {
|
|
if !children.contains(&dep) {
|
|
self.asked(
|
|
dep,
|
|
DrawInfo {
|
|
layer,
|
|
parent: Some(id),
|
|
depth: info.depth + 1,
|
|
parent_move: move_idx,
|
|
region_node: false,
|
|
mask,
|
|
rel_base: UiVec2::FULL_SIZE,
|
|
region: UiRegion::FULL,
|
|
placed: PlaceDesc::WHOLE,
|
|
asked: PlaceDesc::WHOLE,
|
|
re_asked: false,
|
|
},
|
|
rsc,
|
|
);
|
|
rsc.widgets_mut().needs_redraw.remove(&dep);
|
|
}
|
|
}
|
|
|
|
let active = ActiveData {
|
|
id,
|
|
placement: region,
|
|
rel_base: info.rel_base,
|
|
placed: info.placed,
|
|
asked: info.asked,
|
|
region,
|
|
// Whoever asked writes the answer.
|
|
answer: None,
|
|
re_asked: info.re_asked,
|
|
size,
|
|
holds,
|
|
drawn: true,
|
|
parent: info.parent,
|
|
depth: info.depth,
|
|
textures,
|
|
primitives,
|
|
mask_region,
|
|
children,
|
|
declared: rsc.widgets().declared_lens(id),
|
|
own_align: rsc.widgets().alignment(id),
|
|
move_idx,
|
|
parent_move: info.parent_move,
|
|
mask,
|
|
parent_mask: info.mask,
|
|
layer: info.layer,
|
|
};
|
|
rsc.on_draw(&active);
|
|
self.active.insert(id, active);
|
|
Answer {
|
|
size,
|
|
holds: answer_holds,
|
|
}
|
|
}
|
|
|
|
/// Keeps a region node's entry across redraws because descendants retain
|
|
/// its index.
|
|
fn move_slot(&mut self, id: WidgetId, parent: MoveIdx, region: UiRegion) -> MoveIdx {
|
|
if let Some(&idx) = self.slots.get(&id) {
|
|
self.moves.set_parent(idx, parent);
|
|
self.moves.set(idx, region);
|
|
return idx;
|
|
}
|
|
let idx = self.moves.push(parent, region);
|
|
self.slots.insert(id, idx);
|
|
idx
|
|
}
|
|
|
|
/// Removes a region node only after its descendants stop naming it.
|
|
fn drop_slot(&mut self, id: WidgetId) {
|
|
if let Some(idx) = self.slots.remove(&id) {
|
|
self.moves.remove(idx);
|
|
}
|
|
}
|
|
|
|
/// The answer to an ask can be retained independently of where its
|
|
/// drawing ended up. Alignment is exactly that case: the first box is the
|
|
/// question and the smaller placed box holds the drawing. Whether the
|
|
/// answer is stale at all is its caller's question, asked once there.
|
|
fn retained_answer(&self, id: WidgetId, region: UiRegion, info: DrawInfo) -> Option<Answer> {
|
|
let active = self.active.get(&id)?;
|
|
if !active.drawn
|
|
|| active.is_region_node() != info.region_node
|
|
|| active.parent_move != info.parent_move
|
|
{
|
|
return None;
|
|
}
|
|
let answer = active.answer?;
|
|
answer
|
|
.holds
|
|
.contains(self.output_size, info.rel_base, region)
|
|
.then_some(answer)
|
|
}
|
|
|
|
/// Keeps the retained drawing if its contract holds for `part`, the box
|
|
/// asked about, and puts it at `placed`, where the answer places it.
|
|
fn try_reuse(
|
|
&mut self,
|
|
id: WidgetId,
|
|
region: UiRegion,
|
|
placed: UiRegion,
|
|
info: DrawInfo,
|
|
rsc: &mut dyn UiRsc,
|
|
) -> bool {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::ReuseAttempts);
|
|
if rsc.widgets().needs_redraw.contains(&id) {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
diag::bump(Counter::ReuseDirty);
|
|
diag::reuse(id, ReuseOutcome::Dirty);
|
|
}
|
|
return false;
|
|
}
|
|
let Some(active) = self.active.get(&id) else {
|
|
return false;
|
|
};
|
|
if !active.drawn {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
diag::bump(Counter::ReuseUndrawn);
|
|
diag::reuse(id, ReuseOutcome::Undrawn);
|
|
}
|
|
return false;
|
|
}
|
|
if active.is_region_node() != info.region_node {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
diag::bump(Counter::ReuseWrongNode);
|
|
diag::reuse(id, ReuseOutcome::WrongNode);
|
|
}
|
|
return false;
|
|
}
|
|
// Drawn on another layer: the drawing sits in that layer's list and
|
|
// paints at its moment, which no amount of geometry says. A container
|
|
// that measures a child by drawing it and then draws it again where
|
|
// it belongs -- `Stack`, over its background -- asks the second time
|
|
// on a layer the first answer is not good for.
|
|
if active.layer != info.layer {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
diag::bump(Counter::ReuseWrongLayer);
|
|
diag::reuse(id, ReuseOutcome::WrongLayer);
|
|
}
|
|
return false;
|
|
}
|
|
// Its primitives name the mask it inherited, and a masking parent
|
|
// that redrew pushed another: keeping them would clip them by one
|
|
// nothing updates again.
|
|
if active.parent_mask != info.mask {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
diag::bump(Counter::ReuseWrongMask);
|
|
diag::reuse(id, ReuseOutcome::WrongMask);
|
|
}
|
|
return false;
|
|
}
|
|
// Drawn somewhere else in the tree: its box is in coordinates it no
|
|
// longer sits in, and its slot names the wrong parent.
|
|
if active.parent_move != info.parent_move {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
diag::bump(Counter::ReuseWrongParent);
|
|
diag::reuse(id, ReuseOutcome::WrongParent);
|
|
}
|
|
return false;
|
|
}
|
|
// In pixels, because the box is a fraction of the window and that
|
|
// may be what changed -- an unchanged fraction of a window half the
|
|
// size is half the widget.
|
|
if !active
|
|
.holds
|
|
.contains(self.output_size, info.rel_base, region)
|
|
{
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::outside(id, active.holds, region, info.rel_base, self.output_size);
|
|
return false;
|
|
}
|
|
self.relocate(id, placed, info, rsc);
|
|
true
|
|
}
|
|
|
|
/// Puts a retained drawing where its parent now has it, without drawing:
|
|
/// a widget with a node of its own writes that node's translation, and
|
|
/// one without re-expresses its own drawing and everything inside it.
|
|
fn relocate(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
|
|
let active = &self.active[&id];
|
|
debug_assert!(
|
|
!rsc.widgets().needs_redraw.contains(&id),
|
|
"'{}' ({id:?}) placed while marked to draw",
|
|
rsc.widgets().label(id)
|
|
);
|
|
let is_region_node = active.is_region_node();
|
|
let local = match is_region_node {
|
|
true => placed.at_origin(),
|
|
false => placed,
|
|
};
|
|
let moved = active.placement != local;
|
|
let slot = active.move_idx;
|
|
if is_region_node {
|
|
self.moves.set(slot, placed.as_translation());
|
|
}
|
|
if moved {
|
|
self.reposition(id, local, info, rsc);
|
|
}
|
|
self.redepth(id, info.depth);
|
|
let active = self.active.get_mut(&id).unwrap();
|
|
active.rel_base = info.rel_base;
|
|
active.placed = info.placed;
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
let (counter, outcome) = match (moved, is_region_node) {
|
|
(true, true) => (Counter::ReuseMoved, ReuseOutcome::Moved),
|
|
(true, false) => (Counter::ReuseRemapped, ReuseOutcome::Remapped),
|
|
(false, _) => (Counter::ReuseExact, ReuseOutcome::Exact),
|
|
};
|
|
diag::bump(counter);
|
|
diag::reuse(id, outcome);
|
|
}
|
|
}
|
|
|
|
/// Places one child of `at.id` where that widget's own box now has it.
|
|
fn place_child(&mut self, child: WidgetId, at: &Placing, rsc: &mut dyn UiRsc) {
|
|
let place = self.active[&child].placed;
|
|
self.place_in(child, at, place, rsc);
|
|
}
|
|
|
|
/// Puts a child of `at.id` in `place` of that widget's box: its answer
|
|
/// placed inside that part where the place leaves the axis open, the
|
|
/// drawing re-expressed there.
|
|
pub(super) fn place_in(
|
|
&mut self,
|
|
child: WidgetId,
|
|
at: &Placing,
|
|
place: PlaceDesc,
|
|
rsc: &mut dyn UiRsc,
|
|
) {
|
|
let active = &self.active[&child];
|
|
let (rel_base, region) = Self::ask_again(active, at, place);
|
|
let placed = place.placement(
|
|
region,
|
|
active.measured().unwrap_or(active.size),
|
|
active.declared,
|
|
active.own_align,
|
|
);
|
|
let info = DrawInfo {
|
|
layer: active.layer,
|
|
parent: Some(at.id),
|
|
depth: at.depth + 1,
|
|
parent_move: at.move_idx,
|
|
region_node: active.is_region_node(),
|
|
mask: at.mask,
|
|
rel_base,
|
|
region,
|
|
placed: place,
|
|
asked: active.asked,
|
|
re_asked: active.re_asked,
|
|
};
|
|
self.relocate(child, placed, info, rsc);
|
|
}
|
|
|
|
/// The rel base and the box a widget already drawn is given at `place` of
|
|
/// the box its parent is being taken as. What narrowed its rel base and what
|
|
/// it declared are its own record's, so both are resolved against that
|
|
/// parent's rel base again exactly as the first ask resolved them.
|
|
fn ask_again(active: &ActiveData, at: &Placing, place: PlaceDesc) -> (UiVec2, UiRegion) {
|
|
place.rel_base_and_region(at.region, at.rel_base, active.declared, active.own_align)
|
|
}
|
|
|
|
/// Re-places everything inside a widget whose own box moved. Every child
|
|
/// is placed as a part of that box, so each one's new box is its retained
|
|
/// part re-added to the new start -- and a child whose own box then did
|
|
/// not change is not touched at all.
|
|
fn reposition(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
|
|
let active = self.active.get_mut(&id).unwrap();
|
|
active.placement = placed;
|
|
for primitive in &active.primitives {
|
|
let handle = &primitive.handle;
|
|
*self.layers[handle.layer].region_mut(handle) = primitive.region.within(&placed);
|
|
}
|
|
if let Some(mask_region) = active.mask_region {
|
|
rsc.ui_mut().masks.get_mut(active.mask).region = mask_region.within(&placed);
|
|
}
|
|
let at = Placing {
|
|
id,
|
|
region: placed,
|
|
rel_base: info.rel_base,
|
|
depth: info.depth,
|
|
move_idx: active.move_idx,
|
|
mask: active.mask,
|
|
};
|
|
// Taken out and put back so that placing a child can borrow the state
|
|
// it needs; nothing on that path reads this widget's own child list.
|
|
let children = std::mem::take(&mut self.active.get_mut(&id).unwrap().children);
|
|
for &child in &children {
|
|
self.place_child(child, &at, rsc);
|
|
}
|
|
self.active.get_mut(&id).unwrap().children = children;
|
|
}
|
|
|
|
/// A reused subtree keeps its shape, so each widget in it keeps its depth
|
|
/// under the top -- and where the top's own depth did not change, none of
|
|
/// them did, which is what makes this free in the ordinary case.
|
|
fn redepth(&mut self, id: WidgetId, depth: usize) {
|
|
let Some(active) = self.active.get_mut(&id) else {
|
|
return;
|
|
};
|
|
if active.depth == depth {
|
|
return;
|
|
}
|
|
active.depth = depth;
|
|
// Taken out and put back so the walk can borrow the state it needs;
|
|
// it only ever goes further down, so it reads no list but its own.
|
|
let children = std::mem::take(&mut active.children);
|
|
for &child in &children {
|
|
self.redepth(child, depth + 1);
|
|
}
|
|
self.active.get_mut(&id).unwrap().children = children;
|
|
}
|
|
|
|
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
|
|
let Some(widget) = rsc.widgets().get_dyn(id) else {
|
|
return true;
|
|
};
|
|
Axis::BOTH
|
|
.into_iter()
|
|
.all(|axis| widget.size_hint(axis).is_none_or(|hint| hint == size[axis]))
|
|
}
|
|
|
|
/// Takes a widget's record out and frees what it drew.
|
|
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
|
|
let mut active = self.active.remove(&id);
|
|
if let Some(active) = &mut active {
|
|
for primitive in &active.primitives {
|
|
let mask = self.layers.free(&primitive.handle);
|
|
if mask != MaskIdx::NONE {
|
|
rsc.ui_mut().masks.remove(mask);
|
|
}
|
|
}
|
|
if undraw && active.mask_region.take().is_some() {
|
|
rsc.ui_mut().masks.remove(active.mask);
|
|
}
|
|
active.primitives.clear();
|
|
active.textures.clear();
|
|
rsc.ui_mut().textures.free();
|
|
if undraw && active.drawn {
|
|
rsc.on_undraw(active);
|
|
}
|
|
}
|
|
active
|
|
}
|
|
|
|
/// Stops drawing a widget and everything under it, keeping the record
|
|
/// of who asked about it so that a change to it still reaches them.
|
|
pub(super) fn undraw_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
|
let Some(mut active) = self.remove(id, true, rsc) else {
|
|
return;
|
|
};
|
|
for c in std::mem::take(&mut active.children) {
|
|
self.undraw_rec(c, rsc);
|
|
}
|
|
// After the descendants, whose slots name this one as their parent.
|
|
self.drop_slot(id);
|
|
active.drawn = false;
|
|
self.active.insert(id, active);
|
|
}
|
|
|
|
/// Records that `info.parent` asked about a widget it does not draw.
|
|
fn asked(&mut self, id: WidgetId, info: DrawInfo, rsc: &mut dyn UiRsc) {
|
|
if let Some(active) = self.active.get_mut(&id) {
|
|
debug_assert!(!active.drawn, "asked about a widget it drew");
|
|
active.parent = info.parent;
|
|
active.depth = info.depth;
|
|
return;
|
|
}
|
|
// Never drawn, so there is no drawing to hold anything; what its
|
|
// parent read was its hint.
|
|
let widget = rsc.widgets().get_dyn(id);
|
|
let size = Size {
|
|
x: widget
|
|
.and_then(|w| w.size_hint(Axis::X))
|
|
.unwrap_or(LayoutLen::ZERO),
|
|
y: widget
|
|
.and_then(|w| w.size_hint(Axis::Y))
|
|
.unwrap_or(LayoutLen::ZERO),
|
|
};
|
|
self.active.insert(
|
|
id,
|
|
ActiveData {
|
|
id,
|
|
placement: UiRegion::FULL,
|
|
rel_base: UiVec2::FULL_SIZE,
|
|
placed: PlaceDesc::WHOLE,
|
|
asked: PlaceDesc::WHOLE,
|
|
region: UiRegion::FULL,
|
|
answer: None,
|
|
re_asked: false,
|
|
size,
|
|
holds: LayoutHolds::ANY,
|
|
drawn: false,
|
|
parent: info.parent,
|
|
depth: info.depth,
|
|
textures: Vec::new(),
|
|
primitives: Vec::new(),
|
|
mask_region: None,
|
|
children: Vec::new(),
|
|
move_idx: info.parent_move,
|
|
declared: Declared::NONE,
|
|
own_align: rsc.widgets().alignment(id),
|
|
parent_move: info.parent_move,
|
|
mask: info.mask,
|
|
parent_mask: info.mask,
|
|
layer: info.layer,
|
|
},
|
|
);
|
|
}
|
|
|
|
fn clear(&mut self, rsc: &mut dyn UiRsc) {
|
|
for (_, active) in self.active.drain() {
|
|
if active.drawn {
|
|
rsc.on_undraw(&active);
|
|
}
|
|
}
|
|
self.slots.clear();
|
|
self.moves.clear();
|
|
self.layers.clear();
|
|
rsc.ui_mut().masks = Default::default();
|
|
rsc.widgets_mut().needs_redraw.clear();
|
|
self.free(rsc);
|
|
}
|
|
|
|
/// Frees the widgets nothing holds any more, and the records kept of
|
|
/// them: an id is handed on to the next widget made.
|
|
fn free(&mut self, rsc: &mut dyn UiRsc) {
|
|
while let Some(id) = rsc.widgets_mut().free_next() {
|
|
rsc.on_remove(id);
|
|
self.remove(id, true, rsc);
|
|
self.drop_slot(id);
|
|
}
|
|
rsc.ui_mut().textures.free();
|
|
}
|
|
|
|
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
let _layout = diag::timer(TimerKind::IncrementalLayout);
|
|
// Deepest first, and strictly: a widget that cannot settle where it
|
|
// is defers to its parent rather than drawing the parent from
|
|
// inside itself. It marks the parent, stays marked, and waits here
|
|
// until the walk reaches its parent's depth.
|
|
//
|
|
// What that buys is that nothing shallower is ever drawn while
|
|
// anything deeper is still dirty. A parent drawing can therefore
|
|
// trust every answer it reads without descending to check whether
|
|
// something below is about to change it -- which is the whole class
|
|
// of defect where a widget settles inside its parent's draw, clears
|
|
// its mark there, and tells nobody its answer moved.
|
|
// The queue is that set, ordered: a mark made while the walk runs
|
|
// queues itself through `mark`. What ends the walk is still the set
|
|
// being spent, not the queue, so a mark that reached it another way
|
|
// cannot be left for the next frame.
|
|
loop {
|
|
for &id in rsc.widgets().needs_redraw.iter() {
|
|
if !self.deferred.contains(&id) {
|
|
let depth = self.depth(id);
|
|
self.pending.insert((depth, id));
|
|
}
|
|
}
|
|
if self.pending.is_empty() {
|
|
break;
|
|
}
|
|
while let Some((depth, id)) = self.pending.pop_last() {
|
|
// Settled inside an ancestor's draw, or deferred to one,
|
|
// since the mark that queued it.
|
|
if self.deferred.contains(&id) || !rsc.widgets().needs_redraw.contains(&id) {
|
|
continue;
|
|
}
|
|
// A subtree that changed hands takes its descendants' depths
|
|
// with it, so an entry queued before that move names the
|
|
// depth it had under the parent it left.
|
|
let now = self.depth(id);
|
|
if now != depth {
|
|
self.pending.insert((now, id));
|
|
continue;
|
|
}
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::QueuePops);
|
|
if !self.redraw(id, rsc) {
|
|
self.deferred.insert(id);
|
|
}
|
|
}
|
|
}
|
|
self.deferred.clear();
|
|
}
|
|
|
|
/// Marks a widget for the walk to settle, and queues it at its depth.
|
|
fn mark(&mut self, id: WidgetId, widgets: &mut Widgets) {
|
|
if widgets.needs_redraw.insert(id) && !self.deferred.contains(&id) {
|
|
let depth = self.depth(id);
|
|
self.pending.insert((depth, id));
|
|
}
|
|
}
|
|
|
|
fn depth(&self, id: WidgetId) -> usize {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::DepthReads);
|
|
let depth = match self.active.get(&id) {
|
|
Some(active) if active.drawn => active.depth,
|
|
// Nothing keeps an undrawn widget's current, and it only has to
|
|
// reach whoever asked.
|
|
Some(_) => return self.walked_depth(id),
|
|
None => 1,
|
|
};
|
|
debug_assert_eq!(
|
|
depth,
|
|
self.walked_depth(id),
|
|
"a widget's kept depth is not the one its ancestry says"
|
|
);
|
|
depth
|
|
}
|
|
|
|
/// What the kept depth is checked against, and the only thing that reads
|
|
/// the ancestry to find one.
|
|
fn walked_depth(&self, id: WidgetId) -> usize {
|
|
let mut depth = 0;
|
|
let mut at = Some(id);
|
|
while let Some(id) = at {
|
|
at = self.active.get(&id).and_then(|active| active.parent);
|
|
depth += 1;
|
|
}
|
|
depth
|
|
}
|
|
|
|
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
|
|
root.into().map(|r| r.id()) != self.old_root
|
|
}
|
|
|
|
pub fn needs_redraw<'a>(
|
|
&self,
|
|
root: impl Into<Option<&'a StrongWidget>>,
|
|
widgets: &Widgets,
|
|
) -> bool {
|
|
self.root_changed(root) || self.resized || widgets.has_updates()
|
|
}
|
|
|
|
pub fn active_widgets(&self) -> usize {
|
|
self.active.values().filter(|active| active.drawn).count()
|
|
}
|
|
|
|
pub fn debug(&self, widgets: &Widgets, label: &str) -> impl Iterator<Item = &ActiveData> {
|
|
self.active.iter().filter_map(move |(&id, inst)| {
|
|
let l = widgets.label(id);
|
|
if l == label { Some(inst) } else { None }
|
|
})
|
|
}
|
|
|
|
pub fn debug_layers(&self) {
|
|
for ((idx, depth), draws) in self.layers.iter_depth() {
|
|
let indent = " ".repeat(depth * 2);
|
|
let counts: Vec<String> = draws
|
|
.primitives()
|
|
.iter()
|
|
.map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string())
|
|
.collect();
|
|
println!("{indent}{idx}: [{}]", counts.join(", "));
|
|
}
|
|
}
|
|
|
|
/// Where a widget is on screen: its box composed through the boxes it
|
|
/// sits within, the same walk the vertex shader does. `None` for one that
|
|
/// is not drawn.
|
|
///
|
|
/// This is for asking where a drawing landed: hit testing, and a test
|
|
/// reading a box back. Layout decides on the lengths threaded down the
|
|
/// draw instead, and a position is not one of its inputs.
|
|
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
|
let active = self.active.get(&id.id())?;
|
|
active.drawn.then(|| {
|
|
self.moves
|
|
.resolve(active.move_idx, active.placement)
|
|
.to_px(self.output_size)
|
|
})
|
|
}
|
|
|
|
/// Settles a dirty widget: asks it again where its parent asked, and
|
|
/// tells the parent if the answer changed. `false` where the question is
|
|
/// its parent's rather than its own, which leaves it marked for the
|
|
/// parent to draw when the walk reaches that depth.
|
|
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> bool {
|
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
|
let Some(active) = self.active.get(&id) else {
|
|
return true;
|
|
};
|
|
// Its parent resolved its declared lengths into its box and decided
|
|
// whether to draw it at all, so a change to either is the parent's
|
|
// to draw -- with the mark left on, so the parent draws it rather
|
|
// than keeping it. So is a widget the parent asked twice: its
|
|
// layout rests on an answer this widget cannot give again alone.
|
|
let declared_changed = rsc.widgets().declared_lens(id) != active.declared;
|
|
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
|
|
if let Some(parent) = active.parent
|
|
&& (declared_changed
|
|
|| alignment_changed
|
|
|| active.re_asked
|
|
|| !active.drawn
|
|
|| active.answer.is_none())
|
|
{
|
|
// Both stay marked: the parent because it has this to draw, and
|
|
// this because the parent must draw it rather than keep what it
|
|
// has. The mark comes off in `draw_at`, where the parent draws.
|
|
self.mark(id, rsc.widgets_mut());
|
|
self.mark(parent, rsc.widgets_mut());
|
|
return false;
|
|
}
|
|
if !active.drawn {
|
|
return true;
|
|
}
|
|
// Nothing above the root resolved its rules or its alignment, so its
|
|
// box is its own to work out again against the output. Every other
|
|
// widget was given one.
|
|
let Some(parent) = active.parent else {
|
|
let (rel_base, region) = Self::root_layout(id, rsc.widgets());
|
|
let info = DrawInfo {
|
|
mask: active.parent_mask,
|
|
..self.root_info(rel_base, region)
|
|
};
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::LocalRedraws);
|
|
let old = self.remove(id, false, rsc);
|
|
self.draw_inner(id, info, old, rsc);
|
|
return true;
|
|
};
|
|
let (was_answer, was_holds, was_place) = (active.answer, active.holds, active.placed);
|
|
// The question its parent asked, asked again: the same place of the
|
|
// box the parent was asked in, which is the box the parent's own
|
|
// draw ran in and what its children's parts are of. Where the
|
|
// parent's answer put its own drawing is not a question anybody
|
|
// asked, and nothing is asked in it here either.
|
|
let parent_at = self.placing_of(parent, self.active[&parent].region);
|
|
let (rel_base, region) = Self::ask_again(active, &parent_at, active.asked);
|
|
let info = DrawInfo {
|
|
layer: active.layer,
|
|
parent: active.parent,
|
|
depth: active.depth,
|
|
parent_move: active.parent_move,
|
|
region_node: rsc.widgets().is_region_node(id),
|
|
mask: active.parent_mask,
|
|
rel_base,
|
|
region,
|
|
placed: active.asked,
|
|
asked: active.asked,
|
|
re_asked: false,
|
|
};
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::LocalRedraws);
|
|
|
|
let old = self.remove(id, false, rsc);
|
|
let drawn = self.draw_inner(id, info, old, rsc);
|
|
let active = self.active.get_mut(&id).unwrap();
|
|
// A wider contract does not invalidate the guarantee the parent kept.
|
|
// Retain that guarantee so widening and narrowing back do not churn it.
|
|
if let Some(was) = was_answer
|
|
&& drawn.answer.size == was.size
|
|
&& drawn.answer.holds.covers(was.holds)
|
|
{
|
|
active.answer = was_answer;
|
|
}
|
|
// Against the box it was asked in, which is what both contracts are
|
|
// about. Where the answer put the drawing is shorter than that
|
|
// wherever the widget reported less than it was offered.
|
|
if active.holds.covers(was_holds)
|
|
&& was_holds.contains(self.output_size, active.rel_base, active.region)
|
|
{
|
|
active.holds = was_holds;
|
|
}
|
|
if active.answer != was_answer || active.holds != was_holds {
|
|
// The parent retains both the answer and the drawing's validity;
|
|
// even an unchanged size can narrow the range safe for a resize.
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
diag::bump(Counter::SizeChanges);
|
|
diag::bump(Counter::ReaderEdges);
|
|
}
|
|
self.mark(parent, rsc.widgets_mut());
|
|
} else {
|
|
// The answer stands, so where the parent put it stands: the
|
|
// fresh drawing goes back there -- the same place, of the box
|
|
// the parent's answer chose rather than the one it was asked in.
|
|
let at = self.placing_of(parent, self.active[&parent].placement);
|
|
self.place_in(id, &at, was_place, rsc);
|
|
}
|
|
true
|
|
}
|
|
|
|
/// A drawn widget as the thing its children are placed within, with
|
|
/// `region` as the box their parts are of: the box it was asked in for
|
|
/// asking one of them again, the box its answer chose for placing one.
|
|
fn placing_of(&self, id: WidgetId, region: UiRegion) -> Placing {
|
|
let active = &self.active[&id];
|
|
Placing {
|
|
id,
|
|
region,
|
|
rel_base: active.rel_base,
|
|
depth: active.depth,
|
|
move_idx: active.move_idx,
|
|
mask: active.mask,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Size {
|
|
/// Whether what a widget reports along `axis` is inside the box it drew
|
|
/// in. Both are lengths of the window, so the comparison is in its
|
|
/// pixels. A share is a length only to whoever divides one, so it is not
|
|
/// a claim about this box and cannot exceed it.
|
|
fn within_box(self, region: UiRegion, window: PxVec2, axis: Axis) -> bool {
|
|
let len = self[axis];
|
|
let window = window[axis];
|
|
len.leftover != Weight::ZERO
|
|
|| len.without_leftover().to_px(window) <= region[axis].len().to_px(window)
|
|
}
|
|
}
|
|
|
|
impl UiRegion {
|
|
/// A box in a fresh region node keeps its window-unit length and starts
|
|
/// at that node's origin.
|
|
fn at_origin(self) -> UiRegion {
|
|
let size = self.size();
|
|
UiRegion::new(
|
|
UiSpan::new(Len::ZERO, size.x),
|
|
UiSpan::new(Len::ZERO, size.y),
|
|
)
|
|
}
|
|
|
|
/// A region node changes only the origin. A full relative span anchored at
|
|
/// the box start composes as that translation in both the CPU and shader.
|
|
fn as_translation(self) -> UiRegion {
|
|
UiRegion {
|
|
x: UiSpan::new(self.x.start, self.x.start + Len::FULL),
|
|
y: UiSpan::new(self.y.start, self.y.start + Len::FULL),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for UiRenderState {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|