Compare commits

..

4 Commits

Author SHA1 Message Date
Will Temple
23d9f02e3a review pass the last 2026-05-16 16:18:08 -04:00
Will Temple
d9ac6bfeb8 review pass 4 2026-05-16 15:23:43 -04:00
Will Temple
056f356065 Review pass 3 2026-05-16 15:16:49 -04:00
Will Temple
1a083ee12c More cleanup 2026-05-15 16:55:07 -07:00
7 changed files with 610 additions and 247 deletions

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ use std::net::{
}; };
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::pin::Pin; use std::pin::Pin;
use std::sync::{Arc, Mutex, OnceLock, mpsc};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
@@ -16,10 +17,14 @@ use crate::op::completion::completion_for_current_thread;
use crate::op::net::{AcceptedSocket, NetOp, ReceivedDatagram}; use crate::op::net::{AcceptedSocket, NetOp, ReceivedDatagram};
const DEFAULT_LISTENER_BACKLOG: i32 = 1024; 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 RecvFuture = Pin<Box<dyn Future<Output = io::Result<Vec<u8>>> + 'static>>;
type SendFuture = Pin<Box<dyn Future<Output = io::Result<usize>> + 'static>>; type SendFuture = Pin<Box<dyn Future<Output = io::Result<usize>> + 'static>>;
type ShutdownFuture = Pin<Box<dyn Future<Output = io::Result<()>> + '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)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExecutionPath { pub enum ExecutionPath {
@@ -391,16 +396,83 @@ async fn offload<T: Send + 'static>(
work: impl FnOnce() -> io::Result<T> + Send + 'static, work: impl FnOnce() -> io::Result<T> + Send + 'static,
) -> io::Result<T> { ) -> io::Result<T> {
let (future, handle) = completion_for_current_thread::<io::Result<T>>(); let (future, handle) = completion_for_current_thread::<io::Result<T>>();
let handle_for_thread = handle.clone(); if let Err(error) = dns_pool().and_then(|pool| {
if let Err(error) = thread::Builder::new() pool.spawn(Box::new({
.name("ruin-runtime-net-offload".into()) let handle = handle.clone();
.spawn(move || handle_for_thread.complete(work())) move || handle.complete(work())
{ }))
handle.complete(Err(io::Error::other(error))); }) {
handle.complete(Err(error));
} }
future.await 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>( async fn io_timeout<T>(
timeout: Duration, timeout: Duration,
future: impl Future<Output = io::Result<T>>, future: impl Future<Output = io::Result<T>>,
@@ -531,7 +603,7 @@ impl RawSocketAddr {
sin_family: libc::AF_INET as libc::sa_family_t, sin_family: libc::AF_INET as libc::sa_family_t,
sin_port: addr.port().to_be(), sin_port: addr.port().to_be(),
sin_addr: libc::in_addr { 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], sin_zero: [0; 8],
}; };

View File

@@ -34,6 +34,11 @@ pub enum WindowLifecycle {
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct WindowConfigured { 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 actual_inner_size: UiSize,
pub scale_factor: f32, pub scale_factor: f32,
pub visible: bool, pub visible: bool,

File diff suppressed because it is too large Load Diff

View File

@@ -369,6 +369,9 @@ impl WgpuSceneRenderer {
format, format,
width: width.max(1), width: width.max(1),
height: height.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, present_mode: wgpu::PresentMode::AutoNoVsync,
desired_maximum_frame_latency: 1, desired_maximum_frame_latency: 1,
alpha_mode: caps.alpha_modes[0], alpha_mode: caps.alpha_modes[0],
@@ -795,9 +798,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 +1071,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 +1102,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 +1114,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 +1126,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 +1147,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 +1878,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 +1886,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 +1902,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 +1911,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 +1920,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 +1929,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 +1938,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 +1947,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 +2496,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 +2590,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();