diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6bc2b9b6c29b0..3756535b89666 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3396,7 +3396,7 @@ fn symbol_picker(cx: &mut Context) { Some((path, line)) }, ); - picker.truncate_start = false; + picker.content.truncate_start = false; compositor.push(Box::new(picker)) } }, @@ -3456,7 +3456,7 @@ fn workspace_symbol_picker(cx: &mut Context) { Some((path, line)) }, ); - picker.truncate_start = false; + picker.content.truncate_start = false; compositor.push(Box::new(picker)) } }, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 5d650c6592eb4..12dde8e09b65c 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; +mod overlay; mod picker; mod popup; mod prompt; @@ -25,6 +26,8 @@ use helix_view::{Document, Editor, View}; use std::path::PathBuf; +use self::overlay::Overlay; + pub fn regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, @@ -93,7 +96,10 @@ pub fn regex_prompt( ) } -pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker { +pub fn file_picker( + root: PathBuf, + config: &helix_view::editor::Config, +) -> Overlay> { use ignore::{types::TypesBuilder, WalkBuilder}; use std::time; diff --git a/helix-term/src/ui/overlay.rs b/helix-term/src/ui/overlay.rs new file mode 100644 index 0000000000000..a2a69c12a0e83 --- /dev/null +++ b/helix-term/src/ui/overlay.rs @@ -0,0 +1,101 @@ +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 +#[derive(Debug)] +pub struct Overlay { + /// Child component + pub content: T, + /// Fixed margin around the component + pub margin: Margin4, + /// Value between 0 and 1 that indicates how much of the vertical space is used + pub vertical_size: f32, + /// Value between 0 and 1 that indicates how much of the horizontal space is used + pub horizontal_size: f32, +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct Margin4 { + pub top: u16, + pub right: u16, + pub bottom: u16, + pub left: u16, +} + +impl Overlay { + fn get_dimensions(&self, viewport: (u16, u16)) -> Rect { + fn mul_and_cast(size: u16, factor: f32) -> u16 { + let absolute = (size as f32) * factor; + (absolute as i32).try_into().unwrap() + } + + let (outer_w, outer_h) = viewport; + let (margin_w, margin_h) = self.margin.dimensions(); + + let outer_without_margin_w = outer_w.saturating_sub(margin_w); + let outer_without_margin_h = outer_h.saturating_sub(margin_h); + + let inner_w = mul_and_cast(outer_without_margin_w, self.horizontal_size); + let inner_h = mul_and_cast(outer_without_margin_h, self.vertical_size); + + let pos_x = self.margin.left + outer_without_margin_w.saturating_sub(inner_w) / 2; + let pos_y = self.margin.top + outer_without_margin_h.saturating_sub(inner_h) / 2; + + Rect { + x: pos_x, + y: pos_y, + width: inner_w, + height: inner_h, + } + } + + fn get_outer_size_from_inner_size(&self, inner: (u16, u16)) -> (u16, u16) { + fn div_and_cast(size: u16, divisor: f32) -> u16 { + let absolute = (size as f32) / divisor; + (absolute as i32).try_into().unwrap() + } + + let (inner_w, inner_h) = inner; + let (margin_w, margin_h) = self.margin.dimensions(); + ( + margin_w + div_and_cast(inner_w, self.horizontal_size), + margin_h + div_and_cast(inner_h, self.vertical_size), + ) + } +} + +impl Margin4 { + fn dimensions(&self) -> (u16, u16) { + (self.left + self.right, self.top + self.bottom) + } +} + +impl Component for Overlay { + fn render(&mut self, area: Rect, frame: &mut Buffer, ctx: &mut Context) { + let dimensions = self.get_dimensions((area.width, area.height)); + self.content.render(dimensions, frame, ctx) + } + + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + let dimensions = self.get_dimensions(viewport); + let viewport = (dimensions.width, dimensions.height); + let required = self.content.required_size(viewport)?; + Some(self.get_outer_size_from_inner_size(required)) + } + + 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, CursorKind) { + let dimensions = self.get_dimensions((area.width, area.height)); + self.content.cursor(dimensions, ctx) + } +} diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index e4ef1dc7f3b5a..28af1d5e4166f 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -28,7 +28,9 @@ use helix_view::{ Document, Editor, }; -pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80; +use super::overlay::{Margin4, Overlay}; + +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; @@ -88,13 +90,21 @@ impl FilePicker { format_fn: impl Fn(&T) -> Cow + 'static, callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, preview_fn: impl Fn(&Editor, &T) -> Option + 'static, - ) -> Self { - Self { - picker: Picker::new(false, options, format_fn, callback_fn), - truncate_start: true, - preview_cache: HashMap::new(), - read_buffer: Vec::with_capacity(1024), - file_fn: Box::new(preview_fn), + ) -> Overlay { + Overlay { + content: Self { + picker: Picker::new(options, format_fn, callback_fn), + truncate_start: true, + preview_cache: HashMap::new(), + read_buffer: Vec::with_capacity(1024), + file_fn: Box::new(preview_fn), + }, + margin: Margin4 { + bottom: 2, + ..Default::default() + }, + vertical_size: 0.9, + horizontal_size: 0.9, } } @@ -160,8 +170,7 @@ impl Component for FilePicker { // | | | | // +---------+ +---------+ - 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 +269,16 @@ impl Component for FilePicker { fn cursor(&self, area: Rect, ctx: &Editor) -> (Option, 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 { @@ -277,8 +296,6 @@ pub struct Picker { 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, @@ -288,7 +305,6 @@ pub struct Picker { impl Picker { pub fn new( - render_centered: bool, options: Vec, format_fn: impl Fn(&T) -> Cow + 'static, callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, @@ -309,7 +325,6 @@ impl Picker { filters: Vec::new(), cursor: 0, prompt, - render_centered, truncate_start: true, format_fn: Box::new(format_fn), callback_fn: Box::new(callback_fn), @@ -422,23 +437,10 @@ impl Picker { // - 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 Component for Picker { 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.set_height(viewport.1); + Some(viewport) } fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { @@ -508,14 +510,6 @@ impl Component for Picker { } fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { - let area = if self.render_centered { - inner_rect(area) - } else { - area - }; - - self.set_height(area.height); - let text_style = cx.editor.theme.get("ui.text"); // -- Render the frame: @@ -590,8 +584,6 @@ impl Component for Picker { } fn cursor(&self, area: Rect, editor: &Editor) -> (Option, 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);