1#![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, 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}