@@ -0,0 +1,189 @@
1+use crate::compositor::{Component, Compositor, Context, EventResult};
2+use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
3+use tui::buffer::Buffer as Surface;
4+use tui::{
5+ layout::Rect,
6+ style::{Color, Style},
7+ widgets::{Block, Borders},
8+};
9+
10+use std::borrow::Cow;
11+
12+use helix_core::Position;
13+use helix_view::Editor;
14+
15+// TODO: factor out a popup component that we can reuse for displaying docs on autocomplete,
16+// diagnostics popups, etc.
17+
18+pub struct Menu<T> {
19+ options: Vec<T>,
20+
21+ cursor: usize,
22+
23+ position: Position,
24+
25+ format_fn: Box<dyn Fn(&T) -> Cow<str>>,
26+ callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
27+}
28+
29+impl<T> Menu<T> {
30+ // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
31+ // rendering)
32+ pub fn new(
33+ options: Vec<T>,
34+ format_fn: impl Fn(&T) -> Cow<str> + 'static,
35+ callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
36+ ) -> Self {
37+ Self {
38+ options,
39+ cursor: 0,
40+ position: Position::default(),
41+ format_fn: Box::new(format_fn),
42+ callback_fn: Box::new(callback_fn),
43+ }
44+ }
45+
46+ pub fn set_position(&mut self, pos: Position) {
47+ self.position = pos;
48+ }
49+
50+ pub fn move_up(&mut self) {
51+ self.cursor = self.cursor.saturating_sub(1);
52+ }
53+
54+ pub fn move_down(&mut self) {
55+ // TODO: len - 1
56+ if self.cursor < self.options.len() {
57+ self.cursor += 1;
58+ }
59+ }
60+
61+ pub fn selection(&self) -> Option<&T> {
62+ self.options.get(self.cursor)
63+ }
64+}
65+
66+use super::PromptEvent as MenuEvent;
67+
68+impl<T> Component for Menu<T> {
69+ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
70+ let event = match event {
71+ Event::Key(event) => event,
72+ _ => return EventResult::Ignored,
73+ };
74+
75+ let close_fn = EventResult::Consumed(Some(Box::new(
76+ |compositor: &mut Compositor, editor: &mut Editor| {
77+ // remove the layer
78+ compositor.pop();
79+ },
80+ )));
81+
82+ match event {
83+ // esc or ctrl-c aborts the completion and closes the menu
84+ KeyEvent {
85+ code: KeyCode::Esc, ..
86+ }
87+ | KeyEvent {
88+ code: KeyCode::Char('c'),
89+ modifiers: KeyModifiers::CONTROL,
90+ } => {
91+ (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort);
92+ return close_fn;
93+ }
94+ // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
95+ KeyEvent {
96+ code: KeyCode::Tab,
97+ modifiers: KeyModifiers::SHIFT,
98+ }
99+ | KeyEvent {
100+ code: KeyCode::Up, ..
101+ }
102+ | KeyEvent {
103+ code: KeyCode::Char('p'),
104+ modifiers: KeyModifiers::CONTROL,
105+ } => {
106+ self.move_up();
107+ (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
108+ return EventResult::Consumed(None);
109+ }
110+ // arrow down/ctrl-n/tab advances completion choice (including updating the doc)
111+ KeyEvent {
112+ code: KeyCode::Tab,
113+ modifiers: KeyModifiers::NONE,
114+ }
115+ | KeyEvent {
116+ code: KeyCode::Down,
117+ ..
118+ }
119+ | KeyEvent {
120+ code: KeyCode::Char('n'),
121+ modifiers: KeyModifiers::CONTROL,
122+ } => {
123+ self.move_down();
124+ (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
125+ return EventResult::Consumed(None);
126+ }
127+ KeyEvent {
128+ code: KeyCode::Enter,
129+ ..
130+ } => {
131+ (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Validate);
132+ return close_fn;
133+ }
134+ // KeyEvent {
135+ // code: KeyCode::Char(c),
136+ // modifiers: KeyModifiers::NONE,
137+ // } => {
138+ // self.insert_char(c);
139+ // (self.callback_fn)(cx.editor, &self.line, MenuEvent::Update);
140+ // }
141+
142+ // / -> edit_filter?
143+ //
144+ // enter confirms the match and closes the menu
145+ // typing filters the menu
146+ // if we run out of options the menu closes itself
147+ _ => (),
148+ }
149+ // for some events, we want to process them but send ignore, specifically all input except
150+ // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
151+ // EventResult::Consumed(None)
152+ EventResult::Ignored
153+ }
154+ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
155+ // render a box at x, y. Width equal to max width of item.
156+ // initially limit to n items, add support for scrolling
157+ //
158+ const MAX: usize = 5;
159+ let rows = std::cmp::min(self.options.len(), MAX) as u16;
160+ let area = Rect::new(self.position.col as u16, self.position.row as u16, 30, rows);
161+
162+ // clear area
163+ let background = cx.editor.theme.get("ui.popup");
164+ for y in area.top()..area.bottom() {
165+ for x in area.left()..area.right() {
166+ let cell = surface.get_mut(x, y);
167+ cell.reset();
168+ // cell.symbol.clear();
169+ cell.set_style(background);
170+ }
171+ }
172+
173+ // -- Render the contents:
174+
175+ let style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender
176+ let selected = Style::default().fg(Color::Rgb(255, 255, 255));
177+
178+ for (i, option) in self.options.iter().take(rows as usize).enumerate() {
179+ // TODO: set bg for the whole row if selected
180+ surface.set_stringn(
181+ area.x,
182+ area.y + i as u16,
183+ (self.format_fn)(option),
184+ area.width as usize - 1,
185+ if i == self.cursor { selected } else { style },
186+ );
187+ }
188+ }
189+}