Skip to main content

siglus_scene_vm/
desktop_chihaya_bench.rs

1//! Desktop implementation of the original Chihaya benchmark information dialog.
2//!
3//! `eng_chihaya.cpp::C_info_dialog` is a modal, resizable window with a read-only
4//! multi-line text area, Copy-to-Clipboard, and Close.  Keep it separate from the
5//! generic Siglus message box because the benchmark depends on the modal call
6//! boundary and exposes the complete result text for copying.
7
8#![cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
9
10use std::sync::Arc;
11use std::time::Instant;
12
13use anyhow::{Context, Result};
14use egui_wgpu::{Renderer as EguiRenderer, ScreenDescriptor};
15use winit::dpi::{LogicalSize, PhysicalPosition};
16use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent};
17use winit::event_loop::ActiveEventLoop;
18use winit::keyboard::{KeyCode, PhysicalKey};
19use winit::window::{Window, WindowAttributes, WindowId};
20
21use crate::desktop_twitter::write_system_clipboard;
22use crate::render::Renderer;
23use crate::runtime::native_ui::NativeChihayaBenchDialogRequest;
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(Debug, Clone, Copy)]
45struct ButtonRect {
46    x0: f32,
47    y0: f32,
48    x1: f32,
49    y1: f32,
50}
51
52impl ButtonRect {
53    fn contains(self, x: f32, y: f32) -> bool {
54        x >= self.x0 && x <= self.x1 && y >= self.y0 && y <= self.y1
55    }
56
57    fn egui_rect(self) -> egui::Rect {
58        egui::Rect::from_min_max(egui::pos2(self.x0, self.y0), egui::pos2(self.x1, self.y1))
59    }
60}
61
62pub struct DesktopChihayaBenchWindow {
63    request: NativeChihayaBenchDialogRequest,
64    window: Arc<dyn Window>,
65    window_id: WindowId,
66    renderer: Renderer,
67    egui_renderer: EguiRenderer,
68    egui_ctx: egui::Context,
69    start_time: Instant,
70    cursor_pos: Option<(f32, f32)>,
71    selected: usize,
72    scroll_line: usize,
73    notice: Option<String>,
74}
75
76impl DesktopChihayaBenchWindow {
77    pub fn new(
78        elwt: &dyn ActiveEventLoop,
79        request: NativeChihayaBenchDialogRequest,
80    ) -> Result<Self> {
81        let title = if request.title.trim().is_empty() {
82            "Siglus".to_string()
83        } else {
84            request.title.clone()
85        };
86        let window = elwt
87            .create_window(
88                WindowAttributes::default()
89                    .with_title(title)
90                    .with_surface_size(LogicalSize::new(680.0, 460.0))
91                    .with_min_surface_size(LogicalSize::new(440.0, 300.0))
92                    .with_resizable(true),
93            )
94            .context("create Chihaya benchmark information window")?;
95        let window: Arc<dyn Window> = Arc::from(window);
96        let renderer = pollster::block_on(Renderer::new(window.clone()))
97            .context("Chihaya benchmark dialog renderer init")?;
98        let egui_renderer = EguiRenderer::new(&renderer.device, renderer.config.format, None, 1);
99        let egui_ctx = egui::Context::default();
100        configure_egui_default_font(&egui_ctx);
101        window.request_redraw();
102        Ok(Self {
103            request,
104            window_id: window.id(),
105            window,
106            renderer,
107            egui_renderer,
108            egui_ctx,
109            start_time: Instant::now(),
110            cursor_pos: None,
111            selected: 1, // original dialog gives Close the default-button role.
112            scroll_line: 0,
113            notice: None,
114        })
115    }
116
117    pub fn window_id(&self) -> WindowId {
118        self.window_id
119    }
120
121    pub fn request_id(&self) -> u64 {
122        self.request.request_id
123    }
124
125    pub fn hide(&self) {
126        self.window.set_visible(false);
127    }
128
129    pub fn request_redraw(&self) {
130        self.window.request_redraw();
131    }
132
133    pub fn handle_window_event(&mut self, event: WindowEvent) -> Option<i64> {
134        match event {
135            WindowEvent::CloseRequested => Some(0),
136            WindowEvent::SurfaceResized(size) => {
137                self.renderer.resize(size.width.max(1), size.height.max(1));
138                self.clamp_scroll();
139                self.window.request_redraw();
140                None
141            }
142            WindowEvent::PointerMoved {
143                position,
144                primary: true,
145                ..
146            }
147            | WindowEvent::PointerEntered {
148                position,
149                primary: true,
150                ..
151            } => {
152                let pos = self.logical_pos(position);
153                self.cursor_pos = Some(pos);
154                if self.notice.is_none()
155                    && let Some(index) = self.hit_test_button(pos.0, pos.1)
156                {
157                    self.selected = index;
158                }
159                self.window.request_redraw();
160                None
161            }
162            WindowEvent::MouseWheel { delta, .. } => {
163                if self.notice.is_none() {
164                    let lines = match delta {
165                        MouseScrollDelta::LineDelta(_, y) => (-y * 3.0).round() as i32,
166                        MouseScrollDelta::PixelDelta(p) => (-p.y / 22.0).round() as i32,
167                        _ => 0,
168                    };
169                    self.adjust_scroll(lines);
170                }
171                None
172            }
173            WindowEvent::PointerButton {
174                state: ElementState::Released,
175                button,
176                position,
177                primary: true,
178                ..
179            } if button.clone().mouse_button() == Some(MouseButton::Left) => {
180                self.cursor_pos = Some(self.logical_pos(position));
181                if self.notice.is_some() {
182                    self.notice = None;
183                    self.window.request_redraw();
184                    return None;
185                }
186                let (x, y) = self.cursor_pos?;
187                match self.hit_test_button(x, y) {
188                    Some(0) => {
189                        self.copy_to_clipboard();
190                        None
191                    }
192                    Some(1) => Some(0),
193                    _ => None,
194                }
195            }
196            WindowEvent::KeyboardInput {
197                event:
198                    KeyEvent {
199                        state: ElementState::Pressed,
200                        physical_key: PhysicalKey::Code(code),
201                        ..
202                    },
203                ..
204            } => self.handle_key(code),
205            WindowEvent::RedrawRequested => {
206                if let Err(err) = self.render() {
207                    log::error!("Chihaya benchmark dialog render failed: {err:#}");
208                }
209                None
210            }
211            _ => None,
212        }
213    }
214
215    fn handle_key(&mut self, code: KeyCode) -> Option<i64> {
216        if self.notice.is_some() {
217            if matches!(code, KeyCode::Escape | KeyCode::Enter | KeyCode::Space) {
218                self.notice = None;
219                self.window.request_redraw();
220            }
221            return None;
222        }
223        match code {
224            KeyCode::Escape => Some(0),
225            KeyCode::Enter | KeyCode::Space => {
226                if self.selected == 0 {
227                    self.copy_to_clipboard();
228                    None
229                } else {
230                    Some(0)
231                }
232            }
233            KeyCode::ArrowLeft | KeyCode::ArrowRight | KeyCode::Tab => {
234                self.selected ^= 1;
235                self.window.request_redraw();
236                None
237            }
238            KeyCode::ArrowUp => {
239                self.adjust_scroll(-1);
240                None
241            }
242            KeyCode::ArrowDown => {
243                self.adjust_scroll(1);
244                None
245            }
246            KeyCode::PageUp => {
247                self.adjust_scroll(-(self.visible_line_count() as i32));
248                None
249            }
250            KeyCode::PageDown => {
251                self.adjust_scroll(self.visible_line_count() as i32);
252                None
253            }
254            KeyCode::Home => {
255                self.scroll_line = 0;
256                self.window.request_redraw();
257                None
258            }
259            KeyCode::End => {
260                self.scroll_line = self.request.text.lines().count();
261                self.clamp_scroll();
262                self.window.request_redraw();
263                None
264            }
265            _ => None,
266        }
267    }
268
269    fn copy_to_clipboard(&mut self) {
270        self.notice = Some(match write_system_clipboard(&self.request.text) {
271            Ok(()) => "情報をクリップボードにコピーしました。\nメモ帳などのエディタで「貼り付け」または「ペースト」で情報を貼り付けることができます。".to_string(),
272            Err(err) => {
273                log::error!("Chihaya benchmark clipboard copy failed: {err:#}");
274                format!("クリップボードへのコピーに失敗しました。\n{err:#}")
275            }
276        });
277        self.window.request_redraw();
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 logical_size(&self) -> (f32, f32) {
286        let size = self.window.surface_size();
287        let scale = self.window.scale_factor() as f32;
288        (
289            size.width as f32 / scale.max(1.0),
290            size.height as f32 / scale.max(1.0),
291        )
292    }
293
294    fn button_rects(&self) -> [ButtonRect; 2] {
295        let (w, h) = self.logical_size();
296        let y0 = (h - 52.0).max(220.0);
297        [
298            ButtonRect {
299                x0: 18.0,
300                y0,
301                x1: 226.0,
302                y1: y0 + 34.0,
303            },
304            ButtonRect {
305                x0: (w - 116.0).max(250.0),
306                y0,
307                x1: (w - 18.0).max(348.0),
308                y1: y0 + 34.0,
309            },
310        ]
311    }
312
313    fn hit_test_button(&self, x: f32, y: f32) -> Option<usize> {
314        self.button_rects()
315            .into_iter()
316            .position(|rect| rect.contains(x, y))
317    }
318
319    fn visible_line_count(&self) -> usize {
320        let (_, h) = self.logical_size();
321        (((h - 112.0).max(80.0) / 20.0).floor() as usize).max(1)
322    }
323
324    fn max_scroll(&self) -> usize {
325        self.request
326            .text
327            .lines()
328            .count()
329            .saturating_sub(self.visible_line_count())
330    }
331
332    fn clamp_scroll(&mut self) {
333        self.scroll_line = self.scroll_line.min(self.max_scroll());
334    }
335
336    fn adjust_scroll(&mut self, delta: i32) {
337        if delta < 0 {
338            self.scroll_line = self
339                .scroll_line
340                .saturating_sub(delta.unsigned_abs() as usize);
341        } else {
342            self.scroll_line = self.scroll_line.saturating_add(delta as usize);
343        }
344        self.clamp_scroll();
345        self.window.request_redraw();
346    }
347
348    fn render(&mut self) -> Result<()> {
349        let size = self.window.surface_size();
350        if size.width == 0 || size.height == 0 {
351            return Ok(());
352        }
353        self.clamp_scroll();
354        let scale = self.window.scale_factor() as f32;
355        self.egui_ctx.set_pixels_per_point(scale);
356        let logical_w = size.width as f32 / scale.max(1.0);
357        let logical_h = size.height as f32 / scale.max(1.0);
358        let rects = self.button_rects();
359        let selected = self.selected;
360        let visible_lines = self.visible_line_count();
361        let start = self.scroll_line;
362        let body = self
363            .request
364            .text
365            .lines()
366            .skip(start)
367            .take(visible_lines)
368            .collect::<Vec<_>>()
369            .join("\n");
370        let line_count = self.request.text.lines().count();
371        let notice = self.notice.clone();
372
373        let raw_input = egui::RawInput {
374            screen_rect: Some(egui::Rect::from_min_size(
375                egui::Pos2::ZERO,
376                egui::vec2(logical_w, logical_h),
377            )),
378            time: Some(self.start_time.elapsed().as_secs_f64()),
379            ..Default::default()
380        };
381        let output = self.egui_ctx.run(raw_input, |ctx| {
382            egui::CentralPanel::default()
383                .frame(
384                    egui::Frame::default()
385                        .fill(egui::Color32::from_rgb(246, 247, 250))
386                        .inner_margin(egui::Margin::same(12.0)),
387                )
388                .show(ctx, |ui| {
389                    let full = ui.max_rect();
390                    let body_rect = egui::Rect::from_min_max(
391                        egui::pos2(full.left() + 6.0, full.top() + 6.0),
392                        egui::pos2(full.right() - 6.0, rects[0].y0 - 12.0),
393                    );
394                    let painter = ui.painter();
395                    painter.rect_filled(body_rect, egui::Rounding::same(2.0), egui::Color32::WHITE);
396                    let border = egui::Stroke::new(1.0_f32, egui::Color32::from_rgb(150, 154, 160));
397                    painter.line_segment([body_rect.left_top(), body_rect.right_top()], border);
398                    painter.line_segment([body_rect.right_top(), body_rect.right_bottom()], border);
399                    painter
400                        .line_segment([body_rect.right_bottom(), body_rect.left_bottom()], border);
401                    painter.line_segment([body_rect.left_bottom(), body_rect.left_top()], border);
402                    let body_painter = painter.with_clip_rect(body_rect.shrink(2.0));
403                    body_painter.text(
404                        body_rect.left_top() + egui::vec2(8.0, 8.0),
405                        egui::Align2::LEFT_TOP,
406                        body,
407                        egui::FontId::monospace(15.0),
408                        egui::Color32::from_rgb(20, 22, 25),
409                    );
410                    if line_count > visible_lines {
411                        painter.text(
412                            egui::pos2(body_rect.right() - 8.0, body_rect.bottom() - 6.0),
413                            egui::Align2::RIGHT_BOTTOM,
414                            format!("{} / {}", start + 1, line_count),
415                            egui::FontId::proportional(11.0),
416                            egui::Color32::from_rgb(100, 104, 110),
417                        );
418                    }
419
420                    for (idx, rect) in rects.iter().enumerate() {
421                        let r = rect.egui_rect();
422                        let active = idx == selected;
423                        let fill = if active {
424                            egui::Color32::from_rgb(43, 107, 235)
425                        } else {
426                            egui::Color32::WHITE
427                        };
428                        let stroke = if active {
429                            egui::Stroke::new(1.5_f32, egui::Color32::from_rgb(28, 86, 210))
430                        } else {
431                            egui::Stroke::new(1.0_f32, egui::Color32::from_rgb(150, 154, 160))
432                        };
433                        painter.rect_filled(r, egui::Rounding::same(3.0), fill);
434                        painter.line_segment([r.left_top(), r.right_top()], stroke);
435                        painter.line_segment([r.right_top(), r.right_bottom()], stroke);
436                        painter.line_segment([r.right_bottom(), r.left_bottom()], stroke);
437                        painter.line_segment([r.left_bottom(), r.left_top()], stroke);
438                        painter.text(
439                            r.center(),
440                            egui::Align2::CENTER_CENTER,
441                            if idx == 0 {
442                                "クリップボードにコピー"
443                            } else {
444                                "閉じる"
445                            },
446                            egui::FontId::proportional(14.0),
447                            if active {
448                                egui::Color32::WHITE
449                            } else {
450                                egui::Color32::BLACK
451                            },
452                        );
453                    }
454
455                    if let Some(notice) = notice.as_deref() {
456                        let popup = egui::Rect::from_center_size(
457                            full.center(),
458                            egui::vec2((logical_w - 80.0).clamp(300.0, 480.0), 120.0),
459                        );
460                        painter.rect_filled(popup, egui::Rounding::same(5.0), egui::Color32::WHITE);
461                        let stroke =
462                            egui::Stroke::new(1.0_f32, egui::Color32::from_rgb(120, 124, 132));
463                        painter.line_segment([popup.left_top(), popup.right_top()], stroke);
464                        painter.line_segment([popup.right_top(), popup.right_bottom()], stroke);
465                        painter.line_segment([popup.right_bottom(), popup.left_bottom()], stroke);
466                        painter.line_segment([popup.left_bottom(), popup.left_top()], stroke);
467                        painter.text(
468                            egui::pos2(popup.center().x, popup.top() + 34.0),
469                            egui::Align2::CENTER_TOP,
470                            notice,
471                            egui::FontId::proportional(14.0),
472                            egui::Color32::from_rgb(25, 27, 30),
473                        );
474                        painter.text(
475                            egui::pos2(popup.center().x, popup.bottom() - 18.0),
476                            egui::Align2::CENTER_BOTTOM,
477                            "OK",
478                            egui::FontId::proportional(13.0),
479                            egui::Color32::from_rgb(43, 107, 235),
480                        );
481                    }
482                });
483        });
484
485        let screen_desc = ScreenDescriptor {
486            size_in_pixels: [size.width, size.height],
487            pixels_per_point: scale,
488        };
489        let paint_jobs = self.egui_ctx.tessellate(output.shapes, scale);
490        for (id, delta) in &output.textures_delta.set {
491            self.egui_renderer.update_texture(
492                &self.renderer.device,
493                &self.renderer.queue,
494                *id,
495                delta,
496            );
497        }
498
499        let frame = match self.renderer.surface.get_current_texture() {
500            Ok(frame) => frame,
501            Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
502                self.renderer
503                    .resize(self.renderer.config.width, self.renderer.config.height);
504                return Ok(());
505            }
506            Err(wgpu::SurfaceError::OutOfMemory) => {
507                anyhow::bail!("Chihaya benchmark dialog surface out of memory")
508            }
509            Err(wgpu::SurfaceError::Timeout) => return Ok(()),
510        };
511        let view = frame
512            .texture
513            .create_view(&wgpu::TextureViewDescriptor::default());
514        let mut encoder =
515            self.renderer
516                .device
517                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
518                    label: Some("siglus_chihaya_bench_dialog_encoder"),
519                });
520        self.egui_renderer.update_buffers(
521            &self.renderer.device,
522            &self.renderer.queue,
523            &mut encoder,
524            &paint_jobs,
525            &screen_desc,
526        );
527        {
528            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
529                label: Some("siglus_chihaya_bench_dialog_pass"),
530                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
531                    view: &view,
532                    resolve_target: None,
533                    ops: wgpu::Operations {
534                        load: wgpu::LoadOp::Clear(wgpu::Color {
535                            r: 0.965,
536                            g: 0.970,
537                            b: 0.980,
538                            a: 1.0,
539                        }),
540                        store: wgpu::StoreOp::Store,
541                    },
542                })],
543                depth_stencil_attachment: None,
544                timestamp_writes: None,
545                occlusion_query_set: None,
546            });
547            self.egui_renderer
548                .render(&mut pass, &paint_jobs, &screen_desc);
549        }
550        self.renderer.queue.submit(Some(encoder.finish()));
551        frame.present();
552        for id in output.textures_delta.free {
553            self.egui_renderer.free_texture(&id);
554        }
555        Ok(())
556    }
557}