// src/components/handlers/canvas_multi.rs use ratatui::{ widgets::{Paragraph, Block, Borders}, layout::{Layout, Constraint, Direction, Rect}, style::{Style, Modifier}, text::{Line, Span}, Frame, prelude::Alignment, }; use crate::config::colors::themes::Theme; // Import the new trait use crate::state::pages::multi_canvas_state::MultilineEditorState; use crate::state::app::highlight::HighlightState; // Assuming this is your global highlight state use std::cmp::{min, max}; pub fn render_multiline_editor( // Renamed for clarity f: &mut Frame, area: Rect, // Total area for this component editor_state: &impl MultilineEditorState, // Use the new trait theme: &Theme, is_edit_mode: bool, // Is the editor currently active for input? highlight_state: &HighlightState, // Global highlight state for selections ) -> Option { // Returns the Rect of the active line for external cursor management if needed let editor_label_str = editor_state.get_label(); let editor_lines_vec = editor_state.lines(); // This is &Vec // Convert &Vec to Vec<&String> for iteration if needed by existing logic, // or iterate directly over editor_lines_vec. // For consistency with your previous structure, let's map: let editor_lines_refs: Vec<&String> = editor_lines_vec.iter().collect(); let active_line_idx = editor_state.active_line_index(); let cursor_char_pos_in_active_line = editor_state.cursor_char_pos_in_active_line(); let (label_column_rect_opt, editor_area_rect) = if editor_label_str.is_some() { let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage(30), Constraint::Percentage(70), ]) .split(area); (Some(chunks[0]), chunks[1]) } else { (None, area) }; let num_lines_to_display = if editor_lines_vec.is_empty() { 1 } else { editor_lines_vec.len() }; let editor_content_height = num_lines_to_display as u16; // Ensure editor_area_rect.height is at least 2 for borders + 1 for content let available_height_for_block = editor_area_rect.height.max(1); // Min height of 1 for the block itself let desired_input_block_height = (editor_content_height + 2).min(available_height_for_block); let border_style = if editor_state.has_unsaved_changes() { Style::default().fg(theme.warning) } else if is_edit_mode { Style::default().fg(theme.accent) } else { Style::default().fg(theme.secondary) }; let input_container_widget = Block::default() .borders(Borders::ALL) .border_style(border_style) .style(Style::default().bg(theme.bg)); let input_block_rect = Rect { x: editor_area_rect.x, y: editor_area_rect.y, width: editor_area_rect.width, height: desired_input_block_height, }; f.render_widget(&input_container_widget, input_block_rect); let text_lines_render_area = input_container_widget.inner(input_block_rect); if text_lines_render_area.height == 0 || text_lines_render_area.width == 0 { return None; } let line_rows_layout = Layout::default() .direction(Direction::Vertical) .constraints( std::iter::repeat(Constraint::Length(1)) .take(num_lines_to_display) .collect::>(), ) .split(text_lines_render_area); let mut active_line_render_rect = None; if let Some(label_col_rect) = label_column_rect_opt { if let Some(label_text) = editor_label_str { let label_paragraph = Paragraph::new(Line::from(Span::styled( format!("{}:", label_text), Style::default().fg(theme.fg), ))); let label_y_pos = if input_block_rect.height > 0 { input_block_rect.y + 1 } else { input_block_rect.y }; let label_render_rect = Rect { x: label_col_rect.x, y: label_y_pos.min( label_col_rect.y + label_col_rect.height.saturating_sub(1), ), width: label_col_rect.width.saturating_sub(1), height: 1, }; if label_render_rect.area() > 0 { f.render_widget(label_paragraph, label_render_rect); } } } // If editor_lines_refs is empty, but we allocated space for one line (num_lines_to_display = 1) if editor_lines_refs.is_empty() && num_lines_to_display == 1 && !line_rows_layout.is_empty() { let target_rect = line_rows_layout[0]; // Optionally render a placeholder or just set cursor if in edit mode active_line_render_rect = Some(target_rect); if is_edit_mode && active_line_idx == 0 { // Cursor for the empty line let cursor_x = target_rect.x + cursor_char_pos_in_active_line as u16; let cursor_y = target_rect.y; f.set_cursor_position((cursor_x.min(target_rect.right()), cursor_y)); } return active_line_render_rect; } for (line_idx, current_line_str_ref) in editor_lines_refs.iter().enumerate() { if line_idx >= line_rows_layout.len() { break; } let is_active_line = line_idx == active_line_idx; let text_content = current_line_str_ref.as_str(); let text_len = text_content.chars().count(); let current_line_target_rect = line_rows_layout[line_idx]; let line_widget_content: Line; let style_active_text = Style::default().fg(theme.highlight); let style_inactive_text = Style::default().fg(theme.fg); let style_highlight_selection_bg = Style::default() .fg(theme.highlight) .bg(theme.highlight_bg) .add_modifier(Modifier::BOLD); let base_text_style_for_line = if is_active_line && is_edit_mode { style_active_text } else { style_inactive_text }; match highlight_state { HighlightState::Off => { line_widget_content = Line::from(Span::styled( text_content, base_text_style_for_line, )); } HighlightState::Characterwise { anchor } => { let (anchor_ln_idx, anchor_char_idx) = *anchor; let current_ln_idx_for_highlight = active_line_idx; // The editor's active line let current_char_idx_for_highlight = cursor_char_pos_in_active_line; let selection_start_ln = min(anchor_ln_idx, current_ln_idx_for_highlight); let selection_end_ln = max(anchor_ln_idx, current_ln_idx_for_highlight); let sel_start_char = if anchor_ln_idx == current_ln_idx_for_highlight { min(anchor_char_idx, current_char_idx_for_highlight) } else if anchor_ln_idx < current_ln_idx_for_highlight { anchor_char_idx } else { current_char_idx_for_highlight }; let sel_end_char = if anchor_ln_idx == current_ln_idx_for_highlight { max(anchor_char_idx, current_char_idx_for_highlight) } else if anchor_ln_idx < current_ln_idx_for_highlight { current_char_idx_for_highlight } else { anchor_char_idx }; if line_idx >= selection_start_ln && line_idx <= selection_end_ln { let text_style_within_selection = if is_edit_mode { style_active_text } else { style_inactive_text }; if selection_start_ln == selection_end_ln { let start_h = sel_start_char.min(text_len); let end_h = sel_end_char.min(text_len); let before: String = text_content.chars().take(start_h).collect(); let highlighted: String = text_content .chars() .skip(start_h) .take(end_h.saturating_sub(start_h) + 1) .collect(); let after: String = text_content.chars().skip(end_h + 1).collect(); line_widget_content = Line::from(vec![ Span::styled(before, text_style_within_selection), Span::styled( highlighted, style_highlight_selection_bg, ), Span::styled(after, text_style_within_selection), ]); } else if line_idx == selection_start_ln { let start_h = sel_start_char.min(text_len); let before: String = text_content.chars().take(start_h).collect(); let highlighted: String = text_content.chars().skip(start_h).collect(); line_widget_content = Line::from(vec![ Span::styled(before, text_style_within_selection), Span::styled( highlighted, style_highlight_selection_bg, ), ]); } else if line_idx == selection_end_ln { let end_h_inclusive = sel_end_char .min(if text_len > 0 { text_len - 1 } else { 0 }); let highlighted: String = text_content .chars() .take(end_h_inclusive + 1) .collect(); let after: String = text_content .chars() .skip(end_h_inclusive + 1) .collect(); line_widget_content = Line::from(vec![ Span::styled( highlighted, style_highlight_selection_bg, ), Span::styled(after, text_style_within_selection), ]); } else { line_widget_content = Line::from(Span::styled( text_content, style_highlight_selection_bg, )); } } else { line_widget_content = Line::from(Span::styled( text_content, base_text_style_for_line, )); } } HighlightState::Linewise { anchor_line } => { let selection_start_ln = min(*anchor_line, active_line_idx); let selection_end_ln = max(*anchor_line, active_line_idx); if line_idx >= selection_start_ln && line_idx <= selection_end_ln { line_widget_content = Line::from(Span::styled( text_content, style_highlight_selection_bg, )); } else { line_widget_content = Line::from(Span::styled( text_content, base_text_style_for_line, )); } } } let line_paragraph = Paragraph::new(line_widget_content).alignment(Alignment::Left); f.render_widget(line_paragraph, current_line_target_rect); if is_active_line { active_line_render_rect = Some(current_line_target_rect); if is_edit_mode { let max_cursor_x_offset_for_rect = current_line_target_rect .width .saturating_sub(0); // Allow cursor at end of line let cursor_x_offset_within_line = (cursor_char_pos_in_active_line as u16); // The cursor_x_offset should be relative to the start of the text area, // but not exceed the width of the current_line_target_rect. let cursor_x_offset = cursor_x_offset_within_line.min(max_cursor_x_offset_for_rect); let cursor_x = current_line_target_rect.x + cursor_x_offset; let cursor_y = current_line_target_rect.y; // Final check to ensure cursor is within the visible block if cursor_x <= current_line_target_rect.right() && cursor_y < current_line_target_rect.bottom() { f.set_cursor_position((cursor_x, cursor_y)); } } } } active_line_render_rect }