1use std::sync::Mutex;
6
7use crossterm::event::{KeyCode, KeyEvent, MouseEvent};
8use mecomp_prost::{CollectionBrief, SongBrief};
9use ratatui::{
10 layout::{Margin, Rect},
11 style::Style,
12 text::{Line, Span},
13 widgets::{Block, Borders, Scrollbar, ScrollbarOrientation},
14};
15use tokio::sync::mpsc::UnboundedSender;
16
17use crate::{
18 state::action::{Action, ViewAction},
19 ui::{
20 AppState,
21 colors::{TEXT_HIGHLIGHT, border_color},
22 components::{
23 Component, ComponentRender, RenderProps,
24 content_view::{ActiveView, views::generic::SortableItemView},
25 },
26 widgets::tree::{CheckTree, state::CheckTreeState},
27 },
28};
29
30use super::{
31 CollectionViewProps,
32 checktree_utils::create_collection_tree_leaf,
33 sort_mode::{NameSort, SongSort},
34 traits::SortMode,
35};
36
37#[allow(clippy::module_name_repetitions)]
38pub type CollectionView = SortableItemView<CollectionViewProps, SongSort, SongBrief>;
39
40pub struct LibraryCollectionsView {
41 pub action_tx: UnboundedSender<Action>,
43 props: Props,
45 tree_state: Mutex<CheckTreeState<String>>,
47}
48
49struct Props {
50 collections: Vec<CollectionBrief>,
51 sort_mode: NameSort<CollectionBrief>,
52}
53impl Props {
54 fn new(state: &AppState, sort_mode: NameSort<CollectionBrief>) -> Self {
55 let mut collections = state.library.collections.clone();
56 sort_mode.sort_items(&mut collections);
57 Self {
58 collections,
59 sort_mode,
60 }
61 }
62}
63
64impl Component for LibraryCollectionsView {
65 fn new(state: &AppState, action_tx: UnboundedSender<Action>) -> Self
66 where
67 Self: Sized,
68 {
69 let sort_mode = NameSort::default();
70 Self {
71 action_tx,
72 props: Props::new(state, sort_mode),
73 tree_state: Mutex::new(CheckTreeState::default()),
74 }
75 }
76
77 fn move_with_state(self, state: &AppState) -> Self
78 where
79 Self: Sized,
80 {
81 let tree_state = if state.active_view == ActiveView::Collections {
82 self.tree_state
83 } else {
84 Mutex::default()
85 };
86
87 Self {
88 props: Props::new(state, self.props.sort_mode),
89 tree_state,
90 ..self
91 }
92 }
93
94 fn name(&self) -> &'static str {
95 "Library Collections View"
96 }
97
98 fn handle_key_event(&mut self, key: KeyEvent) {
99 match key.code {
100 KeyCode::PageUp => {
102 self.tree_state.lock().unwrap().select_relative(|current| {
103 let first = self.props.collections.len().saturating_sub(1);
104 current.map_or(first, |c| c.saturating_sub(10))
105 });
106 }
107 KeyCode::Up => {
108 self.tree_state.lock().unwrap().key_up();
109 }
110 KeyCode::PageDown => {
111 self.tree_state
112 .lock()
113 .unwrap()
114 .select_relative(|current| current.map_or(0, |c| c.saturating_add(10)));
115 }
116 KeyCode::Down => {
117 self.tree_state.lock().unwrap().key_down();
118 }
119 KeyCode::Left => {
120 self.tree_state.lock().unwrap().key_left();
121 }
122 KeyCode::Right => {
123 self.tree_state.lock().unwrap().key_right();
124 }
125 KeyCode::Enter => {
127 if self.tree_state.lock().unwrap().toggle_selected() {
128 let things = self.tree_state.lock().unwrap().get_selected_thing();
129
130 if let Some(thing) = things {
131 self.action_tx
132 .send(Action::ActiveView(ViewAction::Set(thing.into())))
133 .unwrap();
134 }
135 }
136 }
137 KeyCode::Char('s') => {
139 self.props.sort_mode = self.props.sort_mode.next();
140 self.props.sort_mode.sort_items(&mut self.props.collections);
141 self.tree_state.lock().unwrap().scroll_selected_into_view();
142 }
143 KeyCode::Char('S') => {
144 self.props.sort_mode = self.props.sort_mode.prev();
145 self.props.sort_mode.sort_items(&mut self.props.collections);
146 self.tree_state.lock().unwrap().scroll_selected_into_view();
147 }
148 _ => {}
149 }
150 }
151
152 fn handle_mouse_event(&mut self, mouse: MouseEvent, area: Rect) {
153 let area = area.inner(Margin::new(1, 2));
155
156 let result = self
157 .tree_state
158 .lock()
159 .unwrap()
160 .handle_mouse_event(mouse, area, true);
161 if let Some(action) = result {
162 self.action_tx.send(action).unwrap();
163 }
164 }
165}
166
167impl ComponentRender<RenderProps> for LibraryCollectionsView {
168 fn render_border(&self, frame: &mut ratatui::Frame<'_>, props: RenderProps) -> RenderProps {
169 let border_style = Style::default().fg(border_color(props.is_focused).into());
170
171 let border = Block::bordered()
173 .title_top(Line::from(vec![
174 Span::styled("Library Collections".to_string(), Style::default().bold()),
175 Span::raw(" sorted by: "),
176 Span::styled(self.props.sort_mode.to_string(), Style::default().italic()),
177 ]))
178 .title_bottom(" \u{23CE} : Open | ←/↑/↓/→: Navigate | s/S: change sort")
179 .border_style(border_style);
180 let content_area = border.inner(props.area);
181 frame.render_widget(border, props.area);
182
183 let border = Block::new()
185 .borders(Borders::TOP)
186 .border_style(border_style);
187 frame.render_widget(&border, content_area);
188 let content_area = border.inner(content_area);
189
190 RenderProps {
192 area: content_area,
193 is_focused: props.is_focused,
194 }
195 }
196
197 fn render_content(&self, frame: &mut ratatui::Frame<'_>, props: RenderProps) {
198 let items = self
200 .props
201 .collections
202 .iter()
203 .map(create_collection_tree_leaf)
204 .collect::<Vec<_>>();
205
206 frame.render_stateful_widget(
208 CheckTree::new(&items)
209 .unwrap()
210 .highlight_style(Style::default().fg((*TEXT_HIGHLIGHT).into()).bold())
211 .node_unchecked_symbol("▪ ")
213 .node_checked_symbol("▪ ")
214 .experimental_scrollbar(Some(Scrollbar::new(ScrollbarOrientation::VerticalRight))),
215 props.area,
216 &mut self.tree_state.lock().unwrap(),
217 );
218 }
219}
220
221#[cfg(test)]
222mod sort_mode_tests {
223 use super::*;
224 use mecomp_prost::RecordId;
225 use pretty_assertions::assert_eq;
226 use rstest::rstest;
227
228 #[rstest]
229 #[case(NameSort::default(), NameSort::default())]
230 fn test_sort_mode_next_prev(
231 #[case] mode: NameSort<CollectionBrief>,
232 #[case] expected: NameSort<CollectionBrief>,
233 ) {
234 assert_eq!(mode.next(), expected);
235 assert_eq!(mode.next().prev(), mode);
236 }
237
238 #[rstest]
239 #[case(NameSort::default(), "Name")]
240 fn test_sort_mode_display(#[case] mode: NameSort<CollectionBrief>, #[case] expected: &str) {
241 assert_eq!(mode.to_string(), expected);
242 }
243
244 #[rstest]
245 fn test_sort_collectionss() {
246 let mut collections = vec![
247 CollectionBrief {
248 id: RecordId::new("collection", "3"),
249 name: "C".into(),
250 },
251 CollectionBrief {
252 id: RecordId::new("collection", "1"),
253 name: "A".into(),
254 },
255 CollectionBrief {
256 id: RecordId::new("collection", "2"),
257 name: "B".into(),
258 },
259 ];
260
261 NameSort::default().sort_items(&mut collections);
262 assert_eq!(collections[0].name, "A");
263 assert_eq!(collections[1].name, "B");
264 assert_eq!(collections[2].name, "C");
265 }
266}
267
268#[cfg(test)]
269mod item_view_tests {
270 use super::*;
271 use crate::{
272 state::action::{AudioAction, PopupAction, QueueAction},
273 test_utils::{assert_buffer_eq, item_id, setup_test_terminal, state_with_everything},
274 ui::{components::content_view::ActiveView, widgets::popups::PopupType},
275 };
276 use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
277 use pretty_assertions::assert_eq;
278 use ratatui::buffer::Buffer;
279
280 #[test]
281 fn test_new() {
282 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
283 let state = state_with_everything();
284 let view = CollectionView::new(&state, tx).item_view;
285
286 assert_eq!(view.name(), "Collection View");
287 assert_eq!(
288 view.props,
289 Some(state.additional_view_data.collection.unwrap())
290 );
291 }
292
293 #[test]
294 fn test_move_with_state() {
295 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
296 let state = AppState::default();
297 let new_state = state_with_everything();
298 let view = CollectionView::new(&state, tx)
299 .move_with_state(&new_state)
300 .item_view;
301
302 assert_eq!(
303 view.props,
304 Some(new_state.additional_view_data.collection.unwrap())
305 );
306 }
307 #[test]
308 fn test_render_no_collection() {
309 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
310 let view = CollectionView::new(&AppState::default(), tx);
311
312 let (mut terminal, area) = setup_test_terminal(22, 3);
313 let props = RenderProps {
314 area,
315 is_focused: true,
316 };
317 let buffer = terminal
318 .draw(|frame| view.render(frame, props))
319 .unwrap()
320 .buffer
321 .clone();
322 #[rustfmt::skip]
323 let expected = Buffer::with_lines([
324 "┌Collection View─────┐",
325 "│No active collection│",
326 "└────────────────────┘",
327 ]);
328
329 assert_buffer_eq(&buffer, &expected);
330 }
331
332 #[test]
333 fn test_render() {
334 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
335 let view = CollectionView::new(&state_with_everything(), tx);
336
337 let (mut terminal, area) = setup_test_terminal(60, 9);
338 let props = RenderProps {
339 area,
340 is_focused: true,
341 };
342 let buffer = terminal
343 .draw(|frame| view.render(frame, props))
344 .unwrap()
345 .buffer
346 .clone();
347 let expected = Buffer::with_lines([
348 "┌Collection View sorted by: Artist─────────────────────────┐",
349 "│ Collection 0 │",
350 "│ Songs: 1 Duration: 00:03:00.00 │",
351 "│ │",
352 "│q: add to queue | r: start radio | p: add to playlist─────│",
353 "│Performing operations on entire collection────────────────│",
354 "│☐ Test Song Test Artist │",
355 "│s/S: change sort──────────────────────────────────────────│",
356 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
357 ]);
358
359 assert_buffer_eq(&buffer, &expected);
360 }
361
362 #[test]
363 fn test_render_with_checked() {
364 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
365 let mut view = CollectionView::new(&state_with_everything(), tx);
366 let (mut terminal, area) = setup_test_terminal(60, 9);
367 let props = RenderProps {
368 area,
369 is_focused: true,
370 };
371 let buffer = terminal
372 .draw(|frame| view.render(frame, props))
373 .unwrap()
374 .buffer
375 .clone();
376 let expected = Buffer::with_lines([
377 "┌Collection View sorted by: Artist─────────────────────────┐",
378 "│ Collection 0 │",
379 "│ Songs: 1 Duration: 00:03:00.00 │",
380 "│ │",
381 "│q: add to queue | r: start radio | p: add to playlist─────│",
382 "│Performing operations on entire collection────────────────│",
383 "│☐ Test Song Test Artist │",
384 "│s/S: change sort──────────────────────────────────────────│",
385 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
386 ]);
387 assert_buffer_eq(&buffer, &expected);
388
389 view.handle_key_event(KeyEvent::from(KeyCode::Down));
391 view.handle_key_event(KeyEvent::from(KeyCode::Down));
392 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
393
394 let buffer = terminal
395 .draw(|frame| view.render(frame, props))
396 .unwrap()
397 .buffer
398 .clone();
399 let expected = Buffer::with_lines([
400 "┌Collection View sorted by: Artist─────────────────────────┐",
401 "│ Collection 0 │",
402 "│ Songs: 1 Duration: 00:03:00.00 │",
403 "│ │",
404 "│q: add to queue | r: start radio | p: add to playlist─────│",
405 "│Performing operations on checked items────────────────────│",
406 "│☑ Test Song Test Artist │",
407 "│s/S: change sort──────────────────────────────────────────│",
408 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
409 ]);
410
411 assert_buffer_eq(&buffer, &expected);
412 }
413
414 #[test]
415 fn smoke_navigation() {
416 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
417 let mut view = CollectionView::new(&state_with_everything(), tx);
418
419 view.handle_key_event(KeyEvent::from(KeyCode::Up));
420 view.handle_key_event(KeyEvent::from(KeyCode::PageUp));
421 view.handle_key_event(KeyEvent::from(KeyCode::Down));
422 view.handle_key_event(KeyEvent::from(KeyCode::PageDown));
423 view.handle_key_event(KeyEvent::from(KeyCode::Left));
424 view.handle_key_event(KeyEvent::from(KeyCode::Right));
425 }
426
427 #[test]
428 fn test_actions() {
429 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
430 let mut view = CollectionView::new(&state_with_everything(), tx);
431
432 let (mut terminal, area) = setup_test_terminal(60, 9);
434 let props = RenderProps {
435 area,
436 is_focused: true,
437 };
438 let _frame = terminal.draw(|frame| view.render(frame, props)).unwrap();
439
440 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
443 assert_eq!(
444 rx.blocking_recv().unwrap(),
445 Action::Audio(AudioAction::Queue(QueueAction::Add(vec![
446 ("collection", item_id()).into()
447 ])))
448 );
449 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
450 assert_eq!(
451 rx.blocking_recv().unwrap(),
452 Action::Popup(PopupAction::Open(PopupType::Playlist(vec![
453 ("collection", item_id()).into()
454 ])))
455 );
456 view.handle_key_event(KeyEvent::from(KeyCode::Char('d')));
457
458 view.handle_key_event(KeyEvent::from(KeyCode::Up));
461 let _frame = terminal.draw(|frame| view.render(frame, props)).unwrap();
462
463 view.handle_key_event(KeyEvent::from(KeyCode::Enter));
465 assert_eq!(
466 rx.blocking_recv().unwrap(),
467 Action::ActiveView(ViewAction::Set(ActiveView::Song(item_id())))
468 );
469
470 view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
472
473 view.handle_key_event(KeyEvent::from(KeyCode::Char('q')));
475 assert_eq!(
476 rx.blocking_recv().unwrap(),
477 Action::Audio(AudioAction::Queue(QueueAction::Add(vec![
478 ("song", item_id()).into()
479 ])))
480 );
481
482 view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
484 assert_eq!(
485 rx.blocking_recv().unwrap(),
486 Action::Popup(PopupAction::Open(PopupType::Playlist(vec![
487 ("song", item_id()).into()
488 ])))
489 );
490 }
491
492 #[test]
493 #[allow(clippy::too_many_lines)]
494 fn test_mouse_event() {
495 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
496 let mut view = CollectionView::new(&state_with_everything(), tx);
497
498 let (mut terminal, area) = setup_test_terminal(60, 9);
500 let props = RenderProps {
501 area,
502 is_focused: true,
503 };
504 let buffer = terminal
505 .draw(|frame| view.render(frame, props))
506 .unwrap()
507 .buffer
508 .clone();
509 let expected = Buffer::with_lines([
510 "┌Collection View sorted by: Artist─────────────────────────┐",
511 "│ Collection 0 │",
512 "│ Songs: 1 Duration: 00:03:00.00 │",
513 "│ │",
514 "│q: add to queue | r: start radio | p: add to playlist─────│",
515 "│Performing operations on entire collection────────────────│",
516 "│☐ Test Song Test Artist │",
517 "│s/S: change sort──────────────────────────────────────────│",
518 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
519 ]);
520 assert_buffer_eq(&buffer, &expected);
521
522 view.handle_mouse_event(
524 MouseEvent {
525 kind: MouseEventKind::Down(MouseButton::Left),
526 column: 2,
527 row: 6,
528 modifiers: KeyModifiers::empty(),
529 },
530 area,
531 );
532 let buffer = terminal
533 .draw(|frame| view.render(frame, props))
534 .unwrap()
535 .buffer
536 .clone();
537 let expected = Buffer::with_lines([
538 "┌Collection View sorted by: Artist─────────────────────────┐",
539 "│ Collection 0 │",
540 "│ Songs: 1 Duration: 00:03:00.00 │",
541 "│ │",
542 "│q: add to queue | r: start radio | p: add to playlist─────│",
543 "│Performing operations on checked items────────────────────│",
544 "│☑ Test Song Test Artist │",
545 "│s/S: change sort──────────────────────────────────────────│",
546 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
547 ]);
548 assert_buffer_eq(&buffer, &expected);
549
550 view.handle_mouse_event(
552 MouseEvent {
553 kind: MouseEventKind::Down(MouseButton::Left),
554 column: 2,
555 row: 6,
556 modifiers: KeyModifiers::empty(),
557 },
558 area,
559 );
560 let expected = Buffer::with_lines([
561 "┌Collection View sorted by: Artist─────────────────────────┐",
562 "│ Collection 0 │",
563 "│ Songs: 1 Duration: 00:03:00.00 │",
564 "│ │",
565 "│q: add to queue | r: start radio | p: add to playlist─────│",
566 "│Performing operations on entire collection────────────────│",
567 "│☐ Test Song Test Artist │",
568 "│s/S: change sort──────────────────────────────────────────│",
569 "└ ⏎ : Open | ←/↑/↓/→: Navigate | ␣ Check───────────────────┘",
570 ]);
571 let buffer = terminal
572 .draw(|frame| view.render(frame, props))
573 .unwrap()
574 .buffer
575 .clone();
576 assert_buffer_eq(&buffer, &expected);
577 for _ in 0..2 {
579 view.handle_mouse_event(
580 MouseEvent {
581 kind: MouseEventKind::Down(MouseButton::Left),
582 column: 2,
583 row: 6,
584 modifiers: KeyModifiers::CONTROL,
585 },
586 area,
587 );
588 assert_eq!(
589 rx.blocking_recv().unwrap(),
590 Action::ActiveView(ViewAction::Set(ActiveView::Song(item_id())))
591 );
592 }
593
594 view.handle_mouse_event(
596 MouseEvent {
597 kind: MouseEventKind::ScrollDown,
598 column: 2,
599 row: 6,
600 modifiers: KeyModifiers::empty(),
601 },
602 area,
603 );
604 let buffer = terminal
605 .draw(|frame| view.render(frame, props))
606 .unwrap()
607 .buffer
608 .clone();
609 assert_buffer_eq(&buffer, &expected);
610 view.handle_mouse_event(
612 MouseEvent {
613 kind: MouseEventKind::ScrollUp,
614 column: 2,
615 row: 6,
616 modifiers: KeyModifiers::empty(),
617 },
618 area,
619 );
620 let buffer = terminal
621 .draw(|frame| view.render(frame, props))
622 .unwrap()
623 .buffer
624 .clone();
625 assert_buffer_eq(&buffer, &expected);
626 }
627}
628
629#[cfg(test)]
630mod library_view_tests {
631 use super::*;
632 use crate::{
633 test_utils::{assert_buffer_eq, item_id, setup_test_terminal, state_with_everything},
634 ui::components::content_view::ActiveView,
635 };
636 use crossterm::event::{KeyModifiers, MouseButton, MouseEventKind};
637 use pretty_assertions::assert_eq;
638 use ratatui::buffer::Buffer;
639
640 #[test]
641 fn test_new() {
642 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
643 let state = state_with_everything();
644 let view = LibraryCollectionsView::new(&state, tx);
645
646 assert_eq!(view.name(), "Library Collections View");
647 assert_eq!(view.props.collections, state.library.collections);
648 }
649
650 #[test]
651 fn test_move_with_state() {
652 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
653 let state = AppState::default();
654 let new_state = state_with_everything();
655 let view = LibraryCollectionsView::new(&state, tx).move_with_state(&new_state);
656
657 assert_eq!(view.props.collections, new_state.library.collections);
658 }
659
660 #[test]
661 fn test_render() {
662 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
663 let view = LibraryCollectionsView::new(&state_with_everything(), tx);
664
665 let (mut terminal, area) = setup_test_terminal(60, 6);
666 let props = RenderProps {
667 area,
668 is_focused: true,
669 };
670 let buffer = terminal
671 .draw(|frame| view.render(frame, props))
672 .unwrap()
673 .buffer
674 .clone();
675 let expected = Buffer::with_lines([
676 "┌Library Collections sorted by: Name───────────────────────┐",
677 "│──────────────────────────────────────────────────────────│",
678 "│▪ Collection 0 │",
679 "│ │",
680 "│ │",
681 "└ ⏎ : Open | ←/↑/↓/→: Navigate | s/S: change sort──────────┘",
682 ]);
683
684 assert_buffer_eq(&buffer, &expected);
685 }
686
687 #[test]
688 fn test_sort_keys() {
689 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
690 let mut view = LibraryCollectionsView::new(&state_with_everything(), tx);
691
692 assert_eq!(view.props.sort_mode, NameSort::default());
693 view.handle_key_event(KeyEvent::from(KeyCode::Char('s')));
694 assert_eq!(view.props.sort_mode, NameSort::default());
695 view.handle_key_event(KeyEvent::from(KeyCode::Char('S')));
696 assert_eq!(view.props.sort_mode, NameSort::default());
697 }
698
699 #[test]
700 fn smoke_navigation() {
701 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
702 let mut view = LibraryCollectionsView::new(&state_with_everything(), tx);
703
704 view.handle_key_event(KeyEvent::from(KeyCode::Up));
705 view.handle_key_event(KeyEvent::from(KeyCode::PageUp));
706 view.handle_key_event(KeyEvent::from(KeyCode::Down));
707 view.handle_key_event(KeyEvent::from(KeyCode::PageDown));
708 view.handle_key_event(KeyEvent::from(KeyCode::Left));
709 view.handle_key_event(KeyEvent::from(KeyCode::Right));
710 }
711
712 #[test]
713 fn test_actions() {
714 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
715 let mut view = LibraryCollectionsView::new(&state_with_everything(), tx);
716
717 let (mut terminal, area) = setup_test_terminal(60, 9);
719 let props = RenderProps {
720 area,
721 is_focused: true,
722 };
723 terminal.draw(|frame| view.render(frame, props)).unwrap();
724
725 view.handle_key_event(KeyEvent::from(KeyCode::Down));
727
728 view.handle_key_event(KeyEvent::from(KeyCode::Enter));
730 assert_eq!(
731 rx.blocking_recv().unwrap(),
732 Action::ActiveView(ViewAction::Set(ActiveView::Collection(item_id())))
733 );
734 }
735
736 #[test]
737 fn test_mouse_event() {
738 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
739 let mut view = LibraryCollectionsView::new(&state_with_everything(), tx);
740
741 let (mut terminal, area) = setup_test_terminal(60, 9);
743 let props = RenderProps {
744 area,
745 is_focused: true,
746 };
747 let buffer = terminal
748 .draw(|frame| view.render(frame, props))
749 .unwrap()
750 .buffer
751 .clone();
752 let expected = Buffer::with_lines([
753 "┌Library Collections sorted by: Name───────────────────────┐",
754 "│──────────────────────────────────────────────────────────│",
755 "│▪ Collection 0 │",
756 "│ │",
757 "│ │",
758 "│ │",
759 "│ │",
760 "│ │",
761 "└ ⏎ : Open | ←/↑/↓/→: Navigate | s/S: change sort──────────┘",
762 ]);
763 assert_buffer_eq(&buffer, &expected);
764
765 view.handle_mouse_event(
767 MouseEvent {
768 kind: MouseEventKind::Down(MouseButton::Left),
769 column: 2,
770 row: 2,
771 modifiers: KeyModifiers::empty(),
772 },
773 area,
774 );
775 assert_eq!(
776 rx.blocking_recv().unwrap(),
777 Action::ActiveView(ViewAction::Set(ActiveView::Collection(item_id())))
778 );
779 let buffer = terminal
780 .draw(|frame| view.render(frame, props))
781 .unwrap()
782 .buffer
783 .clone();
784 assert_buffer_eq(&buffer, &expected);
785
786 view.handle_mouse_event(
788 MouseEvent {
789 kind: MouseEventKind::ScrollDown,
790 column: 2,
791 row: 2,
792 modifiers: KeyModifiers::empty(),
793 },
794 area,
795 );
796
797 view.handle_mouse_event(
799 MouseEvent {
800 kind: MouseEventKind::Down(MouseButton::Left),
801 column: 2,
802 row: 2,
803 modifiers: KeyModifiers::empty(),
804 },
805 area,
806 );
807 assert_eq!(
808 rx.blocking_recv().unwrap(),
809 Action::ActiveView(ViewAction::Set(ActiveView::Collection(item_id())))
810 );
811 let buffer = terminal
812 .draw(|frame| view.render(frame, props))
813 .unwrap()
814 .buffer
815 .clone();
816 assert_buffer_eq(&buffer, &expected);
817
818 view.handle_mouse_event(
820 MouseEvent {
821 kind: MouseEventKind::ScrollUp,
822 column: 2,
823 row: 2,
824 modifiers: KeyModifiers::empty(),
825 },
826 area,
827 );
828
829 view.handle_mouse_event(
831 MouseEvent {
832 kind: MouseEventKind::Down(MouseButton::Left),
833 column: 2,
834 row: 2,
835 modifiers: KeyModifiers::empty(),
836 },
837 area,
838 );
839 assert_eq!(
840 rx.blocking_recv().unwrap(),
841 Action::ActiveView(ViewAction::Set(ActiveView::Collection(item_id())))
842 );
843
844 let mouse = MouseEvent {
846 kind: MouseEventKind::Down(MouseButton::Left),
847 column: 2,
848 row: 3,
849 modifiers: KeyModifiers::empty(),
850 };
851 view.handle_mouse_event(mouse, area);
852 assert_eq!(view.tree_state.lock().unwrap().get_selected_thing(), None);
853 view.handle_mouse_event(mouse, area);
854 assert_eq!(
855 rx.try_recv(),
856 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
857 );
858 }
859}