diff --git a/Cargo.lock b/Cargo.lock index 43699fd..75b44da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -557,6 +557,7 @@ version = "0.4.1" dependencies = [ "anyhow", "async-trait", + "canvas", "common", "crossterm", "dirs", diff --git a/canvas/Cargo.toml b/canvas/Cargo.toml index eda8ed0..b020283 100644 --- a/canvas/Cargo.toml +++ b/canvas/Cargo.toml @@ -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" diff --git a/canvas/README.md b/canvas/README.md new file mode 100644 index 0000000..0b532c9 --- /dev/null +++ b/canvas/README.md @@ -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> { + 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 { + 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 { + 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 diff --git a/canvas/examples/basic_usage.rs b/canvas/examples/basic_usage.rs new file mode 100644 index 0000000..b2604f5 --- /dev/null +++ b/canvas/examples/basic_usage.rs @@ -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 { + self.suggestions.selected_index + } + + fn set_selected_suggestion_index(&mut self, index: Option) { + self.suggestions.selected_index = index; + } + + fn activate_suggestions(&mut self, suggestions: Vec) { + 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 { + 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 { + 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); +} diff --git a/canvas/examples/config_screen.rs b/canvas/examples/config_screen.rs new file mode 100644 index 0000000..df808d2 --- /dev/null +++ b/canvas/examples/config_screen.rs @@ -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 { + 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::() { + 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::() { + 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 { + 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 { + self.suggestions.selected_index + } + + fn set_selected_suggestion_index(&mut self, index: Option) { + self.suggestions.selected_index = index; + } + + fn activate_suggestions(&mut self, suggestions: Vec) { + 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 { + 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!("", 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(()) +} diff --git a/canvas/examples/integration_patterns.rs b/canvas/examples/integration_patterns.rs new file mode 100644 index 0000000..95da154 --- /dev/null +++ b/canvas/examples/integration_patterns.rs @@ -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 { + 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, + } + + 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 { + 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 Result<(), String>>; + + #[derive(Debug)] + struct ValidatedForm { + current_field: usize, + cursor_pos: usize, + password: String, + has_changes: bool, + validators: HashMap>, + } + + impl ValidatedForm { + fn new() -> Self { + let mut validators: HashMap> = HashMap::new(); + + // Password validators + let mut password_validators: Vec = 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 { + 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 { + 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!"); +} diff --git a/canvas/examples/simple_login.rs b/canvas/examples/simple_login.rs new file mode 100644 index 0000000..caaadb8 --- /dev/null +++ b/canvas/examples/simple_login.rs @@ -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 { + 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() { + "".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() { + "".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(()) +} diff --git a/canvas/integration_patterns.rs b/canvas/integration_patterns.rs new file mode 100644 index 0000000..015607d --- /dev/null +++ b/canvas/integration_patterns.rs @@ -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 { + 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, + } + + 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 { + 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 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>, + } + + 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> = HashMap::new(); + + // Password validators + let mut password_validators: Vec = 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 { + 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 { + 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); +} diff --git a/canvas/src/modes/manager.rs b/canvas/src/modes/manager.rs index 5c8a15e..dcdf7b9 100644 --- a/canvas/src/modes/manager.rs +++ b/canvas/src/modes/manager.rs @@ -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 {