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