use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use crate::tui::app::App;
const MAX_VISIBLE: usize = 8;
pub fn render_autocomplete(frame: &mut Frame, app: &App, content_area: Rect) {
let ac = match &app.autocomplete {
Some(ac) if ac.visible && !ac.filtered.is_empty() => ac,
_ => return,
};
let (anchor_x, anchor_y) = match app.autocomplete_anchor {
Some(pos) => pos,
None => return,
};
let bg = app.theme.background;
let text_color = app.theme.text;
let bright = app.theme.text_bright;
let dim = app.theme.dim;
let count = ac.filtered.len().min(MAX_VISIBLE);
let max_entry_width = ac.filtered.iter().map(|s| s.len()).max().unwrap_or(8);
let max_width = max_entry_width + 6;
let term_area = frame.area();
let max_popup_w: u16 = 40;
let popup_w = (max_width as u16)
.max(12)
.min(max_popup_w)
.min(content_area.width.saturating_sub(2));
let popup_h = (count as u16) + 2;
let cursor_bottom = anchor_y + 1; let y = if cursor_bottom + popup_h <= term_area.height {
cursor_bottom
} else {
anchor_y.saturating_sub(popup_h)
};
let text_inset: u16 = 4;
let x = anchor_x
.saturating_sub(text_inset)
.min(term_area.width.saturating_sub(popup_w));
let popup_area = Rect::new(x, y, popup_w, popup_h);
let scroll_start = if ac.selected >= MAX_VISIBLE {
ac.selected - MAX_VISIBLE + 1
} else {
0
};
let mut lines: Vec<Line> = Vec::new();
for (i, entry) in ac
.filtered
.iter()
.skip(scroll_start)
.take(MAX_VISIBLE)
.enumerate()
{
let actual_idx = scroll_start + i;
let is_selected = actual_idx == ac.selected;
let style = if is_selected {
Style::default()
.fg(bright)
.bg(app.theme.selection_bg)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(text_color).bg(bg)
};
let prefix = if is_selected { " \u{25B6} " } else { " " };
let label = format!(
"{:<width$}",
entry,
width = (popup_w as usize).saturating_sub(5)
);
lines.push(Line::from(vec![
Span::styled(prefix, style),
Span::styled(label, style),
]));
}
frame.render_widget(Clear, popup_area);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(dim).bg(bg))
.style(Style::default().bg(bg));
let paragraph = Paragraph::new(lines)
.block(block)
.style(Style::default().bg(bg));
frame.render_widget(paragraph, popup_area);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::app::{AutocompleteKind, AutocompleteState};
use crate::tui::render::test_helpers::*;
use insta::assert_snapshot;
#[test]
fn autocomplete_dropdown() {
let mut app = app_with_track(SIMPLE_TRACK_MD);
let mut ac = AutocompleteState::new(
AutocompleteKind::Tag,
vec!["core".into(), "design".into(), "bug".into(), "cc".into()],
);
ac.visible = true;
ac.filtered = vec!["core".into(), "cc".into()];
ac.selected = 0;
app.autocomplete = Some(ac);
app.autocomplete_anchor = Some((10, 5));
let output = render_to_string(TERM_W, TERM_H, |frame, area| {
render_autocomplete(frame, &app, area);
});
assert_snapshot!(output);
}
}