diff options
author | Ludwig Stecher | 2022-02-15 01:24:03 +0000 |
---|---|---|
committer | GitHub | 2022-02-15 01:24:03 +0000 |
commit | 442999384256f89eddfa6625a0ffb0257df65ef7 (patch) | |
tree | 508ed85a7e294b665f543da7fd23d93dc56e1d30 /helix-term/src/ui | |
parent | 23907a063c43f06f120d80a6ec0b6748881236a1 (diff) |
Add `PageUp`, `PageDown`, `Ctrl-u`, `Ctrl-d`, `Home`, `End` keyboard shortcuts to file picker (#1612)
* Add `PageUp`, `PageDown`, `Ctrl-u`, `Ctrl-d`, `Home`, `End` keyboard shortcuts to file picker
* Refactor file picker paging logic
* change key mapping
* Add overlay component
* Use closure instead of margin to calculate size
* Don't wrap file picker in `Overlay` automatically
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r-- | helix-term/src/ui/mod.rs | 1 | ||||
-rw-r--r-- | helix-term/src/ui/overlay.rs | 73 | ||||
-rw-r--r-- | helix-term/src/ui/picker.rs | 110 |
3 files changed, 137 insertions, 47 deletions
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index edff0583..7f6d9f7c 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod editor; mod info; mod markdown; pub mod menu; +pub mod overlay; mod picker; mod popup; mod prompt; diff --git a/helix-term/src/ui/overlay.rs b/helix-term/src/ui/overlay.rs new file mode 100644 index 00000000..9f522e35 --- /dev/null +++ b/helix-term/src/ui/overlay.rs @@ -0,0 +1,73 @@ +use crossterm::event::Event; +use helix_core::Position; +use helix_view::{ + graphics::{CursorKind, Rect}, + Editor, +}; +use tui::buffer::Buffer; + +use crate::compositor::{Component, Context, EventResult}; + +/// Contains a component placed in the center of the parent component +pub struct Overlay<T> { + /// Child component + pub content: T, + /// Function to compute the size and position of the child component + pub calc_child_size: Box<dyn Fn(Rect) -> Rect>, +} + +/// Surrounds the component with a margin of 5% on each side, and an additional 2 rows at the bottom +pub fn overlayed<T>(content: T) -> Overlay<T> { + Overlay { + content, + calc_child_size: Box::new(|rect: Rect| clip_rect_relative(rect.clip_bottom(2), 90, 90)), + } +} + +fn clip_rect_relative(rect: Rect, percent_horizontal: u8, percent_vertical: u8) -> Rect { + fn mul_and_cast(size: u16, factor: u8) -> u16 { + ((size as u32) * (factor as u32) / 100).try_into().unwrap() + } + + let inner_w = mul_and_cast(rect.width, percent_horizontal); + let inner_h = mul_and_cast(rect.height, percent_vertical); + + let offset_x = rect.width.saturating_sub(inner_w) / 2; + let offset_y = rect.height.saturating_sub(inner_h) / 2; + + Rect { + x: rect.x + offset_x, + y: rect.y + offset_y, + width: inner_w, + height: inner_h, + } +} + +impl<T: Component + 'static> Component for Overlay<T> { + fn render(&mut self, area: Rect, frame: &mut Buffer, ctx: &mut Context) { + let dimensions = (self.calc_child_size)(area); + self.content.render(dimensions, frame, ctx) + } + + fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> { + let area = Rect { + x: 0, + y: 0, + width, + height, + }; + let dimensions = (self.calc_child_size)(area); + let viewport = (dimensions.width, dimensions.height); + let _ = self.content.required_size(viewport)?; + Some((width, height)) + } + + fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult { + self.content.handle_event(event, ctx) + } + + fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) { + let dimensions = (self.calc_child_size)(area); + self.content.cursor(dimensions, ctx) + } +} diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 2c7db7f2..9cddbc60 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -21,14 +21,14 @@ use std::{ }; use crate::ui::{Prompt, PromptEvent}; -use helix_core::Position; +use helix_core::{movement::Direction, Position}; use helix_view::{ editor::Action, graphics::{Color, CursorKind, Margin, Rect, Style}, Document, Editor, }; -pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80; +pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; /// Biggest file size to preview in bytes pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; @@ -90,7 +90,7 @@ impl<T> FilePicker<T> { preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static, ) -> Self { Self { - picker: Picker::new(false, options, format_fn, callback_fn), + picker: Picker::new(options, format_fn, callback_fn), truncate_start: true, preview_cache: HashMap::new(), read_buffer: Vec::with_capacity(1024), @@ -160,8 +160,7 @@ impl<T: 'static> Component for FilePicker<T> { // | | | | // +---------+ +---------+ - let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW; - let area = inner_rect(area); + let render_preview = area.width > MIN_AREA_WIDTH_FOR_PREVIEW; // -- Render the frame: // clear area let background = cx.editor.theme.get("ui.background"); @@ -260,6 +259,16 @@ impl<T: 'static> Component for FilePicker<T> { fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) { self.picker.cursor(area, ctx) } + + fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> { + let picker_width = if width > MIN_AREA_WIDTH_FOR_PREVIEW { + width / 2 + } else { + width + }; + self.picker.required_size((picker_width, height))?; + Some((width, height)) + } } pub struct Picker<T> { @@ -271,11 +280,12 @@ pub struct Picker<T> { /// Filter over original options. filters: Vec<usize>, // could be optimized into bit but not worth it now + /// Current height of the completions box + completion_height: u16, + cursor: usize, // pattern: String, prompt: Prompt, - /// Whether to render in the middle of the area - render_centered: bool, /// Wheather to truncate the start (default true) pub truncate_start: bool, @@ -285,7 +295,6 @@ pub struct Picker<T> { impl<T> Picker<T> { pub fn new( - render_centered: bool, options: Vec<T>, format_fn: impl Fn(&T) -> Cow<str> + 'static, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, @@ -306,10 +315,10 @@ impl<T> Picker<T> { filters: Vec::new(), cursor: 0, prompt, - render_centered, truncate_start: true, format_fn: Box::new(format_fn), callback_fn: Box::new(callback_fn), + completion_height: 0, }; // TODO: scoring on empty input should just use a fastpath @@ -346,22 +355,38 @@ impl<T> Picker<T> { self.cursor = 0; } - pub fn move_up(&mut self) { - if self.matches.is_empty() { - return; - } + /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`) + pub fn move_by(&mut self, amount: usize, direction: Direction) { let len = self.matches.len(); - let pos = ((self.cursor + len.saturating_sub(1)) % len) % len; - self.cursor = pos; - } - pub fn move_down(&mut self) { - if self.matches.is_empty() { - return; + match direction { + Direction::Forward => { + self.cursor = self.cursor.saturating_add(amount) % len; + } + Direction::Backward => { + self.cursor = self.cursor.saturating_add(len).saturating_sub(amount) % len; + } } - let len = self.matches.len(); - let pos = (self.cursor + 1) % len; - self.cursor = pos; + } + + /// Move the cursor down by exactly one page. After the last page comes the first page. + pub fn page_up(&mut self) { + self.move_by(self.completion_height as usize, Direction::Backward); + } + + /// Move the cursor up by exactly one page. After the first page comes the last page. + pub fn page_down(&mut self) { + self.move_by(self.completion_height as usize, Direction::Forward); + } + + /// Move the cursor to the first entry + pub fn to_start(&mut self) { + self.cursor = 0; + } + + /// Move the cursor to the last entry + pub fn to_end(&mut self) { + self.cursor = self.matches.len().saturating_sub(1); } pub fn selection(&self) -> Option<&T> { @@ -384,23 +409,10 @@ impl<T> Picker<T> { // - on input change: // - score all the names in relation to input -fn inner_rect(area: Rect) -> Rect { - let margin = Margin { - vertical: area.height * 10 / 100, - horizontal: area.width * 10 / 100, - }; - area.inner(&margin) -} - impl<T: 'static> Component for Picker<T> { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - let max_width = 50.min(viewport.0); - let max_height = 10.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport - - let height = (self.options.len() as u16 + 4) // add some spacing for input + padding - .min(max_height); - let width = max_width; - Some((width, height)) + self.completion_height = viewport.1.saturating_sub(4); + Some(viewport) } fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { @@ -417,10 +429,22 @@ impl<T: 'static> Component for Picker<T> { match key_event.into() { shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => { - self.move_up(); + self.move_by(1, Direction::Backward); } key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => { - self.move_down(); + self.move_by(1, Direction::Forward); + } + key!(PageDown) | ctrl!('f') => { + self.page_down(); + } + key!(PageUp) | ctrl!('b') => { + self.page_up(); + } + key!(Home) => { + self.to_start(); + } + key!(End) => { + self.to_end(); } key!(Esc) | ctrl!('c') => { return close_fn; @@ -458,12 +482,6 @@ impl<T: 'static> Component for Picker<T> { } fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { - let area = if self.render_centered { - inner_rect(area) - } else { - area - }; - let text_style = cx.editor.theme.get("ui.text"); // -- Render the frame: @@ -538,8 +556,6 @@ impl<T: 'static> Component for Picker<T> { } fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) { - // TODO: this is mostly duplicate code - let area = inner_rect(area); let block = Block::default().borders(Borders::ALL); // calculate the inner area inside the box let inner = block.inner(area); |