overriding overflows by using empty spaces as letters

This commit is contained in:
filipriec
2025-05-29 19:32:48 +02:00
parent e9b4b34fb4
commit 3dbc086f10
3 changed files with 154 additions and 72 deletions

View File

@@ -1,4 +1,5 @@
// src/client/components/command_line.rs // src/components/common/command_line.rs
use ratatui::{ use ratatui::{
widgets::{Block, Paragraph}, widgets::{Block, Paragraph},
style::Style, style::Style,
@@ -6,6 +7,8 @@ use ratatui::{
Frame, Frame,
}; };
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
use unicode_width::UnicodeWidthStr; // Import for width calculation
pub fn render_command_line( pub fn render_command_line(
f: &mut Frame, f: &mut Frame,
area: Rect, area: Rect,
@@ -13,33 +16,54 @@ pub fn render_command_line(
active: bool, // This is event_handler.command_mode active: bool, // This is event_handler.command_mode
theme: &Theme, theme: &Theme,
message: &str, // This is event_handler.command_message message: &str, // This is event_handler.command_message
// Palette-specific parameters are removed
) { ) {
// This function now only renders the normal command line. // Original logic for determining display_text
// The find_file_palette_active check in render_ui ensures this is called appropriately. let display_text = if !active {
// If not in normal command mode, but there's a message (e.g. from Find File palette closing)
if !active { // If not in normal command mode, render minimally or nothing // Or if command mode is off and message is empty (render minimally)
let paragraph = Paragraph::new("") if message.is_empty() {
.block(Block::default().style(Style::default().bg(theme.bg))); "".to_string() // Render an empty string, background will cover
f.render_widget(paragraph, area); } else {
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
message.to_string() 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 content_width = UnicodeWidthStr::width(display_text.as_str());
let paragraph = Paragraph::new(display_text) let available_width = area.width as usize;
.block(Block::default().style(Style::default().bg(theme.bg))) let padding_needed = available_width.saturating_sub(content_width);
.style(style);
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::<String>()
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); f.render_widget(paragraph, area);
} }

View File

@@ -1,3 +1,4 @@
// src/components/common/status_line.rs
use ratatui::{ use ratatui::{
style::Style, style::Style,
layout::Rect, layout::Rect,
@@ -35,43 +36,62 @@ pub fn render_status_line(
let separator = " | "; let separator = " | ";
let separator_width = UnicodeWidthStr::width(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; 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( let remaining_width_for_dir = available_width.saturating_sub(
mode_width + separator_width + separator_width + program_info_width + mode_width + separator_width + // after mode
if show_fps { separator_width + fps_width } else { 0 } 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 { // Original directory display logic
display_dir 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 { } else {
let dir_name = Path::new(current_dir) let dir_name = Path::new(current_dir) // Use original current_dir for path logic
.file_name() .file_name()
.and_then(|n| n.to_str()) .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 { if UnicodeWidthStr::width(dir_name) <= remaining_width_for_dir {
dir_name.to_string() dir_name.to_string()
} else { } else {
dir_name.chars().take(remaining_width_for_dir).collect() dir_name.chars().take(remaining_width_for_dir).collect::<String>()
} }
}; };
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(mode_text, Style::default().fg(theme.accent)),
Span::styled(" | ", Style::default().fg(theme.border)), Span::styled(separator, Style::default().fg(theme.border)),
Span::styled(dir_display_text, Style::default().fg(theme.fg)), Span::styled(dir_display_text_str.as_str(), Style::default().fg(theme.fg)),
Span::styled(" | ", Style::default().fg(theme.border)), Span::styled(separator, Style::default().fg(theme.border)),
Span::styled(program_info, Style::default().fg(theme.secondary)), Span::styled(program_info.as_str(), Style::default().fg(theme.secondary)),
]; ];
if show_fps { if show_fps {
spans.push(Span::styled(" | ", Style::default().fg(theme.border))); line_spans.push(Span::styled(separator, Style::default().fg(theme.border)));
spans.push(Span::styled(fps_text, Style::default().fg(theme.secondary))); 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)); .style(Style::default().bg(theme.bg));
f.render_widget(paragraph, area); f.render_widget(paragraph, area);

View File

@@ -19,6 +19,7 @@ use ratatui::{
widgets::{Block, List, ListItem, Paragraph}, widgets::{Block, List, ListItem, Paragraph},
Frame, Frame,
}; };
use unicode_width::UnicodeWidthStr; // Needed for width calculation
use crate::state::pages::canvas_state::CanvasState; use crate::state::pages::canvas_state::CanvasState;
use crate::state::pages::form::FormState; use crate::state::pages::form::FormState;
use crate::state::pages::auth::AuthState; use crate::state::pages::auth::AuthState;
@@ -31,6 +32,7 @@ use crate::state::pages::admin::AdminState;
use crate::state::app::highlight::HighlightState; use crate::state::app::highlight::HighlightState;
use crate::modes::general::command_navigation::NavigationState; 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; const PALETTE_MAX_VISIBLE_OPTIONS: usize = 15;
// ++ New function to render the Find File Palette ++ // ++ New function to render the Find File Palette ++
@@ -39,24 +41,23 @@ fn render_find_file_palette(
area: Rect, area: Rect,
theme: &Theme, theme: &Theme,
palette_input: &str, // Specific input for the palette palette_input: &str, // Specific input for the palette
options: &[String], options: &[String], // These are already filtered options
selected_index: Option<usize>, selected_index: Option<usize>, // Index within the filtered `options`
) { ) {
// Use a regular space character for padding.
const PADDING_CHAR: &str = " ";
let num_total_filtered = options.len(); let num_total_filtered = options.len();
let current_selected_list_idx = selected_index; let current_selected_list_idx = selected_index;
let mut display_start_offset = 0; let mut display_start_offset = 0;
if num_total_filtered > PALETTE_MAX_VISIBLE_OPTIONS { if num_total_filtered > PALETTE_MAX_VISIBLE_OPTIONS {
if let Some(sel_idx) = current_selected_list_idx { 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 { if sel_idx >= display_start_offset + PALETTE_MAX_VISIBLE_OPTIONS {
display_start_offset = sel_idx - PALETTE_MAX_VISIBLE_OPTIONS + 1; display_start_offset = sel_idx - PALETTE_MAX_VISIBLE_OPTIONS + 1;
} } else if sel_idx < display_start_offset {
// If selected item is above the current view window
else if sel_idx < display_start_offset {
display_start_offset = sel_idx; display_start_offset = sel_idx;
} }
// Clamp display_start_offset
display_start_offset = display_start_offset display_start_offset = display_start_offset
.min(num_total_filtered.saturating_sub(PALETTE_MAX_VISIBLE_OPTIONS)); .min(num_total_filtered.saturating_sub(PALETTE_MAX_VISIBLE_OPTIONS));
} }
@@ -65,42 +66,78 @@ fn render_find_file_palette(
let display_end_offset = let display_end_offset =
(display_start_offset + PALETTE_MAX_VISIBLE_OPTIONS).min(num_total_filtered); (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() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
Constraint::Length(1), // For palette input line 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); .split(area);
// Draw the palette input line let input_area = chunks[0];
let prompt_text = format!("Find File: {}", palette_input); // Using palette_input let list_area = chunks[1];
let input_paragraph = Paragraph::new(prompt_text)
// 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)); .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 // --- Draw the list of options, ensuring all PALETTE_MAX_VISIBLE_OPTIONS rows are covered ---
if !visible_options_slice.is_empty() && chunks.len() > 1 { let mut display_list_items: Vec<ListItem> = Vec::with_capacity(PALETTE_MAX_VISIBLE_OPTIONS);
let list_items: Vec<ListItem> = 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();
let options_list = List::new(list_items) for (idx_in_slice, opt_str) in visible_options_slice.iter().enumerate() {
.block(Block::default().style(Style::default().bg(theme.bg))); let original_list_idx = display_start_offset + idx_in_slice;
f.render_widget(options_list, chunks[1]); 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)] #[allow(clippy::too_many_arguments)]
@@ -135,7 +172,7 @@ pub fn render_ui(
let mut bottom_area_constraints: Vec<Constraint> = vec![Constraint::Length(1)]; // Status line let mut bottom_area_constraints: Vec<Constraint> = vec![Constraint::Length(1)]; // Status line
let command_palette_area_height = if navigation_state.active { 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 { } else if event_handler_command_mode_active {
1 // Height for normal command line 1 // Height for normal command line
} else { } else {
@@ -258,6 +295,7 @@ pub fn render_ui(
area, area,
theme, theme,
&navigation_state.input, &navigation_state.input,
// Pass the full filtered options to the palette renderer
&navigation_state.filtered_options.iter().map(|(_, opt)| opt.clone()).collect::<Vec<_>>(), &navigation_state.filtered_options.iter().map(|(_, opt)| opt.clone()).collect::<Vec<_>>(),
navigation_state.selected_index, navigation_state.selected_index,
); );