cadmus_core/view/settings_editor/
setting_row.rs

1use super::super::label::Label;
2use super::super::Align;
3use super::super::{Bus, Event, Hub, Id, RenderQueue, View, ID_FEEDER};
4use super::setting_value::{Kind as ValueKind, SettingValue};
5use crate::context::Context;
6use crate::framebuffer::Framebuffer;
7use crate::geom::Rectangle;
8use crate::settings::Settings;
9use crate::view::settings_editor::ToggleSettings;
10
11pub enum Kind {
12    KeyboardLayout,
13    SleepCover,
14    AutoShare,
15    AutoSuspend,
16    AutoPowerOff,
17    ButtonScheme,
18    Library(usize),
19    LibraryName(usize),
20    LibraryPath(usize),
21    LibraryMode(usize),
22    IntermissionSuspend,
23    IntermissionPowerOff,
24    IntermissionShare,
25    SettingsRetention,
26}
27
28impl Kind {
29    /// Returns the human-readable label for this setting kind.
30    ///
31    /// # Arguments
32    ///
33    /// * `settings` - The current settings, used to look up dynamic labels (e.g., library names)
34    ///
35    /// # Returns
36    ///
37    /// A `String` containing the display label for this setting
38    pub fn label(&self, settings: &Settings) -> String {
39        match self {
40            Kind::KeyboardLayout => "Keyboard Layout".to_string(),
41            Kind::SleepCover => "Enable Sleep Cover".to_string(),
42            Kind::AutoShare => "Enable Auto Share".to_string(),
43            Kind::AutoSuspend => "Auto Suspend (minutes)".to_string(),
44            Kind::AutoPowerOff => "Auto Power Off (days)".to_string(),
45            Kind::ButtonScheme => "Button Scheme".to_string(),
46            Kind::Library(index) => settings
47                .libraries
48                .get(*index)
49                .map(|lib| lib.name.clone())
50                .unwrap_or_else(|| "Unknown".to_string()),
51            Kind::LibraryName(_) => "Name".to_string(),
52            Kind::LibraryPath(_) => "Path".to_string(),
53            Kind::LibraryMode(_) => "Mode".to_string(),
54            Kind::IntermissionSuspend => "Suspend Screen".to_string(),
55            Kind::IntermissionPowerOff => "Power Off Screen".to_string(),
56            Kind::IntermissionShare => "Share Screen".to_string(),
57            Kind::SettingsRetention => "Settings Retention".to_string(),
58        }
59    }
60
61    fn value_kind(&self) -> ValueKind {
62        match self {
63            Kind::KeyboardLayout => ValueKind::KeyboardLayout,
64            Kind::SleepCover => ValueKind::Toggle(ToggleSettings::SleepCover),
65            Kind::AutoShare => ValueKind::Toggle(ToggleSettings::AutoShare),
66            Kind::AutoSuspend => ValueKind::AutoSuspend,
67            Kind::AutoPowerOff => ValueKind::AutoPowerOff,
68            Kind::ButtonScheme => ValueKind::Toggle(ToggleSettings::ButtonScheme),
69            Kind::Library(index) => ValueKind::LibraryInfo(*index),
70            Kind::LibraryName(index) => ValueKind::LibraryName(*index),
71            Kind::LibraryPath(index) => ValueKind::LibraryPath(*index),
72            Kind::LibraryMode(index) => ValueKind::LibraryMode(*index),
73            Kind::IntermissionSuspend => ValueKind::IntermissionSuspend,
74            Kind::IntermissionPowerOff => ValueKind::IntermissionPowerOff,
75            Kind::IntermissionShare => ValueKind::IntermissionShare,
76            Kind::SettingsRetention => ValueKind::SettingsRetention,
77        }
78    }
79}
80
81/// A row in the settings UI that displays a setting label and its corresponding value.
82///
83/// `SettingRow` is a composite view that contains two child views:
84/// - A `Label` displaying the human-readable name of the setting
85/// - A `SettingValue` displaying the current value and allowing modifications
86///
87/// # Fields
88///
89/// * `id` - Unique identifier for this view
90/// * `rect` - The rectangular area occupied by this row
91/// * `children` - Vector containing the label and value child views
92/// * `kind` - The type of setting this row represents
93pub struct SettingRow {
94    id: Id,
95    rect: Rectangle,
96    children: Vec<Box<dyn View>>,
97    kind: Kind,
98}
99
100impl SettingRow {
101    pub fn new(
102        kind: Kind,
103        rect: Rectangle,
104        settings: &Settings,
105        fonts: &mut crate::font::Fonts,
106    ) -> SettingRow {
107        let mut children = Vec::new();
108
109        let half_width = rect.width() as i32 / 2;
110        let label_rect = rect![rect.min.x, rect.min.y, rect.min.x + half_width, rect.max.y];
111        let value_rect = rect![rect.min.x + half_width, rect.min.y, rect.max.x, rect.max.y];
112
113        let label_text = kind.label(settings);
114        let label = Label::new(label_rect, label_text, Align::Left(50));
115        children.push(Box::new(label) as Box<dyn View>);
116
117        let setting_value = SettingValue::new(kind.value_kind(), value_rect, settings, fonts);
118        children.push(Box::new(setting_value) as Box<dyn View>);
119
120        SettingRow {
121            id: ID_FEEDER.next(),
122            rect,
123            children,
124            kind,
125        }
126    }
127}
128
129impl View for SettingRow {
130    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, _bus, rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
131    fn handle_event(
132        &mut self,
133        evt: &Event,
134        _hub: &Hub,
135        _bus: &mut Bus,
136        rq: &mut RenderQueue,
137        _context: &mut Context,
138    ) -> bool {
139        match evt {
140            Event::UpdateLibrary(index, ref library) => match &self.kind {
141                Kind::Library(our_index) => {
142                    if index == our_index {
143                        if let Some(name_view) = self.children.get_mut(0) {
144                            if let Some(name_label) = name_view.as_any_mut().downcast_mut::<Label>()
145                            {
146                                name_label.update(&library.name, rq);
147                                return true;
148                            }
149                        }
150                    }
151                    false
152                }
153                _ => false,
154            },
155            _ => false,
156        }
157    }
158
159    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
160    fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut crate::font::Fonts) {
161    }
162
163    fn rect(&self) -> &Rectangle {
164        &self.rect
165    }
166
167    fn rect_mut(&mut self) -> &mut Rectangle {
168        &mut self.rect
169    }
170
171    fn children(&self) -> &Vec<Box<dyn View>> {
172        &self.children
173    }
174
175    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
176        &mut self.children
177    }
178
179    fn id(&self) -> Id {
180        self.id
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::context::test_helpers::create_test_context;
188    use crate::settings::{LibraryMode, LibrarySettings};
189    use std::collections::VecDeque;
190    use std::path::PathBuf;
191    use std::sync::mpsc::channel;
192
193    fn create_test_settings() -> Settings {
194        let mut settings = Settings::default();
195        settings.libraries.clear();
196        settings.libraries.push(LibrarySettings {
197            name: "Test Library 0".to_string(),
198            path: PathBuf::from("/tmp/lib0"),
199            mode: LibraryMode::Filesystem,
200            ..Default::default()
201        });
202        settings.libraries.push(LibrarySettings {
203            name: "Test Library 1".to_string(),
204            path: PathBuf::from("/tmp/lib1"),
205            mode: LibraryMode::Database,
206            ..Default::default()
207        });
208        settings
209    }
210
211    #[test]
212    fn test_update_library_event_updates_matching_row() {
213        let mut context = create_test_context();
214        let settings = create_test_settings();
215        let rect = rect![0, 0, 400, 60];
216
217        let mut row = SettingRow::new(Kind::Library(0), rect, &settings, &mut context.fonts);
218
219        let (hub, _receiver) = channel();
220        let mut bus = VecDeque::new();
221        let mut rq = RenderQueue::new();
222
223        let updated_library = LibrarySettings {
224            name: "Updated Library Name".to_string(),
225            path: PathBuf::from("/tmp/updated"),
226            mode: LibraryMode::Database,
227            ..Default::default()
228        };
229
230        let event = Event::UpdateLibrary(0, Box::new(updated_library.clone()));
231        let handled = row.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
232
233        assert!(handled);
234        assert!(!rq.is_empty());
235    }
236
237    #[test]
238    fn test_update_library_event_ignores_non_matching() {
239        let mut context = create_test_context();
240        let settings = create_test_settings();
241        let rect = rect![0, 0, 400, 60];
242
243        let mut row = SettingRow::new(Kind::Library(0), rect, &settings, &mut context.fonts);
244
245        let (hub, _receiver) = channel();
246        let mut bus = VecDeque::new();
247        let mut rq = RenderQueue::new();
248
249        let updated_library = LibrarySettings {
250            name: "Updated Library 1".to_string(),
251            path: PathBuf::from("/tmp/lib1_updated"),
252            mode: LibraryMode::Database,
253            ..Default::default()
254        };
255
256        let event = Event::UpdateLibrary(1, Box::new(updated_library));
257        let handled = row.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
258
259        assert!(!handled);
260        assert!(rq.is_empty());
261    }
262}