Cleanup
This commit is contained in:
@@ -268,7 +268,6 @@ fn RuntimeIoExample(server_addr: SocketAddr) -> impl IntoView {
|
||||
};
|
||||
eprintln!("example05: {message}");
|
||||
let _ = save_status.set(Some(message));
|
||||
flush_reactive_updates();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1363,10 +1363,6 @@ pub fn use_effect(effect: impl Fn() + 'static) {
|
||||
with_hook_slot(|| ruin_reactivity::effect(effect), |_| ());
|
||||
}
|
||||
|
||||
pub fn flush_reactive_updates() {
|
||||
current_reactor().flush_now();
|
||||
}
|
||||
|
||||
pub fn use_window_title(compute: impl FnOnce() -> String) {
|
||||
with_render_context_state(|context| {
|
||||
context.side_effects.borrow_mut().window_title = Some(compute());
|
||||
@@ -1580,7 +1576,6 @@ where
|
||||
let result = future.await;
|
||||
if generation.get() == next_generation {
|
||||
let _ = resource.state.replace(ResourceState::Ready(result));
|
||||
current_reactor().flush_now();
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -2058,10 +2053,10 @@ pub mod prelude {
|
||||
ContextKey, FocusScope, FontWeight, IntoBorder, IntoEdges, IntoView, Key, Memo, Mountable,
|
||||
Pending, Ready, Resource, ResourceState, Result, ScrollBoxBuilder, ScrollBoxWidget,
|
||||
Shortcut, ShortcutScope, Signal, TextBuilder, TextChildren, TextRole, TextValue, View,
|
||||
WidgetRef, Window, block, button, colors, column, component, context_provider,
|
||||
flush_reactive_updates, provide, row, scroll_box, surfaces, text, use_context, use_effect,
|
||||
use_memo, use_resource, use_shortcut, use_shortcut_with_context, use_signal,
|
||||
use_widget_ref, use_window_title, view,
|
||||
WidgetRef, Window, block, button, colors, column, component, context_provider, provide,
|
||||
row, scroll_box, surfaces, text, use_context, use_effect, use_memo, use_resource,
|
||||
use_shortcut, use_shortcut_with_context, use_signal, use_widget_ref, use_window_title,
|
||||
view,
|
||||
};
|
||||
pub use ruin_ui::{
|
||||
Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree, PointerButton,
|
||||
@@ -2632,4 +2627,27 @@ mod tests {
|
||||
|
||||
assert!(debug.contains("offset_y: 96.0"), "{debug}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_signal_write_flushes_without_manual_reactor_flush() {
|
||||
let signal = Signal::new("before".to_string());
|
||||
let observed = Rc::new(RefCell::new(String::new()));
|
||||
let _effect = ruin_reactivity::effect({
|
||||
let signal = signal.clone();
|
||||
let observed = Rc::clone(&observed);
|
||||
move || {
|
||||
*observed.borrow_mut() = signal.get();
|
||||
}
|
||||
});
|
||||
|
||||
ruin_runtime::queue_future({
|
||||
let signal = signal.clone();
|
||||
async move {
|
||||
let _ = signal.set("after".to_string());
|
||||
}
|
||||
});
|
||||
ruin_runtime::run_until_stalled();
|
||||
|
||||
assert_eq!(observed.borrow().as_str(), "after");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,16 +13,12 @@ use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
||||
use std::time::Duration;
|
||||
|
||||
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;
|
||||
|
||||
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 {
|
||||
task: LocalTask,
|
||||
/// Wall time at which this task entered the local queue. Populated only
|
||||
@@ -668,12 +664,12 @@ struct ThreadState {
|
||||
driver: Driver,
|
||||
shared: Arc<ThreadShared>,
|
||||
worker_completion: Option<Arc<WorkerCompletion>>,
|
||||
local_microtasks: RefCell<VecDeque<LocalTask>>,
|
||||
local_macrotasks: RefCell<VecDeque<MacroTask>>,
|
||||
local_microtasks: RefCell<LocalTaskQueue>,
|
||||
local_macrotasks: RefCell<MacroTaskQueue<MacroTask>>,
|
||||
timers: RefCell<TimerHeap>,
|
||||
/// Zero-delay intervals bypasses the timer heap entirely. Each entry
|
||||
/// re-enqueues itself as a macrotask on every turn.
|
||||
immediate_intervals: RefCell<HashMap<usize, Rc<RefCell<Box<dyn FnMut()>>>>>,
|
||||
immediate_intervals: RefCell<HashMap<usize, IntervalCallback>>,
|
||||
next_timer_id: Cell<usize>,
|
||||
children: RefCell<Vec<ChildWorker>>,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,37 @@
|
||||
//! Public runtime driver primitives for macOS.
|
||||
|
||||
use std::cell::Cell;
|
||||
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,
|
||||
@@ -73,6 +98,8 @@ pub struct Driver {
|
||||
timer_deadline: Cell<Option<Duration>>,
|
||||
pending_wakes: Cell<u64>,
|
||||
pending_timers: Cell<u64>,
|
||||
next_fd_token: Cell<u64>,
|
||||
fd_waiters: RefCell<HashMap<FdKey, Vec<FdWaiter>>>,
|
||||
}
|
||||
|
||||
/// Creates a new driver and its paired [`ThreadNotifier`].
|
||||
@@ -130,6 +157,8 @@ pub fn create_driver() -> io::Result<(Driver, ThreadNotifier)> {
|
||||
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 {
|
||||
@@ -205,6 +234,51 @@ impl Driver {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_fd_readiness(
|
||||
&self,
|
||||
fd: RawFd,
|
||||
interest: FdInterest,
|
||||
completion: FdCompletion,
|
||||
) -> io::Result<FdReadinessToken> {
|
||||
let key = FdKey { fd, interest };
|
||||
let should_register = !self.fd_waiters.borrow().contains_key(&key);
|
||||
if should_register {
|
||||
self.update_fd_interest(key, libc::EV_ADD | libc::EV_ENABLE)?;
|
||||
}
|
||||
|
||||
let token = self.allocate_fd_token();
|
||||
self.fd_waiters
|
||||
.borrow_mut()
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(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, entries) in waiters.iter_mut() {
|
||||
if let Some(index) = entries.iter().position(|entry| entry.token == token) {
|
||||
entries.swap_remove(index);
|
||||
if entries.is_empty() {
|
||||
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();
|
||||
|
||||
@@ -241,6 +315,8 @@ impl Driver {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,6 +333,59 @@ impl Driver {
|
||||
|
||||
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 waiters = self.fd_waiters.borrow_mut().remove(&key);
|
||||
let Some(waiters) = waiters else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = self.update_fd_interest(key, libc::EV_DELETE);
|
||||
let result = fd_event_result(event);
|
||||
for waiter in waiters {
|
||||
waiter.completion.complete(match &result {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) => Err(io::Error::new(error.kind(), error.to_string())),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Driver {
|
||||
@@ -290,6 +419,31 @@ fn cvt(value: libc::c_int) -> io::Result<libc::c_int> {
|
||||
}
|
||||
}
|
||||
|
||||
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) -> io::Result<()> {
|
||||
if event.flags & libc::EV_ERROR != 0 && event.data != 0 {
|
||||
Err(io::Error::from_raw_os_error(event.data as i32))
|
||||
} 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) })?;
|
||||
|
||||
@@ -12,18 +12,13 @@ use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::driver::{Driver, ThreadNotifier, create_driver, monotonic_now};
|
||||
use super::driver::{Driver, FdReadinessToken, ThreadNotifier, create_driver, monotonic_now};
|
||||
use crate::platform::runtime_shared::{
|
||||
IntervalCallback, LocalBoxFuture, LocalTask, LocalTaskQueue, MICROTASK_STARVATION_THRESHOLD,
|
||||
MacroTaskQueue, SendTask,
|
||||
};
|
||||
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>>;
|
||||
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.
|
||||
const MICROTASK_STARVATION_THRESHOLD: u64 = 1000;
|
||||
|
||||
struct MacroTask {
|
||||
task: LocalTask,
|
||||
/// Wall time at which this task entered the local queue. Populated only
|
||||
@@ -100,6 +95,10 @@ pub(crate) fn with_current_driver<T>(f: impl FnOnce(&Driver) -> T) -> T {
|
||||
f(¤t_thread().driver)
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_fd_readiness(token: FdReadinessToken) {
|
||||
current_thread().driver.cancel_fd_readiness(token);
|
||||
}
|
||||
|
||||
/// Queues a macrotask on the current runtime thread.
|
||||
///
|
||||
/// The task runs after all currently-queued macrotasks, and after all microtasks.
|
||||
@@ -670,8 +669,8 @@ struct ThreadState {
|
||||
driver: Driver,
|
||||
shared: Arc<ThreadShared>,
|
||||
worker_completion: Option<Arc<WorkerCompletion>>,
|
||||
local_microtasks: RefCell<VecDeque<LocalTask>>,
|
||||
local_macrotasks: RefCell<VecDeque<MacroTask>>,
|
||||
local_microtasks: RefCell<LocalTaskQueue>,
|
||||
local_macrotasks: RefCell<MacroTaskQueue<MacroTask>>,
|
||||
timers: RefCell<TimerHeap>,
|
||||
/// Zero-delay intervals bypasses the timer heap entirely. Each entry
|
||||
/// re-enqueues itself as a macrotask on every turn.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub(crate) mod runtime_shared;
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
pub mod linux_x86_64;
|
||||
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||
|
||||
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>;
|
||||
@@ -2,42 +2,48 @@
|
||||
|
||||
use std::io;
|
||||
use std::os::fd::RawFd;
|
||||
use std::time::Duration;
|
||||
|
||||
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<()> {
|
||||
let (future, handle) = completion_for_current_thread::<io::Result<()>>();
|
||||
std::thread::spawn(move || {
|
||||
let mut pollfd = libc::pollfd {
|
||||
fd,
|
||||
events: libc::POLLIN | libc::POLLERR | libc::POLLHUP,
|
||||
revents: 0,
|
||||
};
|
||||
loop {
|
||||
let result = unsafe { libc::poll(&mut pollfd, 1, -1) };
|
||||
if result > 0 {
|
||||
handle.complete(Ok(()));
|
||||
return;
|
||||
}
|
||||
if result == 0 {
|
||||
continue;
|
||||
}
|
||||
wait_fd_readiness(fd, FdInterest::Readable).await
|
||||
}
|
||||
|
||||
let error = io::Error::last_os_error();
|
||||
if error.kind() == io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
handle.complete(Err(error));
|
||||
return;
|
||||
/// 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
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn _duration_to_poll_timeout(duration: Duration) -> i32 {
|
||||
duration.as_millis().min(i32::MAX as u128) as i32
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ 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};
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::task::{Context, Poll, Waker};
|
||||
use std::thread;
|
||||
|
||||
@@ -17,6 +18,10 @@ 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();
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ExecutionPath {
|
||||
Offload,
|
||||
@@ -227,12 +232,9 @@ impl ReadDirStream {
|
||||
let state = Arc::new(ReadDirState::new(current_thread_handle()));
|
||||
let producer = Arc::clone(&state);
|
||||
|
||||
if let Err(error) = thread::Builder::new()
|
||||
.name("ruin-runtime-read-dir".into())
|
||||
.spawn(move || produce_dir_entries(path, producer))
|
||||
{
|
||||
if let Err(error) = spawn_blocking(Box::new(move || produce_dir_entries(path, producer))) {
|
||||
state.release_pending();
|
||||
return Err(io::Error::other(error));
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(Self { state })
|
||||
@@ -357,12 +359,76 @@ 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>>();
|
||||
thread::spawn(move || {
|
||||
handle.complete(work());
|
||||
});
|
||||
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::Sender<BlockingTask>,
|
||||
}
|
||||
|
||||
impl BlockingPool {
|
||||
fn spawn(&self, task: BlockingTask) -> io::Result<()> {
|
||||
self.sender.send(task).map_err(|_| {
|
||||
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::channel::<BlockingTask>();
|
||||
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(
|
||||
|
||||
@@ -23,11 +23,24 @@ type ShutdownFuture = Pin<Box<dyn Future<Output = io::Result<()>> + 'static>>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ExecutionPath {
|
||||
Kqueue,
|
||||
Offload,
|
||||
}
|
||||
|
||||
pub fn execution_path(_op: &NetOp) -> ExecutionPath {
|
||||
ExecutionPath::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>>
|
||||
@@ -59,7 +72,7 @@ pub async fn socket(op: NetOp) -> io::Result<OwnedFd> {
|
||||
unreachable!("socket backend called with non-socket op");
|
||||
};
|
||||
|
||||
offload(move || socket_sync(domain, socket_type, protocol, flags)).await
|
||||
socket_sync(domain, socket_type, protocol, flags)
|
||||
}
|
||||
|
||||
pub async fn connect(op: NetOp) -> io::Result<()> {
|
||||
@@ -67,7 +80,7 @@ pub async fn connect(op: NetOp) -> io::Result<()> {
|
||||
unreachable!("connect backend called with non-connect op");
|
||||
};
|
||||
|
||||
offload(move || connect_sync(fd, RawSocketAddr::from_socket_addr(addr))).await
|
||||
connect_async(fd, RawSocketAddr::from_socket_addr(addr)).await
|
||||
}
|
||||
|
||||
pub async fn bind(op: NetOp) -> io::Result<()> {
|
||||
@@ -75,7 +88,7 @@ pub async fn bind(op: NetOp) -> io::Result<()> {
|
||||
unreachable!("bind backend called with non-bind op");
|
||||
};
|
||||
|
||||
offload(move || bind_sync(fd, RawSocketAddr::from_socket_addr(addr))).await
|
||||
bind_sync(fd, RawSocketAddr::from_socket_addr(addr))
|
||||
}
|
||||
|
||||
pub async fn listen(op: NetOp) -> io::Result<()> {
|
||||
@@ -83,7 +96,7 @@ pub async fn listen(op: NetOp) -> io::Result<()> {
|
||||
unreachable!("listen backend called with non-listen op");
|
||||
};
|
||||
|
||||
offload(move || listen_sync(fd, backlog)).await
|
||||
listen_sync(fd, backlog)
|
||||
}
|
||||
|
||||
pub async fn accept(op: NetOp) -> io::Result<AcceptedSocket> {
|
||||
@@ -91,7 +104,7 @@ pub async fn accept(op: NetOp) -> io::Result<AcceptedSocket> {
|
||||
unreachable!("accept backend called with non-accept op");
|
||||
};
|
||||
|
||||
offload(move || accept_sync(fd)).await
|
||||
accept_async(fd).await
|
||||
}
|
||||
|
||||
pub async fn send(op: NetOp) -> io::Result<usize> {
|
||||
@@ -99,7 +112,7 @@ pub async fn send(op: NetOp) -> io::Result<usize> {
|
||||
unreachable!("send backend called with non-send op");
|
||||
};
|
||||
|
||||
offload(move || send_sync(fd, data, flags)).await
|
||||
send_async(fd, data, flags).await
|
||||
}
|
||||
|
||||
pub async fn send_to(op: NetOp) -> io::Result<usize> {
|
||||
@@ -113,7 +126,7 @@ pub async fn send_to(op: NetOp) -> io::Result<usize> {
|
||||
unreachable!("send_to backend called with non-send_to op");
|
||||
};
|
||||
|
||||
offload(move || send_to_sync(fd, target, data, flags)).await
|
||||
send_to_async(fd, target, data, flags).await
|
||||
}
|
||||
|
||||
pub async fn recv(op: NetOp) -> io::Result<Vec<u8>> {
|
||||
@@ -121,7 +134,7 @@ pub async fn recv(op: NetOp) -> io::Result<Vec<u8>> {
|
||||
unreachable!("recv backend called with non-recv op");
|
||||
};
|
||||
|
||||
offload(move || recv_sync(fd, len, flags)).await
|
||||
recv_async(fd, len, flags).await
|
||||
}
|
||||
|
||||
pub async fn recv_from(op: NetOp) -> io::Result<ReceivedDatagram> {
|
||||
@@ -129,7 +142,7 @@ pub async fn recv_from(op: NetOp) -> io::Result<ReceivedDatagram> {
|
||||
unreachable!("recv_from backend called with non-recv_from op");
|
||||
};
|
||||
|
||||
offload(move || recv_from_sync(fd, len, flags)).await
|
||||
recv_from_async(fd, len, flags).await
|
||||
}
|
||||
|
||||
pub async fn shutdown(op: NetOp) -> io::Result<()> {
|
||||
@@ -137,7 +150,7 @@ pub async fn shutdown(op: NetOp) -> io::Result<()> {
|
||||
unreachable!("shutdown backend called with non-shutdown op");
|
||||
};
|
||||
|
||||
offload(move || shutdown_sync(fd, how)).await
|
||||
shutdown_sync(fd, how)
|
||||
}
|
||||
|
||||
pub async fn close(op: NetOp) -> io::Result<()> {
|
||||
@@ -145,7 +158,7 @@ pub async fn close(op: NetOp) -> io::Result<()> {
|
||||
unreachable!("close backend called with non-close op");
|
||||
};
|
||||
|
||||
offload(move || close_sync(fd)).await
|
||||
close_sync(fd)
|
||||
}
|
||||
|
||||
pub async fn connect_stream(addr: SocketAddr) -> io::Result<OwnedFd> {
|
||||
@@ -234,11 +247,9 @@ async fn bind_datagram_inner(addr: SocketAddr) -> io::Result<OwnedFd> {
|
||||
}
|
||||
|
||||
pub async fn duplicate(fd: RawFd) -> io::Result<OwnedFd> {
|
||||
offload(move || {
|
||||
let duplicated = cvt(unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) })?;
|
||||
Ok(unsafe { OwnedFd::from_raw_fd(duplicated) })
|
||||
})
|
||||
.await
|
||||
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(
|
||||
@@ -247,11 +258,7 @@ pub async fn recv_timeout(
|
||||
flags: i32,
|
||||
timeout: Duration,
|
||||
) -> io::Result<Vec<u8>> {
|
||||
offload(move || {
|
||||
wait_for_fd(fd, libc::POLLIN, timeout)?;
|
||||
recv_sync(fd, len, flags)
|
||||
})
|
||||
.await
|
||||
io_timeout(timeout, recv_async(fd, len, flags)).await
|
||||
}
|
||||
|
||||
pub async fn send_timeout(
|
||||
@@ -260,11 +267,7 @@ pub async fn send_timeout(
|
||||
flags: i32,
|
||||
timeout: Duration,
|
||||
) -> io::Result<usize> {
|
||||
offload(move || {
|
||||
wait_for_fd(fd, libc::POLLOUT, timeout)?;
|
||||
send_sync(fd, data, flags)
|
||||
})
|
||||
.await
|
||||
io_timeout(timeout, send_async(fd, data, flags)).await
|
||||
}
|
||||
|
||||
pub async fn recv_from_timeout(
|
||||
@@ -273,11 +276,7 @@ pub async fn recv_from_timeout(
|
||||
flags: i32,
|
||||
timeout: Duration,
|
||||
) -> io::Result<ReceivedDatagram> {
|
||||
offload(move || {
|
||||
wait_for_fd(fd, libc::POLLIN, timeout)?;
|
||||
recv_from_sync(fd, len, flags)
|
||||
})
|
||||
.await
|
||||
io_timeout(timeout, recv_from_async(fd, len, flags)).await
|
||||
}
|
||||
|
||||
pub async fn send_to_timeout(
|
||||
@@ -287,15 +286,21 @@ pub async fn send_to_timeout(
|
||||
flags: i32,
|
||||
timeout: Duration,
|
||||
) -> io::Result<usize> {
|
||||
offload(move || {
|
||||
wait_for_fd(fd, libc::POLLOUT, timeout)?;
|
||||
send_to_sync(fd, target, data, flags)
|
||||
})
|
||||
.await
|
||||
io_timeout(timeout, send_to_async(fd, target, data, flags)).await
|
||||
}
|
||||
|
||||
pub async fn connect_stream_timeout(addr: SocketAddr, timeout: Duration) -> io::Result<OwnedFd> {
|
||||
offload(move || connect_stream_timeout_sync(addr, timeout)).await
|
||||
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> {
|
||||
@@ -396,6 +401,15 @@ async fn offload<T: Send + 'static>(
|
||||
future.await
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -573,11 +587,28 @@ impl RawSocketAddr {
|
||||
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) })
|
||||
}
|
||||
|
||||
fn connect_sync(fd: RawFd, addr: RawSocketAddr) -> io::Result<()> {
|
||||
cvt(unsafe { libc::connect(fd, addr.as_ptr(), addr.len()) }).map(|_| ())
|
||||
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<()> {
|
||||
@@ -602,12 +633,59 @@ fn accept_sync(fd: RawFd) -> io::Result<AcceptedSocket> {
|
||||
})
|
||||
}
|
||||
|
||||
fn send_sync(fd: RawFd, data: Vec<u8>, flags: i32) -> io::Result<usize> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn send_to_sync(fd: RawFd, target: SocketAddr, data: Vec<u8>, flags: i32) -> io::Result<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(
|
||||
@@ -622,6 +700,19 @@ fn send_to_sync(fd: RawFd, target: SocketAddr, data: Vec<u8>, flags: i32) -> io:
|
||||
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) };
|
||||
@@ -630,6 +721,19 @@ fn recv_sync(fd: RawFd, len: usize, flags: i32) -> io::Result<Vec<u8>> {
|
||||
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();
|
||||
@@ -659,79 +763,6 @@ fn close_sync(fd: RawFd) -> io::Result<()> {
|
||||
cvt(unsafe { libc::close(fd) }).map(|_| ())
|
||||
}
|
||||
|
||||
fn connect_stream_timeout_sync(addr: SocketAddr, timeout: Duration) -> io::Result<OwnedFd> {
|
||||
let fd = cvt(unsafe { libc::socket(socket_domain(addr), libc::SOCK_STREAM, 0) })?;
|
||||
set_cloexec(fd)?;
|
||||
set_nonblocking(fd)?;
|
||||
|
||||
let raw = RawSocketAddr::from_socket_addr(addr);
|
||||
let connect_result = unsafe { libc::connect(fd, raw.as_ptr(), raw.len()) };
|
||||
if connect_result < 0 {
|
||||
let error = io::Error::last_os_error();
|
||||
if error.raw_os_error() != Some(libc::EINPROGRESS) {
|
||||
let _ = unsafe { libc::close(fd) };
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
let wait = wait_for_fd(fd, libc::POLLOUT, timeout);
|
||||
if let Err(error) = wait {
|
||||
let _ = unsafe { libc::close(fd) };
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let mut so_error: libc::c_int = 0;
|
||||
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
|
||||
if unsafe {
|
||||
libc::getsockopt(
|
||||
fd,
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_ERROR,
|
||||
&mut so_error as *mut libc::c_int as *mut c_void,
|
||||
&mut len,
|
||||
)
|
||||
} < 0
|
||||
{
|
||||
let error = io::Error::last_os_error();
|
||||
let _ = unsafe { libc::close(fd) };
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
if so_error != 0 {
|
||||
let _ = unsafe { libc::close(fd) };
|
||||
return Err(io::Error::from_raw_os_error(so_error));
|
||||
}
|
||||
|
||||
clear_nonblocking(fd)?;
|
||||
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
|
||||
}
|
||||
|
||||
fn wait_for_fd(fd: RawFd, events: i16, timeout: Duration) -> io::Result<()> {
|
||||
let timeout_ms = timeout.as_millis().min(i32::MAX as u128) as i32;
|
||||
let mut pfd = libc::pollfd {
|
||||
fd,
|
||||
events,
|
||||
revents: 0,
|
||||
};
|
||||
loop {
|
||||
let result = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
|
||||
if result > 0 {
|
||||
return Ok(());
|
||||
}
|
||||
if result == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"operation timed out",
|
||||
));
|
||||
}
|
||||
let error = io::Error::last_os_error();
|
||||
if error.kind() == io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
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) })?;
|
||||
@@ -744,12 +775,6 @@ fn set_nonblocking(fd: RawFd) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_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!(
|
||||
@@ -758,6 +783,25 @@ fn should_try_ipv4_loopback(addr: SocketAddr, error: &io::Error) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
@@ -2611,4 +2611,38 @@ mod tests {
|
||||
"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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,17 +73,83 @@ struct RunLoopWakeHandle {
|
||||
|
||||
struct MacosWindow {
|
||||
spec: WindowSpec,
|
||||
lifecycle: WindowLifecycle,
|
||||
state: MacosWindowState,
|
||||
configuration: WindowConfigured,
|
||||
latest_scene: Option<SceneSnapshot>,
|
||||
clipboard_text: Option<String>,
|
||||
desired_visible: bool,
|
||||
cursor_icon: CursorIcon,
|
||||
resize_pipeline: ResizePipeline,
|
||||
native: Option<NativeWindow>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct MacosWindowState {
|
||||
lifecycle: WindowLifecycle,
|
||||
desired_visible: bool,
|
||||
close_requested_sent: bool,
|
||||
close_in_progress: bool,
|
||||
has_presented_frame: bool,
|
||||
resize_pipeline: ResizePipeline,
|
||||
native: Option<NativeWindow>,
|
||||
}
|
||||
|
||||
impl MacosWindowState {
|
||||
fn new(desired_visible: bool) -> Self {
|
||||
Self {
|
||||
lifecycle: WindowLifecycle::LogicalClosed,
|
||||
desired_visible,
|
||||
close_requested_sent: false,
|
||||
close_in_progress: false,
|
||||
has_presented_frame: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn open_with_visibility(&mut self, visible: bool) {
|
||||
self.lifecycle = lifecycle_for_visibility(visible);
|
||||
self.desired_visible = visible;
|
||||
self.close_requested_sent = false;
|
||||
self.close_in_progress = false;
|
||||
}
|
||||
|
||||
fn set_native_visibility(&mut self, visible: bool) {
|
||||
self.lifecycle = lifecycle_for_visibility(visible);
|
||||
if visible {
|
||||
self.close_requested_sent = false;
|
||||
} else {
|
||||
self.has_presented_frame = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn request_logical_close(&mut self) {
|
||||
self.desired_visible = false;
|
||||
self.close_in_progress = true;
|
||||
}
|
||||
|
||||
fn finalize_close(&mut self) {
|
||||
self.lifecycle = WindowLifecycle::LogicalClosed;
|
||||
self.desired_visible = false;
|
||||
self.close_in_progress = false;
|
||||
self.has_presented_frame = false;
|
||||
}
|
||||
|
||||
fn note_presented(&mut self) {
|
||||
self.has_presented_frame = true;
|
||||
}
|
||||
|
||||
fn mark_close_requested_sent(&mut self) -> bool {
|
||||
if self.close_requested_sent {
|
||||
false
|
||||
} else {
|
||||
self.close_requested_sent = true;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lifecycle_for_visibility(visible: bool) -> WindowLifecycle {
|
||||
if visible {
|
||||
WindowLifecycle::OpenVisible
|
||||
} else {
|
||||
WindowLifecycle::OpenHidden
|
||||
}
|
||||
}
|
||||
|
||||
struct NativeWindow {
|
||||
@@ -95,6 +161,62 @@ struct NativeWindow {
|
||||
renderer: Option<WgpuSceneRenderer>,
|
||||
}
|
||||
|
||||
impl NativeWindow {
|
||||
fn window_ptr(&self) -> usize {
|
||||
self.window as usize
|
||||
}
|
||||
|
||||
fn set_title(&self, title: &str) {
|
||||
let title = NSString::from_str(title);
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![self.window, setTitle: &*title];
|
||||
}
|
||||
}
|
||||
|
||||
fn set_content_size(&self, size: UiSize) {
|
||||
let content_size = CGSize::new(size.width as f64, size.height as f64);
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![self.window, setContentSize: content_size];
|
||||
}
|
||||
}
|
||||
|
||||
fn show(&self) {
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![self.window, makeKeyAndOrderFront: std::ptr::null_mut::<AnyObject>()];
|
||||
}
|
||||
}
|
||||
|
||||
fn hide(&self) {
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![self.window, orderOut: std::ptr::null_mut::<AnyObject>()];
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![self.window, close];
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_display(&self) -> bool {
|
||||
let in_live_resize: bool = unsafe { objc2::msg_send![self.view, inLiveResize] };
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![self.view, displayIfNeeded];
|
||||
let _: () = objc2::msg_send![self.window, displayIfNeeded];
|
||||
let _: () = objc2::msg_send![objc2::class!(CATransaction), flush];
|
||||
}
|
||||
in_live_resize
|
||||
}
|
||||
|
||||
fn detach_delegate(&self) {
|
||||
unsafe {
|
||||
let _: () =
|
||||
objc2::msg_send![self.window, setDelegate: std::ptr::null_mut::<AnyObject>()];
|
||||
let _: () = objc2::msg_send![self.delegate, release];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DelegateState {
|
||||
pending_close_requests: Vec<usize>,
|
||||
@@ -124,7 +246,6 @@ enum DelegateInputEvent {
|
||||
thread_local! {
|
||||
static ACTIVE_MACOS_STATE: RefCell<Option<Rc<RefCell<MacosState>>>> = const { RefCell::new(None) };
|
||||
static APPKIT_PUMP_ACTIVE: Cell<bool> = const { Cell::new(false) };
|
||||
static IMMEDIATE_DELEGATE_FLUSH_ACTIVE: Cell<bool> = const { Cell::new(false) };
|
||||
}
|
||||
|
||||
fn run_loop_wake_handle() -> &'static Mutex<Option<RunLoopWakeHandle>> {
|
||||
@@ -182,7 +303,7 @@ impl MacosWindow {
|
||||
.unwrap_or_else(|| UiSize::new(960.0, 640.0));
|
||||
Self {
|
||||
spec,
|
||||
lifecycle: WindowLifecycle::LogicalClosed,
|
||||
state: MacosWindowState::new(desired_visible),
|
||||
configuration: WindowConfigured {
|
||||
actual_inner_size,
|
||||
scale_factor: 1.0,
|
||||
@@ -192,11 +313,7 @@ impl MacosWindow {
|
||||
},
|
||||
latest_scene: None,
|
||||
clipboard_text: None,
|
||||
desired_visible,
|
||||
cursor_icon: CursorIcon::Default,
|
||||
close_requested_sent: false,
|
||||
close_in_progress: false,
|
||||
has_presented_frame: false,
|
||||
resize_pipeline: ResizePipeline::default(),
|
||||
native: None,
|
||||
}
|
||||
@@ -494,7 +611,7 @@ fn window_delegate_class() -> &'static AnyClass {
|
||||
"windowShouldClose:"
|
||||
);
|
||||
queue_delegate_close_request(sender);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
Bool::YES
|
||||
}
|
||||
extern "C-unwind" fn did_close(_this: &AnyObject, _cmd: Sel, notification: *mut AnyObject) {
|
||||
@@ -505,7 +622,7 @@ fn window_delegate_class() -> &'static AnyClass {
|
||||
"windowWillClose:"
|
||||
);
|
||||
queue_delegate_window_closed(window);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
extern "C-unwind" fn did_resize(
|
||||
_this: &AnyObject,
|
||||
@@ -535,7 +652,7 @@ fn window_delegate_class() -> &'static AnyClass {
|
||||
UiSize::new(frame.size.width as f32, frame.size.height as f32),
|
||||
(scale_factor as f32).max(1.0),
|
||||
);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
extern "C-unwind" fn did_change_occlusion_state(
|
||||
_this: &AnyObject,
|
||||
@@ -636,7 +753,7 @@ fn content_view_class() -> &'static AnyClass {
|
||||
|
||||
extern "C-unwind" fn mouse_moved(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) {
|
||||
queue_pointer_from_view_event(this, event, PointerEventKind::Move);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
|
||||
extern "C-unwind" fn left_mouse_down(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) {
|
||||
@@ -647,7 +764,7 @@ fn content_view_class() -> &'static AnyClass {
|
||||
button: PointerButton::Primary,
|
||||
},
|
||||
);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
|
||||
extern "C-unwind" fn left_mouse_up(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) {
|
||||
@@ -658,7 +775,7 @@ fn content_view_class() -> &'static AnyClass {
|
||||
button: PointerButton::Primary,
|
||||
},
|
||||
);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
|
||||
extern "C-unwind" fn left_mouse_dragged(
|
||||
@@ -667,7 +784,7 @@ fn content_view_class() -> &'static AnyClass {
|
||||
event: *mut AnyObject,
|
||||
) {
|
||||
queue_pointer_from_view_event(this, event, PointerEventKind::Move);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
|
||||
extern "C-unwind" fn other_mouse_down(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) {
|
||||
@@ -680,7 +797,7 @@ fn content_view_class() -> &'static AnyClass {
|
||||
button: PointerButton::Middle,
|
||||
},
|
||||
);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,7 +811,7 @@ fn content_view_class() -> &'static AnyClass {
|
||||
button: PointerButton::Middle,
|
||||
},
|
||||
);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,7 +823,7 @@ fn content_view_class() -> &'static AnyClass {
|
||||
let button_number: usize = unsafe { objc2::msg_send![event, buttonNumber] };
|
||||
if button_number == 2 {
|
||||
queue_pointer_from_view_event(this, event, PointerEventKind::Move);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,17 +837,17 @@ fn content_view_class() -> &'static AnyClass {
|
||||
delta: Point::new(delta_x as f32, -(delta_y as f32)),
|
||||
},
|
||||
);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
|
||||
extern "C-unwind" fn key_down(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) {
|
||||
queue_keyboard_from_view_event(this, event, KeyboardEventKind::Pressed);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
|
||||
extern "C-unwind" fn key_up(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) {
|
||||
queue_keyboard_from_view_event(this, event, KeyboardEventKind::Released);
|
||||
flush_delegate_events_immediately();
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
|
||||
let class_name = CString::new("RuinContentView").expect("class name is valid");
|
||||
@@ -851,47 +968,6 @@ fn try_enter_appkit_pump() -> Option<AppKitPumpGuard> {
|
||||
})
|
||||
}
|
||||
|
||||
struct ImmediateDelegateFlushGuard;
|
||||
|
||||
impl Drop for ImmediateDelegateFlushGuard {
|
||||
fn drop(&mut self) {
|
||||
IMMEDIATE_DELEGATE_FLUSH_ACTIVE.with(|flag| flag.set(false));
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_delegate_events_immediately() {
|
||||
let Some(_guard) = IMMEDIATE_DELEGATE_FLUSH_ACTIVE.with(|flag| {
|
||||
if flag.replace(true) {
|
||||
None
|
||||
} else {
|
||||
Some(ImmediateDelegateFlushGuard)
|
||||
}
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(state) = ACTIVE_MACOS_STATE.with(|slot| slot.borrow().as_ref().cloned()) else {
|
||||
return;
|
||||
};
|
||||
let mut pending = Vec::new();
|
||||
{
|
||||
let Ok(mut state_ref) = state.try_borrow_mut() else {
|
||||
return;
|
||||
};
|
||||
apply_delegate_events(&mut state_ref, &mut pending);
|
||||
}
|
||||
if pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
trace!(
|
||||
target: "ruin_ui_platform_macos::events",
|
||||
count = pending.len(),
|
||||
"flushing delegate events immediately during AppKit callback"
|
||||
);
|
||||
let sender = state.borrow().endpoint_events.clone();
|
||||
flush_pending_events(&sender, &mut pending);
|
||||
}
|
||||
|
||||
fn drain_macos_run_loop_work(state: &Rc<RefCell<MacosState>>) {
|
||||
let Some(_guard) = try_enter_appkit_pump() else {
|
||||
return;
|
||||
@@ -1021,15 +1097,8 @@ fn handle_create_window(state: &Rc<RefCell<MacosState>>, window_id: WindowId, sp
|
||||
if let Some(window) = state_ref.windows.get_mut(&window_id) {
|
||||
ensure_native_window(window, app);
|
||||
sync_window_metrics(window);
|
||||
window.lifecycle = if visible {
|
||||
WindowLifecycle::OpenVisible
|
||||
} else {
|
||||
WindowLifecycle::OpenHidden
|
||||
};
|
||||
window.state.open_with_visibility(visible);
|
||||
window.configuration.visible = visible;
|
||||
window.desired_visible = visible;
|
||||
window.close_requested_sent = false;
|
||||
window.close_in_progress = false;
|
||||
let _ = window
|
||||
.resize_pipeline
|
||||
.ensure_initial_request(window.configuration.actual_inner_size);
|
||||
@@ -1082,46 +1151,37 @@ fn handle_update_window(
|
||||
if let Some(title) = update.title {
|
||||
window.spec.title = title;
|
||||
if let Some(native) = window.native.as_ref() {
|
||||
let title = NSString::from_str(&window.spec.title);
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![native.window, setTitle: &*title];
|
||||
}
|
||||
native.set_title(&window.spec.title);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(open) = update.open {
|
||||
if open {
|
||||
if window.lifecycle == WindowLifecycle::LogicalClosed {
|
||||
if window.state.lifecycle == WindowLifecycle::LogicalClosed {
|
||||
ensure_native_window(window, app);
|
||||
sync_window_metrics(window);
|
||||
window.lifecycle = if window.configuration.visible {
|
||||
WindowLifecycle::OpenVisible
|
||||
} else {
|
||||
WindowLifecycle::OpenHidden
|
||||
};
|
||||
window.close_requested_sent = false;
|
||||
window.close_in_progress = false;
|
||||
window
|
||||
.state
|
||||
.open_with_visibility(window.configuration.visible);
|
||||
sync_visibility(window);
|
||||
apply_cursor_icon(window.cursor_icon);
|
||||
emit_opened = true;
|
||||
}
|
||||
} else if window.lifecycle != WindowLifecycle::LogicalClosed {
|
||||
} else if window.state.lifecycle != WindowLifecycle::LogicalClosed {
|
||||
debug!(
|
||||
target: "ruin_ui_platform_macos::lifecycle",
|
||||
?window_id,
|
||||
"platform received open(false), closing native window"
|
||||
);
|
||||
window.desired_visible = false;
|
||||
window.state.request_logical_close();
|
||||
if window.native.is_some() {
|
||||
window.close_in_progress = true;
|
||||
close_native_window(window);
|
||||
// Keep behavior aligned with other current platform backends:
|
||||
// treat a requested close as immediately closed for app shutdown.
|
||||
window.lifecycle = WindowLifecycle::LogicalClosed;
|
||||
window.close_in_progress = false;
|
||||
window.state.finalize_close();
|
||||
emit_closed = true;
|
||||
} else {
|
||||
window.lifecycle = WindowLifecycle::LogicalClosed;
|
||||
window.state.finalize_close();
|
||||
emit_closed = true;
|
||||
}
|
||||
}
|
||||
@@ -1131,15 +1191,7 @@ fn handle_update_window(
|
||||
&& window.configuration.visible != visible
|
||||
{
|
||||
window.configuration.visible = visible;
|
||||
window.desired_visible = visible;
|
||||
window.lifecycle = if visible {
|
||||
WindowLifecycle::OpenVisible
|
||||
} else {
|
||||
WindowLifecycle::OpenHidden
|
||||
};
|
||||
if visible {
|
||||
window.close_requested_sent = false;
|
||||
}
|
||||
window.state.open_with_visibility(visible);
|
||||
sync_visibility(window);
|
||||
emit_visibility = Some(visible);
|
||||
}
|
||||
@@ -1269,7 +1321,7 @@ fn try_present_scene(
|
||||
return None;
|
||||
}
|
||||
|
||||
window.has_presented_frame = true;
|
||||
window.state.note_presented();
|
||||
flush_presented_frame(window);
|
||||
if let Some(next_resize) = finish_presented_resize(window, scene_viewport) {
|
||||
trace!(
|
||||
@@ -1304,7 +1356,7 @@ fn repaint_retained_scene_for_resize(window_id: WindowId, window: &mut MacosWind
|
||||
"repainting retained scene into resized drawable"
|
||||
);
|
||||
if render_scene_to_current_drawable(window_id, window, &scene) {
|
||||
window.has_presented_frame = true;
|
||||
window.state.note_presented();
|
||||
flush_presented_frame(window);
|
||||
}
|
||||
}
|
||||
@@ -1486,10 +1538,7 @@ fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option<Nat
|
||||
}
|
||||
|
||||
fn resize_native_window(native: &mut NativeWindow, size: UiSize) {
|
||||
let content_size = CGSize::new(size.width as f64, size.height as f64);
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![native.window, setContentSize: content_size];
|
||||
}
|
||||
native.set_content_size(size);
|
||||
}
|
||||
|
||||
fn sync_visibility(window: &MacosWindow) {
|
||||
@@ -1498,14 +1547,9 @@ fn sync_visibility(window: &MacosWindow) {
|
||||
};
|
||||
|
||||
if window.configuration.visible {
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![native.window, makeKeyAndOrderFront: std::ptr::null_mut::<AnyObject>()];
|
||||
}
|
||||
native.show();
|
||||
} else {
|
||||
unsafe {
|
||||
let _: () =
|
||||
objc2::msg_send![native.window, orderOut: std::ptr::null_mut::<AnyObject>()];
|
||||
}
|
||||
native.hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1513,12 +1557,10 @@ fn close_native_window(window: &mut MacosWindow) {
|
||||
if let Some(native) = window.native.as_ref() {
|
||||
debug!(
|
||||
target: "ruin_ui_platform_macos::lifecycle",
|
||||
window_ptr = native.window as usize,
|
||||
window_ptr = native.window_ptr(),
|
||||
"sending NSWindow close"
|
||||
);
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![native.window, close];
|
||||
}
|
||||
native.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1534,11 +1576,11 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
|
||||
window
|
||||
.native
|
||||
.as_ref()
|
||||
.is_some_and(|native| native.window as usize == resize.window_ptr)
|
||||
.is_some_and(|native| native.window_ptr() == resize.window_ptr)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
if window.lifecycle == WindowLifecycle::LogicalClosed {
|
||||
if window.state.lifecycle == WindowLifecycle::LogicalClosed {
|
||||
continue;
|
||||
}
|
||||
let resized = resize.size.width > 0.0
|
||||
@@ -1563,11 +1605,11 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
|
||||
window
|
||||
.native
|
||||
.as_ref()
|
||||
.is_some_and(|native| native.window as usize == window_ptr)
|
||||
.is_some_and(|native| native.window_ptr() == window_ptr)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
if window.lifecycle != WindowLifecycle::LogicalClosed {
|
||||
if window.state.lifecycle != WindowLifecycle::LogicalClosed {
|
||||
pending.push(PlatformEvent::Pointer {
|
||||
window_id: *window_id,
|
||||
event,
|
||||
@@ -1579,11 +1621,11 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
|
||||
window
|
||||
.native
|
||||
.as_ref()
|
||||
.is_some_and(|native| native.window as usize == window_ptr)
|
||||
.is_some_and(|native| native.window_ptr() == window_ptr)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
if window.lifecycle != WindowLifecycle::LogicalClosed {
|
||||
if window.state.lifecycle != WindowLifecycle::LogicalClosed {
|
||||
pending.push(PlatformEvent::Keyboard {
|
||||
window_id: *window_id,
|
||||
event,
|
||||
@@ -1598,12 +1640,11 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
|
||||
window
|
||||
.native
|
||||
.as_ref()
|
||||
.is_some_and(|native| native.window as usize == window_ptr)
|
||||
.is_some_and(|native| native.window_ptr() == window_ptr)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
if !window.close_requested_sent {
|
||||
window.close_requested_sent = true;
|
||||
if window.state.mark_close_requested_sent() {
|
||||
debug!(
|
||||
target: "ruin_ui_platform_macos::lifecycle",
|
||||
?window_id,
|
||||
@@ -1620,7 +1661,7 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
|
||||
window
|
||||
.native
|
||||
.as_ref()
|
||||
.is_some_and(|native| native.window as usize == window_ptr)
|
||||
.is_some_and(|native| native.window_ptr() == window_ptr)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
@@ -1632,7 +1673,7 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
|
||||
&& state
|
||||
.windows
|
||||
.values()
|
||||
.all(|window| window.lifecycle == WindowLifecycle::LogicalClosed)
|
||||
.all(|window| window.state.lifecycle == WindowLifecycle::LogicalClosed)
|
||||
{
|
||||
debug!(
|
||||
target: "ruin_ui_platform_macos::lifecycle",
|
||||
@@ -1703,12 +1744,7 @@ fn flush_presented_frame(window: &MacosWindow) {
|
||||
let Some(native) = window.native.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let in_live_resize: bool = unsafe { objc2::msg_send![native.view, inLiveResize] };
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![native.view, displayIfNeeded];
|
||||
let _: () = objc2::msg_send![native.window, displayIfNeeded];
|
||||
let _: () = objc2::msg_send![objc2::class!(CATransaction), flush];
|
||||
}
|
||||
let in_live_resize = native.flush_display();
|
||||
trace!(
|
||||
target: "ruin_ui_platform_macos::resize",
|
||||
in_live_resize,
|
||||
@@ -1765,7 +1801,7 @@ fn collect_window_state_events(
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if window.lifecycle == WindowLifecycle::LogicalClosed {
|
||||
if window.state.lifecycle == WindowLifecycle::LogicalClosed {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1788,20 +1824,14 @@ fn collect_window_state_events(
|
||||
let became_visible = is_visible && !window.configuration.visible;
|
||||
if is_visible != window.configuration.visible {
|
||||
window.configuration.visible = is_visible;
|
||||
window.lifecycle = if is_visible {
|
||||
window.close_requested_sent = false;
|
||||
WindowLifecycle::OpenVisible
|
||||
} else {
|
||||
window.has_presented_frame = false;
|
||||
WindowLifecycle::OpenHidden
|
||||
};
|
||||
window.state.set_native_visibility(is_visible);
|
||||
pending.push(PlatformEvent::VisibilityChanged {
|
||||
window_id,
|
||||
visible: is_visible,
|
||||
});
|
||||
}
|
||||
|
||||
if window.close_in_progress && !is_visible {
|
||||
if window.state.close_in_progress && !is_visible {
|
||||
finalize_native_close(window_id, window, pending);
|
||||
return;
|
||||
}
|
||||
@@ -1809,12 +1839,12 @@ fn collect_window_state_events(
|
||||
if is_visible
|
||||
&& let Some(scene) = window.latest_scene.clone()
|
||||
&& (became_visible
|
||||
|| !window.has_presented_frame
|
||||
|| !window.state.has_presented_frame
|
||||
|| window.resize_pipeline.active_resize == Some(scene.logical_size))
|
||||
{
|
||||
let _ = try_present_scene(window_id, window, &scene, pending);
|
||||
} else if is_visible
|
||||
&& !window.has_presented_frame
|
||||
&& !window.state.has_presented_frame
|
||||
&& let Some(initial_viewport) = window
|
||||
.resize_pipeline
|
||||
.ensure_initial_request(window.configuration.actual_inner_size)
|
||||
@@ -1907,18 +1937,12 @@ fn finalize_native_close(
|
||||
?window_id,
|
||||
"finalizing native close"
|
||||
);
|
||||
let should_emit_closed = window.lifecycle != WindowLifecycle::LogicalClosed;
|
||||
let should_emit_closed = window.state.lifecycle != WindowLifecycle::LogicalClosed;
|
||||
if let Some(native) = window.native.take() {
|
||||
unsafe {
|
||||
let _: () =
|
||||
objc2::msg_send![native.window, setDelegate: std::ptr::null_mut::<AnyObject>()];
|
||||
let _: () = objc2::msg_send![native.delegate, release];
|
||||
}
|
||||
native.detach_delegate();
|
||||
}
|
||||
|
||||
window.close_in_progress = false;
|
||||
window.lifecycle = WindowLifecycle::LogicalClosed;
|
||||
window.desired_visible = false;
|
||||
window.state.finalize_close();
|
||||
|
||||
if window.configuration.visible {
|
||||
window.configuration.visible = false;
|
||||
@@ -1954,8 +1978,8 @@ fn sync_window_metrics(window: &mut MacosWindow) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ResizePipeline;
|
||||
use ruin_ui::UiSize;
|
||||
use super::{MacosWindowState, ResizePipeline};
|
||||
use ruin_ui::{UiSize, WindowLifecycle};
|
||||
|
||||
#[test]
|
||||
fn resize_pipeline_stages_latest_resize_while_active_request_is_in_flight() {
|
||||
@@ -2020,4 +2044,40 @@ mod tests {
|
||||
assert_eq!(pipeline.active_resize, Some(active));
|
||||
assert_eq!(pipeline.next_resize, Some(next));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_state_distinguishes_hide_from_close() {
|
||||
let mut state = MacosWindowState::new(true);
|
||||
state.open_with_visibility(true);
|
||||
state.note_presented();
|
||||
|
||||
state.set_native_visibility(false);
|
||||
assert_eq!(state.lifecycle, WindowLifecycle::OpenHidden);
|
||||
assert!(state.desired_visible);
|
||||
assert!(!state.close_in_progress);
|
||||
assert!(!state.has_presented_frame);
|
||||
|
||||
state.set_native_visibility(true);
|
||||
assert_eq!(state.lifecycle, WindowLifecycle::OpenVisible);
|
||||
assert!(state.desired_visible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_state_close_request_is_single_shot_and_finalize_resets_state() {
|
||||
let mut state = MacosWindowState::new(true);
|
||||
state.open_with_visibility(true);
|
||||
|
||||
assert!(state.mark_close_requested_sent());
|
||||
assert!(!state.mark_close_requested_sent());
|
||||
|
||||
state.request_logical_close();
|
||||
assert!(!state.desired_visible);
|
||||
assert!(state.close_in_progress);
|
||||
|
||||
state.finalize_close();
|
||||
assert_eq!(state.lifecycle, WindowLifecycle::LogicalClosed);
|
||||
assert!(!state.desired_visible);
|
||||
assert!(!state.close_in_progress);
|
||||
assert!(!state.has_presented_frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,8 +170,8 @@ struct UploadedAtlasText {
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
struct TextTextureKey {
|
||||
text: String,
|
||||
bounds: Option<(u32, u32)>,
|
||||
clip: Option<(u32, u32, u32, u32)>,
|
||||
bounds: Option<(i32, i32)>,
|
||||
clip: Option<(i32, i32, i32, i32)>,
|
||||
font_size_bits: u32,
|
||||
line_height_bits: u32,
|
||||
color: (u8, u8, u8, u8),
|
||||
@@ -1315,7 +1315,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
clip: ActiveClip,
|
||||
) -> Option<UploadedText> {
|
||||
let raster_clip = text_raster_clip(text, clip, logical_size)?;
|
||||
let key = text_texture_key(text, raster_clip);
|
||||
let key = text_texture_key(text, raster_clip, self.scale_factor);
|
||||
if !self.text_cache.contains_key(&key) {
|
||||
let rasterized = self.rasterize_text(text, raster_clip)?;
|
||||
let cached = self.create_cached_text_texture(&rasterized);
|
||||
@@ -2442,20 +2442,21 @@ fn clip_visible_glyphs(text: &PreparedText, clip: Option<Rect>) -> &[GlyphInstan
|
||||
&text.glyphs[glyph_start..glyph_end.min(text.glyphs.len())]
|
||||
}
|
||||
|
||||
fn text_texture_key(text: &PreparedText, raster_clip: Option<Rect>) -> TextTextureKey {
|
||||
fn text_texture_key(
|
||||
text: &PreparedText,
|
||||
raster_clip: Option<Rect>,
|
||||
scale_factor: f32,
|
||||
) -> TextTextureKey {
|
||||
TextTextureKey {
|
||||
text: text.text.clone(),
|
||||
bounds: text
|
||||
.bounds
|
||||
.map(|bounds| (bounds.width.to_bits(), bounds.height.to_bits())),
|
||||
clip: raster_clip.map(|clip| {
|
||||
bounds: text.bounds.map(|bounds| {
|
||||
(
|
||||
clip.origin.x.to_bits(),
|
||||
clip.origin.y.to_bits(),
|
||||
clip.size.width.to_bits(),
|
||||
clip.size.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(),
|
||||
line_height_bits: text.line_height.to_bits(),
|
||||
color: (
|
||||
@@ -2479,6 +2480,10 @@ fn text_texture_key(text: &PreparedText, raster_clip: Option<Rect>) -> TextTextu
|
||||
}
|
||||
}
|
||||
|
||||
fn pixel_rect_tuple(rect: PixelRect) -> (i32, i32, i32, i32) {
|
||||
(rect.left, rect.top, rect.right, rect.bottom)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
@@ -2539,11 +2544,30 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
text_texture_key(&first, None),
|
||||
text_texture_key(&second, None)
|
||||
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 nested_clip_with_empty_intersection_stays_clipped() {
|
||||
let mut clip_stack = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user