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::{
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}