#![deny(clippy::arithmetic_side_effects)]
use crate::theme::Bg;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Modifier;
use ratatui::text::Span;
use ratatui::widgets::Widget;
use ratatui_textarea::TextArea;
use crate::ui::{Ctx, PAD_Y, centered, cols, fill, label_hint, list_row, padded, text};
const POPUP: Bg = Bg::ContainerHigh;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PaletteItem {
pub label: String,
pub hint: String,
}
pub struct Palette<'a> {
pub ctx: Ctx<'a>,
pub prompt: &'a str,
pub input: &'a TextArea<'static>,
pub items: &'a [PaletteItem],
pub selected: usize,
}
pub const PALETTE_ROWS: usize = 10;
impl Widget for Palette<'_> {
fn render(self, screen: Rect, buf: &mut Buffer) {
let theme = self.ctx.theme;
let max_rows = PALETTE_ROWS.max(usize::from(screen.height / 2));
let fits = usize::from(screen.height.saturating_sub(4 + 2 * PAD_Y)).max(1);
let rows = self.items.len().clamp(1, max_rows).min(fits);
let widest = self
.items
.iter()
.map(|i| {
text::width(&i.label)
.saturating_add(text::width(&i.hint))
.saturating_add(6)
})
.max()
.unwrap_or(0);
let width = cols(widest.clamp(72, 120)).min(screen.width.saturating_sub(4));
let area = centered(screen, width, cols(rows).saturating_add(2 + 2 * PAD_Y), 2);
fill(buf, area, theme, POPUP);
let inner = padded(area);
let prompt = Rect { height: 1, ..inner };
Span::styled(
self.prompt.to_owned(),
theme.accent(POPUP).add_modifier(Modifier::BOLD),
)
.render(prompt, buf);
let count = if self.items.len() > rows {
format!("{}/{}", self.selected.saturating_add(1), self.items.len())
} else {
String::new()
};
let count_w = cols(text::width(&count)).min(prompt.width);
Span::styled(count, theme.meta(POPUP)).render(
Rect {
x: prompt.right().saturating_sub(count_w),
width: count_w,
..prompt
},
buf,
);
let offset = cols(text::width(self.prompt))
.saturating_add(1)
.min(prompt.width);
let field = Rect {
x: prompt.x.saturating_add(offset),
width: prompt
.width
.saturating_sub(offset)
.saturating_sub(count_w.saturating_add(1)),
..prompt
};
let room = usize::from(field.width).saturating_sub(1);
let hint = self.input.placeholder_text();
let typed = self.input.lines().first().map_or("", String::as_str);
if self.input.is_empty() && text::width(hint) > room {
let mut input = self.input.clone();
input.set_placeholder_text(text::truncate(hint, room));
input.render(field, buf);
} else if text::width(typed) > room && field.width > 2 {
let col = self.input.cursor().1;
let narrower = field.width.saturating_sub(1);
let scrolled = col >= usize::from(narrower);
let (dots, rest) = if scrolled {
(field.x, field.x.saturating_add(1))
} else {
(field.right().saturating_sub(1), field.x)
};
self.input.render(
Rect {
x: rest,
width: narrower,
..field
},
buf,
);
crate::ui::put(buf, dots, field.y, "…", theme.meta(POPUP));
} else {
self.input.render(field, buf);
}
let list_top = inner.y.saturating_add(2);
let rows = rows.min(usize::from(inner.bottom().saturating_sub(list_top)));
if rows == 0 {
return;
}
if self.items.is_empty() {
Span::styled("No matches", theme.meta(POPUP)).render(
Rect {
y: list_top,
height: 1,
..inner
},
buf,
);
return;
}
let first = self.selected.saturating_add(1).saturating_sub(rows);
let shown = self.items.iter().enumerate().skip(first).take(rows);
for ((i, item), y) in shown.zip(list_top..inner.bottom()) {
let (row, bg) = list_row(buf, theme, area, y, i == self.selected, POPUP);
let label = Span::styled(item.label.as_str(), theme.body(bg));
label_hint(
buf,
row,
&[label],
Span::styled(item.hint.as_str(), theme.meta(bg)),
);
}
}
}