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, RenderQueue, View, ViewId, ID_FEEDER};
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(ref entry_id) = evt {
200            if 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
233        if let Event::FileChooserClosed(_) = evt {
234            if self.active_file_chooser {
235                if let (Some(display), _) = self.kind.handle(evt, &mut context.settings, bus) {
236                    self.update(display, &context.settings, rq);
237                }
238                return false;
239            }
240            return false;
241        }
242
243        if let Event::Close(ViewId::FileChooser) = evt {
244            if self.active_file_chooser {
245                if let Some(idx) = locate_by_id(self, ViewId::FileChooser) {
246                    self.children.remove(idx);
247                }
248                self.active_file_chooser = false;
249                #[cfg(test)]
250                {
251                    self._temp_dir = None;
252                }
253                rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
254                return false;
255            }
256        }
257
258        if let (Some(display), handled) = self.kind.handle(evt, &mut context.settings, bus) {
259            self.update(display, &context.settings, rq);
260            bus.push_back(Event::Close(ViewId::SettingsValueMenu));
261            return handled;
262        }
263
264        if let Some(input_kind) = self.kind.as_input_kind() {
265            let view_id = input_kind.submit_view_id();
266            let open_entry = input_kind.open_entry_id();
267
268            if let Event::Select(ref id) = evt {
269                if *id == open_entry && self.active_input.is_none() {
270                    bus.push_back(Event::OpenNamedInput {
271                        view_id,
272                        label: input_kind.input_label(),
273                        max_chars: input_kind.input_max_chars(),
274                        initial_text: input_kind.current_text(&context.settings),
275                    });
276                    self.active_input = Some(view_id);
277                    return true;
278                }
279            }
280
281            if let Event::Submit(submitted_id, ref text) = evt {
282                if Some(*submitted_id) == self.active_input {
283                    let display = self
284                        .kind
285                        .as_input_kind()
286                        .unwrap()
287                        .apply_text(text, &mut context.settings);
288                    self.active_input = None;
289                    self.update(display, &context.settings, rq);
290                    return true;
291                }
292            }
293        }
294
295        if let Event::Settings(SettingsEvent::UpdateValue { kind, value }) = evt {
296            if self.kind.identity() == *kind {
297                self.update(value.clone(), &context.settings, rq);
298                return true;
299            }
300        }
301
302        false
303    }
304
305    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
306    fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut crate::font::Fonts) {
307    }
308
309    fn rect(&self) -> &Rectangle {
310        &self.rect
311    }
312
313    fn rect_mut(&mut self) -> &mut Rectangle {
314        &mut self.rect
315    }
316
317    fn children(&self) -> &Vec<Box<dyn View>> {
318        &self.children
319    }
320
321    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
322        &mut self.children
323    }
324
325    fn id(&self) -> Id {
326        self.id
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use crate::context::test_helpers::create_test_context;
334    use crate::gesture::GestureEvent;
335    use crate::settings::Settings;
336    use crate::view::settings_editor::kinds::general::{
337        AutoPowerOff, AutoSuspend, KeyboardLayout, SettingsRetention,
338    };
339    use crate::view::settings_editor::kinds::intermission::{
340        IntermissionPowerOff, IntermissionShare, IntermissionSuspend,
341    };
342    use crate::view::settings_editor::kinds::library::{LibraryInfo, LibraryName, LibraryPath};
343    use crate::view::settings_editor::kinds::telemetry::LogLevel;
344    use crate::view::RenderQueue;
345    use std::collections::VecDeque;
346    use std::path::PathBuf;
347    use std::sync::mpsc::channel;
348
349    #[test]
350    fn test_file_chooser_closed_updates_all_intermission_values() {
351        let mut context = create_test_context();
352        let settings = Settings::default();
353        let rect = rect![0, 0, 200, 50];
354
355        let mut suspend_value =
356            SettingValue::new(&IntermissionSuspend, rect, &settings, &mut context.fonts);
357        let mut power_off_value =
358            SettingValue::new(&IntermissionPowerOff, rect, &settings, &mut context.fonts);
359        let mut share_value =
360            SettingValue::new(&IntermissionShare, rect, &settings, &mut context.fonts);
361
362        let (hub, _receiver) = channel();
363        let mut bus = VecDeque::new();
364        let mut rq = RenderQueue::new();
365
366        let initial_suspend = suspend_value.value().clone();
367        let initial_power_off = power_off_value.value().clone();
368        let initial_share = share_value.value().clone();
369
370        let event = Event::FileChooserClosed(None);
371
372        suspend_value.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
373        power_off_value.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
374        share_value.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
375
376        assert_eq!(suspend_value.value(), initial_suspend);
377        assert_eq!(power_off_value.value(), initial_power_off);
378        assert_eq!(share_value.value(), initial_share);
379    }
380
381    #[test]
382    fn test_intermission_values_update_via_update_value_event() {
383        let mut context = create_test_context();
384        let settings = Settings::default();
385        let rect = rect![0, 0, 200, 50];
386
387        let mut suspend_value =
388            SettingValue::new(&IntermissionSuspend, rect, &settings, &mut context.fonts);
389        let mut power_off_value =
390            SettingValue::new(&IntermissionPowerOff, rect, &settings, &mut context.fonts);
391        let mut share_value =
392            SettingValue::new(&IntermissionShare, rect, &settings, &mut context.fonts);
393
394        let (hub, _receiver) = channel();
395        let mut bus = VecDeque::new();
396        let mut rq = RenderQueue::new();
397
398        let handled_suspend = suspend_value.handle_event(
399            &Event::Settings(SettingsEvent::UpdateValue {
400                kind: SettingIdentity::IntermissionSuspend,
401                value: "suspend_image.png".to_string(),
402            }),
403            &hub,
404            &mut bus,
405            &mut rq,
406            &mut context,
407        );
408        let handled_power_off = power_off_value.handle_event(
409            &Event::Settings(SettingsEvent::UpdateValue {
410                kind: SettingIdentity::IntermissionPowerOff,
411                value: "poweroff_image.png".to_string(),
412            }),
413            &hub,
414            &mut bus,
415            &mut rq,
416            &mut context,
417        );
418        let handled_share = share_value.handle_event(
419            &Event::Settings(SettingsEvent::UpdateValue {
420                kind: SettingIdentity::IntermissionShare,
421                value: "share_image.png".to_string(),
422            }),
423            &hub,
424            &mut bus,
425            &mut rq,
426            &mut context,
427        );
428
429        assert!(handled_suspend);
430        assert!(handled_power_off);
431        assert!(handled_share);
432        assert_eq!(suspend_value.value(), "suspend_image.png");
433        assert_eq!(power_off_value.value(), "poweroff_image.png");
434        assert_eq!(share_value.value(), "share_image.png");
435    }
436
437    #[test]
438    fn test_keyboard_layout_select_updates_value() {
439        let mut context = create_test_context();
440        let settings = Settings {
441            keyboard_layout: "English".to_string(),
442            ..Default::default()
443        };
444        let rect = rect![0, 0, 200, 50];
445
446        let mut value = SettingValue::new(&KeyboardLayout, rect, &settings, &mut context.fonts);
447        let mut rq = RenderQueue::new();
448
449        let update_event = Event::Settings(SettingsEvent::UpdateValue {
450            kind: SettingIdentity::KeyboardLayout,
451            value: "French".to_string(),
452        });
453        let (hub, _receiver) = channel();
454        let mut bus = VecDeque::new();
455        value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
456
457        assert_eq!(value.value(), "French");
458        assert!(!rq.is_empty());
459    }
460
461    #[test]
462    fn test_auto_suspend_submit_updates_value() {
463        let mut context = create_test_context();
464        let settings = Settings::default();
465        let rect = rect![0, 0, 200, 50];
466
467        let mut value = SettingValue::new(&AutoSuspend, rect, &settings, &mut context.fonts);
468        let mut rq = RenderQueue::new();
469        let (hub, _receiver) = channel();
470        let mut bus = VecDeque::new();
471
472        let update_event = Event::Settings(SettingsEvent::UpdateValue {
473            kind: SettingIdentity::AutoSuspend,
474            value: "15.0".to_string(),
475        });
476        value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
477
478        assert_eq!(value.value(), "15.0");
479        assert!(!rq.is_empty());
480    }
481
482    #[test]
483    fn test_auto_power_off_submit_updates_value() {
484        let mut context = create_test_context();
485        let settings = Settings::default();
486        let rect = rect![0, 0, 200, 50];
487
488        let mut value = SettingValue::new(&AutoPowerOff, rect, &settings, &mut context.fonts);
489        let mut rq = RenderQueue::new();
490        let (hub, _receiver) = channel();
491        let mut bus = VecDeque::new();
492
493        let update_event = Event::Settings(SettingsEvent::UpdateValue {
494            kind: SettingIdentity::AutoPowerOff,
495            value: "7.0".to_string(),
496        });
497        value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
498
499        assert_eq!(value.value(), "7.0");
500        assert!(!rq.is_empty());
501    }
502
503    #[test]
504    fn test_library_name_submit_updates_value() {
505        use crate::settings::LibrarySettings;
506        let mut settings = Settings::default();
507        settings.libraries.push(LibrarySettings {
508            name: "Old Name".to_string(),
509            path: PathBuf::from("/tmp"),
510            ..Default::default()
511        });
512        let rect = rect![0, 0, 200, 50];
513
514        let mut context = create_test_context();
515        let mut value = SettingValue::new(LibraryName(0), rect, &settings, &mut context.fonts);
516        let mut rq = RenderQueue::new();
517        let (hub, _receiver) = channel();
518        let mut bus = VecDeque::new();
519
520        let update_event = Event::Settings(SettingsEvent::UpdateValue {
521            kind: SettingIdentity::LibraryName(0),
522            value: "New Name".to_string(),
523        });
524        value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
525
526        assert_eq!(value.value(), "New Name");
527        assert!(!rq.is_empty());
528    }
529
530    #[test]
531    fn test_library_path_file_chooser_closed_updates_value() {
532        use crate::settings::LibrarySettings;
533        let mut settings = Settings::default();
534        settings.libraries.push(LibrarySettings {
535            name: "Test Library".to_string(),
536            path: PathBuf::from("/tmp"),
537            ..Default::default()
538        });
539        let rect = rect![0, 0, 200, 50];
540
541        let mut context = create_test_context();
542        let mut value = SettingValue::new(LibraryPath(0), rect, &settings, &mut context.fonts);
543        let mut rq = RenderQueue::new();
544        let (hub, _receiver) = channel();
545        let mut bus = VecDeque::new();
546
547        let new_path = PathBuf::from("/mnt/onboard/new_library");
548        let update_event = Event::Settings(SettingsEvent::UpdateValue {
549            kind: SettingIdentity::LibraryPath(0),
550            value: new_path.display().to_string(),
551        });
552        value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
553
554        assert_eq!(value.value(), new_path.display().to_string());
555        assert!(!rq.is_empty());
556    }
557
558    #[test]
559    fn test_tap_gesture_on_library_info_emits_edit_event() {
560        use crate::settings::LibrarySettings;
561        let mut settings = Settings::default();
562        settings.libraries.push(LibrarySettings {
563            name: "Test Library".to_string(),
564            path: PathBuf::from("/tmp"),
565            ..Default::default()
566        });
567        let rect = rect![0, 0, 200, 50];
568
569        let mut context = create_test_context();
570        let value = SettingValue::new(LibraryInfo(0), rect, &settings, &mut context.fonts);
571        let (hub, _receiver) = channel();
572        let mut bus = VecDeque::new();
573        let mut rq = RenderQueue::new();
574
575        let point = crate::geom::Point::new(100, 25);
576        let event = Event::Gesture(GestureEvent::Tap(point));
577
578        let mut boxed: Box<dyn View> = Box::new(value);
579        crate::view::handle_event(
580            boxed.as_mut(),
581            &event,
582            &hub,
583            &mut bus,
584            &mut rq,
585            &mut context,
586        );
587
588        assert_eq!(bus.len(), 1);
589        if let Some(Event::EditLibrary(index)) = bus.pop_front() {
590            assert_eq!(index, 0);
591        } else {
592            panic!("Expected EditLibrary event");
593        }
594    }
595
596    #[test]
597    fn test_update_value_event_updates_library_name_display() {
598        use crate::settings::LibrarySettings;
599        let mut context = create_test_context();
600        context.settings.libraries.clear();
601        context.settings.libraries.push(LibrarySettings {
602            name: "Old Name".to_string(),
603            path: PathBuf::from("/tmp"),
604            ..Default::default()
605        });
606        let rect = rect![0, 0, 200, 50];
607
608        let mut value =
609            SettingValue::new(LibraryName(0), rect, &context.settings, &mut context.fonts);
610        let (hub, _receiver) = channel();
611        let mut bus = VecDeque::new();
612        let mut rq = RenderQueue::new();
613
614        assert_eq!(value.value(), "Old Name");
615
616        let update_event = Event::Settings(SettingsEvent::UpdateValue {
617            kind: SettingIdentity::LibraryName(0),
618            value: "New Name".to_string(),
619        });
620        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
621
622        assert!(
623            handled,
624            "UpdateValue event should be handled when kind matches"
625        );
626        assert_eq!(value.value(), "New Name");
627    }
628
629    #[test]
630    fn test_update_value_event_updates_library_path_display() {
631        use crate::settings::LibrarySettings;
632        let mut context = create_test_context();
633        context.settings.libraries.clear();
634        context.settings.libraries.push(LibrarySettings {
635            name: "Test Library".to_string(),
636            path: PathBuf::from("/old/path"),
637            ..Default::default()
638        });
639        let rect = rect![0, 0, 200, 50];
640
641        let mut value =
642            SettingValue::new(LibraryPath(0), rect, &context.settings, &mut context.fonts);
643        let (hub, _receiver) = channel();
644        let mut bus = VecDeque::new();
645        let mut rq = RenderQueue::new();
646
647        assert_eq!(value.value(), "/old/path");
648
649        let update_event = Event::Settings(SettingsEvent::UpdateValue {
650            kind: SettingIdentity::LibraryPath(0),
651            value: "/new/path".to_string(),
652        });
653        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
654
655        assert!(
656            handled,
657            "UpdateValue event should be handled when kind matches"
658        );
659        assert_eq!(value.value(), "/new/path");
660    }
661
662    #[test]
663    fn test_update_value_event_ignores_wrong_kind() {
664        use crate::settings::LibrarySettings;
665        let mut context = create_test_context();
666        context.settings.libraries.clear();
667        context.settings.libraries.push(LibrarySettings {
668            name: "Test Library".to_string(),
669            path: PathBuf::from("/tmp"),
670            ..Default::default()
671        });
672        let rect = rect![0, 0, 200, 50];
673
674        let mut value =
675            SettingValue::new(LibraryName(0), rect, &context.settings, &mut context.fonts);
676        let (hub, _receiver) = channel();
677        let mut bus = VecDeque::new();
678        let mut rq = RenderQueue::new();
679
680        assert_eq!(value.value(), "Test Library");
681
682        let update_event = Event::Settings(SettingsEvent::UpdateValue {
683            kind: SettingIdentity::LibraryPath(0),
684            value: "Some Path".to_string(),
685        });
686        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
687
688        assert!(
689            !handled,
690            "UpdateValue event should not be handled when kind does not match"
691        );
692        assert_eq!(
693            value.value(),
694            "Test Library",
695            "Value should not change when kind mismatches"
696        );
697    }
698
699    #[test]
700    fn test_update_value_event_ignores_wrong_index() {
701        use crate::settings::LibrarySettings;
702        let mut context = create_test_context();
703        context.settings.libraries.clear();
704        context.settings.libraries.push(LibrarySettings {
705            name: "Library 0".to_string(),
706            path: PathBuf::from("/path0"),
707            ..Default::default()
708        });
709        context.settings.libraries.push(LibrarySettings {
710            name: "Library 1".to_string(),
711            path: PathBuf::from("/path1"),
712            ..Default::default()
713        });
714        let rect = rect![0, 0, 200, 50];
715
716        let mut value =
717            SettingValue::new(LibraryName(0), rect, &context.settings, &mut context.fonts);
718        let (hub, _receiver) = channel();
719        let mut bus = VecDeque::new();
720        let mut rq = RenderQueue::new();
721
722        assert_eq!(value.value(), "Library 0");
723
724        let update_event = Event::Settings(SettingsEvent::UpdateValue {
725            kind: SettingIdentity::LibraryName(1),
726            value: "Updated Library 1".to_string(),
727        });
728        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
729
730        assert!(
731            !handled,
732            "UpdateValue event should not be handled when index does not match"
733        );
734        assert_eq!(
735            value.value(),
736            "Library 0",
737            "Value should not change when index mismatches"
738        );
739    }
740
741    #[test]
742    fn test_update_value_event_updates_auto_suspend() {
743        let rect = rect![0, 0, 200, 50];
744        let mut context = create_test_context();
745        let mut value =
746            SettingValue::new(&AutoSuspend, rect, &context.settings, &mut context.fonts);
747        let (hub, _receiver) = channel();
748        let mut bus = VecDeque::new();
749        let mut rq = RenderQueue::new();
750
751        assert_eq!(value.value(), "30.0");
752
753        let update_event = Event::Settings(SettingsEvent::UpdateValue {
754            kind: SettingIdentity::AutoSuspend,
755            value: "60.0".to_string(),
756        });
757        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
758
759        assert!(
760            handled,
761            "UpdateValue event should be handled when kind matches"
762        );
763        assert_eq!(value.value(), "60.0");
764    }
765
766    #[test]
767    fn test_update_value_event_updates_auto_power_off() {
768        let rect = rect![0, 0, 200, 50];
769        let mut context = create_test_context();
770        let mut value =
771            SettingValue::new(&AutoPowerOff, rect, &context.settings, &mut context.fonts);
772        let (hub, _receiver) = channel();
773        let mut bus = VecDeque::new();
774        let mut rq = RenderQueue::new();
775
776        assert_eq!(value.value(), "3.0");
777
778        let update_event = Event::Settings(SettingsEvent::UpdateValue {
779            kind: SettingIdentity::AutoPowerOff,
780            value: "60.0".to_string(),
781        });
782        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
783
784        assert!(
785            handled,
786            "UpdateValue event should be handled when kind matches"
787        );
788        assert_eq!(value.value(), "60.0");
789    }
790
791    #[test]
792    fn test_update_value_event_updates_settings_retention() {
793        let rect = rect![0, 0, 200, 50];
794        let mut context = create_test_context();
795        let mut value = SettingValue::new(
796            &SettingsRetention,
797            rect,
798            &context.settings,
799            &mut context.fonts,
800        );
801        let (hub, _receiver) = channel();
802        let mut bus = VecDeque::new();
803        let mut rq = RenderQueue::new();
804
805        assert_eq!(value.value(), "3");
806
807        let update_event = Event::Settings(SettingsEvent::UpdateValue {
808            kind: SettingIdentity::SettingsRetention,
809            value: "5".to_string(),
810        });
811        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
812
813        assert!(
814            handled,
815            "UpdateValue event should be handled when kind matches"
816        );
817        assert_eq!(value.value(), "5");
818    }
819
820    #[test]
821    fn test_update_value_event_regenerates_log_level_radio_buttons() {
822        let rect = rect![0, 0, 200, 50];
823        let mut context = create_test_context();
824        context.settings.logging.level = "INFO".to_string();
825
826        let mut value = SettingValue::new(&LogLevel, rect, &context.settings, &mut context.fonts);
827        let (hub, _receiver) = channel();
828        let mut bus = VecDeque::new();
829        let mut rq = RenderQueue::new();
830
831        let initial_entries = value.entries.clone();
832        assert_eq!(initial_entries.len(), 5);
833
834        let info_entry = initial_entries.iter().find(|e| {
835            if let EntryKind::RadioButton(label, _, _) = e {
836                label == "INFO"
837            } else {
838                false
839            }
840        });
841        assert!(
842            matches!(info_entry, Some(EntryKind::RadioButton(_, _, true))),
843            "INFO should be initially checked"
844        );
845
846        context.settings.logging.level = "DEBUG".to_string();
847        let update_event = Event::Settings(SettingsEvent::UpdateValue {
848            kind: SettingIdentity::LogLevel,
849            value: "DEBUG".to_string(),
850        });
851        let handled = value.handle_event(&update_event, &hub, &mut bus, &mut rq, &mut context);
852
853        assert!(
854            handled,
855            "UpdateValue event should be handled when kind matches"
856        );
857        assert_eq!(value.value(), "DEBUG");
858    }
859}