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,
|
SizeReads,
|
||||||
HintHits,
|
HintHits,
|
||||||
HintMisses,
|
HintMisses,
|
||||||
|
RetainedSizeHits,
|
||||||
ReuseAttempts,
|
ReuseAttempts,
|
||||||
ReuseExact,
|
ReuseExact,
|
||||||
ReuseMoved,
|
ReuseMoved,
|
||||||
@@ -67,6 +68,7 @@ impl Counter {
|
|||||||
"draw-result size reads",
|
"draw-result size reads",
|
||||||
"hint hits",
|
"hint hits",
|
||||||
"hint misses",
|
"hint misses",
|
||||||
|
"retained size hits",
|
||||||
"reuse attempts",
|
"reuse attempts",
|
||||||
"reuse exact",
|
"reuse exact",
|
||||||
"reuse moved",
|
"reuse moved",
|
||||||
|
|||||||
+10
-2
@@ -19,8 +19,16 @@ pub struct ActiveData {
|
|||||||
pub children: Vec<WidgetId>,
|
pub children: Vec<WidgetId>,
|
||||||
/// The children whose size this widget read while drawing.
|
/// The children whose size this widget read while drawing.
|
||||||
pub size_deps: Vec<WidgetId>,
|
pub size_deps: Vec<WidgetId>,
|
||||||
/// Whether it read the output's size, and so is wrong when that changes.
|
/// Offered pixel axes which flowed into this widget's reported size,
|
||||||
pub reads_output: bool,
|
/// 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
|
/// The slot its primitives are positioned through: its own if its parent
|
||||||
/// placed it, otherwise the nearest ancestor that has one.
|
/// placed it, otherwise the nearest ancestor that has one.
|
||||||
pub move_idx: MoveIdx,
|
pub move_idx: MoveIdx,
|
||||||
|
|||||||
+87
-6
@@ -23,7 +23,10 @@ pub struct Painter<'a> {
|
|||||||
pub(super) children: Vec<WidgetId>,
|
pub(super) children: Vec<WidgetId>,
|
||||||
/// The children whose size this widget read while drawing.
|
/// The children whose size this widget read while drawing.
|
||||||
pub(super) size_deps: Vec<WidgetId>,
|
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
|
/// The slot this widget's primitives are positioned through: its own if
|
||||||
/// its parent placed it, otherwise the nearest ancestor that has one.
|
/// its parent placed it, otherwise the nearest ancestor that has one.
|
||||||
pub(super) move_idx: MoveIdx,
|
pub(super) move_idx: MoveIdx,
|
||||||
@@ -158,7 +161,7 @@ impl<'a> Painter<'a> {
|
|||||||
Some(hint) => {
|
Some(hint) => {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::HintHits);
|
diag::bump(Counter::HintHits);
|
||||||
self.depend_on_size(id);
|
self.depend_on_size(id, false);
|
||||||
Some(hint)
|
Some(hint)
|
||||||
}
|
}
|
||||||
None => {
|
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()) {
|
if !self.size_deps.contains(&child.id()) {
|
||||||
self.size_deps.push(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(
|
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's size in pixels. A widget that reads it draws again when
|
||||||
/// the output changes, since nothing else can put that right.
|
/// the output changes, since nothing else can put that right.
|
||||||
pub fn output_size(&mut self) -> Vec2 {
|
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
|
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
|
/// 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 boxes it sits within, so a widget that reads it draws again when
|
||||||
/// the output changes.
|
/// the output changes.
|
||||||
pub fn px_size(&mut self) -> Vec2 {
|
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);
|
let region = self.state.moves.resolve(self.move_idx, self.region);
|
||||||
region.size().to_abs(self.state.output_size)
|
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 {
|
pub fn text_data(&mut self) -> &mut TextData {
|
||||||
&mut self.rsc.ui_mut().text
|
&mut self.rsc.ui_mut().text
|
||||||
}
|
}
|
||||||
@@ -270,7 +351,7 @@ impl<W: ?Sized> DrawResult<'_, '_, W> {
|
|||||||
diag::bump(Counter::SizeReads);
|
diag::bump(Counter::SizeReads);
|
||||||
diag::size_read(self.child.id(), self.painter.id, self.size);
|
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
|
self.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+128
-19
@@ -7,6 +7,11 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
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 struct UiRenderState {
|
||||||
pub active: HashMap<WidgetId, ActiveData>,
|
pub active: HashMap<WidgetId, ActiveData>,
|
||||||
@@ -14,7 +19,14 @@ pub struct UiRenderState {
|
|||||||
pub(super) output_size: Vec2,
|
pub(super) output_size: Vec2,
|
||||||
|
|
||||||
old_root: Option<WidgetId>,
|
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>,
|
draw_started: HashSet<WidgetId>,
|
||||||
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
|
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
|
||||||
/// replaces that while its children go on pointing at the slot.
|
/// replaces that while its children go on pointing at the slot.
|
||||||
@@ -29,7 +41,9 @@ impl UiRenderState {
|
|||||||
layers: Default::default(),
|
layers: Default::default(),
|
||||||
output_size: Vec2::ZERO,
|
output_size: Vec2::ZERO,
|
||||||
old_root: None,
|
old_root: None,
|
||||||
resized: false,
|
resized: [false; 2],
|
||||||
|
invalid_sizes: Default::default(),
|
||||||
|
resize_marks: Default::default(),
|
||||||
draw_started: Default::default(),
|
draw_started: Default::default(),
|
||||||
slots: Default::default(),
|
slots: Default::default(),
|
||||||
moves: Default::default(),
|
moves: Default::default(),
|
||||||
@@ -38,10 +52,10 @@ impl UiRenderState {
|
|||||||
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||||
let size = size.into();
|
let size = size.into();
|
||||||
if size != self.output_size {
|
for (axis, resized) in AXES.into_iter().zip(self.resized.iter_mut()) {
|
||||||
self.output_size = size;
|
*resized |= size.axis(axis) != self.output_size.axis(axis);
|
||||||
self.resized = true;
|
|
||||||
}
|
}
|
||||||
|
self.output_size = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn output_size(&self) -> Vec2 {
|
pub fn output_size(&self) -> Vec2 {
|
||||||
@@ -53,6 +67,10 @@ impl UiRenderState {
|
|||||||
diag::bump(Counter::Updates);
|
diag::bump(Counter::Updates);
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
let _update = diag::timer(TimerKind::Update);
|
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
|
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
||||||
// decide whether to panic or not
|
// decide whether to panic or not
|
||||||
if !rsc.widgets().waiting.is_empty() {
|
if !rsc.widgets().waiting.is_empty() {
|
||||||
@@ -73,7 +91,7 @@ impl UiRenderState {
|
|||||||
if self.root_changed(root) {
|
if self.root_changed(root) {
|
||||||
self.redraw_all(root, rsc);
|
self.redraw_all(root, rsc);
|
||||||
self.old_root = root.map(|r| r.id());
|
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
|
// A region is a fraction of the output plus an offset, resolved
|
||||||
// against the window in the shader, so a resize moves the whole
|
// 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.
|
// drawing on its own. Only a widget that read pixels can be wrong.
|
||||||
@@ -83,7 +101,19 @@ impl UiRenderState {
|
|||||||
let dependents: Vec<_> = self
|
let dependents: Vec<_> = self
|
||||||
.active
|
.active
|
||||||
.iter()
|
.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();
|
.collect();
|
||||||
for id in dependents {
|
for id in dependents {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
@@ -93,12 +123,21 @@ impl UiRenderState {
|
|||||||
rsc.widgets_mut().needs_redraw.insert(top);
|
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() {
|
if rsc.widgets().has_updates() {
|
||||||
self.redraw_updates(rsc);
|
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) {
|
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
|
||||||
@@ -174,7 +213,9 @@ impl UiRenderState {
|
|||||||
primitives: Vec::new(),
|
primitives: Vec::new(),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
size_deps: 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,
|
move_idx,
|
||||||
rsc,
|
rsc,
|
||||||
};
|
};
|
||||||
@@ -199,6 +240,8 @@ impl UiRenderState {
|
|||||||
primitives,
|
primitives,
|
||||||
children,
|
children,
|
||||||
size_deps,
|
size_deps,
|
||||||
|
size_box_inputs,
|
||||||
|
size_output_inputs,
|
||||||
reads_output,
|
reads_output,
|
||||||
move_idx,
|
move_idx,
|
||||||
layer,
|
layer,
|
||||||
@@ -222,13 +265,15 @@ impl UiRenderState {
|
|||||||
primitives,
|
primitives,
|
||||||
children,
|
children,
|
||||||
size_deps,
|
size_deps,
|
||||||
|
size_box_inputs,
|
||||||
|
size_output_inputs,
|
||||||
|
output_px: self.output_size,
|
||||||
reads_output,
|
reads_output,
|
||||||
move_idx,
|
move_idx,
|
||||||
parent_move,
|
parent_move,
|
||||||
mask,
|
mask,
|
||||||
layer,
|
layer,
|
||||||
};
|
};
|
||||||
|
|
||||||
// remove old children that weren't kept
|
// remove old children that weren't kept
|
||||||
for c in &old_children {
|
for c in &old_children {
|
||||||
if !active.children.contains(c) {
|
if !active.children.contains(c) {
|
||||||
@@ -238,6 +283,8 @@ impl UiRenderState {
|
|||||||
|
|
||||||
rsc.on_draw(&active);
|
rsc.on_draw(&active);
|
||||||
self.active.insert(id, active);
|
self.active.insert(id, active);
|
||||||
|
self.invalid_sizes.remove(&id);
|
||||||
|
self.resize_marks.remove(&id);
|
||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +319,60 @@ impl UiRenderState {
|
|||||||
.to_abs(self.output_size)
|
.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
|
/// The drawing a widget already has, kept for a new box if the box has not
|
||||||
/// changed in a way it depends on.
|
/// changed in a way it depends on.
|
||||||
fn try_reuse(
|
fn try_reuse(
|
||||||
@@ -310,7 +411,7 @@ impl UiRenderState {
|
|||||||
let px = self.px_of(parent_move, region);
|
let px = self.px_of(parent_move, region);
|
||||||
let mut changed = [false; 2];
|
let mut changed = [false; 2];
|
||||||
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
|
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 {
|
if !changed.iter().any(|&c| c) && old_region == region {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
@@ -360,7 +461,6 @@ impl UiRenderState {
|
|||||||
self.moves.set(slot, region);
|
self.moves.set(slot, region);
|
||||||
let active = self.active.get_mut(&id).unwrap();
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
active.region = region;
|
active.region = region;
|
||||||
active.px = px;
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
{
|
{
|
||||||
diag::bump(Counter::ReuseMoved);
|
diag::bump(Counter::ReuseMoved);
|
||||||
@@ -472,6 +572,8 @@ impl UiRenderState {
|
|||||||
self.slots.clear();
|
self.slots.clear();
|
||||||
self.moves.clear();
|
self.moves.clear();
|
||||||
self.layers.clear();
|
self.layers.clear();
|
||||||
|
self.invalid_sizes.clear();
|
||||||
|
self.resize_marks.clear();
|
||||||
rsc.widgets_mut().needs_redraw.clear();
|
rsc.widgets_mut().needs_redraw.clear();
|
||||||
rsc.free();
|
rsc.free();
|
||||||
}
|
}
|
||||||
@@ -486,7 +588,7 @@ impl UiRenderState {
|
|||||||
// reader and gives each changing box its final constraints first.
|
// reader and gives each changing box its final constraints first.
|
||||||
while let Some(id) = {
|
while let Some(id) = {
|
||||||
let dirty = rsc.widgets().needs_redraw.iter().copied();
|
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)),
|
true => dirty.min_by_key(|&id| self.depth(id)),
|
||||||
false => dirty.max_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>>,
|
root: impl Into<Option<&'a StrongWidget>>,
|
||||||
widgets: &Widgets,
|
widgets: &Widgets,
|
||||||
) -> bool {
|
) -> 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 {
|
pub fn active_widgets(&self) -> usize {
|
||||||
@@ -556,16 +660,20 @@ impl UiRenderState {
|
|||||||
/// redraws a widget that's currently active (drawn)
|
/// redraws a widget that's currently active (drawn)
|
||||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
||||||
self.draw_started.remove(&id);
|
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
|
// 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
|
// box its parent chose. If that box changed in pixels, its retained
|
||||||
// placement is stale and the highest size reader must choose the new
|
// placement is stale and the highest size reader must choose the new
|
||||||
// box first. Otherwise the widget can draw locally, and its readers
|
// box first. Otherwise the widget can draw locally, and its readers
|
||||||
// only matter if the returned size actually changed.
|
// only matter if the returned size actually changed.
|
||||||
let box_changed = self
|
let box_changed = self.active.get(&id).is_some_and(|active| {
|
||||||
.active
|
let px = self.px_of(active.parent_move, active.region);
|
||||||
.get(&id)
|
AXES.into_iter()
|
||||||
.is_some_and(|active| self.px_of(active.parent_move, active.region) != active.px);
|
.any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis)))
|
||||||
if (self.resized || box_changed)
|
});
|
||||||
|
if (self.resized.iter().any(|&resized| resized) || box_changed)
|
||||||
&& let Some(top) = self.mark_readers(id, rsc)
|
&& let Some(top) = self.mark_readers(id, rsc)
|
||||||
{
|
{
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
@@ -611,6 +719,7 @@ impl UiRenderState {
|
|||||||
// Propagate one dependency edge at a time. If drawing the reader
|
// Propagate one dependency edge at a time. If drawing the reader
|
||||||
// does not change its own size, nothing above it can observe this.
|
// does not change its own size, nothing above it can observe this.
|
||||||
rsc.widgets_mut().needs_redraw.insert(parent);
|
rsc.widgets_mut().needs_redraw.insert(parent);
|
||||||
|
self.invalid_sizes.insert(parent);
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::ReaderEdges);
|
diag::bump(Counter::ReaderEdges);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,31 @@ pub struct Aligned {
|
|||||||
|
|
||||||
impl Widget for Aligned {
|
impl Widget for Aligned {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
// Drawn where it may be too big, then given its aligned box once its
|
let known = match self.align.tuple() {
|
||||||
// size is known.
|
(Some(_), Some(_)) => painter
|
||||||
let size = painter.place(&self.inner, UiRegion::FULL).size();
|
.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() {
|
let region = match self.align.tuple() {
|
||||||
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
|
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
|
||||||
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
|
(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, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
|
||||||
(None, None) => UiRegion::FULL,
|
(None, None) => UiRegion::FULL,
|
||||||
};
|
};
|
||||||
painter.place(&self.inner, region);
|
let placed = painter.place(&self.inner, region).size();
|
||||||
size
|
if had_size { placed } else { size }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The aligned box is a fraction of its own, so the child keeps its
|
/// 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 {
|
impl Widget for Scroll {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let output_len = painter.output_size().axis(self.axis);
|
let output_len = painter.output_len(self.axis);
|
||||||
let container_len = UiScalar::abs(painter.px_size().axis(self.axis));
|
let container_len = UiScalar::abs(painter.px_len(self.axis));
|
||||||
// Draw in the whole container to learn the content's length, then
|
// Draw in the whole container only when its scrolling-axis length is
|
||||||
// place it at the scrolled offset.
|
// not already known, then place it at the scrolled offset.
|
||||||
let child = painter.place(&self.inner, UiRegion::FULL).size();
|
let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL);
|
||||||
let content_len = child
|
let measured = known_len.is_none();
|
||||||
.axis(self.axis)
|
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()
|
.apply_rest()
|
||||||
.within_len(container_len)
|
.within_len(container_len)
|
||||||
.to_abs(output_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));
|
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);
|
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
||||||
painter.place(&self.inner, region);
|
let placed = painter.place(&self.inner, region).size();
|
||||||
child
|
child.unwrap_or(placed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,16 +15,14 @@ impl Widget for Span {
|
|||||||
let mut cursor = UiScalar::rel_min();
|
let mut cursor = UiScalar::rel_min();
|
||||||
let mut lens = Vec::with_capacity(self.children.len());
|
let mut lens = Vec::with_capacity(self.children.len());
|
||||||
for child in &self.children {
|
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,
|
Some(len) => len,
|
||||||
None => {
|
None => painter.place(child, region).len(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);
|
|
||||||
painter.place(child, region).len(axis)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
cursor.abs += len.abs + self.gap;
|
cursor.abs += len.abs + self.gap;
|
||||||
cursor.rel += len.rel;
|
cursor.rel += len.rel;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ impl TextView {
|
|||||||
|
|
||||||
fn render(&mut self, painter: &mut Painter) -> &RenderedText {
|
fn render(&mut self, painter: &mut Painter) -> &RenderedText {
|
||||||
let width = if self.attrs.wrap {
|
let width = if self.attrs.wrap {
|
||||||
Some(painter.px_size().x)
|
Some(painter.px_len(Axis::X))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-17
@@ -17,20 +17,11 @@ use iris::prelude::*;
|
|||||||
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow};
|
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow};
|
||||||
|
|
||||||
const DEPTH: usize = 4;
|
const DEPTH: usize = 4;
|
||||||
const SEEDS: [u64; 6] = [1, 2, 3, 5, 8, 13];
|
const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98];
|
||||||
const REGION_ULPS: u32 = 4;
|
const REGION_EPSILON_PX: f32 = 0.05;
|
||||||
|
|
||||||
fn ordered_bits(value: f32) -> u32 {
|
|
||||||
const SIGN: u32 = 1 << 31;
|
|
||||||
let bits = value.to_bits();
|
|
||||||
match bits & SIGN {
|
|
||||||
0 => bits | SIGN,
|
|
||||||
_ => !bits,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn same_coordinate(got: f32, want: f32) -> bool {
|
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 {
|
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() {
|
for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() {
|
||||||
let (got, want) = (wh.region(&w), ch.region(&c));
|
let (got, want) = (wh.region(&w), ch.region(&c));
|
||||||
drawn += usize::from(got.is_some());
|
drawn += usize::from(got.is_some());
|
||||||
// Equivalent composition orders can differ by a few f32 ULPs. Bound
|
// This oracle cares where rasterization lands, not whether equivalent
|
||||||
// that representation drift directly, while whether a widget drew
|
// arithmetic produced the same f32. Keep the tolerance to one
|
||||||
// remains exact.
|
// twentieth of a physical pixel, while whether a widget drew remains
|
||||||
|
// exact.
|
||||||
if same_region(got, want) {
|
if same_region(got, want) {
|
||||||
continue;
|
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 --
|
/// same defect, and it wants fixing where the two draws meet -- LAYOUT.md §4 --
|
||||||
/// rather than anywhere in the chain.
|
/// rather than anywhere in the chain.
|
||||||
#[test]
|
#[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() {
|
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);
|
changed_size(seed);
|
||||||
resized(seed);
|
resized(seed);
|
||||||
resized_then_changed(seed);
|
resized_then_changed(seed);
|
||||||
|
|||||||
@@ -75,6 +75,22 @@ fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|||||||
.unwrap_or(fallback)
|
.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) {
|
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
||||||
let mut harness = Harness::new(OUTPUT);
|
let mut harness = Harness::new(OUTPUT);
|
||||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
||||||
@@ -157,6 +173,7 @@ fn layout_cost() {
|
|||||||
"fixture: seed {seed}, depth {depth}, {} widgets",
|
"fixture: seed {seed}, depth {depth}, {} widgets",
|
||||||
tree.ids.len()
|
tree.ids.len()
|
||||||
);
|
);
|
||||||
|
trace_selected(&tree);
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
let _ = iris::core::layout_diagnostics::take();
|
let _ = iris::core::layout_diagnostics::take();
|
||||||
run("cold", 1, &mut harness, |_, _| {});
|
run("cold", 1, &mut harness, |_, _| {});
|
||||||
@@ -165,6 +182,7 @@ fn layout_cost() {
|
|||||||
|
|
||||||
if selected("repaint") {
|
if selected("repaint") {
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
let leaf = tree.ids[0];
|
let leaf = tree.ids[0];
|
||||||
run("repaint", frames, &mut harness, move |harness, _| {
|
run("repaint", frames, &mut harness, move |harness, _| {
|
||||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf);
|
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf);
|
||||||
@@ -173,6 +191,7 @@ fn layout_cost() {
|
|||||||
|
|
||||||
if selected("size") {
|
if selected("size") {
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
let sized = tree.sized[0];
|
let sized = tree.sized[0];
|
||||||
run("size", frames, &mut harness, move |harness, frame| {
|
run("size", frames, &mut harness, move |harness, frame| {
|
||||||
harness.rsc[sized].x = Some(Len::abs(100.0 + (frame % 2) as f32 * 40.0));
|
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") {
|
if selected("scroll") {
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
let scroll = tree.scrolls[0];
|
let scroll = tree.scrolls[0];
|
||||||
run("scroll", frames, &mut harness, move |harness, frame| {
|
run("scroll", frames, &mut harness, move |harness, frame| {
|
||||||
harness.rsc[scroll].scroll(if frame % 2 == 0 { 12.0 } else { -12.0 });
|
harness.rsc[scroll].scroll(if frame % 2 == 0 { 12.0 } else { -12.0 });
|
||||||
@@ -189,8 +209,9 @@ fn layout_cost() {
|
|||||||
|
|
||||||
if selected("resize") {
|
if selected("resize") {
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
run("resize", frames, &mut harness, |harness, frame| {
|
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);
|
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]
|
#[test]
|
||||||
fn a_resize_does_not_redraw_what_the_shader_can_move() {
|
fn a_resize_does_not_redraw_what_the_shader_can_move() {
|
||||||
let mut h = Harness::new((400, 200));
|
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);
|
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]
|
#[test]
|
||||||
fn reporting_the_same_output_size_does_not_start_a_resize() {
|
fn reporting_the_same_output_size_does_not_start_a_resize() {
|
||||||
let mut h = Harness::new((400, 200));
|
let mut h = Harness::new((400, 200));
|
||||||
|
|||||||
Reference in new issue
Block a user