cadmus_core/view/settings_editor/
library_editor.rs

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