cadmus_core/view/settings_editor/
mod.rs

1//! Settings editor module for managing application configuration.
2//!
3//! This module provides a hierarchical settings interface with the following structure:
4//!
5//! ```text
6//! SettingsEditor (Main view)
7//!   ├── TopBar (Back button, "Settings" title)
8//!   ├── StackNavigationBar (Category tabs: General | Libraries | Intermissions)
9//!   └── CategoryEditor (Embedded, shows settings for selected category)
10//!       ├── SettingRow (One for each setting in the category)
11//!       │   ├── Label (Setting name)
12//!       │   └── SettingValue (Current value, can be tapped to edit)
13//!       └── BottomBar (Add Library button for Libraries category)
14//! ```
15//!
16//! ## Components
17//!
18//! - **SettingsEditor**: Top-level view with navigation bar and category editor
19//! - **CategoryNavigationBar**: Horizontal bar with category tabs
20//! - **CategoryEditor**: Embedded editor for a specific category's settings
21//! - **SettingRow**: Individual setting with label and value
22//! - **SettingValue**: Interactive value display that opens editors/menus
23//! - **LibraryEditor**: Specialized editor for library settings
24//!
25//! ## Event Flow
26//!
27//! When a setting is modified, the CategoryEditor directly updates `context.settings`,
28//! providing immediate feedback. Settings are persisted to disk when the settings editor
29//! is closed.
30
31use crate::color::BLACK;
32use crate::context::Context;
33use crate::device::CURRENT_DEVICE;
34use crate::framebuffer::{Framebuffer, UpdateMode};
35use crate::geom::{halves, Rectangle};
36use crate::unit::scale_by_dpi;
37use crate::view::common::toggle_main_menu;
38use crate::view::filler::Filler;
39use crate::view::navigation::stack_navigation_bar::StackNavigationBar;
40use crate::view::top_bar::{TopBar, TopBarVariant};
41use crate::view::{Bus, Event, Hub, Id, RenderData, RenderQueue, View, ViewId, ID_FEEDER};
42use crate::view::{SMALL_BAR_HEIGHT, THICKNESS_MEDIUM};
43
44mod bottom_bar;
45mod category;
46mod category_button;
47mod category_editor;
48mod category_navigation_bar;
49mod category_provider;
50mod library_editor;
51mod setting_row;
52mod setting_value;
53
54pub use setting_value::ToggleSettings;
55
56pub use self::bottom_bar::{BottomBarVariant, SettingsEditorBottomBar};
57pub use self::category::Category;
58pub use self::category_button::CategoryButton;
59pub use self::category_editor::CategoryEditor;
60pub use self::category_navigation_bar::CategoryNavigationBar;
61pub use self::category_provider::SettingsCategoryProvider;
62pub use self::setting_row::{Kind as RowKind, SettingRow};
63pub use self::setting_value::SettingValue;
64
65/// Main settings editor view.
66///
67/// This is the top-level view that displays a navigation bar with category tabs
68/// and an embedded category editor below it. When a category tab is selected,
69/// the editor switches to show that category's settings.
70///
71/// # Structure
72///
73/// - `id`: Unique identifier for this view
74/// - `rect`: Bounding rectangle for the entire settings editor
75/// - `children`: Child views including the top bar, separators, navigation bar, and category editor
76/// - `nav_bar_index`: Index of the StackNavigationBar in the children vector
77/// - `editor_index`: Index of the CategoryEditor in the children vector
78pub struct SettingsEditor {
79    id: Id,
80    rect: Rectangle,
81    children: Vec<Box<dyn View>>,
82    nav_bar_index: usize,
83    editor_index: usize,
84}
85
86impl SettingsEditor {
87    pub fn new(rect: Rectangle, rq: &mut RenderQueue, context: &mut Context) -> Self {
88        let id = ID_FEEDER.next();
89        let mut children = Vec::new();
90
91        let (bar_height, _separator_thickness, separator_top_half, separator_bottom_half) =
92            Self::calculate_dimensions();
93
94        children.push(Self::build_top_bar(
95            &rect,
96            bar_height,
97            separator_top_half,
98            context,
99        ));
100
101        children.push(Self::build_top_separator(
102            &rect,
103            bar_height,
104            separator_top_half,
105            separator_bottom_half,
106        ));
107
108        let nav_bar_rect = rect![
109            rect.min.x,
110            rect.min.y + bar_height + separator_bottom_half,
111            rect.max.x,
112            rect.min.y + bar_height + separator_bottom_half + bar_height
113        ];
114
115        let provider = SettingsCategoryProvider;
116        let mut navigation_bar =
117            StackNavigationBar::new(nav_bar_rect, rect.max.y, 1, provider, Category::General)
118                .disable_resize();
119
120        navigation_bar.set_selected(Category::General, rq, context);
121        let nav_bar_index = children.len();
122        children.push(Box::new(navigation_bar));
123
124        let content_rect = rect![
125            rect.min.x,
126            children[nav_bar_index].rect().max.y,
127            rect.max.x,
128            rect.max.y
129        ];
130
131        let category_editor = CategoryEditor::new(content_rect, Category::General, rq, context);
132
133        let editor_index = children.len();
134        children.push(Box::new(category_editor));
135
136        rq.add(RenderData::new(id, rect, UpdateMode::Gui));
137
138        SettingsEditor {
139            id,
140            rect,
141            children,
142            nav_bar_index,
143            editor_index,
144        }
145    }
146
147    fn calculate_dimensions() -> (i32, i32, i32, i32) {
148        let dpi = CURRENT_DEVICE.dpi;
149        let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
150        let separator_thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
151        let (separator_top_half, separator_bottom_half) = halves(separator_thickness);
152        let bar_height = small_height;
153
154        (
155            bar_height,
156            separator_thickness,
157            separator_top_half,
158            separator_bottom_half,
159        )
160    }
161
162    fn build_top_bar(
163        rect: &Rectangle,
164        bar_height: i32,
165        separator_top_half: i32,
166        context: &mut Context,
167    ) -> Box<dyn View> {
168        let top_bar = TopBar::new(
169            rect![
170                rect.min.x,
171                rect.min.y,
172                rect.max.x,
173                rect.min.y + bar_height - separator_top_half
174            ],
175            TopBarVariant::Back,
176            "Settings".to_string(),
177            context,
178        );
179        Box::new(top_bar) as Box<dyn View>
180    }
181
182    fn build_top_separator(
183        rect: &Rectangle,
184        bar_height: i32,
185        separator_top_half: i32,
186        separator_bottom_half: i32,
187    ) -> Box<dyn View> {
188        let separator = Filler::new(
189            rect![
190                rect.min.x,
191                rect.min.y + bar_height - separator_top_half,
192                rect.max.x,
193                rect.min.y + bar_height + separator_bottom_half
194            ],
195            BLACK,
196        );
197        Box::new(separator) as Box<dyn View>
198    }
199}
200
201impl View for SettingsEditor {
202    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, _bus, rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
203    fn handle_event(
204        &mut self,
205        evt: &Event,
206        _hub: &Hub,
207        _bus: &mut Bus,
208        rq: &mut RenderQueue,
209        context: &mut Context,
210    ) -> bool {
211        match evt {
212            Event::FileChooserClosed(_) => {
213                rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
214                true
215            }
216            Event::SelectSettingsCategory(category) => {
217                let nav_bar_max_y = {
218                    let nav_bar = self.children[self.nav_bar_index]
219                        .downcast_mut::<StackNavigationBar<SettingsCategoryProvider>>()
220                        .unwrap();
221
222                    nav_bar.set_selected(*category, rq, context);
223                    nav_bar.rect.max.y
224                };
225
226                self.children.remove(self.editor_index);
227
228                let content_rect = rect![
229                    self.rect.min.x,
230                    nav_bar_max_y,
231                    self.rect.max.x,
232                    self.rect.max.y
233                ];
234
235                let new_editor = CategoryEditor::new(content_rect, *category, rq, context);
236                self.children
237                    .insert(self.editor_index, Box::new(new_editor));
238
239                rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
240
241                true
242            }
243            Event::NavigationBarResized(_) => {
244                unimplemented!("The settings navigation bar should not be resizable which means this event is not expected to be send.")
245            }
246            Event::ToggleNear(ViewId::MainMenu, rect) => {
247                toggle_main_menu(self, *rect, None, rq, context);
248                true
249            }
250            Event::Close(ViewId::MainMenu) => {
251                toggle_main_menu(self, Rectangle::default(), Some(false), rq, context);
252                true
253            }
254            Event::Close(view_id) => match view_id {
255                ViewId::MainMenu => {
256                    toggle_main_menu(self, Rectangle::default(), Some(false), rq, context);
257                    true
258                }
259                ViewId::FileChooser => {
260                    rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
261                    true
262                }
263                _ => false,
264            },
265            _ => false,
266        }
267    }
268
269    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
270    fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut crate::font::Fonts) {
271    }
272
273    fn rect(&self) -> &Rectangle {
274        &self.rect
275    }
276
277    fn rect_mut(&mut self) -> &mut Rectangle {
278        &mut self.rect
279    }
280
281    fn children(&self) -> &Vec<Box<dyn View>> {
282        &self.children
283    }
284
285    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
286        &mut self.children
287    }
288
289    fn id(&self) -> Id {
290        self.id
291    }
292
293    fn is_background(&self) -> bool {
294        true
295    }
296}