diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c63e6e57..afbf1f6b 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -23,23 +23,19 @@ use crossterm::{ execute, terminal, }; -use tui::{backend::CrosstermBackend, layout::Rect}; - -type Terminal = crate::terminal::Terminal>; +use tui::layout::Rect; pub struct Application { compositor: Compositor, editor: Editor, - terminal: Terminal, executor: &'static smol::Executor<'static>, } impl Application { pub fn new(mut args: Args, executor: &'static smol::Executor<'static>) -> Result { - let backend = CrosstermBackend::new(stdout()); - let mut terminal = Terminal::new(backend)?; - let size = terminal.size()?; + let mut compositor = Compositor::new()?; + let size = compositor.size(); let mut editor = Editor::new(size); let files = args.values_of_t::("files").unwrap(); @@ -47,12 +43,10 @@ impl Application { editor.open(file, executor)?; } - let mut compositor = Compositor::new(); compositor.push(Box::new(ui::EditorView::new())); let mut app = Self { editor, - terminal, compositor, executor, @@ -64,17 +58,11 @@ impl Application { fn render(&mut self) { let executor = &self.executor; let editor = &mut self.editor; - let compositor = &self.compositor; + let compositor = &mut self.compositor; let mut cx = crate::compositor::Context { editor, executor }; - let area = self.terminal.size().unwrap(); - compositor.render(area, self.terminal.current_buffer_mut(), &mut cx); - let pos = compositor - .cursor_position(area, &editor) - .map(|pos| (pos.col as u16, pos.row as u16)); - - self.terminal.draw(pos); + compositor.render(&mut cx); } pub async fn event_loop(&mut self) { @@ -107,7 +95,7 @@ impl Application { // Handle key events let should_redraw = match event { Some(Ok(Event::Resize(width, height))) => { - self.terminal.resize(Rect::new(0, 0, width, height)); + self.compositor.resize(width, height); self.compositor .handle_event(Event::Resize(width, height), &mut cx) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 4a5f1007..47a61b7f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -39,9 +39,12 @@ impl<'a> Context<'a> { } /// Push a new component onto the compositor. - pub fn push_layer(&mut self, component: Box) { + pub fn push_layer(&mut self, mut component: Box) { self.callback = Some(Box::new( |compositor: &mut Compositor, editor: &mut Editor| { + let size = compositor.size(); + // trigger required_size on init + component.required_size((size.width, size.height)); compositor.push(component); }, )); diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 3c90b76a..0a08a0d1 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -1,14 +1,3 @@ -// 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), ) @@ -36,7 +25,7 @@ pub enum EventResult { } use helix_view::{Editor, View}; -// shared with commands.rs + pub struct Context<'a> { pub editor: &'a mut Editor, pub executor: &'static smol::Executor<'static>, @@ -54,46 +43,53 @@ pub trait Component { true } + /// Render the component onto the provided surface. fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context); fn cursor_position(&self, area: Rect, ctx: &Editor) -> Option { None } - fn size_hint(&self, area: Rect) -> Option<(usize, usize)> { + /// May be used by the parent component to compute the child area. + /// viewport is the maximum allowed area, and the child should stay within those bounds. + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + // TODO: the compositor should trigger this on push_layer too so that we can use it as an + // initializer there too. + // + // TODO: for scrolling, the scroll wrapper should place a size + offset on the Context + // that way render can use it None } } -// 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 +use anyhow::Error; +use std::io::stdout; +use tui::backend::CrosstermBackend; +type Terminal = crate::terminal::Terminal>; pub struct Compositor { layers: Vec>, + terminal: Terminal, } impl Compositor { - pub fn new() -> Self { - Self { layers: Vec::new() } + pub fn new() -> Result { + let backend = CrosstermBackend::new(stdout()); + let mut terminal = Terminal::new(backend)?; + Ok(Self { + layers: Vec::new(), + terminal, + }) + } + + pub fn size(&self) -> Rect { + self.terminal.size().expect("couldn't get terminal size") + } + + pub fn resize(&mut self, width: u16, height: u16) { + self.terminal + .resize(Rect::new(0, 0, width, height)) + .expect("Unable to resize terminal") } pub fn push(&mut self, layer: Box) { @@ -120,10 +116,19 @@ impl Compositor { false } - pub fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + pub fn render(&mut self, cx: &mut Context) { + let area = self.size(); + let surface = self.terminal.current_buffer_mut(); + for layer in &self.layers { layer.render(area, surface, cx) } + + let pos = self + .cursor_position(area, cx.editor) + .map(|pos| (pos.col as u16, pos.row as u16)); + + self.terminal.draw(pos); } pub fn cursor_position(&self, area: Rect, editor: &Editor) -> Option { diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 8456ef74..976d00fa 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -59,16 +59,14 @@ impl Component for Markdown { Event::End(tag) => { tags.pop(); match tag { - Tag::Heading(_) | Tag::Paragraph => { - // whenever paragraph closes, new line + Tag::Heading(_) + | Tag::Paragraph + | Tag::CodeBlock(CodeBlockKind::Fenced(_)) => { + // whenever code block or paragraph closes, new line let spans = std::mem::replace(&mut spans, Vec::new()); lines.push(Spans::from(spans)); lines.push(Spans::default()); } - Tag::CodeBlock(CodeBlockKind::Fenced(_)) => { - let spans = std::mem::replace(&mut spans, Vec::new()); - lines.push(Spans::from(spans)); - } _ => (), } } @@ -117,14 +115,15 @@ impl Component for Markdown { let par = Paragraph::new(contents).wrap(Wrap { trim: false }); // .scroll(x, y) offsets - // padding on all sides let area = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2); par.render(area, surface); } - fn size_hint(&self, area: Rect) -> Option<(usize, usize)> { + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { let contents = tui::text::Text::from(self.contents.clone()); - Some((contents.width(), contents.height())) + let width = std::cmp::min(contents.width() as u16, viewport.0); + let height = std::cmp::min(contents.height() as u16, viewport.1); + Some((width, height)) } } diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 9f0e79be..c129420d 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -149,16 +149,21 @@ impl Component for Menu { EventResult::Ignored } - fn size_hint(&self, area: Rect) -> Option<(usize, usize)> { + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + let width = std::cmp::min(30, viewport.0); + const MAX: usize = 5; let height = std::cmp::min(self.options.len(), MAX); - Some((30, height)) + let height = std::cmp::min(height, viewport.1 as usize); + + Some((width as u16, height as u16)) } 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)); + // TODO: instead of a cell, all these numbers should be precomputed in handle_event + init let mut scroll = self.scroll.get(); let len = self.options.len(); diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 625ee2b3..4ca70c50 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -18,6 +18,7 @@ use helix_view::Editor; pub struct Popup { contents: Box, position: Option, + size: (u16, u16), } impl Popup { @@ -27,6 +28,7 @@ impl Popup { Self { contents, position: None, + size: (0, 0), } } @@ -68,6 +70,17 @@ impl Component for Popup { // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll. } + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + let (width, height) = self + .contents + .required_size((120, 26)) // max width, max height + .expect("Component needs required_size implemented in order to be embedded in a popup"); + + self.size = (width, height); + + Some(self.size) + } + fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { use tui::text::Text; use tui::widgets::{Paragraph, Widget, Wrap}; @@ -77,13 +90,7 @@ impl Component for Popup { .or_else(|| cx.editor.cursor_position()) .unwrap_or_default(); - 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(120) as u16; - let height = height.min(26) as u16; + let (width, height) = self.size; // -- make sure frame doesn't stick out of bounds let mut rel_x = position.col as u16; diff --git a/helix-term/src/ui/text.rs b/helix-term/src/ui/text.rs index 133cdd25..9db4d3bc 100644 --- a/helix-term/src/ui/text.rs +++ b/helix-term/src/ui/text.rs @@ -33,8 +33,10 @@ impl Component for Text { par.render(area, surface); } - fn size_hint(&self, area: Rect) -> Option<(usize, usize)> { + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { let contents = tui::text::Text::from(self.contents.clone()); - Some((contents.width(), contents.height())) + let width = std::cmp::min(contents.width() as u16, viewport.0); + let height = std::cmp::min(contents.height() as u16, viewport.1); + Some((width, height)) } }