1mod attribute;
2pub mod theme;
3
4use cmaze::{
5 algorithms::{MazeSpec, MazeSpecType},
6 dims::{Dims, Offset},
7};
8use derivative::Derivative;
9use serde::{Deserialize, Serialize};
10use std::{
11 fs, io,
12 path::PathBuf,
13 sync::{Arc, RwLock},
14};
15use theme::ThemeDefinition;
16
17use crate::{
18 app::{self, app::AppData, Activity, ActivityHandler, Change},
19 helpers::constants::paths::settings_path,
20 menu_actions,
21 renderer::MouseGuard,
22 ui::{split_menu_actions, Menu, MenuAction, MenuConfig, MenuItem, OptionDef, Popup, Screen},
23};
24
25#[cfg(feature = "sound")]
26use crate::sound::create_audio_settings;
27
28const DEFAULT_SETTINGS_JSON: &str = include_str!("./default_settings.json5");
29
30#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
31#[serde(tag = "mode")]
32pub enum CameraMode {
33 #[default]
34 CloseFollow,
35 EdgeFollow {
36 x: Offset,
37 y: Offset,
38 },
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct MazePreset {
43 pub title: String,
44 pub description: Option<String>,
45
46 #[serde(default)]
47 pub default: bool,
48
49 #[serde(flatten)]
51 pub maze_spec: MazeSpec,
52}
53
54impl MazePreset {
55 pub fn short_desc(&self) -> Option<String> {
56 let (size, cells): (_, usize) = match &self.maze_spec.inner_spec {
57 MazeSpecType::Regions { regions, .. } => (
58 self.maze_spec.size()?,
59 regions.iter().map(|r| r.mask.enabled_count()).sum(),
60 ),
61 MazeSpecType::Simple { mask, .. } => {
62 let size = self.maze_spec.size()?;
63 (
64 size,
65 mask.as_ref()
66 .map(|m| m.enabled_count())
67 .unwrap_or(size.product() as usize),
68 )
69 }
70 };
71
72 if size.2 == 1 {
73 Some(format!(
74 "{}: {}x{} ({} cells)",
75 self.title, size.0, size.1, cells
76 ))
77 } else {
78 Some(format!(
79 "{}: {}x{}x{} ({} cells)",
80 self.title, size.0, size.1, size.2, cells
81 ))
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
87pub enum UpdateCheckInterval {
88 Never,
89 #[default]
90 Daily,
91 Weekly,
92 Monthly,
93 Yearly,
94 Always,
95}
96
97#[derive(Debug, Derivative, Serialize, Deserialize)]
98#[derivative(Default)]
99#[serde(rename = "Settings")]
100pub struct SettingsInner {
102 #[serde(default)]
104 pub theme: Option<String>,
105 #[serde(default)]
106 pub logging_level: Option<String>,
107 #[serde(default)]
108 pub debug_logging_level: Option<String>,
109 #[serde(default)]
110 pub file_logging_level: Option<String>,
111
112 #[serde(default)]
114 pub slow: Option<bool>,
115 #[serde(default)]
116 pub disable_tower_auto_up: Option<bool>,
117 #[serde(default)]
118 pub camera_mode: Option<CameraMode>,
119 #[serde(default)]
120 pub camera_smoothing: Option<f32>,
121 #[serde[default]]
122 pub player_smoothing: Option<f32>,
123 #[serde(default)]
124 pub viewport_margin: Option<(i32, i32)>,
125
126 #[serde(default)]
128 pub enable_mouse: Option<bool>,
129 #[serde(default)]
130 pub enable_dpad: Option<bool>,
131 #[serde(default)]
132 pub landscape_dpad_on_left: Option<bool>,
133 #[serde(default)]
134 pub dpad_swap_up_down: Option<bool>,
135 #[serde(default)]
136 pub enable_margin_around_dpad: Option<bool>,
137 #[serde(default)]
138 pub enable_dpad_highlight: Option<bool>,
139
140 #[serde(default)]
142 pub update_check_interval: Option<UpdateCheckInterval>,
143 #[serde(default)]
144 pub display_update_check_errors: Option<bool>,
145
146 #[serde(default)]
148 pub enable_audio: Option<bool>,
149 #[serde(default)]
150 pub audio_volume: Option<f32>,
151 #[serde(default)]
152 pub enable_music: Option<bool>,
153 #[serde(default)]
154 pub music_volume: Option<f32>,
155
156 #[serde(default)]
158 pub presets: Option<Vec<MazePreset>>,
159 }
165
166#[derive(Debug, Clone)]
167pub struct Settings {
168 shared: Arc<RwLock<SettingsInner>>,
169 path: PathBuf,
170 read_only: bool,
171}
172
173impl Default for Settings {
174 fn default() -> Self {
175 let settings = SettingsInner::default();
176 Self {
177 shared: Arc::new(RwLock::new(settings)),
178 path: settings_path(),
179 read_only: false,
180 }
181 }
182}
183
184#[allow(dead_code)]
185impl Settings {
186 pub fn new() -> Self {
187 Self::default()
188 }
189
190 pub fn path(&self) -> PathBuf {
191 self.path.clone()
192 }
193
194 pub fn is_ro(&self) -> bool {
195 self.read_only
196 }
197
198 pub fn read(&self) -> std::sync::RwLockReadGuard<SettingsInner> {
199 self.shared.read().unwrap()
200 }
201
202 pub fn write(&mut self) -> std::sync::RwLockWriteGuard<SettingsInner> {
203 self.shared.write().unwrap()
204 }
205}
206
207impl Settings {
208 pub fn get_theme(&self) -> ThemeDefinition {
209 let theme_name = self.read().theme.clone();
210 if let Some(theme_name) = theme_name {
211 ThemeDefinition::load_by_name(&theme_name).expect("could not load the theme")
212 } else {
213 ThemeDefinition::load_default(self.read_only).expect("could not load the default theme")
214 }
215 }
216
217 pub fn get_logging_level(&self) -> log::Level {
218 self.read()
219 .logging_level
220 .clone()
221 .and_then(|level| level.parse().ok())
222 .unwrap_or(log::Level::Info)
223 }
224
225 pub fn get_debug_logging_level(&self) -> log::Level {
226 self.read()
227 .debug_logging_level
228 .clone()
229 .and_then(|level| level.parse().ok())
230 .unwrap_or(log::Level::Info)
231 }
232
233 pub fn get_file_logging_level(&self) -> log::Level {
234 self.read()
235 .file_logging_level
236 .clone()
237 .and_then(|level| level.parse().ok())
238 .unwrap_or(log::Level::Info)
239 }
240
241 pub fn get_slow(&self) -> bool {
242 self.read().slow.unwrap_or_default()
243 }
244
245 pub fn set_slow(&mut self, value: bool) -> &mut Self {
246 self.write().slow = Some(value);
247 self
248 }
249
250 pub fn get_disable_tower_auto_up(&self) -> bool {
251 self.read().disable_tower_auto_up.unwrap_or_default()
252 }
253
254 pub fn set_disable_tower_auto_up(&mut self, value: bool) -> &mut Self {
255 self.write().disable_tower_auto_up = Some(value);
256 self
257 }
258
259 pub fn get_camera_mode(&self) -> CameraMode {
260 self.read().camera_mode.unwrap_or_default()
261 }
262
263 pub fn set_camera_mode(&mut self, value: CameraMode) -> &mut Self {
264 self.write().camera_mode = Some(value);
265 self
266 }
267
268 pub fn get_camera_smoothing(&self) -> f32 {
269 self.read().camera_smoothing.unwrap_or(0.5).clamp(0.5, 1.0)
270 }
271
272 pub fn set_camera_smoothing(&mut self, value: f32) -> &mut Self {
273 self.write().camera_smoothing = Some(value.clamp(0.5, 1.0));
274 self
275 }
276
277 pub fn get_player_smoothing(&self) -> f32 {
278 self.read().player_smoothing.unwrap_or(0.8).clamp(0.5, 1.0)
279 }
280
281 pub fn set_player_smoothing(&mut self, value: f32) -> &mut Self {
282 self.write().player_smoothing = Some(value.clamp(0.5, 1.0));
283 self
284 }
285
286 pub fn get_viewport_margin(&self) -> Dims {
287 self.read()
288 .viewport_margin
289 .map(Dims::from)
290 .unwrap_or(Dims(4, 3))
291 }
292
293 pub fn set_viewport_margin(&mut self, value: Dims) -> &mut Self {
294 self.write().viewport_margin = Some(value.into());
295 self
296 }
297
298 pub fn get_enable_mouse(&self) -> bool {
299 self.read().enable_mouse.unwrap_or(true)
300 }
301
302 pub fn set_enable_mouse(&mut self, value: bool) -> &mut Self {
303 self.write().enable_mouse = Some(value);
304 self
305 }
306
307 pub fn get_enable_dpad(&self) -> bool {
308 self.read().enable_dpad.unwrap_or(false)
309 }
310
311 pub fn set_enable_dpad(&mut self, value: bool) -> &mut Self {
312 self.write().enable_dpad = Some(value);
313 self
314 }
315
316 pub fn get_landscape_dpad_on_left(&self) -> bool {
317 self.read().landscape_dpad_on_left.unwrap_or(false)
318 }
319
320 pub fn set_landscape_dpad_on_left(&mut self, value: bool) -> &mut Self {
321 self.write().landscape_dpad_on_left = Some(value);
322 self
323 }
324
325 pub fn get_dpad_swap_up_down(&self) -> bool {
326 self.read().dpad_swap_up_down.unwrap_or(false)
327 }
328
329 pub fn set_dpad_swap_up_down(&mut self, value: bool) -> &mut Self {
330 self.write().dpad_swap_up_down = Some(value);
331 self
332 }
333
334 pub fn get_enable_margin_around_dpad(&self) -> bool {
335 self.read().enable_margin_around_dpad.unwrap_or(false)
336 }
337
338 pub fn set_enable_margin_around_dpad(&mut self, value: bool) -> &mut Self {
339 self.write().enable_margin_around_dpad = Some(value);
340 self
341 }
342
343 pub fn get_enable_dpad_highlight(&self) -> bool {
344 self.read().enable_dpad_highlight.unwrap_or(true)
345 }
346
347 pub fn set_enable_dpad_highlight(&mut self, value: bool) -> &mut Self {
348 self.write().enable_dpad_highlight = Some(value);
349 self
350 }
351
352 pub fn set_check_interval(&mut self, value: UpdateCheckInterval) -> &mut Self {
353 self.write().update_check_interval = Some(value);
354 self
355 }
356
357 pub fn get_check_interval(&self) -> UpdateCheckInterval {
358 self.read().update_check_interval.unwrap_or_default()
359 }
360
361 pub fn get_display_update_check_errors(&self) -> bool {
362 self.read().display_update_check_errors.unwrap_or(true)
363 }
364
365 pub fn set_display_update_check_errors(&mut self, value: bool) -> &mut Self {
366 self.write().display_update_check_errors = Some(value);
367 self
368 }
369
370 pub fn get_enable_audio(&self) -> bool {
371 self.read().enable_audio.unwrap_or_default()
372 }
373
374 pub fn set_enable_audio(&mut self, value: bool) -> &mut Self {
375 self.write().enable_audio = Some(value);
376 self
377 }
378
379 pub fn get_audio_volume(&self) -> f32 {
380 self.read().audio_volume.unwrap_or_default().clamp(0., 1.)
381 }
382
383 pub fn set_audio_volume(&mut self, value: f32) -> &mut Self {
384 self.write().audio_volume = Some(value.clamp(0., 1.));
385 self
386 }
387
388 pub fn get_enable_music(&self) -> bool {
389 self.read().enable_music.unwrap_or_default()
390 }
391
392 pub fn set_enable_music(&mut self, value: bool) -> &mut Self {
393 self.write().enable_music = Some(value);
394 self
395 }
396
397 pub fn get_music_volume(&self) -> f32 {
398 self.read().music_volume.unwrap_or_default().clamp(0., 1.)
399 }
400
401 pub fn set_music_volume(&mut self, value: f32) -> &mut Self {
402 self.write().music_volume = Some(value.clamp(0., 1.));
403 self
404 }
405
406 pub fn set_presets(&mut self, value: Vec<MazePreset>) -> &mut Self {
407 self.write().presets = Some(value);
408 self
409 }
410
411 pub fn get_presets(&self) -> Vec<MazePreset> {
412 self.read().presets.clone().unwrap_or_default()
413 }
414}
415
416impl Settings {
418 pub fn load_json(path: PathBuf, read_only: bool) -> io::Result<Self> {
419 let settings_string = fs::read_to_string(&path);
420 let settings: SettingsInner = if let Ok(settings_string) = settings_string {
421 json5::from_str(&settings_string)
422 .expect("Could not parse settings file: check the syntax")
423 } else {
424 if !read_only {
425 fs::create_dir_all(path.parent().unwrap())?;
426 fs::write(&path, DEFAULT_SETTINGS_JSON)?;
427 }
428 json5::from_str(DEFAULT_SETTINGS_JSON).unwrap()
429 };
430
431 Ok(Self {
432 shared: Arc::new(RwLock::new(settings)),
433 path,
434 read_only,
435 })
436 }
437
438 pub fn reset_json(&mut self) {
439 *self.write() = json5::from_str(DEFAULT_SETTINGS_JSON).unwrap();
440
441 let path = settings_path();
442 fs::write(&path, DEFAULT_SETTINGS_JSON).unwrap();
443
444 self.path = path;
445 }
446
447 pub fn reset_json_config(path: PathBuf) {
448 fs::write(path, DEFAULT_SETTINGS_JSON).unwrap();
449 }
450}
451
452struct OtherSettingsPopup(Popup, MouseGuard);
453
454impl OtherSettingsPopup {
455 fn new(settings: &Settings) -> Self {
456 let popup = Popup::new(
457 "Other settings".to_string(),
458 vec![
459 "Path to the current settings:".to_string(),
460 format!(" {}", settings.path().to_string_lossy().to_string()),
461 "".to_string(),
462 "Other settings are not implemented in UI yet.".to_string(),
463 "Please edit the settings file directly.".to_string(),
464 ],
465 );
466
467 Self(popup, MouseGuard::new().unwrap())
468 }
469}
470
471impl ActivityHandler for OtherSettingsPopup {
472 fn update(&mut self, events: Vec<app::Event>, data: &mut AppData) -> Option<Change> {
473 self.0.update(events, data)
474 }
475
476 fn screen(&self) -> &dyn Screen {
477 &self.0
478 }
479}
480
481pub struct SettingsActivity {
482 actions: Vec<MenuAction<Change>>,
483 menu: Menu,
484}
485
486impl SettingsActivity {
487 fn other_settings_popup(settings: &Settings) -> Activity {
488 Activity::new_base_boxed("settings".to_string(), OtherSettingsPopup::new(settings))
489 }
490}
491
492#[allow(clippy::new_without_default)]
493impl SettingsActivity {
494 pub fn new() -> Self {
495 let options = menu_actions!(
496 "Audio" on "sound" -> data => Change::push(create_audio_settings(data)),
497 "Controls" -> data => Change::push(create_controls_settings(data)),
498 "Other settings" -> data => Change::push(SettingsActivity::other_settings_popup(&data.settings)),
499 "Back" -> _ => Change::pop_top(),
500 );
501
502 let (options, actions) = split_menu_actions(options);
503
504 let menu_config = MenuConfig::new("Settings", options).subtitle("Changes are not saved");
505
506 Self {
507 actions,
508 menu: Menu::new(menu_config),
509 }
510 }
511
512 pub fn new_activity() -> Activity {
513 Activity::new_base_boxed("settings".to_string(), Self::new())
514 }
515}
516
517impl ActivityHandler for SettingsActivity {
518 fn update(&mut self, events: Vec<app::Event>, data: &mut AppData) -> Option<Change> {
519 match self.menu.update(events, data)? {
520 Change::Pop {
521 res: Some(sub_activity),
522 ..
523 } => {
524 let index = *sub_activity
525 .downcast::<usize>()
526 .expect("menu should return index");
527 Some((self.actions[index])(data))
528 }
529 res => Some(res),
530 }
531 }
532
533 fn screen(&self) -> &dyn Screen {
534 &self.menu
535 }
536}
537
538pub fn create_controls_settings(data: &mut AppData) -> Activity {
539 let menu_config = MenuConfig::new(
540 "Controls settings",
541 [
542 MenuItem::Option(OptionDef {
543 text: "Enable mouse input".into(),
544 val: data.settings.get_enable_mouse(),
545 fun: Box::new(|enabled, data| {
546 *enabled = !*enabled;
547 data.settings.set_enable_mouse(*enabled);
548 }),
549 }),
550 MenuItem::Option(OptionDef {
551 text: "Enable dpad".into(),
552 val: data.settings.get_enable_dpad(),
553 fun: Box::new(|enabled, data| {
554 *enabled = !*enabled;
555 data.settings.set_enable_dpad(*enabled);
556 }),
557 }),
558 MenuItem::Option(OptionDef {
559 text: "Left-handed dpad".into(),
560 val: data.settings.get_landscape_dpad_on_left(),
561 fun: Box::new(|is_on_left, data| {
562 *is_on_left = !*is_on_left;
563 data.settings.set_landscape_dpad_on_left(*is_on_left);
564 }),
565 }),
566 MenuItem::Option(OptionDef {
567 text: "Swap Up and Down buttons".into(),
568 val: data.settings.get_dpad_swap_up_down(),
569 fun: Box::new(|do_swap, data| {
570 *do_swap = !*do_swap;
571 data.settings.set_dpad_swap_up_down(*do_swap);
572 }),
573 }),
574 MenuItem::Option(OptionDef {
575 text: "Enable margin around dpad".into(),
576 val: data.settings.get_enable_margin_around_dpad(),
577 fun: Box::new(|enabled, data| {
578 *enabled = !*enabled;
579 data.settings.set_enable_margin_around_dpad(*enabled);
580 }),
581 }),
582 MenuItem::Option(OptionDef {
583 text: "Enable dpad highlight".into(),
584 val: data.settings.get_enable_dpad_highlight(),
585 fun: Box::new(|enabled, data| {
586 *enabled = !*enabled;
587 data.settings.set_enable_dpad_highlight(*enabled);
588 }),
589 }),
590 MenuItem::Separator,
591 MenuItem::Text("Exit".into()),
592 ],
593 );
594
595 Activity::new_base_boxed("controls settings", Menu::new(menu_config))
596}