325 lines
13 KiB
Rust
325 lines
13 KiB
Rust
// 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<Rect> { // 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<String>
|
|
|
|
// Convert &Vec<String> 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::<Vec<_>>(),
|
|
)
|
|
.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
|
|
}
|