Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/popup.rs')
| -rw-r--r-- | helix-term/src/ui/popup.rs | 192 |
1 files changed, 88 insertions, 104 deletions
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index db77492d..8d436fda 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -5,7 +5,7 @@ use crate::{ }; use tui::{ buffer::Buffer as Surface, - widgets::{Block, Widget}, + widgets::{Block, Borders, Widget}, }; use helix_core::Position; @@ -15,16 +15,7 @@ use helix_view::{ Editor, }; -const MIN_HEIGHT: u16 = 6; -const MAX_HEIGHT: u16 = 26; -const MAX_WIDTH: u16 = 120; - -struct RenderInfo { - area: Rect, - child_height: u16, - render_borders: bool, - is_menu: bool, -} +const MIN_HEIGHT: u16 = 4; // TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return // a width/height hint. maybe Popup(Box<Component>) @@ -32,6 +23,7 @@ struct RenderInfo { pub struct Popup<T: Component> { contents: T, position: Option<Position>, + margin: Margin, area: Rect, position_bias: Open, scroll_half_pages: usize, @@ -46,6 +38,7 @@ impl<T: Component> Popup<T> { Self { contents, position: None, + margin: Margin::none(), position_bias: Open::Below, area: Rect::new(0, 0, 0, 0), scroll_half_pages: 0, @@ -78,6 +71,11 @@ impl<T: Component> Popup<T> { self } + pub fn margin(mut self, margin: Margin) -> Self { + self.margin = margin; + self + } + pub fn auto_close(mut self, auto_close: bool) -> Self { self.auto_close = auto_close; self @@ -120,38 +118,38 @@ impl<T: Component> Popup<T> { } pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect { - self.render_info(viewport, editor).area + let child_size = self + .contents + .required_size((viewport.width, viewport.height)) + .expect("Component needs required_size implemented in order to be embedded in a popup"); + + self.area_internal(viewport, editor, child_size) } - fn render_info(&mut self, viewport: Rect, editor: &Editor) -> RenderInfo { - let mut position = editor.cursor().0.unwrap_or_default(); - if let Some(old_position) = self + pub fn area_internal( + &mut self, + viewport: Rect, + editor: &Editor, + child_size: (u16, u16), + ) -> Rect { + let width = child_size.0.min(viewport.width); + let height = child_size.1.min(viewport.height.saturating_sub(2)); // add some spacing in the viewport + + let position = self .position - .filter(|old_position| old_position.row == position.row) - { - position = old_position; - } else { - self.position = Some(position); - } + .get_or_insert_with(|| editor.cursor().0.unwrap_or_default()); - let is_menu = self - .contents - .type_name() - .starts_with("helix_term::ui::menu::Menu"); - - let mut render_borders = if is_menu { - editor.menu_border() - } else { - editor.popup_border() - }; + // if there's a orientation preference, use that + // if we're on the top part of the screen, do below + // if we're on the bottom part, do above // -- make sure frame doesn't stick out of bounds let mut rel_x = position.col as u16; let mut rel_y = position.row as u16; + if viewport.width <= rel_x + width { + rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width)); + } - // if there's a orientation preference, use that - // if we're on the top part of the screen, do below - // if we're on the bottom part, do above let can_put_below = viewport.height > rel_y + MIN_HEIGHT; let can_put_above = rel_y.checked_sub(MIN_HEIGHT).is_some(); let final_pos = match self.position_bias { @@ -165,40 +163,7 @@ impl<T: Component> Popup<T> { }, }; - // compute maximum space available for child - let mut max_height = match final_pos { - Open::Above => rel_y, - Open::Below => viewport.height.saturating_sub(1 + rel_y), - }; - max_height = max_height.min(MAX_HEIGHT); - let mut max_width = viewport.width.saturating_sub(2).min(MAX_WIDTH); - render_borders = render_borders && max_height > 3 && max_width > 3; - if render_borders { - max_width -= 2; - max_height -= 2; - } - - // compute required child size and reclamp - let (mut width, child_height) = self - .contents - .required_size((max_width, max_height)) - .expect("Component needs required_size implemented in order to be embedded in a popup"); - - width = width.min(MAX_WIDTH); - let height = if render_borders { - (child_height + 2).min(MAX_HEIGHT) - } else { - child_height.min(MAX_HEIGHT) - }; - if render_borders { - width += 2; - } - if viewport.width <= rel_x + width + 2 { - rel_x = viewport.width.saturating_sub(width + 2); - width = viewport.width.saturating_sub(rel_x + 2) - } - - let area = match final_pos { + match final_pos { Open::Above => { rel_y = rel_y.saturating_sub(height); Rect::new(rel_x, rel_y, width, position.row as u16 - rel_y) @@ -208,12 +173,6 @@ impl<T: Component> Popup<T> { let y_max = viewport.bottom().min(height + rel_y); Rect::new(rel_x, rel_y, width, y_max - rel_y) } - }; - RenderInfo { - area, - child_height, - render_borders, - is_menu, } } @@ -300,59 +259,84 @@ impl<T: Component> Component for Popup<T> { // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll. } + fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { + const MAX_WIDTH: u16 = 120; + const MAX_HEIGHT: u16 = 26; + + let inner = Rect::new(0, 0, MAX_WIDTH, MAX_HEIGHT).inner(&self.margin); + + let (width, height) = self + .contents + .required_size((inner.width, inner.height)) + .expect("Component needs required_size implemented in order to be embedded in a popup"); + + let size = ( + (width + self.margin.width()).min(MAX_WIDTH), + (height + self.margin.height()).min(MAX_HEIGHT), + ); + + Some(size) + } + fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { - let RenderInfo { - area, - child_height, - render_borders, - is_menu, - } = self.render_info(viewport, cx.editor); + let child_size = self + .contents + .required_size((viewport.width, viewport.height)) + .expect("Component needs required_size implemented in order to be embedded in a popup"); + + let area = self.area_internal(viewport, cx.editor, child_size); self.area = area; + let max_offset = child_size.1.saturating_sub(area.height) as usize; + let half_page_size = (area.height / 2) as usize; + let scroll = max_offset.min(self.scroll_half_pages * half_page_size); + if half_page_size > 0 { + self.scroll_half_pages = scroll / half_page_size; + } + cx.scroll = Some(scroll); + // clear area - let background = if is_menu { - // TODO: consistently style menu - cx.editor - .theme - .try_get("ui.menu") - .unwrap_or_else(|| cx.editor.theme.get("ui.text")) + let background = cx.editor.theme.get("ui.popup"); + surface.clear_with(area, background); + + let render_borders = cx.editor.popup_border(); + + let inner = if self + .contents + .type_name() + .starts_with("helix_term::ui::menu::Menu") + { + area } else { - cx.editor.theme.get("ui.popup") + area.inner(&self.margin) }; - surface.clear_with(area, background); - let mut inner = area; + let border = usize::from(render_borders); if render_borders { - inner = area.inner(Margin::all(1)); - Widget::render(Block::bordered(), area, surface); + Widget::render(Block::default().borders(Borders::ALL), area, surface); } - let border = usize::from(render_borders); - let max_offset = child_height.saturating_sub(inner.height) as usize; - let half_page_size = (inner.height / 2) as usize; - let scroll = max_offset.min(self.scroll_half_pages * half_page_size); - if half_page_size > 0 { - self.scroll_half_pages = scroll / half_page_size; - } - cx.scroll = Some(scroll); self.contents.render(inner, surface, cx); // render scrollbar if contents do not fit if self.has_scrollbar { - let win_height = inner.height as usize; - let len = child_height as usize; + let win_height = (inner.height as usize).saturating_sub(2 * border); + let len = (child_size.1 as usize).saturating_sub(2 * border); let fits = len <= win_height; let scroll_style = cx.editor.theme.get("ui.menu.scroll"); + const fn div_ceil(a: usize, b: usize) -> usize { + (a + b - 1) / b + } + if !fits { - let scroll_height = win_height.pow(2).div_ceil(len).min(win_height); + let scroll_height = div_ceil(win_height.pow(2), len).min(win_height); let scroll_line = (win_height - scroll_height) * scroll / std::cmp::max(1, len.saturating_sub(win_height)); let mut cell; for i in 0..win_height { - cell = - &mut surface[(inner.right() - 1 + border as u16, inner.top() + i as u16)]; + cell = &mut surface[(inner.right() - 1, inner.top() + (border + i) as u16)]; let half_block = if render_borders { "▌" } else { "▐" }; |