From b8e1b772227f3aa329f9c33ea5ef4ad3542cf7b9 Mon Sep 17 00:00:00 2001 From: Priec Date: Tue, 29 Jul 2025 21:46:55 +0200 Subject: [PATCH] compiled autocomplete --- canvas/src/actions/edit.rs | 81 ++++++++++++--------- canvas/src/actions/types.rs | 64 +++++++++------- canvas/src/autocomplete.rs | 126 ++++++++++++++++++++++++++++++++ canvas/src/gui/render.rs | 141 ++++++++++++++++++++++++++++++++++-- canvas/src/lib.rs | 46 +++--------- canvas/src/state.rs | 133 ++++++++++++++++++++++++++++++---- 6 files changed, 479 insertions(+), 112 deletions(-) create mode 100644 canvas/src/autocomplete.rs diff --git a/canvas/src/actions/edit.rs b/canvas/src/actions/edit.rs index 2a112d0..a665f3a 100644 --- a/canvas/src/actions/edit.rs +++ b/canvas/src/actions/edit.rs @@ -23,8 +23,8 @@ pub async fn execute_canvas_action( return Ok(ActionResult::HandledByFeature(result)); } - // 2. Handle suggestion actions - if let Some(result) = handle_suggestion_action(&action, state)? { + // 2. Handle autocomplete actions + if let Some(result) = handle_autocomplete_action(&action, state)? { return Ok(result); } @@ -51,52 +51,67 @@ pub async fn execute_edit_action( }; let result = execute_canvas_action(typed_action, state, ideal_cursor_column).await?; - + // Convert ActionResult back to string for backwards compatibility Ok(result.message().unwrap_or("").to_string()) } -/// Handle suggestion-related actions -fn handle_suggestion_action( +/// Handle autocomplete-related actions +fn handle_autocomplete_action( action: &CanvasAction, state: &mut S, ) -> Result> { match action { + CanvasAction::TriggerAutocomplete => { + if state.supports_autocomplete(state.current_field()) { + state.activate_autocomplete(); + Ok(Some(ActionResult::success_with_message("Autocomplete activated - fetching suggestions..."))) + } else { + Ok(Some(ActionResult::error("Autocomplete not supported for this field"))) + } + } + CanvasAction::SuggestionDown => { - if let Some(suggestions) = state.get_suggestions() { - if !suggestions.is_empty() { - let current = state.get_selected_suggestion_index().unwrap_or(0); - let next = (current + 1) % suggestions.len(); - state.set_selected_suggestion_index(Some(next)); + if state.is_autocomplete_ready() { + if let Some(autocomplete_state) = state.autocomplete_state_mut() { + autocomplete_state.select_next(); return Ok(Some(ActionResult::success())); } } - Ok(None) + Ok(None) // Not handled - no active autocomplete } - + CanvasAction::SuggestionUp => { - if let Some(suggestions) = state.get_suggestions() { - if !suggestions.is_empty() { - let current = state.get_selected_suggestion_index().unwrap_or(0); - let prev = if current == 0 { suggestions.len() - 1 } else { current - 1 }; - state.set_selected_suggestion_index(Some(prev)); + if state.is_autocomplete_ready() { + if let Some(autocomplete_state) = state.autocomplete_state_mut() { + autocomplete_state.select_previous(); return Ok(Some(ActionResult::success())); } } - Ok(None) + Ok(None) // Not handled - no active autocomplete } - + CanvasAction::SelectSuggestion => { - // Let feature handle this via handle_feature_action since it's feature-specific - Ok(None) + if state.is_autocomplete_ready() { + if let Some(message) = state.apply_autocomplete_selection() { + return Ok(Some(ActionResult::success_with_message(message))); + } else { + return Ok(Some(ActionResult::error("No suggestion selected"))); + } + } + Ok(None) // Not handled - no active autocomplete } - + CanvasAction::ExitSuggestions => { - state.deactivate_suggestions(); - Ok(Some(ActionResult::success())) + if state.is_autocomplete_active() { + state.deactivate_autocomplete(); + Ok(Some(ActionResult::success_with_message("Autocomplete cancelled"))) + } else { + Ok(None) // Not handled - autocomplete not active + } } - - _ => Ok(None), + + _ => Ok(None), // Not an autocomplete action } } @@ -111,7 +126,7 @@ async fn handle_generic_canvas_action( let cursor_pos = state.current_cursor_pos(); let field_value = state.get_current_input_mut(); let mut chars: Vec = field_value.chars().collect(); - + if cursor_pos <= chars.len() { chars.insert(cursor_pos, c); *field_value = chars.into_iter().collect(); @@ -129,7 +144,7 @@ async fn handle_generic_canvas_action( let cursor_pos = state.current_cursor_pos(); let field_value = state.get_current_input_mut(); let mut chars: Vec = field_value.chars().collect(); - + if cursor_pos <= chars.len() { chars.remove(cursor_pos - 1); *field_value = chars.into_iter().collect(); @@ -146,7 +161,7 @@ async fn handle_generic_canvas_action( let cursor_pos = state.current_cursor_pos(); let field_value = state.get_current_input_mut(); let mut chars: Vec = field_value.chars().collect(); - + if cursor_pos < chars.len() { chars.remove(cursor_pos); *field_value = chars.into_iter().collect(); @@ -321,15 +336,15 @@ async fn handle_generic_canvas_action( Ok(ActionResult::error(format!("Unknown or unhandled custom action: {}", action_str))) } - // Suggestion actions should have been handled above - CanvasAction::SuggestionUp | CanvasAction::SuggestionDown | + // Autocomplete actions should have been handled above + CanvasAction::TriggerAutocomplete | CanvasAction::SuggestionUp | CanvasAction::SuggestionDown | CanvasAction::SelectSuggestion | CanvasAction::ExitSuggestions => { - Ok(ActionResult::error("Suggestion action not handled properly")) + Ok(ActionResult::error("Autocomplete action not handled properly")) } } } -// Word movement helper functions +// Word movement helper functions (unchanged from previous implementation) #[derive(PartialEq)] enum CharType { diff --git a/canvas/src/actions/types.rs b/canvas/src/actions/types.rs index ef2b107..f73b1de 100644 --- a/canvas/src/actions/types.rs +++ b/canvas/src/actions/types.rs @@ -7,39 +7,45 @@ use crossterm::event::KeyCode; pub enum CanvasAction { // Character input InsertChar(char), - + // Deletion DeleteBackward, DeleteForward, - + // Basic cursor movement MoveLeft, MoveRight, MoveUp, MoveDown, - + // Line movement MoveLineStart, MoveLineEnd, MoveFirstLine, MoveLastLine, - + // Word movement MoveWordNext, MoveWordEnd, MoveWordPrev, MoveWordEndPrev, - + // Field navigation NextField, PrevField, - - // Suggestions + + // AUTOCOMPLETE ACTIONS (NEW) + /// Manually trigger autocomplete for current field + TriggerAutocomplete, + /// Move to next suggestion SuggestionUp, + /// Move to previous suggestion SuggestionDown, + /// Select the currently highlighted suggestion SelectSuggestion, + /// Cancel/exit autocomplete mode ExitSuggestions, - + // Custom actions (escape hatch for feature-specific behavior) Custom(String), } @@ -69,6 +75,8 @@ impl CanvasAction { "move_word_end_prev" => Self::MoveWordEndPrev, "next_field" => Self::NextField, "prev_field" => Self::PrevField, + // Autocomplete actions + "trigger_autocomplete" => Self::TriggerAutocomplete, "suggestion_up" => Self::SuggestionUp, "suggestion_down" => Self::SuggestionDown, "select_suggestion" => Self::SelectSuggestion, @@ -76,7 +84,7 @@ impl CanvasAction { _ => Self::Custom(action.to_string()), } } - + /// Get string representation (for logging, debugging) pub fn as_str(&self) -> &str { match self { @@ -97,6 +105,8 @@ impl CanvasAction { Self::MoveWordEndPrev => "move_word_end_prev", Self::NextField => "next_field", Self::PrevField => "prev_field", + // Autocomplete actions + Self::TriggerAutocomplete => "trigger_autocomplete", Self::SuggestionUp => "suggestion_up", Self::SuggestionDown => "suggestion_down", Self::SelectSuggestion => "select_suggestion", @@ -104,7 +114,7 @@ impl CanvasAction { Self::Custom(s) => s, } } - + /// Create action from KeyCode for common cases pub fn from_key(key: KeyCode) -> Option { match key { @@ -122,17 +132,17 @@ impl CanvasAction { _ => None, } } - + /// Check if this action modifies content pub fn is_modifying(&self) -> bool { - matches!(self, - Self::InsertChar(_) | - Self::DeleteBackward | + matches!(self, + Self::InsertChar(_) | + Self::DeleteBackward | Self::DeleteForward | Self::SelectSuggestion ) } - + /// Check if this action moves the cursor pub fn is_movement(&self) -> bool { matches!(self, @@ -142,11 +152,11 @@ impl CanvasAction { Self::NextField | Self::PrevField ) } - + /// Check if this is a suggestion-related action pub fn is_suggestion(&self) -> bool { matches!(self, - Self::SuggestionUp | Self::SuggestionDown | + Self::TriggerAutocomplete | Self::SuggestionUp | Self::SuggestionDown | Self::SelectSuggestion | Self::ExitSuggestions ) } @@ -169,19 +179,19 @@ impl ActionResult { pub fn success() -> Self { Self::Success(None) } - + pub fn success_with_message(msg: impl Into) -> Self { Self::Success(Some(msg.into())) } - + pub fn error(msg: impl Into) -> Self { Self::Error(msg.into()) } - + pub fn is_success(&self) -> bool { matches!(self, Self::Success(_) | Self::HandledByFeature(_)) } - + pub fn message(&self) -> Option<&str> { match self { Self::Success(msg) => msg.as_deref(), @@ -195,14 +205,15 @@ impl ActionResult { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_action_from_string() { assert_eq!(CanvasAction::from_string("move_left"), CanvasAction::MoveLeft); assert_eq!(CanvasAction::from_string("delete_char_backward"), CanvasAction::DeleteBackward); + assert_eq!(CanvasAction::from_string("trigger_autocomplete"), CanvasAction::TriggerAutocomplete); assert_eq!(CanvasAction::from_string("unknown"), CanvasAction::Custom("unknown".to_string())); } - + #[test] fn test_action_from_key() { assert_eq!(CanvasAction::from_key(KeyCode::Char('a')), Some(CanvasAction::InsertChar('a'))); @@ -210,16 +221,17 @@ mod tests { assert_eq!(CanvasAction::from_key(KeyCode::Backspace), Some(CanvasAction::DeleteBackward)); assert_eq!(CanvasAction::from_key(KeyCode::F(1)), None); } - + #[test] fn test_action_properties() { assert!(CanvasAction::InsertChar('a').is_modifying()); assert!(!CanvasAction::MoveLeft.is_modifying()); - + assert!(CanvasAction::MoveLeft.is_movement()); assert!(!CanvasAction::InsertChar('a').is_movement()); - + assert!(CanvasAction::SuggestionUp.is_suggestion()); + assert!(CanvasAction::TriggerAutocomplete.is_suggestion()); assert!(!CanvasAction::MoveLeft.is_suggestion()); } } diff --git a/canvas/src/autocomplete.rs b/canvas/src/autocomplete.rs new file mode 100644 index 0000000..e9f96b2 --- /dev/null +++ b/canvas/src/autocomplete.rs @@ -0,0 +1,126 @@ +// canvas/src/autocomplete.rs + +/// Generic suggestion item that clients push to canvas +#[derive(Debug, Clone)] +pub struct SuggestionItem { + /// The underlying data (client-specific, e.g., Hit, String, etc.) + pub data: T, + /// Text to display in the dropdown + pub display_text: String, + /// Value to store in the form field when selected + pub value_to_store: String, +} + +impl SuggestionItem { + pub fn new(data: T, display_text: String, value_to_store: String) -> Self { + Self { + data, + display_text, + value_to_store, + } + } + + /// Convenience constructor for simple string suggestions + pub fn simple(data: T, text: String) -> Self { + Self { + data, + display_text: text.clone(), + value_to_store: text, + } + } +} + +/// Autocomplete state managed by canvas +#[derive(Debug, Clone)] +pub struct AutocompleteState { + /// Whether autocomplete is currently active/visible + pub is_active: bool, + /// Whether suggestions are being loaded (for spinner/loading indicator) + pub is_loading: bool, + /// Current suggestions to display + pub suggestions: Vec>, + /// Currently selected suggestion index + pub selected_index: Option, + /// Field index that triggered autocomplete (for context) + pub active_field: Option, +} + +impl Default for AutocompleteState { + fn default() -> Self { + Self { + is_active: false, + is_loading: false, + suggestions: Vec::new(), + selected_index: None, + active_field: None, + } + } +} + +impl AutocompleteState { + pub fn new() -> Self { + Self::default() + } + + /// Activate autocomplete for a specific field + pub fn activate(&mut self, field_index: usize) { + self.is_active = true; + self.active_field = Some(field_index); + self.selected_index = None; + self.suggestions.clear(); + self.is_loading = true; + } + + /// Deactivate autocomplete and clear state + pub fn deactivate(&mut self) { + self.is_active = false; + self.is_loading = false; + self.suggestions.clear(); + self.selected_index = None; + self.active_field = None; + } + + /// Set suggestions and stop loading + pub fn set_suggestions(&mut self, suggestions: Vec>) { + self.suggestions = suggestions; + self.is_loading = false; + self.selected_index = if self.suggestions.is_empty() { + None + } else { + Some(0) + }; + } + + /// Move selection down + pub fn select_next(&mut self) { + if !self.suggestions.is_empty() { + let current = self.selected_index.unwrap_or(0); + self.selected_index = Some((current + 1) % self.suggestions.len()); + } + } + + /// Move selection up + pub fn select_previous(&mut self) { + if !self.suggestions.is_empty() { + let current = self.selected_index.unwrap_or(0); + self.selected_index = Some( + if current == 0 { + self.suggestions.len() - 1 + } else { + current - 1 + } + ); + } + } + + /// Get currently selected suggestion + pub fn get_selected(&self) -> Option<&SuggestionItem> { + self.selected_index + .and_then(|idx| self.suggestions.get(idx)) + } + + /// Check if autocomplete is ready for interaction (active and has suggestions) + pub fn is_ready(&self) -> bool { + self.is_active && !self.suggestions.is_empty() && !self.is_loading + } +} diff --git a/canvas/src/gui/render.rs b/canvas/src/gui/render.rs index 8532d5f..4c985a1 100644 --- a/canvas/src/gui/render.rs +++ b/canvas/src/gui/render.rs @@ -5,7 +5,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, BorderType, Paragraph}, + widgets::{Block, Borders, BorderType, List, ListItem, ListState, Paragraph}, Frame, }; use crate::state::CanvasState; @@ -14,6 +14,8 @@ use crate::modes::HighlightState; use super::theme::CanvasTheme; #[cfg(feature = "gui")] use std::cmp::{max, min}; +#[cfg(feature = "gui")] +use unicode_width::UnicodeWidthStr; /// Render canvas using the CanvasState trait and CanvasTheme #[cfg(feature = "gui")] @@ -28,8 +30,8 @@ pub fn render_canvas( let fields: Vec<&str> = form_state.fields(); let current_field_idx = form_state.current_field(); let inputs: Vec<&String> = form_state.inputs(); - - render_canvas_impl( + + let active_field_rect = render_canvas_impl( f, area, &fields, @@ -42,10 +44,137 @@ pub fn render_canvas( form_state.has_unsaved_changes(), |i| form_state.get_display_value_for_field(i).to_string(), |i| form_state.has_display_override(i), - ) + ); + + // NEW: Render autocomplete dropdown if active + if let Some(autocomplete_state) = form_state.autocomplete_state() { + if autocomplete_state.is_active { + if let Some(field_rect) = active_field_rect { + render_autocomplete_dropdown(f, area, field_rect, theme, autocomplete_state); + } + } + } + + active_field_rect } -/// Internal implementation of canvas rendering +/// Render autocomplete dropdown +#[cfg(feature = "gui")] +fn render_autocomplete_dropdown( + f: &mut Frame, + frame_area: Rect, + input_rect: Rect, + theme: &T, + autocomplete_state: &crate::autocomplete::AutocompleteState, +) { + if autocomplete_state.is_loading { + // Show loading indicator + let loading_text = "Loading suggestions..."; + let loading_width = loading_text.width() as u16 + 2; + let loading_height = 3; + + let mut dropdown_area = Rect { + x: input_rect.x, + y: input_rect.y + 1, + width: loading_width, + height: loading_height, + }; + + // Adjust position to stay within frame + if dropdown_area.bottom() > frame_area.height { + dropdown_area.y = input_rect.y.saturating_sub(loading_height); + } + if dropdown_area.right() > frame_area.width { + dropdown_area.x = frame_area.width.saturating_sub(loading_width); + } + + let loading_block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(theme.accent())) + .style(Style::default().bg(theme.bg())); + + let loading_paragraph = Paragraph::new(loading_text) + .block(loading_block) + .style(Style::default().fg(theme.fg())) + .alignment(Alignment::Center); + + f.render_widget(loading_paragraph, dropdown_area); + return; + } + + if autocomplete_state.suggestions.is_empty() { + return; + } + + // Calculate dropdown dimensions + let display_texts: Vec<&str> = autocomplete_state.suggestions + .iter() + .map(|item| item.display_text.as_str()) + .collect(); + + let max_width = display_texts + .iter() + .map(|text| text.width()) + .max() + .unwrap_or(0) as u16; + + let horizontal_padding = 4; // 2 for borders + 2 for internal padding + let dropdown_width = (max_width + horizontal_padding).max(12); + let dropdown_height = (autocomplete_state.suggestions.len() as u16).min(8) + 2; // +2 for borders + + let mut dropdown_area = Rect { + x: input_rect.x, + y: input_rect.y + 1, + width: dropdown_width, + height: dropdown_height, + }; + + // Adjust position to stay within frame bounds + if dropdown_area.bottom() > frame_area.height { + dropdown_area.y = input_rect.y.saturating_sub(dropdown_height); + } + if dropdown_area.right() > frame_area.width { + dropdown_area.x = frame_area.width.saturating_sub(dropdown_width); + } + dropdown_area.x = dropdown_area.x.max(0); + dropdown_area.y = dropdown_area.y.max(0); + + // Create dropdown background + let dropdown_block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(theme.accent())) + .style(Style::default().bg(theme.bg())); + + // Create list items + let items: Vec = display_texts + .iter() + .enumerate() + .map(|(i, text)| { + let is_selected = autocomplete_state.selected_index == Some(i); + let text_width = text.width() as u16; + let available_width = dropdown_width.saturating_sub(horizontal_padding); + let padding_needed = available_width.saturating_sub(text_width); + let padded_text = format!("{}{}", text, " ".repeat(padding_needed as usize)); + + ListItem::new(padded_text).style(if is_selected { + Style::default() + .fg(theme.bg()) + .bg(theme.highlight()) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(theme.fg()).bg(theme.bg()) + }) + }) + .collect(); + + let list = List::new(items).block(dropdown_block); + let mut list_state = ListState::default(); + list_state.select(autocomplete_state.selected_index); + + f.render_stateful_widget(list, dropdown_area, &mut list_state); +} + +/// Internal implementation of canvas rendering (unchanged from previous version) #[cfg(feature = "gui")] fn render_canvas_impl( f: &mut Frame, @@ -77,7 +206,7 @@ where } else { Style::default().fg(theme.secondary()) }; - + let input_container = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index 73da599..596082d 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -1,47 +1,25 @@ // canvas/src/lib.rs -//! Canvas - A reusable text editing and form canvas system -//! -//! This crate provides a generic canvas abstraction for building text-based interfaces -//! with multiple input fields, cursor management, and mode-based editing. -pub mod state; pub mod actions; -pub mod modes; pub mod config; -pub mod suggestions; pub mod dispatcher; +pub mod state; +pub mod suggestions; // Keep for backwards compatibility +pub mod autocomplete; // NEW: Core autocomplete functionality +pub mod modes; -// GUI module (optional, enabled with "gui" feature) #[cfg(feature = "gui")] pub mod gui; -// Re-export the main types for easy use -pub use state::{CanvasState, ActionContext}; -pub use actions::{CanvasAction, ActionResult, execute_edit_action, execute_canvas_action}; -pub use modes::{AppMode, ModeManager, HighlightState}; -pub use suggestions::SuggestionState; +// Re-export commonly used types +pub use actions::{CanvasAction, ActionResult}; pub use dispatcher::ActionDispatcher; +pub use state::{CanvasState, ActionContext}; +pub use autocomplete::{SuggestionItem, AutocompleteState}; // NEW +pub use modes::{AppMode, ModeManager, HighlightState}; -// Re-export GUI types when available #[cfg(feature = "gui")] -pub use gui::{CanvasTheme, render_canvas}; +pub use gui::{render_canvas, CanvasTheme}; -// High-level convenience API -pub mod prelude { - pub use crate::{ - CanvasState, - ActionContext, - CanvasAction, - ActionResult, - execute_edit_action, - execute_canvas_action, - ActionDispatcher, - AppMode, - ModeManager, - HighlightState, - SuggestionState, - }; - - #[cfg(feature = "gui")] - pub use crate::{CanvasTheme, render_canvas}; -} +// Keep backwards compatibility exports +pub use suggestions::SuggestionState; diff --git a/canvas/src/state.rs b/canvas/src/state.rs index 05125d1..e4e6d6b 100644 --- a/canvas/src/state.rs +++ b/canvas/src/state.rs @@ -1,6 +1,7 @@ // canvas/src/state.rs use crate::actions::CanvasAction; +use crate::autocomplete::{AutocompleteState, SuggestionItem}; /// Context passed to feature-specific action handlers #[derive(Debug)] @@ -31,29 +32,133 @@ pub trait CanvasState { fn has_unsaved_changes(&self) -> bool; fn set_has_unsaved_changes(&mut self, changed: bool); - // --- Autocomplete/Suggestions (Optional) --- + // --- AUTOCOMPLETE SUPPORT (NEW) --- + + /// Associated type for suggestion data (e.g., Hit, String, CustomType) + type SuggestionData: Clone + Send + 'static; + + /// Check if a field supports autocomplete + fn supports_autocomplete(&self, _field_index: usize) -> bool { + false // Default: no autocomplete support + } + + /// Get autocomplete state (read-only) + fn autocomplete_state(&self) -> Option<&AutocompleteState> { + None // Default: no autocomplete state + } + + /// Get autocomplete state (mutable) + fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState> { + None // Default: no autocomplete state + } + + /// CLIENT API: Activate autocomplete for current field + fn activate_autocomplete(&mut self) { + let current_field = self.current_field(); // Get field first + if let Some(state) = self.autocomplete_state_mut() { + state.activate(current_field); // Then use it + } + } + + /// CLIENT API: Deactivate autocomplete + fn deactivate_autocomplete(&mut self) { + if let Some(state) = self.autocomplete_state_mut() { + state.deactivate(); + } + } + + /// CLIENT API: Set suggestions (called after async fetch completes) + fn set_autocomplete_suggestions(&mut self, suggestions: Vec>) { + if let Some(state) = self.autocomplete_state_mut() { + state.set_suggestions(suggestions); + } + } + + /// CLIENT API: Set loading state + fn set_autocomplete_loading(&mut self, loading: bool) { + if let Some(state) = self.autocomplete_state_mut() { + state.is_loading = loading; + } + } + + /// Check if autocomplete is currently active + fn is_autocomplete_active(&self) -> bool { + self.autocomplete_state() + .map(|state| state.is_active) + .unwrap_or(false) + } + + /// Check if autocomplete is ready for interaction + fn is_autocomplete_ready(&self) -> bool { + self.autocomplete_state() + .map(|state| state.is_ready()) + .unwrap_or(false) + } + + /// INTERNAL: Apply selected autocomplete value to current field + fn apply_autocomplete_selection(&mut self) -> Option { + // First, get the selected value and display text (if any) + let selection_info = if let Some(state) = self.autocomplete_state() { + state.get_selected().map(|selected| { + (selected.value_to_store.clone(), selected.display_text.clone()) + }) + } else { + None + }; + + // Apply the selection if we have one + if let Some((value, display)) = selection_info { + // Apply the value to current field + *self.get_current_input_mut() = value; + self.set_has_unsaved_changes(true); + + // Deactivate autocomplete + if let Some(state_mut) = self.autocomplete_state_mut() { + state_mut.deactivate(); + } + + Some(format!("Selected: {}", display)) + } else { + None + } + } + + // --- LEGACY AUTOCOMPLETE SUPPORT (for backwards compatibility) --- + + /// Legacy suggestion support (deprecated - use autocomplete_state instead) fn get_suggestions(&self) -> Option<&[String]> { None } + + /// Legacy selected suggestion index (deprecated) fn get_selected_suggestion_index(&self) -> Option { - None - } - fn set_selected_suggestion_index(&mut self, _index: Option) { - // Default: no-op (override if you support suggestions) - } - fn activate_suggestions(&mut self, _suggestions: Vec) { - // Default: no-op (override if you support suggestions) - } - fn deactivate_suggestions(&mut self) { - // Default: no-op (override if you support suggestions) + self.autocomplete_state() + .and_then(|state| state.selected_index) } - // --- Feature-specific action handling (NEW: Type-safe) --- + /// Legacy suggestion index setter (deprecated) + fn set_selected_suggestion_index(&mut self, _index: Option) { + // Deprecated - canvas manages selection internally + } + + /// Legacy activate suggestions (deprecated) + fn activate_suggestions(&mut self, _suggestions: Vec) { + // Deprecated - use set_autocomplete_suggestions instead + } + + /// Legacy deactivate suggestions (deprecated) + fn deactivate_suggestions(&mut self) { + self.deactivate_autocomplete(); + } + + // --- Feature-specific action handling --- + + /// Feature-specific action handling (NEW: Type-safe) fn handle_feature_action(&mut self, _action: &CanvasAction, _context: &ActionContext) -> Option { None // Default: no feature-specific handling } - // --- Legacy string-based action handling (for backwards compatibility) --- + /// Legacy string-based action handling (for backwards compatibility) fn handle_feature_action_legacy(&mut self, action: &str, context: &ActionContext) -> Option { // Convert string to typed action and delegate let typed_action = match action { @@ -71,12 +176,14 @@ pub trait CanvasState { } // --- Display Overrides (for links, computed values, etc.) --- + fn get_display_value_for_field(&self, index: usize) -> &str { self.inputs() .get(index) .map(|s| s.as_str()) .unwrap_or("") } + fn has_display_override(&self, _index: usize) -> bool { false }