Compare commits

..
Author SHA1 Message Date
iris 7cefc72f97 Compare regions, not four loose numbers
`PixelRegion` derives `PartialEq`, `Clone` and `Copy`, so `assert_corners!`
compares one against another instead of flattening both to a tuple whose
order there was nothing to check. A failure now prints two regions.
2026-09-13 21:42:21 -04:00
iris 0f9f379cec Say what these do without naming what they lean on
A doc comment that names something outside the method goes stale when
that thing changes, and nobody editing it looks here.
2026-09-13 21:37:44 -04:00
iris 7eb2b85425 One proxy, and a macro instead of a shared test module
`Proxy` is the task queue as well as the way an application sends its own
events, so `ProxyQueue` is gone. `schedule_redraw` becomes
`request_redraw_if_needed`, which says what the comment beside it was
saying.

`assert_corners!` replaces the region helper, so `tests/common` goes with
it, and the scroll test now states both corners rather than one number.
2026-09-13 21:29:38 -04:00
iris 3a74a04a5b Post task updates to the loop instead of waking it
`WakeTaskQueue` becomes `TaskQueue`, which carries the update itself.
Delivery and waking are then one act: the winit host sends it through the
`EventLoopProxy` as a `DefaultEvent::Update`, so there is no channel
beside the loop and nothing has to claim a redraw is needed in order to be
looked at. `Window::request_redraw` is gone from this path; `event`
applies the update and then asks the tree whether anything became dirty,
which is the same question `window_event` already ended with -- now
`schedule_redraw`, called from both.

The loop's message type is `DefaultEvent<State>`, so `Proxy` becomes a
wrapper that takes the application's own `Event` and requires it to be
`Send`, since it now crosses to the task thread by that route.

The harness supplies a channel-backed queue, which is what lets a test
hold updates until it asks for them.

Tests split by subject -- layout, pointer, scroll, tasks -- with the
region helper in `tests/common`.
2026-09-13 21:04:29 -04:00
iris e97aba30e0 Merge upstream/main (#14) into split/15-harness 2026-09-13 20:44:01 -04:00
iris d5efdd2b97 Run a ui without a window, and test one
`Tasks` held an `Arc<Window>` only to call `request_redraw` when a task
finished, which made the task queue -- and so `DefaultRsc` -- impossible
to build without a window. It now takes an `Arc<dyn WakeTaskQueue>`, and
`Window` implements it.

Waking moves from "the task ended" to "an update was sent", which is when
there is something for the host to apply: a task that keeps running after
sending one no longer holds its update until it finishes, and a task that
sends none no longer asks for a frame it does not need.

`iris::harness` is what that buys. `UiRenderState` already does layout,
hit testing and primitive building with no surface, so a test can build a
tree, run frames, move a pointer and read back where widgets landed.
`tests/harness.rs` does each of those; none of them could be written
before, since the only entry point to layout was a window.

It does not draw. A claim about pixels still needs a real surface.
2026-09-13 20:14:11 -04:00
61 changed files with 723 additions and 4451 deletions

No files matched your search

Generated
+16 -25
View File
@@ -567,9 +567,9 @@ dependencies = [
[[package]] [[package]]
name = "dlib" name = "dlib"
version = "0.5.3" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [ dependencies = [
"libloading", "libloading",
] ]
@@ -2158,9 +2158,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.41.0" version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -2353,15 +2353,6 @@ version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
[[package]]
name = "rig-input"
version = "0.1.0"
dependencies = [
"iris",
"wayland-client",
"wayland-protocols-wlr",
]
[[package]] [[package]]
name = "roxmltree" name = "roxmltree"
version = "0.21.1" version = "0.21.1"
@@ -2918,9 +2909,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-backend" name = "wayland-backend"
version = "0.3.17" version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9"
dependencies = [ dependencies = [
"cc", "cc",
"downcast-rs", "downcast-rs",
@@ -2932,9 +2923,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-client" name = "wayland-client"
version = "0.31.15" version = "0.31.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"rustix 1.1.3", "rustix 1.1.3",
@@ -2966,9 +2957,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-protocols" name = "wayland-protocols"
version = "0.32.13" version = "0.32.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"wayland-backend", "wayland-backend",
@@ -2991,9 +2982,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-protocols-wlr" name = "wayland-protocols-wlr"
version = "0.3.12" version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"wayland-backend", "wayland-backend",
@@ -3004,9 +2995,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-scanner" name = "wayland-scanner"
version = "0.31.11" version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quick-xml", "quick-xml",
@@ -3015,9 +3006,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-sys" name = "wayland-sys"
version = "0.31.11" version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd"
dependencies = [ dependencies = [
"dlib", "dlib",
"log", "log",
+1 -6
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]
@@ -23,7 +20,7 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] } tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
[workspace] [workspace]
members = ["core", "macro", "rig-input"] members = ["core", "macro"]
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.0"
@@ -43,5 +40,3 @@ arboard = "3.6.1"
iris-core = { path = "core" } iris-core = { path = "core" }
iris-macro = { path = "macro" } iris-macro = { path = "macro" }
tokio = "1.49.0" tokio = "1.49.0"
wayland-client = "0.31.15"
wayland-protocols-wlr = { version = "0.3.12", features = ["client"] }
-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 }
+1 -6
View File
@@ -79,7 +79,6 @@ type EventData<Rsc, E> = (E, Rc<dyn for<'a> EventFn<Rsc, <E as Event>::Data<'a>>
pub struct TypeEventManager<Rsc: HasEvents, E: Event> { pub struct TypeEventManager<Rsc: HasEvents, E: Event> {
// TODO: reduce visiblity!! // TODO: reduce visiblity!!
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>, pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
pub global: E::Global,
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>, map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>,
} }
@@ -108,7 +107,6 @@ impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
fn default() -> Self { fn default() -> Self {
Self { Self {
active: Default::default(), active: Default::default(),
global: Default::default(),
map: Default::default(), map: Default::default(),
} }
} }
@@ -140,13 +138,11 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
pub fn run_fn<'a>( pub fn run_fn<'a>(
&mut self, &mut self,
id: impl IdLike, id: impl IdLike,
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) -> bool + 'a { ) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) + 'a {
let fs = self.map.get(&id.id()).cloned().unwrap_or_default(); let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
move |ctx, rsc| { move |ctx, rsc| {
let mut consumed = false;
for (e, f) in fs { for (e, f) in fs {
if let Some(data) = e.should_run(&ctx.data) { if let Some(data) = e.should_run(&ctx.data) {
consumed |= e.consumes(&data);
f( f(
EventCtx { EventCtx {
state: ctx.state, state: ctx.state,
@@ -156,7 +152,6 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
) )
} }
} }
consumed
} }
} }
} }
-9
View File
@@ -9,19 +9,10 @@ pub use rsc::*;
pub trait Event: Sized + 'static + Clone { pub trait Event: Sized + 'static + Clone {
type Data<'a>: Clone = (); type Data<'a>: Clone = ();
type State: Default = (); type State: Default = ();
/// State the whole event type keeps, rather than one copy per widget.
type Global: Default = ();
#[allow(unused_variables)] #[allow(unused_variables)]
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> { fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
Some(data.clone()) Some(data.clone())
} }
/// Whether having run on this data uses up whatever triggered it, so
/// nothing further should see it.
#[allow(unused_variables)]
fn consumes(&self, data: &Self::Data<'_>) -> bool {
false
}
} }
pub trait EventLike { pub trait EventLike {
+1 -2
View File
@@ -21,13 +21,12 @@ pub trait HasEvents: Sized + UiRsc + HasState {
} }
pub trait RunEvents: HasEvents { pub trait RunEvents: HasEvents {
/// Whether anything that ran used up what triggered it.
fn run_event<E: EventLike>( fn run_event<E: EventLike>(
&mut self, &mut self,
id: impl IdLike, id: impl IdLike,
data: <E::Event as Event>::Data<'_>, data: <E::Event as Event>::Data<'_>,
state: &mut Self::State, state: &mut Self::State,
) -> bool { ) {
let f = self.events_mut().get_type::<E>().run_fn(id); let f = self.events_mut().get_type::<E>().run_fn(id);
f(EventCtx { state, data }, self) f(EventCtx { state, data }, self)
} }
-482
View File
@@ -1,482 +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,
DepthSteps,
EagerReaderRedraws,
LocalRedraws,
SizeChanges,
ReaderEdges,
PrimitiveWrites,
TextRenders,
TextShapeHits,
TextShapes,
}
impl Counter {
const COUNT: usize = Self::TextShapes 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 steps",
"eager reader redraws",
"local redraws",
"size changes",
"reader edges",
"primitive writes",
"text renders",
"text shape hits",
"text shapes",
];
}
#[derive(Clone, Copy)]
pub(crate) enum TimerKind {
Update,
FullLayout,
ResizeMarking,
IncrementalLayout,
TextRender,
TextShape,
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",
"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;
+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,
+28 -1
View File
@@ -56,6 +56,13 @@ impl UiVec2 {
} }
} }
pub const fn outside(&self, region: &UiRegion) -> UiVec2 {
UiVec2 {
x: self.x.outside(&region.x),
y: self.y.outside(&region.y),
}
}
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar { pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
match axis { match axis {
Axis::X => &mut self.x, Axis::X => &mut self.x,
@@ -202,6 +209,12 @@ impl UiScalar {
} }
} }
pub const fn outside(&self, span: &UiSpan) -> 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);
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,
@@ -270,6 +283,13 @@ impl UiSpan {
} }
} }
pub const fn outside(&self, parent: &Self) -> Self {
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
} }
@@ -304,7 +324,14 @@ impl UiRegion {
y: self.y.within(&parent.y), y: self.y.within(&parent.y),
} }
} }
pub const fn axis(&self, axis: Axis) -> &UiSpan { pub const fn outside(&self, parent: &Self) -> Self {
Self {
x: self.x.outside(&parent.x),
y: self.y.outside(&parent.y),
}
}
pub const fn axis(&mut self, axis: Axis) -> &UiSpan {
match axis { match axis {
Axis::X => &self.x, Axis::X => &self.x,
Axis::Y => &self.y, Axis::Y => &self.y,
+1 -17
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,
}; };
@@ -140,14 +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;
} }
#[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);
@@ -279,16 +271,8 @@ impl TextData {
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>, width: Option<f32>,
) -> 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);
let glyphs = { let glyphs = self.place(buffer);
#[cfg(feature = "layout-diagnostics")]
let _place = diag::timer(TimerKind::GlyphPlacement);
self.place(buffer)
};
RenderedText { RenderedText {
glyphs, glyphs,
size: buffer.size(), size: buffer.size(),
+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 }
}
} }
+11 -38
View File
@@ -17,7 +17,7 @@ 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");
@@ -34,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 {
@@ -128,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[..]) {
}
if ui_render.moves.changed {
ui_render.moves.changed = false;
regroup |= self.moves.update(device, queue, ui_render.moves.entries());
}
if regroup {
self.shared_group = Self::shared_group( self.shared_group = Self::shared_group(
device, device,
&self.shared_layout, &self.shared_layout,
&self.window_buffer, &self.window_buffer,
&self.masks, &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"),
@@ -170,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,
@@ -187,7 +177,6 @@ impl UiRenderNode {
layers: HashMap::default(), layers: HashMap::default(),
active: Vec::new(), active: Vec::new(),
masks, masks,
moves,
} }
} }
@@ -263,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: &[
@@ -288,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"),
}) })
@@ -308,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,
@@ -321,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"),
}) })
+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 {
+8 -62
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.abs + mix(p.start.abs, p.end.abs, 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 {
@@ -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,15 +52,10 @@ 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_abs = vec2(r.x.start.abs, r.y.start.abs);
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
let bot_right_abs = vec2(r.x.end.abs, r.y.end.abs);
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs); 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_abs); let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
@@ -132,13 +81,10 @@ 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_abs = vec2(m.x.start.abs, m.y.start.abs);
let br = vec2(m.x.end.rel, m.y.end.rel);
let br_abs = vec2(m.x.end.abs, m.y.end.abs);
let top_left = floor(tl * window.dim) + floor(tl_abs); let top_left = floor(tl * window.dim) + floor(tl_abs);
let bot_right = floor(br * window.dim) + floor(br_abs); let bot_right = floor(br * window.dim) + floor(br_abs);
+1 -26
View File
@@ -1,39 +1,14 @@
use crate::{ use crate::{LayerId, MaskIdx, PrimitiveHandle, 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)]
pub struct ActiveData { pub struct ActiveData {
pub id: WidgetId, pub id: WidgetId,
pub region: UiRegion, pub region: UiRegion,
/// What the widget said it used of `region`, the last time it drew.
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,
pub parent: Option<WidgetId>, pub parent: Option<WidgetId>,
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.
pub size_deps: Vec<WidgetId>,
/// Offered pixel axes which flowed into this widget's reported size,
/// directly or through a child size it read.
pub size_box_inputs: [bool; 2],
/// Output axes read while producing `size`, distinct from the widget's
/// own box when that box has a fixed pixel length.
pub size_output_inputs: [bool; 2],
/// The output dimensions against which those dependencies were observed.
pub output_px: Vec2,
/// Output axes it read directly or while resolving its offered box.
pub reads_output: [bool; 2],
/// The slot its primitives are positioned through: its own if its parent
/// placed it, otherwise the nearest ancestor that has one.
pub move_idx: MoveIdx,
/// 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,
} }
+18
View File
@@ -0,0 +1,18 @@
use crate::{BothAxis, Len, UiVec2, WidgetId, util::HashMap};
#[derive(Default)]
pub struct Cache {
pub size: BothAxis<HashMap<WidgetId, (UiVec2, Len)>>,
}
impl Cache {
pub fn remove(&mut self, id: WidgetId) {
self.size.x.remove(&id);
self.size.y.remove(&id);
}
pub fn clear(&mut self) {
self.size.x.clear();
self.size.y.clear();
}
}
+4 -90
View File
@@ -1,21 +1,17 @@
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 cache;
mod painter; mod painter;
mod render_state; mod render_state;
mod size;
pub use active::*; pub use active::*;
pub use painter::{Painter, PrimitiveLike}; pub use painter::{Painter, PrimitiveLike};
pub use render_state::*; pub use render_state::*;
pub use size::*;
#[derive(Default)] #[derive(Default)]
pub struct UiData { pub struct UiData {
@@ -27,88 +23,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;
+27 -214
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, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, 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,21 +13,11 @@ 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 whose size this widget read while drawing.
pub(super) size_deps: Vec<WidgetId>,
/// Offered pixel axes which can affect the size this draw reports.
pub(super) size_box_inputs: [bool; 2],
pub(super) size_output_inputs: [bool; 2],
pub(super) reads_output: [bool; 2],
/// The slot this widget's primitives are positioned through: its own if
/// its parent placed it, otherwise the nearest ancestor that has one.
pub(super) move_idx: MoveIdx,
pub layer: usize, pub layer: usize,
pub(super) id: WidgetId, pub(super) id: WidgetId,
} }
@@ -42,8 +30,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 {
@@ -52,7 +38,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);
@@ -79,159 +64,31 @@ 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<W: ?Sized>(&mut self, id: &StrongWidget<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.
pub fn widget_within<'s, W: ?Sized>( /// Useful for drawing child widgets in select areas.
&'s mut self, pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
id: &'s StrongWidget<W>, self.widget_at(id, region.within(&self.region));
region: UiRegion,
) -> DrawResult<'s, 'a, W> {
let region = region.within(&self.region);
self.widget_at(id, region, false)
} }
/// Draws a child this widget decides the box of, and may decide again fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
/// 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>(
&'s mut self,
id: &'s StrongWidget<W>,
region: UiRegion,
slotted: bool,
) -> DrawResult<'s, 'a, W> {
// A child listed twice would be moved twice.
if !self.children.contains(&id.id()) {
self.children.push(id.id()); self.children.push(id.id());
} self.state.draw_inner(
let size = self.state.draw_inner(
self.layer, self.layer,
id.id(), id.id(),
region, region,
Some(self.id), Some(self.id),
self.move_idx,
slotted,
self.mask, self.mask,
None, None,
self.rsc, self.rsc,
); );
DrawResult {
child: id,
painter: self,
size,
}
}
/// What a child says its length is without being drawn, if it can say.
/// Asking counts as reading its size.
pub fn size_hint<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<Len> {
let hint = self
.rsc
.widgets()
.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_size(id, false);
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
/// offered. Unlike a hint, this is contextual: it is kept only when none
/// of the offered pixel axes which produced it changed.
pub fn known_len<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
axis: Axis,
region: UiRegion,
) -> Option<Len> {
if let Some(hint) = self.size_hint(child, axis) {
return Some(hint);
}
self.retained_size(child, region)
.map(|size| size.axis(axis))
}
fn retained_size<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
region: UiRegion,
) -> Option<Size> {
let region = region.within(&self.region);
let (size, box_inputs, output_inputs) =
self.state
.retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?;
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::RetainedSizeHits);
self.depend_on_size_inputs(child, box_inputs, output_inputs);
Some(size)
}
fn depend_on_size<W: ?Sized>(&mut self, child: &StrongWidget<W>, inherit_inputs: bool) {
let (box_inputs, output_inputs) = match inherit_inputs {
true => self
.state
.active
.get(&child.id())
.map_or(([false; 2], [false; 2]), |active| {
(active.size_box_inputs, active.size_output_inputs)
}),
false => ([false; 2], [false; 2]),
};
self.depend_on_size_inputs(child, box_inputs, output_inputs);
}
fn depend_on_size_inputs<W: ?Sized>(
&mut self,
child: &StrongWidget<W>,
box_inputs: [bool; 2],
output_inputs: [bool; 2],
) {
if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id());
}
for (own, child) in self.size_box_inputs.iter_mut().zip(box_inputs) {
*own |= child;
}
for (own, child) in self.size_output_inputs.iter_mut().zip(output_inputs) {
*own |= child;
}
} }
pub fn render_text( pub fn render_text(
@@ -240,8 +97,6 @@ impl<'a> Painter<'a> {
attrs: &TextAttrs, attrs: &TextAttrs,
width: Option<f32>, width: Option<f32>,
) -> 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)
} }
@@ -270,48 +125,27 @@ 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
} }
/// The output's size in pixels. A widget that reads it draws again when pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
/// the output changes, since nothing else can put that right. self.size_ctx().size(id)
pub fn output_size(&mut self) -> Vec2 { }
self.reads_output = [true; 2];
self.size_output_inputs = [true; 2]; pub fn len_axis<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Len {
match axis {
Axis::X => self.size_ctx().width(id),
Axis::Y => self.size_ctx().height(id),
}
}
pub fn output_size(&self) -> Vec2 {
self.state.output_size self.state.output_size
} }
/// One axis of the output in pixels. Prefer this to [`Self::output_size`]
/// when the other axis cannot affect the size this widget reports.
pub fn output_len(&mut self, axis: Axis) -> f32 {
self.reads_output[axis as usize] = true;
self.size_output_inputs[axis as usize] = true;
self.state.output_size.axis(axis)
}
/// This widget's box in pixels. Resolved against the output's size and
/// 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.reads_output = [true; 2]; self.region.size().to_abs(self.state.output_size)
self.size_box_inputs = [true; 2];
let region = self.state.moves.resolve(self.move_idx, self.region);
region.size().to_abs(self.state.output_size)
}
/// One axis of this widget's box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the reported size.
pub fn px_len(&mut self, axis: Axis) -> f32 {
self.reads_output[axis as usize] = true;
self.size_box_inputs[axis as usize] = true;
let region = self.state.moves.resolve(self.move_idx, self.region);
region
.size()
.axis(axis)
.to_abs(self.state.output_size.axis(axis))
} }
pub fn text_data(&mut self) -> &mut TextData { pub fn text_data(&mut self) -> &mut TextData {
@@ -333,30 +167,9 @@ impl<'a> Painter<'a> {
pub fn id(&self) -> &WidgetId { pub fn id(&self) -> &WidgetId {
&self.id &self.id
} }
}
/// A child that has just been drawn. Reading its size records that this pub fn size_ctx(&mut self) -> SizeCtx<'_> {
/// widget's own size depends on it; dropping it without reading draws the self.state.size_ctx(self.id, self.region.size(), self.rsc)
/// child and leaves the parent independent of what it came to.
pub struct DrawResult<'p, 'a, W: ?Sized> {
painter: &'p mut Painter<'a>,
child: &'p StrongWidget<W>,
size: Size,
}
impl<W: ?Sized> DrawResult<'_, '_, W> {
pub fn size(self) -> Size {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::SizeReads);
diag::size_read(self.child.id(), self.painter.id, self.size);
}
self.painter.depend_on_size(self.child, true);
self.size
}
pub fn len(self, axis: Axis) -> Len {
self.size().axis(axis)
} }
} }
+78 -504
View File
@@ -1,37 +1,19 @@
#[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, Painter, PixelRegion, SizeCtx, StrongWidget,
Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets, UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
util::{HashMap, HashSet, Vec2}, ui::cache::Cache,
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,
pub cache: Cache,
old_root: Option<WidgetId>, old_root: Option<WidgetId>,
resized: [bool; 2], resized: bool,
/// Content/state dirtiness whose retained size cannot answer a layout
/// question until that widget has drawn again.
invalid_sizes: HashSet<WidgetId>,
/// Marks introduced only to traverse resize dependency paths. Unlike
/// content dirtiness, these may retain an answer whose observed pixel
/// axes did not change.
resize_marks: HashSet<WidgetId>,
draw_started: HashSet<WidgetId>, draw_started: HashSet<WidgetId>,
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
/// replaces that while its children go on pointing at the slot.
slots: HashMap<WidgetId, MoveIdx>,
pub moves: Moves,
} }
impl UiRenderState { impl UiRenderState {
@@ -39,23 +21,17 @@ impl UiRenderState {
Self { Self {
active: Default::default(), active: Default::default(),
layers: Default::default(), layers: Default::default(),
cache: Default::default(),
output_size: Vec2::ZERO, output_size: Vec2::ZERO,
old_root: None, old_root: None,
resized: [false; 2], resized: false,
invalid_sizes: Default::default(),
resize_marks: Default::default(),
draw_started: Default::default(), draw_started: Default::default(),
slots: Default::default(),
moves: Default::default(),
} }
} }
pub fn resize(&mut self, size: impl Into<Vec2>) { pub fn resize(&mut self, size: impl Into<Vec2>) {
let size = size.into(); self.output_size = size.into();
for (axis, resized) in AXES.into_iter().zip(self.resized.iter_mut()) { self.resized = true;
*resized |= size.axis(axis) != self.output_size.axis(axis);
}
self.output_size = size;
} }
pub fn output_size(&self) -> Vec2 { pub fn output_size(&self) -> Vec2 {
@@ -63,14 +39,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());
self.resize_marks.clear();
// safety mechanism for memory leaks; might wanna return a result instead so user can // safety mechanism for memory leaks; might wanna return a result instead so user can
// decide whether to panic or not // decide whether to panic or not
if !rsc.widgets().waiting.is_empty() { if !rsc.widgets().waiting.is_empty() {
@@ -88,75 +56,20 @@ impl UiRenderState {
); );
} }
let root = root.into(); let root = root.into();
if self.root_changed(root) { if self.needs_full_redraw(root) {
self.redraw_all(root, rsc); self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id()); self.old_root = root.map(|r| r.id());
} else if self.resized.iter().any(|&resized| resized) { self.resized = false;
// A region is a fraction of the output plus an offset, resolved } else if rsc.widgets().has_updates() {
// against the window in the shader, so a resize moves the whole
// drawing on its own. Only a widget that read pixels can be wrong.
{
#[cfg(feature = "layout-diagnostics")]
let _marking = diag::timer(TimerKind::ResizeMarking);
let dependents: Vec<_> = self
.active
.iter()
.filter_map(|(&id, active)| {
AXES.into_iter()
.zip(self.resized)
.any(|(axis, changed)| {
changed
&& active.reads_output[axis as usize]
&& pixel_len_changed(
active.output_px.axis(axis),
self.output_size.axis(axis),
)
})
.then_some(id)
})
.collect();
for id in dependents {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ResizeDependents);
rsc.widgets_mut().needs_redraw.insert(id);
if let Some(top) = self.mark_readers(id, rsc) {
rsc.widgets_mut().needs_redraw.insert(top);
}
}
self.resize_marks.extend(
rsc.widgets()
.needs_redraw
.iter()
.filter(|id| !self.invalid_sizes.contains(id))
.copied(),
);
}
}
if rsc.widgets().has_updates() {
self.redraw_updates(rsc); self.redraw_updates(rsc);
} }
self.resized = [false; 2];
self.invalid_sizes.clear();
self.resize_marks.clear();
} }
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) { fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
#[cfg(feature = "layout-diagnostics")]
let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc); self.clear(rsc);
// free all resources & cache // free all resources & cache
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,
MoveIdx::NONE,
false,
MaskIdx::NONE,
None,
rsc,
);
} }
} }
@@ -168,21 +81,22 @@ impl UiRenderState {
id: WidgetId, id: WidgetId,
region: UiRegion, region: UiRegion,
parent: Option<WidgetId>, parent: Option<WidgetId>,
parent_move: MoveIdx,
slotted: bool,
mask: MaskIdx, mask: MaskIdx,
old_children: Option<Vec<WidgetId>>, old_children: Option<Vec<WidgetId>>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> Size { ) {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::DrawRequests);
diag::draw_request(id, parent, region, self.px_of(parent_move, region), slotted);
}
let mut old_children = old_children.unwrap_or_default(); let mut old_children = old_children.unwrap_or_default();
if self.active.contains_key(&id) { if let Some(active) = self.active.get_mut(&id)
if let Some(size) = self.try_reuse(id, region, parent_move, rsc) { && !rsc.widgets().needs_redraw.contains(&id)
return size; {
// check to see if we can skip drawing first
if active.region == region {
return;
} else if active.region.size() == region.size() {
// TODO: epsilon?
let from = active.region;
self.mov(id, from, region);
return;
} }
// if not, then maintain resize and track old children to remove unneeded // if not, then maintain resize and track old children to remove unneeded
let active = self.remove(id, false, rsc).unwrap(); let active = self.remove(id, false, rsc).unwrap();
@@ -190,90 +104,48 @@ impl UiRenderState {
} }
// 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);
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(),
size_deps: Vec::new(),
size_box_inputs: [false; 2],
size_output_inputs: [false; 2],
reads_output: [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); 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,
size_deps,
size_box_inputs,
size_output_inputs,
reads_output,
move_idx,
layer, layer,
id, id,
} = painter; } = painter;
debug_assert!(
Self::hints_agree(id, size, rsc),
"'{}' ({id:?}) drew a size its size_hint disagrees with",
rsc.widgets().label(id)
);
// add to active // add to active
let active = ActiveData { let active = ActiveData {
id, id,
region, region,
size,
px,
parent, parent,
textures, textures,
primitives, primitives,
children, children,
size_deps,
size_box_inputs,
size_output_inputs,
output_px: self.output_size,
reads_output,
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) {
@@ -283,253 +155,20 @@ impl UiRenderState {
rsc.on_draw(&active); rsc.on_draw(&active);
self.active.insert(id, active); self.active.insert(id, active);
self.invalid_sizes.remove(&id);
self.resize_marks.remove(&id);
size
} }
/// The slot a widget's box is held in, made on its first placed draw and fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
/// 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.
fn px_of(&self, slot: MoveIdx, region: UiRegion) -> Vec2 {
self.moves
.resolve(slot, region)
.size()
.to_abs(self.output_size)
}
/// A clean widget's retained size, when the offered pixel axes which
/// produced that answer are unchanged. This observes the old answer only;
/// it does not move or otherwise reuse the widget's drawing.
pub(super) fn retained_size(
&self,
id: WidgetId,
region: UiRegion,
parent_move: MoveIdx,
widgets: &Widgets,
) -> Option<(Size, [bool; 2], [bool; 2])> {
if self.size_is_invalid(id, widgets) || self.dirty_size_under(id, widgets) {
return None;
}
let active = self.active.get(&id)?;
if active.parent_move != parent_move {
return None;
}
let px = self.px_of(parent_move, region);
let valid_box = AXES
.into_iter()
.zip(active.size_box_inputs)
.all(|(axis, depends)| {
!depends || !pixel_len_changed(active.px.axis(axis), px.axis(axis))
});
let valid_output =
AXES.into_iter()
.zip(active.size_output_inputs)
.all(|(axis, depends)| {
!depends
|| !pixel_len_changed(
active.output_px.axis(axis),
self.output_size.axis(axis),
)
});
(valid_box && valid_output).then_some((
active.size,
active.size_box_inputs,
active.size_output_inputs,
))
}
fn size_is_invalid(&self, id: WidgetId, widgets: &Widgets) -> bool {
self.invalid_sizes.contains(&id)
|| (widgets.needs_redraw.contains(&id) && !self.resize_marks.contains(&id))
}
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
self.active.get(&id).is_some_and(|active| {
active.size_deps.iter().any(|child| {
self.size_is_invalid(*child, widgets) || self.dirty_size_under(*child, widgets)
})
})
}
/// The drawing a widget already has, kept for a new box if the box has not
/// changed in a way it depends on.
fn try_reuse(
&mut self,
id: WidgetId,
region: UiRegion,
parent_move: MoveIdx,
rsc: &mut dyn UiRsc,
) -> Option<Size> {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ReuseAttempts);
if rsc.widgets().needs_redraw.contains(&id) {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseDirty);
diag::reuse(id, ReuseOutcome::Dirty);
}
return None;
}
let active = self.active.get(&id)?;
// Drawn somewhere else in the tree: its box is in coordinates it no
// longer sits in, and its slot names the wrong parent.
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);
}
return Some(size);
}
// Only a placed widget can be given a different box without drawing
// again: everything it drew is a fraction of its slot's box, so one
// entry says where all of it went.
if slot == parent_move {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseUnslotted);
diag::reuse(id, ReuseOutcome::Unslotted);
}
return None;
}
if changed.iter().any(|&c| c) {
let widget = rsc.widgets().get_dyn(id)?;
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;
}
}
self.moves.set(slot, region);
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
active.region = region; for h in &active.primitives {
#[cfg(feature = "layout-diagnostics")] let region = self.layers[h.layer].region_mut(h);
{ *region = region.outside(&from).within(&to);
diag::bump(Counter::ReuseMoved);
diag::reuse(id, ReuseOutcome::Moved);
} }
Some(size) active.region = active.region.outside(&from).within(&to);
// SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) };
for child in children {
self.mov(*child, from, to);
} }
/// Whether anything under `id` would have to be drawn again for the box
/// it is a fraction of changing length, `changed` saying which axes of
/// that box did.
///
/// 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 {
return false;
};
let size_deps = &active.size_deps;
active.children.iter().any(|&child| {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::ResizeCheckChildren);
let Some(data) = self.active.get(&child) else {
return false;
};
let Some(widget) = rsc.widgets().get_dyn(child) else {
return true;
};
// What it drew to learn this child's size was the child in *this*
// box, so a different box is a different answer -- unless the
// child gave an exact one without being drawn at all.
if size_deps.contains(&child) {
let measured = AXES
.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)
})
}
fn hints_agree(id: WidgetId, size: Size, rsc: &dyn UiRsc) -> bool {
let Some(widget) = rsc.widgets().get_dyn(id) else {
return true;
};
AXES.into_iter().all(|axis| {
widget
.size_hint(axis)
.is_none_or(|hint| hint == size.axis(axis))
})
} }
/// NOTE: instance textures are cleared and self.textures freed /// NOTE: instance textures are cleared and self.textures freed
@@ -552,16 +191,13 @@ impl UiRenderState {
} }
fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> { fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
self.cache.remove(id);
let inst = self.remove(id, true, rsc); let inst = self.remove(id, true, rsc);
if let Some(inst) = &inst { if let Some(inst) = &inst {
for c in &inst.children { for c in &inst.children {
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
} }
@@ -569,61 +205,34 @@ 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.cache.clear();
self.moves.clear();
self.layers.clear(); self.layers.clear();
self.invalid_sizes.clear();
self.resize_marks.clear();
rsc.widgets_mut().needs_redraw.clear(); rsc.widgets_mut().needs_redraw.clear();
rsc.free(); rsc.free();
} }
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. 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();
match self.resized.iter().any(|&resized| resized) {
true => dirty.min_by_key(|&id| self.depth(id)),
false => dirty.max_by_key(|&id| self.depth(id)),
}
} {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::QueuePops);
self.redraw(id, rsc); self.redraw(id, rsc);
} }
rsc.free(); rsc.free();
} }
fn depth(&self, id: WidgetId) -> usize {
let mut depth = 0;
let mut at = Some(id);
while let Some(id) = at {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::DepthSteps);
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
} }
// Scheduling and drawing must use the same full-redraw predicate.
fn needs_full_redraw<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
self.root_changed(root) || self.resized
}
pub fn needs_redraw<'a>( pub fn needs_redraw<'a>(
&self, &self,
root: impl Into<Option<&'a StrongWidget>>, root: impl Into<Option<&'a StrongWidget>>,
widgets: &Widgets, widgets: &Widgets,
) -> bool { ) -> bool {
self.root_changed(root) self.needs_full_redraw(root) || widgets.has_updates()
|| self.resized.iter().any(|&resized| resized)
|| widgets.has_updates()
} }
pub fn active_widgets(&self) -> usize { pub fn active_widgets(&self) -> usize {
@@ -649,40 +258,29 @@ 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) {
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.remove(&id); self.draw_started.remove(&id);
if rsc.widgets().needs_redraw.contains(&id) && !self.resize_marks.contains(&id) { // check if parent depends on the desired size of this, if so then redraw it first
self.invalid_sizes.insert(id); for axis in [Axis::X, Axis::Y] {
} if let Some(&(outer, old)) = self.cache.size.axis_dyn(axis).get(&id)
// A widget can only answer whether its size changed by drawing in the && let Some(current) = self.active.get(&id)
// box its parent chose. If that box changed in pixels, its retained && let Some(pid) = current.parent
// placement is stale and the highest size reader must choose the new
// box first. Otherwise the widget can draw locally, and its readers
// only matter if the returned size actually changed.
let box_changed = self.active.get(&id).is_some_and(|active| {
let px = self.px_of(active.parent_move, active.region);
AXES.into_iter()
.any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis)))
});
if (self.resized.iter().any(|&resized| resized) || box_changed)
&& let Some(top) = self.mark_readers(id, rsc)
{ {
#[cfg(feature = "layout-diagnostics")] self.cache.size.axis_dyn(axis).remove(&id);
diag::bump(Counter::EagerReaderRedraws); let new = self.size_ctx(id, outer, rsc).len_axis(id, axis);
self.redraw(top, rsc); self.cache.size.axis_dyn(axis).insert(id, (outer, new));
rsc.widgets_mut().needs_redraw.remove(&id); if new != old {
return; self.redraw(pid, rsc);
}
}
} }
rsc.widgets_mut().needs_redraw.remove(&id);
if self.draw_started.contains(&id) { if self.draw_started.contains(&id) {
return; return;
@@ -691,58 +289,34 @@ 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.parent_move,
active.move_idx != active.parent_move,
active.mask, active.mask,
Some(active.children), 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 furthest ancestor that read this widget's size, directly or through pub(super) fn size_ctx<'b>(
/// widgets that did the same, marking everything below it on the way. &'b mut self,
fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> { source: WidgetId,
let mut top = None; outer: UiVec2,
let mut at = id; rsc: &'b mut dyn UiRsc,
while let Some(active) = self.active.get(&at) ) -> SizeCtx<'b> {
&& let Some(parent) = active.parent let ui = rsc.ui_mut();
&& self SizeCtx {
.active source,
.get(&parent) cache: &mut self.cache,
.is_some_and(|p| p.size_deps.contains(&at)) text: &mut ui.text,
{ widgets: &ui.widgets,
rsc.widgets_mut().needs_redraw.insert(at); outer,
top = Some(parent); output_size: self.output_size,
at = parent; id: source,
} }
top
} }
} }
+89
View File
@@ -0,0 +1,89 @@
use crate::{
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2,
WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
};
pub struct SizeCtx<'a> {
pub text: &'a mut TextData,
pub(super) source: WidgetId,
pub(super) widgets: &'a Widgets,
pub(super) cache: &'a mut Cache,
/// TODO: should this be pub? rn used for sized
pub outer: UiVec2,
pub(super) output_size: Vec2,
pub(super) id: WidgetId,
}
impl SizeCtx<'_> {
pub fn id(&self) -> &WidgetId {
&self.id
}
pub fn source(&self) -> &WidgetId {
&self.source
}
pub(super) fn len_inner<A: const AxisT>(&mut self, id: WidgetId) -> Len {
if let Some((_, len)) = self.cache.size.axis::<A>().get(&id) {
return *len;
}
let len = self
.widgets
.get_dyn_dynamic(id)
.desired_len::<A>(&mut SizeCtx {
text: self.text,
source: self.source,
widgets: self.widgets,
cache: self.cache,
outer: self.outer,
output_size: self.output_size,
id,
});
self.cache.size.axis::<A>().insert(id, (self.outer, len));
len
}
pub fn width(&mut self, id: impl IdLike) -> Len {
self.len_inner::<XAxis>(id.id())
}
pub fn height(&mut self, id: impl IdLike) -> Len {
self.len_inner::<YAxis>(id.id())
}
pub fn len_axis(&mut self, id: impl IdLike, axis: Axis) -> Len {
match axis {
Axis::X => self.width(id),
Axis::Y => self.height(id),
}
}
pub fn size(&mut self, id: impl IdLike) -> Size {
let id = id.id();
Size {
x: self.width(id),
y: self.height(id),
}
}
pub fn px_size(&mut self) -> Vec2 {
self.outer.to_abs(self.output_size)
}
pub fn output_size(&mut self) -> Vec2 {
self.output_size
}
pub fn draw_text(
&mut self,
buffer: &mut TextBuffer,
attrs: &TextAttrs,
width: Option<f32>,
) -> RenderedText {
self.text.render(buffer, attrs, width)
}
pub fn label(&self, id: WidgetId) -> &String {
self.widgets.label(id)
}
}
-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> {
+22 -1
View File
@@ -1,13 +1,34 @@
use std::ops::*;
pub const trait LerpUtil { pub const trait LerpUtil {
fn lerp(self, from: Self, to: Self) -> Self; fn lerp(self, from: Self, to: Self) -> Self;
fn lerp_inv(self, from: Self, to: Self) -> Self;
} }
const impl LerpUtil for f32 { pub const trait DivOr {
fn div_or(self, rhs: Self, other: Self) -> Self;
}
const impl DivOr for f32 {
fn div_or(self, rhs: Self, other: Self) -> Self {
let res = self / rhs;
if res.is_nan() { other } else { res }
}
}
const impl<
T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy,
> LerpUtil for T
{
/// linear interpolation /// linear interpolation
/// from * (1.0 - self) + to * self /// from * (1.0 - self) + to * self
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
fn lerp_inv(self, from: Self, to: Self) -> Self {
(self - from).div_or(to - from, 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) }
+11 -6
View File
@@ -1,11 +1,7 @@
use crate::util::impl_op; use crate::util::{DivOr, 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,
@@ -71,6 +67,15 @@ impl_op!(Vec2 Sub sub; x y);
impl_op!(Vec2 Mul mul; x y); impl_op!(Vec2 Mul mul; x y);
impl_op!(Vec2 Div div; x y); impl_op!(Vec2 Div div; x y);
const impl DivOr for Vec2 {
fn div_or(self, rhs: Self, other: Self) -> Self {
Self {
x: self.x.div_or(rhs.x, other.x),
y: self.y.div_or(rhs.y, other.y),
}
}
}
impl Neg for Vec2 { impl Neg for Vec2 {
type Output = Self; type Output = Self;
+19 -32
View File
@@ -1,4 +1,4 @@
use crate::{Axis, Len, Painter, Size}; use crate::{Axis, AxisT, Len, Painter, SizeCtx};
use std::any::Any; use std::any::Any;
mod data; mod data;
@@ -15,45 +15,32 @@ pub use tag::*;
pub use view::*; pub use view::*;
pub use widgets::*; pub use widgets::*;
/// What may be done to a widget's drawing when the box it was given changes
/// on this axis, instead of drawing it again. Asked per axis, because wrapped
/// text reads the width it is offered and not the height.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OnResize {
Scale,
Translate,
#[default]
Redraw,
}
pub trait Widget: Any { pub trait Widget: Any {
/// Draws the widget, and returns what it used of the box it was given. fn draw(&mut self, painter: &mut Painter);
fn draw(&mut self, painter: &mut Painter) -> Size; fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len;
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
/// An exact length the widget can give without a painter or its children.
/// Optional, and saves a draw rather than changing one: a hint that
/// disagrees with the eventual draw fails a debug assertion.
fn size_hint(&self, _axis: Axis) -> Option<Len> {
None
} }
fn on_resize(&self, _axis: Axis) -> OnResize { pub trait WidgetAxisFns {
OnResize::default() fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len;
}
impl<W: Widget + ?Sized> WidgetAxisFns for W {
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len {
match A::get() {
Axis::X => self.desired_width(ctx),
Axis::Y => self.desired_height(ctx),
}
} }
} }
impl Widget for () { impl Widget for () {
/// A gap: nothing drawn, at the default length, so a span gives it a share. fn draw(&mut self, _: &mut Painter) {}
fn draw(&mut self, _: &mut Painter) -> Size { fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Size::default() Len::ZERO
} }
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
fn size_hint(&self, _axis: Axis) -> Option<Len> { Len::ZERO
Some(Len::default())
}
fn on_resize(&self, _axis: Axis) -> OnResize {
OnResize::Scale
} }
} }
-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 }
}
}
-63
View File
@@ -1,63 +0,0 @@
//! Text sizing: wrapped text reads the width it is offered, fixed text does
//! not, and both report a height their container lays out around.
use iris::prelude::*;
fn main() {
DefaultApp::<State>::run();
}
#[derive(DefaultUiState)]
struct State {
ui_state: DefaultUiState,
}
const SAMPLE: &str = "Wrapping shapes one source into as many lines as its container \
leaves room for, so the height of a paragraph is an answer rather than a setting, and \
the same words in a narrower box come back taller. Resize the window and watch the \
text below reflow into a different number of lines while nothing about it changes.";
impl DefaultAppState for State {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
let panel = || rect(Color::WHITE.darker(0.85));
let wrapped = wtext(SAMPLE)
.size(28)
.wrap(true)
.text_align(Align::LEFT)
.pad(16)
.background(panel());
let aligned = (
wtext("left").size(24).text_align(Align::LEFT),
wtext("centred").size(24).text_align(Align::CENTER),
wtext("right").size(24).text_align(Align::RIGHT),
)
.span(Dir::DOWN)
.gap(8)
.pad(16)
.background(panel());
// The same words in half the width, which is a different number of
// lines and so a different height. A declared width only holds along
// a span's own axis, hence the row.
let narrow = (
wtext(SAMPLE)
.size(20)
.wrap(true)
.pad(16)
.background(panel())
.align(Align::TOP)
.width(rel(0.5)),
rect(Color::WHITE.darker(0.95)),
)
.span(Dir::RIGHT);
(wrapped, aligned, narrow)
.span(Dir::DOWN)
.gap(12)
.pad(12)
.set_root(rsc, &mut ui_state);
Self { ui_state }
}
}
-16
View File
@@ -1,16 +0,0 @@
[package]
name = "rig-input"
version.workspace = true
edition.workspace = true
# Replays `.touch` recordings through Wayland's virtual-pointer protocol;
# headless sway has no input devices for coordinate-driving tools to move.
[[bin]]
name = "replay-touch"
path = "src/main.rs"
[dependencies]
# Share the harness parser so both ways of replaying read a file the same.
iris = { path = ".." }
wayland-client = { workspace = true }
wayland-protocols-wlr = { workspace = true }
-141
View File
@@ -1,141 +0,0 @@
use iris::harness::{TouchAction, TouchScript};
use std::time::Duration;
use wayland_client::protocol::wl_pointer::ButtonState;
use wayland_client::protocol::{wl_registry, wl_seat};
use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
use wayland_protocols_wlr::virtual_pointer::v1::client::{
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
};
const BTN_LEFT: u32 = 0x110;
const SETTLE: Duration = Duration::from_millis(200);
#[derive(Default)]
struct Globals {
seat: Option<wl_seat::WlSeat>,
manager: Option<ZwlrVirtualPointerManagerV1>,
}
impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
let wl_registry::Event::Global {
name,
interface,
version,
} = event
else {
return;
};
match interface.as_str() {
"wl_seat" => {
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
}
"zwlr_virtual_pointer_manager_v1" => {
state.manager = Some(registry.bind(name, version.min(2), qh, ()));
}
_ => {}
}
}
}
delegate_noop!(Globals: ignore wl_seat::WlSeat);
delegate_noop!(Globals: ZwlrVirtualPointerManagerV1);
delegate_noop!(Globals: ZwlrVirtualPointerV1);
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let [width, height, path] = args.as_slice() else {
eprintln!("usage: replay-touch WIDTH HEIGHT FILE");
std::process::exit(2);
};
let (width, height) = (parse(width, "WIDTH"), parse(height, "HEIGHT"));
let text = std::fs::read_to_string(path)
.unwrap_or_else(|e| fail(&format!("could not read {path}: {e}")));
let script = TouchScript::parse(&text).unwrap_or_else(|e| fail(&e));
let conn = Connection::connect_to_env().unwrap_or_else(|e| {
fail(&format!(
"no wayland display ({e}); is WAYLAND_DISPLAY set?"
))
});
let mut queue = conn.new_event_queue();
let qh = queue.handle();
let display = conn.display();
display.get_registry(&qh, ());
let mut globals = Globals::default();
queue
.roundtrip(&mut globals)
.unwrap_or_else(|e| fail(&format!("wayland roundtrip failed: {e}")));
let manager = globals.manager.as_ref().unwrap_or_else(|| {
fail(
"this compositor does not offer zwlr_virtual_pointer_manager_v1, so a pointer cannot \
be synthesised; sway and every wlroots compositor do",
)
});
let pointer = manager.create_virtual_pointer(globals.seat.as_ref(), &qh, ());
// Put the pointer where the gesture starts and let the compositor
// settle before anything is pressed. Without this the press is
// dropped: sway has just learned about this pointer, and a button
// sent in the same breath as the motion that first puts it over a
// window arrives before there is a focused surface to send it to --
// winit sees `CursorEntered`, the moves and the *release*, never the
// press, so the gesture reads as a hover and nothing scrolls. Found
// by printing winit's own events; the settle is what fixed it.
if let Some(first) = script.samples.first() {
pointer.motion_absolute(0, first.pos.x as u32, first.pos.y as u32, width, height);
pointer.frame();
conn.flush()
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
std::thread::sleep(SETTLE);
}
let mut previous = 0;
for sample in &script.samples {
std::thread::sleep(Duration::from_millis(sample.t_ms - previous));
previous = sample.t_ms;
let t = sample.t_ms as u32;
pointer.motion_absolute(t, sample.pos.x as u32, sample.pos.y as u32, width, height);
pointer.frame();
// The button goes in a frame of its own, *after* the motion has
// been committed. Sent in the same frame as the motion that
// first puts the pointer over the window, sway drops it: the
// client sees `CursorEntered` and the moves but never a
// `MouseInput { state: Pressed }`, so the whole gesture reads as
// a hover and nothing scrolls. Found exactly that way, by
// printing winit's events.
let state = match sample.action {
TouchAction::Down => Some(ButtonState::Pressed),
TouchAction::Up => Some(ButtonState::Released),
TouchAction::Move => None,
};
if let Some(state) = state {
pointer.button(t, BTN_LEFT, state);
pointer.frame();
}
conn.flush()
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
}
pointer.destroy();
conn.flush().ok();
}
fn parse(text: &str, what: &str) -> u32 {
text.parse()
.unwrap_or_else(|_| fail(&format!("{what} is not a whole number: {text:?}")))
}
fn fail(message: &str) -> ! {
eprintln!("replay-touch: {message}");
std::process::exit(1);
}
-14
View File
@@ -1,14 +0,0 @@
# The compositor `scripts/run-headless.sh` starts, so that an example has a
# surface where there is no display. Nothing here is meant to be looked at
# directly; `grim` is.
#
# No Xwayland: winit talks Wayland natively, so an X server is a second thing
# to go wrong for no gain.
xwayland disable
# The default output, overridden per run by `--mode`. Larger than the window
# an example opens, so nothing is scaled or clipped.
output HEADLESS-1 mode 1920x1200@60Hz
default_border none
focus_follows_mouse no
-182
View File
@@ -1,182 +0,0 @@
#!/bin/sh
# Run an iris example on a machine with no display.
#
# ./scripts/run-headless.sh tabs
# ./scripts/run-headless.sh tabs --shot /tmp/tabs.png --seconds 4
# ./scripts/run-headless.sh tabs --replay taps.touch --shot /tmp/tabs.png
# ./scripts/run-headless.sh app --dir ../elsewhere --mode 1080x2424@120Hz
#
# `--dir DIR` names the workspace to build in, defaulting to this one, so a
# project that depends on iris can be run through the same rig. `--bin` runs a
# crate binary rather than an example, and takes its own argv from
# `$RUN_HEADLESS_ARGS`, word-split on purpose.
#
# `--mode` sets the output, for running something at a size other than a
# desktop's -- a phone's, say. Set every run rather than only when it changes:
# the compositor is reused between runs, so a default-shaped run after a
# custom one would otherwise inherit the other's output and quietly screenshot
# 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-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
# `--shot` it also writes `<shot>-before.png` from just before the gesture,
# since "it moved" is a claim about two pictures.
#
# What it supplies is a compositor for winit to open a surface on: a headless
# sway, and `grim` to screenshot it. Sway gets its own socket and runtime
# directory rather than joining whatever else is running, because it tiles --
# adding a window to someone else's compositor resizes theirs.
set -eu
need() {
command -v "$1" >/dev/null 2>&1 || {
echo "run-headless: $1 is not installed ($2)" >&2
exit 127
}
}
need sway "the compositor an example opens its window on"
need swaymsg "sway's control socket"
scripts=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$scripts/.." && pwd)
workdir="$root"
cd "$root"
run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless"
seconds=3
shot=""
replay=""
resize=""
example=""
kind=example
mode=1920x1200@60Hz
while [ $# -gt 0 ]; do
case "$1" in
--shot) shot=$2; shift 2 ;;
--seconds) seconds=$2; shift 2 ;;
--bin) kind=bin; shift ;;
--mode) mode=$2; shift 2 ;;
--resize) resize=$2; shift 2 ;;
--replay) replay=$2; shift 2 ;;
--dir) workdir=$(cd "$2" && pwd); shift 2 ;;
--) shift; break ;;
*) example=$1; shift ;;
esac
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; }
[ -z "$replay" ] || [ -f "$replay" ] || { echo "run-headless: no touch script at $replay" >&2; exit 2; }
[ -z "$shot" ] || need grim "the screenshot --shot writes"
mkdir -p "$run"
export SWAYSOCK="$run/sway.sock"
# Named rather than left to sway's pid-based default, so a second run reuses
# this compositor instead of starting another beside it.
if ! swaymsg -t get_version >/dev/null 2>&1; then
rm -f "$SWAYSOCK"
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 LIBSEAT_BACKEND=noop \
setsid sway -c "$scripts/headless.conf" >"$run/sway.log" 2>&1 &
i=0
while [ $i -lt 20 ]; do
swaymsg -t get_version >/dev/null 2>&1 && break
i=$((i + 1)); sleep 0.5
done
swaymsg -t get_version >/dev/null 2>&1 || {
echo "run-headless: compositor did not start; see $run/sway.log" >&2
exit 1
}
fi
rm -f "$run/display"
swaymsg exec -- "sh -c 'printf %s \"\$WAYLAND_DISPLAY\" > $run/display'" >/dev/null
i=0
while [ $i -lt 20 ]; do
[ -s "$run/display" ] && break
i=$((i + 1)); sleep 0.5
done
[ -s "$run/display" ] || { echo "run-headless: could not read WAYLAND_DISPLAY" >&2; exit 1; }
WAYLAND_DISPLAY=$(cat "$run/display")
export WAYLAND_DISPLAY
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
swaymsg output HEADLESS-1 mode "$mode" >/dev/null
# The extent `replay-touch` positions against, so a script's coordinates
# are the output's own pixels.
out_w=${mode%x*}
out_h=${mode#*x}; out_h=${out_h%@*}
# Built before the app starts, so a compile error is not reported as a
# window that failed to move.
[ -z "$replay" ] || (cd "$root" && cargo build --bin replay-touch -p rig-input) >&2
cd "$workdir"
if [ "$kind" = bin ]; then
cargo build --bin "$example" "$@" >&2
bin="$workdir/target/debug/$example"
else
cargo build --example "$example" "$@" >&2
bin="$workdir/target/debug/examples/$example"
fi
# Deliberately word-split: this is the binary's own argv, not a single path.
# shellcheck disable=SC2086
"$bin" ${RUN_HEADLESS_ARGS:-} >"$run/$example.log" 2>&1 &
pid=$!
trap 'kill "$pid" 2>/dev/null || true' EXIT INT TERM
# Wait for the window to be mapped rather than for a number of seconds. A
# fixed sleep took an all-black screenshot the first time this ran, when sway
# had started in the same invocation and had not composited its output yet --
# which is indistinguishable from an app that draws nothing.
i=0
while [ $i -lt 40 ]; do
kill -0 "$pid" 2>/dev/null || break
swaymsg -t get_tree --raw 2>/dev/null | grep -q "\"pid\":$pid," && break
i=$((i + 1)); sleep 0.25
done
i=0
while [ $i -lt "$((seconds * 2))" ]; do
kill -0 "$pid" 2>/dev/null || break
i=$((i + 1)); sleep 0.5
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 "$shot" ]; then
grim "${shot%.png}-before.png"
echo "run-headless: wrote ${shot%.png}-before.png (before the gesture)" >&2
fi
"$root/target/debug/replay-touch" "$out_w" "$out_h" "$replay"
# A fling outlives the finger: the gesture's own last sample is not
# when the list stops. Long enough for Android's spline to settle
# (`FlingCalculator::duration` tops out around a second and a half).
sleep 2
fi
if kill -0 "$pid" 2>/dev/null; then
[ -n "$shot" ] && grim "$shot" && echo "run-headless: wrote $shot" >&2
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
status=0
else
wait "$pid" 2>/dev/null || status=$?
echo "run-headless: $example exited early (status ${status:-0})" >&2
status=${status:-1}
fi
echo "--- $example output ---" >&2
cat "$run/$example.log" >&2
exit "$status"
+2 -1
View File
@@ -235,7 +235,8 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
ui_state.focus = None; ui_state.focus = None;
} }
if input_changed { if input_changed {
render.run_sensors(rsc, state, cursor_state); let window_size = ui_state.window_size();
render.run_sensors(rsc, state, cursor_state, window_size);
} }
let ui_state = state.default_state_mut(); let ui_state = state.default_state_mut();
if old != ui_state.focus if old != ui_state.focus
+31 -99
View File
@@ -4,14 +4,14 @@ use std::{
rc::Rc, rc::Rc,
}; };
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
pub enum CursorButton { pub enum CursorButton {
Left, Left,
Right, Right,
Middle, Middle,
} }
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
pub enum CursorSense { pub enum CursorSense {
PressStart(CursorButton), PressStart(CursorButton),
Pressing(CursorButton), Pressing(CursorButton),
@@ -27,7 +27,7 @@ pub struct CursorSenses(Vec<CursorSense>);
impl Event for CursorSenses { impl Event for CursorSenses {
type Data<'a> = CursorData<'a>; type Data<'a> = CursorData<'a>;
type Global = Hovered; type State = SensorState;
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> { fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
if let Some(sense) = should_run(self, &data.cursor, data.hover) { if let Some(sense) = should_run(self, &data.cursor, data.hover) {
let mut data = data.clone(); let mut data = data.clone();
@@ -37,24 +37,6 @@ impl Event for CursorSenses {
None None
} }
} }
/// A press or a scroll is used up by whatever answered it, so it stops
/// there. Hovering is not: a cursor resting somewhere goes on resting.
fn consumes(&self, data: &Self::Data<'_>) -> bool {
!data.sense.position_only()
}
}
/// Who the cursor was inside, before and after an input. The difference is
/// whose hover has ended -- including a widget a higher layer has covered,
/// which the walk stops before reaching.
///
/// Two buffers that swap rather than one rebuilt, so an input allocates
/// nothing once they have grown.
#[derive(Default)]
pub struct Hovered {
was: Vec<WidgetId>,
now: Vec<WidgetId>,
} }
impl CursorSense { impl CursorSense {
@@ -70,12 +52,6 @@ impl CursorSense {
pub fn is_dragging(&self) -> bool { pub fn is_dragging(&self) -> bool {
matches!(self, CursorSense::Pressing(CursorButton::Left)) matches!(self, CursorSense::Pressing(CursorButton::Left))
} }
/// False if the sense is a button or a scroll, true if it is only about
/// where the cursor is.
fn position_only(&self) -> bool {
matches!(self, Self::HoverStart | Self::Hovering | Self::HoverEnd)
}
} }
#[derive(Default, Clone)] #[derive(Default, Clone)]
@@ -120,12 +96,6 @@ impl CursorButtons {
} }
impl CursorState { impl CursorState {
/// True if the cursor is only reporting where it is: no button and no
/// scroll this frame.
pub fn position_only(&self) -> bool {
self.scroll_delta == Vec2::ZERO && self.buttons.iter().all(|(_, state)| state.is_off())
}
pub fn end_frame(&mut self) { pub fn end_frame(&mut self) {
self.buttons.end_frame(); self.buttons.end_frame();
self.scroll_delta = Vec2::ZERO; self.scroll_delta = Vec2::ZERO;
@@ -153,6 +123,11 @@ pub struct Sensor<Ctx: HasEvents, Data> {
pub type SenseShape = UiRegion; pub type SenseShape = UiRegion;
#[derive(Default, Debug)]
pub struct SensorState {
pub hover: ActivationState,
}
#[derive(Clone)] #[derive(Clone)]
pub struct CursorData<'a> { pub struct CursorData<'a> {
/// where this widget was hit /// where this widget was hit
@@ -172,6 +147,7 @@ pub trait SensorUi {
rsc: &mut Rsc, rsc: &mut Rsc,
state: &mut Rsc::State, state: &mut Rsc::State,
cursor: CursorState, cursor: CursorState,
window_size: Vec2,
); );
} }
@@ -181,85 +157,46 @@ impl SensorUi for UiRenderState {
rsc: &mut Rsc, rsc: &mut Rsc,
state: &mut Rsc::State, state: &mut Rsc::State,
cursor: CursorState, cursor: CursorState,
window_size: Vec2,
) { ) {
// in order to remove this take, need to store active list in UiRenderState somehow // in order to remove this take, need to store active list in UiRenderState somehow
// this would probably be done through a generic parameter that adds yet another rsc / // this would probably be done through a generic parameter that adds yet another rsc /
// state like thing, but local to render state, and is passed to UiRsc events so you can // state like thing, but local to render state, and is passed to UiRsc events so you can
// update it there? // update it there?
let active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active); let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
let mut hovered = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().global);
hovered.now.clear();
let position_only = cursor.position_only();
let region_of = |id| self.window_region(&id);
for layer in self.layers.indices().rev() { for layer in self.layers.indices().rev() {
let mut consumed = false; let mut sensed = false;
for id in active.get(&layer).into_flat_iter().map(|(id, _)| *id) { for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
let Some(region) = region_of(id) else { let shape = self.active.get(id).unwrap().region;
continue; let region = shape.to_px(window_size);
}; let in_shape = cursor.exists && region.contains(cursor.pos);
if !cursor.exists || !region.contains(cursor.pos) { sensor.hover.update(in_shape);
if sensor.hover == ActivationState::Off {
continue; continue;
} }
hovered.now.push(id); sensed = true;
let hover = match hovered.was.contains(&id) {
true => ActivationState::On,
false => ActivationState::Start,
};
// A press or a scroll stops where something answered it, so a
// button over a list does not swallow the list's scrolling.
consumed |= deliver(self, rsc, state, id, hover, &cursor, region);
// A cursor doing neither stops at whatever it is over, so
// hovering does not reach through.
consumed |= position_only;
}
// Applied after the layer, never during it: senses on one layer do
// not block each other.
if consumed {
break;
}
}
// Whatever the cursor was inside and is not now, whether it left or a let cursor = cursor.clone();
// layer above took the input before the walk reached it. A widget that
// stopped being drawn has no region to report and is simply dropped.
for &id in &hovered.was {
if !hovered.now.contains(&id)
&& let Some(region) = region_of(id)
{
deliver(self, rsc, state, id, ActivationState::End, &cursor, region);
}
}
std::mem::swap(&mut hovered.was, &mut hovered.now);
let senses = rsc.events_mut().get_type::<CursorSense>();
senses.active = active;
senses.global = hovered;
}
}
/// Runs one widget's cursor senses, and says whether they used up the input.
fn deliver<Rsc: HasEvents>(
render: &UiRenderState,
rsc: &mut Rsc,
state: &mut Rsc::State,
id: WidgetId,
hover: ActivationState,
cursor: &CursorState,
region: PixelRegion,
) -> bool {
let data = CursorData { let data = CursorData {
pos: cursor.pos - region.top_left, pos: cursor.pos - region.top_left,
size: region.bot_right - region.top_left, size: region.bot_right - region.top_left,
scroll_delta: cursor.scroll_delta, scroll_delta: cursor.scroll_delta,
hover, hover: sensor.hover,
cursor: cursor.clone(), cursor,
// this does not have any meaning; // this does not have any meaning;
// might wanna set up Event to have a prepare stage // might wanna set up Event to have a prepare stage
sense: CursorSense::Hovering, sense: CursorSense::Hovering,
render, render: self,
}; };
rsc.run_event::<CursorSense>(id, data, state) rsc.run_event::<CursorSense>(*id, data, state);
}
if sensed {
break;
}
}
rsc.events_mut().get_type::<CursorSense>().active = active;
}
} }
pub fn should_run( pub fn should_run(
@@ -268,11 +205,6 @@ pub fn should_run(
hover: ActivationState, hover: ActivationState,
) -> Option<CursorSense> { ) -> Option<CursorSense> {
for sense in senses.iter() { for sense in senses.iter() {
// A widget the cursor is no longer inside senses only its position:
// the press that ended its hover landed on something else.
if !hover.is_on() && !sense.position_only() {
continue;
}
if match sense { if match sense {
CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(), CursorSense::PressStart(button) => cursor.buttons.select(button).is_start(),
CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(), CursorSense::Pressing(button) => cursor.buttons.select(button).is_on(),
+2 -141
View File
@@ -37,81 +37,6 @@ macro_rules! assert_corners {
} }
pub use crate::assert_corners; pub use crate::assert_corners;
/// One replayed pointer sample, cut down to what a window delivers: where the
/// pointer is, and whether the button changed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TouchAction {
Down,
Move,
Up,
}
impl TouchAction {
fn parse(word: &str) -> Option<Self> {
match word {
"down" => Some(Self::Down),
"move" => Some(Self::Move),
"up" => Some(Self::Up),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct TouchSample {
pub t_ms: u64,
pub action: TouchAction,
pub pos: Vec2,
}
/// A recorded gesture, in the output's own pixels: `<ms> down|move|up <x> <y>`
/// a line, `#` to end of line ignored.
///
/// One parser for both ways of replaying a recording -- into a harness, and
/// into a real window -- so the two cannot read the same file differently.
pub struct TouchScript {
pub samples: Vec<TouchSample>,
}
impl TouchScript {
pub fn parse(text: &str) -> Result<Self, String> {
let mut samples: Vec<TouchSample> = Vec::new();
for (i, line) in text.lines().enumerate() {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let at = |what: &str| format!("touch script line {}: {what}: {line:?}", i + 1);
let mut words = line.split_whitespace();
let (Some(t), Some(action), Some(x), Some(y), None) = (
words.next(),
words.next(),
words.next(),
words.next(),
words.next(),
) else {
return Err(at("expected `t_ms action x y`"));
};
let t_ms: u64 = t.parse().map_err(|_| at("t_ms is not a whole number"))?;
let action =
TouchAction::parse(action).ok_or_else(|| at("action is not down/move/up"))?;
let x: f32 = x.parse().map_err(|_| at("x is not a number"))?;
let y: f32 = y.parse().map_err(|_| at("y is not a number"))?;
if let Some(last) = samples.last()
&& t_ms < last.t_ms
{
return Err(at("samples must be in time order"));
}
samples.push(TouchSample {
t_ms,
action,
pos: Vec2::new(x, y),
});
}
Ok(Self { samples })
}
}
#[derive(Default)] #[derive(Default)]
pub struct HarnessState { pub struct HarnessState {
pub root: Option<StrongWidget>, pub root: Option<StrongWidget>,
@@ -235,23 +160,6 @@ impl Harness {
self.release(CursorButton::Left); self.release(CursorButton::Left);
} }
/// Drives a recorded gesture through the harness.
pub fn replay(&mut self, script: &TouchScript) {
for sample in &script.samples {
match sample.action {
TouchAction::Down => {
self.move_to(sample.pos);
self.press(CursorButton::Left);
}
TouchAction::Move => self.move_to(sample.pos),
TouchAction::Up => {
self.move_to(sample.pos);
self.release(CursorButton::Left);
}
}
}
}
fn button(&mut self, button: CursorButton) -> &mut ActivationState { fn button(&mut self, button: CursorButton) -> &mut ActivationState {
let buttons = &mut self.cursor.buttons; let buttons = &mut self.cursor.buttons;
match button { match button {
@@ -265,56 +173,9 @@ impl Harness {
/// window delivers input against too. /// window delivers input against too.
fn sense(&mut self) { fn sense(&mut self) {
let cursor = self.cursor.clone(); let cursor = self.cursor.clone();
let size = self.render.output_size();
self.render self.render
.run_sensors(&mut self.rsc, &mut self.state, cursor); .run_sensors(&mut self.rsc, &mut self.state, cursor, size);
self.cursor.end_frame(); self.cursor.end_frame();
} }
} }
#[cfg(test)]
mod tests {
use super::*;
fn parse(text: &str) -> Result<Vec<(u64, TouchAction, f32, f32)>, String> {
Ok(TouchScript::parse(text)?
.samples
.iter()
.map(|s| (s.t_ms, s.action, s.pos.x, s.pos.y))
.collect())
}
#[test]
fn a_recording_is_time_action_and_a_point() {
assert_eq!(
parse("0 down 10 20\n16 move 10.5 24\n32 up 10.5 24").unwrap(),
[
(0, TouchAction::Down, 10.0, 20.0),
(16, TouchAction::Move, 10.5, 24.0),
(32, TouchAction::Up, 10.5, 24.0),
]
);
}
#[test]
fn blank_lines_and_comments_are_not_samples() {
assert_eq!(
parse("# a flick\n\n 0 down 1 2 # the finger lands\n\n").unwrap(),
[(0, TouchAction::Down, 1.0, 2.0)]
);
}
#[test]
fn a_recording_that_goes_backwards_is_rejected() {
// Replay waits out the gap between samples, so time running backwards
// would silently become no wait at all.
let err = parse("16 down 1 2\n0 up 1 2").unwrap_err();
assert!(err.contains("time order"), "{err}");
}
#[test]
fn a_line_that_is_not_a_sample_says_which_line() {
let err = parse("0 down 1 2\n16 wiggle 1 2").unwrap_err();
assert!(err.contains("line 2"), "{err}");
assert!(err.contains("down/move/up"), "{err}");
}
}
-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;
-271
View File
@@ -1,271 +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>,
}
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::abs(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 == 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)
}
}
+5 -6
View File
@@ -6,17 +6,16 @@ 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) {
painter.primitive(&self.handle); painter.primitive(&self.handle);
Size::abs(self.handle.size())
} }
fn size_hint(&self, axis: Axis) -> Option<Len> { fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Some(Len::abs(self.handle.size().axis(axis))) Len::abs(self.handle.size().x)
} }
fn on_resize(&self, _: Axis) -> OnResize { fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
OnResize::Scale Len::abs(self.handle.size().y)
} }
} }
+8 -5
View File
@@ -5,13 +5,16 @@ pub struct Masked {
} }
impl Widget for Masked { impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
painter.set_mask(painter.region()); painter.set_mask(painter.region());
painter.widget(&self.inner).size() painter.widget(&self.inner);
} }
/// It clips to the box it was given, not to the part its child used. fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
fn on_resize(&self, _: Axis) -> OnResize { ctx.width(&self.inner)
OnResize::Redraw }
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
+20 -27
View File
@@ -6,37 +6,30 @@ 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) {
let known = match self.align.tuple() {
(Some(_), Some(_)) => painter
.known_len(&self.inner, Axis::X, UiRegion::FULL)
.zip(painter.known_len(&self.inner, Axis::Y, UiRegion::FULL))
.map(|(x, y)| Size { x, y }),
(Some(_), None) => painter
.known_len(&self.inner, Axis::X, UiRegion::FULL)
.map(|x| Size { x, y: Len::REST }),
(None, Some(_)) => painter
.known_len(&self.inner, Axis::Y, UiRegion::FULL)
.map(|y| Size { x: Len::REST, y }),
(None, None) => Some(Size::REST),
};
// Drawn where it may be too big only when the aligned axes are not
// already known, then given its aligned box once its size is known.
let had_size = known.is_some();
let size = known.unwrap_or_else(|| painter.place(&self.inner, UiRegion::FULL).size());
let region = match self.align.tuple() { let region = match self.align.tuple() {
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }), (Some(x), Some(y)) => painter
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL), .size(&self.inner)
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)), .to_uivec2()
.align(RegionAlign { x, y }),
(Some(x), None) => {
let x = painter.size_ctx().width(&self.inner).apply_rest().align(x);
UiRegion::new(x, UiSpan::FULL)
}
(None, Some(y)) => {
let y = painter.size_ctx().height(&self.inner).apply_rest().align(y);
UiRegion::new(UiSpan::FULL, 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 }
} }
/// The aligned box is a fraction of its own, so the child keeps its fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
/// length and stays against the edge it was aligned to. ctx.width(&self.inner)
fn on_resize(&self, _: Axis) -> OnResize { }
OnResize::Scale
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
+8 -4
View File
@@ -6,14 +6,18 @@ pub struct LayerOffset {
} }
impl Widget for LayerOffset { impl Widget for LayerOffset {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
for _ in 0..self.offset { for _ in 0..self.offset {
painter.next_layer(); painter.next_layer();
} }
painter.widget(&self.inner).size() painter.widget(&self.inner);
} }
fn on_resize(&self, _: Axis) -> OnResize { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
OnResize::Scale ctx.width(&self.inner)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
+34 -11
View File
@@ -6,20 +6,43 @@ pub struct MaxSize {
pub y: Option<Len>, pub y: Option<Len>,
} }
impl Widget for MaxSize { impl MaxSize {
fn draw(&mut self, painter: &mut Painter) -> Size { fn apply_to_outer(&self, ctx: &mut SizeCtx) {
let child = painter.widget(&self.inner).size(); if let Some(x) = self.x {
let output = painter.output_size(); ctx.outer.x.select_len(x.apply_rest());
Size { }
x: capped(child.x, self.x, output.x), if let Some(y) = self.y {
y: capped(child.y, self.y, output.y), ctx.outer.y.select_len(y.apply_rest());
} }
} }
} }
fn capped(len: Len, max: Option<Len>, output: f32) -> Len { impl Widget for MaxSize {
match max { fn draw(&mut self, painter: &mut Painter) {
Some(max) if len.apply_rest().to_abs(output) > max.apply_rest().to_abs(output) => max, painter.widget(&self.inner);
_ => len, }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.apply_to_outer(ctx);
let width = ctx.width(&self.inner);
if let Some(x) = self.x {
let width_px = width.apply_rest().to_abs(ctx.output_size().x);
let x_px = x.apply_rest().to_abs(ctx.output_size().x);
if width_px > x_px { x } else { width }
} else {
width
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.apply_to_outer(ctx);
let height = ctx.height(&self.inner);
if let Some(y) = self.y {
let height_px = height.apply_rest().to_abs(ctx.output_size().y);
let y_px = y.apply_rest().to_abs(ctx.output_size().y);
if height_px > y_px { y } else { height }
} else {
height
}
} }
} }
+8 -4
View File
@@ -6,12 +6,16 @@ pub struct Offset {
} }
impl Widget for Offset { impl Widget for Offset {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
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);
} }
fn on_resize(&self, _: Axis) -> OnResize { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
OnResize::Scale ctx.width(&self.inner)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
+20 -18
View File
@@ -6,26 +6,28 @@ pub struct Pad {
} }
impl Widget for Pad { impl Widget for Pad {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let inner = painter painter.widget_within(&self.inner, self.padding.region());
.widget_within(&self.inner, self.padding.region())
.size();
Size {
x: Len {
abs: inner.x.abs + self.padding.left + self.padding.right,
..inner.x
},
y: Len {
abs: inner.y.abs + self.padding.top + self.padding.bottom,
..inner.y
},
}
} }
/// The padding is an offset from each edge, so a longer box pads the same fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
/// amount and the child takes the rest. let width = self.padding.left + self.padding.right;
fn on_resize(&self, _: Axis) -> OnResize { let height = self.padding.top + self.padding.bottom;
OnResize::Scale ctx.outer.x.abs -= width;
ctx.outer.y.abs -= height;
let mut size = ctx.width(&self.inner);
size.abs += width;
size
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
let width = self.padding.left + self.padding.right;
let height = self.padding.top + self.padding.bottom;
ctx.outer.x.abs -= width;
ctx.outer.y.abs -= height;
let mut size = ctx.height(&self.inner);
size.abs += height;
size
} }
} }
+14 -12
View File
@@ -10,16 +10,11 @@ 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) {
let output_len = painter.output_len(self.axis); let output_len = painter.output_size().axis(self.axis);
let container_len = UiScalar::abs(painter.px_len(self.axis)); let container_len = painter.region().axis(self.axis).len();
// Draw in the whole container only when its scrolling-axis length is let content_len = painter
// not already known, then place it at the scrolled offset. .len_axis(&self.inner, self.axis)
let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL);
let measured = known_len.is_none();
let child = measured.then(|| painter.place(&self.inner, UiRegion::FULL).size());
let content_len = known_len
.unwrap_or_else(|| child.unwrap().axis(self.axis))
.apply_rest() .apply_rest()
.within_len(container_len) .within_len(container_len)
.to_abs(output_len); .to_abs(output_len);
@@ -33,8 +28,15 @@ 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) }
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.width(&self.inner)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.inner)
} }
} }
+19 -15
View File
@@ -6,25 +6,29 @@ pub struct SetSize {
pub y: Option<Len>, pub y: Option<Len>,
} }
impl SetSize {
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
if let Some(x) = self.x {
ctx.outer.x.select_len(x.apply_rest());
}
if let Some(y) = self.y {
ctx.outer.y.select_len(y.apply_rest());
}
}
}
impl Widget for SetSize { impl Widget for SetSize {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let child = painter.widget(&self.inner).size(); painter.widget(&self.inner);
Size {
x: self.x.unwrap_or(child.x),
y: self.y.unwrap_or(child.y),
}
} }
/// A declared axis is known without looking at the child, which is what fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
/// lets a span lay out around `.height(rest(1))` without drawing it. self.apply_to_outer(ctx);
fn size_hint(&self, axis: Axis) -> Option<Len> { self.x.unwrap_or_else(|| ctx.width(&self.inner))
match axis {
Axis::X => self.x,
Axis::Y => self.y,
}
} }
fn on_resize(&self, _: Axis) -> OnResize { fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
OnResize::Scale self.apply_to_outer(ctx);
self.y.unwrap_or_else(|| ctx.height(&self.inner))
} }
} }
+100 -45
View File
@@ -8,35 +8,13 @@ pub struct Span {
} }
impl Widget for Span { impl Widget for Span {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let axis = self.dir.axis; let total = self.len_sum(&mut painter.size_ctx());
// 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.
let mut cursor = UiScalar::rel_min();
let mut lens = Vec::with_capacity(self.children.len());
for child in &self.children {
let mut span = UiSpan::new(cursor, UiScalar::rel_max());
if self.dir.sign == Sign::Neg {
span.flip();
}
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
let len = match painter.known_len(child, axis, region) {
Some(len) => len,
None => painter.place(child, region).len(axis),
};
cursor.abs += len.abs + self.gap;
cursor.rel += len.rel;
lens.push(len);
}
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
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; for child in &self.children {
for (child, len) in self.children.iter().zip(&lens) {
let mut span = UiSpan::FULL; let mut span = UiSpan::FULL;
span.start = start; span.start = start;
let len = painter.len_axis(child, self.dir.axis);
if len.rest > 0.0 { if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs); 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);
@@ -46,31 +24,27 @@ impl Widget for Span {
start.abs += len.abs; 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 child_region = UiRegion::from_axis(self.dir.axis, span, UiSpan::FULL);
if self.dir.sign == Sign::Neg { if self.dir.sign == Sign::Neg {
region.flip(axis); child_region.flip(self.dir.axis);
}
let used = painter.place(child, region).size().axis(!axis);
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
if used.rel > 0.0 || used.rest > 0.0 {
ortho = Len::REST;
} else if ortho.rest == 0.0 {
ortho.abs = ortho.abs.max(used.abs);
} }
painter.widget_within(child, child_region);
start.abs += self.gap; start.abs += self.gap;
} }
let along = match total.rest == 0.0 && total.rel == 0.0 {
true => total,
false => Len::default(),
};
Size::from_axis(axis, along, ortho)
} }
/// Every child is placed in fractions and offsets of the span's own box, fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
/// so a longer box holds the same layout and the children follow it. match self.dir.axis {
fn on_resize(&self, _: Axis) -> OnResize { Axis::X => self.desired_len(ctx),
OnResize::Scale Axis::Y => self.desired_ortho(ctx),
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
match self.dir.axis {
Axis::X => self.desired_ortho(ctx),
Axis::Y => self.desired_len(ctx),
}
} }
} }
@@ -95,6 +69,87 @@ impl Span {
pub fn pop(&mut self) -> Option<StrongWidget> { pub fn pop(&mut self) -> Option<StrongWidget> {
self.children.pop() self.children.pop()
} }
fn len_sum(&mut self, ctx: &mut SizeCtx) -> Len {
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
self.children.iter().fold(Len::abs(gap), |mut s, id| {
// it's tempting to subtract the abs & rel from the ctx outer,
// but that would create inconsistent sizing if you put
// a rest first vs last & only speed up in one direction.
// I think this is only solvable by restricting how you can
// compute size, bc currently you need child to define parent's
// sectioning and you need parent's sectioning to define child.
// Fortunately, that doesn't matter in most cases
let len = ctx.len_axis(id, self.dir.axis);
s += len;
s
})
}
fn desired_len(&mut self, ctx: &mut SizeCtx) -> Len {
let len = self.len_sum(ctx);
if len.rest == 0.0 && len.rel == 0.0 {
len
} else {
Len::default()
}
}
fn desired_ortho(&mut self, ctx: &mut SizeCtx) -> Len {
// this is a weird hack to get text wrapping to work properly when in a downward span
// the correct solution here is to add a function to widget that lets them
// request that ctx.outer has an axis "resolved" before checking the other,
// and panicking or warning if two request opposite axis (unsolvable in that case)
let outer = ctx.outer.axis(self.dir.axis);
if self.dir.axis == Axis::X {
// so....... this literally copies draw so that the lengths are correctly set in the
// context, which makes this slow and not cool
let total = self.len_sum(ctx);
let mut start = UiScalar::rel_min();
let mut ortho_len = Len::ZERO;
for child in &self.children {
let mut span = UiSpan::FULL;
span.start = start;
let len = ctx.len_axis(child, self.dir.axis);
if len.rest > 0.0 {
let offset = UiScalar::new(total.rel, total.abs);
let rel_end = UiScalar::rel(len.rest / total.rest);
let end = (UiScalar::rel_max() + start) - offset;
start = rel_end.within(&start.to(end));
}
start.abs += len.abs;
start.rel += len.rel;
span.end = start;
let scalar = span.len();
*ctx.outer.axis_mut(self.dir.axis) = outer.select_len(scalar);
let ortho = ctx.len_axis(child, !self.dir.axis);
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
if ortho.rel > 0.0 || ortho.rest > 0.0 {
ortho_len.rest = 1.0;
ortho_len.abs = 0.0;
break;
}
ortho_len.abs = ortho_len.abs.max(ortho.abs);
start.abs += self.gap;
}
ortho_len
} else {
let mut ortho_len = Len::ZERO;
let ortho = !self.dir.axis;
for child in &self.children {
let len = ctx.len_axis(child, ortho);
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
if len.rel > 0.0 || len.rest > 0.0 {
ortho_len.rest = 1.0;
ortho_len.abs = 0.0;
break;
}
ortho_len.abs = ortho_len.abs.max(len.abs);
}
ortho_len
}
}
} }
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> { pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
+20 -19
View File
@@ -8,29 +8,30 @@ pub struct Stack {
} }
impl Widget for Stack { impl Widget for Stack {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let sizing = match self.size { let mut iter = self.children.iter();
StackSize::Default => None, if let Some(child) = iter.next() {
StackSize::Child(i) => Some(i), painter.child_layer();
}; painter.widget(child);
let mut size = Size::default();
for (i, child) in self.children.iter().enumerate() {
match i {
0 => painter.child_layer(),
_ => painter.next_layer(),
} }
let drawn = painter.widget(child); for child in iter {
// Only the child that sizes the stack is read, so the others painter.next_layer();
// changing size does not redraw it. painter.widget(child);
if sizing == Some(i) {
size = drawn.size();
} }
} }
size
}
fn on_resize(&self, _: Axis) -> OnResize { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
OnResize::Scale match self.size {
StackSize::Default => Len::default(),
StackSize::Child(i) => ctx.width(&self.children[i]),
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
match self.size {
StackSize::Default => Len::default(),
StackSize::Child(i) => ctx.height(&self.children[i]),
}
} }
} }
+19 -4
View File
@@ -6,10 +6,25 @@ pub struct WidgetPtr {
} }
impl Widget for WidgetPtr { impl Widget for WidgetPtr {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
match &self.inner { if let Some(id) = &self.inner {
Some(id) => painter.widget(id).size(), painter.widget(id);
None => Size::default(), }
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
if let Some(id) = &self.inner {
ctx.width(id)
} else {
Len::ZERO
}
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
if let Some(id) = &self.inner {
ctx.height(id)
} else {
Len::ZERO
} }
} }
} }
+5 -7
View File
@@ -28,23 +28,21 @@ impl Rect {
} }
impl Widget for Rect { impl Widget for Rect {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
painter.primitive(RectPrimitive { painter.primitive(RectPrimitive {
color: self.color, color: self.color,
radius: self.radius, radius: self.radius,
thickness: self.thickness, thickness: self.thickness,
inner_radius: self.inner_radius, inner_radius: self.inner_radius,
}); });
Size::REST
} }
fn size_hint(&self, _: Axis) -> Option<Len> { fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
Some(Len::REST) Len::rest(1)
} }
/// Its box is its primitive's own region, so a new one is written there. fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
fn on_resize(&self, _: Axis) -> OnResize { Len::rest(1)
OnResize::Scale
} }
} }
+13 -16
View File
@@ -55,47 +55,44 @@ impl TextEdit {
} }
impl Widget for TextEdit { impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
let base = painter.layer; let base = painter.layer;
painter.child_layer(); painter.child_layer();
let (_, size) = self.view.draw(painter); self.view.draw(painter);
painter.layer = base; painter.layer = base;
let region = self.region(); let region = self.region();
let Some(selection) = self.selection else { let Some(selection) = self.selection else {
return size; return;
}; };
let layout = self.view.buf.layout(); let layout = self.view.buf.layout();
// parley reports selection as boxes in layout space, so bidi and // parley reports selection as boxes in layout space, so bidi and
// wrapped lines come out right without this code knowing about either. // wrapped lines come out right without this code knowing about either.
for (rect, _) in selection.geometry(layout) { for (rect, _) in selection.geometry(layout) {
let rect_size = vec2(rect.width() as f32, rect.height() as f32); let size = vec2(rect.width() as f32, rect.height() as f32);
let top_left = vec2(rect.x0 as f32, rect.y0 as f32); let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
painter.primitive_within( painter.primitive_within(
RectPrimitive::color(Color::SKY), RectPrimitive::color(Color::SKY),
rect_size size.align(Align::TOP_LEFT).offset(top_left).within(&region),
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
); );
} }
let caret = selection.focus().geometry(layout, CARET_WIDTH); let caret = selection.focus().geometry(layout, CARET_WIDTH);
let caret_size = vec2(caret.width() as f32, caret.height() as f32); let size = vec2(caret.width() as f32, caret.height() as f32);
let top_left = vec2(caret.x0 as f32, caret.y0 as f32); let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
painter.primitive_within( painter.primitive_within(
RectPrimitive::color(Color::WHITE), RectPrimitive::color(Color::WHITE),
caret_size size.align(Align::TOP_LEFT).offset(top_left).within(&region),
.align(Align::TOP_LEFT)
.offset(top_left)
.within(&region),
); );
size
} }
fn on_resize(&self, axis: Axis) -> OnResize { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.view.on_resize(axis) self.view.desired_width(ctx)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.view.desired_height(ctx)
} }
} }
+41 -36
View File
@@ -52,15 +52,15 @@ impl TextView {
.align(self.align) .align(self.align)
} }
fn render(&mut self, painter: &mut Painter) -> &RenderedText { fn render(&mut self, ctx: &mut SizeCtx) -> &RenderedText {
let width = if self.attrs.wrap { let width = if self.attrs.wrap {
Some(painter.px_len(Axis::X)) Some(ctx.px_size().x)
} else { } else {
None None
}; };
if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed { if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
self.width = width; self.width = width;
self.tex = Some(painter.render_text(&mut self.buf, &self.attrs, width)); self.tex = Some(ctx.draw_text(&mut self.buf, &self.attrs, width));
self.attrs.changed = false; self.attrs.changed = false;
self.buf.changed = false; self.buf.changed = false;
} }
@@ -69,40 +69,39 @@ impl TextView {
pub fn tex(&self) -> Option<&RenderedText> { pub fn tex(&self) -> Option<&RenderedText> {
self.tex.as_ref() self.tex.as_ref()
} }
/// Draws the text, and says where the glyphs went and what they use. pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) { if self.is_empty()
&& let Some(hint) = &self.hint
{
ctx.width(hint)
} else {
Len::abs(self.render(ctx).size.x)
}
}
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
if self.is_empty()
&& let Some(hint) = &self.hint
{
ctx.height(hint)
} else {
Len::abs(self.render(ctx).size.y)
}
}
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
let align = self.align; let align = self.align;
if self.is_empty() && self.hint.is_some() { if self.is_empty() && self.hint.is_some() {
let region = self.render(painter).size.align(align); let region = self.render(&mut painter.size_ctx()).size.align(align);
let size = match &self.hint { if let Some(hint) = &self.hint {
Some(hint) => painter.widget(hint).size(), painter.widget(hint);
None => Size::ZERO, }
}; return region;
return (region, size);
} }
let tex = self.render(painter); let tex = self.render(&mut painter.size_ctx());
let region = tex.size.align(align); let region = tex.size.align(align);
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
}
/// Wrapping reads the width it is offered, so a wider box reshapes it and
/// a taller one does not. Alignment matters too, and separately: glyphs
/// anchored to the start of an axis stay put when that extent changes,
/// but centred or end-aligned ones move even though the shaping stands.
pub fn on_resize(&self, axis: Axis) -> OnResize {
let reshapes = axis == Axis::X && self.attrs.wrap;
let anchored = match axis {
Axis::X => self.align.x,
Axis::Y => self.align.y,
} == AxisAlign::Neg;
match reshapes || !anchored {
true => OnResize::Redraw,
false => OnResize::Translate,
}
} }
pub fn content(&self) -> String { pub fn content(&self) -> String {
@@ -118,7 +117,7 @@ impl Text {
content: content.into(), content: content.into(),
} }
} }
fn update_buf(&mut self) { fn update_buf(&mut self, _ctx: &mut SizeCtx) {
if self.content.changed { if self.content.changed {
self.content.changed = false; self.content.changed = false;
self.view.buf.set_text(self.content.as_str()); self.view.buf.set_text(self.content.as_str());
@@ -127,13 +126,19 @@ impl Text {
} }
impl Widget for Text { impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) {
self.update_buf(); self.update_buf(&mut painter.size_ctx());
self.view.draw(painter).1 self.view.draw(painter);
} }
fn on_resize(&self, axis: Axis) -> OnResize { fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
self.view.on_resize(axis) self.update_buf(ctx);
self.view.desired_width(ctx)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
self.update_buf(ctx);
self.view.desired_height(ctx)
} }
} }
-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, abs: 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
);
}
}
+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,
}, },
); );
} }
-337
View File
@@ -1,337 +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};
const DEPTH: usize = 4;
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
}
/// 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());
let lens = [
Some(Len::abs(20.0 + rng.below(180) as f32)),
Some(Len::abs(20.0 + rng.below(180) as f32)),
];
edits.insert(idx, lens);
let sized = &mut h.rsc[tree.sized[idx]];
sized.x = lens[0];
sized.y = lens[1];
}
edits
}
/// 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)
}
/// 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}", wh.rsc.widgets().label(id)));
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());
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.
if grown.spans.is_empty() {
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 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());
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 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);
}
}
}
/// Reproduces a divergence that predates the position chain: laying a tree out
/// again does not always land where growing it cold does.
///
/// Every one seen so far is a wrapping text on a span's *own* axis, where the
/// two draws do not agree. The span measures the child in the whole box, the
/// child shapes to that width and reports the width it used, the span then
/// places it in exactly that width -- which is a length change, so the child
/// shapes again, and its longest line is shorter than the box it was just
/// given. Each pass narrows it, so where the tree ends up depends on how many
/// passes it has had, and a warm tree has had a different number from a cold
/// one. Layout is supposed to be a function of the state alone.
///
/// A span whose axis is not the wrap axis is stable, which is every real
/// column of text, and why nothing else has run into this.
///
/// 7 of these 90 diverge on `db1751f`, before the chain; 30 do with it, since
/// a placed child reaches the second shaping more often. Both numbers are the
/// same defect, and it wants fixing where the two draws meet -- LAYOUT.md §4 --
/// rather than anywhere in the chain.
#[test]
#[ignore = "a hundred seeds, rather than the 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(1..=100);
for seed in seeds {
changed_size(seed);
resized(seed);
resized_then_changed(seed);
for shuffle in SHUFFLES {
reshuffled(seed, shuffle);
}
}
}
-150
View File
@@ -32,153 +32,3 @@ fn resizing_relays_out_against_the_new_output() {
assert_corners!(h, left, (0, 0), (100, 100)); assert_corners!(h, left, (0, 0), (100, 100));
assert_corners!(h, right, (100, 0), (800, 100)); assert_corners!(h, right, (100, 0), (800, 100));
} }
#[test]
fn an_empty_widget_takes_a_share_of_a_span() {
let mut h = Harness::new((400, 200));
let gap = ().add(&mut h.rsc);
let right = rect(Color::BLUE).width(100).add(&mut h.rsc);
h.set_root((gap, right).span(Dir::RIGHT));
assert_corners!(h, gap, (0, 0), (300, 200));
assert_corners!(h, right, (300, 0), (400, 200));
}
#[test]
fn a_child_drawn_twice_moves_once() {
let mut h = Harness::new((400, 200));
// `Aligned` draws its child twice; listing it twice would move it twice.
let inner = rect(Color::BLUE).add(&mut h.rsc);
let centered = inner.center().width(200).add(&mut h.rsc);
let left = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((left, centered).span(Dir::RIGHT));
assert_corners!(h, inner, (100, 0), (300, 200));
h.rsc[left].x = Some(Len::abs(150));
h.frame();
assert_corners!(h, inner, (150, 0), (350, 200));
}
#[test]
fn a_resize_lands_where_a_cold_start_would() {
let build = |h: &mut Harness| {
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves room \
for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.pad(16)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
let root = (para, below).span(Dir::DOWN).pad(12);
h.set_root(root);
(para, below)
};
let mut cold = Harness::new((900, 1200));
let (cold_para, cold_below) = build(&mut cold);
let mut resized = Harness::new((1920, 1200));
let (para, below) = build(&mut resized);
resized.resize((900, 1200));
resized.frame();
assert_eq!(resized.region(&para), cold.region(&cold_para), "paragraph");
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
}
#[test]
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
let mut h = Harness::new((400, 400));
// The panel fills a stack sized by its sibling, so it is drawn in the
// whole box and then placed in the shorter one. Reusing it in that fixed
// box afterwards would leave it whatever height it happened to have.
let panel = rect(Color::BLUE).add(&mut h.rsc);
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
let stack = (panel, leaf)
.stack()
.size(StackSize::Child(1))
.add(&mut h.rsc);
h.set_root(stack.align(Align::TOP));
assert_corners!(h, panel, (0, 0), (400, 100));
h.rsc[leaf].y = Some(Len::abs(250));
h.frame();
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::abs(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::abs(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::abs(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), 1, "one span above the leaf");
}
-218
View File
@@ -1,218 +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`, `size`, `scroll`, `resize`, or `all`.
//! `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load.
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();
println!(
"{label}: {frames} frame(s), min {:.3} ms, median {:.3} ms, total {:.1} ms",
elapsed[0],
elapsed[frames / 2],
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", "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("size") {
let (mut harness, tree) = warm(seed, depth);
trace_selected(&tree);
let sized = tree.sized[0];
run("size", frames, &mut harness, move |harness, frame| {
harness.rsc[sized].x = Some(Len::abs(100.0 + (frame % 2) as f32 * 40.0));
});
}
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);
}
}
+1 -26
View File
@@ -2,7 +2,7 @@
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use iris::harness::{Harness, TouchScript}; use iris::harness::Harness;
use iris::prelude::*; use iris::prelude::*;
#[test] #[test]
@@ -58,28 +58,3 @@ fn hover_ends_when_the_cursor_leaves_the_window() {
h.leave(); h.leave();
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1)); assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1));
} }
#[test]
fn a_recorded_gesture_presses_where_it_says() {
let mut h = Harness::new((400, 200));
let clicks = Rc::new(RefCell::new(Vec::new()));
let (on_left, on_right) = (clicks.clone(), clicks.clone());
let left = rect(Color::RED)
.width(100)
.on(CursorSense::click(), move |_, _| {
on_left.borrow_mut().push("left")
})
.add(&mut h.rsc);
let right = rect(Color::BLUE)
.on(CursorSense::click(), move |_, _| {
on_right.borrow_mut().push("right")
})
.add(&mut h.rsc);
h.set_root((left, right).span(Dir::RIGHT));
let script = TouchScript::parse("0 down 300 100\n80 up 300 100").unwrap();
h.replay(&script);
assert_eq!(*clicks.borrow(), ["right"]);
}
-225
View File
@@ -1,225 +0,0 @@
//! Input across layers: what stops at a layer, what passes through it, and
//! where hovering stops.
use std::{cell::RefCell, rc::Rc};
use iris::harness::Harness;
use iris::prelude::*;
const WINDOW: f32 = 100.0;
/// Every sense that has fired on one widget since it was last read.
#[derive(Default, Clone)]
struct Fired(Rc<RefCell<Vec<CursorSense>>>);
impl Fired {
fn take(&self) -> Vec<CursorSense> {
std::mem::take(&mut self.0.borrow_mut())
}
}
/// A widget filling whatever it is given, recording the senses it is sent.
fn listener(h: &mut Harness, senses: impl Into<CursorSenses>) -> (WeakWidget<Rect>, Fired) {
let fired = Fired::default();
let record = fired.clone();
let id = rect(Color::WHITE)
.on(senses.into(), move |ctx, _| {
record.0.borrow_mut().push(ctx.data.sense)
})
.add(&mut h.rsc);
(id, fired)
}
/// A widget with no senses of its own, to leave a gap beside one that has.
fn blank(h: &mut Harness) -> WeakWidget<Rect> {
rect(Color::WHITE).add(&mut h.rsc)
}
fn harness() -> Harness {
Harness::new((WINDOW, WINDOW))
}
#[test]
fn hover_stops_at_the_topmost_widget() {
let mut h = harness();
let (bottom, bottom_hover) = listener(&mut h, CursorSense::HoverStart);
let (middle, middle_hover) = listener(&mut h, CursorSense::HoverStart);
let (top, top_hover) = listener(&mut h, CursorSense::HoverStart);
h.set_root((bottom, middle, top).stack());
h.move_to((50, 50));
assert_eq!(top_hover.take(), [CursorSense::HoverStart]);
assert_eq!(
middle_hover.take(),
[],
"hover is not shared with a layer below"
);
assert_eq!(bottom_hover.take(), []);
}
#[test]
fn a_scroll_passes_through_every_widget_that_does_not_want_it() {
let mut h = harness();
let (list, scrolled) = listener(&mut h, CursorSense::Scroll);
let (button, clicked) = listener(&mut h, CursorSense::click());
let (overlay, overlay_clicked) = listener(&mut h, CursorSense::click());
h.set_root((list, button, overlay).stack());
h.move_to((50, 50));
h.scroll((0, 10));
assert_eq!(
scrolled.take(),
[CursorSense::Scroll],
"two layers of click-only widgets do not stop a scroll"
);
assert_eq!(clicked.take(), []);
assert_eq!(overlay_clicked.take(), []);
}
#[test]
fn hovering_a_button_above_does_not_stop_a_later_scroll() {
let mut h = harness();
let (list, scrolled) = listener(&mut h, CursorSense::Scroll);
let (button, _clicked) = listener(&mut h, CursorSense::click());
h.set_root((list, button).stack());
// The hover arrives in its own frame, as a window delivers it.
h.move_to((50, 50));
assert_eq!(scrolled.take(), []);
h.scroll((0, 10));
assert_eq!(
scrolled.take(),
[CursorSense::Scroll],
"a hover already resting on the button must not consume the wheel"
);
}
#[test]
fn only_the_topmost_listener_takes_a_press() {
let mut h = harness();
let (below, below_clicked) = listener(&mut h, CursorSense::click());
let (above, above_clicked) = listener(&mut h, CursorSense::click());
h.set_root((below, above).stack());
h.click((50, 50));
assert_eq!(above_clicked.take(), [CursorSense::click()]);
assert_eq!(below_clicked.take(), [], "one press goes to one widget");
}
#[test]
fn a_press_beside_the_button_reaches_the_layer_below() {
let mut h = harness();
let (list, list_clicked) = listener(&mut h, CursorSense::click());
// The row above the list covers it, but only its left half is the button.
let (button, button_clicked) = listener(&mut h, CursorSense::click());
let row = (button, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((list, row).stack());
h.click((20, 50));
assert_eq!(button_clicked.take(), [CursorSense::click()]);
assert_eq!(list_clicked.take(), []);
h.click((80, 50));
assert_eq!(button_clicked.take(), [], "the cursor is not on the button");
assert_eq!(
list_clicked.take(),
[CursorSense::click()],
"a press beside the button belongs to what is under it"
);
}
#[test]
fn leaving_a_widget_still_ends_its_hover() {
let mut h = harness();
let (widget, hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
h.set_root(widget);
h.move_to((50, 50));
assert_eq!(hover.take(), [CursorSense::HoverStart]);
h.leave();
assert_eq!(hover.take(), [CursorSense::HoverEnd]);
}
#[test]
fn leaving_a_widget_does_not_block_the_layer_below() {
let mut h = harness();
let (below, below_hover) = listener(&mut h, CursorSense::HoverStart);
// Only the left half of the layer above is a widget, so the cursor can
// leave it without leaving the one underneath.
let (above, above_hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
let row = (above, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((below, row).stack());
h.move_to((20, 50));
assert_eq!(above_hover.take(), [CursorSense::HoverStart]);
assert_eq!(below_hover.take(), [], "the layer above is over it");
h.move_to((80, 50));
assert_eq!(above_hover.take(), [CursorSense::HoverEnd]);
assert_eq!(
below_hover.take(),
[CursorSense::HoverStart],
"ending a hover above must not stop the hover below"
);
}
#[test]
fn covering_a_widget_ends_its_hover() {
let mut h = harness();
let (below, below_hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
let (above, above_hover) = listener(&mut h, CursorSense::HoverStart);
let row = (above, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root((below, row).stack());
h.move_to((80, 50));
assert_eq!(below_hover.take(), [CursorSense::HoverStart]);
h.move_to((20, 50));
assert_eq!(above_hover.take(), [CursorSense::HoverStart]);
assert_eq!(
below_hover.take(),
[CursorSense::HoverEnd],
"a widget covered by one that took the input is no longer hovered"
);
h.move_to((80, 50));
assert_eq!(
below_hover.take(),
[CursorSense::HoverStart],
"uncovering it hovers it again"
);
}
#[test]
fn hover_starts_and_ends_once_each() {
let mut h = harness();
// Only the left half is the widget, so the cursor can leave it without
// leaving the window.
let (widget, hover) = listener(&mut h, CursorSense::HoverStart | CursorSense::HoverEnd);
let row = (widget, blank(&mut h)).span(Dir::RIGHT).add(&mut h.rsc);
h.set_root(row);
h.move_to((20, 50));
assert_eq!(hover.take(), [CursorSense::HoverStart]);
h.move_to((30, 50));
assert_eq!(hover.take(), [], "staying inside is not a second start");
h.move_to((80, 50));
assert_eq!(hover.take(), [CursorSense::HoverEnd]);
h.move_to((90, 50));
assert_eq!(hover.take(), [], "an ended hover does not end again");
h.move_to((20, 50));
assert_eq!(
hover.take(),
[CursorSense::HoverStart],
"re-entering starts it"
);
}
-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::abs(40.0 + (i % 2) as f32));
h.frame();
}
}
-445
View File
@@ -1,445 +0,0 @@
//! What a second frame draws again, and what it keeps.
use std::{cell::Cell, rc::Rc};
use iris::harness::{Harness, assert_corners};
use iris::prelude::*;
/// A leaf that counts its draws and reports whatever size it is given, so a
/// test can see what the retained path skipped.
struct Counted {
draws: Rc<Cell<usize>>,
size: Size,
dependence: OnResize,
}
impl Widget for Counted {
fn draw(&mut self, _: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
self.size
}
fn on_resize(&self, _: Axis) -> OnResize {
self.dependence
}
}
struct Counts(Rc<Cell<usize>>);
impl Counts {
fn get(&self) -> usize {
self.0.get()
}
}
fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (WeakWidget<Counted>, Counts) {
let draws = Rc::new(Cell::new(0));
let id = Counted {
draws: draws.clone(),
size,
dependence,
}
.add(&mut h.rsc);
(id, Counts(draws))
}
/// A fixed-width leaf beside one that takes the rest, so changing the first
/// hands the second a different box without the output changing.
fn pair(h: &mut Harness, rest: OnResize) -> (WeakWidget<Counted>, Counts, WidgetId) {
let (first, _) = counted(h, Size::from((100, 200)), OnResize::Translate);
let (second, draws) = counted(h, Size::REST, rest);
h.set_root((first, second).span(Dir::RIGHT));
(first, draws, second.id())
}
#[test]
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, OnResize::Scale);
let settled = draws.get();
assert_corners!(h, second, (100, 0), (400, 200));
h.rsc[first].size = Size::from((150, 200));
h.frame();
assert_eq!(
draws.get(),
settled,
"its box is a field to write, not a reason to draw"
);
assert_corners!(h, second, (150, 0), (400, 200));
}
#[test]
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
let mut h = Harness::new((400, 200));
let (first, draws, second) = pair(&mut h, OnResize::Redraw);
let settled = draws.get();
h.rsc[first].size = Size::from((150, 200));
h.frame();
// The preceding fixed child makes the remaining box this child's real
// box, so measuring it also draws it in its final place.
assert_eq!(draws.get(), settled + 1);
assert_corners!(h, second, (150, 0), (400, 200));
}
#[test]
fn a_span_child_that_declares_its_length_is_drawn_once() {
let mut h = Harness::new((400, 200));
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
// The span takes one child's length from its hint and has to draw the
// other to find out, so only the second is drawn before it is placed.
let hinted = told.width(100).add(&mut h.rsc);
h.set_root((hinted, asked).span(Dir::RIGHT));
assert_eq!(told_draws.get(), 1);
assert_eq!(
asked_draws.get(),
2,
"drawn to be measured, then again to be placed"
);
}
#[test]
fn a_span_relays_out_when_a_child_it_measured_changes() {
let mut h = Harness::new((400, 200));
let (first, _, second) = pair(&mut h, OnResize::Translate);
h.rsc[first].size = Size::from((250, 200));
h.frame();
assert_corners!(h, first, (0, 0), (250, 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]
fn a_placed_child_survives_the_next_frame() {
let mut h = Harness::new((400, 200));
// Both children declare a length, so the span places them from their hints
// rather than drawing them to find out.
let top = rect(Color::RED).height(80).add(&mut h.rsc);
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
h.set_root((top, bottom).span(Dir::DOWN));
h.rsc.widgets_mut().get_dyn_mut(top.id());
h.frame();
assert_corners!(h, top, (0, 0), (400, 80));
assert_corners!(h, bottom, (0, 80), (400, 200));
}
/// Lays its child out from the hint alone, never reading what it drew.
struct FromHint {
inner: StrongWidget,
}
impl Widget for FromHint {
fn draw(&mut self, painter: &mut Painter) -> Size {
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
let mut region = UiRegion::FULL;
region.y.end = region.y.start.offset(len.abs);
painter.widget_within(&self.inner, region);
Size::REST
}
}
#[test]
fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::RED).height(80).add(&mut h.rsc);
let parent = FromHint {
inner: inner.add_strong(&mut h.rsc),
}
.add(&mut h.rsc);
h.set_root(parent);
assert_corners!(h, inner, (0, 0), (400, 80));
h.rsc[inner].y = Some(Len::abs(120));
h.frame();
assert_corners!(h, inner, (0, 0), (400, 120));
}
/// Reads the output's size, which nothing but its own draw can put right.
struct ReadsOutput {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsOutput {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::abs(painter.output_size() / 4.0)
}
}
struct ReadsWidth {
draws: Rc<Cell<usize>>,
}
impl Widget for ReadsWidth {
fn draw(&mut self, painter: &mut Painter) -> Size {
self.draws.set(self.draws.get() + 1);
Size::abs((painter.output_len(Axis::X) / 4.0, 20.0).into())
}
}
#[test]
fn a_resize_does_not_redraw_what_the_shader_can_move() {
let mut h = Harness::new((400, 200));
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
h.set_root(leaf);
let settled = draws.get();
h.resize((800, 100));
assert!(h.needs_redraw());
h.frame();
assert_eq!(
draws.get(),
settled,
"its box is the same fraction of a different output"
);
assert_corners!(h, leaf, (0, 0), (800, 100));
}
#[test]
fn a_resize_redraws_what_read_the_output() {
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((800, 100));
h.frame();
assert_eq!(draws.get(), settled + 1);
}
#[test]
fn a_resize_only_redraws_read_output_axes() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(leaf);
let settled = draws.get();
h.resize((400, 300));
h.frame();
assert_eq!(draws.get(), settled, "height was never read");
h.resize((800, 300));
h.frame();
assert_eq!(draws.get(), settled + 1, "width changes its answer");
}
#[test]
fn subpixel_resize_changes_accumulate_from_the_last_layout() {
let mut h = Harness::new((400, 200));
let draws = Rc::new(Cell::new(0));
let leaf = ReadsWidth {
draws: draws.clone(),
}
.add(&mut h.rsc);
h.set_root(leaf);
let settled = draws.get();
for width in [400.02, 400.04, 400.05] {
h.resize((width, 200.0));
h.frame();
assert_eq!(draws.get(), settled);
}
h.resize((400.06, 200.0));
h.frame();
assert_eq!(draws.get(), settled + 1);
}
#[test]
fn subpixel_box_changes_accumulate_from_the_last_draw() {
let mut h = Harness::new((400, 200));
let (first, draws, _) = pair(&mut h, OnResize::Redraw);
let settled = draws.get();
for width in [100.02, 100.04, 100.05] {
h.rsc[first].size.x = Len::abs(width);
h.frame();
assert_eq!(draws.get(), settled);
}
h.rsc[first].size.x = Len::abs(100.06);
h.frame();
assert_eq!(draws.get(), settled + 1);
}
#[test]
fn reporting_the_same_output_size_does_not_start_a_resize() {
let mut h = Harness::new((400, 200));
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]
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
let mut h = Harness::new((600, 400));
let para = wtext(
"Wrapping shapes one source into as many lines as its container leaves \
room for, so the height of a paragraph is an answer rather than a setting.",
)
.size(20)
.wrap(true)
.add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((para, below).span(Dir::DOWN));
let top = h.region(&below).expect("drew nothing").top_left.y;
h.resize((300, 400));
h.frame();
let lower = h.region(&below).expect("drew nothing").top_left.y;
assert!(lower > top, "same words, half the width: {top} -> {lower}");
}
#[test]
fn a_change_two_levels_under_its_reader_still_reaches_it() {
let mut h = Harness::new((400, 400));
// 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
// size as before, which is what lets a draw reuse its way past the leaf.
let (leaf, _) = counted(&mut h, Size::abs((100, 100).into()), OnResize::Redraw);
let padded = leaf.pad(10).add(&mut h.rsc);
let below = rect(Color::RED).add(&mut h.rsc);
h.set_root((padded, below).span(Dir::DOWN).pad(12));
assert_corners!(h, below, (12, 132), (388, 388));
h.rsc[leaf].size = Size::abs((100, 200).into());
h.frame();
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::abs(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::abs(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::abs(200));
h.frame();
assert_eq!(draws.get(), settled, "its own length did not change");
assert_corners!(h, fixed, (200, 0), (280, 200));
}