184 lines
7.1 KiB
Rust
184 lines
7.1 KiB
Rust
use crate::config::colors::themes::Theme;
|
|
use ratatui::{
|
|
layout::{Constraint, Direction, Layout, Margin, Rect},
|
|
prelude::Alignment,
|
|
style::{Modifier, Style},
|
|
text::{Line, Span, Text},
|
|
widgets::{Block, BorderType, Borders, Paragraph, Clear},
|
|
Frame,
|
|
};
|
|
use unicode_segmentation::UnicodeSegmentation; // For grapheme clusters
|
|
use unicode_width::UnicodeWidthStr; // For accurate width calculation
|
|
|
|
pub fn render_dialog(
|
|
f: &mut Frame,
|
|
area: Rect,
|
|
theme: &Theme,
|
|
dialog_title: &str,
|
|
dialog_message: &str,
|
|
dialog_buttons: &[String],
|
|
dialog_active_button_index: usize,
|
|
is_loading: bool,
|
|
) {
|
|
// Calculate required height based on the actual number of lines in the message
|
|
let message_lines: Vec<_> = dialog_message.lines().collect();
|
|
let message_height = message_lines.len() as u16;
|
|
let button_row_height = if dialog_buttons.is_empty() { 0 } else { 3 };
|
|
let vertical_padding = 2; // Block borders (top/bottom)
|
|
let inner_vertical_margin = 2; // Margin inside block (top/bottom)
|
|
|
|
// Calculate required height based on actual message lines
|
|
let required_inner_height =
|
|
message_height + button_row_height + inner_vertical_margin;
|
|
let required_total_height = required_inner_height + vertical_padding;
|
|
|
|
// Use a fixed percentage width, clamped to min/max
|
|
let width_percentage: u16 = 60;
|
|
let dialog_width = (area.width * width_percentage / 100)
|
|
.max(20) // Minimum width
|
|
.min(area.width); // Maximum width
|
|
|
|
// Ensure height doesn't exceed available area
|
|
let dialog_height = required_total_height.min(area.height);
|
|
|
|
// Calculate centered area manually
|
|
let dialog_x = area.x + (area.width.saturating_sub(dialog_width)) / 2;
|
|
let dialog_y = area.y + (area.height.saturating_sub(dialog_height)) / 2;
|
|
let dialog_area = Rect::new(dialog_x, dialog_y, dialog_width, dialog_height);
|
|
|
|
// Clear the area first before drawing the dialog
|
|
f.render_widget(Clear, dialog_area);
|
|
|
|
let block = Block::default()
|
|
.borders(Borders::ALL)
|
|
.border_type(BorderType::Rounded)
|
|
.border_style(Style::default().fg(theme.accent))
|
|
.title(format!(" {} ", dialog_title)) // Add padding to title
|
|
.style(Style::default().bg(theme.bg));
|
|
|
|
f.render_widget(block, dialog_area);
|
|
|
|
// Calculate inner area *after* defining the block
|
|
let inner_area = dialog_area.inner(Margin {
|
|
horizontal: 2, // Left/Right padding inside border
|
|
vertical: 1, // Top/Bottom padding inside border
|
|
});
|
|
|
|
if is_loading {
|
|
// --- Loading State ---
|
|
let loading_text = Paragraph::new(dialog_message) // Use the message passed for loading
|
|
.style(Style::default().fg(theme.fg).add_modifier(Modifier::ITALIC))
|
|
.alignment(Alignment::Center);
|
|
// Render loading message centered in the inner area
|
|
f.render_widget(loading_text, inner_area);
|
|
} else {
|
|
// --- Normal State (Message + Buttons) ---
|
|
|
|
// Layout for Message and Buttons based on actual message height
|
|
let mut constraints = vec![
|
|
// Allocate space for message, ensuring at least 1 line height
|
|
Constraint::Length(message_height.max(1)), // Use actual calculated height
|
|
];
|
|
if button_row_height > 0 {
|
|
constraints.push(Constraint::Length(button_row_height));
|
|
}
|
|
|
|
let chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(constraints)
|
|
.split(inner_area);
|
|
|
|
// Render Message
|
|
let available_width = inner_area.width as usize;
|
|
let ellipsis = "...";
|
|
let ellipsis_width = UnicodeWidthStr::width(ellipsis);
|
|
|
|
let processed_lines: Vec<Line> = message_lines
|
|
.into_iter()
|
|
.map(|line| {
|
|
let line_width = UnicodeWidthStr::width(line);
|
|
if line_width > available_width {
|
|
// Truncate with ellipsis
|
|
let mut truncated_len = 0;
|
|
let mut current_width = 0;
|
|
for (idx, grapheme) in line.grapheme_indices(true) {
|
|
let grapheme_width = UnicodeWidthStr::width(grapheme);
|
|
if current_width + grapheme_width
|
|
> available_width.saturating_sub(ellipsis_width)
|
|
{
|
|
break;
|
|
}
|
|
current_width += grapheme_width;
|
|
truncated_len = idx + grapheme.len();
|
|
}
|
|
let truncated_line =
|
|
format!("{}{}", &line[..truncated_len], ellipsis);
|
|
Line::from(Span::styled(
|
|
truncated_line,
|
|
Style::default().fg(theme.fg),
|
|
))
|
|
} else {
|
|
Line::from(Span::styled(line, Style::default().fg(theme.fg)))
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let message_paragraph =
|
|
Paragraph::new(Text::from(processed_lines)).alignment(Alignment::Center);
|
|
f.render_widget(message_paragraph, chunks[0]); // Render message in the first chunk
|
|
|
|
// Render Buttons if they exist and there's a chunk for them
|
|
if !dialog_buttons.is_empty() && chunks.len() > 1 {
|
|
let button_area = chunks[1];
|
|
let button_count = dialog_buttons.len();
|
|
|
|
let button_constraints = std::iter::repeat(Constraint::Ratio(
|
|
1,
|
|
button_count as u32,
|
|
))
|
|
.take(button_count)
|
|
.collect::<Vec<_>>();
|
|
|
|
let button_chunks = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints(button_constraints)
|
|
.horizontal_margin(1) // Add space between buttons
|
|
.split(button_area);
|
|
|
|
for (i, button_label) in dialog_buttons.iter().enumerate() {
|
|
if i >= button_chunks.len() {
|
|
break;
|
|
}
|
|
|
|
let is_active = i == dialog_active_button_index;
|
|
let (button_style, border_style) = if is_active {
|
|
(
|
|
Style::default()
|
|
.fg(theme.highlight)
|
|
.add_modifier(Modifier::BOLD),
|
|
Style::default().fg(theme.accent),
|
|
)
|
|
} else {
|
|
(
|
|
Style::default().fg(theme.fg),
|
|
Style::default().fg(theme.border),
|
|
)
|
|
};
|
|
|
|
let button_block = Block::default()
|
|
.borders(Borders::ALL)
|
|
.border_type(BorderType::Plain)
|
|
.border_style(border_style);
|
|
|
|
f.render_widget(
|
|
Paragraph::new(button_label.as_str())
|
|
.block(button_block)
|
|
.style(button_style)
|
|
.alignment(Alignment::Center),
|
|
button_chunks[i],
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|