multiline canvas added
This commit is contained in:
254
client/src/components/handlers/canvas_multi.rs
Normal file
254
client/src/components/handlers/canvas_multi.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
// 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;
|
||||
use crate::state::pages::canvas_state::CanvasState; // Ensure this trait is adapted
|
||||
use crate::state::app::highlight::HighlightState;
|
||||
use std::cmp::{min, max};
|
||||
|
||||
// Note to user: The CanvasState trait will need to be significantly adapted.
|
||||
// It should effectively manage a multiline text buffer.
|
||||
// - `inputs` (passed to this function) should be the lines from CanvasState.
|
||||
// - `current_field_idx` (passed here) should be the active line index from CanvasState.
|
||||
// - `form_state.current_cursor_pos()` should return cursor char position in the active line.
|
||||
// - CanvasState should ensure `inputs` is ideally never empty (e.g., init with `vec!["".to_string()]`).
|
||||
|
||||
pub fn render_canvas(
|
||||
f: &mut Frame,
|
||||
area: Rect, // Total area for this component
|
||||
form_state: &impl CanvasState,
|
||||
fields: &[&str], // fields[0] is an optional label for the editor
|
||||
inputs: &[&String], // These are the lines of text for the editor
|
||||
current_field_idx: &usize, // Active_line_idx within editor_lines
|
||||
theme: &Theme,
|
||||
is_edit_mode: bool,
|
||||
highlight_state: &HighlightState,
|
||||
) -> Option<Rect> {
|
||||
// Interpret parameters for the multiline editor context
|
||||
let editor_label_str = fields.get(0).filter(|s| !s.is_empty()).map(|s| *s);
|
||||
let editor_lines = inputs; // These are the lines of text
|
||||
let active_line_idx = *current_field_idx; // Current active line
|
||||
let cursor_char_pos_in_active_line = form_state.current_cursor_pos();
|
||||
|
||||
// Determine layout: optional label column, editor column
|
||||
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), // Label width
|
||||
Constraint::Percentage(70), // Editor width
|
||||
])
|
||||
.split(area);
|
||||
(Some(chunks[0]), chunks[1])
|
||||
} else {
|
||||
(None, area) // Editor takes full area if no label
|
||||
};
|
||||
|
||||
// --- Input Block Setup ---
|
||||
let num_lines_in_editor = editor_lines.len().max(1); // Ensure at least 1 line height
|
||||
let editor_content_height = num_lines_in_editor as u16;
|
||||
let desired_input_block_height = editor_content_height + 2; // +2 for borders
|
||||
let input_block_actual_height =
|
||||
desired_input_block_height.min(editor_area_rect.height);
|
||||
|
||||
let border_style = if form_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: input_block_actual_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; // No space to render text or cursor
|
||||
}
|
||||
|
||||
let line_rows_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
std::iter::repeat(Constraint::Length(1))
|
||||
.take(num_lines_in_editor)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.split(text_lines_render_area);
|
||||
|
||||
let mut active_line_render_rect = None;
|
||||
|
||||
// --- Render Label (if any) ---
|
||||
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 // Align with first text line
|
||||
} 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), // Prevent overflow
|
||||
height: 1,
|
||||
};
|
||||
if label_render_rect.area() > 0 {
|
||||
f.render_widget(label_paragraph, label_render_rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Render Text Lines and Cursor ---
|
||||
for (line_idx, current_line_str_ref) in editor_lines.iter().enumerate() {
|
||||
if line_idx >= line_rows_layout.len() { // Not enough vertical space in layout
|
||||
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; // Stores the styled Line for the paragraph
|
||||
|
||||
// Define base styles for text
|
||||
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);
|
||||
|
||||
// Determine the base style for text on this line (active/inactive)
|
||||
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 = active_line_idx;
|
||||
let current_char_idx_on_line = cursor_char_pos_in_active_line;
|
||||
|
||||
let selection_start_ln = min(anchor_ln_idx, current_ln_idx);
|
||||
let selection_end_ln = max(anchor_ln_idx, current_ln_idx);
|
||||
|
||||
// Char pos on the line that starts the selection
|
||||
let sel_start_char = if anchor_ln_idx == current_ln_idx { min(anchor_char_idx, current_char_idx_on_line) }
|
||||
else if anchor_ln_idx < current_ln_idx { anchor_char_idx }
|
||||
else { current_char_idx_on_line };
|
||||
// Char pos on the line that ends the selection
|
||||
let sel_end_char = if anchor_ln_idx == current_ln_idx { max(anchor_char_idx, current_char_idx_on_line) }
|
||||
else if anchor_ln_idx < current_ln_idx { current_char_idx_on_line }
|
||||
else { anchor_char_idx };
|
||||
|
||||
if line_idx >= selection_start_ln && line_idx <= selection_end_ln {
|
||||
// This line is part of the selection
|
||||
let text_style_within_selection = if is_edit_mode { style_active_text } else { style_inactive_text };
|
||||
|
||||
if selection_start_ln == selection_end_ln { // Single-line selection
|
||||
let start_h = sel_start_char.min(text_len);
|
||||
let end_h = sel_end_char.min(text_len); // end_h is inclusive char index
|
||||
|
||||
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 { // Start of multi-line selection
|
||||
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 { // End of multi-line selection
|
||||
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 { // Middle of multi-line selection
|
||||
line_widget_content = Line::from(Span::styled(text_content, style_highlight_selection_bg));
|
||||
}
|
||||
} else { // Line is outside character-wise selection
|
||||
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 = current_line_target_rect.width.saturating_sub(0); // Allow cursor at end of line
|
||||
let cursor_x_offset = (cursor_char_pos_in_active_line as u16).min(max_cursor_x_offset);
|
||||
let cursor_x = current_line_target_rect.x + cursor_x_offset;
|
||||
let cursor_y = current_line_target_rect.y;
|
||||
f.set_cursor_position((cursor_x, cursor_y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle case for an empty editor (e.g., initial state with one empty line)
|
||||
if editor_lines.is_empty() && num_lines_in_editor == 1 && active_line_idx == 0 && !line_rows_layout.is_empty() {
|
||||
let target_rect = line_rows_layout[0];
|
||||
// Render an empty paragraph if needed, or just set cursor
|
||||
// The loop above wouldn't run, so active_line_render_rect wouldn't be set.
|
||||
active_line_render_rect = Some(target_rect);
|
||||
if is_edit_mode {
|
||||
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, cursor_y));
|
||||
}
|
||||
}
|
||||
|
||||
active_line_render_rect
|
||||
}
|
||||
324
client/src/components/handlers/multi_canvas.rs
Normal file
324
client/src/components/handlers/multi_canvas.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
// 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::multiline_editor_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
|
||||
}
|
||||
@@ -7,3 +7,4 @@ pub mod intro;
|
||||
pub mod add_table;
|
||||
pub mod add_logic;
|
||||
pub mod canvas_state;
|
||||
pub mod multi_canvas_state;
|
||||
|
||||
321
client/src/state/pages/multi_canvas_state.rs
Normal file
321
client/src/state/pages/multi_canvas_state.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
// src/state/pages/multi_canvas_state.rs (or your chosen filename)
|
||||
use std::cmp::min;
|
||||
|
||||
/// Trait for managing the state of a multiline text editor.
|
||||
pub trait MultilineEditorState {
|
||||
fn lines(&self) -> &Vec<String>;
|
||||
fn active_line_index(&self) -> usize;
|
||||
fn cursor_char_pos_in_active_line(&self) -> usize;
|
||||
fn has_unsaved_changes(&self) -> bool;
|
||||
fn get_label(&self) -> Option<&str>; // Optional label for the editor
|
||||
|
||||
// Methods to be called by your event handling logic to modify the state
|
||||
fn set_lines(&mut self, lines: Vec<String>);
|
||||
fn set_active_line_index(&mut self, index: usize);
|
||||
fn set_cursor_char_pos_in_active_line(&mut self, pos: usize);
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool);
|
||||
|
||||
// Basic editing operations
|
||||
fn insert_char_at_cursor(&mut self, ch: char);
|
||||
fn delete_char_before_cursor(&mut self); // Handles Backspace
|
||||
fn delete_char_at_cursor(&mut self); // Handles Delete
|
||||
fn insert_newline_at_cursor(&mut self); // Handles Enter
|
||||
fn move_cursor_left(&mut self);
|
||||
fn move_cursor_right(&mut self);
|
||||
fn move_cursor_up(&mut self);
|
||||
fn move_cursor_down(&mut self);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BasicMultilineEditor {
|
||||
pub lines: Vec<String>,
|
||||
pub active_line_idx: usize,
|
||||
pub cursor_char_pos: usize, // Cursor char position in the active_line_idx
|
||||
pub unsaved_changes: bool,
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for BasicMultilineEditor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lines: vec!["".to_string()], // Start with one empty line
|
||||
active_line_idx: 0,
|
||||
cursor_char_pos: 0,
|
||||
unsaved_changes: false,
|
||||
label: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BasicMultilineEditor {
|
||||
pub fn new(initial_text: Option<String>, label: Option<String>) -> Self {
|
||||
let mut lines_vec = match initial_text {
|
||||
Some(text) if !text.is_empty() => {
|
||||
text.lines().map(String::from).collect()
|
||||
}
|
||||
_ => Vec::new(), // Handle empty initial text
|
||||
};
|
||||
|
||||
// Ensure there's always at least one line, even if it's empty
|
||||
if lines_vec.is_empty() {
|
||||
lines_vec.push("".to_string());
|
||||
}
|
||||
|
||||
Self {
|
||||
lines: lines_vec,
|
||||
active_line_idx: 0,
|
||||
cursor_char_pos: 0,
|
||||
unsaved_changes: false,
|
||||
label,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get mutable access to the current line, ensuring it exists
|
||||
#[allow(dead_code)] // May not be used directly if functions use self.lines.get_mut directly
|
||||
fn current_line_mut(&mut self) -> Option<&mut String> {
|
||||
self.lines.get_mut(self.active_line_idx)
|
||||
}
|
||||
|
||||
// Helper to get immutable access to the current line
|
||||
#[allow(dead_code)] // May not be used directly
|
||||
fn current_line(&self) -> Option<&String> {
|
||||
self.lines.get(self.active_line_idx)
|
||||
}
|
||||
|
||||
// This method is crucial and used by others.
|
||||
fn clamp_cursor_to_current_line(&mut self) {
|
||||
let active_idx = self.active_line_idx; // Read before any potential borrow of self.lines
|
||||
let current_cursor = self.cursor_char_pos; // Read before any potential borrow
|
||||
|
||||
let new_cursor_val = if let Some(line) = self.lines.get(active_idx) { // Immutable borrow
|
||||
min(current_cursor, line.chars().count())
|
||||
} else {
|
||||
0 // Should not happen if lines always has at least one element and active_idx is valid
|
||||
};
|
||||
self.cursor_char_pos = new_cursor_val; // Assign after borrow
|
||||
}
|
||||
}
|
||||
|
||||
impl MultilineEditorState for BasicMultilineEditor {
|
||||
fn lines(&self) -> &Vec<String> {
|
||||
&self.lines
|
||||
}
|
||||
|
||||
fn active_line_index(&self) -> usize {
|
||||
self.active_line_idx
|
||||
}
|
||||
|
||||
fn cursor_char_pos_in_active_line(&self) -> usize {
|
||||
self.cursor_char_pos
|
||||
}
|
||||
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.unsaved_changes
|
||||
}
|
||||
|
||||
fn get_label(&self) -> Option<&str> {
|
||||
self.label.as_deref()
|
||||
}
|
||||
|
||||
fn set_lines(&mut self, mut new_lines: Vec<String>) {
|
||||
if new_lines.is_empty() {
|
||||
new_lines.push("".to_string());
|
||||
}
|
||||
self.lines = new_lines;
|
||||
self.active_line_idx =
|
||||
min(self.active_line_idx, self.lines.len().saturating_sub(1));
|
||||
self.clamp_cursor_to_current_line();
|
||||
}
|
||||
|
||||
fn set_active_line_index(&mut self, index: usize) {
|
||||
self.active_line_idx =
|
||||
min(index, self.lines.len().saturating_sub(1));
|
||||
self.clamp_cursor_to_current_line();
|
||||
}
|
||||
|
||||
fn set_cursor_char_pos_in_active_line(&mut self, pos: usize) {
|
||||
self.cursor_char_pos = pos;
|
||||
self.clamp_cursor_to_current_line();
|
||||
}
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.unsaved_changes = changed;
|
||||
}
|
||||
|
||||
fn insert_char_at_cursor(&mut self, ch: char) {
|
||||
let char_pos = self.cursor_char_pos; // Read before mutable borrow of self.lines
|
||||
let active_idx = self.active_line_idx;
|
||||
let mut modified = false;
|
||||
|
||||
if let Some(line) = self.lines.get_mut(active_idx) {
|
||||
let byte_idx = line
|
||||
.char_indices()
|
||||
.nth(char_pos)
|
||||
.map_or(line.len(), |(idx, _)| idx);
|
||||
line.insert(byte_idx, ch);
|
||||
modified = true;
|
||||
// `line` borrow ends here
|
||||
}
|
||||
|
||||
if modified {
|
||||
self.cursor_char_pos += 1; // Modify after `line` borrow ended
|
||||
self.unsaved_changes = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_char_before_cursor(&mut self) {
|
||||
// Backspace
|
||||
if self.cursor_char_pos > 0 {
|
||||
let char_pos_to_delete = self.cursor_char_pos - 1;
|
||||
let active_idx = self.active_line_idx;
|
||||
let mut modified_in_line = false;
|
||||
|
||||
if let Some(line) = self.lines.get_mut(active_idx) {
|
||||
if let Some((byte_idx, _)) =
|
||||
line.char_indices().nth(char_pos_to_delete)
|
||||
{
|
||||
line.remove(byte_idx);
|
||||
modified_in_line = true;
|
||||
}
|
||||
// `line` borrow ends here
|
||||
}
|
||||
|
||||
if modified_in_line {
|
||||
self.cursor_char_pos -= 1;
|
||||
self.unsaved_changes = true;
|
||||
}
|
||||
} else if self.active_line_idx > 0 {
|
||||
// At the beginning of a line (not the first line), merge with previous
|
||||
let idx_to_remove = self.active_line_idx;
|
||||
// Ensure the line to remove exists and there's a previous line
|
||||
if idx_to_remove < self.lines.len() {
|
||||
let line_to_append_content = self.lines.remove(idx_to_remove);
|
||||
// The line we are merging into is now at idx_to_remove - 1
|
||||
let target_line_idx = idx_to_remove - 1;
|
||||
|
||||
let mut merged = false;
|
||||
if let Some(target_line) = self.lines.get_mut(target_line_idx) {
|
||||
let original_target_len = target_line.chars().count();
|
||||
target_line.push_str(&line_to_append_content);
|
||||
// target_line borrow ends here
|
||||
self.cursor_char_pos = original_target_len; // Set cursor after borrow
|
||||
merged = true;
|
||||
}
|
||||
|
||||
if merged {
|
||||
self.active_line_idx = target_line_idx; // Update active_line_idx
|
||||
self.unsaved_changes = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_char_at_cursor(&mut self) {
|
||||
// Delete key
|
||||
let current_char_pos = self.cursor_char_pos;
|
||||
let current_active_line_idx = self.active_line_idx;
|
||||
let mut processed = false;
|
||||
|
||||
// Try to delete within the current line
|
||||
if let Some(line_mut) = self.lines.get_mut(current_active_line_idx) {
|
||||
if current_char_pos < line_mut.chars().count() {
|
||||
if let Some((byte_idx, _)) =
|
||||
line_mut.char_indices().nth(current_char_pos)
|
||||
{
|
||||
line_mut.remove(byte_idx);
|
||||
self.unsaved_changes = true;
|
||||
// Cursor position does not change
|
||||
}
|
||||
processed = true;
|
||||
}
|
||||
// line_mut borrow ends here
|
||||
}
|
||||
|
||||
// If not processed (i.e., cursor was at end of line), try to merge with next
|
||||
if !processed {
|
||||
if current_active_line_idx < self.lines.len() - 1 {
|
||||
// There is a next line
|
||||
let next_line_content =
|
||||
self.lines.remove(current_active_line_idx + 1);
|
||||
// self.lines.remove() has happened.
|
||||
// Now get the current line again to append to it.
|
||||
if let Some(current_line_again) =
|
||||
self.lines.get_mut(current_active_line_idx)
|
||||
{
|
||||
current_line_again.push_str(&next_line_content);
|
||||
self.unsaved_changes = true;
|
||||
// Cursor position (self.cursor_char_pos) remains at current_char_pos,
|
||||
// which was the end of the original current line.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_newline_at_cursor(&mut self) {
|
||||
// Enter key
|
||||
let char_pos = self.cursor_char_pos;
|
||||
let current_active_idx = self.active_line_idx;
|
||||
let mut rest_of_line_opt: Option<String> = None;
|
||||
|
||||
if let Some(line_mut_ref) = self.lines.get_mut(current_active_idx) {
|
||||
let byte_idx = line_mut_ref
|
||||
.char_indices()
|
||||
.nth(char_pos)
|
||||
.map_or(line_mut_ref.len(), |(idx, _)| idx);
|
||||
rest_of_line_opt = Some(line_mut_ref.split_off(byte_idx));
|
||||
// line_mut_ref's borrow ends here
|
||||
}
|
||||
|
||||
if let Some(rest_of_line) = rest_of_line_opt {
|
||||
let new_active_idx = current_active_idx + 1;
|
||||
self.lines.insert(new_active_idx, rest_of_line);
|
||||
self.active_line_idx = new_active_idx; // Update after insert
|
||||
self.cursor_char_pos = 0;
|
||||
self.unsaved_changes = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
if self.cursor_char_pos > 0 {
|
||||
self.cursor_char_pos -= 1;
|
||||
} else if self.active_line_idx > 0 {
|
||||
// Move to end of previous line
|
||||
self.active_line_idx -= 1;
|
||||
// active_line_idx is updated, now clamp/set cursor_char_pos
|
||||
let new_cursor_pos = self
|
||||
.lines
|
||||
.get(self.active_line_idx)
|
||||
.map_or(0, |prev_line| prev_line.chars().count());
|
||||
self.cursor_char_pos = new_cursor_pos;
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let current_line_len = self
|
||||
.lines
|
||||
.get(self.active_line_idx)
|
||||
.map_or(0, |line| line.chars().count());
|
||||
|
||||
if self.cursor_char_pos < current_line_len {
|
||||
self.cursor_char_pos += 1;
|
||||
} else if self.active_line_idx < self.lines.len() - 1 {
|
||||
// Move to start of next line
|
||||
self.active_line_idx += 1;
|
||||
self.cursor_char_pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_up(&mut self) {
|
||||
if self.active_line_idx > 0 {
|
||||
self.active_line_idx -= 1;
|
||||
self.clamp_cursor_to_current_line(); // Relies on updated active_line_idx
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_down(&mut self) {
|
||||
if self.active_line_idx < self.lines.len() - 1 {
|
||||
self.active_line_idx += 1;
|
||||
self.clamp_cursor_to_current_line(); // Relies on updated active_line_idx
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user