nvim_gtk/
popup_menu.rs

1use std::cell::RefCell;
2use std::cmp::min;
3use std::iter;
4use std::rc::Rc;
5
6use gdk::{EventButton, EventType};
7use glib;
8use gtk;
9use gtk::prelude::*;
10use pango;
11
12use neovim_lib::{Neovim, NeovimApi};
13
14use crate::highlight::HighlightMap;
15use crate::input;
16use crate::nvim::{self, ErrorReport, NeovimClient};
17use crate::render;
18
19const MAX_VISIBLE_ROWS: i32 = 10;
20
21struct State {
22    nvim: Option<Rc<nvim::NeovimClient>>,
23    renderer: gtk::CellRendererText,
24    tree: gtk::TreeView,
25    scroll: gtk::ScrolledWindow,
26    css_provider: gtk::CssProvider,
27    info_label: gtk::Label,
28    word_column: gtk::TreeViewColumn,
29    kind_column: gtk::TreeViewColumn,
30    menu_column: gtk::TreeViewColumn,
31    preview: bool,
32}
33
34impl State {
35    pub fn new() -> Self {
36        let tree = gtk::TreeView::new();
37        tree.get_selection().set_mode(gtk::SelectionMode::Single);
38        let css_provider = gtk::CssProvider::new();
39
40        let style_context = tree.get_style_context();
41        style_context.add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
42
43        let renderer = gtk::CellRendererText::new();
44        renderer.set_property_ellipsize(pango::EllipsizeMode::End);
45
46        // word
47        let word_column = gtk::TreeViewColumn::new();
48        word_column.pack_start(&renderer, true);
49        word_column.add_attribute(&renderer, "text", 0);
50        tree.append_column(&word_column);
51
52        // kind
53        let kind_column = gtk::TreeViewColumn::new();
54        kind_column.pack_start(&renderer, true);
55        kind_column.add_attribute(&renderer, "text", 1);
56        tree.append_column(&kind_column);
57
58        // menu
59        let menu_column = gtk::TreeViewColumn::new();
60        menu_column.pack_start(&renderer, true);
61        menu_column.add_attribute(&renderer, "text", 2);
62        tree.append_column(&menu_column);
63
64        let info_label = gtk::Label::new(None);
65        info_label.set_line_wrap(true);
66
67        let scroll = gtk::ScrolledWindow::new(
68            Option::<&gtk::Adjustment>::None,
69            Option::<&gtk::Adjustment>::None,
70        );
71
72        tree.connect_size_allocate(
73            clone!(scroll, renderer => move |tree, _| on_treeview_allocate(&scroll, tree, &renderer)),
74        );
75
76        State {
77            nvim: None,
78            tree,
79            renderer,
80            scroll,
81            css_provider,
82            info_label,
83            word_column,
84            kind_column,
85            menu_column,
86            preview: true,
87        }
88    }
89
90    fn before_show(&mut self, ctx: PopupMenuContext) {
91        if self.nvim.is_none() {
92            self.nvim = Some(ctx.nvim.clone());
93        }
94
95        self.scroll.set_max_content_width(ctx.max_width);
96        self.scroll.set_propagate_natural_width(true);
97        self.scroll.set_propagate_natural_height(true);
98        self.update_tree(&ctx);
99        self.select(ctx.selected);
100    }
101
102    fn limit_column_widths(&self, ctx: &PopupMenuContext) {
103        const DEFAULT_PADDING: i32 = 5;
104
105        let layout = ctx.font_ctx.create_layout();
106        let kind_exists = ctx.menu_items.iter().any(|i| !i.kind.is_empty());
107        let max_width = self.scroll.get_max_content_width();
108        let (xpad, _) = self.renderer.get_padding();
109
110        let max_word_line = ctx.menu_items.iter().max_by_key(|m| m.word.len()).unwrap();
111        layout.set_text(max_word_line.word);
112        let (word_max_width, _) = layout.get_pixel_size();
113        let word_column_width = word_max_width + xpad * 2 + DEFAULT_PADDING;
114
115        if kind_exists {
116            layout.set_text("[v]");
117            let (kind_width, _) = layout.get_pixel_size();
118
119            self.kind_column
120                .set_fixed_width(kind_width + xpad * 2 + DEFAULT_PADDING);
121            self.kind_column.set_visible(true);
122
123            self.word_column
124                .set_fixed_width(min(max_width - kind_width, word_column_width));
125        } else {
126            self.kind_column.set_visible(false);
127            self.word_column
128                .set_fixed_width(min(max_width, word_column_width));
129        }
130
131        let max_menu_line = ctx.menu_items.iter().max_by_key(|m| m.menu.len()).unwrap();
132
133        if !max_menu_line.menu.is_empty() {
134            layout.set_text(max_menu_line.menu);
135            let (menu_max_width, _) = layout.get_pixel_size();
136            self.menu_column
137                .set_fixed_width(menu_max_width + xpad * 2 + DEFAULT_PADDING);
138            self.menu_column.set_visible(true);
139        } else {
140            self.menu_column.set_visible(false);
141        }
142    }
143
144    fn update_tree(&self, ctx: &PopupMenuContext) {
145        if ctx.menu_items.is_empty() {
146            return;
147        }
148
149        self.limit_column_widths(ctx);
150
151        self.renderer
152            .set_property_font(Some(ctx.font_ctx.font_description().to_string().as_str()));
153
154        let hl = &ctx.hl;
155        self.renderer
156            .set_property_foreground_rgba(Some(&hl.pmenu_fg().into()));
157
158        update_css(&self.css_provider, hl);
159
160        let list_store = gtk::ListStore::new(&[gtk::Type::String; 4]);
161        let all_column_ids: Vec<u32> = (0..4).map(|i| i as u32).collect();
162
163        for line in ctx.menu_items {
164            let line_array: [&dyn glib::ToValue; 4] = [&line.word, &line.kind, &line.menu, &line.info];
165            list_store.insert_with_values(None, &all_column_ids, &line_array[..]);
166        }
167
168        self.tree.set_model(Some(&list_store));
169    }
170
171    fn select(&self, selected: i64) {
172        if selected >= 0 {
173            let selected_path = gtk::TreePath::new_from_string(&format!("{}", selected));
174            self.tree.get_selection().select_path(&selected_path);
175            self.tree.scroll_to_cell(
176                Some(&selected_path),
177                Option::<&gtk::TreeViewColumn>::None,
178                false,
179                0.0,
180                0.0,
181            );
182
183            self.show_info_column(&selected_path);
184        } else {
185            self.tree.get_selection().unselect_all();
186            self.info_label.hide();
187        }
188    }
189
190    fn show_info_column(&self, selected_path: &gtk::TreePath) {
191        let model = self.tree.get_model().unwrap();
192        let iter = model.get_iter(selected_path);
193
194        if let Some(iter) = iter {
195            let info_value = model.get_value(&iter, 3);
196            let info: &str = info_value.get().unwrap();
197
198            if self.preview && !info.trim().is_empty() {
199                self.info_label.show();
200                self.info_label.set_text(&info);
201            } else {
202                self.info_label.hide();
203            }
204        } else {
205            self.info_label.hide();
206        }
207    }
208
209    fn set_preview(&mut self, preview: bool) {
210        self.preview = preview;
211    }
212}
213
214pub struct PopupMenu {
215    popover: gtk::Popover,
216    open: bool,
217
218    state: Rc<RefCell<State>>,
219}
220
221impl PopupMenu {
222    pub fn new(drawing: &gtk::DrawingArea) -> PopupMenu {
223        let state = State::new();
224        let popover = gtk::Popover::new(Some(drawing));
225        popover.set_modal(false);
226
227        let content = gtk::Box::new(gtk::Orientation::Vertical, 0);
228
229        state.tree.set_headers_visible(false);
230        state.tree.set_can_focus(false);
231
232        state
233            .scroll
234            .set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Automatic);
235
236        state.scroll.add(&state.tree);
237        state.scroll.show_all();
238
239        content.pack_start(&state.scroll, true, true, 0);
240        content.pack_start(&state.info_label, false, true, 0);
241        content.show();
242        popover.add(&content);
243
244        let state = Rc::new(RefCell::new(state));
245        let state_ref = state.clone();
246        state
247            .borrow()
248            .tree
249            .connect_button_press_event(move |tree, ev| {
250                let state = state_ref.borrow();
251                let nvim = state.nvim.as_ref().unwrap().nvim();
252                if let Some(mut nvim) = nvim {
253                    tree_button_press(tree, ev, &mut *nvim, "<C-y>");
254                }
255                Inhibit(false)
256            });
257
258        let state_ref = state.clone();
259        popover.connect_key_press_event(move |_, ev| {
260            let state = state_ref.borrow();
261            let nvim = state.nvim.as_ref().unwrap().nvim();
262            if let Some(mut nvim) = nvim {
263                input::gtk_key_press(&mut *nvim, ev)
264            } else {
265                Inhibit(false)
266            }
267        });
268
269        PopupMenu {
270            popover,
271            state,
272            open: false,
273        }
274    }
275
276    pub fn is_open(&self) -> bool {
277        self.open
278    }
279
280    pub fn show(&mut self, ctx: PopupMenuContext) {
281        self.open = true;
282
283        self.popover.set_pointing_to(&gtk::Rectangle {
284            x: ctx.x,
285            y: ctx.y,
286            width: ctx.width,
287            height: ctx.height,
288        });
289        self.state.borrow_mut().before_show(ctx);
290        self.popover.popup()
291    }
292
293    pub fn hide(&mut self) {
294        self.open = false;
295        // popdown() in case of fast hide/show
296        // situation does not work and just close popup window
297        // so hide() is important here
298        self.popover.hide();
299    }
300
301    pub fn select(&self, selected: i64) {
302        self.state.borrow().select(selected);
303    }
304
305    pub fn set_preview(&self, preview: bool) {
306        self.state.borrow_mut().set_preview(preview);
307    }
308}
309
310pub struct PopupMenuContext<'a> {
311    pub nvim: &'a Rc<NeovimClient>,
312    pub hl: &'a HighlightMap,
313    pub font_ctx: &'a render::Context,
314    pub menu_items: &'a [nvim::CompleteItem<'a>],
315    pub selected: i64,
316    pub x: i32,
317    pub y: i32,
318    pub width: i32,
319    pub height: i32,
320    pub max_width: i32,
321}
322
323pub fn tree_button_press(
324    tree: &gtk::TreeView,
325    ev: &EventButton,
326    nvim: &mut Neovim,
327    last_command: &str,
328) {
329    if ev.get_event_type() != EventType::ButtonPress {
330        return;
331    }
332
333    let (paths, ..) = tree.get_selection().get_selected_rows();
334    let selected_idx = if !paths.is_empty() {
335        let ids = paths[0].get_indices();
336        if !ids.is_empty() {
337            ids[0]
338        } else {
339            -1
340        }
341    } else {
342        -1
343    };
344
345    let (x, y) = ev.get_position();
346    if let Some((Some(tree_path), ..)) = tree.get_path_at_pos(x as i32, y as i32) {
347        let target_idx = tree_path.get_indices()[0];
348
349        let scroll_count = find_scroll_count(selected_idx, target_idx);
350
351        let apply_command: String = if target_idx > selected_idx {
352            (0..scroll_count)
353                .map(|_| "<C-n>")
354                .chain(iter::once(last_command))
355                .collect()
356        } else {
357            (0..scroll_count)
358                .map(|_| "<C-p>")
359                .chain(iter::once(last_command))
360                .collect()
361        };
362
363        nvim.input(&apply_command).report_err();
364    }
365}
366
367fn find_scroll_count(selected_idx: i32, target_idx: i32) -> i32 {
368    if selected_idx < 0 {
369        target_idx + 1
370    } else if target_idx > selected_idx {
371        target_idx - selected_idx
372    } else {
373        selected_idx - target_idx
374    }
375}
376
377fn on_treeview_allocate(
378    scroll: &gtk::ScrolledWindow,
379    tree: &gtk::TreeView,
380    renderer: &gtk::CellRendererText,
381) {
382    let treeview_height = calc_treeview_height(tree, renderer);
383
384    idle_add(clone!(scroll => move || {
385            scroll
386            .set_max_content_height(treeview_height);
387        Continue(false)
388    }));
389}
390
391pub fn update_css(css_provider: &gtk::CssProvider, hl: &HighlightMap) {
392    let bg = hl.pmenu_bg_sel();
393    let fg = hl.pmenu_fg_sel();
394
395    if let Err(e) = gtk::CssProviderExt::load_from_data(
396        css_provider,
397        &format!(
398            ".view :selected {{ color: {}; background-color: {};}}\n
399                .view {{ background-color: {}; }}",
400            fg.to_hex(),
401            bg.to_hex(),
402            hl.pmenu_bg().to_hex(),
403        )
404        .as_bytes(),
405    ) {
406        error!("Can't update css {}", e)
407    };
408}
409
410pub fn calc_treeview_height(tree: &gtk::TreeView, renderer: &gtk::CellRendererText) -> i32 {
411    let (_, natural_size) = renderer.get_preferred_height(tree);
412    let (_, ypad) = renderer.get_padding();
413
414    let row_height = natural_size + ypad;
415
416    let actual_count = tree.get_model().unwrap().iter_n_children(None);
417
418    row_height * min(actual_count, MAX_VISIBLE_ROWS) as i32
419}