diff options
author | Blaž Hrastnik | 2020-12-06 02:53:58 +0000 |
---|---|---|
committer | Blaž Hrastnik | 2020-12-06 02:53:58 +0000 |
commit | 83f2c24115cc5a3dce90a77440f1ef06f6cf9c78 (patch) | |
tree | b048d36f82d716700c3b2784913dde5d98f2691c | |
parent | b7a3e525ed7fed5ed79e8580df2e3496bd994419 (diff) |
wip: Compositor
-rw-r--r-- | helix-term/src/application.rs | 256 | ||||
-rw-r--r-- | helix-term/src/compositor.rs | 111 | ||||
-rw-r--r-- | helix-term/src/main.rs | 1 |
3 files changed, 261 insertions, 107 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 141779ec..30258c1d 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -8,6 +8,8 @@ use helix_view::{ Document, Editor, Theme, View, }; +use crate::compositor::{Component, Compositor}; + use log::{debug, info}; use std::{ @@ -35,23 +37,21 @@ use tui::{ style::{Color, Modifier, Style}, }; -const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter - type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>; const BASE_WIDTH: u16 = 30; pub struct Application<'a> { - editor: Editor, prompt: Option<Prompt>, - terminal: Renderer, - keymap: Keymaps, + compositor: Compositor, + renderer: Renderer, + executor: &'a smol::Executor<'a>, language_server: helix_lsp::Client, } -struct Renderer { +pub(crate) struct Renderer { size: (u16, u16), terminal: Terminal, surface: Surface, @@ -92,7 +92,6 @@ impl Renderer { // TODO: ideally not &mut View but highlights require it because of cursor cache pub fn render_buffer(&mut self, view: &mut View, viewport: Rect, theme: &Theme) { let area = Rect::new(0, 0, self.size.0, self.size.1); - self.surface.reset(); // reset is faster than allocating new empty surface // clear with background color self.surface.set_style(area, theme.get("ui.background")); @@ -221,8 +220,12 @@ impl Renderer { // TODO: paint cursor heads except primary - self.surface - .set_string(OFFSET + visual_x, line, grapheme, style); + self.surface.set_string( + viewport.x + visual_x, + viewport.y + line, + grapheme, + style, + ); visual_x += width; } @@ -321,7 +324,7 @@ impl Renderer { .set_string(2, self.size.1 - 1, &prompt.line, self.text_color); } - pub fn draw(&mut self) { + pub fn draw_and_swap(&mut self) { use tui::backend::Backend; // TODO: theres probably a better place for this self.terminal @@ -363,112 +366,40 @@ impl Renderer { } } -impl<'a> Application<'a> { - pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result<Self, Error> { - let terminal = Renderer::new()?; - let mut editor = Editor::new(); - - if let Some(file) = args.values_of_t::<PathBuf>("files").unwrap().pop() { - editor.open(file, terminal.size)?; - } - - let language_server = helix_lsp::Client::start(&executor, "rust-analyzer", &[]); +struct EditorView { + editor: Editor, + prompt: Option<Prompt>, // TODO: this is None for now, make a layer + keymap: Keymaps, +} - let mut app = Self { +impl EditorView { + fn new(editor: Editor) -> Self { + Self { editor, - terminal, - // TODO; move to state prompt: None, - - // keymap: keymap::default(), - executor, - language_server, - }; - - Ok(app) - } - - fn render(&mut self) { - let viewport = Rect::new(OFFSET, 0, self.terminal.size.0, self.terminal.size.1 - 2); // - 2 for statusline and prompt - - // SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow - // theme. Theme is immutable mutating view won't disrupt theme_ref. - let theme_ref = unsafe { &*(&self.editor.theme as *const Theme) }; - if let Some(view) = self.editor.view_mut() { - self.terminal.render_view(view, viewport, theme_ref); - if let Some(prompt) = &self.prompt { - if prompt.should_close { - self.prompt = None; - } else { - self.terminal.render_prompt(view, prompt, theme_ref); - } - } - } - - self.terminal.draw(); - - // TODO: drop unwrap - self.terminal - .render_cursor(self.editor.view().unwrap(), self.prompt.as_ref(), viewport); - } - - pub async fn event_loop(&mut self) { - let mut reader = EventStream::new(); - - // initialize lsp - self.language_server.initialize().await.unwrap(); - self.language_server - .text_document_did_open(&self.editor.view().unwrap().doc) - .await - .unwrap(); - - self.render(); - - loop { - if self.editor.should_close { - break; - } - - use futures_util::{select, FutureExt}; - select! { - event = reader.next().fuse() => { - self.handle_terminal_events(event).await - } - call = self.language_server.incoming.next().fuse() => { - self.handle_language_server_message(call).await - } - } } } +} - pub async fn handle_terminal_events( - &mut self, - event: Option<Result<Event, crossterm::ErrorKind>>, - ) { - // Handle key events +impl Component for EditorView { + fn handle_event(&mut self, event: Event, executor: &smol::Executor) -> bool { match event { - Some(Ok(Event::Resize(width, height))) => { - self.terminal.resize(width, height); - + Event::Resize(width, height) => { // TODO: simplistic ensure cursor in view for now // TODO: loop over views if let Some(view) = self.editor.view_mut() { - view.size = self.terminal.size; + view.size = (width, height); view.ensure_cursor_in_view() }; - - self.render(); } - Some(Ok(Event::Key(event))) => { + Event::Key(event) => { // if there's a prompt, it takes priority if let Some(prompt) = &mut self.prompt { self.prompt .as_mut() .unwrap() .handle_input(event, &mut self.editor); - - self.render(); } else if let Some(view) = self.editor.view_mut() { let keys = vec![event]; // TODO: sequences (`gg`) @@ -478,7 +409,7 @@ impl<'a> Application<'a> { if let Some(command) = self.keymap[&Mode::Insert].get(&keys) { let mut cx = helix_view::commands::Context { view, - executor: self.executor, + executor: executor, count: 1, }; @@ -490,7 +421,7 @@ impl<'a> Application<'a> { { let mut cx = helix_view::commands::Context { view, - executor: self.executor, + executor: executor, count: 1, }; commands::insert::insert_char(&mut cx, c); @@ -557,7 +488,7 @@ impl<'a> Application<'a> { } else if let Some(command) = self.keymap[&Mode::Normal].get(&keys) { let mut cx = helix_view::commands::Context { view, - executor: self.executor, + executor: executor, count: 1, }; command(&mut cx); @@ -570,7 +501,7 @@ impl<'a> Application<'a> { if let Some(command) = self.keymap[&mode].get(&keys) { let mut cx = helix_view::commands::Context { view, - executor: self.executor, + executor: executor, count: 1, }; command(&mut cx); @@ -580,10 +511,119 @@ impl<'a> Application<'a> { } } } - self.render(); } } - Some(Ok(Event::Mouse(_))) => (), // unhandled + Event::Mouse(_) => (), + } + + true + } + fn render(&mut self, renderer: &mut Renderer) { + const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter + let viewport = Rect::new(OFFSET, 0, renderer.size.0, renderer.size.1 - 2); // - 2 for statusline and prompt + + // SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow + // theme. Theme is immutable mutating view won't disrupt theme_ref. + let theme_ref = unsafe { &*(&self.editor.theme as *const Theme) }; + if let Some(view) = self.editor.view_mut() { + renderer.render_view(view, viewport, theme_ref); + if let Some(prompt) = &self.prompt { + if prompt.should_close { + self.prompt = None; + } else { + renderer.render_prompt(view, prompt, theme_ref); + } + } + } + + // TODO: drop unwrap + renderer.render_cursor(self.editor.view().unwrap(), self.prompt.as_ref(), viewport); + } +} + +impl<'a> Application<'a> { + pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result<Self, Error> { + let renderer = Renderer::new()?; + let mut editor = Editor::new(); + + if let Some(file) = args.values_of_t::<PathBuf>("files").unwrap().pop() { + editor.open(file, renderer.size)?; + } + + let mut compositor = Compositor::new(); + compositor.push(Box::new(EditorView::new(editor))); + + let language_server = helix_lsp::Client::start(&executor, "rust-analyzer", &[]); + + let mut app = Self { + renderer, + // TODO; move to state + compositor, + prompt: None, + + executor, + language_server, + }; + + Ok(app) + } + + fn render(&mut self) { + // v2: + self.renderer.surface.reset(); // reset is faster than allocating new empty surface + self.compositor.render(&mut self.renderer); // viewport, + self.renderer.draw_and_swap(); + } + + pub async fn event_loop(&mut self) { + let mut reader = EventStream::new(); + + // initialize lsp + self.language_server.initialize().await.unwrap(); + // TODO: temp + // self.language_server + // .text_document_did_open(&self.editor.view().unwrap().doc) + // .await + // .unwrap(); + + self.render(); + + loop { + // TODO: + // if self.editor.should_close { + // break; + // } + + use futures_util::{select, FutureExt}; + select! { + event = reader.next().fuse() => { + self.handle_terminal_events(event) + } + call = self.language_server.incoming.next().fuse() => { + self.handle_language_server_message(call).await + } + } + } + } + + pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) { + // Handle key events + match event { + Some(Ok(Event::Resize(width, height))) => { + self.renderer.resize(width, height); + + // TODO: use the response + self.compositor + .handle_event(Event::Resize(width, height), self.executor); + + self.render(); + } + Some(Ok(event)) => { + // TODO: use the response + self.compositor.handle_event(event, self.executor); + + self.render(); + } Some(Err(x)) => panic!(x), None => panic!(), }; @@ -599,11 +639,13 @@ impl<'a> Application<'a> { match notification { Notification::PublishDiagnostics(params) => { let path = Some(params.uri.to_file_path().unwrap()); - let view = self - .editor - .views - .iter_mut() - .find(|view| view.doc.path == path); + let view: Option<&mut helix_view::View> = None; + // TODO: + // let view = self + // .editor + // .views + // .iter_mut() + // .find(|view| view.doc.path == path); if let Some(view) = view { let doc = view.doc.text().slice(..); diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs new file mode 100644 index 00000000..187c5692 --- /dev/null +++ b/helix-term/src/compositor.rs @@ -0,0 +1,111 @@ +// Features: +// Tracks currently focused component which receives all input +// Event loop is external as opposed to cursive-rs +// Calls render on the component and translates screen coords to local component coords +// +// TODO: +// Q: where is the Application state stored? do we store it into an external static var? +// A: probably makes sense to initialize the editor into a `static Lazy<>` global var. +// +// Q: how do we composit nested structures? There should be sub-components/views +// +// Each component declares it's own size constraints and gets fitted based on it's parent. +// Q: how does this work with popups? +// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>) + +use crate::application::Renderer; +use crossterm::event::Event; +use smol::Executor; +use tui::buffer::Buffer as Surface; + +pub(crate) trait Component { + /// Process input events, return true if handled. + fn handle_event(&mut self, event: Event, executor: &Executor) -> bool; + // , args: () + + /// Should redraw? Useful for saving redraw cycles if we know component didn't change. + fn should_update(&self) -> bool { + true + } + + fn render(&mut self, renderer: &mut Renderer); +} + +// struct Editor { }; + +// For v1: +// Child views are something each view needs to handle on it's own for now, positioning and sizing +// options, focus tracking. In practice this is simple: we only will need special solving for +// splits etc + +// impl Editor { +// fn render(&mut self, surface: &mut Surface, args: ()) { +// // compute x, y, w, h rects for sub-views! +// // get surface area +// // get constraints for textarea, statusbar +// // -> cassowary-rs + +// // first render textarea +// // then render statusbar +// } +// } + +// usecases to consider: +// - a single view with subviews (textarea + statusbar) +// - a popup panel / dialog with it's own interactions +// - an autocomplete popup that doesn't change focus + +//fn main() { +// let root = Editor::new(); +// let compositor = Compositor::new(); + +// compositor.push(root); + +// // pos: clip to bottom of screen +// compositor.push_at(pos, Prompt::new( +// ":", +// (), +// |input: &str| match input {} +// )); // TODO: this Prompt needs to somehow call compositor.pop() on close, but it can't refer to parent +// // Cursive solves this by allowing to return a special result on process_event +// // that's either Ignore | Consumed(Opt<C>) where C: fn (Compositor) -> () + +// // TODO: solve popup focus: we want to push autocomplete popups on top of the current layer +// // but retain the focus where it was. The popup will also need to update as we type into the +// // textarea. It should also capture certain input, such as tab presses etc +// // +// // 1) This could be faked by the top layer pushing down edits into the previous layer. +// // 2) Alternatively, +//} + +pub(crate) struct Compositor { + layers: Vec<Box<dyn Component>>, +} + +impl Compositor { + pub fn new() -> Self { + Self { layers: Vec::new() } + } + + pub fn push(&mut self, layer: Box<dyn Component>) { + self.layers.push(layer); + } + + pub fn pop(&mut self) { + self.layers.pop(); + } + + pub fn handle_event(&mut self, event: Event, executor: &Executor) -> () { + // TODO: custom focus + if let Some(layer) = self.layers.last_mut() { + layer.handle_event(event, executor); + // return should_update + } + } + + pub fn render(&mut self, renderer: &mut Renderer) { + for layer in &mut self.layers { + layer.render(renderer) + } + } +} diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 9378d3ee..a43aebd8 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,6 +1,7 @@ #![allow(unused)] mod application; +mod compositor; use application::Application; |