From 9c36e76eaa630d8f23f064c4ae6b1399af936dcb Mon Sep 17 00:00:00 2001 From: Priec Date: Tue, 5 Aug 2025 18:27:16 +0200 Subject: [PATCH] validation of characters length is finished --- canvas/Cargo.toml | 5 +- canvas/examples/validation_1.rs | 831 ++++++++++++++++++++++++++++++++ canvas/src/editor.rs | 71 ++- canvas/src/validation/config.rs | 27 ++ canvas/src/validation/limits.rs | 59 +++ canvas/src/validation/state.rs | 27 +- 6 files changed, 1004 insertions(+), 16 deletions(-) create mode 100644 canvas/examples/validation_1.rs diff --git a/canvas/Cargo.toml b/canvas/Cargo.toml index 9669dc8..aed97f6 100644 --- a/canvas/Cargo.toml +++ b/canvas/Cargo.toml @@ -33,7 +33,6 @@ default = [] gui = ["ratatui"] autocomplete = ["tokio"] cursor-style = ["crossterm"] -regex = ["dep:regex"] validation = ["regex"] [[example]] @@ -45,3 +44,7 @@ path = "examples/autocomplete.rs" name = "canvas_gui_demo" required-features = ["gui"] path = "examples/canvas_gui_demo.rs" + +[[example]] +name = "validation_1" +required-features = ["gui", "validation"] diff --git a/canvas/examples/validation_1.rs b/canvas/examples/validation_1.rs new file mode 100644 index 0000000..b271890 --- /dev/null +++ b/canvas/examples/validation_1.rs @@ -0,0 +1,831 @@ +// examples/validation_1.rs +//! Demonstrates field validation with the canvas library +//! +//! This example REQUIRES the `validation` feature 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(feature = "validation"))] +compile_error!( + "This example requires the 'validation' feature. \ + Run with: cargo run --example validation_1 --features \"gui,validation\"" +); + +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, + }, + 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; + } + + if let Some(result) = self.editor.current_field_validation() { + 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); + } + } + } else { + self.debug_message = format!("Field {}: ๐Ÿ” Not validated yet", self.editor.current_field() + 1); + } + } + + // === MODE TRANSITIONS === + fn enter_edit_mode(&mut self) { + self.editor.enter_edit_mode(); + self.debug_message = "โœ๏ธ INSERT MODE - Type to test validation".to_string(); + } + + fn enter_append_mode(&mut self) { + self.editor.enter_append_mode(); + self.debug_message = "โœ๏ธ INSERT (append) - Validation active".to_string(); + } + + fn exit_edit_mode(&mut self) { + self.editor.exit_edit_mode(); + self.debug_message = "๐Ÿ”’ NORMAL MODE - 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 + if let Some(validation_result) = self.editor.current_field_validation() { + 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() { + if let Some(status) = limits.status_text(self.editor.current_text()) { + self.debug_message = format!("โœ๏ธ {}", status); + } + } + } + ValidationResult::Warning { message } => { + self.debug_message = format!("โš ๏ธ {}", message); + } + ValidationResult::Error { message } => { + self.debug_message = format!("โŒ {}", message); + } + } + } + } + Ok(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(); + } + Ok(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(); + } + Ok(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 { + self.editor.current_text() + } + + 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) { + 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_autocomplete(&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", + AppMode::ReadOnly => "NORMAL", + _ => "OTHER", + }; + + 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 => { + "๐Ÿ” VALIDATION DEMO: Different fields have different limits!\n\ + Fields with MINIMUM requirements will block field switching if too short!\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 - Type to test validation limits!\n\ + Some fields have MINIMUM character 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!" + } + _ => "๐Ÿ” Validation Demo Active!" + }; + + 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 editor = ValidationFormEditor::new(data); + + let res = run_app(&mut terminal, editor); + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err); + } + + println!("๐Ÿ” Validation demo completed!"); + Ok(()) +} diff --git a/canvas/src/editor.rs b/canvas/src/editor.rs index f287fa5..f100372 100644 --- a/canvas/src/editor.rs +++ b/canvas/src/editor.rs @@ -326,6 +326,20 @@ impl FormEditor { pub fn validation_summary(&self) -> crate::validation::ValidationSummary { self.ui_state.validation.summary() } + + /// Check if field switching is allowed from current field + #[cfg(feature = "validation")] + pub fn can_switch_fields(&self) -> bool { + let current_text = self.current_text(); + self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) + } + + /// Get reason why field switching is blocked (if any) + #[cfg(feature = "validation")] + pub fn field_switch_block_reason(&self) -> Option { + let current_text = self.current_text(); + self.ui_state.validation.field_switch_block_reason(self.ui_state.current_field, current_text) + } // =================================================================== // ASYNC OPERATIONS: Only autocomplete needs async @@ -409,10 +423,22 @@ impl FormEditor { // =================================================================== /// Move to previous field (vim k / up arrow) - pub fn move_up(&mut self) { + pub fn move_up(&mut self) -> Result<()> { let field_count = self.data_provider.field_count(); if field_count == 0 { - return; + return Ok(()); + } + + // Check if field switching is allowed (minimum character enforcement) + #[cfg(feature = "validation")] + { + let current_text = self.current_text(); + if !self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) { + if let Some(reason) = self.ui_state.validation.field_switch_block_reason(self.ui_state.current_field, current_text) { + tracing::debug!("Field switch blocked: {}", reason); + return Err(anyhow::anyhow!("Cannot switch fields: {}", reason)); + } + } } // Validate current field before moving @@ -430,13 +456,26 @@ impl FormEditor { self.ui_state.move_to_field(new_field, field_count); self.clamp_cursor_to_current_field(); + Ok(()) } /// Move to next field (vim j / down arrow) - pub fn move_down(&mut self) { + pub fn move_down(&mut self) -> Result<()> { let field_count = self.data_provider.field_count(); if field_count == 0 { - return; + return Ok(()); + } + + // Check if field switching is allowed (minimum character enforcement) + #[cfg(feature = "validation")] + { + let current_text = self.current_text(); + if !self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) { + if let Some(reason) = self.ui_state.validation.field_switch_block_reason(self.ui_state.current_field, current_text) { + tracing::debug!("Field switch blocked: {}", reason); + return Err(anyhow::anyhow!("Cannot switch fields: {}", reason)); + } + } } // Validate current field before moving @@ -454,6 +493,7 @@ impl FormEditor { self.ui_state.move_to_field(new_field, field_count); self.clamp_cursor_to_current_field(); + Ok(()) } /// Move to first field (vim gg) @@ -480,13 +520,13 @@ impl FormEditor { } /// Move to previous field (alternative to move_up) - pub fn prev_field(&mut self) { - self.move_up(); + pub fn prev_field(&mut self) -> Result<()> { + self.move_up() } /// Move to next field (alternative to move_down) - pub fn next_field(&mut self) { - self.move_down(); + pub fn next_field(&mut self) -> Result<()> { + self.move_down() } /// Move to start of current field (vim 0) @@ -649,15 +689,16 @@ impl FormEditor { } /// Exit edit mode to read-only mode (vim Escape) - pub fn exit_edit_mode(&mut self) { + pub fn exit_edit_mode(&mut self) -> Result<()> { // Validate current field content when exiting edit mode #[cfg(feature = "validation")] { - let current_text = self.current_text().to_string(); // Convert to String to avoid borrow conflicts - let _validation_result = self.ui_state.validation.validate_field_content( - self.ui_state.current_field, - ¤t_text, - ); + let current_text = self.current_text(); + if !self.ui_state.validation.allows_field_switch(self.ui_state.current_field, current_text) { + if let Some(reason) = self.ui_state.validation.field_switch_block_reason(self.ui_state.current_field, current_text) { + return Err(anyhow::anyhow!("Cannot exit edit mode: {}", reason)); + } + } } // Adjust cursor position when transitioning from edit to normal mode @@ -674,6 +715,8 @@ impl FormEditor { self.set_mode(AppMode::ReadOnly); // Deactivate autocomplete when exiting edit mode self.ui_state.deactivate_autocomplete(); + + Ok(()) } /// Enter edit mode from read-only mode (vim i/a/o) diff --git a/canvas/src/validation/config.rs b/canvas/src/validation/config.rs index 2ad300d..e37b418 100644 --- a/canvas/src/validation/config.rs +++ b/canvas/src/validation/config.rs @@ -1,3 +1,4 @@ +// src/validation/config.rs //! Validation configuration types and builders use crate::validation::CharacterLimits; @@ -158,6 +159,32 @@ impl ValidationConfig { // || self.custom_formatting.is_some() // || self.external_validation.is_some() } + pub fn allows_field_switch(&self, text: &str) -> bool { + // Character limits validation + if let Some(ref limits) = self.character_limits { + if !limits.allows_field_switch(text) { + return false; + } + } + + // Future: Add other validation types here + + true + } + + /// Get reason why field switching is blocked (if any) + pub fn field_switch_block_reason(&self, text: &str) -> Option { + // Character limits validation + if let Some(ref limits) = self.character_limits { + if let Some(reason) = limits.field_switch_block_reason(text) { + return Some(reason); + } + } + + // Future: Add other validation types here + + None + } } #[cfg(test)] diff --git a/canvas/src/validation/limits.rs b/canvas/src/validation/limits.rs index fcbccd6..921cc8a 100644 --- a/canvas/src/validation/limits.rs +++ b/canvas/src/validation/limits.rs @@ -1,3 +1,4 @@ +// src/validation/limits.rs //! Character limits validation implementation use crate::validation::ValidationResult; @@ -250,6 +251,29 @@ impl CharacterLimits { }, } } + pub fn allows_field_switch(&self, text: &str) -> bool { + if let Some(min) = self.min_length { + let count = self.count(text); + // Allow switching if field is empty OR meets minimum requirement + count == 0 || count >= min + } else { + true // No minimum requirement, always allow switching + } + } + + /// Get reason why field switching is not allowed (if any) + pub fn field_switch_block_reason(&self, text: &str) -> Option { + if let Some(min) = self.min_length { + let count = self.count(text); + if count > 0 && count < min { + return Some(format!( + "Field must be empty or have at least {} characters (currently: {})", + min, count + )); + } + } + None + } } impl Default for CharacterLimits { @@ -362,4 +386,39 @@ mod tests { assert_eq!(limits.status_text("12345678"), Some("8/10 (approaching limit)".to_string())); assert_eq!(limits.status_text("1234567890x"), Some("11/10 (exceeded)".to_string())); } + + #[test] + fn test_field_switch_blocking() { + let limits = CharacterLimits::new_range(3, 10); + + // Empty field: should allow switching + assert!(limits.allows_field_switch("")); + assert!(limits.field_switch_block_reason("").is_none()); + + // Field with content below minimum: should block switching + assert!(!limits.allows_field_switch("hi")); + assert!(limits.field_switch_block_reason("hi").is_some()); + assert!(limits.field_switch_block_reason("hi").unwrap().contains("at least 3 characters")); + + // Field meeting minimum: should allow switching + assert!(limits.allows_field_switch("hello")); + assert!(limits.field_switch_block_reason("hello").is_none()); + + // Field exceeding maximum: should still allow switching (validation shows error but doesn't block) + assert!(limits.allows_field_switch("this is way too long")); + assert!(limits.field_switch_block_reason("this is way too long").is_none()); + } + + #[test] + fn test_field_switch_no_minimum() { + let limits = CharacterLimits::new(10); // Only max, no minimum + + // Should always allow switching when there's no minimum + assert!(limits.allows_field_switch("")); + assert!(limits.allows_field_switch("a")); + assert!(limits.allows_field_switch("hello")); + + assert!(limits.field_switch_block_reason("").is_none()); + assert!(limits.field_switch_block_reason("a").is_none()); + } } diff --git a/canvas/src/validation/state.rs b/canvas/src/validation/state.rs index 4f48727..039c4ea 100644 --- a/canvas/src/validation/state.rs +++ b/canvas/src/validation/state.rs @@ -1,3 +1,4 @@ +// src/validation/state.rs //! Validation state management use crate::validation::{ValidationConfig, ValidationResult}; @@ -174,7 +175,31 @@ impl ValidationState { self.field_configs.len() } - /// Get validation summary + /// Check if field switching is allowed for a specific field + pub fn allows_field_switch(&self, field_index: usize, text: &str) -> bool { + if !self.enabled { + return true; + } + + if let Some(config) = self.field_configs.get(&field_index) { + config.allows_field_switch(text) + } else { + true // No validation configured, allow switching + } + } + + /// Get reason why field switching is blocked (if any) + pub fn field_switch_block_reason(&self, field_index: usize, text: &str) -> Option { + if !self.enabled { + return None; + } + + if let Some(config) = self.field_configs.get(&field_index) { + config.field_switch_block_reason(text) + } else { + None // No validation configured + } + } pub fn summary(&self) -> ValidationSummary { let total_validated = self.validated_fields.len(); let errors = self.fields_with_errors().count();