use crate::theme::ThemeManager;
use tuirealm::ratatui::{
Frame,
layout::{Alignment, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph, Wrap},
};
pub struct PopupStyle {
pub border_color: Color,
pub title_color: Color,
pub text_color: Color,
pub muted_color: Color,
}
impl Default for PopupStyle {
fn default() -> Self {
Self {
border_color: ThemeManager::primary_accent(),
title_color: ThemeManager::title_accent(),
text_color: ThemeManager::text_primary(),
muted_color: ThemeManager::text_muted(),
}
}
}
impl PopupStyle {
pub fn error() -> Self {
Self {
border_color: ThemeManager::status_error(),
title_color: ThemeManager::status_error(),
..Self::default()
}
}
pub fn success() -> Self {
Self {
border_color: ThemeManager::status_success(),
title_color: ThemeManager::status_success(),
..Self::default()
}
}
}
pub struct PopupBuilder {
title: String,
style: PopupStyle,
content_lines: Vec<Line<'static>>,
instructions: Option<String>,
}
impl PopupBuilder {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
style: PopupStyle::default(),
content_lines: Vec::new(),
instructions: None,
}
}
pub fn error(title: impl Into<String>) -> Self {
Self {
title: title.into(),
style: PopupStyle::error(),
content_lines: Vec::new(),
instructions: None,
}
}
pub fn success(title: impl Into<String>) -> Self {
Self {
title: title.into(),
style: PopupStyle::success(),
content_lines: Vec::new(),
instructions: None,
}
}
pub fn add_text(mut self, text: impl Into<String>) -> Self {
self.content_lines.push(Line::from(text.into()));
self
}
pub fn add_error_text(mut self, text: impl Into<String>) -> Self {
self.content_lines.push(Line::from(Span::styled(
text.into(),
Style::default().fg(ThemeManager::status_error()),
)));
self
}
pub fn add_empty_line(mut self) -> Self {
self.content_lines.push(Line::from(""));
self
}
pub fn add_line(mut self, spans: Vec<Span<'static>>) -> Self {
self.content_lines.push(Line::from(spans));
self
}
pub fn add_multiline_text(mut self, text: impl Into<String>) -> Self {
for line in text.into().lines() {
self.content_lines.push(Line::from(line.to_string()));
}
self.content_lines.push(Line::from(""));
self
}
pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
self.instructions = Some(instructions.into());
self
}
pub fn with_confirmation_instructions(mut self, yes_key: &str, no_key: &str) -> Self {
let instruction_line = Line::from(vec![
Span::styled(
format!("[{}]", yes_key.to_uppercase()),
Style::default()
.fg(ThemeManager::status_success())
.add_modifier(Modifier::BOLD),
),
Span::raw(" Yes "),
Span::styled(
format!("[{}]", no_key.to_uppercase()),
Style::default()
.fg(ThemeManager::status_error())
.add_modifier(Modifier::BOLD),
),
Span::raw(" No"),
]);
self.content_lines.push(Line::from(""));
self.content_lines.push(Line::from(""));
self.content_lines.push(instruction_line);
self
}
pub fn create_block_with_title(self, title: impl Into<String>) -> Block<'static> {
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(self.style.border_color))
.title(title.into())
.title_alignment(Alignment::Center)
.title_style(
Style::default()
.fg(self.style.title_color)
.add_modifier(Modifier::BOLD),
)
}
pub fn create_conditional_block(
self,
title: impl Into<String>,
is_focused: bool,
focused_color: Color,
unfocused_color: Color,
) -> Block<'static> {
let border_color = if is_focused {
focused_color
} else {
unfocused_color
};
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(border_color))
.title(title.into())
.title_alignment(Alignment::Center)
.title_style(
Style::default()
.fg(self.style.title_color)
.add_modifier(Modifier::BOLD),
)
}
pub fn render(self, frame: &mut Frame, area: Rect) {
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(self.style.border_color))
.title(format!(" {} ", self.title))
.title_alignment(Alignment::Center)
.title_style(
Style::default()
.fg(self.style.title_color)
.add_modifier(Modifier::BOLD),
);
let mut all_lines = Vec::new();
all_lines.push(Line::from(""));
all_lines.push(Line::from(""));
all_lines.extend(self.content_lines);
if let Some(instructions) = self.instructions {
all_lines.push(Line::from(""));
all_lines.push(Line::from(Span::styled(
instructions,
Style::default().fg(self.style.muted_color),
)));
}
let paragraph = Paragraph::new(all_lines)
.block(block)
.style(Style::default().fg(self.style.text_color))
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
frame.render_widget(paragraph, area);
}
}
pub struct PopupLayout;
impl PopupLayout {
pub fn centered(area: Rect, width_percent: u16, height_percent: u16) -> Rect {
let popup_width = (area.width * width_percent) / 100;
let popup_height = (area.height * height_percent) / 100;
let x = (area.width.saturating_sub(popup_width)) / 2;
let y = (area.height.saturating_sub(popup_height)) / 2;
Rect {
x: area.x + x,
y: area.y + y,
width: popup_width,
height: popup_height,
}
}
pub fn small(area: Rect) -> Rect {
Self::centered(area, 40, 30)
}
pub fn medium(area: Rect) -> Rect {
Self::centered(area, 60, 50)
}
pub fn large(area: Rect) -> Rect {
Self::centered(area, 80, 70)
}
pub fn extra_wide(area: Rect) -> Rect {
Self::centered(area, 90, 60)
}
}