use crossterm::event::{KeyCode, KeyEvent};
use ratatui::buffer::Buffer;
use ratatui::layout::{Position, Rect};
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Widget};
use crate::app::pointer::{self, Hit};
use crate::render::context::RenderContext;
use crate::widgets::ui::Surface;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MenuItem {
pub key: &'static str,
pub label: &'static str,
pub action: Option<MenuAction>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MenuAction {
ChangeType,
CombineDatetime,
}
const fn keyed(key: &'static str, label: &'static str) -> MenuItem {
MenuItem {
key,
label,
action: None,
}
}
pub fn column_items(combine: bool) -> Vec<MenuItem> {
let mut items = vec![MenuItem {
key: "",
label: "Change type...",
action: Some(MenuAction::ChangeType),
}];
if combine {
items.push(MenuItem {
key: "",
label: "Combine into datetime...",
action: Some(MenuAction::CombineDatetime),
});
}
items
}
pub const ITEMS: &[MenuItem] = &[
keyed("+", "Filter to this value"),
keyed("-", "Filter out this value"),
keyed("F", "Value counts"),
keyed("[", "Sort ascending"),
keyed("]", "Sort descending"),
keyed("y", "Copy"),
keyed("Space", "Inspect row"),
];
impl MenuItem {
pub fn key_event(&self) -> KeyEvent {
let chord = datui_cli::keys::chord(self.key).expect("a menu key is one key");
crate::app::help::key_event(chord)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContextMenu {
pub at: Position,
pub selected: usize,
pub items: Vec<MenuItem>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MenuKey {
Moved,
Run(KeyEvent),
Do(MenuAction),
Close,
Other,
}
impl ContextMenu {
pub fn with(at: Position, extra: Vec<MenuItem>) -> Self {
let mut items = ITEMS.to_vec();
items.extend(extra);
Self {
at,
selected: 0,
items,
}
}
pub fn chosen(&self, i: usize) -> Option<MenuKey> {
let item = self.items.get(i)?;
Some(match item.action {
Some(action) => MenuKey::Do(action),
None => MenuKey::Run(item.key_event()),
})
}
pub fn key(&mut self, event: &KeyEvent) -> MenuKey {
let n = self.items.len();
if !event.modifiers.is_empty() {
return MenuKey::Other;
}
match event.code {
KeyCode::Up | KeyCode::Char('k') => {
self.selected = (self.selected + n - 1) % n;
MenuKey::Moved
}
KeyCode::Down | KeyCode::Char('j') => {
self.selected = (self.selected + 1) % n;
MenuKey::Moved
}
KeyCode::Enter => self.chosen(self.selected).unwrap_or(MenuKey::Close),
KeyCode::Esc => MenuKey::Close,
_ => MenuKey::Other,
}
}
pub fn area(&self, screen: Rect) -> Rect {
let key_w = key_width(&self.items);
let label_w = self
.items
.iter()
.map(|item| crate::glyphs::display_width(item.label))
.max()
.unwrap_or(0);
let width = (4 + 1 + key_w + 2 + label_w) as u16;
let height = self.items.len() as u16 + 2;
let width = width.min(screen.width);
let height = height.min(screen.height);
let x = self
.at
.x
.saturating_add(1)
.min(screen.right().saturating_sub(width))
.max(screen.x);
let below = self.at.y.saturating_add(1);
let y = if below.saturating_add(height) <= screen.bottom() {
below
} else {
screen.bottom().saturating_sub(height).max(screen.y)
};
Rect::new(x, y, width, height)
}
pub fn render(&self, screen: Rect, buf: &mut Buffer, ctx: &RenderContext) {
let area = self.area(screen);
pointer::record(area, Hit::Menu);
let content = Surface::new("").render(area, buf, ctx);
let g = crate::glyphs::get();
let key_w = key_width(&self.items);
for (i, item) in self.items.iter().enumerate().take(content.height as usize) {
let row = Rect {
y: content.y + i as u16,
height: 1,
..content
};
let selected = i == self.selected;
let key = format!(
"{}{}",
item.key,
" ".repeat(key_w.saturating_sub(crate::glyphs::display_width(item.key)))
);
let line = Line::from(vec![
Span::styled(
if selected { g.rail } else { " " },
Style::default().fg(ctx.accent),
),
Span::styled(key, Style::default().fg(ctx.keybind_hints)),
Span::raw(" "),
Span::styled(item.label, Style::default().fg(ctx.text_primary)),
]);
let mut paragraph = Paragraph::new(line);
if selected {
paragraph = paragraph.style(ctx.highlight_style());
}
paragraph.render(row, buf);
pointer::record(row, Hit::MenuItem(i));
}
}
}
fn key_width(items: &[MenuItem]) -> usize {
items
.iter()
.map(|item| crate::glyphs::display_width(item.key))
.max()
.unwrap_or(0)
}
#[cfg(test)]
mod tests;