/* examples/validation_4.rs Enhanced Feature 4 Demo: Multiple custom formatters with comprehensive edge cases Demonstrates: - Multiple formatter types: PSC, Phone, Credit Card, Date - Edge case handling: incomplete input, invalid chars, overflow - Real-time validation feedback and format preview - Advanced cursor position mapping - Raw vs formatted data separation - Error handling and fallback behavior */ #![allow(clippy::needless_return)] #[cfg(not(all(feature = "validation", feature = "gui", feature = "cursor-style")))] compile_error!( "This example requires the 'validation', 'gui' and 'cursor-style' features. \ Run with: cargo run --example validation_4 --features \"gui,validation,cursor-style\"" ); use std::io; use std::sync::Arc; 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, CustomFormatter, FormattingResult, }; /// PSC (Postal Code) Formatter: "01001" -> "010 01" struct PSCFormatter; impl CustomFormatter for PSCFormatter { fn format(&self, raw: &str) -> FormattingResult { if raw.is_empty() { return FormattingResult::success(""); } // Validate: only digits allowed if !raw.chars().all(|c| c.is_ascii_digit()) { return FormattingResult::error("PSC must contain only digits"); } let len = raw.chars().count(); match len { 0 => FormattingResult::success(""), 1..=3 => FormattingResult::success(raw), 4 => FormattingResult::warning( format!("{} ", &raw[..3]), "PSC incomplete (4/5 digits)" ), 5 => { let formatted = format!("{} {}", &raw[..3], &raw[3..]); if raw == "00000" { FormattingResult::warning(formatted, "Invalid PSC: 00000") } else { FormattingResult::success(formatted) } }, _ => FormattingResult::error("PSC too long (max 5 digits)"), } } } /// Phone Number Formatter: "1234567890" -> "(123) 456-7890" struct PhoneFormatter; impl CustomFormatter for PhoneFormatter { fn format(&self, raw: &str) -> FormattingResult { if raw.is_empty() { return FormattingResult::success(""); } // Only digits allowed if !raw.chars().all(|c| c.is_ascii_digit()) { return FormattingResult::error("Phone must contain only digits"); } let len = raw.chars().count(); match len { 0 => FormattingResult::success(""), 1..=3 => FormattingResult::success(format!("({raw})")), 4..=6 => FormattingResult::success(format!("({}) {}", &raw[..3], &raw[3..])), 7..=10 => FormattingResult::success(format!("({}) {}-{}", &raw[..3], &raw[3..6], &raw[6..])), 10 => { let formatted = format!("({}) {}-{}", &raw[..3], &raw[3..6], &raw[6..]); FormattingResult::success(formatted) }, _ => FormattingResult::warning( format!("({}) {}-{}", &raw[..3], &raw[3..6], &raw[6..10]), "Phone too long (extra digits ignored)" ), } } } /// Credit Card Formatter: "1234567890123456" -> "1234 5678 9012 3456" struct CreditCardFormatter; impl CustomFormatter for CreditCardFormatter { fn format(&self, raw: &str) -> FormattingResult { if raw.is_empty() { return FormattingResult::success(""); } if !raw.chars().all(|c| c.is_ascii_digit()) { return FormattingResult::error("Card number must contain only digits"); } let mut formatted = String::new(); for (i, ch) in raw.chars().enumerate() { if i > 0 && i % 4 == 0 { formatted.push(' '); } formatted.push(ch); } let len = raw.chars().count(); match len { 0..=15 => FormattingResult::warning(formatted, format!("Card incomplete ({len}/16 digits)")), 16 => FormattingResult::success(formatted), _ => FormattingResult::warning(formatted, "Card too long (extra digits shown)"), } } } /// Date Formatter: "12012024" -> "12/01/2024" struct DateFormatter; impl CustomFormatter for DateFormatter { fn format(&self, raw: &str) -> FormattingResult { if raw.is_empty() { return FormattingResult::success(""); } if !raw.chars().all(|c| c.is_ascii_digit()) { return FormattingResult::error("Date must contain only digits"); } let len = raw.len(); match len { 0 => FormattingResult::success(""), 1..=2 => FormattingResult::success(raw.to_string()), 3..=4 => FormattingResult::success(format!("{}/{}", &raw[..2], &raw[2..])), 5..=8 => FormattingResult::success(format!("{}/{}/{}", &raw[..2], &raw[2..4], &raw[4..])), 8 => { let month = &raw[..2]; let day = &raw[2..4]; let year = &raw[4..]; // Basic validation let m: u32 = month.parse().unwrap_or(0); let d: u32 = day.parse().unwrap_or(0); if m == 0 || m > 12 { FormattingResult::warning( format!("{month}/{day}/{year}"), "Invalid month (01-12)" ) } else if d == 0 || d > 31 { FormattingResult::warning( format!("{month}/{day}/{year}"), "Invalid day (01-31)" ) } else { FormattingResult::success(format!("{month}/{day}/{year}")) } }, _ => FormattingResult::error("Date too long (MMDDYYYY format)"), } } } // Enhanced demo data with multiple formatter types struct MultiFormatterDemoData { fields: Vec<(String, String)>, } impl MultiFormatterDemoData { fn new() -> Self { Self { fields: vec![ ("🏁 PSC (01001)".to_string(), "".to_string()), ("πŸ“ž Phone (1234567890)".to_string(), "".to_string()), ("πŸ’³ Credit Card (16 digits)".to_string(), "".to_string()), ("πŸ“… Date (12012024)".to_string(), "".to_string()), ("πŸ“ Plain Text".to_string(), "".to_string()), ], } } } impl DataProvider for MultiFormatterDemoData { 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; } #[cfg(feature = "validation")] fn validation_config(&self, field_index: usize) -> Option { match field_index { 0 => Some(ValidationConfigBuilder::new() .with_custom_formatter(Arc::new(PSCFormatter)) .with_max_length(5) .build()), 1 => Some(ValidationConfigBuilder::new() .with_custom_formatter(Arc::new(PhoneFormatter)) .with_max_length(12) .build()), 2 => Some(ValidationConfigBuilder::new() .with_custom_formatter(Arc::new(CreditCardFormatter)) .with_max_length(20) .build()), 3 => Some(ValidationConfigBuilder::new() .with_custom_formatter(Arc::new(DateFormatter)) .with_max_length(8) .build()), 4 => Some(ValidationConfigBuilder::new() .with_custom_formatter(Arc::new(DateFormatter)) .with_max_length(8) .build()), _ => None, // Plain text field - no formatter } } } // Enhanced demo editor with comprehensive status tracking struct EnhancedDemoEditor { editor: FormEditor, debug_message: String, validation_enabled: bool, show_raw_data: bool, show_cursor_details: bool, example_mode: usize, } impl EnhancedDemoEditor { fn new(data_provider: D) -> Self { let mut editor = FormEditor::new(data_provider); editor.set_validation_enabled(true); Self { editor, debug_message: "🧩 Enhanced Custom Formatter Demo - Multiple formatters with rich edge cases!".to_string(), validation_enabled: true, show_raw_data: false, show_cursor_details: false, example_mode: 0, } } // Field type detection fn current_field_type(&self) -> &'static str { match self.editor.current_field() { 0 => "PSC", 1 => "Phone", 2 => "Credit Card", 3 => "Date", _ => "Plain Text", } } fn has_formatter(&self) -> bool { self.editor.current_field() < 5 // First 5 fields have formatters } fn get_input_rules(&self) -> &'static str { match self.editor.current_field() { 0 => "5 digits only (PSC format)", 1 => "10+ digits (US phone format)", 2 => "16+ digits (credit card)", 3 => "Digits as cents (12345 = $123.45)", 4 => "8 digits MMDDYYYY (date format)", _ => "Any text (no formatting)", } } fn cycle_example_data(&mut self) { let examples = [ // PSC examples vec!["01001", "1234567890", "1234567890123456", "12345", "12012024", "Plain text here"], // Incomplete examples vec!["010", "123", "1234", "123", "1201", "More text"], // Invalid examples (will show error handling) vec!["0abc1", "12a45", "123abc", "abc", "ab01cd", "Special chars!"], // Edge cases vec!["00000", "0000000000", "0000000000000000", "99", "13012024", ""], ]; self.example_mode = (self.example_mode + 1) % examples.len(); let current_examples = &examples[self.example_mode]; for (i, example) in current_examples.iter().enumerate() { if i < self.editor.data_provider().field_count() { self.editor.data_provider_mut().set_field_value(i, example.to_string()); } } let mode_names = ["Valid Examples", "Incomplete Input", "Invalid Characters", "Edge Cases"]; self.debug_message = format!("πŸ“‹ Loaded: {}", mode_names[self.example_mode]); } // Enhanced status methods fn toggle_validation(&mut self) { self.validation_enabled = !self.validation_enabled; self.editor.set_validation_enabled(self.validation_enabled); self.debug_message = if self.validation_enabled { "βœ… Custom Formatters ENABLED".to_string() } else { "❌ Custom Formatters DISABLED".to_string() }; } fn toggle_raw_data_view(&mut self) { self.show_raw_data = !self.show_raw_data; self.debug_message = if self.show_raw_data { "πŸ‘οΈ Showing RAW data focus".to_string() } else { "✨ Showing FORMATTED display focus".to_string() }; } fn toggle_cursor_details(&mut self) { self.show_cursor_details = !self.show_cursor_details; self.debug_message = if self.show_cursor_details { "πŸ“ Detailed cursor mapping info ON".to_string() } else { "πŸ“ Detailed cursor mapping info OFF".to_string() }; } fn get_current_field_analysis(&self) -> (String, String, String, Option) { let field_index = self.editor.current_field(); let raw = self.editor.data_provider().field_value(field_index); let display = self.editor.current_display_text(); let status = if raw == display { if self.has_formatter() { if self.mode() == AppMode::Edit { "Raw (editing)".to_string() } else { "No formatting needed".to_string() } } else { "No formatter".to_string() } } else { "Custom formatted".to_string() }; let warning = if self.validation_enabled && self.has_formatter() { // Check if there are any formatting warnings if !raw.is_empty() { match self.editor.current_field() { 0 if raw.len() < 5 => Some(format!("PSC incomplete: {}/5", raw.len())), 1 if raw.len() < 10 => Some(format!("Phone incomplete: {}/10", raw.len())), 2 if raw.len() < 16 => Some(format!("Card incomplete: {}/16", raw.len())), 4 if raw.len() < 8 => Some(format!("Date incomplete: {}/8", raw.len())), _ => None, } } else { None } } else { None }; (raw.to_string(), display, status, warning) } // Delegate methods with enhanced feedback fn enter_edit_mode(&mut self) { // Library will automatically update cursor to bar | in insert mode self.editor.enter_edit_mode(); let field_type = self.current_field_type(); let rules = self.get_input_rules(); self.debug_message = format!("✏️ INSERT MODE - Cursor: Steady Bar | - {field_type} - {rules}"); } fn exit_edit_mode(&mut self) { // Library will automatically update cursor to block β–ˆ in normal mode self.editor.exit_edit_mode(); let (raw, display, _, warning) = self.get_current_field_analysis(); if let Some(warn) = warning { self.debug_message = format!("πŸ”’ NORMAL - Cursor: Steady Block β–ˆ - {} | ⚠️ {}", self.current_field_type(), warn); } else if raw != display { self.debug_message = format!("πŸ”’ NORMAL - Cursor: Steady Block β–ˆ - {} formatted successfully", self.current_field_type()); } else { self.debug_message = "πŸ”’ NORMAL MODE - Cursor: Steady Block β–ˆ".to_string(); } } fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { let result = self.editor.insert_char(ch); if result.is_ok() { let (raw, display, _, _) = self.get_current_field_analysis(); if raw != display && self.validation_enabled { self.debug_message = format!("✏️ '{ch}' added - Real-time formatting active"); } else { self.debug_message = format!("✏️ '{ch}' added"); } } result } // Position mapping demo fn show_position_mapping(&mut self) { if !self.has_formatter() { self.debug_message = "πŸ“ No position mapping (plain text field)".to_string(); return; } let raw_pos = self.editor.cursor_position(); let display_pos = self.editor.display_cursor_position(); let field_index = self.editor.current_field(); let raw = self.editor.data_provider().field_value(field_index); let display = self.editor.current_display_text(); if raw_pos != display_pos { self.debug_message = format!( "πŸ—ΊοΈ Position mapping: Raw[{}]='{}' ↔ Display[{}]='{}'", raw_pos, raw.chars().nth(raw_pos).unwrap_or('βˆ…'), display_pos, display.chars().nth(display_pos).unwrap_or('βˆ…') ); } else { self.debug_message = format!("πŸ“ Cursor at position {raw_pos} (no mapping needed)"); } } // Delegate remaining methods fn mode(&self) -> AppMode { self.editor.mode() } fn current_field(&self) -> usize { self.editor.current_field() } fn cursor_position(&self) -> usize { self.editor.cursor_position() } fn data_provider(&self) -> &D { self.editor.data_provider() } fn data_provider_mut(&mut self) -> &mut D { self.editor.data_provider_mut() } fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() } fn move_up(&mut self) { let _ = self.editor.move_up(); } fn move_down(&mut self) { let _ = self.editor.move_down(); } fn move_left(&mut self) { let _ = self.editor.move_left(); } fn move_right(&mut self) { let _ = self.editor.move_right(); } fn delete_backward(&mut self) -> anyhow::Result<()> { self.editor.delete_backward() } fn delete_forward(&mut self) -> anyhow::Result<()> { self.editor.delete_forward() } fn next_field(&mut self) { let _ = self.editor.next_field(); } fn prev_field(&mut self) { let _ = self.editor.prev_field(); } } // Enhanced key handling fn handle_key_press( key: KeyCode, modifiers: KeyModifiers, editor: &mut EnhancedDemoEditor, ) -> anyhow::Result { let mode = editor.mode(); // Quit if matches!(key, KeyCode::F(10)) || (key == KeyCode::Char('q') && modifiers.contains(KeyModifiers::CONTROL)) || (key == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL)) { return Ok(false); } match (mode, key, modifiers) { // Mode transitions (AppMode::ReadOnly, KeyCode::Char('i'), _) => editor.enter_edit_mode(), (AppMode::ReadOnly, KeyCode::Char('a'), _) => { editor.editor.enter_append_mode(); editor.debug_message = format!("✏️ APPEND {} - {}", editor.current_field_type(), editor.get_input_rules()); }, (_, KeyCode::Esc, _) => editor.exit_edit_mode(), // Enhanced demo features (AppMode::ReadOnly, KeyCode::Char('e'), _) => editor.cycle_example_data(), (AppMode::ReadOnly, KeyCode::Char('r'), _) => editor.toggle_raw_data_view(), (AppMode::ReadOnly, KeyCode::Char('c'), _) => editor.toggle_cursor_details(), (AppMode::ReadOnly, KeyCode::Char('m'), _) => editor.show_position_mapping(), (AppMode::ReadOnly, KeyCode::F(1), _) => editor.toggle_validation(), // Movement (_, KeyCode::Up, _) | (AppMode::ReadOnly, KeyCode::Char('k'), _) => editor.move_up(), (_, KeyCode::Down, _) | (AppMode::ReadOnly, KeyCode::Char('j'), _) => editor.move_down(), (_, KeyCode::Left, _) | (AppMode::ReadOnly, KeyCode::Char('h'), _) => editor.move_left(), (_, KeyCode::Right, _) | (AppMode::ReadOnly, KeyCode::Char('l'), _) => editor.move_right(), (_, KeyCode::Tab, _) => editor.next_field(), (_, KeyCode::BackTab, _) => editor.prev_field(), // Editing (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { editor.insert_char(c)?; }, (AppMode::Edit, KeyCode::Backspace, _) => { editor.delete_backward()?; }, (AppMode::Edit, KeyCode::Delete, _) => { editor.delete_forward()?; }, // Field analysis (AppMode::ReadOnly, KeyCode::Char('?'), _) => { let (raw, display, status, warning) = editor.get_current_field_analysis(); let warning_text = warning.map(|w| format!(" ⚠️ {w}")).unwrap_or_default(); editor.debug_message = format!( "πŸ” Field {}: {} | Raw: '{}' | Display: '{}'{}", editor.current_field() + 1, status, raw, display, warning_text ); }, _ => {} } Ok(true) } fn run_app( terminal: &mut Terminal, mut editor: EnhancedDemoEditor, ) -> 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.debug_message = format!("❌ Error: {e}"); } } } } Ok(()) } fn ui(f: &mut Frame, editor: &EnhancedDemoEditor) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(8), Constraint::Length(18)]) .split(f.area()); render_canvas_default(f, chunks[0], &editor.editor); render_enhanced_status(f, chunks[1], editor); } fn render_enhanced_status( f: &mut Frame, area: Rect, editor: &EnhancedDemoEditor, ) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), // Status bar Constraint::Length(6), // Current field analysis Constraint::Length(9), // Help ]) .split(area); // Status bar let mode_text = match editor.mode() { AppMode::Edit => "INSERT | (bar cursor)", AppMode::ReadOnly => "NORMAL β–ˆ (block cursor)", _ => "NORMAL β–ˆ (block cursor)", }; let formatter_count = (0..editor.data_provider().field_count()) .filter(|&i| editor.data_provider().validation_config(i).is_some()) .count(); let status_text = format!( "-- {} -- {} | Formatters: {}/{} active | View: {}{}", mode_text, editor.debug_message, formatter_count, editor.data_provider().field_count(), if editor.show_raw_data { "RAW" } else { "DISPLAY" }, if editor.show_cursor_details { " | CURSOR+" } else { "" } ); let status = Paragraph::new(Line::from(Span::raw(status_text))) .block(Block::default().borders(Borders::ALL).title("🧩 Enhanced Custom Formatter Demo")); f.render_widget(status, chunks[0]); // Current field analysis let (raw, display, status, warning) = editor.get_current_field_analysis(); let field_name = editor.data_provider().field_name(editor.current_field()); let field_type = editor.current_field_type(); let mut analysis_lines = vec![ format!("πŸ“ Current: {} ({})", field_name, field_type), format!("πŸ”§ Status: {}", status), ]; if editor.show_raw_data || editor.mode() == AppMode::Edit { analysis_lines.push(format!("πŸ’Ύ Raw Data: '{raw}'")); analysis_lines.push(format!("✨ Display: '{display}'")); } else { analysis_lines.push(format!("✨ User Sees: '{display}'")); analysis_lines.push(format!("πŸ’Ύ Stored As: '{raw}'")); } if editor.show_cursor_details { analysis_lines.push(format!( "πŸ“ Cursor: Raw[{}] β†’ Display[{}]", editor.cursor_position(), editor.editor.display_cursor_position() )); } if let Some(ref warn) = warning { analysis_lines.push(format!("⚠️ Warning: {warn}")); } let analysis_color = if warning.is_some() { Color::Yellow } else if raw != display && editor.validation_enabled { Color::Green } else { Color::Gray }; let analysis = Paragraph::new(analysis_lines.join("\n")) .block(Block::default().borders(Borders::ALL).title("πŸ” Field Analysis")) .style(Style::default().fg(analysis_color)) .wrap(Wrap { trim: true }); f.render_widget(analysis, chunks[1]); // Enhanced help let help_text = match editor.mode() { AppMode::ReadOnly => { "🎯 CURSOR-STYLE: Normal β–ˆ | Insert |\n\ 🧩 ENHANCED CUSTOM FORMATTER DEMO\n\ \n\ Try these formatters: β€’ PSC: 01001 β†’ 010 01 | Phone: 1234567890 β†’ (123) 456-7890 | Card: 1234567890123456 β†’ 1234 5678 9012 3456 β€’ Date: 12012024 β†’ 12/01/2024 | Plain: no formatting \n\ Commands: i=insert, e=cycle examples, r=toggle raw/display, c=cursor details, m=position mapping\n\ Movement: hjkl/arrows, Tab=next field, ?=analyze current field, F1=toggle formatters\n\ Ctrl+C/F10=quit" } AppMode::Edit => { "🎯 INSERT MODE - Cursor: | (bar)\n\ ✏️ Real-time formatting as you type!\n\ \n\ Current field rules: {}\n\ β€’ Raw input is authoritative (what gets stored)\n\ β€’ Display formatting updates in real-time (what users see)\n\ β€’ Cursor position is mapped between raw and display\n\ \n\ Esc=normal mode, arrows=navigate, Backspace/Del=delete" } _ => "🧩 Enhanced Custom Formatter Demo" }; let formatted_help = if editor.mode() == AppMode::Edit { help_text.replace("{}", editor.get_input_rules()) } else { help_text.to_string() }; let help = Paragraph::new(formatted_help) .block(Block::default().borders(Borders::ALL).title("πŸš€ Enhanced Features & Commands")) .style(Style::default().fg(Color::Gray)) .wrap(Wrap { trim: true }); f.render_widget(help, chunks[2]); } fn main() -> Result<(), Box> { println!("🧩 Enhanced Canvas Custom Formatter Demo (Feature 4)"); println!("βœ… validation feature: ENABLED"); println!("βœ… gui feature: ENABLED"); println!("βœ… cursor-style feature: ENABLED"); println!("🧩 Enhanced features:"); println!(" β€’ 5 different custom formatters with edge cases"); println!(" β€’ Real-time format preview and validation"); println!(" β€’ Advanced cursor position mapping"); println!(" β€’ Comprehensive error handling and warnings"); println!(" β€’ Raw vs formatted data separation demos"); 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 = MultiFormatterDemoData::new(); let mut editor = EnhancedDemoEditor::new(data); // Initialize with normal mode - library automatically sets block cursor editor.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); disable_raw_mode()?; execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?; terminal.show_cursor()?; // Library automatically resets cursor on FormEditor::drop() // But we can also manually reset if needed CursorManager::reset()?; if let Err(err) = res { println!("{err:?}"); } println!("🧩 Enhanced custom formatter demo completed!"); println!("πŸ† You experienced comprehensive custom formatting with:"); println!(" β€’ Multiple formatter types (PSC, Phone, Credit Card, Date)"); println!(" β€’ Edge case handling (incomplete, invalid, overflow)"); println!(" β€’ Real-time format preview and cursor mapping"); println!(" β€’ Clear separation between raw business data and display formatting"); Ok(()) }