feat(tui): improve popup layout and rendering for model selector and theme browser
- Add robust size calculations with configurable width bounds and height clamping. - Guard against zero‑size areas and empty model/theme lists. - Render popups centered with dynamic positioning, preventing negative Y coordinates. - Introduce multi‑line list items, badges, and metadata display for models. - Add ellipsis helper for long descriptions and separate title/metadata generation. - Refactor theme selector to show current theme, built‑in/custom indicators, and a centered footer. - Update highlight styles and selection handling for both popups.
This commit is contained in:
@@ -2655,81 +2655,183 @@ fn model_has_feature(model: &ModelInfo, keywords: &[&str]) -> bool {
|
|||||||
|
|
||||||
fn render_model_selector(frame: &mut Frame<'_>, app: &ChatApp) {
|
fn render_model_selector(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||||
let theme = app.theme();
|
let theme = app.theme();
|
||||||
let area = centered_rect(60, 60, frame.area());
|
let area = frame.area();
|
||||||
frame.render_widget(Clear, area);
|
if area.width == 0 || area.height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let items: Vec<ListItem> = app
|
let selector_items = app.model_selector_items();
|
||||||
.model_selector_items()
|
if selector_items.is_empty() {
|
||||||
.iter()
|
return;
|
||||||
.map(|item| match item.kind() {
|
}
|
||||||
|
|
||||||
|
let max_width: u16 = 80;
|
||||||
|
let min_width: u16 = 50;
|
||||||
|
let mut width = area.width.min(max_width);
|
||||||
|
if area.width >= min_width {
|
||||||
|
width = width.max(min_width);
|
||||||
|
}
|
||||||
|
width = width.max(1);
|
||||||
|
|
||||||
|
let mut height = (selector_items.len().clamp(1, 10) as u16) * 3 + 6;
|
||||||
|
height = height.clamp(6, area.height);
|
||||||
|
|
||||||
|
let x = area.x + (area.width.saturating_sub(width)) / 2;
|
||||||
|
let mut y = area.y + (area.height.saturating_sub(height)) / 3;
|
||||||
|
if y < area.y {
|
||||||
|
y = area.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let popup_area = Rect::new(x, y, width, height);
|
||||||
|
frame.render_widget(Clear, popup_area);
|
||||||
|
|
||||||
|
let title_line = Line::from(vec![
|
||||||
|
Span::styled(
|
||||||
|
" Model Selector ",
|
||||||
|
Style::default().fg(theme.info).add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
format!("· Provider: {}", app.selected_provider),
|
||||||
|
Style::default()
|
||||||
|
.fg(theme.placeholder)
|
||||||
|
.add_modifier(Modifier::DIM),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.title(title_line)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(theme.info))
|
||||||
|
.style(Style::default().bg(theme.background).fg(theme.text));
|
||||||
|
|
||||||
|
let inner = block.inner(popup_area);
|
||||||
|
frame.render_widget(block, popup_area);
|
||||||
|
if inner.width == 0 || inner.height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(4), Constraint::Length(2)])
|
||||||
|
.split(inner);
|
||||||
|
|
||||||
|
let active_model_id = app.selected_model();
|
||||||
|
|
||||||
|
let mut items: Vec<ListItem> = Vec::new();
|
||||||
|
for item in selector_items.iter() {
|
||||||
|
match item.kind() {
|
||||||
ModelSelectorItemKind::Header { provider, expanded } => {
|
ModelSelectorItemKind::Header { provider, expanded } => {
|
||||||
let marker = if *expanded { "▼" } else { "▶" };
|
let marker = if *expanded { "▼" } else { "▶" };
|
||||||
let label = format!("{} {}", marker, provider);
|
let lines = vec![Line::from(vec![
|
||||||
ListItem::new(Span::styled(
|
Span::styled(
|
||||||
label,
|
marker,
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(theme.focused_panel_border)
|
.fg(theme.placeholder)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
))
|
),
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::styled(
|
||||||
|
provider.clone(),
|
||||||
|
Style::default()
|
||||||
|
.fg(theme.mode_command)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
])];
|
||||||
|
items.push(ListItem::new(lines).style(Style::default().bg(theme.background)));
|
||||||
}
|
}
|
||||||
ModelSelectorItemKind::Model { model_index, .. } => {
|
ModelSelectorItemKind::Model { model_index, .. } => {
|
||||||
|
let mut lines = Vec::new();
|
||||||
if let Some(model) = app.model_info_by_index(*model_index) {
|
if let Some(model) = app.model_info_by_index(*model_index) {
|
||||||
let badges = model_badge_icons(model);
|
let badges = model_badge_icons(model);
|
||||||
let detail = app.cached_model_detail(&model.id);
|
let detail = app.cached_model_detail(&model.id);
|
||||||
let label = build_model_selector_label(model, detail, &badges);
|
let (title, metadata) = build_model_selector_label(
|
||||||
ListItem::new(Span::styled(
|
model,
|
||||||
label,
|
detail,
|
||||||
Style::default()
|
&badges,
|
||||||
.fg(theme.user_message_role)
|
model.id == active_model_id,
|
||||||
.add_modifier(Modifier::BOLD),
|
);
|
||||||
))
|
lines.push(Line::from(Span::styled(
|
||||||
|
title,
|
||||||
|
Style::default().fg(theme.text),
|
||||||
|
)));
|
||||||
|
if let Some(meta) = metadata {
|
||||||
|
lines.push(Line::from(Span::styled(
|
||||||
|
meta,
|
||||||
|
Style::default()
|
||||||
|
.fg(theme.placeholder)
|
||||||
|
.add_modifier(Modifier::DIM),
|
||||||
|
)));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ListItem::new(Span::styled(
|
lines.push(Line::from(Span::styled(
|
||||||
" <model unavailable>",
|
" <model unavailable>",
|
||||||
Style::default().fg(theme.error),
|
Style::default().fg(theme.error),
|
||||||
))
|
)));
|
||||||
}
|
}
|
||||||
|
items.push(ListItem::new(lines).style(Style::default().bg(theme.background)));
|
||||||
}
|
}
|
||||||
ModelSelectorItemKind::Empty { provider } => ListItem::new(Span::styled(
|
ModelSelectorItemKind::Empty { provider } => {
|
||||||
format!(" (no models configured for {provider})"),
|
let lines = vec![Line::from(Span::styled(
|
||||||
Style::default()
|
format!(" (no models configured for {provider})"),
|
||||||
.fg(theme.unfocused_panel_border)
|
|
||||||
.add_modifier(Modifier::ITALIC),
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let list = List::new(items)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.title(Span::styled(
|
|
||||||
"Select Model — 🔧 tools • 🧠 thinking • 👁️ vision • 🎧 audio",
|
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(theme.focused_panel_border)
|
.fg(theme.placeholder)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::DIM | Modifier::ITALIC),
|
||||||
))
|
))];
|
||||||
.borders(Borders::ALL)
|
items.push(ListItem::new(lines).style(Style::default().bg(theme.background)));
|
||||||
.style(Style::default().bg(theme.background).fg(theme.text)),
|
}
|
||||||
)
|
}
|
||||||
.highlight_style(
|
}
|
||||||
Style::default()
|
|
||||||
.fg(theme.focused_panel_border)
|
let highlight_style = Style::default()
|
||||||
.add_modifier(Modifier::BOLD),
|
.bg(theme.selection_bg)
|
||||||
)
|
.fg(theme.selection_fg)
|
||||||
.highlight_symbol("▶ ");
|
.add_modifier(Modifier::BOLD);
|
||||||
|
|
||||||
let mut state = ListState::default();
|
let mut state = ListState::default();
|
||||||
state.select(app.selected_model_item());
|
state.select(app.selected_model_item());
|
||||||
frame.render_stateful_widget(list, area, &mut state);
|
|
||||||
|
let list = List::new(items)
|
||||||
|
.highlight_style(highlight_style)
|
||||||
|
.highlight_symbol(" ")
|
||||||
|
.style(Style::default().bg(theme.background).fg(theme.text));
|
||||||
|
|
||||||
|
frame.render_stateful_widget(list, layout[0], &mut state);
|
||||||
|
|
||||||
|
let footer = Paragraph::new(Line::from(Span::styled(
|
||||||
|
"Enter: select · Space: toggle provider · ←/→ collapse/expand · Esc: cancel",
|
||||||
|
Style::default().fg(theme.placeholder),
|
||||||
|
)))
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.style(Style::default().bg(theme.background).fg(theme.placeholder));
|
||||||
|
frame.render_widget(footer, layout[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_model_selector_label(
|
fn build_model_selector_label(
|
||||||
model: &ModelInfo,
|
model: &ModelInfo,
|
||||||
detail: Option<&DetailedModelInfo>,
|
detail: Option<&DetailedModelInfo>,
|
||||||
badges: &[&'static str],
|
badges: &[&'static str],
|
||||||
) -> String {
|
is_current: bool,
|
||||||
let mut parts = vec![model.id.clone()];
|
) -> (String, Option<String>) {
|
||||||
|
let mut display_name = if model.name.trim().is_empty() {
|
||||||
|
model.id.clone()
|
||||||
|
} else {
|
||||||
|
model.name.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if !display_name.eq_ignore_ascii_case(&model.id) {
|
||||||
|
display_name.push_str(&format!(" · {}", model.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut title = format!(" {}", display_name);
|
||||||
|
if !badges.is_empty() {
|
||||||
|
title.push(' ');
|
||||||
|
title.push_str(&badges.join(" "));
|
||||||
|
}
|
||||||
|
if is_current {
|
||||||
|
title.push_str(" ✓");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut meta_parts: Vec<String> = Vec::new();
|
||||||
if let Some(detail) = detail {
|
if let Some(detail) = detail {
|
||||||
if let Some(parameters) = detail
|
if let Some(parameters) = detail
|
||||||
.parameter_size
|
.parameter_size
|
||||||
@@ -2737,24 +2839,54 @@ fn build_model_selector_label(
|
|||||||
.or(detail.parameters.as_ref())
|
.or(detail.parameters.as_ref())
|
||||||
&& !parameters.trim().is_empty()
|
&& !parameters.trim().is_empty()
|
||||||
{
|
{
|
||||||
parts.push(parameters.trim().to_string());
|
meta_parts.push(parameters.trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(size) = detail.size {
|
if let Some(size) = detail.size {
|
||||||
parts.push(format_short_size(size));
|
meta_parts.push(format_short_size(size));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ctx) = detail.context_length {
|
if let Some(ctx) = detail.context_length {
|
||||||
parts.push(format!("ctx {}", ctx));
|
meta_parts.push(format!("ctx {}", ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(quant) = detail
|
||||||
|
.quantization
|
||||||
|
.as_ref()
|
||||||
|
.filter(|q| !q.trim().is_empty())
|
||||||
|
{
|
||||||
|
meta_parts.push(quant.trim().to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut label = format!(" {}", parts.join(" • "));
|
if let Some(desc) = model.description.as_deref() {
|
||||||
if !badges.is_empty() {
|
let trimmed = desc.trim();
|
||||||
label.push(' ');
|
if !trimmed.is_empty() {
|
||||||
label.push_str(&badges.join(" "));
|
meta_parts.push(ellipsize(trimmed, 80));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
label
|
|
||||||
|
let metadata = if meta_parts.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(format!(" {}", meta_parts.join(" • ")))
|
||||||
|
};
|
||||||
|
|
||||||
|
(title, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ellipsize(text: &str, max_chars: usize) -> String {
|
||||||
|
if text.chars().count() <= max_chars {
|
||||||
|
return text.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = max_chars.saturating_sub(1).max(1);
|
||||||
|
let mut truncated = String::new();
|
||||||
|
for ch in text.chars().take(target) {
|
||||||
|
truncated.push(ch);
|
||||||
|
}
|
||||||
|
truncated.push('…');
|
||||||
|
truncated
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_short_size(bytes: u64) -> String {
|
fn format_short_size(bytes: u64) -> String {
|
||||||
@@ -3634,123 +3766,142 @@ fn render_session_browser(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
|
|
||||||
fn render_theme_browser(frame: &mut Frame<'_>, app: &ChatApp) {
|
fn render_theme_browser(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||||
let theme = app.theme();
|
let theme = app.theme();
|
||||||
let area = centered_rect(60, 70, frame.area());
|
let area = frame.area();
|
||||||
frame.render_widget(Clear, area);
|
if area.width == 0 || area.height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let themes = app.available_themes();
|
let themes = app.available_themes();
|
||||||
let current_theme_name = &app.theme().name;
|
let current_theme_name = &app.theme().name;
|
||||||
|
|
||||||
if themes.is_empty() {
|
let max_width: u16 = 80;
|
||||||
let text = vec![
|
let min_width: u16 = 40;
|
||||||
Line::from(""),
|
let mut width = area.width.min(max_width);
|
||||||
Line::from("No themes available."),
|
if area.width >= min_width {
|
||||||
Line::from(""),
|
width = width.max(min_width);
|
||||||
Line::from("Press Esc to close."),
|
} else {
|
||||||
];
|
width = area.width;
|
||||||
|
}
|
||||||
|
width = width.max(1);
|
||||||
|
|
||||||
let paragraph = Paragraph::new(text)
|
let visible_rows = themes.len().clamp(1, 12) as u16;
|
||||||
.style(Style::default().bg(theme.background))
|
let mut height = visible_rows.saturating_mul(2).saturating_add(6);
|
||||||
.block(
|
height = height.clamp(6, area.height);
|
||||||
Block::default()
|
|
||||||
.title(Span::styled(
|
|
||||||
" Themes ",
|
|
||||||
Style::default()
|
|
||||||
.fg(theme.mode_help)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(theme.mode_help))
|
|
||||||
.style(Style::default().bg(theme.background).fg(theme.text)),
|
|
||||||
)
|
|
||||||
.alignment(Alignment::Center);
|
|
||||||
|
|
||||||
frame.render_widget(paragraph, area);
|
let x = area.x + (area.width.saturating_sub(width)) / 2;
|
||||||
|
let mut y = area.y + (area.height.saturating_sub(height)) / 3;
|
||||||
|
if y < area.y {
|
||||||
|
y = area.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let popup_area = Rect::new(x, y, width, height);
|
||||||
|
frame.render_widget(Clear, popup_area);
|
||||||
|
|
||||||
|
let title_line = Line::from(vec![
|
||||||
|
Span::styled(
|
||||||
|
" Theme Selector ",
|
||||||
|
Style::default().fg(theme.info).add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
format!("· Current: {}", current_theme_name),
|
||||||
|
Style::default()
|
||||||
|
.fg(theme.placeholder)
|
||||||
|
.add_modifier(Modifier::DIM),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.title(title_line)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(theme.info))
|
||||||
|
.style(Style::default().bg(theme.background).fg(theme.text));
|
||||||
|
|
||||||
|
let inner = block.inner(popup_area);
|
||||||
|
frame.render_widget(block, popup_area);
|
||||||
|
if inner.width == 0 || inner.height == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get theme metadata to show built-in vs custom
|
if themes.is_empty() {
|
||||||
let all_themes = owlen_core::theme::load_all_themes();
|
let empty = Paragraph::new(Line::from(Span::styled(
|
||||||
let built_in = owlen_core::theme::built_in_themes();
|
"No themes available · Press Esc to close",
|
||||||
|
Style::default()
|
||||||
let items: Vec<ListItem> = themes
|
.fg(theme.placeholder)
|
||||||
.iter()
|
.add_modifier(Modifier::DIM | Modifier::ITALIC),
|
||||||
.enumerate()
|
)))
|
||||||
.map(|(idx, theme_name)| {
|
.alignment(Alignment::Center)
|
||||||
let is_current = theme_name == current_theme_name;
|
.style(Style::default().bg(theme.background).fg(theme.placeholder));
|
||||||
let is_selected = idx == app.selected_theme_index();
|
frame.render_widget(empty, inner);
|
||||||
let is_built_in = built_in.contains_key(theme_name);
|
return;
|
||||||
|
}
|
||||||
// Build display name
|
|
||||||
let mut display = theme_name.clone();
|
|
||||||
if is_current {
|
|
||||||
display.push_str(" ✓");
|
|
||||||
}
|
|
||||||
|
|
||||||
let type_indicator = if is_built_in { "built-in" } else { "custom" };
|
|
||||||
|
|
||||||
let name_style = if is_selected {
|
|
||||||
Style::default()
|
|
||||||
.fg(theme.selection_fg)
|
|
||||||
.bg(theme.selection_bg)
|
|
||||||
.add_modifier(Modifier::BOLD)
|
|
||||||
} else if is_current {
|
|
||||||
Style::default()
|
|
||||||
.fg(theme.focused_panel_border)
|
|
||||||
.add_modifier(Modifier::BOLD)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(theme.text)
|
|
||||||
};
|
|
||||||
|
|
||||||
let info_style = if is_selected {
|
|
||||||
Style::default()
|
|
||||||
.fg(theme.selection_fg)
|
|
||||||
.bg(theme.selection_bg)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(theme.placeholder)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to get theme description or show type
|
|
||||||
let info_text = if all_themes.contains_key(theme_name) {
|
|
||||||
format!(" {} · {}", type_indicator, theme_name)
|
|
||||||
} else {
|
|
||||||
format!(" {}", type_indicator)
|
|
||||||
};
|
|
||||||
|
|
||||||
let lines = vec![
|
|
||||||
Line::from(Span::styled(display, name_style)),
|
|
||||||
Line::from(Span::styled(info_text, info_style)),
|
|
||||||
];
|
|
||||||
|
|
||||||
ListItem::new(lines)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let list = List::new(items).block(
|
|
||||||
Block::default()
|
|
||||||
.title(Span::styled(
|
|
||||||
format!(" Themes ({}) ", themes.len()),
|
|
||||||
Style::default()
|
|
||||||
.fg(theme.mode_help)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(theme.mode_help))
|
|
||||||
.style(Style::default().bg(theme.background).fg(theme.text)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let footer = Paragraph::new(vec![
|
|
||||||
Line::from(""),
|
|
||||||
Line::from("↑/↓ or j/k: Navigate · Enter: Apply theme · g/G: Top/Bottom · Esc: Cancel"),
|
|
||||||
])
|
|
||||||
.alignment(Alignment::Center)
|
|
||||||
.style(Style::default().fg(theme.placeholder).bg(theme.background));
|
|
||||||
|
|
||||||
let layout = Layout::default()
|
let layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Min(5), Constraint::Length(3)])
|
.constraints([Constraint::Min(4), Constraint::Length(2)])
|
||||||
.split(area);
|
.split(inner);
|
||||||
|
|
||||||
|
let built_in = owlen_core::theme::built_in_themes();
|
||||||
|
let mut items: Vec<ListItem> = Vec::with_capacity(themes.len());
|
||||||
|
for theme_name in themes.iter() {
|
||||||
|
let is_current = theme_name == current_theme_name;
|
||||||
|
let descriptor = if built_in.contains_key(theme_name) {
|
||||||
|
"Built-in theme"
|
||||||
|
} else {
|
||||||
|
"Custom theme"
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut title = format!(" {}", theme_name);
|
||||||
|
if is_current {
|
||||||
|
title.push_str(" ✓");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut title_style = Style::default().fg(theme.text);
|
||||||
|
if is_current {
|
||||||
|
title_style = title_style
|
||||||
|
.fg(theme.focused_panel_border)
|
||||||
|
.add_modifier(Modifier::BOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata_style = Style::default()
|
||||||
|
.fg(theme.placeholder)
|
||||||
|
.add_modifier(Modifier::DIM);
|
||||||
|
|
||||||
|
let lines = vec![
|
||||||
|
Line::from(Span::styled(title, title_style)),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::styled(descriptor, metadata_style),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
items.push(ListItem::new(lines).style(Style::default().bg(theme.background)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let highlight_style = Style::default()
|
||||||
|
.bg(theme.selection_bg)
|
||||||
|
.fg(theme.selection_fg)
|
||||||
|
.add_modifier(Modifier::BOLD);
|
||||||
|
|
||||||
|
let mut state = ListState::default();
|
||||||
|
let selected_index = app
|
||||||
|
.selected_theme_index()
|
||||||
|
.min(themes.len().saturating_sub(1));
|
||||||
|
state.select(Some(selected_index));
|
||||||
|
|
||||||
|
let list = List::new(items)
|
||||||
|
.highlight_style(highlight_style)
|
||||||
|
.highlight_symbol(" ")
|
||||||
|
.style(Style::default().bg(theme.background).fg(theme.text));
|
||||||
|
|
||||||
|
frame.render_stateful_widget(list, layout[0], &mut state);
|
||||||
|
|
||||||
|
let footer = Paragraph::new(Line::from(Span::styled(
|
||||||
|
"↑/↓ or j/k: Navigate · Enter: Apply theme · g/G: Top/Bottom · Esc: Cancel",
|
||||||
|
Style::default().fg(theme.placeholder),
|
||||||
|
)))
|
||||||
|
.alignment(Alignment::Center)
|
||||||
|
.style(Style::default().bg(theme.background).fg(theme.placeholder));
|
||||||
|
|
||||||
frame.render_widget(list, layout[0]);
|
|
||||||
frame.render_widget(footer, layout[1]);
|
frame.render_widget(footer, layout[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user