Compare commits
161
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58ce74dd7d | ||
|
|
c55be21761 | ||
|
|
beb138632a | ||
|
|
aeb60e50f5 | ||
|
|
a904cf4f36 | ||
|
|
5642f2010a | ||
|
|
84dad211f5 | ||
|
|
add6774980 | ||
|
|
cadfba05dd | ||
|
|
38b3a81053 | ||
|
|
f6242aa33c | ||
|
|
a888717ee9 | ||
|
|
e8a5792dcb | ||
|
|
a30971e4c5 | ||
|
|
adbedaf264 | ||
|
|
23523eea29 | ||
|
|
1512d8418b | ||
|
|
0ef87ebfcf | ||
|
|
3091fb86df | ||
|
|
4328eac756 | ||
|
|
b842e4f474 | ||
|
|
49cec82c1b | ||
|
|
0954770ceb | ||
|
|
1956be3f3d | ||
|
|
34cafb6edc | ||
|
|
3bf22935ce | ||
|
|
e6ba570d07 | ||
|
|
0e107f0e89 | ||
|
|
f860f716e6 | ||
|
|
c44bd198ee | ||
|
|
a7307d95fd | ||
|
|
7601aa2a5d | ||
|
|
c330ecec2b | ||
|
|
39f7b08c6c | ||
|
|
2ed5503717 | ||
|
|
efb416bbc3 | ||
|
|
5fcace1bfa | ||
|
|
e44dea34b4 | ||
|
|
a0693acc56 | ||
|
|
25e456e0b5 | ||
|
|
53b00c68e9 | ||
|
|
a92c6acdbf | ||
|
|
c8beca5753 | ||
|
|
4bd8607968 | ||
|
|
ffd79f32d3 | ||
|
|
0e0d4af326 | ||
|
|
ea6dbae0dc | ||
|
|
32542d0c0b | ||
|
|
5b7800264d | ||
|
|
5f16617511 | ||
|
|
45a717695b | ||
|
|
d21a21524f | ||
|
|
e166e005dc | ||
|
|
38eba543f6 | ||
|
|
2bc6bdfc77 | ||
|
|
d8ae9c3bdd | ||
|
|
08c9d5aa32 | ||
|
|
60367d806e | ||
|
|
aea878d141 | ||
|
|
98d4e98a29 | ||
|
|
4febabfd2e | ||
|
|
394d5149a5 | ||
|
|
4cbb242a5d | ||
|
|
d75a1e2129 | ||
|
|
1940e85c70 | ||
|
|
cb1bba4682 | ||
|
|
490918b789 | ||
|
|
a8898aaa54 | ||
|
|
4f5e27cba9 | ||
|
|
11c55bcef9 | ||
|
|
f11f5f4825 | ||
|
|
97cc8b32ed | ||
|
|
95fb4f962c | ||
|
|
bdab55824f | ||
|
|
9d8415d65f | ||
|
|
cb955f1023 | ||
|
|
39e4ca20e6 | ||
|
|
bd6de71a55 | ||
|
|
4e28f1047e | ||
|
|
7548139861 | ||
|
|
5ed9e874a3 | ||
|
|
d3b0ebf90c | ||
|
|
8220a78d4a | ||
|
|
0283c9d6c7 | ||
|
|
71c9c39523 | ||
|
|
f437495309 | ||
|
|
29c7881c8a | ||
|
|
691e3eb23c | ||
|
|
9644971daf | ||
|
|
de9ddc0ad4 | ||
|
|
169db7f16f | ||
|
|
4063635f39 | ||
|
|
f61e8936f1 | ||
|
|
02ff8c7454 | ||
|
|
65f68bbb8a | ||
|
|
99131940ab | ||
|
|
e5a3e640d4 | ||
|
|
c596bf12c6 | ||
|
|
f0c7df06ac | ||
|
|
b7caab3b9e | ||
|
|
386a0d1b8f | ||
|
|
1b1378b05a | ||
|
|
60175c3821 | ||
|
|
b165164e59 | ||
|
|
ef815dadfd | ||
|
|
9f4311774b | ||
|
|
7c50a3e51b | ||
|
|
3f7cd8251b | ||
|
|
bf9438087a | ||
|
|
77bb75e5de | ||
|
|
2525637e26 | ||
|
|
e5f8b6b244 | ||
|
|
f1a47e9b7b | ||
|
|
b1b3eca1c0 | ||
|
|
82fa6c1123 | ||
|
|
480f0bc99f | ||
|
|
84f589e364 | ||
|
|
a640c6cce2 | ||
|
|
cdec29351a | ||
|
|
4178dfbff9 | ||
|
|
2272634dc5 | ||
|
|
b0f9f046da | ||
|
|
86a7e8dfc3 | ||
|
|
d98969158f | ||
|
|
1f9dc48b80 | ||
|
|
db1751fdfd | ||
|
|
78a53b6bf6 | ||
|
|
d8c497fcd7 | ||
|
|
c8ec0866d4 | ||
|
|
8223a55cfb | ||
|
|
f9ef7514e7 | ||
|
|
ca2b4b2173 | ||
|
|
f9423855e1 | ||
|
|
43ce8c7d02 | ||
|
|
c8ac669f95 | ||
|
|
32b10383d8 | ||
|
|
00d2230b84 | ||
|
|
b234497d21 | ||
|
|
0f6a28b4dd | ||
|
|
b90c855cf5 | ||
|
|
3b96324333 | ||
|
|
4767384b08 | ||
|
|
0191f2081b | ||
|
|
472736a292 | ||
|
|
6e271e8aee | ||
|
|
a1ff76776c | ||
|
|
cb9cad38f2 | ||
|
|
db9b0f21d5 | ||
|
|
2b6a6ab378 | ||
|
|
ec2b5d4c1d | ||
|
|
780ac82b27 | ||
|
|
465e43075e | ||
|
|
0c9a39fd06 | ||
|
|
936fbdd8ce | ||
|
|
3eaded125e | ||
|
|
23270e49fb | ||
|
|
bc6cdd13c9 | ||
|
|
072f1e31ad | ||
|
|
42753141b7 | ||
|
|
6884160bfe | ||
|
|
fae21a1991 |
No files matched your search
Generated
+306
-1273
File diff suppressed because it is too large.
Load diff
+23
-75
@@ -3,103 +3,51 @@ 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
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iris-core = { workspace = true }
|
iris-core = { workspace = true }
|
||||||
iris-macro = { workspace = true }
|
iris-macro = { workspace = true }
|
||||||
parley = { workspace = true }
|
parley = { workspace = true }
|
||||||
swash = { workspace = true }
|
winit = { workspace = true }
|
||||||
|
arboard = { workspace = true, features = ["wayland-data-control"] }
|
||||||
pollster = { workspace = true }
|
pollster = { workspace = true }
|
||||||
wgpu = { workspace = true }
|
wgpu = { workspace = true }
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
accesskit = { workspace = true }
|
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
|
||||||
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
|
|
||||||
# The embedding app installs the logger.
|
|
||||||
log = "0.4.34"
|
|
||||||
|
|
||||||
# winit's Android backend conflicts with android-view, which owns that platform here.
|
|
||||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
|
||||||
winit = { workspace = true }
|
|
||||||
arboard = { workspace = true, features = ["wayland-data-control"] }
|
|
||||||
accesskit_winit = "0.34.0"
|
|
||||||
|
|
||||||
# Advancing this measured revision requires rechecking rendering, IME, and detach.
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
|
||||||
android-view = { git = "https://github.com/rust-mobile/android-view.git", rev = "bec6c62a96cef8239b0fd7fedeef9b184d02e3a1" }
|
|
||||||
# 0.8.0 still aborts on detach; `view.rs::raise_if_enabled` mitigates it.
|
|
||||||
accesskit_android = "0.8.0"
|
|
||||||
send_wrapper = "0.6.0"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
# Forces GL on Vulkan-capable hosts for comparisons. The emulator already falls back
|
|
||||||
# to hardware GLES; enabling this there would make its build unlike the phone's.
|
|
||||||
force-gles = []
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bytemuck = { workspace = true }
|
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "bench_images"
|
|
||||||
path = "examples/bench_images/desktop.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "message_list"
|
|
||||||
path = "examples/message_list/desktop.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "minimal"
|
|
||||||
path = "examples/minimal/desktop.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "tabs"
|
|
||||||
path = "examples/tabs/desktop.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "task"
|
|
||||||
path = "examples/task/desktop.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "text"
|
|
||||||
path = "examples/text/desktop.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "view"
|
|
||||||
path = "examples/view/desktop.rs"
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "message_list"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = ["core", "macro", "rig-input"]
|
||||||
"cargo-iris",
|
|
||||||
"core",
|
[profile.dev]
|
||||||
"macro",
|
debug = 1
|
||||||
"rig-input",
|
|
||||||
]
|
[profile.test]
|
||||||
|
debug = "line-tables-only"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# Full DWARF once produced 54 GB of writes and an 88 GB target because every test
|
|
||||||
# statically links the renderer stack. Use `RUSTFLAGS="-C debuginfo=2"` when needed.
|
|
||||||
[profile.dev]
|
|
||||||
debug = "line-tables-only"
|
|
||||||
|
|
||||||
[profile.test]
|
|
||||||
debug = "line-tables-only"
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
pollster = "1.0.1"
|
pollster = "0.4.0"
|
||||||
winit = "0.30.13"
|
winit = "0.30.12"
|
||||||
wgpu = "30.0.1"
|
wgpu = "30.0.1"
|
||||||
bytemuck = "1.25.2"
|
bytemuck = "1.23.1"
|
||||||
image = "0.25.10"
|
image = "0.25.10"
|
||||||
parley = "0.11.1"
|
parley = "0.11.1"
|
||||||
swash = "0.2.10"
|
swash = "0.2.10"
|
||||||
fxhash = "0.2.1"
|
fxhash = "0.2.1"
|
||||||
|
log = "0.4.29"
|
||||||
arboard = "3.6.1"
|
arboard = "3.6.1"
|
||||||
accesskit = "0.25.0"
|
|
||||||
iris-core = { path = "core" }
|
iris-core = { path = "core" }
|
||||||
iris-macro = { path = "macro" }
|
iris-macro = { path = "macro" }
|
||||||
tokio = "1.53.1"
|
tokio = "1.49.0"
|
||||||
|
wayland-client = "0.31.15"
|
||||||
|
wayland-protocols-wlr = { version = "0.3.12", features = ["client"] }
|
||||||
@@ -1,75 +1,26 @@
|
|||||||
# iris: known problems and things still to build
|
images
|
||||||
|
settings (sampler)
|
||||||
|
consider typed TextureHandle<T> variants for distinct texture uses
|
||||||
|
|
||||||
Only open Iris framework work lives here. Delete an item when it lands.
|
WidgetRef<W> or smth instead of Id
|
||||||
|
enum that's either an Id or an actual concrete instance of W
|
||||||
|
painter takes them in instead of (or in addition to) id
|
||||||
|
then type wrapper widgets to contain them
|
||||||
|
allows for compile time optimization if a widget wrapper's inner is known at compile time
|
||||||
|
and the id of inner is not needed anywhere
|
||||||
|
maybe introduce InnerWidget trait to allow for editors to expose & modify inner type
|
||||||
|
maybe could also store a parent widget and keep using InnerWidget trait? unsure if possible
|
||||||
|
|
||||||
## Build (for the port)
|
vecs for each widget type?
|
||||||
|
|
||||||
Framework capabilities needed by `RUST.md`'s port plan:
|
POTENTIAL BUG: closures that store IDs will not decrement the id!!! need to not increment id if moved into closure somehow??? wait no, need to decrement ID every time an event fn is added...... only if the id is used in it..??
|
||||||
|
|
||||||
- [ ] **Overflow ellipsis with an explicit retained end.** `TextAttrs` can
|
transforms on a move entry (scale + rotation)
|
||||||
only wrap or clip, so a tool summary is cut with no mark. Parley has no
|
an entry is a translation today; composing through one scales the rel
|
||||||
ellipsis primitive; use its line breaker to find the cut, but keep source
|
part and passes px through untouched, so fixed-size content and glyphs
|
||||||
and displayed strings distinct with one byte mapping shared by spans,
|
do not follow a shortened entry
|
||||||
links, selection and editing. Replace `wrap: bool` with an enum that can
|
want a real transform per entry, resolved in resolve_move the way the
|
||||||
say wrap, clip, head ellipsis and tail ellipsis—the caller must choose
|
translation already is, so a whole subtree transforms with one buffer
|
||||||
because a command is identified by its head and a path by its tail.
|
write and no redraw
|
||||||
- [ ] **Expose the distance from a `LazySpan` viewport to its unloaded
|
wanted for compose-style stretch at the end of a scroll area, and for
|
||||||
edge.** (**P1**.) `viewport_len` and the visible extents are already
|
rotation generally
|
||||||
measured internally, but a paging caller cannot ask whether it is within
|
|
||||||
the product's six-viewport `HISTORY_SCREENS` cushion. The API should
|
|
||||||
answer in pixels or viewport multiples, never rows: a row ranges from one
|
|
||||||
line to a screen, so a fixed row count is not a distance.
|
|
||||||
- [ ] **Let an image fit a bounded box while preserving its aspect ratio.**
|
|
||||||
(**P1**.) `Image` currently always reports and draws the decoded texture's
|
|
||||||
natural pixel size. Decoding and fetching a server-produced attachment
|
|
||||||
belong in `app`; iris only owes the generic fit/scale widget used to
|
|
||||||
draw its thumbnail.
|
|
||||||
- [ ] **Per-range backgrounds for rich text.** (**P1**.) Inline code is
|
|
||||||
already monospace and coloured, but the inline-code chip also needs
|
|
||||||
the glyph run's boxes so a surface can be drawn behind exactly that byte
|
|
||||||
range. The shared `TextSelection` engine already computes the same geometry
|
|
||||||
for selection highlights; expose one shared primitive rather than giving
|
|
||||||
the app a second text-layout path.
|
|
||||||
- [ ] **A horizontal gauge/bar widget.** (**P1**.) For
|
|
||||||
`SessionUsageBar`'s equivalent — a bounded fill reflecting a fraction,
|
|
||||||
nothing fancier.
|
|
||||||
- [ ] **A toggle switch.** (**P3**.) For the delete dialog's
|
|
||||||
`deleteForeign` control; iris has no switch/checkbox widget yet as far
|
|
||||||
as this pass found.
|
|
||||||
|
|
||||||
## Later
|
|
||||||
|
|
||||||
- [ ] **Intern independently constructed solid paint definitions.** Inline
|
|
||||||
`rect(Srgba8::...)` values currently receive a new `PaintId` each time.
|
|
||||||
Cache them by canonical linear RGBA bits, but keep `Paints::add` explicitly
|
|
||||||
unique so two semantic theme roles that start with the same value can later
|
|
||||||
change independently. Cache entries must be weak and disappear when the
|
|
||||||
last real handle releases the slot; gradients and texture paints need their
|
|
||||||
own identity rules rather than inheriting solid-value interning blindly.
|
|
||||||
|
|
||||||
- [ ] **Property/content animations.** Cosmetic, so after correctness and
|
|
||||||
parity. Keep them modular, like input; scrolling already animates through
|
|
||||||
`Widget::tick` and `UiData::animate`. A widget that does not opt in must
|
|
||||||
pay nothing and import nothing for them.
|
|
||||||
|
|
||||||
- [ ] **Remove `WidgetView` unless a real composite adopts it.** Every
|
|
||||||
composite in `app/src/ui` uses ordinary child handles plus a root;
|
|
||||||
`WidgetView` and its derive are used only by `iris/examples/view/lib.rs`.
|
|
||||||
It currently demonstrates itself rather than shortening production code.
|
|
||||||
|
|
||||||
- [ ] **A `Stack` that chooses its mask the way it chooses its size
|
|
||||||
should replace `masked_by`.** For a square-cornered surface,
|
|
||||||
`.background(rect(BAR_FILL)).masked()` was measured
|
|
||||||
against `.masked_by(rect(BAR_FILL))` on the composer at the phone's own
|
|
||||||
size and density and the two are identical to the pixel. What the pair
|
|
||||||
cannot express is a clip that is not a box: `Painter::set_mask` writes
|
|
||||||
a `RectPrimitive::color` using `PaintId::NONE` at the widget's own region,
|
|
||||||
with no radius, so `.background(rect(fill).radius(r)).masked()` draws a
|
|
||||||
rounded panel and then cuts its content square. Both other call sites
|
|
||||||
(`row.rs`'s fence, `tool.rs`'s raw output) are rounded, which is why
|
|
||||||
the method stands for now.
|
|
||||||
Let `Stack` name the mask child the way `StackSize::Child(n)` names the
|
|
||||||
sizing child. Then `.background(x)` remains the one way to add a surface
|
|
||||||
and clipping to it is a stack property; the named mask child must have
|
|
||||||
drawn before any child that uses it. Once that exists, delete
|
|
||||||
`masked_by` and `Masked::shape` rather than retaining two APIs.
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
struct BenchRsc {
|
|
||||||
ui: Ui,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UiRsc for BenchRsc {
|
|
||||||
fn ui(&self) -> &Ui {
|
|
||||||
&self.ui
|
|
||||||
}
|
|
||||||
fn ui_mut(&mut self) -> &mut Ui {
|
|
||||||
&mut self.ui
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const BODY: &str = "The quick brown fox jumps over the lazy dog. Iris lays \
|
|
||||||
out wrapped text by shaping once per width and caching the result, so a \
|
|
||||||
row that is offered the same width twice does not reshape. This sentence \
|
|
||||||
exists only to give a row enough text to wrap across several lines at a \
|
|
||||||
typical phone column width.";
|
|
||||||
|
|
||||||
fn build_row(rsc: &mut BenchRsc, i: usize, image_every: usize) -> StrongWidget {
|
|
||||||
let text = wtext(format!("Message {i}: {BODY}"))
|
|
||||||
.overflow(TextOverflow::Wrap)
|
|
||||||
.add_strong(rsc)
|
|
||||||
.any();
|
|
||||||
|
|
||||||
if image_every > 0 && i.is_multiple_of(image_every) {
|
|
||||||
let img = image::DynamicImage::new_rgba8(64, 64);
|
|
||||||
let image_widget = image::<BenchRsc>(img)(rsc);
|
|
||||||
let image_widget = rsc.ui.widgets.add_strong(image_widget).any();
|
|
||||||
let mut row = Span::empty(Dir::DOWN);
|
|
||||||
row.push(text);
|
|
||||||
row.push(image_widget);
|
|
||||||
rsc.ui.widgets.add_strong(row).any()
|
|
||||||
} else {
|
|
||||||
text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_message_list(
|
|
||||||
rsc: &mut BenchRsc,
|
|
||||||
n: usize,
|
|
||||||
image_every: usize,
|
|
||||||
) -> (WeakWidget<LazySpan>, StrongWidget) {
|
|
||||||
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
|
|
||||||
for i in 0..n {
|
|
||||||
let row = build_row(rsc, i, image_every);
|
|
||||||
list.push_back(LazyItem::new(i as u64, row));
|
|
||||||
}
|
|
||||||
let list = rsc.ui.widgets.add_strong(list);
|
|
||||||
(list.weak(), list.any())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn report(label: &str, elapsed: std::time::Duration, draws: u64, rewrites: u64, moves: u64) {
|
|
||||||
println!(
|
|
||||||
"{label}: {:.2}ms draws={draws} rewrites={rewrites} moves={moves}",
|
|
||||||
elapsed.as_secs_f64() * 1000.0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bench_first_frame(n: usize) {
|
|
||||||
let mut rsc = BenchRsc { ui: Ui::default() };
|
|
||||||
let (_list, root) = build_message_list(&mut rsc, n, 20);
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
render.resize((1080.0, 2000.0));
|
|
||||||
|
|
||||||
let start = Instant::now();
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
let elapsed = start.elapsed();
|
|
||||||
let RenderCounters {
|
|
||||||
draws,
|
|
||||||
region_rewrites: rewrites,
|
|
||||||
moves,
|
|
||||||
..
|
|
||||||
} = render.take_counters();
|
|
||||||
report(
|
|
||||||
&format!("(a) first frame, N={n}"),
|
|
||||||
elapsed,
|
|
||||||
draws,
|
|
||||||
rewrites,
|
|
||||||
moves,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bench_scroll(n: usize, ticks: usize) {
|
|
||||||
let mut rsc = BenchRsc { ui: Ui::default() };
|
|
||||||
let (scroll, root) = build_message_list(&mut rsc, n, 20);
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
render.resize((1080.0, 2000.0));
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0);
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
render.take_counters();
|
|
||||||
|
|
||||||
let mut total = std::time::Duration::ZERO;
|
|
||||||
let mut total_draws = 0u64;
|
|
||||||
let mut total_rewrites = 0u64;
|
|
||||||
let mut total_moves = 0u64;
|
|
||||||
for _ in 0..ticks {
|
|
||||||
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(-8.0);
|
|
||||||
let start = Instant::now();
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
total += start.elapsed();
|
|
||||||
let RenderCounters {
|
|
||||||
draws,
|
|
||||||
region_rewrites: rewrites,
|
|
||||||
moves,
|
|
||||||
..
|
|
||||||
} = render.take_counters();
|
|
||||||
total_draws += draws;
|
|
||||||
total_rewrites += rewrites;
|
|
||||||
total_moves += moves;
|
|
||||||
}
|
|
||||||
report(
|
|
||||||
&format!("(b) scroll, N={n}, {ticks} ticks (totals; expect draws/moves independent of N)"),
|
|
||||||
total,
|
|
||||||
total_draws,
|
|
||||||
total_rewrites,
|
|
||||||
total_moves,
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" per-tick average: {:.4}ms",
|
|
||||||
total.as_secs_f64() * 1000.0 / ticks as f64
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bench_input_grows(n: usize, lines: usize) {
|
|
||||||
let mut rsc = BenchRsc { ui: Ui::default() };
|
|
||||||
let (scroll, list_root) = build_message_list(&mut rsc, n, 20);
|
|
||||||
let list_area = rsc.ui.widgets.add_strong(Sized {
|
|
||||||
inner: list_root,
|
|
||||||
x: None,
|
|
||||||
y: Some(rest(1.0)),
|
|
||||||
});
|
|
||||||
|
|
||||||
let line_height = 24.0;
|
|
||||||
let input_rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
|
||||||
let input_area = rsc.ui.widgets.add_strong(Sized {
|
|
||||||
inner: input_rect.any(),
|
|
||||||
x: None,
|
|
||||||
y: Some(abs(line_height).into()),
|
|
||||||
});
|
|
||||||
|
|
||||||
let input_area_weak = input_area.weak();
|
|
||||||
let mut root_span = Span::empty(Dir::DOWN);
|
|
||||||
root_span.push(list_area.any());
|
|
||||||
root_span.push(input_area.any());
|
|
||||||
let root = rsc.ui.widgets.add_strong(root_span).any();
|
|
||||||
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
render.resize((1080.0, 2000.0));
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
rsc.ui.widgets.get_mut(&scroll).unwrap().scroll(0.0);
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
render.take_counters();
|
|
||||||
|
|
||||||
let mut total = std::time::Duration::ZERO;
|
|
||||||
let mut total_draws = 0u64;
|
|
||||||
let mut total_rewrites = 0u64;
|
|
||||||
let mut total_moves = 0u64;
|
|
||||||
for line in 1..=lines {
|
|
||||||
rsc.ui.widgets.get_mut(&input_area_weak).unwrap().y =
|
|
||||||
Some(abs(line_height * (line + 1) as f32).into());
|
|
||||||
let start = Instant::now();
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
total += start.elapsed();
|
|
||||||
let RenderCounters {
|
|
||||||
draws,
|
|
||||||
region_rewrites: rewrites,
|
|
||||||
moves,
|
|
||||||
..
|
|
||||||
} = render.take_counters();
|
|
||||||
total_draws += draws;
|
|
||||||
total_rewrites += rewrites;
|
|
||||||
total_moves += moves;
|
|
||||||
}
|
|
||||||
report(
|
|
||||||
&format!(
|
|
||||||
"(c) input grows by {lines} lines above N={n} rows (totals; \
|
|
||||||
draws/rewrites must not scale with N)"
|
|
||||||
),
|
|
||||||
total,
|
|
||||||
total_draws,
|
|
||||||
total_rewrites,
|
|
||||||
total_moves,
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" per-line average: {:.4}ms",
|
|
||||||
total.as_secs_f64() * 1000.0 / lines as f64
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bench_insert_above_anchor(n: usize, inserts: usize) {
|
|
||||||
let mut rsc = BenchRsc { ui: Ui::default() };
|
|
||||||
let (list, root) = build_message_list(&mut rsc, n, 20);
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
render.resize((1080.0, 2000.0));
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
rsc.ui.widgets.get_mut(&list).unwrap().jump_to_start();
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
render.take_counters();
|
|
||||||
|
|
||||||
let mut total = std::time::Duration::ZERO;
|
|
||||||
let mut total_draws = 0u64;
|
|
||||||
let mut total_rewrites = 0u64;
|
|
||||||
let mut total_moves = 0u64;
|
|
||||||
for i in 0..inserts {
|
|
||||||
let row = build_row(&mut rsc, usize::MAX - i, 20);
|
|
||||||
rsc.ui
|
|
||||||
.widgets
|
|
||||||
.get_mut(&list)
|
|
||||||
.unwrap()
|
|
||||||
.push_front(LazyItem::new(i as u64, row));
|
|
||||||
let start = Instant::now();
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
total += start.elapsed();
|
|
||||||
let RenderCounters {
|
|
||||||
draws,
|
|
||||||
region_rewrites: rewrites,
|
|
||||||
moves,
|
|
||||||
..
|
|
||||||
} = render.take_counters();
|
|
||||||
total_draws += draws;
|
|
||||||
total_rewrites += rewrites;
|
|
||||||
total_moves += moves;
|
|
||||||
}
|
|
||||||
report(
|
|
||||||
&format!(
|
|
||||||
"(d) insert-above-anchor, N={n}, {inserts} pushes (totals; \
|
|
||||||
must not scale with N)"
|
|
||||||
),
|
|
||||||
total,
|
|
||||||
total_draws,
|
|
||||||
total_rewrites,
|
|
||||||
total_moves,
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" per-push average: {:.4}ms",
|
|
||||||
total.as_secs_f64() * 1000.0 / inserts as f64
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bench_expand_holds_edge(n: usize, growths: usize) {
|
|
||||||
let mut rsc = BenchRsc { ui: Ui::default() };
|
|
||||||
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
|
|
||||||
let growable_index = n.saturating_sub(3);
|
|
||||||
let mut growable = None;
|
|
||||||
for i in 0..n {
|
|
||||||
if i == growable_index {
|
|
||||||
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
|
||||||
let sized = rsc.ui.widgets.add_strong(Sized {
|
|
||||||
inner: rect.any(),
|
|
||||||
x: None,
|
|
||||||
y: Some(abs(40.0).into()),
|
|
||||||
});
|
|
||||||
growable = Some(sized.weak());
|
|
||||||
list.push_back(LazyItem::new(i as u64, sized.any()));
|
|
||||||
} else {
|
|
||||||
let row = build_row(&mut rsc, i, 20);
|
|
||||||
list.push_back(LazyItem::new(i as u64, row));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let list = rsc.ui.widgets.add_strong(list);
|
|
||||||
let list_weak = list.weak();
|
|
||||||
let root = list.any();
|
|
||||||
let growable = growable.unwrap();
|
|
||||||
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
render.resize((1080.0, 2000.0));
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
render.take_counters();
|
|
||||||
|
|
||||||
let mut total = std::time::Duration::ZERO;
|
|
||||||
let mut total_draws = 0u64;
|
|
||||||
let mut total_rewrites = 0u64;
|
|
||||||
let mut total_moves = 0u64;
|
|
||||||
let mut height = 40.0f32;
|
|
||||||
let key = growable_index as u64;
|
|
||||||
for _ in 0..growths {
|
|
||||||
height += 10.0;
|
|
||||||
if let Some((top, _bottom)) = rsc.ui.widgets.get(&list_weak).unwrap().extent(key) {
|
|
||||||
rsc.ui
|
|
||||||
.widgets
|
|
||||||
.get_mut(&list_weak)
|
|
||||||
.unwrap()
|
|
||||||
.note_tap(top + 1.0);
|
|
||||||
}
|
|
||||||
rsc.ui.widgets.get_mut(&growable).unwrap().y = Some(abs(height).into());
|
|
||||||
let start = Instant::now();
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
total += start.elapsed();
|
|
||||||
let RenderCounters {
|
|
||||||
draws,
|
|
||||||
region_rewrites: rewrites,
|
|
||||||
moves,
|
|
||||||
..
|
|
||||||
} = render.take_counters();
|
|
||||||
total_draws += draws;
|
|
||||||
total_rewrites += rewrites;
|
|
||||||
total_moves += moves;
|
|
||||||
}
|
|
||||||
report(
|
|
||||||
&format!(
|
|
||||||
"(e) expand-hold, N={n}, {growths} growths (totals; \
|
|
||||||
must not scale with N)"
|
|
||||||
),
|
|
||||||
total,
|
|
||||||
total_draws,
|
|
||||||
total_rewrites,
|
|
||||||
total_moves,
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" per-growth average: {:.4}ms",
|
|
||||||
total.as_secs_f64() * 1000.0 / growths as f64
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bench_redraw_big_text(chars: usize, redraws: usize) {
|
|
||||||
let mut rsc = BenchRsc { ui: Ui::default() };
|
|
||||||
let content: String = (0..chars)
|
|
||||||
.map(|i| char::from(b'a' + (i % 26) as u8))
|
|
||||||
.collect();
|
|
||||||
let text = wtext(content)
|
|
||||||
.overflow(TextOverflow::Wrap)
|
|
||||||
.add_strong(&mut rsc);
|
|
||||||
let handle = text.weak();
|
|
||||||
let root = text.any();
|
|
||||||
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
render.resize((1080.0, 2000.0));
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
render.take_counters();
|
|
||||||
|
|
||||||
let mut total = std::time::Duration::ZERO;
|
|
||||||
for _ in 0..redraws {
|
|
||||||
rsc.ui.widgets.get_mut(&handle).unwrap();
|
|
||||||
let start = Instant::now();
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
total += start.elapsed();
|
|
||||||
}
|
|
||||||
let RenderCounters {
|
|
||||||
draws,
|
|
||||||
region_rewrites: rewrites,
|
|
||||||
moves,
|
|
||||||
..
|
|
||||||
} = render.take_counters();
|
|
||||||
report(
|
|
||||||
&format!("(g) redraw one {chars}-glyph text, {redraws}x (totals)"),
|
|
||||||
total,
|
|
||||||
draws,
|
|
||||||
rewrites,
|
|
||||||
moves,
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
" per redraw: {:.3}ms, per glyph: {:.4}us",
|
|
||||||
total.as_secs_f64() * 1000.0 / redraws as f64,
|
|
||||||
total.as_secs_f64() * 1_000_000.0 / (redraws * chars) as f64,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
println!("iris message-list benchmark -- release build, this machine's CPU");
|
|
||||||
for &n in &[100usize, 1_000, 10_000] {
|
|
||||||
bench_first_frame(n);
|
|
||||||
}
|
|
||||||
for &n in &[100usize, 1_000, 10_000] {
|
|
||||||
bench_scroll(n, 200);
|
|
||||||
}
|
|
||||||
for &n in &[100usize, 1_000, 10_000] {
|
|
||||||
bench_input_grows(n, 40);
|
|
||||||
}
|
|
||||||
for &n in &[100usize, 1_000, 10_000] {
|
|
||||||
bench_insert_above_anchor(n, 200);
|
|
||||||
}
|
|
||||||
for &n in &[100usize, 1_000, 10_000] {
|
|
||||||
bench_expand_holds_edge(n, 40);
|
|
||||||
}
|
|
||||||
for &chars in &[1_000usize, 10_000, 50_000] {
|
|
||||||
bench_redraw_big_text(chars, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "cargo-iris"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
cargo_metadata = "0.23.1"
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package dev.iris.android;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.Gravity;
|
|
||||||
import android.widget.ScrollView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import org.linebender.android.rustview.RustView;
|
|
||||||
|
|
||||||
public final class IrisView extends RustView {
|
|
||||||
@Override
|
|
||||||
protected native long newViewPeer(Context context);
|
|
||||||
|
|
||||||
native void applyWindowInsetsNative(
|
|
||||||
long peer, int left, int top, int right, int bottom, int imeBottom, int imeVisible);
|
|
||||||
|
|
||||||
native void unregisterInsetsNative(long peer);
|
|
||||||
|
|
||||||
public IrisView(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
void applyWindowInsets(
|
|
||||||
int left, int top, int right, int bottom, int imeBottom, int imeVisible) {
|
|
||||||
applyWindowInsetsNative(mViewPeer, left, top, right, bottom, imeBottom, imeVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDetachedFromWindow() {
|
|
||||||
unregisterInsetsNative(mViewPeer);
|
|
||||||
super.onDetachedFromWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
void showRendererError(String report) {
|
|
||||||
Context context = getContext();
|
|
||||||
if (!(context instanceof Activity)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Activity activity = (Activity) context;
|
|
||||||
TextView text = new TextView(activity);
|
|
||||||
text.setText(report);
|
|
||||||
text.setTextIsSelectable(true);
|
|
||||||
text.setGravity(Gravity.TOP | Gravity.START);
|
|
||||||
int pad = (int) (16 * activity.getResources().getDisplayMetrics().density);
|
|
||||||
text.setPadding(pad, pad, pad, pad);
|
|
||||||
ScrollView scroll = new ScrollView(activity);
|
|
||||||
scroll.addView(text);
|
|
||||||
activity.setContentView(scroll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package dev.iris.android;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.WindowInsets;
|
|
||||||
import android.view.WindowInsetsAnimation;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class MainActivity extends Activity {
|
|
||||||
static {
|
|
||||||
System.loadLibrary("IRIS_NATIVE_LIBRARY");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle state) {
|
|
||||||
super.onCreate(state);
|
|
||||||
IrisView view = new IrisView(this);
|
|
||||||
view.setLayoutParams(new FrameLayout.LayoutParams(
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
|
|
||||||
view.setFocusable(true);
|
|
||||||
view.setFocusableInTouchMode(true);
|
|
||||||
FrameLayout layout = new FrameLayout(this);
|
|
||||||
layout.addView(view);
|
|
||||||
setContentView(layout);
|
|
||||||
view.requestFocus();
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
drawBehindSystemBars();
|
|
||||||
view.setWindowInsetsAnimationCallback(new WindowInsetsAnimation.Callback(
|
|
||||||
WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
|
|
||||||
@Override
|
|
||||||
public WindowInsets onProgress(
|
|
||||||
WindowInsets insets, List<WindowInsetsAnimation> running) {
|
|
||||||
sendInsets(view, insets);
|
|
||||||
return insets;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEnd(WindowInsetsAnimation animation) {
|
|
||||||
WindowInsets settled = view.getRootWindowInsets();
|
|
||||||
if (settled != null) {
|
|
||||||
sendInsets(view, settled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
view.setOnApplyWindowInsetsListener((v, insets) -> {
|
|
||||||
sendInsets((IrisView) v, insets);
|
|
||||||
return insets;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Android 15 deprecated this API in favor of edge-to-edge enforcement,
|
|
||||||
// but API 30 through 34 still need it and Iris supports that whole range.
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
private void drawBehindSystemBars() {
|
|
||||||
getWindow().setDecorFitsSystemWindows(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 29 has no replacement for these four system-window inset getters;
|
|
||||||
// the API 30 methods cannot run on Iris's supported minimum.
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
private static void sendInsets(IrisView view, WindowInsets insets) {
|
|
||||||
int imeBottom = 0;
|
|
||||||
int imeVisible = 0;
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
imeBottom = insets.getInsets(WindowInsets.Type.ime()).bottom;
|
|
||||||
imeVisible = insets.isVisible(WindowInsets.Type.ime()) ? 1 : 0;
|
|
||||||
}
|
|
||||||
view.applyWindowInsets(
|
|
||||||
insets.getSystemWindowInsetLeft(),
|
|
||||||
insets.getSystemWindowInsetTop(),
|
|
||||||
insets.getSystemWindowInsetRight(),
|
|
||||||
insets.getSystemWindowInsetBottom(),
|
|
||||||
imeBottom,
|
|
||||||
imeVisible);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
package org.linebender.android.rustview;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.inputmethod.CompletionInfo;
|
|
||||||
import android.view.inputmethod.CorrectionInfo;
|
|
||||||
import android.view.inputmethod.ExtractedText;
|
|
||||||
import android.view.inputmethod.ExtractedTextRequest;
|
|
||||||
import android.view.inputmethod.InputConnection;
|
|
||||||
import android.view.inputmethod.InputContentInfo;
|
|
||||||
|
|
||||||
class RustInputConnection implements InputConnection {
|
|
||||||
private final RustView mView;
|
|
||||||
|
|
||||||
RustInputConnection(RustView view) {
|
|
||||||
mView = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long getViewPeer() {
|
|
||||||
return mView.mViewPeer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CharSequence getTextBeforeCursor(int n, int flags) {
|
|
||||||
return mView.getTextBeforeCursorNative(getViewPeer(), n);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CharSequence getTextAfterCursor(int n, int flags) {
|
|
||||||
return mView.getTextAfterCursorNative(getViewPeer(), n);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CharSequence getSelectedText(int flags) {
|
|
||||||
return mView.getSelectedTextNative(getViewPeer());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCursorCapsMode(int reqModes) {
|
|
||||||
return mView.getCursorCapsModeNative(getViewPeer(), reqModes);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
|
||||||
return mView.deleteSurroundingTextNative(getViewPeer(), beforeLength, afterLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
|
|
||||||
return mView.deleteSurroundingTextInCodePointsNative(getViewPeer(), beforeLength, afterLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
|
||||||
return mView.setComposingTextNative(getViewPeer(), text.toString(), newCursorPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean setComposingRegion(int start, int end) {
|
|
||||||
return mView.setComposingRegionNative(getViewPeer(), start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean finishComposingText() {
|
|
||||||
return mView.finishComposingTextNative(getViewPeer());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean commitText(CharSequence text, int newCursorPosition) {
|
|
||||||
return mView.commitTextNative(getViewPeer(), text.toString(), newCursorPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean commitCompletion(CompletionInfo text) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean commitCorrection(CorrectionInfo correctionInfo) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean setSelection(int start, int end) {
|
|
||||||
return mView.setSelectionNative(getViewPeer(), start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean performEditorAction(int editorAction) {
|
|
||||||
return mView.performEditorActionNative(getViewPeer(), editorAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean performContextMenuAction(int id) {
|
|
||||||
return mView.performContextMenuActionNative(getViewPeer(), id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean beginBatchEdit() {
|
|
||||||
return mView.beginBatchEditNative(getViewPeer());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean endBatchEdit() {
|
|
||||||
return mView.endBatchEditNative(getViewPeer());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean sendKeyEvent(KeyEvent event) {
|
|
||||||
return mView.inputConnectionSendKeyEventNative(getViewPeer(), event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean clearMetaKeyStates(int states) {
|
|
||||||
return mView.inputConnectionClearMetaKeyStatesNative(getViewPeer(), states);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean reportFullscreenMode(boolean enabled) {
|
|
||||||
return mView.inputConnectionReportFullscreenModeNative(getViewPeer(), enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean performPrivateCommand(String action, Bundle data) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean requestCursorUpdates(int cursorUpdateMode) {
|
|
||||||
return mView.requestCursorUpdatesNative(getViewPeer(), cursorUpdateMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Handler getHandler() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void closeConnection() {
|
|
||||||
mView.closeInputConnectionNative(getViewPeer());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
package org.linebender.android.rustview;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Choreographer;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.SurfaceHolder;
|
|
||||||
import android.view.SurfaceView;
|
|
||||||
import android.view.accessibility.AccessibilityNodeInfo;
|
|
||||||
import android.view.accessibility.AccessibilityNodeProvider;
|
|
||||||
import android.view.inputmethod.EditorInfo;
|
|
||||||
import android.view.inputmethod.InputConnection;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
|
|
||||||
public abstract class RustView extends SurfaceView
|
|
||||||
implements SurfaceHolder.Callback, Choreographer.FrameCallback {
|
|
||||||
// Vendored from android-view bec6c62. The only local change is `protected`,
|
|
||||||
// allowing IrisView to forward insets through this native peer.
|
|
||||||
protected final long mViewPeer;
|
|
||||||
final InputMethodManager mInputMethodManager;
|
|
||||||
|
|
||||||
protected abstract long newViewPeer(Context context);
|
|
||||||
|
|
||||||
public RustView(Context context) {
|
|
||||||
super(context);
|
|
||||||
mViewPeer = newViewPeer(context);
|
|
||||||
getHolder().addCallback(this);
|
|
||||||
mInputMethodManager =
|
|
||||||
(InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native int[] onMeasureNative(long peer, int widthSpec, int heightSpec);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onMeasure(int widthSpec, int heightSpec) {
|
|
||||||
int[] result = onMeasureNative(mViewPeer, widthSpec, heightSpec);
|
|
||||||
if (result != null) {
|
|
||||||
setMeasuredDimension(result[0], result[1]);
|
|
||||||
} else {
|
|
||||||
super.onMeasure(widthSpec, heightSpec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void onLayoutNative(
|
|
||||||
long peer, boolean changed, int left, int top, int right, int bottom);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
||||||
onLayoutNative(mViewPeer, changed, left, top, right, bottom);
|
|
||||||
super.onLayout(changed, left, top, right, bottom);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void onSizeChangedNative(long peer, int w, int h, int oldw, int oldh);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
||||||
onSizeChangedNative(mViewPeer, w, h, oldw, oldh);
|
|
||||||
super.onSizeChanged(w, h, oldw, oldh);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native boolean onKeyDownNative(long peer, int keyCode, KeyEvent event);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
||||||
return onKeyDownNative(mViewPeer, keyCode, event) || super.onKeyDown(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native boolean onKeyUpNative(long peer, int keyCode, KeyEvent event);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
||||||
return onKeyUpNative(mViewPeer, keyCode, event) || super.onKeyUp(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native boolean onTrackballEventNative(long peer, MotionEvent event);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onTrackballEvent(MotionEvent event) {
|
|
||||||
return onTrackballEventNative(mViewPeer, event) || super.onTrackballEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native boolean onTouchEventNative(long peer, MotionEvent event);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
|
||||||
return onTouchEventNative(mViewPeer, event) || super.onTouchEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native boolean onGenericMotionEventNative(long peer, MotionEvent event);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
|
||||||
return onGenericMotionEventNative(mViewPeer, event) || super.onGenericMotionEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native boolean onHoverEventNative(long peer, MotionEvent event);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onHoverEvent(MotionEvent event) {
|
|
||||||
return onHoverEventNative(mViewPeer, event) || super.onHoverEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void onFocusChangedNative(
|
|
||||||
long peer, boolean gainFocus, int direction, Rect previouslyFocusedRect);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
|
|
||||||
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
|
||||||
onFocusChangedNative(mViewPeer, gainFocus, direction, previouslyFocusedRect);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void onWindowFocusChangedNative(long peer, boolean hasWindowFocus);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onWindowFocusChanged(boolean hasWindowFocus) {
|
|
||||||
super.onWindowFocusChanged(hasWindowFocus);
|
|
||||||
onWindowFocusChangedNative(mViewPeer, hasWindowFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void onAttachedToWindowNative(long peer);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAttachedToWindow() {
|
|
||||||
super.onAttachedToWindow();
|
|
||||||
onAttachedToWindowNative(mViewPeer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void onDetachedFromWindowNative(long peer);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDetachedFromWindow() {
|
|
||||||
super.onDetachedFromWindow();
|
|
||||||
onDetachedFromWindowNative(mViewPeer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void onWindowVisibilityChangedNative(long peer, int visibility);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onWindowVisibilityChanged(int visibility) {
|
|
||||||
super.onWindowVisibilityChanged(visibility);
|
|
||||||
onWindowVisibilityChangedNative(mViewPeer, visibility);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void surfaceCreatedNative(long peer, SurfaceHolder holder);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void surfaceCreated(SurfaceHolder holder) {
|
|
||||||
surfaceCreatedNative(mViewPeer, holder);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void surfaceChangedNative(
|
|
||||||
long peer, SurfaceHolder holder, int format, int width, int height);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
|
||||||
surfaceChangedNative(mViewPeer, holder, format, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void surfaceDestroyedNative(long peer, SurfaceHolder holder);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
|
||||||
surfaceDestroyedNative(mViewPeer, holder);
|
|
||||||
}
|
|
||||||
|
|
||||||
void postFrameCallback() {
|
|
||||||
Choreographer c = Choreographer.getInstance();
|
|
||||||
c.removeFrameCallback(this);
|
|
||||||
c.postFrameCallback(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeFrameCallback() {
|
|
||||||
Choreographer.getInstance().removeFrameCallback(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void doFrameNative(long peer, long frameTimeNanos);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFrame(long frameTimeNanos) {
|
|
||||||
doFrameNative(mViewPeer, frameTimeNanos);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native void delayedCallbackNative(long peer);
|
|
||||||
|
|
||||||
private final Runnable mDelayedCallback =
|
|
||||||
new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
delayedCallbackNative(mViewPeer);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
boolean postDelayed(long delayMillis) {
|
|
||||||
return postDelayed(mDelayedCallback, delayMillis);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean removeDelayedCallbacks() {
|
|
||||||
return removeCallbacks(mDelayedCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
private native boolean hasAccessibilityNodeProviderNative(long peer);
|
|
||||||
|
|
||||||
private native AccessibilityNodeInfo createAccessibilityNodeInfoNative(
|
|
||||||
long peer, int virtualViewId);
|
|
||||||
|
|
||||||
private native AccessibilityNodeInfo accessibilityFindFocusNative(long peer, int virtualViewId);
|
|
||||||
|
|
||||||
private native boolean performAccessibilityActionNative(
|
|
||||||
long peer, int virtualViewId, int action, Bundle arguments);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
|
|
||||||
if (!hasAccessibilityNodeProviderNative(mViewPeer)) {
|
|
||||||
return super.getAccessibilityNodeProvider();
|
|
||||||
}
|
|
||||||
return new AccessibilityNodeProvider() {
|
|
||||||
@Override
|
|
||||||
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
|
|
||||||
return createAccessibilityNodeInfoNative(mViewPeer, virtualViewId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AccessibilityNodeInfo findFocus(int focusType) {
|
|
||||||
return accessibilityFindFocusNative(mViewPeer, focusType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean performAction(int virtualViewId, int action, Bundle arguments) {
|
|
||||||
return performAccessibilityActionNative(
|
|
||||||
mViewPeer, virtualViewId, action, arguments);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private native boolean onCreateInputConnectionNative(long peer, EditorInfo outAttrs);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
|
||||||
if (!onCreateInputConnectionNative(mViewPeer, outAttrs)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new RustInputConnection(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
native String getTextBeforeCursorNative(long peer, int n);
|
|
||||||
|
|
||||||
native String getTextAfterCursorNative(long peer, int n);
|
|
||||||
|
|
||||||
native String getSelectedTextNative(long peer);
|
|
||||||
|
|
||||||
native int getCursorCapsModeNative(long peer, int reqModes);
|
|
||||||
|
|
||||||
native boolean deleteSurroundingTextNative(long peer, int beforeLength, int afterLength);
|
|
||||||
|
|
||||||
native boolean deleteSurroundingTextInCodePointsNative(
|
|
||||||
long peer, int beforeLength, int afterLength);
|
|
||||||
|
|
||||||
native boolean setComposingTextNative(long peer, String text, int newCursorPosition);
|
|
||||||
|
|
||||||
native boolean setComposingRegionNative(long peer, int start, int end);
|
|
||||||
|
|
||||||
native boolean finishComposingTextNative(long peer);
|
|
||||||
|
|
||||||
native boolean commitTextNative(long peer, String text, int newCursorPosition);
|
|
||||||
|
|
||||||
native boolean setSelectionNative(long peer, int start, int end);
|
|
||||||
|
|
||||||
native boolean performEditorActionNative(long peer, int editorAction);
|
|
||||||
|
|
||||||
native boolean performContextMenuActionNative(long peer, int id);
|
|
||||||
|
|
||||||
native boolean beginBatchEditNative(long peer);
|
|
||||||
|
|
||||||
native boolean endBatchEditNative(long peer);
|
|
||||||
|
|
||||||
native boolean inputConnectionSendKeyEventNative(long peer, KeyEvent event);
|
|
||||||
|
|
||||||
native boolean inputConnectionClearMetaKeyStatesNative(long peer, int states);
|
|
||||||
|
|
||||||
native boolean inputConnectionReportFullscreenModeNative(long peer, boolean enabled);
|
|
||||||
|
|
||||||
native boolean requestCursorUpdatesNative(long peer, int cursorUpdateMode);
|
|
||||||
|
|
||||||
native void closeInputConnectionNative(long peer);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
mod package;
|
|
||||||
|
|
||||||
use std::{env, process::ExitCode};
|
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
|
||||||
match package::run(env::args().skip(1).collect()) {
|
|
||||||
Ok(()) => ExitCode::SUCCESS,
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("cargo iris: {error}");
|
|
||||||
ExitCode::FAILURE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,902 +0,0 @@
|
|||||||
use cargo_metadata::{CrateType, MetadataCommand, Package, Target, TargetKind};
|
|
||||||
use std::{
|
|
||||||
env,
|
|
||||||
ffi::OsStr,
|
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
process::{Command, Stdio},
|
|
||||||
};
|
|
||||||
|
|
||||||
const MIN_SDK: u32 = 29;
|
|
||||||
const ACTIVITY: &str = "dev.iris.android.MainActivity";
|
|
||||||
|
|
||||||
pub fn run(mut args: Vec<String>) -> Result<(), String> {
|
|
||||||
if args.first().is_some_and(|arg| arg == "iris") {
|
|
||||||
args.remove(0);
|
|
||||||
}
|
|
||||||
let command = args.first().map(String::as_str).unwrap_or("help");
|
|
||||||
if matches!(command, "help" | "--help" | "-h") {
|
|
||||||
print_help();
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if !matches!(command, "apk" | "run") {
|
|
||||||
return Err(format!(
|
|
||||||
"unknown command {command:?}; run `cargo iris --help`"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let options = Options::parse(&args[1..], command == "run")?;
|
|
||||||
let built = build(&options)?;
|
|
||||||
println!("{}", built.apk.display());
|
|
||||||
if command == "run" {
|
|
||||||
install_and_run(&built, options.device.as_deref().unwrap())?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_help() {
|
|
||||||
println!(
|
|
||||||
"Build an Iris application or example as an installable Android APK.\n\n\
|
|
||||||
Usage:\n cargo iris apk [OPTIONS]\n cargo iris run --device SERIAL [OPTIONS]\n\n\
|
|
||||||
Options:\n --manifest-path PATH\n --package NAME\n --example NAME\n --abi arm64-v8a|x86_64\n --release\n\
|
|
||||||
\x20 --application-id ID\n --label TEXT\n --keystore PATH --key-alias ALIAS\n\n\
|
|
||||||
Release signing passwords come from IRIS_KEYSTORE_PASSWORD and, when different,\n\
|
|
||||||
IRIS_KEY_PASSWORD. Iris uses the Android SDK and emulator/device supplied by you."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct Options {
|
|
||||||
manifest_path: Option<PathBuf>,
|
|
||||||
package: Option<String>,
|
|
||||||
example: Option<String>,
|
|
||||||
abi: String,
|
|
||||||
release: bool,
|
|
||||||
application_id: Option<String>,
|
|
||||||
label: Option<String>,
|
|
||||||
keystore: Option<PathBuf>,
|
|
||||||
key_alias: Option<String>,
|
|
||||||
device: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Options {
|
|
||||||
fn parse(args: &[String], run: bool) -> Result<Self, String> {
|
|
||||||
let mut options = Self {
|
|
||||||
abi: "arm64-v8a".into(),
|
|
||||||
..Self::default()
|
|
||||||
};
|
|
||||||
let mut i = 0;
|
|
||||||
while i < args.len() {
|
|
||||||
let value = |name: &str, i: &mut usize| -> Result<String, String> {
|
|
||||||
*i += 1;
|
|
||||||
args.get(*i)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| format!("{name} needs a value"))
|
|
||||||
};
|
|
||||||
match args[i].as_str() {
|
|
||||||
"--manifest-path" => {
|
|
||||||
options.manifest_path = Some(value("--manifest-path", &mut i)?.into())
|
|
||||||
}
|
|
||||||
"--package" => options.package = Some(value("--package", &mut i)?),
|
|
||||||
"--example" => options.example = Some(value("--example", &mut i)?),
|
|
||||||
"--abi" => options.abi = value("--abi", &mut i)?,
|
|
||||||
"--application-id" => {
|
|
||||||
options.application_id = Some(value("--application-id", &mut i)?)
|
|
||||||
}
|
|
||||||
"--label" => options.label = Some(value("--label", &mut i)?),
|
|
||||||
"--keystore" => options.keystore = Some(value("--keystore", &mut i)?.into()),
|
|
||||||
"--key-alias" => options.key_alias = Some(value("--key-alias", &mut i)?),
|
|
||||||
"--device" => options.device = Some(value("--device", &mut i)?),
|
|
||||||
"--release" => options.release = true,
|
|
||||||
other => return Err(format!("unknown option {other:?}")),
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
if !matches!(options.abi.as_str(), "arm64-v8a" | "x86_64") {
|
|
||||||
return Err(format!(
|
|
||||||
"unsupported ABI {:?}; use arm64-v8a or x86_64",
|
|
||||||
options.abi
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if run && options.device.is_none() {
|
|
||||||
return Err(
|
|
||||||
"`cargo iris run` needs --device SERIAL; Iris never chooses or starts an emulator"
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if options.release && (options.keystore.is_none() || options.key_alias.is_none()) {
|
|
||||||
return Err("a release APK needs --keystore PATH and --key-alias ALIAS".into());
|
|
||||||
}
|
|
||||||
Ok(options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Built {
|
|
||||||
apk: PathBuf,
|
|
||||||
application_id: String,
|
|
||||||
sdk: Sdk,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build(options: &Options) -> Result<Built, String> {
|
|
||||||
let mut metadata = MetadataCommand::new();
|
|
||||||
if let Some(path) = &options.manifest_path {
|
|
||||||
metadata.manifest_path(path);
|
|
||||||
}
|
|
||||||
let metadata = metadata
|
|
||||||
.exec()
|
|
||||||
.map_err(|error| format!("could not read Cargo metadata: {error}"))?;
|
|
||||||
let package = select_package(
|
|
||||||
&metadata.packages,
|
|
||||||
metadata.root_package(),
|
|
||||||
options.package.as_deref(),
|
|
||||||
)?;
|
|
||||||
let target = select_target(package, options.example.as_deref())?;
|
|
||||||
let sdk = Sdk::find()?;
|
|
||||||
let application_id = options
|
|
||||||
.application_id
|
|
||||||
.clone()
|
|
||||||
.or_else(|| {
|
|
||||||
options
|
|
||||||
.example
|
|
||||||
.is_none()
|
|
||||||
.then(|| metadata_string(package, "application-id"))
|
|
||||||
.flatten()
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| default_application_id(&package.name, options.example.as_deref()));
|
|
||||||
validate_application_id(&application_id)?;
|
|
||||||
let label = options
|
|
||||||
.label
|
|
||||||
.clone()
|
|
||||||
.or_else(|| {
|
|
||||||
options
|
|
||||||
.example
|
|
||||||
.is_none()
|
|
||||||
.then(|| metadata_string(package, "label"))
|
|
||||||
.flatten()
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
options
|
|
||||||
.example
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| package.name.to_string())
|
|
||||||
});
|
|
||||||
let variant = if options.release { "release" } else { "debug" };
|
|
||||||
let artifact = options.example.as_ref().map_or_else(
|
|
||||||
|| package.name.to_string(),
|
|
||||||
|example| format!("{}-{example}", package.name),
|
|
||||||
);
|
|
||||||
let output = metadata
|
|
||||||
.target_directory
|
|
||||||
.as_std_path()
|
|
||||||
.join("iris-android")
|
|
||||||
.join(&artifact)
|
|
||||||
.join(variant)
|
|
||||||
.join(&options.abi);
|
|
||||||
recreate(&output)?;
|
|
||||||
let staging = output.join("staging");
|
|
||||||
let staging_cleanup = RemoveDirOnDrop(&staging);
|
|
||||||
let native = staging.join("native");
|
|
||||||
let (build_manifest, library_name) = if options.example.is_some() {
|
|
||||||
let wrapper = metadata
|
|
||||||
.target_directory
|
|
||||||
.as_std_path()
|
|
||||||
.join("iris-android")
|
|
||||||
.join("example-wrappers")
|
|
||||||
.join(&artifact);
|
|
||||||
materialize_example_wrapper(&metadata.packages, package, target, &wrapper)?
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
package.manifest_path.as_std_path().to_path_buf(),
|
|
||||||
target.name.clone(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut cargo = Command::new("cargo");
|
|
||||||
cargo
|
|
||||||
.args(["ndk", "-t", &options.abi, "-P", &MIN_SDK.to_string(), "-o"])
|
|
||||||
.arg(&native)
|
|
||||||
.arg("build")
|
|
||||||
.arg("--lib")
|
|
||||||
.arg("--manifest-path")
|
|
||||||
.arg(&build_manifest)
|
|
||||||
.env("CARGO_TARGET_DIR", metadata.target_directory.as_std_path());
|
|
||||||
if options.release {
|
|
||||||
cargo.arg("--release");
|
|
||||||
}
|
|
||||||
run_command(
|
|
||||||
&mut cargo,
|
|
||||||
"Rust Android library",
|
|
||||||
"install cargo-ndk with `cargo install cargo-ndk`",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let library = native
|
|
||||||
.join(&options.abi)
|
|
||||||
.join(format!("lib{library_name}.so"));
|
|
||||||
if !library.is_file() {
|
|
||||||
return Err(format!("cargo-ndk did not produce {}", library.display()));
|
|
||||||
}
|
|
||||||
let classes = staging.join("classes");
|
|
||||||
fs::create_dir_all(&classes).map_err(io_error("create Java output", &classes))?;
|
|
||||||
let sources = materialize_host(&staging, &library_name)?;
|
|
||||||
let java_files = files_with_extension(&sources, "java")?;
|
|
||||||
let mut javac = Command::new("javac");
|
|
||||||
javac
|
|
||||||
.args(["--release", "17", "-classpath"])
|
|
||||||
.arg(&sdk.android_jar)
|
|
||||||
.arg("-d")
|
|
||||||
.arg(&classes)
|
|
||||||
.args(&java_files);
|
|
||||||
run_command(
|
|
||||||
&mut javac,
|
|
||||||
"Iris Android Java host",
|
|
||||||
"install a JDK containing javac",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let dex = staging.join("dex");
|
|
||||||
fs::create_dir_all(&dex).map_err(io_error("create DEX output", &dex))?;
|
|
||||||
let class_files = files_with_extension(&classes, "class")?;
|
|
||||||
let mut d8 = Command::new(&sdk.d8);
|
|
||||||
d8.args(["--min-api", &MIN_SDK.to_string(), "--lib"])
|
|
||||||
.arg(&sdk.android_jar)
|
|
||||||
.arg("--output")
|
|
||||||
.arg(&dex)
|
|
||||||
.args(&class_files);
|
|
||||||
if options.release {
|
|
||||||
d8.arg("--release");
|
|
||||||
} else {
|
|
||||||
d8.arg("--debug");
|
|
||||||
}
|
|
||||||
run_command(
|
|
||||||
&mut d8,
|
|
||||||
"Iris Android DEX",
|
|
||||||
"install Android SDK Build Tools",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let manifest = staging.join("AndroidManifest.xml");
|
|
||||||
fs::write(
|
|
||||||
&manifest,
|
|
||||||
manifest_xml(&application_id, &label, &library_name, sdk.api),
|
|
||||||
)
|
|
||||||
.map_err(io_error("write Android manifest", &manifest))?;
|
|
||||||
let unsigned = staging.join("unsigned.apk");
|
|
||||||
let mut aapt = Command::new(&sdk.aapt2);
|
|
||||||
aapt.arg("link")
|
|
||||||
.arg("-o")
|
|
||||||
.arg(&unsigned)
|
|
||||||
.arg("-I")
|
|
||||||
.arg(&sdk.android_jar)
|
|
||||||
.arg("--manifest")
|
|
||||||
.arg(&manifest)
|
|
||||||
.args([
|
|
||||||
"--min-sdk-version",
|
|
||||||
&MIN_SDK.to_string(),
|
|
||||||
"--target-sdk-version",
|
|
||||||
&sdk.api.to_string(),
|
|
||||||
]);
|
|
||||||
run_command(
|
|
||||||
&mut aapt,
|
|
||||||
"Android resources",
|
|
||||||
"install Android SDK Build Tools",
|
|
||||||
)?;
|
|
||||||
append_payload(
|
|
||||||
&unsigned,
|
|
||||||
&staging,
|
|
||||||
&dex.join("classes.dex"),
|
|
||||||
&library,
|
|
||||||
&options.abi,
|
|
||||||
&library_name,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let aligned = staging.join("aligned.apk");
|
|
||||||
let mut zipalign = Command::new(&sdk.zipalign);
|
|
||||||
zipalign
|
|
||||||
.args(["-P", "16", "-f", "4"])
|
|
||||||
.arg(&unsigned)
|
|
||||||
.arg(&aligned);
|
|
||||||
run_command(
|
|
||||||
&mut zipalign,
|
|
||||||
"APK alignment",
|
|
||||||
"install Android SDK Build Tools",
|
|
||||||
)?;
|
|
||||||
let apk = output.join(format!("{artifact}-{variant}.apk"));
|
|
||||||
sign(&sdk, options, &aligned, &apk)?;
|
|
||||||
verify(&sdk, &apk)?;
|
|
||||||
fs::remove_dir_all(&staging).map_err(io_error("remove APK staging directory", &staging))?;
|
|
||||||
std::mem::forget(staging_cleanup);
|
|
||||||
Ok(Built {
|
|
||||||
apk,
|
|
||||||
application_id,
|
|
||||||
sdk,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_package<'a>(
|
|
||||||
packages: &'a [Package],
|
|
||||||
root: Option<&'a Package>,
|
|
||||||
wanted: Option<&str>,
|
|
||||||
) -> Result<&'a Package, String> {
|
|
||||||
if let Some(wanted) = wanted {
|
|
||||||
return packages
|
|
||||||
.iter()
|
|
||||||
.find(|package| package.name == wanted)
|
|
||||||
.ok_or_else(|| format!("Cargo workspace has no package named {wanted:?}"));
|
|
||||||
}
|
|
||||||
root.ok_or_else(|| {
|
|
||||||
"this is a virtual workspace; select an application with --package NAME".into()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_target<'a>(package: &'a Package, example: Option<&str>) -> Result<&'a Target, String> {
|
|
||||||
if let Some(example) = example {
|
|
||||||
return package
|
|
||||||
.targets
|
|
||||||
.iter()
|
|
||||||
.find(|target| {
|
|
||||||
target.name == example
|
|
||||||
&& target.kind.iter().any(|kind| kind == &TargetKind::Example)
|
|
||||||
})
|
|
||||||
.ok_or_else(|| {
|
|
||||||
format!(
|
|
||||||
"package {} has no example named {example:?}; use one of: {}",
|
|
||||||
package.name,
|
|
||||||
package
|
|
||||||
.targets
|
|
||||||
.iter()
|
|
||||||
.filter(|target| target
|
|
||||||
.kind
|
|
||||||
.iter()
|
|
||||||
.any(|kind| kind == &TargetKind::Example))
|
|
||||||
.map(|target| target.name.as_str())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
package
|
|
||||||
.targets
|
|
||||||
.iter()
|
|
||||||
.find(|target| {
|
|
||||||
target
|
|
||||||
.crate_types
|
|
||||||
.iter()
|
|
||||||
.any(|kind| kind == &CrateType::CDyLib)
|
|
||||||
})
|
|
||||||
.ok_or_else(|| {
|
|
||||||
format!(
|
|
||||||
"package {} has no cdylib target; add `[lib] crate-type = [\"cdylib\", \"rlib\"]` to {}",
|
|
||||||
package.name, package.manifest_path
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn materialize_example_wrapper(
|
|
||||||
packages: &[Package],
|
|
||||||
package: &Package,
|
|
||||||
example: &Target,
|
|
||||||
wrapper: &Path,
|
|
||||||
) -> Result<(PathBuf, String), String> {
|
|
||||||
let android_source = example
|
|
||||||
.src_path
|
|
||||||
.as_std_path()
|
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.join("android.rs");
|
|
||||||
if !android_source.is_file() {
|
|
||||||
return Err(format!(
|
|
||||||
"example {:?} has no Android entry point at {}; put shared code in lib.rs and add sibling desktop.rs and android.rs entries",
|
|
||||||
example.name,
|
|
||||||
android_source.display()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let iris = packages
|
|
||||||
.iter()
|
|
||||||
.find(|dependency| dependency.name == "iris")
|
|
||||||
.ok_or_else(|| {
|
|
||||||
format!(
|
|
||||||
"example {:?} does not depend on Iris; add `iris` to {}",
|
|
||||||
example.name, package.manifest_path
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let source_dir = wrapper.join("src");
|
|
||||||
fs::create_dir_all(&source_dir)
|
|
||||||
.map_err(io_error("create Android example wrapper", &source_dir))?;
|
|
||||||
let library_name = format!(
|
|
||||||
"iris_android_{}_{}",
|
|
||||||
identifier_segment(&package.name),
|
|
||||||
identifier_segment(&example.name)
|
|
||||||
);
|
|
||||||
let iris_root = iris.manifest_path.parent().unwrap();
|
|
||||||
let manifest = format!(
|
|
||||||
"[package]\nname = {name:?}\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\
|
|
||||||
[lib]\nname = {library_name:?}\ncrate-type = [\"cdylib\", \"rlib\"]\n\n\
|
|
||||||
[dependencies]\niris = {{ path = {iris_root:?} }}\n\n[workspace]\n\n\
|
|
||||||
[profile.dev]\ndebug = \"line-tables-only\"\n",
|
|
||||||
name = format!("iris-android-{}-{}", package.name, example.name),
|
|
||||||
iris_root = iris_root.as_str(),
|
|
||||||
);
|
|
||||||
let manifest_path = wrapper.join("Cargo.toml");
|
|
||||||
write_if_changed(&manifest_path, &manifest)?;
|
|
||||||
let source = format!(
|
|
||||||
"#[path = {:?}]\nmod example;\n",
|
|
||||||
android_source.to_string_lossy()
|
|
||||||
);
|
|
||||||
let source_path = source_dir.join("lib.rs");
|
|
||||||
write_if_changed(&source_path, &source)?;
|
|
||||||
Ok((manifest_path, library_name))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_if_changed(path: &Path, contents: &str) -> Result<bool, String> {
|
|
||||||
match fs::read(path) {
|
|
||||||
Ok(existing) if existing == contents.as_bytes() => return Ok(false),
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
|
||||||
Err(error) => return Err(format!("cannot read {}: {error}", path.display())),
|
|
||||||
}
|
|
||||||
fs::write(path, contents).map_err(io_error("write generated file", path))?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn metadata_string(package: &Package, key: &str) -> Option<String> {
|
|
||||||
package
|
|
||||||
.metadata
|
|
||||||
.get("iris")?
|
|
||||||
.get("android")?
|
|
||||||
.get(key)?
|
|
||||||
.as_str()
|
|
||||||
.map(str::to_owned)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn identifier_segment(value: &str) -> String {
|
|
||||||
value
|
|
||||||
.chars()
|
|
||||||
.map(|c| {
|
|
||||||
if c.is_ascii_alphanumeric() {
|
|
||||||
c.to_ascii_lowercase()
|
|
||||||
} else {
|
|
||||||
'_'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_application_id(package: &str, example: Option<&str>) -> String {
|
|
||||||
let package = identifier_segment(package);
|
|
||||||
match example {
|
|
||||||
Some(example) => format!("dev.iris.example.{package}.{}", identifier_segment(example)),
|
|
||||||
None => format!("dev.iris.app.{package}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_application_id(id: &str) -> Result<(), String> {
|
|
||||||
let valid = id.split('.').count() >= 2
|
|
||||||
&& id.split('.').all(|segment| {
|
|
||||||
!segment.is_empty()
|
|
||||||
&& segment.as_bytes()[0].is_ascii_alphabetic()
|
|
||||||
&& segment
|
|
||||||
.bytes()
|
|
||||||
.all(|c| c.is_ascii_alphanumeric() || c == b'_')
|
|
||||||
});
|
|
||||||
if valid {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(format!(
|
|
||||||
"application ID {id:?} is invalid; use dot-separated Java identifiers"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Sdk {
|
|
||||||
api: u32,
|
|
||||||
android_jar: PathBuf,
|
|
||||||
aapt2: PathBuf,
|
|
||||||
d8: PathBuf,
|
|
||||||
zipalign: PathBuf,
|
|
||||||
apksigner: PathBuf,
|
|
||||||
adb: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Failed builds have no reusable staging output either; the next invocation
|
|
||||||
/// starts from scratch, so do not make a failure consume disk indefinitely.
|
|
||||||
struct RemoveDirOnDrop<'a>(&'a Path);
|
|
||||||
|
|
||||||
impl Drop for RemoveDirOnDrop<'_> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = fs::remove_dir_all(self.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sdk {
|
|
||||||
fn find() -> Result<Self, String> {
|
|
||||||
let root = env::var_os("ANDROID_HOME")
|
|
||||||
.or_else(|| env::var_os("ANDROID_SDK_ROOT"))
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.ok_or_else(|| "ANDROID_HOME is unset; point it at your Android SDK".to_string())?;
|
|
||||||
let (api, platform) = newest_numbered(&root.join("platforms"), "android-")?;
|
|
||||||
let (_, tools) = newest_numbered(&root.join("build-tools"), "")?;
|
|
||||||
let executable = |name: &str, windows_extension: &str| {
|
|
||||||
tools.join(if cfg!(windows) {
|
|
||||||
format!("{name}.{windows_extension}")
|
|
||||||
} else {
|
|
||||||
name.to_string()
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let sdk = Self {
|
|
||||||
android_jar: platform.join("android.jar"),
|
|
||||||
aapt2: executable("aapt2", "exe"),
|
|
||||||
d8: executable("d8", "bat"),
|
|
||||||
zipalign: executable("zipalign", "exe"),
|
|
||||||
apksigner: executable("apksigner", "bat"),
|
|
||||||
adb: root
|
|
||||||
.join("platform-tools")
|
|
||||||
.join(if cfg!(windows) { "adb.exe" } else { "adb" }),
|
|
||||||
api,
|
|
||||||
};
|
|
||||||
for (name, path) in [
|
|
||||||
("android.jar", &sdk.android_jar),
|
|
||||||
("aapt2", &sdk.aapt2),
|
|
||||||
("d8", &sdk.d8),
|
|
||||||
("zipalign", &sdk.zipalign),
|
|
||||||
("apksigner", &sdk.apksigner),
|
|
||||||
] {
|
|
||||||
if !path.is_file() {
|
|
||||||
return Err(format!(
|
|
||||||
"Android SDK is missing {name} at {}; install a platform and Build Tools",
|
|
||||||
path.display()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(sdk)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn newest_numbered(parent: &Path, prefix: &str) -> Result<(u32, PathBuf), String> {
|
|
||||||
let entries = fs::read_dir(parent).map_err(|error| {
|
|
||||||
format!(
|
|
||||||
"cannot read {}: {error}; install the required Android SDK component",
|
|
||||||
parent.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
entries
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.filter_map(|entry| {
|
|
||||||
let name = entry.file_name();
|
|
||||||
let version = name
|
|
||||||
.to_string_lossy()
|
|
||||||
.strip_prefix(prefix)?
|
|
||||||
.split('.')
|
|
||||||
.map(str::parse::<u32>)
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
|
||||||
.ok()?;
|
|
||||||
Some((version, entry.path()))
|
|
||||||
})
|
|
||||||
.max_by(|(left, _), (right, _)| left.cmp(right))
|
|
||||||
.map(|(version, path)| (version[0], path))
|
|
||||||
.ok_or_else(|| {
|
|
||||||
format!(
|
|
||||||
"no installed Android SDK component found under {}",
|
|
||||||
parent.display()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn materialize_host(output: &Path, library: &str) -> Result<PathBuf, String> {
|
|
||||||
let root = output.join("java");
|
|
||||||
for (relative, contents) in HOST_FILES {
|
|
||||||
let path = root.join(relative);
|
|
||||||
fs::create_dir_all(path.parent().unwrap())
|
|
||||||
.map_err(io_error("create Java source directory", &path))?;
|
|
||||||
let contents = if relative.ends_with("MainActivity.java") {
|
|
||||||
contents.replace("IRIS_NATIVE_LIBRARY", library)
|
|
||||||
} else {
|
|
||||||
contents.to_string()
|
|
||||||
};
|
|
||||||
fs::write(&path, contents).map_err(io_error("write Java host source", &path))?;
|
|
||||||
}
|
|
||||||
Ok(root)
|
|
||||||
}
|
|
||||||
|
|
||||||
const HOST_FILES: &[(&str, &str)] = &[
|
|
||||||
(
|
|
||||||
"dev/iris/android/MainActivity.java",
|
|
||||||
include_str!("../android-host/dev/iris/android/MainActivity.java"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"dev/iris/android/IrisView.java",
|
|
||||||
include_str!("../android-host/dev/iris/android/IrisView.java"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"org/linebender/android/rustview/RustView.java",
|
|
||||||
include_str!("../android-host/org/linebender/android/rustview/RustView.java"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"org/linebender/android/rustview/RustInputConnection.java",
|
|
||||||
include_str!("../android-host/org/linebender/android/rustview/RustInputConnection.java"),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
fn manifest_xml(application_id: &str, label: &str, library: &str, target_sdk: u32) -> String {
|
|
||||||
format!(
|
|
||||||
r#"<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="{}" android:versionCode="1" android:versionName="0.1.0">
|
|
||||||
<uses-sdk android:minSdkVersion="{MIN_SDK}" android:targetSdkVersion="{target_sdk}" />
|
|
||||||
<application android:allowBackup="true" android:extractNativeLibs="false" android:label="{}" android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
|
||||||
<activity android:name="{ACTIVITY}" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" android:exported="true" android:windowSoftInputMode="adjustResize">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
<meta-data android:name="android.app.lib_name" android:value="{}" />
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
"#,
|
|
||||||
xml_escape(application_id),
|
|
||||||
xml_escape(label),
|
|
||||||
xml_escape(library)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn xml_escape(value: &str) -> String {
|
|
||||||
value
|
|
||||||
.replace('&', "&")
|
|
||||||
.replace('<', "<")
|
|
||||||
.replace('>', ">")
|
|
||||||
.replace('"', """)
|
|
||||||
.replace('\'', "'")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn files_with_extension(root: &Path, extension: &str) -> Result<Vec<PathBuf>, String> {
|
|
||||||
fn visit(dir: &Path, extension: &str, output: &mut Vec<PathBuf>) -> Result<(), String> {
|
|
||||||
for entry in fs::read_dir(dir).map_err(io_error("read directory", dir))? {
|
|
||||||
let path = entry
|
|
||||||
.map_err(|error| format!("cannot read entry under {}: {error}", dir.display()))?
|
|
||||||
.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
visit(&path, extension, output)?;
|
|
||||||
} else if path.extension() == Some(OsStr::new(extension)) {
|
|
||||||
output.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
let mut files = Vec::new();
|
|
||||||
visit(root, extension, &mut files)?;
|
|
||||||
files.sort();
|
|
||||||
Ok(files)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_payload(
|
|
||||||
apk: &Path,
|
|
||||||
output: &Path,
|
|
||||||
dex: &Path,
|
|
||||||
library: &Path,
|
|
||||||
abi: &str,
|
|
||||||
library_name: &str,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let payload = output.join("payload");
|
|
||||||
let native_dir = payload.join("lib").join(abi);
|
|
||||||
fs::create_dir_all(&native_dir).map_err(io_error("create APK payload", &native_dir))?;
|
|
||||||
fs::copy(dex, payload.join("classes.dex")).map_err(io_error("stage classes.dex", dex))?;
|
|
||||||
let native_name = format!("lib{library_name}.so");
|
|
||||||
fs::copy(library, native_dir.join(&native_name))
|
|
||||||
.map_err(io_error("stage native library", library))?;
|
|
||||||
|
|
||||||
// `jar` is part of the JDK already needed for javac. Storing the native
|
|
||||||
// library uncompressed lets zipalign give it the 16 KiB page alignment
|
|
||||||
// required by current Android devices.
|
|
||||||
let mut jar = Command::new("jar");
|
|
||||||
jar.args(["--update", "--file"])
|
|
||||||
.arg(apk)
|
|
||||||
.args(["--no-manifest", "--no-compress", "-C"])
|
|
||||||
.arg(&payload)
|
|
||||||
.arg("classes.dex")
|
|
||||||
.arg("-C")
|
|
||||||
.arg(&payload)
|
|
||||||
.arg("lib");
|
|
||||||
run_command(
|
|
||||||
&mut jar,
|
|
||||||
"APK native payload",
|
|
||||||
"install a JDK containing jar",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign(sdk: &Sdk, options: &Options, input: &Path, output: &Path) -> Result<(), String> {
|
|
||||||
let (keystore, alias, store_password, key_password) = if options.release {
|
|
||||||
let store = env::var("IRIS_KEYSTORE_PASSWORD")
|
|
||||||
.map_err(|_| "IRIS_KEYSTORE_PASSWORD is unset for release signing".to_string())?;
|
|
||||||
let key = env::var("IRIS_KEY_PASSWORD").unwrap_or_else(|_| store.clone());
|
|
||||||
(
|
|
||||||
options.keystore.clone().unwrap(),
|
|
||||||
options.key_alias.clone().unwrap(),
|
|
||||||
store,
|
|
||||||
key,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let home = env::var_os("HOME")
|
|
||||||
.ok_or_else(|| "HOME is unset; cannot locate the Android debug keystore".to_string())?;
|
|
||||||
let keystore = PathBuf::from(home).join(".android/debug.keystore");
|
|
||||||
ensure_debug_keystore(&keystore)?;
|
|
||||||
(
|
|
||||||
keystore,
|
|
||||||
"androiddebugkey".into(),
|
|
||||||
"android".into(),
|
|
||||||
"android".into(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let mut command = Command::new(&sdk.apksigner);
|
|
||||||
// `install_and_run` uses `--no-streaming`, so it cannot consume the separate
|
|
||||||
// v4 `.idsig` file and retaining that sidecar beside the APK serves no caller.
|
|
||||||
command
|
|
||||||
.arg("sign")
|
|
||||||
.args([
|
|
||||||
"--v4-signing-enabled",
|
|
||||||
"false",
|
|
||||||
"--ks-pass",
|
|
||||||
"env:IRIS_APK_STORE_PASSWORD",
|
|
||||||
"--key-pass",
|
|
||||||
"env:IRIS_APK_KEY_PASSWORD",
|
|
||||||
"--ks-key-alias",
|
|
||||||
])
|
|
||||||
.arg(alias)
|
|
||||||
.arg("--ks")
|
|
||||||
.arg(keystore)
|
|
||||||
.arg("--out")
|
|
||||||
.arg(output)
|
|
||||||
.arg(input)
|
|
||||||
.env("IRIS_APK_STORE_PASSWORD", store_password)
|
|
||||||
.env("IRIS_APK_KEY_PASSWORD", key_password);
|
|
||||||
run_command(
|
|
||||||
&mut command,
|
|
||||||
"APK signing",
|
|
||||||
"check the keystore, alias, and signing passwords",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_debug_keystore(path: &Path) -> Result<(), String> {
|
|
||||||
if path.is_file() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
fs::create_dir_all(path.parent().unwrap())
|
|
||||||
.map_err(io_error("create Android configuration directory", path))?;
|
|
||||||
let mut keytool = Command::new("keytool");
|
|
||||||
keytool.args(["-genkeypair", "-keystore"]).arg(path).args([
|
|
||||||
"-storepass",
|
|
||||||
"android",
|
|
||||||
"-alias",
|
|
||||||
"androiddebugkey",
|
|
||||||
"-keypass",
|
|
||||||
"android",
|
|
||||||
"-dname",
|
|
||||||
"CN=Android Debug,O=Android,C=US",
|
|
||||||
"-keyalg",
|
|
||||||
"RSA",
|
|
||||||
"-keysize",
|
|
||||||
"2048",
|
|
||||||
"-validity",
|
|
||||||
"10000",
|
|
||||||
]);
|
|
||||||
run_command(
|
|
||||||
&mut keytool,
|
|
||||||
"Android debug key",
|
|
||||||
"install a JDK containing keytool",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify(sdk: &Sdk, apk: &Path) -> Result<(), String> {
|
|
||||||
let mut align = Command::new(&sdk.zipalign);
|
|
||||||
align.args(["-c", "-P", "16", "4"]).arg(apk);
|
|
||||||
run_command(
|
|
||||||
&mut align,
|
|
||||||
"APK alignment verification",
|
|
||||||
"this indicates a cargo-iris packaging defect",
|
|
||||||
)?;
|
|
||||||
let mut sign = Command::new(&sdk.apksigner);
|
|
||||||
sign.args(["verify", "--verbose"]).arg(apk);
|
|
||||||
run_command(
|
|
||||||
&mut sign,
|
|
||||||
"APK signature verification",
|
|
||||||
"this indicates a cargo-iris signing defect",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn install_and_run(built: &Built, device: &str) -> Result<(), String> {
|
|
||||||
if !built.sdk.adb.is_file() {
|
|
||||||
return Err(format!(
|
|
||||||
"Android SDK is missing adb at {}; install Platform Tools",
|
|
||||||
built.sdk.adb.display()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let mut install = Command::new(&built.sdk.adb);
|
|
||||||
install
|
|
||||||
.args(["-s", device, "install", "--no-streaming", "-r"])
|
|
||||||
.arg(&built.apk);
|
|
||||||
run_command(
|
|
||||||
&mut install,
|
|
||||||
"APK install",
|
|
||||||
"check that the selected device is connected and authorized",
|
|
||||||
)?;
|
|
||||||
let component = format!("{}/{}", built.application_id, ACTIVITY);
|
|
||||||
let mut launch = Command::new(&built.sdk.adb);
|
|
||||||
launch.args(["-s", device, "shell", "am", "start", "-n", &component]);
|
|
||||||
run_command(
|
|
||||||
&mut launch,
|
|
||||||
"APK launch",
|
|
||||||
"check the package activity in the generated APK",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recreate(path: &Path) -> Result<(), String> {
|
|
||||||
if path.exists() {
|
|
||||||
fs::remove_dir_all(path).map_err(io_error("clear prior APK output directory", path))?;
|
|
||||||
}
|
|
||||||
fs::create_dir_all(path).map_err(io_error("create APK output directory", path))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_command(command: &mut Command, thing: &str, fix: &str) -> Result<(), String> {
|
|
||||||
command.stdin(Stdio::null());
|
|
||||||
let status = command
|
|
||||||
.status()
|
|
||||||
.map_err(|error| format!("could not start {thing}: {error}; {fix}"))?;
|
|
||||||
if status.success() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(format!("{thing} failed with {status}; {fix}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn io_error<'a>(action: &'a str, path: &'a Path) -> impl FnOnce(std::io::Error) -> String + 'a {
|
|
||||||
move |error| format!("cannot {action} {}: {error}", path.display())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn application_ids_are_validated() {
|
|
||||||
assert!(validate_application_id("dev.iris.app.demo_2").is_ok());
|
|
||||||
assert!(validate_application_id("one").is_err());
|
|
||||||
assert!(validate_application_id("dev.2demo").is_err());
|
|
||||||
assert!(validate_application_id("dev.iris.bad-name").is_err());
|
|
||||||
assert_eq!(
|
|
||||||
default_application_id("demo-app", Some("color-picker")),
|
|
||||||
"dev.iris.example.demo_app.color_picker"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn manifest_values_are_escaped() {
|
|
||||||
let manifest = manifest_xml("dev.iris.demo", "A & <demo>", "demo", 37);
|
|
||||||
assert!(manifest.contains("A & <demo>"));
|
|
||||||
assert!(manifest.contains("android:minSdkVersion=\"29\""));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn an_unchanged_generated_file_is_not_rewritten() {
|
|
||||||
let root = env::temp_dir().join(format!("cargo-iris-write-test-{}", std::process::id()));
|
|
||||||
let path = root.join("generated.rs");
|
|
||||||
let _ = fs::remove_dir_all(&root);
|
|
||||||
fs::create_dir_all(&root).unwrap();
|
|
||||||
|
|
||||||
assert!(write_if_changed(&path, "first").unwrap());
|
|
||||||
assert!(!write_if_changed(&path, "first").unwrap());
|
|
||||||
assert!(write_if_changed(&path, "second").unwrap());
|
|
||||||
assert_eq!(fs::read_to_string(&path).unwrap(), "second");
|
|
||||||
|
|
||||||
fs::remove_dir_all(root).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn failed_build_staging_is_removed_when_its_scope_ends() {
|
|
||||||
let root = env::temp_dir().join(format!("cargo-iris-staging-test-{}", std::process::id()));
|
|
||||||
let _ = fs::remove_dir_all(&root);
|
|
||||||
fs::create_dir_all(&root).unwrap();
|
|
||||||
fs::write(root.join("intermediate"), "not reusable").unwrap();
|
|
||||||
|
|
||||||
{
|
|
||||||
let _cleanup = RemoveDirOnDrop(&root);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(!root.exists());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-3
@@ -3,13 +3,14 @@ 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 }
|
||||||
# Keeps renderer creation synchronous while retrieving wgpu's async error scope.
|
|
||||||
pollster = { workspace = true }
|
|
||||||
bytemuck ={ workspace = true }
|
bytemuck ={ workspace = true }
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
parley = { workspace = true }
|
parley = { workspace = true }
|
||||||
swash = { workspace = true }
|
swash = { workspace = true }
|
||||||
fxhash = { workspace = true }
|
fxhash = { workspace = true }
|
||||||
accesskit = { workspace = true }
|
log = { workspace = true }
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
UiRenderState, WidgetId,
|
|
||||||
util::{HashMap, HashSet},
|
|
||||||
};
|
|
||||||
use std::any::{Any, TypeId};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
|
||||||
pub struct ControllerId {
|
|
||||||
host: WidgetId,
|
|
||||||
kind: TypeId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ControllerId {
|
|
||||||
pub fn host(self) -> WidgetId {
|
|
||||||
self.host
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is<C: 'static>(self) -> bool {
|
|
||||||
self.kind == TypeId::of::<C>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum Command {
|
|
||||||
Copy,
|
|
||||||
SelectAll,
|
|
||||||
Escape,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
|
||||||
pub enum CommandResult {
|
|
||||||
Unused,
|
|
||||||
Used,
|
|
||||||
Copy(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ControllerValue: Any {
|
|
||||||
fn into_any(self: Box<Self>) -> Box<dyn Any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Any> ControllerValue for T {
|
|
||||||
fn into_any(self: Box<Self>) -> Box<dyn Any> {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Controller<Rsc>: ControllerValue {
|
|
||||||
fn command(&mut self, _command: Command, _rsc: &mut Rsc) -> CommandResult {
|
|
||||||
CommandResult::Unused
|
|
||||||
}
|
|
||||||
|
|
||||||
fn blocks_prior_input(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ControllerManager<Rsc> {
|
|
||||||
by_widget: HashMap<WidgetId, HashMap<TypeId, Box<dyn Controller<Rsc>>>>,
|
|
||||||
borrowed: HashSet<ControllerId>,
|
|
||||||
removed_while_borrowed: HashSet<WidgetId>,
|
|
||||||
command_target: Option<ControllerId>,
|
|
||||||
command_target_revision: u64,
|
|
||||||
command_boundary: Option<WidgetId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Rsc> Default for ControllerManager<Rsc> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
by_widget: Default::default(),
|
|
||||||
borrowed: Default::default(),
|
|
||||||
removed_while_borrowed: Default::default(),
|
|
||||||
command_target: None,
|
|
||||||
command_target_revision: 0,
|
|
||||||
command_boundary: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Rsc: 'static> ControllerManager<Rsc> {
|
|
||||||
#[track_caller]
|
|
||||||
pub fn register<C: Controller<Rsc>>(&mut self, host: WidgetId, controller: C) {
|
|
||||||
let kind = TypeId::of::<C>();
|
|
||||||
let id = ControllerId { host, kind };
|
|
||||||
assert!(
|
|
||||||
!self.borrowed.contains(&id),
|
|
||||||
"a controller cannot be replaced while it is handling input"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!self.removed_while_borrowed.contains(&host),
|
|
||||||
"a controller cannot be attached to a removed widget"
|
|
||||||
);
|
|
||||||
let old = self
|
|
||||||
.by_widget
|
|
||||||
.entry(host)
|
|
||||||
.or_default()
|
|
||||||
.insert(kind, Box::new(controller));
|
|
||||||
assert!(
|
|
||||||
old.is_none(),
|
|
||||||
"a widget cannot have two controllers of type {}",
|
|
||||||
std::any::type_name::<C>()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id<C: Controller<Rsc>>(&self, host: WidgetId) -> Option<ControllerId> {
|
|
||||||
let kind = TypeId::of::<C>();
|
|
||||||
self.by_widget
|
|
||||||
.get(&host)?
|
|
||||||
.contains_key(&kind)
|
|
||||||
.then_some(ControllerId { host, kind })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn nearest_id<C: Controller<Rsc>>(
|
|
||||||
&self,
|
|
||||||
mut origin: WidgetId,
|
|
||||||
render_state: &UiRenderState,
|
|
||||||
) -> Option<ControllerId> {
|
|
||||||
let kind = TypeId::of::<C>();
|
|
||||||
loop {
|
|
||||||
let candidate = ControllerId { host: origin, kind };
|
|
||||||
assert!(
|
|
||||||
!self.borrowed.contains(&candidate),
|
|
||||||
"a controller cannot re-enter itself while it is handling input"
|
|
||||||
);
|
|
||||||
if let Some(id) = self.id::<C>(origin) {
|
|
||||||
return Some(id);
|
|
||||||
}
|
|
||||||
origin = render_state.active.get(&origin)?.parent?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn path_to<C: Controller<Rsc>>(
|
|
||||||
&self,
|
|
||||||
mut origin: WidgetId,
|
|
||||||
render_state: &UiRenderState,
|
|
||||||
) -> Option<(ControllerId, Vec<WidgetId>)> {
|
|
||||||
let mut path = Vec::new();
|
|
||||||
loop {
|
|
||||||
path.push(origin);
|
|
||||||
if let Some(id) = self.id::<C>(origin) {
|
|
||||||
return Some((id, path));
|
|
||||||
}
|
|
||||||
origin = render_state.active.get(&origin)?.parent?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn take<C: Controller<Rsc>>(&mut self, id: ControllerId) -> Option<C> {
|
|
||||||
if id.kind != TypeId::of::<C>() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
assert!(
|
|
||||||
!self.borrowed.contains(&id),
|
|
||||||
"a controller cannot re-enter itself while it is handling input"
|
|
||||||
);
|
|
||||||
let boxed = self.by_widget.get_mut(&id.host)?.remove(&id.kind)?;
|
|
||||||
self.borrowed.insert(id);
|
|
||||||
let boxed = boxed.into_any();
|
|
||||||
boxed.downcast().ok().map(|boxed| *boxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn put<C: Controller<Rsc>>(&mut self, id: ControllerId, controller: C) {
|
|
||||||
debug_assert_eq!(id.kind, TypeId::of::<C>());
|
|
||||||
assert!(
|
|
||||||
self.borrowed.remove(&id),
|
|
||||||
"restored an unborrowed controller"
|
|
||||||
);
|
|
||||||
if self.finish_removed_host(id.host) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let old = self
|
|
||||||
.by_widget
|
|
||||||
.entry(id.host)
|
|
||||||
.or_default()
|
|
||||||
.insert(id.kind, Box::new(controller));
|
|
||||||
debug_assert!(old.is_none(), "a controller was re-entered while borrowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn take_dyn(&mut self, id: ControllerId) -> Option<Box<dyn Controller<Rsc>>> {
|
|
||||||
assert!(
|
|
||||||
!self.borrowed.contains(&id),
|
|
||||||
"a controller cannot re-enter itself while it is handling input"
|
|
||||||
);
|
|
||||||
let controller = self.by_widget.get_mut(&id.host)?.remove(&id.kind)?;
|
|
||||||
self.borrowed.insert(id);
|
|
||||||
Some(controller)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn put_dyn(&mut self, id: ControllerId, controller: Box<dyn Controller<Rsc>>) {
|
|
||||||
assert!(
|
|
||||||
self.borrowed.remove(&id),
|
|
||||||
"restored an unborrowed controller"
|
|
||||||
);
|
|
||||||
if self.finish_removed_host(id.host) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let old = self
|
|
||||||
.by_widget
|
|
||||||
.entry(id.host)
|
|
||||||
.or_default()
|
|
||||||
.insert(id.kind, controller);
|
|
||||||
debug_assert!(old.is_none(), "a controller was re-entered while borrowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_command_target(&mut self, target: Option<ControllerId>) {
|
|
||||||
self.command_target = target;
|
|
||||||
self.command_target_revision = self.command_target_revision.wrapping_add(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn command_target(&self) -> Option<ControllerId> {
|
|
||||||
self.command_target
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn command_target_blocks_input(&self) -> bool {
|
|
||||||
let Some(id) = self.command_target else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
self.by_widget
|
|
||||||
.get(&id.host)
|
|
||||||
.and_then(|controllers| controllers.get(&id.kind))
|
|
||||||
.is_some_and(|controller| controller.blocks_prior_input())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn command_target_revision(&self) -> u64 {
|
|
||||||
self.command_target_revision
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn command_boundary(&self) -> Option<WidgetId> {
|
|
||||||
self.command_boundary
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_below(
|
|
||||||
&self,
|
|
||||||
mut widget: WidgetId,
|
|
||||||
ancestor: WidgetId,
|
|
||||||
render_state: &UiRenderState,
|
|
||||||
) -> bool {
|
|
||||||
loop {
|
|
||||||
let Some(parent) = render_state
|
|
||||||
.active
|
|
||||||
.get(&widget)
|
|
||||||
.and_then(|active| active.parent)
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if parent == ancestor {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
widget = parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn set_command_boundary(&mut self, boundary: Option<WidgetId>) {
|
|
||||||
self.command_boundary = boundary;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(&mut self, host: WidgetId) {
|
|
||||||
self.by_widget.remove(&host);
|
|
||||||
if self.borrowed.iter().any(|id| id.host == host) {
|
|
||||||
self.removed_while_borrowed.insert(host);
|
|
||||||
}
|
|
||||||
if self.command_target.is_some_and(|id| id.host == host) {
|
|
||||||
self.command_target = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn take_command_target(
|
|
||||||
&mut self,
|
|
||||||
) -> Option<(ControllerId, Box<dyn Controller<Rsc>>)> {
|
|
||||||
let id = self.command_target?;
|
|
||||||
match self.take_dyn(id) {
|
|
||||||
Some(controller) => Some((id, controller)),
|
|
||||||
None => {
|
|
||||||
self.command_target = None;
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn restore(&mut self, id: ControllerId, controller: Box<dyn Controller<Rsc>>) {
|
|
||||||
self.put_dyn(id, controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true when a host disappeared during its controller callback,
|
|
||||||
/// in which case restoring the temporarily extracted value would revive
|
|
||||||
/// state belonging to a dead widget generation.
|
|
||||||
fn finish_removed_host(&mut self, host: WidgetId) -> bool {
|
|
||||||
if !self.removed_while_borrowed.contains(&host) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if !self.borrowed.iter().any(|id| id.host == host) {
|
|
||||||
self.removed_while_borrowed.remove(&host);
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ActiveData, ControllerManager, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents,
|
ActiveData, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents, IdLike, LayerId,
|
||||||
IdLike, LayerId, WeakWidget, WidgetEventFn, WidgetId,
|
WeakWidget, WidgetEventFn, WidgetId,
|
||||||
util::{HashMap, HashSet, TypeMap},
|
util::{HashMap, HashSet, TypeMap},
|
||||||
};
|
};
|
||||||
use std::{any::TypeId, rc::Rc};
|
use std::{any::TypeId, rc::Rc};
|
||||||
@@ -8,15 +8,13 @@ use std::{any::TypeId, rc::Rc};
|
|||||||
pub struct EventManager<Rsc> {
|
pub struct EventManager<Rsc> {
|
||||||
widget_to_types: HashMap<WidgetId, HashSet<TypeId>>,
|
widget_to_types: HashMap<WidgetId, HashSet<TypeId>>,
|
||||||
types: TypeMap<dyn EventManagerLike<Rsc>>,
|
types: TypeMap<dyn EventManagerLike<Rsc>>,
|
||||||
pub controllers: ControllerManager<Rsc>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Rsc: 'static> Default for EventManager<Rsc> {
|
impl<Rsc> Default for EventManager<Rsc> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
widget_to_types: Default::default(),
|
widget_to_types: Default::default(),
|
||||||
types: Default::default(),
|
types: Default::default(),
|
||||||
controllers: Default::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,7 +54,6 @@ impl<Rsc: HasEvents + 'static> EventsLike for EventManager<Rsc> {
|
|||||||
for t in self.widget_to_types.get(&id).into_flat_iter() {
|
for t in self.widget_to_types.get(&id).into_flat_iter() {
|
||||||
self.types.get_mut(t).unwrap().remove(id);
|
self.types.get_mut(t).unwrap().remove(id);
|
||||||
}
|
}
|
||||||
self.controllers.remove(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&mut self, active: &ActiveData) {
|
fn draw(&mut self, active: &ActiveData) {
|
||||||
@@ -140,26 +137,16 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The event lists this widget was registered with (`register`'s
|
|
||||||
/// `event` argument, one per call), without running anything. Lets a
|
|
||||||
/// caller ask "would this widget's registrations match the current
|
|
||||||
/// state" separately from actually dispatching to it -- used by
|
|
||||||
/// `sense.rs` to decide whether a widget genuinely consumes a scroll
|
|
||||||
/// or press this frame (so a lower layer can still receive it if not)
|
|
||||||
/// without that decision being conflated with "the cursor happens to
|
|
||||||
/// be over it," which is all `run_fn` running something tells you.
|
|
||||||
pub fn registered(&self, id: WidgetId) -> impl Iterator<Item = &E> {
|
|
||||||
self.map.get(&id).into_iter().flatten().map(|(e, _)| e)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_fn<'a>(
|
pub fn run_fn<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: impl IdLike,
|
id: impl IdLike,
|
||||||
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) + 'a {
|
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) -> bool + 'a {
|
||||||
let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
|
let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
|
||||||
move |ctx, rsc| {
|
move |ctx, rsc| {
|
||||||
|
let mut consumed = false;
|
||||||
for (e, f) in fs {
|
for (e, f) in fs {
|
||||||
if let Some(data) = e.should_run(&ctx.data) {
|
if let Some(data) = e.should_run(&ctx.data) {
|
||||||
|
consumed |= e.consumes(&data);
|
||||||
f(
|
f(
|
||||||
EventCtx {
|
EventCtx {
|
||||||
state: ctx.state,
|
state: ctx.state,
|
||||||
@@ -169,6 +156,7 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
consumed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
mod controller;
|
|
||||||
mod ctx;
|
mod ctx;
|
||||||
mod manager;
|
mod manager;
|
||||||
mod rsc;
|
mod rsc;
|
||||||
|
|
||||||
pub use controller::*;
|
|
||||||
pub use ctx::*;
|
pub use ctx::*;
|
||||||
pub use manager::*;
|
pub use manager::*;
|
||||||
pub use rsc::*;
|
pub use rsc::*;
|
||||||
@@ -11,11 +9,19 @@ pub use rsc::*;
|
|||||||
pub trait Event: Sized + 'static + Clone {
|
pub trait Event: Sized + 'static + Clone {
|
||||||
type Data<'a>: Clone = ();
|
type Data<'a>: Clone = ();
|
||||||
type State: Default = ();
|
type State: Default = ();
|
||||||
|
/// State the whole event type keeps, rather than one copy per widget.
|
||||||
type Global: Default = ();
|
type Global: Default = ();
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
||||||
Some(data.clone())
|
Some(data.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether having run on this data uses up whatever triggered it, so
|
||||||
|
/// nothing further should see it.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn consumes(&self, data: &Self::Data<'_>) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait EventLike {
|
pub trait EventLike {
|
||||||
|
|||||||
+3
-89
@@ -1,6 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Command, CommandResult, Controller, ControllerId, Event, EventCtx, EventLike, EventManager,
|
Event, EventCtx, EventLike, EventManager, IdLike, UiRsc, WeakWidget, Widget, WidgetEventFn,
|
||||||
IdLike, UiRsc, WeakWidget, Widget, WidgetEventFn,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait HasState: 'static {
|
pub trait HasState: 'static {
|
||||||
@@ -19,101 +18,16 @@ pub trait HasEvents: Sized + UiRsc + HasState {
|
|||||||
) {
|
) {
|
||||||
self.events_mut().register(id, event, f);
|
self.events_mut().register(id, event, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_controller<W: ?Sized, C: Controller<Self>>(
|
|
||||||
&mut self,
|
|
||||||
id: WeakWidget<W>,
|
|
||||||
controller: C,
|
|
||||||
) {
|
|
||||||
self.events_mut().controllers.register(id.id(), controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_controller<C: Controller<Self>, T>(
|
|
||||||
&mut self,
|
|
||||||
id: ControllerId,
|
|
||||||
f: impl FnOnce(&mut C, &mut Self) -> T,
|
|
||||||
) -> Option<T> {
|
|
||||||
let mut controller = self.events_mut().controllers.take::<C>(id)?;
|
|
||||||
let result = f(&mut controller, self);
|
|
||||||
self.events_mut().controllers.put(id, controller);
|
|
||||||
Some(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_nearest_controller<C: Controller<Self>, T>(
|
|
||||||
&mut self,
|
|
||||||
origin: impl IdLike,
|
|
||||||
f: impl FnOnce(ControllerId, &mut C, &mut Self) -> T,
|
|
||||||
) -> Option<T> {
|
|
||||||
let render_handle = self.ui().render_state();
|
|
||||||
let id = self
|
|
||||||
.events()
|
|
||||||
.controllers
|
|
||||||
.nearest_id::<C>(origin.id(), &render_handle.get())?;
|
|
||||||
self.with_controller(id, |controller, rsc| f(id, controller, rsc))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_command_target(&mut self, target: Option<ControllerId>) {
|
|
||||||
self.events_mut().controllers.set_command_target(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_command(&mut self, command: Command) -> CommandResult {
|
|
||||||
let revision = self.events().controllers.command_target_revision();
|
|
||||||
if let Some(boundary) = self.events().controllers.command_boundary() {
|
|
||||||
let render_handle = self.ui().render_state();
|
|
||||||
let outside_boundary =
|
|
||||||
self.events()
|
|
||||||
.controllers
|
|
||||||
.command_target()
|
|
||||||
.is_none_or(|target| {
|
|
||||||
!self.events().controllers.is_below(
|
|
||||||
target.host(),
|
|
||||||
boundary,
|
|
||||||
&render_handle.get(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
if outside_boundary {
|
|
||||||
return CommandResult::Unused;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let Some((id, mut controller)) = self.events_mut().controllers.take_command_target() else {
|
|
||||||
return CommandResult::Unused;
|
|
||||||
};
|
|
||||||
let result = controller.command(command, self);
|
|
||||||
self.events_mut().controllers.restore(id, controller);
|
|
||||||
if command == Command::Escape
|
|
||||||
&& result != CommandResult::Unused
|
|
||||||
&& self.events().controllers.command_target_revision() == revision
|
|
||||||
{
|
|
||||||
self.set_command_target(None);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
fn run_command_before(&mut self, command: Command, boundary: impl IdLike) -> CommandResult {
|
|
||||||
let old = self.events().controllers.command_boundary();
|
|
||||||
self.events_mut()
|
|
||||||
.controllers
|
|
||||||
.set_command_boundary(Some(boundary.id()));
|
|
||||||
let result = self.run_command(command);
|
|
||||||
self.events_mut().controllers.set_command_boundary(old);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait RunEvents: HasEvents {
|
pub trait RunEvents: HasEvents {
|
||||||
|
/// Whether anything that ran used up what triggered it.
|
||||||
fn run_event<E: EventLike>(
|
fn run_event<E: EventLike>(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: impl IdLike,
|
id: impl IdLike,
|
||||||
data: <E::Event as Event>::Data<'_>,
|
data: <E::Event as Event>::Data<'_>,
|
||||||
state: &mut Self::State,
|
state: &mut Self::State,
|
||||||
) {
|
) -> bool {
|
||||||
// Keep the last completed frame read-locked for the whole callback.
|
|
||||||
// Rsc methods may take further shared reads through `render_state`,
|
|
||||||
// while any attempt to start a render from an event fails at the
|
|
||||||
// mutable-borrow boundary instead of exposing an in-progress tree.
|
|
||||||
let render_handle = self.ui().render_state();
|
|
||||||
let _render_state = render_handle.get();
|
|
||||||
let f = self.events_mut().get_type::<E>().run_fn(id);
|
let f = self.events_mut().get_type::<E>().run_fn(id);
|
||||||
f(EventCtx { state, data }, self)
|
f(EventCtx { state, data }, self)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,574 @@
|
|||||||
|
use crate::{UiNum, util::Vec2};
|
||||||
|
use std::{
|
||||||
|
fmt::{Debug, Display, Formatter},
|
||||||
|
ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A number held as a whole count of `1 / 2^SHIFT`.
|
||||||
|
///
|
||||||
|
/// Layout reaches one place by more than one route -- a box composed down the
|
||||||
|
/// chain, and the same box summed from what its children asked for -- and has
|
||||||
|
/// to decide whether the two are the same place. In floats they land a few
|
||||||
|
/// bits apart, which is a defect wherever the answer changes what is drawn
|
||||||
|
/// rather than where. Here adding and subtracting are exact, a multiply
|
||||||
|
/// drops to the step below, and a conversion between grids takes the nearest
|
||||||
|
/// one, so two routes to one place land on one number and everything
|
||||||
|
/// downstream compares for equality instead of for nearness.
|
||||||
|
///
|
||||||
|
/// `SHIFT` is the number of fractional bits, which is what makes the steps
|
||||||
|
/// divide a whole number: a power of two also converts to `f32` without
|
||||||
|
/// rounding while the value fits in its mantissa.
|
||||||
|
///
|
||||||
|
/// Arithmetic wraps at the ends of the range, the way the `i32` underneath
|
||||||
|
/// does. Saturating instead was measured at a twelfth of layout's
|
||||||
|
/// instructions -- five per add against one -- to keep the ordering of
|
||||||
|
/// coordinates two million pixels out, where nothing draws anyway. A value
|
||||||
|
/// off the end is a defect either way; wrapping makes it an obvious one.
|
||||||
|
/// Only [`Self::from_f32`] clamps, since a float has further to come from.
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, bytemuck::Pod, bytemuck::Zeroable,
|
||||||
|
)]
|
||||||
|
pub struct Fixed<const SHIFT: u32>(i32);
|
||||||
|
|
||||||
|
/// A length or a coordinate in pixels, in steps of `1/1024`. Finer than
|
||||||
|
/// anything a display can show, and exact in `f32` up to 16,384 px, which is
|
||||||
|
/// what lets the same number reach the GPU.
|
||||||
|
pub type Px = Fixed<PX_SHIFT>;
|
||||||
|
|
||||||
|
/// How many bits of a pixel a [`Px`] keeps. One place, because [`PxVec2`]
|
||||||
|
/// and the shader's own decoding are the same grid or nothing lines up.
|
||||||
|
pub const PX_SHIFT: u32 = 10;
|
||||||
|
|
||||||
|
/// A share of what a box has left over, which is a weight beside its
|
||||||
|
/// siblings rather than a fraction of anything: a list divides its room by
|
||||||
|
/// the total of these, so the range has to hold a whole list's worth and the
|
||||||
|
/// precision only has to tell two weights apart.
|
||||||
|
pub type Weight = Fixed<16>;
|
||||||
|
|
||||||
|
/// A fraction of a box. Twenty-four bits of it, which matches `f32` around a
|
||||||
|
/// half and beats it above one -- where anchors actually sit -- and leaves
|
||||||
|
/// +/-128 of range, enough to sum a hundred children each asking for a whole
|
||||||
|
/// box. A `leftover` weight is not one of these: it is a share of what is
|
||||||
|
/// left rather than a fraction of anything, and it sums over a whole list.
|
||||||
|
pub type Rel = Fixed<REL_SHIFT>;
|
||||||
|
|
||||||
|
/// How many bits of a box a [`Rel`] keeps, beside [`PX_SHIFT`] and for the
|
||||||
|
/// same reason.
|
||||||
|
pub const REL_SHIFT: u32 = 24;
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> Fixed<SHIFT> {
|
||||||
|
pub const ZERO: Self = Self(0);
|
||||||
|
pub const ONE: Self = Self::one();
|
||||||
|
/// The gap between neighbouring values, which is also how far apart two
|
||||||
|
/// numbers can be and still mean the same place.
|
||||||
|
pub const STEP: Self = Self(1);
|
||||||
|
/// Also what stands in for an unbounded end: compared against, never
|
||||||
|
/// added to, since arithmetic wraps past it.
|
||||||
|
pub const MIN: Self = Self(i32::MIN);
|
||||||
|
pub const MAX: Self = Self(i32::MAX);
|
||||||
|
|
||||||
|
const fn one() -> Self {
|
||||||
|
assert!(SHIFT < 31, "a Fixed needs a bit for the whole part");
|
||||||
|
Self(1 << SHIFT)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_raw(raw: i32) -> Self {
|
||||||
|
Self(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The count of steps, for a caller that needs the representation rather
|
||||||
|
/// than the number.
|
||||||
|
pub const fn raw(self) -> i32 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_int(v: i32) -> Self {
|
||||||
|
Self(v.wrapping_mul(Self::one().0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rounds to the nearest step, and clamps to the ends of the grid rather
|
||||||
|
/// than wrapping: this is where a number from outside arrives, and a float
|
||||||
|
/// has the range to be anywhere. A NaN has no nearest step and becomes
|
||||||
|
/// zero, which is a caller's mistake rather than a value worth carrying.
|
||||||
|
///
|
||||||
|
/// Half-away is written out rather than called through `f32::round`,
|
||||||
|
/// which is not `const`: a layout constant has to stay a constant.
|
||||||
|
pub const fn from_f32(v: f32) -> Self {
|
||||||
|
debug_assert!(!v.is_nan(), "a NaN has no place on the grid");
|
||||||
|
let scaled = v * Self::one().0 as f32;
|
||||||
|
// Above 2^23 an `f32` has no fractional part left to round, and
|
||||||
|
// adding a half there rounds the number itself up instead. The cast
|
||||||
|
// saturates at both ends and sends NaN to zero, which is the
|
||||||
|
// behaviour wanted at both.
|
||||||
|
const WHOLE: f32 = (1 << 23) as f32;
|
||||||
|
Self(match (scaled >= WHOLE, scaled <= -WHOLE, scaled < 0.0) {
|
||||||
|
(true, _, _) | (_, true, _) => scaled as i32,
|
||||||
|
(_, _, true) => (scaled - 0.5) as i32,
|
||||||
|
_ => (scaled + 0.5) as i32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first step at or above `v`, where [`Self::from_f32`] takes the
|
||||||
|
/// nearest one and is below it half the time. For a bound that has to
|
||||||
|
/// admit the value it came from: a measurement rounded down is a bound
|
||||||
|
/// that leaves out the thing it was measured from.
|
||||||
|
pub const fn ceil_from_f32(v: f32) -> Self {
|
||||||
|
let nearest = Self::from_f32(v);
|
||||||
|
match nearest.to_f32() < v {
|
||||||
|
true => nearest.next_up(),
|
||||||
|
false => nearest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From a number as it is written in source -- `16`, `1.5` -- which is
|
||||||
|
/// the other place a value enters the grid.
|
||||||
|
pub fn from_num(v: impl UiNum) -> Self {
|
||||||
|
Self::from_f32(v.to_f32())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn to_f32(self) -> f32 {
|
||||||
|
self.0 as f32 / Self::one().0 as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same value on another grid, rounded where the new one is coarser.
|
||||||
|
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
|
||||||
|
Fixed(match TO >= SHIFT {
|
||||||
|
true => self.0 << (TO - SHIFT),
|
||||||
|
false => shift_round(self.0 as i64, SHIFT - TO) as i32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn add(self, rhs: Self) -> Self {
|
||||||
|
Self(self.0.wrapping_add(rhs.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn sub(self, rhs: Self) -> Self {
|
||||||
|
Self(self.0.wrapping_sub(rhs.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn neg(self) -> Self {
|
||||||
|
Self(self.0.wrapping_neg())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scaled by a number on any grid, which is how a length takes a fraction
|
||||||
|
/// of itself and keeps being a length: the product is measured in the
|
||||||
|
/// receiver's steps.
|
||||||
|
///
|
||||||
|
/// Dropped to the step below rather than taken to the nearest one
|
||||||
|
/// (Bryan, 2026-09-16), which costs a share a thousandth of a pixel of
|
||||||
|
/// its row -- less than an even number of pixels draws. Toward negative
|
||||||
|
/// infinity on both sides of zero, since that is a shift and nothing
|
||||||
|
/// else: a value and its negation therefore land different distances
|
||||||
|
/// from where they came, so a flipped span can sit a step from its
|
||||||
|
/// mirror image.
|
||||||
|
pub const fn mul<const BY: u32>(self, by: Fixed<BY>) -> Self {
|
||||||
|
Self(((self.0 as i64 * by.0 as i64) >> BY) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Repeated a whole number of times, which no grid rounds.
|
||||||
|
pub const fn mul_int(self, by: i32) -> Self {
|
||||||
|
Self(self.0.wrapping_mul(by))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Divided into a whole number of parts, rounded to the nearest step.
|
||||||
|
pub const fn div_int(self, by: i32) -> Self {
|
||||||
|
debug_assert!(by != 0, "no part of nothing");
|
||||||
|
if by == 0 {
|
||||||
|
return Self::ZERO;
|
||||||
|
}
|
||||||
|
Self(div_round(self.0 as i64, by as i64) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Divided by a number on any grid. A zero divisor is a caller bug -- a
|
||||||
|
/// box of no length has no fraction of itself -- and answers with the end
|
||||||
|
/// of the range so that a release build lays out something absurd rather
|
||||||
|
/// than dying.
|
||||||
|
pub const fn div<const BY: u32>(self, by: Fixed<BY>) -> Self {
|
||||||
|
debug_assert!(by.0 != 0, "dividing by a length of zero");
|
||||||
|
if by.0 == 0 {
|
||||||
|
return match self.0 < 0 {
|
||||||
|
true => Self::MIN,
|
||||||
|
false => Self::MAX,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Self(div_round((self.0 as i64) << BY, by.0 as i64) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `num / den` on *this* grid rather than on theirs, for weights coarser
|
||||||
|
/// than the share they divide.
|
||||||
|
pub const fn ratio<const OF: u32>(num: Fixed<OF>, den: Fixed<OF>) -> Self {
|
||||||
|
debug_assert!(den.0 != 0, "no part of a whole of nothing");
|
||||||
|
if den.0 == 0 {
|
||||||
|
return Self::ZERO;
|
||||||
|
}
|
||||||
|
Self(div_round((num.0 as i64) << SHIFT, den.0 as i64) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `from` and `to` a fraction of the way apart, the fraction being the
|
||||||
|
/// receiver -- the argument order [`crate::util::LerpUtil`] already uses.
|
||||||
|
pub const fn lerp<const OF: u32>(self, from: Fixed<OF>, to: Fixed<OF>) -> Fixed<OF> {
|
||||||
|
from.add(to.sub(from).mul(self))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn min(self, other: Self) -> Self {
|
||||||
|
match self.0 < other.0 {
|
||||||
|
true => self,
|
||||||
|
false => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn max(self, other: Self) -> Self {
|
||||||
|
match self.0 > other.0 {
|
||||||
|
true => self,
|
||||||
|
false => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn abs(self) -> Self {
|
||||||
|
Self(self.0.wrapping_abs())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn clamp(self, lo: Self, hi: Self) -> Self {
|
||||||
|
debug_assert!(lo.0 <= hi.0, "an empty clamp has no answer");
|
||||||
|
self.max(lo).min(hi)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The next value along, for an interval that must not admit its own
|
||||||
|
/// boundary. The step is the whole gap, so there is nothing to exclude
|
||||||
|
/// between this and the boundary itself.
|
||||||
|
pub const fn next_up(self) -> Self {
|
||||||
|
Self(self.0.wrapping_add(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn next_down(self) -> Self {
|
||||||
|
Self(self.0.wrapping_sub(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Back to a single step, rounding halves away from zero so that a value and
|
||||||
|
/// its negation round to the same distance.
|
||||||
|
const fn shift_round(v: i64, bits: u32) -> i64 {
|
||||||
|
let half = (1i64 << bits) >> 1;
|
||||||
|
match v < 0 {
|
||||||
|
true => -((-v + half) >> bits),
|
||||||
|
false => (v + half) >> bits,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn div_round(num: i64, den: i64) -> i64 {
|
||||||
|
let (q, rem) = (num / den, num % den);
|
||||||
|
match rem.unsigned_abs() * 2 >= den.unsigned_abs() {
|
||||||
|
true => match (num < 0) == (den < 0) {
|
||||||
|
true => q + 1,
|
||||||
|
false => q - 1,
|
||||||
|
},
|
||||||
|
false => q,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toward positive infinity when `up`, toward negative infinity otherwise.
|
||||||
|
pub(crate) const fn div_toward(num: i64, den: i64, up: bool) -> i64 {
|
||||||
|
let (q, rem) = (num / den, num % den);
|
||||||
|
if rem == 0 {
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
match (rem < 0) == (den < 0) {
|
||||||
|
true => q + up as i64,
|
||||||
|
false => q - !up as i64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamped to the ends, unlike a [`Fixed`]'s own arithmetic: a range of box
|
||||||
|
/// lengths that runs past `i32` really is unbounded.
|
||||||
|
pub(crate) const fn narrow(v: i64) -> i32 {
|
||||||
|
if v > i32::MAX as i64 {
|
||||||
|
return i32::MAX;
|
||||||
|
}
|
||||||
|
if v < i32::MIN as i64 {
|
||||||
|
return i32::MIN;
|
||||||
|
}
|
||||||
|
v as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> Add for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self {
|
||||||
|
Fixed::add(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> Sub for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self {
|
||||||
|
Fixed::sub(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> Neg for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn neg(self) -> Self {
|
||||||
|
Fixed::neg(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> AddAssign for Fixed<SHIFT> {
|
||||||
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
|
*self = Fixed::add(*self, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> SubAssign for Fixed<SHIFT> {
|
||||||
|
fn sub_assign(&mut self, rhs: Self) {
|
||||||
|
*self = Fixed::sub(*self, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32, const BY: u32> Mul<Fixed<BY>> for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn mul(self, rhs: Fixed<BY>) -> Self {
|
||||||
|
Fixed::mul(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32, const BY: u32> Div<Fixed<BY>> for Fixed<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn div(self, rhs: Fixed<BY>) -> Self {
|
||||||
|
Fixed::div(self, rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> Display for Fixed<SHIFT> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self.to_f32(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints the number rather than the count of steps: a failing layout test
|
||||||
|
/// reports boxes, and `1126` is not a height anybody can read.
|
||||||
|
impl<const SHIFT: u32> Debug for Fixed<SHIFT> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self.to_f32(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two of them, for the places a size or a position needs both axes: a
|
||||||
|
/// window, a box in pixels, a pointer. Held apart from [`crate::util::Vec2`]
|
||||||
|
/// because that one is what the GPU and the platform speak.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct FixedVec2<const SHIFT: u32> {
|
||||||
|
pub x: Fixed<SHIFT>,
|
||||||
|
pub y: Fixed<SHIFT>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PxVec2 = FixedVec2<PX_SHIFT>;
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
||||||
|
pub const ZERO: Self = Self::splat(Fixed::ZERO);
|
||||||
|
|
||||||
|
pub const fn new(x: Fixed<SHIFT>, y: Fixed<SHIFT>) -> Self {
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn splat(v: Fixed<SHIFT>) -> Self {
|
||||||
|
Self { x: v, y: v }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_f32(v: Vec2) -> Self {
|
||||||
|
Self::new(Fixed::from_f32(v.x), Fixed::from_f32(v.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first step at or above each part, for a measurement reported as a
|
||||||
|
/// box: what it occupies is not less than what was measured.
|
||||||
|
pub fn ceil_from_f32(v: Vec2) -> Self {
|
||||||
|
Self::new(Fixed::ceil_from_f32(v.x), Fixed::ceil_from_f32(v.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_f32(self) -> Vec2 {
|
||||||
|
Vec2::new(self.x.to_f32(), self.y.to_f32())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn div_int(self, by: i32) -> Self {
|
||||||
|
Self::new(self.x.div_int(by), self.y.div_int(by))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn min(self, other: Self) -> Self {
|
||||||
|
Self::new(self.x.min(other.x), self.y.min(other.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn max(self, other: Self) -> Self {
|
||||||
|
Self::new(self.x.max(other.x), self.y.max(other.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `impl_op!` names one concrete type, and this one is generic.
|
||||||
|
const impl<const SHIFT: u32> Add for FixedVec2<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self {
|
||||||
|
Self::new(self.x.add(rhs.x), self.y.add(rhs.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> Sub for FixedVec2<SHIFT> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self {
|
||||||
|
Self::new(self.x.sub(rhs.x), self.y.sub(rhs.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> AddAssign for FixedVec2<SHIFT> {
|
||||||
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
|
*self = Add::add(*self, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl<const SHIFT: u32> SubAssign for FixedVec2<SHIFT> {
|
||||||
|
fn sub_assign(&mut self, rhs: Self) {
|
||||||
|
*self = Sub::sub(*self, rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> Debug for FixedVec2<SHIFT> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "({}, {})", self.x, self.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> Display for FixedVec2<SHIFT> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "({}, {})", self.x, self.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_sum_of_steps_does_not_drift() {
|
||||||
|
let mut at = Px::ZERO;
|
||||||
|
for _ in 0..20_000 {
|
||||||
|
at += Px::from_raw(3);
|
||||||
|
}
|
||||||
|
assert_eq!(at, Px::from_raw(60_000));
|
||||||
|
for _ in 0..20_000 {
|
||||||
|
at -= Px::from_raw(3);
|
||||||
|
}
|
||||||
|
assert_eq!(at, Px::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_pixel_survives_the_trip_through_f32() {
|
||||||
|
for raw in [0, 1, -1, 64, -1000, 16_777_215, -16_777_215] {
|
||||||
|
let px = Px::from_raw(raw);
|
||||||
|
assert_eq!(Px::from_f32(px.to_f32()), px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fraction_of_a_length_is_a_length() {
|
||||||
|
let half = Px::from_int(100) * Rel::from_f32(0.5);
|
||||||
|
assert_eq!(half, Px::from_int(50));
|
||||||
|
assert_eq!(Px::from_int(100) * Rel::ONE, Px::from_int(100));
|
||||||
|
assert_eq!(Px::from_int(100) * Rel::ZERO, Px::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toward negative infinity on both sides of zero, which is what makes
|
||||||
|
/// it a shift rather than a shift and a sign branch -- and what makes a
|
||||||
|
/// value and its negation land different distances from where they came,
|
||||||
|
/// so a flipped span can sit a step from its mirror image.
|
||||||
|
#[test]
|
||||||
|
fn a_multiply_drops_to_the_step_below_on_both_sides_of_zero() {
|
||||||
|
// A step and a half of one, which has no step of its own.
|
||||||
|
let step_and_a_half = Rel::from_f32(1.5).div_int(Px::ONE.raw());
|
||||||
|
assert_eq!(Px::ONE * step_and_a_half, Px::from_raw(1));
|
||||||
|
assert_eq!(Px::ONE.neg() * step_and_a_half, Px::from_raw(-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A division rounds to the nearest step, so it cannot put back the
|
||||||
|
/// steps a truncating multiply dropped: a round trip comes back short,
|
||||||
|
/// never long, and by the few steps the two operations gave up.
|
||||||
|
#[test]
|
||||||
|
fn dividing_by_a_fraction_cannot_undo_a_truncating_multiply() {
|
||||||
|
let third = Rel::ONE / Rel::from_int(3);
|
||||||
|
let len = Px::from_int(300);
|
||||||
|
let back = len * third / third;
|
||||||
|
assert!(back <= len, "{back:?} is longer than {len:?}");
|
||||||
|
assert!(len - back <= Px::from_raw(3), "{back:?} against {len:?}");
|
||||||
|
assert_eq!(Px::from_int(100) / Rel::from_f32(0.5), Px::from_int(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bound a greedy line break needs: the width it was measured at is
|
||||||
|
/// not on the grid, and the narrowest box the break still holds for is
|
||||||
|
/// the step at or above it, never the one below.
|
||||||
|
#[test]
|
||||||
|
fn a_ceiling_never_lands_below_the_number_it_came_from() {
|
||||||
|
let step = 1.0 / (1 << PX_SHIFT) as f32;
|
||||||
|
for n in 0..64 {
|
||||||
|
let v = 189.0 + n as f32 * step / 3.0;
|
||||||
|
let up = Px::ceil_from_f32(v);
|
||||||
|
assert!(up.to_f32() >= v, "{up:?} is below {v}");
|
||||||
|
assert!(
|
||||||
|
up.to_f32() - v < step,
|
||||||
|
"{up:?} is more than a step above {v}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// An exact step is its own ceiling.
|
||||||
|
assert_eq!(Px::ceil_from_f32(189.5), Px::from_f32(189.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_number_from_outside_is_clamped_to_the_grid() {
|
||||||
|
assert_eq!(Px::from_f32(1e12), Px::MAX);
|
||||||
|
assert_eq!(Px::from_f32(-1e12), Px::MIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_coarser_grid_rounds_and_a_finer_one_does_not() {
|
||||||
|
// A third, which neither grid holds exactly.
|
||||||
|
let third = Rel::ONE / Rel::from_int(3);
|
||||||
|
assert_eq!(third.to_scale::<6>(), Fixed::<6>::from_raw(21));
|
||||||
|
let coarse = Fixed::<6>::from_raw(21);
|
||||||
|
assert_eq!(coarse.to_scale::<24>().to_scale::<6>(), coarse);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lerp_takes_the_fraction_as_the_receiver() {
|
||||||
|
let (from, to) = (Px::from_int(10), Px::from_int(20));
|
||||||
|
assert_eq!(Rel::ZERO.lerp(from, to), from);
|
||||||
|
assert_eq!(Rel::ONE.lerp(from, to), to);
|
||||||
|
assert_eq!(Rel::from_f32(0.5).lerp(from, to), Px::from_int(15));
|
||||||
|
assert_eq!(Rel::from_f32(0.5).lerp(to, from), Px::from_int(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_ratio_is_finer_than_the_weights_it_divides() {
|
||||||
|
let (one, three) = (Weight::ONE, Weight::from_int(3));
|
||||||
|
// A third, which the weights' own grid could only hold to 1/65536.
|
||||||
|
assert_eq!(Rel::ratio(one, three), Rel::from_raw(5592405));
|
||||||
|
assert_eq!(Rel::ratio(three, three), Rel::ONE);
|
||||||
|
assert_eq!(Rel::ratio(Weight::ZERO, three), Rel::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nothing_sits_between_a_value_and_the_next_one() {
|
||||||
|
let at = Px::from_int(3);
|
||||||
|
assert_eq!(at.next_up().next_down(), at);
|
||||||
|
assert_eq!(at.next_up().raw() - at.raw(), 1);
|
||||||
|
assert!(at.next_down() < at && at < at.next_up());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_prints_the_number_rather_than_the_steps() {
|
||||||
|
assert_eq!(format!("{:?}", Px::from_f32(17.59375)), "17.59375");
|
||||||
|
assert_eq!(format!("{}", Px::from_int(-2)), "-2");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,485 @@
|
|||||||
|
//! 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, LayoutLen, PxVec2, Size, UiRegion, WidgetId};
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
fmt::Write,
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) enum Counter {
|
||||||
|
Updates,
|
||||||
|
DrawRequests,
|
||||||
|
WidgetDraws,
|
||||||
|
RegionNodeDraws,
|
||||||
|
SizeReads,
|
||||||
|
HintHits,
|
||||||
|
HintMisses,
|
||||||
|
ReuseAttempts,
|
||||||
|
ReuseExact,
|
||||||
|
ReuseMoved,
|
||||||
|
ReuseDirty,
|
||||||
|
ReuseWrongParent,
|
||||||
|
ReuseRemapped,
|
||||||
|
ReuseOutside,
|
||||||
|
ReuseWrongLayer,
|
||||||
|
ReuseWrongNode,
|
||||||
|
QueuePops,
|
||||||
|
DepthReads,
|
||||||
|
LocalRedraws,
|
||||||
|
SizeChanges,
|
||||||
|
ReaderEdges,
|
||||||
|
PrimitiveWrites,
|
||||||
|
TextRenders,
|
||||||
|
TextShapeHits,
|
||||||
|
TextShapes,
|
||||||
|
TextBreaks,
|
||||||
|
GlyphPlacements,
|
||||||
|
OutsidePinnedLen,
|
||||||
|
OutsideRelBase,
|
||||||
|
OutsideRegion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Counter {
|
||||||
|
const COUNT: usize = Self::OutsideRegion as usize + 1;
|
||||||
|
|
||||||
|
const NAMES: [&'static str; Self::COUNT] = [
|
||||||
|
"updates",
|
||||||
|
"draw requests",
|
||||||
|
"widget draws",
|
||||||
|
"region-node draws",
|
||||||
|
"draw-result size reads",
|
||||||
|
"hint hits",
|
||||||
|
"hint misses",
|
||||||
|
"reuse attempts",
|
||||||
|
"reuse exact",
|
||||||
|
"reuse moved",
|
||||||
|
"reuse: dirty",
|
||||||
|
"reuse: wrong parent",
|
||||||
|
"reuse remapped",
|
||||||
|
"reuse: outside what it holds for",
|
||||||
|
"reuse: another layer",
|
||||||
|
"reuse: region-node choice changed",
|
||||||
|
"redraw queue pops",
|
||||||
|
"depth reads",
|
||||||
|
"local redraws",
|
||||||
|
"size changes",
|
||||||
|
"reader edges",
|
||||||
|
"primitive writes",
|
||||||
|
"text renders",
|
||||||
|
"text shape hits",
|
||||||
|
"text shapes",
|
||||||
|
"text line breaks",
|
||||||
|
"glyph placements",
|
||||||
|
"reuse outside: the length it was pinned to",
|
||||||
|
"reuse outside: a rel base",
|
||||||
|
"reuse outside: a region length",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) enum TimerKind {
|
||||||
|
Update,
|
||||||
|
FullLayout,
|
||||||
|
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",
|
||||||
|
"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,
|
||||||
|
WrongLayer,
|
||||||
|
Remapped,
|
||||||
|
Outside,
|
||||||
|
Undrawn,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: PxVec2,
|
||||||
|
region_node: bool,
|
||||||
|
},
|
||||||
|
Reuse {
|
||||||
|
id: WidgetId,
|
||||||
|
outcome: ReuseOutcome,
|
||||||
|
},
|
||||||
|
SizeReported {
|
||||||
|
id: WidgetId,
|
||||||
|
size: Size,
|
||||||
|
},
|
||||||
|
RegionNode {
|
||||||
|
id: WidgetId,
|
||||||
|
parent: WidgetId,
|
||||||
|
region: UiRegion,
|
||||||
|
},
|
||||||
|
SizeRead {
|
||||||
|
id: WidgetId,
|
||||||
|
reader: WidgetId,
|
||||||
|
size: Size,
|
||||||
|
},
|
||||||
|
HintRead {
|
||||||
|
id: WidgetId,
|
||||||
|
reader: WidgetId,
|
||||||
|
axis: Axis,
|
||||||
|
hint: Option<LayoutLen>,
|
||||||
|
},
|
||||||
|
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: PxVec2,
|
||||||
|
region_node: bool,
|
||||||
|
) {
|
||||||
|
trace(
|
||||||
|
id,
|
||||||
|
TraceEvent::DrawRequest {
|
||||||
|
id,
|
||||||
|
parent,
|
||||||
|
region,
|
||||||
|
pixel_size,
|
||||||
|
region_node,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 region_node(id: WidgetId, parent: WidgetId, region: UiRegion) {
|
||||||
|
trace(id, TraceEvent::RegionNode { 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<LayoutLen>) {
|
||||||
|
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,8 +10,12 @@
|
|||||||
#![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 fixed;
|
||||||
mod num;
|
mod num;
|
||||||
mod orientation;
|
mod orientation;
|
||||||
mod primitive;
|
mod primitive;
|
||||||
@@ -23,9 +27,12 @@ pub mod util;
|
|||||||
|
|
||||||
pub use attr::*;
|
pub use attr::*;
|
||||||
pub use event::*;
|
pub use event::*;
|
||||||
|
pub use fixed::*;
|
||||||
pub use num::*;
|
pub use num::*;
|
||||||
pub use orientation::*;
|
pub use orientation::*;
|
||||||
pub use primitive::*;
|
pub use primitive::*;
|
||||||
pub use render::*;
|
pub use render::*;
|
||||||
pub use ui::*;
|
pub use ui::*;
|
||||||
pub use widget::*;
|
pub use widget::*;
|
||||||
|
|
||||||
|
pub type UiColor = primitive::Color<u8>;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::vec2;
|
use crate::{Px, Rel};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
pub struct Align {
|
pub struct Align {
|
||||||
pub x: Option<AxisAlign>,
|
pub x: Option<AxisAlign>,
|
||||||
pub y: Option<AxisAlign>,
|
pub y: Option<AxisAlign>,
|
||||||
@@ -30,20 +30,32 @@ impl Align {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
/// Where a widget sits in a box longer than it is. The default is the middle,
|
||||||
pub enum AxisAlign {
|
/// because the two edges are the ones that assume a direction: which of them
|
||||||
Neg,
|
/// is the near one depends on the writing system and on which way a container
|
||||||
Center,
|
/// runs, and the middle is the same either way.
|
||||||
Pos,
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
}
|
pub struct AxisAlign(Rel);
|
||||||
|
|
||||||
impl AxisAlign {
|
impl AxisAlign {
|
||||||
pub const fn rel(&self) -> f32 {
|
pub const NEG: Self = Self::new(0.0);
|
||||||
match self {
|
pub const CENTER: Self = Self::new(0.5);
|
||||||
Self::Neg => 0.0,
|
pub const POS: Self = Self::new(1.0);
|
||||||
Self::Center => 0.5,
|
|
||||||
Self::Pos => 1.0,
|
pub const fn new(rel: f32) -> Self {
|
||||||
|
Self(Rel::from_f32(rel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A fraction of the room left over, which is what the layout reads: the
|
||||||
|
/// three constants are the familiar places along it, not the only ones.
|
||||||
|
pub const fn rel(&self) -> Rel {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AxisAlign {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::CENTER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,45 +65,60 @@ pub struct CardinalAlign {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CardinalAlign {
|
impl CardinalAlign {
|
||||||
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::Neg);
|
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::NEG);
|
||||||
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::Center);
|
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::CENTER);
|
||||||
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::Pos);
|
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::POS);
|
||||||
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::Neg);
|
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::NEG);
|
||||||
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::Center);
|
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::CENTER);
|
||||||
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::Pos);
|
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::POS);
|
||||||
|
|
||||||
pub const fn new(axis: Axis, align: AxisAlign) -> Self {
|
pub const fn new(axis: Axis, align: AxisAlign) -> Self {
|
||||||
Self { axis, align }
|
Self { axis, align }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
pub struct RegionAlign {
|
pub struct RegionAlign {
|
||||||
pub x: AxisAlign,
|
pub x: AxisAlign,
|
||||||
pub y: AxisAlign,
|
pub y: AxisAlign,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegionAlign {
|
impl RegionAlign {
|
||||||
pub const TOP_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Neg);
|
/// Both axes at the near edge: the start of a box in its own orientation.
|
||||||
pub const TOP_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Neg);
|
pub const NEAR: Self = Self {
|
||||||
pub const TOP_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Neg);
|
x: AxisAlign::NEG,
|
||||||
pub const CENTER_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Center);
|
y: AxisAlign::NEG,
|
||||||
pub const CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Center);
|
};
|
||||||
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Center);
|
|
||||||
pub const BOT_LEFT: Self = Self::new(AxisAlign::Neg, AxisAlign::Pos);
|
pub fn axis(&self, axis: Axis) -> AxisAlign {
|
||||||
pub const BOT_CENTER: Self = Self::new(AxisAlign::Center, AxisAlign::Pos);
|
match axis {
|
||||||
pub const BOT_RIGHT: Self = Self::new(AxisAlign::Pos, AxisAlign::Pos);
|
Axis::X => self.x,
|
||||||
|
Axis::Y => self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn axis_mut(&mut self, axis: Axis) -> &mut AxisAlign {
|
||||||
|
match axis {
|
||||||
|
Axis::X => &mut self.x,
|
||||||
|
Axis::Y => &mut self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegionAlign {
|
||||||
|
pub const TOP_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::NEG);
|
||||||
|
pub const TOP_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::NEG);
|
||||||
|
pub const TOP_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::NEG);
|
||||||
|
pub const CENTER_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::CENTER);
|
||||||
|
pub const CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::CENTER);
|
||||||
|
pub const CENTER_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::CENTER);
|
||||||
|
pub const BOT_LEFT: Self = Self::new(AxisAlign::NEG, AxisAlign::POS);
|
||||||
|
pub const BOT_CENTER: Self = Self::new(AxisAlign::CENTER, AxisAlign::POS);
|
||||||
|
pub const BOT_RIGHT: Self = Self::new(AxisAlign::POS, AxisAlign::POS);
|
||||||
|
|
||||||
pub const fn new(x: AxisAlign, y: AxisAlign) -> Self {
|
pub const fn new(x: AxisAlign, y: AxisAlign) -> Self {
|
||||||
Self { x, y }
|
Self { x, y }
|
||||||
}
|
}
|
||||||
pub const fn rel(&self) -> Vec2 {
|
|
||||||
vec2(self.x.rel(), self.y.rel())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn pos(self) -> UiVec2 {
|
|
||||||
UiVec2::from(self)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiVec2 {
|
impl UiVec2 {
|
||||||
@@ -144,16 +171,15 @@ impl Vec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiScalar {
|
impl Len {
|
||||||
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
||||||
let rel = align.rel();
|
let rel = align.rel();
|
||||||
let mut start = UiScalar::rel(rel);
|
let rest = Rel::ONE.sub(rel);
|
||||||
start.abs -= self.abs * rel;
|
let at = Len::from_parts(rel, Px::ZERO);
|
||||||
start.rel -= self.rel * rel;
|
UiSpan {
|
||||||
let mut end = UiScalar::rel(rel);
|
start: Len::from_parts(at.rel.sub(self.rel.mul(rel)), at.px.sub(self.px.mul(rel))),
|
||||||
end.abs += self.abs * (1.0 - rel);
|
end: Len::from_parts(at.rel.add(self.rel.mul(rest)), at.px.add(self.px.mul(rest))),
|
||||||
end.rel += self.rel * (1.0 - rel);
|
}
|
||||||
UiSpan { start, end }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,8 +195,8 @@ impl From<RegionAlign> for Align {
|
|||||||
impl From<Align> for RegionAlign {
|
impl From<Align> for RegionAlign {
|
||||||
fn from(align: Align) -> Self {
|
fn from(align: Align) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: align.x.unwrap_or(AxisAlign::Center),
|
x: align.x.unwrap_or(AxisAlign::CENTER),
|
||||||
y: align.y.unwrap_or(AxisAlign::Center),
|
y: align.y.unwrap_or(AxisAlign::CENTER),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,6 +219,15 @@ impl From<CardinalAlign> for Align {
|
|||||||
|
|
||||||
const impl From<RegionAlign> for UiVec2 {
|
const impl From<RegionAlign> for UiVec2 {
|
||||||
fn from(align: RegionAlign) -> Self {
|
fn from(align: RegionAlign) -> Self {
|
||||||
Self::rel(align.rel())
|
Self::new(
|
||||||
|
Len::from_parts(align.x.rel(), Px::ZERO),
|
||||||
|
Len::from_parts(align.y.rel(), Px::ZERO),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegionAlign {
|
||||||
|
pub const fn pos(self) -> UiVec2 {
|
||||||
|
UiVec2::from(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,23 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::{Fixed, FixedVec2};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum Axis {
|
pub enum Axis {
|
||||||
X,
|
X,
|
||||||
Y,
|
Y,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Axis {
|
||||||
|
/// A per-axis pair with `aligned` on this axis and `ortho` on the other,
|
||||||
|
/// which is what `from_axis` does for a vector.
|
||||||
|
pub fn pair<T>(self, aligned: T, ortho: T) -> [T; 2] {
|
||||||
|
match self {
|
||||||
|
Self::X => [aligned, ortho],
|
||||||
|
Self::Y => [ortho, aligned],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::ops::Not for Axis {
|
impl std::ops::Not for Axis {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
@@ -40,6 +52,29 @@ pub enum Sign {
|
|||||||
Pos,
|
Pos,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<const SHIFT: u32> FixedVec2<SHIFT> {
|
||||||
|
pub const fn axis(&self, axis: Axis) -> Fixed<SHIFT> {
|
||||||
|
match axis {
|
||||||
|
Axis::X => self.x,
|
||||||
|
Axis::Y => self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn axis_mut(&mut self, axis: Axis) -> &mut Fixed<SHIFT> {
|
||||||
|
match axis {
|
||||||
|
Axis::X => &mut self.x,
|
||||||
|
Axis::Y => &mut self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_axis(axis: Axis, aligned: Fixed<SHIFT>, ortho: Fixed<SHIFT>) -> Self {
|
||||||
|
match axis {
|
||||||
|
Axis::X => Self::new(aligned, ortho),
|
||||||
|
Axis::Y => Self::new(ortho, aligned),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Vec2 {
|
impl Vec2 {
|
||||||
pub fn axis(&self, axis: Axis) -> f32 {
|
pub fn axis(&self, axis: Axis) -> f32 {
|
||||||
match axis {
|
match axis {
|
||||||
|
|||||||
+110
-276
@@ -1,5 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{UiNum, util::impl_op};
|
use crate::{Px, PxVec2, Rel, UiNum, Weight, util::impl_op};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||||
pub struct Size {
|
pub struct Size {
|
||||||
@@ -7,56 +7,29 @@ pub struct Size {
|
|||||||
pub y: LayoutLen,
|
pub y: LayoutLen,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A length resolved from physical pixels, density-independent pixels, and a
|
/// What a widget asks for along one axis: a [`Len`] -- pixels and a fraction
|
||||||
/// fraction of a reference length. Unlike [`LayoutLen`], it carries no claim
|
/// of the box it is given -- plus a share of whatever is left over once
|
||||||
/// on space left over by a layout container.
|
/// everything fixed has been taken. The parts add up rather than choosing
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
/// between one another.
|
||||||
pub struct Len {
|
///
|
||||||
/// Physical pixels -- a raw device pixel, unaffected by the display's
|
/// Only a container dividing its room can answer a share, so a length nobody
|
||||||
/// density. Rare to want directly (a hairline border is the usual
|
/// divides is a `Len`: a position, a padding, a cap, anything already
|
||||||
/// case); most sizes should be `dp` instead. See `dp`'s own doc for why
|
/// resolved.
|
||||||
/// the two are kept separate rather than one field a caller has to
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
/// remember to pre-multiply.
|
|
||||||
pub abs: f32,
|
|
||||||
pub dp: f32,
|
|
||||||
pub rel: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A widget length plus its proportional claim on the space left after fixed
|
|
||||||
/// and relative lengths have been allocated.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct LayoutLen {
|
pub struct LayoutLen {
|
||||||
pub abs: f32,
|
pub px: Px,
|
||||||
pub dp: f32,
|
pub rel: Rel,
|
||||||
pub rel: f32,
|
pub leftover: Weight,
|
||||||
pub rest: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N: UiNum> From<N> for Len {
|
|
||||||
fn from(value: N) -> Self {
|
|
||||||
Len::abs(value.to_f32())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<N: UiNum> From<N> for LayoutLen {
|
impl<N: UiNum> From<N> for LayoutLen {
|
||||||
fn from(value: N) -> Self {
|
fn from(value: N) -> Self {
|
||||||
Self::abs(value.to_f32())
|
LayoutLen::px(value.to_f32())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Len> for LayoutLen {
|
impl<Nx: UiNum, Ny: UiNum> From<(Nx, Ny)> for Size {
|
||||||
fn from(value: Len) -> Self {
|
fn from((x, y): (Nx, Ny)) -> Self {
|
||||||
Self {
|
|
||||||
abs: value.abs,
|
|
||||||
dp: value.dp,
|
|
||||||
rel: value.rel,
|
|
||||||
rest: 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<X: Into<LayoutLen>, Y: Into<LayoutLen>> From<(X, Y)> for Size {
|
|
||||||
fn from((x, y): (X, Y)) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
x: x.into(),
|
x: x.into(),
|
||||||
y: y.into(),
|
y: y.into(),
|
||||||
@@ -64,33 +37,51 @@ impl<X: Into<LayoutLen>, Y: Into<LayoutLen>> From<(X, Y)> for Size {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A length with no share in it is a length a container does not have to
|
||||||
|
/// divide, which is one it can always give.
|
||||||
|
impl From<Len> for LayoutLen {
|
||||||
|
fn from(len: Len) -> Self {
|
||||||
|
Self {
|
||||||
|
px: len.px,
|
||||||
|
rel: len.rel,
|
||||||
|
leftover: Weight::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<LayoutLen> for Size {
|
impl From<LayoutLen> for Size {
|
||||||
fn from(value: LayoutLen) -> Self {
|
fn from(value: LayoutLen) -> Self {
|
||||||
Self { x: value, y: value }
|
Self { x: value, y: value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Len> for Size {
|
|
||||||
fn from(value: Len) -> Self {
|
|
||||||
Self::from(LayoutLen::from(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Size {
|
impl Size {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
x: LayoutLen::ZERO,
|
x: LayoutLen::ZERO,
|
||||||
y: LayoutLen::ZERO,
|
y: LayoutLen::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const REST: Self = Self {
|
pub const LEFTOVER: Self = Self {
|
||||||
x: LayoutLen::REST,
|
x: LayoutLen::LEFTOVER,
|
||||||
y: LayoutLen::REST,
|
y: LayoutLen::LEFTOVER,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn abs(v: Vec2) -> Self {
|
/// From something measured outside layout -- a texture, a shaped line --
|
||||||
|
/// which is where a size in floats comes from.
|
||||||
|
pub fn px(v: Vec2) -> Self {
|
||||||
|
Self::from_px(PxVec2::from_f32(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_px(v: PxVec2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: LayoutLen::abs(v.x),
|
x: LayoutLen {
|
||||||
y: LayoutLen::abs(v.y),
|
px: v.x,
|
||||||
|
..LayoutLen::ZERO
|
||||||
|
},
|
||||||
|
y: LayoutLen {
|
||||||
|
px: v.y,
|
||||||
|
..LayoutLen::ZERO
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,17 +92,17 @@ impl Size {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rest(v: Vec2) -> Self {
|
pub fn leftover(v: Vec2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
x: LayoutLen::rest(v.x),
|
x: LayoutLen::leftover(v.x),
|
||||||
y: LayoutLen::rest(v.y),
|
y: LayoutLen::leftover(v.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_uivec2(self, density: f32) -> UiVec2 {
|
pub fn to_uivec2(self) -> UiVec2 {
|
||||||
UiVec2 {
|
UiVec2 {
|
||||||
x: self.x.apply_rest(density),
|
x: self.x.apply_leftover(),
|
||||||
y: self.y.apply_rest(density),
|
y: self.y.apply_leftover(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,137 +125,67 @@ impl Size {
|
|||||||
Axis::Y => self.y,
|
Axis::Y => self.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn axis_mut(&mut self, axis: Axis) -> &mut LayoutLen {
|
||||||
|
match axis {
|
||||||
|
Axis::X => &mut self.x,
|
||||||
|
Axis::Y => &mut self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutLen {
|
impl LayoutLen {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
abs: 0.0,
|
px: Px::ZERO,
|
||||||
dp: 0.0,
|
rel: Rel::ZERO,
|
||||||
rel: 0.0,
|
leftover: Weight::ZERO,
|
||||||
rest: 0.0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const REST: Self = Self {
|
pub const LEFTOVER: Self = Self {
|
||||||
abs: 0.0,
|
px: Px::ZERO,
|
||||||
dp: 0.0,
|
rel: Rel::ZERO,
|
||||||
rel: 0.0,
|
leftover: Weight::ONE,
|
||||||
rest: 1.0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Resolves to a `UiScalar`, folding `dp` into `abs` pixels against
|
/// The whole of what is left over counts as the whole box, which is what
|
||||||
/// `density` (physical pixels per dp -- 1.0 on a desktop or an
|
/// a length means to something that is not dividing a box between
|
||||||
/// unscaled display, `content_scale` on Android; see `dp`'s field
|
/// siblings -- a scroll asking how long its content is.
|
||||||
/// doc). Every other component of `LayoutLen` is already resolution-
|
pub fn apply_leftover(&self) -> Len {
|
||||||
/// independent (`rel` is a fraction of the parent; `rest` becomes a
|
let share = match self.leftover > Weight::ZERO {
|
||||||
/// fraction too, below), so `density` only ever touches this one term.
|
true => Rel::ONE,
|
||||||
pub fn apply_rest(&self, density: f32) -> UiScalar {
|
false => Rel::ZERO,
|
||||||
UiScalar {
|
};
|
||||||
rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
|
Len::from_parts(self.rel.add(share), self.px)
|
||||||
abs: self.abs + self.dp * density,
|
}
|
||||||
|
|
||||||
|
/// This length, given as a part of a box `len` long, as a part of the
|
||||||
|
/// box `len` is itself a part of. The share is untouched: it is a claim
|
||||||
|
/// on whoever divides the room, not a fraction of anything.
|
||||||
|
pub const fn within_len(self, len: Len) -> Self {
|
||||||
|
let part = Len::from_parts(self.rel, self.px).within_len(len);
|
||||||
|
Self {
|
||||||
|
px: part.px,
|
||||||
|
rel: part.rel,
|
||||||
|
leftover: self.leftover,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The same fold as [`Self::apply_rest`] but staying a `LayoutLen`, so
|
pub fn px(px: impl UiNum) -> Self {
|
||||||
/// `rest` survives: `dp` becomes physical pixels and every other
|
|
||||||
/// component is left alone.
|
|
||||||
///
|
|
||||||
/// **A `LayoutLen` a widget *reports* must have been through this.** `dp` is
|
|
||||||
/// an input unit -- a number the widget author wrote -- and the
|
|
||||||
/// containers that consume a reported length read `abs`/`rel`/`rest`
|
|
||||||
/// directly (`Span::draw`'s placement arithmetic, `Pad`'s addition),
|
|
||||||
/// so a reported `dp` is silently worth zero. That is what made the
|
|
||||||
/// composer's bar collapse to nothing the moment its content grew past
|
|
||||||
/// `MaxSize`'s cap: the cap was `dp(168)` and was returned unresolved,
|
|
||||||
/// so the bar was given a slot of 0 and the field inside it was panned
|
|
||||||
/// out of a container measured at -63px. `UiRenderState::draw_inner`
|
|
||||||
/// debug-asserts the invariant after every `Widget::draw`.
|
|
||||||
pub fn fold_dp(&self, density: f32) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
abs: self.abs + self.dp * density,
|
px: Px::from_num(px),
|
||||||
dp: 0.0,
|
..Self::ZERO
|
||||||
rel: self.rel,
|
|
||||||
rest: self.rest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn abs(abs: impl UiNum) -> Self {
|
|
||||||
Self {
|
|
||||||
abs: abs.to_f32(),
|
|
||||||
dp: 0.0,
|
|
||||||
rel: 0.0,
|
|
||||||
rest: 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn dp(dp: impl UiNum) -> Self {
|
|
||||||
Self {
|
|
||||||
abs: 0.0,
|
|
||||||
dp: dp.to_f32(),
|
|
||||||
rel: 0.0,
|
|
||||||
rest: 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rel(rel: impl UiNum) -> Self {
|
pub fn rel(rel: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
abs: 0.0,
|
rel: Rel::from_num(rel),
|
||||||
dp: 0.0,
|
..Self::ZERO
|
||||||
rel: rel.to_f32(),
|
|
||||||
rest: 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rest(ratio: impl UiNum) -> Self {
|
pub fn leftover(ratio: impl UiNum) -> Self {
|
||||||
Self {
|
Self {
|
||||||
abs: 0.0,
|
leftover: Weight::from_num(ratio),
|
||||||
dp: 0.0,
|
..Self::ZERO
|
||||||
rel: 0.0,
|
|
||||||
rest: ratio.to_f32(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Len {
|
|
||||||
pub const ZERO: Self = Self {
|
|
||||||
abs: 0.0,
|
|
||||||
dp: 0.0,
|
|
||||||
rel: 0.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn abs(abs: impl UiNum) -> Self {
|
|
||||||
Self {
|
|
||||||
abs: abs.to_f32(),
|
|
||||||
dp: 0.0,
|
|
||||||
rel: 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dp(dp: impl UiNum) -> Self {
|
|
||||||
Self {
|
|
||||||
abs: 0.0,
|
|
||||||
dp: dp.to_f32(),
|
|
||||||
rel: 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rel(rel: impl UiNum) -> Self {
|
|
||||||
Self {
|
|
||||||
abs: 0.0,
|
|
||||||
dp: 0.0,
|
|
||||||
rel: rel.to_f32(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn fold_dp(self, density: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
abs: self.abs + self.dp * density,
|
|
||||||
dp: 0.0,
|
|
||||||
rel: self.rel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn resolve(self, density: f32) -> UiScalar {
|
|
||||||
let folded = self.fold_dp(density);
|
|
||||||
UiScalar {
|
|
||||||
rel: folded.rel,
|
|
||||||
abs: folded.abs,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,58 +193,26 @@ impl Len {
|
|||||||
pub mod len_fns {
|
pub mod len_fns {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn abs(abs: impl UiNum) -> Len {
|
pub fn px(px: impl UiNum) -> LayoutLen {
|
||||||
Len::abs(abs)
|
LayoutLen::px(px)
|
||||||
}
|
}
|
||||||
pub fn dp(dp: impl UiNum) -> Len {
|
pub fn rel(rel: impl UiNum) -> LayoutLen {
|
||||||
Len::dp(dp)
|
LayoutLen::rel(rel)
|
||||||
}
|
|
||||||
pub fn rel(rel: impl UiNum) -> Len {
|
|
||||||
Len::rel(rel)
|
|
||||||
}
|
|
||||||
pub fn rest(ratio: impl UiNum) -> LayoutLen {
|
|
||||||
LayoutLen {
|
|
||||||
abs: 0.0,
|
|
||||||
dp: 0.0,
|
|
||||||
rel: 0.0,
|
|
||||||
rest: ratio.to_f32(),
|
|
||||||
}
|
}
|
||||||
|
pub fn leftover(ratio: impl UiNum) -> LayoutLen {
|
||||||
|
LayoutLen::leftover(ratio)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_op!(LayoutLen Add add; abs dp rel rest);
|
impl_op!(same LayoutLen Add add; px rel leftover);
|
||||||
impl_op!(LayoutLen Sub sub; abs dp rel rest);
|
impl_op!(same LayoutLen Sub sub; px rel leftover);
|
||||||
impl_op!(Len Add add; abs dp rel);
|
|
||||||
impl_op!(Len Sub sub; abs dp rel);
|
|
||||||
|
|
||||||
impl std::ops::Add<Len> for LayoutLen {
|
impl_op!(same Size Add add; x y);
|
||||||
type Output = Self;
|
impl_op!(same Size Sub sub; x y);
|
||||||
|
|
||||||
fn add(self, rhs: Len) -> Self::Output {
|
|
||||||
self + Self::from(rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Sub<Len> for LayoutLen {
|
|
||||||
type Output = Self;
|
|
||||||
|
|
||||||
fn sub(self, rhs: Len) -> Self::Output {
|
|
||||||
self - Self::from(rhs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_op!(Size Add add; x y);
|
|
||||||
impl_op!(Size Sub sub; x y);
|
|
||||||
|
|
||||||
impl Default for LayoutLen {
|
impl Default for LayoutLen {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::rest(1.0)
|
Self::leftover(1.0)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Len {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::ZERO
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,70 +224,15 @@ impl std::fmt::Display for Size {
|
|||||||
|
|
||||||
impl std::fmt::Display for LayoutLen {
|
impl std::fmt::Display for LayoutLen {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
if self.abs != 0.0 {
|
if self.px != Px::ZERO {
|
||||||
write!(f, "{} abs;", self.abs)?;
|
write!(f, "{} px;", self.px)?;
|
||||||
}
|
}
|
||||||
if self.dp != 0.0 {
|
if self.rel != Rel::ZERO {
|
||||||
write!(f, "{} dp;", self.dp)?;
|
|
||||||
}
|
|
||||||
if self.rel != 0.0 {
|
|
||||||
write!(f, "{} rel;", self.rel)?;
|
write!(f, "{} rel;", self.rel)?;
|
||||||
}
|
}
|
||||||
if self.rest != 0.0 {
|
if self.leftover != Weight::ZERO {
|
||||||
write!(f, "{} rest;", self.rest)?;
|
write!(f, "{} leftover;", self.leftover)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Len {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
if self.abs != 0.0 {
|
|
||||||
write!(f, "{} abs;", self.abs)?;
|
|
||||||
}
|
|
||||||
if self.dp != 0.0 {
|
|
||||||
write!(f, "{} dp;", self.dp)?;
|
|
||||||
}
|
|
||||||
if self.rel != 0.0 {
|
|
||||||
write!(f, "{} rel;", self.rel)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn an_ordinary_length_enters_layout_without_claiming_rest() {
|
|
||||||
let layout = LayoutLen::from(Len {
|
|
||||||
abs: 3.0,
|
|
||||||
dp: 4.0,
|
|
||||||
rel: 0.5,
|
|
||||||
});
|
|
||||||
assert_eq!(layout.abs, 3.0);
|
|
||||||
assert_eq!(layout.dp, 4.0);
|
|
||||||
assert_eq!(layout.rel, 0.5);
|
|
||||||
assert_eq!(layout.rest, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ordinary_and_layout_lengths_keep_their_own_defaults() {
|
|
||||||
assert_eq!(Len::default(), Len::ZERO);
|
|
||||||
assert_eq!(LayoutLen::default(), LayoutLen::REST);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn adding_an_ordinary_length_preserves_a_layout_claim() {
|
|
||||||
assert_eq!(
|
|
||||||
LayoutLen::rest(2) + Len::dp(8),
|
|
||||||
LayoutLen {
|
|
||||||
abs: 0.0,
|
|
||||||
dp: 8.0,
|
|
||||||
rel: 0.0,
|
|
||||||
rest: 2.0,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+142
-142
@@ -1,41 +1,46 @@
|
|||||||
use std::{fmt::Display, hash::Hash, marker::Destruct};
|
use std::{fmt::Display, marker::Destruct};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{Px, PxVec2, Rel, UiNum, util::impl_op};
|
||||||
UiNum,
|
|
||||||
util::{LerpUtil, impl_op},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||||
pub struct UiVec2 {
|
pub struct UiVec2 {
|
||||||
pub x: UiScalar,
|
pub x: Len,
|
||||||
pub y: UiScalar,
|
pub y: Len,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiVec2 {
|
impl UiVec2 {
|
||||||
pub const ZERO: Self = Self {
|
pub const ZERO: Self = Self {
|
||||||
x: UiScalar::ZERO,
|
x: Len::ZERO,
|
||||||
y: UiScalar::ZERO,
|
y: Len::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const fn new(x: UiScalar, y: UiScalar) -> Self {
|
pub const fn new(x: Len, y: Len) -> Self {
|
||||||
Self { x, y }
|
Self { x, y }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn abs(abs: impl const Into<Vec2>) -> Self {
|
pub const fn px(px: impl const Into<Vec2>) -> Self {
|
||||||
let abs = abs.into();
|
let px = px.into();
|
||||||
Self {
|
Self {
|
||||||
x: UiScalar::abs(abs.x),
|
x: Len::px(px.x),
|
||||||
y: UiScalar::abs(abs.y),
|
y: Len::px(px.y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From lengths already on the grid, with no fraction of a box.
|
||||||
|
pub const fn from_px(px: PxVec2) -> Self {
|
||||||
|
Self {
|
||||||
|
x: Len::from_parts(Rel::ZERO, px.x),
|
||||||
|
y: Len::from_parts(Rel::ZERO, px.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel(rel: impl const Into<Vec2>) -> Self {
|
pub const fn rel(rel: impl const Into<Vec2>) -> Self {
|
||||||
let rel = rel.into();
|
let rel = rel.into();
|
||||||
Self {
|
Self {
|
||||||
x: UiScalar::rel(rel.x),
|
x: Len::rel(rel.x),
|
||||||
y: UiScalar::rel(rel.y),
|
y: Len::rel(rel.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,37 +61,29 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn outside(&self, region: &UiRegion) -> UiVec2 {
|
pub fn axis_mut(&mut self, axis: Axis) -> &mut Len {
|
||||||
UiVec2 {
|
|
||||||
x: self.x.outside(®ion.x),
|
|
||||||
y: self.y.outside(®ion.y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
|
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => &mut self.x,
|
Axis::X => &mut self.x,
|
||||||
Axis::Y => &mut self.y,
|
Axis::Y => &mut self.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn axis(&self, axis: Axis) -> UiScalar {
|
pub fn axis(&self, axis: Axis) -> Len {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => self.x,
|
Axis::X => self.x,
|
||||||
Axis::Y => self.y,
|
Axis::Y => self.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_abs(&self, rel: Vec2) -> Vec2 {
|
/// Resolved against a box of `size`, which is where a fraction stops
|
||||||
Vec2 {
|
/// being one and becomes a place.
|
||||||
x: self.x.to_abs(rel.x),
|
pub fn to_px(&self, size: PxVec2) -> PxVec2 {
|
||||||
y: self.y.to_abs(rel.y),
|
PxVec2::new(self.x.to_px(size.x), self.y.to_px(size.y))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
|
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
|
||||||
|
|
||||||
pub const fn from_axis(axis: Axis, aligned: UiScalar, ortho: UiScalar) -> Self {
|
pub const fn from_axis(axis: Axis, aligned: Len, ortho: Len) -> Self {
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => Self {
|
Axis::X => Self {
|
||||||
x: aligned,
|
x: aligned,
|
||||||
@@ -99,34 +96,27 @@ impl UiVec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_abs(&self) -> Vec2 {
|
pub fn get_px(&self) -> Vec2 {
|
||||||
(self.x.abs, self.y.abs).into()
|
(self.x.px.to_f32(), self.y.px.to_f32()).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rel(&self) -> Vec2 {
|
pub fn get_rel(&self) -> Vec2 {
|
||||||
(self.x.rel, self.y.rel).into()
|
(self.x.rel.to_f32(), self.y.rel.to_f32()).into()
|
||||||
}
|
|
||||||
|
|
||||||
pub fn abs_mut(&mut self) -> Vec2View<'_> {
|
|
||||||
Vec2View {
|
|
||||||
x: &mut self.x.abs,
|
|
||||||
y: &mut self.y.abs,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for UiVec2 {
|
impl Display for UiVec2 {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "rel{};abs{}", self.get_rel(), self.get_abs())
|
write!(f, "rel{};px{}", self.get_rel(), self.get_px())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_op!(UiVec2 Add add; x y);
|
impl_op!(same UiVec2 Add add; x y);
|
||||||
impl_op!(UiVec2 Sub sub; x y);
|
impl_op!(same UiVec2 Sub sub; x y);
|
||||||
|
|
||||||
const impl From<Vec2> for UiVec2 {
|
const impl From<Vec2> for UiVec2 {
|
||||||
fn from(abs: Vec2) -> Self {
|
fn from(px: Vec2) -> Self {
|
||||||
Self::abs(abs)
|
Self::px(px)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,133 +124,149 @@ const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
|
|||||||
where
|
where
|
||||||
(T, U): const Destruct,
|
(T, U): const Destruct,
|
||||||
{
|
{
|
||||||
fn from(abs: (T, U)) -> Self {
|
fn from(px: (T, U)) -> Self {
|
||||||
Self::abs(abs)
|
Self::px(px)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A length along one axis: a fraction of the box it is measured in plus an
|
||||||
|
/// offset, `rel * box + px`. A position is the same number -- the length from
|
||||||
|
/// the start of the box to the point -- which is why a [`UiSpan`] is two of
|
||||||
|
/// these. Both parts are fixed point, so composing one through a chain of
|
||||||
|
/// boxes rounds only where it multiplies, and lands on the same number as any
|
||||||
|
/// other route to the same place.
|
||||||
|
///
|
||||||
|
/// It carries no claim on what a container has left over. That is
|
||||||
|
/// [`crate::LayoutLen`], which is this plus a weight, and which means nothing
|
||||||
|
/// to anyone but whoever divides the room.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, Default, bytemuck::Zeroable)]
|
||||||
pub struct UiScalar {
|
pub struct Len {
|
||||||
pub rel: f32,
|
pub rel: Rel,
|
||||||
pub abs: f32,
|
pub px: Px,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for UiScalar {}
|
impl_op!(same Len Add add; rel px);
|
||||||
impl Hash for UiScalar {
|
impl_op!(same Len Sub sub; rel px);
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
state.write_u32(self.rel.to_bits());
|
impl Len {
|
||||||
state.write_u32(self.abs.to_bits());
|
pub const ZERO: Self = Self {
|
||||||
|
rel: Rel::ZERO,
|
||||||
|
px: Px::ZERO,
|
||||||
|
};
|
||||||
|
pub const FULL: Self = Self {
|
||||||
|
rel: Rel::ONE,
|
||||||
|
px: Px::ZERO,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const fn new(rel: f32, px: f32) -> Self {
|
||||||
|
Self::from_parts(Rel::from_f32(rel), Px::from_f32(px))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl_op!(UiScalar Add add; rel abs);
|
/// From parts already on the grid, rather than numbers to be put on it.
|
||||||
impl_op!(UiScalar Sub sub; rel abs);
|
pub const fn from_parts(rel: Rel, px: Px) -> Self {
|
||||||
|
Self { rel, px }
|
||||||
impl UiScalar {
|
|
||||||
pub const ZERO: Self = Self { rel: 0.0, abs: 0.0 };
|
|
||||||
pub const FULL: Self = Self { rel: 1.0, abs: 0.0 };
|
|
||||||
|
|
||||||
pub const fn new(rel: f32, abs: f32) -> Self {
|
|
||||||
Self { rel, abs }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel(rel: f32) -> Self {
|
pub const fn rel(rel: f32) -> Self {
|
||||||
Self { rel, abs: 0.0 }
|
Self::from_parts(Rel::from_f32(rel), Px::ZERO)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn abs(abs: f32) -> Self {
|
pub const fn px(px: f32) -> Self {
|
||||||
Self { rel: 0.0, abs }
|
Self::from_parts(Rel::ZERO, Px::from_f32(px))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel_min() -> Self {
|
pub const fn rel_min() -> Self {
|
||||||
Self::new(0.0, 0.0)
|
Self::ZERO
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn rel_max() -> Self {
|
pub const fn rel_max() -> Self {
|
||||||
Self::new(1.0, 0.0)
|
Self::FULL
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn max(&self, other: Self) -> Self {
|
pub const fn max(&self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rel: self.rel.max(other.rel),
|
rel: self.rel.max(other.rel),
|
||||||
abs: self.abs.max(other.abs),
|
px: self.px.max(other.px),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn min(&self, other: Self) -> Self {
|
pub const fn min(&self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rel: self.rel.min(other.rel),
|
rel: self.rel.min(other.rel),
|
||||||
abs: self.abs.min(other.abs),
|
px: self.px.min(other.px),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn offset(mut self, amt: f32) -> Self {
|
/// Both parts by the same fraction, which is what a part of a length
|
||||||
self.abs += amt;
|
/// means when the length is part pixels and part a fraction of a box.
|
||||||
|
pub const fn scale(&self, by: Rel) -> Self {
|
||||||
|
Self {
|
||||||
|
rel: self.rel.mul(by),
|
||||||
|
px: self.px.mul(by),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn offset(mut self, amt: Px) -> Self {
|
||||||
|
self.px = self.px.add(amt);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn within(&self, span: &UiSpan) -> Self {
|
pub const fn within(&self, span: &UiSpan) -> Self {
|
||||||
let anchor = self.rel.lerp(span.start.rel, span.end.rel);
|
|
||||||
let offset = self.abs + self.rel.lerp(span.start.abs, span.end.abs);
|
|
||||||
Self {
|
Self {
|
||||||
rel: anchor,
|
rel: self.rel.lerp(span.start.rel, span.end.rel),
|
||||||
abs: offset,
|
px: self.px.add(self.rel.lerp(span.start.px, span.end.px)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn outside(&self, span: &UiSpan) -> Self {
|
pub const fn within_len(&self, len: Len) -> 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 {
|
|
||||||
self.within(&UiSpan {
|
self.within(&UiSpan {
|
||||||
start: UiScalar::ZERO,
|
start: Len::ZERO,
|
||||||
end: len,
|
end: len,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_len(&self, len: UiScalar) -> Self {
|
pub fn select_len(&self, len: Len) -> Self {
|
||||||
len.within_len(*self)
|
len.within_len(*self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn flip(&mut self) {
|
pub const fn flip(&mut self) {
|
||||||
self.rel = 1.0 - self.rel;
|
self.rel = Rel::ONE.sub(self.rel);
|
||||||
self.abs = -self.abs;
|
self.px = self.px.neg();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn to(&self, end: Self) -> UiSpan {
|
pub const fn to(&self, end: Self) -> UiSpan {
|
||||||
UiSpan { start: *self, end }
|
UiSpan { start: *self, end }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn to_abs(&self, rel: f32) -> f32 {
|
/// Resolved against a box of `len`, which is the only place a fraction
|
||||||
self.rel * rel + self.abs
|
/// becomes a number of pixels.
|
||||||
|
pub const fn to_px(&self, len: Px) -> Px {
|
||||||
|
self.px.add(len.mul(self.rel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct UiSpan {
|
pub struct UiSpan {
|
||||||
pub start: UiScalar,
|
pub start: Len,
|
||||||
pub end: UiScalar,
|
pub end: Len,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiSpan {
|
impl UiSpan {
|
||||||
pub const FULL: Self = Self {
|
pub const FULL: Self = Self {
|
||||||
start: UiScalar::ZERO,
|
start: Len::ZERO,
|
||||||
end: UiScalar::FULL,
|
end: Len::FULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const fn rel(rel: f32) -> Self {
|
pub const fn rel(rel: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
start: UiScalar::rel(rel),
|
start: Len::rel(rel),
|
||||||
end: UiScalar::rel(rel),
|
end: Len::rel(rel),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn new(start: UiScalar, end: UiScalar) -> Self {
|
pub const fn new(start: Len, end: Len) -> Self {
|
||||||
Self { start, end }
|
Self { start, end }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,14 +274,19 @@ impl UiSpan {
|
|||||||
self.start.flip();
|
self.start.flip();
|
||||||
self.end.flip();
|
self.end.flip();
|
||||||
std::mem::swap(&mut self.start.rel, &mut self.end.rel);
|
std::mem::swap(&mut self.start.rel, &mut self.end.rel);
|
||||||
std::mem::swap(&mut self.start.abs, &mut self.end.abs);
|
std::mem::swap(&mut self.start.px, &mut self.end.px);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn shift(&mut self, offset: UiScalar) {
|
pub const fn shift(&mut self, offset: Len) {
|
||||||
self.start += offset;
|
self.start += offset;
|
||||||
self.end += offset;
|
self.end += offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Composing a box through the one it sits in, and the hottest line in
|
||||||
|
/// layout. It used to skip the multiplies where a span was the whole of
|
||||||
|
/// its parent or the parent the whole of its own; both come out of the
|
||||||
|
/// multiply unchanged anyway, and the body those comparisons cost was
|
||||||
|
/// what kept the inliner from taking this at all.
|
||||||
pub const fn within(&self, parent: &Self) -> Self {
|
pub const fn within(&self, parent: &Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
start: self.start.within(parent),
|
start: self.start.within(parent),
|
||||||
@@ -283,15 +294,17 @@ impl UiSpan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn outside(&self, parent: &Self) -> Self {
|
pub const fn len(&self) -> Len {
|
||||||
Self {
|
self.end - self.start
|
||||||
start: self.start.outside(parent),
|
|
||||||
end: self.end.outside(parent),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn len(&self) -> UiScalar {
|
/// Both ends by the same amount, which is what moving a box without
|
||||||
self.end - self.start
|
/// changing its length does to every part of it.
|
||||||
|
pub const fn translated(self, by: Len) -> Self {
|
||||||
|
Self {
|
||||||
|
start: self.start + by,
|
||||||
|
end: self.end + by,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +316,17 @@ pub struct UiRegion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl UiRegion {
|
impl UiRegion {
|
||||||
|
/// Every part of the box by the same amount on each axis. Done to the
|
||||||
|
/// whole region rather than an end at a time, because that is what it is
|
||||||
|
/// -- and because four adds in a row are four adds, where four asked for
|
||||||
|
/// separately are four sequences.
|
||||||
|
pub const fn translated(self, x: Len, y: Len) -> Self {
|
||||||
|
Self {
|
||||||
|
x: self.x.translated(x),
|
||||||
|
y: self.y.translated(y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const FULL: Self = Self {
|
pub const FULL: Self = Self {
|
||||||
x: UiSpan::FULL,
|
x: UiSpan::FULL,
|
||||||
y: UiSpan::FULL,
|
y: UiSpan::FULL,
|
||||||
@@ -324,14 +348,7 @@ impl UiRegion {
|
|||||||
y: self.y.within(&parent.y),
|
y: self.y.within(&parent.y),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub const fn outside(&self, parent: &Self) -> Self {
|
pub const fn axis(&self, axis: Axis) -> &UiSpan {
|
||||||
Self {
|
|
||||||
x: self.x.outside(&parent.x),
|
|
||||||
y: self.y.outside(&parent.y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn axis(&mut self, axis: Axis) -> &UiSpan {
|
|
||||||
match axis {
|
match axis {
|
||||||
Axis::X => &self.x,
|
Axis::X => &self.x,
|
||||||
Axis::Y => &self.y,
|
Axis::Y => &self.y,
|
||||||
@@ -363,10 +380,10 @@ impl UiRegion {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_px(&self, size: Vec2) -> PixelRegion {
|
pub fn to_px(&self, size: PxVec2) -> PixelRegion {
|
||||||
PixelRegion {
|
PixelRegion {
|
||||||
top_left: self.top_left().get_rel() * size + self.top_left().get_abs(),
|
top_left: self.top_left().to_px(size),
|
||||||
bot_right: self.bot_right().get_rel() * size + self.bot_right().get_abs(),
|
bot_right: self.bot_right().to_px(size),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,21 +438,21 @@ impl Display for UiRegion {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct PixelRegion {
|
pub struct PixelRegion {
|
||||||
pub top_left: Vec2,
|
pub top_left: PxVec2,
|
||||||
pub bot_right: Vec2,
|
pub bot_right: PxVec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PixelRegion {
|
impl PixelRegion {
|
||||||
pub fn contains(&self, pos: Vec2) -> bool {
|
pub fn contains(&self, pos: PxVec2) -> bool {
|
||||||
pos.x >= self.top_left.x
|
pos.x >= self.top_left.x
|
||||||
&& pos.x <= self.bot_right.x
|
&& pos.x <= self.bot_right.x
|
||||||
&& pos.y >= self.top_left.y
|
&& pos.y >= self.top_left.y
|
||||||
&& pos.y <= self.bot_right.y
|
&& pos.y <= self.bot_right.y
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn size(&self) -> Vec2 {
|
pub fn size(&self) -> PxVec2 {
|
||||||
self.bot_right - self.top_left
|
self.bot_right - self.top_left
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,20 +462,3 @@ impl Display for PixelRegion {
|
|||||||
write!(f, "{} -> {}", self.top_left, self.bot_right)
|
write!(f, "{} -> {}", self.top_left, self.bot_right)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Vec2View<'a> {
|
|
||||||
pub x: &'a mut f32,
|
|
||||||
pub y: &'a mut f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Vec2View<'_> {
|
|
||||||
pub fn set(&mut self, other: Vec2) {
|
|
||||||
*self.x = other.x;
|
|
||||||
*self.y = other.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(&mut self, other: Vec2) {
|
|
||||||
*self.x += other.x;
|
|
||||||
*self.y += other.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+135
-454
@@ -1,488 +1,169 @@
|
|||||||
use crate::util::{Dirty, Resources, StrongRscId};
|
use std::marker::Destruct;
|
||||||
use std::{cell::RefCell, fmt, rc::Rc};
|
|
||||||
|
|
||||||
/// Encoded, straight-alpha sRGB at an input boundary.
|
/// stored in linear for sane manipulation
|
||||||
///
|
|
||||||
/// Palette literals, decoded images and colour glyph bitmaps use this
|
|
||||||
/// convention. A solid paint is converted to linear light when it enters the
|
|
||||||
/// paint table; the renderer never performs colour arithmetic on these bytes.
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Clone, Copy, Hash, PartialEq, Eq, bytemuck::Zeroable, Debug)]
|
||||||
pub struct Srgba8 {
|
pub struct Color<T> {
|
||||||
pub r: u8,
|
pub r: T,
|
||||||
pub g: u8,
|
pub g: T,
|
||||||
pub b: u8,
|
pub b: T,
|
||||||
pub a: u8,
|
pub a: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Srgba8 {
|
impl<T: ColorNum> Default for Color<T> {
|
||||||
pub const BLACK: Self = Self::rgb(0, 0, 0);
|
|
||||||
pub const WHITE: Self = Self::rgb(255, 255, 255);
|
|
||||||
pub const GRAY: Self = Self::rgb(127, 127, 127);
|
|
||||||
pub const RED: Self = Self::rgb(255, 0, 0);
|
|
||||||
pub const ORANGE: Self = Self::rgb(255, 127, 0);
|
|
||||||
pub const YELLOW: Self = Self::rgb(255, 255, 0);
|
|
||||||
pub const LIME: Self = Self::rgb(127, 255, 0);
|
|
||||||
pub const GREEN: Self = Self::rgb(0, 255, 0);
|
|
||||||
pub const TURQUOISE: Self = Self::rgb(0, 255, 127);
|
|
||||||
pub const CYAN: Self = Self::rgb(0, 255, 255);
|
|
||||||
pub const SKY: Self = Self::rgb(0, 127, 255);
|
|
||||||
pub const BLUE: Self = Self::rgb(0, 0, 255);
|
|
||||||
pub const PURPLE: Self = Self::rgb(127, 0, 255);
|
|
||||||
pub const MAGENTA: Self = Self::rgb(255, 0, 255);
|
|
||||||
pub const NONE: Self = Self::new(0, 0, 0, 0);
|
|
||||||
|
|
||||||
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
|
||||||
Self { r, g, b, a }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
|
||||||
Self::new(r, g, b, 255)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_linear(self) -> LinearRgba {
|
|
||||||
LinearRgba::new(
|
|
||||||
srgb_to_linear(self.r as f32 / 255.0),
|
|
||||||
srgb_to_linear(self.g as f32 / 255.0),
|
|
||||||
srgb_to_linear(self.b as f32 / 255.0),
|
|
||||||
self.a as f32 / 255.0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Straight-alpha RGBA in linear-light sRGB primaries.
|
|
||||||
///
|
|
||||||
/// This is Iris's working representation: manipulate and interpolate colours
|
|
||||||
/// here, then put the result in [`Paints`]. The GPU paint buffer stores this
|
|
||||||
/// exact layout as `vec4<f32>`.
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
|
||||||
pub struct LinearRgba {
|
|
||||||
pub r: f32,
|
|
||||||
pub g: f32,
|
|
||||||
pub b: f32,
|
|
||||||
pub a: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinearRgba {
|
|
||||||
pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
|
|
||||||
pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
|
|
||||||
pub const NONE: Self = Self::new(0.0, 0.0, 0.0, 0.0);
|
|
||||||
|
|
||||||
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
|
|
||||||
Self { r, g, b, a }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
|
|
||||||
Self::new(r, g, b, 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mul_rgb(self, amount: f32) -> Self {
|
|
||||||
Self::new(self.r * amount, self.g * amount, self.b * amount, self.a)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn darker(self, amount: f32) -> Self {
|
|
||||||
self.mul_rgb(1.0 - amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn brighter(self, amount: f32) -> Self {
|
|
||||||
Self::new(
|
|
||||||
self.r + (1.0 - self.r) * amount,
|
|
||||||
self.g + (1.0 - self.g) * amount,
|
|
||||||
self.b + (1.0 - self.b) * amount,
|
|
||||||
self.a,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_wgpu(self) -> wgpu::Color {
|
|
||||||
wgpu::Color {
|
|
||||||
r: self.r as f64,
|
|
||||||
g: self.g as f64,
|
|
||||||
b: self.b as f64,
|
|
||||||
a: self.a as f64,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn srgb_to_linear(value: f32) -> f32 {
|
|
||||||
if value <= 0.04045 {
|
|
||||||
value / 12.92
|
|
||||||
} else {
|
|
||||||
((value + 0.055) / 1.055).powf(2.4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A description that can be registered in Iris's paint table.
|
|
||||||
///
|
|
||||||
/// Only solid paints exist today. Keeping registration behind this trait and
|
|
||||||
/// making primitives carry [`PaintId`] leaves one place to add gradient or
|
|
||||||
/// texture paint records later.
|
|
||||||
pub trait Paint: private::Sealed + 'static {
|
|
||||||
#[doc(hidden)]
|
|
||||||
fn add_to(&self, paints: &mut Paints) -> PaintId;
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
fn replace(&self, paints: &mut Paints, slot: u32);
|
|
||||||
|
|
||||||
/// Erases this definition so a widget can register it lazily on its
|
|
||||||
/// first draw. [`PaintId`] overrides this to stay a direct handle.
|
|
||||||
#[doc(hidden)]
|
|
||||||
fn into_value(self) -> PaintValue
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
PaintValue::pending(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Paint for Srgba8 {
|
|
||||||
fn add_to(&self, paints: &mut Paints) -> PaintId {
|
|
||||||
paints.add_linear(self.to_linear())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace(&self, paints: &mut Paints, slot: u32) {
|
|
||||||
paints.replace_linear(slot, self.to_linear());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Paint for LinearRgba {
|
|
||||||
fn add_to(&self, paints: &mut Paints) -> PaintId {
|
|
||||||
paints.add_linear(*self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace(&self, paints: &mut Paints, slot: u32) {
|
|
||||||
paints.replace_linear(slot, *self);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod private {
|
|
||||||
pub trait Sealed {}
|
|
||||||
impl Sealed for super::Srgba8 {}
|
|
||||||
impl Sealed for super::LinearRgba {}
|
|
||||||
impl Sealed for super::PaintId {}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PaintRsc;
|
|
||||||
|
|
||||||
/// A stable reference to one entry in a UI's paint table.
|
|
||||||
///
|
|
||||||
/// Built-in IDs name the same reserved entries in every [`Paints`]. IDs
|
|
||||||
/// returned by [`Paints::add`] retain their slot until the last clone held by a
|
|
||||||
/// widget, shaped text or retained draw is dropped.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct PaintId {
|
|
||||||
slot: u32,
|
|
||||||
strong: Option<StrongRscId<PaintRsc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PaintId {
|
|
||||||
pub const BLACK: Self = Self::builtin(0);
|
|
||||||
pub const WHITE: Self = Self::builtin(1);
|
|
||||||
pub const GRAY: Self = Self::builtin(2);
|
|
||||||
pub const RED: Self = Self::builtin(3);
|
|
||||||
pub const ORANGE: Self = Self::builtin(4);
|
|
||||||
pub const YELLOW: Self = Self::builtin(5);
|
|
||||||
pub const LIME: Self = Self::builtin(6);
|
|
||||||
pub const GREEN: Self = Self::builtin(7);
|
|
||||||
pub const TURQUOISE: Self = Self::builtin(8);
|
|
||||||
pub const CYAN: Self = Self::builtin(9);
|
|
||||||
pub const SKY: Self = Self::builtin(10);
|
|
||||||
pub const BLUE: Self = Self::builtin(11);
|
|
||||||
pub const PURPLE: Self = Self::builtin(12);
|
|
||||||
pub const MAGENTA: Self = Self::builtin(13);
|
|
||||||
pub const NONE: Self = Self::builtin(14);
|
|
||||||
|
|
||||||
const fn builtin(slot: u32) -> Self {
|
|
||||||
Self { slot, strong: None }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn slot(&self) -> u32 {
|
|
||||||
self.slot
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_managed(&self) -> bool {
|
|
||||||
self.strong.is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PaintId {
|
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::BLACK
|
Self::BLACK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for PaintId {
|
impl<T: ColorNum> Color<T> {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
pub const BLACK: Self = Self::rgb(T::MIN, T::MIN, T::MIN);
|
||||||
self.slot == other.slot
|
pub const WHITE: Self = Self::rgb(T::MAX, T::MAX, T::MAX);
|
||||||
|
pub const GRAY: Self = Self::rgb(T::MID, T::MID, T::MID);
|
||||||
|
|
||||||
|
pub const RED: Self = Self::rgb(T::MAX, T::MIN, T::MIN);
|
||||||
|
pub const ORANGE: Self = Self::rgb(T::MAX, T::MID, T::MIN);
|
||||||
|
pub const YELLOW: Self = Self::rgb(T::MAX, T::MAX, T::MIN);
|
||||||
|
pub const LIME: Self = Self::rgb(T::MID, T::MAX, T::MIN);
|
||||||
|
pub const GREEN: Self = Self::rgb(T::MIN, T::MAX, T::MIN);
|
||||||
|
pub const TURQUOISE: Self = Self::rgb(T::MIN, T::MAX, T::MID);
|
||||||
|
pub const CYAN: Self = Self::rgb(T::MIN, T::MAX, T::MAX);
|
||||||
|
pub const SKY: Self = Self::rgb(T::MIN, T::MID, T::MAX);
|
||||||
|
pub const BLUE: Self = Self::rgb(T::MIN, T::MIN, T::MAX);
|
||||||
|
pub const PURPLE: Self = Self::rgb(T::MID, T::MIN, T::MAX);
|
||||||
|
pub const MAGENTA: Self = Self::rgb(T::MAX, T::MIN, T::MAX);
|
||||||
|
|
||||||
|
pub const NONE: Self = Self::new(T::MIN, T::MIN, T::MIN, T::MIN);
|
||||||
|
|
||||||
|
pub const fn new(r: T, g: T, b: T, a: T) -> Self {
|
||||||
|
Self { r, g, b, a }
|
||||||
|
}
|
||||||
|
pub const fn rgb(r: T, g: T, b: T) -> Self {
|
||||||
|
Self { r, g, b, a: T::MAX }
|
||||||
|
}
|
||||||
|
pub fn alpha(mut self, a: T) -> Self {
|
||||||
|
self.a = a;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_arr(self) -> [T; 4] {
|
||||||
|
[self.r, self.g, self.b, self.a]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for PaintId {}
|
pub const trait F32Conversion {
|
||||||
|
fn to(self) -> f32;
|
||||||
impl Paint for PaintId {
|
fn from(x: f32) -> Self;
|
||||||
fn add_to(&self, _paints: &mut Paints) -> PaintId {
|
|
||||||
self.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace(&self, paints: &mut Paints, slot: u32) {
|
|
||||||
let value = paints.entries[self.slot as usize];
|
|
||||||
paints.replace_linear(slot, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_value(self) -> PaintValue {
|
|
||||||
PaintValue(PaintValueInner::Id(self))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PendingPaint {
|
pub trait ColorNum {
|
||||||
definition: Box<dyn Paint>,
|
const MIN: Self;
|
||||||
resolved: RefCell<Option<PaintId>>,
|
const MID: Self;
|
||||||
|
const MAX: Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
macro_rules! map_rgb {
|
||||||
enum PaintValueInner {
|
($x:ident,$self:ident, $e:tt) => {
|
||||||
Id(PaintId),
|
#[allow(unused_braces)]
|
||||||
Pending(Rc<PendingPaint>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A widget property containing either an existing paint-table ID or a paint
|
|
||||||
/// definition that will receive an ID the first time it is drawn.
|
|
||||||
///
|
|
||||||
/// Pending definitions are shared across clones and registered only once.
|
|
||||||
/// Each property replaces its own pending variant with the resulting direct
|
|
||||||
/// ID after that first resolution, so later draws take the direct path.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct PaintValue(PaintValueInner);
|
|
||||||
|
|
||||||
impl PaintValue {
|
|
||||||
fn pending(paint: impl Paint) -> Self {
|
|
||||||
Self(PaintValueInner::Pending(Rc::new(PendingPaint {
|
|
||||||
definition: Box::new(paint),
|
|
||||||
resolved: RefCell::new(None),
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resolve(&mut self, paints: &mut Paints) -> &PaintId {
|
|
||||||
if let PaintValueInner::Pending(pending) = &self.0 {
|
|
||||||
let resolved = pending.resolved.borrow().clone();
|
|
||||||
let id = match resolved {
|
|
||||||
Some(id) => id,
|
|
||||||
None => {
|
|
||||||
let id = pending.definition.add_to(paints);
|
|
||||||
*pending.resolved.borrow_mut() = Some(id.clone());
|
|
||||||
id
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.0 = PaintValueInner::Id(id);
|
|
||||||
}
|
|
||||||
let PaintValueInner::Id(id) = &self.0 else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is(&self, id: &PaintId) -> bool {
|
|
||||||
match &self.0 {
|
|
||||||
PaintValueInner::Id(current) => current == id,
|
|
||||||
PaintValueInner::Pending(pending) => pending.resolved.borrow().as_ref() == Some(id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for PaintValue {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match &self.0 {
|
|
||||||
PaintValueInner::Id(id) => f.debug_tuple("PaintValue").field(id).finish(),
|
|
||||||
PaintValueInner::Pending(_) => f.write_str("PaintValue(Pending)"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const BUILTIN_PAINTS: [Srgba8; 15] = [
|
|
||||||
Srgba8::BLACK,
|
|
||||||
Srgba8::WHITE,
|
|
||||||
Srgba8::GRAY,
|
|
||||||
Srgba8::RED,
|
|
||||||
Srgba8::ORANGE,
|
|
||||||
Srgba8::YELLOW,
|
|
||||||
Srgba8::LIME,
|
|
||||||
Srgba8::GREEN,
|
|
||||||
Srgba8::TURQUOISE,
|
|
||||||
Srgba8::CYAN,
|
|
||||||
Srgba8::SKY,
|
|
||||||
Srgba8::BLUE,
|
|
||||||
Srgba8::PURPLE,
|
|
||||||
Srgba8::MAGENTA,
|
|
||||||
Srgba8::NONE,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// CPU-side paint table and the dirty set for its GPU mirror.
|
|
||||||
pub struct Paints {
|
|
||||||
resources: Resources<PaintRsc>,
|
|
||||||
entries: Vec<LinearRgba>,
|
|
||||||
dirty: Dirty,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Paints {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let mut resources = Resources::new();
|
|
||||||
for (slot, _) in BUILTIN_PAINTS.iter().enumerate() {
|
|
||||||
let id = resources.add_static(PaintRsc);
|
|
||||||
assert_eq!(id.slot(), slot as u32);
|
|
||||||
}
|
|
||||||
Self {
|
Self {
|
||||||
resources,
|
r: {
|
||||||
entries: BUILTIN_PAINTS.map(Srgba8::to_linear).to_vec(),
|
let $x = $self.r;
|
||||||
dirty: Dirty::new_all(),
|
$e
|
||||||
|
},
|
||||||
|
g: {
|
||||||
|
let $x = $self.g;
|
||||||
|
$e
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
let $x = $self.b;
|
||||||
|
$e
|
||||||
|
},
|
||||||
|
a: $self.a,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ColorNum + const F32Conversion> Color<T>
|
||||||
|
where
|
||||||
|
Self: const Destruct,
|
||||||
|
{
|
||||||
|
pub const fn mul_rgb(self, amt: impl const F32Conversion) -> Self {
|
||||||
|
let amt = amt.to();
|
||||||
|
map_rgb!(x, self, { T::from(x.to() * amt) })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn add_rgb(self, amt: impl const F32Conversion) -> Self {
|
||||||
|
let amt = amt.to();
|
||||||
|
map_rgb!(x, self, { T::from(x.to() + amt) })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn darker(self, amt: f32) -> Self {
|
||||||
|
self.mul_rgb(1.0 - amt)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn brighter(self, amt: f32) -> Self {
|
||||||
|
map_rgb!(x, self, {
|
||||||
|
let x = x.to();
|
||||||
|
T::from(x + (1.0 - x) * amt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_rgb(self, f: impl Fn(T) -> T) -> Self {
|
||||||
|
Self {
|
||||||
|
r: f(self.r),
|
||||||
|
g: f(self.g),
|
||||||
|
b: f(self.b),
|
||||||
|
a: self.a,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(&mut self, paint: impl Paint) -> PaintId {
|
pub fn srgb(r: T, g: T, b: T) -> Self {
|
||||||
paint.add_to(self)
|
Self {
|
||||||
|
r: s_to_l(r),
|
||||||
|
g: s_to_l(g),
|
||||||
|
b: s_to_l(b),
|
||||||
|
a: T::MAX,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn add_linear(&mut self, value: LinearRgba) -> PaintId {
|
fn s_to_l<T: F32Conversion>(x: T) -> T {
|
||||||
self.free_released();
|
let x = x.to();
|
||||||
let old_capacity = self.resources.capacity();
|
T::from(if x <= 0.0405 {
|
||||||
let strong = self.resources.add(PaintRsc);
|
x / 12.92
|
||||||
let slot = strong.slot();
|
|
||||||
if (slot as usize) < old_capacity {
|
|
||||||
self.entries[slot as usize] = value;
|
|
||||||
self.dirty.mark(slot as usize);
|
|
||||||
} else {
|
} else {
|
||||||
self.entries.push(value);
|
((x + 0.055) / 1.055).powf(2.4)
|
||||||
self.dirty.mark(slot as usize);
|
})
|
||||||
}
|
}
|
||||||
PaintId {
|
|
||||||
slot,
|
|
||||||
strong: Some(strong),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces one managed paint in place. Every primitive keeps the same
|
impl ColorNum for u8 {
|
||||||
/// index, so a theme change dirties this table and no primitive buffer.
|
const MIN: Self = u8::MIN;
|
||||||
pub fn set(&mut self, id: &PaintId, paint: impl Paint) {
|
const MID: Self = u8::MAX / 2;
|
||||||
assert!(
|
const MAX: Self = u8::MAX;
|
||||||
id.is_managed(),
|
}
|
||||||
"a reserved built-in paint cannot be replaced; allocate a theme slot with Paints::add"
|
|
||||||
);
|
|
||||||
paint.replace(self, id.slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_linear(&mut self, slot: u32, value: LinearRgba) {
|
impl ColorNum for f32 {
|
||||||
self.entries[slot as usize] = value;
|
const MIN: Self = 0.0;
|
||||||
self.dirty.mark(slot as usize);
|
const MID: Self = 0.5;
|
||||||
}
|
const MAX: Self = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get(&self, id: &PaintId) -> LinearRgba {
|
unsafe impl bytemuck::Pod for Color<u8> {}
|
||||||
self.entries[id.slot as usize]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn free_released(&mut self) {
|
const impl F32Conversion for f32 {
|
||||||
let entries = &mut self.entries;
|
fn to(self) -> f32 {
|
||||||
let dirty = &mut self.dirty;
|
self
|
||||||
self.resources.apply(|id, _| {
|
|
||||||
let slot = id.slot();
|
|
||||||
entries[slot as usize] = LinearRgba::NONE;
|
|
||||||
dirty.mark(slot as usize);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
fn from(x: f32) -> Self {
|
||||||
/// A new GPU device has no copy of this table even when the CPU-side UI
|
x
|
||||||
/// and its paint IDs survived an Android surface recreation.
|
|
||||||
pub fn reupload(&mut self) {
|
|
||||||
self.dirty.mark_all();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn for_upload(&mut self) -> (&[LinearRgba], &mut Dirty) {
|
|
||||||
(&self.entries, &mut self.dirty)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Paints {
|
const impl F32Conversion for u8 {
|
||||||
fn default() -> Self {
|
fn to(self) -> f32 {
|
||||||
Self::new()
|
self as f32 / 255.0
|
||||||
}
|
}
|
||||||
}
|
fn from(x: f32) -> Self {
|
||||||
|
(x * 255.0).clamp(0.0, 255.0) as Self
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn srgb_bytes_become_linear_without_transforming_alpha() {
|
|
||||||
let got = Srgba8::new(17, 127, 255, 64).to_linear();
|
|
||||||
assert!((got.r - 0.005605).abs() < 0.000001);
|
|
||||||
assert!((got.g - 0.212231).abs() < 0.000001);
|
|
||||||
assert_eq!(got.b, 1.0);
|
|
||||||
assert!((got.a - 64.0 / 255.0).abs() < f32::EPSILON);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn changing_a_paint_keeps_its_id_and_dirties_only_its_slot() {
|
|
||||||
let mut paints = Paints::new();
|
|
||||||
let id = paints.add(Srgba8::rgb(17, 17, 27));
|
|
||||||
let slot = id.slot();
|
|
||||||
let (_, dirty) = paints.for_upload();
|
|
||||||
dirty.clear();
|
|
||||||
|
|
||||||
paints.set(&id, Srgba8::rgb(205, 214, 244));
|
|
||||||
let (_, dirty) = paints.for_upload();
|
|
||||||
assert!(dirty.contains(slot as usize));
|
|
||||||
assert_eq!(id.slot(), slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_released_paint_slot_is_reused_only_after_the_last_clone() {
|
|
||||||
let mut paints = Paints::new();
|
|
||||||
let id = paints.add(Srgba8::RED);
|
|
||||||
let slot = id.slot();
|
|
||||||
let held = id.clone();
|
|
||||||
drop(id);
|
|
||||||
paints.free_released();
|
|
||||||
let other = paints.add(Srgba8::GREEN);
|
|
||||||
assert_ne!(other.slot(), slot);
|
|
||||||
|
|
||||||
drop(held);
|
|
||||||
paints.free_released();
|
|
||||||
let reused = paints.add(Srgba8::BLUE);
|
|
||||||
assert_eq!(reused.slot(), slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cloned_pending_paints_register_once_and_then_become_direct_ids() {
|
|
||||||
let mut paints = Paints::new();
|
|
||||||
let mut first = Srgba8::rgb(17, 17, 27).into_value();
|
|
||||||
let mut second = first.clone();
|
|
||||||
let before = paints.entries.len();
|
|
||||||
|
|
||||||
let first_id = first.resolve(&mut paints).clone();
|
|
||||||
let second_id = second.resolve(&mut paints).clone();
|
|
||||||
|
|
||||||
assert_eq!(first_id, second_id);
|
|
||||||
assert_eq!(paints.entries.len(), before + 1);
|
|
||||||
assert!(first.is(&first_id));
|
|
||||||
assert!(second.is(&first_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn independently_constructed_inline_solids_get_independent_slots() {
|
|
||||||
let mut paints = Paints::new();
|
|
||||||
let mut first = Srgba8::rgb(23, 42, 71).into_value();
|
|
||||||
let mut second = Srgba8::rgb(23, 42, 71).into_value();
|
|
||||||
let before = paints.entries.len();
|
|
||||||
|
|
||||||
let first_id = first.resolve(&mut paints).clone();
|
|
||||||
let second_id = second.resolve(&mut paints).clone();
|
|
||||||
assert_ne!(first_id, second_id);
|
|
||||||
assert_eq!(paints.entries.len(), before + 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn explicit_theme_paints_with_equal_values_remain_independent() {
|
|
||||||
let mut paints = Paints::new();
|
|
||||||
let first = paints.add(Srgba8::rgb(23, 42, 71));
|
|
||||||
let second = paints.add(Srgba8::rgb(23, 42, 71));
|
|
||||||
assert_ne!(first.slot(), second.slot());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
use std::ops::{Index, IndexMut};
|
use std::ops::{Index, IndexMut};
|
||||||
|
|
||||||
use crate::{render::LayerOrder, util::to_mut};
|
use crate::{
|
||||||
|
render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst},
|
||||||
|
util::to_mut,
|
||||||
|
};
|
||||||
|
|
||||||
pub type LayerId = usize;
|
pub type LayerId = usize;
|
||||||
|
|
||||||
@@ -14,13 +17,19 @@ struct LayerNode<T> {
|
|||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
enum Ptr {
|
enum Ptr {
|
||||||
|
/// continue on same level
|
||||||
Next(usize),
|
Next(usize),
|
||||||
|
/// go back to parent
|
||||||
Parent(usize),
|
Parent(usize),
|
||||||
|
/// end
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TODO: currently this does not ever free layers
|
||||||
|
/// is that realistically desired?
|
||||||
pub struct Layers<T> {
|
pub struct Layers<T> {
|
||||||
vec: Vec<LayerNode<T>>,
|
vec: Vec<LayerNode<T>>,
|
||||||
|
/// index of last layer at top level (start at first = 0)
|
||||||
last: usize,
|
last: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +39,7 @@ struct Child {
|
|||||||
tail: usize,
|
tail: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PrimitiveLayers = Layers<LayerOrder>;
|
pub type DrawLayers = Layers<LayerDraws>;
|
||||||
|
|
||||||
impl<T: Default> Layers<T> {
|
impl<T: Default> Layers<T> {
|
||||||
pub fn new() -> Layers<T> {
|
pub fn new() -> Layers<T> {
|
||||||
@@ -110,6 +119,24 @@ impl<T: Default> Layers<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DrawLayers {
|
||||||
|
/// Inlined on purpose: it is one call per glyph, the innermost thing a
|
||||||
|
/// frame does, and whether the inliner takes it turns out to depend on
|
||||||
|
/// unrelated code elsewhere in the crate -- 12% of a resize frame.
|
||||||
|
#[inline]
|
||||||
|
pub fn write<P: Primitive>(
|
||||||
|
&mut self,
|
||||||
|
layer: LayerId,
|
||||||
|
info: PrimitiveInst<P>,
|
||||||
|
) -> PrimitiveHandle {
|
||||||
|
self[layer].write(layer, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
|
||||||
|
self[h.layer].free(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Default> Default for Layers<T> {
|
impl<T: Default> Default for Layers<T> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
|
|||||||
+316
-1011
File diff suppressed because it is too large.
Load diff
+61
-277
@@ -1,76 +1,36 @@
|
|||||||
use crate::util::{Resources, RscHandle, StrongRscId, Vec2, WeakRscId};
|
use crate::util::{RefCounter, Vec2};
|
||||||
use image::{DynamicImage, GenericImageView};
|
use image::{DynamicImage, GenericImageView};
|
||||||
use std::{cell::RefCell, collections::HashMap, ops::Index, rc::Rc};
|
use std::{
|
||||||
|
ops::Index,
|
||||||
/// Which of the two things a texture slot holds. See TEXTURES.md's
|
sync::mpsc::{Receiver, Sender, channel},
|
||||||
/// "Recommended shape" for why these are drawn so differently: a page is a
|
};
|
||||||
/// layer of one shared array texture and never gets its own bind group; a
|
|
||||||
/// standalone image is the opposite, one texture and one bind group, never a
|
|
||||||
/// layer.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum TextureKind {
|
|
||||||
Image,
|
|
||||||
/// The array-texture layer this page was assigned. Chosen synchronously
|
|
||||||
/// by `Textures::add_page` rather than by the renderer, because glyph
|
|
||||||
/// insertion needs it in the same call, before any GPU sync happens.
|
|
||||||
Page {
|
|
||||||
layer: u32,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct SharedTextureKey {
|
|
||||||
pub owner: &'static str,
|
|
||||||
pub id: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TextureRsc {
|
|
||||||
kind: TextureKind,
|
|
||||||
size: Vec2,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TextureHandle {
|
pub struct TextureHandle {
|
||||||
rsc: RscHandle<TextureRsc>,
|
slot: u32,
|
||||||
|
size: Vec2,
|
||||||
|
counter: RefCounter,
|
||||||
|
send: Sender<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for TextureHandle {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.rsc.id() == other.rsc.id()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for TextureHandle {}
|
|
||||||
|
|
||||||
/// a texture manager for a ui
|
/// a texture manager for a ui
|
||||||
/// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped
|
/// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped
|
||||||
pub struct Textures {
|
pub struct Textures {
|
||||||
resources: Rc<RefCell<Resources<TextureRsc>>>,
|
free: Vec<u32>,
|
||||||
images: Vec<Option<DynamicImage>>,
|
images: Vec<Option<DynamicImage>>,
|
||||||
kinds: Vec<TextureKind>,
|
|
||||||
/// Textures built from a description rather than from a file, one per
|
|
||||||
/// distinct description: see [`Textures::shared`]. The map holds a
|
|
||||||
/// reference of its own, so a shared texture outlives every widget
|
|
||||||
/// drawing it and its slot is never recycled underneath one.
|
|
||||||
shared: HashMap<SharedTextureKey, TextureHandle>,
|
|
||||||
/// Next layer to hand out to an atlas page. Page layers and resource slots
|
|
||||||
/// are separate identities: released page layers are reused without moving
|
|
||||||
/// any still-live page.
|
|
||||||
next_page_layer: u32,
|
|
||||||
free_page_layers: Vec<u32>,
|
|
||||||
updates: Vec<Update>,
|
updates: Vec<Update>,
|
||||||
|
send: Sender<u32>,
|
||||||
|
recv: Receiver<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum TextureUpdate<'a> {
|
pub enum TextureUpdate<'a> {
|
||||||
Push(TextureKind, &'a DynamicImage),
|
Push(&'a DynamicImage),
|
||||||
Set(TextureKind, u32, &'a DynamicImage),
|
Set(u32, &'a DynamicImage),
|
||||||
/// Overwrite a rectangle of an existing texture, rather than replacing it.
|
|
||||||
/// The glyph atlas grows a glyph at a time, and re-uploading a whole atlas
|
|
||||||
/// per glyph is megabytes of copy for a few hundred bytes of change.
|
|
||||||
/// Only ever issued against a page -- a standalone image is never patched.
|
|
||||||
Patch(u32, PatchRect, &'a DynamicImage),
|
Patch(u32, PatchRect, &'a DynamicImage),
|
||||||
Free(u32),
|
Free(u32),
|
||||||
PushFree(TextureKind),
|
/// Added and freed before the renderer drained either update. It still has
|
||||||
|
/// to push a slot to stay lined up with `images`; `Free` then empties it.
|
||||||
|
PushFree,
|
||||||
SetFree,
|
SetFree,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,147 +43,81 @@ pub struct PatchRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Update {
|
enum Update {
|
||||||
Push(TextureKind, u32),
|
Push(u32),
|
||||||
Set(TextureKind, u32),
|
Set(u32),
|
||||||
Patch(u32, PatchRect),
|
Patch(u32, PatchRect),
|
||||||
Free(u32),
|
Free(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Textures {
|
impl Textures {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let (send, recv) = channel();
|
||||||
Self {
|
Self {
|
||||||
resources: Rc::new(RefCell::new(Resources::new())),
|
free: Vec::new(),
|
||||||
images: Vec::new(),
|
images: Vec::new(),
|
||||||
kinds: Vec::new(),
|
|
||||||
shared: HashMap::new(),
|
|
||||||
next_page_layer: 0,
|
|
||||||
free_page_layers: Vec::new(),
|
|
||||||
updates: Vec::new(),
|
updates: Vec::new(),
|
||||||
|
send,
|
||||||
|
recv,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
||||||
let image = image.into();
|
let image = image.into();
|
||||||
let size = image.dimensions().into();
|
let size = image.dimensions().into();
|
||||||
let kind = TextureKind::Image;
|
|
||||||
self.push(kind, size, image)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_page(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
|
||||||
self.free();
|
|
||||||
let image = image.into();
|
|
||||||
let size = image.dimensions().into();
|
|
||||||
let layer = self.free_page_layers.pop().unwrap_or_else(|| {
|
|
||||||
let layer = self.next_page_layer;
|
|
||||||
self.next_page_layer += 1;
|
|
||||||
layer
|
|
||||||
});
|
|
||||||
let kind = TextureKind::Page { layer };
|
|
||||||
self.push(kind, size, image)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle(&self, id: StrongRscId<TextureRsc>) -> TextureHandle {
|
|
||||||
TextureHandle {
|
TextureHandle {
|
||||||
rsc: RscHandle::new(id, self.resources.clone()),
|
slot: self.push(image),
|
||||||
|
size,
|
||||||
|
counter: RefCounter::new(),
|
||||||
|
send: self.send.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upgrade(&mut self, id: WeakRscId<TextureRsc>) -> Option<TextureHandle> {
|
fn push(&mut self, image: DynamicImage) -> u32 {
|
||||||
self.free();
|
if let Some(i) = self.free.pop() {
|
||||||
let id = self.resources.borrow_mut().upgrade(id)?;
|
|
||||||
Some(self.handle(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push(&mut self, kind: TextureKind, size: Vec2, image: DynamicImage) -> TextureHandle {
|
|
||||||
self.free();
|
|
||||||
let old_capacity = self.resources.borrow().capacity();
|
|
||||||
let id = self.resources.borrow_mut().add(TextureRsc { kind, size });
|
|
||||||
let i = id.slot();
|
|
||||||
if (i as usize) < old_capacity {
|
|
||||||
self.images[i as usize] = Some(image);
|
self.images[i as usize] = Some(image);
|
||||||
self.kinds[i as usize] = kind;
|
self.updates.push(Update::Set(i));
|
||||||
self.updates.push(Update::Set(kind, i));
|
i
|
||||||
} else {
|
} else {
|
||||||
|
let i = self.images.len() as u32;
|
||||||
self.images.push(Some(image));
|
self.images.push(Some(image));
|
||||||
self.kinds.push(kind);
|
self.updates.push(Update::Push(i));
|
||||||
self.updates.push(Update::Push(kind, i));
|
i
|
||||||
}
|
}
|
||||||
TextureHandle {
|
|
||||||
rsc: RscHandle::new(id, self.resources.clone()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The map keeps its own reference for the life of the `Textures`, so
|
|
||||||
/// a shared slot is never freed and never reused for something else --
|
|
||||||
/// which is what makes a handle held by a long-lived widget safe.
|
|
||||||
pub fn shared(
|
|
||||||
&mut self,
|
|
||||||
key: SharedTextureKey,
|
|
||||||
make: impl FnOnce() -> DynamicImage,
|
|
||||||
) -> TextureHandle {
|
|
||||||
if let Some(handle) = self.shared.get(&key) {
|
|
||||||
return handle.clone();
|
|
||||||
}
|
|
||||||
let handle = self.add(make());
|
|
||||||
self.shared.insert(key, handle.clone());
|
|
||||||
handle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
|
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
|
||||||
self.images[handle.rsc.id().slot() as usize]
|
self.images[handle.slot as usize]
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.expect("texture was freed while still held")
|
.expect("texture was freed while still held")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Queue an upload of just `rect`, after writing it with `image_mut`.
|
||||||
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
|
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
|
||||||
self.updates
|
self.updates.push(Update::Patch(handle.slot, rect));
|
||||||
.push(Update::Patch(handle.rsc.id().slot(), rect));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A new device starts with no textures, and the renderer-side mirror
|
/// How many textures are live, which is what a ui can ask; the renderer's
|
||||||
/// of these slots (`render::texture::GpuTextures`) starts empty with
|
/// copies follow from the updates it drains.
|
||||||
/// it. What it must not do is start empty while the handles widgets
|
pub fn count(&self) -> usize {
|
||||||
/// are still holding name slots by *index*: `Textures::reset` used to
|
self.images.iter().flatten().count()
|
||||||
/// throw this bookkeeping away, which left every live `TextureHandle`
|
|
||||||
/// -- one per `widget::mark`, hundreds on a transcript screen --
|
|
||||||
/// pointing at a slot nothing recognised, and the first frame after an
|
|
||||||
/// Android surface rebuild panicked in `image_bind_group` ("texture
|
|
||||||
/// slot 89 is not a live standalone image: None"). Re-uploading
|
|
||||||
/// instead keeps every index meaning what it meant, because this side
|
|
||||||
/// still holds the images: the slot list is rebuilt identically,
|
|
||||||
/// including the empty slots, which go across as `PushFree` so the
|
|
||||||
/// ones after them still land where they were.
|
|
||||||
pub fn reupload(&mut self) {
|
|
||||||
self.updates.clear();
|
|
||||||
self.updates.extend(
|
|
||||||
(0..self.resources.borrow().capacity() as u32)
|
|
||||||
.map(|i| Update::Push(self.kinds[i as usize], i)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn free(&mut self) {
|
pub fn free(&mut self) {
|
||||||
let updates = &mut self.updates;
|
for idx in self.recv.try_iter() {
|
||||||
let images = &mut self.images;
|
self.images[idx as usize] = None;
|
||||||
let free_page_layers = &mut self.free_page_layers;
|
self.updates.push(Update::Free(idx));
|
||||||
self.resources.borrow_mut().apply(|id, resource| {
|
self.free.push(idx);
|
||||||
let idx = id.slot();
|
|
||||||
images[idx as usize] = None;
|
|
||||||
updates.push(Update::Free(idx));
|
|
||||||
if let TextureKind::Page { layer } = resource.kind {
|
|
||||||
free_page_layers.push(layer);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn updates(&mut self) -> impl Iterator<Item = TextureUpdate<'_>> {
|
pub fn updates(&mut self) -> impl Iterator<Item = TextureUpdate<'_>> {
|
||||||
self.updates.drain(..).map(|u| match u {
|
self.updates.drain(..).map(|u| match u {
|
||||||
Update::Push(kind, i) => self.images[i as usize]
|
Update::Push(i) => self.images[i as usize]
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|img| TextureUpdate::Push(kind, img))
|
.map(TextureUpdate::Push)
|
||||||
.unwrap_or(TextureUpdate::PushFree(kind)),
|
.unwrap_or(TextureUpdate::PushFree),
|
||||||
Update::Set(kind, i) => self.images[i as usize]
|
Update::Set(i) => self.images[i as usize]
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|img| TextureUpdate::Set(kind, i, img))
|
.map(|img| TextureUpdate::Set(i, img))
|
||||||
.unwrap_or(TextureUpdate::SetFree),
|
.unwrap_or(TextureUpdate::SetFree),
|
||||||
Update::Patch(i, rect) => self.images[i as usize]
|
Update::Patch(i, rect) => self.images[i as usize]
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -235,45 +129,28 @@ impl Textures {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TextureHandle {
|
impl TextureHandle {
|
||||||
|
/// Index into `Textures`, and into the renderer's parallel slots.
|
||||||
|
pub fn slot(&self) -> u32 {
|
||||||
|
self.slot
|
||||||
|
}
|
||||||
pub fn size(&self) -> Vec2 {
|
pub fn size(&self) -> Vec2 {
|
||||||
self.rsc.get().size
|
self.size
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The bind-group index this handle draws with. Only valid for a
|
impl Drop for TextureHandle {
|
||||||
/// standalone image; an atlas page has no bind group of its own -- it
|
fn drop(&mut self) {
|
||||||
/// samples the shared array via `layer()` instead. Getting this wrong is
|
if self.counter.drop() {
|
||||||
/// a caller bug (the wrong kind of handle reached the wrong draw path),
|
let _ = self.send.send(self.slot);
|
||||||
/// not a recoverable condition, so it panics rather than drawing garbage.
|
|
||||||
pub fn image_index(&self) -> u32 {
|
|
||||||
match self.rsc.get().kind {
|
|
||||||
TextureKind::Image => self.rsc.id().slot(),
|
|
||||||
TextureKind::Page { .. } => panic!("image_index() called on an atlas page handle"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layer(&self) -> u32 {
|
|
||||||
match self.rsc.get().kind {
|
|
||||||
TextureKind::Page { layer } => layer,
|
|
||||||
TextureKind::Image => panic!("layer() called on a standalone image handle"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn strong(&self) -> StrongRscId<TextureRsc> {
|
|
||||||
self.rsc.strong()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn weak(&self) -> WeakRscId<TextureRsc> {
|
|
||||||
self.rsc.weak()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Index<&TextureHandle> for Textures {
|
impl Index<&TextureHandle> for Textures {
|
||||||
type Output = DynamicImage;
|
type Output = DynamicImage;
|
||||||
|
|
||||||
fn index(&self, index: &TextureHandle) -> &Self::Output {
|
fn index(&self, index: &TextureHandle) -> &Self::Output {
|
||||||
self.images[index.rsc.id().slot() as usize]
|
self.images[index.slot as usize].as_ref().unwrap()
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,96 +159,3 @@ impl Default for Textures {
|
|||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use image::RgbaImage;
|
|
||||||
|
|
||||||
fn image(n: u32) -> DynamicImage {
|
|
||||||
RgbaImage::new(n, n).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn key(id: u64) -> SharedTextureKey {
|
|
||||||
SharedTextureKey { owner: "test", id }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_shared_texture_is_built_once_and_handed_out_again() {
|
|
||||||
let mut textures = Textures::new();
|
|
||||||
let built = std::cell::Cell::new(0);
|
|
||||||
let make = |textures: &mut Textures, id: u64| {
|
|
||||||
textures.shared(key(id), || {
|
|
||||||
built.set(built.get() + 1);
|
|
||||||
image(4)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let first = make(&mut textures, 1);
|
|
||||||
let again = make(&mut textures, 1);
|
|
||||||
let other = make(&mut textures, 2);
|
|
||||||
assert_eq!(built.get(), 2, "the second ask for key 1 rasterised again");
|
|
||||||
assert_eq!(first.image_index(), again.image_index());
|
|
||||||
assert_ne!(first.image_index(), other.image_index());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_shared_slot_is_not_freed_when_the_last_widget_drops_it() {
|
|
||||||
let mut textures = Textures::new();
|
|
||||||
let slot = textures.shared(key(1), || image(4)).image_index();
|
|
||||||
textures.free();
|
|
||||||
let plain = textures.add(image(4));
|
|
||||||
assert_ne!(
|
|
||||||
plain.image_index(),
|
|
||||||
slot,
|
|
||||||
"an ordinary texture was handed the shared mark's slot"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_released_atlas_page_layer_is_reused_without_moving_live_pages() {
|
|
||||||
let mut textures = Textures::new();
|
|
||||||
let first = textures.add_page(image(4));
|
|
||||||
let second = textures.add_page(image(4));
|
|
||||||
let first_layer = first.layer();
|
|
||||||
let second_layer = second.layer();
|
|
||||||
drop(first);
|
|
||||||
textures.free();
|
|
||||||
|
|
||||||
let replacement = textures.add_page(image(4));
|
|
||||||
|
|
||||||
assert_eq!(replacement.layer(), first_layer);
|
|
||||||
assert_eq!(second.layer(), second_layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reupload_replays_every_slot_in_order_including_the_empty_ones() {
|
|
||||||
let mut textures = Textures::new();
|
|
||||||
let keep_a = textures.add(image(4));
|
|
||||||
let dropped = textures.add(image(4));
|
|
||||||
let keep_b = textures.add(image(4));
|
|
||||||
let (a, gone, b) = (
|
|
||||||
keep_a.image_index(),
|
|
||||||
dropped.image_index(),
|
|
||||||
keep_b.image_index(),
|
|
||||||
);
|
|
||||||
drop(dropped);
|
|
||||||
textures.free();
|
|
||||||
assert!(textures.updates().count() > 0);
|
|
||||||
|
|
||||||
textures.reupload();
|
|
||||||
let kinds: Vec<String> = textures
|
|
||||||
.updates()
|
|
||||||
.map(|u| match u {
|
|
||||||
TextureUpdate::Push(..) => "push".to_string(),
|
|
||||||
TextureUpdate::PushFree(..) => "push-free".to_string(),
|
|
||||||
_ => "other".to_string(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
assert_eq!(
|
|
||||||
kinds,
|
|
||||||
["push", "push-free", "push"],
|
|
||||||
"slots {a}, {gone} (freed) and {b} must replay in order, so the \
|
|
||||||
indices after a hole still land where they were"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+159
-139
@@ -1,218 +1,237 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
PatchRect, TextureHandle, Textures,
|
PatchRect, PxVec2,
|
||||||
util::{HashMap, Vec2},
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
use image::RgbaImage;
|
use image::RgbaImage;
|
||||||
use swash::scale::image::{Content, Image};
|
use swash::scale::image::{Content, Image};
|
||||||
|
|
||||||
|
/// Side of one page, and so of every layer of `render::page`'s array texture.
|
||||||
pub(crate) const PAGE: u32 = 1024;
|
pub(crate) const PAGE: u32 = 1024;
|
||||||
pub(crate) const DEFAULT_GLYPH_BUCKET_ID: u64 = 0;
|
|
||||||
|
|
||||||
|
/// Transparent margin kept around every glyph, so that sampling one cannot
|
||||||
|
/// pick up its neighbour along a shared edge.
|
||||||
const PAD: u32 = 1;
|
const PAD: u32 = 1;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct GlyphKey {
|
pub struct GlyphKey {
|
||||||
pub font: u64,
|
pub font: u64,
|
||||||
pub glyph: u32,
|
pub glyph: u32,
|
||||||
|
/// Font size in 1/16 px, so sizes that round to the same pixels share a
|
||||||
|
/// raster instead of filling the atlas with near-duplicates.
|
||||||
pub size: u32,
|
pub size: u32,
|
||||||
|
/// Horizontal subpixel phase, in 1/4 px.
|
||||||
pub subpixel: u8,
|
pub subpixel: u8,
|
||||||
|
/// Hash of the variation coordinates; a variable font at two weights is two
|
||||||
|
/// different sets of pixels from one glyph id.
|
||||||
pub coords: u64,
|
pub coords: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct GlyphEntry {
|
pub struct GlyphEntry {
|
||||||
pub uv_min: [f32; 2],
|
pub uv_min: Vec2,
|
||||||
pub uv_max: [f32; 2],
|
pub uv_max: Vec2,
|
||||||
|
/// Offset from the glyph's pen position to the top-left of its pixels.
|
||||||
pub left: i32,
|
pub left: i32,
|
||||||
pub top: i32,
|
pub top: i32,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub is_color: bool,
|
pub is_colored: bool,
|
||||||
|
/// Which atlas array layer this glyph is on.
|
||||||
pub layer: u32,
|
pub layer: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GlyphEntry {
|
||||||
|
const IS_COLORED: u32 = 1;
|
||||||
|
|
||||||
|
pub(crate) fn flags(&self) -> u32 {
|
||||||
|
if self.is_colored { Self::IS_COLORED } else { 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Page {
|
struct Page {
|
||||||
handle: TextureHandle,
|
image: RgbaImage,
|
||||||
/// Shelf packing: glyphs are placed left to right along a shelf whose
|
|
||||||
/// height is the tallest glyph on it, and a new shelf starts above when the
|
|
||||||
/// row runs out. Chosen over a real packer because glyphs at one size are
|
|
||||||
/// close to the same height, which is the case shelves are good at.
|
|
||||||
x: u32,
|
x: u32,
|
||||||
y: u32,
|
y: u32,
|
||||||
shelf_height: u32,
|
shelf_height: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
/// A rectangle of one page the renderer has not uploaded yet.
|
||||||
struct Bucket {
|
#[derive(Clone, Copy)]
|
||||||
pages: Vec<Page>,
|
pub struct PageUpload {
|
||||||
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
pub layer: u32,
|
||||||
|
pub rect: PatchRect,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GlyphAtlas {
|
pub struct GlyphAtlas {
|
||||||
buckets: HashMap<u64, Bucket>,
|
pages: Vec<Page>,
|
||||||
generation: u64,
|
/// `None` for a glyph that rasterised to nothing -- a space, say. Cached
|
||||||
|
/// too, so it is not re-rasterised on every layout.
|
||||||
|
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
||||||
|
uploads: Vec<PageUpload>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GlyphAtlas {
|
impl GlyphAtlas {
|
||||||
pub(crate) fn get(&self, bucket: u64, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
|
pub fn get(&self, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
|
||||||
self.buckets.get(&bucket)?.entries.get(key).copied()
|
self.entries.get(key).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rasterised pixels in, a place in the atlas out. `None` means the glyph
|
pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option<GlyphEntry> {
|
||||||
/// has no pixels, which is a normal answer rather than a failure.
|
|
||||||
pub fn insert(
|
|
||||||
&mut self,
|
|
||||||
bucket: u64,
|
|
||||||
key: GlyphKey,
|
|
||||||
image: &Image,
|
|
||||||
textures: &mut Textures,
|
|
||||||
) -> Option<GlyphEntry> {
|
|
||||||
let bucket = self.buckets.entry(bucket).or_default();
|
|
||||||
let w = image.placement.width;
|
let w = image.placement.width;
|
||||||
let h = image.placement.height;
|
let h = image.placement.height;
|
||||||
if w == 0 || h == 0 {
|
if w == 0 || h == 0 {
|
||||||
bucket.entries.insert(key, None);
|
log::warn!(
|
||||||
|
"glyph {} in font {} rasterized at {w}x{h}; skipping it",
|
||||||
|
key.glyph,
|
||||||
|
key.font,
|
||||||
|
);
|
||||||
|
self.entries.insert(key, None);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE {
|
if w > PAGE - PAD * 2 || h > PAGE - PAD * 2 {
|
||||||
// A single glyph larger than a page. Refusing is better than
|
log::warn!(
|
||||||
// silently drawing a cropped one; the caller draws nothing.
|
"glyph {} in font {} rasterized at {w}x{h}, too large for the {PAGE}x{PAGE} atlas; skipping it",
|
||||||
bucket.entries.insert(key, None);
|
key.glyph,
|
||||||
|
key.font,
|
||||||
|
);
|
||||||
|
self.entries.insert(key, None);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (page_idx, x, y) = bucket.allocate(w, h, textures);
|
let upload = self.allocate(w, h);
|
||||||
let page = &bucket.pages[page_idx];
|
let PatchRect { x, y, .. } = upload.rect;
|
||||||
|
write_glyph(&mut self.pages[upload.layer as usize].image, image, x, y);
|
||||||
|
self.uploads.push(upload);
|
||||||
|
|
||||||
let img = textures.image_mut(&page.handle);
|
let scale = 1.0 / PAGE as f32;
|
||||||
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
|
let entry = GlyphEntry {
|
||||||
write_glyph(rgba, image, x, y);
|
uv_min: Vec2::new(x as f32 * scale, y as f32 * scale),
|
||||||
|
uv_max: Vec2::new((x + w) as f32 * scale, (y + h) as f32 * scale),
|
||||||
|
left: image.placement.left,
|
||||||
|
top: image.placement.top,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
is_colored: matches!(image.content, Content::Color),
|
||||||
|
layer: upload.layer,
|
||||||
|
};
|
||||||
|
self.entries.insert(key, Some(entry));
|
||||||
|
Some(entry)
|
||||||
|
}
|
||||||
|
|
||||||
let rect = PatchRect {
|
/// Reserves room for a `w` by `h` glyph, adding a page if none has it.
|
||||||
|
fn allocate(&mut self, w: u32, h: u32) -> PageUpload {
|
||||||
|
let rect = |x, y| PatchRect {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width: w,
|
width: w,
|
||||||
height: h,
|
height: h,
|
||||||
};
|
};
|
||||||
textures.patch(&page.handle, rect);
|
if let Some((i, (x, y))) = self
|
||||||
|
.pages
|
||||||
let page = &bucket.pages[page_idx];
|
.iter_mut()
|
||||||
let scale = 1.0 / PAGE as f32;
|
.enumerate()
|
||||||
let entry = GlyphEntry {
|
.find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position)))
|
||||||
uv_min: [x as f32 * scale, y as f32 * scale],
|
{
|
||||||
uv_max: [(x + w) as f32 * scale, (y + h) as f32 * scale],
|
return PageUpload {
|
||||||
left: image.placement.left,
|
layer: i as u32,
|
||||||
top: image.placement.top,
|
rect: rect(x, y),
|
||||||
width: w,
|
|
||||||
height: h,
|
|
||||||
is_color: matches!(image.content, Content::Color),
|
|
||||||
layer: page.handle.layer(),
|
|
||||||
};
|
};
|
||||||
bucket.entries.insert(key, Some(entry));
|
|
||||||
Some(entry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn insert_empty(&mut self, bucket: u64, key: GlyphKey) {
|
|
||||||
self.buckets
|
|
||||||
.entry(bucket)
|
|
||||||
.or_default()
|
|
||||||
.entries
|
|
||||||
.insert(key, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn clear_bucket(&mut self, bucket: u64) {
|
|
||||||
if self.buckets.remove(&bucket).is_some() {
|
|
||||||
self.generation += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generation(&self) -> u64 {
|
|
||||||
self.generation
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn page_count(&self) -> usize {
|
|
||||||
self.buckets.values().map(|bucket| bucket.pages.len()).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn glyph_count(&self) -> usize {
|
|
||||||
self.buckets
|
|
||||||
.values()
|
|
||||||
.map(|bucket| bucket.entries.len())
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.buckets.clear();
|
|
||||||
self.generation += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Bucket {
|
|
||||||
fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) {
|
|
||||||
let need_w = w + PAD;
|
|
||||||
let need_h = h + PAD;
|
|
||||||
if let Some(i) = self.pages.iter().position(|p| fits(p, need_w, need_h)) {
|
|
||||||
let page = &mut self.pages[i];
|
|
||||||
if page.x + need_w > PAGE {
|
|
||||||
page.y += page.shelf_height;
|
|
||||||
page.x = PAD;
|
|
||||||
page.shelf_height = 0;
|
|
||||||
}
|
|
||||||
let (x, y) = (page.x, page.y);
|
|
||||||
page.x += need_w;
|
|
||||||
page.shelf_height = page.shelf_height.max(need_h);
|
|
||||||
return (i, x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
let handle = textures.add_page(RgbaImage::new(PAGE, PAGE));
|
|
||||||
self.pages.push(Page {
|
self.pages.push(Page {
|
||||||
handle,
|
image: RgbaImage::new(PAGE, PAGE),
|
||||||
x: PAD + w + PAD,
|
x: PAD + w + PAD,
|
||||||
y: PAD,
|
y: PAD,
|
||||||
shelf_height: h + PAD,
|
shelf_height: h + PAD,
|
||||||
});
|
});
|
||||||
(self.pages.len() - 1, PAD, PAD)
|
PageUpload {
|
||||||
|
layer: self.pages.len() as u32 - 1,
|
||||||
|
rect: rect(PAD, PAD),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drains what has been written since the last call, for the renderer to
|
||||||
|
/// upload. A new page needs nothing more: wgpu leaves the rest of a fresh
|
||||||
|
/// layer transparent, which is what an atlas wants.
|
||||||
|
pub fn uploads(&mut self) -> impl Iterator<Item = (PageUpload, &RgbaImage)> {
|
||||||
|
let pages = &self.pages;
|
||||||
|
self.uploads
|
||||||
|
.drain(..)
|
||||||
|
.map(|upload| (upload, &pages[upload.layer as usize].image))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_empty(&mut self, key: GlyphKey) {
|
||||||
|
self.entries.insert(key, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn page_count(&self) -> u32 {
|
||||||
|
self.pages.len() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn glyph_count(&self) -> usize {
|
||||||
|
self.entries.len()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fits(page: &Page, need_w: u32, need_h: u32) -> bool {
|
impl Page {
|
||||||
(page.x + need_w <= PAGE && page.y + need_h <= PAGE)
|
fn allocate(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
|
||||||
|| (need_w + PAD <= PAGE && page.y + page.shelf_height + need_h <= PAGE)
|
let need_w = w + PAD;
|
||||||
|
let need_h = h + PAD;
|
||||||
|
if self.x + need_w > PAGE {
|
||||||
|
if need_w + PAD > PAGE || self.y + self.shelf_height + need_h > PAGE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.y += self.shelf_height;
|
||||||
|
self.x = PAD;
|
||||||
|
self.shelf_height = 0;
|
||||||
|
} else if self.y + need_h > PAGE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let position = (self.x, self.y);
|
||||||
|
self.x += need_w;
|
||||||
|
self.shelf_height = self.shelf_height.max(need_h);
|
||||||
|
Some(position)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mask glyphs keep coverage in alpha so their raster can be tinted at draw time.
|
||||||
fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
|
fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
|
||||||
let w = image.placement.width;
|
let width = image.placement.width as usize;
|
||||||
let h = image.placement.height;
|
let height = image.placement.height as usize;
|
||||||
|
let page_stride = page.width() as usize * 4;
|
||||||
|
let x = x as usize * 4;
|
||||||
|
let y = y as usize;
|
||||||
|
let page = page.as_mut();
|
||||||
|
|
||||||
|
for row in 0..height {
|
||||||
|
let start = (y + row) * page_stride + x;
|
||||||
|
let target = &mut page[start..start + width * 4];
|
||||||
match image.content {
|
match image.content {
|
||||||
Content::Mask => {
|
|
||||||
for row in 0..h {
|
|
||||||
for col in 0..w {
|
|
||||||
let a = image.data[(row * w + col) as usize];
|
|
||||||
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Content::Color => {
|
Content::Color => {
|
||||||
for row in 0..h {
|
let start = row * width * 4;
|
||||||
for col in 0..w {
|
target.copy_from_slice(&image.data[start..start + width * 4]);
|
||||||
let i = ((row * w + col) * 4) as usize;
|
|
||||||
let px = [
|
|
||||||
image.data[i],
|
|
||||||
image.data[i + 1],
|
|
||||||
image.data[i + 2],
|
|
||||||
image.data[i + 3],
|
|
||||||
];
|
|
||||||
page.put_pixel(x + col, y + row, image::Rgba(px));
|
|
||||||
}
|
}
|
||||||
|
Content::Mask => {
|
||||||
|
let start = row * width;
|
||||||
|
for (target, &alpha) in target
|
||||||
|
.as_chunks_mut::<4>()
|
||||||
|
.0
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&image.data[start..start + width])
|
||||||
|
{
|
||||||
|
target.copy_from_slice(&[255, 255, 255, alpha]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Content::SubpixelMask => {
|
Content::SubpixelMask => {
|
||||||
for row in 0..h {
|
let start = row * width * 4;
|
||||||
for col in 0..w {
|
for (target, source) in target
|
||||||
let i = ((row * w + col) * 4) as usize;
|
.as_chunks_mut::<4>()
|
||||||
let a = image.data[i + 1];
|
.0
|
||||||
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
|
.iter_mut()
|
||||||
|
.zip(image.data[start..start + width * 4].as_chunks::<4>().0)
|
||||||
|
{
|
||||||
|
target.copy_from_slice(&[255, 255, 255, source[1]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,6 +241,7 @@ fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
|
|||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct PlacedGlyph {
|
pub struct PlacedGlyph {
|
||||||
pub entry: GlyphEntry,
|
pub entry: GlyphEntry,
|
||||||
pub offset: Vec2,
|
/// Whole pixels from the origin of the text to this glyph's top-left,
|
||||||
pub paint: u32,
|
/// on the grid once here rather than on every frame that draws it.
|
||||||
|
pub offset: PxVec2,
|
||||||
}
|
}
|
||||||
+54
-42
@@ -1,29 +1,38 @@
|
|||||||
use crate::{UiRegion, util::Id};
|
use crate::{UiRegion, util::Id, util::Vec2};
|
||||||
use wgpu::*;
|
use wgpu::*;
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||||
pub struct WindowUniform {
|
pub struct WindowUniform {
|
||||||
pub width: f32,
|
pub dim: Vec2,
|
||||||
pub height: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct PrimitiveInstance {
|
pub struct PrimitiveInstance {
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
pub binding: u32,
|
|
||||||
pub idx: u32,
|
|
||||||
pub mask_idx: MaskIdx,
|
pub mask_idx: MaskIdx,
|
||||||
pub move_idx: MoveIdx,
|
pub move_idx: MoveIdx,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn instance_slot_layout() -> VertexBufferLayout<'static> {
|
impl PrimitiveInstance {
|
||||||
const ATTRIBS: [VertexAttribute; 1] = vertex_attr_array![0 => Uint32];
|
// The region's four scalars, each a `Rel` beside a `Px`: whole counts
|
||||||
|
// that the shader decodes, rather than the numbers themselves.
|
||||||
|
const ATTRIBS: [VertexAttribute; 6] = vertex_attr_array![
|
||||||
|
0 => Sint32x2,
|
||||||
|
1 => Sint32x2,
|
||||||
|
2 => Sint32x2,
|
||||||
|
3 => Sint32x2,
|
||||||
|
4 => Uint32,
|
||||||
|
5 => Uint32,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn desc() -> VertexBufferLayout<'static> {
|
||||||
VertexBufferLayout {
|
VertexBufferLayout {
|
||||||
array_stride: std::mem::size_of::<u32>() as BufferAddress,
|
array_stride: std::mem::size_of::<Self>() as BufferAddress,
|
||||||
step_mode: VertexStepMode::Instance,
|
step_mode: VertexStepMode::Instance,
|
||||||
attributes: &ATTRIBS,
|
attributes: &Self::ATTRIBS,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,46 +42,49 @@ impl MaskIdx {
|
|||||||
pub const NONE: Self = Self::preset(u32::MAX);
|
pub const NONE: Self = Self::preset(u32::MAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type MoveIdx = Id<u32>;
|
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct Mask {
|
pub struct Mask {
|
||||||
pub primitive: u32,
|
pub region: UiRegion,
|
||||||
/// The mask this one was set *inside* (`MaskIdx::NONE` at the top), so
|
pub move_idx: MoveIdx,
|
||||||
/// clipping nests: the fragment stage walks the chain and multiplies
|
|
||||||
/// every coverage on it, which is what makes a pixel inside two
|
|
||||||
/// feathered corners dimmed by both. Chained rather than intersected
|
|
||||||
/// on the CPU because each mask moves with its own widget -- a code
|
|
||||||
/// fence inside a transcript row carries the row's scroll, the list's
|
|
||||||
/// own box does not, and one region resolved when the fence was last
|
|
||||||
/// drawn gets the second of those wrong as soon as the row moves.
|
|
||||||
pub parent: MaskIdx,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `_pad` matches WGSL's storage-buffer layout for `MoveOffset`: `delta` is
|
/// Its own type rather than another `Id<u32>`, because it sits beside
|
||||||
/// a `vec2<f32>`, which gives the struct an 8-byte alignment and rounds its
|
/// `MaskIdx` in an instance and the two must not be swappable.
|
||||||
/// WGSL size up to 16 bytes even though `delta` + `parent` only total 12 --
|
#[repr(transparent)]
|
||||||
/// the same trap `GlyphPrimitive` documents below. `bytemuck` does not
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
/// check this for us, and getting it wrong is a wgpu validation panic at
|
pub struct MoveIdx(u32);
|
||||||
/// draw time ("buffer bound ... with size 12 where the shader expects 16"),
|
|
||||||
/// not a compile error.
|
impl MoveIdx {
|
||||||
#[repr(C)]
|
pub const NONE: Self = Self(u32::MAX);
|
||||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
||||||
pub struct MoveOffset {
|
pub(crate) fn slot(idx: usize) -> Self {
|
||||||
pub delta: [f32; 2],
|
Self(idx as u32)
|
||||||
pub parent: u32,
|
}
|
||||||
_pad: 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 {
|
impl MoveOffset {
|
||||||
pub const NONE_PARENT: u32 = u32::MAX;
|
pub fn new(parent: MoveIdx, region: UiRegion) -> Self {
|
||||||
|
Self { region, parent }
|
||||||
pub fn new(delta: [f32; 2], parent: u32) -> Self {
|
|
||||||
Self {
|
|
||||||
delta,
|
|
||||||
parent,
|
|
||||||
_pad: 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,776 +0,0 @@
|
|||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
/// The frame budget `dumpsys gfxinfo` also uses to call a frame "janky": the
|
|
||||||
/// 60Hz vsync period. Kept as the same threshold so a percentage from this
|
|
||||||
/// report and a percentage from `gfxinfo` mean the same thing. Only a
|
|
||||||
/// fallback now that a caller can read the display's real refresh rate
|
|
||||||
/// (`report_at_hz`/`mark_phase`'s callers) -- most devices are 60Hz, but a
|
|
||||||
/// 90Hz or 120Hz phone judged against this constant would call every frame
|
|
||||||
/// "late" that merely met its own, faster budget.
|
|
||||||
pub const JANK_THRESHOLD: Duration = Duration::from_nanos(16_666_667);
|
|
||||||
|
|
||||||
const RING_CAPACITY: usize = 16384;
|
|
||||||
|
|
||||||
const MIN_CADENCE_SAMPLES: usize = 12;
|
|
||||||
|
|
||||||
struct PhaseMark {
|
|
||||||
name: String,
|
|
||||||
start_index: u64,
|
|
||||||
start_at: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PhaseStats {
|
|
||||||
pub name: String,
|
|
||||||
pub frames: u64,
|
|
||||||
pub duration: Duration,
|
|
||||||
/// On a backend that blocks in `present()` rather than in the
|
|
||||||
/// acquire -- GLES, and so this repo's emulator -- the wait lands in
|
|
||||||
/// `submit` instead and this over-counts. Named rather than
|
|
||||||
/// corrected, since correcting it would mean guessing which part of
|
|
||||||
/// `submit` was a wait.
|
|
||||||
pub late: u64,
|
|
||||||
pub late_percent: f64,
|
|
||||||
pub p50: Duration,
|
|
||||||
pub p90: Duration,
|
|
||||||
pub p99: Duration,
|
|
||||||
pub worst: Duration,
|
|
||||||
/// This phase's own medians of the three parts a frame is made of --
|
|
||||||
/// see [`FrameParts`]. Per phase as well as per run because the parts
|
|
||||||
/// do not divide the same way in every phase: a fling frame spends
|
|
||||||
/// most of itself in `acquire` (waiting its turn at the swapchain,
|
|
||||||
/// which is the display pacing the app and not work) while a
|
|
||||||
/// streaming frame spends it in `build`, and a run-wide median cannot
|
|
||||||
/// say that.
|
|
||||||
/// Vsyncs that went by with no frame produced for them, counted from
|
|
||||||
/// the gap between consecutive frames rather than from their cost.
|
|
||||||
pub missed: u64,
|
|
||||||
pub build_p50: Duration,
|
|
||||||
pub acquire_p50: Duration,
|
|
||||||
pub submit_p50: Duration,
|
|
||||||
pub complete: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for PhaseStats {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
" {}: {} frames over {:.1}s{}",
|
|
||||||
self.name,
|
|
||||||
self.frames,
|
|
||||||
self.duration.as_secs_f64(),
|
|
||||||
if self.complete {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
" (ring evicted some of this phase)"
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
" late: {} ({:.1}%) missed vsyncs: {}",
|
|
||||||
self.late, self.late_percent, self.missed,
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
" total p50 {:.1}ms p90 {:.1}ms p99 {:.1}ms",
|
|
||||||
self.p50.as_secs_f64() * 1000.0,
|
|
||||||
self.p90.as_secs_f64() * 1000.0,
|
|
||||||
self.p99.as_secs_f64() * 1000.0,
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
" build p50 {:.1}ms acquire p50 {:.1}ms submit p50 {:.1}ms",
|
|
||||||
self.build_p50.as_secs_f64() * 1000.0,
|
|
||||||
self.acquire_p50.as_secs_f64() * 1000.0,
|
|
||||||
self.submit_p50.as_secs_f64() * 1000.0,
|
|
||||||
)?;
|
|
||||||
write!(f, " worst {:.1}ms", self.worst.as_secs_f64() * 1000.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The parts one frame's wall time divides into, measured rather than
|
|
||||||
/// inferred: what a caller hands [`FrameReport::record`].
|
|
||||||
#[derive(Clone, Copy, Default, Debug)]
|
|
||||||
pub struct FrameParts {
|
|
||||||
pub total: Duration,
|
|
||||||
pub acquire: Duration,
|
|
||||||
pub submit: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FrameParts {
|
|
||||||
/// A frame measured as one span, with no parts -- honest for a caller
|
|
||||||
/// that never measured them (they read as zero and `build` reads as
|
|
||||||
/// the whole frame) rather than fabricating a split.
|
|
||||||
pub fn whole(total: Duration) -> Self {
|
|
||||||
Self {
|
|
||||||
total,
|
|
||||||
acquire: Duration::ZERO,
|
|
||||||
submit: Duration::ZERO,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The two waits a renderer's `draw` measures, with `total` left at
|
|
||||||
/// zero for the frame loop around it to fill in -- it is the only
|
|
||||||
/// caller that knows when the frame started.
|
|
||||||
pub fn waits(acquire: Duration, submit: Duration) -> Self {
|
|
||||||
Self {
|
|
||||||
total: Duration::ZERO,
|
|
||||||
acquire,
|
|
||||||
submit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What is left once the two measured waits are taken off: laying
|
|
||||||
/// out, shaping text, building primitives and recording the render
|
|
||||||
/// pass. Saturating, since the three come from different `Instant`
|
|
||||||
/// pairs on a clock a caller owns.
|
|
||||||
pub fn build(&self) -> Duration {
|
|
||||||
self.total
|
|
||||||
.saturating_sub(self.acquire)
|
|
||||||
.saturating_sub(self.submit)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn work(&self) -> Duration {
|
|
||||||
self.total.saturating_sub(self.acquire)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **What this does not measure**: wgpu's `present()` call queues the frame
|
|
||||||
/// with the compositor and returns; it is not fenced against the GPU
|
|
||||||
/// actually finishing the frame or the compositor actually showing it, the
|
|
||||||
/// way `gfxinfo`'s own `GPU_DURATION`/vsync accounting is. So a sample here
|
|
||||||
/// is "how long the CPU took to build and submit this frame", not
|
|
||||||
/// "how long the frame took to reach the screen" -- named in
|
|
||||||
/// [`FrameStats`]'s own `Display` line rather than presented as the latter,
|
|
||||||
/// per the standing rule against showing an inferred number as a measured
|
|
||||||
/// one where the two differ.
|
|
||||||
pub struct FrameReport {
|
|
||||||
ring: Box<[Duration; RING_CAPACITY]>,
|
|
||||||
submit_ring: Box<[Duration; RING_CAPACITY]>,
|
|
||||||
/// The `acquire` half of each sample in `ring`, same index, same
|
|
||||||
/// lifetime -- see [`FrameParts::acquire`], which is the part that is
|
|
||||||
/// a wait rather than work.
|
|
||||||
acquire_ring: Box<[Duration; RING_CAPACITY]>,
|
|
||||||
/// How long before each sample the *previous* frame was, same index,
|
|
||||||
/// same lifetime -- the frame's own cadence rather than its cost. See
|
|
||||||
/// [`PhaseStats::missed`] for why a report needs both.
|
|
||||||
gap_ring: Box<[Duration; RING_CAPACITY]>,
|
|
||||||
last_frame: Option<(Instant, bool)>,
|
|
||||||
index_ring: Box<[u64; RING_CAPACITY]>,
|
|
||||||
len: usize,
|
|
||||||
pos: usize,
|
|
||||||
total_frames: u64,
|
|
||||||
janky_frames: u64,
|
|
||||||
phases: Vec<PhaseMark>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FrameStats {
|
|
||||||
pub total_frames: u64,
|
|
||||||
pub janky_percent: f64,
|
|
||||||
pub p50: Duration,
|
|
||||||
pub p90: Duration,
|
|
||||||
pub p99: Duration,
|
|
||||||
pub worst: Duration,
|
|
||||||
pub cpu_p50: Duration,
|
|
||||||
pub acquire_p50: Duration,
|
|
||||||
pub gpu_wait_p50: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for FrameStats {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"frames={} janky%={:.2} p50={:.1}ms p90={:.1}ms p99={:.1}ms worst={:.1}ms \
|
|
||||||
(measures redraw-start to after present() is called, not GPU/compositor \
|
|
||||||
completion)",
|
|
||||||
self.total_frames,
|
|
||||||
self.janky_percent,
|
|
||||||
self.p50.as_secs_f64() * 1000.0,
|
|
||||||
self.p90.as_secs_f64() * 1000.0,
|
|
||||||
self.p99.as_secs_f64() * 1000.0,
|
|
||||||
self.worst.as_secs_f64() * 1000.0,
|
|
||||||
)?;
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
" cpu_p50={:.1}ms acquire_p50={:.1}ms gpu_wait_p50={:.1}ms (own work vs. \
|
|
||||||
waiting for a swapchain image vs. submit-to-after-present)",
|
|
||||||
self.cpu_p50.as_secs_f64() * 1000.0,
|
|
||||||
self.acquire_p50.as_secs_f64() * 1000.0,
|
|
||||||
self.gpu_wait_p50.as_secs_f64() * 1000.0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FrameReport {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
ring: Box::new([Duration::ZERO; RING_CAPACITY]),
|
|
||||||
submit_ring: Box::new([Duration::ZERO; RING_CAPACITY]),
|
|
||||||
acquire_ring: Box::new([Duration::ZERO; RING_CAPACITY]),
|
|
||||||
gap_ring: Box::new([Duration::ZERO; RING_CAPACITY]),
|
|
||||||
last_frame: None,
|
|
||||||
index_ring: Box::new([0; RING_CAPACITY]),
|
|
||||||
len: 0,
|
|
||||||
pos: 0,
|
|
||||||
total_frames: 0,
|
|
||||||
janky_frames: 0,
|
|
||||||
phases: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One entry point rather than one per shape of measurement: a caller
|
|
||||||
/// with nothing but a total passes `FrameParts::whole(total)`, which
|
|
||||||
/// says so in the type instead of leaving the report to guess from a
|
|
||||||
/// zero.
|
|
||||||
pub fn record(&mut self, at: Instant, parts: FrameParts, animating: bool) {
|
|
||||||
self.gap_ring[self.pos] = match self.last_frame {
|
|
||||||
Some((last, true)) => at.saturating_duration_since(last),
|
|
||||||
Some((_, false)) | None => Duration::ZERO,
|
|
||||||
};
|
|
||||||
self.last_frame = Some((at, animating));
|
|
||||||
self.ring[self.pos] = parts.total;
|
|
||||||
self.submit_ring[self.pos] = parts.submit;
|
|
||||||
self.acquire_ring[self.pos] = parts.acquire;
|
|
||||||
self.index_ring[self.pos] = self.total_frames;
|
|
||||||
self.pos = (self.pos + 1) % RING_CAPACITY;
|
|
||||||
self.len = (self.len + 1).min(RING_CAPACITY);
|
|
||||||
self.total_frames += 1;
|
|
||||||
if parts.total > JANK_THRESHOLD {
|
|
||||||
self.janky_frames += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
self.len = 0;
|
|
||||||
self.pos = 0;
|
|
||||||
self.total_frames = 0;
|
|
||||||
self.janky_frames = 0;
|
|
||||||
self.last_frame = None;
|
|
||||||
self.phases.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parts(&self, slot: usize) -> FrameParts {
|
|
||||||
FrameParts {
|
|
||||||
total: self.ring[slot],
|
|
||||||
acquire: self.acquire_ring[slot],
|
|
||||||
submit: self.submit_ring[slot],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mark_phase(&mut self, name: &str) {
|
|
||||||
debug_assert!(
|
|
||||||
self.phases
|
|
||||||
.last()
|
|
||||||
.is_none_or(|p| self.total_frames >= p.start_index)
|
|
||||||
);
|
|
||||||
self.phases.push(PhaseMark {
|
|
||||||
name: name.to_string(),
|
|
||||||
start_index: self.total_frames,
|
|
||||||
start_at: Instant::now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn phase_stats(&self, now: Instant, refresh_hz: f32) -> Vec<PhaseStats> {
|
|
||||||
if self.phases.is_empty() || refresh_hz <= 0.0 {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
let budget = Duration::from_secs_f64(1.0 / refresh_hz as f64);
|
|
||||||
self.phases
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, phase)| {
|
|
||||||
let (end_index, end_at) = match self.phases.get(i + 1) {
|
|
||||||
Some(next) => (next.start_index, next.start_at),
|
|
||||||
None => (self.total_frames, now),
|
|
||||||
};
|
|
||||||
let frames = end_index.saturating_sub(phase.start_index);
|
|
||||||
let slots: Vec<usize> = (0..self.len)
|
|
||||||
.filter(|&j| {
|
|
||||||
let idx = self.index_ring[j];
|
|
||||||
idx >= phase.start_index && idx < end_index
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let mut samples: Vec<Duration> = slots.iter().map(|&j| self.ring[j]).collect();
|
|
||||||
let complete = samples.len() as u64 >= frames;
|
|
||||||
if samples.is_empty() {
|
|
||||||
return PhaseStats {
|
|
||||||
name: phase.name.clone(),
|
|
||||||
frames,
|
|
||||||
duration: end_at.saturating_duration_since(phase.start_at),
|
|
||||||
late: 0,
|
|
||||||
late_percent: 0.0,
|
|
||||||
p50: Duration::ZERO,
|
|
||||||
p90: Duration::ZERO,
|
|
||||||
p99: Duration::ZERO,
|
|
||||||
worst: Duration::ZERO,
|
|
||||||
missed: 0,
|
|
||||||
build_p50: Duration::ZERO,
|
|
||||||
acquire_p50: Duration::ZERO,
|
|
||||||
submit_p50: Duration::ZERO,
|
|
||||||
complete,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let part_p50 = |part: &dyn Fn(usize) -> Duration| {
|
|
||||||
let mut v: Vec<Duration> = slots.iter().map(|&j| part(j)).collect();
|
|
||||||
v.sort_unstable();
|
|
||||||
v[v.len() / 2]
|
|
||||||
};
|
|
||||||
// **The phase's own first frame is skipped**: its gap
|
|
||||||
// reaches back into the previous phase, across whatever
|
|
||||||
// the run did between the two -- a bench pausing a second
|
|
||||||
// between phases would otherwise open each one with sixty
|
|
||||||
// "missed" frames nobody was waiting for.
|
|
||||||
let missed: u64 = slots
|
|
||||||
.iter()
|
|
||||||
.filter(|&&j| self.index_ring[j] > phase.start_index)
|
|
||||||
.map(|&j| self.gap_ring[j])
|
|
||||||
.filter(|gap| !gap.is_zero())
|
|
||||||
.filter(|gap| *gap > budget.mul_f64(1.5))
|
|
||||||
.map(|gap| (gap.as_secs_f64() / budget.as_secs_f64()).round() as u64 - 1)
|
|
||||||
.sum();
|
|
||||||
let build_p50 = part_p50(&|j| self.parts(j).build());
|
|
||||||
let acquire_p50 = part_p50(&|j| self.acquire_ring[j]);
|
|
||||||
let submit_p50 = part_p50(&|j| self.submit_ring[j]);
|
|
||||||
samples.sort_unstable();
|
|
||||||
let pct = |p: usize| samples[(samples.len() * p / 100).min(samples.len() - 1)];
|
|
||||||
let late = slots
|
|
||||||
.iter()
|
|
||||||
.filter(|&&j| self.parts(j).work() > budget)
|
|
||||||
.count() as u64;
|
|
||||||
PhaseStats {
|
|
||||||
name: phase.name.clone(),
|
|
||||||
frames,
|
|
||||||
duration: end_at.saturating_duration_since(phase.start_at),
|
|
||||||
late,
|
|
||||||
late_percent: 100.0 * late as f64 / samples.len() as f64,
|
|
||||||
p50: pct(50),
|
|
||||||
p90: pct(90),
|
|
||||||
p99: pct(99),
|
|
||||||
worst: *samples.last().expect("checked not empty above"),
|
|
||||||
missed,
|
|
||||||
build_p50,
|
|
||||||
acquire_p50,
|
|
||||||
submit_p50,
|
|
||||||
complete,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **This is a floor on the display's refresh rate, never a reading
|
|
||||||
/// of it.** You cannot observe a cadence faster than you draw, so an
|
|
||||||
/// app that never keeps up says nothing about the panel; a caller
|
|
||||||
/// resolves it by taking whichever of this and the platform's own
|
|
||||||
/// answer is *larger*. That matters in both directions and each has
|
|
||||||
/// been seen: `Display.getRefreshRate()` answered 60 for a run that
|
|
||||||
/// sustained 120.3fps, because a phone that varies its rate answers
|
|
||||||
/// with whatever mode it happens to be in when asked -- and this
|
|
||||||
/// answered 88 on an emulator whose display is 60Hz and whose app
|
|
||||||
/// managed 51, because an earlier version took the fastest tenth of
|
|
||||||
/// the gaps rather than the sustained rate. The fastest tenth is a
|
|
||||||
/// measurement of the best moment; the budget wants the rhythm.
|
|
||||||
///
|
|
||||||
/// `None` under `MIN_CADENCE_SAMPLES` measurable gaps, which is the
|
|
||||||
/// honest answer for a run too short or too idle to have seen one.
|
|
||||||
pub fn sustained_frame_hz(&self) -> Option<f32> {
|
|
||||||
let gaps = self.gap_ring[..self.len].iter().filter(|g| !g.is_zero());
|
|
||||||
let (count, total) = gaps.fold((0u32, Duration::ZERO), |(n, sum), g| (n + 1, sum + *g));
|
|
||||||
if (count as usize) < MIN_CADENCE_SAMPLES || total.is_zero() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(count as f32 / total.as_secs_f32())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `None` if nothing has been recorded since the last reset -- the
|
|
||||||
/// "no frames recorded, scroll first" case, not a zeroed report that
|
|
||||||
/// would read as a real (perfect) measurement.
|
|
||||||
pub fn report(&self) -> Option<FrameStats> {
|
|
||||||
if self.len == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut samples: Vec<Duration> = self.ring[..self.len].to_vec();
|
|
||||||
samples.sort_unstable();
|
|
||||||
let pct = |p: usize| samples[(samples.len() * p / 100).min(samples.len() - 1)];
|
|
||||||
|
|
||||||
let submit_samples: Vec<Duration> = self.submit_ring[..self.len].to_vec();
|
|
||||||
let acquire_samples: Vec<Duration> = self.acquire_ring[..self.len].to_vec();
|
|
||||||
let cpu_samples: Vec<Duration> = (0..self.len).map(|j| self.parts(j).build()).collect();
|
|
||||||
let median = |mut v: Vec<Duration>| {
|
|
||||||
v.sort_unstable();
|
|
||||||
v[v.len() / 2]
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(FrameStats {
|
|
||||||
total_frames: self.total_frames,
|
|
||||||
janky_percent: 100.0 * self.janky_frames as f64 / self.total_frames as f64,
|
|
||||||
p50: pct(50),
|
|
||||||
p90: pct(90),
|
|
||||||
p99: pct(99),
|
|
||||||
worst: *samples.last().expect("len > 0 checked above"),
|
|
||||||
cpu_p50: median(cpu_samples),
|
|
||||||
acquire_p50: median(acquire_samples),
|
|
||||||
gpu_wait_p50: median(submit_samples),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn late_at_hz(&self, refresh_hz: f32) -> (u64, f64) {
|
|
||||||
if self.len == 0 || refresh_hz <= 0.0 {
|
|
||||||
return (0, 0.0);
|
|
||||||
}
|
|
||||||
let budget = Duration::from_secs_f64(1.0 / refresh_hz as f64);
|
|
||||||
let late = (0..self.len)
|
|
||||||
.filter(|&j| self.parts(j).work() > budget)
|
|
||||||
.count() as u64;
|
|
||||||
(late, 100.0 * late as f64 / self.len as f64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FrameReport {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_frames_reports_none() {
|
|
||||||
assert!(FrameReport::new().report().is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn one_frame_is_every_percentile_and_the_worst() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_millis(10)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
let stats = r.report().unwrap();
|
|
||||||
assert_eq!(stats.total_frames, 1);
|
|
||||||
assert_eq!(stats.p50, Duration::from_millis(10));
|
|
||||||
assert_eq!(stats.p99, Duration::from_millis(10));
|
|
||||||
assert_eq!(stats.worst, Duration::from_millis(10));
|
|
||||||
assert_eq!(stats.janky_percent, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn percentiles_and_worst_over_a_known_set() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
for ms in (1..=100).rev() {
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_millis(ms)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let stats = r.report().unwrap();
|
|
||||||
assert_eq!(stats.total_frames, 100);
|
|
||||||
assert_eq!(stats.p50, Duration::from_millis(51));
|
|
||||||
assert_eq!(stats.p90, Duration::from_millis(91));
|
|
||||||
assert_eq!(stats.p99, Duration::from_millis(100));
|
|
||||||
assert_eq!(stats.worst, Duration::from_millis(100));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn jank_threshold_matches_gfxinfos_60hz_budget() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_nanos(16_666_667)),
|
|
||||||
true,
|
|
||||||
); // exactly on budget: not janky
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_nanos(16_666_668)),
|
|
||||||
true,
|
|
||||||
); // one ns over: janky
|
|
||||||
let stats = r.report().unwrap();
|
|
||||||
assert_eq!(stats.janky_percent, 50.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn janky_percent_is_over_all_time_frames_not_just_the_ring() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
for _ in 0..10 {
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_millis(50)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert_eq!(r.report().unwrap().janky_percent, 100.0);
|
|
||||||
r.reset();
|
|
||||||
assert!(r.report().is_none());
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_millis(1)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
assert_eq!(r.report().unwrap().janky_percent, 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn record_without_a_split_reports_the_whole_frame_as_cpu() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_millis(20)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
let stats = r.report().unwrap();
|
|
||||||
assert_eq!(stats.cpu_p50, Duration::from_millis(20));
|
|
||||||
assert_eq!(stats.gpu_wait_p50, Duration::ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn each_part_reports_its_own_median_and_build_excludes_the_wait() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
for (acquire, submit) in [(5, 2), (10, 3), (20, 4)] {
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts {
|
|
||||||
total: Duration::from_millis(30),
|
|
||||||
acquire: Duration::from_millis(acquire),
|
|
||||||
submit: Duration::from_millis(submit),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let stats = r.report().unwrap();
|
|
||||||
assert_eq!(stats.p50, Duration::from_millis(30));
|
|
||||||
assert_eq!(stats.acquire_p50, Duration::from_millis(10));
|
|
||||||
assert_eq!(stats.gpu_wait_p50, Duration::from_millis(3));
|
|
||||||
assert_eq!(stats.cpu_p50, Duration::from_millis(17));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_gap_of_more_than_one_vsync_is_counted_as_a_missed_frame() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
let base = Instant::now();
|
|
||||||
let budget = Duration::from_nanos(16_666_667);
|
|
||||||
r.mark_phase("fling");
|
|
||||||
for step in [0u32, 1, 2, 4, 5, 8] {
|
|
||||||
r.record(
|
|
||||||
base + budget * step,
|
|
||||||
FrameParts::whole(Duration::from_millis(2)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let phase = r.phase_stats(Instant::now(), 60.0).remove(0);
|
|
||||||
assert_eq!(phase.late, 0, "no frame here was over its budget");
|
|
||||||
assert_eq!(phase.missed, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn an_idle_gap_is_not_a_missed_frame() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
let base = Instant::now();
|
|
||||||
let budget = Duration::from_nanos(16_666_667);
|
|
||||||
r.mark_phase("fling");
|
|
||||||
r.record(base, FrameParts::whole(Duration::ZERO), true);
|
|
||||||
r.record(base + budget, FrameParts::whole(Duration::ZERO), true);
|
|
||||||
r.record(base + budget * 2, FrameParts::whole(Duration::ZERO), false);
|
|
||||||
r.record(
|
|
||||||
base + Duration::from_millis(300),
|
|
||||||
FrameParts::whole(Duration::ZERO),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
let phase = r.phase_stats(Instant::now(), 60.0).remove(0);
|
|
||||||
assert_eq!(
|
|
||||||
phase.missed, 0,
|
|
||||||
"a rest nobody was waiting through is not a stutter"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_sustained_120hz_run_measures_120_whatever_the_platform_says() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
let base = Instant::now();
|
|
||||||
let period = Duration::from_nanos(8_333_333);
|
|
||||||
for step in 0..120u32 {
|
|
||||||
r.record(
|
|
||||||
base + period * step,
|
|
||||||
FrameParts::whole(Duration::ZERO),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let hz = r.sustained_frame_hz().expect("120 gaps is plenty");
|
|
||||||
assert!((hz - 120.0).abs() < 1.0, "measured {hz}Hz, expected ~120");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn an_app_that_cannot_keep_up_does_not_claim_a_faster_display() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
let base = Instant::now();
|
|
||||||
let mut at = base;
|
|
||||||
for step in 0..120u32 {
|
|
||||||
at += if step % 10 == 0 {
|
|
||||||
Duration::from_millis(8)
|
|
||||||
} else {
|
|
||||||
Duration::from_millis(20)
|
|
||||||
};
|
|
||||||
r.record(at, FrameParts::whole(Duration::ZERO), true);
|
|
||||||
}
|
|
||||||
let hz = r.sustained_frame_hz().expect("120 gaps is plenty");
|
|
||||||
assert!(
|
|
||||||
hz < 60.0,
|
|
||||||
"measured {hz}Hz, which claims more than was drawn"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_frame_held_back_by_the_display_is_not_late() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
let base = Instant::now();
|
|
||||||
let period = Duration::from_nanos(8_333_333);
|
|
||||||
r.mark_phase("fling");
|
|
||||||
for step in 0..30u32 {
|
|
||||||
r.record(
|
|
||||||
base + period * step,
|
|
||||||
FrameParts {
|
|
||||||
total: Duration::from_micros(8_300),
|
|
||||||
acquire: Duration::from_micros(7_900),
|
|
||||||
submit: Duration::from_micros(200),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let phase = r.phase_stats(Instant::now(), 120.0).remove(0);
|
|
||||||
assert_eq!(phase.late, 0);
|
|
||||||
assert_eq!(r.late_at_hz(120.0).0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_phase_does_not_inherit_the_pause_before_it() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
let base = Instant::now();
|
|
||||||
let budget = Duration::from_nanos(16_666_667);
|
|
||||||
r.mark_phase("fling");
|
|
||||||
for step in [0u32, 1, 2] {
|
|
||||||
r.record(
|
|
||||||
base + budget * step,
|
|
||||||
FrameParts::whole(Duration::ZERO),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let after = base + Duration::from_secs(1);
|
|
||||||
r.mark_phase("type");
|
|
||||||
for step in [0u32, 1, 2] {
|
|
||||||
r.record(
|
|
||||||
after + budget * step,
|
|
||||||
FrameParts::whole(Duration::ZERO),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let phases = r.phase_stats(Instant::now(), 60.0);
|
|
||||||
assert_eq!(phases[0].missed, 0);
|
|
||||||
assert_eq!(
|
|
||||||
phases[1].missed, 0,
|
|
||||||
"the rest between phases is not a stutter"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ring_wraps_without_growing_past_capacity() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
for i in 0..(RING_CAPACITY * 2) {
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_millis(1 + (i % 5) as u64)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let stats = r.report().unwrap();
|
|
||||||
assert_eq!(stats.total_frames, (RING_CAPACITY * 2) as u64);
|
|
||||||
assert!(stats.worst <= Duration::from_millis(5));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_marks_means_no_phases() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_millis(5)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
assert!(r.phase_stats(Instant::now(), 60.0).is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn phases_slice_frames_by_when_they_were_marked() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
r.mark_phase("a");
|
|
||||||
for _ in 0..5 {
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_millis(10)),
|
|
||||||
true,
|
|
||||||
); // 10ms: late at 60Hz (16.7ms budget)... no, 10<16.7, not late
|
|
||||||
}
|
|
||||||
r.mark_phase("b");
|
|
||||||
for _ in 0..3 {
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_millis(20)),
|
|
||||||
true,
|
|
||||||
); // 20ms: late at 60Hz
|
|
||||||
}
|
|
||||||
let now = Instant::now();
|
|
||||||
let phases = r.phase_stats(now, 60.0);
|
|
||||||
assert_eq!(phases.len(), 2);
|
|
||||||
assert_eq!(phases[0].name, "a");
|
|
||||||
assert_eq!(phases[0].frames, 5);
|
|
||||||
assert_eq!(phases[0].late, 0);
|
|
||||||
assert_eq!(phases[0].worst, Duration::from_millis(10));
|
|
||||||
assert_eq!(phases[1].name, "b");
|
|
||||||
assert_eq!(phases[1].frames, 3);
|
|
||||||
assert_eq!(phases[1].late, 3);
|
|
||||||
assert_eq!(phases[1].late_percent, 100.0);
|
|
||||||
assert_eq!(phases[1].worst, Duration::from_millis(20));
|
|
||||||
assert!(phases[0].complete);
|
|
||||||
assert!(phases[1].complete);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn the_last_phase_runs_until_now() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
r.mark_phase("only");
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_millis(1)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
std::thread::sleep(Duration::from_millis(20));
|
|
||||||
let now = Instant::now();
|
|
||||||
let phases = r.phase_stats(now, 60.0);
|
|
||||||
assert_eq!(phases.len(), 1);
|
|
||||||
assert!(phases[0].duration >= Duration::from_millis(20));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reset_clears_phase_marks() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
r.mark_phase("a");
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_millis(1)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
r.reset();
|
|
||||||
assert!(r.phase_stats(Instant::now(), 60.0).is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn late_at_hz_uses_the_given_refresh_rate_not_the_fixed_60hz_constant() {
|
|
||||||
let mut r = FrameReport::new();
|
|
||||||
r.record(
|
|
||||||
Instant::now(),
|
|
||||||
FrameParts::whole(Duration::from_millis(10)),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
assert_eq!(r.late_at_hz(60.0), (0, 0.0));
|
|
||||||
assert_eq!(r.late_at_hz(120.0), (1, 100.0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+289
-504
@@ -1,14 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Ui, UiData,
|
UiData, UiRenderState,
|
||||||
render::{
|
render::{data::PrimitiveInstance, util::ArrBuf},
|
||||||
data::{PrimitiveInstance, instance_slot_layout},
|
|
||||||
texture::GpuTextures,
|
|
||||||
util::ArrBuf,
|
|
||||||
},
|
|
||||||
util::{HashMap, Vec2},
|
util::{HashMap, Vec2},
|
||||||
};
|
};
|
||||||
use data::WindowUniform;
|
use data::WindowUniform;
|
||||||
use pollster::FutureExt;
|
|
||||||
use wgpu::{
|
use wgpu::{
|
||||||
util::{BufferInitDescriptor, DeviceExt},
|
util::{BufferInitDescriptor, DeviceExt},
|
||||||
*,
|
*,
|
||||||
@@ -16,294 +11,164 @@ use wgpu::{
|
|||||||
|
|
||||||
mod atlas;
|
mod atlas;
|
||||||
mod data;
|
mod data;
|
||||||
mod frame_report;
|
mod page;
|
||||||
mod primitive;
|
mod primitive;
|
||||||
mod sdf;
|
|
||||||
mod texture;
|
mod texture;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
pub use atlas::*;
|
pub use atlas::*;
|
||||||
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset};
|
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset};
|
||||||
pub use frame_report::{FrameParts, FrameReport, FrameStats, JANK_THRESHOLD};
|
|
||||||
pub use primitive::*;
|
pub use primitive::*;
|
||||||
pub use sdf::{distance_from_rect, rounded_rect_coverage};
|
|
||||||
|
|
||||||
pub const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
const PRELUDE: &str = include_str!("./shader/prelude.wgsl");
|
||||||
|
|
||||||
/// The advertised swapchain format and the sRGB view Iris renders through.
|
fn module_source(wgsl: &str) -> String {
|
||||||
/// A backend may advertise only the non-sRGB member of an RGBA/BGRA pair;
|
// The steps come from the same constants the CPU counts in, rather than
|
||||||
/// wgpu permits its sRGB counterpart as a configured view format.
|
// a second copy of them written into the shader: a grid the two disagree
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
// about puts every coordinate somewhere else.
|
||||||
pub struct SurfaceFormat {
|
format!(
|
||||||
pub surface: TextureFormat,
|
"const PX_STEP: f32 = 1.0 / {}.0;\nconst REL_STEP: f32 = 1.0 / {}.0;\n{PRELUDE}\n{wgsl}",
|
||||||
pub view: TextureFormat,
|
1u32 << crate::PX_SHIFT,
|
||||||
}
|
1u32 << crate::REL_SHIFT,
|
||||||
|
)
|
||||||
pub fn srgb_surface_format(caps: &SurfaceCapabilities) -> Result<SurfaceFormat, String> {
|
|
||||||
let supports_srgb_space = |format| caps.color_spaces(format).contains(SurfaceColorSpaces::SRGB);
|
|
||||||
if let Some(surface) = caps
|
|
||||||
.formats
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.find(|format| format.is_srgb() && supports_srgb_space(*format))
|
|
||||||
{
|
|
||||||
return Ok(SurfaceFormat {
|
|
||||||
surface,
|
|
||||||
view: surface,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Some(surface) = caps
|
|
||||||
.formats
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.find(|format| format.add_srgb_suffix().is_srgb() && supports_srgb_space(*format))
|
|
||||||
{
|
|
||||||
return Ok(SurfaceFormat {
|
|
||||||
surface,
|
|
||||||
view: surface.add_srgb_suffix(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(format!(
|
|
||||||
"the surface has no RGBA/BGRA format with an sRGB render view and sRGB output colour \
|
|
||||||
space; advertised default formats: {:?}",
|
|
||||||
caps.formats
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn device_limits() -> Limits {
|
|
||||||
Limits {
|
|
||||||
max_buffer_size: 1 << 30,
|
|
||||||
max_compute_workgroup_storage_size: 0,
|
|
||||||
max_compute_invocations_per_workgroup: 0,
|
|
||||||
max_compute_workgroup_size_x: 0,
|
|
||||||
max_compute_workgroup_size_y: 0,
|
|
||||||
max_compute_workgroup_size_z: 0,
|
|
||||||
max_compute_workgroups_per_dimension: 0,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct WgpuErrorLog {
|
|
||||||
errors: std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<String>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
const WGPU_ERROR_LOG_CAP: usize = 20;
|
|
||||||
|
|
||||||
impl Default for WgpuErrorLog {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
errors: std::sync::Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WgpuErrorLog {
|
|
||||||
pub fn record(&self, error: impl std::fmt::Display) {
|
|
||||||
let mut errors = self.errors.lock().unwrap();
|
|
||||||
if errors.len() >= WGPU_ERROR_LOG_CAP {
|
|
||||||
errors.pop_front();
|
|
||||||
}
|
|
||||||
errors.push_back(error.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A snapshot for the Diagnostics page -- cloned rather than held,
|
|
||||||
/// since the lock must not outlive one call.
|
|
||||||
pub fn snapshot(&self) -> Vec<String> {
|
|
||||||
self.errors.lock().unwrap().iter().cloned().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.errors.lock().unwrap().len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.errors.lock().unwrap().is_empty()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UiRenderNode {
|
pub struct UiRenderNode {
|
||||||
uniform_group: BindGroup,
|
shared_layout: BindGroupLayout,
|
||||||
primitive_layout: BindGroupLayout,
|
shared_group: BindGroup,
|
||||||
primitives: PrimitiveBuffers,
|
format: TextureFormat,
|
||||||
primitive_group: BindGroup,
|
|
||||||
rsc_layout: BindGroupLayout,
|
|
||||||
rsc_group: BindGroup,
|
|
||||||
|
|
||||||
pipeline: RenderPipeline,
|
/// One per registered primitive, in id order.
|
||||||
|
primitives: Vec<PrimitivePipeline>,
|
||||||
|
|
||||||
layers: HashMap<usize, RenderLayer>,
|
layers: HashMap<usize, RenderLayer>,
|
||||||
active: Vec<usize>,
|
active: Vec<usize>,
|
||||||
window_buffer: Buffer,
|
window_buffer: Buffer,
|
||||||
textures: GpuTextures,
|
|
||||||
instances: ArrBuf<PrimitiveInstance>,
|
|
||||||
masks: ArrBuf<Mask>,
|
masks: ArrBuf<Mask>,
|
||||||
move_offsets: ArrBuf<MoveOffset>,
|
moves: ArrBuf<MoveOffset>,
|
||||||
paints: ArrBuf<crate::LinearRgba>,
|
|
||||||
masks_layout: BindGroupLayout,
|
|
||||||
masks_group: BindGroup,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RenderLayer {
|
struct RenderLayer {
|
||||||
order: ArrBuf<u32>,
|
/// One per registered primitive, `None` where this layer draws none.
|
||||||
/// A standalone image's slots, kept apart from `order` because each
|
primitives: Vec<Option<ListBuffers>>,
|
||||||
/// one draws with its own bind group -- see `UiRenderNode::draw`.
|
}
|
||||||
images: ArrBuf<u32>,
|
|
||||||
/// The texture slot each entry of `images` draws with, in the same
|
/// What draws one registered primitive.
|
||||||
/// order, refreshed alongside it. Not in the vertex buffer itself
|
struct PrimitivePipeline {
|
||||||
/// because it names a bind group, not shader data.
|
data_layout: BindGroupLayout,
|
||||||
image_tex_indices: Vec<u32>,
|
pipeline: RenderPipeline,
|
||||||
|
render: Box<dyn PrimitiveRender>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One list's vertex buffer and the data its shader reads.
|
||||||
|
struct ListBuffers {
|
||||||
|
instance: ArrBuf<PrimitiveInstance>,
|
||||||
|
data: ArrBuf<u8>,
|
||||||
|
group: Option<BindGroup>,
|
||||||
|
/// What the primitive asked to keep per instance, if anything.
|
||||||
|
bindings: Vec<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UiRenderNode {
|
impl UiRenderNode {
|
||||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||||
pass.set_pipeline(&self.pipeline);
|
pass.set_bind_group(0, &self.shared_group, &[]);
|
||||||
pass.set_bind_group(0, &self.uniform_group, &[]);
|
|
||||||
pass.set_bind_group(1, &self.primitive_group, &[]);
|
|
||||||
pass.set_bind_group(3, &self.masks_group, &[]);
|
|
||||||
for i in &self.active {
|
for i in &self.active {
|
||||||
let layer = &self.layers[i];
|
let layer = &self.layers[i];
|
||||||
if layer.order.len() == 0 && layer.images.len() == 0 {
|
for (id, list) in layer.primitives.iter().enumerate() {
|
||||||
continue;
|
let Some(list) = list else { continue };
|
||||||
}
|
let Some(group) = &list.group else { continue };
|
||||||
if layer.order.len() > 0 {
|
let primitive = &self.primitives[id];
|
||||||
pass.set_bind_group(2, &self.rsc_group, &[]);
|
pass.set_pipeline(&primitive.pipeline);
|
||||||
pass.set_vertex_buffer(0, layer.order.buffer.slice(..));
|
pass.set_bind_group(1, group, &[]);
|
||||||
pass.draw(0..4, 0..layer.order.len() as u32);
|
pass.set_vertex_buffer(0, list.instance.buffer.slice(..));
|
||||||
}
|
primitive.render.draw(
|
||||||
// Images draw after this layer's rects and glyphs, one draw call
|
pass,
|
||||||
// each with its own bind group. That draws every image "on top"
|
ListDraw {
|
||||||
// within the layer, which loses nothing that currently exists:
|
instances: list.instance.len() as u32,
|
||||||
// `Primitives::apply_free` frees with `swap_remove`, so a layer's
|
bindings: &list.bindings,
|
||||||
// draw order was already undefined before images had their own
|
},
|
||||||
// list -- nothing before this relied on interleaving a rect
|
);
|
||||||
// between two images at a particular position.
|
|
||||||
if layer.images.len() > 0 {
|
|
||||||
pass.set_vertex_buffer(0, layer.images.buffer.slice(..));
|
|
||||||
for (k, &tex_idx) in layer.image_tex_indices.iter().enumerate() {
|
|
||||||
pass.set_bind_group(2, self.textures.image_bind_group(tex_idx), &[]);
|
|
||||||
pass.draw(0..4, k as u32..k as u32 + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, device: &Device, queue: &Queue, ui: &mut Ui) -> FrameUpdateStats {
|
pub fn update(
|
||||||
let render_handle = ui.render_state.clone();
|
&mut self,
|
||||||
let mut render_guard = render_handle.get_mut();
|
device: &Device,
|
||||||
let ui_render = &mut *render_guard;
|
queue: &Queue,
|
||||||
let ui_data: &mut UiData = ui;
|
ui: &mut UiData,
|
||||||
|
ui_render: &mut UiRenderState,
|
||||||
|
) {
|
||||||
|
// Before the layers: each list is given its pipeline's data layout.
|
||||||
|
self.build_pipelines(device, queue, &ui.primitives);
|
||||||
self.active.clear();
|
self.active.clear();
|
||||||
for (i, order) in ui_render.layers.iter_mut() {
|
for (i, draws) in ui_render.layers.iter_mut() {
|
||||||
self.active.push(i);
|
self.active.push(i);
|
||||||
let rlayer = self.layers.entry(i).or_insert_with(|| RenderLayer {
|
for change in draws.apply_free() {
|
||||||
order: ArrBuf::new(
|
if let Some(inst) = ui_render.active.get_mut(&change.id) {
|
||||||
device,
|
for primitive in &mut inst.primitives {
|
||||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
let h = &mut primitive.handle;
|
||||||
"layer order",
|
if h.layer == i && h.kind == change.kind && h.inst_idx == change.old {
|
||||||
),
|
h.inst_idx = change.new;
|
||||||
images: ArrBuf::new(
|
break;
|
||||||
device,
|
|
||||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
|
||||||
"layer image order",
|
|
||||||
),
|
|
||||||
image_tex_indices: Vec::new(),
|
|
||||||
});
|
|
||||||
if order.updated {
|
|
||||||
let (entries, dirty) = order.order_for_upload();
|
|
||||||
rlayer.order.update(device, queue, entries, dirty);
|
|
||||||
let (entries, dirty) = order.images_for_upload();
|
|
||||||
rlayer.images.update(device, queue, entries, dirty);
|
|
||||||
rlayer.image_tex_indices.clear();
|
|
||||||
rlayer.image_tex_indices.extend(
|
|
||||||
order
|
|
||||||
.images()
|
|
||||||
.iter()
|
|
||||||
.map(|&slot| ui_render.primitives.instance(slot).idx),
|
|
||||||
);
|
|
||||||
order.updated = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let instances_resized = if ui_render.primitives.needs_upload() {
|
}
|
||||||
let (entries, dirty) = ui_render.primitives.instances_for_upload();
|
}
|
||||||
let resized = self.instances.update(device, queue, entries, dirty);
|
let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new);
|
||||||
if self
|
if draws.updated {
|
||||||
|
let lists = draws.primitives();
|
||||||
|
// The zip would otherwise skip a list with no pipeline.
|
||||||
|
assert!(lists.len() <= self.primitives.len());
|
||||||
|
rlayer.primitives.resize_with(lists.len(), || None);
|
||||||
|
for ((buffers, list), primitive) in rlayer
|
||||||
.primitives
|
.primitives
|
||||||
.update(device, queue, ui_render.primitives.data_mut())
|
.iter_mut()
|
||||||
|
.zip(lists)
|
||||||
|
.zip(&self.primitives)
|
||||||
{
|
{
|
||||||
self.primitive_group = Self::primitive_group(
|
let Some(list) = list else {
|
||||||
device,
|
continue;
|
||||||
&self.primitive_layout,
|
|
||||||
self.primitives.buffers(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
resized
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
};
|
||||||
let (entries, dirty) = ui_data.masks.for_upload();
|
buffers
|
||||||
let masks_resized = self.masks.update(device, queue, entries, dirty);
|
.get_or_insert_with(|| ListBuffers::new(device))
|
||||||
let (entries, dirty) = ui_data.move_offsets.for_upload();
|
.update(device, queue, primitive, list);
|
||||||
let moves_resized = self.move_offsets.update(device, queue, entries, dirty);
|
}
|
||||||
let (entries, dirty) = ui_data.paints.for_upload();
|
draws.updated = false;
|
||||||
let paints_resized = self.paints.update(device, queue, entries, dirty);
|
}
|
||||||
if masks_resized || moves_resized || instances_resized || paints_resized {
|
}
|
||||||
self.masks_group = Self::masks_group(
|
for primitive in &mut self.primitives {
|
||||||
|
primitive.render.update(ui);
|
||||||
|
}
|
||||||
|
let mut regroup = false;
|
||||||
|
if ui.masks.changed {
|
||||||
|
ui.masks.changed = false;
|
||||||
|
regroup |= self.masks.update(device, queue, &ui.masks[..]);
|
||||||
|
}
|
||||||
|
if ui_render.moves.changed {
|
||||||
|
ui_render.moves.changed = false;
|
||||||
|
regroup |= self.moves.update(device, queue, ui_render.moves.entries());
|
||||||
|
}
|
||||||
|
if regroup {
|
||||||
|
self.shared_group = Self::shared_group(
|
||||||
device,
|
device,
|
||||||
&self.masks_layout,
|
&self.shared_layout,
|
||||||
|
&self.window_buffer,
|
||||||
&self.masks,
|
&self.masks,
|
||||||
&self.move_offsets,
|
&self.moves,
|
||||||
&self.instances,
|
|
||||||
&self.paints,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let rebuild_main = self
|
|
||||||
.textures
|
|
||||||
.update(&mut ui_data.textures, &self.rsc_layout);
|
|
||||||
if rebuild_main {
|
|
||||||
self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures);
|
|
||||||
}
|
|
||||||
FrameUpdateStats {
|
|
||||||
masks_resized,
|
|
||||||
moves_resized,
|
|
||||||
paints_resized,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes a size rather than a window type: this is the only thing the
|
|
||||||
/// core wanted from winit, and depending on a windowing backend for two
|
|
||||||
/// numbers is what put `android-activity` in the core's graph for an
|
|
||||||
/// Android build that is meant to go through android-view instead.
|
|
||||||
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
pub fn resize(&mut self, size: impl Into<Vec2>, queue: &Queue) {
|
||||||
let size = size.into();
|
let size = size.into();
|
||||||
let slice = &[WindowUniform {
|
let slice = &[WindowUniform { dim: size }];
|
||||||
width: size.x,
|
|
||||||
height: size.y,
|
|
||||||
}];
|
|
||||||
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self {
|
||||||
device: &Device,
|
let window_uniform = WindowUniform {
|
||||||
queue: &Queue,
|
dim: Vec2::new(config.width as f32, config.height as f32),
|
||||||
target_format: TextureFormat,
|
|
||||||
window_size: impl Into<Vec2>,
|
|
||||||
) -> Result<Self, String> {
|
|
||||||
let oom_scope = device.push_error_scope(ErrorFilter::OutOfMemory);
|
|
||||||
let validation_scope = device.push_error_scope(ErrorFilter::Validation);
|
|
||||||
let internal_scope = device.push_error_scope(ErrorFilter::Internal);
|
|
||||||
|
|
||||||
let shader = device.create_shader_module(ShaderModuleDescriptor {
|
|
||||||
label: Some("UI Shape Shader"),
|
|
||||||
source: ShaderSource::Wgsl(SHAPE_SHADER.into()),
|
|
||||||
});
|
|
||||||
|
|
||||||
let window_uniform = {
|
|
||||||
let size = window_size.into();
|
|
||||||
WindowUniform {
|
|
||||||
width: size.x,
|
|
||||||
height: size.y,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
let window_buffer = device.create_buffer_init(&BufferInitDescriptor {
|
||||||
label: Some("window"),
|
label: Some("window"),
|
||||||
@@ -311,97 +176,80 @@ impl UiRenderNode {
|
|||||||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||||||
});
|
});
|
||||||
|
|
||||||
let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
let shared_layout = Self::shared_layout(device);
|
||||||
entries: &[BindGroupLayoutEntry {
|
|
||||||
binding: 0,
|
|
||||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Buffer {
|
|
||||||
ty: BufferBindingType::Uniform,
|
|
||||||
has_dynamic_offset: false,
|
|
||||||
min_binding_size: None,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
}],
|
|
||||||
label: Some("window"),
|
|
||||||
});
|
|
||||||
|
|
||||||
let uniform_group = Self::bind_group_0(device, &uniform_layout, &window_buffer);
|
|
||||||
|
|
||||||
let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
|
||||||
entries: &PrimitiveBuffers::BINDINGS.map(|binding| BindGroupLayoutEntry {
|
|
||||||
binding,
|
|
||||||
visibility: ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Buffer {
|
|
||||||
ty: BufferBindingType::Storage { read_only: true },
|
|
||||||
has_dynamic_offset: false,
|
|
||||||
min_binding_size: None,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
}),
|
|
||||||
label: Some("primitive"),
|
|
||||||
});
|
|
||||||
|
|
||||||
let tex_manager = GpuTextures::new(device, queue);
|
|
||||||
let primitives = PrimitiveBuffers::new(device);
|
|
||||||
let primitive_group =
|
|
||||||
Self::primitive_group(device, &primitive_layout, primitives.buffers());
|
|
||||||
let instances = ArrBuf::new(
|
|
||||||
device,
|
|
||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
|
||||||
"ui instances",
|
|
||||||
);
|
|
||||||
let masks = ArrBuf::new(
|
let masks = ArrBuf::new(
|
||||||
device,
|
device,
|
||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||||
"ui masks",
|
"ui masks",
|
||||||
);
|
);
|
||||||
let move_offsets = ArrBuf::new(
|
let moves = ArrBuf::new(
|
||||||
device,
|
device,
|
||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||||
"ui move offsets",
|
"ui move offsets",
|
||||||
);
|
);
|
||||||
let paints = ArrBuf::new(
|
let shared_group =
|
||||||
device,
|
Self::shared_group(device, &shared_layout, &window_buffer, &masks, &moves);
|
||||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
|
||||||
"ui paints",
|
|
||||||
);
|
|
||||||
|
|
||||||
let rsc_layout = Self::rsc_layout(device);
|
Self {
|
||||||
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager);
|
shared_layout,
|
||||||
let masks_layout = Self::masks_layout(device);
|
shared_group,
|
||||||
let masks_group = Self::masks_group(
|
format: config.format,
|
||||||
device,
|
primitives: Vec::new(),
|
||||||
&masks_layout,
|
window_buffer,
|
||||||
&masks,
|
layers: HashMap::default(),
|
||||||
&move_offsets,
|
active: Vec::new(),
|
||||||
&instances,
|
masks,
|
||||||
&paints,
|
moves,
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
/// Compiles a pipeline for every primitive registered since the last call.
|
||||||
label: Some("UI Shape Pipeline Layout"),
|
/// Sources only ever arrive at the end, so an id keeps its pipeline.
|
||||||
bind_group_layouts: &[
|
fn build_pipelines(&mut self, device: &Device, queue: &Queue, registry: &PrimitiveRegistry) {
|
||||||
Some(&uniform_layout),
|
for source in ®istry.sources()[self.primitives.len()..] {
|
||||||
Some(&primitive_layout),
|
let render = (source.render)(device, queue);
|
||||||
Some(&rsc_layout),
|
let data_layout = Self::data_layout(device, source.stride);
|
||||||
Some(&masks_layout),
|
let mut groups = vec![Some(&self.shared_layout), Some(&data_layout)];
|
||||||
],
|
groups.extend(render.layout().map(Some));
|
||||||
|
let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||||
|
label: Some(source.label),
|
||||||
|
bind_group_layouts: &groups,
|
||||||
immediate_size: 0,
|
immediate_size: 0,
|
||||||
});
|
});
|
||||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label);
|
||||||
label: Some("UI Shape Pipeline"),
|
self.primitives.push(PrimitivePipeline {
|
||||||
layout: Some(&pipeline_layout),
|
data_layout,
|
||||||
|
pipeline,
|
||||||
|
render,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pipeline(
|
||||||
|
device: &Device,
|
||||||
|
layout: &PipelineLayout,
|
||||||
|
format: TextureFormat,
|
||||||
|
wgsl: &str,
|
||||||
|
label: &str,
|
||||||
|
) -> RenderPipeline {
|
||||||
|
let module = device.create_shader_module(ShaderModuleDescriptor {
|
||||||
|
label: Some(label),
|
||||||
|
source: ShaderSource::Wgsl(module_source(wgsl).into()),
|
||||||
|
});
|
||||||
|
device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||||
|
label: Some(label),
|
||||||
|
layout: Some(layout),
|
||||||
vertex: VertexState {
|
vertex: VertexState {
|
||||||
module: &shader,
|
module: &module,
|
||||||
entry_point: Some("vs_main"),
|
entry_point: Some("vs_main"),
|
||||||
buffers: &[Some(instance_slot_layout())],
|
buffers: &[Some(PrimitiveInstance::desc())],
|
||||||
compilation_options: Default::default(),
|
compilation_options: Default::default(),
|
||||||
},
|
},
|
||||||
fragment: Some(FragmentState {
|
fragment: Some(FragmentState {
|
||||||
module: &shader,
|
module: &module,
|
||||||
entry_point: Some("fs_main"),
|
entry_point: Some("fs_main"),
|
||||||
targets: &[Some(ColorTargetState {
|
targets: &[Some(ColorTargetState {
|
||||||
format: target_format,
|
format,
|
||||||
blend: Some(BlendState::ALPHA_BLENDING),
|
blend: Some(BlendState::ALPHA_BLENDING),
|
||||||
write_mask: ColorWrites::ALL,
|
write_mask: ColorWrites::ALL,
|
||||||
})],
|
})],
|
||||||
@@ -424,158 +272,31 @@ impl UiRenderNode {
|
|||||||
},
|
},
|
||||||
multiview_mask: None,
|
multiview_mask: None,
|
||||||
cache: None,
|
cache: None,
|
||||||
});
|
|
||||||
|
|
||||||
// Reverse of the push order above. Only one of these should ever be
|
|
||||||
// `Some` in practice -- three separate scopes exist to name *which*
|
|
||||||
// kind of error it was, not because more than one is expected at
|
|
||||||
// once.
|
|
||||||
let internal_err = internal_scope.pop().block_on();
|
|
||||||
let validation_err = validation_scope.pop().block_on();
|
|
||||||
let oom_err = oom_scope.pop().block_on();
|
|
||||||
if let Some(err) = validation_err.or(oom_err).or(internal_err) {
|
|
||||||
return Err(err.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
uniform_group,
|
|
||||||
primitive_layout,
|
|
||||||
primitives,
|
|
||||||
primitive_group,
|
|
||||||
rsc_layout,
|
|
||||||
rsc_group,
|
|
||||||
pipeline,
|
|
||||||
window_buffer,
|
|
||||||
layers: HashMap::default(),
|
|
||||||
active: Vec::new(),
|
|
||||||
textures: tex_manager,
|
|
||||||
instances,
|
|
||||||
masks,
|
|
||||||
move_offsets,
|
|
||||||
paints,
|
|
||||||
masks_layout,
|
|
||||||
masks_group,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bind_group_0(
|
/// What every draw in the ui is given: the window, the masks and the
|
||||||
device: &Device,
|
/// move chain every position is resolved through.
|
||||||
layout: &BindGroupLayout,
|
fn shared_layout(device: &Device) -> BindGroupLayout {
|
||||||
window_buffer: &Buffer,
|
|
||||||
) -> BindGroup {
|
|
||||||
device.create_bind_group(&BindGroupDescriptor {
|
|
||||||
layout,
|
|
||||||
entries: &[BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: window_buffer.as_entire_binding(),
|
|
||||||
}],
|
|
||||||
label: Some("ui window"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn primitive_group(
|
|
||||||
device: &Device,
|
|
||||||
layout: &BindGroupLayout,
|
|
||||||
buffers: [(u32, &Buffer); PrimitiveBuffers::LEN],
|
|
||||||
) -> BindGroup {
|
|
||||||
device.create_bind_group(&BindGroupDescriptor {
|
|
||||||
layout,
|
|
||||||
entries: &buffers.map(|(binding, buf)| BindGroupEntry {
|
|
||||||
binding,
|
|
||||||
resource: buf.as_entire_binding(),
|
|
||||||
}),
|
|
||||||
label: Some("ui primitives"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Group 2: the shared atlas array and one standalone-image slot (a null
|
|
||||||
/// view for the main draw, a real one for each image's own bind group --
|
|
||||||
/// see `GpuTextures`), plus one sampler. No `count` on any entry: this
|
|
||||||
/// needs nothing beyond plain Vulkan 1.0 / GLES sampling, unlike the
|
|
||||||
/// `binding_array` layout it replaced (see TEXTURES.md's "Recommended
|
|
||||||
/// shape"). Masks and move_offsets are deliberately *not* here -- see
|
|
||||||
/// `masks_layout` below for why they get their own group.
|
|
||||||
fn rsc_layout(device: &Device) -> BindGroupLayout {
|
|
||||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||||
entries: &[
|
entries: &[
|
||||||
BindGroupLayoutEntry {
|
BindGroupLayoutEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
visibility: ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Texture {
|
|
||||||
sample_type: TextureSampleType::Float { filterable: false },
|
|
||||||
view_dimension: TextureViewDimension::D2Array,
|
|
||||||
multisampled: false,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
BindGroupLayoutEntry {
|
|
||||||
binding: 1,
|
|
||||||
visibility: ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Texture {
|
|
||||||
sample_type: TextureSampleType::Float { filterable: false },
|
|
||||||
view_dimension: TextureViewDimension::D2,
|
|
||||||
multisampled: false,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
BindGroupLayoutEntry {
|
|
||||||
binding: 2,
|
|
||||||
visibility: ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
label: Some("ui rsc"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The main group: rects and glyphs never sample the image slot, so it
|
|
||||||
/// gets a 1x1 null view rather than any live standalone image's.
|
|
||||||
fn rsc_group(
|
|
||||||
device: &Device,
|
|
||||||
layout: &BindGroupLayout,
|
|
||||||
tex_manager: &GpuTextures,
|
|
||||||
) -> BindGroup {
|
|
||||||
device.create_bind_group(&BindGroupDescriptor {
|
|
||||||
layout,
|
|
||||||
entries: &[
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: BindingResource::TextureView(tex_manager.array_view()),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: BindingResource::TextureView(tex_manager.null_view()),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 2,
|
|
||||||
resource: BindingResource::Sampler(tex_manager.sampler()),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
label: Some("ui rsc"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn masks_layout(device: &Device) -> BindGroupLayout {
|
|
||||||
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
|
||||||
entries: &[
|
|
||||||
BindGroupLayoutEntry {
|
|
||||||
binding: 0,
|
|
||||||
visibility: ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Buffer {
|
|
||||||
ty: BufferBindingType::Storage { read_only: true },
|
|
||||||
has_dynamic_offset: false,
|
|
||||||
min_binding_size: None,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
BindGroupLayoutEntry {
|
|
||||||
binding: 1,
|
|
||||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
||||||
|
ty: BindingType::Buffer {
|
||||||
|
ty: BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: BufferSize::new(size_of::<WindowUniform>() as u64),
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: ShaderStages::FRAGMENT,
|
||||||
ty: BindingType::Buffer {
|
ty: BindingType::Buffer {
|
||||||
ty: BufferBindingType::Storage { read_only: true },
|
ty: BufferBindingType::Storage { read_only: true },
|
||||||
has_dynamic_offset: false,
|
has_dynamic_offset: false,
|
||||||
min_binding_size: None,
|
min_binding_size: BufferSize::new(size_of::<Mask>() as u64),
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
},
|
},
|
||||||
@@ -585,78 +306,142 @@ impl UiRenderNode {
|
|||||||
ty: BindingType::Buffer {
|
ty: BindingType::Buffer {
|
||||||
ty: BufferBindingType::Storage { read_only: true },
|
ty: BufferBindingType::Storage { read_only: true },
|
||||||
has_dynamic_offset: false,
|
has_dynamic_offset: false,
|
||||||
min_binding_size: None,
|
min_binding_size: BufferSize::new(size_of::<MoveOffset>() as u64),
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
|
||||||
BindGroupLayoutEntry {
|
|
||||||
binding: 3,
|
|
||||||
visibility: ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Buffer {
|
|
||||||
ty: BufferBindingType::Storage { read_only: true },
|
|
||||||
has_dynamic_offset: false,
|
|
||||||
min_binding_size: None,
|
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
label: Some("ui masks"),
|
label: Some("ui shared"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn masks_group(
|
fn shared_group(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
layout: &BindGroupLayout,
|
layout: &BindGroupLayout,
|
||||||
|
window: &Buffer,
|
||||||
masks: &ArrBuf<Mask>,
|
masks: &ArrBuf<Mask>,
|
||||||
move_offsets: &ArrBuf<MoveOffset>,
|
moves: &ArrBuf<MoveOffset>,
|
||||||
instances: &ArrBuf<PrimitiveInstance>,
|
|
||||||
paints: &ArrBuf<crate::LinearRgba>,
|
|
||||||
) -> BindGroup {
|
) -> BindGroup {
|
||||||
device.create_bind_group(&BindGroupDescriptor {
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
layout,
|
layout,
|
||||||
entries: &[
|
entries: &[
|
||||||
BindGroupEntry {
|
BindGroupEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
|
resource: window.as_entire_binding(),
|
||||||
|
},
|
||||||
|
BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
resource: masks.buffer.as_entire_binding(),
|
resource: masks.buffer.as_entire_binding(),
|
||||||
},
|
},
|
||||||
BindGroupEntry {
|
|
||||||
binding: 1,
|
|
||||||
resource: move_offsets.buffer.as_entire_binding(),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
BindGroupEntry {
|
||||||
binding: 2,
|
binding: 2,
|
||||||
resource: instances.buffer.as_entire_binding(),
|
resource: moves.buffer.as_entire_binding(),
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 3,
|
|
||||||
resource: paints.buffer.as_entire_binding(),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
label: Some("ui masks"),
|
label: Some("ui shared"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view_count(&self) -> usize {
|
/// Layout for a list of one primitive's data. Every size in the ui is
|
||||||
self.textures.view_count()
|
/// stated, so "is the buffer big enough for one entry?" is answered when
|
||||||
}
|
/// the bind group is made; a `None` size is wgpu's to check on every draw.
|
||||||
|
fn data_layout(device: &Device, stride: u64) -> BindGroupLayout {
|
||||||
pub fn take_image_bind_group_creates(&mut self) -> u64 {
|
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||||
self.textures.take_bind_group_creates()
|
entries: &[BindGroupLayoutEntry {
|
||||||
}
|
binding: 0,
|
||||||
|
visibility: ShaderStages::FRAGMENT,
|
||||||
pub fn take_atlas_pages_grown(&mut self) -> u64 {
|
ty: BindingType::Buffer {
|
||||||
self.textures.take_pages_grown()
|
ty: BufferBindingType::Storage { read_only: true },
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: BufferSize::new(stride),
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
}],
|
||||||
|
label: Some("ui primitive data"),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What `UiRenderNode::update` changed this frame that a caller building a
|
impl RenderLayer {
|
||||||
/// per-frame diagnostic report cares about -- see `take_image_bind_group_creates`/
|
fn new() -> Self {
|
||||||
/// `take_atlas_pages_grown` for the two counters this doesn't carry (they
|
Self {
|
||||||
/// use the existing "call before update()" convention instead, so as not
|
primitives: Vec::new(),
|
||||||
/// to disturb `bench_images`' documented counts).
|
}
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
}
|
||||||
pub struct FrameUpdateStats {
|
}
|
||||||
pub masks_resized: bool,
|
|
||||||
pub moves_resized: bool,
|
impl ListBuffers {
|
||||||
pub paints_resized: bool,
|
fn new(device: &Device) -> Self {
|
||||||
|
Self {
|
||||||
|
instance: ArrBuf::new(
|
||||||
|
device,
|
||||||
|
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
||||||
|
"instance",
|
||||||
|
),
|
||||||
|
data: ArrBuf::new(
|
||||||
|
device,
|
||||||
|
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||||
|
"primitive data",
|
||||||
|
),
|
||||||
|
group: None,
|
||||||
|
bindings: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
device: &Device,
|
||||||
|
queue: &Queue,
|
||||||
|
primitive: &PrimitivePipeline,
|
||||||
|
list: &InstanceList,
|
||||||
|
) {
|
||||||
|
self.bindings.clear();
|
||||||
|
primitive.render.instance_bindings(list, &mut self.bindings);
|
||||||
|
self.instance.update(device, queue, list.instances());
|
||||||
|
let resized = self.data.update(device, queue, list.data());
|
||||||
|
if list.instances().is_empty() {
|
||||||
|
self.group = None;
|
||||||
|
} else if resized || self.group.is_none() {
|
||||||
|
self.group = Some(device.create_bind_group(&BindGroupDescriptor {
|
||||||
|
layout: &primitive.data_layout,
|
||||||
|
entries: &[BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: self.data.buffer.as_entire_binding(),
|
||||||
|
}],
|
||||||
|
label: Some("ui primitive data"),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::module_source;
|
||||||
|
use wgpu::naga::{
|
||||||
|
front::wgsl,
|
||||||
|
valid::{Capabilities, ValidationFlags, Validator},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Every shader file, composed as the renderer composes it, parses and
|
||||||
|
/// validates with no device -- so an edit that breaks one fails here and
|
||||||
|
/// not in the first window opened.
|
||||||
|
#[test]
|
||||||
|
fn every_shader_validates() {
|
||||||
|
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src/render/shader");
|
||||||
|
let mut checked = 0;
|
||||||
|
for entry in std::fs::read_dir(dir).unwrap() {
|
||||||
|
let path = entry.unwrap().path();
|
||||||
|
if path.extension().is_none_or(|e| e != "wgsl") || path.ends_with("prelude.wgsl") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let source = module_source(&std::fs::read_to_string(&path).unwrap());
|
||||||
|
let module = wgsl::parse_str(&source)
|
||||||
|
.unwrap_or_else(|e| panic!("{}: {}", path.display(), e.emit_to_string(&source)));
|
||||||
|
Validator::new(ValidationFlags::all(), Capabilities::all())
|
||||||
|
.validate(&module)
|
||||||
|
.unwrap_or_else(|e| panic!("{}: {e:?}", path.display()));
|
||||||
|
checked += 1;
|
||||||
|
}
|
||||||
|
assert!(checked > 0, "no shaders found in {dir}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
use wgpu::*;
|
||||||
|
|
||||||
|
use crate::{GlyphAtlas, UiData};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
atlas::PAGE,
|
||||||
|
primitive::{ListDraw, PrimitiveRender},
|
||||||
|
texture::{default_sampler, sampled_group, sampled_layout, write_region},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Draws glyphs from the atlas, which it owns: one array texture bound once
|
||||||
|
/// for a whole list, since every glyph in it reads the same pages.
|
||||||
|
pub struct GlyphRender {
|
||||||
|
pages: GpuPages,
|
||||||
|
layout: BindGroupLayout,
|
||||||
|
sampler: Sampler,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlyphRender {
|
||||||
|
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||||
|
let layout = sampled_layout(device, TextureViewDimension::D2Array, "ui atlas");
|
||||||
|
let sampler = default_sampler(device);
|
||||||
|
Self {
|
||||||
|
pages: GpuPages::new(device, queue, &layout, &sampler),
|
||||||
|
layout,
|
||||||
|
sampler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrimitiveRender for GlyphRender {
|
||||||
|
fn layout(&self) -> Option<&BindGroupLayout> {
|
||||||
|
Some(&self.layout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ui: &mut UiData) {
|
||||||
|
self.pages
|
||||||
|
.update(&mut ui.text.atlas, &self.layout, &self.sampler);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
|
||||||
|
pass.set_bind_group(2, self.pages.group(), &[]);
|
||||||
|
pass.draw(0..4, 0..list.instances);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The glyph atlas on the GPU: one array texture whose layers are the pages
|
||||||
|
/// `GlyphAtlas` packs.
|
||||||
|
///
|
||||||
|
/// One array rather than a texture per page because a layer index is ordinary
|
||||||
|
/// Vulkan 1.0 / GLES sampling, where a `binding_array` would need
|
||||||
|
/// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack.
|
||||||
|
pub struct GpuPages {
|
||||||
|
device: Device,
|
||||||
|
queue: Queue,
|
||||||
|
texture: Texture,
|
||||||
|
group: BindGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GpuPages {
|
||||||
|
pub fn new(
|
||||||
|
device: &Device,
|
||||||
|
queue: &Queue,
|
||||||
|
layout: &BindGroupLayout,
|
||||||
|
sampler: &Sampler,
|
||||||
|
) -> Self {
|
||||||
|
let texture = create_array(device, 1);
|
||||||
|
Self {
|
||||||
|
device: device.clone(),
|
||||||
|
queue: queue.clone(),
|
||||||
|
group: atlas_group(device, layout, &texture, sampler),
|
||||||
|
texture,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, atlas: &mut GlyphAtlas, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||||
|
if atlas.page_count() > self.texture.depth_or_array_layers() {
|
||||||
|
self.grow(atlas.page_count(), layout, sampler);
|
||||||
|
}
|
||||||
|
for (upload, page) in atlas.uploads() {
|
||||||
|
let dst = TexelCopyTextureInfo {
|
||||||
|
texture: &self.texture,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: Origin3d {
|
||||||
|
x: upload.rect.x,
|
||||||
|
y: upload.rect.y,
|
||||||
|
z: upload.layer,
|
||||||
|
},
|
||||||
|
aspect: TextureAspect::All,
|
||||||
|
};
|
||||||
|
write_region(&self.queue, dst, page, upload.rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn group(&self) -> &BindGroup {
|
||||||
|
&self.group
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Doubles until `needed` fits and copies the old layers across GPU side.
|
||||||
|
/// The new texture stales the group, so that is rebuilt here.
|
||||||
|
fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||||
|
let old = self.texture.depth_or_array_layers();
|
||||||
|
let mut layers = old;
|
||||||
|
while layers < needed {
|
||||||
|
layers *= 2;
|
||||||
|
}
|
||||||
|
let texture = create_array(&self.device, layers);
|
||||||
|
let mut encoder = self
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&CommandEncoderDescriptor {
|
||||||
|
label: Some("atlas grow"),
|
||||||
|
});
|
||||||
|
encoder.copy_texture_to_texture(
|
||||||
|
self.texture.as_image_copy(),
|
||||||
|
texture.as_image_copy(),
|
||||||
|
Extent3d {
|
||||||
|
width: PAGE,
|
||||||
|
height: PAGE,
|
||||||
|
depth_or_array_layers: old,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
self.group = atlas_group(&self.device, layout, &texture, sampler);
|
||||||
|
self.texture = texture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn atlas_group(
|
||||||
|
device: &Device,
|
||||||
|
layout: &BindGroupLayout,
|
||||||
|
texture: &Texture,
|
||||||
|
sampler: &Sampler,
|
||||||
|
) -> BindGroup {
|
||||||
|
let view = texture.create_view(&TextureViewDescriptor {
|
||||||
|
dimension: Some(TextureViewDimension::D2Array),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
sampled_group(device, layout, &view, sampler, "ui atlas")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_array(device: &Device, layers: u32) -> Texture {
|
||||||
|
device.create_texture(&TextureDescriptor {
|
||||||
|
label: Some("glyph atlas"),
|
||||||
|
size: Extent3d {
|
||||||
|
width: PAGE,
|
||||||
|
height: PAGE,
|
||||||
|
depth_or_array_layers: layers,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: TextureDimension::D2,
|
||||||
|
format: TextureFormat::Rgba8Unorm,
|
||||||
|
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC,
|
||||||
|
view_formats: &[],
|
||||||
|
})
|
||||||
|
}
|
||||||
+322
-641
File diff suppressed because it is too large.
Load diff
@@ -1,27 +0,0 @@
|
|||||||
use crate::util::Vec2;
|
|
||||||
|
|
||||||
pub fn distance_from_rect(pos: Vec2, center: Vec2, corner: Vec2, radius: f32) -> f32 {
|
|
||||||
let p = pos - center;
|
|
||||||
let q = Vec2::new(
|
|
||||||
p.x.abs() - (corner.x - radius),
|
|
||||||
p.y.abs() - (corner.y - radius),
|
|
||||||
);
|
|
||||||
let clamped = Vec2::new(q.x.max(0.0), q.y.max(0.0));
|
|
||||||
(clamped.x * clamped.x + clamped.y * clamped.y).sqrt() - radius
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rounded_rect_coverage(pos: Vec2, top_left: Vec2, bot_right: Vec2, radius: f32) -> f32 {
|
|
||||||
let edge: f32 = 0.5;
|
|
||||||
let corner = (bot_right - top_left) / 2.0;
|
|
||||||
let center = top_left + corner;
|
|
||||||
let dist = distance_from_rect(pos, center, corner, radius);
|
|
||||||
1.0 - smoothstep(-edge.min(radius), edge, dist)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// WGSL's `smoothstep`, which Rust has no equivalent of. Undefined in WGSL
|
|
||||||
/// when `low == high`, which is why the caller above never passes a zero
|
|
||||||
/// radius into the low edge without `edge` bounding it.
|
|
||||||
fn smoothstep(low: f32, high: f32, x: f32) -> f32 {
|
|
||||||
let t = ((x - low) / (high - low)).clamp(0.0, 1.0);
|
|
||||||
t * t * (3.0 - 2.0 * t)
|
|
||||||
}
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
const RECT: u32 = 0u;
|
|
||||||
// Standalone images select their texture through their own bind group.
|
|
||||||
const TEXTURE: u32 = 1u;
|
|
||||||
const GLYPH: u32 = 2u;
|
|
||||||
|
|
||||||
@group(0) @binding(0)
|
|
||||||
var<uniform> window: WindowUniform;
|
|
||||||
@group(1) @binding(RECT)
|
|
||||||
var<storage> rects: array<Rect>;
|
|
||||||
@group(1) @binding(GLYPH)
|
|
||||||
var<storage> glyphs: array<GlyphInfo>;
|
|
||||||
|
|
||||||
struct Rect {
|
|
||||||
paint: u32,
|
|
||||||
radius: f32,
|
|
||||||
thickness: f32,
|
|
||||||
inner_radius: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GlyphInfo {
|
|
||||||
uv_min: vec2<f32>,
|
|
||||||
uv_max: vec2<f32>,
|
|
||||||
// A layer in the shared atlas array, not a bind-group index.
|
|
||||||
layer: u32,
|
|
||||||
paint: u32,
|
|
||||||
flags: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mirrors `Mask` in data.rs. `parent` is u32::MAX at the root.
|
|
||||||
struct Mask {
|
|
||||||
primitive: u32,
|
|
||||||
parent: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mirrors `MoveOffset` in data.rs.
|
|
||||||
struct MoveOffset {
|
|
||||||
delta: vec2<f32>,
|
|
||||||
parent: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UiSpan {
|
|
||||||
start: UiScalar,
|
|
||||||
end: UiScalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UiScalar {
|
|
||||||
rel: f32,
|
|
||||||
abs: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// One array texture avoids descriptor indexing, which is not universal on Android.
|
|
||||||
@group(2) @binding(0)
|
|
||||||
var atlas: texture_2d_array<f32>;
|
|
||||||
// Image draws bind their texture here; other draws bind a 1x1 placeholder.
|
|
||||||
@group(2) @binding(1)
|
|
||||||
var image_texture: texture_2d<f32>;
|
|
||||||
@group(2) @binding(2)
|
|
||||||
var samp: sampler;
|
|
||||||
// Kept outside group 2 so standalone image bind groups need not name these buffers.
|
|
||||||
@group(3) @binding(0)
|
|
||||||
var<storage> masks: array<Mask>;
|
|
||||||
@group(3) @binding(1)
|
|
||||||
var<storage> move_offsets: array<MoveOffset>;
|
|
||||||
// Shared by the vertex stage's drawn primitive and the fragment stage's mask shape.
|
|
||||||
@group(3) @binding(2)
|
|
||||||
var<storage> instances: array<PrimitiveInstance>;
|
|
||||||
// Solid linear RGBA today. Primitives already refer to paint records rather
|
|
||||||
// than embedding colours so gradients and texture fills can extend this
|
|
||||||
// lookup without rewriting geometry.
|
|
||||||
@group(3) @binding(3)
|
|
||||||
var<storage> paints: array<vec4<f32>>;
|
|
||||||
|
|
||||||
// Keep synchronized with render_state.rs. The bound prevents a malformed
|
|
||||||
// parent cycle from hanging the GPU; real widget trees have exceeded 16.
|
|
||||||
const PARENT_CHAIN_LIMIT: u32 = 64u;
|
|
||||||
|
|
||||||
fn resolve_move(idx: u32) -> vec2<f32> {
|
|
||||||
var total = vec2<f32>(0.0, 0.0);
|
|
||||||
var i = idx;
|
|
||||||
for (var step = 0u; step < PARENT_CHAIN_LIMIT; step++) {
|
|
||||||
let entry = move_offsets[i];
|
|
||||||
total += entry.delta;
|
|
||||||
if entry.parent == 4294967295u {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i = entry.parent;
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WindowUniform {
|
|
||||||
dim: vec2<f32>,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Mirrors `PrimitiveInstance` in data.rs.
|
|
||||||
struct PrimitiveInstance {
|
|
||||||
x: UiSpan,
|
|
||||||
y: UiSpan,
|
|
||||||
binding: u32,
|
|
||||||
idx: u32,
|
|
||||||
mask_idx: u32,
|
|
||||||
move_idx: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct InstanceInput {
|
|
||||||
@location(0) slot: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct VertexOutput {
|
|
||||||
@location(0) top_left: vec2<f32>,
|
|
||||||
@location(1) bot_right: vec2<f32>,
|
|
||||||
@location(2) uv: vec2<f32>,
|
|
||||||
// Naga requires integer varyings to declare flat interpolation.
|
|
||||||
@location(3) @interpolate(flat) binding: u32,
|
|
||||||
@location(4) @interpolate(flat) idx: u32,
|
|
||||||
@location(5) @interpolate(flat) mask_idx: u32,
|
|
||||||
@builtin(position) clip_position: vec4<f32>,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Region {
|
|
||||||
pos: vec2<f32>,
|
|
||||||
uv: vec2<f32>,
|
|
||||||
top_left: vec2<f32>,
|
|
||||||
bot_right: vec2<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shared by drawing and mask coverage so their geometry cannot diverge.
|
|
||||||
struct Corners {
|
|
||||||
top_left: vec2<f32>,
|
|
||||||
bot_right: vec2<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn corners_of(inst: PrimitiveInstance) -> Corners {
|
|
||||||
let top_left_rel = vec2(inst.x.start.rel, inst.y.start.rel);
|
|
||||||
let top_left_abs = vec2(inst.x.start.abs, inst.y.start.abs);
|
|
||||||
let bot_right_rel = vec2(inst.x.end.rel, inst.y.end.rel);
|
|
||||||
let bot_right_abs = vec2(inst.x.end.abs, inst.y.end.abs);
|
|
||||||
let move_delta = resolve_move(inst.move_idx);
|
|
||||||
return Corners(
|
|
||||||
floor(top_left_rel * window.dim) + floor(top_left_abs + move_delta),
|
|
||||||
floor(bot_right_rel * window.dim) + floor(bot_right_abs + move_delta),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@vertex
|
|
||||||
fn vs_main(
|
|
||||||
@builtin(vertex_index) vi: u32,
|
|
||||||
in: InstanceInput,
|
|
||||||
) -> VertexOutput {
|
|
||||||
var out: VertexOutput;
|
|
||||||
let inst = instances[in.slot];
|
|
||||||
|
|
||||||
let c = corners_of(inst);
|
|
||||||
let top_left = c.top_left;
|
|
||||||
let bot_right = c.bot_right;
|
|
||||||
let size = bot_right - top_left;
|
|
||||||
|
|
||||||
let uv = vec2<f32>(
|
|
||||||
f32(vi % 2u),
|
|
||||||
f32(vi / 2u)
|
|
||||||
);
|
|
||||||
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
|
|
||||||
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
|
||||||
out.uv = uv;
|
|
||||||
out.binding = inst.binding;
|
|
||||||
out.idx = inst.idx;
|
|
||||||
out.top_left = top_left;
|
|
||||||
out.bot_right = bot_right;
|
|
||||||
out.mask_idx = inst.mask_idx;
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@fragment
|
|
||||||
fn fs_main(
|
|
||||||
in: VertexOutput
|
|
||||||
) -> @location(0) vec4<f32> {
|
|
||||||
let pos = in.clip_position.xy;
|
|
||||||
let region = Region(pos, in.uv, in.top_left, in.bot_right);
|
|
||||||
let i = in.idx;
|
|
||||||
var color: vec4<f32>;
|
|
||||||
switch in.binding {
|
|
||||||
case RECT: {
|
|
||||||
color = draw_rounded_rect(region, rects[i]);
|
|
||||||
}
|
|
||||||
case TEXTURE: {
|
|
||||||
color = draw_texture(region);
|
|
||||||
}
|
|
||||||
case GLYPH: {
|
|
||||||
color = draw_glyph(region, glyphs[i]);
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
color = vec4(1.0, 0.0, 1.0, 1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Nested masks multiply coverage, matching the CPU hit test.
|
|
||||||
var mask_idx = in.mask_idx;
|
|
||||||
for (var step = 0u; step < PARENT_CHAIN_LIMIT; step++) {
|
|
||||||
if mask_idx == 4294967295u {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let mask = masks[mask_idx];
|
|
||||||
color.a *= mask_coverage(pos, mask);
|
|
||||||
mask_idx = mask.parent;
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Uses the referenced primitive itself so its drawn and clipped edges agree.
|
|
||||||
fn mask_coverage(pos: vec2<f32>, mask: Mask) -> f32 {
|
|
||||||
let inst = instances[mask.primitive];
|
|
||||||
if inst.binding != RECT {
|
|
||||||
// Painter::set_mask rejects non-rect shapes; fail open if that invariant breaks.
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
let c = corners_of(inst);
|
|
||||||
return rounded_rect_coverage(pos, c.top_left, c.bot_right, rects[inst.idx].radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_texture(region: Region) -> vec4<f32> {
|
|
||||||
return textureSample(image_texture, samp, region.uv);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_glyph(region: Region, g: GlyphInfo) -> vec4<f32> {
|
|
||||||
let uv = mix(g.uv_min, g.uv_max, region.uv);
|
|
||||||
let texel = textureSample(atlas, samp, uv, i32(g.layer));
|
|
||||||
if (g.flags & 1u) != 0u {
|
|
||||||
return texel;
|
|
||||||
}
|
|
||||||
var color = paints[g.paint];
|
|
||||||
color.a *= texel.a;
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Keep synchronized with the CPU hit-test implementation in render::sdf.
|
|
||||||
fn rounded_rect_coverage(
|
|
||||||
pos: vec2<f32>,
|
|
||||||
top_left: vec2<f32>,
|
|
||||||
bot_right: vec2<f32>,
|
|
||||||
radius: f32,
|
|
||||||
) -> f32 {
|
|
||||||
let edge = 0.5;
|
|
||||||
let corner = (bot_right - top_left) / 2.0;
|
|
||||||
let center = top_left + corner;
|
|
||||||
let dist = distance_from_rect(pos, center, corner, radius);
|
|
||||||
return 1.0 - smoothstep(-min(edge, radius), edge, dist);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_rounded_rect(region: Region, rect: Rect) -> vec4<f32> {
|
|
||||||
var color = paints[rect.paint];
|
|
||||||
|
|
||||||
let edge = 0.5;
|
|
||||||
|
|
||||||
color.a *= rounded_rect_coverage(region.pos, region.top_left, region.bot_right, rect.radius);
|
|
||||||
|
|
||||||
if rect.thickness > 0.0 {
|
|
||||||
let size = region.bot_right - region.top_left;
|
|
||||||
let corner = size / 2.0;
|
|
||||||
let center = region.top_left + corner;
|
|
||||||
let dist2 = distance_from_rect(region.pos, center, corner - rect.thickness, rect.inner_radius);
|
|
||||||
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
|
|
||||||
let p = pixel_pos - rect_center;
|
|
||||||
let q = abs(p) - (rect_corner - radius);
|
|
||||||
return length(max(q, vec2(0.0))) - radius;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Matches `GlyphEntry::IS_COLORED`.
|
||||||
|
const COLORED: u32 = 1u;
|
||||||
|
|
||||||
|
// The glyph atlas, whose array layers are its pages.
|
||||||
|
@group(2) @binding(0)
|
||||||
|
var atlas: texture_2d_array<f32>;
|
||||||
|
@group(2) @binding(1)
|
||||||
|
var samp: sampler;
|
||||||
|
|
||||||
|
struct GlyphInfo {
|
||||||
|
uv_min: vec2<f32>,
|
||||||
|
uv_max: vec2<f32>,
|
||||||
|
// Which layer of the atlas array this glyph's page is.
|
||||||
|
layer: u32,
|
||||||
|
color: u32,
|
||||||
|
flags: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(1) @binding(0)
|
||||||
|
var<storage> glyphs: array<GlyphInfo>;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let g = glyphs[in.idx];
|
||||||
|
let uv = mix(g.uv_min, g.uv_max, in.uv);
|
||||||
|
let texel = textureSample(atlas, samp, uv, i32(g.layer));
|
||||||
|
if (g.flags & COLORED) != 0u {
|
||||||
|
return masked(in, texel);
|
||||||
|
}
|
||||||
|
var color = unpack4x8unorm(g.color);
|
||||||
|
color.a *= texel.a;
|
||||||
|
return masked(in, color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
// Prepended to every primitive's shader, which declares its own instance data
|
||||||
|
// as `var<storage> <name>: array<T>` at group 1 binding 0, and an `fs_main`
|
||||||
|
// shading one instance of it. What it samples, if anything, is bound at group
|
||||||
|
// 2: the texture at binding 0 and the sampler at binding 1.
|
||||||
|
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> window: WindowUniform;
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage> masks: array<Mask>;
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<storage> move_offsets: array<MoveOffset>;
|
||||||
|
|
||||||
|
struct WindowUniform {
|
||||||
|
dim: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Mask {
|
||||||
|
x: RawSpan,
|
||||||
|
y: RawSpan,
|
||||||
|
move_idx: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MoveOffset {
|
||||||
|
x: RawSpan,
|
||||||
|
y: RawSpan,
|
||||||
|
parent: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// `PX_STEP` and `REL_STEP` are prepended from `iris_core`'s own constants:
|
||||||
|
// what it stores is a whole count of each, both powers of two, so decoding
|
||||||
|
// is exact and the number here is the number the CPU decided.
|
||||||
|
|
||||||
|
// Every coordinate the CPU decided is a whole count of `PX_STEP`, so one that
|
||||||
|
// composes to within half a step of a pixel boundary is on that boundary and
|
||||||
|
// belongs to the pixel above it. Flooring the product instead drops a pixel
|
||||||
|
// wherever a fraction divides a window exactly: a fifth of 1920 comes out of
|
||||||
|
// `REL_STEP` as 383.99998, and five tabs each lose their last column.
|
||||||
|
//
|
||||||
|
// Taken over the whole coordinate, fraction and pixels summed, since a floor
|
||||||
|
// does not distribute over a sum: floored apart, a half of one and a half of
|
||||||
|
// the other lose the pixel the two together make.
|
||||||
|
fn snap_floor(v: vec2<f32>) -> vec2<f32> {
|
||||||
|
return floor(v + PX_STEP * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RawScalar {
|
||||||
|
rel: i32,
|
||||||
|
px: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RawSpan {
|
||||||
|
start: RawScalar,
|
||||||
|
end: RawScalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scalar_of(raw: RawScalar) -> Len {
|
||||||
|
return Len(f32(raw.rel) * REL_STEP, f32(raw.px) * PX_STEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span_of(raw: RawSpan) -> UiSpan {
|
||||||
|
return UiSpan(scalar_of(raw.start), scalar_of(raw.end));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scalar_of_pair(raw: vec2<i32>) -> Len {
|
||||||
|
return Len(f32(raw.x) * REL_STEP, f32(raw.y) * PX_STEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// The same expression `Len::within` uses, in floats rather than on the
|
||||||
|
// CPU's grid: a move is resolved here so that scrolling a subtree writes one
|
||||||
|
// entry instead of walking it. What has to hold is that this agrees with
|
||||||
|
// itself frame to frame, not that it matches the CPU to the last bit.
|
||||||
|
fn scalar_within(s: Len, p: UiSpan) -> Len {
|
||||||
|
return Len(
|
||||||
|
p.start.rel + (p.end.rel - p.start.rel) * s.rel,
|
||||||
|
s.px + (p.start.px + (p.end.px - p.start.px) * s.rel),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span_within(s: UiSpan, p: UiSpan) -> UiSpan {
|
||||||
|
return UiSpan(scalar_within(s.start, p), scalar_within(s.end, p));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_move(idx: u32, local: Region) -> Region {
|
||||||
|
var r = local;
|
||||||
|
var at = idx;
|
||||||
|
for (var step = 0u; step < CHAIN_LIMIT; step++) {
|
||||||
|
if at == MOVE_NONE {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let entry = move_offsets[at];
|
||||||
|
r = Region(span_within(r.x, span_of(entry.x)), span_within(r.y, span_of(entry.y)));
|
||||||
|
at = entry.parent;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UiSpan {
|
||||||
|
start: Len,
|
||||||
|
end: Len,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Len {
|
||||||
|
rel: f32,
|
||||||
|
px: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InstanceInput {
|
||||||
|
@location(0) x_start: vec2<i32>,
|
||||||
|
@location(1) x_end: vec2<i32>,
|
||||||
|
@location(2) y_start: vec2<i32>,
|
||||||
|
@location(3) y_end: vec2<i32>,
|
||||||
|
@location(4) mask_idx: u32,
|
||||||
|
@location(5) move_idx: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@location(0) top_left: vec2<f32>,
|
||||||
|
@location(1) bot_right: vec2<f32>,
|
||||||
|
@location(2) uv: vec2<f32>,
|
||||||
|
@location(3) @interpolate(flat) mask_idx: u32,
|
||||||
|
@location(4) @interpolate(flat) idx: u32,
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(
|
||||||
|
@builtin(vertex_index) vi: u32,
|
||||||
|
@builtin(instance_index) ii: u32,
|
||||||
|
in: InstanceInput,
|
||||||
|
) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
|
||||||
|
let local = Region(
|
||||||
|
UiSpan(scalar_of_pair(in.x_start), scalar_of_pair(in.x_end)),
|
||||||
|
UiSpan(scalar_of_pair(in.y_start), scalar_of_pair(in.y_end)),
|
||||||
|
);
|
||||||
|
let r = resolve_move(in.move_idx, local);
|
||||||
|
let top_left_rel = vec2(r.x.start.rel, r.y.start.rel);
|
||||||
|
let top_left_px = vec2(r.x.start.px, r.y.start.px);
|
||||||
|
let bot_right_rel = vec2(r.x.end.rel, r.y.end.rel);
|
||||||
|
let bot_right_px = vec2(r.x.end.px, r.y.end.px);
|
||||||
|
|
||||||
|
let top_left = snap_floor(top_left_rel * window.dim + top_left_px);
|
||||||
|
let bot_right = snap_floor(bot_right_rel * window.dim + bot_right_px);
|
||||||
|
let size = bot_right - top_left;
|
||||||
|
|
||||||
|
let uv = vec2<f32>(
|
||||||
|
f32(vi % 2u),
|
||||||
|
f32(vi / 2u)
|
||||||
|
);
|
||||||
|
let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0;
|
||||||
|
out.clip_position = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
||||||
|
out.uv = uv;
|
||||||
|
out.top_left = top_left;
|
||||||
|
out.bot_right = bot_right;
|
||||||
|
out.mask_idx = in.mask_idx;
|
||||||
|
out.idx = ii;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn masked(in: VertexOutput, color: vec4<f32>) -> vec4<f32> {
|
||||||
|
if in.mask_idx == 4294967295u {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
let mask = masks[in.mask_idx];
|
||||||
|
// Its own chain, not the drawn primitive's, so a stationary viewport
|
||||||
|
// clips content that moves inside it.
|
||||||
|
let m = resolve_move(mask.move_idx, Region(span_of(mask.x), span_of(mask.y)));
|
||||||
|
let tl = vec2(m.x.start.rel, m.y.start.rel);
|
||||||
|
let tl_px = vec2(m.x.start.px, m.y.start.px);
|
||||||
|
let br = vec2(m.x.end.rel, m.y.end.rel);
|
||||||
|
let br_px = vec2(m.x.end.px, m.y.end.px);
|
||||||
|
|
||||||
|
let top_left = snap_floor(tl * window.dim + tl_px);
|
||||||
|
let bot_right = snap_floor(br * window.dim + br_px);
|
||||||
|
let pos = in.clip_position.xy;
|
||||||
|
if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y {
|
||||||
|
return color * 0.0;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
struct Rect {
|
||||||
|
color: u32,
|
||||||
|
radius: f32,
|
||||||
|
thickness: f32,
|
||||||
|
inner_radius: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(1) @binding(0)
|
||||||
|
var<storage> rects: array<Rect>;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let rect = rects[in.idx];
|
||||||
|
var color = unpack4x8unorm(rect.color);
|
||||||
|
|
||||||
|
let edge = 0.5;
|
||||||
|
let size = in.bot_right - in.top_left;
|
||||||
|
let corner = size / 2.0;
|
||||||
|
let center = in.top_left + corner;
|
||||||
|
let pos = in.clip_position.xy;
|
||||||
|
|
||||||
|
let dist = distance_from_rect(pos, center, corner, rect.radius);
|
||||||
|
color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist);
|
||||||
|
|
||||||
|
if rect.thickness > 0.0 {
|
||||||
|
let dist2 = distance_from_rect(pos, center, corner - rect.thickness, rect.inner_radius);
|
||||||
|
color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return masked(in, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, radius: f32) -> f32 {
|
||||||
|
// vec from center to pixel
|
||||||
|
let p = pixel_pos - rect_center;
|
||||||
|
// vec from inner rect corner to pixel
|
||||||
|
let q = abs(p) - (rect_corner - radius);
|
||||||
|
return length(max(q, vec2(0.0))) - radius;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// The image this instance draws, bound for it alone.
|
||||||
|
@group(2) @binding(0)
|
||||||
|
var image: texture_2d<f32>;
|
||||||
|
@group(2) @binding(1)
|
||||||
|
var samp: sampler;
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return masked(in, textureSample(image, samp, in.uv));
|
||||||
|
}
|
||||||
+173
-381
@@ -1,264 +1,113 @@
|
|||||||
use image::{DynamicImage, EncodableLayout, GenericImageView};
|
use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage};
|
||||||
use wgpu::{util::DeviceExt, *};
|
use wgpu::{util::DeviceExt, *};
|
||||||
|
|
||||||
use crate::{PatchRect, TextureKind, TextureUpdate, Textures};
|
use crate::{
|
||||||
|
PatchRect, TextureUpdate, Textures, UiData,
|
||||||
|
render::{
|
||||||
|
TexturePrimitive,
|
||||||
|
primitive::{ListDraw, PrimitiveRender},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use super::atlas::PAGE;
|
/// Draws standalone images, which it owns. Each is its own texture, so each
|
||||||
|
/// instance binds its own and is a draw of its own.
|
||||||
/// The fewest layers the glyph atlas array is ever created with. Two, not
|
pub struct ImageRender {
|
||||||
/// one, for the GLES reason written on `create_array_texture`.
|
textures: GpuTextures,
|
||||||
const MIN_ARRAY_LAYERS: u32 = 2;
|
layout: BindGroupLayout,
|
||||||
|
sampler: Sampler,
|
||||||
enum Slot {
|
|
||||||
Empty,
|
|
||||||
Image(ImageGpu),
|
|
||||||
/// The array layer a live page occupies. Dropping the page empties this
|
|
||||||
/// resource slot; its array layer may then be assigned to a new page.
|
|
||||||
Page(u32),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImageGpu {
|
impl ImageRender {
|
||||||
#[allow(dead_code)]
|
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||||
texture: Texture,
|
Self {
|
||||||
view: TextureView,
|
textures: GpuTextures::new(device, queue),
|
||||||
bind_group: BindGroup,
|
layout: sampled_layout(device, TextureViewDimension::D2, "ui image"),
|
||||||
|
sampler: default_sampler(device),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// - **The glyph atlas**, one `texture_2d_array` whose layers are pages
|
impl PrimitiveRender for ImageRender {
|
||||||
/// (`Slot::Page`), grown by recreating the array with headroom and
|
fn layout(&self) -> Option<&BindGroupLayout> {
|
||||||
/// `copy_texture_to_texture`-ing the old layers across. No feature beyond
|
Some(&self.layout)
|
||||||
/// Vulkan 1.0/GLES sampling is needed for this -- a layer index is an
|
}
|
||||||
/// ordinary sampling operand.
|
|
||||||
/// - **Standalone images** (`Slot::Image`), each its own `Texture` and
|
fn update(&mut self, ui: &mut UiData) {
|
||||||
/// `BindGroup`, drawn one `draw()` call at a time with that bind group
|
self.textures
|
||||||
/// bound -- see `UiRenderNode::draw`.
|
.update(&mut ui.textures, &self.layout, &self.sampler);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instance_bindings(&self, list: &super::InstanceList, out: &mut Vec<u32>) {
|
||||||
|
let slots = list
|
||||||
|
.data()
|
||||||
|
.chunks_exact(list.stride())
|
||||||
|
.map(|data| bytemuck::pod_read_unaligned::<TexturePrimitive>(data).slot);
|
||||||
|
out.extend(slots);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) {
|
||||||
|
for (i, &slot) in list.bindings.iter().enumerate() {
|
||||||
|
let Some(image) = self.textures.group(slot) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
pass.set_bind_group(2, image, &[]);
|
||||||
|
pass.draw(0..4, i as u32..i as u32 + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The standalone images a ui draws, each its own texture and bind group --
|
||||||
|
/// unlike the glyph atlas in `super::page`, which is one array they share.
|
||||||
pub struct GpuTextures {
|
pub struct GpuTextures {
|
||||||
device: Device,
|
device: Device,
|
||||||
queue: Queue,
|
queue: Queue,
|
||||||
|
slots: Vec<Option<ImageGpu>>,
|
||||||
|
}
|
||||||
|
|
||||||
slots: Vec<Slot>,
|
struct ImageGpu {
|
||||||
|
/// Kept for `patch`, which needs the texture rather than the view.
|
||||||
array_texture: Texture,
|
texture: Texture,
|
||||||
array_view: TextureView,
|
group: BindGroup,
|
||||||
array_capacity: u32,
|
|
||||||
/// One past the highest layer ever populated. Holes below this remain in
|
|
||||||
/// place when the array grows, while `Textures` can reuse their numbers.
|
|
||||||
layer_high_water: u32,
|
|
||||||
|
|
||||||
sampler: Sampler,
|
|
||||||
/// Bound in the image slot of the main draw's bind group, which has
|
|
||||||
/// nothing of its own to put there: rects and glyphs never sample it,
|
|
||||||
/// but the layout requires something bound regardless.
|
|
||||||
null_view: TextureView,
|
|
||||||
|
|
||||||
bind_group_creates: u64,
|
|
||||||
pages_grown: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpuTextures {
|
impl GpuTextures {
|
||||||
/// Applies queued `Textures` updates, then reports whether the *main*
|
pub fn new(device: &Device, queue: &Queue) -> Self {
|
||||||
/// bind group (the one rects and glyphs draw with) needs rebuilding --
|
Self {
|
||||||
/// true exactly when the atlas array was recreated (its view identity
|
device: device.clone(),
|
||||||
/// changed). Pushing or freeing a standalone image never touches that
|
queue: queue.clone(),
|
||||||
/// group: it built or drops its own. Masks/move_offsets resizing is
|
slots: Vec::new(),
|
||||||
/// `UiRenderNode`'s own concern now (its `masks_group`, group 3) --
|
}
|
||||||
/// see that struct's field comment for why standalone images no longer
|
}
|
||||||
/// hear about either buffer at all.
|
|
||||||
pub fn update(&mut self, textures: &mut Textures, rsc_layout: &BindGroupLayout) -> bool {
|
pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) {
|
||||||
let mut rebuild_main = false;
|
|
||||||
for update in textures.updates() {
|
for update in textures.updates() {
|
||||||
match update {
|
match update {
|
||||||
TextureUpdate::Push(kind, image) => {
|
TextureUpdate::Push(image) => {
|
||||||
rebuild_main |= self.push(kind, image, rsc_layout);
|
let image = self.create(image, layout, sampler);
|
||||||
|
self.slots.push(Some(image));
|
||||||
}
|
}
|
||||||
TextureUpdate::Set(kind, i, image) => {
|
TextureUpdate::Set(i, image) => {
|
||||||
rebuild_main |= self.set(kind, i, image, rsc_layout);
|
let image = self.create(image, layout, sampler);
|
||||||
|
self.slots[i as usize] = Some(image);
|
||||||
}
|
}
|
||||||
// A patch changes texture contents, not which layer or bind
|
|
||||||
// group exists, so it never asks for a rebuild -- rebuilding
|
|
||||||
// per glyph is exactly the cost this exists to avoid.
|
|
||||||
TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image),
|
TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image),
|
||||||
|
TextureUpdate::PushFree => self.slots.push(None),
|
||||||
TextureUpdate::SetFree => {}
|
TextureUpdate::SetFree => {}
|
||||||
TextureUpdate::Free(i) => self.free(i),
|
TextureUpdate::Free(i) => self.slots[i as usize] = None,
|
||||||
TextureUpdate::PushFree(_kind) => self.slots.push(Slot::Empty),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rebuild_main
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push(
|
pub fn group(&self, slot: u32) -> Option<&BindGroup> {
|
||||||
&mut self,
|
self.slots.get(slot as usize)?.as_ref().map(|i| &i.group)
|
||||||
kind: TextureKind,
|
}
|
||||||
|
|
||||||
|
fn create(
|
||||||
|
&self,
|
||||||
image: &DynamicImage,
|
image: &DynamicImage,
|
||||||
rsc_layout: &BindGroupLayout,
|
layout: &BindGroupLayout,
|
||||||
) -> bool {
|
sampler: &Sampler,
|
||||||
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout);
|
) -> ImageGpu {
|
||||||
self.slots.push(slot);
|
|
||||||
rebuilt
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set(
|
|
||||||
&mut self,
|
|
||||||
kind: TextureKind,
|
|
||||||
i: u32,
|
|
||||||
image: &DynamicImage,
|
|
||||||
rsc_layout: &BindGroupLayout,
|
|
||||||
) -> bool {
|
|
||||||
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout);
|
|
||||||
self.slots[i as usize] = slot;
|
|
||||||
rebuilt
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_slot(
|
|
||||||
&mut self,
|
|
||||||
kind: TextureKind,
|
|
||||||
image: &DynamicImage,
|
|
||||||
rsc_layout: &BindGroupLayout,
|
|
||||||
) -> (Slot, bool) {
|
|
||||||
match kind {
|
|
||||||
TextureKind::Image => {
|
|
||||||
let gpu = self.create_image(image, rsc_layout);
|
|
||||||
(Slot::Image(gpu), false)
|
|
||||||
}
|
|
||||||
TextureKind::Page { layer } => {
|
|
||||||
let mut rebuilt = false;
|
|
||||||
if layer >= self.array_capacity {
|
|
||||||
self.grow_array(rsc_layout);
|
|
||||||
rebuilt = true;
|
|
||||||
}
|
|
||||||
self.write_full_layer(layer, image);
|
|
||||||
self.layer_high_water = self.layer_high_water.max(layer + 1);
|
|
||||||
(Slot::Page(layer), rebuilt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn free(&mut self, i: u32) {
|
|
||||||
if let Some(slot) = self.slots.get_mut(i as usize) {
|
|
||||||
*slot = Slot::Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
|
||||||
let Some(&Slot::Page(layer)) = self.slots.get(i as usize) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if rect.width == 0 || rect.height == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let sub = image
|
|
||||||
.view(rect.x, rect.y, rect.width, rect.height)
|
|
||||||
.to_image();
|
|
||||||
self.queue.write_texture(
|
|
||||||
TexelCopyTextureInfo {
|
|
||||||
texture: &self.array_texture,
|
|
||||||
mip_level: 0,
|
|
||||||
origin: Origin3d {
|
|
||||||
x: rect.x,
|
|
||||||
y: rect.y,
|
|
||||||
z: layer,
|
|
||||||
},
|
|
||||||
aspect: TextureAspect::All,
|
|
||||||
},
|
|
||||||
sub.as_bytes(),
|
|
||||||
TexelCopyBufferLayout {
|
|
||||||
offset: 0,
|
|
||||||
bytes_per_row: Some(rect.width * 4),
|
|
||||||
rows_per_image: Some(rect.height),
|
|
||||||
},
|
|
||||||
Extent3d {
|
|
||||||
width: rect.width,
|
|
||||||
height: rect.height,
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_full_layer(&self, layer: u32, image: &DynamicImage) {
|
|
||||||
// Every page is created as exactly PAGE x PAGE (`GlyphAtlas::allocate`),
|
|
||||||
// so this is always a whole-layer write, never a crop.
|
|
||||||
let rgba = image.to_rgba8();
|
|
||||||
self.queue.write_texture(
|
|
||||||
TexelCopyTextureInfo {
|
|
||||||
texture: &self.array_texture,
|
|
||||||
mip_level: 0,
|
|
||||||
origin: Origin3d {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
z: layer,
|
|
||||||
},
|
|
||||||
aspect: TextureAspect::All,
|
|
||||||
},
|
|
||||||
rgba.as_bytes(),
|
|
||||||
TexelCopyBufferLayout {
|
|
||||||
offset: 0,
|
|
||||||
bytes_per_row: Some(PAGE * 4),
|
|
||||||
rows_per_image: Some(PAGE),
|
|
||||||
},
|
|
||||||
Extent3d {
|
|
||||||
width: PAGE,
|
|
||||||
height: PAGE,
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn grow_array(&mut self, rsc_layout: &BindGroupLayout) {
|
|
||||||
self.pages_grown += 1;
|
|
||||||
let new_capacity = self.array_capacity * 2;
|
|
||||||
let new_texture = Self::create_array_texture(&self.device, new_capacity);
|
|
||||||
if self.layer_high_water > 0 {
|
|
||||||
let mut encoder = self
|
|
||||||
.device
|
|
||||||
.create_command_encoder(&CommandEncoderDescriptor {
|
|
||||||
label: Some("atlas array grow"),
|
|
||||||
});
|
|
||||||
encoder.copy_texture_to_texture(
|
|
||||||
TexelCopyTextureInfo {
|
|
||||||
texture: &self.array_texture,
|
|
||||||
mip_level: 0,
|
|
||||||
origin: Origin3d::ZERO,
|
|
||||||
aspect: TextureAspect::All,
|
|
||||||
},
|
|
||||||
TexelCopyTextureInfo {
|
|
||||||
texture: &new_texture,
|
|
||||||
mip_level: 0,
|
|
||||||
origin: Origin3d::ZERO,
|
|
||||||
aspect: TextureAspect::All,
|
|
||||||
},
|
|
||||||
Extent3d {
|
|
||||||
width: PAGE,
|
|
||||||
height: PAGE,
|
|
||||||
depth_or_array_layers: self.layer_high_water,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
self.queue.submit(std::iter::once(encoder.finish()));
|
|
||||||
}
|
|
||||||
self.array_texture = new_texture;
|
|
||||||
self.array_view = self.array_texture.create_view(&TextureViewDescriptor {
|
|
||||||
dimension: Some(TextureViewDimension::D2Array),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
self.array_capacity = new_capacity;
|
|
||||||
self.rebuild_image_bind_groups(rsc_layout);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rebuild_image_bind_groups(&mut self, rsc_layout: &BindGroupLayout) {
|
|
||||||
for slot in &mut self.slots {
|
|
||||||
if let Slot::Image(gpu) = slot {
|
|
||||||
gpu.bind_group = Self::make_image_bind_group(
|
|
||||||
&self.device,
|
|
||||||
rsc_layout,
|
|
||||||
&self.array_view,
|
|
||||||
&gpu.view,
|
|
||||||
&self.sampler,
|
|
||||||
);
|
|
||||||
self.bind_group_creates += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_image(&mut self, image: &DynamicImage, rsc_layout: &BindGroupLayout) -> ImageGpu {
|
|
||||||
let rgba = image.to_rgba8();
|
let rgba = image.to_rgba8();
|
||||||
let (width, height) = rgba.dimensions();
|
let (width, height) = rgba.dimensions();
|
||||||
let texture = self.device.create_texture_with_data(
|
let texture = self.device.create_texture_with_data(
|
||||||
@@ -273,11 +122,7 @@ impl GpuTextures {
|
|||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
sample_count: 1,
|
sample_count: 1,
|
||||||
dimension: TextureDimension::D2,
|
dimension: TextureDimension::D2,
|
||||||
// `image` and swash colour-glyph bytes are encoded sRGB.
|
format: TextureFormat::Rgba8Unorm,
|
||||||
// Sampling this view decodes RGB to the linear-light values
|
|
||||||
// used by the paint buffer and render pipeline; alpha stays
|
|
||||||
// linear.
|
|
||||||
format: TextureFormat::Rgba8UnormSrgb,
|
|
||||||
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
||||||
view_formats: &[],
|
view_formats: &[],
|
||||||
},
|
},
|
||||||
@@ -285,165 +130,112 @@ impl GpuTextures {
|
|||||||
rgba.as_bytes(),
|
rgba.as_bytes(),
|
||||||
);
|
);
|
||||||
let view = texture.create_view(&TextureViewDescriptor::default());
|
let view = texture.create_view(&TextureViewDescriptor::default());
|
||||||
let bind_group = Self::make_image_bind_group(
|
let group = sampled_group(&self.device, layout, &view, sampler, "ui image");
|
||||||
&self.device,
|
ImageGpu { texture, group }
|
||||||
rsc_layout,
|
|
||||||
&self.array_view,
|
|
||||||
&view,
|
|
||||||
&self.sampler,
|
|
||||||
);
|
|
||||||
self.bind_group_creates += 1;
|
|
||||||
ImageGpu {
|
|
||||||
texture,
|
|
||||||
view,
|
|
||||||
bind_group,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_image_bind_group(
|
fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) {
|
||||||
|
let Some(Some(slot)) = self.slots.get(i as usize) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let dst = TexelCopyTextureInfo {
|
||||||
|
texture: &slot.texture,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: Origin3d {
|
||||||
|
x: rect.x,
|
||||||
|
y: rect.y,
|
||||||
|
z: 0,
|
||||||
|
},
|
||||||
|
aspect: TextureAspect::All,
|
||||||
|
};
|
||||||
|
match image.as_rgba8() {
|
||||||
|
Some(rgba) => write_region(&self.queue, dst, rgba, rect),
|
||||||
|
// The texture is rgba8, so any other layout has to be converted --
|
||||||
|
// and converting the rectangle is cheaper than the whole image.
|
||||||
|
None => {
|
||||||
|
let sub = image
|
||||||
|
.view(rect.x, rect.y, rect.width, rect.height)
|
||||||
|
.to_image();
|
||||||
|
write_region(&self.queue, dst, &sub, PatchRect { x: 0, y: 0, ..rect });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) {
|
||||||
|
if rect.width == 0 || rect.height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let stride = src.width() * 4;
|
||||||
|
queue.write_texture(
|
||||||
|
dst,
|
||||||
|
src.as_bytes(),
|
||||||
|
TexelCopyBufferLayout {
|
||||||
|
offset: (rect.y * stride + rect.x * 4) as u64,
|
||||||
|
bytes_per_row: Some(stride),
|
||||||
|
rows_per_image: Some(rect.height),
|
||||||
|
},
|
||||||
|
Extent3d {
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a primitive that samples binds: a texture, and the sampler that reads
|
||||||
|
/// it.
|
||||||
|
pub fn sampled_group(
|
||||||
device: &Device,
|
device: &Device,
|
||||||
rsc_layout: &BindGroupLayout,
|
layout: &BindGroupLayout,
|
||||||
array_view: &TextureView,
|
view: &TextureView,
|
||||||
image_view: &TextureView,
|
|
||||||
sampler: &Sampler,
|
sampler: &Sampler,
|
||||||
) -> BindGroup {
|
label: &'static str,
|
||||||
|
) -> BindGroup {
|
||||||
device.create_bind_group(&BindGroupDescriptor {
|
device.create_bind_group(&BindGroupDescriptor {
|
||||||
layout: rsc_layout,
|
layout,
|
||||||
entries: &[
|
entries: &[
|
||||||
BindGroupEntry {
|
BindGroupEntry {
|
||||||
binding: 0,
|
binding: 0,
|
||||||
resource: BindingResource::TextureView(array_view),
|
resource: BindingResource::TextureView(view),
|
||||||
},
|
},
|
||||||
BindGroupEntry {
|
BindGroupEntry {
|
||||||
binding: 1,
|
binding: 1,
|
||||||
resource: BindingResource::TextureView(image_view),
|
|
||||||
},
|
|
||||||
BindGroupEntry {
|
|
||||||
binding: 2,
|
|
||||||
resource: BindingResource::Sampler(sampler),
|
resource: BindingResource::Sampler(sampler),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
label: Some("ui rsc image"),
|
label: Some(label),
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
fn create_array_texture(device: &Device, capacity: u32) -> Texture {
|
|
||||||
debug_assert!(
|
|
||||||
capacity >= MIN_ARRAY_LAYERS,
|
|
||||||
"glyph atlas array asked for {capacity} layers; fewer than {MIN_ARRAY_LAYERS} is a \
|
|
||||||
GL_TEXTURE_2D on the GLES backend and draws every glyph as a box"
|
|
||||||
);
|
|
||||||
device.create_texture(&TextureDescriptor {
|
|
||||||
label: Some("glyph atlas array"),
|
|
||||||
size: Extent3d {
|
|
||||||
width: PAGE,
|
|
||||||
height: PAGE,
|
|
||||||
depth_or_array_layers: capacity,
|
|
||||||
},
|
|
||||||
mip_level_count: 1,
|
|
||||||
sample_count: 1,
|
|
||||||
dimension: TextureDimension::D2,
|
|
||||||
// One array contains both alpha-mask glyphs and colour glyphs.
|
|
||||||
// sRGB decoding leaves mask pages' white RGB and alpha unchanged
|
|
||||||
// while correctly decoding colour-glyph RGB.
|
|
||||||
format: TextureFormat::Rgba8UnormSrgb,
|
|
||||||
usage: TextureUsages::TEXTURE_BINDING
|
|
||||||
| TextureUsages::COPY_DST
|
|
||||||
| TextureUsages::COPY_SRC,
|
|
||||||
view_formats: &[],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(device: &Device, queue: &Queue) -> Self {
|
|
||||||
let sampler = default_sampler(device);
|
|
||||||
let null_view = null_texture_view(device);
|
|
||||||
let array_capacity = MIN_ARRAY_LAYERS;
|
|
||||||
let array_texture = Self::create_array_texture(device, array_capacity);
|
|
||||||
let array_view = array_texture.create_view(&TextureViewDescriptor {
|
|
||||||
dimension: Some(TextureViewDimension::D2Array),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
Self {
|
|
||||||
device: device.clone(),
|
|
||||||
queue: queue.clone(),
|
|
||||||
slots: Vec::new(),
|
|
||||||
array_texture,
|
|
||||||
array_view,
|
|
||||||
array_capacity,
|
|
||||||
layer_high_water: 0,
|
|
||||||
sampler,
|
|
||||||
null_view,
|
|
||||||
bind_group_creates: 0,
|
|
||||||
pages_grown: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn take_bind_group_creates(&mut self) -> u64 {
|
|
||||||
std::mem::take(&mut self.bind_group_creates)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn take_pages_grown(&mut self) -> u64 {
|
|
||||||
std::mem::take(&mut self.pages_grown)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn array_view(&self) -> &TextureView {
|
|
||||||
&self.array_view
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn null_view(&self) -> &TextureView {
|
|
||||||
&self.null_view
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sampler(&self) -> &Sampler {
|
|
||||||
&self.sampler
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The bind group a standalone image draws with. Panics if `idx` names an
|
|
||||||
/// atlas page or a freed slot instead -- either is a caller bug (the
|
|
||||||
/// wrong kind of instance reached this draw path), not a condition to
|
|
||||||
/// recover from.
|
|
||||||
pub fn image_bind_group(&self, idx: u32) -> &BindGroup {
|
|
||||||
match self.slots.get(idx as usize) {
|
|
||||||
Some(Slot::Image(gpu)) => &gpu.bind_group,
|
|
||||||
other => panic!("texture slot {idx} is not a live standalone image: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view_count(&self) -> usize {
|
|
||||||
self.slots
|
|
||||||
.iter()
|
|
||||||
.filter(|s| !matches!(s, Slot::Empty))
|
|
||||||
.count()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Slot {
|
/// The layout for one of those. The dimension differs -- the atlas is an
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
/// array of pages and an image is not -- and nothing else does.
|
||||||
match self {
|
pub fn sampled_layout(
|
||||||
Slot::Empty => write!(f, "Empty"),
|
device: &Device,
|
||||||
Slot::Image(_) => write!(f, "Image"),
|
dimension: TextureViewDimension,
|
||||||
Slot::Page(layer) => write!(f, "Page(layer={layer})"),
|
label: &'static str,
|
||||||
}
|
) -> BindGroupLayout {
|
||||||
}
|
device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||||
}
|
entries: &[
|
||||||
|
BindGroupLayoutEntry {
|
||||||
pub fn null_texture_view(device: &Device) -> TextureView {
|
binding: 0,
|
||||||
device
|
visibility: ShaderStages::FRAGMENT,
|
||||||
.create_texture(&TextureDescriptor {
|
ty: BindingType::Texture {
|
||||||
label: Some("null"),
|
sample_type: TextureSampleType::Float { filterable: false },
|
||||||
size: Extent3d {
|
view_dimension: dimension,
|
||||||
width: 1,
|
multisampled: false,
|
||||||
height: 1,
|
|
||||||
depth_or_array_layers: 1,
|
|
||||||
},
|
},
|
||||||
mip_level_count: 1,
|
count: None,
|
||||||
sample_count: 1,
|
},
|
||||||
dimension: TextureDimension::D2,
|
BindGroupLayoutEntry {
|
||||||
format: TextureFormat::Rgba8UnormSrgb,
|
binding: 1,
|
||||||
usage: TextureUsages::TEXTURE_BINDING,
|
visibility: ShaderStages::FRAGMENT,
|
||||||
view_formats: &[],
|
ty: BindingType::Sampler(SamplerBindingType::NonFiltering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
label: Some(label),
|
||||||
})
|
})
|
||||||
.create_view(&TextureViewDescriptor::default())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_sampler(device: &Device) -> Sampler {
|
pub fn default_sampler(device: &Device) -> Sampler {
|
||||||
|
|||||||
+19
-77
@@ -1,63 +1,47 @@
|
|||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use crate::util::Dirty;
|
|
||||||
use bytemuck::Pod;
|
use bytemuck::Pod;
|
||||||
use wgpu::*;
|
use wgpu::*;
|
||||||
|
|
||||||
/// **The buffer has a capacity, and shrinking never reallocates.** That
|
|
||||||
/// is not only about allocation cost: a fresh `Buffer`'s contents are
|
|
||||||
/// undefined, so a reallocation is the one event after which a *partial*
|
|
||||||
/// upload is not correct. Keeping the buffer alive across a length change
|
|
||||||
/// is therefore the precondition for uploading only what changed, and
|
|
||||||
/// [`Self::update`] says which of the two happened so a caller can force
|
|
||||||
/// the whole range dirty.
|
|
||||||
pub struct ArrBuf<T: Pod> {
|
pub struct ArrBuf<T: Pod> {
|
||||||
label: &'static str,
|
label: &'static str,
|
||||||
usage: BufferUsages,
|
usage: BufferUsages,
|
||||||
pub buffer: Buffer,
|
pub buffer: Buffer,
|
||||||
len: usize,
|
len: usize,
|
||||||
capacity: usize,
|
|
||||||
_pd: PhantomData<T>,
|
_pd: PhantomData<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The smallest allocation worth making, in entries. A buffer that starts
|
|
||||||
/// at the exact first length reallocates on the second frame of anything;
|
|
||||||
/// this is small enough to be free and large enough that a handful of
|
|
||||||
/// masks or move offsets never grows at all.
|
|
||||||
const MIN_CAPACITY: usize = 64;
|
|
||||||
|
|
||||||
impl<T: Pod> ArrBuf<T> {
|
impl<T: Pod> ArrBuf<T> {
|
||||||
pub fn new(device: &Device, usage: BufferUsages, label: &'static str) -> Self {
|
pub fn new(device: &Device, usage: BufferUsages, label: &'static str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
label,
|
label,
|
||||||
usage,
|
usage,
|
||||||
buffer: Self::init_buf(device, MIN_CAPACITY, usage, label),
|
buffer: Self::init_buf(device, 0, usage, label),
|
||||||
len: 0,
|
len: 0,
|
||||||
capacity: MIN_CAPACITY,
|
|
||||||
_pd: PhantomData,
|
_pd: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Returns whether the `Buffer` was recreated, which stales any cached
|
||||||
pub fn reserve(&mut self, device: &Device, len: usize) -> bool {
|
/// `BindGroup` holding it.
|
||||||
if len <= self.capacity {
|
pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool {
|
||||||
return false;
|
let resized = self.len != data.len();
|
||||||
|
if resized {
|
||||||
|
self.len = data.len();
|
||||||
|
self.buffer =
|
||||||
|
Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label);
|
||||||
}
|
}
|
||||||
let mut capacity = self.capacity.max(MIN_CAPACITY);
|
queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data));
|
||||||
while capacity < len {
|
resized
|
||||||
capacity *= 2;
|
|
||||||
}
|
}
|
||||||
self.capacity = capacity;
|
pub fn len(&self) -> usize {
|
||||||
self.buffer = Self::init_buf(device, capacity, self.usage, self.label);
|
self.len
|
||||||
true
|
}
|
||||||
|
fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer {
|
||||||
|
let mut size = size as u64;
|
||||||
|
if usage.contains(BufferUsages::STORAGE) {
|
||||||
|
// A binding cannot be empty or under the layout's minimum.
|
||||||
|
size = size.max(std::mem::size_of::<T>() as u64);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_buf(
|
|
||||||
device: &Device,
|
|
||||||
entries: usize,
|
|
||||||
usage: BufferUsages,
|
|
||||||
label: &'static str,
|
|
||||||
) -> Buffer {
|
|
||||||
let size = (entries.max(1) * std::mem::size_of::<T>()) as u64;
|
|
||||||
device.create_buffer(&BufferDescriptor {
|
device.create_buffer(&BufferDescriptor {
|
||||||
label: Some(label),
|
label: Some(label),
|
||||||
size,
|
size,
|
||||||
@@ -65,46 +49,4 @@ impl<T: Pod> ArrBuf<T> {
|
|||||||
usage,
|
usage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes the entries `dirty` names and clears it, answering whether
|
|
||||||
/// the underlying `Buffer` was **recreated** -- which a caller holding
|
|
||||||
/// a `BindGroup` over it must know, since it has to rebuild that group.
|
|
||||||
///
|
|
||||||
/// Correct only because the buffer outlives the data in it: a
|
|
||||||
/// reallocation leaves the rest of the buffer undefined, which is why
|
|
||||||
/// one forces the whole range dirty here rather than leaving the
|
|
||||||
/// caller to remember. Measured over the bench fixture
|
|
||||||
/// (`scripts/rigs/ui-profile`'s `arena_churn`): a fling writes 3.3% of
|
|
||||||
/// what the whole-array path wrote, and the median frame writes
|
|
||||||
/// nothing at all.
|
|
||||||
pub fn update(
|
|
||||||
&mut self,
|
|
||||||
device: &Device,
|
|
||||||
queue: &Queue,
|
|
||||||
data: &[T],
|
|
||||||
dirty: &mut Dirty,
|
|
||||||
) -> bool {
|
|
||||||
let reallocated = self.reserve(device, data.len());
|
|
||||||
if reallocated {
|
|
||||||
dirty.mark_all();
|
|
||||||
}
|
|
||||||
self.len = data.len();
|
|
||||||
let stride = std::mem::size_of::<T>() as BufferAddress;
|
|
||||||
dirty.for_each_range(data.len(), Self::MERGE_GAP, |range| {
|
|
||||||
queue.write_buffer(
|
|
||||||
&self.buffer,
|
|
||||||
range.start as BufferAddress * stride,
|
|
||||||
bytemuck::cast_slice(&data[range]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
dirty.clear();
|
|
||||||
reallocated
|
|
||||||
}
|
|
||||||
|
|
||||||
const MERGE_GAP: usize = 1024 / std::mem::size_of::<T>();
|
|
||||||
|
|
||||||
#[allow(clippy::len_without_is_empty)]
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.len
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
use crate::{PixelRegion, UiRenderState, UiRsc, WidgetId, Widgets, util::HashMap};
|
|
||||||
use accesskit::{Node, NodeId, Rect, Role, TreeId, TreeInfo, TreeUpdate};
|
|
||||||
|
|
||||||
/// Reserved for the synthetic root; every real widget's `SlotId::as_u64`
|
|
||||||
/// starts at 1, so this can never collide with one (see that method's
|
|
||||||
/// doc comment).
|
|
||||||
const WINDOW_NODE: NodeId = NodeId(0);
|
|
||||||
|
|
||||||
fn node_id(id: WidgetId) -> NodeId {
|
|
||||||
NodeId(id.as_u64())
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Entry {
|
|
||||||
name: String,
|
|
||||||
role: Role,
|
|
||||||
bounds: PixelRegion,
|
|
||||||
seen: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn entry_node(entry: &Entry) -> Node {
|
|
||||||
let mut node = Node::new(entry.role);
|
|
||||||
node.set_label(entry.name.clone());
|
|
||||||
node.set_bounds(Rect {
|
|
||||||
x0: entry.bounds.top_left.x as f64,
|
|
||||||
y0: entry.bounds.top_left.y as f64,
|
|
||||||
x1: entry.bounds.bot_right.x as f64,
|
|
||||||
y1: entry.bounds.bot_right.y as f64,
|
|
||||||
});
|
|
||||||
node
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Owns the last tree pushed out, so `update` can tell "nothing
|
|
||||||
/// accessibility-relevant changed" from "something did" without asking
|
|
||||||
/// the platform adapter to diff two `Node`s itself. One of these per
|
|
||||||
/// window/view -- `desktop::DesktopUiState` and `android::AndroidUiState`
|
|
||||||
/// each keep one.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct AccessTree {
|
|
||||||
known: HashMap<WidgetId, Entry>,
|
|
||||||
generation: u64,
|
|
||||||
rebuilds: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AccessTree {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Refresh the retained accessibility state and report whether it changed.
|
|
||||||
/// Existing entries are updated in place so an ordinary frame allocates
|
|
||||||
/// nothing, including one where only bounds changed.
|
|
||||||
pub fn refresh(&mut self, widgets: &Widgets, render: &UiRenderState, rsc: &dyn UiRsc) -> bool {
|
|
||||||
self.generation = self.generation.wrapping_add(1);
|
|
||||||
if self.generation == 0 {
|
|
||||||
self.known.clear();
|
|
||||||
self.generation = 1;
|
|
||||||
}
|
|
||||||
let generation = self.generation;
|
|
||||||
let mut changed = false;
|
|
||||||
for id in widgets.named() {
|
|
||||||
let Some(bounds) = render.window_region(&id, rsc) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Some(widget) = widgets.get_dyn(id) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let name = widgets.label(id);
|
|
||||||
let role = widget.access_role();
|
|
||||||
match self.known.get_mut(&id) {
|
|
||||||
Some(entry) => {
|
|
||||||
if entry.name != *name {
|
|
||||||
entry.name.clone_from(name);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if entry.role != role || entry.bounds != bounds {
|
|
||||||
entry.role = role;
|
|
||||||
entry.bounds = bounds;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
entry.seen = generation;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
self.known.insert(
|
|
||||||
id,
|
|
||||||
Entry {
|
|
||||||
name: name.clone(),
|
|
||||||
role,
|
|
||||||
bounds,
|
|
||||||
seen: generation,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let old_len = self.known.len();
|
|
||||||
self.known.retain(|_, entry| entry.seen == generation);
|
|
||||||
changed |= self.known.len() != old_len;
|
|
||||||
if changed {
|
|
||||||
self.rebuilds += 1;
|
|
||||||
}
|
|
||||||
changed
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walks `widgets.named()`, looks up each one's current screen bounds
|
|
||||||
/// via `render.window_region` (which resolves the same move-chain
|
|
||||||
/// `resolved_region` does, so a moved subtree reports where it
|
|
||||||
/// actually is), and returns a full `TreeUpdate` if and only if that
|
|
||||||
/// set differs from the last call -- added, removed, renamed, or
|
|
||||||
/// moved/resized. A widget that is named but not currently active
|
|
||||||
/// (not drawn this frame) is left out, the same as one never named at
|
|
||||||
/// all.
|
|
||||||
pub fn update(
|
|
||||||
&mut self,
|
|
||||||
widgets: &Widgets,
|
|
||||||
render: &UiRenderState,
|
|
||||||
rsc: &dyn UiRsc,
|
|
||||||
) -> Option<TreeUpdate> {
|
|
||||||
if !self.refresh(widgets, render, rsc) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(self.tree_update())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tree_update(&self) -> TreeUpdate {
|
|
||||||
build_update(&self.known)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The unconditional twin of `update`, for a platform adapter's
|
|
||||||
/// activation handler (`android/access.rs`'s `AndroidAccessSource`) --
|
|
||||||
/// AccessKit asks for a full tree the first time a client attaches,
|
|
||||||
/// which is exactly the case `update`'s diff-against-`known` is not
|
|
||||||
/// meant to answer (it may have already sent this same snapshot to a
|
|
||||||
/// client that has since detached and reattached).
|
|
||||||
pub fn build_full(widgets: &Widgets, render: &UiRenderState, rsc: &dyn UiRsc) -> TreeUpdate {
|
|
||||||
let mut tree = Self::new();
|
|
||||||
tree.refresh(widgets, render, rsc);
|
|
||||||
tree.tree_update()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn take_rebuilds(&mut self) -> u64 {
|
|
||||||
std::mem::take(&mut self.rebuilds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_update(current: &HashMap<WidgetId, Entry>) -> TreeUpdate {
|
|
||||||
let mut window = Node::new(Role::Window);
|
|
||||||
let mut nodes = Vec::with_capacity(current.len() + 1);
|
|
||||||
for (&id, entry) in current {
|
|
||||||
window.push_child(node_id(id));
|
|
||||||
nodes.push((node_id(id), entry_node(entry)));
|
|
||||||
}
|
|
||||||
nodes.push((WINDOW_NODE, window));
|
|
||||||
TreeUpdate {
|
|
||||||
nodes,
|
|
||||||
tree: Some(TreeInfo::new(WINDOW_NODE)),
|
|
||||||
tree_id: TreeId::ROOT,
|
|
||||||
focus: WINDOW_NODE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+78
-22
@@ -1,31 +1,87 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
LayerId, MaskIdx, MoveIdx, PaintId, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId,
|
LayerId, LayoutHolds, LayoutLen, MaskIdx, MoveIdx, PlaceDesc, RegionAlign, RetainedPrimitive,
|
||||||
util::Vec2,
|
Size, TextureHandle, UiRegion, UiVec2, WidgetId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// What is kept of a widget its parent has asked about. `drawn` says whether
|
||||||
|
/// it currently draws; one that does not is kept so that a change to it, or
|
||||||
|
/// under it, still reaches whoever asked.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ActiveData {
|
pub struct ActiveData {
|
||||||
pub id: WidgetId,
|
pub id: WidgetId,
|
||||||
|
/// Where its drawing goes, in its region node's coordinates.
|
||||||
|
pub placement: UiRegion,
|
||||||
|
/// What a fraction declared or reported under this widget is a fraction
|
||||||
|
/// of, as a length of the window.
|
||||||
|
pub rel_base: UiVec2,
|
||||||
|
/// Where its drawing was put, and where it was asked. The two differ
|
||||||
|
/// where a container asks in one place and puts the answer in another --
|
||||||
|
/// a row measures from its cursor and puts the child in its slot. Each
|
||||||
|
/// carries the rel base that ask stated, so asking again from either is
|
||||||
|
/// the same question it was.
|
||||||
|
pub placed: PlaceDesc,
|
||||||
|
pub asked: PlaceDesc,
|
||||||
|
/// The box it was asked in, in the parent's region-node coordinates: the
|
||||||
|
/// box its drawing was made in and the one its contract is about. Its
|
||||||
|
/// drawing is placed elsewhere by re-expression, never by asking again.
|
||||||
pub region: UiRegion,
|
pub region: UiRegion,
|
||||||
pub parent: Option<WidgetId>,
|
/// The measured answer and its dependencies. A hint-only dependency or
|
||||||
pub textures: Vec<TextureHandle>,
|
/// a widget first encountered during placement has no measurement yet.
|
||||||
pub(crate) spare_textures: Vec<TextureHandle>,
|
pub answer: Option<(Size, LayoutHolds)>,
|
||||||
/// Paint slots retained by this draw. The GPU primitive stores only the
|
/// Asked more than once in its parent's last draw -- measured in one box
|
||||||
/// slot index, so these handles are what prevent a live primitive from
|
/// and then asked in the one the parent decided. The parent's layout
|
||||||
/// observing a recycled paint.
|
/// rests on the first answer and its drawing on the last, so only the
|
||||||
pub paints: Vec<PaintId>,
|
/// parent can ask either again.
|
||||||
pub(crate) spare_paints: Vec<PaintId>,
|
pub re_asked: bool,
|
||||||
pub primitives: Vec<PrimitiveHandle>,
|
/// What the widget reported, in window-unit lengths.
|
||||||
pub(crate) spare_primitives: Vec<PrimitiveHandle>,
|
|
||||||
pub children: Vec<WidgetId>,
|
|
||||||
pub(crate) spare_children: Vec<WidgetId>,
|
|
||||||
pub size_dependencies: Vec<WidgetId>,
|
|
||||||
pub mask: MaskIdx,
|
|
||||||
/// The widget's retained mask slot, or `MaskIdx::NONE`.
|
|
||||||
pub own_mask: MaskIdx,
|
|
||||||
pub layer: LayerId,
|
|
||||||
pub size: Size,
|
pub size: Size,
|
||||||
pub move_slot: MoveIdx,
|
/// The window and region reads that this drawing holds for, and the
|
||||||
pub child_move_slot: Option<MoveIdx>,
|
/// rel base and region it pinned.
|
||||||
pub move_applied: Vec2,
|
pub holds: LayoutHolds,
|
||||||
|
pub drawn: bool,
|
||||||
|
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>,
|
||||||
|
/// Its primitives, each keeping the box it was written in -- in this
|
||||||
|
/// widget's placement coordinates, which is what a move recomposes from.
|
||||||
|
pub primitives: Vec<RetainedPrimitive>,
|
||||||
|
/// An owned mask holds one reference independently of its primitives.
|
||||||
|
pub mask_region: Option<UiRegion>,
|
||||||
|
pub children: Vec<WidgetId>,
|
||||||
|
/// The children whose size this widget read while drawing.
|
||||||
|
pub size_deps: Vec<WidgetId>,
|
||||||
|
/// The movable region its primitives are positioned through: its own when
|
||||||
|
/// opted in, otherwise the nearest ancestor's.
|
||||||
|
pub move_idx: MoveIdx,
|
||||||
|
/// The declared lengths whoever drew this widget resolved into its rel base.
|
||||||
|
/// A change to one moves a box this widget cannot fix by drawing again,
|
||||||
|
/// and comparing them is what says so.
|
||||||
|
pub declared: [Option<LayoutLen>; 2],
|
||||||
|
/// Its alignment when it was last drawn, which a change to the property
|
||||||
|
/// is found against.
|
||||||
|
pub own_align: RegionAlign,
|
||||||
|
/// The movable region whose coordinates its placement is in when this
|
||||||
|
/// widget does not own a region node.
|
||||||
|
pub parent_move: MoveIdx,
|
||||||
|
/// The mask its drawing is clipped to: one it set itself, or the one it
|
||||||
|
/// inherited from whoever drew it.
|
||||||
|
pub mask: MaskIdx,
|
||||||
|
/// That inherited one. The two differ exactly where the widget set a
|
||||||
|
/// mask of its own, which is the one it owns and the one a move rewrites
|
||||||
|
/// -- and the one a redraw of it must not be handed back, since setting
|
||||||
|
/// a mask asserts there is none.
|
||||||
|
pub parent_mask: MaskIdx,
|
||||||
|
pub layer: LayerId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveData {
|
||||||
|
/// What it answered when its parent asked, where it has been asked at
|
||||||
|
/// all. Not `size`, which is what its last drawing reported: a drawing
|
||||||
|
/// re-expressed in the box that answer chose is not a second answer.
|
||||||
|
pub fn measured(&self) -> Option<Size> {
|
||||||
|
self.answer.map(|(size, _)| size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
use crate::{Len, Px, REL_SHIFT, fixed::div_toward, fixed::narrow};
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
/// The lengths of a box, in pixels, that one drawing of a widget holds for:
|
||||||
|
/// give the widget any box in this range and it draws the same thing and
|
||||||
|
/// reports the same size. A widget that never reads its box in pixels holds
|
||||||
|
/// for every length; one that does holds for the one it read unless it says
|
||||||
|
/// otherwise, and a parent holds for whatever keeps every child it asked
|
||||||
|
/// about or drew inside its own range.
|
||||||
|
///
|
||||||
|
/// The ends are lengths on the grid rather than floats with a tolerance
|
||||||
|
/// around them: a box offered back at the length a widget reported comes back
|
||||||
|
/// as the same number, so a range means what it says. The one place a range
|
||||||
|
/// is wider than the length it came from is [`Self::through`], and what it is
|
||||||
|
/// wider by is the floor that inverting a fraction undoes.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Holds {
|
||||||
|
pub lo: Px,
|
||||||
|
pub hi: Px,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Holds {
|
||||||
|
pub const ANY: Self = Self {
|
||||||
|
lo: Px::MIN,
|
||||||
|
hi: Px::MAX,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const fn at(len: Px) -> Self {
|
||||||
|
Self { lo: len, hi: len }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn contains(&self, len: Px) -> bool {
|
||||||
|
len.raw() >= self.lo.raw() && len.raw() <= self.hi.raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn and(self, other: Self) -> Self {
|
||||||
|
Self {
|
||||||
|
lo: self.lo.max(other.lo),
|
||||||
|
hi: self.hi.min(other.hi),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a box has to be for a part of it, `len` of the box long, to stay
|
||||||
|
/// in this range: the exact preimage of `px + floor(rel * box)`, which is
|
||||||
|
/// the one way a box in pixels is reached. A part with no relative extent
|
||||||
|
/// is a fixed length -- it was drawn at that length and any box keeps it
|
||||||
|
/// there.
|
||||||
|
///
|
||||||
|
/// The answer is an interval even where this range is a single length,
|
||||||
|
/// because the multiply on the way in drops to the step below and many
|
||||||
|
/// boxes therefore give one length. That is a floor rather than an
|
||||||
|
/// allowance: inverting it is two divisions and nothing else, and the
|
||||||
|
/// whole of a box maps back to itself.
|
||||||
|
pub const fn through(self, len: Len) -> Self {
|
||||||
|
if self.lo.raw() == Px::MIN.raw() && self.hi.raw() == Px::MAX.raw() {
|
||||||
|
return Self::ANY;
|
||||||
|
}
|
||||||
|
let rel = len.rel.raw() as i64;
|
||||||
|
if rel == 0 {
|
||||||
|
return Self::ANY;
|
||||||
|
}
|
||||||
|
let px = len.px.raw() as i64;
|
||||||
|
// `floor(rel * box) >= lo - px` is `rel * box >= (lo - px) << REL`, and
|
||||||
|
// `floor(rel * box) <= hi - px` is `rel * box < (hi - px + 1) << REL`.
|
||||||
|
let lo = (self.lo.raw() as i64 - px) << REL_SHIFT;
|
||||||
|
let hi = (((self.hi.raw() as i64 - px) + 1) << REL_SHIFT) - 1;
|
||||||
|
// Dividing by a negative fraction turns the ends around, so which
|
||||||
|
// bound each comes from is decided before dividing rather than by
|
||||||
|
// taking the min and max of four divisions.
|
||||||
|
match rel > 0 {
|
||||||
|
true => Self::raws(div_toward(lo, rel, true), div_toward(hi, rel, false)),
|
||||||
|
false => Self::raws(div_toward(hi, rel, true), div_toward(lo, rel, false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn raws(lo: i64, hi: i64) -> Self {
|
||||||
|
Self {
|
||||||
|
lo: Px::from_raw(narrow(lo)),
|
||||||
|
hi: Px::from_raw(narrow(hi)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RangeInclusive<Px>> for Holds {
|
||||||
|
fn from(range: RangeInclusive<Px>) -> Self {
|
||||||
|
Self {
|
||||||
|
lo: *range.start(),
|
||||||
|
hi: *range.end(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Rel;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_unrestricted_range_stays_unrestricted_through_any_length() {
|
||||||
|
for rel in [-2.0, -0.5, 0.0, 0.5, 1.0, 2.0] {
|
||||||
|
for px in [-8, 0, 8] {
|
||||||
|
let len = Len::from_parts(Rel::from_f32(rel), Px::from_int(px));
|
||||||
|
assert_eq!(Holds::ANY.through(len), Holds::ANY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn through_reverses_a_range_for_a_negative_fraction() {
|
||||||
|
// `10 - box / 2` is between 20 and 40 for boxes from -60 to -20.
|
||||||
|
let part = Len::from_parts(Rel::from_f32(-0.5), Px::from_int(10));
|
||||||
|
let holds = Holds::from(Px::from_int(20)..=Px::from_int(40)).through(part);
|
||||||
|
assert!(holds.contains(Px::from_int(-60)) && holds.contains(Px::from_int(-20)));
|
||||||
|
assert!(!holds.contains(Px::from_int(-61)) && !holds.contains(Px::from_int(-19)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The case the widening is for: a part that holds only for the length it
|
||||||
|
/// was drawn at has to hold for the box it was drawn in, and a third of a
|
||||||
|
/// box is not a whole number of steps.
|
||||||
|
#[test]
|
||||||
|
fn a_part_maps_back_onto_the_box_it_was_measured_in() {
|
||||||
|
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
|
||||||
|
for box_len in (440..460).map(Px::from_int) {
|
||||||
|
let holds = Holds::at(part.to_px(box_len)).through(part);
|
||||||
|
assert!(holds.contains(box_len), "{box_len:?} left out by {holds:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget handed the whole of its parent's box, with or without pixels
|
||||||
|
/// taken off it, has no fraction to invert: multiplying by one is exact
|
||||||
|
/// and taking the pixels off again is too, so the box maps back to
|
||||||
|
/// itself. Allowing for anything here compounded a step a level down a
|
||||||
|
/// chain of widgets each taking the whole of its parent.
|
||||||
|
#[test]
|
||||||
|
fn the_whole_of_a_box_maps_back_to_itself() {
|
||||||
|
let at = Px::from_int(956);
|
||||||
|
assert_eq!(Holds::at(at).through(Len::FULL), Holds::at(at));
|
||||||
|
let less_eight = Len::from_parts(Rel::ONE, Px::from_int(-8));
|
||||||
|
assert_eq!(
|
||||||
|
Holds::at(at).through(less_eight),
|
||||||
|
Holds::at(at + Px::from_int(8))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The range is the exact preimage at both ends, so a box one step
|
||||||
|
/// outside it really does give a length outside this range. What a wider
|
||||||
|
/// range costs is a drawing reused where it does not hold.
|
||||||
|
#[test]
|
||||||
|
fn a_box_one_step_outside_the_range_is_outside_it() {
|
||||||
|
let part = Len::from_parts(Rel::from_f32(1.0 / 3.0), Px::from_int(-146));
|
||||||
|
let at = Px::from_int(300);
|
||||||
|
let holds = Holds::at(at).through(part);
|
||||||
|
for inside in [holds.lo, holds.hi] {
|
||||||
|
assert_eq!(part.to_px(inside), at, "{inside:?} left out of {holds:?}");
|
||||||
|
}
|
||||||
|
for outside in [holds.lo.next_down(), holds.hi.next_up()] {
|
||||||
|
assert_ne!(part.to_px(outside), at, "{outside:?} admitted by {holds:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A truncating multiply only ever drops, so the step it needs allowing
|
||||||
|
/// for on the way in belongs at the top of the range and not the bottom.
|
||||||
|
#[test]
|
||||||
|
fn a_fraction_widens_further_up_than_down() {
|
||||||
|
let half = Len::from_parts(Rel::from_f32(0.5), Px::ZERO);
|
||||||
|
let holds = Holds::at(Px::from_int(100)).through(half);
|
||||||
|
let box_len = Px::from_int(200);
|
||||||
|
assert!(holds.hi - box_len > box_len - holds.lo, "{holds:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_boundary_the_next_step_along_does_not_admit_it() {
|
||||||
|
let boundary = Px::from_int(10);
|
||||||
|
let above = Holds::from(boundary.next_up()..=Px::MAX);
|
||||||
|
assert!(!above.contains(boundary));
|
||||||
|
assert!(above.contains(boundary.next_up()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
use crate::{Axis, Holds, Len, PxVec2, UiRegion, UiVec2};
|
||||||
|
|
||||||
|
const AXES: [Axis; 2] = [Axis::X, Axis::Y];
|
||||||
|
|
||||||
|
/// What one evaluation of a widget depends on: the window lengths its reads
|
||||||
|
/// hold for, the pixel lengths of its own box, and the symbolic lengths of
|
||||||
|
/// that box and of its rel base where either one is what it was expressed in.
|
||||||
|
///
|
||||||
|
/// The symbolic lengths are pins rather than ranges: a container places its
|
||||||
|
/// children as lengths of its rel base measured from where its own box starts,
|
||||||
|
/// so what it draws turns on that box's length and on nothing about where it
|
||||||
|
/// is. A box pin reaches the parent only where the box it pinned is the
|
||||||
|
/// parent's own; anywhere else the parent chose that length itself, and a
|
||||||
|
/// widget pinned this way is checked when it is re-placed.
|
||||||
|
///
|
||||||
|
/// A rel base pin says the answer or the drawing is a fraction of the rel base,
|
||||||
|
/// which is a different length wherever the rel base is a different one -- at
|
||||||
|
/// the same window size, so no range of window pixels can say it. A length
|
||||||
|
/// of the rel base that is only pixels is not one: it is that many pixels
|
||||||
|
/// whatever the rel base turns out to be.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct LayoutHolds {
|
||||||
|
pub window: [Holds; 2],
|
||||||
|
pub rel_base: [Option<Len>; 2],
|
||||||
|
pub region: [Holds; 2],
|
||||||
|
pub region_len: [Option<Len>; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutHolds {
|
||||||
|
pub const ANY: Self = Self {
|
||||||
|
window: [Holds::ANY; 2],
|
||||||
|
rel_base: [None; 2],
|
||||||
|
region: [Holds::ANY; 2],
|
||||||
|
region_len: [None; 2],
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn and(self, other: Self) -> Self {
|
||||||
|
let mut result = Self::ANY;
|
||||||
|
for n in 0..2 {
|
||||||
|
result.window[n] = self.window[n].and(other.window[n]);
|
||||||
|
result.region[n] = self.region[n].and(other.region[n]);
|
||||||
|
debug_assert!(
|
||||||
|
self.region_len[n].is_none()
|
||||||
|
|| other.region_len[n].is_none()
|
||||||
|
|| self.region_len[n] == other.region_len[n]
|
||||||
|
);
|
||||||
|
debug_assert!(
|
||||||
|
self.rel_base[n].is_none()
|
||||||
|
|| other.rel_base[n].is_none()
|
||||||
|
|| self.rel_base[n] == other.rel_base[n]
|
||||||
|
);
|
||||||
|
result.region_len[n] = self.region_len[n].or(other.region_len[n]);
|
||||||
|
result.rel_base[n] = self.rel_base[n].or(other.rel_base[n]);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn covers(self, other: Self) -> bool {
|
||||||
|
(0..2).all(|n| {
|
||||||
|
self.window[n].lo <= other.window[n].lo
|
||||||
|
&& self.window[n].hi >= other.window[n].hi
|
||||||
|
&& self.region[n].lo <= other.region[n].lo
|
||||||
|
&& self.region[n].hi >= other.region[n].hi
|
||||||
|
&& self.region_len[n].is_none_or(|len| other.region_len[n] == Some(len))
|
||||||
|
&& self.rel_base[n].is_none_or(|len| other.rel_base[n] == Some(len))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(self, window: PxVec2, rel_base: UiVec2, region: UiRegion) -> bool {
|
||||||
|
AXES.into_iter().all(|axis| {
|
||||||
|
let n = axis as usize;
|
||||||
|
let len = region.axis(axis).len();
|
||||||
|
self.window[n].contains(window.axis(axis))
|
||||||
|
&& self.rel_base[n].is_none_or(|pinned| pinned == rel_base.axis(axis))
|
||||||
|
&& self.region[n].contains(len.to_px(window.axis(axis)))
|
||||||
|
&& self.region_len[n].is_none_or(|pinned| pinned == len)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
-152
@@ -1,193 +1,129 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Mask, MoveOffset, Paints, TextResources, Textures, WeakWidget, WidgetId, Widgets,
|
Mask, MoveIdx, MoveOffset, PrimitiveRegistry, TextData, Textures, UiRegion, WeakWidget,
|
||||||
util::TrackedArena,
|
WidgetId, Widgets,
|
||||||
};
|
util::{Arena, Id, TrackedArena},
|
||||||
use std::{
|
|
||||||
cell::{Ref, RefCell, RefMut},
|
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
rc::Rc,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mod access;
|
/// 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 holds;
|
||||||
|
mod layout_holds;
|
||||||
mod painter;
|
mod painter;
|
||||||
|
mod place;
|
||||||
mod render_state;
|
mod render_state;
|
||||||
|
|
||||||
pub use access::*;
|
|
||||||
pub use active::*;
|
pub use active::*;
|
||||||
pub use painter::{DrawResult, Painter};
|
pub use holds::*;
|
||||||
|
pub use layout_holds::*;
|
||||||
|
pub use painter::{Painter, PrimitiveLike};
|
||||||
|
pub use place::*;
|
||||||
pub use render_state::*;
|
pub use render_state::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct UiData {
|
pub struct UiData {
|
||||||
pub widgets: Widgets,
|
pub widgets: Widgets,
|
||||||
pub paints: Paints,
|
/// Every primitive this ui can draw.
|
||||||
|
pub primitives: PrimitiveRegistry,
|
||||||
pub textures: Textures,
|
pub textures: Textures,
|
||||||
pub text: Rc<RefCell<TextResources>>,
|
pub text: TextData,
|
||||||
pub masks: TrackedArena<Mask, u32>,
|
pub masks: TrackedArena<Mask, u32>,
|
||||||
pub move_offsets: TrackedArena<MoveOffset, u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct RenderHandle {
|
|
||||||
pub(crate) render_state: Rc<RefCell<UiRenderState>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderHandle {
|
|
||||||
/// The retained result of the last completed frame. The framework holds
|
|
||||||
/// the corresponding mutable borrow for the whole of a render update, so
|
|
||||||
/// a read attempted while that state is incomplete fails at the boundary
|
|
||||||
/// instead of observing half a frame.
|
|
||||||
pub fn get(&self) -> Ref<'_, UiRenderState> {
|
|
||||||
self.render_state
|
|
||||||
.try_borrow()
|
|
||||||
.expect("render state cannot be read while a frame is being rendered")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_mut(&self) -> RefMut<'_, UiRenderState> {
|
|
||||||
self.render_state
|
|
||||||
.try_borrow_mut()
|
|
||||||
.expect("render state cannot be mutated while it is being read")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RenderHandle {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
render_state: Rc::new(RefCell::new(UiRenderState::new())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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)]
|
#[derive(Default)]
|
||||||
pub struct Ui {
|
pub struct Moves {
|
||||||
data: UiData,
|
arena: Arena<MoveOffset, u32>,
|
||||||
pub(crate) render_state: RenderHandle,
|
pub changed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ui {
|
impl Moves {
|
||||||
/// Register application-owned font data for a semantic or named family.
|
pub fn push(&mut self, parent: MoveIdx, region: UiRegion) -> MoveIdx {
|
||||||
/// Existing text resources are invalidated and their active widgets are
|
self.changed = true;
|
||||||
/// scheduled for layout again.
|
MoveIdx::slot(self.arena.push(MoveOffset::new(parent, region)).idx())
|
||||||
#[track_caller]
|
|
||||||
pub fn register_font(
|
|
||||||
&mut self,
|
|
||||||
family: impl AsRef<str>,
|
|
||||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
|
||||||
) -> Result<(), crate::FontRegistrationError> {
|
|
||||||
self.update_fonts(|text| text.register_font(family, data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register application-owned font data in a named glyph-atlas bucket.
|
/// Re-points a slot at a different parent, for a widget drawn somewhere
|
||||||
/// Fonts registered without this method share the default bucket.
|
/// else in the tree than it was.
|
||||||
#[track_caller]
|
pub fn set_parent(&mut self, idx: MoveIdx, parent: MoveIdx) {
|
||||||
pub fn register_font_in(
|
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
|
||||||
&mut self,
|
if entry.parent != parent {
|
||||||
family: impl AsRef<str>,
|
entry.parent = parent;
|
||||||
bucket: impl AsRef<str>,
|
self.changed = true;
|
||||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
}
|
||||||
) -> Result<(), crate::FontRegistrationError> {
|
|
||||||
self.update_fonts(|text| text.register_font_in(family, bucket, data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace an application's registered font while retaining its atlas
|
pub fn remove(&mut self, idx: MoveIdx) {
|
||||||
/// bucket. Text is reshaped and the bucket's old glyph pages are released.
|
self.changed = true;
|
||||||
#[track_caller]
|
self.arena.remove(Id::preset(idx.idx() as u32));
|
||||||
pub fn replace_font(
|
|
||||||
&mut self,
|
|
||||||
family: impl AsRef<str>,
|
|
||||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
|
||||||
) -> Result<(), crate::FontRegistrationError> {
|
|
||||||
self.update_fonts(|text| text.replace_font(family, data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace an application's registered font and assign the replacement to
|
/// Sets the box a slot's contents are placed within, itself given in the
|
||||||
/// a named glyph-atlas bucket.
|
/// coordinates of its parent slot.
|
||||||
#[track_caller]
|
pub fn set(&mut self, idx: MoveIdx, region: UiRegion) {
|
||||||
pub fn replace_font_in(
|
let entry = self.arena.get_mut(Id::preset(idx.idx() as u32));
|
||||||
&mut self,
|
if entry.region != region {
|
||||||
family: impl AsRef<str>,
|
entry.region = region;
|
||||||
bucket: impl AsRef<str>,
|
self.changed = true;
|
||||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
}
|
||||||
) -> Result<(), crate::FontRegistrationError> {
|
|
||||||
self.update_fonts(|text| text.replace_font_in(family, bucket, data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_fonts(
|
/// The same walk the vertex shader does, in the same `Len` the shader is
|
||||||
&mut self,
|
/// handed, for asking where a drawing will actually land -- hit testing,
|
||||||
update: impl FnOnce(&mut TextResources) -> Result<(), crate::FontRegistrationError>,
|
/// and nothing layout decides on. Layout threads its lengths down the
|
||||||
) -> Result<(), crate::FontRegistrationError> {
|
/// draw instead, so no box it compares is composed back up this chain.
|
||||||
let owners = {
|
pub fn resolve(&self, idx: MoveIdx, local: UiRegion) -> UiRegion {
|
||||||
let mut text = self.data.text.borrow_mut();
|
let mut region = local;
|
||||||
update(&mut text)?;
|
self.walk(idx, |entry| region = region.within(entry));
|
||||||
text.invalidate_all()
|
region
|
||||||
};
|
|
||||||
let mut active = owners;
|
|
||||||
{
|
|
||||||
let render = self.render_state.get();
|
|
||||||
active.retain(|owner| render.active.contains_key(owner));
|
|
||||||
}
|
|
||||||
self.data.widgets.needs_redraw.extend(active);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_font_registered(&self, family: impl AsRef<str>) -> bool {
|
fn walk(&self, idx: MoveIdx, mut step: impl FnMut(&UiRegion)) {
|
||||||
self.data.text.borrow().is_font_registered(family)
|
let mut at = idx;
|
||||||
|
for _ in 0..CHAIN_LIMIT {
|
||||||
|
if at == MoveIdx::NONE {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let entry = &self.arena[at.idx()];
|
||||||
|
step(&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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A read-only handle to the retained result of the last completed frame.
|
/// How many slots a region in `idx` is composed through, which is what
|
||||||
/// The handle is owned so a caller may keep its read guard while mutating
|
/// the shader's walk costs per primitive.
|
||||||
/// unrelated resources on the `Rsc` that owns this `Ui`.
|
pub fn depth(&self, idx: MoveIdx) -> usize {
|
||||||
pub fn render_state(&self) -> RenderHandle {
|
let mut depth = 0;
|
||||||
self.render_state.clone()
|
let mut at = idx;
|
||||||
|
while at != MoveIdx::NONE && depth < CHAIN_LIMIT as usize {
|
||||||
|
at = self.arena[at.idx()].parent;
|
||||||
|
depth += 1;
|
||||||
|
}
|
||||||
|
depth
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(&self, size: impl Into<crate::util::Vec2>) {
|
pub fn entries(&self) -> &[MoveOffset] {
|
||||||
self.render_state.get_mut().resize(size);
|
&self.arena
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_density(&mut self, density: f32) {
|
pub fn clear(&mut self) {
|
||||||
self.data.text.borrow_mut().density = density;
|
self.changed = true;
|
||||||
self.render_state.get_mut().set_density(density);
|
self.arena = Arena::default();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Ui {
|
|
||||||
type Target = UiData;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for Ui {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.data
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait UiRsc {
|
pub trait UiRsc {
|
||||||
fn ui(&self) -> &Ui;
|
fn ui(&self) -> &UiData;
|
||||||
fn ui_mut(&mut self) -> &mut Ui;
|
fn ui_mut(&mut self) -> &mut UiData;
|
||||||
|
|
||||||
fn draw<'a>(&mut self, root: impl Into<Option<&'a crate::StrongWidget>>)
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
self.draw_at(root, std::time::Instant::now());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_at<'a>(
|
|
||||||
&mut self,
|
|
||||||
root: impl Into<Option<&'a crate::StrongWidget>>,
|
|
||||||
frame_time: std::time::Instant,
|
|
||||||
) -> bool
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
let render_state = self.ui().render_state.clone();
|
|
||||||
render_state.get_mut().update_at(root, self, frame_time)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn on_add(&mut self, id: WeakWidget) {}
|
fn on_add(&mut self, id: WeakWidget) {}
|
||||||
@@ -208,8 +144,6 @@ pub trait UiRsc {
|
|||||||
while let Some(id) = self.widgets_mut().free_next() {
|
while let Some(id) = self.widgets_mut().free_next() {
|
||||||
self.on_remove(id);
|
self.on_remove(id);
|
||||||
}
|
}
|
||||||
self.ui_mut().text.borrow_mut().free_released();
|
|
||||||
self.ui_mut().textures.free();
|
self.ui_mut().textures.free();
|
||||||
self.ui_mut().paints.free_released();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+672
-478
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,249 @@
|
|||||||
|
use crate::{Axis, AxisAlign, Len, PrimitiveHandle, RegionAlign, UiRegion, UiSpan};
|
||||||
|
|
||||||
|
/// How a child's region along one axis comes from the region of the widget
|
||||||
|
/// asking, and what its fractions are of.
|
||||||
|
///
|
||||||
|
/// The three ways of saying a region are the three the geometry already has:
|
||||||
|
/// a span composed into the caller's box, a span shifted to where that box
|
||||||
|
/// starts, and a length placed in it by alignment. Which one is meant cannot
|
||||||
|
/// be read off the numbers, since two of them take the same span and apply
|
||||||
|
/// it differently, so it is said here.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct PlaceDescAxis {
|
||||||
|
span: PlaceSpan,
|
||||||
|
fills: bool,
|
||||||
|
rel_base: RelBase,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
enum PlaceSpan {
|
||||||
|
Within(UiSpan),
|
||||||
|
Shifted(UiSpan),
|
||||||
|
Sized(Len),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a child's fractions are of, where the caller has not named a length.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
enum RelBase {
|
||||||
|
/// The caller's own, unchanged.
|
||||||
|
Inherit,
|
||||||
|
/// The caller's own, narrowed the way the region is.
|
||||||
|
WithRegion,
|
||||||
|
/// This length of the window.
|
||||||
|
Len(Len),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaceDescAxis {
|
||||||
|
/// The whole of the caller's box.
|
||||||
|
pub const WHOLE: Self = Self::within(UiSpan::FULL);
|
||||||
|
|
||||||
|
/// `span` composed into the caller's own box, so it moves and scales
|
||||||
|
/// with it: [`UiSpan::within`], which is what a container that insets
|
||||||
|
/// one speaks. Taking eleven pixels off the end needs no length, where
|
||||||
|
/// saying the same thing in window lengths would make the container read
|
||||||
|
/// its own box -- and a box chosen from its own answer then feeds back
|
||||||
|
/// into the answer.
|
||||||
|
///
|
||||||
|
/// The child's rel base is narrowed the same way, so padding takes its
|
||||||
|
/// pixels off both and `rel(1)` under it fills the caller rather than
|
||||||
|
/// overflowing it.
|
||||||
|
pub const fn within(span: UiSpan) -> Self {
|
||||||
|
Self {
|
||||||
|
span: PlaceSpan::Within(span),
|
||||||
|
fills: false,
|
||||||
|
rel_base: RelBase::WithRegion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `span` shifted to where the caller's own box starts: window lengths
|
||||||
|
/// along a cursor, which is what a container dividing room among its
|
||||||
|
/// children speaks. A child's report is a window length, so the cursor
|
||||||
|
/// that sums those reports is one too, and a moved box re-places every
|
||||||
|
/// child by re-adding its start, exactly.
|
||||||
|
///
|
||||||
|
/// The child's rel base passes through: how far along the cursor a child
|
||||||
|
/// sits says nothing about what a fraction under it is of.
|
||||||
|
pub const fn shifted(span: UiSpan) -> Self {
|
||||||
|
Self {
|
||||||
|
span: PlaceSpan::Shifted(span),
|
||||||
|
fills: false,
|
||||||
|
rel_base: RelBase::Inherit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A box this long, placed in the caller's own by the child's alignment:
|
||||||
|
/// the rule that places an answer, with the length given from above
|
||||||
|
/// rather than reported. What a stack's sizing child decides for the
|
||||||
|
/// rest. It is the child's rel base too.
|
||||||
|
pub const fn sized(len: Len) -> Self {
|
||||||
|
Self {
|
||||||
|
span: PlaceSpan::Sized(len),
|
||||||
|
fills: false,
|
||||||
|
rel_base: RelBase::Len(len),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This region is the child's placement: its answer is not placed inside
|
||||||
|
/// it again. A container uses it where it hands back exactly what the
|
||||||
|
/// child asked for -- a row placing a child at the length it reported.
|
||||||
|
pub const fn fills(mut self) -> Self {
|
||||||
|
self.fills = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the child's fractions are of, as a length of the window: a
|
||||||
|
/// resolved share, or a box a sibling's answer decided.
|
||||||
|
pub const fn rel_base(mut self, len: Len) -> Self {
|
||||||
|
self.rel_base = RelBase::Len(len);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the region is the placement outright, rather than a box the
|
||||||
|
/// answer is placed inside.
|
||||||
|
pub(crate) const fn does_fill(self) -> bool {
|
||||||
|
self.fills
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where it lands in the coordinates `own` is in.
|
||||||
|
pub(crate) fn of(self, own: UiSpan, align: AxisAlign) -> UiSpan {
|
||||||
|
match self.span {
|
||||||
|
PlaceSpan::Within(span) => span.within(&own),
|
||||||
|
PlaceSpan::Shifted(mut span) => {
|
||||||
|
span.shift(own.start);
|
||||||
|
span
|
||||||
|
}
|
||||||
|
PlaceSpan::Sized(len) => {
|
||||||
|
let start = own.start + (own.len() - len).scale(align.rel());
|
||||||
|
UiSpan::new(start, start + len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The child's rel base, where this says one outright. `None` forwards
|
||||||
|
/// the caller's own, and [`RelBase::WithRegion`] is resolved by whoever
|
||||||
|
/// can read that rel base, so it does not reach here.
|
||||||
|
pub(crate) const fn stated_rel_base(self) -> Option<Len> {
|
||||||
|
match self.rel_base {
|
||||||
|
RelBase::Len(len) => Some(len),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The length this narrows the caller's rel base by, where it does.
|
||||||
|
/// `None` leaves that rel base alone, and reading it is then a
|
||||||
|
/// dependency the caller does not take.
|
||||||
|
pub(crate) const fn narrows_rel_base(self) -> Option<UiSpan> {
|
||||||
|
match (self.rel_base, self.span) {
|
||||||
|
(RelBase::WithRegion, PlaceSpan::Within(span)) => Some(span),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The span it composes into the caller's box, where that is what it
|
||||||
|
/// does: the one case whose validity maps back through the part.
|
||||||
|
pub(crate) const fn within_span(self) -> Option<UiSpan> {
|
||||||
|
match self.span {
|
||||||
|
PlaceSpan::Within(span) => Some(span),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the caller decided this length rather than a place along its
|
||||||
|
/// own box, which is what stops its length reaching the child at all.
|
||||||
|
pub(crate) const fn is_sized(self) -> bool {
|
||||||
|
matches!(self.span, PlaceSpan::Sized(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same, with its rel base stated outright.
|
||||||
|
pub(crate) const fn with_rel_base(mut self, len: Option<Len>) -> Self {
|
||||||
|
self.rel_base = match len {
|
||||||
|
Some(len) => RelBase::Len(len),
|
||||||
|
None => RelBase::Inherit,
|
||||||
|
};
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a child is asked, on both axes. A [`UiRegion`] converts into the
|
||||||
|
/// common case: that box of the caller's own, the answer placed inside it.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct PlaceDesc {
|
||||||
|
pub x: PlaceDescAxis,
|
||||||
|
pub y: PlaceDescAxis,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlaceDesc {
|
||||||
|
/// The whole of the caller's box, on both axes.
|
||||||
|
pub const WHOLE: Self = Self::splat(PlaceDescAxis::WHOLE);
|
||||||
|
|
||||||
|
pub const fn new(x: PlaceDescAxis, y: PlaceDescAxis) -> Self {
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same on both axes.
|
||||||
|
pub const fn splat(place: PlaceDescAxis) -> Self {
|
||||||
|
Self { x: place, y: place }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `aligned` on `axis` and `ortho` on the other, which is how a
|
||||||
|
/// container that divides one axis says what it is doing.
|
||||||
|
pub fn from_axis(axis: Axis, aligned: PlaceDescAxis, ortho: PlaceDescAxis) -> Self {
|
||||||
|
match axis {
|
||||||
|
Axis::X => Self::new(aligned, ortho),
|
||||||
|
Axis::Y => Self::new(ortho, aligned),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn axis(&self, axis: Axis) -> &PlaceDescAxis {
|
||||||
|
match axis {
|
||||||
|
Axis::X => &self.x,
|
||||||
|
Axis::Y => &self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn axis_mut(&mut self, axis: Axis) -> &mut PlaceDescAxis {
|
||||||
|
match axis {
|
||||||
|
Axis::X => &mut self.x,
|
||||||
|
Axis::Y => &mut self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Both regions are the child's placement. See [`PlaceDescAxis::fills`].
|
||||||
|
pub const fn fills(self) -> Self {
|
||||||
|
Self::new(self.x.fills(), self.y.fills())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The child's rel base on one axis. See [`PlaceDescAxis::rel_base`].
|
||||||
|
pub const fn rel_base(mut self, axis: Axis, len: Len) -> Self {
|
||||||
|
*self.axis_mut(axis) = self.axis(axis).rel_base(len);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The box each axis names, in the coordinates `own` is in.
|
||||||
|
pub(crate) fn of(self, own: UiRegion, align: RegionAlign) -> UiRegion {
|
||||||
|
UiRegion::new(self.x.of(own.x, align.x), self.y.of(own.y, align.y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UiRegion> for PlaceDesc {
|
||||||
|
fn from(region: UiRegion) -> Self {
|
||||||
|
Self::new(
|
||||||
|
PlaceDescAxis::within(region.x),
|
||||||
|
PlaceDescAxis::within(region.y),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PlaceDescAxis> for PlaceDesc {
|
||||||
|
fn from(place: PlaceDescAxis) -> Self {
|
||||||
|
Self::splat(place)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A primitive as it was written: its box in the widget's own box's
|
||||||
|
/// coordinates, which is what a move of that box re-composes from.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RetainedPrimitive {
|
||||||
|
pub handle: PrimitiveHandle,
|
||||||
|
pub region: UiRegion,
|
||||||
|
}
|
||||||
+1025
-1019
File diff suppressed because it is too large.
Load diff
+11
-10
@@ -1,6 +1,6 @@
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use crate::util::{Dirty, Id, IdNum, IdTracker};
|
use crate::util::{Id, IdNum, IdTracker};
|
||||||
|
|
||||||
pub struct Arena<T, I> {
|
pub struct Arena<T, I> {
|
||||||
data: Vec<T>,
|
data: Vec<T>,
|
||||||
@@ -34,6 +34,10 @@ impl<T, I: IdNum> Arena<T, I> {
|
|||||||
self.tracker.free(id);
|
self.tracker.free(id);
|
||||||
self.data[i]
|
self.data[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) 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> {
|
||||||
@@ -45,7 +49,7 @@ impl<T, I: IdNum> Default for Arena<T, I> {
|
|||||||
pub struct TrackedArena<T, I> {
|
pub struct TrackedArena<T, I> {
|
||||||
inner: Arena<T, I>,
|
inner: Arena<T, I>,
|
||||||
refs: Vec<u32>,
|
refs: Vec<u32>,
|
||||||
pub dirty: Dirty,
|
pub changed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, I: IdNum> TrackedArena<T, I> {
|
impl<T, I: IdNum> TrackedArena<T, I> {
|
||||||
@@ -53,14 +57,14 @@ impl<T, I: IdNum> TrackedArena<T, I> {
|
|||||||
Self {
|
Self {
|
||||||
inner: Arena::default(),
|
inner: Arena::default(),
|
||||||
refs: Vec::new(),
|
refs: Vec::new(),
|
||||||
dirty: Dirty::new_all(),
|
changed: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(&mut self, value: T) -> Id<I> {
|
pub fn push(&mut self, value: T) -> Id<I> {
|
||||||
|
self.changed = true;
|
||||||
let id = self.inner.push(value);
|
let id = self.inner.push(value);
|
||||||
let i = id.idx();
|
let i = id.idx();
|
||||||
self.dirty.mark(i);
|
|
||||||
if i == self.refs.len() {
|
if i == self.refs.len() {
|
||||||
self.refs.push(0);
|
self.refs.push(0);
|
||||||
}
|
}
|
||||||
@@ -72,12 +76,8 @@ impl<T, I: IdNum> TrackedArena<T, I> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
||||||
self.dirty.mark(id.idx());
|
self.changed = true;
|
||||||
&mut self.inner.data[id.idx()]
|
self.inner.get_mut(id)
|
||||||
}
|
|
||||||
|
|
||||||
pub fn for_upload(&mut self) -> (&[T], &mut Dirty) {
|
|
||||||
(&self.inner.data, &mut self.dirty)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, id: Id<I>) -> T
|
pub fn remove(&mut self, id: Id<I>) -> T
|
||||||
@@ -87,6 +87,7 @@ impl<T, I: IdNum> TrackedArena<T, I> {
|
|||||||
let i = id.idx();
|
let i = id.idx();
|
||||||
self.refs[i] -= 1;
|
self.refs[i] -= 1;
|
||||||
if self.refs[i] == 0 {
|
if self.refs[i] == 0 {
|
||||||
|
self.changed = true;
|
||||||
self.inner.remove(id)
|
self.inner.remove(id)
|
||||||
} else {
|
} else {
|
||||||
self[i]
|
self[i]
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Dirty {
|
|
||||||
words: Vec<u64>,
|
|
||||||
/// Everything is dirty regardless of the bits -- the state after a
|
|
||||||
/// buffer reallocation, whose contents are undefined, and the state a
|
|
||||||
/// freshly built arena starts in. Kept as a flag rather than by
|
|
||||||
/// setting every bit so that it costs nothing to say and cannot be
|
|
||||||
/// half-applied as the array grows.
|
|
||||||
all: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Dirty {
|
|
||||||
pub fn new_all() -> Self {
|
|
||||||
Self {
|
|
||||||
words: Vec::new(),
|
|
||||||
all: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mark(&mut self, i: usize) {
|
|
||||||
if self.all {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let word = i / 64;
|
|
||||||
if word >= self.words.len() {
|
|
||||||
self.words.resize(word + 1, 0);
|
|
||||||
}
|
|
||||||
self.words[word] |= 1 << (i % 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn contains(&self, i: usize) -> bool {
|
|
||||||
self.all
|
|
||||||
|| self
|
|
||||||
.words
|
|
||||||
.get(i / 64)
|
|
||||||
.is_some_and(|word| word & (1 << (i % 64)) != 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear one entry that was restored to the value already on the GPU.
|
|
||||||
/// `all` has no per-entry representation and is used only when every
|
|
||||||
/// byte must be uploaded regardless of later writes, so it stays set.
|
|
||||||
pub fn unmark(&mut self, i: usize) {
|
|
||||||
if self.all {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Some(word) = self.words.get_mut(i / 64) {
|
|
||||||
*word &= !(1 << (i % 64));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Everything must be written: the buffer was reallocated (its
|
|
||||||
/// contents are undefined), or the array was cleared.
|
|
||||||
pub fn mark_all(&mut self) {
|
|
||||||
self.all = true;
|
|
||||||
self.words.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_clean(&self) -> bool {
|
|
||||||
!self.all && self.words.iter().all(|w| *w == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ranges(&self, len: usize, gap: usize) -> Vec<Range<usize>> {
|
|
||||||
let mut ranges = Vec::new();
|
|
||||||
self.for_each_range(len, gap, |range| ranges.push(range));
|
|
||||||
ranges
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn for_each_range(
|
|
||||||
&self,
|
|
||||||
len: usize,
|
|
||||||
gap: usize,
|
|
||||||
mut visit: impl FnMut(Range<usize>),
|
|
||||||
) {
|
|
||||||
if self.all {
|
|
||||||
if len > 0 {
|
|
||||||
visit(0..len);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mut pending: Option<Range<usize>> = None;
|
|
||||||
for (w, word) in self.words.iter().enumerate() {
|
|
||||||
let mut bits = *word;
|
|
||||||
while bits != 0 {
|
|
||||||
let start = w * 64 + bits.trailing_zeros() as usize;
|
|
||||||
let run = (bits >> (start - w * 64)).trailing_ones() as usize;
|
|
||||||
let end = (start + run).min(len);
|
|
||||||
if start >= len {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
match pending.as_mut() {
|
|
||||||
Some(last) if start - last.end <= gap => last.end = end,
|
|
||||||
_ => {
|
|
||||||
if let Some(range) = pending.replace(start..end) {
|
|
||||||
visit(range);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bits &= !(((1u128 << run) - 1) as u64) << (start - w * 64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(range) = pending {
|
|
||||||
visit(range);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.all = false;
|
|
||||||
self.words.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn marked(indices: &[usize], len: usize, gap: usize) -> Vec<Range<usize>> {
|
|
||||||
let mut d = Dirty::default();
|
|
||||||
for &i in indices {
|
|
||||||
d.mark(i);
|
|
||||||
}
|
|
||||||
d.ranges(len, gap)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn adjacent_entries_are_one_range() {
|
|
||||||
assert_eq!(marked(&[3, 4, 5], 64, 0), vec![3..6]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_restored_entry_can_be_unmarked() {
|
|
||||||
let mut dirty = Dirty::default();
|
|
||||||
dirty.mark(3);
|
|
||||||
dirty.mark(5);
|
|
||||||
assert!(dirty.contains(3));
|
|
||||||
dirty.unmark(3);
|
|
||||||
assert!(!dirty.contains(3));
|
|
||||||
assert_eq!(dirty.ranges(8, 0), vec![5..6]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_run_that_crosses_a_word_boundary_is_one_range() {
|
|
||||||
assert_eq!(marked(&[62, 63, 64, 65], 128, 0), vec![62..66]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_gap_wider_than_the_threshold_stays_two_ranges() {
|
|
||||||
assert_eq!(marked(&[0, 10], 64, 4), vec![0..1, 10..11]);
|
|
||||||
assert_eq!(marked(&[0, 10], 64, 16), vec![0..11]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ranges_stop_at_the_length() {
|
|
||||||
assert_eq!(marked(&[1, 2, 40], 3, 0), vec![1..3]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn mark_all_covers_everything_and_survives_later_marks() {
|
|
||||||
let mut d = Dirty::new_all();
|
|
||||||
d.mark(2);
|
|
||||||
assert_eq!(d.ranges(9, 0), vec![0..9]);
|
|
||||||
assert!(!d.is_clean());
|
|
||||||
d.clear();
|
|
||||||
assert!(d.is_clean());
|
|
||||||
assert!(d.ranges(9, 0).is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn an_empty_array_has_nothing_to_upload_even_when_all_is_set() {
|
|
||||||
assert!(Dirty::new_all().ranges(0, 0).is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,6 +27,8 @@ impl<I: IdNum> IdTracker<I> {
|
|||||||
|
|
||||||
impl<I: IdNum> Id<I> {
|
impl<I: IdNum> Id<I> {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
/// for debug purposes; should this be exposed?
|
||||||
|
/// generally you want to use labels with widgets
|
||||||
pub(crate) fn raw(id: I) -> Self {
|
pub(crate) fn raw(id: I) -> Self {
|
||||||
Self(id)
|
Self(id)
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-21
@@ -1,31 +1,13 @@
|
|||||||
use std::ops::*;
|
|
||||||
|
|
||||||
pub const trait LerpUtil {
|
pub const trait LerpUtil {
|
||||||
fn lerp(self, from: Self, to: Self) -> Self;
|
fn lerp(self, from: Self, to: Self) -> Self;
|
||||||
fn lerp_inv(self, from: Self, to: Self) -> Self;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const trait DivOr {
|
const impl LerpUtil for f32 {
|
||||||
fn div_or(self, rhs: Self, other: Self) -> Self;
|
/// linear interpolation
|
||||||
}
|
/// from * (1.0 - self) + to * 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
|
|
||||||
{
|
|
||||||
fn lerp(self, from: Self, to: Self) -> Self {
|
fn lerp(self, from: Self, to: Self) -> Self {
|
||||||
from + (to - from) * self
|
from + (to - from) * self
|
||||||
}
|
}
|
||||||
fn lerp_inv(self, from: Self, to: Self) -> Self {
|
|
||||||
(self - from).div_or(to - from, from)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! impl_op {
|
macro_rules! impl_op {
|
||||||
@@ -74,6 +56,34 @@ macro_rules! impl_op {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Without the `f32` operations, for a type whose fields are not all the
|
||||||
|
// same kind of number: there is nothing a bare float means to a fraction
|
||||||
|
// and an offset at once.
|
||||||
|
(same $T:ident $op:ident $fn:ident $opa:ident $fna:ident; $($field:ident)*) => {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
mod ${concat($T, _op_, $fn, _same_impl)} {
|
||||||
|
use super::*;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::ops::*;
|
||||||
|
const impl $op for $T {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn $fn(self, rhs: Self) -> Self::Output {
|
||||||
|
Self {
|
||||||
|
$($field: self.$field.$fn(rhs.$field),)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const impl $opa for $T {
|
||||||
|
fn $fna(&mut self, rhs: Self) {
|
||||||
|
*self = self.$fn(rhs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(same $T:ident $op:ident $fn:ident; $($field:ident)*) => {
|
||||||
|
impl_op!(same $T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
||||||
|
};
|
||||||
($T:ident $op:ident $fn:ident; $($field:ident)*) => {
|
($T:ident $op:ident $fn:ident; $($field:ident)*) => {
|
||||||
impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
mod arena;
|
mod arena;
|
||||||
mod borrow;
|
mod borrow;
|
||||||
mod change;
|
mod change;
|
||||||
mod dirty;
|
|
||||||
mod id;
|
mod id;
|
||||||
mod math;
|
mod math;
|
||||||
mod refcount;
|
mod refcount;
|
||||||
mod resources;
|
|
||||||
mod slot;
|
mod slot;
|
||||||
mod trust;
|
mod trust;
|
||||||
mod typemap;
|
mod typemap;
|
||||||
@@ -14,13 +12,11 @@ mod vec2;
|
|||||||
pub use arena::*;
|
pub use arena::*;
|
||||||
pub use borrow::*;
|
pub use borrow::*;
|
||||||
pub use change::*;
|
pub use change::*;
|
||||||
pub use dirty::*;
|
|
||||||
pub use id::*;
|
pub use id::*;
|
||||||
pub use math::*;
|
pub use math::*;
|
||||||
pub use refcount::*;
|
pub use refcount::*;
|
||||||
pub use resources::*;
|
|
||||||
pub use slot::*;
|
pub use slot::*;
|
||||||
pub use trust::*;
|
pub(crate) use trust::*;
|
||||||
pub use typemap::*;
|
pub use typemap::*;
|
||||||
pub use vec2::*;
|
pub use vec2::*;
|
||||||
|
|
||||||
|
|||||||
@@ -1,414 +0,0 @@
|
|||||||
use super::{SlotId, SlotVec};
|
|
||||||
use std::{
|
|
||||||
cell::{Ref, RefCell, RefMut},
|
|
||||||
fmt,
|
|
||||||
hash::{Hash, Hasher},
|
|
||||||
marker::PhantomData,
|
|
||||||
rc::Rc,
|
|
||||||
sync::mpsc::{Receiver, Sender, channel},
|
|
||||||
};
|
|
||||||
|
|
||||||
enum Event {
|
|
||||||
Clone(SlotId),
|
|
||||||
Drop(SlotId),
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Entry<T> {
|
|
||||||
value: T,
|
|
||||||
strong: Option<u32>,
|
|
||||||
recycle: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generational storage and deferred strong-reference accounting for one kind
|
|
||||||
/// of UI resource.
|
|
||||||
pub struct Resources<T> {
|
|
||||||
entries: SlotVec<Entry<T>>,
|
|
||||||
send: Sender<Event>,
|
|
||||||
recv: Receiver<Event>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Resources<T> {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let (send, recv) = channel();
|
|
||||||
Self {
|
|
||||||
entries: SlotVec::new(),
|
|
||||||
send,
|
|
||||||
recv,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(&mut self, value: T) -> StrongRscId<T> {
|
|
||||||
self.add_with(value, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_unrecycled(&mut self, value: T) -> StrongRscId<T> {
|
|
||||||
self.add_with(value, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_with(&mut self, value: T, recycle: bool) -> StrongRscId<T> {
|
|
||||||
let id = self.entries.add(Entry {
|
|
||||||
value,
|
|
||||||
strong: Some(1),
|
|
||||||
recycle,
|
|
||||||
});
|
|
||||||
StrongRscId::new(id, self.send.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an entry whose lifetime is the lifetime of the arena itself.
|
|
||||||
pub fn add_static(&mut self, value: T) -> WeakRscId<T> {
|
|
||||||
WeakRscId::new(self.entries.add(Entry {
|
|
||||||
value,
|
|
||||||
strong: None,
|
|
||||||
recycle: false,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply(&mut self, mut dropped: impl FnMut(SlotId, T)) {
|
|
||||||
for event in self.recv.try_iter() {
|
|
||||||
match event {
|
|
||||||
Event::Clone(id) => {
|
|
||||||
let entry = self
|
|
||||||
.entries
|
|
||||||
.get_mut(id)
|
|
||||||
.expect("cloned resource id points at a released slot");
|
|
||||||
let strong = entry
|
|
||||||
.strong
|
|
||||||
.as_mut()
|
|
||||||
.expect("a static resource cannot have a strong id");
|
|
||||||
*strong = strong.checked_add(1).expect("resource reference overflow");
|
|
||||||
}
|
|
||||||
Event::Drop(id) => {
|
|
||||||
let remove = {
|
|
||||||
let entry = self
|
|
||||||
.entries
|
|
||||||
.get_mut(id)
|
|
||||||
.expect("dropped resource id points at a released slot");
|
|
||||||
let strong = entry
|
|
||||||
.strong
|
|
||||||
.as_mut()
|
|
||||||
.expect("a static resource cannot have a strong id");
|
|
||||||
*strong = strong.checked_sub(1).expect("resource reference underflow");
|
|
||||||
*strong == 0
|
|
||||||
};
|
|
||||||
if remove {
|
|
||||||
let recycle = self.entries.get(id).unwrap().recycle;
|
|
||||||
let entry = if recycle {
|
|
||||||
self.entries.remove(id)
|
|
||||||
} else {
|
|
||||||
self.entries.remove_unrecycled(id)
|
|
||||||
}
|
|
||||||
.unwrap();
|
|
||||||
dropped(id, entry.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The owning manager must call [`Self::apply`] first so a queued final
|
|
||||||
/// drop cannot be mistaken for a still-live entry.
|
|
||||||
pub fn upgrade(&mut self, id: WeakRscId<T>) -> Option<StrongRscId<T>> {
|
|
||||||
let entry = self.entries.get_mut(id.id)?;
|
|
||||||
let strong = entry.strong.as_mut()?;
|
|
||||||
*strong = strong.checked_add(1).expect("resource reference overflow");
|
|
||||||
Some(StrongRscId::new(id.id, self.send.clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, id: impl RscId<T>) -> Option<&T> {
|
|
||||||
Some(&self.entries.get(id.rsc_id())?.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_mut(&mut self, id: impl RscId<T>) -> Option<&mut T> {
|
|
||||||
Some(&mut self.entries.get_mut(id.rsc_id())?.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
|
||||||
self.entries.values_mut().map(|entry| &mut entry.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn capacity(&self) -> usize {
|
|
||||||
self.entries.capacity()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.entries.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.entries.is_empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Default for Resources<T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait RscId<T> {
|
|
||||||
fn rsc_id(&self) -> SlotId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A sendable owning ID for one entry in [`Resources`]. It keeps the entry
|
|
||||||
/// alive but does not provide access to its value.
|
|
||||||
pub struct StrongRscId<T> {
|
|
||||||
id: SlotId,
|
|
||||||
send: Sender<Event>,
|
|
||||||
ty: PhantomData<fn() -> T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> StrongRscId<T> {
|
|
||||||
fn new(id: SlotId, send: Sender<Event>) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
send,
|
|
||||||
ty: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn weak(&self) -> WeakRscId<T> {
|
|
||||||
WeakRscId::new(self.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> SlotId {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn slot(&self) -> u32 {
|
|
||||||
self.id.slot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Clone for StrongRscId<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
let _ = self.send.send(Event::Clone(self.id));
|
|
||||||
Self::new(self.id, self.send.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Drop for StrongRscId<T> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = self.send.send(Event::Drop(self.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> RscId<T> for StrongRscId<T> {
|
|
||||||
fn rsc_id(&self) -> SlotId {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> RscId<T> for &StrongRscId<T> {
|
|
||||||
fn rsc_id(&self) -> SlotId {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> fmt::Debug for StrongRscId<T> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.id.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> PartialEq for StrongRscId<T> {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.id == other.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Eq for StrongRscId<T> {}
|
|
||||||
|
|
||||||
impl<T> Hash for StrongRscId<T> {
|
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
||||||
self.id.hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A sendable, copyable ID that does not keep its [`Resources`] entry alive.
|
|
||||||
pub struct WeakRscId<T> {
|
|
||||||
id: SlotId,
|
|
||||||
ty: PhantomData<fn() -> T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> WeakRscId<T> {
|
|
||||||
fn new(id: SlotId) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
ty: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(self) -> SlotId {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn slot(self) -> u32 {
|
|
||||||
self.id.slot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Clone for WeakRscId<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
*self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Copy for WeakRscId<T> {}
|
|
||||||
|
|
||||||
impl<T> RscId<T> for WeakRscId<T> {
|
|
||||||
fn rsc_id(&self) -> SlotId {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> RscId<T> for &WeakRscId<T> {
|
|
||||||
fn rsc_id(&self) -> SlotId {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> fmt::Debug for WeakRscId<T> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.id.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> PartialEq for WeakRscId<T> {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.id == other.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Eq for WeakRscId<T> {}
|
|
||||||
|
|
||||||
impl<T> Hash for WeakRscId<T> {
|
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
||||||
self.id.hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenient UI-thread access to a resource through an owning ID. The `Rc`
|
|
||||||
/// deliberately makes this local; tasks carry strong or weak IDs instead.
|
|
||||||
pub struct RscHandle<T> {
|
|
||||||
id: StrongRscId<T>,
|
|
||||||
resources: Rc<RefCell<Resources<T>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> RscHandle<T> {
|
|
||||||
pub fn new(id: StrongRscId<T>, resources: Rc<RefCell<Resources<T>>>) -> Self {
|
|
||||||
Self { id, resources }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(resources: Rc<RefCell<Resources<T>>>, value: T) -> Self {
|
|
||||||
let id = resources.borrow_mut().add(value);
|
|
||||||
Self::new(id, resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self) -> Ref<'_, T> {
|
|
||||||
Ref::map(self.resources.borrow(), |resources| {
|
|
||||||
resources
|
|
||||||
.get(&self.id)
|
|
||||||
.expect("resource handle points at a released slot")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_mut(&mut self) -> RefMut<'_, T> {
|
|
||||||
RefMut::map(self.resources.borrow_mut(), |resources| {
|
|
||||||
resources
|
|
||||||
.get_mut(&self.id)
|
|
||||||
.expect("resource handle points at a released slot")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_mut_shared(&self) -> RefMut<'_, T> {
|
|
||||||
RefMut::map(self.resources.borrow_mut(), |resources| {
|
|
||||||
resources
|
|
||||||
.get_mut(&self.id)
|
|
||||||
.expect("resource handle points at a released slot")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn strong(&self) -> StrongRscId<T> {
|
|
||||||
self.id.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn weak(&self) -> WeakRscId<T> {
|
|
||||||
self.id.weak()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> SlotId {
|
|
||||||
self.id.id()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Clone for RscHandle<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self::new(self.id.clone(), self.resources.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> fmt::Debug for RscHandle<T> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.id.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_slot_lives_until_every_strong_id_is_dropped() {
|
|
||||||
let mut resources = Resources::new();
|
|
||||||
let first = resources.add("value");
|
|
||||||
let weak = first.weak();
|
|
||||||
let second = first.clone();
|
|
||||||
drop(first);
|
|
||||||
resources.apply(|_, _| {});
|
|
||||||
assert_eq!(resources.get(weak), Some(&"value"));
|
|
||||||
|
|
||||||
drop(second);
|
|
||||||
resources.apply(|_, _| {});
|
|
||||||
assert_eq!(resources.get(weak), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_weak_id_can_be_upgraded_while_the_resource_is_alive() {
|
|
||||||
let mut resources = Resources::new();
|
|
||||||
let first = resources.add("value");
|
|
||||||
let weak = first.weak();
|
|
||||||
let second = resources.upgrade(weak).unwrap();
|
|
||||||
drop(first);
|
|
||||||
resources.apply(|_, _| {});
|
|
||||||
assert_eq!(resources.get(&second), Some(&"value"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn strong_and_weak_ids_can_cross_threads() {
|
|
||||||
fn assert_send<T: Send>() {}
|
|
||||||
assert_send::<StrongRscId<String>>();
|
|
||||||
assert_send::<WeakRscId<String>>();
|
|
||||||
|
|
||||||
let mut resources = Resources::new();
|
|
||||||
let first = resources.add("value");
|
|
||||||
let second = first.clone();
|
|
||||||
std::thread::spawn(move || drop(second)).join().unwrap();
|
|
||||||
drop(first);
|
|
||||||
resources.apply(|_, _| {});
|
|
||||||
assert!(resources.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn an_unrecycled_slot_stays_a_hole() {
|
|
||||||
let mut resources = Resources::new();
|
|
||||||
let first = resources.add_unrecycled("first");
|
|
||||||
let first_slot = first.slot();
|
|
||||||
drop(first);
|
|
||||||
resources.apply(|_, _| {});
|
|
||||||
|
|
||||||
let second = resources.add("second");
|
|
||||||
|
|
||||||
assert_ne!(second.slot(), first_slot);
|
|
||||||
assert_eq!(resources.len(), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-51
@@ -1,28 +1,12 @@
|
|||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct SlotId {
|
pub struct SlotId {
|
||||||
idx: u32,
|
idx: u32,
|
||||||
genr: u32,
|
genr: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SlotId {
|
|
||||||
pub(crate) fn slot(self) -> u32 {
|
|
||||||
self.idx
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A stable, collision-free `u64` encoding of this id -- for a caller
|
|
||||||
/// (accesskit's `NodeId`, today) that wants a flat integer key rather
|
|
||||||
/// than the two `u32`s. `idx` is offset by one so no real id ever
|
|
||||||
/// encodes to 0, which callers can then reserve for their own
|
|
||||||
/// out-of-band root/window node.
|
|
||||||
pub fn as_u64(&self) -> u64 {
|
|
||||||
((self.idx as u64) + 1) << 32 | self.genr as u64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SlotVec<T> {
|
pub struct SlotVec<T> {
|
||||||
data: Vec<(u32, Option<T>)>,
|
data: Vec<(u32, Option<T>)>,
|
||||||
free: Vec<u32>,
|
free: Vec<u32>,
|
||||||
len: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> SlotVec<T> {
|
impl<T> SlotVec<T> {
|
||||||
@@ -30,12 +14,11 @@ impl<T> SlotVec<T> {
|
|||||||
Self {
|
Self {
|
||||||
data: Default::default(),
|
data: Default::default(),
|
||||||
free: Default::default(),
|
free: Default::default(),
|
||||||
len: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(&mut self, x: T) -> SlotId {
|
pub fn add(&mut self, x: T) -> SlotId {
|
||||||
let id = if let Some(idx) = self.free.pop() {
|
if let Some(idx) = self.free.pop() {
|
||||||
let (genr, data) = &mut self.data[idx as usize];
|
let (genr, data) = &mut self.data[idx as usize];
|
||||||
*data = Some(x);
|
*data = Some(x);
|
||||||
SlotId { idx, genr: *genr }
|
SlotId { idx, genr: *genr }
|
||||||
@@ -44,36 +27,15 @@ impl<T> SlotVec<T> {
|
|||||||
let genr = 0;
|
let genr = 0;
|
||||||
self.data.push((genr, Some(x)));
|
self.data.push((genr, Some(x)));
|
||||||
SlotId { idx, genr }
|
SlotId { idx, genr }
|
||||||
};
|
}
|
||||||
self.len += 1;
|
|
||||||
id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn free(&mut self, id: SlotId) {
|
pub fn free(&mut self, id: SlotId) {
|
||||||
let _ = self.remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(&mut self, id: SlotId) -> Option<T> {
|
|
||||||
self.remove_inner(id, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_unrecycled(&mut self, id: SlotId) -> Option<T> {
|
|
||||||
self.remove_inner(id, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_inner(&mut self, id: SlotId, recycle: bool) -> Option<T> {
|
|
||||||
let (genr, data) = &mut self.data[id.idx as usize];
|
let (genr, data) = &mut self.data[id.idx as usize];
|
||||||
if *genr != id.genr {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
*genr += 1;
|
*genr += 1;
|
||||||
let value = data.take()?;
|
*data = None;
|
||||||
self.len -= 1;
|
|
||||||
if recycle {
|
|
||||||
self.free.push(id.idx);
|
self.free.push(id.idx);
|
||||||
}
|
}
|
||||||
Some(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, id: SlotId) -> Option<&T> {
|
pub fn get(&self, id: SlotId) -> Option<&T> {
|
||||||
let slot = &self.data[id.idx as usize];
|
let slot = &self.data[id.idx as usize];
|
||||||
@@ -92,20 +54,12 @@ impl<T> SlotVec<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.len
|
self.data.len() - self.free.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.len() == 0
|
self.len() == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
|
|
||||||
self.data.iter_mut().filter_map(|(_, value)| value.as_mut())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn capacity(&self) -> usize {
|
|
||||||
self.data.len()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Default for SlotVec<T> {
|
impl<T> Default for SlotVec<T> {
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
#[allow(clippy::missing_safety_doc)]
|
#[allow(clippy::missing_safety_doc)]
|
||||||
pub unsafe fn forget_ref<'a, T>(x: &T) -> &'a T {
|
pub(crate) unsafe fn forget_mut<'a, T>(x: &mut T) -> &'a mut T {
|
||||||
unsafe { std::mem::transmute::<&T, &T>(x) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::missing_safety_doc)]
|
|
||||||
pub 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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::mut_from_ref, clippy::missing_safety_doc)]
|
#[allow(clippy::mut_from_ref, clippy::missing_safety_doc)]
|
||||||
pub unsafe fn to_mut<T>(x: &T) -> &mut T {
|
pub(crate) unsafe fn to_mut<T>(x: &T) -> &mut T {
|
||||||
#[allow(mutable_transmutes)]
|
#[allow(mutable_transmutes)]
|
||||||
unsafe {
|
unsafe {
|
||||||
std::mem::transmute::<&T, &mut T>(x)
|
std::mem::transmute::<&T, &mut T>(x)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ impl<Trait: ?Sized> TypeMap<Trait> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn convert_mut<T: Unsize<Trait>>(entry: &mut Box<Trait>) -> &mut T {
|
fn convert_mut<T: Unsize<Trait>>(entry: &mut Box<Trait>) -> &mut T {
|
||||||
|
// allegedly this is just what Any does...
|
||||||
unsafe { &mut *(entry.as_mut() as *mut Trait as *mut T) }
|
unsafe { &mut *(entry.as_mut() as *mut Trait as *mut T) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-11
@@ -1,7 +1,11 @@
|
|||||||
use crate::util::{DivOr, impl_op};
|
use crate::util::impl_op;
|
||||||
use std::{hash::Hash, ops::*};
|
use std::{hash::Hash, ops::*};
|
||||||
|
|
||||||
#[repr(C)]
|
/// `align(8)` because that is WGSL's alignment for a `vec2<f32>`, so any GPU
|
||||||
|
/// struct holding one is laid out the way its shader reads it without having
|
||||||
|
/// to say so itself. Those structs still need a manual `unsafe impl Pod`,
|
||||||
|
/// since the trailing padding this introduces is what `derive(Pod)` refuses.
|
||||||
|
#[repr(C, align(8))]
|
||||||
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct Vec2 {
|
pub struct Vec2 {
|
||||||
pub x: f32,
|
pub x: f32,
|
||||||
@@ -61,20 +65,12 @@ impl Vec2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this version looks kinda cool... is it more readable? more annoying to copy and change though
|
||||||
impl_op!(impl Add for Vec2: add x y);
|
impl_op!(impl Add for Vec2: add x y);
|
||||||
impl_op!(Vec2 Sub sub; x y);
|
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;
|
||||||
|
|
||||||
|
|||||||
+11
-12
@@ -1,28 +1,27 @@
|
|||||||
use crate::Widget;
|
use crate::{RegionAlign, SizeRules, Widget};
|
||||||
|
|
||||||
pub struct WidgetData {
|
pub struct WidgetData {
|
||||||
pub widget: Box<dyn Widget>,
|
pub widget: Box<dyn Widget>,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
|
pub(super) region_node: bool,
|
||||||
|
pub(super) size: SizeRules,
|
||||||
|
pub(super) align: RegionAlign,
|
||||||
|
/// dynamic borrow checking
|
||||||
pub borrowed: bool,
|
pub borrowed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetData {
|
impl WidgetData {
|
||||||
pub fn new<W: Widget>(widget: W) -> Self {
|
pub fn new<W: Widget>(widget: W) -> Self {
|
||||||
let name = std::any::type_name::<W>();
|
let mut label = std::any::type_name::<W>().to_string();
|
||||||
let label = match (name.find("::"), name.rfind("::")) {
|
if let (Some(first), Some(last)) = (label.find(":"), label.rfind(":")) {
|
||||||
(Some(first), Some(last)) => {
|
label = label.split_at(first).0.to_string() + "::" + label.split_at(last + 1).1;
|
||||||
let suffix = &name[last + 2..];
|
|
||||||
let mut label = String::with_capacity(first + 2 + suffix.len());
|
|
||||||
label.push_str(&name[..first]);
|
|
||||||
label.push_str("::");
|
|
||||||
label.push_str(suffix);
|
|
||||||
label
|
|
||||||
}
|
}
|
||||||
_ => name.to_owned(),
|
|
||||||
};
|
|
||||||
Self {
|
Self {
|
||||||
widget: Box::new(widget),
|
widget: Box::new(widget),
|
||||||
label,
|
label,
|
||||||
|
region_node: false,
|
||||||
|
size: SizeRules::default(),
|
||||||
|
align: RegionAlign::default(),
|
||||||
borrowed: false,
|
borrowed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ use crate::{
|
|||||||
|
|
||||||
pub type WidgetId = SlotId;
|
pub type WidgetId = SlotId;
|
||||||
|
|
||||||
|
/// An identifier for a widget that can index a UI or event ctx to get it.
|
||||||
|
/// This is a strong handle that does not impl Clone, and when it is dropped,
|
||||||
|
/// a signal is sent to the owning UI to clean up the resources.
|
||||||
|
///
|
||||||
|
/// TODO: ergonomic clones when they get put in rust-analyzer & don't cause ICEs?
|
||||||
pub struct StrongWidget<W: ?Sized = dyn Widget> {
|
pub struct StrongWidget<W: ?Sized = dyn Widget> {
|
||||||
pub(super) id: WidgetId,
|
pub(super) id: WidgetId,
|
||||||
counter: RefCounter,
|
counter: RefCounter,
|
||||||
@@ -14,6 +19,8 @@ pub struct StrongWidget<W: ?Sized = dyn Widget> {
|
|||||||
ty: *const W,
|
ty: *const W,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A weak handle to a widget.
|
||||||
|
/// Will not keep it alive, but can still be used for indexing like WidgetHandle.
|
||||||
pub struct WeakWidget<W: ?Sized = dyn Widget> {
|
pub struct WeakWidget<W: ?Sized = dyn Widget> {
|
||||||
pub(super) id: WidgetId,
|
pub(super) id: WidgetId,
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ pub trait WidgetLike<Rsc: UiRsc, Tag>: Sized {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_root(self, rsc: &mut Rsc, root: &mut impl HasRoot<Rsc>) {
|
fn set_root(self, rsc: &mut Rsc, root: &mut impl HasRoot) {
|
||||||
let id = self.add_strong(rsc);
|
let id = self.add_strong(rsc);
|
||||||
root.set_root(rsc, id);
|
root.set_root(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait HasRoot<Rsc> {
|
pub trait HasRoot {
|
||||||
fn set_root(&mut self, rsc: &mut Rsc, root: StrongWidget);
|
fn set_root(&mut self, root: StrongWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
|
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
|
||||||
@@ -43,6 +43,7 @@ impl<Rsc, const LEN: usize> WidgetArrLike<Rsc, LEN, ArrTag> for WidgetArr<LEN> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// variadic generics please save us
|
||||||
macro_rules! impl_widget_arr {
|
macro_rules! impl_widget_arr {
|
||||||
($n:expr;$($W:ident)*) => {
|
($n:expr;$($W:ident)*) => {
|
||||||
impl_widget_arr!($n;$($W)*;$(${concat($W,Tag)})*);
|
impl_widget_arr!($n;$($W)*;$(${concat($W,Tag)})*);
|
||||||
|
|||||||
+14
-34
@@ -4,6 +4,7 @@ use std::any::Any;
|
|||||||
mod data;
|
mod data;
|
||||||
mod handle;
|
mod handle;
|
||||||
mod like;
|
mod like;
|
||||||
|
mod size_rule;
|
||||||
mod tag;
|
mod tag;
|
||||||
mod view;
|
mod view;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
@@ -11,55 +12,31 @@ mod widgets;
|
|||||||
pub use data::*;
|
pub use data::*;
|
||||||
pub use handle::*;
|
pub use handle::*;
|
||||||
pub use like::*;
|
pub use like::*;
|
||||||
|
pub use size_rule::*;
|
||||||
pub use tag::*;
|
pub use tag::*;
|
||||||
pub use view::*;
|
pub use view::*;
|
||||||
pub use widgets::*;
|
pub use widgets::*;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
||||||
pub enum ChildOrder {
|
|
||||||
/// The order in which the parent drew its children.
|
|
||||||
#[default]
|
|
||||||
Draw,
|
|
||||||
/// Ascending visual position on one screen axis. Equal positions keep
|
|
||||||
/// draw order; the resolved coordinates are sorted only when queried.
|
|
||||||
Axis(Axis),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Widget: Any {
|
pub trait Widget: Any {
|
||||||
fn draw(&mut self, painter: &mut Painter);
|
/// Draws the widget, and returns what it used of the box it was given.
|
||||||
|
fn draw(&mut self, painter: &mut Painter) -> Size;
|
||||||
|
|
||||||
|
/// An exact length the widget can give without a painter or its children.
|
||||||
|
/// Optional, and saves a draw rather than changing one: a hint that
|
||||||
|
/// disagrees with the eventual draw fails a debug assertion.
|
||||||
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
|
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_size_independent(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn requires_exact_region(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn access_role(&self) -> accesskit::Role {
|
|
||||||
accesskit::Role::Unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
fn child_order(&self) -> ChildOrder {
|
|
||||||
ChildOrder::Draw
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for () {
|
impl Widget for () {
|
||||||
fn draw(&mut self, painter: &mut Painter) {
|
/// A gap: nothing drawn, at the default length, so a span gives it a share.
|
||||||
painter.set_size(Size::ZERO);
|
fn draw(&mut self, _: &mut Painter) -> Size {
|
||||||
}
|
Size::default()
|
||||||
|
|
||||||
fn is_size_independent(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
|
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
|
||||||
Some(LayoutLen::ZERO)
|
Some(LayoutLen::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +50,9 @@ impl dyn Widget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A function that returns a widget given a UI.
|
||||||
|
/// Useful for defining trait functions on widgets that create a parent widget so that the children
|
||||||
|
/// don't need to be IDs yet
|
||||||
pub trait WidgetFn<State, W: Widget + ?Sized>: FnOnce(&mut State) -> W {}
|
pub trait WidgetFn<State, W: Widget + ?Sized>: FnOnce(&mut State) -> W {}
|
||||||
impl<State, W: Widget + ?Sized, F: FnOnce(&mut State) -> W> WidgetFn<State, W> for F {}
|
impl<State, W: Widget + ?Sized, F: FnOnce(&mut State) -> W> WidgetFn<State, W> for F {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
use crate::{Axis, LayoutLen, Weight};
|
||||||
|
|
||||||
|
/// What a widget's length on one axis is, as a rule its parent applies where
|
||||||
|
/// it draws it rather than an answer the widget gives about itself.
|
||||||
|
///
|
||||||
|
/// A rule and a drawn size are not two opinions to reconcile: a rule wins on
|
||||||
|
/// the axis it names, and the `Size` returned by `draw` answers only the axes
|
||||||
|
/// with no rule. That is what lets a span divide its space around a length
|
||||||
|
/// nobody has drawn yet, and it is why a rule lives beside the widget rather
|
||||||
|
/// than inside it -- the widget under the rule never has to know about it.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub enum SizeRule {
|
||||||
|
/// Whatever the widget reports from drawing.
|
||||||
|
#[default]
|
||||||
|
Free,
|
||||||
|
/// This length, whatever the widget reports.
|
||||||
|
Exact(LayoutLen),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeRule {
|
||||||
|
/// The length this rule gives without the widget being drawn, if it can
|
||||||
|
/// give one. `leftover` is never among them: a share is a length only to
|
||||||
|
/// whoever divides one, so it passes up in the reported size instead and
|
||||||
|
/// is resolved there.
|
||||||
|
pub fn declared(&self) -> Option<LayoutLen> {
|
||||||
|
match self {
|
||||||
|
Self::Exact(len) if len.leftover == Weight::ZERO => Some(*len),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The length this rule gives outright, whatever the widget reports --
|
||||||
|
/// which makes the widget's answer on that axis moot. A share counts: it
|
||||||
|
/// is a length the widget's parent still has to divide, so it is exact
|
||||||
|
/// here and resolved there, unlike `declared`, which is only the ones
|
||||||
|
/// that give a box directly.
|
||||||
|
pub fn exact(&self) -> Option<LayoutLen> {
|
||||||
|
match self {
|
||||||
|
Self::Free => None,
|
||||||
|
Self::Exact(len) => Some(*len),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The length a widget reporting `reported` ends up with.
|
||||||
|
pub fn apply(&self, reported: LayoutLen) -> LayoutLen {
|
||||||
|
match self {
|
||||||
|
Self::Free => reported,
|
||||||
|
Self::Exact(len) => *len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LayoutLen> for SizeRule {
|
||||||
|
fn from(len: LayoutLen) -> Self {
|
||||||
|
Self::Exact(len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<LayoutLen>> for SizeRule {
|
||||||
|
fn from(len: Option<LayoutLen>) -> Self {
|
||||||
|
len.map_or(Self::Free, Self::Exact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One rule per axis, which is how a widget carries a length on one axis and
|
||||||
|
/// leaves the other to whatever it draws.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
|
pub struct SizeRules {
|
||||||
|
pub x: SizeRule,
|
||||||
|
pub y: SizeRule,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeRules {
|
||||||
|
pub fn axis(&self, axis: Axis) -> SizeRule {
|
||||||
|
match axis {
|
||||||
|
Axis::X => self.x,
|
||||||
|
Axis::Y => self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn axis_mut(&mut self, axis: Axis) -> &mut SizeRule {
|
||||||
|
match axis {
|
||||||
|
Axis::X => &mut self.x,
|
||||||
|
Axis::Y => &mut self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
IdLike, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
|
Axis, AxisAlign, IdLike, RegionAlign, SizeRule, SizeRules, StrongWidget, WeakWidget, Widget,
|
||||||
|
WidgetData, WidgetId,
|
||||||
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -11,7 +12,6 @@ pub struct Widgets {
|
|||||||
send: Sender<WidgetId>,
|
send: Sender<WidgetId>,
|
||||||
recv: Receiver<WidgetId>,
|
recv: Receiver<WidgetId>,
|
||||||
pub(crate) waiting: HashSet<WidgetId>,
|
pub(crate) waiting: HashSet<WidgetId>,
|
||||||
named: HashSet<WidgetId>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widgets {
|
impl Widgets {
|
||||||
@@ -21,7 +21,6 @@ impl Widgets {
|
|||||||
needs_redraw: Default::default(),
|
needs_redraw: Default::default(),
|
||||||
vec: Default::default(),
|
vec: Default::default(),
|
||||||
waiting: Default::default(),
|
waiting: Default::default(),
|
||||||
named: Default::default(),
|
|
||||||
send,
|
send,
|
||||||
recv,
|
recv,
|
||||||
}
|
}
|
||||||
@@ -40,6 +39,8 @@ impl Widgets {
|
|||||||
Some(self.vec.get_mut(id)?.widget.as_mut())
|
Some(self.vec.get_mut(id)?.widget.as_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// get_dyn but dynamic borrow checking of widgets
|
||||||
|
/// lets you do recursive (tree) operations, like the painter does
|
||||||
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> {
|
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> {
|
||||||
// SAFETY: must guarantee no other mutable references to this widget exist
|
// SAFETY: must guarantee no other mutable references to this widget exist
|
||||||
// done through the borrow variable
|
// done through the borrow variable
|
||||||
@@ -95,14 +96,74 @@ impl Widgets {
|
|||||||
&self.data(id.id()).unwrap().label
|
&self.data(id.id()).unwrap().label
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// useful for debugging
|
||||||
pub fn set_label(&mut self, id: impl IdLike, label: String) {
|
pub fn set_label(&mut self, id: impl IdLike, label: String) {
|
||||||
let id = id.id();
|
self.data_mut(id.id()).unwrap().label = label;
|
||||||
self.data_mut(id).unwrap().label = label;
|
|
||||||
self.named.insert(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn named(&self) -> impl Iterator<Item = WidgetId> + '_ {
|
/// Whether this widget owns a movable retained region.
|
||||||
self.named.iter().copied()
|
pub fn is_region_node(&self, id: impl IdLike) -> bool {
|
||||||
|
self.data(id).unwrap().region_node
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chooses whether this widget's retained drawing has one movable region
|
||||||
|
/// of its own. Changing the boundary redraws the subtree once so every
|
||||||
|
/// primitive names the right coordinate space.
|
||||||
|
pub fn set_region_node(&mut self, id: impl IdLike, region_node: bool) {
|
||||||
|
let id = id.id();
|
||||||
|
let data = self.data_mut(id).unwrap();
|
||||||
|
if data.region_node == region_node {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.region_node = region_node;
|
||||||
|
self.needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The length rules whoever draws this widget applies to its box.
|
||||||
|
pub fn size_rules(&self, id: impl IdLike) -> SizeRules {
|
||||||
|
self.data(id).unwrap().size
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets one axis's rule. The widget is marked rather than its parent
|
||||||
|
/// because the parent is not known here; `redraw` escalates a changed
|
||||||
|
/// declared length to whoever resolves it.
|
||||||
|
pub fn set_size_rule(&mut self, id: impl IdLike, axis: Axis, rule: SizeRule) {
|
||||||
|
let id = id.id();
|
||||||
|
let data = self.data_mut(id).unwrap();
|
||||||
|
if *data.size.axis_mut(axis) == rule {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*data.size.axis_mut(axis) = rule;
|
||||||
|
self.needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where this widget sits in a box longer than the length it takes.
|
||||||
|
pub fn alignment(&self, id: impl IdLike) -> RegionAlign {
|
||||||
|
self.data(id).unwrap().align
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets one axis's alignment. Which box a widget ends up in is its
|
||||||
|
/// parent's to decide, so this is escalated the way a length rule is.
|
||||||
|
pub fn set_alignment(&mut self, id: impl IdLike, axis: Axis, align: AxisAlign) {
|
||||||
|
let id = id.id();
|
||||||
|
let data = self.data_mut(id).unwrap();
|
||||||
|
if *data.align.axis_mut(axis) == align {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*data.align.axis_mut(axis) = align;
|
||||||
|
self.needs_redraw.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Both axes at once, for a caller holding a pair.
|
||||||
|
pub fn set_size_rules(
|
||||||
|
&mut self,
|
||||||
|
id: impl IdLike,
|
||||||
|
x: impl Into<SizeRule>,
|
||||||
|
y: impl Into<SizeRule>,
|
||||||
|
) {
|
||||||
|
let id = id.id();
|
||||||
|
self.set_size_rule(id, Axis::X, x.into());
|
||||||
|
self.set_size_rule(id, Axis::Y, y.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
|
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
|
||||||
@@ -112,7 +173,6 @@ impl Widgets {
|
|||||||
pub fn free_next(&mut self) -> Option<WidgetId> {
|
pub fn free_next(&mut self) -> Option<WidgetId> {
|
||||||
let next = self.recv.try_recv().ok()?;
|
let next = self.recv.try_recv().ok()?;
|
||||||
self.vec.free(next);
|
self.vec.free(next);
|
||||||
self.named.remove(&next);
|
|
||||||
Some(next)
|
Some(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
#[iris::android_init]
|
|
||||||
fn create(
|
|
||||||
ui_state: &mut AndroidUiState,
|
|
||||||
rsc: &mut StdRsc<AndroidUiState>,
|
|
||||||
) {
|
|
||||||
let _ = app::build(rsc, ui_state);
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
use winit::event::WindowEvent;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
const SETTLE_FRAMES: usize = 4;
|
|
||||||
const FRAMES: usize = 6;
|
|
||||||
|
|
||||||
#[derive(DesktopUiState)]
|
|
||||||
struct State {
|
|
||||||
ui_state: DesktopUiState,
|
|
||||||
span: WeakWidget<Span>,
|
|
||||||
frame: usize,
|
|
||||||
appended: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DesktopAppState for State {
|
|
||||||
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
|
|
||||||
let span = app::build(rsc, &mut ui_state);
|
|
||||||
Self {
|
|
||||||
ui_state,
|
|
||||||
span,
|
|
||||||
frame: 0,
|
|
||||||
appended: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn window_event(&mut self, event: WindowEvent, rsc: &mut StdRsc<Self>) {
|
|
||||||
if !matches!(event, WindowEvent::RedrawRequested) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.frame += 1;
|
|
||||||
let creates = self.ui_state.renderer.ui.take_image_bind_group_creates();
|
|
||||||
println!(
|
|
||||||
"BENCH_IMAGES frame={} bind_group_creates={creates}",
|
|
||||||
self.frame
|
|
||||||
);
|
|
||||||
if self.frame == SETTLE_FRAMES && !self.appended {
|
|
||||||
self.appended = true;
|
|
||||||
let img = iris::image::DynamicImage::new_rgba8(32, 32);
|
|
||||||
let widget = image::<StdRsc<Self>>(img)(rsc);
|
|
||||||
let widget = rsc.ui.widgets.add_strong(widget);
|
|
||||||
rsc.ui
|
|
||||||
.widgets
|
|
||||||
.get_mut(&self.span)
|
|
||||||
.unwrap()
|
|
||||||
.push(widget.any());
|
|
||||||
println!("BENCH_IMAGES appended one image after settling");
|
|
||||||
}
|
|
||||||
if self.frame < FRAMES {
|
|
||||||
self.ui_state.window.request_redraw();
|
|
||||||
} else {
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
DesktopApp::<State>::run();
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
const ROWS: usize = 1000;
|
|
||||||
|
|
||||||
pub(crate) fn build<Rsc: UiRsc>(
|
|
||||||
rsc: &mut Rsc,
|
|
||||||
ui_state: &mut impl HasRoot<Rsc>,
|
|
||||||
) -> WeakWidget<Span> {
|
|
||||||
let mut span = Span::empty(Dir::DOWN);
|
|
||||||
for _ in 0..ROWS {
|
|
||||||
let img = iris::image::DynamicImage::new_rgba8(32, 32);
|
|
||||||
let widget = image::<Rsc>(img)(rsc);
|
|
||||||
let widget = rsc.ui_mut().widgets.add_strong(widget);
|
|
||||||
span.push(widget.any());
|
|
||||||
}
|
|
||||||
let span = rsc.ui_mut().widgets.add_strong(span);
|
|
||||||
let span_weak = span.weak();
|
|
||||||
let root = rsc
|
|
||||||
.ui_mut()
|
|
||||||
.widgets
|
|
||||||
.add_strong(ScrollArea::new(span.any(), Axis::Y, Pin::End));
|
|
||||||
ui_state.set_root(rsc, root.any());
|
|
||||||
span_weak
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
#[iris::android_init]
|
|
||||||
fn create(
|
|
||||||
ui_state: &mut AndroidUiState,
|
|
||||||
rsc: &mut StdRsc<AndroidUiState>,
|
|
||||||
) {
|
|
||||||
app::build(rsc, ui_state);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
use winit::{dpi::LogicalSize, window::WindowAttributes};
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let attributes = WindowAttributes::default().with_inner_size(LogicalSize::new(420.0, 900.0));
|
|
||||||
DesktopApp::run_with_attributes(attributes, app::build);
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
const ROWS: usize = 800;
|
|
||||||
const IMAGE_EVERY: usize = 12;
|
|
||||||
|
|
||||||
fn row_text(i: usize) -> String {
|
|
||||||
const SENTENCE: &str =
|
|
||||||
"Iris lays out this row once and moves it on scroll, never re-laying it out. ";
|
|
||||||
let repeats = 1 + (i * 7) % 5;
|
|
||||||
format!("Message {i}: {}", SENTENCE.repeat(repeats))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn row_image(i: usize) -> iris::image::DynamicImage {
|
|
||||||
let hue = ((i * 47) % 255) as u8;
|
|
||||||
iris::image::RgbaImage::from_pixel(48, 48, iris::image::Rgba([hue, 128, 255 - hue, 255])).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_row<Rsc: UiRsc + 'static>(rsc: &mut Rsc, i: usize) -> StrongWidget {
|
|
||||||
let tint = if i.is_multiple_of(2) {
|
|
||||||
Srgba8::rgb(120, 130, 170)
|
|
||||||
} else {
|
|
||||||
Srgba8::rgb(70, 80, 140)
|
|
||||||
};
|
|
||||||
let text_color = PaintId::BLACK;
|
|
||||||
if i.is_multiple_of(IMAGE_EVERY) {
|
|
||||||
let text = wtext(row_text(i))
|
|
||||||
.overflow(TextOverflow::Wrap)
|
|
||||||
.color(text_color)
|
|
||||||
.add_strong(rsc)
|
|
||||||
.any();
|
|
||||||
let img = image::<Rsc>(row_image(i))(rsc);
|
|
||||||
let img = rsc.widgets_mut().add_strong(img).any();
|
|
||||||
let mut span = Span::empty(Dir::DOWN);
|
|
||||||
span.push(text);
|
|
||||||
span.push(img);
|
|
||||||
span.pad(dp(8.0))
|
|
||||||
.background(rect(tint))
|
|
||||||
.add_strong(rsc)
|
|
||||||
.any()
|
|
||||||
} else {
|
|
||||||
wtext(row_text(i))
|
|
||||||
.overflow(TextOverflow::Wrap)
|
|
||||||
.color(text_color)
|
|
||||||
.pad(dp(8.0))
|
|
||||||
.background(rect(tint))
|
|
||||||
.add_strong(rsc)
|
|
||||||
.any()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build<Rsc: HasEvents>(rsc: &mut Rsc, ui_state: &mut impl HasRoot<Rsc>) {
|
|
||||||
let mut list = LazySpan::new(Dir::DOWN, Pin::End);
|
|
||||||
for i in 0..ROWS {
|
|
||||||
let row = build_row(rsc, i);
|
|
||||||
list.push_back(LazyItem::new(i as u64, row));
|
|
||||||
}
|
|
||||||
|
|
||||||
let root = list
|
|
||||||
.scrollable()
|
|
||||||
.masked()
|
|
||||||
.background(rect(PaintId::WHITE))
|
|
||||||
.add_strong(rsc);
|
|
||||||
ui_state.set_root(rsc, root.any());
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
rect(Color::RED).set_root(rsc, &mut ui_state);
|
||||||
|
Self { ui_state }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
#[iris::android_init]
|
|
||||||
fn create(
|
|
||||||
ui_state: &mut AndroidUiState,
|
|
||||||
rsc: &mut StdRsc<AndroidUiState>,
|
|
||||||
) {
|
|
||||||
app::build(rsc, ui_state);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
DesktopApp::run_with(app::build);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
pub(crate) fn build<Rsc: UiRsc>(rsc: &mut Rsc, ui_state: &mut impl HasRoot<Rsc>) {
|
|
||||||
rect(PaintId::RED).set_root(rsc, ui_state);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//! The seeded random tree `tests/generated.rs` checks, drawn so it can be
|
||||||
|
//! looked at. `IRIS_SEED` and `IRIS_DEPTH` choose which one.
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
use iris::random::Edits;
|
||||||
|
|
||||||
|
fn env(name: &str, fallback: u64) -> u64 {
|
||||||
|
std::env::var(name)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
DefaultApp::<State>::run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DefaultUiState)]
|
||||||
|
struct State {
|
||||||
|
ui_state: DefaultUiState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultAppState for State {
|
||||||
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
|
let seed = env("IRIS_SEED", 1);
|
||||||
|
let depth = env("IRIS_DEPTH", 4) as usize;
|
||||||
|
let (root, _) = iris::random::grow(rsc, seed, depth, &Edits::default());
|
||||||
|
ui_state.set_root(root);
|
||||||
|
Self { ui_state }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
#[derive(AndroidUiState)]
|
|
||||||
struct Client {
|
|
||||||
ui_state: AndroidUiState,
|
|
||||||
info: WeakWidget<Text>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AndroidAppState for Client {
|
|
||||||
type Resources = StdRsc<Self>;
|
|
||||||
|
|
||||||
fn on_insets_changed(&mut self, rsc: &mut Self::Resources, _: WindowInsets) {
|
|
||||||
let views = self
|
|
||||||
.ui_state
|
|
||||||
.renderer
|
|
||||||
.as_ref()
|
|
||||||
.map_or(0, |renderer| renderer.ui.view_count());
|
|
||||||
app::update_info(rsc, self.info, views);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[iris::android_init]
|
|
||||||
fn create(mut ui_state: AndroidUiState, rsc: &mut StdRsc<Client>) -> Client {
|
|
||||||
let widgets = app::build(rsc, &mut ui_state);
|
|
||||||
app::update_info(rsc, widgets.info, 0);
|
|
||||||
Client {
|
|
||||||
ui_state,
|
|
||||||
info: widgets.info,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Regular → Executable
File mode changed.
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
@@ -1,30 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
use winit::event::WindowEvent;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
#[derive(DesktopUiState)]
|
|
||||||
struct Client {
|
|
||||||
ui_state: DesktopUiState,
|
|
||||||
info: WeakWidget<Text>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DesktopAppState for Client {
|
|
||||||
fn new(mut ui_state: DesktopUiState, rsc: &mut StdRsc<Self>) -> Self {
|
|
||||||
let widgets = app::build(rsc, &mut ui_state);
|
|
||||||
app::update_info(rsc, widgets.info, 0);
|
|
||||||
Self {
|
|
||||||
ui_state,
|
|
||||||
info: widgets.info,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn window_event(&mut self, _: WindowEvent, rsc: &mut StdRsc<Self>) {
|
|
||||||
app::update_info(rsc, self.info, self.ui_state.renderer.ui.view_count());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
DesktopApp::<Client>::run();
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
use std::{cell::RefCell, rc::Rc};
|
|
||||||
|
|
||||||
pub(crate) fn update_info<Rsc: UiRsc>(rsc: &mut Rsc, info: WeakWidget<Text>, views: usize) {
|
|
||||||
let render_state = rsc.ui().render_state();
|
|
||||||
let new = format!(
|
|
||||||
"widgets: {}\nactive: {}\nviews: {views}",
|
|
||||||
rsc.widgets().len(),
|
|
||||||
render_state.get().active_widgets(),
|
|
||||||
);
|
|
||||||
if new != rsc.widgets()[info].content() {
|
|
||||||
rsc.widgets_mut()[info].set_text(new);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct ClientWidgets {
|
|
||||||
pub(crate) info: WeakWidget<Text>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build<Rsc: HasEvents>(
|
|
||||||
rsc: &mut Rsc,
|
|
||||||
ui_state: &mut impl HasRoot<Rsc>,
|
|
||||||
) -> ClientWidgets
|
|
||||||
where
|
|
||||||
Rsc::State: FocusHost,
|
|
||||||
{
|
|
||||||
let rrect = rect(PaintId::WHITE).radius(20);
|
|
||||||
let pad_test = (
|
|
||||||
rrect.clone().color(PaintId::BLUE),
|
|
||||||
(
|
|
||||||
rrect
|
|
||||||
.clone()
|
|
||||||
.color(PaintId::RED)
|
|
||||||
.sized((100, 100))
|
|
||||||
.center()
|
|
||||||
.width(rest(2)),
|
|
||||||
(
|
|
||||||
rrect.clone().color(PaintId::ORANGE),
|
|
||||||
rrect.clone().color(PaintId::LIME).pad(10.0),
|
|
||||||
)
|
|
||||||
.span(Dir::RIGHT)
|
|
||||||
.width(rest(2)),
|
|
||||||
rrect.clone().color(PaintId::YELLOW),
|
|
||||||
)
|
|
||||||
.span(Dir::RIGHT)
|
|
||||||
.pad(10)
|
|
||||||
.width(rest(3)),
|
|
||||||
)
|
|
||||||
.span(Dir::RIGHT)
|
|
||||||
.add(rsc);
|
|
||||||
|
|
||||||
let span_test = (
|
|
||||||
rrect.clone().color(PaintId::GREEN).width(100),
|
|
||||||
rrect.clone().color(PaintId::ORANGE),
|
|
||||||
rrect.clone().color(PaintId::CYAN),
|
|
||||||
rrect.clone().color(PaintId::BLUE).width(rel(0.5)),
|
|
||||||
rrect.clone().color(PaintId::MAGENTA).width(100),
|
|
||||||
rrect.color(PaintId::RED).width(100),
|
|
||||||
)
|
|
||||||
.span(Dir::LEFT)
|
|
||||||
.add(rsc);
|
|
||||||
|
|
||||||
let span_add = Span::empty(Dir::RIGHT).add(rsc);
|
|
||||||
|
|
||||||
let add_button = rect(PaintId::LIME)
|
|
||||||
.radius(30)
|
|
||||||
.on(CursorSense::click(), move |_, rsc| {
|
|
||||||
let child = image(include_bytes!("assets/sungals.png"))
|
|
||||||
.center()
|
|
||||||
.add_strong(rsc);
|
|
||||||
span_add(rsc).push(child);
|
|
||||||
})
|
|
||||||
.sized((150, 150))
|
|
||||||
.align(Align::BOT_RIGHT);
|
|
||||||
|
|
||||||
let del_button = rect(PaintId::RED)
|
|
||||||
.radius(30)
|
|
||||||
.on(CursorSense::click(), move |_, rsc| {
|
|
||||||
span_add(rsc).pop();
|
|
||||||
})
|
|
||||||
.sized((150, 150))
|
|
||||||
.align(Align::BOT_LEFT);
|
|
||||||
|
|
||||||
let span_add_test = (span_add, add_button, del_button).stack().add(rsc);
|
|
||||||
|
|
||||||
let btext = |content| wtext(content).size(30);
|
|
||||||
|
|
||||||
let text_test = (
|
|
||||||
btext("this is a").align(Align::LEFT),
|
|
||||||
btext("teeeeeeeest").align(Align::RIGHT),
|
|
||||||
btext("okkk\nokkkkkk!").align(Align::LEFT),
|
|
||||||
btext("hmm"),
|
|
||||||
btext("a"),
|
|
||||||
(
|
|
||||||
btext("'").family(MONOSPACE).align(Align::TOP),
|
|
||||||
btext("'").family(MONOSPACE),
|
|
||||||
btext(":gamer mode").family(MONOSPACE),
|
|
||||||
rect(PaintId::CYAN).sized((10, 10)).center(),
|
|
||||||
rect(PaintId::RED).sized((100, 100)).center(),
|
|
||||||
rect(PaintId::PURPLE).sized((50, 50)).align(Align::TOP),
|
|
||||||
)
|
|
||||||
.span(Dir::RIGHT)
|
|
||||||
.compact()
|
|
||||||
.center(),
|
|
||||||
wtext("pretty cool right?").size(50),
|
|
||||||
)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.compact()
|
|
||||||
.add(rsc);
|
|
||||||
|
|
||||||
let texts = Span::empty(Dir::DOWN).gap(10).add(rsc);
|
|
||||||
let msg_area = texts
|
|
||||||
.scrollable(Axis::Y, Pin::Start)
|
|
||||||
.masked()
|
|
||||||
.background(rect(PaintId::SKY));
|
|
||||||
let add_text = wtext("add")
|
|
||||||
.editable(EditMode::MultiLine)
|
|
||||||
.text_align(Align::LEFT)
|
|
||||||
.size(30)
|
|
||||||
.attr::<Selectable>(())
|
|
||||||
.on(Submit, move |ctx, rsc: &mut Rsc| {
|
|
||||||
let w = ctx.widget;
|
|
||||||
let content = w(rsc).take();
|
|
||||||
let text = wtext(content)
|
|
||||||
.editable(EditMode::MultiLine)
|
|
||||||
.size(30)
|
|
||||||
.text_align(Align::LEFT)
|
|
||||||
.overflow(TextOverflow::Wrap)
|
|
||||||
.attr::<Selectable>(());
|
|
||||||
let fill = rsc
|
|
||||||
.ui_mut()
|
|
||||||
.paints
|
|
||||||
.add(Srgba8::WHITE.to_linear().darker(0.5));
|
|
||||||
let msg_box = text.background(rect(fill)).add_strong(rsc);
|
|
||||||
texts(rsc).push(msg_box);
|
|
||||||
})
|
|
||||||
.add(rsc);
|
|
||||||
|
|
||||||
let text_edit_scroll = (
|
|
||||||
msg_area.height(rest(1)),
|
|
||||||
(
|
|
||||||
Rect::new(
|
|
||||||
rsc.ui_mut()
|
|
||||||
.paints
|
|
||||||
.add(Srgba8::WHITE.to_linear().darker(0.9)),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
add_text.width(rest(1)),
|
|
||||||
Rect::new(PaintId::GREEN)
|
|
||||||
.on(CursorSense::click(), move |ctx, rsc: &mut Rsc| {
|
|
||||||
rsc.run_event::<Submit>(add_text, (), ctx.state);
|
|
||||||
})
|
|
||||||
.sized((40, 40)),
|
|
||||||
)
|
|
||||||
.span(Dir::RIGHT)
|
|
||||||
.pad(10),
|
|
||||||
)
|
|
||||||
.stack()
|
|
||||||
.size(StackSize::Child(1))
|
|
||||||
.layer_offset(1)
|
|
||||||
.align(Align::BOT),
|
|
||||||
)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.add(rsc);
|
|
||||||
|
|
||||||
let main = WidgetPtr::new().add(rsc);
|
|
||||||
|
|
||||||
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
|
||||||
let mut switch_button = |solid: Srgba8, to: WeakWidget, label| {
|
|
||||||
let value = solid.to_linear();
|
|
||||||
let paint = rsc.ui_mut().paints.add(value);
|
|
||||||
let to = to.upgrade(rsc);
|
|
||||||
let vec = &mut vals.borrow_mut().1;
|
|
||||||
let i = vec.len();
|
|
||||||
if vec.is_empty() {
|
|
||||||
vec.push(None);
|
|
||||||
main(rsc).set(to);
|
|
||||||
} else {
|
|
||||||
vec.push(Some(to));
|
|
||||||
}
|
|
||||||
let vals = vals.clone();
|
|
||||||
let pressed = paint.clone();
|
|
||||||
let hovered = paint.clone();
|
|
||||||
let normal = paint.clone();
|
|
||||||
let rect = rect(paint)
|
|
||||||
.on(CursorSense::click(), move |_ctx, rsc: &mut Rsc| {
|
|
||||||
let (prev, vec) = &mut *vals.borrow_mut();
|
|
||||||
if let Some(h) = vec[i].take() {
|
|
||||||
vec[*prev] = main(rsc).replace(h);
|
|
||||||
*prev = i;
|
|
||||||
}
|
|
||||||
rsc.ui_mut().paints.set(&pressed, value.darker(0.3));
|
|
||||||
})
|
|
||||||
.on(
|
|
||||||
CursorSense::HoverStart | CursorSense::unclick(),
|
|
||||||
move |_ctx, rsc: &mut Rsc| {
|
|
||||||
rsc.ui_mut().paints.set(&hovered, value.brighter(0.2));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.on(CursorSense::HoverEnd, move |_ctx, rsc: &mut Rsc| {
|
|
||||||
rsc.ui_mut().paints.set(&normal, value);
|
|
||||||
})
|
|
||||||
.label(label);
|
|
||||||
(rect, wtext(label).size(30).text_align(Align::CENTER)).stack()
|
|
||||||
};
|
|
||||||
|
|
||||||
let tabs = (
|
|
||||||
switch_button(Srgba8::RED, pad_test, "pad"),
|
|
||||||
switch_button(Srgba8::GREEN, span_test, "span"),
|
|
||||||
switch_button(Srgba8::BLUE, span_add_test, "image span"),
|
|
||||||
switch_button(Srgba8::MAGENTA, text_test, "text layout"),
|
|
||||||
switch_button(Srgba8::YELLOW, text_edit_scroll, "text edit scroll"),
|
|
||||||
)
|
|
||||||
.span(Dir::RIGHT);
|
|
||||||
|
|
||||||
let info = wtext("").add(rsc);
|
|
||||||
let info_sect = info.pad(10).align(Align::RIGHT);
|
|
||||||
|
|
||||||
((tabs.height(40), main.pad(10)).span(Dir::DOWN), info_sect)
|
|
||||||
.stack()
|
|
||||||
.set_root(rsc, ui_state);
|
|
||||||
|
|
||||||
ClientWidgets { info }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
use winit::event::WindowEvent;
|
||||||
|
|
||||||
|
use iris::prelude::*;
|
||||||
|
type ClientRsc = DefaultRsc<Client>;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
DefaultApp::<Client>::run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DefaultUiState)]
|
||||||
|
pub struct Client {
|
||||||
|
ui_state: DefaultUiState,
|
||||||
|
info: WeakWidget<Text>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultAppState for Client {
|
||||||
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
|
let rrect = rect(Color::WHITE).radius(20);
|
||||||
|
let pad_test = (
|
||||||
|
rrect.color(Color::BLUE),
|
||||||
|
(
|
||||||
|
// The square is one widget and the two shares of the row it
|
||||||
|
// sits centred in are another: a length is a property of a
|
||||||
|
// widget, so `.width` here would overwrite the `.sized`.
|
||||||
|
rrect
|
||||||
|
.color(Color::RED)
|
||||||
|
.sized((100, 100))
|
||||||
|
.center()
|
||||||
|
.wrapper()
|
||||||
|
.width(leftover(2)),
|
||||||
|
(
|
||||||
|
rrect.color(Color::ORANGE),
|
||||||
|
rrect.color(Color::LIME).pad(10.0),
|
||||||
|
)
|
||||||
|
.span(Dir::RIGHT)
|
||||||
|
.width(leftover(2)),
|
||||||
|
rrect.color(Color::YELLOW),
|
||||||
|
)
|
||||||
|
.span(Dir::RIGHT)
|
||||||
|
.pad(10)
|
||||||
|
.width(leftover(3)),
|
||||||
|
)
|
||||||
|
.span(Dir::RIGHT)
|
||||||
|
.add(rsc);
|
||||||
|
|
||||||
|
let span_test = (
|
||||||
|
rrect.color(Color::GREEN).width(100),
|
||||||
|
rrect.color(Color::ORANGE),
|
||||||
|
rrect.color(Color::CYAN),
|
||||||
|
rrect.color(Color::BLUE).width(rel(0.5)),
|
||||||
|
rrect.color(Color::MAGENTA).width(100),
|
||||||
|
rrect.color(Color::RED).width(100),
|
||||||
|
)
|
||||||
|
.span(Dir::LEFT)
|
||||||
|
.add(rsc);
|
||||||
|
|
||||||
|
let span_add = Span::empty(Dir::RIGHT).add(rsc);
|
||||||
|
|
||||||
|
let add_button = rect(Color::LIME)
|
||||||
|
.radius(30)
|
||||||
|
.on(CursorSense::click(), move |_, rsc| {
|
||||||
|
let child = image(include_bytes!("assets/sungals.png"))
|
||||||
|
.center()
|
||||||
|
.add_strong(rsc);
|
||||||
|
span_add(rsc).push(child);
|
||||||
|
})
|
||||||
|
.sized((150, 150))
|
||||||
|
.align(Align::BOT_RIGHT);
|
||||||
|
|
||||||
|
let del_button = rect(Color::RED)
|
||||||
|
.radius(30)
|
||||||
|
.on(CursorSense::click(), move |_, rsc| {
|
||||||
|
span_add(rsc).pop();
|
||||||
|
})
|
||||||
|
.sized((150, 150))
|
||||||
|
.align(Align::BOT_LEFT);
|
||||||
|
|
||||||
|
let span_add_test = (span_add, add_button, del_button).stack().add(rsc);
|
||||||
|
|
||||||
|
let btext = |content| wtext(content).size(30);
|
||||||
|
|
||||||
|
let text_test = (
|
||||||
|
btext("this is a").align(Align::LEFT),
|
||||||
|
btext("teeeeeeeest").align(Align::RIGHT),
|
||||||
|
btext("okkk\nokkkkkk!").align(Align::LEFT),
|
||||||
|
btext("hmm"),
|
||||||
|
btext("a"),
|
||||||
|
(
|
||||||
|
btext("'").family(Family::Monospace).align(Align::TOP),
|
||||||
|
btext("'").family(Family::Monospace),
|
||||||
|
btext(":gamer mode").family(Family::Monospace),
|
||||||
|
rect(Color::CYAN).sized((10, 10)).center(),
|
||||||
|
rect(Color::RED).sized((100, 100)).center(),
|
||||||
|
rect(Color::PURPLE).sized((50, 50)).align(Align::TOP),
|
||||||
|
)
|
||||||
|
.span(Dir::RIGHT)
|
||||||
|
.center(),
|
||||||
|
wtext("pretty cool right?").size(50),
|
||||||
|
)
|
||||||
|
.span(Dir::DOWN)
|
||||||
|
.add(rsc);
|
||||||
|
|
||||||
|
let texts = Span::empty(Dir::DOWN).gap(10).add(rsc);
|
||||||
|
let msg_area = texts.scrollable().masked().background(rect(Color::SKY));
|
||||||
|
let add_text = wtext("add")
|
||||||
|
.editable(EditMode::MultiLine)
|
||||||
|
.text_align(Align::LEFT)
|
||||||
|
.size(30)
|
||||||
|
.attr::<Selectable>(())
|
||||||
|
.on(Submit, move |ctx, rsc| {
|
||||||
|
let w = ctx.widget;
|
||||||
|
let content = w.edit(rsc).take();
|
||||||
|
let text = wtext(content)
|
||||||
|
.editable(EditMode::MultiLine)
|
||||||
|
.size(30)
|
||||||
|
.text_align(Align::LEFT)
|
||||||
|
.wrap(true)
|
||||||
|
.attr::<Selectable>(());
|
||||||
|
let msg_box = text
|
||||||
|
.background(rect(Color::WHITE.darker(0.5)))
|
||||||
|
.add_strong(rsc);
|
||||||
|
texts(rsc).push(msg_box);
|
||||||
|
})
|
||||||
|
.add(rsc);
|
||||||
|
|
||||||
|
let text_edit_scroll = (
|
||||||
|
msg_area.height(leftover(1)),
|
||||||
|
(
|
||||||
|
Rect::new(Color::WHITE.darker(0.9)),
|
||||||
|
(
|
||||||
|
add_text.width(leftover(1)),
|
||||||
|
Rect::new(Color::GREEN)
|
||||||
|
.on(CursorSense::click(), move |ctx, rsc: &mut ClientRsc| {
|
||||||
|
rsc.run_event::<Submit>(add_text, (), ctx.state);
|
||||||
|
})
|
||||||
|
.sized((40, 40)),
|
||||||
|
)
|
||||||
|
.span(Dir::RIGHT)
|
||||||
|
.pad(10),
|
||||||
|
)
|
||||||
|
.stack()
|
||||||
|
.size(StackSize::Child(1))
|
||||||
|
.layer_offset(1)
|
||||||
|
.align(Align::BOT),
|
||||||
|
)
|
||||||
|
.span(Dir::DOWN)
|
||||||
|
.add(rsc);
|
||||||
|
|
||||||
|
let main = Wrapper::new().add(rsc);
|
||||||
|
|
||||||
|
let vals = Rc::new(RefCell::new((0, Vec::new())));
|
||||||
|
let mut switch_button = |color, to: WeakWidget, label| {
|
||||||
|
let to = to.upgrade(rsc);
|
||||||
|
let vec = &mut vals.borrow_mut().1;
|
||||||
|
let i = vec.len();
|
||||||
|
if vec.is_empty() {
|
||||||
|
vec.push(None);
|
||||||
|
main(rsc).set(to);
|
||||||
|
} else {
|
||||||
|
vec.push(Some(to));
|
||||||
|
}
|
||||||
|
let vals = vals.clone();
|
||||||
|
let rect = rect(color)
|
||||||
|
.on(CursorSense::click(), move |ctx, rsc| {
|
||||||
|
let (prev, vec) = &mut *vals.borrow_mut();
|
||||||
|
if let Some(h) = vec[i].take() {
|
||||||
|
vec[*prev] = main(rsc).replace(h);
|
||||||
|
*prev = i;
|
||||||
|
}
|
||||||
|
ctx.widget(rsc).color = color.darker(0.3);
|
||||||
|
})
|
||||||
|
.on(
|
||||||
|
CursorSense::HoverStart | CursorSense::unclick(),
|
||||||
|
move |ctx, rsc| {
|
||||||
|
ctx.widget(rsc).color = color.brighter(0.2);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.on(CursorSense::HoverEnd, move |ctx, rsc| {
|
||||||
|
ctx.widget(rsc).color = color;
|
||||||
|
});
|
||||||
|
(rect, wtext(label).size(30).text_align(Align::CENTER)).stack()
|
||||||
|
};
|
||||||
|
|
||||||
|
let tabs = (
|
||||||
|
switch_button(Color::RED, pad_test, "pad"),
|
||||||
|
switch_button(Color::GREEN, span_test, "span"),
|
||||||
|
switch_button(Color::BLUE, span_add_test, "image span"),
|
||||||
|
switch_button(Color::MAGENTA, text_test, "text layout"),
|
||||||
|
switch_button(
|
||||||
|
Color::YELLOW.mul_rgb(0.5),
|
||||||
|
text_edit_scroll,
|
||||||
|
"text edit scroll",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.span(Dir::RIGHT);
|
||||||
|
|
||||||
|
let info = wtext("").add(rsc);
|
||||||
|
let info_sect = info.pad(10).align(Align::RIGHT);
|
||||||
|
|
||||||
|
((tabs.height(40), main.pad(10)).span(Dir::DOWN), info_sect)
|
||||||
|
.stack()
|
||||||
|
.set_root(rsc, &mut ui_state);
|
||||||
|
|
||||||
|
Self { ui_state, info }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(
|
||||||
|
&mut self,
|
||||||
|
_: WindowEvent,
|
||||||
|
rsc: &mut DefaultRsc<Self>,
|
||||||
|
render: &mut UiRenderState,
|
||||||
|
) {
|
||||||
|
let new = format!(
|
||||||
|
"widgets: {}\nactive: {}\ntextures: {}",
|
||||||
|
rsc.widgets().len(),
|
||||||
|
render.active_widgets(),
|
||||||
|
rsc.ui().textures.count(),
|
||||||
|
);
|
||||||
|
if new != *rsc.widgets()[self.info].content {
|
||||||
|
*rsc.widgets_mut()[self.info].content = new;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use iris::prelude::*;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
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 rect = rect(Color::RED).add(rsc);
|
||||||
|
rect.task_on(CursorSense::click(), async move |mut ctx| {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
ctx.update(move |_, rsc| {
|
||||||
|
let rect = rect(rsc);
|
||||||
|
if rect.color == Color::RED {
|
||||||
|
rect.color = Color::BLUE;
|
||||||
|
} else {
|
||||||
|
rect.color = Color::RED;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.set_root(rsc, &mut ui_state);
|
||||||
|
Self { ui_state }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
#[iris::android_init]
|
|
||||||
fn create(
|
|
||||||
ui_state: &mut AndroidUiState,
|
|
||||||
rsc: &mut StdRsc<AndroidUiState>,
|
|
||||||
) {
|
|
||||||
app::build(rsc, ui_state);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
DesktopApp::run_with(app::build);
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub(crate) fn build<Rsc: HasEvents + HasTasks>(rsc: &mut Rsc, ui_state: &mut impl HasRoot<Rsc>)
|
|
||||||
where
|
|
||||||
Rsc::State: FocusHost,
|
|
||||||
{
|
|
||||||
let rect = rect(PaintId::RED).add(rsc);
|
|
||||||
rect.label("Toggle color")
|
|
||||||
.task_on(CursorSense::click(), async move |mut ctx| {
|
|
||||||
iris::task::sleep(Duration::from_secs(1)).await;
|
|
||||||
ctx.update(move |_, rsc| {
|
|
||||||
let rect = rect(rsc);
|
|
||||||
if rect.is_paint(&PaintId::RED) {
|
|
||||||
rect.set_paint(PaintId::BLUE);
|
|
||||||
} else {
|
|
||||||
rect.set_paint(PaintId::RED);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.set_root(rsc, ui_state);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
//! 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());
|
||||||
|
|
||||||
|
// Each one takes the whole width, because `text_align` puts the
|
||||||
|
// glyphs somewhere in the box the text is given and a text that
|
||||||
|
// reports the width of its own glyphs is given exactly that.
|
||||||
|
let label = |text: &str, align| wtext(text).size(24).text_align(align).width(rel(1.0));
|
||||||
|
let aligned = (
|
||||||
|
label("left", Align::LEFT),
|
||||||
|
label("centred", Align::H_CENTER),
|
||||||
|
label("right", 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,9 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
#[iris::android_init]
|
|
||||||
fn create(ui_state: &mut AndroidUiState, rsc: &mut StdRsc<AndroidUiState>) {
|
|
||||||
app::build(rsc, ui_state);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
DesktopApp::run_with(app::build);
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
const SAMPLE: &str = "The quick brown fox jumps over the lazy dog";
|
|
||||||
|
|
||||||
fn overflow_row<Rsc: UiRsc + 'static>(
|
|
||||||
rsc: &mut Rsc,
|
|
||||||
label: &str,
|
|
||||||
overflow: TextOverflow,
|
|
||||||
position: Len,
|
|
||||||
fill: PaintId,
|
|
||||||
) -> WeakWidget<Sized> {
|
|
||||||
(
|
|
||||||
wtext(label).size(14).color(PaintId::GRAY).width(dp(76)),
|
|
||||||
wtext(SAMPLE)
|
|
||||||
.size(20)
|
|
||||||
.overflow(overflow)
|
|
||||||
.overflow_position(position)
|
|
||||||
.width(dp(260))
|
|
||||||
.background(rect(fill)),
|
|
||||||
)
|
|
||||||
.span(Dir::RIGHT)
|
|
||||||
.gap(dp(8))
|
|
||||||
.width(dp(344))
|
|
||||||
.add(rsc)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build<Rsc: HasEvents + 'static>(rsc: &mut Rsc, ui_state: &mut impl HasRoot<Rsc>)
|
|
||||||
where
|
|
||||||
Rsc::State: FocusHost,
|
|
||||||
{
|
|
||||||
let panel = rsc
|
|
||||||
.ui_mut()
|
|
||||||
.paints
|
|
||||||
.add(Srgba8::new(34, 36, 42, 255).to_linear());
|
|
||||||
let field = rsc
|
|
||||||
.ui_mut()
|
|
||||||
.paints
|
|
||||||
.add(Srgba8::new(53, 57, 66, 255).to_linear());
|
|
||||||
|
|
||||||
let styled = "Bold, italic, underlined, and colored spans";
|
|
||||||
let styled = wtext(styled).size(20).spans(vec![
|
|
||||||
SpanStyle::new(0..4).bold(),
|
|
||||||
SpanStyle::new(6..12).italic(),
|
|
||||||
SpanStyle::new(14..24).underline(),
|
|
||||||
SpanStyle::new(30..37).color(PaintId::SKY),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let aligned = (
|
|
||||||
wtext("Left aligned").text_align(Align::CENTER_LEFT),
|
|
||||||
wtext("Centered").text_align(Align::CENTER),
|
|
||||||
wtext("Right aligned").text_align(Align::CENTER_RIGHT),
|
|
||||||
)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.gap(dp(4))
|
|
||||||
.width(rest(1))
|
|
||||||
.background(rect(field.clone()));
|
|
||||||
|
|
||||||
let wrapped = wtext(
|
|
||||||
"Wrapping shapes the same source into as many lines as its container needs. Resize the window to see it reflow.",
|
|
||||||
)
|
|
||||||
.overflow(TextOverflow::Wrap)
|
|
||||||
.size(18)
|
|
||||||
.width(dp(340))
|
|
||||||
.background(rect(field.clone()));
|
|
||||||
|
|
||||||
let editable = wtext(SAMPLE)
|
|
||||||
.overflow(TextOverflow::Ellipsis)
|
|
||||||
.editable(EditMode::SingleLine)
|
|
||||||
.size(20)
|
|
||||||
.attr::<Selectable>(())
|
|
||||||
.width(dp(344))
|
|
||||||
.background(rect(field.clone()));
|
|
||||||
|
|
||||||
let intro = (
|
|
||||||
wtext("Iris text")
|
|
||||||
.size(30)
|
|
||||||
.spans(vec![SpanStyle::new(0..9).bold()]),
|
|
||||||
wtext("Drag across display text to select it. The overflow markers select hidden source text, but are never copied.")
|
|
||||||
.overflow(TextOverflow::Wrap)
|
|
||||||
.color(PaintId::GRAY),
|
|
||||||
)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.gap(dp(6))
|
|
||||||
.add(rsc);
|
|
||||||
let styles = (
|
|
||||||
wtext("Styles and alignment").size(16).color(PaintId::SKY),
|
|
||||||
styled,
|
|
||||||
aligned,
|
|
||||||
)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.gap(dp(6))
|
|
||||||
.add(rsc);
|
|
||||||
let wrapping = (wtext("Wrapping").size(16).color(PaintId::SKY), wrapped)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.gap(dp(6))
|
|
||||||
.add(rsc);
|
|
||||||
let overflow = (
|
|
||||||
wtext("Overflow treatment and position")
|
|
||||||
.size(16)
|
|
||||||
.color(PaintId::SKY),
|
|
||||||
overflow_row(rsc, "hidden", TextOverflow::Hidden, rel(0), field.clone()),
|
|
||||||
overflow_row(
|
|
||||||
rsc,
|
|
||||||
"ellipsis 0",
|
|
||||||
TextOverflow::Ellipsis,
|
|
||||||
rel(0),
|
|
||||||
field.clone(),
|
|
||||||
),
|
|
||||||
overflow_row(
|
|
||||||
rsc,
|
|
||||||
"ellipsis .5",
|
|
||||||
TextOverflow::Ellipsis,
|
|
||||||
rel(0.5),
|
|
||||||
field.clone(),
|
|
||||||
),
|
|
||||||
overflow_row(
|
|
||||||
rsc,
|
|
||||||
"ellipsis 1",
|
|
||||||
TextOverflow::Ellipsis,
|
|
||||||
rel(1),
|
|
||||||
field.clone(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.gap(dp(6))
|
|
||||||
.add(rsc);
|
|
||||||
let editing = (
|
|
||||||
wtext("Editable ellipsis (move the caret through the text)")
|
|
||||||
.size(16)
|
|
||||||
.color(PaintId::SKY),
|
|
||||||
editable,
|
|
||||||
)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.gap(dp(6))
|
|
||||||
.add(rsc);
|
|
||||||
|
|
||||||
let content = (intro, styles, wrapping, overflow, editing)
|
|
||||||
.span(Dir::DOWN)
|
|
||||||
.gap(dp(14))
|
|
||||||
.controller(SelectionController::new().separator("\n"))
|
|
||||||
.add(rsc);
|
|
||||||
|
|
||||||
content
|
|
||||||
.on(CursorSense::drag_senses(), move |ctx, rsc: &mut Rsc| {
|
|
||||||
let input = &ctx.data;
|
|
||||||
rsc.with_nearest_controller::<SelectionController, _>(content, |id, selection, rsc| {
|
|
||||||
selection.drag(id, rsc, input)
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.add(rsc);
|
|
||||||
content
|
|
||||||
.pad(dp(24))
|
|
||||||
.width(rest(1))
|
|
||||||
.background(rect(panel))
|
|
||||||
.set_root(rsc, ui_state);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
use iris::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
DefaultApp::<State>::run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DefaultUiState)]
|
||||||
|
struct State {
|
||||||
|
ui_state: DefaultUiState,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rsc = DefaultRsc<State>;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, WidgetView)]
|
||||||
|
struct Test {
|
||||||
|
#[root]
|
||||||
|
root: WeakWidget<Rect>,
|
||||||
|
cur: WeakState<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Test {
|
||||||
|
pub fn new(rsc: &mut Rsc) -> Self {
|
||||||
|
let root = rect(Color::RED).add(rsc);
|
||||||
|
let cur = rsc.create_state(root, false);
|
||||||
|
Self { root, cur }
|
||||||
|
}
|
||||||
|
pub fn toggle(&self, rsc: &mut Rsc) {
|
||||||
|
let cur = &mut rsc[self.cur];
|
||||||
|
*cur = !*cur;
|
||||||
|
if *cur {
|
||||||
|
rsc[self.root].color = Color::BLUE;
|
||||||
|
} else {
|
||||||
|
rsc[self.root].color = Color::RED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultAppState for State {
|
||||||
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, _: Proxy<Self>) -> Self {
|
||||||
|
let test = Test::new(rsc);
|
||||||
|
|
||||||
|
test.on(CursorSense::click(), move |_, rsc| {
|
||||||
|
test.toggle(rsc);
|
||||||
|
})
|
||||||
|
.set_root(rsc, &mut ui_state);
|
||||||
|
|
||||||
|
Self { ui_state }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
#[iris::android_init]
|
|
||||||
fn create(
|
|
||||||
ui_state: &mut AndroidUiState,
|
|
||||||
rsc: &mut StdRsc<AndroidUiState>,
|
|
||||||
) {
|
|
||||||
app::build(rsc, ui_state);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[path = "lib.rs"]
|
|
||||||
mod app;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
DesktopApp::run_with(app::build);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, WidgetView)]
|
|
||||||
struct Test {
|
|
||||||
#[root]
|
|
||||||
root: WeakWidget<Rect>,
|
|
||||||
cur: WeakState<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Test {
|
|
||||||
pub fn new<State>(rsc: &mut StdRsc<State>) -> Self {
|
|
||||||
let root = rect(PaintId::RED).add(rsc);
|
|
||||||
let cur = rsc.create_state(root, false);
|
|
||||||
Self { root, cur }
|
|
||||||
}
|
|
||||||
pub fn toggle<State>(&self, rsc: &mut StdRsc<State>) {
|
|
||||||
let cur = &mut rsc[self.cur];
|
|
||||||
*cur = !*cur;
|
|
||||||
if *cur {
|
|
||||||
rsc[self.root].set_paint(PaintId::BLUE);
|
|
||||||
} else {
|
|
||||||
rsc[self.root].set_paint(PaintId::RED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build<State>(rsc: &mut StdRsc<State>, ui_state: &mut impl HasRoot<StdRsc<State>>)
|
|
||||||
where
|
|
||||||
State: FocusHost,
|
|
||||||
{
|
|
||||||
let test = Test::new(rsc);
|
|
||||||
test.on(CursorSense::click(), move |_, rsc| {
|
|
||||||
test.toggle(rsc);
|
|
||||||
})
|
|
||||||
.set_root(rsc, ui_state);
|
|
||||||
}
|
|
||||||
+3
-3
@@ -4,9 +4,9 @@ version.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
proc-macro2 = "1.0.107"
|
proc-macro2 = "1.0.103"
|
||||||
quote = "1.0.47"
|
quote = "1.0.42"
|
||||||
syn = { version = "3.0.5", features = ["full"] }
|
syn = { version = "2.0.111", features = ["full"] }
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
+22
-184
@@ -2,115 +2,13 @@ extern crate proc_macro;
|
|||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{
|
use syn::{
|
||||||
Attribute, Block, Error, FnArg, GenericParam, Generics, Ident, ItemFn, ItemStruct, ItemTrait,
|
Attribute, Block, Error, GenericParam, Generics, Ident, ItemStruct, ItemTrait, Signature,
|
||||||
ReturnType, Signature, Token, Type, Visibility,
|
Token, Type, Visibility,
|
||||||
parse::{Parse, ParseStream, Result},
|
parse::{Parse, ParseStream, Result},
|
||||||
parse_macro_input, parse_quote,
|
parse_macro_input, parse_quote,
|
||||||
spanned::Spanned,
|
spanned::Spanned,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Marks the initializer called when Android creates an Iris view.
|
|
||||||
///
|
|
||||||
/// An attribute is necessary here because the Android loader requires one
|
|
||||||
/// exported `JNI_OnLoad` symbol and `android-view` requires a plain function
|
|
||||||
/// pointer monomorphized for the application state. A function returning a
|
|
||||||
/// custom state remains its factory; a function with no return value receives
|
|
||||||
/// `&mut AndroidUiState` and uses that state directly. The generated linker and
|
|
||||||
/// JNI glue is Android-gated; the annotated function therefore does not need
|
|
||||||
/// its own `cfg` attribute.
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn android_init(args: TokenStream, item: TokenStream) -> TokenStream {
|
|
||||||
if !args.is_empty() {
|
|
||||||
return Error::new(
|
|
||||||
proc_macro2::Span::call_site(),
|
|
||||||
"android_init takes no arguments",
|
|
||||||
)
|
|
||||||
.into_compile_error()
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let function = parse_macro_input!(item as ItemFn);
|
|
||||||
let name = &function.sig.ident;
|
|
||||||
let (state, direct_initializer): (Type, bool) = match &function.sig.output {
|
|
||||||
ReturnType::Default => (parse_quote!(::iris::android::AndroidUiState), true),
|
|
||||||
ReturnType::Type(_, state) => ((**state).clone(), false),
|
|
||||||
};
|
|
||||||
if function.sig.inputs.len() != 2
|
|
||||||
|| function
|
|
||||||
.sig
|
|
||||||
.inputs
|
|
||||||
.iter()
|
|
||||||
.any(|argument| !matches!(argument, FnArg::Typed(_)))
|
|
||||||
{
|
|
||||||
return Error::new(
|
|
||||||
function.sig.inputs.span(),
|
|
||||||
"an android_init function takes UI state and resources",
|
|
||||||
)
|
|
||||||
.into_compile_error()
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
if function.sig.asyncness.is_some()
|
|
||||||
|| function.sig.constness.is_some()
|
|
||||||
|| matches!(function.sig.safety, syn::Safety::Unsafe(_))
|
|
||||||
|| !function.sig.generics.params.is_empty()
|
|
||||||
{
|
|
||||||
return Error::new(
|
|
||||||
function.sig.span(),
|
|
||||||
"an android_init function must be a plain, non-generic synchronous function",
|
|
||||||
)
|
|
||||||
.into_compile_error()
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let factory = if direct_initializer {
|
|
||||||
quote! {
|
|
||||||
fn init(
|
|
||||||
mut ui_state: ::iris::android::AndroidUiState,
|
|
||||||
rsc: &mut <#state as ::iris::android::AndroidAppState>::Resources,
|
|
||||||
) -> #state {
|
|
||||||
super::#name(&mut ui_state, rsc);
|
|
||||||
ui_state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quote! {}
|
|
||||||
};
|
|
||||||
let create = if direct_initializer {
|
|
||||||
quote! { init }
|
|
||||||
} else {
|
|
||||||
quote! { super::#name }
|
|
||||||
};
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
#function
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
mod __iris_android_app {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#factory
|
|
||||||
|
|
||||||
extern "system" fn new_view_peer<'local>(
|
|
||||||
env: ::iris::android::__private::JNIEnv<'local>,
|
|
||||||
view: ::iris::android::__private::View<'local>,
|
|
||||||
context: ::iris::android::__private::Context<'local>,
|
|
||||||
) -> ::iris::android::__private::JLong {
|
|
||||||
::iris::android::new_peer::<#state>(env, view, context, #create)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub unsafe extern "system" fn JNI_OnLoad(
|
|
||||||
vm: *mut ::iris::android::__private::RawJavaVM,
|
|
||||||
_: *mut ::core::ffi::c_void,
|
|
||||||
) -> ::iris::android::__private::JInt {
|
|
||||||
unsafe { ::iris::android::__private::on_load(vm, new_view_peer) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Input {
|
struct Input {
|
||||||
attrs: Vec<Attribute>,
|
attrs: Vec<Attribute>,
|
||||||
vis: Visibility,
|
vis: Visibility,
|
||||||
@@ -120,7 +18,6 @@ struct Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct InputFn {
|
struct InputFn {
|
||||||
attrs: Vec<Attribute>,
|
|
||||||
sig: Signature,
|
sig: Signature,
|
||||||
body: Block,
|
body: Block,
|
||||||
}
|
}
|
||||||
@@ -135,10 +32,9 @@ impl Parse for Input {
|
|||||||
input.parse::<Token![;]>()?;
|
input.parse::<Token![;]>()?;
|
||||||
let mut fns = Vec::new();
|
let mut fns = Vec::new();
|
||||||
while !input.is_empty() {
|
while !input.is_empty() {
|
||||||
let attrs = input.call(Attribute::parse_outer)?;
|
|
||||||
let sig = input.parse()?;
|
let sig = input.parse()?;
|
||||||
let body = input.parse()?;
|
let body = input.parse()?;
|
||||||
fns.push(InputFn { attrs, sig, body })
|
fns.push(InputFn { sig, body })
|
||||||
}
|
}
|
||||||
if !input.is_empty() {
|
if !input.is_empty() {
|
||||||
input.error("function expected");
|
input.error("function expected");
|
||||||
@@ -163,13 +59,10 @@ pub fn widget_trait(input: TokenStream) -> TokenStream {
|
|||||||
fns,
|
fns,
|
||||||
} = parse_macro_input!(input as Input);
|
} = parse_macro_input!(input as Input);
|
||||||
|
|
||||||
let sigs: Vec<_> = fns
|
let sigs: Vec<_> = fns.iter().map(|f| f.sig.clone()).collect();
|
||||||
.iter()
|
|
||||||
.map(|InputFn { attrs, sig, .. }| quote! { #(#attrs)* #sig })
|
|
||||||
.collect();
|
|
||||||
let impls: Vec<_> = fns
|
let impls: Vec<_> = fns
|
||||||
.iter()
|
.iter()
|
||||||
.map(|InputFn { sig, body, .. }| quote! { #sig #body })
|
.map(|InputFn { sig, body }| quote! { #sig #body })
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let Some(GenericParam::Type(state)) = generics.params.first() else {
|
let Some(GenericParam::Type(state)) = generics.params.first() else {
|
||||||
@@ -203,74 +96,33 @@ pub fn widget_trait(input: TokenStream) -> TokenStream {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(DesktopUiState, attributes(desktop_ui_state))]
|
#[proc_macro_derive(DefaultUiState, attributes(default_ui_state))]
|
||||||
pub fn derive_desktop_ui_state(input: TokenStream) -> TokenStream {
|
pub fn derive_default_ui_state(input: TokenStream) -> TokenStream {
|
||||||
let state: ItemStruct = parse_macro_input!(input);
|
|
||||||
derive_ui_state(
|
|
||||||
state,
|
|
||||||
UiStateDerive {
|
|
||||||
module: "desktop",
|
|
||||||
state_type: "DesktopUiState",
|
|
||||||
state_trait: "HasDesktopUiState",
|
|
||||||
field_attr: "desktop_ui_state",
|
|
||||||
get: "desktop_state",
|
|
||||||
get_mut: "desktop_state_mut",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_derive(AndroidUiState, attributes(android_ui_state))]
|
|
||||||
pub fn derive_android_ui_state(input: TokenStream) -> TokenStream {
|
|
||||||
let state: ItemStruct = parse_macro_input!(input);
|
|
||||||
derive_ui_state(
|
|
||||||
state,
|
|
||||||
UiStateDerive {
|
|
||||||
module: "android",
|
|
||||||
state_type: "AndroidUiState",
|
|
||||||
state_trait: "HasAndroidUiState",
|
|
||||||
field_attr: "android_ui_state",
|
|
||||||
get: "android_state",
|
|
||||||
get_mut: "android_state_mut",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UiStateDerive {
|
|
||||||
module: &'static str,
|
|
||||||
state_type: &'static str,
|
|
||||||
state_trait: &'static str,
|
|
||||||
field_attr: &'static str,
|
|
||||||
get: &'static str,
|
|
||||||
get_mut: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_ui_state(state: ItemStruct, names: UiStateDerive) -> TokenStream {
|
|
||||||
let UiStateDerive {
|
|
||||||
module,
|
|
||||||
state_type,
|
|
||||||
state_trait,
|
|
||||||
field_attr,
|
|
||||||
get,
|
|
||||||
get_mut,
|
|
||||||
} = names;
|
|
||||||
let mut output = proc_macro2::TokenStream::new();
|
let mut output = proc_macro2::TokenStream::new();
|
||||||
|
|
||||||
|
let state: ItemStruct = parse_macro_input!(input);
|
||||||
|
|
||||||
let mut found_attr = false;
|
let mut found_attr = false;
|
||||||
let mut state_field = None;
|
let mut state_field = None;
|
||||||
for field in &state.fields {
|
for field in &state.fields {
|
||||||
if !found_attr
|
if !found_attr
|
||||||
&& let Type::Path(path) = &field.ty
|
&& let Type::Path(path) = &field.ty
|
||||||
&& path.path.is_ident(state_type)
|
&& path.path.is_ident("DefaultUiState")
|
||||||
{
|
{
|
||||||
state_field = Some(field);
|
state_field = Some(field);
|
||||||
}
|
}
|
||||||
let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident(field_attr)) else {
|
let Some(attr) = field
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.path().is_ident("default_ui_state"))
|
||||||
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if found_attr {
|
if found_attr {
|
||||||
output.extend(
|
output.extend(
|
||||||
Error::new(
|
Error::new(
|
||||||
attr.span(),
|
attr.span(),
|
||||||
format!("cannot have more than one {field_attr} attribute"),
|
"cannot have more than one default_ui_state attribute",
|
||||||
)
|
)
|
||||||
.into_compile_error(),
|
.into_compile_error(),
|
||||||
);
|
);
|
||||||
@@ -281,32 +133,18 @@ fn derive_ui_state(state: ItemStruct, names: UiStateDerive) -> TokenStream {
|
|||||||
}
|
}
|
||||||
let Some(field) = state_field else {
|
let Some(field) = state_field else {
|
||||||
output.extend(
|
output.extend(
|
||||||
Error::new(state.ident.span(), format!("no {state_type} field found"))
|
Error::new(state.ident.span(), "no DefaultUiState field found").into_compile_error(),
|
||||||
.into_compile_error(),
|
|
||||||
);
|
);
|
||||||
return output.into();
|
return output.into();
|
||||||
};
|
};
|
||||||
let sname = &state.ident;
|
let sname = &state.ident;
|
||||||
let Some(fname) = field.ident.as_ref() else {
|
let fname = field.ident.as_ref().unwrap();
|
||||||
return Error::new(
|
|
||||||
field.span(),
|
|
||||||
format!("the {state_type} field must be named"),
|
|
||||||
)
|
|
||||||
.into_compile_error()
|
|
||||||
.into();
|
|
||||||
};
|
|
||||||
let module = Ident::new(module, sname.span());
|
|
||||||
let state_type = Ident::new(state_type, sname.span());
|
|
||||||
let state_trait = Ident::new(state_trait, sname.span());
|
|
||||||
let get = Ident::new(get, sname.span());
|
|
||||||
let get_mut = Ident::new(get_mut, sname.span());
|
|
||||||
let (impl_generics, type_generics, where_clause) = state.generics.split_for_impl();
|
|
||||||
output.extend(quote! {
|
output.extend(quote! {
|
||||||
impl #impl_generics iris::#module::#state_trait for #sname #type_generics #where_clause {
|
impl iris::default::HasDefaultUiState for #sname {
|
||||||
fn #get(&self) -> &iris::#module::#state_type {
|
fn default_state(&self) -> &iris::default::DefaultUiState {
|
||||||
&self.#fname
|
&self.#fname
|
||||||
}
|
}
|
||||||
fn #get_mut(&mut self) -> &mut iris::#module::#state_type {
|
fn default_state_mut(&mut self) -> &mut iris::default::DefaultUiState {
|
||||||
&mut self.#fname
|
&mut self.#fname
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,106 +4,7 @@ My experimental attempt at a rust ui library (also my first ui library).
|
|||||||
|
|
||||||
It's currently designed around using retained data structures (widgets), rather than diffing generated trees from data like xilem or iced. This is an experiment and I'm not sure if it's a good idea or not.
|
It's currently designed around using retained data structures (widgets), rather than diffing generated trees from data like xilem or iced. This is an experiment and I'm not sure if it's a good idea or not.
|
||||||
|
|
||||||
Examples are in `examples`, eg. `cargo run --example tabs`. Each example keeps
|
Examples are in `examples`, eg. `cargo run --example tabs`.
|
||||||
its widget tree in `lib.rs` and its small desktop and Android hosts in
|
|
||||||
`desktop.rs` and `android.rs`.
|
|
||||||
|
|
||||||
## Android applications
|
|
||||||
|
|
||||||
An Android application is a library because Android loads its Rust code as a
|
|
||||||
native shared library:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib", "rlib"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
iris = { path = "../iris" }
|
|
||||||
|
|
||||||
[package.metadata.iris.android]
|
|
||||||
application-id = "com.example.myapp"
|
|
||||||
label = "My app"
|
|
||||||
```
|
|
||||||
|
|
||||||
`#[iris::android_init]` marks the initializer called when Android creates the Iris
|
|
||||||
view. The attribute supplies its own Android target gate and generates the JNI
|
|
||||||
loader glue. An application with no state beyond the UI state receives
|
|
||||||
`AndroidUiState` directly by mutable reference:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use iris::prelude::*;
|
|
||||||
|
|
||||||
fn build<Rsc: UiRsc>(rsc: &mut Rsc, ui_state: &mut impl HasRoot<Rsc>) {
|
|
||||||
rect(PaintId::RED).set_root(rsc, ui_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[iris::android_init]
|
|
||||||
fn create(
|
|
||||||
ui_state: &mut AndroidUiState,
|
|
||||||
rsc: &mut StdRsc<AndroidUiState>,
|
|
||||||
) {
|
|
||||||
build(rsc, ui_state);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Desktop has the corresponding initializer form:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
DesktopApp::run_with(build);
|
|
||||||
```
|
|
||||||
|
|
||||||
Background work updates either host through the same task context. An update
|
|
||||||
wakes the UI thread; Iris schedules a frame automatically if the closure made
|
|
||||||
the retained widget tree dirty:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
rsc.spawn_task(async move |mut ctx| {
|
|
||||||
let text = load_text().await;
|
|
||||||
ctx.update(move |_, rsc| label(rsc).set(&text));
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Applications do not need a winit event proxy or an explicit redraw request.
|
|
||||||
|
|
||||||
Applications with additional fields use their own state type. Its
|
|
||||||
`AndroidAppState::Resources` associated type can also replace `StdRsc` with a
|
|
||||||
custom resource bundle.
|
|
||||||
|
|
||||||
Install the Cargo subcommand from a checkout, then invoke it from the
|
|
||||||
application's directory:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo install --path /path/to/iris/cargo-iris
|
|
||||||
cargo iris apk --abi arm64-v8a
|
|
||||||
cargo iris run --abi x86_64 --device emulator-5554
|
|
||||||
```
|
|
||||||
|
|
||||||
Package examples use the same command with `--example`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --example tabs
|
|
||||||
cargo iris apk --example tabs --abi arm64-v8a
|
|
||||||
cargo iris run --example tabs --abi x86_64 --device emulator-5554
|
|
||||||
```
|
|
||||||
|
|
||||||
`cargo iris` packages directly with `cargo-ndk`, `javac`, `d8`, `aapt2`,
|
|
||||||
`jar`, `zipalign`, and `apksigner`; it does not require Gradle. The caller
|
|
||||||
provides a JDK, Android SDK and NDK, and any emulator or physical device. Set
|
|
||||||
`ANDROID_HOME` to the SDK. `run` always requires an explicit device and never
|
|
||||||
creates or starts one.
|
|
||||||
|
|
||||||
Debug APKs use the standard key at `~/.android/debug.keystore`, creating it
|
|
||||||
with `keytool` when absent. A release build requires the long-lived signing
|
|
||||||
identity explicitly:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
IRIS_KEYSTORE_PASSWORD=... IRIS_KEY_PASSWORD=... \
|
|
||||||
cargo iris apk --release --keystore /secure/upload.jks --key-alias upload
|
|
||||||
```
|
|
||||||
|
|
||||||
The verified APK lives under
|
|
||||||
`target/iris-android/<package>[-<example>]/<debug|release>/<abi>/`; packaging
|
|
||||||
intermediates are removed before the command prints its absolute path.
|
|
||||||
|
|
||||||
Goals, in general order:
|
Goals, in general order:
|
||||||
1. does what I want it to (text, images, video, animations)
|
1. does what I want it to (text, images, video, animations)
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ name = "rig-input"
|
|||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
# Replays harness `.touch` files through Wayland's virtual-pointer protocol;
|
# Replays `.touch` recordings through Wayland's virtual-pointer protocol;
|
||||||
# headless sway has no input devices for coordinate-driving tools to move.
|
# headless sway has no input devices for coordinate-driving tools to move.
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "replay-touch"
|
name = "replay-touch"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Share the harness parser so both layers interpret recordings identically.
|
# Share the harness parser so both ways of replaying read a file the same.
|
||||||
iris = { path = ".." }
|
iris = { path = ".." }
|
||||||
wayland-client = "0.31.15"
|
wayland-client = { workspace = true }
|
||||||
wayland-protocols-wlr = { version = "0.3.12", features = ["client"] }
|
wayland-protocols-wlr = { workspace = true }
|
||||||
@@ -116,7 +116,7 @@ fn main() {
|
|||||||
// printing winit's events.
|
// printing winit's events.
|
||||||
let state = match sample.action {
|
let state = match sample.action {
|
||||||
TouchAction::Down => Some(ButtonState::Pressed),
|
TouchAction::Down => Some(ButtonState::Pressed),
|
||||||
TouchAction::Up | TouchAction::Cancel => Some(ButtonState::Released),
|
TouchAction::Up => Some(ButtonState::Released),
|
||||||
TouchAction::Move => None,
|
TouchAction::Move => None,
|
||||||
};
|
};
|
||||||
if let Some(state) = state {
|
if let Some(state) = state {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "nightly"
|
channel = "nightly"
|
||||||
components = ["clippy", "rustfmt"]
|
components = ["clippy", "rustfmt"]
|
||||||
targets = ["aarch64-linux-android", "x86_64-linux-android"]
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
# The compositor `scripts/run-headless.sh` starts, because this machine has no
|
# The compositor `scripts/run-headless.sh` starts, so that an example has a
|
||||||
# display. Nothing here is meant to be looked at directly; `grim` is.
|
# surface where there is no display. Nothing here is meant to be looked at
|
||||||
|
# directly; `grim` is.
|
||||||
#
|
#
|
||||||
# No Xwayland: winit talks Wayland natively, and starting an X server is a
|
# No Xwayland: winit talks Wayland natively, so an X server is a second thing
|
||||||
# second thing to go wrong for no gain. (`emu`'s config forces it because the
|
# to go wrong for no gain.
|
||||||
# Android emulator's renderer speaks GLX.)
|
|
||||||
xwayland disable
|
xwayland disable
|
||||||
|
|
||||||
# A desktop-shaped output, since this is the desktop half of the port. Larger
|
# The default output, overridden per run by `--mode`. Larger than the window
|
||||||
# than the window an example opens, so nothing is scaled or clipped.
|
# an example opens, so nothing is scaled or clipped.
|
||||||
output HEADLESS-1 mode 1920x1200@60Hz
|
output HEADLESS-1 mode 1920x1200@60Hz
|
||||||
|
|
||||||
default_border none
|
default_border none
|
||||||
|
|||||||
Loaded 100 of 199 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user