From 0b85c16be91d9326876b2e4a1ae6bdc1381d700d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= <blaz@mxxn.io>
Date: Tue, 2 Mar 2021 18:24:24 +0900
Subject: [PATCH] ui: Share popup code with menu.

Menu is now just wrapped in a popup.
---
 helix-term/src/commands.rs   | 31 ++++--------------------
 helix-term/src/compositor.rs |  8 +++++-
 helix-term/src/ui/menu.rs    | 34 ++++++--------------------
 helix-term/src/ui/mod.rs     |  2 ++
 helix-term/src/ui/popup.rs   | 47 ++++++++++++++++++------------------
 helix-term/src/ui/text.rs    | 41 +++++++++++++++++++++++++++++++
 6 files changed, 86 insertions(+), 77 deletions(-)
 create mode 100644 helix-term/src/ui/text.rs

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<Component> 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<Position> {
         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<T> {
 
     cursor: usize,
 
-    position: Position,
-
     format_fn: Box<dyn Fn(&T) -> Cow<str>>,
     callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
 }
@@ -37,16 +35,11 @@ impl<T> Menu<T> {
         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<T> Component for Menu<T> {
         // 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
-        //
+
+    fn size_hint(&self, area: Rect) -> Option<(usize, usize)> {
         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:
+        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<Component>)
 
 pub struct Popup {
-    contents: String,
-    position: Position,
+    contents: Box<dyn Component>,
+    position: Option<Position>,
 }
 
 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<dyn Component>) -> Self {
         Self {
             contents,
-            position: Position::default(),
+            position: None,
         }
     }
 
-    pub fn set_position(&mut self, pos: Position) {
+    pub fn set_position(&mut self, pos: Option<Position>) {
         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()))
+    }
+}