Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
169db7f16f | ||
|
|
4063635f39 | ||
|
|
f61e8936f1 | ||
|
|
02ff8c7454 | ||
|
|
65f68bbb8a | ||
|
|
99131940ab | ||
|
|
e5a3e640d4 | ||
|
|
c596bf12c6 | ||
|
|
f0c7df06ac | ||
|
|
b7caab3b9e | ||
|
|
386a0d1b8f | ||
|
|
1b1378b05a | ||
|
|
60175c3821 | ||
|
|
b165164e59 | ||
|
|
ef815dadfd | ||
|
|
9f4311774b | ||
|
|
7c50a3e51b | ||
|
|
3f7cd8251b | ||
|
|
bf9438087a | ||
|
|
77bb75e5de | ||
|
|
2525637e26 | ||
|
|
e5f8b6b244 | ||
|
|
f1a47e9b7b | ||
|
|
b1b3eca1c0 | ||
|
|
82fa6c1123 | ||
|
|
480f0bc99f | ||
|
|
84f589e364 | ||
|
|
a640c6cce2 | ||
|
|
cdec29351a | ||
|
|
4178dfbff9 | ||
|
|
2272634dc5 | ||
|
|
b0f9f046da | ||
|
|
86a7e8dfc3 | ||
|
|
d98969158f | ||
|
|
1f9dc48b80 | ||
|
|
db1751fdfd | ||
|
|
78a53b6bf6 | ||
|
|
d8c497fcd7 | ||
|
|
c8ec0866d4 | ||
|
|
8223a55cfb | ||
|
|
f9ef7514e7 | ||
|
|
ca2b4b2173 | ||
|
|
f9423855e1 | ||
|
|
43ce8c7d02 | ||
|
|
c8ac669f95 |
No files matched your search
Generated
+25
-16
@@ -567,9 +567,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dlib"
|
name = "dlib"
|
||||||
version = "0.5.2"
|
version = "0.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
@@ -2158,9 +2158,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.38.4"
|
version = "0.41.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
|
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -2353,6 +2353,15 @@ 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"
|
||||||
@@ -2909,9 +2918,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-backend"
|
name = "wayland-backend"
|
||||||
version = "0.3.12"
|
version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9"
|
checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"downcast-rs",
|
"downcast-rs",
|
||||||
@@ -2923,9 +2932,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-client"
|
name = "wayland-client"
|
||||||
version = "0.31.12"
|
version = "0.31.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec"
|
checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"rustix 1.1.3",
|
"rustix 1.1.3",
|
||||||
@@ -2957,9 +2966,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-protocols"
|
name = "wayland-protocols"
|
||||||
version = "0.32.10"
|
version = "0.32.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3"
|
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
@@ -2982,9 +2991,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-protocols-wlr"
|
name = "wayland-protocols-wlr"
|
||||||
version = "0.3.10"
|
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 = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3"
|
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
@@ -2995,9 +3004,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-scanner"
|
name = "wayland-scanner"
|
||||||
version = "0.31.8"
|
version = "0.31.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3"
|
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
@@ -3006,9 +3015,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-sys"
|
name = "wayland-sys"
|
||||||
version = "0.31.8"
|
version = "0.31.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd"
|
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dlib",
|
"dlib",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
+6
-1
@@ -3,6 +3,9 @@ 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]
|
||||||
@@ -20,7 +23,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"]
|
members = ["core", "macro", "rig-input"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -40,3 +43,5 @@ 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,6 +3,9 @@ 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 }
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ 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>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +108,7 @@ 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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,11 +140,13 @@ 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) + 'a {
|
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) -> bool + '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,
|
||||||
@@ -152,6 +156,7 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
consumed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,10 +9,19 @@ 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 {
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,488 @@
|
|||||||
|
//! Opt-in counters and coarse timers for explaining CPU layout cost.
|
||||||
|
//!
|
||||||
|
//! Enable the `layout-diagnostics` feature. With it disabled, none of the
|
||||||
|
//! instrumentation is compiled into Iris. The retained rig in
|
||||||
|
//! `tests/layout_diagnostics.rs` is the ordinary entry point.
|
||||||
|
//!
|
||||||
|
//! Timers are inclusive: `update total` contains `full layout` or
|
||||||
|
//! `incremental layout`, and `text render` contains shaping and glyph
|
||||||
|
//! placement. They locate cost within one instrumented run and must not be
|
||||||
|
//! added together. Use an uninstrumented build under `perf` for final CPU
|
||||||
|
//! totals; counting every primitive and distinct widget deliberately perturbs
|
||||||
|
//! the instrumented run.
|
||||||
|
//!
|
||||||
|
//! Call [`trace_widget`] before a frame to retain the ordered constraint,
|
||||||
|
//! reuse, size, placement, and text events for one suspicious widget. The
|
||||||
|
//! selection is a set and survives [`take`] until cleared.
|
||||||
|
|
||||||
|
use crate::{Axis, Len, Size, UiRegion, WidgetId, util::Vec2};
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
fmt::Write,
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) enum Counter {
|
||||||
|
Updates,
|
||||||
|
ResizeDependents,
|
||||||
|
DrawRequests,
|
||||||
|
WidgetDraws,
|
||||||
|
PlaceCalls,
|
||||||
|
SizeReads,
|
||||||
|
HintHits,
|
||||||
|
HintMisses,
|
||||||
|
RetainedSizeHits,
|
||||||
|
ReuseAttempts,
|
||||||
|
ReuseExact,
|
||||||
|
ReuseMoved,
|
||||||
|
ReuseDirty,
|
||||||
|
ReuseWrongParent,
|
||||||
|
ReuseUnslotted,
|
||||||
|
ReuseOwnResize,
|
||||||
|
ReuseDescendantResize,
|
||||||
|
ResizeChecks,
|
||||||
|
ResizeCheckChildren,
|
||||||
|
QueuePops,
|
||||||
|
DepthReads,
|
||||||
|
EagerReaderRedraws,
|
||||||
|
LocalRedraws,
|
||||||
|
SizeChanges,
|
||||||
|
ReaderEdges,
|
||||||
|
PrimitiveWrites,
|
||||||
|
TextRenders,
|
||||||
|
TextShapeHits,
|
||||||
|
TextShapes,
|
||||||
|
TextBreaks,
|
||||||
|
GlyphPlacements,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Counter {
|
||||||
|
const COUNT: usize = Self::GlyphPlacements as usize + 1;
|
||||||
|
|
||||||
|
const NAMES: [&'static str; Self::COUNT] = [
|
||||||
|
"updates",
|
||||||
|
"resize dependents",
|
||||||
|
"draw requests",
|
||||||
|
"widget draws",
|
||||||
|
"place calls",
|
||||||
|
"draw-result size reads",
|
||||||
|
"hint hits",
|
||||||
|
"hint misses",
|
||||||
|
"retained size hits",
|
||||||
|
"reuse attempts",
|
||||||
|
"reuse exact",
|
||||||
|
"reuse moved",
|
||||||
|
"reuse: dirty",
|
||||||
|
"reuse: wrong parent",
|
||||||
|
"reuse: unslotted",
|
||||||
|
"reuse: own resize",
|
||||||
|
"reuse: descendant resize",
|
||||||
|
"resize checks",
|
||||||
|
"resize children checked",
|
||||||
|
"redraw queue pops",
|
||||||
|
"depth reads",
|
||||||
|
"eager reader redraws",
|
||||||
|
"local redraws",
|
||||||
|
"size changes",
|
||||||
|
"reader edges",
|
||||||
|
"primitive writes",
|
||||||
|
"text renders",
|
||||||
|
"text shape hits",
|
||||||
|
"text shapes",
|
||||||
|
"text line breaks",
|
||||||
|
"glyph placements",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) enum TimerKind {
|
||||||
|
Update,
|
||||||
|
FullLayout,
|
||||||
|
ResizeMarking,
|
||||||
|
IncrementalLayout,
|
||||||
|
TextRender,
|
||||||
|
TextShape,
|
||||||
|
TextBreak,
|
||||||
|
GlyphPlacement,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimerKind {
|
||||||
|
const COUNT: usize = Self::GlyphPlacement as usize + 1;
|
||||||
|
|
||||||
|
const NAMES: [&'static str; Self::COUNT] = [
|
||||||
|
"update total",
|
||||||
|
"full layout",
|
||||||
|
"resize marking",
|
||||||
|
"incremental layout",
|
||||||
|
"text render",
|
||||||
|
"text shape",
|
||||||
|
"text line break",
|
||||||
|
"glyph placement",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Report {
|
||||||
|
counters: [u64; Counter::COUNT],
|
||||||
|
nanos: [u64; TimerKind::COUNT],
|
||||||
|
distinct_widgets: usize,
|
||||||
|
distinct_text_widgets: usize,
|
||||||
|
hot_widgets: Vec<Callsite>,
|
||||||
|
hot_text: Vec<Callsite>,
|
||||||
|
traces: Vec<TraceEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Report {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
counters: [0; Counter::COUNT],
|
||||||
|
nanos: [0; TimerKind::COUNT],
|
||||||
|
distinct_widgets: 0,
|
||||||
|
distinct_text_widgets: 0,
|
||||||
|
hot_widgets: Vec::new(),
|
||||||
|
hot_text: Vec::new(),
|
||||||
|
traces: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Report {
|
||||||
|
pub fn counters(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
|
||||||
|
Counter::NAMES.into_iter().zip(self.counters)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inclusive elapsed time accumulated for each targeted operation.
|
||||||
|
pub fn timings_ns(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
|
||||||
|
TimerKind::NAMES.into_iter().zip(self.nanos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn distinct_widgets(&self) -> usize {
|
||||||
|
self.distinct_widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn distinct_text_widgets(&self) -> usize {
|
||||||
|
self.distinct_text_widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hot_widgets(&self) -> &[Callsite] {
|
||||||
|
&self.hot_widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hot_text(&self) -> &[Callsite] {
|
||||||
|
&self.hot_text
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ordered layout events for widgets selected with [`trace_widget`].
|
||||||
|
pub fn traces(&self) -> &[TraceEvent] {
|
||||||
|
&self.traces
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats nonzero totals divided by `frames`.
|
||||||
|
pub fn per_frame(&self, frames: usize) -> String {
|
||||||
|
let divisor = frames.max(1) as f64;
|
||||||
|
let mut out = String::new();
|
||||||
|
for (name, value) in self.counters() {
|
||||||
|
if value != 0 {
|
||||||
|
let _ = writeln!(out, " {name:<27} {:>12.2}", value as f64 / divisor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.distinct_widgets != 0 {
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
" {:<27} {:>12}",
|
||||||
|
"distinct widgets", self.distinct_widgets
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.distinct_text_widgets != 0 {
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
" {:<27} {:>12}",
|
||||||
|
"distinct text widgets", self.distinct_text_widgets
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (name, nanos) in self.timings_ns() {
|
||||||
|
if nanos != 0 {
|
||||||
|
let ms = nanos as f64 / divisor / 1_000_000.0;
|
||||||
|
let _ = writeln!(out, " {name:<27} {ms:>12.3} ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.hot_widgets.is_empty() {
|
||||||
|
let _ = writeln!(out, " hottest widget draws:");
|
||||||
|
for callsite in &self.hot_widgets {
|
||||||
|
let calls = callsite.calls as f64 / divisor;
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
" {calls:>9.2} {:?} {}",
|
||||||
|
callsite.id, callsite.label
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.hot_text.is_empty() {
|
||||||
|
let _ = writeln!(out, " hottest text renders:");
|
||||||
|
for callsite in &self.hot_text {
|
||||||
|
let calls = callsite.calls as f64 / divisor;
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
" {calls:>9.2} {:>3} widths {:?} {}",
|
||||||
|
callsite.distinct_widths, callsite.id, callsite.label
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.traces.is_empty() {
|
||||||
|
let _ = writeln!(out, " targeted layout trace:");
|
||||||
|
for event in &self.traces {
|
||||||
|
let _ = writeln!(out, " {event:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Callsite {
|
||||||
|
pub id: WidgetId,
|
||||||
|
pub label: String,
|
||||||
|
pub calls: u64,
|
||||||
|
pub distinct_widths: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum ReuseOutcome {
|
||||||
|
Exact,
|
||||||
|
Moved,
|
||||||
|
Dirty,
|
||||||
|
WrongParent,
|
||||||
|
Unslotted,
|
||||||
|
OwnResize,
|
||||||
|
DescendantResize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One targeted layout event. Events are retained in execution order, making
|
||||||
|
/// repeated constraint paths visible without logging every widget globally.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum TraceEvent {
|
||||||
|
DrawRequest {
|
||||||
|
id: WidgetId,
|
||||||
|
parent: Option<WidgetId>,
|
||||||
|
region: UiRegion,
|
||||||
|
pixel_size: Vec2,
|
||||||
|
slotted: bool,
|
||||||
|
},
|
||||||
|
Reuse {
|
||||||
|
id: WidgetId,
|
||||||
|
outcome: ReuseOutcome,
|
||||||
|
},
|
||||||
|
SizeReported {
|
||||||
|
id: WidgetId,
|
||||||
|
size: Size,
|
||||||
|
},
|
||||||
|
Placed {
|
||||||
|
id: WidgetId,
|
||||||
|
parent: WidgetId,
|
||||||
|
region: UiRegion,
|
||||||
|
},
|
||||||
|
SizeRead {
|
||||||
|
id: WidgetId,
|
||||||
|
reader: WidgetId,
|
||||||
|
size: Size,
|
||||||
|
},
|
||||||
|
HintRead {
|
||||||
|
id: WidgetId,
|
||||||
|
reader: WidgetId,
|
||||||
|
axis: Axis,
|
||||||
|
hint: Option<Len>,
|
||||||
|
},
|
||||||
|
TextRendered {
|
||||||
|
id: WidgetId,
|
||||||
|
width: Option<f32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Calls {
|
||||||
|
label: String,
|
||||||
|
count: u64,
|
||||||
|
widths: HashSet<Option<u32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Current {
|
||||||
|
report: Report,
|
||||||
|
widgets: HashMap<WidgetId, Calls>,
|
||||||
|
text_widgets: HashMap<WidgetId, Calls>,
|
||||||
|
traced: HashSet<WidgetId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static CURRENT: RefCell<Current> = RefCell::new(Current::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bump(counter: Counter) {
|
||||||
|
CURRENT.with_borrow_mut(|current| current.report.counters[counter as usize] += 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn draw_widget(id: WidgetId, label: &str) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
let calls = current.widgets.entry(id).or_default();
|
||||||
|
if calls.label.is_empty() {
|
||||||
|
calls.label = label.to_owned();
|
||||||
|
}
|
||||||
|
calls.count += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a widget to the targeted trace set. Selection survives [`take`]
|
||||||
|
/// until explicitly removed or cleared.
|
||||||
|
pub fn trace_widget(id: impl Into<WidgetId>) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
current.traced.insert(id.into());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn untrace_widget(id: impl Into<WidgetId>) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
current.traced.remove(&id.into());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_traced_widgets() {
|
||||||
|
CURRENT.with_borrow_mut(|current| current.traced.clear());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trace(id: WidgetId, event: TraceEvent) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
if current.traced.contains(&id) {
|
||||||
|
current.report.traces.push(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn draw_request(
|
||||||
|
id: WidgetId,
|
||||||
|
parent: Option<WidgetId>,
|
||||||
|
region: UiRegion,
|
||||||
|
pixel_size: Vec2,
|
||||||
|
slotted: bool,
|
||||||
|
) {
|
||||||
|
trace(
|
||||||
|
id,
|
||||||
|
TraceEvent::DrawRequest {
|
||||||
|
id,
|
||||||
|
parent,
|
||||||
|
region,
|
||||||
|
pixel_size,
|
||||||
|
slotted,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
|
||||||
|
trace(id, TraceEvent::Reuse { id, outcome });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn size_reported(id: WidgetId, size: Size) {
|
||||||
|
trace(id, TraceEvent::SizeReported { id, size });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn placed(id: WidgetId, parent: WidgetId, region: UiRegion) {
|
||||||
|
trace(id, TraceEvent::Placed { id, parent, region });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn size_read(id: WidgetId, reader: WidgetId, size: Size) {
|
||||||
|
trace(id, TraceEvent::SizeRead { id, reader, size });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hint_read(id: WidgetId, reader: WidgetId, axis: Axis, hint: Option<Len>) {
|
||||||
|
trace(
|
||||||
|
id,
|
||||||
|
TraceEvent::HintRead {
|
||||||
|
id,
|
||||||
|
reader,
|
||||||
|
axis,
|
||||||
|
hint,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render_text(id: WidgetId, label: &str, width: Option<f32>) {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
let calls = current.text_widgets.entry(id).or_default();
|
||||||
|
if calls.label.is_empty() {
|
||||||
|
calls.label = label.to_owned();
|
||||||
|
}
|
||||||
|
calls.count += 1;
|
||||||
|
calls.widths.insert(width.map(f32::to_bits));
|
||||||
|
if current.traced.contains(&id) {
|
||||||
|
current
|
||||||
|
.report
|
||||||
|
.traces
|
||||||
|
.push(TraceEvent::TextRendered { id, width });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Timer {
|
||||||
|
kind: TimerKind,
|
||||||
|
start: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn timer(kind: TimerKind) -> Timer {
|
||||||
|
Timer {
|
||||||
|
kind,
|
||||||
|
start: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Timer {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let nanos = self.start.elapsed().as_nanos().min(u64::MAX as u128) as u64;
|
||||||
|
CURRENT.with_borrow_mut(|current| current.report.nanos[self.kind as usize] += nanos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes all diagnostics accumulated on this thread and resets them.
|
||||||
|
pub fn take() -> Report {
|
||||||
|
CURRENT.with_borrow_mut(|current| {
|
||||||
|
current.report.distinct_widgets = current.widgets.len();
|
||||||
|
current.report.distinct_text_widgets = current.text_widgets.len();
|
||||||
|
current.report.hot_widgets = hottest(¤t.widgets);
|
||||||
|
current.report.hot_text = hottest(¤t.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@
|
|||||||
#![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;
|
||||||
|
|||||||
@@ -144,10 +144,10 @@ impl UiScalar {
|
|||||||
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
||||||
let rel = align.rel();
|
let rel = align.rel();
|
||||||
let mut start = UiScalar::rel(rel);
|
let mut start = UiScalar::rel(rel);
|
||||||
start.abs -= self.abs * rel;
|
start.px -= self.px * rel;
|
||||||
start.rel -= self.rel * rel;
|
start.rel -= self.rel * rel;
|
||||||
let mut end = UiScalar::rel(rel);
|
let mut end = UiScalar::rel(rel);
|
||||||
end.abs += self.abs * (1.0 - rel);
|
end.px += self.px * (1.0 - rel);
|
||||||
end.rel += self.rel * (1.0 - rel);
|
end.rel += self.rel * (1.0 - rel);
|
||||||
UiSpan { start, end }
|
UiSpan { start, end }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum Axis {
|
pub enum Axis {
|
||||||
X,
|
X,
|
||||||
Y,
|
Y,
|
||||||
|
|||||||
+20
-20
@@ -9,14 +9,14 @@ pub struct Size {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Len {
|
pub struct Len {
|
||||||
pub abs: f32,
|
pub px: f32,
|
||||||
pub rel: f32,
|
pub rel: f32,
|
||||||
pub rest: f32,
|
pub rest: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<N: UiNum> From<N> for Len {
|
impl<N: UiNum> From<N> for Len {
|
||||||
fn from(value: N) -> Self {
|
fn from(value: N) -> Self {
|
||||||
Len::abs(value.to_f32())
|
Len::px(value.to_f32())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +46,10 @@ impl Size {
|
|||||||
y: Len::REST,
|
y: Len::REST,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn abs(v: Vec2) -> Self {
|
pub fn px(v: Vec2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: Len::abs(v.x),
|
x: Len::px(v.x),
|
||||||
y: Len::abs(v.y),
|
y: Len::px(v.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,13 +97,13 @@ impl Size {
|
|||||||
|
|
||||||
impl Len {
|
impl Len {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
abs: 0.0,
|
px: 0.0,
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const REST: Self = Self {
|
pub const REST: Self = Self {
|
||||||
abs: 0.0,
|
px: 0.0,
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: 1.0,
|
rest: 1.0,
|
||||||
};
|
};
|
||||||
@@ -111,27 +111,27 @@ impl Len {
|
|||||||
pub fn apply_rest(&self) -> UiScalar {
|
pub fn apply_rest(&self) -> UiScalar {
|
||||||
UiScalar {
|
UiScalar {
|
||||||
rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
|
rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
|
||||||
abs: self.abs,
|
px: self.px,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn abs(abs: impl UiNum) -> Self {
|
pub fn px(px: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
abs: abs.to_f32(),
|
px: px.to_f32(),
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rel(rel: impl UiNum) -> Self {
|
pub fn rel(rel: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
abs: 0.0,
|
px: 0.0,
|
||||||
rel: rel.to_f32(),
|
rel: rel.to_f32(),
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rest(ratio: impl UiNum) -> Self {
|
pub fn rest(ratio: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
abs: 0.0,
|
px: 0.0,
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: ratio.to_f32(),
|
rest: ratio.to_f32(),
|
||||||
}
|
}
|
||||||
@@ -141,31 +141,31 @@ impl Len {
|
|||||||
pub mod len_fns {
|
pub mod len_fns {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn abs(abs: impl UiNum) -> Len {
|
pub fn px(px: impl UiNum) -> Len {
|
||||||
Len {
|
Len {
|
||||||
abs: abs.to_f32(),
|
px: px.to_f32(),
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rel(rel: impl UiNum) -> Len {
|
pub fn rel(rel: impl UiNum) -> Len {
|
||||||
Len {
|
Len {
|
||||||
abs: 0.0,
|
px: 0.0,
|
||||||
rel: rel.to_f32(),
|
rel: rel.to_f32(),
|
||||||
rest: 0.0,
|
rest: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rest(ratio: impl UiNum) -> Len {
|
pub fn rest(ratio: impl UiNum) -> Len {
|
||||||
Len {
|
Len {
|
||||||
abs: 0.0,
|
px: 0.0,
|
||||||
rel: 0.0,
|
rel: 0.0,
|
||||||
rest: ratio.to_f32(),
|
rest: ratio.to_f32(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_op!(Len Add add; abs rel rest);
|
impl_op!(Len Add add; px rel rest);
|
||||||
impl_op!(Len Sub sub; abs rel rest);
|
impl_op!(Len Sub sub; px rel rest);
|
||||||
|
|
||||||
impl_op!(Size Add add; x y);
|
impl_op!(Size Add add; x y);
|
||||||
impl_op!(Size Sub sub; x y);
|
impl_op!(Size Sub sub; x y);
|
||||||
@@ -184,8 +184,8 @@ impl std::fmt::Display for Size {
|
|||||||
|
|
||||||
impl std::fmt::Display for Len {
|
impl std::fmt::Display for Len {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
if self.abs != 0.0 {
|
if self.px != 0.0 {
|
||||||
write!(f, "{} abs;", self.abs)?;
|
write!(f, "{} px;", self.px)?;
|
||||||
}
|
}
|
||||||
if self.rel != 0.0 {
|
if self.rel != 0.0 {
|
||||||
write!(f, "{} rel;", self.rel)?;
|
write!(f, "{} rel;", self.rel)?;
|
||||||
|
|||||||
+40
-67
@@ -23,11 +23,11 @@ impl UiVec2 {
|
|||||||
Self { x, y }
|
Self { x, y }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn abs(abs: impl const Into<Vec2>) -> Self {
|
pub const fn px(px: impl const Into<Vec2>) -> Self {
|
||||||
let abs = abs.into();
|
let px = px.into();
|
||||||
Self {
|
Self {
|
||||||
x: UiScalar::abs(abs.x),
|
x: UiScalar::px(px.x),
|
||||||
y: UiScalar::abs(abs.y),
|
y: UiScalar::px(px.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,13 +56,6 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn outside(&self, region: &UiRegion) -> UiVec2 {
|
|
||||||
UiVec2 {
|
|
||||||
x: self.x.outside(®ion.x),
|
|
||||||
y: self.y.outside(®ion.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,
|
||||||
@@ -77,10 +70,10 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_abs(&self, rel: Vec2) -> Vec2 {
|
pub fn to_px(&self, rel: Vec2) -> Vec2 {
|
||||||
Vec2 {
|
Vec2 {
|
||||||
x: self.x.to_abs(rel.x),
|
x: self.x.to_px(rel.x),
|
||||||
y: self.y.to_abs(rel.y),
|
y: self.y.to_px(rel.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +92,8 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_abs(&self) -> Vec2 {
|
pub fn get_px(&self) -> Vec2 {
|
||||||
(self.x.abs, self.y.abs).into()
|
(self.x.px, self.y.px).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rel(&self) -> Vec2 {
|
pub fn get_rel(&self) -> Vec2 {
|
||||||
@@ -109,15 +102,15 @@ impl UiVec2 {
|
|||||||
|
|
||||||
pub fn abs_mut(&mut self) -> Vec2View<'_> {
|
pub fn abs_mut(&mut self) -> Vec2View<'_> {
|
||||||
Vec2View {
|
Vec2View {
|
||||||
x: &mut self.x.abs,
|
x: &mut self.x.px,
|
||||||
y: &mut self.y.abs,
|
y: &mut self.y.px,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for UiVec2 {
|
impl Display for UiVec2 {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "rel{};abs{}", self.get_rel(), self.get_abs())
|
write!(f, "rel{};px{}", self.get_rel(), self.get_px())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,8 +118,8 @@ impl_op!(UiVec2 Add add; x y);
|
|||||||
impl_op!(UiVec2 Sub sub; x y);
|
impl_op!(UiVec2 Sub sub; x y);
|
||||||
|
|
||||||
const impl From<Vec2> for UiVec2 {
|
const impl From<Vec2> for UiVec2 {
|
||||||
fn from(abs: Vec2) -> Self {
|
fn from(px: Vec2) -> Self {
|
||||||
Self::abs(abs)
|
Self::px(px)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +127,8 @@ const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
|
|||||||
where
|
where
|
||||||
(T, U): const Destruct,
|
(T, U): const Destruct,
|
||||||
{
|
{
|
||||||
fn from(abs: (T, U)) -> Self {
|
fn from(px: (T, U)) -> Self {
|
||||||
Self::abs(abs)
|
Self::px(px)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,34 +136,34 @@ where
|
|||||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
|
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
|
||||||
pub struct UiScalar {
|
pub struct UiScalar {
|
||||||
pub rel: f32,
|
pub rel: f32,
|
||||||
pub abs: f32,
|
pub px: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for UiScalar {}
|
impl Eq for UiScalar {}
|
||||||
impl Hash for UiScalar {
|
impl Hash for UiScalar {
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
state.write_u32(self.rel.to_bits());
|
state.write_u32(self.rel.to_bits());
|
||||||
state.write_u32(self.abs.to_bits());
|
state.write_u32(self.px.to_bits());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_op!(UiScalar Add add; rel abs);
|
impl_op!(UiScalar Add add; rel px);
|
||||||
impl_op!(UiScalar Sub sub; rel abs);
|
impl_op!(UiScalar Sub sub; rel px);
|
||||||
|
|
||||||
impl UiScalar {
|
impl UiScalar {
|
||||||
pub const ZERO: Self = Self { rel: 0.0, abs: 0.0 };
|
pub const ZERO: Self = Self { rel: 0.0, px: 0.0 };
|
||||||
pub const FULL: Self = Self { rel: 1.0, abs: 0.0 };
|
pub const FULL: Self = Self { rel: 1.0, px: 0.0 };
|
||||||
|
|
||||||
pub const fn new(rel: f32, abs: f32) -> Self {
|
pub const fn new(rel: f32, px: f32) -> Self {
|
||||||
Self { rel, abs }
|
Self { rel, px }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel(rel: f32) -> Self {
|
pub const fn rel(rel: f32) -> Self {
|
||||||
Self { rel, abs: 0.0 }
|
Self { rel, px: 0.0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn abs(abs: f32) -> Self {
|
pub const fn px(px: f32) -> Self {
|
||||||
Self { rel: 0.0, abs }
|
Self { rel: 0.0, px }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel_min() -> Self {
|
pub const fn rel_min() -> Self {
|
||||||
@@ -184,37 +177,31 @@ impl UiScalar {
|
|||||||
pub const fn max(&self, other: Self) -> Self {
|
pub const fn max(&self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rel: self.rel.max(other.rel),
|
rel: self.rel.max(other.rel),
|
||||||
abs: self.abs.max(other.abs),
|
px: self.px.max(other.px),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn min(&self, other: Self) -> Self {
|
pub const fn min(&self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rel: self.rel.min(other.rel),
|
rel: self.rel.min(other.rel),
|
||||||
abs: self.abs.min(other.abs),
|
px: self.px.min(other.px),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn offset(mut self, amt: f32) -> Self {
|
pub const fn offset(mut self, amt: f32) -> Self {
|
||||||
self.abs += amt;
|
self.px += amt;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn within(&self, span: &UiSpan) -> Self {
|
pub const fn within(&self, span: &UiSpan) -> Self {
|
||||||
let anchor = self.rel.lerp(span.start.rel, span.end.rel);
|
let anchor = self.rel.lerp(span.start.rel, span.end.rel);
|
||||||
let offset = self.abs + self.rel.lerp(span.start.abs, span.end.abs);
|
let offset = self.px + self.rel.lerp(span.start.px, span.end.px);
|
||||||
Self {
|
Self {
|
||||||
rel: anchor,
|
rel: anchor,
|
||||||
abs: offset,
|
px: offset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -228,15 +215,15 @@ impl UiScalar {
|
|||||||
|
|
||||||
pub const fn flip(&mut self) {
|
pub const fn flip(&mut self) {
|
||||||
self.rel = 1.0 - self.rel;
|
self.rel = 1.0 - self.rel;
|
||||||
self.abs = -self.abs;
|
self.px = -self.px;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn to(&self, end: Self) -> UiSpan {
|
pub const fn to(&self, end: Self) -> UiSpan {
|
||||||
UiSpan { start: *self, end }
|
UiSpan { start: *self, end }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn to_abs(&self, rel: f32) -> f32 {
|
pub const fn to_px(&self, rel: f32) -> f32 {
|
||||||
self.rel * rel + self.abs
|
self.rel * rel + self.px
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +255,7 @@ impl UiSpan {
|
|||||||
self.start.flip();
|
self.start.flip();
|
||||||
self.end.flip();
|
self.end.flip();
|
||||||
std::mem::swap(&mut self.start.rel, &mut self.end.rel);
|
std::mem::swap(&mut self.start.rel, &mut self.end.rel);
|
||||||
std::mem::swap(&mut self.start.abs, &mut self.end.abs);
|
std::mem::swap(&mut self.start.px, &mut self.end.px);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn shift(&mut self, offset: UiScalar) {
|
pub const fn shift(&mut self, offset: UiScalar) {
|
||||||
@@ -283,13 +270,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -324,14 +304,7 @@ impl UiRegion {
|
|||||||
y: self.y.within(&parent.y),
|
y: self.y.within(&parent.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub const fn outside(&self, parent: &Self) -> Self {
|
pub const fn axis(&self, axis: Axis) -> &UiSpan {
|
||||||
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,
|
||||||
@@ -365,8 +338,8 @@ impl UiRegion {
|
|||||||
|
|
||||||
pub fn to_px(&self, size: Vec2) -> PixelRegion {
|
pub fn to_px(&self, size: Vec2) -> PixelRegion {
|
||||||
PixelRegion {
|
PixelRegion {
|
||||||
top_left: self.top_left().get_rel() * size + self.top_left().get_abs(),
|
top_left: self.top_left().get_rel() * size + self.top_left().get_px(),
|
||||||
bot_right: self.bot_right().get_rel() * size + self.bot_right().get_abs(),
|
bot_right: self.bot_right().get_rel() * size + self.bot_right().get_px(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +394,7 @@ impl Display for UiRegion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct PixelRegion {
|
pub struct PixelRegion {
|
||||||
pub top_left: Vec2,
|
pub top_left: Vec2,
|
||||||
pub bot_right: Vec2,
|
pub bot_right: Vec2,
|
||||||
|
|||||||
+154
-11
@@ -1,3 +1,5 @@
|
|||||||
|
#[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,
|
||||||
};
|
};
|
||||||
@@ -5,7 +7,10 @@ use parley::{
|
|||||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
||||||
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||||
};
|
};
|
||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
|
};
|
||||||
use swash::{
|
use swash::{
|
||||||
FontRef,
|
FontRef,
|
||||||
scale::{Render, ScaleContext, Source, StrikeWith},
|
scale::{Render, ScaleContext, Source, StrikeWith},
|
||||||
@@ -17,8 +22,32 @@ pub struct TextData {
|
|||||||
pub layout_ctx: LayoutContext<UiColor>,
|
pub layout_ctx: LayoutContext<UiColor>,
|
||||||
scale_ctx: ScaleContext,
|
scale_ctx: ScaleContext,
|
||||||
pub atlas: GlyphAtlas,
|
pub atlas: GlyphAtlas,
|
||||||
|
spare: VecDeque<Placed>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The glyphs of one text at one width. A buffer holds the ones it is drawn
|
||||||
|
/// as; these are the ones it had before, kept because a container measures a
|
||||||
|
/// child by drawing it in a box it may not keep, and so comes back to widths
|
||||||
|
/// it has already asked for.
|
||||||
|
struct Placed {
|
||||||
|
/// Where the glyphs land is a function of these three and nothing else,
|
||||||
|
/// so no widget or buffer identity is involved and two texts of the same
|
||||||
|
/// words share an answer.
|
||||||
|
text: String,
|
||||||
|
key: LayoutKey,
|
||||||
|
glyphs: RenderedText,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many to keep. Bounding the whole store rather than each buffer is what
|
||||||
|
/// makes this a fixed cost instead of one a tree of ten thousand texts pays
|
||||||
|
/// ten thousand times; the re-asks come from laying out one subtree, so they
|
||||||
|
/// are close together and few are needed. Instructions over 500 resize frames
|
||||||
|
/// of `tests/revision_cost.rs`, both the repeating widths and the sweep that
|
||||||
|
/// cannot hit across frames: 13.7B at 32, 12.1B at 64, 10.4B and 12.1B at 128,
|
||||||
|
/// and nothing past that -- so 128, which is no worse in the case that never
|
||||||
|
/// repeats and better in the one that does.
|
||||||
|
const SPARE_PLACED: usize = 128;
|
||||||
|
|
||||||
impl Default for TextData {
|
impl Default for TextData {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -26,6 +55,7 @@ impl Default for TextData {
|
|||||||
layout_ctx: LayoutContext::new(),
|
layout_ctx: LayoutContext::new(),
|
||||||
scale_ctx: ScaleContext::new(),
|
scale_ctx: ScaleContext::new(),
|
||||||
atlas: GlyphAtlas::default(),
|
atlas: GlyphAtlas::default(),
|
||||||
|
spare: VecDeque::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,11 +106,21 @@ impl Default for TextAttrs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How far below the longest line a width may fall and still be answered by
|
||||||
|
/// the break in hand. A parent that offers a child the length it reported
|
||||||
|
/// composes that length back through the box chain, so the two differ in the
|
||||||
|
/// last bits -- and at exactly the longest line, that decides whether a line
|
||||||
|
/// fits. Sub-pixel, so no break it admits is one a reader could see.
|
||||||
|
const BREAK_EPSILON_PX: f32 = 0.05;
|
||||||
|
|
||||||
/// Keeps text and its corresponding layout from getting out of sync.
|
/// Keeps text and its corresponding layout from getting out of sync.
|
||||||
pub struct TextBuffer {
|
pub struct TextBuffer {
|
||||||
text: String,
|
text: String,
|
||||||
layout: Layout<UiColor>,
|
layout: Layout<UiColor>,
|
||||||
layout_key: Option<LayoutKey>,
|
layout_key: Option<LayoutKey>,
|
||||||
|
/// The glyphs placed from `layout`, so drawing this text again at the
|
||||||
|
/// width it already has places them once.
|
||||||
|
placed: Option<RenderedText>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
@@ -95,6 +135,7 @@ impl TextBuffer {
|
|||||||
text: text.into(),
|
text: text.into(),
|
||||||
layout: Layout::new(),
|
layout: Layout::new(),
|
||||||
layout_key: None,
|
layout_key: None,
|
||||||
|
placed: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,15 +160,28 @@ impl TextBuffer {
|
|||||||
if text != self.text {
|
if text != self.text {
|
||||||
self.text = text;
|
self.text = text;
|
||||||
self.layout_key = None;
|
self.layout_key = None;
|
||||||
|
self.placed = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Invalidates the layout and returns the underlying string for editing.
|
/// Invalidates the layout and returns the underlying string for editing.
|
||||||
pub fn edit(&mut self) -> &mut String {
|
pub fn edit(&mut self) -> &mut String {
|
||||||
self.layout_key = None;
|
self.layout_key = None;
|
||||||
|
self.placed = None;
|
||||||
&mut self.text
|
&mut self.text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The glyphs of the shaping it is drawn as, once they are placed.
|
||||||
|
pub fn rendered(&self) -> Option<&RenderedText> {
|
||||||
|
self.placed.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The width its shaping wraps at, and `None` where it does not wrap or
|
||||||
|
/// has not been shaped.
|
||||||
|
pub fn wrap_width(&self) -> Option<f32> {
|
||||||
|
self.layout_key.as_ref()?.max_width
|
||||||
|
}
|
||||||
|
|
||||||
pub fn size(&self) -> Vec2 {
|
pub fn size(&self) -> Vec2 {
|
||||||
Vec2::new(self.layout.width(), self.layout.height())
|
Vec2::new(self.layout.width(), self.layout.height())
|
||||||
}
|
}
|
||||||
@@ -138,8 +192,58 @@ impl TextBuffer {
|
|||||||
max_width: width,
|
max_width: width,
|
||||||
};
|
};
|
||||||
if self.layout_key.as_ref() == Some(&layout_key) {
|
if self.layout_key.as_ref() == Some(&layout_key) {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::TextShapeHits);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// A greedy break at one width is the same break at every width down
|
||||||
|
// to the longest line it produced: each line still fits, and none can
|
||||||
|
// take a word that would not fit in the wider box. So the layout in
|
||||||
|
// hand already answers, and re-breaking would only be a chance to
|
||||||
|
// disagree with itself -- which is what happens when a parent offers
|
||||||
|
// a child the length that child just reported, and the two land
|
||||||
|
// either side of a float.
|
||||||
|
if let Some(key) = &self.layout_key
|
||||||
|
&& key.attrs == *attrs
|
||||||
|
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
||||||
|
&& want <= broke_at
|
||||||
|
&& want + BREAK_EPSILON_PX >= self.layout.width()
|
||||||
|
{
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::TextShapeHits);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let same_shaping = self
|
||||||
|
.layout_key
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|key| key.attrs == *attrs);
|
||||||
|
let old_key = self.layout_key.replace(layout_key);
|
||||||
|
// The glyphs it holds are of the width it held, which the layout may
|
||||||
|
// well come back to.
|
||||||
|
if let Some(key) = old_key
|
||||||
|
&& let Some(glyphs) = self.placed.take()
|
||||||
|
{
|
||||||
|
data.keep_placed(Placed {
|
||||||
|
text: self.text.clone(),
|
||||||
|
key,
|
||||||
|
glyphs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Only the line breaking depends on the width: the shaped runs under
|
||||||
|
// it are a function of the text and the attrs, and parley re-breaks
|
||||||
|
// them in place. So a new width is a break, not a shaping.
|
||||||
|
if same_shaping {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::TextBreaks);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _break = diag::timer(TimerKind::TextBreak);
|
||||||
|
self.break_lines(width);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::TextShapes);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _shape = diag::timer(TimerKind::TextShape);
|
||||||
let mut builder = data
|
let mut builder = data
|
||||||
.layout_ctx
|
.layout_ctx
|
||||||
.ranged_builder(&mut data.font_ctx, &self.text, 1.0, true);
|
.ranged_builder(&mut data.font_ctx, &self.text, 1.0, true);
|
||||||
@@ -150,10 +254,13 @@ impl TextBuffer {
|
|||||||
)));
|
)));
|
||||||
builder.push_default(StyleProperty::Brush(attrs.color));
|
builder.push_default(StyleProperty::Brush(attrs.color));
|
||||||
builder.build_into(&mut self.layout, &self.text);
|
builder.build_into(&mut self.layout, &self.text);
|
||||||
|
self.break_lines(width);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn break_lines(&mut self, width: Option<f32>) {
|
||||||
self.layout.break_all_lines(width);
|
self.layout.break_all_lines(width);
|
||||||
self.layout
|
self.layout
|
||||||
.align(Alignment::Start, AlignmentOptions::default());
|
.align(Alignment::Start, AlignmentOptions::default());
|
||||||
self.layout_key = Some(layout_key);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,18 +372,54 @@ pub struct RenderedText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TextData {
|
impl TextData {
|
||||||
pub fn render(
|
/// The glyphs of this text at this width, taken out of what is kept.
|
||||||
|
fn take_placed(&mut self, text: &str, key: &LayoutKey) -> Option<RenderedText> {
|
||||||
|
// From the newest, since a re-ask is usually of something recent.
|
||||||
|
let at = self
|
||||||
|
.spare
|
||||||
|
.iter()
|
||||||
|
.rposition(|spare| spare.key == *key && spare.text == text)?;
|
||||||
|
self.spare.remove(at).map(|spare| spare.glyphs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keep_placed(&mut self, placed: Placed) {
|
||||||
|
if self.spare.len() >= SPARE_PLACED {
|
||||||
|
self.spare.pop_front();
|
||||||
|
}
|
||||||
|
self.spare.push_back(placed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<'b>(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &mut TextBuffer,
|
buffer: &'b mut TextBuffer,
|
||||||
attrs: &TextAttrs,
|
attrs: &TextAttrs,
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
) -> RenderedText {
|
) -> &'b 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 = self.place(buffer);
|
// Only asked for when the buffer no longer holds them: taking one out
|
||||||
RenderedText {
|
// of the store to then drop it would throw an answer away.
|
||||||
glyphs,
|
let placed = buffer.placed.take().or_else(|| {
|
||||||
size: buffer.size(),
|
let key = buffer.layout_key.as_ref()?;
|
||||||
color: attrs.color,
|
self.take_placed(&buffer.text, key)
|
||||||
}
|
});
|
||||||
|
let placed = match placed {
|
||||||
|
Some(placed) => placed,
|
||||||
|
None => {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::GlyphPlacements);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _place = diag::timer(TimerKind::GlyphPlacement);
|
||||||
|
RenderedText {
|
||||||
|
glyphs: self.place(buffer),
|
||||||
|
size: buffer.size(),
|
||||||
|
color: attrs.color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
buffer.placed.insert(placed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+46
-4
@@ -1,11 +1,10 @@
|
|||||||
use crate::{UiRegion, util::Id};
|
use crate::{UiRegion, util::Id, util::Vec2};
|
||||||
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 width: f32,
|
pub dim: Vec2,
|
||||||
pub height: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -13,15 +12,17 @@ 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; 5] = vertex_attr_array![
|
const ATTRIBS: [VertexAttribute; 6] = 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> {
|
||||||
@@ -43,4 +44,45 @@ 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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+81
-18
@@ -17,11 +17,15 @@ mod texture;
|
|||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
pub use atlas::*;
|
pub use atlas::*;
|
||||||
pub use data::{Mask, MaskIdx};
|
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset};
|
||||||
pub use primitive::*;
|
pub use primitive::*;
|
||||||
|
|
||||||
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||||
|
|
||||||
|
fn module_source(wgsl: &str) -> String {
|
||||||
|
format!("{PRELUDE}\n{wgsl}")
|
||||||
|
}
|
||||||
|
|
||||||
pub struct UiRenderNode {
|
pub struct UiRenderNode {
|
||||||
shared_layout: BindGroupLayout,
|
shared_layout: BindGroupLayout,
|
||||||
shared_group: BindGroup,
|
shared_group: BindGroup,
|
||||||
@@ -34,6 +38,7 @@ 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 {
|
||||||
@@ -127,32 +132,35 @@ 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;
|
||||||
if self.masks.update(device, queue, &ui.masks[..]) {
|
regroup |= self.masks.update(device, queue, &ui.masks[..]);
|
||||||
self.shared_group = Self::shared_group(
|
}
|
||||||
device,
|
if ui_render.moves.changed {
|
||||||
&self.shared_layout,
|
ui_render.moves.changed = false;
|
||||||
&self.window_buffer,
|
regroup |= self.moves.update(device, queue, ui_render.moves.entries());
|
||||||
&self.masks,
|
}
|
||||||
);
|
if regroup {
|
||||||
}
|
self.shared_group = Self::shared_group(
|
||||||
|
device,
|
||||||
|
&self.shared_layout,
|
||||||
|
&self.window_buffer,
|
||||||
|
&self.masks,
|
||||||
|
&self.moves,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
||||||
let size = size.into();
|
let size = size.into();
|
||||||
let slice = &[WindowUniform {
|
let slice = &[WindowUniform { dim: size }];
|
||||||
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 {
|
||||||
width: config.width as f32,
|
dim: Vec2::new(config.width as f32, config.height 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"),
|
||||||
@@ -166,7 +174,13 @@ impl UiRenderNode {
|
|||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||||
"ui masks",
|
"ui masks",
|
||||||
);
|
);
|
||||||
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
|
let moves = ArrBuf::new(
|
||||||
|
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,
|
||||||
@@ -177,6 +191,7 @@ impl UiRenderNode {
|
|||||||
layers: HashMap::default(),
|
layers: HashMap::default(),
|
||||||
active: Vec::new(),
|
active: Vec::new(),
|
||||||
masks,
|
masks,
|
||||||
|
moves,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +226,7 @@ impl UiRenderNode {
|
|||||||
) -> RenderPipeline {
|
) -> RenderPipeline {
|
||||||
let module = device.create_shader_module(ShaderModuleDescriptor {
|
let module = device.create_shader_module(ShaderModuleDescriptor {
|
||||||
label: Some(label),
|
label: Some(label),
|
||||||
source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()),
|
source: ShaderSource::Wgsl(module_source(wgsl).into()),
|
||||||
});
|
});
|
||||||
device.create_render_pipeline(&RenderPipelineDescriptor {
|
device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||||
label: Some(label),
|
label: Some(label),
|
||||||
@@ -252,7 +267,8 @@ impl UiRenderNode {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What every draw in the ui is given: the window and the masks.
|
/// What every draw in the ui is given: the window, the masks and the
|
||||||
|
/// 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: &[
|
||||||
@@ -276,6 +292,16 @@ 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"),
|
||||||
})
|
})
|
||||||
@@ -286,6 +312,7 @@ 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,
|
||||||
@@ -298,6 +325,10 @@ 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"),
|
||||||
})
|
})
|
||||||
@@ -374,3 +405,35 @@ impl ListBuffers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::module_source;
|
||||||
|
use wgpu::naga::{
|
||||||
|
front::wgsl,
|
||||||
|
valid::{Capabilities, ValidationFlags, Validator},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Every shader file, composed as the renderer composes it, parses and
|
||||||
|
/// validates with no device -- so an edit that breaks one fails here and
|
||||||
|
/// not in the first window opened.
|
||||||
|
#[test]
|
||||||
|
fn every_shader_validates() {
|
||||||
|
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src/render/shader");
|
||||||
|
let mut checked = 0;
|
||||||
|
for entry in std::fs::read_dir(dir).unwrap() {
|
||||||
|
let path = entry.unwrap().path();
|
||||||
|
if path.extension().is_none_or(|e| e != "wgsl") || path.ends_with("prelude.wgsl") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let source = module_source(&std::fs::read_to_string(&path).unwrap());
|
||||||
|
let module = wgsl::parse_str(&source)
|
||||||
|
.unwrap_or_else(|e| panic!("{}: {}", path.display(), e.emit_to_string(&source)));
|
||||||
|
Validator::new(ValidationFlags::all(), Capabilities::all())
|
||||||
|
.validate(&module)
|
||||||
|
.unwrap_or_else(|e| panic!("{}: {e:?}", path.display()));
|
||||||
|
checked += 1;
|
||||||
|
}
|
||||||
|
assert!(checked > 0, "no shaders found in {dir}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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, PrimitiveInstance},
|
data::{MaskIdx, MoveIdx, PrimitiveInstance},
|
||||||
page::GlyphRender,
|
page::GlyphRender,
|
||||||
texture::ImageRender,
|
texture::ImageRender,
|
||||||
},
|
},
|
||||||
@@ -246,6 +246,7 @@ impl LayerDraws {
|
|||||||
primitive,
|
primitive,
|
||||||
region,
|
region,
|
||||||
mask_idx,
|
mask_idx,
|
||||||
|
move_idx,
|
||||||
}: PrimitiveInst<P>,
|
}: PrimitiveInst<P>,
|
||||||
) -> PrimitiveHandle {
|
) -> PrimitiveHandle {
|
||||||
self.updated = true;
|
self.updated = true;
|
||||||
@@ -258,7 +259,11 @@ impl LayerDraws {
|
|||||||
.get_or_insert_with(InstanceList::new::<P>)
|
.get_or_insert_with(InstanceList::new::<P>)
|
||||||
.push(
|
.push(
|
||||||
id,
|
id,
|
||||||
PrimitiveInstance { region, mask_idx },
|
PrimitiveInstance {
|
||||||
|
region,
|
||||||
|
mask_idx,
|
||||||
|
move_idx,
|
||||||
|
},
|
||||||
bytemuck::bytes_of(&primitive),
|
bytemuck::bytes_of(&primitive),
|
||||||
);
|
);
|
||||||
PrimitiveHandle {
|
PrimitiveHandle {
|
||||||
@@ -304,6 +309,7 @@ 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 {
|
||||||
@@ -347,7 +353,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, align(8))]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct GlyphPrimitive {
|
pub struct GlyphPrimitive {
|
||||||
pub uv_min: Vec2,
|
pub uv_min: Vec2,
|
||||||
@@ -358,8 +364,8 @@ pub struct GlyphPrimitive {
|
|||||||
pub flags: u32,
|
pub flags: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual rather than derived: the align(8) leaves four bytes of padding, which
|
// Manual rather than derived: `Vec2`'s alignment leaves four bytes of padding
|
||||||
// is how WGSL lays the struct out.
|
// here, which 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 {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
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>,
|
||||||
@@ -15,6 +17,52 @@ 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;
|
||||||
|
|
||||||
|
// Written the way `UiScalar::within` writes it rather than as `mix`, so the
|
||||||
|
// CPU and the shader compose a position with the same arithmetic and answer
|
||||||
|
// the same thing about where a widget is.
|
||||||
|
fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar {
|
||||||
|
return UiScalar(
|
||||||
|
p.start.rel + (p.end.rel - p.start.rel) * s.rel,
|
||||||
|
s.px + (p.start.px + (p.end.px - p.start.px) * s.rel),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span_within(s: UiSpan, p: UiSpan) -> UiSpan {
|
||||||
|
return UiSpan(scalar_within(s.start, p), scalar_within(s.end, p));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_move(idx: u32, local: Region) -> Region {
|
||||||
|
var r = local;
|
||||||
|
var at = idx;
|
||||||
|
for (var step = 0u; step < CHAIN_LIMIT; step++) {
|
||||||
|
if at == MOVE_NONE {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let entry = move_offsets[at];
|
||||||
|
r = Region(span_within(r.x, entry.x), span_within(r.y, entry.y));
|
||||||
|
at = entry.parent;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UiSpan {
|
struct UiSpan {
|
||||||
@@ -24,7 +72,7 @@ struct UiSpan {
|
|||||||
|
|
||||||
struct UiScalar {
|
struct UiScalar {
|
||||||
rel: f32,
|
rel: f32,
|
||||||
abs: f32,
|
px: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InstanceInput {
|
struct InstanceInput {
|
||||||
@@ -33,6 +81,7 @@ 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 {
|
||||||
@@ -52,13 +101,18 @@ fn vs_main(
|
|||||||
) -> VertexOutput {
|
) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
|
|
||||||
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
|
let local = Region(
|
||||||
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
|
UiSpan(UiScalar(in.x_start.x, in.x_start.y), UiScalar(in.x_end.x, in.x_end.y)),
|
||||||
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
|
UiSpan(UiScalar(in.y_start.x, in.y_start.y), UiScalar(in.y_end.x, in.y_end.y)),
|
||||||
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
|
);
|
||||||
|
let r = resolve_move(in.move_idx, local);
|
||||||
|
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
|
||||||
|
let top_left_px = vec2(r.x.start.px, r.y.start.px);
|
||||||
|
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
|
||||||
|
let bot_right_px = vec2(r.x.end.px, r.y.end.px);
|
||||||
|
|
||||||
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
let top_left = floor(top_left_rel * window.dim) + floor(top_left_px);
|
||||||
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_px);
|
||||||
let size = bot_right - top_left;
|
let size = bot_right - top_left;
|
||||||
|
|
||||||
let uv = vec2<f32>(
|
let uv = vec2<f32>(
|
||||||
@@ -81,13 +135,16 @@ 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];
|
||||||
let tl = vec2(mask.x.start.rel, mask.y.start.rel);
|
// Its own chain, not the drawn primitive's, so a stationary viewport
|
||||||
let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
|
// clips content that moves inside it.
|
||||||
let br = vec2(mask.x.end.rel, mask.y.end.rel);
|
let m = resolve_move(mask.move_idx, Region(mask.x, mask.y));
|
||||||
let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
|
let tl = vec2(m.x.start.rel, m.y.start.rel);
|
||||||
|
let tl_px = vec2(m.x.start.px, m.y.start.px);
|
||||||
|
let br = vec2(m.x.end.rel, m.y.end.rel);
|
||||||
|
let br_px = vec2(m.x.end.px, m.y.end.px);
|
||||||
|
|
||||||
let top_left = floor(tl * window.dim) + floor(tl_abs);
|
let top_left = floor(tl * window.dim) + floor(tl_px);
|
||||||
let bot_right = floor(br * window.dim) + floor(br_abs);
|
let bot_right = floor(br * window.dim) + floor(br_px);
|
||||||
let pos = in.clip_position.xy;
|
let pos = in.clip_position.xy;
|
||||||
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
||||||
return color * 0.0;
|
return color * 0.0;
|
||||||
|
|||||||
+32
-1
@@ -1,14 +1,45 @@
|
|||||||
use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId};
|
use crate::{
|
||||||
|
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,
|
||||||
|
/// The pixel size of the box its parent first asked about it in, before
|
||||||
|
/// knowing what it came to. `px` may be a box derived from that answer,
|
||||||
|
/// and a size measured there is only the same answer asked again.
|
||||||
|
pub offered_px: Vec2,
|
||||||
pub parent: Option<WidgetId>,
|
pub parent: Option<WidgetId>,
|
||||||
|
/// How far down the tree it was drawn, the root being 1. Carried down a
|
||||||
|
/// draw rather than worked out by walking up, so it is right for every
|
||||||
|
/// widget a frame visits and cannot drift while one is being drawn.
|
||||||
|
pub depth: usize,
|
||||||
pub textures: Vec<TextureHandle>,
|
pub textures: Vec<TextureHandle>,
|
||||||
pub primitives: Vec<PrimitiveHandle>,
|
pub primitives: Vec<PrimitiveHandle>,
|
||||||
pub children: Vec<WidgetId>,
|
pub children: Vec<WidgetId>,
|
||||||
|
/// The children whose size this widget read while drawing.
|
||||||
|
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,
|
||||||
|
/// The slot its primitives are positioned through: its own if its parent
|
||||||
|
/// placed it, otherwise the nearest ancestor that has one.
|
||||||
|
pub move_idx: MoveIdx,
|
||||||
|
/// The slot `region` is given in, which is whatever its parent drew in.
|
||||||
|
pub parent_move: MoveIdx,
|
||||||
pub mask: MaskIdx,
|
pub mask: MaskIdx,
|
||||||
pub layer: LayerId,
|
pub layer: LayerId,
|
||||||
}
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+90
-4
@@ -1,17 +1,21 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget,
|
||||||
|
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 {
|
||||||
@@ -23,6 +27,88 @@ 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;
|
||||||
|
|||||||
+250
-34
@@ -1,9 +1,11 @@
|
|||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
use crate::layout_diagnostics::{self as diag, Counter};
|
||||||
use crate::{
|
use crate::{
|
||||||
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
|
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
|
||||||
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
|
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
|
||||||
render::{
|
render::{
|
||||||
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
|
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
||||||
TexturePrimitive,
|
PrimitiveKind, TexturePrimitive,
|
||||||
},
|
},
|
||||||
util::Vec2,
|
util::Vec2,
|
||||||
};
|
};
|
||||||
@@ -13,12 +15,25 @@ pub struct Painter<'a> {
|
|||||||
pub(super) state: &'a mut UiRenderState,
|
pub(super) state: &'a mut UiRenderState,
|
||||||
pub(super) rsc: &'a mut dyn UiRsc,
|
pub(super) rsc: &'a mut dyn UiRsc,
|
||||||
|
|
||||||
|
/// This widget's box, in the coordinates of `move_idx`.
|
||||||
pub(super) region: UiRegion,
|
pub(super) region: UiRegion,
|
||||||
pub(super) mask: MaskIdx,
|
pub(super) mask: MaskIdx,
|
||||||
pub(super) textures: Vec<TextureHandle>,
|
pub(super) textures: Vec<TextureHandle>,
|
||||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||||
pub(super) children: Vec<WidgetId>,
|
pub(super) children: Vec<WidgetId>,
|
||||||
|
/// The children asked about so far, so the first box each was asked
|
||||||
|
/// about is the one recorded as its offer.
|
||||||
|
pub(super) offered: Vec<WidgetId>,
|
||||||
|
/// The children whose size this widget read while drawing.
|
||||||
|
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],
|
||||||
|
/// The slot this widget's primitives are positioned through: its own if
|
||||||
|
/// its parent placed it, otherwise the nearest ancestor that has one.
|
||||||
|
pub(super) move_idx: MoveIdx,
|
||||||
pub layer: usize,
|
pub layer: usize,
|
||||||
|
pub(super) depth: usize,
|
||||||
pub(super) id: WidgetId,
|
pub(super) id: WidgetId,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +45,8 @@ 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 {
|
||||||
@@ -38,6 +55,7 @@ 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);
|
||||||
@@ -64,39 +82,192 @@ 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 { region });
|
self.mask = self.rsc.ui_mut().masks.push(Mask {
|
||||||
|
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<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
|
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
||||||
self.widget_at(id, self.region);
|
self.widget_at(id, self.region, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a widget somewhere within this one.
|
/// Draws a widget somewhere within this one.
|
||||||
/// Useful for drawing child widgets in select areas.
|
pub fn widget_within<'s, W: ?Sized>(
|
||||||
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
|
&'s mut self,
|
||||||
self.widget_at(id, region.within(&self.region));
|
id: &'s StrongWidget<W>,
|
||||||
|
region: UiRegion,
|
||||||
|
) -> DrawResult<'s, 'a, W> {
|
||||||
|
let region = region.within(&self.region);
|
||||||
|
self.widget_at(id, region, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
|
/// Draws a child this widget decides the box of, and may decide again
|
||||||
self.children.push(id.id());
|
/// once it knows what the child came to. The child gets a slot of its
|
||||||
self.state.draw_inner(
|
/// 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());
|
||||||
|
}
|
||||||
|
let size = self.state.draw_inner(
|
||||||
self.layer,
|
self.layer,
|
||||||
id.id(),
|
id.id(),
|
||||||
region,
|
region,
|
||||||
Some(self.id),
|
Some(self.id),
|
||||||
|
self.depth + 1,
|
||||||
|
self.move_idx,
|
||||||
|
slotted,
|
||||||
self.mask,
|
self.mask,
|
||||||
None,
|
None,
|
||||||
self.rsc,
|
self.rsc,
|
||||||
);
|
);
|
||||||
|
self.offer(id.id(), region);
|
||||||
|
DrawResult {
|
||||||
|
child: id,
|
||||||
|
painter: self,
|
||||||
|
size,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_text(
|
/// 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_hint(id);
|
||||||
|
Some(hint)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::HintMisses);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A retained child length valid under the region it is about to be
|
||||||
|
/// 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,
|
&mut self,
|
||||||
buffer: &mut TextBuffer,
|
child: &StrongWidget<W>,
|
||||||
|
axis: Axis,
|
||||||
|
region: UiRegion,
|
||||||
|
) -> Option<Len> {
|
||||||
|
let region = region.within(&self.region);
|
||||||
|
self.offer(child.id(), region);
|
||||||
|
if let Some(hint) = self.size_hint(child, axis) {
|
||||||
|
return Some(hint);
|
||||||
|
}
|
||||||
|
self.retained_size(child, region)
|
||||||
|
.map(|size| size.axis(axis))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `region` in this widget's own coordinates.
|
||||||
|
fn retained_size<W: ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
child: &StrongWidget<W>,
|
||||||
|
region: UiRegion,
|
||||||
|
) -> Option<Size> {
|
||||||
|
let (size, box_inputs, output_inputs) =
|
||||||
|
self.state
|
||||||
|
.retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?;
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::RetainedSizeHits);
|
||||||
|
self.depend_on_size_inputs(child, box_inputs, output_inputs);
|
||||||
|
Some(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records the box a child was first asked about in this draw. Any later
|
||||||
|
/// box this draw gives it was decided knowing its answer, so a size the
|
||||||
|
/// child measures there is not an answer to this widget's question.
|
||||||
|
fn offer(&mut self, child: WidgetId, region: UiRegion) {
|
||||||
|
if self.offered.contains(&child) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.offered.push(child);
|
||||||
|
let px = self.state.px_of(self.move_idx, region);
|
||||||
|
if let Some(active) = self.state.active.get_mut(&child) {
|
||||||
|
active.offered_px = px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Depends on a length the child gave without being drawn. A hint is
|
||||||
|
/// context-free, so this depends on the child but on no pixel axis.
|
||||||
|
fn depend_on_hint<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||||
|
self.depend_on_size_inputs(child, [false; 2], [false; 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Depends on a size the child produced by drawing, which carries
|
||||||
|
/// whatever the child read to produce it.
|
||||||
|
fn depend_on_drawn_size<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
|
||||||
|
let (box_inputs, output_inputs) = self
|
||||||
|
.state
|
||||||
|
.active
|
||||||
|
.get(&child.id())
|
||||||
|
.map_or(([false; 2], [false; 2]), |active| {
|
||||||
|
(active.size_box_inputs, active.size_output_inputs)
|
||||||
|
});
|
||||||
|
self.depend_on_size_inputs(child, box_inputs, output_inputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn depend_on_size_inputs<W: ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
child: &StrongWidget<W>,
|
||||||
|
box_inputs: [bool; 2],
|
||||||
|
output_inputs: [bool; 2],
|
||||||
|
) {
|
||||||
|
if !self.size_deps.contains(&child.id()) {
|
||||||
|
self.size_deps.push(child.id());
|
||||||
|
}
|
||||||
|
for (own, child) in self.size_box_inputs.iter_mut().zip(box_inputs) {
|
||||||
|
*own |= child;
|
||||||
|
}
|
||||||
|
for (own, child) in self.size_output_inputs.iter_mut().zip(output_inputs) {
|
||||||
|
*own |= child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_text<'b>(
|
||||||
|
&mut self,
|
||||||
|
buffer: &'b mut TextBuffer,
|
||||||
attrs: &TextAttrs,
|
attrs: &TextAttrs,
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
) -> RenderedText {
|
) -> &'b 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)
|
||||||
}
|
}
|
||||||
@@ -108,9 +279,9 @@ impl<'a> Painter<'a> {
|
|||||||
let mut region = origin;
|
let mut region = origin;
|
||||||
region.x.end = region.x.start;
|
region.x.end = region.x.start;
|
||||||
region.y.end = region.y.start;
|
region.y.end = region.y.start;
|
||||||
let mut region = region.offset(UiVec2::abs(glyph.offset));
|
let mut region = region.offset(UiVec2::px(glyph.offset));
|
||||||
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
|
region.x.end = region.x.start + UiScalar::px(glyph.entry.width as f32);
|
||||||
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
|
region.y.end = region.y.start + UiScalar::px(glyph.entry.height as f32);
|
||||||
self.write(
|
self.write(
|
||||||
kind,
|
kind,
|
||||||
GlyphPrimitive {
|
GlyphPrimitive {
|
||||||
@@ -125,27 +296,51 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
|
/// The output's size in pixels. A widget that reads it draws again when
|
||||||
self.size_ctx().size(id)
|
/// the output changes, since nothing else can put that right.
|
||||||
}
|
pub fn output_size(&mut self) -> Vec2 {
|
||||||
|
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.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.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_px(self.state.output_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One axis of this widget's box in pixels. Prefer this to
|
||||||
|
/// [`Self::px_size`] when the other axis cannot affect the reported size.
|
||||||
|
pub fn px_len(&mut self, axis: Axis) -> f32 {
|
||||||
|
self.size_box_inputs[axis as usize] = true;
|
||||||
|
self.px_len_for_draw(axis)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One axis of this widget's box in pixels, for a draw whose reported
|
||||||
|
/// size does not follow from it -- a clamp or a position. Nothing records
|
||||||
|
/// the read, so a size that does depend on it would go stale.
|
||||||
|
pub fn px_len_for_draw(&self, axis: Axis) -> f32 {
|
||||||
|
let region = self.state.moves.resolve(self.move_idx, self.region);
|
||||||
|
region
|
||||||
|
.size()
|
||||||
|
.axis(axis)
|
||||||
|
.to_px(self.state.output_size.axis(axis))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_data(&mut self) -> &mut TextData {
|
pub fn text_data(&mut self) -> &mut TextData {
|
||||||
@@ -167,9 +362,30 @@ impl<'a> Painter<'a> {
|
|||||||
pub fn id(&self) -> &WidgetId {
|
pub fn id(&self) -> &WidgetId {
|
||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn size_ctx(&mut self) -> SizeCtx<'_> {
|
/// A child that has just been drawn. Reading its size records that this
|
||||||
self.state.size_ctx(self.id, self.region.size(), self.rsc)
|
/// widget's own size depends on it; dropping it without reading draws the
|
||||||
|
/// 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_drawn_size(self.child);
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(self, axis: Axis) -> Len {
|
||||||
|
self.size().axis(axis)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+630
-83
@@ -1,19 +1,40 @@
|
|||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
||||||
use crate::{
|
use crate::{
|
||||||
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget,
|
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion,
|
||||||
UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
Size, StrongWidget, UiRegion, UiRsc, UiScalar, UiSpan, WidgetId, Widgets,
|
||||||
ui::cache::Cache,
|
util::{HashMap, HashSet, Vec2},
|
||||||
util::{HashMap, HashSet, Vec2, forget_ref},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
||||||
|
const LAYOUT_EPSILON_PX: f32 = 0.05;
|
||||||
|
|
||||||
|
fn pixel_len_changed(old: f32, new: f32) -> bool {
|
||||||
|
(old - new).abs() > LAYOUT_EPSILON_PX
|
||||||
|
}
|
||||||
|
|
||||||
pub struct UiRenderState {
|
pub struct UiRenderState {
|
||||||
pub active: HashMap<WidgetId, ActiveData>,
|
pub active: HashMap<WidgetId, ActiveData>,
|
||||||
pub layers: DrawLayers,
|
pub layers: DrawLayers,
|
||||||
pub(super) output_size: Vec2,
|
pub(super) output_size: Vec2,
|
||||||
pub cache: Cache,
|
|
||||||
|
|
||||||
old_root: Option<WidgetId>,
|
old_root: Option<WidgetId>,
|
||||||
resized: bool,
|
/// The slot every chain bottoms out in, holding the output as a box.
|
||||||
|
root_move: MoveIdx,
|
||||||
|
/// Widgets whose reported size depends on the root box rather than on
|
||||||
|
/// their own, so nothing below them changing length can reach them.
|
||||||
|
root_readers: HashSet<WidgetId>,
|
||||||
|
/// Content/state dirtiness whose retained size cannot answer a layout
|
||||||
|
/// question until that widget has drawn again.
|
||||||
|
invalid_sizes: HashSet<WidgetId>,
|
||||||
|
/// What has already been drawn during the pass under way, so a widget
|
||||||
|
/// reached by redrawing an ancestor is not drawn again on its own
|
||||||
|
/// account. Emptied when the pass ends.
|
||||||
draw_started: HashSet<WidgetId>,
|
draw_started: HashSet<WidgetId>,
|
||||||
|
/// A widget's move slot, which outlives any one `ActiveData`: a redraw
|
||||||
|
/// replaces that while its children go on pointing at the slot.
|
||||||
|
slots: HashMap<WidgetId, MoveIdx>,
|
||||||
|
pub moves: Moves,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiRenderState {
|
impl UiRenderState {
|
||||||
@@ -21,20 +42,64 @@ 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,
|
invalid_sizes: Default::default(),
|
||||||
draw_started: Default::default(),
|
draw_started: Default::default(),
|
||||||
|
slots: Default::default(),
|
||||||
|
moves: Default::default(),
|
||||||
|
root_move: MoveIdx::NONE,
|
||||||
|
root_readers: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The window as a box, so a chain bottoms out in one rather than in a
|
||||||
|
/// multiplication applied after it. Composing through a box held in
|
||||||
|
/// pixels leaves everything below it in pixels, which is why nothing
|
||||||
|
/// downstream has to know the output's size to resolve a position.
|
||||||
|
fn write_root(&mut self) {
|
||||||
|
let region = UiRegion::new(
|
||||||
|
UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.x)),
|
||||||
|
UiSpan::new(UiScalar::ZERO, UiScalar::px(self.output_size.y)),
|
||||||
|
);
|
||||||
|
match self.root_move == MoveIdx::NONE {
|
||||||
|
true => self.root_move = self.moves.push(MoveIdx::NONE, region),
|
||||||
|
false => self.moves.set(self.root_move, region),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||||
self.output_size = size.into();
|
self.output_size = size.into();
|
||||||
self.resized = true;
|
self.write_root();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which axes of the root widget's box are no longer the ones the root
|
||||||
|
/// slot holds, which is all a resize now is: one slot written, found by
|
||||||
|
/// the same comparison every other box change is found by.
|
||||||
|
fn root_axes_changed(&self) -> [bool; 2] {
|
||||||
|
let Some(active) = self.old_root.and_then(|root| self.active.get(&root)) else {
|
||||||
|
return [false; 2];
|
||||||
|
};
|
||||||
|
let px = self.px_of(active.parent_move, active.region);
|
||||||
|
let mut changed = [false; 2];
|
||||||
|
for (axis, c) in AXES.into_iter().zip(changed.iter_mut()) {
|
||||||
|
*c = pixel_len_changed(active.px.axis(axis), px.axis(axis));
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_size(&self) -> Vec2 {
|
||||||
|
self.output_size
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::Updates);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _update = diag::timer(TimerKind::Update);
|
||||||
|
self.invalid_sizes.clear();
|
||||||
|
self.invalid_sizes
|
||||||
|
.extend(rsc.widgets().needs_redraw.iter().copied());
|
||||||
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
||||||
// decide whether to panic or not
|
// decide whether to panic or not
|
||||||
if !rsc.widgets().waiting.is_empty() {
|
if !rsc.widgets().waiting.is_empty() {
|
||||||
@@ -52,20 +117,77 @@ impl UiRenderState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let root = root.into();
|
let root = root.into();
|
||||||
if self.needs_full_redraw(root) {
|
if self.root_changed(root) {
|
||||||
self.redraw_all(root, rsc);
|
self.redraw_all(root, rsc);
|
||||||
self.old_root = root.map(|r| r.id());
|
self.old_root = root.map(|r| r.id());
|
||||||
self.resized = false;
|
} else if self.root_axes_changed().iter().any(|&c| c) {
|
||||||
} else if rsc.widgets().has_updates() {
|
// Every box is a part of the root box, so writing it is a box
|
||||||
|
// that changed length like any other. Offering the root widget
|
||||||
|
// its box again puts that through `try_reuse`, which answers per
|
||||||
|
// axis and lets `redraws_under` price the subtree -- rather than
|
||||||
|
// marking it, which would redraw it whichever axis moved. What
|
||||||
|
// that cannot reach is a widget whose size came from the root box
|
||||||
|
// instead of its own, since its own box need not have changed.
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _marking = diag::timer(TimerKind::ResizeMarking);
|
||||||
|
let changed = self.root_axes_changed();
|
||||||
|
for id in self.root_readers.clone() {
|
||||||
|
let reads = self
|
||||||
|
.active
|
||||||
|
.get(&id)
|
||||||
|
.map_or([false; 2], |active| active.size_output_inputs);
|
||||||
|
if !AXES
|
||||||
|
.into_iter()
|
||||||
|
.zip(changed)
|
||||||
|
.any(|(axis, c)| c && reads[axis as usize])
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::ResizeDependents);
|
||||||
|
rsc.widgets_mut().needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
if let Some(root) = root {
|
||||||
|
self.draw_inner(
|
||||||
|
0,
|
||||||
|
root.id(),
|
||||||
|
UiRegion::FULL,
|
||||||
|
None,
|
||||||
|
1,
|
||||||
|
self.root_move,
|
||||||
|
false,
|
||||||
|
MaskIdx::NONE,
|
||||||
|
None,
|
||||||
|
rsc,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rsc.widgets().has_updates() {
|
||||||
self.redraw_updates(rsc);
|
self.redraw_updates(rsc);
|
||||||
}
|
}
|
||||||
|
self.invalid_sizes.clear();
|
||||||
|
self.draw_started.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
|
fn redraw_all(&mut self, root: Option<&StrongWidget>, rsc: &mut dyn UiRsc) {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _layout = diag::timer(TimerKind::FullLayout);
|
||||||
self.clear(rsc);
|
self.clear(rsc);
|
||||||
// free all resources & cache
|
// free all resources & cache
|
||||||
|
self.write_root();
|
||||||
if let Some(id) = root {
|
if let Some(id) = root {
|
||||||
self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None, rsc);
|
self.draw_inner(
|
||||||
|
0,
|
||||||
|
id.id(),
|
||||||
|
UiRegion::FULL,
|
||||||
|
None,
|
||||||
|
1,
|
||||||
|
self.root_move,
|
||||||
|
false,
|
||||||
|
MaskIdx::NONE,
|
||||||
|
None,
|
||||||
|
rsc,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,71 +199,120 @@ impl UiRenderState {
|
|||||||
id: WidgetId,
|
id: WidgetId,
|
||||||
region: UiRegion,
|
region: UiRegion,
|
||||||
parent: Option<WidgetId>,
|
parent: Option<WidgetId>,
|
||||||
|
depth: usize,
|
||||||
|
parent_move: MoveIdx,
|
||||||
|
slotted: bool,
|
||||||
mask: MaskIdx,
|
mask: MaskIdx,
|
||||||
old_children: Option<Vec<WidgetId>>,
|
mut old: Option<ActiveData>,
|
||||||
rsc: &mut dyn UiRsc,
|
rsc: &mut dyn UiRsc,
|
||||||
) {
|
) -> Size {
|
||||||
let mut old_children = old_children.unwrap_or_default();
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
if let Some(active) = self.active.get_mut(&id)
|
|
||||||
&& !rsc.widgets().needs_redraw.contains(&id)
|
|
||||||
{
|
{
|
||||||
// check to see if we can skip drawing first
|
diag::bump(Counter::DrawRequests);
|
||||||
if active.region == region {
|
diag::draw_request(id, parent, region, self.px_of(parent_move, region), slotted);
|
||||||
return;
|
}
|
||||||
} else if active.region.size() == region.size() {
|
if self.active.contains_key(&id) {
|
||||||
// TODO: epsilon?
|
if let Some(size) = self.try_reuse(id, region, depth, parent_move, rsc) {
|
||||||
let from = active.region;
|
return size;
|
||||||
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();
|
old = self.remove(id, false, rsc);
|
||||||
old_children = active.children;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw widget
|
// draw widget
|
||||||
|
let (move_idx, local) = match slotted {
|
||||||
|
// Its box becomes its slot's, so it draws in the slot's own
|
||||||
|
// coordinates and the box it was given is one entry to rewrite.
|
||||||
|
true => (self.move_slot(id, parent_move, region), UiRegion::FULL),
|
||||||
|
false => {
|
||||||
|
self.drop_slot(id);
|
||||||
|
(parent_move, region)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let px = self.px_of(move_idx, local);
|
||||||
|
// Drawn again in a box its parent already decided: the offer is the
|
||||||
|
// one recorded when the parent first asked, not this box.
|
||||||
|
let (old_children, offered_px) = match old {
|
||||||
|
Some(old) => (old.children, old.offered_px),
|
||||||
|
None => (Vec::new(), px),
|
||||||
|
};
|
||||||
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
self.draw_started.insert(id);
|
self.draw_started.insert(id);
|
||||||
|
|
||||||
let mut painter = Painter {
|
let mut painter = Painter {
|
||||||
state: self,
|
state: self,
|
||||||
region,
|
region: local,
|
||||||
mask,
|
mask,
|
||||||
layer,
|
layer,
|
||||||
id,
|
id,
|
||||||
textures: Vec::new(),
|
textures: Vec::new(),
|
||||||
primitives: Vec::new(),
|
primitives: Vec::new(),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
|
offered: Vec::new(),
|
||||||
|
size_deps: Vec::new(),
|
||||||
|
depth,
|
||||||
|
size_box_inputs: [false; 2],
|
||||||
|
size_output_inputs: [false; 2],
|
||||||
|
move_idx,
|
||||||
rsc,
|
rsc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
{
|
||||||
|
diag::bump(Counter::WidgetDraws);
|
||||||
|
diag::draw_widget(id, painter.rsc.widgets().label(id));
|
||||||
|
}
|
||||||
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
||||||
widget.draw(&mut painter);
|
let size = widget.draw(&mut painter);
|
||||||
drop(widget);
|
drop(widget);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::size_reported(id, size);
|
||||||
|
|
||||||
let Painter {
|
let Painter {
|
||||||
state: _,
|
state: _,
|
||||||
rsc: _,
|
rsc: _,
|
||||||
region,
|
region: _,
|
||||||
mask,
|
mask,
|
||||||
textures,
|
textures,
|
||||||
primitives,
|
primitives,
|
||||||
children,
|
children,
|
||||||
|
offered: _,
|
||||||
|
size_deps,
|
||||||
|
size_box_inputs,
|
||||||
|
size_output_inputs,
|
||||||
|
move_idx,
|
||||||
layer,
|
layer,
|
||||||
|
depth: _,
|
||||||
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,
|
||||||
|
offered_px,
|
||||||
parent,
|
parent,
|
||||||
|
depth,
|
||||||
textures,
|
textures,
|
||||||
primitives,
|
primitives,
|
||||||
children,
|
children,
|
||||||
|
size_deps,
|
||||||
|
size_box_inputs,
|
||||||
|
size_output_inputs,
|
||||||
|
output_px: self.output_size,
|
||||||
|
move_idx,
|
||||||
|
parent_move,
|
||||||
mask,
|
mask,
|
||||||
layer,
|
layer,
|
||||||
};
|
};
|
||||||
|
|
||||||
// remove old children that weren't kept
|
// remove old children that weren't kept
|
||||||
for c in &old_children {
|
for c in &old_children {
|
||||||
if !active.children.contains(c) {
|
if !active.children.contains(c) {
|
||||||
@@ -149,22 +320,272 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match active.size_output_inputs.iter().any(|&reads| reads) {
|
||||||
|
true => self.root_readers.insert(id),
|
||||||
|
false => self.root_readers.remove(&id),
|
||||||
|
};
|
||||||
rsc.on_draw(&active);
|
rsc.on_draw(&active);
|
||||||
self.active.insert(id, active);
|
self.active.insert(id, active);
|
||||||
|
self.invalid_sizes.remove(&id);
|
||||||
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
|
/// The slot a widget's box is held in, made on its first placed draw and
|
||||||
|
/// kept until it stops being drawn -- a redraw replaces its `ActiveData`
|
||||||
|
/// while descendants go on naming the slot.
|
||||||
|
fn move_slot(&mut self, id: WidgetId, parent: MoveIdx, region: UiRegion) -> MoveIdx {
|
||||||
|
if let Some(&idx) = self.slots.get(&id) {
|
||||||
|
self.moves.set_parent(idx, parent);
|
||||||
|
self.moves.set(idx, region);
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
let idx = self.moves.push(parent, region);
|
||||||
|
self.slots.insert(id, idx);
|
||||||
|
idx
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gives up a slot a widget no longer needs, because it is drawn somewhere
|
||||||
|
/// that does not place it. Its descendants name it, so this is only
|
||||||
|
/// reached where they are about to be drawn again.
|
||||||
|
fn drop_slot(&mut self, id: WidgetId) {
|
||||||
|
if let Some(idx) = self.slots.remove(&id) {
|
||||||
|
self.moves.remove(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pixel size of a region held in `slot`'s coordinates.
|
||||||
|
pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> Vec2 {
|
||||||
|
self.moves
|
||||||
|
.resolve(slot, region)
|
||||||
|
.size()
|
||||||
|
.to_px(self.output_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A clean widget's retained size, when the offered pixel axes which
|
||||||
|
/// produced that answer are unchanged. This observes the old answer only;
|
||||||
|
/// it does not move or otherwise reuse the widget's drawing.
|
||||||
|
pub(super) fn retained_size(
|
||||||
|
&self,
|
||||||
|
id: WidgetId,
|
||||||
|
region: UiRegion,
|
||||||
|
parent_move: MoveIdx,
|
||||||
|
widgets: &Widgets,
|
||||||
|
) -> Option<(Size, [bool; 2], [bool; 2])> {
|
||||||
|
if self.size_is_invalid(id, widgets) || self.dirty_size_under(id, widgets) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let active = self.active.get(&id)?;
|
||||||
|
if active.parent_move != parent_move {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let px = self.px_of(parent_move, region);
|
||||||
|
let valid_box = AXES
|
||||||
|
.into_iter()
|
||||||
|
.zip(active.size_box_inputs)
|
||||||
|
.all(|(axis, depends)| {
|
||||||
|
!depends || !pixel_len_changed(active.px.axis(axis), px.axis(axis))
|
||||||
|
});
|
||||||
|
let valid_output =
|
||||||
|
AXES.into_iter()
|
||||||
|
.zip(active.size_output_inputs)
|
||||||
|
.all(|(axis, depends)| {
|
||||||
|
!depends
|
||||||
|
|| !pixel_len_changed(
|
||||||
|
active.output_px.axis(axis),
|
||||||
|
self.output_size.axis(axis),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
(valid_box && valid_output).then_some((
|
||||||
|
active.size,
|
||||||
|
active.size_box_inputs,
|
||||||
|
active.size_output_inputs,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_is_invalid(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
||||||
|
self.invalid_sizes.contains(&id) || widgets.needs_redraw.contains(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
||||||
|
self.active.get(&id).is_some_and(|active| {
|
||||||
|
active.size_deps.iter().any(|child| {
|
||||||
|
self.size_is_invalid(*child, widgets) || self.dirty_size_under(*child, widgets)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The drawing a widget already has, kept for a new box if the box has not
|
||||||
|
/// changed in a way it depends on.
|
||||||
|
fn try_reuse(
|
||||||
|
&mut self,
|
||||||
|
id: WidgetId,
|
||||||
|
region: UiRegion,
|
||||||
|
depth: usize,
|
||||||
|
parent_move: MoveIdx,
|
||||||
|
rsc: &mut dyn UiRsc,
|
||||||
|
) -> Option<Size> {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::ReuseAttempts);
|
||||||
|
// Only its own dirtiness, not anything dirty under it that could
|
||||||
|
// change the size this hands back. What makes that safe is the order
|
||||||
|
// `redraw_updates` settles in, and nothing else: by the time a reader
|
||||||
|
// draws, everything dirty below it has been drawn and has propagated.
|
||||||
|
// Draw in another order and this returns a stale size -- measured, on
|
||||||
|
// seed 2 of `tests/generated.rs`.
|
||||||
|
if rsc.widgets().needs_redraw.contains(&id) {
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
self.keep_depth(id, depth);
|
||||||
|
return Some(size);
|
||||||
|
}
|
||||||
|
// Only a placed widget can be given a different *region* without
|
||||||
|
// drawing again: it has an entry of its own to say where it went,
|
||||||
|
// where an unslotted one shares its parent's and has nothing to
|
||||||
|
// write. Its parent's box changing length is not that -- everything
|
||||||
|
// it drew is a fraction of that box, so the slot already above it
|
||||||
|
// carries the change and `on_resize` below decides whether the
|
||||||
|
// drawing survives it.
|
||||||
|
if slot == parent_move && old_region != region {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
{
|
||||||
|
diag::bump(Counter::ReuseUnslotted);
|
||||||
|
diag::reuse(id, ReuseOutcome::Unslotted);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if slot != parent_move {
|
||||||
|
self.moves.set(slot, region);
|
||||||
|
}
|
||||||
|
self.keep_depth(id, depth);
|
||||||
let active = self.active.get_mut(&id).unwrap();
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
for h in &active.primitives {
|
active.region = region;
|
||||||
let region = self.layers[h.layer].region_mut(h);
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
*region = region.outside(&from).within(&to);
|
{
|
||||||
}
|
diag::bump(Counter::ReuseMoved);
|
||||||
active.region = active.region.outside(&from).within(&to);
|
diag::reuse(id, ReuseOutcome::Moved);
|
||||||
// SAFETY: children cannot be recursive
|
|
||||||
let children = unsafe { forget_ref(&active.children) };
|
|
||||||
for child in children {
|
|
||||||
self.mov(*child, from, to);
|
|
||||||
}
|
}
|
||||||
|
Some(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
@@ -187,13 +608,16 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,26 +625,72 @@ impl UiRenderState {
|
|||||||
for (_, active) in self.active.drain() {
|
for (_, active) in self.active.drain() {
|
||||||
rsc.on_undraw(&active);
|
rsc.on_undraw(&active);
|
||||||
}
|
}
|
||||||
self.cache.clear();
|
self.slots.clear();
|
||||||
|
self.moves.clear();
|
||||||
|
self.root_move = MoveIdx::NONE;
|
||||||
self.layers.clear();
|
self.layers.clear();
|
||||||
|
self.invalid_sizes.clear();
|
||||||
|
self.draw_started.clear();
|
||||||
rsc.widgets_mut().needs_redraw.clear();
|
rsc.widgets_mut().needs_redraw.clear();
|
||||||
rsc.free();
|
rsc.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
|
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
|
||||||
while let Some(&id) = rsc.widgets().needs_redraw.iter().next() {
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _layout = diag::timer(TimerKind::IncrementalLayout);
|
||||||
|
// A reader's answer is only valid after every dirty size it reads has
|
||||||
|
// settled, and taking the deepest first is what arranges that --
|
||||||
|
// `try_reuse` hands back a retained size without asking whether
|
||||||
|
// anything dirty sits under it, so this order is load-bearing for the
|
||||||
|
// answer and not only for the cost. Equal-depth widgets are
|
||||||
|
// independent, so their order does not matter. Resize dirtiness already marks whole reader chains, so
|
||||||
|
// choosing their shallowest roots coalesces descendants that share a
|
||||||
|
// reader and gives each changing box its final constraints first.
|
||||||
|
while let Some(id) = {
|
||||||
|
let dirty = rsc.widgets().needs_redraw.iter().copied();
|
||||||
|
dirty.max_by_key(|&id| self.depth(id))
|
||||||
|
} {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::QueuePops);
|
||||||
self.redraw(id, rsc);
|
self.redraw(id, rsc);
|
||||||
}
|
}
|
||||||
rsc.free();
|
rsc.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
|
/// Keeps a reused widget's depth current, since being reused is being
|
||||||
root.into().map(|r| r.id()) != self.old_root
|
/// visited: only a subtree nobody looked at can hold a stale one.
|
||||||
|
fn keep_depth(&mut self, id: WidgetId, depth: usize) {
|
||||||
|
if let Some(active) = self.active.get_mut(&id) {
|
||||||
|
active.depth = depth;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scheduling and drawing must use the same full-redraw predicate.
|
fn depth(&self, id: WidgetId) -> usize {
|
||||||
fn needs_full_redraw<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
self.root_changed(root) || self.resized
|
diag::bump(Counter::DepthReads);
|
||||||
|
let depth = self.active.get(&id).map_or(1, |active| active.depth);
|
||||||
|
debug_assert_eq!(
|
||||||
|
depth,
|
||||||
|
self.walked_depth(id),
|
||||||
|
"a widget's kept depth is not the one its ancestry says"
|
||||||
|
);
|
||||||
|
depth
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the kept depth is checked against, and the only thing that reads
|
||||||
|
/// the ancestry to find one.
|
||||||
|
fn walked_depth(&self, id: WidgetId) -> usize {
|
||||||
|
let mut depth = 0;
|
||||||
|
let mut at = Some(id);
|
||||||
|
while let Some(id) = at {
|
||||||
|
at = self.active.get(&id).and_then(|active| active.parent);
|
||||||
|
depth += 1;
|
||||||
|
}
|
||||||
|
depth
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
|
||||||
|
root.into().map(|r| r.id()) != self.old_root
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn needs_redraw<'a>(
|
pub fn needs_redraw<'a>(
|
||||||
@@ -228,7 +698,9 @@ impl UiRenderState {
|
|||||||
root: impl Into<Option<&'a StrongWidget>>,
|
root: impl Into<Option<&'a StrongWidget>>,
|
||||||
widgets: &Widgets,
|
widgets: &Widgets,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
self.needs_full_redraw(root) || widgets.has_updates()
|
self.root_changed(root)
|
||||||
|
|| self.root_axes_changed().iter().any(|&c| c)
|
||||||
|
|| widgets.has_updates()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active_widgets(&self) -> usize {
|
pub fn active_widgets(&self) -> usize {
|
||||||
@@ -254,29 +726,47 @@ 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 region = self.active.get(&id.id())?.region;
|
let active = self.active.get(&id.id())?;
|
||||||
|
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);
|
||||||
// check if parent depends on the desired size of this, if so then redraw it first
|
if rsc.widgets().needs_redraw.contains(&id) {
|
||||||
for axis in [Axis::X, Axis::Y] {
|
self.invalid_sizes.insert(id);
|
||||||
if let Some(&(outer, old)) = self.cache.size.axis_dyn(axis).get(&id)
|
|
||||||
&& let Some(current) = self.active.get(&id)
|
|
||||||
&& let Some(pid) = current.parent
|
|
||||||
{
|
|
||||||
self.cache.size.axis_dyn(axis).remove(&id);
|
|
||||||
let new = self.size_ctx(id, outer, rsc).len_axis(id, axis);
|
|
||||||
self.cache.size.axis_dyn(axis).insert(id, (outer, new));
|
|
||||||
if new != old {
|
|
||||||
self.redraw(pid, rsc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// A widget can only answer whether its size changed by drawing in the
|
||||||
|
// box its parent chose. If that box changed in pixels, its retained
|
||||||
|
// placement is stale and the highest size reader must choose the new
|
||||||
|
// box first. The same holds when the box was decided from the
|
||||||
|
// widget's own answer: measuring there again can only repeat it,
|
||||||
|
// whatever the content now says. Otherwise the widget can draw
|
||||||
|
// locally, and its readers only matter if the returned size actually
|
||||||
|
// changed.
|
||||||
|
let box_changed = self.active.get(&id).is_some_and(|active| {
|
||||||
|
let px = self.px_of(active.parent_move, active.region);
|
||||||
|
AXES.into_iter()
|
||||||
|
.any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis)))
|
||||||
|
});
|
||||||
|
let top = match box_changed {
|
||||||
|
true => self.top_reader(id),
|
||||||
|
false => None,
|
||||||
|
}
|
||||||
|
.or_else(|| self.derived_box_reader(id));
|
||||||
|
if let Some(top) = top {
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::EagerReaderRedraws);
|
||||||
|
self.mark_below(id, top, rsc);
|
||||||
|
self.redraw(top, rsc);
|
||||||
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
|
|
||||||
if self.draw_started.contains(&id) {
|
if self.draw_started.contains(&id) {
|
||||||
return;
|
return;
|
||||||
@@ -285,33 +775,90 @@ 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);
|
||||||
|
|
||||||
self.draw_inner(
|
let old_size = active.size;
|
||||||
|
let size = self.draw_inner(
|
||||||
active.layer,
|
active.layer,
|
||||||
id,
|
id,
|
||||||
active.region,
|
active.region,
|
||||||
active.parent,
|
active.parent,
|
||||||
|
active.depth,
|
||||||
|
active.parent_move,
|
||||||
|
active.move_idx != active.parent_move,
|
||||||
active.mask,
|
active.mask,
|
||||||
Some(active.children),
|
Some(active),
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn size_ctx<'b>(
|
/// The highest reader up the chain that gave what it read a box other
|
||||||
&'b mut self,
|
/// than the one it asked in, on an axis this widget's size reads. Above
|
||||||
source: WidgetId,
|
/// it every box is a constraint rather than an answer. It is the highest
|
||||||
outer: UiVec2,
|
/// and not the nearest because a pass-through hands a derived box down
|
||||||
rsc: &'b mut dyn UiRsc,
|
/// unchanged.
|
||||||
) -> SizeCtx<'b> {
|
fn derived_box_reader(&self, id: WidgetId) -> Option<WidgetId> {
|
||||||
let ui = rsc.ui_mut();
|
let reads = self.active.get(&id)?.size_box_inputs;
|
||||||
SizeCtx {
|
let mut top = None;
|
||||||
source,
|
for (active, parent) in self.reader_chain(id) {
|
||||||
cache: &mut self.cache,
|
let px = self.px_of(active.parent_move, active.region);
|
||||||
text: &mut ui.text,
|
if AXES.into_iter().zip(reads).any(|(axis, r)| {
|
||||||
widgets: &ui.widgets,
|
r && pixel_len_changed(active.offered_px.axis(axis), px.axis(axis))
|
||||||
outer,
|
}) {
|
||||||
output_size: self.output_size,
|
top = Some(parent);
|
||||||
id: source,
|
}
|
||||||
|
}
|
||||||
|
top
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The furthest ancestor that read this widget's size, directly or through
|
||||||
|
/// widgets that did the same.
|
||||||
|
fn top_reader(&self, id: WidgetId) -> Option<WidgetId> {
|
||||||
|
self.reader_chain(id).last().map(|(_, parent)| parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Each widget from `id` upward whose parent read its size, with that
|
||||||
|
/// parent.
|
||||||
|
fn reader_chain(&self, id: WidgetId) -> impl Iterator<Item = (&ActiveData, WidgetId)> {
|
||||||
|
let mut at = Some(id);
|
||||||
|
std::iter::from_fn(move || {
|
||||||
|
let active = self.active.get(&at?)?;
|
||||||
|
let parent = active.parent?;
|
||||||
|
let read = self.active.get(&parent)?.size_deps.contains(&active.id);
|
||||||
|
at = read.then_some(parent);
|
||||||
|
read.then_some((active, parent))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks everything from `id` up to, and not including, `top`, so that
|
||||||
|
/// drawing `top` draws each of them rather than reusing it.
|
||||||
|
fn mark_below(&self, id: WidgetId, top: WidgetId, rsc: &mut dyn UiRsc) {
|
||||||
|
let mut at = id;
|
||||||
|
while at != top {
|
||||||
|
rsc.widgets_mut().needs_redraw.insert(at);
|
||||||
|
let Some(parent) = self.active.get(&at).and_then(|active| active.parent) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
at = parent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,6 +34,10 @@ 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> {
|
||||||
|
|||||||
+1
-22
@@ -1,34 +1,13 @@
|
|||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const trait DivOr {
|
const impl LerpUtil for f32 {
|
||||||
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 {
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
#[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) }
|
||||||
|
|||||||
+6
-11
@@ -1,7 +1,11 @@
|
|||||||
use crate::util::{DivOr, impl_op};
|
use crate::util::impl_op;
|
||||||
use std::{hash::Hash, ops::*};
|
use std::{hash::Hash, ops::*};
|
||||||
|
|
||||||
#[repr(C)]
|
/// `align(8)` because that is WGSL's alignment for a `vec2<f32>`, so any GPU
|
||||||
|
/// 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,
|
||||||
@@ -67,15 +71,6 @@ 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;
|
||||||
|
|
||||||
|
|||||||
+35
-19
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Axis, AxisT, Len, Painter, SizeCtx};
|
use crate::{Axis, Len, Painter, Size};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
mod data;
|
mod data;
|
||||||
@@ -15,32 +15,48 @@ 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,
|
||||||
|
/// Reserved: nothing reads this yet, so a widget saying it is redrawn.
|
||||||
|
/// Keeping an unchanged drawing in a bigger box needs the widget to say
|
||||||
|
/// *where* in that box it should sit, which is the alignment work.
|
||||||
|
Translate,
|
||||||
|
#[default]
|
||||||
|
Redraw,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait Widget: Any {
|
pub trait Widget: Any {
|
||||||
fn draw(&mut self, painter: &mut Painter);
|
/// Draws the widget, and returns what it used of the box it was given.
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len;
|
fn draw(&mut self, painter: &mut Painter) -> Size;
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait WidgetAxisFns {
|
/// An exact length the widget can give without a painter or its children.
|
||||||
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len;
|
/// 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
|
||||||
|
}
|
||||||
|
|
||||||
impl<W: Widget + ?Sized> WidgetAxisFns for W {
|
fn on_resize(&self, _axis: Axis) -> OnResize {
|
||||||
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len {
|
OnResize::default()
|
||||||
match A::get() {
|
|
||||||
Axis::X => self.desired_width(ctx),
|
|
||||||
Axis::Y => self.desired_height(ctx),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for () {
|
impl Widget for () {
|
||||||
fn draw(&mut self, _: &mut Painter) {}
|
/// A gap: nothing drawn, at the default length, so a span gives it a share.
|
||||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
fn draw(&mut self, _: &mut Painter) -> Size {
|
||||||
Len::ZERO
|
Size::default()
|
||||||
}
|
}
|
||||||
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
|
||||||
Len::ZERO
|
fn size_hint(&self, _axis: Axis) -> Option<Len> {
|
||||||
|
Some(Len::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_resize(&self, _axis: Axis) -> OnResize {
|
||||||
|
OnResize::Scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -10,11 +10,7 @@ struct State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DefaultAppState for State {
|
impl DefaultAppState for State {
|
||||||
fn new(
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
mut ui_state: DefaultUiState,
|
|
||||||
rsc: &mut DefaultRsc<Self>,
|
|
||||||
_: Proxy<Self::Event>,
|
|
||||||
) -> Self {
|
|
||||||
rect(Color::RED).set_root(rsc, &mut ui_state);
|
rect(Color::RED).set_root(rsc, &mut ui_state);
|
||||||
Self { ui_state }
|
Self { ui_state }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//! 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,11 +15,7 @@ pub struct Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DefaultAppState for Client {
|
impl DefaultAppState for Client {
|
||||||
fn new(
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
mut ui_state: DefaultUiState,
|
|
||||||
rsc: &mut DefaultRsc<Self>,
|
|
||||||
_: Proxy<Self::Event>,
|
|
||||||
) -> Self {
|
|
||||||
let rrect = rect(Color::WHITE).radius(20);
|
let rrect = rect(Color::WHITE).radius(20);
|
||||||
let pad_test = (
|
let pad_test = (
|
||||||
rrect.color(Color::BLUE),
|
rrect.color(Color::BLUE),
|
||||||
|
|||||||
+1
-5
@@ -11,11 +11,7 @@ struct State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DefaultAppState for State {
|
impl DefaultAppState for State {
|
||||||
fn new(
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
mut ui_state: DefaultUiState,
|
|
||||||
rsc: &mut DefaultRsc<Self>,
|
|
||||||
_: Proxy<Self::Event>,
|
|
||||||
) -> Self {
|
|
||||||
let rect = rect(Color::RED).add(rsc);
|
let rect = rect(Color::RED).add(rsc);
|
||||||
rect.task_on(CursorSense::click(), async move |mut ctx| {
|
rect.task_on(CursorSense::click(), async move |mut ctx| {
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
//! 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-5
@@ -36,11 +36,7 @@ impl Test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DefaultAppState for State {
|
impl DefaultAppState for State {
|
||||||
fn new(
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
mut ui_state: DefaultUiState,
|
|
||||||
rsc: &mut DefaultRsc<Self>,
|
|
||||||
_: Proxy<Self::Event>,
|
|
||||||
) -> Self {
|
|
||||||
let test = Test::new(rsc);
|
let test = Test::new(rsc);
|
||||||
|
|
||||||
test.on(CursorSense::click(), move |_, rsc| {
|
test.on(CursorSense::click(), move |_, rsc| {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[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 }
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# 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
|
||||||
Executable
+182
@@ -0,0 +1,182 @@
|
|||||||
|
#!/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"
|
||||||
+61
-44
@@ -25,7 +25,35 @@ pub use sense::*;
|
|||||||
pub use state::*;
|
pub use state::*;
|
||||||
pub use task::*;
|
pub use task::*;
|
||||||
|
|
||||||
pub type Proxy<Event> = EventLoopProxy<Event>;
|
/// Sends an application's own events to its event loop. It wraps the proxy
|
||||||
|
/// rather than being one because task updates travel the same way: what an
|
||||||
|
/// application sends is its `Event`, not the loop's whole message type.
|
||||||
|
pub struct Proxy<State: DefaultAppState>(EventLoopProxy<DefaultEvent<State>>);
|
||||||
|
|
||||||
|
impl<State: DefaultAppState> Clone for Proxy<State> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: DefaultAppState> Proxy<State> {
|
||||||
|
pub fn send_event(&self, event: State::Event) {
|
||||||
|
let _ = self.0.send_event(DefaultEvent::User(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the event loop carries: the application's own events, and the
|
||||||
|
/// updates tasks send back to the ui thread.
|
||||||
|
pub enum DefaultEvent<State: DefaultAppState> {
|
||||||
|
User(State::Event),
|
||||||
|
Update(Box<dyn TaskUpdate<DefaultRsc<State>>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: DefaultAppState> TaskQueue<DefaultRsc<State>> for Proxy<State> {
|
||||||
|
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<State>>>) {
|
||||||
|
let _ = self.0.send_event(DefaultEvent::Update(update));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct DefaultUiState {
|
pub struct DefaultUiState {
|
||||||
pub root: Option<StrongWidget>,
|
pub root: Option<StrongWidget>,
|
||||||
@@ -66,9 +94,8 @@ pub trait HasDefaultUiState: Sized + 'static {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait DefaultAppState: HasDefaultUiState {
|
pub trait DefaultAppState: HasDefaultUiState {
|
||||||
type Event = ();
|
type Event: Send = ();
|
||||||
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
|
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self>) -> Self;
|
||||||
-> Self;
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn event(
|
fn event(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -101,18 +128,14 @@ pub struct DefaultRsc<State: 'static> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<State> DefaultRsc<State> {
|
impl<State> DefaultRsc<State> {
|
||||||
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
|
pub fn init(queue: Arc<dyn TaskQueue<Self>>) -> Self {
|
||||||
let (tasks, recv) = Tasks::init(window);
|
Self {
|
||||||
(
|
ui: Default::default(),
|
||||||
Self {
|
events: Default::default(),
|
||||||
ui: Default::default(),
|
tasks: Tasks::init(queue),
|
||||||
events: Default::default(),
|
state: Default::default(),
|
||||||
tasks,
|
_state: Default::default(),
|
||||||
state: Default::default(),
|
}
|
||||||
_state: Default::default(),
|
|
||||||
},
|
|
||||||
recv,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
|
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
|
||||||
@@ -177,43 +200,32 @@ pub struct DefaultApp<State: DefaultAppState> {
|
|||||||
rsc: DefaultRsc<State>,
|
rsc: DefaultRsc<State>,
|
||||||
render: UiRenderState,
|
render: UiRenderState,
|
||||||
state: State,
|
state: State,
|
||||||
task_recv: TaskMsgReceiver<DefaultRsc<State>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||||
type Event = State::Event;
|
type Event = DefaultEvent<State>;
|
||||||
|
|
||||||
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
|
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
|
||||||
let window = event_loop
|
let window = event_loop
|
||||||
.create_window(State::window_attributes())
|
.create_window(State::window_attributes())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let default_state = DefaultUiState::new(window);
|
let default_state = DefaultUiState::new(window);
|
||||||
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone());
|
let mut rsc = DefaultRsc::init(Arc::new(Proxy(proxy.clone())));
|
||||||
let state = State::new(default_state, &mut rsc, proxy);
|
let state = State::new(default_state, &mut rsc, Proxy(proxy));
|
||||||
let render = UiRenderState::new();
|
let render = UiRenderState::new();
|
||||||
Self {
|
Self { rsc, state, render }
|
||||||
rsc,
|
|
||||||
state,
|
|
||||||
render,
|
|
||||||
task_recv,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
|
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
|
||||||
self.state.event(event, &mut self.rsc, &mut self.render);
|
match event {
|
||||||
|
DefaultEvent::User(event) => self.state.event(event, &mut self.rsc, &mut self.render),
|
||||||
|
DefaultEvent::Update(update) => update(&mut self.state, &mut self.rsc),
|
||||||
|
}
|
||||||
|
self.request_redraw_if_needed();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
|
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
|
||||||
let Self {
|
let Self { rsc, render, state } = self;
|
||||||
rsc,
|
|
||||||
render,
|
|
||||||
state,
|
|
||||||
task_recv,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
for update in task_recv.try_iter() {
|
|
||||||
update(state, rsc);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ui_state = state.default_state_mut();
|
let ui_state = state.default_state_mut();
|
||||||
let input_changed = ui_state.input.event(&event);
|
let input_changed = ui_state.input.event(&event);
|
||||||
@@ -223,8 +235,7 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
ui_state.focus = None;
|
ui_state.focus = None;
|
||||||
}
|
}
|
||||||
if input_changed {
|
if input_changed {
|
||||||
let window_size = ui_state.window_size();
|
render.run_sensors(rsc, state, cursor_state);
|
||||||
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
|
||||||
@@ -293,11 +304,8 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
state.window_event(event, rsc, render);
|
state.window_event(event, rsc, render);
|
||||||
let ui_state = self.state.default_state_mut();
|
self.request_redraw_if_needed();
|
||||||
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
|
self.state.default_state_mut().input.end_frame();
|
||||||
ui_state.renderer.window().request_redraw();
|
|
||||||
}
|
|
||||||
ui_state.input.end_frame();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit(&mut self) {
|
fn exit(&mut self) {
|
||||||
@@ -305,6 +313,15 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<State: DefaultAppState> DefaultApp<State> {
|
||||||
|
fn request_redraw_if_needed(&mut self) {
|
||||||
|
let ui_state = self.state.default_state_mut();
|
||||||
|
if self.render.needs_redraw(&ui_state.root, self.rsc.widgets()) {
|
||||||
|
ui_state.renderer.window().request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait RscIdx<Rsc> {
|
pub trait RscIdx<Rsc> {
|
||||||
type Output;
|
type Output;
|
||||||
fn get(self, rsc: &Rsc) -> &Self::Output;
|
fn get(self, rsc: &Rsc) -> &Self::Output;
|
||||||
|
|||||||
+103
-35
@@ -4,14 +4,14 @@ use std::{
|
|||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum CursorButton {
|
pub enum CursorButton {
|
||||||
Left,
|
Left,
|
||||||
Right,
|
Right,
|
||||||
Middle,
|
Middle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Debug, 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 State = SensorState;
|
type Global = Hovered;
|
||||||
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,6 +37,24 @@ 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 {
|
||||||
@@ -52,6 +70,12 @@ 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)]
|
||||||
@@ -96,6 +120,12 @@ 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;
|
||||||
@@ -123,11 +153,6 @@ 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
|
||||||
@@ -147,7 +172,6 @@ pub trait SensorUi {
|
|||||||
rsc: &mut Rsc,
|
rsc: &mut Rsc,
|
||||||
state: &mut Rsc::State,
|
state: &mut Rsc::State,
|
||||||
cursor: CursorState,
|
cursor: CursorState,
|
||||||
window_size: Vec2,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,54 +181,98 @@ 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 mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
|
let 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 sensed = false;
|
let mut consumed = false;
|
||||||
for (id, sensor) in active.get_mut(&layer).into_flat_iter() {
|
for id in active.get(&layer).into_flat_iter().map(|(id, _)| *id) {
|
||||||
let shape = self.active.get(id).unwrap().region;
|
let Some(region) = region_of(id) else {
|
||||||
let region = shape.to_px(window_size);
|
continue;
|
||||||
let in_shape = cursor.exists && region.contains(cursor.pos);
|
};
|
||||||
sensor.hover.update(in_shape);
|
if !cursor.exists || !region.contains(cursor.pos) {
|
||||||
if sensor.hover == ActivationState::Off {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sensed = true;
|
hovered.now.push(id);
|
||||||
|
let hover = match hovered.was.contains(&id) {
|
||||||
let cursor = cursor.clone();
|
true => ActivationState::On,
|
||||||
|
false => ActivationState::Start,
|
||||||
let data = CursorData {
|
|
||||||
pos: cursor.pos - region.top_left,
|
|
||||||
size: region.bot_right - region.top_left,
|
|
||||||
scroll_delta: cursor.scroll_delta,
|
|
||||||
hover: sensor.hover,
|
|
||||||
cursor,
|
|
||||||
// this does not have any meaning;
|
|
||||||
// might wanna set up Event to have a prepare stage
|
|
||||||
sense: CursorSense::Hovering,
|
|
||||||
render: self,
|
|
||||||
};
|
};
|
||||||
rsc.run_event::<CursorSense>(*id, data, state);
|
// 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;
|
||||||
}
|
}
|
||||||
if sensed {
|
// Applied after the layer, never during it: senses on one layer do
|
||||||
|
// not block each other.
|
||||||
|
if consumed {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rsc.events_mut().get_type::<CursorSense>().active = active;
|
|
||||||
|
// Whatever the cursor was inside and is not now, whether it left or a
|
||||||
|
// 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 {
|
||||||
|
pos: cursor.pos - region.top_left,
|
||||||
|
size: region.bot_right - region.top_left,
|
||||||
|
scroll_delta: cursor.scroll_delta,
|
||||||
|
hover,
|
||||||
|
cursor: cursor.clone(),
|
||||||
|
// this does not have any meaning;
|
||||||
|
// might wanna set up Event to have a prepare stage
|
||||||
|
sense: CursorSense::Hovering,
|
||||||
|
render,
|
||||||
|
};
|
||||||
|
rsc.run_event::<CursorSense>(id, data, state)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn should_run(
|
pub fn should_run(
|
||||||
senses: &CursorSenses,
|
senses: &CursorSenses,
|
||||||
cursor: &CursorState,
|
cursor: &CursorState,
|
||||||
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(),
|
||||||
|
|||||||
+16
-34
@@ -1,11 +1,5 @@
|
|||||||
use iris_core::HasState;
|
use iris_core::HasState;
|
||||||
use std::{
|
use std::{pin::Pin, sync::Arc};
|
||||||
pin::Pin,
|
|
||||||
sync::{
|
|
||||||
Arc,
|
|
||||||
mpsc::{Receiver as SyncReceiver, Sender as SyncSender, channel as sync_channel},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
runtime::Runtime,
|
runtime::Runtime,
|
||||||
sync::mpsc::{
|
sync::mpsc::{
|
||||||
@@ -13,64 +7,52 @@ use tokio::{
|
|||||||
unbounded_channel as async_channel,
|
unbounded_channel as async_channel,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use winit::window::Window;
|
|
||||||
|
|
||||||
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
|
|
||||||
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
|
|
||||||
|
|
||||||
pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {}
|
pub trait TaskUpdate<Rsc: HasState>: FnOnce(&mut Rsc::State, &mut Rsc) + Send {}
|
||||||
impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {}
|
impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc> for F {}
|
||||||
|
|
||||||
|
/// Hands an update from a task to the thread that owns the ui. Delivery and
|
||||||
|
/// waking are one act: a host posts the update as a message its loop already
|
||||||
|
/// carries, so nothing has to wake the loop separately, or claim a redraw to
|
||||||
|
/// be looked at.
|
||||||
|
pub trait TaskQueue<Rsc: HasState>: Send + Sync + 'static {
|
||||||
|
fn send(&self, update: Box<dyn TaskUpdate<Rsc>>);
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Tasks<Rsc: HasState> {
|
pub struct Tasks<Rsc: HasState> {
|
||||||
start: AsyncSender<BoxTask>,
|
start: AsyncSender<BoxTask>,
|
||||||
window: Arc<Window>,
|
queue: Arc<dyn TaskQueue<Rsc>>,
|
||||||
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TaskCtx<Rsc: HasState> {
|
pub struct TaskCtx<Rsc: HasState> {
|
||||||
send: TaskMsgSender<Rsc>,
|
queue: Arc<dyn TaskQueue<Rsc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Rsc: HasState> TaskCtx<Rsc> {
|
impl<Rsc: HasState> TaskCtx<Rsc> {
|
||||||
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
|
pub fn update(&mut self, f: impl TaskUpdate<Rsc> + 'static) {
|
||||||
let _ = self.send.send(Box::new(f));
|
self.queue.send(Box::new(f));
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
|
|
||||||
fn new(send: TaskMsgSender<Rsc>) -> Self {
|
|
||||||
Self { send }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||||
|
|
||||||
impl<Rsc: HasState> Tasks<Rsc> {
|
impl<Rsc: HasState> Tasks<Rsc> {
|
||||||
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
|
pub fn init(queue: Arc<dyn TaskQueue<Rsc>>) -> Self {
|
||||||
let (start, start_recv) = async_channel();
|
let (start, start_recv) = async_channel();
|
||||||
let (msgs, msgs_recv) = sync_channel();
|
|
||||||
std::thread::spawn(|| {
|
std::thread::spawn(|| {
|
||||||
let rt = Runtime::new().unwrap();
|
let rt = Runtime::new().unwrap();
|
||||||
rt.block_on(listen(start_recv))
|
rt.block_on(listen(start_recv))
|
||||||
});
|
});
|
||||||
(
|
Self { start, queue }
|
||||||
Self {
|
|
||||||
start,
|
|
||||||
msg_send: msgs,
|
|
||||||
window,
|
|
||||||
},
|
|
||||||
msgs_recv,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
|
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
|
||||||
where
|
where
|
||||||
F::CallOnceFuture: Send,
|
F::CallOnceFuture: Send,
|
||||||
{
|
{
|
||||||
let send = self.msg_send.clone();
|
let queue = self.queue.clone();
|
||||||
let window = self.window.clone();
|
|
||||||
let _ = self.start.send(Box::pin(async move {
|
let _ = self.start.send(Box::pin(async move {
|
||||||
task(TaskCtx::new(send)).await;
|
task(TaskCtx { queue }).await;
|
||||||
window.request_redraw();
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+320
@@ -0,0 +1,320 @@
|
|||||||
|
//! A ui with no window: build a tree, run frames, move a pointer, and read
|
||||||
|
//! back where widgets landed.
|
||||||
|
//!
|
||||||
|
//! It does not draw. A claim about pixels still needs a real surface.
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
use std::{
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
mpsc::{Receiver, SyncSender, sync_channel},
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// There is no loop here to post to, so updates queue until the test asks
|
||||||
|
/// for them.
|
||||||
|
struct Queue(SyncSender<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>);
|
||||||
|
|
||||||
|
impl TaskQueue<DefaultRsc<HarnessState>> for Queue {
|
||||||
|
fn send(&self, update: Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>) {
|
||||||
|
let _ = self.0.send(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `assert_eq!` for where a frame put a widget, written as its two corners.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! assert_corners {
|
||||||
|
($harness:expr, $id:expr, ($x0:expr, $y0:expr), ($x1:expr, $y1:expr)) => {
|
||||||
|
assert_eq!(
|
||||||
|
$harness.region(&$id).expect("widget drew nothing"),
|
||||||
|
$crate::core::PixelRegion {
|
||||||
|
top_left: $crate::core::util::Vec2::new($x0 as f32, $y0 as f32),
|
||||||
|
bot_right: $crate::core::util::Vec2::new($x1 as f32, $y1 as f32),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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)]
|
||||||
|
pub struct HarnessState {
|
||||||
|
pub root: Option<StrongWidget>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasRoot for HarnessState {
|
||||||
|
fn set_root(&mut self, root: StrongWidget) {
|
||||||
|
self.root = Some(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Harness {
|
||||||
|
pub rsc: DefaultRsc<HarnessState>,
|
||||||
|
pub render: UiRenderState,
|
||||||
|
pub state: HarnessState,
|
||||||
|
updates: Receiver<Box<dyn TaskUpdate<DefaultRsc<HarnessState>>>>,
|
||||||
|
cursor: CursorState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Harness {
|
||||||
|
/// `size` is the output in physical pixels.
|
||||||
|
pub fn new(size: impl Into<Vec2>) -> Self {
|
||||||
|
// A `TaskQueue` must be `Sync`, which `mpsc::Sender` is not; the
|
||||||
|
// bound that comes with `SyncSender` is far past anything a test
|
||||||
|
// leaves unread.
|
||||||
|
let (send, updates) = sync_channel(1024);
|
||||||
|
let rsc = DefaultRsc::init(Arc::new(Queue(send)));
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
render.resize(size);
|
||||||
|
Self {
|
||||||
|
rsc,
|
||||||
|
render,
|
||||||
|
state: HarnessState::default(),
|
||||||
|
updates,
|
||||||
|
cursor: CursorState::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> Vec2 {
|
||||||
|
self.render.output_size()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||||
|
self.render.resize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the root and lays it out, so a pointer event has something to hit.
|
||||||
|
pub fn set_root<T>(&mut self, widget: impl WidgetLike<DefaultRsc<HarnessState>, T>) {
|
||||||
|
widget.set_root(&mut self.rsc, &mut self.state);
|
||||||
|
self.frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn needs_redraw(&self) -> bool {
|
||||||
|
self.render
|
||||||
|
.needs_redraw(&self.state.root, self.rsc.widgets())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_updates(&mut self) -> usize {
|
||||||
|
let mut applied = 0;
|
||||||
|
while let Ok(update) = self.updates.try_recv() {
|
||||||
|
update(&mut self.state, &mut self.rsc);
|
||||||
|
applied += 1;
|
||||||
|
}
|
||||||
|
applied
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for a task's first update, then applies everything waiting.
|
||||||
|
/// False if none arrived in time.
|
||||||
|
#[must_use]
|
||||||
|
pub fn await_update(&mut self, timeout: Duration) -> bool {
|
||||||
|
let Ok(update) = self.updates.recv_timeout(timeout) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
update(&mut self.state, &mut self.rsc);
|
||||||
|
self.apply_updates();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays the tree out and builds its primitives.
|
||||||
|
pub fn frame(&mut self) {
|
||||||
|
self.apply_updates();
|
||||||
|
self.render.update(&self.state.root, &mut self.rsc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the last frame put a widget, or `None` if it drew nothing.
|
||||||
|
pub fn region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
||||||
|
self.render.window_region(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_to(&mut self, pos: impl Into<Vec2>) {
|
||||||
|
self.cursor.pos = pos.into();
|
||||||
|
self.cursor.exists = true;
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leave(&mut self) {
|
||||||
|
self.cursor.exists = false;
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn press(&mut self, button: CursorButton) {
|
||||||
|
self.button(button).update(true);
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(&mut self, button: CursorButton) {
|
||||||
|
self.button(button).update(false);
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wheel carries no position, so this goes wherever the cursor was last
|
||||||
|
/// moved to -- nowhere, until it has been moved.
|
||||||
|
pub fn scroll(&mut self, delta: impl Into<Vec2>) {
|
||||||
|
self.cursor.scroll_delta = delta.into();
|
||||||
|
self.sense();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn click(&mut self, pos: impl Into<Vec2>) {
|
||||||
|
self.move_to(pos);
|
||||||
|
self.press(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 {
|
||||||
|
let buttons = &mut self.cursor.buttons;
|
||||||
|
match button {
|
||||||
|
CursorButton::Left => &mut buttons.left,
|
||||||
|
CursorButton::Middle => &mut buttons.middle,
|
||||||
|
CursorButton::Right => &mut buttons.right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatches against the layout of the last frame, which is what a
|
||||||
|
/// window delivers input against too.
|
||||||
|
fn sense(&mut self) {
|
||||||
|
let cursor = self.cursor.clone();
|
||||||
|
self.render
|
||||||
|
.run_sensors(&mut self.rsc, &mut self.state, cursor);
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
pub mod default;
|
pub mod default;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
pub mod harness;
|
||||||
|
pub mod random;
|
||||||
pub mod widget;
|
pub mod widget;
|
||||||
|
|
||||||
pub use iris_core as core;
|
pub use iris_core as core;
|
||||||
|
|||||||
+317
@@ -0,0 +1,317 @@
|
|||||||
|
//! A seeded random widget tree, for tests and for looking at.
|
||||||
|
//!
|
||||||
|
//! One seed is one tree, on any machine and after any upgrade, so a test can
|
||||||
|
//! grow the same tree twice and a failing seed is reproduced by its number.
|
||||||
|
//! `examples/random.rs` draws one; `tests/generated.rs` checks that laying one
|
||||||
|
//! out again lands where growing it from scratch would.
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// The declared lengths of one `SetSize`, by axis.
|
||||||
|
pub type Lens = [Option<Len>; 2];
|
||||||
|
|
||||||
|
/// What a test changes between two trees grown from the same seed, so the
|
||||||
|
/// warm one can be mutated and the cold one grown that way to begin with.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Edits {
|
||||||
|
/// Declared sizes, by the order the `SetSize` wrappers were made.
|
||||||
|
pub sizes: HashMap<usize, Lens>,
|
||||||
|
/// Which children a span has, by the order the spans were made.
|
||||||
|
pub spans: HashMap<usize, SpanEdit>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct SpanEdit {
|
||||||
|
/// Children to leave out, by index among the ones grown.
|
||||||
|
pub detach: Vec<usize>,
|
||||||
|
/// How many of the span's spares are in it, appended in order.
|
||||||
|
pub attach: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// xorshift64, written out rather than taken from a crate so that a seed
|
||||||
|
/// keeps meaning the same tree.
|
||||||
|
pub struct Rng(u64);
|
||||||
|
|
||||||
|
impl Rng {
|
||||||
|
pub fn new(seed: u64) -> Self {
|
||||||
|
Self(seed | 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bits(&mut self) -> u64 {
|
||||||
|
self.0 ^= self.0 << 13;
|
||||||
|
self.0 ^= self.0 >> 7;
|
||||||
|
self.0 ^= self.0 << 17;
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn below(&mut self, n: usize) -> usize {
|
||||||
|
(self.bits() % n as u64) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chance(&mut self) -> bool {
|
||||||
|
self.bits() & 1 == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLORS: [UiColor; 6] = [
|
||||||
|
UiColor::RED,
|
||||||
|
UiColor::GREEN,
|
||||||
|
UiColor::BLUE,
|
||||||
|
UiColor::YELLOW,
|
||||||
|
UiColor::CYAN,
|
||||||
|
UiColor::MAGENTA,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Leaves grown beside every span, for a test to put into it.
|
||||||
|
const SPARES: usize = 3;
|
||||||
|
|
||||||
|
const WORDS: &str = "Wrapping shapes one source into as many lines as the box \
|
||||||
|
leaves room for, so a paragraph's height is an answer and not a setting.";
|
||||||
|
|
||||||
|
/// What growing a tree gives back: every widget in creation order, so two
|
||||||
|
/// trees from one seed line up index for index, and the declared sizes, which
|
||||||
|
/// are what a test changes to watch the change propagate.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Tree {
|
||||||
|
pub ids: Vec<WidgetId>,
|
||||||
|
pub sized: Vec<WeakWidget<SetSize>>,
|
||||||
|
pub spans: Vec<Spanned>,
|
||||||
|
pub scrolls: Vec<WeakWidget<Scroll>>,
|
||||||
|
/// Children a `SpanEdit` took out, held so that dropping the last share
|
||||||
|
/// of one does not free its id for the next widget to be given -- which
|
||||||
|
/// would put the two trees' `ids` out of step.
|
||||||
|
pub detached: Vec<StrongWidget>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Branches on a child's measured length. Comparing boxes catches a widget
|
||||||
|
/// that moved; this catches one that believed a measurement a cold start
|
||||||
|
/// would not have given it, by turning that into a different tree. Its own
|
||||||
|
/// configuration never changes, so which side draws is a property of the
|
||||||
|
/// layout alone.
|
||||||
|
pub struct Branch {
|
||||||
|
pub probe: StrongWidget,
|
||||||
|
pub wide: StrongWidget,
|
||||||
|
pub narrow: StrongWidget,
|
||||||
|
pub threshold: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Branch {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
let mut top = UiRegion::FULL;
|
||||||
|
top.y.end = top.y.start.offset(40.0);
|
||||||
|
let measured = painter.place(&self.probe, top).len(Axis::X);
|
||||||
|
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
|
||||||
|
|
||||||
|
let mut rest = UiRegion::FULL;
|
||||||
|
rest.y.start = rest.y.start.offset(40.0);
|
||||||
|
match px > self.threshold {
|
||||||
|
true => painter.place(&self.wide, rest),
|
||||||
|
false => painter.place(&self.narrow, rest),
|
||||||
|
};
|
||||||
|
Size::REST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Spanned {
|
||||||
|
pub id: WeakWidget<Span>,
|
||||||
|
/// Leaves grown with the span whether or not they end up in it, so both
|
||||||
|
/// trees make the same widgets in the same order either way. Attaching
|
||||||
|
/// one moves it out of here: a widget belongs to one parent, and one that
|
||||||
|
/// belongs to nobody still has to be held or it reads as a leak.
|
||||||
|
pub spares: Vec<StrongWidget>,
|
||||||
|
/// How many children it was grown with, before any edit.
|
||||||
|
pub grown: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it
|
||||||
|
/// would otherwise have given those wrappers.
|
||||||
|
pub fn grow<Rsc: UiRsc + 'static>(
|
||||||
|
rsc: &mut Rsc,
|
||||||
|
seed: u64,
|
||||||
|
depth: usize,
|
||||||
|
edits: &Edits,
|
||||||
|
) -> (StrongWidget, Tree) {
|
||||||
|
let mut grow = Grow {
|
||||||
|
rsc,
|
||||||
|
rng: Rng::new(seed),
|
||||||
|
tree: Tree::default(),
|
||||||
|
edits,
|
||||||
|
};
|
||||||
|
let root = grow.node(depth);
|
||||||
|
(root, grow.tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Grow<'a, Rsc> {
|
||||||
|
rsc: &'a mut Rsc,
|
||||||
|
rng: Rng,
|
||||||
|
tree: Tree,
|
||||||
|
edits: &'a Edits,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
|
||||||
|
fn leaf(&mut self) -> StrongWidget {
|
||||||
|
let id: StrongWidget = match self.rng.below(4) {
|
||||||
|
// Wrapped and unwrapped, because only one of them reads the width
|
||||||
|
// it is given and so only one has to be drawn again for a new one.
|
||||||
|
0 => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
|
||||||
|
1 => wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add_strong(self.rsc),
|
||||||
|
_ => {
|
||||||
|
let color = COLORS[self.rng.below(COLORS.len())];
|
||||||
|
let alpha = (self.rng.below(5) * 63) as u8;
|
||||||
|
rect(color.alpha(alpha)).add_strong(self.rsc)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.tree.ids.push(id.id());
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&mut self) -> Option<Len> {
|
||||||
|
match self.rng.below(4) {
|
||||||
|
0 => Some(Len::px(20.0 + self.rng.below(180) as f32)),
|
||||||
|
1 => Some(Len::REST),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn align(&mut self) -> Align {
|
||||||
|
let mut axis = || match self.rng.below(4) {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(AxisAlign::Neg),
|
||||||
|
2 => Some(AxisAlign::Center),
|
||||||
|
_ => Some(AxisAlign::Pos),
|
||||||
|
};
|
||||||
|
let (mut x, y) = (axis(), axis());
|
||||||
|
// Aligning on neither axis is just another transparent wrapper and
|
||||||
|
// would leave this branch unexercised.
|
||||||
|
if x.is_none() && y.is_none() {
|
||||||
|
x = Some(AxisAlign::Center);
|
||||||
|
}
|
||||||
|
Align { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A declared size over half the tree, kept where a test can change it.
|
||||||
|
fn sized(&mut self, inner: StrongWidget) -> StrongWidget {
|
||||||
|
if !self.rng.chance() {
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
let idx = self.tree.sized.len();
|
||||||
|
let lens = [self.len(), self.len()];
|
||||||
|
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
|
||||||
|
let id = SetSize {
|
||||||
|
inner,
|
||||||
|
x: lens[0],
|
||||||
|
y: lens[1],
|
||||||
|
}
|
||||||
|
.add(self.rsc);
|
||||||
|
self.tree.sized.push(id);
|
||||||
|
self.tree.ids.push(id.id());
|
||||||
|
id.add_strong(self.rsc)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node(&mut self, depth: usize) -> StrongWidget {
|
||||||
|
if depth == 0 {
|
||||||
|
return self.leaf();
|
||||||
|
}
|
||||||
|
let positioned = self.rng.below(6);
|
||||||
|
if positioned == 0 {
|
||||||
|
// Scrolling reads the pixel length of its box, which nothing
|
||||||
|
// else here does, and gives its child a box longer than its own.
|
||||||
|
let inner = self.node(depth - 1);
|
||||||
|
let inner = self.sized(inner);
|
||||||
|
let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
|
||||||
|
let id = Scroll::new(inner, axis).add(self.rsc);
|
||||||
|
self.tree.scrolls.push(id);
|
||||||
|
self.tree.ids.push(id.id());
|
||||||
|
return id.add_strong(self.rsc);
|
||||||
|
}
|
||||||
|
if positioned == 2 {
|
||||||
|
// Both sides are grown either way, so a tree that draws one has
|
||||||
|
// the same ids as a tree that draws the other.
|
||||||
|
let probe = self.node(depth - 1);
|
||||||
|
let wide = self.node(depth - 1);
|
||||||
|
let narrow = self.node(depth - 1);
|
||||||
|
let threshold = self.rng.below(500) as f32;
|
||||||
|
let id = Branch {
|
||||||
|
probe,
|
||||||
|
wide,
|
||||||
|
narrow,
|
||||||
|
threshold,
|
||||||
|
}
|
||||||
|
.add(self.rsc);
|
||||||
|
self.tree.ids.push(id.id());
|
||||||
|
return id.add_strong(self.rsc);
|
||||||
|
}
|
||||||
|
if positioned == 1 {
|
||||||
|
let inner = self.node(depth - 1);
|
||||||
|
let inner = self.sized(inner);
|
||||||
|
let id = Aligned {
|
||||||
|
inner,
|
||||||
|
align: self.align(),
|
||||||
|
}
|
||||||
|
.add_strong(self.rsc);
|
||||||
|
self.tree.ids.push(id.id());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
if self.rng.below(4) == 0 {
|
||||||
|
let inner = self.node(depth - 1);
|
||||||
|
let inner = self.sized(inner);
|
||||||
|
// Each side its own, since a padding that is the same all round
|
||||||
|
// hides anything that treats one edge differently from another.
|
||||||
|
let mut side = || self.rng.below(24) as f32;
|
||||||
|
let padding = Padding {
|
||||||
|
left: side(),
|
||||||
|
right: side(),
|
||||||
|
top: side(),
|
||||||
|
bottom: side(),
|
||||||
|
};
|
||||||
|
let id = Pad { padding, inner }.add_strong(self.rsc);
|
||||||
|
self.tree.ids.push(id.id());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
let grown = 2 + self.rng.below(3);
|
||||||
|
let mut children = Vec::with_capacity(grown);
|
||||||
|
for _ in 0..grown {
|
||||||
|
let child = self.node(depth - 1);
|
||||||
|
children.push(self.sized(child));
|
||||||
|
}
|
||||||
|
if self.rng.chance() {
|
||||||
|
let id = Stack {
|
||||||
|
children,
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
}
|
||||||
|
.add_strong(self.rsc);
|
||||||
|
self.tree.ids.push(id.id());
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
// Grown either way, so the widget after them has the same id in a
|
||||||
|
// tree that leaves them out as in one that puts them in.
|
||||||
|
let mut spares: Vec<StrongWidget> = (0..SPARES).map(|_| self.leaf()).collect();
|
||||||
|
let idx = self.tree.spans.len();
|
||||||
|
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
|
||||||
|
// Highest first, so an index means the same child however many of its
|
||||||
|
// neighbours are going too.
|
||||||
|
let mut detach = edit.detach.clone();
|
||||||
|
detach.sort_unstable();
|
||||||
|
for j in detach.into_iter().rev() {
|
||||||
|
if j < children.len() {
|
||||||
|
self.tree.detached.push(children.remove(j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let attach = edit.attach.min(spares.len());
|
||||||
|
children.extend(spares.drain(..attach));
|
||||||
|
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)];
|
||||||
|
let id = Span {
|
||||||
|
children,
|
||||||
|
dir,
|
||||||
|
gap: self.rng.below(3) as f32 * 4.0,
|
||||||
|
}
|
||||||
|
.add(self.rsc);
|
||||||
|
self.tree.ids.push(id.id());
|
||||||
|
self.tree.spans.push(Spanned { id, spares, grown });
|
||||||
|
id.add_strong(self.rsc)
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-5
@@ -6,16 +6,17 @@ pub struct Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Image {
|
impl Widget for Image {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
painter.primitive(&self.handle);
|
painter.primitive(&self.handle);
|
||||||
|
Size::px(self.handle.size())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
fn size_hint(&self, axis: Axis) -> Option<Len> {
|
||||||
Len::abs(self.handle.size().x)
|
Some(Len::px(self.handle.size().axis(axis)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
Len::abs(self.handle.size().y)
|
OnResize::Scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-8
@@ -5,16 +5,13 @@ pub struct Masked {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Masked {
|
impl Widget for Masked {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
painter.set_mask(painter.region());
|
painter.set_mask(painter.region());
|
||||||
painter.widget(&self.inner);
|
painter.widget(&self.inner).size()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
/// It clips to the box it was given, not to the part its child used.
|
||||||
ctx.width(&self.inner)
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
}
|
OnResize::Redraw
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
ctx.height(&self.inner)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,30 +6,37 @@ pub struct Aligned {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Aligned {
|
impl Widget for Aligned {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
let known = match self.align.tuple() {
|
||||||
|
(Some(_), Some(_)) => painter
|
||||||
|
.known_len(&self.inner, Axis::X, UiRegion::FULL)
|
||||||
|
.zip(painter.known_len(&self.inner, Axis::Y, UiRegion::FULL))
|
||||||
|
.map(|(x, y)| Size { x, y }),
|
||||||
|
(Some(_), None) => painter
|
||||||
|
.known_len(&self.inner, Axis::X, UiRegion::FULL)
|
||||||
|
.map(|x| Size { x, y: Len::REST }),
|
||||||
|
(None, Some(_)) => painter
|
||||||
|
.known_len(&self.inner, Axis::Y, UiRegion::FULL)
|
||||||
|
.map(|y| Size { x: Len::REST, y }),
|
||||||
|
(None, None) => Some(Size::REST),
|
||||||
|
};
|
||||||
|
// Drawn where it may be too big only when the aligned axes are not
|
||||||
|
// already known, then given its aligned box once its size is known.
|
||||||
|
let had_size = known.is_some();
|
||||||
|
let size = known.unwrap_or_else(|| painter.place(&self.inner, UiRegion::FULL).size());
|
||||||
let region = match self.align.tuple() {
|
let region = match self.align.tuple() {
|
||||||
(Some(x), Some(y)) => painter
|
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
|
||||||
.size(&self.inner)
|
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
|
||||||
.to_uivec2()
|
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
|
||||||
.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,
|
||||||
};
|
};
|
||||||
painter.widget_within(&self.inner, region);
|
let placed = painter.place(&self.inner, region).size();
|
||||||
|
if had_size { placed } else { size }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
/// The aligned box is a fraction of its own, so the child keeps its
|
||||||
ctx.width(&self.inner)
|
/// length and stays against the edge it was aligned to.
|
||||||
}
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
|
OnResize::Scale
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
ctx.height(&self.inner)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,18 +6,14 @@ pub struct LayerOffset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for LayerOffset {
|
impl Widget for LayerOffset {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
for _ in 0..self.offset {
|
for _ in 0..self.offset {
|
||||||
painter.next_layer();
|
painter.next_layer();
|
||||||
}
|
}
|
||||||
painter.widget(&self.inner);
|
painter.widget(&self.inner).size()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
ctx.width(&self.inner)
|
OnResize::Scale
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
ctx.height(&self.inner)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,43 +6,20 @@ pub struct MaxSize {
|
|||||||
pub y: Option<Len>,
|
pub y: Option<Len>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MaxSize {
|
|
||||||
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 MaxSize {
|
impl Widget for MaxSize {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
painter.widget(&self.inner);
|
let child = painter.widget(&self.inner).size();
|
||||||
}
|
let output = painter.output_size();
|
||||||
|
Size {
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
x: capped(child.x, self.x, output.x),
|
||||||
self.apply_to_outer(ctx);
|
y: capped(child.y, self.y, output.y),
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn capped(len: Len, max: Option<Len>, output: f32) -> Len {
|
||||||
|
match max {
|
||||||
|
Some(max) if len.apply_rest().to_px(output) > max.apply_rest().to_px(output) => max,
|
||||||
|
_ => len,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,16 +6,12 @@ pub struct Offset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Offset {
|
impl Widget for Offset {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let region = UiRegion::FULL.offset(self.amt);
|
let region = UiRegion::FULL.offset(self.amt);
|
||||||
painter.widget_within(&self.inner, region);
|
painter.widget_within(&self.inner, region).size()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
ctx.width(&self.inner)
|
OnResize::Scale
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
ctx.height(&self.inner)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+22
-24
@@ -6,28 +6,26 @@ pub struct Pad {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Pad {
|
impl Widget for Pad {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
painter.widget_within(&self.inner, self.padding.region());
|
let inner = painter
|
||||||
|
.widget_within(&self.inner, self.padding.region())
|
||||||
|
.size();
|
||||||
|
Size {
|
||||||
|
x: Len {
|
||||||
|
px: inner.x.px + self.padding.left + self.padding.right,
|
||||||
|
..inner.x
|
||||||
|
},
|
||||||
|
y: Len {
|
||||||
|
px: inner.y.px + self.padding.top + self.padding.bottom,
|
||||||
|
..inner.y
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
/// The padding is an offset from each edge, so a longer box pads the same
|
||||||
let width = self.padding.left + self.padding.right;
|
/// amount and the child takes the rest.
|
||||||
let height = self.padding.top + self.padding.bottom;
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
ctx.outer.x.abs -= width;
|
OnResize::Scale
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,10 +55,10 @@ impl Padding {
|
|||||||
}
|
}
|
||||||
pub fn region(&self) -> UiRegion {
|
pub fn region(&self) -> UiRegion {
|
||||||
let mut region = UiRegion::FULL;
|
let mut region = UiRegion::FULL;
|
||||||
region.x.start.abs += self.left;
|
region.x.start.px += self.left;
|
||||||
region.y.start.abs += self.top;
|
region.y.start.px += self.top;
|
||||||
region.x.end.abs -= self.right;
|
region.x.end.px -= self.right;
|
||||||
region.y.end.abs -= self.bottom;
|
region.y.end.px -= self.bottom;
|
||||||
region
|
region
|
||||||
}
|
}
|
||||||
pub fn x(amt: impl UiNum) -> Self {
|
pub fn x(amt: impl UiNum) -> Self {
|
||||||
|
|||||||
@@ -10,15 +10,21 @@ pub struct Scroll {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Scroll {
|
impl Widget for Scroll {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let output_len = painter.output_size().axis(self.axis);
|
let output_len = painter.output_len(self.axis);
|
||||||
let container_len = painter.region().axis(self.axis).len();
|
// Its size is its content's, whatever box that is scrolled within.
|
||||||
let content_len = painter
|
let container_len = UiScalar::px(painter.px_len_for_draw(self.axis));
|
||||||
.len_axis(&self.inner, self.axis)
|
// Draw in the whole container only when its scrolling-axis length is
|
||||||
|
// not already known, then place it at the scrolled offset.
|
||||||
|
let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL);
|
||||||
|
let measured = known_len.is_none();
|
||||||
|
let child = measured.then(|| painter.place(&self.inner, UiRegion::FULL).size());
|
||||||
|
let content_len = known_len
|
||||||
|
.unwrap_or_else(|| child.unwrap().axis(self.axis))
|
||||||
.apply_rest()
|
.apply_rest()
|
||||||
.within_len(container_len)
|
.within_len(container_len)
|
||||||
.to_abs(output_len);
|
.to_px(output_len);
|
||||||
self.container_len = container_len.to_abs(output_len);
|
self.container_len = container_len.to_px(output_len);
|
||||||
self.content_len = content_len;
|
self.content_len = content_len;
|
||||||
|
|
||||||
if self.snap_end {
|
if self.snap_end {
|
||||||
@@ -28,15 +34,8 @@ impl Widget for Scroll {
|
|||||||
|
|
||||||
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
|
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
|
||||||
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
||||||
painter.widget_within(&self.inner, region);
|
let placed = painter.place(&self.inner, region).size();
|
||||||
}
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,29 +6,38 @@ 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) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
painter.widget(&self.inner);
|
// A declared length is what the child gets, whatever box this widget
|
||||||
|
// was offered before its parent knew that. Measuring it anywhere else
|
||||||
|
// asks about a box it will not have, and the answer on the other axis
|
||||||
|
// is taken under that: a wrapping text measured in the whole width
|
||||||
|
// reports one line, and nothing revisits it once the real width
|
||||||
|
// arrives.
|
||||||
|
let mut region = UiRegion::FULL;
|
||||||
|
for (axis, len) in [(Axis::X, self.x), (Axis::Y, self.y)] {
|
||||||
|
if let Some(len) = len {
|
||||||
|
let span = region.axis_mut(axis);
|
||||||
|
span.end = span.start + len.apply_rest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let child = painter.widget_within(&self.inner, region).size();
|
||||||
|
Size {
|
||||||
|
x: self.x.unwrap_or(child.x),
|
||||||
|
y: self.y.unwrap_or(child.y),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
/// A declared axis is known without looking at the child, which is what
|
||||||
self.apply_to_outer(ctx);
|
/// lets a span lay out around `.height(rest(1))` without drawing it.
|
||||||
self.x.unwrap_or_else(|| ctx.width(&self.inner))
|
fn size_hint(&self, axis: Axis) -> Option<Len> {
|
||||||
|
match axis {
|
||||||
|
Axis::X => self.x,
|
||||||
|
Axis::Y => self.y,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
self.apply_to_outer(ctx);
|
OnResize::Scale
|
||||||
self.y.unwrap_or_else(|| ctx.height(&self.inner))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+52
-103
@@ -8,43 +8,73 @@ pub struct Span {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Span {
|
impl Widget for Span {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let total = self.len_sum(&mut painter.size_ctx());
|
let axis = self.dir.axis;
|
||||||
let mut start = UiScalar::rel_min();
|
// 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 {
|
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.px += len.px + 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::px(gap), |sum, len| sum + *len);
|
||||||
|
|
||||||
|
let mut start = UiScalar::rel_min();
|
||||||
|
let mut ortho = Len::ZERO;
|
||||||
|
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.px);
|
||||||
let rel_end = UiScalar::rel(len.rest / total.rest);
|
let rel_end = UiScalar::rel(len.rest / total.rest);
|
||||||
let end = (UiScalar::rel_max() + start) - offset;
|
let end = (UiScalar::rel_max() + start) - offset;
|
||||||
start = rel_end.within(&start.to(end));
|
start = rel_end.within(&start.to(end));
|
||||||
}
|
}
|
||||||
start.abs += len.abs;
|
start.px += len.px;
|
||||||
start.rel += len.rel;
|
start.rel += len.rel;
|
||||||
span.end = start;
|
span.end = start;
|
||||||
let mut child_region = UiRegion::from_axis(self.dir.axis, span, UiSpan::FULL);
|
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
||||||
if self.dir.sign == Sign::Neg {
|
if self.dir.sign == Sign::Neg {
|
||||||
child_region.flip(self.dir.axis);
|
region.flip(axis);
|
||||||
}
|
}
|
||||||
painter.widget_within(child, child_region);
|
let used = painter.place(child, region).size().axis(!axis);
|
||||||
start.abs += self.gap;
|
// 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.px = ortho.px.max(used.px);
|
||||||
|
}
|
||||||
|
start.px += self.gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Carried whole rather than collapsed to one share: a span that sizes
|
||||||
|
// from its children does not resolve `rest`, it passes the weight up,
|
||||||
|
// so nesting spans divides the same space rather than re-dividing a
|
||||||
|
// share of it. Four `rest(1)` children under two spans under one span
|
||||||
|
// get a quarter each, which collapsing to `rest(1)` per level does
|
||||||
|
// not give. Resolution happens at the nearest ancestor with a length,
|
||||||
|
// and the root always has one.
|
||||||
|
let along = total;
|
||||||
|
Size::from_axis(axis, along, ortho)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
/// Every child is placed in fractions and offsets of the span's own box,
|
||||||
match self.dir.axis {
|
/// so a longer box holds the same layout and the children follow it.
|
||||||
Axis::X => self.desired_len(ctx),
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
Axis::Y => self.desired_ortho(ctx),
|
OnResize::Scale
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,87 +99,6 @@ 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> {
|
||||||
|
|||||||
@@ -8,30 +8,29 @@ pub struct Stack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Stack {
|
impl Widget for Stack {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let mut iter = self.children.iter();
|
let sizing = match self.size {
|
||||||
if let Some(child) = iter.next() {
|
StackSize::Default => None,
|
||||||
painter.child_layer();
|
StackSize::Child(i) => Some(i),
|
||||||
painter.widget(child);
|
};
|
||||||
}
|
let mut size = Size::default();
|
||||||
for child in iter {
|
for (i, child) in self.children.iter().enumerate() {
|
||||||
painter.next_layer();
|
match i {
|
||||||
painter.widget(child);
|
0 => painter.child_layer(),
|
||||||
|
_ => painter.next_layer(),
|
||||||
|
}
|
||||||
|
let drawn = painter.widget(child);
|
||||||
|
// Only the child that sizes the stack is read, so the others
|
||||||
|
// changing size does not redraw it.
|
||||||
|
if sizing == Some(i) {
|
||||||
|
size = drawn.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
match self.size {
|
OnResize::Scale
|
||||||
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]),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-19
@@ -6,25 +6,10 @@ pub struct WidgetPtr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for WidgetPtr {
|
impl Widget for WidgetPtr {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
if let Some(id) = &self.inner {
|
match &self.inner {
|
||||||
painter.widget(id);
|
Some(id) => painter.widget(id).size(),
|
||||||
}
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-5
@@ -28,21 +28,23 @@ impl Rect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Rect {
|
impl Widget for Rect {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
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 desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
fn size_hint(&self, _: Axis) -> Option<Len> {
|
||||||
Len::rest(1)
|
Some(Len::REST)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
/// Its box is its primitive's own region, so a new one is written there.
|
||||||
Len::rest(1)
|
fn on_resize(&self, _: Axis) -> OnResize {
|
||||||
|
OnResize::Scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-18
@@ -55,44 +55,47 @@ impl TextEdit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for TextEdit {
|
impl Widget for TextEdit {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
let base = painter.layer;
|
let base = painter.layer;
|
||||||
painter.child_layer();
|
painter.child_layer();
|
||||||
self.view.draw(painter);
|
let (_, size) = 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;
|
return size;
|
||||||
};
|
};
|
||||||
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 size = vec2(rect.width() as f32, rect.height() as f32);
|
let rect_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),
|
||||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
rect_size
|
||||||
|
.align(Align::TOP_LEFT)
|
||||||
|
.offset(top_left)
|
||||||
|
.within(®ion),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
||||||
let size = vec2(caret.width() as f32, caret.height() as f32);
|
let caret_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),
|
||||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
caret_size
|
||||||
|
.align(Align::TOP_LEFT)
|
||||||
|
.offset(top_left)
|
||||||
|
.within(®ion),
|
||||||
);
|
);
|
||||||
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn on_resize(&self, axis: Axis) -> OnResize {
|
||||||
self.view.desired_width(ctx)
|
self.view.on_resize(axis)
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
self.view.desired_height(ctx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +130,6 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
pub fn set(&mut self, text: &str) {
|
pub fn set(&mut self, text: &str) {
|
||||||
let text = self.string(text);
|
let text = self.string(text);
|
||||||
self.text.view.buf.set_text(text);
|
self.text.view.buf.set_text(text);
|
||||||
self.text.view.buf.changed = true;
|
|
||||||
self.text.selection = None;
|
self.text.selection = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +176,6 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
};
|
};
|
||||||
let at = at.min(self.text.view.buf.text().len());
|
let at = at.min(self.text.view.buf.text().len());
|
||||||
self.text.view.buf.edit().insert_str(at, text);
|
self.text.view.buf.edit().insert_str(at, text);
|
||||||
self.text.view.buf.changed = true;
|
|
||||||
self.set_caret(at + text.len());
|
self.set_caret(at + text.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +188,6 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
let range = sel.text_range();
|
let range = sel.text_range();
|
||||||
self.text.view.buf.edit().replace_range(range.clone(), "");
|
self.text.view.buf.edit().replace_range(range.clone(), "");
|
||||||
self.text.view.buf.changed = true;
|
|
||||||
self.set_caret(range.start);
|
self.set_caret(range.start);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -265,7 +265,6 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
|
|
||||||
fn delete_range(&mut self, start: usize, end: usize) {
|
fn delete_range(&mut self, start: usize, end: usize) {
|
||||||
self.text.view.buf.edit().replace_range(start..end, "");
|
self.text.view.buf.edit().replace_range(start..end, "");
|
||||||
self.text.view.buf.changed = true;
|
|
||||||
self.set_caret(start);
|
self.set_caret(start);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +280,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
||||||
let pos = pos - self.text.region().top_left().to_abs(size);
|
let pos = pos - self.text.region().top_left().to_px(size);
|
||||||
let prev_sel = self.text.selection;
|
let prev_sel = self.text.selection;
|
||||||
let prev_hit = self.text.double_hit;
|
let prev_hit = self.text.double_hit;
|
||||||
|
|
||||||
|
|||||||
+45
-61
@@ -14,11 +14,8 @@ pub struct Text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct TextView {
|
pub struct TextView {
|
||||||
pub attrs: MutDetect<TextAttrs>,
|
pub attrs: TextAttrs,
|
||||||
pub buf: MutDetect<TextBuffer>,
|
pub buf: TextBuffer,
|
||||||
// cache
|
|
||||||
tex: Option<RenderedText>,
|
|
||||||
width: Option<f32>,
|
|
||||||
pub hint: Option<StrongWidget>,
|
pub hint: Option<StrongWidget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,19 +25,13 @@ impl TextView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn wrap_width(&self) -> Option<f32> {
|
pub fn wrap_width(&self) -> Option<f32> {
|
||||||
self.width
|
self.buf.wrap_width()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextView {
|
impl TextView {
|
||||||
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
|
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
|
||||||
Self {
|
Self { attrs, buf, hint }
|
||||||
attrs: attrs.into(),
|
|
||||||
buf: buf.into(),
|
|
||||||
tex: None,
|
|
||||||
width: None,
|
|
||||||
hint,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// region where the text should be draw
|
/// region where the text should be draw
|
||||||
@@ -52,56 +43,55 @@ impl TextView {
|
|||||||
.align(self.align)
|
.align(self.align)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, ctx: &mut SizeCtx) -> &RenderedText {
|
/// The text shaped for the width it is drawn in. The buffer keeps its
|
||||||
|
/// answers under the attrs too, so changing those asks a new question
|
||||||
|
/// rather than invalidating anything.
|
||||||
|
fn render(&mut self, painter: &mut Painter) -> &RenderedText {
|
||||||
let width = if self.attrs.wrap {
|
let width = if self.attrs.wrap {
|
||||||
Some(ctx.px_size().x)
|
Some(painter.px_len(Axis::X))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
|
painter.render_text(&mut self.buf, &self.attrs, width)
|
||||||
self.width = width;
|
|
||||||
self.tex = Some(ctx.draw_text(&mut self.buf, &self.attrs, width));
|
|
||||||
self.attrs.changed = false;
|
|
||||||
self.buf.changed = false;
|
|
||||||
}
|
|
||||||
self.tex.as_ref().unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tex(&self) -> Option<&RenderedText> {
|
pub fn tex(&self) -> Option<&RenderedText> {
|
||||||
self.tex.as_ref()
|
self.buf.rendered()
|
||||||
}
|
}
|
||||||
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
/// Draws the text, and says where the glyphs went and what they use.
|
||||||
if self.is_empty()
|
pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) {
|
||||||
&& 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(&mut painter.size_ctx()).size.align(align);
|
let region = self.render(painter).size.align(align);
|
||||||
if let Some(hint) = &self.hint {
|
let size = match &self.hint {
|
||||||
painter.widget(hint);
|
Some(hint) => painter.widget(hint).size(),
|
||||||
}
|
None => Size::ZERO,
|
||||||
return region;
|
};
|
||||||
|
return (region, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tex = self.render(&mut painter.size_ctx());
|
let tex = self.render(painter);
|
||||||
let region = tex.size.align(align);
|
let region = tex.size.align(align);
|
||||||
|
let size = Size::px(tex.size);
|
||||||
let within = region.within(&painter.region());
|
let within = region.within(&painter.region());
|
||||||
painter.glyphs(tex, within);
|
painter.glyphs(tex, within);
|
||||||
region
|
(region, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
@@ -117,7 +107,7 @@ impl Text {
|
|||||||
content: content.into(),
|
content: content.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn update_buf(&mut self, _ctx: &mut SizeCtx) {
|
fn update_buf(&mut self) {
|
||||||
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());
|
||||||
@@ -126,19 +116,13 @@ impl Text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Text {
|
impl Widget for Text {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
self.update_buf(&mut painter.size_ctx());
|
self.update_buf();
|
||||||
self.view.draw(painter);
|
self.view.draw(painter).1
|
||||||
}
|
}
|
||||||
|
|
||||||
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
fn on_resize(&self, axis: Axis) -> OnResize {
|
||||||
self.update_buf(ctx);
|
self.view.on_resize(axis)
|
||||||
self.view.desired_width(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
|
||||||
self.update_buf(ctx);
|
|
||||||
self.view.desired_height(ctx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
//! What the vertex shader's move-chain walk costs, against how deep the chain
|
||||||
|
//! is. Every active widget owns a slot, so the depth a primitive resolves
|
||||||
|
//! through is its depth in the widget tree.
|
||||||
|
//!
|
||||||
|
//! cargo test --release --test chain_cost -- --ignored --nocapture
|
||||||
|
//!
|
||||||
|
//! Timed on the GPU with timestamp queries rather than by the clock: wall time
|
||||||
|
//! here varied by 2x between runs of one unchanged binary. The pass is
|
||||||
|
//! submitted and waited on, so this is the GPU's cost and not the recording
|
||||||
|
//! loop's -- which is what `draw_cost.rs` measures instead.
|
||||||
|
//!
|
||||||
|
//! The instances are two pixels wide so that vertex work dominates; a chain
|
||||||
|
//! walk that does not show up against small quads will not show up against
|
||||||
|
//! anything.
|
||||||
|
//!
|
||||||
|
//! The instance is leaked deliberately, for the reason `draw_cost.rs` gives.
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris_core::{
|
||||||
|
MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
|
||||||
|
UiScalar, UiSpan,
|
||||||
|
};
|
||||||
|
use wgpu::{Color as GpuColor, *};
|
||||||
|
|
||||||
|
const SIZE: u32 = 1024;
|
||||||
|
const INSTANCES: usize = 200_000;
|
||||||
|
const FRAMES: u32 = 20;
|
||||||
|
/// Reported as the best of this many batches, since the mean moves by more
|
||||||
|
/// than the thing being measured.
|
||||||
|
const BATCHES: u32 = 8;
|
||||||
|
|
||||||
|
fn gpu() -> Option<(Device, Queue, f32)> {
|
||||||
|
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
||||||
|
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
||||||
|
{
|
||||||
|
Ok(_) => all,
|
||||||
|
Err(_) => Instance::new(InstanceDescriptor {
|
||||||
|
backends: Backends::GL,
|
||||||
|
..InstanceDescriptor::new_without_display_handle()
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let instance: &'static Instance = Box::leak(Box::new(instance));
|
||||||
|
let adapter =
|
||||||
|
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
||||||
|
if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
|
||||||
|
println!("no timestamp queries on {:?}", adapter.get_info().name);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
println!("adapter: {:?}", adapter.get_info().name);
|
||||||
|
let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor {
|
||||||
|
required_features: Features::TIMESTAMP_QUERY,
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.ok()?;
|
||||||
|
let period = queue.get_timestamp_period();
|
||||||
|
Some((device, queue, period))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config(format: TextureFormat) -> SurfaceConfiguration {
|
||||||
|
SurfaceConfiguration {
|
||||||
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format,
|
||||||
|
color_space: SurfaceColorSpace::Auto,
|
||||||
|
width: SIZE,
|
||||||
|
height: SIZE,
|
||||||
|
present_mode: PresentMode::Fifo,
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
alpha_mode: CompositeAlphaMode::Auto,
|
||||||
|
view_formats: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A chain `depth` slots long, and instances that all resolve through its end.
|
||||||
|
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
||||||
|
let kind = ui.primitives.kind::<RectPrimitive>();
|
||||||
|
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
||||||
|
|
||||||
|
let mut slot = MoveIdx::NONE;
|
||||||
|
for _ in 0..depth {
|
||||||
|
slot = render.moves.push(slot, UiRegion::FULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
let px = |v: f32| UiScalar { rel: 0.0, px: v };
|
||||||
|
for i in 0..INSTANCES {
|
||||||
|
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
|
||||||
|
let y = (i / (SIZE as usize / 2)) as f32;
|
||||||
|
render.layers.write(
|
||||||
|
0,
|
||||||
|
PrimitiveInst {
|
||||||
|
kind,
|
||||||
|
id,
|
||||||
|
primitive: RectPrimitive::color(UiColor::WHITE),
|
||||||
|
region: UiRegion::new(
|
||||||
|
UiSpan::new(px(x), px(x + 2.0)),
|
||||||
|
UiSpan::new(px(y), px(y + 1.0)),
|
||||||
|
),
|
||||||
|
mask_idx: MaskIdx::NONE,
|
||||||
|
move_idx: slot,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
|
||||||
|
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
|
||||||
|
let format = TextureFormat::Bgra8Unorm;
|
||||||
|
let mut node = UiRenderNode::new(device, &config(format));
|
||||||
|
let mut ui = UiData::default();
|
||||||
|
let mut render = UiRenderState::new();
|
||||||
|
fill(&mut ui, &mut render, depth);
|
||||||
|
node.update(device, queue, &mut ui, &mut render);
|
||||||
|
|
||||||
|
let target = device.create_texture(&TextureDescriptor {
|
||||||
|
label: Some("chain cost"),
|
||||||
|
size: Extent3d {
|
||||||
|
width: SIZE,
|
||||||
|
height: SIZE,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: TextureDimension::D2,
|
||||||
|
format,
|
||||||
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let view = target.create_view(&TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
let queries = device.create_query_set(&QuerySetDescriptor {
|
||||||
|
label: Some("chain cost"),
|
||||||
|
ty: QueryType::Timestamp,
|
||||||
|
count: 2,
|
||||||
|
});
|
||||||
|
let resolved = device.create_buffer(&BufferDescriptor {
|
||||||
|
label: Some("resolved"),
|
||||||
|
size: 16,
|
||||||
|
usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
let readback = device.create_buffer(&BufferDescriptor {
|
||||||
|
label: Some("readback"),
|
||||||
|
size: 16,
|
||||||
|
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let frame = || {
|
||||||
|
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
||||||
|
{
|
||||||
|
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
||||||
|
label: None,
|
||||||
|
color_attachments: &[Some(RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: Operations {
|
||||||
|
load: LoadOp::Clear(GpuColor::BLACK),
|
||||||
|
store: StoreOp::Store,
|
||||||
|
},
|
||||||
|
depth_slice: None,
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
timestamp_writes: Some(RenderPassTimestampWrites {
|
||||||
|
query_set: &queries,
|
||||||
|
beginning_of_pass_write_index: Some(0),
|
||||||
|
end_of_pass_write_index: Some(1),
|
||||||
|
}),
|
||||||
|
occlusion_query_set: None,
|
||||||
|
multiview_mask: None,
|
||||||
|
});
|
||||||
|
node.draw(pass);
|
||||||
|
}
|
||||||
|
encoder.resolve_query_set(&queries, 0..2, &resolved, 0);
|
||||||
|
encoder.copy_buffer_to_buffer(&resolved, 0, &readback, 0, 16);
|
||||||
|
queue.submit(Some(encoder.finish()));
|
||||||
|
|
||||||
|
let slice = readback.slice(..);
|
||||||
|
slice.map_async(MapMode::Read, |_| {});
|
||||||
|
let _ = device.poll(PollType::Wait {
|
||||||
|
submission_index: None,
|
||||||
|
timeout: None,
|
||||||
|
});
|
||||||
|
let ns = {
|
||||||
|
let view = slice.get_mapped_range().expect("timestamps did not map");
|
||||||
|
let stamps: [u64; 2] = [
|
||||||
|
u64::from_le_bytes(view[..8].try_into().unwrap()),
|
||||||
|
u64::from_le_bytes(view[8..16].try_into().unwrap()),
|
||||||
|
];
|
||||||
|
(stamps[1].saturating_sub(stamps[0])) as f64 * period as f64
|
||||||
|
};
|
||||||
|
readback.unmap();
|
||||||
|
ns
|
||||||
|
};
|
||||||
|
|
||||||
|
frame();
|
||||||
|
let mut best = f64::MAX;
|
||||||
|
for _ in 0..BATCHES {
|
||||||
|
let mut total = 0.0;
|
||||||
|
for _ in 0..FRAMES {
|
||||||
|
total += frame();
|
||||||
|
}
|
||||||
|
best = best.min(total / FRAMES as f64);
|
||||||
|
}
|
||||||
|
best
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "measurement, not a check"]
|
||||||
|
fn chain_cost_by_depth() {
|
||||||
|
let Some((device, queue, period)) = gpu() else {
|
||||||
|
println!("no gpu with timestamps; nothing measured");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
println!("{INSTANCES} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
|
||||||
|
let mut base = None;
|
||||||
|
for depth in [1, 2, 4, 8, 16, 32, 64] {
|
||||||
|
let ns = pass_cost(&device, &queue, period, depth);
|
||||||
|
let base = *base.get_or_insert(ns);
|
||||||
|
println!(
|
||||||
|
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
|
||||||
|
ns / 1000.0,
|
||||||
|
(ns - base) / base * 100.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
//! A measurement that decides control flow.
|
||||||
|
//!
|
||||||
|
//! Comparing boxes catches a widget that moved. It does not catch a widget
|
||||||
|
//! that measured a child, believed a different answer from the one a cold
|
||||||
|
//! start would give, and took the other branch -- which is the same defect
|
||||||
|
//! arriving somewhere it cannot be ignored. A widget here branches on what it
|
||||||
|
//! measured, so a disagreement shows up as a different tree.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
/// Measures `probe` across `axis` and draws one of two children on the
|
||||||
|
/// answer. Its own configuration never changes, so which child is drawn is a
|
||||||
|
/// property of the layout alone.
|
||||||
|
struct BranchesOnMeasurement {
|
||||||
|
probe: StrongWidget,
|
||||||
|
wide: StrongWidget,
|
||||||
|
narrow: StrongWidget,
|
||||||
|
threshold: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for BranchesOnMeasurement {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
let mut top = UiRegion::FULL;
|
||||||
|
top.y.end = top.y.start.offset(40.0);
|
||||||
|
let measured = painter.place(&self.probe, top).len(Axis::X);
|
||||||
|
let px = measured.apply_rest().to_px(painter.px_len(Axis::X));
|
||||||
|
|
||||||
|
let mut rest = UiRegion::FULL;
|
||||||
|
rest.y.start = rest.y.start.offset(40.0);
|
||||||
|
match px > self.threshold {
|
||||||
|
true => painter.place(&self.wide, rest),
|
||||||
|
false => painter.place(&self.narrow, rest),
|
||||||
|
};
|
||||||
|
Size::REST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plant(h: &mut Harness, threshold: f32) -> (WidgetId, WidgetId) {
|
||||||
|
let words = "the quick brown fox jumps over the lazy dog and keeps running";
|
||||||
|
let probe = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let wide = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let narrow = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let branch = BranchesOnMeasurement {
|
||||||
|
probe: probe.add_strong(&mut h.rsc),
|
||||||
|
wide: wide.add_strong(&mut h.rsc),
|
||||||
|
narrow: narrow.add_strong(&mut h.rsc),
|
||||||
|
threshold,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let side = rect(Color::GREEN).width(120).add(&mut h.rsc);
|
||||||
|
h.set_root((side, branch).span(Dir::RIGHT));
|
||||||
|
(wide.id(), narrow.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which of the two branches drew, as a pair a test can compare.
|
||||||
|
fn taken(h: &Harness, wide: WidgetId, narrow: WidgetId) -> (bool, bool) {
|
||||||
|
(h.region(&wide).is_some(), h.region(&narrow).is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_branch_taken_on_a_measurement_holds_across_repaints() {
|
||||||
|
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
|
||||||
|
let mut h = Harness::new((900, 600));
|
||||||
|
let (wide, narrow) = plant(&mut h, threshold);
|
||||||
|
let first = taken(&h, wide, narrow);
|
||||||
|
assert_ne!(first, (false, false), "threshold {threshold}: neither drew");
|
||||||
|
|
||||||
|
for frame in 0..4 {
|
||||||
|
h.rsc.widgets_mut().get_dyn_mut(wide);
|
||||||
|
h.rsc.widgets_mut().get_dyn_mut(narrow);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(
|
||||||
|
taken(&h, wide, narrow),
|
||||||
|
first,
|
||||||
|
"threshold {threshold}, repaint {frame}: the branch moved when nothing did"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_branch_taken_on_a_measurement_is_the_one_a_cold_start_takes() {
|
||||||
|
for threshold in [0.0, 200.0, 400.0, 600.0, 779.0, 780.0, 781.0, 2000.0] {
|
||||||
|
let mut warm = Harness::new((900, 600));
|
||||||
|
let (wide, narrow) = plant(&mut warm, threshold);
|
||||||
|
warm.resize((640, 480));
|
||||||
|
warm.frame();
|
||||||
|
warm.rsc.widgets_mut().get_dyn_mut(wide);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 480));
|
||||||
|
let (cwide, cnarrow) = plant(&mut cold, threshold);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
taken(&warm, wide, narrow),
|
||||||
|
taken(&cold, cwide, cnarrow),
|
||||||
|
"threshold {threshold}: warm and cold took different branches"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-2
@@ -22,8 +22,8 @@ use std::time::Instant;
|
|||||||
|
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use iris_core::{
|
use iris_core::{
|
||||||
GlyphPrimitive, MaskIdx, PrimitiveInst, RectPrimitive, TextureHandle, TexturePrimitive, UiData,
|
GlyphPrimitive, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, TextureHandle,
|
||||||
UiRegion, UiRenderNode, UiRenderState,
|
TexturePrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
|
||||||
};
|
};
|
||||||
use wgpu::{Color as GpuColor, *};
|
use wgpu::{Color as GpuColor, *};
|
||||||
|
|
||||||
@@ -95,6 +95,7 @@ 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(
|
||||||
@@ -111,6 +112,7 @@ fn fill(
|
|||||||
},
|
},
|
||||||
region: UiRegion::FULL,
|
region: UiRegion::FULL,
|
||||||
mask_idx: MaskIdx::NONE,
|
mask_idx: MaskIdx::NONE,
|
||||||
|
move_idx: MoveIdx::NONE,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -123,6 +125,7 @@ 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,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,468 @@
|
|||||||
|
//! Random trees, checked against building the same tree cold.
|
||||||
|
//!
|
||||||
|
//! A frame reaches its layout by keeping most of the last one: slots
|
||||||
|
//! rewritten, some widgets drawn again, the rest untouched. The property here
|
||||||
|
//! is that what comes out is the tree a cold start would have produced, so
|
||||||
|
//! anything the retained path carried over that it should not have shows up
|
||||||
|
//! as a difference in somebody's box.
|
||||||
|
//!
|
||||||
|
//! `iris::random` grows the tree and `examples/random.rs` draws one. A seed is
|
||||||
|
//! the whole reproduction; `a_long_run_of_seeds_agrees` is the ignored sweep
|
||||||
|
//! for when it is worth spending the time.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow};
|
||||||
|
|
||||||
|
/// How deep the generator branches. The generator widens two to four ways per
|
||||||
|
/// level, so depth is exponential in width and a deep narrow tree is not
|
||||||
|
/// reachable by raising this -- it buys more overlap between dependency
|
||||||
|
/// paths, not more ancestry.
|
||||||
|
fn depth() -> usize {
|
||||||
|
env("IRIS_GENERATED_DEPTH", 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||||
|
std::env::var(name)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse().ok())
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98];
|
||||||
|
const REGION_EPSILON_PX: f32 = 0.05;
|
||||||
|
|
||||||
|
fn same_coordinate(got: f32, want: f32) -> bool {
|
||||||
|
(got - want).abs() <= REGION_EPSILON_PX
|
||||||
|
}
|
||||||
|
|
||||||
|
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
||||||
|
match (got, want) {
|
||||||
|
(Some(got), Some(want)) => {
|
||||||
|
same_coordinate(got.top_left.x, want.top_left.x)
|
||||||
|
&& same_coordinate(got.top_left.y, want.top_left.y)
|
||||||
|
&& same_coordinate(got.bot_right.x, want.bot_right.x)
|
||||||
|
&& same_coordinate(got.bot_right.y, want.bot_right.y)
|
||||||
|
}
|
||||||
|
(None, None) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree {
|
||||||
|
let (root, tree) = grow(&mut h.rsc, seed, depth(), edits);
|
||||||
|
h.state.root = Some(root);
|
||||||
|
h.frame();
|
||||||
|
tree
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
|
||||||
|
let lens = [
|
||||||
|
Some(Len::px(20.0 + rng.below(180) as f32)),
|
||||||
|
Some(Len::px(20.0 + rng.below(180) as f32)),
|
||||||
|
];
|
||||||
|
let sized = &mut h.rsc[tree.sized[idx]];
|
||||||
|
sized.x = lens[0];
|
||||||
|
sized.y = lens[1];
|
||||||
|
lens
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes a few of the declared sizes, and says which, so the cold tree can
|
||||||
|
/// be grown with the same ones.
|
||||||
|
fn edit(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
|
||||||
|
let mut edits = HashMap::new();
|
||||||
|
for _ in 0..4 {
|
||||||
|
let idx = rng.below(tree.sized.len());
|
||||||
|
edits.insert(idx, resize_one(h, tree, idx, rng));
|
||||||
|
}
|
||||||
|
edits
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every declared size at once, so every reader of a size in the tree has a
|
||||||
|
/// changed descendant in the same frame and the whole dirty set has to settle
|
||||||
|
/// together.
|
||||||
|
fn edit_every(h: &mut Harness, tree: &Tree, rng: &mut Rng) -> HashMap<usize, Lens> {
|
||||||
|
(0..tree.sized.len())
|
||||||
|
.map(|idx| (idx, resize_one(h, tree, idx, rng)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A way of changing what a span holds. Each is a shape worth its own case:
|
||||||
|
/// taking a child out of the middle is not the same as emptying a span, and
|
||||||
|
/// adding one is not the same as adding three.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum Shuffle {
|
||||||
|
/// Every other child, so what is left is interleaved with what went.
|
||||||
|
EveryOther,
|
||||||
|
/// Everything but the first, which is the last step before empty.
|
||||||
|
AllButFirst,
|
||||||
|
/// Three more on the end at once.
|
||||||
|
AddThree,
|
||||||
|
/// The first out and three more on, so the count moves both ways.
|
||||||
|
SwapForThree,
|
||||||
|
/// One out of the middle and one on the end.
|
||||||
|
TradeOne,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHUFFLES: [Shuffle; 5] = [
|
||||||
|
Shuffle::EveryOther,
|
||||||
|
Shuffle::AllButFirst,
|
||||||
|
Shuffle::AddThree,
|
||||||
|
Shuffle::SwapForThree,
|
||||||
|
Shuffle::TradeOne,
|
||||||
|
];
|
||||||
|
|
||||||
|
impl Shuffle {
|
||||||
|
fn of(self, grown: usize) -> SpanEdit {
|
||||||
|
let all = |step: usize, from: usize| (from..grown).step_by(step).collect();
|
||||||
|
match self {
|
||||||
|
Self::EveryOther => SpanEdit {
|
||||||
|
detach: all(2, 0),
|
||||||
|
attach: 0,
|
||||||
|
},
|
||||||
|
Self::AllButFirst => SpanEdit {
|
||||||
|
detach: all(1, 1),
|
||||||
|
attach: 0,
|
||||||
|
},
|
||||||
|
Self::AddThree => SpanEdit {
|
||||||
|
detach: Vec::new(),
|
||||||
|
attach: 3,
|
||||||
|
},
|
||||||
|
Self::SwapForThree => SpanEdit {
|
||||||
|
detach: vec![0],
|
||||||
|
attach: 3,
|
||||||
|
},
|
||||||
|
Self::TradeOne => SpanEdit {
|
||||||
|
detach: vec![grown / 2],
|
||||||
|
attach: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies `shuffle` to every third span, and says what it did so the cold
|
||||||
|
/// tree can be grown that way. The widgets it takes out are given back: the
|
||||||
|
/// last share of one must outlive the comparison, or its id is handed to
|
||||||
|
/// something else and the two trees stop lining up.
|
||||||
|
fn reshuffle(
|
||||||
|
h: &mut Harness,
|
||||||
|
tree: &mut Tree,
|
||||||
|
shuffle: Shuffle,
|
||||||
|
) -> (HashMap<usize, SpanEdit>, Vec<StrongWidget>) {
|
||||||
|
let mut edits = HashMap::new();
|
||||||
|
let mut detached = Vec::new();
|
||||||
|
for (idx, span) in tree.spans.iter_mut().enumerate().step_by(3) {
|
||||||
|
let span_edit = shuffle.of(span.grown);
|
||||||
|
let mut take = span_edit.detach.clone();
|
||||||
|
take.sort_unstable();
|
||||||
|
let children = &mut h.rsc[span.id].children;
|
||||||
|
// Highest first, so an index means the same child however many of
|
||||||
|
// its neighbours are going too.
|
||||||
|
for j in take.into_iter().rev() {
|
||||||
|
if j < children.len() {
|
||||||
|
detached.push(children.remove(j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let attach = span_edit.attach.min(span.spares.len());
|
||||||
|
children.extend(span.spares.drain(..attach));
|
||||||
|
edits.insert(idx, span_edit);
|
||||||
|
}
|
||||||
|
(edits, detached)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a widget was configured with, so a tree the generator found can be
|
||||||
|
/// written out by hand. A fuzz failure is a lead; the fast test that replaces
|
||||||
|
/// it has to be buildable from what the failure printed.
|
||||||
|
fn describe(id: WidgetId, h: &Harness) -> String {
|
||||||
|
let label = h.rsc.widgets().label(id).to_string();
|
||||||
|
let Some(widget) = h.rsc.widgets().get_dyn(id) else {
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
let any: &dyn std::any::Any = widget;
|
||||||
|
let len = |l: &Option<Len>| match l {
|
||||||
|
Some(l) => format!("{l}"),
|
||||||
|
None => "-".into(),
|
||||||
|
};
|
||||||
|
if let Some(w) = any.downcast_ref::<SetSize>() {
|
||||||
|
return format!("SetSize{{x:{},y:{}}}", len(&w.x), len(&w.y));
|
||||||
|
}
|
||||||
|
if let Some(w) = any.downcast_ref::<Span>() {
|
||||||
|
let sign = if w.dir.sign == Sign::Neg { "-" } else { "+" };
|
||||||
|
return format!(
|
||||||
|
"Span{{dir:{:?}{sign},gap:{},n:{}}}",
|
||||||
|
w.dir.axis,
|
||||||
|
w.gap,
|
||||||
|
w.children.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(w) = any.downcast_ref::<Pad>() {
|
||||||
|
let p = &w.padding;
|
||||||
|
return format!(
|
||||||
|
"Pad{{l:{},r:{},t:{},b:{}}}",
|
||||||
|
p.left, p.right, p.top, p.bottom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(w) = any.downcast_ref::<Aligned>() {
|
||||||
|
let a = |v: Option<AxisAlign>| match v {
|
||||||
|
None => "-",
|
||||||
|
Some(AxisAlign::Neg) => "neg",
|
||||||
|
Some(AxisAlign::Center) => "mid",
|
||||||
|
Some(AxisAlign::Pos) => "pos",
|
||||||
|
};
|
||||||
|
return format!("Aligned{{x:{},y:{}}}", a(w.align.x), a(w.align.y));
|
||||||
|
}
|
||||||
|
if let Some(w) = any.downcast_ref::<Stack>() {
|
||||||
|
return format!("Stack{{n:{}}}", w.children.len());
|
||||||
|
}
|
||||||
|
label
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every widget in one tree against the matching widget in the other. A
|
||||||
|
/// mismatch prints the widget's ancestry, marking the ones that own a slot,
|
||||||
|
/// since where two trees disagree is rarely where the cause is.
|
||||||
|
fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, &Tree)) {
|
||||||
|
let ((wh, wt), (ch, ct)) = (warm, cold);
|
||||||
|
assert_eq!(wt.ids.len(), ct.ids.len(), "seed {seed}: different trees");
|
||||||
|
let mut drawn = 0;
|
||||||
|
let mut wrong = 0;
|
||||||
|
for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() {
|
||||||
|
let (got, want) = (wh.region(&w), ch.region(&c));
|
||||||
|
drawn += usize::from(got.is_some());
|
||||||
|
// This oracle cares where rasterization lands, not whether equivalent
|
||||||
|
// arithmetic produced the same f32. Keep the tolerance to one
|
||||||
|
// twentieth of a physical pixel, while whether a widget drew remains
|
||||||
|
// exact.
|
||||||
|
if same_region(got, want) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
wrong += 1;
|
||||||
|
if wrong <= 3 {
|
||||||
|
let mut chain = Vec::new();
|
||||||
|
let mut at = Some(w);
|
||||||
|
while let Some(id) = at {
|
||||||
|
let active = &wh.render.active[&id];
|
||||||
|
let slot = match active.move_idx == active.parent_move {
|
||||||
|
true => "",
|
||||||
|
false => "*",
|
||||||
|
};
|
||||||
|
chain.push(format!("{}{slot}", describe(id, wh)));
|
||||||
|
at = active.parent;
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"seed {seed} after {what}: widget {i}\n warm {got:?}\n cold {want:?}\n {}",
|
||||||
|
chain.join(" < ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(drawn > 0, "seed {seed}: nothing was drawn");
|
||||||
|
assert_eq!(wrong, 0, "seed {seed}: {wrong} widgets differ after {what}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changed_size(seed: u64) {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let grown = plant(&mut warm, seed, &Edits::default());
|
||||||
|
// Not every tree grows a declared size to change.
|
||||||
|
if grown.sized.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rng = Rng::new(seed ^ 0x5eed);
|
||||||
|
let sizes = edit(&mut warm, &grown, &mut rng);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let same = plant(
|
||||||
|
&mut cold,
|
||||||
|
seed,
|
||||||
|
&Edits {
|
||||||
|
sizes,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reshuffled(seed: u64, shuffle: Shuffle) {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let mut grown = plant(&mut warm, seed, &Edits::default());
|
||||||
|
// Some seeds grow nothing but wrappers, and a shuffle with no span to
|
||||||
|
// shuffle is not the same thing as one that had no effect. A span behind
|
||||||
|
// a branch nobody took is the same kind of nothing: it is not drawn, so
|
||||||
|
// shuffling it cannot move anything.
|
||||||
|
let shuffles = grown
|
||||||
|
.spans
|
||||||
|
.iter()
|
||||||
|
.step_by(3)
|
||||||
|
.any(|span| warm.region(&span.id.id()).is_some());
|
||||||
|
if !shuffles {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let before: Vec<_> = grown.ids.iter().map(|id| warm.region(id)).collect();
|
||||||
|
|
||||||
|
let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
// Or the two trees would agree for want of anything having happened.
|
||||||
|
let after = grown.ids.iter().map(|id| warm.region(id));
|
||||||
|
let moved = before.iter().zip(after).filter(|(a, b)| *a != b).count();
|
||||||
|
assert!(moved > 0, "seed {seed}: {shuffle:?} changed nothing");
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let same = plant(
|
||||||
|
&mut cold,
|
||||||
|
seed,
|
||||||
|
&Edits {
|
||||||
|
spans,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let what = format!("{shuffle:?}");
|
||||||
|
assert_same(seed, &what, (&warm, &grown), (&cold, &same));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changed_every_size(seed: u64) {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let grown = plant(&mut warm, seed, &Edits::default());
|
||||||
|
if grown.sized.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rng = Rng::new(seed ^ 0xa11);
|
||||||
|
let sizes = edit_every(&mut warm, &grown, &mut rng);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let same = plant(
|
||||||
|
&mut cold,
|
||||||
|
seed,
|
||||||
|
&Edits {
|
||||||
|
sizes,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_same(seed, "every size at once", (&warm, &grown), (&cold, &same));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks a spread of widgets for redraw at once. Nothing changes, so no box
|
||||||
|
/// may either; what this exercises is the order a frame settles a dirty set
|
||||||
|
/// in, which the other cases reach one dependency path at a time.
|
||||||
|
fn repainted_together(seed: u64) {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let grown = plant(&mut warm, seed, &Edits::default());
|
||||||
|
for &id in grown.ids.iter().step_by(5) {
|
||||||
|
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
!warm.rsc.widgets().needs_redraw.is_empty(),
|
||||||
|
"seed {seed}: nothing was marked"
|
||||||
|
);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let same = plant(&mut cold, seed, &Edits::default());
|
||||||
|
|
||||||
|
let what = "many repaints at once";
|
||||||
|
assert_same(seed, what, (&warm, &grown), (&cold, &same));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resized(seed: u64) {
|
||||||
|
let mut warm = Harness::new((1920, 1200));
|
||||||
|
let grown = plant(&mut warm, seed, &Edits::default());
|
||||||
|
warm.resize((640, 900));
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let same = plant(&mut cold, seed, &Edits::default());
|
||||||
|
|
||||||
|
assert_same(seed, "a resize", (&warm, &grown), (&cold, &same));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resized_then_changed(seed: u64) {
|
||||||
|
let mut warm = Harness::new((1920, 1200));
|
||||||
|
let grown = plant(&mut warm, seed, &Edits::default());
|
||||||
|
if grown.sized.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
warm.resize((640, 900));
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut rng = Rng::new(seed ^ 0xb0a7);
|
||||||
|
let sizes = edit(&mut warm, &grown, &mut rng);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let same = plant(
|
||||||
|
&mut cold,
|
||||||
|
seed,
|
||||||
|
&Edits {
|
||||||
|
sizes,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let what = "a resize then a size change";
|
||||||
|
assert_same(seed, what, (&warm, &grown), (&cold, &same));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_changed_size_lands_where_growing_it_that_way_would() {
|
||||||
|
SEEDS.into_iter().for_each(changed_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_size_changing_at_once_lands_where_growing_it_that_way_would() {
|
||||||
|
SEEDS.into_iter().for_each(changed_every_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn many_widgets_redrawing_at_once_leaves_every_box_where_it_was() {
|
||||||
|
SEEDS.into_iter().for_each(repainted_together);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_resize_lands_where_starting_at_that_size_would() {
|
||||||
|
SEEDS.into_iter().for_each(resized);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_size_change_after_a_resize_lands_the_same_way() {
|
||||||
|
SEEDS.into_iter().for_each(resized_then_changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adding_and_removing_span_children_lands_where_growing_it_that_way_would() {
|
||||||
|
for shuffle in SHUFFLES {
|
||||||
|
for seed in SEEDS {
|
||||||
|
reshuffled(seed, shuffle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same property over a hundred seeds and every scenario. What it has
|
||||||
|
/// found so far was never where the trees disagreed: a text measured in a box
|
||||||
|
/// it was not going to get, and a widget re-measured in a box its own answer
|
||||||
|
/// had decided. `tests/shrink.rs` is how a seed from here becomes a tree
|
||||||
|
/// small enough to read.
|
||||||
|
#[test]
|
||||||
|
#[ignore = "a hundred seeds, rather than the seven the others check"]
|
||||||
|
fn a_long_run_of_seeds_agrees() {
|
||||||
|
let seeds = std::env::var("IRIS_GENERATED_SEED")
|
||||||
|
.ok()
|
||||||
|
.and_then(|seed| seed.parse().ok())
|
||||||
|
.map(|seed| seed..=seed)
|
||||||
|
.unwrap_or_else(|| 1..=env("IRIS_GENERATED_SEEDS", 100));
|
||||||
|
for seed in seeds {
|
||||||
|
changed_size(seed);
|
||||||
|
changed_every_size(seed);
|
||||||
|
repainted_together(seed);
|
||||||
|
resized(seed);
|
||||||
|
resized_then_changed(seed);
|
||||||
|
for shuffle in SHUFFLES {
|
||||||
|
reshuffled(seed, shuffle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
//! Whether measuring a widget and then giving it the length it reported is a
|
||||||
|
//! fixed point, which is what a span that sizes to its children needs.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_wrapping_text_in_a_span_settles_on_one_width() {
|
||||||
|
let mut h = Harness::new((900, 600));
|
||||||
|
let words = "the quick brown fox jumps over the lazy dog and keeps on running \
|
||||||
|
until it reaches the end of a rather long line of text";
|
||||||
|
let t = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((t, filler).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
let mut widths = Vec::new();
|
||||||
|
for _ in 0..6 {
|
||||||
|
let r = h.region(&t.id()).unwrap();
|
||||||
|
widths.push(r.bot_right.x - r.top_left.x);
|
||||||
|
// Redrawing it changes nothing about the state, so nothing may move.
|
||||||
|
h.rsc.widgets_mut().get_dyn_mut(t.id());
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
println!("widths over six frames: {widths:?}");
|
||||||
|
assert!(
|
||||||
|
widths.windows(2).all(|w| w[0] == w[1]),
|
||||||
|
"a repaint that changed nothing moved it: {widths:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
+351
@@ -0,0 +1,351 @@
|
|||||||
|
//! Where a frame puts things, with no window to put them in.
|
||||||
|
|
||||||
|
use iris::harness::{Harness, assert_corners};
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
/// A fixed 100 wide, and the rest of the 400 to its neighbour.
|
||||||
|
fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) {
|
||||||
|
let left = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
let right = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
h.set_root((left, right).span(Dir::RIGHT));
|
||||||
|
(left.id(), right.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_span_gives_each_child_the_width_it_asked_for() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (left, right) = two_rects(&mut h);
|
||||||
|
|
||||||
|
assert_corners!(h, left, (0, 0), (100, 200));
|
||||||
|
assert_corners!(h, right, (100, 0), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resizing_relays_out_against_the_new_output() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (left, right) = two_rects(&mut h);
|
||||||
|
|
||||||
|
h.resize((800, 100));
|
||||||
|
assert!(h.needs_redraw());
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_corners!(h, left, (0, 0), (100, 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::px(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(¶), 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::px(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::px(80));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
// The row is the same shape somewhere else, so one slot moved it and
|
||||||
|
// `inner`'s own region was never rewritten.
|
||||||
|
assert_corners!(h, inner, (10, 90), (390, 110));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc);
|
||||||
|
let rest = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
let panel = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
// Changing the bar's width is the only thing that changes the box the
|
||||||
|
// panel and everything under it was drawn for.
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, panel).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, fixed, (100, 0), (150, 200));
|
||||||
|
assert_corners!(h, rest, (150, 0), (400, 200));
|
||||||
|
|
||||||
|
h.rsc[bar].x = Some(Len::px(200));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
// The panel's box is 100 shorter, so the fixed child is the same 50 wide
|
||||||
|
// against its new start and the one taking the rest absorbs the change.
|
||||||
|
assert_corners!(h, fixed, (200, 0), (250, 200));
|
||||||
|
assert_corners!(h, rest, (250, 0), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// The row is 40 tall whatever happens, which used to make its drawing
|
||||||
|
// impossible to take out of: recovering a fraction of a box needs a
|
||||||
|
// relative extent, and it has none on that axis.
|
||||||
|
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
let row = inner.pad(10).height(40).add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::GREEN).add(&mut h.rsc);
|
||||||
|
let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc);
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, column).span(Dir::RIGHT));
|
||||||
|
assert_corners!(h, inner, (110, 10), (390, 30));
|
||||||
|
|
||||||
|
h.rsc[bar].x = Some(Len::px(200));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_corners!(h, inner, (210, 10), (390, 30));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_a_container_that_places_its_children_lengthens_the_chain() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let leaf = rect(Color::BLUE).add(&mut h.rsc);
|
||||||
|
// Four widgets between the span and the leaf, none of which places what
|
||||||
|
// it draws, so all of them share the span's slot.
|
||||||
|
let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc);
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, buried).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
let slot = h.render.active[&leaf.id()].parent_move;
|
||||||
|
assert_eq!(
|
||||||
|
h.render.moves.depth(slot),
|
||||||
|
2,
|
||||||
|
"the span above the leaf, and the root the window is held in"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A span that sizes from its children passes their `rest` weight up rather
|
||||||
|
/// than collapsing it to one share, so nesting divides the same space instead
|
||||||
|
/// of re-dividing a share of it.
|
||||||
|
#[test]
|
||||||
|
fn nested_spans_divide_the_space_once_however_deep_the_nesting_is() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (a, b, c, d) = (
|
||||||
|
rect(Color::RED).add(&mut h.rsc),
|
||||||
|
rect(Color::BLUE).add(&mut h.rsc),
|
||||||
|
rect(Color::GREEN).add(&mut h.rsc),
|
||||||
|
rect(Color::WHITE).add(&mut h.rsc),
|
||||||
|
);
|
||||||
|
let left = (a, b).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let right = (c, d).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root((left, right).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
for (i, id) in [a, b, c, d].into_iter().enumerate() {
|
||||||
|
let x = i as f32 * 100.0;
|
||||||
|
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same space, unevenly nested: weights carried up mean a share is a
|
||||||
|
/// share of the whole, not of whatever branch a widget happens to sit in.
|
||||||
|
#[test]
|
||||||
|
fn an_uneven_nesting_still_gives_every_share_the_same_length() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (a, b, c, d) = (
|
||||||
|
rect(Color::RED).add(&mut h.rsc),
|
||||||
|
rect(Color::BLUE).add(&mut h.rsc),
|
||||||
|
rect(Color::GREEN).add(&mut h.rsc),
|
||||||
|
rect(Color::WHITE).add(&mut h.rsc),
|
||||||
|
);
|
||||||
|
let one = (a,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let three = (b, c, d).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root((one, three).span(Dir::RIGHT));
|
||||||
|
|
||||||
|
for (i, id) in [a, b, c, d].into_iter().enumerate() {
|
||||||
|
let x = i as f32 * 100.0;
|
||||||
|
assert_corners!(h, id, (x, 0), (x + 100.0, 200));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the shader puts an edge: the two parts of a scalar are floored
|
||||||
|
/// apart, so a fraction and a pixel offset snap independently.
|
||||||
|
fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) {
|
||||||
|
let active = &h.render.active[&id];
|
||||||
|
let region = h.render.moves.resolve(active.parent_move, active.region);
|
||||||
|
let dim = h.size().axis(axis);
|
||||||
|
let edge = |s: UiScalar| (s.rel * dim).floor() + s.px.floor();
|
||||||
|
let span = region.axis(axis);
|
||||||
|
(edge(span.start), edge(span.end))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hairline(h: &mut Harness, marks: &mut Vec<WidgetId>) -> StrongWidget {
|
||||||
|
let inner = rect(Color::RED).add_strong(&mut h.rsc);
|
||||||
|
let mark = SetSize {
|
||||||
|
inner,
|
||||||
|
x: Some(Len::px(1.0)),
|
||||||
|
y: None,
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
marks.push(mark.id());
|
||||||
|
mark
|
||||||
|
}
|
||||||
|
|
||||||
|
fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget {
|
||||||
|
SetSize {
|
||||||
|
inner,
|
||||||
|
x: Some(Len::rest(ratio)),
|
||||||
|
y: None,
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shares in weights no binary fraction lands on, a padding on one branch
|
||||||
|
/// and not the other, so an edge falls near an integer as often as it can.
|
||||||
|
fn hairlines(h: &mut Harness, depth: usize, marks: &mut Vec<WidgetId>) -> StrongWidget {
|
||||||
|
let mut span = Span::empty(Dir::RIGHT);
|
||||||
|
if depth == 0 {
|
||||||
|
let left = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let left = share(h, left, 3.0);
|
||||||
|
span.push(left);
|
||||||
|
let mark = hairline(h, marks);
|
||||||
|
span.push(mark);
|
||||||
|
let right = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let right = share(h, right, 7.0);
|
||||||
|
span.push(right);
|
||||||
|
return span.add_strong(&mut h.rsc);
|
||||||
|
}
|
||||||
|
let first = hairlines(h, depth - 1, marks);
|
||||||
|
let first = share(h, first, 3.0);
|
||||||
|
span.push(first);
|
||||||
|
let second = hairlines(h, depth - 1, marks);
|
||||||
|
let second = Pad {
|
||||||
|
padding: Padding {
|
||||||
|
left: 3.0,
|
||||||
|
right: 7.0,
|
||||||
|
top: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
},
|
||||||
|
inner: second,
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
let second = share(h, second, 5.0);
|
||||||
|
span.push(second);
|
||||||
|
span.add_strong(&mut h.rsc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A one-pixel line is a pixel wherever it is drawn. Both edges of a fixed
|
||||||
|
/// length share their box's fraction, so composing the chain moves them
|
||||||
|
/// together and the shader's `floor` cannot round the pixel between them
|
||||||
|
/// away -- only shift it. A separator that disappeared at one window size
|
||||||
|
/// would be a defect no size comparison catches.
|
||||||
|
#[test]
|
||||||
|
fn a_one_pixel_line_keeps_its_pixel_through_a_chain() {
|
||||||
|
let mut h = Harness::new((1920, 1200));
|
||||||
|
let mut marks = Vec::new();
|
||||||
|
let root = hairlines(&mut h, 4, &mut marks);
|
||||||
|
h.state.set_root(root);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(marks.len(), 16);
|
||||||
|
|
||||||
|
for size in [(1920, 1200), (1919, 1201), (997, 1003), (1367, 733)] {
|
||||||
|
h.resize(size);
|
||||||
|
h.frame();
|
||||||
|
for mark in &marks {
|
||||||
|
let (start, end) = drawn_edges(&h, *mark, Axis::X);
|
||||||
|
assert_eq!(end - start, 1.0, "at {size:?}, mark {mark:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A span short of room takes it from its shares, which go to nothing and
|
||||||
|
/// then to nothing wider; the fixed lengths between them keep their pixels.
|
||||||
|
/// Collapsing those to make room would delete a separator the caller asked
|
||||||
|
/// for, which is worse than overflowing.
|
||||||
|
#[test]
|
||||||
|
fn a_span_out_of_room_shrinks_its_shares_and_not_its_fixed_lengths() {
|
||||||
|
let mut h = Harness::new((400, 20));
|
||||||
|
let mut marks = Vec::new();
|
||||||
|
let mut span = Span::empty(Dir::RIGHT);
|
||||||
|
for _ in 0..3 {
|
||||||
|
let share_of = rect(Color::BLUE).add_strong(&mut h.rsc);
|
||||||
|
let share_of = share(&mut h, share_of, 1.0);
|
||||||
|
span.push(share_of);
|
||||||
|
let mark = hairline(&mut h, &mut marks);
|
||||||
|
span.push(mark);
|
||||||
|
}
|
||||||
|
let root = span.add_strong(&mut h.rsc);
|
||||||
|
h.state.set_root(root);
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
for width in [400, 10, 3, 1] {
|
||||||
|
h.resize((width, 20));
|
||||||
|
h.frame();
|
||||||
|
for mark in &marks {
|
||||||
|
let (start, end) = drawn_edges(&h, *mark, Axis::X);
|
||||||
|
assert_eq!(end - start, 1.0, "at {width} wide, mark {mark:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
//! Retained CPU-layout diagnostics on one reproducible random tree.
|
||||||
|
//!
|
||||||
|
//! Counters and phase timers:
|
||||||
|
//!
|
||||||
|
//! cargo test --release --features layout-diagnostics \
|
||||||
|
//! --test layout_diagnostics -- --ignored --nocapture
|
||||||
|
//!
|
||||||
|
//! Uninstrumented hardware totals for one phase:
|
||||||
|
//!
|
||||||
|
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \
|
||||||
|
//! -e cycles:u,instructions:u cargo test --release \
|
||||||
|
//! --test layout_diagnostics -- --ignored --nocapture
|
||||||
|
//!
|
||||||
|
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
|
||||||
|
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
|
||||||
|
//! `IRIS_DIRTY` how many widgets `many` marks at once.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::{Edits, Tree, grow};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
|
||||||
|
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
#[test]
|
||||||
|
fn a_selected_widget_retains_its_layout_events() {
|
||||||
|
use iris::core::layout_diagnostics::{self as diagnostics, TraceEvent};
|
||||||
|
|
||||||
|
diagnostics::clear_traced_widgets();
|
||||||
|
let _ = diagnostics::take();
|
||||||
|
let mut harness = Harness::new((400, 200));
|
||||||
|
let leaf = rect(Color::RED).add(&mut harness.rsc);
|
||||||
|
let other = rect(Color::BLUE).add(&mut harness.rsc);
|
||||||
|
let root = (leaf, other).span(Dir::RIGHT).add(&mut harness.rsc);
|
||||||
|
harness.set_root(root);
|
||||||
|
diagnostics::trace_widget(leaf.id());
|
||||||
|
let _ = diagnostics::take();
|
||||||
|
|
||||||
|
let _ = harness.rsc.widgets_mut().get_dyn_mut(root.id());
|
||||||
|
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf.id());
|
||||||
|
harness.frame();
|
||||||
|
|
||||||
|
let report = diagnostics::take();
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.traces()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, TraceEvent::Placed { id, .. } if *id == leaf.id()))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.traces()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, TraceEvent::DrawRequest { id, .. } if *id == leaf.id()))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.traces()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, TraceEvent::SizeRead { id, .. } if *id == leaf.id()))
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.traces()
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event, TraceEvent::SizeReported { id, .. } if *id == leaf.id()))
|
||||||
|
);
|
||||||
|
diagnostics::clear_traced_widgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||||
|
std::env::var(name)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse().ok())
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
fn trace_selected(tree: &Tree) {
|
||||||
|
let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let index = value
|
||||||
|
.parse::<usize>()
|
||||||
|
.expect("IRIS_TRACE_INDEX must be a tree.ids index");
|
||||||
|
let id = tree.ids[index];
|
||||||
|
iris::core::layout_diagnostics::trace_widget(id);
|
||||||
|
println!("tracing tree.ids[{index}] = {id:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "layout-diagnostics"))]
|
||||||
|
fn trace_selected(_: &Tree) {}
|
||||||
|
|
||||||
|
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
||||||
|
let mut harness = Harness::new(OUTPUT);
|
||||||
|
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
||||||
|
harness.state.root = Some(root);
|
||||||
|
harness.frame();
|
||||||
|
println!(
|
||||||
|
"fixture: seed {seed}, depth {depth}, {} widgets, {} active",
|
||||||
|
tree.ids.len(),
|
||||||
|
harness.render.active_widgets()
|
||||||
|
);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _ = iris::core::layout_diagnostics::take();
|
||||||
|
(harness, tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
|
||||||
|
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
let frames = elapsed.len();
|
||||||
|
// The worst frame is the stutter somebody sees, so it goes beside the
|
||||||
|
// median; p99 says whether it is the load or a single interruption.
|
||||||
|
println!(
|
||||||
|
"{label}: {frames} frame(s), min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
|
||||||
|
max {:.3} ms, total {:.1} ms",
|
||||||
|
elapsed[0],
|
||||||
|
elapsed[frames / 2],
|
||||||
|
elapsed[frames * 99 / 100],
|
||||||
|
elapsed[frames - 1],
|
||||||
|
elapsed.iter().sum::<f64>(),
|
||||||
|
);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
{
|
||||||
|
let diagnostics = iris::core::layout_diagnostics::take();
|
||||||
|
print!("{}", diagnostics.per_frame(frames));
|
||||||
|
for callsite in diagnostics.hot_text().iter().take(3) {
|
||||||
|
let mut ancestry = Vec::new();
|
||||||
|
let mut id = Some(callsite.id);
|
||||||
|
while let Some(widget) = id {
|
||||||
|
ancestry.push(_harness.rsc.widgets().label(widget).as_str());
|
||||||
|
id = _harness
|
||||||
|
.render
|
||||||
|
.active
|
||||||
|
.get(&widget)
|
||||||
|
.and_then(|active| active.parent);
|
||||||
|
}
|
||||||
|
println!(" text ancestry: {}", ancestry.join(" < "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
label: &str,
|
||||||
|
frames: usize,
|
||||||
|
harness: &mut Harness,
|
||||||
|
mut change: impl FnMut(&mut Harness, usize),
|
||||||
|
) {
|
||||||
|
let mut elapsed = Vec::with_capacity(frames);
|
||||||
|
for frame in 0..frames {
|
||||||
|
change(harness, frame);
|
||||||
|
let start = Instant::now();
|
||||||
|
harness.frame();
|
||||||
|
elapsed.push(start.elapsed().as_secs_f64() * 1_000.0);
|
||||||
|
}
|
||||||
|
report(label, elapsed, harness);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "measurement, not a check"]
|
||||||
|
fn layout_cost() {
|
||||||
|
let seed = env("IRIS_SEED", 1_u64);
|
||||||
|
let depth = env("IRIS_DEPTH", 7_usize);
|
||||||
|
let frames = env("IRIS_FRAMES", 100_usize);
|
||||||
|
assert!(frames > 0, "IRIS_FRAMES must be greater than zero");
|
||||||
|
let phase = env("IRIS_PHASE", String::from("all"));
|
||||||
|
assert!(
|
||||||
|
["all", "cold", "repaint", "many", "size", "scroll", "resize"].contains(&phase.as_str()),
|
||||||
|
"unknown IRIS_PHASE {phase:?}"
|
||||||
|
);
|
||||||
|
let selected = |name| phase == "all" || phase == name;
|
||||||
|
|
||||||
|
if selected("cold") {
|
||||||
|
let mut harness = Harness::new(OUTPUT);
|
||||||
|
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
||||||
|
harness.state.root = Some(root);
|
||||||
|
println!(
|
||||||
|
"fixture: seed {seed}, depth {depth}, {} widgets",
|
||||||
|
tree.ids.len()
|
||||||
|
);
|
||||||
|
trace_selected(&tree);
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
let _ = iris::core::layout_diagnostics::take();
|
||||||
|
run("cold", 1, &mut harness, |_, _| {});
|
||||||
|
drop(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected("repaint") {
|
||||||
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
|
let leaf = tree.ids[0];
|
||||||
|
run("repaint", frames, &mut harness, move |harness, _| {
|
||||||
|
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected("many") {
|
||||||
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
|
// Spread through the tree rather than taken from one subtree, so the
|
||||||
|
// dependency paths the frame settles overlap.
|
||||||
|
let wanted = env("IRIS_DIRTY", 32_usize).max(1);
|
||||||
|
let step = (tree.ids.len() / wanted).max(1);
|
||||||
|
let dirty: Vec<_> = tree.ids.iter().copied().step_by(step).collect();
|
||||||
|
println!("marking {} of {} widgets", dirty.len(), tree.ids.len());
|
||||||
|
run("many", frames, &mut harness, move |harness, _| {
|
||||||
|
for &id in &dirty {
|
||||||
|
harness.rsc.widgets_mut().get_dyn_mut(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected("size") {
|
||||||
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
|
let sized = tree.sized[0];
|
||||||
|
run("size", frames, &mut harness, move |harness, frame| {
|
||||||
|
harness.rsc[sized].x = Some(Len::px(100.0 + (frame % 2) as f32 * 40.0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected("scroll") {
|
||||||
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
|
let scroll = tree.scrolls[0];
|
||||||
|
run("scroll", frames, &mut harness, move |harness, frame| {
|
||||||
|
harness.rsc[scroll].scroll(if frame % 2 == 0 { 12.0 } else { -12.0 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected("resize") {
|
||||||
|
let (mut harness, tree) = warm(seed, depth);
|
||||||
|
trace_selected(&tree);
|
||||||
|
run("resize", frames, &mut harness, |harness, frame| {
|
||||||
|
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
|
||||||
|
});
|
||||||
|
drop(tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
//! Which widget an input reaches.
|
||||||
|
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use iris::harness::{Harness, TouchScript};
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_press_reaches_only_the_widget_under_the_cursor() {
|
||||||
|
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));
|
||||||
|
|
||||||
|
h.click((50, 100));
|
||||||
|
assert_eq!(*clicks.borrow(), ["left"]);
|
||||||
|
|
||||||
|
h.click((300, 100));
|
||||||
|
assert_eq!(*clicks.borrow(), ["left", "right"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_ends_when_the_cursor_leaves_the_window() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let hovered = Rc::new(RefCell::new(0));
|
||||||
|
let ended = Rc::new(RefCell::new(0));
|
||||||
|
|
||||||
|
let (h_count, e_count) = (hovered.clone(), ended.clone());
|
||||||
|
let widget = rect(Color::RED)
|
||||||
|
.on(CursorSense::HoverStart, move |_, _| {
|
||||||
|
*h_count.borrow_mut() += 1
|
||||||
|
})
|
||||||
|
.on(CursorSense::HoverEnd, move |_, _| {
|
||||||
|
*e_count.borrow_mut() += 1
|
||||||
|
})
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(widget);
|
||||||
|
|
||||||
|
h.move_to((200, 100));
|
||||||
|
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
|
||||||
|
|
||||||
|
// A second sample inside the same widget is not a second hover.
|
||||||
|
h.move_to((210, 100));
|
||||||
|
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 0));
|
||||||
|
|
||||||
|
h.leave();
|
||||||
|
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"]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
//! 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
//! What re-placing a subtree costs per frame, as a load for a counter rather
|
||||||
|
//! than a check. A span of 200 fixed-height rows, five primitives each, with
|
||||||
|
//! the row above them changing height every frame, so every row below is
|
||||||
|
//! offered a box the same shape somewhere else.
|
||||||
|
//!
|
||||||
|
//! cargo test --release --test replace_cost -- --ignored
|
||||||
|
//! perf stat -e instructions:u target/release/.../replace_cost-* --ignored
|
||||||
|
//!
|
||||||
|
//! Wall time is the wrong number here; see `draw_cost.rs`. Measured on
|
||||||
|
//! 2026-09-14 at 1.98M instructions per frame, against 2.38M for rewriting
|
||||||
|
//! each row's regions instead and 7.13M for redrawing them.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
const ROWS: usize = 200;
|
||||||
|
const FRAMES: usize = 200;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "measurement, not a check"]
|
||||||
|
fn replacing_rows_every_frame() {
|
||||||
|
let mut h = Harness::new((1920, 1200));
|
||||||
|
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
||||||
|
let mut span = Span::empty(Dir::DOWN);
|
||||||
|
span.push(first.add_strong(&mut h.rsc));
|
||||||
|
for i in 0..ROWS {
|
||||||
|
let row = (
|
||||||
|
rect(Color::BLUE.darker(i as f32 / (ROWS * 2) as f32)),
|
||||||
|
rect(Color::GREEN).pad(2),
|
||||||
|
wtext("row").size(16).pad(2),
|
||||||
|
)
|
||||||
|
.span(Dir::RIGHT)
|
||||||
|
.pad(4)
|
||||||
|
.height(40)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
span.push(row.add_strong(&mut h.rsc));
|
||||||
|
}
|
||||||
|
h.set_root(span);
|
||||||
|
for i in 0..FRAMES {
|
||||||
|
h.rsc[first].y = Some(Len::px(40.0 + (i % 2) as f32));
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,471 @@
|
|||||||
|
//! 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.px);
|
||||||
|
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::px(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::px(painter.output_size() / 4.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the output across one axis only, and says so: its drawing follows
|
||||||
|
/// a taller box on its own, so only a wider one is worth a draw.
|
||||||
|
struct ReadsWidth {
|
||||||
|
draws: Rc<Cell<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for ReadsWidth {
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size {
|
||||||
|
self.draws.set(self.draws.get() + 1);
|
||||||
|
Size::px((painter.output_len(Axis::X) / 4.0, 20.0).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_resize(&self, axis: Axis) -> OnResize {
|
||||||
|
match axis {
|
||||||
|
Axis::X => OnResize::Redraw,
|
||||||
|
Axis::Y => OnResize::Scale,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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::Scale);
|
||||||
|
h.set_root(leaf);
|
||||||
|
let settled = draws.get();
|
||||||
|
|
||||||
|
h.resize((800, 100));
|
||||||
|
assert!(h.needs_redraw());
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
draws.get(),
|
||||||
|
settled,
|
||||||
|
"a scaling drawing follows its box, and the output is one"
|
||||||
|
);
|
||||||
|
assert_corners!(h, leaf, (0, 0), (800, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The output is the root of the box chain, so a resize is a box that changed
|
||||||
|
/// length and `OnResize` answers for it -- there is not a second rule for the
|
||||||
|
/// window. A drawing that does not scale is redrawn whichever box moved.
|
||||||
|
#[test]
|
||||||
|
fn a_resize_redraws_what_does_not_scale() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
|
||||||
|
h.set_root(leaf);
|
||||||
|
let settled = draws.get();
|
||||||
|
|
||||||
|
h.resize((800, 100));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(draws.get(), settled + 1, "its box is a different length");
|
||||||
|
assert_corners!(h, leaf, (0, 0), (800, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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::px(width);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(draws.get(), settled);
|
||||||
|
}
|
||||||
|
|
||||||
|
h.rsc[first].size.x = Len::px(100.06);
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(draws.get(), settled + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reporting_the_same_output_size_does_not_start_a_resize() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let draws = Rc::new(Cell::new(0));
|
||||||
|
let leaf = ReadsOutput {
|
||||||
|
draws: draws.clone(),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(leaf);
|
||||||
|
let settled = draws.get();
|
||||||
|
|
||||||
|
h.resize((400, 200));
|
||||||
|
|
||||||
|
assert!(!h.needs_redraw());
|
||||||
|
h.frame();
|
||||||
|
assert_eq!(draws.get(), settled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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::px((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::px((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::px(80));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
draws.get(),
|
||||||
|
settled,
|
||||||
|
"its drawing follows its box, rather than being made again"
|
||||||
|
);
|
||||||
|
assert_corners!(h, outer, (0, 80), (400, 400));
|
||||||
|
assert_corners!(h, inner, (0, 80), (400, 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// What a transcript row is: something whose shaping depends on the width
|
||||||
|
// it is given, beside something that only has to be the right shape.
|
||||||
|
let (wraps, wrap_draws) = counted(&mut h, Size::REST, OnResize::Redraw);
|
||||||
|
let (backing, back_draws) = counted(&mut h, Size::REST, OnResize::Scale);
|
||||||
|
let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, row).span(Dir::RIGHT));
|
||||||
|
let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get());
|
||||||
|
|
||||||
|
h.rsc[bar].x = Some(Len::px(200));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
// The span reads every child's size, so redrawing one takes the span
|
||||||
|
// with it -- and the span then measures and places the redrawn child.
|
||||||
|
assert!(wrap_draws.get() > settled_wrap, "reads the width it got");
|
||||||
|
assert_eq!(back_draws.get(), settled_back, "only has to be the shape");
|
||||||
|
assert_corners!(h, backing, (200, 0), (300, 200));
|
||||||
|
assert_corners!(h, wraps, (300, 0), (400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// Its box is a fixed 80 wherever the row's edges end up, so drawing it
|
||||||
|
// again would be for a width it does not have. The declared width is what
|
||||||
|
// lets the span say that without drawing it: a width the span learnt by
|
||||||
|
// drawing the child in its own box is only an answer for that box.
|
||||||
|
let (counter, draws) = counted(&mut h, Size::from((80, 200)), OnResize::Redraw);
|
||||||
|
let fixed = counter.width(80).add(&mut h.rsc);
|
||||||
|
let (rest, _) = counted(&mut h, Size::REST, OnResize::Scale);
|
||||||
|
let row = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
||||||
|
h.set_root((bar, row).span(Dir::RIGHT));
|
||||||
|
let settled = draws.get();
|
||||||
|
|
||||||
|
h.rsc[bar].x = Some(Len::px(200));
|
||||||
|
h.frame();
|
||||||
|
|
||||||
|
assert_eq!(draws.get(), settled, "its own length did not change");
|
||||||
|
assert_corners!(h, fixed, (200, 0), (280, 200));
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
//! What a resize frame costs and what it holds, on a tree the revision before
|
||||||
|
//! #16 also builds.
|
||||||
|
//!
|
||||||
|
//! Deliberately written in the API subset `43ce8c7` and this branch share, so
|
||||||
|
//! the same source can be dropped into an old worktree and measured there:
|
||||||
|
//! that is the only like-for-like comparison with the code the retained
|
||||||
|
//! layout replaced. The random tree cannot carry one, because the generator
|
||||||
|
//! itself changed with the work.
|
||||||
|
//!
|
||||||
|
//! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \
|
||||||
|
//! -- --ignored --nocapture resize_cost
|
||||||
|
//! ROWS=2000 cargo test --release --test revision_cost \
|
||||||
|
//! -- --ignored --nocapture text_memory
|
||||||
|
//!
|
||||||
|
//! Wall time on this machine varies with CPU frequency; take the number from
|
||||||
|
//! `perf stat -e instructions:u` on the test binary directly.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// xorshift64, so one seed is one set of paragraphs on any machine.
|
||||||
|
struct Rng(u64);
|
||||||
|
|
||||||
|
impl Rng {
|
||||||
|
fn bits(&mut self) -> u64 {
|
||||||
|
self.0 ^= self.0 << 13;
|
||||||
|
self.0 ^= self.0 >> 7;
|
||||||
|
self.0 ^= self.0 << 17;
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn below(&mut self, n: usize) -> usize {
|
||||||
|
(self.bits() % n as u64) as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORDS: [&str; 24] = [
|
||||||
|
"wrapping",
|
||||||
|
"shapes",
|
||||||
|
"one",
|
||||||
|
"source",
|
||||||
|
"into",
|
||||||
|
"as",
|
||||||
|
"many",
|
||||||
|
"lines",
|
||||||
|
"as",
|
||||||
|
"the",
|
||||||
|
"box",
|
||||||
|
"leaves",
|
||||||
|
"room",
|
||||||
|
"for",
|
||||||
|
"paragraph",
|
||||||
|
"height",
|
||||||
|
"answer",
|
||||||
|
"setting",
|
||||||
|
"container",
|
||||||
|
"width",
|
||||||
|
"before",
|
||||||
|
"knows",
|
||||||
|
"measured",
|
||||||
|
"again",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A run of its own words, so nothing here is fast for two texts being the
|
||||||
|
/// same string.
|
||||||
|
fn words(rng: &mut Rng, least: usize, most: usize) -> String {
|
||||||
|
let words = least + rng.below(most - least);
|
||||||
|
let mut out = String::new();
|
||||||
|
for _ in 0..words {
|
||||||
|
if !out.is_empty() {
|
||||||
|
out.push(' ');
|
||||||
|
}
|
||||||
|
out.push_str(WORDS[rng.below(WORDS.len())]);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
const OUTPUT: (f32, f32) = (900.0, 1200.0);
|
||||||
|
|
||||||
|
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||||
|
std::env::var(name)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.parse().ok())
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A row of a fixed-width rect beside a column of one wrapping and one
|
||||||
|
/// overflowing text: the shape that makes a container measure a child in a
|
||||||
|
/// box it will not keep.
|
||||||
|
fn build(h: &mut Harness, rows: usize) -> Vec<WidgetId> {
|
||||||
|
let mut rng = Rng(1);
|
||||||
|
let mut paragraphs = Vec::new();
|
||||||
|
let mut col = Span::empty(Dir::DOWN);
|
||||||
|
for _ in 0..rows {
|
||||||
|
let mut row = Span::empty(Dir::RIGHT);
|
||||||
|
row.push(rect(Color::RED).width(Len::px(40.0)).add_strong(&mut h.rsc));
|
||||||
|
let mut body = Span::empty(Dir::DOWN);
|
||||||
|
let para = wtext(words(&mut rng, 12, 52))
|
||||||
|
.size(16)
|
||||||
|
.wrap(true)
|
||||||
|
.add_strong(&mut h.rsc);
|
||||||
|
paragraphs.push(para.id());
|
||||||
|
body.push(para);
|
||||||
|
body.push(
|
||||||
|
// Short, or its unwrapped width decides the row and the
|
||||||
|
// paragraph beside it never wraps.
|
||||||
|
wtext(words(&mut rng, 2, 6))
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add_strong(&mut h.rsc),
|
||||||
|
);
|
||||||
|
row.push(body.add_strong(&mut h.rsc));
|
||||||
|
col.push(row.add_strong(&mut h.rsc));
|
||||||
|
}
|
||||||
|
let root = col.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
paragraphs
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "measurement, not a check"]
|
||||||
|
fn resize_cost() {
|
||||||
|
let rows = env("ROWS", 40_usize);
|
||||||
|
let frames = env("FRAMES", 500_usize);
|
||||||
|
let mut h = Harness::new(OUTPUT);
|
||||||
|
let paragraphs = build(&mut h, rows);
|
||||||
|
// What it cost is only half the comparison: the old code is cheaper
|
||||||
|
// partly because it wraps at the container's whole width rather than the
|
||||||
|
// part left beside the rect, and draws past the edge of the output.
|
||||||
|
println!("output width {}", OUTPUT.0);
|
||||||
|
for (at, id) in paragraphs.iter().enumerate().take(3) {
|
||||||
|
println!("paragraph {at}: {:?}", h.region(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two widths in turn is the friendly case for anything that remembers an
|
||||||
|
// answer, so `SWEEP=1` never repeats one -- a drag rather than a toggle.
|
||||||
|
let sweep = env("SWEEP", 0_usize) != 0;
|
||||||
|
let mut elapsed = Vec::with_capacity(frames);
|
||||||
|
for frame in 0..frames {
|
||||||
|
let narrower = match sweep {
|
||||||
|
true => (frame % 256) as f32,
|
||||||
|
false => ((frame + 1) % 2) as f32 * 8.0,
|
||||||
|
};
|
||||||
|
h.resize((OUTPUT.0 - narrower, OUTPUT.1));
|
||||||
|
let start = Instant::now();
|
||||||
|
h.frame();
|
||||||
|
elapsed.push(start.elapsed().as_secs_f64() * 1000.0);
|
||||||
|
}
|
||||||
|
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||||
|
println!(
|
||||||
|
"resize: {frames} frames, min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
|
||||||
|
max {:.3} ms, total {:.1} ms",
|
||||||
|
elapsed[0],
|
||||||
|
elapsed[frames / 2],
|
||||||
|
elapsed[frames * 99 / 100],
|
||||||
|
elapsed[frames - 1],
|
||||||
|
elapsed.iter().sum::<f64>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kb(field: &str) -> u64 {
|
||||||
|
std::fs::read_to_string("/proc/self/status")
|
||||||
|
.unwrap()
|
||||||
|
.lines()
|
||||||
|
.find(|line| line.starts_with(field))
|
||||||
|
.and_then(|line| line.split_whitespace().nth(1)?.parse().ok())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report(label: &str) {
|
||||||
|
println!(
|
||||||
|
"{label:24} rss {:>7} kB peak {:>7} kB",
|
||||||
|
kb("VmRSS:"),
|
||||||
|
kb("VmHWM:")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run this one on its own: the figures are the whole process's.
|
||||||
|
#[test]
|
||||||
|
#[ignore = "measurement, not a check"]
|
||||||
|
fn text_memory() {
|
||||||
|
let rows = env("ROWS", 2000_usize);
|
||||||
|
report("before");
|
||||||
|
let mut h = Harness::new(OUTPUT);
|
||||||
|
let paragraphs = build(&mut h, rows);
|
||||||
|
report("after cold frame");
|
||||||
|
for frame in 0..40 {
|
||||||
|
h.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
report("after 40 resizes");
|
||||||
|
// Settled: the output holds still and one leaf repaints per frame.
|
||||||
|
for _ in 0..10 {
|
||||||
|
let _ = h.rsc.widgets_mut().get_dyn_mut(paragraphs[0]);
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
report("after settling");
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//! Scrolling moves content and stops at its ends.
|
||||||
|
|
||||||
|
use iris::harness::{Harness, assert_corners};
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
// Twice the window's height, so there is 200 to scroll.
|
||||||
|
let top = rect(Color::RED).height(200).add(&mut h.rsc);
|
||||||
|
let bottom = rect(Color::BLUE).height(200).add(&mut h.rsc);
|
||||||
|
h.set_root((top, bottom).span(Dir::DOWN).scrollable());
|
||||||
|
h.move_to((200, 100));
|
||||||
|
|
||||||
|
// `Scroll` starts snapped to the end.
|
||||||
|
assert_corners!(h, top, (0, -200), (400, 0));
|
||||||
|
|
||||||
|
// The handler scales a wheel line by 50.
|
||||||
|
h.scroll((0, 1));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, top, (0, -150), (400, 50));
|
||||||
|
|
||||||
|
h.scroll((0, 10));
|
||||||
|
h.frame();
|
||||||
|
assert_corners!(h, top, (0, 0), (400, 200));
|
||||||
|
}
|
||||||
+525
@@ -0,0 +1,525 @@
|
|||||||
|
//! A property test that shrinks its own counterexample.
|
||||||
|
//!
|
||||||
|
//! `generated.rs` reproduces a failure from a seed, but a seed is not a lead
|
||||||
|
//! anybody can read: the tree is hundreds of widgets, and reconstructing the
|
||||||
|
//! part that matters by hand has failed every time it has been tried. This
|
||||||
|
//! grows trees it can take apart, so a failure is reduced to the smallest
|
||||||
|
//! tree that still shows it and printed as something to write a fast test
|
||||||
|
//! from.
|
||||||
|
//!
|
||||||
|
//! cargo test --release --test shrink -- --ignored --nocapture
|
||||||
|
//!
|
||||||
|
//! `SHRINK_SEEDS` how many trees to try, `SHRINK_DEPTH` how deep to grow
|
||||||
|
//! them, `SHRINK_CASE` which scenario. It is a fuzzer: run it once the
|
||||||
|
//! ordinary tests pass, and turn what it finds into a test of its own rather
|
||||||
|
//! than leaving a seed as the record.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::{Branch, Rng};
|
||||||
|
|
||||||
|
/// The same two leaves `iris::random` grows, since only one of them reads the
|
||||||
|
/// width it is given and that is the difference that matters.
|
||||||
|
const WORDS: &[&str] = &[
|
||||||
|
"Wrapping",
|
||||||
|
"shapes",
|
||||||
|
"one",
|
||||||
|
"source",
|
||||||
|
"into",
|
||||||
|
"as",
|
||||||
|
"many",
|
||||||
|
"lines",
|
||||||
|
"as",
|
||||||
|
"the",
|
||||||
|
"box",
|
||||||
|
"leaves",
|
||||||
|
"room",
|
||||||
|
"for,",
|
||||||
|
"so",
|
||||||
|
"a",
|
||||||
|
"paragraph's",
|
||||||
|
"height",
|
||||||
|
"is",
|
||||||
|
"an",
|
||||||
|
"answer",
|
||||||
|
"and",
|
||||||
|
"not",
|
||||||
|
"a",
|
||||||
|
"setting.",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ONE_LINE: &str = "one line, overflowing whatever it is given";
|
||||||
|
|
||||||
|
const OUTER: (f32, f32) = (1920.0, 1200.0);
|
||||||
|
const INNER: (f32, f32) = (640.0, 900.0);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
enum Node {
|
||||||
|
/// Words taken from [`WORDS`], and whether it wraps.
|
||||||
|
Text(usize, bool),
|
||||||
|
/// The leaf that overflows whatever box it is given rather than wrapping.
|
||||||
|
OneLine,
|
||||||
|
Rect,
|
||||||
|
/// Direction, gap, children in creation order, and the order they are
|
||||||
|
/// attached in -- separate so a tree that reorders its children
|
||||||
|
/// still makes the same widgets in the same order, and two
|
||||||
|
/// builds line up index for index.
|
||||||
|
Span(bool, f32, Vec<Node>, Vec<usize>),
|
||||||
|
Stack(Vec<Node>),
|
||||||
|
Pad(f32, Box<Node>),
|
||||||
|
Aligned(u8, u8, Box<Node>),
|
||||||
|
Sized(Option<Len>, Option<Len>, Box<Node>),
|
||||||
|
Scroll(bool, Box<Node>),
|
||||||
|
Branch(Box<Node>, Box<Node>, Box<Node>, f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn axis_align(v: u8) -> Option<AxisAlign> {
|
||||||
|
match v % 4 {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(AxisAlign::Neg),
|
||||||
|
2 => Some(AxisAlign::Center),
|
||||||
|
_ => Some(AxisAlign::Pos),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir(down: bool) -> Dir {
|
||||||
|
if down { Dir::DOWN } else { Dir::RIGHT }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
/// Builds into `h`, pushing every id in tree order, so two builds of one
|
||||||
|
/// node line up index for index and their boxes can be compared.
|
||||||
|
fn build(
|
||||||
|
&self,
|
||||||
|
h: &mut Harness,
|
||||||
|
out: &mut Vec<WidgetId>,
|
||||||
|
spans: &mut Vec<WeakWidget<Span>>,
|
||||||
|
) -> StrongWidget {
|
||||||
|
let id: StrongWidget = match self {
|
||||||
|
Node::Text(words, wrap) => {
|
||||||
|
let n = (*words).clamp(1, WORDS.len());
|
||||||
|
wtext(WORDS[..n].join(" "))
|
||||||
|
.size(16)
|
||||||
|
.wrap(*wrap)
|
||||||
|
.add_strong(&mut h.rsc)
|
||||||
|
}
|
||||||
|
Node::OneLine => wtext(ONE_LINE).size(16).wrap(false).add_strong(&mut h.rsc),
|
||||||
|
Node::Rect => rect(Color::RED).add_strong(&mut h.rsc),
|
||||||
|
Node::Span(down, gap, kids, order) => {
|
||||||
|
let mut built: Vec<_> = kids.iter().map(|k| Some(k.build(h, out, spans))).collect();
|
||||||
|
// `order` is a permutation, so each is taken exactly once.
|
||||||
|
let children = order
|
||||||
|
.iter()
|
||||||
|
.map(|&i| built[i].take().expect("order repeats an index"))
|
||||||
|
.collect();
|
||||||
|
let handle = Span {
|
||||||
|
children,
|
||||||
|
dir: dir(*down),
|
||||||
|
gap: *gap,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
spans.push(handle);
|
||||||
|
handle.add_strong(&mut h.rsc)
|
||||||
|
}
|
||||||
|
Node::Stack(kids) => {
|
||||||
|
let children = kids.iter().map(|k| k.build(h, out, spans)).collect();
|
||||||
|
Stack {
|
||||||
|
children,
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc)
|
||||||
|
}
|
||||||
|
Node::Pad(p, kid) => {
|
||||||
|
let inner = kid.build(h, out, spans);
|
||||||
|
Pad {
|
||||||
|
padding: Padding {
|
||||||
|
left: *p,
|
||||||
|
right: *p,
|
||||||
|
top: *p,
|
||||||
|
bottom: *p,
|
||||||
|
},
|
||||||
|
inner,
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc)
|
||||||
|
}
|
||||||
|
Node::Aligned(x, y, kid) => {
|
||||||
|
let inner = kid.build(h, out, spans);
|
||||||
|
Aligned {
|
||||||
|
inner,
|
||||||
|
align: Align {
|
||||||
|
x: axis_align(*x),
|
||||||
|
y: axis_align(*y),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc)
|
||||||
|
}
|
||||||
|
Node::Sized(x, y, kid) => {
|
||||||
|
let inner = kid.build(h, out, spans);
|
||||||
|
SetSize {
|
||||||
|
inner,
|
||||||
|
x: *x,
|
||||||
|
y: *y,
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc)
|
||||||
|
}
|
||||||
|
Node::Scroll(down, kid) => {
|
||||||
|
let inner = kid.build(h, out, spans);
|
||||||
|
let axis = if *down { Axis::Y } else { Axis::X };
|
||||||
|
Scroll::new(inner, axis).add_strong(&mut h.rsc)
|
||||||
|
}
|
||||||
|
Node::Branch(probe, a, b, at) => {
|
||||||
|
let probe = probe.build(h, out, spans);
|
||||||
|
let wide = a.build(h, out, spans);
|
||||||
|
let narrow = b.build(h, out, spans);
|
||||||
|
Branch {
|
||||||
|
probe,
|
||||||
|
wide,
|
||||||
|
narrow,
|
||||||
|
threshold: *at,
|
||||||
|
}
|
||||||
|
.add_strong(&mut h.rsc)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
out.push(id.id());
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> usize {
|
||||||
|
1 + match self {
|
||||||
|
Node::Text(..) | Node::OneLine | Node::Rect => 0,
|
||||||
|
Node::Span(_, _, kids, _) | Node::Stack(kids) => kids.iter().map(Node::size).sum(),
|
||||||
|
Node::Pad(_, k)
|
||||||
|
| Node::Aligned(_, _, k)
|
||||||
|
| Node::Sized(_, _, k)
|
||||||
|
| Node::Scroll(_, k) => k.size(),
|
||||||
|
Node::Branch(p, a, b, _) => p.size() + a.size() + b.size(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every one-step simplification: a wrapper replaced by what it wrapped, a
|
||||||
|
/// child dropped, a length or a word count reduced. Ordered cheapest-first
|
||||||
|
/// so the greedy walk takes the biggest bites early.
|
||||||
|
fn smaller(&self) -> Vec<Node> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let leaf = Node::Rect;
|
||||||
|
match self {
|
||||||
|
Node::Text(words, wrap) => {
|
||||||
|
if *words > 1 {
|
||||||
|
out.push(Node::Text(words / 2, *wrap));
|
||||||
|
out.push(Node::Text(words - 1, *wrap));
|
||||||
|
}
|
||||||
|
if *wrap {
|
||||||
|
out.push(Node::Text(*words, false));
|
||||||
|
}
|
||||||
|
out.push(leaf);
|
||||||
|
}
|
||||||
|
Node::OneLine => out.push(Node::Rect),
|
||||||
|
Node::Rect => {}
|
||||||
|
Node::Span(down, gap, kids, order) => {
|
||||||
|
out.extend(order.iter().map(|&i| kids[i].clone()));
|
||||||
|
for i in 0..kids.len() {
|
||||||
|
if kids.len() > 1 {
|
||||||
|
let mut less = kids.clone();
|
||||||
|
less.remove(i);
|
||||||
|
let order = (0..less.len()).collect();
|
||||||
|
out.push(Node::Span(*down, *gap, less, order));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if *gap != 0.0 {
|
||||||
|
out.push(Node::Span(*down, 0.0, kids.clone(), order.clone()));
|
||||||
|
}
|
||||||
|
for (i, kid) in kids.iter().enumerate() {
|
||||||
|
for small in kid.smaller() {
|
||||||
|
let mut next = kids.clone();
|
||||||
|
next[i] = small;
|
||||||
|
out.push(Node::Span(*down, *gap, next, order.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node::Stack(kids) => {
|
||||||
|
out.extend(kids.iter().cloned());
|
||||||
|
for i in 0..kids.len() {
|
||||||
|
if kids.len() > 1 {
|
||||||
|
let mut less = kids.clone();
|
||||||
|
less.remove(i);
|
||||||
|
out.push(Node::Stack(less));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i, kid) in kids.iter().enumerate() {
|
||||||
|
for small in kid.smaller() {
|
||||||
|
let mut next = kids.clone();
|
||||||
|
next[i] = small;
|
||||||
|
out.push(Node::Stack(next));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Node::Pad(p, kid) => {
|
||||||
|
out.push((**kid).clone());
|
||||||
|
if *p != 0.0 {
|
||||||
|
out.push(Node::Pad(0.0, kid.clone()));
|
||||||
|
}
|
||||||
|
out.extend(
|
||||||
|
kid.smaller()
|
||||||
|
.into_iter()
|
||||||
|
.map(|k| Node::Pad(*p, Box::new(k))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Node::Aligned(x, y, kid) => {
|
||||||
|
out.push((**kid).clone());
|
||||||
|
for (nx, ny) in [(0, *y), (*x, 0)] {
|
||||||
|
if (nx, ny) != (*x, *y) {
|
||||||
|
out.push(Node::Aligned(nx, ny, kid.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.extend(
|
||||||
|
kid.smaller()
|
||||||
|
.into_iter()
|
||||||
|
.map(|k| Node::Aligned(*x, *y, Box::new(k))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Node::Sized(x, y, kid) => {
|
||||||
|
out.push((**kid).clone());
|
||||||
|
if x.is_some() {
|
||||||
|
out.push(Node::Sized(None, *y, kid.clone()));
|
||||||
|
}
|
||||||
|
if y.is_some() {
|
||||||
|
out.push(Node::Sized(*x, None, kid.clone()));
|
||||||
|
}
|
||||||
|
out.extend(
|
||||||
|
kid.smaller()
|
||||||
|
.into_iter()
|
||||||
|
.map(|k| Node::Sized(*x, *y, Box::new(k))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Node::Scroll(down, kid) => {
|
||||||
|
out.push((**kid).clone());
|
||||||
|
out.extend(
|
||||||
|
kid.smaller()
|
||||||
|
.into_iter()
|
||||||
|
.map(|k| Node::Scroll(*down, Box::new(k))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Node::Branch(p, a, b, at) => {
|
||||||
|
out.push((**p).clone());
|
||||||
|
out.push((**a).clone());
|
||||||
|
out.push((**b).clone());
|
||||||
|
for small in p.smaller() {
|
||||||
|
out.push(Node::Branch(Box::new(small), a.clone(), b.clone(), *at));
|
||||||
|
}
|
||||||
|
for small in a.smaller() {
|
||||||
|
out.push(Node::Branch(p.clone(), Box::new(small), b.clone(), *at));
|
||||||
|
}
|
||||||
|
for small in b.smaller() {
|
||||||
|
out.push(Node::Branch(p.clone(), a.clone(), Box::new(small), *at));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A declared size over about half the tree, the way `iris::random` puts them
|
||||||
|
/// in: on the way into every child rather than as a node kind of its own, so
|
||||||
|
/// readers of a size are dense rather than occasional.
|
||||||
|
fn sized(rng: &mut Rng, inner: Node) -> Node {
|
||||||
|
if !rng.chance() {
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
let len = |rng: &mut Rng| match rng.below(4) {
|
||||||
|
0 => Some(Len::px(20.0 + rng.below(180) as f32)),
|
||||||
|
1 => Some(Len::REST),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
Node::Sized(len(rng), len(rng), Box::new(inner))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grow(rng: &mut Rng, depth: usize) -> Node {
|
||||||
|
if depth == 0 {
|
||||||
|
return match rng.below(4) {
|
||||||
|
0 => Node::Text(1 + rng.below(WORDS.len()), true),
|
||||||
|
1 => Node::OneLine,
|
||||||
|
_ => Node::Rect,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let len = |rng: &mut Rng| match rng.below(4) {
|
||||||
|
0 => Some(Len::px(20.0 + rng.below(180) as f32)),
|
||||||
|
1 => Some(Len::REST),
|
||||||
|
2 => Some(Len::rel(0.25 + rng.below(3) as f32 * 0.25)),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let kid = |rng: &mut Rng| {
|
||||||
|
let inner = grow(rng, depth - 1);
|
||||||
|
sized(rng, inner)
|
||||||
|
};
|
||||||
|
match rng.below(8) {
|
||||||
|
0 => Node::Scroll(rng.chance(), Box::new(kid(rng))),
|
||||||
|
1 => Node::Aligned(rng.below(4) as u8, rng.below(4) as u8, Box::new(kid(rng))),
|
||||||
|
2 => Node::Pad(rng.below(24) as f32, Box::new(kid(rng))),
|
||||||
|
3 => Node::Sized(len(rng), len(rng), Box::new(kid(rng))),
|
||||||
|
4 => Node::Branch(
|
||||||
|
Box::new(kid(rng)),
|
||||||
|
Box::new(kid(rng)),
|
||||||
|
Box::new(kid(rng)),
|
||||||
|
rng.below(500) as f32,
|
||||||
|
),
|
||||||
|
5 => Node::Stack((0..2 + rng.below(2)).map(|_| kid(rng)).collect()),
|
||||||
|
_ => {
|
||||||
|
let kids: Vec<_> = (0..2 + rng.below(3)).map(|_| kid(rng)).collect();
|
||||||
|
let order = (0..kids.len()).collect();
|
||||||
|
Node::Span(rng.chance(), rng.below(3) as f32 * 4.0, kids, order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
enum Case {
|
||||||
|
Resize,
|
||||||
|
Repaint,
|
||||||
|
ResizeRepaint,
|
||||||
|
Reorder,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every span's children rotated by one, as a tree rather than as a change:
|
||||||
|
/// what a warm frame reaches by moving them has to be where growing them that
|
||||||
|
/// way lands.
|
||||||
|
fn reordered(node: &Node) -> Node {
|
||||||
|
match node {
|
||||||
|
Node::Span(down, gap, kids, order) => {
|
||||||
|
let kids = kids.iter().map(reordered).collect::<Vec<_>>();
|
||||||
|
let mut order = order.clone();
|
||||||
|
order.rotate_left(1);
|
||||||
|
Node::Span(*down, *gap, kids, order)
|
||||||
|
}
|
||||||
|
Node::Stack(kids) => Node::Stack(kids.iter().map(reordered).collect()),
|
||||||
|
Node::Pad(p, k) => Node::Pad(*p, Box::new(reordered(k))),
|
||||||
|
Node::Aligned(x, y, k) => Node::Aligned(*x, *y, Box::new(reordered(k))),
|
||||||
|
Node::Sized(x, y, k) => Node::Sized(*x, *y, Box::new(reordered(k))),
|
||||||
|
Node::Scroll(d, k) => Node::Scroll(*d, Box::new(reordered(k))),
|
||||||
|
Node::Branch(p, a, b, at) => Node::Branch(
|
||||||
|
Box::new(reordered(p)),
|
||||||
|
Box::new(reordered(a)),
|
||||||
|
Box::new(reordered(b)),
|
||||||
|
*at,
|
||||||
|
),
|
||||||
|
leaf => leaf.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs one scenario warm and cold and says where they disagree.
|
||||||
|
fn diverges(node: &Node, case: Case) -> Option<String> {
|
||||||
|
let resizes = matches!(case, Case::Resize | Case::ResizeRepaint);
|
||||||
|
let repaints = matches!(case, Case::Repaint | Case::ResizeRepaint);
|
||||||
|
let start = if resizes { OUTER } else { INNER };
|
||||||
|
let mut warm = Harness::new(start);
|
||||||
|
let mut warm_ids = Vec::new();
|
||||||
|
let mut warm_spans = Vec::new();
|
||||||
|
let root = node.build(&mut warm, &mut warm_ids, &mut warm_spans);
|
||||||
|
warm.state.root = Some(root);
|
||||||
|
// The frame that makes it warm: without it there is nothing retained and
|
||||||
|
// the comparison is two cold starts agreeing with each other.
|
||||||
|
warm.frame();
|
||||||
|
if resizes {
|
||||||
|
warm.resize(INNER);
|
||||||
|
warm.frame();
|
||||||
|
}
|
||||||
|
if repaints {
|
||||||
|
for &id in &warm_ids {
|
||||||
|
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
}
|
||||||
|
if case == Case::Reorder {
|
||||||
|
for span in &warm_spans {
|
||||||
|
warm.rsc[*span].children.rotate_left(1);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the warm tree was moved into, grown that way from the start.
|
||||||
|
let want = match case {
|
||||||
|
Case::Reorder => reordered(node),
|
||||||
|
_ => node.clone(),
|
||||||
|
};
|
||||||
|
let mut cold = Harness::new(INNER);
|
||||||
|
let mut cold_ids = Vec::new();
|
||||||
|
let mut cold_spans = Vec::new();
|
||||||
|
let root = want.build(&mut cold, &mut cold_ids, &mut cold_spans);
|
||||||
|
cold.state.root = Some(root);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
for (i, (&w, &c)) in warm_ids.iter().zip(&cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
let same = match (got, want) {
|
||||||
|
(Some(g), Some(c)) => {
|
||||||
|
let d = |a: f32, b: f32| (a - b).abs() <= 0.05;
|
||||||
|
d(g.top_left.x, c.top_left.x)
|
||||||
|
&& d(g.top_left.y, c.top_left.y)
|
||||||
|
&& d(g.bot_right.x, c.bot_right.x)
|
||||||
|
&& d(g.bot_right.y, c.bot_right.y)
|
||||||
|
}
|
||||||
|
(None, None) => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if !same {
|
||||||
|
return Some(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes the first simplification that still fails, until none does.
|
||||||
|
fn shrink(mut node: Node, case: Case) -> Node {
|
||||||
|
loop {
|
||||||
|
let Some(next) = node
|
||||||
|
.smaller()
|
||||||
|
.into_iter()
|
||||||
|
.find(|small| diverges(small, case).is_some())
|
||||||
|
else {
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
node = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
||||||
|
std::env::var(name)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "a fuzzer; run it once the ordinary tests pass"]
|
||||||
|
fn no_grown_tree_lays_out_differently_warm_than_cold() {
|
||||||
|
let seeds: u64 = env("SHRINK_SEEDS", 400);
|
||||||
|
let depth: usize = env("SHRINK_DEPTH", 5);
|
||||||
|
let case = match env("SHRINK_CASE", String::from("resize")).as_str() {
|
||||||
|
"repaint" => Case::Repaint,
|
||||||
|
"resize-repaint" => Case::ResizeRepaint,
|
||||||
|
"reorder" => Case::Reorder,
|
||||||
|
_ => Case::Resize,
|
||||||
|
};
|
||||||
|
|
||||||
|
for seed in 1..=seeds {
|
||||||
|
let node = grow(&mut Rng::new(seed), depth);
|
||||||
|
let Some(how) = diverges(&node, case) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let small = shrink(node.clone(), case);
|
||||||
|
println!(
|
||||||
|
"seed {seed}: {how}\ngrown {} widgets, shrank to {}\n{small:#?}",
|
||||||
|
node.size(),
|
||||||
|
small.size()
|
||||||
|
);
|
||||||
|
panic!("seed {seed} lays out differently warm than cold");
|
||||||
|
}
|
||||||
|
let sizes: Vec<usize> = (1..=seeds)
|
||||||
|
.map(|seed| grow(&mut Rng::new(seed), depth).size())
|
||||||
|
.collect();
|
||||||
|
let total: usize = sizes.iter().sum();
|
||||||
|
println!(
|
||||||
|
"{seeds} trees at depth {depth} agree: {} widgets total, largest {}",
|
||||||
|
total,
|
||||||
|
sizes.iter().max().copied().unwrap_or(0)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//! What a background task can change, and how it gets back to the ui.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_task_update_reaches_the_tree() {
|
||||||
|
let mut h = Harness::new((400, 200));
|
||||||
|
let widget = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
h.set_root(widget.task_on(CursorSense::click(), async move |mut ctx| {
|
||||||
|
ctx.update(move |_, rsc| widget(rsc).color = Color::BLUE);
|
||||||
|
}));
|
||||||
|
|
||||||
|
h.click((200, 100));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
h.await_update(Duration::from_secs(5)),
|
||||||
|
"the task sent no update"
|
||||||
|
);
|
||||||
|
assert_eq!(h.rsc[widget].color, Color::BLUE);
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
//! Traces the six-widget tree in `unsettled.rs`, to see what box its text is
|
||||||
|
//! actually drawn in on a first frame against a settled one.
|
||||||
|
|
||||||
|
#![cfg(feature = "layout-diagnostics")]
|
||||||
|
|
||||||
|
use iris::core::layout_diagnostics::{self as diag, TraceEvent};
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
|
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
||||||
|
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let sized = SetSize {
|
||||||
|
inner: wrapped.add_strong(&mut h.rsc),
|
||||||
|
x: Some(Len::px(76.0)),
|
||||||
|
y: None,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let aligned = Aligned {
|
||||||
|
inner: sized.add_strong(&mut h.rsc),
|
||||||
|
align: Align {
|
||||||
|
x: Some(AxisAlign::Pos),
|
||||||
|
y: Some(AxisAlign::Pos),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let stack = Stack {
|
||||||
|
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||||
|
vec![
|
||||||
|
plain.id(),
|
||||||
|
wrapped.id(),
|
||||||
|
sized.id(),
|
||||||
|
aligned.id(),
|
||||||
|
stack.id(),
|
||||||
|
root.id(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump(label: &str, report: &diag::Report, text: WidgetId) {
|
||||||
|
println!("--- {label} ---");
|
||||||
|
for event in report.traces() {
|
||||||
|
match event {
|
||||||
|
TraceEvent::DrawRequest {
|
||||||
|
id,
|
||||||
|
region,
|
||||||
|
pixel_size,
|
||||||
|
..
|
||||||
|
} if *id == text => {
|
||||||
|
println!(
|
||||||
|
" draw in {:.2}x{:.2} region {region:?}",
|
||||||
|
pixel_size.x, pixel_size.y
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TraceEvent::SizeReported { id, size } if *id == text => {
|
||||||
|
println!(" reported {size}")
|
||||||
|
}
|
||||||
|
TraceEvent::SizeRead { id, reader, size } if *id == text => {
|
||||||
|
println!(" size read by {reader:?}: {size}")
|
||||||
|
}
|
||||||
|
TraceEvent::Placed { id, parent, region } if *id == text => {
|
||||||
|
println!(" placed by {parent:?} at {region:?}")
|
||||||
|
}
|
||||||
|
TraceEvent::Reuse { id, outcome } if *id == text => println!(" reuse: {outcome:?}"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "a diagnostic, not a check"]
|
||||||
|
fn what_box_the_text_is_drawn_in() {
|
||||||
|
diag::clear_traced_widgets();
|
||||||
|
let _ = diag::take();
|
||||||
|
let mut h = Harness::new((640, 900));
|
||||||
|
let ids = plant(&mut h);
|
||||||
|
let text = ids[1];
|
||||||
|
diag::trace_widget(text);
|
||||||
|
let _ = diag::take();
|
||||||
|
|
||||||
|
h.frame();
|
||||||
|
dump("first frame", &diag::take(), text);
|
||||||
|
|
||||||
|
for _ in 0..2 {
|
||||||
|
for &id in &ids {
|
||||||
|
h.rsc.widgets_mut().get_dyn_mut(id);
|
||||||
|
}
|
||||||
|
let _ = diag::take();
|
||||||
|
h.frame();
|
||||||
|
dump("repaint", &diag::take(), text);
|
||||||
|
}
|
||||||
|
diag::clear_traced_widgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
|
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
||||||
|
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let aligned = Aligned {
|
||||||
|
inner: text.add_strong(&mut h.rsc),
|
||||||
|
align: Align {
|
||||||
|
x: Some(AxisAlign::Neg),
|
||||||
|
y: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let sized = SetSize {
|
||||||
|
inner: inner.add_strong(&mut h.rsc),
|
||||||
|
x: Some(Len::px(189.0)),
|
||||||
|
y: Some(Len::px(176.0)),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||||
|
vec![
|
||||||
|
text.id(),
|
||||||
|
aligned.id(),
|
||||||
|
inner.id(),
|
||||||
|
sized.id(),
|
||||||
|
filler.id(),
|
||||||
|
root.id(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "a diagnostic, not a check"]
|
||||||
|
fn what_box_the_fixed_text_is_drawn_in() {
|
||||||
|
diag::clear_traced_widgets();
|
||||||
|
let _ = diag::take();
|
||||||
|
let mut h = Harness::new((1920, 1200));
|
||||||
|
let ids = plant_fixed(&mut h);
|
||||||
|
let text = ids[0];
|
||||||
|
diag::trace_widget(text);
|
||||||
|
let _ = diag::take();
|
||||||
|
|
||||||
|
h.frame();
|
||||||
|
dump("first frame at 1920", &diag::take(), text);
|
||||||
|
h.resize((640, 900));
|
||||||
|
h.frame();
|
||||||
|
dump("after resize to 640", &diag::take(), text);
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let cids = plant_fixed(&mut cold);
|
||||||
|
diag::clear_traced_widgets();
|
||||||
|
diag::trace_widget(cids[0]);
|
||||||
|
let _ = diag::take();
|
||||||
|
cold.frame();
|
||||||
|
dump("cold at 640", &diag::take(), cids[0]);
|
||||||
|
diag::clear_traced_widgets();
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
//! The smallest trees that laid out differently warm than cold, each shrunk
|
||||||
|
//! by `tests/shrink.rs` from hundreds of widgets. The first two are a cold
|
||||||
|
//! frame that had not settled: a wrapping text shaped at a width it was
|
||||||
|
//! measured in rather than the one it was given. The rest are a widget
|
||||||
|
//! measured again in a box its own answer had decided, where the old answer
|
||||||
|
//! is a fixed point whatever the content now says.
|
||||||
|
|
||||||
|
use iris::harness::Harness;
|
||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
/// Six widgets, shrunk from a 402-widget tree the fuzzer found. Nothing about
|
||||||
|
/// the tree changes -- every widget is marked for redraw and the frame is
|
||||||
|
/// taken again -- so no box may move, and a warm frame has to land where a
|
||||||
|
/// cold one does.
|
||||||
|
fn plant(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
|
let plain = wtext("Wrapping").size(16).wrap(false).add(&mut h.rsc);
|
||||||
|
let wrapped = wtext("Wrapping shapes").size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let sized = SetSize {
|
||||||
|
inner: wrapped.add_strong(&mut h.rsc),
|
||||||
|
x: Some(Len::px(76.0)),
|
||||||
|
y: None,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let aligned = Aligned {
|
||||||
|
inner: sized.add_strong(&mut h.rsc),
|
||||||
|
align: Align {
|
||||||
|
x: Some(AxisAlign::Pos),
|
||||||
|
y: Some(AxisAlign::Pos),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let stack = Stack {
|
||||||
|
children: vec![plain.add_strong(&mut h.rsc), aligned.add_strong(&mut h.rsc)],
|
||||||
|
size: StackSize::Child(0),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let root = (stack,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
vec![
|
||||||
|
plain.id(),
|
||||||
|
wrapped.id(),
|
||||||
|
sized.id(),
|
||||||
|
aligned.id(),
|
||||||
|
stack.id(),
|
||||||
|
root.id(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first frame does not reach the layout a second one does, so "cold" is
|
||||||
|
/// not a fixed point and comparing against it compares against a tree that
|
||||||
|
/// has not settled.
|
||||||
|
#[test]
|
||||||
|
fn one_frame_is_enough() {
|
||||||
|
let mut h = Harness::new((640, 900));
|
||||||
|
let ids = plant(&mut h);
|
||||||
|
let first = h.region(&ids[1]).unwrap();
|
||||||
|
for _ in 0..3 {
|
||||||
|
for &id in &ids {
|
||||||
|
h.rsc.widgets_mut().get_dyn_mut(id);
|
||||||
|
}
|
||||||
|
h.frame();
|
||||||
|
}
|
||||||
|
let settled = h.region(&ids[1]).unwrap();
|
||||||
|
println!(
|
||||||
|
"first frame {} tall, settled {} tall",
|
||||||
|
first.bot_right.y - first.top_left.y,
|
||||||
|
settled.bot_right.y - settled.top_left.y
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
first.bot_right.y - first.top_left.y,
|
||||||
|
settled.bot_right.y - settled.top_left.y,
|
||||||
|
"the first frame had not finished laying out"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repainting_everything_moves_nothing() {
|
||||||
|
let mut warm = Harness::new((640, 900));
|
||||||
|
let ids = plant(&mut warm);
|
||||||
|
for &id in &ids {
|
||||||
|
warm.rsc.widgets_mut().get_dyn_mut(id);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let cold_ids = plant(&mut cold);
|
||||||
|
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
if got != want {
|
||||||
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Six widgets, shrunk from 905. Everything inside the declared 189x176 box
|
||||||
|
/// is the same size whatever the output is, so a resize may not change any of
|
||||||
|
/// it -- but the text comes out 3.92px narrower warm than cold.
|
||||||
|
fn plant_fixed(h: &mut Harness) -> Vec<WidgetId> {
|
||||||
|
let words = "Wrapping shapes one source into as many lines as the box leaves";
|
||||||
|
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let aligned = Aligned {
|
||||||
|
inner: text.add_strong(&mut h.rsc),
|
||||||
|
align: Align {
|
||||||
|
x: Some(AxisAlign::Neg),
|
||||||
|
y: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let inner = (aligned,).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
let sized = SetSize {
|
||||||
|
inner: inner.add_strong(&mut h.rsc),
|
||||||
|
x: Some(Len::px(189.0)),
|
||||||
|
y: Some(Len::px(176.0)),
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let root = (filler, sized).span(Dir::RIGHT).add(&mut h.rsc);
|
||||||
|
h.state.root = Some(root.add_strong(&mut h.rsc));
|
||||||
|
vec![
|
||||||
|
text.id(),
|
||||||
|
aligned.id(),
|
||||||
|
inner.id(),
|
||||||
|
sized.id(),
|
||||||
|
filler.id(),
|
||||||
|
root.id(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_resize_does_not_reach_inside_a_box_of_declared_pixels() {
|
||||||
|
let mut warm = Harness::new((1920, 1200));
|
||||||
|
let ids = plant_fixed(&mut warm);
|
||||||
|
warm.frame();
|
||||||
|
warm.resize((640, 900));
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let cold_ids = plant_fixed(&mut cold);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
if got != want {
|
||||||
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Four widgets, shrunk from 486. A span's two children are swapped: warm by
|
||||||
|
/// moving them, cold by growing them that way. Same widgets, same sizes, one
|
||||||
|
/// ends up 29.9px from where the other does.
|
||||||
|
fn plant_pair(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, WeakWidget<Span>) {
|
||||||
|
let wrapped = wtext("Wrapping shapes one source into as many lines")
|
||||||
|
.size(16)
|
||||||
|
.wrap(true)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let plain = wtext("one line, overflowing whatever it is given")
|
||||||
|
.size(16)
|
||||||
|
.wrap(false)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let first: StrongWidget = wrapped.add_strong(&mut h.rsc);
|
||||||
|
let second: StrongWidget = plain.add_strong(&mut h.rsc);
|
||||||
|
let children = match swapped {
|
||||||
|
true => vec![second, first],
|
||||||
|
false => vec![first, second],
|
||||||
|
};
|
||||||
|
let span = Span {
|
||||||
|
children,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: 0.0,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let span_handle = span;
|
||||||
|
let aligned = Aligned {
|
||||||
|
inner: span.add_strong(&mut h.rsc),
|
||||||
|
align: Align {
|
||||||
|
x: Some(AxisAlign::Center),
|
||||||
|
y: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.state.root = Some(aligned.add_strong(&mut h.rsc));
|
||||||
|
(
|
||||||
|
vec![wrapped.id(), plain.id(), span.id(), aligned.id()],
|
||||||
|
span_handle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swapping_two_children_lands_where_growing_them_that_way_does() {
|
||||||
|
let mut warm = Harness::new((640, 900));
|
||||||
|
let (ids, span) = plant_pair(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
warm.rsc[span].children.rotate_left(1);
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let (cold_ids, _) = plant_pair(&mut cold, true);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
if got != want {
|
||||||
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eight widgets, shrunk from 80. The scroll decides how wide to make its
|
||||||
|
/// content from what the content says, and hands that box down through a
|
||||||
|
/// pass-through; the span under it was placed once, in that box, so nothing
|
||||||
|
/// at its own edge says the box was its own answer.
|
||||||
|
fn plant_scrolled(h: &mut Harness, swapped: bool) -> (Vec<WidgetId>, [WeakWidget<Span>; 2]) {
|
||||||
|
let words = "Wrapping shapes one source into as many lines as the box leaves room for,";
|
||||||
|
let text = wtext(words).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let mut inner_children: Vec<StrongWidget> =
|
||||||
|
vec![text.add_strong(&mut h.rsc), filler.add_strong(&mut h.rsc)];
|
||||||
|
if swapped {
|
||||||
|
inner_children.rotate_left(1);
|
||||||
|
}
|
||||||
|
let inner = Span {
|
||||||
|
children: inner_children,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: 0.0,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let block = rect(Color::RED).add(&mut h.rsc);
|
||||||
|
let fixed = SetSize {
|
||||||
|
inner: block.add_strong(&mut h.rsc),
|
||||||
|
x: Some(Len::px(87.0)),
|
||||||
|
y: None,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let mut outer_children: Vec<StrongWidget> =
|
||||||
|
vec![fixed.add_strong(&mut h.rsc), inner.add_strong(&mut h.rsc)];
|
||||||
|
if swapped {
|
||||||
|
outer_children.rotate_left(1);
|
||||||
|
}
|
||||||
|
let outer = Span {
|
||||||
|
children: outer_children,
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: 0.0,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let through = SetSize {
|
||||||
|
inner: outer.add_strong(&mut h.rsc),
|
||||||
|
x: None,
|
||||||
|
y: None,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let scroll = Scroll::new(through.add_strong(&mut h.rsc), Axis::X).add(&mut h.rsc);
|
||||||
|
h.state.root = Some(scroll.add_strong(&mut h.rsc));
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
text.id(),
|
||||||
|
filler.id(),
|
||||||
|
inner.id(),
|
||||||
|
block.id(),
|
||||||
|
fixed.id(),
|
||||||
|
outer.id(),
|
||||||
|
through.id(),
|
||||||
|
scroll.id(),
|
||||||
|
],
|
||||||
|
[inner, outer],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_span_placed_once_in_a_box_its_answer_decided() {
|
||||||
|
let mut warm = Harness::new((640, 900));
|
||||||
|
let (ids, spans) = plant_scrolled(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
for span in spans {
|
||||||
|
warm.rsc[span].children.rotate_left(1);
|
||||||
|
}
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((640, 900));
|
||||||
|
let (cold_ids, _) = plant_scrolled(&mut cold, true);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
let mut wrong = Vec::new();
|
||||||
|
for (i, (&w, &c)) in ids.iter().zip(&cold_ids).enumerate() {
|
||||||
|
let (got, want) = (warm.region(&w), cold.region(&c));
|
||||||
|
if got != want {
|
||||||
|
wrong.push(format!("widget {i}: warm {got:?} cold {want:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user