Skip to main content

siglus_scene_vm/
pump_host.rs

1//! Desktop pump-mode FFI for macOS bundle launchers and other GUI hosts.
2//!
3//! A bundle UI can own its process/UI lifecycle and drive Siglus with
4//! `siglus_pump_step` rather than entering winit's blocking `run_app`.
5
6#![cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
7
8use std::ffi::{CStr, c_char, c_void};
9use std::path::PathBuf;
10use std::time::Duration;
11
12use winit::application::ApplicationHandler;
13use winit::dpi::{LogicalPosition, LogicalSize};
14use winit::event::{ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent};
15use winit::event_loop::pump_events::{EventLoopExtPumpEvents, PumpStatus};
16use winit::event_loop::run_on_demand::EventLoopExtRunOnDemand;
17use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
18use winit::keyboard::{KeyCode, PhysicalKey};
19use winit::window::{Window, WindowAttributes, WindowId};
20
21use crate::host::{
22    SiglusHost, SiglusHostConfig, SiglusNativeMessageBoxCallback, cstr_opt, cstr_required,
23    parse_bool_exit,
24};
25use crate::render::Renderer;
26use crate::runtime::game_display_info::resolve_game_name_from_project_dir;
27use crate::runtime::input::{VmKey, VmMouseButton};
28
29pub struct SiglusPumpHandle {
30    event_loop: EventLoop,
31    app: PumpApp,
32}
33
34struct PumpApp {
35    config: SiglusHostConfig,
36    window: Option<&'static dyn Window>,
37    window_id: Option<WindowId>,
38    host: Option<SiglusHost>,
39    init_error: Option<String>,
40    exit_requested: bool,
41    native_messagebox_callback: Option<SiglusNativeMessageBoxCallback>,
42    native_messagebox_user_data: *mut c_void,
43    /// Size the window and map input in physical pixels, ignoring the
44    /// display's HiDPI scale (the launcher app does this for every engine).
45    physical_pixels: bool,
46}
47
48impl PumpApp {
49    fn new(config: SiglusHostConfig) -> Self {
50        Self {
51            physical_pixels: false,
52            config,
53            window: None,
54            window_id: None,
55            host: None,
56            init_error: None,
57            exit_requested: false,
58            native_messagebox_callback: None,
59            native_messagebox_user_data: std::ptr::null_mut(),
60        }
61    }
62
63    /// The window's scale factor, or 1 in physical-pixel mode.
64    fn scale_factor(&self) -> f64 {
65        if self.physical_pixels {
66            return 1.0;
67        }
68        self.window.map_or(1.0, |w| w.scale_factor())
69    }
70
71    fn ensure_created(&mut self, elwt: &dyn ActiveEventLoop) {
72        if self.window.is_some() || self.init_error.is_some() {
73            return;
74        }
75        let width = self.config.width.unwrap_or(1280).max(1);
76        let height = self.config.height.unwrap_or(720).max(1);
77        let title = resolve_game_name_from_project_dir(&self.config.project_dir);
78        let attributes = WindowAttributes::default().with_title(title);
79        let attributes = if self.physical_pixels {
80            attributes.with_surface_size(winit::dpi::PhysicalSize::new(width, height))
81        } else {
82            attributes.with_surface_size(LogicalSize::new(width as f64, height as f64))
83        };
84        let window = match elwt.create_window(attributes) {
85            Ok(w) => w,
86            Err(e) => {
87                self.init_error = Some(format!("create window: {e:?}"));
88                elwt.exit();
89                return;
90            }
91        };
92        let window: &'static dyn Window = Box::leak(window);
93        let renderer = match pollster::block_on(Renderer::new(window)) {
94            Ok(r) => r,
95            Err(e) => {
96                self.init_error = Some(format!("renderer init: {e:?}"));
97                elwt.exit();
98                return;
99            }
100        };
101        let mut host = match pollster::block_on(SiglusHost::new_with_renderer(
102            self.config.clone(),
103            renderer,
104        )) {
105            Ok(h) => h,
106            Err(e) => {
107                self.init_error = Some(format!("host init: {e:?}"));
108                elwt.exit();
109                return;
110            }
111        };
112        host.set_native_messagebox_callback(
113            self.native_messagebox_callback,
114            self.native_messagebox_user_data,
115        );
116        if self.physical_pixels {
117            // The renderer starts from the window's own scale factor.
118            let size = window.surface_size();
119            host.resize(size.width.max(1), size.height.max(1), 1.0);
120        }
121        self.window_id = Some(window.id());
122        self.window = Some(window);
123        self.host = Some(host);
124        window.request_redraw();
125    }
126
127    fn handle_window_event(&mut self, event: WindowEvent, elwt: &dyn ActiveEventLoop) {
128        let scale = self.scale_factor();
129        let physical_pixels = self.physical_pixels;
130        let Some(host) = self.host.as_mut() else {
131            return;
132        };
133        match event {
134            WindowEvent::CloseRequested => {
135                self.exit_requested = true;
136                elwt.exit();
137            }
138            WindowEvent::SurfaceResized(size) => {
139                let sf = scale as f32;
140                host.resize(size.width.max(1), size.height.max(1), sf);
141            }
142            WindowEvent::KeyboardInput {
143                event:
144                    KeyEvent {
145                        state: ElementState::Pressed,
146                        physical_key: PhysicalKey::Code(code),
147                        text,
148                        ..
149                    },
150                ..
151            } => {
152                if let Some(k) = map_keycode(code) {
153                    host.key_down(k);
154                }
155                if host.vm_mut().ctx.editbox_accepts_direct_text()
156                    && let Some(text) = text.as_deref()
157                {
158                    host.text_input(text);
159                }
160            }
161            WindowEvent::KeyboardInput {
162                event:
163                    KeyEvent {
164                        state: ElementState::Released,
165                        physical_key: PhysicalKey::Code(code),
166                        ..
167                    },
168                ..
169            } => {
170                if let Some(k) = map_keycode(code) {
171                    host.key_up(k);
172                }
173            }
174            WindowEvent::Ime(Ime::Preedit(text, cursor)) => host.ime_preedit(&text, cursor),
175            WindowEvent::Ime(Ime::Commit(text)) => host.text_input(&text),
176            WindowEvent::Ime(Ime::Disabled) => host.ime_disabled(),
177            WindowEvent::Ime(Ime::Enabled) => {}
178            WindowEvent::PointerMoved {
179                position,
180                primary: true,
181                ..
182            }
183            | WindowEvent::PointerEntered {
184                position,
185                primary: true,
186                ..
187            } => {
188                let p = position.to_logical::<f64>(scale);
189                let (x, y) = (p.x, p.y);
190                host.mouse_move(x, y);
191            }
192            WindowEvent::PointerButton {
193                state,
194                button,
195                position,
196                primary: true,
197                ..
198            } => {
199                let Some(button) = button.mouse_button() else {
200                    return;
201                };
202                let point = position.to_logical::<f64>(scale);
203                host.mouse_move(point.x, point.y);
204                if let Some(b) = map_mouse_button(button) {
205                    match (state, b) {
206                        (ElementState::Pressed, VmMouseButton::Left) => {
207                            let (x, y) = current_mouse_pos(host);
208                            host.touch(0, x, y);
209                        }
210                        (ElementState::Released, VmMouseButton::Left) => {
211                            let (x, y) = current_mouse_pos(host);
212                            host.touch(2, x, y);
213                        }
214                        (ElementState::Pressed, b) => host.mouse_down(b),
215                        (ElementState::Released, b) => host.mouse_up(b),
216                    }
217                }
218            }
219            WindowEvent::MouseWheel { delta, .. } => {
220                let dy = match delta {
221                    MouseScrollDelta::LineDelta(_, y) => (y * 120.0) as i32,
222                    MouseScrollDelta::PixelDelta(p) => p.y.round() as i32,
223                    _ => 0,
224                };
225                host.mouse_wheel(dy);
226            }
227            WindowEvent::RedrawRequested => {
228                if let Some(window) = self.window.as_ref() {
229                    apply_ime_window_state(*window, host, physical_pixels);
230                }
231                let status = parse_bool_exit(host.step(16), "siglus_pump_step/redraw");
232                if status != 0 {
233                    self.exit_requested = true;
234                    elwt.exit();
235                }
236            }
237            _ => {}
238        }
239    }
240}
241
242impl ApplicationHandler for PumpApp {
243    fn can_create_surfaces(&mut self, elwt: &dyn ActiveEventLoop) {
244        self.ensure_created(elwt);
245    }
246
247    fn window_event(
248        &mut self,
249        elwt: &dyn ActiveEventLoop,
250        window_id: WindowId,
251        event: WindowEvent,
252    ) {
253        if self.window_id == Some(window_id) {
254            self.handle_window_event(event, elwt);
255        }
256    }
257
258    fn about_to_wait(&mut self, elwt: &dyn ActiveEventLoop) {
259        if self.exit_requested {
260            elwt.exit();
261            return;
262        }
263        if let Some(w) = self.window.as_ref() {
264            w.request_redraw();
265        }
266        // elwt.set_control_flow(ControlFlow::WaitUntil(std::time::Instant::now() + Duration::from_millis(16)));
267        elwt.set_control_flow(ControlFlow::Poll);
268    }
269}
270
271fn apply_ime_window_state(window: &dyn Window, host: &mut SiglusHost, physical_pixels: bool) {
272    if let Some((x, y, width, height)) = host.vm_mut().ctx.focused_editbox_ime_area() {
273        let (position, size) = if physical_pixels {
274            (
275                winit::dpi::PhysicalPosition::new(x.max(0), y.max(0)).into(),
276                winit::dpi::PhysicalSize::new(width.max(1) as u32, height.max(1) as u32).into(),
277            )
278        } else {
279            (
280                LogicalPosition::new(x.max(0) as f64, y.max(0) as f64).into(),
281                LogicalSize::new(width.max(1) as f64, height.max(1) as f64).into(),
282            )
283        };
284        crate::ime::enable_ime(window, position, size);
285    } else {
286        crate::ime::disable_ime(window);
287    }
288}
289
290fn current_mouse_pos(host: &mut SiglusHost) -> (f64, f64) {
291    let input = &host.vm_mut().ctx.input;
292    (input.mouse_x as f64, input.mouse_y as f64)
293}
294
295fn map_mouse_button(b: MouseButton) -> Option<VmMouseButton> {
296    match b {
297        MouseButton::Left => Some(VmMouseButton::Left),
298        MouseButton::Right => Some(VmMouseButton::Right),
299        MouseButton::Middle => Some(VmMouseButton::Middle),
300        _ => None,
301    }
302}
303
304fn map_keycode(k: KeyCode) -> Option<VmKey> {
305    use KeyCode::*;
306    match k {
307        Escape => Some(VmKey::Escape),
308        Enter | NumpadEnter => Some(VmKey::Enter),
309        Space => Some(VmKey::Space),
310        Backspace => Some(VmKey::Backspace),
311        Delete => Some(VmKey::Delete),
312        Tab => Some(VmKey::Tab),
313        ShiftLeft | ShiftRight => Some(VmKey::Shift),
314        ControlLeft | ControlRight => Some(VmKey::Control),
315        MetaLeft | MetaRight => Some(VmKey::Meta),
316        AltLeft | AltRight => Some(VmKey::Alt),
317        Home => Some(VmKey::Home),
318        End => Some(VmKey::End),
319        ArrowLeft => Some(VmKey::ArrowLeft),
320        ArrowUp => Some(VmKey::ArrowUp),
321        ArrowRight => Some(VmKey::ArrowRight),
322        ArrowDown => Some(VmKey::ArrowDown),
323        KeyA => Some(VmKey::Letter('A')),
324        KeyB => Some(VmKey::Letter('B')),
325        KeyC => Some(VmKey::Letter('C')),
326        KeyD => Some(VmKey::Letter('D')),
327        KeyE => Some(VmKey::Letter('E')),
328        KeyF => Some(VmKey::Letter('F')),
329        KeyG => Some(VmKey::Letter('G')),
330        KeyH => Some(VmKey::Letter('H')),
331        KeyI => Some(VmKey::Letter('I')),
332        KeyJ => Some(VmKey::Letter('J')),
333        KeyK => Some(VmKey::Letter('K')),
334        KeyL => Some(VmKey::Letter('L')),
335        KeyM => Some(VmKey::Letter('M')),
336        KeyN => Some(VmKey::Letter('N')),
337        KeyO => Some(VmKey::Letter('O')),
338        KeyP => Some(VmKey::Letter('P')),
339        KeyQ => Some(VmKey::Letter('Q')),
340        KeyR => Some(VmKey::Letter('R')),
341        KeyS => Some(VmKey::Letter('S')),
342        KeyT => Some(VmKey::Letter('T')),
343        KeyU => Some(VmKey::Letter('U')),
344        KeyV => Some(VmKey::Letter('V')),
345        KeyW => Some(VmKey::Letter('W')),
346        KeyX => Some(VmKey::Letter('X')),
347        KeyY => Some(VmKey::Letter('Y')),
348        KeyZ => Some(VmKey::Letter('Z')),
349        Digit0 => Some(VmKey::Digit(0)),
350        Digit1 => Some(VmKey::Digit(1)),
351        Digit2 => Some(VmKey::Digit(2)),
352        Digit3 => Some(VmKey::Digit(3)),
353        Digit4 => Some(VmKey::Digit(4)),
354        Digit5 => Some(VmKey::Digit(5)),
355        Digit6 => Some(VmKey::Digit(6)),
356        Digit7 => Some(VmKey::Digit(7)),
357        Digit8 => Some(VmKey::Digit(8)),
358        Digit9 => Some(VmKey::Digit(9)),
359        F1 => Some(VmKey::F(1)),
360        F2 => Some(VmKey::F(2)),
361        F3 => Some(VmKey::F(3)),
362        F4 => Some(VmKey::F(4)),
363        F5 => Some(VmKey::F(5)),
364        F6 => Some(VmKey::F(6)),
365        F7 => Some(VmKey::F(7)),
366        F8 => Some(VmKey::F(8)),
367        F9 => Some(VmKey::F(9)),
368        F10 => Some(VmKey::F(10)),
369        F11 => Some(VmKey::F(11)),
370        F12 => Some(VmKey::F(12)),
371        _ => None,
372    }
373}
374
375/// Create a host to drive with `siglus_pump_step`, or return null on failure.
376///
377/// # Safety
378///
379/// If non-null, `game_root_utf8` must point to a NUL-terminated string in a
380/// single readable allocation of at most `isize::MAX` bytes, including the
381/// terminator. The bytes must remain valid and unchanged for this call.
382/// Call on the platform event-loop thread (the main thread on platforms that
383/// require it). Use and destroy the returned handle only on that thread.
384#[unsafe(no_mangle)]
385pub unsafe extern "C" fn siglus_pump_create(
386    game_root_utf8: *const c_char,
387) -> *mut SiglusPumpHandle {
388    let game_root = match unsafe { cstr_required(game_root_utf8, "game_root_utf8") } {
389        Ok(s) => s,
390        Err(e) => {
391            log::error!("siglus_pump_create: {e:?}");
392            return std::ptr::null_mut();
393        }
394    };
395    let config = SiglusHostConfig::new(PathBuf::from(game_root));
396    let mut event_loop = match EventLoop::new() {
397        Ok(el) => el,
398        Err(e) => {
399            log::error!("siglus_pump_create: EventLoop::new: {e:?}");
400            return std::ptr::null_mut();
401        }
402    };
403    let mut handle = Box::new(SiglusPumpHandle {
404        event_loop,
405        app: PumpApp::new(config),
406    });
407    {
408        let event_loop = &mut handle.event_loop;
409        let app = &mut handle.app;
410        let _ = event_loop.pump_app_events(Some(Duration::from_millis(0)), app);
411    }
412    Box::into_raw(handle)
413}
414
415/// Set the callback used to display native message boxes.
416///
417/// # Safety
418///
419/// If non-null, `handle` must be a live pointer returned by `siglus_pump_create`.
420/// Call on the thread that created it, with exclusive access to the handle
421/// for the entire call, including during callbacks.
422/// The callback and any data it accesses through `user_data` must remain valid
423/// until the callback is replaced or the handle is destroyed. The callback
424/// must not unwind or reenter functions that access this handle.
425#[unsafe(no_mangle)]
426pub unsafe extern "C" fn siglus_pump_set_native_messagebox_callback(
427    handle: *mut SiglusPumpHandle,
428    callback: Option<SiglusNativeMessageBoxCallback>,
429    user_data: *mut c_void,
430) {
431    if handle.is_null() {
432        return;
433    }
434    let h = unsafe { &mut *handle };
435    h.app.native_messagebox_callback = callback;
436    h.app.native_messagebox_user_data = user_data;
437    if let Some(host) = h.app.host.as_mut() {
438        host.set_native_messagebox_callback(callback, user_data);
439    }
440}
441
442/// Submit the result of a native message box.
443///
444/// # Safety
445///
446/// If non-null, `handle` must be a live pointer returned by `siglus_pump_create`.
447/// Call on the thread that created it, with exclusive access to the handle
448/// for the entire call, including during callbacks.
449#[unsafe(no_mangle)]
450pub unsafe extern "C" fn siglus_pump_submit_messagebox_result(
451    handle: *mut SiglusPumpHandle,
452    request_id: u64,
453    value: i64,
454) {
455    if handle.is_null() {
456        return;
457    }
458    let h = unsafe { &mut *handle };
459    if let Some(host) = h.app.host.as_mut() {
460        host.submit_native_messagebox_result(request_id, value);
461    }
462}
463
464/// Send committed text to the host.
465///
466/// # Safety
467///
468/// If non-null, `handle` must be a live pointer returned by `siglus_pump_create`.
469/// Call on the thread that created it, with exclusive access to the handle
470/// for the entire call, including during callbacks.
471/// If non-null, `text_utf8` must point to a NUL-terminated string in a single
472/// readable allocation of at most `isize::MAX` bytes, including the terminator.
473/// The bytes must remain valid and unchanged for this call.
474#[unsafe(no_mangle)]
475pub unsafe extern "C" fn siglus_pump_text_input(
476    handle: *mut SiglusPumpHandle,
477    text_utf8: *const c_char,
478) {
479    let Some(handle) = (unsafe { handle.as_mut() }) else {
480        return;
481    };
482    let Some(host) = handle.app.host.as_mut() else {
483        return;
484    };
485    if let Some(text) = unsafe { cstr_opt(text_utf8) } {
486        host.text_input(&text);
487    }
488}
489
490/// Update IME composition, or disable IME if the text pointer is null.
491///
492/// # Safety
493///
494/// If non-null, `handle` must be a live pointer returned by `siglus_pump_create`.
495/// Call on the thread that created it, with exclusive access to the handle
496/// for the entire call, including during callbacks.
497/// If non-null, `text_utf8` must point to a NUL-terminated string in a single
498/// readable allocation of at most `isize::MAX` bytes, including the terminator.
499/// The bytes must remain valid and unchanged for this call.
500#[unsafe(no_mangle)]
501pub unsafe extern "C" fn siglus_pump_ime_preedit(
502    handle: *mut SiglusPumpHandle,
503    text_utf8: *const c_char,
504    cursor_start: i32,
505    cursor_end: i32,
506) {
507    let Some(handle) = (unsafe { handle.as_mut() }) else {
508        return;
509    };
510    let Some(host) = handle.app.host.as_mut() else {
511        return;
512    };
513    if text_utf8.is_null() {
514        host.ime_disabled();
515        return;
516    }
517    let text = unsafe { CStr::from_ptr(text_utf8) }
518        .to_string_lossy()
519        .into_owned();
520    let cursor = if cursor_start >= 0 && cursor_end >= 0 {
521        Some((cursor_start as usize, cursor_end as usize))
522    } else {
523        None
524    };
525    host.ime_preedit(&text, cursor);
526}
527
528/// Send a key press to the host.
529///
530/// # Safety
531///
532/// If non-null, `handle` must be a live pointer returned by `siglus_pump_create`.
533/// Call on the thread that created it, with exclusive access to the handle
534/// for the entire call, including during callbacks.
535#[unsafe(no_mangle)]
536pub unsafe extern "C" fn siglus_pump_key_down(handle: *mut SiglusPumpHandle, key_code: i32) {
537    let Some(handle) = (unsafe { handle.as_mut() }) else {
538        return;
539    };
540    let Some(host) = handle.app.host.as_mut() else {
541        return;
542    };
543    host.key_down_code(key_code);
544}
545
546/// Send a key release to the host.
547///
548/// # Safety
549///
550/// If non-null, `handle` must be a live pointer returned by `siglus_pump_create`.
551/// Call on the thread that created it, with exclusive access to the handle
552/// for the entire call, including during callbacks.
553#[unsafe(no_mangle)]
554pub unsafe extern "C" fn siglus_pump_key_up(handle: *mut SiglusPumpHandle, key_code: i32) {
555    let Some(handle) = (unsafe { handle.as_mut() }) else {
556        return;
557    };
558    let Some(host) = handle.app.host.as_mut() else {
559        return;
560    };
561    host.key_up_code(key_code);
562}
563
564/// Pump pending events and return the host status.
565///
566/// # Safety
567///
568/// If non-null, `handle` must be a live pointer returned by `siglus_pump_create`.
569/// Call on the thread that created it, with exclusive access to the handle
570/// for the entire call, including during callbacks.
571#[unsafe(no_mangle)]
572pub unsafe extern "C" fn siglus_pump_step(handle: *mut SiglusPumpHandle, timeout_ms: u32) -> i32 {
573    if handle.is_null() {
574        return 2;
575    }
576    let h = unsafe { &mut *handle };
577    if let Some(w) = h.app.window.as_ref() {
578        w.request_redraw();
579    }
580    let status = {
581        let event_loop = &mut h.event_loop;
582        let app = &mut h.app;
583        event_loop.pump_app_events(Some(Duration::from_millis(timeout_ms.max(1) as u64)), app)
584    };
585    match status {
586        PumpStatus::Continue => 0,
587        _ => 1,
588    }
589}
590
591/// Destroy a pump host. A null pointer is ignored.
592///
593/// # Safety
594///
595/// If non-null, `handle` must be a live pointer returned by `siglus_pump_create`.
596/// Call on the thread that created it, with exclusive access to the handle
597/// for the entire call, including during callbacks.
598/// The handle must not have been destroyed before. It and all pointers into it
599/// become invalid after this call and must not be used again.
600#[unsafe(no_mangle)]
601pub unsafe extern "C" fn siglus_pump_destroy(handle: *mut SiglusPumpHandle) {
602    if handle.is_null() {
603        return;
604    }
605    drop(unsafe { Box::from_raw(handle) });
606}
607
608/// Run the host event loop until exit.
609///
610/// # Safety
611///
612/// If non-null, `game_root_utf8` must point to a NUL-terminated string in a
613/// single readable allocation of at most `isize::MAX` bytes, including the
614/// terminator. The bytes must remain valid and unchanged for this call.
615/// Call on the platform event-loop thread (the main thread on platforms that
616/// require it).
617#[unsafe(no_mangle)]
618pub unsafe extern "C" fn siglus_run_entry(game_root_utf8: *const c_char) -> i32 {
619    let game_root = match unsafe { cstr_required(game_root_utf8, "game_root_utf8") } {
620        Ok(s) => s,
621        Err(e) => {
622            log::error!("siglus_run_entry: {e:?}");
623            return 1;
624        }
625    };
626    run_game(&game_root, false)
627}
628
629/// Runs a game in its own window until it ends. With `physical_pixels`, the
630/// window is sized in physical pixels and input is mapped without the
631/// display's HiDPI scale (what the launcher app uses for every engine).
632pub fn run_game(game_root: &str, physical_pixels: bool) -> i32 {
633    let mut event_loop = match EventLoop::new() {
634        Ok(el) => el,
635        Err(e) => {
636            log::error!("siglus_run_entry: EventLoop::new: {e:?}");
637            return 1;
638        }
639    };
640    event_loop.set_control_flow(ControlFlow::Poll);
641    let config = SiglusHostConfig::new(PathBuf::from(game_root));
642    let mut app = PumpApp::new(config);
643    app.physical_pixels = physical_pixels;
644    match event_loop.run_app_on_demand(&mut app) {
645        Ok(()) => {
646            if let Some(e) = app.init_error {
647                log::error!("siglus_run_entry: {e}");
648                1
649            } else {
650                0
651            }
652        }
653        Err(e) => {
654            log::error!("siglus_run_entry: run_app: {e:?}");
655            1
656        }
657    }
658}