193 lines
7.8 KiB
Rust
193 lines
7.8 KiB
Rust
// src/components/handlers/canvas.rs
|
|
|
|
use ratatui::{
|
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
|
style::{Modifier, Style},
|
|
text::{Line, Span},
|
|
widgets::{Block, Borders, Paragraph},
|
|
Frame,
|
|
};
|
|
use crate::config::colors::themes::Theme;
|
|
use crate::state::app::highlight::HighlightState;
|
|
use crate::state::pages::canvas_state::CanvasState;
|
|
use std::cmp::{max, min};
|
|
|
|
pub fn render_canvas(
|
|
f: &mut Frame,
|
|
area: Rect,
|
|
form_state: &impl CanvasState,
|
|
fields: &[&str],
|
|
current_field_idx: &usize,
|
|
inputs: &[&String],
|
|
theme: &Theme,
|
|
is_edit_mode: bool,
|
|
highlight_state: &HighlightState,
|
|
) -> Option<Rect> {
|
|
let columns = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
|
.split(area);
|
|
|
|
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 = Block::default()
|
|
.borders(Borders::ALL)
|
|
.border_style(border_style)
|
|
.style(Style::default().bg(theme.bg));
|
|
|
|
let input_block = Rect {
|
|
x: columns[1].x,
|
|
y: columns[1].y,
|
|
width: columns[1].width,
|
|
height: fields.len() as u16 + 2,
|
|
};
|
|
|
|
f.render_widget(&input_container, input_block);
|
|
|
|
let input_area = input_container.inner(input_block);
|
|
let input_rows = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(vec![Constraint::Length(1); fields.len()])
|
|
.split(input_area);
|
|
|
|
let mut active_field_input_rect = None;
|
|
|
|
for (i, field) in fields.iter().enumerate() {
|
|
let label = Paragraph::new(Line::from(Span::styled(
|
|
format!("{}:", field),
|
|
Style::default().fg(theme.fg),
|
|
)));
|
|
f.render_widget(
|
|
label,
|
|
Rect {
|
|
x: columns[0].x,
|
|
y: input_block.y + 1 + i as u16,
|
|
width: columns[0].width,
|
|
height: 1,
|
|
},
|
|
);
|
|
}
|
|
|
|
for (i, _input) in inputs.iter().enumerate() {
|
|
let is_active = i == *current_field_idx;
|
|
let current_cursor_pos = form_state.current_cursor_pos();
|
|
|
|
// Use the trait method to get display value
|
|
let text = form_state.get_display_value_for_field(i);
|
|
let text_len = text.chars().count();
|
|
let line: Line;
|
|
|
|
match highlight_state {
|
|
HighlightState::Off => {
|
|
line = Line::from(Span::styled(
|
|
text,
|
|
if is_active {
|
|
Style::default().fg(theme.highlight)
|
|
} else {
|
|
Style::default().fg(theme.fg)
|
|
},
|
|
));
|
|
}
|
|
HighlightState::Characterwise { anchor } => {
|
|
let (anchor_field, anchor_char) = *anchor;
|
|
let start_field = min(anchor_field, *current_field_idx);
|
|
let end_field = max(anchor_field, *current_field_idx);
|
|
|
|
let (start_char, end_char) = if anchor_field == *current_field_idx {
|
|
(min(anchor_char, current_cursor_pos), max(anchor_char, current_cursor_pos))
|
|
} else if anchor_field < *current_field_idx {
|
|
(anchor_char, current_cursor_pos)
|
|
} else {
|
|
(current_cursor_pos, anchor_char)
|
|
};
|
|
|
|
let highlight_style = Style::default().fg(theme.highlight).bg(theme.highlight_bg).add_modifier(Modifier::BOLD);
|
|
let normal_style_in_highlight = Style::default().fg(theme.highlight);
|
|
let normal_style_outside = Style::default().fg(theme.fg);
|
|
|
|
if i >= start_field && i <= end_field {
|
|
if start_field == end_field {
|
|
let clamped_start = start_char.min(text_len);
|
|
let clamped_end = end_char.min(text_len);
|
|
|
|
let before: String = text.chars().take(clamped_start).collect();
|
|
let highlighted: String = text.chars().skip(clamped_start).take(clamped_end.saturating_sub(clamped_start) + 1).collect();
|
|
let after: String = text.chars().skip(clamped_end + 1).collect();
|
|
|
|
line = Line::from(vec![
|
|
Span::styled(before, normal_style_in_highlight),
|
|
Span::styled(highlighted, highlight_style),
|
|
Span::styled(after, normal_style_in_highlight),
|
|
]);
|
|
} else if i == start_field {
|
|
let safe_start = start_char.min(text_len);
|
|
let before: String = text.chars().take(safe_start).collect();
|
|
let highlighted: String = text.chars().skip(safe_start).collect();
|
|
line = Line::from(vec![
|
|
Span::styled(before, normal_style_in_highlight),
|
|
Span::styled(highlighted, highlight_style),
|
|
]);
|
|
} else if i == end_field {
|
|
let safe_end_inclusive = if text_len > 0 { end_char.min(text_len - 1) } else { 0 };
|
|
let highlighted: String = text.chars().take(safe_end_inclusive + 1).collect();
|
|
let after: String = text.chars().skip(safe_end_inclusive + 1).collect();
|
|
line = Line::from(vec![
|
|
Span::styled(highlighted, highlight_style),
|
|
Span::styled(after, normal_style_in_highlight),
|
|
]);
|
|
} else {
|
|
line = Line::from(Span::styled(text, highlight_style));
|
|
}
|
|
} else {
|
|
line = Line::from(Span::styled(
|
|
text,
|
|
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
|
));
|
|
}
|
|
}
|
|
HighlightState::Linewise { anchor_line } => {
|
|
let start_field = min(*anchor_line, *current_field_idx);
|
|
let end_field = max(*anchor_line, *current_field_idx);
|
|
let highlight_style = Style::default().fg(theme.highlight).bg(theme.highlight_bg).add_modifier(Modifier::BOLD);
|
|
let normal_style_in_highlight = Style::default().fg(theme.highlight);
|
|
let normal_style_outside = Style::default().fg(theme.fg);
|
|
|
|
if i >= start_field && i <= end_field {
|
|
line = Line::from(Span::styled(text, highlight_style));
|
|
} else {
|
|
line = Line::from(Span::styled(
|
|
text,
|
|
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
let input_display = Paragraph::new(line).alignment(Alignment::Left);
|
|
f.render_widget(input_display, input_rows[i]);
|
|
|
|
if is_active {
|
|
active_field_input_rect = Some(input_rows[i]);
|
|
|
|
// --- CORRECTED CURSOR POSITIONING LOGIC ---
|
|
// Use the new generic trait method to check for an override.
|
|
let cursor_x = if form_state.has_display_override(i) {
|
|
// If an override exists, place the cursor at the end.
|
|
input_rows[i].x + text.chars().count() as u16
|
|
} else {
|
|
// Otherwise, use the real cursor position.
|
|
input_rows[i].x + form_state.current_cursor_pos() as u16
|
|
};
|
|
let cursor_y = input_rows[i].y;
|
|
f.set_cursor_position((cursor_x, cursor_y));
|
|
}
|
|
}
|
|
|
|
active_field_input_rect
|
|
}
|