entrust_dialog/select/
widget.rs1use 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}