on master help sign in
ui: Share popup code with menu.
0b85c16Blaž Hrastnik
helix-term/src/commands.rs+5−26
@@ -1117,16 +1117,8 @@ pub fn completion(cx: &mut Context) {
1117 },
1117 },
1118 );
1118 );
1119
1119
1120− cx.callback = Some(Box::new(
1121− move |compositor: &mut Compositor, editor: &mut Editor| {
1122− if let Some(mut pos) = editor.cursor_position() {
1123− pos.row += 1; // shift down by one row
1124− menu.set_position(pos);
1125− };
1126−
1127− compositor.push(Box::new(menu));
1120+ let popup = Popup::new(Box::new(menu));
1128− },
1121+ cx.push_layer(Box::new(popup));
1129− ));
1130
1122
1131 // TODO!: when iterating over items, show the docs in popup
1123 // TODO!: when iterating over items, show the docs in popup
1132
1124
@@ -1171,22 +1163,9 @@ pub fn hover(cx: &mut Context) {
1171
1163
1172 // skip if contents empty
1164 // skip if contents empty
1173
1165
1174− // Popup: box frame + Box<Component> for internal content.
1166+ let contents = ui::Text::new(contents);
1175− // it will use the contents.size_hint/required size to figure out sizing & positioning
1176− // can also use render_buffer to render the content.
1177− // render_buffer(highlights/scopes, text, surface, theme)
1178− //
1179− let mut popup = Popup::new(contents);
1167+ let mut popup = Popup::new(Box::new(contents));
1180−
1181− cx.callback = Some(Box::new(
1182− move |compositor: &mut Compositor, editor: &mut Editor| {
1183− if let Some(mut pos) = editor.cursor_position() {
1184− popup.set_position(pos);
1185− };
1186−
1187− compositor.push(Box::new(popup));
1168+ cx.push_layer(Box::new(popup));
1188− },
1189− ));
1190 }
1169 }
1191}
1170}
1192
1171
helix-term/src/compositor.rs+7−1
@@ -44,7 +44,9 @@ pub struct Context<'a> {
44
44
45pub trait Component {
45pub trait Component {
46 /// Process input events, return true if handled.
46 /// Process input events, return true if handled.
47− fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult;
47+ fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult {
48+ EventResult::Ignored
49+ }
48 // , args: ()
50 // , args: ()
49
51
50 /// Should redraw? Useful for saving redraw cycles if we know component didn't change.
52 /// Should redraw? Useful for saving redraw cycles if we know component didn't change.
@@ -57,6 +59,10 @@ pub trait Component {
57 fn cursor_position(&self, area: Rect, ctx: &Editor) -> Option<Position> {
59 fn cursor_position(&self, area: Rect, ctx: &Editor) -> Option<Position> {
58 None
60 None
59 }
61 }
62+
63+ fn size_hint(&self, area: Rect) -> Option<(usize, usize)> {
64+ None
65+ }
60}
66}
61
67
62// For v1:
68// For v1:
helix-term/src/ui/menu.rs+7−27
@@ -20,8 +20,6 @@ pub struct Menu<T> {
20
20
21 cursor: usize,
21 cursor: usize,
22
22
23− position: Position,
24−
25 format_fn: Box<dyn Fn(&T) -> Cow<str>>,
23 format_fn: Box<dyn Fn(&T) -> Cow<str>>,
26 callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
24 callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
27}
25}
@@ -37,16 +35,11 @@ impl<T> Menu<T> {
37 Self {
35 Self {
38 options,
36 options,
39 cursor: 0,
37 cursor: 0,
40− position: Position::default(),
41 format_fn: Box::new(format_fn),
38 format_fn: Box::new(format_fn),
42 callback_fn: Box::new(callback_fn),
39 callback_fn: Box::new(callback_fn),
43 }
40 }
44 }
41 }
45
42
46− pub fn set_position(&mut self, pos: Position) {
47− self.position = pos;
48− }
49−
50 pub fn move_up(&mut self) {
43 pub fn move_up(&mut self) {
51 self.cursor = self.cursor.saturating_sub(1);
44 self.cursor = self.cursor.saturating_sub(1);
52 }
45 }
@@ -151,31 +144,18 @@ impl<T> Component for Menu<T> {
151 // EventResult::Consumed(None)
144 // EventResult::Consumed(None)
152 EventResult::Ignored
145 EventResult::Ignored
153 }
146 }
154− fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
155− // render a box at x, y. Width equal to max width of item.
156− // initially limit to n items, add support for scrolling
157− //
158− const MAX: usize = 5;
159− let rows = std::cmp::min(self.options.len(), MAX) as u16;
160− let area = Rect::new(self.position.col as u16, self.position.row as u16, 30, rows);
161−
162− // clear area
163− let background = cx.editor.theme.get("ui.popup");
164− for y in area.top()..area.bottom() {
165− for x in area.left()..area.right() {
166− let cell = surface.get_mut(x, y);
167− cell.reset();
168− // cell.symbol.clear();
169− cell.set_style(background);
170− }
171− }
172
147
173− // -- Render the contents:
148+ fn size_hint(&self, area: Rect) -> Option<(usize, usize)> {
149+ const MAX: usize = 5;
150+ let height = std::cmp::min(self.options.len(), MAX);
151+ Some((30, height))
152+ }
174
153
154+ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
175 let style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender
155 let style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender
176 let selected = Style::default().fg(Color::Rgb(255, 255, 255));
156 let selected = Style::default().fg(Color::Rgb(255, 255, 255));
177
157
178− for (i, option) in self.options.iter().take(rows as usize).enumerate() {
158+ for (i, option) in self.options.iter().take(area.height as usize).enumerate() {
179 // TODO: set bg for the whole row if selected
159 // TODO: set bg for the whole row if selected
180 surface.set_stringn(
160 surface.set_stringn(
181 area.x,
161 area.x,
helix-term/src/ui/mod.rs+2
@@ -3,12 +3,14 @@ mod menu;
3mod picker;
3mod picker;
4mod popup;
4mod popup;
5mod prompt;
5mod prompt;
6+mod text;
6
7
7pub use editor::EditorView;
8pub use editor::EditorView;
8pub use menu::Menu;
9pub use menu::Menu;
9pub use picker::Picker;
10pub use picker::Picker;
10pub use popup::Popup;
11pub use popup::Popup;
11pub use prompt::{Prompt, PromptEvent};
12pub use prompt::{Prompt, PromptEvent};
13+pub use text::Text;
12
14
13pub use tui::layout::Rect;
15pub use tui::layout::Rect;
14pub use tui::style::{Color, Modifier, Style};
16pub use tui::style::{Color, Modifier, Style};
helix-term/src/ui/popup.rs+24−23
@@ -16,28 +16,28 @@ use helix_view::Editor;
16// a width/height hint. maybe Popup(Box<Component>)
16// a width/height hint. maybe Popup(Box<Component>)
17
17
18pub struct Popup {
18pub struct Popup {
19− contents: String,
19+ contents: Box<dyn Component>,
20− position: Position,
20+ position: Option<Position>,
21}
21}
22
22
23impl Popup {
23impl Popup {
24 // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
24 // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
25 // rendering)
25 // rendering)
26− pub fn new(contents: String) -> Self {
26+ pub fn new(contents: Box<dyn Component>) -> Self {
27 Self {
27 Self {
28 contents,
28 contents,
29− position: Position::default(),
29+ position: None,
30 }
30 }
31 }
31 }
32
32
33− pub fn set_position(&mut self, pos: Position) {
33+ pub fn set_position(&mut self, pos: Option<Position>) {
34 self.position = pos;
34 self.position = pos;
35 }
35 }
36}
36}
37
37
38impl Component for Popup {
38impl Component for Popup {
39 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
39 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
40− let event = match event {
40+ let key = match event {
41 Event::Key(event) => event,
41 Event::Key(event) => event,
42 _ => return EventResult::Ignored,
42 _ => return EventResult::Ignored,
43 };
43 };
@@ -49,7 +49,7 @@ impl Component for Popup {
49 },
49 },
50 )));
50 )));
51
51
52− match event {
52+ match key {
53 // esc or ctrl-c aborts the completion and closes the menu
53 // esc or ctrl-c aborts the completion and closes the menu
54 KeyEvent {
54 KeyEvent {
55 code: KeyCode::Esc, ..
55 code: KeyCode::Esc, ..
@@ -60,29 +60,37 @@ impl Component for Popup {
60 } => {
60 } => {
61 return close_fn;
61 return close_fn;
62 }
62 }
63− _ => (),
63+ _ => self.contents.handle_event(event, cx),
64 }
64 }
65 // for some events, we want to process them but send ignore, specifically all input except
65 // for some events, we want to process them but send ignore, specifically all input except
66 // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
66 // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
67− // EventResult::Consumed(None)
68− EventResult::Consumed(None)
69 }
67 }
70 fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
68 fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
71 use tui::text::Text;
69 use tui::text::Text;
72 use tui::widgets::{Paragraph, Widget, Wrap};
70 use tui::widgets::{Paragraph, Widget, Wrap};
73
71
74− let contents = Text::from(self.contents.clone());
72+ let position = self
73+ .position
74+ .or_else(|| cx.editor.cursor_position())
75+ .unwrap_or_default();
75
76
77+ let (width, height) = self
76− let width = contents.width().min(150) as u16;
78+ .contents
79+ .size_hint(viewport)
80+ .expect("Component needs size_hint implemented in order to be embedded in a popup");
81+
82+ let width = width.min(150) as u16;
77− let height = contents.height().min(13) as u16;
83+ let height = height.min(13) as u16;
78
84
79 // -- make sure frame doesn't stick out of bounds
85 // -- make sure frame doesn't stick out of bounds
80− let mut rel_x = self.position.col as u16;
86+ let mut rel_x = position.col as u16;
81− let mut rel_y = self.position.row as u16;
87+ let mut rel_y = position.row as u16;
82 if viewport.width <= rel_x + width {
88 if viewport.width <= rel_x + width {
83 rel_x -= ((rel_x + width) - viewport.width)
89 rel_x -= ((rel_x + width) - viewport.width)
84 };
90 };
85
91
92+ // TODO: be able to specify orientation preference. We want above for most popups, below
93+ // for menus/autocomplete.
86 if height <= rel_y {
94 if height <= rel_y {
87 rel_y -= height // position above point
95 rel_y -= height // position above point
88 } else {
96 } else {
@@ -104,13 +112,6 @@ impl Component for Popup {
104 }
112 }
105 }
113 }
106
114
107− // -- Render the contents:
108−
109− let style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender
110−
111− let par = Paragraph::new(contents).wrap(Wrap { trim: false });
112− // .scroll(x, y) offsets
113−
114− par.render(area, surface);
115+ self.contents.render(area, surface, cx);
115 }
116 }
116}
117}
helix-term/src/ui/text.rsnew file+41
@@ -0,0 +1,41 @@
1+use crate::compositor::{Component, Compositor, Context, EventResult};
2+use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
3+use tui::buffer::Buffer as Surface;
4+use tui::{
5+ layout::Rect,
6+ style::{Color, Style},
7+ widgets::{Block, Borders},
8+};
9+
10+use std::borrow::Cow;
11+
12+use helix_core::Position;
13+use helix_view::Editor;
14+
15+pub struct Text {
16+ contents: String,
17+}
18+
19+impl Text {
20+ pub fn new(contents: String) -> Self {
21+ Self { contents }
22+ }
23+}
24+impl Component for Text {
25+ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
26+ use tui::widgets::{Paragraph, Widget, Wrap};
27+ let contents = tui::text::Text::from(self.contents.clone());
28+
29+ let style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender
30+
31+ let par = Paragraph::new(contents).wrap(Wrap { trim: false });
32+ // .scroll(x, y) offsets
33+
34+ par.render(area, surface);
35+ }
36+
37+ fn size_hint(&self, area: Rect) -> Option<(usize, usize)> {
38+ let contents = tui::text::Text::from(self.contents.clone());
39+ Some((contents.width(), contents.height()))
40+ }
41+}