Compare commits
10 Commits
6dcc2d0d00
...
profiling
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5ae067bc2 | ||
|
|
4347232b98 | ||
|
|
ccc3984b01 | ||
|
|
23d9f02e3a | ||
|
|
d9ac6bfeb8 | ||
|
|
056f356065 | ||
|
|
1a083ee12c | ||
|
|
bfda24ad0a | ||
|
|
67400f1499 | ||
|
|
861bf63621 |
79
Cargo.lock
generated
79
Cargo.lock
generated
@@ -1359,10 +1359,38 @@ name = "objc2-core-foundation"
|
|||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"block2",
|
||||||
|
"dispatch2",
|
||||||
|
"libc",
|
||||||
|
"objc2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-graphics"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
"objc2",
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-io-surface",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-video"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-io-surface",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1376,6 +1404,19 @@ name = "objc2-foundation"
|
|||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"block2",
|
||||||
|
"libc",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-io-surface"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"objc2",
|
"objc2",
|
||||||
@@ -1401,8 +1442,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-core-video",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"objc2-metal",
|
"objc2-metal",
|
||||||
]
|
]
|
||||||
@@ -1883,11 +1928,28 @@ dependencies = [
|
|||||||
"ruin-runtime",
|
"ruin-runtime",
|
||||||
"ruin_reactivity",
|
"ruin_reactivity",
|
||||||
"ruin_ui",
|
"ruin_ui",
|
||||||
|
"ruin_ui_platform_macos",
|
||||||
"ruin_ui_platform_wayland",
|
"ruin_ui_platform_wayland",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruin_perf"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruin_perf_scenarios"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"ruin_perf",
|
||||||
|
"ruin_ui",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruin_reactivity"
|
name = "ruin_reactivity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1912,6 +1974,23 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruin_ui_platform_macos"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation",
|
||||||
|
"objc2-quartz-core",
|
||||||
|
"raw-window-handle",
|
||||||
|
"raw-window-metal",
|
||||||
|
"ruin-runtime",
|
||||||
|
"ruin_ui",
|
||||||
|
"ruin_ui_renderer_wgpu",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruin_ui_platform_wayland"
|
name = "ruin_ui_platform_wayland"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
15
Cargo.toml
15
Cargo.toml
@@ -1,3 +1,18 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["lib/*", "examples/*"]
|
members = ["lib/*", "examples/*"]
|
||||||
|
default-members = [
|
||||||
|
"lib/reactivity",
|
||||||
|
"lib/reactivity_parsing",
|
||||||
|
"lib/ruin_app",
|
||||||
|
"lib/ruin_app_proc_macros",
|
||||||
|
"lib/runtime",
|
||||||
|
"lib/runtime_proc_macros",
|
||||||
|
"lib/perf",
|
||||||
|
"lib/ui",
|
||||||
|
"lib/ui_platform_macos",
|
||||||
|
"lib/ui_platform_wayland",
|
||||||
|
"lib/ui_renderer_wgpu",
|
||||||
|
"examples/calculator",
|
||||||
|
"examples/perf_scenarios",
|
||||||
|
]
|
||||||
|
|||||||
94
docs/performance-profiling.md
Normal file
94
docs/performance-profiling.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Performance profiling workflow
|
||||||
|
|
||||||
|
RUIN performance investigations should start with reproducible reports before
|
||||||
|
deep profiler sessions. The first-pass tooling is intentionally headless and
|
||||||
|
focused on high-signal app-build, layout, text, scene, and cache counters.
|
||||||
|
|
||||||
|
## Run a scenario
|
||||||
|
|
||||||
|
Use the `ruin_perf_scenarios` package in release mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p ruin_perf_scenarios --release -- \
|
||||||
|
--scenario large-scroll-list \
|
||||||
|
--frames 300 \
|
||||||
|
--output target/ruin-perf/large-scroll-list.json
|
||||||
|
```
|
||||||
|
|
||||||
|
List available scenarios:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p ruin_perf_scenarios -- --list
|
||||||
|
```
|
||||||
|
|
||||||
|
Current scenarios:
|
||||||
|
|
||||||
|
| Scenario | What it stresses |
|
||||||
|
| --- | --- |
|
||||||
|
| `counter` | tiny reactive-style app tree rebuild and layout baseline |
|
||||||
|
| `large-scroll-list` | scroll culling, text cache reuse, layout cache behavior |
|
||||||
|
| `calculator` | realistic small app tree with display, button grid, and history |
|
||||||
|
| `resize-configure` | repeated viewport-size changes and layout reflow |
|
||||||
|
| `large-text-body` | huge wrapped text body, text layout cache behavior, scroll clipping |
|
||||||
|
|
||||||
|
The large text scenario accepts a body-size option:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p ruin_perf_scenarios --release -- \
|
||||||
|
--scenario large-text-body \
|
||||||
|
--frames 100 \
|
||||||
|
--text-kb 1024
|
||||||
|
```
|
||||||
|
|
||||||
|
Each run writes JSON under `target/ruin-perf/` by default and prints a compact
|
||||||
|
summary table to stdout.
|
||||||
|
|
||||||
|
## Interpreting the report
|
||||||
|
|
||||||
|
Start with the timing rows:
|
||||||
|
|
||||||
|
- `app_build`: time spent building the deterministic app tree for the frame.
|
||||||
|
- `layout_total`: full layout snapshot time.
|
||||||
|
- `layout_intrinsic`: intrinsic measurement work inside layout.
|
||||||
|
- `text_prepare` / `text_cache_miss`: text shaping/cache miss work.
|
||||||
|
|
||||||
|
Then inspect counters:
|
||||||
|
|
||||||
|
- High `layout_cache_misses` with low `layout_cache_hits` means the tree or
|
||||||
|
layout constraints are changing in a way that defeats caching.
|
||||||
|
- High `viewport_culled` with low `scene_items` is expected for large scroll
|
||||||
|
lists and means scene culling is working.
|
||||||
|
- High `text_cache_misses` or `text_cache_miss` time usually means text content,
|
||||||
|
bounds, style, or cache keys are changing too much.
|
||||||
|
- Large `scene_items` or `text_glyphs` for tiny workloads suggests excessive UI
|
||||||
|
tree expansion or missing culling.
|
||||||
|
|
||||||
|
## Investigation loop
|
||||||
|
|
||||||
|
1. Reproduce the suspected degenerate workload with the closest scenario.
|
||||||
|
2. Run it in release mode and keep the JSON report.
|
||||||
|
3. Compare the stdout summary with a known-good run or a smaller frame count.
|
||||||
|
4. If one phase dominates, use an OS profiler on that scenario:
|
||||||
|
- macOS: Instruments Time Profiler / Allocations / Metal System Trace.
|
||||||
|
- Linux: `perf`, `samply`, `heaptrack`, or RenderDoc as appropriate.
|
||||||
|
5. After identifying a concrete hot path, add or update a narrow Criterion
|
||||||
|
benchmark before optimizing it.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
The scenario runner is part of the default workspace. The usual repository
|
||||||
|
validation should keep it healthy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test
|
||||||
|
cargo clippy --all-targets -- -D warnings
|
||||||
|
cargo clippy --release --all-targets -- -D warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Current limitations
|
||||||
|
|
||||||
|
This first pass does not automate GUI input, GPU frame capture, or platform
|
||||||
|
presentation traces. Renderer frame counters are exposed for platform-backed
|
||||||
|
runs, but the headless scenario reports focus on app/layout/text degeneracy
|
||||||
|
first. Platform-backed resize/presentation report generation should be added
|
||||||
|
after these reports are useful enough to guide concrete optimization work.
|
||||||
@@ -28,7 +28,11 @@ pub fn Display(state: CalcState) -> impl IntoView {
|
|||||||
let result = state.result.clone();
|
let result = state.result.clone();
|
||||||
move || {
|
move || {
|
||||||
let r = result.get();
|
let r = result.get();
|
||||||
if r.is_empty() { " ".to_string() } else { format!("= {r}") }
|
if r.is_empty() {
|
||||||
|
" ".to_string()
|
||||||
|
} else {
|
||||||
|
format!("= {r}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -282,9 +286,9 @@ fn mix(base: Color, overlay: Color, amount: f32) -> Color {
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn Keypad(state: CalcState) -> impl IntoView {
|
pub fn Keypad(state: CalcState) -> impl IntoView {
|
||||||
let bg_num = Color::rgba(255, 255, 255, 90);
|
let bg_num = Color::rgba(255, 255, 255, 90);
|
||||||
let bg_op = Color::rgba(210, 230, 255, 140);
|
let bg_op = Color::rgba(210, 230, 255, 140);
|
||||||
let bg_eq = Color::rgba(160, 230, 160, 160);
|
let bg_eq = Color::rgba(160, 230, 160, 160);
|
||||||
let bg_fn = Color::rgba(255, 200, 180, 140);
|
let bg_fn = Color::rgba(255, 200, 180, 140);
|
||||||
|
|
||||||
macro_rules! digit {
|
macro_rules! digit {
|
||||||
($ch:expr) => {{
|
($ch:expr) => {{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ mod state;
|
|||||||
use ruin_app::prelude::*;
|
use ruin_app::prelude::*;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
use tracing_subscriber::{EnvFilter, fmt};
|
use tracing_subscriber::{fmt, EnvFilter};
|
||||||
|
|
||||||
use components::{Display, Keypad, ModeToggle, ScientificPanel};
|
use components::{Display, Keypad, ModeToggle, ScientificPanel};
|
||||||
use state::{CalcMode, CalcState};
|
use state::{CalcMode, CalcState};
|
||||||
@@ -59,32 +59,56 @@ fn CalculatorApp() -> impl IntoView {
|
|||||||
} else {
|
} else {
|
||||||
display.to_string()
|
display.to_string()
|
||||||
};
|
};
|
||||||
use_shortcut(Shortcut::new(Key::Character(ch)), ShortcutScope::Application, {
|
use_shortcut(
|
||||||
let text = text.clone();
|
Shortcut::new(Key::Character(ch)),
|
||||||
move || s.input(&text)
|
ShortcutScope::Application,
|
||||||
});
|
{
|
||||||
|
let text = text.clone();
|
||||||
|
move || s.input(&text)
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// = key also evaluates
|
// = key also evaluates
|
||||||
{
|
{
|
||||||
let s = state.clone();
|
let s = state.clone();
|
||||||
use_shortcut(Shortcut::new(Key::Character('=')), ShortcutScope::Application, move || s.evaluate());
|
use_shortcut(
|
||||||
|
Shortcut::new(Key::Character('=')),
|
||||||
|
ShortcutScope::Application,
|
||||||
|
move || s.evaluate(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let s = state.clone();
|
let s = state.clone();
|
||||||
use_shortcut(Shortcut::new(Key::Enter), ShortcutScope::Application, move || s.evaluate());
|
use_shortcut(
|
||||||
|
Shortcut::new(Key::Enter),
|
||||||
|
ShortcutScope::Application,
|
||||||
|
move || s.evaluate(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let s = state.clone();
|
let s = state.clone();
|
||||||
use_shortcut(Shortcut::new(Key::Backspace), ShortcutScope::Application, move || s.backspace());
|
use_shortcut(
|
||||||
|
Shortcut::new(Key::Backspace),
|
||||||
|
ShortcutScope::Application,
|
||||||
|
move || s.backspace(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let s = state.clone();
|
let s = state.clone();
|
||||||
use_shortcut(Shortcut::new(Key::Escape), ShortcutScope::Application, move || s.clear());
|
use_shortcut(
|
||||||
|
Shortcut::new(Key::Escape),
|
||||||
|
ShortcutScope::Application,
|
||||||
|
move || s.clear(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let s = state.clone();
|
let s = state.clone();
|
||||||
use_shortcut(Shortcut::new(Key::Delete), ShortcutScope::Application, move || s.clear());
|
use_shortcut(
|
||||||
|
Shortcut::new(Key::Delete),
|
||||||
|
ShortcutScope::Application,
|
||||||
|
move || s.clear(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Layout ----
|
// ---- Layout ----
|
||||||
@@ -153,7 +177,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(state.expression.get(), "4");
|
assert_eq!(state.expression.get(), "4");
|
||||||
assert_eq!(state.result.get(), "");
|
assert_eq!(state.result.get(), "");
|
||||||
assert_eq!(state.is_error.get(), false);
|
assert!(!state.is_error.get());
|
||||||
assert_eq!(state.history.get().len(), 1);
|
assert_eq!(state.history.get().len(), 1);
|
||||||
assert_eq!(state.history.get()[0].expression, "2+2");
|
assert_eq!(state.history.get()[0].expression, "2+2");
|
||||||
assert_eq!(state.history.get()[0].result, "4");
|
assert_eq!(state.history.get()[0].result, "4");
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ pub struct CalcState {
|
|||||||
impl CalcState {
|
impl CalcState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
expression: use_signal(|| String::new()),
|
expression: use_signal(String::new),
|
||||||
result: use_signal(|| String::new()),
|
result: use_signal(String::new),
|
||||||
is_error: use_signal(|| false),
|
is_error: use_signal(|| false),
|
||||||
history: use_signal(|| Vec::new()),
|
history: use_signal(Vec::new),
|
||||||
mode: use_signal(|| CalcMode::Basic),
|
mode: use_signal(|| CalcMode::Basic),
|
||||||
fresh: use_signal(|| false),
|
fresh: use_signal(|| false),
|
||||||
history_scroll: use_signal(|| 10_000.0_f32),
|
history_scroll: use_signal(|| 10_000.0_f32),
|
||||||
@@ -77,11 +77,7 @@ impl CalcState {
|
|||||||
}
|
}
|
||||||
self.expression.update(|e| {
|
self.expression.update(|e| {
|
||||||
// Pop a full UTF-8 character
|
// Pop a full UTF-8 character
|
||||||
let new_len = e
|
let new_len = e.char_indices().next_back().map(|(i, _)| i).unwrap_or(0);
|
||||||
.char_indices()
|
|
||||||
.next_back()
|
|
||||||
.map(|(i, _)| i)
|
|
||||||
.unwrap_or(0);
|
|
||||||
e.truncate(new_len);
|
e.truncate(new_len);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -186,7 +182,11 @@ fn format_result(value: f64) -> String {
|
|||||||
return "Error".to_string();
|
return "Error".to_string();
|
||||||
}
|
}
|
||||||
if value.is_infinite() {
|
if value.is_infinite() {
|
||||||
return if value > 0.0 { "∞".to_string() } else { "-∞".to_string() };
|
return if value > 0.0 {
|
||||||
|
"∞".to_string()
|
||||||
|
} else {
|
||||||
|
"-∞".to_string()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if value.fract() == 0.0 && value.abs() < 1e15 {
|
if value.fract() == 0.0 && value.abs() < 1e15 {
|
||||||
return format!("{}", value as i64);
|
return format!("{}", value as i64);
|
||||||
|
|||||||
8
examples/perf_scenarios/Cargo.toml
Normal file
8
examples/perf_scenarios/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "ruin_perf_scenarios"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ruin_perf = { path = "../../lib/perf" }
|
||||||
|
ruin_ui = { path = "../../lib/ui" }
|
||||||
366
examples/perf_scenarios/src/main.rs
Normal file
366
examples/perf_scenarios/src/main.rs
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use ruin_perf::{PerfReportBuilder, RunMetadata};
|
||||||
|
use ruin_ui::{
|
||||||
|
AlignItems, Color, Edges, Element, FlexDirection, LayoutCache, LayoutFrameStats,
|
||||||
|
ScrollbarStyle, TextStyle, TextSystem, UiSize, layout_snapshot_with_cache_and_stats,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_FRAMES: u64 = 300;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct Config {
|
||||||
|
scenario: Scenario,
|
||||||
|
frames: u64,
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
text_kb: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum Scenario {
|
||||||
|
Counter,
|
||||||
|
LargeScrollList,
|
||||||
|
Calculator,
|
||||||
|
ResizeConfigure,
|
||||||
|
LargeTextBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scenario {
|
||||||
|
const ALL: &'static [Self] = &[
|
||||||
|
Self::Counter,
|
||||||
|
Self::LargeScrollList,
|
||||||
|
Self::Calculator,
|
||||||
|
Self::ResizeConfigure,
|
||||||
|
Self::LargeTextBody,
|
||||||
|
];
|
||||||
|
|
||||||
|
const fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Counter => "counter",
|
||||||
|
Self::LargeScrollList => "large-scroll-list",
|
||||||
|
Self::Calculator => "calculator",
|
||||||
|
Self::ResizeConfigure => "resize-configure",
|
||||||
|
Self::LargeTextBody => "large-text-body",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(value: &str) -> Option<Self> {
|
||||||
|
Self::ALL
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|scenario| scenario.name() == value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let config = Config::parse(env::args().skip(1))?;
|
||||||
|
let report = run_scenario(&config);
|
||||||
|
let output = config
|
||||||
|
.output
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| report.default_output_path());
|
||||||
|
report.write_json(&output)?;
|
||||||
|
report.print_summary();
|
||||||
|
println!();
|
||||||
|
println!("wrote {}", output.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
fn parse(mut args: impl Iterator<Item = String>) -> io::Result<Self> {
|
||||||
|
let mut scenario = Scenario::LargeScrollList;
|
||||||
|
let mut frames = DEFAULT_FRAMES;
|
||||||
|
let mut output = None;
|
||||||
|
let mut text_kb = 256usize;
|
||||||
|
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
match arg.as_str() {
|
||||||
|
"--scenario" => {
|
||||||
|
let value = args.next().ok_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidInput, "--scenario needs a value")
|
||||||
|
})?;
|
||||||
|
scenario = Scenario::parse(&value).ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("unknown scenario '{value}'"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
"--frames" => {
|
||||||
|
let value = args.next().ok_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidInput, "--frames needs a value")
|
||||||
|
})?;
|
||||||
|
frames = value.parse::<u64>().map_err(|error| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("invalid frame count '{value}': {error}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
"--output" => {
|
||||||
|
let value = args.next().ok_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidInput, "--output needs a value")
|
||||||
|
})?;
|
||||||
|
output = Some(PathBuf::from(value));
|
||||||
|
}
|
||||||
|
"--text-kb" => {
|
||||||
|
let value = args.next().ok_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidInput, "--text-kb needs a value")
|
||||||
|
})?;
|
||||||
|
text_kb = value.parse::<usize>().map_err(|error| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("invalid text size '{value}': {error}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
"--list" => {
|
||||||
|
for scenario in Scenario::ALL {
|
||||||
|
println!("{}", scenario.name());
|
||||||
|
}
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
"--help" | "-h" => {
|
||||||
|
print_help();
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("unknown argument '{other}'"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
scenario,
|
||||||
|
frames,
|
||||||
|
output,
|
||||||
|
text_kb,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
println!(
|
||||||
|
"ruin_perf_scenarios --scenario <name> [--frames <n>] [--output <path>] [--text-kb <n>]"
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
println!("scenarios:");
|
||||||
|
for scenario in Scenario::ALL {
|
||||||
|
println!(" {}", scenario.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_scenario(config: &Config) -> ruin_perf::PerfReport {
|
||||||
|
let mut report =
|
||||||
|
PerfReportBuilder::new(RunMetadata::new(config.scenario.name(), config.frames));
|
||||||
|
let mut text_system = TextSystem::new();
|
||||||
|
let mut layout_cache = LayoutCache::new();
|
||||||
|
|
||||||
|
report.note("headless layout/app-build scenario; renderer/platform timings are not included");
|
||||||
|
if config.scenario == Scenario::LargeTextBody {
|
||||||
|
report.set_counter("text_body_kb", config.text_kb as f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
for frame in 0..config.frames {
|
||||||
|
let build_started = Instant::now();
|
||||||
|
let (tree, viewport) = match config.scenario {
|
||||||
|
Scenario::Counter => counter_tree(frame),
|
||||||
|
Scenario::LargeScrollList => large_scroll_list_tree(frame),
|
||||||
|
Scenario::Calculator => calculator_tree(frame),
|
||||||
|
Scenario::ResizeConfigure => resize_configure_tree(frame),
|
||||||
|
Scenario::LargeTextBody => large_text_body_tree(frame, config.text_kb),
|
||||||
|
};
|
||||||
|
let build_ms = build_started.elapsed().as_secs_f64() * 1_000.0;
|
||||||
|
let (_snapshot, stats) = layout_snapshot_with_cache_and_stats(
|
||||||
|
frame + 1,
|
||||||
|
viewport,
|
||||||
|
&tree,
|
||||||
|
&mut text_system,
|
||||||
|
&mut layout_cache,
|
||||||
|
);
|
||||||
|
|
||||||
|
report.record_ms("app_build", build_ms);
|
||||||
|
record_layout_stats(&mut report, stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
report.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_layout_stats(report: &mut PerfReportBuilder, stats: LayoutFrameStats) {
|
||||||
|
report.record_ms("layout_total", stats.total_ms);
|
||||||
|
report.record_ms("layout_intrinsic", stats.intrinsic_ms);
|
||||||
|
report.record_ms("text_prepare", stats.text_prepare_ms);
|
||||||
|
report.record_ms("text_cache_miss", stats.text_miss_ms);
|
||||||
|
report.record("layout_nodes", "count", stats.nodes as f64);
|
||||||
|
report.record("scene_items", "count", stats.scene_items as f64);
|
||||||
|
report.record("text_glyphs", "count", stats.text_output_glyphs as f64);
|
||||||
|
report.add_counter("layout_cache_hits", stats.layout_cache_hits as f64);
|
||||||
|
report.add_counter("layout_cache_misses", stats.layout_cache_misses as f64);
|
||||||
|
report.add_counter("intrinsic_cache_hits", stats.intrinsic_cache_hits as f64);
|
||||||
|
report.add_counter("viewport_culled", stats.viewport_culled as f64);
|
||||||
|
report.add_counter("text_cache_hits", stats.text_cache_hits as f64);
|
||||||
|
report.add_counter("text_cache_misses", stats.text_cache_misses as f64);
|
||||||
|
report.add_counter("text_requests", stats.text_requests as f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_style(size: f32) -> TextStyle {
|
||||||
|
TextStyle::new(size, Color::rgb(0x20, 0x20, 0x20))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn counter_tree(frame: u64) -> (Element, UiSize) {
|
||||||
|
let count = frame as i64;
|
||||||
|
let root = Element::column()
|
||||||
|
.gap(12.0)
|
||||||
|
.padding(Edges::all(24.0))
|
||||||
|
.align_items(AlignItems::Center)
|
||||||
|
.child(Element::text("Counter", text_style(28.0)))
|
||||||
|
.child(Element::text(format!("count = {count}"), text_style(18.0)))
|
||||||
|
.child(Element::text(
|
||||||
|
format!("double = {}", count * 2),
|
||||||
|
text_style(18.0),
|
||||||
|
))
|
||||||
|
.child(button_row(["-1", "Reset", "+1"]));
|
||||||
|
(root, UiSize::new(480.0, 320.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn large_scroll_list_tree(frame: u64) -> (Element, UiSize) {
|
||||||
|
let item_count = 1_000usize;
|
||||||
|
let item_height = 28.0;
|
||||||
|
let viewport = UiSize::new(480.0, 640.0);
|
||||||
|
let max_offset = (item_count as f32 * item_height - viewport.height).max(0.0);
|
||||||
|
let offset = ((frame as f32 * 19.0) % max_offset.max(1.0)).round();
|
||||||
|
let mut list = Element::column().gap(0.0);
|
||||||
|
for index in 0..item_count {
|
||||||
|
list = list.child(Element::new().height(item_height).child(Element::text(
|
||||||
|
format!("list item {index:04}"),
|
||||||
|
text_style(14.0),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let root = Element::new().child(
|
||||||
|
Element::scroll_box(offset)
|
||||||
|
.width(viewport.width)
|
||||||
|
.height(viewport.height)
|
||||||
|
.scrollbar_style(ScrollbarStyle::default())
|
||||||
|
.child(list),
|
||||||
|
);
|
||||||
|
(root, viewport)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculator_tree(frame: u64) -> (Element, UiSize) {
|
||||||
|
let expression = match frame % 6 {
|
||||||
|
0 => "2",
|
||||||
|
1 => "2+",
|
||||||
|
2 => "2+2",
|
||||||
|
3 => "4",
|
||||||
|
4 => "4*8",
|
||||||
|
_ => "32",
|
||||||
|
};
|
||||||
|
let history_len = (frame as usize / 12).min(40);
|
||||||
|
let mut history = Element::column().gap(4.0);
|
||||||
|
for index in 0..history_len {
|
||||||
|
history = history.child(Element::text(
|
||||||
|
format!("{} + {} = {}", index, index + 1, index * 2 + 1),
|
||||||
|
text_style(12.0),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let root = Element::column()
|
||||||
|
.gap(10.0)
|
||||||
|
.padding(Edges::all(20.0))
|
||||||
|
.child(Element::text("Calculator", text_style(28.0)))
|
||||||
|
.child(display_panel(expression))
|
||||||
|
.child(button_grid())
|
||||||
|
.child(Element::scroll_box(10_000.0).height(180.0).child(history));
|
||||||
|
(root, UiSize::new(420.0, 720.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize_configure_tree(frame: u64) -> (Element, UiSize) {
|
||||||
|
let width = 360.0 + (frame % 90) as f32 * 4.0;
|
||||||
|
let height = 280.0 + (frame % 60) as f32 * 3.0;
|
||||||
|
let mut content = Element::column().gap(8.0).padding(Edges::all(16.0));
|
||||||
|
for index in 0..40 {
|
||||||
|
content = content.child(Element::text(
|
||||||
|
format!("resize row {index:02}: {width:.0} x {height:.0}"),
|
||||||
|
text_style(14.0),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
(content, UiSize::new(width, height))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn large_text_body_tree(frame: u64, text_kb: usize) -> (Element, UiSize) {
|
||||||
|
let viewport = UiSize::new(720.0, 720.0);
|
||||||
|
let text_width = 680.0;
|
||||||
|
let line_height = 18.0;
|
||||||
|
let text = large_text_body(text_kb);
|
||||||
|
let estimated_line_count = (text.len() as f32 / 72.0).ceil();
|
||||||
|
let estimated_height = (estimated_line_count * line_height).max(viewport.height);
|
||||||
|
let max_offset = (estimated_height - viewport.height).max(0.0);
|
||||||
|
let offset = ((frame as f32 * 127.0) % max_offset.max(1.0)).round();
|
||||||
|
let style = text_style(14.0)
|
||||||
|
.with_line_height(line_height)
|
||||||
|
.with_bounds(UiSize::new(text_width, estimated_height));
|
||||||
|
|
||||||
|
let root = Element::new().child(
|
||||||
|
Element::scroll_box(offset)
|
||||||
|
.width(viewport.width)
|
||||||
|
.height(viewport.height)
|
||||||
|
.child(
|
||||||
|
Element::new()
|
||||||
|
.width(text_width)
|
||||||
|
.child(Element::paragraph(text, style)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
(root, viewport)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn large_text_body(text_kb: usize) -> String {
|
||||||
|
let target_bytes = text_kb.saturating_mul(1024).max(1);
|
||||||
|
let paragraph = "RUIN large text profiling paragraph: the quick brown fox jumps over the lazy dog. This line is intentionally ordinary prose with enough words to exercise wrapping, glyph collection, hit-test geometry, and cache behavior.\n";
|
||||||
|
let mut text = String::with_capacity(target_bytes + paragraph.len());
|
||||||
|
while text.len() < target_bytes {
|
||||||
|
text.push_str(paragraph);
|
||||||
|
}
|
||||||
|
text.truncate(target_bytes);
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_panel(expression: &str) -> Element {
|
||||||
|
Element::new()
|
||||||
|
.padding(Edges::all(12.0))
|
||||||
|
.border(1.0, Color::rgb(0x80, 0x80, 0x80))
|
||||||
|
.child(Element::text(expression, text_style(24.0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn button_grid() -> Element {
|
||||||
|
let rows = [
|
||||||
|
["7", "8", "9", "/"],
|
||||||
|
["4", "5", "6", "*"],
|
||||||
|
["1", "2", "3", "-"],
|
||||||
|
["0", ".", "=", "+"],
|
||||||
|
];
|
||||||
|
let mut grid = Element::column().gap(6.0);
|
||||||
|
for row in rows {
|
||||||
|
grid = grid.child(button_row(row));
|
||||||
|
}
|
||||||
|
grid
|
||||||
|
}
|
||||||
|
|
||||||
|
fn button_row<const N: usize>(labels: [&str; N]) -> Element {
|
||||||
|
let mut row = Element::new().direction(FlexDirection::Row).gap(6.0);
|
||||||
|
for label in labels {
|
||||||
|
row = row.child(
|
||||||
|
Element::new()
|
||||||
|
.width(76.0)
|
||||||
|
.height(36.0)
|
||||||
|
.border(1.0, Color::rgb(0x90, 0x90, 0x90))
|
||||||
|
.child(Element::text(label, text_style(16.0))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
row
|
||||||
|
}
|
||||||
8
lib/perf/Cargo.toml
Normal file
8
lib/perf/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "ruin_perf"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
226
lib/perf/src/lib.rs
Normal file
226
lib/perf/src/lib.rs
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RunMetadata {
|
||||||
|
pub scenario: String,
|
||||||
|
pub frames: u64,
|
||||||
|
pub profile: String,
|
||||||
|
pub target_os: String,
|
||||||
|
pub target_arch: String,
|
||||||
|
pub started_unix_ms: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunMetadata {
|
||||||
|
pub fn new(scenario: impl Into<String>, frames: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
scenario: scenario.into(),
|
||||||
|
frames,
|
||||||
|
profile: if cfg!(debug_assertions) {
|
||||||
|
"debug".into()
|
||||||
|
} else {
|
||||||
|
"release".into()
|
||||||
|
},
|
||||||
|
target_os: std::env::consts::OS.into(),
|
||||||
|
target_arch: std::env::consts::ARCH.into(),
|
||||||
|
started_unix_ms: SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or(Duration::ZERO)
|
||||||
|
.as_millis(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
pub struct SampleSeries {
|
||||||
|
pub name: String,
|
||||||
|
pub unit: String,
|
||||||
|
pub samples: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SampleSeries {
|
||||||
|
pub fn new(name: impl Into<String>, unit: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
unit: unit.into(),
|
||||||
|
samples: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, sample: f64) {
|
||||||
|
if sample.is_finite() {
|
||||||
|
self.samples.push(sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn summary(&self) -> SampleSummary {
|
||||||
|
SampleSummary::from_samples(&self.name, &self.unit, &self.samples)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SampleSummary {
|
||||||
|
pub name: String,
|
||||||
|
pub unit: String,
|
||||||
|
pub count: usize,
|
||||||
|
pub avg: f64,
|
||||||
|
pub p50: f64,
|
||||||
|
pub p95: f64,
|
||||||
|
pub max: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SampleSummary {
|
||||||
|
fn from_samples(name: &str, unit: &str, samples: &[f64]) -> Self {
|
||||||
|
if samples.is_empty() {
|
||||||
|
return Self {
|
||||||
|
name: name.into(),
|
||||||
|
unit: unit.into(),
|
||||||
|
count: 0,
|
||||||
|
avg: 0.0,
|
||||||
|
p50: 0.0,
|
||||||
|
p95: 0.0,
|
||||||
|
max: 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sorted = samples.to_vec();
|
||||||
|
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||||
|
let sum = sorted.iter().sum::<f64>();
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
unit: unit.into(),
|
||||||
|
count: sorted.len(),
|
||||||
|
avg: sum / sorted.len() as f64,
|
||||||
|
p50: percentile(&sorted, 0.50),
|
||||||
|
p95: percentile(&sorted, 0.95),
|
||||||
|
max: *sorted.last().unwrap_or(&0.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct PerfReport {
|
||||||
|
pub metadata: RunMetadata,
|
||||||
|
pub summaries: Vec<SampleSummary>,
|
||||||
|
pub counters: BTreeMap<String, f64>,
|
||||||
|
pub notes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PerfReportBuilder {
|
||||||
|
metadata: RunMetadata,
|
||||||
|
series: BTreeMap<String, SampleSeries>,
|
||||||
|
counters: BTreeMap<String, f64>,
|
||||||
|
notes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PerfReportBuilder {
|
||||||
|
pub fn new(metadata: RunMetadata) -> Self {
|
||||||
|
Self {
|
||||||
|
metadata,
|
||||||
|
series: BTreeMap::new(),
|
||||||
|
counters: BTreeMap::new(),
|
||||||
|
notes: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_ms(&mut self, name: impl Into<String>, value: f64) {
|
||||||
|
self.record(name, "ms", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record(&mut self, name: impl Into<String>, unit: impl Into<String>, value: f64) {
|
||||||
|
let name = name.into();
|
||||||
|
let unit = unit.into();
|
||||||
|
self.series
|
||||||
|
.entry(name.clone())
|
||||||
|
.or_insert_with(|| SampleSeries::new(name, unit))
|
||||||
|
.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_counter(&mut self, name: impl Into<String>, value: impl Into<f64>) {
|
||||||
|
*self.counters.entry(name.into()).or_insert(0.0) += value.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_counter(&mut self, name: impl Into<String>, value: impl Into<f64>) {
|
||||||
|
self.counters.insert(name.into(), value.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn note(&mut self, note: impl Into<String>) {
|
||||||
|
self.notes.push(note.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(self) -> PerfReport {
|
||||||
|
PerfReport {
|
||||||
|
metadata: self.metadata,
|
||||||
|
summaries: self.series.values().map(SampleSeries::summary).collect(),
|
||||||
|
counters: self.counters,
|
||||||
|
notes: self.notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PerfReport {
|
||||||
|
pub fn write_json(&self, path: impl AsRef<Path>) -> io::Result<()> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let json = serde_json::to_string_pretty(self).map_err(io::Error::other)?;
|
||||||
|
fs::write(path, json)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_output_path(&self) -> PathBuf {
|
||||||
|
PathBuf::from("target")
|
||||||
|
.join("ruin-perf")
|
||||||
|
.join(format!("{}.json", self.metadata.scenario))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_summary(&self) {
|
||||||
|
println!(
|
||||||
|
"scenario={} frames={} profile={} target={}/{}",
|
||||||
|
self.metadata.scenario,
|
||||||
|
self.metadata.frames,
|
||||||
|
self.metadata.profile,
|
||||||
|
self.metadata.target_os,
|
||||||
|
self.metadata.target_arch
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"{:<32} {:>8} {:>10} {:>10} {:>10} {:>10}",
|
||||||
|
"metric", "count", "avg", "p50", "p95", "max"
|
||||||
|
);
|
||||||
|
for summary in &self.summaries {
|
||||||
|
println!(
|
||||||
|
"{:<32} {:>8} {:>10.3} {:>10.3} {:>10.3} {:>10.3} {}",
|
||||||
|
summary.name,
|
||||||
|
summary.count,
|
||||||
|
summary.avg,
|
||||||
|
summary.p50,
|
||||||
|
summary.p95,
|
||||||
|
summary.max,
|
||||||
|
summary.unit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !self.counters.is_empty() {
|
||||||
|
println!();
|
||||||
|
println!("{:<32} {:>12}", "counter", "value");
|
||||||
|
for (name, value) in &self.counters {
|
||||||
|
println!("{name:<32} {value:>12.3}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn percentile(sorted: &[f64], percentile: f64) -> f64 {
|
||||||
|
if sorted.is_empty() {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let index = ((sorted.len() - 1) as f64 * percentile).round() as usize;
|
||||||
|
sorted[index.min(sorted.len() - 1)]
|
||||||
|
}
|
||||||
@@ -290,6 +290,14 @@ impl Reactor {
|
|||||||
self.inner.ensure_flush_scheduled();
|
self.inner.ensure_flush_scheduled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Flushes currently queued reactive jobs immediately on the calling thread.
|
||||||
|
///
|
||||||
|
/// This is useful when host integrations need synchronous propagation
|
||||||
|
/// (for example, during native resize loops).
|
||||||
|
pub fn flush_now(&self) {
|
||||||
|
Rc::clone(&self.inner).flush_jobs();
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn allocate_node(&self) -> NodeId {
|
pub(crate) fn allocate_node(&self) -> NodeId {
|
||||||
let raw = self.inner.next_node.get();
|
let raw = self.inner.next_node.get();
|
||||||
self.inner.next_node.set(raw.wrapping_add(1));
|
self.inner.next_node.set(raw.wrapping_add(1));
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ struct JsonParser {
|
|||||||
next_id: u64,
|
next_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ReuseContext<'a, 'count> {
|
||||||
|
old_text: &'a str,
|
||||||
|
new_text: &'a str,
|
||||||
|
delta: &'a EditDelta,
|
||||||
|
reused: &'count mut usize,
|
||||||
|
}
|
||||||
|
|
||||||
struct Cursor<'a> {
|
struct Cursor<'a> {
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
@@ -73,10 +80,22 @@ struct Cursor<'a> {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum Command {
|
enum Command {
|
||||||
Insert { offset: usize, text: String },
|
Insert {
|
||||||
Replace { start: usize, end: usize, text: String },
|
offset: usize,
|
||||||
ReplaceAll { text: String },
|
text: String,
|
||||||
Delete { start: usize, end: usize },
|
},
|
||||||
|
Replace {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
ReplaceAll {
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
Print,
|
Print,
|
||||||
Help,
|
Help,
|
||||||
Quit,
|
Quit,
|
||||||
@@ -102,7 +121,7 @@ async fn main() {
|
|||||||
let parse_state = cell_in(&reactor, initial_snapshot);
|
let parse_state = cell_in(&reactor, initial_snapshot);
|
||||||
let last_delta = cell_in(&reactor, None::<EditDelta>);
|
let last_delta = cell_in(&reactor, None::<EditDelta>);
|
||||||
|
|
||||||
let _parser_subscription = {
|
{
|
||||||
let rope = rope.clone();
|
let rope = rope.clone();
|
||||||
let parser = Rc::clone(&parser);
|
let parser = Rc::clone(&parser);
|
||||||
let parse_state = parse_state.clone();
|
let parse_state = parse_state.clone();
|
||||||
@@ -130,7 +149,9 @@ async fn main() {
|
|||||||
let parse_state = parse_state.clone();
|
let parse_state = parse_state.clone();
|
||||||
let last_delta = last_delta.clone();
|
let last_delta = last_delta.clone();
|
||||||
move || {
|
move || {
|
||||||
parse_state.with(|snapshot| print_snapshot(snapshot, &rope, last_delta.with(Clone::clone).as_ref()));
|
parse_state.with(|snapshot| {
|
||||||
|
print_snapshot(snapshot, &rope, last_delta.with(Clone::clone).as_ref())
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.leak();
|
.leak();
|
||||||
@@ -199,7 +220,9 @@ fn parse_command(input: &str) -> Result<Command, String> {
|
|||||||
.split_once(' ')
|
.split_once(' ')
|
||||||
.ok_or_else(|| "insert expects: insert <offset> <text>".to_string())?;
|
.ok_or_else(|| "insert expects: insert <offset> <text>".to_string())?;
|
||||||
return Ok(Command::Insert {
|
return Ok(Command::Insert {
|
||||||
offset: offset.parse().map_err(|_| "invalid insert offset".to_string())?,
|
offset: offset
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "invalid insert offset".to_string())?,
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -215,7 +238,9 @@ fn parse_command(input: &str) -> Result<Command, String> {
|
|||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| "replace expects: replace <start> <end> <text>".to_string())?;
|
.ok_or_else(|| "replace expects: replace <start> <end> <text>".to_string())?;
|
||||||
return Ok(Command::Replace {
|
return Ok(Command::Replace {
|
||||||
start: start.parse().map_err(|_| "invalid replace start".to_string())?,
|
start: start
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "invalid replace start".to_string())?,
|
||||||
end: end.parse().map_err(|_| "invalid replace end".to_string())?,
|
end: end.parse().map_err(|_| "invalid replace end".to_string())?,
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
});
|
});
|
||||||
@@ -230,7 +255,9 @@ fn parse_command(input: &str) -> Result<Command, String> {
|
|||||||
.split_once(' ')
|
.split_once(' ')
|
||||||
.ok_or_else(|| "delete expects: delete <start> <end>".to_string())?;
|
.ok_or_else(|| "delete expects: delete <start> <end>".to_string())?;
|
||||||
return Ok(Command::Delete {
|
return Ok(Command::Delete {
|
||||||
start: start.parse().map_err(|_| "invalid delete start".to_string())?,
|
start: start
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "invalid delete start".to_string())?,
|
||||||
end: end.parse().map_err(|_| "invalid delete end".to_string())?,
|
end: end.parse().map_err(|_| "invalid delete end".to_string())?,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -263,15 +290,18 @@ impl JsonParser {
|
|||||||
let mut root_anchor = previous.root_anchor;
|
let mut root_anchor = previous.root_anchor;
|
||||||
root_anchor.map_through(delta);
|
root_anchor.map_through(delta);
|
||||||
let mut reused = 0usize;
|
let mut reused = 0usize;
|
||||||
|
let mut reuse = ReuseContext {
|
||||||
|
old_text: &previous.text,
|
||||||
|
new_text: &text,
|
||||||
|
delta,
|
||||||
|
reused: &mut reused,
|
||||||
|
};
|
||||||
let root = self.reuse_node(
|
let root = self.reuse_node(
|
||||||
&parsed,
|
&parsed,
|
||||||
Some(&previous.root),
|
Some(&previous.root),
|
||||||
previous.root_anchor.byte,
|
previous.root_anchor.byte,
|
||||||
root_anchor.byte,
|
root_anchor.byte,
|
||||||
&previous.text,
|
&mut reuse,
|
||||||
&text,
|
|
||||||
delta,
|
|
||||||
&mut reused,
|
|
||||||
);
|
);
|
||||||
Ok(ParseSnapshot {
|
Ok(ParseSnapshot {
|
||||||
text,
|
text,
|
||||||
@@ -302,20 +332,18 @@ impl JsonParser {
|
|||||||
old: Option<&AstNode>,
|
old: Option<&AstNode>,
|
||||||
old_parent_start: usize,
|
old_parent_start: usize,
|
||||||
new_parent_start: usize,
|
new_parent_start: usize,
|
||||||
old_text: &str,
|
reuse: &mut ReuseContext<'_, '_>,
|
||||||
new_text: &str,
|
|
||||||
delta: &EditDelta,
|
|
||||||
reused: &mut usize,
|
|
||||||
) -> AstNode {
|
) -> AstNode {
|
||||||
let parsed_local = localize(parsed.range, new_parent_start);
|
let parsed_local = localize(parsed.range, new_parent_start);
|
||||||
if let Some(old) = old
|
if let Some(old) = old
|
||||||
&& old.kind == parsed.kind
|
&& old.kind == parsed.kind
|
||||||
&& map_range(absolute_range(old, old_parent_start), delta) == parsed.range
|
&& map_range(absolute_range(old, old_parent_start), reuse.delta) == parsed.range
|
||||||
&& old_text
|
&& reuse.old_text.get(
|
||||||
.get(absolute_range(old, old_parent_start).start..absolute_range(old, old_parent_start).end)
|
absolute_range(old, old_parent_start).start
|
||||||
== new_text.get(parsed.range.start..parsed.range.end)
|
..absolute_range(old, old_parent_start).end,
|
||||||
|
) == reuse.new_text.get(parsed.range.start..parsed.range.end)
|
||||||
{
|
{
|
||||||
*reused += count_nodes(old);
|
*reuse.reused += count_nodes(old);
|
||||||
return AstNode {
|
return AstNode {
|
||||||
id: old.id,
|
id: old.id,
|
||||||
kind: old.kind,
|
kind: old.kind,
|
||||||
@@ -338,10 +366,7 @@ impl JsonParser {
|
|||||||
previous_child,
|
previous_child,
|
||||||
next_old_parent_start,
|
next_old_parent_start,
|
||||||
parsed.range.start,
|
parsed.range.start,
|
||||||
old_text,
|
reuse,
|
||||||
new_text,
|
|
||||||
delta,
|
|
||||||
reused,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -403,7 +428,10 @@ fn parse_json(text: &str) -> Result<ParsedNode, String> {
|
|||||||
let node = cursor.parse_value()?;
|
let node = cursor.parse_value()?;
|
||||||
cursor.skip_ws();
|
cursor.skip_ws();
|
||||||
if !cursor.is_eof() {
|
if !cursor.is_eof() {
|
||||||
return Err(format!("unexpected trailing data at byte {}", cursor.offset));
|
return Err(format!(
|
||||||
|
"unexpected trailing data at byte {}",
|
||||||
|
cursor.offset
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Ok(node)
|
Ok(node)
|
||||||
}
|
}
|
||||||
@@ -496,7 +524,10 @@ impl<'a> Cursor<'a> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Some(other) => {
|
Some(other) => {
|
||||||
return Err(format!("expected `,` or `}}` at byte {}, found `{other}`", self.offset));
|
return Err(format!(
|
||||||
|
"expected `,` or `}}` at byte {}, found `{other}`",
|
||||||
|
self.offset
|
||||||
|
));
|
||||||
}
|
}
|
||||||
None => return Err("unterminated object".to_string()),
|
None => return Err("unterminated object".to_string()),
|
||||||
}
|
}
|
||||||
@@ -537,7 +568,10 @@ impl<'a> Cursor<'a> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Some(other) => {
|
Some(other) => {
|
||||||
return Err(format!("expected `,` or `]` at byte {}, found `{other}`", self.offset));
|
return Err(format!(
|
||||||
|
"expected `,` or `]` at byte {}, found `{other}`",
|
||||||
|
self.offset
|
||||||
|
));
|
||||||
}
|
}
|
||||||
None => return Err("unterminated array".to_string()),
|
None => return Err("unterminated array".to_string()),
|
||||||
}
|
}
|
||||||
@@ -569,14 +603,24 @@ impl<'a> Cursor<'a> {
|
|||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
match self.bump_char() {
|
match self.bump_char() {
|
||||||
Some(ch) if ch.is_ascii_hexdigit() => {}
|
Some(ch) if ch.is_ascii_hexdigit() => {}
|
||||||
_ => return Err(format!("invalid unicode escape at byte {}", self.offset)),
|
_ => {
|
||||||
|
return Err(format!(
|
||||||
|
"invalid unicode escape at byte {}",
|
||||||
|
self.offset
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => return Err(format!("invalid escape sequence at byte {}", self.offset)),
|
_ => return Err(format!("invalid escape sequence at byte {}", self.offset)),
|
||||||
},
|
},
|
||||||
Some(ch) if ch >= '\u{20}' => {}
|
Some(ch) if ch >= '\u{20}' => {}
|
||||||
Some(_) => return Err(format!("control character in string at byte {}", self.offset)),
|
Some(_) => {
|
||||||
|
return Err(format!(
|
||||||
|
"control character in string at byte {}",
|
||||||
|
self.offset
|
||||||
|
));
|
||||||
|
}
|
||||||
None => return Err("unterminated string".to_string()),
|
None => return Err("unterminated string".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,22 @@ use ruin_reactivity_parsing::{
|
|||||||
use ruin_runtime::{async_main, fs, stdio};
|
use ruin_runtime::{async_main, fs, stdio};
|
||||||
|
|
||||||
enum Command {
|
enum Command {
|
||||||
Insert { offset: usize, text: String },
|
Insert {
|
||||||
Replace { start: usize, end: usize, text: String },
|
offset: usize,
|
||||||
ReplaceAll { text: String },
|
text: String,
|
||||||
Delete { start: usize, end: usize },
|
},
|
||||||
|
Replace {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
ReplaceAll {
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
Print,
|
Print,
|
||||||
Help,
|
Help,
|
||||||
Quit,
|
Quit,
|
||||||
@@ -108,7 +120,9 @@ fn parse_command(input: &str) -> Result<Command, String> {
|
|||||||
.split_once(' ')
|
.split_once(' ')
|
||||||
.ok_or_else(|| "insert expects: insert <offset> <text>".to_string())?;
|
.ok_or_else(|| "insert expects: insert <offset> <text>".to_string())?;
|
||||||
return Ok(Command::Insert {
|
return Ok(Command::Insert {
|
||||||
offset: offset.parse().map_err(|_| "invalid insert offset".to_string())?,
|
offset: offset
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "invalid insert offset".to_string())?,
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -124,7 +138,9 @@ fn parse_command(input: &str) -> Result<Command, String> {
|
|||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| "replace expects: replace <start> <end> <text>".to_string())?;
|
.ok_or_else(|| "replace expects: replace <start> <end> <text>".to_string())?;
|
||||||
return Ok(Command::Replace {
|
return Ok(Command::Replace {
|
||||||
start: start.parse().map_err(|_| "invalid replace start".to_string())?,
|
start: start
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "invalid replace start".to_string())?,
|
||||||
end: end.parse().map_err(|_| "invalid replace end".to_string())?,
|
end: end.parse().map_err(|_| "invalid replace end".to_string())?,
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
});
|
});
|
||||||
@@ -139,35 +155,35 @@ fn parse_command(input: &str) -> Result<Command, String> {
|
|||||||
.split_once(' ')
|
.split_once(' ')
|
||||||
.ok_or_else(|| "delete expects: delete <start> <end>".to_string())?;
|
.ok_or_else(|| "delete expects: delete <start> <end>".to_string())?;
|
||||||
return Ok(Command::Delete {
|
return Ok(Command::Delete {
|
||||||
start: start.parse().map_err(|_| "invalid delete start".to_string())?,
|
start: start
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "invalid delete start".to_string())?,
|
||||||
end: end.parse().map_err(|_| "invalid delete end".to_string())?,
|
end: end.parse().map_err(|_| "invalid delete end".to_string())?,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err("unknown command; type `help`".to_string())
|
Err("unknown command; type `help`".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_snapshot(snapshot: &TokenTreeParse, rope: &ReactiveRope, update: Option<&TokenTreeUpdate>) {
|
fn print_snapshot(
|
||||||
|
snapshot: &TokenTreeParse,
|
||||||
|
rope: &ReactiveRope,
|
||||||
|
update: Option<&TokenTreeUpdate>,
|
||||||
|
) {
|
||||||
let metrics = rope.snapshot_metrics();
|
let metrics = rope.snapshot_metrics();
|
||||||
println!();
|
println!();
|
||||||
println!("Document revision {}", metrics.revision);
|
println!("Document revision {}", metrics.revision);
|
||||||
println!(
|
println!(
|
||||||
"Text bytes={}, leaves={}, nodes={}, reused_on_last_edit={}",
|
"Text bytes={}, leaves={}, nodes={}, reused_on_last_edit={}",
|
||||||
metrics.text.bytes,
|
metrics.text.bytes, metrics.leaves, snapshot.stats.total_nodes, snapshot.stats.reused_nodes
|
||||||
metrics.leaves,
|
|
||||||
snapshot.stats.total_nodes,
|
|
||||||
snapshot.stats.reused_nodes
|
|
||||||
);
|
);
|
||||||
if let Some(update) = update {
|
if let Some(update) = update {
|
||||||
println!(
|
println!(
|
||||||
"Last edit: [{}..{}) -> {:?}",
|
"Last edit: [{}..{}) -> {:?}",
|
||||||
update.delta.range.start,
|
update.delta.range.start, update.delta.range.end, update.delta.inserted_text
|
||||||
update.delta.range.end,
|
|
||||||
update.delta.inserted_text
|
|
||||||
);
|
);
|
||||||
println!(
|
println!(
|
||||||
"Diagnostics changed={}, trivia changed={}",
|
"Diagnostics changed={}, trivia changed={}",
|
||||||
update.diagnostics_changed,
|
update.diagnostics_changed, update.trivia_changed
|
||||||
update.trivia_changed
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
println!("Parse tree:");
|
println!("Parse tree:");
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ mod tokentree;
|
|||||||
pub use delta::{Anchor, AnchorBias, EditDelta, RopeEdit, TextRange};
|
pub use delta::{Anchor, AnchorBias, EditDelta, RopeEdit, TextRange};
|
||||||
pub use metrics::{TextMetrics, TextPoint};
|
pub use metrics::{TextMetrics, TextPoint};
|
||||||
pub use parse::{
|
pub use parse::{
|
||||||
IncrementalParser, ParseError, ParseInvalidation, ParseResult, ParserCheckpoint, ParseTree,
|
IncrementalParser, ParseError, ParseInvalidation, ParseResult, ParseTree, ParserCheckpoint,
|
||||||
SyntaxFragment,
|
SyntaxFragment,
|
||||||
};
|
};
|
||||||
pub use rope::{
|
pub use rope::{
|
||||||
@@ -17,8 +17,8 @@ pub use rope::{
|
|||||||
RopeSnapshotMetrics,
|
RopeSnapshotMetrics,
|
||||||
};
|
};
|
||||||
pub use tokentree::{
|
pub use tokentree::{
|
||||||
DelimiterKind, NumberBase, NumberLiteral, NumberSuffix, StringDelimiter, StringFragment,
|
DelimiterKind, NumberBase, NumberLiteral, NumberSuffix, ReactiveTokenTree, StringDelimiter,
|
||||||
ReactiveTokenTree, TokenTreeDiagnostic, TokenTreeDiagnosticKind, TokenTreeNode,
|
StringFragment, TokenTreeDiagnostic, TokenTreeDiagnosticKind, TokenTreeNode, TokenTreeNodeKind,
|
||||||
TokenTreeNodeKind, TokenTreeParse, TokenTreeParser, TokenTreeStats, TokenTreeUpdate,
|
TokenTreeParse, TokenTreeParser, TokenTreeStats, TokenTreeUpdate, TriviaComment, TriviaKind,
|
||||||
TriviaComment, TriviaKind, TriviaStore,
|
TriviaStore,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ pub struct ParseError {
|
|||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type ParserResult<C, T, D> = Result<ParseResult<C, T, D>, ParseError>;
|
||||||
|
|
||||||
impl ParseError {
|
impl ParseError {
|
||||||
pub fn new(message: impl Into<String>) -> Self {
|
pub fn new(message: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -66,7 +68,7 @@ pub trait IncrementalParser {
|
|||||||
cursor: RopeCursor,
|
cursor: RopeCursor,
|
||||||
fragments: &[SyntaxFragment<Self::Checkpoint, Self::Tree, Self::Diagnostic>],
|
fragments: &[SyntaxFragment<Self::Checkpoint, Self::Tree, Self::Diagnostic>],
|
||||||
invalidation: ParseInvalidation<Self::Checkpoint>,
|
invalidation: ParseInvalidation<Self::Checkpoint>,
|
||||||
) -> Result<ParseResult<Self::Checkpoint, Self::Tree, Self::Diagnostic>, ParseError>;
|
) -> ParserResult<Self::Checkpoint, Self::Tree, Self::Diagnostic>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -102,7 +104,11 @@ mod tests {
|
|||||||
fn invalidate(
|
fn invalidate(
|
||||||
&self,
|
&self,
|
||||||
delta_range: TextRange,
|
delta_range: TextRange,
|
||||||
_previous_fragments: &[SyntaxFragment<Self::Checkpoint, Self::Tree, Self::Diagnostic>],
|
_previous_fragments: &[SyntaxFragment<
|
||||||
|
Self::Checkpoint,
|
||||||
|
Self::Tree,
|
||||||
|
Self::Diagnostic,
|
||||||
|
>],
|
||||||
) -> ParseInvalidation<Self::Checkpoint> {
|
) -> ParseInvalidation<Self::Checkpoint> {
|
||||||
ParseInvalidation {
|
ParseInvalidation {
|
||||||
resume_at: delta_range.start.saturating_sub(1),
|
resume_at: delta_range.start.saturating_sub(1),
|
||||||
@@ -115,13 +121,17 @@ mod tests {
|
|||||||
mut cursor: RopeCursor,
|
mut cursor: RopeCursor,
|
||||||
_fragments: &[SyntaxFragment<Self::Checkpoint, Self::Tree, Self::Diagnostic>],
|
_fragments: &[SyntaxFragment<Self::Checkpoint, Self::Tree, Self::Diagnostic>],
|
||||||
invalidation: ParseInvalidation<Self::Checkpoint>,
|
invalidation: ParseInvalidation<Self::Checkpoint>,
|
||||||
) -> Result<ParseResult<Self::Checkpoint, Self::Tree, Self::Diagnostic>, ParseError> {
|
) -> Result<ParseResult<Self::Checkpoint, Self::Tree, Self::Diagnostic>, ParseError>
|
||||||
|
{
|
||||||
self.parse_calls.set(self.parse_calls.get() + 1);
|
self.parse_calls.set(self.parse_calls.get() + 1);
|
||||||
let text = cursor.read_bytes(usize::MAX);
|
let text = cursor.read_bytes(usize::MAX);
|
||||||
Ok(ParseResult {
|
Ok(ParseResult {
|
||||||
tree: ParseTree { root: text.clone() },
|
tree: ParseTree { root: text.clone() },
|
||||||
fragments: vec![SyntaxFragment {
|
fragments: vec![SyntaxFragment {
|
||||||
range: TextRange::new(invalidation.resume_at, invalidation.resume_at + text.len()),
|
range: TextRange::new(
|
||||||
|
invalidation.resume_at,
|
||||||
|
invalidation.resume_at + text.len(),
|
||||||
|
),
|
||||||
start_checkpoint: invalidation.restart_checkpoint,
|
start_checkpoint: invalidation.restart_checkpoint,
|
||||||
end_checkpoint: 2,
|
end_checkpoint: 2,
|
||||||
tree: text,
|
tree: text,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
use std::fmt;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use ruin_reactivity::{Event, Reactor, Source, event_in, source_in};
|
use ruin_reactivity::{Event, Reactor, Source, event_in, source_in};
|
||||||
@@ -139,10 +140,6 @@ impl ReactiveRope {
|
|||||||
self.len_bytes() == 0
|
self.len_bytes() == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_string(&self) -> String {
|
|
||||||
self.slice(TextRange::new(0, self.len_bytes()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn point_at(&self, offset: usize) -> TextPoint {
|
pub fn point_at(&self, offset: usize) -> TextPoint {
|
||||||
self.inner.structure.observe();
|
self.inner.structure.observe();
|
||||||
let offset = offset.min(self.len_bytes());
|
let offset = offset.min(self.len_bytes());
|
||||||
@@ -259,7 +256,10 @@ impl ReactiveRope {
|
|||||||
|
|
||||||
fn apply_edit(&self, edit: RopeEdit) -> EditDelta {
|
fn apply_edit(&self, edit: RopeEdit) -> EditDelta {
|
||||||
assert!(edit.range.start <= edit.range.end, "invalid edit range");
|
assert!(edit.range.start <= edit.range.end, "invalid edit range");
|
||||||
assert!(edit.range.end <= self.len_bytes(), "edit range out of bounds");
|
assert!(
|
||||||
|
edit.range.end <= self.len_bytes(),
|
||||||
|
"edit range out of bounds"
|
||||||
|
);
|
||||||
self.assert_char_boundary(edit.range.start);
|
self.assert_char_boundary(edit.range.start);
|
||||||
self.assert_char_boundary(edit.range.end);
|
self.assert_char_boundary(edit.range.end);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -304,7 +304,8 @@ impl ReactiveRope {
|
|||||||
replacement.push_str(&edit.insert);
|
replacement.push_str(&edit.insert);
|
||||||
replacement.push_str(&suffix);
|
replacement.push_str(&suffix);
|
||||||
|
|
||||||
let structure_changed = last_index > first_index || replacement.len() > DEFAULT_LEAF_MAX_BYTES;
|
let structure_changed =
|
||||||
|
last_index > first_index || replacement.len() > DEFAULT_LEAF_MAX_BYTES;
|
||||||
if !structure_changed {
|
if !structure_changed {
|
||||||
if let Some(leaf) = leaves.get(first_index) {
|
if let Some(leaf) = leaves.get(first_index) {
|
||||||
leaf.set_text(replacement);
|
leaf.set_text(replacement);
|
||||||
@@ -338,7 +339,9 @@ impl ReactiveRope {
|
|||||||
self.inner.structure.trigger();
|
self.inner.structure.trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.inner.revision.set(self.inner.revision.get().wrapping_add(1));
|
self.inner
|
||||||
|
.revision
|
||||||
|
.set(self.inner.revision.get().wrapping_add(1));
|
||||||
|
|
||||||
let new_end = old_start.advance(&edit.insert);
|
let new_end = old_start.advance(&edit.insert);
|
||||||
drop(leaves);
|
drop(leaves);
|
||||||
@@ -399,7 +402,10 @@ impl ReactiveRope {
|
|||||||
let end = cursor + bytes;
|
let end = cursor + bytes;
|
||||||
if offset < end {
|
if offset < end {
|
||||||
let text = leaf.read();
|
let text = leaf.read();
|
||||||
assert!(text.is_char_boundary(offset - cursor), "offset must be a char boundary");
|
assert!(
|
||||||
|
text.is_char_boundary(offset - cursor),
|
||||||
|
"offset must be a char boundary"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cursor = end;
|
cursor = end;
|
||||||
@@ -407,6 +413,12 @@ impl ReactiveRope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ReactiveRope {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.slice(TextRange::new(0, self.len_bytes())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RopeChunk {
|
impl RopeChunk {
|
||||||
pub fn text(&self) -> &str {
|
pub fn text(&self) -> &str {
|
||||||
&self.text
|
&self.text
|
||||||
@@ -710,7 +722,10 @@ mod tests {
|
|||||||
let seen = Rc::new(RefCell::new(Vec::new()));
|
let seen = Rc::new(RefCell::new(Vec::new()));
|
||||||
let _sub = rope.edit_events().subscribe({
|
let _sub = rope.edit_events().subscribe({
|
||||||
let seen = Rc::clone(&seen);
|
let seen = Rc::clone(&seen);
|
||||||
move |delta| seen.borrow_mut().push((delta.range, delta.inserted_text.clone()))
|
move |delta| {
|
||||||
|
seen.borrow_mut()
|
||||||
|
.push((delta.range, delta.inserted_text.clone()))
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
rope.edit(RopeEdit::insert(1, "Z"));
|
rope.edit(RopeEdit::insert(1, "Z"));
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ use ruin_reactivity::{Cell, Event, Subscription, cell_in, event_in};
|
|||||||
use crate::{Anchor, AnchorBias, EditDelta, ReactiveRope, TextPoint, TextRange};
|
use crate::{Anchor, AnchorBias, EditDelta, ReactiveRope, TextPoint, TextRange};
|
||||||
|
|
||||||
const SIGILS: &[&str] = &[
|
const SIGILS: &[&str] = &[
|
||||||
"<<=", ">>=", "..=", "::", "->", "=>", "==", "!=", "<=", ">=", "&&", "||", "+=", "-=",
|
"<<=", ">>=", "..=", "::", "->", "=>", "==", "!=", "<=", ">=", "&&", "||", "+=", "-=", "*=",
|
||||||
"*=", "/=", "%=", "&=", "|=", "^=", "<<", ">>", "..", "+", "-", "*", "/", "%", "&", "|",
|
"/=", "%=", "&=", "|=", "^=", "<<", ">>", "..", "+", "-", "*", "/", "%", "&", "|", "^", "!",
|
||||||
"^", "!", "~", "=", "<", ">", ":", ";", "?", ".", ",", "@", "#", "$",
|
"~", "=", "<", ">", ":", ";", "?", ".", ",", "@", "#", "$",
|
||||||
];
|
];
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -28,8 +28,14 @@ pub enum StringDelimiter {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum StringFragment {
|
pub enum StringFragment {
|
||||||
Text { range: TextRange, text: String },
|
Text {
|
||||||
Interpolation { range: TextRange, child_index: usize },
|
range: TextRange,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
Interpolation {
|
||||||
|
range: TextRange,
|
||||||
|
child_index: usize,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -317,6 +323,13 @@ pub struct TokenTreeParser {
|
|||||||
next_id: u64,
|
next_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ReuseContext<'a, 'count> {
|
||||||
|
old_text: &'a str,
|
||||||
|
new_text: &'a str,
|
||||||
|
delta: &'a EditDelta,
|
||||||
|
reused_nodes: &'count mut usize,
|
||||||
|
}
|
||||||
|
|
||||||
impl TokenTreeParser {
|
impl TokenTreeParser {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
@@ -352,15 +365,18 @@ impl TokenTreeParser {
|
|||||||
let mut root_anchor = previous.root_anchor;
|
let mut root_anchor = previous.root_anchor;
|
||||||
root_anchor.map_through(delta);
|
root_anchor.map_through(delta);
|
||||||
let mut reused_nodes = 0usize;
|
let mut reused_nodes = 0usize;
|
||||||
|
let mut reuse = ReuseContext {
|
||||||
|
old_text: &previous.text,
|
||||||
|
new_text: &text,
|
||||||
|
delta,
|
||||||
|
reused_nodes: &mut reused_nodes,
|
||||||
|
};
|
||||||
let root = self.reuse_node(
|
let root = self.reuse_node(
|
||||||
&raw.root,
|
&raw.root,
|
||||||
Some(&previous.root),
|
Some(&previous.root),
|
||||||
previous.root_anchor.byte,
|
previous.root_anchor.byte,
|
||||||
root_anchor.byte,
|
root_anchor.byte,
|
||||||
&previous.text,
|
&mut reuse,
|
||||||
&text,
|
|
||||||
delta,
|
|
||||||
&mut reused_nodes,
|
|
||||||
);
|
);
|
||||||
let total_nodes = count_nodes(&root);
|
let total_nodes = count_nodes(&root);
|
||||||
|
|
||||||
@@ -397,14 +413,20 @@ impl TokenTreeParser {
|
|||||||
old: Option<&TokenTreeNode>,
|
old: Option<&TokenTreeNode>,
|
||||||
old_parent_start: usize,
|
old_parent_start: usize,
|
||||||
new_parent_start: usize,
|
new_parent_start: usize,
|
||||||
old_text: &str,
|
reuse: &mut ReuseContext<'_, '_>,
|
||||||
new_text: &str,
|
|
||||||
delta: &EditDelta,
|
|
||||||
reused_nodes: &mut usize,
|
|
||||||
) -> TokenTreeNode {
|
) -> TokenTreeNode {
|
||||||
let parsed_local = localize(parsed.range, new_parent_start);
|
let parsed_local = localize(parsed.range, new_parent_start);
|
||||||
if let Some(old) = old.filter(|old| can_reuse_node(old, old_parent_start, parsed, old_text, new_text, delta)) {
|
if let Some(old) = old.filter(|old| {
|
||||||
*reused_nodes += count_nodes(old);
|
can_reuse_node(
|
||||||
|
old,
|
||||||
|
old_parent_start,
|
||||||
|
parsed,
|
||||||
|
reuse.old_text,
|
||||||
|
reuse.new_text,
|
||||||
|
reuse.delta,
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
*reuse.reused_nodes += count_nodes(old);
|
||||||
return TokenTreeNode {
|
return TokenTreeNode {
|
||||||
id: old.id,
|
id: old.id,
|
||||||
local_range: parsed_local,
|
local_range: parsed_local,
|
||||||
@@ -423,24 +445,22 @@ impl TokenTreeParser {
|
|||||||
let candidate = old
|
let candidate = old
|
||||||
.children
|
.children
|
||||||
.get(index)
|
.get(index)
|
||||||
.filter(|candidate| {
|
.filter(|candidate| !used[index] && can_descend_into_node(candidate, child))
|
||||||
!used[index]
|
.inspect(|_| {
|
||||||
&& can_descend_into_node(candidate, child)
|
|
||||||
})
|
|
||||||
.map(|candidate| {
|
|
||||||
used[index] = true;
|
used[index] = true;
|
||||||
candidate
|
|
||||||
})
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
old.children.iter().enumerate().find_map(|(candidate_index, candidate)| {
|
old.children.iter().enumerate().find_map(
|
||||||
if used[candidate_index]
|
|(candidate_index, candidate)| {
|
||||||
|| !can_descend_into_node(candidate, child)
|
if used[candidate_index]
|
||||||
{
|
|| !can_descend_into_node(candidate, child)
|
||||||
return None;
|
{
|
||||||
}
|
return None;
|
||||||
used[candidate_index] = true;
|
}
|
||||||
Some(candidate)
|
used[candidate_index] = true;
|
||||||
})
|
Some(candidate)
|
||||||
|
},
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
self.reuse_node(
|
self.reuse_node(
|
||||||
@@ -448,10 +468,7 @@ impl TokenTreeParser {
|
|||||||
candidate,
|
candidate,
|
||||||
absolute_range(old, old_parent_start).start,
|
absolute_range(old, old_parent_start).start,
|
||||||
parsed.range.start,
|
parsed.range.start,
|
||||||
old_text,
|
reuse,
|
||||||
new_text,
|
|
||||||
delta,
|
|
||||||
reused_nodes,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -460,16 +477,7 @@ impl TokenTreeParser {
|
|||||||
.children
|
.children
|
||||||
.iter()
|
.iter()
|
||||||
.map(|child| {
|
.map(|child| {
|
||||||
self.reuse_node(
|
self.reuse_node(child, None, old_parent_start, parsed.range.start, reuse)
|
||||||
child,
|
|
||||||
None,
|
|
||||||
old_parent_start,
|
|
||||||
parsed.range.start,
|
|
||||||
old_text,
|
|
||||||
new_text,
|
|
||||||
delta,
|
|
||||||
reused_nodes,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
@@ -538,12 +546,12 @@ impl<'a> Cursor<'a> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(close) = terminator {
|
if let Some(close) = terminator
|
||||||
if self.peek_char() == Some(close) {
|
&& self.peek_char() == Some(close)
|
||||||
self.bump_char();
|
{
|
||||||
terminated = true;
|
self.bump_char();
|
||||||
break;
|
terminated = true;
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(ch) = self.peek_char() else {
|
let Some(ch) = self.peek_char() else {
|
||||||
@@ -602,8 +610,8 @@ impl<'a> Cursor<'a> {
|
|||||||
let (_, close) = delimiter_chars(delimiter);
|
let (_, close) = delimiter_chars(delimiter);
|
||||||
self.bump_char();
|
self.bump_char();
|
||||||
let children = self.parse_sequence(Some(close));
|
let children = self.parse_sequence(Some(close));
|
||||||
let terminated =
|
let terminated = self.slice(self.offset.saturating_sub(close.len_utf8()), self.offset)
|
||||||
self.slice(self.offset.saturating_sub(close.len_utf8()), self.offset) == close.to_string();
|
== close.to_string();
|
||||||
RawNode {
|
RawNode {
|
||||||
range: TextRange::new(start, self.offset),
|
range: TextRange::new(start, self.offset),
|
||||||
kind: TokenTreeNodeKind::List {
|
kind: TokenTreeNodeKind::List {
|
||||||
@@ -667,7 +675,9 @@ impl<'a> Cursor<'a> {
|
|||||||
|
|
||||||
fn parse_string(&mut self) -> RawNode {
|
fn parse_string(&mut self) -> RawNode {
|
||||||
let start = self.offset;
|
let start = self.offset;
|
||||||
let delimiter_char = self.bump_char().expect("string should start with delimiter");
|
let delimiter_char = self
|
||||||
|
.bump_char()
|
||||||
|
.expect("string should start with delimiter");
|
||||||
let delimiter = match delimiter_char {
|
let delimiter = match delimiter_char {
|
||||||
'\'' => StringDelimiter::SingleQuote,
|
'\'' => StringDelimiter::SingleQuote,
|
||||||
'"' => StringDelimiter::DoubleQuote,
|
'"' => StringDelimiter::DoubleQuote,
|
||||||
@@ -702,9 +712,8 @@ impl<'a> Cursor<'a> {
|
|||||||
let interp_start = self.offset;
|
let interp_start = self.offset;
|
||||||
self.offset += 2;
|
self.offset += 2;
|
||||||
let interpolation_children = self.parse_sequence(Some(')'));
|
let interpolation_children = self.parse_sequence(Some(')'));
|
||||||
let terminated_interp = self
|
let terminated_interp =
|
||||||
.slice(self.offset.saturating_sub(1), self.offset)
|
self.slice(self.offset.saturating_sub(1), self.offset) == ")";
|
||||||
== ")";
|
|
||||||
if !terminated_interp {
|
if !terminated_interp {
|
||||||
self.diagnostics.push(TokenTreeDiagnostic {
|
self.diagnostics.push(TokenTreeDiagnostic {
|
||||||
kind: TokenTreeDiagnosticKind::UnterminatedInterpolation,
|
kind: TokenTreeDiagnosticKind::UnterminatedInterpolation,
|
||||||
@@ -972,7 +981,11 @@ impl<'a> Cursor<'a> {
|
|||||||
fn scan_number(
|
fn scan_number(
|
||||||
&self,
|
&self,
|
||||||
start: usize,
|
start: usize,
|
||||||
) -> (NumberLiteral, usize, Option<(TokenTreeDiagnosticKind, String)>) {
|
) -> (
|
||||||
|
NumberLiteral,
|
||||||
|
usize,
|
||||||
|
Option<(TokenTreeDiagnosticKind, String)>,
|
||||||
|
) {
|
||||||
let mut cursor = start;
|
let mut cursor = start;
|
||||||
let mut base = NumberBase::Decimal;
|
let mut base = NumberBase::Decimal;
|
||||||
if self.starts_with_at(start, "0b") {
|
if self.starts_with_at(start, "0b") {
|
||||||
@@ -1190,14 +1203,17 @@ fn can_reuse_node(
|
|||||||
let old_abs = absolute_range(old, old_parent_start);
|
let old_abs = absolute_range(old, old_parent_start);
|
||||||
old.kind == parsed.kind
|
old.kind == parsed.kind
|
||||||
&& map_range(old_abs, delta) == parsed.range
|
&& map_range(old_abs, delta) == parsed.range
|
||||||
&& old_text.get(old_abs.start..old_abs.end) == new_text.get(parsed.range.start..parsed.range.end)
|
&& old_text.get(old_abs.start..old_abs.end)
|
||||||
|
== new_text.get(parsed.range.start..parsed.range.end)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_descend_into_node(old: &TokenTreeNode, parsed: &RawNode) -> bool {
|
fn can_descend_into_node(old: &TokenTreeNode, parsed: &RawNode) -> bool {
|
||||||
match (&old.kind, &parsed.kind) {
|
match (&old.kind, &parsed.kind) {
|
||||||
(TokenTreeNodeKind::Body, TokenTreeNodeKind::Body) => true,
|
(TokenTreeNodeKind::Body, TokenTreeNodeKind::Body) => true,
|
||||||
(
|
(
|
||||||
TokenTreeNodeKind::List { delimiter: left, .. },
|
TokenTreeNodeKind::List {
|
||||||
|
delimiter: left, ..
|
||||||
|
},
|
||||||
TokenTreeNodeKind::List {
|
TokenTreeNodeKind::List {
|
||||||
delimiter: right, ..
|
delimiter: right, ..
|
||||||
},
|
},
|
||||||
@@ -1225,15 +1241,12 @@ fn count_nodes(node: &TokenTreeNode) -> usize {
|
|||||||
1 + node.children.iter().map(count_nodes).sum::<usize>()
|
1 + node.children.iter().map(count_nodes).sum::<usize>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn number_body_valid(
|
fn number_body_valid(raw: &str, base: NumberBase, saw_radix: bool, suffix_start: usize) -> bool {
|
||||||
raw: &str,
|
|
||||||
base: NumberBase,
|
|
||||||
saw_radix: bool,
|
|
||||||
suffix_start: usize,
|
|
||||||
) -> bool {
|
|
||||||
let mut body = &raw[..suffix_start];
|
let mut body = &raw[..suffix_start];
|
||||||
if matches!(base, NumberBase::Binary | NumberBase::Octal | NumberBase::Decimal | NumberBase::Hexadecimal)
|
if matches!(
|
||||||
&& body.starts_with("0")
|
base,
|
||||||
|
NumberBase::Binary | NumberBase::Octal | NumberBase::Decimal | NumberBase::Hexadecimal
|
||||||
|
) && body.starts_with("0")
|
||||||
&& body.len() >= 2
|
&& body.len() >= 2
|
||||||
&& matches!(body.as_bytes()[1], b'b' | b'o' | b'd' | b'x')
|
&& matches!(body.as_bytes()[1], b'b' | b'o' | b'd' | b'x')
|
||||||
{
|
{
|
||||||
@@ -1334,12 +1347,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn node_text<'a>(parse: &'a TokenTreeParse, node: &TokenTreeNode) -> &'a str {
|
fn node_text<'a>(parse: &'a TokenTreeParse, node: &TokenTreeNode) -> &'a str {
|
||||||
let range = parse.absolute_range(node.id).expect("node should have absolute range");
|
let range = parse
|
||||||
|
.absolute_range(node.id)
|
||||||
|
.expect("node should have absolute range");
|
||||||
&parse.text()[range.start..range.end]
|
&parse.text()[range.start..range.end]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn symbol_texts(parse: &TokenTreeParse) -> Vec<&str> {
|
fn symbol_texts(parse: &TokenTreeParse) -> Vec<&str> {
|
||||||
parse.root
|
parse
|
||||||
|
.root
|
||||||
.children
|
.children
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|node| match &node.kind {
|
.filter_map(|node| match &node.kind {
|
||||||
@@ -1350,7 +1366,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sigil_texts(parse: &TokenTreeParse) -> Vec<&str> {
|
fn sigil_texts(parse: &TokenTreeParse) -> Vec<&str> {
|
||||||
parse.root
|
parse
|
||||||
|
.root
|
||||||
.children
|
.children
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|node| match &node.kind {
|
.filter_map(|node| match &node.kind {
|
||||||
@@ -1360,7 +1377,7 @@ mod tests {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_list_child<'a>(node: &'a TokenTreeNode, delimiter: DelimiterKind) -> &'a TokenTreeNode {
|
fn find_list_child(node: &TokenTreeNode, delimiter: DelimiterKind) -> &TokenTreeNode {
|
||||||
node.children
|
node.children
|
||||||
.iter()
|
.iter()
|
||||||
.find(|child| {
|
.find(|child| {
|
||||||
@@ -1376,7 +1393,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn diagnostic_kinds(parse: &TokenTreeParse) -> Vec<TokenTreeDiagnosticKind> {
|
fn diagnostic_kinds(parse: &TokenTreeParse) -> Vec<TokenTreeDiagnosticKind> {
|
||||||
parse.diagnostics.iter().map(|diagnostic| diagnostic.kind).collect()
|
parse
|
||||||
|
.diagnostics
|
||||||
|
.iter()
|
||||||
|
.map(|diagnostic| diagnostic.kind)
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1399,11 +1420,26 @@ mod tests {
|
|||||||
fn parses_mixed_top_level_sequence_in_order() {
|
fn parses_mixed_top_level_sequence_in_order() {
|
||||||
let parse = parse_text("alpha + 12 \"hi\" [beta]");
|
let parse = parse_text("alpha + 12 \"hi\" [beta]");
|
||||||
assert_eq!(parse.root.children.len(), 5);
|
assert_eq!(parse.root.children.len(), 5);
|
||||||
assert!(matches!(parse.root.children[0].kind, TokenTreeNodeKind::Symbol { .. }));
|
assert!(matches!(
|
||||||
assert!(matches!(parse.root.children[1].kind, TokenTreeNodeKind::Sigil { .. }));
|
parse.root.children[0].kind,
|
||||||
assert!(matches!(parse.root.children[2].kind, TokenTreeNodeKind::Number(..)));
|
TokenTreeNodeKind::Symbol { .. }
|
||||||
assert!(matches!(parse.root.children[3].kind, TokenTreeNodeKind::String { .. }));
|
));
|
||||||
assert!(matches!(parse.root.children[4].kind, TokenTreeNodeKind::List { .. }));
|
assert!(matches!(
|
||||||
|
parse.root.children[1].kind,
|
||||||
|
TokenTreeNodeKind::Sigil { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
parse.root.children[2].kind,
|
||||||
|
TokenTreeNodeKind::Number(..)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
parse.root.children[3].kind,
|
||||||
|
TokenTreeNodeKind::String { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
parse.root.children[4].kind,
|
||||||
|
TokenTreeNodeKind::List { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1418,12 +1454,24 @@ mod tests {
|
|||||||
}
|
}
|
||||||
));
|
));
|
||||||
let outer = &parse.root.children[0];
|
let outer = &parse.root.children[0];
|
||||||
assert!(matches!(outer.children[0].kind, TokenTreeNodeKind::Symbol { .. }));
|
assert!(matches!(
|
||||||
|
outer.children[0].kind,
|
||||||
|
TokenTreeNodeKind::Symbol { .. }
|
||||||
|
));
|
||||||
let inner_paren = find_list_child(outer, DelimiterKind::Parenthesis);
|
let inner_paren = find_list_child(outer, DelimiterKind::Parenthesis);
|
||||||
assert!(matches!(inner_paren.children[0].kind, TokenTreeNodeKind::Symbol { .. }));
|
assert!(matches!(
|
||||||
assert!(matches!(inner_paren.children[1].kind, TokenTreeNodeKind::Sigil { .. }));
|
inner_paren.children[0].kind,
|
||||||
|
TokenTreeNodeKind::Symbol { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
inner_paren.children[1].kind,
|
||||||
|
TokenTreeNodeKind::Sigil { .. }
|
||||||
|
));
|
||||||
let inner_bracket = find_list_child(inner_paren, DelimiterKind::Bracket);
|
let inner_bracket = find_list_child(inner_paren, DelimiterKind::Bracket);
|
||||||
assert!(matches!(inner_bracket.children[0].kind, TokenTreeNodeKind::Symbol { .. }));
|
assert!(matches!(
|
||||||
|
inner_bracket.children[0].kind,
|
||||||
|
TokenTreeNodeKind::Symbol { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1491,9 +1539,18 @@ mod tests {
|
|||||||
TokenTreeDiagnosticKind::UnmatchedClosingDelimiter,
|
TokenTreeDiagnosticKind::UnmatchedClosingDelimiter,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert!(matches!(parse.root.children[0].kind, TokenTreeNodeKind::Error { .. }));
|
assert!(matches!(
|
||||||
assert!(matches!(parse.root.children[1].kind, TokenTreeNodeKind::Symbol { .. }));
|
parse.root.children[0].kind,
|
||||||
assert!(matches!(parse.root.children[2].kind, TokenTreeNodeKind::Error { .. }));
|
TokenTreeNodeKind::Error { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
parse.root.children[1].kind,
|
||||||
|
TokenTreeNodeKind::Symbol { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
parse.root.children[2].kind,
|
||||||
|
TokenTreeNodeKind::Error { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1514,8 +1571,14 @@ mod tests {
|
|||||||
terminated: false
|
terminated: false
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
assert!(matches!(list.children[0].kind, TokenTreeNodeKind::Symbol { .. }));
|
assert!(matches!(
|
||||||
assert!(matches!(list.children[1].kind, TokenTreeNodeKind::Error { .. }));
|
list.children[0].kind,
|
||||||
|
TokenTreeNodeKind::Symbol { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
list.children[1].kind,
|
||||||
|
TokenTreeNodeKind::Error { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1525,8 +1588,14 @@ mod tests {
|
|||||||
diagnostic_kinds(&parse),
|
diagnostic_kinds(&parse),
|
||||||
vec![TokenTreeDiagnosticKind::UnmatchedClosingDelimiter]
|
vec![TokenTreeDiagnosticKind::UnmatchedClosingDelimiter]
|
||||||
);
|
);
|
||||||
assert!(matches!(parse.root.children[0].kind, TokenTreeNodeKind::List { .. }));
|
assert!(matches!(
|
||||||
assert!(matches!(parse.root.children[1].kind, TokenTreeNodeKind::Error { .. }));
|
parse.root.children[0].kind,
|
||||||
|
TokenTreeNodeKind::List { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
parse.root.children[1].kind,
|
||||||
|
TokenTreeNodeKind::Error { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1546,7 +1615,10 @@ mod tests {
|
|||||||
let parse = parse_text("left+right(foo)");
|
let parse = parse_text("left+right(foo)");
|
||||||
assert_eq!(symbol_texts(&parse), vec!["left", "right"]);
|
assert_eq!(symbol_texts(&parse), vec!["left", "right"]);
|
||||||
assert_eq!(sigil_texts(&parse), vec!["+"]);
|
assert_eq!(sigil_texts(&parse), vec!["+"]);
|
||||||
assert!(matches!(parse.root.children[3].kind, TokenTreeNodeKind::List { .. }));
|
assert!(matches!(
|
||||||
|
parse.root.children[3].kind,
|
||||||
|
TokenTreeNodeKind::List { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1556,14 +1628,21 @@ mod tests {
|
|||||||
diagnostic_kinds(&parse),
|
diagnostic_kinds(&parse),
|
||||||
vec![TokenTreeDiagnosticKind::InvalidNumber]
|
vec![TokenTreeDiagnosticKind::InvalidNumber]
|
||||||
);
|
);
|
||||||
assert!(matches!(parse.root.children[0].kind, TokenTreeNodeKind::Error { .. }));
|
assert!(matches!(
|
||||||
|
parse.root.children[0].kind,
|
||||||
|
TokenTreeNodeKind::Error { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_defined_sigil_parses_as_a_sigil_node() {
|
fn every_defined_sigil_parses_as_a_sigil_node() {
|
||||||
for sigil in SIGILS {
|
for sigil in SIGILS {
|
||||||
let parse = parse_text(sigil);
|
let parse = parse_text(sigil);
|
||||||
assert_eq!(parse.root.children.len(), 1, "sigil `{sigil}` should be a single token");
|
assert_eq!(
|
||||||
|
parse.root.children.len(),
|
||||||
|
1,
|
||||||
|
"sigil `{sigil}` should be a single token"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(
|
matches!(
|
||||||
&parse.root.children[0].kind,
|
&parse.root.children[0].kind,
|
||||||
@@ -1592,8 +1671,14 @@ mod tests {
|
|||||||
fn preserves_comments_as_trivia() {
|
fn preserves_comments_as_trivia() {
|
||||||
let parse = parse_text("foo // hi\n/* block */ bar");
|
let parse = parse_text("foo // hi\n/* block */ bar");
|
||||||
assert_eq!(parse.trivia.comments().len(), 2);
|
assert_eq!(parse.trivia.comments().len(), 2);
|
||||||
assert!(matches!(parse.root.children[0].kind, TokenTreeNodeKind::Symbol { .. }));
|
assert!(matches!(
|
||||||
assert!(matches!(parse.root.children[1].kind, TokenTreeNodeKind::Symbol { .. }));
|
parse.root.children[0].kind,
|
||||||
|
TokenTreeNodeKind::Symbol { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
parse.root.children[1].kind,
|
||||||
|
TokenTreeNodeKind::Symbol { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1602,7 +1687,10 @@ mod tests {
|
|||||||
assert!(parse.diagnostics.is_empty());
|
assert!(parse.diagnostics.is_empty());
|
||||||
assert_eq!(parse.trivia.comments().len(), 1);
|
assert_eq!(parse.trivia.comments().len(), 1);
|
||||||
assert_eq!(parse.trivia.comments()[0].kind, TriviaKind::BlockComment);
|
assert_eq!(parse.trivia.comments()[0].kind, TriviaKind::BlockComment);
|
||||||
assert_eq!(parse.trivia.comments()[0].text, "/* outer /* inner */ done */");
|
assert_eq!(
|
||||||
|
parse.trivia.comments()[0].text,
|
||||||
|
"/* outer /* inner */ done */"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1621,7 +1709,9 @@ mod tests {
|
|||||||
let parse = parse_text("foo // hi\nbar /* block */ baz");
|
let parse = parse_text("foo // hi\nbar /* block */ baz");
|
||||||
let line = &parse.trivia.comments()[0];
|
let line = &parse.trivia.comments()[0];
|
||||||
let block = &parse.trivia.comments()[1];
|
let block = &parse.trivia.comments()[1];
|
||||||
let overlapping = parse.trivia.comments_in_range(TextRange::new(line.range.start, block.range.end));
|
let overlapping = parse
|
||||||
|
.trivia
|
||||||
|
.comments_in_range(TextRange::new(line.range.start, block.range.end));
|
||||||
assert_eq!(overlapping.len(), 2);
|
assert_eq!(overlapping.len(), 2);
|
||||||
let before_block = parse.trivia.comments_before(block.range.start);
|
let before_block = parse.trivia.comments_before(block.range.start);
|
||||||
assert_eq!(before_block.len(), 1);
|
assert_eq!(before_block.len(), 1);
|
||||||
@@ -1721,7 +1811,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
parse.root.children[0].kind,
|
parse.root.children[0].kind,
|
||||||
TokenTreeNodeKind::String { terminated: true, .. }
|
TokenTreeNodeKind::String {
|
||||||
|
terminated: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1754,9 +1847,15 @@ mod tests {
|
|||||||
assert_eq!(fragments.len(), 4);
|
assert_eq!(fragments.len(), 4);
|
||||||
assert_eq!(string.children.len(), 2);
|
assert_eq!(string.children.len(), 2);
|
||||||
assert!(matches!(fragments[0], StringFragment::Text { .. }));
|
assert!(matches!(fragments[0], StringFragment::Text { .. }));
|
||||||
assert!(matches!(fragments[1], StringFragment::Interpolation { child_index: 0, .. }));
|
assert!(matches!(
|
||||||
|
fragments[1],
|
||||||
|
StringFragment::Interpolation { child_index: 0, .. }
|
||||||
|
));
|
||||||
assert!(matches!(fragments[2], StringFragment::Text { .. }));
|
assert!(matches!(fragments[2], StringFragment::Text { .. }));
|
||||||
assert!(matches!(fragments[3], StringFragment::Interpolation { child_index: 1, .. }));
|
assert!(matches!(
|
||||||
|
fragments[3],
|
||||||
|
StringFragment::Interpolation { child_index: 1, .. }
|
||||||
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
string.children[0].kind,
|
string.children[0].kind,
|
||||||
TokenTreeNodeKind::List {
|
TokenTreeNodeKind::List {
|
||||||
@@ -1784,7 +1883,10 @@ mod tests {
|
|||||||
TokenTreeNodeKind::String { fragments, .. } => {
|
TokenTreeNodeKind::String { fragments, .. } => {
|
||||||
assert_eq!(fragments.len(), 2);
|
assert_eq!(fragments.len(), 2);
|
||||||
assert_eq!(string.children.len(), 1);
|
assert_eq!(string.children.len(), 1);
|
||||||
assert!(matches!(string.children[0].kind, TokenTreeNodeKind::List { .. }));
|
assert!(matches!(
|
||||||
|
string.children[0].kind,
|
||||||
|
TokenTreeNodeKind::List { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
other => panic!("expected string, got {other:?}"),
|
other => panic!("expected string, got {other:?}"),
|
||||||
}
|
}
|
||||||
@@ -1818,19 +1920,52 @@ mod tests {
|
|||||||
fn parses_numbers_with_supported_bases_and_suffixes() {
|
fn parses_numbers_with_supported_bases_and_suffixes() {
|
||||||
let cases = [
|
let cases = [
|
||||||
("0", NumberBase::Decimal, false, None),
|
("0", NumberBase::Decimal, false, None),
|
||||||
("12.3f64", NumberBase::Decimal, true, Some(NumberSuffix::F64)),
|
(
|
||||||
|
"12.3f64",
|
||||||
|
NumberBase::Decimal,
|
||||||
|
true,
|
||||||
|
Some(NumberSuffix::F64),
|
||||||
|
),
|
||||||
("0d1_2_3", NumberBase::Decimal, false, None),
|
("0d1_2_3", NumberBase::Decimal, false, None),
|
||||||
("0b1010u8", NumberBase::Binary, false, Some(NumberSuffix::U8)),
|
(
|
||||||
|
"0b1010u8",
|
||||||
|
NumberBase::Binary,
|
||||||
|
false,
|
||||||
|
Some(NumberSuffix::U8),
|
||||||
|
),
|
||||||
("0o77i16", NumberBase::Octal, false, Some(NumberSuffix::I16)),
|
("0o77i16", NumberBase::Octal, false, Some(NumberSuffix::I16)),
|
||||||
("0xffu", NumberBase::Hexadecimal, false, Some(NumberSuffix::BigUint)),
|
(
|
||||||
("99d", NumberBase::Decimal, false, Some(NumberSuffix::BigDecimal)),
|
"0xffu",
|
||||||
("42d128", NumberBase::Decimal, false, Some(NumberSuffix::Decimal128)),
|
NumberBase::Hexadecimal,
|
||||||
("5f", NumberBase::Decimal, false, Some(NumberSuffix::BigFloat)),
|
false,
|
||||||
|
Some(NumberSuffix::BigUint),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"99d",
|
||||||
|
NumberBase::Decimal,
|
||||||
|
false,
|
||||||
|
Some(NumberSuffix::BigDecimal),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"42d128",
|
||||||
|
NumberBase::Decimal,
|
||||||
|
false,
|
||||||
|
Some(NumberSuffix::Decimal128),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"5f",
|
||||||
|
NumberBase::Decimal,
|
||||||
|
false,
|
||||||
|
Some(NumberSuffix::BigFloat),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (text, base, has_radix_point, suffix) in cases {
|
for (text, base, has_radix_point, suffix) in cases {
|
||||||
let parse = parse_text(text);
|
let parse = parse_text(text);
|
||||||
assert!(parse.diagnostics.is_empty(), "unexpected diagnostics for `{text}`");
|
assert!(
|
||||||
|
parse.diagnostics.is_empty(),
|
||||||
|
"unexpected diagnostics for `{text}`"
|
||||||
|
);
|
||||||
match &parse.root.children[0].kind {
|
match &parse.root.children[0].kind {
|
||||||
TokenTreeNodeKind::Number(number) => {
|
TokenTreeNodeKind::Number(number) => {
|
||||||
assert_eq!(number.raw, text);
|
assert_eq!(number.raw, text);
|
||||||
@@ -1848,7 +1983,13 @@ mod tests {
|
|||||||
let parse = parse_text("1_2_3 0x_ff 0d12_.3f64 1._5");
|
let parse = parse_text("1_2_3 0x_ff 0d12_.3f64 1._5");
|
||||||
assert!(parse.diagnostics.is_empty());
|
assert!(parse.diagnostics.is_empty());
|
||||||
assert_eq!(parse.root.children.len(), 4);
|
assert_eq!(parse.root.children.len(), 4);
|
||||||
assert!(parse.root.children.iter().all(|node| matches!(node.kind, TokenTreeNodeKind::Number(..))));
|
assert!(
|
||||||
|
parse
|
||||||
|
.root
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.all(|node| matches!(node.kind, TokenTreeNodeKind::Number(..)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1858,7 +1999,10 @@ mod tests {
|
|||||||
diagnostic_kinds(&parse),
|
diagnostic_kinds(&parse),
|
||||||
vec![TokenTreeDiagnosticKind::InvalidNumber]
|
vec![TokenTreeDiagnosticKind::InvalidNumber]
|
||||||
);
|
);
|
||||||
assert!(matches!(parse.root.children[0].kind, TokenTreeNodeKind::Error { .. }));
|
assert!(matches!(
|
||||||
|
parse.root.children[0].kind,
|
||||||
|
TokenTreeNodeKind::Error { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1872,28 +2016,52 @@ mod tests {
|
|||||||
TokenTreeDiagnosticKind::InvalidNumber,
|
TokenTreeDiagnosticKind::InvalidNumber,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert!(parse.root.children.iter().all(|node| matches!(node.kind, TokenTreeNodeKind::Error { .. })));
|
assert!(
|
||||||
|
parse
|
||||||
|
.root
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.all(|node| matches!(node.kind, TokenTreeNodeKind::Error { .. }))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn numbers_reject_unknown_or_underscored_suffixes() {
|
fn numbers_reject_unknown_or_underscored_suffixes() {
|
||||||
let parse = parse_text("12foo 9u_8");
|
let parse = parse_text("12foo 9u_8");
|
||||||
assert_eq!(diagnostic_kinds(&parse), vec![TokenTreeDiagnosticKind::InvalidNumber]);
|
assert_eq!(
|
||||||
assert!(matches!(parse.root.children[0].kind, TokenTreeNodeKind::Error { .. }));
|
diagnostic_kinds(&parse),
|
||||||
assert!(matches!(parse.root.children[1].kind, TokenTreeNodeKind::Number(..)));
|
vec![TokenTreeDiagnosticKind::InvalidNumber]
|
||||||
assert!(matches!(parse.root.children[2].kind, TokenTreeNodeKind::Symbol { .. }));
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
parse.root.children[0].kind,
|
||||||
|
TokenTreeNodeKind::Error { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
parse.root.children[1].kind,
|
||||||
|
TokenTreeNodeKind::Number(..)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
parse.root.children[2].kind,
|
||||||
|
TokenTreeNodeKind::Symbol { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn numbers_stop_before_non_numeric_sigils() {
|
fn numbers_stop_before_non_numeric_sigils() {
|
||||||
let parse = parse_text("0x1..2");
|
let parse = parse_text("0x1..2");
|
||||||
assert_eq!(parse.root.children.len(), 3);
|
assert_eq!(parse.root.children.len(), 3);
|
||||||
assert!(matches!(parse.root.children[0].kind, TokenTreeNodeKind::Number(..)));
|
assert!(matches!(
|
||||||
|
parse.root.children[0].kind,
|
||||||
|
TokenTreeNodeKind::Number(..)
|
||||||
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
parse.root.children[1].kind,
|
parse.root.children[1].kind,
|
||||||
TokenTreeNodeKind::Sigil { ref text } if text == ".."
|
TokenTreeNodeKind::Sigil { ref text } if text == ".."
|
||||||
));
|
));
|
||||||
assert!(matches!(parse.root.children[2].kind, TokenTreeNodeKind::Number(..)));
|
assert!(matches!(
|
||||||
|
parse.root.children[2].kind,
|
||||||
|
TokenTreeNodeKind::Number(..)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1910,9 +2078,13 @@ mod tests {
|
|||||||
let (rope, parse) = parse_with_rope("{\n foo\n [bar]\n}");
|
let (rope, parse) = parse_with_rope("{\n foo\n [bar]\n}");
|
||||||
let outer = &parse.root.children[0];
|
let outer = &parse.root.children[0];
|
||||||
let bracket = find_list_child(outer, DelimiterKind::Bracket);
|
let bracket = find_list_child(outer, DelimiterKind::Bracket);
|
||||||
let range = parse.absolute_range(bracket.id).expect("bracket should have range");
|
let range = parse
|
||||||
|
.absolute_range(bracket.id)
|
||||||
|
.expect("bracket should have range");
|
||||||
assert_eq!(&parse.text()[range.start..range.end], "[bar]");
|
assert_eq!(&parse.text()[range.start..range.end], "[bar]");
|
||||||
let (start, end) = parse.absolute_span(&rope, bracket.id).expect("span should exist");
|
let (start, end) = parse
|
||||||
|
.absolute_span(&rope, bracket.id)
|
||||||
|
.expect("span should exist");
|
||||||
assert_eq!(start.line, 2);
|
assert_eq!(start.line, 2);
|
||||||
assert_eq!(start.column, 2);
|
assert_eq!(start.column, 2);
|
||||||
assert_eq!(end.line, 2);
|
assert_eq!(end.line, 2);
|
||||||
@@ -1931,7 +2103,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reparsing_reuses_shifted_subtrees() {
|
fn reparsing_reuses_shifted_subtrees() {
|
||||||
let (_rope, first, second, _delta) = reparse_after_edit("{ a b }", RopeEdit::insert(2, "long "));
|
let (_rope, first, second, _delta) =
|
||||||
|
reparse_after_edit("{ a b }", RopeEdit::insert(2, "long "));
|
||||||
let original_b = first.root.children[0]
|
let original_b = first.root.children[0]
|
||||||
.children
|
.children
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1962,16 +2135,22 @@ mod tests {
|
|||||||
.find(|node| matches!(&node.kind, TokenTreeNodeKind::Symbol { text } if text == "b"))
|
.find(|node| matches!(&node.kind, TokenTreeNodeKind::Symbol { text } if text == "b"))
|
||||||
.expect("reparsed tree should contain symbol b");
|
.expect("reparsed tree should contain symbol b");
|
||||||
assert_eq!(original_b.id, reparsed_b.id);
|
assert_eq!(original_b.id, reparsed_b.id);
|
||||||
let first_span = first.absolute_span(&rope, original_b.id).expect("first span should exist");
|
let first_span = first
|
||||||
let second_span = second.absolute_span(&rope, reparsed_b.id).expect("second span should exist");
|
.absolute_span(&rope, original_b.id)
|
||||||
|
.expect("first span should exist");
|
||||||
|
let second_span = second
|
||||||
|
.absolute_span(&rope, reparsed_b.id)
|
||||||
|
.expect("second span should exist");
|
||||||
assert_ne!(first_span, second_span);
|
assert_ne!(first_span, second_span);
|
||||||
assert_eq!(second_span.0.line, 2);
|
assert_eq!(second_span.0.line, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reparsing_reuses_unaffected_siblings_when_one_symbol_changes() {
|
fn reparsing_reuses_unaffected_siblings_when_one_symbol_changes() {
|
||||||
let (_rope, first, second, _delta) =
|
let (_rope, first, second, _delta) = reparse_after_edit(
|
||||||
reparse_after_edit("{ left right }", RopeEdit::replace(TextRange::new(2, 6), "long"));
|
"{ left right }",
|
||||||
|
RopeEdit::replace(TextRange::new(2, 6), "long"),
|
||||||
|
);
|
||||||
let first_list = &first.root.children[0];
|
let first_list = &first.root.children[0];
|
||||||
let second_list = &second.root.children[0];
|
let second_list = &second.root.children[0];
|
||||||
let old_right = first_list.children[1].id;
|
let old_right = first_list.children[1].id;
|
||||||
@@ -1986,8 +2165,14 @@ mod tests {
|
|||||||
fn reparsing_does_not_reuse_node_when_kind_changes() {
|
fn reparsing_does_not_reuse_node_when_kind_changes() {
|
||||||
let (_rope, first, second, _delta) =
|
let (_rope, first, second, _delta) =
|
||||||
reparse_after_edit("foo", RopeEdit::replace(TextRange::new(0, 3), "123"));
|
reparse_after_edit("foo", RopeEdit::replace(TextRange::new(0, 3), "123"));
|
||||||
assert!(matches!(first.root.children[0].kind, TokenTreeNodeKind::Symbol { .. }));
|
assert!(matches!(
|
||||||
assert!(matches!(second.root.children[0].kind, TokenTreeNodeKind::Number(..)));
|
first.root.children[0].kind,
|
||||||
|
TokenTreeNodeKind::Symbol { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
second.root.children[0].kind,
|
||||||
|
TokenTreeNodeKind::Number(..)
|
||||||
|
));
|
||||||
assert_ne!(first.root.children[0].id, second.root.children[0].id);
|
assert_ne!(first.root.children[0].id, second.root.children[0].id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2046,5 +2231,4 @@ mod tests {
|
|||||||
assert_eq!(seen.len(), 1);
|
assert_eq!(seen.len(), 1);
|
||||||
assert!(seen[0].diagnostics_changed || seen[0].stats.total_nodes > 0);
|
assert!(seen[0].diagnostics_changed || seen[0].stats.total_nodes > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,14 @@ ruin_reactivity = { path = "../reactivity" }
|
|||||||
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
||||||
ruin_app_proc_macros = { package = "ruin-app-proc-macros", path = "../ruin_app_proc_macros" }
|
ruin_app_proc_macros = { package = "ruin-app-proc-macros", path = "../ruin_app_proc_macros" }
|
||||||
ruin_ui = { path = "../ui" }
|
ruin_ui = { path = "../ui" }
|
||||||
ruin_ui_platform_wayland = { path = "../ui_platform_wayland" }
|
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
ruin_ui_platform_wayland = { path = "../ui_platform_wayland" }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
ruin_ui_platform_macos = { path = "../ui_platform_macos" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tracing-subscriber = { version = "0.3", default-features = false, features = [
|
tracing-subscriber = { version = "0.3", default-features = false, features = [
|
||||||
"env-filter",
|
"env-filter",
|
||||||
|
|||||||
18
lib/ruin_app/build.rs
Normal file
18
lib/ruin_app/build.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=macos/Info.plist");
|
||||||
|
|
||||||
|
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
if target_os != "macos" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("manifest dir"));
|
||||||
|
let plist = manifest_dir.join("macos/Info.plist");
|
||||||
|
let arg = format!("-Wl,-sectcreate,__TEXT,__info_plist,{}", plist.display());
|
||||||
|
|
||||||
|
// Ensure all ruin_app executables/examples opt into Retina rendering.
|
||||||
|
println!("cargo:rustc-link-arg-examples={arg}");
|
||||||
|
}
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
use ruin_app::{PartialTextStyle, prelude::*};
|
use ruin_app::{PartialTextStyle, prelude::*};
|
||||||
use ruin_ui::{BoxShadow, BoxShadowKind, Point};
|
use ruin_ui::{BoxShadow, BoxShadowKind, Point};
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
#[ruin_runtime::async_main]
|
#[ruin_runtime::async_main]
|
||||||
async fn main() -> ruin_app::Result<()> {
|
async fn main() -> ruin_app::Result<()> {
|
||||||
|
let _ = tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
|
||||||
|
)
|
||||||
|
.with_target(true)
|
||||||
|
.without_time()
|
||||||
|
.try_init();
|
||||||
|
|
||||||
let title = "RUIN Counter";
|
let title = "RUIN Counter";
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
|
|||||||
@@ -90,21 +90,12 @@ fn TaskBoard() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let all_label = if matches!(filter.get(), TaskFilter::All) {
|
let all_label = filter_label(matches!(filter.get(), TaskFilter::All), "All");
|
||||||
"● All"
|
let open_label = filter_label(matches!(filter.get(), TaskFilter::OpenOnly), "Open");
|
||||||
} else {
|
let done_label = filter_label(
|
||||||
"○ All"
|
matches!(filter.get(), TaskFilter::CompletedOnly),
|
||||||
};
|
"Completed",
|
||||||
let open_label = if matches!(filter.get(), TaskFilter::OpenOnly) {
|
);
|
||||||
"● Open"
|
|
||||||
} else {
|
|
||||||
"○ Open"
|
|
||||||
};
|
|
||||||
let done_label = if matches!(filter.get(), TaskFilter::CompletedOnly) {
|
|
||||||
"● Completed"
|
|
||||||
} else {
|
|
||||||
"○ Completed"
|
|
||||||
};
|
|
||||||
let task_rows = visible_ids.with(|ids| {
|
let task_rows = visible_ids.with(|ids| {
|
||||||
ids.iter()
|
ids.iter()
|
||||||
.map(|task_id| {
|
.map(|task_id| {
|
||||||
@@ -466,6 +457,14 @@ fn move_task(tasks: &Signal<Vec<Task>>, task_id: u64, direction: MoveDirection)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn filter_label(active: bool, label: &str) -> String {
|
||||||
|
if active {
|
||||||
|
format!("[x] {label}")
|
||||||
|
} else {
|
||||||
|
format!("[ ] {label}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn seed_tasks() -> Vec<Task> {
|
fn seed_tasks() -> Vec<Task> {
|
||||||
vec![
|
vec![
|
||||||
Task {
|
Task {
|
||||||
|
|||||||
@@ -259,7 +259,12 @@ fn CardFrame(title: View, toolbar: ChildViews, children: ChildViews) -> impl Int
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn InlineDialog(open: bool, title: View, actions: ChildViews, children: ChildViews) -> impl IntoView {
|
fn InlineDialog(
|
||||||
|
open: bool,
|
||||||
|
title: View,
|
||||||
|
actions: ChildViews,
|
||||||
|
children: ChildViews,
|
||||||
|
) -> impl IntoView {
|
||||||
if open {
|
if open {
|
||||||
let actions_row = view! {
|
let actions_row = view! {
|
||||||
row(gap = 10.0) {
|
row(gap = 10.0) {
|
||||||
|
|||||||
16
lib/ruin_app/macos/Info.plist
Normal file
16
lib/ruin_app/macos/Info.plist
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>RUIN</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>dev.ruin.example</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -4,13 +4,18 @@ use std::iter;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use ruin_reactivity::effect;
|
use ruin_reactivity::{current as current_reactor, effect};
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use ruin_runtime::{queue_future, spawn_worker};
|
||||||
use ruin_ui::{
|
use ruin_ui::{
|
||||||
Color, Element, InteractionTree, KeyboardEvent, LayoutCache, LayoutSnapshot, PlatformEvent,
|
Color, Element, InteractionTree, KeyboardEvent, LayoutCache, LayoutSnapshot, PlatformEvent,
|
||||||
PointerButton, PointerEvent, PointerEventKind, PointerRouter, RoutedPointerEvent,
|
PointerButton, PointerEvent, PointerEventKind, PointerRouter, RoutedPointerEvent,
|
||||||
RoutedPointerEventKind, TextSystem, UiSize, WindowController, WindowSpec, WindowUpdate,
|
RoutedPointerEventKind, TextSystem, UiSize, WindowController, WindowSpec, WindowUpdate,
|
||||||
layout_snapshot_with_cache,
|
layout_snapshot_with_cache,
|
||||||
};
|
};
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use ruin_ui_platform_macos::{run_macos_app_event_loop, start_macos_ui};
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
use ruin_ui_platform_wayland::start_wayland_ui;
|
use ruin_ui_platform_wayland::start_wayland_ui;
|
||||||
|
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
@@ -24,6 +29,16 @@ use crate::input::{
|
|||||||
};
|
};
|
||||||
use crate::view::View;
|
use crate::view::View;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn start_platform_ui() -> ruin_ui::UiRuntime {
|
||||||
|
start_wayland_ui()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn start_platform_ui() -> ruin_ui::UiRuntime {
|
||||||
|
start_macos_ui()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
pub(crate) spec: WindowSpec,
|
pub(crate) spec: WindowSpec,
|
||||||
@@ -92,8 +107,7 @@ impl App {
|
|||||||
pub fn mount<M: Mountable>(self, root: M) -> MountedApp<M> {
|
pub fn mount<M: Mountable>(self, root: M) -> MountedApp<M> {
|
||||||
MountedApp {
|
MountedApp {
|
||||||
window: self.window,
|
window: self.window,
|
||||||
root: Rc::new(root),
|
root,
|
||||||
render_state: Rc::new(RenderState::default()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,19 +153,17 @@ impl Mountable for Element {
|
|||||||
|
|
||||||
pub struct MountedApp<M: Mountable> {
|
pub struct MountedApp<M: Mountable> {
|
||||||
pub(crate) window: Window,
|
pub(crate) window: Window,
|
||||||
pub(crate) root: Rc<M>,
|
pub(crate) root: M,
|
||||||
pub(crate) render_state: Rc<RenderState>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Mountable> MountedApp<M> {
|
impl<M: Mountable> MountedApp<M> {
|
||||||
pub async fn run(self) -> Result<()> {
|
async fn run_with_ui_runtime(
|
||||||
let MountedApp {
|
app_window: Window,
|
||||||
window: app_window,
|
root: M,
|
||||||
root,
|
mut ui: ruin_ui::UiRuntime,
|
||||||
render_state,
|
) -> Result<()> {
|
||||||
} = self;
|
let root = Rc::new(root);
|
||||||
|
let render_state = Rc::new(RenderState::default());
|
||||||
let mut ui = start_wayland_ui();
|
|
||||||
let window = ui.create_window(app_window.spec.clone())?;
|
let window = ui.create_window(app_window.spec.clone())?;
|
||||||
let initial_viewport = app_window
|
let initial_viewport = app_window
|
||||||
.spec
|
.spec
|
||||||
@@ -159,6 +171,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
.unwrap_or(UiSize::new(960.0, 640.0));
|
.unwrap_or(UiSize::new(960.0, 640.0));
|
||||||
|
|
||||||
let viewport = ruin_reactivity::cell(initial_viewport);
|
let viewport = ruin_reactivity::cell(initial_viewport);
|
||||||
|
let configure_serial = ruin_reactivity::cell(0_u64);
|
||||||
let scene_version = StdCell::new(0_u64);
|
let scene_version = StdCell::new(0_u64);
|
||||||
let text_system = Rc::new(RefCell::new(TextSystem::new()));
|
let text_system = Rc::new(RefCell::new(TextSystem::new()));
|
||||||
let layout_cache = Rc::new(RefCell::new(LayoutCache::new()));
|
let layout_cache = Rc::new(RefCell::new(LayoutCache::new()));
|
||||||
@@ -177,6 +190,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
let _scene_effect = effect({
|
let _scene_effect = effect({
|
||||||
let window = window.clone();
|
let window = window.clone();
|
||||||
let viewport = viewport.clone();
|
let viewport = viewport.clone();
|
||||||
|
let configure_serial = configure_serial.clone();
|
||||||
let text_system = Rc::clone(&text_system);
|
let text_system = Rc::clone(&text_system);
|
||||||
let layout_cache = Rc::clone(&layout_cache);
|
let layout_cache = Rc::clone(&layout_cache);
|
||||||
let interaction_tree = Rc::clone(&interaction_tree);
|
let interaction_tree = Rc::clone(&interaction_tree);
|
||||||
@@ -193,6 +207,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
let interaction_version = input_state.interaction_version.clone();
|
let interaction_version = input_state.interaction_version.clone();
|
||||||
move || {
|
move || {
|
||||||
let viewport = viewport.get();
|
let viewport = viewport.get();
|
||||||
|
let _ = configure_serial.get();
|
||||||
let version = scene_version.get().wrapping_add(1);
|
let version = scene_version.get().wrapping_add(1);
|
||||||
scene_version.set(version);
|
scene_version.set(version);
|
||||||
let _ = text_selection.version.get();
|
let _ = text_selection.version.get();
|
||||||
@@ -245,7 +260,8 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
if window_resizable {
|
if window_resizable {
|
||||||
let current_min = *last_min_size.borrow();
|
let current_min = *last_min_size.borrow();
|
||||||
if current_min != Some(desired_min) {
|
if current_min != Some(desired_min) {
|
||||||
let _ = window.update(WindowUpdate::new().min_inner_size(Some(desired_min)));
|
let _ =
|
||||||
|
window.update(WindowUpdate::new().min_inner_size(Some(desired_min)));
|
||||||
*last_min_size.borrow_mut() = Some(desired_min);
|
*last_min_size.borrow_mut() = Some(desired_min);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -297,6 +313,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut should_flush_reactive_work = false;
|
||||||
for event in iter::once(event).chain(ui.take_pending_events()) {
|
for event in iter::once(event).chain(ui.take_pending_events()) {
|
||||||
match event {
|
match event {
|
||||||
PlatformEvent::Configured {
|
PlatformEvent::Configured {
|
||||||
@@ -310,6 +327,8 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
"app received Configured, queuing layout effect"
|
"app received Configured, queuing layout effect"
|
||||||
);
|
);
|
||||||
let _ = viewport.set(configuration.actual_inner_size);
|
let _ = viewport.set(configuration.actual_inner_size);
|
||||||
|
configure_serial.update(|value| *value = value.wrapping_add(1));
|
||||||
|
should_flush_reactive_work = true;
|
||||||
}
|
}
|
||||||
PlatformEvent::Pointer { window_id, event } if window_id == window.id() => {
|
PlatformEvent::Pointer { window_id, event } if window_id == window.id() => {
|
||||||
Self::handle_pointer_event(
|
Self::handle_pointer_event(
|
||||||
@@ -320,6 +339,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
&mut input_state,
|
&mut input_state,
|
||||||
event,
|
event,
|
||||||
)?;
|
)?;
|
||||||
|
should_flush_reactive_work = true;
|
||||||
}
|
}
|
||||||
PlatformEvent::Keyboard { window_id, event } if window_id == window.id() => {
|
PlatformEvent::Keyboard { window_id, event } if window_id == window.id() => {
|
||||||
Self::handle_keyboard_event(
|
Self::handle_keyboard_event(
|
||||||
@@ -329,22 +349,74 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
&input_state,
|
&input_state,
|
||||||
event,
|
event,
|
||||||
)?;
|
)?;
|
||||||
|
should_flush_reactive_work = true;
|
||||||
}
|
}
|
||||||
PlatformEvent::CloseRequested { window_id } if window_id == window.id() => {
|
PlatformEvent::CloseRequested { window_id } if window_id == window.id() => {
|
||||||
|
tracing::debug!(
|
||||||
|
target: "ruin_app::lifecycle",
|
||||||
|
?window_id,
|
||||||
|
"app received CloseRequested, issuing open(false)"
|
||||||
|
);
|
||||||
let _ = window.update(WindowUpdate::new().open(false));
|
let _ = window.update(WindowUpdate::new().open(false));
|
||||||
}
|
}
|
||||||
PlatformEvent::Closed { window_id } if window_id == window.id() => {
|
PlatformEvent::Closed { window_id } if window_id == window.id() => {
|
||||||
|
tracing::debug!(
|
||||||
|
target: "ruin_app::lifecycle",
|
||||||
|
?window_id,
|
||||||
|
"app received Closed, shutting down ui runtime"
|
||||||
|
);
|
||||||
ui.shutdown()?;
|
ui.shutdown()?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if should_flush_reactive_work {
|
||||||
|
current_reactor().flush_now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.shutdown()?;
|
ui.shutdown()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: Mountable> MountedApp<M> {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
pub async fn run(self) -> Result<()> {
|
||||||
|
let ui = start_platform_ui();
|
||||||
|
Self::run_with_ui_runtime(self.window, self.root, ui).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub async fn run(self) -> Result<()>
|
||||||
|
where
|
||||||
|
M: Send + 'static,
|
||||||
|
{
|
||||||
|
let ui = start_platform_ui();
|
||||||
|
let (done_tx, done_rx) = std::sync::mpsc::channel::<std::result::Result<(), String>>();
|
||||||
|
let window = self.window;
|
||||||
|
let root = self.root;
|
||||||
|
let _worker = spawn_worker(
|
||||||
|
move || {
|
||||||
|
queue_future(async move {
|
||||||
|
let result = Self::run_with_ui_runtime(window, root, ui)
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_string());
|
||||||
|
let _ = done_tx.send(result);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|| {},
|
||||||
|
);
|
||||||
|
|
||||||
|
run_macos_app_event_loop();
|
||||||
|
|
||||||
|
match done_rx.recv() {
|
||||||
|
Ok(Ok(())) => Ok(()),
|
||||||
|
Ok(Err(err)) => Err(err.into()),
|
||||||
|
Err(_) => Err("ui worker terminated before completion".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_pointer_event(
|
pub(crate) fn handle_pointer_event(
|
||||||
window: &WindowController,
|
window: &WindowController,
|
||||||
@@ -508,7 +580,10 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn interaction_ids(targets: &[ruin_ui::HitTarget]) -> Vec<ruin_ui::ElementId> {
|
fn interaction_ids(targets: &[ruin_ui::HitTarget]) -> Vec<ruin_ui::ElementId> {
|
||||||
targets.iter().filter_map(|target| target.element_id).collect()
|
targets
|
||||||
|
.iter()
|
||||||
|
.filter_map(|target| target.element_id)
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_interaction_state(
|
fn update_interaction_state(
|
||||||
@@ -518,8 +593,14 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
) -> bool {
|
) -> bool {
|
||||||
let previous_hovered = input_state.hovered_elements.get();
|
let previous_hovered = input_state.hovered_elements.get();
|
||||||
let previous_pressed = input_state.pressed_elements.get();
|
let previous_pressed = input_state.pressed_elements.get();
|
||||||
let hovered_changed = input_state.hovered_elements.set(hovered_ids.clone()).is_some();
|
let hovered_changed = input_state
|
||||||
let pressed_changed = input_state.pressed_elements.set(pressed_ids.clone()).is_some();
|
.hovered_elements
|
||||||
|
.set(hovered_ids.clone())
|
||||||
|
.is_some();
|
||||||
|
let pressed_changed = input_state
|
||||||
|
.pressed_elements
|
||||||
|
.set(pressed_ids.clone())
|
||||||
|
.is_some();
|
||||||
if hovered_changed || pressed_changed {
|
if hovered_changed || pressed_changed {
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
target: "ruin_app::interaction",
|
target: "ruin_app::interaction",
|
||||||
|
|||||||
@@ -99,10 +99,7 @@ pub(crate) fn render_with_context_and_interaction(
|
|||||||
RenderOutput { view, side_effects }
|
RenderOutput { view, side_effects }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_render_context(
|
pub(crate) fn with_render_context(context: RenderContext, render: impl FnOnce() -> View) -> View {
|
||||||
context: RenderContext,
|
|
||||||
render: impl FnOnce() -> View,
|
|
||||||
) -> View {
|
|
||||||
CURRENT_RENDER_CONTEXT.with(|slot| {
|
CURRENT_RENDER_CONTEXT.with(|slot| {
|
||||||
let previous = slot.replace(Some(context.clone()));
|
let previous = slot.replace(Some(context.clone()));
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::any::type_name;
|
|
||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
|
use std::any::type_name;
|
||||||
use std::cell::Cell as StdCell;
|
use std::cell::Cell as StdCell;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
@@ -9,8 +9,8 @@ use ruin_ui::{ElementId, InteractionTree, KeyboardEvent, KeyboardEventKind, Keyb
|
|||||||
|
|
||||||
use crate::ContextKey;
|
use crate::ContextKey;
|
||||||
use crate::context::{
|
use crate::context::{
|
||||||
ContextEntry, MemoSlot, ResourceSlot, with_hook_slot,
|
ContextEntry, MemoSlot, ResourceSlot, with_hook_slot, with_render_context,
|
||||||
with_render_context, with_render_context_state,
|
with_render_context_state,
|
||||||
};
|
};
|
||||||
use crate::input::ShortcutBinding;
|
use crate::input::ShortcutBinding;
|
||||||
|
|
||||||
@@ -114,7 +114,10 @@ pub fn use_context<C: ContextKey>() -> C::Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn provide<C: ContextKey>(value: C::Value, render: impl FnOnce() -> crate::view::View) -> crate::view::View {
|
pub fn provide<C: ContextKey>(
|
||||||
|
value: C::Value,
|
||||||
|
render: impl FnOnce() -> crate::view::View,
|
||||||
|
) -> crate::view::View {
|
||||||
with_render_context_state(|context| {
|
with_render_context_state(|context| {
|
||||||
let mut context_entries = (*context.context_entries).clone();
|
let mut context_entries = (*context.context_entries).clone();
|
||||||
context_entries.push(ContextEntry::new::<C>(value));
|
context_entries.push(ContextEntry::new::<C>(value));
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ use std::collections::HashMap;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use ruin_ui::{
|
use ruin_ui::{
|
||||||
CursorIcon, DisplayItem, ElementId, HitTarget, InteractionTree, KeyboardEvent,
|
CursorIcon, DisplayItem, ElementId, HitTarget, InteractionTree, KeyboardEvent, PointerButton,
|
||||||
PointerButton, PointerEvent, Quad, RoutedPointerEvent,
|
PointerEvent, Quad, RoutedPointerEvent, RoutedPointerEventKind, WindowController,
|
||||||
RoutedPointerEventKind, WindowController,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::hooks::{Shortcut, ShortcutScope, Signal};
|
use crate::hooks::{Shortcut, ShortcutScope, Signal};
|
||||||
@@ -76,8 +75,7 @@ impl EventBindings {
|
|||||||
event: &KeyboardEvent,
|
event: &KeyboardEvent,
|
||||||
interaction_tree: &InteractionTree,
|
interaction_tree: &InteractionTree,
|
||||||
) {
|
) {
|
||||||
let Some(handler) =
|
let Some(handler) = key_handler_for_focus(&self.on_key, focused_element, interaction_tree)
|
||||||
key_handler_for_focus(&self.on_key, focused_element, interaction_tree)
|
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -200,20 +198,29 @@ pub(crate) fn sync_primary_selection(
|
|||||||
interaction_tree: &InteractionTree,
|
interaction_tree: &InteractionTree,
|
||||||
selection: Option<TextSelection>,
|
selection: Option<TextSelection>,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
let Some(selection) = selection else {
|
#[cfg(not(target_os = "linux"))]
|
||||||
window.set_primary_selection_text(String::new())?;
|
{
|
||||||
return Ok(());
|
let _ = (window, interaction_tree, selection);
|
||||||
};
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
let Some(text) = interaction_tree.text_for_element(selection.element_id) else {
|
#[cfg(target_os = "linux")]
|
||||||
return Ok(());
|
{
|
||||||
};
|
let Some(selection) = selection else {
|
||||||
let copied = text
|
window.set_primary_selection_text(String::new())?;
|
||||||
.selected_text(selection.anchor, selection.focus)
|
return Ok(());
|
||||||
.unwrap_or_default()
|
};
|
||||||
.to_owned();
|
|
||||||
window.set_primary_selection_text(copied)?;
|
let Some(text) = interaction_tree.text_for_element(selection.element_id) else {
|
||||||
Ok(())
|
return Ok(());
|
||||||
|
};
|
||||||
|
let copied = text
|
||||||
|
.selected_text(selection.anchor, selection.focus)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_owned();
|
||||||
|
window.set_primary_selection_text(copied)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn scroll_handler_for_event<'a>(
|
pub(crate) fn scroll_handler_for_event<'a>(
|
||||||
@@ -325,7 +332,11 @@ pub(crate) fn build_focused_ancestor_chain(
|
|||||||
|
|
||||||
// View helper methods for attaching handlers — used by primitives
|
// View helper methods for attaching handlers — used by primitives
|
||||||
impl View {
|
impl View {
|
||||||
pub(crate) fn with_press_handler(mut self, element_id: ElementId, handler: PressHandler) -> Self {
|
pub(crate) fn with_press_handler(
|
||||||
|
mut self,
|
||||||
|
element_id: ElementId,
|
||||||
|
handler: PressHandler,
|
||||||
|
) -> Self {
|
||||||
self.bindings.on_press.insert(element_id, handler);
|
self.bindings.on_press.insert(element_id, handler);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -339,11 +350,7 @@ impl View {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_key_handler(
|
pub(crate) fn with_key_handler(mut self, element_id: ElementId, handler: KeyHandler) -> Self {
|
||||||
mut self,
|
|
||||||
element_id: ElementId,
|
|
||||||
handler: KeyHandler,
|
|
||||||
) -> Self {
|
|
||||||
self.bindings.on_key.insert(element_id, handler);
|
self.bindings.on_key.insert(element_id, handler);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ use std::error::Error;
|
|||||||
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod colors;
|
pub mod colors;
|
||||||
pub mod converters;
|
|
||||||
mod context;
|
mod context;
|
||||||
|
pub mod converters;
|
||||||
mod hooks;
|
mod hooks;
|
||||||
mod input;
|
mod input;
|
||||||
pub mod primitives;
|
pub mod primitives;
|
||||||
@@ -20,22 +20,22 @@ pub mod view;
|
|||||||
|
|
||||||
// Re-export public items from each module.
|
// Re-export public items from each module.
|
||||||
|
|
||||||
pub use app::{App, Component, ContextKey, MountedApp, Mountable, Window, __render_mountable_for_test};
|
pub use app::{
|
||||||
|
__render_mountable_for_test, App, Component, ContextKey, Mountable, MountedApp, Window,
|
||||||
|
};
|
||||||
pub use converters::{IntoBorder, IntoEdges, IntoShadow};
|
pub use converters::{IntoBorder, IntoEdges, IntoShadow};
|
||||||
pub use hooks::{
|
pub use hooks::{
|
||||||
BlockWidget, FocusScope, InteractionState, Key, Memo, Resource, ResourceState,
|
BlockWidget, FocusScope, InteractionState, Key, Memo, Resource, ResourceState, ScrollBoxWidget,
|
||||||
ScrollBoxWidget, Shortcut, ShortcutScope, Signal, WidgetRef, provide, use_context,
|
Shortcut, ShortcutScope, Signal, WidgetRef, provide, use_context, use_effect,
|
||||||
use_effect, use_interaction_state, use_memo, use_resource, use_shortcut,
|
use_interaction_state, use_memo, use_resource, use_shortcut, use_shortcut_with_context,
|
||||||
use_shortcut_with_context, use_signal, use_widget_ref, use_window_title,
|
use_signal, use_widget_ref, use_window_title,
|
||||||
};
|
};
|
||||||
pub use primitives::{
|
pub use primitives::{
|
||||||
ButtonBuilder, ContainerBuilder, ContainerProps, FontWeight, ScrollBoxBuilder, TextBuilder,
|
ButtonBuilder, ContainerBuilder, ContainerProps, FontWeight, ScrollBoxBuilder, TextBuilder,
|
||||||
TextRole, block, button, column, row, scroll_box, text,
|
TextRole, block, button, column, row, scroll_box, text,
|
||||||
};
|
};
|
||||||
pub use text_style::PartialTextStyle;
|
pub use text_style::PartialTextStyle;
|
||||||
pub use view::{
|
pub use view::{ChildViews, Children, IntoView, LazyChildren, TextChildren, TextValue, View};
|
||||||
ChildViews, Children, IntoView, LazyChildren, TextChildren, TextValue, View,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bare string literals as container children produce a text element with context-aware styling.
|
// Bare string literals as container children produce a text element with context-aware styling.
|
||||||
// These impls live here (not in view.rs) so they can call primitives::text().
|
// These impls live here (not in view.rs) so they can call primitives::text().
|
||||||
@@ -58,10 +58,11 @@ pub use ruin_app_proc_macros::{component, context_provider, view};
|
|||||||
pub type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
pub type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
|
pub use crate::PartialTextStyle;
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
App, BlockWidget, ButtonBuilder, ChildViews, Children, Component, ContainerBuilder,
|
App, BlockWidget, ButtonBuilder, ChildViews, Children, Component, ContainerBuilder,
|
||||||
ContextKey, FocusScope, FontWeight, InteractionState, IntoBorder, IntoEdges, IntoView,
|
ContextKey, FocusScope, FontWeight, InteractionState, IntoBorder, IntoEdges, IntoView, Key,
|
||||||
Key, Memo, Mountable, Pending, Ready, Resource, ResourceState, Result, ScrollBoxBuilder,
|
Memo, Mountable, Pending, Ready, Resource, ResourceState, Result, ScrollBoxBuilder,
|
||||||
ScrollBoxWidget, Shortcut, ShortcutScope, Signal, TextBuilder, TextChildren, TextRole,
|
ScrollBoxWidget, Shortcut, ShortcutScope, Signal, TextBuilder, TextChildren, TextRole,
|
||||||
TextValue, View, WidgetRef, Window, block, button, colors, column, component,
|
TextValue, View, WidgetRef, Window, block, button, colors, column, component,
|
||||||
context_provider, provide, row, scroll_box, surfaces, text, use_context, use_effect,
|
context_provider, provide, row, scroll_box, surfaces, text, use_context, use_effect,
|
||||||
@@ -73,7 +74,6 @@ pub mod prelude {
|
|||||||
PointerButton, PointerEventKind, RoutedPointerEvent, RoutedPointerEventKind,
|
PointerButton, PointerEventKind, RoutedPointerEvent, RoutedPointerEventKind,
|
||||||
ScrollbarStyle, TextFontFamily, TextStyle, TextWrap, UiSize,
|
ScrollbarStyle, TextFontFamily, TextStyle, TextWrap, UiSize,
|
||||||
};
|
};
|
||||||
pub use crate::PartialTextStyle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -201,7 +201,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let widget_ref = widget_ref_slot.borrow().clone().expect("widget ref should be set");
|
let widget_ref = widget_ref_slot
|
||||||
|
.borrow()
|
||||||
|
.clone()
|
||||||
|
.expect("widget ref should be set");
|
||||||
let element_id = widget_ref
|
let element_id = widget_ref
|
||||||
.element_id()
|
.element_id()
|
||||||
.expect("widget ref should have an element id after render");
|
.expect("widget ref should have an element id after render");
|
||||||
@@ -216,7 +219,10 @@ mod tests {
|
|||||||
let pressed = Rc::clone(&pressed);
|
let pressed = Rc::clone(&pressed);
|
||||||
let widget_ref_slot = Rc::clone(&widget_ref_slot);
|
let widget_ref_slot = Rc::clone(&widget_ref_slot);
|
||||||
move || {
|
move || {
|
||||||
let widget_ref = widget_ref_slot.borrow().clone().expect("widget ref should persist");
|
let widget_ref = widget_ref_slot
|
||||||
|
.borrow()
|
||||||
|
.clone()
|
||||||
|
.expect("widget ref should persist");
|
||||||
*hovered.borrow_mut() = Some(use_interaction_state(widget_ref.clone()).hovered);
|
*hovered.borrow_mut() = Some(use_interaction_state(widget_ref.clone()).hovered);
|
||||||
*pressed.borrow_mut() = Some(use_interaction_state(widget_ref.clone()).pressed);
|
*pressed.borrow_mut() = Some(use_interaction_state(widget_ref.clone()).pressed);
|
||||||
block().widget_ref(widget_ref).children(())
|
block().widget_ref(widget_ref).children(())
|
||||||
@@ -292,7 +298,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scroll_box_arrow_keys_work_after_clicking_text_content() {
|
fn scroll_box_arrow_keys_work_after_clicking_text_content() {
|
||||||
use ruin_ui::{KeyboardEvent, KeyboardEventKind, KeyboardKey, PointerEvent, PointerEventKind, PointerButton};
|
use ruin_ui::{
|
||||||
|
KeyboardEvent, KeyboardEventKind, KeyboardKey, PointerButton, PointerEvent,
|
||||||
|
PointerEventKind,
|
||||||
|
};
|
||||||
|
|
||||||
let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
|
let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
|
||||||
let render = render_with_context(Rc::new(RenderState::default()), {
|
let render = render_with_context(Rc::new(RenderState::default()), {
|
||||||
@@ -428,7 +437,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn live_input_path_scrolls_a_scroll_box_rendered_inside_a_branch() {
|
fn live_input_path_scrolls_a_scroll_box_rendered_inside_a_branch() {
|
||||||
use ruin_ui::{PointerEvent, PointerEventKind, PointerButton};
|
use ruin_ui::{PointerButton, PointerEvent, PointerEventKind};
|
||||||
|
|
||||||
let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
|
let offset_slot = Rc::new(RefCell::new(None::<Signal<f32>>));
|
||||||
let render = render_with_context(Rc::new(RenderState::default()), {
|
let render = render_with_context(Rc::new(RenderState::default()), {
|
||||||
@@ -466,8 +475,11 @@ mod tests {
|
|||||||
.borrow()
|
.borrow()
|
||||||
.clone()
|
.clone()
|
||||||
.expect("scroll signal should have been captured");
|
.expect("scroll signal should have been captured");
|
||||||
let snapshot =
|
let snapshot = ruin_ui::layout_snapshot(
|
||||||
ruin_ui::layout_snapshot(1, ruin_ui::UiSize::new(1080.0, 760.0), render.view.element());
|
1,
|
||||||
|
ruin_ui::UiSize::new(1080.0, 760.0),
|
||||||
|
render.view.element(),
|
||||||
|
);
|
||||||
let interaction_tree = RefCell::new(Some(snapshot.interaction_tree.clone()));
|
let interaction_tree = RefCell::new(Some(snapshot.interaction_tree.clone()));
|
||||||
let bindings = RefCell::new(render.view.bindings.clone());
|
let bindings = RefCell::new(render.view.bindings.clone());
|
||||||
let mut pointer_router = ruin_ui::PointerRouter::new();
|
let mut pointer_router = ruin_ui::PointerRouter::new();
|
||||||
@@ -511,7 +523,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scroll_box_stays_interactive_when_it_appears_on_a_later_render() {
|
fn scroll_box_stays_interactive_when_it_appears_on_a_later_render() {
|
||||||
use ruin_ui::{PointerEvent, PointerEventKind, PointerButton};
|
use ruin_ui::{PointerButton, PointerEvent, PointerEventKind};
|
||||||
|
|
||||||
let state = Rc::new(RenderState::default());
|
let state = Rc::new(RenderState::default());
|
||||||
let ready_slot = Rc::new(RefCell::new(None::<Signal<bool>>));
|
let ready_slot = Rc::new(RefCell::new(None::<Signal<bool>>));
|
||||||
@@ -571,8 +583,11 @@ mod tests {
|
|||||||
let _ = ready.set(true);
|
let _ = ready.set(true);
|
||||||
|
|
||||||
let render = render_once(state, ready_slot, Rc::clone(&offset_slot));
|
let render = render_once(state, ready_slot, Rc::clone(&offset_slot));
|
||||||
let snapshot =
|
let snapshot = ruin_ui::layout_snapshot(
|
||||||
ruin_ui::layout_snapshot(1, ruin_ui::UiSize::new(1080.0, 760.0), render.view.element());
|
1,
|
||||||
|
ruin_ui::UiSize::new(1080.0, 760.0),
|
||||||
|
render.view.element(),
|
||||||
|
);
|
||||||
let interaction_tree = RefCell::new(Some(snapshot.interaction_tree.clone()));
|
let interaction_tree = RefCell::new(Some(snapshot.interaction_tree.clone()));
|
||||||
let bindings = RefCell::new(render.view.bindings.clone());
|
let bindings = RefCell::new(render.view.bindings.clone());
|
||||||
let mut pointer_router = ruin_ui::PointerRouter::new();
|
let mut pointer_router = ruin_ui::PointerRouter::new();
|
||||||
@@ -646,8 +661,11 @@ mod tests {
|
|||||||
.borrow()
|
.borrow()
|
||||||
.clone()
|
.clone()
|
||||||
.expect("scroll signal should have been captured");
|
.expect("scroll signal should have been captured");
|
||||||
let snapshot =
|
let snapshot = ruin_ui::layout_snapshot(
|
||||||
ruin_ui::layout_snapshot(1, ruin_ui::UiSize::new(1080.0, 760.0), render.view.element());
|
1,
|
||||||
|
ruin_ui::UiSize::new(1080.0, 760.0),
|
||||||
|
render.view.element(),
|
||||||
|
);
|
||||||
let interaction_tree = RefCell::new(Some(snapshot.interaction_tree.clone()));
|
let interaction_tree = RefCell::new(Some(snapshot.interaction_tree.clone()));
|
||||||
let bindings = RefCell::new(render.view.bindings.clone());
|
let bindings = RefCell::new(render.view.bindings.clone());
|
||||||
let mut pointer_router = ruin_ui::PointerRouter::new();
|
let mut pointer_router = ruin_ui::PointerRouter::new();
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ use ruin_ui::{CursorIcon, Edges, Element, RoutedPointerEvent};
|
|||||||
use crate::context::allocate_element_id;
|
use crate::context::allocate_element_id;
|
||||||
use crate::input::PressHandler;
|
use crate::input::PressHandler;
|
||||||
use crate::primitives::ContainerProps;
|
use crate::primitives::ContainerProps;
|
||||||
|
use crate::surfaces;
|
||||||
use crate::text_style::{pop_text_style, push_text_style};
|
use crate::text_style::{pop_text_style, push_text_style};
|
||||||
use crate::view::{Children, View};
|
use crate::view::{Children, View};
|
||||||
use crate::surfaces;
|
|
||||||
|
|
||||||
pub struct ButtonBuilder {
|
pub struct ButtonBuilder {
|
||||||
pub(crate) props: ContainerProps,
|
pub(crate) props: ContainerProps,
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ pub(crate) fn current_text_style() -> PartialTextStyle {
|
|||||||
stack
|
stack
|
||||||
.borrow()
|
.borrow()
|
||||||
.iter()
|
.iter()
|
||||||
.fold(PartialTextStyle::default(), |acc, layer| acc.merge_over(layer))
|
.fold(PartialTextStyle::default(), |acc, layer| {
|
||||||
|
acc.merge_over(layer)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ impl Children for () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl<T: IntoView> Children for T {
|
impl<T: IntoView> Children for T {
|
||||||
fn into_views(self) -> Vec<View> {
|
fn into_views(self) -> Vec<View> {
|
||||||
vec![self.into_view()]
|
vec![self.into_view()]
|
||||||
|
|||||||
@@ -60,10 +60,7 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
|||||||
Some(prop) if prop.ident == "children" => {
|
Some(prop) if prop.ident == "children" => {
|
||||||
let prop = inputs.pop().expect("last input should exist");
|
let prop = inputs.pop().expect("last input should exist");
|
||||||
let kind = parse_children_contract_kind(&prop.ty)?;
|
let kind = parse_children_contract_kind(&prop.ty)?;
|
||||||
Some(ChildContract {
|
Some(ChildContract { ty: prop.ty, kind })
|
||||||
ty: prop.ty,
|
|
||||||
kind,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
@@ -87,7 +84,8 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
|||||||
}
|
}
|
||||||
Some(contract) => {
|
Some(contract) => {
|
||||||
let child_arg_ty = child_builder_arg_type_tokens(contract.kind);
|
let child_arg_ty = child_builder_arg_type_tokens(contract.kind);
|
||||||
let child_value = wrap_special_value_tokens(contract.kind, &format_ident!("children"));
|
let child_value =
|
||||||
|
wrap_special_value_tokens(contract.kind, &format_ident!("children"));
|
||||||
let child_field_ident = child_field_ident
|
let child_field_ident = child_field_ident
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("child field ident should exist");
|
.expect("child field ident should exist");
|
||||||
@@ -574,10 +572,7 @@ fn child_builder_arg_type_tokens(kind: SpecialValueKind) -> proc_macro2::TokenSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wrap_special_value_tokens(
|
fn wrap_special_value_tokens(kind: SpecialValueKind, ident: &Ident) -> proc_macro2::TokenStream {
|
||||||
kind: SpecialValueKind,
|
|
||||||
ident: &Ident,
|
|
||||||
) -> proc_macro2::TokenStream {
|
|
||||||
match kind {
|
match kind {
|
||||||
SpecialValueKind::Plain => quote! { #ident },
|
SpecialValueKind::Plain => quote! { #ident },
|
||||||
SpecialValueKind::ChildViews => quote! { ::ruin_app::ChildViews::from_children(#ident) },
|
SpecialValueKind::ChildViews => quote! { ::ruin_app::ChildViews::from_children(#ident) },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# RUIN - Runtime
|
# RUIN - Runtime
|
||||||
|
|
||||||
The RUIN runtime is an event-loop-per-thread, non-workstealing async executor implemented on top of Linux io_uring
|
The RUIN runtime is an event-loop-per-thread, non-workstealing async executor implemented on top of Linux io_uring
|
||||||
with
|
with
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
use crate::op::completion::{CompletionFuture, CompletionHandle};
|
use crate::op::completion::{CompletionFuture, CompletionHandle};
|
||||||
use crate::sys::linux::channel::runtime_waiter;
|
use crate::sys::current::channel::runtime_waiter;
|
||||||
|
|
||||||
/// Creates a bounded channel with room for at most `capacity` queued messages.
|
/// Creates a bounded channel with room for at most `capacity` queued messages.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
use crate::op::completion::{CompletionFuture, CompletionHandle};
|
use crate::op::completion::{CompletionFuture, CompletionHandle};
|
||||||
use crate::sys::linux::channel::runtime_waiter;
|
use crate::sys::current::channel::runtime_waiter;
|
||||||
|
|
||||||
/// Creates a single-use channel for transferring one value from a [`Sender`] to a [`Receiver`].
|
/// Creates a single-use channel for transferring one value from a [`Sender`] to a [`Receiver`].
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -3,44 +3,9 @@
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::RawFd;
|
use std::os::fd::RawFd;
|
||||||
|
|
||||||
use crate::op::completion::completion_for_current_thread;
|
|
||||||
use crate::platform::linux_x86_64::runtime::with_current_driver;
|
|
||||||
use crate::platform::linux_x86_64::uring::{IORING_OP_POLL_ADD, IoUringCqe};
|
|
||||||
|
|
||||||
/// Waits until `fd` becomes readable or reports an error/hangup condition.
|
/// Waits until `fd` becomes readable or reports an error/hangup condition.
|
||||||
pub async fn wait_readable(fd: RawFd) -> io::Result<()> {
|
pub async fn wait_readable(fd: RawFd) -> io::Result<()> {
|
||||||
submit_poll(fd, libc::POLLIN | libc::POLLERR | libc::POLLHUP).await
|
crate::sys::current::fd::wait_readable(fd).await
|
||||||
}
|
|
||||||
|
|
||||||
async fn submit_poll(fd: RawFd, mask: i16) -> io::Result<()> {
|
|
||||||
let (future, handle) = completion_for_current_thread::<io::Result<()>>();
|
|
||||||
let callback_handle = handle.clone();
|
|
||||||
let token = with_current_driver(|driver| {
|
|
||||||
driver.submit_operation(
|
|
||||||
move |sqe| {
|
|
||||||
sqe.opcode = IORING_OP_POLL_ADD;
|
|
||||||
sqe.fd = fd;
|
|
||||||
sqe.len = 0;
|
|
||||||
sqe.op_flags = mask as u32;
|
|
||||||
},
|
|
||||||
move |cqe| {
|
|
||||||
callback_handle.complete(cqe_to_result(cqe));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
handle.set_cancel(move || {
|
|
||||||
let _ = with_current_driver(|driver| driver.cancel_operation(token));
|
|
||||||
});
|
|
||||||
|
|
||||||
future.await
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cqe_to_result(cqe: IoUringCqe) -> io::Result<()> {
|
|
||||||
if cqe.res < 0 {
|
|
||||||
return Err(io::Error::from_raw_os_error(-cqe.res));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -53,8 +18,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn wait_readable_resolves_for_pipe() {
|
fn wait_readable_resolves_for_pipe() {
|
||||||
let mut fds = [0; 2];
|
let mut fds = [0; 2];
|
||||||
let result = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) };
|
let result = unsafe { libc::pipe(fds.as_mut_ptr()) };
|
||||||
assert_eq!(result, 0, "pipe2 should succeed");
|
assert_eq!(result, 0, "pipe should succeed");
|
||||||
let read_fd = fds[0];
|
let read_fd = fds[0];
|
||||||
let write_fd = fds[1];
|
let write_fd = fds[1];
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use crate::op::fs::{
|
|||||||
FileType as RawFileType, FsOp, MetadataTarget, OpenOptions as OpOpenOptions,
|
FileType as RawFileType, FsOp, MetadataTarget, OpenOptions as OpOpenOptions,
|
||||||
RawDirEntry as OpDirEntry, RawMetadata,
|
RawDirEntry as OpDirEntry, RawMetadata,
|
||||||
};
|
};
|
||||||
use crate::sys::linux::fs as sys_fs;
|
use crate::sys::current::fs as sys_fs;
|
||||||
|
|
||||||
struct FileInner {
|
struct FileInner {
|
||||||
fd: OwnedFd,
|
fd: OwnedFd,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Runtime, driver, async I/O, and channel primitives for RUIN.
|
//! Runtime, driver, async I/O, and channel primitives for RUIN.
|
||||||
//!
|
//!
|
||||||
//! The crate is centered around a single-threaded event loop with explicit worker threads,
|
//! The crate is centered around a single-threaded event loop with explicit worker threads,
|
||||||
//! JavaScript-style microtask/macrotask scheduling, and Linux `io_uring`-backed I/O.
|
//! JavaScript-style microtask/macrotask scheduling and platform-specific async I/O backends.
|
||||||
//!
|
//!
|
||||||
//! Most users will start with:
|
//! Most users will start with:
|
||||||
//!
|
//!
|
||||||
@@ -11,17 +11,22 @@
|
|||||||
//!
|
//!
|
||||||
//! # Platform support
|
//! # Platform support
|
||||||
//!
|
//!
|
||||||
//! `ruin-runtime` currently targets Linux on `x86_64`.
|
//! `ruin-runtime` currently targets:
|
||||||
|
//! - Linux `x86_64`
|
||||||
|
//! - macOS `aarch64`
|
||||||
//!
|
//!
|
||||||
//! RUIN runtime foundations.
|
//! RUIN runtime foundations.
|
||||||
//!
|
//!
|
||||||
//! This crate provides a Linux x86_64 runtime substrate: the mesh allocator, the driver, and a
|
//! This crate provides a platform runtime substrate with a single-threaded runtime loop and
|
||||||
//! single-threaded runtime loop with worker-thread task forwarding.
|
//! worker-thread task forwarding.
|
||||||
|
|
||||||
#![feature(thread_local)]
|
#![feature(thread_local)]
|
||||||
|
|
||||||
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
|
#[cfg(not(any(
|
||||||
compile_error!("ruin-runtime currently supports only Linux x86_64.");
|
all(target_os = "linux", target_arch = "x86_64"),
|
||||||
|
all(target_os = "macos", target_arch = "aarch64")
|
||||||
|
)))]
|
||||||
|
compile_error!("ruin-runtime currently supports Linux x86_64 and macOS aarch64.");
|
||||||
|
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
@@ -60,11 +65,24 @@ pub use ruin_runtime_proc_macros::async_main;
|
|||||||
/// thread before calling [`run`].
|
/// thread before calling [`run`].
|
||||||
pub use ruin_runtime_proc_macros::main;
|
pub use ruin_runtime_proc_macros::main;
|
||||||
|
|
||||||
/// Driver primitives re-exported from the Linux x86_64 backend.
|
/// Driver primitives re-exported from the active backend.
|
||||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
#[cfg(any(
|
||||||
pub use platform::linux_x86_64::driver::{
|
all(target_os = "linux", target_arch = "x86_64"),
|
||||||
|
all(target_os = "macos", target_arch = "aarch64")
|
||||||
|
))]
|
||||||
|
pub use platform::current::driver::{
|
||||||
Driver, ReadyEvents, ThreadNotifier, create, create_driver, monotonic_now,
|
Driver, ReadyEvents, ThreadNotifier, create, create_driver, monotonic_now,
|
||||||
};
|
};
|
||||||
|
/// Runtime/event-loop primitives re-exported from the active backend.
|
||||||
|
#[cfg(any(
|
||||||
|
all(target_os = "linux", target_arch = "x86_64"),
|
||||||
|
all(target_os = "macos", target_arch = "aarch64")
|
||||||
|
))]
|
||||||
|
pub use platform::current::runtime::{
|
||||||
|
IntervalHandle, JoinHandle, ThreadHandle, TimeoutHandle, WorkerHandle, clear_interval,
|
||||||
|
clear_timeout, current_thread_handle, queue_future, queue_microtask, queue_task, run,
|
||||||
|
run_ready_tasks, run_until_stalled, set_interval, set_timeout, spawn_worker, yield_now,
|
||||||
|
};
|
||||||
/// Public mesh-allocator surface re-exported from the Linux x86_64 backend.
|
/// Public mesh-allocator surface re-exported from the Linux x86_64 backend.
|
||||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
pub use platform::linux_x86_64::mesh_alloc::{
|
pub use platform::linux_x86_64::mesh_alloc::{
|
||||||
@@ -81,22 +99,17 @@ pub use platform::linux_x86_64::mesh_alloc::{
|
|||||||
/// Additional allocator helpers re-exported from the Linux x86_64 backend.
|
/// Additional allocator helpers re-exported from the Linux x86_64 backend.
|
||||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
pub use platform::linux_x86_64::mesh_alloc::{FreelistId, bitmaps_meshable};
|
pub use platform::linux_x86_64::mesh_alloc::{FreelistId, bitmaps_meshable};
|
||||||
/// Runtime/event-loop primitives re-exported from the Linux x86_64 backend.
|
|
||||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
|
||||||
pub use platform::linux_x86_64::runtime::{
|
|
||||||
IntervalHandle, JoinHandle, ThreadHandle, TimeoutHandle, WorkerHandle, clear_interval,
|
|
||||||
clear_timeout, current_thread_handle, queue_future, queue_microtask, queue_task, run,
|
|
||||||
set_interval, set_timeout, spawn_worker, yield_now,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Returns the default global mesh allocator configuration for this crate.
|
/// Returns the default global mesh allocator configuration for this crate.
|
||||||
///
|
///
|
||||||
/// This is useful when embedding the allocator in a `#[global_allocator]` static.
|
/// This is useful when embedding the allocator in a `#[global_allocator]` static.
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
pub const fn default_global_allocator() -> GlobalMeshAllocator {
|
pub const fn default_global_allocator() -> GlobalMeshAllocator {
|
||||||
GlobalMeshAllocator::with_default_config()
|
GlobalMeshAllocator::with_default_config()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{MeshAllocator, page_size};
|
use super::{MeshAllocator, page_size};
|
||||||
|
|
||||||
|
|||||||
@@ -73,10 +73,10 @@ impl TcpStream {
|
|||||||
where
|
where
|
||||||
A: ToSocketAddrs + Send + 'static,
|
A: ToSocketAddrs + Send + 'static,
|
||||||
{
|
{
|
||||||
let addrs = crate::sys::linux::net::resolve_addrs(addr).await?;
|
let addrs = crate::sys::current::net::resolve_addrs(addr).await?;
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
match crate::sys::linux::net::connect_stream(addr).await {
|
match crate::sys::current::net::connect_stream(addr).await {
|
||||||
Ok(fd) => return Ok(Self::from_owned_fd(fd)),
|
Ok(fd) => return Ok(Self::from_owned_fd(fd)),
|
||||||
Err(error) => last_error = Some(error),
|
Err(error) => last_error = Some(error),
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ impl TcpStream {
|
|||||||
/// Connects to `addr`, failing if the deadline elapses first.
|
/// Connects to `addr`, failing if the deadline elapses first.
|
||||||
pub async fn connect_timeout(addr: &SocketAddr, timeout: Duration) -> io::Result<Self> {
|
pub async fn connect_timeout(addr: &SocketAddr, timeout: Duration) -> io::Result<Self> {
|
||||||
validate_timeout(timeout)?;
|
validate_timeout(timeout)?;
|
||||||
crate::sys::linux::net::connect_stream_timeout(*addr, timeout)
|
crate::sys::current::net::connect_stream_timeout(*addr, timeout)
|
||||||
.await
|
.await
|
||||||
.map(Self::from_owned_fd)
|
.map(Self::from_owned_fd)
|
||||||
}
|
}
|
||||||
@@ -102,10 +102,10 @@ impl TcpStream {
|
|||||||
pub async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
pub async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
let data = match self.read_timeout_value() {
|
let data = match self.read_timeout_value() {
|
||||||
Some(timeout) => {
|
Some(timeout) => {
|
||||||
crate::sys::linux::net::recv_timeout(self.raw_fd(), buf.len(), 0, timeout).await?
|
crate::sys::current::net::recv_timeout(self.raw_fd(), buf.len(), 0, timeout).await?
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
crate::sys::linux::net::recv(NetOp::Recv {
|
crate::sys::current::net::recv(NetOp::Recv {
|
||||||
fd: self.raw_fd(),
|
fd: self.raw_fd(),
|
||||||
len: buf.len(),
|
len: buf.len(),
|
||||||
flags: 0,
|
flags: 0,
|
||||||
@@ -137,10 +137,11 @@ impl TcpStream {
|
|||||||
pub async fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
pub async fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
match self.write_timeout_value() {
|
match self.write_timeout_value() {
|
||||||
Some(timeout) => {
|
Some(timeout) => {
|
||||||
crate::sys::linux::net::send_timeout(self.raw_fd(), buf.to_vec(), 0, timeout).await
|
crate::sys::current::net::send_timeout(self.raw_fd(), buf.to_vec(), 0, timeout)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
crate::sys::linux::net::send(NetOp::Send {
|
crate::sys::current::net::send(NetOp::Send {
|
||||||
fd: self.raw_fd(),
|
fd: self.raw_fd(),
|
||||||
data: buf.to_vec(),
|
data: buf.to_vec(),
|
||||||
flags: 0,
|
flags: 0,
|
||||||
@@ -167,7 +168,7 @@ impl TcpStream {
|
|||||||
|
|
||||||
/// Shuts down the read, write, or both halves of the connection.
|
/// Shuts down the read, write, or both halves of the connection.
|
||||||
pub async fn shutdown(&self, how: Shutdown) -> io::Result<()> {
|
pub async fn shutdown(&self, how: Shutdown) -> io::Result<()> {
|
||||||
crate::sys::linux::net::shutdown(NetOp::Shutdown {
|
crate::sys::current::net::shutdown(NetOp::Shutdown {
|
||||||
fd: self.raw_fd(),
|
fd: self.raw_fd(),
|
||||||
how,
|
how,
|
||||||
})
|
})
|
||||||
@@ -176,39 +177,39 @@ impl TcpStream {
|
|||||||
|
|
||||||
/// Duplicates the underlying stream socket.
|
/// Duplicates the underlying stream socket.
|
||||||
pub async fn try_clone(&self) -> io::Result<Self> {
|
pub async fn try_clone(&self) -> io::Result<Self> {
|
||||||
crate::sys::linux::net::duplicate(self.raw_fd())
|
crate::sys::current::net::duplicate(self.raw_fd())
|
||||||
.await
|
.await
|
||||||
.map(Self::from_owned_fd)
|
.map(Self::from_owned_fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the local socket address of this stream.
|
/// Returns the local socket address of this stream.
|
||||||
pub fn local_addr(&self) -> io::Result<SocketAddr> {
|
pub fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||||
crate::sys::linux::net::local_addr(self.raw_fd())
|
crate::sys::current::net::local_addr(self.raw_fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the remote peer address of this stream.
|
/// Returns the remote peer address of this stream.
|
||||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||||
crate::sys::linux::net::peer_addr(self.raw_fd())
|
crate::sys::current::net::peer_addr(self.raw_fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the current `TCP_NODELAY` setting.
|
/// Reads the current `TCP_NODELAY` setting.
|
||||||
pub fn nodelay(&self) -> io::Result<bool> {
|
pub fn nodelay(&self) -> io::Result<bool> {
|
||||||
crate::sys::linux::net::nodelay(self.raw_fd())
|
crate::sys::current::net::nodelay(self.raw_fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enables or disables `TCP_NODELAY`.
|
/// Enables or disables `TCP_NODELAY`.
|
||||||
pub fn set_nodelay(&self, enabled: bool) -> io::Result<()> {
|
pub fn set_nodelay(&self, enabled: bool) -> io::Result<()> {
|
||||||
crate::sys::linux::net::set_nodelay(self.raw_fd(), enabled)
|
crate::sys::current::net::set_nodelay(self.raw_fd(), enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the socket's IP time-to-live value.
|
/// Reads the socket's IP time-to-live value.
|
||||||
pub fn ttl(&self) -> io::Result<u32> {
|
pub fn ttl(&self) -> io::Result<u32> {
|
||||||
crate::sys::linux::net::ttl(self.raw_fd())
|
crate::sys::current::net::ttl(self.raw_fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the socket's IP time-to-live value.
|
/// Sets the socket's IP time-to-live value.
|
||||||
pub fn set_ttl(&self, ttl: u32) -> io::Result<()> {
|
pub fn set_ttl(&self, ttl: u32) -> io::Result<()> {
|
||||||
crate::sys::linux::net::set_ttl(self.raw_fd(), ttl)
|
crate::sys::current::net::set_ttl(self.raw_fd(), ttl)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the read timeout used by async read operations on this handle.
|
/// Returns the read timeout used by async read operations on this handle.
|
||||||
@@ -270,10 +271,10 @@ impl TcpListener {
|
|||||||
where
|
where
|
||||||
A: ToSocketAddrs + Send + 'static,
|
A: ToSocketAddrs + Send + 'static,
|
||||||
{
|
{
|
||||||
let addrs = crate::sys::linux::net::resolve_addrs(addr).await?;
|
let addrs = crate::sys::current::net::resolve_addrs(addr).await?;
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
match crate::sys::linux::net::bind_listener(addr, None).await {
|
match crate::sys::current::net::bind_listener(addr, None).await {
|
||||||
Ok(fd) => return Ok(Self::from_owned_fd(fd)),
|
Ok(fd) => return Ok(Self::from_owned_fd(fd)),
|
||||||
Err(error) => last_error = Some(error),
|
Err(error) => last_error = Some(error),
|
||||||
}
|
}
|
||||||
@@ -289,7 +290,8 @@ impl TcpListener {
|
|||||||
|
|
||||||
/// Accepts an incoming connection.
|
/// Accepts an incoming connection.
|
||||||
pub async fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> {
|
pub async fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> {
|
||||||
let accepted = crate::sys::linux::net::accept(NetOp::Accept { fd: self.raw_fd() }).await?;
|
let accepted =
|
||||||
|
crate::sys::current::net::accept(NetOp::Accept { fd: self.raw_fd() }).await?;
|
||||||
|
|
||||||
let stream = TcpStream::from_owned_fd(unsafe { OwnedFd::from_raw_fd(accepted.fd) });
|
let stream = TcpStream::from_owned_fd(unsafe { OwnedFd::from_raw_fd(accepted.fd) });
|
||||||
Ok((stream, accepted.peer_addr))
|
Ok((stream, accepted.peer_addr))
|
||||||
@@ -297,17 +299,17 @@ impl TcpListener {
|
|||||||
|
|
||||||
/// Returns the local socket address of this listener.
|
/// Returns the local socket address of this listener.
|
||||||
pub fn local_addr(&self) -> io::Result<SocketAddr> {
|
pub fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||||
crate::sys::linux::net::local_addr(self.raw_fd())
|
crate::sys::current::net::local_addr(self.raw_fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the listener socket's IP time-to-live value.
|
/// Reads the listener socket's IP time-to-live value.
|
||||||
pub fn ttl(&self) -> io::Result<u32> {
|
pub fn ttl(&self) -> io::Result<u32> {
|
||||||
crate::sys::linux::net::ttl(self.raw_fd())
|
crate::sys::current::net::ttl(self.raw_fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the listener socket's IP time-to-live value.
|
/// Sets the listener socket's IP time-to-live value.
|
||||||
pub fn set_ttl(&self, ttl: u32) -> io::Result<()> {
|
pub fn set_ttl(&self, ttl: u32) -> io::Result<()> {
|
||||||
crate::sys::linux::net::set_ttl(self.raw_fd(), ttl)
|
crate::sys::current::net::set_ttl(self.raw_fd(), ttl)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_owned_fd(fd: OwnedFd) -> Self {
|
fn from_owned_fd(fd: OwnedFd) -> Self {
|
||||||
@@ -327,10 +329,10 @@ impl UdpSocket {
|
|||||||
where
|
where
|
||||||
A: ToSocketAddrs + Send + 'static,
|
A: ToSocketAddrs + Send + 'static,
|
||||||
{
|
{
|
||||||
let addrs = crate::sys::linux::net::resolve_addrs(addr).await?;
|
let addrs = crate::sys::current::net::resolve_addrs(addr).await?;
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
match crate::sys::linux::net::bind_datagram(addr).await {
|
match crate::sys::current::net::bind_datagram(addr).await {
|
||||||
Ok(fd) => return Ok(Self::from_owned_fd(fd)),
|
Ok(fd) => return Ok(Self::from_owned_fd(fd)),
|
||||||
Err(error) => last_error = Some(error),
|
Err(error) => last_error = Some(error),
|
||||||
}
|
}
|
||||||
@@ -352,10 +354,10 @@ impl UdpSocket {
|
|||||||
where
|
where
|
||||||
A: ToSocketAddrs + Send + 'static,
|
A: ToSocketAddrs + Send + 'static,
|
||||||
{
|
{
|
||||||
let addrs = crate::sys::linux::net::resolve_addrs(addr).await?;
|
let addrs = crate::sys::current::net::resolve_addrs(addr).await?;
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
match crate::sys::linux::net::connect(NetOp::Connect {
|
match crate::sys::current::net::connect(NetOp::Connect {
|
||||||
fd: self.raw_fd(),
|
fd: self.raw_fd(),
|
||||||
addr,
|
addr,
|
||||||
})
|
})
|
||||||
@@ -378,10 +380,11 @@ impl UdpSocket {
|
|||||||
pub async fn send(&self, buf: &[u8]) -> io::Result<usize> {
|
pub async fn send(&self, buf: &[u8]) -> io::Result<usize> {
|
||||||
match self.write_timeout_value() {
|
match self.write_timeout_value() {
|
||||||
Some(timeout) => {
|
Some(timeout) => {
|
||||||
crate::sys::linux::net::send_timeout(self.raw_fd(), buf.to_vec(), 0, timeout).await
|
crate::sys::current::net::send_timeout(self.raw_fd(), buf.to_vec(), 0, timeout)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
crate::sys::linux::net::send(NetOp::Send {
|
crate::sys::current::net::send(NetOp::Send {
|
||||||
fd: self.raw_fd(),
|
fd: self.raw_fd(),
|
||||||
data: buf.to_vec(),
|
data: buf.to_vec(),
|
||||||
flags: 0,
|
flags: 0,
|
||||||
@@ -395,10 +398,10 @@ impl UdpSocket {
|
|||||||
pub async fn recv(&self, buf: &mut [u8]) -> io::Result<usize> {
|
pub async fn recv(&self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
let data = match self.read_timeout_value() {
|
let data = match self.read_timeout_value() {
|
||||||
Some(timeout) => {
|
Some(timeout) => {
|
||||||
crate::sys::linux::net::recv_timeout(self.raw_fd(), buf.len(), 0, timeout).await?
|
crate::sys::current::net::recv_timeout(self.raw_fd(), buf.len(), 0, timeout).await?
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
crate::sys::linux::net::recv(NetOp::Recv {
|
crate::sys::current::net::recv(NetOp::Recv {
|
||||||
fd: self.raw_fd(),
|
fd: self.raw_fd(),
|
||||||
len: buf.len(),
|
len: buf.len(),
|
||||||
flags: 0,
|
flags: 0,
|
||||||
@@ -415,7 +418,7 @@ impl UdpSocket {
|
|||||||
pub async fn peek(&self, buf: &mut [u8]) -> io::Result<usize> {
|
pub async fn peek(&self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
let data = match self.read_timeout_value() {
|
let data = match self.read_timeout_value() {
|
||||||
Some(timeout) => {
|
Some(timeout) => {
|
||||||
crate::sys::linux::net::recv_timeout(
|
crate::sys::current::net::recv_timeout(
|
||||||
self.raw_fd(),
|
self.raw_fd(),
|
||||||
buf.len(),
|
buf.len(),
|
||||||
libc::MSG_PEEK,
|
libc::MSG_PEEK,
|
||||||
@@ -424,7 +427,7 @@ impl UdpSocket {
|
|||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
crate::sys::linux::net::recv(NetOp::Recv {
|
crate::sys::current::net::recv(NetOp::Recv {
|
||||||
fd: self.raw_fd(),
|
fd: self.raw_fd(),
|
||||||
len: buf.len(),
|
len: buf.len(),
|
||||||
flags: libc::MSG_PEEK,
|
flags: libc::MSG_PEEK,
|
||||||
@@ -442,13 +445,13 @@ impl UdpSocket {
|
|||||||
where
|
where
|
||||||
A: ToSocketAddrs + Send + 'static,
|
A: ToSocketAddrs + Send + 'static,
|
||||||
{
|
{
|
||||||
let addrs = crate::sys::linux::net::resolve_addrs(addr).await?;
|
let addrs = crate::sys::current::net::resolve_addrs(addr).await?;
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
let timeout = self.write_timeout_value();
|
let timeout = self.write_timeout_value();
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
let result = match timeout {
|
let result = match timeout {
|
||||||
Some(timeout) => {
|
Some(timeout) => {
|
||||||
crate::sys::linux::net::send_to_timeout(
|
crate::sys::current::net::send_to_timeout(
|
||||||
self.raw_fd(),
|
self.raw_fd(),
|
||||||
buf.to_vec(),
|
buf.to_vec(),
|
||||||
addr,
|
addr,
|
||||||
@@ -458,7 +461,7 @@ impl UdpSocket {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
crate::sys::linux::net::send_to(NetOp::SendTo {
|
crate::sys::current::net::send_to(NetOp::SendTo {
|
||||||
fd: self.raw_fd(),
|
fd: self.raw_fd(),
|
||||||
target: addr,
|
target: addr,
|
||||||
data: buf.to_vec(),
|
data: buf.to_vec(),
|
||||||
@@ -485,11 +488,11 @@ impl UdpSocket {
|
|||||||
pub async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
pub async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||||
let datagram = match self.read_timeout_value() {
|
let datagram = match self.read_timeout_value() {
|
||||||
Some(timeout) => {
|
Some(timeout) => {
|
||||||
crate::sys::linux::net::recv_from_timeout(self.raw_fd(), buf.len(), 0, timeout)
|
crate::sys::current::net::recv_from_timeout(self.raw_fd(), buf.len(), 0, timeout)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
crate::sys::linux::net::recv_from(NetOp::RecvFrom {
|
crate::sys::current::net::recv_from(NetOp::RecvFrom {
|
||||||
fd: self.raw_fd(),
|
fd: self.raw_fd(),
|
||||||
len: buf.len(),
|
len: buf.len(),
|
||||||
flags: 0,
|
flags: 0,
|
||||||
@@ -506,7 +509,7 @@ impl UdpSocket {
|
|||||||
pub async fn peek_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
pub async fn peek_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||||
let datagram = match self.read_timeout_value() {
|
let datagram = match self.read_timeout_value() {
|
||||||
Some(timeout) => {
|
Some(timeout) => {
|
||||||
crate::sys::linux::net::recv_from_timeout(
|
crate::sys::current::net::recv_from_timeout(
|
||||||
self.raw_fd(),
|
self.raw_fd(),
|
||||||
buf.len(),
|
buf.len(),
|
||||||
libc::MSG_PEEK,
|
libc::MSG_PEEK,
|
||||||
@@ -515,7 +518,7 @@ impl UdpSocket {
|
|||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
crate::sys::linux::net::recv_from(NetOp::RecvFrom {
|
crate::sys::current::net::recv_from(NetOp::RecvFrom {
|
||||||
fd: self.raw_fd(),
|
fd: self.raw_fd(),
|
||||||
len: buf.len(),
|
len: buf.len(),
|
||||||
flags: libc::MSG_PEEK,
|
flags: libc::MSG_PEEK,
|
||||||
@@ -530,39 +533,39 @@ impl UdpSocket {
|
|||||||
|
|
||||||
/// Duplicates the underlying UDP socket.
|
/// Duplicates the underlying UDP socket.
|
||||||
pub async fn try_clone(&self) -> io::Result<Self> {
|
pub async fn try_clone(&self) -> io::Result<Self> {
|
||||||
crate::sys::linux::net::duplicate(self.raw_fd())
|
crate::sys::current::net::duplicate(self.raw_fd())
|
||||||
.await
|
.await
|
||||||
.map(Self::from_owned_fd)
|
.map(Self::from_owned_fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the local socket address of this socket.
|
/// Returns the local socket address of this socket.
|
||||||
pub fn local_addr(&self) -> io::Result<SocketAddr> {
|
pub fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||||
crate::sys::linux::net::local_addr(self.raw_fd())
|
crate::sys::current::net::local_addr(self.raw_fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the connected peer address, if the socket has been connected.
|
/// Returns the connected peer address, if the socket has been connected.
|
||||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||||
crate::sys::linux::net::peer_addr(self.raw_fd())
|
crate::sys::current::net::peer_addr(self.raw_fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the `SO_BROADCAST` setting.
|
/// Reads the `SO_BROADCAST` setting.
|
||||||
pub fn broadcast(&self) -> io::Result<bool> {
|
pub fn broadcast(&self) -> io::Result<bool> {
|
||||||
crate::sys::linux::net::broadcast(self.raw_fd())
|
crate::sys::current::net::broadcast(self.raw_fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enables or disables `SO_BROADCAST`.
|
/// Enables or disables `SO_BROADCAST`.
|
||||||
pub fn set_broadcast(&self, enabled: bool) -> io::Result<()> {
|
pub fn set_broadcast(&self, enabled: bool) -> io::Result<()> {
|
||||||
crate::sys::linux::net::set_broadcast(self.raw_fd(), enabled)
|
crate::sys::current::net::set_broadcast(self.raw_fd(), enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the socket's IP time-to-live value.
|
/// Reads the socket's IP time-to-live value.
|
||||||
pub fn ttl(&self) -> io::Result<u32> {
|
pub fn ttl(&self) -> io::Result<u32> {
|
||||||
crate::sys::linux::net::ttl(self.raw_fd())
|
crate::sys::current::net::ttl(self.raw_fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the socket's IP time-to-live value.
|
/// Sets the socket's IP time-to-live value.
|
||||||
pub fn set_ttl(&self, ttl: u32) -> io::Result<()> {
|
pub fn set_ttl(&self, ttl: u32) -> io::Result<()> {
|
||||||
crate::sys::linux::net::set_ttl(self.raw_fd(), ttl)
|
crate::sys::current::net::set_ttl(self.raw_fd(), ttl)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the read timeout used by async receive operations on this handle.
|
/// Returns the read timeout used by async receive operations on this handle.
|
||||||
@@ -628,13 +631,13 @@ impl HyperRead for TcpStream {
|
|||||||
|
|
||||||
if this.pending_read.is_none() {
|
if this.pending_read.is_none() {
|
||||||
this.pending_read = Some(match this.read_timeout_value() {
|
this.pending_read = Some(match this.read_timeout_value() {
|
||||||
Some(timeout) => Box::pin(crate::sys::linux::net::recv_timeout(
|
Some(timeout) => Box::pin(crate::sys::current::net::recv_timeout(
|
||||||
this.raw_fd(),
|
this.raw_fd(),
|
||||||
buf.remaining(),
|
buf.remaining(),
|
||||||
0,
|
0,
|
||||||
timeout,
|
timeout,
|
||||||
)),
|
)),
|
||||||
None => crate::sys::linux::net::recv_future(this.raw_fd(), buf.remaining()),
|
None => crate::sys::current::net::recv_future(this.raw_fd(), buf.remaining()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,13 +675,13 @@ impl HyperWrite for TcpStream {
|
|||||||
|
|
||||||
if this.pending_write.is_none() {
|
if this.pending_write.is_none() {
|
||||||
this.pending_write = Some(match this.write_timeout_value() {
|
this.pending_write = Some(match this.write_timeout_value() {
|
||||||
Some(timeout) => Box::pin(crate::sys::linux::net::send_timeout(
|
Some(timeout) => Box::pin(crate::sys::current::net::send_timeout(
|
||||||
this.raw_fd(),
|
this.raw_fd(),
|
||||||
buf.to_vec(),
|
buf.to_vec(),
|
||||||
0,
|
0,
|
||||||
timeout,
|
timeout,
|
||||||
)),
|
)),
|
||||||
None => crate::sys::linux::net::send_future(this.raw_fd(), buf.to_vec()),
|
None => crate::sys::current::net::send_future(this.raw_fd(), buf.to_vec()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,7 +711,7 @@ impl HyperWrite for TcpStream {
|
|||||||
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
|
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
|
||||||
let this = self.get_mut();
|
let this = self.get_mut();
|
||||||
if this.pending_shutdown.is_none() {
|
if this.pending_shutdown.is_none() {
|
||||||
this.pending_shutdown = Some(crate::sys::linux::net::shutdown_future(
|
this.pending_shutdown = Some(crate::sys::current::net::shutdown_future(
|
||||||
this.raw_fd(),
|
this.raw_fd(),
|
||||||
Shutdown::Write,
|
Shutdown::Write,
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::task::{Context, Poll, Waker};
|
use std::task::{Context, Poll, Waker};
|
||||||
|
|
||||||
use crate::platform::linux_x86_64::runtime::{ThreadHandle, current_thread_handle};
|
use crate::platform::current::runtime::{ThreadHandle, current_thread_handle};
|
||||||
|
|
||||||
type CancelCallback = Box<dyn FnOnce() + Send + 'static>;
|
type CancelCallback = Box<dyn FnOnce() + Send + 'static>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! RUIN Runtime Driver for Linux x86_64.
|
//! RUIN Runtime Driver for Linux x86_64.
|
||||||
//!
|
//!
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
|||||||
@@ -13,16 +13,12 @@ use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use super::driver::{Driver, ThreadNotifier, create_driver, monotonic_now};
|
use super::driver::{Driver, ThreadNotifier, create_driver, monotonic_now};
|
||||||
|
use crate::platform::runtime_shared::{
|
||||||
|
IntervalCallback, LocalBoxFuture, LocalTask, LocalTaskQueue, MICROTASK_STARVATION_THRESHOLD,
|
||||||
|
MacroTaskQueue, SendTask,
|
||||||
|
};
|
||||||
use crate::trace_targets;
|
use crate::trace_targets;
|
||||||
|
|
||||||
type LocalTask = Box<dyn FnOnce() + 'static>;
|
|
||||||
type SendTask = Box<dyn FnOnce() + Send + 'static>;
|
|
||||||
type LocalBoxFuture = Pin<Box<dyn Future<Output = ()> + 'static>>;
|
|
||||||
|
|
||||||
/// If the microtask queue runs more than this many tasks in a single turn
|
|
||||||
/// without yielding to the macrotask queue, a warning is emitted.
|
|
||||||
const MICROTASK_STARVATION_THRESHOLD: u64 = 1000;
|
|
||||||
|
|
||||||
struct MacroTask {
|
struct MacroTask {
|
||||||
task: LocalTask,
|
task: LocalTask,
|
||||||
/// Wall time at which this task entered the local queue. Populated only
|
/// Wall time at which this task entered the local queue. Populated only
|
||||||
@@ -493,6 +489,99 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drains ready work on the current runtime thread without blocking for future work.
|
||||||
|
///
|
||||||
|
/// Unlike [`run`], this returns as soon as there are no immediately runnable
|
||||||
|
/// microtasks or macrotasks left. It is intended for host integrations that
|
||||||
|
/// need to re-enter the scheduler while an outer platform loop remains active.
|
||||||
|
pub fn run_until_stalled() {
|
||||||
|
let _ = current_thread();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
drain_all();
|
||||||
|
|
||||||
|
let mut microtasks_run: u64 = 0;
|
||||||
|
while let Some(task) = pop_microtask() {
|
||||||
|
task();
|
||||||
|
microtasks_run += 1;
|
||||||
|
drain_all();
|
||||||
|
}
|
||||||
|
if microtasks_run >= MICROTASK_STARVATION_THRESHOLD {
|
||||||
|
tracing::warn!(
|
||||||
|
target: trace_targets::SCHEDULER,
|
||||||
|
event = "microtask_starvation",
|
||||||
|
count = microtasks_run,
|
||||||
|
"microtask queue ran {microtasks_run} tasks in a single turn; macrotask handlers may be starved",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(task) = pop_macrotask() {
|
||||||
|
task();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
drain_all();
|
||||||
|
|
||||||
|
if has_ready_work() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_thread()
|
||||||
|
.shared
|
||||||
|
.closing
|
||||||
|
.store(false, Ordering::Release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drains already-queued work on the current runtime thread without polling the
|
||||||
|
/// driver for timers or I/O readiness.
|
||||||
|
///
|
||||||
|
/// This is intended for host integrations that need to flush application work
|
||||||
|
/// from inside a host callback without re-entering timer callbacks.
|
||||||
|
pub fn run_ready_tasks() {
|
||||||
|
let _ = current_thread();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
drain_remote_tasks();
|
||||||
|
drain_completed_workers();
|
||||||
|
|
||||||
|
let mut microtasks_run: u64 = 0;
|
||||||
|
while let Some(task) = pop_microtask() {
|
||||||
|
task();
|
||||||
|
microtasks_run += 1;
|
||||||
|
drain_remote_tasks();
|
||||||
|
drain_completed_workers();
|
||||||
|
}
|
||||||
|
if microtasks_run >= MICROTASK_STARVATION_THRESHOLD {
|
||||||
|
tracing::warn!(
|
||||||
|
target: trace_targets::SCHEDULER,
|
||||||
|
event = "microtask_starvation",
|
||||||
|
count = microtasks_run,
|
||||||
|
"microtask queue ran {microtasks_run} tasks in a single turn; macrotask handlers may be starved",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(task) = pop_macrotask() {
|
||||||
|
task();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
drain_remote_tasks();
|
||||||
|
drain_completed_workers();
|
||||||
|
|
||||||
|
if has_ready_work() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_thread()
|
||||||
|
.shared
|
||||||
|
.closing
|
||||||
|
.store(false, Ordering::Release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn drain_all() {
|
fn drain_all() {
|
||||||
drain_driver_events();
|
drain_driver_events();
|
||||||
drain_remote_tasks();
|
drain_remote_tasks();
|
||||||
@@ -592,8 +681,8 @@ struct ThreadState {
|
|||||||
driver: Driver,
|
driver: Driver,
|
||||||
shared: Arc<ThreadShared>,
|
shared: Arc<ThreadShared>,
|
||||||
worker_completion: Option<Arc<WorkerCompletion>>,
|
worker_completion: Option<Arc<WorkerCompletion>>,
|
||||||
local_microtasks: RefCell<VecDeque<LocalTask>>,
|
local_microtasks: RefCell<LocalTaskQueue>,
|
||||||
local_macrotasks: RefCell<VecDeque<MacroTask>>,
|
local_macrotasks: RefCell<MacroTaskQueue<MacroTask>>,
|
||||||
timers: RefCell<TimerHeap>,
|
timers: RefCell<TimerHeap>,
|
||||||
/// Zero-delay intervals bypasses the timer heap entirely. Each entry
|
/// Zero-delay intervals bypasses the timer heap entirely. Each entry
|
||||||
/// re-enqueues itself as a macrotask on every turn.
|
/// re-enqueues itself as a macrotask on every turn.
|
||||||
|
|||||||
504
lib/runtime/src/platform/macos_aarch64/driver.rs
Normal file
504
lib/runtime/src/platform/macos_aarch64/driver.rs
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
//! Public runtime driver primitives for macOS.
|
||||||
|
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io;
|
||||||
|
use std::os::fd::RawFd;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::op::completion::CompletionHandle;
|
||||||
|
|
||||||
|
type FdCompletion = CompletionHandle<io::Result<()>>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||||
|
pub(crate) struct FdReadinessToken(u64);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||||
|
pub(crate) enum FdInterest {
|
||||||
|
Readable,
|
||||||
|
Writable,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||||
|
struct FdKey {
|
||||||
|
fd: RawFd,
|
||||||
|
interest: FdInterest,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FdWaiter {
|
||||||
|
token: FdReadinessToken,
|
||||||
|
completion: FdCompletion,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct NotifierInner {
|
||||||
|
write_fd: RawFd,
|
||||||
|
closed: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NotifierInner {
|
||||||
|
fn notify(&self) -> io::Result<()> {
|
||||||
|
if self.closed.load(Ordering::Acquire) {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::BrokenPipe,
|
||||||
|
"target runtime driver is closed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let byte = 1u8;
|
||||||
|
let written = unsafe {
|
||||||
|
libc::write(
|
||||||
|
self.write_fd,
|
||||||
|
&byte as *const u8 as *const libc::c_void,
|
||||||
|
std::mem::size_of::<u8>(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if written < 0 {
|
||||||
|
let error = io::Error::last_os_error();
|
||||||
|
if error.kind() == io::ErrorKind::WouldBlock {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
/// Cross-thread notifier for a runtime thread's driver.
|
||||||
|
pub struct ThreadNotifier {
|
||||||
|
inner: NotifierInner,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThreadNotifier {
|
||||||
|
/// Sends a wake notification to the target runtime thread.
|
||||||
|
pub fn notify(&self) -> io::Result<()> {
|
||||||
|
self.inner.notify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
|
||||||
|
/// Readiness information returned by [`Driver::poll`].
|
||||||
|
pub struct ReadyEvents {
|
||||||
|
/// One or more timer expirations are pending.
|
||||||
|
pub timer: bool,
|
||||||
|
/// One or more cross-thread wake notifications are pending.
|
||||||
|
pub wake: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Low-level macOS runtime driver backed by `kqueue` and a wake pipe.
|
||||||
|
pub struct Driver {
|
||||||
|
kqueue_fd: RawFd,
|
||||||
|
wake_read_fd: RawFd,
|
||||||
|
wake_write_fd: RawFd,
|
||||||
|
closed: Arc<AtomicBool>,
|
||||||
|
timer_deadline: Cell<Option<Duration>>,
|
||||||
|
pending_wakes: Cell<u64>,
|
||||||
|
pending_timers: Cell<u64>,
|
||||||
|
next_fd_token: Cell<u64>,
|
||||||
|
fd_waiters: RefCell<HashMap<FdKey, FdWaiter>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new driver and its paired [`ThreadNotifier`].
|
||||||
|
pub fn create() -> io::Result<(Driver, ThreadNotifier)> {
|
||||||
|
create_driver()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new driver and its paired [`ThreadNotifier`].
|
||||||
|
pub fn create_driver() -> io::Result<(Driver, ThreadNotifier)> {
|
||||||
|
let kqueue_fd = cvt(unsafe { libc::kqueue() })?;
|
||||||
|
|
||||||
|
let mut pipe_fds = [0; 2];
|
||||||
|
cvt(unsafe { libc::pipe(pipe_fds.as_mut_ptr()) })?;
|
||||||
|
let wake_read_fd = pipe_fds[0];
|
||||||
|
let wake_write_fd = pipe_fds[1];
|
||||||
|
|
||||||
|
set_nonblocking(wake_read_fd)?;
|
||||||
|
set_nonblocking(wake_write_fd)?;
|
||||||
|
|
||||||
|
let event = libc::kevent {
|
||||||
|
ident: wake_read_fd as usize,
|
||||||
|
filter: libc::EVFILT_READ,
|
||||||
|
flags: libc::EV_ADD | libc::EV_ENABLE,
|
||||||
|
fflags: 0,
|
||||||
|
data: 0,
|
||||||
|
udata: std::ptr::null_mut(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let submitted = unsafe {
|
||||||
|
libc::kevent(
|
||||||
|
kqueue_fd,
|
||||||
|
&event,
|
||||||
|
1,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
std::ptr::null(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if submitted < 0 {
|
||||||
|
let error = io::Error::last_os_error();
|
||||||
|
unsafe {
|
||||||
|
libc::close(wake_read_fd);
|
||||||
|
libc::close(wake_write_fd);
|
||||||
|
libc::close(kqueue_fd);
|
||||||
|
}
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let closed = Arc::new(AtomicBool::new(false));
|
||||||
|
let driver = Driver {
|
||||||
|
kqueue_fd,
|
||||||
|
wake_read_fd,
|
||||||
|
wake_write_fd,
|
||||||
|
closed: Arc::clone(&closed),
|
||||||
|
timer_deadline: Cell::new(None),
|
||||||
|
pending_wakes: Cell::new(0),
|
||||||
|
pending_timers: Cell::new(0),
|
||||||
|
next_fd_token: Cell::new(1),
|
||||||
|
fd_waiters: RefCell::new(HashMap::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let notifier = ThreadNotifier {
|
||||||
|
inner: NotifierInner {
|
||||||
|
write_fd: wake_write_fd,
|
||||||
|
closed,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((driver, notifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Driver {
|
||||||
|
pub(crate) fn bind_current_thread(&self) {}
|
||||||
|
|
||||||
|
pub(crate) fn unbind_current_thread(&self) {}
|
||||||
|
|
||||||
|
/// Polls the driver without blocking.
|
||||||
|
pub fn poll(&self) -> io::Result<Option<ReadyEvents>> {
|
||||||
|
let mut pending = ReadyEvents::default();
|
||||||
|
if self.pending_wakes.get() > 0 {
|
||||||
|
pending.wake = true;
|
||||||
|
}
|
||||||
|
if self.pending_timers.get() > 0 {
|
||||||
|
pending.timer = true;
|
||||||
|
}
|
||||||
|
if pending.wake || pending.timer {
|
||||||
|
return Ok(Some(pending));
|
||||||
|
}
|
||||||
|
self.process(Some(Duration::ZERO))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blocks until at least one event is available.
|
||||||
|
pub fn wait(&self) -> io::Result<()> {
|
||||||
|
let now = monotonic_now()?;
|
||||||
|
let timeout = self
|
||||||
|
.timer_deadline
|
||||||
|
.get()
|
||||||
|
.map(|deadline| deadline.saturating_sub(now));
|
||||||
|
let _ = self.process(timeout)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the currently armed timer deadline.
|
||||||
|
pub fn rearm_timer(&self, deadline: Option<Duration>) -> io::Result<()> {
|
||||||
|
self.timer_deadline.set(deadline);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drains the accumulated wake notification count.
|
||||||
|
pub fn drain_wake(&self) -> io::Result<u64> {
|
||||||
|
let wakes = self.pending_wakes.replace(0);
|
||||||
|
if wakes == 0 {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::WouldBlock,
|
||||||
|
"no wake events are pending",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(wakes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drains the accumulated timer-expiration count.
|
||||||
|
pub fn drain_timer(&self) -> io::Result<u64> {
|
||||||
|
let timers = self.pending_timers.replace(0);
|
||||||
|
if timers == 0 {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::WouldBlock,
|
||||||
|
"no timer events are pending",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(timers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn register_fd_readiness(
|
||||||
|
&self,
|
||||||
|
fd: RawFd,
|
||||||
|
interest: FdInterest,
|
||||||
|
completion: FdCompletion,
|
||||||
|
) -> io::Result<FdReadinessToken> {
|
||||||
|
let key = FdKey { fd, interest };
|
||||||
|
let removed_stale_waiter = {
|
||||||
|
let mut waiters = self.fd_waiters.borrow_mut();
|
||||||
|
match waiters.get(&key) {
|
||||||
|
Some(waiter) if !waiter.completion.is_interested() => {
|
||||||
|
waiters.remove(&key);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::AlreadyExists,
|
||||||
|
"fd readiness already has a waiter for this interest",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if removed_stale_waiter {
|
||||||
|
let _ = self.update_fd_interest(key, libc::EV_DELETE);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = self.allocate_fd_token();
|
||||||
|
self.update_fd_interest(key, libc::EV_ADD | libc::EV_ENABLE | libc::EV_ONESHOT)?;
|
||||||
|
self.fd_waiters
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(key, FdWaiter { token, completion });
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cancel_fd_readiness(&self, token: FdReadinessToken) {
|
||||||
|
let mut empty_key = None;
|
||||||
|
{
|
||||||
|
let mut waiters = self.fd_waiters.borrow_mut();
|
||||||
|
for (key, entry) in waiters.iter() {
|
||||||
|
if entry.token == token {
|
||||||
|
empty_key = Some(*key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(key) = empty_key {
|
||||||
|
waiters.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(key) = empty_key {
|
||||||
|
let _ = self.update_fd_interest(key, libc::EV_DELETE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, timeout: Option<Duration>) -> io::Result<Option<ReadyEvents>> {
|
||||||
|
let mut ready = ReadyEvents::default();
|
||||||
|
|
||||||
|
let mut events = [unsafe { std::mem::zeroed::<libc::kevent>() }; 16];
|
||||||
|
let timeout_spec = timeout_to_timespec(timeout);
|
||||||
|
let timeout_ptr = timeout_spec
|
||||||
|
.as_ref()
|
||||||
|
.map_or(std::ptr::null(), |value| value as *const libc::timespec);
|
||||||
|
|
||||||
|
let result = unsafe {
|
||||||
|
libc::kevent(
|
||||||
|
self.kqueue_fd,
|
||||||
|
std::ptr::null(),
|
||||||
|
0,
|
||||||
|
events.as_mut_ptr(),
|
||||||
|
events.len() as i32,
|
||||||
|
timeout_ptr,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if result < 0 {
|
||||||
|
let error = io::Error::last_os_error();
|
||||||
|
if error.kind() != io::ErrorKind::Interrupted {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut saw_any = false;
|
||||||
|
let count = result.max(0) as usize;
|
||||||
|
if count > 0 {
|
||||||
|
saw_any = true;
|
||||||
|
for event in events.iter().take(count) {
|
||||||
|
if event.ident as RawFd == self.wake_read_fd {
|
||||||
|
ready.wake = true;
|
||||||
|
let wakes = drain_wake_pipe(self.wake_read_fd)?;
|
||||||
|
self.pending_wakes
|
||||||
|
.set(self.pending_wakes.get().saturating_add(wakes));
|
||||||
|
} else if let Some(interest) = interest_from_filter(event.filter) {
|
||||||
|
self.complete_fd_waiters(event.ident as RawFd, interest, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(deadline) = self.timer_deadline.get()
|
||||||
|
&& monotonic_now()? >= deadline
|
||||||
|
{
|
||||||
|
ready.timer = true;
|
||||||
|
saw_any = true;
|
||||||
|
self.timer_deadline.set(None);
|
||||||
|
self.pending_timers
|
||||||
|
.set(self.pending_timers.get().saturating_add(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if saw_any { Ok(Some(ready)) } else { Ok(None) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allocate_fd_token(&self) -> FdReadinessToken {
|
||||||
|
let token = self.next_fd_token.get();
|
||||||
|
self.next_fd_token.set(
|
||||||
|
token
|
||||||
|
.checked_add(1)
|
||||||
|
.expect("fd readiness token space exhausted"),
|
||||||
|
);
|
||||||
|
FdReadinessToken(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_fd_interest(&self, key: FdKey, flags: u16) -> io::Result<()> {
|
||||||
|
let event = libc::kevent {
|
||||||
|
ident: key.fd as usize,
|
||||||
|
filter: filter_for_interest(key.interest),
|
||||||
|
flags,
|
||||||
|
fflags: 0,
|
||||||
|
data: 0,
|
||||||
|
udata: std::ptr::null_mut(),
|
||||||
|
};
|
||||||
|
let submitted = unsafe {
|
||||||
|
libc::kevent(
|
||||||
|
self.kqueue_fd,
|
||||||
|
&event,
|
||||||
|
1,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
std::ptr::null(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if submitted < 0 {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_fd_waiters(&self, fd: RawFd, interest: FdInterest, event: &libc::kevent) {
|
||||||
|
let key = FdKey { fd, interest };
|
||||||
|
let waiter = self.fd_waiters.borrow_mut().remove(&key);
|
||||||
|
let Some(waiter) = waiter else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = fd_event_result(event, interest);
|
||||||
|
waiter.completion.complete(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Driver {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.closed.store(true, Ordering::Release);
|
||||||
|
unsafe {
|
||||||
|
libc::close(self.wake_read_fd);
|
||||||
|
libc::close(self.wake_write_fd);
|
||||||
|
libc::close(self.kqueue_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current monotonic clock reading.
|
||||||
|
pub fn monotonic_now() -> io::Result<Duration> {
|
||||||
|
let mut now = std::mem::MaybeUninit::<libc::timespec>::uninit();
|
||||||
|
let result = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, now.as_mut_ptr()) };
|
||||||
|
if result < 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = unsafe { now.assume_init() };
|
||||||
|
Ok(Duration::new(now.tv_sec as u64, now.tv_nsec as u32))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cvt(value: libc::c_int) -> io::Result<libc::c_int> {
|
||||||
|
if value < 0 {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_for_interest(interest: FdInterest) -> i16 {
|
||||||
|
match interest {
|
||||||
|
FdInterest::Readable => libc::EVFILT_READ,
|
||||||
|
FdInterest::Writable => libc::EVFILT_WRITE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interest_from_filter(filter: i16) -> Option<FdInterest> {
|
||||||
|
if filter == libc::EVFILT_READ {
|
||||||
|
Some(FdInterest::Readable)
|
||||||
|
} else if filter == libc::EVFILT_WRITE {
|
||||||
|
Some(FdInterest::Writable)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fd_event_result(event: &libc::kevent, interest: FdInterest) -> io::Result<()> {
|
||||||
|
if event.flags & libc::EV_ERROR != 0 && event.data != 0 {
|
||||||
|
Err(io::Error::from_raw_os_error(event.data as i32))
|
||||||
|
} else if event.flags & libc::EV_EOF != 0 && interest == FdInterest::Writable {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::BrokenPipe,
|
||||||
|
"fd write side reached EOF",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_nonblocking(fd: RawFd) -> io::Result<()> {
|
||||||
|
let flags = cvt(unsafe { libc::fcntl(fd, libc::F_GETFL) })?;
|
||||||
|
cvt(unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) })?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timeout_to_timespec(timeout: Option<Duration>) -> Option<libc::timespec> {
|
||||||
|
timeout.map(|value| libc::timespec {
|
||||||
|
tv_sec: value.as_secs() as libc::time_t,
|
||||||
|
tv_nsec: value.subsec_nanos() as libc::c_long,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drain_wake_pipe(fd: RawFd) -> io::Result<u64> {
|
||||||
|
let mut wakes = 0u64;
|
||||||
|
let mut buf = [0u8; 256];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let read = unsafe {
|
||||||
|
libc::read(
|
||||||
|
fd,
|
||||||
|
buf.as_mut_ptr() as *mut libc::c_void,
|
||||||
|
buf.len() as libc::size_t,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if read > 0 {
|
||||||
|
wakes = wakes.saturating_add(read as u64);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let error = io::Error::last_os_error();
|
||||||
|
if error.kind() == io::ErrorKind::WouldBlock {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if error.kind() == io::ErrorKind::Interrupted {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(wakes.max(1))
|
||||||
|
}
|
||||||
2
lib/runtime/src/platform/macos_aarch64/mod.rs
Normal file
2
lib/runtime/src/platform/macos_aarch64/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod driver;
|
||||||
|
pub mod runtime;
|
||||||
1514
lib/runtime/src/platform/macos_aarch64/runtime.rs
Normal file
1514
lib/runtime/src/platform/macos_aarch64/runtime.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,11 @@
|
|||||||
|
pub(crate) mod runtime_shared;
|
||||||
|
|
||||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
pub mod linux_x86_64;
|
pub mod linux_x86_64;
|
||||||
|
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||||
|
pub mod macos_aarch64;
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
|
pub use linux_x86_64 as current;
|
||||||
|
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||||
|
pub use macos_aarch64 as current;
|
||||||
|
|||||||
17
lib/runtime/src/platform/runtime_shared.rs
Normal file
17
lib/runtime/src/platform/runtime_shared.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
pub(crate) type LocalTask = Box<dyn FnOnce() + 'static>;
|
||||||
|
pub(crate) type SendTask = Box<dyn FnOnce() + Send + 'static>;
|
||||||
|
pub(crate) type LocalBoxFuture = Pin<Box<dyn Future<Output = ()> + 'static>>;
|
||||||
|
pub(crate) type IntervalCallback = Rc<RefCell<Box<dyn FnMut()>>>;
|
||||||
|
|
||||||
|
/// If the microtask queue runs more than this many tasks in a single turn
|
||||||
|
/// without yielding to the macrotask queue, a warning is emitted.
|
||||||
|
pub(crate) const MICROTASK_STARVATION_THRESHOLD: u64 = 1000;
|
||||||
|
|
||||||
|
pub(crate) type LocalTaskQueue = VecDeque<LocalTask>;
|
||||||
|
pub(crate) type MacroTaskQueue<T> = VecDeque<T>;
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
//! Async standard-input helpers.
|
//! Async standard-input helpers.
|
||||||
|
|
||||||
use std::cell::Cell;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use crate::op::completion::completion_for_current_thread;
|
use crate::op::completion::completion_for_current_thread;
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
use crate::platform::linux_x86_64::runtime::with_current_driver;
|
use crate::platform::linux_x86_64::runtime::with_current_driver;
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
use crate::platform::linux_x86_64::uring::{IORING_OP_READ, IoUringCqe, IoUringSqe};
|
use crate::platform::linux_x86_64::uring::{IORING_OP_READ, IoUringCqe, IoUringSqe};
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static STDIN_URING_SUPPORTED: Cell<Option<bool>> = const { Cell::new(None) };
|
static STDIN_URING_SUPPORTED: Cell<Option<bool>> = const { Cell::new(None) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
const FILE_CURSOR: u64 = u64::MAX;
|
const FILE_CURSOR: u64 = u64::MAX;
|
||||||
const READ_CHUNK_BYTES: usize = 1024;
|
const READ_CHUNK_BYTES: usize = 1024;
|
||||||
|
|
||||||
@@ -60,17 +65,20 @@ impl Stdin {
|
|||||||
|
|
||||||
async fn read_chunk(&self) -> io::Result<Vec<u8>> {
|
async fn read_chunk(&self) -> io::Result<Vec<u8>> {
|
||||||
let fd = self.fd.as_raw_fd();
|
let fd = self.fd.as_raw_fd();
|
||||||
let support = STDIN_URING_SUPPORTED.with(Cell::get);
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
if support != Some(false) {
|
{
|
||||||
match submit_uring_read(fd, READ_CHUNK_BYTES).await {
|
let support = STDIN_URING_SUPPORTED.with(Cell::get);
|
||||||
Ok(bytes) => {
|
if support != Some(false) {
|
||||||
STDIN_URING_SUPPORTED.with(|state| state.set(Some(true)));
|
match submit_uring_read(fd, READ_CHUNK_BYTES).await {
|
||||||
return Ok(bytes);
|
Ok(bytes) => {
|
||||||
|
STDIN_URING_SUPPORTED.with(|state| state.set(Some(true)));
|
||||||
|
return Ok(bytes);
|
||||||
|
}
|
||||||
|
Err(error) if should_fallback_to_offload(&error) => {
|
||||||
|
STDIN_URING_SUPPORTED.with(|state| state.set(Some(false)));
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error),
|
||||||
}
|
}
|
||||||
Err(error) if should_fallback_to_offload(&error) => {
|
|
||||||
STDIN_URING_SUPPORTED.with(|state| state.set(Some(false)));
|
|
||||||
}
|
|
||||||
Err(error) => return Err(error),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +107,7 @@ async fn offload<T: Send + 'static>(
|
|||||||
future.await
|
future.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
async fn submit_uring_read(fd: RawFd, len: usize) -> io::Result<Vec<u8>> {
|
async fn submit_uring_read(fd: RawFd, len: usize) -> io::Result<Vec<u8>> {
|
||||||
let mut buffer = vec![0; len];
|
let mut buffer = vec![0; len];
|
||||||
let ptr = buffer.as_mut_ptr();
|
let ptr = buffer.as_mut_ptr();
|
||||||
@@ -120,6 +129,7 @@ async fn submit_uring_read(fd: RawFd, len: usize) -> io::Result<Vec<u8>> {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
async fn submit_uring<T: Send + 'static, M>(
|
async fn submit_uring<T: Send + 'static, M>(
|
||||||
fill: impl FnOnce(&mut IoUringSqe),
|
fill: impl FnOnce(&mut IoUringSqe),
|
||||||
map: M,
|
map: M,
|
||||||
@@ -144,13 +154,8 @@ where
|
|||||||
|
|
||||||
fn blocking_read(fd: RawFd, buffer: &mut [u8]) -> io::Result<usize> {
|
fn blocking_read(fd: RawFd, buffer: &mut [u8]) -> io::Result<usize> {
|
||||||
loop {
|
loop {
|
||||||
let read = unsafe {
|
let read =
|
||||||
libc::read(
|
unsafe { libc::read(fd, buffer.as_mut_ptr().cast::<libc::c_void>(), buffer.len()) };
|
||||||
fd,
|
|
||||||
buffer.as_mut_ptr().cast::<libc::c_void>(),
|
|
||||||
buffer.len(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if read >= 0 {
|
if read >= 0 {
|
||||||
return Ok(read as usize);
|
return Ok(read as usize);
|
||||||
}
|
}
|
||||||
@@ -167,6 +172,7 @@ fn decode_line(bytes: Vec<u8>) -> io::Result<String> {
|
|||||||
String::from_utf8(bytes).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
|
String::from_utf8(bytes).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
fn cqe_to_result(cqe: IoUringCqe) -> io::Result<i32> {
|
fn cqe_to_result(cqe: IoUringCqe) -> io::Result<i32> {
|
||||||
if cqe.res < 0 {
|
if cqe.res < 0 {
|
||||||
Err(io::Error::from_raw_os_error(-cqe.res))
|
Err(io::Error::from_raw_os_error(-cqe.res))
|
||||||
@@ -175,6 +181,7 @@ fn cqe_to_result(cqe: IoUringCqe) -> io::Result<i32> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
fn should_fallback_to_offload(error: &io::Error) -> bool {
|
fn should_fallback_to_offload(error: &io::Error) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
error.raw_os_error(),
|
error.raw_os_error(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Linux channel wake helpers.
|
//! Linux channel wake helpers.
|
||||||
|
|
||||||
use crate::op::completion::{CompletionFuture, CompletionHandle, completion};
|
use crate::op::completion::{CompletionFuture, CompletionHandle, completion};
|
||||||
use crate::platform::linux_x86_64::runtime::try_current_thread_handle;
|
use crate::platform::current::runtime::try_current_thread_handle;
|
||||||
|
|
||||||
pub(crate) fn runtime_waiter<T: Send + 'static>() -> (CompletionFuture<T>, CompletionHandle<T>) {
|
pub(crate) fn runtime_waiter<T: Send + 'static>() -> (CompletionFuture<T>, CompletionHandle<T>) {
|
||||||
let owner = try_current_thread_handle()
|
let owner = try_current_thread_handle()
|
||||||
|
|||||||
44
lib/runtime/src/sys/linux/fd.rs
Normal file
44
lib/runtime/src/sys/linux/fd.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//! Linux fd readiness backend.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::os::fd::RawFd;
|
||||||
|
|
||||||
|
use crate::op::completion::completion_for_current_thread;
|
||||||
|
use crate::platform::current::runtime::with_current_driver;
|
||||||
|
use crate::platform::linux_x86_64::uring::{IORING_OP_POLL_ADD, IoUringCqe};
|
||||||
|
|
||||||
|
/// Waits until `fd` becomes readable or reports an error/hangup condition.
|
||||||
|
pub async fn wait_readable(fd: RawFd) -> io::Result<()> {
|
||||||
|
submit_poll(fd, libc::POLLIN | libc::POLLERR | libc::POLLHUP).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn submit_poll(fd: RawFd, mask: i16) -> io::Result<()> {
|
||||||
|
let (future, handle) = completion_for_current_thread::<io::Result<()>>();
|
||||||
|
let callback_handle = handle.clone();
|
||||||
|
let token = with_current_driver(|driver| {
|
||||||
|
driver.submit_operation(
|
||||||
|
move |sqe| {
|
||||||
|
sqe.opcode = IORING_OP_POLL_ADD;
|
||||||
|
sqe.fd = fd;
|
||||||
|
sqe.len = 0;
|
||||||
|
sqe.op_flags = mask as u32;
|
||||||
|
},
|
||||||
|
move |cqe| {
|
||||||
|
callback_handle.complete(cqe_to_result(cqe));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
handle.set_cancel(move || {
|
||||||
|
let _ = with_current_driver(|driver| driver.cancel_operation(token));
|
||||||
|
});
|
||||||
|
|
||||||
|
future.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cqe_to_result(cqe: IoUringCqe) -> io::Result<()> {
|
||||||
|
if cqe.res < 0 {
|
||||||
|
return Err(io::Error::from_raw_os_error(-cqe.res));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Linux backend modules.
|
//! Linux backend modules.
|
||||||
|
|
||||||
pub mod channel;
|
pub mod channel;
|
||||||
|
pub mod fd;
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
pub mod net;
|
pub mod net;
|
||||||
|
|||||||
@@ -803,13 +803,9 @@ where
|
|||||||
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
||||||
let callback_handle = handle.clone();
|
let callback_handle = handle.clone();
|
||||||
let token = with_current_driver(|driver| {
|
let token = with_current_driver(|driver| {
|
||||||
driver.submit_operation_with_linked_timeout(
|
driver.submit_operation_with_linked_timeout(fill, timeout, move |cqe| {
|
||||||
fill,
|
callback_handle.complete(map(cqe));
|
||||||
timeout,
|
})
|
||||||
move |cqe| {
|
|
||||||
callback_handle.complete(map(cqe));
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
handle.set_cancel(move || {
|
handle.set_cancel(move || {
|
||||||
@@ -1079,7 +1075,6 @@ fn send_sync(fd: RawFd, data: Vec<u8>, flags: i32) -> io::Result<usize> {
|
|||||||
cvt_long(written).map(|written| written as usize)
|
cvt_long(written).map(|written| written as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn recv_sync(fd: RawFd, len: usize, flags: i32) -> io::Result<Vec<u8>> {
|
fn recv_sync(fd: RawFd, len: usize, flags: i32) -> io::Result<Vec<u8>> {
|
||||||
let mut buffer = vec![0; len];
|
let mut buffer = vec![0; len];
|
||||||
let read = unsafe {
|
let read = unsafe {
|
||||||
@@ -1127,7 +1122,6 @@ fn close_sync(fd: RawFd) -> io::Result<()> {
|
|||||||
cvt(unsafe { libc::close(fd) }).map(|_| ())
|
cvt(unsafe { libc::close(fd) }).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Wrapper making `Box<libc::iovec>` sendable across the async CQE boundary.
|
/// Wrapper making `Box<libc::iovec>` sendable across the async CQE boundary.
|
||||||
///
|
///
|
||||||
/// Safety: `iov_base` points into a `Vec<u8>` that is owned by the same
|
/// Safety: `iov_base` points into a `Vec<u8>` that is owned by the same
|
||||||
|
|||||||
10
lib/runtime/src/sys/macos/channel.rs
Normal file
10
lib/runtime/src/sys/macos/channel.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//! macOS channel wake helpers.
|
||||||
|
|
||||||
|
use crate::op::completion::{CompletionFuture, CompletionHandle, completion};
|
||||||
|
use crate::platform::current::runtime::try_current_thread_handle;
|
||||||
|
|
||||||
|
pub(crate) fn runtime_waiter<T: Send + 'static>() -> (CompletionFuture<T>, CompletionHandle<T>) {
|
||||||
|
let owner = try_current_thread_handle()
|
||||||
|
.expect("async channel operations must be polled on a runtime thread");
|
||||||
|
completion(owner)
|
||||||
|
}
|
||||||
49
lib/runtime/src/sys/macos/fd.rs
Normal file
49
lib/runtime/src/sys/macos/fd.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//! macOS fd readiness backend.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::os::fd::RawFd;
|
||||||
|
|
||||||
|
use crate::op::completion::completion_for_current_thread;
|
||||||
|
use crate::platform::current::driver::FdInterest;
|
||||||
|
use crate::platform::current::runtime::{
|
||||||
|
cancel_fd_readiness, current_thread_handle, with_current_driver,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Waits until `fd` becomes readable or reports an error/hangup condition.
|
||||||
|
pub async fn wait_readable(fd: RawFd) -> io::Result<()> {
|
||||||
|
wait_fd_readiness(fd, FdInterest::Readable).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits until `fd` becomes writable or reports an error/hangup condition.
|
||||||
|
pub async fn wait_writable(fd: RawFd) -> io::Result<()> {
|
||||||
|
wait_fd_readiness(fd, FdInterest::Writable).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_fd_readiness(fd: RawFd, interest: FdInterest) -> io::Result<()> {
|
||||||
|
let (future, handle) = completion_for_current_thread::<io::Result<()>>();
|
||||||
|
let owner = current_thread_handle();
|
||||||
|
let token =
|
||||||
|
with_current_driver(|driver| driver.register_fd_readiness(fd, interest, handle.clone()));
|
||||||
|
match token {
|
||||||
|
Ok(token) => {
|
||||||
|
handle.set_cancel({
|
||||||
|
let handle = handle.clone();
|
||||||
|
move || {
|
||||||
|
let queued_handle = handle.clone();
|
||||||
|
let queued = owner.queue_task(move || {
|
||||||
|
cancel_fd_readiness(token);
|
||||||
|
queued_handle.finish(None);
|
||||||
|
});
|
||||||
|
if !queued {
|
||||||
|
handle.finish(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
handle.complete(Err(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
future.await
|
||||||
|
}
|
||||||
498
lib/runtime/src/sys/macos/fs.rs
Normal file
498
lib/runtime/src/sys/macos/fs.rs
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
//! macOS filesystem backend.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::future::poll_fn;
|
||||||
|
use std::io;
|
||||||
|
use std::os::fd::{FromRawFd, OwnedFd};
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex, mpsc};
|
||||||
|
use std::task::{Context, Poll, Waker};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use crate::op::completion::completion_for_current_thread;
|
||||||
|
use crate::op::fs::{FileType, FsOp, MetadataTarget, RawDirEntry, RawMetadata};
|
||||||
|
use crate::platform::current::runtime::{ThreadHandle, current_thread_handle};
|
||||||
|
|
||||||
|
type BlockingTask = Box<dyn FnOnce() + Send + 'static>;
|
||||||
|
|
||||||
|
static BLOCKING_POOL: OnceLock<io::Result<BlockingPool>> = OnceLock::new();
|
||||||
|
const BLOCKING_QUEUE_CAPACITY: usize = 1024;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum ExecutionPath {
|
||||||
|
Offload,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execution_path(_op: &FsOp) -> ExecutionPath {
|
||||||
|
ExecutionPath::Offload
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn open(op: FsOp) -> io::Result<OwnedFd> {
|
||||||
|
let FsOp::Open { path, options } = op else {
|
||||||
|
unreachable!("open backend called with non-open op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || {
|
||||||
|
let mut open = std::fs::OpenOptions::new();
|
||||||
|
open.read(options.read)
|
||||||
|
.write(options.write)
|
||||||
|
.append(options.append)
|
||||||
|
.truncate(options.truncate)
|
||||||
|
.create(options.create)
|
||||||
|
.create_new(options.create_new)
|
||||||
|
.mode(0o666);
|
||||||
|
let file = open.open(path)?;
|
||||||
|
Ok(unsafe { OwnedFd::from_raw_fd(std::os::fd::IntoRawFd::into_raw_fd(file)) })
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read(op: FsOp) -> io::Result<Vec<u8>> {
|
||||||
|
let FsOp::Read { fd, offset, len } = op else {
|
||||||
|
unreachable!("read backend called with non-read op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || {
|
||||||
|
let mut buffer = vec![0; len];
|
||||||
|
let read = match offset {
|
||||||
|
Some(offset) => unsafe {
|
||||||
|
libc::pread(
|
||||||
|
fd,
|
||||||
|
buffer.as_mut_ptr().cast::<libc::c_void>(),
|
||||||
|
len,
|
||||||
|
offset as libc::off_t,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
None => unsafe { libc::read(fd, buffer.as_mut_ptr().cast::<libc::c_void>(), len) },
|
||||||
|
};
|
||||||
|
if read < 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
buffer.truncate(read as usize);
|
||||||
|
Ok(buffer)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write(op: FsOp) -> io::Result<usize> {
|
||||||
|
let FsOp::Write { fd, offset, data } = op else {
|
||||||
|
unreachable!("write backend called with non-write op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || {
|
||||||
|
let written = match offset {
|
||||||
|
Some(offset) => unsafe {
|
||||||
|
libc::pwrite(
|
||||||
|
fd,
|
||||||
|
data.as_ptr().cast::<libc::c_void>(),
|
||||||
|
data.len(),
|
||||||
|
offset as libc::off_t,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
None => unsafe { libc::write(fd, data.as_ptr().cast::<libc::c_void>(), data.len()) },
|
||||||
|
};
|
||||||
|
if written < 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(written as usize)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn metadata(op: FsOp) -> io::Result<RawMetadata> {
|
||||||
|
let FsOp::Metadata {
|
||||||
|
target,
|
||||||
|
follow_symlinks,
|
||||||
|
} = op
|
||||||
|
else {
|
||||||
|
unreachable!("metadata backend called with non-metadata op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || {
|
||||||
|
let metadata = match target {
|
||||||
|
MetadataTarget::Path(path) => {
|
||||||
|
if follow_symlinks {
|
||||||
|
std::fs::metadata(path)
|
||||||
|
} else {
|
||||||
|
std::fs::symlink_metadata(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MetadataTarget::File(fd) => {
|
||||||
|
let mut stat = unsafe { std::mem::zeroed::<libc::stat>() };
|
||||||
|
let result = unsafe { libc::fstat(fd, &mut stat) };
|
||||||
|
if result < 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
return Ok(raw_metadata_from_stat(&stat));
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
Ok(raw_metadata_from_std(&metadata))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sync_all(op: FsOp) -> io::Result<()> {
|
||||||
|
let FsOp::SyncAll { fd } = op else {
|
||||||
|
unreachable!("sync_all backend called with non-sync_all op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || cvt(unsafe { libc::fsync(fd) }).map(|_| ())).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sync_data(op: FsOp) -> io::Result<()> {
|
||||||
|
let FsOp::SyncData { fd } = op else {
|
||||||
|
unreachable!("sync_data backend called with non-sync_data op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || cvt(unsafe { libc::fcntl(fd, libc::F_FULLFSYNC) }).map(|_| ())).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_len(op: FsOp) -> io::Result<()> {
|
||||||
|
let FsOp::SetLen { fd, len } = op else {
|
||||||
|
unreachable!("set_len backend called with non-set_len op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || cvt(unsafe { libc::ftruncate(fd, len as libc::off_t) }).map(|_| ())).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn try_clone(op: FsOp) -> io::Result<OwnedFd> {
|
||||||
|
let FsOp::Duplicate { fd } = op else {
|
||||||
|
unreachable!("try_clone backend called with non-duplicate op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || {
|
||||||
|
let duplicated = cvt(unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) })?;
|
||||||
|
Ok(unsafe { OwnedFd::from_raw_fd(duplicated) })
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_dir(op: FsOp) -> io::Result<()> {
|
||||||
|
let FsOp::CreateDir { path, mode } = op else {
|
||||||
|
unreachable!("create_dir backend called with non-create_dir op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || {
|
||||||
|
let c_path = path_to_c_string(path)?;
|
||||||
|
cvt(unsafe { libc::mkdir(c_path.as_ptr(), mode as libc::mode_t) }).map(|_| ())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_file(op: FsOp) -> io::Result<()> {
|
||||||
|
let FsOp::RemoveFile { path } = op else {
|
||||||
|
unreachable!("remove_file backend called with non-remove_file op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || std::fs::remove_file(path)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_dir(op: FsOp) -> io::Result<()> {
|
||||||
|
let FsOp::RemoveDir { path } = op else {
|
||||||
|
unreachable!("remove_dir backend called with non-remove_dir op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || std::fs::remove_dir(path)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn rename(op: FsOp) -> io::Result<()> {
|
||||||
|
let FsOp::Rename { from, to } = op else {
|
||||||
|
unreachable!("rename backend called with non-rename op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || std::fs::rename(from, to)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close(op: FsOp) -> io::Result<()> {
|
||||||
|
let FsOp::Close { fd } = op else {
|
||||||
|
unreachable!("close backend called with non-close op");
|
||||||
|
};
|
||||||
|
|
||||||
|
offload(move || cvt(unsafe { libc::close(fd) }).map(|_| ())).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_dir(op: FsOp) -> io::Result<ReadDirStream> {
|
||||||
|
let FsOp::ReadDir { path } = op else {
|
||||||
|
unreachable!("read_dir backend called with non-read_dir op");
|
||||||
|
};
|
||||||
|
|
||||||
|
ReadDirStream::new(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ReadDirStream {
|
||||||
|
state: Arc<ReadDirState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadDirStream {
|
||||||
|
fn new(path: PathBuf) -> io::Result<Self> {
|
||||||
|
let state = Arc::new(ReadDirState::new(current_thread_handle()));
|
||||||
|
let producer = Arc::clone(&state);
|
||||||
|
|
||||||
|
if let Err(error) = spawn_blocking(Box::new(move || produce_dir_entries(path, producer))) {
|
||||||
|
state.release_pending();
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { state })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn next_entry(&mut self) -> io::Result<Option<RawDirEntry>> {
|
||||||
|
poll_fn(|cx| self.state.poll_next(cx)).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ReadDirState {
|
||||||
|
owner: ThreadHandle,
|
||||||
|
queue: Mutex<VecDeque<io::Result<RawDirEntry>>>,
|
||||||
|
done: AtomicBool,
|
||||||
|
pending: AtomicBool,
|
||||||
|
wake_queued: AtomicBool,
|
||||||
|
waker: Mutex<Option<Waker>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadDirState {
|
||||||
|
fn new(owner: ThreadHandle) -> Self {
|
||||||
|
owner.begin_async_operation();
|
||||||
|
Self {
|
||||||
|
owner,
|
||||||
|
queue: Mutex::new(VecDeque::new()),
|
||||||
|
done: AtomicBool::new(false),
|
||||||
|
pending: AtomicBool::new(true),
|
||||||
|
wake_queued: AtomicBool::new(false),
|
||||||
|
waker: Mutex::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(self: &Arc<Self>, entry: io::Result<RawDirEntry>) {
|
||||||
|
self.queue.lock().unwrap().push_back(entry);
|
||||||
|
self.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self: &Arc<Self>) {
|
||||||
|
self.done.store(true, Ordering::Release);
|
||||||
|
self.release_pending();
|
||||||
|
self.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release_pending(&self) {
|
||||||
|
if self.pending.swap(false, Ordering::AcqRel) {
|
||||||
|
self.owner.finish_async_operation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify(self: &Arc<Self>) {
|
||||||
|
if self.wake_queued.swap(true, Ordering::AcqRel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = Arc::clone(self);
|
||||||
|
if !self.owner.queue_task(move || {
|
||||||
|
state.wake_queued.store(false, Ordering::Release);
|
||||||
|
if let Some(waker) = state.waker.lock().unwrap().take() {
|
||||||
|
waker.wake();
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
self.wake_queued.store(false, Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_next(&self, cx: &mut Context<'_>) -> Poll<io::Result<Option<RawDirEntry>>> {
|
||||||
|
if let Some(entry) = self.queue.lock().unwrap().pop_front() {
|
||||||
|
return Poll::Ready(entry.map(Some));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.done.load(Ordering::Acquire) {
|
||||||
|
return Poll::Ready(Ok(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.waker.lock().unwrap() = Some(cx.waker().clone());
|
||||||
|
|
||||||
|
if let Some(entry) = self.queue.lock().unwrap().pop_front() {
|
||||||
|
let _ = self.waker.lock().unwrap().take();
|
||||||
|
return Poll::Ready(entry.map(Some));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.done.load(Ordering::Acquire) {
|
||||||
|
let _ = self.waker.lock().unwrap().take();
|
||||||
|
return Poll::Ready(Ok(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ReadDirStream {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.state.release_pending();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn produce_dir_entries(path: PathBuf, state: Arc<ReadDirState>) {
|
||||||
|
match std::fs::read_dir(path) {
|
||||||
|
Ok(entries) => {
|
||||||
|
for entry in entries {
|
||||||
|
match entry {
|
||||||
|
Ok(entry) => {
|
||||||
|
let file_name = entry.file_name();
|
||||||
|
state.push(Ok(RawDirEntry {
|
||||||
|
path: entry.path(),
|
||||||
|
file_name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Err(error) => state.push(Err(error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.finish();
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
state.push(Err(error));
|
||||||
|
state.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn offload<T: Send + 'static>(
|
||||||
|
work: impl FnOnce() -> io::Result<T> + Send + 'static,
|
||||||
|
) -> io::Result<T> {
|
||||||
|
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
||||||
|
let handle_for_task = handle.clone();
|
||||||
|
if let Err(error) = spawn_blocking(Box::new(move || handle_for_task.complete(work()))) {
|
||||||
|
handle.complete(Err(error));
|
||||||
|
}
|
||||||
|
future.await
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BlockingPool {
|
||||||
|
sender: mpsc::SyncSender<BlockingTask>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockingPool {
|
||||||
|
fn spawn(&self, task: BlockingTask) -> io::Result<()> {
|
||||||
|
self.sender.try_send(task).map_err(|error| match error {
|
||||||
|
mpsc::TrySendError::Full(_) => io::Error::new(
|
||||||
|
io::ErrorKind::WouldBlock,
|
||||||
|
"filesystem blocking worker queue is full",
|
||||||
|
),
|
||||||
|
mpsc::TrySendError::Disconnected(_) => io::Error::new(
|
||||||
|
io::ErrorKind::BrokenPipe,
|
||||||
|
"filesystem blocking worker pool has stopped",
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_blocking(task: BlockingTask) -> io::Result<()> {
|
||||||
|
blocking_pool()?.spawn(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blocking_pool() -> io::Result<&'static BlockingPool> {
|
||||||
|
match BLOCKING_POOL.get_or_init(create_blocking_pool) {
|
||||||
|
Ok(pool) => Ok(pool),
|
||||||
|
Err(error) => Err(io::Error::new(error.kind(), error.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_blocking_pool() -> io::Result<BlockingPool> {
|
||||||
|
let (sender, receiver) = mpsc::sync_channel::<BlockingTask>(BLOCKING_QUEUE_CAPACITY);
|
||||||
|
let receiver = Arc::new(Mutex::new(receiver));
|
||||||
|
let worker_count = std::thread::available_parallelism()
|
||||||
|
.map(usize::from)
|
||||||
|
.unwrap_or(2)
|
||||||
|
.clamp(2, 4);
|
||||||
|
let mut spawned = 0usize;
|
||||||
|
let mut last_error = None;
|
||||||
|
|
||||||
|
for index in 0..worker_count {
|
||||||
|
let receiver = Arc::clone(&receiver);
|
||||||
|
match thread::Builder::new()
|
||||||
|
.name(format!("ruin-runtime-fs-offload-{index}"))
|
||||||
|
.spawn(move || {
|
||||||
|
loop {
|
||||||
|
let task = receiver.lock().unwrap().recv();
|
||||||
|
match task {
|
||||||
|
Ok(task) => task(),
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Ok(_) => spawned += 1,
|
||||||
|
Err(error) => last_error = Some(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if spawned == 0 {
|
||||||
|
return Err(io::Error::other(
|
||||||
|
last_error.expect("at least one worker spawn should have been attempted"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(BlockingPool { sender })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_to_c_string(path: PathBuf) -> io::Result<CString> {
|
||||||
|
CString::new(path.as_os_str().as_bytes()).map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"path contains interior NUL bytes",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_metadata_from_std(metadata: &std::fs::Metadata) -> RawMetadata {
|
||||||
|
let file_type = metadata.file_type();
|
||||||
|
let kind = if file_type.is_file() {
|
||||||
|
FileType::File
|
||||||
|
} else if file_type.is_dir() {
|
||||||
|
FileType::Directory
|
||||||
|
} else if file_type.is_symlink() {
|
||||||
|
FileType::Symlink
|
||||||
|
} else if file_type.is_block_device() {
|
||||||
|
FileType::BlockDevice
|
||||||
|
} else if file_type.is_char_device() {
|
||||||
|
FileType::CharacterDevice
|
||||||
|
} else if file_type.is_fifo() {
|
||||||
|
FileType::Fifo
|
||||||
|
} else if file_type.is_socket() {
|
||||||
|
FileType::Socket
|
||||||
|
} else {
|
||||||
|
FileType::Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
RawMetadata {
|
||||||
|
file_type: kind,
|
||||||
|
mode: (metadata.mode() & 0o7777) as u16,
|
||||||
|
len: metadata.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_metadata_from_stat(stat: &libc::stat) -> RawMetadata {
|
||||||
|
let kind = match stat.st_mode & libc::S_IFMT {
|
||||||
|
libc::S_IFREG => FileType::File,
|
||||||
|
libc::S_IFDIR => FileType::Directory,
|
||||||
|
libc::S_IFLNK => FileType::Symlink,
|
||||||
|
libc::S_IFBLK => FileType::BlockDevice,
|
||||||
|
libc::S_IFCHR => FileType::CharacterDevice,
|
||||||
|
libc::S_IFIFO => FileType::Fifo,
|
||||||
|
libc::S_IFSOCK => FileType::Socket,
|
||||||
|
_ => FileType::Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
RawMetadata {
|
||||||
|
file_type: kind,
|
||||||
|
mode: stat.st_mode & 0o7777,
|
||||||
|
len: stat.st_size as u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cvt(value: libc::c_int) -> io::Result<libc::c_int> {
|
||||||
|
if value < 0 {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
lib/runtime/src/sys/macos/mod.rs
Normal file
6
lib/runtime/src/sys/macos/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
//! macOS backend modules.
|
||||||
|
|
||||||
|
pub mod channel;
|
||||||
|
pub mod fd;
|
||||||
|
pub mod fs;
|
||||||
|
pub mod net;
|
||||||
895
lib/runtime/src/sys/macos/net.rs
Normal file
895
lib/runtime/src/sys/macos/net.rs
Normal file
@@ -0,0 +1,895 @@
|
|||||||
|
//! macOS networking backend.
|
||||||
|
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::io;
|
||||||
|
use std::mem::MaybeUninit;
|
||||||
|
use std::net::{
|
||||||
|
Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs,
|
||||||
|
};
|
||||||
|
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock, mpsc};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::op::completion::completion_for_current_thread;
|
||||||
|
use crate::op::net::{AcceptedSocket, NetOp, ReceivedDatagram};
|
||||||
|
|
||||||
|
const DEFAULT_LISTENER_BACKLOG: i32 = 1024;
|
||||||
|
const DNS_QUEUE_CAPACITY: usize = 256;
|
||||||
|
|
||||||
|
type RecvFuture = Pin<Box<dyn Future<Output = io::Result<Vec<u8>>> + 'static>>;
|
||||||
|
type SendFuture = Pin<Box<dyn Future<Output = io::Result<usize>> + 'static>>;
|
||||||
|
type ShutdownFuture = Pin<Box<dyn Future<Output = io::Result<()>> + 'static>>;
|
||||||
|
type BlockingTask = Box<dyn FnOnce() + Send + 'static>;
|
||||||
|
|
||||||
|
static DNS_POOL: OnceLock<io::Result<BlockingPool>> = OnceLock::new();
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum ExecutionPath {
|
||||||
|
Kqueue,
|
||||||
|
Offload,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execution_path(op: &NetOp) -> ExecutionPath {
|
||||||
|
match op {
|
||||||
|
NetOp::Socket { .. }
|
||||||
|
| NetOp::Connect { .. }
|
||||||
|
| NetOp::Bind { .. }
|
||||||
|
| NetOp::Listen { .. }
|
||||||
|
| NetOp::Accept { .. }
|
||||||
|
| NetOp::Send { .. }
|
||||||
|
| NetOp::SendTo { .. }
|
||||||
|
| NetOp::Recv { .. }
|
||||||
|
| NetOp::RecvFrom { .. }
|
||||||
|
| NetOp::Shutdown { .. }
|
||||||
|
| NetOp::Close { .. } => ExecutionPath::Kqueue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn resolve_addrs<A>(addr: A) -> io::Result<Vec<SocketAddr>>
|
||||||
|
where
|
||||||
|
A: ToSocketAddrs + Send + 'static,
|
||||||
|
{
|
||||||
|
offload(move || {
|
||||||
|
let addrs = addr.to_socket_addrs()?.collect::<Vec<_>>();
|
||||||
|
if addrs.is_empty() {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"address resolved to no socket addresses",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(addrs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn socket(op: NetOp) -> io::Result<OwnedFd> {
|
||||||
|
let NetOp::Socket {
|
||||||
|
domain,
|
||||||
|
socket_type,
|
||||||
|
protocol,
|
||||||
|
flags,
|
||||||
|
} = op
|
||||||
|
else {
|
||||||
|
unreachable!("socket backend called with non-socket op");
|
||||||
|
};
|
||||||
|
|
||||||
|
socket_sync(domain, socket_type, protocol, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect(op: NetOp) -> io::Result<()> {
|
||||||
|
let NetOp::Connect { fd, addr } = op else {
|
||||||
|
unreachable!("connect backend called with non-connect op");
|
||||||
|
};
|
||||||
|
|
||||||
|
connect_async(fd, RawSocketAddr::from_socket_addr(addr)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bind(op: NetOp) -> io::Result<()> {
|
||||||
|
let NetOp::Bind { fd, addr } = op else {
|
||||||
|
unreachable!("bind backend called with non-bind op");
|
||||||
|
};
|
||||||
|
|
||||||
|
bind_sync(fd, RawSocketAddr::from_socket_addr(addr))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn listen(op: NetOp) -> io::Result<()> {
|
||||||
|
let NetOp::Listen { fd, backlog } = op else {
|
||||||
|
unreachable!("listen backend called with non-listen op");
|
||||||
|
};
|
||||||
|
|
||||||
|
listen_sync(fd, backlog)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn accept(op: NetOp) -> io::Result<AcceptedSocket> {
|
||||||
|
let NetOp::Accept { fd } = op else {
|
||||||
|
unreachable!("accept backend called with non-accept op");
|
||||||
|
};
|
||||||
|
|
||||||
|
accept_async(fd).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send(op: NetOp) -> io::Result<usize> {
|
||||||
|
let NetOp::Send { fd, data, flags } = op else {
|
||||||
|
unreachable!("send backend called with non-send op");
|
||||||
|
};
|
||||||
|
|
||||||
|
send_async(fd, data, flags).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_to(op: NetOp) -> io::Result<usize> {
|
||||||
|
let NetOp::SendTo {
|
||||||
|
fd,
|
||||||
|
target,
|
||||||
|
data,
|
||||||
|
flags,
|
||||||
|
} = op
|
||||||
|
else {
|
||||||
|
unreachable!("send_to backend called with non-send_to op");
|
||||||
|
};
|
||||||
|
|
||||||
|
send_to_async(fd, target, data, flags).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn recv(op: NetOp) -> io::Result<Vec<u8>> {
|
||||||
|
let NetOp::Recv { fd, len, flags } = op else {
|
||||||
|
unreachable!("recv backend called with non-recv op");
|
||||||
|
};
|
||||||
|
|
||||||
|
recv_async(fd, len, flags).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn recv_from(op: NetOp) -> io::Result<ReceivedDatagram> {
|
||||||
|
let NetOp::RecvFrom { fd, len, flags } = op else {
|
||||||
|
unreachable!("recv_from backend called with non-recv_from op");
|
||||||
|
};
|
||||||
|
|
||||||
|
recv_from_async(fd, len, flags).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn shutdown(op: NetOp) -> io::Result<()> {
|
||||||
|
let NetOp::Shutdown { fd, how } = op else {
|
||||||
|
unreachable!("shutdown backend called with non-shutdown op");
|
||||||
|
};
|
||||||
|
|
||||||
|
shutdown_sync(fd, how)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close(op: NetOp) -> io::Result<()> {
|
||||||
|
let NetOp::Close { fd } = op else {
|
||||||
|
unreachable!("close backend called with non-close op");
|
||||||
|
};
|
||||||
|
|
||||||
|
close_sync(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_stream(addr: SocketAddr) -> io::Result<OwnedFd> {
|
||||||
|
match connect_stream_inner(addr).await {
|
||||||
|
Err(error) if should_try_ipv4_loopback(addr, &error) => {
|
||||||
|
connect_stream_inner(localhost_v4(addr)).await
|
||||||
|
}
|
||||||
|
result => result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bind_listener(addr: SocketAddr, backlog: Option<i32>) -> io::Result<OwnedFd> {
|
||||||
|
match bind_listener_inner(addr, backlog).await {
|
||||||
|
Err(error) if should_try_ipv4_loopback(addr, &error) => {
|
||||||
|
bind_listener_inner(localhost_v4(addr), backlog).await
|
||||||
|
}
|
||||||
|
result => result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bind_datagram(addr: SocketAddr) -> io::Result<OwnedFd> {
|
||||||
|
match bind_datagram_inner(addr).await {
|
||||||
|
Err(error) if should_try_ipv4_loopback(addr, &error) => {
|
||||||
|
bind_datagram_inner(localhost_v4(addr)).await
|
||||||
|
}
|
||||||
|
result => result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_stream_inner(addr: SocketAddr) -> io::Result<OwnedFd> {
|
||||||
|
let stream = socket(NetOp::Socket {
|
||||||
|
domain: socket_domain(addr),
|
||||||
|
socket_type: libc::SOCK_STREAM,
|
||||||
|
protocol: 0,
|
||||||
|
flags: 0,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
connect(NetOp::Connect {
|
||||||
|
fd: stream.as_raw_fd(),
|
||||||
|
addr,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn bind_listener_inner(addr: SocketAddr, backlog: Option<i32>) -> io::Result<OwnedFd> {
|
||||||
|
let listener = socket(NetOp::Socket {
|
||||||
|
domain: socket_domain(addr),
|
||||||
|
socket_type: libc::SOCK_STREAM,
|
||||||
|
protocol: 0,
|
||||||
|
flags: 0,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
set_reuse_addr(listener.as_raw_fd(), true)?;
|
||||||
|
|
||||||
|
bind(NetOp::Bind {
|
||||||
|
fd: listener.as_raw_fd(),
|
||||||
|
addr,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
listen(NetOp::Listen {
|
||||||
|
fd: listener.as_raw_fd(),
|
||||||
|
backlog: backlog.unwrap_or(DEFAULT_LISTENER_BACKLOG),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn bind_datagram_inner(addr: SocketAddr) -> io::Result<OwnedFd> {
|
||||||
|
let socket = socket(NetOp::Socket {
|
||||||
|
domain: socket_domain(addr),
|
||||||
|
socket_type: libc::SOCK_DGRAM,
|
||||||
|
protocol: 0,
|
||||||
|
flags: 0,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
bind(NetOp::Bind {
|
||||||
|
fd: socket.as_raw_fd(),
|
||||||
|
addr,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn duplicate(fd: RawFd) -> io::Result<OwnedFd> {
|
||||||
|
let duplicated = cvt(unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) })?;
|
||||||
|
set_nonblocking(duplicated)?;
|
||||||
|
Ok(unsafe { OwnedFd::from_raw_fd(duplicated) })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn recv_timeout(
|
||||||
|
fd: RawFd,
|
||||||
|
len: usize,
|
||||||
|
flags: i32,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> io::Result<Vec<u8>> {
|
||||||
|
io_timeout(timeout, recv_async(fd, len, flags)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_timeout(
|
||||||
|
fd: RawFd,
|
||||||
|
data: Vec<u8>,
|
||||||
|
flags: i32,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> io::Result<usize> {
|
||||||
|
io_timeout(timeout, send_async(fd, data, flags)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn recv_from_timeout(
|
||||||
|
fd: RawFd,
|
||||||
|
len: usize,
|
||||||
|
flags: i32,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> io::Result<ReceivedDatagram> {
|
||||||
|
io_timeout(timeout, recv_from_async(fd, len, flags)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_to_timeout(
|
||||||
|
fd: RawFd,
|
||||||
|
data: Vec<u8>,
|
||||||
|
target: SocketAddr,
|
||||||
|
flags: i32,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> io::Result<usize> {
|
||||||
|
io_timeout(timeout, send_to_async(fd, target, data, flags)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_stream_timeout(addr: SocketAddr, timeout: Duration) -> io::Result<OwnedFd> {
|
||||||
|
let fd = socket_sync(socket_domain(addr), libc::SOCK_STREAM, 0, 0)?;
|
||||||
|
if let Err(error) = io_timeout(
|
||||||
|
timeout,
|
||||||
|
connect_async(fd.as_raw_fd(), RawSocketAddr::from_socket_addr(addr)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
drop(fd);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
Ok(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local_addr(fd: RawFd) -> io::Result<SocketAddr> {
|
||||||
|
socket_addr_with(libc::getsockname, fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peer_addr(fd: RawFd) -> io::Result<SocketAddr> {
|
||||||
|
socket_addr_with(libc::getpeername, fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nodelay(fd: RawFd) -> io::Result<bool> {
|
||||||
|
let mut value = 0;
|
||||||
|
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
|
||||||
|
cvt(unsafe {
|
||||||
|
libc::getsockopt(
|
||||||
|
fd,
|
||||||
|
libc::IPPROTO_TCP,
|
||||||
|
libc::TCP_NODELAY,
|
||||||
|
&mut value as *mut libc::c_int as *mut c_void,
|
||||||
|
&mut len,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(value != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn broadcast(fd: RawFd) -> io::Result<bool> {
|
||||||
|
getsockopt_int(fd, libc::SOL_SOCKET, libc::SO_BROADCAST).map(|value| value != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_broadcast(fd: RawFd, enabled: bool) -> io::Result<()> {
|
||||||
|
setsockopt_int(fd, libc::SOL_SOCKET, libc::SO_BROADCAST, enabled.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ttl(fd: RawFd) -> io::Result<u32> {
|
||||||
|
match socket_family(fd)? {
|
||||||
|
libc::AF_INET => {
|
||||||
|
getsockopt_int(fd, libc::IPPROTO_IP, libc::IP_TTL).map(|value| value as u32)
|
||||||
|
}
|
||||||
|
libc::AF_INET6 => getsockopt_int(fd, libc::IPPROTO_IPV6, libc::IPV6_UNICAST_HOPS)
|
||||||
|
.map(|value| value as u32),
|
||||||
|
family => Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("unsupported socket family {family} for TTL"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_ttl(fd: RawFd, ttl: u32) -> io::Result<()> {
|
||||||
|
let ttl = i32::try_from(ttl)
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "TTL exceeds i32 range"))?;
|
||||||
|
match socket_family(fd)? {
|
||||||
|
libc::AF_INET => setsockopt_int(fd, libc::IPPROTO_IP, libc::IP_TTL, ttl),
|
||||||
|
libc::AF_INET6 => setsockopt_int(fd, libc::IPPROTO_IPV6, libc::IPV6_UNICAST_HOPS, ttl),
|
||||||
|
family => Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("unsupported socket family {family} for TTL"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_nodelay(fd: RawFd, enabled: bool) -> io::Result<()> {
|
||||||
|
let value: libc::c_int = enabled.into();
|
||||||
|
cvt(unsafe {
|
||||||
|
libc::setsockopt(
|
||||||
|
fd,
|
||||||
|
libc::IPPROTO_TCP,
|
||||||
|
libc::TCP_NODELAY,
|
||||||
|
&value as *const libc::c_int as *const c_void,
|
||||||
|
std::mem::size_of_val(&value) as libc::socklen_t,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recv_future(fd: RawFd, len: usize) -> RecvFuture {
|
||||||
|
Box::pin(recv(NetOp::Recv { fd, len, flags: 0 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_future(fd: RawFd, data: Vec<u8>) -> SendFuture {
|
||||||
|
Box::pin(send(NetOp::Send { fd, data, flags: 0 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown_future(fd: RawFd, how: Shutdown) -> ShutdownFuture {
|
||||||
|
Box::pin(shutdown(NetOp::Shutdown { fd, how }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn offload<T: Send + 'static>(
|
||||||
|
work: impl FnOnce() -> io::Result<T> + Send + 'static,
|
||||||
|
) -> io::Result<T> {
|
||||||
|
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
||||||
|
if let Err(error) = dns_pool().and_then(|pool| {
|
||||||
|
pool.spawn(Box::new({
|
||||||
|
let handle = handle.clone();
|
||||||
|
move || handle.complete(work())
|
||||||
|
}))
|
||||||
|
}) {
|
||||||
|
handle.complete(Err(error));
|
||||||
|
}
|
||||||
|
future.await
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BlockingPool {
|
||||||
|
sender: mpsc::SyncSender<BlockingTask>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockingPool {
|
||||||
|
fn spawn(&self, task: BlockingTask) -> io::Result<()> {
|
||||||
|
self.sender.try_send(task).map_err(|error| match error {
|
||||||
|
mpsc::TrySendError::Full(_) => io::Error::new(
|
||||||
|
io::ErrorKind::WouldBlock,
|
||||||
|
"DNS resolver blocking worker queue is full",
|
||||||
|
),
|
||||||
|
mpsc::TrySendError::Disconnected(_) => io::Error::new(
|
||||||
|
io::ErrorKind::BrokenPipe,
|
||||||
|
"DNS resolver blocking worker pool has stopped",
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dns_pool() -> io::Result<&'static BlockingPool> {
|
||||||
|
match DNS_POOL.get_or_init(create_dns_pool) {
|
||||||
|
Ok(pool) => Ok(pool),
|
||||||
|
Err(error) => Err(io::Error::new(error.kind(), error.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_dns_pool() -> io::Result<BlockingPool> {
|
||||||
|
let (sender, receiver) = mpsc::sync_channel::<BlockingTask>(DNS_QUEUE_CAPACITY);
|
||||||
|
let receiver = Arc::new(Mutex::new(receiver));
|
||||||
|
let worker_count = std::thread::available_parallelism()
|
||||||
|
.map(usize::from)
|
||||||
|
.unwrap_or(2)
|
||||||
|
.clamp(2, 4);
|
||||||
|
let mut spawned = 0usize;
|
||||||
|
let mut last_error = None;
|
||||||
|
|
||||||
|
for index in 0..worker_count {
|
||||||
|
let receiver = Arc::clone(&receiver);
|
||||||
|
match thread::Builder::new()
|
||||||
|
.name(format!("ruin-runtime-dns-{index}"))
|
||||||
|
.spawn(move || {
|
||||||
|
loop {
|
||||||
|
let task = {
|
||||||
|
let receiver = receiver.lock().expect("DNS worker queue mutex poisoned");
|
||||||
|
receiver.recv()
|
||||||
|
};
|
||||||
|
match task {
|
||||||
|
Ok(task) => task(),
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Ok(_) => spawned += 1,
|
||||||
|
Err(error) => last_error = Some(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if spawned == 0 {
|
||||||
|
return Err(io::Error::other(last_error.expect(
|
||||||
|
"at least one DNS worker spawn should have been attempted",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(BlockingPool { sender })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn io_timeout<T>(
|
||||||
|
timeout: Duration,
|
||||||
|
future: impl Future<Output = io::Result<T>>,
|
||||||
|
) -> io::Result<T> {
|
||||||
|
crate::time::timeout(timeout, future)
|
||||||
|
.await
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "operation timed out"))?
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_domain(addr: SocketAddr) -> i32 {
|
||||||
|
match addr {
|
||||||
|
SocketAddr::V4(_) => libc::AF_INET,
|
||||||
|
SocketAddr::V6(_) => libc::AF_INET6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdown_how(how: Shutdown) -> i32 {
|
||||||
|
match how {
|
||||||
|
Shutdown::Read => libc::SHUT_RD,
|
||||||
|
Shutdown::Write => libc::SHUT_WR,
|
||||||
|
Shutdown::Both => libc::SHUT_RDWR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_addr_with(
|
||||||
|
op: unsafe extern "C" fn(RawFd, *mut libc::sockaddr, *mut libc::socklen_t) -> libc::c_int,
|
||||||
|
fd: RawFd,
|
||||||
|
) -> io::Result<SocketAddr> {
|
||||||
|
let mut storage = MaybeUninit::<libc::sockaddr_storage>::zeroed();
|
||||||
|
let mut len = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
|
||||||
|
cvt(unsafe { op(fd, storage.as_mut_ptr().cast::<libc::sockaddr>(), &mut len) })?;
|
||||||
|
let storage = unsafe { storage.assume_init() };
|
||||||
|
socket_addr_from_storage(&storage, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_reuse_addr(fd: RawFd, enabled: bool) -> io::Result<()> {
|
||||||
|
setsockopt_int(fd, libc::SOL_SOCKET, libc::SO_REUSEADDR, enabled.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_family(fd: RawFd) -> io::Result<i32> {
|
||||||
|
let mut storage = MaybeUninit::<libc::sockaddr_storage>::zeroed();
|
||||||
|
let mut len = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
|
||||||
|
cvt(unsafe { libc::getsockname(fd, storage.as_mut_ptr().cast::<libc::sockaddr>(), &mut len) })?;
|
||||||
|
let storage = unsafe { storage.assume_init() };
|
||||||
|
Ok(storage.ss_family as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getsockopt_int(fd: RawFd, level: i32, name: i32) -> io::Result<i32> {
|
||||||
|
let mut value = 0;
|
||||||
|
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
|
||||||
|
cvt(unsafe {
|
||||||
|
libc::getsockopt(
|
||||||
|
fd,
|
||||||
|
level,
|
||||||
|
name,
|
||||||
|
&mut value as *mut libc::c_int as *mut c_void,
|
||||||
|
&mut len,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setsockopt_int(fd: RawFd, level: i32, name: i32, value: i32) -> io::Result<()> {
|
||||||
|
cvt(unsafe {
|
||||||
|
libc::setsockopt(
|
||||||
|
fd,
|
||||||
|
level,
|
||||||
|
name,
|
||||||
|
&value as *const libc::c_int as *const c_void,
|
||||||
|
std::mem::size_of_val(&value) as libc::socklen_t,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_addr_from_storage(
|
||||||
|
storage: &libc::sockaddr_storage,
|
||||||
|
len: libc::socklen_t,
|
||||||
|
) -> io::Result<SocketAddr> {
|
||||||
|
match storage.ss_family as i32 {
|
||||||
|
libc::AF_INET => {
|
||||||
|
if len < std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"sockaddr_in length is truncated",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let addr = unsafe { *(storage as *const _ as *const libc::sockaddr_in) };
|
||||||
|
Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||||
|
Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr)),
|
||||||
|
u16::from_be(addr.sin_port),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
libc::AF_INET6 => {
|
||||||
|
if len < std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"sockaddr_in6 length is truncated",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let addr = unsafe { *(storage as *const _ as *const libc::sockaddr_in6) };
|
||||||
|
Ok(SocketAddr::V6(SocketAddrV6::new(
|
||||||
|
Ipv6Addr::from(addr.sin6_addr.s6_addr),
|
||||||
|
u16::from_be(addr.sin6_port),
|
||||||
|
addr.sin6_flowinfo,
|
||||||
|
addr.sin6_scope_id,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
family => Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("unsupported socket family {family}"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct RawSocketAddr {
|
||||||
|
storage: libc::sockaddr_storage,
|
||||||
|
len: libc::socklen_t,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawSocketAddr {
|
||||||
|
fn from_socket_addr(addr: SocketAddr) -> Self {
|
||||||
|
match addr {
|
||||||
|
SocketAddr::V4(addr) => {
|
||||||
|
let sockaddr = libc::sockaddr_in {
|
||||||
|
sin_len: std::mem::size_of::<libc::sockaddr_in>() as u8,
|
||||||
|
sin_family: libc::AF_INET as libc::sa_family_t,
|
||||||
|
sin_port: addr.port().to_be(),
|
||||||
|
sin_addr: libc::in_addr {
|
||||||
|
s_addr: u32::from_be_bytes(addr.ip().octets()).to_be(),
|
||||||
|
},
|
||||||
|
sin_zero: [0; 8],
|
||||||
|
};
|
||||||
|
let mut storage =
|
||||||
|
unsafe { MaybeUninit::<libc::sockaddr_storage>::zeroed().assume_init() };
|
||||||
|
unsafe {
|
||||||
|
std::ptr::write(
|
||||||
|
&mut storage as *mut libc::sockaddr_storage as *mut libc::sockaddr_in,
|
||||||
|
sockaddr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
storage,
|
||||||
|
len: std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SocketAddr::V6(addr) => {
|
||||||
|
let sockaddr = libc::sockaddr_in6 {
|
||||||
|
sin6_len: std::mem::size_of::<libc::sockaddr_in6>() as u8,
|
||||||
|
sin6_family: libc::AF_INET6 as libc::sa_family_t,
|
||||||
|
sin6_port: addr.port().to_be(),
|
||||||
|
sin6_flowinfo: addr.flowinfo(),
|
||||||
|
sin6_addr: libc::in6_addr {
|
||||||
|
s6_addr: addr.ip().octets(),
|
||||||
|
},
|
||||||
|
sin6_scope_id: addr.scope_id(),
|
||||||
|
};
|
||||||
|
let mut storage =
|
||||||
|
unsafe { MaybeUninit::<libc::sockaddr_storage>::zeroed().assume_init() };
|
||||||
|
unsafe {
|
||||||
|
std::ptr::write(
|
||||||
|
&mut storage as *mut libc::sockaddr_storage as *mut libc::sockaddr_in6,
|
||||||
|
sockaddr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
storage,
|
||||||
|
len: std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_ptr(&self) -> *const libc::sockaddr {
|
||||||
|
&self.storage as *const libc::sockaddr_storage as *const libc::sockaddr
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> libc::socklen_t {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_sync(domain: i32, socket_type: i32, protocol: i32, _flags: u32) -> io::Result<OwnedFd> {
|
||||||
|
let fd = cvt(unsafe { libc::socket(domain, socket_type, protocol) })?;
|
||||||
|
set_cloexec(fd)?;
|
||||||
|
set_nonblocking(fd)?;
|
||||||
|
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_async(fd: RawFd, addr: RawSocketAddr) -> io::Result<()> {
|
||||||
|
loop {
|
||||||
|
let result = unsafe { libc::connect(fd, addr.as_ptr(), addr.len()) };
|
||||||
|
if result == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let error = io::Error::last_os_error();
|
||||||
|
match error.raw_os_error() {
|
||||||
|
Some(libc::EINTR) => continue,
|
||||||
|
Some(libc::EINPROGRESS) | Some(libc::EALREADY) => {
|
||||||
|
crate::sys::current::fd::wait_writable(fd).await?;
|
||||||
|
return socket_error(fd);
|
||||||
|
}
|
||||||
|
Some(libc::EISCONN) => return Ok(()),
|
||||||
|
_ => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bind_sync(fd: RawFd, addr: RawSocketAddr) -> io::Result<()> {
|
||||||
|
cvt(unsafe { libc::bind(fd, addr.as_ptr(), addr.len()) }).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listen_sync(fd: RawFd, backlog: i32) -> io::Result<()> {
|
||||||
|
cvt(unsafe { libc::listen(fd, backlog) }).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_sync(fd: RawFd) -> io::Result<AcceptedSocket> {
|
||||||
|
let mut storage = MaybeUninit::<libc::sockaddr_storage>::zeroed();
|
||||||
|
let mut len = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
|
||||||
|
let accepted_fd =
|
||||||
|
cvt(unsafe { libc::accept(fd, storage.as_mut_ptr().cast::<libc::sockaddr>(), &mut len) })?;
|
||||||
|
set_cloexec(accepted_fd)?;
|
||||||
|
let storage = unsafe { storage.assume_init() };
|
||||||
|
let peer_addr = socket_addr_from_storage(&storage, len)?;
|
||||||
|
Ok(AcceptedSocket {
|
||||||
|
fd: accepted_fd,
|
||||||
|
peer_addr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn accept_async(fd: RawFd) -> io::Result<AcceptedSocket> {
|
||||||
|
loop {
|
||||||
|
match accept_sync(fd) {
|
||||||
|
Ok(socket) => {
|
||||||
|
set_nonblocking(socket.fd)?;
|
||||||
|
return Ok(socket);
|
||||||
|
}
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||||
|
crate::sys::current::fd::wait_readable(fd).await?;
|
||||||
|
}
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_async(fd: RawFd, data: Vec<u8>, flags: i32) -> io::Result<usize> {
|
||||||
|
loop {
|
||||||
|
match send_slice_sync(fd, &data, flags) {
|
||||||
|
Ok(written) => return Ok(written),
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||||
|
crate::sys::current::fd::wait_writable(fd).await?;
|
||||||
|
}
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_slice_sync(fd: RawFd, data: &[u8], flags: i32) -> io::Result<usize> {
|
||||||
|
let written = unsafe { libc::send(fd, data.as_ptr().cast::<c_void>(), data.len(), flags) };
|
||||||
|
cvt_long(written).map(|written| written as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_to_async(
|
||||||
|
fd: RawFd,
|
||||||
|
target: SocketAddr,
|
||||||
|
data: Vec<u8>,
|
||||||
|
flags: i32,
|
||||||
|
) -> io::Result<usize> {
|
||||||
|
loop {
|
||||||
|
match send_to_slice_sync(fd, target, &data, flags) {
|
||||||
|
Ok(written) => return Ok(written),
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||||
|
crate::sys::current::fd::wait_writable(fd).await?;
|
||||||
|
}
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_to_slice_sync(fd: RawFd, target: SocketAddr, data: &[u8], flags: i32) -> io::Result<usize> {
|
||||||
|
let addr = RawSocketAddr::from_socket_addr(target);
|
||||||
|
let written = unsafe {
|
||||||
|
libc::sendto(
|
||||||
|
fd,
|
||||||
|
data.as_ptr().cast::<c_void>(),
|
||||||
|
data.len(),
|
||||||
|
flags,
|
||||||
|
addr.as_ptr(),
|
||||||
|
addr.len(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
cvt_long(written).map(|written| written as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recv_async(fd: RawFd, len: usize, flags: i32) -> io::Result<Vec<u8>> {
|
||||||
|
loop {
|
||||||
|
match recv_sync(fd, len, flags) {
|
||||||
|
Ok(data) => return Ok(data),
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||||
|
crate::sys::current::fd::wait_readable(fd).await?;
|
||||||
|
}
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recv_sync(fd: RawFd, len: usize, flags: i32) -> io::Result<Vec<u8>> {
|
||||||
|
let mut data = vec![0u8; len];
|
||||||
|
let read = unsafe { libc::recv(fd, data.as_mut_ptr().cast::<c_void>(), len, flags) };
|
||||||
|
let read = cvt_long(read)? as usize;
|
||||||
|
data.truncate(read);
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recv_from_async(fd: RawFd, len: usize, flags: i32) -> io::Result<ReceivedDatagram> {
|
||||||
|
loop {
|
||||||
|
match recv_from_sync(fd, len, flags) {
|
||||||
|
Ok(datagram) => return Ok(datagram),
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||||
|
crate::sys::current::fd::wait_readable(fd).await?;
|
||||||
|
}
|
||||||
|
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recv_from_sync(fd: RawFd, len: usize, flags: i32) -> io::Result<ReceivedDatagram> {
|
||||||
|
let mut data = vec![0u8; len];
|
||||||
|
let mut storage = MaybeUninit::<libc::sockaddr_storage>::zeroed();
|
||||||
|
let mut addr_len = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
|
||||||
|
let read = unsafe {
|
||||||
|
libc::recvfrom(
|
||||||
|
fd,
|
||||||
|
data.as_mut_ptr().cast::<c_void>(),
|
||||||
|
len,
|
||||||
|
flags,
|
||||||
|
storage.as_mut_ptr().cast::<libc::sockaddr>(),
|
||||||
|
&mut addr_len,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let read = cvt_long(read)? as usize;
|
||||||
|
data.truncate(read);
|
||||||
|
let storage = unsafe { storage.assume_init() };
|
||||||
|
let peer_addr = socket_addr_from_storage(&storage, addr_len)?;
|
||||||
|
Ok(ReceivedDatagram { data, peer_addr })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdown_sync(fd: RawFd, how: Shutdown) -> io::Result<()> {
|
||||||
|
cvt(unsafe { libc::shutdown(fd, shutdown_how(how)) }).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_sync(fd: RawFd) -> io::Result<()> {
|
||||||
|
cvt(unsafe { libc::close(fd) }).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_cloexec(fd: RawFd) -> io::Result<()> {
|
||||||
|
let flags = cvt(unsafe { libc::fcntl(fd, libc::F_GETFD) })?;
|
||||||
|
cvt(unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) })?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_nonblocking(fd: RawFd) -> io::Result<()> {
|
||||||
|
let flags = cvt(unsafe { libc::fcntl(fd, libc::F_GETFL) })?;
|
||||||
|
cvt(unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) })?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_try_ipv4_loopback(addr: SocketAddr, error: &io::Error) -> bool {
|
||||||
|
matches!(addr, SocketAddr::V6(v6) if v6.ip().is_loopback())
|
||||||
|
&& matches!(
|
||||||
|
error.raw_os_error(),
|
||||||
|
Some(libc::EADDRNOTAVAIL | libc::EAFNOSUPPORT | libc::ENETUNREACH)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_error(fd: RawFd) -> io::Result<()> {
|
||||||
|
let mut so_error: libc::c_int = 0;
|
||||||
|
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
|
||||||
|
cvt(unsafe {
|
||||||
|
libc::getsockopt(
|
||||||
|
fd,
|
||||||
|
libc::SOL_SOCKET,
|
||||||
|
libc::SO_ERROR,
|
||||||
|
&mut so_error as *mut libc::c_int as *mut c_void,
|
||||||
|
&mut len,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if so_error == 0 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(io::Error::from_raw_os_error(so_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn localhost_v4(addr: SocketAddr) -> SocketAddr {
|
||||||
|
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, addr.port()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cvt(value: libc::c_int) -> io::Result<libc::c_int> {
|
||||||
|
if value < 0 {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cvt_long(value: libc::ssize_t) -> io::Result<libc::ssize_t> {
|
||||||
|
if value < 0 {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,3 +2,10 @@
|
|||||||
|
|
||||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
pub mod linux;
|
pub mod linux;
|
||||||
|
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||||
|
pub mod macos;
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
|
pub use linux as current;
|
||||||
|
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||||
|
pub use macos as current;
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ fn bench_scroll_list(c: &mut Criterion) {
|
|||||||
let viewport_height = 640.0;
|
let viewport_height = 640.0;
|
||||||
let n = 500;
|
let n = 500;
|
||||||
|
|
||||||
let mut scroll_box = Element::scroll_box(0.0).width(400.0).height(viewport_height);
|
let mut scroll_box = Element::scroll_box(0.0)
|
||||||
|
.width(400.0)
|
||||||
|
.height(viewport_height);
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
scroll_box = scroll_box.child(
|
scroll_box = scroll_box.child(
|
||||||
Element::new()
|
Element::new()
|
||||||
@@ -90,5 +92,10 @@ fn bench_scroll_list(c: &mut Criterion) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
criterion_group!(benches, bench_static_tree, bench_single_change, bench_scroll_list);
|
criterion_group!(
|
||||||
|
benches,
|
||||||
|
bench_static_tree,
|
||||||
|
bench_single_change,
|
||||||
|
bench_scroll_list
|
||||||
|
);
|
||||||
criterion_main!(benches);
|
criterion_main!(benches);
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ fn log_platform_event(event: &PlatformEvent) {
|
|||||||
"clipboard text received"
|
"clipboard text received"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
PlatformEvent::PrimarySelectionText { window_id, text } => {
|
PlatformEvent::PrimarySelectionText { window_id, text } => {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
event = "primary_selection_text",
|
event = "primary_selection_text",
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ use crate::scene::{
|
|||||||
UiSize,
|
UiSize,
|
||||||
};
|
};
|
||||||
use crate::text::TextSystem;
|
use crate::text::TextSystem;
|
||||||
use crate::tree::{AlignItems, CursorIcon, Edges, Element, ElementId, FlexDirection, ImageNode, Style};
|
use crate::tree::{
|
||||||
|
AlignItems, CursorIcon, Edges, Element, ElementId, FlexDirection, ImageNode, Style,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn layout_scene(version: u64, logical_size: UiSize, root: &Element) -> SceneSnapshot {
|
pub fn layout_scene(version: u64, logical_size: UiSize, root: &Element) -> SceneSnapshot {
|
||||||
let mut text_system = TextSystem::new();
|
let mut text_system = TextSystem::new();
|
||||||
@@ -34,6 +36,35 @@ pub struct LayoutSnapshot {
|
|||||||
pub root_min_size: UiSize,
|
pub root_min_size: UiSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
|
pub struct LayoutFrameStats {
|
||||||
|
pub total_ms: f64,
|
||||||
|
pub nodes: usize,
|
||||||
|
pub text_nodes: usize,
|
||||||
|
pub container_nodes: usize,
|
||||||
|
pub background_quads: usize,
|
||||||
|
pub intrinsic_calls: usize,
|
||||||
|
pub intrinsic_size_calls: usize,
|
||||||
|
pub intrinsic_text_calls: usize,
|
||||||
|
pub intrinsic_container_calls: usize,
|
||||||
|
pub intrinsic_ms: f64,
|
||||||
|
pub text_prepare_calls: usize,
|
||||||
|
pub text_prepare_ms: f64,
|
||||||
|
pub viewport_culled: usize,
|
||||||
|
pub layout_cache_hits: usize,
|
||||||
|
pub layout_cache_misses: usize,
|
||||||
|
pub intrinsic_cache_hits: usize,
|
||||||
|
pub text_requests: u32,
|
||||||
|
pub text_cache_hits: u32,
|
||||||
|
pub text_cache_misses: u32,
|
||||||
|
pub text_output_glyphs: u32,
|
||||||
|
pub text_family_resolve_ms: f64,
|
||||||
|
pub text_buffer_build_ms: f64,
|
||||||
|
pub text_glyph_collect_ms: f64,
|
||||||
|
pub text_miss_ms: f64,
|
||||||
|
pub scene_items: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
pub struct LayoutPath(Vec<u32>);
|
pub struct LayoutPath(Vec<u32>);
|
||||||
|
|
||||||
@@ -192,6 +223,16 @@ pub fn layout_snapshot_with_cache(
|
|||||||
text_system: &mut TextSystem,
|
text_system: &mut TextSystem,
|
||||||
layout_cache: &mut LayoutCache,
|
layout_cache: &mut LayoutCache,
|
||||||
) -> LayoutSnapshot {
|
) -> LayoutSnapshot {
|
||||||
|
layout_snapshot_with_cache_and_stats(version, logical_size, root, text_system, layout_cache).0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn layout_snapshot_with_cache_and_stats(
|
||||||
|
version: u64,
|
||||||
|
logical_size: UiSize,
|
||||||
|
root: &Element,
|
||||||
|
text_system: &mut TextSystem,
|
||||||
|
layout_cache: &mut LayoutCache,
|
||||||
|
) -> (LayoutSnapshot, LayoutFrameStats) {
|
||||||
let layout_started = Instant::now();
|
let layout_started = Instant::now();
|
||||||
let perf_enabled = tracing::enabled!(target: "ruin_ui::layout_perf", tracing::Level::DEBUG);
|
let perf_enabled = tracing::enabled!(target: "ruin_ui::layout_perf", tracing::Level::DEBUG);
|
||||||
let mut perf_stats = LayoutPerfStats::new(perf_enabled);
|
let mut perf_stats = LayoutPerfStats::new(perf_enabled);
|
||||||
@@ -213,48 +254,78 @@ pub fn layout_snapshot_with_cache(
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
let text_stats = text_system.take_frame_stats();
|
let text_stats = text_system.take_frame_stats();
|
||||||
|
let stats = LayoutFrameStats {
|
||||||
|
total_ms: layout_started.elapsed().as_secs_f64() * 1_000.0,
|
||||||
|
nodes: perf_stats.nodes,
|
||||||
|
text_nodes: perf_stats.text_nodes,
|
||||||
|
container_nodes: perf_stats.container_nodes,
|
||||||
|
background_quads: perf_stats.background_quads,
|
||||||
|
intrinsic_calls: perf_stats.intrinsic_calls,
|
||||||
|
intrinsic_size_calls: perf_stats.intrinsic_size_calls,
|
||||||
|
intrinsic_text_calls: perf_stats.intrinsic_text_calls,
|
||||||
|
intrinsic_container_calls: perf_stats.intrinsic_container_calls,
|
||||||
|
intrinsic_ms: perf_stats.intrinsic_ms,
|
||||||
|
text_prepare_calls: perf_stats.text_prepare_calls,
|
||||||
|
text_prepare_ms: perf_stats.text_prepare_ms,
|
||||||
|
viewport_culled: perf_stats.viewport_culled,
|
||||||
|
layout_cache_hits: perf_stats.layout_cache_hits,
|
||||||
|
layout_cache_misses: perf_stats.layout_cache_misses,
|
||||||
|
intrinsic_cache_hits: perf_stats.intrinsic_cache_hits,
|
||||||
|
text_requests: text_stats.requests,
|
||||||
|
text_cache_hits: text_stats.cache_hits,
|
||||||
|
text_cache_misses: text_stats.cache_misses,
|
||||||
|
text_output_glyphs: text_stats.output_glyphs,
|
||||||
|
text_family_resolve_ms: text_stats.family_resolve_ms,
|
||||||
|
text_buffer_build_ms: text_stats.buffer_build_ms,
|
||||||
|
text_glyph_collect_ms: text_stats.glyph_collect_ms,
|
||||||
|
text_miss_ms: text_stats.miss_ms,
|
||||||
|
scene_items: scene.items.len(),
|
||||||
|
};
|
||||||
if perf_stats.enabled {
|
if perf_stats.enabled {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
target: "ruin_ui::layout_perf",
|
target: "ruin_ui::layout_perf",
|
||||||
scene_version = version,
|
scene_version = version,
|
||||||
width = logical_size.width,
|
width = logical_size.width,
|
||||||
height = logical_size.height,
|
height = logical_size.height,
|
||||||
total_ms = layout_started.elapsed().as_secs_f64() * 1_000.0,
|
total_ms = stats.total_ms,
|
||||||
nodes = perf_stats.nodes,
|
nodes = stats.nodes,
|
||||||
text_nodes = perf_stats.text_nodes,
|
text_nodes = stats.text_nodes,
|
||||||
container_nodes = perf_stats.container_nodes,
|
container_nodes = stats.container_nodes,
|
||||||
background_quads = perf_stats.background_quads,
|
background_quads = stats.background_quads,
|
||||||
intrinsic_calls = perf_stats.intrinsic_calls,
|
intrinsic_calls = stats.intrinsic_calls,
|
||||||
intrinsic_size_calls = perf_stats.intrinsic_size_calls,
|
intrinsic_size_calls = stats.intrinsic_size_calls,
|
||||||
intrinsic_text_calls = perf_stats.intrinsic_text_calls,
|
intrinsic_text_calls = stats.intrinsic_text_calls,
|
||||||
intrinsic_container_calls = perf_stats.intrinsic_container_calls,
|
intrinsic_container_calls = stats.intrinsic_container_calls,
|
||||||
intrinsic_ms = perf_stats.intrinsic_ms,
|
intrinsic_ms = stats.intrinsic_ms,
|
||||||
text_prepare_calls = perf_stats.text_prepare_calls,
|
text_prepare_calls = stats.text_prepare_calls,
|
||||||
text_prepare_ms = perf_stats.text_prepare_ms,
|
text_prepare_ms = stats.text_prepare_ms,
|
||||||
viewport_culled = perf_stats.viewport_culled,
|
viewport_culled = stats.viewport_culled,
|
||||||
layout_cache_hits = perf_stats.layout_cache_hits,
|
layout_cache_hits = stats.layout_cache_hits,
|
||||||
layout_cache_misses = perf_stats.layout_cache_misses,
|
layout_cache_misses = stats.layout_cache_misses,
|
||||||
intrinsic_cache_hits = perf_stats.intrinsic_cache_hits,
|
intrinsic_cache_hits = stats.intrinsic_cache_hits,
|
||||||
text_requests = text_stats.requests,
|
text_requests = stats.text_requests,
|
||||||
text_cache_hits = text_stats.cache_hits,
|
text_cache_hits = stats.text_cache_hits,
|
||||||
text_cache_misses = text_stats.cache_misses,
|
text_cache_misses = stats.text_cache_misses,
|
||||||
text_output_glyphs = text_stats.output_glyphs,
|
text_output_glyphs = stats.text_output_glyphs,
|
||||||
text_family_resolve_ms = text_stats.family_resolve_ms,
|
text_family_resolve_ms = stats.text_family_resolve_ms,
|
||||||
text_buffer_build_ms = text_stats.buffer_build_ms,
|
text_buffer_build_ms = stats.text_buffer_build_ms,
|
||||||
text_glyph_collect_ms = text_stats.glyph_collect_ms,
|
text_glyph_collect_ms = stats.text_glyph_collect_ms,
|
||||||
text_miss_ms = text_stats.miss_ms,
|
text_miss_ms = stats.text_miss_ms,
|
||||||
scene_items = scene.items.len(),
|
scene_items = stats.scene_items,
|
||||||
"layout snapshot perf"
|
"layout snapshot perf"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let root_min_size = element_min_size(root);
|
let root_min_size = element_min_size(root);
|
||||||
LayoutSnapshot {
|
(
|
||||||
scene,
|
LayoutSnapshot {
|
||||||
interaction_tree: InteractionTree {
|
scene,
|
||||||
root: interaction_root,
|
interaction_tree: InteractionTree {
|
||||||
|
root: interaction_root,
|
||||||
|
},
|
||||||
|
root_min_size,
|
||||||
},
|
},
|
||||||
root_min_size,
|
stats,
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -278,20 +349,14 @@ fn layout_element(
|
|||||||
&& !rects_overlap(rect, clip)
|
&& !rects_overlap(rect, clip)
|
||||||
{
|
{
|
||||||
perf_stats.viewport_culled += 1;
|
perf_stats.viewport_culled += 1;
|
||||||
return LayoutNode {
|
return layout_interaction_skeleton(
|
||||||
path,
|
element,
|
||||||
element_id: element.id,
|
|
||||||
rect,
|
rect,
|
||||||
corner_radius: 0.0,
|
path,
|
||||||
clip_rect: None,
|
text_system,
|
||||||
pointer_events: element.style.pointer_events,
|
perf_stats,
|
||||||
focusable: element.style.focusable,
|
layout_cache,
|
||||||
cursor: element.style.cursor.unwrap_or(CursorIcon::Default),
|
);
|
||||||
scroll_metrics: None,
|
|
||||||
prepared_image: None,
|
|
||||||
prepared_text: None,
|
|
||||||
children: Vec::new(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Incremental layout cache check.
|
// Incremental layout cache check.
|
||||||
@@ -303,6 +368,7 @@ fn layout_element(
|
|||||||
subtree_hash,
|
subtree_hash,
|
||||||
avail_width_bits: rect.size.width.round() as u32,
|
avail_width_bits: rect.size.width.round() as u32,
|
||||||
avail_height_bits: rect.size.height.round() as u32,
|
avail_height_bits: rect.size.height.round() as u32,
|
||||||
|
clip_rect_bits: cache_clip_rect_bits(rect, clip_rect),
|
||||||
};
|
};
|
||||||
if let Some(cached) = layout_cache.results.get(&cache_key) {
|
if let Some(cached) = layout_cache.results.get(&cache_key) {
|
||||||
let offset = rect.origin;
|
let offset = rect.origin;
|
||||||
@@ -586,13 +652,7 @@ fn layout_element(
|
|||||||
text_system,
|
text_system,
|
||||||
perf_stats,
|
perf_stats,
|
||||||
layout_cache,
|
layout_cache,
|
||||||
// Do NOT propagate clip_rect into children: the scroll-box PushClip already clips
|
clip_rect,
|
||||||
// them visually, and propagating the clip causes the layout cache to bake in
|
|
||||||
// culling results for a specific scroll position. When the scroll changes, the
|
|
||||||
// same cache entry would replay with stale "empty" children that were culled at
|
|
||||||
// the previous offset. Culling applies only at the direct-child level of the
|
|
||||||
// scroll box (which passes Some(viewport_rect) to layout_container_children).
|
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if pushed_clip {
|
if pushed_clip {
|
||||||
@@ -879,25 +939,23 @@ fn layout_container_children(
|
|||||||
// children with no explicit cross-axis style, since flex main size is unknown here.
|
// children with no explicit cross-axis style, since flex main size is unknown here.
|
||||||
// align_self on the child overrides the parent's align_items for this child.
|
// align_self on the child overrides the parent's align_items for this child.
|
||||||
let effective_align = child.style.align_self.unwrap_or(element.style.align_items);
|
let effective_align = child.style.align_self.unwrap_or(element.style.align_items);
|
||||||
let natural_cross =
|
let natural_cross = if effective_align != AlignItems::Stretch
|
||||||
if effective_align != AlignItems::Stretch
|
&& !is_flex
|
||||||
&& !is_flex
|
&& child_cross_size(child, element.style.direction).is_none()
|
||||||
&& child_cross_size(child, element.style.direction).is_none()
|
{
|
||||||
{
|
let offered = match element.style.direction {
|
||||||
let offered = match element.style.direction {
|
FlexDirection::Row => UiSize::new(measured_main, available_cross),
|
||||||
FlexDirection::Row => UiSize::new(measured_main, available_cross),
|
FlexDirection::Column => UiSize::new(available_cross, measured_main),
|
||||||
FlexDirection::Column => UiSize::new(available_cross, measured_main),
|
|
||||||
};
|
|
||||||
let natural =
|
|
||||||
intrinsic_size(child, offered, text_system, perf_stats, layout_cache);
|
|
||||||
Some(
|
|
||||||
cross_axis_size(natural, element.style.direction)
|
|
||||||
.max(min_cross_outer)
|
|
||||||
.min(available_cross.max(min_cross_outer)),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
};
|
||||||
|
let natural = intrinsic_size(child, offered, text_system, perf_stats, layout_cache);
|
||||||
|
Some(
|
||||||
|
cross_axis_size(natural, element.style.direction)
|
||||||
|
.max(min_cross_outer)
|
||||||
|
.min(available_cross.max(min_cross_outer)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
if is_flex {
|
if is_flex {
|
||||||
flex_total += child_flex_weight(child);
|
flex_total += child_flex_weight(child);
|
||||||
@@ -966,6 +1024,256 @@ fn layout_container_children(
|
|||||||
children
|
children
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn layout_interaction_skeleton(
|
||||||
|
element: &Element,
|
||||||
|
rect: Rect,
|
||||||
|
path: LayoutPath,
|
||||||
|
text_system: &mut TextSystem,
|
||||||
|
perf_stats: &mut LayoutPerfStats,
|
||||||
|
layout_cache: &mut LayoutCache,
|
||||||
|
) -> LayoutNode {
|
||||||
|
let cursor = element.style.cursor.unwrap_or_else(|| {
|
||||||
|
if element.text_node().is_some() {
|
||||||
|
CursorIcon::Text
|
||||||
|
} else {
|
||||||
|
CursorIcon::Default
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut interaction = LayoutNode {
|
||||||
|
path,
|
||||||
|
element_id: element.id,
|
||||||
|
rect,
|
||||||
|
corner_radius: uniform_corner_radius(&element.style, rect),
|
||||||
|
clip_rect: None,
|
||||||
|
pointer_events: element.style.pointer_events,
|
||||||
|
focusable: element.style.focusable,
|
||||||
|
cursor,
|
||||||
|
scroll_metrics: None,
|
||||||
|
prepared_image: None,
|
||||||
|
prepared_text: None,
|
||||||
|
children: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
|
||||||
|
return interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(scroll_box) = element.scroll_box_node() {
|
||||||
|
let content = inset_rect(rect, content_insets(&element.style));
|
||||||
|
if content.size.width <= 0.0 || content.size.height <= 0.0 {
|
||||||
|
return interaction;
|
||||||
|
}
|
||||||
|
let gutter_width = scroll_box
|
||||||
|
.scrollbar
|
||||||
|
.gutter_width
|
||||||
|
.max(0.0)
|
||||||
|
.min(content.size.width);
|
||||||
|
let viewport_rect = Rect::new(
|
||||||
|
content.origin.x,
|
||||||
|
content.origin.y,
|
||||||
|
(content.size.width - gutter_width).max(0.0),
|
||||||
|
content.size.height,
|
||||||
|
);
|
||||||
|
interaction.clip_rect = Some(viewport_rect);
|
||||||
|
let content_size = intrinsic_container_content_size(
|
||||||
|
element,
|
||||||
|
UiSize::new(
|
||||||
|
viewport_rect.size.width.max(0.0),
|
||||||
|
viewport_rect.size.height.max(0.0),
|
||||||
|
),
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
layout_cache,
|
||||||
|
);
|
||||||
|
let content_height = content_size.height.max(viewport_rect.size.height);
|
||||||
|
let offset_y = scroll_box
|
||||||
|
.offset_y
|
||||||
|
.max(0.0)
|
||||||
|
.min((content_height - viewport_rect.size.height).max(0.0));
|
||||||
|
interaction.children = layout_interaction_skeleton_children(
|
||||||
|
element,
|
||||||
|
Rect::new(
|
||||||
|
viewport_rect.origin.x,
|
||||||
|
viewport_rect.origin.y - offset_y,
|
||||||
|
viewport_rect.size.width,
|
||||||
|
content_height,
|
||||||
|
),
|
||||||
|
&interaction.path,
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
layout_cache,
|
||||||
|
);
|
||||||
|
let (scrollbar_track, scrollbar_thumb) = scrollbar_geometry(
|
||||||
|
content,
|
||||||
|
scroll_box.scrollbar,
|
||||||
|
content_height,
|
||||||
|
viewport_rect.size.height,
|
||||||
|
offset_y,
|
||||||
|
);
|
||||||
|
interaction.scroll_metrics = Some(ScrollMetrics {
|
||||||
|
viewport_rect,
|
||||||
|
content_height,
|
||||||
|
offset_y,
|
||||||
|
max_offset_y: (content_height - viewport_rect.size.height).max(0.0),
|
||||||
|
scrollbar_track,
|
||||||
|
scrollbar_thumb,
|
||||||
|
});
|
||||||
|
return interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if element.text_node().is_some()
|
||||||
|
|| element.image_node().is_some()
|
||||||
|
|| element.children.is_empty()
|
||||||
|
{
|
||||||
|
return interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = inset_rect(rect, content_insets(&element.style));
|
||||||
|
interaction.children = layout_interaction_skeleton_children(
|
||||||
|
element,
|
||||||
|
content,
|
||||||
|
&interaction.path,
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
layout_cache,
|
||||||
|
);
|
||||||
|
interaction
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_interaction_skeleton_children(
|
||||||
|
element: &Element,
|
||||||
|
content: Rect,
|
||||||
|
path: &LayoutPath,
|
||||||
|
text_system: &mut TextSystem,
|
||||||
|
perf_stats: &mut LayoutPerfStats,
|
||||||
|
layout_cache: &mut LayoutCache,
|
||||||
|
) -> Vec<LayoutNode> {
|
||||||
|
if element.children.is_empty() || content.size.width <= 0.0 || content.size.height <= 0.0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let gap_count = element.children.len().saturating_sub(1) as f32;
|
||||||
|
let total_gap = element.style.gap * gap_count;
|
||||||
|
let available_main =
|
||||||
|
(main_axis_size(content.size, element.style.direction).max(0.0) - total_gap).max(0.0);
|
||||||
|
let available_cross = cross_axis_size(content.size, element.style.direction).max(0.0);
|
||||||
|
|
||||||
|
let mut measured_children = Vec::with_capacity(element.children.len());
|
||||||
|
let mut fixed_total = 0.0;
|
||||||
|
let mut flex_total = 0.0;
|
||||||
|
for child in &element.children {
|
||||||
|
let child_insets = content_insets(&child.style);
|
||||||
|
let main_inset = main_axis_padding(child_insets, element.style.direction);
|
||||||
|
let cross_inset = cross_axis_padding(child_insets, element.style.direction);
|
||||||
|
let min_cross_outer = child_min_cross_size(child, element.style.direction)
|
||||||
|
.map(|c| (c + cross_inset).max(0.0))
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let min_main_outer = child_min_main_size(child, element.style.direction)
|
||||||
|
.map(|m| (m + main_inset).max(0.0))
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let cross = child_cross_size(child, element.style.direction)
|
||||||
|
.map(|c| (c + cross_inset).max(0.0).max(min_cross_outer))
|
||||||
|
.unwrap_or(available_cross.max(min_cross_outer))
|
||||||
|
.min(available_cross.max(min_cross_outer));
|
||||||
|
let explicit_main = child_main_size(child, element.style.direction)
|
||||||
|
.map(|main| (main + main_inset).max(0.0).max(min_main_outer));
|
||||||
|
let is_flex = explicit_main.is_none() && child.style.flex_grow > 0.0;
|
||||||
|
let measured_main = explicit_main.unwrap_or_else(|| {
|
||||||
|
if is_flex {
|
||||||
|
min_main_outer
|
||||||
|
} else {
|
||||||
|
intrinsic_main_size(
|
||||||
|
child,
|
||||||
|
element.style.direction,
|
||||||
|
cross,
|
||||||
|
available_main,
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
layout_cache,
|
||||||
|
)
|
||||||
|
.max(min_main_outer)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let effective_align = child.style.align_self.unwrap_or(element.style.align_items);
|
||||||
|
let natural_cross = if effective_align != AlignItems::Stretch
|
||||||
|
&& !is_flex
|
||||||
|
&& child_cross_size(child, element.style.direction).is_none()
|
||||||
|
{
|
||||||
|
let offered = match element.style.direction {
|
||||||
|
FlexDirection::Row => UiSize::new(measured_main, available_cross),
|
||||||
|
FlexDirection::Column => UiSize::new(available_cross, measured_main),
|
||||||
|
};
|
||||||
|
let natural = intrinsic_size(child, offered, text_system, perf_stats, layout_cache);
|
||||||
|
Some(
|
||||||
|
cross_axis_size(natural, element.style.direction)
|
||||||
|
.max(min_cross_outer)
|
||||||
|
.min(available_cross.max(min_cross_outer)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if is_flex {
|
||||||
|
flex_total += child_flex_weight(child);
|
||||||
|
} else {
|
||||||
|
fixed_total += measured_main;
|
||||||
|
}
|
||||||
|
measured_children.push(MeasuredChild {
|
||||||
|
cross,
|
||||||
|
natural_cross,
|
||||||
|
main: measured_main,
|
||||||
|
is_flex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining_main = (available_main - fixed_total).max(0.0);
|
||||||
|
let mut cursor = main_axis_origin(content, element.style.direction);
|
||||||
|
let mut children = Vec::with_capacity(element.children.len());
|
||||||
|
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
||||||
|
let child_main = if measured.is_flex {
|
||||||
|
let allocated = if flex_total <= 0.0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
remaining_main * (child_flex_weight(child) / flex_total)
|
||||||
|
};
|
||||||
|
let child_insets = content_insets(&child.style);
|
||||||
|
let main_inset = main_axis_padding(child_insets, element.style.direction);
|
||||||
|
let min_main_outer = child_min_main_size(child, element.style.direction)
|
||||||
|
.map(|m| (m + main_inset).max(0.0))
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
allocated.max(min_main_outer)
|
||||||
|
} else {
|
||||||
|
measured.main
|
||||||
|
};
|
||||||
|
let effective_align = child.style.align_self.unwrap_or(element.style.align_items);
|
||||||
|
let actual_cross = measured.natural_cross.unwrap_or(measured.cross);
|
||||||
|
let cross_origin = {
|
||||||
|
let base = cross_axis_origin(content, element.style.direction);
|
||||||
|
match effective_align {
|
||||||
|
AlignItems::Stretch | AlignItems::Start => base,
|
||||||
|
AlignItems::Center => base + (available_cross - actual_cross) * 0.5,
|
||||||
|
AlignItems::End => base + available_cross - actual_cross,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let child_rect = child_rect(
|
||||||
|
cross_origin,
|
||||||
|
element.style.direction,
|
||||||
|
cursor,
|
||||||
|
child_main.max(0.0),
|
||||||
|
actual_cross,
|
||||||
|
);
|
||||||
|
children.push(layout_interaction_skeleton(
|
||||||
|
child,
|
||||||
|
child_rect,
|
||||||
|
path.child(index),
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
layout_cache,
|
||||||
|
));
|
||||||
|
cursor += child_main.max(0.0) + element.style.gap;
|
||||||
|
}
|
||||||
|
children
|
||||||
|
}
|
||||||
|
|
||||||
fn intrinsic_main_size(
|
fn intrinsic_main_size(
|
||||||
child: &Element,
|
child: &Element,
|
||||||
direction: FlexDirection,
|
direction: FlexDirection,
|
||||||
@@ -1436,6 +1744,7 @@ struct LayoutCacheKey {
|
|||||||
subtree_hash: u64,
|
subtree_hash: u64,
|
||||||
avail_width_bits: u32,
|
avail_width_bits: u32,
|
||||||
avail_height_bits: u32,
|
avail_height_bits: u32,
|
||||||
|
clip_rect_bits: Option<(u32, u32, u32, u32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CachedLayout {
|
struct CachedLayout {
|
||||||
@@ -1497,6 +1806,16 @@ fn rects_overlap(a: Rect, b: Rect) -> bool {
|
|||||||
&& a.origin.y + a.size.height > b.origin.y
|
&& a.origin.y + a.size.height > b.origin.y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cache_clip_rect_bits(rect: Rect, clip_rect: Option<Rect>) -> Option<(u32, u32, u32, u32)> {
|
||||||
|
let clip = clip_rect?;
|
||||||
|
Some((
|
||||||
|
(clip.origin.x - rect.origin.x).round().to_bits(),
|
||||||
|
(clip.origin.y - rect.origin.y).round().to_bits(),
|
||||||
|
clip.size.width.round().to_bits(),
|
||||||
|
clip.size.height.round().to_bits(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn prepare_image(image: &ImageNode, rect: Rect, element_id: Option<ElementId>) -> PreparedImage {
|
fn prepare_image(image: &ImageNode, rect: Rect, element_id: Option<ElementId>) -> PreparedImage {
|
||||||
let source_size = image.resource.size();
|
let source_size = image.resource.size();
|
||||||
let source_aspect = if source_size.height > 0.0 {
|
let source_aspect = if source_size.height > 0.0 {
|
||||||
@@ -1929,7 +2248,9 @@ fn child_rect(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{LayoutCache, layout_scene, layout_snapshot, layout_snapshot_with_cache};
|
use super::{
|
||||||
|
LayoutCache, LayoutSnapshot, layout_scene, layout_snapshot, layout_snapshot_with_cache,
|
||||||
|
};
|
||||||
use crate::scene::{Color, DisplayItem, Point, Quad, Rect, UiSize};
|
use crate::scene::{Color, DisplayItem, Point, Quad, Rect, UiSize};
|
||||||
use crate::text::TextSystem;
|
use crate::text::TextSystem;
|
||||||
use crate::text::{TextStyle, TextWrap};
|
use crate::text::{TextStyle, TextWrap};
|
||||||
@@ -2625,4 +2946,142 @@ mod tests {
|
|||||||
"expected >= 4 text items visible, got {text_items}"
|
"expected >= 4 text items visible, got {text_items}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cached_scroll_container_recomputes_visible_children_after_offset_changes() {
|
||||||
|
fn scroll_root(offset_y: f32) -> Element {
|
||||||
|
let mut list = Element::column().gap(0.0);
|
||||||
|
for i in 0..20 {
|
||||||
|
list = list.child(
|
||||||
|
Element::new()
|
||||||
|
.height(40.0)
|
||||||
|
.child(Element::text(format!("item {i:02}"), text_style())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Element::new().child(
|
||||||
|
Element::scroll_box(offset_y)
|
||||||
|
.width(400.0)
|
||||||
|
.height(200.0)
|
||||||
|
.child(list),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible_texts(snapshot: &LayoutSnapshot) -> Vec<&str> {
|
||||||
|
snapshot
|
||||||
|
.scene
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| match item {
|
||||||
|
DisplayItem::Text(text) => Some(text.text.as_ref()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut text_system = TextSystem::new();
|
||||||
|
let mut layout_cache = LayoutCache::new();
|
||||||
|
let size = UiSize::new(400.0, 200.0);
|
||||||
|
|
||||||
|
let first = layout_snapshot_with_cache(
|
||||||
|
1,
|
||||||
|
size,
|
||||||
|
&scroll_root(0.0),
|
||||||
|
&mut text_system,
|
||||||
|
&mut layout_cache,
|
||||||
|
);
|
||||||
|
let second = layout_snapshot_with_cache(
|
||||||
|
2,
|
||||||
|
size,
|
||||||
|
&scroll_root(240.0),
|
||||||
|
&mut text_system,
|
||||||
|
&mut layout_cache,
|
||||||
|
);
|
||||||
|
|
||||||
|
let first_visible = visible_texts(&first);
|
||||||
|
let second_visible = visible_texts(&second);
|
||||||
|
assert!(first_visible.iter().any(|text| text.contains("item 00")));
|
||||||
|
assert!(
|
||||||
|
second_visible.iter().any(|text| text.contains("item 06")),
|
||||||
|
"expected scrolled snapshot to include newly visible rows, got {second_visible:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!second_visible.iter().any(|text| text.contains("item 00")),
|
||||||
|
"expected initial rows to be culled after scrolling, got {second_visible:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn culled_container_keeps_descendant_geometry_for_scroll_into_view() {
|
||||||
|
let target_id = ElementId::new(42);
|
||||||
|
let root = Element::new().child(
|
||||||
|
Element::scroll_box(0.0).width(400.0).height(120.0).child(
|
||||||
|
Element::column()
|
||||||
|
.child(
|
||||||
|
Element::column()
|
||||||
|
.height(160.0)
|
||||||
|
.child(Element::text("visible group", text_style())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Element::column().height(160.0).child(
|
||||||
|
Element::new()
|
||||||
|
.id(target_id)
|
||||||
|
.height(40.0)
|
||||||
|
.child(Element::text("offscreen target", text_style())),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let snapshot = layout_snapshot(1, UiSize::new(400.0, 120.0), &root);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
snapshot
|
||||||
|
.scene
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.all(|item| !matches!(item, DisplayItem::Text(text) if text.text.contains("offscreen target"))),
|
||||||
|
"offscreen target text should still be scene-culled"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
snapshot
|
||||||
|
.interaction_tree
|
||||||
|
.rect_for_element(target_id)
|
||||||
|
.is_some(),
|
||||||
|
"offscreen descendants must keep geometry for widget-ref scrolling"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_culling_keeps_visible_text_selectable_inside_viewport_clip() {
|
||||||
|
let text_id = ElementId::new(77);
|
||||||
|
let root = Element::new().child(
|
||||||
|
Element::scroll_box(120.0).width(360.0).height(120.0).child(
|
||||||
|
Element::paragraph(
|
||||||
|
"line 01\nline 02\nline 03\nline 04\nline 05\nline 06\nline 07\nline 08\nline 09",
|
||||||
|
text_style().with_line_height(20.0),
|
||||||
|
)
|
||||||
|
.id(text_id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let snapshot = layout_snapshot(1, UiSize::new(360.0, 120.0), &root);
|
||||||
|
let text = snapshot
|
||||||
|
.interaction_tree
|
||||||
|
.text_for_element(text_id)
|
||||||
|
.expect("selectable text should remain in the interaction tree");
|
||||||
|
let visible_hit = snapshot
|
||||||
|
.interaction_tree
|
||||||
|
.text_hit_test(Point::new(8.0, 8.0))
|
||||||
|
.expect("visible scrolled text should be selectable");
|
||||||
|
|
||||||
|
assert_eq!(text.element_id, Some(text_id));
|
||||||
|
assert_eq!(visible_hit.target.element_id, Some(text_id));
|
||||||
|
assert!(
|
||||||
|
snapshot
|
||||||
|
.interaction_tree
|
||||||
|
.text_hit_test(Point::new(8.0, 160.0))
|
||||||
|
.is_none(),
|
||||||
|
"text hit testing should still honor the scroll viewport clip"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub(crate) mod trace_targets {
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
pub const PLATFORM: &str = "ruin_ui::platform";
|
pub const PLATFORM: &str = "ruin_ui::platform";
|
||||||
pub const SCENE: &str = "ruin_ui::scene";
|
pub const SCENE: &str = "ruin_ui::scene";
|
||||||
|
#[allow(dead_code)]
|
||||||
pub const TEXT_PERF: &str = "ruin_ui::text_perf";
|
pub const TEXT_PERF: &str = "ruin_ui::text_perf";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,14 +31,15 @@ pub use interaction::{
|
|||||||
};
|
};
|
||||||
pub use keyboard::{KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers};
|
pub use keyboard::{KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers};
|
||||||
pub use layout::{
|
pub use layout::{
|
||||||
HitTarget, InteractionTree, LayoutCache, LayoutNode, LayoutPath, LayoutSnapshot, ScrollMetrics,
|
HitTarget, InteractionTree, LayoutCache, LayoutFrameStats, LayoutNode, LayoutPath,
|
||||||
TextHitTarget, element_min_size, layout_snapshot, layout_snapshot_with_cache,
|
LayoutSnapshot, ScrollMetrics, TextHitTarget, element_min_size, layout_snapshot,
|
||||||
|
layout_snapshot_with_cache, layout_snapshot_with_cache_and_stats,
|
||||||
layout_snapshot_with_text_system,
|
layout_snapshot_with_text_system,
|
||||||
};
|
};
|
||||||
pub use layout::{layout_scene, layout_scene_with_text_system};
|
pub use layout::{layout_scene, layout_scene_with_text_system};
|
||||||
pub use platform::{
|
pub use platform::{
|
||||||
PlatformClosed, PlatformEndpoint, PlatformEvent, PlatformProxy, PlatformRequest,
|
PlatformClosed, PlatformEndpoint, PlatformEvent, PlatformProxy, PlatformRequest,
|
||||||
PlatformRuntime, start_headless,
|
PlatformRuntime, set_command_wake_hook, start_headless,
|
||||||
};
|
};
|
||||||
pub use runtime::{EventStreamClosed, UiRuntime, WindowController};
|
pub use runtime::{EventStreamClosed, UiRuntime, WindowController};
|
||||||
pub use scene::{
|
pub use scene::{
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use std::cell::RefCell;
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
use ruin_runtime::channel::mpsc;
|
use ruin_runtime::channel::mpsc;
|
||||||
use ruin_runtime::{WorkerHandle, queue_future, queue_microtask, spawn_worker};
|
use ruin_runtime::{WorkerHandle, queue_future, queue_microtask, spawn_worker};
|
||||||
@@ -18,6 +18,13 @@ use crate::trace_targets;
|
|||||||
use crate::tree::CursorIcon;
|
use crate::tree::CursorIcon;
|
||||||
use crate::window::{WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate};
|
use crate::window::{WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate};
|
||||||
|
|
||||||
|
type CommandWakeHook = Arc<dyn Fn() + Send + Sync + 'static>;
|
||||||
|
|
||||||
|
fn command_wake_hook() -> &'static Mutex<Option<CommandWakeHook>> {
|
||||||
|
static HOOK: OnceLock<Mutex<Option<CommandWakeHook>>> = OnceLock::new();
|
||||||
|
HOOK.get_or_init(|| Mutex::new(None))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PlatformProxy {
|
pub struct PlatformProxy {
|
||||||
command_tx: mpsc::UnboundedSender<PlatformRequest>,
|
command_tx: mpsc::UnboundedSender<PlatformRequest>,
|
||||||
@@ -32,7 +39,7 @@ pub struct PlatformProxy {
|
|||||||
pub struct PlatformRuntime {
|
pub struct PlatformRuntime {
|
||||||
proxy: PlatformProxy,
|
proxy: PlatformProxy,
|
||||||
events: mpsc::Receiver<PlatformEvent>,
|
events: mpsc::Receiver<PlatformEvent>,
|
||||||
_worker: WorkerHandle,
|
worker: Option<WorkerHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PlatformEndpoint {
|
pub struct PlatformEndpoint {
|
||||||
@@ -73,6 +80,7 @@ pub enum PlatformEvent {
|
|||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
text: String,
|
text: String,
|
||||||
},
|
},
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
PrimarySelectionText {
|
PrimarySelectionText {
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
text: String,
|
text: String,
|
||||||
@@ -118,10 +126,12 @@ pub enum PlatformRequest {
|
|||||||
RequestClipboardText {
|
RequestClipboardText {
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
},
|
},
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
SetPrimarySelectionText {
|
SetPrimarySelectionText {
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
text: String,
|
text: String,
|
||||||
},
|
},
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
RequestPrimarySelectionText {
|
RequestPrimarySelectionText {
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
},
|
},
|
||||||
@@ -203,7 +213,30 @@ impl PlatformRuntime {
|
|||||||
Self {
|
Self {
|
||||||
proxy,
|
proxy,
|
||||||
events: event_rx,
|
events: event_rx,
|
||||||
_worker: worker,
|
worker: Some(worker),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a platform runtime hosted by the current runtime thread.
|
||||||
|
///
|
||||||
|
/// This is useful for backends that must run on a specific host thread
|
||||||
|
/// (for example, AppKit on macOS main thread).
|
||||||
|
pub fn custom_local(start: impl FnOnce(PlatformEndpoint)) -> Self {
|
||||||
|
let (command_tx, command_rx) = mpsc::unbounded_channel::<PlatformRequest>();
|
||||||
|
let (event_tx, event_rx) = mpsc::unbounded_channel::<PlatformEvent>();
|
||||||
|
let proxy = PlatformProxy {
|
||||||
|
command_tx,
|
||||||
|
next_window_id: Arc::new(AtomicU64::new(1)),
|
||||||
|
};
|
||||||
|
start(PlatformEndpoint {
|
||||||
|
commands: command_rx,
|
||||||
|
events: event_tx,
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
proxy,
|
||||||
|
events: event_rx,
|
||||||
|
worker: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +249,7 @@ impl PlatformRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn take_pending_events(&mut self) -> Vec<PlatformEvent> {
|
pub fn take_pending_events(&mut self) -> Vec<PlatformEvent> {
|
||||||
|
let _ = self.worker.as_ref();
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
while let Ok(event) = self.events.try_recv() {
|
while let Ok(event) = self.events.try_recv() {
|
||||||
events.push(event);
|
events.push(event);
|
||||||
@@ -247,6 +281,7 @@ impl PlatformProxy {
|
|||||||
self.send(PlatformRequest::ReplaceScene { window_id, scene })
|
self.send(PlatformRequest::ReplaceScene { window_id, scene })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
pub fn set_primary_selection_text(
|
pub fn set_primary_selection_text(
|
||||||
&self,
|
&self,
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
@@ -258,6 +293,7 @@ impl PlatformProxy {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
pub fn request_primary_selection_text(
|
pub fn request_primary_selection_text(
|
||||||
&self,
|
&self,
|
||||||
window_id: WindowId,
|
window_id: WindowId,
|
||||||
@@ -317,7 +353,20 @@ impl PlatformProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn send(&self, command: PlatformRequest) -> Result<(), PlatformClosed> {
|
fn send(&self, command: PlatformRequest) -> Result<(), PlatformClosed> {
|
||||||
self.command_tx.send(command).map_err(|_| PlatformClosed)
|
self.command_tx.send(command).map_err(|_| PlatformClosed)?;
|
||||||
|
if let Ok(hook) = command_wake_hook().lock()
|
||||||
|
&& let Some(hook) = hook.as_ref().cloned()
|
||||||
|
{
|
||||||
|
hook();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn set_command_wake_hook(hook: Option<CommandWakeHook>) {
|
||||||
|
if let Ok(mut slot) = command_wake_hook().lock() {
|
||||||
|
*slot = hook;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +405,9 @@ pub fn start_headless() -> PlatformRuntime {
|
|||||||
}
|
}
|
||||||
PlatformRequest::SetClipboardText { .. } => {}
|
PlatformRequest::SetClipboardText { .. } => {}
|
||||||
PlatformRequest::RequestClipboardText { .. } => {}
|
PlatformRequest::RequestClipboardText { .. } => {}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
PlatformRequest::SetPrimarySelectionText { .. } => {}
|
PlatformRequest::SetPrimarySelectionText { .. } => {}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
PlatformRequest::RequestPrimarySelectionText { .. } => {}
|
PlatformRequest::RequestPrimarySelectionText { .. } => {}
|
||||||
PlatformRequest::SetCursorIcon { .. } => {}
|
PlatformRequest::SetCursorIcon { .. } => {}
|
||||||
PlatformRequest::EmitCloseRequested { window_id } => {
|
PlatformRequest::EmitCloseRequested { window_id } => {
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ impl WindowController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Copies plain text to the platform primary-selection buffer for this window.
|
/// Copies plain text to the platform primary-selection buffer for this window.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
pub fn set_primary_selection_text(
|
pub fn set_primary_selection_text(
|
||||||
&self,
|
&self,
|
||||||
text: impl Into<String>,
|
text: impl Into<String>,
|
||||||
@@ -128,6 +129,7 @@ impl WindowController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Requests the current plain-text primary selection contents from the platform.
|
/// Requests the current plain-text primary selection contents from the platform.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
pub fn request_primary_selection_text(&self) -> Result<(), PlatformClosed> {
|
pub fn request_primary_selection_text(&self) -> Result<(), PlatformClosed> {
|
||||||
self.proxy.request_primary_selection_text(self.id)
|
self.proxy.request_primary_selection_text(self.id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ pub struct PreparedTextLine {
|
|||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct PreparedText {
|
pub struct PreparedText {
|
||||||
pub element_id: Option<ElementId>,
|
pub element_id: Option<ElementId>,
|
||||||
pub text: String,
|
pub text: Arc<str>,
|
||||||
pub origin: Point,
|
pub origin: Point,
|
||||||
pub bounds: Option<UiSize>,
|
pub bounds: Option<UiSize>,
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
@@ -206,6 +206,8 @@ pub struct PreparedText {
|
|||||||
/// Add `origin` to convert to absolute window coords.
|
/// Add `origin` to convert to absolute window coords.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct TextLayoutData {
|
pub struct TextLayoutData {
|
||||||
|
pub text: Arc<str>,
|
||||||
|
pub cache_key: u64,
|
||||||
pub lines: Vec<PreparedTextLine>,
|
pub lines: Vec<PreparedTextLine>,
|
||||||
pub glyphs: Vec<GlyphInstance>,
|
pub glyphs: Vec<GlyphInstance>,
|
||||||
/// Measured (unclamped) size of the laid-out text.
|
/// Measured (unclamped) size of the laid-out text.
|
||||||
@@ -225,7 +227,6 @@ impl PreparedText {
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn from_layout(
|
pub(crate) fn from_layout(
|
||||||
element_id: Option<ElementId>,
|
element_id: Option<ElementId>,
|
||||||
text: String,
|
|
||||||
origin: Point,
|
origin: Point,
|
||||||
bounds: Option<UiSize>,
|
bounds: Option<UiSize>,
|
||||||
font_size: f32,
|
font_size: f32,
|
||||||
@@ -237,7 +238,7 @@ impl PreparedText {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
element_id,
|
element_id,
|
||||||
text,
|
text: layout.text.clone(),
|
||||||
origin,
|
origin,
|
||||||
bounds,
|
bounds,
|
||||||
font_size,
|
font_size,
|
||||||
@@ -256,6 +257,10 @@ impl PreparedText {
|
|||||||
Arc::as_ptr(&self.layout)
|
Arc::as_ptr(&self.layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn layout_cache_key(&self) -> u64 {
|
||||||
|
self.layout.cache_key
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a monospace `PreparedText` without going through the text system.
|
/// Create a monospace `PreparedText` without going through the text system.
|
||||||
/// Used for testing and low-level terminal-style rendering.
|
/// Used for testing and low-level terminal-style rendering.
|
||||||
pub fn monospace(
|
pub fn monospace(
|
||||||
@@ -265,7 +270,7 @@ impl PreparedText {
|
|||||||
advance: f32,
|
advance: f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let text = text.into();
|
let text: Arc<str> = Arc::from(text.into());
|
||||||
// Glyphs are stored in LOCAL (origin-relative) coordinates.
|
// Glyphs are stored in LOCAL (origin-relative) coordinates.
|
||||||
let mut local_x = 0.0f32;
|
let mut local_x = 0.0f32;
|
||||||
let mut glyphs = Vec::with_capacity(text.chars().count());
|
let mut glyphs = Vec::with_capacity(text.chars().count());
|
||||||
@@ -293,7 +298,7 @@ impl PreparedText {
|
|||||||
let size = UiSize::new(local_x, font_size);
|
let size = UiSize::new(local_x, font_size);
|
||||||
Self {
|
Self {
|
||||||
element_id: None,
|
element_id: None,
|
||||||
text,
|
text: text.clone(),
|
||||||
origin,
|
origin,
|
||||||
bounds: None,
|
bounds: None,
|
||||||
font_size,
|
font_size,
|
||||||
@@ -302,6 +307,8 @@ impl PreparedText {
|
|||||||
selectable: true,
|
selectable: true,
|
||||||
selection_style: TextSelectionStyle::DEFAULT,
|
selection_style: TextSelectionStyle::DEFAULT,
|
||||||
layout: Arc::new(TextLayoutData {
|
layout: Arc::new(TextLayoutData {
|
||||||
|
text: text.clone(),
|
||||||
|
cache_key: monospace_text_cache_key(&text, font_size, advance, color),
|
||||||
lines: vec![line],
|
lines: vec![line],
|
||||||
glyphs,
|
glyphs,
|
||||||
size,
|
size,
|
||||||
@@ -466,7 +473,10 @@ impl PreparedText {
|
|||||||
if range.is_empty() {
|
if range.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for glyph in Arc::make_mut(&mut self.layout).glyphs.iter_mut() {
|
let layout = Arc::make_mut(&mut self.layout);
|
||||||
|
layout.cache_key =
|
||||||
|
selected_text_cache_key(layout.cache_key, range.start, range.end, selected_color);
|
||||||
|
for glyph in layout.glyphs.iter_mut() {
|
||||||
if glyph.text_end > range.start && glyph.text_start < range.end {
|
if glyph.text_end > range.start && glyph.text_start < range.end {
|
||||||
glyph.color = selected_color;
|
glyph.color = selected_color;
|
||||||
}
|
}
|
||||||
@@ -583,6 +593,28 @@ impl PreparedText {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn monospace_text_cache_key(text: &str, font_size: f32, advance: f32, color: Color) -> u64 {
|
||||||
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
|
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
text.hash(&mut hasher);
|
||||||
|
font_size.to_bits().hash(&mut hasher);
|
||||||
|
advance.to_bits().hash(&mut hasher);
|
||||||
|
color.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_text_cache_key(base: u64, start: usize, end: usize, color: Color) -> u64 {
|
||||||
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
|
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
base.hash(&mut hasher);
|
||||||
|
start.hash(&mut hasher);
|
||||||
|
end.hash(&mut hasher);
|
||||||
|
color.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
enum WordClass {
|
enum WordClass {
|
||||||
Word,
|
Word,
|
||||||
@@ -886,6 +918,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let orig_size = text.layout.size;
|
let orig_size = text.layout.size;
|
||||||
text.layout = Arc::new(TextLayoutData {
|
text.layout = Arc::new(TextLayoutData {
|
||||||
|
text: text.text.clone(),
|
||||||
|
cache_key: text.layout.cache_key,
|
||||||
lines,
|
lines,
|
||||||
glyphs,
|
glyphs,
|
||||||
size: orig_size,
|
size: orig_size,
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ use cosmic_text::{
|
|||||||
};
|
};
|
||||||
use fontconfig::Fontconfig;
|
use fontconfig::Fontconfig;
|
||||||
|
|
||||||
use crate::{Color, GlyphInstance, Point, PreparedText, PreparedTextLine, Rect, TextLayoutData,
|
use crate::{
|
||||||
UiSize};
|
Color, GlyphInstance, Point, PreparedText, PreparedTextLine, Rect, TextLayoutData, UiSize,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum TextAlign {
|
pub enum TextAlign {
|
||||||
@@ -268,7 +269,7 @@ pub struct TextSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
pub(crate) struct TextFrameStats {
|
pub struct TextFrameStats {
|
||||||
pub requests: u32,
|
pub requests: u32,
|
||||||
pub cache_hits: u32,
|
pub cache_hits: u32,
|
||||||
pub cache_misses: u32,
|
pub cache_misses: u32,
|
||||||
@@ -340,7 +341,6 @@ impl TextSystem {
|
|||||||
bounds: Option<UiSize>,
|
bounds: Option<UiSize>,
|
||||||
) -> PreparedText {
|
) -> PreparedText {
|
||||||
let bounds = bounds.or(style.bounds);
|
let bounds = bounds.or(style.bounds);
|
||||||
let text = combined_text(spans);
|
|
||||||
// Use the cached Arc directly — glyphs are in local (origin-0) coords.
|
// Use the cached Arc directly — glyphs are in local (origin-0) coords.
|
||||||
let layout_data = self.layout(
|
let layout_data = self.layout(
|
||||||
spans,
|
spans,
|
||||||
@@ -351,7 +351,6 @@ impl TextSystem {
|
|||||||
|
|
||||||
PreparedText::from_layout(
|
PreparedText::from_layout(
|
||||||
None,
|
None,
|
||||||
text,
|
|
||||||
origin,
|
origin,
|
||||||
bounds,
|
bounds,
|
||||||
style.font_size,
|
style.font_size,
|
||||||
@@ -443,7 +442,11 @@ impl TextSystem {
|
|||||||
// For no-wrap + start-aligned text, width doesn't affect shaping.
|
// For no-wrap + start-aligned text, width doesn't affect shaping.
|
||||||
// Pass None so cosmic-text doesn't truncate and the result is
|
// Pass None so cosmic-text doesn't truncate and the result is
|
||||||
// consistent with the width-independent cache key.
|
// consistent with the width-independent cache key.
|
||||||
let effective_width = if width_affects_layout(style) { width } else { None };
|
let effective_width = if width_affects_layout(style) {
|
||||||
|
width
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
borrowed.set_size(effective_width, None);
|
borrowed.set_size(effective_width, None);
|
||||||
let default_attrs = default_attrs_for_style(style, default_family.as_deref());
|
let default_attrs = default_attrs_for_style(style, default_family.as_deref());
|
||||||
if uses_plain_text_fast_path {
|
if uses_plain_text_fast_path {
|
||||||
@@ -471,7 +474,8 @@ impl TextSystem {
|
|||||||
}
|
}
|
||||||
self.frame_stats.buffer_build_ms += buffer_build_started.elapsed().as_secs_f64() * 1_000.0;
|
self.frame_stats.buffer_build_ms += buffer_build_started.elapsed().as_secs_f64() * 1_000.0;
|
||||||
|
|
||||||
let line_starts = line_start_offsets(&combined_text(spans));
|
let text = combined_text(spans);
|
||||||
|
let line_starts = line_start_offsets(&text);
|
||||||
let mut measured_width: f32 = 0.0;
|
let mut measured_width: f32 = 0.0;
|
||||||
let mut measured_height: f32 = 0.0;
|
let mut measured_height: f32 = 0.0;
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
@@ -495,9 +499,7 @@ impl TextSystem {
|
|||||||
// is color-independent. Per-span colors (from glyph.color_opt)
|
// is color-independent. Per-span colors (from glyph.color_opt)
|
||||||
// are stored directly since they are part of the shape key via
|
// are stored directly since they are part of the shape key via
|
||||||
// the spans hash.
|
// the spans hash.
|
||||||
color: glyph
|
color: glyph.color_opt.map_or(Color::SENTINEL, color_from_cosmic),
|
||||||
.color_opt
|
|
||||||
.map_or(Color::SENTINEL, color_from_cosmic),
|
|
||||||
cache_key: Some(physical.cache_key),
|
cache_key: Some(physical.cache_key),
|
||||||
text_start: line_offset + glyph.start,
|
text_start: line_offset + glyph.start,
|
||||||
text_end: line_offset + glyph.end,
|
text_end: line_offset + glyph.end,
|
||||||
@@ -524,6 +526,8 @@ impl TextSystem {
|
|||||||
glyph_collect_started.elapsed().as_secs_f64() * 1_000.0;
|
glyph_collect_started.elapsed().as_secs_f64() * 1_000.0;
|
||||||
|
|
||||||
let layout = Arc::new(TextLayoutData {
|
let layout = Arc::new(TextLayoutData {
|
||||||
|
text: Arc::from(text),
|
||||||
|
cache_key,
|
||||||
lines,
|
lines,
|
||||||
glyphs,
|
glyphs,
|
||||||
size: UiSize::new(measured_width.max(0.0), measured_height.max(0.0)),
|
size: UiSize::new(measured_width.max(0.0), measured_height.max(0.0)),
|
||||||
@@ -696,16 +700,16 @@ fn attrs_for_span<'a>(
|
|||||||
base
|
base
|
||||||
};
|
};
|
||||||
base.weight(match span.weight {
|
base.weight(match span.weight {
|
||||||
TextSpanWeight::Normal => CosmicWeight::NORMAL,
|
TextSpanWeight::Normal => CosmicWeight::NORMAL,
|
||||||
TextSpanWeight::Medium => CosmicWeight::MEDIUM,
|
TextSpanWeight::Medium => CosmicWeight::MEDIUM,
|
||||||
TextSpanWeight::Semibold => CosmicWeight::SEMIBOLD,
|
TextSpanWeight::Semibold => CosmicWeight::SEMIBOLD,
|
||||||
TextSpanWeight::Bold => CosmicWeight::BOLD,
|
TextSpanWeight::Bold => CosmicWeight::BOLD,
|
||||||
})
|
})
|
||||||
.style(match span.slant {
|
.style(match span.slant {
|
||||||
TextSpanSlant::Normal => CosmicStyle::Normal,
|
TextSpanSlant::Normal => CosmicStyle::Normal,
|
||||||
TextSpanSlant::Italic => CosmicStyle::Italic,
|
TextSpanSlant::Italic => CosmicStyle::Italic,
|
||||||
TextSpanSlant::Oblique => CosmicStyle::Oblique,
|
TextSpanSlant::Oblique => CosmicStyle::Oblique,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_cosmic_color(color: Color) -> CosmicColor {
|
fn to_cosmic_color(color: Color) -> CosmicColor {
|
||||||
@@ -930,7 +934,9 @@ impl std::hash::Hash for TextStyle {
|
|||||||
self.line_height.to_bits().hash(state);
|
self.line_height.to_bits().hash(state);
|
||||||
self.color.hash(state);
|
self.color.hash(state);
|
||||||
self.font_family.hash(state);
|
self.font_family.hash(state);
|
||||||
self.bounds.map(|b| (b.width.to_bits(), b.height.to_bits())).hash(state);
|
self.bounds
|
||||||
|
.map(|b| (b.width.to_bits(), b.height.to_bits()))
|
||||||
|
.hash(state);
|
||||||
self.wrap.hash(state);
|
self.wrap.hash(state);
|
||||||
self.align.hash(state);
|
self.align.hash(state);
|
||||||
self.max_lines.hash(state);
|
self.max_lines.hash(state);
|
||||||
@@ -1038,16 +1044,32 @@ mod tests {
|
|||||||
let blue = text_system.prepare("hello", origin, &style_blue);
|
let blue = text_system.prepare("hello", origin, &style_blue);
|
||||||
// Both PreparedTexts must have identical glyph shapes and sentinel colors —
|
// Both PreparedTexts must have identical glyph shapes and sentinel colors —
|
||||||
// the second call hits the shape cache and produces the same layout data.
|
// the second call hits the shape cache and produces the same layout data.
|
||||||
assert_eq!(red.glyphs.len(), blue.glyphs.len(), "glyph count must match");
|
assert_eq!(
|
||||||
|
red.glyphs.len(),
|
||||||
|
blue.glyphs.len(),
|
||||||
|
"glyph count must match"
|
||||||
|
);
|
||||||
for (r, b) in red.glyphs.iter().zip(blue.glyphs.iter()) {
|
for (r, b) in red.glyphs.iter().zip(blue.glyphs.iter()) {
|
||||||
assert_eq!(r.position, b.position, "glyph positions must match");
|
assert_eq!(r.position, b.position, "glyph positions must match");
|
||||||
assert_eq!(r.cache_key, b.cache_key, "glyph cache keys must match");
|
assert_eq!(r.cache_key, b.cache_key, "glyph cache keys must match");
|
||||||
assert_eq!(r.color, Color::SENTINEL, "default-color glyphs must use SENTINEL");
|
assert_eq!(
|
||||||
assert_eq!(b.color, Color::SENTINEL, "default-color glyphs must use SENTINEL");
|
r.color,
|
||||||
|
Color::SENTINEL,
|
||||||
|
"default-color glyphs must use SENTINEL"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
b.color,
|
||||||
|
Color::SENTINEL,
|
||||||
|
"default-color glyphs must use SENTINEL"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// PreparedText::clone() is an Arc clone — O(1).
|
// PreparedText::clone() is an Arc clone — O(1).
|
||||||
let cloned = red.clone();
|
let cloned = red.clone();
|
||||||
assert_eq!(cloned.layout_ptr(), red.layout_ptr(), "clone must share the same Arc");
|
assert_eq!(
|
||||||
|
cloned.layout_ptr(),
|
||||||
|
red.layout_ptr(),
|
||||||
|
"clone must share the same Arc"
|
||||||
|
);
|
||||||
// But they should carry different default colors.
|
// But they should carry different default colors.
|
||||||
assert_eq!(red.default_color, Color::rgb(0xFF, 0x00, 0x00));
|
assert_eq!(red.default_color, Color::rgb(0xFF, 0x00, 0x00));
|
||||||
assert_eq!(blue.default_color, Color::rgb(0x00, 0x00, 0xFF));
|
assert_eq!(blue.default_color, Color::rgb(0x00, 0x00, 0xFF));
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ enum ElementContent {
|
|||||||
pub(crate) struct TextNode {
|
pub(crate) struct TextNode {
|
||||||
pub spans: Vec<TextSpan>,
|
pub spans: Vec<TextSpan>,
|
||||||
pub style: TextStyle,
|
pub style: TextStyle,
|
||||||
|
content_hash: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -281,12 +282,14 @@ impl Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn spans(spans: impl IntoIterator<Item = TextSpan>, style: TextStyle) -> Self {
|
pub fn spans(spans: impl IntoIterator<Item = TextSpan>, style: TextStyle) -> Self {
|
||||||
|
let spans = spans.into_iter().collect::<Vec<_>>();
|
||||||
Self {
|
Self {
|
||||||
id: None,
|
id: None,
|
||||||
style: Style::default(),
|
style: Style::default(),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
content: ElementContent::Text(TextNode {
|
content: ElementContent::Text(TextNode {
|
||||||
spans: spans.into_iter().collect(),
|
content_hash: text_node_content_hash(&spans, &style),
|
||||||
|
spans,
|
||||||
style,
|
style,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -599,8 +602,7 @@ impl Hash for Style {
|
|||||||
|
|
||||||
impl Hash for TextNode {
|
impl Hash for TextNode {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
self.spans.hash(state);
|
self.content_hash.hash(state);
|
||||||
self.style.hash(state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,6 +613,15 @@ impl Hash for ImageNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn text_node_content_hash(spans: &[TextSpan], style: &TextStyle) -> u64 {
|
||||||
|
use std::hash::DefaultHasher;
|
||||||
|
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
spans.hash(&mut hasher);
|
||||||
|
style.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
impl Hash for ScrollBoxNode {
|
impl Hash for ScrollBoxNode {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
self.offset_y.to_bits().hash(state);
|
self.offset_y.to_bits().hash(state);
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ pub enum WindowLifecycle {
|
|||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct WindowConfigured {
|
pub struct WindowConfigured {
|
||||||
|
/// Current logical content size and platform presentation scale.
|
||||||
|
///
|
||||||
|
/// Backends should emit a new configuration when either the logical size or
|
||||||
|
/// scale changes. This lets hosts relayout for size changes while renderers
|
||||||
|
/// can update drawable/text scale when a window moves between displays.
|
||||||
pub actual_inner_size: UiSize,
|
pub actual_inner_size: UiSize,
|
||||||
pub scale_factor: f32,
|
pub scale_factor: f32,
|
||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
|
|||||||
19
lib/ui_platform_macos/Cargo.toml
Normal file
19
lib/ui_platform_macos/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "ruin_ui_platform_macos"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ruin_ui = { path = "../ui" }
|
||||||
|
ruin_ui_renderer_wgpu = { path = "../ui_renderer_wgpu" }
|
||||||
|
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
||||||
|
tracing = "0.1"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
libc = "0.2"
|
||||||
|
objc2 = "0.6.4"
|
||||||
|
objc2-core-foundation = "0.3.2"
|
||||||
|
objc2-foundation = "0.3.2"
|
||||||
|
objc2-quartz-core = "0.3.2"
|
||||||
|
raw-window-handle = "0.6"
|
||||||
|
raw-window-metal = "1.1.0"
|
||||||
2275
lib/ui_platform_macos/src/lib.rs
Normal file
2275
lib/ui_platform_macos/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,14 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libc = "0.2"
|
|
||||||
raw-window-handle = "0.6"
|
|
||||||
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
||||||
ruin_ui = { path = "../ui" }
|
ruin_ui = { path = "../ui" }
|
||||||
ruin_ui_renderer_wgpu = { path = "../ui_renderer_wgpu" }
|
ruin_ui_renderer_wgpu = { path = "../ui_renderer_wgpu" }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
libc = "0.2"
|
||||||
|
raw-window-handle = "0.6"
|
||||||
wayland-backend = { version = "0.3", features = ["client_system"] }
|
wayland-backend = { version = "0.3", features = ["client_system"] }
|
||||||
wayland-client = "0.31"
|
wayland-client = "0.31"
|
||||||
wayland-protocols = { version = "0.32", features = ["client", "staging", "unstable"] }
|
wayland-protocols = { version = "0.32", features = ["client", "staging", "unstable"] }
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![cfg(target_os = "linux")]
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
@@ -40,13 +42,13 @@ use wayland_client::{
|
|||||||
use wayland_protocols::wp::cursor_shape::v1::client::{
|
use wayland_protocols::wp::cursor_shape::v1::client::{
|
||||||
wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
|
wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
|
||||||
};
|
};
|
||||||
use wayland_protocols::xdg::decoration::zv1::client::{
|
|
||||||
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
|
|
||||||
};
|
|
||||||
use wayland_protocols::wp::primary_selection::zv1::client::{
|
use wayland_protocols::wp::primary_selection::zv1::client::{
|
||||||
zwp_primary_selection_device_manager_v1, zwp_primary_selection_device_v1,
|
zwp_primary_selection_device_manager_v1, zwp_primary_selection_device_v1,
|
||||||
zwp_primary_selection_offer_v1, zwp_primary_selection_source_v1,
|
zwp_primary_selection_offer_v1, zwp_primary_selection_source_v1,
|
||||||
};
|
};
|
||||||
|
use wayland_protocols::xdg::decoration::zv1::client::{
|
||||||
|
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
|
||||||
|
};
|
||||||
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
||||||
use xkbcommon::xkb;
|
use xkbcommon::xkb;
|
||||||
|
|
||||||
@@ -242,7 +244,10 @@ fn fixed_size_request_for_spec(spec: &WindowSpec) -> Option<(u32, u32)> {
|
|||||||
.or(spec.min_inner_size)
|
.or(spec.min_inner_size)
|
||||||
.or(spec.max_inner_size)
|
.or(spec.max_inner_size)
|
||||||
.unwrap_or_else(|| UiSize::new(800.0, 500.0));
|
.unwrap_or_else(|| UiSize::new(800.0, 500.0));
|
||||||
Some((size.width.max(1.0).round() as u32, size.height.max(1.0).round() as u32))
|
Some((
|
||||||
|
size.width.max(1.0).round() as u32,
|
||||||
|
size.height.max(1.0).round() as u32,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wayland_cursor_shape(icon: CursorIcon) -> wp_cursor_shape_device_v1::Shape {
|
fn wayland_cursor_shape(icon: CursorIcon) -> wp_cursor_shape_device_v1::Shape {
|
||||||
@@ -1352,7 +1357,9 @@ fn spawn_window_worker(
|
|||||||
"worker observed resized frame"
|
"worker observed resized frame"
|
||||||
);
|
);
|
||||||
state_ref.pending_viewport = Some(current_viewport);
|
state_ref.pending_viewport = Some(current_viewport);
|
||||||
state_ref.pending_viewport_since.get_or_insert_with(Instant::now);
|
state_ref
|
||||||
|
.pending_viewport_since
|
||||||
|
.get_or_insert_with(Instant::now);
|
||||||
// Emit configure BEFORE touching the GPU so the app
|
// Emit configure BEFORE touching the GPU so the app
|
||||||
// thread starts layout immediately in parallel.
|
// thread starts layout immediately in parallel.
|
||||||
// The actual swapchain recreation is deferred to just
|
// The actual swapchain recreation is deferred to just
|
||||||
@@ -1397,7 +1404,9 @@ fn spawn_window_worker(
|
|||||||
"scene size does not match current viewport; holding last buffer"
|
"scene size does not match current viewport; holding last buffer"
|
||||||
);
|
);
|
||||||
state_ref.pending_viewport = Some(current_viewport);
|
state_ref.pending_viewport = Some(current_viewport);
|
||||||
state_ref.pending_viewport_since.get_or_insert_with(Instant::now);
|
state_ref
|
||||||
|
.pending_viewport_since
|
||||||
|
.get_or_insert_with(Instant::now);
|
||||||
// Deadlock prevention: if the app responded to the
|
// Deadlock prevention: if the app responded to the
|
||||||
// configure we sent (scene matches in_flight) but the
|
// configure we sent (scene matches in_flight) but the
|
||||||
// viewport has since moved on, clear the in_flight
|
// viewport has since moved on, clear the in_flight
|
||||||
@@ -1421,7 +1430,9 @@ fn spawn_window_worker(
|
|||||||
// before rendering. Deferring from frame.resized means
|
// before rendering. Deferring from frame.resized means
|
||||||
// rapid configures pay one recreation instead of one
|
// rapid configures pay one recreation instead of one
|
||||||
// per event.
|
// per event.
|
||||||
if let Some((w, h)) = state_ref.pending_swapchain_size.take() {
|
if let Some((w, h)) =
|
||||||
|
state_ref.pending_swapchain_size.take()
|
||||||
|
{
|
||||||
let t_resize = std::time::Instant::now();
|
let t_resize = std::time::Instant::now();
|
||||||
state_ref.renderer.resize(w, h);
|
state_ref.renderer.resize(w, h);
|
||||||
let resize_gpu_us = t_resize.elapsed().as_micros();
|
let resize_gpu_us = t_resize.elapsed().as_micros();
|
||||||
@@ -1438,7 +1449,8 @@ fn spawn_window_worker(
|
|||||||
let t_render = std::time::Instant::now();
|
let t_render = std::time::Instant::now();
|
||||||
match state_ref.renderer.render(scene) {
|
match state_ref.renderer.render(scene) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let render_gpu_us = t_render.elapsed().as_micros();
|
let render_gpu_us =
|
||||||
|
t_render.elapsed().as_micros();
|
||||||
debug!(
|
debug!(
|
||||||
target: "ruin_ui_platform_wayland::perf",
|
target: "ruin_ui_platform_wayland::perf",
|
||||||
window_id = state_ref.window_id.raw(),
|
window_id = state_ref.window_id.raw(),
|
||||||
@@ -1520,10 +1532,10 @@ fn spawn_window_worker(
|
|||||||
{
|
{
|
||||||
let delay =
|
let delay =
|
||||||
repeat.next_at.saturating_duration_since(Instant::now());
|
repeat.next_at.saturating_duration_since(Instant::now());
|
||||||
state_ref.keyboard_repeat_timer = Some(set_timeout(
|
state_ref.keyboard_repeat_timer =
|
||||||
delay,
|
Some(set_timeout(delay, move || {
|
||||||
move || write_wakeup(pipe_write_fd),
|
write_wakeup(pipe_write_fd)
|
||||||
));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ struct PreparedGlyphBitmap {
|
|||||||
|
|
||||||
struct RasterizedText {
|
struct RasterizedText {
|
||||||
origin_offset: Point,
|
origin_offset: Point,
|
||||||
size: UiSize,
|
draw_size: UiSize,
|
||||||
|
texture_size: UiSize,
|
||||||
pixels: Vec<u8>,
|
pixels: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,21 +169,12 @@ struct UploadedAtlasText {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
struct TextTextureKey {
|
struct TextTextureKey {
|
||||||
text: String,
|
layout_key: u64,
|
||||||
bounds: Option<(u32, u32)>,
|
bounds: Option<(i32, i32)>,
|
||||||
|
clip: Option<(i32, i32, i32, i32)>,
|
||||||
font_size_bits: u32,
|
font_size_bits: u32,
|
||||||
line_height_bits: u32,
|
line_height_bits: u32,
|
||||||
color: (u8, u8, u8, u8),
|
color: (u8, u8, u8, u8),
|
||||||
glyphs: Vec<TextTextureGlyph>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
||||||
struct TextTextureGlyph {
|
|
||||||
local_x_bits: u32,
|
|
||||||
local_y_bits: u32,
|
|
||||||
advance_bits: u32,
|
|
||||||
color: (u8, u8, u8, u8),
|
|
||||||
cache_key: Option<CacheKey>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 11] = [
|
const VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 11] = [
|
||||||
@@ -313,6 +305,21 @@ pub enum RenderError {
|
|||||||
Validation,
|
Validation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
|
pub struct RendererFrameStats {
|
||||||
|
pub scene_version: u64,
|
||||||
|
pub scene_items: usize,
|
||||||
|
pub quad_vertices: usize,
|
||||||
|
pub image_batches: usize,
|
||||||
|
pub atlas_text_vertices: u32,
|
||||||
|
pub fallback_text_batches: usize,
|
||||||
|
pub fallback_text_vertices: u32,
|
||||||
|
pub atlas_glyphs_total: u32,
|
||||||
|
pub atlas_glyphs_clip_culled: u32,
|
||||||
|
pub text_prepare_ms: f64,
|
||||||
|
pub render_ms: f64,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct WgpuSceneRenderer {
|
pub struct WgpuSceneRenderer {
|
||||||
surface: wgpu::Surface<'static>,
|
surface: wgpu::Surface<'static>,
|
||||||
device: wgpu::Device,
|
device: wgpu::Device,
|
||||||
@@ -330,7 +337,9 @@ pub struct WgpuSceneRenderer {
|
|||||||
image_cache_order: VecDeque<u64>,
|
image_cache_order: VecDeque<u64>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
glyph_atlas: GlyphAtlas,
|
glyph_atlas: GlyphAtlas,
|
||||||
|
scale_factor: f32,
|
||||||
base_color: Color,
|
base_color: Color,
|
||||||
|
last_frame_stats: RendererFrameStats,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TEXT_CACHE_ENTRIES: usize = 64;
|
const MAX_TEXT_CACHE_ENTRIES: usize = 64;
|
||||||
@@ -377,8 +386,11 @@ impl WgpuSceneRenderer {
|
|||||||
format,
|
format,
|
||||||
width: width.max(1),
|
width: width.max(1),
|
||||||
height: height.max(1),
|
height: height.max(1),
|
||||||
present_mode: wgpu::PresentMode::AutoVsync,
|
// RUIN prioritizes immediate resize/input feedback over frame pacing
|
||||||
desired_maximum_frame_latency: 2,
|
// here. Keep latency to one frame and let callers opt into a more
|
||||||
|
// power-conservative policy once presentation settings are exposed.
|
||||||
|
present_mode: wgpu::PresentMode::AutoNoVsync,
|
||||||
|
desired_maximum_frame_latency: 1,
|
||||||
alpha_mode,
|
alpha_mode,
|
||||||
view_formats: vec![],
|
view_formats: vec![],
|
||||||
};
|
};
|
||||||
@@ -721,7 +733,9 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
image_cache: HashMap::new(),
|
image_cache: HashMap::new(),
|
||||||
image_cache_order: VecDeque::new(),
|
image_cache_order: VecDeque::new(),
|
||||||
glyph_atlas,
|
glyph_atlas,
|
||||||
|
scale_factor: 1.0,
|
||||||
base_color,
|
base_color,
|
||||||
|
last_frame_stats: RendererFrameStats::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,9 +749,37 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
self.surface.configure(&self.device, &self.config);
|
self.surface.configure(&self.device, &self.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_scale_factor(&mut self, scale_factor: f32) {
|
||||||
|
let next = scale_factor.max(1.0);
|
||||||
|
if (self.scale_factor - next).abs() <= f32::EPSILON {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.scale_factor = next;
|
||||||
|
// Text cache is scale-dependent; flush on scale transitions (e.g. display move).
|
||||||
|
self.text_cache.clear();
|
||||||
|
self.text_cache_order.clear();
|
||||||
|
self.glyph_atlas = create_glyph_atlas(
|
||||||
|
&self.device,
|
||||||
|
&self.text_bind_group_layout,
|
||||||
|
&self.text_sampler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last_frame_stats(&self) -> RendererFrameStats {
|
||||||
|
self.last_frame_stats
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render(&mut self, scene: &SceneSnapshot) -> Result<(), RenderError> {
|
pub fn render(&mut self, scene: &SceneSnapshot) -> Result<(), RenderError> {
|
||||||
|
self.render_with_logical_size(scene, scene.logical_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_with_logical_size(
|
||||||
|
&mut self,
|
||||||
|
scene: &SceneSnapshot,
|
||||||
|
logical_size: UiSize,
|
||||||
|
) -> Result<(), RenderError> {
|
||||||
let render_start = std::time::Instant::now();
|
let render_start = std::time::Instant::now();
|
||||||
let vertices = build_vertices(scene);
|
let vertices = build_vertices(scene, logical_size);
|
||||||
let frame = match self.surface.get_current_texture() {
|
let frame = match self.surface.get_current_texture() {
|
||||||
wgpu::CurrentSurfaceTexture::Success(frame)
|
wgpu::CurrentSurfaceTexture::Success(frame)
|
||||||
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
|
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
|
||||||
@@ -761,7 +803,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
let mut uploaded_images = Vec::new();
|
let mut uploaded_images = Vec::new();
|
||||||
let mut uploaded_texts = Vec::new();
|
let mut uploaded_texts = Vec::new();
|
||||||
let mut atlas_perf = AtlasTextPerfStats::default();
|
let mut atlas_perf = AtlasTextPerfStats::default();
|
||||||
let uploaded_atlas_text = self.prepare_uploaded_atlas_text(scene, &mut atlas_perf);
|
let uploaded_atlas_text =
|
||||||
|
self.prepare_uploaded_atlas_text(scene, logical_size, &mut atlas_perf);
|
||||||
let mut clip_stack = Vec::new();
|
let mut clip_stack = Vec::new();
|
||||||
let mut active_clip = ActiveClip::default();
|
let mut active_clip = ActiveClip::default();
|
||||||
for item in &scene.items {
|
for item in &scene.items {
|
||||||
@@ -772,7 +815,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
DisplayItem::PopClip => pop_clip_state(&mut clip_stack, &mut active_clip),
|
DisplayItem::PopClip => pop_clip_state(&mut clip_stack, &mut active_clip),
|
||||||
DisplayItem::Image(image) => {
|
DisplayItem::Image(image) => {
|
||||||
if let Some(uploaded) =
|
if let Some(uploaded) =
|
||||||
self.prepare_uploaded_image(image, scene.logical_size, active_clip)
|
self.prepare_uploaded_image(image, logical_size, active_clip)
|
||||||
{
|
{
|
||||||
uploaded_images.push(uploaded);
|
uploaded_images.push(uploaded);
|
||||||
}
|
}
|
||||||
@@ -782,7 +825,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(uploaded) =
|
if let Some(uploaded) =
|
||||||
self.prepare_uploaded_text(text, scene.logical_size, active_clip)
|
self.prepare_uploaded_text(text, logical_size, active_clip)
|
||||||
{
|
{
|
||||||
uploaded_texts.push(uploaded);
|
uploaded_texts.push(uploaded);
|
||||||
}
|
}
|
||||||
@@ -853,39 +896,60 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
}
|
}
|
||||||
self.queue.submit([encoder.finish()]);
|
self.queue.submit([encoder.finish()]);
|
||||||
frame.present();
|
frame.present();
|
||||||
trace!(
|
self.last_frame_stats = RendererFrameStats {
|
||||||
target: "ruin_ui_renderer_wgpu::perf",
|
scene_version: scene.version,
|
||||||
scene_version = scene.version,
|
scene_items: scene.items.len(),
|
||||||
quad_vertices = vertices.len(),
|
quad_vertices: vertices.len(),
|
||||||
image_batches = uploaded_images.len(),
|
image_batches: uploaded_images.len(),
|
||||||
atlas_text_vertices = uploaded_atlas_text
|
atlas_text_vertices: uploaded_atlas_text
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(0_u32, |text| text.vertex_count),
|
.map_or(0_u32, |text| text.vertex_count),
|
||||||
fallback_text_batches = uploaded_texts.len(),
|
fallback_text_batches: uploaded_texts.len(),
|
||||||
atlas_glyphs_total = atlas_perf.glyphs_total,
|
fallback_text_vertices: uploaded_texts.iter().map(|text| text.vertex_count).sum(),
|
||||||
atlas_glyphs_clip_culled = atlas_perf.glyphs_clip_culled,
|
atlas_glyphs_total: atlas_perf.glyphs_total,
|
||||||
text_prepare_ms = text_prepare_ms,
|
atlas_glyphs_clip_culled: atlas_perf.glyphs_clip_culled,
|
||||||
render_ms = render_start.elapsed().as_secs_f64() * 1_000.0,
|
text_prepare_ms,
|
||||||
|
render_ms: render_start.elapsed().as_secs_f64() * 1_000.0,
|
||||||
|
};
|
||||||
|
trace!(
|
||||||
|
target: "ruin_ui_renderer_wgpu::perf",
|
||||||
|
scene_version = self.last_frame_stats.scene_version,
|
||||||
|
scene_items = self.last_frame_stats.scene_items,
|
||||||
|
quad_vertices = self.last_frame_stats.quad_vertices,
|
||||||
|
image_batches = self.last_frame_stats.image_batches,
|
||||||
|
atlas_text_vertices = self.last_frame_stats.atlas_text_vertices,
|
||||||
|
fallback_text_batches = self.last_frame_stats.fallback_text_batches,
|
||||||
|
fallback_text_vertices = self.last_frame_stats.fallback_text_vertices,
|
||||||
|
atlas_glyphs_total = self.last_frame_stats.atlas_glyphs_total,
|
||||||
|
atlas_glyphs_clip_culled = self.last_frame_stats.atlas_glyphs_clip_culled,
|
||||||
|
text_prepare_ms = self.last_frame_stats.text_prepare_ms,
|
||||||
|
render_ms = self.last_frame_stats.render_ms,
|
||||||
"rendered scene"
|
"rendered scene"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rasterize_text(&mut self, text: &PreparedText) -> Option<RasterizedText> {
|
fn rasterize_text(
|
||||||
|
&mut self,
|
||||||
|
text: &PreparedText,
|
||||||
|
raster_clip: Option<Rect>,
|
||||||
|
) -> Option<RasterizedText> {
|
||||||
if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
|
if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
|
||||||
return self.rasterize_prepared_text(text);
|
return self.rasterize_prepared_text(text, raster_clip, self.scale_factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
let glyphs = self.collect_glyph_bitmaps(text);
|
let clip = raster_clip.map(|clip| scale_rect_to_pixel_rect(clip, self.scale_factor));
|
||||||
let clip = match text.bounds {
|
let glyphs = self.collect_glyph_bitmaps(text, clip);
|
||||||
Some(bounds) => PixelRect {
|
let clip = clip
|
||||||
left: 0,
|
.or_else(|| {
|
||||||
top: 0,
|
text.bounds.map(|bounds| PixelRect {
|
||||||
right: bounds.width.ceil() as i32,
|
left: 0,
|
||||||
bottom: bounds.height.ceil() as i32,
|
top: 0,
|
||||||
},
|
right: (bounds.width * self.scale_factor).ceil() as i32,
|
||||||
None => glyph_union(&glyphs)?,
|
bottom: (bounds.height * self.scale_factor).ceil() as i32,
|
||||||
};
|
})
|
||||||
|
})
|
||||||
|
.or_else(|| glyph_union(&glyphs))?;
|
||||||
if clip.width() == 0 || clip.height() == 0 {
|
if clip.width() == 0 || clip.height() == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -896,23 +960,38 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Some(RasterizedText {
|
Some(RasterizedText {
|
||||||
origin_offset: Point::new(clip.left as f32, clip.top as f32),
|
origin_offset: Point::new(
|
||||||
size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
clip.left as f32 / self.scale_factor,
|
||||||
|
clip.top as f32 / self.scale_factor,
|
||||||
|
),
|
||||||
|
draw_size: UiSize::new(
|
||||||
|
clip.width() as f32 / self.scale_factor,
|
||||||
|
clip.height() as f32 / self.scale_factor,
|
||||||
|
),
|
||||||
|
texture_size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||||
pixels,
|
pixels,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rasterize_prepared_text(&mut self, text: &PreparedText) -> Option<RasterizedText> {
|
fn rasterize_prepared_text(
|
||||||
let glyphs = self.collect_prepared_glyphs(text);
|
&mut self,
|
||||||
let clip = match text.bounds {
|
text: &PreparedText,
|
||||||
Some(bounds) => PixelRect {
|
raster_clip: Option<Rect>,
|
||||||
left: 0,
|
scale_factor: f32,
|
||||||
top: 0,
|
) -> Option<RasterizedText> {
|
||||||
right: bounds.width.ceil() as i32,
|
let clip = raster_clip.map(|clip| scale_rect_to_pixel_rect(clip, scale_factor));
|
||||||
bottom: bounds.height.ceil() as i32,
|
let glyphs = self.collect_prepared_glyphs(text, raster_clip, clip, scale_factor);
|
||||||
},
|
let clip = raster_clip
|
||||||
None => glyph_union_prepared(&glyphs)?,
|
.map(|clip| scale_rect_to_pixel_rect(clip, scale_factor))
|
||||||
};
|
.or_else(|| {
|
||||||
|
text.bounds.map(|bounds| PixelRect {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: (bounds.width * scale_factor).ceil() as i32,
|
||||||
|
bottom: (bounds.height * scale_factor).ceil() as i32,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.or_else(|| glyph_union_prepared(&glyphs))?;
|
||||||
if clip.width() == 0 || clip.height() == 0 {
|
if clip.width() == 0 || clip.height() == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -930,16 +1009,41 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Some(RasterizedText {
|
Some(RasterizedText {
|
||||||
origin_offset: Point::new(clip.left as f32, clip.top as f32),
|
origin_offset: Point::new(
|
||||||
size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
clip.left as f32 / scale_factor,
|
||||||
|
clip.top as f32 / scale_factor,
|
||||||
|
),
|
||||||
|
draw_size: UiSize::new(
|
||||||
|
clip.width() as f32 / scale_factor,
|
||||||
|
clip.height() as f32 / scale_factor,
|
||||||
|
),
|
||||||
|
texture_size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||||
pixels,
|
pixels,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_prepared_glyphs(&mut self, text: &PreparedText) -> Vec<PreparedGlyphBitmap> {
|
fn collect_prepared_glyphs(
|
||||||
let mut glyphs = Vec::with_capacity(text.glyphs.len());
|
&mut self,
|
||||||
for glyph in &text.glyphs {
|
text: &PreparedText,
|
||||||
let Some(cache_key) = glyph.cache_key else {
|
logical_clip: Option<Rect>,
|
||||||
|
pixel_clip: Option<PixelRect>,
|
||||||
|
scale_factor: f32,
|
||||||
|
) -> Vec<PreparedGlyphBitmap> {
|
||||||
|
let absolute_clip = logical_clip.map(|clip| {
|
||||||
|
Rect::new(
|
||||||
|
text.origin.x + clip.origin.x,
|
||||||
|
text.origin.y + clip.origin.y,
|
||||||
|
clip.size.width,
|
||||||
|
clip.size.height,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let visible_glyphs = clip_visible_glyphs(text, absolute_clip);
|
||||||
|
let mut glyphs = Vec::with_capacity(visible_glyphs.len());
|
||||||
|
for glyph in visible_glyphs {
|
||||||
|
let Some(cache_key) = glyph
|
||||||
|
.cache_key
|
||||||
|
.map(|cache_key| scale_glyph_cache_key(cache_key, scale_factor))
|
||||||
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some(image) = self
|
let Some(image) = self
|
||||||
@@ -956,15 +1060,19 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Glyph positions are LOCAL (origin-relative); no subtraction needed.
|
// Glyph positions are LOCAL (origin-relative); no subtraction needed.
|
||||||
let local_x = glyph.position.x.round() as i32;
|
let local_x = (glyph.position.x * scale_factor).round() as i32;
|
||||||
let local_y = glyph.position.y.round() as i32;
|
let local_y = (glyph.position.y * scale_factor).round() as i32;
|
||||||
|
let rect = PixelRect {
|
||||||
|
left: local_x + image.placement.left,
|
||||||
|
top: local_y - image.placement.top,
|
||||||
|
right: local_x + image.placement.left + width,
|
||||||
|
bottom: local_y - image.placement.top + height,
|
||||||
|
};
|
||||||
|
if pixel_clip.is_some_and(|clip| !pixel_rects_intersect(rect, clip)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
glyphs.push(PreparedGlyphBitmap {
|
glyphs.push(PreparedGlyphBitmap {
|
||||||
rect: PixelRect {
|
rect,
|
||||||
left: local_x + image.placement.left,
|
|
||||||
top: local_y - image.placement.top,
|
|
||||||
right: local_x + image.placement.left + width,
|
|
||||||
bottom: local_y - image.placement.top + height,
|
|
||||||
},
|
|
||||||
cache_key,
|
cache_key,
|
||||||
color: resolve_glyph_color(glyph.color, text.default_color),
|
color: resolve_glyph_color(glyph.color, text.default_color),
|
||||||
});
|
});
|
||||||
@@ -972,19 +1080,33 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
glyphs
|
glyphs
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_glyph_bitmaps(&mut self, text: &PreparedText) -> Vec<GlyphBitmap> {
|
fn collect_glyph_bitmaps(
|
||||||
let mut buffer = Buffer::new_empty(Metrics::new(text.font_size, text.line_height));
|
&mut self,
|
||||||
|
text: &PreparedText,
|
||||||
|
raster_clip: Option<PixelRect>,
|
||||||
|
) -> Vec<GlyphBitmap> {
|
||||||
|
let mut buffer = Buffer::new_empty(Metrics::new(
|
||||||
|
text.font_size * self.scale_factor,
|
||||||
|
text.line_height * self.scale_factor,
|
||||||
|
));
|
||||||
{
|
{
|
||||||
let mut borrowed = buffer.borrow_with(&mut self.font_system);
|
let mut borrowed = buffer.borrow_with(&mut self.font_system);
|
||||||
borrowed.set_size(
|
borrowed.set_size(
|
||||||
text.bounds.map(|bounds| bounds.width),
|
text.bounds.map(|bounds| bounds.width * self.scale_factor),
|
||||||
text.bounds.map(|bounds| bounds.height),
|
text.bounds.map(|bounds| bounds.height * self.scale_factor),
|
||||||
);
|
);
|
||||||
borrowed.set_text(&text.text, &Attrs::new(), Shaping::Advanced, None);
|
borrowed.set_text(&text.text, &Attrs::new(), Shaping::Advanced, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut glyphs = Vec::new();
|
let mut glyphs = Vec::new();
|
||||||
for run in buffer.layout_runs() {
|
for run in buffer.layout_runs() {
|
||||||
|
if let Some(clip) = raster_clip {
|
||||||
|
let line_top = run.line_top.floor() as i32;
|
||||||
|
let line_bottom = (run.line_top + run.line_height).ceil() as i32;
|
||||||
|
if line_bottom < clip.top || line_top > clip.bottom {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
for glyph in run.glyphs {
|
for glyph in run.glyphs {
|
||||||
let physical = glyph.physical((0.0, run.line_y), 1.0);
|
let physical = glyph.physical((0.0, run.line_y), 1.0);
|
||||||
let Some(image) = self
|
let Some(image) = self
|
||||||
@@ -999,13 +1121,17 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
if width == 0 || height == 0 {
|
if width == 0 || height == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let rect = PixelRect {
|
||||||
|
left: physical.x + image.placement.left,
|
||||||
|
top: physical.y - image.placement.top,
|
||||||
|
right: physical.x + image.placement.left + width,
|
||||||
|
bottom: physical.y - image.placement.top + height,
|
||||||
|
};
|
||||||
|
if raster_clip.is_some_and(|clip| !pixel_rects_intersect(rect, clip)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
glyphs.push(GlyphBitmap {
|
glyphs.push(GlyphBitmap {
|
||||||
rect: PixelRect {
|
rect,
|
||||||
left: physical.x + image.placement.left,
|
|
||||||
top: physical.y - image.placement.top,
|
|
||||||
right: physical.x + image.placement.left + width,
|
|
||||||
bottom: physical.y - image.placement.top + height,
|
|
||||||
},
|
|
||||||
content: image.content,
|
content: image.content,
|
||||||
color: glyph
|
color: glyph
|
||||||
.color_opt
|
.color_opt
|
||||||
@@ -1020,6 +1146,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
fn prepare_uploaded_atlas_text(
|
fn prepare_uploaded_atlas_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
scene: &SceneSnapshot,
|
scene: &SceneSnapshot,
|
||||||
|
logical_size: UiSize,
|
||||||
perf: &mut AtlasTextPerfStats,
|
perf: &mut AtlasTextPerfStats,
|
||||||
) -> Option<UploadedAtlasText> {
|
) -> Option<UploadedAtlasText> {
|
||||||
let mut vertices = Vec::new();
|
let mut vertices = Vec::new();
|
||||||
@@ -1053,7 +1180,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
if active_clip.rect_active && logical_clip_rect.is_none() {
|
if active_clip.rect_active && logical_clip_rect.is_none() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let clip_rect = logical_clip_rect.map(rect_to_pixel_rect);
|
let clip_rect = logical_clip_rect
|
||||||
|
.map(|rect| scale_rect_to_pixel_rect(rect, self.scale_factor));
|
||||||
|
|
||||||
// Only iterate glyphs whose lines fall within the clip rect.
|
// Only iterate glyphs whose lines fall within the clip rect.
|
||||||
// For large text nodes (e.g. full Cargo.lock) this reduces
|
// For large text nodes (e.g. full Cargo.lock) this reduces
|
||||||
@@ -1064,7 +1192,10 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
perf.glyphs_clip_culled += (all_count - visible.len()) as u32;
|
perf.glyphs_clip_culled += (all_count - visible.len()) as u32;
|
||||||
|
|
||||||
for glyph in visible {
|
for glyph in visible {
|
||||||
let Some(cache_key) = glyph.cache_key else {
|
let Some(cache_key) = glyph
|
||||||
|
.cache_key
|
||||||
|
.map(|cache_key| scale_glyph_cache_key(cache_key, self.scale_factor))
|
||||||
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let resolved_color = resolve_glyph_color(glyph.color, text.default_color);
|
let resolved_color = resolve_glyph_color(glyph.color, text.default_color);
|
||||||
@@ -1073,9 +1204,12 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Glyph positions are LOCAL; add text.origin for absolute screen coords.
|
// Atlas glyphs are rasterized in device pixels while scene geometry remains
|
||||||
let abs_x = (text.origin.x + glyph.position.x).round() as i32;
|
// logical. Build and clip in device space, then convert back for vertices.
|
||||||
let abs_y = (text.origin.y + glyph.position.y).round() as i32;
|
let abs_x =
|
||||||
|
((text.origin.x + glyph.position.x) * self.scale_factor).round() as i32;
|
||||||
|
let abs_y =
|
||||||
|
((text.origin.y + glyph.position.y) * self.scale_factor).round() as i32;
|
||||||
let glyph_rect = PixelRect {
|
let glyph_rect = PixelRect {
|
||||||
left: abs_x + atlas_glyph.placement_left,
|
left: abs_x + atlas_glyph.placement_left,
|
||||||
top: abs_y - atlas_glyph.placement_top,
|
top: abs_y - atlas_glyph.placement_top,
|
||||||
@@ -1090,9 +1224,12 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
glyph_rect,
|
glyph_rect,
|
||||||
atlas_glyph.atlas_rect,
|
atlas_glyph.atlas_rect,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
scene.logical_size,
|
GlyphVertexContext {
|
||||||
resolved_color,
|
logical_size,
|
||||||
active_clip,
|
scale_factor: self.scale_factor,
|
||||||
|
color: resolved_color,
|
||||||
|
clip: active_clip,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1263,9 +1400,10 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
logical_size: UiSize,
|
logical_size: UiSize,
|
||||||
clip: ActiveClip,
|
clip: ActiveClip,
|
||||||
) -> Option<UploadedText> {
|
) -> Option<UploadedText> {
|
||||||
let key = text_texture_key(text);
|
let raster_clip = text_raster_clip(text, clip, logical_size)?;
|
||||||
|
let key = text_texture_key(text, raster_clip, self.scale_factor);
|
||||||
if !self.text_cache.contains_key(&key) {
|
if !self.text_cache.contains_key(&key) {
|
||||||
let rasterized = self.rasterize_text(text)?;
|
let rasterized = self.rasterize_text(text, raster_clip)?;
|
||||||
let cached = self.create_cached_text_texture(&rasterized);
|
let cached = self.create_cached_text_texture(&rasterized);
|
||||||
self.text_cache.insert(key.clone(), cached);
|
self.text_cache.insert(key.clone(), cached);
|
||||||
}
|
}
|
||||||
@@ -1333,8 +1471,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
|
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some("ruin-ui-renderer-wgpu-texture"),
|
label: Some("ruin-ui-renderer-wgpu-texture"),
|
||||||
size: wgpu::Extent3d {
|
size: wgpu::Extent3d {
|
||||||
width: text.size.width as u32,
|
width: text.texture_size.width as u32,
|
||||||
height: text.size.height as u32,
|
height: text.texture_size.height as u32,
|
||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
},
|
},
|
||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
@@ -1354,12 +1492,12 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
&text.pixels,
|
&text.pixels,
|
||||||
wgpu::TexelCopyBufferLayout {
|
wgpu::TexelCopyBufferLayout {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
bytes_per_row: Some(text.size.width as u32 * 4),
|
bytes_per_row: Some(text.texture_size.width as u32 * 4),
|
||||||
rows_per_image: Some(text.size.height as u32),
|
rows_per_image: Some(text.texture_size.height as u32),
|
||||||
},
|
},
|
||||||
wgpu::Extent3d {
|
wgpu::Extent3d {
|
||||||
width: text.size.width as u32,
|
width: text.texture_size.width as u32,
|
||||||
height: text.size.height as u32,
|
height: text.texture_size.height as u32,
|
||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1382,7 +1520,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
_texture: texture,
|
_texture: texture,
|
||||||
bind_group,
|
bind_group,
|
||||||
origin_offset: text.origin_offset,
|
origin_offset: text.origin_offset,
|
||||||
size: text.size,
|
size: text.draw_size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1813,32 +1951,44 @@ fn build_textured_vertices(
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct GlyphVertexContext {
|
||||||
|
logical_size: UiSize,
|
||||||
|
scale_factor: f32,
|
||||||
|
color: Color,
|
||||||
|
clip: ActiveClip,
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn push_glyph_vertices(
|
fn push_glyph_vertices(
|
||||||
vertices: &mut Vec<TextVertex>,
|
vertices: &mut Vec<TextVertex>,
|
||||||
glyph_rect: PixelRect,
|
glyph_rect: PixelRect,
|
||||||
atlas_rect: AtlasRect,
|
atlas_rect: AtlasRect,
|
||||||
clip_rect: Option<PixelRect>,
|
clip_rect: Option<PixelRect>,
|
||||||
logical_size: UiSize,
|
context: GlyphVertexContext,
|
||||||
color: Color,
|
|
||||||
clip: ActiveClip,
|
|
||||||
) {
|
) {
|
||||||
let Some((dest_rect, uv_rect)) = clipped_glyph_quad(glyph_rect, atlas_rect, clip_rect) else {
|
let Some((dest_rect, uv_rect)) = clipped_glyph_quad(glyph_rect, atlas_rect, clip_rect) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let left = to_ndc_x(dest_rect.left as f32, logical_size.width.max(1.0));
|
let scale_factor = context.scale_factor.max(1.0);
|
||||||
let right = to_ndc_x(dest_rect.right as f32, logical_size.width.max(1.0));
|
let logical_left = dest_rect.left as f32 / scale_factor;
|
||||||
let top = to_ndc_y(dest_rect.top as f32, logical_size.height.max(1.0));
|
let logical_top = dest_rect.top as f32 / scale_factor;
|
||||||
let bottom = to_ndc_y(dest_rect.bottom as f32, logical_size.height.max(1.0));
|
let logical_right = dest_rect.right as f32 / scale_factor;
|
||||||
|
let logical_bottom = dest_rect.bottom as f32 / scale_factor;
|
||||||
|
let left = to_ndc_x(logical_left, context.logical_size.width.max(1.0));
|
||||||
|
let right = to_ndc_x(logical_right, context.logical_size.width.max(1.0));
|
||||||
|
let top = to_ndc_y(logical_top, context.logical_size.height.max(1.0));
|
||||||
|
let bottom = to_ndc_y(logical_bottom, context.logical_size.height.max(1.0));
|
||||||
|
|
||||||
let color = color_to_f32(color);
|
let color = color_to_f32(context.color);
|
||||||
let clip_rect = clip_rect_array(clip);
|
let clip_rect = clip_rect_array(context.clip);
|
||||||
let (rounded_clip_rect, clip_params) = rounded_clip_arrays(clip);
|
let (rounded_clip_rect, clip_params) = rounded_clip_arrays(context.clip);
|
||||||
vertices.extend_from_slice(&[
|
vertices.extend_from_slice(&[
|
||||||
TextVertex {
|
TextVertex {
|
||||||
position: [left, top],
|
position: [left, top],
|
||||||
world_position: [dest_rect.left as f32, dest_rect.top as f32],
|
world_position: [logical_left, logical_top],
|
||||||
uv: [uv_rect.0, uv_rect.1],
|
uv: [uv_rect.0, uv_rect.1],
|
||||||
color,
|
color,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
@@ -1847,7 +1997,7 @@ fn push_glyph_vertices(
|
|||||||
},
|
},
|
||||||
TextVertex {
|
TextVertex {
|
||||||
position: [left, bottom],
|
position: [left, bottom],
|
||||||
world_position: [dest_rect.left as f32, dest_rect.bottom as f32],
|
world_position: [logical_left, logical_bottom],
|
||||||
uv: [uv_rect.0, uv_rect.3],
|
uv: [uv_rect.0, uv_rect.3],
|
||||||
color,
|
color,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
@@ -1856,7 +2006,7 @@ fn push_glyph_vertices(
|
|||||||
},
|
},
|
||||||
TextVertex {
|
TextVertex {
|
||||||
position: [right, top],
|
position: [right, top],
|
||||||
world_position: [dest_rect.right as f32, dest_rect.top as f32],
|
world_position: [logical_right, logical_top],
|
||||||
uv: [uv_rect.2, uv_rect.1],
|
uv: [uv_rect.2, uv_rect.1],
|
||||||
color,
|
color,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
@@ -1865,7 +2015,7 @@ fn push_glyph_vertices(
|
|||||||
},
|
},
|
||||||
TextVertex {
|
TextVertex {
|
||||||
position: [right, top],
|
position: [right, top],
|
||||||
world_position: [dest_rect.right as f32, dest_rect.top as f32],
|
world_position: [logical_right, logical_top],
|
||||||
uv: [uv_rect.2, uv_rect.1],
|
uv: [uv_rect.2, uv_rect.1],
|
||||||
color,
|
color,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
@@ -1874,7 +2024,7 @@ fn push_glyph_vertices(
|
|||||||
},
|
},
|
||||||
TextVertex {
|
TextVertex {
|
||||||
position: [left, bottom],
|
position: [left, bottom],
|
||||||
world_position: [dest_rect.left as f32, dest_rect.bottom as f32],
|
world_position: [logical_left, logical_bottom],
|
||||||
uv: [uv_rect.0, uv_rect.3],
|
uv: [uv_rect.0, uv_rect.3],
|
||||||
color,
|
color,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
@@ -1883,7 +2033,7 @@ fn push_glyph_vertices(
|
|||||||
},
|
},
|
||||||
TextVertex {
|
TextVertex {
|
||||||
position: [right, bottom],
|
position: [right, bottom],
|
||||||
world_position: [dest_rect.right as f32, dest_rect.bottom as f32],
|
world_position: [logical_right, logical_bottom],
|
||||||
uv: [uv_rect.2, uv_rect.3],
|
uv: [uv_rect.2, uv_rect.3],
|
||||||
color,
|
color,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
@@ -1902,9 +2052,9 @@ fn color_to_f32(color: Color) -> [f32; 4] {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_vertices(scene: &SceneSnapshot) -> Vec<Vertex> {
|
fn build_vertices(scene: &SceneSnapshot, logical_size: UiSize) -> Vec<Vertex> {
|
||||||
let width = scene.logical_size.width.max(1.0);
|
let width = logical_size.width.max(1.0);
|
||||||
let height = scene.logical_size.height.max(1.0);
|
let height = logical_size.height.max(1.0);
|
||||||
let mut vertices = Vec::new();
|
let mut vertices = Vec::new();
|
||||||
let mut clip_stack = Vec::new();
|
let mut clip_stack = Vec::new();
|
||||||
let mut active_clip = ActiveClip::default();
|
let mut active_clip = ActiveClip::default();
|
||||||
@@ -2157,15 +2307,22 @@ fn expand_rect(rect: Rect, inset: f32) -> Rect {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rect_to_pixel_rect(rect: Rect) -> PixelRect {
|
fn scale_rect_to_pixel_rect(rect: Rect, scale: f32) -> PixelRect {
|
||||||
PixelRect {
|
PixelRect {
|
||||||
left: rect.origin.x.floor() as i32,
|
left: (rect.origin.x * scale).floor() as i32,
|
||||||
top: rect.origin.y.floor() as i32,
|
top: (rect.origin.y * scale).floor() as i32,
|
||||||
right: (rect.origin.x + rect.size.width).ceil() as i32,
|
right: ((rect.origin.x + rect.size.width) * scale).ceil() as i32,
|
||||||
bottom: (rect.origin.y + rect.size.height).ceil() as i32,
|
bottom: ((rect.origin.y + rect.size.height) * scale).ceil() as i32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pixel_rects_intersect(first: PixelRect, second: PixelRect) -> bool {
|
||||||
|
first.left < second.right
|
||||||
|
&& first.right > second.left
|
||||||
|
&& first.top < second.bottom
|
||||||
|
&& first.bottom > second.top
|
||||||
|
}
|
||||||
|
|
||||||
fn shadow_blur_extent(blur: f32) -> f32 {
|
fn shadow_blur_extent(blur: f32) -> f32 {
|
||||||
blur.max(0.0) * 2.0
|
blur.max(0.0) * 2.0
|
||||||
}
|
}
|
||||||
@@ -2225,6 +2382,31 @@ fn intersect_rects(first: Option<Rect>, second: Option<Rect>) -> Option<Rect> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn text_raster_clip(
|
||||||
|
text: &PreparedText,
|
||||||
|
clip: ActiveClip,
|
||||||
|
logical_size: UiSize,
|
||||||
|
) -> Option<Option<Rect>> {
|
||||||
|
let mut absolute_clip = Some(Rect::new(0.0, 0.0, logical_size.width, logical_size.height));
|
||||||
|
if clip.rect_active {
|
||||||
|
absolute_clip = intersect_rects(absolute_clip, Some(clip.resolved_rect()?));
|
||||||
|
}
|
||||||
|
if let Some(bounds) = text.bounds {
|
||||||
|
let text_bounds = Rect::new(text.origin.x, text.origin.y, bounds.width, bounds.height);
|
||||||
|
absolute_clip = intersect_rects(absolute_clip, Some(text_bounds));
|
||||||
|
}
|
||||||
|
absolute_clip?;
|
||||||
|
|
||||||
|
Some(absolute_clip.map(|rect| {
|
||||||
|
Rect::new(
|
||||||
|
rect.origin.x - text.origin.x,
|
||||||
|
rect.origin.y - text.origin.y,
|
||||||
|
rect.size.width,
|
||||||
|
rect.size.height,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
fn empty_clip_rect() -> Rect {
|
fn empty_clip_rect() -> Rect {
|
||||||
Rect::new(1.0, 1.0, -1.0, -1.0)
|
Rect::new(1.0, 1.0, -1.0, -1.0)
|
||||||
}
|
}
|
||||||
@@ -2334,7 +2516,7 @@ fn clip_visible_glyphs(text: &PreparedText, clip: Option<Rect>) -> &[GlyphInstan
|
|||||||
let local_bottom = local_top + clip.size.height;
|
let local_bottom = local_top + clip.size.height;
|
||||||
|
|
||||||
// First line whose bottom edge reaches or passes the clip top.
|
// First line whose bottom edge reaches or passes the clip top.
|
||||||
let start = lines.partition_point(|l| l.rect.origin.y + l.rect.size.height < local_top);
|
let start = lines.partition_point(|l| l.rect.origin.y + l.rect.size.height <= local_top);
|
||||||
// First line whose top edge is strictly past the clip bottom.
|
// First line whose top edge is strictly past the clip bottom.
|
||||||
let end = lines.partition_point(|l| l.rect.origin.y <= local_bottom);
|
let end = lines.partition_point(|l| l.rect.origin.y <= local_bottom);
|
||||||
|
|
||||||
@@ -2347,12 +2529,21 @@ fn clip_visible_glyphs(text: &PreparedText, clip: Option<Rect>) -> &[GlyphInstan
|
|||||||
&text.glyphs[glyph_start..glyph_end.min(text.glyphs.len())]
|
&text.glyphs[glyph_start..glyph_end.min(text.glyphs.len())]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_texture_key(text: &PreparedText) -> TextTextureKey {
|
fn text_texture_key(
|
||||||
|
text: &PreparedText,
|
||||||
|
raster_clip: Option<Rect>,
|
||||||
|
scale_factor: f32,
|
||||||
|
) -> TextTextureKey {
|
||||||
TextTextureKey {
|
TextTextureKey {
|
||||||
text: text.text.clone(),
|
layout_key: text.layout_cache_key(),
|
||||||
bounds: text
|
bounds: text.bounds.map(|bounds| {
|
||||||
.bounds
|
(
|
||||||
.map(|bounds| (bounds.width.to_bits(), bounds.height.to_bits())),
|
(bounds.width * scale_factor).ceil() as i32,
|
||||||
|
(bounds.height * scale_factor).ceil() as i32,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
clip: raster_clip
|
||||||
|
.map(|clip| pixel_rect_tuple(scale_rect_to_pixel_rect(clip, scale_factor))),
|
||||||
font_size_bits: text.font_size.to_bits(),
|
font_size_bits: text.font_size.to_bits(),
|
||||||
line_height_bits: text.line_height.to_bits(),
|
line_height_bits: text.line_height.to_bits(),
|
||||||
color: (
|
color: (
|
||||||
@@ -2361,28 +2552,33 @@ fn text_texture_key(text: &PreparedText) -> TextTextureKey {
|
|||||||
text.default_color.b,
|
text.default_color.b,
|
||||||
text.default_color.a,
|
text.default_color.a,
|
||||||
),
|
),
|
||||||
glyphs: text
|
}
|
||||||
.glyphs
|
}
|
||||||
.iter()
|
|
||||||
.map(|glyph| TextTextureGlyph {
|
fn pixel_rect_tuple(rect: PixelRect) -> (i32, i32, i32, i32) {
|
||||||
// Glyph positions are already LOCAL; no subtraction needed.
|
(rect.left, rect.top, rect.right, rect.bottom)
|
||||||
local_x_bits: glyph.position.x.to_bits(),
|
}
|
||||||
local_y_bits: glyph.position.y.to_bits(),
|
|
||||||
advance_bits: glyph.advance.to_bits(),
|
fn scale_glyph_cache_key(cache_key: CacheKey, scale_factor: f32) -> CacheKey {
|
||||||
color: (glyph.color.r, glyph.color.g, glyph.color.b, glyph.color.a),
|
CacheKey {
|
||||||
cache_key: glyph.cache_key,
|
font_size_bits: (f32::from_bits(cache_key.font_size_bits) * scale_factor.max(1.0))
|
||||||
})
|
.to_bits(),
|
||||||
.collect(),
|
..cache_key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
ActiveClip, blend_rgba, build_text_vertices, build_vertices, clip_rect_array,
|
ActiveClip, AtlasRect, GlyphVertexContext, PixelRect, blend_rgba, build_text_vertices,
|
||||||
push_clip_state, rounded_clip_arrays, text_texture_key,
|
build_vertices, clip_rect_array, clip_visible_glyphs, push_clip_state, push_glyph_vertices,
|
||||||
|
rounded_clip_arrays, scale_glyph_cache_key, text_texture_key,
|
||||||
|
};
|
||||||
|
use cosmic_text::{CacheKey, CacheKeyFlags, SubpixelBin, fontdb};
|
||||||
|
use ruin_ui::{
|
||||||
|
ClipRegion, Color, Point, PreparedText, Rect, SceneSnapshot, TextStyle, TextSystem,
|
||||||
|
TextWrap, UiSize,
|
||||||
};
|
};
|
||||||
use ruin_ui::{ClipRegion, Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quad_scenes_expand_to_six_vertices_per_quad() {
|
fn quad_scenes_expand_to_six_vertices_per_quad() {
|
||||||
@@ -2403,7 +2599,7 @@ mod tests {
|
|||||||
Color::rgb(0xFF, 0xFF, 0xFF),
|
Color::rgb(0xFF, 0xFF, 0xFF),
|
||||||
));
|
));
|
||||||
|
|
||||||
let vertices = build_vertices(&scene);
|
let vertices = build_vertices(&scene, scene.logical_size);
|
||||||
assert_eq!(vertices.len(), 12);
|
assert_eq!(vertices.len(), 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2435,7 +2631,97 @@ mod tests {
|
|||||||
Color::rgb(0xEE, 0xEE, 0xEE),
|
Color::rgb(0xEE, 0xEE, 0xEE),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(text_texture_key(&first), text_texture_key(&second));
|
assert_eq!(
|
||||||
|
text_texture_key(&first, None, 1.0),
|
||||||
|
text_texture_key(&second, None, 1.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_texture_key_normalizes_clip_to_device_pixels() {
|
||||||
|
let text = PreparedText::monospace(
|
||||||
|
"scrolling text",
|
||||||
|
Point::new(20.0, 30.0),
|
||||||
|
16.0,
|
||||||
|
8.0,
|
||||||
|
Color::rgb(0xEE, 0xEE, 0xEE),
|
||||||
|
);
|
||||||
|
|
||||||
|
let first = text_texture_key(&text, Some(Rect::new(0.001, 4.001, 80.001, 20.001)), 2.0);
|
||||||
|
let second = text_texture_key(&text, Some(Rect::new(0.002, 4.002, 80.002, 20.002)), 2.0);
|
||||||
|
let different_pixel =
|
||||||
|
text_texture_key(&text, Some(Rect::new(0.6, 4.001, 80.001, 20.001)), 2.0);
|
||||||
|
|
||||||
|
assert_eq!(first, second);
|
||||||
|
assert_ne!(first, different_pixel);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clip_visible_glyphs_returns_only_lines_intersecting_clip() {
|
||||||
|
let mut text_system = TextSystem::new();
|
||||||
|
let style = TextStyle::new(16.0, Color::rgb(0xEE, 0xEE, 0xEE))
|
||||||
|
.with_wrap(TextWrap::None)
|
||||||
|
.with_line_height(20.0);
|
||||||
|
let text = text_system.prepare("abcd\nefgh", Point::new(10.0, 20.0), &style);
|
||||||
|
let visible = clip_visible_glyphs(&text, Some(Rect::new(10.0, 40.0, 100.0, 20.0)));
|
||||||
|
|
||||||
|
assert_eq!(visible.len(), 4);
|
||||||
|
assert_eq!(visible[0].text_start, 5);
|
||||||
|
assert_eq!(visible[3].text_end, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scale_glyph_cache_key_scales_font_size_only() {
|
||||||
|
let key = CacheKey {
|
||||||
|
font_id: fontdb::ID::dummy(),
|
||||||
|
glyph_id: 42,
|
||||||
|
font_size_bits: 13.0_f32.to_bits(),
|
||||||
|
x_bin: SubpixelBin::One,
|
||||||
|
y_bin: SubpixelBin::Two,
|
||||||
|
font_weight: fontdb::Weight::BOLD,
|
||||||
|
flags: CacheKeyFlags::FAKE_ITALIC,
|
||||||
|
};
|
||||||
|
|
||||||
|
let scaled = scale_glyph_cache_key(key, 2.0);
|
||||||
|
|
||||||
|
assert_eq!(f32::from_bits(scaled.font_size_bits), 26.0);
|
||||||
|
assert_eq!(scaled.font_id, key.font_id);
|
||||||
|
assert_eq!(scaled.glyph_id, key.glyph_id);
|
||||||
|
assert_eq!(scaled.x_bin, key.x_bin);
|
||||||
|
assert_eq!(scaled.y_bin, key.y_bin);
|
||||||
|
assert_eq!(scaled.font_weight, key.font_weight);
|
||||||
|
assert_eq!(scaled.flags, key.flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scaled_atlas_vertices_convert_device_pixels_to_logical_space() {
|
||||||
|
let mut vertices = Vec::new();
|
||||||
|
push_glyph_vertices(
|
||||||
|
&mut vertices,
|
||||||
|
PixelRect {
|
||||||
|
left: 20,
|
||||||
|
top: 10,
|
||||||
|
right: 40,
|
||||||
|
bottom: 30,
|
||||||
|
},
|
||||||
|
AtlasRect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
GlyphVertexContext {
|
||||||
|
logical_size: UiSize::new(100.0, 100.0),
|
||||||
|
scale_factor: 2.0,
|
||||||
|
color: Color::rgb(0xFF, 0xFF, 0xFF),
|
||||||
|
clip: ActiveClip::default(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(vertices.len(), 6);
|
||||||
|
assert_eq!(vertices[0].world_position, [10.0, 5.0]);
|
||||||
|
assert_eq!(vertices[5].world_position, [20.0, 15.0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user