use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Modifier, Style},
widgets::{Block, BorderType, Widget},
};
use crate::layout::LayoutMode;
pub trait PanelStyleProvider {
fn default_style(&self) -> Style;
fn accent_style(&self) -> Style;
fn border_style(&self) -> Style;
}
pub struct Panel<'a, S: PanelStyleProvider> {
styles: &'a S,
title: Option<&'a str>,
active: bool,
mode: LayoutMode,
border_type: Option<BorderType>,
}
impl<'a, S: PanelStyleProvider> Panel<'a, S> {
pub fn new(styles: &'a S) -> Self {
Self {
styles,
title: None,
active: false,
mode: LayoutMode::Standard,
border_type: None,
}
}
#[must_use]
pub fn title(mut self, title: &'a str) -> Self {
self.title = Some(title);
self
}
#[must_use]
pub fn active(mut self, active: bool) -> Self {
self.active = active;
self
}
#[must_use]
pub fn mode(mut self, mode: LayoutMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub fn border_type(mut self, border_type: BorderType) -> Self {
self.border_type = Some(border_type);
self
}
pub fn render_and_get_inner(self, area: Rect, buf: &mut Buffer) -> Rect {
if !self.mode.show_borders() {
return area;
}
let border_style = if self.active {
self.styles.accent_style()
} else {
self.styles.border_style()
};
let border_type = self.border_type.unwrap_or(BorderType::Plain);
let mut block = Block::bordered()
.border_type(border_type)
.style(self.styles.default_style())
.border_style(border_style);
if self.mode.show_titles()
&& let Some(title) = self.title
{
let title_style = if self.active {
self.styles.accent_style().add_modifier(Modifier::BOLD)
} else {
self.styles.default_style().add_modifier(Modifier::BOLD)
};
block = block.title(title).title_style(title_style);
}
let inner = block.inner(area);
block.render(area, buf);
inner
}
}
pub trait PanelStyles {
fn muted_style(&self) -> Style;
fn title_style(&self) -> Style;
fn border_active_style(&self) -> Style;
fn divider_style(&self) -> Style;
}
#[cfg(test)]
mod tests {
use super::*;
struct MockStyles {
default: Style,
accent: Style,
border: Style,
}
impl MockStyles {
fn new() -> Self {
Self {
default: Style::default(),
accent: Style::default().fg(ratatui::style::Color::Cyan),
border: Style::default().fg(ratatui::style::Color::Gray),
}
}
}
impl PanelStyleProvider for MockStyles {
fn default_style(&self) -> Style {
self.default
}
fn accent_style(&self) -> Style {
self.accent
}
fn border_style(&self) -> Style {
self.border
}
}
#[test]
fn compact_mode_returns_full_area() {
let styles = MockStyles::new();
let area = Rect::new(0, 0, 40, 10);
let mut buf = Buffer::empty(area);
let inner = Panel::new(&styles)
.mode(LayoutMode::Compact)
.render_and_get_inner(area, &mut buf);
assert_eq!(inner, area);
}
#[test]
fn standard_mode_returns_smaller_inner_area() {
let styles = MockStyles::new();
let area = Rect::new(0, 0, 80, 24);
let mut buf = Buffer::empty(area);
let inner = Panel::new(&styles)
.mode(LayoutMode::Standard)
.render_and_get_inner(area, &mut buf);
assert_eq!(inner.x, area.x + 1);
assert_eq!(inner.y, area.y + 1);
assert_eq!(inner.width, area.width - 2);
assert_eq!(inner.height, area.height - 2);
}
#[test]
fn wide_mode_with_title() {
let styles = MockStyles::new();
let area = Rect::new(0, 0, 120, 30);
let mut buf = Buffer::empty(area);
let inner = Panel::new(&styles)
.title("Test Panel")
.active(true)
.mode(LayoutMode::Wide)
.render_and_get_inner(area, &mut buf);
assert_eq!(inner.x, area.x + 1);
assert_eq!(inner.y, area.y + 1);
assert_eq!(inner.width, area.width - 2);
assert_eq!(inner.height, area.height - 2);
}
#[test]
fn active_panel_uses_accent_style() {
let styles = MockStyles::new();
let area = Rect::new(0, 0, 80, 24);
let mut buf = Buffer::empty(area);
Panel::new(&styles)
.active(true)
.mode(LayoutMode::Standard)
.render_and_get_inner(area, &mut buf);
}
}