use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
#[derive(Default)]
pub struct Card<'a> {
pub eyebrow: Option<&'a str>, pub title: Option<&'a str>, pub body: Vec<Line<'a>>, pub footer: Option<Line<'a>>, pub error: Option<&'a str>, pub step: Option<&'a str>, pub max_width: u16, }
impl<'a> Card<'a> {
pub fn new() -> Self {
Self {
max_width: 76,
..Default::default()
}
}
}
pub fn centered_card_rect(area: Rect, width: u16, height: u16) -> Rect {
let width = width.min(area.width.saturating_sub(4));
let height = height.min(area.height.saturating_sub(4));
let x = area.x + (area.width.saturating_sub(width)) / 2;
let y = area.y + (area.height.saturating_sub(height)) / 2;
Rect {
x,
y,
width,
height,
}
}
pub fn render(frame: &mut Frame, area: Rect, card: &Card<'_>) {
let mut content_rows: u16 = 2; if card.eyebrow.is_some() {
content_rows += 1;
}
if card.title.is_some() {
content_rows += 2; }
content_rows += card.body.len() as u16;
if card.footer.is_some() {
content_rows += 2;
}
if card.error.is_some() {
content_rows += 2;
}
let total_height = content_rows + 2;
let outer = centered_card_rect(area, card.max_width, total_height);
frame.render_widget(Clear, outer);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray));
let inner = block.inner(outer);
frame.render_widget(block, outer);
let inner = Rect {
x: inner.x + 2,
y: inner.y + 1,
width: inner.width.saturating_sub(4),
height: inner.height.saturating_sub(2),
};
let mut lines: Vec<Line> = Vec::new();
if let Some(eb) = card.eyebrow {
lines.push(Line::from(Span::styled(
eb.to_string(),
Style::default().fg(Color::DarkGray),
)));
}
if let Some(t) = card.title {
lines.push(Line::from(Span::styled(
t.to_string(),
Style::default()
.fg(Color::Rgb(150, 130, 255))
.add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(""));
}
for body_line in &card.body {
lines.push(body_line.clone());
}
if let Some(footer) = &card.footer {
lines.push(Line::from(""));
lines.push(footer.clone());
}
if let Some(err) = card.error {
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
format!("! {}", err),
Style::default().fg(Color::Red),
)));
}
let para = Paragraph::new(lines);
frame.render_widget(para, inner);
if let Some(step) = card.step {
let step_width = step.len() as u16;
if outer.width > step_width + 2 && outer.height > 1 {
let rect = Rect {
x: outer.x + outer.width - step_width - 2,
y: outer.y + outer.height - 1,
width: step_width,
height: 1,
};
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
step.to_string(),
Style::default().fg(Color::DarkGray),
))),
rect,
);
}
}
}
pub fn selectable_row<'a>(
badge: &'a str,
title: &'a str,
subtitle: &'a str,
selected: bool,
) -> Vec<Line<'a>> {
let (title_style, sub_style, bg) = if selected {
(
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
Style::default().fg(Color::Gray),
Style::default().bg(Color::Rgb(40, 30, 80)),
)
} else {
(
Style::default().fg(Color::Gray),
Style::default().fg(Color::DarkGray),
Style::default(),
)
};
vec![
Line::from(vec![
Span::styled(
format!(" {} ", badge),
Style::default()
.fg(Color::White)
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(title.to_string(), title_style),
])
.style(bg),
Line::from(vec![
Span::raw(" "),
Span::styled(subtitle.to_string(), sub_style),
])
.style(bg),
]
}