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 = 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::>(); 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], ); } } } }