tmaze/settings/
mod.rs

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    // TODO: make `serde(flatten)` once switched to TOML/JSON
50    #[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")]
100// FIXME: separate sections into their own struct
101pub struct SettingsInner {
102    // general
103    #[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    // viewport
113    #[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    // navigation
127    #[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    // update check
141    #[serde(default)]
142    pub update_check_interval: Option<UpdateCheckInterval>,
143    #[serde(default)]
144    pub display_update_check_errors: Option<bool>,
145
146    // audio
147    #[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    // presets
157    #[serde(default)]
158    pub presets: Option<Vec<MazePreset>>,
159    // TODO: it's not possible in RON to have a HashMap with flattened keys,
160    // so we will support it in different way formats
161    // once we support them - this would mean dropping RON support
162    // https://github.com/ron-rs/ron/issues/115
163    // pub unknown_fields: HashMap<String, Value>,
164}
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
416// JSON
417impl 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}