From 1a083ee12c67fa976a895fcb31b473e117e372a6 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 15 May 2026 16:55:07 -0700 Subject: [PATCH] More cleanup --- lib/ruin_app/src/lib.rs | 10 +- .../src/platform/macos_aarch64/driver.rs | 58 ++--- lib/runtime/src/sys/macos/fs.rs | 15 +- lib/ui_platform_macos/src/lib.rs | 200 ++++++++++++++---- lib/ui_renderer_wgpu/src/lib.rs | 117 ++++++++-- 5 files changed, 303 insertions(+), 97 deletions(-) diff --git a/lib/ruin_app/src/lib.rs b/lib/ruin_app/src/lib.rs index a296fca..bc10aeb 100644 --- a/lib/ruin_app/src/lib.rs +++ b/lib/ruin_app/src/lib.rs @@ -254,6 +254,7 @@ impl MountedApp { 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 MountedApp { ); 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 MountedApp { &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 MountedApp { &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 MountedApp { _ => {} } } + if should_flush_reactive_work { + current_reactor().flush_now(); + } } ui.shutdown()?; diff --git a/lib/runtime/src/platform/macos_aarch64/driver.rs b/lib/runtime/src/platform/macos_aarch64/driver.rs index d8389ec..7d75108 100644 --- a/lib/runtime/src/platform/macos_aarch64/driver.rs +++ b/lib/runtime/src/platform/macos_aarch64/driver.rs @@ -99,7 +99,7 @@ pub struct Driver { pending_wakes: Cell, pending_timers: Cell, next_fd_token: Cell, - fd_waiters: RefCell>>, + fd_waiters: RefCell>, } /// Creates a new driver and its paired [`ThreadNotifier`]. @@ -241,17 +241,31 @@ impl Driver { completion: FdCompletion, ) -> io::Result { 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() { - empty_key = Some(*key); - } + 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 { } } -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(()) } diff --git a/lib/runtime/src/sys/macos/fs.rs b/lib/runtime/src/sys/macos/fs.rs index 1fb241e..163d192 100644 --- a/lib/runtime/src/sys/macos/fs.rs +++ b/lib/runtime/src/sys/macos/fs.rs @@ -21,6 +21,7 @@ use crate::platform::current::runtime::{ThreadHandle, current_thread_handle}; type BlockingTask = Box; static BLOCKING_POOL: OnceLock> = OnceLock::new(); +const BLOCKING_QUEUE_CAPACITY: usize = 1024; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ExecutionPath { @@ -367,16 +368,20 @@ async fn offload( } struct BlockingPool { - sender: mpsc::Sender, + sender: mpsc::SyncSender, } 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 { - let (sender, receiver) = mpsc::channel::(); + let (sender, receiver) = mpsc::sync_channel::(BLOCKING_QUEUE_CAPACITY); let receiver = Arc::new(Mutex::new(receiver)); let worker_count = std::thread::available_parallelism() .map(usize::from) diff --git a/lib/ui_platform_macos/src/lib.rs b/lib/ui_platform_macos/src/lib.rs index 609b4f0..745f9dc 100644 --- a/lib/ui_platform_macos/src/lib.rs +++ b/lib/ui_platform_macos/src/lib.rs @@ -91,6 +91,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 +149,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,69 +163,124 @@ fn lifecycle_for_visibility(visible: bool) -> WindowLifecycle { } } +struct ObjcStrong { + object: Option>, +} + +impl ObjcStrong { + unsafe fn from_owned(object: *mut AnyObject) -> Option { + 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, + window: ObjcStrong, #[allow(dead_code)] view: *mut AnyObject, - delegate: *mut AnyObject, + delegate: Option, _metal_layer: Retained, renderer: Option, } 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 } 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::()]; + let _: () = objc2::msg_send![self.window_object(), makeKeyAndOrderFront: std::ptr::null_mut::()]; } } fn hide(&self) { unsafe { - let _: () = objc2::msg_send![self.window, orderOut: std::ptr::null_mut::()]; + let _: () = + objc2::msg_send![self.window_object(), orderOut: std::ptr::null_mut::()]; } } 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, inLiveResize] } + } + + fn display_if_needed(&self) { unsafe { let _: () = objc2::msg_send![self.view, displayIfNeeded]; - let _: () = objc2::msg_send![self.window, displayIfNeeded]; - let _: () = objc2::msg_send![objc2::class!(CATransaction), flush]; + let _: () = objc2::msg_send![self.window_object(), displayIfNeeded]; } - in_live_resize } - fn detach_delegate(&self) { + fn flush_core_animation_transaction(&self) { unsafe { - let _: () = - objc2::msg_send![self.window, setDelegate: std::ptr::null_mut::()]; - let _: () = objc2::msg_send![self.delegate, release]; + let _: () = objc2::msg_send![objc2::class!(CATransaction), flush]; } } + + fn detach_delegate(&mut self) { + unsafe { + let _: () = objc2::msg_send![self.window_object(), setDelegate: std::ptr::null_mut::()]; + } + if let Some(mut delegate) = self.delegate.take() { + delegate.release_now(); + } + } +} + +impl Drop for NativeWindow { + fn drop(&mut self) { + self.detach_delegate(); + } } #[derive(Default)] @@ -1176,10 +1242,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; @@ -1321,8 +1383,8 @@ 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", @@ -1356,8 +1418,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 +1432,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 +1441,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; @@ -1474,9 +1537,8 @@ fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option Option Option()]; 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)?; @@ -1529,9 +1592,9 @@ fn create_native_window(window: &MacosWindow, app: *mut AnyObject) -> Option Optio 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> { let view_ptr = NonNull::new(view.cast::())?; 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 { 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 +1883,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; }; @@ -1938,7 +2024,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 +2051,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,7 +2064,9 @@ fn sync_window_metrics(window: &mut MacosWindow) { #[cfg(test)] mod tests { - use super::{MacosWindowState, ResizePipeline}; + use super::{ + MacosWindowState, PresentationFlushPolicy, ResizePipeline, presentation_flush_policy, + }; use ruin_ui::{UiSize, WindowLifecycle}; #[test] @@ -2045,6 +2133,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 +2184,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); + } } diff --git a/lib/ui_renderer_wgpu/src/lib.rs b/lib/ui_renderer_wgpu/src/lib.rs index 47bba4c..85cdc15 100644 --- a/lib/ui_renderer_wgpu/src/lib.rs +++ b/lib/ui_renderer_wgpu/src/lib.rs @@ -795,9 +795,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4 { } } 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 +1068,6 @@ fn fs_main(input: VertexOut) -> @location(0) vec4 { logical_size: UiSize, perf: &mut AtlasTextPerfStats, ) -> Option { - 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 +1099,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4 { 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 +1111,10 @@ fn fs_main(input: VertexOut) -> @location(0) vec4 { 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 +1123,12 @@ fn fs_main(input: VertexOut) -> @location(0) vec4 { 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 +1144,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4 { atlas_glyph.atlas_rect, clip_rect, logical_size, + self.scale_factor, resolved_color, active_clip, ); @@ -1872,6 +1875,7 @@ fn push_glyph_vertices( atlas_rect: AtlasRect, clip_rect: Option, logical_size: UiSize, + scale_factor: f32, color: Color, clip: ActiveClip, ) { @@ -1879,10 +1883,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 +1899,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 +1908,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 +1917,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 +1926,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 +1935,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 +1944,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 +2493,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 +2587,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();