Move Iris to its standalone repository
This commit is contained in:
1 parent
715c86e7e2
commit
50d214e5b1
160 files changed
+21
-36285
No files matched your search
@@ -1,3 +1,7 @@
|
||||
[submodule "wg-app-link"]
|
||||
path = wg-app-link
|
||||
url = git@git.arirex.me:iris/wg-app-link.git
|
||||
[submodule "iris"]
|
||||
path = iris
|
||||
url = git@git.arirex.me:iris-ai/iris.git
|
||||
branch = main
|
||||
@@ -1,7 +1,8 @@
|
||||
# ai-app
|
||||
|
||||
A phone and desktop interface to AI coding sessions. The backend is Rust/Axum;
|
||||
the shared client and UI are Rust, drawn by the in-tree `iris` framework. The
|
||||
the shared client and UI are Rust, drawn by the `iris` framework pinned as a
|
||||
submodule. The
|
||||
Android app uses a thin Java activity and `android-view`; desktop uses winit.
|
||||
|
||||
`docs/PLAN.md` is the design source of truth. Read it before structural work
|
||||
@@ -32,12 +33,14 @@ dependency runs one way.
|
||||
contains Iris widget trees; `android` and `desktop` are thin hosts.
|
||||
`android-project/` packages the Rust cdylib. The `bench` feature and
|
||||
`bench-fixture/` are retained performance rigs, not a second app.
|
||||
- `iris/` — the framework, proc macro, tabs demo, and input rig.
|
||||
- `iris/` — the pinned framework submodule: proc macro, demos, and input rig.
|
||||
- `scripts/` — repository-wide scripts and independent profiling rigs.
|
||||
- `wg-app-link/` — a git submodule shared with dev-updater. Clone with
|
||||
`--recurse-submodules` or run `git submodule update --init`.
|
||||
- `wg-app-link/` — a git submodule shared with dev-updater.
|
||||
- `docs/` — design and working documents.
|
||||
|
||||
Clone with `--recurse-submodules` or run `git submodule update --init` to
|
||||
populate both `iris/` and `wg-app-link/`.
|
||||
|
||||
Nerd Font icons are an app-owned committed subset. `app/build-icon-font.sh`
|
||||
produces `app/assets/fonts/nerd_icons.ttf`; its codepoints must match
|
||||
`app/src/ui/icon.rs`. The app registers it with Iris at startup. Body and
|
||||
|
||||
@@ -39,6 +39,13 @@ backend (Rust/Axum, desktop)
|
||||
└─ config.ron + per-session transcript files
|
||||
```
|
||||
|
||||
**Iris is developed in its own repository and pinned here as a submodule**
|
||||
(2026-09-12). The framework accumulated enough generally useful Android,
|
||||
desktop, layout, text, input, accessibility, rendering, and packaging work to
|
||||
stand independently of this product. Keeping it under `iris/` preserves the
|
||||
app's local path dependency and test commands, while the repository boundary
|
||||
keeps product code and framework code from sharing commits.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Setups and providers (2026-08-28)
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
# Rust app and Iris
|
||||
|
||||
The production app is one Rust crate under `app/`, with shared Android and
|
||||
desktop UI drawn by the in-tree `iris` framework. This document keeps the
|
||||
desktop UI drawn by the `iris` framework pinned under `iris/` as a submodule.
|
||||
This document keeps the
|
||||
open work and measured constraints that are expensive to rediscover.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Submodule
+1
Submodule iris added at 32f6ad8c79.
@@ -1,2 +0,0 @@
|
||||
/target
|
||||
perf.data*
|
||||
Generated
-4848
File diff suppressed because it is too large.
Load diff
-105
@@ -1,105 +0,0 @@
|
||||
[package]
|
||||
name = "iris"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
iris-core = { workspace = true }
|
||||
iris-macro = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
swash = { workspace = true }
|
||||
pollster = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
image = { workspace = true }
|
||||
accesskit = { workspace = true }
|
||||
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]
|
||||
bytemuck = { workspace = true }
|
||||
|
||||
[[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]
|
||||
members = [
|
||||
"cargo-iris",
|
||||
"core",
|
||||
"macro",
|
||||
"rig-input",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
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]
|
||||
pollster = "1.0.1"
|
||||
winit = "0.30.13"
|
||||
wgpu = "30.0.1"
|
||||
bytemuck = "1.25.2"
|
||||
image = "0.25.10"
|
||||
parley = "0.11.1"
|
||||
swash = "0.2.10"
|
||||
fxhash = "0.2.1"
|
||||
arboard = "3.6.1"
|
||||
accesskit = "0.25.0"
|
||||
iris-core = { path = "core" }
|
||||
iris-macro = { path = "macro" }
|
||||
tokio = "1.53.1"
|
||||
@@ -1,75 +0,0 @@
|
||||
# iris: known problems and things still to build
|
||||
|
||||
Only open Iris framework work lives here. Delete an item when it lands.
|
||||
|
||||
## Build (for the port)
|
||||
|
||||
Framework capabilities needed by `RUST.md`'s port plan:
|
||||
|
||||
- [ ] **Overflow ellipsis with an explicit retained end.** `TextAttrs` can
|
||||
only wrap or clip, so a tool summary is cut with no mark. Parley has no
|
||||
ellipsis primitive; use its line breaker to find the cut, but keep source
|
||||
and displayed strings distinct with one byte mapping shared by spans,
|
||||
links, selection and editing. Replace `wrap: bool` with an enum that can
|
||||
say wrap, clip, head ellipsis and tail ellipsis—the caller must choose
|
||||
because a command is identified by its head and a path by its tail.
|
||||
- [ ] **Expose the distance from a `LazySpan` viewport to its unloaded
|
||||
edge.** (**P1**.) `viewport_len` and the visible extents are already
|
||||
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);
|
||||
}
|
||||
}
|
||||
-153
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "iris-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
wgpu = { workspace = true }
|
||||
# Keeps renderer creation synchronous while retrieving wgpu's async error scope.
|
||||
pollster = { workspace = true }
|
||||
bytemuck ={ workspace = true }
|
||||
image = { workspace = true }
|
||||
parley = { workspace = true }
|
||||
swash = { workspace = true }
|
||||
fxhash = { workspace = true }
|
||||
accesskit = { workspace = true }
|
||||
@@ -1,23 +0,0 @@
|
||||
use crate::{UiRsc, WeakWidget, WidgetIdFn, WidgetLike};
|
||||
|
||||
pub trait WidgetAttr<Rsc, W: ?Sized> {
|
||||
type Input;
|
||||
fn run(rsc: &mut Rsc, id: WeakWidget<W>, input: Self::Input);
|
||||
}
|
||||
|
||||
pub trait Attrable<Rsc, W: ?Sized, Tag> {
|
||||
fn attr<A: WidgetAttr<Rsc, W>>(self, input: A::Input) -> impl WidgetIdFn<Rsc, W>;
|
||||
}
|
||||
|
||||
impl<Rsc: UiRsc, WL: WidgetLike<Rsc, Tag>, Tag> Attrable<Rsc, WL::Widget, Tag> for WL {
|
||||
fn attr<A: WidgetAttr<Rsc, WL::Widget>>(
|
||||
self,
|
||||
input: A::Input,
|
||||
) -> impl WidgetIdFn<Rsc, WL::Widget> {
|
||||
|rsc| {
|
||||
let id = self.add(rsc);
|
||||
A::run(rsc, id, input);
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,18 +0,0 @@
|
||||
use crate::{HasEvents, WeakWidget, Widget};
|
||||
|
||||
pub struct EventCtx<'a, Rsc: HasEvents, Data> {
|
||||
pub state: &'a mut Rsc::State,
|
||||
pub data: Data,
|
||||
}
|
||||
|
||||
pub struct EventIdCtx<'a, Rsc: HasEvents, Data, W: ?Sized> {
|
||||
pub widget: WeakWidget<W>,
|
||||
pub state: &'a mut Rsc::State,
|
||||
pub data: Data,
|
||||
}
|
||||
|
||||
impl<Rsc: HasEvents, Data, W: Widget> EventIdCtx<'_, Rsc, Data, W> {
|
||||
pub fn widget<'a>(&self, rsc: &'a mut Rsc) -> &'a mut W {
|
||||
&mut rsc.ui_mut().widgets[self.widget]
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
use crate::{
|
||||
ActiveData, ControllerManager, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents,
|
||||
IdLike, LayerId, WeakWidget, WidgetEventFn, WidgetId,
|
||||
util::{HashMap, HashSet, TypeMap},
|
||||
};
|
||||
use std::{any::TypeId, rc::Rc};
|
||||
|
||||
pub struct EventManager<Rsc> {
|
||||
widget_to_types: HashMap<WidgetId, HashSet<TypeId>>,
|
||||
types: TypeMap<dyn EventManagerLike<Rsc>>,
|
||||
pub controllers: ControllerManager<Rsc>,
|
||||
}
|
||||
|
||||
impl<Rsc: 'static> Default for EventManager<Rsc> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
widget_to_types: Default::default(),
|
||||
types: Default::default(),
|
||||
controllers: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rsc: HasEvents + 'static> EventManager<Rsc> {
|
||||
pub fn get_type<E: EventLike>(&mut self) -> &mut TypeEventManager<Rsc, E::Event> {
|
||||
self.types.type_or_default()
|
||||
}
|
||||
|
||||
pub fn register<I: IdLike + 'static, E: EventLike>(
|
||||
&mut self,
|
||||
id: I,
|
||||
event: E,
|
||||
f: impl for<'a> WidgetEventFn<Rsc, <E::Event as Event>::Data<'a>, I::Widget>,
|
||||
) {
|
||||
let i = id.id();
|
||||
self.get_type::<E>().register(id, event, f);
|
||||
self.widget_to_types
|
||||
.entry(i)
|
||||
.or_default()
|
||||
.insert(Self::type_key::<E>());
|
||||
}
|
||||
|
||||
pub fn type_key<E: EventLike>() -> TypeId {
|
||||
TypeId::of::<TypeEventManager<Rsc, E::Event>>()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EventsLike {
|
||||
fn remove(&mut self, id: WidgetId);
|
||||
fn draw(&mut self, active: &ActiveData);
|
||||
fn undraw(&mut self, active: &ActiveData);
|
||||
}
|
||||
|
||||
impl<Rsc: HasEvents + 'static> EventsLike for EventManager<Rsc> {
|
||||
fn remove(&mut self, id: WidgetId) {
|
||||
for t in self.widget_to_types.get(&id).into_flat_iter() {
|
||||
self.types.get_mut(t).unwrap().remove(id);
|
||||
}
|
||||
self.controllers.remove(id);
|
||||
}
|
||||
|
||||
fn draw(&mut self, active: &ActiveData) {
|
||||
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
|
||||
self.types.get_mut(t).unwrap().draw(active);
|
||||
}
|
||||
}
|
||||
|
||||
fn undraw(&mut self, active: &ActiveData) {
|
||||
for t in self.widget_to_types.get(&active.id).into_flat_iter() {
|
||||
self.types.get_mut(t).unwrap().undraw(active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EventManagerLike<State> {
|
||||
fn remove(&mut self, id: WidgetId);
|
||||
fn draw(&mut self, data: &ActiveData);
|
||||
fn undraw(&mut self, data: &ActiveData);
|
||||
}
|
||||
|
||||
type EventData<Rsc, E> = (E, Rc<dyn for<'a> EventFn<Rsc, <E as Event>::Data<'a>>>);
|
||||
pub struct TypeEventManager<Rsc: HasEvents, E: Event> {
|
||||
// TODO: reduce visiblity!!
|
||||
pub active: HashMap<LayerId, HashMap<WidgetId, E::State>>,
|
||||
pub global: E::Global,
|
||||
map: HashMap<WidgetId, Vec<EventData<Rsc, E>>>,
|
||||
}
|
||||
|
||||
impl<Rsc: HasEvents, E: Event> EventManagerLike<Rsc> for TypeEventManager<Rsc, E> {
|
||||
fn remove(&mut self, id: WidgetId) {
|
||||
self.map.remove(&id);
|
||||
for layer in self.active.values_mut() {
|
||||
layer.remove(&id);
|
||||
}
|
||||
}
|
||||
fn draw(&mut self, data: &ActiveData) {
|
||||
self.active
|
||||
.entry(data.layer)
|
||||
.or_default()
|
||||
.entry(data.id)
|
||||
.or_default();
|
||||
}
|
||||
fn undraw(&mut self, data: &ActiveData) {
|
||||
if let Some(layer) = self.active.get_mut(&data.layer) {
|
||||
layer.remove(&data.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active: Default::default(),
|
||||
global: Default::default(),
|
||||
map: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
|
||||
fn register<I: IdLike + 'static>(
|
||||
&mut self,
|
||||
widget: I,
|
||||
event: impl EventLike<Event = E>,
|
||||
f: impl for<'a> WidgetEventFn<Rsc, E::Data<'a>, I::Widget>,
|
||||
) {
|
||||
let event = event.into_event();
|
||||
self.map.entry(widget.id()).or_default().push((
|
||||
event,
|
||||
Rc::new(move |ctx, rsc| {
|
||||
f(
|
||||
EventIdCtx {
|
||||
widget: WeakWidget::new(widget.id()),
|
||||
state: ctx.state,
|
||||
data: ctx.data,
|
||||
},
|
||||
rsc,
|
||||
);
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
/// 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>(
|
||||
&mut self,
|
||||
id: impl IdLike,
|
||||
) -> impl for<'b> FnOnce(EventCtx<'_, Rsc, E::Data<'b>>, &mut Rsc) + 'a {
|
||||
let fs = self.map.get(&id.id()).cloned().unwrap_or_default();
|
||||
move |ctx, rsc| {
|
||||
for (e, f) in fs {
|
||||
if let Some(data) = e.should_run(&ctx.data) {
|
||||
f(
|
||||
EventCtx {
|
||||
state: ctx.state,
|
||||
data,
|
||||
},
|
||||
rsc,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
mod controller;
|
||||
mod ctx;
|
||||
mod manager;
|
||||
mod rsc;
|
||||
|
||||
pub use controller::*;
|
||||
pub use ctx::*;
|
||||
pub use manager::*;
|
||||
pub use rsc::*;
|
||||
|
||||
pub trait Event: Sized + 'static + Clone {
|
||||
type Data<'a>: Clone = ();
|
||||
type State: Default = ();
|
||||
type Global: Default = ();
|
||||
#[allow(unused_variables)]
|
||||
fn should_run<'a>(&self, data: &Self::Data<'a>) -> Option<Self::Data<'a>> {
|
||||
Some(data.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EventLike {
|
||||
type Event: Event;
|
||||
fn into_event(self) -> Self::Event;
|
||||
}
|
||||
|
||||
impl<E: Event> EventLike for E {
|
||||
type Event = Self;
|
||||
|
||||
fn into_event(self) -> Self::Event {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EventFn<Rsc: HasEvents, Data>: Fn(EventCtx<Rsc, Data>, &mut Rsc) + 'static {}
|
||||
impl<Rsc: HasEvents, F: Fn(EventCtx<Rsc, Data>, &mut Rsc) + 'static, Data> EventFn<Rsc, Data>
|
||||
for F
|
||||
{
|
||||
}
|
||||
|
||||
pub trait WidgetEventFn<Rsc: HasEvents, Data, W: ?Sized>:
|
||||
Fn(EventIdCtx<Rsc, Data, W>, &mut Rsc) + 'static
|
||||
{
|
||||
}
|
||||
impl<Rsc: HasEvents, F: Fn(EventIdCtx<Rsc, Data, W>, &mut Rsc) + 'static, Data, W: ?Sized>
|
||||
WidgetEventFn<Rsc, Data, W> for F
|
||||
{
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
use crate::{
|
||||
Command, CommandResult, Controller, ControllerId, Event, EventCtx, EventLike, EventManager,
|
||||
IdLike, UiRsc, WeakWidget, Widget, WidgetEventFn,
|
||||
};
|
||||
|
||||
pub trait HasState: 'static {
|
||||
type State;
|
||||
}
|
||||
|
||||
pub trait HasEvents: Sized + UiRsc + HasState {
|
||||
fn events(&self) -> &EventManager<Self>;
|
||||
fn events_mut(&mut self) -> &mut EventManager<Self>;
|
||||
|
||||
fn register_event<W: Widget + ?Sized, E: EventLike>(
|
||||
&mut self,
|
||||
id: WeakWidget<W>,
|
||||
event: E,
|
||||
f: impl for<'a> WidgetEventFn<Self, <E::Event as Event>::Data<'a>, W>,
|
||||
) {
|
||||
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 {
|
||||
fn run_event<E: EventLike>(
|
||||
&mut self,
|
||||
id: impl IdLike,
|
||||
data: <E::Event as Event>::Data<'_>,
|
||||
state: &mut Self::State,
|
||||
) {
|
||||
// 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);
|
||||
f(EventCtx { state, data }, self)
|
||||
}
|
||||
}
|
||||
impl<T: HasEvents> RunEvents for T {}
|
||||
@@ -1,31 +0,0 @@
|
||||
#![feature(macro_metavar_expr_concat)]
|
||||
#![feature(const_ops)]
|
||||
#![feature(const_trait_impl)]
|
||||
#![feature(const_convert)]
|
||||
#![feature(unboxed_closures)]
|
||||
#![feature(fn_traits)]
|
||||
#![feature(const_destruct)]
|
||||
#![feature(associated_type_defaults)]
|
||||
#![feature(unsize)]
|
||||
#![feature(coerce_unsized)]
|
||||
#![feature(option_into_flat_iter)]
|
||||
|
||||
mod attr;
|
||||
mod event;
|
||||
mod num;
|
||||
mod orientation;
|
||||
mod primitive;
|
||||
mod render;
|
||||
mod ui;
|
||||
mod widget;
|
||||
|
||||
pub mod util;
|
||||
|
||||
pub use attr::*;
|
||||
pub use event::*;
|
||||
pub use num::*;
|
||||
pub use orientation::*;
|
||||
pub use primitive::*;
|
||||
pub use render::*;
|
||||
pub use ui::*;
|
||||
pub use widget::*;
|
||||
@@ -1,49 +0,0 @@
|
||||
use crate::util::Vec2;
|
||||
use std::marker::Destruct;
|
||||
|
||||
pub const trait UiNum {
|
||||
fn to_f32(self) -> f32;
|
||||
}
|
||||
|
||||
const impl UiNum for f32 {
|
||||
fn to_f32(self) -> f32 {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
const impl UiNum for u32 {
|
||||
fn to_f32(self) -> f32 {
|
||||
self as f32
|
||||
}
|
||||
}
|
||||
|
||||
const impl UiNum for i32 {
|
||||
fn to_f32(self) -> f32 {
|
||||
self as f32
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn vec2(x: impl const UiNum, y: impl const UiNum) -> Vec2 {
|
||||
Vec2::new(x.to_f32(), y.to_f32())
|
||||
}
|
||||
|
||||
const impl<T: const UiNum + Copy> From<T> for Vec2 {
|
||||
fn from(v: T) -> Self {
|
||||
Self {
|
||||
x: v.to_f32(),
|
||||
y: v.to_f32(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const impl<T: const UiNum, U: const UiNum> From<(T, U)> for Vec2
|
||||
where
|
||||
(T, U): const Destruct,
|
||||
{
|
||||
fn from((x, y): (T, U)) -> Self {
|
||||
Self {
|
||||
x: x.to_f32(),
|
||||
y: y.to_f32(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
use crate::vec2;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Align {
|
||||
pub x: Option<AxisAlign>,
|
||||
pub y: Option<AxisAlign>,
|
||||
}
|
||||
|
||||
impl Align {
|
||||
pub const TOP_LEFT: RegionAlign = RegionAlign::TOP_LEFT;
|
||||
pub const TOP_CENTER: RegionAlign = RegionAlign::TOP_CENTER;
|
||||
pub const TOP_RIGHT: RegionAlign = RegionAlign::TOP_RIGHT;
|
||||
pub const CENTER_LEFT: RegionAlign = RegionAlign::CENTER_LEFT;
|
||||
pub const CENTER: RegionAlign = RegionAlign::CENTER;
|
||||
pub const CENTER_RIGHT: RegionAlign = RegionAlign::CENTER_RIGHT;
|
||||
pub const BOT_LEFT: RegionAlign = RegionAlign::BOT_LEFT;
|
||||
pub const BOT_CENTER: RegionAlign = RegionAlign::BOT_CENTER;
|
||||
pub const BOT_RIGHT: RegionAlign = RegionAlign::BOT_RIGHT;
|
||||
pub const LEFT: CardinalAlign = CardinalAlign::LEFT;
|
||||
pub const H_CENTER: CardinalAlign = CardinalAlign::H_CENTER;
|
||||
pub const RIGHT: CardinalAlign = CardinalAlign::RIGHT;
|
||||
pub const TOP: CardinalAlign = CardinalAlign::TOP;
|
||||
pub const V_CENTER: CardinalAlign = CardinalAlign::V_CENTER;
|
||||
pub const BOT: CardinalAlign = CardinalAlign::BOT;
|
||||
|
||||
pub fn tuple(&self) -> (Option<AxisAlign>, Option<AxisAlign>) {
|
||||
(self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AxisAlign {
|
||||
Neg,
|
||||
Center,
|
||||
Pos,
|
||||
}
|
||||
|
||||
impl AxisAlign {
|
||||
pub const fn rel(&self) -> f32 {
|
||||
match self {
|
||||
Self::Neg => 0.0,
|
||||
Self::Center => 0.5,
|
||||
Self::Pos => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CardinalAlign {
|
||||
axis: Axis,
|
||||
align: AxisAlign,
|
||||
}
|
||||
|
||||
impl CardinalAlign {
|
||||
pub const LEFT: Self = Self::new(Axis::X, AxisAlign::Neg);
|
||||
pub const H_CENTER: Self = Self::new(Axis::X, AxisAlign::Center);
|
||||
pub const RIGHT: Self = Self::new(Axis::X, AxisAlign::Pos);
|
||||
pub const TOP: Self = Self::new(Axis::Y, AxisAlign::Neg);
|
||||
pub const V_CENTER: Self = Self::new(Axis::Y, AxisAlign::Center);
|
||||
pub const BOT: Self = Self::new(Axis::Y, AxisAlign::Pos);
|
||||
|
||||
pub const fn new(axis: Axis, align: AxisAlign) -> Self {
|
||||
Self { axis, align }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RegionAlign {
|
||||
pub x: AxisAlign,
|
||||
pub y: AxisAlign,
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
pub fn partial_align(&self, align: Align) -> UiRegion {
|
||||
UiRegion {
|
||||
x: if let Some(align) = align.x {
|
||||
self.x.align(align)
|
||||
} else {
|
||||
UiSpan::FULL
|
||||
},
|
||||
y: if let Some(align) = align.y {
|
||||
self.y.align(align)
|
||||
} else {
|
||||
UiSpan::FULL
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn align(&self, align: RegionAlign) -> UiRegion {
|
||||
UiRegion {
|
||||
x: self.x.align(align.x),
|
||||
y: self.y.align(align.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
pub fn partial_align(&self, align: Align) -> UiRegion {
|
||||
let s = UiVec2::from(*self);
|
||||
UiRegion {
|
||||
x: if let Some(align) = align.x {
|
||||
s.x.align(align)
|
||||
} else {
|
||||
UiSpan::FULL
|
||||
},
|
||||
y: if let Some(align) = align.y {
|
||||
s.y.align(align)
|
||||
} else {
|
||||
UiSpan::FULL
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn align(&self, align: RegionAlign) -> UiRegion {
|
||||
let s = UiVec2::from(*self);
|
||||
UiRegion {
|
||||
x: s.x.align(align.x),
|
||||
y: s.y.align(align.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UiScalar {
|
||||
pub const fn align(&self, align: AxisAlign) -> UiSpan {
|
||||
let rel = align.rel();
|
||||
let mut start = UiScalar::rel(rel);
|
||||
start.abs -= self.abs * rel;
|
||||
start.rel -= self.rel * rel;
|
||||
let mut end = UiScalar::rel(rel);
|
||||
end.abs += self.abs * (1.0 - rel);
|
||||
end.rel += self.rel * (1.0 - rel);
|
||||
UiSpan { start, end }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RegionAlign> for Align {
|
||||
fn from(region: RegionAlign) -> Self {
|
||||
Self {
|
||||
x: Some(region.x),
|
||||
y: Some(region.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Align> for RegionAlign {
|
||||
fn from(align: Align) -> Self {
|
||||
Self {
|
||||
x: align.x.unwrap_or(AxisAlign::Center),
|
||||
y: align.y.unwrap_or(AxisAlign::Center),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CardinalAlign> for RegionAlign {
|
||||
fn from(align: CardinalAlign) -> Self {
|
||||
Align::from(align).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CardinalAlign> for Align {
|
||||
fn from(cardinal: CardinalAlign) -> Self {
|
||||
let align = Some(cardinal.align);
|
||||
match cardinal.axis {
|
||||
Axis::X => Self { x: align, y: None },
|
||||
Axis::Y => Self { x: None, y: align },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const impl From<RegionAlign> for UiVec2 {
|
||||
fn from(align: RegionAlign) -> Self {
|
||||
Self::rel(align.rel())
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
pub enum Axis {
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
impl std::ops::Not for Axis {
|
||||
type Output = Self;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
match self {
|
||||
Self::X => Self::Y,
|
||||
Self::Y => Self::X,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub struct Dir {
|
||||
pub axis: Axis,
|
||||
pub sign: Sign,
|
||||
}
|
||||
|
||||
impl Dir {
|
||||
pub const fn new(axis: Axis, dir: Sign) -> Self {
|
||||
Self { axis, sign: dir }
|
||||
}
|
||||
|
||||
pub const LEFT: Self = Self::new(Axis::X, Sign::Neg);
|
||||
pub const RIGHT: Self = Self::new(Axis::X, Sign::Pos);
|
||||
pub const UP: Self = Self::new(Axis::Y, Sign::Neg);
|
||||
pub const DOWN: Self = Self::new(Axis::Y, Sign::Pos);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub enum Sign {
|
||||
Neg,
|
||||
Pos,
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
pub fn axis(&self, axis: Axis) -> f32 {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut f32 {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_axis(axis: Axis, aligned: f32, ortho: f32) -> Self {
|
||||
Self {
|
||||
x: match axis {
|
||||
Axis::X => aligned,
|
||||
Axis::Y => ortho,
|
||||
},
|
||||
y: match axis {
|
||||
Axis::Y => aligned,
|
||||
Axis::X => ortho,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const trait AxisT {
|
||||
fn get() -> Axis;
|
||||
}
|
||||
|
||||
pub struct XAxis;
|
||||
const impl AxisT for XAxis {
|
||||
fn get() -> Axis {
|
||||
Axis::X
|
||||
}
|
||||
}
|
||||
|
||||
pub struct YAxis;
|
||||
const impl AxisT for YAxis {
|
||||
fn get() -> Axis {
|
||||
Axis::Y
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct BothAxis<T> {
|
||||
pub x: T,
|
||||
pub y: T,
|
||||
}
|
||||
|
||||
impl<T> BothAxis<T> {
|
||||
pub const fn axis<A: const AxisT>(&mut self) -> &mut T {
|
||||
match A::get() {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
pub fn take_axis<A: const AxisT>(self) -> T {
|
||||
match A::get() {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
pub fn axis_dyn(&mut self, axis: Axis) -> &mut T {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,404 +0,0 @@
|
||||
use super::*;
|
||||
use crate::{UiNum, util::impl_op};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub struct Size {
|
||||
pub x: LayoutLen,
|
||||
pub y: LayoutLen,
|
||||
}
|
||||
|
||||
/// A length resolved from physical pixels, density-independent pixels, and a
|
||||
/// fraction of a reference length. Unlike [`LayoutLen`], it carries no claim
|
||||
/// on space left over by a layout container.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Len {
|
||||
/// Physical pixels -- a raw device pixel, unaffected by the display's
|
||||
/// density. Rare to want directly (a hairline border is the usual
|
||||
/// case); most sizes should be `dp` instead. See `dp`'s own doc for why
|
||||
/// the two are kept separate rather than one field a caller has to
|
||||
/// 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 abs: f32,
|
||||
pub dp: f32,
|
||||
pub rel: f32,
|
||||
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 {
|
||||
fn from(value: N) -> Self {
|
||||
Self::abs(value.to_f32())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Len> for LayoutLen {
|
||||
fn from(value: Len) -> 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 {
|
||||
x: x.into(),
|
||||
y: y.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LayoutLen> for Size {
|
||||
fn from(value: LayoutLen) -> Self {
|
||||
Self { x: value, y: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Len> for Size {
|
||||
fn from(value: Len) -> Self {
|
||||
Self::from(LayoutLen::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl Size {
|
||||
pub const ZERO: Self = Self {
|
||||
x: LayoutLen::ZERO,
|
||||
y: LayoutLen::ZERO,
|
||||
};
|
||||
|
||||
pub const REST: Self = Self {
|
||||
x: LayoutLen::REST,
|
||||
y: LayoutLen::REST,
|
||||
};
|
||||
|
||||
pub fn abs(v: Vec2) -> Self {
|
||||
Self {
|
||||
x: LayoutLen::abs(v.x),
|
||||
y: LayoutLen::abs(v.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rel(v: Vec2) -> Self {
|
||||
Self {
|
||||
x: LayoutLen::rel(v.x),
|
||||
y: LayoutLen::rel(v.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rest(v: Vec2) -> Self {
|
||||
Self {
|
||||
x: LayoutLen::rest(v.x),
|
||||
y: LayoutLen::rest(v.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_uivec2(self, density: f32) -> UiVec2 {
|
||||
UiVec2 {
|
||||
x: self.x.apply_rest(density),
|
||||
y: self.y.apply_rest(density),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_axis(axis: Axis, aligned: LayoutLen, ortho: LayoutLen) -> Self {
|
||||
match axis {
|
||||
Axis::X => Self {
|
||||
x: aligned,
|
||||
y: ortho,
|
||||
},
|
||||
Axis::Y => Self {
|
||||
x: ortho,
|
||||
y: aligned,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis(&self, axis: Axis) -> LayoutLen {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutLen {
|
||||
pub const ZERO: Self = Self {
|
||||
abs: 0.0,
|
||||
dp: 0.0,
|
||||
rel: 0.0,
|
||||
rest: 0.0,
|
||||
};
|
||||
|
||||
pub const REST: Self = Self {
|
||||
abs: 0.0,
|
||||
dp: 0.0,
|
||||
rel: 0.0,
|
||||
rest: 1.0,
|
||||
};
|
||||
|
||||
/// Resolves to a `UiScalar`, folding `dp` into `abs` pixels against
|
||||
/// `density` (physical pixels per dp -- 1.0 on a desktop or an
|
||||
/// unscaled display, `content_scale` on Android; see `dp`'s field
|
||||
/// doc). Every other component of `LayoutLen` is already resolution-
|
||||
/// independent (`rel` is a fraction of the parent; `rest` becomes a
|
||||
/// fraction too, below), so `density` only ever touches this one term.
|
||||
pub fn apply_rest(&self, density: f32) -> UiScalar {
|
||||
UiScalar {
|
||||
rel: self.rel + if self.rest > 0.0 { 1.0 } else { 0.0 },
|
||||
abs: self.abs + self.dp * density,
|
||||
}
|
||||
}
|
||||
|
||||
/// The same fold as [`Self::apply_rest`] but staying a `LayoutLen`, so
|
||||
/// `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 {
|
||||
abs: self.abs + self.dp * density,
|
||||
dp: 0.0,
|
||||
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 {
|
||||
Self {
|
||||
abs: 0.0,
|
||||
dp: 0.0,
|
||||
rel: rel.to_f32(),
|
||||
rest: 0.0,
|
||||
}
|
||||
}
|
||||
pub fn rest(ratio: impl UiNum) -> Self {
|
||||
Self {
|
||||
abs: 0.0,
|
||||
dp: 0.0,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod len_fns {
|
||||
use super::*;
|
||||
|
||||
pub fn abs(abs: impl UiNum) -> Len {
|
||||
Len::abs(abs)
|
||||
}
|
||||
pub fn dp(dp: impl UiNum) -> Len {
|
||||
Len::dp(dp)
|
||||
}
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_op!(LayoutLen Add add; abs dp rel rest);
|
||||
impl_op!(LayoutLen Sub sub; abs dp rel rest);
|
||||
impl_op!(Len Add add; abs dp rel);
|
||||
impl_op!(Len Sub sub; abs dp rel);
|
||||
|
||||
impl std::ops::Add<Len> for LayoutLen {
|
||||
type Output = Self;
|
||||
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
Self::rest(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Len {
|
||||
fn default() -> Self {
|
||||
Self::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Size {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "({}, {})", self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LayoutLen {
|
||||
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)?;
|
||||
}
|
||||
if self.rest != 0.0 {
|
||||
write!(f, "{} rest;", self.rest)?;
|
||||
}
|
||||
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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
mod align;
|
||||
mod axis;
|
||||
mod len;
|
||||
mod pos;
|
||||
|
||||
use crate::util::Vec2;
|
||||
|
||||
pub use align::*;
|
||||
pub use axis::*;
|
||||
pub use len::*;
|
||||
pub use pos::*;
|
||||
@@ -1,464 +0,0 @@
|
||||
use std::{fmt::Display, hash::Hash, marker::Destruct};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
UiNum,
|
||||
util::{LerpUtil, impl_op},
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||
pub struct UiVec2 {
|
||||
pub x: UiScalar,
|
||||
pub y: UiScalar,
|
||||
}
|
||||
|
||||
impl UiVec2 {
|
||||
pub const ZERO: Self = Self {
|
||||
x: UiScalar::ZERO,
|
||||
y: UiScalar::ZERO,
|
||||
};
|
||||
|
||||
pub const fn new(x: UiScalar, y: UiScalar) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub const fn abs(abs: impl const Into<Vec2>) -> Self {
|
||||
let abs = abs.into();
|
||||
Self {
|
||||
x: UiScalar::abs(abs.x),
|
||||
y: UiScalar::abs(abs.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn rel(rel: impl const Into<Vec2>) -> Self {
|
||||
let rel = rel.into();
|
||||
Self {
|
||||
x: UiScalar::rel(rel.x),
|
||||
y: UiScalar::rel(rel.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn shift(&mut self, offset: impl const Into<UiVec2>) {
|
||||
let offset = offset.into();
|
||||
*self += offset;
|
||||
}
|
||||
|
||||
pub const fn offset(mut self, offset: impl const Into<UiVec2>) -> Self {
|
||||
self.shift(offset);
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn within(&self, region: &UiRegion) -> UiVec2 {
|
||||
UiVec2 {
|
||||
x: self.x.within(®ion.x),
|
||||
y: self.y.within(®ion.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn outside(&self, region: &UiRegion) -> UiVec2 {
|
||||
UiVec2 {
|
||||
x: self.x.outside(®ion.x),
|
||||
y: self.y.outside(®ion.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis_mut(&mut self, axis: Axis) -> &mut UiScalar {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn axis(&self, axis: Axis) -> UiScalar {
|
||||
match axis {
|
||||
Axis::X => self.x,
|
||||
Axis::Y => self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_abs(&self, rel: Vec2) -> Vec2 {
|
||||
Vec2 {
|
||||
x: self.x.to_abs(rel.x),
|
||||
y: self.y.to_abs(rel.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub const FULL_SIZE: Self = Self::rel(Vec2::ONE);
|
||||
|
||||
pub const fn from_axis(axis: Axis, aligned: UiScalar, ortho: UiScalar) -> Self {
|
||||
match axis {
|
||||
Axis::X => Self {
|
||||
x: aligned,
|
||||
y: ortho,
|
||||
},
|
||||
Axis::Y => Self {
|
||||
x: ortho,
|
||||
y: aligned,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_abs(&self) -> Vec2 {
|
||||
(self.x.abs, self.y.abs).into()
|
||||
}
|
||||
|
||||
pub fn get_rel(&self) -> Vec2 {
|
||||
(self.x.rel, self.y.rel).into()
|
||||
}
|
||||
|
||||
pub fn abs_mut(&mut self) -> Vec2View<'_> {
|
||||
Vec2View {
|
||||
x: &mut self.x.abs,
|
||||
y: &mut self.y.abs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UiVec2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "rel{};abs{}", self.get_rel(), self.get_abs())
|
||||
}
|
||||
}
|
||||
|
||||
impl_op!(UiVec2 Add add; x y);
|
||||
impl_op!(UiVec2 Sub sub; x y);
|
||||
|
||||
const impl From<Vec2> for UiVec2 {
|
||||
fn from(abs: Vec2) -> Self {
|
||||
Self::abs(abs)
|
||||
}
|
||||
}
|
||||
|
||||
const impl<T: const UiNum, U: const UiNum> From<(T, U)> for UiVec2
|
||||
where
|
||||
(T, U): const Destruct,
|
||||
{
|
||||
fn from(abs: (T, U)) -> Self {
|
||||
Self::abs(abs)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, Default, bytemuck::Zeroable)]
|
||||
pub struct UiScalar {
|
||||
pub rel: f32,
|
||||
pub abs: f32,
|
||||
}
|
||||
|
||||
impl Eq for UiScalar {}
|
||||
impl Hash for UiScalar {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
state.write_u32(self.rel.to_bits());
|
||||
state.write_u32(self.abs.to_bits());
|
||||
}
|
||||
}
|
||||
|
||||
impl_op!(UiScalar Add add; rel abs);
|
||||
impl_op!(UiScalar Sub sub; rel abs);
|
||||
|
||||
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 {
|
||||
Self { rel, abs: 0.0 }
|
||||
}
|
||||
|
||||
pub const fn abs(abs: f32) -> Self {
|
||||
Self { rel: 0.0, abs }
|
||||
}
|
||||
|
||||
pub const fn rel_min() -> Self {
|
||||
Self::new(0.0, 0.0)
|
||||
}
|
||||
|
||||
pub const fn rel_max() -> Self {
|
||||
Self::new(1.0, 0.0)
|
||||
}
|
||||
|
||||
pub const fn max(&self, other: Self) -> Self {
|
||||
Self {
|
||||
rel: self.rel.max(other.rel),
|
||||
abs: self.abs.max(other.abs),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn min(&self, other: Self) -> Self {
|
||||
Self {
|
||||
rel: self.rel.min(other.rel),
|
||||
abs: self.abs.min(other.abs),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn offset(mut self, amt: f32) -> Self {
|
||||
self.abs += amt;
|
||||
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 {
|
||||
rel: anchor,
|
||||
abs: offset,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn outside(&self, span: &UiSpan) -> Self {
|
||||
let rel = self.rel.lerp_inv(span.start.rel, span.end.rel);
|
||||
let abs = self.abs - rel.lerp(span.start.abs, span.end.abs);
|
||||
Self { rel, abs }
|
||||
}
|
||||
|
||||
pub fn within_len(&self, len: UiScalar) -> Self {
|
||||
self.within(&UiSpan {
|
||||
start: UiScalar::ZERO,
|
||||
end: len,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_len(&self, len: UiScalar) -> Self {
|
||||
len.within_len(*self)
|
||||
}
|
||||
|
||||
pub const fn flip(&mut self) {
|
||||
self.rel = 1.0 - self.rel;
|
||||
self.abs = -self.abs;
|
||||
}
|
||||
|
||||
pub const fn to(&self, end: Self) -> UiSpan {
|
||||
UiSpan { start: *self, end }
|
||||
}
|
||||
|
||||
pub const fn to_abs(&self, rel: f32) -> f32 {
|
||||
self.rel * rel + self.abs
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct UiSpan {
|
||||
pub start: UiScalar,
|
||||
pub end: UiScalar,
|
||||
}
|
||||
|
||||
impl UiSpan {
|
||||
pub const FULL: Self = Self {
|
||||
start: UiScalar::ZERO,
|
||||
end: UiScalar::FULL,
|
||||
};
|
||||
|
||||
pub const fn rel(rel: f32) -> Self {
|
||||
Self {
|
||||
start: UiScalar::rel(rel),
|
||||
end: UiScalar::rel(rel),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn new(start: UiScalar, end: UiScalar) -> Self {
|
||||
Self { start, end }
|
||||
}
|
||||
|
||||
pub const fn flip(&mut self) {
|
||||
self.start.flip();
|
||||
self.end.flip();
|
||||
std::mem::swap(&mut self.start.rel, &mut self.end.rel);
|
||||
std::mem::swap(&mut self.start.abs, &mut self.end.abs);
|
||||
}
|
||||
|
||||
pub const fn shift(&mut self, offset: UiScalar) {
|
||||
self.start += offset;
|
||||
self.end += offset;
|
||||
}
|
||||
|
||||
pub const fn within(&self, parent: &Self) -> Self {
|
||||
Self {
|
||||
start: self.start.within(parent),
|
||||
end: self.end.within(parent),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn outside(&self, parent: &Self) -> Self {
|
||||
Self {
|
||||
start: self.start.outside(parent),
|
||||
end: self.end.outside(parent),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn len(&self) -> UiScalar {
|
||||
self.end - self.start
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct UiRegion {
|
||||
pub x: UiSpan,
|
||||
pub y: UiSpan,
|
||||
}
|
||||
|
||||
impl UiRegion {
|
||||
pub const FULL: Self = Self {
|
||||
x: UiSpan::FULL,
|
||||
y: UiSpan::FULL,
|
||||
};
|
||||
|
||||
pub const fn new(x: UiSpan, y: UiSpan) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub const fn rel(rel: Vec2) -> Self {
|
||||
Self {
|
||||
x: UiSpan::rel(rel.x),
|
||||
y: UiSpan::rel(rel.y),
|
||||
}
|
||||
}
|
||||
pub const fn within(&self, parent: &Self) -> Self {
|
||||
Self {
|
||||
x: self.x.within(&parent.x),
|
||||
y: self.y.within(&parent.y),
|
||||
}
|
||||
}
|
||||
pub const fn outside(&self, parent: &Self) -> Self {
|
||||
Self {
|
||||
x: self.x.outside(&parent.x),
|
||||
y: self.y.outside(&parent.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn axis(&mut self, axis: Axis) -> &UiSpan {
|
||||
match axis {
|
||||
Axis::X => &self.x,
|
||||
Axis::Y => &self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn axis_mut(&mut self, axis: Axis) -> &mut UiSpan {
|
||||
match axis {
|
||||
Axis::X => &mut self.x,
|
||||
Axis::Y => &mut self.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn flip(&mut self, axis: Axis) {
|
||||
match axis {
|
||||
Axis::X => self.x.flip(),
|
||||
Axis::Y => self.y.flip(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shift(&mut self, offset: impl Into<UiVec2>) {
|
||||
let offset = offset.into();
|
||||
self.x.shift(offset.x);
|
||||
self.y.shift(offset.y);
|
||||
}
|
||||
|
||||
pub fn offset(mut self, offset: impl Into<UiVec2>) -> Self {
|
||||
self.shift(offset);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn to_px(&self, size: Vec2) -> PixelRegion {
|
||||
PixelRegion {
|
||||
top_left: self.top_left().get_rel() * size + self.top_left().get_abs(),
|
||||
bot_right: self.bot_right().get_rel() * size + self.bot_right().get_abs(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn center(&self) -> UiVec2 {
|
||||
Align::CENTER.pos().within(self)
|
||||
}
|
||||
|
||||
pub const fn size(&self) -> UiVec2 {
|
||||
UiVec2 {
|
||||
x: self.x.len(),
|
||||
y: self.y.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn top_left(&self) -> UiVec2 {
|
||||
UiVec2 {
|
||||
x: self.x.start,
|
||||
y: self.y.start,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn bot_right(&self) -> UiVec2 {
|
||||
UiVec2 {
|
||||
x: self.x.end,
|
||||
y: self.y.end,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_axis(axis: Axis, aligned: UiSpan, ortho: UiSpan) -> Self {
|
||||
Self {
|
||||
x: match axis {
|
||||
Axis::X => aligned,
|
||||
Axis::Y => ortho,
|
||||
},
|
||||
y: match axis {
|
||||
Axis::X => ortho,
|
||||
Axis::Y => aligned,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UiRegion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} -> {} (size: {})",
|
||||
self.top_left(),
|
||||
self.bot_right(),
|
||||
self.size()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct PixelRegion {
|
||||
pub top_left: Vec2,
|
||||
pub bot_right: Vec2,
|
||||
}
|
||||
|
||||
impl PixelRegion {
|
||||
pub fn contains(&self, pos: Vec2) -> bool {
|
||||
pos.x >= self.top_left.x
|
||||
&& pos.x <= self.bot_right.x
|
||||
&& pos.y >= self.top_left.y
|
||||
&& pos.y <= self.bot_right.y
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.bot_right - self.top_left
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PixelRegion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,488 +0,0 @@
|
||||
use crate::util::{Dirty, Resources, StrongRscId};
|
||||
use std::{cell::RefCell, fmt, rc::Rc};
|
||||
|
||||
/// Encoded, straight-alpha sRGB at an input boundary.
|
||||
///
|
||||
/// 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)]
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Srgba8 {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
pub a: u8,
|
||||
}
|
||||
|
||||
impl Srgba8 {
|
||||
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 {
|
||||
Self::BLACK
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PaintId {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.slot == other.slot
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PaintId {}
|
||||
|
||||
impl Paint for PaintId {
|
||||
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 {
|
||||
definition: Box<dyn Paint>,
|
||||
resolved: RefCell<Option<PaintId>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum PaintValueInner {
|
||||
Id(PaintId),
|
||||
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 {
|
||||
resources,
|
||||
entries: BUILTIN_PAINTS.map(Srgba8::to_linear).to_vec(),
|
||||
dirty: Dirty::new_all(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, paint: impl Paint) -> PaintId {
|
||||
paint.add_to(self)
|
||||
}
|
||||
|
||||
fn add_linear(&mut self, value: LinearRgba) -> PaintId {
|
||||
self.free_released();
|
||||
let old_capacity = self.resources.capacity();
|
||||
let strong = self.resources.add(PaintRsc);
|
||||
let slot = strong.slot();
|
||||
if (slot as usize) < old_capacity {
|
||||
self.entries[slot as usize] = value;
|
||||
self.dirty.mark(slot as usize);
|
||||
} else {
|
||||
self.entries.push(value);
|
||||
self.dirty.mark(slot as usize);
|
||||
}
|
||||
PaintId {
|
||||
slot,
|
||||
strong: Some(strong),
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces one managed paint in place. Every primitive keeps the same
|
||||
/// index, so a theme change dirties this table and no primitive buffer.
|
||||
pub fn set(&mut self, id: &PaintId, paint: impl Paint) {
|
||||
assert!(
|
||||
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) {
|
||||
self.entries[slot as usize] = value;
|
||||
self.dirty.mark(slot as usize);
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &PaintId) -> LinearRgba {
|
||||
self.entries[id.slot as usize]
|
||||
}
|
||||
|
||||
pub fn free_released(&mut self) {
|
||||
let entries = &mut self.entries;
|
||||
let dirty = &mut self.dirty;
|
||||
self.resources.apply(|id, _| {
|
||||
let slot = id.slot();
|
||||
entries[slot as usize] = LinearRgba::NONE;
|
||||
dirty.mark(slot as usize);
|
||||
});
|
||||
}
|
||||
|
||||
/// A new GPU device has no copy of this table even when the CPU-side UI
|
||||
/// 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 {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[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,252 +0,0 @@
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
use crate::{render::LayerOrder, util::to_mut};
|
||||
|
||||
pub type LayerId = usize;
|
||||
|
||||
struct LayerNode<T> {
|
||||
next: Ptr,
|
||||
prev: Ptr,
|
||||
child: Option<Child>,
|
||||
depth: usize,
|
||||
data: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Ptr {
|
||||
Next(usize),
|
||||
Parent(usize),
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct Layers<T> {
|
||||
vec: Vec<LayerNode<T>>,
|
||||
last: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Child {
|
||||
head: usize,
|
||||
tail: usize,
|
||||
}
|
||||
|
||||
pub type PrimitiveLayers = Layers<LayerOrder>;
|
||||
|
||||
impl<T: Default> Layers<T> {
|
||||
pub fn new() -> Layers<T> {
|
||||
Self {
|
||||
vec: vec![LayerNode::head()],
|
||||
last: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.vec.clear();
|
||||
self.vec.push(LayerNode::head());
|
||||
}
|
||||
|
||||
fn push(&mut self, node: LayerNode<T>) -> LayerId {
|
||||
let i = self.vec.len();
|
||||
self.vec.push(node);
|
||||
i
|
||||
}
|
||||
|
||||
pub fn next(&mut self, i: LayerId) -> LayerId {
|
||||
if let Ptr::Next(i) = self.vec[i].next {
|
||||
return i;
|
||||
}
|
||||
let i_new = self.push(LayerNode::new(
|
||||
T::default(),
|
||||
self.vec[i].next,
|
||||
Ptr::Next(i),
|
||||
self.vec[i].depth,
|
||||
));
|
||||
self.vec[i].next = Ptr::Next(i_new);
|
||||
self.vec[i_new].prev = Ptr::Next(i);
|
||||
match self.vec[i_new].next {
|
||||
Ptr::Next(i) => self.vec[i].prev = Ptr::Next(i_new),
|
||||
Ptr::Parent(i) => self.vec[i].child.as_mut().unwrap().tail = i_new,
|
||||
Ptr::None => self.last = i_new,
|
||||
}
|
||||
i_new
|
||||
}
|
||||
|
||||
pub fn child(&mut self, i: LayerId) -> LayerId {
|
||||
if let Some(c) = self.vec[i].child {
|
||||
return c.head;
|
||||
}
|
||||
let i_child = self.push(LayerNode::new(
|
||||
T::default(),
|
||||
Ptr::Parent(i),
|
||||
Ptr::Parent(i),
|
||||
self.vec[i].depth + 1,
|
||||
));
|
||||
self.vec[i].child = Some(Child {
|
||||
head: i_child,
|
||||
tail: i_child,
|
||||
});
|
||||
i_child
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> LayerIteratorMut<'_, T> {
|
||||
LayerIteratorMut::new(&mut self.vec, self.last)
|
||||
}
|
||||
|
||||
pub fn iter_orderless_mut(&mut self) -> impl Iterator<Item = (usize, &mut T)> {
|
||||
self.vec.iter_mut().map(|n| &mut n.data).enumerate()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (LayerId, &T)> {
|
||||
self.indices().map(|i| (i, &self.vec[i].data))
|
||||
}
|
||||
|
||||
pub fn iter_depth(&self) -> impl Iterator<Item = ((LayerId, usize), &T)> {
|
||||
self.indices()
|
||||
.map(|i| ((i, self.vec[i].depth), &self.vec[i].data))
|
||||
}
|
||||
|
||||
pub fn indices(&self) -> LayerIndexIterator<'_, T> {
|
||||
LayerIndexIterator::new(&self.vec, self.last)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> Default for Layers<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Index<LayerId> for Layers<T> {
|
||||
type Output = T;
|
||||
|
||||
fn index(&self, index: LayerId) -> &Self::Output {
|
||||
&self.vec[index].data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IndexMut<LayerId> for Layers<T> {
|
||||
fn index_mut(&mut self, index: LayerId) -> &mut Self::Output {
|
||||
&mut self.vec[index].data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> LayerNode<T> {
|
||||
pub fn new(data: T, next: Ptr, prev: Ptr, depth: usize) -> Self {
|
||||
Self {
|
||||
next,
|
||||
prev,
|
||||
child: None,
|
||||
data,
|
||||
depth,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn head() -> Self {
|
||||
Self::new(T::default(), Ptr::None, Ptr::None, 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LayerIteratorMut<'a, T> {
|
||||
inner: LayerIndexIterator<'a, T>,
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for LayerIteratorMut<'a, T> {
|
||||
type Item = (usize, &'a mut T);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let i = self.inner.next()?;
|
||||
// SAFETY: requires index iterator to work properly
|
||||
let layer = unsafe { to_mut(&self.inner.vec[i].data) };
|
||||
Some((i, layer))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> DoubleEndedIterator for LayerIteratorMut<'a, T> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
let i = self.inner.next_back()?;
|
||||
// SAFETY: requires index iterator to work properly
|
||||
let layer = unsafe { to_mut(&self.inner.vec[i].data) };
|
||||
Some((i, layer))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> LayerIteratorMut<'a, T> {
|
||||
fn new(vec: &'a mut Vec<LayerNode<T>>, last: usize) -> Self {
|
||||
Self {
|
||||
inner: LayerIndexIterator::new(vec, last),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LayerIndexIterator<'a, T> {
|
||||
next: Option<usize>,
|
||||
next_back: Option<usize>,
|
||||
vec: &'a Vec<LayerNode<T>>,
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for LayerIndexIterator<'a, T> {
|
||||
type Item = usize;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let ret_i = self.next?;
|
||||
let node = &self.vec[ret_i];
|
||||
self.next = if let Some(c) = node.child {
|
||||
Some(c.head)
|
||||
} else if let Ptr::Next(i) = node.next {
|
||||
Some(i)
|
||||
} else if let Ptr::Parent(i) = node.next {
|
||||
let mut node = &self.vec[i];
|
||||
while let Ptr::Parent(i) = node.next {
|
||||
node = &self.vec[i];
|
||||
}
|
||||
if let Ptr::Next(i) = node.next {
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if self.next_back.unwrap() == ret_i {
|
||||
self.next = None;
|
||||
self.next_back = None;
|
||||
}
|
||||
Some(ret_i)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> DoubleEndedIterator for LayerIndexIterator<'a, T> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
let ret_i = self.next_back?;
|
||||
let node = &self.vec[ret_i];
|
||||
self.next_back = if let Ptr::Next(mut i) = node.prev {
|
||||
while let Some(c) = self.vec[i].child {
|
||||
i = c.tail
|
||||
}
|
||||
Some(i)
|
||||
} else if let Ptr::Parent(i) = node.prev {
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if self.next.unwrap() == ret_i {
|
||||
self.next = None;
|
||||
self.next_back = None;
|
||||
}
|
||||
Some(ret_i)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> LayerIndexIterator<'a, T> {
|
||||
fn new(vec: &'a Vec<LayerNode<T>>, last: usize) -> Self {
|
||||
let mut last = last;
|
||||
while let Some(c) = vec[last].child {
|
||||
last = c.tail;
|
||||
}
|
||||
Self {
|
||||
next: Some(0),
|
||||
next_back: Some(last),
|
||||
vec,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
mod color;
|
||||
mod layer;
|
||||
mod text;
|
||||
mod texture;
|
||||
|
||||
pub use color::*;
|
||||
pub use layer::*;
|
||||
pub use text::*;
|
||||
pub use texture::*;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,377 +0,0 @@
|
||||
use crate::util::{Resources, RscHandle, StrongRscId, Vec2, WeakRscId};
|
||||
use image::{DynamicImage, GenericImageView};
|
||||
use std::{cell::RefCell, collections::HashMap, ops::Index, rc::Rc};
|
||||
|
||||
/// Which of the two things a texture slot holds. See TEXTURES.md's
|
||||
/// "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)]
|
||||
pub struct TextureHandle {
|
||||
rsc: RscHandle<TextureRsc>,
|
||||
}
|
||||
|
||||
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
|
||||
/// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped
|
||||
pub struct Textures {
|
||||
resources: Rc<RefCell<Resources<TextureRsc>>>,
|
||||
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>,
|
||||
}
|
||||
|
||||
pub enum TextureUpdate<'a> {
|
||||
Push(TextureKind, &'a DynamicImage),
|
||||
Set(TextureKind, 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),
|
||||
Free(u32),
|
||||
PushFree(TextureKind),
|
||||
SetFree,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PatchRect {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
enum Update {
|
||||
Push(TextureKind, u32),
|
||||
Set(TextureKind, u32),
|
||||
Patch(u32, PatchRect),
|
||||
Free(u32),
|
||||
}
|
||||
|
||||
impl Textures {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
resources: Rc::new(RefCell::new(Resources::new())),
|
||||
images: Vec::new(),
|
||||
kinds: Vec::new(),
|
||||
shared: HashMap::new(),
|
||||
next_page_layer: 0,
|
||||
free_page_layers: Vec::new(),
|
||||
updates: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
|
||||
let image = image.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 {
|
||||
rsc: RscHandle::new(id, self.resources.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn upgrade(&mut self, id: WeakRscId<TextureRsc>) -> Option<TextureHandle> {
|
||||
self.free();
|
||||
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.kinds[i as usize] = kind;
|
||||
self.updates.push(Update::Set(kind, i));
|
||||
} else {
|
||||
self.images.push(Some(image));
|
||||
self.kinds.push(kind);
|
||||
self.updates.push(Update::Push(kind, 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 {
|
||||
self.images[handle.rsc.id().slot() as usize]
|
||||
.as_mut()
|
||||
.expect("texture was freed while still held")
|
||||
}
|
||||
|
||||
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
|
||||
self.updates
|
||||
.push(Update::Patch(handle.rsc.id().slot(), rect));
|
||||
}
|
||||
|
||||
/// A new device starts with no textures, and the renderer-side mirror
|
||||
/// of these slots (`render::texture::GpuTextures`) starts empty with
|
||||
/// it. What it must not do is start empty while the handles widgets
|
||||
/// are still holding name slots by *index*: `Textures::reset` used to
|
||||
/// 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) {
|
||||
let updates = &mut self.updates;
|
||||
let images = &mut self.images;
|
||||
let free_page_layers = &mut self.free_page_layers;
|
||||
self.resources.borrow_mut().apply(|id, resource| {
|
||||
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<'_>> {
|
||||
self.updates.drain(..).map(|u| match u {
|
||||
Update::Push(kind, i) => self.images[i as usize]
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Push(kind, img))
|
||||
.unwrap_or(TextureUpdate::PushFree(kind)),
|
||||
Update::Set(kind, i) => self.images[i as usize]
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Set(kind, i, img))
|
||||
.unwrap_or(TextureUpdate::SetFree),
|
||||
Update::Patch(i, rect) => self.images[i as usize]
|
||||
.as_ref()
|
||||
.map(|img| TextureUpdate::Patch(i, rect, img))
|
||||
.unwrap_or(TextureUpdate::SetFree),
|
||||
Update::Free(i) => TextureUpdate::Free(i),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TextureHandle {
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.rsc.get().size
|
||||
}
|
||||
|
||||
/// The bind-group index this handle draws with. Only valid for a
|
||||
/// standalone image; an atlas page has no bind group of its own -- it
|
||||
/// samples the shared array via `layer()` instead. Getting this wrong is
|
||||
/// a caller bug (the wrong kind of handle reached the wrong draw path),
|
||||
/// 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 {
|
||||
type Output = DynamicImage;
|
||||
|
||||
fn index(&self, index: &TextureHandle) -> &Self::Output {
|
||||
self.images[index.rsc.id().slot() as usize]
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Textures {
|
||||
fn default() -> Self {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
use crate::{
|
||||
PatchRect, TextureHandle, Textures,
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use image::RgbaImage;
|
||||
use swash::scale::image::{Content, Image};
|
||||
|
||||
pub(crate) const PAGE: u32 = 1024;
|
||||
pub(crate) const DEFAULT_GLYPH_BUCKET_ID: u64 = 0;
|
||||
|
||||
const PAD: u32 = 1;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct GlyphKey {
|
||||
pub font: u64,
|
||||
pub glyph: u32,
|
||||
pub size: u32,
|
||||
pub subpixel: u8,
|
||||
pub coords: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GlyphEntry {
|
||||
pub uv_min: [f32; 2],
|
||||
pub uv_max: [f32; 2],
|
||||
pub left: i32,
|
||||
pub top: i32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub is_color: bool,
|
||||
pub layer: u32,
|
||||
}
|
||||
|
||||
struct Page {
|
||||
handle: TextureHandle,
|
||||
/// 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,
|
||||
y: u32,
|
||||
shelf_height: u32,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Bucket {
|
||||
pages: Vec<Page>,
|
||||
entries: HashMap<GlyphKey, Option<GlyphEntry>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GlyphAtlas {
|
||||
buckets: HashMap<u64, Bucket>,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
impl GlyphAtlas {
|
||||
pub(crate) fn get(&self, bucket: u64, key: &GlyphKey) -> Option<Option<GlyphEntry>> {
|
||||
self.buckets.get(&bucket)?.entries.get(key).copied()
|
||||
}
|
||||
|
||||
/// Rasterised pixels in, a place in the atlas out. `None` means the glyph
|
||||
/// 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 h = image.placement.height;
|
||||
if w == 0 || h == 0 {
|
||||
bucket.entries.insert(key, None);
|
||||
return None;
|
||||
}
|
||||
if w + PAD * 2 > PAGE || h + PAD * 2 > PAGE {
|
||||
// A single glyph larger than a page. Refusing is better than
|
||||
// silently drawing a cropped one; the caller draws nothing.
|
||||
bucket.entries.insert(key, None);
|
||||
return None;
|
||||
}
|
||||
|
||||
let (page_idx, x, y) = bucket.allocate(w, h, textures);
|
||||
let page = &bucket.pages[page_idx];
|
||||
|
||||
let img = textures.image_mut(&page.handle);
|
||||
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
|
||||
write_glyph(rgba, image, x, y);
|
||||
|
||||
let rect = PatchRect {
|
||||
x,
|
||||
y,
|
||||
width: w,
|
||||
height: h,
|
||||
};
|
||||
textures.patch(&page.handle, rect);
|
||||
|
||||
let page = &bucket.pages[page_idx];
|
||||
let scale = 1.0 / PAGE as f32;
|
||||
let entry = GlyphEntry {
|
||||
uv_min: [x as f32 * scale, y as f32 * scale],
|
||||
uv_max: [(x + w) as f32 * scale, (y + h) as f32 * scale],
|
||||
left: image.placement.left,
|
||||
top: image.placement.top,
|
||||
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 {
|
||||
handle,
|
||||
x: PAD + w + PAD,
|
||||
y: PAD,
|
||||
shelf_height: h + PAD,
|
||||
});
|
||||
(self.pages.len() - 1, PAD, PAD)
|
||||
}
|
||||
}
|
||||
|
||||
fn fits(page: &Page, need_w: u32, need_h: u32) -> bool {
|
||||
(page.x + need_w <= PAGE && page.y + need_h <= PAGE)
|
||||
|| (need_w + PAD <= PAGE && page.y + page.shelf_height + need_h <= PAGE)
|
||||
}
|
||||
|
||||
fn write_glyph(page: &mut RgbaImage, image: &Image, x: u32, y: u32) {
|
||||
let w = image.placement.width;
|
||||
let h = image.placement.height;
|
||||
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 => {
|
||||
for row in 0..h {
|
||||
for col in 0..w {
|
||||
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::SubpixelMask => {
|
||||
for row in 0..h {
|
||||
for col in 0..w {
|
||||
let i = ((row * w + col) * 4) as usize;
|
||||
let a = image.data[i + 1];
|
||||
page.put_pixel(x + col, y + row, image::Rgba([255, 255, 255, a]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PlacedGlyph {
|
||||
pub entry: GlyphEntry,
|
||||
pub offset: Vec2,
|
||||
pub paint: u32,
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
use crate::{UiRegion, util::Id};
|
||||
use wgpu::*;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable, Default)]
|
||||
pub struct WindowUniform {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct PrimitiveInstance {
|
||||
pub region: UiRegion,
|
||||
pub binding: u32,
|
||||
pub idx: u32,
|
||||
pub mask_idx: MaskIdx,
|
||||
pub move_idx: MoveIdx,
|
||||
}
|
||||
|
||||
pub fn instance_slot_layout() -> VertexBufferLayout<'static> {
|
||||
const ATTRIBS: [VertexAttribute; 1] = vertex_attr_array![0 => Uint32];
|
||||
VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<u32>() as BufferAddress,
|
||||
step_mode: VertexStepMode::Instance,
|
||||
attributes: &ATTRIBS,
|
||||
}
|
||||
}
|
||||
|
||||
pub type MaskIdx = Id<u32>;
|
||||
|
||||
impl MaskIdx {
|
||||
pub const NONE: Self = Self::preset(u32::MAX);
|
||||
}
|
||||
|
||||
pub type MoveIdx = Id<u32>;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Mask {
|
||||
pub primitive: u32,
|
||||
/// The mask this one was set *inside* (`MaskIdx::NONE` at the top), so
|
||||
/// 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
|
||||
/// a `vec2<f32>`, which gives the struct an 8-byte alignment and rounds its
|
||||
/// WGSL size up to 16 bytes even though `delta` + `parent` only total 12 --
|
||||
/// the same trap `GlyphPrimitive` documents below. `bytemuck` does not
|
||||
/// check this for us, and getting it wrong is a wgpu validation panic at
|
||||
/// draw time ("buffer bound ... with size 12 where the shader expects 16"),
|
||||
/// not a compile error.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct MoveOffset {
|
||||
pub delta: [f32; 2],
|
||||
pub parent: u32,
|
||||
_pad: u32,
|
||||
}
|
||||
|
||||
impl MoveOffset {
|
||||
pub const NONE_PARENT: u32 = u32::MAX;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,662 +0,0 @@
|
||||
use crate::{
|
||||
Ui, UiData,
|
||||
render::{
|
||||
data::{PrimitiveInstance, instance_slot_layout},
|
||||
texture::GpuTextures,
|
||||
util::ArrBuf,
|
||||
},
|
||||
util::{HashMap, Vec2},
|
||||
};
|
||||
use data::WindowUniform;
|
||||
use pollster::FutureExt;
|
||||
use wgpu::{
|
||||
util::{BufferInitDescriptor, DeviceExt},
|
||||
*,
|
||||
};
|
||||
|
||||
mod atlas;
|
||||
mod data;
|
||||
mod frame_report;
|
||||
mod primitive;
|
||||
mod sdf;
|
||||
mod texture;
|
||||
mod util;
|
||||
|
||||
pub use atlas::*;
|
||||
pub use data::{Mask, MaskIdx, MoveIdx, MoveOffset};
|
||||
pub use frame_report::{FrameParts, FrameReport, FrameStats, JANK_THRESHOLD};
|
||||
pub use primitive::*;
|
||||
pub use sdf::{distance_from_rect, rounded_rect_coverage};
|
||||
|
||||
pub const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
|
||||
|
||||
/// The advertised swapchain format and the sRGB view Iris renders through.
|
||||
/// A backend may advertise only the non-sRGB member of an RGBA/BGRA pair;
|
||||
/// wgpu permits its sRGB counterpart as a configured view format.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct SurfaceFormat {
|
||||
pub surface: TextureFormat,
|
||||
pub view: TextureFormat,
|
||||
}
|
||||
|
||||
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 {
|
||||
uniform_group: BindGroup,
|
||||
primitive_layout: BindGroupLayout,
|
||||
primitives: PrimitiveBuffers,
|
||||
primitive_group: BindGroup,
|
||||
rsc_layout: BindGroupLayout,
|
||||
rsc_group: BindGroup,
|
||||
|
||||
pipeline: RenderPipeline,
|
||||
|
||||
layers: HashMap<usize, RenderLayer>,
|
||||
active: Vec<usize>,
|
||||
window_buffer: Buffer,
|
||||
textures: GpuTextures,
|
||||
instances: ArrBuf<PrimitiveInstance>,
|
||||
masks: ArrBuf<Mask>,
|
||||
move_offsets: ArrBuf<MoveOffset>,
|
||||
paints: ArrBuf<crate::LinearRgba>,
|
||||
masks_layout: BindGroupLayout,
|
||||
masks_group: BindGroup,
|
||||
}
|
||||
|
||||
struct RenderLayer {
|
||||
order: ArrBuf<u32>,
|
||||
/// A standalone image's slots, kept apart from `order` because each
|
||||
/// 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
|
||||
/// order, refreshed alongside it. Not in the vertex buffer itself
|
||||
/// because it names a bind group, not shader data.
|
||||
image_tex_indices: Vec<u32>,
|
||||
}
|
||||
|
||||
impl UiRenderNode {
|
||||
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
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 {
|
||||
let layer = &self.layers[i];
|
||||
if layer.order.len() == 0 && layer.images.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
if layer.order.len() > 0 {
|
||||
pass.set_bind_group(2, &self.rsc_group, &[]);
|
||||
pass.set_vertex_buffer(0, layer.order.buffer.slice(..));
|
||||
pass.draw(0..4, 0..layer.order.len() as u32);
|
||||
}
|
||||
// Images draw after this layer's rects and glyphs, one draw call
|
||||
// each with its own bind group. That draws every image "on top"
|
||||
// within the layer, which loses nothing that currently exists:
|
||||
// `Primitives::apply_free` frees with `swap_remove`, so a layer's
|
||||
// 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 {
|
||||
let render_handle = ui.render_state.clone();
|
||||
let mut render_guard = render_handle.get_mut();
|
||||
let ui_render = &mut *render_guard;
|
||||
let ui_data: &mut UiData = ui;
|
||||
self.active.clear();
|
||||
for (i, order) in ui_render.layers.iter_mut() {
|
||||
self.active.push(i);
|
||||
let rlayer = self.layers.entry(i).or_insert_with(|| RenderLayer {
|
||||
order: ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
||||
"layer order",
|
||||
),
|
||||
images: ArrBuf::new(
|
||||
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);
|
||||
if self
|
||||
.primitives
|
||||
.update(device, queue, ui_render.primitives.data_mut())
|
||||
{
|
||||
self.primitive_group = Self::primitive_group(
|
||||
device,
|
||||
&self.primitive_layout,
|
||||
self.primitives.buffers(),
|
||||
);
|
||||
}
|
||||
resized
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let (entries, dirty) = ui_data.masks.for_upload();
|
||||
let masks_resized = self.masks.update(device, queue, entries, dirty);
|
||||
let (entries, dirty) = ui_data.move_offsets.for_upload();
|
||||
let moves_resized = self.move_offsets.update(device, queue, entries, dirty);
|
||||
let (entries, dirty) = ui_data.paints.for_upload();
|
||||
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(
|
||||
device,
|
||||
&self.masks_layout,
|
||||
&self.masks,
|
||||
&self.move_offsets,
|
||||
&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) {
|
||||
let size = size.into();
|
||||
let slice = &[WindowUniform {
|
||||
width: size.x,
|
||||
height: size.y,
|
||||
}];
|
||||
queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice));
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
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 {
|
||||
label: Some("window"),
|
||||
contents: bytemuck::cast_slice(&[window_uniform]),
|
||||
usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
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(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"ui masks",
|
||||
);
|
||||
let move_offsets = ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"ui move offsets",
|
||||
);
|
||||
let paints = ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
"ui paints",
|
||||
);
|
||||
|
||||
let rsc_layout = Self::rsc_layout(device);
|
||||
let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager);
|
||||
let masks_layout = Self::masks_layout(device);
|
||||
let masks_group = Self::masks_group(
|
||||
device,
|
||||
&masks_layout,
|
||||
&masks,
|
||||
&move_offsets,
|
||||
&instances,
|
||||
&paints,
|
||||
);
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||
label: Some("UI Shape Pipeline Layout"),
|
||||
bind_group_layouts: &[
|
||||
Some(&uniform_layout),
|
||||
Some(&primitive_layout),
|
||||
Some(&rsc_layout),
|
||||
Some(&masks_layout),
|
||||
],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
|
||||
label: Some("UI Shape Pipeline"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[Some(instance_slot_layout())],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(ColorTargetState {
|
||||
format: target_format,
|
||||
blend: Some(BlendState::ALPHA_BLENDING),
|
||||
write_mask: ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
primitive: PrimitiveState {
|
||||
topology: PrimitiveTopology::TriangleStrip,
|
||||
strip_index_format: None,
|
||||
front_face: FrontFace::Cw,
|
||||
cull_mode: Some(Face::Back),
|
||||
polygon_mode: PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
multiview_mask: 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(
|
||||
device: &Device,
|
||||
layout: &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 {
|
||||
entries: &[
|
||||
BindGroupLayoutEntry {
|
||||
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,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
|
||||
ty: BindingType::Buffer {
|
||||
ty: BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
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,
|
||||
},
|
||||
],
|
||||
label: Some("ui masks"),
|
||||
})
|
||||
}
|
||||
|
||||
fn masks_group(
|
||||
device: &Device,
|
||||
layout: &BindGroupLayout,
|
||||
masks: &ArrBuf<Mask>,
|
||||
move_offsets: &ArrBuf<MoveOffset>,
|
||||
instances: &ArrBuf<PrimitiveInstance>,
|
||||
paints: &ArrBuf<crate::LinearRgba>,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: masks.buffer.as_entire_binding(),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: move_offsets.buffer.as_entire_binding(),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: instances.buffer.as_entire_binding(),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: paints.buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
label: Some("ui masks"),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn view_count(&self) -> usize {
|
||||
self.textures.view_count()
|
||||
}
|
||||
|
||||
pub fn take_image_bind_group_creates(&mut self) -> u64 {
|
||||
self.textures.take_bind_group_creates()
|
||||
}
|
||||
|
||||
pub fn take_atlas_pages_grown(&mut self) -> u64 {
|
||||
self.textures.take_pages_grown()
|
||||
}
|
||||
}
|
||||
|
||||
/// What `UiRenderNode::update` changed this frame that a caller building a
|
||||
/// per-frame diagnostic report cares about -- see `take_image_bind_group_creates`/
|
||||
/// `take_atlas_pages_grown` for the two counters this doesn't carry (they
|
||||
/// use the existing "call before update()" convention instead, so as not
|
||||
/// to disturb `bench_images`' documented counts).
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct FrameUpdateStats {
|
||||
pub masks_resized: bool,
|
||||
pub moves_resized: bool,
|
||||
pub paints_resized: bool,
|
||||
}
|
||||
@@ -1,720 +0,0 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::{
|
||||
UiRegion, WidgetId,
|
||||
render::{
|
||||
ArrBuf,
|
||||
data::{MaskIdx, MoveIdx, PrimitiveInstance},
|
||||
},
|
||||
util::{Dirty, HashSet},
|
||||
};
|
||||
use bytemuck::Pod;
|
||||
use wgpu::*;
|
||||
|
||||
/// The `binding` tag `Painter` writes on an image instance. Distinct from any
|
||||
/// `Primitive::BINDING` because images have no `PrimitiveData` entry to key
|
||||
/// one from -- a bind group already selects the texture -- so this only ever
|
||||
/// has to match the shader's `TEXTURE` constant and flag "this instance is
|
||||
/// drawn with its own bind group" to the code below.
|
||||
pub const IMAGE_BINDING: u32 = 1;
|
||||
|
||||
pub trait Primitive: Pod {
|
||||
const BINDING: u32;
|
||||
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self>;
|
||||
fn vec_ref(data: &PrimitiveData) -> &PrimitiveVec<Self>;
|
||||
}
|
||||
|
||||
macro_rules! primitives {
|
||||
($($name:ident: $ty:ty => $binding:expr,)*) => {
|
||||
#[derive(Default)]
|
||||
pub struct PrimitiveData {
|
||||
$(pub(crate) $name: PrimitiveVec<$ty>,)*
|
||||
}
|
||||
|
||||
pub struct PrimitiveBuffers {
|
||||
$($name: ArrBuf<$ty>,)*
|
||||
}
|
||||
|
||||
impl PrimitiveBuffers {
|
||||
/// Answers whether **any** of the per-primitive buffers was
|
||||
/// reallocated, which is the only thing that obliges the
|
||||
/// caller to rebuild the bind group naming them. It used to
|
||||
/// return nothing and the group was rebuilt on every frame
|
||||
/// the arena changed -- once `ArrBuf` kept its buffer across
|
||||
/// a length change, that was a bind group per frame for a
|
||||
/// buffer identity that had not moved.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
device: &Device,
|
||||
queue: &Queue,
|
||||
data: &mut PrimitiveData,
|
||||
) -> bool {
|
||||
let mut reallocated = false;
|
||||
$(
|
||||
let (entries, dirty) = data.$name.for_upload();
|
||||
reallocated |= self.$name.update(device, queue, entries, dirty);
|
||||
)*
|
||||
reallocated
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveBuffers {
|
||||
pub const LEN: usize = primitives!(@count $($name)*);
|
||||
pub const BINDINGS: [u32; Self::LEN] = [$(<$ty>::BINDING,)*];
|
||||
pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] {
|
||||
[
|
||||
$((<$ty>::BINDING, &self.$name.buffer),)*
|
||||
]
|
||||
}
|
||||
pub fn new(device: &Device) -> Self {
|
||||
Self {
|
||||
$($name: ArrBuf::new(
|
||||
device,
|
||||
BufferUsages::STORAGE | BufferUsages::COPY_DST,
|
||||
stringify!($name),
|
||||
),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveData {
|
||||
pub fn needs_upload(&self) -> bool {
|
||||
$(!self.$name.dirty.is_clean() ||)* false
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
$(self.$name.clear();)*
|
||||
}
|
||||
pub fn free(&mut self, binding: u32, idx: usize) {
|
||||
match binding {
|
||||
$(<$ty>::BINDING => self.$name.free(idx),)*
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
unsafe impl bytemuck::Pod for $ty {}
|
||||
unsafe impl bytemuck::Zeroable for $ty {}
|
||||
impl Primitive for $ty {
|
||||
const BINDING: u32 = $binding;
|
||||
fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec<Self> {
|
||||
&mut data.$name
|
||||
}
|
||||
fn vec_ref(data: &PrimitiveData) -> &PrimitiveVec<Self> {
|
||||
&data.$name
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
// The recursion has to hand back the same shape it matches -- space
|
||||
// separated, not comma separated. Written with `$($t),+` it re-entered
|
||||
// with a comma as the first token and never terminated, which happened to
|
||||
// work only because there were exactly two primitives: the first step left
|
||||
// a single token, and a single token matches the base case whichever
|
||||
// separator it was written with.
|
||||
(@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t)+) };
|
||||
(@count $t:tt) => { 1 };
|
||||
}
|
||||
|
||||
pub struct Primitives {
|
||||
instances: Vec<PrimitiveInstance>,
|
||||
original_instances: Vec<Option<PrimitiveInstance>>,
|
||||
assoc: Vec<WidgetId>,
|
||||
handle_idx: Vec<u32>,
|
||||
freed: Vec<usize>,
|
||||
reusable: Vec<usize>,
|
||||
data: PrimitiveData,
|
||||
pub dirty: Dirty,
|
||||
}
|
||||
|
||||
impl Default for Primitives {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
instances: Default::default(),
|
||||
original_instances: Default::default(),
|
||||
assoc: Default::default(),
|
||||
handle_idx: Default::default(),
|
||||
freed: Vec::new(),
|
||||
reusable: Vec::new(),
|
||||
data: Default::default(),
|
||||
dirty: Dirty::new_all(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Primitives {
|
||||
const NO_HANDLE: u32 = u32::MAX;
|
||||
|
||||
/// Writes a primitive into the arena and hands back its slot and its
|
||||
/// entry in the per-primitive data. The caller (`UiRenderState`) puts
|
||||
/// the slot into a layer's draw order -- an instance that no layer
|
||||
/// names is never rasterized, which is what a mask shape drawn only to
|
||||
/// be *referenced* uses.
|
||||
pub fn alloc<P: Primitive>(
|
||||
&mut self,
|
||||
PrimitiveInst {
|
||||
id,
|
||||
primitive,
|
||||
region,
|
||||
mask_idx,
|
||||
move_idx,
|
||||
}: PrimitiveInst<P>,
|
||||
) -> (u32, usize) {
|
||||
let data_idx = P::vec(&mut self.data).add(primitive);
|
||||
let slot = self.push(
|
||||
PrimitiveInstance {
|
||||
region,
|
||||
idx: data_idx as u32,
|
||||
mask_idx,
|
||||
move_idx,
|
||||
binding: P::BINDING,
|
||||
},
|
||||
id,
|
||||
);
|
||||
(slot, data_idx)
|
||||
}
|
||||
|
||||
pub fn alloc_image(
|
||||
&mut self,
|
||||
id: WidgetId,
|
||||
texture_idx: u32,
|
||||
region: UiRegion,
|
||||
mask_idx: MaskIdx,
|
||||
move_idx: MoveIdx,
|
||||
) -> u32 {
|
||||
self.push(
|
||||
PrimitiveInstance {
|
||||
region,
|
||||
idx: texture_idx,
|
||||
mask_idx,
|
||||
move_idx,
|
||||
binding: IMAGE_BINDING,
|
||||
},
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
fn push(&mut self, inst: PrimitiveInstance, id: WidgetId) -> u32 {
|
||||
let slot = if let Some(i) = self.reusable.pop() {
|
||||
self.instances[i] = inst;
|
||||
self.original_instances[i] = None;
|
||||
self.assoc[i] = id;
|
||||
self.handle_idx[i] = Self::NO_HANDLE;
|
||||
i
|
||||
} else {
|
||||
self.instances.push(inst);
|
||||
self.original_instances.push(None);
|
||||
self.assoc.push(id);
|
||||
self.handle_idx.push(Self::NO_HANDLE);
|
||||
self.instances.len() - 1
|
||||
};
|
||||
self.dirty.mark(slot);
|
||||
slot as u32
|
||||
}
|
||||
|
||||
/// Rewrites a slot this widget already owns. Freed slots are not reusable
|
||||
/// until the end of a frame, so nested provisional redraws must recycle.
|
||||
/// The caller has already checked that `h` is the same kind of
|
||||
/// primitive in the same layer, which is what makes the slot, its
|
||||
/// entry in the per-primitive data, and its position in the layer's
|
||||
/// draw order all still the right ones -- so nothing here touches
|
||||
/// `freed`, `reusable` or `LayerOrder`, and no renumbering follows.
|
||||
pub fn recycle<P: Primitive>(
|
||||
&mut self,
|
||||
h: &PrimitiveHandle,
|
||||
PrimitiveInst {
|
||||
id,
|
||||
primitive,
|
||||
region,
|
||||
mask_idx,
|
||||
move_idx,
|
||||
}: PrimitiveInst<P>,
|
||||
) {
|
||||
debug_assert_eq!(
|
||||
h.binding,
|
||||
P::BINDING,
|
||||
"recycling slot {} as a different kind of primitive than it holds",
|
||||
h.slot,
|
||||
);
|
||||
P::vec(&mut self.data).set(h.data_idx, primitive);
|
||||
self.set_instance(
|
||||
h.slot,
|
||||
PrimitiveInstance {
|
||||
region,
|
||||
idx: h.data_idx as u32,
|
||||
mask_idx,
|
||||
move_idx,
|
||||
binding: P::BINDING,
|
||||
},
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn recycle_image(
|
||||
&mut self,
|
||||
h: &PrimitiveHandle,
|
||||
id: WidgetId,
|
||||
texture_idx: u32,
|
||||
region: UiRegion,
|
||||
mask_idx: MaskIdx,
|
||||
move_idx: MoveIdx,
|
||||
) {
|
||||
debug_assert_eq!(
|
||||
h.binding, IMAGE_BINDING,
|
||||
"recycling slot {} as an image when it holds a primitive",
|
||||
h.slot,
|
||||
);
|
||||
self.set_instance(
|
||||
h.slot,
|
||||
PrimitiveInstance {
|
||||
region,
|
||||
idx: texture_idx,
|
||||
mask_idx,
|
||||
move_idx,
|
||||
binding: IMAGE_BINDING,
|
||||
},
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
/// Writes an instance into a slot that already holds one, marking it
|
||||
/// dirty only if it differs -- the same rule as
|
||||
/// [`PrimitiveVec::set`], for the same reason. `assoc` is not part of
|
||||
/// the comparison because it is never uploaded.
|
||||
fn set_instance(&mut self, slot: u32, inst: PrimitiveInstance, id: WidgetId) {
|
||||
let slot = slot as usize;
|
||||
self.assoc[slot] = id;
|
||||
if bytemuck::bytes_of(&self.instances[slot]) == bytemuck::bytes_of(&inst) {
|
||||
return;
|
||||
}
|
||||
if !self.dirty.contains(slot) {
|
||||
self.original_instances[slot] = Some(self.instances[slot]);
|
||||
}
|
||||
self.instances[slot] = inst;
|
||||
if self.original_instances[slot]
|
||||
.is_some_and(|original| bytemuck::bytes_of(&original) == bytemuck::bytes_of(&inst))
|
||||
{
|
||||
self.original_instances[slot] = None;
|
||||
self.dirty.unmark(slot);
|
||||
} else {
|
||||
self.dirty.mark(slot);
|
||||
}
|
||||
}
|
||||
|
||||
/// Retires a slot, answering the mask it was drawn under so the caller
|
||||
/// can drop that mask's ref. The slot itself only becomes reusable at
|
||||
/// the next [`Self::apply_free`] -- see `freed`.
|
||||
pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx {
|
||||
let slot = h.slot as usize;
|
||||
if h.binding != IMAGE_BINDING {
|
||||
self.data.free(h.binding, h.data_idx);
|
||||
}
|
||||
self.freed.push(slot);
|
||||
self.instances[slot].mask_idx
|
||||
}
|
||||
|
||||
pub fn release_freed(&mut self) {
|
||||
self.reusable.append(&mut self.freed);
|
||||
}
|
||||
|
||||
pub fn owner(&self, slot: u32) -> WidgetId {
|
||||
self.assoc[slot as usize]
|
||||
}
|
||||
|
||||
pub fn set_handle_index(&mut self, slot: u32, idx: u32) {
|
||||
self.handle_idx[slot as usize] = idx;
|
||||
}
|
||||
|
||||
/// Where `slot`'s handle sits in its owner's `ActiveData::primitives`
|
||||
/// -- see [`Self::handle_idx`]. `None` only for a slot whose owner
|
||||
/// never took the handle, which nothing in this crate does.
|
||||
pub fn handle_index(&self, slot: u32) -> Option<usize> {
|
||||
match self.handle_idx[slot as usize] {
|
||||
Self::NO_HANDLE => None,
|
||||
idx => Some(idx as usize),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.dirty.mark_all();
|
||||
self.instances.clear();
|
||||
self.original_instances.clear();
|
||||
self.assoc.clear();
|
||||
self.handle_idx.clear();
|
||||
self.freed.clear();
|
||||
self.reusable.clear();
|
||||
self.data.clear();
|
||||
}
|
||||
|
||||
pub fn live_count(&self) -> usize {
|
||||
self.instances.len() - self.freed.len() - self.reusable.len()
|
||||
}
|
||||
|
||||
pub fn live_instances(&self) -> impl Iterator<Item = (u32, WidgetId, bool)> + '_ {
|
||||
let dead: HashSet<usize> = self.freed.iter().chain(&self.reusable).copied().collect();
|
||||
(0..self.instances.len())
|
||||
.filter(move |i| !dead.contains(i))
|
||||
.map(|i| {
|
||||
(
|
||||
i as u32,
|
||||
self.assoc[i],
|
||||
self.instances[i].binding == IMAGE_BINDING,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &PrimitiveData {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn instances(&self) -> &Vec<PrimitiveInstance> {
|
||||
&self.instances
|
||||
}
|
||||
|
||||
pub fn instances_for_upload(&mut self) -> (&[PrimitiveInstance], &mut Dirty) {
|
||||
(&self.instances, &mut self.dirty)
|
||||
}
|
||||
|
||||
/// The per-primitive data, mutably, for the one caller that uploads it
|
||||
/// (`UiRenderNode::update`) and so has to clear its dirty sets.
|
||||
pub fn data_mut(&mut self) -> &mut PrimitiveData {
|
||||
&mut self.data
|
||||
}
|
||||
|
||||
pub fn needs_upload(&self) -> bool {
|
||||
!self.dirty.is_clean() || self.data.needs_upload()
|
||||
}
|
||||
|
||||
pub fn instance(&self, slot: u32) -> &PrimitiveInstance {
|
||||
&self.instances[slot as usize]
|
||||
}
|
||||
|
||||
/// The per-primitive data behind `slot`, or `None` if that slot holds
|
||||
/// a different kind of primitive -- the `binding` check is the same
|
||||
/// one the shader's dispatch switch makes, and it is what stops a
|
||||
/// caller reading a glyph's index into the rect table.
|
||||
pub fn primitive_data<P: Primitive>(&self, slot: u32) -> Option<&P> {
|
||||
let inst = self.instance(slot);
|
||||
(inst.binding == P::BINDING).then(|| &P::vec_ref(&self.data)[inst.idx as usize])
|
||||
}
|
||||
|
||||
pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion {
|
||||
let slot = h.slot as usize;
|
||||
// The caller receives unrestricted mutable access, so this path
|
||||
// cannot tell whether a later write restored the prior bytes. Leave
|
||||
// the entry dirty rather than letting a stale `set_instance` baseline
|
||||
// cancel it.
|
||||
self.original_instances[slot] = None;
|
||||
self.dirty.mark(slot);
|
||||
&mut self.instances[h.slot as usize].region
|
||||
}
|
||||
}
|
||||
|
||||
/// Both lists free with `swap_remove`, so a layer's draw order was already
|
||||
/// undefined before this split: nothing here may assume one primitive
|
||||
/// stays adjacent to another once anything in the layer has been freed.
|
||||
#[derive(Default)]
|
||||
pub struct LayerOrder {
|
||||
order: Vec<u32>,
|
||||
order_dirty: Dirty,
|
||||
images_dirty: Dirty,
|
||||
/// Standalone images, kept apart because each draws with its own bind
|
||||
/// group rather than sharing the layer's one instanced draw -- see
|
||||
/// `UiRenderNode::draw`.
|
||||
images: Vec<u32>,
|
||||
free: Vec<usize>,
|
||||
image_free: Vec<usize>,
|
||||
pub updated: bool,
|
||||
}
|
||||
|
||||
impl LayerOrder {
|
||||
pub fn push(&mut self, slot: u32, is_image: bool) -> usize {
|
||||
self.updated = true;
|
||||
let (list, dirty) = if is_image {
|
||||
(&mut self.images, &mut self.images_dirty)
|
||||
} else {
|
||||
(&mut self.order, &mut self.order_dirty)
|
||||
};
|
||||
list.push(slot);
|
||||
dirty.mark(list.len() - 1);
|
||||
list.len() - 1
|
||||
}
|
||||
|
||||
pub fn free(&mut self, pos: usize, is_image: bool) {
|
||||
self.updated = true;
|
||||
if is_image {
|
||||
self.image_free.push(pos);
|
||||
} else {
|
||||
self.free.push(pos);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_free(&mut self) -> Vec<OrderChange> {
|
||||
let mut changes = Vec::new();
|
||||
self.apply_free_into(&mut changes);
|
||||
changes
|
||||
}
|
||||
|
||||
pub(crate) fn apply_free_into(&mut self, changes: &mut Vec<OrderChange>) {
|
||||
Self::apply_free_list(
|
||||
&mut self.free,
|
||||
&mut self.order,
|
||||
&mut self.order_dirty,
|
||||
false,
|
||||
changes,
|
||||
);
|
||||
Self::apply_free_list(
|
||||
&mut self.image_free,
|
||||
&mut self.images,
|
||||
&mut self.images_dirty,
|
||||
true,
|
||||
changes,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn order_for_upload(&mut self) -> (&[u32], &mut Dirty) {
|
||||
(&self.order, &mut self.order_dirty)
|
||||
}
|
||||
|
||||
pub fn images_for_upload(&mut self) -> (&[u32], &mut Dirty) {
|
||||
(&self.images, &mut self.images_dirty)
|
||||
}
|
||||
|
||||
fn apply_free_list(
|
||||
free: &mut Vec<usize>,
|
||||
list: &mut Vec<u32>,
|
||||
dirty: &mut Dirty,
|
||||
is_image: bool,
|
||||
changes: &mut Vec<OrderChange>,
|
||||
) {
|
||||
free.sort_by(|a, b| b.cmp(a));
|
||||
for pos in free.drain(..) {
|
||||
list.swap_remove(pos);
|
||||
if pos != list.len() {
|
||||
dirty.mark(pos);
|
||||
changes.push(OrderChange {
|
||||
slot: list[pos],
|
||||
is_image,
|
||||
pos,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn order(&self) -> &Vec<u32> {
|
||||
&self.order
|
||||
}
|
||||
|
||||
pub fn images(&self) -> &Vec<u32> {
|
||||
&self.images
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OrderChange {
|
||||
pub slot: u32,
|
||||
pub is_image: bool,
|
||||
pub pos: usize,
|
||||
}
|
||||
|
||||
/// Whether a primitive goes into its layer's draw order. [`Drawn::No`] is
|
||||
/// a primitive written only to be *referenced* -- a mask's shape
|
||||
/// (LAYOUT.md's "Masks with a shape"). It is owned, moved, resized and
|
||||
/// freed exactly like any other; it is simply never rasterized.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Drawn {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
pub const NOT_DRAWN: usize = usize::MAX;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PrimitiveHandle {
|
||||
pub layer: usize,
|
||||
pub pos: usize,
|
||||
pub slot: u32,
|
||||
pub data_idx: usize,
|
||||
pub binding: u32,
|
||||
}
|
||||
|
||||
impl PrimitiveHandle {
|
||||
pub fn is_image(&self) -> bool {
|
||||
self.binding == IMAGE_BINDING
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PrimitiveInst<P> {
|
||||
pub id: WidgetId,
|
||||
pub primitive: P,
|
||||
pub region: UiRegion,
|
||||
pub mask_idx: MaskIdx,
|
||||
pub move_idx: MoveIdx,
|
||||
}
|
||||
|
||||
primitives!(
|
||||
rects: RectPrimitive => 0,
|
||||
glyphs: GlyphPrimitive => 2,
|
||||
);
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct RectPrimitive {
|
||||
/// Index into the separate paint buffer. Geometry stays untouched when a
|
||||
/// theme replaces the value at this index.
|
||||
pub paint: u32,
|
||||
pub radius: f32,
|
||||
pub thickness: f32,
|
||||
pub inner_radius: f32,
|
||||
}
|
||||
|
||||
impl RectPrimitive {
|
||||
pub fn color(paint: u32) -> Self {
|
||||
Self {
|
||||
paint,
|
||||
radius: 0.0,
|
||||
thickness: 0.0,
|
||||
inner_radius: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct GlyphPrimitive {
|
||||
pub uv_min: [f32; 2],
|
||||
pub uv_max: [f32; 2],
|
||||
/// Layer of the shared atlas array texture this glyph's page occupies --
|
||||
/// not a bind-group or view index, since a page never gets one of its
|
||||
/// own. See TEXTURES.md's "Recommended shape".
|
||||
pub layer: u32,
|
||||
pub paint: u32,
|
||||
pub flags: u32,
|
||||
_pad: u32,
|
||||
}
|
||||
|
||||
impl GlyphPrimitive {
|
||||
pub const IS_COLOR: u32 = 1;
|
||||
|
||||
pub fn new(uv_min: [f32; 2], uv_max: [f32; 2], layer: u32, paint: u32, flags: u32) -> Self {
|
||||
Self {
|
||||
uv_min,
|
||||
uv_max,
|
||||
layer,
|
||||
paint,
|
||||
flags,
|
||||
_pad: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PrimitiveVec<T> {
|
||||
vec: Vec<T>,
|
||||
free: Vec<usize>,
|
||||
/// Which entries have changed since the last upload. Every way to
|
||||
/// write one goes through [`Self::add`] or [`Self::set`], which is
|
||||
/// what keeps this in step -- there is deliberately no `DerefMut`,
|
||||
/// because an entry written through one would never be uploaded.
|
||||
pub dirty: Dirty,
|
||||
}
|
||||
|
||||
impl<T> PrimitiveVec<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
vec: Vec::new(),
|
||||
free: Vec::new(),
|
||||
dirty: Dirty::new_all(),
|
||||
}
|
||||
}
|
||||
pub fn add(&mut self, t: T) -> usize {
|
||||
let i = match self.free.pop() {
|
||||
Some(i) => {
|
||||
self.vec[i] = t;
|
||||
i
|
||||
}
|
||||
None => {
|
||||
self.vec.push(t);
|
||||
self.vec.len() - 1
|
||||
}
|
||||
};
|
||||
self.dirty.mark(i);
|
||||
i
|
||||
}
|
||||
pub fn set(&mut self, i: usize, t: T)
|
||||
where
|
||||
T: Pod,
|
||||
{
|
||||
if bytemuck::bytes_of(&self.vec[i]) == bytemuck::bytes_of(&t) {
|
||||
return;
|
||||
}
|
||||
self.vec[i] = t;
|
||||
self.dirty.mark(i);
|
||||
}
|
||||
pub fn free(&mut self, i: usize) {
|
||||
self.free.push(i);
|
||||
}
|
||||
/// The entries and the dirty set together, so an uploader can read one
|
||||
/// while clearing the other -- they are different fields, but a
|
||||
/// caller reaching for both through `Deref` cannot say so.
|
||||
pub fn for_upload(&mut self) -> (&[T], &mut Dirty) {
|
||||
(&self.vec, &mut self.dirty)
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.free.clear();
|
||||
self.vec.clear();
|
||||
self.dirty.mark_all();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for PrimitiveVec<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for PrimitiveVec<T> {
|
||||
type Target = Vec<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.vec
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{UiVec2, Widgets, util::Id};
|
||||
|
||||
#[test]
|
||||
fn an_instance_restored_before_upload_is_clean() {
|
||||
let mut primitives = Primitives::default();
|
||||
let mut widgets = Widgets::new();
|
||||
let widget = widgets.add_strong(());
|
||||
let owner = widget.id();
|
||||
let original = PrimitiveInstance {
|
||||
region: UiRegion::FULL,
|
||||
binding: RectPrimitive::BINDING,
|
||||
idx: 0,
|
||||
mask_idx: MaskIdx::NONE,
|
||||
move_idx: Id::preset(0),
|
||||
};
|
||||
primitives.push(original, owner);
|
||||
primitives.dirty.clear();
|
||||
|
||||
let mut moved = original;
|
||||
moved.region = moved.region.offset(UiVec2::abs((0.0, 20.0)));
|
||||
primitives.set_instance(0, moved, owner);
|
||||
assert!(!primitives.dirty.is_clean());
|
||||
|
||||
primitives.set_instance(0, original, owner);
|
||||
assert!(
|
||||
primitives.dirty.is_clean(),
|
||||
"the GPU never observes the provisional position"
|
||||
);
|
||||
|
||||
primitives.set_instance(0, moved, owner);
|
||||
assert!(!primitives.dirty.is_clean());
|
||||
primitives.dirty.clear();
|
||||
primitives.set_instance(0, original, owner);
|
||||
assert!(!primitives.dirty.is_clean());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
use image::{DynamicImage, EncodableLayout, GenericImageView};
|
||||
use wgpu::{util::DeviceExt, *};
|
||||
|
||||
use crate::{PatchRect, TextureKind, TextureUpdate, Textures};
|
||||
|
||||
use super::atlas::PAGE;
|
||||
|
||||
/// The fewest layers the glyph atlas array is ever created with. Two, not
|
||||
/// one, for the GLES reason written on `create_array_texture`.
|
||||
const MIN_ARRAY_LAYERS: u32 = 2;
|
||||
|
||||
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 {
|
||||
#[allow(dead_code)]
|
||||
texture: Texture,
|
||||
view: TextureView,
|
||||
bind_group: BindGroup,
|
||||
}
|
||||
|
||||
/// - **The glyph atlas**, one `texture_2d_array` whose layers are pages
|
||||
/// (`Slot::Page`), grown by recreating the array with headroom and
|
||||
/// `copy_texture_to_texture`-ing the old layers across. No feature beyond
|
||||
/// 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
|
||||
/// `BindGroup`, drawn one `draw()` call at a time with that bind group
|
||||
/// bound -- see `UiRenderNode::draw`.
|
||||
pub struct GpuTextures {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
|
||||
slots: Vec<Slot>,
|
||||
|
||||
array_texture: Texture,
|
||||
array_view: TextureView,
|
||||
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 {
|
||||
/// Applies queued `Textures` updates, then reports whether the *main*
|
||||
/// bind group (the one rects and glyphs draw with) needs rebuilding --
|
||||
/// true exactly when the atlas array was recreated (its view identity
|
||||
/// changed). Pushing or freeing a standalone image never touches that
|
||||
/// group: it built or drops its own. Masks/move_offsets resizing is
|
||||
/// `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 {
|
||||
let mut rebuild_main = false;
|
||||
for update in textures.updates() {
|
||||
match update {
|
||||
TextureUpdate::Push(kind, image) => {
|
||||
rebuild_main |= self.push(kind, image, rsc_layout);
|
||||
}
|
||||
TextureUpdate::Set(kind, i, image) => {
|
||||
rebuild_main |= self.set(kind, i, image, rsc_layout);
|
||||
}
|
||||
// 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::SetFree => {}
|
||||
TextureUpdate::Free(i) => self.free(i),
|
||||
TextureUpdate::PushFree(_kind) => self.slots.push(Slot::Empty),
|
||||
}
|
||||
}
|
||||
rebuild_main
|
||||
}
|
||||
|
||||
fn push(
|
||||
&mut self,
|
||||
kind: TextureKind,
|
||||
image: &DynamicImage,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
) -> bool {
|
||||
let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout);
|
||||
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 (width, height) = rgba.dimensions();
|
||||
let texture = self.device.create_texture_with_data(
|
||||
&self.queue,
|
||||
&TextureDescriptor {
|
||||
label: Some("image"),
|
||||
size: Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
// `image` and swash colour-glyph bytes are encoded sRGB.
|
||||
// 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,
|
||||
view_formats: &[],
|
||||
},
|
||||
wgt::TextureDataOrder::MipMajor,
|
||||
rgba.as_bytes(),
|
||||
);
|
||||
let view = texture.create_view(&TextureViewDescriptor::default());
|
||||
let bind_group = Self::make_image_bind_group(
|
||||
&self.device,
|
||||
rsc_layout,
|
||||
&self.array_view,
|
||||
&view,
|
||||
&self.sampler,
|
||||
);
|
||||
self.bind_group_creates += 1;
|
||||
ImageGpu {
|
||||
texture,
|
||||
view,
|
||||
bind_group,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_image_bind_group(
|
||||
device: &Device,
|
||||
rsc_layout: &BindGroupLayout,
|
||||
array_view: &TextureView,
|
||||
image_view: &TextureView,
|
||||
sampler: &Sampler,
|
||||
) -> BindGroup {
|
||||
device.create_bind_group(&BindGroupDescriptor {
|
||||
layout: rsc_layout,
|
||||
entries: &[
|
||||
BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: BindingResource::TextureView(array_view),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: BindingResource::TextureView(image_view),
|
||||
},
|
||||
BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: BindingResource::Sampler(sampler),
|
||||
},
|
||||
],
|
||||
label: Some("ui rsc image"),
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Slot::Empty => write!(f, "Empty"),
|
||||
Slot::Image(_) => write!(f, "Image"),
|
||||
Slot::Page(layer) => write!(f, "Page(layer={layer})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn null_texture_view(device: &Device) -> TextureView {
|
||||
device
|
||||
.create_texture(&TextureDescriptor {
|
||||
label: Some("null"),
|
||||
size: Extent3d {
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: TextureDimension::D2,
|
||||
format: TextureFormat::Rgba8UnormSrgb,
|
||||
usage: TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
})
|
||||
.create_view(&TextureViewDescriptor::default())
|
||||
}
|
||||
|
||||
pub fn default_sampler(device: &Device) -> Sampler {
|
||||
device.create_sampler(&SamplerDescriptor::default())
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use crate::util::Dirty;
|
||||
use bytemuck::Pod;
|
||||
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> {
|
||||
label: &'static str,
|
||||
usage: BufferUsages,
|
||||
pub buffer: Buffer,
|
||||
len: usize,
|
||||
capacity: usize,
|
||||
_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> {
|
||||
pub fn new(device: &Device, usage: BufferUsages, label: &'static str) -> Self {
|
||||
Self {
|
||||
label,
|
||||
usage,
|
||||
buffer: Self::init_buf(device, MIN_CAPACITY, usage, label),
|
||||
len: 0,
|
||||
capacity: MIN_CAPACITY,
|
||||
_pd: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reserve(&mut self, device: &Device, len: usize) -> bool {
|
||||
if len <= self.capacity {
|
||||
return false;
|
||||
}
|
||||
let mut capacity = self.capacity.max(MIN_CAPACITY);
|
||||
while capacity < len {
|
||||
capacity *= 2;
|
||||
}
|
||||
self.capacity = capacity;
|
||||
self.buffer = Self::init_buf(device, capacity, self.usage, self.label);
|
||||
true
|
||||
}
|
||||
|
||||
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 {
|
||||
label: Some(label),
|
||||
size,
|
||||
mapped_at_creation: false,
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
use crate::{
|
||||
LayerId, MaskIdx, MoveIdx, PaintId, PrimitiveHandle, Size, TextureHandle, UiRegion, WidgetId,
|
||||
util::Vec2,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ActiveData {
|
||||
pub id: WidgetId,
|
||||
pub region: UiRegion,
|
||||
pub parent: Option<WidgetId>,
|
||||
pub textures: Vec<TextureHandle>,
|
||||
pub(crate) spare_textures: Vec<TextureHandle>,
|
||||
/// Paint slots retained by this draw. The GPU primitive stores only the
|
||||
/// slot index, so these handles are what prevent a live primitive from
|
||||
/// observing a recycled paint.
|
||||
pub paints: Vec<PaintId>,
|
||||
pub(crate) spare_paints: Vec<PaintId>,
|
||||
pub primitives: Vec<PrimitiveHandle>,
|
||||
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 move_slot: MoveIdx,
|
||||
pub child_move_slot: Option<MoveIdx>,
|
||||
pub move_applied: Vec2,
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
use crate::{
|
||||
Mask, MoveOffset, Paints, TextResources, Textures, WeakWidget, WidgetId, Widgets,
|
||||
util::TrackedArena,
|
||||
};
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
ops::{Deref, DerefMut},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
mod access;
|
||||
mod active;
|
||||
mod painter;
|
||||
mod render_state;
|
||||
|
||||
pub use access::*;
|
||||
pub use active::*;
|
||||
pub use painter::{DrawResult, Painter};
|
||||
pub use render_state::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UiData {
|
||||
pub widgets: Widgets,
|
||||
pub paints: Paints,
|
||||
pub textures: Textures,
|
||||
pub text: Rc<RefCell<TextResources>>,
|
||||
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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Ui {
|
||||
data: UiData,
|
||||
pub(crate) render_state: RenderHandle,
|
||||
}
|
||||
|
||||
impl Ui {
|
||||
/// Register application-owned font data for a semantic or named family.
|
||||
/// Existing text resources are invalidated and their active widgets are
|
||||
/// scheduled for layout again.
|
||||
#[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.
|
||||
/// Fonts registered without this method share the default bucket.
|
||||
#[track_caller]
|
||||
pub fn register_font_in(
|
||||
&mut self,
|
||||
family: impl AsRef<str>,
|
||||
bucket: impl AsRef<str>,
|
||||
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
|
||||
/// bucket. Text is reshaped and the bucket's old glyph pages are released.
|
||||
#[track_caller]
|
||||
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
|
||||
/// a named glyph-atlas bucket.
|
||||
#[track_caller]
|
||||
pub fn replace_font_in(
|
||||
&mut self,
|
||||
family: impl AsRef<str>,
|
||||
bucket: impl AsRef<str>,
|
||||
data: impl AsRef<[u8]> + Send + Sync + 'static,
|
||||
) -> Result<(), crate::FontRegistrationError> {
|
||||
self.update_fonts(|text| text.replace_font_in(family, bucket, data))
|
||||
}
|
||||
|
||||
fn update_fonts(
|
||||
&mut self,
|
||||
update: impl FnOnce(&mut TextResources) -> Result<(), crate::FontRegistrationError>,
|
||||
) -> Result<(), crate::FontRegistrationError> {
|
||||
let owners = {
|
||||
let mut text = self.data.text.borrow_mut();
|
||||
update(&mut text)?;
|
||||
text.invalidate_all()
|
||||
};
|
||||
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 {
|
||||
self.data.text.borrow().is_font_registered(family)
|
||||
}
|
||||
|
||||
/// A read-only handle to the retained result of the last completed frame.
|
||||
/// The handle is owned so a caller may keep its read guard while mutating
|
||||
/// unrelated resources on the `Rsc` that owns this `Ui`.
|
||||
pub fn render_state(&self) -> RenderHandle {
|
||||
self.render_state.clone()
|
||||
}
|
||||
|
||||
pub fn resize(&self, size: impl Into<crate::util::Vec2>) {
|
||||
self.render_state.get_mut().resize(size);
|
||||
}
|
||||
|
||||
pub fn set_density(&mut self, density: f32) {
|
||||
self.data.text.borrow_mut().density = density;
|
||||
self.render_state.get_mut().set_density(density);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
fn ui(&self) -> &Ui;
|
||||
fn ui_mut(&mut self) -> &mut Ui;
|
||||
|
||||
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)]
|
||||
fn on_add(&mut self, id: WeakWidget) {}
|
||||
#[allow(unused_variables)]
|
||||
fn on_remove(&mut self, id: WidgetId) {}
|
||||
#[allow(unused_variables)]
|
||||
fn on_draw(&mut self, active: &ActiveData) {}
|
||||
#[allow(unused_variables)]
|
||||
fn on_undraw(&mut self, active: &ActiveData) {}
|
||||
|
||||
fn widgets(&self) -> &Widgets {
|
||||
&self.ui().widgets
|
||||
}
|
||||
fn widgets_mut(&mut self) -> &mut Widgets {
|
||||
&mut self.ui_mut().widgets
|
||||
}
|
||||
fn free(&mut self) {
|
||||
while let Some(id) = self.widgets_mut().free_next() {
|
||||
self.on_remove(id);
|
||||
}
|
||||
self.ui_mut().text.borrow_mut().free_released();
|
||||
self.ui_mut().textures.free();
|
||||
self.ui_mut().paints.free_released();
|
||||
}
|
||||
}
|
||||
@@ -1,553 +0,0 @@
|
||||
use crate::{
|
||||
Axis, LayoutLen, MoveOffset, PaintId, RegionAlign, RenderedText, Size, StrongWidget,
|
||||
TextHandle, TextResources, TextureHandle, UiData, UiRegion, UiRenderState, UiRsc, UiScalar,
|
||||
UiVec2, WidgetId,
|
||||
render::{
|
||||
Drawn, GlyphPrimitive, IMAGE_BINDING, Mask, MaskIdx, MoveIdx, NOT_DRAWN, Primitive,
|
||||
PrimitiveHandle, PrimitiveInst, RectPrimitive,
|
||||
},
|
||||
ui::render_state::Retained,
|
||||
util::Vec2,
|
||||
};
|
||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||
|
||||
pub struct Painter<'a> {
|
||||
pub(super) render_state: &'a mut UiRenderState,
|
||||
pub(super) rsc: &'a mut dyn UiRsc,
|
||||
|
||||
pub(super) region: UiRegion,
|
||||
pub(super) mask: MaskIdx,
|
||||
pub(super) move_slot: MoveIdx,
|
||||
pub(super) child_move_slot: Option<MoveIdx>,
|
||||
pub(super) own_mask: MaskIdx,
|
||||
pub(super) textures: Vec<TextureHandle>,
|
||||
pub(super) recycle_textures: Vec<TextureHandle>,
|
||||
pub(super) paints: Vec<PaintId>,
|
||||
pub(super) recycle_paints: Vec<PaintId>,
|
||||
pub(super) primitives: Vec<PrimitiveHandle>,
|
||||
pub(super) recycle: Vec<PrimitiveHandle>,
|
||||
pub(super) recycle_at: usize,
|
||||
pub(super) children: Vec<WidgetId>,
|
||||
pub(super) size_dependencies: Vec<WidgetId>,
|
||||
pub(super) size: Option<Size>,
|
||||
/// Whether a retained child's length on each axis is still valid. A
|
||||
/// child's length may change when the parent's orthogonal extent changes
|
||||
/// (most importantly, wrapped text gets taller when it gets narrower),
|
||||
/// but not merely because a content-sized parent grew along that same
|
||||
/// axis around one of its siblings.
|
||||
pub(super) reuse_child_sizes: [bool; 2],
|
||||
pub layer: usize,
|
||||
pub(super) id: WidgetId,
|
||||
}
|
||||
|
||||
pub struct DrawResult<'p, 'a> {
|
||||
painter: &'p mut Painter<'a>,
|
||||
child: WidgetId,
|
||||
}
|
||||
|
||||
impl DrawResult<'_, '_> {
|
||||
pub fn size(self) -> Size {
|
||||
if !self.painter.size_dependencies.contains(&self.child) {
|
||||
self.painter.size_dependencies.push(self.child);
|
||||
}
|
||||
self.painter.render_state.active[&self.child].size
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Painter<'a> {
|
||||
/// The presentation time of this frame, supplied by the platform. On a
|
||||
/// backend with a display clock this is the vsync timestamp, not the time
|
||||
/// at which this widget happened to be drawn.
|
||||
pub fn frame_time(&self) -> Instant {
|
||||
self.render_state.frame_time
|
||||
}
|
||||
|
||||
/// Redraw this widget on the following frame and ask the platform to
|
||||
/// produce that frame. The invalidation is staged until the current
|
||||
/// retained redraw has finished, so it cannot be consumed again in this
|
||||
/// frame.
|
||||
pub fn request_next_frame(&mut self) {
|
||||
self.render_state.next_frame.insert(self.id);
|
||||
}
|
||||
|
||||
pub fn set_size(&mut self, size: Size) {
|
||||
assert!(
|
||||
self.size.replace(size).is_none(),
|
||||
"a widget set its size more than once during one draw"
|
||||
);
|
||||
}
|
||||
|
||||
fn primitive_at<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||
self.write_primitive(primitive, region, Drawn::Yes);
|
||||
}
|
||||
|
||||
/// **Consumed strictly in order, and one mismatch ends recycling for
|
||||
/// the rest of the draw.** A widget's `draw` is a function of its own
|
||||
/// state, so a redraw writes the same sequence of primitives in the
|
||||
/// same order in the overwhelmingly common case; searching the
|
||||
/// remainder for a match would turn an O(1) step into an O(primitives)
|
||||
/// one to rescue a case that means the widget's content changed shape
|
||||
/// anyway. Stopping is also what keeps the invariant simple: every
|
||||
/// handle from `recycled` on is untouched and gets freed together.
|
||||
fn take_recycled(&mut self, binding: u32, drawn: Drawn) -> Option<PrimitiveHandle> {
|
||||
let h = *self.recycle.get(self.recycle_at)?;
|
||||
let drawn_matches = (h.pos == NOT_DRAWN) == (drawn == Drawn::No);
|
||||
if h.binding != binding || h.layer != self.layer || !drawn_matches {
|
||||
return None;
|
||||
}
|
||||
self.recycle_at += 1;
|
||||
Some(h)
|
||||
}
|
||||
|
||||
fn write_primitive<P: Primitive>(
|
||||
&mut self,
|
||||
primitive: P,
|
||||
region: UiRegion,
|
||||
drawn: Drawn,
|
||||
) -> u32 {
|
||||
let inst = PrimitiveInst {
|
||||
id: self.id,
|
||||
primitive,
|
||||
region,
|
||||
mask_idx: self.mask,
|
||||
move_idx: self.move_slot,
|
||||
};
|
||||
let h = match self.take_recycled(P::BINDING, drawn) {
|
||||
Some(h) => {
|
||||
self.render_state.primitives.recycle(&h, inst);
|
||||
h
|
||||
}
|
||||
None => self.render_state.write_primitive(self.layer, drawn, inst),
|
||||
};
|
||||
if self.mask != MaskIdx::NONE {
|
||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
||||
}
|
||||
let slot = h.slot;
|
||||
self.own(h);
|
||||
slot
|
||||
}
|
||||
|
||||
/// Take ownership of a handle this widget just wrote.
|
||||
fn own(&mut self, h: PrimitiveHandle) {
|
||||
self.render_state
|
||||
.primitives
|
||||
.set_handle_index(h.slot, self.primitives.len() as u32);
|
||||
self.primitives.push(h);
|
||||
}
|
||||
|
||||
pub fn primitive<P: Primitive>(&mut self, primitive: P) {
|
||||
self.primitive_at(primitive, self.region)
|
||||
}
|
||||
|
||||
/// Resolves a public paint handle to the compact index stored by a GPU
|
||||
/// primitive and retains the handle for exactly as long as that draw.
|
||||
pub fn paint(&mut self, paint: &PaintId) -> u32 {
|
||||
if !self.paints.contains(paint) {
|
||||
if let Some(i) = self.recycle_paints.iter().position(|old| old == paint) {
|
||||
self.paints.push(self.recycle_paints.swap_remove(i));
|
||||
} else {
|
||||
self.paints.push(paint.clone());
|
||||
}
|
||||
}
|
||||
paint.slot()
|
||||
}
|
||||
|
||||
pub fn paint_value(&mut self, paint: &mut crate::PaintValue) -> u32 {
|
||||
let paint = paint.resolve(&mut self.rsc.ui_mut().paints);
|
||||
self.paint(paint)
|
||||
}
|
||||
|
||||
pub fn primitive_within<P: Primitive>(&mut self, primitive: P, region: UiRegion) {
|
||||
self.primitive_at(primitive, region.within(&self.region));
|
||||
}
|
||||
|
||||
/// The slot is allocated once and **rewritten in place** on every
|
||||
/// later draw rather than pushed again, because a descendant whose own
|
||||
/// region did not change is not redrawn (`draw_inner`'s fast path) and
|
||||
/// so keeps pointing at whichever slot it was drawn under. See
|
||||
/// `ActiveData::own_mask` for what pushing a fresh one cost.
|
||||
pub fn set_mask(&mut self, region: UiRegion) {
|
||||
let paint = self.paint(&PaintId::NONE);
|
||||
let shape = self.write_primitive(RectPrimitive::color(paint), region, Drawn::No);
|
||||
self.set_mask_to(shape);
|
||||
}
|
||||
|
||||
/// Clip everything this widget draws after this call to `shape`'s
|
||||
/// own shape -- the first primitive `shape`'s subtree drew, which
|
||||
/// must already have been drawn this frame
|
||||
/// (`UiRenderState::first_primitive`). What `.masked_by()` uses to
|
||||
/// clip a container's content to the rounded background it draws,
|
||||
/// with no radius argument anywhere that could fall out of step with
|
||||
/// the one being drawn.
|
||||
pub fn set_mask_to_widget<W: ?Sized>(&mut self, shape: &StrongWidget<W>) {
|
||||
let slot = self.render_state.first_primitive(shape.id()).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"'{}' was given as a mask's shape but drew no primitive, so there is nothing to \
|
||||
clip to",
|
||||
self.rsc.widgets().label(shape.id()),
|
||||
)
|
||||
});
|
||||
self.set_mask_to(slot);
|
||||
}
|
||||
|
||||
fn set_mask_to(&mut self, shape: u32) {
|
||||
assert!(
|
||||
self.own_mask == MaskIdx::NONE || self.mask != self.own_mask,
|
||||
"set_mask called twice while drawing one widget: the second would replace the first \
|
||||
rather than nest inside it",
|
||||
);
|
||||
let binding = self.render_state.primitives.instance(shape).binding;
|
||||
assert_eq!(
|
||||
binding,
|
||||
RectPrimitive::BINDING,
|
||||
"a mask's shape must be a rect primitive; primitive {shape} is binding {binding}",
|
||||
);
|
||||
let parent = self.mask;
|
||||
let mask = Mask {
|
||||
primitive: shape,
|
||||
parent,
|
||||
};
|
||||
let old_parent = if self.own_mask == MaskIdx::NONE {
|
||||
let slot = self.rsc.ui_mut().masks.push(mask);
|
||||
self.rsc.ui_mut().masks.push_ref(slot);
|
||||
self.own_mask = slot;
|
||||
MaskIdx::NONE
|
||||
} else {
|
||||
let old = self.rsc.ui().masks[self.own_mask.idx()].parent;
|
||||
*self.rsc.ui_mut().masks.get_mut(self.own_mask) = mask;
|
||||
old
|
||||
};
|
||||
if old_parent != parent {
|
||||
if parent != MaskIdx::NONE {
|
||||
self.rsc.ui_mut().masks.push_ref(parent);
|
||||
}
|
||||
if old_parent != MaskIdx::NONE {
|
||||
self.rsc.ui_mut().masks.remove(old_parent);
|
||||
}
|
||||
}
|
||||
self.mask = self.own_mask;
|
||||
}
|
||||
|
||||
pub fn widget<'p, W: ?Sized>(&'p mut self, id: &StrongWidget<W>) -> DrawResult<'p, 'a> {
|
||||
self.widget_at(id, self.region)
|
||||
}
|
||||
|
||||
pub fn widget_within<'p, W: ?Sized>(
|
||||
&'p mut self,
|
||||
id: &StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
) -> DrawResult<'p, 'a> {
|
||||
self.widget_at(id, region.within(&self.region))
|
||||
}
|
||||
|
||||
/// Translate this widget's children as one retained subtree, in output
|
||||
/// pixels. The first call must happen before drawing a child, because the
|
||||
/// slot becomes the parent of every direct child's ordinary move slot.
|
||||
/// Once retained, it may be updated later in a redraw (for example after
|
||||
/// measuring a changed child). All deeper descendants inherit it and the
|
||||
/// CPU hit-test walk resolves the same translation as the shader.
|
||||
pub fn set_child_offset(&mut self, offset: Vec2) {
|
||||
let slot = match self.child_move_slot {
|
||||
Some(slot) => slot,
|
||||
None => {
|
||||
assert!(
|
||||
self.children.is_empty(),
|
||||
"a child offset must be created before drawing a child"
|
||||
);
|
||||
let parent = self.move_slot.idx() as u32;
|
||||
let slot = self
|
||||
.rsc
|
||||
.ui_mut()
|
||||
.move_offsets
|
||||
.push(MoveOffset::new([offset.x, offset.y], parent));
|
||||
// One ref for this widget's ownership and one on the
|
||||
// up-link. Direct children take their own refs when their
|
||||
// move slots are allocated.
|
||||
self.rsc.ui_mut().move_offsets.push_ref(slot);
|
||||
self.rsc.ui_mut().move_offsets.push_ref(self.move_slot);
|
||||
self.child_move_slot = Some(slot);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let next = [offset.x, offset.y];
|
||||
if self.rsc.ui().move_offsets[slot.idx()].delta != next {
|
||||
self.rsc.ui_mut().move_offsets.get_mut(slot).delta = next;
|
||||
self.render_state.note_move();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn known_len<W: ?Sized>(&mut self, id: &StrongWidget<W>, axis: Axis) -> Option<LayoutLen> {
|
||||
let len = if let Some(len) = self.rsc.widgets().get_dyn(id.id())?.size_hint(axis) {
|
||||
Some(len.fold_dp(self.density()))
|
||||
} else if !self.reuse_child_sizes[match axis {
|
||||
Axis::X => 0,
|
||||
Axis::Y => 1,
|
||||
}] || self.rsc.widgets().needs_redraw.contains(&id.id())
|
||||
{
|
||||
None
|
||||
} else {
|
||||
self.render_state
|
||||
.active
|
||||
.get(&id.id())
|
||||
.map(|a| a.size.axis(axis))
|
||||
};
|
||||
if len.is_some() && !self.size_dependencies.contains(&id.id()) {
|
||||
self.size_dependencies.push(id.id());
|
||||
}
|
||||
len
|
||||
}
|
||||
|
||||
fn widget_at<'p, W: ?Sized>(
|
||||
&'p mut self,
|
||||
id: &StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
) -> DrawResult<'p, 'a> {
|
||||
self.children.push(id.id());
|
||||
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
|
||||
self.render_state.draw_inner(
|
||||
self.layer,
|
||||
id.id(),
|
||||
region,
|
||||
Some(self.id),
|
||||
parent_move_slot.idx() as u32,
|
||||
self.mask,
|
||||
Retained::default(),
|
||||
self.rsc,
|
||||
);
|
||||
DrawResult {
|
||||
painter: self,
|
||||
child: id.id(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn place<'p, W: ?Sized>(
|
||||
&'p mut self,
|
||||
id: &StrongWidget<W>,
|
||||
region: UiRegion,
|
||||
) -> DrawResult<'p, 'a> {
|
||||
let region = region.within(&self.region);
|
||||
let retained = self
|
||||
.render_state
|
||||
.active
|
||||
.get(&id.id())
|
||||
.map(|active| (active.layer, active.mask));
|
||||
if self.render_state.place(id.id(), region, self.rsc).is_some() {
|
||||
} else if let Some((layer, mask)) = retained {
|
||||
self.children.push(id.id());
|
||||
self.rsc.widgets_mut().needs_redraw.insert(id.id());
|
||||
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
|
||||
self.render_state.draw_inner(
|
||||
layer,
|
||||
id.id(),
|
||||
region,
|
||||
Some(self.id),
|
||||
parent_move_slot.idx() as u32,
|
||||
mask,
|
||||
Retained::default(),
|
||||
self.rsc,
|
||||
);
|
||||
} else {
|
||||
self.children.push(id.id());
|
||||
let parent_move_slot = self.child_move_slot.unwrap_or(self.move_slot);
|
||||
self.render_state.draw_inner(
|
||||
self.layer,
|
||||
id.id(),
|
||||
region,
|
||||
Some(self.id),
|
||||
parent_move_slot.idx() as u32,
|
||||
self.mask,
|
||||
Retained::default(),
|
||||
self.rsc,
|
||||
);
|
||||
}
|
||||
DrawResult {
|
||||
painter: self,
|
||||
child: id.id(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn place_used<W: ?Sized>(
|
||||
&mut self,
|
||||
id: &StrongWidget<W>,
|
||||
used: Size,
|
||||
within: UiRegion,
|
||||
) -> DrawResult<'_, 'a> {
|
||||
let region = self.fit_region(used, within);
|
||||
self.place(id, region)
|
||||
}
|
||||
|
||||
pub fn fit_region(&mut self, used: Size, mut within: UiRegion) -> UiRegion {
|
||||
let mut region = used
|
||||
.to_uivec2(self.density())
|
||||
.align(RegionAlign::TOP_LEFT)
|
||||
.within(&within);
|
||||
let output = self.output_size();
|
||||
for axis in [Axis::X, Axis::Y] {
|
||||
let mut actual = region.within(&self.region);
|
||||
let mut available = within.within(&self.region);
|
||||
if actual.axis(axis).len().to_abs(output.axis(axis))
|
||||
> available.axis(axis).len().to_abs(output.axis(axis))
|
||||
{
|
||||
*region.axis_mut(axis) = *within.axis(axis);
|
||||
}
|
||||
}
|
||||
region
|
||||
}
|
||||
|
||||
pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||
self.retain_texture(handle);
|
||||
self.write_image(handle.image_index(), region.within(&self.region));
|
||||
}
|
||||
|
||||
pub fn texture(&mut self, handle: &TextureHandle) {
|
||||
self.retain_texture(handle);
|
||||
self.write_image(handle.image_index(), self.region);
|
||||
}
|
||||
|
||||
pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) {
|
||||
self.retain_texture(handle);
|
||||
self.write_image(handle.image_index(), region);
|
||||
}
|
||||
|
||||
fn retain_texture(&mut self, handle: &TextureHandle) {
|
||||
if self.textures.contains(handle) {
|
||||
return;
|
||||
}
|
||||
if let Some(i) = self.recycle_textures.iter().position(|old| old == handle) {
|
||||
self.textures.push(self.recycle_textures.swap_remove(i));
|
||||
} else {
|
||||
self.textures.push(handle.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn write_image(&mut self, texture_idx: u32, region: UiRegion) {
|
||||
let h = match self.take_recycled(IMAGE_BINDING, Drawn::Yes) {
|
||||
Some(h) => {
|
||||
self.render_state.primitives.recycle_image(
|
||||
&h,
|
||||
self.id,
|
||||
texture_idx,
|
||||
region,
|
||||
self.mask,
|
||||
self.move_slot,
|
||||
);
|
||||
h
|
||||
}
|
||||
None => self.render_state.write_image(
|
||||
self.layer,
|
||||
self.id,
|
||||
texture_idx,
|
||||
region,
|
||||
self.mask,
|
||||
self.move_slot,
|
||||
),
|
||||
};
|
||||
if self.mask != MaskIdx::NONE {
|
||||
self.rsc.ui_mut().masks.push_ref(self.mask);
|
||||
}
|
||||
self.own(h);
|
||||
}
|
||||
|
||||
pub fn render_text(&mut self, text: &TextHandle, width: Option<f32>) -> RenderedText {
|
||||
let density = self.render_state.density;
|
||||
let ui: &mut UiData = self.rsc.ui_mut();
|
||||
let (rendered, prepared) = text.render(width, self.id, &mut ui.textures, density);
|
||||
self.render_state.shape_count += u64::from(prepared);
|
||||
rendered
|
||||
}
|
||||
|
||||
pub fn render_ellipsis(&mut self, text: &TextHandle) -> RenderedText {
|
||||
let density = self.render_state.density;
|
||||
let ui: &mut UiData = self.rsc.ui_mut();
|
||||
let (rendered, prepared) = text.render_ellipsis(self.id, &mut ui.textures, density);
|
||||
self.render_state.shape_count += u64::from(prepared);
|
||||
rendered
|
||||
}
|
||||
|
||||
fn atlas_generation(&self) -> u64 {
|
||||
self.rsc.ui().text.borrow().atlas.generation()
|
||||
}
|
||||
|
||||
pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) {
|
||||
// A caller re-emitting quads placed against an atlas that has since
|
||||
// been cleared draws every glyph from coordinates now holding
|
||||
// something else. Caught at the submission rather than on screen,
|
||||
// where it reads as fragments of unrelated letters. `assert_eq!`
|
||||
// for R1's reason: two integers per laid-out string, not per
|
||||
// glyph, and the failure is unreadable text on a release build.
|
||||
assert_eq!(
|
||||
text.generation,
|
||||
self.atlas_generation(),
|
||||
"glyphs placed against atlas generation {} submitted against {}: the holder did not \
|
||||
re-render after the atlas was cleared",
|
||||
text.generation,
|
||||
self.atlas_generation(),
|
||||
);
|
||||
let flags_for = |is_color| {
|
||||
if is_color {
|
||||
GlyphPrimitive::IS_COLOR
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
for paint in text.paints.iter() {
|
||||
self.paint(paint);
|
||||
}
|
||||
for glyph in text.glyphs.iter() {
|
||||
let mut region = origin;
|
||||
region.x.end = region.x.start;
|
||||
region.y.end = region.y.start;
|
||||
let mut region = region.offset(UiVec2::abs(glyph.offset));
|
||||
region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32);
|
||||
region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32);
|
||||
self.primitive_at(
|
||||
GlyphPrimitive::new(
|
||||
glyph.entry.uv_min,
|
||||
glyph.entry.uv_max,
|
||||
glyph.entry.layer,
|
||||
glyph.paint,
|
||||
flags_for(glyph.entry.is_color),
|
||||
),
|
||||
region,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn region(&self) -> UiRegion {
|
||||
self.region
|
||||
}
|
||||
|
||||
pub fn output_size(&self) -> Vec2 {
|
||||
self.render_state.output_size
|
||||
}
|
||||
|
||||
/// Physical pixels per `dp` -- see `UiRenderState::density`'s field
|
||||
/// doc. What `LayoutLen::dp`'s `apply_rest` call resolves against.
|
||||
pub fn density(&self) -> f32 {
|
||||
self.render_state.density
|
||||
}
|
||||
|
||||
pub fn px_size(&mut self) -> Vec2 {
|
||||
self.region.size().to_abs(self.render_state.output_size)
|
||||
}
|
||||
|
||||
pub fn text_resources(&mut self) -> Rc<RefCell<TextResources>> {
|
||||
self.rsc.ui().text.clone()
|
||||
}
|
||||
|
||||
pub fn child_layer(&mut self) {
|
||||
self.layer = self.render_state.layers.child(self.layer);
|
||||
}
|
||||
|
||||
pub fn next_layer(&mut self) {
|
||||
self.layer = self.render_state.layers.next(self.layer);
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &str {
|
||||
&self.rsc.widgets().data(self.id).unwrap().label
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &WidgetId {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,117 +0,0 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::util::{Dirty, Id, IdNum, IdTracker};
|
||||
|
||||
pub struct Arena<T, I> {
|
||||
data: Vec<T>,
|
||||
tracker: IdTracker<I>,
|
||||
}
|
||||
|
||||
impl<T, I: IdNum> Arena<T, I> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Vec::new(),
|
||||
tracker: IdTracker::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, value: T) -> Id<I> {
|
||||
let id = self.tracker.next();
|
||||
let i = id.idx();
|
||||
if i == self.data.len() {
|
||||
self.data.push(value);
|
||||
} else {
|
||||
self.data[i] = value;
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: Id<I>) -> T
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
let i = id.idx();
|
||||
self.tracker.free(id);
|
||||
self.data[i]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, I: IdNum> Default for Arena<T, I> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TrackedArena<T, I> {
|
||||
inner: Arena<T, I>,
|
||||
refs: Vec<u32>,
|
||||
pub dirty: Dirty,
|
||||
}
|
||||
|
||||
impl<T, I: IdNum> TrackedArena<T, I> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arena::default(),
|
||||
refs: Vec::new(),
|
||||
dirty: Dirty::new_all(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, value: T) -> Id<I> {
|
||||
let id = self.inner.push(value);
|
||||
let i = id.idx();
|
||||
self.dirty.mark(i);
|
||||
if i == self.refs.len() {
|
||||
self.refs.push(0);
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
pub fn push_ref(&mut self, i: Id<I>) {
|
||||
self.refs[i.idx()] += 1;
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, id: Id<I>) -> &mut T {
|
||||
self.dirty.mark(id.idx());
|
||||
&mut self.inner.data[id.idx()]
|
||||
}
|
||||
|
||||
pub fn for_upload(&mut self) -> (&[T], &mut Dirty) {
|
||||
(&self.inner.data, &mut self.dirty)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: Id<I>) -> T
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
let i = id.idx();
|
||||
self.refs[i] -= 1;
|
||||
if self.refs[i] == 0 {
|
||||
self.inner.remove(id)
|
||||
} else {
|
||||
self[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, I: IdNum> Default for TrackedArena<T, I> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, I> Deref for TrackedArena<T, I> {
|
||||
type Target = Vec<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, I> Deref for Arena<T, I> {
|
||||
type Target = Vec<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.data
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub struct DynBorrower<'a, T: ?Sized> {
|
||||
data: &'a mut T,
|
||||
borrowed: &'a mut bool,
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized> DynBorrower<'a, T> {
|
||||
pub fn new(data: &'a mut T, borrowed: &'a mut bool) -> Self {
|
||||
if *borrowed {
|
||||
panic!("tried to mutably borrow the same thing twice");
|
||||
}
|
||||
Self { data, borrowed }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Drop for DynBorrower<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
*self.borrowed = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Deref for DynBorrower<'_, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.data
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> DerefMut for DynBorrower<'_, T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.data
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
pub struct MutDetect<T> {
|
||||
inner: T,
|
||||
pub changed: bool,
|
||||
}
|
||||
|
||||
impl<T> Deref for MutDetect<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for MutDetect<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.changed = true;
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for MutDetect<T> {
|
||||
fn from(inner: T) -> Self {
|
||||
MutDetect {
|
||||
inner,
|
||||
changed: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
#[repr(C)]
|
||||
#[derive(Eq, Hash, PartialEq, Debug, Clone, Copy, bytemuck::Zeroable)]
|
||||
pub struct Id<I = u64>(I);
|
||||
|
||||
unsafe impl<I: Copy + bytemuck::Zeroable + 'static> bytemuck::Pod for Id<I> {}
|
||||
|
||||
pub struct IdTracker<I = u64> {
|
||||
free: Vec<Id<I>>,
|
||||
cur: Id<I>,
|
||||
}
|
||||
|
||||
impl<I: IdNum> IdTracker<I> {
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> Id<I> {
|
||||
if let Some(id) = self.free.pop() {
|
||||
return id;
|
||||
}
|
||||
let next = self.cur.next();
|
||||
std::mem::replace(&mut self.cur, next)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn free(&mut self, id: Id<I>) {
|
||||
self.free.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: IdNum> Id<I> {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn raw(id: I) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
pub fn idx(&self) -> usize {
|
||||
self.0.idx()
|
||||
}
|
||||
pub fn next(&self) -> Id<I> {
|
||||
Self(self.0.next())
|
||||
}
|
||||
pub const fn preset(value: I) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: IdNum> Default for IdTracker<I> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
free: Vec::new(),
|
||||
cur: Id(I::first()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IdNum {
|
||||
fn first() -> Self;
|
||||
fn next(&self) -> Self;
|
||||
fn idx(&self) -> usize;
|
||||
}
|
||||
|
||||
impl IdNum for u64 {
|
||||
fn first() -> Self {
|
||||
0
|
||||
}
|
||||
|
||||
fn next(&self) -> Self {
|
||||
self + 1
|
||||
}
|
||||
|
||||
fn idx(&self) -> usize {
|
||||
*self as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl IdNum for u32 {
|
||||
fn first() -> Self {
|
||||
0
|
||||
}
|
||||
|
||||
fn next(&self) -> Self {
|
||||
self + 1
|
||||
}
|
||||
|
||||
fn idx(&self) -> usize {
|
||||
*self as usize
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
use std::ops::*;
|
||||
|
||||
pub const trait LerpUtil {
|
||||
fn lerp(self, from: Self, to: Self) -> Self;
|
||||
fn lerp_inv(self, from: Self, to: Self) -> Self;
|
||||
}
|
||||
|
||||
pub const trait DivOr {
|
||||
fn div_or(self, rhs: Self, other: Self) -> Self;
|
||||
}
|
||||
|
||||
const impl DivOr for f32 {
|
||||
fn div_or(self, rhs: Self, other: Self) -> Self {
|
||||
let res = self / rhs;
|
||||
if res.is_nan() { other } else { res }
|
||||
}
|
||||
}
|
||||
|
||||
const impl<
|
||||
T: const Add<Output = T> + const Sub<Output = T> + const Mul<Output = T> + const DivOr + Copy,
|
||||
> LerpUtil for T
|
||||
{
|
||||
fn lerp(self, from: Self, to: Self) -> 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 {
|
||||
($T:ident $op:ident $fn:ident $opa:ident $fna:ident; $($field:ident)*) => {
|
||||
#[allow(non_snake_case)]
|
||||
mod ${concat($T, _op_, $fn, _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);
|
||||
}
|
||||
}
|
||||
const impl $op<f32> for $T {
|
||||
type Output = Self;
|
||||
|
||||
fn $fn(self, rhs: f32) -> Self::Output {
|
||||
Self {
|
||||
$($field: self.$field.$fn(rhs),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
const impl $op<$T> for f32 {
|
||||
type Output = $T;
|
||||
|
||||
fn $fn(self, rhs: $T) -> Self::Output {
|
||||
$T {
|
||||
$($field: self.$fn(rhs.$field),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
const impl $opa<f32> for $T {
|
||||
fn $fna(&mut self, rhs: f32) {
|
||||
*self = self.$fn(rhs);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
($T:ident $op:ident $fn:ident; $($field:ident)*) => {
|
||||
impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
||||
};
|
||||
(impl $op:ident for $T:ident: $fn:ident $($field:ident)*) => {
|
||||
impl_op!($T $op $fn ${concat($op,Assign)} ${concat($fn,_assign)}; $($field)*);
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use impl_op;
|
||||
@@ -1,28 +0,0 @@
|
||||
mod arena;
|
||||
mod borrow;
|
||||
mod change;
|
||||
mod dirty;
|
||||
mod id;
|
||||
mod math;
|
||||
mod refcount;
|
||||
mod resources;
|
||||
mod slot;
|
||||
mod trust;
|
||||
mod typemap;
|
||||
mod vec2;
|
||||
|
||||
pub use arena::*;
|
||||
pub use borrow::*;
|
||||
pub use change::*;
|
||||
pub use dirty::*;
|
||||
pub use id::*;
|
||||
pub use math::*;
|
||||
pub use refcount::*;
|
||||
pub use resources::*;
|
||||
pub use slot::*;
|
||||
pub use trust::*;
|
||||
pub use typemap::*;
|
||||
pub use vec2::*;
|
||||
|
||||
pub type HashMap<K, V> = fxhash::FxHashMap<K, V>;
|
||||
pub type HashSet<K> = fxhash::FxHashSet<K>;
|
||||
@@ -1,36 +0,0 @@
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RefCounter(Arc<AtomicU32>);
|
||||
|
||||
impl RefCounter {
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(0.into()))
|
||||
}
|
||||
pub fn refs(&self) -> u32 {
|
||||
self.0.load(Ordering::Acquire)
|
||||
}
|
||||
pub fn drop(&mut self) -> bool {
|
||||
let refs = self.0.fetch_sub(1, Ordering::Release);
|
||||
refs == 0
|
||||
}
|
||||
pub fn quiet_clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RefCounter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RefCounter {
|
||||
fn clone(&self) -> Self {
|
||||
self.0.fetch_add(1, Ordering::Release);
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct SlotId {
|
||||
idx: 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> {
|
||||
data: Vec<(u32, Option<T>)>,
|
||||
free: Vec<u32>,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl<T> SlotVec<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: Default::default(),
|
||||
free: Default::default(),
|
||||
len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, x: T) -> SlotId {
|
||||
let id = if let Some(idx) = self.free.pop() {
|
||||
let (genr, data) = &mut self.data[idx as usize];
|
||||
*data = Some(x);
|
||||
SlotId { idx, genr: *genr }
|
||||
} else {
|
||||
let idx = self.data.len() as u32;
|
||||
let genr = 0;
|
||||
self.data.push((genr, Some(x)));
|
||||
SlotId { idx, genr }
|
||||
};
|
||||
self.len += 1;
|
||||
id
|
||||
}
|
||||
|
||||
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];
|
||||
if *genr != id.genr {
|
||||
return None;
|
||||
}
|
||||
*genr += 1;
|
||||
let value = data.take()?;
|
||||
self.len -= 1;
|
||||
if recycle {
|
||||
self.free.push(id.idx);
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
|
||||
pub fn get(&self, id: SlotId) -> Option<&T> {
|
||||
let slot = &self.data[id.idx as usize];
|
||||
if slot.0 != id.genr {
|
||||
return None;
|
||||
}
|
||||
slot.1.as_ref()
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, id: SlotId) -> Option<&mut T> {
|
||||
let slot = &mut self.data[id.idx as usize];
|
||||
if slot.0 != id.genr {
|
||||
return None;
|
||||
}
|
||||
slot.1.as_mut()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
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> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#[allow(clippy::missing_safety_doc)]
|
||||
pub unsafe fn forget_ref<'a, T>(x: &T) -> &'a 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) }
|
||||
}
|
||||
|
||||
#[allow(clippy::mut_from_ref, clippy::missing_safety_doc)]
|
||||
pub unsafe fn to_mut<T>(x: &T) -> &mut T {
|
||||
#[allow(mutable_transmutes)]
|
||||
unsafe {
|
||||
std::mem::transmute::<&T, &mut T>(x)
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
use crate::util::HashMap;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
marker::Unsize,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
pub struct TypeMap<Trait: ?Sized> {
|
||||
map: HashMap<TypeId, Box<Trait>>,
|
||||
}
|
||||
|
||||
impl<Trait: ?Sized> TypeMap<Trait> {
|
||||
pub fn set_type<T: Unsize<Trait> + 'static>(&mut self, val: T) {
|
||||
self.map
|
||||
.insert(TypeId::of::<T>(), Box::new(val) as Box<Trait>);
|
||||
}
|
||||
|
||||
pub fn type_mut<T: Unsize<Trait> + 'static>(&mut self) -> Option<&mut T> {
|
||||
Some(Self::convert_mut(self.map.get_mut(&TypeId::of::<T>())?))
|
||||
}
|
||||
|
||||
pub fn type_or_default<T: Default + Unsize<Trait> + 'static>(&mut self) -> &mut T {
|
||||
Self::convert_mut(
|
||||
self.map
|
||||
.entry(TypeId::of::<T>())
|
||||
.or_insert(Box::new(T::default()) as Box<Trait>),
|
||||
)
|
||||
}
|
||||
|
||||
fn convert_mut<T: Unsize<Trait>>(entry: &mut Box<Trait>) -> &mut T {
|
||||
unsafe { &mut *(entry.as_mut() as *mut Trait as *mut T) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Deref for TypeMap<T> {
|
||||
type Target = HashMap<TypeId, Box<T>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> DerefMut for TypeMap<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Default for TypeMap<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
map: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
use crate::util::{DivOr, impl_op};
|
||||
use std::{hash::Hash, ops::*};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct Vec2 {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl Eq for Vec2 {}
|
||||
|
||||
impl Hash for Vec2 {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
state.write_u32(self.x.to_bits());
|
||||
state.write_u32(self.y.to_bits());
|
||||
}
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
pub const ZERO: Self = Self::new(0.0, 0.0);
|
||||
pub const ONE: Self = Self::new(1.0, 1.0);
|
||||
|
||||
pub const fn new(x: f32, y: f32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub const fn round(self) -> Self {
|
||||
Self {
|
||||
x: self.x.round(),
|
||||
y: self.y.round(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn floor(self) -> Self {
|
||||
Self {
|
||||
x: self.x.floor(),
|
||||
y: self.y.floor(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn ceil(self) -> Self {
|
||||
Self {
|
||||
x: self.x.ceil(),
|
||||
y: self.y.ceil(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn tuple(&self) -> (f32, f32) {
|
||||
(self.x, self.y)
|
||||
}
|
||||
|
||||
pub const fn with_x(mut self, x: f32) -> Self {
|
||||
self.x = x;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn with_y(mut self, y: f32) -> Self {
|
||||
self.y = y;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl_op!(impl Add for Vec2: add x y);
|
||||
impl_op!(Vec2 Sub sub; x y);
|
||||
impl_op!(Vec2 Mul mul; 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 {
|
||||
type Output = Self;
|
||||
|
||||
fn neg(mut self) -> Self::Output {
|
||||
self.x = -self.x;
|
||||
self.y = -self.y;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Vec2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "({}, {})", self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Vec2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "({}, {})", self.x, self.y)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use crate::Widget;
|
||||
|
||||
pub struct WidgetData {
|
||||
pub widget: Box<dyn Widget>,
|
||||
pub label: String,
|
||||
pub borrowed: bool,
|
||||
}
|
||||
|
||||
impl WidgetData {
|
||||
pub fn new<W: Widget>(widget: W) -> Self {
|
||||
let name = std::any::type_name::<W>();
|
||||
let label = match (name.find("::"), name.rfind("::")) {
|
||||
(Some(first), Some(last)) => {
|
||||
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 {
|
||||
widget: Box::new(widget),
|
||||
label,
|
||||
borrowed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
use std::{marker::Unsize, ops::CoerceUnsized, sync::mpsc::Sender};
|
||||
|
||||
use crate::{
|
||||
UiRsc, Widget,
|
||||
util::{RefCounter, SlotId},
|
||||
};
|
||||
|
||||
pub type WidgetId = SlotId;
|
||||
|
||||
pub struct StrongWidget<W: ?Sized = dyn Widget> {
|
||||
pub(super) id: WidgetId,
|
||||
counter: RefCounter,
|
||||
send: Sender<WidgetId>,
|
||||
ty: *const W,
|
||||
}
|
||||
|
||||
pub struct WeakWidget<W: ?Sized = dyn Widget> {
|
||||
pub(super) id: WidgetId,
|
||||
#[allow(unused)]
|
||||
ty: *const W,
|
||||
}
|
||||
|
||||
impl<W: Widget + ?Sized + Unsize<dyn Widget>> StrongWidget<W> {
|
||||
pub fn any(self) -> StrongWidget<dyn Widget> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> StrongWidget<W> {
|
||||
pub(crate) fn new(id: WidgetId, send: Sender<WidgetId>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
counter: RefCounter::new(),
|
||||
send,
|
||||
ty: null_ptr(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn refs(&self) -> u32 {
|
||||
self.counter.refs()
|
||||
}
|
||||
|
||||
pub fn weak(&self) -> WeakWidget<W> {
|
||||
let Self { ty, id, .. } = *self;
|
||||
WeakWidget { ty, id }
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> WeakWidget<W> {
|
||||
pub(crate) fn new(id: WidgetId) -> Self {
|
||||
Self { id, ty: null_ptr() }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn upgrade(self, ui: &mut impl UiRsc) -> StrongWidget<W> {
|
||||
ui.widgets_mut().upgrade(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> Drop for StrongWidget<W> {
|
||||
fn drop(&mut self) {
|
||||
if self.counter.drop() {
|
||||
let _ = self.send.send(self.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WidgetIdFn<Rsc, W: ?Sized = dyn Widget>: FnOnce(&mut Rsc) -> WeakWidget<W> {}
|
||||
impl<Rsc, W: ?Sized, F: FnOnce(&mut Rsc) -> WeakWidget<W>> WidgetIdFn<Rsc, W> for F {}
|
||||
|
||||
pub trait IdLike {
|
||||
type Widget: ?Sized;
|
||||
fn id(&self) -> WidgetId;
|
||||
}
|
||||
|
||||
impl<W: ?Sized> IdLike for &StrongWidget<W> {
|
||||
type Widget = W;
|
||||
fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> IdLike for StrongWidget<W> {
|
||||
type Widget = W;
|
||||
fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: ?Sized> IdLike for WeakWidget<W> {
|
||||
type Widget = W;
|
||||
fn id(&self) -> WidgetId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl IdLike for WidgetId {
|
||||
type Widget = dyn Widget;
|
||||
fn id(&self) -> WidgetId {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<StrongWidget<U>> for StrongWidget<T> {}
|
||||
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<WeakWidget<U>> for WeakWidget<T> {}
|
||||
|
||||
impl<W: ?Sized> Clone for WeakWidget<W> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
impl<W: ?Sized> Copy for WeakWidget<W> {}
|
||||
impl<W: ?Sized> PartialEq for WeakWidget<W> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> PartialEq for StrongWidget<W> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> std::fmt::Debug for StrongWidget<W> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.id.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, W: Widget + 'a, State: UiRsc> FnOnce<(&'a mut State,)> for WeakWidget<W> {
|
||||
type Output = &'a mut W;
|
||||
|
||||
extern "rust-call" fn call_once(self, args: (&'a mut State,)) -> Self::Output {
|
||||
&mut args.0.widgets_mut()[self]
|
||||
}
|
||||
}
|
||||
|
||||
fn null_ptr<W: ?Sized>() -> *const W {
|
||||
if size_of::<&W>() == size_of::<*const dyn Widget>() {
|
||||
let w: *const dyn Widget = &();
|
||||
unsafe { std::mem::transmute_copy(&w) }
|
||||
} else {
|
||||
unsafe { std::mem::transmute_copy(&[0usize; 1]) }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<W: ?Sized> Send for WeakWidget<W> {}
|
||||
unsafe impl<W: ?Sized> Sync for WeakWidget<W> {}
|
||||
@@ -1,74 +0,0 @@
|
||||
use crate::UiRsc;
|
||||
|
||||
use super::*;
|
||||
use std::marker::Unsize;
|
||||
|
||||
pub trait WidgetLike<Rsc: UiRsc, Tag>: Sized {
|
||||
type Widget: Widget + ?Sized + Unsize<dyn Widget>;
|
||||
|
||||
fn add(self, rsc: &mut Rsc) -> WeakWidget<Self::Widget>;
|
||||
|
||||
fn add_strong(self, rsc: &mut Rsc) -> StrongWidget<Self::Widget> {
|
||||
self.add(rsc).upgrade(rsc)
|
||||
}
|
||||
|
||||
fn with_id<W2>(
|
||||
self,
|
||||
f: impl FnOnce(&mut Rsc, WeakWidget<Self::Widget>) -> WeakWidget<W2>,
|
||||
) -> impl WidgetIdFn<Rsc, W2> {
|
||||
move |state| {
|
||||
let id = self.add(state);
|
||||
f(state, id)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_root(self, rsc: &mut Rsc, root: &mut impl HasRoot<Rsc>) {
|
||||
let id = self.add_strong(rsc);
|
||||
root.set_root(rsc, id);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasRoot<Rsc> {
|
||||
fn set_root(&mut self, rsc: &mut Rsc, root: StrongWidget);
|
||||
}
|
||||
|
||||
pub trait WidgetArrLike<Rsc, const LEN: usize, Tag> {
|
||||
#[track_caller]
|
||||
fn add(self, state: &mut Rsc) -> WidgetArr<LEN>;
|
||||
}
|
||||
|
||||
impl<Rsc, const LEN: usize> WidgetArrLike<Rsc, LEN, ArrTag> for WidgetArr<LEN> {
|
||||
fn add(self, _: &mut Rsc) -> WidgetArr<LEN> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_widget_arr {
|
||||
($n:expr;$($W:ident)*) => {
|
||||
impl_widget_arr!($n;$($W)*;$(${concat($W,Tag)})*);
|
||||
};
|
||||
($n:expr;$($W:ident)*;$($Tag:ident)*) => {
|
||||
impl<Rsc: UiRsc, $($W: WidgetLike<Rsc, $Tag>,$Tag,)*> WidgetArrLike<Rsc, $n, ($($Tag,)*)> for ($($W,)*) {
|
||||
fn add(self, rsc: &mut Rsc) -> WidgetArr<$n> {
|
||||
#[allow(non_snake_case)]
|
||||
let ($($W,)*) = self;
|
||||
WidgetArr::new(
|
||||
[$($W.add(rsc).upgrade(rsc),)*],
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_widget_arr!(1;A);
|
||||
impl_widget_arr!(2;A B);
|
||||
impl_widget_arr!(3;A B C);
|
||||
impl_widget_arr!(4;A B C D);
|
||||
impl_widget_arr!(5;A B C D E);
|
||||
impl_widget_arr!(6;A B C D E F);
|
||||
impl_widget_arr!(7;A B C D E F G);
|
||||
impl_widget_arr!(8;A B C D E F G H);
|
||||
impl_widget_arr!(9;A B C D E F G H I);
|
||||
impl_widget_arr!(10;A B C D E F G H I J);
|
||||
impl_widget_arr!(11;A B C D E F G H I J K);
|
||||
impl_widget_arr!(12;A B C D E F G H I J K L);
|
||||
@@ -1,103 +0,0 @@
|
||||
use crate::{Axis, LayoutLen, Painter, Size};
|
||||
use std::any::Any;
|
||||
|
||||
mod data;
|
||||
mod handle;
|
||||
mod like;
|
||||
mod tag;
|
||||
mod view;
|
||||
mod widgets;
|
||||
|
||||
pub use data::*;
|
||||
pub use handle::*;
|
||||
pub use like::*;
|
||||
pub use tag::*;
|
||||
pub use view::*;
|
||||
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 {
|
||||
fn draw(&mut self, painter: &mut Painter);
|
||||
|
||||
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
|
||||
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 () {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
painter.set_size(Size::ZERO);
|
||||
}
|
||||
|
||||
fn is_size_independent(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn size_hint(&self, _axis: Axis) -> Option<LayoutLen> {
|
||||
Some(LayoutLen::ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn Widget {
|
||||
pub fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
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 {}
|
||||
|
||||
pub struct WidgetArr<const LEN: usize> {
|
||||
pub arr: [StrongWidget; LEN],
|
||||
}
|
||||
|
||||
impl<const LEN: usize> WidgetArr<LEN> {
|
||||
pub fn new(arr: [StrongWidget; LEN]) -> Self {
|
||||
Self { arr }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WidgetOption<State> {
|
||||
fn get(self, state: &mut State) -> Option<StrongWidget>;
|
||||
}
|
||||
|
||||
impl<State> WidgetOption<State> for () {
|
||||
fn get(self, _: &mut State) -> Option<StrongWidget> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<State, F: FnOnce(&mut State) -> Option<StrongWidget>> WidgetOption<State> for F {
|
||||
fn get(self, state: &mut State) -> Option<StrongWidget> {
|
||||
self(state)
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
use super::*;
|
||||
use crate::UiRsc;
|
||||
use std::marker::Unsize;
|
||||
|
||||
pub struct WidgetTag;
|
||||
impl<Rsc: UiRsc, W: Widget> WidgetLike<Rsc, WidgetTag> for W {
|
||||
type Widget = W;
|
||||
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
|
||||
let w = rsc.ui_mut().widgets.add_weak(self);
|
||||
rsc.on_add(w);
|
||||
w
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FnTag;
|
||||
impl<Rsc: UiRsc, W: Widget, F: FnOnce(&mut Rsc) -> W> WidgetLike<Rsc, FnTag> for F {
|
||||
type Widget = W;
|
||||
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
|
||||
self(rsc).add(rsc)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WidgetFnTrait<Rsc> {
|
||||
type Widget: Widget;
|
||||
fn run(self, rsc: &mut Rsc) -> Self::Widget;
|
||||
}
|
||||
pub struct FnTraitTag;
|
||||
impl<Rsc: UiRsc, T: WidgetFnTrait<Rsc>> WidgetLike<Rsc, FnTraitTag> for T {
|
||||
type Widget = T::Widget;
|
||||
#[track_caller]
|
||||
fn add(self, rsc: &mut Rsc) -> WeakWidget<T::Widget> {
|
||||
self.run(rsc).add(rsc)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RefTag;
|
||||
impl<Rsc: UiRsc, W: ?Sized + Widget + Unsize<dyn Widget>> WidgetLike<Rsc, RefTag>
|
||||
for WeakWidget<W>
|
||||
{
|
||||
type Widget = W;
|
||||
fn add(self, _: &mut Rsc) -> WeakWidget<W> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RefFnTag;
|
||||
impl<Rsc: UiRsc, W: ?Sized + Widget + Unsize<dyn Widget>, F: FnOnce(&mut Rsc) -> WeakWidget<W>>
|
||||
WidgetLike<Rsc, RefFnTag> for F
|
||||
{
|
||||
type Widget = W;
|
||||
fn add(self, rsc: &mut Rsc) -> WeakWidget<W> {
|
||||
self(rsc)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ViewTag;
|
||||
impl<Rsc: UiRsc, V: WidgetView> WidgetLike<Rsc, ViewTag> for V {
|
||||
type Widget = V::Widget;
|
||||
fn add(self, _: &mut Rsc) -> WeakWidget<Self::Widget> {
|
||||
self.root()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ArrTag;
|
||||
@@ -1,24 +0,0 @@
|
||||
use std::marker::Unsize;
|
||||
|
||||
use crate::{IdLike, WeakWidget, Widget};
|
||||
|
||||
pub trait WidgetView {
|
||||
type Widget: Widget + ?Sized + Unsize<dyn Widget>;
|
||||
fn root(&self) -> WeakWidget<Self::Widget>;
|
||||
}
|
||||
|
||||
pub trait HasWidget {
|
||||
type Widget: Widget + ?Sized + Unsize<dyn Widget>;
|
||||
}
|
||||
|
||||
impl<W: Widget + Unsize<dyn Widget> + ?Sized> HasWidget for WeakWidget<W> {
|
||||
type Widget = W;
|
||||
}
|
||||
|
||||
impl<WV: WidgetView> IdLike for WV {
|
||||
type Widget = WV::Widget;
|
||||
|
||||
fn id(&self) -> super::WidgetId {
|
||||
self.root().id
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
|
||||
use crate::{
|
||||
IdLike, StrongWidget, WeakWidget, Widget, WidgetData, WidgetId,
|
||||
util::{DynBorrower, HashSet, SlotVec, forget_mut, to_mut},
|
||||
};
|
||||
|
||||
pub struct Widgets {
|
||||
pub needs_redraw: HashSet<WidgetId>,
|
||||
vec: SlotVec<WidgetData>,
|
||||
send: Sender<WidgetId>,
|
||||
recv: Receiver<WidgetId>,
|
||||
pub(crate) waiting: HashSet<WidgetId>,
|
||||
named: HashSet<WidgetId>,
|
||||
}
|
||||
|
||||
impl Widgets {
|
||||
pub fn new() -> Self {
|
||||
let (send, recv) = channel();
|
||||
Self {
|
||||
needs_redraw: Default::default(),
|
||||
vec: Default::default(),
|
||||
waiting: Default::default(),
|
||||
named: Default::default(),
|
||||
send,
|
||||
recv,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_updates(&self) -> bool {
|
||||
!self.needs_redraw.is_empty()
|
||||
}
|
||||
|
||||
pub fn get_dyn(&self, id: WidgetId) -> Option<&dyn Widget> {
|
||||
Some(self.vec.get(id)?.widget.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_dyn_mut(&mut self, id: WidgetId) -> Option<&mut dyn Widget> {
|
||||
self.needs_redraw.insert(id);
|
||||
Some(self.vec.get_mut(id)?.widget.as_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn get_dyn_dynamic<'a>(&self, id: WidgetId) -> WidgetWrapper<'a> {
|
||||
// SAFETY: must guarantee no other mutable references to this widget exist
|
||||
// done through the borrow variable
|
||||
let data = unsafe { forget_mut(to_mut(self.vec.get(id).unwrap())) };
|
||||
if data.borrowed {
|
||||
panic!("tried to mutably borrow the same widget twice");
|
||||
}
|
||||
WidgetWrapper::new(data.widget.as_mut(), &mut data.borrowed)
|
||||
}
|
||||
|
||||
pub fn get<I: IdLike>(&self, id: &I) -> Option<&I::Widget>
|
||||
where
|
||||
I::Widget: Sized + Widget,
|
||||
{
|
||||
self.get_dyn(id.id())?.as_any().downcast_ref()
|
||||
}
|
||||
|
||||
pub fn get_mut<I: IdLike>(&mut self, id: &I) -> Option<&mut I::Widget>
|
||||
where
|
||||
I::Widget: Sized + Widget,
|
||||
{
|
||||
self.get_dyn_mut(id.id())?.as_any_mut().downcast_mut()
|
||||
}
|
||||
|
||||
pub fn add_strong<W: Widget>(&mut self, widget: W) -> StrongWidget<W> {
|
||||
let id = self.vec.add(WidgetData::new(widget));
|
||||
StrongWidget::new(id, self.send.clone())
|
||||
}
|
||||
|
||||
pub fn add_weak<W: Widget>(&mut self, widget: W) -> WeakWidget<W> {
|
||||
let id = self.vec.add(WidgetData::new(widget));
|
||||
self.waiting.insert(id);
|
||||
WeakWidget::new(id)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn upgrade<W: ?Sized>(&mut self, rf: WeakWidget<W>) -> StrongWidget<W> {
|
||||
if !self.waiting.remove(&rf.id()) {
|
||||
let label = self.label(rf);
|
||||
let id = rf.id();
|
||||
panic!(
|
||||
"widget '{label}' ({id:?}) was already added\ncannot add a widget twice; consider creating two"
|
||||
)
|
||||
}
|
||||
StrongWidget::new(rf.id(), self.send.clone())
|
||||
}
|
||||
|
||||
pub fn data(&self, id: impl IdLike) -> Option<&WidgetData> {
|
||||
self.vec.get(id.id())
|
||||
}
|
||||
|
||||
pub fn label(&self, id: impl IdLike) -> &String {
|
||||
&self.data(id.id()).unwrap().label
|
||||
}
|
||||
|
||||
pub fn set_label(&mut self, id: impl IdLike, label: String) {
|
||||
let id = id.id();
|
||||
self.data_mut(id).unwrap().label = label;
|
||||
self.named.insert(id);
|
||||
}
|
||||
|
||||
pub fn named(&self) -> impl Iterator<Item = WidgetId> + '_ {
|
||||
self.named.iter().copied()
|
||||
}
|
||||
|
||||
pub fn data_mut(&mut self, id: impl IdLike) -> Option<&mut WidgetData> {
|
||||
self.vec.get_mut(id.id())
|
||||
}
|
||||
|
||||
pub fn free_next(&mut self) -> Option<WidgetId> {
|
||||
let next = self.recv.try_recv().ok()?;
|
||||
self.vec.free(next);
|
||||
self.named.remove(&next);
|
||||
Some(next)
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.vec.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Widgets {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub type WidgetWrapper<'a> = DynBorrower<'a, dyn Widget>;
|
||||
|
||||
impl<I: IdLike> std::ops::Index<I> for Widgets
|
||||
where
|
||||
I::Widget: Sized + Widget,
|
||||
{
|
||||
type Output = I::Widget;
|
||||
|
||||
fn index(&self, id: I) -> &Self::Output {
|
||||
self.get(&id).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: IdLike> std::ops::IndexMut<I> for Widgets
|
||||
where
|
||||
I::Widget: Sized + Widget,
|
||||
{
|
||||
fn index_mut(&mut self, id: I) -> &mut Self::Output {
|
||||
self.get_mut(&id).unwrap()
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before 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 }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "iris-macro"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.107"
|
||||
quote = "1.0.47"
|
||||
syn = { version = "3.0.5", features = ["full"] }
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
@@ -1,358 +0,0 @@
|
||||
extern crate proc_macro;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
Attribute, Block, Error, FnArg, GenericParam, Generics, Ident, ItemFn, ItemStruct, ItemTrait,
|
||||
ReturnType, Signature, Token, Type, Visibility,
|
||||
parse::{Parse, ParseStream, Result},
|
||||
parse_macro_input, parse_quote,
|
||||
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 {
|
||||
attrs: Vec<Attribute>,
|
||||
vis: Visibility,
|
||||
name: Ident,
|
||||
generics: Generics,
|
||||
fns: Vec<InputFn>,
|
||||
}
|
||||
|
||||
struct InputFn {
|
||||
attrs: Vec<Attribute>,
|
||||
sig: Signature,
|
||||
body: Block,
|
||||
}
|
||||
|
||||
impl Parse for Input {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let attrs = input.call(Attribute::parse_outer)?;
|
||||
let vis = input.parse()?;
|
||||
input.parse::<Token![trait]>()?;
|
||||
let name = input.parse()?;
|
||||
let generics = input.parse::<Generics>()?;
|
||||
input.parse::<Token![;]>()?;
|
||||
let mut fns = Vec::new();
|
||||
while !input.is_empty() {
|
||||
let attrs = input.call(Attribute::parse_outer)?;
|
||||
let sig = input.parse()?;
|
||||
let body = input.parse()?;
|
||||
fns.push(InputFn { attrs, sig, body })
|
||||
}
|
||||
if !input.is_empty() {
|
||||
input.error("function expected");
|
||||
}
|
||||
Ok(Input {
|
||||
attrs,
|
||||
vis,
|
||||
name,
|
||||
generics,
|
||||
fns,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn widget_trait(input: TokenStream) -> TokenStream {
|
||||
let Input {
|
||||
attrs,
|
||||
vis,
|
||||
name,
|
||||
mut generics,
|
||||
fns,
|
||||
} = parse_macro_input!(input as Input);
|
||||
|
||||
let sigs: Vec<_> = fns
|
||||
.iter()
|
||||
.map(|InputFn { attrs, sig, .. }| quote! { #(#attrs)* #sig })
|
||||
.collect();
|
||||
let impls: Vec<_> = fns
|
||||
.iter()
|
||||
.map(|InputFn { sig, body, .. }| quote! { #sig #body })
|
||||
.collect();
|
||||
|
||||
let Some(GenericParam::Type(state)) = generics.params.first() else {
|
||||
return Error::new(name.span(), "expected state generic parameter")
|
||||
.into_compile_error()
|
||||
.into();
|
||||
};
|
||||
|
||||
let state = &state.ident;
|
||||
|
||||
generics
|
||||
.params
|
||||
.push(parse_quote!(WL: WidgetLike<#state, Tag>));
|
||||
generics.params.push(parse_quote!(Tag));
|
||||
|
||||
let mut trai: ItemTrait = parse_quote!(
|
||||
#vis trait #name #generics {
|
||||
#(#sigs;)*
|
||||
}
|
||||
);
|
||||
|
||||
trai.attrs = attrs;
|
||||
|
||||
quote! {
|
||||
#trai
|
||||
|
||||
impl #generics #name<Rsc, WL, Tag> for WL {
|
||||
#(#impls)*
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(DesktopUiState, attributes(desktop_ui_state))]
|
||||
pub fn derive_desktop_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 found_attr = false;
|
||||
let mut state_field = None;
|
||||
for field in &state.fields {
|
||||
if !found_attr
|
||||
&& let Type::Path(path) = &field.ty
|
||||
&& path.path.is_ident(state_type)
|
||||
{
|
||||
state_field = Some(field);
|
||||
}
|
||||
let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident(field_attr)) else {
|
||||
continue;
|
||||
};
|
||||
if found_attr {
|
||||
output.extend(
|
||||
Error::new(
|
||||
attr.span(),
|
||||
format!("cannot have more than one {field_attr} attribute"),
|
||||
)
|
||||
.into_compile_error(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
found_attr = true;
|
||||
state_field = Some(field);
|
||||
}
|
||||
let Some(field) = state_field else {
|
||||
output.extend(
|
||||
Error::new(state.ident.span(), format!("no {state_type} field found"))
|
||||
.into_compile_error(),
|
||||
);
|
||||
return output.into();
|
||||
};
|
||||
let sname = &state.ident;
|
||||
let Some(fname) = field.ident.as_ref() else {
|
||||
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! {
|
||||
impl #impl_generics iris::#module::#state_trait for #sname #type_generics #where_clause {
|
||||
fn #get(&self) -> &iris::#module::#state_type {
|
||||
&self.#fname
|
||||
}
|
||||
fn #get_mut(&mut self) -> &mut iris::#module::#state_type {
|
||||
&mut self.#fname
|
||||
}
|
||||
}
|
||||
});
|
||||
output.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(WidgetView, attributes(root))]
|
||||
pub fn derive_widget_view(input: TokenStream) -> TokenStream {
|
||||
let mut output = proc_macro2::TokenStream::new();
|
||||
|
||||
let state: ItemStruct = parse_macro_input!(input);
|
||||
|
||||
let mut found_attr = false;
|
||||
let mut state_field = None;
|
||||
for field in &state.fields {
|
||||
let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident("root")) else {
|
||||
continue;
|
||||
};
|
||||
if found_attr {
|
||||
output.extend(
|
||||
Error::new(attr.span(), "cannot have more than one root widget")
|
||||
.into_compile_error(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
found_attr = true;
|
||||
state_field = Some(field);
|
||||
}
|
||||
let Some(field) = state_field else {
|
||||
output.extend(
|
||||
Error::new(state.ident.span(), "no root widget field found (#[root])")
|
||||
.into_compile_error(),
|
||||
);
|
||||
return output.into();
|
||||
};
|
||||
let sname = &state.ident;
|
||||
let fname = field.ident.as_ref().unwrap();
|
||||
let fty = &field.ty;
|
||||
output.extend(quote! {
|
||||
impl iris::core::WidgetView for #sname {
|
||||
type Widget = <#fty as iris::core::HasWidget>::Widget;
|
||||
fn root(&self) -> #fty {
|
||||
self.#fname
|
||||
}
|
||||
}
|
||||
});
|
||||
output.into()
|
||||
}
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
# iris
|
||||
|
||||
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.
|
||||
|
||||
Examples are in `examples`, eg. `cargo run --example tabs`. Each example keeps
|
||||
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:
|
||||
1. does what I want it to (text, images, video, animations)
|
||||
2. very easy to use ignoring ergonomic ref counting
|
||||
3. reasonably fast / efficient (a lot faster than electron, save battery life, try to beat iced and xilem)
|
||||
|
||||
## dev details
|
||||
|
||||
not targeting web rn cause wanna use actual nice gpu features & entire point of this is to make desktop apps / not need a web browser
|
||||
|
||||
general ideas trynna use rn / experiment with:
|
||||
- retained mode
|
||||
- specifically designed around wgpu so there's no translation
|
||||
- postfix functions for most things to prevent unreadable indentation (going very well)
|
||||
- events can be done directly where you draw the widgets
|
||||
- almost no macros in user code & actual LSP typechecking (variadic generics if you can hear me please save us)
|
||||
- relative anchor + absolute offset coord system (+ "rest" / leftover during widget layout)
|
||||
- single threaded ui & pass context around to make non async usage straightforward (pretty unsure about this)
|
||||
- widgets store outside of the actual rendering so they can be moved around and swapped easily (unsure about this but seems to work good for now)
|
||||
|
||||
under heavy initial development so not gonna try to explain status, maybe check TODO for that;
|
||||
sizable chance it gets a rewrite once I know everything I need and what seems to work best
|
||||
|
||||
it's called iris because it's the structure around what you actually want to display and colorful
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "rig-input"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
# Replays harness `.touch` files through Wayland's virtual-pointer protocol;
|
||||
# headless sway has no input devices for coordinate-driving tools to move.
|
||||
[[bin]]
|
||||
name = "replay-touch"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Share the harness parser so both layers interpret recordings identically.
|
||||
iris = { path = ".." }
|
||||
wayland-client = "0.31.15"
|
||||
wayland-protocols-wlr = { version = "0.3.12", features = ["client"] }
|
||||
@@ -1,141 +0,0 @@
|
||||
use iris::harness::{TouchAction, TouchScript};
|
||||
use std::time::Duration;
|
||||
use wayland_client::protocol::wl_pointer::ButtonState;
|
||||
use wayland_client::protocol::{wl_registry, wl_seat};
|
||||
use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
|
||||
use wayland_protocols_wlr::virtual_pointer::v1::client::{
|
||||
zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1,
|
||||
zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
|
||||
};
|
||||
|
||||
const BTN_LEFT: u32 = 0x110;
|
||||
|
||||
const SETTLE: Duration = Duration::from_millis(200);
|
||||
|
||||
#[derive(Default)]
|
||||
struct Globals {
|
||||
seat: Option<wl_seat::WlSeat>,
|
||||
manager: Option<ZwlrVirtualPointerManagerV1>,
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
registry: &wl_registry::WlRegistry,
|
||||
event: wl_registry::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
let wl_registry::Event::Global {
|
||||
name,
|
||||
interface,
|
||||
version,
|
||||
} = event
|
||||
else {
|
||||
return;
|
||||
};
|
||||
match interface.as_str() {
|
||||
"wl_seat" => {
|
||||
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
|
||||
}
|
||||
"zwlr_virtual_pointer_manager_v1" => {
|
||||
state.manager = Some(registry.bind(name, version.min(2), qh, ()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate_noop!(Globals: ignore wl_seat::WlSeat);
|
||||
delegate_noop!(Globals: ZwlrVirtualPointerManagerV1);
|
||||
delegate_noop!(Globals: ZwlrVirtualPointerV1);
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let [width, height, path] = args.as_slice() else {
|
||||
eprintln!("usage: replay-touch WIDTH HEIGHT FILE");
|
||||
std::process::exit(2);
|
||||
};
|
||||
let (width, height) = (parse(width, "WIDTH"), parse(height, "HEIGHT"));
|
||||
let text = std::fs::read_to_string(path)
|
||||
.unwrap_or_else(|e| fail(&format!("could not read {path}: {e}")));
|
||||
let script = TouchScript::parse(&text).unwrap_or_else(|e| fail(&e));
|
||||
|
||||
let conn = Connection::connect_to_env().unwrap_or_else(|e| {
|
||||
fail(&format!(
|
||||
"no wayland display ({e}); is WAYLAND_DISPLAY set?"
|
||||
))
|
||||
});
|
||||
let mut queue = conn.new_event_queue();
|
||||
let qh = queue.handle();
|
||||
let display = conn.display();
|
||||
display.get_registry(&qh, ());
|
||||
let mut globals = Globals::default();
|
||||
queue
|
||||
.roundtrip(&mut globals)
|
||||
.unwrap_or_else(|e| fail(&format!("wayland roundtrip failed: {e}")));
|
||||
|
||||
let manager = globals.manager.as_ref().unwrap_or_else(|| {
|
||||
fail(
|
||||
"this compositor does not offer zwlr_virtual_pointer_manager_v1, so a pointer cannot \
|
||||
be synthesised; sway and every wlroots compositor do",
|
||||
)
|
||||
});
|
||||
let pointer = manager.create_virtual_pointer(globals.seat.as_ref(), &qh, ());
|
||||
|
||||
// Put the pointer where the gesture starts and let the compositor
|
||||
// settle before anything is pressed. Without this the press is
|
||||
// dropped: sway has just learned about this pointer, and a button
|
||||
// sent in the same breath as the motion that first puts it over a
|
||||
// window arrives before there is a focused surface to send it to --
|
||||
// winit sees `CursorEntered`, the moves and the *release*, never the
|
||||
// press, so the gesture reads as a hover and nothing scrolls. Found
|
||||
// by printing winit's own events; the settle is what fixed it.
|
||||
if let Some(first) = script.samples.first() {
|
||||
pointer.motion_absolute(0, first.pos.x as u32, first.pos.y as u32, width, height);
|
||||
pointer.frame();
|
||||
conn.flush()
|
||||
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
|
||||
std::thread::sleep(SETTLE);
|
||||
}
|
||||
|
||||
let mut previous = 0;
|
||||
for sample in &script.samples {
|
||||
std::thread::sleep(Duration::from_millis(sample.t_ms - previous));
|
||||
previous = sample.t_ms;
|
||||
let t = sample.t_ms as u32;
|
||||
pointer.motion_absolute(t, sample.pos.x as u32, sample.pos.y as u32, width, height);
|
||||
pointer.frame();
|
||||
// The button goes in a frame of its own, *after* the motion has
|
||||
// been committed. Sent in the same frame as the motion that
|
||||
// first puts the pointer over the window, sway drops it: the
|
||||
// client sees `CursorEntered` and the moves but never a
|
||||
// `MouseInput { state: Pressed }`, so the whole gesture reads as
|
||||
// a hover and nothing scrolls. Found exactly that way, by
|
||||
// printing winit's events.
|
||||
let state = match sample.action {
|
||||
TouchAction::Down => Some(ButtonState::Pressed),
|
||||
TouchAction::Up | TouchAction::Cancel => Some(ButtonState::Released),
|
||||
TouchAction::Move => None,
|
||||
};
|
||||
if let Some(state) = state {
|
||||
pointer.button(t, BTN_LEFT, state);
|
||||
pointer.frame();
|
||||
}
|
||||
conn.flush()
|
||||
.unwrap_or_else(|e| fail(&format!("flush: {e}")));
|
||||
}
|
||||
pointer.destroy();
|
||||
conn.flush().ok();
|
||||
}
|
||||
|
||||
fn parse(text: &str, what: &str) -> u32 {
|
||||
text.parse()
|
||||
.unwrap_or_else(|_| fail(&format!("{what} is not a whole number: {text:?}")))
|
||||
}
|
||||
|
||||
fn fail(message: &str) -> ! {
|
||||
eprintln!("replay-touch: {message}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["clippy", "rustfmt"]
|
||||
targets = ["aarch64-linux-android", "x86_64-linux-android"]
|
||||
@@ -1,14 +0,0 @@
|
||||
# The compositor `scripts/run-headless.sh` starts, because this machine has 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
|
||||
# second thing to go wrong for no gain. (`emu`'s config forces it because the
|
||||
# Android emulator's renderer speaks GLX.)
|
||||
xwayland disable
|
||||
|
||||
# A desktop-shaped output, since this is the desktop half of the port. Larger
|
||||
# than the window an example opens, so nothing is scaled or clipped.
|
||||
output HEADLESS-1 mode 1920x1200@60Hz
|
||||
|
||||
default_border none
|
||||
focus_follows_mouse no
|
||||
@@ -1,151 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""AOSP's fling spline, transcribed independently of the Rust port.
|
||||
|
||||
This exists so the numbers in `sense.rs`'s `the_spline_matches_aosps_own_table`
|
||||
and `a_flick_decelerates_the_way_aosp_says_it_does` are not the Rust code
|
||||
grading its own homework. Every test iris's fling had before 2026-09-07
|
||||
compared the curve with itself -- monotonic, signed, integrates to the closed
|
||||
form -- and all of them passed while `distance_fraction(t)` was returning
|
||||
exactly `t` (see `android_fling_spline`'s doc comment). Numbers checked into a
|
||||
test have to come from somewhere else, and this is the somewhere else.
|
||||
|
||||
Transcribed by hand from, and only from:
|
||||
|
||||
* frameworks/base `core/java/android/widget/OverScroller.java`,
|
||||
`SplineOverScroller`'s static initialiser, `getSplineDeceleration`,
|
||||
`getSplineFlingDistance`, `getSplineFlingDuration` and `update`.
|
||||
* androidx.compose.animation:animation:1.12.0 `SplineBasedDecay.kt`
|
||||
(`computeSplineInfo`, `AndroidFlingSpline.flingPosition`) and
|
||||
`FlingCalculator.kt` (`computeDeceleration`, `flingDistance`,
|
||||
`flingDuration`, `FlingInfo.position`/`velocity`). The two agree line for
|
||||
line, which is why iris ports one curve rather than two.
|
||||
|
||||
Run it with no arguments; it prints the table entries and the (velocity,
|
||||
density, t) points the Rust tests assert on.
|
||||
"""
|
||||
|
||||
NB_SAMPLES = 100
|
||||
INFLEXION = 0.35
|
||||
START_TENSION = 0.5
|
||||
END_TENSION = 1.0
|
||||
P1 = START_TENSION * INFLEXION
|
||||
P2 = 1.0 - END_TENSION * (1.0 - INFLEXION)
|
||||
|
||||
SCROLL_FRICTION = 0.015
|
||||
TUNING = 0.84
|
||||
GRAVITY_EARTH = 9.80665
|
||||
INCHES_PER_METER = 39.37
|
||||
|
||||
import math
|
||||
|
||||
DECELERATION_RATE = math.log(0.78) / math.log(0.9)
|
||||
|
||||
|
||||
def spline_positions():
|
||||
"""SPLINE_POSITION: distance fraction at each of 101 even time steps."""
|
||||
position = [0.0] * (NB_SAMPLES + 1)
|
||||
x_min = 0.0
|
||||
for i in range(NB_SAMPLES):
|
||||
alpha = i / NB_SAMPLES
|
||||
x_max = 1.0
|
||||
while True:
|
||||
x = x_min + (x_max - x_min) / 2.0
|
||||
coef = 3.0 * x * (1.0 - x)
|
||||
tx = coef * ((1.0 - x) * P1 + x * P2) + x * x * x
|
||||
if abs(tx - alpha) < 1e-5:
|
||||
break
|
||||
if tx > alpha:
|
||||
x_max = x
|
||||
else:
|
||||
x_min = x
|
||||
position[i] = coef * ((1.0 - x) * START_TENSION + x * END_TENSION) + x * x * x
|
||||
position[NB_SAMPLES] = 1.0
|
||||
return position
|
||||
|
||||
|
||||
POSITION = spline_positions()
|
||||
|
||||
|
||||
def fling_sample(t):
|
||||
"""(distance fraction, velocity fraction) at time fraction `t`."""
|
||||
t = min(max(t, 0.0), 1.0)
|
||||
index = int(t * NB_SAMPLES)
|
||||
if index >= NB_SAMPLES:
|
||||
return 1.0, 0.0
|
||||
t_inf = index / NB_SAMPLES
|
||||
t_sup = (index + 1) / NB_SAMPLES
|
||||
velocity_coef = (POSITION[index + 1] - POSITION[index]) / (t_sup - t_inf)
|
||||
return POSITION[index] + (t - t_inf) * velocity_coef, velocity_coef
|
||||
|
||||
|
||||
def physical_coefficient(density):
|
||||
return GRAVITY_EARTH * INCHES_PER_METER * density * 160.0 * TUNING
|
||||
|
||||
|
||||
def deceleration(velocity, density):
|
||||
return math.log(
|
||||
INFLEXION * abs(velocity) / (SCROLL_FRICTION * physical_coefficient(density))
|
||||
)
|
||||
|
||||
|
||||
def fling_distance(velocity, density):
|
||||
l = deceleration(velocity, density)
|
||||
return (
|
||||
SCROLL_FRICTION
|
||||
* physical_coefficient(density)
|
||||
* math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l)
|
||||
)
|
||||
|
||||
|
||||
def fling_duration_s(velocity, density):
|
||||
l = deceleration(velocity, density)
|
||||
return math.exp(l / (DECELERATION_RATE - 1.0))
|
||||
|
||||
|
||||
def position_at(velocity, density, t_seconds):
|
||||
d = fling_duration_s(velocity, density)
|
||||
return fling_distance(velocity, density) * fling_sample(t_seconds / d)[0]
|
||||
|
||||
|
||||
def velocity_at(velocity, density, t_seconds):
|
||||
d = fling_duration_s(velocity, density)
|
||||
return fling_sample(t_seconds / d)[1] * fling_distance(velocity, density) / d
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("SPLINE_POSITION at a few indices (index: value)")
|
||||
for i in (0, 1, 10, 25, 50, 75, 99, 100):
|
||||
print(f" {i:3}: {POSITION[i]:.6f}")
|
||||
print()
|
||||
print("distance/velocity fraction at time fractions")
|
||||
for t in (0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0):
|
||||
d, v = fling_sample(t)
|
||||
print(f" t={t:<5} distance={d:.6f} velocity={v:.6f}")
|
||||
print()
|
||||
# 2.55 is Iris's Pixel 9 Pro XL (docs/bench/iris-phone-v2-2026-09-06.md);
|
||||
# 2.75 is this checkout's emulator.
|
||||
for density in (2.55, 2.75):
|
||||
# 15250 is `app/touch/flick-120hz.touch`'s own
|
||||
# release velocity (velocity_reference.py), so `phone_screen.rs`
|
||||
# can bound the fling it produces from *here* rather than from the
|
||||
# `FlingCalculator` under test (docs/REVIEW-2026-09-07.md's T1).
|
||||
for velocity in (5000.0, 11064.0, 15250.0):
|
||||
dur = fling_duration_s(velocity, density)
|
||||
print(
|
||||
f"density={density} v={velocity}: "
|
||||
f"distance={fling_distance(velocity, density):.3f}px "
|
||||
f"duration={dur:.4f}s"
|
||||
)
|
||||
# Deliberately not round fractions. The velocity coefficient is
|
||||
# piecewise *constant* across each of the 100 samples, so it
|
||||
# steps at t = k/100 and a test asserting on 0.75 is asserting
|
||||
# on which side of a discontinuity the last float landed --
|
||||
# which is genuinely different between Python and Rust and says
|
||||
# nothing about the curve.
|
||||
for frac in (0.125, 0.335, 0.505, 0.755):
|
||||
t = frac * dur
|
||||
print(
|
||||
f" t={frac:>4} of duration ({t:.4f}s): "
|
||||
f"pos={position_at(velocity, density, t):.3f}px "
|
||||
f"vel={velocity_at(velocity, density, t):.3f}px/s"
|
||||
)
|
||||
Loaded 100 of 160 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user