This commit is contained in:
Will Temple
2026-05-15 16:28:06 -07:00
parent 67400f1499
commit bfda24ad0a
13 changed files with 782 additions and 363 deletions

View File

@@ -268,7 +268,6 @@ fn RuntimeIoExample(server_addr: SocketAddr) -> impl IntoView {
};
eprintln!("example05: {message}");
let _ = save_status.set(Some(message));
flush_reactive_updates();
}
}));
}

View File

@@ -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");
}
}

View File

@@ -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>>,
}

View File

@@ -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) })?;

View File

@@ -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(&current_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.

View File

@@ -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"))]

View 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>;

View File

@@ -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;
/// 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);
}
handle.complete(Err(error));
return;
}
});
}
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
}

View File

@@ -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(

View File

@@ -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) })?;
set_nonblocking(duplicated)?;
Ok(unsafe { OwnedFd::from_raw_fd(duplicated) })
})
.await
}
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()))
}

View File

@@ -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"
);
}
}

View File

@@ -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);
}
}

View File

@@ -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();