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 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 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 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::<>k::Adjustment>::None,
69 Option::<>k::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::<>k::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: >k::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: >k::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(>k::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 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: >k::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: >k::ScrolledWindow,
379 tree: >k::TreeView,
380 renderer: >k::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: >k::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: >k::TreeView, renderer: >k::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}