use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Padding},
};
use super::style;
use crate::theme::Skin;
const MODAL_BG_LIFT: f32 = 0.06;
pub fn panel() -> Block<'static> {
Block::default().padding(Padding::uniform(1))
}
pub fn menu_panel(skin: &Skin) -> Block<'static> {
panel().style(style::bg(skin.palette.panel))
}
pub fn modal_block(skin: &Skin, title: &str) -> Block<'static> {
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(style::border(&skin.palette))
.style(style::bg(skin.palette.background.lighten(MODAL_BG_LIFT)))
.title(border_title(
skin,
title,
style::accent(&skin.palette).add_modifier(Modifier::BOLD),
))
}
pub fn border_title(skin: &Skin, title: &str, label: Style) -> Line<'static> {
Line::from(vec![
border_title_lead(style::border(&skin.palette)),
Span::styled(format!("{} ", title.trim()), label),
])
}
#[must_use]
pub fn border_title_lead(border: Style) -> Span<'static> {
Span::styled("\u{2500} ", border)
}
#[derive(Debug, Clone, Default)]
pub enum Badge {
#[default]
Auto,
Text(String),
Hidden,
}
#[derive(Debug, Clone, Default)]
pub struct BoxDecor {
pub caption: Option<String>,
pub badge: Badge,
}
impl BoxDecor {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn caption(mut self, caption: impl Into<String>) -> Self {
self.caption = Some(caption.into());
self
}
#[must_use]
pub fn badge(mut self, text: impl Into<String>) -> Self {
self.badge = Badge::Text(text.into());
self
}
#[must_use]
pub fn no_badge(mut self) -> Self {
self.badge = Badge::Hidden;
self
}
fn badge_text<'a>(&'a self, auto: &'a str) -> Option<&'a str> {
match &self.badge {
Badge::Auto => (!auto.is_empty()).then_some(auto),
Badge::Text(text) => Some(text.as_str()),
Badge::Hidden => None,
}
}
}
pub fn framed_decor(
frame: &mut Frame,
area: Rect,
skin: &Skin,
decor: &BoxDecor,
auto_badge: &str,
) -> Rect {
let mut block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(style::border(&skin.palette))
.padding(Padding::horizontal(1));
if let Some(caption) = &decor.caption {
let label = style::accent(&skin.palette).add_modifier(Modifier::BOLD);
block = block.title(border_title(skin, caption, label));
}
let badge = decor.badge_text(auto_badge).unwrap_or_default();
let inner = block.inner(area);
frame.render_widget(block, area);
render_badge(frame, area, skin, badge);
inner
}
pub fn position_badge(selected: usize, total: usize) -> String {
if total == 0 {
return String::new();
}
format!("{}/{}", selected + 1, total)
}
pub fn render_badge(frame: &mut Frame, area: Rect, skin: &Skin, text: &str) {
if text.trim().is_empty() || area.height == 0 {
return;
}
let line = badge_line(skin, text);
let width = line.width() as u16;
if width + 2 > area.width {
return;
}
let x = area.right() - 1 - width;
let y = area.bottom() - 1;
frame.buffer_mut().set_line(x, y, &line, width);
}
pub fn render_corner_badge(
frame: &mut Frame,
area: Rect,
skin: &Skin,
text: &str,
) {
if text.trim().is_empty() || area.height == 0 {
return;
}
let line = Line::from(Span::styled(
text.trim().to_string(),
style::muted(&skin.palette),
));
let width = line.width() as u16;
if width > area.width {
return;
}
let x = area.right() - width;
let y = area.bottom() - 1;
frame.buffer_mut().set_line(x, y, &line, width);
}
fn badge_line(skin: &Skin, text: &str) -> Line<'static> {
let border = style::border(&skin.palette);
Line::from(vec![
Span::styled("\u{2500} ", border),
Span::styled(format!("{} ", text.trim()), style::muted(&skin.palette)),
Span::styled("\u{2500}", border),
])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn position_badge_is_one_based() {
assert_eq!(position_badge(0, 12), "1/12");
assert_eq!(position_badge(11, 12), "12/12");
}
#[test]
fn position_badge_is_empty_without_items() {
assert!(position_badge(0, 0).is_empty());
}
}