Coalesce resize layout diagnostics
This commit is contained in:
1 parent
480f0bc99f
commit
82fa6c1123
7 files changed
+303
-15
No files matched your search
@@ -10,8 +10,12 @@
|
|||||||
//! added together. Use an uninstrumented build under `perf` for final CPU
|
//! added together. Use an uninstrumented build under `perf` for final CPU
|
||||||
//! totals; counting every primitive and distinct widget deliberately perturbs
|
//! totals; counting every primitive and distinct widget deliberately perturbs
|
||||||
//! the instrumented run.
|
//! the instrumented run.
|
||||||
|
//!
|
||||||
|
//! Call [`trace_widget`] before a frame to retain the ordered constraint,
|
||||||
|
//! reuse, size, placement, and text events for one suspicious widget. The
|
||||||
|
//! selection is a set and survives [`take`] until cleared.
|
||||||
|
|
||||||
use crate::WidgetId;
|
use crate::{Axis, Len, Size, UiRegion, WidgetId, util::Vec2};
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
@@ -119,6 +123,7 @@ pub struct Report {
|
|||||||
distinct_text_widgets: usize,
|
distinct_text_widgets: usize,
|
||||||
hot_widgets: Vec<Callsite>,
|
hot_widgets: Vec<Callsite>,
|
||||||
hot_text: Vec<Callsite>,
|
hot_text: Vec<Callsite>,
|
||||||
|
traces: Vec<TraceEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Report {
|
impl Default for Report {
|
||||||
@@ -130,6 +135,7 @@ impl Default for Report {
|
|||||||
distinct_text_widgets: 0,
|
distinct_text_widgets: 0,
|
||||||
hot_widgets: Vec::new(),
|
hot_widgets: Vec::new(),
|
||||||
hot_text: Vec::new(),
|
hot_text: Vec::new(),
|
||||||
|
traces: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,6 +166,11 @@ impl Report {
|
|||||||
&self.hot_text
|
&self.hot_text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ordered layout events for widgets selected with [`trace_widget`].
|
||||||
|
pub fn traces(&self) -> &[TraceEvent] {
|
||||||
|
&self.traces
|
||||||
|
}
|
||||||
|
|
||||||
/// Formats nonzero totals divided by `frames`.
|
/// Formats nonzero totals divided by `frames`.
|
||||||
pub fn per_frame(&self, frames: usize) -> String {
|
pub fn per_frame(&self, frames: usize) -> String {
|
||||||
let divisor = frames.max(1) as f64;
|
let divisor = frames.max(1) as f64;
|
||||||
@@ -211,6 +222,12 @@ impl Report {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !self.traces.is_empty() {
|
||||||
|
let _ = writeln!(out, " targeted layout trace:");
|
||||||
|
for event in &self.traces {
|
||||||
|
let _ = writeln!(out, " {event:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,6 +240,58 @@ pub struct Callsite {
|
|||||||
pub distinct_widths: usize,
|
pub distinct_widths: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum ReuseOutcome {
|
||||||
|
Exact,
|
||||||
|
Moved,
|
||||||
|
Dirty,
|
||||||
|
WrongParent,
|
||||||
|
Unslotted,
|
||||||
|
OwnResize,
|
||||||
|
DescendantResize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One targeted layout event. Events are retained in execution order, making
|
||||||
|
/// repeated constraint paths visible without logging every widget globally.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum TraceEvent {
|
||||||
|
DrawRequest {
|
||||||
|
id: WidgetId,
|
||||||
|
parent: Option<WidgetId>,
|
||||||
|
region: UiRegion,
|
||||||
|
pixel_size: Vec2,
|
||||||
|
slotted: bool,
|
||||||
|
},
|
||||||
|
Reuse {
|
||||||
|
id: WidgetId,
|
||||||
|
outcome: ReuseOutcome,
|
||||||
|
},
|
||||||
|
SizeReported {
|
||||||
|
id: WidgetId,
|
||||||
|
size: Size,
|
||||||
|
},
|
||||||
|
Placed {
|
||||||
|
id: WidgetId,
|
||||||
|
parent: WidgetId,
|
||||||
|
region: UiRegion,
|
||||||
|
},
|
||||||
|
SizeRead {
|
||||||
|
id: WidgetId,
|
||||||
|
reader: WidgetId,
|
||||||
|
size: Size,
|
||||||
|
},
|
||||||
|
HintRead {
|
||||||
|
id: WidgetId,
|
||||||
|
reader: WidgetId,
|
||||||
|
axis: Axis,
|
||||||
|
hint: Option<Len>,
|
||||||
|
},
|
||||||
|
TextRendered {
|
||||||
|
id: WidgetId,
|
||||||
|
width: Option<f32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Calls {
|
struct Calls {
|
||||||
label: String,
|
label: String,
|
||||||
@@ -235,6 +304,7 @@ struct Current {
|
|||||||
report: Report,
|
report: Report,
|
||||||
widgets: HashMap<WidgetId, Calls>,
|
widgets: HashMap<WidgetId, Calls>,
|
||||||
text_widgets: HashMap<WidgetId, Calls>,
|
text_widgets: HashMap<WidgetId, Calls>,
|
||||||
|
traced: HashSet<WidgetId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
@@ -255,6 +325,79 @@ pub(crate) fn draw_widget(id: WidgetId, label: &str) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a widget to the targeted trace set. Selection survives [`take`]
|
||||||
|
/// until explicitly removed or cleared.
|
||||||
|
pub fn trace_widget(id: impl Into<WidgetId>) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
current.traced.insert(id.into());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn untrace_widget(id: impl Into<WidgetId>) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
current.traced.remove(&id.into());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_traced_widgets() {
|
||||||
|
CURRENT.with_borrow_mut(|current| current.traced.clear());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trace(id: WidgetId, event: TraceEvent) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
if current.traced.contains(&id) {
|
||||||
|
current.report.traces.push(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn draw_request(
|
||||||
|
id: WidgetId,
|
||||||
|
parent: Option<WidgetId>,
|
||||||
|
region: UiRegion,
|
||||||
|
pixel_size: Vec2,
|
||||||
|
slotted: bool,
|
||||||
|
) {
|
||||||
|
trace(
|
||||||
|
id,
|
||||||
|
TraceEvent::DrawRequest {
|
||||||
|
id,
|
||||||
|
parent,
|
||||||
|
region,
|
||||||
|
pixel_size,
|
||||||
|
slotted,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
|
||||||
|
trace(id, TraceEvent::Reuse { id, outcome });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn size_reported(id: WidgetId, size: Size) {
|
||||||
|
trace(id, TraceEvent::SizeReported { id, size });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn placed(id: WidgetId, parent: WidgetId, region: UiRegion) {
|
||||||
|
trace(id, TraceEvent::Placed { id, parent, region });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn size_read(id: WidgetId, reader: WidgetId, size: Size) {
|
||||||
|
trace(id, TraceEvent::SizeRead { id, reader, size });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hint_read(id: WidgetId, reader: WidgetId, axis: Axis, hint: Option<Len>) {
|
||||||
|
trace(
|
||||||
|
id,
|
||||||
|
TraceEvent::HintRead {
|
||||||
|
id,
|
||||||
|
reader,
|
||||||
|
axis,
|
||||||
|
hint,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn render_text(id: WidgetId, label: &str, width: Option<f32>) {
|
pub(crate) fn render_text(id: WidgetId, label: &str, width: Option<f32>) {
|
||||||
CURRENT.with_borrow_mut(|current| {
|
CURRENT.with_borrow_mut(|current| {
|
||||||
let calls = current.text_widgets.entry(id).or_default();
|
let calls = current.text_widgets.entry(id).or_default();
|
||||||
@@ -263,6 +406,12 @@ pub(crate) fn render_text(id: WidgetId, label: &str, width: Option<f32>) {
|
|||||||
}
|
}
|
||||||
calls.count += 1;
|
calls.count += 1;
|
||||||
calls.widths.insert(width.map(f32::to_bits));
|
calls.widths.insert(width.map(f32::to_bits));
|
||||||
|
if current.traced.contains(&id) {
|
||||||
|
current
|
||||||
|
.report
|
||||||
|
.traces
|
||||||
|
.push(TraceEvent::TextRendered { id, width });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum Axis {
|
pub enum Axis {
|
||||||
X,
|
X,
|
||||||
Y,
|
Y,
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ impl<'a> Painter<'a> {
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::PlaceCalls);
|
diag::bump(Counter::PlaceCalls);
|
||||||
let region = region.within(&self.region);
|
let region = region.within(&self.region);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::placed(id.id(), self.id, region);
|
||||||
self.widget_at(id, region, true)
|
self.widget_at(id, region, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +152,8 @@ impl<'a> Painter<'a> {
|
|||||||
.widgets()
|
.widgets()
|
||||||
.get_dyn(id.id())
|
.get_dyn(id.id())
|
||||||
.and_then(|widget| widget.size_hint(axis));
|
.and_then(|widget| widget.size_hint(axis));
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::hint_read(id.id(), self.id, axis, hint);
|
||||||
match hint {
|
match hint {
|
||||||
Some(hint) => {
|
Some(hint) => {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
@@ -262,7 +266,10 @@ pub struct DrawResult<'p, 'a, W: ?Sized> {
|
|||||||
impl<W: ?Sized> DrawResult<'_, '_, W> {
|
impl<W: ?Sized> DrawResult<'_, '_, W> {
|
||||||
pub fn size(self) -> Size {
|
pub fn size(self) -> Size {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
{
|
||||||
diag::bump(Counter::SizeReads);
|
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);
|
||||||
self.size
|
self.size
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-12
@@ -1,5 +1,5 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
use crate::layout_diagnostics::{self as diag, Counter, TimerKind};
|
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
||||||
use crate::{
|
use crate::{
|
||||||
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion,
|
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion,
|
||||||
Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
|
Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
|
||||||
@@ -37,9 +37,12 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||||
self.output_size = size.into();
|
let size = size.into();
|
||||||
|
if size != self.output_size {
|
||||||
|
self.output_size = size;
|
||||||
self.resized = true;
|
self.resized = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn output_size(&self) -> Vec2 {
|
pub fn output_size(&self) -> Vec2 {
|
||||||
self.output_size
|
self.output_size
|
||||||
@@ -77,11 +80,17 @@ impl UiRenderState {
|
|||||||
{
|
{
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
let _marking = diag::timer(TimerKind::ResizeMarking);
|
let _marking = diag::timer(TimerKind::ResizeMarking);
|
||||||
for (&id, active) in &self.active {
|
let dependents: Vec<_> = self
|
||||||
if active.reads_output {
|
.active
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(&id, active)| active.reads_output.then_some(id))
|
||||||
|
.collect();
|
||||||
|
for id in dependents {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::ResizeDependents);
|
diag::bump(Counter::ResizeDependents);
|
||||||
rsc.widgets_mut().needs_redraw.insert(id);
|
rsc.widgets_mut().needs_redraw.insert(id);
|
||||||
|
if let Some(top) = self.mark_readers(id, rsc) {
|
||||||
|
rsc.widgets_mut().needs_redraw.insert(top);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +136,10 @@ impl UiRenderState {
|
|||||||
rsc: &mut dyn UiRsc,
|
rsc: &mut dyn UiRsc,
|
||||||
) -> Size {
|
) -> Size {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
{
|
||||||
diag::bump(Counter::DrawRequests);
|
diag::bump(Counter::DrawRequests);
|
||||||
|
diag::draw_request(id, parent, region, self.px_of(parent_move, region), slotted);
|
||||||
|
}
|
||||||
let mut old_children = old_children.unwrap_or_default();
|
let mut old_children = old_children.unwrap_or_default();
|
||||||
if self.active.contains_key(&id) {
|
if self.active.contains_key(&id) {
|
||||||
if let Some(size) = self.try_reuse(id, region, parent_move, rsc) {
|
if let Some(size) = self.try_reuse(id, region, parent_move, rsc) {
|
||||||
@@ -175,6 +187,8 @@ impl UiRenderState {
|
|||||||
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
||||||
let size = widget.draw(&mut painter);
|
let size = widget.draw(&mut painter);
|
||||||
drop(widget);
|
drop(widget);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::size_reported(id, size);
|
||||||
|
|
||||||
let Painter {
|
let Painter {
|
||||||
state: _,
|
state: _,
|
||||||
@@ -271,7 +285,10 @@ impl UiRenderState {
|
|||||||
diag::bump(Counter::ReuseAttempts);
|
diag::bump(Counter::ReuseAttempts);
|
||||||
if rsc.widgets().needs_redraw.contains(&id) {
|
if rsc.widgets().needs_redraw.contains(&id) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
{
|
||||||
diag::bump(Counter::ReuseDirty);
|
diag::bump(Counter::ReuseDirty);
|
||||||
|
diag::reuse(id, ReuseOutcome::Dirty);
|
||||||
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let active = self.active.get(&id)?;
|
let active = self.active.get(&id)?;
|
||||||
@@ -279,7 +296,10 @@ impl UiRenderState {
|
|||||||
// longer sits in, and its slot names the wrong parent.
|
// longer sits in, and its slot names the wrong parent.
|
||||||
if active.parent_move != parent_move {
|
if active.parent_move != parent_move {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
{
|
||||||
diag::bump(Counter::ReuseWrongParent);
|
diag::bump(Counter::ReuseWrongParent);
|
||||||
|
diag::reuse(id, ReuseOutcome::WrongParent);
|
||||||
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let (size, old_region, slot, old_px) =
|
let (size, old_region, slot, old_px) =
|
||||||
@@ -294,7 +314,10 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
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")]
|
||||||
|
{
|
||||||
diag::bump(Counter::ReuseExact);
|
diag::bump(Counter::ReuseExact);
|
||||||
|
diag::reuse(id, ReuseOutcome::Exact);
|
||||||
|
}
|
||||||
return Some(size);
|
return Some(size);
|
||||||
}
|
}
|
||||||
// Only a placed widget can be given a different box without drawing
|
// Only a placed widget can be given a different box without drawing
|
||||||
@@ -302,7 +325,10 @@ impl UiRenderState {
|
|||||||
// entry says where all of it went.
|
// entry says where all of it went.
|
||||||
if slot == parent_move {
|
if slot == parent_move {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
{
|
||||||
diag::bump(Counter::ReuseUnslotted);
|
diag::bump(Counter::ReuseUnslotted);
|
||||||
|
diag::reuse(id, ReuseOutcome::Unslotted);
|
||||||
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if changed.iter().any(|&c| c) {
|
if changed.iter().any(|&c| c) {
|
||||||
@@ -316,12 +342,18 @@ impl UiRenderState {
|
|||||||
// and has to lay out around what it comes to.
|
// and has to lay out around what it comes to.
|
||||||
if redraws {
|
if redraws {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
{
|
||||||
diag::bump(Counter::ReuseOwnResize);
|
diag::bump(Counter::ReuseOwnResize);
|
||||||
|
diag::reuse(id, ReuseOutcome::OwnResize);
|
||||||
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if self.redraws_under(id, changed, rsc) {
|
if self.redraws_under(id, changed, rsc) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
{
|
||||||
diag::bump(Counter::ReuseDescendantResize);
|
diag::bump(Counter::ReuseDescendantResize);
|
||||||
|
diag::reuse(id, ReuseOutcome::DescendantResize);
|
||||||
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,7 +362,10 @@ impl UiRenderState {
|
|||||||
active.region = region;
|
active.region = region;
|
||||||
active.px = px;
|
active.px = px;
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
{
|
||||||
diag::bump(Counter::ReuseMoved);
|
diag::bump(Counter::ReuseMoved);
|
||||||
|
diag::reuse(id, ReuseOutcome::Moved);
|
||||||
|
}
|
||||||
Some(size)
|
Some(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,14 +481,16 @@ impl UiRenderState {
|
|||||||
let _layout = diag::timer(TimerKind::IncrementalLayout);
|
let _layout = diag::timer(TimerKind::IncrementalLayout);
|
||||||
// A reader's answer is only valid after every dirty size it reads has
|
// A reader's answer is only valid after every dirty size it reads has
|
||||||
// settled. Equal-depth widgets are independent, so their order does
|
// settled. Equal-depth widgets are independent, so their order does
|
||||||
// not matter.
|
// not matter. Resize dirtiness already marks whole reader chains, so
|
||||||
while let Some(id) = rsc
|
// choosing their shallowest roots coalesces descendants that share a
|
||||||
.widgets()
|
// reader and gives each changing box its final constraints first.
|
||||||
.needs_redraw
|
while let Some(id) = {
|
||||||
.iter()
|
let dirty = rsc.widgets().needs_redraw.iter().copied();
|
||||||
.copied()
|
match self.resized {
|
||||||
.max_by_key(|&id| self.depth(id))
|
true => dirty.min_by_key(|&id| self.depth(id)),
|
||||||
{
|
false => dirty.max_by_key(|&id| self.depth(id)),
|
||||||
|
}
|
||||||
|
} {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
diag::bump(Counter::QueuePops);
|
diag::bump(Counter::QueuePops);
|
||||||
self.redraw(id, rsc);
|
self.redraw(id, rsc);
|
||||||
|
|||||||
+31
-1
@@ -18,6 +18,33 @@ 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; 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn same_coordinate(got: f32, want: f32) -> bool {
|
||||||
|
got == want || ordered_bits(got).abs_diff(ordered_bits(want)) <= REGION_ULPS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
||||||
|
match (got, want) {
|
||||||
|
(Some(got), Some(want)) => {
|
||||||
|
same_coordinate(got.top_left.x, want.top_left.x)
|
||||||
|
&& same_coordinate(got.top_left.y, want.top_left.y)
|
||||||
|
&& same_coordinate(got.bot_right.x, want.bot_right.x)
|
||||||
|
&& same_coordinate(got.bot_right.y, want.bot_right.y)
|
||||||
|
}
|
||||||
|
(None, None) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree {
|
fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree {
|
||||||
let (root, tree) = grow(&mut h.rsc, seed, DEPTH, edits);
|
let (root, tree) = grow(&mut h.rsc, seed, DEPTH, edits);
|
||||||
@@ -138,7 +165,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());
|
||||||
if got == want {
|
// Equivalent composition orders can differ by a few f32 ULPs. Bound
|
||||||
|
// that representation drift directly, while whether a widget drew
|
||||||
|
// remains exact.
|
||||||
|
if same_region(got, want) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
wrong += 1;
|
wrong += 1;
|
||||||
|
|||||||
@@ -21,6 +21,53 @@ use std::time::Instant;
|
|||||||
|
|
||||||
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
|
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
|
||||||
|
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
#[test]
|
||||||
|
fn a_selected_widget_retains_its_layout_events() {
|
||||||
|
use iris::core::layout_diagnostics::{self as diagnostics, TraceEvent};
|
||||||
|
|
||||||
|
diagnostics::clear_traced_widgets();
|
||||||
|
let _ = diagnostics::take();
|
||||||
|
let mut harness = Harness::new((400, 200));
|
||||||
|
let leaf = rect(Color::RED).add(&mut harness.rsc);
|
||||||
|
let other = rect(Color::BLUE).add(&mut harness.rsc);
|
||||||
|
let root = (leaf, other).span(Dir::RIGHT).add(&mut harness.rsc);
|
||||||
|
harness.set_root(root);
|
||||||
|
diagnostics::trace_widget(leaf.id());
|
||||||
|
let _ = diagnostics::take();
|
||||||
|
|
||||||
|
let _ = harness.rsc.widgets_mut().get_dyn_mut(root.id());
|
||||||
|
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf.id());
|
||||||
|
harness.frame();
|
||||||
|
|
||||||
|
let report = diagnostics::take();
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.traces()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, TraceEvent::Placed { id, .. } if *id == leaf.id()))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.traces()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, TraceEvent::DrawRequest { id, .. } if *id == leaf.id()))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.traces()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, TraceEvent::SizeRead { id, .. } if *id == leaf.id()))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.traces()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, TraceEvent::SizeReported { id, .. } if *id == leaf.id()))
|
||||||
|
);
|
||||||
|
diagnostics::clear_traced_widgets();
|
||||||
|
}
|
||||||
|
|
||||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||||
std::env::var(name)
|
std::env::var(name)
|
||||||
.ok()
|
.ok()
|
||||||
|
|||||||
@@ -227,6 +227,24 @@ fn a_resize_redraws_what_read_the_output() {
|
|||||||
assert_eq!(draws.get(), settled + 1);
|
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));
|
||||||
|
let draws = Rc::new(Cell::new(0));
|
||||||
|
let leaf = ReadsOutput {
|
||||||
|
draws: draws.clone(),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(leaf);
|
||||||
|
let settled = draws.get();
|
||||||
|
|
||||||
|
h.resize((400, 200));
|
||||||
|
|
||||||
|
assert!(!h.needs_redraw());
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(draws.get(), settled);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
|
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
|
||||||
let mut h = Harness::new((600, 400));
|
let mut h = Harness::new((600, 400));
|
||||||
|
|||||||
Reference in new issue
Block a user