use std::time::Duration;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::Style;
use ratatui_core::text::Line;
use ratatui_core::widgets::Widget;
use ratatui_widgets::block::Block;
use crate::element::{AnyElement, Element};
pub struct Panel<'a> {
child: AnyElement<'a>,
title: String,
title_right: String,
footer: String,
border_style: Style,
title_style: Style,
pad_x: u16,
}
pub fn panel<'a>(child: impl Element + 'a) -> Panel<'a> {
Panel {
child: Box::new(child),
title: String::new(),
title_right: String::new(),
footer: String::new(),
border_style: Style::default(),
title_style: Style::default(),
pad_x: 0,
}
}
impl Panel<'_> {
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn title_right(mut self, title: impl Into<String>) -> Self {
self.title_right = title.into();
self
}
pub fn footer(mut self, footer: impl Into<String>) -> Self {
self.footer = footer.into();
self
}
pub fn border_style(mut self, style: Style) -> Self {
self.border_style = style;
self
}
pub fn title_style(mut self, style: Style) -> Self {
self.title_style = style;
self
}
pub fn pad_x(mut self, cols: u16) -> Self {
self.pad_x = cols;
self
}
fn inner(&self, area: Rect) -> Rect {
let inset_x = 1u16.saturating_add(self.pad_x);
Rect::new(
area.x.saturating_add(inset_x),
area.y.saturating_add(1),
area.width.saturating_sub(inset_x.saturating_mul(2)),
area.height.saturating_sub(2),
)
}
}
impl Element for Panel<'_> {
fn height(&self, width: u16) -> u16 {
let inner_width = width.saturating_sub(2 + self.pad_x * 2);
if inner_width == 0 {
return 0;
}
self.child.height(inner_width).max(1).saturating_add(2)
}
fn render(&self, area: Rect, buf: &mut Buffer) {
if area.width < 2 || area.height < 2 {
return;
}
let mut block = Block::bordered().border_style(self.border_style);
if !self.title.is_empty() {
block = block.title_top(
Line::styled(format!(" {} ", self.title), self.title_style).left_aligned(),
);
}
if !self.title_right.is_empty() {
block = block.title_top(
Line::styled(format!(" {} ", self.title_right), self.border_style).right_aligned(),
);
}
if !self.footer.is_empty() {
block = block.title_bottom(
Line::styled(format!(" {} ", self.footer), self.border_style).right_aligned(),
);
}
block.render(area, buf);
let inner = self.inner(area);
if inner.width > 0 && inner.height > 0 {
self.child.render(inner, buf);
}
}
fn animated(&self) -> Option<Duration> {
self.child.animated()
}
fn cursor(&self, area: Rect) -> Option<(u16, u16)> {
if area.width < 2 || area.height < 2 {
return None;
}
let inner = self.inner(area);
if inner.width == 0 || inner.height == 0 {
return None;
}
self.child.cursor(inner).map(|(col, row)| {
(
col.saturating_add(inner.x - area.x),
row.saturating_add(inner.y - area.y),
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::text::text;
fn rendered(el: &impl Element, width: u16) -> Vec<String> {
let height = el.height(width);
let area = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(area);
el.render(area, &mut buf);
(0..height)
.map(|y| {
let mut line: String = (0..width).map(|x| buf[(x, y)].symbol()).collect();
while line.ends_with(' ') {
line.pop();
}
line
})
.collect()
}
#[test]
fn border_adds_two_rows_and_titles_render() {
let el = panel(text("hi")).title("box");
assert_eq!(el.height(12), 3);
let lines = rendered(&el, 12);
assert!(lines[0].contains(" box "));
assert!(lines[1].contains("hi"));
assert!(lines[2].starts_with("└"));
}
#[test]
fn pad_x_narrows_content_and_offsets_it() {
let el = panel(text("hello world")).pad_x(1);
assert_eq!(el.height(10), 4);
let lines = rendered(&el, 10);
assert!(lines[1].contains("hello"));
assert!(lines[1].starts_with("│ h"));
}
#[test]
fn cursor_offsets_through_chrome() {
struct CursorAt;
impl Element for CursorAt {
fn height(&self, _w: u16) -> u16 {
1
}
fn render(&self, _a: Rect, _b: &mut Buffer) {}
fn cursor(&self, _a: Rect) -> Option<(u16, u16)> {
Some((3, 0))
}
}
let el = panel(CursorAt).pad_x(1);
let area = Rect::new(0, 0, 10, 3);
assert_eq!(el.cursor(area), Some((5, 1)));
}
}
#[cfg(test)]
mod cursor_guard_tests {
use super::*;
use ratatui_core::buffer::Buffer;
struct CursorAt;
impl Element for CursorAt {
fn height(&self, _w: u16) -> u16 {
1
}
fn render(&self, _a: Rect, _b: &mut Buffer) {}
fn cursor(&self, _a: Rect) -> Option<(u16, u16)> {
Some((0, 0))
}
}
#[test]
fn no_cursor_when_panel_cannot_render() {
let p = panel(CursorAt);
assert_eq!(p.cursor(Rect::new(0, 0, 1, 1)), None, "smaller than border");
assert_eq!(p.cursor(Rect::new(0, 0, 2, 2)), None, "no interior");
assert!(p.cursor(Rect::new(0, 0, 10, 3)).is_some());
}
#[test]
fn huge_padding_saturates_instead_of_panicking() {
let p = panel(CursorAt).pad_x(u16::MAX);
assert_eq!(p.cursor(Rect::new(0, 0, 10, 3)), None);
}
}