More cleanup
This commit is contained in:
@@ -254,6 +254,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut should_flush_reactive_work = false;
|
||||||
for event in iter::once(event).chain(ui.take_pending_events()) {
|
for event in iter::once(event).chain(ui.take_pending_events()) {
|
||||||
match event {
|
match event {
|
||||||
PlatformEvent::Configured {
|
PlatformEvent::Configured {
|
||||||
@@ -268,7 +269,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
);
|
);
|
||||||
let _ = viewport.set(configuration.actual_inner_size);
|
let _ = viewport.set(configuration.actual_inner_size);
|
||||||
configure_serial.update(|value| *value = value.wrapping_add(1));
|
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() => {
|
PlatformEvent::Pointer { window_id, event } if window_id == window.id() => {
|
||||||
Self::handle_pointer_event(
|
Self::handle_pointer_event(
|
||||||
@@ -279,7 +280,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
&mut input_state,
|
&mut input_state,
|
||||||
event,
|
event,
|
||||||
)?;
|
)?;
|
||||||
current_reactor().flush_now();
|
should_flush_reactive_work = true;
|
||||||
}
|
}
|
||||||
PlatformEvent::Keyboard { window_id, event } if window_id == window.id() => {
|
PlatformEvent::Keyboard { window_id, event } if window_id == window.id() => {
|
||||||
Self::handle_keyboard_event(
|
Self::handle_keyboard_event(
|
||||||
@@ -289,7 +290,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
&input_state,
|
&input_state,
|
||||||
event,
|
event,
|
||||||
)?;
|
)?;
|
||||||
current_reactor().flush_now();
|
should_flush_reactive_work = true;
|
||||||
}
|
}
|
||||||
PlatformEvent::CloseRequested { window_id } if window_id == window.id() => {
|
PlatformEvent::CloseRequested { window_id } if window_id == window.id() => {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
@@ -311,6 +312,9 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if should_flush_reactive_work {
|
||||||
|
current_reactor().flush_now();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.shutdown()?;
|
ui.shutdown()?;
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ pub struct Driver {
|
|||||||
pending_wakes: Cell<u64>,
|
pending_wakes: Cell<u64>,
|
||||||
pending_timers: Cell<u64>,
|
pending_timers: Cell<u64>,
|
||||||
next_fd_token: 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`].
|
/// Creates a new driver and its paired [`ThreadNotifier`].
|
||||||
@@ -241,17 +241,31 @@ impl Driver {
|
|||||||
completion: FdCompletion,
|
completion: FdCompletion,
|
||||||
) -> io::Result<FdReadinessToken> {
|
) -> io::Result<FdReadinessToken> {
|
||||||
let key = FdKey { fd, interest };
|
let key = FdKey { fd, interest };
|
||||||
let should_register = !self.fd_waiters.borrow().contains_key(&key);
|
let removed_stale_waiter = {
|
||||||
if should_register {
|
let mut waiters = self.fd_waiters.borrow_mut();
|
||||||
self.update_fd_interest(key, libc::EV_ADD | libc::EV_ENABLE)?;
|
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();
|
let token = self.allocate_fd_token();
|
||||||
|
self.update_fd_interest(key, libc::EV_ADD | libc::EV_ENABLE | libc::EV_ONESHOT)?;
|
||||||
self.fd_waiters
|
self.fd_waiters
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.entry(key)
|
.insert(key, FdWaiter { token, completion });
|
||||||
.or_default()
|
|
||||||
.push(FdWaiter { token, completion });
|
|
||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,12 +273,9 @@ impl Driver {
|
|||||||
let mut empty_key = None;
|
let mut empty_key = None;
|
||||||
{
|
{
|
||||||
let mut waiters = self.fd_waiters.borrow_mut();
|
let mut waiters = self.fd_waiters.borrow_mut();
|
||||||
for (key, entries) in waiters.iter_mut() {
|
for (key, entry) in waiters.iter() {
|
||||||
if let Some(index) = entries.iter().position(|entry| entry.token == token) {
|
if entry.token == token {
|
||||||
entries.swap_remove(index);
|
empty_key = Some(*key);
|
||||||
if entries.is_empty() {
|
|
||||||
empty_key = Some(*key);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,19 +383,13 @@ impl Driver {
|
|||||||
|
|
||||||
fn complete_fd_waiters(&self, fd: RawFd, interest: FdInterest, event: &libc::kevent) {
|
fn complete_fd_waiters(&self, fd: RawFd, interest: FdInterest, event: &libc::kevent) {
|
||||||
let key = FdKey { fd, interest };
|
let key = FdKey { fd, interest };
|
||||||
let waiters = self.fd_waiters.borrow_mut().remove(&key);
|
let waiter = self.fd_waiters.borrow_mut().remove(&key);
|
||||||
let Some(waiters) = waiters else {
|
let Some(waiter) = waiter else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = self.update_fd_interest(key, libc::EV_DELETE);
|
let result = fd_event_result(event, interest);
|
||||||
let result = fd_event_result(event);
|
waiter.completion.complete(result);
|
||||||
for waiter in waiters {
|
|
||||||
waiter.completion.complete(match &result {
|
|
||||||
Ok(()) => Ok(()),
|
|
||||||
Err(error) => Err(io::Error::new(error.kind(), error.to_string())),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 {
|
if event.flags & libc::EV_ERROR != 0 && event.data != 0 {
|
||||||
Err(io::Error::from_raw_os_error(event.data as i32))
|
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 {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use crate::platform::current::runtime::{ThreadHandle, current_thread_handle};
|
|||||||
type BlockingTask = Box<dyn FnOnce() + Send + 'static>;
|
type BlockingTask = Box<dyn FnOnce() + Send + 'static>;
|
||||||
|
|
||||||
static BLOCKING_POOL: OnceLock<io::Result<BlockingPool>> = OnceLock::new();
|
static BLOCKING_POOL: OnceLock<io::Result<BlockingPool>> = OnceLock::new();
|
||||||
|
const BLOCKING_QUEUE_CAPACITY: usize = 1024;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ExecutionPath {
|
pub enum ExecutionPath {
|
||||||
@@ -367,16 +368,20 @@ async fn offload<T: Send + 'static>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct BlockingPool {
|
struct BlockingPool {
|
||||||
sender: mpsc::Sender<BlockingTask>,
|
sender: mpsc::SyncSender<BlockingTask>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlockingPool {
|
impl BlockingPool {
|
||||||
fn spawn(&self, task: BlockingTask) -> io::Result<()> {
|
fn spawn(&self, task: BlockingTask) -> io::Result<()> {
|
||||||
self.sender.send(task).map_err(|_| {
|
self.sender.try_send(task).map_err(|error| match error {
|
||||||
io::Error::new(
|
mpsc::TrySendError::Full(_) => io::Error::new(
|
||||||
|
io::ErrorKind::WouldBlock,
|
||||||
|
"filesystem blocking worker queue is full",
|
||||||
|
),
|
||||||
|
mpsc::TrySendError::Disconnected(_) => io::Error::new(
|
||||||
io::ErrorKind::BrokenPipe,
|
io::ErrorKind::BrokenPipe,
|
||||||
"filesystem blocking worker pool has stopped",
|
"filesystem blocking worker pool has stopped",
|
||||||
)
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -393,7 +398,7 @@ fn blocking_pool() -> io::Result<&'static BlockingPool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn create_blocking_pool() -> io::Result<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 receiver = Arc::new(Mutex::new(receiver));
|
||||||
let worker_count = std::thread::available_parallelism()
|
let worker_count = std::thread::available_parallelism()
|
||||||
.map(usize::from)
|
.map(usize::from)
|
||||||
|
|||||||
@@ -91,6 +91,13 @@ struct MacosWindowState {
|
|||||||
has_presented_frame: bool,
|
has_presented_frame: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum PresentationFlushPolicy {
|
||||||
|
None,
|
||||||
|
DisplayIfNeeded,
|
||||||
|
DisplayAndTransaction,
|
||||||
|
}
|
||||||
|
|
||||||
impl MacosWindowState {
|
impl MacosWindowState {
|
||||||
fn new(desired_visible: bool) -> Self {
|
fn new(desired_visible: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -142,6 +149,10 @@ impl MacosWindowState {
|
|||||||
true
|
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 {
|
fn lifecycle_for_visibility(visible: bool) -> WindowLifecycle {
|
||||||
@@ -152,69 +163,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 {
|
struct NativeWindow {
|
||||||
window: *mut AnyObject,
|
window: ObjcStrong,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
view: *mut AnyObject,
|
view: *mut AnyObject,
|
||||||
delegate: *mut AnyObject,
|
delegate: Option<ObjcStrong>,
|
||||||
_metal_layer: Retained<CAMetalLayer>,
|
_metal_layer: Retained<CAMetalLayer>,
|
||||||
renderer: Option<WgpuSceneRenderer>,
|
renderer: Option<WgpuSceneRenderer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NativeWindow {
|
impl NativeWindow {
|
||||||
fn window_ptr(&self) -> usize {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_title(&self, title: &str) {
|
fn set_title(&self, title: &str) {
|
||||||
let title = NSString::from_str(title);
|
let title = NSString::from_str(title);
|
||||||
unsafe {
|
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) {
|
fn set_content_size(&self, size: UiSize) {
|
||||||
let content_size = CGSize::new(size.width as f64, size.height as f64);
|
let content_size = CGSize::new(size.width as f64, size.height as f64);
|
||||||
unsafe {
|
unsafe {
|
||||||
let _: () = objc2::msg_send![self.window, setContentSize: content_size];
|
let _: () = objc2::msg_send![self.window_object(), setContentSize: content_size];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&self) {
|
fn show(&self) {
|
||||||
unsafe {
|
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) {
|
fn hide(&self) {
|
||||||
unsafe {
|
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) {
|
fn close(&self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let _: () = objc2::msg_send![self.window, close];
|
let _: () = objc2::msg_send![self.window_object(), close];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush_display(&self) -> bool {
|
fn in_live_resize(&self) -> bool {
|
||||||
let in_live_resize: bool = unsafe { objc2::msg_send![self.view, inLiveResize] };
|
unsafe { objc2::msg_send![self.view, inLiveResize] }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_if_needed(&self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let _: () = objc2::msg_send![self.view, displayIfNeeded];
|
let _: () = objc2::msg_send![self.view, displayIfNeeded];
|
||||||
let _: () = objc2::msg_send![self.window, displayIfNeeded];
|
let _: () = objc2::msg_send![self.window_object(), displayIfNeeded];
|
||||||
let _: () = objc2::msg_send![objc2::class!(CATransaction), flush];
|
|
||||||
}
|
}
|
||||||
in_live_resize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn detach_delegate(&self) {
|
fn flush_core_animation_transaction(&self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let _: () =
|
let _: () = objc2::msg_send![objc2::class!(CATransaction), flush];
|
||||||
objc2::msg_send![self.window, setDelegate: std::ptr::null_mut::<AnyObject>()];
|
|
||||||
let _: () = objc2::msg_send![self.delegate, release];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn detach_delegate(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -1176,10 +1242,6 @@ fn handle_update_window(
|
|||||||
window.state.request_logical_close();
|
window.state.request_logical_close();
|
||||||
if window.native.is_some() {
|
if window.native.is_some() {
|
||||||
close_native_window(window);
|
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 {
|
} else {
|
||||||
window.state.finalize_close();
|
window.state.finalize_close();
|
||||||
emit_closed = true;
|
emit_closed = true;
|
||||||
@@ -1321,8 +1383,8 @@ fn try_present_scene(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.state.note_presented();
|
|
||||||
flush_presented_frame(window);
|
flush_presented_frame(window);
|
||||||
|
window.state.note_presented();
|
||||||
if let Some(next_resize) = finish_presented_resize(window, scene_viewport) {
|
if let Some(next_resize) = finish_presented_resize(window, scene_viewport) {
|
||||||
trace!(
|
trace!(
|
||||||
target: "ruin_ui_platform_macos::resize",
|
target: "ruin_ui_platform_macos::resize",
|
||||||
@@ -1356,8 +1418,8 @@ fn repaint_retained_scene_for_resize(window_id: WindowId, window: &mut MacosWind
|
|||||||
"repainting retained scene into resized drawable"
|
"repainting retained scene into resized drawable"
|
||||||
);
|
);
|
||||||
if render_scene_to_current_drawable(window_id, window, &scene) {
|
if render_scene_to_current_drawable(window_id, window, &scene) {
|
||||||
window.state.note_presented();
|
|
||||||
flush_presented_frame(window);
|
flush_presented_frame(window);
|
||||||
|
window.state.note_presented();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1370,6 +1432,7 @@ fn render_scene_to_current_drawable(
|
|||||||
let scene_viewport = scene.logical_size;
|
let scene_viewport = scene.logical_size;
|
||||||
let mut render_succeeded = false;
|
let mut render_succeeded = false;
|
||||||
if let Some(native) = window.native.as_mut() {
|
if let Some(native) = window.native.as_mut() {
|
||||||
|
let view = native.view_object();
|
||||||
let Some(renderer) = native.renderer.as_mut() else {
|
let Some(renderer) = native.renderer.as_mut() else {
|
||||||
trace!(
|
trace!(
|
||||||
target: "ruin_ui_platform_macos::resize",
|
target: "ruin_ui_platform_macos::resize",
|
||||||
@@ -1378,7 +1441,7 @@ fn render_scene_to_current_drawable(
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
configure_view_resize_policy(native.view);
|
configure_view_resize_policy(view);
|
||||||
renderer.set_scale_factor(window.configuration.scale_factor);
|
renderer.set_scale_factor(window.configuration.scale_factor);
|
||||||
let scale = window.configuration.scale_factor.max(1.0);
|
let scale = window.configuration.scale_factor.max(1.0);
|
||||||
let drawable_width = (current_viewport.width * scale).max(1.0) as u32;
|
let drawable_width = (current_viewport.width * scale).max(1.0) as u32;
|
||||||
@@ -1474,9 +1537,8 @@ fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option<Nat
|
|||||||
defer: false
|
defer: false
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
if window_obj.is_null() {
|
let window_owner = unsafe { ObjcStrong::from_owned(window_obj) }?;
|
||||||
return None;
|
let window_obj = window_owner.as_ptr();
|
||||||
}
|
|
||||||
|
|
||||||
let title = NSString::from_str(&window.spec.title);
|
let title = NSString::from_str(&window.spec.title);
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -1488,14 +1550,12 @@ fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option<Nat
|
|||||||
let view_class = content_view_class();
|
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_class, alloc] };
|
||||||
let view_obj: *mut AnyObject = unsafe { objc2::msg_send![view_obj, initWithFrame: rect] };
|
let view_obj: *mut AnyObject = unsafe { objc2::msg_send![view_obj, initWithFrame: rect] };
|
||||||
if view_obj.is_null() {
|
let mut view_owner = unsafe { ObjcStrong::from_owned(view_obj) }?;
|
||||||
return None;
|
let view_obj = view_owner.as_ptr();
|
||||||
}
|
|
||||||
let delegate_class = window_delegate_class();
|
let delegate_class = window_delegate_class();
|
||||||
let delegate_obj: *mut AnyObject = unsafe { objc2::msg_send![delegate_class, new] };
|
let delegate_obj: *mut AnyObject = unsafe { objc2::msg_send![delegate_class, new] };
|
||||||
if delegate_obj.is_null() {
|
let delegate_owner = unsafe { ObjcStrong::from_owned(delegate_obj) }?;
|
||||||
return None;
|
let delegate_obj = delegate_owner.as_ptr();
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let _: () = objc2::msg_send![
|
let _: () = objc2::msg_send![
|
||||||
@@ -1511,6 +1571,9 @@ fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option<Nat
|
|||||||
objc2::msg_send![window_obj, makeKeyAndOrderFront: std::ptr::null_mut::<AnyObject>()];
|
objc2::msg_send![window_obj, makeKeyAndOrderFront: std::ptr::null_mut::<AnyObject>()];
|
||||||
let _: () = objc2::msg_send![app, activateIgnoringOtherApps: true];
|
let _: () = objc2::msg_send![app, activateIgnoringOtherApps: true];
|
||||||
}
|
}
|
||||||
|
// NSWindow retains its content view. Drop our construction-time ownership so
|
||||||
|
// the window is the single owner from here.
|
||||||
|
view_owner.release_now();
|
||||||
|
|
||||||
let metal_layer = configure_metal_surface_layer(view_obj)?;
|
let metal_layer = configure_metal_surface_layer(view_obj)?;
|
||||||
|
|
||||||
@@ -1529,9 +1592,9 @@ fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option<Nat
|
|||||||
}
|
}
|
||||||
|
|
||||||
Some(NativeWindow {
|
Some(NativeWindow {
|
||||||
window: window_obj,
|
window: window_owner,
|
||||||
view: view_obj,
|
view: view_obj,
|
||||||
delegate: delegate_obj,
|
delegate: Some(delegate_owner),
|
||||||
_metal_layer: metal_layer,
|
_metal_layer: metal_layer,
|
||||||
renderer,
|
renderer,
|
||||||
})
|
})
|
||||||
@@ -1644,7 +1707,7 @@ fn apply_delegate_events(state: &mut MacosState, pending: &mut Vec<PlatformEvent
|
|||||||
}) else {
|
}) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if window.state.mark_close_requested_sent() {
|
if window.state.note_delegate_close_request() {
|
||||||
debug!(
|
debug!(
|
||||||
target: "ruin_ui_platform_macos::lifecycle",
|
target: "ruin_ui_platform_macos::lifecycle",
|
||||||
?window_id,
|
?window_id,
|
||||||
@@ -1730,6 +1793,19 @@ fn finish_presented_resize(window: &mut MacosWindow, presented: UiSize) -> Optio
|
|||||||
window.resize_pipeline.note_presented_scene(presented)
|
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>> {
|
fn configure_metal_surface_layer(view: *mut AnyObject) -> Option<Retained<CAMetalLayer>> {
|
||||||
let view_ptr = NonNull::new(view.cast::<c_void>())?;
|
let view_ptr = NonNull::new(view.cast::<c_void>())?;
|
||||||
let layer = unsafe { MetalLayer::from_ns_view(view_ptr) };
|
let layer = unsafe { MetalLayer::from_ns_view(view_ptr) };
|
||||||
@@ -1744,11 +1820,21 @@ fn flush_presented_frame(window: &MacosWindow) {
|
|||||||
let Some(native) = window.native.as_ref() else {
|
let Some(native) = window.native.as_ref() else {
|
||||||
return;
|
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!(
|
trace!(
|
||||||
target: "ruin_ui_platform_macos::resize",
|
target: "ruin_ui_platform_macos::resize",
|
||||||
in_live_resize,
|
in_live_resize,
|
||||||
"requested display after presented frame"
|
?policy,
|
||||||
|
"applied presentation flush policy after presented frame"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1797,7 +1883,7 @@ fn collect_window_state_events(
|
|||||||
let Some((window_ptr, view_ptr)) = window
|
let Some((window_ptr, view_ptr)) = window
|
||||||
.native
|
.native
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|native| (native.window, native.view))
|
.map(|native| (native.window_object(), native.view_object()))
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1938,7 +2024,7 @@ fn finalize_native_close(
|
|||||||
"finalizing native close"
|
"finalizing native close"
|
||||||
);
|
);
|
||||||
let should_emit_closed = window.state.lifecycle != WindowLifecycle::LogicalClosed;
|
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();
|
native.detach_delegate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1965,7 +2051,7 @@ fn sync_window_metrics(window: &mut MacosWindow) {
|
|||||||
let Some((window_ptr, view_ptr)) = window
|
let Some((window_ptr, view_ptr)) = window
|
||||||
.native
|
.native
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|native| (native.window, native.view))
|
.map(|native| (native.window_object(), native.view_object()))
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1978,7 +2064,9 @@ fn sync_window_metrics(window: &mut MacosWindow) {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{MacosWindowState, ResizePipeline};
|
use super::{
|
||||||
|
MacosWindowState, PresentationFlushPolicy, ResizePipeline, presentation_flush_policy,
|
||||||
|
};
|
||||||
use ruin_ui::{UiSize, WindowLifecycle};
|
use ruin_ui::{UiSize, WindowLifecycle};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2045,6 +2133,22 @@ mod tests {
|
|||||||
assert_eq!(pipeline.next_resize, Some(next));
|
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]
|
#[test]
|
||||||
fn window_state_distinguishes_hide_from_close() {
|
fn window_state_distinguishes_hide_from_close() {
|
||||||
let mut state = MacosWindowState::new(true);
|
let mut state = MacosWindowState::new(true);
|
||||||
@@ -2080,4 +2184,16 @@ mod tests {
|
|||||||
assert!(!state.close_in_progress);
|
assert!(!state.close_in_progress);
|
||||||
assert!(!state.has_presented_frame);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -795,9 +795,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
DisplayItem::Text(text) => {
|
DisplayItem::Text(text) => {
|
||||||
if (self.scale_factor - 1.0).abs() <= f32::EPSILON
|
if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
|
||||||
&& text.glyphs.iter().all(|glyph| glyph.cache_key.is_some())
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(uploaded) =
|
if let Some(uploaded) =
|
||||||
@@ -1070,9 +1068,6 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
logical_size: UiSize,
|
logical_size: UiSize,
|
||||||
perf: &mut AtlasTextPerfStats,
|
perf: &mut AtlasTextPerfStats,
|
||||||
) -> Option<UploadedAtlasText> {
|
) -> Option<UploadedAtlasText> {
|
||||||
if (self.scale_factor - 1.0).abs() > f32::EPSILON {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut vertices = Vec::new();
|
let mut vertices = Vec::new();
|
||||||
let mut clip_stack = Vec::new();
|
let mut clip_stack = Vec::new();
|
||||||
let mut active_clip = ActiveClip::default();
|
let mut active_clip = ActiveClip::default();
|
||||||
@@ -1104,7 +1099,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
if active_clip.rect_active && logical_clip_rect.is_none() {
|
if active_clip.rect_active && logical_clip_rect.is_none() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let clip_rect = logical_clip_rect.map(rect_to_pixel_rect);
|
let clip_rect = logical_clip_rect
|
||||||
|
.map(|rect| scale_rect_to_pixel_rect(rect, self.scale_factor));
|
||||||
|
|
||||||
// Only iterate glyphs whose lines fall within the clip rect.
|
// Only iterate glyphs whose lines fall within the clip rect.
|
||||||
// For large text nodes (e.g. full Cargo.lock) this reduces
|
// For large text nodes (e.g. full Cargo.lock) this reduces
|
||||||
@@ -1115,7 +1111,10 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
perf.glyphs_clip_culled += (all_count - visible.len()) as u32;
|
perf.glyphs_clip_culled += (all_count - visible.len()) as u32;
|
||||||
|
|
||||||
for glyph in visible {
|
for glyph in visible {
|
||||||
let Some(cache_key) = glyph.cache_key else {
|
let Some(cache_key) = glyph
|
||||||
|
.cache_key
|
||||||
|
.map(|cache_key| scale_glyph_cache_key(cache_key, self.scale_factor))
|
||||||
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let resolved_color = resolve_glyph_color(glyph.color, text.default_color);
|
let resolved_color = resolve_glyph_color(glyph.color, text.default_color);
|
||||||
@@ -1124,9 +1123,12 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Glyph positions are LOCAL; add text.origin for absolute screen coords.
|
// Atlas glyphs are rasterized in device pixels while scene geometry remains
|
||||||
let abs_x = (text.origin.x + glyph.position.x).round() as i32;
|
// logical. Build and clip in device space, then convert back for vertices.
|
||||||
let abs_y = (text.origin.y + glyph.position.y).round() as i32;
|
let abs_x =
|
||||||
|
((text.origin.x + glyph.position.x) * self.scale_factor).round() as i32;
|
||||||
|
let abs_y =
|
||||||
|
((text.origin.y + glyph.position.y) * self.scale_factor).round() as i32;
|
||||||
let glyph_rect = PixelRect {
|
let glyph_rect = PixelRect {
|
||||||
left: abs_x + atlas_glyph.placement_left,
|
left: abs_x + atlas_glyph.placement_left,
|
||||||
top: abs_y - atlas_glyph.placement_top,
|
top: abs_y - atlas_glyph.placement_top,
|
||||||
@@ -1142,6 +1144,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
atlas_glyph.atlas_rect,
|
atlas_glyph.atlas_rect,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
logical_size,
|
logical_size,
|
||||||
|
self.scale_factor,
|
||||||
resolved_color,
|
resolved_color,
|
||||||
active_clip,
|
active_clip,
|
||||||
);
|
);
|
||||||
@@ -1872,6 +1875,7 @@ fn push_glyph_vertices(
|
|||||||
atlas_rect: AtlasRect,
|
atlas_rect: AtlasRect,
|
||||||
clip_rect: Option<PixelRect>,
|
clip_rect: Option<PixelRect>,
|
||||||
logical_size: UiSize,
|
logical_size: UiSize,
|
||||||
|
scale_factor: f32,
|
||||||
color: Color,
|
color: Color,
|
||||||
clip: ActiveClip,
|
clip: ActiveClip,
|
||||||
) {
|
) {
|
||||||
@@ -1879,10 +1883,15 @@ fn push_glyph_vertices(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let left = to_ndc_x(dest_rect.left as f32, logical_size.width.max(1.0));
|
let scale_factor = scale_factor.max(1.0);
|
||||||
let right = to_ndc_x(dest_rect.right as f32, logical_size.width.max(1.0));
|
let logical_left = dest_rect.left as f32 / scale_factor;
|
||||||
let top = to_ndc_y(dest_rect.top as f32, logical_size.height.max(1.0));
|
let logical_top = dest_rect.top as f32 / scale_factor;
|
||||||
let bottom = to_ndc_y(dest_rect.bottom as f32, logical_size.height.max(1.0));
|
let logical_right = dest_rect.right as f32 / scale_factor;
|
||||||
|
let logical_bottom = dest_rect.bottom as f32 / scale_factor;
|
||||||
|
let left = to_ndc_x(logical_left, logical_size.width.max(1.0));
|
||||||
|
let right = to_ndc_x(logical_right, logical_size.width.max(1.0));
|
||||||
|
let top = to_ndc_y(logical_top, logical_size.height.max(1.0));
|
||||||
|
let bottom = to_ndc_y(logical_bottom, logical_size.height.max(1.0));
|
||||||
|
|
||||||
let color = color_to_f32(color);
|
let color = color_to_f32(color);
|
||||||
let clip_rect = clip_rect_array(clip);
|
let clip_rect = clip_rect_array(clip);
|
||||||
@@ -1890,7 +1899,7 @@ fn push_glyph_vertices(
|
|||||||
vertices.extend_from_slice(&[
|
vertices.extend_from_slice(&[
|
||||||
TextVertex {
|
TextVertex {
|
||||||
position: [left, top],
|
position: [left, top],
|
||||||
world_position: [dest_rect.left as f32, dest_rect.top as f32],
|
world_position: [logical_left, logical_top],
|
||||||
uv: [uv_rect.0, uv_rect.1],
|
uv: [uv_rect.0, uv_rect.1],
|
||||||
color,
|
color,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
@@ -1899,7 +1908,7 @@ fn push_glyph_vertices(
|
|||||||
},
|
},
|
||||||
TextVertex {
|
TextVertex {
|
||||||
position: [left, bottom],
|
position: [left, bottom],
|
||||||
world_position: [dest_rect.left as f32, dest_rect.bottom as f32],
|
world_position: [logical_left, logical_bottom],
|
||||||
uv: [uv_rect.0, uv_rect.3],
|
uv: [uv_rect.0, uv_rect.3],
|
||||||
color,
|
color,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
@@ -1908,7 +1917,7 @@ fn push_glyph_vertices(
|
|||||||
},
|
},
|
||||||
TextVertex {
|
TextVertex {
|
||||||
position: [right, top],
|
position: [right, top],
|
||||||
world_position: [dest_rect.right as f32, dest_rect.top as f32],
|
world_position: [logical_right, logical_top],
|
||||||
uv: [uv_rect.2, uv_rect.1],
|
uv: [uv_rect.2, uv_rect.1],
|
||||||
color,
|
color,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
@@ -1917,7 +1926,7 @@ fn push_glyph_vertices(
|
|||||||
},
|
},
|
||||||
TextVertex {
|
TextVertex {
|
||||||
position: [right, top],
|
position: [right, top],
|
||||||
world_position: [dest_rect.right as f32, dest_rect.top as f32],
|
world_position: [logical_right, logical_top],
|
||||||
uv: [uv_rect.2, uv_rect.1],
|
uv: [uv_rect.2, uv_rect.1],
|
||||||
color,
|
color,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
@@ -1926,7 +1935,7 @@ fn push_glyph_vertices(
|
|||||||
},
|
},
|
||||||
TextVertex {
|
TextVertex {
|
||||||
position: [left, bottom],
|
position: [left, bottom],
|
||||||
world_position: [dest_rect.left as f32, dest_rect.bottom as f32],
|
world_position: [logical_left, logical_bottom],
|
||||||
uv: [uv_rect.0, uv_rect.3],
|
uv: [uv_rect.0, uv_rect.3],
|
||||||
color,
|
color,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
@@ -1935,7 +1944,7 @@ fn push_glyph_vertices(
|
|||||||
},
|
},
|
||||||
TextVertex {
|
TextVertex {
|
||||||
position: [right, bottom],
|
position: [right, bottom],
|
||||||
world_position: [dest_rect.right as f32, dest_rect.bottom as f32],
|
world_position: [logical_right, logical_bottom],
|
||||||
uv: [uv_rect.2, uv_rect.3],
|
uv: [uv_rect.2, uv_rect.3],
|
||||||
color,
|
color,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
@@ -2484,12 +2493,22 @@ fn pixel_rect_tuple(rect: PixelRect) -> (i32, i32, i32, i32) {
|
|||||||
(rect.left, rect.top, rect.right, rect.bottom)
|
(rect.left, rect.top, rect.right, rect.bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scale_glyph_cache_key(cache_key: CacheKey, scale_factor: f32) -> CacheKey {
|
||||||
|
CacheKey {
|
||||||
|
font_size_bits: (f32::from_bits(cache_key.font_size_bits) * scale_factor.max(1.0))
|
||||||
|
.to_bits(),
|
||||||
|
..cache_key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
ActiveClip, blend_rgba, build_text_vertices, build_vertices, clip_rect_array,
|
ActiveClip, AtlasRect, PixelRect, blend_rgba, build_text_vertices, build_vertices,
|
||||||
push_clip_state, rounded_clip_arrays, text_texture_key,
|
clip_rect_array, push_clip_state, push_glyph_vertices, rounded_clip_arrays,
|
||||||
|
scale_glyph_cache_key, text_texture_key,
|
||||||
};
|
};
|
||||||
|
use cosmic_text::{CacheKey, CacheKeyFlags, SubpixelBin, fontdb};
|
||||||
use ruin_ui::{ClipRegion, Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
|
use ruin_ui::{ClipRegion, Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2568,6 +2587,58 @@ mod tests {
|
|||||||
assert_ne!(first, different_pixel);
|
assert_ne!(first, different_pixel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scale_glyph_cache_key_scales_font_size_only() {
|
||||||
|
let key = CacheKey {
|
||||||
|
font_id: fontdb::ID::dummy(),
|
||||||
|
glyph_id: 42,
|
||||||
|
font_size_bits: 13.0_f32.to_bits(),
|
||||||
|
x_bin: SubpixelBin::One,
|
||||||
|
y_bin: SubpixelBin::Two,
|
||||||
|
font_weight: fontdb::Weight::BOLD,
|
||||||
|
flags: CacheKeyFlags::FAKE_ITALIC,
|
||||||
|
};
|
||||||
|
|
||||||
|
let scaled = scale_glyph_cache_key(key, 2.0);
|
||||||
|
|
||||||
|
assert_eq!(f32::from_bits(scaled.font_size_bits), 26.0);
|
||||||
|
assert_eq!(scaled.font_id, key.font_id);
|
||||||
|
assert_eq!(scaled.glyph_id, key.glyph_id);
|
||||||
|
assert_eq!(scaled.x_bin, key.x_bin);
|
||||||
|
assert_eq!(scaled.y_bin, key.y_bin);
|
||||||
|
assert_eq!(scaled.font_weight, key.font_weight);
|
||||||
|
assert_eq!(scaled.flags, key.flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scaled_atlas_vertices_convert_device_pixels_to_logical_space() {
|
||||||
|
let mut vertices = Vec::new();
|
||||||
|
push_glyph_vertices(
|
||||||
|
&mut vertices,
|
||||||
|
PixelRect {
|
||||||
|
left: 20,
|
||||||
|
top: 10,
|
||||||
|
right: 40,
|
||||||
|
bottom: 30,
|
||||||
|
},
|
||||||
|
AtlasRect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
UiSize::new(100.0, 100.0),
|
||||||
|
2.0,
|
||||||
|
Color::rgb(0xFF, 0xFF, 0xFF),
|
||||||
|
ActiveClip::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(vertices.len(), 6);
|
||||||
|
assert_eq!(vertices[0].world_position, [10.0, 5.0]);
|
||||||
|
assert_eq!(vertices[5].world_position, [20.0, 15.0]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nested_clip_with_empty_intersection_stays_clipped() {
|
fn nested_clip_with_empty_intersection_stays_clipped() {
|
||||||
let mut clip_stack = Vec::new();
|
let mut clip_stack = Vec::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user