Skip to main content

cadmus_core/view/settings_editor/kinds/
reader.rs

1//! Setting kinds for the Reader category.
2
3use super::{SettingData, SettingIdentity, SettingKind, WidgetKind};
4use crate::fl;
5use crate::geom::Rectangle;
6use crate::i18n::I18nDisplay;
7use crate::settings::{FileExtension, FinishedAction, RefreshRatePair, Settings};
8use crate::view::{Bus, EntryId, EntryKind, Event, ViewId};
9
10/// Reader finished action setting
11pub struct FinishedActionSetting;
12
13/// File kinds rendered with dithering.
14pub struct DitheredKindsSetting;
15
16impl SettingKind for DitheredKindsSetting {
17    fn identity(&self) -> SettingIdentity {
18        SettingIdentity::DitheredKinds
19    }
20
21    fn label(&self, _settings: &Settings) -> String {
22        fl!("settings-reader-dithered-kinds")
23    }
24
25    fn fetch(&self, settings: &Settings) -> SettingData {
26        let entries = FileExtension::all()
27            .iter()
28            .copied()
29            .map(|kind| {
30                EntryKind::CheckBox(
31                    kind.to_string().to_uppercase(),
32                    EntryId::ToggleDitheredKind(kind),
33                    settings.reader.dithered_kinds.contains(&kind),
34                )
35            })
36            .collect();
37
38        SettingData {
39            value: kinds_summary(settings.reader.dithered_kinds.len()),
40            widget: WidgetKind::SubMenu(entries),
41        }
42    }
43
44    fn handle(
45        &self,
46        evt: &Event,
47        settings: &mut Settings,
48        _bus: &mut Bus,
49    ) -> (Option<String>, bool) {
50        if let Event::Select(EntryId::ToggleDitheredKind(kind)) = evt {
51            if !settings.reader.dithered_kinds.remove(kind) {
52                settings.reader.dithered_kinds.insert(*kind);
53            }
54
55            return (
56                Some(kinds_summary(settings.reader.dithered_kinds.len())),
57                true,
58            );
59        }
60
61        (None, false)
62    }
63
64    fn keep_menu_open(&self) -> bool {
65        true
66    }
67}
68
69fn kinds_summary(selected: usize) -> String {
70    format!("{selected} / {}", FileExtension::all().len())
71}
72
73impl SettingKind for FinishedActionSetting {
74    fn identity(&self) -> SettingIdentity {
75        SettingIdentity::FinishedAction
76    }
77
78    fn label(&self, _settings: &Settings) -> String {
79        fl!("settings-reader-end-of-book-action")
80    }
81
82    fn fetch(&self, settings: &Settings) -> SettingData {
83        let current = settings.reader.finished;
84
85        let entries = vec![
86            EntryKind::RadioButton(
87                FinishedAction::Notify.to_i18n_string(),
88                EntryId::SetFinishedAction(FinishedAction::Notify),
89                current == FinishedAction::Notify,
90            ),
91            EntryKind::RadioButton(
92                FinishedAction::Close.to_i18n_string(),
93                EntryId::SetFinishedAction(FinishedAction::Close),
94                current == FinishedAction::Close,
95            ),
96            EntryKind::RadioButton(
97                FinishedAction::GoToNext.to_i18n_string(),
98                EntryId::SetFinishedAction(FinishedAction::GoToNext),
99                current == FinishedAction::GoToNext,
100            ),
101        ];
102
103        SettingData {
104            value: current.to_i18n_string(),
105            widget: WidgetKind::SubMenu(entries),
106        }
107    }
108
109    fn handle(
110        &self,
111        evt: &Event,
112        settings: &mut Settings,
113        _bus: &mut Bus,
114    ) -> (Option<String>, bool) {
115        if let Event::Select(EntryId::SetFinishedAction(action)) = evt {
116            settings.reader.finished = *action;
117            return (Some(action.to_i18n_string()), true);
118        }
119        (None, false)
120    }
121}
122
123/// Shows global refresh rate and opens the RefreshRateByKindEditor on tap.
124pub struct RefreshRateInfo;
125
126impl SettingKind for RefreshRateInfo {
127    fn identity(&self) -> SettingIdentity {
128        SettingIdentity::RefreshRate
129    }
130
131    fn label(&self, _settings: &Settings) -> String {
132        fl!("settings-reader-refresh-rate")
133    }
134
135    fn fetch(&self, settings: &Settings) -> SettingData {
136        let global = &settings.reader.refresh_rate.global;
137        let value = format!("{} / {}", global.regular, global.inverted);
138
139        SettingData {
140            value,
141            widget: WidgetKind::ActionLabel(Event::OpenRefreshRateEditor),
142        }
143    }
144
145    /// Updates the summary label when either global input is submitted.
146    ///
147    /// Settings are intentionally not written here. Each input (`RefreshRateRegularInput`,
148    /// `RefreshRateInvertedInput`) has its own [`SettingKind`] row (`RefreshRateRegularSetting`,
149    /// `RefreshRateInvertedSetting`) that owns the write.
150    fn handle(
151        &self,
152        evt: &Event,
153        settings: &mut Settings,
154        _bus: &mut Bus,
155    ) -> (Option<String>, bool) {
156        let global = &settings.reader.refresh_rate.global;
157        match evt {
158            Event::Submit(ViewId::RefreshRateRegularInput, text) => {
159                let regular = text.parse::<u8>().unwrap_or(global.regular);
160                (
161                    Some(fl!(
162                        "settings-reader-refresh-rate-summary",
163                        regular = regular,
164                        inverted = global.inverted
165                    )),
166                    false,
167                )
168            }
169            Event::Submit(ViewId::RefreshRateInvertedInput, text) => {
170                let inverted = text.parse::<u8>().unwrap_or(global.inverted);
171                (
172                    Some(fl!(
173                        "settings-reader-refresh-rate-summary",
174                        regular = global.regular,
175                        inverted = inverted
176                    )),
177                    false,
178                )
179            }
180            _ => (None, false),
181        }
182    }
183}
184
185/// Shows the refresh rate pair for a specific file extension.
186pub struct RefreshRateByKindInfo(pub FileExtension);
187
188impl SettingKind for RefreshRateByKindInfo {
189    fn identity(&self) -> SettingIdentity {
190        SettingIdentity::RefreshRateByKind(self.0.as_str().to_string())
191    }
192
193    fn label(&self, _settings: &Settings) -> String {
194        self.0.to_string().to_uppercase()
195    }
196
197    fn fetch(&self, settings: &Settings) -> SettingData {
198        let pair = settings
199            .reader
200            .refresh_rate
201            .by_kind
202            .get(self.0.as_str())
203            .cloned()
204            .unwrap_or(RefreshRatePair {
205                regular: 0,
206                inverted: 0,
207            });
208
209        let value = format!("{} / {}", pair.regular, pair.inverted);
210
211        SettingData {
212            value,
213            widget: WidgetKind::ActionLabel(Event::Select(EntryId::EditRefreshRateByKind(self.0))),
214        }
215    }
216
217    fn hold_event(&self, rect: Rectangle) -> Option<Event> {
218        let entries = vec![EntryKind::Command(
219            fl!("delete"),
220            EntryId::DeleteRefreshRateByKind(self.0),
221        )];
222
223        Some(Event::SubMenu(rect, entries))
224    }
225}
226
227/// The "regular" field of the global refresh rate pair.
228pub struct RefreshRateRegularSetting;
229
230impl SettingKind for RefreshRateRegularSetting {
231    fn identity(&self) -> SettingIdentity {
232        SettingIdentity::RefreshRateRegular
233    }
234
235    fn label(&self, _settings: &Settings) -> String {
236        fl!("settings-reader-refresh-rate-regular")
237    }
238
239    fn fetch(&self, settings: &Settings) -> SettingData {
240        let value = settings.reader.refresh_rate.global.regular.to_string();
241
242        SettingData {
243            value,
244            widget: WidgetKind::ActionLabel(Event::OpenNamedInput {
245                view_id: crate::view::ViewId::RefreshRateRegularInput,
246                label: fl!("settings-reader-refresh-rate-regular-input"),
247                max_chars: 3,
248                initial_text: settings.reader.refresh_rate.global.regular.to_string(),
249            }),
250        }
251    }
252
253    fn handle(
254        &self,
255        evt: &Event,
256        settings: &mut Settings,
257        _bus: &mut Bus,
258    ) -> (Option<String>, bool) {
259        if let Event::Submit(crate::view::ViewId::RefreshRateRegularInput, text) = evt
260            && let Ok(v) = text.parse::<u8>()
261        {
262            settings.reader.refresh_rate.global.regular = v;
263            return (Some(v.to_string()), true);
264        }
265
266        (None, false)
267    }
268}
269
270/// The "inverted" field of the global refresh rate pair.
271pub struct RefreshRateInvertedSetting;
272
273impl SettingKind for RefreshRateInvertedSetting {
274    fn identity(&self) -> SettingIdentity {
275        SettingIdentity::RefreshRateInverted
276    }
277
278    fn label(&self, _settings: &Settings) -> String {
279        fl!("settings-reader-refresh-rate-inverted")
280    }
281
282    fn fetch(&self, settings: &Settings) -> SettingData {
283        let value = settings.reader.refresh_rate.global.inverted.to_string();
284
285        SettingData {
286            value,
287            widget: WidgetKind::ActionLabel(Event::OpenNamedInput {
288                view_id: crate::view::ViewId::RefreshRateInvertedInput,
289                label: fl!("settings-reader-refresh-rate-inverted-input"),
290                max_chars: 3,
291                initial_text: settings.reader.refresh_rate.global.inverted.to_string(),
292            }),
293        }
294    }
295
296    fn handle(
297        &self,
298        evt: &Event,
299        settings: &mut Settings,
300        _bus: &mut Bus,
301    ) -> (Option<String>, bool) {
302        if let Event::Submit(crate::view::ViewId::RefreshRateInvertedInput, text) = evt
303            && let Ok(v) = text.parse::<u8>()
304        {
305            settings.reader.refresh_rate.global.inverted = v;
306            return (Some(v.to_string()), true);
307        }
308
309        (None, false)
310    }
311}
312
313/// The "regular" field of a per-kind refresh rate pair.
314pub struct RefreshRateByKindRegular(pub FileExtension);
315
316impl SettingKind for RefreshRateByKindRegular {
317    fn identity(&self) -> SettingIdentity {
318        SettingIdentity::RefreshRateByKindRegular(self.0.as_str().to_string())
319    }
320
321    fn label(&self, _settings: &Settings) -> String {
322        fl!("settings-reader-refresh-rate-regular")
323    }
324
325    fn fetch(&self, settings: &Settings) -> SettingData {
326        let regular = settings
327            .reader
328            .refresh_rate
329            .by_kind
330            .get(self.0.as_str())
331            .map(|p| p.regular)
332            .unwrap_or(0);
333
334        SettingData {
335            value: regular.to_string(),
336            widget: WidgetKind::ActionLabel(Event::OpenNamedInput {
337                view_id: crate::view::ViewId::RefreshRateByKindRegularInput,
338                label: fl!(
339                    "settings-reader-refresh-rate-by-kind-regular-input",
340                    ext = self.0.as_str()
341                ),
342                max_chars: 3,
343                initial_text: regular.to_string(),
344            }),
345        }
346    }
347
348    fn handle(
349        &self,
350        evt: &Event,
351        settings: &mut Settings,
352        _bus: &mut Bus,
353    ) -> (Option<String>, bool) {
354        if let Event::Submit(crate::view::ViewId::RefreshRateByKindRegularInput, text) = evt
355            && let Ok(v) = text.parse::<u8>()
356        {
357            let pair = settings
358                .reader
359                .refresh_rate
360                .by_kind
361                .entry(self.0.as_str().to_string())
362                .or_insert(RefreshRatePair {
363                    regular: 0,
364                    inverted: 0,
365                });
366            pair.regular = v;
367            return (Some(v.to_string()), true);
368        }
369
370        (None, false)
371    }
372}
373
374/// The "inverted" field of a per-kind refresh rate pair.
375pub struct RefreshRateByKindInverted(pub FileExtension);
376
377impl SettingKind for RefreshRateByKindInverted {
378    fn identity(&self) -> SettingIdentity {
379        SettingIdentity::RefreshRateByKindInverted(self.0.as_str().to_string())
380    }
381
382    fn label(&self, _settings: &Settings) -> String {
383        fl!("settings-reader-refresh-rate-inverted")
384    }
385
386    fn fetch(&self, settings: &Settings) -> SettingData {
387        let inverted = settings
388            .reader
389            .refresh_rate
390            .by_kind
391            .get(self.0.as_str())
392            .map(|p| p.inverted)
393            .unwrap_or(0);
394
395        SettingData {
396            value: inverted.to_string(),
397            widget: WidgetKind::ActionLabel(Event::OpenNamedInput {
398                view_id: crate::view::ViewId::RefreshRateByKindInvertedInput,
399                label: fl!(
400                    "settings-reader-refresh-rate-by-kind-inverted-input",
401                    ext = self.0.as_str()
402                ),
403                max_chars: 3,
404                initial_text: inverted.to_string(),
405            }),
406        }
407    }
408
409    fn handle(
410        &self,
411        evt: &Event,
412        settings: &mut Settings,
413        _bus: &mut Bus,
414    ) -> (Option<String>, bool) {
415        if let Event::Submit(crate::view::ViewId::RefreshRateByKindInvertedInput, text) = evt
416            && let Ok(v) = text.parse::<u8>()
417        {
418            let pair = settings
419                .reader
420                .refresh_rate
421                .by_kind
422                .entry(self.0.as_str().to_string())
423                .or_insert(RefreshRatePair {
424                    regular: 0,
425                    inverted: 0,
426                });
427            pair.inverted = v;
428            return (Some(v.to_string()), true);
429        }
430
431        (None, false)
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::settings::{FileExtension, FinishedAction, Settings};
439    use crate::view::{Bus, EntryId, Event};
440    use std::collections::VecDeque;
441
442    mod finished_action_setting {
443        use super::*;
444
445        #[test]
446        fn handle_set_action_updates_settings() {
447            let setting = FinishedActionSetting;
448            let mut settings = Settings::default();
449            settings.reader.finished = FinishedAction::Close;
450            let mut bus: Bus = VecDeque::new();
451            let event = Event::Select(EntryId::SetFinishedAction(FinishedAction::GoToNext));
452
453            let result = setting.handle(&event, &mut settings, &mut bus);
454
455            assert!(result.0.is_some());
456            assert_eq!(settings.reader.finished, FinishedAction::GoToNext);
457        }
458
459        #[test]
460        fn handle_can_set_all_actions() {
461            let setting = FinishedActionSetting;
462            let mut settings = Settings::default();
463            let mut bus: Bus = VecDeque::new();
464
465            for action in [
466                FinishedAction::Notify,
467                FinishedAction::Close,
468                FinishedAction::GoToNext,
469            ] {
470                let event = Event::Select(EntryId::SetFinishedAction(action));
471                setting.handle(&event, &mut settings, &mut bus);
472                assert_eq!(settings.reader.finished, action);
473            }
474        }
475
476        mod dithered_kinds_setting {
477            use super::*;
478
479            #[test]
480            fn fetch_builds_checkbox_submenu_for_all_extensions() {
481                let setting = DitheredKindsSetting;
482                let settings = Settings::default();
483
484                let data = setting.fetch(&settings);
485
486                assert_eq!(
487                    data.value,
488                    kinds_summary(settings.reader.dithered_kinds.len())
489                );
490                let WidgetKind::SubMenu(entries) = data.widget else {
491                    panic!("expected submenu widget");
492                };
493                assert_eq!(entries.len(), FileExtension::all().len());
494                assert!(matches!(
495                    entries.first(),
496                    Some(EntryKind::CheckBox(_, EntryId::ToggleDitheredKind(_), _))
497                ));
498            }
499
500            #[test]
501            fn handle_toggle_adds_and_removes_extensions() {
502                let setting = DitheredKindsSetting;
503                let mut settings = Settings::default();
504                settings.reader.dithered_kinds.remove(&FileExtension::Pdf);
505                let mut bus: Bus = VecDeque::new();
506
507                let add = setting.handle(
508                    &Event::Select(EntryId::ToggleDitheredKind(FileExtension::Pdf)),
509                    &mut settings,
510                    &mut bus,
511                );
512                assert_eq!(
513                    add.0,
514                    Some(kinds_summary(settings.reader.dithered_kinds.len()))
515                );
516                assert!(settings.reader.dithered_kinds.contains(&FileExtension::Pdf));
517
518                let remove = setting.handle(
519                    &Event::Select(EntryId::ToggleDitheredKind(FileExtension::Pdf)),
520                    &mut settings,
521                    &mut bus,
522                );
523                assert_eq!(
524                    remove.0,
525                    Some(kinds_summary(settings.reader.dithered_kinds.len()))
526                );
527                assert!(!settings.reader.dithered_kinds.contains(&FileExtension::Pdf));
528            }
529        }
530
531        #[test]
532        fn handle_returns_none_for_wrong_event() {
533            let setting = FinishedActionSetting;
534            let mut settings = Settings::default();
535            let mut bus: Bus = VecDeque::new();
536
537            let result = setting.handle(&Event::Select(EntryId::About), &mut settings, &mut bus);
538
539            assert!(result.0.is_none());
540        }
541
542        #[test]
543        fn handle_returns_none_for_per_library_entry_id() {
544            let setting = FinishedActionSetting;
545            let mut settings = Settings::default();
546            let mut bus: Bus = VecDeque::new();
547            let event = Event::Select(EntryId::SetLibraryFinishedAction(0, FinishedAction::Notify));
548
549            let result = setting.handle(&event, &mut settings, &mut bus);
550
551            assert!(result.0.is_none());
552        }
553    }
554
555    mod refresh_rate_info {
556        use super::*;
557
558        #[test]
559        fn handle_regular_submit_updates_display_without_writing_settings() {
560            let setting = RefreshRateInfo;
561            let mut settings = Settings::default();
562            settings.reader.refresh_rate.global.regular = 5;
563            settings.reader.refresh_rate.global.inverted = 10;
564            let mut bus: Bus = VecDeque::new();
565
566            let event = Event::Submit(ViewId::RefreshRateRegularInput, "3".to_string());
567            let (display, handled) = setting.handle(&event, &mut settings, &mut bus);
568
569            assert_eq!(
570                display.as_deref(),
571                Some(
572                    fl!(
573                        "settings-reader-refresh-rate-summary",
574                        regular = 3u8,
575                        inverted = 10u8
576                    )
577                    .as_str()
578                )
579            );
580            assert!(!handled);
581            assert_eq!(settings.reader.refresh_rate.global.regular, 5);
582        }
583
584        #[test]
585        fn handle_inverted_submit_updates_display_without_writing_settings() {
586            let setting = RefreshRateInfo;
587            let mut settings = Settings::default();
588            settings.reader.refresh_rate.global.regular = 5;
589            settings.reader.refresh_rate.global.inverted = 10;
590            let mut bus: Bus = VecDeque::new();
591
592            let event = Event::Submit(ViewId::RefreshRateInvertedInput, "7".to_string());
593            let (display, handled) = setting.handle(&event, &mut settings, &mut bus);
594
595            assert_eq!(
596                display.as_deref(),
597                Some(
598                    fl!(
599                        "settings-reader-refresh-rate-summary",
600                        regular = 5u8,
601                        inverted = 7u8
602                    )
603                    .as_str()
604                )
605            );
606            assert!(!handled);
607            assert_eq!(settings.reader.refresh_rate.global.inverted, 10);
608        }
609
610        #[test]
611        fn handle_invalid_text_falls_back_to_current_value() {
612            let setting = RefreshRateInfo;
613            let mut settings = Settings::default();
614            settings.reader.refresh_rate.global.regular = 5;
615            settings.reader.refresh_rate.global.inverted = 10;
616            let mut bus: Bus = VecDeque::new();
617
618            let event = Event::Submit(ViewId::RefreshRateRegularInput, "bad".to_string());
619            let (display, _) = setting.handle(&event, &mut settings, &mut bus);
620
621            assert_eq!(
622                display.as_deref(),
623                Some(
624                    fl!(
625                        "settings-reader-refresh-rate-summary",
626                        regular = 5u8,
627                        inverted = 10u8
628                    )
629                    .as_str()
630                )
631            );
632        }
633
634        #[test]
635        fn handle_unrelated_event_returns_none() {
636            let setting = RefreshRateInfo;
637            let mut settings = Settings::default();
638            let mut bus: Bus = VecDeque::new();
639
640            let (display, handled) =
641                setting.handle(&Event::Select(EntryId::About), &mut settings, &mut bus);
642
643            assert!(display.is_none());
644            assert!(!handled);
645        }
646    }
647}