diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7ffa28a3..bbd78092 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1117,16 +1117,8 @@ pub fn completion(cx: &mut Context) { }, ); - cx.callback = Some(Box::new( - move |compositor: &mut Compositor, editor: &mut Editor| { - if let Some(mut pos) = editor.cursor_position() { - pos.row += 1; // shift down by one row - menu.set_position(pos); - }; - - compositor.push(Box::new(menu)); - }, - )); + let popup = Popup::new(Box::new(menu)); + cx.push_layer(Box::new(popup)); // TODO!: when iterating over items, show the docs in popup @@ -1171,22 +1163,9 @@ pub fn hover(cx: &mut Context) { // skip if contents empty - // Popup: box frame + Box for internal content. - // it will use the contents.size_hint/required size to figure out sizing & positioning - // can also use render_buffer to render the content. - // render_buffer(highlights/scopes, text, surface, theme) - // - let mut popup = Popup::new(contents); - - cx.callback = Some(Box::new( - move |compositor: &mut Compositor, editor: &mut Editor| { - if let Some(mut pos) = editor.cursor_position() { - popup.set_position(pos); - }; - - compositor.push(Box::new(popup)); - }, - )); + let contents = ui::Text::new(contents); + let mut popup = Popup::new(Box::new(contents)); + cx.push_layer(Box::new(popup)); } } diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 59e93e03..3c90b76a 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -44,7 +44,9 @@ pub struct Context<'a> { pub trait Component { /// Process input events, return true if handled. - fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult; + fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult { + EventResult::Ignored + } // , args: () /// Should redraw? Useful for saving redraw cycles if we know component didn't change. @@ -57,6 +59,10 @@ pub trait Component { fn cursor_position(&self, area: Rect, ctx: &Editor) -> Option { None } + + fn size_hint(&self, area: Rect) -> Option<(usize, usize)> { + None + } } // For v1: diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 7053a179..3fd5ed63 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -20,8 +20,6 @@ pub struct Menu { cursor: usize, - position: Position, - format_fn: Box Cow>, callback_fn: Box, MenuEvent)>, } @@ -37,16 +35,11 @@ impl Menu { Self { options, cursor: 0, - position: Position::default(), format_fn: Box::new(format_fn), callback_fn: Box::new(callback_fn), } } - pub fn set_position(&mut self, pos: Position) { - self.position = pos; - } - pub fn move_up(&mut self) { self.cursor = self.cursor.saturating_sub(1); } @@ -151,31 +144,18 @@ impl Component for Menu { // EventResult::Consumed(None) EventResult::Ignored } - fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { - // render a box at x, y. Width equal to max width of item. - // initially limit to n items, add support for scrolling - // - const MAX: usize = 5; - let rows = std::cmp::min(self.options.len(), MAX) as u16; - let area = Rect::new(self.position.col as u16, self.position.row as u16, 30, rows); - - // clear area - let background = cx.editor.theme.get("ui.popup"); - for y in area.top()..area.bottom() { - for x in area.left()..area.right() { - let cell = surface.get_mut(x, y); - cell.reset(); - // cell.symbol.clear(); - cell.set_style(background); - } - } - // -- Render the contents: + fn size_hint(&self, area: Rect) -> Option<(usize, usize)> { + const MAX: usize = 5; + let height = std::cmp::min(self.options.len(), MAX); + Some((30, height)) + } + fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { let style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender let selected = Style::default().fg(Color::Rgb(255, 255, 255)); - for (i, option) in self.options.iter().take(rows as usize).enumerate() { + for (i, option) in self.options.iter().take(area.height as usize).enumerate() { // TODO: set bg for the whole row if selected surface.set_stringn( area.x, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 1526a210..4fbdd550 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -3,12 +3,14 @@ mod menu; mod picker; mod popup; mod prompt; +mod text; pub use editor::EditorView; pub use menu::Menu; pub use picker::Picker; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; +pub use text::Text; pub use tui::layout::Rect; pub use tui::style::{Color, Modifier, Style}; diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 673321dc..ba32e6b5 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -16,28 +16,28 @@ use helix_view::Editor; // a width/height hint. maybe Popup(Box) pub struct Popup { - contents: String, - position: Position, + contents: Box, + position: Option, } impl Popup { // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different // rendering) - pub fn new(contents: String) -> Self { + pub fn new(contents: Box) -> Self { Self { contents, - position: Position::default(), + position: None, } } - pub fn set_position(&mut self, pos: Position) { + pub fn set_position(&mut self, pos: Option) { self.position = pos; } } impl Component for Popup { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { - let event = match event { + let key = match event { Event::Key(event) => event, _ => return EventResult::Ignored, }; @@ -49,7 +49,7 @@ impl Component for Popup { }, ))); - match event { + match key { // esc or ctrl-c aborts the completion and closes the menu KeyEvent { code: KeyCode::Esc, .. @@ -60,29 +60,37 @@ impl Component for Popup { } => { return close_fn; } - _ => (), + _ => self.contents.handle_event(event, cx), } // for some events, we want to process them but send ignore, specifically all input except // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll. - // EventResult::Consumed(None) - EventResult::Consumed(None) } fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { use tui::text::Text; use tui::widgets::{Paragraph, Widget, Wrap}; - let contents = Text::from(self.contents.clone()); + let position = self + .position + .or_else(|| cx.editor.cursor_position()) + .unwrap_or_default(); - let width = contents.width().min(150) as u16; - let height = contents.height().min(13) as u16; + let (width, height) = self + .contents + .size_hint(viewport) + .expect("Component needs size_hint implemented in order to be embedded in a popup"); + + let width = width.min(150) as u16; + let height = height.min(13) as u16; // -- make sure frame doesn't stick out of bounds - let mut rel_x = self.position.col as u16; - let mut rel_y = self.position.row as u16; + 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 + width) - viewport.width) }; + // TODO: be able to specify orientation preference. We want above for most popups, below + // for menus/autocomplete. if height <= rel_y { rel_y -= height // position above point } else { @@ -104,13 +112,6 @@ impl Component for Popup { } } - // -- Render the contents: - - let style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender - - let par = Paragraph::new(contents).wrap(Wrap { trim: false }); - // .scroll(x, y) offsets - - par.render(area, surface); + self.contents.render(area, surface, cx); } } diff --git a/helix-term/src/ui/text.rs b/helix-term/src/ui/text.rs new file mode 100644 index 00000000..bacb68b8 --- /dev/null +++ b/helix-term/src/ui/text.rs @@ -0,0 +1,41 @@ +use crate::compositor::{Component, Compositor, Context, EventResult}; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; +use tui::buffer::Buffer as Surface; +use tui::{ + layout::Rect, + style::{Color, Style}, + widgets::{Block, Borders}, +}; + +use std::borrow::Cow; + +use helix_core::Position; +use helix_view::Editor; + +pub struct Text { + contents: String, +} + +impl Text { + pub fn new(contents: String) -> Self { + Self { contents } + } +} +impl Component for Text { + fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + use tui::widgets::{Paragraph, Widget, Wrap}; + let contents = tui::text::Text::from(self.contents.clone()); + + let style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender + + let par = Paragraph::new(contents).wrap(Wrap { trim: false }); + // .scroll(x, y) offsets + + par.render(area, surface); + } + + fn size_hint(&self, area: Rect) -> Option<(usize, usize)> { + let contents = tui::text::Text::from(self.contents.clone()); + Some((contents.width(), contents.height())) + } +}