oracle_lib/ui/components/
list.rs1use ratatui::{
4 buffer::Buffer,
5 layout::Rect,
6 style::{Color, Modifier, Style},
7 text::{Line, Span},
8 widgets::{Block, Borders, List, ListItem, ListState, StatefulWidget},
9};
10
11use crate::ui::theme::Theme;
12
13pub struct SelectableList<'a> {
15 items: Vec<ListItem<'a>>,
16 title: Option<&'a str>,
17 theme: &'a Theme,
18 highlight_style: Style,
19 border_style: Style,
20}
21
22impl<'a> SelectableList<'a> {
23 pub fn new(theme: &'a Theme) -> Self {
24 Self {
25 items: Vec::new(),
26 title: None,
27 theme,
28 highlight_style: theme.style_selected(),
29 border_style: theme.style_border(),
30 }
31 }
32
33 pub fn items<I, T>(mut self, items: I) -> Self
34 where
35 I: IntoIterator<Item = T>,
36 T: Into<ListItem<'a>>,
37 {
38 self.items = items.into_iter().map(Into::into).collect();
39 self
40 }
41
42 pub fn title(mut self, title: &'a str) -> Self {
43 self.title = Some(title);
44 self
45 }
46
47 pub fn focused(mut self, focused: bool) -> Self {
48 if focused {
49 self.border_style = self.theme.style_border_focused();
50 }
51 self
52 }
53
54 #[allow(dead_code)]
55 pub fn highlight_style(mut self, style: Style) -> Self {
56 self.highlight_style = style;
57 self
58 }
59}
60
61impl<'a> StatefulWidget for SelectableList<'a> {
62 type State = ListState;
63
64 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
65 let mut block = Block::default()
66 .borders(Borders::ALL)
67 .border_style(self.border_style);
68
69 if let Some(title) = self.title {
70 block = block.title(format!(" {} ", title));
71 }
72
73 let list = List::new(self.items)
74 .block(block)
75 .highlight_style(self.highlight_style)
76 .highlight_symbol("▸ ");
77
78 StatefulWidget::render(list, area, buf, state);
79 }
80}
81
82#[allow(dead_code)]
84pub fn highlight_fuzzy_match<'a>(text: &'a str, query: &str, theme: &Theme) -> Line<'a> {
85 if query.is_empty() {
86 return Line::from(text.to_string());
87 }
88
89 let lower_query = query.to_lowercase();
90 let mut spans = Vec::new();
91 let mut last_end = 0;
92
93 let mut query_chars = lower_query.chars().peekable();
95 let chars: Vec<(usize, char)> = text.char_indices().collect();
96 let mut match_indices = Vec::new();
97
98 for (i, c) in &chars {
99 if let Some(&qc) = query_chars.peek() {
100 if c.to_lowercase().next() == qc.to_lowercase().next() {
101 match_indices.push(*i);
102 query_chars.next();
103 }
104 }
105 }
106
107 for idx in match_indices {
109 if idx > last_end {
110 spans.push(Span::raw(text[last_end..idx].to_string()));
111 }
112 let char_len = text[idx..]
113 .chars()
114 .next()
115 .map(|c| c.len_utf8())
116 .unwrap_or(1);
117 spans.push(Span::styled(
118 text[idx..idx + char_len].to_string(),
119 Style::default()
120 .fg(theme.accent)
121 .add_modifier(Modifier::BOLD),
122 ));
123 last_end = idx + char_len;
124 }
125
126 if last_end < text.len() {
127 spans.push(Span::raw(text[last_end..].to_string()));
128 }
129
130 Line::from(spans)
131}
132
133#[allow(dead_code)]
135pub fn highlight_substring_match<'a>(text: &'a str, query: &str, theme: &Theme) -> Line<'a> {
136 if query.is_empty() {
137 return Line::from(text.to_string());
138 }
139
140 let lower_text = text.to_lowercase();
141 let lower_query = query.to_lowercase();
142
143 if let Some(start) = lower_text.find(&lower_query) {
144 let end = start + query.len();
145 Line::from(vec![
146 Span::raw(text[..start].to_string()),
147 Span::styled(
148 text[start..end].to_string(),
149 Style::default()
150 .bg(theme.accent)
151 .fg(Color::Black)
152 .add_modifier(Modifier::BOLD),
153 ),
154 Span::raw(text[end..].to_string()),
155 ])
156 } else {
157 Line::from(text.to_string())
158 }
159}