use ratatui::{
Frame,
layout::{Alignment, Constraint, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Clear, Paragraph}
};
use rust_i18n::t;
use crate::tui::{app::App, themes::Palette};
pub(super) fn render_create_form(
frame: &mut Frame,
area: Rect,
form: &crate::tui::app::CreateForm,
palette: &Palette
) {
let width = 56u16.min(area.width.saturating_sub(4));
let height = u16::try_from(form.fields.len()).unwrap_or(2) + 4;
let popup = Rect {
x: (area.width.saturating_sub(width)) / 2,
y: (area.height.saturating_sub(height)) / 2,
width,
height
};
frame.render_widget(Clear, popup);
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(palette.accent))
.title(Line::from(Span::styled(
format!(" {} ", form.title),
Style::default()
.fg(palette.title)
.add_modifier(Modifier::BOLD)
)));
let inner = block.inner(popup);
frame.render_widget(block, popup);
let mut constraints: Vec<Constraint> =
form.fields.iter().map(|_| Constraint::Length(1)).collect();
constraints.push(Constraint::Length(1));
let rows = Layout::vertical(constraints).split(inner);
for (i, field) in form.fields.iter().enumerate() {
let focused = i == form.active;
let marker = if focused { "▸ " } else { " " };
let label = format!("{marker}{}: ", field.label);
let value_style = if focused {
Style::default()
.fg(palette.fg)
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
} else {
Style::default().fg(palette.dim)
};
let cursor = if focused { "█" } else { "" };
let line = Line::from(vec![
Span::styled(label, Style::default().fg(palette.header)),
Span::styled(format!("{}{cursor}", field.value), value_style),
]);
if let Some(row) = rows.get(i) {
frame.render_widget(Paragraph::new(line), *row);
}
}
if let Some(hint_row) = rows.last() {
let hint = Paragraph::new(Line::from(Span::styled(
"Tab next · Enter create · Esc cancel",
Style::default().fg(palette.dim)
)));
frame.render_widget(hint, *hint_row);
}
}
pub(super) fn render_confirm(frame: &mut Frame, area: Rect, app: &App, palette: &Palette) {
let Some(pending) = app.pending_action() else {
return;
};
let accent = if pending.kind.is_destructive() {
palette.error
} else {
palette.warning
};
let mut lines = vec![
Line::from(Span::styled(
t!(
"ui.confirm_prompt",
verb => pending.kind.display_label(),
name => pending.resource_name,
id => pending.resource_id
)
.to_string(),
Style::default().fg(palette.fg).add_modifier(Modifier::BOLD)
)),
Line::from(""),
];
if pending.kind.is_destructive() {
lines.push(Line::from(Span::styled(
t!("ui.confirm_irreversible").to_string(),
Style::default().fg(palette.error)
)));
lines.push(Line::from(""));
}
lines.push(Line::from(vec![
Span::styled(
" [y] ",
Style::default().fg(accent).add_modifier(Modifier::BOLD)
),
Span::styled(
format!("{} ", t!("ui.confirm_yes")),
Style::default().fg(palette.dim)
),
Span::styled(
"[n] ",
Style::default().fg(palette.fg).add_modifier(Modifier::BOLD)
),
Span::styled(
t!("ui.confirm_no").to_string(),
Style::default().fg(palette.dim)
),
]));
let width = 54u16.min(area.width.saturating_sub(4));
let height = u16::try_from(lines.len()).unwrap_or(4) + 2;
let popup = Rect {
x: area.width.saturating_sub(width) / 2,
y: area.height.saturating_sub(height) / 2,
width,
height
};
let paragraph = Paragraph::new(lines)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(accent))
.title(Line::from(Span::styled(
t!("ui.confirm_title").to_string(),
Style::default()
.fg(palette.title)
.add_modifier(Modifier::BOLD)
)))
)
.alignment(Alignment::Left)
.style(Style::default().bg(palette.bg));
frame.render_widget(Clear, popup);
frame.render_widget(paragraph, popup);
}
pub(super) fn render_settings_picker(frame: &mut Frame, area: Rect, app: &App, palette: &Palette) {
let Some(picker) = app.picker.as_ref() else {
return;
};
let longest_option = picker
.options
.iter()
.map(|o| o.chars().count())
.max()
.unwrap_or(10);
let is_theme = picker.row == crate::tui::app::SettingRow::Theme;
let lines: Vec<Line> = picker
.options
.iter()
.enumerate()
.map(|(idx, option)| {
let selected = idx == picker.selected;
let marker = if selected { "\u{25B6} " } else { " " };
let mut style =
Style::default().fg(if selected { palette.accent } else { palette.fg });
if selected {
style = style.add_modifier(Modifier::BOLD);
}
let mut spans = vec![
Span::styled(marker, Style::default().fg(palette.accent)),
Span::styled(format!("{option:<longest_option$}"), style),
];
if is_theme && let Some(theme) = crate::tui::themes::Theme::ALL.get(idx) {
let swatch = theme.palette();
spans.push(Span::raw(" "));
for color in [
swatch.bg,
swatch.fg,
swatch.accent,
swatch.success,
swatch.warning,
swatch.error,
swatch.dim
] {
spans.push(Span::styled("\u{2588}\u{2588}", Style::default().fg(color)));
}
}
Line::from(spans)
})
.collect();
let swatch_width = if is_theme { 16 } else { 0 };
let longest = longest_option.max(picker.title.chars().count()) + swatch_width;
let width = u16::try_from(longest + 8)
.unwrap_or(32)
.clamp(24, area.width.saturating_sub(4));
let height = u16::try_from(picker.options.len()).unwrap_or(4) + 2;
let popup = Rect {
x: area.width.saturating_sub(width) / 2,
y: area.height.saturating_sub(height) / 2,
width,
height
};
let paragraph = Paragraph::new(lines).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(palette.accent))
.title(Line::from(Span::styled(
format!(" {} ", picker.title),
Style::default()
.fg(palette.title)
.add_modifier(Modifier::BOLD)
)))
);
frame.render_widget(Clear, popup);
frame.render_widget(paragraph, popup);
}
pub(super) fn render_info_popup(frame: &mut Frame, area: Rect, app: &App, palette: &Palette) {
let Some((title, body)) = app.info_popup.as_ref() else {
return;
};
let width = 64u16.min(area.width.saturating_sub(4));
let wrap_cols = usize::from(width.saturating_sub(4)).max(10);
let body_rows =
u16::try_from(body.chars().count() / wrap_cols + body.matches('\n').count() + 2)
.unwrap_or(6);
let height = (body_rows + 4).min(area.height.saturating_sub(2));
let popup = Rect {
x: area.width.saturating_sub(width) / 2,
y: area.height.saturating_sub(height) / 2,
width,
height
};
let mut lines: Vec<Line> = body
.split('\n')
.map(|part| {
Line::from(Span::styled(
part.to_string(),
Style::default().fg(palette.fg)
))
})
.collect();
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
t!("create.popup_hint").into_owned(),
Style::default().fg(palette.dim)
)));
let paragraph = Paragraph::new(lines)
.wrap(ratatui::widgets::Wrap {
trim: true
})
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(palette.accent))
.title(Line::from(Span::styled(
format!(" {title} "),
Style::default()
.fg(palette.title)
.add_modifier(Modifier::BOLD)
)))
)
.style(Style::default().bg(palette.bg));
frame.render_widget(Clear, popup);
frame.render_widget(paragraph, popup);
}