Files
komp_ac/canvas/examples/simple_login.rs
2025-07-29 12:47:43 +02:00

355 lines
12 KiB
Rust

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