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