Compare commits
4 Commits
bfda24ad0a
...
23d9f02e3a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23d9f02e3a | ||
|
|
d9ac6bfeb8 | ||
|
|
056f356065 | ||
|
|
1a083ee12c |
@@ -254,6 +254,7 @@ impl<M: Mountable> MountedApp<M> {
|
||||
break;
|
||||
};
|
||||
|
||||
let mut should_flush_reactive_work = false;
|
||||
for event in iter::once(event).chain(ui.take_pending_events()) {
|
||||
match event {
|
||||
PlatformEvent::Configured {
|
||||
@@ -268,7 +269,7 @@ impl<M: Mountable> MountedApp<M> {
|
||||
);
|
||||
let _ = viewport.set(configuration.actual_inner_size);
|
||||
configure_serial.update(|value| *value = value.wrapping_add(1));
|
||||
current_reactor().flush_now();
|
||||
should_flush_reactive_work = true;
|
||||
}
|
||||
PlatformEvent::Pointer { window_id, event } if window_id == window.id() => {
|
||||
Self::handle_pointer_event(
|
||||
@@ -279,7 +280,7 @@ impl<M: Mountable> MountedApp<M> {
|
||||
&mut input_state,
|
||||
event,
|
||||
)?;
|
||||
current_reactor().flush_now();
|
||||
should_flush_reactive_work = true;
|
||||
}
|
||||
PlatformEvent::Keyboard { window_id, event } if window_id == window.id() => {
|
||||
Self::handle_keyboard_event(
|
||||
@@ -289,7 +290,7 @@ impl<M: Mountable> MountedApp<M> {
|
||||
&input_state,
|
||||
event,
|
||||
)?;
|
||||
current_reactor().flush_now();
|
||||
should_flush_reactive_work = true;
|
||||
}
|
||||
PlatformEvent::CloseRequested { window_id } if window_id == window.id() => {
|
||||
tracing::debug!(
|
||||
@@ -311,6 +312,9 @@ impl<M: Mountable> MountedApp<M> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if should_flush_reactive_work {
|
||||
current_reactor().flush_now();
|
||||
}
|
||||
}
|
||||
|
||||
ui.shutdown()?;
|
||||
|
||||
@@ -99,7 +99,7 @@ pub struct Driver {
|
||||
pending_wakes: Cell<u64>,
|
||||
pending_timers: Cell<u64>,
|
||||
next_fd_token: Cell<u64>,
|
||||
fd_waiters: RefCell<HashMap<FdKey, Vec<FdWaiter>>>,
|
||||
fd_waiters: RefCell<HashMap<FdKey, FdWaiter>>,
|
||||
}
|
||||
|
||||
/// Creates a new driver and its paired [`ThreadNotifier`].
|
||||
@@ -241,17 +241,31 @@ impl Driver {
|
||||
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 removed_stale_waiter = {
|
||||
let mut waiters = self.fd_waiters.borrow_mut();
|
||||
match waiters.get(&key) {
|
||||
Some(waiter) if !waiter.completion.is_interested() => {
|
||||
waiters.remove(&key);
|
||||
true
|
||||
}
|
||||
Some(_) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::AlreadyExists,
|
||||
"fd readiness already has a waiter for this interest",
|
||||
));
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
};
|
||||
if removed_stale_waiter {
|
||||
let _ = self.update_fd_interest(key, libc::EV_DELETE);
|
||||
}
|
||||
|
||||
let token = self.allocate_fd_token();
|
||||
self.update_fd_interest(key, libc::EV_ADD | libc::EV_ENABLE | libc::EV_ONESHOT)?;
|
||||
self.fd_waiters
|
||||
.borrow_mut()
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(FdWaiter { token, completion });
|
||||
.insert(key, FdWaiter { token, completion });
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
@@ -259,12 +273,9 @@ impl Driver {
|
||||
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() {
|
||||
for (key, entry) in waiters.iter() {
|
||||
if entry.token == token {
|
||||
empty_key = Some(*key);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -372,19 +383,13 @@ impl Driver {
|
||||
|
||||
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 {
|
||||
let waiter = self.fd_waiters.borrow_mut().remove(&key);
|
||||
let Some(waiter) = waiter 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())),
|
||||
});
|
||||
}
|
||||
let result = fd_event_result(event, interest);
|
||||
waiter.completion.complete(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,9 +441,14 @@ fn interest_from_filter(filter: i16) -> Option<FdInterest> {
|
||||
}
|
||||
}
|
||||
|
||||
fn fd_event_result(event: &libc::kevent) -> io::Result<()> {
|
||||
fn fd_event_result(event: &libc::kevent, interest: FdInterest) -> io::Result<()> {
|
||||
if event.flags & libc::EV_ERROR != 0 && event.data != 0 {
|
||||
Err(io::Error::from_raw_os_error(event.data as i32))
|
||||
} else if event.flags & libc::EV_EOF != 0 && interest == FdInterest::Writable {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"fd write side reached EOF",
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::platform::current::runtime::{ThreadHandle, current_thread_handle};
|
||||
type BlockingTask = Box<dyn FnOnce() + Send + 'static>;
|
||||
|
||||
static BLOCKING_POOL: OnceLock<io::Result<BlockingPool>> = OnceLock::new();
|
||||
const BLOCKING_QUEUE_CAPACITY: usize = 1024;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ExecutionPath {
|
||||
@@ -367,16 +368,20 @@ async fn offload<T: Send + 'static>(
|
||||
}
|
||||
|
||||
struct BlockingPool {
|
||||
sender: mpsc::Sender<BlockingTask>,
|
||||
sender: mpsc::SyncSender<BlockingTask>,
|
||||
}
|
||||
|
||||
impl BlockingPool {
|
||||
fn spawn(&self, task: BlockingTask) -> io::Result<()> {
|
||||
self.sender.send(task).map_err(|_| {
|
||||
io::Error::new(
|
||||
self.sender.try_send(task).map_err(|error| match error {
|
||||
mpsc::TrySendError::Full(_) => io::Error::new(
|
||||
io::ErrorKind::WouldBlock,
|
||||
"filesystem blocking worker queue is full",
|
||||
),
|
||||
mpsc::TrySendError::Disconnected(_) => io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"filesystem blocking worker pool has stopped",
|
||||
)
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -393,7 +398,7 @@ fn blocking_pool() -> io::Result<&'static BlockingPool> {
|
||||
}
|
||||
|
||||
fn create_blocking_pool() -> io::Result<BlockingPool> {
|
||||
let (sender, receiver) = mpsc::channel::<BlockingTask>();
|
||||
let (sender, receiver) = mpsc::sync_channel::<BlockingTask>(BLOCKING_QUEUE_CAPACITY);
|
||||
let receiver = Arc::new(Mutex::new(receiver));
|
||||
let worker_count = std::thread::available_parallelism()
|
||||
.map(usize::from)
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::net::{
|
||||
};
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex, OnceLock, mpsc};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -16,10 +17,14 @@ use crate::op::completion::completion_for_current_thread;
|
||||
use crate::op::net::{AcceptedSocket, NetOp, ReceivedDatagram};
|
||||
|
||||
const DEFAULT_LISTENER_BACKLOG: i32 = 1024;
|
||||
const DNS_QUEUE_CAPACITY: usize = 256;
|
||||
|
||||
type RecvFuture = Pin<Box<dyn Future<Output = io::Result<Vec<u8>>> + 'static>>;
|
||||
type SendFuture = Pin<Box<dyn Future<Output = io::Result<usize>> + 'static>>;
|
||||
type ShutdownFuture = Pin<Box<dyn Future<Output = io::Result<()>> + 'static>>;
|
||||
type BlockingTask = Box<dyn FnOnce() + Send + 'static>;
|
||||
|
||||
static DNS_POOL: OnceLock<io::Result<BlockingPool>> = OnceLock::new();
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ExecutionPath {
|
||||
@@ -391,16 +396,83 @@ async fn offload<T: Send + 'static>(
|
||||
work: impl FnOnce() -> io::Result<T> + Send + 'static,
|
||||
) -> io::Result<T> {
|
||||
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
||||
let handle_for_thread = handle.clone();
|
||||
if let Err(error) = thread::Builder::new()
|
||||
.name("ruin-runtime-net-offload".into())
|
||||
.spawn(move || handle_for_thread.complete(work()))
|
||||
{
|
||||
handle.complete(Err(io::Error::other(error)));
|
||||
if let Err(error) = dns_pool().and_then(|pool| {
|
||||
pool.spawn(Box::new({
|
||||
let handle = handle.clone();
|
||||
move || handle.complete(work())
|
||||
}))
|
||||
}) {
|
||||
handle.complete(Err(error));
|
||||
}
|
||||
future.await
|
||||
}
|
||||
|
||||
struct BlockingPool {
|
||||
sender: mpsc::SyncSender<BlockingTask>,
|
||||
}
|
||||
|
||||
impl BlockingPool {
|
||||
fn spawn(&self, task: BlockingTask) -> io::Result<()> {
|
||||
self.sender.try_send(task).map_err(|error| match error {
|
||||
mpsc::TrySendError::Full(_) => io::Error::new(
|
||||
io::ErrorKind::WouldBlock,
|
||||
"DNS resolver blocking worker queue is full",
|
||||
),
|
||||
mpsc::TrySendError::Disconnected(_) => io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"DNS resolver blocking worker pool has stopped",
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn dns_pool() -> io::Result<&'static BlockingPool> {
|
||||
match DNS_POOL.get_or_init(create_dns_pool) {
|
||||
Ok(pool) => Ok(pool),
|
||||
Err(error) => Err(io::Error::new(error.kind(), error.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_dns_pool() -> io::Result<BlockingPool> {
|
||||
let (sender, receiver) = mpsc::sync_channel::<BlockingTask>(DNS_QUEUE_CAPACITY);
|
||||
let receiver = Arc::new(Mutex::new(receiver));
|
||||
let worker_count = std::thread::available_parallelism()
|
||||
.map(usize::from)
|
||||
.unwrap_or(2)
|
||||
.clamp(2, 4);
|
||||
let mut spawned = 0usize;
|
||||
let mut last_error = None;
|
||||
|
||||
for index in 0..worker_count {
|
||||
let receiver = Arc::clone(&receiver);
|
||||
match thread::Builder::new()
|
||||
.name(format!("ruin-runtime-dns-{index}"))
|
||||
.spawn(move || {
|
||||
loop {
|
||||
let task = {
|
||||
let receiver = receiver.lock().expect("DNS worker queue mutex poisoned");
|
||||
receiver.recv()
|
||||
};
|
||||
match task {
|
||||
Ok(task) => task(),
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Ok(_) => spawned += 1,
|
||||
Err(error) => last_error = Some(error),
|
||||
}
|
||||
}
|
||||
|
||||
if spawned == 0 {
|
||||
return Err(io::Error::other(last_error.expect(
|
||||
"at least one DNS worker spawn should have been attempted",
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(BlockingPool { sender })
|
||||
}
|
||||
|
||||
async fn io_timeout<T>(
|
||||
timeout: Duration,
|
||||
future: impl Future<Output = io::Result<T>>,
|
||||
@@ -531,7 +603,7 @@ impl RawSocketAddr {
|
||||
sin_family: libc::AF_INET as libc::sa_family_t,
|
||||
sin_port: addr.port().to_be(),
|
||||
sin_addr: libc::in_addr {
|
||||
s_addr: u32::from_ne_bytes(addr.ip().octets()),
|
||||
s_addr: u32::from_be_bytes(addr.ip().octets()).to_be(),
|
||||
},
|
||||
sin_zero: [0; 8],
|
||||
};
|
||||
|
||||
@@ -34,6 +34,11 @@ pub enum WindowLifecycle {
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct WindowConfigured {
|
||||
/// Current logical content size and platform presentation scale.
|
||||
///
|
||||
/// Backends should emit a new configuration when either the logical size or
|
||||
/// scale changes. This lets hosts relayout for size changes while renderers
|
||||
/// can update drawable/text scale when a window moves between displays.
|
||||
pub actual_inner_size: UiSize,
|
||||
pub scale_factor: f32,
|
||||
pub visible: bool,
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::collections::BTreeMap;
|
||||
use std::ffi::{CStr, CString, c_void};
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex, Once, OnceLock};
|
||||
use std::sync::{Arc, Mutex, MutexGuard, Once, OnceLock};
|
||||
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::{AnyClass, AnyObject, Bool, ClassBuilder, Sel};
|
||||
@@ -65,12 +65,17 @@ struct RunLoopSourceRegistration {
|
||||
_source: CFRetained<CFRunLoopSource>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone)]
|
||||
struct RunLoopWakeHandle {
|
||||
run_loop: usize,
|
||||
source: usize,
|
||||
run_loop: CFRetained<CFRunLoop>,
|
||||
source: CFRetained<CFRunLoopSource>,
|
||||
}
|
||||
|
||||
// CFRunLoopSourceSignal and CFRunLoopWakeUp are designed for cross-thread wakeups.
|
||||
// The handle owns retained references and never exposes mutable access to the CF objects.
|
||||
unsafe impl Send for RunLoopWakeHandle {}
|
||||
unsafe impl Sync for RunLoopWakeHandle {}
|
||||
|
||||
struct MacosWindow {
|
||||
spec: WindowSpec,
|
||||
state: MacosWindowState,
|
||||
@@ -91,6 +96,13 @@ struct MacosWindowState {
|
||||
has_presented_frame: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum PresentationFlushPolicy {
|
||||
None,
|
||||
DisplayIfNeeded,
|
||||
DisplayAndTransaction,
|
||||
}
|
||||
|
||||
impl MacosWindowState {
|
||||
fn new(desired_visible: bool) -> Self {
|
||||
Self {
|
||||
@@ -142,6 +154,10 @@ impl MacosWindowState {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn note_delegate_close_request(&mut self) -> bool {
|
||||
!self.close_in_progress && self.mark_close_requested_sent()
|
||||
}
|
||||
}
|
||||
|
||||
fn lifecycle_for_visibility(visible: bool) -> WindowLifecycle {
|
||||
@@ -152,68 +168,124 @@ fn lifecycle_for_visibility(visible: bool) -> WindowLifecycle {
|
||||
}
|
||||
}
|
||||
|
||||
struct ObjcStrong {
|
||||
object: Option<NonNull<AnyObject>>,
|
||||
}
|
||||
|
||||
impl ObjcStrong {
|
||||
unsafe fn from_owned(object: *mut AnyObject) -> Option<Self> {
|
||||
Some(Self {
|
||||
object: Some(NonNull::new(object)?),
|
||||
})
|
||||
}
|
||||
|
||||
fn as_ptr(&self) -> *mut AnyObject {
|
||||
self.object
|
||||
.map_or(std::ptr::null_mut(), |object| object.as_ptr())
|
||||
}
|
||||
|
||||
fn release_now(&mut self) {
|
||||
let Some(object) = self.object.take() else {
|
||||
return;
|
||||
};
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![object.as_ptr(), release];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ObjcStrong {
|
||||
fn drop(&mut self) {
|
||||
self.release_now();
|
||||
}
|
||||
}
|
||||
|
||||
struct NativeWindow {
|
||||
window: *mut AnyObject,
|
||||
#[allow(dead_code)]
|
||||
view: *mut AnyObject,
|
||||
delegate: *mut AnyObject,
|
||||
_metal_layer: Retained<CAMetalLayer>,
|
||||
// Renderer/surface must drop before the AppKit objects whose raw handles it
|
||||
// was created from.
|
||||
renderer: Option<WgpuSceneRenderer>,
|
||||
_metal_layer: Retained<CAMetalLayer>,
|
||||
view: ObjcStrong,
|
||||
delegate: Option<ObjcStrong>,
|
||||
window: ObjcStrong,
|
||||
}
|
||||
|
||||
impl NativeWindow {
|
||||
fn window_ptr(&self) -> usize {
|
||||
self.window as usize
|
||||
self.window.as_ptr() as usize
|
||||
}
|
||||
|
||||
fn window_object(&self) -> *mut AnyObject {
|
||||
self.window.as_ptr()
|
||||
}
|
||||
|
||||
fn view_object(&self) -> *mut AnyObject {
|
||||
self.view.as_ptr()
|
||||
}
|
||||
|
||||
fn set_title(&self, title: &str) {
|
||||
let title = NSString::from_str(title);
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![self.window, setTitle: &*title];
|
||||
let _: () = objc2::msg_send![self.window_object(), 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];
|
||||
let _: () = objc2::msg_send![self.window_object(), setContentSize: content_size];
|
||||
}
|
||||
}
|
||||
|
||||
fn show(&self) {
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![self.window, makeKeyAndOrderFront: std::ptr::null_mut::<AnyObject>()];
|
||||
let _: () = objc2::msg_send![self.window_object(), makeKeyAndOrderFront: std::ptr::null_mut::<AnyObject>()];
|
||||
}
|
||||
}
|
||||
|
||||
fn hide(&self) {
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![self.window, orderOut: std::ptr::null_mut::<AnyObject>()];
|
||||
let _: () =
|
||||
objc2::msg_send![self.window_object(), orderOut: std::ptr::null_mut::<AnyObject>()];
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![self.window, close];
|
||||
let _: () = objc2::msg_send![self.window_object(), close];
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_display(&self) -> bool {
|
||||
let in_live_resize: bool = unsafe { objc2::msg_send![self.view, inLiveResize] };
|
||||
fn in_live_resize(&self) -> bool {
|
||||
unsafe { objc2::msg_send![self.view_object(), inLiveResize] }
|
||||
}
|
||||
|
||||
fn display_if_needed(&self) {
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![self.view_object(), displayIfNeeded];
|
||||
let _: () = objc2::msg_send![self.window_object(), displayIfNeeded];
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_core_animation_transaction(&self) {
|
||||
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) {
|
||||
fn detach_delegate(&mut self) {
|
||||
unsafe {
|
||||
let _: () =
|
||||
objc2::msg_send![self.window, setDelegate: std::ptr::null_mut::<AnyObject>()];
|
||||
let _: () = objc2::msg_send![self.delegate, release];
|
||||
let _: () = objc2::msg_send![self.window_object(), setDelegate: std::ptr::null_mut::<AnyObject>()];
|
||||
}
|
||||
if let Some(mut delegate) = self.delegate.take() {
|
||||
delegate.release_now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NativeWindow {
|
||||
fn drop(&mut self) {
|
||||
self.detach_delegate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,35 +327,41 @@ fn run_loop_wake_handle() -> &'static Mutex<Option<RunLoopWakeHandle>> {
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
struct ResizePipeline {
|
||||
active_resize: Option<UiSize>,
|
||||
next_resize: Option<UiSize>,
|
||||
active_resize: Option<WindowConfigured>,
|
||||
next_resize: Option<WindowConfigured>,
|
||||
}
|
||||
|
||||
impl ResizePipeline {
|
||||
fn note_resize(&mut self, viewport: UiSize) -> Option<UiSize> {
|
||||
fn note_resize(&mut self, configuration: WindowConfigured) -> Option<WindowConfigured> {
|
||||
if self.active_resize.is_some() {
|
||||
self.next_resize = Some(viewport);
|
||||
self.next_resize = Some(configuration);
|
||||
return None;
|
||||
}
|
||||
self.active_resize = Some(viewport);
|
||||
Some(viewport)
|
||||
self.active_resize = Some(configuration);
|
||||
Some(configuration)
|
||||
}
|
||||
|
||||
fn ensure_initial_request(&mut self, viewport: UiSize) -> Option<UiSize> {
|
||||
fn ensure_initial_request(
|
||||
&mut self,
|
||||
configuration: WindowConfigured,
|
||||
) -> Option<WindowConfigured> {
|
||||
if self.active_resize.is_some() || self.next_resize.is_some() {
|
||||
return None;
|
||||
}
|
||||
self.active_resize = Some(viewport);
|
||||
Some(viewport)
|
||||
self.active_resize = Some(configuration);
|
||||
Some(configuration)
|
||||
}
|
||||
|
||||
fn note_presented_scene(&mut self, scene_viewport: UiSize) -> Option<UiSize> {
|
||||
if self.active_resize != Some(scene_viewport) {
|
||||
fn note_presented_scene(&mut self, scene_viewport: UiSize) -> Option<WindowConfigured> {
|
||||
let Some(presented_configuration) = self.active_resize else {
|
||||
return None;
|
||||
};
|
||||
if presented_configuration.actual_inner_size != scene_viewport {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.active_resize = None;
|
||||
if self.next_resize == Some(scene_viewport) {
|
||||
if self.next_resize == Some(presented_configuration) {
|
||||
self.next_resize = None;
|
||||
}
|
||||
|
||||
@@ -332,6 +410,7 @@ unsafe impl Sync for AppKitSurfaceTarget {}
|
||||
|
||||
impl HasDisplayHandle for AppKitSurfaceTarget {
|
||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||
assert_main_thread();
|
||||
let raw = RawDisplayHandle::AppKit(AppKitDisplayHandle::new());
|
||||
Ok(unsafe { DisplayHandle::borrow_raw(raw) })
|
||||
}
|
||||
@@ -339,6 +418,7 @@ impl HasDisplayHandle for AppKitSurfaceTarget {
|
||||
|
||||
impl HasWindowHandle for AppKitSurfaceTarget {
|
||||
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
||||
assert_main_thread();
|
||||
let ptr = NonNull::new(self.view.cast::<c_void>()).ok_or(HandleError::Unavailable)?;
|
||||
let raw = RawWindowHandle::AppKit(AppKitWindowHandle::new(ptr));
|
||||
Ok(unsafe { WindowHandle::borrow_raw(raw) })
|
||||
@@ -380,6 +460,10 @@ pub fn run_macos_app_event_loop() {
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![app, run];
|
||||
}
|
||||
set_command_wake_hook(None);
|
||||
if let Ok(mut slot) = run_loop_wake_handle().lock() {
|
||||
*slot = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn install_platform_run_loop_source() -> Option<RunLoopSourceRegistration> {
|
||||
@@ -401,8 +485,8 @@ fn install_platform_run_loop_source() -> Option<RunLoopSourceRegistration> {
|
||||
run_loop.add_source(Some(&source), common_modes);
|
||||
|
||||
let handle = RunLoopWakeHandle {
|
||||
run_loop: CFRetained::as_ptr(&run_loop).as_ptr().cast::<c_void>() as usize,
|
||||
source: CFRetained::as_ptr(&source).as_ptr().cast::<c_void>() as usize,
|
||||
run_loop: run_loop.clone(),
|
||||
source: source.clone(),
|
||||
};
|
||||
if let Ok(mut slot) = run_loop_wake_handle().lock() {
|
||||
*slot = Some(handle);
|
||||
@@ -423,13 +507,15 @@ extern "C-unwind" fn platform_run_loop_source_perform(_info: *mut c_void) {
|
||||
}
|
||||
|
||||
fn signal_platform_run_loop_source() {
|
||||
let Some(handle) = run_loop_wake_handle().lock().ok().and_then(|slot| *slot) else {
|
||||
let Some(handle) = run_loop_wake_handle()
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|slot| slot.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let source = unsafe { &*(handle.source as *const CFRunLoopSource) };
|
||||
let run_loop = unsafe { &*(handle.run_loop as *const CFRunLoop) };
|
||||
source.signal();
|
||||
run_loop.wake_up();
|
||||
handle.source.signal();
|
||||
handle.run_loop.wake_up();
|
||||
}
|
||||
|
||||
fn assert_main_thread() {
|
||||
@@ -508,91 +594,98 @@ fn delegate_state() -> &'static Mutex<DelegateState> {
|
||||
DELEGATE_STATE.get_or_init(|| Mutex::new(DelegateState::default()))
|
||||
}
|
||||
|
||||
fn lock_delegate_state() -> MutexGuard<'static, DelegateState> {
|
||||
delegate_state()
|
||||
.lock()
|
||||
.expect("macOS delegate event state mutex poisoned")
|
||||
}
|
||||
|
||||
fn queue_delegate_close_request(window: *mut AnyObject) {
|
||||
if window.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut state) = delegate_state().lock() {
|
||||
state.pending_close_requests.push(window as usize);
|
||||
}
|
||||
lock_delegate_state()
|
||||
.pending_close_requests
|
||||
.push(window as usize);
|
||||
}
|
||||
|
||||
fn queue_delegate_window_closed(window: *mut AnyObject) {
|
||||
if window.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut state) = delegate_state().lock() {
|
||||
state.pending_closed_windows.push(window as usize);
|
||||
}
|
||||
lock_delegate_state()
|
||||
.pending_closed_windows
|
||||
.push(window as usize);
|
||||
}
|
||||
|
||||
fn queue_delegate_resize(window: *mut AnyObject, size: UiSize, scale_factor: f32) {
|
||||
if window.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut state) = delegate_state().lock() {
|
||||
state.pending_resize_events.push(DelegateResizeEvent {
|
||||
lock_delegate_state()
|
||||
.pending_resize_events
|
||||
.push(DelegateResizeEvent {
|
||||
window_ptr: window as usize,
|
||||
size,
|
||||
scale_factor,
|
||||
});
|
||||
}
|
||||
|
||||
fn queue_delegate_configuration_from_window(window: *mut AnyObject) {
|
||||
if window.is_null() {
|
||||
return;
|
||||
}
|
||||
let view: *mut AnyObject = unsafe { objc2::msg_send![window, contentView] };
|
||||
if view.is_null() {
|
||||
return;
|
||||
}
|
||||
let frame: CGRect = unsafe { objc2::msg_send![view, frame] };
|
||||
let scale_factor: f64 = unsafe { objc2::msg_send![window, backingScaleFactor] };
|
||||
queue_delegate_resize(
|
||||
window,
|
||||
UiSize::new(frame.size.width as f32, frame.size.height as f32),
|
||||
(scale_factor as f32).max(1.0),
|
||||
);
|
||||
}
|
||||
|
||||
fn queue_delegate_pointer_event(window: *mut AnyObject, event: PointerEvent) {
|
||||
if window.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut state) = delegate_state().lock() {
|
||||
state
|
||||
lock_delegate_state()
|
||||
.pending_input_events
|
||||
.push(DelegateInputEvent::Pointer {
|
||||
window_ptr: window as usize,
|
||||
event,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_delegate_keyboard_event(window: *mut AnyObject, event: KeyboardEvent) {
|
||||
if window.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut state) = delegate_state().lock() {
|
||||
state
|
||||
lock_delegate_state()
|
||||
.pending_input_events
|
||||
.push(DelegateInputEvent::Keyboard {
|
||||
window_ptr: window as usize,
|
||||
event,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn take_delegate_close_requests() -> Vec<usize> {
|
||||
if let Ok(mut state) = delegate_state().lock() {
|
||||
return std::mem::take(&mut state.pending_close_requests);
|
||||
}
|
||||
Vec::new()
|
||||
std::mem::take(&mut lock_delegate_state().pending_close_requests)
|
||||
}
|
||||
|
||||
fn take_delegate_closed_windows() -> Vec<usize> {
|
||||
if let Ok(mut state) = delegate_state().lock() {
|
||||
return std::mem::take(&mut state.pending_closed_windows);
|
||||
}
|
||||
Vec::new()
|
||||
std::mem::take(&mut lock_delegate_state().pending_closed_windows)
|
||||
}
|
||||
|
||||
fn take_delegate_resize_events() -> Vec<DelegateResizeEvent> {
|
||||
if let Ok(mut state) = delegate_state().lock() {
|
||||
return std::mem::take(&mut state.pending_resize_events);
|
||||
}
|
||||
Vec::new()
|
||||
std::mem::take(&mut lock_delegate_state().pending_resize_events)
|
||||
}
|
||||
|
||||
fn take_delegate_input_events() -> Vec<DelegateInputEvent> {
|
||||
if let Ok(mut state) = delegate_state().lock() {
|
||||
return std::mem::take(&mut state.pending_input_events);
|
||||
}
|
||||
Vec::new()
|
||||
std::mem::take(&mut lock_delegate_state().pending_input_events)
|
||||
}
|
||||
|
||||
fn window_delegate_class() -> &'static AnyClass {
|
||||
@@ -630,28 +723,26 @@ fn window_delegate_class() -> &'static AnyClass {
|
||||
notification: *mut AnyObject,
|
||||
) {
|
||||
let window: *mut AnyObject = unsafe { objc2::msg_send![notification, object] };
|
||||
if window.is_null() {
|
||||
return;
|
||||
}
|
||||
let view: *mut AnyObject = unsafe { objc2::msg_send![window, contentView] };
|
||||
if view.is_null() {
|
||||
return;
|
||||
}
|
||||
let frame: CGRect = unsafe { objc2::msg_send![view, frame] };
|
||||
let scale_factor: f64 = unsafe { objc2::msg_send![window, backingScaleFactor] };
|
||||
trace!(
|
||||
target: "ruin_ui_platform_macos::resize",
|
||||
window_ptr = window as usize,
|
||||
width = frame.size.width,
|
||||
height = frame.size.height,
|
||||
scale_factor,
|
||||
"windowDidResize:"
|
||||
);
|
||||
queue_delegate_resize(
|
||||
window,
|
||||
UiSize::new(frame.size.width as f32, frame.size.height as f32),
|
||||
(scale_factor as f32).max(1.0),
|
||||
queue_delegate_configuration_from_window(window);
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
extern "C-unwind" fn did_change_screen(
|
||||
_this: &AnyObject,
|
||||
_cmd: Sel,
|
||||
notification: *mut AnyObject,
|
||||
) {
|
||||
let window: *mut AnyObject = unsafe { objc2::msg_send![notification, object] };
|
||||
trace!(
|
||||
target: "ruin_ui_platform_macos::resize",
|
||||
window_ptr = window as usize,
|
||||
"windowDidChangeScreen:"
|
||||
);
|
||||
queue_delegate_configuration_from_window(window);
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
extern "C-unwind" fn did_change_occlusion_state(
|
||||
@@ -716,6 +807,8 @@ fn window_delegate_class() -> &'static AnyClass {
|
||||
builder.add_method::<AnyObject, _>(sel!(windowWillClose:), method);
|
||||
let method: extern "C-unwind" fn(_, _, _) = did_resize;
|
||||
builder.add_method::<AnyObject, _>(sel!(windowDidResize:), method);
|
||||
let method: extern "C-unwind" fn(_, _, _) = did_change_screen;
|
||||
builder.add_method::<AnyObject, _>(sel!(windowDidChangeScreen:), method);
|
||||
let method: extern "C-unwind" fn(_, _, _) = did_change_occlusion_state;
|
||||
builder.add_method::<AnyObject, _>(sel!(windowDidChangeOcclusionState:), method);
|
||||
let method: extern "C-unwind" fn(_, _, _) = did_become_key;
|
||||
@@ -751,6 +844,17 @@ fn content_view_class() -> &'static AnyClass {
|
||||
Bool::YES
|
||||
}
|
||||
|
||||
extern "C-unwind" fn did_change_backing_properties(this: &AnyObject, _cmd: Sel) {
|
||||
let window: *mut AnyObject = unsafe { objc2::msg_send![this, window] };
|
||||
trace!(
|
||||
target: "ruin_ui_platform_macos::resize",
|
||||
window_ptr = window as usize,
|
||||
"viewDidChangeBackingProperties"
|
||||
);
|
||||
queue_delegate_configuration_from_window(window);
|
||||
signal_platform_run_loop_source();
|
||||
}
|
||||
|
||||
extern "C-unwind" fn mouse_moved(this: &AnyObject, _cmd: Sel, event: *mut AnyObject) {
|
||||
queue_pointer_from_view_event(this, event, PointerEventKind::Move);
|
||||
signal_platform_run_loop_source();
|
||||
@@ -857,6 +961,8 @@ fn content_view_class() -> &'static AnyClass {
|
||||
builder.add_method::<AnyObject, _>(sel!(acceptsFirstResponder), method);
|
||||
let method: extern "C-unwind" fn(_, _) -> _ = is_flipped;
|
||||
builder.add_method::<AnyObject, _>(sel!(isFlipped), method);
|
||||
let method: extern "C-unwind" fn(_, _) = did_change_backing_properties;
|
||||
builder.add_method::<AnyObject, _>(sel!(viewDidChangeBackingProperties), method);
|
||||
let method: extern "C-unwind" fn(_, _, _) = mouse_moved;
|
||||
builder.add_method::<AnyObject, _>(sel!(mouseMoved:), method);
|
||||
let method: extern "C-unwind" fn(_, _, _) = mouse_moved;
|
||||
@@ -1071,9 +1177,6 @@ fn drain_platform_commands(state: &Rc<RefCell<MacosState>>) -> bool {
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![app, stop: std::ptr::null_mut::<AnyObject>()];
|
||||
}
|
||||
if let Ok(mut slot) = run_loop_wake_handle().lock() {
|
||||
*slot = None;
|
||||
}
|
||||
set_command_wake_hook(None);
|
||||
break;
|
||||
}
|
||||
@@ -1101,7 +1204,7 @@ fn handle_create_window(state: &Rc<RefCell<MacosState>>, window_id: WindowId, sp
|
||||
window.configuration.visible = visible;
|
||||
let _ = window
|
||||
.resize_pipeline
|
||||
.ensure_initial_request(window.configuration.actual_inner_size);
|
||||
.ensure_initial_request(window.configuration);
|
||||
sync_visibility(window);
|
||||
apply_cursor_icon(window.cursor_icon);
|
||||
emit_opened = true;
|
||||
@@ -1176,10 +1279,6 @@ fn handle_update_window(
|
||||
window.state.request_logical_close();
|
||||
if window.native.is_some() {
|
||||
close_native_window(window);
|
||||
// Keep behavior aligned with other current platform backends:
|
||||
// treat a requested close as immediately closed for app shutdown.
|
||||
window.state.finalize_close();
|
||||
emit_closed = true;
|
||||
} else {
|
||||
window.state.finalize_close();
|
||||
emit_closed = true;
|
||||
@@ -1203,13 +1302,15 @@ fn handle_update_window(
|
||||
resize_native_window(native, size);
|
||||
}
|
||||
repaint_retained_scene_for_resize(window_id, window);
|
||||
if let Some(active_resize) = window.resize_pipeline.note_resize(size) {
|
||||
if let Some(active_resize) =
|
||||
window.resize_pipeline.note_resize(window.configuration)
|
||||
{
|
||||
trace!(
|
||||
target: "ruin_ui_platform_macos::resize",
|
||||
?window_id,
|
||||
width = active_resize.width,
|
||||
height = active_resize.height,
|
||||
scale_factor = window.configuration.scale_factor,
|
||||
width = active_resize.actual_inner_size.width,
|
||||
height = active_resize.actual_inner_size.height,
|
||||
scale_factor = active_resize.scale_factor,
|
||||
"emitting Configured from window update resize"
|
||||
);
|
||||
emit_configured = Some(window.configuration);
|
||||
@@ -1217,10 +1318,10 @@ fn handle_update_window(
|
||||
trace!(
|
||||
target: "ruin_ui_platform_macos::resize",
|
||||
?window_id,
|
||||
active_width = window.resize_pipeline.active_resize.map(|s| s.width),
|
||||
active_height = window.resize_pipeline.active_resize.map(|s| s.height),
|
||||
next_width = window.resize_pipeline.next_resize.map(|s| s.width),
|
||||
next_height = window.resize_pipeline.next_resize.map(|s| s.height),
|
||||
active_width = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.width),
|
||||
active_height = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.height),
|
||||
next_width = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.width),
|
||||
next_height = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.height),
|
||||
"staged next resize from window update while another layout is in flight"
|
||||
);
|
||||
}
|
||||
@@ -1309,10 +1410,10 @@ fn try_present_scene(
|
||||
scene_height = scene_viewport.height,
|
||||
current_width = current_viewport.width,
|
||||
current_height = current_viewport.height,
|
||||
active_width = window.resize_pipeline.active_resize.map(|s| s.width),
|
||||
active_height = window.resize_pipeline.active_resize.map(|s| s.height),
|
||||
next_width = window.resize_pipeline.next_resize.map(|s| s.width),
|
||||
next_height = window.resize_pipeline.next_resize.map(|s| s.height),
|
||||
active_width = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.width),
|
||||
active_height = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.height),
|
||||
next_width = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.width),
|
||||
next_height = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.height),
|
||||
"handle_replace_scene start"
|
||||
);
|
||||
let render_succeeded = render_scene_to_current_drawable(window_id, window, scene);
|
||||
@@ -1321,19 +1422,20 @@ fn try_present_scene(
|
||||
return None;
|
||||
}
|
||||
|
||||
window.state.note_presented();
|
||||
flush_presented_frame(window);
|
||||
window.state.note_presented();
|
||||
if let Some(next_resize) = finish_presented_resize(window, scene_viewport) {
|
||||
trace!(
|
||||
target: "ruin_ui_platform_macos::resize",
|
||||
?window_id,
|
||||
scene_width = scene_viewport.width,
|
||||
scene_height = scene_viewport.height,
|
||||
next_width = next_resize.width,
|
||||
next_height = next_resize.height,
|
||||
next_width = next_resize.actual_inner_size.width,
|
||||
next_height = next_resize.actual_inner_size.height,
|
||||
next_scale = next_resize.scale_factor,
|
||||
"presented active resize scene; promoting staged next resize"
|
||||
);
|
||||
emit_resize_request(window_id, window, next_resize, followup_events);
|
||||
emit_resize_request(window_id, next_resize, followup_events);
|
||||
}
|
||||
Some((scene_version, item_count, window.configuration.visible))
|
||||
}
|
||||
@@ -1356,8 +1458,8 @@ 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.state.note_presented();
|
||||
flush_presented_frame(window);
|
||||
window.state.note_presented();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1370,6 +1472,7 @@ fn render_scene_to_current_drawable(
|
||||
let scene_viewport = scene.logical_size;
|
||||
let mut render_succeeded = false;
|
||||
if let Some(native) = window.native.as_mut() {
|
||||
let view = native.view_object();
|
||||
let Some(renderer) = native.renderer.as_mut() else {
|
||||
trace!(
|
||||
target: "ruin_ui_platform_macos::resize",
|
||||
@@ -1378,7 +1481,7 @@ fn render_scene_to_current_drawable(
|
||||
);
|
||||
return false;
|
||||
};
|
||||
configure_view_resize_policy(native.view);
|
||||
configure_view_resize_policy(view);
|
||||
renderer.set_scale_factor(window.configuration.scale_factor);
|
||||
let scale = window.configuration.scale_factor.max(1.0);
|
||||
let drawable_width = (current_viewport.width * scale).max(1.0) as u32;
|
||||
@@ -1432,8 +1535,8 @@ fn render_scene_to_current_drawable(
|
||||
?window_id,
|
||||
scene_width = scene_viewport.width,
|
||||
scene_height = scene_viewport.height,
|
||||
active_width = window.resize_pipeline.active_resize.map(|s| s.width),
|
||||
active_height = window.resize_pipeline.active_resize.map(|s| s.height),
|
||||
active_width = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.width),
|
||||
active_height = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.height),
|
||||
current_width = current_viewport.width,
|
||||
current_height = current_viewport.height,
|
||||
render_succeeded,
|
||||
@@ -1474,9 +1577,8 @@ fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option<Nat
|
||||
defer: false
|
||||
]
|
||||
};
|
||||
if window_obj.is_null() {
|
||||
return None;
|
||||
}
|
||||
let window_owner = unsafe { ObjcStrong::from_owned(window_obj) }?;
|
||||
let window_obj = window_owner.as_ptr();
|
||||
|
||||
let title = NSString::from_str(&window.spec.title);
|
||||
unsafe {
|
||||
@@ -1488,14 +1590,12 @@ fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option<Nat
|
||||
let view_class = content_view_class();
|
||||
let view_obj: *mut AnyObject = unsafe { objc2::msg_send![view_class, alloc] };
|
||||
let view_obj: *mut AnyObject = unsafe { objc2::msg_send![view_obj, initWithFrame: rect] };
|
||||
if view_obj.is_null() {
|
||||
return None;
|
||||
}
|
||||
let view_owner = unsafe { ObjcStrong::from_owned(view_obj) }?;
|
||||
let view_obj = view_owner.as_ptr();
|
||||
let delegate_class = window_delegate_class();
|
||||
let delegate_obj: *mut AnyObject = unsafe { objc2::msg_send![delegate_class, new] };
|
||||
if delegate_obj.is_null() {
|
||||
return None;
|
||||
}
|
||||
let delegate_owner = unsafe { ObjcStrong::from_owned(delegate_obj) }?;
|
||||
let delegate_obj = delegate_owner.as_ptr();
|
||||
|
||||
unsafe {
|
||||
let _: () = objc2::msg_send![
|
||||
@@ -1511,7 +1611,6 @@ fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option<Nat
|
||||
objc2::msg_send![window_obj, makeKeyAndOrderFront: std::ptr::null_mut::<AnyObject>()];
|
||||
let _: () = objc2::msg_send![app, activateIgnoringOtherApps: true];
|
||||
}
|
||||
|
||||
let metal_layer = configure_metal_surface_layer(view_obj)?;
|
||||
|
||||
let scale_factor: f64 = unsafe { objc2::msg_send![window_obj, backingScaleFactor] };
|
||||
@@ -1529,11 +1628,11 @@ fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option<Nat
|
||||
}
|
||||
|
||||
Some(NativeWindow {
|
||||
window: window_obj,
|
||||
view: view_obj,
|
||||
delegate: delegate_obj,
|
||||
_metal_layer: metal_layer,
|
||||
renderer,
|
||||
_metal_layer: metal_layer,
|
||||
view: view_owner,
|
||||
delegate: Some(delegate_owner),
|
||||
window: window_owner,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1644,7 +1743,7 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
if window.state.mark_close_requested_sent() {
|
||||
if window.state.note_delegate_close_request() {
|
||||
debug!(
|
||||
target: "ruin_ui_platform_macos::lifecycle",
|
||||
?window_id,
|
||||
@@ -1691,45 +1790,63 @@ fn note_window_resize(
|
||||
viewport: UiSize,
|
||||
pending: &mut Vec<PlatformEvent>,
|
||||
) {
|
||||
if let Some(active_resize) = window.resize_pipeline.note_resize(viewport) {
|
||||
emit_resize_request(window_id, window, active_resize, pending);
|
||||
debug_assert_eq!(window.configuration.actual_inner_size, viewport);
|
||||
if let Some(active_resize) = window.resize_pipeline.note_resize(window.configuration) {
|
||||
emit_resize_request(window_id, active_resize, pending);
|
||||
} else {
|
||||
trace!(
|
||||
target: "ruin_ui_platform_macos::resize",
|
||||
?window_id,
|
||||
active_width = window.resize_pipeline.active_resize.map(|s| s.width),
|
||||
active_height = window.resize_pipeline.active_resize.map(|s| s.height),
|
||||
next_width = window.resize_pipeline.next_resize.map(|s| s.width),
|
||||
next_height = window.resize_pipeline.next_resize.map(|s| s.height),
|
||||
"staged next resize while another layout is in flight"
|
||||
active_width = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.width),
|
||||
active_height = window.resize_pipeline.active_resize.map(|s| s.actual_inner_size.height),
|
||||
active_scale = window.resize_pipeline.active_resize.map(|s| s.scale_factor),
|
||||
next_width = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.width),
|
||||
next_height = window.resize_pipeline.next_resize.map(|s| s.actual_inner_size.height),
|
||||
next_scale = window.resize_pipeline.next_resize.map(|s| s.scale_factor),
|
||||
"staged next configuration while another layout is in flight"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_resize_request(
|
||||
window_id: WindowId,
|
||||
window: &mut MacosWindow,
|
||||
viewport: UiSize,
|
||||
configuration: WindowConfigured,
|
||||
pending: &mut Vec<PlatformEvent>,
|
||||
) {
|
||||
pending.push(PlatformEvent::Configured {
|
||||
window_id,
|
||||
configuration: window.configuration,
|
||||
configuration,
|
||||
});
|
||||
trace!(
|
||||
target: "ruin_ui_platform_macos::resize",
|
||||
?window_id,
|
||||
width = viewport.width,
|
||||
height = viewport.height,
|
||||
scale_factor = window.configuration.scale_factor,
|
||||
"emitting Configured from delegate resize"
|
||||
width = configuration.actual_inner_size.width,
|
||||
height = configuration.actual_inner_size.height,
|
||||
scale_factor = configuration.scale_factor,
|
||||
"emitting Configured from platform configuration change"
|
||||
);
|
||||
}
|
||||
|
||||
fn finish_presented_resize(window: &mut MacosWindow, presented: UiSize) -> Option<UiSize> {
|
||||
fn finish_presented_resize(
|
||||
window: &mut MacosWindow,
|
||||
presented: UiSize,
|
||||
) -> Option<WindowConfigured> {
|
||||
window.resize_pipeline.note_presented_scene(presented)
|
||||
}
|
||||
|
||||
fn presentation_flush_policy(
|
||||
has_presented_frame: bool,
|
||||
in_live_resize: bool,
|
||||
) -> PresentationFlushPolicy {
|
||||
if in_live_resize {
|
||||
PresentationFlushPolicy::DisplayAndTransaction
|
||||
} else if !has_presented_frame {
|
||||
PresentationFlushPolicy::DisplayIfNeeded
|
||||
} else {
|
||||
PresentationFlushPolicy::None
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_metal_surface_layer(view: *mut AnyObject) -> Option<Retained<CAMetalLayer>> {
|
||||
let view_ptr = NonNull::new(view.cast::<c_void>())?;
|
||||
let layer = unsafe { MetalLayer::from_ns_view(view_ptr) };
|
||||
@@ -1744,11 +1861,21 @@ fn flush_presented_frame(window: &MacosWindow) {
|
||||
let Some(native) = window.native.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let in_live_resize = native.flush_display();
|
||||
let in_live_resize = native.in_live_resize();
|
||||
let policy = presentation_flush_policy(window.state.has_presented_frame, in_live_resize);
|
||||
match policy {
|
||||
PresentationFlushPolicy::None => {}
|
||||
PresentationFlushPolicy::DisplayIfNeeded => native.display_if_needed(),
|
||||
PresentationFlushPolicy::DisplayAndTransaction => {
|
||||
native.display_if_needed();
|
||||
native.flush_core_animation_transaction();
|
||||
}
|
||||
}
|
||||
trace!(
|
||||
target: "ruin_ui_platform_macos::resize",
|
||||
in_live_resize,
|
||||
"requested display after presented frame"
|
||||
?policy,
|
||||
"applied presentation flush policy after presented frame"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1797,7 +1924,7 @@ fn collect_window_state_events(
|
||||
let Some((window_ptr, view_ptr)) = window
|
||||
.native
|
||||
.as_ref()
|
||||
.map(|native| (native.window, native.view))
|
||||
.map(|native| (native.window_object(), native.view_object()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -1840,16 +1967,19 @@ fn collect_window_state_events(
|
||||
&& let Some(scene) = window.latest_scene.clone()
|
||||
&& (became_visible
|
||||
|| !window.state.has_presented_frame
|
||||
|| window.resize_pipeline.active_resize == Some(scene.logical_size))
|
||||
|| window
|
||||
.resize_pipeline
|
||||
.active_resize
|
||||
.is_some_and(|configuration| configuration.actual_inner_size == scene.logical_size))
|
||||
{
|
||||
let _ = try_present_scene(window_id, window, &scene, pending);
|
||||
} else if is_visible
|
||||
&& !window.state.has_presented_frame
|
||||
&& let Some(initial_viewport) = window
|
||||
.resize_pipeline
|
||||
.ensure_initial_request(window.configuration.actual_inner_size)
|
||||
.ensure_initial_request(window.configuration)
|
||||
{
|
||||
emit_resize_request(window_id, window, initial_viewport, pending);
|
||||
emit_resize_request(window_id, initial_viewport, pending);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1938,7 +2068,7 @@ fn finalize_native_close(
|
||||
"finalizing native close"
|
||||
);
|
||||
let should_emit_closed = window.state.lifecycle != WindowLifecycle::LogicalClosed;
|
||||
if let Some(native) = window.native.take() {
|
||||
if let Some(mut native) = window.native.take() {
|
||||
native.detach_delegate();
|
||||
}
|
||||
|
||||
@@ -1965,7 +2095,7 @@ fn sync_window_metrics(window: &mut MacosWindow) {
|
||||
let Some((window_ptr, view_ptr)) = window
|
||||
.native
|
||||
.as_ref()
|
||||
.map(|native| (native.window, native.view))
|
||||
.map(|native| (native.window_object(), native.view_object()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -1978,39 +2108,54 @@ fn sync_window_metrics(window: &mut MacosWindow) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MacosWindowState, ResizePipeline};
|
||||
use ruin_ui::{UiSize, WindowLifecycle};
|
||||
use super::{
|
||||
MacosWindowState, PresentationFlushPolicy, ResizePipeline, presentation_flush_policy,
|
||||
};
|
||||
use ruin_ui::{UiSize, WindowConfigured, WindowLifecycle};
|
||||
|
||||
fn configuration(width: f32, height: f32, scale_factor: f32) -> WindowConfigured {
|
||||
WindowConfigured {
|
||||
actual_inner_size: UiSize::new(width, height),
|
||||
scale_factor,
|
||||
visible: true,
|
||||
maximized: false,
|
||||
fullscreen: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_pipeline_stages_latest_resize_while_active_request_is_in_flight() {
|
||||
let mut pipeline = ResizePipeline::default();
|
||||
let first = configuration(640.0, 480.0, 1.0);
|
||||
let second = configuration(800.0, 600.0, 1.0);
|
||||
let third = configuration(1024.0, 768.0, 1.0);
|
||||
|
||||
assert_eq!(
|
||||
pipeline.note_resize(UiSize::new(640.0, 480.0)),
|
||||
Some(UiSize::new(640.0, 480.0))
|
||||
);
|
||||
assert_eq!(pipeline.active_resize, Some(UiSize::new(640.0, 480.0)));
|
||||
assert_eq!(pipeline.note_resize(first), Some(first));
|
||||
assert_eq!(pipeline.active_resize, Some(first));
|
||||
assert_eq!(pipeline.next_resize, None);
|
||||
|
||||
assert_eq!(pipeline.note_resize(UiSize::new(800.0, 600.0)), None);
|
||||
assert_eq!(pipeline.active_resize, Some(UiSize::new(640.0, 480.0)));
|
||||
assert_eq!(pipeline.next_resize, Some(UiSize::new(800.0, 600.0)));
|
||||
assert_eq!(pipeline.note_resize(second), None);
|
||||
assert_eq!(pipeline.active_resize, Some(first));
|
||||
assert_eq!(pipeline.next_resize, Some(second));
|
||||
|
||||
assert_eq!(pipeline.note_resize(UiSize::new(1024.0, 768.0)), None);
|
||||
assert_eq!(pipeline.active_resize, Some(UiSize::new(640.0, 480.0)));
|
||||
assert_eq!(pipeline.next_resize, Some(UiSize::new(1024.0, 768.0)));
|
||||
assert_eq!(pipeline.note_resize(third), None);
|
||||
assert_eq!(pipeline.active_resize, Some(first));
|
||||
assert_eq!(pipeline.next_resize, Some(third));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_pipeline_promotes_next_resize_after_presenting_active_scene() {
|
||||
let mut pipeline = ResizePipeline::default();
|
||||
let first = UiSize::new(640.0, 480.0);
|
||||
let second = UiSize::new(1024.0, 768.0);
|
||||
let first = configuration(640.0, 480.0, 1.0);
|
||||
let second = configuration(1024.0, 768.0, 1.0);
|
||||
|
||||
assert_eq!(pipeline.note_resize(first), Some(first));
|
||||
assert_eq!(pipeline.note_resize(second), None);
|
||||
|
||||
assert_eq!(pipeline.note_presented_scene(first), Some(second));
|
||||
assert_eq!(
|
||||
pipeline.note_presented_scene(first.actual_inner_size),
|
||||
Some(second)
|
||||
);
|
||||
assert_eq!(pipeline.active_resize, Some(second));
|
||||
assert_eq!(pipeline.next_resize, None);
|
||||
}
|
||||
@@ -2018,21 +2163,41 @@ mod tests {
|
||||
#[test]
|
||||
fn resize_pipeline_drops_duplicate_staged_resize_when_present_already_satisfies_it() {
|
||||
let mut pipeline = ResizePipeline::default();
|
||||
let viewport = UiSize::new(800.0, 600.0);
|
||||
let viewport = configuration(800.0, 600.0, 1.0);
|
||||
|
||||
assert_eq!(pipeline.note_resize(viewport), Some(viewport));
|
||||
assert_eq!(pipeline.note_resize(viewport), None);
|
||||
|
||||
assert_eq!(pipeline.note_presented_scene(viewport), None);
|
||||
assert_eq!(
|
||||
pipeline.note_presented_scene(viewport.actual_inner_size),
|
||||
None
|
||||
);
|
||||
assert_eq!(pipeline.active_resize, None);
|
||||
assert_eq!(pipeline.next_resize, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_pipeline_preserves_same_size_scale_changes() {
|
||||
let mut pipeline = ResizePipeline::default();
|
||||
let one_x = configuration(800.0, 600.0, 1.0);
|
||||
let two_x = configuration(800.0, 600.0, 2.0);
|
||||
|
||||
assert_eq!(pipeline.note_resize(one_x), Some(one_x));
|
||||
assert_eq!(pipeline.note_resize(two_x), None);
|
||||
|
||||
assert_eq!(
|
||||
pipeline.note_presented_scene(one_x.actual_inner_size),
|
||||
Some(two_x)
|
||||
);
|
||||
assert_eq!(pipeline.active_resize, Some(two_x));
|
||||
assert_eq!(pipeline.next_resize, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_pipeline_ignores_presents_for_non_active_scene_sizes() {
|
||||
let mut pipeline = ResizePipeline::default();
|
||||
let active = UiSize::new(640.0, 480.0);
|
||||
let next = UiSize::new(900.0, 700.0);
|
||||
let active = configuration(640.0, 480.0, 1.0);
|
||||
let next = configuration(900.0, 700.0, 1.0);
|
||||
|
||||
assert_eq!(pipeline.note_resize(active), Some(active));
|
||||
assert_eq!(pipeline.note_resize(next), None);
|
||||
@@ -2045,6 +2210,22 @@ mod tests {
|
||||
assert_eq!(pipeline.next_resize, Some(next));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presentation_flush_policy_is_narrowed_to_first_present_and_live_resize() {
|
||||
assert_eq!(
|
||||
presentation_flush_policy(false, false),
|
||||
PresentationFlushPolicy::DisplayIfNeeded
|
||||
);
|
||||
assert_eq!(
|
||||
presentation_flush_policy(true, false),
|
||||
PresentationFlushPolicy::None
|
||||
);
|
||||
assert_eq!(
|
||||
presentation_flush_policy(true, true),
|
||||
PresentationFlushPolicy::DisplayAndTransaction
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_state_distinguishes_hide_from_close() {
|
||||
let mut state = MacosWindowState::new(true);
|
||||
@@ -2080,4 +2261,16 @@ mod tests {
|
||||
assert!(!state.close_in_progress);
|
||||
assert!(!state.has_presented_frame);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_state_app_requested_close_suppresses_close_requested_event() {
|
||||
let mut state = MacosWindowState::new(true);
|
||||
state.open_with_visibility(true);
|
||||
|
||||
state.request_logical_close();
|
||||
|
||||
assert!(state.close_in_progress);
|
||||
assert!(!state.note_delegate_close_request());
|
||||
assert!(!state.close_requested_sent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +369,9 @@ impl WgpuSceneRenderer {
|
||||
format,
|
||||
width: width.max(1),
|
||||
height: height.max(1),
|
||||
// RUIN prioritizes immediate resize/input feedback over frame pacing
|
||||
// here. Keep latency to one frame and let callers opt into a more
|
||||
// power-conservative policy once presentation settings are exposed.
|
||||
present_mode: wgpu::PresentMode::AutoNoVsync,
|
||||
desired_maximum_frame_latency: 1,
|
||||
alpha_mode: caps.alpha_modes[0],
|
||||
@@ -795,9 +798,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
}
|
||||
}
|
||||
DisplayItem::Text(text) => {
|
||||
if (self.scale_factor - 1.0).abs() <= f32::EPSILON
|
||||
&& text.glyphs.iter().all(|glyph| glyph.cache_key.is_some())
|
||||
{
|
||||
if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(uploaded) =
|
||||
@@ -1070,9 +1071,6 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
logical_size: UiSize,
|
||||
perf: &mut AtlasTextPerfStats,
|
||||
) -> Option<UploadedAtlasText> {
|
||||
if (self.scale_factor - 1.0).abs() > f32::EPSILON {
|
||||
return None;
|
||||
}
|
||||
let mut vertices = Vec::new();
|
||||
let mut clip_stack = Vec::new();
|
||||
let mut active_clip = ActiveClip::default();
|
||||
@@ -1104,7 +1102,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
if active_clip.rect_active && logical_clip_rect.is_none() {
|
||||
continue;
|
||||
}
|
||||
let clip_rect = logical_clip_rect.map(rect_to_pixel_rect);
|
||||
let clip_rect = logical_clip_rect
|
||||
.map(|rect| scale_rect_to_pixel_rect(rect, self.scale_factor));
|
||||
|
||||
// Only iterate glyphs whose lines fall within the clip rect.
|
||||
// For large text nodes (e.g. full Cargo.lock) this reduces
|
||||
@@ -1115,7 +1114,10 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
perf.glyphs_clip_culled += (all_count - visible.len()) as u32;
|
||||
|
||||
for glyph in visible {
|
||||
let Some(cache_key) = glyph.cache_key else {
|
||||
let Some(cache_key) = glyph
|
||||
.cache_key
|
||||
.map(|cache_key| scale_glyph_cache_key(cache_key, self.scale_factor))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let resolved_color = resolve_glyph_color(glyph.color, text.default_color);
|
||||
@@ -1124,9 +1126,12 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Glyph positions are LOCAL; add text.origin for absolute screen coords.
|
||||
let abs_x = (text.origin.x + glyph.position.x).round() as i32;
|
||||
let abs_y = (text.origin.y + glyph.position.y).round() as i32;
|
||||
// Atlas glyphs are rasterized in device pixels while scene geometry remains
|
||||
// logical. Build and clip in device space, then convert back for vertices.
|
||||
let abs_x =
|
||||
((text.origin.x + glyph.position.x) * self.scale_factor).round() as i32;
|
||||
let abs_y =
|
||||
((text.origin.y + glyph.position.y) * self.scale_factor).round() as i32;
|
||||
let glyph_rect = PixelRect {
|
||||
left: abs_x + atlas_glyph.placement_left,
|
||||
top: abs_y - atlas_glyph.placement_top,
|
||||
@@ -1142,6 +1147,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
atlas_glyph.atlas_rect,
|
||||
clip_rect,
|
||||
logical_size,
|
||||
self.scale_factor,
|
||||
resolved_color,
|
||||
active_clip,
|
||||
);
|
||||
@@ -1872,6 +1878,7 @@ fn push_glyph_vertices(
|
||||
atlas_rect: AtlasRect,
|
||||
clip_rect: Option<PixelRect>,
|
||||
logical_size: UiSize,
|
||||
scale_factor: f32,
|
||||
color: Color,
|
||||
clip: ActiveClip,
|
||||
) {
|
||||
@@ -1879,10 +1886,15 @@ fn push_glyph_vertices(
|
||||
return;
|
||||
};
|
||||
|
||||
let left = to_ndc_x(dest_rect.left as f32, logical_size.width.max(1.0));
|
||||
let right = to_ndc_x(dest_rect.right as f32, logical_size.width.max(1.0));
|
||||
let top = to_ndc_y(dest_rect.top as f32, logical_size.height.max(1.0));
|
||||
let bottom = to_ndc_y(dest_rect.bottom as f32, logical_size.height.max(1.0));
|
||||
let scale_factor = scale_factor.max(1.0);
|
||||
let logical_left = dest_rect.left as f32 / scale_factor;
|
||||
let logical_top = dest_rect.top as f32 / scale_factor;
|
||||
let logical_right = dest_rect.right as f32 / scale_factor;
|
||||
let logical_bottom = dest_rect.bottom as f32 / scale_factor;
|
||||
let left = to_ndc_x(logical_left, logical_size.width.max(1.0));
|
||||
let right = to_ndc_x(logical_right, logical_size.width.max(1.0));
|
||||
let top = to_ndc_y(logical_top, logical_size.height.max(1.0));
|
||||
let bottom = to_ndc_y(logical_bottom, logical_size.height.max(1.0));
|
||||
|
||||
let color = color_to_f32(color);
|
||||
let clip_rect = clip_rect_array(clip);
|
||||
@@ -1890,7 +1902,7 @@ fn push_glyph_vertices(
|
||||
vertices.extend_from_slice(&[
|
||||
TextVertex {
|
||||
position: [left, top],
|
||||
world_position: [dest_rect.left as f32, dest_rect.top as f32],
|
||||
world_position: [logical_left, logical_top],
|
||||
uv: [uv_rect.0, uv_rect.1],
|
||||
color,
|
||||
clip_rect,
|
||||
@@ -1899,7 +1911,7 @@ fn push_glyph_vertices(
|
||||
},
|
||||
TextVertex {
|
||||
position: [left, bottom],
|
||||
world_position: [dest_rect.left as f32, dest_rect.bottom as f32],
|
||||
world_position: [logical_left, logical_bottom],
|
||||
uv: [uv_rect.0, uv_rect.3],
|
||||
color,
|
||||
clip_rect,
|
||||
@@ -1908,7 +1920,7 @@ fn push_glyph_vertices(
|
||||
},
|
||||
TextVertex {
|
||||
position: [right, top],
|
||||
world_position: [dest_rect.right as f32, dest_rect.top as f32],
|
||||
world_position: [logical_right, logical_top],
|
||||
uv: [uv_rect.2, uv_rect.1],
|
||||
color,
|
||||
clip_rect,
|
||||
@@ -1917,7 +1929,7 @@ fn push_glyph_vertices(
|
||||
},
|
||||
TextVertex {
|
||||
position: [right, top],
|
||||
world_position: [dest_rect.right as f32, dest_rect.top as f32],
|
||||
world_position: [logical_right, logical_top],
|
||||
uv: [uv_rect.2, uv_rect.1],
|
||||
color,
|
||||
clip_rect,
|
||||
@@ -1926,7 +1938,7 @@ fn push_glyph_vertices(
|
||||
},
|
||||
TextVertex {
|
||||
position: [left, bottom],
|
||||
world_position: [dest_rect.left as f32, dest_rect.bottom as f32],
|
||||
world_position: [logical_left, logical_bottom],
|
||||
uv: [uv_rect.0, uv_rect.3],
|
||||
color,
|
||||
clip_rect,
|
||||
@@ -1935,7 +1947,7 @@ fn push_glyph_vertices(
|
||||
},
|
||||
TextVertex {
|
||||
position: [right, bottom],
|
||||
world_position: [dest_rect.right as f32, dest_rect.bottom as f32],
|
||||
world_position: [logical_right, logical_bottom],
|
||||
uv: [uv_rect.2, uv_rect.3],
|
||||
color,
|
||||
clip_rect,
|
||||
@@ -2484,12 +2496,22 @@ fn pixel_rect_tuple(rect: PixelRect) -> (i32, i32, i32, i32) {
|
||||
(rect.left, rect.top, rect.right, rect.bottom)
|
||||
}
|
||||
|
||||
fn scale_glyph_cache_key(cache_key: CacheKey, scale_factor: f32) -> CacheKey {
|
||||
CacheKey {
|
||||
font_size_bits: (f32::from_bits(cache_key.font_size_bits) * scale_factor.max(1.0))
|
||||
.to_bits(),
|
||||
..cache_key
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
ActiveClip, blend_rgba, build_text_vertices, build_vertices, clip_rect_array,
|
||||
push_clip_state, rounded_clip_arrays, text_texture_key,
|
||||
ActiveClip, AtlasRect, PixelRect, blend_rgba, build_text_vertices, build_vertices,
|
||||
clip_rect_array, push_clip_state, push_glyph_vertices, rounded_clip_arrays,
|
||||
scale_glyph_cache_key, text_texture_key,
|
||||
};
|
||||
use cosmic_text::{CacheKey, CacheKeyFlags, SubpixelBin, fontdb};
|
||||
use ruin_ui::{ClipRegion, Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
|
||||
|
||||
#[test]
|
||||
@@ -2568,6 +2590,58 @@ mod tests {
|
||||
assert_ne!(first, different_pixel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_glyph_cache_key_scales_font_size_only() {
|
||||
let key = CacheKey {
|
||||
font_id: fontdb::ID::dummy(),
|
||||
glyph_id: 42,
|
||||
font_size_bits: 13.0_f32.to_bits(),
|
||||
x_bin: SubpixelBin::One,
|
||||
y_bin: SubpixelBin::Two,
|
||||
font_weight: fontdb::Weight::BOLD,
|
||||
flags: CacheKeyFlags::FAKE_ITALIC,
|
||||
};
|
||||
|
||||
let scaled = scale_glyph_cache_key(key, 2.0);
|
||||
|
||||
assert_eq!(f32::from_bits(scaled.font_size_bits), 26.0);
|
||||
assert_eq!(scaled.font_id, key.font_id);
|
||||
assert_eq!(scaled.glyph_id, key.glyph_id);
|
||||
assert_eq!(scaled.x_bin, key.x_bin);
|
||||
assert_eq!(scaled.y_bin, key.y_bin);
|
||||
assert_eq!(scaled.font_weight, key.font_weight);
|
||||
assert_eq!(scaled.flags, key.flags);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaled_atlas_vertices_convert_device_pixels_to_logical_space() {
|
||||
let mut vertices = Vec::new();
|
||||
push_glyph_vertices(
|
||||
&mut vertices,
|
||||
PixelRect {
|
||||
left: 20,
|
||||
top: 10,
|
||||
right: 40,
|
||||
bottom: 30,
|
||||
},
|
||||
AtlasRect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
None,
|
||||
UiSize::new(100.0, 100.0),
|
||||
2.0,
|
||||
Color::rgb(0xFF, 0xFF, 0xFF),
|
||||
ActiveClip::default(),
|
||||
);
|
||||
|
||||
assert_eq!(vertices.len(), 6);
|
||||
assert_eq!(vertices[0].world_position, [10.0, 5.0]);
|
||||
assert_eq!(vertices[5].world_position, [20.0, 15.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_clip_with_empty_intersection_stays_clipped() {
|
||||
let mut clip_stack = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user