Compare commits
8 Commits
6dcc2d0d00
...
macos
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccc3984b01 | ||
|
|
23d9f02e3a | ||
|
|
d9ac6bfeb8 | ||
|
|
056f356065 | ||
|
|
1a083ee12c | ||
|
|
bfda24ad0a | ||
|
|
67400f1499 | ||
|
|
861bf63621 |
63
Cargo.lock
generated
63
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,6 +1928,7 @@ 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",
|
||||||
@@ -1912,6 +1958,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"
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
Shortcut::new(Key::Character(ch)),
|
||||||
|
ShortcutScope::Application,
|
||||||
|
{
|
||||||
let text = text.clone();
|
let text = text.clone();
|
||||||
move || s.input(&text)
|
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 ----
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -73,10 +73,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,
|
||||||
@@ -130,7 +142,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 +213,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 +231,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 +248,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())?,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -311,9 +331,10 @@ impl JsonParser {
|
|||||||
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), delta) == parsed.range
|
||||||
&& old_text
|
&& 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,
|
||||||
|
) == new_text.get(parsed.range.start..parsed.range.end)
|
||||||
{
|
{
|
||||||
*reused += count_nodes(old);
|
*reused += count_nodes(old);
|
||||||
return AstNode {
|
return AstNode {
|
||||||
@@ -403,7 +424,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 +520,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 +564,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 +599,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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -102,7 +102,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 +119,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,
|
||||||
|
|||||||
@@ -259,7 +259,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 +307,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 +342,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 +405,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;
|
||||||
@@ -710,7 +719,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)]
|
||||||
@@ -403,7 +409,9 @@ impl TokenTreeParser {
|
|||||||
reused_nodes: &mut usize,
|
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| can_reuse_node(old, old_parent_start, parsed, old_text, new_text, delta))
|
||||||
|
{
|
||||||
*reused_nodes += count_nodes(old);
|
*reused_nodes += count_nodes(old);
|
||||||
return TokenTreeNode {
|
return TokenTreeNode {
|
||||||
id: old.id,
|
id: old.id,
|
||||||
@@ -423,16 +431,14 @@ 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]
|
|
||||||
&& can_descend_into_node(candidate, child)
|
|
||||||
})
|
|
||||||
.map(|candidate| {
|
.map(|candidate| {
|
||||||
used[index] = true;
|
used[index] = true;
|
||||||
candidate
|
candidate
|
||||||
})
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
old.children.iter().enumerate().find_map(|(candidate_index, candidate)| {
|
old.children.iter().enumerate().find_map(
|
||||||
|
|(candidate_index, candidate)| {
|
||||||
if used[candidate_index]
|
if used[candidate_index]
|
||||||
|| !can_descend_into_node(candidate, child)
|
|| !can_descend_into_node(candidate, child)
|
||||||
{
|
{
|
||||||
@@ -440,7 +446,8 @@ impl TokenTreeParser {
|
|||||||
}
|
}
|
||||||
used[candidate_index] = true;
|
used[candidate_index] = true;
|
||||||
Some(candidate)
|
Some(candidate)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
self.reuse_node(
|
self.reuse_node(
|
||||||
@@ -602,8 +609,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 +674,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 +711,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 +980,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 +1202,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 +1240,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 +1346,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 +1365,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 {
|
||||||
@@ -1376,7 +1392,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 +1419,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 +1453,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 +1538,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 +1570,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 +1587,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 +1614,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 +1627,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 +1670,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 +1686,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 +1708,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 +1810,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 +1846,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 +1882,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 +1919,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 +1982,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 +1998,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 +2015,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 +2077,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 +2102,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 +2134,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 +2164,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 +2230,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,6 +198,14 @@ pub(crate) fn sync_primary_selection(
|
|||||||
interaction_tree: &InteractionTree,
|
interaction_tree: &InteractionTree,
|
||||||
selection: Option<TextSelection>,
|
selection: Option<TextSelection>,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
let _ = (window, interaction_tree, selection);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
let Some(selection) = selection else {
|
let Some(selection) = selection else {
|
||||||
window.set_primary_selection_text(String::new())?;
|
window.set_primary_selection_text(String::new())?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -215,6 +221,7 @@ pub(crate) fn sync_primary_selection(
|
|||||||
window.set_primary_selection_text(copied)?;
|
window.set_primary_selection_text(copied)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn scroll_handler_for_event<'a>(
|
pub(crate) fn scroll_handler_for_event<'a>(
|
||||||
handlers: &'a HashMap<ElementId, ScrollHandler>,
|
handlers: &'a HashMap<ElementId, ScrollHandler>,
|
||||||
@@ -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) },
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +65,8 @@ 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();
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
|
{
|
||||||
let support = STDIN_URING_SUPPORTED.with(Cell::get);
|
let support = STDIN_URING_SUPPORTED.with(Cell::get);
|
||||||
if support != Some(false) {
|
if support != Some(false) {
|
||||||
match submit_uring_read(fd, READ_CHUNK_BYTES).await {
|
match submit_uring_read(fd, READ_CHUNK_BYTES).await {
|
||||||
@@ -73,6 +80,7 @@ impl Stdin {
|
|||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
offload(move || {
|
offload(move || {
|
||||||
let mut buffer = vec![0; READ_CHUNK_BYTES];
|
let mut buffer = vec![0; READ_CHUNK_BYTES];
|
||||||
@@ -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,
|
|
||||||
timeout,
|
|
||||||
move |cqe| {
|
|
||||||
callback_handle.complete(map(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();
|
||||||
@@ -278,20 +280,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 +299,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 +583,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,8 +870,7 @@ 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()
|
||||||
{
|
{
|
||||||
@@ -888,8 +878,7 @@ fn layout_container_children(
|
|||||||
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 =
|
let natural = intrinsic_size(child, offered, text_system, perf_stats, layout_cache);
|
||||||
intrinsic_size(child, offered, text_system, perf_stats, layout_cache);
|
|
||||||
Some(
|
Some(
|
||||||
cross_axis_size(natural, element.style.direction)
|
cross_axis_size(natural, element.style.direction)
|
||||||
.max(min_cross_outer)
|
.max(min_cross_outer)
|
||||||
@@ -966,6 +955,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 +1675,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 +1737,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 +2179,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 +2877,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_str()),
|
||||||
|
_ => 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ pub use layout::{
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -443,7 +444,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 {
|
||||||
@@ -495,9 +500,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,
|
||||||
@@ -930,7 +933,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 +1043,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));
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
17
lib/ui_platform_macos/Cargo.toml
Normal file
17
lib/ui_platform_macos/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "ruin_ui_platform_macos"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[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"
|
||||||
|
ruin_ui = { path = "../ui" }
|
||||||
|
ruin_ui_renderer_wgpu = { path = "../ui_renderer_wgpu" }
|
||||||
|
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
||||||
|
tracing = "0.1"
|
||||||
2277
lib/ui_platform_macos/src/lib.rs
Normal file
2277
lib/ui_platform_macos/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -40,13 +40,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 +242,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 +1355,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 +1402,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 +1428,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 +1447,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 +1530,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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +170,8 @@ struct UploadedAtlasText {
|
|||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
struct TextTextureKey {
|
struct TextTextureKey {
|
||||||
text: String,
|
text: String,
|
||||||
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),
|
||||||
@@ -330,6 +332,7 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,8 +380,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,6 +727,7 @@ 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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -735,9 +742,33 @@ 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 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 +792,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 +804,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 +814,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);
|
||||||
}
|
}
|
||||||
@@ -871,21 +903,29 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rasterize_text(&mut self, text: &PreparedText) -> Option<RasterizedText> {
|
fn rasterize_text(
|
||||||
if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
|
&mut self,
|
||||||
return self.rasterize_prepared_text(text);
|
text: &PreparedText,
|
||||||
|
raster_clip: Option<Rect>,
|
||||||
|
) -> Option<RasterizedText> {
|
||||||
|
if (self.scale_factor - 1.0).abs() <= f32::EPSILON
|
||||||
|
&& text.glyphs.iter().all(|glyph| glyph.cache_key.is_some())
|
||||||
|
{
|
||||||
|
return self.rasterize_prepared_text(text, raster_clip);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
.or_else(|| {
|
||||||
|
text.bounds.map(|bounds| PixelRect {
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
right: bounds.width.ceil() as i32,
|
right: (bounds.width * self.scale_factor).ceil() as i32,
|
||||||
bottom: bounds.height.ceil() as i32,
|
bottom: (bounds.height * self.scale_factor).ceil() as i32,
|
||||||
},
|
})
|
||||||
None => glyph_union(&glyphs)?,
|
})
|
||||||
};
|
.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 +936,36 @@ 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(
|
||||||
|
&mut self,
|
||||||
|
text: &PreparedText,
|
||||||
|
raster_clip: Option<Rect>,
|
||||||
|
) -> Option<RasterizedText> {
|
||||||
let glyphs = self.collect_prepared_glyphs(text);
|
let glyphs = self.collect_prepared_glyphs(text);
|
||||||
let clip = match text.bounds {
|
let clip = raster_clip
|
||||||
Some(bounds) => PixelRect {
|
.map(rect_to_pixel_rect)
|
||||||
|
.or_else(|| {
|
||||||
|
text.bounds.map(|bounds| PixelRect {
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
right: bounds.width.ceil() as i32,
|
right: bounds.width.ceil() as i32,
|
||||||
bottom: bounds.height.ceil() as i32,
|
bottom: bounds.height.ceil() as i32,
|
||||||
},
|
})
|
||||||
None => glyph_union_prepared(&glyphs)?,
|
})
|
||||||
};
|
.or_else(|| glyph_union_prepared(&glyphs))?;
|
||||||
if clip.width() == 0 || clip.height() == 0 {
|
if clip.width() == 0 || clip.height() == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -931,7 +984,8 @@ 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(clip.left as f32, clip.top as f32),
|
||||||
size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
draw_size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||||
|
texture_size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||||
pixels,
|
pixels,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -972,13 +1026,20 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -999,13 +1060,17 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
if width == 0 || height == 0 {
|
if width == 0 || height == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
glyphs.push(GlyphBitmap {
|
let rect = PixelRect {
|
||||||
rect: PixelRect {
|
|
||||||
left: physical.x + image.placement.left,
|
left: physical.x + image.placement.left,
|
||||||
top: physical.y - image.placement.top,
|
top: physical.y - image.placement.top,
|
||||||
right: physical.x + image.placement.left + width,
|
right: physical.x + image.placement.left + width,
|
||||||
bottom: physical.y - image.placement.top + height,
|
bottom: physical.y - image.placement.top + height,
|
||||||
},
|
};
|
||||||
|
if raster_clip.is_some_and(|clip| !pixel_rects_intersect(rect, clip)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
glyphs.push(GlyphBitmap {
|
||||||
|
rect,
|
||||||
content: image.content,
|
content: image.content,
|
||||||
color: glyph
|
color: glyph
|
||||||
.color_opt
|
.color_opt
|
||||||
@@ -1020,6 +1085,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 +1119,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 +1131,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 +1143,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,7 +1163,8 @@ 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,
|
logical_size,
|
||||||
|
self.scale_factor,
|
||||||
resolved_color,
|
resolved_color,
|
||||||
active_clip,
|
active_clip,
|
||||||
);
|
);
|
||||||
@@ -1263,9 +1337,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 +1408,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 +1429,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 +1457,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1820,6 +1895,7 @@ fn push_glyph_vertices(
|
|||||||
atlas_rect: AtlasRect,
|
atlas_rect: AtlasRect,
|
||||||
clip_rect: Option<PixelRect>,
|
clip_rect: Option<PixelRect>,
|
||||||
logical_size: UiSize,
|
logical_size: UiSize,
|
||||||
|
scale_factor: f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
clip: ActiveClip,
|
clip: ActiveClip,
|
||||||
) {
|
) {
|
||||||
@@ -1827,10 +1903,15 @@ fn push_glyph_vertices(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let left = to_ndc_x(dest_rect.left as f32, logical_size.width.max(1.0));
|
let scale_factor = 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, logical_size.width.max(1.0));
|
||||||
|
let right = to_ndc_x(logical_right, logical_size.width.max(1.0));
|
||||||
|
let top = to_ndc_y(logical_top, logical_size.height.max(1.0));
|
||||||
|
let bottom = to_ndc_y(logical_bottom, logical_size.height.max(1.0));
|
||||||
|
|
||||||
let color = color_to_f32(color);
|
let color = color_to_f32(color);
|
||||||
let clip_rect = clip_rect_array(clip);
|
let clip_rect = clip_rect_array(clip);
|
||||||
@@ -1838,7 +1919,7 @@ fn push_glyph_vertices(
|
|||||||
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 +1928,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 +1937,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 +1946,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 +1955,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 +1964,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 +1983,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();
|
||||||
@@ -2166,6 +2247,22 @@ fn rect_to_pixel_rect(rect: Rect) -> PixelRect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scale_rect_to_pixel_rect(rect: Rect, scale: f32) -> PixelRect {
|
||||||
|
PixelRect {
|
||||||
|
left: (rect.origin.x * scale).floor() as i32,
|
||||||
|
top: (rect.origin.y * scale).floor() as i32,
|
||||||
|
right: ((rect.origin.x + rect.size.width) * scale).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 +2322,33 @@ 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));
|
||||||
|
}
|
||||||
|
if absolute_clip.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -2347,12 +2471,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(),
|
text: text.text.clone(),
|
||||||
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: (
|
||||||
@@ -2376,12 +2509,26 @@ fn text_texture_key(text: &PreparedText) -> TextTextureKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pixel_rect_tuple(rect: PixelRect) -> (i32, i32, i32, i32) {
|
||||||
|
(rect.left, rect.top, rect.right, rect.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scale_glyph_cache_key(cache_key: CacheKey, scale_factor: f32) -> CacheKey {
|
||||||
|
CacheKey {
|
||||||
|
font_size_bits: (f32::from_bits(cache_key.font_size_bits) * scale_factor.max(1.0))
|
||||||
|
.to_bits(),
|
||||||
|
..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, PixelRect, blend_rgba, build_text_vertices, build_vertices,
|
||||||
push_clip_state, rounded_clip_arrays, text_texture_key,
|
clip_rect_array, 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, UiSize};
|
use ruin_ui::{ClipRegion, Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2403,7 +2550,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 +2582,81 @@ 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 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,
|
||||||
|
UiSize::new(100.0, 100.0),
|
||||||
|
2.0,
|
||||||
|
Color::rgb(0xFF, 0xFF, 0xFF),
|
||||||
|
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