Skip to content

Commit

Permalink
Add overlay component
Browse files Browse the repository at this point in the history
  • Loading branch information
Aloso committed Feb 3, 2022
1 parent 256cad6 commit 7cbd215
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 77 deletions.
4 changes: 2 additions & 2 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
},
Expand Down Expand Up @@ -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))
}
},
Expand Down
8 changes: 7 additions & 1 deletion helix-term/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub(crate) mod editor;
mod info;
mod markdown;
pub mod menu;
mod overlay;
mod picker;
mod popup;
mod prompt;
Expand All @@ -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>,
Expand Down Expand Up @@ -93,7 +96,10 @@ pub fn regex_prompt(
)
}

pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
pub fn file_picker(
root: PathBuf,
config: &helix_view::editor::Config,
) -> Overlay<FilePicker<PathBuf>> {
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time;

Expand Down
101 changes: 101 additions & 0 deletions helix-term/src/ui/overlay.rs
Original file line number Diff line number Diff line change
@@ -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<T> {
/// 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<T> Overlay<T> {
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<T: Component + 'static> Component for Overlay<T> {
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<Position>, CursorKind) {
let dimensions = self.get_dimensions((area.width, area.height));
self.content.cursor(dimensions, ctx)
}
}
122 changes: 48 additions & 74 deletions helix-term/src/ui/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ 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;
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;

Expand Down Expand Up @@ -88,13 +90,21 @@ impl<T> FilePicker<T> {
format_fn: impl Fn(&T) -> Cow<str> + 'static,
callback_fn: impl Fn(&mut Editor, &T, Action) + 'static,
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + '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<Self> {
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,
}
}

Expand Down Expand Up @@ -160,8 +170,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");
Expand Down Expand Up @@ -260,6 +269,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> {
Expand All @@ -277,8 +296,6 @@ pub struct Picker<T> {
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,

Expand All @@ -288,7 +305,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 Editor, &T, Action) + 'static,
Expand All @@ -309,7 +325,6 @@ 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),
Expand Down Expand Up @@ -350,56 +365,38 @@ impl<T> Picker<T> {
self.cursor = 0;
}

/// Move the cursor by a number of lines, `amount`.
///
/// Positive numbers move the cursor down, negative numbers move it up. The cursor cycles
/// through the entries.
pub fn move_by(&mut self, amount: isize) {
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 cursor: isize = self.cursor.try_into().unwrap();
let len: isize = self.matches.len().try_into().unwrap();

self.cursor = cursor
.saturating_add(amount)
.rem_euclid(len)
.try_into()
.unwrap();
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;
}
}
}

/// 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 isize));
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 isize);
self.move_by(self.completion_height as usize, Direction::Forward);
}

/// Move the cursor to the first entry
pub fn to_start(&mut self) {
if self.matches.is_empty() {
return;
}
self.cursor = 0;
}

/// Move the cursor to the last entry
pub fn to_end(&mut self) {
if self.matches.is_empty() {
return;
}
self.cursor = self.matches.len() - 1;
}

/// Sets the height of the component, which is necessary for [`Picker::page_down`] and
/// [`Picker::page_up`]
pub fn set_height(&mut self, new_height: u16) {
// subtract borders and input line
self.completion_height = new_height - 4;
self.cursor = self.matches.len().saturating_sub(1);
}

pub fn selection(&self) -> Option<&T> {
Expand All @@ -422,23 +419,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 {
Expand All @@ -455,10 +439,10 @@ impl<T: 'static> Component for Picker<T> {

match key_event.into() {
shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_by(-1);
self.move_by(1, Direction::Backward);
}
key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
self.move_by(1);
self.move_by(1, Direction::Forward);
}
key!(PageDown) | ctrl!('f') => {
self.page_down();
Expand Down Expand Up @@ -508,14 +492,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
};

self.set_height(area.height);

let text_style = cx.editor.theme.get("ui.text");

// -- Render the frame:
Expand Down Expand Up @@ -590,8 +566,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);
Expand Down

0 comments on commit 7cbd215

Please sign in to comment.