cadmus_core/view/settings_editor/
setting_value.rs

1use super::super::action_label::ActionLabel;
2use super::super::EntryKind;
3use super::super::{Align, Bus, Event, Hub, Id, RenderQueue, View, ID_FEEDER};
4use crate::context::Context;
5use crate::framebuffer::Framebuffer;
6use crate::geom::Rectangle;
7use crate::settings::{ButtonScheme, IntermKind, Settings};
8use crate::view::toggle::Toggle;
9use crate::view::{EntryId, ToggleEvent};
10use anyhow::Error;
11use std::fs;
12use std::path::Path;
13
14#[derive(Debug, Clone)]
15pub enum ToggleSettings {
16    /// Sleep cover enable/disable setting
17    SleepCover,
18    /// Auto-share enable/disable setting
19    AutoShare,
20    /// Button scheme selection (natural or inverted)
21    ButtonScheme,
22}
23
24/// Represents the type of setting value being displayed.
25///
26/// This enum categorizes different settings that can be configured in the application,
27/// including keyboard layout, power management, button schemes, and library settings.
28#[derive(Debug, Clone)]
29pub enum Kind {
30    /// Keyboard layout selection setting
31    KeyboardLayout,
32    /// Auto-suspend timeout setting (in minutes)
33    AutoSuspend,
34    /// Auto power-off timeout setting (in minutes)
35    AutoPowerOff,
36
37    /// Generic toggle setting
38    Toggle(ToggleSettings),
39
40    /// Library info display for the library at the given index
41    LibraryInfo(usize),
42    /// Library name setting for the library at the given index
43    LibraryName(usize),
44    /// Library path setting for the library at the given index
45    LibraryPath(usize),
46    /// Library mode setting (database or filesystem) for the library at the given index
47    LibraryMode(usize),
48    /// Intermission display setting for suspend screen
49    IntermissionSuspend,
50    /// Intermission display setting for power-off screen
51    IntermissionPowerOff,
52    /// Intermission display setting for share screen
53    IntermissionShare,
54    /// Settings retention setting (how many old versions to keep)
55    SettingsRetention,
56}
57
58impl Kind {
59    pub fn matches_interm_kind(&self, interm_kind: &IntermKind) -> bool {
60        matches!(
61            (self, interm_kind),
62            (Kind::IntermissionSuspend, IntermKind::Suspend)
63                | (Kind::IntermissionPowerOff, IntermKind::PowerOff)
64                | (Kind::IntermissionShare, IntermKind::Share)
65        )
66    }
67}
68
69/// Represents a single setting value display in the settings UI.
70///
71/// This struct manages the display and interaction of a setting value, including
72/// the current value, available options (entries), and associated UI components.
73/// It acts as a View that can be rendered and handle events related to setting changes.
74#[derive(Debug)]
75pub struct SettingValue {
76    /// Unique identifier for this setting value view
77    id: Id,
78    /// The type of setting this value represents
79    kind: Kind,
80    /// The rectangular area occupied by this view
81    rect: Rectangle,
82    /// Child views, typically containing an ActionLabel for display
83    children: Vec<Box<dyn View>>,
84    /// Available options/entries for this setting (e.g., radio buttons, checkboxes)
85    ///
86    /// # Important
87    /// Whenever this field is modified, the underlying ActionLabel's event must be updated
88    /// by calling `create_tap_event()` and setting it via `action_label.set_event()`.
89    /// This ensures the tap behavior reflects the current entries state.
90    entries: Vec<EntryKind>,
91}
92
93impl SettingValue {
94    pub fn new(
95        kind: Kind,
96        rect: Rectangle,
97        settings: &Settings,
98        fonts: &mut crate::font::Fonts,
99    ) -> SettingValue {
100        let (value, entries, enabled_toggle) = Self::fetch_data_for_kind(&kind, settings);
101
102        let mut setting_value = SettingValue {
103            id: ID_FEEDER.next(),
104            kind,
105            rect,
106            children: vec![],
107            entries,
108        };
109
110        setting_value.children =
111            vec![setting_value.kind_to_child_view(value, enabled_toggle, fonts)];
112
113        setting_value
114    }
115
116    fn kind_to_child_view(
117        &self,
118        value: String,
119        enabled_toggle: Option<bool>,
120        fonts: &mut crate::font::Fonts,
121    ) -> Box<dyn View> {
122        let event = self.create_tap_event();
123
124        match self.kind {
125            Kind::Toggle(ref toggle) => match toggle {
126                ToggleSettings::AutoShare => Box::new(Toggle::new(
127                    self.rect,
128                    "on",
129                    "off",
130                    enabled_toggle.expect("enabled bool should be Some for toggle settings"),
131                    event.expect("Event should not be None for toggle"),
132                    fonts,
133                    Align::Right(10),
134                )),
135                ToggleSettings::ButtonScheme => Box::new(Toggle::new(
136                    self.rect,
137                    ButtonScheme::Natural.to_string().as_str(),
138                    ButtonScheme::Inverted.to_string().as_str(),
139                    enabled_toggle.expect("enabled bool should be Some for toggle settings"),
140                    event.expect("Event should not be None for toggle"),
141                    fonts,
142                    Align::Right(10),
143                )),
144                ToggleSettings::SleepCover => Box::new(Toggle::new(
145                    self.rect,
146                    "on",
147                    "off",
148                    enabled_toggle.expect("enabled bool should be Some for toggle settings"),
149                    event.expect("Event should not be None for toggle"),
150                    fonts,
151                    Align::Right(10),
152                )),
153            },
154            _ => Box::new(ActionLabel::new(self.rect, value, Align::Right(10)).event(event)),
155        }
156    }
157
158    /// Refreshes the displayed value by re-reading from context.settings.
159    ///
160    /// This method updates the ActionLabel text to reflect the current state of the setting
161    /// in context.settings. It should be called whenever the underlying setting changes.
162    pub fn refresh_from_context(&mut self, context: &Context, rq: &mut RenderQueue) {
163        let (value, entries, _enabled_toggle) =
164            Self::fetch_data_for_kind(&self.kind, &context.settings);
165        self.entries = entries;
166        let event = self.create_tap_event();
167
168        if let Some(action_label) = self.children.get_mut(0) {
169            if let Some(label) = action_label.as_any_mut().downcast_mut::<ActionLabel>() {
170                label.update(&value, rq);
171                label.set_event(event);
172            }
173        }
174    }
175
176    fn fetch_data_for_kind(
177        kind: &Kind,
178        settings: &Settings,
179    ) -> (String, Vec<EntryKind>, Option<bool>) {
180        match kind {
181            Kind::KeyboardLayout => Self::fetch_keyboard_layout_data(settings),
182            Kind::AutoSuspend => Self::fetch_auto_suspend_data(settings),
183            Kind::AutoPowerOff => Self::fetch_auto_power_off_data(settings),
184            Kind::LibraryInfo(index) => Self::fetch_library_info_data(*index, settings),
185            Kind::LibraryName(index) => Self::fetch_library_name_data(*index, settings),
186            Kind::LibraryPath(index) => Self::fetch_library_path_data(*index, settings),
187            Kind::LibraryMode(index) => Self::fetch_library_mode_data(*index, settings),
188            Kind::IntermissionSuspend => {
189                Self::fetch_intermission_data(crate::settings::IntermKind::Suspend, settings)
190            }
191            Kind::IntermissionPowerOff => {
192                Self::fetch_intermission_data(crate::settings::IntermKind::PowerOff, settings)
193            }
194            Kind::IntermissionShare => {
195                Self::fetch_intermission_data(crate::settings::IntermKind::Share, settings)
196            }
197            Kind::SettingsRetention => Self::fetch_settings_retention_data(settings),
198            Kind::Toggle(toggle) => match toggle {
199                ToggleSettings::SleepCover => Self::fetch_sleep_cover_data(settings),
200                ToggleSettings::AutoShare => Self::fetch_auto_share_data(settings),
201                ToggleSettings::ButtonScheme => Self::fetch_button_scheme_data(settings),
202            },
203        }
204    }
205
206    fn fetch_keyboard_layout_data(settings: &Settings) -> (String, Vec<EntryKind>, Option<bool>) {
207        let current_layout = settings.keyboard_layout.clone();
208        let available_layouts = Self::get_available_layouts().unwrap_or_default();
209
210        let entries: Vec<EntryKind> = available_layouts
211            .iter()
212            .map(|layout| {
213                EntryKind::RadioButton(
214                    layout.clone(),
215                    EntryId::SetKeyboardLayout(layout.clone()),
216                    current_layout == *layout,
217                )
218            })
219            .collect();
220
221        (current_layout, entries, None)
222    }
223
224    fn fetch_sleep_cover_data(settings: &Settings) -> (String, Vec<EntryKind>, Option<bool>) {
225        let enabled = settings.sleep_cover;
226        let value = if enabled {
227            "Enabled".to_string()
228        } else {
229            "Disabled".to_string()
230        };
231
232        (value, vec![], Some(settings.sleep_cover))
233    }
234
235    fn fetch_auto_share_data(settings: &Settings) -> (String, Vec<EntryKind>, Option<bool>) {
236        let enabled = settings.auto_share;
237        let value = if enabled {
238            "Enabled".to_string()
239        } else {
240            "Disabled".to_string()
241        };
242
243        (value, vec![], Some(settings.auto_share))
244    }
245
246    fn fetch_button_scheme_data(settings: &Settings) -> (String, Vec<EntryKind>, Option<bool>) {
247        let current_scheme = settings.button_scheme;
248        let value = format!("{:?}", current_scheme);
249
250        (
251            value,
252            vec![],
253            Some(settings.button_scheme == ButtonScheme::Natural),
254        )
255    }
256
257    fn fetch_auto_suspend_data(settings: &Settings) -> (String, Vec<EntryKind>, Option<bool>) {
258        let value = if settings.auto_suspend == 0.0 {
259            "Never".to_string()
260        } else {
261            format!("{:.1}", settings.auto_suspend)
262        };
263
264        (value, vec![], None)
265    }
266
267    fn fetch_auto_power_off_data(settings: &Settings) -> (String, Vec<EntryKind>, Option<bool>) {
268        let value = if settings.auto_power_off == 0.0 {
269            "Never".to_string()
270        } else {
271            format!("{:.1}", settings.auto_power_off)
272        };
273
274        (value, vec![], None)
275    }
276
277    #[inline]
278    fn fetch_settings_retention_data(
279        settings: &Settings,
280    ) -> (String, Vec<EntryKind>, Option<bool>) {
281        let value = settings.settings_retention.to_string();
282
283        (value, vec![], None)
284    }
285
286    fn fetch_library_info_data(
287        index: usize,
288        settings: &Settings,
289    ) -> (String, Vec<EntryKind>, Option<bool>) {
290        if let Some(library) = settings.libraries.get(index) {
291            let value = library.path.display().to_string();
292
293            (value, vec![], None)
294        } else {
295            ("Unknown".to_string(), vec![], None)
296        }
297    }
298
299    fn fetch_library_name_data(
300        index: usize,
301        settings: &Settings,
302    ) -> (String, Vec<EntryKind>, Option<bool>) {
303        if let Some(library) = settings.libraries.get(index) {
304            (library.name.clone(), vec![], None)
305        } else {
306            ("Unknown".to_string(), vec![], None)
307        }
308    }
309
310    fn fetch_library_path_data(
311        index: usize,
312        settings: &Settings,
313    ) -> (String, Vec<EntryKind>, Option<bool>) {
314        if let Some(library) = settings.libraries.get(index) {
315            (library.path.display().to_string(), vec![], None)
316        } else {
317            ("Unknown".to_string(), vec![], None)
318        }
319    }
320
321    fn fetch_library_mode_data(
322        index: usize,
323        settings: &Settings,
324    ) -> (String, Vec<EntryKind>, Option<bool>) {
325        use crate::settings::LibraryMode;
326        let mut mode = LibraryMode::Filesystem;
327
328        if let Some(library) = settings.libraries.get(index) {
329            mode = library.mode;
330        }
331
332        let entries = vec![
333            EntryKind::RadioButton(
334                LibraryMode::Database.to_string(),
335                EntryId::SetLibraryMode(LibraryMode::Database),
336                mode == LibraryMode::Database,
337            ),
338            EntryKind::RadioButton(
339                LibraryMode::Filesystem.to_string(),
340                EntryId::SetLibraryMode(LibraryMode::Filesystem),
341                mode == LibraryMode::Filesystem,
342            ),
343        ];
344        (mode.to_string(), entries, None)
345    }
346
347    fn get_available_layouts() -> Result<Vec<String>, Error> {
348        let layouts_dir = Path::new("keyboard-layouts");
349        let mut layouts = Vec::new();
350
351        if layouts_dir.exists() {
352            for entry in fs::read_dir(layouts_dir)? {
353                let entry = entry?;
354                let path = entry.path();
355
356                if path.extension().and_then(|s| s.to_str()) == Some("json") {
357                    if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
358                        let layout_name = stem
359                            .chars()
360                            .enumerate()
361                            .map(|(i, c)| {
362                                if i == 0 {
363                                    c.to_uppercase().collect::<String>()
364                                } else {
365                                    c.to_string()
366                                }
367                            })
368                            .collect::<String>();
369                        layouts.push(layout_name);
370                    }
371                }
372            }
373        }
374
375        layouts.sort();
376        Ok(layouts)
377    }
378
379    fn fetch_intermission_data(
380        kind: IntermKind,
381        settings: &Settings,
382    ) -> (String, Vec<EntryKind>, Option<bool>) {
383        use crate::settings::IntermissionDisplay;
384
385        let display = &settings.intermissions[kind];
386
387        let (value, is_logo, is_cover) = match display {
388            IntermissionDisplay::Logo => ("Logo".to_string(), true, false),
389            IntermissionDisplay::Cover => ("Cover".to_string(), false, true),
390            IntermissionDisplay::Image(path) => {
391                let display_name = path
392                    .file_name()
393                    .and_then(|n| n.to_str())
394                    .unwrap_or("Custom")
395                    .to_string();
396                (display_name, false, false)
397            }
398        };
399
400        let entries = vec![
401            EntryKind::RadioButton(
402                "Logo".to_string(),
403                EntryId::SetIntermission(kind, IntermissionDisplay::Logo),
404                is_logo,
405            ),
406            EntryKind::RadioButton(
407                "Cover".to_string(),
408                EntryId::SetIntermission(kind, IntermissionDisplay::Cover),
409                is_cover,
410            ),
411            EntryKind::Command(
412                "Custom Image...".to_string(),
413                EntryId::EditIntermissionImage(kind),
414            ),
415        ];
416
417        (value, entries, None)
418    }
419
420    pub fn update(&mut self, value: String, rq: &mut RenderQueue) {
421        if let Some(action_label) = self.children[0].downcast_mut::<ActionLabel>() {
422            action_label.update(&value, rq);
423        }
424    }
425
426    pub fn value(&self) -> String {
427        if let Some(action_label) = self.children[0].downcast_ref::<ActionLabel>() {
428            action_label.value()
429        } else {
430            String::new()
431        }
432    }
433
434    /// Generates the appropriate event to be triggered when this setting value is tapped.
435    ///
436    /// This method determines what event should be emitted based on the type of setting.
437    /// It's used during initialization (in `new()`) and after updates (in various `handle_*` methods)
438    /// to ensure the ActionLabel always has the correct tap behavior.
439    ///
440    /// The behavior varies by setting type:
441    /// - **Direct edit settings** (LibraryInfo, LibraryName, LibraryPath, AutoSuspend, AutoPowerOff):
442    ///   Return specific edit events that trigger their corresponding input dialogs.
443    /// - **Settings with multiple options** (KeyboardLayout, SleepCover, AutoShare, ButtonScheme, LibraryMode, Intermission*):
444    ///   Return a SubMenu event that displays all available entries as radio buttons or checkboxes.
445    ///
446    /// # Returns
447    /// An Option containing:
448    /// - `Some(Event)` - the event to emit on tap
449    /// - `None`
450    ///
451    /// # Important
452    /// **This method must be called every time `self.entries` is updated** to ensure the tap event
453    /// reflects the current state of available entries.
454    fn create_tap_event(&self) -> Option<Event> {
455        match self.kind {
456            Kind::LibraryInfo(index) => Some(Event::EditLibrary(index)),
457            Kind::LibraryName(_) => Some(Event::Select(EntryId::EditLibraryName)),
458            Kind::LibraryPath(_) => Some(Event::Select(EntryId::EditLibraryPath)),
459            Kind::AutoSuspend => Some(Event::Select(EntryId::EditAutoSuspend)),
460            Kind::AutoPowerOff => Some(Event::Select(EntryId::EditAutoPowerOff)),
461            Kind::SettingsRetention => Some(Event::Select(EntryId::EditSettingsRetention)),
462            Kind::Toggle(ref toggle) => {
463                Some(Event::NewToggle(ToggleEvent::Setting(toggle.clone())))
464            }
465            _ if !self.entries.is_empty() => Some(Event::SubMenu(self.rect, self.entries.clone())),
466            _ => None,
467        }
468    }
469}
470
471impl View for SettingValue {
472    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, _bus, _rq, _context), fields(event = ?_evt), ret(level=tracing::Level::TRACE)))]
473    fn handle_event(
474        &mut self,
475        _evt: &Event,
476        _hub: &Hub,
477        _bus: &mut Bus,
478        _rq: &mut RenderQueue,
479        _context: &mut Context,
480    ) -> bool {
481        false
482    }
483
484    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
485    fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut crate::font::Fonts) {
486    }
487
488    fn rect(&self) -> &Rectangle {
489        &self.rect
490    }
491
492    fn rect_mut(&mut self) -> &mut Rectangle {
493        &mut self.rect
494    }
495
496    fn children(&self) -> &Vec<Box<dyn View>> {
497        &self.children
498    }
499
500    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
501        &mut self.children
502    }
503
504    fn id(&self) -> Id {
505        self.id
506    }
507}
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512    use crate::context::test_helpers::create_test_context;
513    use crate::gesture::GestureEvent;
514    use crate::settings::Settings;
515    use crate::view::RenderQueue;
516    use std::collections::VecDeque;
517    use std::path::PathBuf;
518    use std::sync::mpsc::channel;
519
520    #[test]
521    fn test_file_chooser_closed_updates_all_intermission_values() {
522        let mut context = create_test_context();
523        let settings = Settings::default();
524        let rect = rect![0, 0, 200, 50];
525
526        let mut suspend_value = SettingValue::new(
527            Kind::IntermissionSuspend,
528            rect,
529            &settings,
530            &mut context.fonts,
531        );
532        let mut power_off_value = SettingValue::new(
533            Kind::IntermissionPowerOff,
534            rect,
535            &settings,
536            &mut context.fonts,
537        );
538        let mut share_value =
539            SettingValue::new(Kind::IntermissionShare, rect, &settings, &mut context.fonts);
540
541        let (hub, _receiver) = channel();
542        let mut bus = VecDeque::new();
543        let mut rq = RenderQueue::new();
544
545        let initial_suspend = suspend_value.value().clone();
546        let initial_power_off = power_off_value.value().clone();
547        let initial_share = share_value.value().clone();
548
549        let test_path = PathBuf::from("/mnt/onboard/test_image.png");
550        let event = Event::FileChooserClosed(Some(test_path.clone()));
551
552        suspend_value.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
553        power_off_value.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
554        share_value.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
555
556        println!("Initial suspend value: {}", initial_suspend);
557        println!("After event suspend value: {}", suspend_value.value());
558        println!("Initial power_off value: {}", initial_power_off);
559        println!("After event power_off value: {}", power_off_value.value());
560        println!("Initial share value: {}", initial_share);
561        println!("After event share value: {}", share_value.value());
562
563        assert_eq!(suspend_value.value(), initial_suspend);
564        assert_eq!(power_off_value.value(), initial_power_off);
565        assert_eq!(share_value.value(), initial_share);
566    }
567
568    #[test]
569    fn test_intermission_values_update_via_submit_event() {
570        use crate::settings::IntermKind;
571        let mut context = create_test_context();
572        let settings = Settings::default();
573        let rect = rect![0, 0, 200, 50];
574
575        let mut suspend_value = SettingValue::new(
576            Kind::IntermissionSuspend,
577            rect,
578            &settings,
579            &mut context.fonts,
580        );
581        let mut power_off_value = SettingValue::new(
582            Kind::IntermissionPowerOff,
583            rect,
584            &settings,
585            &mut context.fonts,
586        );
587        let mut share_value =
588            SettingValue::new(Kind::IntermissionShare, rect, &settings, &mut context.fonts);
589
590        let mut rq = RenderQueue::new();
591
592        context.settings.intermissions[IntermKind::Suspend] =
593            crate::settings::IntermissionDisplay::Image(PathBuf::from("suspend_image.png"));
594        context.settings.intermissions[IntermKind::PowerOff] =
595            crate::settings::IntermissionDisplay::Image(PathBuf::from("poweroff_image.png"));
596        context.settings.intermissions[IntermKind::Share] =
597            crate::settings::IntermissionDisplay::Image(PathBuf::from("share_image.png"));
598
599        suspend_value.refresh_from_context(&context, &mut rq);
600        power_off_value.refresh_from_context(&context, &mut rq);
601        share_value.refresh_from_context(&context, &mut rq);
602
603        assert_eq!(suspend_value.value(), "suspend_image.png");
604        assert_eq!(power_off_value.value(), "poweroff_image.png");
605        assert_eq!(share_value.value(), "share_image.png");
606    }
607
608    #[test]
609    fn test_keyboard_layout_select_updates_value() {
610        let mut context = create_test_context();
611        let settings = Settings {
612            keyboard_layout: "English".to_string(),
613            ..Default::default()
614        };
615        let rect = rect![0, 0, 200, 50];
616
617        let mut value =
618            SettingValue::new(Kind::KeyboardLayout, rect, &settings, &mut context.fonts);
619        let mut rq = RenderQueue::new();
620
621        context.settings.keyboard_layout = "French".to_string();
622        value.refresh_from_context(&context, &mut rq);
623
624        assert_eq!(value.value(), "French");
625        assert!(!rq.is_empty());
626    }
627
628    #[test]
629    fn test_library_mode_select_updates_value() {
630        use crate::settings::{LibraryMode, LibrarySettings};
631        let mut settings = Settings::default();
632        settings.libraries.clear();
633        let library = LibrarySettings {
634            name: "Test Library".to_string(),
635            path: PathBuf::from("/tmp"),
636            mode: LibraryMode::Filesystem,
637            ..Default::default()
638        };
639        settings.libraries.push(library);
640        let rect = rect![0, 0, 200, 50];
641
642        let mut context = create_test_context();
643        let mut value =
644            SettingValue::new(Kind::LibraryMode(0), rect, &settings, &mut context.fonts);
645        let mut rq = RenderQueue::new();
646
647        assert_eq!(value.value(), "Filesystem");
648
649        context.settings.libraries[0].mode = LibraryMode::Database;
650        value.refresh_from_context(&context, &mut rq);
651
652        assert_eq!(value.value(), "Database");
653        assert!(!rq.is_empty());
654    }
655
656    #[test]
657    fn test_auto_suspend_submit_updates_value() {
658        let mut context = create_test_context();
659        let settings = Settings::default();
660        let rect = rect![0, 0, 200, 50];
661
662        let mut value = SettingValue::new(Kind::AutoSuspend, rect, &settings, &mut context.fonts);
663        let mut rq = RenderQueue::new();
664
665        context.settings.auto_suspend = 15.0;
666        value.refresh_from_context(&context, &mut rq);
667
668        assert_eq!(value.value(), "15.0");
669        assert!(!rq.is_empty());
670    }
671
672    #[test]
673    fn test_auto_power_off_submit_updates_value() {
674        let mut context = create_test_context();
675        let settings = Settings::default();
676        let rect = rect![0, 0, 200, 50];
677
678        let mut value = SettingValue::new(Kind::AutoPowerOff, rect, &settings, &mut context.fonts);
679        let mut rq = RenderQueue::new();
680
681        context.settings.auto_power_off = 7.0;
682        value.refresh_from_context(&context, &mut rq);
683
684        assert_eq!(value.value(), "7.0");
685        assert!(!rq.is_empty());
686    }
687
688    #[test]
689    fn test_library_name_submit_updates_value() {
690        use crate::settings::LibrarySettings;
691        let mut settings = Settings::default();
692        settings.libraries.push(LibrarySettings {
693            name: "Old Name".to_string(),
694            path: PathBuf::from("/tmp"),
695            mode: crate::settings::LibraryMode::Filesystem,
696            ..Default::default()
697        });
698        let rect = rect![0, 0, 200, 50];
699
700        let mut context = create_test_context();
701        let mut value =
702            SettingValue::new(Kind::LibraryName(0), rect, &settings, &mut context.fonts);
703        let mut rq = RenderQueue::new();
704
705        context.settings.libraries[0].name = "New Name".to_string();
706        value.refresh_from_context(&context, &mut rq);
707
708        assert_eq!(value.value(), "New Name");
709        assert!(!rq.is_empty());
710    }
711
712    #[test]
713    fn test_library_path_file_chooser_closed_updates_value() {
714        use crate::settings::LibrarySettings;
715        let mut settings = Settings::default();
716        settings.libraries.push(LibrarySettings {
717            name: "Test Library".to_string(),
718            path: PathBuf::from("/tmp"),
719            mode: crate::settings::LibraryMode::Filesystem,
720            ..Default::default()
721        });
722        let rect = rect![0, 0, 200, 50];
723
724        let mut context = create_test_context();
725        let mut value =
726            SettingValue::new(Kind::LibraryPath(0), rect, &settings, &mut context.fonts);
727        let mut rq = RenderQueue::new();
728
729        let new_path = PathBuf::from("/mnt/onboard/new_library");
730        context.settings.libraries[0].path = new_path.clone();
731        value.refresh_from_context(&context, &mut rq);
732
733        assert_eq!(value.value(), new_path.display().to_string());
734        assert!(!rq.is_empty());
735    }
736
737    #[test]
738    fn test_tap_gesture_on_library_info_emits_edit_event() {
739        use crate::settings::LibrarySettings;
740        let mut settings = Settings::default();
741        settings.libraries.push(LibrarySettings {
742            name: "Test Library".to_string(),
743            path: PathBuf::from("/tmp"),
744            mode: crate::settings::LibraryMode::Filesystem,
745            ..Default::default()
746        });
747        let rect = rect![0, 0, 200, 50];
748
749        let mut context = create_test_context();
750        let value = SettingValue::new(Kind::LibraryInfo(0), rect, &settings, &mut context.fonts);
751        let (hub, _receiver) = channel();
752        let mut bus = VecDeque::new();
753        let mut rq = RenderQueue::new();
754
755        let point = crate::geom::Point::new(100, 25);
756        let event = Event::Gesture(GestureEvent::Tap(point));
757
758        let mut boxed: Box<dyn View> = Box::new(value);
759        crate::view::handle_event(
760            boxed.as_mut(),
761            &event,
762            &hub,
763            &mut bus,
764            &mut rq,
765            &mut context,
766        );
767
768        assert_eq!(bus.len(), 1);
769        if let Some(Event::EditLibrary(index)) = bus.pop_front() {
770            assert_eq!(index, 0);
771        } else {
772            panic!("Expected EditLibrary event");
773        }
774    }
775}