fm/io/
draw_menu.rs

1use std::borrow::Cow;
2use std::cmp::min;
3
4use ratatui::{layout::Rect, prelude::Widget, style::Color, text::Line, widgets::Paragraph, Frame};
5
6use crate::config::{ColorG, Gradient, MENU_STYLES};
7use crate::io::Offseted;
8use crate::modes::{Content, ContentWindow};
9
10/// Iter over the content, returning a triplet of `(index, line, style)`.
11#[macro_export]
12macro_rules! colored_skip_take {
13    ($t:ident, $u:ident) => {
14        std::iter::zip(
15            $t.iter().enumerate(),
16            Gradient::new(
17                ColorG::from_ratatui(
18                    MENU_STYLES
19                        .get()
20                        .expect("Menu colors should be set")
21                        .first
22                        .fg
23                        .unwrap_or(Color::Rgb(0, 0, 0)),
24                )
25                .unwrap_or_default(),
26                ColorG::from_ratatui(
27                    MENU_STYLES
28                        .get()
29                        .expect("Menu colors should be set")
30                        .palette_3
31                        .fg
32                        .unwrap_or(Color::Rgb(0, 0, 0)),
33                )
34                .unwrap_or_default(),
35                $t.len(),
36            )
37            .gradient()
38            .map(|color| color.into()),
39        )
40        .map(|((index, line), style)| (index, line, style))
41        .skip($u.top)
42        .take(min($u.len, $t.len()))
43    };
44}
45
46/// Converts itself into a [`std::borrow::Cow<str>`].
47/// It's used to call `print_with_style` which requires an `&str`.
48pub trait CowStr {
49    fn cow_str(&self) -> Cow<str>;
50}
51
52impl CowStr for (char, std::path::PathBuf) {
53    fn cow_str(&self) -> Cow<str> {
54        format!("{c} {p}", c = self.0, p = self.1.display()).into()
55    }
56}
57
58impl CowStr for std::path::PathBuf {
59    fn cow_str(&self) -> Cow<str> {
60        self.to_string_lossy()
61    }
62}
63
64impl CowStr for String {
65    fn cow_str(&self) -> Cow<str> {
66        self.into()
67    }
68}
69
70impl CowStr for &str {
71    fn cow_str(&self) -> Cow<str> {
72        (*self).into()
73    }
74}
75
76/// Trait used to display a scrollable content
77/// Element are itered from the top to the bottom of the window index
78/// and printed in the canvas.
79/// Since the content kind is linked to a mode,
80/// it doesn't print the second line of the mode.
81pub trait DrawMenu<T: CowStr> {
82    fn draw_menu(&self, f: &mut Frame, rect: &Rect, window: &ContentWindow)
83    where
84        Self: Content<T>,
85    {
86        let mut p_rect = rect.offseted(4, 3);
87        p_rect.height = p_rect.height.saturating_sub(2);
88        let content = self.content();
89        let lines: Vec<_> = colored_skip_take!(content, window)
90            .map(|(index, item, style)| Line::styled(item.cow_str(), self.style(index, &style)))
91            .collect();
92        Paragraph::new(lines).render(p_rect, f.buffer_mut());
93    }
94}
95
96/// Used to implement a [`crate::io::DrawMenu`] trait for Navigable menu which
97/// allows their item to be selected with a `char` bind.
98/// Every menu which allows the user to select an item from a list without reading input
99/// should use this macro for the rendering.
100///
101/// It will display a `char` alongside the item. Typing this char should execute the
102/// corresponding element.
103#[macro_export]
104macro_rules! impl_draw_menu_with_char {
105    ($struct:ident, $field_type:ty) => {
106        use std::{cmp::min, iter::zip};
107
108        use ratatui::{
109            layout::{Offset, Rect},
110            prelude::Widget,
111            style::Color,
112            text::Line,
113            widgets::Paragraph,
114            Frame,
115        };
116
117        use $crate::colored_skip_take;
118        use $crate::config::{ColorG, Gradient, MENU_STYLES};
119        use $crate::io::{CowStr, DrawMenu};
120        use $crate::modes::ContentWindow;
121
122        impl DrawMenu<$field_type> for $struct {
123            fn draw_menu(&self, f: &mut Frame, rect: &Rect, window: &ContentWindow)
124            where
125                Self: Content<$field_type>,
126            {
127                let mut p_rect = rect.offset(Offset { x: 2, y: 3 }).intersection(*rect);
128                p_rect.height = p_rect.height.saturating_sub(2);
129                let content = self.content();
130                let lines: Vec<_> = zip(
131                    ('a'..='z').cycle().skip(window.top),
132                    colored_skip_take!(content, window),
133                )
134                .filter(|(_, (index, _, _))| {
135                    ((*index) as u16 + ContentWindow::WINDOW_MARGIN_TOP_U16 + 1)
136                        .saturating_sub(window.top as u16)
137                        + 2
138                        <= rect.height
139                })
140                .map(|(letter, (index, path, style))| {
141                    Line::styled(
142                        format!("{letter} {path}", path = path.cow_str()),
143                        self.style(index, &style),
144                    )
145                })
146                .collect();
147                Paragraph::new(lines).render(p_rect, f.buffer_mut());
148            }
149        }
150    };
151}