// examples/validation_1.rs //! Demonstrates field validation with the canvas library //! //! This example REQUIRES the `validation` and `cursor-style` features to compile. //! //! Run with: //! cargo run --example validation_1 --features "gui,validation" //! //! This will fail without validation: //! cargo run --example validation_1 --features "gui" // REQUIRE validation feature - example won't compile without it #[cfg(not(all(feature = "validation", feature = "cursor-style")))] compile_error!( "This example requires the 'validation' and 'cursor-style' features. \ Run with: cargo run --example validation_1 --features \"gui,validation,cursor-style\"" ); use std::io; use crossterm::{ event::{ self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, }, execute, terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, }, }; use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style}, text::{Line, Span}, widgets::{Block, Borders, Paragraph, Wrap}, Frame, Terminal, }; use canvas::{ canvas::{ gui::render_canvas_default, modes::AppMode, CursorManager, }, DataProvider, FormEditor, ValidationConfig, ValidationConfigBuilder, CharacterLimits, ValidationResult, }; // Import CountMode from the validation module directly use canvas::validation::limits::CountMode; // Enhanced FormEditor that demonstrates validation functionality struct ValidationFormEditor { editor: FormEditor, has_unsaved_changes: bool, debug_message: String, command_buffer: String, validation_enabled: bool, field_switch_blocked: bool, block_reason: Option, } impl ValidationFormEditor { fn new(data_provider: D) -> Self { let mut editor = FormEditor::new(data_provider); // Enable validation by default editor.set_validation_enabled(true); Self { editor, has_unsaved_changes: false, debug_message: "๐Ÿ” Validation Demo - Try typing in different fields!".to_string(), command_buffer: String::new(), validation_enabled: true, field_switch_blocked: false, block_reason: None, } } // === COMMAND BUFFER HANDLING === fn clear_command_buffer(&mut self) { self.command_buffer.clear(); } fn add_to_command_buffer(&mut self, ch: char) { self.command_buffer.push(ch); } fn get_command_buffer(&self) -> &str { &self.command_buffer } fn has_pending_command(&self) -> bool { !self.command_buffer.is_empty() } // === VALIDATION CONTROL === fn toggle_validation(&mut self) { self.validation_enabled = !self.validation_enabled; self.editor.set_validation_enabled(self.validation_enabled); if self.validation_enabled { self.debug_message = "โœ… Validation ENABLED - Try exceeding limits!".to_string(); } else { self.debug_message = "โŒ Validation DISABLED - No limits enforced".to_string(); } } fn check_field_switch_allowed(&self) -> (bool, Option) { if !self.validation_enabled { return (true, None); } let can_switch = self.editor.can_switch_fields(); let reason = if !can_switch { self.editor.field_switch_block_reason() } else { None }; (can_switch, reason) } fn get_validation_status(&self) -> String { if !self.validation_enabled { return "โŒ DISABLED".to_string(); } if self.field_switch_blocked { return "๐Ÿšซ SWITCH BLOCKED".to_string(); } let summary = self.editor.validation_summary(); if summary.has_errors() { format!("โŒ {} ERRORS", summary.error_fields) } else if summary.has_warnings() { format!("โš ๏ธ {} WARNINGS", summary.warning_fields) } else if summary.validated_fields > 0 { format!("โœ… {} VALID", summary.valid_fields) } else { "๐Ÿ” READY".to_string() } } fn validate_current_field(&mut self) { let result = self.editor.validate_current_field(); match result { ValidationResult::Valid => { self.debug_message = "โœ… Current field is valid!".to_string(); } ValidationResult::Warning { message } => { self.debug_message = format!("โš ๏ธ Warning: {message}"); } ValidationResult::Error { message } => { self.debug_message = format!("โŒ Error: {message}"); } } } fn validate_all_fields(&mut self) { let field_count = self.editor.data_provider().field_count(); for i in 0..field_count { self.editor.validate_field(i); } let summary = self.editor.validation_summary(); self.debug_message = format!( "๐Ÿ” Validated all fields: {} valid, {} warnings, {} errors", summary.valid_fields, summary.warning_fields, summary.error_fields ); } fn clear_validation_results(&mut self) { self.editor.clear_validation_results(); self.debug_message = "๐Ÿงน Cleared all validation results".to_string(); } // === ENHANCED MOVEMENT WITH VALIDATION === fn move_left(&mut self) { self.editor.move_left(); self.field_switch_blocked = false; self.block_reason = None; } fn move_right(&mut self) { self.editor.move_right(); self.field_switch_blocked = false; self.block_reason = None; } fn move_up(&mut self) { match self.editor.move_up() { Ok(()) => { self.update_field_validation_status(); self.field_switch_blocked = false; self.block_reason = None; } Err(e) => { self.field_switch_blocked = true; self.block_reason = Some(e.to_string()); self.debug_message = format!("๐Ÿšซ Field switch blocked: {e}"); } } } fn move_down(&mut self) { match self.editor.move_down() { Ok(()) => { self.update_field_validation_status(); self.field_switch_blocked = false; self.block_reason = None; } Err(e) => { self.field_switch_blocked = true; self.block_reason = Some(e.to_string()); self.debug_message = format!("๐Ÿšซ Field switch blocked: {e}"); } } } fn move_line_start(&mut self) { self.editor.move_line_start(); } fn move_line_end(&mut self) { self.editor.move_line_end(); } fn move_word_next(&mut self) { self.editor.move_word_next(); } fn move_word_prev(&mut self) { self.editor.move_word_prev(); } fn move_word_end(&mut self) { self.editor.move_word_end(); } fn move_first_line(&mut self) { self.editor.move_first_line(); } fn move_last_line(&mut self) { self.editor.move_last_line(); } fn update_field_validation_status(&mut self) { if !self.validation_enabled { return; } let result = self.editor.validate_current_field(); match result { ValidationResult::Valid => { self.debug_message = format!("Field {}: โœ… Valid", self.editor.current_field() + 1); } ValidationResult::Warning { message } => { self.debug_message = format!("Field {}: โš ๏ธ {}", self.editor.current_field() + 1, message); } ValidationResult::Error { message } => { self.debug_message = format!("Field {}: โŒ {}", self.editor.current_field() + 1, message); } } } // === MODE TRANSITIONS === fn enter_edit_mode(&mut self) { // Library will automatically update cursor to bar | in insert mode self.editor.enter_edit_mode(); self.debug_message = "โœ๏ธ INSERT MODE - Cursor: Steady Bar | - Type to test validation".to_string(); } fn enter_append_mode(&mut self) { // Library will automatically update cursor to bar | in insert mode self.editor.enter_append_mode(); self.debug_message = "โœ๏ธ INSERT (append) - Cursor: Steady Bar | - Validation active".to_string(); } fn exit_edit_mode(&mut self) { // Library will automatically update cursor to block โ–ˆ in normal mode self.editor.exit_edit_mode(); self.debug_message = "๐Ÿ”’ NORMAL MODE - Cursor: Steady Block โ–ˆ - Press 'v' to validate current field".to_string(); self.update_field_validation_status(); } fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { let result = self.editor.insert_char(ch); if result.is_ok() { self.has_unsaved_changes = true; // Show real-time validation feedback let validation_result = self.editor.validate_current_field(); match validation_result { ValidationResult::Valid => { // Don't spam with valid messages, just show character count if applicable if let Some(limits) = self.get_current_field_limits() { let field_index = self.editor.current_field(); if let Some(status) = limits.status_text( self.editor.data_provider().field_value(field_index) ) { self.debug_message = format!("โœ๏ธ {status}"); } } } ValidationResult::Warning { message } => { self.debug_message = format!("โš ๏ธ {message}"); } ValidationResult::Error { message } => { self.debug_message = format!("โŒ {message}"); } } } result } fn get_current_field_limits(&self) -> Option<&CharacterLimits> { let validation_state = self.editor.validation_state(); let config = validation_state.get_field_config(self.editor.current_field())?; config.character_limits.as_ref() } // === DELETE OPERATIONS === fn delete_backward(&mut self) -> anyhow::Result<()> { let result = self.editor.delete_backward(); if result.is_ok() { self.has_unsaved_changes = true; self.debug_message = "โŒซ Deleted character".to_string(); } result } fn delete_forward(&mut self) -> anyhow::Result<()> { let result = self.editor.delete_forward(); if result.is_ok() { self.has_unsaved_changes = true; self.debug_message = "โŒฆ Deleted character".to_string(); } result } // === DELEGATE TO ORIGINAL EDITOR === fn current_field(&self) -> usize { self.editor.current_field() } fn cursor_position(&self) -> usize { self.editor.cursor_position() } fn mode(&self) -> AppMode { self.editor.mode() } fn current_text(&self) -> &str { let field_index = self.editor.current_field(); self.editor.data_provider().field_value(field_index) } fn data_provider(&self) -> &D { self.editor.data_provider() } fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() } fn set_mode(&mut self, mode: AppMode) { // Library automatically updates cursor for the mode self.editor.set_mode(mode); } fn next_field(&mut self) { match self.editor.next_field() { Ok(()) => { self.update_field_validation_status(); self.field_switch_blocked = false; self.block_reason = None; } Err(e) => { self.field_switch_blocked = true; self.block_reason = Some(e.to_string()); self.debug_message = format!("๐Ÿšซ Cannot move to next field: {e}"); } } } fn prev_field(&mut self) { match self.editor.prev_field() { Ok(()) => { self.update_field_validation_status(); self.field_switch_blocked = false; self.block_reason = None; } Err(e) => { self.field_switch_blocked = true; self.block_reason = Some(e.to_string()); self.debug_message = format!("๐Ÿšซ Cannot move to previous field: {e}"); } } } // === STATUS AND DEBUG === fn set_debug_message(&mut self, msg: String) { self.debug_message = msg; } fn debug_message(&self) -> &str { &self.debug_message } fn has_unsaved_changes(&self) -> bool { self.has_unsaved_changes } } // Demo form data with different validation rules struct ValidationDemoData { fields: Vec<(String, String)>, } impl ValidationDemoData { fn new() -> Self { Self { fields: vec![ ("๐Ÿ‘ค Name (max 20)".to_string(), "".to_string()), ("๐Ÿ“ง Email (max 50, warn@40)".to_string(), "".to_string()), ("๐Ÿ”‘ Password (5-20 chars)".to_string(), "".to_string()), ("๐Ÿ”ข ID (min 3, max 10)".to_string(), "".to_string()), ("๐Ÿ“ Comment (min 10, max 100)".to_string(), "".to_string()), ("๐Ÿท๏ธ Tag (max 30, bytes)".to_string(), "".to_string()), ("๐ŸŒ Unicode (width, min 2)".to_string(), "".to_string()), ], } } } impl DataProvider for ValidationDemoData { fn field_count(&self) -> usize { self.fields.len() } fn field_name(&self, index: usize) -> &str { &self.fields[index].0 } fn field_value(&self, index: usize) -> &str { &self.fields[index].1 } fn set_field_value(&mut self, index: usize, value: String) { self.fields[index].1 = value; } fn supports_suggestions(&self, _field_index: usize) -> bool { false } fn display_value(&self, _index: usize) -> Option<&str> { None } // ๐ŸŽฏ NEW: Validation configuration per field fn validation_config(&self, field_index: usize) -> Option { match field_index { 0 => Some(ValidationConfig::with_max_length(20)), // Name: simple 20 char limit 1 => Some( ValidationConfigBuilder::new() .with_character_limits( CharacterLimits::new(50).with_warning_threshold(40) ) .build() ), // Email: 50 chars with warning at 40 2 => Some( ValidationConfigBuilder::new() .with_character_limits(CharacterLimits::new_range(5, 20)) .build() ), // Password: must be 5-20 characters (blocks field switching if 1-4 chars) 3 => Some( ValidationConfigBuilder::new() .with_character_limits(CharacterLimits::new_range(3, 10)) .build() ), // ID: must be 3-10 characters (blocks field switching if 1-2 chars) 4 => Some( ValidationConfigBuilder::new() .with_character_limits(CharacterLimits::new_range(10, 100)) .build() ), // Comment: must be 10-100 characters (blocks field switching if 1-9 chars) 5 => Some( ValidationConfigBuilder::new() .with_character_limits( CharacterLimits::new(30).with_count_mode(CountMode::Bytes) ) .build() ), // Tag: 30 bytes (useful for UTF-8) 6 => Some( ValidationConfigBuilder::new() .with_character_limits( CharacterLimits::new_range(2, 20).with_count_mode(CountMode::DisplayWidth) ) .build() ), // Unicode: 2-20 display width (useful for CJK characters, blocks if 1 char) _ => None, } } } /// Handle key presses with validation-focused commands fn handle_key_press( key: KeyCode, modifiers: KeyModifiers, editor: &mut ValidationFormEditor, ) -> anyhow::Result { let mode = editor.mode(); // Quit handling if (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) || (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) || key == KeyCode::F(10) { return Ok(false); } match (mode, key, modifiers) { // === MODE TRANSITIONS === (AppMode::ReadOnly, KeyCode::Char('i'), _) => { editor.enter_edit_mode(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('a'), _) => { editor.enter_append_mode(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('A'), _) => { editor.move_line_end(); editor.enter_edit_mode(); editor.clear_command_buffer(); } // Escape: Exit edit mode (_, KeyCode::Esc, _) => { if mode == AppMode::Edit { editor.exit_edit_mode(); } else { editor.clear_command_buffer(); } } // === VALIDATION COMMANDS === (AppMode::ReadOnly, KeyCode::Char('v'), _) => { editor.validate_current_field(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('V'), _) => { editor.validate_all_fields(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('c'), _) => { editor.clear_validation_results(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::F(1), _) => { editor.toggle_validation(); } // === MOVEMENT === (AppMode::ReadOnly, KeyCode::Char('h'), _) | (AppMode::ReadOnly, KeyCode::Left, _) => { editor.move_left(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('l'), _) | (AppMode::ReadOnly, KeyCode::Right, _) => { editor.move_right(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('j'), _) | (AppMode::ReadOnly, KeyCode::Down, _) => { editor.move_down(); editor.clear_command_buffer(); } (AppMode::ReadOnly, KeyCode::Char('k'), _) | (AppMode::ReadOnly, KeyCode::Up, _) => { editor.move_up(); editor.clear_command_buffer(); } // === EDIT MODE MOVEMENT === (AppMode::Edit, KeyCode::Left, _) => { editor.move_left(); } (AppMode::Edit, KeyCode::Right, _) => { editor.move_right(); } (AppMode::Edit, KeyCode::Up, _) => { editor.move_up(); } (AppMode::Edit, KeyCode::Down, _) => { editor.move_down(); } // === DELETE OPERATIONS === (AppMode::Edit, KeyCode::Backspace, _) => { editor.delete_backward()?; } (AppMode::Edit, KeyCode::Delete, _) => { editor.delete_forward()?; } // === TAB NAVIGATION === (_, KeyCode::Tab, _) => { editor.next_field(); } (_, KeyCode::BackTab, _) => { editor.prev_field(); } // === CHARACTER INPUT === (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { editor.insert_char(c)?; } // === DEBUG/INFO COMMANDS === (AppMode::ReadOnly, KeyCode::Char('?'), _) => { let summary = editor.editor.validation_summary(); editor.set_debug_message(format!( "Field {}/{}, Pos {}, Mode: {:?}, Validation: {} fields configured, {} validated", editor.current_field() + 1, editor.data_provider().field_count(), editor.cursor_position(), editor.mode(), summary.total_fields, summary.validated_fields )); } _ => { if editor.has_pending_command() { editor.clear_command_buffer(); editor.set_debug_message("Invalid command sequence".to_string()); } } } Ok(true) } fn run_app( terminal: &mut Terminal, mut editor: ValidationFormEditor, ) -> io::Result<()> { loop { terminal.draw(|f| ui(f, &editor))?; if let Event::Key(key) = event::read()? { match handle_key_press(key.code, key.modifiers, &mut editor) { Ok(should_continue) => { if !should_continue { break; } } Err(e) => { editor.set_debug_message(format!("Error: {e}")); } } } } Ok(()) } fn ui(f: &mut Frame, editor: &ValidationFormEditor) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(8), Constraint::Length(12)]) .split(f.area()); render_enhanced_canvas(f, chunks[0], editor); render_validation_status(f, chunks[1], editor); } fn render_enhanced_canvas( f: &mut Frame, area: Rect, editor: &ValidationFormEditor, ) { render_canvas_default(f, area, &editor.editor); } fn render_validation_status( f: &mut Frame, area: Rect, editor: &ValidationFormEditor, ) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), // Status bar Constraint::Length(4), // Validation summary Constraint::Length(5), // Help ]) .split(area); // Status bar with validation information let mode_text = match editor.mode() { AppMode::Edit => "INSERT | (bar cursor)", AppMode::ReadOnly => "NORMAL โ–ˆ (block cursor)", _ => "NORMAL โ–ˆ (block cursor)", }; let validation_status = editor.get_validation_status(); let status_text = if editor.has_pending_command() { format!("-- {} -- {} [{}] | Validation: {}", mode_text, editor.debug_message(), editor.get_command_buffer(), validation_status) } else if editor.has_unsaved_changes() { format!("-- {} -- [Modified] {} | Validation: {}", mode_text, editor.debug_message(), validation_status) } else { format!("-- {} -- {} | Validation: {}", mode_text, editor.debug_message(), validation_status) }; let status = Paragraph::new(Line::from(Span::raw(status_text))) .block(Block::default().borders(Borders::ALL).title("๐Ÿ” Validation Status")); f.render_widget(status, chunks[0]); // Validation summary with field switching info let summary = editor.editor.validation_summary(); let summary_text = if editor.validation_enabled { let switch_info = if editor.field_switch_blocked { format!("\n๐Ÿšซ Field switching blocked: {}", editor.block_reason.as_deref().unwrap_or("Unknown reason")) } else { let (can_switch, reason) = editor.check_field_switch_allowed(); if !can_switch { format!("\nโš ๏ธ Field switching will be blocked: {}", reason.as_deref().unwrap_or("Unknown reason")) } else { "\nโœ… Field switching allowed".to_string() } }; format!( "๐Ÿ“Š Validation Summary: {} fields configured, {} validated{}\n\ โœ… Valid: {} โš ๏ธ Warnings: {} โŒ Errors: {} ๐Ÿ“ˆ Progress: {:.0}%", summary.total_fields, summary.validated_fields, switch_info, summary.valid_fields, summary.warning_fields, summary.error_fields, summary.completion_percentage() * 100.0 ) } else { "โŒ Validation is currently DISABLED\nPress F1 to enable validation".to_string() }; let summary_style = if summary.has_errors() { Style::default().fg(Color::Red) } else if summary.has_warnings() { Style::default().fg(Color::Yellow) } else { Style::default().fg(Color::Green) }; let validation_summary = Paragraph::new(summary_text) .block(Block::default().borders(Borders::ALL).title("๐Ÿ“ˆ Validation Overview")) .style(summary_style) .wrap(Wrap { trim: true }); f.render_widget(validation_summary, chunks[1]); // Enhanced help text let help_text = match editor.mode() { AppMode::ReadOnly => { "๐ŸŽฏ CURSOR-STYLE: Normal โ–ˆ | Insert |\n\ ๐Ÿ” VALIDATION: Different fields have different limits (some block field switching)!\n\ Movement: hjkl/arrows=move, Tab/Shift+Tab=fields\n\ Edit: i/a/A=insert modes, Esc=normal\n\ Validation: v=validate current, V=validate all, c=clear results, F1=toggle\n\ ?=info, Ctrl+C/Ctrl+Q=quit" } AppMode::Edit => { "๐ŸŽฏ INSERT MODE - Cursor: | (bar)\n\ ๐Ÿ” Type to test validation limits (some fields have MIN requirements)!\n\ Try typing 1-2 chars in Password/ID/Comment fields, then try to switch!\n\ arrows=move, Backspace/Del=delete, Esc=normal, Tab=next field\n\ Field switching may be BLOCKED if minimum requirements not met!" } _ => "๐ŸŽฏ Watch the cursor change automatically while validating!" }; let help = Paragraph::new(help_text) .block(Block::default().borders(Borders::ALL).title("๐Ÿš€ Validation Commands")) .style(Style::default().fg(Color::Gray)) .wrap(Wrap { trim: true }); f.render_widget(help, chunks[2]); } fn main() -> Result<(), Box> { // Print feature status println!("๐Ÿ” Canvas Validation Demo"); println!("โœ… validation feature: ENABLED"); println!("๐Ÿš€ Field validation: ACTIVE"); println!("๐Ÿšซ Field switching validation: ACTIVE"); println!("๐Ÿ“Š Try typing in fields with minimum requirements!"); println!(" - Password (min 5): Type 1-4 chars, then try to switch fields"); println!(" - ID (min 3): Type 1-2 chars, then try to switch fields"); println!(" - Comment (min 10): Type 1-9 chars, then try to switch fields"); println!(" - Unicode (min 2): Type 1 char, then try to switch fields"); println!(); enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let data = ValidationDemoData::new(); let mut editor = ValidationFormEditor::new(data); // Initialize with normal mode - library automatically sets block cursor editor.set_mode(AppMode::ReadOnly); // Demonstrate that CursorManager is available and working CursorManager::update_for_mode(AppMode::ReadOnly)?; let res = run_app(&mut terminal, editor); // Library automatically resets cursor on FormEditor::drop() // But we can also manually reset if needed CursorManager::reset()?; disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; if let Err(err) = res { println!("{err:?}"); } println!("๐Ÿ” Validation demo completed!"); Ok(()) }