Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dc7614ae6 | ||
|
|
e865467a3f | ||
|
|
23376aef25 | ||
|
|
36fec09d11 | ||
|
|
5494642dec | ||
|
|
8cac927438 | ||
|
|
827d317f41 | ||
|
|
e53ce585e6 | ||
|
|
f3fd9417d4 | ||
|
|
3ab9c922fd | ||
|
|
71ba3723ff | ||
|
|
0a14df2cc3 | ||
|
|
0e7076a01c | ||
|
|
f62131eecf | ||
|
|
028521b419 |
No files matched your search
Generated
+16
-25
@@ -567,9 +567,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dlib"
|
name = "dlib"
|
||||||
version = "0.5.3"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
|
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
@@ -2158,9 +2158,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.41.0"
|
version = "0.38.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
|
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -2353,15 +2353,6 @@ version = "0.8.52"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
|
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rig-input"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"iris",
|
|
||||||
"wayland-client",
|
|
||||||
"wayland-protocols-wlr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "roxmltree"
|
name = "roxmltree"
|
||||||
version = "0.21.1"
|
version = "0.21.1"
|
||||||
@@ -2918,9 +2909,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-backend"
|
name = "wayland-backend"
|
||||||
version = "0.3.17"
|
version = "0.3.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078"
|
checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"downcast-rs",
|
"downcast-rs",
|
||||||
@@ -2932,9 +2923,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-client"
|
name = "wayland-client"
|
||||||
version = "0.31.15"
|
version = "0.31.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073"
|
checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"rustix 1.1.3",
|
"rustix 1.1.3",
|
||||||
@@ -2966,9 +2957,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-protocols"
|
name = "wayland-protocols"
|
||||||
version = "0.32.13"
|
version = "0.32.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
|
checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
@@ -2991,9 +2982,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-protocols-wlr"
|
name = "wayland-protocols-wlr"
|
||||||
version = "0.3.12"
|
version = "0.3.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
|
checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
@@ -3004,9 +2995,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-scanner"
|
name = "wayland-scanner"
|
||||||
version = "0.31.11"
|
version = "0.31.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
|
checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
@@ -3015,9 +3006,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-sys"
|
name = "wayland-sys"
|
||||||
version = "0.31.11"
|
version = "0.31.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
|
checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dlib",
|
"dlib",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
+1
-6
@@ -3,9 +3,6 @@ name = "iris"
|
|||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
layout-diagnostics = ["iris-core/layout-diagnostics"]
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -23,7 +20,7 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
|
|||||||
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
|
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["core", "macro", "rig-input"]
|
members = ["core", "macro"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -43,5 +40,3 @@ arboard = "3.6.1"
|
|||||||
iris-core = { path = "core" }
|
iris-core = { path = "core" }
|
||||||
iris-macro = { path = "macro" }
|
iris-macro = { path = "macro" }
|
||||||
tokio = "1.49.0"
|
tokio = "1.49.0"
|
||||||
wayland-client = "0.31.15"
|
|
||||||
wayland-protocols-wlr = { version = "0.3.12", features = ["client"] }
|
|
||||||
@@ -3,9 +3,6 @@ name = "iris-core"
|
|||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[features]
|
|
||||||
layout-diagnostics = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wgpu = { workspace = true }
|
wgpu = { workspace = true }
|
||||||
bytemuck ={ workspace = true }
|
bytemuck ={ workspace = true }
|
||||||
|
|||||||
@@ -1,488 +0,0 @@
|
|||||||
//! Opt-in counters and coarse timers for explaining CPU layout cost.
|
|
||||||
//!
|
|
||||||
//! Enable the `layout-diagnostics` feature. With it disabled, none of the
|
|
||||||
//! instrumentation is compiled into Iris. The retained rig in
|
|
||||||
//! `tests/layout_diagnostics.rs` is the ordinary entry point.
|
|
||||||
//!
|
|
||||||
//! Timers are inclusive: `update total` contains `full layout` or
|
|
||||||
//! `incremental layout`, and `text render` contains shaping and glyph
|
|
||||||
//! placement. They locate cost within one instrumented run and must not be
|
|
||||||
//! added together. Use an uninstrumented build under `perf` for final CPU
|
|
||||||
//! totals; counting every primitive and distinct widget deliberately perturbs
|
|
||||||
//! the instrumented run.
|
|
||||||
//!
|
|
||||||
//! Call [`trace_widget`] before a frame to retain the ordered constraint,
|
|
||||||
//! reuse, size, placement, and text events for one suspicious widget. The
|
|
||||||
//! selection is a set and survives [`take`] until cleared.
|
|
||||||
|
|
||||||
use crate::{Axis, Len, Size, UiRegion, WidgetId, util::Vec2};
|
|
||||||
use std::{
|
|
||||||
cell::RefCell,
|
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
fmt::Write,
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub(crate) enum Counter {
|
|
||||||
Updates,
|
|
||||||
ResizeDependents,
|
|
||||||
DrawRequests,
|
|
||||||
WidgetDraws,
|
|
||||||
PlaceCalls,
|
|
||||||
SizeReads,
|
|
||||||
HintHits,
|
|
||||||
HintMisses,
|
|
||||||
RetainedSizeHits,
|
|
||||||
ReuseAttempts,
|
|
||||||
ReuseExact,
|
|
||||||
ReuseMoved,
|
|
||||||
ReuseDirty,
|
|
||||||
ReuseWrongParent,
|
|
||||||
ReuseUnslotted,
|
|
||||||
ReuseOwnResize,
|
|
||||||
ReuseDescendantResize,
|
|
||||||
ResizeChecks,
|
|
||||||
ResizeCheckChildren,
|
|
||||||
QueuePops,
|
|
||||||
DepthReads,
|
|
||||||
EagerReaderRedraws,
|
|
||||||
LocalRedraws,
|
|
||||||
SizeChanges,
|
|
||||||
ReaderEdges,
|
|
||||||
PrimitiveWrites,
|
|
||||||
TextRenders,
|
|
||||||
TextShapeHits,
|
|
||||||
TextShapes,
|
|
||||||
TextBreaks,
|
|
||||||
GlyphPlacements,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Counter {
|
|
||||||
const COUNT: usize = Self::GlyphPlacements as usize + 1;
|
|
||||||
|
|
||||||
const NAMES: [&'static str; Self::COUNT] = [
|
|
||||||
"updates",
|
|
||||||
"resize dependents",
|
|
||||||
"draw requests",
|
|
||||||
"widget draws",
|
|
||||||
"place calls",
|
|
||||||
"draw-result size reads",
|
|
||||||
"hint hits",
|
|
||||||
"hint misses",
|
|
||||||
"retained size hits",
|
|
||||||
"reuse attempts",
|
|
||||||
"reuse exact",
|
|
||||||
"reuse moved",
|
|
||||||
"reuse: dirty",
|
|
||||||
"reuse: wrong parent",
|
|
||||||
"reuse: unslotted",
|
|
||||||
"reuse: own resize",
|
|
||||||
"reuse: descendant resize",
|
|
||||||
"resize checks",
|
|
||||||
"resize children checked",
|
|
||||||
"redraw queue pops",
|
|
||||||
"depth reads",
|
|
||||||
"eager reader redraws",
|
|
||||||
"local redraws",
|
|
||||||
"size changes",
|
|
||||||
"reader edges",
|
|
||||||
"primitive writes",
|
|
||||||
"text renders",
|
|
||||||
"text shape hits",
|
|
||||||
"text shapes",
|
|
||||||
"text line breaks",
|
|
||||||
"glyph placements",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub(crate) enum TimerKind {
|
|
||||||
Update,
|
|
||||||
FullLayout,
|
|
||||||
ResizeMarking,
|
|
||||||
IncrementalLayout,
|
|
||||||
TextRender,
|
|
||||||
TextShape,
|
|
||||||
TextBreak,
|
|
||||||
GlyphPlacement,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TimerKind {
|
|
||||||
const COUNT: usize = Self::GlyphPlacement as usize + 1;
|
|
||||||
|
|
||||||
const NAMES: [&'static str; Self::COUNT] = [
|
|
||||||
"update total",
|
|
||||||
"full layout",
|
|
||||||
"resize marking",
|
|
||||||
"incremental layout",
|
|
||||||
"text render",
|
|
||||||
"text shape",
|
|
||||||
"text line break",
|
|
||||||
"glyph placement",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Report {
|
|
||||||
counters: [u64; Counter::COUNT],
|
|
||||||
nanos: [u64; TimerKind::COUNT],
|
|
||||||
distinct_widgets: usize,
|
|
||||||
distinct_text_widgets: usize,
|
|
||||||
hot_widgets: Vec<Callsite>,
|
|
||||||
hot_text: Vec<Callsite>,
|
|
||||||
traces: Vec<TraceEvent>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Report {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
counters: [0; Counter::COUNT],
|
|
||||||
nanos: [0; TimerKind::COUNT],
|
|
||||||
distinct_widgets: 0,
|
|
||||||
distinct_text_widgets: 0,
|
|
||||||
hot_widgets: Vec::new(),
|
|
||||||
hot_text: Vec::new(),
|
|
||||||
traces: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Report {
|
|
||||||
pub fn counters(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
|
|
||||||
Counter::NAMES.into_iter().zip(self.counters)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Inclusive elapsed time accumulated for each targeted operation.
|
|
||||||
pub fn timings_ns(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
|
|
||||||
TimerKind::NAMES.into_iter().zip(self.nanos)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn distinct_widgets(&self) -> usize {
|
|
||||||
self.distinct_widgets
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn distinct_text_widgets(&self) -> usize {
|
|
||||||
self.distinct_text_widgets
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hot_widgets(&self) -> &[Callsite] {
|
|
||||||
&self.hot_widgets
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hot_text(&self) -> &[Callsite] {
|
|
||||||
&self.hot_text
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ordered layout events for widgets selected with [`trace_widget`].
|
|
||||||
pub fn traces(&self) -> &[TraceEvent] {
|
|
||||||
&self.traces
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formats nonzero totals divided by `frames`.
|
|
||||||
pub fn per_frame(&self, frames: usize) -> String {
|
|
||||||
let divisor = frames.max(1) as f64;
|
|
||||||
let mut out = String::new();
|
|
||||||
for (name, value) in self.counters() {
|
|
||||||
if value != 0 {
|
|
||||||
let _ = writeln!(out, " {name:<27} {:>12.2}", value as f64 / divisor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.distinct_widgets != 0 {
|
|
||||||
let _ = writeln!(
|
|
||||||
out,
|
|
||||||
" {:<27} {:>12}",
|
|
||||||
"distinct widgets", self.distinct_widgets
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if self.distinct_text_widgets != 0 {
|
|
||||||
let _ = writeln!(
|
|
||||||
out,
|
|
||||||
" {:<27} {:>12}",
|
|
||||||
"distinct text widgets", self.distinct_text_widgets
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (name, nanos) in self.timings_ns() {
|
|
||||||
if nanos != 0 {
|
|
||||||
let ms = nanos as f64 / divisor / 1_000_000.0;
|
|
||||||
let _ = writeln!(out, " {name:<27} {ms:>12.3} ms");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !self.hot_widgets.is_empty() {
|
|
||||||
let _ = writeln!(out, " hottest widget draws:");
|
|
||||||
for callsite in &self.hot_widgets {
|
|
||||||
let calls = callsite.calls as f64 / divisor;
|
|
||||||
let _ = writeln!(
|
|
||||||
out,
|
|
||||||
" {calls:>9.2} {:?} {}",
|
|
||||||
callsite.id, callsite.label
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !self.hot_text.is_empty() {
|
|
||||||
let _ = writeln!(out, " hottest text renders:");
|
|
||||||
for callsite in &self.hot_text {
|
|
||||||
let calls = callsite.calls as f64 / divisor;
|
|
||||||
let _ = writeln!(
|
|
||||||
out,
|
|
||||||
" {calls:>9.2} {:>3} widths {:?} {}",
|
|
||||||
callsite.distinct_widths, callsite.id, callsite.label
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !self.traces.is_empty() {
|
|
||||||
let _ = writeln!(out, " targeted layout trace:");
|
|
||||||
for event in &self.traces {
|
|
||||||
let _ = writeln!(out, " {event:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Callsite {
|
|
||||||
pub id: WidgetId,
|
|
||||||
pub label: String,
|
|
||||||
pub calls: u64,
|
|
||||||
pub distinct_widths: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum ReuseOutcome {
|
|
||||||
Exact,
|
|
||||||
Moved,
|
|
||||||
Dirty,
|
|
||||||
WrongParent,
|
|
||||||
Unslotted,
|
|
||||||
OwnResize,
|
|
||||||
DescendantResize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One targeted layout event. Events are retained in execution order, making
|
|
||||||
/// repeated constraint paths visible without logging every widget globally.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum TraceEvent {
|
|
||||||
DrawRequest {
|
|
||||||
id: WidgetId,
|
|
||||||
parent: Option<WidgetId>,
|
|
||||||
region: UiRegion,
|
|
||||||
pixel_size: Vec2,
|
|
||||||
slotted: bool,
|
|
||||||
},
|
|
||||||
Reuse {
|
|
||||||
id: WidgetId,
|
|
||||||
outcome: ReuseOutcome,
|
|
||||||
},
|
|
||||||
SizeReported {
|
|
||||||
id: WidgetId,
|
|
||||||
size: Size,
|
|
||||||
},
|
|
||||||
Placed {
|
|
||||||
id: WidgetId,
|
|
||||||
parent: WidgetId,
|
|
||||||
region: UiRegion,
|
|
||||||
},
|
|
||||||
SizeRead {
|
|
||||||
id: WidgetId,
|
|
||||||
reader: WidgetId,
|
|
||||||
size: Size,
|
|
||||||
},
|
|
||||||
HintRead {
|
|
||||||
id: WidgetId,
|
|
||||||
reader: WidgetId,
|
|
||||||
axis: Axis,
|
|
||||||
hint: Option<Len>,
|
|
||||||
},
|
|
||||||
TextRendered {
|
|
||||||
id: WidgetId,
|
|
||||||
width: Option<f32>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct Calls {
|
|
||||||
label: String,
|
|
||||||
count: u64,
|
|
||||||
widths: HashSet<Option<u32>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct Current {
|
|
||||||
report: Report,
|
|
||||||
widgets: HashMap<WidgetId, Calls>,
|
|
||||||
text_widgets: HashMap<WidgetId, Calls>,
|
|
||||||
traced: HashSet<WidgetId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
static CURRENT: RefCell<Current> = RefCell::new(Current::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn bump(counter: Counter) {
|
|
||||||
CURRENT.with_borrow_mut(|current| current.report.counters[counter as usize] += 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn draw_widget(id: WidgetId, label: &str) {
|
|
||||||
CURRENT.with_borrow_mut(|current| {
|
|
||||||
let calls = current.widgets.entry(id).or_default();
|
|
||||||
if calls.label.is_empty() {
|
|
||||||
calls.label = label.to_owned();
|
|
||||||
}
|
|
||||||
calls.count += 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a widget to the targeted trace set. Selection survives [`take`]
|
|
||||||
/// until explicitly removed or cleared.
|
|
||||||
pub fn trace_widget(id: impl Into<WidgetId>) {
|
|
||||||
CURRENT.with_borrow_mut(|current| {
|
|
||||||
current.traced.insert(id.into());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn untrace_widget(id: impl Into<WidgetId>) {
|
|
||||||
CURRENT.with_borrow_mut(|current| {
|
|
||||||
current.traced.remove(&id.into());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_traced_widgets() {
|
|
||||||
CURRENT.with_borrow_mut(|current| current.traced.clear());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trace(id: WidgetId, event: TraceEvent) {
|
|
||||||
CURRENT.with_borrow_mut(|current| {
|
|
||||||
if current.traced.contains(&id) {
|
|
||||||
current.report.traces.push(event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn draw_request(
|
|
||||||
id: WidgetId,
|
|
||||||
parent: Option<WidgetId>,
|
|
||||||
region: UiRegion,
|
|
||||||
pixel_size: Vec2,
|
|
||||||
slotted: bool,
|
|
||||||
) {
|
|
||||||
trace(
|
|
||||||
id,
|
|
||||||
TraceEvent::DrawRequest {
|
|
||||||
id,
|
|
||||||
parent,
|
|
||||||
region,
|
|
||||||
pixel_size,
|
|
||||||
slotted,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn reuse(id: WidgetId, outcome: ReuseOutcome) {
|
|
||||||
trace(id, TraceEvent::Reuse { id, outcome });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn size_reported(id: WidgetId, size: Size) {
|
|
||||||
trace(id, TraceEvent::SizeReported { id, size });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn placed(id: WidgetId, parent: WidgetId, region: UiRegion) {
|
|
||||||
trace(id, TraceEvent::Placed { id, parent, region });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn size_read(id: WidgetId, reader: WidgetId, size: Size) {
|
|
||||||
trace(id, TraceEvent::SizeRead { id, reader, size });
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn hint_read(id: WidgetId, reader: WidgetId, axis: Axis, hint: Option<Len>) {
|
|
||||||
trace(
|
|
||||||
id,
|
|
||||||
TraceEvent::HintRead {
|
|
||||||
id,
|
|
||||||
reader,
|
|
||||||
axis,
|
|
||||||
hint,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn render_text(id: WidgetId, label: &str, width: Option<f32>) {
|
|
||||||
CURRENT.with_borrow_mut(|current| {
|
|
||||||
let calls = current.text_widgets.entry(id).or_default();
|
|
||||||
if calls.label.is_empty() {
|
|
||||||
calls.label = label.to_owned();
|
|
||||||
}
|
|
||||||
calls.count += 1;
|
|
||||||
calls.widths.insert(width.map(f32::to_bits));
|
|
||||||
if current.traced.contains(&id) {
|
|
||||||
current
|
|
||||||
.report
|
|
||||||
.traces
|
|
||||||
.push(TraceEvent::TextRendered { id, width });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct Timer {
|
|
||||||
kind: TimerKind,
|
|
||||||
start: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn timer(kind: TimerKind) -> Timer {
|
|
||||||
Timer {
|
|
||||||
kind,
|
|
||||||
start: Instant::now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Timer {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let nanos = self.start.elapsed().as_nanos().min(u64::MAX as u128) as u64;
|
|
||||||
CURRENT.with_borrow_mut(|current| current.report.nanos[self.kind as usize] += nanos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes all diagnostics accumulated on this thread and resets them.
|
|
||||||
pub fn take() -> Report {
|
|
||||||
CURRENT.with_borrow_mut(|current| {
|
|
||||||
current.report.distinct_widgets = current.widgets.len();
|
|
||||||
current.report.distinct_text_widgets = current.text_widgets.len();
|
|
||||||
current.report.hot_widgets = hottest(¤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,9 +10,6 @@
|
|||||||
#![feature(coerce_unsized)]
|
#![feature(coerce_unsized)]
|
||||||
#![feature(option_into_flat_iter)]
|
#![feature(option_into_flat_iter)]
|
||||||
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
pub mod layout_diagnostics;
|
|
||||||
|
|
||||||
mod attr;
|
mod attr;
|
||||||
mod event;
|
mod event;
|
||||||
mod num;
|
mod num;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
pub enum Axis {
|
pub enum Axis {
|
||||||
X,
|
X,
|
||||||
Y,
|
Y,
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ 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,
|
||||||
@@ -202,6 +209,12 @@ impl UiScalar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn outside(&self, span: &UiSpan) -> Self {
|
||||||
|
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel);
|
||||||
|
let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
|
||||||
|
Self { rel, abs }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn within_len(&self, len: UiScalar) -> Self {
|
pub fn within_len(&self, len: UiScalar) -> Self {
|
||||||
self.within(&UiSpan {
|
self.within(&UiSpan {
|
||||||
start: UiScalar::ZERO,
|
start: UiScalar::ZERO,
|
||||||
@@ -270,6 +283,13 @@ impl UiSpan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn outside(&self, parent: &Self) -> Self {
|
||||||
|
Self {
|
||||||
|
start: self.start.outside(parent),
|
||||||
|
end: self.end.outside(parent),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn len(&self) -> UiScalar {
|
pub const fn len(&self) -> UiScalar {
|
||||||
self.end - self.start
|
self.end - self.start
|
||||||
}
|
}
|
||||||
@@ -304,7 +324,14 @@ impl UiRegion {
|
|||||||
y: self.y.within(&parent.y),
|
y: self.y.within(&parent.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub const fn axis(&self, axis: Axis) -> &UiSpan {
|
pub const fn outside(&self, parent: &Self) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x.outside(&parent.x),
|
||||||
|
y: self.y.outside(&parent.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn axis(&mut self, axis: Axis) -> &UiSpan {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => &self.x,
|
Axis::X => &self.x,
|
||||||
Axis::Y => &self.y,
|
Axis::Y => &self.y,
|
||||||
|
|||||||
+11
-130
@@ -1,5 +1,3 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
use crate::layout_diagnostics::{self as diag, Counter, TimerKind};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
|
Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2,
|
||||||
};
|
};
|
||||||
@@ -7,10 +5,7 @@ use parley::{
|
|||||||
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout,
|
||||||
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
collections::VecDeque,
|
|
||||||
hash::{DefaultHasher, Hash, Hasher},
|
|
||||||
};
|
|
||||||
use swash::{
|
use swash::{
|
||||||
FontRef,
|
FontRef,
|
||||||
scale::{Render, ScaleContext, Source, StrikeWith},
|
scale::{Render, ScaleContext, Source, StrikeWith},
|
||||||
@@ -22,32 +17,8 @@ pub struct TextData {
|
|||||||
pub layout_ctx: LayoutContext<UiColor>,
|
pub layout_ctx: LayoutContext<UiColor>,
|
||||||
scale_ctx: ScaleContext,
|
scale_ctx: ScaleContext,
|
||||||
pub atlas: GlyphAtlas,
|
pub atlas: GlyphAtlas,
|
||||||
spare: VecDeque<Placed>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The glyphs of one text at one width. A buffer holds the ones it is drawn
|
|
||||||
/// as; these are the ones it had before, kept because a container measures a
|
|
||||||
/// child by drawing it in a box it may not keep, and so comes back to widths
|
|
||||||
/// it has already asked for.
|
|
||||||
struct Placed {
|
|
||||||
/// Where the glyphs land is a function of these three and nothing else,
|
|
||||||
/// so no widget or buffer identity is involved and two texts of the same
|
|
||||||
/// words share an answer.
|
|
||||||
text: String,
|
|
||||||
key: LayoutKey,
|
|
||||||
glyphs: RenderedText,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How many to keep. Bounding the whole store rather than each buffer is what
|
|
||||||
/// makes this a fixed cost instead of one a tree of ten thousand texts pays
|
|
||||||
/// ten thousand times; the re-asks come from laying out one subtree, so they
|
|
||||||
/// are close together and few are needed. Instructions over 500 resize frames
|
|
||||||
/// of `tests/revision_cost.rs`, both the repeating widths and the sweep that
|
|
||||||
/// cannot hit across frames: 13.7B at 32, 12.1B at 64, 10.4B and 12.1B at 128,
|
|
||||||
/// and nothing past that -- so 128, which is no worse in the case that never
|
|
||||||
/// repeats and better in the one that does.
|
|
||||||
const SPARE_PLACED: usize = 128;
|
|
||||||
|
|
||||||
impl Default for TextData {
|
impl Default for TextData {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -55,7 +26,6 @@ impl Default for TextData {
|
|||||||
layout_ctx: LayoutContext::new(),
|
layout_ctx: LayoutContext::new(),
|
||||||
scale_ctx: ScaleContext::new(),
|
scale_ctx: ScaleContext::new(),
|
||||||
atlas: GlyphAtlas::default(),
|
atlas: GlyphAtlas::default(),
|
||||||
spare: VecDeque::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,9 +81,6 @@ 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)]
|
||||||
@@ -128,7 +95,6 @@ impl TextBuffer {
|
|||||||
text: text.into(),
|
text: text.into(),
|
||||||
layout: Layout::new(),
|
layout: Layout::new(),
|
||||||
layout_key: None,
|
layout_key: None,
|
||||||
placed: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,28 +119,15 @@ impl TextBuffer {
|
|||||||
if text != self.text {
|
if text != self.text {
|
||||||
self.text = text;
|
self.text = text;
|
||||||
self.layout_key = None;
|
self.layout_key = None;
|
||||||
self.placed = None;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Invalidates the layout and returns the underlying string for editing.
|
/// Invalidates the layout and returns the underlying string for editing.
|
||||||
pub fn edit(&mut self) -> &mut String {
|
pub fn edit(&mut self) -> &mut String {
|
||||||
self.layout_key = None;
|
self.layout_key = None;
|
||||||
self.placed = None;
|
|
||||||
&mut self.text
|
&mut self.text
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The glyphs of the shaping it is drawn as, once they are placed.
|
|
||||||
pub fn rendered(&self) -> Option<&RenderedText> {
|
|
||||||
self.placed.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The width its shaping wraps at, and `None` where it does not wrap or
|
|
||||||
/// has not been shaped.
|
|
||||||
pub fn wrap_width(&self) -> Option<f32> {
|
|
||||||
self.layout_key.as_ref()?.max_width
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn size(&self) -> Vec2 {
|
pub fn size(&self) -> Vec2 {
|
||||||
Vec2::new(self.layout.width(), self.layout.height())
|
Vec2::new(self.layout.width(), self.layout.height())
|
||||||
}
|
}
|
||||||
@@ -185,41 +138,8 @@ impl TextBuffer {
|
|||||||
max_width: width,
|
max_width: width,
|
||||||
};
|
};
|
||||||
if self.layout_key.as_ref() == Some(&layout_key) {
|
if self.layout_key.as_ref() == Some(&layout_key) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::TextShapeHits);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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);
|
||||||
@@ -230,13 +150,10 @@ impl TextBuffer {
|
|||||||
)));
|
)));
|
||||||
builder.push_default(StyleProperty::Brush(attrs.color));
|
builder.push_default(StyleProperty::Brush(attrs.color));
|
||||||
builder.build_into(&mut self.layout, &self.text);
|
builder.build_into(&mut self.layout, &self.text);
|
||||||
self.break_lines(width);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn break_lines(&mut self, width: Option<f32>) {
|
|
||||||
self.layout.break_all_lines(width);
|
self.layout.break_all_lines(width);
|
||||||
self.layout
|
self.layout
|
||||||
.align(Alignment::Start, AlignmentOptions::default());
|
.align(Alignment::Start, AlignmentOptions::default());
|
||||||
|
self.layout_key = Some(layout_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,54 +265,18 @@ pub struct RenderedText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TextData {
|
impl TextData {
|
||||||
/// The glyphs of this text at this width, taken out of what is kept.
|
pub fn render(
|
||||||
fn take_placed(&mut self, text: &str, key: &LayoutKey) -> Option<RenderedText> {
|
|
||||||
// From the newest, since a re-ask is usually of something recent.
|
|
||||||
let at = self
|
|
||||||
.spare
|
|
||||||
.iter()
|
|
||||||
.rposition(|spare| spare.key == *key && spare.text == text)?;
|
|
||||||
self.spare.remove(at).map(|spare| spare.glyphs)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn keep_placed(&mut self, placed: Placed) {
|
|
||||||
if self.spare.len() >= SPARE_PLACED {
|
|
||||||
self.spare.pop_front();
|
|
||||||
}
|
|
||||||
self.spare.push_back(placed);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render<'b>(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &'b mut TextBuffer,
|
buffer: &mut TextBuffer,
|
||||||
attrs: &TextAttrs,
|
attrs: &TextAttrs,
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
) -> &'b RenderedText {
|
) -> RenderedText {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::TextRenders);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
let _render = diag::timer(TimerKind::TextRender);
|
|
||||||
buffer.shape(self, attrs, width);
|
buffer.shape(self, attrs, width);
|
||||||
// Only asked for when the buffer no longer holds them: taking one out
|
let glyphs = self.place(buffer);
|
||||||
// of the store to then drop it would throw an answer away.
|
RenderedText {
|
||||||
let placed = buffer.placed.take().or_else(|| {
|
glyphs,
|
||||||
let key = buffer.layout_key.as_ref()?;
|
size: buffer.size(),
|
||||||
self.take_placed(&buffer.text, key)
|
color: attrs.color,
|
||||||
});
|
}
|
||||||
let placed = match placed {
|
|
||||||
Some(placed) => placed,
|
|
||||||
None => {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::GlyphPlacements);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
let _place = diag::timer(TimerKind::GlyphPlacement);
|
|
||||||
RenderedText {
|
|
||||||
glyphs: self.place(buffer),
|
|
||||||
size: buffer.size(),
|
|
||||||
color: attrs.color,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
buffer.placed.insert(placed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-46
@@ -1,10 +1,11 @@
|
|||||||
use crate::{UiRegion, util::Id, util::Vec2};
|
use crate::{UiRegion, util::Id};
|
||||||
use wgpu::*;
|
use wgpu::*;
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||||
pub struct WindowUniform {
|
pub struct WindowUniform {
|
||||||
pub dim: Vec2,
|
pub width: f32,
|
||||||
|
pub height: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -12,17 +13,15 @@ pub struct WindowUniform {
|
|||||||
pub struct PrimitiveInstance {
|
pub struct PrimitiveInstance {
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
pub mask_idx: MaskIdx,
|
pub mask_idx: MaskIdx,
|
||||||
pub move_idx: MoveIdx,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrimitiveInstance {
|
impl PrimitiveInstance {
|
||||||
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![
|
const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![
|
||||||
0 => Float32x2,
|
0 => Float32x2,
|
||||||
1 => Float32x2,
|
1 => Float32x2,
|
||||||
2 => Float32x2,
|
2 => Float32x2,
|
||||||
3 => Float32x2,
|
3 => Float32x2,
|
||||||
4 => Uint32,
|
4 => Uint32,
|
||||||
5 => Uint32,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn desc() -> VertexBufferLayout<'static> {
|
pub fn desc() -> VertexBufferLayout<'static> {
|
||||||
@@ -44,45 +43,4 @@ impl MaskIdx {
|
|||||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct Mask {
|
pub struct Mask {
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
pub move_idx: MoveIdx,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Its own type rather than another `Id<u32>`, because it sits beside
|
|
||||||
/// `MaskIdx` in an instance and the two must not be swappable.
|
|
||||||
#[repr(transparent)]
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)]
|
|
||||||
pub struct MoveIdx(u32);
|
|
||||||
|
|
||||||
impl MoveIdx {
|
|
||||||
pub const NONE: Self = Self(u32::MAX);
|
|
||||||
|
|
||||||
pub(crate) fn slot(idx: usize) -> Self {
|
|
||||||
Self(idx as u32)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn idx(self) -> usize {
|
|
||||||
self.0 as usize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One link of the chain a primitive's position is resolved through: the box
|
|
||||||
/// its contents are placed within, given in the coordinates of the slot it
|
|
||||||
/// names. Moving or resizing a subtree writes its own slot and nothing else.
|
|
||||||
///
|
|
||||||
/// The identity is `UiRegion::FULL`, not zero: a zeroed entry is a box of no
|
|
||||||
/// extent, which collapses everything under it to a point.
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub struct MoveOffset {
|
|
||||||
pub region: UiRegion,
|
|
||||||
pub parent: MoveIdx,
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl bytemuck::Pod for MoveOffset {}
|
|
||||||
unsafe impl bytemuck::Zeroable for MoveOffset {}
|
|
||||||
|
|
||||||
impl MoveOffset {
|
|
||||||
pub fn new(parent: MoveIdx, region: UiRegion) -> Self {
|
|
||||||
Self { region, parent }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+17
-44
@@ -17,7 +17,7 @@ mod texture;
|
|||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
pub use atlas::*;
|
pub use atlas::*;
|
||||||
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset};
|
pub use data::{Mask, MaskIdx};
|
||||||
pub use primitive::*;
|
pub use primitive::*;
|
||||||
|
|
||||||
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||||
@@ -34,7 +34,6 @@ pub struct UiRenderNode {
|
|||||||
active: Vec<usize>,
|
active: Vec<usize>,
|
||||||
window_buffer: Buffer,
|
window_buffer: Buffer,
|
||||||
masks: ArrBuf<Mask>,
|
masks: ArrBuf<Mask>,
|
||||||
moves: ArrBuf<MoveOffset>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RenderLayer {
|
struct RenderLayer {
|
||||||
@@ -128,35 +127,32 @@ impl UiRenderNode {
|
|||||||
for primitive in &mut self.primitives {
|
for primitive in &mut self.primitives {
|
||||||
primitive.render.update(ui);
|
primitive.render.update(ui);
|
||||||
}
|
}
|
||||||
let mut regroup = false;
|
|
||||||
if ui.masks.changed {
|
if ui.masks.changed {
|
||||||
ui.masks.changed = false;
|
ui.masks.changed = false;
|
||||||
regroup |= self.masks.update(device, queue, &ui.masks[..]);
|
if self.masks.update(device, queue, &ui.masks[..]) {
|
||||||
}
|
self.shared_group = Self::shared_group(
|
||||||
if ui_render.moves.changed {
|
device,
|
||||||
ui_render.moves.changed = false;
|
&self.shared_layout,
|
||||||
regroup |= self.moves.update(device, queue, ui_render.moves.entries());
|
&self.window_buffer,
|
||||||
}
|
&self.masks,
|
||||||
if regroup {
|
);
|
||||||
self.shared_group = Self::shared_group(
|
}
|
||||||
device,
|
|
||||||
&self.shared_layout,
|
|
||||||
&self.window_buffer,
|
|
||||||
&self.masks,
|
|
||||||
&self.moves,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
||||||
let size = size.into();
|
let size = size.into();
|
||||||
let slice = &[WindowUniform { dim: size }];
|
let slice = &[WindowUniform {
|
||||||
|
width: size.x,
|
||||||
|
height: size.y,
|
||||||
|
}];
|
||||||
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
||||||
let window_uniform = WindowUniform {
|
let window_uniform = WindowUniform {
|
||||||
dim: Vec2::new(config.width as f32, config.height as f32),
|
width: config.width as f32,
|
||||||
|
height: config.height as f32,
|
||||||
};
|
};
|
||||||
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
||||||
label: Some("window"),
|
label: Some("window"),
|
||||||
@@ -170,13 +166,7 @@ impl UiRenderNode {
|
|||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||||
"ui masks",
|
"ui masks",
|
||||||
);
|
);
|
||||||
let moves = ArrBuf::new(
|
let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks);
|
||||||
device,
|
|
||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
|
||||||
"ui move offsets",
|
|
||||||
);
|
|
||||||
let shared_group =
|
|
||||||
Self::shared_group(device, &shared_layout, &window_buffer, &masks, &moves);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
shared_layout,
|
shared_layout,
|
||||||
@@ -187,7 +177,6 @@ impl UiRenderNode {
|
|||||||
layers: HashMap::default(),
|
layers: HashMap::default(),
|
||||||
active: Vec::new(),
|
active: Vec::new(),
|
||||||
masks,
|
masks,
|
||||||
moves,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,8 +252,7 @@ impl UiRenderNode {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What every draw in the ui is given: the window, the masks and the
|
/// What every draw in the ui is given: the window and the masks.
|
||||||
/// move chain every position is resolved through.
|
|
||||||
fn shared_layout(device: &Device) -> BindGroupLayout {
|
fn shared_layout(device: &Device) -> BindGroupLayout {
|
||||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||||
entries: &[
|
entries: &[
|
||||||
@@ -288,16 +276,6 @@ impl UiRenderNode {
|
|||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
},
|
},
|
||||||
BindGroupLayoutEntry {
|
|
||||||
binding: 2,
|
|
||||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Buffer {
|
|
||||||
ty: BufferBindingType::Storage { read_only: true },
|
|
||||||
has_dynamic_offset: false,
|
|
||||||
min_binding_size: BufferSize::new(size_of::<MoveOffset>() as u64),
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
label: Some("ui shared"),
|
label: Some("ui shared"),
|
||||||
})
|
})
|
||||||
@@ -308,7 +286,6 @@ impl UiRenderNode {
|
|||||||
layout: &BindGroupLayout,
|
layout: &BindGroupLayout,
|
||||||
window: &Buffer,
|
window: &Buffer,
|
||||||
masks: &ArrBuf<Mask>,
|
masks: &ArrBuf<Mask>,
|
||||||
moves: &ArrBuf<MoveOffset>,
|
|
||||||
) -> BindGroup {
|
) -> BindGroup {
|
||||||
device.create_bind_group(&BindGroupDescriptor {
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
layout,
|
layout,
|
||||||
@@ -321,10 +298,6 @@ impl UiRenderNode {
|
|||||||
binding: 1,
|
binding: 1,
|
||||||
resource: masks.buffer.as_entire_binding(),
|
resource: masks.buffer.as_entire_binding(),
|
||||||
},
|
},
|
||||||
BindGroupEntry {
|
|
||||||
binding: 2,
|
|
||||||
resource: moves.buffer.as_entire_binding(),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
label: Some("ui shared"),
|
label: Some("ui shared"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::{any::TypeId, marker::PhantomData};
|
|||||||
use crate::{
|
use crate::{
|
||||||
Color, TextureHandle, UiData, UiRegion, WidgetId,
|
Color, TextureHandle, UiData, UiRegion, WidgetId,
|
||||||
render::{
|
render::{
|
||||||
data::{MaskIdx, MoveIdx, PrimitiveInstance},
|
data::{MaskIdx, PrimitiveInstance},
|
||||||
page::GlyphRender,
|
page::GlyphRender,
|
||||||
texture::ImageRender,
|
texture::ImageRender,
|
||||||
},
|
},
|
||||||
@@ -246,7 +246,6 @@ impl LayerDraws {
|
|||||||
primitive,
|
primitive,
|
||||||
region,
|
region,
|
||||||
mask_idx,
|
mask_idx,
|
||||||
move_idx,
|
|
||||||
}: PrimitiveInst<P>,
|
}: PrimitiveInst<P>,
|
||||||
) -> PrimitiveHandle {
|
) -> PrimitiveHandle {
|
||||||
self.updated = true;
|
self.updated = true;
|
||||||
@@ -259,11 +258,7 @@ impl LayerDraws {
|
|||||||
.get_or_insert_with(InstanceList::new::<P>)
|
.get_or_insert_with(InstanceList::new::<P>)
|
||||||
.push(
|
.push(
|
||||||
id,
|
id,
|
||||||
PrimitiveInstance {
|
PrimitiveInstance { region, mask_idx },
|
||||||
region,
|
|
||||||
mask_idx,
|
|
||||||
move_idx,
|
|
||||||
},
|
|
||||||
bytemuck::bytes_of(&primitive),
|
bytemuck::bytes_of(&primitive),
|
||||||
);
|
);
|
||||||
PrimitiveHandle {
|
PrimitiveHandle {
|
||||||
@@ -309,7 +304,6 @@ pub struct PrimitiveInst<P> {
|
|||||||
pub primitive: P,
|
pub primitive: P,
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
pub mask_idx: MaskIdx,
|
pub mask_idx: MaskIdx,
|
||||||
pub move_idx: MoveIdx,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PrimitiveChange {
|
pub struct PrimitiveChange {
|
||||||
@@ -353,7 +347,7 @@ impl RectPrimitive {
|
|||||||
|
|
||||||
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
|
/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph
|
||||||
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
|
/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects.
|
||||||
#[repr(C)]
|
#[repr(C, align(8))]
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct GlyphPrimitive {
|
pub struct GlyphPrimitive {
|
||||||
pub uv_min: Vec2,
|
pub uv_min: Vec2,
|
||||||
@@ -364,8 +358,8 @@ pub struct GlyphPrimitive {
|
|||||||
pub flags: u32,
|
pub flags: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual rather than derived: `Vec2`'s alignment leaves four bytes of padding
|
// Manual rather than derived: the align(8) leaves four bytes of padding, which
|
||||||
// here, which is how WGSL lays the struct out.
|
// is how WGSL lays the struct out.
|
||||||
unsafe impl bytemuck::Pod for GlyphPrimitive {}
|
unsafe impl bytemuck::Pod for GlyphPrimitive {}
|
||||||
unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
|
unsafe impl bytemuck::Zeroable for GlyphPrimitive {}
|
||||||
impl Primitive for GlyphPrimitive {
|
impl Primitive for GlyphPrimitive {
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
var<uniform> window: WindowUniform;
|
var<uniform> window: WindowUniform;
|
||||||
@group(0) @binding(1)
|
@group(0) @binding(1)
|
||||||
var<storage> masks: array<Mask>;
|
var<storage> masks: array<Mask>;
|
||||||
@group(0) @binding(2)
|
|
||||||
var<storage> move_offsets: array<MoveOffset>;
|
|
||||||
|
|
||||||
struct WindowUniform {
|
struct WindowUniform {
|
||||||
dim: vec2<f32>,
|
dim: vec2<f32>,
|
||||||
@@ -17,49 +15,6 @@ struct WindowUniform {
|
|||||||
struct Mask {
|
struct Mask {
|
||||||
x: UiSpan,
|
x: UiSpan,
|
||||||
y: UiSpan,
|
y: UiSpan,
|
||||||
move_idx: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MoveOffset {
|
|
||||||
x: UiSpan,
|
|
||||||
y: UiSpan,
|
|
||||||
parent: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Region {
|
|
||||||
x: UiSpan,
|
|
||||||
y: UiSpan,
|
|
||||||
}
|
|
||||||
|
|
||||||
const MOVE_NONE: u32 = 4294967295u;
|
|
||||||
// Keep in step with `iris_core::CHAIN_LIMIT`. It bounds a malformed cycle
|
|
||||||
// rather than any real tree, and the CPU walk uses the same number so both
|
|
||||||
// resolve a deep one the same way.
|
|
||||||
const CHAIN_LIMIT: u32 = 64u;
|
|
||||||
|
|
||||||
fn scalar_within(s: UiScalar, p: UiSpan) -> UiScalar {
|
|
||||||
return UiScalar(
|
|
||||||
mix(p.start.rel, p.end.rel, s.rel),
|
|
||||||
s.abs + mix(p.start.abs, p.end.abs, s.rel),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn span_within(s: UiSpan, p: UiSpan) -> UiSpan {
|
|
||||||
return UiSpan(scalar_within(s.start, p), scalar_within(s.end, p));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_move(idx: u32, local: Region) -> Region {
|
|
||||||
var r = local;
|
|
||||||
var at = idx;
|
|
||||||
for (var step = 0u; step < CHAIN_LIMIT; step++) {
|
|
||||||
if at == MOVE_NONE {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let entry = move_offsets[at];
|
|
||||||
r = Region(span_within(r.x, entry.x), span_within(r.y, entry.y));
|
|
||||||
at = entry.parent;
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UiSpan {
|
struct UiSpan {
|
||||||
@@ -78,7 +33,6 @@ struct InstanceInput {
|
|||||||
@location(2) y_start: vec2<f32>,
|
@location(2) y_start: vec2<f32>,
|
||||||
@location(3) y_end: vec2<f32>,
|
@location(3) y_end: vec2<f32>,
|
||||||
@location(4) mask_idx: u32,
|
@location(4) mask_idx: u32,
|
||||||
@location(5) move_idx: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@@ -98,15 +52,10 @@ fn vs_main(
|
|||||||
) -> VertexOutput {
|
) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
|
|
||||||
let local = Region(
|
let top_left_rel = vec2(in.x_start.x, in.y_start.x);
|
||||||
UiSpan(UiScalar(in.x_start.x, in.x_start.y), UiScalar(in.x_end.x, in.x_end.y)),
|
let top_left_abs = vec2(in.x_start.y, in.y_start.y);
|
||||||
UiSpan(UiScalar(in.y_start.x, in.y_start.y), UiScalar(in.y_end.x, in.y_end.y)),
|
let bot_right_rel = vec2(in.x_end.x, in.y_end.x);
|
||||||
);
|
let bot_right_abs = vec2(in.x_end.y, in.y_end.y);
|
||||||
let r = resolve_move(in.move_idx, local);
|
|
||||||
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
|
|
||||||
let top_left_abs = vec2(r.x.start.abs, r.y.start.abs);
|
|
||||||
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
|
|
||||||
let bot_right_abs = vec2(r.x.end.abs, r.y.end.abs);
|
|
||||||
|
|
||||||
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs);
|
||||||
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
|
let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs);
|
||||||
@@ -132,13 +81,10 @@ fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
|||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
let mask = masks[in.mask_idx];
|
let mask = masks[in.mask_idx];
|
||||||
// Its own chain, not the drawn primitive's, so a stationary viewport
|
let tl = vec2(mask.x.start.rel, mask.y.start.rel);
|
||||||
// clips content that moves inside it.
|
let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs);
|
||||||
let m = resolve_move(mask.move_idx, Region(mask.x, mask.y));
|
let br = vec2(mask.x.end.rel, mask.y.end.rel);
|
||||||
let tl = vec2(m.x.start.rel, m.y.start.rel);
|
let br_abs = vec2(mask.x.end.abs, mask.y.end.abs);
|
||||||
let tl_abs = vec2(m.x.start.abs, m.y.start.abs);
|
|
||||||
let br = vec2(m.x.end.rel, m.y.end.rel);
|
|
||||||
let br_abs = vec2(m.x.end.abs, m.y.end.abs);
|
|
||||||
|
|
||||||
let top_left = floor(tl * window.dim) + floor(tl_abs);
|
let top_left = floor(tl * window.dim) + floor(tl_abs);
|
||||||
let bot_right = floor(br * window.dim) + floor(br_abs);
|
let bot_right = floor(br * window.dim) + floor(br_abs);
|
||||||
|
|||||||
+1
-30
@@ -1,43 +1,14 @@
|
|||||||
use crate::{
|
use crate::{LayerId, MaskIdx, PrimitiveHandle, TextureHandle, UiRegion, WidgetId};
|
||||||
LayerId, MaskIdx, MoveIdx, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId, util::Vec2,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// important non rendering data for retained drawing
|
/// important non rendering data for retained drawing
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ActiveData {
|
pub struct ActiveData {
|
||||||
pub id: WidgetId,
|
pub id: WidgetId,
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
/// What the widget said it used of `region`, the last time it drew.
|
|
||||||
pub size: Size,
|
|
||||||
/// The pixel size of the box it drew against. `region` alone cannot say:
|
|
||||||
/// it is a fraction of a slot's box, and the same fraction of a box that
|
|
||||||
/// has since changed is a different number of pixels.
|
|
||||||
pub px: Vec2,
|
|
||||||
pub parent: Option<WidgetId>,
|
pub parent: Option<WidgetId>,
|
||||||
/// 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,
|
|
||||||
/// Output axes it read directly or while resolving its offered box.
|
|
||||||
pub reads_output: [bool; 2],
|
|
||||||
/// The slot its primitives are positioned through: its own if its parent
|
|
||||||
/// placed it, otherwise the nearest ancestor that has one.
|
|
||||||
pub move_idx: MoveIdx,
|
|
||||||
/// The slot `region` is given in, which is whatever its parent drew in.
|
|
||||||
pub parent_move: MoveIdx,
|
|
||||||
pub mask: MaskIdx,
|
pub mask: MaskIdx,
|
||||||
pub layer: LayerId,
|
pub layer: LayerId,
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use crate::{BothAxis, Len, UiVec2, WidgetId, util::HashMap};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Cache {
|
||||||
|
pub size: BothAxis<HashMap<WidgetId, (UiVec2, Len)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cache {
|
||||||
|
pub fn remove(&mut self, id: WidgetId) {
|
||||||
|
self.size.x.remove(&id);
|
||||||
|
self.size.y.remove(&id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.size.x.clear();
|
||||||
|
self.size.y.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-90
@@ -1,21 +1,17 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget,
|
Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena,
|
||||||
WidgetId, Widgets,
|
|
||||||
util::{Arena, Id, TrackedArena},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// How far the shader will walk a move chain. It bounds a malformed cycle
|
|
||||||
/// rather than any real tree; `Moves::resolve` uses the same number so the
|
|
||||||
/// two agree on what a deep tree resolves to.
|
|
||||||
pub const CHAIN_LIMIT: u32 = 64;
|
|
||||||
|
|
||||||
mod active;
|
mod active;
|
||||||
|
mod cache;
|
||||||
mod painter;
|
mod painter;
|
||||||
mod render_state;
|
mod render_state;
|
||||||
|
mod size;
|
||||||
|
|
||||||
pub use active::*;
|
pub use active::*;
|
||||||
pub use painter::{Painter, PrimitiveLike};
|
pub use painter::{Painter, PrimitiveLike};
|
||||||
pub use render_state::*;
|
pub use render_state::*;
|
||||||
|
pub use size::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct UiData {
|
pub struct UiData {
|
||||||
@@ -27,88 +23,6 @@ pub struct UiData {
|
|||||||
pub masks: TrackedArena<Mask, u32>,
|
pub masks: TrackedArena<Mask, u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where each widget's drawing sits relative to its parent's slot, so moving
|
|
||||||
/// a subtree writes one entry rather than every descendant's primitives.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Moves {
|
|
||||||
arena: Arena<MoveOffset, u32>,
|
|
||||||
pub changed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Moves {
|
|
||||||
pub fn push(&mut self, parent: MoveIdx, region: UiRegion) -> MoveIdx {
|
|
||||||
self.changed = true;
|
|
||||||
MoveIdx::slot(self.arena.push(MoveOffset::new(parent, region)).idx())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-points a slot at a different parent, for a widget drawn somewhere
|
|
||||||
/// else in the tree than it was.
|
|
||||||
pub fn set_parent(&mut self, idx: MoveIdx, parent: MoveIdx) {
|
|
||||||
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
|
|
||||||
if entry.parent != parent {
|
|
||||||
entry.parent = parent;
|
|
||||||
self.changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(&mut self, idx: MoveIdx) {
|
|
||||||
self.changed = true;
|
|
||||||
self.arena.remove(Id::preset(idx.idx() as u32));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the box a slot's contents are placed within, itself given in the
|
|
||||||
/// coordinates of its parent slot.
|
|
||||||
pub fn set(&mut self, idx: MoveIdx, region: UiRegion) {
|
|
||||||
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
|
|
||||||
if entry.region != region {
|
|
||||||
entry.region = region;
|
|
||||||
self.changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Composes a region held in `idx`'s coordinates down the chain, which is
|
|
||||||
/// the same walk the vertex shader does.
|
|
||||||
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
|
|
||||||
let mut region = local;
|
|
||||||
let mut at = idx;
|
|
||||||
for _ in 0..CHAIN_LIMIT {
|
|
||||||
if at == MoveIdx::NONE {
|
|
||||||
return region;
|
|
||||||
}
|
|
||||||
let entry = self.arena[at.idx()];
|
|
||||||
region = region.within(&entry.region);
|
|
||||||
at = entry.parent;
|
|
||||||
}
|
|
||||||
debug_assert!(
|
|
||||||
at == MoveIdx::NONE,
|
|
||||||
"a move chain longer than {CHAIN_LIMIT} resolves to the wrong place, \
|
|
||||||
and the shader stops at the same depth"
|
|
||||||
);
|
|
||||||
region
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How many slots a region in `idx` is composed through, which is what
|
|
||||||
/// the shader's walk costs per primitive.
|
|
||||||
pub fn depth(&self, idx: MoveIdx) -> usize {
|
|
||||||
let mut depth = 0;
|
|
||||||
let mut at = idx;
|
|
||||||
while at != MoveIdx::NONE && depth < CHAIN_LIMIT as usize {
|
|
||||||
at = self.arena[at.idx()].parent;
|
|
||||||
depth += 1;
|
|
||||||
}
|
|
||||||
depth
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn entries(&self) -> &[MoveOffset] {
|
|
||||||
&self.arena
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.changed = true;
|
|
||||||
self.arena = Arena::default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait UiRsc {
|
pub trait UiRsc {
|
||||||
fn ui(&self) -> &UiData;
|
fn ui(&self) -> &UiData;
|
||||||
fn ui_mut(&mut self) -> &mut UiData;
|
fn ui_mut(&mut self) -> &mut UiData;
|
||||||
|
|||||||
+31
-225
@@ -1,11 +1,9 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
use crate::layout_diagnostics::{self as diag, Counter};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Axis, Len, RenderedText, Size, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle,
|
Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData,
|
||||||
UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, WidgetId,
|
TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId,
|
||||||
render::{
|
render::{
|
||||||
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
|
GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind,
|
||||||
PrimitiveKind, TexturePrimitive,
|
TexturePrimitive,
|
||||||
},
|
},
|
||||||
util::Vec2,
|
util::Vec2,
|
||||||
};
|
};
|
||||||
@@ -15,23 +13,12 @@ pub struct Painter<'a> {
|
|||||||
pub(super) state: &'a mut UiRenderState,
|
pub(super) state: &'a mut UiRenderState,
|
||||||
pub(super) rsc: &'a mut dyn UiRsc,
|
pub(super) rsc: &'a mut dyn UiRsc,
|
||||||
|
|
||||||
/// This widget's box, in the coordinates of `move_idx`.
|
|
||||||
pub(super) region: UiRegion,
|
pub(super) region: UiRegion,
|
||||||
pub(super) mask: MaskIdx,
|
pub(super) mask: MaskIdx,
|
||||||
pub(super) textures: Vec<TextureHandle>,
|
pub(super) textures: Vec<TextureHandle>,
|
||||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||||
pub(super) children: Vec<WidgetId>,
|
pub(super) children: Vec<WidgetId>,
|
||||||
/// The children whose size this widget read while drawing.
|
|
||||||
pub(super) size_deps: Vec<WidgetId>,
|
|
||||||
/// Offered pixel axes which can affect the size this draw reports.
|
|
||||||
pub(super) size_box_inputs: [bool; 2],
|
|
||||||
pub(super) size_output_inputs: [bool; 2],
|
|
||||||
pub(super) reads_output: [bool; 2],
|
|
||||||
/// The slot this widget's primitives are positioned through: its own if
|
|
||||||
/// its parent placed it, otherwise the nearest ancestor that has one.
|
|
||||||
pub(super) move_idx: MoveIdx,
|
|
||||||
pub layer: usize,
|
pub layer: usize,
|
||||||
pub(super) depth: usize,
|
|
||||||
pub(super) id: WidgetId,
|
pub(super) id: WidgetId,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,8 +30,6 @@ impl<'a> Painter<'a> {
|
|||||||
|
|
||||||
/// Takes the kind, for a caller writing many of one primitive.
|
/// Takes the kind, for a caller writing many of one primitive.
|
||||||
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
|
fn write<P: Primitive>(&mut self, kind: PrimitiveKind<P>, primitive: P, region: UiRegion) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::PrimitiveWrites);
|
|
||||||
let h = self.state.layers.write(
|
let h = self.state.layers.write(
|
||||||
self.layer,
|
self.layer,
|
||||||
PrimitiveInst {
|
PrimitiveInst {
|
||||||
@@ -53,7 +38,6 @@ impl<'a> Painter<'a> {
|
|||||||
primitive,
|
primitive,
|
||||||
region,
|
region,
|
||||||
mask_idx: self.mask,
|
mask_idx: self.mask,
|
||||||
move_idx: self.move_idx,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.push_primitive(h);
|
self.push_primitive(h);
|
||||||
@@ -80,175 +64,39 @@ impl<'a> Painter<'a> {
|
|||||||
|
|
||||||
pub fn set_mask(&mut self, region: UiRegion) {
|
pub fn set_mask(&mut self, region: UiRegion) {
|
||||||
assert!(self.mask == MaskIdx::NONE);
|
assert!(self.mask == MaskIdx::NONE);
|
||||||
self.mask = self.rsc.ui_mut().masks.push(Mask {
|
self.mask = self.rsc.ui_mut().masks.push(Mask { region });
|
||||||
region,
|
|
||||||
move_idx: self.move_idx,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a widget within this widget's region.
|
/// Draws a widget within this widget's region.
|
||||||
pub fn widget<'s, W: ?Sized>(&'s mut self, id: &'s StrongWidget<W>) -> DrawResult<'s, 'a, W> {
|
pub fn widget<W: ?Sized>(&mut self, id: &StrongWidget<W>) {
|
||||||
self.widget_at(id, self.region, false)
|
self.widget_at(id, self.region);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a widget somewhere within this one.
|
/// Draws a widget somewhere within this one.
|
||||||
pub fn widget_within<'s, W: ?Sized>(
|
/// Useful for drawing child widgets in select areas.
|
||||||
&'s mut self,
|
pub fn widget_within<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
|
||||||
id: &'s StrongWidget<W>,
|
self.widget_at(id, region.within(&self.region));
|
||||||
region: UiRegion,
|
|
||||||
) -> DrawResult<'s, 'a, W> {
|
|
||||||
let region = region.within(&self.region);
|
|
||||||
self.widget_at(id, region, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a child this widget decides the box of, and may decide again
|
fn widget_at<W: ?Sized>(&mut self, id: &StrongWidget<W>, region: UiRegion) {
|
||||||
/// once it knows what the child came to. The child gets a slot of its
|
self.children.push(id.id());
|
||||||
/// own, so placing it a second time writes one entry however much it
|
self.state.draw_inner(
|
||||||
/// 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,
|
||||||
);
|
);
|
||||||
DrawResult {
|
|
||||||
child: id,
|
|
||||||
painter: self,
|
|
||||||
size,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a child says its length is without being drawn, if it can say.
|
pub fn render_text(
|
||||||
/// 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,
|
||||||
child: &StrongWidget<W>,
|
buffer: &mut TextBuffer,
|
||||||
axis: Axis,
|
|
||||||
region: UiRegion,
|
|
||||||
) -> Option<Len> {
|
|
||||||
if let Some(hint) = self.size_hint(child, axis) {
|
|
||||||
return Some(hint);
|
|
||||||
}
|
|
||||||
self.retained_size(child, region)
|
|
||||||
.map(|size| size.axis(axis))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn retained_size<W: ?Sized>(
|
|
||||||
&mut self,
|
|
||||||
child: &StrongWidget<W>,
|
|
||||||
region: UiRegion,
|
|
||||||
) -> Option<Size> {
|
|
||||||
let region = region.within(&self.region);
|
|
||||||
let (size, box_inputs, output_inputs) =
|
|
||||||
self.state
|
|
||||||
.retained_size(child.id(), region, self.move_idx, self.rsc.widgets())?;
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::RetainedSizeHits);
|
|
||||||
self.depend_on_size_inputs(child, box_inputs, output_inputs);
|
|
||||||
Some(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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>,
|
||||||
) -> &'b RenderedText {
|
) -> RenderedText {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::render_text(self.id, self.rsc.widgets().label(self.id), width);
|
|
||||||
let ui = self.rsc.ui_mut();
|
let ui = self.rsc.ui_mut();
|
||||||
ui.text.render(buffer, attrs, width)
|
ui.text.render(buffer, attrs, width)
|
||||||
}
|
}
|
||||||
@@ -277,48 +125,27 @@ impl<'a> Painter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This widget's box, in the coordinates its own primitives are written
|
|
||||||
/// in -- so a region composed `within` it may be drawn directly.
|
|
||||||
pub fn region(&self) -> UiRegion {
|
pub fn region(&self) -> UiRegion {
|
||||||
self.region
|
self.region
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The output's size in pixels. A widget that reads it draws again when
|
pub fn size<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>) -> Size {
|
||||||
/// the output changes, since nothing else can put that right.
|
self.size_ctx().size(id)
|
||||||
pub fn output_size(&mut self) -> Vec2 {
|
}
|
||||||
self.reads_output = [true; 2];
|
|
||||||
self.size_output_inputs = [true; 2];
|
pub fn len_axis<W: ?Sized + Widget>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Len {
|
||||||
|
match axis {
|
||||||
|
Axis::X => self.size_ctx().width(id),
|
||||||
|
Axis::Y => self.size_ctx().height(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_size(&self) -> Vec2 {
|
||||||
self.state.output_size
|
self.state.output_size
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One axis of the output in pixels. Prefer this to [`Self::output_size`]
|
|
||||||
/// when the other axis cannot affect the size this widget reports.
|
|
||||||
pub fn output_len(&mut self, axis: Axis) -> f32 {
|
|
||||||
self.reads_output[axis as usize] = true;
|
|
||||||
self.size_output_inputs[axis as usize] = true;
|
|
||||||
self.state.output_size.axis(axis)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This widget's box in pixels. Resolved against the output's size and
|
|
||||||
/// the boxes it sits within, so a widget that reads it draws again when
|
|
||||||
/// the output changes.
|
|
||||||
pub fn px_size(&mut self) -> Vec2 {
|
pub fn px_size(&mut self) -> Vec2 {
|
||||||
self.reads_output = [true; 2];
|
self.region.size().to_abs(self.state.output_size)
|
||||||
self.size_box_inputs = [true; 2];
|
|
||||||
let region = self.state.moves.resolve(self.move_idx, self.region);
|
|
||||||
region.size().to_abs(self.state.output_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One axis of this widget's box in pixels. Prefer this to
|
|
||||||
/// [`Self::px_size`] when the other axis cannot affect the reported size.
|
|
||||||
pub fn px_len(&mut self, axis: Axis) -> f32 {
|
|
||||||
self.reads_output[axis as usize] = true;
|
|
||||||
self.size_box_inputs[axis as usize] = true;
|
|
||||||
let region = self.state.moves.resolve(self.move_idx, self.region);
|
|
||||||
region
|
|
||||||
.size()
|
|
||||||
.axis(axis)
|
|
||||||
.to_abs(self.state.output_size.axis(axis))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_data(&mut self) -> &mut TextData {
|
pub fn text_data(&mut self) -> &mut TextData {
|
||||||
@@ -340,30 +167,9 @@ impl<'a> Painter<'a> {
|
|||||||
pub fn id(&self) -> &WidgetId {
|
pub fn id(&self) -> &WidgetId {
|
||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// A child that has just been drawn. Reading its size records that this
|
pub fn size_ctx(&mut self) -> SizeCtx<'_> {
|
||||||
/// widget's own size depends on it; dropping it without reading draws the
|
self.state.size_ctx(self.id, self.region.size(), self.rsc)
|
||||||
/// child and leaves the parent independent of what it came to.
|
|
||||||
pub struct DrawResult<'p, 'a, W: ?Sized> {
|
|
||||||
painter: &'p mut Painter<'a>,
|
|
||||||
child: &'p StrongWidget<W>,
|
|
||||||
size: Size,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W: ?Sized> DrawResult<'_, '_, W> {
|
|
||||||
pub fn size(self) -> Size {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::SizeReads);
|
|
||||||
diag::size_read(self.child.id(), self.painter.id, self.size);
|
|
||||||
}
|
|
||||||
self.painter.depend_on_drawn_size(self.child);
|
|
||||||
self.size
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn len(self, axis: Axis) -> Len {
|
|
||||||
self.size().axis(axis)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+81
-550
@@ -1,40 +1,19 @@
|
|||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, MoveIdx, Moves, OnResize, Painter, PixelRegion,
|
ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget,
|
||||||
Size, StrongWidget, UiRegion, UiRsc, WidgetId, Widgets,
|
UiRegion, UiRsc, UiVec2, WidgetId, Widgets,
|
||||||
util::{HashMap, HashSet, Vec2},
|
ui::cache::Cache,
|
||||||
|
util::{HashMap, HashSet, Vec2, forget_ref},
|
||||||
};
|
};
|
||||||
|
|
||||||
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
|
||||||
const LAYOUT_EPSILON_PX: f32 = 0.05;
|
|
||||||
|
|
||||||
fn pixel_len_changed(old: f32, new: f32) -> bool {
|
|
||||||
(old - new).abs() > LAYOUT_EPSILON_PX
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UiRenderState {
|
pub struct UiRenderState {
|
||||||
pub active: HashMap<WidgetId, ActiveData>,
|
pub active: HashMap<WidgetId, ActiveData>,
|
||||||
pub layers: DrawLayers,
|
pub layers: DrawLayers,
|
||||||
pub(super) output_size: Vec2,
|
pub(super) output_size: Vec2,
|
||||||
|
pub cache: Cache,
|
||||||
|
|
||||||
old_root: Option<WidgetId>,
|
old_root: Option<WidgetId>,
|
||||||
resized: [bool; 2],
|
resized: bool,
|
||||||
/// Content/state dirtiness whose retained size cannot answer a layout
|
|
||||||
/// question until that widget has drawn again.
|
|
||||||
invalid_sizes: HashSet<WidgetId>,
|
|
||||||
/// Marks introduced only to traverse resize dependency paths. Unlike
|
|
||||||
/// content dirtiness, these may retain an answer whose observed pixel
|
|
||||||
/// axes did not change.
|
|
||||||
resize_marks: HashSet<WidgetId>,
|
|
||||||
/// 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 {
|
||||||
@@ -42,23 +21,17 @@ impl UiRenderState {
|
|||||||
Self {
|
Self {
|
||||||
active: Default::default(),
|
active: Default::default(),
|
||||||
layers: Default::default(),
|
layers: Default::default(),
|
||||||
|
cache: Default::default(),
|
||||||
output_size: Vec2::ZERO,
|
output_size: Vec2::ZERO,
|
||||||
old_root: None,
|
old_root: None,
|
||||||
resized: [false; 2],
|
resized: false,
|
||||||
invalid_sizes: Default::default(),
|
|
||||||
resize_marks: Default::default(),
|
|
||||||
draw_started: Default::default(),
|
draw_started: Default::default(),
|
||||||
slots: Default::default(),
|
|
||||||
moves: Default::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
pub fn resize(&mut self, size: impl Into<Vec2>) {
|
||||||
let size = size.into();
|
self.output_size = size.into();
|
||||||
for (axis, resized) in AXES.into_iter().zip(self.resized.iter_mut()) {
|
self.resized = true;
|
||||||
*resized |= size.axis(axis) != self.output_size.axis(axis);
|
|
||||||
}
|
|
||||||
self.output_size = size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn output_size(&self) -> Vec2 {
|
pub fn output_size(&self) -> Vec2 {
|
||||||
@@ -66,14 +39,6 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
pub fn update<'a>(&mut self, root: impl Into<Option<&'a StrongWidget>>, rsc: &mut dyn UiRsc) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::Updates);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
let _update = diag::timer(TimerKind::Update);
|
|
||||||
self.invalid_sizes.clear();
|
|
||||||
self.invalid_sizes
|
|
||||||
.extend(rsc.widgets().needs_redraw.iter().copied());
|
|
||||||
self.resize_marks.clear();
|
|
||||||
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
// safety mechanism for memory leaks; might wanna return a result instead so user can
|
||||||
// decide whether to panic or not
|
// decide whether to panic or not
|
||||||
if !rsc.widgets().waiting.is_empty() {
|
if !rsc.widgets().waiting.is_empty() {
|
||||||
@@ -91,77 +56,20 @@ impl UiRenderState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let root = root.into();
|
let root = root.into();
|
||||||
if self.root_changed(root) {
|
if self.needs_full_redraw(root) {
|
||||||
self.redraw_all(root, rsc);
|
self.redraw_all(root, rsc);
|
||||||
self.old_root = root.map(|r| r.id());
|
self.old_root = root.map(|r| r.id());
|
||||||
} else if self.resized.iter().any(|&resized| resized) {
|
self.resized = false;
|
||||||
// A region is a fraction of the output plus an offset, resolved
|
} else if rsc.widgets().has_updates() {
|
||||||
// against the window in the shader, so a resize moves the whole
|
|
||||||
// drawing on its own. Only a widget that read pixels can be wrong.
|
|
||||||
{
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
let _marking = diag::timer(TimerKind::ResizeMarking);
|
|
||||||
let dependents: Vec<_> = self
|
|
||||||
.active
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(&id, active)| {
|
|
||||||
AXES.into_iter()
|
|
||||||
.zip(self.resized)
|
|
||||||
.any(|(axis, changed)| {
|
|
||||||
changed
|
|
||||||
&& active.reads_output[axis as usize]
|
|
||||||
&& pixel_len_changed(
|
|
||||||
active.output_px.axis(axis),
|
|
||||||
self.output_size.axis(axis),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.then_some(id)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
for id in dependents {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::ResizeDependents);
|
|
||||||
rsc.widgets_mut().needs_redraw.insert(id);
|
|
||||||
if let Some(top) = self.mark_readers(id, rsc) {
|
|
||||||
rsc.widgets_mut().needs_redraw.insert(top);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.resize_marks.extend(
|
|
||||||
rsc.widgets()
|
|
||||||
.needs_redraw
|
|
||||||
.iter()
|
|
||||||
.filter(|id| !self.invalid_sizes.contains(id))
|
|
||||||
.copied(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rsc.widgets().has_updates() {
|
|
||||||
self.redraw_updates(rsc);
|
self.redraw_updates(rsc);
|
||||||
}
|
}
|
||||||
self.resized = [false; 2];
|
|
||||||
self.invalid_sizes.clear();
|
|
||||||
self.resize_marks.clear();
|
|
||||||
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
|
||||||
if let Some(id) = root {
|
if let Some(id) = root {
|
||||||
self.draw_inner(
|
self.draw_inner(0, id.id(), UiRegion::FULL, None, MaskIdx::NONE, None, rsc);
|
||||||
0,
|
|
||||||
id.id(),
|
|
||||||
UiRegion::FULL,
|
|
||||||
None,
|
|
||||||
1,
|
|
||||||
MoveIdx::NONE,
|
|
||||||
false,
|
|
||||||
MaskIdx::NONE,
|
|
||||||
None,
|
|
||||||
rsc,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,22 +81,22 @@ 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>>,
|
old_children: Option<Vec<WidgetId>>,
|
||||||
rsc: &mut dyn UiRsc,
|
rsc: &mut dyn UiRsc,
|
||||||
) -> Size {
|
) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::DrawRequests);
|
|
||||||
diag::draw_request(id, parent, region, self.px_of(parent_move, region), slotted);
|
|
||||||
}
|
|
||||||
let mut old_children = old_children.unwrap_or_default();
|
let mut old_children = old_children.unwrap_or_default();
|
||||||
if self.active.contains_key(&id) {
|
if let Some(active) = self.active.get_mut(&id)
|
||||||
if let Some(size) = self.try_reuse(id, region, depth, parent_move, rsc) {
|
&& !rsc.widgets().needs_redraw.contains(&id)
|
||||||
return size;
|
{
|
||||||
|
// check to see if we can skip drawing first
|
||||||
|
if active.region == region {
|
||||||
|
return;
|
||||||
|
} else if active.region.size() == region.size() {
|
||||||
|
// TODO: epsilon?
|
||||||
|
let from = active.region;
|
||||||
|
self.mov(id, from, region);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// if not, then maintain resize and track old children to remove unneeded
|
// if not, then maintain resize and track old children to remove unneeded
|
||||||
let active = self.remove(id, false, rsc).unwrap();
|
let active = self.remove(id, false, rsc).unwrap();
|
||||||
@@ -196,93 +104,48 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// draw widget
|
// draw widget
|
||||||
let (move_idx, local) = match slotted {
|
|
||||||
// Its box becomes its slot's, so it draws in the slot's own
|
|
||||||
// coordinates and the box it was given is one entry to rewrite.
|
|
||||||
true => (self.move_slot(id, parent_move, region), UiRegion::FULL),
|
|
||||||
false => {
|
|
||||||
self.drop_slot(id);
|
|
||||||
(parent_move, region)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let px = self.px_of(move_idx, local);
|
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
|
||||||
self.draw_started.insert(id);
|
self.draw_started.insert(id);
|
||||||
|
|
||||||
let mut painter = Painter {
|
let mut painter = Painter {
|
||||||
state: self,
|
state: self,
|
||||||
region: local,
|
region,
|
||||||
mask,
|
mask,
|
||||||
layer,
|
layer,
|
||||||
id,
|
id,
|
||||||
textures: Vec::new(),
|
textures: Vec::new(),
|
||||||
primitives: Vec::new(),
|
primitives: Vec::new(),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
size_deps: Vec::new(),
|
|
||||||
depth,
|
|
||||||
size_box_inputs: [false; 2],
|
|
||||||
size_output_inputs: [false; 2],
|
|
||||||
reads_output: [false; 2],
|
|
||||||
move_idx,
|
|
||||||
rsc,
|
rsc,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::WidgetDraws);
|
|
||||||
diag::draw_widget(id, painter.rsc.widgets().label(id));
|
|
||||||
}
|
|
||||||
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
let mut widget = painter.rsc.widgets().get_dyn_dynamic(id);
|
||||||
let size = widget.draw(&mut painter);
|
widget.draw(&mut painter);
|
||||||
drop(widget);
|
drop(widget);
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::size_reported(id, size);
|
|
||||||
|
|
||||||
let Painter {
|
let Painter {
|
||||||
state: _,
|
state: _,
|
||||||
rsc: _,
|
rsc: _,
|
||||||
region: _,
|
region,
|
||||||
mask,
|
mask,
|
||||||
textures,
|
textures,
|
||||||
primitives,
|
primitives,
|
||||||
children,
|
children,
|
||||||
size_deps,
|
|
||||||
size_box_inputs,
|
|
||||||
size_output_inputs,
|
|
||||||
reads_output,
|
|
||||||
move_idx,
|
|
||||||
layer,
|
layer,
|
||||||
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,
|
|
||||||
parent,
|
parent,
|
||||||
depth,
|
|
||||||
textures,
|
textures,
|
||||||
primitives,
|
primitives,
|
||||||
children,
|
children,
|
||||||
size_deps,
|
|
||||||
size_box_inputs,
|
|
||||||
size_output_inputs,
|
|
||||||
output_px: self.output_size,
|
|
||||||
reads_output,
|
|
||||||
move_idx,
|
|
||||||
parent_move,
|
|
||||||
mask,
|
mask,
|
||||||
layer,
|
layer,
|
||||||
};
|
};
|
||||||
|
|
||||||
// remove old children that weren't kept
|
// remove old children that weren't kept
|
||||||
for c in &old_children {
|
for c in &old_children {
|
||||||
if !active.children.contains(c) {
|
if !active.children.contains(c) {
|
||||||
@@ -292,262 +155,20 @@ impl UiRenderState {
|
|||||||
|
|
||||||
rsc.on_draw(&active);
|
rsc.on_draw(&active);
|
||||||
self.active.insert(id, active);
|
self.active.insert(id, active);
|
||||||
self.invalid_sizes.remove(&id);
|
|
||||||
self.resize_marks.remove(&id);
|
|
||||||
size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The slot a widget's box is held in, made on its first placed draw and
|
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
|
||||||
/// kept until it stops being drawn -- a redraw replaces its `ActiveData`
|
|
||||||
/// while descendants go on naming the slot.
|
|
||||||
fn move_slot(&mut self, id: WidgetId, parent: MoveIdx, region: UiRegion) -> MoveIdx {
|
|
||||||
if let Some(&idx) = self.slots.get(&id) {
|
|
||||||
self.moves.set_parent(idx, parent);
|
|
||||||
self.moves.set(idx, region);
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
let idx = self.moves.push(parent, region);
|
|
||||||
self.slots.insert(id, idx);
|
|
||||||
idx
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gives up a slot a widget no longer needs, because it is drawn somewhere
|
|
||||||
/// that does not place it. Its descendants name it, so this is only
|
|
||||||
/// reached where they are about to be drawn again.
|
|
||||||
fn drop_slot(&mut self, id: WidgetId) {
|
|
||||||
if let Some(idx) = self.slots.remove(&id) {
|
|
||||||
self.moves.remove(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The pixel size of a region held in `slot`'s coordinates.
|
|
||||||
fn px_of(&self, slot: MoveIdx, region: UiRegion) -> Vec2 {
|
|
||||||
self.moves
|
|
||||||
.resolve(slot, region)
|
|
||||||
.size()
|
|
||||||
.to_abs(self.output_size)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A clean widget's retained size, when the offered pixel axes which
|
|
||||||
/// produced that answer are unchanged. This observes the old answer only;
|
|
||||||
/// it does not move or otherwise reuse the widget's drawing.
|
|
||||||
pub(super) fn retained_size(
|
|
||||||
&self,
|
|
||||||
id: WidgetId,
|
|
||||||
region: UiRegion,
|
|
||||||
parent_move: MoveIdx,
|
|
||||||
widgets: &Widgets,
|
|
||||||
) -> Option<(Size, [bool; 2], [bool; 2])> {
|
|
||||||
if self.size_is_invalid(id, widgets) || self.dirty_size_under(id, widgets) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let active = self.active.get(&id)?;
|
|
||||||
if active.parent_move != parent_move {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let px = self.px_of(parent_move, region);
|
|
||||||
let valid_box = AXES
|
|
||||||
.into_iter()
|
|
||||||
.zip(active.size_box_inputs)
|
|
||||||
.all(|(axis, depends)| {
|
|
||||||
!depends || !pixel_len_changed(active.px.axis(axis), px.axis(axis))
|
|
||||||
});
|
|
||||||
let valid_output =
|
|
||||||
AXES.into_iter()
|
|
||||||
.zip(active.size_output_inputs)
|
|
||||||
.all(|(axis, depends)| {
|
|
||||||
!depends
|
|
||||||
|| !pixel_len_changed(
|
|
||||||
active.output_px.axis(axis),
|
|
||||||
self.output_size.axis(axis),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
(valid_box && valid_output).then_some((
|
|
||||||
active.size,
|
|
||||||
active.size_box_inputs,
|
|
||||||
active.size_output_inputs,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn size_is_invalid(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
|
||||||
self.invalid_sizes.contains(&id)
|
|
||||||
|| (widgets.needs_redraw.contains(&id) && !self.resize_marks.contains(&id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dirty_size_under(&self, id: WidgetId, widgets: &Widgets) -> bool {
|
|
||||||
self.active.get(&id).is_some_and(|active| {
|
|
||||||
active.size_deps.iter().any(|child| {
|
|
||||||
self.size_is_invalid(*child, widgets) || self.dirty_size_under(*child, widgets)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The drawing a widget already has, kept for a new box if the box has not
|
|
||||||
/// changed in a way it depends on.
|
|
||||||
fn try_reuse(
|
|
||||||
&mut self,
|
|
||||||
id: WidgetId,
|
|
||||||
region: UiRegion,
|
|
||||||
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 box without drawing
|
|
||||||
// again: everything it drew is a fraction of its slot's box, so one
|
|
||||||
// entry says where all of it went.
|
|
||||||
if slot == parent_move {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::ReuseUnslotted);
|
|
||||||
diag::reuse(id, ReuseOutcome::Unslotted);
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if changed.iter().any(|&c| c) {
|
|
||||||
let widget = rsc.widgets().get_dyn(id)?;
|
|
||||||
let redraws = AXES
|
|
||||||
.into_iter()
|
|
||||||
.zip(changed)
|
|
||||||
.any(|(axis, c)| c && widget.on_resize(axis) != OnResize::Scale);
|
|
||||||
// Anything under it that has to be drawn again is drawn by drawing
|
|
||||||
// this, because whatever reads that widget's size sits in between
|
|
||||||
// and has to lay out around what it comes to.
|
|
||||||
if redraws {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::ReuseOwnResize);
|
|
||||||
diag::reuse(id, ReuseOutcome::OwnResize);
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if self.redraws_under(id, changed, rsc) {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
diag::bump(Counter::ReuseDescendantResize);
|
|
||||||
diag::reuse(id, ReuseOutcome::DescendantResize);
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.moves.set(slot, region);
|
|
||||||
self.keep_depth(id, depth);
|
|
||||||
let active = self.active.get_mut(&id).unwrap();
|
let active = self.active.get_mut(&id).unwrap();
|
||||||
active.region = region;
|
for h in &active.primitives {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
let region = self.layers[h.layer].region_mut(h);
|
||||||
{
|
*region = region.outside(&from).within(&to);
|
||||||
diag::bump(Counter::ReuseMoved);
|
}
|
||||||
diag::reuse(id, ReuseOutcome::Moved);
|
active.region = active.region.outside(&from).within(&to);
|
||||||
|
// SAFETY: children cannot be recursive
|
||||||
|
let children = unsafe { forget_ref(&active.children) };
|
||||||
|
for child in children {
|
||||||
|
self.mov(*child, from, to);
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -570,16 +191,13 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
|
fn remove_rec(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<ActiveData> {
|
||||||
|
self.cache.remove(id);
|
||||||
let inst = self.remove(id, true, rsc);
|
let inst = self.remove(id, true, rsc);
|
||||||
if let Some(inst) = &inst {
|
if let Some(inst) = &inst {
|
||||||
for c in &inst.children {
|
for c in &inst.children {
|
||||||
self.remove_rec(*c, rsc);
|
self.remove_rec(*c, rsc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// After the descendants, whose slots name this one as their parent.
|
|
||||||
if let Some(idx) = self.slots.remove(&id) {
|
|
||||||
self.moves.remove(idx);
|
|
||||||
}
|
|
||||||
inst
|
inst
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,85 +205,34 @@ impl UiRenderState {
|
|||||||
for (_, active) in self.active.drain() {
|
for (_, active) in self.active.drain() {
|
||||||
rsc.on_undraw(&active);
|
rsc.on_undraw(&active);
|
||||||
}
|
}
|
||||||
self.slots.clear();
|
self.cache.clear();
|
||||||
self.moves.clear();
|
|
||||||
self.layers.clear();
|
self.layers.clear();
|
||||||
self.invalid_sizes.clear();
|
|
||||||
self.resize_marks.clear();
|
|
||||||
self.draw_started.clear();
|
|
||||||
rsc.widgets_mut().needs_redraw.clear();
|
rsc.widgets_mut().needs_redraw.clear();
|
||||||
rsc.free();
|
rsc.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
|
pub fn redraw_updates(&mut self, rsc: &mut dyn UiRsc) {
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
while let Some(&id) = rsc.widgets().needs_redraw.iter().next() {
|
||||||
let _layout = diag::timer(TimerKind::IncrementalLayout);
|
|
||||||
// A reader's answer is only valid after every dirty size it reads has
|
|
||||||
// settled, and taking the deepest first is what arranges that --
|
|
||||||
// `try_reuse` hands back a retained size without asking whether
|
|
||||||
// anything dirty sits under it, so this order is load-bearing for the
|
|
||||||
// answer and not only for the cost. Equal-depth widgets are
|
|
||||||
// independent, so their order does not matter. Resize dirtiness already marks whole reader chains, so
|
|
||||||
// choosing their shallowest roots coalesces descendants that share a
|
|
||||||
// reader and gives each changing box its final constraints first.
|
|
||||||
while let Some(id) = {
|
|
||||||
let dirty = rsc.widgets().needs_redraw.iter().copied();
|
|
||||||
match self.resized.iter().any(|&resized| resized) {
|
|
||||||
true => dirty.min_by_key(|&id| self.depth(id)),
|
|
||||||
false => dirty.max_by_key(|&id| self.depth(id)),
|
|
||||||
}
|
|
||||||
} {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::QueuePops);
|
|
||||||
self.redraw(id, rsc);
|
self.redraw(id, rsc);
|
||||||
}
|
}
|
||||||
rsc.free();
|
rsc.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keeps a reused widget's depth current, since being reused is being
|
|
||||||
/// visited: only a subtree nobody looked at can hold a stale one.
|
|
||||||
fn keep_depth(&mut self, id: WidgetId, depth: usize) {
|
|
||||||
if let Some(active) = self.active.get_mut(&id) {
|
|
||||||
active.depth = depth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn depth(&self, id: WidgetId) -> usize {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::DepthReads);
|
|
||||||
let depth = self.active.get(&id).map_or(1, |active| active.depth);
|
|
||||||
debug_assert_eq!(
|
|
||||||
depth,
|
|
||||||
self.walked_depth(id),
|
|
||||||
"a widget's kept depth is not the one its ancestry says"
|
|
||||||
);
|
|
||||||
depth
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What the kept depth is checked against, and the only thing that reads
|
|
||||||
/// the ancestry to find one.
|
|
||||||
fn walked_depth(&self, id: WidgetId) -> usize {
|
|
||||||
let mut depth = 0;
|
|
||||||
let mut at = Some(id);
|
|
||||||
while let Some(id) = at {
|
|
||||||
at = self.active.get(&id).and_then(|active| active.parent);
|
|
||||||
depth += 1;
|
|
||||||
}
|
|
||||||
depth
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
|
pub fn root_changed<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
|
||||||
root.into().map(|r| r.id()) != self.old_root
|
root.into().map(|r| r.id()) != self.old_root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scheduling and drawing must use the same full-redraw predicate.
|
||||||
|
fn needs_full_redraw<'a>(&self, root: impl Into<Option<&'a StrongWidget>>) -> bool {
|
||||||
|
self.root_changed(root) || self.resized
|
||||||
|
}
|
||||||
|
|
||||||
pub fn needs_redraw<'a>(
|
pub fn needs_redraw<'a>(
|
||||||
&self,
|
&self,
|
||||||
root: impl Into<Option<&'a StrongWidget>>,
|
root: impl Into<Option<&'a StrongWidget>>,
|
||||||
widgets: &Widgets,
|
widgets: &Widgets,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
self.root_changed(root)
|
self.needs_full_redraw(root) || widgets.has_updates()
|
||||||
|| self.resized.iter().any(|&resized| resized)
|
|
||||||
|| widgets.has_updates()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn active_widgets(&self) -> usize {
|
pub fn active_widgets(&self) -> usize {
|
||||||
@@ -691,40 +258,29 @@ impl UiRenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where a widget is on screen: its box composed through the boxes it
|
|
||||||
/// sits within, which is the walk the vertex shader does.
|
|
||||||
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
|
||||||
let active = self.active.get(&id.id())?;
|
let region = self.active.get(&id.id())?.region;
|
||||||
let region = self.moves.resolve(active.parent_move, active.region);
|
|
||||||
Some(region.to_px(self.output_size))
|
Some(region.to_px(self.output_size))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// redraws a widget that's currently active (drawn)
|
/// redraws a widget that's currently active (drawn)
|
||||||
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
|
||||||
self.draw_started.remove(&id);
|
|
||||||
if rsc.widgets().needs_redraw.contains(&id) && !self.resize_marks.contains(&id) {
|
|
||||||
self.invalid_sizes.insert(id);
|
|
||||||
}
|
|
||||||
// A widget can only answer whether its size changed by drawing in the
|
|
||||||
// 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. Otherwise the widget can draw locally, and its readers
|
|
||||||
// only matter if the returned size actually changed.
|
|
||||||
let box_changed = self.active.get(&id).is_some_and(|active| {
|
|
||||||
let px = self.px_of(active.parent_move, active.region);
|
|
||||||
AXES.into_iter()
|
|
||||||
.any(|axis| pixel_len_changed(active.px.axis(axis), px.axis(axis)))
|
|
||||||
});
|
|
||||||
if (self.resized.iter().any(|&resized| resized) || box_changed)
|
|
||||||
&& let Some(top) = self.mark_readers(id, rsc)
|
|
||||||
{
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::EagerReaderRedraws);
|
|
||||||
self.redraw(top, rsc);
|
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rsc.widgets_mut().needs_redraw.remove(&id);
|
rsc.widgets_mut().needs_redraw.remove(&id);
|
||||||
|
self.draw_started.remove(&id);
|
||||||
|
// check if parent depends on the desired size of this, if so then redraw it first
|
||||||
|
for axis in [Axis::X, Axis::Y] {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.draw_started.contains(&id) {
|
if self.draw_started.contains(&id) {
|
||||||
return;
|
return;
|
||||||
@@ -733,59 +289,34 @@ impl UiRenderState {
|
|||||||
let Some(active) = self.remove(id, false, rsc) else {
|
let Some(active) = self.remove(id, false, rsc) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::LocalRedraws);
|
|
||||||
|
|
||||||
let old_size = active.size;
|
self.draw_inner(
|
||||||
let size = self.draw_inner(
|
|
||||||
active.layer,
|
active.layer,
|
||||||
id,
|
id,
|
||||||
active.region,
|
active.region,
|
||||||
active.parent,
|
active.parent,
|
||||||
active.depth,
|
|
||||||
active.parent_move,
|
|
||||||
active.move_idx != active.parent_move,
|
|
||||||
active.mask,
|
active.mask,
|
||||||
Some(active.children),
|
Some(active.children),
|
||||||
rsc,
|
rsc,
|
||||||
);
|
);
|
||||||
|
|
||||||
if size != old_size {
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::SizeChanges);
|
|
||||||
if let Some(parent) = self.active.get(&id).and_then(|active| active.parent)
|
|
||||||
&& self
|
|
||||||
.active
|
|
||||||
.get(&parent)
|
|
||||||
.is_some_and(|active| active.size_deps.contains(&id))
|
|
||||||
{
|
|
||||||
// Propagate one dependency edge at a time. If drawing the reader
|
|
||||||
// does not change its own size, nothing above it can observe this.
|
|
||||||
rsc.widgets_mut().needs_redraw.insert(parent);
|
|
||||||
self.invalid_sizes.insert(parent);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
diag::bump(Counter::ReaderEdges);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The furthest ancestor that read this widget's size, directly or through
|
pub(super) fn size_ctx<'b>(
|
||||||
/// widgets that did the same, marking everything below it on the way.
|
&'b mut self,
|
||||||
fn mark_readers(&self, id: WidgetId, rsc: &mut dyn UiRsc) -> Option<WidgetId> {
|
source: WidgetId,
|
||||||
let mut top = None;
|
outer: UiVec2,
|
||||||
let mut at = id;
|
rsc: &'b mut dyn UiRsc,
|
||||||
while let Some(active) = self.active.get(&at)
|
) -> SizeCtx<'b> {
|
||||||
&& let Some(parent) = active.parent
|
let ui = rsc.ui_mut();
|
||||||
&& self
|
SizeCtx {
|
||||||
.active
|
source,
|
||||||
.get(&parent)
|
cache: &mut self.cache,
|
||||||
.is_some_and(|p| p.size_deps.contains(&at))
|
text: &mut ui.text,
|
||||||
{
|
widgets: &ui.widgets,
|
||||||
rsc.widgets_mut().needs_redraw.insert(at);
|
outer,
|
||||||
top = Some(parent);
|
output_size: self.output_size,
|
||||||
at = parent;
|
id: source,
|
||||||
}
|
}
|
||||||
top
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
use crate::{
|
||||||
|
Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2,
|
||||||
|
WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct SizeCtx<'a> {
|
||||||
|
pub text: &'a mut TextData,
|
||||||
|
pub(super) source: WidgetId,
|
||||||
|
pub(super) widgets: &'a Widgets,
|
||||||
|
pub(super) cache: &'a mut Cache,
|
||||||
|
/// TODO: should this be pub? rn used for sized
|
||||||
|
pub outer: UiVec2,
|
||||||
|
pub(super) output_size: Vec2,
|
||||||
|
pub(super) id: WidgetId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeCtx<'_> {
|
||||||
|
pub fn id(&self) -> &WidgetId {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn source(&self) -> &WidgetId {
|
||||||
|
&self.source
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn len_inner<A: const AxisT>(&mut self, id: WidgetId) -> Len {
|
||||||
|
if let Some((_, len)) = self.cache.size.axis::<A>().get(&id) {
|
||||||
|
return *len;
|
||||||
|
}
|
||||||
|
let len = self
|
||||||
|
.widgets
|
||||||
|
.get_dyn_dynamic(id)
|
||||||
|
.desired_len::<A>(&mut SizeCtx {
|
||||||
|
text: self.text,
|
||||||
|
source: self.source,
|
||||||
|
widgets: self.widgets,
|
||||||
|
cache: self.cache,
|
||||||
|
outer: self.outer,
|
||||||
|
output_size: self.output_size,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
self.cache.size.axis::<A>().insert(id, (self.outer, len));
|
||||||
|
len
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(&mut self, id: impl IdLike) -> Len {
|
||||||
|
self.len_inner::<XAxis>(id.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(&mut self, id: impl IdLike) -> Len {
|
||||||
|
self.len_inner::<YAxis>(id.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len_axis(&mut self, id: impl IdLike, axis: Axis) -> Len {
|
||||||
|
match axis {
|
||||||
|
Axis::X => self.width(id),
|
||||||
|
Axis::Y => self.height(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&mut self, id: impl IdLike) -> Size {
|
||||||
|
let id = id.id();
|
||||||
|
Size {
|
||||||
|
x: self.width(id),
|
||||||
|
y: self.height(id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn px_size(&mut self) -> Vec2 {
|
||||||
|
self.outer.to_abs(self.output_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_size(&mut self) -> Vec2 {
|
||||||
|
self.output_size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_text(
|
||||||
|
&mut self,
|
||||||
|
buffer: &mut TextBuffer,
|
||||||
|
attrs: &TextAttrs,
|
||||||
|
width: Option<f32>,
|
||||||
|
) -> RenderedText {
|
||||||
|
self.text.render(buffer, attrs, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(&self, id: WidgetId) -> &String {
|
||||||
|
self.widgets.label(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,10 +34,6 @@ impl<T, I: IdNum> Arena<T, I> {
|
|||||||
self.tracker.free(id);
|
self.tracker.free(id);
|
||||||
self.data[i]
|
self.data[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
|
||||||
&mut self.data[id.idx()]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, I: IdNum> Default for Arena<T, I> {
|
impl<T, I: IdNum> Default for Arena<T, I> {
|
||||||
|
|||||||
+22
-1
@@ -1,13 +1,34 @@
|
|||||||
|
use std::ops::*;
|
||||||
|
|
||||||
pub const trait LerpUtil {
|
pub const trait LerpUtil {
|
||||||
fn lerp(self, from: Self, to: Self) -> Self;
|
fn lerp(self, from: Self, to: Self) -> Self;
|
||||||
|
fn lerp_inv(self, from: Self, to: Self) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
const impl LerpUtil for f32 {
|
pub const trait DivOr {
|
||||||
|
fn div_or(self, rhs: Self, other: Self) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl DivOr for f32 {
|
||||||
|
fn div_or(self, rhs: Self, other: Self) -> Self {
|
||||||
|
let res = self / rhs;
|
||||||
|
if res.is_nan() { other } else { res }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<
|
||||||
|
T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy,
|
||||||
|
> LerpUtil for T
|
||||||
|
{
|
||||||
/// linear interpolation
|
/// linear interpolation
|
||||||
/// from * (1.0 - self) + to * self
|
/// from * (1.0 - self) + to * self
|
||||||
fn lerp(self, from: Self, to: Self) -> Self {
|
fn lerp(self, from: Self, to: Self) -> Self {
|
||||||
from + (to - from) * self
|
from + (to - from) * self
|
||||||
}
|
}
|
||||||
|
/// inverse of lerp
|
||||||
|
fn lerp_inv(self, from: Self, to: Self) -> Self {
|
||||||
|
(self - from).div_or(to - from, from)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! impl_op {
|
macro_rules! impl_op {
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
#[allow(clippy::missing_safety_doc)]
|
||||||
|
pub(crate) unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
|
||||||
|
unsafe { std::mem::transmute::<&T, &T>(x) }
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::missing_safety_doc)]
|
#[allow(clippy::missing_safety_doc)]
|
||||||
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
|
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
|
||||||
unsafe { std::mem::transmute::<&mut T, &mut T>(x) }
|
unsafe { std::mem::transmute::<&mut T, &mut T>(x) }
|
||||||
|
|||||||
+11
-6
@@ -1,11 +1,7 @@
|
|||||||
use crate::util::impl_op;
|
use crate::util::{DivOr, impl_op};
|
||||||
use std::{hash::Hash, ops::*};
|
use std::{hash::Hash, ops::*};
|
||||||
|
|
||||||
/// `align(8)` because that is WGSL's alignment for a `vec2<f32>`, so any GPU
|
#[repr(C)]
|
||||||
/// struct holding one is laid out the way its shader reads it without having
|
|
||||||
/// to say so itself. Those structs still need a manual `unsafe impl Pod`,
|
|
||||||
/// since the trailing padding this introduces is what `derive(Pod)` refuses.
|
|
||||||
#[repr(C, align(8))]
|
|
||||||
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct Vec2 {
|
pub struct Vec2 {
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
@@ -71,6 +67,15 @@ impl_op!(Vec2 Sub sub; x y);
|
|||||||
impl_op!(Vec2 Mul mul; x y);
|
impl_op!(Vec2 Mul mul; x y);
|
||||||
impl_op!(Vec2 Div div; x y);
|
impl_op!(Vec2 Div div; x y);
|
||||||
|
|
||||||
|
const impl DivOr for Vec2 {
|
||||||
|
fn div_or(self, rhs: Self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x.div_or(rhs.x, other.x),
|
||||||
|
y: self.y.div_or(rhs.y, other.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Neg for Vec2 {
|
impl Neg for Vec2 {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
|
|||||||
+19
-35
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Axis, Len, Painter, Size};
|
use crate::{Axis, AxisT, Len, Painter, SizeCtx};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
mod data;
|
mod data;
|
||||||
@@ -15,48 +15,32 @@ pub use tag::*;
|
|||||||
pub use view::*;
|
pub use view::*;
|
||||||
pub use widgets::*;
|
pub use widgets::*;
|
||||||
|
|
||||||
/// What may be done to a widget's drawing when the box it was given changes
|
pub trait Widget: Any {
|
||||||
/// on this axis, instead of drawing it again. Asked per axis, because wrapped
|
fn draw(&mut self, painter: &mut Painter);
|
||||||
/// text reads the width it is offered and not the height.
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||||
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 WidgetAxisFns {
|
||||||
/// Draws the widget, and returns what it used of the box it was given.
|
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len;
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size;
|
}
|
||||||
|
|
||||||
/// An exact length the widget can give without a painter or its children.
|
impl<W: Widget + ?Sized> WidgetAxisFns for W {
|
||||||
/// Optional, and saves a draw rather than changing one: a hint that
|
fn desired_len<A: AxisT>(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
/// disagrees with the eventual draw fails a debug assertion.
|
match A::get() {
|
||||||
fn size_hint(&self, _axis: Axis) -> Option<Len> {
|
Axis::X => self.desired_width(ctx),
|
||||||
None
|
Axis::Y => self.desired_height(ctx),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _axis: Axis) -> OnResize {
|
|
||||||
OnResize::default()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for () {
|
impl Widget for () {
|
||||||
/// A gap: nothing drawn, at the default length, so a span gives it a share.
|
fn draw(&mut self, _: &mut Painter) {}
|
||||||
fn draw(&mut self, _: &mut Painter) -> Size {
|
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||||
Size::default()
|
Len::ZERO
|
||||||
}
|
}
|
||||||
|
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
||||||
fn size_hint(&self, _axis: Axis) -> Option<Len> {
|
Len::ZERO
|
||||||
Some(Len::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_resize(&self, _axis: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
//! The seeded random tree `tests/generated.rs` checks, drawn so it can be
|
|
||||||
//! looked at. `IRIS_SEED` and `IRIS_DEPTH` choose which one.
|
|
||||||
|
|
||||||
use iris::prelude::*;
|
|
||||||
use iris::random::Edits;
|
|
||||||
|
|
||||||
fn env(name: &str, fallback: u64) -> u64 {
|
|
||||||
std::env::var(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.parse().ok())
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
DefaultApp::<State>::run();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(DefaultUiState)]
|
|
||||||
struct State {
|
|
||||||
ui_state: DefaultUiState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DefaultAppState for State {
|
|
||||||
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
|
||||||
let seed = env("IRIS_SEED", 1);
|
|
||||||
let depth = env("IRIS_DEPTH", 4) as usize;
|
|
||||||
let (root, _) = iris::random::grow(rsc, seed, depth, &Edits::default());
|
|
||||||
ui_state.set_root(root);
|
|
||||||
Self { ui_state }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
//! Text sizing: wrapped text reads the width it is offered, fixed text does
|
|
||||||
//! not, and both report a height their container lays out around.
|
|
||||||
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
DefaultApp::<State>::run();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(DefaultUiState)]
|
|
||||||
struct State {
|
|
||||||
ui_state: DefaultUiState,
|
|
||||||
}
|
|
||||||
|
|
||||||
const SAMPLE: &str = "Wrapping shapes one source into as many lines as its container \
|
|
||||||
leaves room for, so the height of a paragraph is an answer rather than a setting, and \
|
|
||||||
the same words in a narrower box come back taller. Resize the window and watch the \
|
|
||||||
text below reflow into a different number of lines while nothing about it changes.";
|
|
||||||
|
|
||||||
impl DefaultAppState for State {
|
|
||||||
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
|
||||||
let panel = || rect(Color::WHITE.darker(0.85));
|
|
||||||
|
|
||||||
let wrapped = wtext(SAMPLE)
|
|
||||||
.size(28)
|
|
||||||
.wrap(true)
|
|
||||||
.text_align(Align::LEFT)
|
|
||||||
.pad(16)
|
|
||||||
.background(panel());
|
|
||||||
|
|
||||||
let aligned = (
|
|
||||||
wtext("left").size(24).text_align(Align::LEFT),
|
|
||||||
wtext("centred").size(24).text_align(Align::CENTER),
|
|
||||||
wtext("right").size(24).text_align(Align::RIGHT),
|
|
||||||
)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.gap(8)
|
|
||||||
.pad(16)
|
|
||||||
.background(panel());
|
|
||||||
|
|
||||||
// The same words in half the width, which is a different number of
|
|
||||||
// lines and so a different height. A declared width only holds along
|
|
||||||
// a span's own axis, hence the row.
|
|
||||||
let narrow = (
|
|
||||||
wtext(SAMPLE)
|
|
||||||
.size(20)
|
|
||||||
.wrap(true)
|
|
||||||
.pad(16)
|
|
||||||
.background(panel())
|
|
||||||
.align(Align::TOP)
|
|
||||||
.width(rel(0.5)),
|
|
||||||
rect(Color::WHITE.darker(0.95)),
|
|
||||||
)
|
|
||||||
.span(Dir::RIGHT);
|
|
||||||
|
|
||||||
(wrapped, aligned, narrow)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.gap(12)
|
|
||||||
.pad(12)
|
|
||||||
.set_root(rsc, &mut ui_state);
|
|
||||||
Self { ui_state }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rig-input"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
|
|
||||||
# Replays `.touch` recordings through Wayland's virtual-pointer protocol;
|
|
||||||
# headless sway has no input devices for coordinate-driving tools to move.
|
|
||||||
[[bin]]
|
|
||||||
name = "replay-touch"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
# Share the harness parser so both ways of replaying read a file the same.
|
|
||||||
iris = { path = ".." }
|
|
||||||
wayland-client = { workspace = true }
|
|
||||||
wayland-protocols-wlr = { workspace = true }
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
use iris::harness::{TouchAction, TouchScript};
|
|
||||||
use std::time::Duration;
|
|
||||||
use wayland_client::protocol::wl_pointer::ButtonState;
|
|
||||||
use wayland_client::protocol::{wl_registry, wl_seat};
|
|
||||||
use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
|
|
||||||
use wayland_protocols_wlr::virtual_pointer::v1::client::{
|
|
||||||
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
|
|
||||||
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const BTN_LEFT: u32 = 0x110;
|
|
||||||
|
|
||||||
const SETTLE: Duration = Duration::from_millis(200);
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct Globals {
|
|
||||||
seat: Option<wl_seat::WlSeat>,
|
|
||||||
manager: Option<ZwlrVirtualPointerManagerV1>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
|
|
||||||
fn event(
|
|
||||||
state: &mut Self,
|
|
||||||
registry: &wl_registry::WlRegistry,
|
|
||||||
event: wl_registry::Event,
|
|
||||||
_: &(),
|
|
||||||
_: &Connection,
|
|
||||||
qh: &QueueHandle<Self>,
|
|
||||||
) {
|
|
||||||
let wl_registry::Event::Global {
|
|
||||||
name,
|
|
||||||
interface,
|
|
||||||
version,
|
|
||||||
} = event
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
match interface.as_str() {
|
|
||||||
"wl_seat" => {
|
|
||||||
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
|
|
||||||
}
|
|
||||||
"zwlr_virtual_pointer_manager_v1" => {
|
|
||||||
state.manager = Some(registry.bind(name, version.min(2), qh, ()));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate_noop!(Globals: ignore wl_seat::WlSeat);
|
|
||||||
delegate_noop!(Globals: ZwlrVirtualPointerManagerV1);
|
|
||||||
delegate_noop!(Globals: ZwlrVirtualPointerV1);
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
||||||
let [width, height, path] = args.as_slice() else {
|
|
||||||
eprintln!("usage: replay-touch WIDTH HEIGHT FILE");
|
|
||||||
std::process::exit(2);
|
|
||||||
};
|
|
||||||
let (width, height) = (parse(width, "WIDTH"), parse(height, "HEIGHT"));
|
|
||||||
let text = std::fs::read_to_string(path)
|
|
||||||
.unwrap_or_else(|e| fail(&format!("could not read {path}: {e}")));
|
|
||||||
let script = TouchScript::parse(&text).unwrap_or_else(|e| fail(&e));
|
|
||||||
|
|
||||||
let conn = Connection::connect_to_env().unwrap_or_else(|e| {
|
|
||||||
fail(&format!(
|
|
||||||
"no wayland display ({e}); is WAYLAND_DISPLAY set?"
|
|
||||||
))
|
|
||||||
});
|
|
||||||
let mut queue = conn.new_event_queue();
|
|
||||||
let qh = queue.handle();
|
|
||||||
let display = conn.display();
|
|
||||||
display.get_registry(&qh, ());
|
|
||||||
let mut globals = Globals::default();
|
|
||||||
queue
|
|
||||||
.roundtrip(&mut globals)
|
|
||||||
.unwrap_or_else(|e| fail(&format!("wayland roundtrip failed: {e}")));
|
|
||||||
|
|
||||||
let manager = globals.manager.as_ref().unwrap_or_else(|| {
|
|
||||||
fail(
|
|
||||||
"this compositor does not offer zwlr_virtual_pointer_manager_v1, so a pointer cannot \
|
|
||||||
be synthesised; sway and every wlroots compositor do",
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let pointer = manager.create_virtual_pointer(globals.seat.as_ref(), &qh, ());
|
|
||||||
|
|
||||||
// Put the pointer where the gesture starts and let the compositor
|
|
||||||
// settle before anything is pressed. Without this the press is
|
|
||||||
// dropped: sway has just learned about this pointer, and a button
|
|
||||||
// sent in the same breath as the motion that first puts it over a
|
|
||||||
// window arrives before there is a focused surface to send it to --
|
|
||||||
// winit sees `CursorEntered`, the moves and the *release*, never the
|
|
||||||
// press, so the gesture reads as a hover and nothing scrolls. Found
|
|
||||||
// by printing winit's own events; the settle is what fixed it.
|
|
||||||
if let Some(first) = script.samples.first() {
|
|
||||||
pointer.motion_absolute(0, first.pos.x as u32, first.pos.y as u32, width, height);
|
|
||||||
pointer.frame();
|
|
||||||
conn.flush()
|
|
||||||
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
|
|
||||||
std::thread::sleep(SETTLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut previous = 0;
|
|
||||||
for sample in &script.samples {
|
|
||||||
std::thread::sleep(Duration::from_millis(sample.t_ms - previous));
|
|
||||||
previous = sample.t_ms;
|
|
||||||
let t = sample.t_ms as u32;
|
|
||||||
pointer.motion_absolute(t, sample.pos.x as u32, sample.pos.y as u32, width, height);
|
|
||||||
pointer.frame();
|
|
||||||
// The button goes in a frame of its own, *after* the motion has
|
|
||||||
// been committed. Sent in the same frame as the motion that
|
|
||||||
// first puts the pointer over the window, sway drops it: the
|
|
||||||
// client sees `CursorEntered` and the moves but never a
|
|
||||||
// `MouseInput { state: Pressed }`, so the whole gesture reads as
|
|
||||||
// a hover and nothing scrolls. Found exactly that way, by
|
|
||||||
// printing winit's events.
|
|
||||||
let state = match sample.action {
|
|
||||||
TouchAction::Down => Some(ButtonState::Pressed),
|
|
||||||
TouchAction::Up => Some(ButtonState::Released),
|
|
||||||
TouchAction::Move => None,
|
|
||||||
};
|
|
||||||
if let Some(state) = state {
|
|
||||||
pointer.button(t, BTN_LEFT, state);
|
|
||||||
pointer.frame();
|
|
||||||
}
|
|
||||||
conn.flush()
|
|
||||||
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
|
|
||||||
}
|
|
||||||
pointer.destroy();
|
|
||||||
conn.flush().ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse(text: &str, what: &str) -> u32 {
|
|
||||||
text.parse()
|
|
||||||
.unwrap_or_else(|_| fail(&format!("{what} is not a whole number: {text:?}")))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fail(message: &str) -> ! {
|
|
||||||
eprintln!("replay-touch: {message}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# The compositor `scripts/run-headless.sh` starts, so that an example has a
|
|
||||||
# surface where there is no display. Nothing here is meant to be looked at
|
|
||||||
# directly; `grim` is.
|
|
||||||
#
|
|
||||||
# No Xwayland: winit talks Wayland natively, so an X server is a second thing
|
|
||||||
# to go wrong for no gain.
|
|
||||||
xwayland disable
|
|
||||||
|
|
||||||
# The default output, overridden per run by `--mode`. Larger than the window
|
|
||||||
# an example opens, so nothing is scaled or clipped.
|
|
||||||
output HEADLESS-1 mode 1920x1200@60Hz
|
|
||||||
|
|
||||||
default_border none
|
|
||||||
focus_follows_mouse no
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Run an iris example on a machine with no display.
|
|
||||||
#
|
|
||||||
# ./scripts/run-headless.sh tabs
|
|
||||||
# ./scripts/run-headless.sh tabs --shot /tmp/tabs.png --seconds 4
|
|
||||||
# ./scripts/run-headless.sh tabs --replay taps.touch --shot /tmp/tabs.png
|
|
||||||
# ./scripts/run-headless.sh app --dir ../elsewhere --mode 1080x2424@120Hz
|
|
||||||
#
|
|
||||||
# `--dir DIR` names the workspace to build in, defaulting to this one, so a
|
|
||||||
# project that depends on iris can be run through the same rig. `--bin` runs a
|
|
||||||
# crate binary rather than an example, and takes its own argv from
|
|
||||||
# `$RUN_HEADLESS_ARGS`, word-split on purpose.
|
|
||||||
#
|
|
||||||
# `--mode` sets the output, for running something at a size other than a
|
|
||||||
# desktop's -- a phone's, say. Set every run rather than only when it changes:
|
|
||||||
# the compositor is reused between runs, so a default-shaped run after a
|
|
||||||
# custom one would otherwise inherit the other's output and quietly screenshot
|
|
||||||
# the wrong size.
|
|
||||||
#
|
|
||||||
# `--resize WxH@Hz` changes the output under the app once it is up, then
|
|
||||||
# screenshots. A resize is its own case: what it has to match is a cold start
|
|
||||||
# at that size, byte for byte, and nothing in `cargo test` can see it.
|
|
||||||
#
|
|
||||||
# `--replay FILE` drives a `.touch` recording into the window through
|
|
||||||
# `replay-touch`, which reads it with the same parser `iris::harness` uses. A
|
|
||||||
# recording is `<ms> down|move|up <x> <y>` in the output's own pixels. With
|
|
||||||
# `--shot` it also writes `<shot>-before.png` from just before the gesture,
|
|
||||||
# since "it moved" is a claim about two pictures.
|
|
||||||
#
|
|
||||||
# What it supplies is a compositor for winit to open a surface on: a headless
|
|
||||||
# sway, and `grim` to screenshot it. Sway gets its own socket and runtime
|
|
||||||
# directory rather than joining whatever else is running, because it tiles --
|
|
||||||
# adding a window to someone else's compositor resizes theirs.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
need() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || {
|
|
||||||
echo "run-headless: $1 is not installed ($2)" >&2
|
|
||||||
exit 127
|
|
||||||
}
|
|
||||||
}
|
|
||||||
need sway "the compositor an example opens its window on"
|
|
||||||
need swaymsg "sway's control socket"
|
|
||||||
|
|
||||||
scripts=$(cd "$(dirname "$0")" && pwd)
|
|
||||||
root=$(cd "$scripts/.." && pwd)
|
|
||||||
workdir="$root"
|
|
||||||
cd "$root"
|
|
||||||
run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless"
|
|
||||||
seconds=3
|
|
||||||
shot=""
|
|
||||||
replay=""
|
|
||||||
resize=""
|
|
||||||
example=""
|
|
||||||
kind=example
|
|
||||||
mode=1920x1200@60Hz
|
|
||||||
|
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--shot) shot=$2; shift 2 ;;
|
|
||||||
--seconds) seconds=$2; shift 2 ;;
|
|
||||||
--bin) kind=bin; shift ;;
|
|
||||||
--mode) mode=$2; shift 2 ;;
|
|
||||||
--resize) resize=$2; shift 2 ;;
|
|
||||||
--replay) replay=$2; shift 2 ;;
|
|
||||||
--dir) workdir=$(cd "$2" && pwd); shift 2 ;;
|
|
||||||
--) shift; break ;;
|
|
||||||
*) example=$1; shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
[ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--dir DIR] [--mode WxH@Hz] [--resize WxH@Hz] [--replay TOUCH] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; }
|
|
||||||
[ -z "$replay" ] || [ -f "$replay" ] || { echo "run-headless: no touch script at $replay" >&2; exit 2; }
|
|
||||||
[ -z "$shot" ] || need grim "the screenshot --shot writes"
|
|
||||||
|
|
||||||
mkdir -p "$run"
|
|
||||||
export SWAYSOCK="$run/sway.sock"
|
|
||||||
|
|
||||||
# Named rather than left to sway's pid-based default, so a second run reuses
|
|
||||||
# this compositor instead of starting another beside it.
|
|
||||||
if ! swaymsg -t get_version >/dev/null 2>&1; then
|
|
||||||
rm -f "$SWAYSOCK"
|
|
||||||
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 LIBSEAT_BACKEND=noop \
|
|
||||||
setsid sway -c "$scripts/headless.conf" >"$run/sway.log" 2>&1 &
|
|
||||||
i=0
|
|
||||||
while [ $i -lt 20 ]; do
|
|
||||||
swaymsg -t get_version >/dev/null 2>&1 && break
|
|
||||||
i=$((i + 1)); sleep 0.5
|
|
||||||
done
|
|
||||||
swaymsg -t get_version >/dev/null 2>&1 || {
|
|
||||||
echo "run-headless: compositor did not start; see $run/sway.log" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$run/display"
|
|
||||||
swaymsg exec -- "sh -c 'printf %s \"\$WAYLAND_DISPLAY\" > $run/display'" >/dev/null
|
|
||||||
i=0
|
|
||||||
while [ $i -lt 20 ]; do
|
|
||||||
[ -s "$run/display" ] && break
|
|
||||||
i=$((i + 1)); sleep 0.5
|
|
||||||
done
|
|
||||||
[ -s "$run/display" ] || { echo "run-headless: could not read WAYLAND_DISPLAY" >&2; exit 1; }
|
|
||||||
WAYLAND_DISPLAY=$(cat "$run/display")
|
|
||||||
export WAYLAND_DISPLAY
|
|
||||||
|
|
||||||
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
|
|
||||||
|
|
||||||
swaymsg output HEADLESS-1 mode "$mode" >/dev/null
|
|
||||||
# The extent `replay-touch` positions against, so a script's coordinates
|
|
||||||
# are the output's own pixels.
|
|
||||||
out_w=${mode%x*}
|
|
||||||
out_h=${mode#*x}; out_h=${out_h%@*}
|
|
||||||
|
|
||||||
# Built before the app starts, so a compile error is not reported as a
|
|
||||||
# window that failed to move.
|
|
||||||
[ -z "$replay" ] || (cd "$root" && cargo build --bin replay-touch -p rig-input) >&2
|
|
||||||
|
|
||||||
cd "$workdir"
|
|
||||||
if [ "$kind" = bin ]; then
|
|
||||||
cargo build --bin "$example" "$@" >&2
|
|
||||||
bin="$workdir/target/debug/$example"
|
|
||||||
else
|
|
||||||
cargo build --example "$example" "$@" >&2
|
|
||||||
bin="$workdir/target/debug/examples/$example"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Deliberately word-split: this is the binary's own argv, not a single path.
|
|
||||||
# shellcheck disable=SC2086
|
|
||||||
"$bin" ${RUN_HEADLESS_ARGS:-} >"$run/$example.log" 2>&1 &
|
|
||||||
pid=$!
|
|
||||||
trap 'kill "$pid" 2>/dev/null || true' EXIT INT TERM
|
|
||||||
|
|
||||||
# Wait for the window to be mapped rather than for a number of seconds. A
|
|
||||||
# fixed sleep took an all-black screenshot the first time this ran, when sway
|
|
||||||
# had started in the same invocation and had not composited its output yet --
|
|
||||||
# which is indistinguishable from an app that draws nothing.
|
|
||||||
i=0
|
|
||||||
while [ $i -lt 40 ]; do
|
|
||||||
kill -0 "$pid" 2>/dev/null || break
|
|
||||||
swaymsg -t get_tree --raw 2>/dev/null | grep -q "\"pid\":$pid," && break
|
|
||||||
i=$((i + 1)); sleep 0.25
|
|
||||||
done
|
|
||||||
|
|
||||||
i=0
|
|
||||||
while [ $i -lt "$((seconds * 2))" ]; do
|
|
||||||
kill -0 "$pid" 2>/dev/null || break
|
|
||||||
i=$((i + 1)); sleep 0.5
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then
|
|
||||||
swaymsg output HEADLESS-1 mode "$resize" >/dev/null
|
|
||||||
echo "run-headless: resized to $resize" >&2
|
|
||||||
sleep 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$replay" ] && kill -0 "$pid" 2>/dev/null; then
|
|
||||||
if [ -n "$shot" ]; then
|
|
||||||
grim "${shot%.png}-before.png"
|
|
||||||
echo "run-headless: wrote ${shot%.png}-before.png (before the gesture)" >&2
|
|
||||||
fi
|
|
||||||
"$root/target/debug/replay-touch" "$out_w" "$out_h" "$replay"
|
|
||||||
# A fling outlives the finger: the gesture's own last sample is not
|
|
||||||
# when the list stops. Long enough for Android's spline to settle
|
|
||||||
# (`FlingCalculator::duration` tops out around a second and a half).
|
|
||||||
sleep 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if kill -0 "$pid" 2>/dev/null; then
|
|
||||||
[ -n "$shot" ] && grim "$shot" && echo "run-headless: wrote $shot" >&2
|
|
||||||
kill "$pid" 2>/dev/null || true
|
|
||||||
wait "$pid" 2>/dev/null || true
|
|
||||||
status=0
|
|
||||||
else
|
|
||||||
wait "$pid" 2>/dev/null || status=$?
|
|
||||||
echo "run-headless: $example exited early (status ${status:-0})" >&2
|
|
||||||
status=${status:-1}
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "--- $example output ---" >&2
|
|
||||||
cat "$run/$example.log" >&2
|
|
||||||
exit "$status"
|
|
||||||
-140
@@ -37,81 +37,6 @@ macro_rules! assert_corners {
|
|||||||
}
|
}
|
||||||
pub use crate::assert_corners;
|
pub use crate::assert_corners;
|
||||||
|
|
||||||
/// One replayed pointer sample, cut down to what a window delivers: where the
|
|
||||||
/// pointer is, and whether the button changed.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum TouchAction {
|
|
||||||
Down,
|
|
||||||
Move,
|
|
||||||
Up,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TouchAction {
|
|
||||||
fn parse(word: &str) -> Option<Self> {
|
|
||||||
match word {
|
|
||||||
"down" => Some(Self::Down),
|
|
||||||
"move" => Some(Self::Move),
|
|
||||||
"up" => Some(Self::Up),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct TouchSample {
|
|
||||||
pub t_ms: u64,
|
|
||||||
pub action: TouchAction,
|
|
||||||
pub pos: Vec2,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A recorded gesture, in the output's own pixels: `<ms> down|move|up <x> <y>`
|
|
||||||
/// a line, `#` to end of line ignored.
|
|
||||||
///
|
|
||||||
/// One parser for both ways of replaying a recording -- into a harness, and
|
|
||||||
/// into a real window -- so the two cannot read the same file differently.
|
|
||||||
pub struct TouchScript {
|
|
||||||
pub samples: Vec<TouchSample>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TouchScript {
|
|
||||||
pub fn parse(text: &str) -> Result<Self, String> {
|
|
||||||
let mut samples: Vec<TouchSample> = Vec::new();
|
|
||||||
for (i, line) in text.lines().enumerate() {
|
|
||||||
let line = line.split('#').next().unwrap_or("").trim();
|
|
||||||
if line.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let at = |what: &str| format!("touch script line {}: {what}: {line:?}", i + 1);
|
|
||||||
let mut words = line.split_whitespace();
|
|
||||||
let (Some(t), Some(action), Some(x), Some(y), None) = (
|
|
||||||
words.next(),
|
|
||||||
words.next(),
|
|
||||||
words.next(),
|
|
||||||
words.next(),
|
|
||||||
words.next(),
|
|
||||||
) else {
|
|
||||||
return Err(at("expected `t_ms action x y`"));
|
|
||||||
};
|
|
||||||
let t_ms: u64 = t.parse().map_err(|_| at("t_ms is not a whole number"))?;
|
|
||||||
let action =
|
|
||||||
TouchAction::parse(action).ok_or_else(|| at("action is not down/move/up"))?;
|
|
||||||
let x: f32 = x.parse().map_err(|_| at("x is not a number"))?;
|
|
||||||
let y: f32 = y.parse().map_err(|_| at("y is not a number"))?;
|
|
||||||
if let Some(last) = samples.last()
|
|
||||||
&& t_ms < last.t_ms
|
|
||||||
{
|
|
||||||
return Err(at("samples must be in time order"));
|
|
||||||
}
|
|
||||||
samples.push(TouchSample {
|
|
||||||
t_ms,
|
|
||||||
action,
|
|
||||||
pos: Vec2::new(x, y),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(Self { samples })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct HarnessState {
|
pub struct HarnessState {
|
||||||
pub root: Option<StrongWidget>,
|
pub root: Option<StrongWidget>,
|
||||||
@@ -235,23 +160,6 @@ impl Harness {
|
|||||||
self.release(CursorButton::Left);
|
self.release(CursorButton::Left);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drives a recorded gesture through the harness.
|
|
||||||
pub fn replay(&mut self, script: &TouchScript) {
|
|
||||||
for sample in &script.samples {
|
|
||||||
match sample.action {
|
|
||||||
TouchAction::Down => {
|
|
||||||
self.move_to(sample.pos);
|
|
||||||
self.press(CursorButton::Left);
|
|
||||||
}
|
|
||||||
TouchAction::Move => self.move_to(sample.pos),
|
|
||||||
TouchAction::Up => {
|
|
||||||
self.move_to(sample.pos);
|
|
||||||
self.release(CursorButton::Left);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn button(&mut self, button: CursorButton) -> &mut ActivationState {
|
fn button(&mut self, button: CursorButton) -> &mut ActivationState {
|
||||||
let buttons = &mut self.cursor.buttons;
|
let buttons = &mut self.cursor.buttons;
|
||||||
match button {
|
match button {
|
||||||
@@ -270,51 +178,3 @@ impl Harness {
|
|||||||
self.cursor.end_frame();
|
self.cursor.end_frame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn parse(text: &str) -> Result<Vec<(u64, TouchAction, f32, f32)>, String> {
|
|
||||||
Ok(TouchScript::parse(text)?
|
|
||||||
.samples
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.t_ms, s.action, s.pos.x, s.pos.y))
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_recording_is_time_action_and_a_point() {
|
|
||||||
assert_eq!(
|
|
||||||
parse("0 down 10 20\n16 move 10.5 24\n32 up 10.5 24").unwrap(),
|
|
||||||
[
|
|
||||||
(0, TouchAction::Down, 10.0, 20.0),
|
|
||||||
(16, TouchAction::Move, 10.5, 24.0),
|
|
||||||
(32, TouchAction::Up, 10.5, 24.0),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn blank_lines_and_comments_are_not_samples() {
|
|
||||||
assert_eq!(
|
|
||||||
parse("# a flick\n\n 0 down 1 2 # the finger lands\n\n").unwrap(),
|
|
||||||
[(0, TouchAction::Down, 1.0, 2.0)]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_recording_that_goes_backwards_is_rejected() {
|
|
||||||
// Replay waits out the gap between samples, so time running backwards
|
|
||||||
// would silently become no wait at all.
|
|
||||||
let err = parse("16 down 1 2\n0 up 1 2").unwrap_err();
|
|
||||||
assert!(err.contains("time order"), "{err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_line_that_is_not_a_sample_says_which_line() {
|
|
||||||
let err = parse("0 down 1 2\n16 wiggle 1 2").unwrap_err();
|
|
||||||
assert!(err.contains("line 2"), "{err}");
|
|
||||||
assert!(err.contains("down/move/up"), "{err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
pub mod default;
|
pub mod default;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod harness;
|
pub mod harness;
|
||||||
pub mod random;
|
|
||||||
pub mod widget;
|
pub mod widget;
|
||||||
|
|
||||||
pub use iris_core as core;
|
pub use iris_core as core;
|
||||||
|
|||||||
-271
@@ -1,271 +0,0 @@
|
|||||||
//! A seeded random widget tree, for tests and for looking at.
|
|
||||||
//!
|
|
||||||
//! One seed is one tree, on any machine and after any upgrade, so a test can
|
|
||||||
//! grow the same tree twice and a failing seed is reproduced by its number.
|
|
||||||
//! `examples/random.rs` draws one; `tests/generated.rs` checks that laying one
|
|
||||||
//! out again lands where growing it from scratch would.
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// The declared lengths of one `SetSize`, by axis.
|
|
||||||
pub type Lens = [Option<Len>; 2];
|
|
||||||
|
|
||||||
/// What a test changes between two trees grown from the same seed, so the
|
|
||||||
/// warm one can be mutated and the cold one grown that way to begin with.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Edits {
|
|
||||||
/// Declared sizes, by the order the `SetSize` wrappers were made.
|
|
||||||
pub sizes: HashMap<usize, Lens>,
|
|
||||||
/// Which children a span has, by the order the spans were made.
|
|
||||||
pub spans: HashMap<usize, SpanEdit>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
pub struct SpanEdit {
|
|
||||||
/// Children to leave out, by index among the ones grown.
|
|
||||||
pub detach: Vec<usize>,
|
|
||||||
/// How many of the span's spares are in it, appended in order.
|
|
||||||
pub attach: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// xorshift64, written out rather than taken from a crate so that a seed
|
|
||||||
/// keeps meaning the same tree.
|
|
||||||
pub struct Rng(u64);
|
|
||||||
|
|
||||||
impl Rng {
|
|
||||||
pub fn new(seed: u64) -> Self {
|
|
||||||
Self(seed | 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bits(&mut self) -> u64 {
|
|
||||||
self.0 ^= self.0 << 13;
|
|
||||||
self.0 ^= self.0 >> 7;
|
|
||||||
self.0 ^= self.0 << 17;
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn below(&mut self, n: usize) -> usize {
|
|
||||||
(self.bits() % n as u64) as usize
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn chance(&mut self) -> bool {
|
|
||||||
self.bits() & 1 == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const COLORS: [UiColor; 6] = [
|
|
||||||
UiColor::RED,
|
|
||||||
UiColor::GREEN,
|
|
||||||
UiColor::BLUE,
|
|
||||||
UiColor::YELLOW,
|
|
||||||
UiColor::CYAN,
|
|
||||||
UiColor::MAGENTA,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Leaves grown beside every span, for a test to put into it.
|
|
||||||
const SPARES: usize = 3;
|
|
||||||
|
|
||||||
const WORDS: &str = "Wrapping shapes one source into as many lines as the box \
|
|
||||||
leaves room for, so a paragraph's height is an answer and not a setting.";
|
|
||||||
|
|
||||||
/// What growing a tree gives back: every widget in creation order, so two
|
|
||||||
/// trees from one seed line up index for index, and the declared sizes, which
|
|
||||||
/// are what a test changes to watch the change propagate.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Tree {
|
|
||||||
pub ids: Vec<WidgetId>,
|
|
||||||
pub sized: Vec<WeakWidget<SetSize>>,
|
|
||||||
pub spans: Vec<Spanned>,
|
|
||||||
pub scrolls: Vec<WeakWidget<Scroll>>,
|
|
||||||
/// Children a `SpanEdit` took out, held so that dropping the last share
|
|
||||||
/// of one does not free its id for the next widget to be given -- which
|
|
||||||
/// would put the two trees' `ids` out of step.
|
|
||||||
pub detached: Vec<StrongWidget>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Spanned {
|
|
||||||
pub id: WeakWidget<Span>,
|
|
||||||
/// Leaves grown with the span whether or not they end up in it, so both
|
|
||||||
/// trees make the same widgets in the same order either way. Attaching
|
|
||||||
/// one moves it out of here: a widget belongs to one parent, and one that
|
|
||||||
/// belongs to nobody still has to be held or it reads as a leak.
|
|
||||||
pub spares: Vec<StrongWidget>,
|
|
||||||
/// How many children it was grown with, before any edit.
|
|
||||||
pub grown: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Grows the tree `seed` describes, `edits` replacing the declared sizes it
|
|
||||||
/// would otherwise have given those wrappers.
|
|
||||||
pub fn grow<Rsc: UiRsc + 'static>(
|
|
||||||
rsc: &mut Rsc,
|
|
||||||
seed: u64,
|
|
||||||
depth: usize,
|
|
||||||
edits: &Edits,
|
|
||||||
) -> (StrongWidget, Tree) {
|
|
||||||
let mut grow = Grow {
|
|
||||||
rsc,
|
|
||||||
rng: Rng::new(seed),
|
|
||||||
tree: Tree::default(),
|
|
||||||
edits,
|
|
||||||
};
|
|
||||||
let root = grow.node(depth);
|
|
||||||
(root, grow.tree)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Grow<'a, Rsc> {
|
|
||||||
rsc: &'a mut Rsc,
|
|
||||||
rng: Rng,
|
|
||||||
tree: Tree,
|
|
||||||
edits: &'a Edits,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
|
|
||||||
fn leaf(&mut self) -> StrongWidget {
|
|
||||||
let id: StrongWidget = match self.rng.below(4) {
|
|
||||||
// Wrapped and unwrapped, because only one of them reads the width
|
|
||||||
// it is given and so only one has to be drawn again for a new one.
|
|
||||||
0 => wtext(WORDS).size(16).wrap(true).add_strong(self.rsc),
|
|
||||||
1 => wtext("one line, overflowing whatever it is given")
|
|
||||||
.size(16)
|
|
||||||
.wrap(false)
|
|
||||||
.add_strong(self.rsc),
|
|
||||||
_ => {
|
|
||||||
let color = COLORS[self.rng.below(COLORS.len())];
|
|
||||||
let alpha = (self.rng.below(5) * 63) as u8;
|
|
||||||
rect(color.alpha(alpha)).add_strong(self.rsc)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn len(&mut self) -> Option<Len> {
|
|
||||||
match self.rng.below(4) {
|
|
||||||
0 => Some(Len::abs(20.0 + self.rng.below(180) as f32)),
|
|
||||||
1 => Some(Len::REST),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn align(&mut self) -> Align {
|
|
||||||
let mut axis = || match self.rng.below(4) {
|
|
||||||
0 => None,
|
|
||||||
1 => Some(AxisAlign::Neg),
|
|
||||||
2 => Some(AxisAlign::Center),
|
|
||||||
_ => Some(AxisAlign::Pos),
|
|
||||||
};
|
|
||||||
let (mut x, y) = (axis(), axis());
|
|
||||||
// Aligning on neither axis is just another transparent wrapper and
|
|
||||||
// would leave this branch unexercised.
|
|
||||||
if x.is_none() && y.is_none() {
|
|
||||||
x = Some(AxisAlign::Center);
|
|
||||||
}
|
|
||||||
Align { x, y }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A declared size over half the tree, kept where a test can change it.
|
|
||||||
fn sized(&mut self, inner: StrongWidget) -> StrongWidget {
|
|
||||||
if !self.rng.chance() {
|
|
||||||
return inner;
|
|
||||||
}
|
|
||||||
let idx = self.tree.sized.len();
|
|
||||||
let lens = [self.len(), self.len()];
|
|
||||||
let lens = self.edits.sizes.get(&idx).copied().unwrap_or(lens);
|
|
||||||
let id = SetSize {
|
|
||||||
inner,
|
|
||||||
x: lens[0],
|
|
||||||
y: lens[1],
|
|
||||||
}
|
|
||||||
.add(self.rsc);
|
|
||||||
self.tree.sized.push(id);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
id.add_strong(self.rsc)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn node(&mut self, depth: usize) -> StrongWidget {
|
|
||||||
if depth == 0 {
|
|
||||||
return self.leaf();
|
|
||||||
}
|
|
||||||
let positioned = self.rng.below(6);
|
|
||||||
if positioned == 0 {
|
|
||||||
// Scrolling reads the pixel length of its box, which nothing
|
|
||||||
// else here does, and gives its child a box longer than its own.
|
|
||||||
let inner = self.node(depth - 1);
|
|
||||||
let inner = self.sized(inner);
|
|
||||||
let axis = if self.rng.chance() { Axis::X } else { Axis::Y };
|
|
||||||
let id = Scroll::new(inner, axis).add(self.rsc);
|
|
||||||
self.tree.scrolls.push(id);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
return id.add_strong(self.rsc);
|
|
||||||
}
|
|
||||||
if positioned == 1 {
|
|
||||||
let inner = self.node(depth - 1);
|
|
||||||
let inner = self.sized(inner);
|
|
||||||
let id = Aligned {
|
|
||||||
inner,
|
|
||||||
align: self.align(),
|
|
||||||
}
|
|
||||||
.add_strong(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
if self.rng.below(4) == 0 {
|
|
||||||
let inner = self.node(depth - 1);
|
|
||||||
let inner = self.sized(inner);
|
|
||||||
// Each side its own, since a padding that is the same all round
|
|
||||||
// hides anything that treats one edge differently from another.
|
|
||||||
let mut side = || self.rng.below(24) as f32;
|
|
||||||
let padding = Padding {
|
|
||||||
left: side(),
|
|
||||||
right: side(),
|
|
||||||
top: side(),
|
|
||||||
bottom: side(),
|
|
||||||
};
|
|
||||||
let id = Pad { padding, inner }.add_strong(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
let grown = 2 + self.rng.below(3);
|
|
||||||
let mut children = Vec::with_capacity(grown);
|
|
||||||
for _ in 0..grown {
|
|
||||||
let child = self.node(depth - 1);
|
|
||||||
children.push(self.sized(child));
|
|
||||||
}
|
|
||||||
if self.rng.chance() {
|
|
||||||
let id = Stack {
|
|
||||||
children,
|
|
||||||
size: StackSize::Child(0),
|
|
||||||
}
|
|
||||||
.add_strong(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
// Grown either way, so the widget after them has the same id in a
|
|
||||||
// tree that leaves them out as in one that puts them in.
|
|
||||||
let mut spares: Vec<StrongWidget> = (0..SPARES).map(|_| self.leaf()).collect();
|
|
||||||
let idx = self.tree.spans.len();
|
|
||||||
let edit = self.edits.spans.get(&idx).cloned().unwrap_or_default();
|
|
||||||
// Highest first, so an index means the same child however many of its
|
|
||||||
// neighbours are going too.
|
|
||||||
let mut detach = edit.detach.clone();
|
|
||||||
detach.sort_unstable();
|
|
||||||
for j in detach.into_iter().rev() {
|
|
||||||
if j < children.len() {
|
|
||||||
self.tree.detached.push(children.remove(j));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let attach = edit.attach.min(spares.len());
|
|
||||||
children.extend(spares.drain(..attach));
|
|
||||||
let dir = [Dir::RIGHT, Dir::DOWN, Dir::LEFT, Dir::UP][self.rng.below(4)];
|
|
||||||
let id = Span {
|
|
||||||
children,
|
|
||||||
dir,
|
|
||||||
gap: self.rng.below(3) as f32 * 4.0,
|
|
||||||
}
|
|
||||||
.add(self.rsc);
|
|
||||||
self.tree.ids.push(id.id());
|
|
||||||
self.tree.spans.push(Spanned { id, spares, grown });
|
|
||||||
id.add_strong(self.rsc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-6
@@ -6,17 +6,16 @@ pub struct Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Image {
|
impl Widget for Image {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
painter.primitive(&self.handle);
|
painter.primitive(&self.handle);
|
||||||
Size::abs(self.handle.size())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size_hint(&self, axis: Axis) -> Option<Len> {
|
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||||
Some(Len::abs(self.handle.size().axis(axis)))
|
Len::abs(self.handle.size().x)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
||||||
OnResize::Scale
|
Len::abs(self.handle.size().y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-5
@@ -5,13 +5,16 @@ pub struct Masked {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Masked {
|
impl Widget for Masked {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
painter.set_mask(painter.region());
|
painter.set_mask(painter.region());
|
||||||
painter.widget(&self.inner).size()
|
painter.widget(&self.inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// It clips to the box it was given, not to the part its child used.
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
ctx.width(&self.inner)
|
||||||
OnResize::Redraw
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
ctx.height(&self.inner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,37 +6,30 @@ pub struct Aligned {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Aligned {
|
impl Widget for Aligned {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
let known = match self.align.tuple() {
|
|
||||||
(Some(_), Some(_)) => painter
|
|
||||||
.known_len(&self.inner, Axis::X, UiRegion::FULL)
|
|
||||||
.zip(painter.known_len(&self.inner, Axis::Y, UiRegion::FULL))
|
|
||||||
.map(|(x, y)| Size { x, y }),
|
|
||||||
(Some(_), None) => painter
|
|
||||||
.known_len(&self.inner, Axis::X, UiRegion::FULL)
|
|
||||||
.map(|x| Size { x, y: Len::REST }),
|
|
||||||
(None, Some(_)) => painter
|
|
||||||
.known_len(&self.inner, Axis::Y, UiRegion::FULL)
|
|
||||||
.map(|y| Size { x: Len::REST, y }),
|
|
||||||
(None, None) => Some(Size::REST),
|
|
||||||
};
|
|
||||||
// Drawn where it may be too big only when the aligned axes are not
|
|
||||||
// already known, then given its aligned box once its size is known.
|
|
||||||
let had_size = known.is_some();
|
|
||||||
let size = known.unwrap_or_else(|| painter.place(&self.inner, UiRegion::FULL).size());
|
|
||||||
let region = match self.align.tuple() {
|
let region = match self.align.tuple() {
|
||||||
(Some(x), Some(y)) => size.to_uivec2().align(RegionAlign { x, y }),
|
(Some(x), Some(y)) => painter
|
||||||
(Some(x), None) => UiRegion::new(size.x.apply_rest().align(x), UiSpan::FULL),
|
.size(&self.inner)
|
||||||
(None, Some(y)) => UiRegion::new(UiSpan::FULL, size.y.apply_rest().align(y)),
|
.to_uivec2()
|
||||||
|
.align(RegionAlign { x, y }),
|
||||||
|
(Some(x), None) => {
|
||||||
|
let x = painter.size_ctx().width(&self.inner).apply_rest().align(x);
|
||||||
|
UiRegion::new(x, UiSpan::FULL)
|
||||||
|
}
|
||||||
|
(None, Some(y)) => {
|
||||||
|
let y = painter.size_ctx().height(&self.inner).apply_rest().align(y);
|
||||||
|
UiRegion::new(UiSpan::FULL, y)
|
||||||
|
}
|
||||||
(None, None) => UiRegion::FULL,
|
(None, None) => UiRegion::FULL,
|
||||||
};
|
};
|
||||||
let placed = painter.place(&self.inner, region).size();
|
painter.widget_within(&self.inner, region);
|
||||||
if had_size { placed } else { size }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The aligned box is a fraction of its own, so the child keeps its
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
/// length and stays against the edge it was aligned to.
|
ctx.width(&self.inner)
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
}
|
||||||
OnResize::Scale
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
ctx.height(&self.inner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,14 +6,18 @@ pub struct LayerOffset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for LayerOffset {
|
impl Widget for LayerOffset {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
for _ in 0..self.offset {
|
for _ in 0..self.offset {
|
||||||
painter.next_layer();
|
painter.next_layer();
|
||||||
}
|
}
|
||||||
painter.widget(&self.inner).size()
|
painter.widget(&self.inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
OnResize::Scale
|
ctx.width(&self.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
ctx.height(&self.inner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,20 +6,43 @@ pub struct MaxSize {
|
|||||||
pub y: Option<Len>,
|
pub y: Option<Len>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for MaxSize {
|
impl MaxSize {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
|
||||||
let child = painter.widget(&self.inner).size();
|
if let Some(x) = self.x {
|
||||||
let output = painter.output_size();
|
ctx.outer.x.select_len(x.apply_rest());
|
||||||
Size {
|
}
|
||||||
x: capped(child.x, self.x, output.x),
|
if let Some(y) = self.y {
|
||||||
y: capped(child.y, self.y, output.y),
|
ctx.outer.y.select_len(y.apply_rest());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn capped(len: Len, max: Option<Len>, output: f32) -> Len {
|
impl Widget for MaxSize {
|
||||||
match max {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
Some(max) if len.apply_rest().to_abs(output) > max.apply_rest().to_abs(output) => max,
|
painter.widget(&self.inner);
|
||||||
_ => len,
|
}
|
||||||
|
|
||||||
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
self.apply_to_outer(ctx);
|
||||||
|
let width = ctx.width(&self.inner);
|
||||||
|
if let Some(x) = self.x {
|
||||||
|
let width_px = width.apply_rest().to_abs(ctx.output_size().x);
|
||||||
|
let x_px = x.apply_rest().to_abs(ctx.output_size().x);
|
||||||
|
if width_px > x_px { x } else { width }
|
||||||
|
} else {
|
||||||
|
width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
self.apply_to_outer(ctx);
|
||||||
|
let height = ctx.height(&self.inner);
|
||||||
|
if let Some(y) = self.y {
|
||||||
|
let height_px = height.apply_rest().to_abs(ctx.output_size().y);
|
||||||
|
let y_px = y.apply_rest().to_abs(ctx.output_size().y);
|
||||||
|
if height_px > y_px { y } else { height }
|
||||||
|
} else {
|
||||||
|
height
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,12 +6,16 @@ pub struct Offset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Offset {
|
impl Widget for Offset {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
let region = UiRegion::FULL.offset(self.amt);
|
let region = UiRegion::FULL.offset(self.amt);
|
||||||
painter.widget_within(&self.inner, region).size()
|
painter.widget_within(&self.inner, region);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
OnResize::Scale
|
ctx.width(&self.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
ctx.height(&self.inner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+20
-18
@@ -6,26 +6,28 @@ pub struct Pad {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Pad {
|
impl Widget for Pad {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
let inner = painter
|
painter.widget_within(&self.inner, self.padding.region());
|
||||||
.widget_within(&self.inner, self.padding.region())
|
|
||||||
.size();
|
|
||||||
Size {
|
|
||||||
x: Len {
|
|
||||||
abs: inner.x.abs + self.padding.left + self.padding.right,
|
|
||||||
..inner.x
|
|
||||||
},
|
|
||||||
y: Len {
|
|
||||||
abs: inner.y.abs + self.padding.top + self.padding.bottom,
|
|
||||||
..inner.y
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The padding is an offset from each edge, so a longer box pads the same
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
/// amount and the child takes the rest.
|
let width = self.padding.left + self.padding.right;
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
let height = self.padding.top + self.padding.bottom;
|
||||||
OnResize::Scale
|
ctx.outer.x.abs -= width;
|
||||||
|
ctx.outer.y.abs -= height;
|
||||||
|
let mut size = ctx.width(&self.inner);
|
||||||
|
size.abs += width;
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
let width = self.padding.left + self.padding.right;
|
||||||
|
let height = self.padding.top + self.padding.bottom;
|
||||||
|
ctx.outer.x.abs -= width;
|
||||||
|
ctx.outer.y.abs -= height;
|
||||||
|
let mut size = ctx.height(&self.inner);
|
||||||
|
size.abs += height;
|
||||||
|
size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,11 @@ pub struct Scroll {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Scroll {
|
impl Widget for Scroll {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
let output_len = painter.output_len(self.axis);
|
let output_len = painter.output_size().axis(self.axis);
|
||||||
let container_len = UiScalar::abs(painter.px_len(self.axis));
|
let container_len = painter.region().axis(self.axis).len();
|
||||||
// Draw in the whole container only when its scrolling-axis length is
|
let content_len = painter
|
||||||
// not already known, then place it at the scrolled offset.
|
.len_axis(&self.inner, self.axis)
|
||||||
let known_len = painter.known_len(&self.inner, self.axis, UiRegion::FULL);
|
|
||||||
let measured = known_len.is_none();
|
|
||||||
let child = measured.then(|| painter.place(&self.inner, UiRegion::FULL).size());
|
|
||||||
let content_len = known_len
|
|
||||||
.unwrap_or_else(|| child.unwrap().axis(self.axis))
|
|
||||||
.apply_rest()
|
.apply_rest()
|
||||||
.within_len(container_len)
|
.within_len(container_len)
|
||||||
.to_abs(output_len);
|
.to_abs(output_len);
|
||||||
@@ -33,8 +28,15 @@ impl Widget for Scroll {
|
|||||||
|
|
||||||
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
|
let mut region = UiRegion::FULL.offset(Vec2::from_axis(self.axis, -self.amt, 0.0));
|
||||||
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
region.axis_mut(self.axis).end = region.axis(self.axis).start.offset(self.content_len);
|
||||||
let placed = painter.place(&self.inner, region).size();
|
painter.widget_within(&self.inner, region);
|
||||||
child.unwrap_or(placed)
|
}
|
||||||
|
|
||||||
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
ctx.width(&self.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
ctx.height(&self.inner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,25 +6,29 @@ pub struct SetSize {
|
|||||||
pub y: Option<Len>,
|
pub y: Option<Len>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for SetSize {
|
impl SetSize {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn apply_to_outer(&self, ctx: &mut SizeCtx) {
|
||||||
let child = painter.widget(&self.inner).size();
|
if let Some(x) = self.x {
|
||||||
Size {
|
ctx.outer.x.select_len(x.apply_rest());
|
||||||
x: self.x.unwrap_or(child.x),
|
|
||||||
y: self.y.unwrap_or(child.y),
|
|
||||||
}
|
}
|
||||||
}
|
if let Some(y) = self.y {
|
||||||
|
ctx.outer.y.select_len(y.apply_rest());
|
||||||
/// A declared axis is known without looking at the child, which is what
|
|
||||||
/// lets a span lay out around `.height(rest(1))` without drawing it.
|
|
||||||
fn size_hint(&self, axis: Axis) -> Option<Len> {
|
|
||||||
match axis {
|
|
||||||
Axis::X => self.x,
|
|
||||||
Axis::Y => self.y,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
impl Widget for SetSize {
|
||||||
OnResize::Scale
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
|
painter.widget(&self.inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
self.apply_to_outer(ctx);
|
||||||
|
self.x.unwrap_or_else(|| ctx.width(&self.inner))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
self.apply_to_outer(ctx);
|
||||||
|
self.y.unwrap_or_else(|| ctx.height(&self.inner))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+100
-45
@@ -8,35 +8,13 @@ pub struct Span {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Span {
|
impl Widget for Span {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
let axis = self.dir.axis;
|
let total = self.len_sum(&mut painter.size_ctx());
|
||||||
// A length for every child before any is placed: from its own hint
|
|
||||||
// where it has one, and from drawing it where it does not.
|
|
||||||
let mut cursor = UiScalar::rel_min();
|
|
||||||
let mut lens = Vec::with_capacity(self.children.len());
|
|
||||||
for child in &self.children {
|
|
||||||
let mut span = UiSpan::new(cursor, UiScalar::rel_max());
|
|
||||||
if self.dir.sign == Sign::Neg {
|
|
||||||
span.flip();
|
|
||||||
}
|
|
||||||
let region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
|
||||||
let len = match painter.known_len(child, axis, region) {
|
|
||||||
Some(len) => len,
|
|
||||||
None => painter.place(child, region).len(axis),
|
|
||||||
};
|
|
||||||
cursor.abs += len.abs + self.gap;
|
|
||||||
cursor.rel += len.rel;
|
|
||||||
lens.push(len);
|
|
||||||
}
|
|
||||||
|
|
||||||
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
|
|
||||||
let total = lens.iter().fold(Len::abs(gap), |sum, len| sum + *len);
|
|
||||||
|
|
||||||
let mut start = UiScalar::rel_min();
|
let mut start = UiScalar::rel_min();
|
||||||
let mut ortho = Len::ZERO;
|
for child in &self.children {
|
||||||
for (child, len) in self.children.iter().zip(&lens) {
|
|
||||||
let mut span = UiSpan::FULL;
|
let mut span = UiSpan::FULL;
|
||||||
span.start = start;
|
span.start = start;
|
||||||
|
let len = painter.len_axis(child, self.dir.axis);
|
||||||
if len.rest > 0.0 {
|
if len.rest > 0.0 {
|
||||||
let offset = UiScalar::new(total.rel, total.abs);
|
let offset = UiScalar::new(total.rel, total.abs);
|
||||||
let rel_end = UiScalar::rel(len.rest / total.rest);
|
let rel_end = UiScalar::rel(len.rest / total.rest);
|
||||||
@@ -46,31 +24,27 @@ impl Widget for Span {
|
|||||||
start.abs += len.abs;
|
start.abs += len.abs;
|
||||||
start.rel += len.rel;
|
start.rel += len.rel;
|
||||||
span.end = start;
|
span.end = start;
|
||||||
let mut region = UiRegion::from_axis(axis, span, UiSpan::FULL);
|
let mut child_region = UiRegion::from_axis(self.dir.axis, span, UiSpan::FULL);
|
||||||
if self.dir.sign == Sign::Neg {
|
if self.dir.sign == Sign::Neg {
|
||||||
region.flip(axis);
|
child_region.flip(self.dir.axis);
|
||||||
}
|
|
||||||
let used = painter.place(child, region).size().axis(!axis);
|
|
||||||
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
|
|
||||||
if used.rel > 0.0 || used.rest > 0.0 {
|
|
||||||
ortho = Len::REST;
|
|
||||||
} else if ortho.rest == 0.0 {
|
|
||||||
ortho.abs = ortho.abs.max(used.abs);
|
|
||||||
}
|
}
|
||||||
|
painter.widget_within(child, child_region);
|
||||||
start.abs += self.gap;
|
start.abs += self.gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
let along = match total.rest == 0.0 && total.rel == 0.0 {
|
|
||||||
true => total,
|
|
||||||
false => Len::default(),
|
|
||||||
};
|
|
||||||
Size::from_axis(axis, along, ortho)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every child is placed in fractions and offsets of the span's own box,
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
/// so a longer box holds the same layout and the children follow it.
|
match self.dir.axis {
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
Axis::X => self.desired_len(ctx),
|
||||||
OnResize::Scale
|
Axis::Y => self.desired_ortho(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
match self.dir.axis {
|
||||||
|
Axis::X => self.desired_ortho(ctx),
|
||||||
|
Axis::Y => self.desired_len(ctx),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +69,87 @@ impl Span {
|
|||||||
pub fn pop(&mut self) -> Option<StrongWidget> {
|
pub fn pop(&mut self) -> Option<StrongWidget> {
|
||||||
self.children.pop()
|
self.children.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn len_sum(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
let gap = self.gap * self.children.len().saturating_sub(1) as f32;
|
||||||
|
self.children.iter().fold(Len::abs(gap), |mut s, id| {
|
||||||
|
// it's tempting to subtract the abs & rel from the ctx outer,
|
||||||
|
// but that would create inconsistent sizing if you put
|
||||||
|
// a rest first vs last & only speed up in one direction.
|
||||||
|
// I think this is only solvable by restricting how you can
|
||||||
|
// compute size, bc currently you need child to define parent's
|
||||||
|
// sectioning and you need parent's sectioning to define child.
|
||||||
|
// Fortunately, that doesn't matter in most cases
|
||||||
|
let len = ctx.len_axis(id, self.dir.axis);
|
||||||
|
s += len;
|
||||||
|
s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_len(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
let len = self.len_sum(ctx);
|
||||||
|
if len.rest == 0.0 && len.rel == 0.0 {
|
||||||
|
len
|
||||||
|
} else {
|
||||||
|
Len::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_ortho(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
// this is a weird hack to get text wrapping to work properly when in a downward span
|
||||||
|
// the correct solution here is to add a function to widget that lets them
|
||||||
|
// request that ctx.outer has an axis "resolved" before checking the other,
|
||||||
|
// and panicking or warning if two request opposite axis (unsolvable in that case)
|
||||||
|
let outer = ctx.outer.axis(self.dir.axis);
|
||||||
|
if self.dir.axis == Axis::X {
|
||||||
|
// so....... this literally copies draw so that the lengths are correctly set in the
|
||||||
|
// context, which makes this slow and not cool
|
||||||
|
let total = self.len_sum(ctx);
|
||||||
|
let mut start = UiScalar::rel_min();
|
||||||
|
let mut ortho_len = Len::ZERO;
|
||||||
|
for child in &self.children {
|
||||||
|
let mut span = UiSpan::FULL;
|
||||||
|
span.start = start;
|
||||||
|
let len = ctx.len_axis(child, self.dir.axis);
|
||||||
|
if len.rest > 0.0 {
|
||||||
|
let offset = UiScalar::new(total.rel, total.abs);
|
||||||
|
let rel_end = UiScalar::rel(len.rest / total.rest);
|
||||||
|
let end = (UiScalar::rel_max() + start) - offset;
|
||||||
|
start = rel_end.within(&start.to(end));
|
||||||
|
}
|
||||||
|
start.abs += len.abs;
|
||||||
|
start.rel += len.rel;
|
||||||
|
span.end = start;
|
||||||
|
|
||||||
|
let scalar = span.len();
|
||||||
|
*ctx.outer.axis_mut(self.dir.axis) = outer.select_len(scalar);
|
||||||
|
let ortho = ctx.len_axis(child, !self.dir.axis);
|
||||||
|
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
|
||||||
|
if ortho.rel > 0.0 || ortho.rest > 0.0 {
|
||||||
|
ortho_len.rest = 1.0;
|
||||||
|
ortho_len.abs = 0.0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ortho_len.abs = ortho_len.abs.max(ortho.abs);
|
||||||
|
start.abs += self.gap;
|
||||||
|
}
|
||||||
|
ortho_len
|
||||||
|
} else {
|
||||||
|
let mut ortho_len = Len::ZERO;
|
||||||
|
let ortho = !self.dir.axis;
|
||||||
|
for child in &self.children {
|
||||||
|
let len = ctx.len_axis(child, ortho);
|
||||||
|
// TODO: rel shouldn't do this, but no easy way before actually calculating pixels
|
||||||
|
if len.rel > 0.0 || len.rest > 0.0 {
|
||||||
|
ortho_len.rest = 1.0;
|
||||||
|
ortho_len.abs = 0.0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ortho_len.abs = ortho_len.abs.max(len.abs);
|
||||||
|
}
|
||||||
|
ortho_len
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
pub struct SpanBuilder<State, const LEN: usize, Wa: WidgetArrLike<State, LEN, Tag>, Tag> {
|
||||||
|
|||||||
@@ -8,29 +8,30 @@ pub struct Stack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Stack {
|
impl Widget for Stack {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
let sizing = match self.size {
|
let mut iter = self.children.iter();
|
||||||
StackSize::Default => None,
|
if let Some(child) = iter.next() {
|
||||||
StackSize::Child(i) => Some(i),
|
painter.child_layer();
|
||||||
};
|
painter.widget(child);
|
||||||
let mut size = Size::default();
|
}
|
||||||
for (i, child) in self.children.iter().enumerate() {
|
for child in iter {
|
||||||
match i {
|
painter.next_layer();
|
||||||
0 => painter.child_layer(),
|
painter.widget(child);
|
||||||
_ => 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 on_resize(&self, _: Axis) -> OnResize {
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
OnResize::Scale
|
match self.size {
|
||||||
|
StackSize::Default => Len::default(),
|
||||||
|
StackSize::Child(i) => ctx.width(&self.children[i]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
match self.size {
|
||||||
|
StackSize::Default => Len::default(),
|
||||||
|
StackSize::Child(i) => ctx.height(&self.children[i]),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-4
@@ -6,10 +6,25 @@ pub struct WidgetPtr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for WidgetPtr {
|
impl Widget for WidgetPtr {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
match &self.inner {
|
if let Some(id) = &self.inner {
|
||||||
Some(id) => painter.widget(id).size(),
|
painter.widget(id);
|
||||||
None => Size::default(),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
if let Some(id) = &self.inner {
|
||||||
|
ctx.width(id)
|
||||||
|
} else {
|
||||||
|
Len::ZERO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
if let Some(id) = &self.inner {
|
||||||
|
ctx.height(id)
|
||||||
|
} else {
|
||||||
|
Len::ZERO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-7
@@ -28,23 +28,21 @@ impl Rect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Rect {
|
impl Widget for Rect {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
painter.primitive(RectPrimitive {
|
painter.primitive(RectPrimitive {
|
||||||
color: self.color,
|
color: self.color,
|
||||||
radius: self.radius,
|
radius: self.radius,
|
||||||
thickness: self.thickness,
|
thickness: self.thickness,
|
||||||
inner_radius: self.inner_radius,
|
inner_radius: self.inner_radius,
|
||||||
});
|
});
|
||||||
Size::REST
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size_hint(&self, _: Axis) -> Option<Len> {
|
fn desired_width(&mut self, _: &mut SizeCtx) -> Len {
|
||||||
Some(Len::REST)
|
Len::rest(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Its box is its primitive's own region, so a new one is written there.
|
fn desired_height(&mut self, _: &mut SizeCtx) -> Len {
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
Len::rest(1)
|
||||||
OnResize::Scale
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-16
@@ -55,47 +55,44 @@ impl TextEdit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for TextEdit {
|
impl Widget for TextEdit {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
let base = painter.layer;
|
let base = painter.layer;
|
||||||
painter.child_layer();
|
painter.child_layer();
|
||||||
let (_, size) = self.view.draw(painter);
|
self.view.draw(painter);
|
||||||
painter.layer = base;
|
painter.layer = base;
|
||||||
let region = self.region();
|
let region = self.region();
|
||||||
|
|
||||||
let Some(selection) = self.selection else {
|
let Some(selection) = self.selection else {
|
||||||
return size;
|
return;
|
||||||
};
|
};
|
||||||
let layout = self.view.buf.layout();
|
let layout = self.view.buf.layout();
|
||||||
|
|
||||||
// parley reports selection as boxes in layout space, so bidi and
|
// parley reports selection as boxes in layout space, so bidi and
|
||||||
// wrapped lines come out right without this code knowing about either.
|
// wrapped lines come out right without this code knowing about either.
|
||||||
for (rect, _) in selection.geometry(layout) {
|
for (rect, _) in selection.geometry(layout) {
|
||||||
let rect_size = vec2(rect.width() as f32, rect.height() as f32);
|
let size = vec2(rect.width() as f32, rect.height() as f32);
|
||||||
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
||||||
painter.primitive_within(
|
painter.primitive_within(
|
||||||
RectPrimitive::color(Color::SKY),
|
RectPrimitive::color(Color::SKY),
|
||||||
rect_size
|
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||||
.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 caret_size = vec2(caret.width() as f32, caret.height() as f32);
|
let size = vec2(caret.width() as f32, caret.height() as f32);
|
||||||
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
||||||
painter.primitive_within(
|
painter.primitive_within(
|
||||||
RectPrimitive::color(Color::WHITE),
|
RectPrimitive::color(Color::WHITE),
|
||||||
caret_size
|
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||||
.align(Align::TOP_LEFT)
|
|
||||||
.offset(top_left)
|
|
||||||
.within(®ion),
|
|
||||||
);
|
);
|
||||||
size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, axis: Axis) -> OnResize {
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
self.view.on_resize(axis)
|
self.view.desired_width(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
self.view.desired_height(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +127,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
pub fn set(&mut self, text: &str) {
|
pub fn set(&mut self, text: &str) {
|
||||||
let text = self.string(text);
|
let text = self.string(text);
|
||||||
self.text.view.buf.set_text(text);
|
self.text.view.buf.set_text(text);
|
||||||
|
self.text.view.buf.changed = true;
|
||||||
self.text.selection = None;
|
self.text.selection = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +174,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
};
|
};
|
||||||
let at = at.min(self.text.view.buf.text().len());
|
let at = at.min(self.text.view.buf.text().len());
|
||||||
self.text.view.buf.edit().insert_str(at, text);
|
self.text.view.buf.edit().insert_str(at, text);
|
||||||
|
self.text.view.buf.changed = true;
|
||||||
self.set_caret(at + text.len());
|
self.set_caret(at + text.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +187,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
let range = sel.text_range();
|
let range = sel.text_range();
|
||||||
self.text.view.buf.edit().replace_range(range.clone(), "");
|
self.text.view.buf.edit().replace_range(range.clone(), "");
|
||||||
|
self.text.view.buf.changed = true;
|
||||||
self.set_caret(range.start);
|
self.set_caret(range.start);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -265,6 +265,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
|
|
||||||
fn delete_range(&mut self, start: usize, end: usize) {
|
fn delete_range(&mut self, start: usize, end: usize) {
|
||||||
self.text.view.buf.edit().replace_range(start..end, "");
|
self.text.view.buf.edit().replace_range(start..end, "");
|
||||||
|
self.text.view.buf.changed = true;
|
||||||
self.set_caret(start);
|
self.set_caret(start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+61
-45
@@ -14,8 +14,11 @@ pub struct Text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct TextView {
|
pub struct TextView {
|
||||||
pub attrs: TextAttrs,
|
pub attrs: MutDetect<TextAttrs>,
|
||||||
pub buf: TextBuffer,
|
pub buf: MutDetect<TextBuffer>,
|
||||||
|
// cache
|
||||||
|
tex: Option<RenderedText>,
|
||||||
|
width: Option<f32>,
|
||||||
pub hint: Option<StrongWidget>,
|
pub hint: Option<StrongWidget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,13 +28,19 @@ impl TextView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn wrap_width(&self) -> Option<f32> {
|
pub fn wrap_width(&self) -> Option<f32> {
|
||||||
self.buf.wrap_width()
|
self.width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextView {
|
impl TextView {
|
||||||
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
|
pub fn new(buf: TextBuffer, attrs: TextAttrs, hint: Option<StrongWidget>) -> Self {
|
||||||
Self { attrs, buf, hint }
|
Self {
|
||||||
|
attrs: attrs.into(),
|
||||||
|
buf: buf.into(),
|
||||||
|
tex: None,
|
||||||
|
width: None,
|
||||||
|
hint,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// region where the text should be draw
|
/// region where the text should be draw
|
||||||
@@ -43,55 +52,56 @@ impl TextView {
|
|||||||
.align(self.align)
|
.align(self.align)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The text shaped for the width it is drawn in. The buffer keeps its
|
fn render(&mut self, ctx: &mut SizeCtx) -> &RenderedText {
|
||||||
/// 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(painter.px_len(Axis::X))
|
Some(ctx.px_size().x)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
painter.render_text(&mut self.buf, &self.attrs, width)
|
if width != self.width || self.tex.is_none() || self.attrs.changed || self.buf.changed {
|
||||||
|
self.width = width;
|
||||||
|
self.tex = Some(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.buf.rendered()
|
self.tex.as_ref()
|
||||||
}
|
}
|
||||||
/// Draws the text, and says where the glyphs went and what they use.
|
pub fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
pub fn draw(&mut self, painter: &mut Painter) -> (UiRegion, Size) {
|
if self.is_empty()
|
||||||
|
&& let Some(hint) = &self.hint
|
||||||
|
{
|
||||||
|
ctx.width(hint)
|
||||||
|
} else {
|
||||||
|
Len::abs(self.render(ctx).size.x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
if self.is_empty()
|
||||||
|
&& let Some(hint) = &self.hint
|
||||||
|
{
|
||||||
|
ctx.height(hint)
|
||||||
|
} else {
|
||||||
|
Len::abs(self.render(ctx).size.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn draw(&mut self, painter: &mut Painter) -> UiRegion {
|
||||||
let align = self.align;
|
let align = self.align;
|
||||||
if self.is_empty() && self.hint.is_some() {
|
if self.is_empty() && self.hint.is_some() {
|
||||||
let region = self.render(painter).size.align(align);
|
let region = self.render(&mut painter.size_ctx()).size.align(align);
|
||||||
let size = match &self.hint {
|
if let Some(hint) = &self.hint {
|
||||||
Some(hint) => painter.widget(hint).size(),
|
painter.widget(hint);
|
||||||
None => Size::ZERO,
|
}
|
||||||
};
|
return region;
|
||||||
return (region, size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let tex = self.render(painter);
|
let tex = self.render(&mut painter.size_ctx());
|
||||||
let region = tex.size.align(align);
|
let region = tex.size.align(align);
|
||||||
let size = Size::abs(tex.size);
|
|
||||||
let within = region.within(&painter.region());
|
let within = region.within(&painter.region());
|
||||||
painter.glyphs(tex, within);
|
painter.glyphs(tex, within);
|
||||||
(region, size)
|
region
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrapping reads the width it is offered, so a wider box reshapes it and
|
|
||||||
/// a taller one does not. Alignment matters too, and separately: glyphs
|
|
||||||
/// anchored to the start of an axis stay put when that extent changes,
|
|
||||||
/// but centred or end-aligned ones move even though the shaping stands.
|
|
||||||
pub fn on_resize(&self, axis: Axis) -> OnResize {
|
|
||||||
let reshapes = axis == Axis::X && self.attrs.wrap;
|
|
||||||
let anchored = match axis {
|
|
||||||
Axis::X => self.align.x,
|
|
||||||
Axis::Y => self.align.y,
|
|
||||||
} == AxisAlign::Neg;
|
|
||||||
match reshapes || !anchored {
|
|
||||||
true => OnResize::Redraw,
|
|
||||||
false => OnResize::Translate,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(&self) -> String {
|
pub fn content(&self) -> String {
|
||||||
@@ -107,7 +117,7 @@ impl Text {
|
|||||||
content: content.into(),
|
content: content.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn update_buf(&mut self) {
|
fn update_buf(&mut self, _ctx: &mut SizeCtx) {
|
||||||
if self.content.changed {
|
if self.content.changed {
|
||||||
self.content.changed = false;
|
self.content.changed = false;
|
||||||
self.view.buf.set_text(self.content.as_str());
|
self.view.buf.set_text(self.content.as_str());
|
||||||
@@ -116,13 +126,19 @@ impl Text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Text {
|
impl Widget for Text {
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
fn draw(&mut self, painter: &mut Painter) {
|
||||||
self.update_buf();
|
self.update_buf(&mut painter.size_ctx());
|
||||||
self.view.draw(painter).1
|
self.view.draw(painter);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&self, axis: Axis) -> OnResize {
|
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
self.view.on_resize(axis)
|
self.update_buf(ctx);
|
||||||
|
self.view.desired_width(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
|
||||||
|
self.update_buf(ctx);
|
||||||
|
self.view.desired_height(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
//! What the vertex shader's move-chain walk costs, against how deep the chain
|
|
||||||
//! is. Every active widget owns a slot, so the depth a primitive resolves
|
|
||||||
//! through is its depth in the widget tree.
|
|
||||||
//!
|
|
||||||
//! cargo test --release --test chain_cost -- --ignored --nocapture
|
|
||||||
//!
|
|
||||||
//! Timed on the GPU with timestamp queries rather than by the clock: wall time
|
|
||||||
//! here varied by 2x between runs of one unchanged binary. The pass is
|
|
||||||
//! submitted and waited on, so this is the GPU's cost and not the recording
|
|
||||||
//! loop's -- which is what `draw_cost.rs` measures instead.
|
|
||||||
//!
|
|
||||||
//! The instances are two pixels wide so that vertex work dominates; a chain
|
|
||||||
//! walk that does not show up against small quads will not show up against
|
|
||||||
//! anything.
|
|
||||||
//!
|
|
||||||
//! The instance is leaked deliberately, for the reason `draw_cost.rs` gives.
|
|
||||||
|
|
||||||
use iris::prelude::*;
|
|
||||||
use iris_core::{
|
|
||||||
MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
|
|
||||||
UiScalar, UiSpan,
|
|
||||||
};
|
|
||||||
use wgpu::{Color as GpuColor, *};
|
|
||||||
|
|
||||||
const SIZE: u32 = 1024;
|
|
||||||
const INSTANCES: usize = 200_000;
|
|
||||||
const FRAMES: u32 = 20;
|
|
||||||
/// Reported as the best of this many batches, since the mean moves by more
|
|
||||||
/// than the thing being measured.
|
|
||||||
const BATCHES: u32 = 8;
|
|
||||||
|
|
||||||
fn gpu() -> Option<(Device, Queue, f32)> {
|
|
||||||
let all = Instance::new(InstanceDescriptor::new_without_display_handle());
|
|
||||||
let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default()))
|
|
||||||
{
|
|
||||||
Ok(_) => all,
|
|
||||||
Err(_) => Instance::new(InstanceDescriptor {
|
|
||||||
backends: Backends::GL,
|
|
||||||
..InstanceDescriptor::new_without_display_handle()
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
let instance: &'static Instance = Box::leak(Box::new(instance));
|
|
||||||
let adapter =
|
|
||||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?;
|
|
||||||
if !adapter.features().contains(Features::TIMESTAMP_QUERY) {
|
|
||||||
println!("no timestamp queries on {:?}", adapter.get_info().name);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
println!("adapter: {:?}", adapter.get_info().name);
|
|
||||||
let (device, queue) = pollster::block_on(adapter.request_device(&DeviceDescriptor {
|
|
||||||
required_features: Features::TIMESTAMP_QUERY,
|
|
||||||
..Default::default()
|
|
||||||
}))
|
|
||||||
.ok()?;
|
|
||||||
let period = queue.get_timestamp_period();
|
|
||||||
Some((device, queue, period))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config(format: TextureFormat) -> SurfaceConfiguration {
|
|
||||||
SurfaceConfiguration {
|
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
||||||
format,
|
|
||||||
color_space: SurfaceColorSpace::Auto,
|
|
||||||
width: SIZE,
|
|
||||||
height: SIZE,
|
|
||||||
present_mode: PresentMode::Fifo,
|
|
||||||
desired_maximum_frame_latency: 2,
|
|
||||||
alpha_mode: CompositeAlphaMode::Auto,
|
|
||||||
view_formats: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A chain `depth` slots long, and instances that all resolve through its end.
|
|
||||||
fn fill(ui: &mut UiData, render: &mut UiRenderState, depth: usize) {
|
|
||||||
let kind = ui.primitives.kind::<RectPrimitive>();
|
|
||||||
let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id();
|
|
||||||
|
|
||||||
let mut slot = MoveIdx::NONE;
|
|
||||||
for _ in 0..depth {
|
|
||||||
slot = render.moves.push(slot, UiRegion::FULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
let px = |v: f32| UiScalar { rel: 0.0, abs: v };
|
|
||||||
for i in 0..INSTANCES {
|
|
||||||
let x = (i % (SIZE as usize / 2)) as f32 * 2.0;
|
|
||||||
let y = (i / (SIZE as usize / 2)) as f32;
|
|
||||||
render.layers.write(
|
|
||||||
0,
|
|
||||||
PrimitiveInst {
|
|
||||||
kind,
|
|
||||||
id,
|
|
||||||
primitive: RectPrimitive::color(UiColor::WHITE),
|
|
||||||
region: UiRegion::new(
|
|
||||||
UiSpan::new(px(x), px(x + 2.0)),
|
|
||||||
UiSpan::new(px(y), px(y + 1.0)),
|
|
||||||
),
|
|
||||||
mask_idx: MaskIdx::NONE,
|
|
||||||
move_idx: slot,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Nanoseconds the pass took on the GPU, best of `BATCHES`.
|
|
||||||
fn pass_cost(device: &Device, queue: &Queue, period: f32, depth: usize) -> f64 {
|
|
||||||
let format = TextureFormat::Bgra8Unorm;
|
|
||||||
let mut node = UiRenderNode::new(device, &config(format));
|
|
||||||
let mut ui = UiData::default();
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
fill(&mut ui, &mut render, depth);
|
|
||||||
node.update(device, queue, &mut ui, &mut render);
|
|
||||||
|
|
||||||
let target = device.create_texture(&TextureDescriptor {
|
|
||||||
label: Some("chain cost"),
|
|
||||||
size: Extent3d {
|
|
||||||
width: SIZE,
|
|
||||||
height: SIZE,
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
},
|
|
||||||
mip_level_count: 1,
|
|
||||||
sample_count: 1,
|
|
||||||
dimension: TextureDimension::D2,
|
|
||||||
format,
|
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
||||||
view_formats: &[],
|
|
||||||
});
|
|
||||||
let view = target.create_view(&TextureViewDescriptor::default());
|
|
||||||
|
|
||||||
let queries = device.create_query_set(&QuerySetDescriptor {
|
|
||||||
label: Some("chain cost"),
|
|
||||||
ty: QueryType::Timestamp,
|
|
||||||
count: 2,
|
|
||||||
});
|
|
||||||
let resolved = device.create_buffer(&BufferDescriptor {
|
|
||||||
label: Some("resolved"),
|
|
||||||
size: 16,
|
|
||||||
usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
let readback = device.create_buffer(&BufferDescriptor {
|
|
||||||
label: Some("readback"),
|
|
||||||
size: 16,
|
|
||||||
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
let frame = || {
|
|
||||||
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
|
||||||
{
|
|
||||||
let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
|
||||||
label: None,
|
|
||||||
color_attachments: &[Some(RenderPassColorAttachment {
|
|
||||||
view: &view,
|
|
||||||
resolve_target: None,
|
|
||||||
ops: Operations {
|
|
||||||
load: LoadOp::Clear(GpuColor::BLACK),
|
|
||||||
store: StoreOp::Store,
|
|
||||||
},
|
|
||||||
depth_slice: None,
|
|
||||||
})],
|
|
||||||
depth_stencil_attachment: None,
|
|
||||||
timestamp_writes: Some(RenderPassTimestampWrites {
|
|
||||||
query_set: &queries,
|
|
||||||
beginning_of_pass_write_index: Some(0),
|
|
||||||
end_of_pass_write_index: Some(1),
|
|
||||||
}),
|
|
||||||
occlusion_query_set: None,
|
|
||||||
multiview_mask: None,
|
|
||||||
});
|
|
||||||
node.draw(pass);
|
|
||||||
}
|
|
||||||
encoder.resolve_query_set(&queries, 0..2, &resolved, 0);
|
|
||||||
encoder.copy_buffer_to_buffer(&resolved, 0, &readback, 0, 16);
|
|
||||||
queue.submit(Some(encoder.finish()));
|
|
||||||
|
|
||||||
let slice = readback.slice(..);
|
|
||||||
slice.map_async(MapMode::Read, |_| {});
|
|
||||||
let _ = device.poll(PollType::Wait {
|
|
||||||
submission_index: None,
|
|
||||||
timeout: None,
|
|
||||||
});
|
|
||||||
let ns = {
|
|
||||||
let view = slice.get_mapped_range().expect("timestamps did not map");
|
|
||||||
let stamps: [u64; 2] = [
|
|
||||||
u64::from_le_bytes(view[..8].try_into().unwrap()),
|
|
||||||
u64::from_le_bytes(view[8..16].try_into().unwrap()),
|
|
||||||
];
|
|
||||||
(stamps[1].saturating_sub(stamps[0])) as f64 * period as f64
|
|
||||||
};
|
|
||||||
readback.unmap();
|
|
||||||
ns
|
|
||||||
};
|
|
||||||
|
|
||||||
frame();
|
|
||||||
let mut best = f64::MAX;
|
|
||||||
for _ in 0..BATCHES {
|
|
||||||
let mut total = 0.0;
|
|
||||||
for _ in 0..FRAMES {
|
|
||||||
total += frame();
|
|
||||||
}
|
|
||||||
best = best.min(total / FRAMES as f64);
|
|
||||||
}
|
|
||||||
best
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "measurement, not a check"]
|
|
||||||
fn chain_cost_by_depth() {
|
|
||||||
let Some((device, queue, period)) = gpu() else {
|
|
||||||
println!("no gpu with timestamps; nothing measured");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
println!("{INSTANCES} instances, {SIZE}x{SIZE}, best of {BATCHES} batches");
|
|
||||||
let mut base = None;
|
|
||||||
for depth in [1, 2, 4, 8, 16, 32, 64] {
|
|
||||||
let ns = pass_cost(&device, &queue, period, depth);
|
|
||||||
let base = *base.get_or_insert(ns);
|
|
||||||
println!(
|
|
||||||
"depth {depth:>3}: {:>9.1} us {:+6.1}% against depth 1",
|
|
||||||
ns / 1000.0,
|
|
||||||
(ns - base) / base * 100.0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-5
@@ -22,8 +22,8 @@ use std::time::Instant;
|
|||||||
|
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use iris_core::{
|
use iris_core::{
|
||||||
GlyphPrimitive, MaskIdx, MoveIdx, PrimitiveInst, RectPrimitive, TextureHandle,
|
GlyphPrimitive, MaskIdx, PrimitiveInst, RectPrimitive, TextureHandle, TexturePrimitive, UiData,
|
||||||
TexturePrimitive, UiData, UiRegion, UiRenderNode, UiRenderState,
|
UiRegion, UiRenderNode, UiRenderState,
|
||||||
};
|
};
|
||||||
use wgpu::{Color as GpuColor, *};
|
use wgpu::{Color as GpuColor, *};
|
||||||
|
|
||||||
@@ -95,7 +95,6 @@ fn fill(
|
|||||||
primitive: RectPrimitive::color(UiColor::WHITE),
|
primitive: RectPrimitive::color(UiColor::WHITE),
|
||||||
region: UiRegion::FULL,
|
region: UiRegion::FULL,
|
||||||
mask_idx: MaskIdx::NONE,
|
mask_idx: MaskIdx::NONE,
|
||||||
move_idx: MoveIdx::NONE,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
render.layers.write(
|
render.layers.write(
|
||||||
@@ -112,7 +111,6 @@ fn fill(
|
|||||||
},
|
},
|
||||||
region: UiRegion::FULL,
|
region: UiRegion::FULL,
|
||||||
mask_idx: MaskIdx::NONE,
|
mask_idx: MaskIdx::NONE,
|
||||||
move_idx: MoveIdx::NONE,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -125,7 +123,6 @@ fn fill(
|
|||||||
primitive: TexturePrimitive::from(h),
|
primitive: TexturePrimitive::from(h),
|
||||||
region: UiRegion::FULL,
|
region: UiRegion::FULL,
|
||||||
mask_idx: MaskIdx::NONE,
|
mask_idx: MaskIdx::NONE,
|
||||||
move_idx: MoveIdx::NONE,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,408 +0,0 @@
|
|||||||
//! Random trees, checked against building the same tree cold.
|
|
||||||
//!
|
|
||||||
//! A frame reaches its layout by keeping most of the last one: slots
|
|
||||||
//! rewritten, some widgets drawn again, the rest untouched. The property here
|
|
||||||
//! is that what comes out is the tree a cold start would have produced, so
|
|
||||||
//! anything the retained path carried over that it should not have shows up
|
|
||||||
//! as a difference in somebody's box.
|
|
||||||
//!
|
|
||||||
//! `iris::random` grows the tree and `examples/random.rs` draws one. A seed is
|
|
||||||
//! the whole reproduction; `a_long_run_of_seeds_agrees` is the ignored sweep
|
|
||||||
//! for when it is worth spending the time.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
|
||||||
use iris::prelude::*;
|
|
||||||
use iris::random::{Edits, Lens, Rng, SpanEdit, Tree, grow};
|
|
||||||
|
|
||||||
const DEPTH: usize = 4;
|
|
||||||
const SEEDS: [u64; 7] = [1, 2, 3, 5, 8, 13, 98];
|
|
||||||
const REGION_EPSILON_PX: f32 = 0.05;
|
|
||||||
|
|
||||||
fn same_coordinate(got: f32, want: f32) -> bool {
|
|
||||||
(got - want).abs() <= REGION_EPSILON_PX
|
|
||||||
}
|
|
||||||
|
|
||||||
fn same_region(got: Option<PixelRegion>, want: Option<PixelRegion>) -> bool {
|
|
||||||
match (got, want) {
|
|
||||||
(Some(got), Some(want)) => {
|
|
||||||
same_coordinate(got.top_left.x, want.top_left.x)
|
|
||||||
&& same_coordinate(got.top_left.y, want.top_left.y)
|
|
||||||
&& same_coordinate(got.bot_right.x, want.bot_right.x)
|
|
||||||
&& same_coordinate(got.bot_right.y, want.bot_right.y)
|
|
||||||
}
|
|
||||||
(None, None) => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn plant(h: &mut Harness, seed: u64, edits: &Edits) -> Tree {
|
|
||||||
let (root, tree) = grow(&mut h.rsc, seed, DEPTH, edits);
|
|
||||||
h.state.root = Some(root);
|
|
||||||
h.frame();
|
|
||||||
tree
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resize_one(h: &mut Harness, tree: &Tree, idx: usize, rng: &mut Rng) -> Lens {
|
|
||||||
let lens = [
|
|
||||||
Some(Len::abs(20.0 + rng.below(180) as f32)),
|
|
||||||
Some(Len::abs(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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Every widget in one tree against the matching widget in the other. A
|
|
||||||
/// mismatch prints the widget's ancestry, marking the ones that own a slot,
|
|
||||||
/// since where two trees disagree is rarely where the cause is.
|
|
||||||
fn assert_same(seed: u64, what: &str, warm: (&Harness, &Tree), cold: (&Harness, &Tree)) {
|
|
||||||
let ((wh, wt), (ch, ct)) = (warm, cold);
|
|
||||||
assert_eq!(wt.ids.len(), ct.ids.len(), "seed {seed}: different trees");
|
|
||||||
let mut drawn = 0;
|
|
||||||
let mut wrong = 0;
|
|
||||||
for (i, (&w, &c)) in wt.ids.iter().zip(&ct.ids).enumerate() {
|
|
||||||
let (got, want) = (wh.region(&w), ch.region(&c));
|
|
||||||
drawn += usize::from(got.is_some());
|
|
||||||
// This oracle cares where rasterization lands, not whether equivalent
|
|
||||||
// arithmetic produced the same f32. Keep the tolerance to one
|
|
||||||
// twentieth of a physical pixel, while whether a widget drew remains
|
|
||||||
// exact.
|
|
||||||
if same_region(got, want) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
wrong += 1;
|
|
||||||
if wrong <= 3 {
|
|
||||||
let mut chain = Vec::new();
|
|
||||||
let mut at = Some(w);
|
|
||||||
while let Some(id) = at {
|
|
||||||
let active = &wh.render.active[&id];
|
|
||||||
let slot = match active.move_idx == active.parent_move {
|
|
||||||
true => "",
|
|
||||||
false => "*",
|
|
||||||
};
|
|
||||||
chain.push(format!("{}{slot}", wh.rsc.widgets().label(id)));
|
|
||||||
at = active.parent;
|
|
||||||
}
|
|
||||||
println!(
|
|
||||||
"seed {seed} after {what}: widget {i}\n warm {got:?}\n cold {want:?}\n {}",
|
|
||||||
chain.join(" < ")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(drawn > 0, "seed {seed}: nothing was drawn");
|
|
||||||
assert_eq!(wrong, 0, "seed {seed}: {wrong} widgets differ after {what}");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn changed_size(seed: u64) {
|
|
||||||
let mut warm = Harness::new((900, 1200));
|
|
||||||
let grown = plant(&mut warm, seed, &Edits::default());
|
|
||||||
|
|
||||||
let mut rng = Rng::new(seed ^ 0x5eed);
|
|
||||||
let sizes = edit(&mut warm, &grown, &mut rng);
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
let mut cold = Harness::new((900, 1200));
|
|
||||||
let same = plant(
|
|
||||||
&mut cold,
|
|
||||||
seed,
|
|
||||||
&Edits {
|
|
||||||
sizes,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_same(seed, "a size change", (&warm, &grown), (&cold, &same));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reshuffled(seed: u64, shuffle: Shuffle) {
|
|
||||||
let mut warm = Harness::new((900, 1200));
|
|
||||||
let mut grown = plant(&mut warm, seed, &Edits::default());
|
|
||||||
// Some seeds grow nothing but wrappers, and a shuffle with no span to
|
|
||||||
// shuffle is not the same thing as one that had no effect.
|
|
||||||
if grown.spans.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let before: Vec<_> = grown.ids.iter().map(|id| warm.region(id)).collect();
|
|
||||||
|
|
||||||
let (spans, _held) = reshuffle(&mut warm, &mut grown, shuffle);
|
|
||||||
warm.frame();
|
|
||||||
|
|
||||||
// Or the two trees would agree for want of anything having happened.
|
|
||||||
let after = grown.ids.iter().map(|id| warm.region(id));
|
|
||||||
let moved = before.iter().zip(after).filter(|(a, b)| *a != b).count();
|
|
||||||
assert!(moved > 0, "seed {seed}: {shuffle:?} changed nothing");
|
|
||||||
|
|
||||||
let mut cold = Harness::new((900, 1200));
|
|
||||||
let same = plant(
|
|
||||||
&mut cold,
|
|
||||||
seed,
|
|
||||||
&Edits {
|
|
||||||
spans,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let what = format!("{shuffle:?}");
|
|
||||||
assert_same(seed, &what, (&warm, &grown), (&cold, &same));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn 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());
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reproduces a divergence that predates the position chain: laying a tree out
|
|
||||||
/// again does not always land where growing it cold does.
|
|
||||||
///
|
|
||||||
/// Every one seen so far is a wrapping text on a span's *own* axis, where the
|
|
||||||
/// two draws do not agree. The span measures the child in the whole box, the
|
|
||||||
/// child shapes to that width and reports the width it used, the span then
|
|
||||||
/// places it in exactly that width -- which is a length change, so the child
|
|
||||||
/// shapes again, and its longest line is shorter than the box it was just
|
|
||||||
/// given. Each pass narrows it, so where the tree ends up depends on how many
|
|
||||||
/// passes it has had, and a warm tree has had a different number from a cold
|
|
||||||
/// one. Layout is supposed to be a function of the state alone.
|
|
||||||
///
|
|
||||||
/// A span whose axis is not the wrap axis is stable, which is every real
|
|
||||||
/// column of text, and why nothing else has run into this.
|
|
||||||
///
|
|
||||||
/// 7 of these 90 diverge on `db1751f`, before the chain; 30 do with it, since
|
|
||||||
/// a placed child reaches the second shaping more often. Both numbers are the
|
|
||||||
/// same defect, and it wants fixing where the two draws meet -- LAYOUT.md §4 --
|
|
||||||
/// rather than anywhere in the chain.
|
|
||||||
#[test]
|
|
||||||
#[ignore = "a hundred seeds, rather than the seven the others check"]
|
|
||||||
fn a_long_run_of_seeds_agrees() {
|
|
||||||
let seeds = std::env::var("IRIS_GENERATED_SEED")
|
|
||||||
.ok()
|
|
||||||
.and_then(|seed| seed.parse().ok())
|
|
||||||
.map(|seed| seed..=seed)
|
|
||||||
.unwrap_or(1..=100);
|
|
||||||
for seed in seeds {
|
|
||||||
changed_size(seed);
|
|
||||||
changed_every_size(seed);
|
|
||||||
repainted_together(seed);
|
|
||||||
resized(seed);
|
|
||||||
resized_then_changed(seed);
|
|
||||||
for shuffle in SHUFFLES {
|
|
||||||
reshuffled(seed, shuffle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-150
@@ -32,153 +32,3 @@ fn resizing_relays_out_against_the_new_output() {
|
|||||||
assert_corners!(h, left, (0, 0), (100, 100));
|
assert_corners!(h, left, (0, 0), (100, 100));
|
||||||
assert_corners!(h, right, (100, 0), (800, 100));
|
assert_corners!(h, right, (100, 0), (800, 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn an_empty_widget_takes_a_share_of_a_span() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let gap = ().add(&mut h.rsc);
|
|
||||||
let right = rect(Color::BLUE).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((gap, right).span(Dir::RIGHT));
|
|
||||||
|
|
||||||
assert_corners!(h, gap, (0, 0), (300, 200));
|
|
||||||
assert_corners!(h, right, (300, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_child_drawn_twice_moves_once() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
// `Aligned` draws its child twice; listing it twice would move it twice.
|
|
||||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
let centered = inner.center().width(200).add(&mut h.rsc);
|
|
||||||
let left = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((left, centered).span(Dir::RIGHT));
|
|
||||||
assert_corners!(h, inner, (100, 0), (300, 200));
|
|
||||||
|
|
||||||
h.rsc[left].x = Some(Len::abs(150));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, inner, (150, 0), (350, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_resize_lands_where_a_cold_start_would() {
|
|
||||||
let build = |h: &mut Harness| {
|
|
||||||
let para = wtext(
|
|
||||||
"Wrapping shapes one source into as many lines as its container leaves room \
|
|
||||||
for, so the height of a paragraph is an answer rather than a setting.",
|
|
||||||
)
|
|
||||||
.size(20)
|
|
||||||
.wrap(true)
|
|
||||||
.pad(16)
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let below = rect(Color::RED).add(&mut h.rsc);
|
|
||||||
let root = (para, below).span(Dir::DOWN).pad(12);
|
|
||||||
h.set_root(root);
|
|
||||||
(para, below)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut cold = Harness::new((900, 1200));
|
|
||||||
let (cold_para, cold_below) = build(&mut cold);
|
|
||||||
|
|
||||||
let mut resized = Harness::new((1920, 1200));
|
|
||||||
let (para, below) = build(&mut resized);
|
|
||||||
resized.resize((900, 1200));
|
|
||||||
resized.frame();
|
|
||||||
|
|
||||||
assert_eq!(resized.region(¶), cold.region(&cold_para), "paragraph");
|
|
||||||
assert_eq!(resized.region(&below), cold.region(&cold_below), "below");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_fixed_box_is_drawn_again_rather_than_stretched() {
|
|
||||||
let mut h = Harness::new((400, 400));
|
|
||||||
// The panel fills a stack sized by its sibling, so it is drawn in the
|
|
||||||
// whole box and then placed in the shorter one. Reusing it in that fixed
|
|
||||||
// box afterwards would leave it whatever height it happened to have.
|
|
||||||
let panel = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
let leaf = rect(Color::RED).height(100).add(&mut h.rsc);
|
|
||||||
let stack = (panel, leaf)
|
|
||||||
.stack()
|
|
||||||
.size(StackSize::Child(1))
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(stack.align(Align::TOP));
|
|
||||||
assert_corners!(h, panel, (0, 0), (400, 100));
|
|
||||||
|
|
||||||
h.rsc[leaf].y = Some(Len::abs(250));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, panel, (0, 0), (400, 250));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_moved_subtree_takes_its_children_with_it() {
|
|
||||||
let mut h = Harness::new((400, 400));
|
|
||||||
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
|
||||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
let row = inner.pad(10).height(40).add(&mut h.rsc);
|
|
||||||
h.set_root((first, row).span(Dir::DOWN));
|
|
||||||
assert_corners!(h, inner, (10, 50), (390, 70));
|
|
||||||
|
|
||||||
h.rsc[first].y = Some(Len::abs(80));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
// The row is the same shape somewhere else, so one slot moved it and
|
|
||||||
// `inner`'s own region was never rewritten.
|
|
||||||
assert_corners!(h, inner, (10, 90), (390, 110));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc);
|
|
||||||
let rest = rect(Color::GREEN).add(&mut h.rsc);
|
|
||||||
let panel = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
// Changing the bar's width is the only thing that changes the box the
|
|
||||||
// panel and everything under it was drawn for.
|
|
||||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((bar, panel).span(Dir::RIGHT));
|
|
||||||
assert_corners!(h, fixed, (100, 0), (150, 200));
|
|
||||||
assert_corners!(h, rest, (150, 0), (400, 200));
|
|
||||||
|
|
||||||
h.rsc[bar].x = Some(Len::abs(200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
// The panel's box is 100 shorter, so the fixed child is the same 50 wide
|
|
||||||
// against its new start and the one taking the rest absorbs the change.
|
|
||||||
assert_corners!(h, fixed, (200, 0), (250, 200));
|
|
||||||
assert_corners!(h, rest, (250, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
// The row is 40 tall whatever happens, which used to make its drawing
|
|
||||||
// impossible to take out of: recovering a fraction of a box needs a
|
|
||||||
// relative extent, and it has none on that axis.
|
|
||||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
let row = inner.pad(10).height(40).add(&mut h.rsc);
|
|
||||||
let filler = rect(Color::GREEN).add(&mut h.rsc);
|
|
||||||
let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc);
|
|
||||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((bar, column).span(Dir::RIGHT));
|
|
||||||
assert_corners!(h, inner, (110, 10), (390, 30));
|
|
||||||
|
|
||||||
h.rsc[bar].x = Some(Len::abs(200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, inner, (210, 10), (390, 30));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn only_a_container_that_places_its_children_lengthens_the_chain() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let leaf = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
// Four widgets between the span and the leaf, none of which places what
|
|
||||||
// it draws, so all of them share the span's slot.
|
|
||||||
let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc);
|
|
||||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((bar, buried).span(Dir::RIGHT));
|
|
||||||
|
|
||||||
let slot = h.render.active[&leaf.id()].parent_move;
|
|
||||||
assert_eq!(h.render.moves.depth(slot), 1, "one span above the leaf");
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
//! Retained CPU-layout diagnostics on one reproducible random tree.
|
|
||||||
//!
|
|
||||||
//! Counters and phase timers:
|
|
||||||
//!
|
|
||||||
//! cargo test --release --features layout-diagnostics \
|
|
||||||
//! --test layout_diagnostics -- --ignored --nocapture
|
|
||||||
//!
|
|
||||||
//! Uninstrumented hardware totals for one phase:
|
|
||||||
//!
|
|
||||||
//! IRIS_PHASE=resize IRIS_FRAMES=1000 perf stat \
|
|
||||||
//! -e cycles:u,instructions:u cargo test --release \
|
|
||||||
//! --test layout_diagnostics -- --ignored --nocapture
|
|
||||||
//!
|
|
||||||
//! `IRIS_PHASE` is `cold`, `repaint`, `many`, `size`, `scroll`, `resize`, or
|
|
||||||
//! `all`. `IRIS_SEED`, `IRIS_DEPTH`, and `IRIS_FRAMES` select the load, and
|
|
||||||
//! `IRIS_DIRTY` how many widgets `many` marks at once.
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
|
||||||
use iris::prelude::*;
|
|
||||||
use iris::random::{Edits, Tree, grow};
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
const OUTPUT: (f32, f32) = (1920.0, 1200.0);
|
|
||||||
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
#[test]
|
|
||||||
fn a_selected_widget_retains_its_layout_events() {
|
|
||||||
use iris::core::layout_diagnostics::{self as diagnostics, TraceEvent};
|
|
||||||
|
|
||||||
diagnostics::clear_traced_widgets();
|
|
||||||
let _ = diagnostics::take();
|
|
||||||
let mut harness = Harness::new((400, 200));
|
|
||||||
let leaf = rect(Color::RED).add(&mut harness.rsc);
|
|
||||||
let other = rect(Color::BLUE).add(&mut harness.rsc);
|
|
||||||
let root = (leaf, other).span(Dir::RIGHT).add(&mut harness.rsc);
|
|
||||||
harness.set_root(root);
|
|
||||||
diagnostics::trace_widget(leaf.id());
|
|
||||||
let _ = diagnostics::take();
|
|
||||||
|
|
||||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(root.id());
|
|
||||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf.id());
|
|
||||||
harness.frame();
|
|
||||||
|
|
||||||
let report = diagnostics::take();
|
|
||||||
assert!(
|
|
||||||
report
|
|
||||||
.traces()
|
|
||||||
.iter()
|
|
||||||
.any(|event| matches!(event, TraceEvent::Placed { id, .. } if *id == leaf.id()))
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
report
|
|
||||||
.traces()
|
|
||||||
.iter()
|
|
||||||
.any(|event| matches!(event, TraceEvent::DrawRequest { id, .. } if *id == leaf.id()))
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
report
|
|
||||||
.traces()
|
|
||||||
.iter()
|
|
||||||
.any(|event| matches!(event, TraceEvent::SizeRead { id, .. } if *id == leaf.id()))
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
report
|
|
||||||
.traces()
|
|
||||||
.iter()
|
|
||||||
.any(|event| matches!(event, TraceEvent::SizeReported { id, .. } if *id == leaf.id()))
|
|
||||||
);
|
|
||||||
diagnostics::clear_traced_widgets();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|
||||||
std::env::var(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse().ok())
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
fn trace_selected(tree: &Tree) {
|
|
||||||
let Ok(value) = std::env::var("IRIS_TRACE_INDEX") else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let index = value
|
|
||||||
.parse::<usize>()
|
|
||||||
.expect("IRIS_TRACE_INDEX must be a tree.ids index");
|
|
||||||
let id = tree.ids[index];
|
|
||||||
iris::core::layout_diagnostics::trace_widget(id);
|
|
||||||
println!("tracing tree.ids[{index}] = {id:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "layout-diagnostics"))]
|
|
||||||
fn trace_selected(_: &Tree) {}
|
|
||||||
|
|
||||||
fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
|
|
||||||
let mut harness = Harness::new(OUTPUT);
|
|
||||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
|
||||||
harness.state.root = Some(root);
|
|
||||||
harness.frame();
|
|
||||||
println!(
|
|
||||||
"fixture: seed {seed}, depth {depth}, {} widgets, {} active",
|
|
||||||
tree.ids.len(),
|
|
||||||
harness.render.active_widgets()
|
|
||||||
);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
let _ = iris::core::layout_diagnostics::take();
|
|
||||||
(harness, tree)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn report(label: &str, mut elapsed: Vec<f64>, _harness: &Harness) {
|
|
||||||
elapsed.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
||||||
let frames = elapsed.len();
|
|
||||||
// The worst frame is the stutter somebody sees, so it goes beside the
|
|
||||||
// median; p99 says whether it is the load or a single interruption.
|
|
||||||
println!(
|
|
||||||
"{label}: {frames} frame(s), min {:.3} ms, median {:.3} ms, p99 {:.3} ms, \
|
|
||||||
max {:.3} ms, total {:.1} ms",
|
|
||||||
elapsed[0],
|
|
||||||
elapsed[frames / 2],
|
|
||||||
elapsed[frames * 99 / 100],
|
|
||||||
elapsed[frames - 1],
|
|
||||||
elapsed.iter().sum::<f64>(),
|
|
||||||
);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
{
|
|
||||||
let diagnostics = iris::core::layout_diagnostics::take();
|
|
||||||
print!("{}", diagnostics.per_frame(frames));
|
|
||||||
for callsite in diagnostics.hot_text().iter().take(3) {
|
|
||||||
let mut ancestry = Vec::new();
|
|
||||||
let mut id = Some(callsite.id);
|
|
||||||
while let Some(widget) = id {
|
|
||||||
ancestry.push(_harness.rsc.widgets().label(widget).as_str());
|
|
||||||
id = _harness
|
|
||||||
.render
|
|
||||||
.active
|
|
||||||
.get(&widget)
|
|
||||||
.and_then(|active| active.parent);
|
|
||||||
}
|
|
||||||
println!(" text ancestry: {}", ancestry.join(" < "));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(
|
|
||||||
label: &str,
|
|
||||||
frames: usize,
|
|
||||||
harness: &mut Harness,
|
|
||||||
mut change: impl FnMut(&mut Harness, usize),
|
|
||||||
) {
|
|
||||||
let mut elapsed = Vec::with_capacity(frames);
|
|
||||||
for frame in 0..frames {
|
|
||||||
change(harness, frame);
|
|
||||||
let start = Instant::now();
|
|
||||||
harness.frame();
|
|
||||||
elapsed.push(start.elapsed().as_secs_f64() * 1_000.0);
|
|
||||||
}
|
|
||||||
report(label, elapsed, harness);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "measurement, not a check"]
|
|
||||||
fn layout_cost() {
|
|
||||||
let seed = env("IRIS_SEED", 1_u64);
|
|
||||||
let depth = env("IRIS_DEPTH", 7_usize);
|
|
||||||
let frames = env("IRIS_FRAMES", 100_usize);
|
|
||||||
assert!(frames > 0, "IRIS_FRAMES must be greater than zero");
|
|
||||||
let phase = env("IRIS_PHASE", String::from("all"));
|
|
||||||
assert!(
|
|
||||||
["all", "cold", "repaint", "many", "size", "scroll", "resize"].contains(&phase.as_str()),
|
|
||||||
"unknown IRIS_PHASE {phase:?}"
|
|
||||||
);
|
|
||||||
let selected = |name| phase == "all" || phase == name;
|
|
||||||
|
|
||||||
if selected("cold") {
|
|
||||||
let mut harness = Harness::new(OUTPUT);
|
|
||||||
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default());
|
|
||||||
harness.state.root = Some(root);
|
|
||||||
println!(
|
|
||||||
"fixture: seed {seed}, depth {depth}, {} widgets",
|
|
||||||
tree.ids.len()
|
|
||||||
);
|
|
||||||
trace_selected(&tree);
|
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
|
||||||
let _ = iris::core::layout_diagnostics::take();
|
|
||||||
run("cold", 1, &mut harness, |_, _| {});
|
|
||||||
drop(tree);
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected("repaint") {
|
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
|
||||||
trace_selected(&tree);
|
|
||||||
let leaf = tree.ids[0];
|
|
||||||
run("repaint", frames, &mut harness, move |harness, _| {
|
|
||||||
let _ = harness.rsc.widgets_mut().get_dyn_mut(leaf);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected("many") {
|
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
|
||||||
trace_selected(&tree);
|
|
||||||
// Spread through the tree rather than taken from one subtree, so the
|
|
||||||
// dependency paths the frame settles overlap.
|
|
||||||
let wanted = env("IRIS_DIRTY", 32_usize).max(1);
|
|
||||||
let step = (tree.ids.len() / wanted).max(1);
|
|
||||||
let dirty: Vec<_> = tree.ids.iter().copied().step_by(step).collect();
|
|
||||||
println!("marking {} of {} widgets", dirty.len(), tree.ids.len());
|
|
||||||
run("many", frames, &mut harness, move |harness, _| {
|
|
||||||
for &id in &dirty {
|
|
||||||
harness.rsc.widgets_mut().get_dyn_mut(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected("size") {
|
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
|
||||||
trace_selected(&tree);
|
|
||||||
let sized = tree.sized[0];
|
|
||||||
run("size", frames, &mut harness, move |harness, frame| {
|
|
||||||
harness.rsc[sized].x = Some(Len::abs(100.0 + (frame % 2) as f32 * 40.0));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected("scroll") {
|
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
|
||||||
trace_selected(&tree);
|
|
||||||
let scroll = tree.scrolls[0];
|
|
||||||
run("scroll", frames, &mut harness, move |harness, frame| {
|
|
||||||
harness.rsc[scroll].scroll(if frame % 2 == 0 { 12.0 } else { -12.0 });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected("resize") {
|
|
||||||
let (mut harness, tree) = warm(seed, depth);
|
|
||||||
trace_selected(&tree);
|
|
||||||
run("resize", frames, &mut harness, |harness, frame| {
|
|
||||||
harness.resize((OUTPUT.0 - ((frame + 1) % 2) as f32 * 8.0, OUTPUT.1));
|
|
||||||
});
|
|
||||||
drop(tree);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-26
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
use iris::harness::{Harness, TouchScript};
|
use iris::harness::Harness;
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -58,28 +58,3 @@ fn hover_ends_when_the_cursor_leaves_the_window() {
|
|||||||
h.leave();
|
h.leave();
|
||||||
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1));
|
assert_eq!((*hovered.borrow(), *ended.borrow()), (1, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_recorded_gesture_presses_where_it_says() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let clicks = Rc::new(RefCell::new(Vec::new()));
|
|
||||||
|
|
||||||
let (on_left, on_right) = (clicks.clone(), clicks.clone());
|
|
||||||
let left = rect(Color::RED)
|
|
||||||
.width(100)
|
|
||||||
.on(CursorSense::click(), move |_, _| {
|
|
||||||
on_left.borrow_mut().push("left")
|
|
||||||
})
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let right = rect(Color::BLUE)
|
|
||||||
.on(CursorSense::click(), move |_, _| {
|
|
||||||
on_right.borrow_mut().push("right")
|
|
||||||
})
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root((left, right).span(Dir::RIGHT));
|
|
||||||
|
|
||||||
let script = TouchScript::parse("0 down 300 100\n80 up 300 100").unwrap();
|
|
||||||
h.replay(&script);
|
|
||||||
|
|
||||||
assert_eq!(*clicks.borrow(), ["right"]);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
//! What re-placing a subtree costs per frame, as a load for a counter rather
|
|
||||||
//! than a check. A span of 200 fixed-height rows, five primitives each, with
|
|
||||||
//! the row above them changing height every frame, so every row below is
|
|
||||||
//! offered a box the same shape somewhere else.
|
|
||||||
//!
|
|
||||||
//! cargo test --release --test replace_cost -- --ignored
|
|
||||||
//! perf stat -e instructions:u target/release/.../replace_cost-* --ignored
|
|
||||||
//!
|
|
||||||
//! Wall time is the wrong number here; see `draw_cost.rs`. Measured on
|
|
||||||
//! 2026-09-14 at 1.98M instructions per frame, against 2.38M for rewriting
|
|
||||||
//! each row's regions instead and 7.13M for redrawing them.
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
const ROWS: usize = 200;
|
|
||||||
const FRAMES: usize = 200;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore = "measurement, not a check"]
|
|
||||||
fn replacing_rows_every_frame() {
|
|
||||||
let mut h = Harness::new((1920, 1200));
|
|
||||||
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
|
||||||
let mut span = Span::empty(Dir::DOWN);
|
|
||||||
span.push(first.add_strong(&mut h.rsc));
|
|
||||||
for i in 0..ROWS {
|
|
||||||
let row = (
|
|
||||||
rect(Color::BLUE.darker(i as f32 / (ROWS * 2) as f32)),
|
|
||||||
rect(Color::GREEN).pad(2),
|
|
||||||
wtext("row").size(16).pad(2),
|
|
||||||
)
|
|
||||||
.span(Dir::RIGHT)
|
|
||||||
.pad(4)
|
|
||||||
.height(40)
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
span.push(row.add_strong(&mut h.rsc));
|
|
||||||
}
|
|
||||||
h.set_root(span);
|
|
||||||
for i in 0..FRAMES {
|
|
||||||
h.rsc[first].y = Some(Len::abs(40.0 + (i % 2) as f32));
|
|
||||||
h.frame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,445 +0,0 @@
|
|||||||
//! What a second frame draws again, and what it keeps.
|
|
||||||
|
|
||||||
use std::{cell::Cell, rc::Rc};
|
|
||||||
|
|
||||||
use iris::harness::{Harness, assert_corners};
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
/// A leaf that counts its draws and reports whatever size it is given, so a
|
|
||||||
/// test can see what the retained path skipped.
|
|
||||||
struct Counted {
|
|
||||||
draws: Rc<Cell<usize>>,
|
|
||||||
size: Size,
|
|
||||||
dependence: OnResize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Counted {
|
|
||||||
fn draw(&mut self, _: &mut Painter) -> Size {
|
|
||||||
self.draws.set(self.draws.get() + 1);
|
|
||||||
self.size
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
self.dependence
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Counts(Rc<Cell<usize>>);
|
|
||||||
|
|
||||||
impl Counts {
|
|
||||||
fn get(&self) -> usize {
|
|
||||||
self.0.get()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn counted(h: &mut Harness, size: Size, dependence: OnResize) -> (WeakWidget<Counted>, Counts) {
|
|
||||||
let draws = Rc::new(Cell::new(0));
|
|
||||||
let id = Counted {
|
|
||||||
draws: draws.clone(),
|
|
||||||
size,
|
|
||||||
dependence,
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
(id, Counts(draws))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A fixed-width leaf beside one that takes the rest, so changing the first
|
|
||||||
/// hands the second a different box without the output changing.
|
|
||||||
fn pair(h: &mut Harness, rest: OnResize) -> (WeakWidget<Counted>, Counts, WidgetId) {
|
|
||||||
let (first, _) = counted(h, Size::from((100, 200)), OnResize::Translate);
|
|
||||||
let (second, draws) = counted(h, Size::REST, rest);
|
|
||||||
h.set_root((first, second).span(Dir::RIGHT));
|
|
||||||
(first, draws, second.id())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_leaf_that_ignores_its_box_is_not_drawn_again_when_the_box_changes() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (first, draws, second) = pair(&mut h, OnResize::Scale);
|
|
||||||
let settled = draws.get();
|
|
||||||
assert_corners!(h, second, (100, 0), (400, 200));
|
|
||||||
|
|
||||||
h.rsc[first].size = Size::from((150, 200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
draws.get(),
|
|
||||||
settled,
|
|
||||||
"its box is a field to write, not a reason to draw"
|
|
||||||
);
|
|
||||||
assert_corners!(h, second, (150, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_leaf_that_depends_on_its_box_is_drawn_again_when_the_box_changes() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (first, draws, second) = pair(&mut h, OnResize::Redraw);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.rsc[first].size = Size::from((150, 200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
// The preceding fixed child makes the remaining box this child's real
|
|
||||||
// box, so measuring it also draws it in its final place.
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
|
||||||
assert_corners!(h, second, (150, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_span_child_that_declares_its_length_is_drawn_once() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (told, told_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
|
|
||||||
let (asked, asked_draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
|
|
||||||
// The span takes one child's length from its hint and has to draw the
|
|
||||||
// other to find out, so only the second is drawn before it is placed.
|
|
||||||
let hinted = told.width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((hinted, asked).span(Dir::RIGHT));
|
|
||||||
|
|
||||||
assert_eq!(told_draws.get(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
asked_draws.get(),
|
|
||||||
2,
|
|
||||||
"drawn to be measured, then again to be placed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_span_relays_out_when_a_child_it_measured_changes() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (first, _, second) = pair(&mut h, OnResize::Translate);
|
|
||||||
|
|
||||||
h.rsc[first].size = Size::from((250, 200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, first, (0, 0), (250, 200));
|
|
||||||
assert_corners!(h, second, (250, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_repaint_that_keeps_its_size_does_not_relay_out() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (first, draws) = counted(&mut h, Size::from((100, 200)), OnResize::Translate);
|
|
||||||
let (second, _) = counted(&mut h, Size::REST, OnResize::Translate);
|
|
||||||
h.set_root((first, second).span(Dir::RIGHT));
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
// Taking mutable access is the ordinary content-change signal. This
|
|
||||||
// widget returns the same size, so the parent has nothing to lay out.
|
|
||||||
let _ = h.rsc.widgets_mut().get_dyn_mut(first.id());
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_placed_child_survives_the_next_frame() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
// Both children declare a length, so the span places them from their hints
|
|
||||||
// rather than drawing them to find out.
|
|
||||||
let top = rect(Color::RED).height(80).add(&mut h.rsc);
|
|
||||||
let bottom = rect(Color::BLUE).height(120).add(&mut h.rsc);
|
|
||||||
h.set_root((top, bottom).span(Dir::DOWN));
|
|
||||||
|
|
||||||
h.rsc.widgets_mut().get_dyn_mut(top.id());
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, top, (0, 0), (400, 80));
|
|
||||||
assert_corners!(h, bottom, (0, 80), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lays its child out from the hint alone, never reading what it drew.
|
|
||||||
struct FromHint {
|
|
||||||
inner: StrongWidget,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for FromHint {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
let len = painter.size_hint(&self.inner, Axis::Y).unwrap();
|
|
||||||
let mut region = UiRegion::FULL;
|
|
||||||
region.y.end = region.y.start.offset(len.abs);
|
|
||||||
painter.widget_within(&self.inner, region);
|
|
||||||
Size::REST
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_parent_that_only_read_a_hint_relays_out_when_the_hint_changes() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let inner = rect(Color::RED).height(80).add(&mut h.rsc);
|
|
||||||
let parent = FromHint {
|
|
||||||
inner: inner.add_strong(&mut h.rsc),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(parent);
|
|
||||||
assert_corners!(h, inner, (0, 0), (400, 80));
|
|
||||||
|
|
||||||
h.rsc[inner].y = Some(Len::abs(120));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, inner, (0, 0), (400, 120));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads the output's size, which nothing but its own draw can put right.
|
|
||||||
struct ReadsOutput {
|
|
||||||
draws: Rc<Cell<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for ReadsOutput {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
self.draws.set(self.draws.get() + 1);
|
|
||||||
Size::abs(painter.output_size() / 4.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ReadsWidth {
|
|
||||||
draws: Rc<Cell<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for ReadsWidth {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
self.draws.set(self.draws.get() + 1);
|
|
||||||
Size::abs((painter.output_len(Axis::X) / 4.0, 20.0).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_resize_does_not_redraw_what_the_shader_can_move() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (leaf, draws) = counted(&mut h, Size::REST, OnResize::Redraw);
|
|
||||||
h.set_root(leaf);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.resize((800, 100));
|
|
||||||
assert!(h.needs_redraw());
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
draws.get(),
|
|
||||||
settled,
|
|
||||||
"its box is the same fraction of a different output"
|
|
||||||
);
|
|
||||||
assert_corners!(h, leaf, (0, 0), (800, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_resize_redraws_what_read_the_output() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let draws = Rc::new(Cell::new(0));
|
|
||||||
let leaf = ReadsOutput {
|
|
||||||
draws: draws.clone(),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(leaf);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.resize((800, 100));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_resize_only_redraws_read_output_axes() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let draws = Rc::new(Cell::new(0));
|
|
||||||
let leaf = ReadsWidth {
|
|
||||||
draws: draws.clone(),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(leaf);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.resize((400, 300));
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled, "height was never read");
|
|
||||||
|
|
||||||
h.resize((800, 300));
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled + 1, "width changes its answer");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn subpixel_resize_changes_accumulate_from_the_last_layout() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let draws = Rc::new(Cell::new(0));
|
|
||||||
let leaf = ReadsWidth {
|
|
||||||
draws: draws.clone(),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(leaf);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
for width in [400.02, 400.04, 400.05] {
|
|
||||||
h.resize((width, 200.0));
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled);
|
|
||||||
}
|
|
||||||
|
|
||||||
h.resize((400.06, 200.0));
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn subpixel_box_changes_accumulate_from_the_last_draw() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let (first, draws, _) = pair(&mut h, OnResize::Redraw);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
for width in [100.02, 100.04, 100.05] {
|
|
||||||
h.rsc[first].size.x = Len::abs(width);
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled);
|
|
||||||
}
|
|
||||||
|
|
||||||
h.rsc[first].size.x = Len::abs(100.06);
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reporting_the_same_output_size_does_not_start_a_resize() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
let draws = Rc::new(Cell::new(0));
|
|
||||||
let leaf = ReadsOutput {
|
|
||||||
draws: draws.clone(),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root(leaf);
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.resize((400, 200));
|
|
||||||
|
|
||||||
assert!(!h.needs_redraw());
|
|
||||||
h.frame();
|
|
||||||
assert_eq!(draws.get(), settled);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn narrowing_the_output_reflows_text_and_relays_out_around_it() {
|
|
||||||
let mut h = Harness::new((600, 400));
|
|
||||||
let para = wtext(
|
|
||||||
"Wrapping shapes one source into as many lines as its container leaves \
|
|
||||||
room for, so the height of a paragraph is an answer rather than a setting.",
|
|
||||||
)
|
|
||||||
.size(20)
|
|
||||||
.wrap(true)
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
let below = rect(Color::RED).add(&mut h.rsc);
|
|
||||||
h.set_root((para, below).span(Dir::DOWN));
|
|
||||||
let top = h.region(&below).expect("drew nothing").top_left.y;
|
|
||||||
|
|
||||||
h.resize((300, 400));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
let lower = h.region(&below).expect("drew nothing").top_left.y;
|
|
||||||
assert!(lower > top, "same words, half the width: {top} -> {lower}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_change_two_levels_under_its_reader_still_reaches_it() {
|
|
||||||
let mut h = Harness::new((400, 400));
|
|
||||||
// Every wrapper up to the outer pad read the size below it, so the outer
|
|
||||||
// pad is what draws again -- and the span it hands the box to is the same
|
|
||||||
// size as before, which is what lets a draw reuse its way past the leaf.
|
|
||||||
let (leaf, _) = counted(&mut h, Size::abs((100, 100).into()), OnResize::Redraw);
|
|
||||||
let padded = leaf.pad(10).add(&mut h.rsc);
|
|
||||||
let below = rect(Color::RED).add(&mut h.rsc);
|
|
||||||
h.set_root((padded, below).span(Dir::DOWN).pad(12));
|
|
||||||
assert_corners!(h, below, (12, 132), (388, 388));
|
|
||||||
|
|
||||||
h.rsc[leaf].size = Size::abs((100, 200).into());
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_corners!(h, below, (12, 232), (388, 388));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Claims its drawing survives its box changing length, and has a child so
|
|
||||||
/// that the walk looking for what does not has one to reach.
|
|
||||||
struct Stretchy {
|
|
||||||
inner: StrongWidget,
|
|
||||||
draws: Rc<Cell<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Stretchy {
|
|
||||||
fn draw(&mut self, painter: &mut Painter) -> Size {
|
|
||||||
self.draws.set(self.draws.get() + 1);
|
|
||||||
painter.widget(&self.inner).size()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_resize(&self, _: Axis) -> OnResize {
|
|
||||||
OnResize::Scale
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn stretching_a_subtree_carries_the_children_in_it() {
|
|
||||||
let mut h = Harness::new((400, 400));
|
|
||||||
let first = rect(Color::RED).height(40).add(&mut h.rsc);
|
|
||||||
let inner = rect(Color::BLUE).add(&mut h.rsc);
|
|
||||||
let draws = Rc::new(Cell::new(0));
|
|
||||||
let outer = Stretchy {
|
|
||||||
inner: inner.add_strong(&mut h.rsc),
|
|
||||||
draws: draws.clone(),
|
|
||||||
}
|
|
||||||
.add(&mut h.rsc);
|
|
||||||
h.set_root((first, outer).span(Dir::DOWN));
|
|
||||||
let settled = draws.get();
|
|
||||||
assert_corners!(h, inner, (0, 40), (400, 400));
|
|
||||||
|
|
||||||
h.rsc[first].y = Some(Len::abs(80));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
draws.get(),
|
|
||||||
settled,
|
|
||||||
"its drawing follows its box, rather than being made again"
|
|
||||||
);
|
|
||||||
assert_corners!(h, outer, (0, 80), (400, 400));
|
|
||||||
assert_corners!(h, inner, (0, 80), (400, 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_widened_row_redraws_what_reads_its_length_and_nothing_else() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
// What a transcript row is: something whose shaping depends on the width
|
|
||||||
// it is given, beside something that only has to be the right shape.
|
|
||||||
let (wraps, wrap_draws) = counted(&mut h, Size::REST, OnResize::Redraw);
|
|
||||||
let (backing, back_draws) = counted(&mut h, Size::REST, OnResize::Scale);
|
|
||||||
let row = (backing, wraps).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((bar, row).span(Dir::RIGHT));
|
|
||||||
let (settled_wrap, settled_back) = (wrap_draws.get(), back_draws.get());
|
|
||||||
|
|
||||||
h.rsc[bar].x = Some(Len::abs(200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
// The span reads every child's size, so redrawing one takes the span
|
|
||||||
// with it -- and the span then measures and places the redrawn child.
|
|
||||||
assert!(wrap_draws.get() > settled_wrap, "reads the width it got");
|
|
||||||
assert_eq!(back_draws.get(), settled_back, "only has to be the shape");
|
|
||||||
assert_corners!(h, backing, (200, 0), (300, 200));
|
|
||||||
assert_corners!(h, wraps, (300, 0), (400, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_declared_length_child_is_not_redrawn_when_the_box_around_it_grows() {
|
|
||||||
let mut h = Harness::new((400, 200));
|
|
||||||
// Its box is a fixed 80 wherever the row's edges end up, so drawing it
|
|
||||||
// again would be for a width it does not have. The declared width is what
|
|
||||||
// lets the span say that without drawing it: a width the span learnt by
|
|
||||||
// drawing the child in its own box is only an answer for that box.
|
|
||||||
let (counter, draws) = counted(&mut h, Size::from((80, 200)), OnResize::Redraw);
|
|
||||||
let fixed = counter.width(80).add(&mut h.rsc);
|
|
||||||
let (rest, _) = counted(&mut h, Size::REST, OnResize::Scale);
|
|
||||||
let row = (fixed, rest).span(Dir::RIGHT).add(&mut h.rsc);
|
|
||||||
let bar = rect(Color::RED).width(100).add(&mut h.rsc);
|
|
||||||
h.set_root((bar, row).span(Dir::RIGHT));
|
|
||||||
let settled = draws.get();
|
|
||||||
|
|
||||||
h.rsc[bar].x = Some(Len::abs(200));
|
|
||||||
h.frame();
|
|
||||||
|
|
||||||
assert_eq!(draws.get(), settled, "its own length did not change");
|
|
||||||
assert_corners!(h, fixed, (200, 0), (280, 200));
|
|
||||||
}
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
//! What a resize frame costs and what it holds, on a tree the revision before
|
|
||||||
//! #16 also builds.
|
|
||||||
//!
|
|
||||||
//! Deliberately written in the API subset `43ce8c7` and this branch share, so
|
|
||||||
//! the same source can be dropped into an old worktree and measured there:
|
|
||||||
//! that is the only like-for-like comparison with the code the retained
|
|
||||||
//! layout replaced. The random tree cannot carry one, because the generator
|
|
||||||
//! itself changed with the work.
|
|
||||||
//!
|
|
||||||
//! ROWS=40 FRAMES=500 cargo test --release --test revision_cost \
|
|
||||||
//! -- --ignored --nocapture resize_cost
|
|
||||||
//! ROWS=2000 cargo test --release --test revision_cost \
|
|
||||||
//! -- --ignored --nocapture text_memory
|
|
||||||
//!
|
|
||||||
//! Wall time on this machine varies with CPU frequency; take the number from
|
|
||||||
//! `perf stat -e instructions:u` on the test binary directly.
|
|
||||||
|
|
||||||
use iris::harness::Harness;
|
|
||||||
use iris::prelude::*;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
/// xorshift64, so one seed is one set of paragraphs on any machine.
|
|
||||||
struct Rng(u64);
|
|
||||||
|
|
||||||
impl Rng {
|
|
||||||
fn bits(&mut self) -> u64 {
|
|
||||||
self.0 ^= self.0 << 13;
|
|
||||||
self.0 ^= self.0 >> 7;
|
|
||||||
self.0 ^= self.0 << 17;
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
fn below(&mut self, n: usize) -> usize {
|
|
||||||
(self.bits() % n as u64) as usize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const WORDS: [&str; 24] = [
|
|
||||||
"wrapping",
|
|
||||||
"shapes",
|
|
||||||
"one",
|
|
||||||
"source",
|
|
||||||
"into",
|
|
||||||
"as",
|
|
||||||
"many",
|
|
||||||
"lines",
|
|
||||||
"as",
|
|
||||||
"the",
|
|
||||||
"box",
|
|
||||||
"leaves",
|
|
||||||
"room",
|
|
||||||
"for",
|
|
||||||
"paragraph",
|
|
||||||
"height",
|
|
||||||
"answer",
|
|
||||||
"setting",
|
|
||||||
"container",
|
|
||||||
"width",
|
|
||||||
"before",
|
|
||||||
"knows",
|
|
||||||
"measured",
|
|
||||||
"again",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// A run of its own words, so nothing here is fast for two texts being the
|
|
||||||
/// same string.
|
|
||||||
fn words(rng: &mut Rng, least: usize, most: usize) -> String {
|
|
||||||
let words = least + rng.below(most - least);
|
|
||||||
let mut out = String::new();
|
|
||||||
for _ in 0..words {
|
|
||||||
if !out.is_empty() {
|
|
||||||
out.push(' ');
|
|
||||||
}
|
|
||||||
out.push_str(WORDS[rng.below(WORDS.len())]);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
const OUTPUT: (f32, f32) = (900.0, 1200.0);
|
|
||||||
|
|
||||||
fn env<T: std::str::FromStr>(name: &str, fallback: T) -> T {
|
|
||||||
std::env::var(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.parse().ok())
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A row of a fixed-width rect beside a column of one wrapping and one
|
|
||||||
/// overflowing text: the shape that makes a container measure a child in a
|
|
||||||
/// box it will not keep.
|
|
||||||
fn build(h: &mut Harness, rows: usize) -> Vec<WidgetId> {
|
|
||||||
let mut rng = Rng(1);
|
|
||||||
let mut paragraphs = Vec::new();
|
|
||||||
let mut col = Span::empty(Dir::DOWN);
|
|
||||||
for _ in 0..rows {
|
|
||||||
let mut row = Span::empty(Dir::RIGHT);
|
|
||||||
row.push(
|
|
||||||
rect(Color::RED)
|
|
||||||
.width(Len::abs(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");
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user