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