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