use crate::layout::{LayoutDisplay, Renderable, SizeHint};
use crate::terminal::{terminal_width, truncate_to_visual_width, visual_line_width};
use core::fmt;
pub struct Panel<'a, T: Renderable> {
pub content: &'a T,
pub title: Option<&'a str>,
}
impl<'a, T: Renderable> Panel<'a, T> {
pub fn new(content: &'a T) -> Self {
Self {
content,
title: None,
}
}
pub fn title(mut self, title: &'a str) -> Self {
self.title = Some(title);
self
}
}
impl<'a, T: Renderable> fmt::Display for Panel<'a, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let display = LayoutDisplay {
layout: self,
width: terminal_width(),
};
fmt::Display::fmt(&display, f)
}
}
impl<'a, T: Renderable> Renderable for Panel<'a, T> {
fn measure(&self, max_width: usize) -> SizeHint {
let inner_hint = self.content.measure(max_width.saturating_sub(2));
SizeHint {
min: (inner_hint.min + 2).min(max_width),
max: inner_hint.max.map(|m| (m + 2).min(max_width)),
}
}
fn total_rows(&self, width: usize) -> usize {
self.content.total_rows(width.saturating_sub(2)) + 2
}
fn row_width(&self, _row_idx: usize, width: usize) -> usize {
width
}
fn render_row(&self, row_idx: usize, width: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let content_width = width.saturating_sub(2);
let total_lines = self.total_rows(width);
if row_idx == 0 {
if let Some(title) = self.title {
let max_title_len = content_width.saturating_sub(2);
let truncated_title = truncate_to_visual_width(title, max_title_len);
let truncated_title_width = visual_line_width(truncated_title);
f.write_str("┌─")?;
f.write_str(truncated_title)?;
f.write_str("─")?;
let remaining = content_width.saturating_sub(truncated_title_width + 2);
for _ in 0..remaining {
f.write_str("─")?;
}
f.write_str("┐")?;
} else {
f.write_str("┌")?;
for _ in 0..content_width {
f.write_str("─")?;
}
f.write_str("┐")?;
}
} else if row_idx == total_lines - 1 {
f.write_str("└")?;
for _ in 0..content_width {
f.write_str("─")?;
}
f.write_str("┘")?;
} else {
f.write_str("│")?;
let inner_row_idx = row_idx - 1;
self.content.render_row(inner_row_idx, content_width, f)?;
let child_row_width = self.content.row_width(inner_row_idx, content_width);
let padding = content_width.saturating_sub(child_row_width);
for _ in 0..padding {
f.write_str(" ")?;
}
f.write_str("│")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Text;
struct RowRenderer<'a, T: Renderable>(&'a T, usize, usize);
impl<'a, T: Renderable> fmt::Display for RowRenderer<'a, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.render_row(self.1, self.2, f)
}
}
#[test]
fn test_panel_new_defaults() {
let content = Text::new("hello");
let panel = Panel::new(&content);
assert!(panel.title.is_none());
}
#[test]
fn test_panel_fluent_title() {
let content = Text::new("hello");
let panel = Panel::new(&content).title("My Title");
assert_eq!(panel.title, Some("My Title"));
}
#[test]
fn test_panel_geometry_measure() {
let content = Text::new("1234567890");
let panel = Panel::new(&content);
let hint = panel.measure(40);
assert_eq!(hint.min, 12);
assert_eq!(hint.max, Some(12));
let clamped_hint = panel.measure(8);
assert_eq!(clamped_hint.min, 8);
assert_eq!(clamped_hint.max, Some(8));
}
#[test]
fn test_panel_total_rows() {
let content = Text::new("Line 1\nLine 2\nLine 3");
let panel = Panel::new(&content);
assert_eq!(panel.total_rows(20), 5);
}
#[test]
fn test_panel_row_width() {
let content = Text::new("A");
let panel = Panel::new(&content);
assert_eq!(panel.row_width(0, 40), 40);
assert_eq!(panel.row_width(1, 40), 40);
}
#[test]
fn test_panel_render_borders_without_title() {
let content = Text::new("OK");
let panel = Panel::new(&content);
let width = 10;
let top = format!("{}", RowRenderer(&panel, 0, width));
assert_eq!(top, "┌────────┐");
let middle = format!("{}", RowRenderer(&panel, 1, width));
assert_eq!(middle, "│OK │");
let bottom = format!("{}", RowRenderer(&panel, 2, width));
assert_eq!(bottom, "└────────┘");
}
#[test]
fn test_panel_render_borders_with_title() {
let content = Text::new("Go");
let panel = Panel::new(&content).title("App");
let width = 12;
let top = format!("{}", RowRenderer(&panel, 0, width));
assert_eq!(top, "┌─App──────┐");
let middle = format!("{}", RowRenderer(&panel, 1, width));
assert_eq!(middle, "│Go │");
}
#[test]
fn test_panel_render_handling_asymmetrical_content_padding() {
let content = Text::new("LongerLine\nShort");
let panel = Panel::new(&content);
let width = 16;
let middle_1 = format!("{}", RowRenderer(&panel, 1, width));
assert_eq!(middle_1, "│LongerLine │");
let middle_2 = format!("{}", RowRenderer(&panel, 2, width));
assert_eq!(middle_2, "│Short │");
}
#[test]
fn test_panel_layout_display_lazy_integration() {
let content = Text::new("Hi");
let panel = Panel::new(&content).title("Box");
let display = LayoutDisplay {
layout: &panel,
width: 8, };
let output = format!("{}", display);
let expected = "┌─Box──┐\n\
│Hi │\n\
└──────┘";
assert_eq!(output, expected);
}
#[test]
fn test_panel_layout_for_nested_panels() {
let content = Text::new("HELLO");
let panel1 = Panel::new(&content);
let panel2 = Panel::new(&panel1);
let display = LayoutDisplay {
layout: &panel2,
width: 10,
};
let output = format!("{}", display);
let expected = "┌────────┐\n\
│┌──────┐│\n\
││HELLO ││\n\
│└──────┘│\n\
└────────┘";
assert_eq!(output, expected);
}
#[test]
fn test_panel_title_truncates_before_emoji_when_it_does_not_fit() {
let content = Text::new("OK");
let panel = Panel::new(&content).title("Build 🚀 Done");
let width = 10;
let top = format!("{}", RowRenderer(&panel, 0, width));
assert_eq!(top, "┌─Build ─┐");
assert_eq!(visual_line_width(&top), width);
}
#[test]
fn test_panel_title_keeps_emoji_when_it_fits() {
let content = Text::new("OK");
let panel = Panel::new(&content).title("Go 🚀");
let width = 9;
let top = format!("{}", RowRenderer(&panel, 0, width));
assert_eq!(top, "┌─Go 🚀─┐");
assert_eq!(visual_line_width(&top), width);
}
#[test]
fn test_panel_title_truncates_before_cjk_when_it_does_not_fit() {
let content = Text::new("OK");
let panel = Panel::new(&content).title("Title 华文");
let width = 11;
let top = format!("{}", RowRenderer(&panel, 0, width));
assert_eq!(top, "┌─Title ──┐");
assert_eq!(visual_line_width(&top), width);
}
#[test]
fn test_panel_title_keeps_cjk_when_it_fits() {
let content = Text::new("OK");
let panel = Panel::new(&content).title("Hi 华");
let width = 10;
let top = format!("{}", RowRenderer(&panel, 0, width));
assert_eq!(top, "┌─Hi 华──┐");
assert_eq!(visual_line_width(&top), width);
}
}