on master help sign in
wip: Compositor
83f2c24Blaž Hrastnik
helix-term/src/application.rs+149−107
@@ -8,6 +8,8 @@ use helix_view::{
8 Document, Editor, Theme, View,
8 Document, Editor, Theme, View,
9};
9};
10
10
11+use crate::compositor::{Component, Compositor};
12+
11use log::{debug, info};
13use log::{debug, info};
12
14
13use std::{
15use std::{
@@ -35,23 +37,21 @@ use tui::{
35 style::{Color, Modifier, Style},
37 style::{Color, Modifier, Style},
36};
38};
37
39
38−const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
39−
40type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
40type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
41
41
42const BASE_WIDTH: u16 = 30;
42const BASE_WIDTH: u16 = 30;
43
43
44pub struct Application<'a> {
44pub struct Application<'a> {
45− editor: Editor,
46 prompt: Option<Prompt>,
45 prompt: Option<Prompt>,
47− terminal: Renderer,
48
46
49− keymap: Keymaps,
47+ compositor: Compositor,
48+ renderer: Renderer,
49+
50 executor: &'a smol::Executor<'a>,
50 executor: &'a smol::Executor<'a>,
51 language_server: helix_lsp::Client,
51 language_server: helix_lsp::Client,
52}
52}
53
53
54−struct Renderer {
54+pub(crate) struct Renderer {
55 size: (u16, u16),
55 size: (u16, u16),
56 terminal: Terminal,
56 terminal: Terminal,
57 surface: Surface,
57 surface: Surface,
@@ -92,7 +92,6 @@ impl Renderer {
92 // TODO: ideally not &mut View but highlights require it because of cursor cache
92 // TODO: ideally not &mut View but highlights require it because of cursor cache
93 pub fn render_buffer(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
93 pub fn render_buffer(&mut self, view: &mut View, viewport: Rect, theme: &Theme) {
94 let area = Rect::new(0, 0, self.size.0, self.size.1);
94 let area = Rect::new(0, 0, self.size.0, self.size.1);
95− self.surface.reset(); // reset is faster than allocating new empty surface
96
95
97 // clear with background color
96 // clear with background color
98 self.surface.set_style(area, theme.get("ui.background"));
97 self.surface.set_style(area, theme.get("ui.background"));
@@ -221,8 +220,12 @@ impl Renderer {
221
220
222 // TODO: paint cursor heads except primary
221 // TODO: paint cursor heads except primary
223
222
224− self.surface
223+ self.surface.set_string(
225− .set_string(OFFSET + visual_x, line, grapheme, style);
224+ viewport.x + visual_x,
225+ viewport.y + line,
226+ grapheme,
227+ style,
228+ );
226
229
227 visual_x += width;
230 visual_x += width;
228 }
231 }
@@ -321,7 +324,7 @@ impl Renderer {
321 .set_string(2, self.size.1 - 1, &prompt.line, self.text_color);
324 .set_string(2, self.size.1 - 1, &prompt.line, self.text_color);
322 }
325 }
323
326
324− pub fn draw(&mut self) {
327+ pub fn draw_and_swap(&mut self) {
325 use tui::backend::Backend;
328 use tui::backend::Backend;
326 // TODO: theres probably a better place for this
329 // TODO: theres probably a better place for this
327 self.terminal
330 self.terminal
@@ -363,112 +366,40 @@ impl Renderer {
363 }
366 }
364}
367}
365
368
366−impl<'a> Application<'a> {
369+struct EditorView {
367− pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result<Self, Error> {
368− let terminal = Renderer::new()?;
369− let mut editor = Editor::new();
370+ editor: Editor,
370−
371+ prompt: Option<Prompt>, // TODO: this is None for now, make a layer
371− if let Some(file) = args.values_of_t::<PathBuf>("files").unwrap().pop() {
372+ keymap: Keymaps,
372− editor.open(file, terminal.size)?;
373− }
373+}
374−
375− let language_server = helix_lsp::Client::start(&executor, "rust-analyzer", &[]);
376
374
375+impl EditorView {
376+ fn new(editor: Editor) -> Self {
377− let mut app = Self {
377+ Self {
378 editor,
378 editor,
379− terminal,
380− // TODO; move to state
381 prompt: None,
379 prompt: None,
382−
383− //
384 keymap: keymap::default(),
380 keymap: keymap::default(),
385− executor,
386− language_server,
387− };
388−
389− Ok(app)
390− }
391−
392− fn render(&mut self) {
393− let viewport = Rect::new(OFFSET, 0, self.terminal.size.0, self.terminal.size.1 - 2); // - 2 for statusline and prompt
394−
395− // SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow
396− // theme. Theme is immutable mutating view won't disrupt theme_ref.
397− let theme_ref = unsafe { &*(&self.editor.theme as *const Theme) };
398− if let Some(view) = self.editor.view_mut() {
399− self.terminal.render_view(view, viewport, theme_ref);
400− if let Some(prompt) = &self.prompt {
401− if prompt.should_close {
402− self.prompt = None;
403− } else {
404− self.terminal.render_prompt(view, prompt, theme_ref);
405− }
406− }
407− }
408−
409− self.terminal.draw();
410−
411− // TODO: drop unwrap
412− self.terminal
413− .render_cursor(self.editor.view().unwrap(), self.prompt.as_ref(), viewport);
414− }
415−
416− pub async fn event_loop(&mut self) {
417− let mut reader = EventStream::new();
418−
419− // initialize lsp
420− self.language_server.initialize().await.unwrap();
421− self.language_server
422− .text_document_did_open(&self.editor.view().unwrap().doc)
423− .await
424− .unwrap();
425−
426− self.render();
427−
428− loop {
429− if self.editor.should_close {
430− break;
431− }
432−
433− use futures_util::{select, FutureExt};
434− select! {
435− event = reader.next().fuse() => {
436− self.handle_terminal_events(event).await
437− }
438− call = self.language_server.incoming.next().fuse() => {
439− self.handle_language_server_message(call).await
440− }
441− }
442 }
381 }
443 }
382 }
383+}
444
384
445− pub async fn handle_terminal_events(
385+impl Component for EditorView {
446− &mut self,
386+ fn handle_event(&mut self, event: Event, executor: &smol::Executor) -> bool {
447− event: Option<Result<Event, crossterm::ErrorKind>>,
448− ) {
449− // Handle key events
450 match event {
387 match event {
451− Some(Ok(Event::Resize(width, height))) => {
388+ Event::Resize(width, height) => {
452− self.terminal.resize(width, height);
453−
454 // TODO: simplistic ensure cursor in view for now
389 // TODO: simplistic ensure cursor in view for now
455 // TODO: loop over views
390 // TODO: loop over views
456 if let Some(view) = self.editor.view_mut() {
391 if let Some(view) = self.editor.view_mut() {
457− view.size = self.terminal.size;
392+ view.size = (width, height);
458 view.ensure_cursor_in_view()
393 view.ensure_cursor_in_view()
459 };
394 };
460−
461− self.render();
462 }
395 }
463− Some(Ok(Event::Key(event))) => {
396+ Event::Key(event) => {
464 // if there's a prompt, it takes priority
397 // if there's a prompt, it takes priority
465 if let Some(prompt) = &mut self.prompt {
398 if let Some(prompt) = &mut self.prompt {
466 self.prompt
399 self.prompt
467 .as_mut()
400 .as_mut()
468 .unwrap()
401 .unwrap()
469 .handle_input(event, &mut self.editor);
402 .handle_input(event, &mut self.editor);
470−
471− self.render();
472 } else if let Some(view) = self.editor.view_mut() {
403 } else if let Some(view) = self.editor.view_mut() {
473 let keys = vec![event];
404 let keys = vec![event];
474 // TODO: sequences (`gg`)
405 // TODO: sequences (`gg`)
@@ -478,7 +409,7 @@ impl<'a> Application<'a> {
478 if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
409 if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
479 let mut cx = helix_view::commands::Context {
410 let mut cx = helix_view::commands::Context {
480 view,
411 view,
481− executor: self.executor,
412+ executor: executor,
482 count: 1,
413 count: 1,
483 };
414 };
484
415
@@ -490,7 +421,7 @@ impl<'a> Application<'a> {
490 {
421 {
491 let mut cx = helix_view::commands::Context {
422 let mut cx = helix_view::commands::Context {
492 view,
423 view,
493− executor: self.executor,
424+ executor: executor,
494 count: 1,
425 count: 1,
495 };
426 };
496 commands::insert::insert_char(&mut cx, c);
427 commands::insert::insert_char(&mut cx, c);
@@ -557,7 +488,7 @@ impl<'a> Application<'a> {
557 } else if let Some(command) = self.keymap[&Mode::Normal].get(&keys) {
488 } else if let Some(command) = self.keymap[&Mode::Normal].get(&keys) {
558 let mut cx = helix_view::commands::Context {
489 let mut cx = helix_view::commands::Context {
559 view,
490 view,
560− executor: self.executor,
491+ executor: executor,
561 count: 1,
492 count: 1,
562 };
493 };
563 command(&mut cx);
494 command(&mut cx);
@@ -570,7 +501,7 @@ impl<'a> Application<'a> {
570 if let Some(command) = self.keymap[&mode].get(&keys) {
501 if let Some(command) = self.keymap[&mode].get(&keys) {
571 let mut cx = helix_view::commands::Context {
502 let mut cx = helix_view::commands::Context {
572 view,
503 view,
573− executor: self.executor,
504+ executor: executor,
574 count: 1,
505 count: 1,
575 };
506 };
576 command(&mut cx);
507 command(&mut cx);
@@ -580,10 +511,119 @@ impl<'a> Application<'a> {
580 }
511 }
581 }
512 }
582 }
513 }
583− self.render();
584 }
514 }
585 }
515 }
586− Some(Ok(Event::Mouse(_))) => (), // unhandled
516+ Event::Mouse(_) => (),
517+ }
518+
519+ true
520+ }
521+ fn render(&mut self, renderer: &mut Renderer) {
522+ const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
523+ let viewport = Rect::new(OFFSET, 0, renderer.size.0, renderer.size.1 - 2); // - 2 for statusline and prompt
524+
525+ // SAFETY: we cheat around the view_mut() borrow because it doesn't allow us to also borrow
526+ // theme. Theme is immutable mutating view won't disrupt theme_ref.
527+ let theme_ref = unsafe { &*(&self.editor.theme as *const Theme) };
528+ if let Some(view) = self.editor.view_mut() {
529+ renderer.render_view(view, viewport, theme_ref);
530+ if let Some(prompt) = &self.prompt {
531+ if prompt.should_close {
532+ self.prompt = None;
533+ } else {
534+ renderer.render_prompt(view, prompt, theme_ref);
535+ }
536+ }
537+ }
538+
539+ // TODO: drop unwrap
540+ renderer.render_cursor(self.editor.view().unwrap(), self.prompt.as_ref(), viewport);
541+ }
542+}
543+
544+impl<'a> Application<'a> {
545+ pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result<Self, Error> {
546+ let renderer = Renderer::new()?;
547+ let mut editor = Editor::new();
548+
549+ if let Some(file) = args.values_of_t::<PathBuf>("files").unwrap().pop() {
550+ editor.open(file, renderer.size)?;
551+ }
552+
553+ let mut compositor = Compositor::new();
554+ compositor.push(Box::new(EditorView::new(editor)));
555+
556+ let language_server = helix_lsp::Client::start(&executor, "rust-analyzer", &[]);
557+
558+ let mut app = Self {
559+ renderer,
560+ // TODO; move to state
561+ compositor,
562+ prompt: None,
563+
564+ executor,
565+ language_server,
566+ };
567+
568+ Ok(app)
569+ }
570+
571+ fn render(&mut self) {
572+ // v2:
573+ self.renderer.surface.reset(); // reset is faster than allocating new empty surface
574+ self.compositor.render(&mut self.renderer); // viewport,
575+ self.renderer.draw_and_swap();
576+ }
577+
578+ pub async fn event_loop(&mut self) {
579+ let mut reader = EventStream::new();
580+
581+ // initialize lsp
582+ self.language_server.initialize().await.unwrap();
583+ // TODO: temp
584+ // self.language_server
585+ // .text_document_did_open(&self.editor.view().unwrap().doc)
586+ // .await
587+ // .unwrap();
588+
589+ self.render();
590+
591+ loop {
592+ // TODO:
593+ // if self.editor.should_close {
594+ // break;
595+ // }
596+
597+ use futures_util::{select, FutureExt};
598+ select! {
599+ event = reader.next().fuse() => {
600+ self.handle_terminal_events(event)
601+ }
602+ call = self.language_server.incoming.next().fuse() => {
603+ self.handle_language_server_message(call).await
604+ }
605+ }
606+ }
607+ }
608+
609+ pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
610+ // Handle key events
611+ match event {
612+ Some(Ok(Event::Resize(width, height))) => {
613+ self.renderer.resize(width, height);
614+
615+ // TODO: use the response
616+ self.compositor
617+ .handle_event(Event::Resize(width, height), self.executor);
618+
619+ self.render();
620+ }
621+ Some(Ok(event)) => {
622+ // TODO: use the response
623+ self.compositor.handle_event(event, self.executor);
624+
625+ self.render();
626+ }
587 Some(Err(x)) => panic!(x),
627 Some(Err(x)) => panic!(x),
588 None => panic!(),
628 None => panic!(),
589 };
629 };
@@ -599,11 +639,13 @@ impl<'a> Application<'a> {
599 match notification {
639 match notification {
600 Notification::PublishDiagnostics(params) => {
640 Notification::PublishDiagnostics(params) => {
601 let path = Some(params.uri.to_file_path().unwrap());
641 let path = Some(params.uri.to_file_path().unwrap());
602− let view = self
642+ let view: Option<&mut helix_view::View> = None;
643+ // TODO:
644+ // let view = self
603− .editor
645+ // .editor
604− .views
646+ // .views
605− .iter_mut()
647+ // .iter_mut()
606− .find(|view| view.doc.path == path);
648+ // .find(|view| view.doc.path == path);
607
649
608 if let Some(view) = view {
650 if let Some(view) = view {
609 let doc = view.doc.text().slice(..);
651 let doc = view.doc.text().slice(..);
helix-term/src/compositor.rsnew file+111
@@ -0,0 +1,111 @@
1+// Features:
2+// Tracks currently focused component which receives all input
3+// Event loop is external as opposed to cursive-rs
4+// Calls render on the component and translates screen coords to local component coords
5+//
6+// TODO:
7+// Q: where is the Application state stored? do we store it into an external static var?
8+// A: probably makes sense to initialize the editor into a `static Lazy<>` global var.
9+//
10+// Q: how do we composit nested structures? There should be sub-components/views
11+//
12+// Each component declares it's own size constraints and gets fitted based on it's parent.
13+// Q: how does this work with popups?
14+// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
15+
16+use crate::application::Renderer;
17+use crossterm::event::Event;
18+use smol::Executor;
19+use tui::buffer::Buffer as Surface;
20+
21+pub(crate) trait Component {
22+ /// Process input events, return true if handled.
23+ fn handle_event(&mut self, event: Event, executor: &Executor) -> bool;
24+ // , args: ()
25+
26+ /// Should redraw? Useful for saving redraw cycles if we know component didn't change.
27+ fn should_update(&self) -> bool {
28+ true
29+ }
30+
31+ fn render(&mut self, renderer: &mut Renderer);
32+}
33+
34+// struct Editor { };
35+
36+// For v1:
37+// Child views are something each view needs to handle on it's own for now, positioning and sizing
38+// options, focus tracking. In practice this is simple: we only will need special solving for
39+// splits etc
40+
41+// impl Editor {
42+// fn render(&mut self, surface: &mut Surface, args: ()) {
43+// // compute x, y, w, h rects for sub-views!
44+// // get surface area
45+// // get constraints for textarea, statusbar
46+// // -> cassowary-rs
47+
48+// // first render textarea
49+// // then render statusbar
50+// }
51+// }
52+
53+// usecases to consider:
54+// - a single view with subviews (textarea + statusbar)
55+// - a popup panel / dialog with it's own interactions
56+// - an autocomplete popup that doesn't change focus
57+
58+//fn main() {
59+// let root = Editor::new();
60+// let compositor = Compositor::new();
61+
62+// compositor.push(root);
63+
64+// // pos: clip to bottom of screen
65+// compositor.push_at(pos, Prompt::new(
66+// ":",
67+// (),
68+// |input: &str| match input {}
69+// )); // TODO: this Prompt needs to somehow call compositor.pop() on close, but it can't refer to parent
70+// // Cursive solves this by allowing to return a special result on process_event
71+// // that's either Ignore | Consumed(Opt<C>) where C: fn (Compositor) -> ()
72+
73+// // TODO: solve popup focus: we want to push autocomplete popups on top of the current layer
74+// // but retain the focus where it was. The popup will also need to update as we type into the
75+// // textarea. It should also capture certain input, such as tab presses etc
76+// //
77+// // 1) This could be faked by the top layer pushing down edits into the previous layer.
78+// // 2) Alternatively,
79+//}
80+
81+pub(crate) struct Compositor {
82+ layers: Vec<Box<dyn Component>>,
83+}
84+
85+impl Compositor {
86+ pub fn new() -> Self {
87+ Self { layers: Vec::new() }
88+ }
89+
90+ pub fn push(&mut self, layer: Box<dyn Component>) {
91+ self.layers.push(layer);
92+ }
93+
94+ pub fn pop(&mut self) {
95+ self.layers.pop();
96+ }
97+
98+ pub fn handle_event(&mut self, event: Event, executor: &Executor) -> () {
99+ // TODO: custom focus
100+ if let Some(layer) = self.layers.last_mut() {
101+ layer.handle_event(event, executor);
102+ // return should_update
103+ }
104+ }
105+
106+ pub fn render(&mut self, renderer: &mut Renderer) {
107+ for layer in &mut self.layers {
108+ layer.render(renderer)
109+ }
110+ }
111+}
helix-term/src/main.rs+1
@@ -1,6 +1,7 @@
1#![allow(unused)]
1#![allow(unused)]
2
2
3mod application;
3mod application;
4+mod compositor;
4
5
5use application::Application;
6use application::Application;
6
7