1use crate::NativeEmulatorResult; 2use jgenesis_renderer::renderer; 3use jgenesis_renderer::renderer::WgpuRenderer; 4use sdl3::VideoSubsystem; 5use sdl3::video::{FullscreenType, HitTestResult, Window}; 6use std::ffi::NulError; 7use std::ptr; 8use std::sync::Arc; 9use std::sync::atomic::{AtomicU32, Ordering}; 10 11pub struct AtomicWindowSize { 12 width: AtomicU32, 13 height: AtomicU32, 14 pixel_density: AtomicU32, // f32 bits as u32 15} 16 17impl AtomicWindowSize { 18 fn new(window: &Window) -> Self { 19 let window_size = size(window); 20 21 Self { 22 width: AtomicU32::new(window_size.width), 23 height: AtomicU32::new(window_size.height), 24 pixel_density: AtomicU32::new(window_size.pixel_density.to_bits()), 25 } 26 } 27 28 pub fn update(&self, window_size: renderer::WindowSize) { 29 self.width.store(window_size.width, Ordering::Relaxed); 30 self.height.store(window_size.height, Ordering::Relaxed); 31 self.pixel_density.store(window_size.pixel_density.to_bits(), Ordering::Relaxed); 32 } 33 34 fn width(&self) -> u32 { 35 self.width.load(Ordering::Relaxed) 36 } 37 38 fn height(&self) -> u32 { 39 self.height.load(Ordering::Relaxed) 40 } 41 42 fn pixel_density(&self) -> f32 { 43 f32::from_bits(self.pixel_density.load(Ordering::Relaxed)) 44 } 45} 46 47pub trait RendererExt { 48 fn focus(&mut self); 49 50 fn window_id(&self) -> u32; 51 52 fn is_fullscreen(&self) -> bool;
Toggle fullscreen on/off. Returns the new fullscreen state.
Errors
Propagates any SDL3 video errors.
65 fn update_borderless(&mut self, borderless: bool, window_size: &Arc<AtomicWindowSize>);
Change the window title.
Errors
Returns an error if the title contains any null characters.
75impl RendererExt for WgpuRenderer<Window> { 76 fn focus(&mut self) { 77 // SAFETY: This is not reassigning the window 78 unsafe { 79 self.window_mut().raise(); 80 } 81 } 82 83 fn window_id(&self) -> u32 { 84 self.window().id() 85 } 86 87 fn is_fullscreen(&self) -> bool { 88 matches!(self.window().fullscreen_state(), FullscreenType::Desktop | FullscreenType::True) 89 } 90 91 fn toggle_fullscreen( 92 &mut self, 93 borderless_window: bool, 94 window_size: &Arc<AtomicWindowSize>, 95 ) -> Result<bool, sdl3::Error> { 96 // SAFETY: This is not reassigning the window 97 unsafe { 98 let window = self.window_mut(); 99 let currently_fullscreen = window.fullscreen_state() != FullscreenType::Off; 100 let new_fullscreen = !currently_fullscreen; 101 window.set_fullscreen(new_fullscreen)?; 102 103 if borderless_window { 104 update_window_hit_test( 105 self.window_mut(), 106 new_fullscreen, 107 borderless_window, 108 window_size, 109 ); 110 } 111 112 Ok(new_fullscreen) 113 } 114 } 115 116 fn update_borderless(&mut self, borderless: bool, window_size: &Arc<AtomicWindowSize>) { 117 // SAFETY: This is not reassigning the window 118 unsafe { 119 self.window_mut().set_bordered(!borderless); 120 121 let fullscreen = self.is_fullscreen(); 122 update_window_hit_test(self.window_mut(), fullscreen, borderless, window_size); 123 } 124 } 125 126 fn set_window_title(&mut self, title: &str) -> Result<(), NulError> { 127 // SAFETY: This is not reassigning the window 128 unsafe { self.window_mut().set_title(title) } 129 } 130} 131 132pub struct CreateWindowArgs<'a> { 133 pub title: &'a str, 134 pub width: u32, 135 pub height: u32, 136 pub fullscreen: bool, 137 pub borderless: bool, 138} 139 140pub fn create( 141 video: &VideoSubsystem, 142 CreateWindowArgs { title, width, height, fullscreen, borderless }: CreateWindowArgs<'_>, 143) -> NativeEmulatorResult<(Window, Arc<AtomicWindowSize>)> { 144 let display_scale = video 145 .get_primary_display() 146 .ok() 147 .and_then(|display| display.get_content_scale().ok()) 148 .unwrap_or(1.0); 149 150 let mut window_builder = video.window( 151 title, 152 (width as f32 * display_scale).round() as u32, 153 (height as f32 * display_scale).round() as u32, 154 ); 155 window_builder.metal_view(); 156 window_builder.high_pixel_density(); 157 window_builder.resizable(); 158 window_builder.position_centered(); 159 160 if fullscreen { 161 window_builder.fullscreen(); 162 } 163 164 if borderless { 165 window_builder.borderless(); 166 } 167 168 let mut window = window_builder.build()?; 169 let window_size = Arc::new(AtomicWindowSize::new(&window)); 170 171 if borderless && !fullscreen { 172 set_window_hit_test(&mut window, borderless_hit_test(Arc::clone(&window_size))); 173 } 174 175 Ok((window, window_size)) 176} 177 178pub fn size(window: &Window) -> renderer::WindowSize { 179 let (width, height) = window.size_in_pixels(); 180 let pixel_density = window.pixel_density(); 181 182 renderer::WindowSize { width, height, pixel_density } 183} 184 185fn update_window_hit_test( 186 window: &mut Window, 187 fullscreen: bool, 188 borderless: bool, 189 window_size: &Arc<AtomicWindowSize>, 190) { 191 if borderless && !fullscreen { 192 set_window_hit_test(window, borderless_hit_test(Arc::clone(window_size))); 193 } else { 194 disable_window_hit_test(window); 195 } 196} 197 198fn set_window_hit_test( 199 window: &mut Window, 200 hit_test: impl Fn(sdl3::rect::Point) -> HitTestResult + 'static, 201) { 202 if let Err(err) = window.set_hit_test(hit_test) { 203 log::error!("Error setting window hit test: {err}"); 204 } 205} 206 207fn disable_window_hit_test(window: &mut Window) { 208 // Setting a hit test that always returns Normal works on Linux but not Windows; on Windows it 209 // makes the window non-resizable. Disabling hit testing works on every platform (that I've tested) 210 // 211 // SAFETY: Window pointer is guaranteed to be valid, and SDL3 documentation states that passing 212 // None/NULL for the callback disables hit testing. The third parameter (callback data) is not 213 // used when hit testing is disabled 214 unsafe { 215 if !sdl3_sys::video::SDL_SetWindowHitTest(window.raw(), None, ptr::null_mut()) { 216 log::error!("Error disabling window hit testing: {}", sdl3::get_error()); 217 } 218 } 219} 220 221fn borderless_hit_test( 222 window_size: Arc<AtomicWindowSize>, 223) -> impl Fn(sdl3::rect::Point) -> HitTestResult { 224 const RESIZE_BORDER_PIXELS: f64 = 20.0; 225 226 move |point| { 227 let width: f64 = window_size.width().into(); 228 let height: f64 = window_size.height().into(); 229 let pixel_density: f64 = window_size.pixel_density().into(); 230 231 let x = f64::from(point.x) * pixel_density; 232 let y = f64::from(point.y) * pixel_density; 233 234 let mut resize_left = x < RESIZE_BORDER_PIXELS; 235 let mut resize_right = x >= width - RESIZE_BORDER_PIXELS; 236 let mut resize_top = y < RESIZE_BORDER_PIXELS; 237 let mut resize_bottom = y >= height - RESIZE_BORDER_PIXELS; 238 239 if resize_left && resize_right { 240 resize_left = false; 241 resize_right = false; 242 } 243 244 if resize_top && resize_bottom { 245 resize_top = false; 246 resize_bottom = false; 247 } 248 249 if resize_top { 250 if resize_left { 251 HitTestResult::ResizeTopLeft 252 } else if resize_right { 253 HitTestResult::ResizeTopRight 254 } else { 255 HitTestResult::ResizeTop 256 } 257 } else if resize_bottom { 258 if resize_left { 259 HitTestResult::ResizeBottomLeft 260 } else if resize_right { 261 HitTestResult::ResizeBottomRight 262 } else { 263 HitTestResult::ResizeBottom 264 } 265 } else if resize_left { 266 HitTestResult::ResizeLeft 267 } else if resize_right { 268 HitTestResult::ResizeRight 269 } else { 270 HitTestResult::Draggable 271 } 272 } 273}