Compare commits

..
Author SHA1 Message Date
iris-ai a123d13490 Merge remote-tracking branch 'upstream/main' into split/17-headless-rig 2026-09-14 02:48:37 -04:00
iris-ai deb9c1b6d7 Detect the rig's binaries, and take the machine out of its comments
The comments described the machine the rig was written on -- "this
machine has no display", "there is a real GPU here", an Android
emulator's GLX quirk -- which says nothing to anyone reading it from a
different checkout. What the reader needs is what the script supplies
and why, which is now all they get.

`sway`, `swaymsg` and, when `--shot` is passed, `grim` are checked up
front and named in the failure, rather than surfacing as a compositor
that would not start.

The `# shellcheck disable=SC2086 -- prose` directive did not parse, so
the suppression was not in effect; the prose moves to its own line.
Clean under shellcheck now.
2026-09-14 00:36:19 -04:00
iris 9d13f15bee Bring the headless rig into the repository
A rendering claim about iris was verified by hand from another checkout,
because the compositor script and the input replay lived in ai-app's
submodule and not here.

`scripts/run-headless.sh` starts a headless sway on its own socket, runs
an example against it and screenshots the result. `rig-input`'s
`replay-touch` drives a recorded gesture in through Wayland's virtual
pointer, since a headless compositor has no input device to move.

`iris::harness` gains the `.touch` parser, so a recording means the same
thing replayed into a harness as into a window rather than being read
twice by two parsers.

The ai-app copy's `--phone` became `--mode`, since which phone is not
iris's business; its `IRIS_SCALE` has nothing to hand a density to here,
so it waits for one.
2026-09-13 23:10:23 -04:00
52 changed files with 420 additions and 4948 deletions

No files matched your search

-3
View File
@@ -3,9 +3,6 @@ name = "iris"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
[features]
layout-diagnostics = ["iris-core/layout-diagnostics"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
-3
View File
@@ -3,9 +3,6 @@ name = "iris-core"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
[features]
layout-diagnostics = []
[dependencies] [dependencies]
wgpu = { workspace = true } wgpu = { workspace = true }
bytemuck ={ workspace = true } bytemuck ={ workspace = true }
-488
View File
@@ -1,488 +0,0 @@
//! Opt-in counters and coarse timers for explaining CPU layout cost.
//!
//! Enable the `layout-diagnostics` feature. With it disabled, none of the
//! instrumentation is compiled into Iris. The retained rig in
//! `tests/layout_diagnostics.rs` is the ordinary entry point.
//!
//! Timers are inclusive: `update total` contains `full layout` or
//! `incremental layout`, and `text render` contains shaping and glyph
//! placement. They locate cost within one instrumented run and must not be
//! added together. Use an uninstrumented build under `perf` for final CPU
//! totals; counting every primitive and distinct widget deliberately perturbs
//! 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::{Axis, Len, Size, UiRegion, WidgetId, util::Vec2};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
fmt::Write,
time::Instant,
};
#[derive(Clone, Copy)]
pub(crate) enum Counter {
Updates,
ResizeDependents,
DrawRequests,
WidgetDraws,
PlaceCalls,
SizeReads,
HintHits,
HintMisses,
RetainedSizeHits,
ReuseAttempts,
ReuseExact,
ReuseMoved,
ReuseDirty,
ReuseWrongParent,
ReuseUnslotted,
ReuseOwnResize,
ReuseDescendantResize,
ResizeChecks,
ResizeCheckChildren,
QueuePops,
DepthReads,
EagerReaderRedraws,
LocalRedraws,
SizeChanges,
ReaderEdges,
PrimitiveWrites,
TextRenders,
TextShapeHits,
TextShapes,
TextBreaks,
GlyphPlacements,
}
impl Counter {
const COUNT: usize = Self::GlyphPlacements as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [
"updates",
"resize dependents",
"draw requests",
"widget draws",
"place calls",
"draw-result size reads",
"hint hits",
"hint misses",
"retained size hits",
"reuse attempts",
"reuse exact",
"reuse moved",
"reuse: dirty",
"reuse: wrong parent",
"reuse: unslotted",
"reuse: own resize",
"reuse: descendant resize",
"resize checks",
"resize children checked",
"redraw queue pops",
"depth reads",
"eager reader redraws",
"local redraws",
"size changes",
"reader edges",
"primitive writes",
"text renders",
"text shape hits",
"text shapes",
"text line breaks",
"glyph placements",
];
}
#[derive(Clone, Copy)]
pub(crate) enum TimerKind {
Update,
FullLayout,
ResizeMarking,
IncrementalLayout,
TextRender,
TextShape,
TextBreak,
GlyphPlacement,
}
impl TimerKind {
const COUNT: usize = Self::GlyphPlacement as usize + 1;
const NAMES: [&'static str; Self::COUNT] = [
"update total",
"full layout",
"resize marking",
"incremental layout",
"text render",
"text shape",
"text line break",
"glyph placement",
];
}
#[derive(Clone)]
pub struct Report {
counters: [u64; Counter::COUNT],
nanos: [u64; TimerKind::COUNT],
distinct_widgets: usize,
distinct_text_widgets: usize,
hot_widgets: Vec<Callsite>,
hot_text: Vec<Callsite>,
traces: Vec<TraceEvent>,
}
impl Default for Report {
fn default() -> Self {
Self {
counters: [0; Counter::COUNT],
nanos: [0; TimerKind::COUNT],
distinct_widgets: 0,
distinct_text_widgets: 0,
hot_widgets: Vec::new(),
hot_text: Vec::new(),
traces: Vec::new(),
}
}
}
impl Report {
pub fn counters(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
Counter::NAMES.into_iter().zip(self.counters)
}
/// Inclusive elapsed time accumulated for each targeted operation.
pub fn timings_ns(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
TimerKind::NAMES.into_iter().zip(self.nanos)
}
pub fn distinct_widgets(&self) -> usize {
self.distinct_widgets
}
pub fn distinct_text_widgets(&self) -> usize {
self.distinct_text_widgets
}
pub fn hot_widgets(&self) -> &[Callsite] {
&self.hot_widgets
}
pub fn hot_text(&self) -> &[Callsite] {
&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`.
pub fn per_frame(&self, frames: usize) -> String {
let divisor = frames.max(1) as f64;
let mut out = String::new();
for (name, value) in self.counters() {
if value != 0 {
let _ = writeln!(out, " {name:<27} {:>12.2}", value as f64 / divisor);
}
}
if self.distinct_widgets != 0 {
let _ = writeln!(
out,
" {:<27} {:>12}",
"distinct widgets", self.distinct_widgets
);
}
if self.distinct_text_widgets != 0 {
let _ = writeln!(
out,
" {:<27} {:>12}",
"distinct text widgets", self.distinct_text_widgets
);
}
for (name, nanos) in self.timings_ns() {
if nanos != 0 {
let ms = nanos as f64 / divisor / 1_000_000.0;
let _ = writeln!(out, " {name:<27} {ms:>12.3} ms");
}
}
if !self.hot_widgets.is_empty() {
let _ = writeln!(out, " hottest widget draws:");
for callsite in &self.hot_widgets {
let calls = callsite.calls as f64 / divisor;
let _ = writeln!(
out,
" {calls:>9.2} {:?} {}",
callsite.id, callsite.label
);
}
}
if !self.hot_text.is_empty() {
let _ = writeln!(out, " hottest text renders:");
for callsite in &self.hot_text {
let calls = callsite.calls as f64 / divisor;
let _ = writeln!(
out,
" {calls:>9.2} {:>3} widths {:?} {}",
callsite.distinct_widths, callsite.id, callsite.label
);
}
}
if !self.traces.is_empty() {
let _ = writeln!(out, " targeted layout trace:");
for event in &self.traces {
let _ = writeln!(out, " {event:?}");
}
}
out
}
}
#[derive(Clone)]
pub struct Callsite {
pub id: WidgetId,
pub label: String,
pub calls: u64,
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)]
struct Calls {
label: String,
count: u64,
widths: HashSet<Option<u32>>,
}
#[derive(Default)]
struct Current {
report: Report,
widgets: HashMap<WidgetId, Calls>,
text_widgets: HashMap<WidgetId, Calls>,
traced: HashSet<WidgetId>,
}
thread_local! {
static CURRENT: RefCell<Current> = RefCell::new(Current::default());
}
pub(crate) fn bump(counter: Counter) {
CURRENT.with_borrow_mut(|current| current.report.counters[counter as usize] += 1);
}
pub(crate) fn draw_widget(id: WidgetId, label: &str) {
CURRENT.with_borrow_mut(|current| {
let calls = current.widgets.entry(id).or_default();
if calls.label.is_empty() {
calls.label = label.to_owned();
}
calls.count += 1;
});
}
/// 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>) {
CURRENT.with_borrow_mut(|current| {
let calls = current.text_widgets.entry(id).or_default();
if calls.label.is_empty() {
calls.label = label.to_owned();
}
calls.count += 1;
calls.widths.insert(width.map(f32::to_bits));
if current.traced.contains(&id) {
current
.report
.traces
.push(TraceEvent::TextRendered { id, width });
}
});
}
pub(crate) struct Timer {
kind: TimerKind,
start: Instant,
}
pub(crate) fn timer(kind: TimerKind) -> Timer {
Timer {
kind,
start: Instant::now(),
}
}
impl Drop for Timer {
fn drop(&mut self) {
let nanos = self.start.elapsed().as_nanos().min(u64::MAX as u128) as u64;
CURRENT.with_borrow_mut(|current| current.report.nanos[self.kind as usize] += nanos);
}
}
/// Takes all diagnostics accumulated on this thread and resets them.
pub fn take() -> Report {
CURRENT.with_borrow_mut(|current| {
current.report.distinct_widgets = current.widgets.len();
current.report.distinct_text_widgets = current.text_widgets.len();
current.report.hot_widgets = hottest(&current.widgets);
current.report.hot_text = hottest(&current.text_widgets);
let report = std::mem::take(&mut current.report);
current.widgets.clear();
current.text_widgets.clear();
report
})
}
fn hottest(calls: &HashMap<WidgetId, Calls>) -> Vec<Callsite> {
let mut calls: Vec<_> = calls
.iter()
.map(|(&id, calls)| Callsite {
id,
label: calls.label.clone(),
calls: calls.count,
distinct_widths: calls.widths.len(),
})
.collect();
calls.sort_by(|a, b| b.calls.cmp(&a.calls).then_with(|| a.label.cmp(&b.label)));
calls.truncate(8);
calls
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn taking_a_report_resets_its_counters() {
let _ = take();
bump(Counter::Updates);
bump(Counter::Updates);
let report = take();
assert_eq!(report.counters().next(), Some(("updates", 2)));
assert!(take().counters().all(|(_, count)| count == 0));
}
}
-3
View File
@@ -10,9 +10,6 @@
#![feature(coerce_unsized)] #![feature(coerce_unsized)]
#![feature(option_into_flat_iter)] #![feature(option_into_flat_iter)]
#[cfg(feature = "layout-diagnostics")]
pub mod layout_diagnostics;
mod attr; mod attr;
mod event; mod event;
mod num; mod num;
+2 -2
View File
@@ -144,10 +144,10 @@ impl UiScalar {
pub const fn align(&self, align: AxisAlign) -> UiSpan { pub const fn align(&self, align: AxisAlign) -> UiSpan {
let rel = align.rel(); let rel = align.rel();
let mut start = UiScalar::rel(rel); let mut start = UiScalar::rel(rel);
start.px -= self.px * rel; start.abs -= self.abs * rel;
start.rel -= self.rel * rel; start.rel -= self.rel * rel;
let mut end = UiScalar::rel(rel); let mut end = UiScalar::rel(rel);
end.px += self.px * (1.0 - rel); end.abs += self.abs * (1.0 - rel);
end.rel += self.rel * (1.0 - rel); end.rel += self.rel * (1.0 - rel);
UiSpan { start, end } UiSpan { start, end }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
use super::*; use super::*;
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Eq, PartialEq)]
pub enum Axis { pub enum Axis {
X, X,
Y, Y,
+20 -20
View File
@@ -9,14 +9,14 @@ pub struct Size {
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Len { pub struct Len {
pub px: f32, pub abs: f32,
pub rel: f32, pub rel: f32,
pub rest: f32, pub rest: f32,
} }
impl<N: UiNum> From<N> for Len { impl<N: UiNum> From<N> for Len {
fn from(value: N) -> Self { fn from(value: N) -> Self {
Len::px(value.to_f32()) Len::abs(value.to_f32())
} }
} }
@@ -46,10 +46,10 @@ impl Size {
y: Len::REST, y: Len::REST,
}; };
pub fn px(v: Vec2) -> Self { pub fn abs(v: Vec2) -> Self {
Self { Self {
x: Len::px(v.x), x: Len::abs(v.x),
y: Len::px(v.y), y: Len::abs(v.y),
} }
} }
@@ -97,13 +97,13 @@ impl Size {
impl Len { impl Len {
pub const ZERO: Self = Self { pub const ZERO: Self = Self {
px: 0.0, abs: 0.0,
rel: 0.0, rel: 0.0,
rest: 0.0, rest: 0.0,
}; };
pub const REST: Self = Self { pub const REST: Self = Self {
px: 0.0, abs: 0.0,
rel: 0.0, rel: 0.0,
rest: 1.0, rest: 1.0,
}; };
@@ -111,27 +111,27 @@ impl Len {
pub fn apply_rest(&self) -> UiScalar { pub fn apply_rest(&self) -> UiScalar {
UiScalar { UiScalar {
rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 }, rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
px: self.px, abs: self.abs,
} }
} }
pub fn px(px: impl UiNum) -> Self { pub fn abs(abs: impl UiNum) -> Self {
Self { Self {
px: px.to_f32(), abs: abs.to_f32(),
rel: 0.0, rel: 0.0,
rest: 0.0, rest: 0.0,
} }
} }
pub fn rel(rel: impl UiNum) -> Self { pub fn rel(rel: impl UiNum) -> Self {
Self { Self {
px: 0.0, abs: 0.0,
rel: rel.to_f32(), rel: rel.to_f32(),
rest: 0.0, rest: 0.0,
} }
} }
pub fn rest(ratio: impl UiNum) -> Self { pub fn rest(ratio: impl UiNum) -> Self {
Self { Self {
px: 0.0, abs: 0.0,
rel: 0.0, rel: 0.0,
rest: ratio.to_f32(), rest: ratio.to_f32(),
} }
@@ -141,31 +141,31 @@ impl Len {
pub mod len_fns { pub mod len_fns {
use super::*; use super::*;
pub fn px(px: impl UiNum) -> Len { pub fn abs(abs: impl UiNum) -> Len {
Len { Len {
px: px.to_f32(), abs: abs.to_f32(),
rel: 0.0, rel: 0.0,
rest: 0.0, rest: 0.0,
} }
} }
pub fn rel(rel: impl UiNum) -> Len { pub fn rel(rel: impl UiNum) -> Len {
Len { Len {
px: 0.0, abs: 0.0,
rel: rel.to_f32(), rel: rel.to_f32(),
rest: 0.0, rest: 0.0,
} }
} }
pub fn rest(ratio: impl UiNum) -> Len { pub fn rest(ratio: impl UiNum) -> Len {
Len { Len {
px: 0.0, abs: 0.0,
rel: 0.0, rel: 0.0,
rest: ratio.to_f32(), rest: ratio.to_f32(),
} }
} }
} }
impl_op!(Len Add add; px rel rest); impl_op!(Len Add add; abs rel rest);
impl_op!(Len Sub sub; px rel rest); impl_op!(Len Sub sub; abs rel rest);
impl_op!(Size Add add; x y); impl_op!(Size Add add; x y);
impl_op!(Size Sub sub; x y); impl_op!(Size Sub sub; x y);
@@ -184,8 +184,8 @@ impl std::fmt::Display for Size {
impl std::fmt::Display for Len { impl std::fmt::Display for Len {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.px != 0.0 { if self.abs != 0.0 {
write!(f, "{} px;", self.px)?; write!(f, "{} abs;", self.abs)?;
} }
if self.rel != 0.0 { if self.rel != 0.0 {
write!(f, "{} rel;", self.rel)?; write!(f, "{} rel;", self.rel)?;
+97 -38
View File
@@ -23,11 +23,11 @@ impl UiVec2 {
Self { x, y } Self { x, y }
} }
pub const fn px(px: impl const Into<Vec2>) -> Self { pub const fn abs(abs: impl const Into<Vec2>) -> Self {
let px = px.into(); let abs = abs.into();
Self { Self {
x: UiScalar::px(px.x), x: UiScalar::abs(abs.x),
y: UiScalar::px(px.y), y: UiScalar::abs(abs.y),
} }
} }
@@ -70,10 +70,10 @@ impl UiVec2 {
} }
} }
pub fn to_px(&self, rel: Vec2) -> Vec2 { pub fn to_abs(&self, rel: Vec2) -> Vec2 {
Vec2 { Vec2 {
x: self.x.to_px(rel.x), x: self.x.to_abs(rel.x),
y: self.y.to_px(rel.y), y: self.y.to_abs(rel.y),
} }
} }
@@ -92,8 +92,8 @@ impl UiVec2 {
} }
} }
pub fn get_px(&self) -> Vec2 { pub fn get_abs(&self) -> Vec2 {
(self.x.px, self.y.px).into() (self.x.abs, self.y.abs).into()
} }
pub fn get_rel(&self) -> Vec2 { pub fn get_rel(&self) -> Vec2 {
@@ -102,15 +102,15 @@ impl UiVec2 {
pub fn abs_mut(&mut self) -> Vec2View<'_> { pub fn abs_mut(&mut self) -> Vec2View<'_> {
Vec2View { Vec2View {
x: &mut self.x.px, x: &mut self.x.abs,
y: &mut self.y.px, y: &mut self.y.abs,
} }
} }
} }
impl Display for UiVec2 { impl Display for UiVec2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "rel{};px{}", self.get_rel(), self.get_px()) write!(f, "rel{};abs{}", self.get_rel(), self.get_abs())
} }
} }
@@ -118,8 +118,8 @@ impl_op!(UiVec2 Add add; x y);
impl_op!(UiVec2 Sub sub; x y); impl_op!(UiVec2 Sub sub; x y);
const impl From<Vec2> for UiVec2 { const impl From<Vec2> for UiVec2 {
fn from(px: Vec2) -> Self { fn from(abs: Vec2) -> Self {
Self::px(px) Self::abs(abs)
} }
} }
@@ -127,8 +127,8 @@ const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
where where
(T, U): const Destruct, (T, U): const Destruct,
{ {
fn from(px: (T, U)) -> Self { fn from(abs: (T, U)) -> Self {
Self::px(px) Self::abs(abs)
} }
} }
@@ -136,34 +136,34 @@ where
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
pub struct UiScalar { pub struct UiScalar {
pub rel: f32, pub rel: f32,
pub px: f32, pub abs: f32,
} }
impl Eq for UiScalar {} impl Eq for UiScalar {}
impl Hash for UiScalar { impl Hash for UiScalar {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write_u32(self.rel.to_bits()); state.write_u32(self.rel.to_bits());
state.write_u32(self.px.to_bits()); state.write_u32(self.abs.to_bits());
} }
} }
impl_op!(UiScalar Add add; rel px); impl_op!(UiScalar Add add; rel abs);
impl_op!(UiScalar Sub sub; rel px); impl_op!(UiScalar Sub sub; rel abs);
impl UiScalar { impl UiScalar {
pub const ZERO: Self = Self { rel: 0.0, px: 0.0 }; pub const ZERO: Self = Self { rel: 0.0, abs: 0.0 };
pub const FULL: Self = Self { rel: 1.0, px: 0.0 }; pub const FULL: Self = Self { rel: 1.0, abs: 0.0 };
pub const fn new(rel: f32, px: f32) -> Self { pub const fn new(rel: f32, abs: f32) -> Self {
Self { rel, px } Self { rel, abs }
} }
pub const fn rel(rel: f32) -> Self { pub const fn rel(rel: f32) -> Self {
Self { rel, px: 0.0 } Self { rel, abs: 0.0 }
} }
pub const fn px(px: f32) -> Self { pub const fn abs(abs: f32) -> Self {
Self { rel: 0.0, px } Self { rel: 0.0, abs }
} }
pub const fn rel_min() -> Self { pub const fn rel_min() -> Self {
@@ -177,31 +177,39 @@ impl UiScalar {
pub const fn max(&self, other: Self) -> Self { pub const fn max(&self, other: Self) -> Self {
Self { Self {
rel: self.rel.max(other.rel), rel: self.rel.max(other.rel),
px: self.px.max(other.px), abs: self.abs.max(other.abs),
} }
} }
pub const fn min(&self, other: Self) -> Self { pub const fn min(&self, other: Self) -> Self {
Self { Self {
rel: self.rel.min(other.rel), rel: self.rel.min(other.rel),
px: self.px.min(other.px), abs: self.abs.min(other.abs),
} }
} }
pub const fn offset(mut self, amt: f32) -> Self { pub const fn offset(mut self, amt: f32) -> Self {
self.px += amt; self.abs += amt;
self self
} }
pub const fn within(&self, span: &UiSpan) -> Self { pub const fn within(&self, span: &UiSpan) -> Self {
let anchor = self.rel.lerp(span.start.rel, span.end.rel); let anchor = self.rel.lerp(span.start.rel, span.end.rel);
let offset = self.px + self.rel.lerp(span.start.px, span.end.px); let offset = self.abs + self.rel.lerp(span.start.abs, span.end.abs);
Self { Self {
rel: anchor, rel: anchor,
px: offset, abs: offset,
} }
} }
/// Undoes `within`, and `None` where the span has a fixed length: every
/// fraction of it lands on the same `rel`, so none can be told apart.
pub fn outside(&self, span: &UiSpan) -> Option<Self> {
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel)?;
let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
Some(Self { rel, abs })
}
pub fn within_len(&self, len: UiScalar) -> Self { pub fn within_len(&self, len: UiScalar) -> Self {
self.within(&UiSpan { self.within(&UiSpan {
start: UiScalar::ZERO, start: UiScalar::ZERO,
@@ -215,15 +223,15 @@ impl UiScalar {
pub const fn flip(&mut self) { pub const fn flip(&mut self) {
self.rel = 1.0 - self.rel; self.rel = 1.0 - self.rel;
self.px = -self.px; self.abs = -self.abs;
} }
pub const fn to(&self, end: Self) -> UiSpan { pub const fn to(&self, end: Self) -> UiSpan {
UiSpan { start: *self, end } UiSpan { start: *self, end }
} }
pub const fn to_px(&self, rel: f32) -> f32 { pub const fn to_abs(&self, rel: f32) -> f32 {
self.rel * rel + self.px self.rel * rel + self.abs
} }
} }
@@ -255,7 +263,7 @@ impl UiSpan {
self.start.flip(); self.start.flip();
self.end.flip(); self.end.flip();
std::mem::swap(&mut self.start.rel, &mut self.end.rel); std::mem::swap(&mut self.start.rel, &mut self.end.rel);
std::mem::swap(&mut self.start.px, &mut self.end.px); std::mem::swap(&mut self.start.abs, &mut self.end.abs);
} }
pub const fn shift(&mut self, offset: UiScalar) { pub const fn shift(&mut self, offset: UiScalar) {
@@ -270,6 +278,13 @@ impl UiSpan {
} }
} }
pub fn outside(&self, parent: &Self) -> Option<Self> {
Some(Self {
start: self.start.outside(parent)?,
end: self.end.outside(parent)?,
})
}
pub const fn len(&self) -> UiScalar { pub const fn len(&self) -> UiScalar {
self.end - self.start self.end - self.start
} }
@@ -338,8 +353,8 @@ impl UiRegion {
pub fn to_px(&self, size: Vec2) -> PixelRegion { pub fn to_px(&self, size: Vec2) -> PixelRegion {
PixelRegion { PixelRegion {
top_left: self.top_left().get_rel() * size + self.top_left().get_px(), top_left: self.top_left().get_rel() * size + self.top_left().get_abs(),
bot_right: self.bot_right().get_rel() * size + self.bot_right().get_px(), bot_right: self.bot_right().get_rel() * size + self.bot_right().get_abs(),
} }
} }
@@ -382,6 +397,50 @@ impl UiRegion {
} }
} }
/// Taking a drawing out of one box and putting it in another, checked once
/// for a whole subtree so that applying it cannot fail.
///
/// A box of a fixed length holds each part as an offset from its start rather
/// than as a fraction of it, so those parts can be carried to a box of the
/// same length but never stretched to a different one.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Remap {
from: UiRegion,
to: UiRegion,
}
impl Remap {
pub fn new(from: UiRegion, to: UiRegion) -> Option<Self> {
[Axis::X, Axis::Y]
.into_iter()
.all(|axis| {
let (from, to) = (from.axis(axis), to.axis(axis));
from.start.rel != from.end.rel || from.len() == to.len()
})
.then_some(Self { from, to })
}
pub fn apply(&self, region: UiRegion) -> UiRegion {
UiRegion {
x: Self::span(region.x, self.from.x, self.to.x),
y: Self::span(region.y, self.from.y, self.to.y),
}
}
fn span(span: UiSpan, from: UiSpan, to: UiSpan) -> UiSpan {
match span.outside(&from) {
Some(out) => out.within(&to),
// `new` admits this only where the two are the same length, so
// the difference between their starts is the whole move.
None => {
let mut span = span;
span.shift(to.start - from.start);
span
}
}
}
}
impl Display for UiRegion { impl Display for UiRegion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
+11 -154
View File
@@ -1,5 +1,3 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, TimerKind};
use crate::{ use crate::{
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2, Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
}; };
@@ -7,10 +5,7 @@ use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
}; };
use std::{ use std::hash::{DefaultHasher, Hash, Hasher};
collections::VecDeque,
hash::{DefaultHasher, Hash, Hasher},
};
use swash::{ use swash::{
FontRef, FontRef,
scale::{Render, ScaleContext, Source, StrikeWith}, scale::{Render, ScaleContext, Source, StrikeWith},
@@ -22,32 +17,8 @@ pub struct TextData {
pub layout_ctx: LayoutContext<UiColor>, pub layout_ctx: LayoutContext<UiColor>,
scale_ctx: ScaleContext, scale_ctx: ScaleContext,
pub atlas: GlyphAtlas, pub atlas: GlyphAtlas,
spare: VecDeque<Placed>,
} }
/// The glyphs of one text at one width. A buffer holds the ones it is drawn
/// as; these are the ones it had before, kept because a container measures a
/// child by drawing it in a box it may not keep, and so comes back to widths
/// it has already asked for.
struct Placed {
/// Where the glyphs land is a function of these three and nothing else,
/// so no widget or buffer identity is involved and two texts of the same
/// words share an answer.
text: String,
key: LayoutKey,
glyphs: RenderedText,
}
/// How many to keep. Bounding the whole store rather than each buffer is what
/// makes this a fixed cost instead of one a tree of ten thousand texts pays
/// ten thousand times; the re-asks come from laying out one subtree, so they
/// are close together and few are needed. Instructions over 500 resize frames
/// of `tests/revision_cost.rs`, both the repeating widths and the sweep that
/// cannot hit across frames: 13.7B at 32, 12.1B at 64, 10.4B and 12.1B at 128,
/// and nothing past that -- so 128, which is no worse in the case that never
/// repeats and better in the one that does.
const SPARE_PLACED: usize = 128;
impl Default for TextData { impl Default for TextData {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -55,7 +26,6 @@ impl Default for TextData {
layout_ctx: LayoutContext::new(), layout_ctx: LayoutContext::new(),
scale_ctx: ScaleContext::new(), scale_ctx: ScaleContext::new(),
atlas: GlyphAtlas::default(), atlas: GlyphAtlas::default(),
spare: VecDeque::new(),
} }
} }
} }
@@ -106,21 +76,11 @@ impl Default for TextAttrs {
} }
} }
/// How far below the longest line a width may fall and still be answered by
/// the break in hand. A parent that offers a child the length it reported
/// composes that length back through the box chain, so the two differ in the
/// last bits -- and at exactly the longest line, that decides whether a line
/// fits. Sub-pixel, so no break it admits is one a reader could see.
const BREAK_EPSILON_PX: f32 = 0.05;
/// Keeps text and its corresponding layout from getting out of sync. /// Keeps text and its corresponding layout from getting out of sync.
pub struct TextBuffer { pub struct TextBuffer {
text: String, text: String,
layout: Layout<UiColor>, layout: Layout<UiColor>,
layout_key: Option<LayoutKey>, layout_key: Option<LayoutKey>,
/// The glyphs placed from `layout`, so drawing this text again at the
/// width it already has places them once.
placed: Option<RenderedText>,
} }
#[derive(PartialEq)] #[derive(PartialEq)]
@@ -135,7 +95,6 @@ impl TextBuffer {
text: text.into(), text: text.into(),
layout: Layout::new(), layout: Layout::new(),
layout_key: None, layout_key: None,
placed: None,
} }
} }
@@ -160,28 +119,15 @@ impl TextBuffer {
if text != self.text { if text != self.text {
self.text = text; self.text = text;
self.layout_key = None; self.layout_key = None;
self.placed = None;
} }
} }
/// Invalidates the layout and returns the underlying string for editing. /// Invalidates the layout and returns the underlying string for editing.
pub fn edit(&mut self) -> &mut String { pub fn edit(&mut self) -> &mut String {
self.layout_key = None; self.layout_key = None;
self.placed = None;
&mut self.text &mut self.text
} }
/// The glyphs of the shaping it is drawn as, once they are placed.
pub fn rendered(&self) -> Option<&RenderedText> {
self.placed.as_ref()
}
/// The width its shaping wraps at, and `None` where it does not wrap or
/// has not been shaped.
pub fn wrap_width(&self) -> Option<f32> {
self.layout_key.as_ref()?.max_width
}
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
Vec2::new(self.layout.width(), self.layout.height()) Vec2::new(self.layout.width(), self.layout.height())
} }
@@ -192,58 +138,8 @@ impl TextBuffer {
max_width: width, max_width: width,
}; };
if self.layout_key.as_ref() == Some(&layout_key) { if self.layout_key.as_ref() == Some(&layout_key) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapeHits);
return; return;
} }
// A greedy break at one width is the same break at every width down
// to the longest line it produced: each line still fits, and none can
// take a word that would not fit in the wider box. So the layout in
// hand already answers, and re-breaking would only be a chance to
// disagree with itself -- which is what happens when a parent offers
// a child the length that child just reported, and the two land
// either side of a float.
if let Some(key) = &self.layout_key
&& key.attrs == *attrs
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
&& want <= broke_at
&& want + BREAK_EPSILON_PX >= self.layout.width()
{
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapeHits);
return;
}
let same_shaping = self
.layout_key
.as_ref()
.is_some_and(|key| key.attrs == *attrs);
let old_key = self.layout_key.replace(layout_key);
// The glyphs it holds are of the width it held, which the layout may
// well come back to.
if let Some(key) = old_key
&& let Some(glyphs) = self.placed.take()
{
data.keep_placed(Placed {
text: self.text.clone(),
key,
glyphs,
});
}
// Only the line breaking depends on the width: the shaped runs under
// it are a function of the text and the attrs, and parley re-breaks
// them in place. So a new width is a break, not a shaping.
if same_shaping {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextBreaks);
#[cfg(feature = "layout-diagnostics")]
let _break = diag::timer(TimerKind::TextBreak);
self.break_lines(width);
return;
}
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextShapes);
#[cfg(feature = "layout-diagnostics")]
let _shape = diag::timer(TimerKind::TextShape);
let mut builder = data let mut builder = data
.layout_ctx .layout_ctx
.ranged_builder(&mut data.font_ctx, &self.text, 1.0, true); .ranged_builder(&mut data.font_ctx, &self.text, 1.0, true);
@@ -254,13 +150,10 @@ impl TextBuffer {
))); )));
builder.push_default(StyleProperty::Brush(attrs.color)); builder.push_default(StyleProperty::Brush(attrs.color));
builder.build_into(&mut self.layout, &self.text); builder.build_into(&mut self.layout, &self.text);
self.break_lines(width);
}
fn break_lines(&mut self, width: Option<f32>) {
self.layout.break_all_lines(width); self.layout.break_all_lines(width);
self.layout self.layout
.align(Alignment::Start, AlignmentOptions::default()); .align(Alignment::Start, AlignmentOptions::default());
self.layout_key = Some(layout_key);
} }
} }
@@ -372,54 +265,18 @@ pub struct RenderedText {
} }
impl TextData { impl TextData {
/// The glyphs of this text at this width, taken out of what is kept. pub fn render(
fn take_placed(&mut self, text: &str, key: &LayoutKey) -> Option<RenderedText> {
// From the newest, since a re-ask is usually of something recent.
let at = self
.spare
.iter()
.rposition(|spare| spare.key == *key && spare.text == text)?;
self.spare.remove(at).map(|spare| spare.glyphs)
}
fn keep_placed(&mut self, placed: Placed) {
if self.spare.len() >= SPARE_PLACED {
self.spare.pop_front();
}
self.spare.push_back(placed);
}
pub fn render<'b>(
&mut self, &mut self,
buffer: &'b mut TextBuffer, buffer: &mut TextBuffer,
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>, width: Option<f32>,
) -> &'b RenderedText { ) -> RenderedText {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::TextRenders);
#[cfg(feature = "layout-diagnostics")]
let _render = diag::timer(TimerKind::TextRender);
buffer.shape(self, attrs, width); buffer.shape(self, attrs, width);
// Only asked for when the buffer no longer holds them: taking one out let glyphs = self.place(buffer);
// of the store to then drop it would throw an answer away. RenderedText {
let placed = buffer.placed.take().or_else(|| { glyphs,
let key = buffer.layout_key.as_ref()?; size: buffer.size(),
self.take_placed(&buffer.text, key) color: attrs.color,
}); }
let placed = match placed {
Some(placed) => placed,
None => {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::GlyphPlacements);
#[cfg(feature = "layout-diagnostics")]
let _place = diag::timer(TimerKind::GlyphPlacement);
RenderedText {
glyphs: self.place(buffer),
size: buffer.size(),
color: attrs.color,
}
}
};
buffer.placed.insert(placed)
} }
} }
+4 -46
View File
@@ -1,10 +1,11 @@
use crate::{UiRegion, util::Id, util::Vec2}; use crate::{UiRegion, util::Id};
use wgpu::*; use wgpu::*;
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
pub struct WindowUniform { pub struct WindowUniform {
pub dim: Vec2, pub width: f32,
pub height: f32,
} }
#[repr(C)] #[repr(C)]
@@ -12,17 +13,15 @@ pub struct WindowUniform {
pub struct PrimitiveInstance { pub struct PrimitiveInstance {
pub region: UiRegion, pub region: UiRegion,
pub mask_idx: MaskIdx, pub mask_idx: MaskIdx,
pub move_idx: MoveIdx,
} }
impl PrimitiveInstance { impl PrimitiveInstance {
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![ const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
0 => Float32x2, 0 => Float32x2,
1 => Float32x2, 1 => Float32x2,
2 => Float32x2, 2 => Float32x2,
3 => Float32x2, 3 => Float32x2,
4 => Uint32, 4 => Uint32,
5 => Uint32,
]; ];
pub fn desc() -> VertexBufferLayout<'static> { pub fn desc() -> VertexBufferLayout<'static> {
@@ -44,45 +43,4 @@ impl MaskIdx {
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Mask { pub struct Mask {
pub region: UiRegion, pub region: UiRegion,
pub move_idx: MoveIdx,
}
/// Its own type rather than another `Id<u32>`, because it sits beside
/// `MaskIdx` in an instance and the two must not be swappable.
#[repr(transparent)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)]
pub struct MoveIdx(u32);
impl MoveIdx {
pub const NONE: Self = Self(u32::MAX);
pub(crate) fn slot(idx: usize) -> Self {
Self(idx as u32)
}
pub(crate) fn idx(self) -> usize {
self.0 as usize
}
}
/// One link of the chain a primitive's position is resolved through: the box
/// its contents are placed within, given in the coordinates of the slot it
/// names. Moving or resizing a subtree writes its own slot and nothing else.
///
/// The identity is `UiRegion::FULL`, not zero: a zeroed entry is a box of no
/// extent, which collapses everything under it to a point.
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct MoveOffset {
pub region: UiRegion,
pub parent: MoveIdx,
}
unsafe impl bytemuck::Pod for MoveOffset {}
unsafe impl bytemuck::Zeroable for MoveOffset {}
impl MoveOffset {
pub fn new(parent: MoveIdx, region: UiRegion) -> Self {
Self { region, parent }
}
} }
+18 -81
View File
@@ -17,15 +17,11 @@ mod texture;
mod util; mod util;
pub use atlas::*; pub use atlas::*;
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset}; pub use data::{Mask, MaskIdx};
pub use primitive::*; pub use primitive::*;
const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
fn module_source(wgsl: &str) -> String {
format!("{PRELUDE}\n{wgsl}")
}
pub struct UiRenderNode { pub struct UiRenderNode {
shared_layout: BindGroupLayout, shared_layout: BindGroupLayout,
shared_group: BindGroup, shared_group: BindGroup,
@@ -38,7 +34,6 @@ pub struct UiRenderNode {
active: Vec<usize>, active: Vec<usize>,
window_buffer: Buffer, window_buffer: Buffer,
masks: ArrBuf<Mask>, masks: ArrBuf<Mask>,
moves: ArrBuf<MoveOffset>,
} }
struct RenderLayer { struct RenderLayer {
@@ -132,35 +127,32 @@ impl UiRenderNode {
for primitive in &mut self.primitives { for primitive in &mut self.primitives {
primitive.render.update(ui); primitive.render.update(ui);
} }
let mut regroup = false;
if ui.masks.changed { if ui.masks.changed {
ui.masks.changed = false; ui.masks.changed = false;
regroup |= self.masks.update(device, queue, &ui.masks[..]); if self.masks.update(device, queue, &ui.masks[..]) {
} self.shared_group = Self::shared_group(
if ui_render.moves.changed { device,
ui_render.moves.changed = false; &self.shared_layout,
regroup |= self.moves.update(device, queue, ui_render.moves.entries()); &self.window_buffer,
} &self.masks,
if regroup { );
self.shared_group = Self::shared_group( }
device,
&self.shared_layout,
&self.window_buffer,
&self.masks,
&self.moves,
);
} }
} }
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) { pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
let size = size.into(); let size = size.into();
let slice = &[WindowUniform { dim: size }]; let slice = &[WindowUniform {
width: size.x,
height: size.y,
}];
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice)); queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
} }
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self { pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
let window_uniform = WindowUniform { let window_uniform = WindowUniform {
dim: Vec2::new(config.width as f32, config.height as f32), width: config.width as f32,
height: config.height as f32,
}; };
let window_buffer = device.create_buffer_init(&BufferInitDescriptor { let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("window"), label: Some("window"),
@@ -174,13 +166,7 @@ impl UiRenderNode {
BufferUsages::STORAGE | BufferUsages::COPY_DST, BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui masks", "ui masks",
); );
let moves = ArrBuf::new( let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
device,
BufferUsages::STORAGE | BufferUsages::COPY_DST,
"ui move offsets",
);
let shared_group =
Self::shared_group(device, &shared_layout, &window_buffer, &masks, &moves);
Self { Self {
shared_layout, shared_layout,
@@ -191,7 +177,6 @@ impl UiRenderNode {
layers: HashMap::default(), layers: HashMap::default(),
active: Vec::new(), active: Vec::new(),
masks, masks,
moves,
} }
} }
@@ -226,7 +211,7 @@ impl UiRenderNode {
) -> RenderPipeline { ) -> RenderPipeline {
let module = device.create_shader_module(ShaderModuleDescriptor { let module = device.create_shader_module(ShaderModuleDescriptor {
label: Some(label), label: Some(label),
source: ShaderSource::Wgsl(module_source(wgsl).into()), source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()),
}); });
device.create_render_pipeline(&RenderPipelineDescriptor { device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some(label), label: Some(label),
@@ -267,8 +252,7 @@ impl UiRenderNode {
}) })
} }
/// What every draw in the ui is given: the window, the masks and the /// What every draw in the ui is given: the window and the masks.
/// move chain every position is resolved through.
fn shared_layout(device: &Device) -> BindGroupLayout { fn shared_layout(device: &Device) -> BindGroupLayout {
device.create_bind_group_layout(&BindGroupLayoutDescriptor { device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[ entries: &[
@@ -292,16 +276,6 @@ impl UiRenderNode {
}, },
count: None, count: None,
}, },
BindGroupLayoutEntry {
binding: 2,
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
ty: BindingType::Buffer {
ty: BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: BufferSize::new(size_of::<MoveOffset>() as u64),
},
count: None,
},
], ],
label: Some("ui shared"), label: Some("ui shared"),
}) })
@@ -312,7 +286,6 @@ impl UiRenderNode {
layout: &BindGroupLayout, layout: &BindGroupLayout,
window: &Buffer, window: &Buffer,
masks: &ArrBuf<Mask>, masks: &ArrBuf<Mask>,
moves: &ArrBuf<MoveOffset>,
) -> BindGroup { ) -> BindGroup {
device.create_bind_group(&BindGroupDescriptor { device.create_bind_group(&BindGroupDescriptor {
layout, layout,
@@ -325,10 +298,6 @@ impl UiRenderNode {
binding: 1, binding: 1,
resource: masks.buffer.as_entire_binding(), resource: masks.buffer.as_entire_binding(),
}, },
BindGroupEntry {
binding: 2,
resource: moves.buffer.as_entire_binding(),
},
], ],
label: Some("ui shared"), label: Some("ui shared"),
}) })
@@ -405,35 +374,3 @@ impl ListBuffers {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::module_source;
use wgpu::naga::{
front::wgsl,
valid::{Capabilities, ValidationFlags, Validator},
};
/// Every shader file, composed as the renderer composes it, parses and
/// validates with no device -- so an edit that breaks one fails here and
/// not in the first window opened.
#[test]
fn every_shader_validates() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src/render/shader");
let mut checked = 0;
for entry in std::fs::read_dir(dir).unwrap() {
let path = entry.unwrap().path();
if path.extension().is_none_or(|e| e != "wgsl") || path.ends_with("prelude.wgsl") {
continue;
}
let source = module_source(&std::fs::read_to_string(&path).unwrap());
let module = wgsl::parse_str(&source)
.unwrap_or_else(|e| panic!("{}: {}", path.display(), e.emit_to_string(&source)));
Validator::new(ValidationFlags::all(), Capabilities::all())
.validate(&module)
.unwrap_or_else(|e| panic!("{}: {e:?}", path.display()));
checked += 1;
}
assert!(checked > 0, "no shaders found in {dir}");
}
}
+5 -11
View File
@@ -3,7 +3,7 @@ use std::{any::TypeId, marker::PhantomData};
use crate::{ use crate::{
Color, TextureHandle, UiData, UiRegion, WidgetId, Color, TextureHandle, UiData, UiRegion, WidgetId,
render::{ render::{
data::{MaskIdx, MoveIdx, PrimitiveInstance}, data::{MaskIdx, PrimitiveInstance},
page::GlyphRender, page::GlyphRender,
texture::ImageRender, texture::ImageRender,
}, },
@@ -246,7 +246,6 @@ impl LayerDraws {
primitive, primitive,
region, region,
mask_idx, mask_idx,
move_idx,
}: PrimitiveInst<P>, }: PrimitiveInst<P>,
) -> PrimitiveHandle { ) -> PrimitiveHandle {
self.updated = true; self.updated = true;
@@ -259,11 +258,7 @@ impl LayerDraws {
.get_or_insert_with(InstanceList::new::<P>) .get_or_insert_with(InstanceList::new::<P>)
.push( .push(
id, id,
PrimitiveInstance { PrimitiveInstance { region, mask_idx },
region,
mask_idx,
move_idx,
},
bytemuck::bytes_of(&primitive), bytemuck::bytes_of(&primitive),
); );
PrimitiveHandle { PrimitiveHandle {
@@ -309,7 +304,6 @@ pub struct PrimitiveInst<P> {
pub primitive: P, pub primitive: P,
pub region: UiRegion, pub region: UiRegion,
pub mask_idx: MaskIdx, pub mask_idx: MaskIdx,
pub move_idx: MoveIdx,
} }
pub struct PrimitiveChange { pub struct PrimitiveChange {
@@ -353,7 +347,7 @@ impl RectPrimitive {
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph /// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects. /// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
#[repr(C)] #[repr(C, align(8))]
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct GlyphPrimitive { pub struct GlyphPrimitive {
pub uv_min: Vec2, pub uv_min: Vec2,
@@ -364,8 +358,8 @@ pub struct GlyphPrimitive {
pub flags: u32, pub flags: u32,
} }
// Manual rather than derived: `Vec2`'s alignment leaves four bytes of padding // Manual rather than derived: the align(8) leaves four bytes of padding, which
// here, which is how WGSL lays the struct out. // is how WGSL lays the struct out.
unsafe impl bytemuck::Pod for GlyphPrimitive {} unsafe impl bytemuck::Pod for GlyphPrimitive {}
unsafe impl bytemuck::Zeroable for GlyphPrimitive {} unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
impl Primitive for GlyphPrimitive { impl Primitive for GlyphPrimitive {
+13 -67
View File
@@ -7,8 +7,6 @@
var<uniform> window: WindowUniform; var<uniform> window: WindowUniform;
@group(0) @binding(1) @group(0) @binding(1)
var<storage> masks: array<Mask>; var<storage> masks: array<Mask>;
@group(0) @binding(2)
var<storage> move_offsets: array<MoveOffset>;
struct WindowUniform { struct WindowUniform {
dim: vec2<f32>, dim: vec2<f32>,
@@ -17,49 +15,6 @@ struct WindowUniform {
struct Mask { struct Mask {
x: UiSpan, x: UiSpan,
y: UiSpan, y: UiSpan,
move_idx: u32,
}
struct MoveOffset {
x: UiSpan,
y: UiSpan,
parent: u32,
}
struct Region {
x: UiSpan,
y: UiSpan,
}
const MOVE_NONE: u32 = 4294967295u;
// Keep in step with `iris_core::CHAIN_LIMIT`. It bounds a malformed cycle
// rather than any real tree, and the CPU walk uses the same number so both
// resolve a deep one the same way.
const CHAIN_LIMIT: u32 = 64u;
fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar {
return UiScalar(
mix(p.start.rel, p.end.rel, s.rel),
s.px + mix(p.start.px, p.end.px, s.rel),
);
}
fn span_within(s: UiSpan, p: UiSpan) -> UiSpan {
return UiSpan(scalar_within(s.start, p), scalar_within(s.end, p));
}
fn resolve_move(idx: u32, local: Region) -> Region {
var r = local;
var at = idx;
for (var step = 0u; step < CHAIN_LIMIT; step++) {
if at == MOVE_NONE {
break;
}
let entry = move_offsets[at];
r = Region(span_within(r.x, entry.x), span_within(r.y, entry.y));
at = entry.parent;
}
return r;
} }
struct UiSpan { struct UiSpan {
@@ -69,7 +24,7 @@ struct UiSpan {
struct UiScalar { struct UiScalar {
rel: f32, rel: f32,
px: f32, abs: f32,
} }
struct InstanceInput { struct InstanceInput {
@@ -78,7 +33,6 @@ struct InstanceInput {
@location(2) y_start: vec2<f32>, @location(2) y_start: vec2<f32>,
@location(3) y_end: vec2<f32>, @location(3) y_end: vec2<f32>,
@location(4) mask_idx: u32, @location(4) mask_idx: u32,
@location(5) move_idx: u32,
} }
struct VertexOutput { struct VertexOutput {
@@ -98,18 +52,13 @@ fn vs_main(
) -> VertexOutput { ) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
let local = Region( let top_left_rel = vec2(in.x_start.x, in.y_start.x);
UiSpan(UiScalar(in.x_start.x, in.x_start.y), UiScalar(in.x_end.x, in.x_end.y)), let top_left_abs = vec2(in.x_start.y, in.y_start.y);
UiSpan(UiScalar(in.y_start.x, in.y_start.y), UiScalar(in.y_end.x, in.y_end.y)), let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
); let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
let r = resolve_move(in.move_idx, local);
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
let top_left_px = vec2(r.x.start.px, r.y.start.px);
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
let bot_right_px = vec2(r.x.end.px, r.y.end.px);
let top_left = floor(top_left_rel * window.dim) + floor(top_left_px); let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_px); let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
let size = bot_right - top_left; let size = bot_right - top_left;
let uv = vec2<f32>( let uv = vec2<f32>(
@@ -132,16 +81,13 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
return color; return color;
} }
let mask = masks[in.mask_idx]; let mask = masks[in.mask_idx];
// Its own chain, not the drawn primitive's, so a stationary viewport let tl = vec2(mask.x.start.rel, mask.y.start.rel);
// clips content that moves inside it. let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
let m = resolve_move(mask.move_idx, Region(mask.x, mask.y)); let br = vec2(mask.x.end.rel, mask.y.end.rel);
let tl = vec2(m.x.start.rel, m.y.start.rel); let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
let tl_px = vec2(m.x.start.px, m.y.start.px);
let br = vec2(m.x.end.rel, m.y.end.rel);
let br_px = vec2(m.x.end.px, m.y.end.px);
let top_left = floor(tl * window.dim) + floor(tl_px); let top_left = floor(tl * window.dim) + floor(tl_abs);
let bot_right = floor(br * window.dim) + floor(br_px); let bot_right = floor(br * window.dim) + floor(br_abs);
let pos = in.clip_position.xy; let pos = in.clip_position.xy;
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y { if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
return color * 0.0; return color * 0.0;
+3 -28
View File
@@ -1,6 +1,4 @@
use crate::{ use crate::{LayerId, MaskIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId};
LayerId, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId, util::Vec2,
};
/// important non rendering data for retained drawing /// important non rendering data for retained drawing
#[derive(Debug)] #[derive(Debug)]
@@ -9,37 +7,14 @@ pub struct ActiveData {
pub region: UiRegion, pub region: UiRegion,
/// What the widget said it used of `region`, the last time it drew. /// What the widget said it used of `region`, the last time it drew.
pub size: Size, pub size: Size,
/// The pixel size of the box it drew against. `region` alone cannot say:
/// it is a fraction of a slot's box, and the same fraction of a box that
/// has since changed is a different number of pixels.
pub px: Vec2,
/// The pixel size of the box its parent first asked about it in, before
/// knowing what it came to. `px` may be a box derived from that answer,
/// and a size measured there is only the same answer asked again.
pub offered_px: Vec2,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
/// How far down the tree it was drawn, the root being 1. Carried down a
/// draw rather than worked out by walking up, so it is right for every
/// widget a frame visits and cannot drift while one is being drawn.
pub depth: usize,
pub textures: Vec<TextureHandle>, pub textures: Vec<TextureHandle>,
pub primitives: Vec<PrimitiveHandle>, pub primitives: Vec<PrimitiveHandle>,
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>,
/// Offered pixel axes which flowed into this widget's reported size, /// Whether it read the output's size, and so is wrong when that changes.
/// directly or through a child size it read. pub reads_output: bool,
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,
/// 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,
/// The slot `region` is given in, which is whatever its parent drew in.
pub parent_move: MoveIdx,
pub mask: MaskIdx, pub mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
} }
+1 -90
View File
@@ -1,14 +1,7 @@
use crate::{ use crate::{
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget, Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
WidgetId, Widgets,
util::{Arena, Id, TrackedArena},
}; };
/// How far the shader will walk a move chain. It bounds a malformed cycle
/// rather than any real tree; `Moves::resolve` uses the same number so the
/// two agree on what a deep tree resolves to.
pub const CHAIN_LIMIT: u32 = 64;
mod active; mod active;
mod painter; mod painter;
mod render_state; mod render_state;
@@ -27,88 +20,6 @@ pub struct UiData {
pub masks: TrackedArena<Mask, u32>, pub masks: TrackedArena<Mask, u32>,
} }
/// Where each widget's drawing sits relative to its parent's slot, so moving
/// a subtree writes one entry rather than every descendant's primitives.
#[derive(Default)]
pub struct Moves {
arena: Arena<MoveOffset, u32>,
pub changed: bool,
}
impl Moves {
pub fn push(&mut self, parent: MoveIdx, region: UiRegion) -> MoveIdx {
self.changed = true;
MoveIdx::slot(self.arena.push(MoveOffset::new(parent, region)).idx())
}
/// Re-points a slot at a different parent, for a widget drawn somewhere
/// else in the tree than it was.
pub fn set_parent(&mut self, idx: MoveIdx, parent: MoveIdx) {
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
if entry.parent != parent {
entry.parent = parent;
self.changed = true;
}
}
pub fn remove(&mut self, idx: MoveIdx) {
self.changed = true;
self.arena.remove(Id::preset(idx.idx() as u32));
}
/// Sets the box a slot's contents are placed within, itself given in the
/// coordinates of its parent slot.
pub fn set(&mut self, idx: MoveIdx, region: UiRegion) {
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
if entry.region != region {
entry.region = region;
self.changed = true;
}
}
/// Composes a region held in `idx`'s coordinates down the chain, which is
/// the same walk the vertex shader does.
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
let mut region = local;
let mut at = idx;
for _ in 0..CHAIN_LIMIT {
if at == MoveIdx::NONE {
return region;
}
let entry = self.arena[at.idx()];
region = region.within(&entry.region);
at = entry.parent;
}
debug_assert!(
at == MoveIdx::NONE,
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
and the shader stops at the same depth"
);
region
}
/// How many slots a region in `idx` is composed through, which is what
/// the shader's walk costs per primitive.
pub fn depth(&self, idx: MoveIdx) -> usize {
let mut depth = 0;
let mut at = idx;
while at != MoveIdx::NONE && depth < CHAIN_LIMIT as usize {
at = self.arena[at.idx()].parent;
depth += 1;
}
depth
}
pub fn entries(&self) -> &[MoveOffset] {
&self.arena
}
pub fn clear(&mut self) {
self.changed = true;
self.arena = Arena::default();
}
}
pub trait UiRsc { pub trait UiRsc {
fn ui(&self) -> &UiData; fn ui(&self) -> &UiData;
fn ui_mut(&mut self) -> &mut UiData; fn ui_mut(&mut self) -> &mut UiData;
+24 -195
View File
@@ -1,11 +1,9 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
PrimitiveKind, TexturePrimitive, TexturePrimitive,
}, },
util::Vec2, util::Vec2,
}; };
@@ -15,25 +13,15 @@ pub struct Painter<'a> {
pub(super) state: &'a mut UiRenderState, pub(super) state: &'a mut UiRenderState,
pub(super) rsc: &'a mut dyn UiRsc, pub(super) rsc: &'a mut dyn UiRsc,
/// This widget's box, in the coordinates of `move_idx`.
pub(super) region: UiRegion, pub(super) region: UiRegion,
pub(super) mask: MaskIdx, pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>, pub(super) primitives: Vec<PrimitiveHandle>,
pub(super) children: Vec<WidgetId>, pub(super) children: Vec<WidgetId>,
/// The children asked about so far, so the first box each was asked
/// about is the one recorded as its offer.
pub(super) offered: 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>,
/// Offered pixel axes which can affect the size this draw reports. pub(super) reads_output: bool,
pub(super) size_box_inputs: [bool; 2],
pub(super) size_output_inputs: [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,
pub layer: usize, pub layer: usize,
pub(super) depth: usize,
pub(super) id: WidgetId, pub(super) id: WidgetId,
} }
@@ -45,8 +33,6 @@ impl<'a> Painter<'a> {
/// Takes the kind, for a caller writing many of one primitive. /// Takes the kind, for a caller writing many of one primitive.
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) { fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PrimitiveWrites);
let h = self.state.layers.write( let h = self.state.layers.write(
self.layer, self.layer,
PrimitiveInst { PrimitiveInst {
@@ -55,7 +41,6 @@ impl<'a> Painter<'a> {
primitive, primitive,
region, region,
mask_idx: self.mask, mask_idx: self.mask,
move_idx: self.move_idx,
}, },
); );
self.push_primitive(h); self.push_primitive(h);
@@ -82,51 +67,29 @@ impl<'a> Painter<'a> {
pub fn set_mask(&mut self, region: UiRegion) { pub fn set_mask(&mut self, region: UiRegion) {
assert!(self.mask == MaskIdx::NONE); assert!(self.mask == MaskIdx::NONE);
self.mask = self.rsc.ui_mut().masks.push(Mask { self.mask = self.rsc.ui_mut().masks.push(Mask { region });
region,
move_idx: self.move_idx,
});
} }
/// Draws a widget within this widget's region. /// Draws a widget within this widget's region.
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> { pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
self.widget_at(id, self.region, false) self.widget_at(id, self.region)
} }
/// Draws a widget somewhere within this one. /// Draws a widget somewhere within this one. Drawing one a second time
/// gives it a new box, keeping the drawing it already has where it can.
pub fn widget_within<'s, W: ?Sized>( pub fn widget_within<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
let region = region.within(&self.region); let region = region.within(&self.region);
self.widget_at(id, region, false) self.widget_at(id, region)
}
/// Draws a child this widget decides the box of, and may decide again
/// once it knows what the child came to. The child gets a slot of its
/// own, so placing it a second time writes one entry however much it
/// drew -- moved or resized alike, since everything under the slot is
/// held as a fraction of its box. A child drawn any other way has no slot
/// and can only be given a different box by drawing again.
pub fn place<'s, W: ?Sized>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceCalls);
let region = region.within(&self.region);
#[cfg(feature = "layout-diagnostics")]
diag::placed(id.id(), self.id, region);
self.widget_at(id, region, true)
} }
fn widget_at<'s, W: ?Sized>( fn widget_at<'s, W: ?Sized>(
&'s mut self, &'s mut self,
id: &'s StrongWidget<W>, id: &'s StrongWidget<W>,
region: UiRegion, region: UiRegion,
slotted: bool,
) -> DrawResult<'s, 'a, W> { ) -> DrawResult<'s, 'a, W> {
// A child listed twice would be moved twice. // A child listed twice would be moved twice.
if !self.children.contains(&id.id()) { if !self.children.contains(&id.id()) {
@@ -137,14 +100,10 @@ impl<'a> Painter<'a> {
id.id(), id.id(),
region, region,
Some(self.id), Some(self.id),
self.depth + 1,
self.move_idx,
slotted,
self.mask, self.mask,
None, None,
self.rsc, self.rsc,
); );
self.offer(id.id(), region);
DrawResult { DrawResult {
child: id, child: id,
painter: self, painter: self,
@@ -155,119 +114,23 @@ impl<'a> Painter<'a> {
/// What a child says its length is without being drawn, if it can say. /// What a child says its length is without being drawn, if it can say.
/// Asking counts as reading its size. /// Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> { pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
let hint = self let hint = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis)?;
.rsc self.depend_on_size(id);
.widgets() Some(hint)
.get_dyn(id.id())
.and_then(|widget| widget.size_hint(axis));
#[cfg(feature = "layout-diagnostics")]
diag::hint_read(id.id(), self.id, axis, hint);
match hint {
Some(hint) => {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintHits);
self.depend_on_hint(id);
Some(hint)
}
None => {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::HintMisses);
None
}
}
} }
/// A retained child length valid under the region it is about to be fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
/// 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> {
let region = region.within(&self.region);
self.offer(child.id(), region);
if let Some(hint) = self.size_hint(child, axis) {
return Some(hint);
}
self.retained_size(child, region)
.map(|size| size.axis(axis))
}
/// `region` in this widget's own coordinates.
fn retained_size<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
region: UiRegion,
) -> Option<Size> {
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)
}
/// Records the box a child was first asked about in this draw. Any later
/// box this draw gives it was decided knowing its answer, so a size the
/// child measures there is not an answer to this widget's question.
fn offer(&mut self, child: WidgetId, region: UiRegion) {
if self.offered.contains(&child) {
return;
}
self.offered.push(child);
let px = self.state.px_of(self.move_idx, region);
if let Some(active) = self.state.active.get_mut(&child) {
active.offered_px = px;
}
}
/// Depends on a length the child gave without being drawn. A hint is
/// context-free, so this depends on the child but on no pixel axis.
fn depend_on_hint<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
self.depend_on_size_inputs(child, [false; 2], [false; 2]);
}
/// Depends on a size the child produced by drawing, which carries
/// whatever the child read to produce it.
fn depend_on_drawn_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
let (box_inputs, output_inputs) = self
.state
.active
.get(&child.id())
.map_or(([false; 2], [false; 2]), |active| {
(active.size_box_inputs, active.size_output_inputs)
});
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<'b>( pub fn render_text(
&mut self, &mut self,
buffer: &'b mut TextBuffer, buffer: &mut TextBuffer,
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>, width: Option<f32>,
) -> &'b RenderedText { ) -> RenderedText {
#[cfg(feature = "layout-diagnostics")]
diag::render_text(self.id, self.rsc.widgets().label(self.id), width);
let ui = self.rsc.ui_mut(); let ui = self.rsc.ui_mut();
ui.text.render(buffer, attrs, width) ui.text.render(buffer, attrs, width)
} }
@@ -279,9 +142,9 @@ impl<'a> Painter<'a> {
let mut region = origin; let mut region = origin;
region.x.end = region.x.start; region.x.end = region.x.start;
region.y.end = region.y.start; region.y.end = region.y.start;
let mut region = region.offset(UiVec2::px(glyph.offset)); let mut region = region.offset(UiVec2::abs(glyph.offset));
region.x.end = region.x.start + UiScalar::px(glyph.entry.width as f32); region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
region.y.end = region.y.start + UiScalar::px(glyph.entry.height as f32); region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
self.write( self.write(
kind, kind,
GlyphPrimitive { GlyphPrimitive {
@@ -296,8 +159,6 @@ impl<'a> Painter<'a> {
} }
} }
/// This widget's box, in the coordinates its own primitives are written
/// in -- so a region composed `within` it may be drawn directly.
pub fn region(&self) -> UiRegion { pub fn region(&self) -> UiRegion {
self.region self.region
} }
@@ -305,42 +166,15 @@ 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.size_output_inputs = [true; 2]; self.reads_output = true;
self.state.output_size self.state.output_size
} }
/// One axis of the output in pixels. Prefer this to [`Self::output_size`] /// This widget's box in pixels. Resolved against the output's size, so a
/// when the other axis cannot affect the size this widget reports. /// widget that reads it draws again when the output changes.
pub fn output_len(&mut self, axis: Axis) -> f32 {
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 { pub fn px_size(&mut self) -> Vec2 {
self.size_box_inputs = [true; 2]; self.reads_output = true;
let region = self.state.moves.resolve(self.move_idx, self.region); self.region.size().to_abs(self.state.output_size)
region.size().to_px(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.size_box_inputs[axis as usize] = true;
self.px_len_for_draw(axis)
}
/// One axis of this widget's box in pixels, for a draw whose reported
/// size does not follow from it -- a clamp or a position. Nothing records
/// the read, so a size that does depend on it would go stale.
pub fn px_len_for_draw(&self, axis: Axis) -> f32 {
let region = self.state.moves.resolve(self.move_idx, self.region);
region
.size()
.axis(axis)
.to_px(self.state.output_size.axis(axis))
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
@@ -375,12 +209,7 @@ 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")] self.painter.depend_on_size(self.child);
{
diag::bump(Counter::SizeReads);
diag::size_read(self.child.id(), self.painter.id, self.size);
}
self.painter.depend_on_drawn_size(self.child);
self.size self.size
} }
+91 -572
View File
@@ -1,40 +1,17 @@
#[cfg(feature = "layout-diagnostics")]
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, OnResize, Painter, PixelRegion, Remap, Size,
Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId, Widgets, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
util::{HashMap, HashSet, Vec2}, util::{HashMap, HashSet, Vec2, forget_ref},
}; };
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>,
pub layers: DrawLayers, pub layers: DrawLayers,
pub(super) output_size: Vec2, pub(super) output_size: Vec2,
old_root: Option<WidgetId>, old_root: Option<WidgetId>,
/// The slot every chain bottoms out in, holding the output as a box. resized: bool,
root_move: MoveIdx,
/// Widgets whose reported size depends on the root box rather than on
/// their own, so nothing below them changing length can reach them.
root_readers: HashSet<WidgetId>,
/// Content/state dirtiness whose retained size cannot answer a layout
/// question until that widget has drawn again.
invalid_sizes: HashSet<WidgetId>,
/// What has already been drawn during the pass under way, so a widget
/// reached by redrawing an ancestor is not drawn again on its own
/// account. Emptied when the pass ends.
draw_started: 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.
slots: HashMap<WidgetId, MoveIdx>,
pub moves: Moves,
} }
impl UiRenderState { impl UiRenderState {
@@ -44,48 +21,14 @@ impl UiRenderState {
layers: Default::default(), layers: Default::default(),
output_size: Vec2::ZERO, output_size: Vec2::ZERO,
old_root: None, old_root: None,
invalid_sizes: Default::default(), resized: false,
draw_started: Default::default(), draw_started: Default::default(),
slots: Default::default(),
moves: Default::default(),
root_move: MoveIdx::NONE,
root_readers: Default::default(),
}
}
/// The window as a box, so a chain bottoms out in one rather than in a
/// multiplication applied after it. Composing through a box held in
/// pixels leaves everything below it in pixels, which is why nothing
/// downstream has to know the output's size to resolve a position.
fn write_root(&mut self) {
let region = UiRegion::new(
UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.x)),
UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.y)),
);
match self.root_move == MoveIdx::NONE {
true => self.root_move = self.moves.push(MoveIdx::NONE, region),
false => self.moves.set(self.root_move, region),
} }
} }
pub fn resize(&mut self, size: impl Into<Vec2>) { pub fn resize(&mut self, size: impl Into<Vec2>) {
self.output_size = size.into(); self.output_size = size.into();
self.write_root(); self.resized = true;
}
/// Which axes of the root widget's box are no longer the ones the root
/// slot holds, which is all a resize now is: one slot written, found by
/// the same comparison every other box change is found by.
fn root_axes_changed(&self) -> [bool; 2] {
let Some(active) = self.old_root.and_then(|root| self.active.get(&root)) else {
return [false; 2];
};
let px = self.px_of(active.parent_move, active.region);
let mut changed = [false; 2];
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
*c = pixel_len_changed(active.px.axis(axis), px.axis(axis));
}
changed
} }
pub fn output_size(&self) -> Vec2 { pub fn output_size(&self) -> Vec2 {
@@ -93,13 +36,6 @@ impl UiRenderState {
} }
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) { pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::Updates);
#[cfg(feature = "layout-diagnostics")]
let _update = diag::timer(TimerKind::Update);
self.invalid_sizes.clear();
self.invalid_sizes
.extend(rsc.widgets().needs_redraw.iter().copied());
// 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() {
@@ -120,74 +56,27 @@ 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.root_axes_changed().iter().any(|&c| c) { } else if self.resized {
// Every box is a part of the root box, so writing it is a box // A region is a fraction of the output plus an offset, resolved
// that changed length like any other. Offering the root widget // against the window in the shader, so a resize moves the whole
// its box again puts that through `try_reuse`, which answers per // drawing on its own. Only a widget that read pixels can be wrong.
// axis and lets `redraws_under` price the subtree -- rather than for (&id, active) in &self.active {
// marking it, which would redraw it whichever axis moved. What if active.reads_output {
// that cannot reach is a widget whose size came from the root box rsc.widgets_mut().needs_redraw.insert(id);
// instead of its own, since its own box need not have changed.
#[cfg(feature = "layout-diagnostics")]
let _marking = diag::timer(TimerKind::ResizeMarking);
let changed = self.root_axes_changed();
for id in self.root_readers.clone() {
let reads = self
.active
.get(&id)
.map_or([false; 2], |active| active.size_output_inputs);
if !AXES
.into_iter()
.zip(changed)
.any(|(axis, c)| c && reads[axis as usize])
{
continue;
} }
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ResizeDependents);
rsc.widgets_mut().needs_redraw.insert(id);
}
if let Some(root) = root {
self.draw_inner(
0,
root.id(),
UiRegion::FULL,
None,
1,
self.root_move,
false,
MaskIdx::NONE,
None,
rsc,
);
} }
} }
self.resized = false;
if rsc.widgets().has_updates() { if rsc.widgets().has_updates() {
self.redraw_updates(rsc); self.redraw_updates(rsc);
} }
self.invalid_sizes.clear();
self.draw_started.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) {
#[cfg(feature = "layout-diagnostics")]
let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc); self.clear(rsc);
// free all resources & cache // free all resources & cache
self.write_root();
if let Some(id) = root { if let Some(id) = root {
self.draw_inner( self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None, rsc);
0,
id.id(),
UiRegion::FULL,
None,
1,
self.root_move,
false,
MaskIdx::NONE,
None,
rsc,
);
} }
} }
@@ -199,90 +88,53 @@ impl UiRenderState {
id: WidgetId, id: WidgetId,
region: UiRegion, region: UiRegion,
parent: Option<WidgetId>, parent: Option<WidgetId>,
depth: usize,
parent_move: MoveIdx,
slotted: bool,
mask: MaskIdx, mask: MaskIdx,
mut old: Option<ActiveData>, old_children: Option<Vec<WidgetId>>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> Size { ) -> Size {
#[cfg(feature = "layout-diagnostics")] let mut old_children = old_children.unwrap_or_default();
{
diag::bump(Counter::DrawRequests);
diag::draw_request(id, parent, region, self.px_of(parent_move, region), slotted);
}
if self.active.contains_key(&id) { if self.active.contains_key(&id) {
if let Some(size) = self.try_reuse(id, region, depth, parent_move, rsc) { if let Some(size) = self.try_reuse(id, region, rsc) {
return size; return size;
} }
// if not, then maintain resize and track old children to remove unneeded // if not, then maintain resize and track old children to remove unneeded
old = self.remove(id, false, rsc); let active = self.remove(id, false, rsc).unwrap();
old_children = active.children;
} }
// draw widget // draw widget
let (move_idx, local) = match slotted {
// Its box becomes its slot's, so it draws in the slot's own
// coordinates and the box it was given is one entry to rewrite.
true => (self.move_slot(id, parent_move, region), UiRegion::FULL),
false => {
self.drop_slot(id);
(parent_move, region)
}
};
let px = self.px_of(move_idx, local);
// Drawn again in a box its parent already decided: the offer is the
// one recorded when the parent first asked, not this box.
let (old_children, offered_px) = match old {
Some(old) => (old.children, old.offered_px),
None => (Vec::new(), px),
};
rsc.widgets_mut().needs_redraw.remove(&id); rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.insert(id); self.draw_started.insert(id);
let mut painter = Painter { let mut painter = Painter {
state: self, state: self,
region: local, region,
mask, mask,
layer, layer,
id, id,
textures: Vec::new(), textures: Vec::new(),
primitives: Vec::new(), primitives: Vec::new(),
children: Vec::new(), children: Vec::new(),
offered: Vec::new(),
size_deps: Vec::new(), size_deps: Vec::new(),
depth, reads_output: false,
size_box_inputs: [false; 2],
size_output_inputs: [false; 2],
move_idx,
rsc, rsc,
}; };
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::WidgetDraws);
diag::draw_widget(id, painter.rsc.widgets().label(id));
}
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id); let 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: _,
rsc: _, rsc: _,
region: _, region,
mask, mask,
textures, textures,
primitives, primitives,
children, children,
offered: _,
size_deps, size_deps,
size_box_inputs, reads_output,
size_output_inputs,
move_idx,
layer, layer,
depth: _,
id, id,
} = painter; } = painter;
@@ -297,22 +149,16 @@ impl UiRenderState {
id, id,
region, region,
size, size,
px,
offered_px,
parent, parent,
depth,
textures, textures,
primitives, primitives,
children, children,
size_deps, size_deps,
size_box_inputs, reads_output,
size_output_inputs,
output_px: self.output_size,
move_idx,
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) {
@@ -320,260 +166,54 @@ impl UiRenderState {
} }
} }
match active.size_output_inputs.iter().any(|&reads| reads) {
true => self.root_readers.insert(id),
false => self.root_readers.remove(&id),
};
rsc.on_draw(&active); rsc.on_draw(&active);
self.active.insert(id, active); self.active.insert(id, active);
self.invalid_sizes.remove(&id);
size size
} }
/// The slot a widget's box is held in, made on its first placed draw and
/// kept until it stops being drawn -- a redraw replaces its `ActiveData`
/// while descendants go on naming the slot.
fn move_slot(&mut self, id: WidgetId, parent: MoveIdx, region: UiRegion) -> MoveIdx {
if let Some(&idx) = self.slots.get(&id) {
self.moves.set_parent(idx, parent);
self.moves.set(idx, region);
return idx;
}
let idx = self.moves.push(parent, region);
self.slots.insert(id, idx);
idx
}
/// Gives up a slot a widget no longer needs, because it is drawn somewhere
/// that does not place it. Its descendants name it, so this is only
/// reached where they are about to be drawn again.
fn drop_slot(&mut self, id: WidgetId) {
if let Some(idx) = self.slots.remove(&id) {
self.moves.remove(idx);
}
}
/// The pixel size of a region held in `slot`'s coordinates.
pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> Vec2 {
self.moves
.resolve(slot, region)
.size()
.to_px(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)
}
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(&mut self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> Option<Size> {
&mut self,
id: WidgetId,
region: UiRegion,
depth: usize,
parent_move: MoveIdx,
rsc: &mut dyn UiRsc,
) -> Option<Size> {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseAttempts);
// Only its own dirtiness, not anything dirty under it that could
// change the size this hands back. What makes that safe is the order
// `redraw_updates` settles in, and nothing else: by the time a reader
// draws, everything dirty below it has been drawn and has propagated.
// Draw in another order and this returns a stale size -- measured, on
// seed 2 of `tests/generated.rs`.
if rsc.widgets().needs_redraw.contains(&id) { if rsc.widgets().needs_redraw.contains(&id) {
#[cfg(feature = "layout-diagnostics")]
{
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)?;
// Drawn somewhere else in the tree: its box is in coordinates it no let (size, old) = (active.size, active.region);
// longer sits in, and its slot names the wrong parent. if old == region {
if active.parent_move != parent_move {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseWrongParent);
diag::reuse(id, ReuseOutcome::WrongParent);
}
return None;
}
let (size, old_region, slot, old_px) =
(active.size, active.region, active.move_idx, active.px);
// In pixels, because `region` is a fraction of a slot's box and that
// box may be what changed -- an unchanged fraction of a box half the
// size is half the widget.
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 = pixel_len_changed(old_px.axis(axis), px.axis(axis));
}
if !changed.iter().any(|&c| c) && old_region == region {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseExact);
diag::reuse(id, ReuseOutcome::Exact);
}
self.keep_depth(id, depth);
return Some(size); return Some(size);
} }
// Only a placed widget can be given a different *region* without // TODO: epsilon?
// drawing again: it has an entry of its own to say where it went, if old.size() != region.size() && !self.reusable(id, region, rsc) {
// where an unslotted one shares its parent's and has nothing to
// write. Its parent's box changing length is not that -- everything
// it drew is a fraction of that box, so the slot already above it
// carries the change and `on_resize` below decides whether the
// drawing survives it.
if slot == parent_move && old_region != region {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseUnslotted);
diag::reuse(id, ReuseOutcome::Unslotted);
}
return None; return None;
} }
if changed.iter().any(|&c| c) { // Its drawing stands, if the new box can be reached from the old one.
let widget = rsc.widgets().get_dyn(id)?; self.mov(id, &Remap::new(old, region)?);
let redraws = AXES
.into_iter()
.zip(changed)
.any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale);
// Anything under it that has to be drawn again is drawn by drawing
// this, because whatever reads that widget's size sits in between
// and has to lay out around what it comes to.
if redraws {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseOwnResize);
diag::reuse(id, ReuseOutcome::OwnResize);
}
return None;
}
if self.redraws_under(id, changed, rsc) {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseDescendantResize);
diag::reuse(id, ReuseOutcome::DescendantResize);
}
return None;
}
}
if slot != parent_move {
self.moves.set(slot, region);
}
self.keep_depth(id, depth);
let active = self.active.get_mut(&id).unwrap();
active.region = region;
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseMoved);
diag::reuse(id, ReuseOutcome::Moved);
}
Some(size) Some(size)
} }
/// Whether anything under `id` would have to be drawn again for the box /// Whether the widget can keep the drawing it has and be given `region`
/// it is a fraction of changing length, `changed` saying which axes of /// instead, asked one axis at a time: a change on an axis it does not
/// that box did. /// depend on costs nothing, whatever it depends on elsewhere.
/// fn reusable(&self, id: WidgetId, region: UiRegion, rsc: &dyn UiRsc) -> bool {
/// A part of a box with no relative extent on an axis is a fixed length,
/// held as offsets from that box's start, and composing anything into it
/// leaves no relative extent either. So a widget whose own box did not
/// change length has no descendant whose box did, and the walk stops
/// there -- an 80-wide child of a widened row is not asked at all.
fn redraws_under(&self, id: WidgetId, changed: [bool; 2], rsc: &dyn UiRsc) -> bool {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ResizeChecks);
let Some(active) = self.active.get(&id) else { let Some(active) = self.active.get(&id) else {
return false; return false;
}; };
let size_deps = &active.size_deps; let Some(widget) = rsc.widgets().get_dyn(id) else {
active.children.iter().any(|&child| { return false;
#[cfg(feature = "layout-diagnostics")] };
diag::bump(Counter::ResizeCheckChildren); [Axis::X, Axis::Y].into_iter().all(|axis| {
let Some(data) = self.active.get(&child) else { let offered = region.axis(axis).len();
return false; let had = active.region.axis(axis).len();
}; match widget.on_resize(axis) {
let Some(widget) = rsc.widgets().get_dyn(child) else { OnResize::Scale => true,
return true; // `Translate` is not acted on yet, and cannot be until a
}; // drawing can sit somewhere other than its box. `region` is
// What it drew to learn this child's size was the child in *this* // both the box a widget was given and the box its primitives
// box, so a different box is a different answer -- unless the // are in, and `mov` remaps from it -- so carrying a drawing at
// child gave an exact one without being drawn at all. // its old size while the box grows makes the next move stretch
if size_deps.contains(&child) { // it. The offset chain is what separates the two.
let measured = AXES OnResize::Translate | OnResize::Redraw => offered == had,
.into_iter()
.zip(changed)
.any(|(axis, c)| c && widget.size_hint(axis).is_none());
if measured {
return true;
}
} }
let mut own = changed;
for (axis, c) in AXES.into_iter().zip(own.iter_mut()) {
*c &= data.region.axis(axis).len().rel != 0.0;
}
if !own.iter().any(|&c| c) {
return false;
}
let redraws = AXES
.into_iter()
.zip(own)
.any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale);
redraws || self.redraws_under(child, own, rsc)
}) })
} }
@@ -581,13 +221,27 @@ impl UiRenderState {
let Some(widget) = rsc.widgets().get_dyn(id) else { let Some(widget) = rsc.widgets().get_dyn(id) else {
return true; return true;
}; };
AXES.into_iter().all(|axis| { [Axis::X, Axis::Y].into_iter().all(|axis| {
widget widget
.size_hint(axis) .size_hint(axis)
.is_none_or(|hint| hint == size.axis(axis)) .is_none_or(|hint| hint == size.axis(axis))
}) })
} }
fn mov(&mut self, id: WidgetId, remap: &Remap) {
let active = self.active.get_mut(&id).unwrap();
for h in &active.primitives {
let region = self.layers[h.layer].region_mut(h);
*region = remap.apply(*region);
}
active.region = remap.apply(active.region);
// SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) };
for child in children {
self.mov(*child, remap);
}
}
/// NOTE: instance textures are cleared and self.textures freed /// NOTE: instance textures are cleared and self.textures freed
fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> { fn remove(&mut self, id: WidgetId, undraw: bool, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
let mut active = self.active.remove(&id); let mut active = self.active.remove(&id);
@@ -614,10 +268,6 @@ impl UiRenderState {
self.remove_rec(*c, rsc); self.remove_rec(*c, rsc);
} }
} }
// After the descendants, whose slots name this one as their parent.
if let Some(idx) = self.slots.remove(&id) {
self.moves.remove(idx);
}
inst inst
} }
@@ -625,70 +275,18 @@ impl UiRenderState {
for (_, active) in self.active.drain() { for (_, active) in self.active.drain() {
rsc.on_undraw(&active); rsc.on_undraw(&active);
} }
self.slots.clear();
self.moves.clear();
self.root_move = MoveIdx::NONE;
self.layers.clear(); self.layers.clear();
self.invalid_sizes.clear();
self.draw_started.clear();
rsc.widgets_mut().needs_redraw.clear(); rsc.widgets_mut().needs_redraw.clear();
rsc.free(); rsc.free();
} }
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) { pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")] while let Some(&id) = rsc.widgets().needs_redraw.iter().next() {
let _layout = diag::timer(TimerKind::IncrementalLayout);
// A reader's answer is only valid after every dirty size it reads has
// settled, and taking the deepest first is what arranges that --
// `try_reuse` hands back a retained size without asking whether
// anything dirty sits under it, so this order is load-bearing for the
// answer and not only for the cost. Equal-depth widgets are
// independent, so their order does not matter. Resize dirtiness already marks whole reader chains, so
// choosing their shallowest roots coalesces descendants that share a
// reader and gives each changing box its final constraints first.
while let Some(id) = {
let dirty = rsc.widgets().needs_redraw.iter().copied();
dirty.max_by_key(|&id| self.depth(id))
} {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::QueuePops);
self.redraw(id, rsc); self.redraw(id, rsc);
} }
rsc.free(); rsc.free();
} }
/// Keeps a reused widget's depth current, since being reused is being
/// visited: only a subtree nobody looked at can hold a stale one.
fn keep_depth(&mut self, id: WidgetId, depth: usize) {
if let Some(active) = self.active.get_mut(&id) {
active.depth = depth;
}
}
fn depth(&self, id: WidgetId) -> usize {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::DepthReads);
let depth = self.active.get(&id).map_or(1, |active| active.depth);
debug_assert_eq!(
depth,
self.walked_depth(id),
"a widget's kept depth is not the one its ancestry says"
);
depth
}
/// What the kept depth is checked against, and the only thing that reads
/// the ancestry to find one.
fn walked_depth(&self, id: WidgetId) -> usize {
let mut depth = 0;
let mut at = Some(id);
while let Some(id) = at {
at = self.active.get(&id).and_then(|active| active.parent);
depth += 1;
}
depth
}
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool { pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
root.into().map(|r| r.id()) != self.old_root root.into().map(|r| r.id()) != self.old_root
} }
@@ -698,9 +296,7 @@ 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.root_changed(root) || self.resized || widgets.has_updates()
|| self.root_axes_changed().iter().any(|&c| c)
|| widgets.has_updates()
} }
pub fn active_widgets(&self) -> usize { pub fn active_widgets(&self) -> usize {
@@ -726,43 +322,22 @@ impl UiRenderState {
} }
} }
/// Where a widget is on screen: its box composed through the boxes it
/// sits within, which is the walk the vertex shader does.
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> { pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let active = self.active.get(&id.id())?; let region = self.active.get(&id.id())?.region;
let region = self.moves.resolve(active.parent_move, active.region);
Some(region.to_px(self.output_size)) Some(region.to_px(self.output_size))
} }
/// 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) { // Whoever read this widget's size may be a different size now, so the
self.invalid_sizes.insert(id); // highest reader is what draws. Everything between the two is marked
} // as well: their own boxes have not changed, so the mark is the only
// A widget can only answer whether its size changed by drawing in the // thing stopping the draw reusing its way past this widget.
// box its parent chose. If that box changed in pixels, its retained if let Some(top) = self.mark_readers(id, rsc) {
// placement is stale and the highest size reader must choose the new
// box first. The same holds when the box was decided from the
// widget's own answer: measuring there again can only repeat it,
// whatever the content now says. 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| {
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)))
});
let top = match box_changed {
true => self.top_reader(id),
false => None,
}
.or_else(|| self.derived_box_reader(id));
if let Some(top) = top {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::EagerReaderRedraws);
self.mark_below(id, top, rsc);
self.redraw(top, rsc); self.redraw(top, rsc);
// Cleared by that draw if it reached here; if it did not, this is
// no longer drawn and asking again would not end.
rsc.widgets_mut().needs_redraw.remove(&id); rsc.widgets_mut().needs_redraw.remove(&id);
return; return;
} }
@@ -775,91 +350,35 @@ impl UiRenderState {
let Some(active) = self.remove(id, false, rsc) else { let Some(active) = self.remove(id, false, rsc) else {
return; return;
}; };
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
let old_size = active.size; self.draw_inner(
let size = self.draw_inner(
active.layer, active.layer,
id, id,
active.region, active.region,
active.parent, active.parent,
active.depth,
active.parent_move,
active.move_idx != active.parent_move,
active.mask, active.mask,
Some(active), Some(active.children),
rsc, rsc,
); );
if size != old_size {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::SizeChanges);
if let Some(parent) = self.active.get(&id).and_then(|active| active.parent)
&& self
.active
.get(&parent)
.is_some_and(|active| active.size_deps.contains(&id))
{
// 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);
}
}
}
/// The highest reader up the chain that gave what it read a box other
/// than the one it asked in, on an axis this widget's size reads. Above
/// it every box is a constraint rather than an answer. It is the highest
/// and not the nearest because a pass-through hands a derived box down
/// unchanged.
fn derived_box_reader(&self, id: WidgetId) -> Option<WidgetId> {
let reads = self.active.get(&id)?.size_box_inputs;
let mut top = None;
for (active, parent) in self.reader_chain(id) {
let px = self.px_of(active.parent_move, active.region);
if AXES.into_iter().zip(reads).any(|(axis, r)| {
r && pixel_len_changed(active.offered_px.axis(axis), px.axis(axis))
}) {
top = Some(parent);
}
}
top
} }
/// The furthest ancestor that read this widget's size, directly or through /// The furthest ancestor that read this widget's size, directly or through
/// widgets that did the same. /// widgets that did the same, marking everything below it on the way.
fn top_reader(&self, id: WidgetId) -> Option<WidgetId> { fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> {
self.reader_chain(id).last().map(|(_, parent)| parent) let mut top = None;
}
/// Each widget from `id` upward whose parent read its size, with that
/// parent.
fn reader_chain(&self, id: WidgetId) -> impl Iterator<Item = (&ActiveData, WidgetId)> {
let mut at = Some(id);
std::iter::from_fn(move || {
let active = self.active.get(&at?)?;
let parent = active.parent?;
let read = self.active.get(&parent)?.size_deps.contains(&active.id);
at = read.then_some(parent);
read.then_some((active, parent))
})
}
/// Marks everything from `id` up to, and not including, `top`, so that
/// drawing `top` draws each of them rather than reusing it.
fn mark_below(&self, id: WidgetId, top: WidgetId, rsc: &mut dyn UiRsc) {
let mut at = id; let mut at = id;
while at != top { while let Some(active) = self.active.get(&at)
&& let Some(parent) = active.parent
&& self
.active
.get(&parent)
.is_some_and(|p| p.size_deps.contains(&at))
{
rsc.widgets_mut().needs_redraw.insert(at); rsc.widgets_mut().needs_redraw.insert(at);
let Some(parent) = self.active.get(&at).and_then(|active| active.parent) else { top = Some(parent);
return;
};
at = parent; at = parent;
} }
top
} }
} }
-4
View File
@@ -34,10 +34,6 @@ impl<T, I: IdNum> Arena<T, I> {
self.tracker.free(id); self.tracker.free(id);
self.data[i] self.data[i]
} }
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
&mut self.data[id.idx()]
}
} }
impl<T, I: IdNum> Default for Arena<T, I> { impl<T, I: IdNum> Default for Arena<T, I> {
+10 -1
View File
@@ -1,5 +1,6 @@
pub const trait LerpUtil { pub const trait LerpUtil: Sized {
fn lerp(self, from: Self, to: Self) -> Self; fn lerp(self, from: Self, to: Self) -> Self;
fn lerp_inv(self, from: Self, to: Self) -> Option<Self>;
} }
const impl LerpUtil for f32 { const impl LerpUtil for f32 {
@@ -8,6 +9,14 @@ const impl LerpUtil for f32 {
fn lerp(self, from: Self, to: Self) -> Self { fn lerp(self, from: Self, to: Self) -> Self {
from + (to - from) * self from + (to - from) * self
} }
/// inverse of lerp, and `None` where `from` and `to` are the same point:
/// every input lerps to it, so there is no one answer to come back to.
fn lerp_inv(self, from: Self, to: Self) -> Option<Self> {
match to == from {
true => None,
false => Some((self - from) / (to - from)),
}
}
} }
macro_rules! impl_op { macro_rules! impl_op {
+5
View File
@@ -1,3 +1,8 @@
#[allow(clippy::missing_safety_doc)]
pub(crate) unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
unsafe { std::mem::transmute::<&T, &T>(x) }
}
#[allow(clippy::missing_safety_doc)] #[allow(clippy::missing_safety_doc)]
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T { pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
unsafe { std::mem::transmute::<&mut T, &mut T>(x) } unsafe { std::mem::transmute::<&mut T, &mut T>(x) }
+1 -5
View File
@@ -1,11 +1,7 @@
use crate::util::impl_op; use crate::util::impl_op;
use std::{hash::Hash, ops::*}; use std::{hash::Hash, ops::*};
/// `align(8)` because that is WGSL's alignment for a `vec2<f32>`, so any GPU #[repr(C)]
/// struct holding one is laid out the way its shader reads it without having
/// to say so itself. Those structs still need a manual `unsafe impl Pod`,
/// since the trailing padding this introduces is what `derive(Pod)` refuses.
#[repr(C, align(8))]
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vec2 { pub struct Vec2 {
pub x: f32, pub x: f32,
-3
View File
@@ -21,9 +21,6 @@ pub use widgets::*;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OnResize { pub enum OnResize {
Scale, Scale,
/// Reserved: nothing reads this yet, so a widget saying it is redrawn.
/// Keeping an unchanged drawing in a bigger box needs the widget to say
/// *where* in that box it should sit, which is the alignment work.
Translate, Translate,
#[default] #[default]
Redraw, Redraw,
-31
View File
@@ -1,31 +0,0 @@
//! The seeded random tree `tests/generated.rs` checks, drawn so it can be
//! looked at. `IRIS_SEED` and `IRIS_DEPTH` choose which one.
use iris::prelude::*;
use iris::random::Edits;
fn env(name: &str, fallback: u64) -> u64 {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(fallback)
}
fn main() {
DefaultApp::<State>::run();
}
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
let seed = env("IRIS_SEED", 1);
let depth = env("IRIS_DEPTH", 4) as usize;
let (root, _) = iris::random::grow(rsc, seed, depth, &Edits::default());
ui_state.set_root(root);
Self { ui_state }
}
}
+1 -13
View File
@@ -17,10 +17,6 @@
# custom one would otherwise inherit the other's output and quietly screenshot # custom one would otherwise inherit the other's output and quietly screenshot
# the wrong size. # the wrong size.
# #
# `--resize WxH@Hz` changes the output under the app once it is up, then
# screenshots. A resize is its own case: what it has to match is a cold start
# at that size, byte for byte, and nothing in `cargo test` can see it.
#
# `--replay FILE` drives a `.touch` recording into the window through # `--replay FILE` drives a `.touch` recording into the window through
# `replay-touch`, which reads it with the same parser `iris::harness` uses. A # `replay-touch`, which reads it with the same parser `iris::harness` uses. A
# recording is `<ms> down|move|up <x> <y>` in the output's own pixels. With # recording is `<ms> down|move|up <x> <y>` in the output's own pixels. With
@@ -50,7 +46,6 @@ run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless"
seconds=3 seconds=3
shot="" shot=""
replay="" replay=""
resize=""
example="" example=""
kind=example kind=example
mode=1920x1200@60Hz mode=1920x1200@60Hz
@@ -62,14 +57,13 @@ while [ $# -gt 0 ]; do
--seconds) seconds=$2; shift 2 ;; --seconds) seconds=$2; shift 2 ;;
--bin) kind=bin; shift ;; --bin) kind=bin; shift ;;
--mode) mode=$2; shift 2 ;; --mode) mode=$2; shift 2 ;;
--resize) resize=$2; shift 2 ;;
--replay) replay=$2; shift 2 ;; --replay) replay=$2; shift 2 ;;
--dir) workdir=$(cd "$2" && pwd); shift 2 ;; --dir) workdir=$(cd "$2" && pwd); shift 2 ;;
--) shift; break ;; --) shift; break ;;
*) example=$1; shift ;; *) example=$1; shift ;;
esac esac
done done
[ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--dir DIR] [--mode WxH@Hz] [--resize WxH@Hz] [--replay TOUCH] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; } [ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--dir DIR] [--mode WxH@Hz] [--replay TOUCH] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; }
[ -z "$replay" ] || [ -f "$replay" ] || { echo "run-headless: no touch script at $replay" >&2; exit 2; } [ -z "$replay" ] || [ -f "$replay" ] || { echo "run-headless: no touch script at $replay" >&2; exit 2; }
[ -z "$shot" ] || need grim "the screenshot --shot writes" [ -z "$shot" ] || need grim "the screenshot --shot writes"
@@ -148,12 +142,6 @@ while [ $i -lt "$((seconds * 2))" ]; do
i=$((i + 1)); sleep 0.5 i=$((i + 1)); sleep 0.5
done done
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
swaymsg output HEADLESS-1 mode "$resize" >/dev/null
echo "run-headless: resized to $resize" >&2
sleep 2
fi
if [ -n "$replay" ] && kill -0 "$pid" 2>/dev/null; then if [ -n "$replay" ] && kill -0 "$pid" 2>/dev/null; then
if [ -n "$shot" ]; then if [ -n "$shot" ]; then
grim "${shot%.png}-before.png" grim "${shot%.png}-before.png"
-1
View File
@@ -8,7 +8,6 @@
pub mod default; pub mod default;
pub mod event; pub mod event;
pub mod harness; pub mod harness;
pub mod random;
pub mod widget; pub mod widget;
pub use iris_core as core; pub use iris_core as core;
-317
View File
@@ -1,317 +0,0 @@
//! A seeded random widget tree, for tests and for looking at.
//!
//! One seed is one tree, on any machine and after any upgrade, so a test can
//! grow the same tree twice and a failing seed is reproduced by its number.
//! `examples/random.rs` draws one; `tests/generated.rs` checks that laying one
//! out again lands where growing it from scratch would.
use crate::prelude::*;
use std::collections::HashMap;
/// The declared lengths of one `SetSize`, by axis.
pub type Lens = [Option<Len>; 2];
/// What a test changes between two trees grown from the same seed, so the
/// warm one can be mutated and the cold one grown that way to begin with.
#[derive(Default)]
pub struct Edits {
/// Declared sizes, by the order the `SetSize` wrappers were made.
pub sizes: HashMap<usize, Lens>,
/// Which children a span has, by the order the spans were made.
pub spans: HashMap<usize, SpanEdit>,
}
#[derive(Default, Clone)]
pub struct SpanEdit {
/// Children to leave out, by index among the ones grown.
pub detach: Vec<usize>,
/// How many of the span's spares are in it, appended in order.
pub attach: usize,
}
/// xorshift64, written out rather than taken from a crate so that a seed
/// keeps meaning the same tree.
pub struct Rng(u64);
impl Rng {
pub fn new(seed: u64) -> Self {
Self(seed | 1)
}
pub fn bits(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
pub fn below(&mut self, n: usize) -> usize {
(self.bits() % n as u64) as usize
}
pub fn chance(&mut self) -> bool {
self.bits() & 1 == 0
}
}
const COLORS: [UiColor; 6] = [
UiColor::RED,
UiColor::GREEN,
UiColor::BLUE,
UiColor::YELLOW,
UiColor::CYAN,
UiColor::MAGENTA,
];
/// Leaves grown beside every span, for a test to put into it.
const SPARES: usize = 3;
const WORDS: &str = "Wrapping shapes one source into as many lines as the box \
leaves room for, so a paragraph's height is an answer and not a setting.";
/// What growing a tree gives back: every widget in creation order, so two
/// trees from one seed line up index for index, and the declared sizes, which
/// are what a test changes to watch the change propagate.
#[derive(Default)]
pub struct Tree {
pub ids: Vec<WidgetId>,
pub sized: Vec<WeakWidget<SetSize>>,
pub spans: Vec<Spanned>,
pub scrolls: Vec<WeakWidget<Scroll>>,
/// Children a `SpanEdit` took out, held so that dropping the last share
/// of one does not free its id for the next widget to be given -- which
/// would put the two trees' `ids` out of step.
pub detached: Vec<StrongWidget>,
}
/// Branches on a child's measured length. Comparing boxes catches a widget
/// that moved; this catches one that believed a measurement a cold start
/// would not have given it, by turning that into a different tree. Its own
/// configuration never changes, so which side draws is a property of the
/// layout alone.
pub struct Branch {
pub probe: StrongWidget,
pub wide: StrongWidget,
pub narrow: StrongWidget,
pub threshold: f32,
}
impl Widget for Branch {
fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL;
top.y.end = top.y.start.offset(40.0);
let measured = painter.place(&self.probe, top).len(Axis::X);
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
let mut rest = UiRegion::FULL;
rest.y.start = rest.y.start.offset(40.0);
match px > self.threshold {
true => painter.place(&self.wide, rest),
false => painter.place(&self.narrow, rest),
};
Size::REST
}
}
pub struct Spanned {
pub id: WeakWidget<Span>,
/// Leaves grown with the span whether or not they end up in it, so both
/// trees make the same widgets in the same order either way. Attaching
/// one moves it out of here: a widget belongs to one parent, and one that
/// belongs to nobody still has to be held or it reads as a leak.
pub spares: Vec<StrongWidget>,
/// How many children it was grown with, before any edit.
pub grown: usize,
}
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it
/// would otherwise have given those wrappers.
pub fn grow<Rsc: UiRsc + 'static>(
rsc: &mut Rsc,
seed: u64,
depth: usize,
edits: &Edits,
) -> (StrongWidget, Tree) {
let mut grow = Grow {
rsc,
rng: Rng::new(seed),
tree: Tree::default(),
edits,
};
let root = grow.node(depth);
(root, grow.tree)
}
struct Grow<'a, Rsc> {
rsc: &'a mut Rsc,
rng: Rng,
tree: Tree,
edits: &'a Edits,
}
impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
fn leaf(&mut self) -> StrongWidget {
let id: StrongWidget = match self.rng.below(4) {
// Wrapped and unwrapped, because only one of them reads the width
// it is given and so only one has to be drawn again for a new one.
0 => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
1 => wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add_strong(self.rsc),
_ => {
let color = COLORS[self.rng.below(COLORS.len())];
let alpha = (self.rng.below(5) * 63) as u8;
rect(color.alpha(alpha)).add_strong(self.rsc)
}
};
self.tree.ids.push(id.id());
id
}
fn len(&mut self) -> Option<Len> {
match self.rng.below(4) {
0 => Some(Len::px(20.0 + self.rng.below(180) as f32)),
1 => Some(Len::REST),
_ => None,
}
}
fn align(&mut self) -> Align {
let mut axis = || match self.rng.below(4) {
0 => None,
1 => Some(AxisAlign::Neg),
2 => Some(AxisAlign::Center),
_ => Some(AxisAlign::Pos),
};
let (mut x, y) = (axis(), axis());
// Aligning on neither axis is just another transparent wrapper and
// would leave this branch unexercised.
if x.is_none() && y.is_none() {
x = Some(AxisAlign::Center);
}
Align { x, y }
}
/// A declared size over half the tree, kept where a test can change it.
fn sized(&mut self, inner: StrongWidget) -> StrongWidget {
if !self.rng.chance() {
return inner;
}
let idx = self.tree.sized.len();
let lens = [self.len(), self.len()];
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
let id = SetSize {
inner,
x: lens[0],
y: lens[1],
}
.add(self.rsc);
self.tree.sized.push(id);
self.tree.ids.push(id.id());
id.add_strong(self.rsc)
}
fn node(&mut self, depth: usize) -> StrongWidget {
if depth == 0 {
return self.leaf();
}
let positioned = self.rng.below(6);
if positioned == 0 {
// Scrolling reads the pixel length of its box, which nothing
// else here does, and gives its child a box longer than its own.
let inner = self.node(depth - 1);
let inner = self.sized(inner);
let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
let id = Scroll::new(inner, axis).add(self.rsc);
self.tree.scrolls.push(id);
self.tree.ids.push(id.id());
return id.add_strong(self.rsc);
}
if positioned == 2 {
// Both sides are grown either way, so a tree that draws one has
// the same ids as a tree that draws the other.
let probe = self.node(depth - 1);
let wide = self.node(depth - 1);
let narrow = self.node(depth - 1);
let threshold = self.rng.below(500) as f32;
let id = Branch {
probe,
wide,
narrow,
threshold,
}
.add(self.rsc);
self.tree.ids.push(id.id());
return id.add_strong(self.rsc);
}
if positioned == 1 {
let inner = self.node(depth - 1);
let inner = self.sized(inner);
let id = Aligned {
inner,
align: self.align(),
}
.add_strong(self.rsc);
self.tree.ids.push(id.id());
return id;
}
if self.rng.below(4) == 0 {
let inner = self.node(depth - 1);
let inner = self.sized(inner);
// Each side its own, since a padding that is the same all round
// hides anything that treats one edge differently from another.
let mut side = || self.rng.below(24) as f32;
let padding = Padding {
left: side(),
right: side(),
top: side(),
bottom: side(),
};
let id = Pad { padding, inner }.add_strong(self.rsc);
self.tree.ids.push(id.id());
return id;
}
let grown = 2 + self.rng.below(3);
let mut children = Vec::with_capacity(grown);
for _ in 0..grown {
let child = self.node(depth - 1);
children.push(self.sized(child));
}
if self.rng.chance() {
let id = Stack {
children,
size: StackSize::Child(0),
}
.add_strong(self.rsc);
self.tree.ids.push(id.id());
return id;
}
// Grown either way, so the widget after them has the same id in a
// tree that leaves them out as in one that puts them in.
let mut spares: Vec<StrongWidget> = (0..SPARES).map(|_| self.leaf()).collect();
let idx = self.tree.spans.len();
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
// Highest first, so an index means the same child however many of its
// neighbours are going too.
let mut detach = edit.detach.clone();
detach.sort_unstable();
for j in detach.into_iter().rev() {
if j < children.len() {
self.tree.detached.push(children.remove(j));
}
}
let attach = edit.attach.min(spares.len());
children.extend(spares.drain(..attach));
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)];
let id = Span {
children,
dir,
gap: self.rng.below(3) as f32 * 4.0,
}
.add(self.rsc);
self.tree.ids.push(id.id());
self.tree.spans.push(Spanned { id, spares, grown });
id.add_strong(self.rsc)
}
}
+2 -2
View File
@@ -8,11 +8,11 @@ pub struct Image {
impl Widget for Image { impl Widget for Image {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.primitive(&self.handle); painter.primitive(&self.handle);
Size::px(self.handle.size()) Size::abs(self.handle.size())
} }
fn size_hint(&self, axis: Axis) -> Option<Len> { fn size_hint(&self, axis: Axis) -> Option<Len> {
Some(Len::px(self.handle.size().axis(axis))) Some(Len::abs(self.handle.size().axis(axis)))
} }
fn on_resize(&self, _: Axis) -> OnResize { fn on_resize(&self, _: Axis) -> OnResize {
+5 -25
View File
@@ -7,36 +7,16 @@ 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 {
let known = match self.align.tuple() { // Drawn where it may be too big, then given its aligned box once its
(Some(_), Some(_)) => painter // size is known.
.known_len(&self.inner, Axis::X, UiRegion::FULL) let size = painter.widget(&self.inner).size();
.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,
}; };
let placed = painter.place(&self.inner, region).size(); painter.widget_within(&self.inner, region);
if had_size { placed } else { size } size
}
/// The aligned box is a fraction of its own, so the child keeps its
/// length and stays against the edge it was aligned to.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
} }
} }
-4
View File
@@ -12,8 +12,4 @@ impl Widget for LayerOffset {
} }
painter.widget(&self.inner).size() painter.widget(&self.inner).size()
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
+1 -1
View File
@@ -19,7 +19,7 @@ impl Widget for MaxSize {
fn capped(len: Len, max: Option<Len>, output: f32) -> Len { fn capped(len: Len, max: Option<Len>, output: f32) -> Len {
match max { match max {
Some(max) if len.apply_rest().to_px(output) > max.apply_rest().to_px(output) => max, Some(max) if len.apply_rest().to_abs(output) > max.apply_rest().to_abs(output) => max,
_ => len, _ => len,
} }
} }
-4
View File
@@ -10,8 +10,4 @@ impl Widget for Offset {
let region = UiRegion::FULL.offset(self.amt); let region = UiRegion::FULL.offset(self.amt);
painter.widget_within(&self.inner, region).size() painter.widget_within(&self.inner, region).size()
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
+6 -12
View File
@@ -12,21 +12,15 @@ impl Widget for Pad {
.size(); .size();
Size { Size {
x: Len { x: Len {
px: inner.x.px + self.padding.left + self.padding.right, abs: inner.x.abs + self.padding.left + self.padding.right,
..inner.x ..inner.x
}, },
y: Len { y: Len {
px: inner.y.px + self.padding.top + self.padding.bottom, abs: inner.y.abs + self.padding.top + self.padding.bottom,
..inner.y ..inner.y
}, },
} }
} }
/// The padding is an offset from each edge, so a longer box pads the same
/// amount and the child takes the rest.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
pub struct Padding { pub struct Padding {
@@ -55,10 +49,10 @@ impl Padding {
} }
pub fn region(&self) -> UiRegion { pub fn region(&self) -> UiRegion {
let mut region = UiRegion::FULL; let mut region = UiRegion::FULL;
region.x.start.px += self.left; region.x.start.abs += self.left;
region.y.start.px += self.top; region.y.start.abs += self.top;
region.x.end.px -= self.right; region.x.end.abs -= self.right;
region.y.end.px -= self.bottom; region.y.end.abs -= self.bottom;
region region
} }
pub fn x(amt: impl UiNum) -> Self { pub fn x(amt: impl UiNum) -> Self {
+11 -14
View File
@@ -11,20 +11,17 @@ 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_len(self.axis); let output_len = painter.output_size().axis(self.axis);
// Its size is its content's, whatever box that is scrolled within. let container_len = painter.region().axis(self.axis).len();
let container_len = UiScalar::px(painter.px_len_for_draw(self.axis)); // Drawn in the whole container to learn its length, then placed at
// Draw in the whole container only when its scrolling-axis length is // the scrolled offset.
// not already known, then place it at the scrolled offset. let child = painter.widget(&self.inner).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_px(output_len); .to_abs(output_len);
self.container_len = container_len.to_px(output_len); self.container_len = container_len.to_abs(output_len);
self.content_len = content_len; self.content_len = content_len;
if self.snap_end { if self.snap_end {
@@ -34,8 +31,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);
let placed = painter.place(&self.inner, region).size(); painter.widget_within(&self.inner, region);
child.unwrap_or(placed) child
} }
} }
+1 -18
View File
@@ -8,20 +8,7 @@ pub struct SetSize {
impl Widget for SetSize { impl Widget for SetSize {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
// A declared length is what the child gets, whatever box this widget let child = painter.widget(&self.inner).size();
// was offered before its parent knew that. Measuring it anywhere else
// asks about a box it will not have, and the answer on the other axis
// is taken under that: a wrapping text measured in the whole width
// reports one line, and nothing revisits it once the real width
// arrives.
let mut region = UiRegion::FULL;
for (axis, len) in [(Axis::X, self.x), (Axis::Y, self.y)] {
if let Some(len) = len {
let span = region.axis_mut(axis);
span.end = span.start + len.apply_rest();
}
}
let child = painter.widget_within(&self.inner, region).size();
Size { Size {
x: self.x.unwrap_or(child.x), x: self.x.unwrap_or(child.x),
y: self.y.unwrap_or(child.y), y: self.y.unwrap_or(child.y),
@@ -36,8 +23,4 @@ impl Widget for SetSize {
Axis::Y => self.y, Axis::Y => self.y,
} }
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
+17 -35
View File
@@ -12,25 +12,17 @@ impl Widget for Span {
let axis = self.dir.axis; let axis = self.dir.axis;
// A length for every child before any is placed: from its own hint // A length for every child before any is placed: from its own hint
// where it has one, and from drawing it where it does not. // where it has one, and from drawing it where it does not.
let mut cursor = UiScalar::rel_min(); let lens: Vec<Len> = self
let mut lens = Vec::with_capacity(self.children.len()); .children
for child in &self.children { .iter()
let mut span = UiSpan::new(cursor, UiScalar::rel_max()); .map(|child| match painter.size_hint(child, axis) {
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 => painter.place(child, region).len(axis), None => painter.widget(child).len(axis),
}; })
cursor.px += len.px + self.gap; .collect();
cursor.rel += len.rel;
lens.push(len);
}
let gap = self.gap * self.children.len().saturating_sub(1) as f32; let gap = self.gap * self.children.len().saturating_sub(1) as f32;
let total = lens.iter().fold(Len::px(gap), |sum, len| sum + *len); let total = lens.iter().fold(Len::abs(gap), |sum, len| sum + *len);
let mut start = UiScalar::rel_min(); let mut start = UiScalar::rel_min();
let mut ortho = Len::ZERO; let mut ortho = Len::ZERO;
@@ -38,44 +30,34 @@ impl Widget for Span {
let mut span = UiSpan::FULL; let mut span = UiSpan::FULL;
span.start = start; span.start = start;
if len.rest > 0.0 { if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.px); let offset = UiScalar::new(total.rel, total.abs);
let rel_end = UiScalar::rel(len.rest / total.rest); let rel_end = UiScalar::rel(len.rest / total.rest);
let end = (UiScalar::rel_max() + start) - offset; let end = (UiScalar::rel_max() + start) - offset;
start = rel_end.within(&start.to(end)); start = rel_end.within(&start.to(end));
} }
start.px += len.px; start.abs += len.abs;
start.rel += len.rel; start.rel += len.rel;
span.end = start; span.end = start;
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL); let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg { if self.dir.sign == Sign::Neg {
region.flip(axis); region.flip(axis);
} }
let used = painter.place(child, region).size().axis(!axis); let used = painter.widget_within(child, region).size().axis(!axis);
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels // TODO: rel shouldn't do this, but no easy way before actually calculating pixels
if used.rel > 0.0 || used.rest > 0.0 { if used.rel > 0.0 || used.rest > 0.0 {
ortho = Len::REST; ortho = Len::REST;
} else if ortho.rest == 0.0 { } else if ortho.rest == 0.0 {
ortho.px = ortho.px.max(used.px); ortho.abs = ortho.abs.max(used.abs);
} }
start.px += self.gap; start.abs += self.gap;
} }
// Carried whole rather than collapsed to one share: a span that sizes let along = match total.rest == 0.0 && total.rel == 0.0 {
// from its children does not resolve `rest`, it passes the weight up, true => total,
// so nesting spans divides the same space rather than re-dividing a false => Len::default(),
// share of it. Four `rest(1)` children under two spans under one span };
// get a quarter each, which collapsing to `rest(1)` per level does
// not give. Resolution happens at the nearest ancestor with a length,
// and the root always has one.
let along = total;
Size::from_axis(axis, along, ortho) Size::from_axis(axis, along, ortho)
} }
/// Every child is placed in fractions and offsets of the span's own box,
/// so a longer box holds the same layout and the children follow it.
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
impl Span { impl Span {
-4
View File
@@ -28,10 +28,6 @@ impl Widget for Stack {
} }
size size
} }
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
+5 -1
View File
@@ -130,6 +130,7 @@ impl<'a> TextEditCtx<'a> {
pub fn set(&mut self, text: &str) { pub fn set(&mut self, text: &str) {
let text = self.string(text); let text = self.string(text);
self.text.view.buf.set_text(text); self.text.view.buf.set_text(text);
self.text.view.buf.changed = true;
self.text.selection = None; self.text.selection = None;
} }
@@ -176,6 +177,7 @@ impl<'a> TextEditCtx<'a> {
}; };
let at = at.min(self.text.view.buf.text().len()); let at = at.min(self.text.view.buf.text().len());
self.text.view.buf.edit().insert_str(at, text); self.text.view.buf.edit().insert_str(at, text);
self.text.view.buf.changed = true;
self.set_caret(at + text.len()); self.set_caret(at + text.len());
} }
@@ -188,6 +190,7 @@ impl<'a> TextEditCtx<'a> {
} }
let range = sel.text_range(); let range = sel.text_range();
self.text.view.buf.edit().replace_range(range.clone(), ""); self.text.view.buf.edit().replace_range(range.clone(), "");
self.text.view.buf.changed = true;
self.set_caret(range.start); self.set_caret(range.start);
true true
} }
@@ -265,6 +268,7 @@ impl<'a> TextEditCtx<'a> {
fn delete_range(&mut self, start: usize, end: usize) { fn delete_range(&mut self, start: usize, end: usize) {
self.text.view.buf.edit().replace_range(start..end, ""); self.text.view.buf.edit().replace_range(start..end, "");
self.text.view.buf.changed = true;
self.set_caret(start); self.set_caret(start);
} }
@@ -280,7 +284,7 @@ impl<'a> TextEditCtx<'a> {
} }
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) { pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
let pos = pos - self.text.region().top_left().to_px(size); let pos = pos - self.text.region().top_left().to_abs(size);
let prev_sel = self.text.selection; let prev_sel = self.text.selection;
let prev_hit = self.text.double_hit; let prev_hit = self.text.double_hit;
+23 -12
View File
@@ -14,8 +14,11 @@ pub struct Text {
} }
pub struct TextView { pub struct TextView {
pub attrs: TextAttrs, pub attrs: MutDetect<TextAttrs>,
pub buf: TextBuffer, pub buf: MutDetect<TextBuffer>,
// cache
tex: Option<RenderedText>,
width: Option<f32>,
pub hint: Option<StrongWidget>, pub hint: Option<StrongWidget>,
} }
@@ -25,13 +28,19 @@ impl TextView {
} }
pub fn wrap_width(&self) -> Option<f32> { pub fn wrap_width(&self) -> Option<f32> {
self.buf.wrap_width() self.width
} }
} }
impl TextView { impl TextView {
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self { pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
Self { attrs, buf, hint } Self {
attrs: attrs.into(),
buf: buf.into(),
tex: None,
width: None,
hint,
}
} }
/// region where the text should be draw /// region where the text should be draw
@@ -43,20 +52,22 @@ impl TextView {
.align(self.align) .align(self.align)
} }
/// The text shaped for the width it is drawn in. The buffer keeps its
/// answers under the attrs too, so changing those asks a new question
/// rather than invalidating anything.
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_len(Axis::X)) Some(painter.px_size().x)
} else { } else {
None None
}; };
painter.render_text(&mut self.buf, &self.attrs, width) if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
self.width = width;
self.tex = Some(painter.render_text(&mut self.buf, &self.attrs, width));
self.attrs.changed = false;
self.buf.changed = false;
}
self.tex.as_ref().unwrap()
} }
pub fn tex(&self) -> Option<&RenderedText> { pub fn tex(&self) -> Option<&RenderedText> {
self.buf.rendered() self.tex.as_ref()
} }
/// Draws the text, and says where the glyphs went and what they use. /// Draws the text, and says where the glyphs went and what they use.
pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) { pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) {
@@ -72,7 +83,7 @@ impl TextView {
let tex = self.render(painter); let tex = self.render(painter);
let region = tex.size.align(align); let region = tex.size.align(align);
let size = Size::px(tex.size); let size = Size::abs(tex.size);
let within = region.within(&painter.region()); let within = region.within(&painter.region());
painter.glyphs(tex, within); painter.glyphs(tex, within);
(region, size) (region, size)
-224
View File
@@ -1,224 +0,0 @@
//! What the vertex shader's move-chain walk costs, against how deep the chain
//! is. Every active widget owns a slot, so the depth a primitive resolves
//! through is its depth in the widget tree.
//!
//! cargo test --release --test chain_cost -- --ignored --nocapture
//!
//! Timed on the GPU with timestamp queries rather than by the clock: wall time
//! here varied by 2x between runs of one unchanged binary. The pass is
//! submitted and waited on, so this is the GPU's cost and not the recording
//! loop's -- which is what `draw_cost.rs` measures instead.
//!
//! The instances are two pixels wide so that vertex work dominates; a chain
//! walk that does not show up against small quads will not show up against
//! anything.
//!
//! The instance is leaked deliberately, for the reason `draw_cost.rs` gives.
use iris::prelude::*;
use iris_core::{
MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
UiScalar, UiSpan,
};
use wgpu::{Color as GpuColor, *};
const SIZE: u32 = 1024;
const INSTANCES: usize = 200_000;
const FRAMES: u32 = 20;
/// Reported as the best of this many batches, since the mean moves by more
/// than the thing being measured.
const BATCHES: u32 = 8;
fn gpu() -> Option<(Device, Queue, f32)> {
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
{
Ok(_) => all,
Err(_) => Instance::new(InstanceDescriptor {
backends: Backends::GL,
..InstanceDescriptor::new_without_display_handle()
}),
};
let instance: &'static Instance = Box::leak(Box::new(instance));
let adapter =
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
println!("no timestamp queries on {:?}", adapter.get_info().name);
return None;
}
println!("adapter: {:?}", adapter.get_info().name);
let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor {
required_features: Features::TIMESTAMP_QUERY,
..Default::default()
}))
.ok()?;
let period = queue.get_timestamp_period();
Some((device, queue, period))
}
fn config(format: TextureFormat) -> SurfaceConfiguration {
SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format,
color_space: SurfaceColorSpace::Auto,
width: SIZE,
height: SIZE,
present_mode: PresentMode::Fifo,
desired_maximum_frame_latency: 2,
alpha_mode: CompositeAlphaMode::Auto,
view_formats: vec![],
}
}
/// A chain `depth` slots long, and instances that all resolve through its end.
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
let kind = ui.primitives.kind::<RectPrimitive>();
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
let mut slot = MoveIdx::NONE;
for _ in 0..depth {
slot = render.moves.push(slot, UiRegion::FULL);
}
let px = |v: f32| UiScalar { rel: 0.0, px: v };
for i in 0..INSTANCES {
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
let y = (i / (SIZE as usize / 2)) as f32;
render.layers.write(
0,
PrimitiveInst {
kind,
id,
primitive: RectPrimitive::color(UiColor::WHITE),
region: UiRegion::new(
UiSpan::new(px(x), px(x + 2.0)),
UiSpan::new(px(y), px(y + 1.0)),
),
mask_idx: MaskIdx::NONE,
move_idx: slot,
},
);
}
}
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
let format = TextureFormat::Bgra8Unorm;
let mut node = UiRenderNode::new(device, &config(format));
let mut ui = UiData::default();
let mut render = UiRenderState::new();
fill(&mut ui, &mut render, depth);
node.update(device, queue, &mut ui, &mut render);
let target = device.create_texture(&TextureDescriptor {
label: Some("chain cost"),
size: Extent3d {
width: SIZE,
height: SIZE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format,
usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = target.create_view(&TextureViewDescriptor::default());
let queries = device.create_query_set(&QuerySetDescriptor {
label: Some("chain cost"),
ty: QueryType::Timestamp,
count: 2,
});
let resolved = device.create_buffer(&BufferDescriptor {
label: Some("resolved"),
size: 16,
usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let readback = device.create_buffer(&BufferDescriptor {
label: Some("readback"),
size: 16,
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let frame = || {
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
{
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
label: None,
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(GpuColor::BLACK),
store: StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: Some(RenderPassTimestampWrites {
query_set: &queries,
beginning_of_pass_write_index: Some(0),
end_of_pass_write_index: Some(1),
}),
occlusion_query_set: None,
multiview_mask: None,
});
node.draw(pass);
}
encoder.resolve_query_set(&queries, 0..2, &resolved, 0);
encoder.copy_buffer_to_buffer(&resolved, 0, &readback, 0, 16);
queue.submit(Some(encoder.finish()));
let slice = readback.slice(..);
slice.map_async(MapMode::Read, |_| {});
let _ = device.poll(PollType::Wait {
submission_index: None,
timeout: None,
});
let ns = {
let view = slice.get_mapped_range().expect("timestamps did not map");
let stamps: [u64; 2] = [
u64::from_le_bytes(view[..8].try_into().unwrap()),
u64::from_le_bytes(view[8..16].try_into().unwrap()),
];
(stamps[1].saturating_sub(stamps[0])) as f64 * period as f64
};
readback.unmap();
ns
};
frame();
let mut best = f64::MAX;
for _ in 0..BATCHES {
let mut total = 0.0;
for _ in 0..FRAMES {
total += frame();
}
best = best.min(total / FRAMES as f64);
}
best
}
#[test]
#[ignore = "measurement, not a check"]
fn chain_cost_by_depth() {
let Some((device, queue, period)) = gpu() else {
println!("no gpu with timestamps; nothing measured");
return;
};
println!("{INSTANCES} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
let mut base = None;
for depth in [1, 2, 4, 8, 16, 32, 64] {
let ns = pass_cost(&device, &queue, period, depth);
let base = *base.get_or_insert(ns);
println!(
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
ns / 1000.0,
(ns - base) / base * 100.0
);
}
}
-101
View File
@@ -1,101 +0,0 @@
//! A measurement that decides control flow.
//!
//! Comparing boxes catches a widget that moved. It does not catch a widget
//! that measured a child, believed a different answer from the one a cold
//! start would give, and took the other branch -- which is the same defect
//! arriving somewhere it cannot be ignored. A widget here branches on what it
//! measured, so a disagreement shows up as a different tree.
use iris::harness::Harness;
use iris::prelude::*;
/// Measures `probe` across `axis` and draws one of two children on the
/// answer. Its own configuration never changes, so which child is drawn is a
/// property of the layout alone.
struct BranchesOnMeasurement {
probe: StrongWidget,
wide: StrongWidget,
narrow: StrongWidget,
threshold: f32,
}
impl Widget for BranchesOnMeasurement {
fn draw(&mut self, painter: &mut Painter) -> Size {
let mut top = UiRegion::FULL;
top.y.end = top.y.start.offset(40.0);
let measured = painter.place(&self.probe, top).len(Axis::X);
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
let mut rest = UiRegion::FULL;
rest.y.start = rest.y.start.offset(40.0);
match px > self.threshold {
true => painter.place(&self.wide, rest),
false => painter.place(&self.narrow, rest),
};
Size::REST
}
}
fn plant(h: &mut Harness, threshold: f32) -> (WidgetId, WidgetId) {
let words = "the quick brown fox jumps over the lazy dog and keeps running";
let probe = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let wide = rect(Color::RED).add(&mut h.rsc);
let narrow = rect(Color::BLUE).add(&mut h.rsc);
let branch = BranchesOnMeasurement {
probe: probe.add_strong(&mut h.rsc),
wide: wide.add_strong(&mut h.rsc),
narrow: narrow.add_strong(&mut h.rsc),
threshold,
}
.add(&mut h.rsc);
let side = rect(Color::GREEN).width(120).add(&mut h.rsc);
h.set_root((side, branch).span(Dir::RIGHT));
(wide.id(), narrow.id())
}
/// Which of the two branches drew, as a pair a test can compare.
fn taken(h: &Harness, wide: WidgetId, narrow: WidgetId) -> (bool, bool) {
(h.region(&wide).is_some(), h.region(&narrow).is_some())
}
#[test]
fn a_branch_taken_on_a_measurement_holds_across_repaints() {
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
let mut h = Harness::new((900, 600));
let (wide, narrow) = plant(&mut h, threshold);
let first = taken(&h, wide, narrow);
assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
for frame in 0..4 {
h.rsc.widgets_mut().get_dyn_mut(wide);
h.rsc.widgets_mut().get_dyn_mut(narrow);
h.frame();
assert_eq!(
taken(&h, wide, narrow),
first,
"threshold {threshold}, repaint {frame}: the branch moved when nothing did"
);
}
}
}
#[test]
fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
let mut warm = Harness::new((900, 600));
let (wide, narrow) = plant(&mut warm, threshold);
warm.resize((640, 480));
warm.frame();
warm.rsc.widgets_mut().get_dyn_mut(wide);
warm.frame();
let mut cold = Harness::new((640, 480));
let (cwide, cnarrow) = plant(&mut cold, threshold);
assert_eq!(
taken(&warm, wide, narrow),
taken(&cold, cwide, cnarrow),
"threshold {threshold}: warm and cold took different branches"
);
}
}
+2 -5
View File
@@ -22,8 +22,8 @@ use std::time::Instant;
use iris::prelude::*; use iris::prelude::*;
use iris_core::{ use iris_core::{
GlyphPrimitive, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, TextureHandle, GlyphPrimitive, MaskIdx, PrimitiveInst, RectPrimitive, TextureHandle, TexturePrimitive, UiData,
TexturePrimitive, UiData, UiRegion, UiRenderNode, UiRenderState, UiRegion, UiRenderNode, UiRenderState,
}; };
use wgpu::{Color as GpuColor, *}; use wgpu::{Color as GpuColor, *};
@@ -95,7 +95,6 @@ fn fill(
primitive: RectPrimitive::color(UiColor::WHITE), primitive: RectPrimitive::color(UiColor::WHITE),
region: UiRegion::FULL, region: UiRegion::FULL,
mask_idx: MaskIdx::NONE, mask_idx: MaskIdx::NONE,
move_idx: MoveIdx::NONE,
}, },
); );
render.layers.write( render.layers.write(
@@ -112,7 +111,6 @@ fn fill(
}, },
region: UiRegion::FULL, region: UiRegion::FULL,
mask_idx: MaskIdx::NONE, mask_idx: MaskIdx::NONE,
move_idx: MoveIdx::NONE,
}, },
); );
} }
@@ -125,7 +123,6 @@ fn fill(
primitive: TexturePrimitive::from(h), primitive: TexturePrimitive::from(h),
region: UiRegion::FULL, region: UiRegion::FULL,
mask_idx: MaskIdx::NONE, mask_idx: MaskIdx::NONE,
move_idx: MoveIdx::NONE,
}, },
); );
} }
-468
View File
@@ -1,468 +0,0 @@
//! Random trees, checked against building the same tree cold.
//!
//! A frame reaches its layout by keeping most of the last one: slots
//! rewritten, some widgets drawn again, the rest untouched. The property here
//! is that what comes out is the tree a cold start would have produced, so
//! anything the retained path carried over that it should not have shows up
//! as a difference in somebody's box.
//!
//! `iris::random` grows the tree and `examples/random.rs` draws one. A seed is
//! the whole reproduction; `a_long_run_of_seeds_agrees` is the ignored sweep
//! for when it is worth spending the time.
use std::collections::HashMap;
use iris::harness::Harness;
use iris::prelude::*;
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow};
/// How deep the generator branches. The generator widens two to four ways per
/// level, so depth is exponential in width and a deep narrow tree is not
/// reachable by raising this -- it buys more overlap between dependency
/// paths, not more ancestry.
fn depth() -> usize {
env("IRIS_GENERATED_DEPTH", 4)
}
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(fallback)
}
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).abs() <= REGION_EPSILON_PX
}
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 {
let (root, tree) = grow(&mut h.rsc, seed, depth(), edits);
h.state.root = Some(root);
h.frame();
tree
}
fn resize_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
let lens = [
Some(Len::px(20.0 + rng.below(180) as f32)),
Some(Len::px(20.0 + rng.below(180) as f32)),
];
let sized = &mut h.rsc[tree.sized[idx]];
sized.x = lens[0];
sized.y = lens[1];
lens
}
/// Changes a few of the declared sizes, and says which, so the cold tree can
/// be grown with the same ones.
fn edit(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
let mut edits = HashMap::new();
for _ in 0..4 {
let idx = rng.below(tree.sized.len());
edits.insert(idx, resize_one(h, tree, idx, rng));
}
edits
}
/// Every declared size at once, so every reader of a size in the tree has a
/// changed descendant in the same frame and the whole dirty set has to settle
/// together.
fn edit_every(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
(0..tree.sized.len())
.map(|idx| (idx, resize_one(h, tree, idx, rng)))
.collect()
}
/// A way of changing what a span holds. Each is a shape worth its own case:
/// taking a child out of the middle is not the same as emptying a span, and
/// adding one is not the same as adding three.
#[derive(Clone, Copy, Debug)]
enum Shuffle {
/// Every other child, so what is left is interleaved with what went.
EveryOther,
/// Everything but the first, which is the last step before empty.
AllButFirst,
/// Three more on the end at once.
AddThree,
/// The first out and three more on, so the count moves both ways.
SwapForThree,
/// One out of the middle and one on the end.
TradeOne,
}
const SHUFFLES: [Shuffle; 5] = [
Shuffle::EveryOther,
Shuffle::AllButFirst,
Shuffle::AddThree,
Shuffle::SwapForThree,
Shuffle::TradeOne,
];
impl Shuffle {
fn of(self, grown: usize) -> SpanEdit {
let all = |step: usize, from: usize| (from..grown).step_by(step).collect();
match self {
Self::EveryOther => SpanEdit {
detach: all(2, 0),
attach: 0,
},
Self::AllButFirst => SpanEdit {
detach: all(1, 1),
attach: 0,
},
Self::AddThree => SpanEdit {
detach: Vec::new(),
attach: 3,
},
Self::SwapForThree => SpanEdit {
detach: vec![0],
attach: 3,
},
Self::TradeOne => SpanEdit {
detach: vec![grown / 2],
attach: 1,
},
}
}
}
/// Applies `shuffle` to every third span, and says what it did so the cold
/// tree can be grown that way. The widgets it takes out are given back: the
/// last share of one must outlive the comparison, or its id is handed to
/// something else and the two trees stop lining up.
fn reshuffle(
h: &mut Harness,
tree: &mut Tree,
shuffle: Shuffle,
) -> (HashMap<usize, SpanEdit>, Vec<StrongWidget>) {
let mut edits = HashMap::new();
let mut detached = Vec::new();
for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) {
let span_edit = shuffle.of(span.grown);
let mut take = span_edit.detach.clone();
take.sort_unstable();
let children = &mut h.rsc[span.id].children;
// Highest first, so an index means the same child however many of
// its neighbours are going too.
for j in take.into_iter().rev() {
if j < children.len() {
detached.push(children.remove(j));
}
}
let attach = span_edit.attach.min(span.spares.len());
children.extend(span.spares.drain(..attach));
edits.insert(idx, span_edit);
}
(edits, detached)
}
/// What a widget was configured with, so a tree the generator found can be
/// written out by hand. A fuzz failure is a lead; the fast test that replaces
/// it has to be buildable from what the failure printed.
fn describe(id: WidgetId, h: &Harness) -> String {
let label = h.rsc.widgets().label(id).to_string();
let Some(widget) = h.rsc.widgets().get_dyn(id) else {
return label;
};
let any: &dyn std::any::Any = widget;
let len = |l: &Option<Len>| match l {
Some(l) => format!("{l}"),
None => "-".into(),
};
if let Some(w) = any.downcast_ref::<SetSize>() {
return format!("SetSize{{x:{},y:{}}}", len(&w.x), len(&w.y));
}
if let Some(w) = any.downcast_ref::<Span>() {
let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" };
return format!(
"Span{{dir:{:?}{sign},gap:{},n:{}}}",
w.dir.axis,
w.gap,
w.children.len()
);
}
if let Some(w) = any.downcast_ref::<Pad>() {
let p = &w.padding;
return format!(
"Pad{{l:{},r:{},t:{},b:{}}}",
p.left, p.right, p.top, p.bottom
);
}
if let Some(w) = any.downcast_ref::<Aligned>() {
let a = |v: Option<AxisAlign>| match v {
None => "-",
Some(AxisAlign::Neg) => "neg",
Some(AxisAlign::Center) => "mid",
Some(AxisAlign::Pos) => "pos",
};
return format!("Aligned{{x:{},y:{}}}", a(w.align.x), a(w.align.y));
}
if let Some(w) = any.downcast_ref::<Stack>() {
return format!("Stack{{n:{}}}", w.children.len());
}
label
}
/// Every widget in one tree against the matching widget in the other. A
/// mismatch prints the widget's ancestry, marking the ones that own a slot,
/// since where two trees disagree is rarely where the cause is.
fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, &Tree)) {
let ((wh, wt), (ch, ct)) = (warm, cold);
assert_eq!(wt.ids.len(), ct.ids.len(), "seed {seed}: different trees");
let mut drawn = 0;
let mut wrong = 0;
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());
// 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;
}
wrong += 1;
if wrong <= 3 {
let mut chain = Vec::new();
let mut at = Some(w);
while let Some(id) = at {
let active = &wh.render.active[&id];
let slot = match active.move_idx == active.parent_move {
true => "",
false => "*",
};
chain.push(format!("{}{slot}", describe(id, wh)));
at = active.parent;
}
println!(
"seed {seed} after {what}: widget {i}\n warm {got:?}\n cold {want:?}\n {}",
chain.join(" < ")
);
}
}
assert!(drawn > 0, "seed {seed}: nothing was drawn");
assert_eq!(wrong, 0, "seed {seed}: {wrong} widgets differ after {what}");
}
fn changed_size(seed: u64) {
let mut warm = Harness::new((900, 1200));
let grown = plant(&mut warm, seed, &Edits::default());
// Not every tree grows a declared size to change.
if grown.sized.is_empty() {
return;
}
let mut rng = Rng::new(seed ^ 0x5eed);
let sizes = edit(&mut warm, &grown, &mut rng);
warm.frame();
let mut cold = Harness::new((900, 1200));
let same = plant(
&mut cold,
seed,
&Edits {
sizes,
..Default::default()
},
);
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same));
}
fn reshuffled(seed: u64, shuffle: Shuffle) {
let mut warm = Harness::new((900, 1200));
let mut grown = plant(&mut warm, seed, &Edits::default());
// Some seeds grow nothing but wrappers, and a shuffle with no span to
// shuffle is not the same thing as one that had no effect. A span behind
// a branch nobody took is the same kind of nothing: it is not drawn, so
// shuffling it cannot move anything.
let shuffles = grown
.spans
.iter()
.step_by(3)
.any(|span| warm.region(&span.id.id()).is_some());
if !shuffles {
return;
}
let before: Vec<_> = grown.ids.iter().map(|id| warm.region(id)).collect();
let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle);
warm.frame();
// Or the two trees would agree for want of anything having happened.
let after = grown.ids.iter().map(|id| warm.region(id));
let moved = before.iter().zip(after).filter(|(a, b)| *a != b).count();
assert!(moved > 0, "seed {seed}: {shuffle:?} changed nothing");
let mut cold = Harness::new((900, 1200));
let same = plant(
&mut cold,
seed,
&Edits {
spans,
..Default::default()
},
);
let what = format!("{shuffle:?}");
assert_same(seed, &what, (&warm, &grown), (&cold, &same));
}
fn changed_every_size(seed: u64) {
let mut warm = Harness::new((900, 1200));
let grown = plant(&mut warm, seed, &Edits::default());
if grown.sized.is_empty() {
return;
}
let mut rng = Rng::new(seed ^ 0xa11);
let sizes = edit_every(&mut warm, &grown, &mut rng);
warm.frame();
let mut cold = Harness::new((900, 1200));
let same = plant(
&mut cold,
seed,
&Edits {
sizes,
..Default::default()
},
);
assert_same(seed, "every size at once", (&warm, &grown), (&cold, &same));
}
/// Marks a spread of widgets for redraw at once. Nothing changes, so no box
/// may either; what this exercises is the order a frame settles a dirty set
/// in, which the other cases reach one dependency path at a time.
fn repainted_together(seed: u64) {
let mut warm = Harness::new((900, 1200));
let grown = plant(&mut warm, seed, &Edits::default());
for &id in grown.ids.iter().step_by(5) {
warm.rsc.widgets_mut().get_dyn_mut(id);
}
assert!(
!warm.rsc.widgets().needs_redraw.is_empty(),
"seed {seed}: nothing was marked"
);
warm.frame();
let mut cold = Harness::new((900, 1200));
let same = plant(&mut cold, seed, &Edits::default());
let what = "many repaints at once";
assert_same(seed, what, (&warm, &grown), (&cold, &same));
}
fn resized(seed: u64) {
let mut warm = Harness::new((1920, 1200));
let grown = plant(&mut warm, seed, &Edits::default());
warm.resize((640, 900));
warm.frame();
let mut cold = Harness::new((640, 900));
let same = plant(&mut cold, seed, &Edits::default());
assert_same(seed, "a resize", (&warm, &grown), (&cold, &same));
}
fn resized_then_changed(seed: u64) {
let mut warm = Harness::new((1920, 1200));
let grown = plant(&mut warm, seed, &Edits::default());
if grown.sized.is_empty() {
return;
}
warm.resize((640, 900));
warm.frame();
let mut rng = Rng::new(seed ^ 0xb0a7);
let sizes = edit(&mut warm, &grown, &mut rng);
warm.frame();
let mut cold = Harness::new((640, 900));
let same = plant(
&mut cold,
seed,
&Edits {
sizes,
..Default::default()
},
);
let what = "a resize then a size change";
assert_same(seed, what, (&warm, &grown), (&cold, &same));
}
#[test]
fn a_changed_size_lands_where_growing_it_that_way_would() {
SEEDS.into_iter().for_each(changed_size);
}
#[test]
fn every_size_changing_at_once_lands_where_growing_it_that_way_would() {
SEEDS.into_iter().for_each(changed_every_size);
}
#[test]
fn many_widgets_redrawing_at_once_leaves_every_box_where_it_was() {
SEEDS.into_iter().for_each(repainted_together);
}
#[test]
fn a_resize_lands_where_starting_at_that_size_would() {
SEEDS.into_iter().for_each(resized);
}
#[test]
fn a_size_change_after_a_resize_lands_the_same_way() {
SEEDS.into_iter().for_each(resized_then_changed);
}
#[test]
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
for shuffle in SHUFFLES {
for seed in SEEDS {
reshuffled(seed, shuffle);
}
}
}
/// The same property over a hundred seeds and every scenario. What it has
/// found so far was never where the trees disagreed: a text measured in a box
/// it was not going to get, and a widget re-measured in a box its own answer
/// had decided. `tests/shrink.rs` is how a seed from here becomes a tree
/// small enough to read.
#[test]
#[ignore = "a hundred seeds, rather than the seven the others check"]
fn a_long_run_of_seeds_agrees() {
let seeds = std::env::var("IRIS_GENERATED_SEED")
.ok()
.and_then(|seed| seed.parse().ok())
.map(|seed| seed..=seed)
.unwrap_or_else(|| 1..=env("IRIS_GENERATED_SEEDS", 100));
for seed in seeds {
changed_size(seed);
changed_every_size(seed);
repainted_together(seed);
resized(seed);
resized_then_changed(seed);
for shuffle in SHUFFLES {
reshuffled(seed, shuffle);
}
}
}
-29
View File
@@ -1,29 +0,0 @@
//! Whether measuring a widget and then giving it the length it reported is a
//! fixed point, which is what a span that sizes to its children needs.
use iris::harness::Harness;
use iris::prelude::*;
#[test]
fn a_wrapping_text_in_a_span_settles_on_one_width() {
let mut h = Harness::new((900, 600));
let words = "the quick brown fox jumps over the lazy dog and keeps on running \
until it reaches the end of a rather long line of text";
let t = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let filler = rect(Color::BLUE).add(&mut h.rsc);
h.set_root((t, filler).span(Dir::RIGHT));
let mut widths = Vec::new();
for _ in 0..6 {
let r = h.region(&t.id()).unwrap();
widths.push(r.bot_right.x - r.top_left.x);
// Redrawing it changes nothing about the state, so nothing may move.
h.rsc.widgets_mut().get_dyn_mut(t.id());
h.frame();
}
println!("widths over six frames: {widths:?}");
assert!(
widths.windows(2).all(|w| w[0] == w[1]),
"a repaint that changed nothing moved it: {widths:?}"
);
}
+2 -122
View File
@@ -54,7 +54,7 @@ fn a_child_drawn_twice_moves_once() {
h.set_root((left, centered).span(Dir::RIGHT)); h.set_root((left, centered).span(Dir::RIGHT));
assert_corners!(h, inner, (100, 0), (300, 200)); assert_corners!(h, inner, (100, 0), (300, 200));
h.rsc[left].x = Some(Len::px(150)); h.rsc[left].x = Some(Len::abs(150));
h.frame(); h.frame();
assert_corners!(h, inner, (150, 0), (350, 200)); assert_corners!(h, inner, (150, 0), (350, 200));
@@ -104,128 +104,8 @@ fn a_fixed_box_is_drawn_again_rather_than_stretched() {
h.set_root(stack.align(Align::TOP)); h.set_root(stack.align(Align::TOP));
assert_corners!(h, panel, (0, 0), (400, 100)); assert_corners!(h, panel, (0, 0), (400, 100));
h.rsc[leaf].y = Some(Len::px(250)); h.rsc[leaf].y = Some(Len::abs(250));
h.frame(); h.frame();
assert_corners!(h, panel, (0, 0), (400, 250)); assert_corners!(h, panel, (0, 0), (400, 250));
} }
#[test]
fn a_moved_subtree_takes_its_children_with_it() {
let mut h = Harness::new((400, 400));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.pad(10).height(40).add(&mut h.rsc);
h.set_root((first, row).span(Dir::DOWN));
assert_corners!(h, inner, (10, 50), (390, 70));
h.rsc[first].y = Some(Len::px(80));
h.frame();
// The row is the same shape somewhere else, so one slot moved it and
// `inner`'s own region was never rewritten.
assert_corners!(h, inner, (10, 90), (390, 110));
}
#[test]
fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
let mut h = Harness::new((400, 200));
let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc);
let rest = rect(Color::GREEN).add(&mut h.rsc);
let panel = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
// Changing the bar's width is the only thing that changes the box the
// panel and everything under it was drawn for.
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, panel).span(Dir::RIGHT));
assert_corners!(h, fixed, (100, 0), (150, 200));
assert_corners!(h, rest, (150, 0), (400, 200));
h.rsc[bar].x = Some(Len::px(200));
h.frame();
// The panel's box is 100 shorter, so the fixed child is the same 50 wide
// against its new start and the one taking the rest absorbs the change.
assert_corners!(h, fixed, (200, 0), (250, 200));
assert_corners!(h, rest, (250, 0), (400, 200));
}
#[test]
fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
let mut h = Harness::new((400, 200));
// The row is 40 tall whatever happens, which used to make its drawing
// impossible to take out of: recovering a fraction of a box needs a
// relative extent, and it has none on that axis.
let inner = rect(Color::BLUE).add(&mut h.rsc);
let row = inner.pad(10).height(40).add(&mut h.rsc);
let filler = rect(Color::GREEN).add(&mut h.rsc);
let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, column).span(Dir::RIGHT));
assert_corners!(h, inner, (110, 10), (390, 30));
h.rsc[bar].x = Some(Len::px(200));
h.frame();
assert_corners!(h, inner, (210, 10), (390, 30));
}
#[test]
fn only_a_container_that_places_its_children_lengthens_the_chain() {
let mut h = Harness::new((400, 200));
let leaf = rect(Color::BLUE).add(&mut h.rsc);
// Four widgets between the span and the leaf, none of which places what
// it draws, so all of them share the span's slot.
let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, buried).span(Dir::RIGHT));
let slot = h.render.active[&leaf.id()].parent_move;
assert_eq!(
h.render.moves.depth(slot),
2,
"the span above the leaf, and the root the window is held in"
);
}
/// A span that sizes from its children passes their `rest` weight up rather
/// than collapsing it to one share, so nesting divides the same space instead
/// of re-dividing a share of it.
#[test]
fn nested_spans_divide_the_space_once_however_deep_the_nesting_is() {
let mut h = Harness::new((400, 200));
let (a, b, c, d) = (
rect(Color::RED).add(&mut h.rsc),
rect(Color::BLUE).add(&mut h.rsc),
rect(Color::GREEN).add(&mut h.rsc),
rect(Color::WHITE).add(&mut h.rsc),
);
let left = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
let right = (c, d).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
for (i, id) in [a, b, c, d].into_iter().enumerate() {
let x = i as f32 * 100.0;
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
}
}
/// The same space, unevenly nested: weights carried up mean a share is a
/// share of the whole, not of whatever branch a widget happens to sit in.
#[test]
fn an_uneven_nesting_still_gives_every_share_the_same_length() {
let mut h = Harness::new((400, 200));
let (a, b, c, d) = (
rect(Color::RED).add(&mut h.rsc),
rect(Color::BLUE).add(&mut h.rsc),
rect(Color::GREEN).add(&mut h.rsc),
rect(Color::WHITE).add(&mut h.rsc),
);
let one = (a,).span(Dir::RIGHT).add(&mut h.rsc);
let three = (b, c, d).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((one, three).span(Dir::RIGHT));
for (i, id) in [a, b, c, d].into_iter().enumerate() {
let x = i as f32 * 100.0;
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
}
}
-240
View File
@@ -1,240 +0,0 @@
//! Retained CPU-layout diagnostics on one reproducible random tree.
//!
//! Counters and phase timers:
//!
//! cargo test --release --features layout-diagnostics \
//! --test layout_diagnostics -- --ignored --nocapture
//!
//! Uninstrumented hardware totals for one phase:
//!
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \
//! -e cycles:u,instructions:u cargo test --release \
//! --test layout_diagnostics -- --ignored --nocapture
//!
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
//! `IRIS_DIRTY` how many widgets `many` marks at once.
use iris::harness::Harness;
use iris::prelude::*;
use iris::random::{Edits, Tree, grow};
use std::time::Instant;
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 {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.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());
harness.state.root = Some(root);
harness.frame();
println!(
"fixture: seed {seed}, depth {depth}, {} widgets, {} active",
tree.ids.len(),
harness.render.active_widgets()
);
#[cfg(feature = "layout-diagnostics")]
let _ = iris::core::layout_diagnostics::take();
(harness, tree)
}
fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
let frames = elapsed.len();
// The worst frame is the stutter somebody sees, so it goes beside the
// median; p99 says whether it is the load or a single interruption.
println!(
"{label}: {frames} frame(s), min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
max {:.3} ms, total {:.1} ms",
elapsed[0],
elapsed[frames / 2],
elapsed[frames * 99 / 100],
elapsed[frames - 1],
elapsed.iter().sum::<f64>(),
);
#[cfg(feature = "layout-diagnostics")]
{
let diagnostics = iris::core::layout_diagnostics::take();
print!("{}", diagnostics.per_frame(frames));
for callsite in diagnostics.hot_text().iter().take(3) {
let mut ancestry = Vec::new();
let mut id = Some(callsite.id);
while let Some(widget) = id {
ancestry.push(_harness.rsc.widgets().label(widget).as_str());
id = _harness
.render
.active
.get(&widget)
.and_then(|active| active.parent);
}
println!(" text ancestry: {}", ancestry.join(" < "));
}
}
}
fn run(
label: &str,
frames: usize,
harness: &mut Harness,
mut change: impl FnMut(&mut Harness, usize),
) {
let mut elapsed = Vec::with_capacity(frames);
for frame in 0..frames {
change(harness, frame);
let start = Instant::now();
harness.frame();
elapsed.push(start.elapsed().as_secs_f64() * 1_000.0);
}
report(label, elapsed, harness);
}
#[test]
#[ignore = "measurement, not a check"]
fn layout_cost() {
let seed = env("IRIS_SEED", 1_u64);
let depth = env("IRIS_DEPTH", 7_usize);
let frames = env("IRIS_FRAMES", 100_usize);
assert!(frames > 0, "IRIS_FRAMES must be greater than zero");
let phase = env("IRIS_PHASE", String::from("all"));
assert!(
["all", "cold", "repaint", "many", "size", "scroll", "resize"].contains(&phase.as_str()),
"unknown IRIS_PHASE {phase:?}"
);
let selected = |name| phase == "all" || phase == name;
if selected("cold") {
let mut harness = Harness::new(OUTPUT);
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
harness.state.root = Some(root);
println!(
"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, |_, _| {});
drop(tree);
}
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);
});
}
if selected("many") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
// Spread through the tree rather than taken from one subtree, so the
// dependency paths the frame settles overlap.
let wanted = env("IRIS_DIRTY", 32_usize).max(1);
let step = (tree.ids.len() / wanted).max(1);
let dirty: Vec<_> = tree.ids.iter().copied().step_by(step).collect();
println!("marking {} of {} widgets", dirty.len(), tree.ids.len());
run("many", frames, &mut harness, move |harness, _| {
for &id in &dirty {
harness.rsc.widgets_mut().get_dyn_mut(id);
}
});
}
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::px(100.0 + (frame % 2) as f32 * 40.0));
});
}
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 });
});
}
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 + 1) % 2) as f32 * 8.0, OUTPUT.1));
});
drop(tree);
}
}
+27
View File
@@ -0,0 +1,27 @@
//! What a drawing can be taken out of, and what it cannot.
use iris::core::{Remap, UiRegion, UiScalar, UiSpan};
/// A box `size` tall whose top is `rel` of the way down the window.
fn fixed(rel: f32, size: f32) -> UiRegion {
UiRegion::new(
UiSpan::FULL,
UiSpan::new(UiScalar { rel, abs: 0.0 }, UiScalar { rel, abs: size }),
)
}
#[test]
fn a_fixed_box_can_be_carried_but_not_stretched() {
let from = fixed(0.0, 164.0);
assert!(Remap::new(from, UiRegion::FULL).is_none());
assert!(Remap::new(from, fixed(0.5, 164.0)).is_some());
assert!(Remap::new(from, fixed(0.0, 98.0)).is_none());
}
#[test]
fn a_relative_box_can_be_stretched_to_any_other() {
let remap = Remap::new(UiRegion::FULL, fixed(0.0, 98.0)).expect("relative boxes remap");
// A part that filled the window keeps filling what replaced it, which is
// exactly what `outside` could not say for a box of a fixed length.
assert_eq!(remap.apply(UiRegion::FULL), fixed(0.0, 98.0));
}
-43
View File
@@ -1,43 +0,0 @@
//! What re-placing a subtree costs per frame, as a load for a counter rather
//! than a check. A span of 200 fixed-height rows, five primitives each, with
//! the row above them changing height every frame, so every row below is
//! offered a box the same shape somewhere else.
//!
//! cargo test --release --test replace_cost -- --ignored
//! perf stat -e instructions:u target/release/.../replace_cost-* --ignored
//!
//! Wall time is the wrong number here; see `draw_cost.rs`. Measured on
//! 2026-09-14 at 1.98M instructions per frame, against 2.38M for rewriting
//! each row's regions instead and 7.13M for redrawing them.
use iris::harness::Harness;
use iris::prelude::*;
const ROWS: usize = 200;
const FRAMES: usize = 200;
#[test]
#[ignore = "measurement, not a check"]
fn replacing_rows_every_frame() {
let mut h = Harness::new((1920, 1200));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let mut span = Span::empty(Dir::DOWN);
span.push(first.add_strong(&mut h.rsc));
for i in 0..ROWS {
let row = (
rect(Color::BLUE.darker(i as f32 / (ROWS * 2) as f32)),
rect(Color::GREEN).pad(2),
wtext("row").size(16).pad(2),
)
.span(Dir::RIGHT)
.pad(4)
.height(40)
.add(&mut h.rsc);
span.push(row.add_strong(&mut h.rsc));
}
h.set_root(span);
for i in 0..FRAMES {
h.rsc[first].y = Some(Len::px(40.0 + (i % 2) as f32));
h.frame();
}
}
+11 -230
View File
@@ -79,9 +79,10 @@ fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
h.rsc[first].size = Size::from((150, 200)); h.rsc[first].size = Size::from((150, 200));
h.frame(); h.frame();
// The preceding fixed child makes the remaining box this child's real // Twice: once for the span to measure it, once for its real box. A child
// box, so measuring it also draws it in its final place. // that can hint its length is spared the first, and a smaller number here
assert_eq!(draws.get(), settled + 1); // means someone has made that cheaper rather than broken it.
assert_eq!(draws.get(), settled + 2);
assert_corners!(h, second, (150, 0), (400, 200)); assert_corners!(h, second, (150, 0), (400, 200));
} }
@@ -115,22 +116,6 @@ fn a_span_relays_out_when_a_child_it_measured_changes() {
assert_corners!(h, second, (250, 0), (400, 200)); assert_corners!(h, second, (250, 0), (400, 200));
} }
#[test]
fn a_repaint_that_keeps_its_size_does_not_relay_out() {
let mut h = Harness::new((400, 200));
let (first, draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
let (second, _) = counted(&mut h, Size::REST, OnResize::Translate);
h.set_root((first, second).span(Dir::RIGHT));
let settled = draws.get();
// Taking mutable access is the ordinary content-change signal. This
// widget returns the same size, so the parent has nothing to lay out.
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
h.frame();
assert_eq!(draws.get(), settled + 1);
}
#[test] #[test]
fn a_placed_child_survives_the_next_frame() { fn a_placed_child_survives_the_next_frame() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
@@ -156,7 +141,7 @@ impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap(); let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let mut region = UiRegion::FULL; let mut region = UiRegion::FULL;
region.y.end = region.y.start.offset(len.px); region.y.end = region.y.start.offset(len.abs);
painter.widget_within(&self.inner, region); painter.widget_within(&self.inner, region);
Size::REST Size::REST
} }
@@ -173,7 +158,7 @@ fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
h.set_root(parent); h.set_root(parent);
assert_corners!(h, inner, (0, 0), (400, 80)); assert_corners!(h, inner, (0, 0), (400, 80));
h.rsc[inner].y = Some(Len::px(120)); h.rsc[inner].y = Some(Len::abs(120));
h.frame(); h.frame();
assert_corners!(h, inner, (0, 0), (400, 120)); assert_corners!(h, inner, (0, 0), (400, 120));
@@ -187,34 +172,14 @@ struct ReadsOutput {
impl Widget for ReadsOutput { impl Widget for ReadsOutput {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1); self.draws.set(self.draws.get() + 1);
Size::px(painter.output_size() / 4.0) Size::abs(painter.output_size() / 4.0)
}
}
/// Reads the output across one axis only, and says so: its drawing follows
/// a taller box on its own, so only a wider one is worth a draw.
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::px((painter.output_len(Axis::X) / 4.0, 20.0).into())
}
fn on_resize(&self, axis: Axis) -> OnResize {
match axis {
Axis::X => OnResize::Redraw,
Axis::Y => OnResize::Scale,
}
} }
} }
#[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));
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Scale); let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
h.set_root(leaf); h.set_root(leaf);
let settled = draws.get(); let settled = draws.get();
@@ -225,28 +190,11 @@ fn a_resize_does_not_redraw_what_the_shader_can_move() {
assert_eq!( assert_eq!(
draws.get(), draws.get(),
settled, settled,
"a scaling drawing follows its box, and the output is one" "its box is the same fraction of a different output"
); );
assert_corners!(h, leaf, (0, 0), (800, 100)); assert_corners!(h, leaf, (0, 0), (800, 100));
} }
/// The output is the root of the box chain, so a resize is a box that changed
/// length and `OnResize` answers for it -- there is not a second rule for the
/// window. A drawing that does not scale is redrawn whichever box moved.
#[test]
fn a_resize_redraws_what_does_not_scale() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
h.frame();
assert_eq!(draws.get(), settled + 1, "its box is a different length");
assert_corners!(h, leaf, (0, 0), (800, 100));
}
#[test] #[test]
fn a_resize_redraws_what_read_the_output() { fn a_resize_redraws_what_read_the_output() {
let mut h = Harness::new((400, 200)); let mut h = Harness::new((400, 200));
@@ -264,83 +212,6 @@ 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::px(width);
h.frame();
assert_eq!(draws.get(), settled);
}
h.rsc[first].size.x = Len::px(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));
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));
@@ -368,104 +239,14 @@ fn a_change_two_levels_under_its_reader_still_reaches_it() {
// Every wrapper up to the outer pad read the size below it, so the outer // Every wrapper up to the outer pad read the size below it, so the outer
// pad is what draws again -- and the span it hands the box to is the same // pad is what draws again -- and the span it hands the box to is the same
// size as before, which is what lets a draw reuse its way past the leaf. // size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::px((100, 100).into()), OnResize::Redraw); let (leaf, _) = counted(&mut h, Size::abs((100, 100).into()), OnResize::Redraw);
let padded = leaf.pad(10).add(&mut h.rsc); let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc); let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12)); h.set_root((padded, below).span(Dir::DOWN).pad(12));
assert_corners!(h, below, (12, 132), (388, 388)); assert_corners!(h, below, (12, 132), (388, 388));
h.rsc[leaf].size = Size::px((100, 200).into()); h.rsc[leaf].size = Size::abs((100, 200).into());
h.frame(); h.frame();
assert_corners!(h, below, (12, 232), (388, 388)); assert_corners!(h, below, (12, 232), (388, 388));
} }
/// Claims its drawing survives its box changing length, and has a child so
/// that the walk looking for what does not has one to reach.
struct Stretchy {
inner: StrongWidget,
draws: Rc<Cell<usize>>,
}
impl Widget for Stretchy {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
painter.widget(&self.inner).size()
}
fn on_resize(&self, _: Axis) -> OnResize {
OnResize::Scale
}
}
#[test]
fn stretching_a_subtree_carries_the_children_in_it() {
let mut h = Harness::new((400, 400));
let first = rect(Color::RED).height(40).add(&mut h.rsc);
let inner = rect(Color::BLUE).add(&mut h.rsc);
let draws = Rc::new(Cell::new(0));
let outer = Stretchy {
inner: inner.add_strong(&mut h.rsc),
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root((first, outer).span(Dir::DOWN));
let settled = draws.get();
assert_corners!(h, inner, (0, 40), (400, 400));
h.rsc[first].y = Some(Len::px(80));
h.frame();
assert_eq!(
draws.get(),
settled,
"its drawing follows its box, rather than being made again"
);
assert_corners!(h, outer, (0, 80), (400, 400));
assert_corners!(h, inner, (0, 80), (400, 400));
}
#[test]
fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() {
let mut h = Harness::new((400, 200));
// What a transcript row is: something whose shaping depends on the width
// it is given, beside something that only has to be the right shape.
let (wraps, wrap_draws) = counted(&mut h, Size::REST, OnResize::Redraw);
let (backing, back_draws) = counted(&mut h, Size::REST, OnResize::Scale);
let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, row).span(Dir::RIGHT));
let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get());
h.rsc[bar].x = Some(Len::px(200));
h.frame();
// The span reads every child's size, so redrawing one takes the span
// with it -- and the span then measures and places the redrawn child.
assert!(wrap_draws.get() > settled_wrap, "reads the width it got");
assert_eq!(back_draws.get(), settled_back, "only has to be the shape");
assert_corners!(h, backing, (200, 0), (300, 200));
assert_corners!(h, wraps, (300, 0), (400, 200));
}
#[test]
fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() {
let mut h = Harness::new((400, 200));
// Its box is a fixed 80 wherever the row's edges end up, so drawing it
// again would be for a width it does not have. The declared width is what
// lets the span say that without drawing it: a width the span learnt by
// drawing the child in its own box is only an answer for that box.
let (counter, draws) = counted(&mut h, Size::from((80, 200)), OnResize::Redraw);
let fixed = counter.width(80).add(&mut h.rsc);
let (rest, _) = counted(&mut h, Size::REST, OnResize::Scale);
let row = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((bar, row).span(Dir::RIGHT));
let settled = draws.get();
h.rsc[bar].x = Some(Len::px(200));
h.frame();
assert_eq!(draws.get(), settled, "its own length did not change");
assert_corners!(h, fixed, (200, 0), (280, 200));
}
-199
View File
@@ -1,199 +0,0 @@
//! What a resize frame costs and what it holds, on a tree the revision before
//! #16 also builds.
//!
//! Deliberately written in the API subset `43ce8c7` and this branch share, so
//! the same source can be dropped into an old worktree and measured there:
//! that is the only like-for-like comparison with the code the retained
//! layout replaced. The random tree cannot carry one, because the generator
//! itself changed with the work.
//!
//! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \
//! -- --ignored --nocapture resize_cost
//! ROWS=2000 cargo test --release --test revision_cost \
//! -- --ignored --nocapture text_memory
//!
//! Wall time on this machine varies with CPU frequency; take the number from
//! `perf stat -e instructions:u` on the test binary directly.
use iris::harness::Harness;
use iris::prelude::*;
use std::time::Instant;
/// xorshift64, so one seed is one set of paragraphs on any machine.
struct Rng(u64);
impl Rng {
fn bits(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.bits() % n as u64) as usize
}
}
const WORDS: [&str; 24] = [
"wrapping",
"shapes",
"one",
"source",
"into",
"as",
"many",
"lines",
"as",
"the",
"box",
"leaves",
"room",
"for",
"paragraph",
"height",
"answer",
"setting",
"container",
"width",
"before",
"knows",
"measured",
"again",
];
/// A run of its own words, so nothing here is fast for two texts being the
/// same string.
fn words(rng: &mut Rng, least: usize, most: usize) -> String {
let words = least + rng.below(most - least);
let mut out = String::new();
for _ in 0..words {
if !out.is_empty() {
out.push(' ');
}
out.push_str(WORDS[rng.below(WORDS.len())]);
}
out
}
const OUTPUT: (f32, f32) = (900.0, 1200.0);
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(fallback)
}
/// A row of a fixed-width rect beside a column of one wrapping and one
/// overflowing text: the shape that makes a container measure a child in a
/// box it will not keep.
fn build(h: &mut Harness, rows: usize) -> Vec<WidgetId> {
let mut rng = Rng(1);
let mut paragraphs = Vec::new();
let mut col = Span::empty(Dir::DOWN);
for _ in 0..rows {
let mut row = Span::empty(Dir::RIGHT);
row.push(rect(Color::RED).width(Len::px(40.0)).add_strong(&mut h.rsc));
let mut body = Span::empty(Dir::DOWN);
let para = wtext(words(&mut rng, 12, 52))
.size(16)
.wrap(true)
.add_strong(&mut h.rsc);
paragraphs.push(para.id());
body.push(para);
body.push(
// Short, or its unwrapped width decides the row and the
// paragraph beside it never wraps.
wtext(words(&mut rng, 2, 6))
.size(16)
.wrap(false)
.add_strong(&mut h.rsc),
);
row.push(body.add_strong(&mut h.rsc));
col.push(row.add_strong(&mut h.rsc));
}
let root = col.add(&mut h.rsc);
h.set_root(root);
paragraphs
}
#[test]
#[ignore = "measurement, not a check"]
fn resize_cost() {
let rows = env("ROWS", 40_usize);
let frames = env("FRAMES", 500_usize);
let mut h = Harness::new(OUTPUT);
let paragraphs = build(&mut h, rows);
// What it cost is only half the comparison: the old code is cheaper
// partly because it wraps at the container's whole width rather than the
// part left beside the rect, and draws past the edge of the output.
println!("output width {}", OUTPUT.0);
for (at, id) in paragraphs.iter().enumerate().take(3) {
println!("paragraph {at}: {:?}", h.region(id));
}
// Two widths in turn is the friendly case for anything that remembers an
// answer, so `SWEEP=1` never repeats one -- a drag rather than a toggle.
let sweep = env("SWEEP", 0_usize) != 0;
let mut elapsed = Vec::with_capacity(frames);
for frame in 0..frames {
let narrower = match sweep {
true => (frame % 256) as f32,
false => ((frame + 1) % 2) as f32 * 8.0,
};
h.resize((OUTPUT.0 - narrower, OUTPUT.1));
let start = Instant::now();
h.frame();
elapsed.push(start.elapsed().as_secs_f64() * 1000.0);
}
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
println!(
"resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
max {:.3} ms, total {:.1} ms",
elapsed[0],
elapsed[frames / 2],
elapsed[frames * 99 / 100],
elapsed[frames - 1],
elapsed.iter().sum::<f64>()
);
}
fn kb(field: &str) -> u64 {
std::fs::read_to_string("/proc/self/status")
.unwrap()
.lines()
.find(|line| line.starts_with(field))
.and_then(|line| line.split_whitespace().nth(1)?.parse().ok())
.unwrap()
}
fn report(label: &str) {
println!(
"{label:24} rss {:>7} kB peak {:>7} kB",
kb("VmRSS:"),
kb("VmHWM:")
);
}
/// Run this one on its own: the figures are the whole process's.
#[test]
#[ignore = "measurement, not a check"]
fn text_memory() {
let rows = env("ROWS", 2000_usize);
report("before");
let mut h = Harness::new(OUTPUT);
let paragraphs = build(&mut h, rows);
report("after cold frame");
for frame in 0..40 {
h.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
h.frame();
}
report("after 40 resizes");
// Settled: the output holds still and one leaf repaints per frame.
for _ in 0..10 {
let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]);
h.frame();
}
report("after settling");
}
-525
View File
@@ -1,525 +0,0 @@
//! A property test that shrinks its own counterexample.
//!
//! `generated.rs` reproduces a failure from a seed, but a seed is not a lead
//! anybody can read: the tree is hundreds of widgets, and reconstructing the
//! part that matters by hand has failed every time it has been tried. This
//! grows trees it can take apart, so a failure is reduced to the smallest
//! tree that still shows it and printed as something to write a fast test
//! from.
//!
//! cargo test --release --test shrink -- --ignored --nocapture
//!
//! `SHRINK_SEEDS` how many trees to try, `SHRINK_DEPTH` how deep to grow
//! them, `SHRINK_CASE` which scenario. It is a fuzzer: run it once the
//! ordinary tests pass, and turn what it finds into a test of its own rather
//! than leaving a seed as the record.
use iris::harness::Harness;
use iris::prelude::*;
use iris::random::{Branch, Rng};
/// The same two leaves `iris::random` grows, since only one of them reads the
/// width it is given and that is the difference that matters.
const WORDS: &[&str] = &[
"Wrapping",
"shapes",
"one",
"source",
"into",
"as",
"many",
"lines",
"as",
"the",
"box",
"leaves",
"room",
"for,",
"so",
"a",
"paragraph's",
"height",
"is",
"an",
"answer",
"and",
"not",
"a",
"setting.",
];
const ONE_LINE: &str = "one line, overflowing whatever it is given";
const OUTER: (f32, f32) = (1920.0, 1200.0);
const INNER: (f32, f32) = (640.0, 900.0);
#[derive(Clone, Debug, PartialEq)]
enum Node {
/// Words taken from [`WORDS`], and whether it wraps.
Text(usize, bool),
/// The leaf that overflows whatever box it is given rather than wrapping.
OneLine,
Rect,
/// Direction, gap, children in creation order, and the order they are
/// attached in -- separate so a tree that reorders its children
/// still makes the same widgets in the same order, and two
/// builds line up index for index.
Span(bool, f32, Vec<Node>, Vec<usize>),
Stack(Vec<Node>),
Pad(f32, Box<Node>),
Aligned(u8, u8, Box<Node>),
Sized(Option<Len>, Option<Len>, Box<Node>),
Scroll(bool, Box<Node>),
Branch(Box<Node>, Box<Node>, Box<Node>, f32),
}
fn axis_align(v: u8) -> Option<AxisAlign> {
match v % 4 {
0 => None,
1 => Some(AxisAlign::Neg),
2 => Some(AxisAlign::Center),
_ => Some(AxisAlign::Pos),
}
}
fn dir(down: bool) -> Dir {
if down { Dir::DOWN } else { Dir::RIGHT }
}
impl Node {
/// Builds into `h`, pushing every id in tree order, so two builds of one
/// node line up index for index and their boxes can be compared.
fn build(
&self,
h: &mut Harness,
out: &mut Vec<WidgetId>,
spans: &mut Vec<WeakWidget<Span>>,
) -> StrongWidget {
let id: StrongWidget = match self {
Node::Text(words, wrap) => {
let n = (*words).clamp(1, WORDS.len());
wtext(WORDS[..n].join(" "))
.size(16)
.wrap(*wrap)
.add_strong(&mut h.rsc)
}
Node::OneLine => wtext(ONE_LINE).size(16).wrap(false).add_strong(&mut h.rsc),
Node::Rect => rect(Color::RED).add_strong(&mut h.rsc),
Node::Span(down, gap, kids, order) => {
let mut built: Vec<_> = kids.iter().map(|k| Some(k.build(h, out, spans))).collect();
// `order` is a permutation, so each is taken exactly once.
let children = order
.iter()
.map(|&i| built[i].take().expect("order repeats an index"))
.collect();
let handle = Span {
children,
dir: dir(*down),
gap: *gap,
}
.add(&mut h.rsc);
spans.push(handle);
handle.add_strong(&mut h.rsc)
}
Node::Stack(kids) => {
let children = kids.iter().map(|k| k.build(h, out, spans)).collect();
Stack {
children,
size: StackSize::Child(0),
}
.add_strong(&mut h.rsc)
}
Node::Pad(p, kid) => {
let inner = kid.build(h, out, spans);
Pad {
padding: Padding {
left: *p,
right: *p,
top: *p,
bottom: *p,
},
inner,
}
.add_strong(&mut h.rsc)
}
Node::Aligned(x, y, kid) => {
let inner = kid.build(h, out, spans);
Aligned {
inner,
align: Align {
x: axis_align(*x),
y: axis_align(*y),
},
}
.add_strong(&mut h.rsc)
}
Node::Sized(x, y, kid) => {
let inner = kid.build(h, out, spans);
SetSize {
inner,
x: *x,
y: *y,
}
.add_strong(&mut h.rsc)
}
Node::Scroll(down, kid) => {
let inner = kid.build(h, out, spans);
let axis = if *down { Axis::Y } else { Axis::X };
Scroll::new(inner, axis).add_strong(&mut h.rsc)
}
Node::Branch(probe, a, b, at) => {
let probe = probe.build(h, out, spans);
let wide = a.build(h, out, spans);
let narrow = b.build(h, out, spans);
Branch {
probe,
wide,
narrow,
threshold: *at,
}
.add_strong(&mut h.rsc)
}
};
out.push(id.id());
id
}
fn size(&self) -> usize {
1 + match self {
Node::Text(..) | Node::OneLine | Node::Rect => 0,
Node::Span(_, _, kids, _) | Node::Stack(kids) => kids.iter().map(Node::size).sum(),
Node::Pad(_, k)
| Node::Aligned(_, _, k)
| Node::Sized(_, _, k)
| Node::Scroll(_, k) => k.size(),
Node::Branch(p, a, b, _) => p.size() + a.size() + b.size(),
}
}
/// Every one-step simplification: a wrapper replaced by what it wrapped, a
/// child dropped, a length or a word count reduced. Ordered cheapest-first
/// so the greedy walk takes the biggest bites early.
fn smaller(&self) -> Vec<Node> {
let mut out = Vec::new();
let leaf = Node::Rect;
match self {
Node::Text(words, wrap) => {
if *words > 1 {
out.push(Node::Text(words / 2, *wrap));
out.push(Node::Text(words - 1, *wrap));
}
if *wrap {
out.push(Node::Text(*words, false));
}
out.push(leaf);
}
Node::OneLine => out.push(Node::Rect),
Node::Rect => {}
Node::Span(down, gap, kids, order) => {
out.extend(order.iter().map(|&i| kids[i].clone()));
for i in 0..kids.len() {
if kids.len() > 1 {
let mut less = kids.clone();
less.remove(i);
let order = (0..less.len()).collect();
out.push(Node::Span(*down, *gap, less, order));
}
}
if *gap != 0.0 {
out.push(Node::Span(*down, 0.0, kids.clone(), order.clone()));
}
for (i, kid) in kids.iter().enumerate() {
for small in kid.smaller() {
let mut next = kids.clone();
next[i] = small;
out.push(Node::Span(*down, *gap, next, order.clone()));
}
}
}
Node::Stack(kids) => {
out.extend(kids.iter().cloned());
for i in 0..kids.len() {
if kids.len() > 1 {
let mut less = kids.clone();
less.remove(i);
out.push(Node::Stack(less));
}
}
for (i, kid) in kids.iter().enumerate() {
for small in kid.smaller() {
let mut next = kids.clone();
next[i] = small;
out.push(Node::Stack(next));
}
}
}
Node::Pad(p, kid) => {
out.push((**kid).clone());
if *p != 0.0 {
out.push(Node::Pad(0.0, kid.clone()));
}
out.extend(
kid.smaller()
.into_iter()
.map(|k| Node::Pad(*p, Box::new(k))),
);
}
Node::Aligned(x, y, kid) => {
out.push((**kid).clone());
for (nx, ny) in [(0, *y), (*x, 0)] {
if (nx, ny) != (*x, *y) {
out.push(Node::Aligned(nx, ny, kid.clone()));
}
}
out.extend(
kid.smaller()
.into_iter()
.map(|k| Node::Aligned(*x, *y, Box::new(k))),
);
}
Node::Sized(x, y, kid) => {
out.push((**kid).clone());
if x.is_some() {
out.push(Node::Sized(None, *y, kid.clone()));
}
if y.is_some() {
out.push(Node::Sized(*x, None, kid.clone()));
}
out.extend(
kid.smaller()
.into_iter()
.map(|k| Node::Sized(*x, *y, Box::new(k))),
);
}
Node::Scroll(down, kid) => {
out.push((**kid).clone());
out.extend(
kid.smaller()
.into_iter()
.map(|k| Node::Scroll(*down, Box::new(k))),
);
}
Node::Branch(p, a, b, at) => {
out.push((**p).clone());
out.push((**a).clone());
out.push((**b).clone());
for small in p.smaller() {
out.push(Node::Branch(Box::new(small), a.clone(), b.clone(), *at));
}
for small in a.smaller() {
out.push(Node::Branch(p.clone(), Box::new(small), b.clone(), *at));
}
for small in b.smaller() {
out.push(Node::Branch(p.clone(), a.clone(), Box::new(small), *at));
}
}
}
out
}
}
/// A declared size over about half the tree, the way `iris::random` puts them
/// in: on the way into every child rather than as a node kind of its own, so
/// readers of a size are dense rather than occasional.
fn sized(rng: &mut Rng, inner: Node) -> Node {
if !rng.chance() {
return inner;
}
let len = |rng: &mut Rng| match rng.below(4) {
0 => Some(Len::px(20.0 + rng.below(180) as f32)),
1 => Some(Len::REST),
_ => None,
};
Node::Sized(len(rng), len(rng), Box::new(inner))
}
fn grow(rng: &mut Rng, depth: usize) -> Node {
if depth == 0 {
return match rng.below(4) {
0 => Node::Text(1 + rng.below(WORDS.len()), true),
1 => Node::OneLine,
_ => Node::Rect,
};
}
let len = |rng: &mut Rng| match rng.below(4) {
0 => Some(Len::px(20.0 + rng.below(180) as f32)),
1 => Some(Len::REST),
2 => Some(Len::rel(0.25 + rng.below(3) as f32 * 0.25)),
_ => None,
};
let kid = |rng: &mut Rng| {
let inner = grow(rng, depth - 1);
sized(rng, inner)
};
match rng.below(8) {
0 => Node::Scroll(rng.chance(), Box::new(kid(rng))),
1 => Node::Aligned(rng.below(4) as u8, rng.below(4) as u8, Box::new(kid(rng))),
2 => Node::Pad(rng.below(24) as f32, Box::new(kid(rng))),
3 => Node::Sized(len(rng), len(rng), Box::new(kid(rng))),
4 => Node::Branch(
Box::new(kid(rng)),
Box::new(kid(rng)),
Box::new(kid(rng)),
rng.below(500) as f32,
),
5 => Node::Stack((0..2 + rng.below(2)).map(|_| kid(rng)).collect()),
_ => {
let kids: Vec<_> = (0..2 + rng.below(3)).map(|_| kid(rng)).collect();
let order = (0..kids.len()).collect();
Node::Span(rng.chance(), rng.below(3) as f32 * 4.0, kids, order)
}
}
}
#[derive(Clone, Copy, PartialEq)]
enum Case {
Resize,
Repaint,
ResizeRepaint,
Reorder,
}
/// Every span's children rotated by one, as a tree rather than as a change:
/// what a warm frame reaches by moving them has to be where growing them that
/// way lands.
fn reordered(node: &Node) -> Node {
match node {
Node::Span(down, gap, kids, order) => {
let kids = kids.iter().map(reordered).collect::<Vec<_>>();
let mut order = order.clone();
order.rotate_left(1);
Node::Span(*down, *gap, kids, order)
}
Node::Stack(kids) => Node::Stack(kids.iter().map(reordered).collect()),
Node::Pad(p, k) => Node::Pad(*p, Box::new(reordered(k))),
Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(reordered(k))),
Node::Sized(x, y, k) => Node::Sized(*x, *y, Box::new(reordered(k))),
Node::Scroll(d, k) => Node::Scroll(*d, Box::new(reordered(k))),
Node::Branch(p, a, b, at) => Node::Branch(
Box::new(reordered(p)),
Box::new(reordered(a)),
Box::new(reordered(b)),
*at,
),
leaf => leaf.clone(),
}
}
/// Runs one scenario warm and cold and says where they disagree.
fn diverges(node: &Node, case: Case) -> Option<String> {
let resizes = matches!(case, Case::Resize | Case::ResizeRepaint);
let repaints = matches!(case, Case::Repaint | Case::ResizeRepaint);
let start = if resizes { OUTER } else { INNER };
let mut warm = Harness::new(start);
let mut warm_ids = Vec::new();
let mut warm_spans = Vec::new();
let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans);
warm.state.root = Some(root);
// The frame that makes it warm: without it there is nothing retained and
// the comparison is two cold starts agreeing with each other.
warm.frame();
if resizes {
warm.resize(INNER);
warm.frame();
}
if repaints {
for &id in &warm_ids {
warm.rsc.widgets_mut().get_dyn_mut(id);
}
warm.frame();
}
if case == Case::Reorder {
for span in &warm_spans {
warm.rsc[*span].children.rotate_left(1);
}
warm.frame();
}
// What the warm tree was moved into, grown that way from the start.
let want = match case {
Case::Reorder => reordered(node),
_ => node.clone(),
};
let mut cold = Harness::new(INNER);
let mut cold_ids = Vec::new();
let mut cold_spans = Vec::new();
let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans);
cold.state.root = Some(root);
cold.frame();
for (i, (&w, &c)) in warm_ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
let same = match (got, want) {
(Some(g), Some(c)) => {
let d = |a: f32, b: f32| (a - b).abs() <= 0.05;
d(g.top_left.x, c.top_left.x)
&& d(g.top_left.y, c.top_left.y)
&& d(g.bot_right.x, c.bot_right.x)
&& d(g.bot_right.y, c.bot_right.y)
}
(None, None) => true,
_ => false,
};
if !same {
return Some(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
None
}
/// Takes the first simplification that still fails, until none does.
fn shrink(mut node: Node, case: Case) -> Node {
loop {
let Some(next) = node
.smaller()
.into_iter()
.find(|small| diverges(small, case).is_some())
else {
return node;
};
node = next;
}
}
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(fallback)
}
#[test]
#[ignore = "a fuzzer; run it once the ordinary tests pass"]
fn no_grown_tree_lays_out_differently_warm_than_cold() {
let seeds: u64 = env("SHRINK_SEEDS", 400);
let depth: usize = env("SHRINK_DEPTH", 5);
let case = match env("SHRINK_CASE", String::from("resize")).as_str() {
"repaint" => Case::Repaint,
"resize-repaint" => Case::ResizeRepaint,
"reorder" => Case::Reorder,
_ => Case::Resize,
};
for seed in 1..=seeds {
let node = grow(&mut Rng::new(seed), depth);
let Some(how) = diverges(&node, case) else {
continue;
};
let small = shrink(node.clone(), case);
println!(
"seed {seed}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
node.size(),
small.size()
);
panic!("seed {seed} lays out differently warm than cold");
}
let sizes: Vec<usize> = (1..=seeds)
.map(|seed| grow(&mut Rng::new(seed), depth).size())
.collect();
let total: usize = sizes.iter().sum();
println!(
"{seeds} trees at depth {depth} agree: {} widgets total, largest {}",
total,
sizes.iter().max().copied().unwrap_or(0)
);
}
-155
View File
@@ -1,155 +0,0 @@
//! Traces the six-widget tree in `unsettled.rs`, to see what box its text is
//! actually drawn in on a first frame against a settled one.
#![cfg(feature = "layout-diagnostics")]
use iris::core::layout_diagnostics::{self as diag, TraceEvent};
use iris::harness::Harness;
use iris::prelude::*;
fn plant(h: &mut Harness) -> Vec<WidgetId> {
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
let sized = SetSize {
inner: wrapped.add_strong(&mut h.rsc),
x: Some(Len::px(76.0)),
y: None,
}
.add(&mut h.rsc);
let aligned = Aligned {
inner: sized.add_strong(&mut h.rsc),
align: Align {
x: Some(AxisAlign::Pos),
y: Some(AxisAlign::Pos),
},
}
.add(&mut h.rsc);
let stack = Stack {
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
vec![
plain.id(),
wrapped.id(),
sized.id(),
aligned.id(),
stack.id(),
root.id(),
]
}
fn dump(label: &str, report: &diag::Report, text: WidgetId) {
println!("--- {label} ---");
for event in report.traces() {
match event {
TraceEvent::DrawRequest {
id,
region,
pixel_size,
..
} if *id == text => {
println!(
" draw in {:.2}x{:.2} region {region:?}",
pixel_size.x, pixel_size.y
)
}
TraceEvent::SizeReported { id, size } if *id == text => {
println!(" reported {size}")
}
TraceEvent::SizeRead { id, reader, size } if *id == text => {
println!(" size read by {reader:?}: {size}")
}
TraceEvent::Placed { id, parent, region } if *id == text => {
println!(" placed by {parent:?} at {region:?}")
}
TraceEvent::Reuse { id, outcome } if *id == text => println!(" reuse: {outcome:?}"),
_ => {}
}
}
}
#[test]
#[ignore = "a diagnostic, not a check"]
fn what_box_the_text_is_drawn_in() {
diag::clear_traced_widgets();
let _ = diag::take();
let mut h = Harness::new((640, 900));
let ids = plant(&mut h);
let text = ids[1];
diag::trace_widget(text);
let _ = diag::take();
h.frame();
dump("first frame", &diag::take(), text);
for _ in 0..2 {
for &id in &ids {
h.rsc.widgets_mut().get_dyn_mut(id);
}
let _ = diag::take();
h.frame();
dump("repaint", &diag::take(), text);
}
diag::clear_traced_widgets();
}
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
let words = "Wrapping shapes one source into as many lines as the box leaves";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let aligned = Aligned {
inner: text.add_strong(&mut h.rsc),
align: Align {
x: Some(AxisAlign::Neg),
y: None,
},
}
.add(&mut h.rsc);
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
let sized = SetSize {
inner: inner.add_strong(&mut h.rsc),
x: Some(Len::px(189.0)),
y: Some(Len::px(176.0)),
}
.add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc);
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
vec![
text.id(),
aligned.id(),
inner.id(),
sized.id(),
filler.id(),
root.id(),
]
}
#[test]
#[ignore = "a diagnostic, not a check"]
fn what_box_the_fixed_text_is_drawn_in() {
diag::clear_traced_widgets();
let _ = diag::take();
let mut h = Harness::new((1920, 1200));
let ids = plant_fixed(&mut h);
let text = ids[0];
diag::trace_widget(text);
let _ = diag::take();
h.frame();
dump("first frame at 1920", &diag::take(), text);
h.resize((640, 900));
h.frame();
dump("after resize to 640", &diag::take(), text);
let mut cold = Harness::new((640, 900));
let cids = plant_fixed(&mut cold);
diag::clear_traced_widgets();
diag::trace_widget(cids[0]);
let _ = diag::take();
cold.frame();
dump("cold at 640", &diag::take(), cids[0]);
diag::clear_traced_widgets();
}
-298
View File
@@ -1,298 +0,0 @@
//! The smallest trees that laid out differently warm than cold, each shrunk
//! by `tests/shrink.rs` from hundreds of widgets. The first two are a cold
//! frame that had not settled: a wrapping text shaped at a width it was
//! measured in rather than the one it was given. The rest are a widget
//! measured again in a box its own answer had decided, where the old answer
//! is a fixed point whatever the content now says.
use iris::harness::Harness;
use iris::prelude::*;
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
/// the tree changes -- every widget is marked for redraw and the frame is
/// taken again -- so no box may move, and a warm frame has to land where a
/// cold one does.
fn plant(h: &mut Harness) -> Vec<WidgetId> {
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
let sized = SetSize {
inner: wrapped.add_strong(&mut h.rsc),
x: Some(Len::px(76.0)),
y: None,
}
.add(&mut h.rsc);
let aligned = Aligned {
inner: sized.add_strong(&mut h.rsc),
align: Align {
x: Some(AxisAlign::Pos),
y: Some(AxisAlign::Pos),
},
}
.add(&mut h.rsc);
let stack = Stack {
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
size: StackSize::Child(0),
}
.add(&mut h.rsc);
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(root);
vec![
plain.id(),
wrapped.id(),
sized.id(),
aligned.id(),
stack.id(),
root.id(),
]
}
/// The first frame does not reach the layout a second one does, so "cold" is
/// not a fixed point and comparing against it compares against a tree that
/// has not settled.
#[test]
fn one_frame_is_enough() {
let mut h = Harness::new((640, 900));
let ids = plant(&mut h);
let first = h.region(&ids[1]).unwrap();
for _ in 0..3 {
for &id in &ids {
h.rsc.widgets_mut().get_dyn_mut(id);
}
h.frame();
}
let settled = h.region(&ids[1]).unwrap();
println!(
"first frame {} tall, settled {} tall",
first.bot_right.y - first.top_left.y,
settled.bot_right.y - settled.top_left.y
);
assert_eq!(
first.bot_right.y - first.top_left.y,
settled.bot_right.y - settled.top_left.y,
"the first frame had not finished laying out"
);
}
#[test]
fn repainting_everything_moves_nothing() {
let mut warm = Harness::new((640, 900));
let ids = plant(&mut warm);
for &id in &ids {
warm.rsc.widgets_mut().get_dyn_mut(id);
}
warm.frame();
let mut cold = Harness::new((640, 900));
let cold_ids = plant(&mut cold);
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box
/// is the same size whatever the output is, so a resize may not change any of
/// it -- but the text comes out 3.92px narrower warm than cold.
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
let words = "Wrapping shapes one source into as many lines as the box leaves";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let aligned = Aligned {
inner: text.add_strong(&mut h.rsc),
align: Align {
x: Some(AxisAlign::Neg),
y: None,
},
}
.add(&mut h.rsc);
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
let sized = SetSize {
inner: inner.add_strong(&mut h.rsc),
x: Some(Len::px(189.0)),
y: Some(Len::px(176.0)),
}
.add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc);
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
h.state.root = Some(root.add_strong(&mut h.rsc));
vec![
text.id(),
aligned.id(),
inner.id(),
sized.id(),
filler.id(),
root.id(),
]
}
#[test]
fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
let mut warm = Harness::new((1920, 1200));
let ids = plant_fixed(&mut warm);
warm.frame();
warm.resize((640, 900));
warm.frame();
let mut cold = Harness::new((640, 900));
let cold_ids = plant_fixed(&mut cold);
cold.frame();
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by
/// moving them, cold by growing them that way. Same widgets, same sizes, one
/// ends up 29.9px from where the other does.
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
let wrapped = wtext("Wrapping shapes one source into as many lines")
.size(16)
.wrap(true)
.add(&mut h.rsc);
let plain = wtext("one line, overflowing whatever it is given")
.size(16)
.wrap(false)
.add(&mut h.rsc);
let first: StrongWidget = wrapped.add_strong(&mut h.rsc);
let second: StrongWidget = plain.add_strong(&mut h.rsc);
let children = match swapped {
true => vec![second, first],
false => vec![first, second],
};
let span = Span {
children,
dir: Dir::RIGHT,
gap: 0.0,
}
.add(&mut h.rsc);
let span_handle = span;
let aligned = Aligned {
inner: span.add_strong(&mut h.rsc),
align: Align {
x: Some(AxisAlign::Center),
y: None,
},
}
.add(&mut h.rsc);
h.state.root = Some(aligned.add_strong(&mut h.rsc));
(
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
span_handle,
)
}
#[test]
fn swapping_two_children_lands_where_growing_them_that_way_does() {
let mut warm = Harness::new((640, 900));
let (ids, span) = plant_pair(&mut warm, false);
warm.frame();
warm.rsc[span].children.rotate_left(1);
warm.frame();
let mut cold = Harness::new((640, 900));
let (cold_ids, _) = plant_pair(&mut cold, true);
cold.frame();
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its
/// content from what the content says, and hands that box down through a
/// pass-through; the span under it was placed once, in that box, so nothing
/// at its own edge says the box was its own answer.
fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
let words = "Wrapping shapes one source into as many lines as the box leaves room for,";
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
let filler = rect(Color::RED).add(&mut h.rsc);
let mut inner_children: Vec<StrongWidget> =
vec![text.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)];
if swapped {
inner_children.rotate_left(1);
}
let inner = Span {
children: inner_children,
dir: Dir::RIGHT,
gap: 0.0,
}
.add(&mut h.rsc);
let block = rect(Color::RED).add(&mut h.rsc);
let fixed = SetSize {
inner: block.add_strong(&mut h.rsc),
x: Some(Len::px(87.0)),
y: None,
}
.add(&mut h.rsc);
let mut outer_children: Vec<StrongWidget> =
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
if swapped {
outer_children.rotate_left(1);
}
let outer = Span {
children: outer_children,
dir: Dir::RIGHT,
gap: 0.0,
}
.add(&mut h.rsc);
let through = SetSize {
inner: outer.add_strong(&mut h.rsc),
x: None,
y: None,
}
.add(&mut h.rsc);
let scroll = Scroll::new(through.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
h.state.root = Some(scroll.add_strong(&mut h.rsc));
(
vec![
text.id(),
filler.id(),
inner.id(),
block.id(),
fixed.id(),
outer.id(),
through.id(),
scroll.id(),
],
[inner, outer],
)
}
#[test]
fn a_span_placed_once_in_a_box_its_answer_decided() {
let mut warm = Harness::new((640, 900));
let (ids, spans) = plant_scrolled(&mut warm, false);
warm.frame();
for span in spans {
warm.rsc[span].children.rotate_left(1);
}
warm.frame();
let mut cold = Harness::new((640, 900));
let (cold_ids, _) = plant_scrolled(&mut cold, true);
cold.frame();
let mut wrong = Vec::new();
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
let (got, want) = (warm.region(&w), cold.region(&c));
if got != want {
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}