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::{c_char, c_void};
9use std::path::PathBuf;
10use std::time::Duration;
11
12use winit::dpi::{LogicalPosition, LogicalSize};
13use winit::event::{ElementState, Event, Ime, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent};
14use winit::application::ApplicationHandler;
15use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
16use winit::keyboard::{KeyCode, PhysicalKey};
17use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus};
18use winit::window::{Window, WindowAttributes, WindowId};
19
20use crate::host::{cstr_opt, cstr_required, parse_bool_exit, SiglusHost, SiglusHostConfig, SiglusNativeMessageBoxCallback};
21use crate::render::Renderer;
22use crate::runtime::game_display_info::resolve_game_name_from_project_dir;
23use crate::runtime::input::{VmKey, VmMouseButton};
24
25pub struct SiglusPumpHandle {
26    event_loop: EventLoop<()>,
27    app: PumpApp,
28}
29
30struct PumpApp {
31    config: SiglusHostConfig,
32    window: Option<&'static Window>,
33    window_id: Option<WindowId>,
34    host: Option<SiglusHost>,
35    init_error: Option<String>,
36    exit_requested: bool,
37    native_messagebox_callback: Option<SiglusNativeMessageBoxCallback>,
38    native_messagebox_user_data: *mut c_void,
39}
40
41impl PumpApp {
42    fn new(config: SiglusHostConfig) -> Self {
43        Self {
44            config,
45            window: None,
46            window_id: None,
47            host: None,
48            init_error: None,
49            exit_requested: false,
50            native_messagebox_callback: None,
51            native_messagebox_user_data: std::ptr::null_mut(),
52        }
53    }
54
55    fn ensure_created(&mut self, elwt: &ActiveEventLoop) {
56        if self.window.is_some() || self.init_error.is_some() {
57            return;
58        }
59        let width = self.config.width.unwrap_or(1280).max(1);
60        let height = self.config.height.unwrap_or(720).max(1);
61        let title = resolve_game_name_from_project_dir(&self.config.project_dir);
62        let window = match elwt.create_window(
63            WindowAttributes::default()
64                .with_title(title)
65                .with_inner_size(LogicalSize::new(width as f64, height as f64)),
66        ) {
67            Ok(w) => w,
68            Err(e) => {
69                self.init_error = Some(format!("create window: {e:?}"));
70                elwt.exit();
71                return;
72            }
73        };
74        let window: &'static Window = Box::leak(Box::new(window));
75        let renderer = match pollster::block_on(Renderer::new(window)) {
76            Ok(r) => r,
77            Err(e) => {
78                self.init_error = Some(format!("renderer init: {e:?}"));
79                elwt.exit();
80                return;
81            }
82        };
83        let mut host = match pollster::block_on(SiglusHost::new_with_renderer(self.config.clone(), renderer)) {
84            Ok(h) => h,
85            Err(e) => {
86                self.init_error = Some(format!("host init: {e:?}"));
87                elwt.exit();
88                return;
89            }
90        };
91        host.set_native_messagebox_callback(self.native_messagebox_callback, self.native_messagebox_user_data);
92        self.window_id = Some(window.id());
93        self.window = Some(window);
94        self.host = Some(host);
95        window.request_redraw();
96    }
97
98    fn handle_event(&mut self, event: Event<()>, elwt: &ActiveEventLoop) {
99        match event {
100            Event::Resumed => self.ensure_created(elwt),
101            Event::WindowEvent { window_id, event } if self.window_id == Some(window_id) => {
102                self.handle_window_event(event, elwt);
103            }
104            Event::AboutToWait => {
105                if self.exit_requested {
106                    elwt.exit();
107                    return;
108                }
109                if let Some(w) = self.window.as_ref() {
110                    w.request_redraw();
111                }
112            }
113            _ => {}
114        }
115    }
116
117    fn handle_window_event(&mut self, event: WindowEvent, elwt: &ActiveEventLoop) {
118        let Some(host) = self.host.as_mut() else {
119            return;
120        };
121        match event {
122            WindowEvent::CloseRequested => {
123                self.exit_requested = true;
124                elwt.exit();
125            }
126            WindowEvent::Resized(size) => {
127                let sf = self.window.as_ref().map(|w| w.scale_factor() as f32).unwrap_or(1.0);
128                host.resize(size.width.max(1), size.height.max(1), sf);
129            }
130            WindowEvent::KeyboardInput {
131                event: KeyEvent { state: ElementState::Pressed, physical_key: PhysicalKey::Code(code), .. },
132                ..
133            } => {
134                if let Some(k) = map_keycode(code) {
135                    host.key_down(k);
136                }
137            }
138            WindowEvent::KeyboardInput {
139                event: KeyEvent { state: ElementState::Released, physical_key: PhysicalKey::Code(code), .. },
140                ..
141            } => {
142                if let Some(k) = map_keycode(code) {
143                    host.key_up(k);
144                }
145            }
146            WindowEvent::Ime(Ime::Commit(text)) => host.text_input(&text),
147            WindowEvent::CursorMoved { position, .. } => {
148                let (x, y) = if let Some(w) = self.window.as_ref() {
149                    let p = position.to_logical::<f64>(w.scale_factor());
150                    (p.x, p.y)
151                } else {
152                    (position.x, position.y)
153                };
154                host.mouse_move(x, y);
155            }
156            WindowEvent::MouseInput { state, button, .. } => {
157                if let Some(b) = map_mouse_button(button) {
158                    match state {
159                        ElementState::Pressed => {
160                            if matches!(b, VmMouseButton::Left) {
161                                let (x, y) = current_mouse_pos(host);
162                                host.touch(0, x, y);
163                            } else {
164                                host.mouse_down(b);
165                            }
166                        }
167                        ElementState::Released => {
168                            if matches!(b, VmMouseButton::Left) {
169                                let (x, y) = current_mouse_pos(host);
170                                host.touch(2, x, y);
171                            } else {
172                                host.mouse_up(b);
173                            }
174                        }
175                    }
176                }
177            }
178            WindowEvent::MouseWheel { delta, .. } => {
179                let dy = match delta {
180                    MouseScrollDelta::LineDelta(_, y) => (y * 120.0) as i32,
181                    MouseScrollDelta::PixelDelta(p) => p.y.round() as i32,
182                };
183                host.mouse_wheel(dy);
184            }
185            WindowEvent::RedrawRequested => {
186                if let Some(window) = self.window.as_ref() {
187                    apply_ime_window_state(window, host);
188                }
189                let status = parse_bool_exit(host.step(16), "siglus_pump_step/redraw");
190                if status != 0 {
191                    self.exit_requested = true;
192                    elwt.exit();
193                }
194            }
195            _ => {}
196        }
197    }
198}
199
200
201impl ApplicationHandler for PumpApp {
202    fn resumed(&mut self, elwt: &ActiveEventLoop) {
203        self.ensure_created(elwt);
204    }
205
206    fn window_event(&mut self, elwt: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) {
207        if self.window_id == Some(window_id) {
208            self.handle_window_event(event, elwt);
209        }
210    }
211
212    fn about_to_wait(&mut self, elwt: &ActiveEventLoop) {
213        if self.exit_requested {
214            elwt.exit();
215            return;
216        }
217        if let Some(w) = self.window.as_ref() {
218            w.request_redraw();
219        }
220        elwt.set_control_flow(ControlFlow::WaitUntil(std::time::Instant::now() + Duration::from_millis(16)));
221    }
222}
223
224fn apply_ime_window_state(window: &Window, host: &mut SiglusHost) {
225    if let Some((x, y, width, height)) = host.vm_mut().ctx.focused_editbox_ime_area() {
226        window.set_ime_allowed(true);
227        window.set_ime_cursor_area(
228            LogicalPosition::new(x.max(0) as f64, y.max(0) as f64),
229            LogicalSize::new(width.max(1) as f64, height.max(1) as f64),
230        );
231    } else {
232        window.set_ime_allowed(false);
233    }
234}
235
236fn current_mouse_pos(host: &mut SiglusHost) -> (f64, f64) {
237    let input = &host.vm_mut().ctx.input;
238    (input.mouse_x as f64, input.mouse_y as f64)
239}
240
241fn map_mouse_button(b: MouseButton) -> Option<VmMouseButton> {
242    match b {
243        MouseButton::Left => Some(VmMouseButton::Left),
244        MouseButton::Right => Some(VmMouseButton::Right),
245        MouseButton::Middle => Some(VmMouseButton::Middle),
246        _ => None,
247    }
248}
249
250fn map_keycode(k: KeyCode) -> Option<VmKey> {
251    use KeyCode::*;
252    match k {
253        Escape => Some(VmKey::Escape),
254        Enter => Some(VmKey::Enter),
255        Space => Some(VmKey::Space),
256        Backspace => Some(VmKey::Backspace),
257        Tab => Some(VmKey::Tab),
258        ShiftLeft | ShiftRight => Some(VmKey::Shift),
259        AltLeft | AltRight => Some(VmKey::Alt),
260        ArrowLeft => Some(VmKey::ArrowLeft),
261        ArrowUp => Some(VmKey::ArrowUp),
262        ArrowRight => Some(VmKey::ArrowRight),
263        ArrowDown => Some(VmKey::ArrowDown),
264        KeyA => Some(VmKey::Letter('A')),
265        KeyB => Some(VmKey::Letter('B')),
266        KeyC => Some(VmKey::Letter('C')),
267        KeyD => Some(VmKey::Letter('D')),
268        KeyE => Some(VmKey::Letter('E')),
269        KeyF => Some(VmKey::Letter('F')),
270        KeyG => Some(VmKey::Letter('G')),
271        KeyH => Some(VmKey::Letter('H')),
272        KeyI => Some(VmKey::Letter('I')),
273        KeyJ => Some(VmKey::Letter('J')),
274        KeyK => Some(VmKey::Letter('K')),
275        KeyL => Some(VmKey::Letter('L')),
276        KeyM => Some(VmKey::Letter('M')),
277        KeyN => Some(VmKey::Letter('N')),
278        KeyO => Some(VmKey::Letter('O')),
279        KeyP => Some(VmKey::Letter('P')),
280        KeyQ => Some(VmKey::Letter('Q')),
281        KeyR => Some(VmKey::Letter('R')),
282        KeyS => Some(VmKey::Letter('S')),
283        KeyT => Some(VmKey::Letter('T')),
284        KeyU => Some(VmKey::Letter('U')),
285        KeyV => Some(VmKey::Letter('V')),
286        KeyW => Some(VmKey::Letter('W')),
287        KeyX => Some(VmKey::Letter('X')),
288        KeyY => Some(VmKey::Letter('Y')),
289        KeyZ => Some(VmKey::Letter('Z')),
290        Digit0 => Some(VmKey::Digit(0)),
291        Digit1 => Some(VmKey::Digit(1)),
292        Digit2 => Some(VmKey::Digit(2)),
293        Digit3 => Some(VmKey::Digit(3)),
294        Digit4 => Some(VmKey::Digit(4)),
295        Digit5 => Some(VmKey::Digit(5)),
296        Digit6 => Some(VmKey::Digit(6)),
297        Digit7 => Some(VmKey::Digit(7)),
298        Digit8 => Some(VmKey::Digit(8)),
299        Digit9 => Some(VmKey::Digit(9)),
300        F1 => Some(VmKey::F(1)),
301        F2 => Some(VmKey::F(2)),
302        F3 => Some(VmKey::F(3)),
303        F4 => Some(VmKey::F(4)),
304        F5 => Some(VmKey::F(5)),
305        F6 => Some(VmKey::F(6)),
306        F7 => Some(VmKey::F(7)),
307        F8 => Some(VmKey::F(8)),
308        F9 => Some(VmKey::F(9)),
309        F10 => Some(VmKey::F(10)),
310        F11 => Some(VmKey::F(11)),
311        F12 => Some(VmKey::F(12)),
312        _ => None,
313    }
314}
315
316#[no_mangle]
317pub unsafe extern "C" fn siglus_pump_create(
318    game_root_utf8: *const c_char,
319) -> *mut SiglusPumpHandle {
320    let game_root = match cstr_required(game_root_utf8, "game_root_utf8") {
321        Ok(s) => s,
322        Err(e) => {
323            log::error!("siglus_pump_create: {e:?}");
324            return std::ptr::null_mut();
325        }
326    };
327    let config = SiglusHostConfig::new(PathBuf::from(game_root));
328    let event_loop = match EventLoop::new() {
329        Ok(el) => el,
330        Err(e) => {
331            log::error!("siglus_pump_create: EventLoop::new: {e:?}");
332            return std::ptr::null_mut();
333        }
334    };
335    let mut handle = Box::new(SiglusPumpHandle { event_loop, app: PumpApp::new(config) });
336    {
337        let event_loop = &mut handle.event_loop;
338        let app = &mut handle.app;
339        let _ = event_loop.pump_events(Some(Duration::from_millis(0)), |event, elwt| {
340            app.handle_event(event, elwt);
341        });
342    }
343    Box::into_raw(handle)
344}
345
346#[no_mangle]
347pub unsafe extern "C" fn siglus_pump_set_native_messagebox_callback(
348    handle: *mut SiglusPumpHandle,
349    callback: Option<SiglusNativeMessageBoxCallback>,
350    user_data: *mut c_void,
351) {
352    if handle.is_null() {
353        return;
354    }
355    let h = &mut *handle;
356    h.app.native_messagebox_callback = callback;
357    h.app.native_messagebox_user_data = user_data;
358    if let Some(host) = h.app.host.as_mut() {
359        host.set_native_messagebox_callback(callback, user_data);
360    }
361}
362
363#[no_mangle]
364pub unsafe extern "C" fn siglus_pump_submit_messagebox_result(
365    handle: *mut SiglusPumpHandle,
366    request_id: u64,
367    value: i64,
368) {
369    if handle.is_null() {
370        return;
371    }
372    let h = &mut *handle;
373    if let Some(host) = h.app.host.as_mut() {
374        host.submit_native_messagebox_result(request_id, value);
375    }
376}
377
378#[no_mangle]
379pub unsafe extern "C" fn siglus_pump_text_input(handle: *mut SiglusPumpHandle, text_utf8: *const c_char) {
380    let Some(handle) = handle.as_mut() else {
381        return;
382    };
383    let Some(host) = handle.app.host.as_mut() else {
384        return;
385    };
386    if let Some(text) = cstr_opt(text_utf8) {
387        host.text_input(&text);
388    }
389}
390
391#[no_mangle]
392pub unsafe extern "C" fn siglus_pump_key_down(handle: *mut SiglusPumpHandle, key_code: i32) {
393    let Some(handle) = handle.as_mut() else {
394        return;
395    };
396    let Some(host) = handle.app.host.as_mut() else {
397        return;
398    };
399    host.key_down_code(key_code);
400}
401
402#[no_mangle]
403pub unsafe extern "C" fn siglus_pump_key_up(handle: *mut SiglusPumpHandle, key_code: i32) {
404    let Some(handle) = handle.as_mut() else {
405        return;
406    };
407    let Some(host) = handle.app.host.as_mut() else {
408        return;
409    };
410    host.key_up_code(key_code);
411}
412
413#[no_mangle]
414pub unsafe extern "C" fn siglus_pump_step(handle: *mut SiglusPumpHandle, timeout_ms: u32) -> i32 {
415    if handle.is_null() {
416        return 2;
417    }
418    let h = &mut *handle;
419    if let Some(w) = h.app.window.as_ref() {
420        w.request_redraw();
421    }
422    let status = {
423        let event_loop = &mut h.event_loop;
424        let app = &mut h.app;
425        event_loop.pump_events(Some(Duration::from_millis(timeout_ms.max(1) as u64)), |event, elwt| {
426            app.handle_event(event, elwt);
427        })
428    };
429    match status {
430        PumpStatus::Continue => 0,
431        _ => 1,
432    }
433}
434
435#[no_mangle]
436pub unsafe extern "C" fn siglus_pump_destroy(handle: *mut SiglusPumpHandle) {
437    if handle.is_null() {
438        return;
439    }
440    drop(Box::from_raw(handle));
441}
442
443#[no_mangle]
444pub unsafe extern "C" fn siglus_run_entry(game_root_utf8: *const c_char) -> i32 {
445    let game_root = match cstr_required(game_root_utf8, "game_root_utf8") {
446        Ok(s) => s,
447        Err(e) => {
448            log::error!("siglus_run_entry: {e:?}");
449            return 1;
450        }
451    };
452    let event_loop = match EventLoop::new() {
453        Ok(el) => el,
454        Err(e) => {
455            log::error!("siglus_run_entry: EventLoop::new: {e:?}");
456            return 1;
457        }
458    };
459    event_loop.set_control_flow(ControlFlow::Poll);
460    let config = SiglusHostConfig::new(PathBuf::from(game_root));
461    let mut app = PumpApp::new(config);
462    match event_loop.run_app(&mut app) {
463        Ok(()) => {
464            if let Some(e) = app.init_error {
465                log::error!("siglus_run_entry: {e}");
466                1
467            } else {
468                0
469            }
470        }
471        Err(e) => {
472            log::error!("siglus_run_entry: run_app: {e:?}");
473            1
474        }
475    }
476}