working, cleaning trash via cargo fix
This commit is contained in:
@@ -24,19 +24,3 @@ tokio-test = "0.4.4"
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
gui = ["ratatui"]
|
gui = ["ratatui"]
|
||||||
|
|
||||||
[[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"
|
|
||||||
|
|||||||
@@ -100,35 +100,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📚 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
|
## 🎯 Type-Safe Actions
|
||||||
|
|
||||||
The Canvas system uses strongly-typed actions instead of error-prone strings:
|
The Canvas system uses strongly-typed actions instead of error-prone strings:
|
||||||
|
|||||||
@@ -1,378 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
@@ -1,590 +0,0 @@
|
|||||||
// examples/config_screen.rs
|
|
||||||
//! Advanced configuration screen with suggestions and validation
|
|
||||||
//!
|
|
||||||
//! This example demonstrates:
|
|
||||||
//! - Multiple field types
|
|
||||||
//! - Auto-suggestions
|
|
||||||
//! - Field validation
|
|
||||||
//! - Custom actions
|
|
||||||
//!
|
|
||||||
//! Run with: cargo run --example config_screen
|
|
||||||
|
|
||||||
use canvas::prelude::*;
|
|
||||||
use crossterm::{
|
|
||||||
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
};
|
|
||||||
use std::io::{self, Write};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ConfigForm {
|
|
||||||
current_field: usize,
|
|
||||||
cursor_pos: usize,
|
|
||||||
|
|
||||||
// Configuration fields
|
|
||||||
server_host: String,
|
|
||||||
server_port: String,
|
|
||||||
database_url: String,
|
|
||||||
log_level: String,
|
|
||||||
max_connections: String,
|
|
||||||
|
|
||||||
has_changes: bool,
|
|
||||||
suggestions: SuggestionState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigForm {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
current_field: 0,
|
|
||||||
cursor_pos: 0,
|
|
||||||
server_host: "localhost".to_string(),
|
|
||||||
server_port: "8080".to_string(),
|
|
||||||
database_url: String::new(),
|
|
||||||
log_level: "info".to_string(),
|
|
||||||
max_connections: "100".to_string(),
|
|
||||||
has_changes: false,
|
|
||||||
suggestions: SuggestionState::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn field_names() -> Vec<&'static str> {
|
|
||||||
vec![
|
|
||||||
"Server Host",
|
|
||||||
"Server Port",
|
|
||||||
"Database URL",
|
|
||||||
"Log Level",
|
|
||||||
"Max Connections"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_field_value(&self, index: usize) -> &String {
|
|
||||||
match index {
|
|
||||||
0 => &self.server_host,
|
|
||||||
1 => &self.server_port,
|
|
||||||
2 => &self.database_url,
|
|
||||||
3 => &self.log_level,
|
|
||||||
4 => &self.max_connections,
|
|
||||||
_ => panic!("Invalid field index: {}", index),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_field_value_mut(&mut self, index: usize) -> &mut String {
|
|
||||||
match index {
|
|
||||||
0 => &mut self.server_host,
|
|
||||||
1 => &mut self.server_port,
|
|
||||||
2 => &mut self.database_url,
|
|
||||||
3 => &mut self.log_level,
|
|
||||||
4 => &mut self.max_connections,
|
|
||||||
_ => panic!("Invalid field index: {}", index),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_field(&self, index: usize) -> Option<String> {
|
|
||||||
let value = self.get_field_value(index);
|
|
||||||
match index {
|
|
||||||
0 => { // Server Host
|
|
||||||
if value.trim().is_empty() {
|
|
||||||
Some("Server host cannot be empty".to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1 => { // Server Port
|
|
||||||
if let Ok(port) = value.parse::<u16>() {
|
|
||||||
if port == 0 {
|
|
||||||
Some("Port must be greater than 0".to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Some("Port must be a valid number (1-65535)".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2 => { // Database URL
|
|
||||||
if !value.is_empty() && !value.starts_with("postgresql://") && !value.starts_with("mysql://") && !value.starts_with("sqlite://") {
|
|
||||||
Some("Database URL should start with postgresql://, mysql://, or sqlite://".to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3 => { // Log Level
|
|
||||||
let valid_levels = ["trace", "debug", "info", "warn", "error"];
|
|
||||||
if !valid_levels.contains(&value.to_lowercase().as_str()) {
|
|
||||||
Some("Log level must be one of: trace, debug, info, warn, error".to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4 => { // Max Connections
|
|
||||||
if let Ok(connections) = value.parse::<u32>() {
|
|
||||||
if connections == 0 {
|
|
||||||
Some("Max connections must be greater than 0".to_string())
|
|
||||||
} else if connections > 10000 {
|
|
||||||
Some("Max connections seems too high (>10000)".to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Some("Max connections must be a valid number".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_suggestions_for_field(&self, index: usize, current_value: &str) -> Vec<String> {
|
|
||||||
match index {
|
|
||||||
0 => { // Server Host
|
|
||||||
vec![
|
|
||||||
"localhost".to_string(),
|
|
||||||
"127.0.0.1".to_string(),
|
|
||||||
"0.0.0.0".to_string(),
|
|
||||||
format!("{}.local", current_value),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
1 => { // Server Port
|
|
||||||
vec![
|
|
||||||
"8080".to_string(),
|
|
||||||
"3000".to_string(),
|
|
||||||
"8000".to_string(),
|
|
||||||
"80".to_string(),
|
|
||||||
"443".to_string(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
2 => { // Database URL
|
|
||||||
if current_value.is_empty() {
|
|
||||||
vec![
|
|
||||||
"postgresql://localhost:5432/mydb".to_string(),
|
|
||||||
"mysql://localhost:3306/mydb".to_string(),
|
|
||||||
"sqlite://./database.db".to_string(),
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3 => { // Log Level
|
|
||||||
vec![
|
|
||||||
"trace".to_string(),
|
|
||||||
"debug".to_string(),
|
|
||||||
"info".to_string(),
|
|
||||||
"warn".to_string(),
|
|
||||||
"error".to_string(),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.filter(|level| level.starts_with(¤t_value.to_lowercase()))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
4 => { // Max Connections
|
|
||||||
vec![
|
|
||||||
"10".to_string(),
|
|
||||||
"50".to_string(),
|
|
||||||
"100".to_string(),
|
|
||||||
"200".to_string(),
|
|
||||||
"500".to_string(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
_ => vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CanvasState for ConfigForm {
|
|
||||||
fn current_field(&self) -> usize {
|
|
||||||
self.current_field
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_cursor_pos(&self) -> usize {
|
|
||||||
self.cursor_pos
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_current_field(&mut self, index: usize) {
|
|
||||||
self.current_field = index.min(4); // 5 fields total (0-4)
|
|
||||||
// Deactivate suggestions when changing fields
|
|
||||||
self.deactivate_suggestions();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
|
||||||
self.cursor_pos = pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_current_input(&self) -> &str {
|
|
||||||
self.get_field_value(self.current_field)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_current_input_mut(&mut self) -> &mut String {
|
|
||||||
self.get_field_value_mut(self.current_field)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inputs(&self) -> Vec<&String> {
|
|
||||||
vec![
|
|
||||||
&self.server_host,
|
|
||||||
&self.server_port,
|
|
||||||
&self.database_url,
|
|
||||||
&self.log_level,
|
|
||||||
&self.max_connections,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fields(&self) -> Vec<&str> {
|
|
||||||
Self::field_names()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_unsaved_changes(&self) -> bool {
|
|
||||||
self.has_changes
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
|
||||||
self.has_changes = changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suggestion support
|
|
||||||
fn get_suggestions(&self) -> Option<&[String]> {
|
|
||||||
if self.suggestions.is_active {
|
|
||||||
Some(&self.suggestions.suggestions)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
|
||||||
self.suggestions.selected_index
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_selected_suggestion_index(&mut self, index: Option<usize>) {
|
|
||||||
self.suggestions.selected_index = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn activate_suggestions(&mut self, suggestions: Vec<String>) {
|
|
||||||
self.suggestions.activate_with_suggestions(suggestions);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deactivate_suggestions(&mut self) {
|
|
||||||
self.suggestions.deactivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
|
||||||
match action {
|
|
||||||
CanvasAction::SelectSuggestion => {
|
|
||||||
// Fix: Clone the suggestion first to avoid borrow checker issues
|
|
||||||
if let Some(suggestion) = self.suggestions.get_selected().cloned() {
|
|
||||||
*self.get_current_input_mut() = suggestion.clone();
|
|
||||||
self.set_current_cursor_pos(suggestion.len());
|
|
||||||
self.deactivate_suggestions();
|
|
||||||
self.set_has_unsaved_changes(true);
|
|
||||||
return Some("Applied suggestion".to_string());
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
|
||||||
"trigger_suggestions" => {
|
|
||||||
let current_value = self.get_current_input();
|
|
||||||
let suggestions = self.get_suggestions_for_field(self.current_field, current_value);
|
|
||||||
if !suggestions.is_empty() {
|
|
||||||
self.activate_suggestions(suggestions);
|
|
||||||
Some("Showing suggestions".to_string())
|
|
||||||
} else {
|
|
||||||
Some("No suggestions available".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"validate_current" => {
|
|
||||||
if let Some(error) = self.validate_field(self.current_field) {
|
|
||||||
Some(format!("Validation Error: {}", error))
|
|
||||||
} else {
|
|
||||||
Some("Field is valid".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"validate_all" => {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
for i in 0..5 {
|
|
||||||
if let Some(error) = self.validate_field(i) {
|
|
||||||
errors.push(format!("{}: {}", Self::field_names()[i], error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if errors.is_empty() {
|
|
||||||
Some("All fields are valid!".to_string())
|
|
||||||
} else {
|
|
||||||
Some(format!("Errors found: {}", errors.join("; ")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"save_config" => {
|
|
||||||
// Validate all fields first
|
|
||||||
for i in 0..5 {
|
|
||||||
if self.validate_field(i).is_some() {
|
|
||||||
return Some("Cannot save: Please fix validation errors first".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.set_has_unsaved_changes(false);
|
|
||||||
Some("Configuration saved successfully!".to_string())
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Auto-trigger suggestions for certain fields
|
|
||||||
CanvasAction::InsertChar(_) => {
|
|
||||||
// After character insertion, check if we should show suggestions
|
|
||||||
match self.current_field {
|
|
||||||
3 => { // Log level - always show suggestions for autocomplete
|
|
||||||
let current_value = self.get_current_input();
|
|
||||||
let suggestions = self.get_suggestions_for_field(self.current_field, current_value);
|
|
||||||
if !suggestions.is_empty() {
|
|
||||||
self.activate_suggestions(suggestions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
None // Let the generic handler insert the character
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_ui(form: &ConfigForm, message: &str) -> io::Result<()> {
|
|
||||||
print!("\x1B[2J\x1B[1;1H");
|
|
||||||
|
|
||||||
println!("╔════════════════════════════════════════════════════════════════╗");
|
|
||||||
println!("║ CONFIGURATION EDITOR ║");
|
|
||||||
println!("╠════════════════════════════════════════════════════════════════╣");
|
|
||||||
|
|
||||||
let field_names = ConfigForm::field_names();
|
|
||||||
|
|
||||||
for (i, field_name) in field_names.iter().enumerate() {
|
|
||||||
let is_current = i == form.current_field;
|
|
||||||
let indicator = if is_current { ">" } else { " " };
|
|
||||||
let value = form.get_field_value(i);
|
|
||||||
let display_value = if value.is_empty() {
|
|
||||||
format!("<enter {}>", field_name.to_lowercase())
|
|
||||||
} else {
|
|
||||||
value.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Truncate long values for display
|
|
||||||
let display_value = if display_value.len() > 35 {
|
|
||||||
format!("{}...", &display_value[..32])
|
|
||||||
} else {
|
|
||||||
display_value
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("║ {} {:15}: {:35} ║", indicator, field_name, display_value);
|
|
||||||
|
|
||||||
// Show cursor for current field
|
|
||||||
if is_current {
|
|
||||||
let cursor_pos = form.cursor_pos.min(value.len());
|
|
||||||
let cursor_line = format!("║ {}{}║",
|
|
||||||
" ".repeat(18 + cursor_pos),
|
|
||||||
"▊"
|
|
||||||
);
|
|
||||||
println!("{:66}", cursor_line);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show validation error if any
|
|
||||||
if let Some(error) = form.validate_field(i) {
|
|
||||||
let error_display = if error.len() > 58 {
|
|
||||||
format!("{}...", &error[..55])
|
|
||||||
} else {
|
|
||||||
error
|
|
||||||
};
|
|
||||||
println!("║ ⚠️ {:58} ║", error_display);
|
|
||||||
} else if is_current {
|
|
||||||
println!("║{:64}║", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("╠════════════════════════════════════════════════════════════════╣");
|
|
||||||
|
|
||||||
// Show suggestions if active
|
|
||||||
if let Some(suggestions) = form.get_suggestions() {
|
|
||||||
println!("║ SUGGESTIONS: ║");
|
|
||||||
for (i, suggestion) in suggestions.iter().enumerate() {
|
|
||||||
let selected = form.get_selected_suggestion_index() == Some(i);
|
|
||||||
let marker = if selected { "→" } else { " " };
|
|
||||||
let display_suggestion = if suggestion.len() > 55 {
|
|
||||||
format!("{}...", &suggestion[..52])
|
|
||||||
} else {
|
|
||||||
suggestion.clone()
|
|
||||||
};
|
|
||||||
println!("║ {} {:58} ║", marker, display_suggestion);
|
|
||||||
}
|
|
||||||
println!("╠════════════════════════════════════════════════════════════════╣");
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("║ CONTROLS: ║");
|
|
||||||
println!("║ Tab/↑↓ - Navigate fields ║");
|
|
||||||
println!("║ Ctrl+Space - Show suggestions ║");
|
|
||||||
println!("║ ↑↓ - Navigate suggestions (when shown) ║");
|
|
||||||
println!("║ Enter - Select suggestion / Validate field ║");
|
|
||||||
println!("║ Ctrl+S - Save configuration ║");
|
|
||||||
println!("║ Ctrl+V - Validate all fields ║");
|
|
||||||
println!("║ Ctrl+C - Exit ║");
|
|
||||||
println!("╠════════════════════════════════════════════════════════════════╣");
|
|
||||||
|
|
||||||
// Status
|
|
||||||
let status = if !message.is_empty() {
|
|
||||||
message.to_string()
|
|
||||||
} else if form.has_changes {
|
|
||||||
"Configuration modified - press Ctrl+S to save".to_string()
|
|
||||||
} else {
|
|
||||||
"Ready".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let status_display = if status.len() > 58 {
|
|
||||||
format!("{}...", &status[..55])
|
|
||||||
} else {
|
|
||||||
status
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("║ Status: {:55} ║", status_display);
|
|
||||||
println!("╚════════════════════════════════════════════════════════════════╝");
|
|
||||||
|
|
||||||
io::stdout().flush()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> io::Result<()> {
|
|
||||||
enable_raw_mode()?;
|
|
||||||
io::stdout().execute(EnterAlternateScreen)?;
|
|
||||||
|
|
||||||
let mut form = ConfigForm::new();
|
|
||||||
let mut ideal_cursor = 0;
|
|
||||||
let mut message = String::new();
|
|
||||||
|
|
||||||
draw_ui(&form, &message)?;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if let Event::Key(key) = event::read()? {
|
|
||||||
if !message.is_empty() {
|
|
||||||
message.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
match key {
|
|
||||||
// Exit
|
|
||||||
KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, .. } => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show suggestions
|
|
||||||
KeyEvent { code: KeyCode::Char(' '), modifiers: KeyModifiers::CONTROL, .. } => {
|
|
||||||
let result = ActionDispatcher::dispatch(
|
|
||||||
CanvasAction::Custom("trigger_suggestions".to_string()),
|
|
||||||
&mut form,
|
|
||||||
&mut ideal_cursor,
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
if let Some(msg) = result.message() {
|
|
||||||
message = msg.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate current field or select suggestion
|
|
||||||
KeyEvent { code: KeyCode::Enter, .. } => {
|
|
||||||
if form.get_suggestions().is_some() {
|
|
||||||
// Select suggestion
|
|
||||||
let result = ActionDispatcher::dispatch(
|
|
||||||
CanvasAction::SelectSuggestion,
|
|
||||||
&mut form,
|
|
||||||
&mut ideal_cursor,
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
if let Some(msg) = result.message() {
|
|
||||||
message = msg.to_string();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Validate current field
|
|
||||||
let result = ActionDispatcher::dispatch(
|
|
||||||
CanvasAction::Custom("validate_current".to_string()),
|
|
||||||
&mut form,
|
|
||||||
&mut ideal_cursor,
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
if let Some(msg) = result.message() {
|
|
||||||
message = msg.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save configuration
|
|
||||||
KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::CONTROL, .. } => {
|
|
||||||
let result = ActionDispatcher::dispatch(
|
|
||||||
CanvasAction::Custom("save_config".to_string()),
|
|
||||||
&mut form,
|
|
||||||
&mut ideal_cursor,
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
if let Some(msg) = result.message() {
|
|
||||||
message = msg.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all fields
|
|
||||||
KeyEvent { code: KeyCode::Char('v'), modifiers: KeyModifiers::CONTROL, .. } => {
|
|
||||||
let result = ActionDispatcher::dispatch(
|
|
||||||
CanvasAction::Custom("validate_all".to_string()),
|
|
||||||
&mut form,
|
|
||||||
&mut ideal_cursor,
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
if let Some(msg) = result.message() {
|
|
||||||
message = msg.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle up/down for suggestions
|
|
||||||
KeyEvent { code: KeyCode::Up, .. } => {
|
|
||||||
let action = if form.get_suggestions().is_some() {
|
|
||||||
CanvasAction::SuggestionUp
|
|
||||||
} else {
|
|
||||||
CanvasAction::MoveUp
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyEvent { code: KeyCode::Down, .. } => {
|
|
||||||
let action = if form.get_suggestions().is_some() {
|
|
||||||
CanvasAction::SuggestionDown
|
|
||||||
} else {
|
|
||||||
CanvasAction::MoveDown
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle escape to close suggestions
|
|
||||||
KeyEvent { code: KeyCode::Esc, .. } => {
|
|
||||||
if form.get_suggestions().is_some() {
|
|
||||||
let _ = ActionDispatcher::dispatch(
|
|
||||||
CanvasAction::ExitSuggestions,
|
|
||||||
&mut form,
|
|
||||||
&mut ideal_cursor,
|
|
||||||
).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regular key handling
|
|
||||||
_ => {
|
|
||||||
if let Some(action) = CanvasAction::from_key(key.code) {
|
|
||||||
let result = ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await.unwrap();
|
|
||||||
|
|
||||||
if !result.is_success() {
|
|
||||||
if let Some(msg) = result.message() {
|
|
||||||
message = format!("Error: {}", msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
draw_ui(&form, &message)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disable_raw_mode()?;
|
|
||||||
io::stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
println!("Configuration editor closed!");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,617 +0,0 @@
|
|||||||
// 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!");
|
|
||||||
}
|
|
||||||
@@ -1,354 +0,0 @@
|
|||||||
// 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(())
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,7 @@ move_word_end_prev = ["ge"]
|
|||||||
move_line_start = ["0"]
|
move_line_start = ["0"]
|
||||||
move_line_end = ["$"]
|
move_line_end = ["$"]
|
||||||
move_first_line = ["gg"]
|
move_first_line = ["gg"]
|
||||||
move_last_line = ["CapsLock"]
|
move_last_line = ["shift+g"]
|
||||||
next_field = ["Tab"]
|
next_field = ["Tab"]
|
||||||
prev_field = ["Shift+Tab"]
|
prev_field = ["Shift+Tab"]
|
||||||
|
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ pub fn render_search_palette(
|
|||||||
.style(Style::default().fg(theme.fg));
|
.style(Style::default().fg(theme.fg));
|
||||||
f.render_widget(input_text, inner_chunks[0]);
|
f.render_widget(input_text, inner_chunks[0]);
|
||||||
// Set cursor position
|
// Set cursor position
|
||||||
f.set_cursor(
|
f.set_cursor_position((
|
||||||
inner_chunks[0].x + state.cursor_position as u16 + 1,
|
inner_chunks[0].x + state.cursor_position as u16 + 1,
|
||||||
inner_chunks[0].y + 1,
|
inner_chunks[0].y + 1,
|
||||||
);
|
));
|
||||||
|
|
||||||
// --- Render Results List ---
|
// --- Render Results List ---
|
||||||
if state.is_loading {
|
if state.is_loading {
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ use ratatui::{
|
|||||||
layout::Rect,
|
layout::Rect,
|
||||||
style::Style,
|
style::Style,
|
||||||
text::{Line, Span, Text},
|
text::{Line, Span, Text},
|
||||||
widgets::{Paragraph, Wrap}, // Make sure Wrap is imported
|
widgets::Paragraph,
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
use ratatui::widgets::Wrap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use anyhow::Result;
|
|||||||
use common::proto::komp_ac::search::search_response::Hit;
|
use common::proto::komp_ac::search::search_response::Hit;
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{debug, info};
|
use tracing::info;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum EditEventOutcome {
|
pub enum EditEventOutcome {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use crate::modes::{
|
|||||||
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||||
use crate::services::auth::AuthClient;
|
use crate::services::auth::AuthClient;
|
||||||
use crate::services::grpc_client::GrpcClient;
|
use crate::services::grpc_client::GrpcClient;
|
||||||
use canvas::{CanvasAction, ActionDispatcher, ActionResult};
|
use canvas::{CanvasAction, ActionDispatcher};
|
||||||
use canvas::CanvasState as LibraryCanvasState;
|
use canvas::CanvasState as LibraryCanvasState;
|
||||||
use super::event_helper::*;
|
use super::event_helper::*;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use crate::components::{
|
|||||||
common::dialog::render_dialog,
|
common::dialog::render_dialog,
|
||||||
common::find_file_palette,
|
common::find_file_palette,
|
||||||
common::search_palette::render_search_palette,
|
common::search_palette::render_search_palette,
|
||||||
form::form::render_form,
|
|
||||||
handlers::sidebar::{self, calculate_sidebar_layout},
|
handlers::sidebar::{self, calculate_sidebar_layout},
|
||||||
intro::intro::render_intro,
|
intro::intro::render_intro,
|
||||||
render_background,
|
render_background,
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ use crate::ui::handlers::context::DialogPurpose;
|
|||||||
use crate::tui::functions::common::login;
|
use crate::tui::functions::common::login;
|
||||||
use crate::tui::functions::common::register;
|
use crate::tui::functions::common::register;
|
||||||
use crate::utils::columns::filter_user_columns;
|
use crate::utils::columns::filter_user_columns;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use crossterm::cursor::SetCursorStyle;
|
use crossterm::cursor::SetCursorStyle;
|
||||||
use crossterm::event as crossterm_event;
|
use crossterm::event as crossterm_event;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Instant, Duration};
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
use crate::state::app::state::DebugState;
|
use crate::state::app::state::DebugState;
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
use rstest::{fixture, rstest};
|
use rstest::{fixture, rstest};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use client::state::pages::form::{FormState, FieldDefinition};
|
use client::state::pages::form::{FormState, FieldDefinition};
|
||||||
|
use canvas::state::CanvasState
|
||||||
use client::state::pages::canvas_state::CanvasState;
|
use client::state::pages::canvas_state::CanvasState;
|
||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
|
|||||||
Reference in New Issue
Block a user