canvas ready, lets implement now client
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -557,6 +557,7 @@ version = "0.4.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"canvas",
|
||||
"common",
|
||||
"crossterm",
|
||||
"dirs",
|
||||
|
||||
@@ -18,3 +18,19 @@ tokio = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4.4"
|
||||
|
||||
[[example]]
|
||||
name = "simple_login"
|
||||
path = "examples/simple_login.rs"
|
||||
|
||||
[[example]]
|
||||
name = "config_screen"
|
||||
path = "examples/config_screen.rs"
|
||||
|
||||
[[example]]
|
||||
name = "basic_usage"
|
||||
path = "examples/basic_usage.rs"
|
||||
|
||||
[[example]]
|
||||
name = "integration_patterns"
|
||||
path = "examples/integration_patterns.rs"
|
||||
|
||||
366
canvas/README.md
Normal file
366
canvas/README.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# Canvas 🎨
|
||||
|
||||
A reusable, type-safe canvas system for building form-based TUI applications with vim-like modal editing.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **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
|
||||
- **Vim-Like Experience**: Modal editing with familiar keybindings
|
||||
- **Suggestion System**: Built-in autocomplete and suggestions support
|
||||
- **Framework Agnostic**: Works with any TUI framework or raw terminal handling
|
||||
- **Async Ready**: Full async/await support for modern Rust applications
|
||||
- **Batch Operations**: Execute multiple actions atomically
|
||||
- **Extensible**: Custom actions and feature-specific handling
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
Add to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
cargo add canvas
|
||||
```
|
||||
|
||||
Implement the `CanvasState` trait:
|
||||
|
||||
```rust
|
||||
use canvas::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LoginForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
username: String,
|
||||
password: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
impl CanvasState for LoginForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.username,
|
||||
1 => &self.password,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.username,
|
||||
1 => &mut self.password,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.username, &self.password] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Username", "Password"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
```
|
||||
|
||||
Use the type-safe action dispatcher:
|
||||
|
||||
```rust
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut form = LoginForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Type a character - compile-time safe!
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar('h'),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await?;
|
||||
|
||||
// Move to next field
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::NextField,
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await?;
|
||||
|
||||
// Batch operations
|
||||
let actions = vec![
|
||||
CanvasAction::InsertChar('p'),
|
||||
CanvasAction::InsertChar('a'),
|
||||
CanvasAction::InsertChar('s'),
|
||||
CanvasAction::InsertChar('s'),
|
||||
];
|
||||
|
||||
ActionDispatcher::dispatch_batch(actions, &mut form, &mut ideal_cursor).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 📚 Examples
|
||||
|
||||
The `examples/` directory contains comprehensive examples showing different usage patterns:
|
||||
|
||||
### Run the Examples
|
||||
|
||||
```bash
|
||||
# Basic login form with TUI
|
||||
cargo run --example simple_login
|
||||
|
||||
# Advanced configuration screen with suggestions and validation
|
||||
cargo run --example config_screen
|
||||
|
||||
# API usage patterns and quick start guide
|
||||
cargo run --example basic_usage
|
||||
|
||||
# Advanced integration patterns (state machines, events, validation)
|
||||
cargo run --example integration_patterns
|
||||
```
|
||||
|
||||
### Example Overview
|
||||
|
||||
| Example | Description | Key Features |
|
||||
|---------|-------------|--------------|
|
||||
| `simple_login` | Interactive login form TUI | Basic form, custom actions, password masking |
|
||||
| `config_screen` | Configuration editor | Auto-suggestions, field validation, complex UI |
|
||||
| `basic_usage` | API demonstration | All core patterns, non-interactive |
|
||||
| `integration_patterns` | Architecture patterns | State machines, events, validation pipelines |
|
||||
|
||||
## 🎯 Type-Safe Actions
|
||||
|
||||
The Canvas system uses strongly-typed actions instead of error-prone strings:
|
||||
|
||||
```rust
|
||||
// ✅ Type-safe - impossible to make typos
|
||||
ActionDispatcher::dispatch(CanvasAction::MoveLeft, &mut form, &mut cursor).await?;
|
||||
|
||||
// ❌ Old way - runtime errors waiting to happen
|
||||
execute_edit_action("move_left", key, &mut form, &mut cursor).await?;
|
||||
execute_edit_action("move_leftt", key, &mut form, &mut cursor).await?; // Oops!
|
||||
```
|
||||
|
||||
### Available Actions
|
||||
|
||||
```rust
|
||||
pub enum CanvasAction {
|
||||
// Character input
|
||||
InsertChar(char),
|
||||
|
||||
// Deletion
|
||||
DeleteBackward,
|
||||
DeleteForward,
|
||||
|
||||
// Movement
|
||||
MoveLeft, MoveRight, MoveUp, MoveDown,
|
||||
MoveLineStart, MoveLineEnd,
|
||||
MoveWordNext, MoveWordPrev,
|
||||
|
||||
// Navigation
|
||||
NextField, PrevField,
|
||||
MoveFirstLine, MoveLastLine,
|
||||
|
||||
// Suggestions
|
||||
SuggestionUp, SuggestionDown,
|
||||
SelectSuggestion, ExitSuggestions,
|
||||
|
||||
// Extensibility
|
||||
Custom(String),
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Advanced Features
|
||||
|
||||
### Suggestions and Autocomplete
|
||||
|
||||
```rust
|
||||
impl CanvasState for MyForm {
|
||||
fn get_suggestions(&self) -> Option<&[String]> {
|
||||
if self.suggestions.is_active {
|
||||
Some(&self.suggestions.suggestions)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::InsertChar('@') => {
|
||||
// Trigger email suggestions
|
||||
let suggestions = vec![
|
||||
format!("{}@gmail.com", self.username),
|
||||
format!("{}@company.com", self.username),
|
||||
];
|
||||
self.activate_suggestions(suggestions);
|
||||
None // Let generic handler insert the '@'
|
||||
}
|
||||
CanvasAction::SelectSuggestion => {
|
||||
if let Some(suggestion) = self.suggestions.get_selected() {
|
||||
*self.get_current_input_mut() = suggestion.clone();
|
||||
self.deactivate_suggestions();
|
||||
Some("Applied suggestion".to_string())
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Actions
|
||||
|
||||
```rust
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"uppercase" => {
|
||||
*self.get_current_input_mut() = self.get_current_input().to_uppercase();
|
||||
Some("Converted to uppercase".to_string())
|
||||
}
|
||||
"validate_email" => {
|
||||
if self.get_current_input().contains('@') {
|
||||
Some("Email is valid".to_string())
|
||||
} else {
|
||||
Some("Invalid email format".to_string())
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration with TUI Frameworks
|
||||
|
||||
Canvas is framework-agnostic and works with any TUI library:
|
||||
|
||||
```rust
|
||||
// Works with crossterm (see examples)
|
||||
// Works with termion
|
||||
// Works with ratatui/tui-rs
|
||||
// Works with cursive
|
||||
// Works with raw terminal I/O
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
Canvas follows a clean, layered architecture:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Your Application │
|
||||
├─────────────────────────────────────┤
|
||||
│ ActionDispatcher │ ← High-level API
|
||||
├─────────────────────────────────────┤
|
||||
│ CanvasAction (Type-Safe) │ ← Type safety layer
|
||||
├─────────────────────────────────────┤
|
||||
│ Action Handlers │ ← Core logic
|
||||
├─────────────────────────────────────┤
|
||||
│ CanvasState Trait │ ← Your implementation
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🤝 Why Canvas?
|
||||
|
||||
### Before Canvas
|
||||
```rust
|
||||
// ❌ Error-prone string actions
|
||||
execute_action("move_left", key, state)?;
|
||||
execute_action("move_leftt", key, state)?; // Runtime error!
|
||||
|
||||
// ❌ Duplicate navigation logic everywhere
|
||||
impl MyLoginForm { /* navigation code */ }
|
||||
impl MyConfigForm { /* same navigation code */ }
|
||||
impl MyDataForm { /* same navigation code again */ }
|
||||
|
||||
// ❌ Manual cursor and field management
|
||||
if key == Key::Tab {
|
||||
current_field = (current_field + 1) % fields.len();
|
||||
cursor_pos = cursor_pos.min(current_input.len());
|
||||
}
|
||||
```
|
||||
|
||||
### With Canvas
|
||||
```rust
|
||||
// ✅ Type-safe actions
|
||||
ActionDispatcher::dispatch(CanvasAction::MoveLeft, state, cursor)?;
|
||||
// Typos are impossible - won't compile!
|
||||
|
||||
// ✅ Implement once, use everywhere
|
||||
impl CanvasState for MyForm { /* minimal implementation */ }
|
||||
// All navigation, editing, suggestions work automatically!
|
||||
|
||||
// ✅ High-level operations
|
||||
ActionDispatcher::dispatch_batch(actions, state, cursor)?;
|
||||
```
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
- **API Docs**: `cargo doc --open`
|
||||
- **Examples**: See `examples/` directory
|
||||
- **Migration Guide**: See `CANVAS_MIGRATION.md`
|
||||
|
||||
## 🔄 Migration from String-Based Actions
|
||||
|
||||
Canvas provides backwards compatibility during migration:
|
||||
|
||||
```rust
|
||||
// Legacy support (deprecated)
|
||||
execute_edit_action("move_left", key, state, cursor).await?;
|
||||
|
||||
// New type-safe way
|
||||
ActionDispatcher::dispatch(CanvasAction::MoveLeft, state, cursor).await?;
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run specific example
|
||||
cargo run --example simple_login
|
||||
|
||||
# Check type safety
|
||||
cargo check
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- Rust 1.70+
|
||||
- Terminal with cursor support
|
||||
- Optional: async runtime (tokio) for examples
|
||||
|
||||
## 🤔 FAQ
|
||||
|
||||
**Q: Does Canvas work with [my TUI framework]?**
|
||||
A: Yes! Canvas is framework-agnostic. Just implement `CanvasState` and handle the key events.
|
||||
|
||||
**Q: Can I extend Canvas with custom actions?**
|
||||
A: Absolutely! Use `CanvasAction::Custom("my_action")` or implement `handle_feature_action`.
|
||||
|
||||
**Q: Is Canvas suitable for complex forms?**
|
||||
A: Yes! See the `config_screen` example for validation, suggestions, and multi-field forms.
|
||||
|
||||
**Q: How do I migrate from string-based actions?**
|
||||
A: Canvas provides backwards compatibility. Migrate incrementally using the type-safe APIs.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under either of:
|
||||
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
- MIT License ([LICENSE-MIT](LICENSE-MIT))
|
||||
|
||||
at your option.
|
||||
|
||||
## 🙏 Contributing
|
||||
|
||||
Will write here something later on, too busy rn
|
||||
|
||||
---
|
||||
|
||||
Built with ❤️ for the Rust TUI community
|
||||
378
canvas/examples/basic_usage.rs
Normal file
378
canvas/examples/basic_usage.rs
Normal file
@@ -0,0 +1,378 @@
|
||||
// examples/basic_usage.rs
|
||||
//! Basic usage patterns and quick start guide
|
||||
//!
|
||||
//! This example demonstrates the core patterns for using the canvas crate:
|
||||
//! 1. Implementing CanvasState
|
||||
//! 2. Using the ActionDispatcher
|
||||
//! 3. Handling different types of actions
|
||||
//! 4. Working with suggestions
|
||||
//!
|
||||
//! Run with: cargo run --example basic_usage
|
||||
|
||||
use canvas::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("🎨 Canvas Crate - Basic Usage Patterns");
|
||||
println!("=====================================\n");
|
||||
|
||||
// Example 1: Minimal form implementation
|
||||
example_1_minimal_form();
|
||||
|
||||
// Example 2: Form with suggestions
|
||||
example_2_with_suggestions();
|
||||
|
||||
// Example 3: Custom actions
|
||||
example_3_custom_actions().await;
|
||||
|
||||
// Example 4: Batch operations
|
||||
example_4_batch_operations().await;
|
||||
}
|
||||
|
||||
// Example 1: Minimal form - just the required methods
|
||||
fn example_1_minimal_form() {
|
||||
println!("📝 Example 1: Minimal Form Implementation");
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SimpleForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
name: String,
|
||||
email: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
impl SimpleForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
name: String::new(),
|
||||
email: String::new(),
|
||||
has_changes: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for SimpleForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.name,
|
||||
1 => &self.email,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.name,
|
||||
1 => &mut self.email,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.name, &self.email] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Name", "Email"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
|
||||
let form = SimpleForm::new();
|
||||
println!(" Created form with {} fields", form.fields().len());
|
||||
println!(" Current field: {}", form.fields()[form.current_field()]);
|
||||
println!(" ✅ Minimal implementation works!\n");
|
||||
}
|
||||
|
||||
// Example 2: Form with suggestion support
|
||||
fn example_2_with_suggestions() {
|
||||
println!("💡 Example 2: Form with Suggestions");
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FormWithSuggestions {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
country: String,
|
||||
has_changes: bool,
|
||||
suggestions: SuggestionState,
|
||||
}
|
||||
|
||||
impl FormWithSuggestions {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
country: String::new(),
|
||||
has_changes: false,
|
||||
suggestions: SuggestionState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for FormWithSuggestions {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.country }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.country }
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.country] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Country"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
|
||||
// Suggestion support
|
||||
fn get_suggestions(&self) -> Option<&[String]> {
|
||||
if self.suggestions.is_active {
|
||||
Some(&self.suggestions.suggestions)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||
self.suggestions.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_suggestion_index(&mut self, index: Option<usize>) {
|
||||
self.suggestions.selected_index = index;
|
||||
}
|
||||
|
||||
fn activate_suggestions(&mut self, suggestions: Vec<String>) {
|
||||
self.suggestions.activate_with_suggestions(suggestions);
|
||||
}
|
||||
|
||||
fn deactivate_suggestions(&mut self) {
|
||||
self.suggestions.deactivate();
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::SelectSuggestion => {
|
||||
// Fix: Clone the suggestion first to avoid borrow checker issues
|
||||
if let Some(suggestion) = self.suggestions.get_selected().cloned() {
|
||||
self.country = suggestion.clone();
|
||||
self.cursor_pos = suggestion.len();
|
||||
self.deactivate_suggestions();
|
||||
self.has_changes = true;
|
||||
return Some(format!("Selected: {}", suggestion));
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = FormWithSuggestions::new();
|
||||
|
||||
// Simulate user typing and triggering suggestions
|
||||
form.activate_suggestions(vec![
|
||||
"United States".to_string(),
|
||||
"United Kingdom".to_string(),
|
||||
"Ukraine".to_string(),
|
||||
]);
|
||||
|
||||
println!(" Activated suggestions: {:?}", form.get_suggestions().unwrap());
|
||||
println!(" Current selection: {:?}", form.get_selected_suggestion_index());
|
||||
|
||||
// Navigate suggestions
|
||||
form.set_selected_suggestion_index(Some(1));
|
||||
println!(" Navigated to: {}", form.suggestions.get_selected().unwrap());
|
||||
println!(" ✅ Suggestions work!\n");
|
||||
}
|
||||
|
||||
// Example 3: Custom actions
|
||||
async fn example_3_custom_actions() {
|
||||
println!("⚡ Example 3: Custom Actions");
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FormWithCustomActions {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
text: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
impl FormWithCustomActions {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
text: "hello world".to_string(),
|
||||
has_changes: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for FormWithCustomActions {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.text }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.text }
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.text] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Text"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"uppercase" => {
|
||||
self.text = self.text.to_uppercase();
|
||||
self.has_changes = true;
|
||||
Some("Converted to uppercase".to_string())
|
||||
}
|
||||
"reverse" => {
|
||||
self.text = self.text.chars().rev().collect();
|
||||
self.has_changes = true;
|
||||
Some("Reversed text".to_string())
|
||||
}
|
||||
"word_count" => {
|
||||
let count = self.text.split_whitespace().count();
|
||||
Some(format!("Word count: {}", count))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = FormWithCustomActions::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
println!(" Initial text: '{}'", form.text);
|
||||
|
||||
// Execute custom actions
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("uppercase".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
println!(" After uppercase: '{}' - {}", form.text, result.message().unwrap());
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("reverse".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
println!(" After reverse: '{}' - {}", form.text, result.message().unwrap());
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("word_count".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
println!(" {}", result.message().unwrap());
|
||||
println!(" ✅ Custom actions work!\n");
|
||||
}
|
||||
|
||||
// Example 4: Batch operations
|
||||
async fn example_4_batch_operations() {
|
||||
println!("📦 Example 4: Batch Operations");
|
||||
|
||||
// Reuse the simple form from example 1
|
||||
#[derive(Debug)]
|
||||
struct SimpleForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
name: String,
|
||||
email: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
impl SimpleForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
name: String::new(),
|
||||
email: String::new(),
|
||||
has_changes: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for SimpleForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.name,
|
||||
1 => &self.email,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.name,
|
||||
1 => &mut self.email,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.name, &self.email] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Name", "Email"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
|
||||
let mut form = SimpleForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Execute a sequence of actions to type "John" in the name field
|
||||
let actions = vec![
|
||||
CanvasAction::InsertChar('J'),
|
||||
CanvasAction::InsertChar('o'),
|
||||
CanvasAction::InsertChar('h'),
|
||||
CanvasAction::InsertChar('n'),
|
||||
CanvasAction::NextField, // Move to email field
|
||||
CanvasAction::InsertChar('j'),
|
||||
CanvasAction::InsertChar('o'),
|
||||
CanvasAction::InsertChar('h'),
|
||||
CanvasAction::InsertChar('n'),
|
||||
CanvasAction::InsertChar('@'),
|
||||
CanvasAction::InsertChar('e'),
|
||||
CanvasAction::InsertChar('x'),
|
||||
CanvasAction::InsertChar('a'),
|
||||
CanvasAction::InsertChar('m'),
|
||||
CanvasAction::InsertChar('p'),
|
||||
CanvasAction::InsertChar('l'),
|
||||
CanvasAction::InsertChar('e'),
|
||||
CanvasAction::InsertChar('.'),
|
||||
CanvasAction::InsertChar('c'),
|
||||
CanvasAction::InsertChar('o'),
|
||||
CanvasAction::InsertChar('m'),
|
||||
];
|
||||
|
||||
println!(" Executing {} actions in batch...", actions.len());
|
||||
|
||||
let results = ActionDispatcher::dispatch_batch(
|
||||
actions,
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
println!(" Completed {} actions", results.len());
|
||||
println!(" Final state:");
|
||||
println!(" Name: '{}'", form.name);
|
||||
println!(" Email: '{}'", form.email);
|
||||
println!(" Current field: {}", form.fields()[form.current_field()]);
|
||||
println!(" Has changes: {}", form.has_changes);
|
||||
}
|
||||
590
canvas/examples/config_screen.rs
Normal file
590
canvas/examples/config_screen.rs
Normal file
@@ -0,0 +1,590 @@
|
||||
// examples/config_screen.rs
|
||||
//! Advanced configuration screen with suggestions and validation
|
||||
//!
|
||||
//! This example demonstrates:
|
||||
//! - Multiple field types
|
||||
//! - Auto-suggestions
|
||||
//! - Field validation
|
||||
//! - Custom actions
|
||||
//!
|
||||
//! Run with: cargo run --example config_screen
|
||||
|
||||
use canvas::prelude::*;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use std::io::{self, Write};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ConfigForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
|
||||
// Configuration fields
|
||||
server_host: String,
|
||||
server_port: String,
|
||||
database_url: String,
|
||||
log_level: String,
|
||||
max_connections: String,
|
||||
|
||||
has_changes: bool,
|
||||
suggestions: SuggestionState,
|
||||
}
|
||||
|
||||
impl ConfigForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
server_host: "localhost".to_string(),
|
||||
server_port: "8080".to_string(),
|
||||
database_url: String::new(),
|
||||
log_level: "info".to_string(),
|
||||
max_connections: "100".to_string(),
|
||||
has_changes: false,
|
||||
suggestions: SuggestionState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn field_names() -> Vec<&'static str> {
|
||||
vec![
|
||||
"Server Host",
|
||||
"Server Port",
|
||||
"Database URL",
|
||||
"Log Level",
|
||||
"Max Connections"
|
||||
]
|
||||
}
|
||||
|
||||
fn get_field_value(&self, index: usize) -> &String {
|
||||
match index {
|
||||
0 => &self.server_host,
|
||||
1 => &self.server_port,
|
||||
2 => &self.database_url,
|
||||
3 => &self.log_level,
|
||||
4 => &self.max_connections,
|
||||
_ => panic!("Invalid field index: {}", index),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_field_value_mut(&mut self, index: usize) -> &mut String {
|
||||
match index {
|
||||
0 => &mut self.server_host,
|
||||
1 => &mut self.server_port,
|
||||
2 => &mut self.database_url,
|
||||
3 => &mut self.log_level,
|
||||
4 => &mut self.max_connections,
|
||||
_ => panic!("Invalid field index: {}", index),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_field(&self, index: usize) -> Option<String> {
|
||||
let value = self.get_field_value(index);
|
||||
match index {
|
||||
0 => { // Server Host
|
||||
if value.trim().is_empty() {
|
||||
Some("Server host cannot be empty".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
1 => { // Server Port
|
||||
if let Ok(port) = value.parse::<u16>() {
|
||||
if port == 0 {
|
||||
Some("Port must be greater than 0".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
Some("Port must be a valid number (1-65535)".to_string())
|
||||
}
|
||||
}
|
||||
2 => { // Database URL
|
||||
if !value.is_empty() && !value.starts_with("postgresql://") && !value.starts_with("mysql://") && !value.starts_with("sqlite://") {
|
||||
Some("Database URL should start with postgresql://, mysql://, or sqlite://".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
3 => { // Log Level
|
||||
let valid_levels = ["trace", "debug", "info", "warn", "error"];
|
||||
if !valid_levels.contains(&value.to_lowercase().as_str()) {
|
||||
Some("Log level must be one of: trace, debug, info, warn, error".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
4 => { // Max Connections
|
||||
if let Ok(connections) = value.parse::<u32>() {
|
||||
if connections == 0 {
|
||||
Some("Max connections must be greater than 0".to_string())
|
||||
} else if connections > 10000 {
|
||||
Some("Max connections seems too high (>10000)".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
Some("Max connections must be a valid number".to_string())
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_suggestions_for_field(&self, index: usize, current_value: &str) -> Vec<String> {
|
||||
match index {
|
||||
0 => { // Server Host
|
||||
vec![
|
||||
"localhost".to_string(),
|
||||
"127.0.0.1".to_string(),
|
||||
"0.0.0.0".to_string(),
|
||||
format!("{}.local", current_value),
|
||||
]
|
||||
}
|
||||
1 => { // Server Port
|
||||
vec![
|
||||
"8080".to_string(),
|
||||
"3000".to_string(),
|
||||
"8000".to_string(),
|
||||
"80".to_string(),
|
||||
"443".to_string(),
|
||||
]
|
||||
}
|
||||
2 => { // Database URL
|
||||
if current_value.is_empty() {
|
||||
vec![
|
||||
"postgresql://localhost:5432/mydb".to_string(),
|
||||
"mysql://localhost:3306/mydb".to_string(),
|
||||
"sqlite://./database.db".to_string(),
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
3 => { // Log Level
|
||||
vec![
|
||||
"trace".to_string(),
|
||||
"debug".to_string(),
|
||||
"info".to_string(),
|
||||
"warn".to_string(),
|
||||
"error".to_string(),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|level| level.starts_with(¤t_value.to_lowercase()))
|
||||
.collect()
|
||||
}
|
||||
4 => { // Max Connections
|
||||
vec![
|
||||
"10".to_string(),
|
||||
"50".to_string(),
|
||||
"100".to_string(),
|
||||
"200".to_string(),
|
||||
"500".to_string(),
|
||||
]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for ConfigForm {
|
||||
fn current_field(&self) -> usize {
|
||||
self.current_field
|
||||
}
|
||||
|
||||
fn current_cursor_pos(&self) -> usize {
|
||||
self.cursor_pos
|
||||
}
|
||||
|
||||
fn set_current_field(&mut self, index: usize) {
|
||||
self.current_field = index.min(4); // 5 fields total (0-4)
|
||||
// Deactivate suggestions when changing fields
|
||||
self.deactivate_suggestions();
|
||||
}
|
||||
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||
self.cursor_pos = pos;
|
||||
}
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
self.get_field_value(self.current_field)
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
self.get_field_value_mut(self.current_field)
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> {
|
||||
vec![
|
||||
&self.server_host,
|
||||
&self.server_port,
|
||||
&self.database_url,
|
||||
&self.log_level,
|
||||
&self.max_connections,
|
||||
]
|
||||
}
|
||||
|
||||
fn fields(&self) -> Vec<&str> {
|
||||
Self::field_names()
|
||||
}
|
||||
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.has_changes
|
||||
}
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_changes = changed;
|
||||
}
|
||||
|
||||
// Suggestion support
|
||||
fn get_suggestions(&self) -> Option<&[String]> {
|
||||
if self.suggestions.is_active {
|
||||
Some(&self.suggestions.suggestions)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||
self.suggestions.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_suggestion_index(&mut self, index: Option<usize>) {
|
||||
self.suggestions.selected_index = index;
|
||||
}
|
||||
|
||||
fn activate_suggestions(&mut self, suggestions: Vec<String>) {
|
||||
self.suggestions.activate_with_suggestions(suggestions);
|
||||
}
|
||||
|
||||
fn deactivate_suggestions(&mut self) {
|
||||
self.suggestions.deactivate();
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::SelectSuggestion => {
|
||||
// Fix: Clone the suggestion first to avoid borrow checker issues
|
||||
if let Some(suggestion) = self.suggestions.get_selected().cloned() {
|
||||
*self.get_current_input_mut() = suggestion.clone();
|
||||
self.set_current_cursor_pos(suggestion.len());
|
||||
self.deactivate_suggestions();
|
||||
self.set_has_unsaved_changes(true);
|
||||
return Some("Applied suggestion".to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"trigger_suggestions" => {
|
||||
let current_value = self.get_current_input();
|
||||
let suggestions = self.get_suggestions_for_field(self.current_field, current_value);
|
||||
if !suggestions.is_empty() {
|
||||
self.activate_suggestions(suggestions);
|
||||
Some("Showing suggestions".to_string())
|
||||
} else {
|
||||
Some("No suggestions available".to_string())
|
||||
}
|
||||
}
|
||||
"validate_current" => {
|
||||
if let Some(error) = self.validate_field(self.current_field) {
|
||||
Some(format!("Validation Error: {}", error))
|
||||
} else {
|
||||
Some("Field is valid".to_string())
|
||||
}
|
||||
}
|
||||
"validate_all" => {
|
||||
let mut errors = Vec::new();
|
||||
for i in 0..5 {
|
||||
if let Some(error) = self.validate_field(i) {
|
||||
errors.push(format!("{}: {}", Self::field_names()[i], error));
|
||||
}
|
||||
}
|
||||
if errors.is_empty() {
|
||||
Some("All fields are valid!".to_string())
|
||||
} else {
|
||||
Some(format!("Errors found: {}", errors.join("; ")))
|
||||
}
|
||||
}
|
||||
"save_config" => {
|
||||
// Validate all fields first
|
||||
for i in 0..5 {
|
||||
if self.validate_field(i).is_some() {
|
||||
return Some("Cannot save: Please fix validation errors first".to_string());
|
||||
}
|
||||
}
|
||||
self.set_has_unsaved_changes(false);
|
||||
Some("Configuration saved successfully!".to_string())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
|
||||
// Auto-trigger suggestions for certain fields
|
||||
CanvasAction::InsertChar(_) => {
|
||||
// After character insertion, check if we should show suggestions
|
||||
match self.current_field {
|
||||
3 => { // Log level - always show suggestions for autocomplete
|
||||
let current_value = self.get_current_input();
|
||||
let suggestions = self.get_suggestions_for_field(self.current_field, current_value);
|
||||
if !suggestions.is_empty() {
|
||||
self.activate_suggestions(suggestions);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None // Let the generic handler insert the character
|
||||
}
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_ui(form: &ConfigForm, message: &str) -> io::Result<()> {
|
||||
print!("\x1B[2J\x1B[1;1H");
|
||||
|
||||
println!("╔════════════════════════════════════════════════════════════════╗");
|
||||
println!("║ CONFIGURATION EDITOR ║");
|
||||
println!("╠════════════════════════════════════════════════════════════════╣");
|
||||
|
||||
let field_names = ConfigForm::field_names();
|
||||
|
||||
for (i, field_name) in field_names.iter().enumerate() {
|
||||
let is_current = i == form.current_field;
|
||||
let indicator = if is_current { ">" } else { " " };
|
||||
let value = form.get_field_value(i);
|
||||
let display_value = if value.is_empty() {
|
||||
format!("<enter {}>", field_name.to_lowercase())
|
||||
} else {
|
||||
value.clone()
|
||||
};
|
||||
|
||||
// Truncate long values for display
|
||||
let display_value = if display_value.len() > 35 {
|
||||
format!("{}...", &display_value[..32])
|
||||
} else {
|
||||
display_value
|
||||
};
|
||||
|
||||
println!("║ {} {:15}: {:35} ║", indicator, field_name, display_value);
|
||||
|
||||
// Show cursor for current field
|
||||
if is_current {
|
||||
let cursor_pos = form.cursor_pos.min(value.len());
|
||||
let cursor_line = format!("║ {}{}║",
|
||||
" ".repeat(18 + cursor_pos),
|
||||
"▊"
|
||||
);
|
||||
println!("{:66}", cursor_line);
|
||||
}
|
||||
|
||||
// Show validation error if any
|
||||
if let Some(error) = form.validate_field(i) {
|
||||
let error_display = if error.len() > 58 {
|
||||
format!("{}...", &error[..55])
|
||||
} else {
|
||||
error
|
||||
};
|
||||
println!("║ ⚠️ {:58} ║", error_display);
|
||||
} else if is_current {
|
||||
println!("║{:64}║", "");
|
||||
}
|
||||
}
|
||||
|
||||
println!("╠════════════════════════════════════════════════════════════════╣");
|
||||
|
||||
// Show suggestions if active
|
||||
if let Some(suggestions) = form.get_suggestions() {
|
||||
println!("║ SUGGESTIONS: ║");
|
||||
for (i, suggestion) in suggestions.iter().enumerate() {
|
||||
let selected = form.get_selected_suggestion_index() == Some(i);
|
||||
let marker = if selected { "→" } else { " " };
|
||||
let display_suggestion = if suggestion.len() > 55 {
|
||||
format!("{}...", &suggestion[..52])
|
||||
} else {
|
||||
suggestion.clone()
|
||||
};
|
||||
println!("║ {} {:58} ║", marker, display_suggestion);
|
||||
}
|
||||
println!("╠════════════════════════════════════════════════════════════════╣");
|
||||
}
|
||||
|
||||
println!("║ CONTROLS: ║");
|
||||
println!("║ Tab/↑↓ - Navigate fields ║");
|
||||
println!("║ Ctrl+Space - Show suggestions ║");
|
||||
println!("║ ↑↓ - Navigate suggestions (when shown) ║");
|
||||
println!("║ Enter - Select suggestion / Validate field ║");
|
||||
println!("║ Ctrl+S - Save configuration ║");
|
||||
println!("║ Ctrl+V - Validate all fields ║");
|
||||
println!("║ Ctrl+C - Exit ║");
|
||||
println!("╠════════════════════════════════════════════════════════════════╣");
|
||||
|
||||
// Status
|
||||
let status = if !message.is_empty() {
|
||||
message.to_string()
|
||||
} else if form.has_changes {
|
||||
"Configuration modified - press Ctrl+S to save".to_string()
|
||||
} else {
|
||||
"Ready".to_string()
|
||||
};
|
||||
|
||||
let status_display = if status.len() > 58 {
|
||||
format!("{}...", &status[..55])
|
||||
} else {
|
||||
status
|
||||
};
|
||||
|
||||
println!("║ Status: {:55} ║", status_display);
|
||||
println!("╚════════════════════════════════════════════════════════════════╝");
|
||||
|
||||
io::stdout().flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
enable_raw_mode()?;
|
||||
io::stdout().execute(EnterAlternateScreen)?;
|
||||
|
||||
let mut form = ConfigForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
let mut message = String::new();
|
||||
|
||||
draw_ui(&form, &message)?;
|
||||
|
||||
loop {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if !message.is_empty() {
|
||||
message.clear();
|
||||
}
|
||||
|
||||
match key {
|
||||
// Exit
|
||||
KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, .. } => {
|
||||
break;
|
||||
}
|
||||
|
||||
// Show suggestions
|
||||
KeyEvent { code: KeyCode::Char(' '), modifiers: KeyModifiers::CONTROL, .. } => {
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("trigger_suggestions".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Validate current field or select suggestion
|
||||
KeyEvent { code: KeyCode::Enter, .. } => {
|
||||
if form.get_suggestions().is_some() {
|
||||
// Select suggestion
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::SelectSuggestion,
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
} else {
|
||||
// Validate current field
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate_current".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::CONTROL, .. } => {
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("save_config".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Validate all fields
|
||||
KeyEvent { code: KeyCode::Char('v'), modifiers: KeyModifiers::CONTROL, .. } => {
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate_all".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle up/down for suggestions
|
||||
KeyEvent { code: KeyCode::Up, .. } => {
|
||||
let action = if form.get_suggestions().is_some() {
|
||||
CanvasAction::SuggestionUp
|
||||
} else {
|
||||
CanvasAction::MoveUp
|
||||
};
|
||||
|
||||
let _ = ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await;
|
||||
}
|
||||
|
||||
KeyEvent { code: KeyCode::Down, .. } => {
|
||||
let action = if form.get_suggestions().is_some() {
|
||||
CanvasAction::SuggestionDown
|
||||
} else {
|
||||
CanvasAction::MoveDown
|
||||
};
|
||||
|
||||
let _ = ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await;
|
||||
}
|
||||
|
||||
// Handle escape to close suggestions
|
||||
KeyEvent { code: KeyCode::Esc, .. } => {
|
||||
if form.get_suggestions().is_some() {
|
||||
let _ = ActionDispatcher::dispatch(
|
||||
CanvasAction::ExitSuggestions,
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Regular key handling
|
||||
_ => {
|
||||
if let Some(action) = CanvasAction::from_key(key.code) {
|
||||
let result = ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await.unwrap();
|
||||
|
||||
if !result.is_success() {
|
||||
if let Some(msg) = result.message() {
|
||||
message = format!("Error: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw_ui(&form, &message)?;
|
||||
}
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
io::stdout().execute(LeaveAlternateScreen)?;
|
||||
println!("Configuration editor closed!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
617
canvas/examples/integration_patterns.rs
Normal file
617
canvas/examples/integration_patterns.rs
Normal file
@@ -0,0 +1,617 @@
|
||||
// examples/integration_patterns.rs
|
||||
//! Advanced integration patterns showing how Canvas works with:
|
||||
//! - State management patterns
|
||||
//! - Event-driven architectures
|
||||
//! - Validation systems
|
||||
//! - Custom rendering
|
||||
//!
|
||||
//! Run with: cargo run --example integration_patterns
|
||||
|
||||
use canvas::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("🔧 Canvas Integration Patterns");
|
||||
println!("==============================\n");
|
||||
|
||||
// Pattern 1: State machine integration
|
||||
state_machine_example().await;
|
||||
|
||||
// Pattern 2: Event-driven architecture
|
||||
event_driven_example().await;
|
||||
|
||||
// Pattern 3: Validation pipeline
|
||||
validation_pipeline_example().await;
|
||||
|
||||
// Pattern 4: Multi-form orchestration
|
||||
multi_form_example().await;
|
||||
}
|
||||
|
||||
// Pattern 1: Canvas with state machine
|
||||
async fn state_machine_example() {
|
||||
println!("🔄 Pattern 1: State Machine Integration");
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum FormState {
|
||||
Initial,
|
||||
Editing,
|
||||
Validating,
|
||||
Submitting,
|
||||
Success,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StateMachineForm {
|
||||
// Canvas state
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
username: String,
|
||||
password: String,
|
||||
has_changes: bool,
|
||||
|
||||
// State machine
|
||||
state: FormState,
|
||||
}
|
||||
|
||||
impl StateMachineForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
has_changes: false,
|
||||
state: FormState::Initial,
|
||||
}
|
||||
}
|
||||
|
||||
fn transition_to(&mut self, new_state: FormState) -> String {
|
||||
let old_state = self.state.clone();
|
||||
self.state = new_state;
|
||||
format!("State transition: {:?} -> {:?}", old_state, self.state)
|
||||
}
|
||||
|
||||
fn can_submit(&self) -> bool {
|
||||
matches!(self.state, FormState::Editing) &&
|
||||
!self.username.trim().is_empty() &&
|
||||
!self.password.trim().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for StateMachineForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.username,
|
||||
1 => &self.password,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.username,
|
||||
1 => &mut self.password,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.username, &self.password] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Username", "Password"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_changes = changed;
|
||||
// Transition to editing state when user starts typing
|
||||
if changed && self.state == FormState::Initial {
|
||||
self.state = FormState::Editing;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"submit" => {
|
||||
if self.can_submit() {
|
||||
let msg = self.transition_to(FormState::Submitting);
|
||||
// Simulate submission
|
||||
self.state = FormState::Success;
|
||||
Some(format!("{} -> Form submitted successfully", msg))
|
||||
} else {
|
||||
let msg = self.transition_to(FormState::Error("Invalid form data".to_string()));
|
||||
Some(msg)
|
||||
}
|
||||
}
|
||||
"reset" => {
|
||||
self.username.clear();
|
||||
self.password.clear();
|
||||
self.has_changes = false;
|
||||
Some(self.transition_to(FormState::Initial))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = StateMachineForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
println!(" Initial state: {:?}", form.state);
|
||||
|
||||
// Type some text to trigger state change
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar('u'),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" After typing: {:?}", form.state);
|
||||
|
||||
// Try to submit (should fail)
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("submit".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" Submit result: {}", result.message().unwrap_or(""));
|
||||
println!(" ✅ State machine integration works!\n");
|
||||
}
|
||||
|
||||
// Pattern 2: Event-driven architecture
|
||||
async fn event_driven_example() {
|
||||
println!("📡 Pattern 2: Event-Driven Architecture");
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum FormEvent {
|
||||
FieldChanged { field: usize, old_value: String, new_value: String },
|
||||
ValidationTriggered { field: usize, is_valid: bool },
|
||||
ActionExecuted { action: String, success: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EventDrivenForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
email: String,
|
||||
has_changes: bool,
|
||||
events: Vec<FormEvent>,
|
||||
}
|
||||
|
||||
impl EventDrivenForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
email: String::new(),
|
||||
has_changes: false,
|
||||
events: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_event(&mut self, event: FormEvent) {
|
||||
println!(" 📡 Event: {:?}", event);
|
||||
self.events.push(event);
|
||||
}
|
||||
|
||||
fn validate_email(&self) -> bool {
|
||||
self.email.contains('@') && self.email.contains('.')
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for EventDrivenForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.email }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.email }
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.email] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Email"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
if changed != self.has_changes {
|
||||
let old_value = if self.has_changes { "modified" } else { "unmodified" };
|
||||
let new_value = if changed { "modified" } else { "unmodified" };
|
||||
|
||||
self.emit_event(FormEvent::FieldChanged {
|
||||
field: self.current_field,
|
||||
old_value: old_value.to_string(),
|
||||
new_value: new_value.to_string(),
|
||||
});
|
||||
}
|
||||
self.has_changes = changed;
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"validate" => {
|
||||
let is_valid = self.validate_email();
|
||||
self.emit_event(FormEvent::ValidationTriggered {
|
||||
field: self.current_field,
|
||||
is_valid,
|
||||
});
|
||||
|
||||
self.emit_event(FormEvent::ActionExecuted {
|
||||
action: "validate".to_string(),
|
||||
success: true,
|
||||
});
|
||||
|
||||
if is_valid {
|
||||
Some("Email is valid!".to_string())
|
||||
} else {
|
||||
Some("Email is invalid".to_string())
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = EventDrivenForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Type an email address
|
||||
let email = "user@example.com";
|
||||
for c in email.chars() {
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar(c),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
}
|
||||
|
||||
// Validate the email
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
println!(" Final email: {}", form.email);
|
||||
println!(" Validation result: {}", result.message().unwrap_or(""));
|
||||
println!(" Total events captured: {}", form.events.len());
|
||||
println!(" ✅ Event-driven architecture works!\n");
|
||||
}
|
||||
|
||||
// Pattern 3: Validation pipeline
|
||||
async fn validation_pipeline_example() {
|
||||
println!("✅ Pattern 3: Validation Pipeline");
|
||||
|
||||
type ValidationRule = Box<dyn Fn(&str) -> Result<(), String>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ValidatedForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
password: String,
|
||||
has_changes: bool,
|
||||
validators: HashMap<usize, Vec<ValidationRule>>,
|
||||
}
|
||||
|
||||
impl ValidatedForm {
|
||||
fn new() -> Self {
|
||||
let mut validators: HashMap<usize, Vec<ValidationRule>> = HashMap::new();
|
||||
|
||||
// Password validators
|
||||
let mut password_validators: Vec<ValidationRule> = Vec::new();
|
||||
password_validators.push(Box::new(|value| {
|
||||
if value.len() < 8 {
|
||||
Err("Password must be at least 8 characters".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
password_validators.push(Box::new(|value| {
|
||||
if !value.chars().any(|c| c.is_uppercase()) {
|
||||
Err("Password must contain at least one uppercase letter".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
password_validators.push(Box::new(|value| {
|
||||
if !value.chars().any(|c| c.is_numeric()) {
|
||||
Err("Password must contain at least one number".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
validators.insert(0, password_validators);
|
||||
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
password: String::new(),
|
||||
has_changes: false,
|
||||
validators,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_field(&self, field_index: usize) -> Vec<String> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
if let Some(validators) = self.validators.get(&field_index) {
|
||||
let value = match field_index {
|
||||
0 => &self.password,
|
||||
_ => return errors,
|
||||
};
|
||||
|
||||
for validator in validators {
|
||||
if let Err(error) = validator(value) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for ValidatedForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.password }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.password }
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.password] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Password"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"validate" => {
|
||||
let errors = self.validate_field(self.current_field);
|
||||
if errors.is_empty() {
|
||||
Some("Password meets all requirements!".to_string())
|
||||
} else {
|
||||
Some(format!("Validation errors: {}", errors.join(", ")))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = ValidatedForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Test with weak password
|
||||
let weak_password = "abc";
|
||||
for c in weak_password.chars() {
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar(c),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
}
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" Weak password '{}': {}", form.password, result.message().unwrap_or(""));
|
||||
|
||||
// Clear and test with strong password
|
||||
form.password.clear();
|
||||
form.cursor_pos = 0;
|
||||
|
||||
let strong_password = "StrongPass123";
|
||||
for c in strong_password.chars() {
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar(c),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
}
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" Strong password '{}': {}", form.password, result.message().unwrap_or(""));
|
||||
println!(" ✅ Validation pipeline works!\n");
|
||||
}
|
||||
|
||||
// Pattern 4: Multi-form orchestration
|
||||
async fn multi_form_example() {
|
||||
println!("🎭 Pattern 4: Multi-Form Orchestration");
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PersonalInfoForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
name: String,
|
||||
age: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ContactInfoForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
email: String,
|
||||
phone: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
// Implement CanvasState for both forms
|
||||
impl CanvasState for PersonalInfoForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.name,
|
||||
1 => &self.age,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.name,
|
||||
1 => &mut self.age,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.name, &self.age] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Name", "Age"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
|
||||
impl CanvasState for ContactInfoForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.email,
|
||||
1 => &self.phone,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.email,
|
||||
1 => &mut self.phone,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.email, &self.phone] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Email", "Phone"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
|
||||
// Form orchestrator
|
||||
#[derive(Debug)]
|
||||
struct FormOrchestrator {
|
||||
personal_form: PersonalInfoForm,
|
||||
contact_form: ContactInfoForm,
|
||||
current_form: usize, // 0 = personal, 1 = contact
|
||||
}
|
||||
|
||||
impl FormOrchestrator {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
personal_form: PersonalInfoForm {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
name: String::new(),
|
||||
age: String::new(),
|
||||
has_changes: false,
|
||||
},
|
||||
contact_form: ContactInfoForm {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
email: String::new(),
|
||||
phone: String::new(),
|
||||
has_changes: false,
|
||||
},
|
||||
current_form: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_action(&mut self, action: CanvasAction) -> ActionResult {
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
match self.current_form {
|
||||
0 => ActionDispatcher::dispatch(action, &mut self.personal_form, &mut ideal_cursor).await.unwrap(),
|
||||
1 => ActionDispatcher::dispatch(action, &mut self.contact_form, &mut ideal_cursor).await.unwrap(),
|
||||
_ => ActionResult::error("Invalid form index"),
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_form(&mut self) -> String {
|
||||
self.current_form = (self.current_form + 1) % 2;
|
||||
match self.current_form {
|
||||
0 => "Switched to Personal Info form".to_string(),
|
||||
1 => "Switched to Contact Info form".to_string(),
|
||||
_ => "Unknown form".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_form_name(&self) -> &str {
|
||||
match self.current_form {
|
||||
0 => "Personal Info",
|
||||
1 => "Contact Info",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut orchestrator = FormOrchestrator::new();
|
||||
|
||||
println!(" Current form: {}", orchestrator.current_form_name());
|
||||
|
||||
// Fill personal info
|
||||
let personal_data = vec![
|
||||
('J', 'o', 'h', 'n'),
|
||||
];
|
||||
|
||||
for &c in &['J', 'o', 'h', 'n'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
orchestrator.execute_action(CanvasAction::NextField).await;
|
||||
|
||||
for &c in &['2', '5'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
println!(" Personal form - Name: '{}', Age: '{}'",
|
||||
orchestrator.personal_form.name,
|
||||
orchestrator.personal_form.age);
|
||||
|
||||
// Switch to contact form
|
||||
let switch_msg = orchestrator.switch_form();
|
||||
println!(" {}", switch_msg);
|
||||
|
||||
// Fill contact info
|
||||
for &c in &['j', 'o', 'h', 'n', '@', 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
orchestrator.execute_action(CanvasAction::NextField).await;
|
||||
|
||||
for &c in &['5', '5', '5', '-', '1', '2', '3', '4'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
println!(" Contact form - Email: '{}', Phone: '{}'",
|
||||
orchestrator.contact_form.email,
|
||||
orchestrator.contact_form.phone);
|
||||
|
||||
println!(" ✅ Multi-form orchestration works!\n");
|
||||
|
||||
println!("🎉 All integration patterns completed!");
|
||||
println!("The Canvas crate seamlessly integrates with various architectural patterns!");
|
||||
}
|
||||
354
canvas/examples/simple_login.rs
Normal file
354
canvas/examples/simple_login.rs
Normal file
@@ -0,0 +1,354 @@
|
||||
// examples/simple_login.rs
|
||||
//! A simple login form demonstrating basic canvas usage
|
||||
//!
|
||||
//! Run with: cargo run --example simple_login
|
||||
|
||||
use canvas::prelude::*;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use std::io::{self, Write};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LoginForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
username: String,
|
||||
password: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
impl LoginForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
has_changes: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.username.clear();
|
||||
self.password.clear();
|
||||
self.current_field = 0;
|
||||
self.cursor_pos = 0;
|
||||
self.has_changes = false;
|
||||
}
|
||||
|
||||
fn is_valid(&self) -> bool {
|
||||
!self.username.trim().is_empty() && !self.password.trim().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for LoginForm {
|
||||
fn current_field(&self) -> usize {
|
||||
self.current_field
|
||||
}
|
||||
|
||||
fn current_cursor_pos(&self) -> usize {
|
||||
self.cursor_pos
|
||||
}
|
||||
|
||||
fn set_current_field(&mut self, index: usize) {
|
||||
self.current_field = index.min(1); // Only 2 fields: username(0), password(1)
|
||||
}
|
||||
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||
self.cursor_pos = pos;
|
||||
}
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.username,
|
||||
1 => &self.password,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.username,
|
||||
1 => &mut self.password,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> {
|
||||
vec![&self.username, &self.password]
|
||||
}
|
||||
|
||||
fn fields(&self) -> Vec<&str> {
|
||||
vec!["Username", "Password"]
|
||||
}
|
||||
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.has_changes
|
||||
}
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_changes = changed;
|
||||
}
|
||||
|
||||
// Custom action handling
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"submit" => {
|
||||
if self.is_valid() {
|
||||
Some(format!("Login successful! Welcome, {}", self.username))
|
||||
} else {
|
||||
Some("Error: Username and password are required".to_string())
|
||||
}
|
||||
}
|
||||
"clear" => {
|
||||
self.reset();
|
||||
Some("Form cleared".to_string())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Override display for password field
|
||||
fn get_display_value_for_field(&self, index: usize) -> &str {
|
||||
match index {
|
||||
0 => &self.username, // Username shows normally
|
||||
1 => &self.password, // We'll handle masking in the UI drawing
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn has_display_override(&self, index: usize) -> bool {
|
||||
index == 1 // Password field has display override
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_ui(form: &LoginForm, message: &str) -> io::Result<()> {
|
||||
// Clear screen and move cursor to top-left
|
||||
print!("\x1B[2J\x1B[1;1H");
|
||||
|
||||
println!("╔═══════════════════════════════════════╗");
|
||||
println!("║ LOGIN FORM ║");
|
||||
println!("╠═══════════════════════════════════════╣");
|
||||
|
||||
// Username field
|
||||
let username_indicator = if form.current_field == 0 { "→" } else { " " };
|
||||
let username_display = if form.username.is_empty() {
|
||||
"<enter username>".to_string()
|
||||
} else {
|
||||
form.username.clone()
|
||||
};
|
||||
println!("║ {} Username: {:22} ║", username_indicator,
|
||||
if username_display.len() > 22 {
|
||||
format!("{}...", &username_display[..19])
|
||||
} else {
|
||||
format!("{:22}", username_display)
|
||||
});
|
||||
|
||||
// Show cursor for username field
|
||||
if form.current_field == 0 && !form.username.is_empty() {
|
||||
let cursor_pos = form.cursor_pos.min(form.username.len());
|
||||
let spaces_before = 11 + cursor_pos; // "Username: " = 10 chars + 1 space
|
||||
let cursor_line = format!("║ {}█{:width$}║",
|
||||
" ".repeat(spaces_before),
|
||||
"",
|
||||
width = 25_usize.saturating_sub(spaces_before)
|
||||
);
|
||||
println!("{}", cursor_line);
|
||||
} else {
|
||||
println!("║{:37}║", "");
|
||||
}
|
||||
|
||||
// Password field
|
||||
let password_indicator = if form.current_field == 1 { "→" } else { " " };
|
||||
let password_display = if form.password.is_empty() {
|
||||
"<enter password>".to_string()
|
||||
} else {
|
||||
"*".repeat(form.password.len())
|
||||
};
|
||||
println!("║ {} Password: {:22} ║", password_indicator,
|
||||
if password_display.len() > 22 {
|
||||
format!("{}...", &password_display[..19])
|
||||
} else {
|
||||
format!("{:22}", password_display)
|
||||
});
|
||||
|
||||
// Show cursor for password field
|
||||
if form.current_field == 1 && !form.password.is_empty() {
|
||||
let cursor_pos = form.cursor_pos.min(form.password.len());
|
||||
let spaces_before = 11 + cursor_pos; // "Password: " = 10 chars + 1 space
|
||||
let cursor_line = format!("║ {}█{:width$}║",
|
||||
" ".repeat(spaces_before),
|
||||
"",
|
||||
width = 25_usize.saturating_sub(spaces_before)
|
||||
);
|
||||
println!("{}", cursor_line);
|
||||
} else {
|
||||
println!("║{:37}║", "");
|
||||
}
|
||||
|
||||
println!("╠═══════════════════════════════════════╣");
|
||||
println!("║ CONTROLS: ║");
|
||||
println!("║ Tab/↑↓ - Navigate fields ║");
|
||||
println!("║ Enter - Submit form ║");
|
||||
println!("║ Ctrl+R - Clear form ║");
|
||||
println!("║ Ctrl+C - Exit ║");
|
||||
println!("╠═══════════════════════════════════════╣");
|
||||
|
||||
// Status message
|
||||
let status = if !message.is_empty() {
|
||||
message.to_string()
|
||||
} else if form.has_changes {
|
||||
"Form modified".to_string()
|
||||
} else {
|
||||
"Ready - enter your credentials".to_string()
|
||||
};
|
||||
|
||||
let status_display = if status.len() > 33 {
|
||||
format!("{}...", &status[..30])
|
||||
} else {
|
||||
format!("{:33}", status)
|
||||
};
|
||||
|
||||
println!("║ Status: {} ║", status_display);
|
||||
println!("╚═══════════════════════════════════════╝");
|
||||
|
||||
// Show current state info
|
||||
println!();
|
||||
println!("Current field: {} ({})",
|
||||
form.current_field,
|
||||
form.fields()[form.current_field]);
|
||||
println!("Cursor position: {}", form.cursor_pos);
|
||||
println!("Has changes: {}", form.has_changes);
|
||||
|
||||
io::stdout().flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
println!("Starting Canvas Login Demo...");
|
||||
println!("Setting up terminal...");
|
||||
|
||||
// Setup terminal
|
||||
enable_raw_mode()?;
|
||||
io::stdout().execute(EnterAlternateScreen)?;
|
||||
|
||||
let mut form = LoginForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
let mut message = String::new();
|
||||
|
||||
// Initial draw
|
||||
if let Err(e) = draw_ui(&form, &message) {
|
||||
// Cleanup on error
|
||||
let _ = disable_raw_mode();
|
||||
let _ = io::stdout().execute(LeaveAlternateScreen);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
println!("Canvas Login Demo started. Use Ctrl+C to exit.");
|
||||
|
||||
loop {
|
||||
match event::read() {
|
||||
Ok(Event::Key(key)) => {
|
||||
// Clear message after key press
|
||||
if !message.is_empty() {
|
||||
message.clear();
|
||||
}
|
||||
|
||||
match key {
|
||||
// Exit
|
||||
KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, .. } => {
|
||||
break;
|
||||
}
|
||||
|
||||
// Clear form
|
||||
KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::CONTROL, .. } => {
|
||||
match ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("clear".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await {
|
||||
Ok(result) => {
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
message = format!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Submit form
|
||||
KeyEvent { code: KeyCode::Enter, .. } => {
|
||||
match ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("submit".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await {
|
||||
Ok(result) => {
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
message = format!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Regular key handling - let canvas handle it!
|
||||
_ => {
|
||||
if let Some(action) = CanvasAction::from_key(key.code) {
|
||||
match ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await {
|
||||
Ok(result) => {
|
||||
if !result.is_success() {
|
||||
if let Some(msg) = result.message() {
|
||||
message = format!("Error: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
message = format!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redraw UI
|
||||
if let Err(e) = draw_ui(&form, &message) {
|
||||
eprintln!("Error drawing UI: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
// Ignore other events (mouse, resize, etc.)
|
||||
}
|
||||
Err(e) => {
|
||||
message = format!("Event error: {}", e);
|
||||
if let Err(_) = draw_ui(&form, &message) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
disable_raw_mode()?;
|
||||
io::stdout().execute(LeaveAlternateScreen)?;
|
||||
|
||||
println!("Thanks for using Canvas Login Demo!");
|
||||
println!("Final form state:");
|
||||
println!(" Username: '{}'", form.username);
|
||||
println!(" Password: '{}'", "*".repeat(form.password.len()));
|
||||
println!(" Valid: {}", form.is_valid());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
620
canvas/integration_patterns.rs
Normal file
620
canvas/integration_patterns.rs
Normal file
@@ -0,0 +1,620 @@
|
||||
// examples/integration_patterns.rs
|
||||
//! Advanced integration patterns showing how Canvas works with:
|
||||
//! - State management patterns
|
||||
//! - Event-driven architectures
|
||||
//! - Validation systems
|
||||
//! - Custom rendering
|
||||
//!
|
||||
//! Run with: cargo run --example integration_patterns
|
||||
|
||||
use canvas::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("🔧 Canvas Integration Patterns");
|
||||
println!("==============================\n");
|
||||
|
||||
// Pattern 1: State machine integration
|
||||
state_machine_example().await;
|
||||
|
||||
// Pattern 2: Event-driven architecture
|
||||
event_driven_example().await;
|
||||
|
||||
// Pattern 3: Validation pipeline
|
||||
validation_pipeline_example().await;
|
||||
|
||||
// Pattern 4: Multi-form orchestration
|
||||
multi_form_example().await;
|
||||
}
|
||||
|
||||
// Pattern 1: Canvas with state machine
|
||||
async fn state_machine_example() {
|
||||
println!("🔄 Pattern 1: State Machine Integration");
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum FormState {
|
||||
Initial,
|
||||
Editing,
|
||||
Validating,
|
||||
Submitting,
|
||||
Success,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StateMachineForm {
|
||||
// Canvas state
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
username: String,
|
||||
password: String,
|
||||
has_changes: bool,
|
||||
|
||||
// State machine
|
||||
state: FormState,
|
||||
}
|
||||
|
||||
impl StateMachineForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
has_changes: false,
|
||||
state: FormState::Initial,
|
||||
}
|
||||
}
|
||||
|
||||
fn transition_to(&mut self, new_state: FormState) -> String {
|
||||
let old_state = self.state.clone();
|
||||
self.state = new_state;
|
||||
format!("State transition: {:?} -> {:?}", old_state, self.state)
|
||||
}
|
||||
|
||||
fn can_submit(&self) -> bool {
|
||||
matches!(self.state, FormState::Editing) &&
|
||||
!self.username.trim().is_empty() &&
|
||||
!self.password.trim().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for StateMachineForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.username,
|
||||
1 => &self.password,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.username,
|
||||
1 => &mut self.password,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.username, &self.password] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Username", "Password"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_changes = changed;
|
||||
// Transition to editing state when user starts typing
|
||||
if changed && self.state == FormState::Initial {
|
||||
self.state = FormState::Editing;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"submit" => {
|
||||
if self.can_submit() {
|
||||
let msg = self.transition_to(FormState::Submitting);
|
||||
// Simulate submission
|
||||
self.state = FormState::Success;
|
||||
Some(format!("{} -> Form submitted successfully", msg))
|
||||
} else {
|
||||
let msg = self.transition_to(FormState::Error("Invalid form data".to_string()));
|
||||
Some(msg)
|
||||
}
|
||||
}
|
||||
"reset" => {
|
||||
self.username.clear();
|
||||
self.password.clear();
|
||||
self.has_changes = false;
|
||||
Some(self.transition_to(FormState::Initial))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = StateMachineForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
println!(" Initial state: {:?}", form.state);
|
||||
|
||||
// Type some text to trigger state change
|
||||
let _result = ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar('u'),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" After typing: {:?}", form.state);
|
||||
|
||||
// Try to submit (should fail)
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("submit".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" Submit result: {}", result.message().unwrap_or(""));
|
||||
println!(" ✅ State machine integration works!\n");
|
||||
}
|
||||
|
||||
// Pattern 2: Event-driven architecture
|
||||
async fn event_driven_example() {
|
||||
println!("📡 Pattern 2: Event-Driven Architecture");
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum FormEvent {
|
||||
FieldChanged { field: usize, old_value: String, new_value: String },
|
||||
ValidationTriggered { field: usize, is_valid: bool },
|
||||
ActionExecuted { action: String, success: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EventDrivenForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
email: String,
|
||||
has_changes: bool,
|
||||
events: Vec<FormEvent>,
|
||||
}
|
||||
|
||||
impl EventDrivenForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
email: String::new(),
|
||||
has_changes: false,
|
||||
events: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_event(&mut self, event: FormEvent) {
|
||||
println!(" 📡 Event: {:?}", event);
|
||||
self.events.push(event);
|
||||
}
|
||||
|
||||
fn validate_email(&self) -> bool {
|
||||
self.email.contains('@') && self.email.contains('.')
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for EventDrivenForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.email }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.email }
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.email] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Email"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
if changed != self.has_changes {
|
||||
let old_value = if self.has_changes { "modified" } else { "unmodified" };
|
||||
let new_value = if changed { "modified" } else { "unmodified" };
|
||||
|
||||
self.emit_event(FormEvent::FieldChanged {
|
||||
field: self.current_field,
|
||||
old_value: old_value.to_string(),
|
||||
new_value: new_value.to_string(),
|
||||
});
|
||||
}
|
||||
self.has_changes = changed;
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"validate" => {
|
||||
let is_valid = self.validate_email();
|
||||
self.emit_event(FormEvent::ValidationTriggered {
|
||||
field: self.current_field,
|
||||
is_valid,
|
||||
});
|
||||
|
||||
self.emit_event(FormEvent::ActionExecuted {
|
||||
action: "validate".to_string(),
|
||||
success: true,
|
||||
});
|
||||
|
||||
if is_valid {
|
||||
Some("Email is valid!".to_string())
|
||||
} else {
|
||||
Some("Email is invalid".to_string())
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = EventDrivenForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Type an email address
|
||||
let email = "user@example.com";
|
||||
for c in email.chars() {
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar(c),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
}
|
||||
|
||||
// Validate the email
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
println!(" Final email: {}", form.email);
|
||||
println!(" Validation result: {}", result.message().unwrap_or(""));
|
||||
println!(" Total events captured: {}", form.events.len());
|
||||
println!(" ✅ Event-driven architecture works!\n");
|
||||
}
|
||||
|
||||
// Pattern 3: Validation pipeline
|
||||
async fn validation_pipeline_example() {
|
||||
println!("✅ Pattern 3: Validation Pipeline");
|
||||
|
||||
type ValidationRule = Box<dyn Fn(&str) -> Result<(), String>>;
|
||||
|
||||
// Custom Debug implementation since function pointers don't implement Debug
|
||||
struct ValidatedForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
password: String,
|
||||
has_changes: bool,
|
||||
validators: HashMap<usize, Vec<ValidationRule>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ValidatedForm {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ValidatedForm")
|
||||
.field("current_field", &self.current_field)
|
||||
.field("cursor_pos", &self.cursor_pos)
|
||||
.field("password", &self.password)
|
||||
.field("has_changes", &self.has_changes)
|
||||
.field("validators", &format!("HashMap with {} entries", self.validators.len()))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidatedForm {
|
||||
fn new() -> Self {
|
||||
let mut validators: HashMap<usize, Vec<ValidationRule>> = HashMap::new();
|
||||
|
||||
// Password validators
|
||||
let mut password_validators: Vec<ValidationRule> = Vec::new();
|
||||
password_validators.push(Box::new(|value| {
|
||||
if value.len() < 8 {
|
||||
Err("Password must be at least 8 characters".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
password_validators.push(Box::new(|value| {
|
||||
if !value.chars().any(|c| c.is_uppercase()) {
|
||||
Err("Password must contain at least one uppercase letter".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
password_validators.push(Box::new(|value| {
|
||||
if !value.chars().any(|c| c.is_numeric()) {
|
||||
Err("Password must contain at least one number".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
validators.insert(0, password_validators);
|
||||
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
password: String::new(),
|
||||
has_changes: false,
|
||||
validators,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_field(&self, field_index: usize) -> Vec<String> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
if let Some(validators) = self.validators.get(&field_index) {
|
||||
let value = match field_index {
|
||||
0 => &self.password,
|
||||
_ => return errors,
|
||||
};
|
||||
|
||||
for validator in validators {
|
||||
if let Err(error) = validator(value) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for ValidatedForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.password }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.password }
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.password] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Password"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"validate" => {
|
||||
let errors = self.validate_field(self.current_field);
|
||||
if errors.is_empty() {
|
||||
Some("Password meets all requirements!".to_string())
|
||||
} else {
|
||||
Some(format!("Validation errors: {}", errors.join(", ")))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = ValidatedForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Test with weak password
|
||||
let weak_password = "abc";
|
||||
for c in weak_password.chars() {
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar(c),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
}
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" Weak password '{}': {}", form.password, result.message().unwrap_or(""));
|
||||
|
||||
// Clear and test with strong password
|
||||
form.password.clear();
|
||||
form.cursor_pos = 0;
|
||||
|
||||
let strong_password = "StrongPass123";
|
||||
for c in strong_password.chars() {
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar(c),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
}
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" Strong password '{}': {}", form.password, result.message().unwrap_or(""));
|
||||
println!(" ✅ Validation pipeline works!\n");
|
||||
}
|
||||
|
||||
// Pattern 4: Multi-form orchestration
|
||||
async fn multi_form_example() {
|
||||
println!("🎭 Pattern 4: Multi-Form Orchestration");
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PersonalInfoForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
name: String,
|
||||
age: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ContactInfoForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
email: String,
|
||||
phone: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
// Implement CanvasState for both forms
|
||||
impl CanvasState for PersonalInfoForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.name,
|
||||
1 => &self.age,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.name,
|
||||
1 => &mut self.age,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.name, &self.age] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Name", "Age"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
|
||||
impl CanvasState for ContactInfoForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.email,
|
||||
1 => &self.phone,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.email,
|
||||
1 => &mut self.phone,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.email, &self.phone] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Email", "Phone"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
|
||||
// Form orchestrator
|
||||
#[derive(Debug)]
|
||||
struct FormOrchestrator {
|
||||
personal_form: PersonalInfoForm,
|
||||
contact_form: ContactInfoForm,
|
||||
current_form: usize, // 0 = personal, 1 = contact
|
||||
}
|
||||
|
||||
impl FormOrchestrator {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
personal_form: PersonalInfoForm {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
name: String::new(),
|
||||
age: String::new(),
|
||||
has_changes: false,
|
||||
},
|
||||
contact_form: ContactInfoForm {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
email: String::new(),
|
||||
phone: String::new(),
|
||||
has_changes: false,
|
||||
},
|
||||
current_form: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_action(&mut self, action: CanvasAction) -> ActionResult {
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
match self.current_form {
|
||||
0 => ActionDispatcher::dispatch(action, &mut self.personal_form, &mut ideal_cursor).await.unwrap(),
|
||||
1 => ActionDispatcher::dispatch(action, &mut self.contact_form, &mut ideal_cursor).await.unwrap(),
|
||||
_ => ActionResult::error("Invalid form index"),
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_form(&mut self) -> String {
|
||||
self.current_form = (self.current_form + 1) % 2;
|
||||
match self.current_form {
|
||||
0 => "Switched to Personal Info form".to_string(),
|
||||
1 => "Switched to Contact Info form".to_string(),
|
||||
_ => "Unknown form".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_form_name(&self) -> &str {
|
||||
match self.current_form {
|
||||
0 => "Personal Info",
|
||||
1 => "Contact Info",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut orchestrator = FormOrchestrator::new();
|
||||
|
||||
println!(" Current form: {}", orchestrator.current_form_name());
|
||||
|
||||
// Fill personal info
|
||||
for &c in &['J', 'o', 'h', 'n'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
orchestrator.execute_action(CanvasAction::NextField).await;
|
||||
|
||||
for &c in &['2', '5'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
println!(" Personal form - Name: '{}', Age: '{}'",
|
||||
orchestrator.personal_form.name,
|
||||
orchestrator.personal_form.age);
|
||||
|
||||
// Switch to contact form
|
||||
let switch_msg = orchestrator.switch_form();
|
||||
println!(" {}", switch_msg);
|
||||
|
||||
// Fill contact info
|
||||
for &c in &['j', 'o', 'h', 'n', '@', 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
orchestrator.execute_action(CanvasAction::NextField).await;
|
||||
|
||||
for &c in &['5', '5', '5', '-', '1', '2', '3', '4'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
println!(" Contact form - Email: '{}', Phone: '{}'",
|
||||
orchestrator.contact_form.email,
|
||||
orchestrator.contact_form.phone);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// src/modes/handlers/mode_manager.rs
|
||||
// canvas/src/modes/manager.rs
|
||||
|
||||
use crate::modes::highlight::HighlightState;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AppMode {
|
||||
|
||||
Reference in New Issue
Block a user