Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14435de6a5 |
No files matched your search
@@ -1,2 +1 @@
|
|||||||
/target
|
/target
|
||||||
perf.data*
|
|
||||||
Generated
+607
-2974
File diff suppressed because it is too large.
Load diff
+9
-101
@@ -1,105 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "iris"
|
name = "gui"
|
||||||
version.workspace = true
|
version = "0.1.0"
|
||||||
edition.workspace = true
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iris-core = { workspace = true }
|
pollster = "0.4.0"
|
||||||
iris-macro = { workspace = true }
|
winit = "0.30.11"
|
||||||
parley = { workspace = true }
|
wgpu = "26.0.1"
|
||||||
swash = { workspace = true }
|
bytemuck = "1.23.1"
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
)
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Compose's touch velocity tracker, transcribed independently of the Rust port.
|
|
||||||
|
|
||||||
Same reason `fling_spline_reference.py` exists: the numbers checked into
|
|
||||||
`sense.rs`'s velocity tests must not be numbers the Rust produced. The old
|
|
||||||
estimator -- total motion over the sample span, an average -- passed every test
|
|
||||||
it had, because every one of those tests asserted the average's own definition
|
|
||||||
back at it. An average cannot tell an accelerating flick from a steady drag, and
|
|
||||||
that is exactly what Iris reported from the phone on 2026-09-07: "flinging now
|
|
||||||
actually works but is slower than Compose's immediately after releasing the
|
|
||||||
flick".
|
|
||||||
|
|
||||||
Transcribed by hand from, and only from, the `-sources.jar` of
|
|
||||||
**androidx.compose.ui:ui-android:1.12.0** and
|
|
||||||
**androidx.compose.foundation:foundation-android:1.12.0**
|
|
||||||
(dl.google.com/dl/android/maven2), read 2026-09-07:
|
|
||||||
|
|
||||||
* `androidx/compose/ui/input/pointer/util/VelocityTracker.kt` --
|
|
||||||
`VelocityTracker1D.calculateVelocity`, `polyFitLeastSquares`,
|
|
||||||
`calculateImpulseVelocity`, `kineticEnergyToVelocity`, and the constants
|
|
||||||
`HistorySize = 20`, `HorizonMilliseconds = 100`,
|
|
||||||
`AssumePointerMoveStoppedMilliseconds = 40`.
|
|
||||||
* `androidx/compose/ui/input/pointer/util/PlatformVelocityTracker.kt` --
|
|
||||||
`Lsq2VelocityTracker`, which is what the 2D `VelocityTracker` delegates to.
|
|
||||||
* `androidx/compose/ui/input/pointer/util/PlatformVelocityTracker.android.kt`
|
|
||||||
-- the `AndroidComposeUiFlags.isFrameworkVelocityTrackerEnabled` fork.
|
|
||||||
* `androidx/compose/ui/AndroidComposeUiFlags.android.kt` -- that flag's
|
|
||||||
default, which is `false`.
|
|
||||||
* `androidx/compose/foundation/gestures/Draggable.kt` -- `sendDragStart` /
|
|
||||||
`sendDragEvent` / `sendDragStopped`, i.e. *which* samples a touch drag
|
|
||||||
feeds the tracker and where the maximum-velocity clamp is applied.
|
|
||||||
* `androidx/compose/foundation/gestures/DifferentialVelocityTracker.kt` and
|
|
||||||
`NonTouchScrollingLogic.kt` -- the Impulse strategy's only caller.
|
|
||||||
* `androidx/compose/foundation/gestures/Scrollable.kt` --
|
|
||||||
`DefaultFlingBehavior.performFling`, for the minimum-velocity question.
|
|
||||||
|
|
||||||
**Which strategy a touch fling actually uses, since this was the surprise.**
|
|
||||||
`Strategy.Impulse` is *not* it. `scrollable`/`draggable` release through
|
|
||||||
`DragGestureNode.sendDragStopped`, which calls the 2D `VelocityTracker`; on
|
|
||||||
Android that is `Lsq2VelocityTracker` (the framework-tracker flag defaults to
|
|
||||||
false), which is two `VelocityTracker1D(strategy = Lsq2)` -- a degree-2
|
|
||||||
least-squares fit over **absolute positions**, whose velocity is the fitted
|
|
||||||
polynomial's derivative at the newest sample. Impulse is reached only through
|
|
||||||
`DifferentialVelocityTracker`, whose sole caller is `NonTouchScrollingLogic`:
|
|
||||||
mouse wheel and trackpad, never a finger. So this script transcribes Lsq2 and
|
|
||||||
iris ports Lsq2. `calculate_impulse_velocity` is here anyway, unused by the
|
|
||||||
printed points, because ruling it out by reading is cheaper than ruling it out
|
|
||||||
again next time somebody remembers "Compose uses impulse".
|
|
||||||
|
|
||||||
**Which samples a touch drag feeds it.** `sendDragStart` adds the DOWN change;
|
|
||||||
every subsequent MOVE, historical samples included, is added by `sendDragEvent`.
|
|
||||||
The **UP position is never added**: `Lsq2VelocityTracker.addPointerInputChange`
|
|
||||||
wraps its two `addPosition` calls in `if (!event.changedToUpIgnoreConsumed())`,
|
|
||||||
and all the UP branch does is reset the tracker when more than 40ms have passed
|
|
||||||
since the last MOVE (b/238654963). So a finger that stops before lifting reads
|
|
||||||
as a stop, not as a decelerating tail. Positions are the raw event positions,
|
|
||||||
so the touch slop is inside the motion the tracker sees even though the list
|
|
||||||
never scrolled by it.
|
|
||||||
|
|
||||||
Two of Compose's samples iris does *not* reproduce, both noted rather than
|
|
||||||
copied: pre-slop MOVEs (iris's `DragArbiter` is `Undecided` then too, so it
|
|
||||||
feeds none either -- these agree), and the single MOVE that *crosses* the slop,
|
|
||||||
which Compose drops because `sendDragStart` adds only the DOWN. iris feeds that
|
|
||||||
one, since it is a real measured position and dropping it would be copying a
|
|
||||||
quirk of where Compose happens to split its state machine.
|
|
||||||
|
|
||||||
**The clamps.** Maximum: `sendDragStopped` passes
|
|
||||||
`LocalViewConfiguration.maximumFlingVelocity`, which on Android is
|
|
||||||
`ViewConfiguration.getScaledMaximumFlingVelocity()` -- 8000 dp/s. Minimum:
|
|
||||||
there is **none** on this path. `ViewConfiguration.minimumFlingVelocity`
|
|
||||||
exists in Compose's `ViewConfiguration` interface but its only use in either
|
|
||||||
artifact is `NestedScrollInteropConnection`, for View interop.
|
|
||||||
`DefaultFlingBehavior.performFling` guards with `abs(initialVelocity) > 1f`
|
|
||||||
and says why in its own comment: "we need it since spline curve gives us
|
|
||||||
NaNs". 1 px/s, not 50 dp/s.
|
|
||||||
|
|
||||||
Run it with no arguments; it prints the sample sets and the velocities the
|
|
||||||
Rust tests assert on.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
|
|
||||||
HISTORY_SIZE = 20
|
|
||||||
HORIZON_MILLISECONDS = 100.0
|
|
||||||
ASSUME_POINTER_MOVE_STOPPED_MILLISECONDS = 40.0
|
|
||||||
MIN_SAMPLE_SIZE_LSQ2 = 3
|
|
||||||
|
|
||||||
MAXIMUM_FLING_VELOCITY_DP_S = 8000.0
|
|
||||||
# DefaultFlingBehavior.performFling's own threshold, in the units of the
|
|
||||||
# positions fed to the tracker -- pixels per second here.
|
|
||||||
FLING_MINIMUM_PX_S = 1.0
|
|
||||||
|
|
||||||
|
|
||||||
def poly_fit_least_squares(x, y, sample_count, degree):
|
|
||||||
"""`polyFitLeastSquares`: Gram-Schmidt QR, coefficients low order first."""
|
|
||||||
if degree < 1:
|
|
||||||
raise ValueError("The degree must be at positive integer")
|
|
||||||
if sample_count == 0:
|
|
||||||
raise ValueError("At least one point must be provided")
|
|
||||||
|
|
||||||
truncated_degree = sample_count - 1 if degree >= sample_count else degree
|
|
||||||
m = sample_count
|
|
||||||
n = truncated_degree + 1
|
|
||||||
|
|
||||||
a = [[0.0] * m for _ in range(n)]
|
|
||||||
for h in range(m):
|
|
||||||
a[0][h] = 1.0
|
|
||||||
for i in range(1, n):
|
|
||||||
a[i][h] = a[i - 1][h] * x[h]
|
|
||||||
|
|
||||||
q = [[0.0] * m for _ in range(n)]
|
|
||||||
r = [[0.0] * n for _ in range(n)]
|
|
||||||
for j in range(n):
|
|
||||||
w = q[j]
|
|
||||||
w[:] = a[j][:m]
|
|
||||||
for i in range(j):
|
|
||||||
z = q[i]
|
|
||||||
dot = sum(w[h] * z[h] for h in range(m))
|
|
||||||
for h in range(m):
|
|
||||||
w[h] -= dot * z[h]
|
|
||||||
norm = math.sqrt(sum(v * v for v in w))
|
|
||||||
inverse_norm = 1.0 / max(norm, 1e-6)
|
|
||||||
for h in range(m):
|
|
||||||
w[h] *= inverse_norm
|
|
||||||
for i in range(n):
|
|
||||||
r[j][i] = 0.0 if i < j else sum(w[h] * a[i][h] for h in range(m))
|
|
||||||
|
|
||||||
coefficients = [0.0] * n
|
|
||||||
for i in range(n - 1, -1, -1):
|
|
||||||
c = sum(q[i][h] * y[h] for h in range(m))
|
|
||||||
for j in range(n - 1, i, -1):
|
|
||||||
c -= r[i][j] * coefficients[j]
|
|
||||||
coefficients[i] = c / r[i][i]
|
|
||||||
return coefficients
|
|
||||||
|
|
||||||
|
|
||||||
def kinetic_energy_to_velocity(kinetic_energy):
|
|
||||||
sign = 0.0 if kinetic_energy == 0.0 else math.copysign(1.0, kinetic_energy)
|
|
||||||
return sign * math.sqrt(2 * abs(kinetic_energy))
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_impulse_velocity(data_points, time, sample_count, is_data_differential):
|
|
||||||
"""`calculateImpulseVelocity` -- not on the touch path; see the module doc."""
|
|
||||||
work = 0.0
|
|
||||||
start = sample_count - 1
|
|
||||||
next_time = time[start]
|
|
||||||
for i in range(start, 0, -1):
|
|
||||||
current_time = next_time
|
|
||||||
next_time = time[i - 1]
|
|
||||||
if current_time == next_time:
|
|
||||||
continue
|
|
||||||
if is_data_differential:
|
|
||||||
delta = -data_points[i - 1]
|
|
||||||
else:
|
|
||||||
delta = data_points[i] - data_points[i - 1]
|
|
||||||
v_curr = delta / (current_time - next_time)
|
|
||||||
v_prev = kinetic_energy_to_velocity(work)
|
|
||||||
work += (v_curr - v_prev) * abs(v_curr)
|
|
||||||
if i == start:
|
|
||||||
work = work * 0.5
|
|
||||||
return kinetic_energy_to_velocity(work)
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_velocity(samples):
|
|
||||||
"""`VelocityTracker1D.calculateVelocity` with `Strategy.Lsq2`.
|
|
||||||
|
|
||||||
`samples` is `(time_millis, position)` oldest first, at most the last
|
|
||||||
`HISTORY_SIZE` of which the circular buffer would still be holding.
|
|
||||||
Returns units per second.
|
|
||||||
"""
|
|
||||||
held = samples[-HISTORY_SIZE:]
|
|
||||||
if not held:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
data_points = []
|
|
||||||
time = []
|
|
||||||
newest_time, _ = held[-1]
|
|
||||||
previous_time = newest_time
|
|
||||||
for sample_time, sample_position in reversed(held):
|
|
||||||
age = float(newest_time - sample_time)
|
|
||||||
delta = abs(float(sample_time - previous_time))
|
|
||||||
# Lsq2 walks back sample to sample; only the non-differential
|
|
||||||
# Impulse branch compares every sample against the newest one.
|
|
||||||
previous_time = sample_time
|
|
||||||
if age > HORIZON_MILLISECONDS or delta > ASSUME_POINTER_MOVE_STOPPED_MILLISECONDS:
|
|
||||||
break
|
|
||||||
data_points.append(sample_position)
|
|
||||||
time.append(-age)
|
|
||||||
if len(data_points) == HISTORY_SIZE:
|
|
||||||
break
|
|
||||||
|
|
||||||
if len(data_points) < MIN_SAMPLE_SIZE_LSQ2:
|
|
||||||
return 0.0
|
|
||||||
try:
|
|
||||||
coefficients = poly_fit_least_squares(time, data_points, len(data_points), 2)
|
|
||||||
except ValueError:
|
|
||||||
return 0.0
|
|
||||||
# The 2nd coefficient is the fitted polynomial's derivative at x = 0,
|
|
||||||
# which is the newest sample's timestamp. units/ms -> units/s.
|
|
||||||
return coefficients[1] * 1000.0
|
|
||||||
|
|
||||||
|
|
||||||
def clamped(velocity, maximum):
|
|
||||||
"""`VelocityTracker1D.calculateVelocity(maximumVelocity)`."""
|
|
||||||
if velocity == 0.0 or math.isnan(velocity):
|
|
||||||
return 0.0
|
|
||||||
return min(velocity, maximum) if velocity > 0 else max(velocity, -maximum)
|
|
||||||
|
|
||||||
|
|
||||||
def average(samples):
|
|
||||||
"""The estimator being replaced: total motion over the span."""
|
|
||||||
if len(samples) < 2:
|
|
||||||
return 0.0
|
|
||||||
span = (samples[-1][0] - samples[0][0]) / 1000.0
|
|
||||||
if span <= 0.0:
|
|
||||||
return 0.0
|
|
||||||
return (samples[-1][1] - samples[0][1]) / span
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 1. `app/touch/flick-120hz.touch`, as `DragGesture` feeds it:
|
|
||||||
# the DOWN position, then one position per MOVE. The UP at t=20 adds no
|
|
||||||
# sample (see the module doc), which is why the finger sitting still for its
|
|
||||||
# last 4ms does not drag the estimate down. y only; the flick is vertical.
|
|
||||||
FLICK_120HZ = [(0, 1000.0), (4, 1040.0), (8, 1086.0), (12, 1138.0), (16, 1196.0)]
|
|
||||||
|
|
||||||
# 2. A steady drag: 5px every 10ms for 100ms. A constant-velocity fit and an
|
|
||||||
# average must agree here -- this is the case that cannot tell the two
|
|
||||||
# estimators apart, which is why it is not the only one.
|
|
||||||
STEADY_DRAG = [(i * 10, float(i * 5)) for i in range(11)]
|
|
||||||
|
|
||||||
# 3. A flick that accelerates into the release: 10ms apart, deltas doubling.
|
|
||||||
# This is the case the average gets wrong, and the negative control for
|
|
||||||
# the port -- reverting to the average must fail this test and only this
|
|
||||||
# kind of test.
|
|
||||||
ACCELERATING_FLICK = [(0, 0.0), (10, 2.0), (20, 6.0), (30, 14.0), (40, 30.0), (50, 54.0)]
|
|
||||||
|
|
||||||
# 4. The two edges of the sample walk, checked here so the Rust asserts
|
|
||||||
# Compose's answer rather than iris's own reading of the rule.
|
|
||||||
# (a) An old, fast burst outside the 100ms horizon, then a slow steady
|
|
||||||
# drag: the burst must not leak into the estimate.
|
|
||||||
OLD_BURST_THEN_STEADY = [(0, 0.0)] + [(10 + i * 10, 1000.0 + i) for i in range(11)]
|
|
||||||
STOPPED_BEFORE_RELEASE = [(0, 0.0), (4, 40.0), (8, 90.0), (12, 150.0), (60, 152.0)]
|
|
||||||
|
|
||||||
TWO_MOVE_FRAMES = [(0, 0.0), (8, 100.0), (16, 220.0)]
|
|
||||||
# ... and one move frame, which Compose cannot fit either.
|
|
||||||
ONE_MOVE_FRAME = [(0, 0.0), (8, 100.0)]
|
|
||||||
|
|
||||||
PHONE_DENSITY = 2.55
|
|
||||||
|
|
||||||
|
|
||||||
def report(name, samples):
|
|
||||||
v = calculate_velocity(samples)
|
|
||||||
print(f"{name}:")
|
|
||||||
print(f" samples (t_ms, position): {samples}")
|
|
||||||
print(f" Lsq2 (Compose's touch path): {v:.4f} px/s")
|
|
||||||
print(f" average (the old estimator): {average(samples):.4f} px/s")
|
|
||||||
print(f" impulse (non-touch, for ref): ", end="")
|
|
||||||
held = list(reversed(samples[-HISTORY_SIZE:]))
|
|
||||||
newest = held[0][0]
|
|
||||||
print(
|
|
||||||
f"{calculate_impulse_velocity([p for _, p in held], [-(newest - t) for t, _ in held], len(held), False) * 1000.0:.4f} px/s"
|
|
||||||
)
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("Compose 1.12.0 touch velocity: VelocityTracker1D, Strategy.Lsq2,")
|
|
||||||
print("non-differential (positions), HistorySize=20, Horizon=100ms,")
|
|
||||||
print("AssumePointerMoveStopped=40ms, minSampleSize=3.\n")
|
|
||||||
report("flick-120hz.touch", FLICK_120HZ)
|
|
||||||
report("steady drag (5px/10ms)", STEADY_DRAG)
|
|
||||||
report("accelerating flick (deltas 2,4,8,16,24 per 10ms)", ACCELERATING_FLICK)
|
|
||||||
|
|
||||||
report("old burst then steady 1px/10ms", OLD_BURST_THEN_STEADY)
|
|
||||||
report("stopped 48ms before release", STOPPED_BEFORE_RELEASE)
|
|
||||||
report("press and two move frames", TWO_MOVE_FRAMES)
|
|
||||||
report("press and one move frame", ONE_MOVE_FRAME)
|
|
||||||
|
|
||||||
print("Clamps:")
|
|
||||||
print(f" maximum: {MAXIMUM_FLING_VELOCITY_DP_S} dp/s")
|
|
||||||
print(
|
|
||||||
f" = {MAXIMUM_FLING_VELOCITY_DP_S * PHONE_DENSITY:.1f} px/s at the phone's density {PHONE_DENSITY}"
|
|
||||||
)
|
|
||||||
print(f" minimum: none on the fling path; DefaultFlingBehavior skips |v| <= {FLING_MINIMUM_PX_S} px/s")
|
|
||||||
print()
|
|
||||||
print("Two samples only (a press and one move, the phone's 120Hz worst case):")
|
|
||||||
print(f" Lsq2 needs 3 and answers {calculate_velocity(FLICK_120HZ[:2]):.4f} px/s")
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Turns `iris::input` debug lines -- from a phone's diagnostics report, or
|
|
||||||
from a report the layer-1 harness produced with tracing on
|
|
||||||
(`iris::diagnostics::set_trace(true)`) -- back into a `TouchScript` file
|
|
||||||
`iris::harness::Harness::replay` can play back at layer 1.
|
|
||||||
|
|
||||||
Why this exists: `docs/RUST.md`'s "Three test layers" box says the cheapest
|
|
||||||
layer that can answer a question wins, and a gesture that misbehaves on
|
|
||||||
Iris's phone is otherwise only describable in words. `iris::sense::
|
|
||||||
log_input_event`'s one line per platform event (Android's on_touch_event
|
|
||||||
once per `MotionEvent`, with historical samples inline; winit's once per
|
|
||||||
pointer `WindowEvent`; the harness's `touch`, once per script line) already
|
|
||||||
carries everything a `.touch` file's `t_ms action x y` needs -- this just
|
|
||||||
reads it back out and reconstructs the samples in order, expanding each
|
|
||||||
event's inline historical samples into their own `move` lines first (they
|
|
||||||
are always intermediate positions of a move, and Android documents them as
|
|
||||||
oldest first, which is also the order they appear in the line).
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
report_to_touch.py < report.txt > replay.touch
|
|
||||||
report_to_touch.py report.txt > replay.touch
|
|
||||||
|
|
||||||
Only lines containing "iris input: action=..." are read; everything else in
|
|
||||||
the report (insets, frame timings, drag-release summaries) is ignored, so
|
|
||||||
this can be pointed at Copy report's whole clipboard text directly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# The message half of `sense::log_input_event`'s format string, prefix-
|
|
||||||
# agnostic: a real report line also carries the ring's own
|
|
||||||
# `HH:MM:SS.mmm LEVEL target:` header (`LogLine::format`) or, forwarded
|
|
||||||
# through `ai_server::client_log`, a `[<source> <clock> #<seq>]` tag ahead
|
|
||||||
# of that -- neither of which this needs to understand, since `search`
|
|
||||||
# (not `match`) finds the marker wherever it starts.
|
|
||||||
LINE_RE = re.compile(
|
|
||||||
r"iris input: action=(?P<action>\w+) x=(?P<x>-?[0-9.]+) y=(?P<y>-?[0-9.]+) "
|
|
||||||
r"t=(?P<t>[0-9]+)ms history=(?P<hist>[0-9]+)(?P<rest>.*)$"
|
|
||||||
)
|
|
||||||
HIST_RE = re.compile(r"(?P<t>[0-9]+):(?P<x>-?[0-9.]+),(?P<y>-?[0-9.]+)")
|
|
||||||
|
|
||||||
|
|
||||||
def _fmt(value: float) -> str:
|
|
||||||
"""The number as `TouchScript::parse`'s own `f32::parse` would round-trip
|
|
||||||
it -- an integer without a trailing `.0` where the source was one
|
|
||||||
(every coordinate here is a physical pixel), `{:g}` otherwise so a
|
|
||||||
fractional value from a real device is not silently truncated."""
|
|
||||||
if value == int(value):
|
|
||||||
return str(int(value))
|
|
||||||
return f"{value:g}"
|
|
||||||
|
|
||||||
|
|
||||||
def convert(lines):
|
|
||||||
"""Every `iris::input` line, oldest first, expanded to one `(t_ms,
|
|
||||||
action, x, y)` tuple per touch sample -- a historical sample is always
|
|
||||||
an intermediate `move`, and the event's own sample keeps its real
|
|
||||||
action (`down`/`move`/`up`/`cancel`)."""
|
|
||||||
rows = []
|
|
||||||
for line in lines:
|
|
||||||
m = LINE_RE.search(line)
|
|
||||||
if not m:
|
|
||||||
continue
|
|
||||||
hist_count = int(m.group("hist"))
|
|
||||||
hist_matches = list(HIST_RE.finditer(m.group("rest")))
|
|
||||||
if len(hist_matches) != hist_count:
|
|
||||||
print(
|
|
||||||
f"report_to_touch: {line.strip()!r} says history={hist_count} but "
|
|
||||||
f"holds {len(hist_matches)} samples -- skipped",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
for hm in hist_matches:
|
|
||||||
rows.append(
|
|
||||||
(int(hm.group("t")), "move", float(hm.group("x")), float(hm.group("y")))
|
|
||||||
)
|
|
||||||
rows.append(
|
|
||||||
(int(m.group("t")), m.group("action"), float(m.group("x")), float(m.group("y")))
|
|
||||||
)
|
|
||||||
return rows
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if len(sys.argv) > 2:
|
|
||||||
print("usage: report_to_touch.py [report.txt] < report.txt", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
text = open(sys.argv[1]) if len(sys.argv) == 2 else sys.stdin
|
|
||||||
for t_ms, action, x, y in convert(text):
|
|
||||||
print(f"{t_ms} {action} {_fmt(x)} {_fmt(y)}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Runs iris's on-demand benchmark suite.
|
|
||||||
# Never run by `cargo test`; run this by hand or before/after a layout
|
|
||||||
# change. Always release -- see AGENTS.md's own rule against reading a
|
|
||||||
# frame time from a debug build.
|
|
||||||
#
|
|
||||||
# ./scripts/run-bench.sh # everything
|
|
||||||
# ./scripts/run-bench.sh list # just the CPU-only message-list scenarios
|
|
||||||
# ./scripts/run-bench.sh images # just the GPU bind-group-creation scenario
|
|
||||||
set -eu
|
|
||||||
scripts=$(cd "$(dirname "$0")" && pwd)
|
|
||||||
root=$(cd "$scripts/.." && pwd)
|
|
||||||
cd "$root"
|
|
||||||
|
|
||||||
what="${1:-all}"
|
|
||||||
|
|
||||||
if [ "$what" = "all" ] || [ "$what" = "list" ]; then
|
|
||||||
echo "=== message_list (CPU-only, no window) ==="
|
|
||||||
cargo bench --bench message_list
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$what" = "all" ] || [ "$what" = "images" ]; then
|
|
||||||
echo "=== bench_images (real wgpu device, via run-headless.sh) ==="
|
|
||||||
timeout 60 "$scripts/run-headless.sh" bench_images --seconds 4 2>&1 | grep "^BENCH_IMAGES"
|
|
||||||
fi
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Run an iris example on this machine, which has no display.
|
|
||||||
#
|
|
||||||
# ./scripts/run-headless.sh tabs [-- cargo args]
|
|
||||||
# ./scripts/run-headless.sh tabs --shot /tmp/tabs.png --seconds 4
|
|
||||||
# ./scripts/run-headless.sh phone --phone --dir ../app --shot /tmp/p.png
|
|
||||||
# ./scripts/run-headless.sh phone --phone --dir ../app \
|
|
||||||
# --replay ../app/touch/flick-120hz.touch --shot /tmp/p.png
|
|
||||||
#
|
|
||||||
# `--dir DIR` names the workspace to build in, defaulting to `iris/`. The
|
|
||||||
# app's examples -- the phone-sized transcript
|
|
||||||
# screen and everything else that is about *this product* -- live in
|
|
||||||
# `app/`, which is a workspace of its own; `replay-touch` is still
|
|
||||||
# built from iris, since it is part of the rig rather than of either app.
|
|
||||||
#
|
|
||||||
# `--phone` is layer 2 of docs/RUST.md's "Three test layers": the output
|
|
||||||
# and the window take Iris's phone's own size and density (1080x2424 at
|
|
||||||
# `content_scale` 2.55, from docs/bench/iris-phone-v2-2026-09-06.md,
|
|
||||||
# carried in `ai_app::ui::fixture::PHONE_*`), and `IRIS_SCALE` hands that
|
|
||||||
# density to iris the way `DisplayMetrics.density` does on Android
|
|
||||||
# (`iris::desktop::content_scale`). So a screenshot from here and one
|
|
||||||
# from the phone are the same layout at the same density, and what
|
|
||||||
# differs is only the renderer. Without it the output stays desktop-
|
|
||||||
# shaped, which is what every other example wants.
|
|
||||||
#
|
|
||||||
# `--replay FILE` drives one of the `.touch` recordings the headless
|
|
||||||
# tests use (`app/touch/`) into the window through
|
|
||||||
# `rig-input`'s `replay-touch` -- one recording, both layers. With
|
|
||||||
# `--shot` it also writes `<shot>-before.png` from just before the
|
|
||||||
# gesture, since "the list moved" is a claim about two pictures.
|
|
||||||
#
|
|
||||||
# `--bin` runs a real crate binary instead of an example (E4's
|
|
||||||
# `ai-app-desktop`, which is a window a person runs, not a demo) --
|
|
||||||
# `cargo build --bin NAME` instead of `--example NAME`, and
|
|
||||||
# `target/debug/NAME` instead of `target/debug/examples/NAME`. Its own
|
|
||||||
# argv (the CLI flags a real binary takes, as opposed to `cargo build`'s
|
|
||||||
# own flags after `--`) comes through `$RUN_HEADLESS_ARGS`, word-split on
|
|
||||||
# purpose -- an example never needed one, so there was nowhere to plumb it
|
|
||||||
# through positionally without disturbing the existing `-- cargo args`
|
|
||||||
# convention above.
|
|
||||||
#
|
|
||||||
# The VM has a real GPU and no display (the `this-machine-graphics` skill
|
|
||||||
# says what it is and how it fails), so what is missing here is only a
|
|
||||||
# compositor to give winit a surface. So: a headless sway, the same trick
|
|
||||||
# `emu` uses for the Android emulator, and `grim` to see the result.
|
|
||||||
#
|
|
||||||
# It is deliberately *not* `emu`'s compositor. sway tiles, so adding a window
|
|
||||||
# to the one an emulator is sitting in resizes that emulator's window, and a
|
|
||||||
# peer session's `emu up` could join at any moment. This one has its own
|
|
||||||
# socket and its own runtime directory and goes away with the machine.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
scripts=$(cd "$(dirname "$0")" && pwd)
|
|
||||||
root=$(cd "$scripts/.." && pwd)
|
|
||||||
workdir="$root"
|
|
||||||
cd "$root"
|
|
||||||
run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless"
|
|
||||||
seconds=3
|
|
||||||
shot=""
|
|
||||||
replay=""
|
|
||||||
example=""
|
|
||||||
kind=example
|
|
||||||
phone=no
|
|
||||||
|
|
||||||
# The phone Iris runs the bench on. Not typed from memory: these are
|
|
||||||
# `ai_app::ui::fixture::PHONE_WIDTH`/`PHONE_HEIGHT`/`PHONE_SCALE`, which
|
|
||||||
# in turn come from her own reports -- keep the three in step.
|
|
||||||
PHONE_MODE=1080x2424@120Hz
|
|
||||||
PHONE_SCALE=2.55
|
|
||||||
DESKTOP_MODE=1920x1200@60Hz
|
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--shot) shot=$2; shift 2 ;;
|
|
||||||
--seconds) seconds=$2; shift 2 ;;
|
|
||||||
--bin) kind=bin; shift ;;
|
|
||||||
--phone) phone=yes; shift ;;
|
|
||||||
--replay) replay=$2; shift 2 ;;
|
|
||||||
--dir) workdir=$(cd "$2" && pwd); shift 2 ;;
|
|
||||||
--) shift; break ;;
|
|
||||||
*) example=$1; shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
[ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--phone] [--dir DIR] [--replay TOUCH] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; }
|
|
||||||
[ -z "$replay" ] || [ -f "$replay" ] || { echo "run-headless: no touch script at $replay" >&2; exit 2; }
|
|
||||||
|
|
||||||
mkdir -p "$run"
|
|
||||||
export SWAYSOCK="$run/sway.sock"
|
|
||||||
|
|
||||||
# Named rather than left to sway's pid-based default, so a second run reuses
|
|
||||||
# this compositor instead of starting another beside it.
|
|
||||||
if ! swaymsg -t get_version >/dev/null 2>&1; then
|
|
||||||
rm -f "$SWAYSOCK"
|
|
||||||
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 LIBSEAT_BACKEND=noop \
|
|
||||||
setsid sway -c "$scripts/headless.conf" >"$run/sway.log" 2>&1 &
|
|
||||||
i=0
|
|
||||||
while [ $i -lt 20 ]; do
|
|
||||||
swaymsg -t get_version >/dev/null 2>&1 && break
|
|
||||||
i=$((i + 1)); sleep 0.5
|
|
||||||
done
|
|
||||||
swaymsg -t get_version >/dev/null 2>&1 || {
|
|
||||||
echo "run-headless: compositor did not start; see $run/sway.log" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$run/display"
|
|
||||||
swaymsg exec -- "sh -c 'printf %s \"\$WAYLAND_DISPLAY\" > $run/display'" >/dev/null
|
|
||||||
i=0
|
|
||||||
while [ $i -lt 20 ]; do
|
|
||||||
[ -s "$run/display" ] && break
|
|
||||||
i=$((i + 1)); sleep 0.5
|
|
||||||
done
|
|
||||||
[ -s "$run/display" ] || { echo "run-headless: could not read WAYLAND_DISPLAY" >&2; exit 1; }
|
|
||||||
WAYLAND_DISPLAY=$(cat "$run/display")
|
|
||||||
export WAYLAND_DISPLAY
|
|
||||||
|
|
||||||
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
|
|
||||||
|
|
||||||
# Set every run rather than only when it changes: this compositor is
|
|
||||||
# reused across runs (see the socket comment above), so a desktop-shaped
|
|
||||||
# run after a phone-shaped one would otherwise inherit the phone's output
|
|
||||||
# and silently screenshot the wrong size.
|
|
||||||
if [ "$phone" = yes ]; then
|
|
||||||
mode=$PHONE_MODE
|
|
||||||
export IRIS_SCALE="$PHONE_SCALE"
|
|
||||||
echo "run-headless: phone-shaped output $PHONE_MODE at IRIS_SCALE=$PHONE_SCALE" >&2
|
|
||||||
else
|
|
||||||
mode=$DESKTOP_MODE
|
|
||||||
fi
|
|
||||||
swaymsg output HEADLESS-1 mode "$mode" >/dev/null
|
|
||||||
# The extent `replay-touch` positions against, so a script's coordinates
|
|
||||||
# are the output's own pixels.
|
|
||||||
out_w=${mode%x*}
|
|
||||||
out_h=${mode#*x}; out_h=${out_h%@*}
|
|
||||||
|
|
||||||
# Built before the app starts, so a compile error is not reported as a
|
|
||||||
# window that failed to move.
|
|
||||||
[ -z "$replay" ] || (cd "$root" && cargo build --bin replay-touch -p rig-input) >&2
|
|
||||||
|
|
||||||
cd "$workdir"
|
|
||||||
if [ "$kind" = bin ]; then
|
|
||||||
cargo build --bin "$example" "$@" >&2
|
|
||||||
bin="$workdir/target/debug/$example"
|
|
||||||
else
|
|
||||||
cargo build --example "$example" "$@" >&2
|
|
||||||
bin="$workdir/target/debug/examples/$example"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# shellcheck disable=SC2086 -- deliberately word-split: this is the
|
|
||||||
# binary's own argv, not a single path.
|
|
||||||
"$bin" ${RUN_HEADLESS_ARGS:-} >"$run/$example.log" 2>&1 &
|
|
||||||
pid=$!
|
|
||||||
trap 'kill "$pid" 2>/dev/null || true' EXIT INT TERM
|
|
||||||
|
|
||||||
# Wait for the window to be mapped rather than for a number of seconds. A
|
|
||||||
# fixed sleep took an all-black screenshot the first time this ran, when sway
|
|
||||||
# had started in the same invocation and had not composited its output yet --
|
|
||||||
# which is indistinguishable from an app that draws nothing.
|
|
||||||
i=0
|
|
||||||
while [ $i -lt 40 ]; do
|
|
||||||
kill -0 "$pid" 2>/dev/null || break
|
|
||||||
swaymsg -t get_tree --raw 2>/dev/null | grep -q "\"pid\":$pid," && break
|
|
||||||
i=$((i + 1)); sleep 0.25
|
|
||||||
done
|
|
||||||
|
|
||||||
i=0
|
|
||||||
while [ $i -lt "$((seconds * 2))" ]; do
|
|
||||||
kill -0 "$pid" 2>/dev/null || break
|
|
||||||
i=$((i + 1)); sleep 0.5
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -n "$replay" ] && kill -0 "$pid" 2>/dev/null; then
|
|
||||||
if [ -n "$shot" ]; then
|
|
||||||
grim "${shot%.png}-before.png"
|
|
||||||
echo "run-headless: wrote ${shot%.png}-before.png (before the gesture)" >&2
|
|
||||||
fi
|
|
||||||
"$root/target/debug/replay-touch" "$out_w" "$out_h" "$replay"
|
|
||||||
# A fling outlives the finger: the gesture's own last sample is not
|
|
||||||
# when the list stops. Long enough for Android's spline to settle
|
|
||||||
# (`FlingCalculator::duration` tops out around a second and a half).
|
|
||||||
sleep 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if kill -0 "$pid" 2>/dev/null; then
|
|
||||||
[ -n "$shot" ] && grim "$shot" && echo "run-headless: wrote $shot" >&2
|
|
||||||
kill "$pid" 2>/dev/null || true
|
|
||||||
wait "$pid" 2>/dev/null || true
|
|
||||||
status=0
|
|
||||||
else
|
|
||||||
wait "$pid" 2>/dev/null || status=$?
|
|
||||||
echo "run-headless: $example exited early (status ${status:-0})" >&2
|
|
||||||
status=${status:-1}
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "--- $example output ---" >&2
|
|
||||||
cat "$run/$example.log" >&2
|
|
||||||
exit "$status"
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
use crate::layout_tests::TestRsc;
|
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_named_widget_reaches_the_tree_with_its_role_and_bounds() {
|
|
||||||
let mut rsc = TestRsc { ui: Ui::default() };
|
|
||||||
let leaf: WeakWidget<Rect> = rect(PaintId::WHITE).label("Add task").add(&mut rsc);
|
|
||||||
let root = leaf.upgrade(&mut rsc).any();
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
render.resize((800.0, 600.0));
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
|
|
||||||
let mut access = AccessTree::new();
|
|
||||||
let update = access
|
|
||||||
.update(rsc.widgets(), &render, &rsc)
|
|
||||||
.expect("a first draw with a named widget must produce a tree");
|
|
||||||
|
|
||||||
assert_eq!(update.nodes.len(), 2);
|
|
||||||
let (_, node) = update
|
|
||||||
.nodes
|
|
||||||
.iter()
|
|
||||||
.find(|(_, n)| n.role() != accesskit::Role::Window)
|
|
||||||
.expect("the named widget's own node");
|
|
||||||
assert_eq!(node.label(), Some("Add task"));
|
|
||||||
assert_eq!(node.role(), accesskit::Role::Unknown);
|
|
||||||
let bounds = node.bounds().expect("a drawn widget reports its bounds");
|
|
||||||
let region = render
|
|
||||||
.window_region(&leaf, &rsc)
|
|
||||||
.expect("the widget is active after render.update");
|
|
||||||
assert_eq!(bounds.x0, region.top_left.x as f64);
|
|
||||||
assert_eq!(bounds.y0, region.top_left.y as f64);
|
|
||||||
assert_eq!(bounds.x1, region.bot_right.x as f64);
|
|
||||||
assert_eq!(bounds.y1, region.bot_right.y as f64);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_widget_with_no_label_never_reaches_the_tree() {
|
|
||||||
let mut rsc = TestRsc { ui: Ui::default() };
|
|
||||||
let root = rsc.ui.widgets.add_strong(rect(PaintId::WHITE));
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
render.resize((800.0, 600.0));
|
|
||||||
render.update(&root.any(), &mut rsc);
|
|
||||||
|
|
||||||
let mut access = AccessTree::new();
|
|
||||||
assert!(
|
|
||||||
access.update(rsc.widgets(), &render, &rsc).is_none(),
|
|
||||||
"no widget was ever `.label()`ed, so there is nothing to report -- \
|
|
||||||
not even an empty tree change"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bounds_follow_a_moved_widget_and_updates_stay_incremental() {
|
|
||||||
let mut rsc = TestRsc { ui: Ui::default() };
|
|
||||||
let leaf: WeakWidget<Rect> = rect(PaintId::WHITE).label("thing").add(&mut rsc);
|
|
||||||
let leaf_strong = leaf.upgrade(&mut rsc).any();
|
|
||||||
let offset = rsc.ui.widgets.add_strong(Offset {
|
|
||||||
inner: leaf_strong,
|
|
||||||
amt: UiVec2::ZERO,
|
|
||||||
});
|
|
||||||
let offset_id = offset.weak();
|
|
||||||
let root = offset.any();
|
|
||||||
let mut render = UiRenderState::new();
|
|
||||||
render.resize((800.0, 600.0));
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
|
|
||||||
let mut access = AccessTree::new();
|
|
||||||
access
|
|
||||||
.update(rsc.widgets(), &render, &rsc)
|
|
||||||
.expect("the first draw is always a change");
|
|
||||||
assert_eq!(access.take_rebuilds(), 1);
|
|
||||||
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
assert!(access.update(rsc.widgets(), &render, &rsc).is_none());
|
|
||||||
assert_eq!(access.take_rebuilds(), 0);
|
|
||||||
|
|
||||||
let before = render
|
|
||||||
.window_region(&leaf, &rsc)
|
|
||||||
.expect("active before the move");
|
|
||||||
rsc.ui.widgets.get_mut(&offset_id).unwrap().amt = UiVec2::abs(Vec2::new(50.0, 0.0));
|
|
||||||
render.update(&root, &mut rsc);
|
|
||||||
let update = access
|
|
||||||
.update(rsc.widgets(), &render, &rsc)
|
|
||||||
.expect("a moved named widget is a change");
|
|
||||||
assert_eq!(access.take_rebuilds(), 1);
|
|
||||||
|
|
||||||
let after = render
|
|
||||||
.window_region(&leaf, &rsc)
|
|
||||||
.expect("still active after the move");
|
|
||||||
assert!(
|
|
||||||
after.top_left.x > before.top_left.x,
|
|
||||||
"the leaf's reported bounds must move right along with its offset"
|
|
||||||
);
|
|
||||||
|
|
||||||
let (_, node) = update
|
|
||||||
.nodes
|
|
||||||
.iter()
|
|
||||||
.find(|(_, n)| n.role() != accesskit::Role::Window)
|
|
||||||
.unwrap();
|
|
||||||
let bounds = node.bounds().unwrap();
|
|
||||||
assert_eq!(bounds.x0, after.top_left.x as f64);
|
|
||||||
}
|
|
||||||
Loaded 100 of 175 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user