// src/editor.rs //! Main API for the canvas library - FormEditor with library-owned state use anyhow::Result; use crate::canvas::state::EditorState; use crate::data_provider::{DataProvider, AutocompleteProvider, SuggestionItem}; use crate::canvas::modes::AppMode; /// Main editor that manages UI state internally and delegates data to user pub struct FormEditor { // Library owns all UI state ui_state: EditorState, // User owns business data data_provider: D, // Autocomplete suggestions (library manages UI, user provides data) pub(crate) suggestions: Vec>, } impl FormEditor { pub fn new(data_provider: D) -> Self { Self { ui_state: EditorState::new(), data_provider, suggestions: Vec::new(), } } // =================================================================== // READ-ONLY ACCESS: User can fetch UI state // =================================================================== /// Get current field index (for user's compatibility) pub fn current_field(&self) -> usize { self.ui_state.current_field() } /// Get current cursor position (for user's compatibility) pub fn cursor_position(&self) -> usize { self.ui_state.cursor_position() } /// Get current mode (for user's mode-dependent logic) pub fn mode(&self) -> AppMode { self.ui_state.mode() } /// Check if autocomplete is active (for user's logic) pub fn is_autocomplete_active(&self) -> bool { self.ui_state.is_autocomplete_active() } /// Get current field text (convenience method) pub fn current_text(&self) -> &str { let field_index = self.ui_state.current_field; if field_index < self.data_provider.field_count() { self.data_provider.field_value(field_index) } else { "" } } /// Get reference to UI state for rendering pub fn ui_state(&self) -> &EditorState { &self.ui_state } /// Get reference to data provider for rendering pub fn data_provider(&self) -> &D { &self.data_provider } /// Get autocomplete suggestions for rendering (read-only) pub fn suggestions(&self) -> &[SuggestionItem] { &self.suggestions } // =================================================================== // SYNC OPERATIONS: No async needed for basic editing // =================================================================== /// Handle character insertion pub fn insert_char(&mut self, ch: char) -> Result<()> { if self.ui_state.current_mode != AppMode::Edit { return Ok(()); // Ignore in non-edit modes } let field_index = self.ui_state.current_field; let cursor_pos = self.ui_state.cursor_pos; // Get current text from user let mut current_text = self.data_provider.field_value(field_index).to_string(); // Insert character current_text.insert(cursor_pos, ch); // Update user's data self.data_provider.set_field_value(field_index, current_text); // Update library's UI state self.ui_state.cursor_pos += 1; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; Ok(()) } /// Handle cursor movement pub fn move_left(&mut self) { if self.ui_state.cursor_pos > 0 { self.ui_state.cursor_pos -= 1; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; } } pub fn move_right(&mut self) { let current_text = self.current_text(); let max_pos = if self.ui_state.current_mode == AppMode::Edit { current_text.len() // Edit mode: can go past end } else { current_text.len().saturating_sub(1) // ReadOnly: stay in bounds }; if self.ui_state.cursor_pos < max_pos { self.ui_state.cursor_pos += 1; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; } } /// Handle field navigation pub fn move_to_next_field(&mut self) { let field_count = self.data_provider.field_count(); let next_field = (self.ui_state.current_field + 1) % field_count; self.ui_state.move_to_field(next_field, field_count); // Clamp cursor to new field let current_text = self.current_text(); let max_pos = current_text.len(); self.ui_state.set_cursor( self.ui_state.ideal_cursor_column, max_pos, self.ui_state.current_mode == AppMode::Edit ); } /// Change mode (for vim compatibility) pub fn set_mode(&mut self, mode: AppMode) { self.ui_state.current_mode = mode; // Clear autocomplete when changing modes if mode != AppMode::Edit { self.ui_state.deactivate_autocomplete(); } } // =================================================================== // ASYNC OPERATIONS: Only autocomplete needs async // =================================================================== /// Trigger autocomplete (async because it fetches data) pub async fn trigger_autocomplete(&mut self, provider: &mut A) -> Result<()> where A: AutocompleteProvider, A::SuggestionData: std::fmt::Debug, // Change from Display to Debug { let field_index = self.ui_state.current_field; if !self.data_provider.supports_autocomplete(field_index) { return Ok(()); } // Activate autocomplete UI self.ui_state.activate_autocomplete(field_index); // Fetch suggestions from user let query = self.current_text(); let suggestions = provider.fetch_suggestions(field_index, query).await?; // Convert to library's format (could be avoided with better generics) self.suggestions = suggestions.into_iter() .map(|item| SuggestionItem { data: format!("{:?}", item.data), // Use Debug formatting instead display_text: item.display_text, value_to_store: item.value_to_store, }) .collect(); // Update UI state self.ui_state.autocomplete.is_loading = false; if !self.suggestions.is_empty() { self.ui_state.autocomplete.selected_index = Some(0); } Ok(()) } /// Navigate autocomplete suggestions pub fn autocomplete_next(&mut self) { if !self.ui_state.autocomplete.is_active || self.suggestions.is_empty() { return; } let current = self.ui_state.autocomplete.selected_index.unwrap_or(0); let next = (current + 1) % self.suggestions.len(); self.ui_state.autocomplete.selected_index = Some(next); } /// Apply selected autocomplete suggestion pub fn apply_autocomplete(&mut self) -> Option { if let Some(selected_index) = self.ui_state.autocomplete.selected_index { if let Some(suggestion) = self.suggestions.get(selected_index).cloned() { let field_index = self.ui_state.current_field; // Apply to user's data self.data_provider.set_field_value( field_index, suggestion.value_to_store.clone() ); // Update cursor position self.ui_state.cursor_pos = suggestion.value_to_store.len(); self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; // Close autocomplete self.ui_state.deactivate_autocomplete(); self.suggestions.clear(); return Some(suggestion.display_text); } } None } }