compiled autocomplete

This commit is contained in:
Priec
2025-07-29 21:46:55 +02:00
parent 1a451a576f
commit b8e1b77222
6 changed files with 479 additions and 112 deletions

View File

@@ -23,8 +23,8 @@ pub async fn execute_canvas_action<S: CanvasState>(
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<S: CanvasState>(
};
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<S: CanvasState>(
/// Handle autocomplete-related actions
fn handle_autocomplete_action<S: CanvasState>(
action: &CanvasAction,
state: &mut S,
) -> Result<Option<ActionResult>> {
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<S: CanvasState>(
let cursor_pos = state.current_cursor_pos();
let field_value = state.get_current_input_mut();
let mut chars: Vec<char> = 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<S: CanvasState>(
let cursor_pos = state.current_cursor_pos();
let field_value = state.get_current_input_mut();
let mut chars: Vec<char> = 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<S: CanvasState>(
let cursor_pos = state.current_cursor_pos();
let field_value = state.get_current_input_mut();
let mut chars: Vec<char> = 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<S: CanvasState>(
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 {

View File

@@ -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<Self> {
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<String>) -> Self {
Self::Success(Some(msg.into()))
}
pub fn error(msg: impl Into<String>) -> 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());
}
}

126
canvas/src/autocomplete.rs Normal file
View File

@@ -0,0 +1,126 @@
// canvas/src/autocomplete.rs
/// Generic suggestion item that clients push to canvas
#[derive(Debug, Clone)]
pub struct SuggestionItem<T> {
/// 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<T> SuggestionItem<T> {
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<T> {
/// 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<SuggestionItem<T>>,
/// Currently selected suggestion index
pub selected_index: Option<usize>,
/// Field index that triggered autocomplete (for context)
pub active_field: Option<usize>,
}
impl<T> Default for AutocompleteState<T> {
fn default() -> Self {
Self {
is_active: false,
is_loading: false,
suggestions: Vec::new(),
selected_index: None,
active_field: None,
}
}
}
impl<T> AutocompleteState<T> {
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<SuggestionItem<T>>) {
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<T>> {
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
}
}

View File

@@ -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<T: CanvasTheme>(
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<T: CanvasTheme>(
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<T: CanvasTheme, S: CanvasState>(
f: &mut Frame,
frame_area: Rect,
input_rect: Rect,
theme: &T,
autocomplete_state: &crate::autocomplete::AutocompleteState<S::SuggestionData>,
) {
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<ListItem> = 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<T: CanvasTheme, F1, F2>(
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)

View File

@@ -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;

View File

@@ -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<Self::SuggestionData>> {
None // Default: no autocomplete state
}
/// Get autocomplete state (mutable)
fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState<Self::SuggestionData>> {
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<SuggestionItem<Self::SuggestionData>>) {
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<String> {
// 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<usize> {
None
}
fn set_selected_suggestion_index(&mut self, _index: Option<usize>) {
// Default: no-op (override if you support suggestions)
}
fn activate_suggestions(&mut self, _suggestions: Vec<String>) {
// 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<usize>) {
// Deprecated - canvas manages selection internally
}
/// Legacy activate suggestions (deprecated)
fn activate_suggestions(&mut self, _suggestions: Vec<String>) {
// 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<String> {
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<String> {
// 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
}