Skip to main content

cadmus_core/view/settings_editor/
library_editor.rs

1use super::bottom_bar::BottomBarVariant;
2use super::editor_utils::{
3    build_bottom_separator, build_two_button_bottom_bar, calculate_dimensions,
4};
5use super::kinds::library::{LibraryFinishedAction, LibraryName, LibraryPath};
6use super::kinds::SettingIdentity;
7use super::setting_row::SettingRow;
8use super::setting_value::SettingsEvent;
9use crate::color::WHITE;
10use crate::context::Context;
11use crate::device::CURRENT_DEVICE;
12use crate::fl;
13use crate::font::Fonts;
14use crate::framebuffer::{Framebuffer, UpdateMode};
15use crate::geom::Rectangle;
16use crate::gesture::GestureEvent;
17use crate::settings::{FinishedAction, LibrarySettings, Settings};
18use crate::unit::scale_by_dpi;
19use crate::view::common::locate_by_id;
20use crate::view::file_chooser::{FileChooser, SelectionMode};
21use crate::view::filler::Filler;
22use crate::view::menu::{Menu, MenuKind};
23use crate::view::named_input::NamedInput;
24use crate::view::toggleable_keyboard::ToggleableKeyboard;
25use crate::view::SMALL_BAR_HEIGHT;
26use crate::view::{Bus, Event, Hub, Id, RenderData, RenderQueue, View, ViewId, ID_FEEDER};
27use crate::view::{EntryId, NotificationEvent};
28
29/// A view for editing library settings.
30///
31/// The `LibraryEditor` provides a user interface for configuring library properties
32/// such as name, path, and mode. It manages a collection of child views including
33/// setting rows, a keyboard for text input, and various overlays (dialogs, menus).
34///
35/// # Fields
36///
37/// * `id` - Unique identifier for this view
38/// * `rect` - The rectangular area occupied by this editor
39/// * `children` - Child views including separators, rows, bars, and overlays
40/// * `library_index` - Index of the library being edited in the settings
41/// * `library` - Current library settings being edited
42/// * `_original_library` - Original library settings before modifications (for potential rollback)
43/// * `focus` - The currently focused child view, if any
44/// * `keyboard_index` - Index of the keyboard view in the children vector
45pub struct LibraryEditor {
46    id: Id,
47    rect: Rectangle,
48    children: Vec<Box<dyn View>>,
49    library_index: usize,
50    library: LibrarySettings,
51    _original_library: LibrarySettings,
52    focus: Option<ViewId>,
53    keyboard_index: usize,
54}
55
56impl LibraryEditor {
57    #[cfg_attr(feature = "tracing", tracing::instrument(skip(_hub, context, rq)))]
58    pub fn new(
59        rect: Rectangle,
60        library_index: usize,
61        library: LibrarySettings,
62        _hub: &Hub,
63        rq: &mut RenderQueue,
64        context: &mut Context,
65    ) -> LibraryEditor {
66        let id = ID_FEEDER.next();
67        let mut children = Vec::new();
68
69        let mut settings = context.settings.clone();
70        if library_index <= settings.libraries.len() {
71            settings.libraries.insert(library_index, library.clone());
72        }
73        let settings = settings;
74
75        children.push(Box::new(Filler::new(rect, WHITE)) as Box<dyn View>);
76
77        let (bar_height, separator_thickness, separator_top_half, separator_bottom_half) =
78            calculate_dimensions();
79
80        children.extend(Self::build_content_rows(
81            rect,
82            bar_height,
83            separator_thickness,
84            library_index,
85            &settings,
86            &mut context.fonts,
87        ));
88
89        children.push(build_bottom_separator(
90            rect,
91            bar_height,
92            separator_top_half,
93            separator_bottom_half,
94        ));
95        children.push(Self::build_bottom_bar(
96            rect,
97            bar_height,
98            separator_bottom_half,
99        ));
100
101        let keyboard = ToggleableKeyboard::new(rect, false);
102        children.push(Box::new(keyboard) as Box<dyn View>);
103
104        let keyboard_index = children.len() - 1;
105
106        rq.add(RenderData::new(id, rect, UpdateMode::Gui));
107
108        LibraryEditor {
109            id,
110            rect,
111            children,
112            library_index,
113            library: library.clone(),
114            _original_library: library,
115            focus: None,
116            keyboard_index,
117        }
118    }
119
120    #[inline]
121    fn build_content_rows(
122        rect: Rectangle,
123        bar_height: i32,
124        separator_thickness: i32,
125        library_index: usize,
126        settings: &Settings,
127        fonts: &mut crate::font::Fonts,
128    ) -> Vec<Box<dyn View>> {
129        let mut children = Vec::new();
130        let dpi = CURRENT_DEVICE.dpi;
131        let row_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
132
133        let content_start_y = rect.min.y;
134        let content_end_y = rect.max.y - bar_height - separator_thickness;
135
136        let mut current_y = content_start_y;
137
138        if current_y + row_height <= content_end_y {
139            let name_row_rect = rect![rect.min.x, current_y, rect.max.x, current_y + row_height];
140            children.push(Self::build_name_row(
141                name_row_rect,
142                library_index,
143                settings,
144                fonts,
145            ));
146            current_y += row_height;
147        }
148
149        if current_y + row_height <= content_end_y {
150            let path_row_rect = rect![rect.min.x, current_y, rect.max.x, current_y + row_height];
151            children.push(Self::build_path_row(
152                path_row_rect,
153                library_index,
154                settings,
155                fonts,
156            ));
157            current_y += row_height;
158        }
159
160        if current_y + row_height <= content_end_y {
161            let finished_row_rect =
162                rect![rect.min.x, current_y, rect.max.x, current_y + row_height];
163            children.push(Self::build_finished_action_row(
164                finished_row_rect,
165                library_index,
166                settings,
167                fonts,
168            ));
169        }
170
171        children
172    }
173
174    #[inline]
175    fn build_name_row(
176        rect: Rectangle,
177        library_index: usize,
178        settings: &Settings,
179        fonts: &mut crate::font::Fonts,
180    ) -> Box<dyn View> {
181        Box::new(SettingRow::new(
182            Box::new(LibraryName(library_index)),
183            rect,
184            settings,
185            fonts,
186        )) as Box<dyn View>
187    }
188
189    fn build_path_row(
190        rect: Rectangle,
191        library_index: usize,
192        settings: &Settings,
193        fonts: &mut crate::font::Fonts,
194    ) -> Box<dyn View> {
195        Box::new(SettingRow::new(
196            Box::new(LibraryPath(library_index)),
197            rect,
198            settings,
199            fonts,
200        )) as Box<dyn View>
201    }
202
203    #[inline]
204    fn build_finished_action_row(
205        rect: Rectangle,
206        library_index: usize,
207        settings: &Settings,
208        fonts: &mut crate::font::Fonts,
209    ) -> Box<dyn View> {
210        Box::new(SettingRow::new(
211            Box::new(LibraryFinishedAction(library_index)),
212            rect,
213            settings,
214            fonts,
215        )) as Box<dyn View>
216    }
217
218    #[inline]
219    fn build_bottom_bar(
220        rect: Rectangle,
221        bar_height: i32,
222        separator_bottom_half: i32,
223    ) -> Box<dyn View> {
224        build_two_button_bottom_bar(
225            rect,
226            bar_height,
227            separator_bottom_half,
228            BottomBarVariant::TwoButtons {
229                left_event: Event::Close(ViewId::LibraryEditor),
230                left_icon: "close",
231                right_event: Event::Validate,
232                right_icon: "check_mark-large",
233            },
234        )
235    }
236
237    #[inline]
238    fn toggle_keyboard(
239        &mut self,
240        visible: bool,
241        _id: Option<ViewId>,
242        hub: &Hub,
243        rq: &mut RenderQueue,
244        context: &mut Context,
245    ) {
246        let keyboard = self.children[self.keyboard_index]
247            .downcast_mut::<ToggleableKeyboard>()
248            .expect("keyboard_index points to non-ToggleableKeyboard view");
249        keyboard.set_visible(visible, hub, rq, context);
250    }
251
252    #[inline]
253    fn handle_focus_event(
254        &mut self,
255        focus: Option<ViewId>,
256        hub: &Hub,
257        rq: &mut RenderQueue,
258        context: &mut Context,
259    ) -> bool {
260        if self.focus != focus {
261            self.focus = focus;
262            if focus.is_some() {
263                self.toggle_keyboard(true, focus, hub, rq, context);
264            } else {
265                self.toggle_keyboard(false, None, hub, rq, context);
266            }
267        }
268        true
269    }
270
271    #[inline]
272    fn handle_validate_event(&self, hub: &Hub, bus: &mut Bus) -> bool {
273        if self.library.name.trim().is_empty() {
274            hub.send(Event::Notification(NotificationEvent::Show(
275                "Library name cannot be empty".to_string(),
276            )))
277            .ok();
278            return true;
279        }
280
281        if !self.library.path.exists() {
282            hub.send(Event::Notification(NotificationEvent::Show(
283                "Path does not exist".to_string(),
284            )))
285            .ok();
286            return true;
287        }
288
289        bus.push_back(Event::UpdateLibrary(
290            self.library_index,
291            Box::new(self.library.clone()),
292        ));
293        bus.push_back(Event::Close(ViewId::LibraryEditor));
294
295        true
296    }
297
298    #[inline]
299    fn handle_edit_name_event(
300        &mut self,
301        hub: &Hub,
302        rq: &mut RenderQueue,
303        context: &mut Context,
304    ) -> bool {
305        let mut name_input = NamedInput::new(
306            "Library Name".to_string(),
307            ViewId::LibraryRename,
308            ViewId::LibraryRenameInput,
309            10,
310            context,
311        );
312        name_input.set_text(&self.library.name, rq, context);
313
314        self.children.push(Box::new(name_input));
315        rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
316
317        hub.send(Event::Focus(Some(ViewId::LibraryRenameInput)))
318            .ok();
319        true
320    }
321
322    #[inline]
323    fn handle_edit_path_event(
324        &mut self,
325        hub: &Hub,
326        rq: &mut RenderQueue,
327        context: &mut Context,
328    ) -> bool {
329        let screen_rect = rect!(
330            0,
331            0,
332            context.display.dims.0 as i32,
333            context.display.dims.1 as i32
334        );
335
336        let file_chooser = FileChooser::new(
337            screen_rect,
338            self.library.path.clone(),
339            SelectionMode::Directory,
340            hub,
341            rq,
342            context,
343        );
344        self.children.push(Box::new(file_chooser));
345        rq.add(RenderData::new(self.id, screen_rect, UpdateMode::Gui));
346
347        true
348    }
349
350    #[inline]
351    fn handle_submit_name_event(&mut self, text: &str, bus: &mut Bus) -> bool {
352        self.library.name = text.to_string();
353        bus.push_back(Event::Settings(SettingsEvent::UpdateValue {
354            kind: SettingIdentity::LibraryName(self.library_index),
355            value: text.to_string(),
356        }));
357        false
358    }
359
360    #[inline]
361    fn handle_set_library_finished_action(
362        &mut self,
363        index: usize,
364        action: FinishedAction,
365        bus: &mut Bus,
366    ) -> bool {
367        if index != self.library_index {
368            return false;
369        }
370        self.library.finished = Some(action);
371        bus.push_back(Event::Settings(SettingsEvent::UpdateValue {
372            kind: SettingIdentity::LibraryFinishedAction(self.library_index),
373            value: action.to_string(),
374        }));
375        true
376    }
377
378    #[inline]
379    fn handle_clear_library_finished_action(&mut self, index: usize, bus: &mut Bus) -> bool {
380        if index != self.library_index {
381            return false;
382        }
383        self.library.finished = None;
384        bus.push_back(Event::Settings(SettingsEvent::UpdateValue {
385            kind: SettingIdentity::LibraryFinishedAction(self.library_index),
386            value: fl!("settings-library-inherit"),
387        }));
388        true
389    }
390
391    #[inline]
392    fn handle_file_chooser_closed_event(
393        &mut self,
394        path: &Option<std::path::PathBuf>,
395        bus: &mut Bus,
396    ) -> bool {
397        if let Some(path) = path {
398            self.library.path = path.clone();
399            bus.push_back(Event::Settings(SettingsEvent::UpdateValue {
400                kind: SettingIdentity::LibraryPath(self.library_index),
401                value: path.display().to_string(),
402            }));
403        }
404        false
405    }
406
407    #[inline]
408    fn handle_submenu_event(
409        &mut self,
410        rect: Rectangle,
411        entries: &[crate::view::EntryKind],
412        rq: &mut RenderQueue,
413        context: &mut Context,
414    ) -> bool {
415        let menu = Menu::new(
416            rect,
417            ViewId::SettingsValueMenu,
418            MenuKind::Contextual,
419            entries.to_vec(),
420            context,
421        );
422        rq.add(RenderData::new(menu.id(), *menu.rect(), UpdateMode::Gui));
423        self.children.push(Box::new(menu));
424        true
425    }
426
427    /// Handles closing of overlay views (menus, dialogs, file choosers).
428    ///
429    /// Removes the specified view from the children vector and triggers a re-render.
430    /// When closing the rename dialog, also clears focus to hide the keyboard.
431    ///
432    /// # Arguments
433    ///
434    /// * `view_id` - The ID of the view to close
435    /// * `hub` - Event hub for sending focus events
436    /// * `rq` - Render queue for scheduling screen updates
437    ///
438    /// # Returns
439    ///
440    /// `true` if the event was handled and should not propagate to parent,
441    /// `false` if the parent should handle additional cleanup (e.g., FileChooser
442    /// requires the parent to redraw the entire screen as it temporarily captures
443    /// the full display area).
444    #[inline]
445    fn handle_close_event(&mut self, view_id: ViewId, hub: &Hub, rq: &mut RenderQueue) -> bool {
446        match view_id {
447            ViewId::SettingsValueMenu => {
448                if let Some(index) = locate_by_id(self, ViewId::SettingsValueMenu) {
449                    self.children.remove(index);
450                    rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
451                }
452                true
453            }
454            ViewId::LibraryRename => {
455                if let Some(index) = locate_by_id(self, ViewId::LibraryRename) {
456                    self.children.remove(index);
457                    rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
458                }
459                hub.send(Event::Focus(None)).ok();
460                true
461            }
462            ViewId::FileChooser => {
463                if let Some(index) = locate_by_id(self, ViewId::FileChooser) {
464                    self.children.remove(index);
465                }
466                false
467            }
468            _ => false,
469        }
470    }
471}
472
473impl View for LibraryEditor {
474    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, hub, bus, rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
475    fn handle_event(
476        &mut self,
477        evt: &Event,
478        hub: &Hub,
479        bus: &mut Bus,
480        rq: &mut RenderQueue,
481        context: &mut Context,
482    ) -> bool {
483        match *evt {
484            Event::Gesture(GestureEvent::HoldFingerShort(_, _)) => true,
485            Event::Focus(v) => self.handle_focus_event(v, hub, rq, context),
486            Event::Validate => self.handle_validate_event(hub, bus),
487            Event::Select(EntryId::EditLibraryName) => {
488                self.handle_edit_name_event(hub, rq, context)
489            }
490            Event::Select(EntryId::EditLibraryPath) => {
491                self.handle_edit_path_event(hub, rq, context)
492            }
493            Event::Select(EntryId::SetLibraryFinishedAction(index, action)) => {
494                self.handle_set_library_finished_action(index, action, bus)
495            }
496            Event::Select(EntryId::ClearLibraryFinishedAction(index)) => {
497                self.handle_clear_library_finished_action(index, bus)
498            }
499            Event::Submit(ViewId::LibraryRenameInput, ref text) => {
500                self.handle_submit_name_event(text, bus)
501            }
502            Event::FileChooserClosed(ref path) => self.handle_file_chooser_closed_event(path, bus),
503            Event::SubMenu(rect, ref entries) => {
504                self.handle_submenu_event(rect, entries, rq, context)
505            }
506            Event::Close(view) => self.handle_close_event(view, hub, rq),
507            _ => false,
508        }
509    }
510
511    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
512    fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {}
513
514    fn rect(&self) -> &Rectangle {
515        &self.rect
516    }
517
518    fn rect_mut(&mut self) -> &mut Rectangle {
519        &mut self.rect
520    }
521
522    fn children(&self) -> &Vec<Box<dyn View>> {
523        &self.children
524    }
525
526    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
527        &mut self.children
528    }
529
530    fn id(&self) -> Id {
531        self.id
532    }
533
534    fn view_id(&self) -> Option<ViewId> {
535        Some(ViewId::LibraryEditor)
536    }
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542    use crate::context::test_helpers::create_test_context;
543    use std::collections::VecDeque;
544    use std::sync::mpsc::channel;
545
546    fn create_test_library() -> LibrarySettings {
547        LibrarySettings {
548            name: "Test Library".to_string(),
549            path: std::path::PathBuf::from("/tmp"),
550            ..Default::default()
551        }
552    }
553
554    #[test]
555    fn test_validate_empty_name_shows_notification() {
556        let mut context = create_test_context();
557        let rect = rect![0, 0, 600, 800];
558        let (hub, receiver) = channel();
559        let mut rq = RenderQueue::new();
560
561        let mut library = create_test_library();
562        library.name = "".to_string();
563
564        let mut editor = LibraryEditor::new(rect, 0, library, &hub, &mut rq, &mut context);
565
566        let mut bus = VecDeque::new();
567
568        let handled = editor.handle_event(&Event::Validate, &hub, &mut bus, &mut rq, &mut context);
569
570        assert!(handled);
571        assert_eq!(bus.len(), 0);
572
573        if let Ok(Event::Notification(NotificationEvent::Show(msg))) = receiver.try_recv() {
574            assert_eq!(msg, "Library name cannot be empty");
575        } else {
576            panic!("Expected notification event about empty name");
577        }
578    }
579
580    #[test]
581    fn test_validate_nonexistent_path_shows_notification() {
582        let mut context = create_test_context();
583        let rect = rect![0, 0, 600, 800];
584        let (hub, receiver) = channel();
585        let mut rq = RenderQueue::new();
586
587        let mut library = create_test_library();
588        library.path = std::path::PathBuf::from("/nonexistent/path/that/does/not/exist");
589
590        let mut editor = LibraryEditor::new(rect, 0, library, &hub, &mut rq, &mut context);
591
592        let mut bus = VecDeque::new();
593
594        let handled = editor.handle_event(&Event::Validate, &hub, &mut bus, &mut rq, &mut context);
595
596        assert!(handled);
597        assert_eq!(bus.len(), 0);
598
599        if let Ok(Event::Notification(NotificationEvent::Show(msg))) = receiver.try_recv() {
600            assert_eq!(msg, "Path does not exist");
601        } else {
602            panic!("Expected notification event about nonexistent path");
603        }
604    }
605
606    #[test]
607    fn test_validate_success_emits_update_and_close() {
608        let mut context = create_test_context();
609        let rect = rect![0, 0, 600, 800];
610        let (hub, _receiver) = channel();
611        let mut rq = RenderQueue::new();
612
613        let library = create_test_library();
614        let library_index = 0;
615
616        let mut editor = LibraryEditor::new(
617            rect,
618            library_index,
619            library.clone(),
620            &hub,
621            &mut rq,
622            &mut context,
623        );
624
625        let mut bus = VecDeque::new();
626
627        let handled = editor.handle_event(&Event::Validate, &hub, &mut bus, &mut rq, &mut context);
628
629        assert!(handled);
630        assert_eq!(bus.len(), 2);
631
632        if let Some(Event::UpdateLibrary(idx, lib)) = bus.pop_front() {
633            assert_eq!(idx, library_index);
634            assert_eq!(lib.name, library.name);
635        } else {
636            panic!("Expected UpdateLibrary event");
637        }
638
639        if let Some(Event::Close(view_id)) = bus.pop_front() {
640            assert_eq!(view_id, ViewId::LibraryEditor);
641        } else {
642            panic!("Expected Close event");
643        }
644    }
645
646    #[test]
647    fn test_edit_library_name_opens_input() {
648        let mut context = create_test_context();
649        let rect = rect![0, 0, 600, 800];
650        let (hub, receiver) = channel();
651        let mut rq = RenderQueue::new();
652
653        let library = create_test_library();
654
655        let mut editor = LibraryEditor::new(rect, 0, library, &hub, &mut rq, &mut context);
656
657        let initial_children_count = editor.children.len();
658
659        let mut bus = VecDeque::new();
660
661        let handled = editor.handle_event(
662            &Event::Select(EntryId::EditLibraryName),
663            &hub,
664            &mut bus,
665            &mut rq,
666            &mut context,
667        );
668
669        assert!(handled);
670        assert_eq!(editor.children.len(), initial_children_count + 1);
671        assert!(!rq.is_empty());
672
673        if let Ok(Event::Focus(Some(ViewId::LibraryRenameInput))) = receiver.try_recv() {
674        } else {
675            panic!("Expected Focus event for LibraryRenameInput");
676        }
677    }
678
679    #[test]
680    fn test_edit_library_path_opens_file_chooser() {
681        let mut context = create_test_context();
682        let rect = rect![0, 0, 600, 800];
683        let (hub, _receiver) = channel();
684        let mut rq = RenderQueue::new();
685
686        let library = create_test_library();
687
688        let mut editor = LibraryEditor::new(rect, 0, library, &hub, &mut rq, &mut context);
689
690        let initial_children_count = editor.children.len();
691
692        let mut bus = VecDeque::new();
693
694        let handled = editor.handle_event(
695            &Event::Select(EntryId::EditLibraryPath),
696            &hub,
697            &mut bus,
698            &mut rq,
699            &mut context,
700        );
701
702        assert!(handled);
703        assert_eq!(editor.children.len(), initial_children_count + 1);
704        assert!(!rq.is_empty());
705    }
706
707    #[test]
708    fn test_file_chooser_closed_updates_path() {
709        let mut context = create_test_context();
710        let rect = rect![0, 0, 600, 800];
711        let (hub, _receiver) = channel();
712        let mut rq = RenderQueue::new();
713
714        let library = create_test_library();
715
716        let mut editor = LibraryEditor::new(rect, 0, library, &hub, &mut rq, &mut context);
717
718        let original_path = editor.library.path.clone();
719        let new_path = std::path::PathBuf::from("/mnt/onboard/newpath");
720
721        let mut bus = VecDeque::new();
722        rq = RenderQueue::new();
723
724        let handled = editor.handle_event(
725            &Event::FileChooserClosed(Some(new_path.clone())),
726            &hub,
727            &mut bus,
728            &mut rq,
729            &mut context,
730        );
731
732        assert!(!handled);
733        assert_ne!(editor.library.path, original_path);
734        assert_eq!(editor.library.path, new_path);
735        assert!(rq.is_empty());
736    }
737
738    #[test]
739    fn test_submit_library_name_updates_library() {
740        let mut context = create_test_context();
741        let rect = rect![0, 0, 600, 800];
742        let (hub, _receiver) = channel();
743        let mut rq = RenderQueue::new();
744
745        let library = create_test_library();
746
747        let mut editor = LibraryEditor::new(rect, 0, library, &hub, &mut rq, &mut context);
748
749        let original_name = editor.library.name.clone();
750        let new_name = "Updated Library Name".to_string();
751
752        let mut bus = VecDeque::new();
753        rq = RenderQueue::new();
754
755        let handled = editor.handle_event(
756            &Event::Submit(ViewId::LibraryRenameInput, new_name.clone()),
757            &hub,
758            &mut bus,
759            &mut rq,
760            &mut context,
761        );
762
763        assert!(!handled);
764        assert_ne!(editor.library.name, original_name);
765        assert_eq!(editor.library.name, new_name);
766        assert!(rq.is_empty());
767    }
768}