Retain layout sizes by pixel axis
This commit is contained in:
1 parent
82fa6c1123
commit
b1b3eca1c0
11 files changed
+371
-69
No files matched your search
+128
-19
@@ -7,6 +7,11 @@ use crate::{
|
||||
};
|
||||
|
||||
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
||||
const LAYOUT_EPSILON_PX: f32 = 0.05;
|
||||
|
||||
fn pixel_len_changed(old: f32, new: f32) -> bool {
|
||||
(old - new).abs() > LAYOUT_EPSILON_PX
|
||||
}
|
||||
|
||||
pub struct UiRenderState {
|
||||
pub active: HashMap<WidgetId, ActiveData>,
|
||||
@@ -14,7 +19,14 @@ pub struct UiRenderState {
|
||||
pub(super) output_size: Vec2,
|
||||
|
||||
old_root: Option<WidgetId>,
|
||||
resized: bool,
|
||||
resized: [bool; 2],
|
||||
/// Content/state dirtiness whose retained size cannot answer a layout
|
||||
/// question until that widget has drawn again.
|
||||
invalid_sizes: HashSet<WidgetId>,
|
||||
/// Marks introduced only to traverse resize dependency paths. Unlike
|
||||
/// content dirtiness, these may retain an answer whose observed pixel
|
||||
/// axes did not change.
|
||||
resize_marks: HashSet<WidgetId>,
|
||||
draw_started: HashSet<WidgetId>,
|
||||
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
|
||||
/// replaces that while its children go on pointing at the slot.
|
||||
@@ -29,7 +41,9 @@ impl UiRenderState {
|
||||
layers: Default::default(),
|
||||
output_size: Vec2::ZERO,
|
||||
old_root: None,
|
||||
resized: false,
|
||||
resized: [false; 2],
|
||||
invalid_sizes: Default::default(),
|
||||
resize_marks: Default::default(),
|
||||
draw_started: Default::default(),
|
||||
slots: Default::default(),
|
||||
moves: Default::default(),
|
||||
@@ -38,10 +52,10 @@ impl UiRenderState {
|
||||
|
||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||
let size = size.into();
|
||||
if size != self.output_size {
|
||||
self.output_size = size;
|
||||
self.resized = true;
|
||||
for (axis, resized) in AXES.into_iter().zip(self.resized.iter_mut()) {
|
||||
*resized |= size.axis(axis) != self.output_size.axis(axis);
|
||||
}
|
||||
self.output_size = size;
|
||||
}
|
||||
|
||||
pub fn output_size(&self) -> Vec2 {
|
||||
@@ -53,6 +67,10 @@ impl UiRenderState {
|
||||
diag::bump(Counter::Updates);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _update = diag::timer(TimerKind::Update);
|
||||
self.invalid_sizes.clear();
|
||||
self.invalid_sizes
|
||||
.extend(rsc.widgets().needs_redraw.iter().copied());
|
||||
self.resize_marks.clear();
|
||||
// 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() {
|
||||
@@ -73,7 +91,7 @@ impl UiRenderState {
|
||||
if self.root_changed(root) {
|
||||
self.redraw_all(root, rsc);
|
||||
self.old_root = root.map(|r| r.id());
|
||||
} else if self.resized {
|
||||
} else if self.resized.iter().any(|&resized| resized) {
|
||||
// A region is a fraction of the output plus an offset, resolved
|
||||
// against the window in the shader, so a resize moves the whole
|
||||
// drawing on its own. Only a widget that read pixels can be wrong.
|
||||
@@ -83,7 +101,19 @@ impl UiRenderState {
|
||||
let dependents: Vec<_> = self
|
||||
.active
|
||||
.iter()
|
||||
.filter_map(|(&id, active)| active.reads_output.then_some(id))
|
||||
.filter_map(|(&id, active)| {
|
||||
AXES.into_iter()
|
||||
.zip(self.resized)
|
||||
.any(|(axis, changed)| {
|
||||
changed
|
||||
&& active.reads_output[axis as usize]
|
||||
&& pixel_len_changed(
|
||||
active.output_px.axis(axis),
|
||||
self.output_size.axis(axis),
|
||||
)
|
||||
})
|
||||
.then_some(id)
|
||||
})
|
||||
.collect();
|
||||
for id in dependents {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
@@ -93,12 +123,21 @@ impl UiRenderState {
|
||||
rsc.widgets_mut().needs_redraw.insert(top);
|
||||
}
|
||||
}
|
||||
self.resize_marks.extend(
|
||||
rsc.widgets()
|
||||
.needs_redraw
|
||||
.iter()
|
||||
.filter(|id| !self.invalid_sizes.contains(id))
|
||||
.copied(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if rsc.widgets().has_updates() {
|
||||
self.redraw_updates(rsc);
|
||||
}
|
||||
self.resized = false;
|
||||
self.resized = [false; 2];
|
||||
self.invalid_sizes.clear();
|
||||
self.resize_marks.clear();
|
||||
}
|
||||
|
||||
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
|
||||
@@ -174,7 +213,9 @@ impl UiRenderState {
|
||||
primitives: Vec::new(),
|
||||
children: Vec::new(),
|
||||
size_deps: Vec::new(),
|
||||
reads_output: false,
|
||||
size_box_inputs: [false; 2],
|
||||
size_output_inputs: [false; 2],
|
||||
reads_output: [false; 2],
|
||||
move_idx,
|
||||
rsc,
|
||||
};
|
||||
@@ -199,6 +240,8 @@ impl UiRenderState {
|
||||
primitives,
|
||||
children,
|
||||
size_deps,
|
||||
size_box_inputs,
|
||||
size_output_inputs,
|
||||
reads_output,
|
||||
move_idx,
|
||||
layer,
|
||||
@@ -222,13 +265,15 @@ impl UiRenderState {
|
||||
primitives,
|
||||
children,
|
||||
size_deps,
|
||||
size_box_inputs,
|
||||
size_output_inputs,
|
||||
output_px: self.output_size,
|
||||
reads_output,
|
||||
move_idx,
|
||||
parent_move,
|
||||
mask,
|
||||
layer,
|
||||
};
|
||||
|
||||
// remove old children that weren't kept
|
||||
for c in &old_children {
|
||||
if !active.children.contains(c) {
|
||||
@@ -238,6 +283,8 @@ impl UiRenderState {
|
||||
|
||||
rsc.on_draw(&active);
|
||||
self.active.insert(id, active);
|
||||
self.invalid_sizes.remove(&id);
|
||||
self.resize_marks.remove(&id);
|
||||
size
|
||||
}
|
||||
|
||||
@@ -272,6 +319,60 @@ impl UiRenderState {
|
||||
.to_abs(self.output_size)
|
||||
}
|
||||
|
||||
/// A clean widget's retained size, when the offered pixel axes which
|
||||
/// produced that answer are unchanged. This observes the old answer only;
|
||||
/// it does not move or otherwise reuse the widget's drawing.
|
||||
pub(super) fn retained_size(
|
||||
&self,
|
||||
id: WidgetId,
|
||||
region: UiRegion,
|
||||
parent_move: MoveIdx,
|
||||
widgets: &Widgets,
|
||||
) -> Option<(Size, [bool; 2], [bool; 2])> {
|
||||
if self.size_is_invalid(id, widgets) || self.dirty_size_under(id, widgets) {
|
||||
return None;
|
||||
}
|
||||
let active = self.active.get(&id)?;
|
||||
if active.parent_move != parent_move {
|
||||
return None;
|
||||
}
|
||||
let px = self.px_of(parent_move, region);
|
||||
let valid_box = AXES
|
||||
.into_iter()
|
||||
.zip(active.size_box_inputs)
|
||||
.all(|(axis, depends)| {
|
||||
!depends || !pixel_len_changed(active.px.axis(axis), px.axis(axis))
|
||||
});
|
||||
let valid_output =
|
||||
AXES.into_iter()
|
||||
.zip(active.size_output_inputs)
|
||||
.all(|(axis, depends)| {
|
||||
!depends
|
||||
|| !pixel_len_changed(
|
||||
active.output_px.axis(axis),
|
||||
self.output_size.axis(axis),
|
||||
)
|
||||
});
|
||||
(valid_box && valid_output).then_some((
|
||||
active.size,
|
||||
active.size_box_inputs,
|
||||
active.size_output_inputs,
|
||||
))
|
||||
}
|
||||
|
||||
fn size_is_invalid(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
||||
self.invalid_sizes.contains(&id)
|
||||
|| (widgets.needs_redraw.contains(&id) && !self.resize_marks.contains(&id))
|
||||
}
|
||||
|
||||
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
||||
self.active.get(&id).is_some_and(|active| {
|
||||
active.size_deps.iter().any(|child| {
|
||||
self.size_is_invalid(*child, widgets) || self.dirty_size_under(*child, widgets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// The drawing a widget already has, kept for a new box if the box has not
|
||||
/// changed in a way it depends on.
|
||||
fn try_reuse(
|
||||
@@ -310,7 +411,7 @@ impl UiRenderState {
|
||||
let px = self.px_of(parent_move, region);
|
||||
let mut changed = [false; 2];
|
||||
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
|
||||
*c = px.axis(axis) != old_px.axis(axis);
|
||||
*c = pixel_len_changed(old_px.axis(axis), px.axis(axis));
|
||||
}
|
||||
if !changed.iter().any(|&c| c) && old_region == region {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
@@ -360,7 +461,6 @@ impl UiRenderState {
|
||||
self.moves.set(slot, region);
|
||||
let active = self.active.get_mut(&id).unwrap();
|
||||
active.region = region;
|
||||
active.px = px;
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
{
|
||||
diag::bump(Counter::ReuseMoved);
|
||||
@@ -472,6 +572,8 @@ impl UiRenderState {
|
||||
self.slots.clear();
|
||||
self.moves.clear();
|
||||
self.layers.clear();
|
||||
self.invalid_sizes.clear();
|
||||
self.resize_marks.clear();
|
||||
rsc.widgets_mut().needs_redraw.clear();
|
||||
rsc.free();
|
||||
}
|
||||
@@ -486,7 +588,7 @@ impl UiRenderState {
|
||||
// reader and gives each changing box its final constraints first.
|
||||
while let Some(id) = {
|
||||
let dirty = rsc.widgets().needs_redraw.iter().copied();
|
||||
match self.resized {
|
||||
match self.resized.iter().any(|&resized| resized) {
|
||||
true => dirty.min_by_key(|&id| self.depth(id)),
|
||||
false => dirty.max_by_key(|&id| self.depth(id)),
|
||||
}
|
||||
@@ -519,7 +621,9 @@ impl UiRenderState {
|
||||
root: impl Into<Option<&'a StrongWidget>>,
|
||||
widgets: &Widgets,
|
||||
) -> bool {
|
||||
self.root_changed(root) || self.resized || widgets.has_updates()
|
||||
self.root_changed(root)
|
||||
|| self.resized.iter().any(|&resized| resized)
|
||||
|| widgets.has_updates()
|
||||
}
|
||||
|
||||
pub fn active_widgets(&self) -> usize {
|
||||
@@ -556,16 +660,20 @@ impl UiRenderState {
|
||||
/// redraws a widget that's currently active (drawn)
|
||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
||||
self.draw_started.remove(&id);
|
||||
if rsc.widgets().needs_redraw.contains(&id) && !self.resize_marks.contains(&id) {
|
||||
self.invalid_sizes.insert(id);
|
||||
}
|
||||
// A widget can only answer whether its size changed by drawing in the
|
||||
// box its parent chose. If that box changed in pixels, its retained
|
||||
// placement is stale and the highest size reader must choose the new
|
||||
// box first. Otherwise the widget can draw locally, and its readers
|
||||
// only matter if the returned size actually changed.
|
||||
let box_changed = self
|
||||
.active
|
||||
.get(&id)
|
||||
.is_some_and(|active| self.px_of(active.parent_move, active.region) != active.px);
|
||||
if (self.resized || box_changed)
|
||||
let box_changed = self.active.get(&id).is_some_and(|active| {
|
||||
let px = self.px_of(active.parent_move, active.region);
|
||||
AXES.into_iter()
|
||||
.any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis)))
|
||||
});
|
||||
if (self.resized.iter().any(|&resized| resized) || box_changed)
|
||||
&& let Some(top) = self.mark_readers(id, rsc)
|
||||
{
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
@@ -611,6 +719,7 @@ impl UiRenderState {
|
||||
// Propagate one dependency edge at a time. If drawing the reader
|
||||
// does not change its own size, nothing above it can observe this.
|
||||
rsc.widgets_mut().needs_redraw.insert(parent);
|
||||
self.invalid_sizes.insert(parent);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::ReaderEdges);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user