use self::buffer::{Buffer, Rect, Size};
use super::*;
#[derive(Clone, Debug)]
pub struct Border<V> {
pub(crate) child: V,
pub(crate) border_color: Color,
pub(crate) border_style: BorderStyle,
pub(crate) title: Option<String>,
}
impl<V> private::Sealed for Border<V> {}
impl<V> Border<V> {
pub fn new(child: V, border_color: Color) -> Self {
Self {
child,
border_color,
border_style: BorderStyle::Single,
title: None,
}
}
pub fn border_color(mut self, border_color: Color) -> Self {
self.border_color = border_color;
self
}
pub fn border_style(mut self, border_style: BorderStyle) -> Self {
self.border_style = border_style;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
fn draw_corner(buffer: &mut Buffer, x: u16, y: u16, symbol: char, color: Color) {
buffer
.get_mut(x, y)
.set_symbol(&symbol.to_string())
.set_fg(color);
}
fn draw_borders(&self, buffer: &mut Buffer, rect: Rect) {
let left = rect.left();
let top = rect.top();
let right = rect.right().saturating_sub(1);
let bottom_y = rect.bottom().saturating_sub(1);
let components = self.border_style.components();
Self::draw_corner(buffer, left, top, components.top_left, self.border_color);
Self::draw_corner(buffer, right, top, components.top_right, self.border_color);
Self::draw_corner(
buffer,
left,
bottom_y,
components.bottom_left,
self.border_color,
);
Self::draw_corner(
buffer,
right,
bottom_y,
components.bottom_right,
self.border_color,
);
draw_horizontal_line(
buffer,
top,
left + 1,
right,
components.horizontal,
self.border_color,
);
draw_horizontal_line(
buffer,
bottom_y,
left + 1,
right,
components.horizontal,
self.border_color,
);
draw_vertical_line(
buffer,
left,
top + 1,
bottom_y,
components.vertical,
self.border_color,
);
draw_vertical_line(
buffer,
right,
top + 1,
bottom_y,
components.vertical,
self.border_color,
);
if let Some(ref title) = self.title {
title.render(
&mut ViewId::empty(),
Context {
rect: Rect {
point: Point {
x: left + 1,
y: top,
},
size: Size {
width: rect.size.width.saturating_sub(2),
height: 1,
},
},
fg: self.border_color,
modifier: Modifier::empty(),
},
&mut AppState::new(),
buffer,
);
}
}
}
#[derive(Debug, Clone)]
pub enum BorderStyle {
Single,
Double,
Rounded,
}
impl BorderStyle {
fn components(&self) -> &'static BorderComponents {
match self {
BorderStyle::Single => &SINGLE_BORDER_COMPONENTS,
BorderStyle::Double => &DOUBLE_BORDER_COMPONENTS,
BorderStyle::Rounded => &ROUNDED_BORDER_COMPONENTS,
}
}
}
struct BorderComponents {
top_left: char,
top_right: char,
bottom_left: char,
bottom_right: char,
horizontal: char,
vertical: char,
}
const SINGLE_BORDER_COMPONENTS: BorderComponents = BorderComponents {
top_left: '┌',
top_right: '┐',
bottom_left: '└',
bottom_right: '┘',
horizontal: '─',
vertical: '│',
};
const DOUBLE_BORDER_COMPONENTS: BorderComponents = BorderComponents {
top_left: '╔',
top_right: '╗',
bottom_left: '╚',
bottom_right: '╝',
horizontal: '═',
vertical: '║',
};
const ROUNDED_BORDER_COMPONENTS: BorderComponents = BorderComponents {
top_left: '╭',
top_right: '╮',
bottom_left: '╰',
bottom_right: '╯',
horizontal: '─',
vertical: '│',
};
fn draw_horizontal_line(
buffer: &mut Buffer,
y: u16,
start_x: u16,
end_x: u16,
char: char,
color: Color,
) {
for x in start_x..end_x {
buffer.set_char_at(x, y, char, color, None, Modifier::empty());
}
}
fn draw_vertical_line(
buffer: &mut Buffer,
x: u16,
start_y: u16,
end_y: u16,
char: char,
color: Color,
) {
for y in start_y..end_y {
buffer.set_char_at(x, y, char, color, None, Modifier::empty());
}
}
impl<V: View> View for Border<V> {
fn size(&self, proposed: Size) -> Size {
let inset = proposed.inset_by(2, 2, 1, 1);
let child_size = self.child.size(inset);
child_size.outset_by(2, 2, 1, 1).min(proposed)
}
fn render(&self, id: &mut ViewId, context: Context, state: &mut AppState, buffer: &mut Buffer) {
let size = self.size(context.rect.size);
let border_context = context.with_size(size);
let inner_context = border_context.clone().inset_by(2, 2, 1, 1);
self.child.render(id, inner_context, state, buffer);
self.draw_borders(buffer, border_context.rect);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
tests::assert_rendered_view,
view::{hstack, text, Size},
};
use pretty_assertions::assert_eq;
#[test]
fn test_border_size() {
let view = hstack((text("Test"), text("View")));
let border = view.border();
let expected_size = Size {
width: 13,
height: 3,
};
assert_eq!(border.size(Size::max()), expected_size);
}
#[test]
fn test_border_rendering() {
let view = hstack((text("Test"), text("View"))).border();
let expected_output = vec![
"┌───────────┐", "│ Test View │", "└───────────┘", ];
assert_rendered_view(view, expected_output, 13, 3);
}
#[test]
fn test_nested_bordered_view() {
let inner_view = text("Nested").border();
let outer_view = inner_view.border();
let expected_output = vec![
"┌────────────┐", "│ ┌────────┐ │", "│ │ Nested │ │", "│ └────────┘ │", "└────────────┘", ];
assert_eq!(
outer_view.size(Size::max()),
Size {
width: 14,
height: 5
}
);
assert_rendered_view(outer_view, expected_output, 14, 5);
}
#[test]
fn test_nested_bordered_view_double() {
let inner_view = text("Nested").border().border_style(BorderStyle::Double);
let outer_view = inner_view.border().border_style(BorderStyle::Double);
let expected_output = vec![
"╔════════════╗", "║ ╔════════╗ ║", "║ ║ Nested ║ ║", "║ ╚════════╝ ║", "╚════════════╝", ];
assert_eq!(
outer_view.size(Size::max()),
Size {
width: 14,
height: 5
}
);
assert_rendered_view(outer_view, expected_output, 14, 5);
}
#[test]
fn test_title() {
let view = text("Titled View").border().title("Title");
let expected_output = vec![
"┌Title────────┐", "│ Titled View │", "└─────────────┘", ];
assert_eq!(
view.size(Size::max()),
Size {
width: 15,
height: 3
}
);
assert_rendered_view(view, expected_output, 15, 3);
}
}