Skip to main content

cadmus_core/view/settings_editor/
setting_value.rs

1use super::super::action_label::ActionLabel;
2use super::super::{Align, Bus, Event, Hub, ID_FEEDER, Id, RenderQueue, View, ViewId};
3use super::kinds::{SettingIdentity, SettingKind, WidgetKind};
4use crate::context::Context;
5use crate::framebuffer::{Framebuffer, UpdateMode};
6use crate::geom::Rectangle;
7use crate::settings::Settings;
8use crate::view::common::locate_by_id;
9use crate::view::file_chooser::{FileChooser, SelectionMode};
10use crate::view::label::Label;
11use crate::view::toggle::Toggle;
12use crate::view::{EntryKind, RenderData};
13
14/// Re-exported for use in `ToggleEvent::Setting` and `CategoryEditor`.
15pub use super::kinds::ToggleSettings;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum SettingsEvent {
19    /// Updates a SettingValue view by its identity with a new value.
20    ///
21    /// Each SettingValue checks if the identity matches its own, and updates
22    /// itself if there is a match. This allows targeted updates without needing
23    /// to know the specific view ID.
24    UpdateValue {
25        /// The identity of the SettingValue to update (matches against self.identity)
26        kind: SettingIdentity,
27        /// The new value to display
28        value: String,
29    },
30}
31
32/// Represents a single setting value display in the settings UI.
33///
34/// This struct manages the display and interaction of a setting value and its
35/// associated UI widget (an [`ActionLabel`], a sub-menu label, or a [`Toggle`]).
36/// It acts as a View that handles events via the [`SettingKind`] trait, updating the
37/// displayed text and sub-menu checked state when the underlying setting changes.
38pub struct SettingValue {
39    /// Unique identifier for this setting value view
40    id: Id,
41    /// The rectangular area occupied by this view
42    rect: Rectangle,
43    /// Child views — a single ActionLabel or Toggle widget, plus any active NamedInput overlay
44    children: Vec<Box<dyn View>>,
45    /// Retained so that SubMenu entries can be rebuilt with updated checked
46    /// state each time UpdateValue is received, and to provide identity for
47    /// routing [`SettingsEvent::UpdateValue`] without a separate field.
48    kind: Box<dyn SettingKind>,
49    /// Current SubMenu entries, exposed for test inspection.
50    #[cfg(test)]
51    pub entries: Vec<EntryKind>,
52    #[cfg(not(test))]
53    entries: Vec<EntryKind>,
54    /// Tracks the ViewId of an active NamedInput child, if one is open.
55    active_input: Option<ViewId>,
56    /// Whether a FileChooser overlay is currently open as a child.
57    active_file_chooser: bool,
58    /// Keeps the temporary directory alive for the duration of the FileChooser session.
59    /// Without this, the directory would be deleted before FileChooser can use it.
60    #[cfg(test)]
61    _temp_dir: Option<tempfile::TempDir>,
62}
63
64impl std::fmt::Debug for SettingValue {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.debug_struct("SettingValue")
67            .field("id", &self.id)
68            .field("identity", &self.kind.identity())
69            .field("rect", &self.rect)
70            .finish_non_exhaustive()
71    }
72}
73
74impl SettingValue {
75    /// Creates a new `SettingValue` for the given `kind`.
76    ///
77    /// `kind` accepts any owned value that implements [`SettingKind`], including references
78    /// (`&T`), boxes (`Box<T>`), and concrete types directly. The value is erased into a
79    /// `Box<dyn SettingKind>` internally, so the caller does not need to box it beforehand.
80    pub fn new(
81        kind: impl SettingKind + 'static,
82        rect: Rectangle,
83        settings: &Settings,
84        fonts: &mut crate::font::Fonts,
85    ) -> SettingValue {
86        let kind: Box<dyn SettingKind> = Box::new(kind);
87        let data = kind.fetch(settings);
88
89        let entries = if let WidgetKind::SubMenu(ref e) = data.widget {
90            e.clone()
91        } else {
92            Vec::new()
93        };
94
95        let mut setting_value = SettingValue {
96            id: ID_FEEDER.next(),
97            rect,
98            children: vec![],
99            kind,
100            entries,
101            active_input: None,
102            active_file_chooser: false,
103            #[cfg(test)]
104            _temp_dir: None,
105        };
106
107        setting_value.children =
108            vec![setting_value.build_child_view(data.value, data.widget, fonts)];
109
110        setting_value
111    }
112
113    fn build_child_view(
114        &self,
115        value: String,
116        widget: WidgetKind,
117        fonts: &mut crate::font::Fonts,
118    ) -> Box<dyn View> {
119        match widget {
120            WidgetKind::None => Box::new(Label::new(self.rect, value, Align::Right(10))),
121            WidgetKind::Toggle {
122                left_label,
123                right_label,
124                enabled,
125                tap_event,
126            } => Box::new(Toggle::new(
127                self.rect,
128                &left_label,
129                &right_label,
130                enabled,
131                tap_event,
132                fonts,
133                Align::Right(10),
134            )),
135            WidgetKind::ActionLabel(tap_event) => Box::new(
136                ActionLabel::new(self.rect, value, Align::Right(10)).event(Some(tap_event)),
137            ),
138            WidgetKind::SubMenu(entries) => {
139                let event = Some(Event::SubMenu(self.rect, entries));
140                Box::new(ActionLabel::new(self.rect, value, Align::Right(10)).event(event))
141            }
142        }
143    }
144
145    pub fn update(&mut self, value: String, settings: &Settings, rq: &mut RenderQueue) {
146        if let Some(action_label) = self.children[0].downcast_mut::<ActionLabel>() {
147            action_label.update(&value, rq);
148
149            if let WidgetKind::SubMenu(entries) = self.kind.fetch(settings).widget {
150                self.entries = entries.clone();
151                action_label.set_event(Some(Event::SubMenu(self.rect, entries)));
152            }
153        }
154    }
155
156    pub fn value(&self) -> String {
157        if let Some(action_label) = self.children[0].downcast_ref::<ActionLabel>() {
158            action_label.value()
159        } else {
160            String::new()
161        }
162    }
163
164    /// Propagates a hold event to the child `ActionLabel`.
165    pub fn hold_event(mut self, event: Option<Event>) -> Self {
166        if let Some(action_label) = self.children[0].downcast_mut::<ActionLabel>() {
167            action_label.set_hold_event(event);
168        }
169
170        self
171    }
172}
173
174impl View for SettingValue {
175    /// Handles events in three passes.
176    ///
177    /// 1. Delegates to [`SettingKind::handle`] for direct mutation events such as
178    ///    submenu selections and toggle taps. When the kind handles the event it
179    ///    returns the updated display string, which is applied immediately.
180    ///
181    /// 2. For [`InputSettingKind`](crate::view::settings_editor::InputSettingKind) settings, opens a [`NamedInput`] overlay on the
182    ///    matching [`EntryId`](crate::view::EntryId) tap, applies the submitted text on [`Event::Submit`],
183    ///    and closes the overlay on [`Event::Close`] or after submission.
184    ///
185    /// 3. Falls back to [`SettingsEvent::UpdateValue`] routing so that
186    ///    `LibraryEditor` and other callers can push targeted display updates
187    ///    without going through the event bus.
188    ///
189    /// [`NamedInput`]: crate::view::named_input::NamedInput
190    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, hub, bus, rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
191    fn handle_event(
192        &mut self,
193        evt: &Event,
194        hub: &Hub,
195        bus: &mut Bus,
196        rq: &mut RenderQueue,
197        context: &mut Context,
198    ) -> bool {
199        if let Event::Select(entry_id) = evt
200            && Some(entry_id) == self.kind.file_chooser_entry_id().as_ref()
201            && !self.active_file_chooser
202        {
203            #[cfg(not(test))]
204            let initial_path = std::path::PathBuf::from("/mnt/onboard");
205            #[cfg(test)]
206            let initial_path = {
207                let temp_dir = tempfile::tempdir().expect("failed to create temp dir for test");
208                let path = temp_dir.path().to_path_buf();
209                self._temp_dir = Some(temp_dir);
210                path
211            };
212
213            let file_chooser = FileChooser::new(
214                rect!(
215                    0,
216                    0,
217                    context.display.dims.0 as i32,
218                    context.display.dims.1 as i32
219                ),
220                initial_path,
221                SelectionMode::File,
222                hub,
223                rq,
224                context,
225            );
226            rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
227            self.children.push(Box::new(file_chooser));
228            self.active_file_chooser = true;
229            return true;
230        }
231
232        if let Event::FileChooserClosed(_) = evt {
233            if self.active_file_chooser {
234                if let (Some(display), _) = self.kind.handle(evt, &mut context.settings, bus) {
235                    self.update(display, &context.settings, rq);
236                }
237                return false;
238            }
239            return false;
240        }
241
242        if let Event::Close(ViewId::FileChooser) = evt {
243            if self.active_file_chooser {
244                if let Some(idx) = locate_by_id(self, ViewId::FileChooser) {
245                    self.children.remove(idx);
246                }
247                self.active_file_chooser = false;
248                #[cfg(test)]
249                {
250                    self._temp_dir = None;
251                }
252                rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
253                return false;
254            }
255        }
256
257        if let (Some(display), handled) = self.kind.handle(evt, &mut context.settings, bus) {
258            self.update(display, &context.settings, rq);
259            if !self.kind.keep_menu_open() {
260                bus.push_back(Event::Close(ViewId::SettingsValueMenu));
261            }
262            return handled;
263        }
264
265        if let Some(input_kind) = self.kind.as_input_kind() {
266            let view_id = input_kind.submit_view_id();
267            let open_entry = input_kind.open_entry_id();
268
269            if let Event::Select(id) = evt
270                && *id == open_entry
271                && self.active_input.is_none()
272            {
273                bus.push_back(Event::OpenNamedInput {
274                    view_id,
275                    label: input_kind.input_label(),
276                    max_chars: input_kind.input_max_chars(),
277                    initial_text: input_kind.current_text(&context.settings),
278                });
279                self.active_input = Some(view_id);
280                return true;
281            }
282
283            if let Event::Submit(submitted_id, text) = evt
284                && Some(*submitted_id) == self.active_input
285            {
286                let display = self
287                    .kind
288                    .as_input_kind()
289                    .unwrap()
290                    .apply_text(text, &mut context.settings);
291                self.active_input = None;
292                self.update(display, &context.settings, rq);
293                return true;
294            }
295        }
296
297        if let Event::Settings(SettingsEvent::UpdateValue { kind, value }) = evt {
298            if self.kind.identity() == *kind {
299                self.update(value.clone(), &context.settings, rq);
300                return true;
301            }
302        }
303
304        false
305    }
306
307    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
308    fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut crate::font::Fonts) {
309    }
310
311    fn rect(&self) -> &Rectangle {
312        &self.rect
313    }
314
315    fn rect_mut(&mut self) -> &mut Rectangle {
316        &mut self.rect
317    }
318
319    fn children(&self) -> &Vec<Box<dyn View>> {
320        &self.children
321    }
322
323    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
324        &mut self.children
325    }
326
327    fn id(&self) -> Id {
328        self.id
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use crate::context::test_helpers::create_test_context;
336    use crate::gesture::GestureEvent;
337    use crate::settings::Settings;
338    use crate::view::settings_editor::kinds::general::{
339        AutoPowerOff, AutoSuspend, KeyboardLayout, SettingsRetention,
340    };
341    use crate::view::settings_editor::kinds::import::AllowedKindsSetting;
342    use crate::view::settings_editor::kinds::intermission::{
343        IntermissionPowerOff, IntermissionShare, IntermissionSuspend,
344    };
345    use crate::view::settings_editor::kinds::library::{LibraryInfo, LibraryName, LibraryPath};
346    use crate::view::settings_editor::kinds::telemetry::LogLevel;
347    use crate::view::{EntryId, RenderQueue};
348    use std::collections::VecDeque;
349    use std::path::PathBuf;
350    use std::sync::mpsc::channel;
351
352    #[test]
353    fn test_file_chooser_closed_updates_all_intermission_values() {
354        let mut context = create_test_context();
355        let settings = Settings::default();
356        let rect = rect![0, 0, 200, 50];
357
358        let mut suspend_value =
359            SettingValue::new(&IntermissionSuspend, rect, &settings, &mut context.fonts);
360        let mut power_off_value =
361            SettingValue::new(&IntermissionPowerOff, rect, &settings, &mut context.fonts);
362        let mut share_value =
363            SettingValue::new(&IntermissionShare, rect, &settings, &mut context.fonts);
364
365        let (hub, _receiver) = channel();
366        let mut bus = VecDeque::new();
367        let mut rq = RenderQueue::new();
368
369        let initial_suspend = suspend_value.value().clone();
370        let initial_power_off = power_off_value.value().clone();
371        let initial_share = share_value.value().clone();
372
373        let event = Event::FileChooserClosed(None);
374
375        suspend_value.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
376        power_off_value.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
377        share_value.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
378
379        assert_eq!(suspend_value.value(), initial_suspend);
380        assert_eq!(power_off_value.value(), initial_power_off);
381        assert_eq!(share_value.value(), initial_share);
382    }
383
384    #[test]
385    fn test_intermission_values_update_via_update_value_event() {
386        let mut context = create_test_context();
387        let settings = Settings::default();
388        let rect = rect![0, 0, 200, 50];
389
390        let mut suspend_value =
391            SettingValue::new(&IntermissionSuspend, rect, &settings, &mut context.fonts);
392        let mut power_off_value =
393            SettingValue::new(&IntermissionPowerOff, rect, &settings, &mut context.fonts);
394        let mut share_value =
395            SettingValue::new(&IntermissionShare, rect, &settings, &mut context.fonts);
396
397        let (hub, _receiver) = channel();
398        let mut bus = VecDeque::new();
399        let mut rq = RenderQueue::new();
400
401        let handled_suspend = suspend_value.handle_event(
402            &Event::Settings(SettingsEvent::UpdateValue {
403                kind: SettingIdentity::IntermissionSuspend,
404                value: "suspend_image.png".to_string(),
405            }),
406            &hub,
407            &mut bus,
408            &mut rq,
409            &mut context,
410        );
411        let handled_power_off = power_off_value.handle_event(
412            &Event::Settings(SettingsEvent::UpdateValue {
413                kind: SettingIdentity::IntermissionPowerOff,
414                value: "poweroff_image.png".to_string(),
415            }),
416            &hub,
417            &mut bus,
418            &mut rq,
419            &mut context,
420        );
421        let handled_share = share_value.handle_event(
422            &Event::Settings(SettingsEvent::UpdateValue {
423                kind: SettingIdentity::IntermissionShare,
424                value: "share_image.png".to_string(),
425            }),
426            &hub,
427            &mut bus,
428            &mut rq,
429            &mut context,
430        );
431
432        assert!(handled_suspend);
433        assert!(handled_power_off);
434        assert!(handled_share);
435        assert_eq!(suspend_value.value(), "suspend_image.png");
436        assert_eq!(power_off_value.value(), "poweroff_image.png");
437        assert_eq!(share_value.value(), "share_image.png");
438    }
439
440    #[test]
441    fn test_keyboard_layout_select_updates_value() {
442        let mut context = create_test_context();
443        let settings = Settings {
444            keyboard_layout: "English".to_string(),
445            ..Default::default()
446        };
447        let rect = rect![0, 0, 200, 50];
448
449        let mut value = SettingValue::new(&KeyboardLayout, rect, &settings, &mut context.fonts);
450        let mut rq = RenderQueue::new();
451
452        let update_event = Event::Settings(SettingsEvent::UpdateValue {
453            kind: SettingIdentity::KeyboardLayout,
454            value: "French".to_string(),
455        });
456        let (hub, _receiver) = channel();
457        let mut bus = VecDeque::new();
458        value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
459
460        assert_eq!(value.value(), "French");
461        assert!(!rq.is_empty());
462    }
463
464    #[test]
465    fn test_auto_suspend_submit_updates_value() {
466        let mut context = create_test_context();
467        let settings = Settings::default();
468        let rect = rect![0, 0, 200, 50];
469
470        let mut value = SettingValue::new(&AutoSuspend, rect, &settings, &mut context.fonts);
471        let mut rq = RenderQueue::new();
472        let (hub, _receiver) = channel();
473        let mut bus = VecDeque::new();
474
475        let update_event = Event::Settings(SettingsEvent::UpdateValue {
476            kind: SettingIdentity::AutoSuspend,
477            value: "15.0".to_string(),
478        });
479        value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
480
481        assert_eq!(value.value(), "15.0");
482        assert!(!rq.is_empty());
483    }
484
485    #[test]
486    fn test_auto_power_off_submit_updates_value() {
487        let mut context = create_test_context();
488        let settings = Settings::default();
489        let rect = rect![0, 0, 200, 50];
490
491        let mut value = SettingValue::new(&AutoPowerOff, rect, &settings, &mut context.fonts);
492        let mut rq = RenderQueue::new();
493        let (hub, _receiver) = channel();
494        let mut bus = VecDeque::new();
495
496        let update_event = Event::Settings(SettingsEvent::UpdateValue {
497            kind: SettingIdentity::AutoPowerOff,
498            value: "7.0".to_string(),
499        });
500        value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
501
502        assert_eq!(value.value(), "7.0");
503        assert!(!rq.is_empty());
504    }
505
506    #[test]
507    fn test_library_name_submit_updates_value() {
508        use crate::settings::LibrarySettings;
509        let mut settings = Settings::default();
510        settings.libraries.push(LibrarySettings {
511            name: "Old Name".to_string(),
512            path: PathBuf::from("/tmp"),
513            ..Default::default()
514        });
515        let rect = rect![0, 0, 200, 50];
516
517        let mut context = create_test_context();
518        let mut value = SettingValue::new(LibraryName(0), rect, &settings, &mut context.fonts);
519        let mut rq = RenderQueue::new();
520        let (hub, _receiver) = channel();
521        let mut bus = VecDeque::new();
522
523        let update_event = Event::Settings(SettingsEvent::UpdateValue {
524            kind: SettingIdentity::LibraryName(0),
525            value: "New Name".to_string(),
526        });
527        value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
528
529        assert_eq!(value.value(), "New Name");
530        assert!(!rq.is_empty());
531    }
532
533    #[test]
534    fn test_library_path_file_chooser_closed_updates_value() {
535        use crate::settings::LibrarySettings;
536        let mut settings = Settings::default();
537        settings.libraries.push(LibrarySettings {
538            name: "Test Library".to_string(),
539            path: PathBuf::from("/tmp"),
540            ..Default::default()
541        });
542        let rect = rect![0, 0, 200, 50];
543
544        let mut context = create_test_context();
545        let mut value = SettingValue::new(LibraryPath(0), rect, &settings, &mut context.fonts);
546        let mut rq = RenderQueue::new();
547        let (hub, _receiver) = channel();
548        let mut bus = VecDeque::new();
549
550        let new_path = PathBuf::from("/mnt/onboard/new_library");
551        let update_event = Event::Settings(SettingsEvent::UpdateValue {
552            kind: SettingIdentity::LibraryPath(0),
553            value: new_path.display().to_string(),
554        });
555        value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
556
557        assert_eq!(value.value(), new_path.display().to_string());
558        assert!(!rq.is_empty());
559    }
560
561    #[test]
562    fn test_tap_gesture_on_library_info_emits_edit_event() {
563        use crate::settings::LibrarySettings;
564        let mut settings = Settings::default();
565        settings.libraries.push(LibrarySettings {
566            name: "Test Library".to_string(),
567            path: PathBuf::from("/tmp"),
568            ..Default::default()
569        });
570        let rect = rect![0, 0, 200, 50];
571
572        let mut context = create_test_context();
573        let value = SettingValue::new(LibraryInfo(0), rect, &settings, &mut context.fonts);
574        let (hub, _receiver) = channel();
575        let mut bus = VecDeque::new();
576        let mut rq = RenderQueue::new();
577
578        let point = crate::geom::Point::new(100, 25);
579        let event = Event::Gesture(GestureEvent::Tap(point));
580
581        let mut boxed: Box<dyn View> = Box::new(value);
582        crate::view::handle_event(
583            boxed.as_mut(),
584            &event,
585            &hub,
586            &mut bus,
587            &mut rq,
588            &mut context,
589        );
590
591        assert_eq!(bus.len(), 1);
592        if let Some(Event::EditLibrary(index)) = bus.pop_front() {
593            assert_eq!(index, 0);
594        } else {
595            panic!("Expected EditLibrary event");
596        }
597    }
598
599    #[test]
600    fn test_update_value_event_updates_library_name_display() {
601        use crate::settings::LibrarySettings;
602        let mut context = create_test_context();
603        context.settings.libraries.clear();
604        context.settings.libraries.push(LibrarySettings {
605            name: "Old Name".to_string(),
606            path: PathBuf::from("/tmp"),
607            ..Default::default()
608        });
609        let rect = rect![0, 0, 200, 50];
610
611        let mut value =
612            SettingValue::new(LibraryName(0), rect, &context.settings, &mut context.fonts);
613        let (hub, _receiver) = channel();
614        let mut bus = VecDeque::new();
615        let mut rq = RenderQueue::new();
616
617        assert_eq!(value.value(), "Old Name");
618
619        let update_event = Event::Settings(SettingsEvent::UpdateValue {
620            kind: SettingIdentity::LibraryName(0),
621            value: "New Name".to_string(),
622        });
623        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
624
625        assert!(
626            handled,
627            "UpdateValue event should be handled when kind matches"
628        );
629        assert_eq!(value.value(), "New Name");
630    }
631
632    #[test]
633    fn test_update_value_event_updates_library_path_display() {
634        use crate::settings::LibrarySettings;
635        let mut context = create_test_context();
636        context.settings.libraries.clear();
637        context.settings.libraries.push(LibrarySettings {
638            name: "Test Library".to_string(),
639            path: PathBuf::from("/old/path"),
640            ..Default::default()
641        });
642        let rect = rect![0, 0, 200, 50];
643
644        let mut value =
645            SettingValue::new(LibraryPath(0), rect, &context.settings, &mut context.fonts);
646        let (hub, _receiver) = channel();
647        let mut bus = VecDeque::new();
648        let mut rq = RenderQueue::new();
649
650        assert_eq!(value.value(), "/old/path");
651
652        let update_event = Event::Settings(SettingsEvent::UpdateValue {
653            kind: SettingIdentity::LibraryPath(0),
654            value: "/new/path".to_string(),
655        });
656        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
657
658        assert!(
659            handled,
660            "UpdateValue event should be handled when kind matches"
661        );
662        assert_eq!(value.value(), "/new/path");
663    }
664
665    #[test]
666    fn test_update_value_event_ignores_wrong_kind() {
667        use crate::settings::LibrarySettings;
668        let mut context = create_test_context();
669        context.settings.libraries.clear();
670        context.settings.libraries.push(LibrarySettings {
671            name: "Test Library".to_string(),
672            path: PathBuf::from("/tmp"),
673            ..Default::default()
674        });
675        let rect = rect![0, 0, 200, 50];
676
677        let mut value =
678            SettingValue::new(LibraryName(0), rect, &context.settings, &mut context.fonts);
679        let (hub, _receiver) = channel();
680        let mut bus = VecDeque::new();
681        let mut rq = RenderQueue::new();
682
683        assert_eq!(value.value(), "Test Library");
684
685        let update_event = Event::Settings(SettingsEvent::UpdateValue {
686            kind: SettingIdentity::LibraryPath(0),
687            value: "Some Path".to_string(),
688        });
689        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
690
691        assert!(
692            !handled,
693            "UpdateValue event should not be handled when kind does not match"
694        );
695        assert_eq!(
696            value.value(),
697            "Test Library",
698            "Value should not change when kind mismatches"
699        );
700    }
701
702    #[test]
703    fn test_update_value_event_ignores_wrong_index() {
704        use crate::settings::LibrarySettings;
705        let mut context = create_test_context();
706        context.settings.libraries.clear();
707        context.settings.libraries.push(LibrarySettings {
708            name: "Library 0".to_string(),
709            path: PathBuf::from("/path0"),
710            ..Default::default()
711        });
712        context.settings.libraries.push(LibrarySettings {
713            name: "Library 1".to_string(),
714            path: PathBuf::from("/path1"),
715            ..Default::default()
716        });
717        let rect = rect![0, 0, 200, 50];
718
719        let mut value =
720            SettingValue::new(LibraryName(0), rect, &context.settings, &mut context.fonts);
721        let (hub, _receiver) = channel();
722        let mut bus = VecDeque::new();
723        let mut rq = RenderQueue::new();
724
725        assert_eq!(value.value(), "Library 0");
726
727        let update_event = Event::Settings(SettingsEvent::UpdateValue {
728            kind: SettingIdentity::LibraryName(1),
729            value: "Updated Library 1".to_string(),
730        });
731        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
732
733        assert!(
734            !handled,
735            "UpdateValue event should not be handled when index does not match"
736        );
737        assert_eq!(
738            value.value(),
739            "Library 0",
740            "Value should not change when index mismatches"
741        );
742    }
743
744    #[test]
745    fn test_update_value_event_updates_auto_suspend() {
746        let rect = rect![0, 0, 200, 50];
747        let mut context = create_test_context();
748        let mut value =
749            SettingValue::new(&AutoSuspend, rect, &context.settings, &mut context.fonts);
750        let (hub, _receiver) = channel();
751        let mut bus = VecDeque::new();
752        let mut rq = RenderQueue::new();
753
754        assert_eq!(value.value(), "30.0");
755
756        let update_event = Event::Settings(SettingsEvent::UpdateValue {
757            kind: SettingIdentity::AutoSuspend,
758            value: "60.0".to_string(),
759        });
760        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
761
762        assert!(
763            handled,
764            "UpdateValue event should be handled when kind matches"
765        );
766        assert_eq!(value.value(), "60.0");
767    }
768
769    #[test]
770    fn test_update_value_event_updates_auto_power_off() {
771        let rect = rect![0, 0, 200, 50];
772        let mut context = create_test_context();
773        let mut value =
774            SettingValue::new(&AutoPowerOff, rect, &context.settings, &mut context.fonts);
775        let (hub, _receiver) = channel();
776        let mut bus = VecDeque::new();
777        let mut rq = RenderQueue::new();
778
779        assert_eq!(value.value(), "3.0");
780
781        let update_event = Event::Settings(SettingsEvent::UpdateValue {
782            kind: SettingIdentity::AutoPowerOff,
783            value: "60.0".to_string(),
784        });
785        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
786
787        assert!(
788            handled,
789            "UpdateValue event should be handled when kind matches"
790        );
791        assert_eq!(value.value(), "60.0");
792    }
793
794    #[test]
795    fn test_update_value_event_updates_settings_retention() {
796        let rect = rect![0, 0, 200, 50];
797        let mut context = create_test_context();
798        let mut value = SettingValue::new(
799            &SettingsRetention,
800            rect,
801            &context.settings,
802            &mut context.fonts,
803        );
804        let (hub, _receiver) = channel();
805        let mut bus = VecDeque::new();
806        let mut rq = RenderQueue::new();
807
808        assert_eq!(value.value(), "3");
809
810        let update_event = Event::Settings(SettingsEvent::UpdateValue {
811            kind: SettingIdentity::SettingsRetention,
812            value: "5".to_string(),
813        });
814        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
815
816        assert!(
817            handled,
818            "UpdateValue event should be handled when kind matches"
819        );
820        assert_eq!(value.value(), "5");
821    }
822
823    #[test]
824    fn test_update_value_event_regenerates_log_level_radio_buttons() {
825        let rect = rect![0, 0, 200, 50];
826        let mut context = create_test_context();
827        context.settings.logging.level = "INFO".to_string();
828
829        let mut value = SettingValue::new(&LogLevel, rect, &context.settings, &mut context.fonts);
830        let (hub, _receiver) = channel();
831        let mut bus = VecDeque::new();
832        let mut rq = RenderQueue::new();
833
834        let initial_entries = value.entries.clone();
835        assert_eq!(initial_entries.len(), 5);
836
837        let info_entry = initial_entries.iter().find(|e| {
838            if let EntryKind::RadioButton(label, _, _) = e {
839                label == "INFO"
840            } else {
841                false
842            }
843        });
844        assert!(
845            matches!(info_entry, Some(EntryKind::RadioButton(_, _, true))),
846            "INFO should be initially checked"
847        );
848
849        context.settings.logging.level = "DEBUG".to_string();
850        let update_event = Event::Settings(SettingsEvent::UpdateValue {
851            kind: SettingIdentity::LogLevel,
852            value: "DEBUG".to_string(),
853        });
854        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
855
856        assert!(
857            handled,
858            "UpdateValue event should be handled when kind matches"
859        );
860        assert_eq!(value.value(), "DEBUG");
861    }
862
863    #[test]
864    fn test_keep_open_submenu_does_not_queue_menu_close() {
865        use crate::settings::FileExtension;
866
867        let rect = rect![0, 0, 200, 50];
868        let mut context = create_test_context();
869        let mut value = SettingValue::new(
870            &AllowedKindsSetting,
871            rect,
872            &context.settings,
873            &mut context.fonts,
874        );
875        let (hub, _receiver) = channel();
876        let mut bus = VecDeque::new();
877        let mut rq = RenderQueue::new();
878
879        let handled = value.handle_event(
880            &Event::Select(EntryId::ToggleAllowedKind(FileExtension::Cbr)),
881            &hub,
882            &mut bus,
883            &mut rq,
884            &mut context,
885        );
886
887        assert!(handled);
888        assert!(
889            context
890                .settings
891                .import
892                .allowed_kinds
893                .contains(&FileExtension::Cbr)
894        );
895        assert!(
896            !bus.iter()
897                .any(|evt| matches!(evt, Event::Close(ViewId::SettingsValueMenu))),
898            "submenu should remain open for multi-select settings"
899        );
900    }
901}