fixed generics
This commit is contained in:
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user