diff --git a/canvas/examples/canvas_cursor_auto.rs b/canvas/examples/canvas_cursor_auto.rs index dc27d05..e83d673 100644 --- a/canvas/examples/canvas_cursor_auto.rs +++ b/canvas/examples/canvas_cursor_auto.rs @@ -47,7 +47,6 @@ use canvas::{ // Enhanced FormEditor that demonstrates automatic cursor management struct AutoCursorFormEditor { editor: FormEditor, - highlight_state: HighlightState, has_unsaved_changes: bool, debug_message: String, command_buffer: String, // For multi-key vim commands like "gg" @@ -57,7 +56,6 @@ impl AutoCursorFormEditor { fn new(data_provider: D) -> Self { Self { editor: FormEditor::new(data_provider), - highlight_state: HighlightState::Off, has_unsaved_changes: false, debug_message: "🎯 Automatic Cursor Demo - cursor-style feature enabled!".to_string(), command_buffer: String::new(), @@ -84,49 +82,41 @@ impl AutoCursorFormEditor { // === VISUAL/HIGHLIGHT MODE SUPPORT === + fn enter_visual_mode(&mut self) { - if ModeManager::can_enter_highlight_mode(self.editor.mode()) { - self.editor.set_mode(AppMode::Highlight); - self.highlight_state = HighlightState::Characterwise { - anchor: ( - self.editor.current_field(), - self.editor.cursor_position(), - ), - }; - self.debug_message = "πŸ”₯ VISUAL MODE - Cursor: Blinking Block β–ˆ".to_string(); - } + // 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) { - if ModeManager::can_enter_highlight_mode(self.editor.mode()) { - self.editor.set_mode(AppMode::Highlight); - self.highlight_state = - HighlightState::Linewise { anchor_line: self.editor.current_field() }; - self.debug_message = "πŸ”₯ VISUAL LINE MODE - Cursor: Blinking Block β–ˆ".to_string(); - } + // 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) { - self.highlight_state = HighlightState::Off; - if self.editor.mode() == AppMode::Highlight { - self.editor.set_mode(AppMode::ReadOnly); - self.debug_message = "πŸ”’ NORMAL MODE - Cursor: Steady Block β–ˆ".to_string(); - } + // 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.mode() == AppMode::Highlight { - match &self.highlight_state { - HighlightState::Characterwise { anchor: _ } => { + 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: char {} in field {} - Cursor: Blinking Block β–ˆ", - self.editor.cursor_position(), - self.editor.current_field() + "🎯 Visual selection: anchor=({},{}) current=({},{}) - Cursor: Blinking Block β–ˆ", + anchor.0, anchor.1, + self.editor.current_field(), + self.editor.cursor_position() ); } - HighlightState::Linewise { anchor_line: _ } => { + SelectionState::Linewise { anchor_field } => { self.debug_message = format!( - "🎯 Visual line selection: field {} - Cursor: Blinking Block β–ˆ", + "🎯 Visual LINE selection: anchor={} current={} - Cursor: Blinking Block β–ˆ", + anchor_field, self.editor.current_field() ); } @@ -313,10 +303,6 @@ impl AutoCursorFormEditor { &self.debug_message } - fn highlight_state(&self) -> &HighlightState { - &self.highlight_state - } - fn has_unsaved_changes(&self) -> bool { self.has_unsaved_changes } @@ -645,14 +631,18 @@ fn render_status_and_help( .constraints([Constraint::Length(3), Constraint::Length(7)]) .split(area); - // Status bar with cursor information + // 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 => match editor.highlight_state() { - HighlightState::Characterwise { .. } => "VISUAL β–ˆ (blinking block)", - HighlightState::Linewise { .. } => "VISUAL LINE β–ˆ (blinking block)", - _ => "VISUAL β–ˆ (blinking block)", + 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)", }; @@ -670,7 +660,7 @@ fn render_status_and_help( f.render_widget(status, chunks[0]); - // Enhanced help text + // Enhanced help text (no changes needed here) let help_text = match editor.mode() { AppMode::ReadOnly => { if editor.has_pending_command() { @@ -681,7 +671,7 @@ fn render_status_and_help( } 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/V=visual, x/X=delete, ?=info\n\ + i/a/A=insert, v/b=visual, x/X=delete, ?=info\n\ F1=demo manual cursor, F2=restore automatic" } } 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 614ccce..5dc050d 100644 --- a/canvas/src/editor.rs +++ b/canvas/src/editor.rs @@ -10,6 +10,7 @@ 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 { @@ -150,31 +151,26 @@ 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(); - } - - // 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); - - // IMMEDIATELY update terminal cursor position for the new mode - // This prevents flicker by ensuring position and style change atomically - if let Ok((x, y)) = crossterm::cursor::position() { - let display_pos = self.display_cursor_position(); - let current_text = self.current_text(); - let adjusted_x = x.saturating_sub(current_text.len() as u16) + display_pos as u16; - let _ = crossterm::execute!( - std::io::stdout(), - crossterm::cursor::MoveTo(adjusted_x, y) - ); + 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); + } } } } @@ -597,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