diff --git a/client/src/components/common/command_line.rs b/client/src/components/common/command_line.rs index 970dc3a..3a5b809 100644 --- a/client/src/components/common/command_line.rs +++ b/client/src/components/common/command_line.rs @@ -1,4 +1,5 @@ -// src/client/components/command_line.rs +// src/components/common/command_line.rs + use ratatui::{ widgets::{Block, Paragraph}, style::Style, @@ -6,6 +7,8 @@ use ratatui::{ Frame, }; use crate::config::colors::themes::Theme; +use unicode_width::UnicodeWidthStr; // Import for width calculation + pub fn render_command_line( f: &mut Frame, area: Rect, @@ -13,33 +16,54 @@ pub fn render_command_line( active: bool, // This is event_handler.command_mode theme: &Theme, message: &str, // This is event_handler.command_message - // Palette-specific parameters are removed ) { - // This function now only renders the normal command line. - // The find_file_palette_active check in render_ui ensures this is called appropriately. - - if !active { // If not in normal command mode, render minimally or nothing - let paragraph = Paragraph::new("") - .block(Block::default().style(Style::default().bg(theme.bg))); - f.render_widget(paragraph, area); - return; - } - - let prompt = ":"; - let display_text = if message.is_empty() || message == ":" { - format!("{}{}", prompt, input) - } else { - if input.is_empty() { // If command was just executed, input is cleared, show message + // Original logic for determining display_text + let display_text = if !active { + // If not in normal command mode, but there's a message (e.g. from Find File palette closing) + // Or if command mode is off and message is empty (render minimally) + if message.is_empty() { + "".to_string() // Render an empty string, background will cover + } else { message.to_string() - } else { // Show input and message - format!("{}{} | {}", prompt, input, message) + } + } else { // active is true (normal command mode) + let prompt = ":"; + if message.is_empty() || message == ":" { + format!("{}{}", prompt, input) + } else { + if input.is_empty() { // If command was just executed, input is cleared, show message + message.to_string() + } else { // Show input and message + format!("{}{} | {}", prompt, input, message) + } } }; - let style = Style::default().fg(theme.accent); // Style for active command line - let paragraph = Paragraph::new(display_text) - .block(Block::default().style(Style::default().bg(theme.bg))) - .style(style); + let content_width = UnicodeWidthStr::width(display_text.as_str()); + let available_width = area.width as usize; + let padding_needed = available_width.saturating_sub(content_width); + + let display_text_padded = if padding_needed > 0 { + format!("{}{}", display_text, " ".repeat(padding_needed)) + } else { + // If text is too long, ratatui's Paragraph will handle truncation. + // We could also truncate here if specific behavior is needed: + // display_text.chars().take(available_width).collect::() + display_text + }; + + // Determine style based on active state, but apply to the whole paragraph + let text_style = if active { + Style::default().fg(theme.accent) + } else { + // If not active, but there's a message, use default foreground. + // If message is also empty, this style won't matter much for empty text. + Style::default().fg(theme.fg) + }; + + let paragraph = Paragraph::new(display_text_padded) + .block(Block::default().style(Style::default().bg(theme.bg))) // Block ensures bg for whole area + .style(text_style); // Style for the text itself f.render_widget(paragraph, area); } diff --git a/client/src/components/common/status_line.rs b/client/src/components/common/status_line.rs index c3273a8..91ee799 100644 --- a/client/src/components/common/status_line.rs +++ b/client/src/components/common/status_line.rs @@ -1,3 +1,4 @@ +// src/components/common/status_line.rs use ratatui::{ style::Style, layout::Rect, @@ -35,43 +36,62 @@ pub fn render_status_line( let separator = " | "; let separator_width = UnicodeWidthStr::width(separator); - let fixed_width_with_fps = mode_width + separator_width + separator_width + + let fixed_width_with_fps = mode_width + separator_width + separator_width + program_info_width + separator_width + fps_width; - let show_fps = fixed_width_with_fps < available_width; + let show_fps = fixed_width_with_fps <= available_width; // Use <= to show if it fits exactly let remaining_width_for_dir = available_width.saturating_sub( - mode_width + separator_width + separator_width + program_info_width + - if show_fps { separator_width + fps_width } else { 0 } + mode_width + separator_width + // after mode + separator_width + program_info_width + // after program_info + if show_fps { separator_width + fps_width } else { 0 } // after fps ); - let dir_display_text = if UnicodeWidthStr::width(display_dir.as_str()) <= remaining_width_for_dir { - display_dir + // Original directory display logic + let dir_display_text_str = if UnicodeWidthStr::width(display_dir.as_str()) <= remaining_width_for_dir { + display_dir // display_dir is already a String here } else { - let dir_name = Path::new(current_dir) + let dir_name = Path::new(current_dir) // Use original current_dir for path logic .file_name() .and_then(|n| n.to_str()) - .unwrap_or(current_dir); + .unwrap_or(current_dir); // Fallback to current_dir if no filename if UnicodeWidthStr::width(dir_name) <= remaining_width_for_dir { dir_name.to_string() } else { - dir_name.chars().take(remaining_width_for_dir).collect() + dir_name.chars().take(remaining_width_for_dir).collect::() } }; - let mut spans = vec![ + // Calculate current content width based on what will be displayed + let mut current_content_width = mode_width + separator_width + + UnicodeWidthStr::width(dir_display_text_str.as_str()) + + separator_width + program_info_width; + if show_fps { + current_content_width += separator_width + fps_width; + } + + let mut line_spans = vec![ Span::styled(mode_text, Style::default().fg(theme.accent)), - Span::styled(" | ", Style::default().fg(theme.border)), - Span::styled(dir_display_text, Style::default().fg(theme.fg)), - Span::styled(" | ", Style::default().fg(theme.border)), - Span::styled(program_info, Style::default().fg(theme.secondary)), + Span::styled(separator, Style::default().fg(theme.border)), + Span::styled(dir_display_text_str.as_str(), Style::default().fg(theme.fg)), + Span::styled(separator, Style::default().fg(theme.border)), + Span::styled(program_info.as_str(), Style::default().fg(theme.secondary)), ]; if show_fps { - spans.push(Span::styled(" | ", Style::default().fg(theme.border))); - spans.push(Span::styled(fps_text, Style::default().fg(theme.secondary))); + line_spans.push(Span::styled(separator, Style::default().fg(theme.border))); + line_spans.push(Span::styled(fps_text.as_str(), Style::default().fg(theme.secondary))); } - let paragraph = Paragraph::new(Line::from(spans)) + // Calculate padding + let padding_needed = available_width.saturating_sub(current_content_width); + if padding_needed > 0 { + line_spans.push(Span::styled( + " ".repeat(padding_needed), + Style::default().bg(theme.bg), // Ensure padding uses background color + )); + } + + let paragraph = Paragraph::new(Line::from(line_spans)) .style(Style::default().bg(theme.bg)); f.render_widget(paragraph, area); diff --git a/client/src/ui/handlers/render.rs b/client/src/ui/handlers/render.rs index 3c8b1e4..ad8394f 100644 --- a/client/src/ui/handlers/render.rs +++ b/client/src/ui/handlers/render.rs @@ -19,6 +19,7 @@ use ratatui::{ widgets::{Block, List, ListItem, Paragraph}, Frame, }; +use unicode_width::UnicodeWidthStr; // Needed for width calculation use crate::state::pages::canvas_state::CanvasState; use crate::state::pages::form::FormState; use crate::state::pages::auth::AuthState; @@ -31,6 +32,7 @@ use crate::state::pages::admin::AdminState; use crate::state::app::highlight::HighlightState; use crate::modes::general::command_navigation::NavigationState; +// Define a fixed height for the options list in the command palette const PALETTE_MAX_VISIBLE_OPTIONS: usize = 15; // ++ New function to render the Find File Palette ++ @@ -39,24 +41,23 @@ fn render_find_file_palette( area: Rect, theme: &Theme, palette_input: &str, // Specific input for the palette - options: &[String], - selected_index: Option, + options: &[String], // These are already filtered options + selected_index: Option, // Index within the filtered `options` ) { + // Use a regular space character for padding. + const PADDING_CHAR: &str = " "; + let num_total_filtered = options.len(); let current_selected_list_idx = selected_index; let mut display_start_offset = 0; if num_total_filtered > PALETTE_MAX_VISIBLE_OPTIONS { if let Some(sel_idx) = current_selected_list_idx { - // If selected item is below the current view window if sel_idx >= display_start_offset + PALETTE_MAX_VISIBLE_OPTIONS { display_start_offset = sel_idx - PALETTE_MAX_VISIBLE_OPTIONS + 1; - } - // If selected item is above the current view window - else if sel_idx < display_start_offset { + } else if sel_idx < display_start_offset { display_start_offset = sel_idx; } - // Clamp display_start_offset display_start_offset = display_start_offset .min(num_total_filtered.saturating_sub(PALETTE_MAX_VISIBLE_OPTIONS)); } @@ -65,42 +66,78 @@ fn render_find_file_palette( let display_end_offset = (display_start_offset + PALETTE_MAX_VISIBLE_OPTIONS).min(num_total_filtered); - let visible_options_slice = &options[display_start_offset..display_end_offset]; + + let visible_options_slice = if num_total_filtered > 0 { + &options[display_start_offset..display_end_offset] + } else { + &[] + }; let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(1), // For palette input line - Constraint::Length(PALETTE_MAX_VISIBLE_OPTIONS as u16), // For options list + Constraint::Length(PALETTE_MAX_VISIBLE_OPTIONS as u16), // For options list (fixed height) ]) .split(area); - // Draw the palette input line - let prompt_text = format!("Find File: {}", palette_input); // Using palette_input - let input_paragraph = Paragraph::new(prompt_text) + let input_area = chunks[0]; + let list_area = chunks[1]; + + // Draw the palette input line (with padding) + let base_prompt_text = format!("Find File: {}", palette_input); + let prompt_text_width = UnicodeWidthStr::width(base_prompt_text.as_str()); + let input_area_width = input_area.width as usize; + let input_padding_needed = input_area_width.saturating_sub(prompt_text_width); + + let padded_prompt_text = if input_padding_needed > 0 { + format!("{}{}", base_prompt_text, PADDING_CHAR.repeat(input_padding_needed)) + } else { + base_prompt_text // Or truncate if necessary: base_prompt_text.chars().take(input_area_width).collect() + }; + + let input_paragraph = Paragraph::new(padded_prompt_text) .style(Style::default().fg(theme.accent).bg(theme.bg)); - f.render_widget(input_paragraph, chunks[0]); + f.render_widget(input_paragraph, input_area); - // Draw the list of options - if !visible_options_slice.is_empty() && chunks.len() > 1 { - let list_items: Vec = visible_options_slice - .iter() - .enumerate() - .map(|(idx, opt_str)| { - let original_list_idx = display_start_offset + idx; - let style = if current_selected_list_idx == Some(original_list_idx) { - Style::default().fg(theme.bg).bg(theme.accent) // Highlight selected - } else { - Style::default().fg(theme.fg).bg(theme.bg) - }; - ListItem::new(opt_str.as_str()).style(style) - }) - .collect(); + // --- Draw the list of options, ensuring all PALETTE_MAX_VISIBLE_OPTIONS rows are covered --- + let mut display_list_items: Vec = Vec::with_capacity(PALETTE_MAX_VISIBLE_OPTIONS); - let options_list = List::new(list_items) - .block(Block::default().style(Style::default().bg(theme.bg))); - f.render_widget(options_list, chunks[1]); + for (idx_in_slice, opt_str) in visible_options_slice.iter().enumerate() { + let original_list_idx = display_start_offset + idx_in_slice; + let is_selected = current_selected_list_idx == Some(original_list_idx); + + let style = if is_selected { + Style::default().fg(theme.bg).bg(theme.accent) + } else { + Style::default().fg(theme.fg).bg(theme.bg) + }; + + let opt_width = opt_str.width() as u16; + let list_item_width = list_area.width; + let padding_amount = list_item_width.saturating_sub(opt_width); + let padded_opt_str = format!( + "{}{}", + opt_str, + PADDING_CHAR.repeat(padding_amount as usize) + ); + display_list_items.push(ListItem::new(padded_opt_str).style(style)); } + + let num_rendered_options = display_list_items.len(); + if num_rendered_options < PALETTE_MAX_VISIBLE_OPTIONS { + for _ in num_rendered_options..PALETTE_MAX_VISIBLE_OPTIONS { + let empty_padded_str = PADDING_CHAR.repeat(list_area.width as usize); + display_list_items.push( + ListItem::new(empty_padded_str) + .style(Style::default().fg(theme.bg).bg(theme.bg)), + ); + } + } + + let options_list_widget = List::new(display_list_items) + .block(Block::default().style(Style::default().bg(theme.bg))); + f.render_widget(options_list_widget, list_area); } #[allow(clippy::too_many_arguments)] @@ -135,7 +172,7 @@ pub fn render_ui( let mut bottom_area_constraints: Vec = vec![Constraint::Length(1)]; // Status line let command_palette_area_height = if navigation_state.active { - 1 + PALETTE_MAX_VISIBLE_OPTIONS as u16 + 1 + PALETTE_MAX_VISIBLE_OPTIONS as u16 // Input line + fixed height for options } else if event_handler_command_mode_active { 1 // Height for normal command line } else { @@ -258,6 +295,7 @@ pub fn render_ui( area, theme, &navigation_state.input, + // Pass the full filtered options to the palette renderer &navigation_state.filtered_options.iter().map(|(_, opt)| opt.clone()).collect::>(), navigation_state.selected_index, );