Skip to main content

siglus_scene_vm/
desktop_messagebox.rs

1//! Desktop winit message box support.
2//!
3//! Desktop ports keep modal dialogs inside the winit event loop. Mobile ports use
4//! the native callback path in `host.rs`.
5
6#![cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
7
8use std::collections::VecDeque;
9use std::sync::{Arc, Mutex};
10use std::time::Instant;
11
12use anyhow::{Context, Result};
13use egui_wgpu::{Renderer as EguiRenderer, ScreenDescriptor};
14use winit::dpi::{LogicalSize, PhysicalPosition};
15use winit::event::{ElementState, KeyEvent, MouseButton, WindowEvent};
16use winit::event_loop::ActiveEventLoop;
17use winit::keyboard::{KeyCode, PhysicalKey};
18use winit::window::{Window, WindowAttributes, WindowId};
19
20use crate::render::Renderer;
21use crate::runtime::native_ui::{
22    NativeChihayaBenchDialogRequest, NativeMessageBoxRequest, NativeUiBackend,
23};
24
25fn configure_egui_default_font(ctx: &egui::Context) {
26    let mut fonts = egui::FontDefinitions::default();
27    fonts.font_data.insert(
28        "siglus_default".to_string(),
29        egui::FontData::from_static(crate::text_render::DEFAULT_FONT_BYTES),
30    );
31    fonts
32        .families
33        .entry(egui::FontFamily::Proportional)
34        .or_default()
35        .insert(0, "siglus_default".to_string());
36    fonts
37        .families
38        .entry(egui::FontFamily::Monospace)
39        .or_default()
40        .insert(0, "siglus_default".to_string());
41    ctx.set_fonts(fonts);
42}
43
44#[derive(Clone, Default)]
45pub struct DesktopMessageBoxBridge {
46    queue: Arc<Mutex<VecDeque<NativeMessageBoxRequest>>>,
47    chihaya_queue: Arc<Mutex<VecDeque<NativeChihayaBenchDialogRequest>>>,
48}
49
50impl DesktopMessageBoxBridge {
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    pub fn backend(&self) -> Arc<dyn NativeUiBackend> {
56        Arc::new(DesktopMessageBoxBackend {
57            queue: Arc::clone(&self.queue),
58            chihaya_queue: Arc::clone(&self.chihaya_queue),
59        })
60    }
61
62    pub fn pop_request(&self) -> Option<NativeMessageBoxRequest> {
63        self.queue.lock().ok().and_then(|mut q| q.pop_front())
64    }
65
66    pub fn pop_chihaya_request(&self) -> Option<NativeChihayaBenchDialogRequest> {
67        self.chihaya_queue
68            .lock()
69            .ok()
70            .and_then(|mut q| q.pop_front())
71    }
72}
73
74struct DesktopMessageBoxBackend {
75    queue: Arc<Mutex<VecDeque<NativeMessageBoxRequest>>>,
76    chihaya_queue: Arc<Mutex<VecDeque<NativeChihayaBenchDialogRequest>>>,
77}
78
79impl NativeUiBackend for DesktopMessageBoxBackend {
80    fn show_system_messagebox(&self, request: NativeMessageBoxRequest) {
81        if let Ok(mut q) = self.queue.lock() {
82            q.push_back(request);
83        } else {
84            log::error!("desktop messagebox queue lock failed");
85        }
86    }
87
88    fn show_chihaya_bench_dialog(&self, request: NativeChihayaBenchDialogRequest) {
89        if let Ok(mut q) = self.chihaya_queue.lock() {
90            q.push_back(request);
91        } else {
92            log::error!("desktop Chihaya benchmark dialog queue lock failed");
93        }
94    }
95}
96
97#[derive(Debug, Clone, Copy)]
98struct ButtonRect {
99    x0: f32,
100    y0: f32,
101    x1: f32,
102    y1: f32,
103}
104
105impl ButtonRect {
106    fn contains(self, x: f32, y: f32) -> bool {
107        x >= self.x0 && x <= self.x1 && y >= self.y0 && y <= self.y1
108    }
109}
110
111pub struct DesktopMessageBoxWindow {
112    request: NativeMessageBoxRequest,
113    window: Arc<dyn Window>,
114    window_id: WindowId,
115    renderer: Renderer,
116    egui_renderer: EguiRenderer,
117    egui_ctx: egui::Context,
118    start_time: Instant,
119    selected: usize,
120    cursor_pos: Option<(f32, f32)>,
121}
122
123impl DesktopMessageBoxWindow {
124    pub fn new(elwt: &dyn ActiveEventLoop, request: NativeMessageBoxRequest) -> Result<Self> {
125        let button_count = request.buttons.len().max(1) as f64;
126        let width = (420.0f64).max(220.0 + button_count * 112.0);
127        let height = 190.0f64;
128        let title = if request.title.trim().is_empty() {
129            "Siglus".to_string()
130        } else {
131            request.title.clone()
132        };
133        let window = elwt
134            .create_window(
135                WindowAttributes::default()
136                    .with_title(title)
137                    .with_surface_size(LogicalSize::new(width, height))
138                    .with_min_surface_size(LogicalSize::new(360.0, 160.0))
139                    .with_resizable(false),
140            )
141            .context("create desktop messagebox window")?;
142        let window: Arc<dyn Window> = Arc::from(window);
143        let renderer = pollster::block_on(Renderer::new(window.clone()))
144            .context("messagebox renderer init")?;
145        let egui_renderer = EguiRenderer::new(&renderer.device, renderer.config.format, None, 1);
146        let egui_ctx = egui::Context::default();
147        configure_egui_default_font(&egui_ctx);
148        let selected = request.buttons.len().saturating_sub(1).min(1);
149        window.request_redraw();
150        Ok(Self {
151            request,
152            window_id: window.id(),
153            window,
154            renderer,
155            egui_renderer,
156            egui_ctx,
157            start_time: Instant::now(),
158            selected,
159            cursor_pos: None,
160        })
161    }
162
163    pub fn window_id(&self) -> WindowId {
164        self.window_id
165    }
166
167    pub fn request_id(&self) -> u64 {
168        self.request.request_id
169    }
170
171    pub fn request_redraw(&self) {
172        self.window.request_redraw();
173    }
174
175    pub fn hide(&self) {
176        self.window.set_visible(false);
177    }
178
179    pub fn cancel_value(&self) -> i64 {
180        self.request
181            .buttons
182            .last()
183            .map(|button| button.value)
184            .unwrap_or(0)
185    }
186
187    pub fn handle_window_event(&mut self, event: WindowEvent) -> Option<i64> {
188        match event {
189            WindowEvent::CloseRequested => Some(self.cancel_value()),
190            WindowEvent::SurfaceResized(size) => {
191                self.renderer.resize(size.width.max(1), size.height.max(1));
192                self.window.request_redraw();
193                None
194            }
195            WindowEvent::PointerMoved {
196                position,
197                primary: true,
198                ..
199            }
200            | WindowEvent::PointerEntered {
201                position,
202                primary: true,
203                ..
204            } => {
205                let pos = self.logical_pos(position);
206                self.cursor_pos = Some(pos);
207                if let Some(idx) = self.hit_test_button(pos.0, pos.1) {
208                    self.selected = idx;
209                }
210                self.window.request_redraw();
211                None
212            }
213            WindowEvent::PointerButton {
214                state: ElementState::Released,
215                button,
216                position,
217                primary: true,
218                ..
219            } if button.clone().mouse_button() == Some(MouseButton::Left) => {
220                self.cursor_pos = Some(self.logical_pos(position));
221                let pos = self.cursor_pos?;
222                let idx = self.hit_test_button(pos.0, pos.1)?;
223                self.selected = idx;
224                self.request.buttons.get(idx).map(|button| button.value)
225            }
226            WindowEvent::KeyboardInput {
227                event:
228                    KeyEvent {
229                        state: ElementState::Pressed,
230                        physical_key: PhysicalKey::Code(code),
231                        ..
232                    },
233                ..
234            } => self.handle_key(code),
235            WindowEvent::RedrawRequested => {
236                if let Err(err) = self.render() {
237                    log::error!("desktop messagebox render failed: {err:#}");
238                }
239                None
240            }
241            _ => None,
242        }
243    }
244
245    fn handle_key(&mut self, code: KeyCode) -> Option<i64> {
246        match code {
247            KeyCode::Escape => Some(self.cancel_value()),
248            KeyCode::Enter | KeyCode::Space => self
249                .request
250                .buttons
251                .get(
252                    self.selected
253                        .min(self.request.buttons.len().saturating_sub(1)),
254                )
255                .map(|button| button.value),
256            KeyCode::ArrowLeft | KeyCode::ArrowUp => {
257                let len = self.request.buttons.len();
258                if len > 0 {
259                    self.selected = if self.selected == 0 {
260                        len - 1
261                    } else {
262                        self.selected - 1
263                    };
264                    self.window.request_redraw();
265                }
266                None
267            }
268            KeyCode::ArrowRight | KeyCode::ArrowDown | KeyCode::Tab => {
269                let len = self.request.buttons.len();
270                if len > 0 {
271                    self.selected = (self.selected + 1) % len;
272                    self.window.request_redraw();
273                }
274                None
275            }
276            _ => None,
277        }
278    }
279
280    fn logical_pos(&self, position: PhysicalPosition<f64>) -> (f32, f32) {
281        let p = position.to_logical::<f64>(self.window.scale_factor());
282        (p.x as f32, p.y as f32)
283    }
284
285    fn button_rects_for_size(&self, logical_w: f32, logical_h: f32) -> Vec<ButtonRect> {
286        let count = self.request.buttons.len().max(1);
287        let button_w = 96.0f32;
288        let button_h = 32.0f32;
289        let gap = 12.0f32;
290        let total_w = count as f32 * button_w + count.saturating_sub(1) as f32 * gap;
291        let mut x = ((logical_w - total_w) * 0.5).max(16.0);
292        let y = (logical_h - 52.0).max(104.0);
293        let mut rects = Vec::with_capacity(count);
294        for _ in 0..count {
295            rects.push(ButtonRect {
296                x0: x,
297                y0: y,
298                x1: x + button_w,
299                y1: y + button_h,
300            });
301            x += button_w + gap;
302        }
303        rects
304    }
305
306    fn hit_test_button(&self, x: f32, y: f32) -> Option<usize> {
307        let size = self.window.surface_size();
308        let scale = self.window.scale_factor() as f32;
309        let logical_w = size.width as f32 / scale.max(1.0);
310        let logical_h = size.height as f32 / scale.max(1.0);
311        self.button_rects_for_size(logical_w, logical_h)
312            .into_iter()
313            .position(|rect| rect.contains(x, y))
314    }
315
316    fn render(&mut self) -> Result<()> {
317        let size = self.window.surface_size();
318        if size.width == 0 || size.height == 0 {
319            return Ok(());
320        }
321        let scale = self.window.scale_factor() as f32;
322        self.egui_ctx.set_pixels_per_point(scale);
323        let logical_w = size.width as f32 / scale.max(1.0);
324        let logical_h = size.height as f32 / scale.max(1.0);
325        let button_rects = self.button_rects_for_size(logical_w, logical_h);
326        let message = self.request.message.clone();
327        let title = self.request.title.clone();
328        let buttons = self.request.buttons.clone();
329        let selected = self.selected;
330        let raw_input = egui::RawInput {
331            screen_rect: Some(egui::Rect::from_min_size(
332                egui::Pos2::ZERO,
333                egui::vec2(logical_w, logical_h),
334            )),
335            time: Some(self.start_time.elapsed().as_secs_f64()),
336            ..Default::default()
337        };
338        let output = self.egui_ctx.run(raw_input, |ctx| {
339            egui::CentralPanel::default()
340                .frame(
341                    egui::Frame::default()
342                        .fill(egui::Color32::from_rgb(246, 247, 250))
343                        .inner_margin(egui::Margin::same(18.0)),
344                )
345                .show(ctx, |ui| {
346                    let full = ui.max_rect();
347                    let painter = ui.painter();
348                    let icon_rect = egui::Rect::from_min_size(
349                        egui::pos2(full.left() + 4.0, full.top() + 12.0),
350                        egui::vec2(36.0, 36.0),
351                    );
352                    painter.circle_filled(
353                        icon_rect.center(),
354                        18.0,
355                        egui::Color32::from_rgb(66, 133, 244),
356                    );
357                    painter.text(
358                        icon_rect.center(),
359                        egui::Align2::CENTER_CENTER,
360                        "?",
361                        egui::FontId::proportional(24.0),
362                        egui::Color32::WHITE,
363                    );
364
365                    let text_x = icon_rect.right() + 16.0;
366                    let title_text = if title.trim().is_empty() {
367                        "Siglus"
368                    } else {
369                        title.as_str()
370                    };
371                    painter.text(
372                        egui::pos2(text_x, full.top() + 8.0),
373                        egui::Align2::LEFT_TOP,
374                        title_text,
375                        egui::FontId::proportional(16.0),
376                        egui::Color32::from_rgb(35, 38, 42),
377                    );
378                    painter.text(
379                        egui::pos2(text_x, full.top() + 42.0),
380                        egui::Align2::LEFT_TOP,
381                        message,
382                        egui::FontId::proportional(18.0),
383                        egui::Color32::from_rgb(20, 22, 25),
384                    );
385
386                    for (idx, rect) in button_rects.iter().enumerate() {
387                        let r = egui::Rect::from_min_max(
388                            egui::pos2(rect.x0, rect.y0),
389                            egui::pos2(rect.x1, rect.y1),
390                        );
391                        let is_selected = idx == selected;
392                        let fill = if is_selected {
393                            egui::Color32::from_rgb(43, 107, 235)
394                        } else {
395                            egui::Color32::from_rgb(255, 255, 255)
396                        };
397                        let stroke = if is_selected {
398                            egui::Stroke::new(1.5_f32, egui::Color32::from_rgb(28, 86, 210))
399                        } else {
400                            egui::Stroke::new(1.0_f32, egui::Color32::from_rgb(166, 174, 186))
401                        };
402                        painter.rect_filled(r, egui::Rounding::same(4.0), fill);
403                        painter.line_segment([r.left_top(), r.right_top()], stroke);
404                        painter.line_segment([r.right_top(), r.right_bottom()], stroke);
405                        painter.line_segment([r.right_bottom(), r.left_bottom()], stroke);
406                        painter.line_segment([r.left_bottom(), r.left_top()], stroke);
407                        let label = buttons
408                            .get(idx)
409                            .map(|button| button.label.as_str())
410                            .unwrap_or("OK");
411                        let text_color = if is_selected {
412                            egui::Color32::WHITE
413                        } else {
414                            egui::Color32::from_rgb(25, 27, 30)
415                        };
416                        painter.text(
417                            r.center(),
418                            egui::Align2::CENTER_CENTER,
419                            label,
420                            egui::FontId::proportional(15.0),
421                            text_color,
422                        );
423                    }
424                });
425        });
426
427        let screen_desc = ScreenDescriptor {
428            size_in_pixels: [size.width, size.height],
429            pixels_per_point: scale,
430        };
431        let paint_jobs = self.egui_ctx.tessellate(output.shapes, scale);
432        for (id, delta) in &output.textures_delta.set {
433            self.egui_renderer.update_texture(
434                &self.renderer.device,
435                &self.renderer.queue,
436                *id,
437                delta,
438            );
439        }
440
441        let frame = match self.renderer.surface.get_current_texture() {
442            Ok(frame) => frame,
443            Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
444                self.renderer
445                    .resize(self.renderer.config.width, self.renderer.config.height);
446                return Ok(());
447            }
448            Err(wgpu::SurfaceError::OutOfMemory) => {
449                anyhow::bail!("messagebox surface out of memory")
450            }
451            Err(wgpu::SurfaceError::Timeout) => return Ok(()),
452        };
453        let view = frame
454            .texture
455            .create_view(&wgpu::TextureViewDescriptor::default());
456        let mut encoder =
457            self.renderer
458                .device
459                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
460                    label: Some("siglus_messagebox_egui_encoder"),
461                });
462        self.egui_renderer.update_buffers(
463            &self.renderer.device,
464            &self.renderer.queue,
465            &mut encoder,
466            &paint_jobs,
467            &screen_desc,
468        );
469        {
470            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
471                label: Some("siglus_messagebox_egui_pass"),
472                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
473                    view: &view,
474                    resolve_target: None,
475                    ops: wgpu::Operations {
476                        load: wgpu::LoadOp::Clear(wgpu::Color {
477                            r: 0.965,
478                            g: 0.970,
479                            b: 0.980,
480                            a: 1.0,
481                        }),
482                        store: wgpu::StoreOp::Store,
483                    },
484                })],
485                depth_stencil_attachment: None,
486                timestamp_writes: None,
487                occlusion_query_set: None,
488            });
489            self.egui_renderer
490                .render(&mut pass, &paint_jobs, &screen_desc);
491        }
492        self.renderer.queue.submit(Some(encoder.finish()));
493        frame.present();
494        for id in output.textures_delta.free {
495            self.egui_renderer.free_texture(&id);
496        }
497        Ok(())
498    }
499}