use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
use crate::studio::state::ThemeOptionInfo;
use crate::theme;
use crate::theme::names::{gradients, tokens};
pub fn render(
frame: &mut Frame,
area: Rect,
input: &str,
themes: &[ThemeOptionInfo],
selected: usize,
scroll: usize,
) {
let t = theme::current();
let block = Block::default()
.title(" Select Theme ")
.title_style(
Style::default()
.fg(Color::from(t.color(tokens::TEXT_PRIMARY)))
.add_modifier(Modifier::BOLD),
)
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::from(t.color(tokens::ACCENT_PRIMARY))));
let inner = block.inner(area);
frame.render_widget(block, area);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
.split(inner);
render_theme_list(frame, chunks[0], input, themes, selected, scroll);
render_theme_preview(frame, chunks[1], themes, selected);
}
fn render_theme_list(
frame: &mut Frame,
area: Rect,
input: &str,
themes: &[ThemeOptionInfo],
selected: usize,
scroll: usize,
) {
let t = theme::current();
let filtered: Vec<(usize, &ThemeOptionInfo)> = themes
.iter()
.enumerate()
.filter(|(_, theme)| {
input.is_empty()
|| theme
.display_name
.to_lowercase()
.contains(&input.to_lowercase())
|| theme.author.to_lowercase().contains(&input.to_lowercase())
})
.collect();
let visible_height = area.height.saturating_sub(5) as usize;
let mut lines = vec![
Line::from(vec![
Span::styled(
" Filter: ",
Style::default().fg(Color::from(t.color(tokens::TEXT_MUTED))),
),
Span::styled(
input,
Style::default().fg(Color::from(t.color(tokens::TEXT_PRIMARY))),
),
Span::styled(
if input.is_empty() { "│" } else { "█" },
Style::default().fg(Color::from(t.color(tokens::ACCENT_SECONDARY))),
),
]),
Line::from(""),
];
let mut current_variant: Option<&str> = None;
for (display_idx, (original_idx, theme)) in filtered
.iter()
.enumerate()
.skip(scroll)
.take(visible_height)
{
if current_variant != Some(&theme.variant) {
if current_variant.is_some() && display_idx > scroll {
lines.push(Line::from(""));
}
let variant_label = if theme.variant == "dark" {
" Dark Themes"
} else {
" Light Themes"
};
lines.push(Line::from(Span::styled(
variant_label,
Style::default()
.fg(Color::from(t.color(tokens::TEXT_DIM)))
.add_modifier(Modifier::ITALIC),
)));
current_variant = Some(&theme.variant);
}
let is_selected = *original_idx == selected;
let prefix = if is_selected { " > " } else { " " };
let name_style = if is_selected {
Style::default()
.fg(Color::from(t.color(tokens::ACCENT_SECONDARY)))
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::from(t.color(tokens::TEXT_PRIMARY)))
};
let variant_icon = if theme.variant == "light" { " ☀" } else { "" };
lines.push(Line::from(vec![
Span::styled(prefix, name_style),
Span::styled(&theme.display_name, name_style),
Span::styled(
variant_icon,
Style::default().fg(Color::from(t.color(tokens::WARNING))),
),
]));
}
if filtered.is_empty() {
lines.push(Line::from(Span::styled(
" No matching themes",
Style::default().fg(Color::from(t.color(tokens::TEXT_MUTED))),
)));
}
lines.push(Line::from(""));
let scroll_hint = if filtered.len() > visible_height {
format!(" ({}/{})", selected + 1, themes.len())
} else {
String::new()
};
lines.push(Line::from(vec![
Span::styled(
" ↑↓",
Style::default().fg(Color::from(t.color(tokens::ACCENT_PRIMARY))),
),
Span::styled(
" nav ",
Style::default().fg(Color::from(t.color(tokens::TEXT_MUTED))),
),
Span::styled(
"Enter",
Style::default().fg(Color::from(t.color(tokens::ACCENT_PRIMARY))),
),
Span::styled(
" select ",
Style::default().fg(Color::from(t.color(tokens::TEXT_MUTED))),
),
Span::styled(
"Esc",
Style::default().fg(Color::from(t.color(tokens::WARNING))),
),
Span::styled(
" cancel",
Style::default().fg(Color::from(t.color(tokens::TEXT_MUTED))),
),
Span::styled(
scroll_hint,
Style::default().fg(Color::from(t.color(tokens::TEXT_DIM))),
),
]));
frame.render_widget(Paragraph::new(lines), area);
}
#[allow(clippy::cast_precision_loss)]
fn render_theme_preview(
frame: &mut Frame,
area: Rect,
themes: &[ThemeOptionInfo],
selected: usize,
) {
let t = theme::current();
let Some(theme_info) = themes.get(selected) else {
return;
};
let block = Block::default()
.borders(Borders::LEFT)
.border_style(Style::default().fg(Color::from(t.color(tokens::TEXT_DIM))));
let inner = block.inner(area);
frame.render_widget(block, area);
let mut lines = vec![
Line::from(Span::styled(
format!(" {}", theme_info.display_name),
Style::default()
.fg(Color::from(t.color(tokens::ACCENT_PRIMARY)))
.add_modifier(Modifier::BOLD),
)),
Line::from(vec![
Span::styled(
" by ",
Style::default().fg(Color::from(t.color(tokens::TEXT_MUTED))),
),
Span::styled(
&theme_info.author,
Style::default().fg(Color::from(t.color(tokens::TEXT_SECONDARY))),
),
]),
Line::from(""),
Line::from(Span::styled(
format!(" {}", theme_info.description),
Style::default().fg(Color::from(t.color(tokens::TEXT_MUTED))),
)),
Line::from(""),
Line::from(vec![
Span::styled(
" Variant: ",
Style::default().fg(Color::from(t.color(tokens::TEXT_DIM))),
),
Span::styled(
if theme_info.variant == "light" {
"Light ☀"
} else {
"Dark"
},
Style::default().fg(Color::from(t.color(tokens::TEXT_SECONDARY))),
),
]),
Line::from(""),
Line::from(Span::styled(
" Preview",
Style::default()
.fg(Color::from(t.color(tokens::TEXT_DIM)))
.add_modifier(Modifier::ITALIC),
)),
];
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(
"██",
Style::default().fg(Color::from(t.color(tokens::ACCENT_PRIMARY))),
),
Span::raw(" "),
Span::styled(
"██",
Style::default().fg(Color::from(t.color(tokens::ACCENT_SECONDARY))),
),
Span::raw(" "),
Span::styled(
"██",
Style::default().fg(Color::from(t.color(tokens::ACCENT_TERTIARY))),
),
Span::raw(" "),
Span::styled(
"██",
Style::default().fg(Color::from(t.color(tokens::SUCCESS))),
),
Span::raw(" "),
Span::styled(
"██",
Style::default().fg(Color::from(t.color(tokens::WARNING))),
),
Span::raw(" "),
Span::styled(
"██",
Style::default().fg(Color::from(t.color(tokens::ERROR))),
),
]));
lines.push(Line::from(""));
let gradient_width = 18;
let mut gradient_spans = vec![Span::styled(" ", Style::default())];
for i in 0..gradient_width {
let t_pos = i as f32 / (gradient_width - 1) as f32;
let color = Color::from(t.gradient(gradients::PRIMARY, t_pos));
gradient_spans.push(Span::styled("▀", Style::default().fg(color)));
}
lines.push(Line::from(gradient_spans));
frame.render_widget(Paragraph::new(lines), inner);
}