neophyte_ui/
lib.rs

1pub mod cmdline;
2pub mod grid;
3pub mod messages;
4pub mod options;
5pub mod window;
6
7use self::{
8    cmdline::Cmdline, grid::Grid, messages::Messages, options::GuiFont, window::WindowOffset,
9};
10use neophyte_linalg::{CellVec, PixelVec, Vec2};
11use neophyte_ui_event::{
12    hl_attr_define::Attributes, mode_info_set::ModeInfo, Chdir, CmdlineBlockAppend,
13    CmdlineBlockShow, CmdlinePos, DefaultColorsSet, Event, GridClear, GridCursorGoto, GridDestroy,
14    GridLine, GridResize, GridScroll, HlGroupSet, ModeChange, ModeInfoSet, MsgHistoryShow,
15    MsgRuler, MsgSetPos, MsgShowcmd, MsgShowmode, OptionSet, PopupmenuSelect, PopupmenuShow,
16    TablineUpdate, WinClose, WinExternalPos, WinFloatPos, WinHide, WinPos, WinViewport,
17};
18use std::{collections::HashMap, fmt::Debug};
19use window::{FloatingWindow, NormalWindow, Window};
20
21pub type HlId = u32;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct DrawItem {
25    pub grid: grid::Id,
26    pub z: Option<u32>,
27}
28
29impl DrawItem {
30    pub const fn new(grid: grid::Id, z: Option<u32>) -> Self {
31        Self { grid, z }
32    }
33}
34
35/// Manages updates to the UI state from UI events
36#[derive(Clone, Default)]
37pub struct Ui {
38    /// UI grids, ordered by ID
39    pub grids: Vec<Grid>,
40    /// The order in which grids should be drawn, ordered from bottom to top
41    pub draw_order: Vec<DrawItem>,
42    /// The index into self.draw_order at which floating windows begin
43    pub float_windows_start: usize,
44    /// Cursor information
45    pub cursor: CursorInfo,
46    /// Whether the mouse is enabled
47    pub mouse: bool,
48    /// UI highlights, indexed by their ID
49    // TODO: Only store the rgb_attr part
50    pub highlights: Vec<Option<Attributes>>,
51    /// A lookup from highlight names to highlight IDs
52    pub highlight_groups: HashMap<String, HlId>,
53    /// Whether the highlights changed since the last flush
54    pub did_highlights_change: bool,
55    /// The ID of the current mode
56    pub current_mode: u32,
57    /// Information about Vim modes, indexed by ID
58    pub modes: Vec<ModeInfo>,
59    /// UI options set by the option_set event
60    pub guifont_update: Option<GuiFont>,
61    /// Default highlight colors
62    pub default_colors: DefaultColorsSet,
63    /// Manages ext_hlstate events
64    pub messages: Messages,
65    /// Manages ext_cmdline events
66    pub cmdline: Cmdline,
67    /// Manages ext_popupmenu events
68    pub popupmenu: Option<PopupmenuShow>,
69    /// Manages ext_tabline events
70    pub tabline: Option<TablineUpdate>,
71    /// Did we receive a flush event?
72    pub did_flush: bool,
73    pub ignore_next_scroll: bool,
74}
75
76impl Ui {
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Index of the grid with the given id, or else the index where the
82    /// grid should be inserted
83    pub fn grid_index(&self, id: grid::Id) -> Result<usize, usize> {
84        self.grids.binary_search_by(|probe| probe.id.cmp(&id))
85    }
86
87    /// Grid with the given ID
88    pub fn grid_mut(&mut self, id: grid::Id) -> Option<&mut Grid> {
89        self.grid_index(id)
90            .map(|i| self.grids.get_mut(i))
91            .ok()
92            .flatten()
93    }
94
95    /// Grid with the given ID
96    pub fn grid(&self, id: grid::Id) -> Option<&Grid> {
97        self.grid_index(id)
98            .map(|i| self.grids.get(i))
99            .ok()
100            .flatten()
101    }
102
103    /// Get the grid with the given ID or create it if it does not exist
104    fn get_or_create_grid(&mut self, id: grid::Id) -> &mut Grid {
105        match self.grid_index(id) {
106            Ok(i) => &mut self.grids[i],
107            Err(i) => {
108                self.grids.insert(i, Grid::new(id));
109                self.show_normal(id);
110                &mut self.grids[i]
111            }
112        }
113    }
114
115    /// Reset dirty flags
116    pub fn clear_dirty(&mut self) {
117        self.did_highlights_change = false;
118        self.did_flush = false;
119        self.guifont_update = None;
120        self.ignore_next_scroll = false;
121        self.messages.dirty = false;
122        for grid in self.grids.iter_mut() {
123            grid.clear_dirty();
124        }
125    }
126
127    /// Update the UI with the given event
128    pub fn process(&mut self, event: Event) {
129        match event {
130            Event::OptionSet(event) => match event {
131                OptionSet::Guifont(s) if !s.is_empty() => self.guifont_update = Some(s.into()),
132                _ => {}
133            },
134            Event::DefaultColorsSet(event) => {
135                self.did_highlights_change = true;
136                self.default_colors = event;
137            }
138            Event::HlAttrDefine(event) => {
139                self.did_highlights_change = true;
140                let i = event.id as usize;
141                if i > self.highlights.len() {
142                    self.highlights.resize(i * 2, None);
143                }
144                self.highlights.insert(i, Some(event.rgb_attr));
145            }
146            Event::HlGroupSet(HlGroupSet { name, hl_id }) => {
147                self.did_highlights_change = true;
148                self.highlight_groups.insert(name, hl_id);
149            }
150            Event::ModeChange(ModeChange { mode_idx, mode: _ }) => self.current_mode = mode_idx,
151            Event::ModeInfoSet(ModeInfoSet {
152                cursor_style_enabled,
153                mode_info,
154            }) => {
155                self.cursor.style_enabled = cursor_style_enabled;
156                self.modes = mode_info;
157            }
158
159            Event::GridResize(GridResize {
160                grid,
161                width,
162                height,
163            }) => {
164                self.get_or_create_grid(grid)
165                    .contents_mut()
166                    .resize(CellVec(Vec2::new(width, height)));
167            }
168            Event::GridClear(GridClear { grid }) => self
169                .grid_mut(grid)
170                .expect("Tried to clear nonexistent grid")
171                .contents_mut()
172                .clear(),
173            Event::GridDestroy(GridDestroy { grid }) => self.delete_grid(grid),
174            Event::GridCursorGoto(GridCursorGoto { grid, row, column }) => {
175                self.cursor.pos = CellVec::new(column, row);
176                self.cursor.grid = grid;
177            }
178            Event::GridScroll(GridScroll {
179                grid,
180                top,
181                bot,
182                left,
183                right,
184                rows,
185                cols: _,
186            }) => {
187                self.grid_mut(grid)
188                    .expect("Tried to scroll nonexistent grid")
189                    .contents_mut()
190                    .scroll(top, bot, left, right, rows);
191            }
192            Event::GridLine(GridLine {
193                grid,
194                row,
195                col_start,
196                cells,
197            }) => {
198                self.grid_mut(grid)
199                    .expect("Tried to update a line of a nonexistent grid")
200                    .contents_mut()
201                    .grid_line(row, col_start, cells);
202            }
203
204            Event::WinPos(WinPos {
205                grid,
206                win: _,
207                start_row,
208                start_col,
209                width,
210                height,
211            }) => {
212                self.show_normal(grid);
213                *self
214                    .grid_mut(grid)
215                    .expect("Tried to update the position of a nonexistent grid")
216                    .window_mut() = Window::Normal(NormalWindow {
217                    start: CellVec(Vec2::new(start_col, start_row)),
218                    size: CellVec(Vec2::new(width, height)),
219                });
220            }
221            Event::WinFloatPos(WinFloatPos {
222                grid,
223                win: _,
224                anchor,
225                anchor_grid,
226                anchor_row,
227                anchor_col,
228                focusable,
229                zindex,
230            }) => {
231                self.show_float(DrawItem::new(grid, zindex));
232                *self
233                    .grid_mut(grid)
234                    .expect("Tried to update the position of a nonexistent grid")
235                    .window_mut() = Window::Floating(FloatingWindow {
236                    anchor,
237                    focusable,
238                    anchor_grid,
239                    anchor_pos: CellVec(Vec2::new(anchor_col, anchor_row)),
240                });
241            }
242            Event::WinExternalPos(WinExternalPos { grid, win: _ }) => {
243                *self
244                    .grid_mut(grid)
245                    .expect("Tried to update the position of a nonexistent grid")
246                    .window_mut() = Window::External;
247            }
248            Event::WinHide(WinHide { grid }) => {
249                self.hide(grid);
250            }
251            Event::WinClose(WinClose { grid }) => {
252                self.hide(grid);
253                // It seems like we shouldn't be able to receive this event
254                // when a grid doesn't exist, but I have had this happen when
255                // opening DAP UI.
256                if let Some(grid) = self.grid_mut(grid) {
257                    *grid.window_mut() = Window::None;
258                }
259            }
260            Event::WinViewport(WinViewport {
261                grid,
262                scroll_delta,
263                win: _,
264                topline: _,
265                botline: _,
266                curline: _,
267                curcol: _,
268                line_count: _,
269            }) => {
270                if !self.ignore_next_scroll {
271                    self.grid_mut(grid)
272                        .expect("Tried to update the viewport of a nonexistent grid")
273                        .scroll_delta = scroll_delta;
274                }
275            }
276            Event::WinViewportMargins(_) | Event::WinExtmark(_) => {}
277
278            Event::PopupmenuShow(event) => self.popupmenu = Some(event),
279            Event::PopupmenuSelect(PopupmenuSelect { selected }) => {
280                if let Some(menu) = &mut self.popupmenu {
281                    menu.selected = selected
282                }
283            }
284            Event::PopupmenuHide => self.popupmenu = None,
285
286            Event::CmdlineShow(event) => self.cmdline.show(event),
287            Event::CmdlinePos(CmdlinePos { pos, level: _ }) => self.cmdline.set_cursor_pos(pos),
288            Event::CmdlineBlockShow(CmdlineBlockShow { lines }) => self.cmdline.show_block(lines),
289            Event::CmdlineBlockAppend(CmdlineBlockAppend { line }) => {
290                self.cmdline.append_block(line)
291            }
292            Event::CmdlineSpecialChar(event) => self.cmdline.special(event),
293            Event::CmdlineHide => self.cmdline.hide(),
294            Event::CmdlineBlockHide => self.cmdline.hide_block(),
295
296            Event::MsgHistoryShow(MsgHistoryShow { entries }) => {
297                self.messages.history = entries;
298                self.messages.dirty = true;
299            }
300            Event::MsgRuler(MsgRuler { content }) => self.messages.ruler = content,
301            Event::MsgSetPos(MsgSetPos {
302                grid,
303                row,
304                scrolled: _,
305                sep_char: _,
306            }) => {
307                // Message scrollback is a hard-coded z-index
308                // https://neovim.io/doc/user/api.html#nvim_open_win()
309                self.show_float(DrawItem::new(grid, Some(200)));
310                *self.get_or_create_grid(grid).window_mut() = Window::Messages { row };
311            }
312            Event::MsgShow(event) => {
313                self.messages.show(event);
314                self.messages.dirty = true;
315            }
316            Event::MsgShowmode(MsgShowmode { content }) => self.messages.showmode = content,
317            Event::MsgShowcmd(MsgShowcmd { content }) => self.messages.showcmd = content,
318            Event::MsgClear => {
319                self.messages.show.clear();
320                self.messages.dirty = true;
321            }
322            Event::MsgHistoryClear => {
323                self.messages.history.clear();
324            }
325
326            Event::TablineUpdate(event) => self.tabline = Some(event),
327            Event::Chdir(Chdir { path }) => match std::env::set_current_dir(path) {
328                Ok(_) => {}
329                Err(e) => log::error!("Failed to change directory: {e:?}"),
330            },
331
332            Event::MouseOn => self.mouse = true,
333            Event::MouseOff => self.mouse = false,
334            Event::BusyStart => self.cursor.enabled = false,
335            Event::BusyStop => self.cursor.enabled = true,
336            Event::Flush => self.did_flush = true,
337
338            Event::Suspend
339            | Event::SetTitle(_)
340            | Event::SetIcon(_)
341            | Event::UpdateMenu
342            | Event::Bell
343            | Event::VisualBell => {}
344        }
345    }
346
347    /// Move the given grid to the top of the draw order
348    fn show_float(&mut self, draw_item: DrawItem) {
349        self.hide(draw_item.grid);
350        // Default float value is 50
351        // https://neovim.io/doc/user/api.html#nvim_open_win()
352        let z_of = |item: DrawItem| item.z.unwrap_or(50);
353        let z = z_of(draw_item);
354        let insert_position = self
355            .draw_order
356            .iter()
357            .enumerate()
358            .skip(self.float_windows_start)
359            .rev()
360            .find_map(|(i, item)| (z >= z_of(*item)).then_some(i + 1))
361            .unwrap_or(self.float_windows_start);
362        self.draw_order.insert(insert_position, draw_item);
363    }
364
365    /// Move the given grid to the top of the draw order
366    fn show_normal(&mut self, grid: grid::Id) {
367        self.hide(grid);
368        self.draw_order
369            .insert(self.float_windows_start, DrawItem::new(grid, None));
370        self.float_windows_start += 1;
371    }
372
373    /// Remove the given grid from the draw order
374    fn hide(&mut self, grid: grid::Id) {
375        if let Some(i) = self.draw_order.iter().position(|&r| r.grid == grid) {
376            self.draw_order.remove(i);
377            if i < self.float_windows_start {
378                self.float_windows_start -= 1;
379            }
380        }
381    }
382
383    /// Delete the given grid
384    fn delete_grid(&mut self, grid: grid::Id) {
385        if let Ok(i) = self.grids.binary_search_by(|probe| probe.id.cmp(&grid)) {
386            self.grids.remove(i);
387        }
388        self.hide(grid);
389    }
390
391    /// Get the position of the grid, accounting for anchor grids and other
392    /// windowing details
393    pub fn position(&self, grid: grid::Id) -> Option<CellVec<f32>> {
394        if grid == 1 {
395            return Some(CellVec::new(0., 0.));
396        }
397        if let Ok(index) = self.grid_index(grid) {
398            let grid = &self.grids[index];
399            if grid.window() == &Window::None {
400                return None;
401            }
402
403            let WindowOffset {
404                offset,
405                anchor_grid,
406            } = grid.window().offset(grid.contents().size);
407
408            let position = if let Some(anchor_grid) = anchor_grid {
409                self.position(anchor_grid)? + offset
410            } else {
411                offset
412            };
413
414            match grid.window() {
415                Window::Floating(_) => {
416                    let base_grid_size = self.grids[0].contents().size;
417                    let grid_max = position + grid.contents().size.cast_as();
418                    let overflow = (grid_max - base_grid_size.cast_as()).map(|x| x.max(0.));
419                    Some((position - overflow).map(|x| x.max(0.)))
420                }
421                _ => Some(position),
422            }
423        } else {
424            None
425        }
426    }
427
428    /// The grid under the cursor, accounting for anchor grids and other
429    /// windowing details
430    pub fn grid_under_cursor(
431        &self,
432        cursor: PixelVec<u32>,
433        cell_size: Vec2<u32>,
434    ) -> Option<GridUnderCursor> {
435        // TODO: Can this be derived from CursorInfo instead?
436        let cursor = cursor.cast_as::<f32>();
437        let cell_size = cell_size.cast_as::<f32>();
438        for &draw_item in self.draw_order.iter().rev() {
439            let grid = self.grid(draw_item.grid).unwrap();
440            let size: CellVec<f32> = grid.contents().size.cast_as();
441            let start = self.position(draw_item.grid)?.into_pixels(cell_size);
442            let end = start + size.into_pixels(cell_size);
443            if cursor.0.x > start.0.x
444                && cursor.0.y > start.0.y
445                && cursor.0.x < end.0.x
446                && cursor.0.y < end.0.y
447            {
448                let position = (cursor - start).into_cells(cell_size);
449                let position = position.cast_as::<i64>();
450                let Ok(position) = position.try_cast() else {
451                    continue;
452                };
453                return Some(GridUnderCursor {
454                    grid: draw_item.grid,
455                    position,
456                });
457            }
458        }
459        None
460    }
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464pub struct GridUnderCursor {
465    /// The ID of the grid
466    pub grid: grid::Id,
467    /// The position of the cursor in grid cells relative to the grid
468    pub position: CellVec<u32>,
469}
470
471#[derive(Debug, Copy, Clone)]
472pub struct CursorInfo {
473    /// The position of the cursor in grid cells
474    pub pos: CellVec<u16>,
475    /// The grid the cursor is on
476    pub grid: grid::Id,
477    /// Whether the cursor should be rendered
478    pub enabled: bool,
479    /// Whether the UI should set the cursor style
480    pub style_enabled: bool,
481}
482
483impl Default for CursorInfo {
484    fn default() -> Self {
485        Self {
486            pos: Default::default(),
487            grid: 1,
488            enabled: true,
489            style_enabled: false,
490        }
491    }
492}