From f9375f449c8b7bbf792c88b89903fe38d524f2e5 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sat, 21 Aug 2021 10:51:20 +0530 Subject: Refactor new Rect construction (#575) * Refactor new Rect construction Introduces methods that can be chained to construct new Rects out of pre-existing ones * Clamp x and y to edges in Rect chop methods * Rename Rect clipping functions--- helix-term/src/ui/editor.rs | 24 ++++-------- helix-term/src/ui/markdown.rs | 9 +++-- helix-term/src/ui/menu.rs | 8 ---- helix-term/src/ui/picker.rs | 37 +++++++++--------- helix-view/src/graphics.rs | 87 ++++++++++++++++++++++++++++++++++++++++++- helix-view/src/view.rs | 7 +--- 6 files changed, 119 insertions(+), 53 deletions(-) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index b0e6de3e..d01d08e8 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -108,13 +108,11 @@ impl EditorView { self.render_diagnostics(doc, view, inner, surface, theme); - let area = Rect::new( - view.area.x, - view.area.y + view.area.height.saturating_sub(1), - view.area.width, - 1, - ); - self.render_statusline(doc, view, area, surface, theme, is_focused); + let statusline_area = view + .area + .clip_top(view.area.height.saturating_sub(1)) + .clip_bottom(1); // -1 from bottom to remove commandline + self.render_statusline(doc, view, statusline_area, surface, theme, is_focused); } /// Get syntax highlights for a document in a view represented by the first line @@ -528,12 +526,7 @@ impl EditorView { let width = 80.min(viewport.width); let height = 15.min(viewport.height); paragraph.render( - Rect::new( - viewport.right() - width, - viewport.y as u16 + 1, - width, - height, - ), + Rect::new(viewport.right() - width, viewport.y + 1, width, height), surface, ); } @@ -572,7 +565,7 @@ impl EditorView { theme.get("ui.statusline.inactive") }; // statusline - surface.set_style(Rect::new(viewport.x, viewport.y, viewport.width, 1), style); + surface.set_style(viewport.with_height(1), style); if is_focused { surface.set_string(viewport.x + 1, viewport.y, mode, style); } @@ -1001,8 +994,7 @@ impl Component for EditorView { surface.set_style(area, cx.editor.theme.get("ui.background")); // if the terminal size suddenly changed, we need to trigger a resize - cx.editor - .resize(Rect::new(area.x, area.y, area.width, area.height - 1)); // - 1 to account for commandline + cx.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline for (view, is_focused) in cx.editor.tree.views() { let doc = cx.editor.document(view.doc).unwrap(); diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index b7e29f21..28542cdc 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -13,7 +13,7 @@ use helix_core::{ Rope, }; use helix_view::{ - graphics::{Color, Rect, Style}, + graphics::{Color, Margin, Rect, Style}, Theme, }; @@ -207,8 +207,11 @@ impl Component for Markdown { .wrap(Wrap { trim: false }) .scroll((cx.scroll.unwrap_or_default() as u16, 0)); - let area = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2); - par.render(area, surface); + let margin = Margin { + vertical: 1, + horizontal: 1, + }; + par.render(area.inner(&margin), surface); } fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 3e63db35..a56cf19b 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -304,14 +304,6 @@ impl Component for Menu { }, ); - // // TODO: set bg for the whole row if selected - // if line == self.cursor { - // surface.set_style( - // Rect::new(area.x, area.y + i as u16, area.width - 1, 1), - // selected, - // ) - // } - for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() { let is_marked = i >= scroll_line && i < scroll_line + scroll_height; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 2cc25c24..ecf9d528 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -19,7 +19,7 @@ use helix_core::Position; use helix_view::{ document::canonicalize_path, editor::Action, - graphics::{Color, CursorKind, Rect, Style}, + graphics::{Color, CursorKind, Margin, Rect, Style}, Document, Editor, }; @@ -90,23 +90,26 @@ impl Component for FilePicker { area.width }; - let picker_area = Rect::new(area.x, area.y, picker_width, area.height); + let picker_area = area.with_width(picker_width); self.picker.render(picker_area, surface, cx); if !render_preview { return; } - let preview_area = Rect::new(area.x + picker_width, area.y, area.width / 2, area.height); + let preview_area = area.clip_left(picker_width); // don't like this but the lifetime sucks let block = Block::default().borders(Borders::ALL); // calculate the inner area inside the box - let mut inner = block.inner(preview_area); + let inner = block.inner(preview_area); // 1 column gap on either side - inner.x += 1; - inner.width = inner.width.saturating_sub(2); + let margin = Margin { + vertical: 1, + horizontal: 0, + }; + let inner = inner.inner(&margin); block.render(preview_area, surface); @@ -282,15 +285,11 @@ impl Picker { // - score all the names in relation to input fn inner_rect(area: Rect) -> Rect { - let padding_vertical = area.height * 10 / 100; - let padding_horizontal = area.width * 10 / 100; - - Rect::new( - area.x + padding_horizontal, - area.y + padding_vertical, - area.width - padding_horizontal * 2, - area.height - padding_vertical * 2, - ) + let margin = Margin { + vertical: area.height * 10 / 100, + horizontal: area.width * 10 / 100, + }; + area.inner(&margin) } impl Component for Picker { @@ -410,7 +409,7 @@ impl Component for Picker { // -- Render the input bar: - let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1); + let area = inner.clip_left(1).with_height(1); let count = format!("{}/{}", self.matches.len(), self.options.len()); surface.set_stringn( @@ -434,8 +433,8 @@ impl Component for Picker { } // -- Render the contents: - // subtract the area of the prompt (-2) and current item marker " > " (-3) - let inner = Rect::new(inner.x + 3, inner.y + 2, inner.width - 3, inner.height - 2); + // subtract area of prompt from top and current item marker " > " from left + let inner = inner.clip_top(2).clip_left(3); let selected = cx.editor.theme.get("ui.text.focus"); @@ -474,7 +473,7 @@ impl Component for Picker { let inner = block.inner(area); // prompt area - let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1); + let area = inner.clip_left(1).with_height(1); self.prompt.cursor(area, editor) } diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 9a7a86c3..66013ee5 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -24,7 +24,7 @@ pub struct Margin { } /// A simple rectangle used in the computation of the layout and to give widgets an hint about the -/// area they are supposed to render to. +/// area they are supposed to render to. (x, y) = (0, 0) is at the top left corner of the screen. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct Rect { pub x: u16, @@ -92,6 +92,57 @@ impl Rect { self.y.saturating_add(self.height) } + // Returns a new Rect with width reduced from the left side. + // This changes the `x` coordinate and clamps it to the right + // edge of the original Rect. + pub fn clip_left(self, width: u16) -> Rect { + let width = std::cmp::min(width, self.width); + Rect { + x: self.x.saturating_add(width), + width: self.width.saturating_sub(width), + ..self + } + } + + // Returns a new Rect with width reduced from the right side. + // This does _not_ change the `x` coordinate. + pub fn clip_right(self, width: u16) -> Rect { + Rect { + width: self.width.saturating_sub(width), + ..self + } + } + + // Returns a new Rect with height reduced from the top. + // This changes the `y` coordinate and clamps it to the bottom + // edge of the original Rect. + pub fn clip_top(self, height: u16) -> Rect { + let height = std::cmp::min(height, self.height); + Rect { + y: self.y.saturating_add(height), + height: self.height.saturating_sub(height), + ..self + } + } + + // Returns a new Rect with height reduced from the bottom. + // This does _not_ change the `y` coordinate. + pub fn clip_bottom(self, height: u16) -> Rect { + Rect { + height: self.height.saturating_sub(height), + ..self + } + } + + pub fn with_height(self, height: u16) -> Rect { + // new height may make area > u16::max_value, so use new() + Self::new(self.x, self.y, self.width, height) + } + + pub fn with_width(self, width: u16) -> Rect { + Self::new(self.x, self.y, width, self.height) + } + pub fn inner(self, margin: &Margin) -> Rect { if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical { Rect::default() @@ -495,6 +546,40 @@ mod tests { assert_eq!(rect.height, 100); } + #[test] + fn test_rect_chop_from_left() { + let rect = Rect::new(0, 0, 20, 30); + assert_eq!(Rect::new(10, 0, 10, 30), rect.clip_left(10)); + assert_eq!( + Rect::new(20, 0, 0, 30), + rect.clip_left(40), + "x should be clamped to original width if new width is bigger" + ); + } + + #[test] + fn test_rect_chop_from_right() { + let rect = Rect::new(0, 0, 20, 30); + assert_eq!(Rect::new(0, 0, 10, 30), rect.clip_right(10)); + } + + #[test] + fn test_rect_chop_from_top() { + let rect = Rect::new(0, 0, 20, 30); + assert_eq!(Rect::new(0, 10, 20, 20), rect.clip_top(10)); + assert_eq!( + Rect::new(0, 30, 20, 0), + rect.clip_top(50), + "y should be clamped to original height if new height is bigger" + ); + } + + #[test] + fn test_rect_chop_from_bottom() { + let rect = Rect::new(0, 0, 20, 30); + assert_eq!(Rect::new(0, 0, 20, 20), rect.clip_bottom(10)); + } + fn styles() -> Vec