use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Clear, Paragraph};
use super::to_color;
use crate::tui::theme::Theme;
pub(in crate::tui) const PANEL_PAD_X: u16 = 2;
pub(in crate::tui) const PANEL_PAD_Y: u16 = 1;
pub(in crate::tui) fn borderless_panel(
f: &mut ratatui::Frame<'_>,
area: Rect,
title: Option<&str>,
theme: &Theme,
) -> Rect {
f.render_widget(Clear, area);
let bg = Style::default().bg(to_color(theme.ui.tooltip_bg));
f.render_widget(Block::default().style(bg), area);
if area.width <= PANEL_PAD_X * 2 || area.height <= PANEL_PAD_Y * 2 {
return area;
}
let mut inner = Rect {
x: area.x + PANEL_PAD_X,
y: area.y + PANEL_PAD_Y,
width: area.width - PANEL_PAD_X * 2,
height: area.height - PANEL_PAD_Y * 2,
};
if let Some(t) = title {
if inner.height >= 1 {
f.render_widget(
Paragraph::new(Line::from(Span::styled(
t.to_string(),
Style::default()
.fg(to_color(theme.ui.neon_brand))
.add_modifier(Modifier::BOLD),
)))
.style(bg),
Rect {
x: inner.x,
y: inner.y,
width: inner.width,
height: 1,
},
);
inner.y += 1;
inner.height -= 1;
}
}
inner
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::backend::TestBackend;
use ratatui::Terminal;
fn render_to_string(w: u16, h: u16, title: Option<&str>) -> String {
let mut term = Terminal::new(TestBackend::new(w, h)).unwrap();
term.draw(|f| {
borderless_panel(f, Rect::new(0, 0, w, h), title, &crate::tui::theme::NORMAL);
})
.unwrap();
let buf = term.backend().buffer().clone();
let mut s = String::new();
for y in 0..h {
for x in 0..w {
if let Some(cell) = buf.cell((x, y)) {
s.push_str(cell.symbol());
}
}
s.push('\n');
}
s
}
#[test]
fn borderless_panel_has_no_border_glyphs_and_renders_title() {
let s = render_to_string(40, 8, Some("Connection"));
for g in ['╭', '╮', '╰', '╯', '│', '─', '┌', '┐', '└', '┘'] {
assert!(!s.contains(g), "panel must be borderless, found {g:?}");
}
assert!(s.contains("Connection"), "title must render in the body");
}
#[test]
fn borderless_panel_returns_padded_inner_below_the_title_row() {
let mut term = Terminal::new(TestBackend::new(20, 6)).unwrap();
let mut inner = Rect::default();
term.draw(|f| {
inner = borderless_panel(
f,
Rect::new(0, 0, 20, 6),
Some("X"),
&crate::tui::theme::NORMAL,
);
})
.unwrap();
assert_eq!(inner.x, PANEL_PAD_X);
assert_eq!(
inner.y,
PANEL_PAD_Y + 1,
"content starts below the title row"
);
assert_eq!(inner.width, 20 - PANEL_PAD_X * 2);
assert_eq!(inner.height, 6 - PANEL_PAD_Y * 2 - 1);
term.draw(|f| {
inner = borderless_panel(f, Rect::new(0, 0, 20, 6), None, &crate::tui::theme::NORMAL);
})
.unwrap();
assert_eq!(inner.y, PANEL_PAD_Y);
assert_eq!(inner.height, 6 - PANEL_PAD_Y * 2);
}
#[test]
fn borderless_panel_never_panics_across_sizes() {
for (w, h) in [(80, 20), (40, 8), (10, 3), (4, 2), (2, 1)] {
let _ = render_to_string(w, h, Some("T"));
let _ = render_to_string(w, h, None);
}
}
}