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