use crate::{ buffer::Buffer, symbols::line, text::{Span, Spans}, widgets::{Borders, Widget}, }; use helix_view::graphics::{Rect, Style}; #[derive(Debug, Clone, Copy, PartialEq)] pub enum BorderType { Plain, Rounded, Double, Thick, } impl BorderType { pub fn line_symbols(border_type: Self) -> line::Set { match border_type { Self::Plain => line::NORMAL, Self::Rounded => line::ROUNDED, Self::Double => line::DOUBLE, Self::Thick => line::THICK, } } } /// Base widget to be used with all upper level ones. It may be used to display a box border around /// the widget and/or add a title. /// /// # Examples /// /// ``` /// # use helix_tui::widgets::{Block, BorderType, Borders}; /// # use helix_view::graphics::{Style, Color}; /// Block::default() /// .title("Block") /// .borders(Borders::LEFT | Borders::RIGHT) /// .border_style(Style::default().fg(Color::White)) /// .border_type(BorderType::Rounded) /// .style(Style::default().bg(Color::Black)); /// ``` #[derive(Debug, Clone, PartialEq)] pub struct Block<'a> { /// Optional title place on the upper left of the block title: Option>, /// Visible borders borders: Borders, /// Border style border_style: Style, /// Type of the border. The default is plain lines but one can choose to have rounded corners /// or doubled lines instead. border_type: BorderType, /// Widget style style: Style, } impl<'a> Default for Block<'a> { fn default() -> Block<'a> { Block { title: None, borders: Borders::NONE, border_style: Default::default(), border_type: BorderType::Plain, style: Default::default(), } } } impl<'a> Block<'a> { pub fn title(mut self, title: T) -> Block<'a> where T: Into>, { self.title = Some(title.into()); self } #[deprecated( since = "0.10.0", note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title." )] pub fn title_style(mut self, style: Style) -> Block<'a> { if let Some(t) = self.title { let title = String::from(t); self.title = Some(Spans::from(Span::styled(title, style))); } self } pub fn border_style(mut self, style: Style) -> Block<'a> { self.border_style = style; self } pub fn style(mut self, style: Style) -> Block<'a> { self.style = style; self } pub fn borders(mut self, flag: Borders) -> Block<'a> { self.borders = flag; self } pub fn border_type(mut self, border_type: BorderType) -> Block<'a> { self.border_type = border_type; self } /// Compute the inner area of a block based on its border visibility rules. pub fn inner(&self, area: Rect) -> Rect { let mut inner = area; if self.borders.intersects(Borders::LEFT) { inner.x = inner.x.saturating_add(1).min(inner.right()); inner.width = inner.width.saturating_sub(1); } if self.borders.intersects(Borders::TOP) || self.title.is_some() { inner.y = inner.y.saturating_add(1).min(inner.bottom()); inner.height = inner.height.saturating_sub(1); } if self.borders.intersects(Borders::RIGHT) { inner.width = inner.width.saturating_sub(1); } if self.borders.intersects(Borders::BOTTOM) { inner.height = inner.height.saturating_sub(1); } inner } } impl<'a> Widget for Block<'a> { fn render(self, area: Rect, buf: &mut Buffer) { if area.area() == 0 { return; } buf.clear_with(area, self.style); let symbols = BorderType::line_symbols(self.border_type); // Sides if self.borders.intersects(Borders::LEFT) { for y in area.top()..area.bottom() { buf[(area.left(), y)] .set_symbol(symbols.vertical) .set_style(self.border_style); } } if self.borders.intersects(Borders::TOP) { for x in area.left()..area.right() { buf[(x, area.top())] .set_symbol(symbols.horizontal) .set_style(self.border_style); } } if self.borders.intersects(Borders::RIGHT) { let x = area.right() - 1; for y in area.top()..area.bottom() { buf[(x, y)] .set_symbol(symbols.vertical) .set_style(self.border_style); } } if self.borders.intersects(Borders::BOTTOM) { let y = area.bottom() - 1; for x in area.left()..area.right() { buf[(x, y)] .set_symbol(symbols.horizontal) .set_style(self.border_style); } } // Corners if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) { buf[(area.right() - 1, area.bottom() - 1)] .set_symbol(symbols.bottom_right) .set_style(self.border_style); } if self.borders.contains(Borders::RIGHT | Borders::TOP) { buf[(area.right() - 1, area.top())] .set_symbol(symbols.top_right) .set_style(self.border_style); } if self.borders.contains(Borders::LEFT | Borders::BOTTOM) { buf[(area.left(), area.bottom() - 1)] .set_symbol(symbols.bottom_left) .set_style(self.border_style); } if self.borders.contains(Borders::LEFT | Borders::TOP) { buf[(area.left(), area.top())] .set_symbol(symbols.top_left) .set_style(self.border_style); } if let Some(title) = self.title { let lx = if self.borders.intersects(Borders::LEFT) { 1 } else { 0 }; let rx = if self.borders.intersects(Borders::RIGHT) { 1 } else { 0 }; let width = area.width.saturating_sub(lx).saturating_sub(rx); buf.set_spans(area.left() + lx, area.top(), &title, width); } } } #[cfg(test)] mod tests { use super::*; #[test] fn inner_takes_into_account_the_borders() { // No borders assert_eq!( Block::default().inner(Rect::default()), Rect { x: 0, y: 0, width: 0, height: 0 }, "no borders, width=0, height=0" ); assert_eq!( Block::default().inner(Rect { x: 0, y: 0, width: 1, height: 1 }), Rect { x: 0, y: 0, width: 1, height: 1 }, "no borders, width=1, height=1" ); // Left border assert_eq!( Block::default().borders(Borders::LEFT).inner(Rect { x: 0, y: 0, width: 0, height: 1 }), Rect { x: 0, y: 0, width: 0, height: 1 }, "left, width=0" ); assert_eq!( Block::default().borders(Borders::LEFT).inner(Rect { x: 0, y: 0, width: 1, height: 1 }), Rect { x: 1, y: 0, width: 0, height: 1 }, "left, width=1" ); assert_eq!( Block::default().borders(Borders::LEFT).inner(Rect { x: 0, y: 0, width: 2, height: 1 }), Rect { x: 1, y: 0, width: 1, height: 1 }, "left, width=2" ); // Top border assert_eq!( Block::default().borders(Borders::TOP).inner(Rect { x: 0, y: 0, width: 1, height: 0 }), Rect { x: 0, y: 0, width: 1, height: 0 }, "top, height=0" ); assert_eq!( Block::default().borders(Borders::TOP).inner(Rect { x: 0, y: 0, width: 1, height: 1 }), Rect { x: 0, y: 1, width: 1, height: 0 }, "top, height=1" ); assert_eq!( Block::default().borders(Borders::TOP).inner(Rect { x: 0, y: 0, width: 1, height: 2 }), Rect { x: 0, y: 1, width: 1, height: 1 }, "top, height=2" ); // Right border assert_eq!( Block::default().borders(Borders::RIGHT).inner(Rect { x: 0, y: 0, width: 0, height: 1 }), Rect { x: 0, y: 0, width: 0, height: 1 }, "right, width=0" ); assert_eq!( Block::default().borders(Borders::RIGHT).inner(Rect { x: 0, y: 0, width: 1, height: 1 }), Rect { x: 0, y: 0, width: 0, height: 1 }, "right, width=1" ); assert_eq!( Block::default().borders(Borders::RIGHT).inner(Rect { x: 0, y: 0, width: 2, height: 1 }), Rect { x: 0, y: 0, width: 1, height: 1 }, "right, width=2" ); // Bottom border assert_eq!( Block::default().borders(Borders::BOTTOM).inner(Rect { x: 0, y: 0, width: 1, height: 0 }), Rect { x: 0, y: 0, width: 1, height: 0 }, "bottom, height=0" ); assert_eq!( Block::default().borders(Borders::BOTTOM).inner(Rect { x: 0, y: 0, width: 1, height: 1 }), Rect { x: 0, y: 0, width: 1, height: 0 }, "bottom, height=1" ); assert_eq!( Block::default().borders(Borders::BOTTOM).inner(Rect { x: 0, y: 0, width: 1, height: 2 }), Rect { x: 0, y: 0, width: 1, height: 1 }, "bottom, height=2" ); // All borders assert_eq!( Block::default() .borders(Borders::ALL) .inner(Rect::default()), Rect { x: 0, y: 0, width: 0, height: 0 }, "all borders, width=0, height=0" ); assert_eq!( Block::default().borders(Borders::ALL).inner(Rect { x: 0, y: 0, width: 1, height: 1 }), Rect { x: 1, y: 1, width: 0, height: 0, }, "all borders, width=1, height=1" ); assert_eq!( Block::default().borders(Borders::ALL).inner(Rect { x: 0, y: 0, width: 2, height: 2, }), Rect { x: 1, y: 1, width: 0, height: 0, }, "all borders, width=2, height=2" ); assert_eq!( Block::default().borders(Borders::ALL).inner(Rect { x: 0, y: 0, width: 3, height: 3, }), Rect { x: 1, y: 1, width: 1, height: 1, }, "all borders, width=3, height=3" ); } #[test] fn inner_takes_into_account_the_title() { assert_eq!( Block::default().title("Test").inner(Rect { x: 0, y: 0, width: 0, height: 1, }), Rect { x: 0, y: 1, width: 0, height: 0, }, ); } }