Skip to main content

stynx_code_tui/widgets/
slash_popover.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::Rect,
4    style::{Modifier, Style},
5    text::{Line, Span},
6    widgets::{Block, Borders, Clear, Paragraph, Widget},
7};
8
9use crate::state::InputState;
10use crate::theme;
11
12pub struct SlashPopover<'a> {
13    pub state: &'a InputState,
14    pub anchor: Rect,
15}
16
17impl<'a> SlashPopover<'a> {
18    pub fn new(state: &'a InputState, anchor: Rect) -> Self {
19        Self { state, anchor }
20    }
21}
22
23impl<'a> Widget for SlashPopover<'a> {
24    fn render(self, _area: Rect, buf: &mut Buffer) {
25        if self.state.slash_matches.is_empty() { return; }
26        let max_show = 8usize;
27        let count = self.state.slash_matches.len().min(max_show) as u16;
28        let max_cmd = self
29            .state
30            .slash_matches
31            .iter()
32            .map(|(c, _)| c.len())
33            .max()
34            .unwrap_or(8);
35        let max_desc = self
36            .state
37            .slash_matches
38            .iter()
39            .map(|(_, d)| d.len())
40            .max()
41            .unwrap_or(20);
42        let inner_w = (max_cmd + max_desc + 6) as u16;
43        let width = inner_w.min(self.anchor.width.saturating_sub(2)).max(20);
44        let height = count + 2;
45
46        let x = self.anchor.x + 1;
47        let y = self.anchor.y.saturating_sub(height);
48        if y < 1 { return; }
49        let rect = Rect { x, y, width, height };
50        Clear.render(rect, buf);
51
52        let block = Block::default()
53            .borders(Borders::ALL)
54            .border_style(Style::default().fg(theme::BORDER_ACTIVE()))
55            .style(Style::default().bg(theme::BACKGROUND_PANEL()));
56        let inner = block.inner(rect);
57        block.render(rect, buf);
58
59        let mut lines: Vec<Line<'static>> = Vec::new();
60        let selected = self.state.slash_selected.min(self.state.slash_matches.len().saturating_sub(1));
61        for (i, (cmd, desc)) in self.state.slash_matches.iter().take(max_show).enumerate() {
62            let is_sel = i == selected;
63            let bg = if is_sel { theme::BACKGROUND_MENU() } else { theme::BACKGROUND_PANEL() };
64            let cmd_padded = format!("{:width$}", cmd, width = max_cmd);
65            lines.push(Line::from(vec![
66                Span::styled(
67                    format!(" {cmd_padded}  "),
68                    Style::default()
69                        .fg(if is_sel { theme::ACCENT() } else { theme::TEXT() })
70                        .bg(bg)
71                        .add_modifier(Modifier::BOLD),
72                ),
73                Span::styled(
74                    desc.clone(),
75                    Style::default().fg(theme::TEXT_MUTED()).bg(bg),
76                ),
77            ]));
78        }
79
80        Paragraph::new(lines)
81            .style(Style::default().bg(theme::BACKGROUND_PANEL()))
82            .render(inner, buf);
83    }
84}