use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, List, ListItem};
use crate::app::App;
const MAX_WIDTH: u16 = 60;
const MAX_HEIGHT: u16 = 10;
pub(super) fn draw_completion(f: &mut Frame, app: &App, buf_area: Rect) {
let Some(state) = app.completion.as_ref() else {
return;
};
if state.is_empty() {
return;
}
let row = state.prefix_start.row;
let Some(rel_y) = app.visual_row_offset(row) else {
return;
};
let gutter_width: u16 = 1 + 5;
let anchor_x = buf_area.x + gutter_width + state.prefix_start.col as u16;
let anchor_y = buf_area.y + rel_y;
let label_w = state
.filtered
.iter()
.map(|i| state.items[*i].label.chars().count() as u16)
.max()
.unwrap_or(0)
.min(MAX_WIDTH.saturating_sub(6));
let inner_w = (label_w + 4).min(MAX_WIDTH);
let popup_w = (inner_w + 2).min(buf_area.width);
let visible = state.filtered.len() as u16;
let popup_h = (visible + 2).min(MAX_HEIGHT + 2);
let below_y = anchor_y.saturating_add(1);
let space_below = buf_area.bottom().saturating_sub(below_y);
let y = if space_below >= popup_h {
below_y
} else if anchor_y >= buf_area.y + popup_h {
anchor_y - popup_h
} else {
below_y.min(buf_area.bottom().saturating_sub(1))
};
let max_x = buf_area.right().saturating_sub(popup_w);
let x = anchor_x.min(max_x).max(buf_area.x);
let area = Rect {
x,
y,
width: popup_w,
height: popup_h.min(buf_area.bottom().saturating_sub(y)),
};
if area.width <= 2 || area.height <= 2 {
return;
}
f.render_widget(Clear, area);
let block = Block::default()
.borders(Borders::ALL)
.style(Style::default().bg(Color::Rgb(30, 30, 40)));
let inner = block.inner(area);
f.render_widget(block, area);
let body_h = inner.height as usize;
let scroll = state
.selected
.saturating_sub(body_h.saturating_sub(1));
let inner_w = inner.width as usize;
let items: Vec<ListItem> = state
.filtered
.iter()
.enumerate()
.skip(scroll)
.take(body_h)
.map(|(i, item_idx)| {
let item = &state.items[*item_idx];
let is_sel = i == state.selected;
let badge = kind_badge(item.kind);
let badge_w = 3 + 1;
let label_chars = item.label.chars().count();
let detail = item.detail.as_deref().unwrap_or("");
let detail_chars = detail.chars().count();
let usable = inner_w.saturating_sub(badge_w);
let (label_room, detail_room) = if detail_chars == 0 || label_chars >= usable {
(usable, 0)
} else {
let max_detail = usable.saturating_sub(label_chars).saturating_sub(1);
(label_chars.min(usable), detail_chars.min(max_detail))
};
let label = truncate(&item.label, label_room);
let detail_text = truncate(detail, detail_room);
let row_style = if is_sel {
Style::default()
.bg(Color::Rgb(58, 78, 122))
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
let badge_style = if is_sel {
row_style
} else {
Style::default().fg(Color::Rgb(140, 160, 200))
};
let detail_style = if is_sel {
row_style
} else {
Style::default().fg(Color::Rgb(150, 150, 150))
};
let gap = inner_w
.saturating_sub(badge_w)
.saturating_sub(label.chars().count())
.saturating_sub(detail_text.chars().count());
let mut spans = vec![
Span::styled(format!("{:<3}", badge), badge_style),
Span::styled(format!(" {}", label), row_style),
];
if !detail_text.is_empty() {
spans.push(Span::styled(" ".repeat(gap), row_style));
spans.push(Span::styled(detail_text, detail_style));
}
ListItem::new(Line::from(spans))
})
.collect();
f.render_widget(List::new(items), inner);
}
fn kind_badge(kind: u8) -> &'static str {
match kind {
1 => "Txt",
2 => "Fn",
3 => "Fn",
4 => "Ctr", 5 => "Fld", 6 => "Var",
7 => "Cls", 8 => "Itf", 9 => "Mod",
10 => "Prp", 11 => "Uni", 12 => "Val", 13 => "Enu", 14 => "Kw",
15 => "Sni", 16 => "Col", 17 => "Fil", 18 => "Ref",
19 => "Dir", 20 => "EnM", 21 => "Cst", 22 => "Str", 23 => "Evt", 24 => "Op",
25 => "Tpr", _ => "·",
}
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
if max == 0 {
return String::new();
}
let mut out: String = s.chars().take(max.saturating_sub(1)).collect();
out.push('…');
out
}