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
22pub 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 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 #[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}