diff --git a/canvas/examples/canvas_cursor_auto.rs b/canvas/examples/canvas_cursor_auto.rs new file mode 100644 index 0000000..e83d673 --- /dev/null +++ b/canvas/examples/canvas_cursor_auto.rs @@ -0,0 +1,741 @@ +// examples/canvas-cursor-auto.rs +//! Demonstrates automatic cursor management with the canvas library +//! +//! This example REQUIRES the `cursor-style` feature to compile. +//! +//! Run with: +//! cargo run --example canvas_cursor_auto --features "gui,cursor-style" +//! +//! This will fail without cursor-style: +//! cargo run --example canvas-cursor-auto --features "gui" + +// REQUIRE cursor-style feature - example won't compile without it +#[cfg(not(feature = "cursor-style"))] +compile_error!( + "This example requires the 'cursor-style' feature. \ + Run with: cargo run --example canvas-cursor-auto --features \"gui,cursor-style\"" +); + +use std::io; +use crossterm::{ + event::{ + self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, + }, + execute, + terminal::{ + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, + }, +}; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, + Frame, Terminal, +}; + +use canvas::{ + canvas::{ + gui::render_canvas_default, + modes::{AppMode, ModeManager, HighlightState}, + CursorManager, // This import only exists when cursor-style feature is enabled + }, + DataProvider, FormEditor, +}; + +// Enhanced FormEditor that demonstrates automatic cursor management +struct AutoCursorFormEditor { + editor: FormEditor, + has_unsaved_changes: bool, + debug_message: String, + command_buffer: String, // For multi-key vim commands like "gg" +} + +impl AutoCursorFormEditor { + fn new(data_provider: D) -> Self { + Self { + editor: FormEditor::new(data_provider), + has_unsaved_changes: false, + debug_message: "🎯 Automatic Cursor Demo - cursor-style feature enabled!".to_string(), + command_buffer: String::new(), + } + } + + // === COMMAND BUFFER HANDLING === + + fn clear_command_buffer(&mut self) { + self.command_buffer.clear(); + } + + fn add_to_command_buffer(&mut self, ch: char) { + self.command_buffer.push(ch); + } + + fn get_command_buffer(&self) -> &str { + &self.command_buffer + } + + fn has_pending_command(&self) -> bool { + !self.command_buffer.is_empty() + } + + // === VISUAL/HIGHLIGHT MODE SUPPORT === + + + fn enter_visual_mode(&mut self) { + // Use the library method instead of manual state setting + self.editor.enter_highlight_mode(); + self.debug_message = "πŸ”₯ VISUAL MODE - Cursor: Blinking Block β–ˆ".to_string(); + } + + fn enter_visual_line_mode(&mut self) { + // Use the library method instead of manual state setting + self.editor.enter_highlight_line_mode(); + self.debug_message = "πŸ”₯ VISUAL LINE MODE - Cursor: Blinking Block β–ˆ".to_string(); + } + + fn exit_visual_mode(&mut self) { + // Use the library method + self.editor.exit_highlight_mode(); + self.debug_message = "πŸ”’ NORMAL MODE - Cursor: Steady Block β–ˆ".to_string(); + } + + fn update_visual_selection(&mut self) { + if self.editor.is_highlight_mode() { + use canvas::canvas::state::SelectionState; + match self.editor.selection_state() { + SelectionState::Characterwise { anchor } => { + self.debug_message = format!( + "🎯 Visual selection: anchor=({},{}) current=({},{}) - Cursor: Blinking Block β–ˆ", + anchor.0, anchor.1, + self.editor.current_field(), + self.editor.cursor_position() + ); + } + SelectionState::Linewise { anchor_field } => { + self.debug_message = format!( + "🎯 Visual LINE selection: anchor={} current={} - Cursor: Blinking Block β–ˆ", + anchor_field, + self.editor.current_field() + ); + } + _ => {} + } + } + } + + // === ENHANCED MOVEMENT WITH VISUAL UPDATES === + + fn move_left(&mut self) { + self.editor.move_left(); + self.update_visual_selection(); + } + + fn move_right(&mut self) { + self.editor.move_right(); + self.update_visual_selection(); + } + + fn move_up(&mut self) { + self.editor.move_up(); + self.update_visual_selection(); + } + + fn move_down(&mut self) { + self.editor.move_down(); + self.update_visual_selection(); + } + + fn move_word_next(&mut self) { + self.editor.move_word_next(); + self.update_visual_selection(); + } + + fn move_word_prev(&mut self) { + self.editor.move_word_prev(); + self.update_visual_selection(); + } + + fn move_word_end(&mut self) { + self.editor.move_word_end(); + self.update_visual_selection(); + } + + fn move_word_end_prev(&mut self) { + self.editor.move_word_end_prev(); + self.update_visual_selection(); + } + + fn move_line_start(&mut self) { + self.editor.move_line_start(); + self.update_visual_selection(); + } + + fn move_line_end(&mut self) { + self.editor.move_line_end(); + self.update_visual_selection(); + } + + fn move_first_line(&mut self) { + self.editor.move_first_line(); + self.update_visual_selection(); + } + + fn move_last_line(&mut self) { + self.editor.move_last_line(); + self.update_visual_selection(); + } + + fn prev_field(&mut self) { + self.editor.prev_field(); + self.update_visual_selection(); + } + + fn next_field(&mut self) { + self.editor.next_field(); + self.update_visual_selection(); + } + + // === DELETE OPERATIONS === + + fn delete_backward(&mut self) -> anyhow::Result<()> { + let result = self.editor.delete_backward(); + if result.is_ok() { + self.has_unsaved_changes = true; + self.debug_message = "⌫ Deleted character backward".to_string(); + } + Ok(result?) + } + + fn delete_forward(&mut self) -> anyhow::Result<()> { + let result = self.editor.delete_forward(); + if result.is_ok() { + self.has_unsaved_changes = true; + self.debug_message = "⌦ Deleted character forward".to_string(); + } + Ok(result?) + } + + // === MODE TRANSITIONS WITH AUTOMATIC CURSOR MANAGEMENT === + + fn enter_edit_mode(&mut self) { + self.editor.enter_edit_mode(); // 🎯 Library automatically sets cursor to bar | + self.debug_message = "✏️ INSERT MODE - Cursor: Steady Bar |".to_string(); + } + + fn enter_append_mode(&mut self) { + self.editor.enter_append_mode(); // 🎯 Library automatically positions cursor and sets mode + self.debug_message = "✏️ INSERT (append) - Cursor: Steady Bar |".to_string(); + } + + fn exit_edit_mode(&mut self) { + self.editor.exit_edit_mode(); // 🎯 Library automatically sets cursor to block β–ˆ + self.exit_visual_mode(); + self.debug_message = "πŸ”’ NORMAL MODE - Cursor: Steady Block β–ˆ".to_string(); + } + + fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { + let result = self.editor.insert_char(ch); + if result.is_ok() { + self.has_unsaved_changes = true; + } + Ok(result?) + } + + // === MANUAL CURSOR OVERRIDE DEMONSTRATION === + + /// Demonstrate manual cursor control (for advanced users) + fn demo_manual_cursor_control(&mut self) -> std::io::Result<()> { + // Users can still manually control cursor if needed + CursorManager::update_for_mode(AppMode::Command)?; + self.debug_message = "πŸ”§ Manual override: Command cursor _".to_string(); + Ok(()) + } + + fn restore_automatic_cursor(&mut self) -> std::io::Result<()> { + // Restore automatic cursor based on current mode + CursorManager::update_for_mode(self.editor.mode())?; + self.debug_message = "🎯 Restored automatic cursor management".to_string(); + Ok(()) + } + + // === DELEGATE TO ORIGINAL EDITOR === + + fn current_field(&self) -> usize { + self.editor.current_field() + } + + fn cursor_position(&self) -> usize { + self.editor.cursor_position() + } + + fn mode(&self) -> AppMode { + self.editor.mode() + } + + fn current_text(&self) -> &str { + self.editor.current_text() + } + + fn data_provider(&self) -> &D { + self.editor.data_provider() + } + + fn ui_state(&self) -> &canvas::EditorState { + self.editor.ui_state() + } + + fn set_mode(&mut self, mode: AppMode) { + self.editor.set_mode(mode); // 🎯 Library automatically updates cursor + if mode != AppMode::Highlight { + self.exit_visual_mode(); + } + } + + // === STATUS AND DEBUG === + + fn set_debug_message(&mut self, msg: String) { + self.debug_message = msg; + } + + fn debug_message(&self) -> &str { + &self.debug_message + } + + fn has_unsaved_changes(&self) -> bool { + self.has_unsaved_changes + } +} + +// Demo form data with interesting text for cursor demonstration +struct CursorDemoData { + fields: Vec<(String, String)>, +} + +impl CursorDemoData { + fn new() -> Self { + Self { + fields: vec![ + ("πŸ‘€ Name".to_string(), "John-Paul McDonald".to_string()), + ("πŸ“§ Email".to_string(), "user@example-domain.com".to_string()), + ("πŸ“± Phone".to_string(), "+1 (555) 123-4567".to_string()), + ("🏠 Address".to_string(), "123 Main St, Apt 4B".to_string()), + ("🏷️ Tags".to_string(), "urgent,important,follow-up".to_string()), + ("πŸ“ Notes".to_string(), "Watch the cursor change! Normal=β–ˆ Insert=| Visual=blinkingβ–ˆ".to_string()), + ("🎯 Cursor Demo".to_string(), "Press 'i' for insert, 'v' for visual, 'Esc' for normal".to_string()), + ], + } + } +} + +impl DataProvider for CursorDemoData { + fn field_count(&self) -> usize { + self.fields.len() + } + + fn field_name(&self, index: usize) -> &str { + &self.fields[index].0 + } + + fn field_value(&self, index: usize) -> &str { + &self.fields[index].1 + } + + fn set_field_value(&mut self, index: usize, value: String) { + self.fields[index].1 = value; + } + + fn supports_autocomplete(&self, _field_index: usize) -> bool { + false + } + + fn display_value(&self, _index: usize) -> Option<&str> { + None + } +} + +/// Automatic cursor management demonstration +/// Features the CursorManager directly to show it's working +fn handle_key_press( + key: KeyCode, + modifiers: KeyModifiers, + editor: &mut AutoCursorFormEditor, +) -> anyhow::Result { + let mode = editor.mode(); + + // Quit handling + if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) + || (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) + || key == KeyCode::F(10) + { + return Ok(false); + } + + match (mode, key, modifiers) { + // === MODE TRANSITIONS WITH AUTOMATIC CURSOR MANAGEMENT === + (AppMode::ReadOnly, KeyCode::Char('i'), _) => { + editor.enter_edit_mode(); // 🎯 Automatic: cursor becomes bar | + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('a'), _) => { + editor.enter_append_mode(); + editor.set_debug_message("✏️ INSERT (append) - Cursor: Steady Bar |".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('A'), _) => { + editor.move_line_end(); + editor.enter_edit_mode(); // 🎯 Automatic: cursor becomes bar | + editor.set_debug_message("✏️ INSERT (end of line) - Cursor: Steady Bar |".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('o'), _) => { + editor.move_line_end(); + editor.enter_edit_mode(); // 🎯 Automatic: cursor becomes bar | + editor.set_debug_message("✏️ INSERT (open line) - Cursor: Steady Bar |".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('v'), _) => { + editor.enter_visual_mode(); // 🎯 Automatic: cursor becomes blinking block + editor.clear_command_buffer(); + } + (AppMode::ReadOnly, KeyCode::Char('V'), _) => { + editor.enter_visual_line_mode(); // 🎯 Automatic: cursor becomes blinking block + editor.clear_command_buffer(); + } + (_, KeyCode::Esc, _) => { + editor.exit_edit_mode(); // 🎯 Automatic: cursor becomes steady block + editor.clear_command_buffer(); + } + + // === CURSOR MANAGEMENT DEMONSTRATION === + (AppMode::ReadOnly, KeyCode::F(1), _) => { + editor.demo_manual_cursor_control()?; + } + (AppMode::ReadOnly, KeyCode::F(2), _) => { + editor.restore_automatic_cursor()?; + } + + // === MOVEMENT: VIM-STYLE NAVIGATION === + + // Basic movement (hjkl and arrows) + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('h'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Left, _) => { + editor.move_left(); + editor.set_debug_message("← left".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('l'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Right, _) => { + editor.move_right(); + editor.set_debug_message("β†’ right".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('j'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Down, _) => { + editor.move_down(); + editor.set_debug_message("↓ next field".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('k'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Up, _) => { + editor.move_up(); + editor.set_debug_message("↑ previous field".to_string()); + editor.clear_command_buffer(); + } + + // Word movement + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('w'), _) => { + editor.move_word_next(); + editor.set_debug_message("w: next word start".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('b'), _) => { + editor.move_word_prev(); + editor.set_debug_message("b: previous word start".to_string()); + editor.clear_command_buffer(); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('e'), _) => { + editor.move_word_end(); + editor.set_debug_message("e: word end".to_string()); + editor.clear_command_buffer(); + } + + // Line movement + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('0'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Home, _) => { + editor.move_line_start(); + editor.set_debug_message("0: line start".to_string()); + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('$'), _) + | (AppMode::ReadOnly | AppMode::Highlight, KeyCode::End, _) => { + editor.move_line_end(); + editor.set_debug_message("$: line end".to_string()); + } + + // Field/document movement + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('g'), _) => { + if editor.get_command_buffer() == "g" { + editor.move_first_line(); + editor.set_debug_message("gg: first field".to_string()); + editor.clear_command_buffer(); + } else { + editor.clear_command_buffer(); + editor.add_to_command_buffer('g'); + editor.set_debug_message("g".to_string()); + } + } + (AppMode::ReadOnly | AppMode::Highlight, KeyCode::Char('G'), _) => { + editor.move_last_line(); + editor.set_debug_message("G: last field".to_string()); + editor.clear_command_buffer(); + } + + // === EDIT MODE MOVEMENT === + (AppMode::Edit, KeyCode::Left, m) if m.contains(KeyModifiers::CONTROL) => { + editor.move_word_prev(); + editor.set_debug_message("Ctrl+← word back".to_string()); + } + (AppMode::Edit, KeyCode::Right, m) if m.contains(KeyModifiers::CONTROL) => { + editor.move_word_next(); + editor.set_debug_message("Ctrl+β†’ word forward".to_string()); + } + (AppMode::Edit, KeyCode::Left, _) => { + editor.move_left(); + } + (AppMode::Edit, KeyCode::Right, _) => { + editor.move_right(); + } + (AppMode::Edit, KeyCode::Up, _) => { + editor.move_up(); + } + (AppMode::Edit, KeyCode::Down, _) => { + editor.move_down(); + } + (AppMode::Edit, KeyCode::Home, _) => { + editor.move_line_start(); + } + (AppMode::Edit, KeyCode::End, _) => { + editor.move_line_end(); + } + + // === DELETE OPERATIONS === + (AppMode::Edit, KeyCode::Backspace, _) => { + editor.delete_backward()?; + } + (AppMode::Edit, KeyCode::Delete, _) => { + editor.delete_forward()?; + } + + // Delete operations in normal mode (vim x) + (AppMode::ReadOnly, KeyCode::Char('x'), _) => { + editor.delete_forward()?; + editor.set_debug_message("x: deleted character".to_string()); + } + (AppMode::ReadOnly, KeyCode::Char('X'), _) => { + editor.delete_backward()?; + editor.set_debug_message("X: deleted character backward".to_string()); + } + + // === TAB NAVIGATION === + (_, KeyCode::Tab, _) => { + editor.next_field(); + editor.set_debug_message("Tab: next field".to_string()); + } + (_, KeyCode::BackTab, _) => { + editor.prev_field(); + editor.set_debug_message("Shift+Tab: previous field".to_string()); + } + + // === CHARACTER INPUT === + (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { + editor.insert_char(c)?; + } + + // === DEBUG/INFO COMMANDS === + (AppMode::ReadOnly, KeyCode::Char('?'), _) => { + editor.set_debug_message(format!( + "Field {}/{}, Pos {}, Mode: {:?} - Cursor managed automatically!", + editor.current_field() + 1, + editor.data_provider().field_count(), + editor.cursor_position(), + editor.mode() + )); + } + + _ => { + if editor.has_pending_command() { + editor.clear_command_buffer(); + editor.set_debug_message("Invalid command sequence".to_string()); + } else { + editor.set_debug_message(format!( + "Unhandled: {:?} + {:?} in {:?} mode", + key, modifiers, mode + )); + } + } + } + + Ok(true) +} + +fn run_app( + terminal: &mut Terminal, + mut editor: AutoCursorFormEditor, +) -> io::Result<()> { + loop { + terminal.draw(|f| ui(f, &editor))?; + + if let Event::Key(key) = event::read()? { + match handle_key_press(key.code, key.modifiers, &mut editor) { + Ok(should_continue) => { + if !should_continue { + break; + } + } + Err(e) => { + editor.set_debug_message(format!("Error: {}", e)); + } + } + } + } + + Ok(()) +} + +fn ui(f: &mut Frame, editor: &AutoCursorFormEditor) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(8), Constraint::Length(10)]) + .split(f.area()); + + render_enhanced_canvas(f, chunks[0], editor); + render_status_and_help(f, chunks[1], editor); +} + +fn render_enhanced_canvas( + f: &mut Frame, + area: ratatui::layout::Rect, + editor: &AutoCursorFormEditor, +) { + render_canvas_default(f, area, &editor.editor); +} + +fn render_status_and_help( + f: &mut Frame, + area: ratatui::layout::Rect, + editor: &AutoCursorFormEditor, +) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Length(7)]) + .split(area); + + // Status bar with cursor information - FIXED VERSION + let mode_text = match editor.mode() { + AppMode::Edit => "INSERT | (bar cursor)", + AppMode::ReadOnly => "NORMAL β–ˆ (block cursor)", + AppMode::Highlight => { + // Use library selection state instead of editor.highlight_state() + use canvas::canvas::state::SelectionState; + match editor.editor.selection_state() { + SelectionState::Characterwise { .. } => "VISUAL β–ˆ (blinking block)", + SelectionState::Linewise { .. } => "VISUAL LINE β–ˆ (blinking block)", + _ => "VISUAL β–ˆ (blinking block)", + } + }, + _ => "NORMAL β–ˆ (block cursor)", + }; + + let status_text = if editor.has_pending_command() { + format!("-- {} -- {} [{}]", mode_text, editor.debug_message(), editor.get_command_buffer()) + } else if editor.has_unsaved_changes() { + format!("-- {} -- [Modified] {}", mode_text, editor.debug_message()) + } else { + format!("-- {} -- {}", mode_text, editor.debug_message()) + }; + + let status = Paragraph::new(Line::from(Span::raw(status_text))) + .block(Block::default().borders(Borders::ALL).title("🎯 Automatic Cursor Status")); + + f.render_widget(status, chunks[0]); + + // Enhanced help text (no changes needed here) + let help_text = match editor.mode() { + AppMode::ReadOnly => { + if editor.has_pending_command() { + match editor.get_command_buffer() { + "g" => "Press 'g' again for first field, or any other key to cancel", + _ => "Pending command... (Esc to cancel)" + } + } else { + "🎯 CURSOR-STYLE DEMO: Normal β–ˆ | Insert | | Visual blinkingβ–ˆ\n\ + Normal: hjkl/arrows=move, w/b/e=words, 0/$=line, gg/G=first/last\n\ + i/a/A=insert, v/b=visual, x/X=delete, ?=info\n\ + F1=demo manual cursor, F2=restore automatic" + } + } + AppMode::Edit => { + "🎯 INSERT MODE - Cursor: | (bar)\n\ + arrows=move, Ctrl+arrows=words, Backspace/Del=delete\n\ + Esc=normal, Tab/Shift+Tab=fields" + } + AppMode::Highlight => { + "🎯 VISUAL MODE - Cursor: β–ˆ (blinking block)\n\ + hjkl/arrows=extend selection, w/b/e=word selection\n\ + Esc=normal" + } + _ => "🎯 Watch the cursor change automatically!" + }; + + let help = Paragraph::new(help_text) + .block(Block::default().borders(Borders::ALL).title("πŸš€ Automatic Cursor Management")) + .style(Style::default().fg(Color::Gray)); + + f.render_widget(help, chunks[1]); +} + +fn main() -> Result<(), Box> { + // Print feature status + println!("🎯 Canvas Cursor Auto Demo"); + println!("βœ… cursor-style feature: ENABLED"); + println!("πŸš€ Automatic cursor management: ACTIVE"); + println!("πŸ“– Watch your terminal cursor change based on mode!"); + println!(); + + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let data = CursorDemoData::new(); + let mut editor = AutoCursorFormEditor::new(data); + + // Initialize with normal mode - library automatically sets block cursor + editor.set_mode(AppMode::ReadOnly); + + // Demonstrate that CursorManager is available and working + CursorManager::update_for_mode(AppMode::ReadOnly)?; + + let res = run_app(&mut terminal, editor); + + // Library automatically resets cursor on FormEditor::drop() + // But we can also manually reset if needed + CursorManager::reset()?; + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err); + } + + println!("🎯 Cursor automatically reset to default!"); + Ok(()) +} diff --git a/canvas/src/canvas/gui.rs b/canvas/src/canvas/gui.rs index dcd106e..3337ab2 100644 --- a/canvas/src/canvas/gui.rs +++ b/canvas/src/canvas/gui.rs @@ -27,26 +27,37 @@ pub fn render_canvas( area: Rect, editor: &FormEditor, theme: &T, +) -> Option { + // Convert SelectionState to HighlightState + let highlight_state = convert_selection_to_highlight(editor.ui_state().selection_state()); + render_canvas_with_highlight(f, area, editor, theme, &highlight_state) +} + +/// Render canvas with explicit highlight state (for advanced use) +#[cfg(feature = "gui")] +pub fn render_canvas_with_highlight( + f: &mut Frame, + area: Rect, + editor: &FormEditor, + theme: &T, + highlight_state: &HighlightState, ) -> Option { let ui_state = editor.ui_state(); let data_provider = editor.data_provider(); - + // Build field information let field_count = data_provider.field_count(); let mut fields: Vec<&str> = Vec::with_capacity(field_count); let mut inputs: Vec = Vec::with_capacity(field_count); - + for i in 0..field_count { fields.push(data_provider.field_name(i)); inputs.push(data_provider.field_value(i).to_string()); } - + let current_field_idx = ui_state.current_field(); let is_edit_mode = matches!(ui_state.mode(), crate::canvas::modes::AppMode::Edit); - - // For now, create a default highlight state (TODO: get from editor state) - let highlight_state = HighlightState::Off; - + render_canvas_fields( f, area, @@ -55,7 +66,7 @@ pub fn render_canvas( &inputs, theme, is_edit_mode, - &highlight_state, + highlight_state, // Now using the actual highlight state! ui_state.cursor_position(), false, // TODO: track unsaved changes in editor |i| { @@ -65,6 +76,18 @@ pub fn render_canvas( ) } +/// Convert SelectionState to HighlightState for rendering +#[cfg(feature = "gui")] +fn convert_selection_to_highlight(selection: &crate::canvas::state::SelectionState) -> HighlightState { + use crate::canvas::state::SelectionState; + + match selection { + SelectionState::None => HighlightState::Off, + SelectionState::Characterwise { anchor } => HighlightState::Characterwise { anchor: *anchor }, + SelectionState::Linewise { anchor_field } => HighlightState::Linewise { anchor_line: *anchor_field }, + } +} + /// Core canvas field rendering #[cfg(feature = "gui")] fn render_canvas_fields( @@ -246,7 +269,7 @@ fn apply_highlighting<'a, T: CanvasTheme>( } } -/// Apply characterwise highlighting +/// Apply characterwise highlighting - DIRECTION-AWARE VERSION #[cfg(feature = "gui")] fn apply_characterwise_highlighting<'a, T: CanvasTheme>( text: &'a str, @@ -271,6 +294,7 @@ fn apply_characterwise_highlighting<'a, T: CanvasTheme>( if field_index >= start_field && field_index <= end_field { if start_field == end_field { + // Single field selection - same as before 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 { @@ -295,8 +319,57 @@ fn apply_characterwise_highlighting<'a, T: CanvasTheme>( Span::styled(after, normal_style_in_highlight), ]) } else { - // Multi-field selection - Line::from(Span::styled(text, highlight_style)) + // Multi-field selection - think in terms of anchorβ†’current direction + if field_index == anchor_field { + // Anchor field: highlight from anchor position toward the selection + if anchor_field < *current_field_idx { + // Downward selection: highlight from anchor to end of field + let clamped_start = anchor_char.min(text_len); + let before: String = text.chars().take(clamped_start).collect(); + let highlighted: String = text.chars().skip(clamped_start).collect(); + + Line::from(vec![ + Span::styled(before, normal_style_in_highlight), + Span::styled(highlighted, highlight_style), + ]) + } else { + // Upward selection: highlight from start of field to anchor + let clamped_end = anchor_char.min(text_len); + let highlighted: String = text.chars().take(clamped_end + 1).collect(); + let after: String = text.chars().skip(clamped_end + 1).collect(); + + Line::from(vec![ + Span::styled(highlighted, highlight_style), + Span::styled(after, normal_style_in_highlight), + ]) + } + } else if field_index == *current_field_idx { + // Current field: highlight toward the cursor position + if anchor_field < *current_field_idx { + // Downward selection: highlight from start of field to cursor + let clamped_end = current_cursor_pos.min(text_len); + let highlighted: String = text.chars().take(clamped_end + 1).collect(); + let after: String = text.chars().skip(clamped_end + 1).collect(); + + Line::from(vec![ + Span::styled(highlighted, highlight_style), + Span::styled(after, normal_style_in_highlight), + ]) + } else { + // Upward selection: highlight from cursor to end of field + let clamped_start = current_cursor_pos.min(text_len); + let before: String = text.chars().take(clamped_start).collect(); + let highlighted: String = text.chars().skip(clamped_start).collect(); + + Line::from(vec![ + Span::styled(before, normal_style_in_highlight), + Span::styled(highlighted, highlight_style), + ]) + } + } else { + // Middle field between anchor and current: highlight entire field + Line::from(Span::styled(text, highlight_style)) + } } } else { Line::from(Span::styled( @@ -306,7 +379,7 @@ fn apply_characterwise_highlighting<'a, T: CanvasTheme>( } } -/// Apply linewise highlighting +/// Apply linewise highlighting - VISUALLY DISTINCT VERSION #[cfg(feature = "gui")] fn apply_linewise_highlighting<'a, T: CanvasTheme>( text: &'a str, @@ -319,14 +392,17 @@ fn apply_linewise_highlighting<'a, T: CanvasTheme>( let start_field = min(*anchor_line, *current_field_idx); let end_field = max(*anchor_line, *current_field_idx); + // Use the SAME style as characterwise highlighting 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 field_index >= start_field && field_index <= end_field { + // ALWAYS highlight entire line - no markers, just full line highlighting Line::from(Span::styled(text, highlight_style)) } else { Line::from(Span::styled( diff --git a/canvas/src/editor.rs b/canvas/src/editor.rs index 47165a1..5dc050d 100644 --- a/canvas/src/editor.rs +++ b/canvas/src/editor.rs @@ -3,11 +3,14 @@ #[cfg(feature = "cursor-style")] use crate::canvas::CursorManager; +#[cfg(feature = "cursor-style")] +use crossterm; use anyhow::Result; use crate::canvas::state::EditorState; use crate::data_provider::{DataProvider, AutocompleteProvider, SuggestionItem}; use crate::canvas::modes::AppMode; +use crate::canvas::state::SelectionState; /// Main editor that manages UI state internally and delegates data to user pub struct FormEditor { @@ -148,21 +151,47 @@ impl FormEditor { /// Change mode (for vim compatibility) pub fn set_mode(&mut self, mode: AppMode) { - #[cfg(feature = "cursor-style")] - let old_mode = self.ui_state.current_mode; - - self.ui_state.current_mode = mode; - - // Clear autocomplete when changing modes - if mode != AppMode::Edit { - self.ui_state.deactivate_autocomplete(); + match (self.ui_state.current_mode, mode) { + // Entering highlight mode from read-only + (AppMode::ReadOnly, AppMode::Highlight) => { + self.enter_highlight_mode(); + } + // Exiting highlight mode + (AppMode::Highlight, AppMode::ReadOnly) => { + self.exit_highlight_mode(); + } + // Other transitions + (_, new_mode) => { + self.ui_state.current_mode = new_mode; + if new_mode != AppMode::Highlight { + self.ui_state.selection = SelectionState::None; + } + + #[cfg(feature = "cursor-style")] + { + let _ = CursorManager::update_for_mode(new_mode); + } + } } + } - // Update cursor style if mode changed and cursor-style feature is enabled - #[cfg(feature = "cursor-style")] - if old_mode != mode { - let _ = crate::canvas::CursorManager::update_for_mode(mode); - } + /// Enter edit mode with cursor positioned for append (vim 'a' command) + pub fn enter_append_mode(&mut self) { + let current_text = self.current_text(); + + // Calculate append position: always move right, even at line end + let append_pos = if current_text.is_empty() { + 0 + } else { + (self.ui_state.cursor_pos + 1).min(current_text.len()) + }; + + // Set cursor position for append + self.ui_state.cursor_pos = append_pos; + self.ui_state.ideal_cursor_column = append_pos; + + // Enter edit mode (which will update cursor style) + self.set_mode(AppMode::Edit); } // =================================================================== @@ -440,7 +469,19 @@ impl FormEditor { } /// Exit edit mode to read-only mode (vim Escape) + // TODO this is still flickering, I have no clue how to fix it pub fn exit_edit_mode(&mut self) { + // Adjust cursor position when transitioning from edit to normal mode + let current_text = self.current_text(); + if !current_text.is_empty() { + // In normal mode, cursor must be ON a character, not after the last one + let max_normal_pos = current_text.len().saturating_sub(1); + if self.ui_state.cursor_pos > max_normal_pos { + self.ui_state.cursor_pos = max_normal_pos; + self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; + } + } + self.set_mode(AppMode::ReadOnly); // Deactivate autocomplete when exiting edit mode self.ui_state.deactivate_autocomplete(); @@ -521,6 +562,26 @@ impl FormEditor { self.ui_state.ideal_cursor_column = clamped_pos; } + /// Get cursor position for display (respects mode-specific positioning rules) + pub fn display_cursor_position(&self) -> usize { + let current_text = self.current_text(); + + match self.ui_state.current_mode { + AppMode::Edit => { + // Edit mode: cursor can be past end of text + self.ui_state.cursor_pos.min(current_text.len()) + } + _ => { + // Normal/other modes: cursor must be on a character + if current_text.is_empty() { + 0 + } else { + self.ui_state.cursor_pos.min(current_text.len().saturating_sub(1)) + } + } + } + } + /// Cleanup cursor style (call this when shutting down) pub fn cleanup_cursor(&self) -> std::io::Result<()> { #[cfg(feature = "cursor-style")] @@ -532,6 +593,102 @@ impl FormEditor { Ok(()) } } + + + // =================================================================== + // HIGHLIGHT MODE + // =================================================================== + + /// Enter highlight mode (visual mode) + pub fn enter_highlight_mode(&mut self) { + if self.ui_state.current_mode == AppMode::ReadOnly { + self.ui_state.current_mode = AppMode::Highlight; + self.ui_state.selection = SelectionState::Characterwise { + anchor: (self.ui_state.current_field, self.ui_state.cursor_pos), + }; + + #[cfg(feature = "cursor-style")] + { + let _ = CursorManager::update_for_mode(AppMode::Highlight); + } + } + } + + /// Enter highlight line mode (visual line mode) + pub fn enter_highlight_line_mode(&mut self) { + if self.ui_state.current_mode == AppMode::ReadOnly { + self.ui_state.current_mode = AppMode::Highlight; + self.ui_state.selection = SelectionState::Linewise { + anchor_field: self.ui_state.current_field, + }; + + #[cfg(feature = "cursor-style")] + { + let _ = CursorManager::update_for_mode(AppMode::Highlight); + } + } + } + + /// Exit highlight mode back to read-only + pub fn exit_highlight_mode(&mut self) { + if self.ui_state.current_mode == AppMode::Highlight { + self.ui_state.current_mode = AppMode::ReadOnly; + self.ui_state.selection = SelectionState::None; + + #[cfg(feature = "cursor-style")] + { + let _ = CursorManager::update_for_mode(AppMode::ReadOnly); + } + } + } + + /// Check if currently in highlight mode + pub fn is_highlight_mode(&self) -> bool { + self.ui_state.current_mode == AppMode::Highlight + } + + /// Get current selection state + pub fn selection_state(&self) -> &SelectionState { + &self.ui_state.selection + } + + /// Enhanced movement methods that update selection in highlight mode + pub fn move_left_with_selection(&mut self) { + self.move_left(); + // Selection anchor stays in place, cursor position updates automatically + } + + pub fn move_right_with_selection(&mut self) { + self.move_right(); + // Selection anchor stays in place, cursor position updates automatically + } + + pub fn move_up_with_selection(&mut self) { + self.move_up(); + // Selection anchor stays in place, cursor position updates automatically + } + + pub fn move_down_with_selection(&mut self) { + self.move_down(); + // Selection anchor stays in place, cursor position updates automatically + } + + // Add similar methods for word movement, line movement, etc. + pub fn move_word_next_with_selection(&mut self) { + self.move_word_next(); + } + + pub fn move_word_prev_with_selection(&mut self) { + self.move_word_prev(); + } + + pub fn move_line_start_with_selection(&mut self) { + self.move_line_start(); + } + + pub fn move_line_end_with_selection(&mut self) { + self.move_line_end(); + } } // Add Drop implementation for automatic cleanup