mecomp_tui/ui/components/content_view/views/
generic.rs1use std::sync::Mutex;
2
3use crossterm::event::{KeyCode, KeyEvent, MouseEvent};
4use ratatui::{
5 layout::{Alignment, Margin, Rect},
6 style::{Style, Stylize},
7 text::{Line, Span},
8 widgets::{Block, Borders},
9};
10use tokio::sync::mpsc::UnboundedSender;
11
12use crate::{
13 state::action::{Action, ViewAction},
14 ui::{
15 AppState,
16 colors::{TEXT_HIGHLIGHT, TEXT_NORMAL, border_color},
17 components::{
18 Component, ComponentRender, RenderProps,
19 content_view::views::traits::{SortMode, SortableViewProps},
20 },
21 widgets::tree::{CheckTree, state::CheckTreeState},
22 },
23};
24
25use super::{
26 ItemViewProps,
27 checktree_utils::{
28 construct_add_to_playlist_action, construct_add_to_queue_action,
29 construct_start_radio_action,
30 },
31};
32
33#[derive(Debug)]
34pub struct ItemView<Props> {
35 pub action_tx: UnboundedSender<Action>,
37 pub props: Option<Props>,
39 pub tree_state: Mutex<CheckTreeState<String>>,
41}
42
43impl<Props> Component for ItemView<Props>
44where
45 Props: ItemViewProps,
46{
47 fn new(state: &AppState, action_tx: UnboundedSender<Action>) -> Self
48 where
49 Self: Sized,
50 {
51 let props = Props::retrieve(&state.additional_view_data);
52 let tree_state = Mutex::new(CheckTreeState::default());
53 Self {
54 action_tx,
55 props,
56 tree_state,
57 }
58 }
59
60 fn move_with_state(self, state: &AppState) -> Self
61 where
62 Self: Sized,
63 {
64 if let Some(props) = Props::retrieve(&state.additional_view_data) {
65 Self {
66 props: Some(props),
67 tree_state: Mutex::new(CheckTreeState::default()),
68 ..self
69 }
70 } else {
71 self
72 }
73 }
74
75 fn name(&self) -> &str {
76 Props::title()
77 }
78
79 fn handle_key_event(&mut self, key: KeyEvent) {
80 match key.code {
81 KeyCode::Up => {
83 self.tree_state.lock().unwrap().key_up();
84 }
85 KeyCode::Down => {
86 self.tree_state.lock().unwrap().key_down();
87 }
88 KeyCode::Left => {
89 self.tree_state.lock().unwrap().key_left();
90 }
91 KeyCode::Right => {
92 self.tree_state.lock().unwrap().key_right();
93 }
94 KeyCode::Char(' ') => {
95 self.tree_state.lock().unwrap().key_space();
96 }
97 KeyCode::Enter => {
99 if self.tree_state.lock().unwrap().toggle_selected() {
100 let things = self.tree_state.lock().unwrap().get_selected_thing();
101
102 if let Some(thing) = things {
103 self.action_tx
104 .send(Action::ActiveView(ViewAction::Set(thing.into())))
105 .unwrap();
106 }
107 }
108 }
109 KeyCode::Char('q') => {
111 let checked_things = self.tree_state.lock().unwrap().get_checked_things();
112 if let Some(action) = construct_add_to_queue_action(
113 checked_things,
114 self.props.as_ref().map(super::ItemViewProps::id),
115 ) {
116 self.action_tx.send(action).unwrap();
117 }
118 }
119 KeyCode::Char('r') => {
121 let checked_things = self.tree_state.lock().unwrap().get_checked_things();
122 if let Some(action) = construct_start_radio_action(
123 checked_things,
124 self.props.as_ref().map(super::ItemViewProps::id),
125 ) {
126 self.action_tx.send(action).unwrap();
127 }
128 }
129 KeyCode::Char('p') => {
131 let checked_things = self.tree_state.lock().unwrap().get_checked_things();
132 if let Some(action) = construct_add_to_playlist_action(
133 checked_things,
134 self.props.as_ref().map(super::ItemViewProps::id),
135 ) {
136 self.action_tx.send(action).unwrap();
137 }
138 }
139 _ => {
140 if let Some(props) = &mut self.props {
141 props.handle_extra_key_events(
142 key,
143 self.action_tx.clone(),
144 &mut self.tree_state.lock().unwrap(),
145 );
146 }
147 }
148 }
149 }
150
151 fn handle_mouse_event(&mut self, mouse: MouseEvent, area: Rect) {
152 let area = area.inner(Margin::new(1, 1));
154 let [_, content_area] = Props::split_area(area);
155 let footer = u16::from(Props::extra_footer().is_some());
156 let content_area = Rect {
157 y: content_area.y + 2,
158 height: content_area.height - 2 - footer,
159 ..content_area
160 };
161
162 let result = self
163 .tree_state
164 .lock()
165 .unwrap()
166 .handle_mouse_event(mouse, content_area, false);
167 if let Some(action) = result {
168 self.action_tx.send(action).unwrap();
169 }
170 }
171}
172
173impl<Props> ComponentRender<RenderProps> for ItemView<Props>
174where
175 Props: ItemViewProps,
176{
177 fn render_border(&self, frame: &mut ratatui::Frame<'_>, props: RenderProps) -> RenderProps {
178 let border_style = Style::default().fg(border_color(props.is_focused).into());
179
180 let area = if let Some(state) = &self.props {
182 let border = Block::bordered()
183 .title_top(Props::title())
184 .title_bottom(" \u{23CE} : Open | ←/↑/↓/→: Navigate | \u{2423} Check")
185 .border_style(border_style);
186 let content_area = border.inner(props.area);
187 frame.render_widget(border, props.area);
188
189 let [info_area, content_area] = Props::split_area(content_area);
191
192 frame.render_widget(state.info_widget(), info_area);
194
195 let border = Block::default()
197 .borders(Borders::TOP)
198 .title_top("q: add to queue | r: start radio | p: add to playlist")
199 .border_style(border_style);
200 frame.render_widget(&border, content_area);
201 let content_area = border.inner(content_area);
202
203 let border = Block::default()
205 .borders(Borders::TOP)
206 .title_top(Line::from(vec![
207 Span::raw("Performing operations on "),
208 Span::raw(
209 if self
210 .tree_state
211 .lock()
212 .unwrap()
213 .get_checked_things()
214 .is_empty()
215 {
216 Props::none_checked_string()
217 } else {
218 "checked items"
219 },
220 )
221 .fg(*TEXT_HIGHLIGHT),
222 ]))
223 .italic()
224 .border_style(border_style);
225 frame.render_widget(&border, content_area);
226 border.inner(content_area)
227 } else {
228 let border = Block::bordered()
229 .title_top(Props::title())
230 .border_style(border_style);
231 frame.render_widget(&border, props.area);
232 border.inner(props.area)
233 };
234
235 RenderProps { area, ..props }
236 }
237
238 fn render_content(&self, frame: &mut ratatui::Frame<'_>, props: RenderProps) {
239 let Some(state) = &self.props else {
240 let text = format!("No active {}", Props::name());
241
242 frame.render_widget(
243 Line::from(text)
244 .style(Style::default().fg((*TEXT_NORMAL).into()))
245 .alignment(Alignment::Center),
246 props.area,
247 );
248 return;
249 };
250
251 let items = state.tree_items().unwrap();
253
254 frame.render_stateful_widget(
256 CheckTree::new(&items)
257 .unwrap()
258 .highlight_style(Style::default().fg((*TEXT_HIGHLIGHT).into()).bold())
259 .experimental_scrollbar(Props::scrollbar()),
260 props.area,
261 &mut self.tree_state.lock().unwrap(),
262 );
263 }
264}
265
266#[derive(Debug)]
272pub struct SortableItemView<Props, Mode, Item> {
273 pub item_view: ItemView<Props>,
274 pub sort_mode: Mode,
275 _item: std::marker::PhantomData<Item>,
276}
277
278impl<Props, Mode, Item> Component for SortableItemView<Props, Mode, Item>
279where
280 Props: ItemViewProps + SortableViewProps<Item>,
281 Mode: SortMode<Item>,
282{
283 fn new(state: &AppState, action_tx: UnboundedSender<Action>) -> Self
284 where
285 Self: Sized,
286 {
287 let item_view = ItemView::new(state, action_tx);
288 let sort_mode = Mode::default();
289 Self {
290 item_view,
291 sort_mode,
292 _item: std::marker::PhantomData,
293 }
294 }
295
296 fn move_with_state(self, state: &AppState) -> Self
297 where
298 Self: Sized,
299 {
300 let mut item_view = self.item_view.move_with_state(state);
301
302 if let Some(props) = &mut item_view.props {
303 props.sort_items(&self.sort_mode);
304 }
305
306 Self { item_view, ..self }
307 }
308
309 fn name(&self) -> &str {
310 self.item_view.name()
311 }
312
313 fn handle_key_event(&mut self, key: KeyEvent) {
314 match key.code {
315 crossterm::event::KeyCode::Char('s') => {
317 self.sort_mode = self.sort_mode.next();
318 if let Some(props) = &mut self.item_view.props {
319 props.sort_items(&self.sort_mode);
320 self.item_view
321 .tree_state
322 .lock()
323 .unwrap()
324 .scroll_selected_into_view();
325 }
326 }
327 crossterm::event::KeyCode::Char('S') => {
328 self.sort_mode = self.sort_mode.prev();
329 if let Some(props) = &mut self.item_view.props {
330 props.sort_items(&self.sort_mode);
331 self.item_view
332 .tree_state
333 .lock()
334 .unwrap()
335 .scroll_selected_into_view();
336 }
337 }
338 _ => self.item_view.handle_key_event(key),
339 }
340 }
341
342 fn handle_mouse_event(&mut self, mouse: MouseEvent, area: Rect) {
343 self.item_view.handle_mouse_event(mouse, area);
344 }
345}
346
347impl<Props, Mode, Item> ComponentRender<RenderProps> for SortableItemView<Props, Mode, Item>
348where
349 Props: ItemViewProps + SortableViewProps<Item>,
350 Mode: SortMode<Item>,
351{
352 fn render_border(&self, frame: &mut ratatui::Frame<'_>, props: RenderProps) -> RenderProps {
353 let border_style = Style::default().fg(border_color(props.is_focused).into());
354
355 let area = if let Some(state) = &self.item_view.props {
357 let border = Block::bordered()
358 .title_top(Line::from(vec![
359 Span::styled(Props::title(), Style::default().bold()),
360 Span::raw(" sorted by: "),
361 Span::styled(self.sort_mode.to_string(), Style::default().italic()),
362 ]))
363 .title_bottom(" \u{23CE} : Open | ←/↑/↓/→: Navigate | \u{2423} Check")
364 .border_style(border_style);
365 let content_area = border.inner(props.area);
366 frame.render_widget(border, props.area);
367
368 let [info_area, content_area] = Props::split_area(content_area);
370
371 frame.render_widget(state.info_widget(), info_area);
373
374 let border = Block::default()
376 .title_top("q: add to queue | r: start radio | p: add to playlist")
377 .border_style(border_style);
378 let border = if let Some(extra_footer) = Props::extra_footer() {
379 border
380 .borders(Borders::TOP | Borders::BOTTOM)
381 .title_bottom(extra_footer)
382 } else {
383 border.border_style(border_style)
384 };
385 frame.render_widget(&border, content_area);
386 let content_area = border.inner(content_area);
387
388 let border = Block::default()
390 .borders(Borders::TOP)
391 .title_top(Line::from(vec![
392 Span::raw("Performing operations on "),
393 Span::raw(
394 if self
395 .item_view
396 .tree_state
397 .lock()
398 .unwrap()
399 .get_checked_things()
400 .is_empty()
401 {
402 Props::none_checked_string()
403 } else {
404 "checked items"
405 },
406 )
407 .fg(*TEXT_HIGHLIGHT),
408 ]))
409 .italic()
410 .border_style(border_style);
411 frame.render_widget(&border, content_area);
412 border.inner(content_area)
413 } else {
414 let border = Block::bordered()
415 .title_top(Props::title())
416 .border_style(border_style);
417 frame.render_widget(&border, props.area);
418 border.inner(props.area)
419 };
420
421 RenderProps { area, ..props }
422 }
423
424 fn render_content(&self, frame: &mut ratatui::Frame<'_>, props: RenderProps) {
425 self.item_view.render_content(frame, props);
426 }
427}