Skip to main content

oracle_lib/ui/components/
list.rs

1//! Selectable list component
2
3use 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
13/// A selectable list widget with fuzzy highlight support
14pub 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/// Create a line with highlighted matching characters for fuzzy search
83#[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    // Find all matching character positions
94    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    // Build spans with highlights
108    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/// Create a line with substring match highlighted
134#[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}