1use crossterm::event::{KeyCode, KeyEvent};
7use ratatui::buffer::Buffer;
8use ratatui::layout::{Position, Rect};
9use ratatui::style::Style;
10use ratatui::text::{Line, Span};
11use ratatui::widgets::{Paragraph, Widget};
12
13use crate::app::pointer::{self, Hit};
14use crate::render::context::RenderContext;
15use crate::widgets::ui::Surface;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct MenuItem {
21 pub key: &'static str,
22 pub label: &'static str,
23 pub action: Option<MenuAction>,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum MenuAction {
29 ChangeType,
31 CombineDatetime,
33}
34
35const fn keyed(key: &'static str, label: &'static str) -> MenuItem {
36 MenuItem {
37 key,
38 label,
39 action: None,
40 }
41}
42
43pub fn column_items(combine: bool) -> Vec<MenuItem> {
46 let mut items = vec![MenuItem {
47 key: "",
48 label: "Change type...",
49 action: Some(MenuAction::ChangeType),
50 }];
51 if combine {
52 items.push(MenuItem {
53 key: "",
54 label: "Combine into datetime...",
55 action: Some(MenuAction::CombineDatetime),
56 });
57 }
58 items
59}
60
61pub const ITEMS: &[MenuItem] = &[
63 keyed("+", "Filter to this value"),
64 keyed("-", "Filter out this value"),
65 keyed("F", "Value counts"),
66 keyed("[", "Sort ascending"),
67 keyed("]", "Sort descending"),
68 keyed("y", "Copy"),
69 keyed("Space", "Inspect row"),
70];
71
72impl MenuItem {
73 pub fn key_event(&self) -> KeyEvent {
75 let chord = datui_cli::keys::chord(self.key).expect("a menu key is one key");
76 crate::app::help::key_event(chord)
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct ContextMenu {
83 pub at: Position,
84 pub selected: usize,
85 pub items: Vec<MenuItem>,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum MenuKey {
91 Moved,
93 Run(KeyEvent),
95 Do(MenuAction),
97 Close,
99 Other,
101}
102
103impl ContextMenu {
104 pub fn with(at: Position, extra: Vec<MenuItem>) -> Self {
105 let mut items = ITEMS.to_vec();
106 items.extend(extra);
107 Self {
108 at,
109 selected: 0,
110 items,
111 }
112 }
113
114 pub fn chosen(&self, i: usize) -> Option<MenuKey> {
116 let item = self.items.get(i)?;
117 Some(match item.action {
118 Some(action) => MenuKey::Do(action),
119 None => MenuKey::Run(item.key_event()),
120 })
121 }
122
123 pub fn key(&mut self, event: &KeyEvent) -> MenuKey {
126 let n = self.items.len();
127 if !event.modifiers.is_empty() {
128 return MenuKey::Other;
129 }
130 match event.code {
131 KeyCode::Up | KeyCode::Char('k') => {
132 self.selected = (self.selected + n - 1) % n;
133 MenuKey::Moved
134 }
135 KeyCode::Down | KeyCode::Char('j') => {
136 self.selected = (self.selected + 1) % n;
137 MenuKey::Moved
138 }
139 KeyCode::Enter => self.chosen(self.selected).unwrap_or(MenuKey::Close),
140 KeyCode::Esc => MenuKey::Close,
141 _ => MenuKey::Other,
142 }
143 }
144
145 pub fn area(&self, screen: Rect) -> Rect {
148 let key_w = key_width(&self.items);
149 let label_w = self
150 .items
151 .iter()
152 .map(|item| crate::glyphs::display_width(item.label))
153 .max()
154 .unwrap_or(0);
155 let width = (4 + 1 + key_w + 2 + label_w) as u16;
157 let height = self.items.len() as u16 + 2;
158 let width = width.min(screen.width);
159 let height = height.min(screen.height);
160 let x = self
161 .at
162 .x
163 .saturating_add(1)
164 .min(screen.right().saturating_sub(width))
165 .max(screen.x);
166 let below = self.at.y.saturating_add(1);
167 let y = if below.saturating_add(height) <= screen.bottom() {
168 below
169 } else {
170 screen.bottom().saturating_sub(height).max(screen.y)
171 };
172 Rect::new(x, y, width, height)
173 }
174
175 pub fn render(&self, screen: Rect, buf: &mut Buffer, ctx: &RenderContext) {
177 let area = self.area(screen);
178 pointer::record(area, Hit::Menu);
179 let content = Surface::new("").render(area, buf, ctx);
180 let g = crate::glyphs::get();
181 let key_w = key_width(&self.items);
182 for (i, item) in self.items.iter().enumerate().take(content.height as usize) {
183 let row = Rect {
184 y: content.y + i as u16,
185 height: 1,
186 ..content
187 };
188 let selected = i == self.selected;
189 let key = format!(
190 "{}{}",
191 item.key,
192 " ".repeat(key_w.saturating_sub(crate::glyphs::display_width(item.key)))
193 );
194 let line = Line::from(vec![
195 Span::styled(
196 if selected { g.rail } else { " " },
197 Style::default().fg(ctx.accent),
198 ),
199 Span::styled(key, Style::default().fg(ctx.keybind_hints)),
200 Span::raw(" "),
201 Span::styled(item.label, Style::default().fg(ctx.text_primary)),
202 ]);
203 let mut paragraph = Paragraph::new(line);
204 if selected {
205 paragraph = paragraph.style(ctx.highlight_style());
206 }
207 paragraph.render(row, buf);
208 pointer::record(row, Hit::MenuItem(i));
209 }
210 }
211}
212
213fn key_width(items: &[MenuItem]) -> usize {
214 items
215 .iter()
216 .map(|item| crate::glyphs::display_width(item.key))
217 .max()
218 .unwrap_or(0)
219}
220
221#[cfg(test)]
222mod tests;