Skip to main content

ratatui_explorer/
widget.rs

1use std::sync::Arc;
2
3use ratatui::{
4    buffer::Buffer,
5    layout::Rect,
6    style::{Color, Style},
7    text::{Line, Span, Text},
8    widgets::{Block, Borders, HighlightSpacing, List, ListState, WidgetRef},
9};
10
11use crate::{File, FileExplorer};
12
13type LineFactory = Arc<dyn Fn(&FileExplorer) -> Line<'_> + Send + Sync>;
14
15pub struct Renderer<'a>(pub(crate) &'a FileExplorer);
16
17impl WidgetRef for Renderer<'_> {
18    fn render_ref(&self, area: Rect, buf: &mut Buffer)
19    where
20        Self: Sized,
21    {
22        let mut state = ListState::default().with_selected(Some(self.0.selected_idx()));
23
24        let highlight_style = if self.0.current().is_dir {
25            self.0.theme().highlight_dir_style
26        } else {
27            self.0.theme().highlight_item_style
28        };
29
30        let mut list = List::new(self.0.files().iter().map(|file| file.text(self.0.theme())))
31            .style(self.0.theme().style)
32            .highlight_spacing(self.0.theme().highlight_spacing.clone())
33            .highlight_style(highlight_style)
34            .scroll_padding(self.0.theme().scroll_padding);
35
36        if let Some(symbol) = self.0.theme().highlight_symbol.as_deref() {
37            list = list.highlight_symbol(symbol);
38        }
39
40        if let Some(block) = self.0.theme().block.as_ref() {
41            let mut block = block.clone();
42
43            for title_top in self.0.theme().title_top(self.0) {
44                block = block.title_top(title_top);
45            }
46            for title_bottom in self.0.theme().title_bottom(self.0) {
47                block = block.title_bottom(title_bottom);
48            }
49
50            list = list.block(block);
51        }
52
53        ratatui::widgets::StatefulWidget::render(&list, area, buf, &mut state);
54    }
55}
56
57impl File {
58    /// Returns the text with the appropriate style to be displayed for the file.
59    fn text(&self, theme: &Theme) -> Text<'_> {
60        let style = if self.is_dir {
61            *theme.dir_style()
62        } else {
63            *theme.item_style()
64        };
65        Span::styled(&self.name, style).into()
66    }
67}
68
69/// The theme of the file explorer.
70///
71/// This struct is used to customize the look of the file explorer.
72/// It allows to set the style of the widget and the style of the files.
73/// You can also wrap the widget in a block with the [`with_block`](Theme::with_block)
74/// method and add dinamic titles to it with [`with_title_top`](Theme::with_title_top)
75/// and [`with_title_bottom`](Theme::with_title_bottom).
76#[derive(Clone, educe::Educe)]
77#[educe(Debug, PartialEq, Eq, Hash)]
78pub struct Theme {
79    block: Option<Block<'static>>,
80    #[educe(Debug(ignore), PartialEq(ignore), Hash(ignore))]
81    title_top: Vec<LineFactory>,
82    #[educe(Debug(ignore), PartialEq(ignore), Hash(ignore))]
83    title_bottom: Vec<LineFactory>,
84    style: Style,
85    item_style: Style,
86    dir_style: Style,
87    highlight_spacing: HighlightSpacing,
88    highlight_item_style: Style,
89    highlight_dir_style: Style,
90    highlight_symbol: Option<String>,
91    scroll_padding: usize,
92}
93
94impl Theme {
95    /// Create a new empty theme.
96    ///
97    /// The theme will not have any style set. To get a theme with the default style, use [`default`](Theme::default).
98    ///
99    /// # Example
100    /// ```no_run
101    /// # use ratatui_explorer::Theme;
102    /// let theme = Theme::new();
103    /// ```
104    #[must_use]
105    pub const fn new() -> Self {
106        Self {
107            block: None,
108            title_top: Vec::new(),
109            title_bottom: Vec::new(),
110            style: Style::new(),
111            item_style: Style::new(),
112            dir_style: Style::new(),
113            highlight_spacing: HighlightSpacing::WhenSelected,
114            highlight_item_style: Style::new(),
115            highlight_dir_style: Style::new(),
116            highlight_symbol: None,
117            scroll_padding: 0,
118        }
119    }
120
121    /// Add a top title to the theme.
122    /// The title is the current working directory.
123    ///
124    /// # Example
125    /// Suppose you have this tree file, with `passport.png` selected inside `file_explorer`:
126    /// ```plaintext
127    /// /
128    /// ├── .git
129    /// └── Documents
130    ///     ├── passport.png  <- selected
131    ///     └── resume.pdf
132    /// ```
133    /// You will end up with something like this:
134    /// ```plaintext
135    /// ┌/Documents────────────────────────┐
136    /// │ ../                              │
137    /// │ passport.png                     │
138    /// │ resume.pdf                       │
139    /// └──────────────────────────────────┘
140    /// ```
141    /// With this code:
142    /// ```no_run
143    /// use ratatui::widgets::*;
144    /// use ratatui_explorer::{FileExplorerBuilder, Theme};
145    ///
146    /// let theme = Theme::default()
147    ///     .with_block(Block::default().borders(Borders::ALL))
148    ///     .add_default_title();
149    ///
150    /// let file_explorer = FileExplorerBuilder::build_with_theme(theme).unwrap();
151    ///
152    /// /* user select `password.png` */
153    ///
154    /// let widget = file_explorer.widget();
155    /// /* render the widget */
156    /// ```
157    #[inline]
158    #[must_use = "method moves the value of self and returns the modified value"]
159    pub fn add_default_title(self) -> Self {
160        self.with_title_top(|file_explorer: &FileExplorer| {
161            Line::from(file_explorer.cwd().display().to_string())
162        })
163    }
164
165    /// Wrap the file explorer with a custom [`Block`](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html) widget.
166    ///
167    /// Behind the scene, it use the [`List::block`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.block) method.
168    /// See its documentation for more.
169    ///
170    /// You can use [`with_title_top`](Theme::with_title_top) and [`with_title_bottom`](Theme::with_title_top) to add dynamic titles to the block.
171    ///
172    /// # Example
173    /// ```no_run
174    /// # use ratatui::widgets::*;
175    /// # use ratatui_explorer::Theme;
176    /// let theme = Theme::default().with_block(Block::default().borders(Borders::ALL));
177    /// ```
178    #[inline]
179    #[must_use = "method moves the value of self and returns the modified value"]
180    pub fn with_block(mut self, block: Block<'static>) -> Self {
181        self.block = Some(block);
182        self
183    }
184
185    /// Set the style of the widget.
186    ///
187    /// Behind the scene, it use the [`List::style`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.style) method.
188    /// See its documentation for more.
189    ///
190    /// # Example
191    /// ```no_run
192    /// # use ratatui::prelude::*;
193    /// # use ratatui_explorer::Theme;
194    /// let theme = Theme::default().with_style(Style::default().fg(Color::Yellow));
195    /// ```
196    #[inline]
197    #[must_use = "method moves the value of self and returns the modified value"]
198    pub fn with_style<S: Into<Style>>(mut self, style: S) -> Self {
199        self.style = style.into();
200        self
201    }
202
203    /// Set the style of all non directories items. To set the style of the directories, use [`with_dir_style`](Theme::with_dir_style).
204    ///
205    /// Behind the scene, it use the [`Span::styled`](https://docs.rs/ratatui/latest/ratatui/text/struct.Span.html#method.styled) method.
206    /// See its documentation for more.
207    ///
208    /// # Example
209    /// ```no_run
210    /// # use ratatui::prelude::*;
211    /// # use ratatui_explorer::Theme;
212    /// let theme = Theme::default().with_item_style(Style::default().fg(Color::White));
213    /// ```
214    #[inline]
215    #[must_use = "method moves the value of self and returns the modified value"]
216    pub fn with_item_style<S: Into<Style>>(mut self, item_style: S) -> Self {
217        self.item_style = item_style.into();
218        self
219    }
220
221    /// Set the style of all directories items. To set the style of the non directories, use [`with_item_style`](Theme::with_item_style).
222    ///
223    /// Behind the scene, it use the [`Span::styled`](https://docs.rs/ratatui/latest/ratatui/text/struct.Span.html#method.styled) method.
224    /// See its documentation for more.
225    ///
226    /// # Example
227    /// ```no_run
228    /// # use ratatui::prelude::*;
229    /// # use ratatui_explorer::Theme;
230    /// let theme = Theme::default().with_dir_style(Style::default().fg(Color::Blue));
231    /// ```
232    #[inline]
233    #[must_use = "method moves the value of self and returns the modified value"]
234    pub fn with_dir_style<S: Into<Style>>(mut self, dir_style: S) -> Self {
235        self.dir_style = dir_style.into();
236        self
237    }
238
239    /// Set the style of all highlighted non directories items. To set the style of the highlighted directories, use [`with_highlight_dir_style`](Theme::with_highlight_dir_style).
240    ///
241    /// Behind the scene, it use the [`List::highlight_style`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.highlight_style) method.
242    /// See its documentation for more.
243    ///
244    /// # Example
245    /// ```no_run
246    /// # use ratatui::prelude::*;
247    /// # use ratatui_explorer::Theme;
248    /// let theme = Theme::default().with_highlight_item_style(Style::default().add_modifier(Modifier::BOLD));
249    /// ```
250    #[inline]
251    #[must_use = "method moves the value of self and returns the modified value"]
252    pub fn with_highlight_item_style<S: Into<Style>>(mut self, highlight_item_style: S) -> Self {
253        self.highlight_item_style = highlight_item_style.into();
254        self
255    }
256
257    /// Set the style of all highlighted directories items. To set the style of the highlighted non directories, use [`with_highlight_item_style`](Theme::with_highlight_item_style).
258    ///
259    /// Behind the scene, it use the [`List::highlight_style`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.highlight_style) method.
260    /// See its documentation for more.
261    ///
262    /// # Example
263    /// ```no_run
264    /// # use ratatui::prelude::*;
265    /// # use ratatui_explorer::Theme;
266    /// let theme = Theme::default().with_highlight_dir_style(Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD));
267    /// ```
268    #[inline]
269    #[must_use = "method moves the value of self and returns the modified value"]
270    pub fn with_highlight_dir_style<S: Into<Style>>(mut self, highlight_dir_style: S) -> Self {
271        self.highlight_dir_style = highlight_dir_style.into();
272        self
273    }
274
275    /// Set the symbol used to highlight the selected item.
276    ///
277    /// Behind the scene, it use the [`List::highlight_symbol`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.highlight_symbol) method.
278    /// See its documentation for more.
279    ///
280    /// # Example
281    /// ```no_run
282    /// # use ratatui_explorer::Theme;
283    /// let theme = Theme::default().with_highlight_symbol("> ");
284    /// ```
285    #[inline]
286    #[must_use = "method moves the value of self and returns the modified value"]
287    pub fn with_highlight_symbol(mut self, highlight_symbol: &str) -> Self {
288        self.highlight_symbol = Some(highlight_symbol.to_owned());
289        self
290    }
291
292    /// Set the spacing between the highlighted item and the other items.
293    ///
294    /// Behind the scene, it use the [`List::highlight_spacing`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.highlight_spacing) method.
295    /// See its documentation for more.
296    ///
297    /// # Example
298    /// ```no_run
299    /// # use ratatui::widgets::*;
300    /// # use ratatui_explorer::Theme;
301    /// let theme = Theme::default().with_highlight_spacing(HighlightSpacing::Never);
302    /// ```
303    #[inline]
304    #[must_use = "method moves the value of self and returns the modified value"]
305    pub fn with_highlight_spacing(mut self, highlight_spacing: HighlightSpacing) -> Self {
306        self.highlight_spacing = highlight_spacing;
307        self
308    }
309
310    /// Sets the number of items around the currently selected item that should be kept visible.
311    ///
312    /// /// Behind the scene, it use the [`List::scroll_padding`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.scroll_padding) method.
313    /// See its documentation for more.
314    ///
315    /// # Example
316    /// ```no_run
317    /// # use ratatui::widgets::*;
318    /// # use ratatui_explorer::Theme;
319    /// let theme = Theme::default().with_scroll_padding(1);
320    /// ```
321    #[inline]
322    #[must_use = "method moves the value of self and returns the modified value"]
323    pub fn with_scroll_padding(mut self, scroll_padding: usize) -> Self {
324        self.scroll_padding = scroll_padding;
325        self
326    }
327
328    /// Add a top title factory to the theme.
329    ///
330    /// `title_top` is a function that take a reference to the current [`FileExplorer`] and returns
331    /// a [`Line`](https://docs.rs/ratatui/latest/ratatui/text/struct.Line.html)
332    /// to be displayed as a title at the top of the wrapping block (if it exist) of the file explorer. You can call
333    /// this function multiple times to add multiple titles.
334    ///
335    /// Behind the scene, it use the [`Block::title_top`](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html#method.title_top) method.
336    /// See its documentation for more.
337    ///
338    /// # Example
339    /// ```no_run
340    /// # use ratatui::prelude::*;
341    /// # use ratatui_explorer::{FileExplorer, Theme};
342    /// let theme = Theme::default()
343    ///     .with_title_top(|file_explorer: &FileExplorer| {
344    ///         Line::from(format!("cwd - {}", file_explorer.cwd().display()))
345    ///     })
346    ///     .with_title_top(|file_explorer: &FileExplorer| {
347    ///         Line::from(format!("{} files", file_explorer.files().len() - 1)).right_aligned()
348    ///     });
349    /// ```
350    #[inline]
351    #[must_use = "method moves the value of self and returns the modified value"]
352    pub fn with_title_top(
353        mut self,
354        title_top: impl Fn(&FileExplorer) -> Line<'_> + 'static + Send + Sync,
355    ) -> Self {
356        self.title_top.push(Arc::new(title_top));
357        self
358    }
359
360    /// Add a bottom title factory to the theme.
361    ///
362    /// `title_bottom` is a function that take a reference to the current [`FileExplorer`] and returns
363    /// a [`Line`](https://docs.rs/ratatui/latest/ratatui/text/struct.Line.html)
364    /// to be displayed as a title at the bottom of the wrapping block (if it exist) of the file explorer. You can call
365    /// this function multiple times to add multiple titles.
366    ///
367    /// Behind the scene, it use the [`Block::title_bottom`](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html#method.title_bottom) method.
368    /// See its documentation for more.
369    ///
370    /// # Example
371    /// ```no_run
372    /// # use ratatui::prelude::*;
373    /// # use ratatui_explorer::{FileExplorer, Theme};
374    /// let theme = Theme::default()
375    ///     .with_title_bottom(|file_explorer: &FileExplorer| {
376    ///         Line::from(format!("cwd - {}", file_explorer.cwd().display()))
377    ///     })
378    ///     .with_title_bottom(|file_explorer: &FileExplorer| {
379    ///         Line::from(format!("{} files", file_explorer.files().len() - 1)).right_aligned()
380    ///     });
381    /// ```
382    #[inline]
383    #[must_use = "method moves the value of self and returns the modified value"]
384    pub fn with_title_bottom(
385        mut self,
386        title_bottom: impl Fn(&FileExplorer) -> Line<'_> + 'static + Send + Sync,
387    ) -> Self {
388        self.title_bottom.push(Arc::new(title_bottom));
389        self
390    }
391
392    /// Returns the wrapping block (if it exist) of the file explorer of the theme.
393    #[inline]
394    #[must_use]
395    pub const fn block(&self) -> Option<&Block<'static>> {
396        self.block.as_ref()
397    }
398
399    /// Returns the style of the widget of the theme.
400    #[inline]
401    #[must_use]
402    pub const fn style(&self) -> &Style {
403        &self.style
404    }
405
406    /// Returns the style of the non directories items of the theme.
407    #[inline]
408    #[must_use]
409    pub const fn item_style(&self) -> &Style {
410        &self.item_style
411    }
412
413    /// Returns the style of the directories items of the theme.
414    #[inline]
415    #[must_use]
416    pub const fn dir_style(&self) -> &Style {
417        &self.dir_style
418    }
419
420    /// Returns the style of the highlighted non directories items of the theme.
421    #[inline]
422    #[must_use]
423    pub const fn highlight_item_style(&self) -> &Style {
424        &self.highlight_item_style
425    }
426
427    /// Returns the style of the highlighted directories items of the theme.
428    #[inline]
429    #[must_use]
430    pub const fn highlight_dir_style(&self) -> &Style {
431        &self.highlight_dir_style
432    }
433
434    /// Returns the symbol used to highlight the selected item of the theme.
435    #[inline]
436    #[must_use]
437    pub fn highlight_symbol(&self) -> Option<&str> {
438        self.highlight_symbol.as_deref()
439    }
440
441    /// Returns the spacing between the highlighted item and the other items of the theme.
442    #[inline]
443    #[must_use]
444    pub const fn highlight_spacing(&self) -> &HighlightSpacing {
445        &self.highlight_spacing
446    }
447
448    /// Returns the number of items around the currently selected item that should be kept visible.
449    #[inline]
450    #[must_use]
451    pub const fn scroll_padding(&self) -> usize {
452        self.scroll_padding
453    }
454
455    /// Returns the generated top titles of the theme.
456    #[inline]
457    #[must_use]
458    pub fn title_top<'a>(&self, file_explorer: &'a FileExplorer) -> Vec<Line<'a>> {
459        self.title_top
460            .iter()
461            .map(|title_top| title_top(file_explorer))
462            .collect()
463    }
464
465    /// Returns the generated bottom titles of the theme.
466    #[inline]
467    #[must_use]
468    pub fn title_bottom<'a>(&self, file_explorer: &'a FileExplorer) -> Vec<Line<'a>> {
469        self.title_bottom
470            .iter()
471            .map(|title_bottom| title_bottom(file_explorer))
472            .collect()
473    }
474}
475
476impl Default for Theme {
477    /// Return a slightly customized default theme. To get a theme with no style set, use [`new`](Theme::new).
478    ///
479    /// The theme will have a block with all borders, a white style for the items, a light blue style for the directories,
480    /// a dark gray background for all the highlighted items.
481    ///
482    /// # Example
483    /// ```no_run
484    /// # use ratatui_explorer::Theme;
485    /// let theme = Theme::default();
486    /// ```
487    fn default() -> Self {
488        Self {
489            block: Some(Block::default().borders(Borders::ALL)),
490            title_top: Vec::new(),
491            title_bottom: Vec::new(),
492            style: Style::default(),
493            item_style: Style::default().fg(Color::White),
494            dir_style: Style::default().fg(Color::LightBlue),
495            highlight_spacing: HighlightSpacing::Always,
496            highlight_item_style: Style::default().fg(Color::White).bg(Color::DarkGray),
497            highlight_dir_style: Style::default().fg(Color::LightBlue).bg(Color::DarkGray),
498            highlight_symbol: None,
499            scroll_padding: 0,
500        }
501    }
502}