fixed generics

This commit is contained in:
Priec
2025-08-02 00:19:45 +02:00
parent 643db8e586
commit 2b08e64db8
2 changed files with 59 additions and 72 deletions

View File

@@ -8,23 +8,23 @@ use async_trait::async_trait;
pub trait DataProvider { pub trait DataProvider {
/// How many fields in the form /// How many fields in the form
fn field_count(&self) -> usize; fn field_count(&self) -> usize;
/// Get field label/name /// Get field label/name
fn field_name(&self, index: usize) -> &str; fn field_name(&self, index: usize) -> &str;
/// Get field value /// Get field value
fn field_value(&self, index: usize) -> &str; fn field_value(&self, index: usize) -> &str;
/// Set field value (library calls this when text changes) /// Set field value (library calls this when text changes)
fn set_field_value(&mut self, index: usize, value: String); fn set_field_value(&mut self, index: usize, value: String);
/// Check if field supports autocomplete (optional) /// Check if field supports autocomplete (optional)
fn supports_autocomplete(&self, _field_index: usize) -> bool { fn supports_autocomplete(&self, _field_index: usize) -> bool {
false false
} }
/// Get display value (for password masking, etc.) - optional /// Get display value (for password masking, etc.) - optional
fn display_value(&self, index: usize) -> Option<&str> { fn display_value(&self, _index: usize) -> Option<&str> {
None // Default: use actual value None // Default: use actual value
} }
} }
@@ -32,16 +32,13 @@ pub trait DataProvider {
/// Optional: User implements this for autocomplete data /// Optional: User implements this for autocomplete data
#[async_trait] #[async_trait]
pub trait AutocompleteProvider { pub trait AutocompleteProvider {
type SuggestionData: Clone + Send + 'static;
/// Fetch autocomplete suggestions (user's business logic) /// Fetch autocomplete suggestions (user's business logic)
async fn fetch_suggestions(&mut self, field_index: usize, query: &str) async fn fetch_suggestions(&mut self, field_index: usize, query: &str)
-> Result<Vec<SuggestionItem<Self::SuggestionData>>>; -> Result<Vec<SuggestionItem>>;
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SuggestionItem<T> { pub struct SuggestionItem {
pub data: T,
pub display_text: String, pub display_text: String,
pub value_to_store: String, pub value_to_store: String,
} }

View File

@@ -10,12 +10,12 @@ use crate::canvas::modes::AppMode;
pub struct FormEditor<D: DataProvider> { pub struct FormEditor<D: DataProvider> {
// Library owns all UI state // Library owns all UI state
ui_state: EditorState, ui_state: EditorState,
// User owns business data // User owns business data
data_provider: D, data_provider: D,
// Autocomplete suggestions (library manages UI, user provides data) // Autocomplete suggestions (library manages UI, user provides data)
pub(crate) suggestions: Vec<SuggestionItem<String>>, pub(crate) suggestions: Vec<SuggestionItem>,
} }
impl<D: DataProvider> FormEditor<D> { impl<D: DataProvider> FormEditor<D> {
@@ -26,31 +26,31 @@ impl<D: DataProvider> FormEditor<D> {
suggestions: Vec::new(), suggestions: Vec::new(),
} }
} }
// =================================================================== // ===================================================================
// READ-ONLY ACCESS: User can fetch UI state // READ-ONLY ACCESS: User can fetch UI state
// =================================================================== // ===================================================================
/// Get current field index (for user's compatibility) /// Get current field index (for user's compatibility)
pub fn current_field(&self) -> usize { pub fn current_field(&self) -> usize {
self.ui_state.current_field() self.ui_state.current_field()
} }
/// Get current cursor position (for user's compatibility) /// Get current cursor position (for user's compatibility)
pub fn cursor_position(&self) -> usize { pub fn cursor_position(&self) -> usize {
self.ui_state.cursor_position() self.ui_state.cursor_position()
} }
/// Get current mode (for user's mode-dependent logic) /// Get current mode (for user's mode-dependent logic)
pub fn mode(&self) -> AppMode { pub fn mode(&self) -> AppMode {
self.ui_state.mode() self.ui_state.mode()
} }
/// Check if autocomplete is active (for user's logic) /// Check if autocomplete is active (for user's logic)
pub fn is_autocomplete_active(&self) -> bool { pub fn is_autocomplete_active(&self) -> bool {
self.ui_state.is_autocomplete_active() self.ui_state.is_autocomplete_active()
} }
/// Get current field text (convenience method) /// Get current field text (convenience method)
pub fn current_text(&self) -> &str { pub fn current_text(&self) -> &str {
let field_index = self.ui_state.current_field; let field_index = self.ui_state.current_field;
@@ -60,51 +60,51 @@ impl<D: DataProvider> FormEditor<D> {
"" ""
} }
} }
/// Get reference to UI state for rendering /// Get reference to UI state for rendering
pub fn ui_state(&self) -> &EditorState { pub fn ui_state(&self) -> &EditorState {
&self.ui_state &self.ui_state
} }
/// Get reference to data provider for rendering /// Get reference to data provider for rendering
pub fn data_provider(&self) -> &D { pub fn data_provider(&self) -> &D {
&self.data_provider &self.data_provider
} }
/// Get autocomplete suggestions for rendering (read-only) /// Get autocomplete suggestions for rendering (read-only)
pub fn suggestions(&self) -> &[SuggestionItem<String>] { pub fn suggestions(&self) -> &[SuggestionItem] {
&self.suggestions &self.suggestions
} }
// =================================================================== // ===================================================================
// SYNC OPERATIONS: No async needed for basic editing // SYNC OPERATIONS: No async needed for basic editing
// =================================================================== // ===================================================================
/// Handle character insertion /// Handle character insertion
pub fn insert_char(&mut self, ch: char) -> Result<()> { pub fn insert_char(&mut self, ch: char) -> Result<()> {
if self.ui_state.current_mode != AppMode::Edit { if self.ui_state.current_mode != AppMode::Edit {
return Ok(()); // Ignore in non-edit modes return Ok(()); // Ignore in non-edit modes
} }
let field_index = self.ui_state.current_field; let field_index = self.ui_state.current_field;
let cursor_pos = self.ui_state.cursor_pos; let cursor_pos = self.ui_state.cursor_pos;
// Get current text from user // Get current text from user
let mut current_text = self.data_provider.field_value(field_index).to_string(); let mut current_text = self.data_provider.field_value(field_index).to_string();
// Insert character // Insert character
current_text.insert(cursor_pos, ch); current_text.insert(cursor_pos, ch);
// Update user's data // Update user's data
self.data_provider.set_field_value(field_index, current_text); self.data_provider.set_field_value(field_index, current_text);
// Update library's UI state // Update library's UI state
self.ui_state.cursor_pos += 1; self.ui_state.cursor_pos += 1;
self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos;
Ok(()) Ok(())
} }
/// Handle cursor movement /// Handle cursor movement
pub fn move_left(&mut self) { pub fn move_left(&mut self) {
if self.ui_state.cursor_pos > 0 { if self.ui_state.cursor_pos > 0 {
@@ -112,7 +112,7 @@ impl<D: DataProvider> FormEditor<D> {
self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos;
} }
} }
pub fn move_right(&mut self) { pub fn move_right(&mut self) {
let current_text = self.current_text(); let current_text = self.current_text();
let max_pos = if self.ui_state.current_mode == AppMode::Edit { let max_pos = if self.ui_state.current_mode == AppMode::Edit {
@@ -120,111 +120,101 @@ impl<D: DataProvider> FormEditor<D> {
} else { } else {
current_text.len().saturating_sub(1) // ReadOnly: stay in bounds current_text.len().saturating_sub(1) // ReadOnly: stay in bounds
}; };
if self.ui_state.cursor_pos < max_pos { if self.ui_state.cursor_pos < max_pos {
self.ui_state.cursor_pos += 1; self.ui_state.cursor_pos += 1;
self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos;
} }
} }
/// Handle field navigation /// Handle field navigation
pub fn move_to_next_field(&mut self) { pub fn move_to_next_field(&mut self) {
let field_count = self.data_provider.field_count(); let field_count = self.data_provider.field_count();
let next_field = (self.ui_state.current_field + 1) % field_count; let next_field = (self.ui_state.current_field + 1) % field_count;
self.ui_state.move_to_field(next_field, field_count); self.ui_state.move_to_field(next_field, field_count);
// Clamp cursor to new field // Clamp cursor to new field
let current_text = self.current_text(); let current_text = self.current_text();
let max_pos = current_text.len(); let max_pos = current_text.len();
self.ui_state.set_cursor( self.ui_state.set_cursor(
self.ui_state.ideal_cursor_column, self.ui_state.ideal_cursor_column,
max_pos, max_pos,
self.ui_state.current_mode == AppMode::Edit self.ui_state.current_mode == AppMode::Edit
); );
} }
/// Change mode (for vim compatibility) /// Change mode (for vim compatibility)
pub fn set_mode(&mut self, mode: AppMode) { pub fn set_mode(&mut self, mode: AppMode) {
self.ui_state.current_mode = mode; self.ui_state.current_mode = mode;
// Clear autocomplete when changing modes // Clear autocomplete when changing modes
if mode != AppMode::Edit { if mode != AppMode::Edit {
self.ui_state.deactivate_autocomplete(); self.ui_state.deactivate_autocomplete();
} }
} }
// =================================================================== // ===================================================================
// ASYNC OPERATIONS: Only autocomplete needs async // ASYNC OPERATIONS: Only autocomplete needs async
// =================================================================== // ===================================================================
/// Trigger autocomplete (async because it fetches data) /// Trigger autocomplete (async because it fetches data)
pub async fn trigger_autocomplete<A>(&mut self, provider: &mut A) -> Result<()> pub async fn trigger_autocomplete<A>(&mut self, provider: &mut A) -> Result<()>
where where
A: AutocompleteProvider, A: AutocompleteProvider,
A::SuggestionData: std::fmt::Debug, // Change from Display to Debug
{ {
let field_index = self.ui_state.current_field; let field_index = self.ui_state.current_field;
if !self.data_provider.supports_autocomplete(field_index) { if !self.data_provider.supports_autocomplete(field_index) {
return Ok(()); return Ok(());
} }
// Activate autocomplete UI // Activate autocomplete UI
self.ui_state.activate_autocomplete(field_index); self.ui_state.activate_autocomplete(field_index);
// Fetch suggestions from user // Fetch suggestions from user (no conversion needed!)
let query = self.current_text(); let query = self.current_text();
let suggestions = provider.fetch_suggestions(field_index, query).await?; self.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 // Update UI state
self.ui_state.autocomplete.is_loading = false; self.ui_state.autocomplete.is_loading = false;
if !self.suggestions.is_empty() { if !self.suggestions.is_empty() {
self.ui_state.autocomplete.selected_index = Some(0); self.ui_state.autocomplete.selected_index = Some(0);
} }
Ok(()) Ok(())
} }
/// Navigate autocomplete suggestions /// Navigate autocomplete suggestions
pub fn autocomplete_next(&mut self) { pub fn autocomplete_next(&mut self) {
if !self.ui_state.autocomplete.is_active || self.suggestions.is_empty() { if !self.ui_state.autocomplete.is_active || self.suggestions.is_empty() {
return; return;
} }
let current = self.ui_state.autocomplete.selected_index.unwrap_or(0); let current = self.ui_state.autocomplete.selected_index.unwrap_or(0);
let next = (current + 1) % self.suggestions.len(); let next = (current + 1) % self.suggestions.len();
self.ui_state.autocomplete.selected_index = Some(next); self.ui_state.autocomplete.selected_index = Some(next);
} }
/// Apply selected autocomplete suggestion /// Apply selected autocomplete suggestion
pub fn apply_autocomplete(&mut self) -> Option<String> { pub fn apply_autocomplete(&mut self) -> Option<String> {
if let Some(selected_index) = self.ui_state.autocomplete.selected_index { if let Some(selected_index) = self.ui_state.autocomplete.selected_index {
if let Some(suggestion) = self.suggestions.get(selected_index).cloned() { if let Some(suggestion) = self.suggestions.get(selected_index).cloned() {
let field_index = self.ui_state.current_field; let field_index = self.ui_state.current_field;
// Apply to user's data // Apply to user's data
self.data_provider.set_field_value( self.data_provider.set_field_value(
field_index, field_index,
suggestion.value_to_store.clone() suggestion.value_to_store.clone()
); );
// Update cursor position // Update cursor position
self.ui_state.cursor_pos = suggestion.value_to_store.len(); self.ui_state.cursor_pos = suggestion.value_to_store.len();
self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos; self.ui_state.ideal_cursor_column = self.ui_state.cursor_pos;
// Close autocomplete // Close autocomplete
self.ui_state.deactivate_autocomplete(); self.ui_state.deactivate_autocomplete();
self.suggestions.clear(); self.suggestions.clear();
return Some(suggestion.display_text); return Some(suggestion.display_text);
} }
} }