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
@@ -33,6 +33,7 @@ pub(crate) enum Counter {
|
||||
SizeReads,
|
||||
HintHits,
|
||||
HintMisses,
|
||||
RetainedSizeHits,
|
||||
ReuseAttempts,
|
||||
ReuseExact,
|
||||
ReuseMoved,
|
||||
@@ -67,6 +68,7 @@ impl Counter {
|
||||
"draw-result size reads",
|
||||
"hint hits",
|
||||
"hint misses",
|
||||
"retained size hits",
|
||||
"reuse attempts",
|
||||
"reuse exact",
|
||||
"reuse moved",
|
||||
|
||||
+10
-2
@@ -19,8 +19,16 @@ pub struct ActiveData {
|
||||
pub children: Vec<WidgetId>,
|
||||
/// The children whose size this widget read while drawing.
|
||||
pub size_deps: Vec<WidgetId>,
|
||||
/// Whether it read the output's size, and so is wrong when that changes.
|
||||
pub reads_output: bool,
|
||||
/// Offered pixel axes which flowed into this widget's reported size,
|
||||
/// directly or through a child size it read.
|
||||
pub size_box_inputs: [bool; 2],
|
||||
/// Output axes read while producing `size`, distinct from the widget's
|
||||
/// own box when that box has a fixed pixel length.
|
||||
pub size_output_inputs: [bool; 2],
|
||||
/// The output dimensions against which those dependencies were observed.
|
||||
pub output_px: Vec2,
|
||||
/// Output axes it read directly or while resolving its offered box.
|
||||
pub reads_output: [bool; 2],
|
||||
/// The slot its primitives are positioned through: its own if its parent
|
||||
/// placed it, otherwise the nearest ancestor that has one.
|
||||
pub move_idx: MoveIdx,
|
||||
|
||||
+87
-6
@@ -23,7 +23,10 @@ pub struct Painter<'a> {
|
||||
pub(super) children: Vec<WidgetId>,
|
||||
/// The children whose size this widget read while drawing.
|
||||
pub(super) size_deps: Vec<WidgetId>,
|
||||
pub(super) reads_output: bool,
|
||||
/// Offered pixel axes which can affect the size this draw reports.
|
||||
pub(super) size_box_inputs: [bool; 2],
|
||||
pub(super) size_output_inputs: [bool; 2],
|
||||
pub(super) reads_output: [bool; 2],
|
||||
/// The slot this widget's primitives are positioned through: its own if
|
||||
/// its parent placed it, otherwise the nearest ancestor that has one.
|
||||
pub(super) move_idx: MoveIdx,
|
||||
@@ -158,7 +161,7 @@ impl<'a> Painter<'a> {
|
||||
Some(hint) => {
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::HintHits);
|
||||
self.depend_on_size(id);
|
||||
self.depend_on_size(id, false);
|
||||
Some(hint)
|
||||
}
|
||||
None => {
|
||||
@@ -169,10 +172,66 @@ impl<'a> Painter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||
/// A retained child length valid under the region it is about to be
|
||||
/// offered. Unlike a hint, this is contextual: it is kept only when none
|
||||
/// of the offered pixel axes which produced it changed.
|
||||
pub fn known_len<W: ?Sized>(
|
||||
&mut self,
|
||||
child: &StrongWidget<W>,
|
||||
axis: Axis,
|
||||
region: UiRegion,
|
||||
) -> Option<Len> {
|
||||
if let Some(hint) = self.size_hint(child, axis) {
|
||||
return Some(hint);
|
||||
}
|
||||
self.retained_size(child, region)
|
||||
.map(|size| size.axis(axis))
|
||||
}
|
||||
|
||||
fn retained_size<W: ?Sized>(
|
||||
&mut self,
|
||||
child: &StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
) -> Option<Size> {
|
||||
let region = region.within(&self.region);
|
||||
let (size, box_inputs, output_inputs) =
|
||||
self.state
|
||||
.retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?;
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
diag::bump(Counter::RetainedSizeHits);
|
||||
self.depend_on_size_inputs(child, box_inputs, output_inputs);
|
||||
Some(size)
|
||||
}
|
||||
|
||||
fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>, inherit_inputs: bool) {
|
||||
let (box_inputs, output_inputs) = match inherit_inputs {
|
||||
true => self
|
||||
.state
|
||||
.active
|
||||
.get(&child.id())
|
||||
.map_or(([false; 2], [false; 2]), |active| {
|
||||
(active.size_box_inputs, active.size_output_inputs)
|
||||
}),
|
||||
false => ([false; 2], [false; 2]),
|
||||
};
|
||||
self.depend_on_size_inputs(child, box_inputs, output_inputs);
|
||||
}
|
||||
|
||||
fn depend_on_size_inputs<W: ?Sized>(
|
||||
&mut self,
|
||||
child: &StrongWidget<W>,
|
||||
box_inputs: [bool; 2],
|
||||
output_inputs: [bool; 2],
|
||||
) {
|
||||
if !self.size_deps.contains(&child.id()) {
|
||||
self.size_deps.push(child.id());
|
||||
}
|
||||
for (own, child) in self.size_box_inputs.iter_mut().zip(box_inputs) {
|
||||
*own |= child;
|
||||
}
|
||||
for (own, child) in self.size_output_inputs.iter_mut().zip(output_inputs) {
|
||||
*own |= child;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_text(
|
||||
@@ -220,19 +279,41 @@ impl<'a> Painter<'a> {
|
||||
/// The output's size in pixels. A widget that reads it draws again when
|
||||
/// the output changes, since nothing else can put that right.
|
||||
pub fn output_size(&mut self) -> Vec2 {
|
||||
self.reads_output = true;
|
||||
self.reads_output = [true; 2];
|
||||
self.size_output_inputs = [true; 2];
|
||||
self.state.output_size
|
||||
}
|
||||
|
||||
/// One axis of the output in pixels. Prefer this to [`Self::output_size`]
|
||||
/// when the other axis cannot affect the size this widget reports.
|
||||
pub fn output_len(&mut self, axis: Axis) -> f32 {
|
||||
self.reads_output[axis as usize] = true;
|
||||
self.size_output_inputs[axis as usize] = true;
|
||||
self.state.output_size.axis(axis)
|
||||
}
|
||||
|
||||
/// This widget's box in pixels. Resolved against the output's size and
|
||||
/// the boxes it sits within, so a widget that reads it draws again when
|
||||
/// the output changes.
|
||||
pub fn px_size(&mut self) -> Vec2 {
|
||||
self.reads_output = true;
|
||||
self.reads_output = [true; 2];
|
||||
self.size_box_inputs = [true; 2];
|
||||
let region = self.state.moves.resolve(self.move_idx, self.region);
|
||||
region.size().to_abs(self.state.output_size)
|
||||
}
|
||||
|
||||
/// One axis of this widget's box in pixels. Prefer this to
|
||||
/// [`Self::px_size`] when the other axis cannot affect the reported size.
|
||||
pub fn px_len(&mut self, axis: Axis) -> f32 {
|
||||
self.reads_output[axis as usize] = true;
|
||||
self.size_box_inputs[axis as usize] = true;
|
||||
let region = self.state.moves.resolve(self.move_idx, self.region);
|
||||
region
|
||||
.size()
|
||||
.axis(axis)
|
||||
.to_abs(self.state.output_size.axis(axis))
|
||||
}
|
||||
|
||||
pub fn text_data(&mut self) -> &mut TextData {
|
||||
&mut self.rsc.ui_mut().text
|
||||
}
|
||||
@@ -270,7 +351,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
|
||||
diag::bump(Counter::SizeReads);
|
||||
diag::size_read(self.child.id(), self.painter.id, self.size);
|
||||
}
|
||||
self.painter.depend_on_size(self.child);
|
||||
self.painter.depend_on_size(self.child, true);
|
||||
self.size
|
||||
}
|
||||
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
@@ -7,17 +7,31 @@ pub struct Aligned {
|
||||
|
||||
impl Widget for Aligned {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
// Drawn where it may be too big, then given its aligned box once its
|
||||
// size is known.
|
||||
let size = painter.place(&self.inner, UiRegion::FULL).size();
|
||||
let known = match self.align.tuple() {
|
||||
(Some(_), Some(_)) => painter
|
||||
.known_len(&self.inner, Axis::X, UiRegion::FULL)
|
||||
.zip(painter.known_len(&self.inner, Axis::Y, UiRegion::FULL))
|
||||
.map(|(x, y)| Size { x, y }),
|
||||
(Some(_), None) => painter
|
||||
.known_len(&self.inner, Axis::X, UiRegion::FULL)
|
||||
.map(|x| Size { x, y: Len::REST }),
|
||||
(None, Some(_)) => painter
|
||||
.known_len(&self.inner, Axis::Y, UiRegion::FULL)
|
||||
.map(|y| Size { x: Len::REST, y }),
|
||||
(None, None) => Some(Size::REST),
|
||||
};
|
||||
// Drawn where it may be too big only when the aligned axes are not
|
||||
// already known, then given its aligned box once its size is known.
|
||||
let had_size = known.is_some();
|
||||
let size = known.unwrap_or_else(|| painter.place(&self.inner, UiRegion::FULL).size());
|
||||
let region = match self.align.tuple() {
|
||||
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
|
||||
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
|
||||
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
|
||||
(None, None) => UiRegion::FULL,
|
||||
};
|
||||
painter.place(&self.inner, region);
|
||||
size
|
||||
let placed = painter.place(&self.inner, region).size();
|
||||
if had_size { placed } else { size }
|
||||
}
|
||||
|
||||
/// The aligned box is a fraction of its own, so the child keeps its
|
||||
|
||||
@@ -11,13 +11,15 @@ pub struct Scroll {
|
||||
|
||||
impl Widget for Scroll {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
let output_len = painter.output_size().axis(self.axis);
|
||||
let container_len = UiScalar::abs(painter.px_size().axis(self.axis));
|
||||
// Draw in the whole container to learn the content's length, then
|
||||
// place it at the scrolled offset.
|
||||
let child = painter.place(&self.inner, UiRegion::FULL).size();
|
||||
let content_len = child
|
||||
.axis(self.axis)
|
||||
let output_len = painter.output_len(self.axis);
|
||||
let container_len = UiScalar::abs(painter.px_len(self.axis));
|
||||
// Draw in the whole container only when its scrolling-axis length is
|
||||
// not already known, then place it at the scrolled offset.
|
||||
let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL);
|
||||
let measured = known_len.is_none();
|
||||
let child = measured.then(|| painter.place(&self.inner, UiRegion::FULL).size());
|
||||
let content_len = known_len
|
||||
.unwrap_or_else(|| child.unwrap().axis(self.axis))
|
||||
.apply_rest()
|
||||
.within_len(container_len)
|
||||
.to_abs(output_len);
|
||||
@@ -31,8 +33,8 @@ impl Widget for Scroll {
|
||||
|
||||
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
|
||||
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
||||
painter.place(&self.inner, region);
|
||||
child
|
||||
let placed = painter.place(&self.inner, region).size();
|
||||
child.unwrap_or(placed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,16 +15,14 @@ impl Widget for Span {
|
||||
let mut cursor = UiScalar::rel_min();
|
||||
let mut lens = Vec::with_capacity(self.children.len());
|
||||
for child in &self.children {
|
||||
let len = match painter.size_hint(child, axis) {
|
||||
let mut span = UiSpan::new(cursor, UiScalar::rel_max());
|
||||
if self.dir.sign == Sign::Neg {
|
||||
span.flip();
|
||||
}
|
||||
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
||||
let len = match painter.known_len(child, axis, region) {
|
||||
Some(len) => len,
|
||||
None => {
|
||||
let mut span = UiSpan::new(cursor, UiScalar::rel_max());
|
||||
if self.dir.sign == Sign::Neg {
|
||||
span.flip();
|
||||
}
|
||||
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
||||
painter.place(child, region).len(axis)
|
||||
}
|
||||
None => painter.place(child, region).len(axis),
|
||||
};
|
||||
cursor.abs += len.abs + self.gap;
|
||||
cursor.rel += len.rel;
|
||||
|
||||
@@ -54,7 +54,7 @@ impl TextView {
|
||||
|
||||
fn render(&mut self, painter: &mut Painter) -> &RenderedText {
|
||||
let width = if self.attrs.wrap {
|
||||
Some(painter.px_size().x)
|
||||
Some(painter.px_len(Axis::X))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
+14
-17
@@ -17,20 +17,11 @@ use iris::prelude::*;
|
||||
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow};
|
||||
|
||||
const DEPTH: usize = 4;
|
||||
const SEEDS: [u64; 6] = [1, 2, 3, 5, 8, 13];
|
||||
const REGION_ULPS: u32 = 4;
|
||||
|
||||
fn ordered_bits(value: f32) -> u32 {
|
||||
const SIGN: u32 = 1 << 31;
|
||||
let bits = value.to_bits();
|
||||
match bits & SIGN {
|
||||
0 => bits | SIGN,
|
||||
_ => !bits,
|
||||
}
|
||||
}
|
||||
const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98];
|
||||
const REGION_EPSILON_PX: f32 = 0.05;
|
||||
|
||||
fn same_coordinate(got: f32, want: f32) -> bool {
|
||||
got == want || ordered_bits(got).abs_diff(ordered_bits(want)) <= REGION_ULPS
|
||||
(got - want).abs() <= REGION_EPSILON_PX
|
||||
}
|
||||
|
||||
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
||||
@@ -165,9 +156,10 @@ fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness,
|
||||
for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() {
|
||||
let (got, want) = (wh.region(&w), ch.region(&c));
|
||||
drawn += usize::from(got.is_some());
|
||||
// Equivalent composition orders can differ by a few f32 ULPs. Bound
|
||||
// that representation drift directly, while whether a widget drew
|
||||
// remains exact.
|
||||
// This oracle cares where rasterization lands, not whether equivalent
|
||||
// arithmetic produced the same f32. Keep the tolerance to one
|
||||
// twentieth of a physical pixel, while whether a widget drew remains
|
||||
// exact.
|
||||
if same_region(got, want) {
|
||||
continue;
|
||||
}
|
||||
@@ -327,9 +319,14 @@ fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
|
||||
/// same defect, and it wants fixing where the two draws meet -- LAYOUT.md §4 --
|
||||
/// rather than anywhere in the chain.
|
||||
#[test]
|
||||
#[ignore = "a hundred seeds, rather than the six the others check"]
|
||||
#[ignore = "a hundred seeds, rather than the seven the others check"]
|
||||
fn a_long_run_of_seeds_agrees() {
|
||||
for seed in 1..=100 {
|
||||
let seeds = std::env::var("IRIS_GENERATED_SEED")
|
||||
.ok()
|
||||
.and_then(|seed| seed.parse().ok())
|
||||
.map(|seed| seed..=seed)
|
||||
.unwrap_or(1..=100);
|
||||
for seed in seeds {
|
||||
changed_size(seed);
|
||||
resized(seed);
|
||||
resized_then_changed(seed);
|
||||
|
||||
@@ -75,6 +75,22 @@ fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
fn trace_selected(tree: &Tree) {
|
||||
let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else {
|
||||
return;
|
||||
};
|
||||
let index = value
|
||||
.parse::<usize>()
|
||||
.expect("IRIS_TRACE_INDEX must be a tree.ids index");
|
||||
let id = tree.ids[index];
|
||||
iris::core::layout_diagnostics::trace_widget(id);
|
||||
println!("tracing tree.ids[{index}] = {id:?}");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "layout-diagnostics"))]
|
||||
fn trace_selected(_: &Tree) {}
|
||||
|
||||
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
||||
let mut harness = Harness::new(OUTPUT);
|
||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
||||
@@ -157,6 +173,7 @@ fn layout_cost() {
|
||||
"fixture: seed {seed}, depth {depth}, {} widgets",
|
||||
tree.ids.len()
|
||||
);
|
||||
trace_selected(&tree);
|
||||
#[cfg(feature = "layout-diagnostics")]
|
||||
let _ = iris::core::layout_diagnostics::take();
|
||||
run("cold", 1, &mut harness, |_, _| {});
|
||||
@@ -165,6 +182,7 @@ fn layout_cost() {
|
||||
|
||||
if selected("repaint") {
|
||||
let (mut harness, tree) = warm(seed, depth);
|
||||
trace_selected(&tree);
|
||||
let leaf = tree.ids[0];
|
||||
run("repaint", frames, &mut harness, move |harness, _| {
|
||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf);
|
||||
@@ -173,6 +191,7 @@ fn layout_cost() {
|
||||
|
||||
if selected("size") {
|
||||
let (mut harness, tree) = warm(seed, depth);
|
||||
trace_selected(&tree);
|
||||
let sized = tree.sized[0];
|
||||
run("size", frames, &mut harness, move |harness, frame| {
|
||||
harness.rsc[sized].x = Some(Len::abs(100.0 + (frame % 2) as f32 * 40.0));
|
||||
@@ -181,6 +200,7 @@ fn layout_cost() {
|
||||
|
||||
if selected("scroll") {
|
||||
let (mut harness, tree) = warm(seed, depth);
|
||||
trace_selected(&tree);
|
||||
let scroll = tree.scrolls[0];
|
||||
run("scroll", frames, &mut harness, move |harness, frame| {
|
||||
harness.rsc[scroll].scroll(if frame % 2 == 0 { 12.0 } else { -12.0 });
|
||||
@@ -189,8 +209,9 @@ fn layout_cost() {
|
||||
|
||||
if selected("resize") {
|
||||
let (mut harness, tree) = warm(seed, depth);
|
||||
trace_selected(&tree);
|
||||
run("resize", frames, &mut harness, |harness, frame| {
|
||||
harness.resize((OUTPUT.0 - (frame % 2) as f32 * 8.0, OUTPUT.1));
|
||||
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
|
||||
});
|
||||
drop(tree);
|
||||
}
|
||||
|
||||
@@ -191,6 +191,17 @@ impl Widget for ReadsOutput {
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadsWidth {
|
||||
draws: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Widget for ReadsWidth {
|
||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||
self.draws.set(self.draws.get() + 1);
|
||||
Size::abs((painter.output_len(Axis::X) / 4.0, 20.0).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_does_not_redraw_what_the_shader_can_move() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
@@ -227,6 +238,65 @@ fn a_resize_redraws_what_read_the_output() {
|
||||
assert_eq!(draws.get(), settled + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resize_only_redraws_read_output_axes() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let leaf = ReadsWidth {
|
||||
draws: draws.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(leaf);
|
||||
let settled = draws.get();
|
||||
|
||||
h.resize((400, 300));
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled, "height was never read");
|
||||
|
||||
h.resize((800, 300));
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled + 1, "width changes its answer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subpixel_resize_changes_accumulate_from_the_last_layout() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let draws = Rc::new(Cell::new(0));
|
||||
let leaf = ReadsWidth {
|
||||
draws: draws.clone(),
|
||||
}
|
||||
.add(&mut h.rsc);
|
||||
h.set_root(leaf);
|
||||
let settled = draws.get();
|
||||
|
||||
for width in [400.02, 400.04, 400.05] {
|
||||
h.resize((width, 200.0));
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled);
|
||||
}
|
||||
|
||||
h.resize((400.06, 200.0));
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subpixel_box_changes_accumulate_from_the_last_draw() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
let (first, draws, _) = pair(&mut h, OnResize::Redraw);
|
||||
let settled = draws.get();
|
||||
|
||||
for width in [100.02, 100.04, 100.05] {
|
||||
h.rsc[first].size.x = Len::abs(width);
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled);
|
||||
}
|
||||
|
||||
h.rsc[first].size.x = Len::abs(100.06);
|
||||
h.frame();
|
||||
assert_eq!(draws.get(), settled + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reporting_the_same_output_size_does_not_start_a_resize() {
|
||||
let mut h = Harness::new((400, 200));
|
||||
|
||||
Reference in new issue
Block a user