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