Skip to main content

entrust_dialog/select/
widget.rs

1use crate::select::SelectDialog;
2use crate::select::filter::{FilteredItem, apply_filter};
3use crate::text::Text;
4use crate::theme::Theme;
5use ratatui::buffer::Buffer;
6use ratatui::layout::{Constraint, Layout, Rect};
7use ratatui::prelude::{Line, Span, StatefulWidget, Widget};
8use ratatui::widgets::{HighlightSpacing, List, Scrollbar, ScrollbarOrientation, ScrollbarState};
9use std::cmp::max;
10use std::sync::OnceLock;
11
12fn width(select_dialog: &SelectDialog) -> u16 {
13    static CELL: OnceLock<u16> = OnceLock::new();
14    *CELL.get_or_init(|| {
15        let max_item_width = select_dialog
16            .items
17            .iter()
18            .map(|i| i.content.len())
19            .max()
20            .unwrap_or(0);
21        let prefix_length = select_dialog.theme.select_indicator.len();
22        let width = max(25, prefix_length + max_item_width + 3);
23        u16::try_from(width).unwrap_or(u16::MAX)
24    })
25}
26
27impl<'a> Widget for &mut SelectDialog<'a> {
28    fn render(self, area: Rect, buf: &mut Buffer)
29    where
30        Self: Sized,
31    {
32        let (header_area, separator_area, list_area, scrollbar_area) = {
33            let (header_area, separator_area, list_scroll_area) = {
34                let filter_height = if self.filter_dialog.is_some() { 1 } else { 0 };
35                let separator_height = filter_height;
36                let rects = Layout::vertical(vec![
37                    Constraint::Length(filter_height),
38                    Constraint::Length(separator_height),
39                    Constraint::Percentage(100),
40                ])
41                .split(area);
42                (rects[0], rects[1], rects[2])
43            };
44            let rects =
45                Layout::horizontal(vec![Constraint::Length(width(self)), Constraint::Length(1)])
46                    .split(list_scroll_area);
47            (header_area, separator_area, rects[0], rects[1])
48        };
49
50        if let Some(ref mut filter_dialog) = self.filter_dialog {
51            filter_dialog.render(header_area, buf);
52            Text::from("────────────────────").render(separator_area, buf);
53        }
54
55        let lines: Vec<Line> = if let Some(ref mut filter_dialog) = self.filter_dialog {
56            let filtered = apply_filter(
57                self.items.as_slice(),
58                &mut self.list_state,
59                filter_dialog.current_content().as_str(),
60            );
61            filtered
62                .iter()
63                .map(|s| render_filtered_item(s, &self.theme))
64                .collect()
65        } else {
66            self.items
67                .iter()
68                .map(|i| i.content.as_ref().into())
69                .collect()
70        };
71        let len = lines.len();
72
73        let list = List::new(lines)
74            .highlight_symbol(self.theme.select_indicator.as_str())
75            .highlight_style(self.theme.selected_style)
76            .highlight_spacing(HighlightSpacing::Always);
77        StatefulWidget::render(list, list_area, buf, &mut self.list_state);
78
79        if len > list_area.height as usize {
80            let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight);
81            let mut scrollbar_state = ScrollbarState::default()
82                .content_length(len - list_area.height as usize)
83                .position(self.list_state.offset());
84            StatefulWidget::render(scrollbar, scrollbar_area, buf, &mut scrollbar_state);
85        }
86    }
87}
88
89fn render_filtered_item(item: &FilteredItem, theme: &Theme) -> Line<'static> {
90    let char_spans: Vec<Span> = item
91        .item
92        .content
93        .chars()
94        .enumerate()
95        .map(|(index, char)| {
96            let string = char.to_string();
97            if item.matching_chars.contains(&index) {
98                Span::styled(string, theme.match_style)
99            } else {
100                Span::raw(string)
101            }
102        })
103        .collect();
104    char_spans.into()
105}