canvas ready, lets implement now client

This commit is contained in:
Priec
2025-07-29 12:47:43 +02:00
parent 2a7f94cf17
commit f24156775a
9 changed files with 2942 additions and 1 deletions

1
Cargo.lock generated
View File

@@ -557,6 +557,7 @@ version = "0.4.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"canvas",
"common", "common",
"crossterm", "crossterm",
"dirs", "dirs",

View File

@@ -18,3 +18,19 @@ tokio = { workspace = true }
[dev-dependencies] [dev-dependencies]
tokio-test = "0.4.4" 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
View 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

View 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);
}

View 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(&current_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(())
}

View 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!");
}

View 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(())
}

View 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);
}

View File

@@ -1,7 +1,6 @@
// src/modes/handlers/mode_manager.rs // src/modes/handlers/mode_manager.rs
// canvas/src/modes/manager.rs // canvas/src/modes/manager.rs
use crate::modes::highlight::HighlightState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppMode { pub enum AppMode {