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)); return Ok(ActionResult::HandledByFeature(result));
} }
// 2. Handle suggestion actions // 2. Handle autocomplete actions
if let Some(result) = handle_suggestion_action(&action, state)? { if let Some(result) = handle_autocomplete_action(&action, state)? {
return Ok(result); return Ok(result);
} }
@@ -56,47 +56,62 @@ pub async fn execute_edit_action<S: CanvasState>(
Ok(result.message().unwrap_or("").to_string()) Ok(result.message().unwrap_or("").to_string())
} }
/// Handle suggestion-related actions /// Handle autocomplete-related actions
fn handle_suggestion_action<S: CanvasState>( fn handle_autocomplete_action<S: CanvasState>(
action: &CanvasAction, action: &CanvasAction,
state: &mut S, state: &mut S,
) -> Result<Option<ActionResult>> { ) -> Result<Option<ActionResult>> {
match action { 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 => { CanvasAction::SuggestionDown => {
if let Some(suggestions) = state.get_suggestions() { if state.is_autocomplete_ready() {
if !suggestions.is_empty() { if let Some(autocomplete_state) = state.autocomplete_state_mut() {
let current = state.get_selected_suggestion_index().unwrap_or(0); autocomplete_state.select_next();
let next = (current + 1) % suggestions.len();
state.set_selected_suggestion_index(Some(next));
return Ok(Some(ActionResult::success())); return Ok(Some(ActionResult::success()));
} }
} }
Ok(None) Ok(None) // Not handled - no active autocomplete
} }
CanvasAction::SuggestionUp => { CanvasAction::SuggestionUp => {
if let Some(suggestions) = state.get_suggestions() { if state.is_autocomplete_ready() {
if !suggestions.is_empty() { if let Some(autocomplete_state) = state.autocomplete_state_mut() {
let current = state.get_selected_suggestion_index().unwrap_or(0); autocomplete_state.select_previous();
let prev = if current == 0 { suggestions.len() - 1 } else { current - 1 };
state.set_selected_suggestion_index(Some(prev));
return Ok(Some(ActionResult::success())); return Ok(Some(ActionResult::success()));
} }
} }
Ok(None) Ok(None) // Not handled - no active autocomplete
} }
CanvasAction::SelectSuggestion => { CanvasAction::SelectSuggestion => {
// Let feature handle this via handle_feature_action since it's feature-specific if state.is_autocomplete_ready() {
Ok(None) 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 => { CanvasAction::ExitSuggestions => {
state.deactivate_suggestions(); if state.is_autocomplete_active() {
Ok(Some(ActionResult::success())) 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
} }
} }
@@ -321,15 +336,15 @@ async fn handle_generic_canvas_action<S: CanvasState>(
Ok(ActionResult::error(format!("Unknown or unhandled custom action: {}", action_str))) Ok(ActionResult::error(format!("Unknown or unhandled custom action: {}", action_str)))
} }
// Suggestion actions should have been handled above // Autocomplete actions should have been handled above
CanvasAction::SuggestionUp | CanvasAction::SuggestionDown | CanvasAction::TriggerAutocomplete | CanvasAction::SuggestionUp | CanvasAction::SuggestionDown |
CanvasAction::SelectSuggestion | CanvasAction::ExitSuggestions => { 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)] #[derive(PartialEq)]
enum CharType { enum CharType {

View File

@@ -34,10 +34,16 @@ pub enum CanvasAction {
NextField, NextField,
PrevField, PrevField,
// Suggestions // AUTOCOMPLETE ACTIONS (NEW)
/// Manually trigger autocomplete for current field
TriggerAutocomplete,
/// Move to next suggestion
SuggestionUp, SuggestionUp,
/// Move to previous suggestion
SuggestionDown, SuggestionDown,
/// Select the currently highlighted suggestion
SelectSuggestion, SelectSuggestion,
/// Cancel/exit autocomplete mode
ExitSuggestions, ExitSuggestions,
// Custom actions (escape hatch for feature-specific behavior) // Custom actions (escape hatch for feature-specific behavior)
@@ -69,6 +75,8 @@ impl CanvasAction {
"move_word_end_prev" => Self::MoveWordEndPrev, "move_word_end_prev" => Self::MoveWordEndPrev,
"next_field" => Self::NextField, "next_field" => Self::NextField,
"prev_field" => Self::PrevField, "prev_field" => Self::PrevField,
// Autocomplete actions
"trigger_autocomplete" => Self::TriggerAutocomplete,
"suggestion_up" => Self::SuggestionUp, "suggestion_up" => Self::SuggestionUp,
"suggestion_down" => Self::SuggestionDown, "suggestion_down" => Self::SuggestionDown,
"select_suggestion" => Self::SelectSuggestion, "select_suggestion" => Self::SelectSuggestion,
@@ -97,6 +105,8 @@ impl CanvasAction {
Self::MoveWordEndPrev => "move_word_end_prev", Self::MoveWordEndPrev => "move_word_end_prev",
Self::NextField => "next_field", Self::NextField => "next_field",
Self::PrevField => "prev_field", Self::PrevField => "prev_field",
// Autocomplete actions
Self::TriggerAutocomplete => "trigger_autocomplete",
Self::SuggestionUp => "suggestion_up", Self::SuggestionUp => "suggestion_up",
Self::SuggestionDown => "suggestion_down", Self::SuggestionDown => "suggestion_down",
Self::SelectSuggestion => "select_suggestion", Self::SelectSuggestion => "select_suggestion",
@@ -146,7 +156,7 @@ impl CanvasAction {
/// Check if this is a suggestion-related action /// Check if this is a suggestion-related action
pub fn is_suggestion(&self) -> bool { pub fn is_suggestion(&self) -> bool {
matches!(self, matches!(self,
Self::SuggestionUp | Self::SuggestionDown | Self::TriggerAutocomplete | Self::SuggestionUp | Self::SuggestionDown |
Self::SelectSuggestion | Self::ExitSuggestions Self::SelectSuggestion | Self::ExitSuggestions
) )
} }
@@ -200,6 +210,7 @@ mod tests {
fn test_action_from_string() { fn test_action_from_string() {
assert_eq!(CanvasAction::from_string("move_left"), CanvasAction::MoveLeft); assert_eq!(CanvasAction::from_string("move_left"), CanvasAction::MoveLeft);
assert_eq!(CanvasAction::from_string("delete_char_backward"), CanvasAction::DeleteBackward); 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())); assert_eq!(CanvasAction::from_string("unknown"), CanvasAction::Custom("unknown".to_string()));
} }
@@ -220,6 +231,7 @@ mod tests {
assert!(!CanvasAction::InsertChar('a').is_movement()); assert!(!CanvasAction::InsertChar('a').is_movement());
assert!(CanvasAction::SuggestionUp.is_suggestion()); assert!(CanvasAction::SuggestionUp.is_suggestion());
assert!(CanvasAction::TriggerAutocomplete.is_suggestion());
assert!(!CanvasAction::MoveLeft.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}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style}, style::{Modifier, Style},
text::{Line, Span}, text::{Line, Span},
widgets::{Block, Borders, BorderType, Paragraph}, widgets::{Block, Borders, BorderType, List, ListItem, ListState, Paragraph},
Frame, Frame,
}; };
use crate::state::CanvasState; use crate::state::CanvasState;
@@ -14,6 +14,8 @@ use crate::modes::HighlightState;
use super::theme::CanvasTheme; use super::theme::CanvasTheme;
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
use std::cmp::{max, min}; use std::cmp::{max, min};
#[cfg(feature = "gui")]
use unicode_width::UnicodeWidthStr;
/// Render canvas using the CanvasState trait and CanvasTheme /// Render canvas using the CanvasState trait and CanvasTheme
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
@@ -29,7 +31,7 @@ pub fn render_canvas<T: CanvasTheme>(
let current_field_idx = form_state.current_field(); let current_field_idx = form_state.current_field();
let inputs: Vec<&String> = form_state.inputs(); let inputs: Vec<&String> = form_state.inputs();
render_canvas_impl( let active_field_rect = render_canvas_impl(
f, f,
area, area,
&fields, &fields,
@@ -42,10 +44,137 @@ pub fn render_canvas<T: CanvasTheme>(
form_state.has_unsaved_changes(), form_state.has_unsaved_changes(),
|i| form_state.get_display_value_for_field(i).to_string(), |i| form_state.get_display_value_for_field(i).to_string(),
|i| form_state.has_display_override(i), |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")] #[cfg(feature = "gui")]
fn render_canvas_impl<T: CanvasTheme, F1, F2>( fn render_canvas_impl<T: CanvasTheme, F1, F2>(
f: &mut Frame, f: &mut Frame,

View File

@@ -1,47 +1,25 @@
// canvas/src/lib.rs // 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 actions;
pub mod modes;
pub mod config; pub mod config;
pub mod suggestions;
pub mod dispatcher; 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")] #[cfg(feature = "gui")]
pub mod gui; pub mod gui;
// Re-export the main types for easy use // Re-export commonly used types
pub use state::{CanvasState, ActionContext}; pub use actions::{CanvasAction, ActionResult};
pub use actions::{CanvasAction, ActionResult, execute_edit_action, execute_canvas_action};
pub use modes::{AppMode, ModeManager, HighlightState};
pub use suggestions::SuggestionState;
pub use dispatcher::ActionDispatcher; 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")] #[cfg(feature = "gui")]
pub use gui::{CanvasTheme, render_canvas}; pub use gui::{render_canvas, CanvasTheme};
// High-level convenience API // Keep backwards compatibility exports
pub mod prelude { pub use suggestions::SuggestionState;
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};
}

View File

@@ -1,6 +1,7 @@
// canvas/src/state.rs // canvas/src/state.rs
use crate::actions::CanvasAction; use crate::actions::CanvasAction;
use crate::autocomplete::{AutocompleteState, SuggestionItem};
/// Context passed to feature-specific action handlers /// Context passed to feature-specific action handlers
#[derive(Debug)] #[derive(Debug)]
@@ -31,29 +32,133 @@ pub trait CanvasState {
fn has_unsaved_changes(&self) -> bool; fn has_unsaved_changes(&self) -> bool;
fn set_has_unsaved_changes(&mut self, changed: 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]> { fn get_suggestions(&self) -> Option<&[String]> {
None None
} }
/// Legacy selected suggestion index (deprecated)
fn get_selected_suggestion_index(&self) -> Option<usize> { fn get_selected_suggestion_index(&self) -> Option<usize> {
None self.autocomplete_state()
} .and_then(|state| state.selected_index)
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)
} }
// --- 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> { fn handle_feature_action(&mut self, _action: &CanvasAction, _context: &ActionContext) -> Option<String> {
None // Default: no feature-specific handling 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> { fn handle_feature_action_legacy(&mut self, action: &str, context: &ActionContext) -> Option<String> {
// Convert string to typed action and delegate // Convert string to typed action and delegate
let typed_action = match action { let typed_action = match action {
@@ -71,12 +176,14 @@ pub trait CanvasState {
} }
// --- Display Overrides (for links, computed values, etc.) --- // --- Display Overrides (for links, computed values, etc.) ---
fn get_display_value_for_field(&self, index: usize) -> &str { fn get_display_value_for_field(&self, index: usize) -> &str {
self.inputs() self.inputs()
.get(index) .get(index)
.map(|s| s.as_str()) .map(|s| s.as_str())
.unwrap_or("") .unwrap_or("")
} }
fn has_display_override(&self, _index: usize) -> bool { fn has_display_override(&self, _index: usize) -> bool {
false false
} }