Compare commits

...

7 Commits

Author SHA1 Message Date
Priec
8e3c85991c fixed example, now working everything properly well 2025-08-07 23:30:31 +02:00
Priec
d3e5418221 fixed example of suggestions2 2025-08-07 20:05:39 +02:00
Priec
0d0e54032c better suggestions2 example, not there yet 2025-08-07 18:51:45 +02:00
Priec
a8de16f66d suggestions is getting more and more strong than ever before 2025-08-07 16:00:46 +02:00
Priec
5b2e0e976f fixing examples 2025-08-07 13:51:59 +02:00
Priec
d601134535 computed fields are working perfectly well now 2025-08-07 12:38:09 +02:00
Priec
dff320d534 autocomplete to suggestions 2025-08-07 12:08:02 +02:00
29 changed files with 2540 additions and 287 deletions

View File

@@ -2,7 +2,7 @@
## Overview ## Overview
This guide covers the migration from the legacy canvas library structure to the new clean, modular architecture. The new design separates core canvas functionality from autocomplete features, providing better type safety and maintainability. This guide covers the migration from the legacy canvas library structure to the new clean, modular architecture. The new design separates core canvas functionality from suggestions features, providing better type safety and maintainability.
## Key Changes ## Key Changes
@@ -10,7 +10,7 @@ This guide covers the migration from the legacy canvas library structure to the
``` ```
# Old Structure (LEGACY) # Old Structure (LEGACY)
src/ src/
├── state.rs # Mixed canvas + autocomplete ├── state.rs # Mixed canvas + suggestions
├── actions/edit.rs # Mixed concerns ├── actions/edit.rs # Mixed concerns
├── gui/render.rs # Everything together ├── gui/render.rs # Everything together
└── suggestions.rs # Legacy file └── suggestions.rs # Legacy file
@@ -21,9 +21,9 @@ src/
│ ├── state.rs # CanvasState trait only │ ├── state.rs # CanvasState trait only
│ ├── actions/edit.rs # Canvas actions only │ ├── actions/edit.rs # Canvas actions only
│ └── gui.rs # Canvas rendering │ └── gui.rs # Canvas rendering
├── autocomplete/ # Rich autocomplete features ├── suggestions/ # Suggestions dropdown features (not inline autocomplete)
│ ├── state.rs # AutocompleteCanvasState trait │ ├── state.rs # Suggestion provider types
│ ├── types.rs # SuggestionItem, AutocompleteState │ ├── gui.rs # Suggestions dropdown rendering
│ ├── actions.rs # Autocomplete actions │ ├── actions.rs # Autocomplete actions
│ └── gui.rs # Autocomplete dropdown rendering │ └── gui.rs # Autocomplete dropdown rendering
└── dispatcher.rs # Action routing └── dispatcher.rs # Action routing
@@ -31,7 +31,7 @@ src/
### 2. **Trait Separation** ### 2. **Trait Separation**
- **CanvasState**: Core form functionality (navigation, input, validation) - **CanvasState**: Core form functionality (navigation, input, validation)
- **AutocompleteCanvasState**: Optional rich autocomplete features - Suggestions module: Optional dropdown suggestions support
### 3. **Rich Suggestions** ### 3. **Rich Suggestions**
Replaced simple string suggestions with typed, rich suggestion objects. Replaced simple string suggestions with typed, rich suggestion objects.
@@ -93,34 +93,29 @@ impl CanvasState for YourFormState {
### Step 3: Implement Rich Autocomplete (Optional) ### Step 3: Implement Rich Autocomplete (Optional)
**If you want rich autocomplete features:** **If you want suggestions dropdown features:**
```rust ```rust
use canvas::autocomplete::{AutocompleteCanvasState, SuggestionItem, AutocompleteState}; use canvas::{SuggestionItem};
impl AutocompleteCanvasState for YourFormState { impl YourFormState {
type SuggestionData = YourDataType; // e.g., Hit, CustomRecord, etc. fn supports_suggestions(&self, field_index: usize) -> bool {
// Define which fields support suggestions
fn supports_autocomplete(&self, field_index: usize) -> bool {
// Define which fields support autocomplete
matches!(field_index, 2 | 3 | 5) // Example: only certain fields matches!(field_index, 2 | 3 | 5) // Example: only certain fields
} }
fn autocomplete_state(&self) -> Option<&AutocompleteState<Self::SuggestionData>> { // Manage your own suggestion state or rely on FormEditor APIs
Some(&self.autocomplete)
}
fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState<Self::SuggestionData>> { // Manage your own suggestion state or rely on FormEditor APIs
Some(&mut self.autocomplete)
}
} }
``` ```
**Add autocomplete field to your state:** **Add suggestions storage to your state (optional, if you need to persist outside the editor):**
```rust ```rust
pub struct YourFormState { pub struct YourFormState {
// ... existing fields // ... existing fields
pub autocomplete: AutocompleteState<YourDataType>, // Optional: your own suggestions cache if needed
// pub suggestion_cache: Vec<SuggestionItem>,
} }
``` ```
@@ -149,9 +144,9 @@ form_state.set_autocomplete_suggestions(suggestions);
**Old rendering:** **Old rendering:**
```rust ```rust
// Manual autocomplete rendering // Manual suggestions rendering
if form_state.autocomplete_active { if editor.is_suggestions_active() {
render_autocomplete_dropdown(/* ... */); suggestions::gui::render_suggestions_dropdown(/* ... */);
} }
``` ```
@@ -162,13 +157,12 @@ use canvas::canvas::render_canvas;
let active_field_rect = render_canvas(f, area, form_state, theme, edit_mode, highlight_state); let active_field_rect = render_canvas(f, area, form_state, theme, edit_mode, highlight_state);
// Optional: Rich autocomplete (if implementing AutocompleteCanvasState) // Suggestions dropdown (if active)
if form_state.is_autocomplete_active() { if editor.is_suggestions_active() {
if let Some(autocomplete_state) = form_state.autocomplete_state() { canvas::suggestions::render_suggestions_dropdown(
canvas::autocomplete::render_autocomplete_dropdown( f, f.area(), active_field_rect.unwrap(), theme, &editor
f, f.area(), active_field_rect.unwrap(), theme, autocomplete_state );
); }
}
} }
``` ```
@@ -181,16 +175,16 @@ form_state.deactivate_suggestions();
# NEW - Option A: Add your own method # NEW - Option A: Add your own method
impl YourFormState { impl YourFormState {
pub fn deactivate_autocomplete(&mut self) { pub fn deactivate_suggestions(&mut self) {
self.autocomplete_active = false; self.autocomplete_active = false;
self.autocomplete_suggestions.clear(); self.autocomplete_suggestions.clear();
self.selected_suggestion_index = None; self.selected_suggestion_index = None;
} }
} }
form_state.deactivate_autocomplete(); editor.ui_state_mut().deactivate_suggestions();
# NEW - Option B: Use rich autocomplete trait # NEW - Option B: Suggestions via editor APIs
form_state.deactivate_autocomplete(); // If implementing AutocompleteCanvasState editor.ui_state_mut().deactivate_suggestions();
``` ```
## Benefits of New Architecture ## Benefits of New Architecture
@@ -217,8 +211,8 @@ let suggestions: Vec<SuggestionItem<UserRecord>> = vec![
- **Display Overrides**: Show friendly text while storing normalized data - **Display Overrides**: Show friendly text while storing normalized data
### 4. **Future-Proof** ### 4. **Future-Proof**
- Easy to add new autocomplete features - Easy to add new suggestion features
- Canvas features don't interfere with autocomplete - Canvas features don't interfere with suggestions
- Modular: Use only what you need - Modular: Use only what you need
## Advanced Features ## Advanced Features
@@ -262,7 +256,7 @@ SuggestionItem::new(user, "John Doe (Manager)".to_string(), "123".to_string());
## Breaking Changes Summary ## Breaking Changes Summary
1. **Import paths changed**: Add `canvas::` or `dispatcher::` prefixes 1. **Import paths changed**: Add `canvas::` or `dispatcher::` prefixes
2. **Legacy suggestion methods removed**: Replace with rich autocomplete or custom methods 2. **Legacy suggestion methods removed**: Replace with SuggestionItem-based dropdown or custom methods
3. **No more simple suggestions**: Use `SuggestionItem` for typed suggestions 3. **No more simple suggestions**: Use `SuggestionItem` for typed suggestions
4. **Trait split**: `AutocompleteCanvasState` is now separate and optional 4. **Trait split**: `AutocompleteCanvasState` is now separate and optional
@@ -283,11 +277,11 @@ SuggestionItem::new(user, "John Doe (Manager)".to_string(), "123".to_string());
- [ ] Updated all import paths - [ ] Updated all import paths
- [ ] Removed legacy methods from CanvasState implementation - [ ] Removed legacy methods from CanvasState implementation
- [ ] Added custom autocomplete methods if needed - [ ] Added custom suggestion methods if needed
- [ ] Updated suggestion usage to SuggestionItem - [ ] Updated usage to SuggestionItem
- [ ] Updated rendering calls - [ ] Updated rendering calls
- [ ] Tested form functionality - [ ] Tested form functionality
- [ ] Tested autocomplete functionality (if using) - [ ] Tested suggestions functionality (if using)
## Example: Complete Migration ## Example: Complete Migration
@@ -305,29 +299,25 @@ impl CanvasState for FormState {
**After:** **After:**
```rust ```rust
use canvas::canvas::{CanvasState, CanvasAction}; use canvas::canvas::{CanvasState, CanvasAction};
use canvas::autocomplete::{AutocompleteCanvasState, SuggestionItem}; use canvas::SuggestionItem;
impl CanvasState for FormState { impl CanvasState for FormState {
// Only core canvas methods, no suggestion methods // Only core canvas methods
fn current_field(&self) -> usize { /* ... */ } fn current_field(&self) -> usize { /* ... */ }
fn get_current_input(&self) -> &str { /* ... */ } fn get_current_input(&self) -> &str { /* ... */ }
// ... other core methods only // ... other core methods only
} }
impl AutocompleteCanvasState for FormState { // Use FormEditor + SuggestionsProvider for suggestions dropdown
type SuggestionData = Hit; type SuggestionData = Hit;
fn supports_autocomplete(&self, field_index: usize) -> bool { fn supports_suggestions(&self, field_index: usize) -> bool {
self.fields[field_index].is_link self.fields[field_index].is_link
} }
fn autocomplete_state(&self) -> Option<&AutocompleteState<Self::SuggestionData>> { // Maintain suggestion state through FormEditor and DataProvider
Some(&self.autocomplete)
}
fn autocomplete_state_mut(&mut self) -> Option<&mut AutocompleteState<Self::SuggestionData>> { // Maintain suggestion state through FormEditor and DataProvider
Some(&mut self.autocomplete)
}
} }
``` ```

View File

@@ -30,21 +30,42 @@ tokio-test = "0.4.4"
[features] [features]
default = [] default = []
gui = ["ratatui"] gui = ["ratatui", "crossterm"]
autocomplete = ["tokio"] suggestions = ["tokio"]
cursor-style = ["crossterm"] cursor-style = ["crossterm"]
validation = ["regex"] validation = ["regex"]
computed = []
[[example]] [[example]]
name = "autocomplete" name = "suggestions"
required-features = ["autocomplete", "gui"] required-features = ["suggestions", "gui", "cursor-style"]
path = "examples/autocomplete.rs" path = "examples/suggestions.rs"
[[example]] [[example]]
name = "canvas_gui_demo" name = "canvas_gui_demo"
required-features = ["gui"] required-features = ["gui", "cursor-style"]
path = "examples/canvas_gui_demo.rs" path = "examples/canvas_gui_demo.rs"
[[example]] [[example]]
name = "validation_1" name = "validation_1"
required-features = ["gui", "validation"] required-features = ["gui", "validation"]
[[example]]
name = "validation_2"
required-features = ["gui", "validation"]
[[example]]
name = "validation_3"
required-features = ["gui", "validation"]
[[example]]
name = "validation_4"
required-features = ["gui", "validation"]
[[example]]
name = "validation_5"
required-features = ["gui", "validation"]
[[example]]
name = "computed_fields"
required-features = ["gui", "computed"]

View File

@@ -7,7 +7,7 @@ A reusable, type-safe canvas system for building form-based TUI applications wit
- **Type-Safe Actions**: No more string-based action names - everything is compile-time checked - **Type-Safe Actions**: No more string-based action names - everything is compile-time checked
- **Generic Design**: Implement `CanvasState` once, get navigation, editing, and suggestions for free - **Generic Design**: Implement `CanvasState` once, get navigation, editing, and suggestions for free
- **Vim-Like Experience**: Modal editing with familiar keybindings - **Vim-Like Experience**: Modal editing with familiar keybindings
- **Suggestion System**: Built-in autocomplete and suggestions support - **Suggestion System**: Built-in suggestions dropdown support
- **Framework Agnostic**: Works with any TUI framework or raw terminal handling - **Framework Agnostic**: Works with any TUI framework or raw terminal handling
- **Async Ready**: Full async/await support for modern Rust applications - **Async Ready**: Full async/await support for modern Rust applications
- **Batch Operations**: Execute multiple actions atomically - **Batch Operations**: Execute multiple actions atomically
@@ -144,7 +144,7 @@ pub enum CanvasAction {
## 🔧 Advanced Features ## 🔧 Advanced Features
### Suggestions and Autocomplete ### Suggestions Dropdown (not inline autocomplete)
```rust ```rust
impl CanvasState for MyForm { impl CanvasState for MyForm {
@@ -170,7 +170,7 @@ impl CanvasState for MyForm {
CanvasAction::SelectSuggestion => { CanvasAction::SelectSuggestion => {
if let Some(suggestion) = self.suggestions.get_selected() { if let Some(suggestion) = self.suggestions.get_selected() {
*self.get_current_input_mut() = suggestion.clone(); *self.get_current_input_mut() = suggestion.clone();
self.deactivate_autocomplete(); self.deactivate_suggestions();
Some("Applied suggestion".to_string()) Some("Applied suggestion".to_string())
} }
None None

View File

@@ -346,7 +346,7 @@ impl DataProvider for CursorDemoData {
self.fields[index].1 = value; self.fields[index].1 = value;
} }
fn supports_autocomplete(&self, _field_index: usize) -> bool { fn supports_suggestions(&self, _field_index: usize) -> bool {
false false
} }

View File

@@ -0,0 +1,620 @@
// examples/computed_fields.rs - COMPLETE WORKING VERSION
//! Demonstrates computed fields with the canvas library - Invoice Calculator Example
//!
//! This example REQUIRES the `computed` feature to compile.
//!
//! Run with:
//! cargo run --example computed_fields --features "gui,computed"
#[cfg(not(feature = "computed"))]
compile_error!(
"This example requires the 'computed' feature. \
Run with: cargo run --example computed_fields --features \"gui,computed\""
);
use std::io;
use crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style, Modifier},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame, Terminal,
};
use canvas::{
canvas::{gui::render_canvas_default, modes::AppMode},
DataProvider, FormEditor,
computed::{ComputedProvider, ComputedContext},
};
/// Invoice data with computed fields
struct InvoiceData {
fields: Vec<(String, String)>,
computed_indices: std::collections::HashSet<usize>,
}
impl InvoiceData {
fn new() -> Self {
let mut computed_indices = std::collections::HashSet::new();
// Mark computed fields (read-only, calculated)
computed_indices.insert(4); // Subtotal
computed_indices.insert(5); // Tax Amount
computed_indices.insert(6); // Total
Self {
fields: vec![
("📦 Product Name".to_string(), "".to_string()),
("🔢 Quantity".to_string(), "".to_string()),
("💰 Unit Price ($)".to_string(), "".to_string()),
("📊 Tax Rate (%)".to_string(), "".to_string()),
(" Subtotal ($)".to_string(), "".to_string()), // COMPUTED
("🧾 Tax Amount ($)".to_string(), "".to_string()), // COMPUTED
("💳 Total ($)".to_string(), "".to_string()), // COMPUTED
("📝 Notes".to_string(), "".to_string()),
],
computed_indices,
}
}
}
impl DataProvider for InvoiceData {
fn field_count(&self) -> usize {
self.fields.len()
}
fn field_name(&self, index: usize) -> &str {
&self.fields[index].0
}
fn field_value(&self, index: usize) -> &str {
&self.fields[index].1
}
fn set_field_value(&mut self, index: usize, value: String) {
// 🔥 FIXED: Allow computed fields to be updated for display purposes
// The editing protection happens at the editor level, not here
self.fields[index].1 = value;
}
fn supports_suggestions(&self, _field_index: usize) -> bool {
false
}
fn display_value(&self, _index: usize) -> Option<&str> {
None
}
/// Mark computed fields
fn is_computed_field(&self, field_index: usize) -> bool {
self.computed_indices.contains(&field_index)
}
/// Get computed field values
fn computed_field_value(&self, field_index: usize) -> Option<String> {
if self.computed_indices.contains(&field_index) {
Some(self.fields[field_index].1.clone())
} else {
None
}
}
}
/// Invoice calculator - computes totals based on input fields
struct InvoiceCalculator;
impl ComputedProvider for InvoiceCalculator {
fn compute_field(&mut self, context: ComputedContext) -> String {
// Helper to parse field values safely
let parse_field = |index: usize| -> f64 {
let value = context.field_values[index].trim();
if value.is_empty() {
0.0
} else {
value.parse().unwrap_or(0.0)
}
};
match context.target_field {
4 => {
// Subtotal = Quantity × Unit Price
let qty = parse_field(1);
let price = parse_field(2);
let subtotal = qty * price;
if qty == 0.0 || price == 0.0 {
"".to_string() // Show empty if no meaningful calculation
} else {
format!("{:.2}", subtotal)
}
}
5 => {
// Tax Amount = Subtotal × (Tax Rate / 100)
let qty = parse_field(1);
let price = parse_field(2);
let tax_rate = parse_field(3);
let subtotal = qty * price;
let tax_amount = subtotal * (tax_rate / 100.0);
if subtotal == 0.0 || tax_rate == 0.0 {
"".to_string()
} else {
format!("{:.2}", tax_amount)
}
}
6 => {
// Total = Subtotal + Tax Amount
let qty = parse_field(1);
let price = parse_field(2);
let tax_rate = parse_field(3);
let subtotal = qty * price;
if subtotal == 0.0 {
"".to_string()
} else {
let tax_amount = subtotal * (tax_rate / 100.0);
let total = subtotal + tax_amount;
format!("{:.2}", total)
}
}
_ => "".to_string(),
}
}
fn handles_field(&self, field_index: usize) -> bool {
matches!(field_index, 4 | 5 | 6) // Subtotal, Tax Amount, Total
}
fn field_dependencies(&self, field_index: usize) -> Vec<usize> {
match field_index {
4 => vec![1, 2], // Subtotal depends on Quantity, Unit Price
5 => vec![1, 2, 3], // Tax Amount depends on Quantity, Unit Price, Tax Rate
6 => vec![1, 2, 3], // Total depends on Quantity, Unit Price, Tax Rate
_ => vec![],
}
}
}
/// Enhanced editor with computed fields
struct ComputedFieldsEditor<D: DataProvider> {
editor: FormEditor<D>,
calculator: InvoiceCalculator,
debug_message: String,
last_computed_values: Vec<String>,
}
impl<D: DataProvider> ComputedFieldsEditor<D> {
fn new(data_provider: D) -> Self {
let mut editor = FormEditor::new(data_provider);
editor.set_computed_provider(InvoiceCalculator);
let calculator = InvoiceCalculator;
let last_computed_values = vec!["".to_string(); 8];
Self {
editor,
calculator,
debug_message: "💰 Invoice Calculator - Start typing in fields to see calculations!".to_string(),
last_computed_values,
}
}
fn is_computed_field(&self, field_index: usize) -> bool {
self.editor.ui_state().is_computed_field(field_index)
}
fn update_computed_fields(&mut self) {
// Trigger recomputation of all computed fields
self.editor.recompute_all_fields(&mut self.calculator);
// 🔥 CRITICAL FIX: Sync computed values to DataProvider so GUI shows them!
for i in [4, 5, 6] { // Computed field indices
let computed_value = self.editor.effective_field_value(i);
self.editor.data_provider_mut().set_field_value(i, computed_value.clone());
}
// Check if values changed to show feedback
let mut changed = false;
let mut has_calculations = false;
for i in [4, 5, 6] {
let new_value = self.editor.effective_field_value(i);
if new_value != self.last_computed_values[i] {
changed = true;
self.last_computed_values[i] = new_value.clone();
}
if !new_value.is_empty() {
has_calculations = true;
}
}
if changed {
if has_calculations {
let subtotal = &self.last_computed_values[4];
let tax = &self.last_computed_values[5];
let total = &self.last_computed_values[6];
let mut parts = Vec::new();
if !subtotal.is_empty() {
parts.push(format!("Subtotal=${}", subtotal));
}
if !tax.is_empty() {
parts.push(format!("Tax=${}", tax));
}
if !total.is_empty() {
parts.push(format!("Total=${}", total));
}
if !parts.is_empty() {
self.debug_message = format!("🧮 Calculated: {}", parts.join(", "));
} else {
self.debug_message = "💰 Enter Quantity and Unit Price to see calculations".to_string();
}
} else {
self.debug_message = "💰 Enter Quantity and Unit Price to see calculations".to_string();
}
}
}
fn insert_char(&mut self, ch: char) -> anyhow::Result<()> {
let current_field = self.editor.current_field();
let result = self.editor.insert_char(ch);
if result.is_ok() && matches!(current_field, 1 | 2 | 3) {
self.editor.on_field_changed(&mut self.calculator, current_field);
self.update_computed_fields();
}
result
}
fn delete_backward(&mut self) -> anyhow::Result<()> {
let current_field = self.editor.current_field();
let result = self.editor.delete_backward();
if result.is_ok() && matches!(current_field, 1 | 2 | 3) {
self.editor.on_field_changed(&mut self.calculator, current_field);
self.update_computed_fields();
}
result
}
fn delete_forward(&mut self) -> anyhow::Result<()> {
let current_field = self.editor.current_field();
let result = self.editor.delete_forward();
if result.is_ok() && matches!(current_field, 1 | 2 | 3) {
self.editor.on_field_changed(&mut self.calculator, current_field);
self.update_computed_fields();
}
result
}
fn next_field(&mut self) {
let old_field = self.editor.current_field();
let _ = self.editor.next_field();
let new_field = self.editor.current_field();
if old_field != new_field {
let field_name = self.editor.data_provider().field_name(new_field);
let field_type = if self.is_computed_field(new_field) {
"computed (read-only)"
} else {
"editable"
};
self.debug_message = format!("{} - {} field", field_name, field_type);
}
}
fn prev_field(&mut self) {
let old_field = self.editor.current_field();
let _ = self.editor.prev_field();
let new_field = self.editor.current_field();
if old_field != new_field {
let field_name = self.editor.data_provider().field_name(new_field);
let field_type = if self.is_computed_field(new_field) {
"computed (read-only)"
} else {
"editable"
};
self.debug_message = format!("{} - {} field", field_name, field_type);
}
}
fn enter_edit_mode(&mut self) {
let current = self.editor.current_field();
// Double protection: check both ways
if self.editor.data_provider().is_computed_field(current) || self.is_computed_field(current) {
let field_name = self.editor.data_provider().field_name(current);
self.debug_message = format!(
"🚫 {} is computed (read-only) - Press Tab to move to editable fields",
field_name
);
return;
}
self.editor.enter_edit_mode();
let field_name = self.editor.data_provider().field_name(current);
self.debug_message = format!("✏️ Editing {} - Type to see calculations update", field_name);
}
fn enter_append_mode(&mut self) {
let current = self.editor.current_field();
if self.editor.data_provider().is_computed_field(current) || self.is_computed_field(current) {
let field_name = self.editor.data_provider().field_name(current);
self.debug_message = format!(
"🚫 {} is computed (read-only) - Press Tab to move to editable fields",
field_name
);
return;
}
self.editor.enter_append_mode();
let field_name = self.editor.data_provider().field_name(current);
self.debug_message = format!("✏️ Appending to {} - Type to see calculations", field_name);
}
fn exit_edit_mode(&mut self) {
let current_field = self.editor.current_field();
self.editor.exit_edit_mode();
if matches!(current_field, 1 | 2 | 3) {
self.editor.on_field_changed(&mut self.calculator, current_field);
self.update_computed_fields();
}
self.debug_message = "🔒 Normal mode - Press 'i' to edit fields".to_string();
}
// Delegate methods
fn current_field(&self) -> usize { self.editor.current_field() }
fn cursor_position(&self) -> usize { self.editor.cursor_position() }
fn mode(&self) -> AppMode { self.editor.mode() }
fn current_text(&self) -> &str { self.editor.current_text() }
fn data_provider(&self) -> &D { self.editor.data_provider() }
fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() }
fn move_left(&mut self) { self.editor.move_left(); }
fn move_right(&mut self) { self.editor.move_right(); }
fn move_up(&mut self) { let _ = self.editor.move_up(); }
fn move_down(&mut self) { let _ = self.editor.move_down(); }
}
fn handle_key_press(
key: KeyCode,
modifiers: KeyModifiers,
editor: &mut ComputedFieldsEditor<InvoiceData>,
) -> anyhow::Result<bool> {
let mode = editor.mode();
if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL))
|| (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL))
|| key == KeyCode::F(10)
{
return Ok(false);
}
match (mode, key, modifiers) {
(AppMode::ReadOnly, KeyCode::Char('i'), _) => {
editor.enter_edit_mode();
}
(AppMode::ReadOnly, KeyCode::Char('a'), _) => {
editor.enter_append_mode();
}
(AppMode::ReadOnly, KeyCode::Char('A'), _) => {
editor.editor.move_line_end();
editor.enter_edit_mode();
}
(_, KeyCode::Esc, _) => {
if mode == AppMode::Edit {
editor.exit_edit_mode();
}
}
// Movement
(AppMode::ReadOnly, KeyCode::Char('h'), _) | (AppMode::ReadOnly, KeyCode::Left, _) => {
editor.move_left();
}
(AppMode::ReadOnly, KeyCode::Char('l'), _) | (AppMode::ReadOnly, KeyCode::Right, _) => {
editor.move_right();
}
(AppMode::ReadOnly, KeyCode::Char('j'), _) | (AppMode::ReadOnly, KeyCode::Down, _) => {
editor.move_down();
}
(AppMode::ReadOnly, KeyCode::Char('k'), _) | (AppMode::ReadOnly, KeyCode::Up, _) => {
editor.move_up();
}
// Edit mode movement
(AppMode::Edit, KeyCode::Left, _) => { editor.move_left(); }
(AppMode::Edit, KeyCode::Right, _) => { editor.move_right(); }
(AppMode::Edit, KeyCode::Up, _) => { editor.move_up(); }
(AppMode::Edit, KeyCode::Down, _) => { editor.move_down(); }
// Navigation
(_, KeyCode::Tab, _) => {
editor.next_field();
}
(_, KeyCode::BackTab, _) => {
editor.prev_field();
}
// Editing
(AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => {
editor.insert_char(c)?;
}
(AppMode::Edit, KeyCode::Backspace, _) => {
editor.delete_backward()?;
}
(AppMode::Edit, KeyCode::Delete, _) => {
editor.delete_forward()?;
}
// Debug info
(AppMode::ReadOnly, KeyCode::Char('?'), _) => {
let current = editor.current_field();
let field_name = editor.data_provider().field_name(current);
let field_type = if editor.is_computed_field(current) {
"COMPUTED (read-only)"
} else {
"EDITABLE"
};
editor.debug_message = format!(
"{} - {} - Position {} - Mode: {:?}",
field_name, field_type, editor.cursor_position(), mode
);
}
_ => {}
}
Ok(true)
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut editor: ComputedFieldsEditor<InvoiceData>,
) -> io::Result<()> {
editor.update_computed_fields(); // Initial computation
loop {
terminal.draw(|f| ui(f, &editor))?;
if let Event::Key(key) = event::read()? {
match handle_key_press(key.code, key.modifiers, &mut editor) {
Ok(should_continue) => {
if !should_continue {
break;
}
}
Err(e) => {
editor.debug_message = format!("Error: {}", e);
}
}
}
}
Ok(())
}
fn ui(f: &mut Frame, editor: &ComputedFieldsEditor<InvoiceData>) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(8), Constraint::Length(10)])
.split(f.area());
render_enhanced_canvas(f, chunks[0], editor);
render_computed_status(f, chunks[1], editor);
}
fn render_enhanced_canvas(f: &mut Frame, area: Rect, editor: &ComputedFieldsEditor<InvoiceData>) {
render_canvas_default(f, area, &editor.editor);
}
fn render_computed_status(f: &mut Frame, area: Rect, editor: &ComputedFieldsEditor<InvoiceData>) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Length(7)])
.split(area);
let mode_text = match editor.mode() {
AppMode::Edit => "INSERT",
AppMode::ReadOnly => "NORMAL",
_ => "OTHER",
};
let current = editor.current_field();
let field_status = if editor.is_computed_field(current) {
"📊 COMPUTED FIELD (read-only)"
} else {
"✏️ EDITABLE FIELD"
};
let status_text = format!("-- {} -- {} | {}", mode_text, field_status, editor.debug_message);
let status = Paragraph::new(Line::from(Span::raw(status_text)))
.block(Block::default().borders(Borders::ALL).title("💰 Invoice Calculator"));
f.render_widget(status, chunks[0]);
let help_text = match editor.mode() {
AppMode::ReadOnly => {
"💰 COMPUTED FIELDS DEMO: Real-time invoice calculations!\n\
🔢 EDITABLE: Product, Quantity, Unit Price, Tax Rate, Notes\n\
📊 COMPUTED: Subtotal, Tax Amount, Total (calculated automatically)\n\
\n\
🚀 START: Press 'i' to edit Quantity, type '5', Tab to Unit Price, type '19.99'\n\
Watch Subtotal and Total appear! Add Tax Rate to see tax calculations.\n\
Navigation: Tab/Shift+Tab skips computed fields automatically"
}
AppMode::Edit => {
"✏️ EDIT MODE: Type numbers to see calculations appear!\n\
\n\
💡 EXAMPLE: Type '5' in Quantity, then Tab to Unit Price and type '19.99'\n\
• Subtotal appears: $99.95\n\
• Total appears: $99.95\n\
• Add Tax Rate (like '10') to see tax: $9.99, Total: $109.94\n\
\n\
Esc=normal, Tab=next field (auto-skips computed fields)"
}
_ => "💰 Invoice Calculator with Computed Fields"
};
let help_style = if editor.is_computed_field(editor.current_field()) {
Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC)
} else {
Style::default().fg(Color::Gray)
};
let help = Paragraph::new(help_text)
.block(Block::default().borders(Borders::ALL).title("🚀 Try It Now!"))
.style(help_style)
.wrap(Wrap { trim: true });
f.render_widget(help, chunks[1]);
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("💰 Canvas Computed Fields Demo - Invoice Calculator");
println!("✅ computed feature: ENABLED");
println!("🚀 QUICK TEST:");
println!(" 1. Press 'i' to edit Quantity");
println!(" 2. Type '5' and press Tab");
println!(" 3. Type '19.99' in Unit Price");
println!(" 4. Watch Subtotal ($99.95) and Total ($99.95) appear!");
println!();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let data = InvoiceData::new();
let editor = ComputedFieldsEditor::new(data);
let res = run_app(&mut terminal, editor);
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err);
}
println!("💰 Demo completed! Computed fields should have updated in real-time!");
Ok(())
}

View File

@@ -345,7 +345,7 @@ impl DataProvider for FullDemoData {
self.fields[index].1 = value; self.fields[index].1 = value;
} }
fn supports_autocomplete(&self, _field_index: usize) -> bool { fn supports_suggestions(&self, _field_index: usize) -> bool {
false false
} }

View File

@@ -1,5 +1,5 @@
// examples/autocomplete.rs // examples/suggestions.rs
// Run with: cargo run --example autocomplete --features "autocomplete,gui" // Run with: cargo run --example suggestions --features "suggestions,gui"
use std::io; use std::io;
use crossterm::{ use crossterm::{
@@ -22,8 +22,8 @@ use canvas::{
modes::AppMode, modes::AppMode,
theme::CanvasTheme, theme::CanvasTheme,
}, },
autocomplete::gui::render_autocomplete_dropdown, suggestions::gui::render_suggestions_dropdown,
FormEditor, DataProvider, AutocompleteProvider, SuggestionItem, FormEditor, DataProvider, SuggestionsProvider, SuggestionItem,
}; };
use async_trait::async_trait; use async_trait::async_trait;
@@ -108,7 +108,7 @@ impl DataProvider for ContactForm {
} }
} }
fn supports_autocomplete(&self, field_index: usize) -> bool { fn supports_suggestions(&self, field_index: usize) -> bool {
field_index == 1 // Only email field field_index == 1 // Only email field
} }
} }
@@ -120,11 +120,9 @@ impl DataProvider for ContactForm {
struct EmailAutocomplete; struct EmailAutocomplete;
#[async_trait] #[async_trait]
impl AutocompleteProvider for EmailAutocomplete { impl SuggestionsProvider for EmailAutocomplete {
type SuggestionData = EmailSuggestion;
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>>
{ {
// Extract domain part from email // Extract domain part from email
let (email_prefix, domain_part) = if let Some(at_pos) = query.find('@') { let (email_prefix, domain_part) = if let Some(at_pos) = query.find('@') {
@@ -153,10 +151,6 @@ impl AutocompleteProvider for EmailAutocomplete {
if domain.starts_with(&domain_part) || domain_part.is_empty() { if domain.starts_with(&domain_part) || domain_part.is_empty() {
let full_email = format!("{}@{}", email_prefix, domain); let full_email = format!("{}@{}", email_prefix, domain);
results.push(SuggestionItem { results.push(SuggestionItem {
data: EmailSuggestion {
email: full_email.clone(),
provider: provider.to_string(),
},
display_text: format!("{} ({})", full_email, provider), display_text: format!("{} ({})", full_email, provider),
value_to_store: full_email, value_to_store: full_email,
}); });
@@ -175,7 +169,7 @@ impl AutocompleteProvider for EmailAutocomplete {
struct AppState { struct AppState {
editor: FormEditor<ContactForm>, editor: FormEditor<ContactForm>,
autocomplete: EmailAutocomplete, suggestions_provider: EmailAutocomplete,
debug_message: String, debug_message: String,
} }
@@ -190,8 +184,8 @@ impl AppState {
Self { Self {
editor, editor,
autocomplete: EmailAutocomplete, suggestions_provider: EmailAutocomplete,
debug_message: "Type in email field, Tab to trigger autocomplete, Enter to select, Esc to cancel".to_string(), debug_message: "Type in email field, Tab to trigger suggestions, Enter to select, Esc to cancel".to_string(),
} }
} }
} }
@@ -207,14 +201,14 @@ async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut App
// Handle input based on key // Handle input based on key
let result = match key { let result = match key {
// === AUTOCOMPLETE KEYS === // === SUGGESTIONS KEYS ===
KeyCode::Tab => { KeyCode::Tab => {
if state.editor.is_autocomplete_active() { if state.editor.is_suggestions_active() {
state.editor.autocomplete_next(); state.editor.suggestions_next();
Ok("Navigated to next suggestion".to_string()) Ok("Navigated to next suggestion".to_string())
} else if state.editor.data_provider().supports_autocomplete(state.editor.current_field()) { } else if state.editor.data_provider().supports_suggestions(state.editor.current_field()) {
state.editor.trigger_autocomplete(&mut state.autocomplete).await state.editor.trigger_suggestions(&mut state.suggestions_provider).await
.map(|_| "Triggered autocomplete".to_string()) .map(|_| "Triggered suggestions".to_string())
} else { } else {
state.editor.move_to_next_field(); state.editor.move_to_next_field();
Ok("Moved to next field".to_string()) Ok("Moved to next field".to_string())
@@ -222,8 +216,8 @@ async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut App
} }
KeyCode::Enter => { KeyCode::Enter => {
if state.editor.is_autocomplete_active() { if state.editor.is_suggestions_active() {
if let Some(applied) = state.editor.apply_autocomplete() { if let Some(applied) = state.editor.apply_suggestion() {
Ok(format!("Applied: {}", applied)) Ok(format!("Applied: {}", applied))
} else { } else {
Ok("No suggestion to apply".to_string()) Ok("No suggestion to apply".to_string())
@@ -235,9 +229,9 @@ async fn handle_key_press(key: KeyCode, modifiers: KeyModifiers, state: &mut App
} }
KeyCode::Esc => { KeyCode::Esc => {
if state.editor.is_autocomplete_active() { if state.editor.is_suggestions_active() {
// Autocomplete will be cleared automatically by mode change // Suggestions will be cleared automatically by mode change
Ok("Cancelled autocomplete".to_string()) Ok("Cancelled suggestions".to_string())
} else { } else {
// Toggle between edit and readonly mode // Toggle between edit and readonly mode
let new_mode = match state.editor.mode() { let new_mode = match state.editor.mode() {
@@ -324,9 +318,9 @@ fn ui(f: &mut Frame, state: &AppState, theme: &DemoTheme) {
theme, theme,
); );
// Render autocomplete dropdown if active // Render suggestions dropdown if active
if let Some(input_rect) = active_field_rect { if let Some(input_rect) = active_field_rect {
render_autocomplete_dropdown( render_suggestions_dropdown(
f, f,
chunks[0], chunks[0],
input_rect, input_rect,
@@ -336,8 +330,8 @@ fn ui(f: &mut Frame, state: &AppState, theme: &DemoTheme) {
} }
// Status info // Status info
let autocomplete_status = if state.editor.is_autocomplete_active() { let autocomplete_status = if state.editor.is_suggestions_active() {
if state.editor.ui_state().is_autocomplete_loading() { if state.editor.ui_state().is_suggestions_loading() {
"Loading suggestions..." "Loading suggestions..."
} else if !state.editor.suggestions().is_empty() { } else if !state.editor.suggestions().is_empty() {
"Use Tab to navigate, Enter to select, Esc to cancel" "Use Tab to navigate, Enter to select, Esc to cancel"
@@ -345,7 +339,7 @@ fn ui(f: &mut Frame, state: &AppState, theme: &DemoTheme) {
"No suggestions found" "No suggestions found"
} }
} else { } else {
"Tab to trigger autocomplete" "Tab to trigger suggestions"
}; };
let status_lines = vec![ let status_lines = vec![
@@ -354,9 +348,9 @@ fn ui(f: &mut Frame, state: &AppState, theme: &DemoTheme) {
state.editor.current_field() + 1, state.editor.current_field() + 1,
state.editor.data_provider().field_count(), state.editor.data_provider().field_count(),
state.editor.cursor_position()))), state.editor.cursor_position()))),
Line::from(Span::raw(format!("Autocomplete: {}", autocomplete_status))), Line::from(Span::raw(format!("Suggestions: {}", autocomplete_status))),
Line::from(Span::raw(state.debug_message.clone())), Line::from(Span::raw(state.debug_message.clone())),
Line::from(Span::raw("F10: Quit | Tab: Trigger/Navigate autocomplete | Enter: Select | Esc: Cancel/Toggle mode")), Line::from(Span::raw("F10: Quit | Tab: Trigger/Navigate suggestions | Enter: Select | Esc: Cancel/Toggle mode")),
]; ];
let status = Paragraph::new(status_lines) let status = Paragraph::new(status_lines)

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
// examples/validation_1.rs // examples/validation_1.rs
//! Demonstrates field validation with the canvas library //! Demonstrates field validation with the canvas library
//! //!
//! This example REQUIRES the `validation` feature to compile. //! This example REQUIRES the `validation` and `cursor-style` features to compile.
//! //!
//! Run with: //! Run with:
//! cargo run --example validation_1 --features "gui,validation" //! cargo run --example validation_1 --features "gui,validation"
@@ -10,10 +10,10 @@
//! cargo run --example validation_1 --features "gui" //! cargo run --example validation_1 --features "gui"
// REQUIRE validation feature - example won't compile without it // REQUIRE validation feature - example won't compile without it
#[cfg(not(feature = "validation"))] #[cfg(not(all(feature = "validation", feature = "cursor-style")))]
compile_error!( compile_error!(
"This example requires the 'validation' feature. \ "This example requires the 'validation' and 'cursor-style' features. \
Run with: cargo run --example validation_1 --features \"gui,validation\"" Run with: cargo run --example validation_1 --features \"gui,validation,cursor-style\""
); );
use std::io; use std::io;
@@ -39,6 +39,7 @@ use canvas::{
canvas::{ canvas::{
gui::render_canvas_default, gui::render_canvas_default,
modes::AppMode, modes::AppMode,
CursorManager,
}, },
DataProvider, FormEditor, DataProvider, FormEditor,
ValidationConfig, ValidationConfigBuilder, CharacterLimits, ValidationResult, ValidationConfig, ValidationConfigBuilder, CharacterLimits, ValidationResult,
@@ -269,18 +270,21 @@ impl<D: DataProvider> ValidationFormEditor<D> {
// === MODE TRANSITIONS === // === MODE TRANSITIONS ===
fn enter_edit_mode(&mut self) { fn enter_edit_mode(&mut self) {
// Library will automatically update cursor to bar | in insert mode
self.editor.enter_edit_mode(); self.editor.enter_edit_mode();
self.debug_message = "✏️ INSERT MODE - Type to test validation".to_string(); self.debug_message = "✏️ INSERT MODE - Cursor: Steady Bar | - Type to test validation".to_string();
} }
fn enter_append_mode(&mut self) { fn enter_append_mode(&mut self) {
// Library will automatically update cursor to bar | in insert mode
self.editor.enter_append_mode(); self.editor.enter_append_mode();
self.debug_message = "✏️ INSERT (append) - Validation active".to_string(); self.debug_message = "✏️ INSERT (append) - Cursor: Steady Bar | - Validation active".to_string();
} }
fn exit_edit_mode(&mut self) { fn exit_edit_mode(&mut self) {
// Library will automatically update cursor to block █ in normal mode
self.editor.exit_edit_mode(); self.editor.exit_edit_mode();
self.debug_message = "🔒 NORMAL MODE - Press 'v' to validate current field".to_string(); self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █ - Press 'v' to validate current field".to_string();
self.update_field_validation_status(); self.update_field_validation_status();
} }
@@ -362,6 +366,7 @@ impl<D: DataProvider> ValidationFormEditor<D> {
} }
fn set_mode(&mut self, mode: AppMode) { fn set_mode(&mut self, mode: AppMode) {
// Library automatically updates cursor for the mode
self.editor.set_mode(mode); self.editor.set_mode(mode);
} }
@@ -447,7 +452,7 @@ impl DataProvider for ValidationDemoData {
self.fields[index].1 = value; self.fields[index].1 = value;
} }
fn supports_autocomplete(&self, _field_index: usize) -> bool { fn supports_suggestions(&self, _field_index: usize) -> bool {
false false
} }
@@ -694,9 +699,9 @@ fn render_validation_status(
// Status bar with validation information // Status bar with validation information
let mode_text = match editor.mode() { let mode_text = match editor.mode() {
AppMode::Edit => "INSERT", AppMode::Edit => "INSERT | (bar cursor)",
AppMode::ReadOnly => "NORMAL", AppMode::ReadOnly => "NORMAL █ (block cursor)",
_ => "OTHER", _ => "NORMAL █ (block cursor)",
}; };
let validation_status = editor.get_validation_status(); let validation_status = editor.get_validation_status();
@@ -765,21 +770,21 @@ fn render_validation_status(
// Enhanced help text // Enhanced help text
let help_text = match editor.mode() { let help_text = match editor.mode() {
AppMode::ReadOnly => { AppMode::ReadOnly => {
"🔍 VALIDATION DEMO: Different fields have different limits!\n\ "🎯 CURSOR-STYLE: Normal █ | Insert |\n\
Fields with MINIMUM requirements will block field switching if too short!\n\ 🔍 VALIDATION: Different fields have different limits (some block field switching)!\n\
Movement: hjkl/arrows=move, Tab/Shift+Tab=fields\n\ Movement: hjkl/arrows=move, Tab/Shift+Tab=fields\n\
Edit: i/a/A=insert modes, Esc=normal\n\ Edit: i/a/A=insert modes, Esc=normal\n\
Validation: v=validate current, V=validate all, c=clear results, F1=toggle\n\ Validation: v=validate current, V=validate all, c=clear results, F1=toggle\n\
?=info, Ctrl+C/Ctrl+Q=quit" ?=info, Ctrl+C/Ctrl+Q=quit"
} }
AppMode::Edit => { AppMode::Edit => {
"✏️ INSERT MODE - Type to test validation limits!\n\ "🎯 INSERT MODE - Cursor: | (bar)\n\
Some fields have MINIMUM character requirements!\n\ 🔍 Type to test validation limits (some fields have MIN requirements)!\n\
Try typing 1-2 chars in Password/ID/Comment fields, then try to switch!\n\ Try typing 1-2 chars in Password/ID/Comment fields, then try to switch!\n\
arrows=move, Backspace/Del=delete, Esc=normal, Tab=next field\n\ arrows=move, Backspace/Del=delete, Esc=normal, Tab=next field\n\
Field switching may be BLOCKED if minimum requirements not met!" Field switching may be BLOCKED if minimum requirements not met!"
} }
_ => "🔍 Validation Demo Active!" _ => "🎯 Watch the cursor change automatically while validating!"
}; };
let help = Paragraph::new(help_text) let help = Paragraph::new(help_text)
@@ -810,10 +815,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
let data = ValidationDemoData::new(); let data = ValidationDemoData::new();
let editor = ValidationFormEditor::new(data); let mut editor = ValidationFormEditor::new(data);
// Initialize with normal mode - library automatically sets block cursor
editor.set_mode(AppMode::ReadOnly);
// Demonstrate that CursorManager is available and working
CursorManager::update_for_mode(AppMode::ReadOnly)?;
let res = run_app(&mut terminal, editor); let res = run_app(&mut terminal, editor);
// Library automatically resets cursor on FormEditor::drop()
// But we can also manually reset if needed
CursorManager::reset()?;
disable_raw_mode()?; disable_raw_mode()?;
execute!( execute!(
terminal.backend_mut(), terminal.backend_mut(),

View File

@@ -4,13 +4,13 @@
//! This example showcases the full potential of the pattern validation system //! This example showcases the full potential of the pattern validation system
//! with creative real-world scenarios and edge cases. //! with creative real-world scenarios and edge cases.
//! //!
//! Run with: cargo run --example validation_advanced_patterns --features "validation,gui" //! Run with: cargo run --example validation_advanced_patterns --features "validation,gui,cursor-style"
// REQUIRE validation and gui features // REQUIRE validation, gui and cursor-style features
#[cfg(not(all(feature = "validation", feature = "gui")))] #[cfg(not(all(feature = "validation", feature = "gui", feature = "cursor-style")))]
compile_error!( compile_error!(
"This example requires the 'validation' and 'gui' features. \ "This example requires the 'validation', 'gui' and 'cursor-style' features. \
Run with: cargo run --example validation_advanced_patterns --features \"validation,gui\"" Run with: cargo run --example validation_advanced_patterns --features \"validation,gui,cursor-style\""
); );
use std::io; use std::io;
@@ -38,6 +38,7 @@ use canvas::{
canvas::{ canvas::{
gui::render_canvas_default, gui::render_canvas_default,
modes::AppMode, modes::AppMode,
CursorManager,
}, },
DataProvider, FormEditor, DataProvider, FormEditor,
ValidationConfig, ValidationConfigBuilder, PatternFilters, PositionFilter, PositionRange, CharacterFilter, ValidationConfig, ValidationConfigBuilder, PatternFilters, PositionFilter, PositionRange, CharacterFilter,
@@ -107,18 +108,21 @@ impl<D: DataProvider> AdvancedPatternFormEditor<D> {
fn move_line_end(&mut self) { self.editor.move_line_end(); } fn move_line_end(&mut self) { self.editor.move_line_end(); }
fn enter_edit_mode(&mut self) { fn enter_edit_mode(&mut self) {
// Library will automatically update cursor to bar | in insert mode
self.editor.enter_edit_mode(); self.editor.enter_edit_mode();
self.debug_message = "✏️ INSERT MODE - Testing advanced pattern validation".to_string(); self.debug_message = "✏️ INSERT MODE - Cursor: Steady Bar | - Testing advanced pattern validation".to_string();
} }
fn enter_append_mode(&mut self) { fn enter_append_mode(&mut self) {
// Library will automatically update cursor to bar | in insert mode
self.editor.enter_append_mode(); self.editor.enter_append_mode();
self.debug_message = "✏️ INSERT (append) - Advanced patterns active".to_string(); self.debug_message = "✏️ INSERT (append) - Cursor: Steady Bar | - Advanced patterns active".to_string();
} }
fn exit_edit_mode(&mut self) { fn exit_edit_mode(&mut self) {
// Library will automatically update cursor to block █ in normal mode
self.editor.exit_edit_mode(); self.editor.exit_edit_mode();
self.debug_message = "🔒 NORMAL MODE".to_string(); self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █".to_string();
self.update_field_validation_status(); self.update_field_validation_status();
} }
@@ -522,9 +526,9 @@ fn render_advanced_validation_status(
// Status bar // Status bar
let mode_text = match editor.mode() { let mode_text = match editor.mode() {
AppMode::Edit => "INSERT", AppMode::Edit => "INSERT | (bar cursor)",
AppMode::ReadOnly => "NORMAL", AppMode::ReadOnly => "NORMAL █ (block cursor)",
_ => "OTHER", _ => "NORMAL █ (block cursor)",
}; };
let validation_status = editor.get_validation_status(); let validation_status = editor.get_validation_status();
@@ -613,6 +617,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🚀 Canvas Advanced Pattern Validation Demo"); println!("🚀 Canvas Advanced Pattern Validation Demo");
println!("✅ validation feature: ENABLED"); println!("✅ validation feature: ENABLED");
println!("✅ gui feature: ENABLED"); println!("✅ gui feature: ENABLED");
println!("✅ cursor-style feature: ENABLED");
println!("🎯 Advanced pattern filtering: ACTIVE"); println!("🎯 Advanced pattern filtering: ACTIVE");
println!("🧪 Edge cases and complex patterns: READY"); println!("🧪 Edge cases and complex patterns: READY");
println!("💡 Each field showcases different validation capabilities!"); println!("💡 Each field showcases different validation capabilities!");
@@ -625,10 +630,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
let data = AdvancedPatternData::new(); let data = AdvancedPatternData::new();
let editor = AdvancedPatternFormEditor::new(data); let mut editor = AdvancedPatternFormEditor::new(data);
// Initialize with normal mode - library automatically sets block cursor
editor.set_mode(AppMode::ReadOnly);
// Demonstrate that CursorManager is available and working
CursorManager::update_for_mode(AppMode::ReadOnly)?;
let res = run_app(&mut terminal, editor); let res = run_app(&mut terminal, editor);
// Library automatically resets cursor on FormEditor::drop()
// But we can also manually reset if needed
CursorManager::reset()?;
disable_raw_mode()?; disable_raw_mode()?;
execute!( execute!(
terminal.backend_mut(), terminal.backend_mut(),

View File

@@ -18,13 +18,13 @@
//! Each mask's input position count EXACTLY matches its character limit to prevent //! Each mask's input position count EXACTLY matches its character limit to prevent
//! the critical bug where users can type more characters than they can see. //! the critical bug where users can type more characters than they can see.
//! //!
//! Run with: cargo run --example validation_3 --features "gui,validation" //! Run with: cargo run --example validation_3 --features "gui,validation,cursor-style"
// REQUIRE validation and gui features for mask functionality // REQUIRE validation, gui and cursor-style features for mask functionality
#[cfg(not(all(feature = "validation", feature = "gui")))] #[cfg(not(all(feature = "validation", feature = "gui", feature = "cursor-style")))]
compile_error!( compile_error!(
"This example requires the 'validation' and 'gui' features. \ "This example requires the 'validation', 'gui' and 'cursor-style' features. \
Run with: cargo run --example validation_3 --features \"gui,validation\"" Run with: cargo run --example validation_3 --features \"gui,validation,cursor-style\""
); );
use std::io; use std::io;
@@ -50,6 +50,7 @@ use canvas::{
canvas::{ canvas::{
gui::render_canvas_default, gui::render_canvas_default,
modes::AppMode, modes::AppMode,
CursorManager,
}, },
DataProvider, FormEditor, DataProvider, FormEditor,
ValidationConfig, ValidationConfigBuilder, DisplayMask, ValidationConfig, ValidationConfigBuilder, DisplayMask,
@@ -183,18 +184,21 @@ impl<D: DataProvider> MaskDemoFormEditor<D> {
// === MODE TRANSITIONS === // === MODE TRANSITIONS ===
fn enter_edit_mode(&mut self) { fn enter_edit_mode(&mut self) {
// Library will automatically update cursor to bar | in insert mode
self.editor.enter_edit_mode(); self.editor.enter_edit_mode();
self.debug_message = "✏️ INSERT MODE - Type to see mask formatting in real-time".to_string(); self.debug_message = "✏️ INSERT MODE - Cursor: Steady Bar | - Type to see mask formatting in real-time".to_string();
} }
fn enter_append_mode(&mut self) { fn enter_append_mode(&mut self) {
// Library will automatically update cursor to bar | in insert mode
self.editor.enter_append_mode(); self.editor.enter_append_mode();
self.debug_message = "✏️ INSERT (append) - Mask formatting active".to_string(); self.debug_message = "✏️ INSERT (append) - Cursor: Steady Bar | - Mask formatting active".to_string();
} }
fn exit_edit_mode(&mut self) { fn exit_edit_mode(&mut self) {
// Library will automatically update cursor to block █ in normal mode
self.editor.exit_edit_mode(); self.editor.exit_edit_mode();
self.debug_message = "🔒 NORMAL MODE - Press 'r' to see raw data, 'm' for mask info".to_string(); self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █ - Press 'r' to see raw data, 'm' for mask info".to_string();
} }
fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { fn insert_char(&mut self, ch: char) -> anyhow::Result<()> {
@@ -236,7 +240,10 @@ impl<D: DataProvider> MaskDemoFormEditor<D> {
fn current_text(&self) -> &str { self.editor.current_text() } fn current_text(&self) -> &str { self.editor.current_text() }
fn data_provider(&self) -> &D { self.editor.data_provider() } fn data_provider(&self) -> &D { self.editor.data_provider() }
fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() } fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() }
fn set_mode(&mut self, mode: AppMode) { self.editor.set_mode(mode); } fn set_mode(&mut self, mode: AppMode) {
// Library automatically updates cursor for the mode
self.editor.set_mode(mode);
}
fn next_field(&mut self) { fn next_field(&mut self) {
match self.editor.next_field() { match self.editor.next_field() {
@@ -582,9 +589,9 @@ fn render_mask_status(
// Status bar with mask information // Status bar with mask information
let mode_text = match editor.mode() { let mode_text = match editor.mode() {
AppMode::Edit => "INSERT", AppMode::Edit => "INSERT | (bar cursor)",
AppMode::ReadOnly => "NORMAL", AppMode::ReadOnly => "NORMAL █ (block cursor)",
_ => "OTHER", _ => "NORMAL █ (block cursor)",
}; };
let mask_status = editor.get_mask_status(); let mask_status = editor.get_mask_status();
@@ -634,7 +641,8 @@ fn render_mask_status(
// Enhanced help text // Enhanced help text
let help_text = match editor.mode() { let help_text = match editor.mode() {
AppMode::ReadOnly => { AppMode::ReadOnly => {
"🎭 MASK DEMO: See how visual formatting keeps business logic clean!\n\ "🎯 CURSOR-STYLE: Normal █ | Insert |\n\
🎭 MASK DEMO: Visual formatting keeps business logic clean!\n\
\n\ \n\
📱 Try different fields to see various mask patterns:\n\ 📱 Try different fields to see various mask patterns:\n\
• Dynamic vs Template modes • Custom separators • Different input chars\n\ • Dynamic vs Template modes • Custom separators • Different input chars\n\
@@ -644,7 +652,8 @@ fn render_mask_status(
?=detailed info, Ctrl+C=quit" ?=detailed info, Ctrl+C=quit"
} }
AppMode::Edit => { AppMode::Edit => {
"✏️ INSERT MODE - Type to see real-time mask formatting!\n\ "🎯 INSERT MODE - Cursor: | (bar)\n\
✏️ Type to see real-time mask formatting!\n\
\n\ \n\
🔥 Key Features in Action:\n\ 🔥 Key Features in Action:\n\
• Separators auto-appear as you type • Cursor skips over separators\n\ • Separators auto-appear as you type • Cursor skips over separators\n\
@@ -670,6 +679,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("✅ validation feature: ENABLED"); println!("✅ validation feature: ENABLED");
println!("✅ gui feature: ENABLED"); println!("✅ gui feature: ENABLED");
println!("🎭 Display masks: ACTIVE"); println!("🎭 Display masks: ACTIVE");
println!("✅ cursor-style feature: ENABLED");
println!("🔥 Key Benefits Demonstrated:"); println!("🔥 Key Benefits Demonstrated:");
println!(" • Clean separation: Visual formatting ≠ Business logic"); println!(" • Clean separation: Visual formatting ≠ Business logic");
println!(" • User-friendly: Pretty displays with automatic cursor handling"); println!(" • User-friendly: Pretty displays with automatic cursor handling");
@@ -690,7 +700,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
let data = MaskDemoData::new(); let data = MaskDemoData::new();
let editor = MaskDemoFormEditor::new(data); let mut editor = MaskDemoFormEditor::new(data);
// Initialize with normal mode - library automatically sets block cursor
editor.set_mode(AppMode::ReadOnly);
// Demonstrate that CursorManager is available and working
CursorManager::update_for_mode(AppMode::ReadOnly)?;
let res = run_app(&mut terminal, editor); let res = run_app(&mut terminal, editor);
@@ -702,6 +718,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
)?; )?;
terminal.show_cursor()?; terminal.show_cursor()?;
// Library automatically resets cursor on FormEditor::drop()
// But we can also manually reset if needed
CursorManager::reset()?;
if let Err(err) = res { if let Err(err) = res {
println!("{:?}", err); println!("{:?}", err);
} }

View File

@@ -13,10 +13,10 @@
#![allow(clippy::needless_return)] #![allow(clippy::needless_return)]
#[cfg(not(all(feature = "validation", feature = "gui")))] #[cfg(not(all(feature = "validation", feature = "gui", feature = "cursor-style")))]
compile_error!( compile_error!(
"This example requires the 'validation' and 'gui' features. \ "This example requires the 'validation', 'gui' and 'cursor-style' features. \
Run with: cargo run --example validation_4 --features \"gui,validation\"" Run with: cargo run --example validation_4 --features \"gui,validation,cursor-style\""
); );
use std::io; use std::io;
@@ -39,7 +39,7 @@ use ratatui::{
}; };
use canvas::{ use canvas::{
canvas::{gui::render_canvas_default, modes::AppMode}, canvas::{gui::render_canvas_default, modes::AppMode, CursorManager},
DataProvider, FormEditor, DataProvider, FormEditor,
ValidationConfig, ValidationConfigBuilder, ValidationConfig, ValidationConfigBuilder,
CustomFormatter, FormattingResult, CustomFormatter, FormattingResult,
@@ -403,21 +403,23 @@ impl<D: DataProvider> EnhancedDemoEditor<D> {
// Delegate methods with enhanced feedback // Delegate methods with enhanced feedback
fn enter_edit_mode(&mut self) { fn enter_edit_mode(&mut self) {
// Library will automatically update cursor to bar | in insert mode
self.editor.enter_edit_mode(); self.editor.enter_edit_mode();
let field_type = self.current_field_type(); let field_type = self.current_field_type();
let rules = self.get_input_rules(); let rules = self.get_input_rules();
self.debug_message = format!("✏️ EDITING {} - {}", field_type, rules); self.debug_message = format!("✏️ INSERT MODE - Cursor: Steady Bar | - {} - {}", field_type, rules);
} }
fn exit_edit_mode(&mut self) { fn exit_edit_mode(&mut self) {
// Library will automatically update cursor to block █ in normal mode
self.editor.exit_edit_mode(); self.editor.exit_edit_mode();
let (raw, display, _, warning) = self.get_current_field_analysis(); let (raw, display, _, warning) = self.get_current_field_analysis();
if let Some(warn) = warning { if let Some(warn) = warning {
self.debug_message = format!("🔒 NORMAL - {} | ⚠️ {}", self.current_field_type(), warn); self.debug_message = format!("🔒 NORMAL - Cursor: Steady Block █ - {} | ⚠️ {}", self.current_field_type(), warn);
} else if raw != display { } else if raw != display {
self.debug_message = format!("🔒 NORMAL - {} formatted successfully", self.current_field_type()); self.debug_message = format!("🔒 NORMAL - Cursor: Steady Block █ - {} formatted successfully", self.current_field_type());
} else { } else {
self.debug_message = "🔒 NORMAL MODE".to_string(); self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █".to_string();
} }
} }
@@ -588,9 +590,9 @@ fn render_enhanced_status(
// Status bar // Status bar
let mode_text = match editor.mode() { let mode_text = match editor.mode() {
AppMode::Edit => "INSERT", AppMode::Edit => "INSERT | (bar cursor)",
AppMode::ReadOnly => "NORMAL", AppMode::ReadOnly => "NORMAL █ (block cursor)",
_ => "OTHER", _ => "NORMAL █ (block cursor)",
}; };
let formatter_count = (0..editor.data_provider().field_count()) let formatter_count = (0..editor.data_provider().field_count())
@@ -660,7 +662,8 @@ fn render_enhanced_status(
// Enhanced help // Enhanced help
let help_text = match editor.mode() { let help_text = match editor.mode() {
AppMode::ReadOnly => { AppMode::ReadOnly => {
"🧩 ENHANCED CUSTOM FORMATTER DEMO\n\ "🎯 CURSOR-STYLE: Normal █ | Insert |\n\
🧩 ENHANCED CUSTOM FORMATTER DEMO\n\
\n\ \n\
Try these formatters: Try these formatters:
• PSC: 01001 → 010 01 | Phone: 1234567890 → (123) 456-7890 | Card: 1234567890123456 → 1234 5678 9012 3456 • PSC: 01001 → 010 01 | Phone: 1234567890 → (123) 456-7890 | Card: 1234567890123456 → 1234 5678 9012 3456
@@ -671,7 +674,8 @@ fn render_enhanced_status(
Ctrl+C/F10=quit" Ctrl+C/F10=quit"
} }
AppMode::Edit => { AppMode::Edit => {
"✏️ INSERT MODE - Real-time formatting as you type!\n\ "🎯 INSERT MODE - Cursor: | (bar)\n\
✏️ Real-time formatting as you type!\n\
\n\ \n\
Current field rules: {}\n\ Current field rules: {}\n\
• Raw input is authoritative (what gets stored)\n\ • Raw input is authoritative (what gets stored)\n\
@@ -701,6 +705,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🧩 Enhanced Canvas Custom Formatter Demo (Feature 4)"); println!("🧩 Enhanced Canvas Custom Formatter Demo (Feature 4)");
println!("✅ validation feature: ENABLED"); println!("✅ validation feature: ENABLED");
println!("✅ gui feature: ENABLED"); println!("✅ gui feature: ENABLED");
println!("✅ cursor-style feature: ENABLED");
println!("🧩 Enhanced features:"); println!("🧩 Enhanced features:");
println!(" • 5 different custom formatters with edge cases"); println!(" • 5 different custom formatters with edge cases");
println!(" • Real-time format preview and validation"); println!(" • Real-time format preview and validation");
@@ -716,7 +721,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
let data = MultiFormatterDemoData::new(); let data = MultiFormatterDemoData::new();
let editor = EnhancedDemoEditor::new(data); let mut editor = EnhancedDemoEditor::new(data);
// Initialize with normal mode - library automatically sets block cursor
editor.editor.set_mode(AppMode::ReadOnly);
// Demonstrate that CursorManager is available and working
CursorManager::update_for_mode(AppMode::ReadOnly)?;
let res = run_app(&mut terminal, editor); let res = run_app(&mut terminal, editor);
@@ -724,6 +735,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?; execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
terminal.show_cursor()?; terminal.show_cursor()?;
// Library automatically resets cursor on FormEditor::drop()
// But we can also manually reset if needed
CursorManager::reset()?;
if let Err(err) = res { if let Err(err) = res {
println!("{:?}", err); println!("{:?}", err);
} }

View File

@@ -26,14 +26,14 @@
//! - F1: toggle external validation globally //! - F1: toggle external validation globally
//! - F10/Ctrl+C: quit //! - F10/Ctrl+C: quit
//! //!
//! Run: cargo run --example validation_5 --features "gui,validation" //! Run: cargo run --example validation_5 --features "gui,validation,cursor-style"
#![allow(clippy::needless_return)] #![allow(clippy::needless_return)]
#[cfg(not(all(feature = "validation", feature = "gui")))] #[cfg(not(all(feature = "validation", feature = "gui", feature = "cursor-style")))]
compile_error!( compile_error!(
"This example requires the 'validation' and 'gui' features. \ "This example requires the 'validation', 'gui' and 'cursor-style' features. \
Run with: cargo run --example validation_5 --features \"gui,validation\"" Run with: cargo run --example validation_5 --features \"gui,validation,cursor-style\""
); );
use std::io; use std::io;
@@ -59,7 +59,7 @@ use ratatui::{
}; };
use canvas::{ use canvas::{
canvas::{gui::render_canvas_default, modes::AppMode}, canvas::{gui::render_canvas_default, modes::AppMode, CursorManager},
DataProvider, FormEditor, DataProvider, FormEditor,
ValidationConfigBuilder, CustomFormatter, FormattingResult, ValidationConfigBuilder, CustomFormatter, FormattingResult,
validation::ExternalValidationState, validation::ExternalValidationState,
@@ -762,7 +762,7 @@ impl<D: DataProvider> ValidationDemoEditor<D> {
fn enter_edit_mode(&mut self) { fn enter_edit_mode(&mut self) {
self.editor.enter_edit_mode(); self.editor.enter_edit_mode();
let rules = self.field_validation_rules(); let rules = self.field_validation_rules();
self.debug_message = format!("✏️ EDITING {} - {}", self.field_type(), rules); self.debug_message = format!("✏️ INSERT MODE - Cursor: Steady Bar | - {} - {}", self.field_type(), rules);
} }
fn exit_edit_mode(&mut self) { fn exit_edit_mode(&mut self) {
@@ -774,7 +774,7 @@ impl<D: DataProvider> ValidationDemoEditor<D> {
self.validate_field(current_field); self.validate_field(current_field);
} }
self.debug_message = format!("🔒 NORMAL - {}", self.field_type()); self.debug_message = format!("🔒 NORMAL - Cursor: Steady Block █ - {}", self.field_type());
} }
fn next_field(&mut self) { fn next_field(&mut self) {
@@ -848,21 +848,21 @@ fn run_app<B: Backend>(
(_, KeyCode::Tab, _) => editor.next_field(), (_, KeyCode::Tab, _) => editor.next_field(),
(_, KeyCode::BackTab, _) => editor.prev_field(), (_, KeyCode::BackTab, _) => editor.prev_field(),
// Validation commands // Validation commands (ONLY in ReadOnly mode)
(_, KeyCode::Char('v'), _) => { (AppMode::ReadOnly, KeyCode::Char('v'), _) => {
let field = editor.current_field(); let field = editor.current_field();
editor.validate_field(field); editor.validate_field(field);
}, },
(_, KeyCode::Char('V'), _) => editor.validate_all_fields(), (AppMode::ReadOnly, KeyCode::Char('V'), _) => editor.validate_all_fields(),
(_, KeyCode::Char('c'), _) => { (AppMode::ReadOnly, KeyCode::Char('c'), _) => {
let field = editor.current_field(); let field = editor.current_field();
editor.clear_validation_state(Some(field)); editor.clear_validation_state(Some(field));
}, },
(_, KeyCode::Char('C'), _) => editor.clear_validation_state(None), (AppMode::ReadOnly, KeyCode::Char('C'), _) => editor.clear_validation_state(None),
// UI toggles // UI toggles (ONLY in ReadOnly mode for alpha keys to avoid blocking text input)
(_, KeyCode::Char('r'), _) => editor.toggle_history_view(), (AppMode::ReadOnly, KeyCode::Char('r'), _) => editor.toggle_history_view(),
(_, KeyCode::Char('e'), _) => editor.cycle_examples(), (AppMode::ReadOnly, KeyCode::Char('e'), _) => editor.cycle_examples(),
(_, KeyCode::F(1), _) => editor.toggle_validation(), (_, KeyCode::F(1), _) => editor.toggle_validation(),
// Editing // Editing
@@ -915,9 +915,9 @@ fn render_validation_panel(
// Status bar // Status bar
let mode_text = match editor.mode() { let mode_text = match editor.mode() {
AppMode::Edit => "INSERT", AppMode::Edit => "INSERT | (bar cursor)",
AppMode::ReadOnly => "NORMAL", AppMode::ReadOnly => "NORMAL █ (block cursor)",
_ => "OTHER", _ => "NORMAL █ (block cursor)",
}; };
let summary = editor.get_validation_summary(); let summary = editor.get_validation_summary();
@@ -1019,7 +1019,8 @@ fn render_validation_panel(
} else { } else {
let help_text = match editor.mode() { let help_text = match editor.mode() {
AppMode::ReadOnly => { AppMode::ReadOnly => {
"🧪 EXTERNAL VALIDATION DEMO - Multiple validation types with async simulation\n\ "🎯 CURSOR-STYLE: Normal █ | Insert |\n\
🧪 EXTERNAL VALIDATION DEMO - Multiple validation types with async simulation\n\
\n\ \n\
Commands: v=validate current, V=validate all, c=clear current, C=clear all\n\ Commands: v=validate current, V=validate all, c=clear current, C=clear all\n\
e=cycle examples, r=toggle history, h=field help, F1=toggle validation\n\ e=cycle examples, r=toggle history, h=field help, F1=toggle validation\n\
@@ -1028,7 +1029,8 @@ fn render_validation_panel(
Try different values to see validation in action!" Try different values to see validation in action!"
} }
AppMode::Edit => { AppMode::Edit => {
"✏️ EDITING MODE - Type to see validation on field blur\n\ "🎯 INSERT MODE - Cursor: | (bar)\n\
✏️ Type to see validation on field blur\n\
\n\ \n\
Current field validation will trigger when you:\n\ Current field validation will trigger when you:\n\
• Press Esc (exit edit mode)\n\ • Press Esc (exit edit mode)\n\
@@ -1052,6 +1054,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🧪 Enhanced External Validation Demo (Feature 5)"); println!("🧪 Enhanced External Validation Demo (Feature 5)");
println!("✅ validation feature: ENABLED"); println!("✅ validation feature: ENABLED");
println!("✅ gui feature: ENABLED"); println!("✅ gui feature: ENABLED");
println!("✅ cursor-style feature: ENABLED");
println!("🧪 Enhanced features:"); println!("🧪 Enhanced features:");
println!(" • 5 different external validation types with realistic scenarios"); println!(" • 5 different external validation types with realistic scenarios");
println!(" • Validation caching and performance metrics"); println!(" • Validation caching and performance metrics");
@@ -1067,7 +1070,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
let data = ValidationDemoData::new(); let data = ValidationDemoData::new();
let editor = ValidationDemoEditor::new(data); let mut editor = ValidationDemoEditor::new(data);
// Initialize with normal mode - library automatically sets block cursor
editor.editor.set_mode(AppMode::ReadOnly);
// Demonstrate that CursorManager is available and working
CursorManager::update_for_mode(AppMode::ReadOnly)?;
let res = run_app(&mut terminal, editor); let res = run_app(&mut terminal, editor);
@@ -1075,6 +1084,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?; execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
terminal.show_cursor()?; terminal.show_cursor()?;
// Library automatically resets cursor on FormEditor::drop()
// But we can also manually reset if needed
CursorManager::reset()?;
if let Err(err) = res { if let Err(err) = res {
println!("{:?}", err); println!("{:?}", err);
} }

View File

@@ -1,12 +0,0 @@
// src/autocomplete/mod.rs
pub mod state;
#[cfg(feature = "gui")]
pub mod gui;
// Re-export the main autocomplete types
pub use state::{AutocompleteProvider, SuggestionItem};
// Re-export GUI functions if available
#[cfg(feature = "gui")]
pub use gui::render_autocomplete_dropdown;

View File

@@ -1,5 +0,0 @@
// src/autocomplete/state.rs
//! Autocomplete provider types
// Re-export the main types from data_provider
pub use crate::data_provider::{AutocompleteProvider, SuggestionItem};

View File

@@ -30,12 +30,12 @@ pub enum CanvasAction {
DeleteBackward, DeleteBackward,
DeleteForward, DeleteForward,
// Autocomplete actions // Suggestions actions
TriggerAutocomplete, TriggerSuggestions,
SuggestionUp, SuggestionUp,
SuggestionDown, SuggestionDown,
SelectSuggestion, SelectSuggestion,
ExitSuggestions, ExitSuggestions,
// Custom actions // Custom actions
Custom(String), Custom(String),
@@ -101,7 +101,7 @@ impl CanvasAction {
Self::InsertChar(_c) => "insert character", Self::InsertChar(_c) => "insert character",
Self::DeleteBackward => "delete backward", Self::DeleteBackward => "delete backward",
Self::DeleteForward => "delete forward", Self::DeleteForward => "delete forward",
Self::TriggerAutocomplete => "trigger autocomplete", Self::TriggerSuggestions => "trigger suggestions",
Self::SuggestionUp => "suggestion up", Self::SuggestionUp => "suggestion up",
Self::SuggestionDown => "suggestion down", Self::SuggestionDown => "suggestion down",
Self::SelectSuggestion => "select suggestion", Self::SelectSuggestion => "select suggestion",
@@ -139,10 +139,10 @@ impl CanvasAction {
] ]
} }
/// Get all autocomplete-related actions /// Get all suggestions-related actions
pub fn autocomplete_actions() -> Vec<CanvasAction> { pub fn suggestions_actions() -> Vec<CanvasAction> {
vec![ vec![
Self::TriggerAutocomplete, Self::TriggerSuggestions,
Self::SuggestionUp, Self::SuggestionUp,
Self::SuggestionDown, Self::SuggestionDown,
Self::SelectSuggestion, Self::SelectSuggestion,

View File

@@ -19,7 +19,7 @@ use crate::editor::FormEditor;
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
use std::cmp::{max, min}; use std::cmp::{max, min};
/// Render ONLY the canvas form fields - no autocomplete /// Render ONLY the canvas form fields - no suggestions rendering here
/// Updated to work with FormEditor instead of CanvasState trait /// Updated to work with FormEditor instead of CanvasState trait
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
pub fn render_canvas<T: CanvasTheme, D: DataProvider>( pub fn render_canvas<T: CanvasTheme, D: DataProvider>(
@@ -67,6 +67,15 @@ pub fn render_canvas_with_highlight<T: CanvasTheme, D: DataProvider>(
let current_field_idx = ui_state.current_field(); let current_field_idx = ui_state.current_field();
let is_edit_mode = matches!(ui_state.mode(), crate::canvas::modes::AppMode::Edit); let is_edit_mode = matches!(ui_state.mode(), crate::canvas::modes::AppMode::Edit);
// Precompute completion for active field
let active_completion = if ui_state.is_suggestions_active()
&& ui_state.suggestions.active_field == Some(current_field_idx)
{
ui_state.suggestions.completion_text.clone()
} else {
None
};
render_canvas_fields( render_canvas_fields(
f, f,
area, area,
@@ -111,6 +120,14 @@ pub fn render_canvas_with_highlight<T: CanvasTheme, D: DataProvider>(
false false
} }
}, },
// NEW: provide completion for the active field
|i| {
if i == current_field_idx {
active_completion.clone()
} else {
None
}
},
) )
} }
@@ -128,7 +145,7 @@ fn convert_selection_to_highlight(selection: &crate::canvas::state::SelectionSta
/// Core canvas field rendering /// Core canvas field rendering
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
fn render_canvas_fields<T: CanvasTheme, F1, F2>( fn render_canvas_fields<T: CanvasTheme, F1, F2, F3>(
f: &mut Frame, f: &mut Frame,
area: Rect, area: Rect,
fields: &[&str], fields: &[&str],
@@ -141,10 +158,12 @@ fn render_canvas_fields<T: CanvasTheme, F1, F2>(
has_unsaved_changes: bool, has_unsaved_changes: bool,
get_display_value: F1, get_display_value: F1,
has_display_override: F2, has_display_override: F2,
get_completion: F3,
) -> Option<Rect> ) -> Option<Rect>
where where
F1: Fn(usize) -> String, F1: Fn(usize) -> String,
F2: Fn(usize) -> bool, F2: Fn(usize) -> bool,
F3: Fn(usize) -> Option<String>,
{ {
// Create layout // Create layout
let columns = Layout::default() let columns = Layout::default()
@@ -198,6 +217,7 @@ where
current_cursor_pos, current_cursor_pos,
get_display_value, get_display_value,
has_display_override, has_display_override,
get_completion,
) )
} }
@@ -229,7 +249,7 @@ fn render_field_labels<T: CanvasTheme>(
/// Render field values with highlighting /// Render field values with highlighting
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
fn render_field_values<T: CanvasTheme, F1, F2>( fn render_field_values<T: CanvasTheme, F1, F2, F3>(
f: &mut Frame, f: &mut Frame,
input_rows: Vec<Rect>, input_rows: Vec<Rect>,
inputs: &[String], inputs: &[String],
@@ -239,35 +259,54 @@ fn render_field_values<T: CanvasTheme, F1, F2>(
current_cursor_pos: usize, current_cursor_pos: usize,
get_display_value: F1, get_display_value: F1,
has_display_override: F2, has_display_override: F2,
get_completion: F3,
) -> Option<Rect> ) -> Option<Rect>
where where
F1: Fn(usize) -> String, F1: Fn(usize) -> String,
F2: Fn(usize) -> bool, F2: Fn(usize) -> bool,
F3: Fn(usize) -> Option<String>,
{ {
let mut active_field_input_rect = None; let mut active_field_input_rect = None;
for (i, _input) in inputs.iter().enumerate() { for (i, _input) in inputs.iter().enumerate() {
let is_active = i == *current_field_idx; let is_active = i == *current_field_idx;
let text = get_display_value(i); let typed_text = get_display_value(i);
// Apply highlighting let line = if is_active {
let line = apply_highlighting( // Compose typed + gray completion for the active field
&text, let normal_style = Style::default().fg(theme.fg());
i, let gray_style = Style::default().fg(theme.suggestion_gray());
current_field_idx,
current_cursor_pos, let mut spans: Vec<Span> = Vec::new();
highlight_state, spans.push(Span::styled(typed_text.clone(), normal_style));
theme,
is_active, if let Some(completion) = get_completion(i) {
); if !completion.is_empty() {
spans.push(Span::styled(completion, gray_style));
}
}
Line::from(spans)
} else {
// Non-active fields: keep existing highlighting logic
apply_highlighting(
&typed_text,
i,
current_field_idx,
current_cursor_pos,
highlight_state,
theme,
is_active,
)
};
let input_display = Paragraph::new(line).alignment(Alignment::Left); let input_display = Paragraph::new(line).alignment(Alignment::Left);
f.render_widget(input_display, input_rows[i]); f.render_widget(input_display, input_rows[i]);
// Set cursor for active field // Set cursor for active field at end of typed text (not after completion)
if is_active { if is_active {
active_field_input_rect = Some(input_rows[i]); active_field_input_rect = Some(input_rows[i]);
set_cursor_position(f, input_rows[i], &text, current_cursor_pos, has_display_override(i)); set_cursor_position(f, input_rows[i], &typed_text, current_cursor_pos, has_display_override(i));
} }
} }

View File

@@ -14,8 +14,8 @@ pub struct EditorState {
// Mode state // Mode state
pub(crate) current_mode: AppMode, pub(crate) current_mode: AppMode,
// Autocomplete state // Suggestions dropdown state
pub(crate) autocomplete: AutocompleteUIState, pub(crate) suggestions: SuggestionsUIState,
// Selection state (for vim visual mode) // Selection state (for vim visual mode)
pub(crate) selection: SelectionState, pub(crate) selection: SelectionState,
@@ -23,14 +23,19 @@ pub struct EditorState {
// Validation state (only available with validation feature) // Validation state (only available with validation feature)
#[cfg(feature = "validation")] #[cfg(feature = "validation")]
pub(crate) validation: crate::validation::ValidationState, pub(crate) validation: crate::validation::ValidationState,
/// Computed fields state (only when computed feature is enabled)
#[cfg(feature = "computed")]
pub(crate) computed: Option<crate::computed::ComputedState>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AutocompleteUIState { pub struct SuggestionsUIState {
pub(crate) is_active: bool, pub(crate) is_active: bool,
pub(crate) is_loading: bool, pub(crate) is_loading: bool,
pub(crate) selected_index: Option<usize>, pub(crate) selected_index: Option<usize>,
pub(crate) active_field: Option<usize>, pub(crate) active_field: Option<usize>,
pub(crate) completion_text: Option<String>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -47,15 +52,18 @@ impl EditorState {
cursor_pos: 0, cursor_pos: 0,
ideal_cursor_column: 0, ideal_cursor_column: 0,
current_mode: AppMode::Edit, current_mode: AppMode::Edit,
autocomplete: AutocompleteUIState { suggestions: SuggestionsUIState {
is_active: false, is_active: false,
is_loading: false, is_loading: false,
selected_index: None, selected_index: None,
active_field: None, active_field: None,
completion_text: None,
}, },
selection: SelectionState::None, selection: SelectionState::None,
#[cfg(feature = "validation")] #[cfg(feature = "validation")]
validation: crate::validation::ValidationState::new(), validation: crate::validation::ValidationState::new(),
#[cfg(feature = "computed")]
computed: None,
} }
} }
@@ -68,6 +76,15 @@ impl EditorState {
self.current_field self.current_field
} }
/// Check if field is computed
#[cfg(feature = "computed")]
pub fn is_computed_field(&self, field_index: usize) -> bool {
self.computed
.as_ref()
.map(|state| state.is_computed_field(field_index))
.unwrap_or(false)
}
/// Get current cursor position (for user's business logic) /// Get current cursor position (for user's business logic)
pub fn cursor_position(&self) -> usize { pub fn cursor_position(&self) -> usize {
self.cursor_pos self.cursor_pos
@@ -83,14 +100,14 @@ impl EditorState {
self.current_mode self.current_mode
} }
/// Check if autocomplete is active (for user's business logic) /// Check if suggestions dropdown is active (for user's business logic)
pub fn is_autocomplete_active(&self) -> bool { pub fn is_suggestions_active(&self) -> bool {
self.autocomplete.is_active self.suggestions.is_active
} }
/// Check if autocomplete is loading (for user's business logic) /// Check if suggestions dropdown is loading (for user's business logic)
pub fn is_autocomplete_loading(&self) -> bool { pub fn is_suggestions_loading(&self) -> bool {
self.autocomplete.is_loading self.suggestions.is_loading
} }
/// Get selection state (for user's business logic) /// Get selection state (for user's business logic)
@@ -117,7 +134,12 @@ impl EditorState {
} }
} }
pub(crate) fn set_cursor(&mut self, position: usize, max_position: usize, for_edit_mode: bool) { pub(crate) fn set_cursor(
&mut self,
position: usize,
max_position: usize,
for_edit_mode: bool,
) {
if for_edit_mode { if for_edit_mode {
// Edit mode: can go past end for insertion // Edit mode: can go past end for insertion
self.cursor_pos = position.min(max_position); self.cursor_pos = position.min(max_position);
@@ -128,18 +150,40 @@ impl EditorState {
self.ideal_cursor_column = self.cursor_pos; self.ideal_cursor_column = self.cursor_pos;
} }
pub(crate) fn activate_autocomplete(&mut self, field_index: usize) { /// Legacy internal activation (still used internally if needed)
self.autocomplete.is_active = true; pub(crate) fn activate_suggestions(&mut self, field_index: usize) {
self.autocomplete.is_loading = true; self.suggestions.is_active = true;
self.autocomplete.active_field = Some(field_index); self.suggestions.is_loading = true;
self.autocomplete.selected_index = None; self.suggestions.active_field = Some(field_index);
self.suggestions.selected_index = None;
self.suggestions.completion_text = None;
} }
pub(crate) fn deactivate_autocomplete(&mut self) { /// Legacy internal deactivation
self.autocomplete.is_active = false; pub(crate) fn deactivate_suggestions(&mut self) {
self.autocomplete.is_loading = false; self.suggestions.is_active = false;
self.autocomplete.active_field = None; self.suggestions.is_loading = false;
self.autocomplete.selected_index = None; self.suggestions.active_field = None;
self.suggestions.selected_index = None;
self.suggestions.completion_text = None;
}
/// Explicitly open suggestions — should only be called on Tab
pub(crate) fn open_suggestions(&mut self, field_index: usize) {
self.suggestions.is_active = true;
self.suggestions.is_loading = true;
self.suggestions.active_field = Some(field_index);
self.suggestions.selected_index = None;
self.suggestions.completion_text = None;
}
/// Explicitly close suggestions — should be called on Esc or field change
pub(crate) fn close_suggestions(&mut self) {
self.suggestions.is_active = false;
self.suggestions.is_loading = false;
self.suggestions.active_field = None;
self.suggestions.selected_index = None;
self.suggestions.completion_text = None;
} }
} }

View File

@@ -14,6 +14,7 @@ pub trait CanvasTheme {
fn highlight(&self) -> Color; fn highlight(&self) -> Color;
fn highlight_bg(&self) -> Color; fn highlight_bg(&self) -> Color;
fn warning(&self) -> Color; fn warning(&self) -> Color;
fn suggestion_gray(&self) -> Color;
} }
@@ -47,4 +48,7 @@ impl CanvasTheme for DefaultCanvasTheme {
fn warning(&self) -> Color { fn warning(&self) -> Color {
Color::Red Color::Red
} }
fn suggestion_gray(&self) -> Color {
Color::DarkGray
}
} }

View File

@@ -0,0 +1,5 @@
pub mod provider;
pub mod state;
pub use provider::{ComputedContext, ComputedProvider};
pub use state::ComputedState;

View File

@@ -0,0 +1,31 @@
// ================================================================================================
// COMPUTED FIELDS - Provider and Context
// ================================================================================================
/// Context information provided to computed field calculations
#[derive(Debug, Clone)]
pub struct ComputedContext<'a> {
/// All field values in the form (index -> value)
pub field_values: &'a [&'a str],
/// The field index being computed
pub target_field: usize,
/// Current field that user is editing (if any)
pub current_field: Option<usize>,
}
/// User implements this to provide computed field logic
pub trait ComputedProvider {
/// Compute value for a field based on other field values.
/// Called automatically when any field changes.
fn compute_field(&mut self, context: ComputedContext) -> String;
/// Check if this provider handles the given field.
fn handles_field(&self, field_index: usize) -> bool;
/// Get list of field dependencies for optimization.
/// If field A depends on fields [1, 3], only recompute A when fields 1 or 3 change.
/// Default: depend on all fields (always recompute) with a reasonable upper bound.
fn field_dependencies(&self, _field_index: usize) -> Vec<usize> {
(0..100).collect()
}
}

View File

@@ -0,0 +1,88 @@
/* file: canvas/src/computed/state.rs */
/*
Add computed state module file implementing caching and dependencies
*/
// ================================================================================================
// COMPUTED FIELDS - State: caching and dependencies
// ================================================================================================
use std::collections::{HashMap, HashSet};
/// Internal state for computed field management
#[derive(Debug, Clone)]
pub struct ComputedState {
/// Cached computed values (field_index -> computed_value)
computed_values: HashMap<usize, String>,
/// Field dependency graph (field_index -> depends_on_fields)
dependencies: HashMap<usize, Vec<usize>>,
/// Track which fields are computed (display-only)
computed_fields: HashSet<usize>,
}
impl ComputedState {
/// Create a new, empty computed state
pub fn new() -> Self {
Self {
computed_values: HashMap::new(),
dependencies: HashMap::new(),
computed_fields: HashSet::new(),
}
}
/// Register a field as computed with its dependencies
///
/// - `field_index`: the field that is computed (display-only)
/// - `dependencies`: indices of fields this computed field depends on
pub fn register_computed_field(&mut self, field_index: usize, mut dependencies: Vec<usize>) {
// Deduplicate dependencies to keep graph lean
dependencies.sort_unstable();
dependencies.dedup();
self.computed_fields.insert(field_index);
self.dependencies.insert(field_index, dependencies);
}
/// Check if a field is computed (read-only, skip editing/navigation)
pub fn is_computed_field(&self, field_index: usize) -> bool {
self.computed_fields.contains(&field_index)
}
/// Get cached computed value for a field, if available
pub fn get_computed_value(&self, field_index: usize) -> Option<&String> {
self.computed_values.get(&field_index)
}
/// Update cached computed value for a field
pub fn set_computed_value(&mut self, field_index: usize, value: String) {
self.computed_values.insert(field_index, value);
}
/// Get fields that should be recomputed when `changed_field` changed
///
/// This scans the dependency graph and returns all computed fields
/// that list `changed_field` as a dependency.
pub fn fields_to_recompute(&self, changed_field: usize) -> Vec<usize> {
self.dependencies
.iter()
.filter_map(|(field, deps)| {
if deps.contains(&changed_field) {
Some(*field)
} else {
None
}
})
.collect()
}
/// Iterator over all computed field indices
pub fn computed_fields(&self) -> impl Iterator<Item = usize> + '_ {
self.computed_fields.iter().copied()
}
}
impl Default for ComputedState {
fn default() -> Self {
Self::new()
}
}

View File

@@ -18,8 +18,8 @@ pub trait DataProvider {
/// 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 suggestions (optional)
fn supports_autocomplete(&self, _field_index: usize) -> bool { fn supports_suggestions(&self, _field_index: usize) -> bool {
false false
} }
@@ -34,12 +34,26 @@ pub trait DataProvider {
fn validation_config(&self, _field_index: usize) -> Option<crate::validation::ValidationConfig> { fn validation_config(&self, _field_index: usize) -> Option<crate::validation::ValidationConfig> {
None None
} }
/// Check if field is computed (display-only, skip in navigation)
/// Default: not computed
#[cfg(feature = "computed")]
fn is_computed_field(&self, _field_index: usize) -> bool {
false
}
/// Get computed field value if this is a computed field.
/// Returns None for regular fields. Default: not computed.
#[cfg(feature = "computed")]
fn computed_field_value(&self, _field_index: usize) -> Option<String> {
None
}
} }
/// Optional: User implements this for autocomplete data /// Optional: User implements this for suggestions data
#[async_trait] #[async_trait]
pub trait AutocompleteProvider { pub trait SuggestionsProvider {
/// Fetch autocomplete suggestions (user's business logic) /// Fetch 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>>; -> Result<Vec<SuggestionItem>>;
} }

View File

@@ -7,7 +7,7 @@ use crate::canvas::CursorManager;
use anyhow::Result; use anyhow::Result;
use crate::canvas::state::EditorState; use crate::canvas::state::EditorState;
use crate::data_provider::{DataProvider, AutocompleteProvider, SuggestionItem}; use crate::data_provider::{DataProvider, SuggestionsProvider, SuggestionItem};
use crate::canvas::modes::AppMode; use crate::canvas::modes::AppMode;
use crate::canvas::state::SelectionState; use crate::canvas::state::SelectionState;
@@ -40,6 +40,24 @@ impl<D: DataProvider> FormEditor<D> {
editor editor
} }
/// Compute inline completion for current selection and current text.
fn compute_current_completion(&self) -> Option<String> {
let typed = self.current_text();
let idx = self.ui_state.suggestions.selected_index?;
let sugg = self.suggestions.get(idx)?;
if let Some(rest) = sugg.value_to_store.strip_prefix(typed) {
if !rest.is_empty() {
return Some(rest.to_string());
}
}
None
}
/// Update UI state's completion text from current selection
pub fn update_inline_completion(&mut self) {
self.ui_state.suggestions.completion_text = self.compute_current_completion();
}
/// Initialize validation configurations from data provider /// Initialize validation configurations from data provider
#[cfg(feature = "validation")] #[cfg(feature = "validation")]
fn initialize_validation(&mut self) { fn initialize_validation(&mut self) {
@@ -70,9 +88,9 @@ impl<D: DataProvider> FormEditor<D> {
self.ui_state.mode() self.ui_state.mode()
} }
/// Check if autocomplete is active (for user's logic) /// Check if suggestions dropdown is active (for user's logic)
pub fn is_autocomplete_active(&self) -> bool { pub fn is_suggestions_active(&self) -> bool {
self.ui_state.is_autocomplete_active() self.ui_state.is_suggestions_active()
} }
/// Get current field text (convenience method) /// Get current field text (convenience method)
@@ -134,6 +152,22 @@ impl<D: DataProvider> FormEditor<D> {
&self.ui_state &self.ui_state
} }
/// Mutable access to UI state for internal crate use only.
pub(crate) fn ui_state_mut(&mut self) -> &mut EditorState {
&mut self.ui_state
}
/// Open the suggestions UI for `field_index` (UI-only; does not fetch).
pub fn open_suggestions(&mut self, field_index: usize) {
self.ui_state.open_suggestions(field_index);
}
/// Close suggestions UI and clear the current suggestion results.
pub fn close_suggestions(&mut self) {
self.ui_state.close_suggestions();
self.suggestions.clear();
}
/// Set external validation state for a field (Feature 5) /// Set external validation state for a field (Feature 5)
#[cfg(feature = "validation")] #[cfg(feature = "validation")]
pub fn set_external_validation( pub fn set_external_validation(
@@ -580,50 +614,56 @@ impl<D: DataProvider> FormEditor<D> {
} }
// =================================================================== // ===================================================================
// ASYNC OPERATIONS: Only autocomplete needs async // ASYNC OPERATIONS: Only suggestions need async
// =================================================================== // ===================================================================
/// Trigger autocomplete (async because it fetches data) /// Trigger suggestions (async because it fetches data)
pub async fn trigger_autocomplete<A>(&mut self, provider: &mut A) -> Result<()> pub async fn trigger_suggestions<A>(&mut self, provider: &mut A) -> Result<()>
where where
A: AutocompleteProvider, A: SuggestionsProvider,
{ {
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_suggestions(field_index) {
return Ok(()); return Ok(());
} }
// Activate autocomplete UI // Activate suggestions UI
self.ui_state.activate_autocomplete(field_index); self.ui_state.activate_suggestions(field_index);
// Fetch suggestions from user (no conversion needed!) // Fetch suggestions from user (no conversion needed!)
let query = self.current_text(); let query = self.current_text();
self.suggestions = provider.fetch_suggestions(field_index, query).await?; self.suggestions = provider.fetch_suggestions(field_index, query).await?;
// Update UI state // Update UI state
self.ui_state.autocomplete.is_loading = false; self.ui_state.suggestions.is_loading = false;
if !self.suggestions.is_empty() { if !self.suggestions.is_empty() {
self.ui_state.autocomplete.selected_index = Some(0); self.ui_state.suggestions.selected_index = Some(0);
// Compute initial inline completion from first suggestion
self.update_inline_completion();
} }
Ok(()) Ok(())
} }
/// Navigate autocomplete suggestions /// Navigate suggestions
pub fn autocomplete_next(&mut self) { pub fn suggestions_next(&mut self) {
if !self.ui_state.autocomplete.is_active || self.suggestions.is_empty() { if !self.ui_state.suggestions.is_active || self.suggestions.is_empty() {
return; return;
} }
let current = self.ui_state.autocomplete.selected_index.unwrap_or(0); let current = self.ui_state.suggestions.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.suggestions.selected_index = Some(next);
// Update inline completion to reflect new highlighted item
self.update_inline_completion();
} }
/// Apply selected autocomplete suggestion /// Apply selected suggestion
pub fn apply_autocomplete(&mut self) -> Option<String> { /// Apply selected suggestion
if let Some(selected_index) = self.ui_state.autocomplete.selected_index { pub fn apply_suggestion(&mut self) -> Option<String> {
if let Some(selected_index) = self.ui_state.suggestions.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;
@@ -637,8 +677,8 @@ impl<D: DataProvider> FormEditor<D> {
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 suggestions
self.ui_state.deactivate_autocomplete(); self.ui_state.deactivate_suggestions();
self.suggestions.clear(); self.suggestions.clear();
// Validate the new content if validation is enabled // Validate the new content if validation is enabled
@@ -667,6 +707,45 @@ impl<D: DataProvider> FormEditor<D> {
return Ok(()); return Ok(());
} }
// Skip computed fields during navigation when feature enabled
#[cfg(feature = "computed")]
{
if let Some(computed_state) = &self.ui_state.computed {
// Find previous non-computed field
let mut candidate = self.ui_state.current_field;
for _ in 0..field_count {
candidate = candidate.saturating_sub(1);
if !computed_state.is_computed_field(candidate) {
// Validate and move as usual
#[cfg(feature = "validation")]
{
let current_text = self.current_text();
if !self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) {
if let Some(reason) = self.ui_state.validation.field_switch_block_reason(self.ui_state.current_field, current_text) {
tracing::debug!("Field switch blocked: {}", reason);
return Err(anyhow::anyhow!("Cannot switch fields: {}", reason));
}
}
}
#[cfg(feature = "validation")]
{
let current_text = self.current_text().to_string();
let _validation_result = self.ui_state.validation.validate_field_content(
self.ui_state.current_field,
&current_text,
);
}
self.ui_state.move_to_field(candidate, field_count);
self.clamp_cursor_to_current_field();
return Ok(());
}
if candidate == 0 {
break;
}
}
}
}
// Check if field switching is allowed (minimum character enforcement) // Check if field switching is allowed (minimum character enforcement)
#[cfg(feature = "validation")] #[cfg(feature = "validation")]
{ {
@@ -704,6 +783,45 @@ impl<D: DataProvider> FormEditor<D> {
return Ok(()); return Ok(());
} }
// Skip computed fields during navigation when feature enabled
#[cfg(feature = "computed")]
{
if let Some(computed_state) = &self.ui_state.computed {
// Find next non-computed field
let mut candidate = self.ui_state.current_field;
for _ in 0..field_count {
candidate = (candidate + 1).min(field_count - 1);
if !computed_state.is_computed_field(candidate) {
// Validate and move as usual
#[cfg(feature = "validation")]
{
let current_text = self.current_text();
if !self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) {
if let Some(reason) = self.ui_state.validation.field_switch_block_reason(self.ui_state.current_field, current_text) {
tracing::debug!("Field switch blocked: {}", reason);
return Err(anyhow::anyhow!("Cannot switch fields: {}", reason));
}
}
}
#[cfg(feature = "validation")]
{
let current_text = self.current_text().to_string();
let _validation_result = self.ui_state.validation.validate_field_content(
self.ui_state.current_field,
&current_text,
);
}
self.ui_state.move_to_field(candidate, field_count);
self.clamp_cursor_to_current_field();
return Ok(());
}
if candidate == field_count - 1 {
break;
}
}
}
}
// Check if field switching is allowed (minimum character enforcement) // Check if field switching is allowed (minimum character enforcement)
#[cfg(feature = "validation")] #[cfg(feature = "validation")]
{ {
@@ -762,6 +880,112 @@ impl<D: DataProvider> FormEditor<D> {
self.move_up() self.move_up()
} }
// ================================================================================================
// COMPUTED FIELDS (behind 'computed' feature)
// ================================================================================================
/// Initialize computed fields from provider and computed provider
#[cfg(feature = "computed")]
pub fn set_computed_provider<C>(&mut self, mut provider: C)
where
C: crate::computed::ComputedProvider,
{
// Initialize computed state
self.ui_state.computed = Some(crate::computed::ComputedState::new());
// Register computed fields and their dependencies
let field_count = self.data_provider.field_count();
for field_index in 0..field_count {
if provider.handles_field(field_index) {
let deps = provider.field_dependencies(field_index);
if let Some(computed_state) = &mut self.ui_state.computed {
computed_state.register_computed_field(field_index, deps);
}
}
}
// Initial computation of all computed fields
self.recompute_all_fields(&mut provider);
}
/// Recompute specific computed fields
#[cfg(feature = "computed")]
pub fn recompute_fields<C>(&mut self, provider: &mut C, field_indices: &[usize])
where
C: crate::computed::ComputedProvider,
{
if let Some(computed_state) = &mut self.ui_state.computed {
// Collect all field values for context
let field_values: Vec<String> = (0..self.data_provider.field_count())
.map(|i| {
if computed_state.is_computed_field(i) {
// Use cached computed value
computed_state
.get_computed_value(i)
.cloned()
.unwrap_or_default()
} else {
// Use regular field value
self.data_provider.field_value(i).to_string()
}
})
.collect();
let field_refs: Vec<&str> = field_values.iter().map(|s| s.as_str()).collect();
// Recompute specified fields
for &field_index in field_indices {
if provider.handles_field(field_index) {
let context = crate::computed::ComputedContext {
field_values: &field_refs,
target_field: field_index,
current_field: Some(self.ui_state.current_field),
};
let computed_value = provider.compute_field(context);
computed_state.set_computed_value(field_index, computed_value);
}
}
}
}
/// Recompute all computed fields
#[cfg(feature = "computed")]
pub fn recompute_all_fields<C>(&mut self, provider: &mut C)
where
C: crate::computed::ComputedProvider,
{
if let Some(computed_state) = &self.ui_state.computed {
let computed_fields: Vec<usize> = computed_state.computed_fields().collect();
self.recompute_fields(provider, &computed_fields);
}
}
/// Trigger recomputation when field changes (call this after set_field_value)
#[cfg(feature = "computed")]
pub fn on_field_changed<C>(&mut self, provider: &mut C, changed_field: usize)
where
C: crate::computed::ComputedProvider,
{
if let Some(computed_state) = &self.ui_state.computed {
let fields_to_update = computed_state.fields_to_recompute(changed_field);
if !fields_to_update.is_empty() {
self.recompute_fields(provider, &fields_to_update);
}
}
}
/// Enhanced getter that returns computed values for computed fields when available
#[cfg(feature = "computed")]
pub fn effective_field_value(&self, field_index: usize) -> String {
if let Some(computed_state) = &self.ui_state.computed {
if let Some(computed_value) = computed_state.get_computed_value(field_index) {
return computed_value.clone();
}
}
self.data_provider.field_value(field_index).to_string()
}
/// Move to next field (alternative to move_down) /// Move to next field (alternative to move_down)
pub fn next_field(&mut self) -> Result<()> { pub fn next_field(&mut self) -> Result<()> {
self.move_down() self.move_down()
@@ -951,14 +1175,23 @@ impl<D: DataProvider> FormEditor<D> {
} }
self.set_mode(AppMode::ReadOnly); self.set_mode(AppMode::ReadOnly);
// Deactivate autocomplete when exiting edit mode // Deactivate suggestions when exiting edit mode
self.ui_state.deactivate_autocomplete(); self.ui_state.deactivate_suggestions();
Ok(()) Ok(())
} }
/// Enter edit mode from read-only mode (vim i/a/o) /// Enter edit mode from read-only mode (vim i/a/o)
pub fn enter_edit_mode(&mut self) { pub fn enter_edit_mode(&mut self) {
#[cfg(feature = "computed")]
{
if let Some(computed_state) = &self.ui_state.computed {
if computed_state.is_computed_field(self.ui_state.current_field) {
// Can't edit computed fields - silently ignore
return;
}
}
}
self.set_mode(AppMode::Edit); self.set_mode(AppMode::Edit);
} }

View File

@@ -4,14 +4,18 @@ pub mod canvas;
pub mod editor; pub mod editor;
pub mod data_provider; pub mod data_provider;
// Only include autocomplete module if feature is enabled // Only include suggestions module if feature is enabled
#[cfg(feature = "autocomplete")] #[cfg(feature = "suggestions")]
pub mod autocomplete; pub mod suggestions;
// Only include validation module if feature is enabled // Only include validation module if feature is enabled
#[cfg(feature = "validation")] #[cfg(feature = "validation")]
pub mod validation; pub mod validation;
// Only include computed module if feature is enabled
#[cfg(feature = "computed")]
pub mod computed;
#[cfg(feature = "cursor-style")] #[cfg(feature = "cursor-style")]
pub use canvas::CursorManager; pub use canvas::CursorManager;
@@ -21,7 +25,7 @@ pub use canvas::CursorManager;
// Main API exports // Main API exports
pub use editor::FormEditor; pub use editor::FormEditor;
pub use data_provider::{DataProvider, AutocompleteProvider, SuggestionItem}; pub use data_provider::{DataProvider, SuggestionsProvider, SuggestionItem};
// UI state (read-only access for users) // UI state (read-only access for users)
pub use canvas::state::EditorState; pub use canvas::state::EditorState;
@@ -41,6 +45,10 @@ pub use validation::{
CustomFormatter, FormattingResult, PositionMapper, DefaultPositionMapper, CustomFormatter, FormattingResult, PositionMapper, DefaultPositionMapper,
}; };
// Computed exports (only when computed feature is enabled)
#[cfg(feature = "computed")]
pub use computed::{ComputedProvider, ComputedContext, ComputedState};
// Theming and GUI // Theming and GUI
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
pub use canvas::theme::{CanvasTheme, DefaultCanvasTheme}; pub use canvas::theme::{CanvasTheme, DefaultCanvasTheme};
@@ -51,5 +59,5 @@ pub use canvas::gui::render_canvas;
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
pub use canvas::gui::render_canvas_default; pub use canvas::gui::render_canvas_default;
#[cfg(all(feature = "gui", feature = "autocomplete"))] #[cfg(all(feature = "gui", feature = "suggestions"))]
pub use autocomplete::gui::render_autocomplete_dropdown; pub use suggestions::gui::render_suggestions_dropdown;

View File

@@ -1,5 +1,5 @@
// src/autocomplete/gui.rs // src/suggestions/gui.rs
//! Autocomplete GUI updated to work with FormEditor //! Suggestions dropdown GUI (not inline autocomplete) updated to work with FormEditor
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
use ratatui::{ use ratatui::{
@@ -17,9 +17,9 @@ use crate::editor::FormEditor;
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
/// Render autocomplete dropdown for FormEditor - call this AFTER rendering canvas /// Render suggestions dropdown for FormEditor - call this AFTER rendering canvas
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
pub fn render_autocomplete_dropdown<T: CanvasTheme, D: DataProvider>( pub fn render_suggestions_dropdown<T: CanvasTheme, D: DataProvider>(
f: &mut Frame, f: &mut Frame,
frame_area: Rect, frame_area: Rect,
input_rect: Rect, input_rect: Rect,
@@ -28,14 +28,14 @@ pub fn render_autocomplete_dropdown<T: CanvasTheme, D: DataProvider>(
) { ) {
let ui_state = editor.ui_state(); let ui_state = editor.ui_state();
if !ui_state.is_autocomplete_active() { if !ui_state.is_suggestions_active() {
return; return;
} }
if ui_state.autocomplete.is_loading { if ui_state.suggestions.is_loading {
render_loading_indicator(f, frame_area, input_rect, theme); render_loading_indicator(f, frame_area, input_rect, theme);
} else if !editor.suggestions().is_empty() { } else if !editor.suggestions().is_empty() {
render_suggestions_dropdown(f, frame_area, input_rect, theme, editor.suggestions(), ui_state.autocomplete.selected_index); render_suggestions_dropdown_list(f, frame_area, input_rect, theme, editor.suggestions(), ui_state.suggestions.selected_index);
} }
} }
@@ -71,7 +71,7 @@ fn render_loading_indicator<T: CanvasTheme>(
/// Show actual suggestions list /// Show actual suggestions list
#[cfg(feature = "gui")] #[cfg(feature = "gui")]
fn render_suggestions_dropdown<T: CanvasTheme>( fn render_suggestions_dropdown_list<T: CanvasTheme>(
f: &mut Frame, f: &mut Frame,
frame_area: Rect, frame_area: Rect,
input_rect: Rect, input_rect: Rect,

View File

@@ -0,0 +1,12 @@
// src/suggestions/mod.rs
pub mod state;
#[cfg(feature = "gui")]
pub mod gui;
// Re-export the main suggestion types
pub use state::{SuggestionsProvider, SuggestionItem};
// Re-export GUI functions if available
#[cfg(feature = "gui")]
pub use gui::render_suggestions_dropdown;

View File

@@ -0,0 +1,5 @@
// src/suggestions/state.rs
//! Suggestions provider types (for dropdown suggestions, not real inline autocomplete)
// Re-export the main types from data_provider
pub use crate::data_provider::{SuggestionsProvider, SuggestionItem};

View File

@@ -27,7 +27,7 @@ show_module() {
# Main modules # Main modules
show_module "canvas" "CANVAS SYSTEM" show_module "canvas" "CANVAS SYSTEM"
show_module "autocomplete" "AUTOCOMPLETE SYSTEM" show_module "suggestions" "SUGGESTIONS SYSTEM"
show_module "config" "CONFIGURATION SYSTEM" show_module "config" "CONFIGURATION SYSTEM"
# Show lib.rs and other root files # Show lib.rs and other root files
@@ -45,7 +45,7 @@ fi
echo -e "\n\033[1;36m==========================================" echo -e "\n\033[1;36m=========================================="
echo "To view specific module documentation:" echo "To view specific module documentation:"
echo " ./view_canvas_docs.sh canvas" echo " ./view_canvas_docs.sh canvas"
echo " ./view_canvas_docs.sh autocomplete" echo " ./view_canvas_docs.sh suggestions"
echo " ./view_canvas_docs.sh config" echo " ./view_canvas_docs.sh config"
echo "==========================================\033[0m" echo "==========================================\033[0m"