1#![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 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 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 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::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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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
629pub 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}