cadmus_core/view/settings_editor/
library_editor.rs

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