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.

59    fn toggle_fullscreen(
60        &mut self,
61        borderless_window: bool,
62        window_size: &Arc<AtomicWindowSize>,
63    ) -> Result<bool, sdl3::Error>;
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.

72    fn set_window_title(&mut self, title: &str) -> Result<(), NulError>;
73}
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}