1use crate::formats::gameexe::GameexeConfig;
5use crate::render::Renderer;
6use crate::runtime::CommandContext;
7use crate::runtime::forms::syscom;
8use crate::runtime::globals::OriginalConfigRuntimeState;
9use anyhow::{Context, Result};
10use egui_wgpu::{Renderer as EguiRenderer, ScreenDescriptor};
11use std::sync::Arc;
12use std::time::Instant;
13use winit::dpi::{LogicalPosition, LogicalSize};
14use winit::event::{ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent};
15use winit::event_loop::ActiveEventLoop;
16use winit::keyboard::{KeyCode, ModifiersState, PhysicalKey};
17use winit::window::{Window, WindowAttributes, WindowId};
18
19fn configure_egui_default_font(ctx: &egui::Context) {
20 let mut fonts = egui::FontDefinitions::default();
21 fonts.font_data.insert(
22 "siglus_default".to_string(),
23 egui::FontData::from_static(crate::text_render::DEFAULT_FONT_BYTES),
24 );
25 fonts
26 .families
27 .entry(egui::FontFamily::Proportional)
28 .or_default()
29 .insert(0, "siglus_default".to_string());
30 fonts
31 .families
32 .entry(egui::FontFamily::Monospace)
33 .or_default()
34 .insert(0, "siglus_default".to_string());
35 ctx.set_fonts(fonts);
36}
37
38pub enum DesktopConfigAction {
39 Changed,
40 Close,
41}
42
43pub struct DesktopConfigWindow {
44 window: Arc<dyn Window>,
45 window_id: WindowId,
46 renderer: Renderer,
47 egui_renderer: EguiRenderer,
48 egui_ctx: egui::Context,
49 start_time: Instant,
50 input_events: Vec<egui::Event>,
51 modifiers: egui::Modifiers,
52 pointer_pos: egui::Pos2,
53 pub dialog: ConfigDialog,
54}
55
56impl DesktopConfigWindow {
57 pub fn new(elwt: &dyn ActiveEventLoop, dialog: ConfigDialog) -> Result<Self> {
58 let window = elwt
59 .create_window(
60 WindowAttributes::default()
61 .with_title("環境設定")
62 .with_surface_size(LogicalSize::new(760.0, 570.0))
63 .with_min_surface_size(LogicalSize::new(640.0, 440.0)),
64 )
65 .context("create configuration window")?;
66 let window: Arc<dyn Window> = Arc::from(window);
67 crate::ime::enable_ime(
68 window.as_ref(),
69 LogicalPosition::new(0, 0).into(),
70 LogicalSize::new(0, 0).into(),
71 );
72 let renderer =
73 pollster::block_on(Renderer::new(window.clone())).context("config renderer init")?;
74 let egui_renderer = EguiRenderer::new(&renderer.device, renderer.config.format, None, 1);
75 let egui_ctx = egui::Context::default();
76 configure_egui_default_font(&egui_ctx);
77 egui_ctx.set_visuals(egui::Visuals::light());
78 window.request_redraw();
79 Ok(Self {
80 window_id: window.id(),
81 window,
82 renderer,
83 egui_renderer,
84 egui_ctx,
85 start_time: Instant::now(),
86 input_events: Vec::new(),
87 modifiers: egui::Modifiers::default(),
88 pointer_pos: egui::Pos2::ZERO,
89 dialog,
90 })
91 }
92 pub fn window_id(&self) -> WindowId {
93 self.window_id
94 }
95 pub fn into_dialog(self) -> ConfigDialog {
96 self.dialog
99 }
100 pub fn handle_window_event(&mut self, event: WindowEvent) -> Option<DesktopConfigAction> {
101 match event {
102 WindowEvent::CloseRequested => return Some(DesktopConfigAction::Close),
103 WindowEvent::SurfaceResized(size) => {
104 self.renderer.resize(size.width.max(1), size.height.max(1));
105 self.window.request_redraw();
106 }
107 WindowEvent::ModifiersChanged(modifiers) => {
108 self.modifiers = map_modifiers(modifiers.state());
109 }
110 WindowEvent::PointerMoved {
111 position,
112 primary: true,
113 ..
114 }
115 | WindowEvent::PointerEntered {
116 position,
117 primary: true,
118 ..
119 } => {
120 let logical = position.to_logical::<f64>(self.window.scale_factor());
121 self.pointer_pos = egui::pos2(logical.x as f32, logical.y as f32);
122 self.input_events
123 .push(egui::Event::PointerMoved(self.pointer_pos));
124 self.window.request_redraw();
125 }
126 WindowEvent::PointerLeft { primary: true, .. } => {
127 self.input_events.push(egui::Event::PointerGone);
128 self.window.request_redraw();
129 }
130 WindowEvent::PointerButton {
131 state,
132 button,
133 position,
134 primary: true,
135 ..
136 } => {
137 let button = button.mouse_button()?;
138 let logical = position.to_logical::<f64>(self.window.scale_factor());
139 self.pointer_pos = egui::pos2(logical.x as f32, logical.y as f32);
140 if let Some(button) = map_pointer_button(button) {
141 self.input_events.push(egui::Event::PointerButton {
142 pos: self.pointer_pos,
143 button,
144 pressed: state == ElementState::Pressed,
145 modifiers: self.modifiers,
146 });
147 self.window.request_redraw();
148 }
149 }
150 WindowEvent::MouseWheel { delta, .. } => {
151 let delta = match delta {
152 MouseScrollDelta::LineDelta(x, y) => egui::vec2(x * 24.0, y * 24.0),
153 MouseScrollDelta::PixelDelta(pos) => egui::vec2(pos.x as f32, pos.y as f32),
154 _ => return None,
155 };
156 self.input_events.push(egui::Event::MouseWheel {
157 unit: egui::MouseWheelUnit::Point,
158 delta,
159 modifiers: self.modifiers,
160 });
161 self.window.request_redraw();
162 }
163 WindowEvent::Ime(ime) => {
164 let event = match ime {
165 Ime::Enabled => egui::ImeEvent::Enabled,
166 Ime::Preedit(text, _) => egui::ImeEvent::Preedit(text),
167 Ime::Commit(text) => egui::ImeEvent::Commit(text),
168 Ime::Disabled => egui::ImeEvent::Disabled,
169 _ => return None,
170 };
171 self.input_events.push(egui::Event::Ime(event));
172 self.window.request_redraw();
173 }
174 #[cfg(target_os = "windows")]
175 WindowEvent::KeyboardInput {
176 event:
177 KeyEvent {
178 logical_key: winit::keyboard::Key::Named(winit::keyboard::NamedKey::Process),
179 ..
180 },
181 ..
182 } => return None,
183 WindowEvent::KeyboardInput { event, .. } => {
184 if let PhysicalKey::Code(code) = event.physical_key
185 && let Some(key) = map_key(code)
186 {
187 self.input_events.push(egui::Event::Key {
188 key,
189 physical_key: Some(key),
190 pressed: event.state == ElementState::Pressed,
191 repeat: event.repeat,
192 modifiers: self.modifiers,
193 });
194 }
195 if let KeyEvent {
196 state: ElementState::Pressed,
197 text: Some(text),
198 ..
199 } = event
200 && !self.modifiers.command
201 {
202 let text = text.to_string();
203 if !text.is_empty() && !text.chars().all(char::is_control) {
204 self.input_events.push(egui::Event::Text(text));
205 }
206 }
207 self.window.request_redraw();
208 }
209 WindowEvent::RedrawRequested => match self.render() {
210 Ok(action) => return action,
211 Err(err) => log::error!("desktop config render failed: {err:#}"),
212 },
213 _ => {}
214 }
215 None
216 }
217
218 fn render(&mut self) -> Result<Option<DesktopConfigAction>> {
219 let size = self.window.surface_size();
220 if size.width == 0 || size.height == 0 {
221 return Ok(None);
222 }
223 let scale = self.window.scale_factor() as f32;
224 self.egui_ctx.set_pixels_per_point(scale);
225 let logical_w = size.width as f32 / scale.max(1.0);
226 let logical_h = size.height as f32 / scale.max(1.0);
227 let events = std::mem::take(&mut self.input_events);
228 let modifiers = self.modifiers;
229 let raw_input = egui::RawInput {
230 screen_rect: Some(egui::Rect::from_min_size(
231 egui::Pos2::ZERO,
232 egui::vec2(logical_w, logical_h),
233 )),
234 time: Some(self.start_time.elapsed().as_secs_f64()),
235 modifiers,
236 events,
237 ..Default::default()
238 };
239
240 let before = self.dialog.state.clone();
241 let mut close = false;
242 let egui_ctx = self.egui_ctx.clone();
243 let output = egui_ctx.run(raw_input, |ctx| {
244 close = self.dialog.show(ctx);
245 });
246 let action = if close {
247 Some(DesktopConfigAction::Close)
248 } else if self.dialog.state != before {
249 Some(DesktopConfigAction::Changed)
250 } else {
251 None
252 };
253 let screen_desc = ScreenDescriptor {
254 size_in_pixels: [size.width, size.height],
255 pixels_per_point: scale,
256 };
257 let paint_jobs = self.egui_ctx.tessellate(output.shapes, scale);
258 for (id, delta) in &output.textures_delta.set {
259 self.egui_renderer.update_texture(
260 &self.renderer.device,
261 &self.renderer.queue,
262 *id,
263 delta,
264 );
265 }
266 let frame = match self.renderer.surface.get_current_texture() {
267 Ok(frame) => frame,
268 Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
269 self.renderer
270 .resize(self.renderer.config.width, self.renderer.config.height);
271 return Ok(action);
272 }
273 Err(wgpu::SurfaceError::OutOfMemory) => anyhow::bail!("Config surface out of memory"),
274 Err(wgpu::SurfaceError::Timeout) => return Ok(action),
275 };
276 let view = frame
277 .texture
278 .create_view(&wgpu::TextureViewDescriptor::default());
279 let mut encoder =
280 self.renderer
281 .device
282 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
283 label: Some("siglus_config_egui_encoder"),
284 });
285 self.egui_renderer.update_buffers(
286 &self.renderer.device,
287 &self.renderer.queue,
288 &mut encoder,
289 &paint_jobs,
290 &screen_desc,
291 );
292 {
293 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
294 label: Some("siglus_config_egui_pass"),
295 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
296 view: &view,
297 resolve_target: None,
298 ops: wgpu::Operations {
299 load: wgpu::LoadOp::Clear(wgpu::Color {
300 r: 0.965,
301 g: 0.970,
302 b: 0.980,
303 a: 1.0,
304 }),
305 store: wgpu::StoreOp::Store,
306 },
307 })],
308 depth_stencil_attachment: None,
309 timestamp_writes: None,
310 occlusion_query_set: None,
311 });
312 self.egui_renderer
313 .render(&mut pass, &paint_jobs, &screen_desc);
314 }
315 self.renderer.queue.submit(Some(encoder.finish()));
316 frame.present();
317 for id in output.textures_delta.free {
318 self.egui_renderer.free_texture(&id);
319 }
320 Ok(action)
321 }
322}
323fn map_pointer_button(button: MouseButton) -> Option<egui::PointerButton> {
324 match button {
325 MouseButton::Left => Some(egui::PointerButton::Primary),
326 MouseButton::Right => Some(egui::PointerButton::Secondary),
327 MouseButton::Middle => Some(egui::PointerButton::Middle),
328 _ => None,
329 }
330}
331
332fn map_modifiers(state: ModifiersState) -> egui::Modifiers {
333 egui::Modifiers {
334 alt: state.alt_key(),
335 ctrl: state.control_key(),
336 shift: state.shift_key(),
337 mac_cmd: cfg!(target_os = "macos") && state.meta_key(),
338 command: if cfg!(target_os = "macos") {
339 state.meta_key()
340 } else {
341 state.control_key()
342 },
343 }
344}
345
346fn map_key(code: KeyCode) -> Option<egui::Key> {
347 use KeyCode::*;
348 Some(match code {
349 ArrowDown => egui::Key::ArrowDown,
350 ArrowLeft => egui::Key::ArrowLeft,
351 ArrowRight => egui::Key::ArrowRight,
352 ArrowUp => egui::Key::ArrowUp,
353 Escape => egui::Key::Escape,
354 Tab => egui::Key::Tab,
355 Backspace => egui::Key::Backspace,
356 Enter | NumpadEnter => egui::Key::Enter,
357 Space => egui::Key::Space,
358 Insert => egui::Key::Insert,
359 Delete => egui::Key::Delete,
360 Home => egui::Key::Home,
361 End => egui::Key::End,
362 PageUp => egui::Key::PageUp,
363 PageDown => egui::Key::PageDown,
364 KeyA => egui::Key::A,
365 KeyC => egui::Key::C,
366 KeyV => egui::Key::V,
367 KeyX => egui::Key::X,
368 KeyY => egui::Key::Y,
369 KeyZ => egui::Key::Z,
370 _ => return None,
371 })
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
375enum Tab {
376 Screen,
377 Volume,
378 Message,
379 Background,
380 Voice,
381 Auto,
382 Jitan,
383 Other,
384 System,
385}
386
387impl Tab {
388 const ALL: [Self; 9] = [
389 Self::Screen,
390 Self::Volume,
391 Self::Message,
392 Self::Background,
393 Self::Voice,
394 Self::Auto,
395 Self::Jitan,
396 Self::Other,
397 Self::System,
398 ];
399 fn definition(self) -> (&'static str, &'static str) {
400 match self {
401 Self::Screen => ("SCREEN", "画面"),
402 Self::Volume => ("VOLUME", "音量"),
403 Self::Message => ("MESSAGE", "文章"),
404 Self::Background => ("MWNDBK", "背景色"),
405 Self::Voice => ("KOE", "音声"),
406 Self::Auto => ("AUTOMODE", "オートモード"),
407 Self::Jitan => ("JITAN", "時短再生"),
408 Self::Other => ("ELSE", "その他"),
409 Self::System => ("SYSTEM", "システム"),
410 }
411 }
412}
413
414pub struct ConfigDialog {
415 pub state: OriginalConfigRuntimeState,
416 defaults: OriginalConfigRuntimeState,
417 gameexe: GameexeConfig,
418 tabs: Vec<Tab>,
419 tab: Tab,
420 voices: Vec<(usize, String)>,
421 remember_tab: bool,
422}
423
424impl ConfigDialog {
425 pub fn remember_tab_from(&mut self, previous: &Self) {
426 if self.remember_tab && self.tabs.contains(&previous.tab) {
427 self.tab = previous.tab;
428 }
429 }
430
431 pub fn new(ctx: &CommandContext) -> Self {
432 use crate::runtime::forms::codes::syscom_op::*;
433 let gameexe = ctx.tables.gameexe.clone().unwrap_or_default();
434 let tabs: Vec<_> = Tab::ALL
435 .into_iter()
436 .filter(|t| {
437 gameexe
438 .get_i64(&format!("DIALOG_TAB_EXIST.{}", t.definition().0))
439 .unwrap_or(1)
440 != 0
441 })
442 .collect();
443 let requested = match ctx.globals.syscom.last_menu_call {
444 CALL_CONFIG_WINDOW_MODE_MENU => Tab::Screen,
445 CALL_CONFIG_VOLUME_MENU | CALL_CONFIG_BGMFADE_MENU => Tab::Volume,
446 CALL_CONFIG_FONT_MENU | CALL_CONFIG_MESSAGE_SPEED_MENU => Tab::Message,
447 CALL_CONFIG_FILTER_COLOR_MENU => Tab::Background,
448 CALL_CONFIG_KOEMODE_MENU | CALL_CONFIG_CHARAKOE_MENU => Tab::Voice,
449 CALL_CONFIG_AUTO_MODE_MENU => Tab::Auto,
450 CALL_CONFIG_JITAN_MENU => Tab::Jitan,
451 CALL_CONFIG_SYSTEM_MENU | CALL_CONFIG_MOVIE_MENU => Tab::System,
452 _ => Tab::Screen,
453 };
454 let tab = if tabs.contains(&requested) {
455 requested
456 } else {
457 tabs.first().copied().unwrap_or(Tab::Screen)
458 };
459 let state = syscom::config_state_for_save(ctx);
460 let voices = (0..state.chrkoe.len())
461 .filter_map(|i| {
462 let e = gameexe.get_indexed_entry("CHRKOE", i)?;
463 let name = e.item_unquoted(0)?.to_owned();
464 if name.is_empty() {
465 return None;
466 }
467 let label = if syscom::config_chrkoe_name_visible(ctx, i) {
468 name
469 } else {
470 gameexe
471 .get_unquoted("CHRKOE.NOT_LOOK_NAME_STR")
472 .unwrap_or("???")
473 .to_owned()
474 };
475 Some((i, label))
476 })
477 .collect();
478 Self {
479 state,
480 defaults: syscom::original_config_defaults(ctx),
481 gameexe,
482 tabs,
483 tab,
484 voices,
485 remember_tab: ctx.globals.syscom.last_menu_call == CALL_CONFIG_MENU,
486 }
487 }
488
489 fn show(&mut self, ctx: &egui::Context) -> bool {
490 let mut close = ctx.input(|i| i.key_pressed(egui::Key::Escape));
491 egui::TopBottomPanel::bottom("config_footer").show(ctx, |ui| {
492 ui.add_space(8.0);
493 ui.horizontal(|ui| {
494 ui.label(self.gameexe.get_unquoted("GAMEVERSION").unwrap_or(""));
495 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
496 if ui.button("閉じる").clicked() {
497 close = true;
498 }
499 if ui.button("全て初期状態に戻す").clicked() {
500 for tab in Tab::ALL {
501 self.reset_tab(tab);
502 }
503 }
504 });
505 });
506 ui.add_space(8.0);
507 });
508 egui::TopBottomPanel::top("config_tabs").show(ctx, |ui| {
509 ui.add_space(6.0);
510 ui.horizontal_wrapped(|ui| {
511 for &tab in &self.tabs {
512 ui.selectable_value(&mut self.tab, tab, tab.definition().1);
513 }
514 });
515 ui.add_space(6.0);
516 });
517 egui::CentralPanel::default().show(ctx, |ui| {
518 ui.spacing_mut().item_spacing = egui::vec2(10.0, 12.0);
519 ui.spacing_mut().slider_width = 270.0;
520 egui::ScrollArea::vertical().show(ui, |ui| {
521 if self.tabs.is_empty() {
522 return;
523 }
524 match self.tab {
525 Tab::Screen => self.screen(ui),
526 Tab::Volume => self.volume(ui),
527 Tab::Message => self.message(ui),
528 Tab::Background => self.background(ui),
529 Tab::Voice => self.voice(ui),
530 Tab::Auto => self.auto(ui),
531 Tab::Jitan => self.jitan(ui),
532 Tab::Other => self.other(ui),
533 Tab::System => self.system(ui),
534 }
535 ui.separator();
536 if ui.button("初期状態に戻す").clicked() {
537 self.reset_tab(self.tab);
538 }
539 });
540 });
541 close
542 }
543
544 fn screen(&mut self, ui: &mut egui::Ui) {
545 ui.group(|ui| {
546 ui.label("画面モード");
547 ui.horizontal(|ui| {
548 ui.radio_value(&mut self.state.screen_size_mode, 1, "フルスクリーン");
549 ui.radio_value(&mut self.state.screen_size_mode, 0, "ウィンドウ");
550 });
551 });
552 ui.add_enabled_ui(self.state.screen_size_mode == 0, |ui| {
553 ui.group(|ui| {
554 ui.label("ウィンドウサイズ");
555 ui.add(
556 egui::Slider::new(&mut self.state.screen_size_scale.0, 50..=200).suffix(" %"),
557 );
558 self.state.screen_size_scale.1 = self.state.screen_size_scale.0;
559 ui.horizontal(|ui| {
560 for size in [50, 75, 100, 125, 150, 200] {
561 if ui.button(format!("{size}%")).clicked() {
562 self.state.screen_size_scale = (size, size);
563 }
564 }
565 });
566 });
567 });
568 ui.checkbox(
569 &mut self.state.mouse_cursor_hide_onoff,
570 "自動でマウスカーソルを隠す",
571 );
572 ui.add_enabled(
573 self.state.mouse_cursor_hide_onoff,
574 egui::Slider::new(&mut self.state.mouse_cursor_hide_time, 0..=10000).suffix(" ms"),
575 );
576 }
577
578 fn volume(&mut self, ui: &mut egui::Ui) {
579 ui.label("音量");
580 volume_row(
581 ui,
582 "全体",
583 &mut self.state.all_sound_user_volume,
584 &mut self.state.play_all_sound_check,
585 );
586 for (i, (key, label)) in [
587 ("BGM", "BGM"),
588 ("KOE", "音声"),
589 ("PCM", "効果音"),
590 ("SE", "システム音"),
591 ("MOVIE", "ムービー"),
592 ]
593 .into_iter()
594 .enumerate()
595 {
596 if self
597 .gameexe
598 .get_i64(&format!("DIALOG_EXIST.{key}"))
599 .unwrap_or(1)
600 != 0
601 {
602 volume_row(
603 ui,
604 label,
605 &mut self.state.sound_user_volume[i],
606 &mut self.state.play_sound_check[i],
607 );
608 }
609 }
610 if self.gameexe.get_i64("DIALOG_STYLE.VOLUME").unwrap_or(0) != 1 {
611 ui.separator();
612 ui.checkbox(
613 &mut self.state.bgmfade_use_check,
614 "音声再生時にBGMの音量を下げる",
615 );
616 ui.add_enabled(
617 self.state.bgmfade_use_check,
618 egui::Slider::new(&mut self.state.bgmfade_volume, 0..=255).text("BGM音量"),
619 );
620 }
621 }
622
623 fn message(&mut self, ui: &mut egui::Ui) {
624 ui.group(|ui| {
625 ui.label("フォント");
626 egui::ComboBox::from_id_source("font_name")
627 .selected_text(&self.state.font_name)
628 .show_ui(ui, |ui| {
629 let mut names = vec!["MS ゴシック", "MS 明朝", "メイリオ"];
630 if !names.contains(&self.defaults.font_name.as_str()) {
631 names.push(&self.defaults.font_name);
632 }
633 for name in names {
634 ui.selectable_value(&mut self.state.font_name, name.to_owned(), name);
635 }
636 });
637 });
638 ui.group(|ui| {
639 ui.label("文字速度");
640 let mut speed = 100 - self.state.message_speed;
642 if ui
643 .add_enabled(
644 !self.state.message_speed_nowait,
645 egui::Slider::new(&mut speed, 0..=100).text("遅い ← → 速い"),
646 )
647 .changed()
648 {
649 self.state.message_speed = 100 - speed;
650 }
651 ui.checkbox(&mut self.state.message_speed_nowait, "一瞬で表示する");
652 });
653 }
654
655 fn background(&mut self, ui: &mut egui::Ui) {
656 ui.label("メッセージウィンドウ背景色");
657 let mut rgba = [
658 ((self.state.filter_color_argb >> 16) & 255) as i64,
659 ((self.state.filter_color_argb >> 8) & 255) as i64,
660 (self.state.filter_color_argb & 255) as i64,
661 ((self.state.filter_color_argb >> 24) & 255) as i64,
662 ];
663 for (v, label) in rgba.iter_mut().zip(["赤", "緑", "青", "不透明度"]) {
664 ui.add(egui::Slider::new(v, 0..=255).text(label));
665 }
666 self.state.filter_color_argb = ((rgba[3] as u32) << 24)
667 | ((rgba[0] as u32) << 16)
668 | ((rgba[1] as u32) << 8)
669 | rgba[2] as u32;
670 let (rect, _) = ui.allocate_exact_size(egui::vec2(320.0, 70.0), egui::Sense::hover());
671 ui.painter().rect_filled(
672 rect,
673 0.0,
674 egui::Color32::from_rgba_unmultiplied(
675 rgba[0] as u8,
676 rgba[1] as u8,
677 rgba[2] as u8,
678 rgba[3] as u8,
679 ),
680 );
681 }
682
683 fn voice(&mut self, ui: &mut egui::Ui) {
684 let style = self.gameexe.get_i64("DIALOG_STYLE.KOE").unwrap_or(0);
685 if style != 2 {
686 ui.label("音声モード");
687 for (i, label) in [
688 "音声あり(文章あり)",
689 "音声なし(文章のみ)",
690 "音声あり(文章なし)",
691 ]
692 .into_iter()
693 .enumerate()
694 {
695 ui.radio_value(&mut self.state.koe_mode, i as i64, label);
696 }
697 }
698 if style != 1 {
699 ui.separator();
700 ui.label("キャラクター音声");
701 for (i, name) in &self.voices {
702 if let Some(voice) = self.state.chrkoe.get_mut(*i) {
703 ui.push_id(i, |ui| {
704 volume_row(ui, name, &mut voice.volume, &mut voice.onoff);
705 });
706 }
707 }
708 }
709 }
710
711 fn auto(&mut self, ui: &mut egui::Ui) {
712 ui.label("オートモード");
713 ui.add(
714 egui::Slider::new(&mut self.state.auto_mode_moji_wait, 0..=500)
715 .suffix(" ms")
716 .text("1文字あたりの待ち時間"),
717 );
718 ui.add(
719 egui::Slider::new(&mut self.state.auto_mode_min_wait, 0..=10000)
720 .suffix(" ms")
721 .text("最小待ち時間"),
722 );
723 }
724
725 fn jitan(&mut self, ui: &mut egui::Ui) {
726 ui.label("時短再生");
727 ui.add(
728 egui::Slider::new(&mut self.state.jitan_speed, 100..=300)
729 .step_by(25.0)
730 .suffix(" %"),
731 );
732 ui.checkbox(
733 &mut self.state.jitan_normal_onoff,
734 "文章を普通に読み進めている時に使用する",
735 );
736 ui.checkbox(
737 &mut self.state.jitan_auto_mode_onoff,
738 "オートモード中に使用する",
739 );
740 ui.checkbox(
741 &mut self.state.jitan_msgbk_onoff,
742 "声のリプレイ時の再生速度を変更する",
743 );
744 }
745
746 fn other(&mut self, ui: &mut egui::Ui) {
747 configured_checkbox(
748 ui,
749 &self.gameexe,
750 "MESSAGE_CHRCOLOR",
751 true,
752 "文章を色分けする。",
753 &mut self.state.message_chrcolor_flag,
754 );
755 for i in 0..4 {
756 configured_checkbox(
757 ui,
758 &self.gameexe,
759 &format!("OBJECT_DISP.{i:03}"),
760 i < 2,
761 &format!("オブジェクト表示{i}番を表示する。"),
762 &mut self.state.object_disp_flag[i],
763 );
764 configured_checkbox(
765 ui,
766 &self.gameexe,
767 &format!("GLOBAL_EXTRA_SWITCH.{i:03}"),
768 i < 2,
769 &format!("グローバル汎用スイッチ{i}番を使用する。"),
770 &mut self.state.global_extra_switch_flag[i],
771 );
772 let key = format!("DIALOG.GLOBAL_EXTRA_MODE.{i:03}");
773 if self.gameexe.get_i64(&format!("{key}.EXIST")).unwrap_or(1) == 0 {
774 continue;
775 }
776 ui.group(|ui| {
777 ui.label(
778 self.gameexe
779 .get_unquoted(&format!("{key}.STR"))
780 .map(str::to_owned)
781 .unwrap_or_else(|| format!("グローバル汎用モード{i}番")),
782 );
783 let count = self
784 .gameexe
785 .get_i64(&format!("{key}.ITEM_CNT"))
786 .unwrap_or(if i < 2 { 3 } else { 1 })
787 .clamp(0, 32);
788 for j in 0..count {
789 let label = self
790 .gameexe
791 .get_unquoted(&format!("{key}.ITEM.{j:03}.STR"))
792 .map(str::to_owned)
793 .unwrap_or_else(|| format!("モード{j}"));
794 ui.radio_value(&mut self.state.global_extra_mode_flag[i], j, label);
795 }
796 });
797 }
798 }
799
800 fn system(&mut self, ui: &mut egui::Ui) {
801 let c = &mut self.state;
802 for (key, label, value) in [
803 (
804 "SLEEP",
805 "本プログラムの動作を遅くして、他のプログラムがスムーズに動作するようにする。",
806 &mut c.sleep_flag,
807 ),
808 (
809 "NO_WIPE_ANIME",
810 "画面暗転効果のアニメを無効にする。",
811 &mut c.no_wipe_anime_flag,
812 ),
813 (
814 "SKIP_WIPE_ANIME",
815 "画面暗転効果をマウスクリックで飛ばす。",
816 &mut c.skip_wipe_anime_flag,
817 ),
818 (
819 "NO_MWND_ANIME",
820 "メッセージウィンドウの開閉時のアニメを無効にする。",
821 &mut c.no_mwnd_anime_flag,
822 ),
823 (
824 "WHEEL_NEXT_MESSAGE",
825 "マウスのホイールボタンの下回しで文章を読み進める。",
826 &mut c.wheel_next_message_flag,
827 ),
828 (
829 "KOE_DONT_STOP",
830 "声の再生中に次の文章に進んでも再生を続ける。",
831 &mut c.koe_dont_stop_flag,
832 ),
833 (
834 "SKIP_UNREAD_MESSAGE",
835 "未読の文章も早送りできるようにする。",
836 &mut c.skip_unread_message_flag,
837 ),
838 ] {
839 configured_checkbox(ui, &self.gameexe, key, true, label, value);
840 }
841 ui.checkbox(
844 &mut c.joypad_swap_ab,
845 "コントローラーの A / B ボタンを入れ替える。",
846 );
847 ui.checkbox(
848 &mut c.joypad_swap_xy,
849 "コントローラーの X / Y ボタンを入れ替える。",
850 );
851 }
852
853 fn reset_tab(&mut self, tab: Tab) {
854 let c = &mut self.state;
855 let d = &self.defaults;
856 match tab {
858 Tab::Screen => {
859 c.screen_size_mode = d.screen_size_mode;
860 c.screen_size_scale = d.screen_size_scale;
861 c.mouse_cursor_hide_onoff = d.mouse_cursor_hide_onoff;
862 c.mouse_cursor_hide_time = d.mouse_cursor_hide_time;
863 }
864 Tab::Volume => {
865 c.all_sound_user_volume = d.all_sound_user_volume;
866 c.sound_user_volume = d.sound_user_volume;
867 c.play_all_sound_check = d.play_all_sound_check;
868 c.play_sound_check = d.play_sound_check;
869 c.bgmfade_use_check = d.bgmfade_use_check;
870 c.bgmfade_volume = d.bgmfade_volume;
871 }
872 Tab::Message => {
873 c.font_name = d.font_name.clone();
874 c.font_futoku = d.font_futoku;
875 c.font_shadow = d.font_shadow;
876 c.message_speed = d.message_speed;
877 c.message_speed_nowait = d.message_speed_nowait;
878 }
879 Tab::Background => c.filter_color_argb = d.filter_color_argb,
880 Tab::Voice => {
881 c.koe_mode = d.koe_mode;
882 c.chrkoe = d.chrkoe.clone();
883 }
884 Tab::Auto => {
885 c.auto_mode_moji_wait = d.auto_mode_moji_wait;
886 c.auto_mode_min_wait = d.auto_mode_min_wait;
887 }
888 Tab::Jitan => {
889 c.jitan_normal_onoff = d.jitan_normal_onoff;
890 c.jitan_auto_mode_onoff = d.jitan_auto_mode_onoff;
891 c.jitan_msgbk_onoff = d.jitan_msgbk_onoff;
892 c.jitan_speed = d.jitan_speed;
893 }
894 Tab::Other => {
895 c.message_chrcolor_flag = d.message_chrcolor_flag;
896 c.object_disp_flag = d.object_disp_flag.clone();
897 c.global_extra_switch_flag = d.global_extra_switch_flag.clone();
898 c.global_extra_mode_flag = d.global_extra_mode_flag.clone();
899 }
900 Tab::System => {
901 c.sleep_flag = d.sleep_flag;
902 c.no_wipe_anime_flag = d.no_wipe_anime_flag;
903 c.skip_wipe_anime_flag = d.skip_wipe_anime_flag;
904 c.no_mwnd_anime_flag = d.no_mwnd_anime_flag;
905 c.wheel_next_message_flag = d.wheel_next_message_flag;
906 c.koe_dont_stop_flag = d.koe_dont_stop_flag;
907 c.skip_unread_message_flag = d.skip_unread_message_flag;
908 c.joypad_swap_ab = d.joypad_swap_ab;
909 c.joypad_swap_xy = d.joypad_swap_xy;
910 }
911 }
912 }
913}
914
915fn volume_row(ui: &mut egui::Ui, label: &str, volume: &mut i64, enabled: &mut bool) {
916 ui.horizontal(|ui| {
917 ui.add_sized([90.0, 20.0], egui::Label::new(label));
918 ui.add_enabled(*enabled, egui::Slider::new(volume, 0..=255));
919 ui.checkbox(enabled, "再生");
920 });
921}
922
923fn configured_checkbox(
924 ui: &mut egui::Ui,
925 gameexe: &GameexeConfig,
926 key: &str,
927 default_exist: bool,
928 default_label: &str,
929 value: &mut bool,
930) {
931 if gameexe
932 .get_i64(&format!("DIALOG.{key}.EXIST"))
933 .unwrap_or(default_exist as i64)
934 != 0
935 {
936 ui.checkbox(
937 value,
938 gameexe
939 .get_unquoted(&format!("DIALOG.{key}.STR"))
940 .unwrap_or(default_label),
941 );
942 }
943}
944
945#[cfg(test)]
946mod tests {
947 use super::*;
948 use crate::runtime::forms::codes::syscom_op::*;
949
950 #[cfg(target_os = "linux")]
951 #[test]
952 #[ignore = "requires a desktop session and GPU"]
953 fn native_config_close_releases_window_and_surface() {
954 use winit::application::ApplicationHandler;
955 use winit::event_loop::EventLoop;
956 use winit::platform::wayland::EventLoopBuilderExtWayland;
957
958 struct Probe;
959 impl ApplicationHandler for Probe {
960 fn can_create_surfaces(&mut self, elwt: &dyn ActiveEventLoop) {
961 let ctx =
962 CommandContext::new(std::env::temp_dir().join("siglus-config-close-test"));
963 let mut dialog = ConfigDialog::new(&ctx);
964 for _ in 0..2 {
965 let mut window = DesktopConfigWindow::new(elwt, dialog).unwrap();
966 let weak = Arc::downgrade(&window.window);
967 window.render().unwrap();
968 assert!(matches!(
969 window.handle_window_event(WindowEvent::CloseRequested),
970 Some(DesktopConfigAction::Close)
971 ));
972 dialog = window.into_dialog();
973 assert!(weak.upgrade().is_none(), "native window or surface leaked");
974 }
975 elwt.exit();
976 }
977
978 fn window_event(&mut self, _: &dyn ActiveEventLoop, _: WindowId, _: WindowEvent) {}
979 }
980
981 let event_loop = EventLoop::builder().with_any_thread(true).build().unwrap();
982 event_loop.run_app(Probe).unwrap();
983 }
984
985 #[test]
986 fn recreated_dialog_remembers_tab_without_reusing_stale_settings() {
987 let mut ctx = CommandContext::new(std::env::temp_dir().join("siglus-config-reopen-test"));
988 ctx.globals.syscom.last_menu_call = CALL_CONFIG_MENU;
989 let mut previous = ConfigDialog::new(&ctx);
990 previous.tab = Tab::Volume;
991 ctx.globals
992 .syscom
993 .config_int
994 .insert(GET_WINDOW_MODE_SIZE, 75);
995 let mut reopened = ConfigDialog::new(&ctx);
996 reopened.remember_tab_from(&previous);
997 assert_eq!(reopened.tab, Tab::Volume);
998 assert_eq!(reopened.state.screen_size_scale, (75, 75));
999
1000 ctx.globals.syscom.last_menu_call = CALL_CONFIG_WINDOW_MODE_MENU;
1001 let mut requested = ConfigDialog::new(&ctx);
1002 requested.remember_tab_from(&previous);
1003 assert_eq!(requested.tab, Tab::Screen);
1004
1005 ctx.globals.syscom.last_menu_call = CALL_CONFIG_FONT_MENU;
1006 let mut font = ConfigDialog::new(&ctx);
1007 font.remember_tab_from(&previous);
1008 assert_eq!(font.tab, Tab::Message);
1009
1010 ctx.globals.syscom.last_menu_call = CALL_CONFIG_MENU;
1011 ctx.tables.gameexe = Some(GameexeConfig::from_text("#DIALOG_TAB_EXIST.VOLUME=0"));
1012 let mut hidden = ConfigDialog::new(&ctx);
1013 hidden.remember_tab_from(&previous);
1014 assert_eq!(hidden.tab, Tab::Screen);
1015 }
1016
1017 #[test]
1018 fn game_definitions_control_tabs_voice_names_and_reset_scope() {
1019 let mut ctx = CommandContext::new(std::env::temp_dir().join("siglus-config-ui-test"));
1020 ctx.tables.gameexe = Some(GameexeConfig::from_text(
1021 r#"
1022#DIALOG_TAB_EXIST.ELSE=0
1023#DIALOG_TAB_EXIST.ELSE=1
1024#DIALOG_TAB_EXIST.JITAN=0
1025#CONFIG.VOLUME.BGM=165
1026#CONFIG.GLOBAL_EXTRA_SWITCH.000.ONOFF=0
1027#CHRKOE.000="First",1,"First",1,235,(0)
1028#CHRKOE.001="Hidden",2,"Alias",1,210,(1)
1029#CHRKOE.NOT_LOOK_NAME_STR="???"
1030"#,
1031 ));
1032 syscom::apply_config_dialog_state(&mut ctx, OriginalConfigRuntimeState::default());
1033 ctx.globals.syscom.config_int.insert(GET_BGM_VOLUME, 77);
1035 ctx.globals.syscom.last_menu_call = CALL_CONFIG_VOLUME_MENU;
1036 let mut dialog = ConfigDialog::new(&ctx);
1037 assert_eq!(dialog.tab, Tab::Volume);
1038 assert!(dialog.tabs.contains(&Tab::Other));
1039 assert!(!dialog.tabs.contains(&Tab::Jitan));
1040 assert_eq!(dialog.voices, vec![(0, "First".into()), (1, "???".into())]);
1041 assert_eq!(dialog.state.sound_user_volume[0], 77);
1042 dialog.state.editor_path = "keep this path".into();
1043 dialog.state.message_speed = 42;
1044 dialog.reset_tab(Tab::Volume);
1045 assert_eq!(dialog.state.sound_user_volume[0], 165);
1046 assert_eq!(dialog.state.message_speed, 42);
1047 assert_eq!(dialog.state.editor_path, "keep this path");
1048 dialog.reset_tab(Tab::Other);
1049 assert!(!dialog.state.global_extra_switch_flag[0]);
1050 syscom::reveal_config_voice_name(&mut ctx, "Alias");
1051 assert_eq!(ConfigDialog::new(&ctx).voices[1].1, "Hidden");
1052
1053 let egui = egui::Context::default();
1055 for tab in Tab::ALL {
1056 dialog.tab = tab;
1057 let _ = egui.run(egui::RawInput::default(), |ctx| {
1058 dialog.show(ctx);
1059 });
1060 }
1061 }
1062}