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.rs192
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 { "▐" };