entrust_dialog/select/
widget.rs

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