Three things the measurements asked for, all about how much a box that came from an answer costs. **A part in the box's own coordinates.** Saying "less eleven pixels at the end" in frame lengths from the box's start means reading how long the box is, and a container whose box is its own answer then depends on its own answer: `Pad` drew sixty-four times in one resize frame at seed 13, chasing its own width. `Part::Of` says the same thing as a part of the box, which composes without a length -- pixels are pixels wherever the box lands -- and what a child under it holds for maps back through that part onto the container's own box rather than onto the frame. **One axis of the box at a time.** `extent_len` pinned both axes, so a span dividing one of them held for one length of the other as well, and a resize broke every span whose cross-axis answer moved. **No lazy placement.** Leaving a child's answer to be placed at the end of the parent's draw, rather than as the child answers, was meant to save a recomposition. It costs one instead: the drawing is put in the part first and in the answer's box after, and where it does not hold for both that is two drawings rather than one. Seed 1 at depth 8 went from 391 widget draws on a resize to 29 with it gone. The test that pinned three draws for a numeric leaf in a span goes with it. Seed 1 at depth 8, widget draws / distinct widgets / update, against #18's head and against the commit this branch started from: | phase |e44dea3|34cafb6| here | | --- | --- | --- | --- | | cold | 369/261/10.6 | 463/274/13.3 | 516/288/12.0 | | repaint | 1 | 1 | 1 | | many | 157/95/0.33 | 263/108/0.59 | 187/119/0.52 | | size | 16/12/0.018 | 3/3 | 3/3/0.010 | | scroll | 2/0.002 | 1 | 1/0.004 | | resize | 13/13/0.019 | 22/15/0.032 | 24/76/0.090 | Seed 13 at depth 8 is where the protocol still costs: `many` 1091 draws against #18's 524, and `resize` 2215 against a frame #18 does not draw at all. Both are the same shape -- an answer measured in one box and drawn in another -- and the handoff says where that comes from. Checked: fmt, clippy with -D warnings, 108 suite tests, 20 core tests, the 11 generated cases, and the shrinker at 400 trees of depth 5, which fails seeds 2 (repaint) and 108 (reorder).
1282 lines
50 KiB
Rust
1282 lines
50 KiB
Rust
#[cfg(feature = "layout-diagnostics")]
|
|
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
|
use crate::ui::painter::{declared_lens, frame_and_extent, part_of, placed_extent};
|
|
use crate::{
|
|
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutHolds, LayoutLen, Len, MaskIdx, MoveIdx,
|
|
Moves, Painter, Part, PixelRegion, Place, PxVec2, Size, StrongWidget, UiRegion, UiRsc, Weight,
|
|
WidgetId, Widgets,
|
|
util::{HashMap, Vec2},
|
|
};
|
|
|
|
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
|
|
|
/// 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,
|
|
/// The frame in the parent widget's frame coordinates, before
|
|
/// composition. Its length is the same on every ask of the widget.
|
|
pub frame: UiRegion,
|
|
/// That frame composed into `parent_move`'s coordinates, which is what
|
|
/// the widget's own drawing is written within.
|
|
pub frame_abs: UiRegion,
|
|
/// The box the drawing is given, in the frame's own coordinates: the
|
|
/// part of the parent's own box that `place` names, before the widget's
|
|
/// answer is placed inside it.
|
|
pub part: UiRegion,
|
|
/// What of the parent's extent the drawing was given, and what it was
|
|
/// given at the parent's first ask of it. See [`Place`].
|
|
pub place: [Place; 2],
|
|
pub offer_place: [Place; 2],
|
|
/// Whether this ask is the one the widget's answer is kept from: the
|
|
/// first box its parent asked about, in the parent's own measuring draw.
|
|
pub offer: bool,
|
|
/// The frame in pixels: one multiply from the parent's own, which is
|
|
/// where every pixel length in layout comes from.
|
|
pub px: PxVec2,
|
|
}
|
|
|
|
impl DrawInfo {
|
|
/// The axes where the part is the drawing's box outright, which are the
|
|
/// axes the answer is not placed inside it again.
|
|
fn fill(&self) -> [bool; 2] {
|
|
self.place.map(Place::fills)
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
struct Placing {
|
|
id: WidgetId,
|
|
extent: UiRegion,
|
|
/// The widget's frame in the coordinates its children compose within:
|
|
/// `FULL` where it is a region node, since its box is that node.
|
|
local: UiRegion,
|
|
px: PxVec2,
|
|
depth: usize,
|
|
move_idx: MoveIdx,
|
|
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| {
|
|
let px = active.frame.size().to_px(size);
|
|
// Nothing above the root chose anything, so the box it was first
|
|
// asked about is the whole of its frame.
|
|
let offer = part_of(UiRegion::FULL, active.offer_place);
|
|
active.answers_at(px, offer) && active.holds.contains(px, active.extent)
|
|
});
|
|
if !stands {
|
|
widgets.needs_redraw.insert(root);
|
|
}
|
|
}
|
|
|
|
/// The root is asked about in the output: the window is where a fraction
|
|
/// becomes pixels rather than a box of its own, so the root's box is the
|
|
/// first length threaded down. Its own rules narrow that box, and where
|
|
/// they do the narrowed box is also the offer -- nothing above it chose
|
|
/// anything else.
|
|
fn root_info(&self, region: UiRegion) -> DrawInfo {
|
|
let px = region.size().to_px(self.output_size);
|
|
DrawInfo {
|
|
layer: 0,
|
|
parent: None,
|
|
depth: 1,
|
|
parent_move: MoveIdx::NONE,
|
|
region_node: false,
|
|
mask: MaskIdx::NONE,
|
|
frame: region,
|
|
frame_abs: region,
|
|
part: UiRegion::FULL,
|
|
place: [Place::Within(Part::All); 2],
|
|
offer_place: [Place::Within(Part::All); 2],
|
|
offer: true,
|
|
px,
|
|
}
|
|
}
|
|
|
|
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 region = Self::root_region(id.id(), rsc.widgets());
|
|
let info = self.root_info(region);
|
|
self.draw_inner(id.id(), info, None, rsc);
|
|
}
|
|
}
|
|
|
|
/// The root's frame: the window, narrowed by the root's own rules. Its
|
|
/// extent is that frame, since nothing above it chose anything else.
|
|
fn root_region(id: WidgetId, widgets: &Widgets) -> UiRegion {
|
|
let declared = declared_lens(widgets, id);
|
|
let narrow =
|
|
AXES.map(|axis| declared[axis as usize].map(|len| Len::from_parts(len.rel, len.px)));
|
|
frame_and_extent(
|
|
UiRegion::FULL,
|
|
UiRegion::FULL,
|
|
narrow,
|
|
widgets.alignment(id),
|
|
)
|
|
.0
|
|
}
|
|
|
|
pub(super) fn draw_inner(
|
|
&mut self,
|
|
id: WidgetId,
|
|
info: DrawInfo,
|
|
mut old: Option<ActiveData>,
|
|
rsc: &mut dyn UiRsc,
|
|
) -> (Size, LayoutHolds, LayoutHolds) {
|
|
let (frame, part) = (info.frame_abs, info.part);
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
diag::bump(Counter::DrawRequests);
|
|
diag::draw_request(id, info.parent, frame, info.px, info.region_node);
|
|
}
|
|
let align = rsc.widgets().alignment(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);
|
|
let retained = match stale {
|
|
true => None,
|
|
false => self
|
|
.retained_answer(id, part, info)
|
|
.or_else(|| self.try_reuse(id, frame, part, info, rsc)),
|
|
};
|
|
let answer = retained.unwrap_or_else(|| {
|
|
if old.is_none() {
|
|
old = self.remove(id, false, rsc);
|
|
}
|
|
self.draw_at(id, part, info, old.take(), rsc)
|
|
});
|
|
|
|
// Where the drawing goes, in the frame's own coordinates: the part
|
|
// its parent gave it, with the answer placed inside that part on any
|
|
// axis the parent left open. The frame itself does not change, so
|
|
// nothing under it resolves a fraction a second time.
|
|
//
|
|
// From the answer it gave when its parent first asked, and not from
|
|
// what a placing evaluation reported: a drawing made in the box that
|
|
// answer chose is answering a different question, and placing it by
|
|
// that would move the box out from under itself.
|
|
let measured = match info.offer {
|
|
true => answer.0,
|
|
false => self.active[&id].answer.map_or(answer.0, |(size, _)| size),
|
|
};
|
|
let extent = placed_extent(
|
|
part,
|
|
measured,
|
|
declared_lens(rsc.widgets(), id),
|
|
info.fill(),
|
|
align,
|
|
);
|
|
self.place(id, extent, info, rsc);
|
|
|
|
// On axes the parent filled, measurement and drawing share an extent.
|
|
// Otherwise the answer fixes the final extent as a function of the
|
|
// frame, so pull that drawing's validity back through it.
|
|
let drawing_holds = self.active[&id].holds;
|
|
let mut settled = answer;
|
|
for axis in AXES {
|
|
let n = axis as usize;
|
|
settled.1.frame[n] = settled.1.frame[n].and(drawing_holds.frame[n]);
|
|
if info.fill()[n] {
|
|
settled.1.extent[n] = settled.1.extent[n].and(drawing_holds.extent[n]);
|
|
} else {
|
|
settled.1.frame[n] = settled.1.frame[n]
|
|
.and(drawing_holds.extent[n].through(extent.axis(axis).len()));
|
|
}
|
|
}
|
|
let active = self.active.get_mut(&id).unwrap();
|
|
// Whoever asked owns how the boxes were reached: the frame it stated,
|
|
// and what of its own box it gave the drawing. A local redraw asks
|
|
// the same question again from these.
|
|
active.frame_abs = frame;
|
|
active.frame = info.frame;
|
|
if info.offer {
|
|
active.answer = Some(answer);
|
|
active.offer_place = info.offer_place;
|
|
active.offer_part = part;
|
|
}
|
|
active.place = info.place;
|
|
active.own_align = align;
|
|
// A subtree can be reused whole under a different parent -- same box,
|
|
// same layer, same region node -- and nothing in the drawing says it
|
|
// changed hands. Two things read who its parent is: a deferral, which
|
|
// marks whoever has it to draw, and the old parent's list of children,
|
|
// which its next draw undraws whatever is missing from.
|
|
let old_parent = std::mem::replace(&mut 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);
|
|
}
|
|
(answer.0, answer.1, settled.1)
|
|
}
|
|
|
|
/// Recompose retained geometry when the evaluation still holds at this extent.
|
|
fn place(&mut self, id: WidgetId, extent: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
|
|
if self
|
|
.try_reuse(id, info.frame_abs, extent, info, rsc)
|
|
.is_some()
|
|
{
|
|
return;
|
|
}
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::PlaceRedraws);
|
|
let old = self.remove(id, false, rsc);
|
|
self.draw_at(id, extent, info, old, rsc);
|
|
}
|
|
|
|
/// Calls a widget's `draw` and keeps what it drew in `extent` of `frame`.
|
|
fn draw_at(
|
|
&mut self,
|
|
id: WidgetId,
|
|
extent: UiRegion,
|
|
info: DrawInfo,
|
|
old: Option<ActiveData>,
|
|
rsc: &mut dyn UiRsc,
|
|
) -> (Size, LayoutHolds) {
|
|
let frame = info.frame_abs;
|
|
let (move_idx, local, retired_move) = match info.region_node {
|
|
// Its box becomes its movable region, so it draws in that
|
|
// region's coordinates and its box is one entry to rewrite.
|
|
true => (
|
|
self.move_slot(id, info.parent_move, frame),
|
|
UiRegion::FULL,
|
|
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, frame, self.slots.remove(&id)),
|
|
};
|
|
let (old_children, old_answer, old_offer_part) = match old {
|
|
Some(old) => (old.children, old.answer, Some(old.offer_part)),
|
|
None => (Vec::new(), None, None),
|
|
};
|
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
|
// Only evaluation at the original offer establishes the children's
|
|
// offers. A placing evaluation must not overwrite that question.
|
|
let px = info.px;
|
|
let at_offer = info.offer;
|
|
|
|
let mut painter = Painter {
|
|
state: self,
|
|
frame: local,
|
|
extent,
|
|
extent_len: [None; 2],
|
|
px,
|
|
mask: info.mask,
|
|
layer: info.layer,
|
|
own_layer: info.layer,
|
|
id,
|
|
textures: Vec::new(),
|
|
primitives: Vec::new(),
|
|
mask_region: None,
|
|
children: Vec::new(),
|
|
offered: Vec::new(),
|
|
at_offer,
|
|
size_deps: Vec::new(),
|
|
own: [Holds::ANY; 2],
|
|
under: LayoutHolds::ANY,
|
|
extent_own: [Holds::ANY; 2],
|
|
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: _,
|
|
frame: _,
|
|
extent: _,
|
|
px: _,
|
|
mask,
|
|
textures,
|
|
primitives,
|
|
mask_region,
|
|
extent_own,
|
|
extent_len,
|
|
answer_under,
|
|
children,
|
|
offered: _,
|
|
at_offer: _,
|
|
size_deps,
|
|
own,
|
|
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.
|
|
let rules = rsc.widgets().size_rules(id);
|
|
let size = Size {
|
|
x: rules.x.apply(size.x),
|
|
y: rules.y.apply(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 || AXES.into_iter().all(|axis| within_box(size, px, axis)),
|
|
"'{}' ({id:?}) clips to {px:?} and reports {size}",
|
|
rsc.widgets().label(id),
|
|
);
|
|
for c in &old_children {
|
|
if !children.contains(c) {
|
|
self.undraw_rec(*c, rsc);
|
|
}
|
|
}
|
|
if let Some(idx) = retired_move {
|
|
self.moves.remove(idx);
|
|
}
|
|
let own_holds = LayoutHolds {
|
|
frame: own,
|
|
extent: extent_own,
|
|
extent_len,
|
|
};
|
|
let answer_holds = own_holds.and(answer_under);
|
|
let holds = answer_holds.and(under);
|
|
debug_assert!(
|
|
holds.contains(px, extent),
|
|
"'{}' ({id:?}) drew in {px:?}, outside the ranges it reported: {holds:?}",
|
|
rsc.widgets().label(id),
|
|
);
|
|
// 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,
|
|
frame: UiRegion::FULL,
|
|
frame_abs: UiRegion::FULL,
|
|
part: UiRegion::FULL,
|
|
place: [Place::Within(Part::All); 2],
|
|
offer_place: [Place::Within(Part::All); 2],
|
|
offer: false,
|
|
px,
|
|
},
|
|
rsc,
|
|
);
|
|
rsc.widgets_mut().needs_redraw.remove(&dep);
|
|
}
|
|
}
|
|
|
|
let active = ActiveData {
|
|
id,
|
|
frame_abs: frame,
|
|
extent,
|
|
frame: info.frame,
|
|
place: info.place,
|
|
offer_place: info.offer_place,
|
|
offer_part: old_offer_part.unwrap_or(extent),
|
|
// Whoever asked writes the answer, if this was the asking.
|
|
answer: old_answer,
|
|
size,
|
|
holds,
|
|
drawn: true,
|
|
parent: info.parent,
|
|
depth: info.depth,
|
|
textures,
|
|
primitives,
|
|
mask_region,
|
|
children,
|
|
size_deps,
|
|
declared: declared_lens(rsc.widgets(), 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);
|
|
(size, 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,
|
|
part: UiRegion,
|
|
info: DrawInfo,
|
|
) -> Option<(Size, LayoutHolds)> {
|
|
let active = self.active.get(&id)?;
|
|
let has_region_node = active.move_idx != active.parent_move;
|
|
if !active.drawn
|
|
|| has_region_node != info.region_node
|
|
|| active.parent_move != info.parent_move
|
|
{
|
|
return None;
|
|
}
|
|
let answer = active.answer?;
|
|
answer.1.contains(info.px, part).then_some(answer)
|
|
}
|
|
|
|
/// The pixel lengths of a widget's frame, which is what a local redraw
|
|
/// needs to ask the question its parent asked.
|
|
///
|
|
/// It is threaded down from the window a length of a box at a time, and
|
|
/// this takes the same steps back up: a widget's frame is a length of its
|
|
/// parent's frame, and that chain has no coordinate frame in it, so a
|
|
/// region node cannot break it -- and it lands on the number a cold
|
|
/// layout computes rather than near it.
|
|
fn asked_px(&self, id: WidgetId) -> PxVec2 {
|
|
let active = &self.active[&id];
|
|
// Nothing above the root: the window is where a fraction becomes
|
|
// pixels, which is also the whole of the frame the root is given.
|
|
let parent_px = match active.parent.and_then(|p| self.active.get(&p)) {
|
|
Some(parent) => self.asked_px(parent.id),
|
|
None => self.output_size,
|
|
};
|
|
active.frame.size().to_px(parent_px)
|
|
}
|
|
|
|
/// Reuses the actual drawing in a new box if its retained contract holds
|
|
/// there. Answers retained from a different ask are handled separately.
|
|
fn try_reuse(
|
|
&mut self,
|
|
id: WidgetId,
|
|
frame: UiRegion,
|
|
extent: UiRegion,
|
|
info: DrawInfo,
|
|
rsc: &mut dyn UiRsc,
|
|
) -> Option<(Size, LayoutHolds)> {
|
|
#[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 None;
|
|
}
|
|
let active = self.active.get(&id)?;
|
|
if !active.drawn {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::reuse(id, ReuseOutcome::Undrawn);
|
|
return None;
|
|
}
|
|
let has_region_node = active.move_idx != active.parent_move;
|
|
if has_region_node != info.region_node {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::ReuseWrongNode);
|
|
return None;
|
|
}
|
|
// 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 None;
|
|
}
|
|
// 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 None;
|
|
}
|
|
// In pixels, because the frame is a fraction of its parent's and
|
|
// that may be what changed -- an unchanged fraction of a box half the
|
|
// size is half the widget.
|
|
if !active.holds.contains(info.px, extent) {
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
// Which of the three said no, so a frame that redraws more
|
|
// than it should says where to look. They overlap: a drawing
|
|
// can be outside two of them at once.
|
|
let holds = active.holds;
|
|
for axis in AXES {
|
|
let n = axis as usize;
|
|
if holds.extent_len[n].is_some_and(|pinned| pinned != extent.axis(axis).len()) {
|
|
diag::bump(Counter::OutsidePlacement);
|
|
}
|
|
if !holds.frame[n].contains(info.px.axis(axis)) {
|
|
diag::bump(Counter::OutsideFrame);
|
|
}
|
|
if !holds.extent[n].contains(extent.axis(axis).len().to_px(info.px.axis(axis)))
|
|
{
|
|
diag::bump(Counter::OutsideExtent);
|
|
}
|
|
}
|
|
diag::bump(Counter::ReuseOutside);
|
|
diag::reuse(id, ReuseOutcome::Outside);
|
|
}
|
|
return None;
|
|
}
|
|
let extent_moved = active.extent != extent;
|
|
let moved = active.frame_abs != frame;
|
|
let (answer, slot) = ((active.size, active.holds), active.move_idx);
|
|
if moved {
|
|
if has_region_node {
|
|
self.moves.set(slot, frame);
|
|
} else {
|
|
self.recompose_subtree(id, frame, info.parent_move, rsc);
|
|
}
|
|
}
|
|
if extent_moved {
|
|
self.reposition(id, frame, extent, info, rsc);
|
|
}
|
|
self.redepth(id, info.depth);
|
|
let active = self.active.get_mut(&id).unwrap();
|
|
active.frame_abs = frame;
|
|
active.frame = info.frame;
|
|
active.place = info.place;
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
{
|
|
match (moved, has_region_node) {
|
|
(true, true) => diag::bump(Counter::ReuseMoved),
|
|
(true, false) => diag::bump(Counter::ReuseRemapped),
|
|
(false, _) => diag::bump(Counter::ReuseExact),
|
|
}
|
|
diag::reuse(
|
|
id,
|
|
if moved {
|
|
if has_region_node {
|
|
ReuseOutcome::Moved
|
|
} else {
|
|
ReuseOutcome::Remapped
|
|
}
|
|
} else {
|
|
ReuseOutcome::Exact
|
|
},
|
|
);
|
|
}
|
|
Some(answer)
|
|
}
|
|
|
|
/// Places one child of `at.id` in the box that widget's own box gives
|
|
/// it: its part of the extent, with its answer placed inside that part
|
|
/// where the ask left the axis open.
|
|
fn place_child(&mut self, child: WidgetId, at: &Placing, rsc: &mut dyn UiRsc) {
|
|
let active = &self.active[&child];
|
|
let (frame, part) = Self::re_ask(active, at.extent, active.place);
|
|
// The answer it gave, and not what its last drawing reported: a
|
|
// drawing made in the box that answer chose is answering a different
|
|
// question.
|
|
let answer = active.answer.map_or(active.size, |(size, _)| size);
|
|
let extent = placed_extent(
|
|
part,
|
|
answer,
|
|
active.declared,
|
|
active.place.map(Place::fills),
|
|
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.move_idx != active.parent_move,
|
|
mask: at.mask,
|
|
frame,
|
|
frame_abs: frame.within(&at.local),
|
|
part,
|
|
place: active.place,
|
|
offer_place: active.offer_place,
|
|
// Putting it back where it was asked about is that ask again, so
|
|
// what it answers there is the answer -- and putting it anywhere
|
|
// else is not, however the box was arrived at.
|
|
offer: active.place == active.offer_place,
|
|
px: frame.size().to_px(at.px),
|
|
};
|
|
self.place(child, extent, info, rsc);
|
|
}
|
|
|
|
/// The frame and the box a widget being asked again is given, from what
|
|
/// it already has and where its parent's box is now. A frame's length is
|
|
/// the same on every ask, so a declared length is put back where it sits
|
|
/// in the part rather than resolved from its rule a second time.
|
|
fn re_ask(
|
|
active: &ActiveData,
|
|
parent_extent: UiRegion,
|
|
place: [Place; 2],
|
|
) -> (UiRegion, UiRegion) {
|
|
let narrow = AXES.map(|axis| {
|
|
let n = axis as usize;
|
|
active.declared[n]
|
|
.is_some()
|
|
.then(|| active.frame.axis(axis).len())
|
|
});
|
|
frame_and_extent(
|
|
active.frame,
|
|
part_of(parent_extent, place),
|
|
narrow,
|
|
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,
|
|
frame: UiRegion,
|
|
extent: UiRegion,
|
|
info: DrawInfo,
|
|
rsc: &mut dyn UiRsc,
|
|
) {
|
|
let active = self.active.get_mut(&id).unwrap();
|
|
active.frame_abs = frame;
|
|
active.extent = extent;
|
|
let local = if info.region_node {
|
|
UiRegion::FULL
|
|
} else {
|
|
frame
|
|
};
|
|
for primitive in &active.primitives {
|
|
let handle = &primitive.handle;
|
|
*self.layers[handle.layer].region_mut(handle) =
|
|
primitive.region.within(&extent).within(&local);
|
|
}
|
|
if let Some(mask_region) = active.mask_region {
|
|
rsc.ui_mut().masks.get_mut(active.mask).region =
|
|
mask_region.within(&extent).within(&local);
|
|
}
|
|
let at = Placing {
|
|
id,
|
|
extent,
|
|
local,
|
|
px: info.px,
|
|
depth: info.depth,
|
|
move_idx: active.move_idx,
|
|
mask: active.mask,
|
|
};
|
|
let children = active.children.len();
|
|
for index in 0..children {
|
|
let child = self.active[&id].children[index];
|
|
self.place_child(child, &at, rsc);
|
|
}
|
|
}
|
|
|
|
/// A reused subtree keeps its shape, so every widget in it moves by the
|
|
/// same amount -- and where the top of it did not move, none of it 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;
|
|
let children = active.children.len();
|
|
for index in 0..children {
|
|
let child = self.active[&id].children[index];
|
|
self.redepth(child, depth + 1);
|
|
}
|
|
}
|
|
|
|
/// Replays the original local compositions, including their rounding order.
|
|
/// A region node terminates the walk because its contents name its slot.
|
|
fn recompose_subtree(
|
|
&mut self,
|
|
id: WidgetId,
|
|
frame: UiRegion,
|
|
parent_move: MoveIdx,
|
|
rsc: &mut dyn UiRsc,
|
|
) {
|
|
let active = self.active.get_mut(&id).unwrap();
|
|
active.frame_abs = frame;
|
|
if active.move_idx != parent_move {
|
|
self.moves.set(active.move_idx, frame);
|
|
return;
|
|
}
|
|
let extent = active.extent;
|
|
for primitive in &active.primitives {
|
|
let handle = &primitive.handle;
|
|
*self.layers[handle.layer].region_mut(handle) =
|
|
primitive.region.within(&extent).within(&frame);
|
|
}
|
|
if let Some(local) = active.mask_region {
|
|
rsc.ui_mut().masks.get_mut(active.mask).region = local.within(&extent).within(&frame);
|
|
}
|
|
let children = active.children.len();
|
|
for index in 0..children {
|
|
let child = self.active[&id].children[index];
|
|
let local = self.active[&child].frame;
|
|
self.recompose_subtree(child, local.within(&frame), parent_move, rsc);
|
|
}
|
|
}
|
|
|
|
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
|
|
let Some(widget) = rsc.widgets().get_dyn(id) else {
|
|
return true;
|
|
};
|
|
AXES.into_iter().all(|axis| {
|
|
widget
|
|
.size_hint(axis)
|
|
.is_none_or(|hint| hint == size.axis(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);
|
|
}
|
|
}
|
|
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.size_deps.clear();
|
|
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,
|
|
frame_abs: UiRegion::FULL,
|
|
extent: UiRegion::FULL,
|
|
frame: UiRegion::FULL,
|
|
place: [Place::Within(Part::All); 2],
|
|
offer_place: [Place::Within(Part::All); 2],
|
|
offer_part: UiRegion::FULL,
|
|
answer: None,
|
|
size,
|
|
holds: LayoutHolds::ANY,
|
|
drawn: false,
|
|
parent: info.parent,
|
|
depth: info.depth,
|
|
textures: Vec::new(),
|
|
primitives: Vec::new(),
|
|
mask_region: None,
|
|
children: Vec::new(),
|
|
size_deps: Vec::new(),
|
|
move_idx: info.parent_move,
|
|
declared: [None; 2],
|
|
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.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(|| {
|
|
let placed = active.extent.within(&active.frame_abs);
|
|
self.moves
|
|
.resolve(active.parent_move, placed)
|
|
.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.
|
|
let declared_changed = declared_lens(rsc.widgets(), id) != active.declared;
|
|
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
|
|
if let Some(parent) = active.parent
|
|
&& (declared_changed || alignment_changed || !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 region = Self::root_region(id, rsc.widgets());
|
|
let info = DrawInfo {
|
|
mask: active.parent_mask,
|
|
..self.root_info(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 px = self.asked_px(id);
|
|
let (was_answer, was_holds) = (active.answer, active.holds);
|
|
// The boxes its parent gave it, then and now: its frame is the same
|
|
// on every ask, so the question its parent asked is the one this
|
|
// asks again -- there is no box here that could be its parent's to
|
|
// choose instead.
|
|
let parent_extent = self.active[&parent].extent;
|
|
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,
|
|
frame: active.frame,
|
|
frame_abs: active.frame_abs,
|
|
part: Self::re_ask(active, parent_extent, active.place).1,
|
|
place: active.place,
|
|
offer_place: active.offer_place,
|
|
offer: false,
|
|
px,
|
|
};
|
|
// The ask that measured it, asked again: the box it was measured in
|
|
// as its parent left it, rather than where that ask's place resolves
|
|
// to now -- a parent drawn again in the box its own answer chose
|
|
// gives its children boxes it never measured anything in.
|
|
let offered = DrawInfo {
|
|
place: info.offer_place,
|
|
part: active.offer_part,
|
|
offer: true,
|
|
..info
|
|
};
|
|
#[cfg(feature = "layout-diagnostics")]
|
|
diag::bump(Counter::LocalRedraws);
|
|
|
|
// Asked again in the box its parent gave it, which is the question
|
|
// its parent asked only while that box is as long as the one it was
|
|
// measured in. Any other box is a different question, so the parent
|
|
// asks it, with the mark left on. Lengths and not whole boxes: what
|
|
// a drawing depends on is its lengths, so the same lengths elsewhere
|
|
// is one question.
|
|
//
|
|
// The frame is the same on every ask now, so this is about the box
|
|
// the drawing goes in alone. Removing it -- asking the measuring
|
|
// question here and placing the answer afterwards -- is what the
|
|
// transparent-frames plan asks for next, and it does not hold yet:
|
|
// seeds 104 (align) and 210 (reorder) at depth 5 settle differently
|
|
// warm and cold without it.
|
|
if info.part.size() != offered.part.size() {
|
|
self.mark(id, rsc.widgets_mut());
|
|
self.mark(parent, rsc.widgets_mut());
|
|
return false;
|
|
}
|
|
let old = self.remove(id, false, rsc);
|
|
// The original measurement is refreshed before the assigned slot is
|
|
// restored: its lengths may differ even though the frame is
|
|
// unchanged.
|
|
let answer = self.draw_inner(id, offered, old, rsc);
|
|
if info.place != info.offer_place || info.part != offered.part {
|
|
self.draw_inner(id, info, None, 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((size, holds)) = was_answer
|
|
&& answer.0 == size
|
|
&& answer.1.covers(holds)
|
|
{
|
|
active.answer = was_answer;
|
|
}
|
|
if active.holds.covers(was_holds) && was_holds.contains(px, active.extent) {
|
|
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());
|
|
}
|
|
true
|
|
}
|
|
}
|
|
|
|
/// Whether what a widget reports along `axis` is inside the box it drew in.
|
|
/// 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(size: Size, px: PxVec2, axis: Axis) -> bool {
|
|
let len = size.axis(axis);
|
|
let box_len = px.axis(axis);
|
|
len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len
|
|
}
|
|
|
|
impl Default for UiRenderState {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|