// examples/validation_5.rs //! Feature 5: External validation (UI-only) demo with Feature 4 (custom formatter) //! //! Behavior: //! - Field 1 (PSC) uses a custom formatter (Feature 4) and external validation (Feature 5). //! β€’ While editing PSC: raw text, capped at 5 digits //! β€’ When not editing PSC (moved focus or Esc) and raw is 5 digits: shows formatted ("XXX XX") //! β€’ After leaving PSC (or pressing 'v'), external validation kicks in: //! - Validating -> Valid/Invalid/Warning based on simple rules (LSP-like simulation) //! - Field 2 (Notes) is plain text, no formatter, no external validation. //! //! Controls: //! - i/a: insert/append //! - Esc: exit edit mode (triggers PSC validation if enabled) //! - Tab/Shift+Tab: next/prev field (triggers PSC validation if enabled) //! - v: manually trigger validation of current field //! - c: clear external validation state for current field //! - r: toggle raw/format view flag in panel (visual only; canvas follows library rules) //! - F10/Ctrl+C: quit //! //! Run: cargo run --example validation_5 --features "gui,validation" #![allow(clippy::needless_return)] #[cfg(not(all(feature = "validation", feature = "gui")))] compile_error!( "This example requires the 'validation' and 'gui' features. \ Run with: cargo run --example validation_5 --features \"gui,validation\"" ); 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}, DataProvider, FormEditor, ValidationConfigBuilder, CustomFormatter, FormattingResult, validation::ExternalValidationState, }; /// PSC custom formatter for display ("XXX XX"). struct PSCFormatter; impl CustomFormatter for PSCFormatter { fn format(&self, raw: &str) -> FormattingResult { let mut out = String::new(); for (i, ch) in raw.chars().enumerate() { if i == 3 { out.push(' '); } out.push(ch); } FormattingResult::success(out) } } // Demo data provider: PSC + Notes struct DemoData { fields: Vec<(String, String)>, } impl DemoData { fn new() -> Self { Self { fields: vec![ ("🏁 PSC (5 digits)".to_string(), "".to_string()), ("πŸ“ Notes".to_string(), "".to_string()), ], } } } impl DataProvider for DemoData { 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 => { // PSC: Feature 4 + Feature 5 + max length cap Some( ValidationConfigBuilder::new() .with_custom_formatter(Arc::new(PSCFormatter)) .with_max_length(5) // Enforce raw max length during typing .with_external_validation_enabled(true) // Show external validation indicator .build() ) } _ => None, } } } /// Simulated LSP-like validator: /// - Receives the raw PSC and returns a state (Validating -> Valid/Warn/Invalid). /// - This is called by our "frontend" workflow when we leave PSC (or press 'v'). /// In real apps, this would be an async backend call; we just simulate results. fn simulate_external_psc_validation(raw_psc: &str) -> ExternalValidationState { // Edge cases and outcomes: // - Empty -> NotValidated (frontend shouldn't call on empty, but handle gracefully) // - Non-digit -> Invalid // - Length < 5 -> Warning (incomplete) // - Exactly 5 digits: // * "00000" -> Invalid (nonsensical) // * "12345" -> Warning (pretend partial region) // * "01001" -> Valid(Some("Known district")) // * otherwise Valid(None) if raw_psc.is_empty() { return ExternalValidationState::NotValidated; } if !raw_psc.chars().all(|c| c.is_ascii_digit()) { return ExternalValidationState::Invalid { message: "PSC must be digits".to_string(), suggestion: Some("Only 0-9".to_string()) }; } let len = raw_psc.chars().count(); if len < 5 { return ExternalValidationState::Warning { message: format!("PSC incomplete: {}/5", len) }; } // len == 5 match raw_psc { "00000" => ExternalValidationState::Invalid { message: "PSC cannot be 00000".to_string(), suggestion: Some("Try 01001".to_string()) }, "12345" => ExternalValidationState::Warning { message: "Unrecognized region: verify".to_string() }, "01001" => ExternalValidationState::Valid(Some("Known district".to_string())), _ => ExternalValidationState::Valid(None), } } // Editor wrapper to manage external validation workflow and show UI info struct DemoEditor { editor: FormEditor, debug_message: String, show_raw_hint: bool, // panel-only toggle, does not affect canvas } impl DemoEditor { fn new(data_provider: D) -> Self { let mut editor = FormEditor::new(data_provider); editor.set_validation_enabled(true); Self { editor, debug_message: "πŸ§ͺ External validation demo (Feature 5) + Custom formatting (Feature 4)".to_string(), show_raw_hint: false, } } fn current_field(&self) -> usize { self.editor.current_field() } fn mode(&self) -> AppMode { self.editor.mode() } fn data_provider(&self) -> &D { self.editor.data_provider() } fn cursor_position(&self) -> usize { self.editor.cursor_position() } fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() } // Minimal pass-through editing controls fn enter_edit_mode(&mut self) { self.editor.enter_edit_mode(); self.debug_message = "✏️ Edit PSC or Notes. PSC: raw while editing, formatted when not editing".to_string(); } fn enter_append_mode(&mut self) { self.editor.enter_append_mode(); self.debug_message = "✏️ Append mode active".to_string(); } fn exit_edit_mode(&mut self) { self.editor.exit_edit_mode(); self.debug_message = "πŸ”’ Normal mode".to_string(); // Trigger external validation upon exiting the field (Feature 5 timing) self.trigger_field_validation(self.editor.current_field()); } fn next_field(&mut self) { match self.editor.next_field() { Ok(()) => { self.debug_message = "➑ Switched to next field".to_string(); // Validate field we just left (Feature 5 timing) let prev = self.editor.current_field().saturating_sub(1); self.trigger_field_validation(prev); } Err(e) => { self.debug_message = format!("🚫 Cannot move to next field: {}", e); } } } fn prev_field(&mut self) { match self.editor.prev_field() { Ok(()) => { self.debug_message = "β¬… Switched to previous field".to_string(); // Validate field we just left let prev = self.editor.current_field() + 1; self.trigger_field_validation(prev); } Err(e) => { self.debug_message = format!("🚫 Cannot move to previous field: {}", e); } } } fn insert_char(&mut self, ch: char) -> anyhow::Result<()> { self.editor.insert_char(ch) } fn delete_backward(&mut self) -> anyhow::Result<()> { self.editor.delete_backward() } fn delete_forward(&mut self) -> anyhow::Result<()> { self.editor.delete_forward() } // Toggle panel hint: raw vs formatted clarification (canvas obeys library rules) fn toggle_raw_hint(&mut self) { self.show_raw_hint = !self.show_raw_hint; self.debug_message = if self.show_raw_hint { "πŸ‘ Showing raw vs display hints in panel".to_string() } else { "🎭 Hints off".to_string() }; } // Manually trigger validation for current field (press 'v') fn trigger_current_field_validation(&mut self) { let idx = self.editor.current_field(); self.trigger_field_validation(idx); } // Core: "frontend" triggers validation on blur fn trigger_field_validation(&mut self, field_index: usize) { // Only if field exists and has external_validation enabled if let Some(cfg) = self.editor.validation_state().get_field_config(field_index) { if cfg.external_validation_enabled { let raw = self.editor.data_provider().field_value(field_index).to_string(); if raw.is_empty() { // Clear if empty self.editor.clear_external_validation(field_index); return; } // Set Validating self.editor.set_external_validation(field_index, ExternalValidationState::Validating); // "Async" backend simulation: immediate result calculation. In a real app, do this later. let result = simulate_external_psc_validation(&raw); self.editor.set_external_validation(field_index, result); } } } // Read external validation state for panel fn external_state(&self, field_index: usize) -> ExternalValidationState { self.editor.validation_state().get_external_validation(field_index) } // Clear external validation state (press 'c') fn clear_external_state(&mut self) { let idx = self.editor.current_field(); self.editor.clear_external_validation(idx); self.debug_message = "🧹 Cleared external validation state".to_string(); } } // UI fn run_app( terminal: &mut Terminal, mut editor: DemoEditor, ) -> io::Result<()> { loop { terminal.draw(|f| ui(f, &editor))?; if let Event::Key(key) = event::read()? { let mode = editor.mode(); let kc = key.code; let km = key.modifiers; // Quit if (kc == KeyCode::Char('q') && km.contains(KeyModifiers::CONTROL)) || (kc == KeyCode::Char('c') && km.contains(KeyModifiers::CONTROL)) || kc == KeyCode::F(10) { break; } match (mode, kc, km) { // Modes (AppMode::ReadOnly, KeyCode::Char('i'), _) => editor.enter_edit_mode(), (AppMode::ReadOnly, KeyCode::Char('a'), _) => editor.enter_append_mode(), (_, KeyCode::Esc, _) => editor.exit_edit_mode(), // Movement (_, KeyCode::Tab, _) => editor.next_field(), (_, KeyCode::BackTab, _) => editor.prev_field(), // Edit (AppMode::Edit, KeyCode::Char(c), m) if !m.contains(KeyModifiers::CONTROL) => { let _ = editor.insert_char(c); } (AppMode::Edit, KeyCode::Backspace, _) => { let _ = editor.delete_backward(); } (AppMode::Edit, KeyCode::Delete, _) => { let _ = editor.delete_forward(); } // External validation (_, KeyCode::Char('v'), _) => { editor.trigger_current_field_validation(); } (_, KeyCode::Char('c'), _) => { editor.clear_external_state(); } // Panel (_, KeyCode::Char('r'), _) => editor.toggle_raw_hint(), _ => {} } } } Ok(()) } fn ui(f: &mut Frame, editor: &DemoEditor) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(8), Constraint::Length(14)]) .split(f.area()); render_canvas_default(f, chunks[0], &editor.editor); render_status_panel(f, chunks[1], editor); } fn render_status_panel(f: &mut Frame, area: Rect, editor: &DemoEditor) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), Constraint::Length(5), Constraint::Length(6), ]) .split(area); // Bar let mode_text = match editor.mode() { AppMode::Edit => "INSERT", AppMode::ReadOnly => "NORMAL", _ => "OTHER", }; let bar = Paragraph::new(Line::from(Span::raw(format!( "-- {} -- {}", mode_text, editor.debug_message )))) .block(Block::default().borders(Borders::ALL).title("πŸ§ͺ External Validation Demo")); f.render_widget(bar, chunks[0]); // External validation snapshot let mut lines = Vec::new(); let field_count = editor.data_provider().field_count(); for i in 0..field_count { let name = editor.data_provider().field_name(i); let raw = editor.editor.data_provider().field_value(i); let display = editor.editor.display_text_for_field(i); let state = editor.external_state(i); let (label, color) = match state { ExternalValidationState::NotValidated => ("Not validated", Color::Gray), ExternalValidationState::Validating => ("Validating…", Color::Blue), ExternalValidationState::Valid(Some(_)) => ("Valid βœ“", Color::Green), ExternalValidationState::Valid(None) => ("Valid βœ“", Color::Green), ExternalValidationState::Invalid { .. } => ("Invalid βœ–", Color::Red), ExternalValidationState::Warning { .. } => ("Warning ⚠", Color::Yellow), }; let mut text = format!("{}: ", name); if editor.show_raw_hint { text.push_str(&format!("raw='{}' display='{}' | ", raw, display)); } text.push_str(label); lines.push(Span::styled(text, Style::default().fg(color))); lines.push(Span::raw("\n")); } let snapshot = Paragraph::new(Line::from(lines)) .block(Block::default().borders(Borders::ALL).title("πŸ“‘ External validation states")); f.render_widget(snapshot, chunks[1]); // Help let help = Paragraph::new( "Controls:\n\ β€’ i/a: insert/append, Esc: exit edit (validates PSC)\n\ β€’ Tab/Shift+Tab: switch fields (validates PSC)\n\ β€’ v: validate current field now, c: clear validation state\n\ β€’ r: toggle raw/display hints (panel only)\n\ β€’ Ctrl+C/F10: quit\n\ \n\ PSC rules:\n\ β€’ Raw typing only while editing (Feature 4) and max 5 digits\n\ β€’ On blur/exit: external validation occurs (Feature 5)\n\ β€’ Known Valid: 01001; Invalid: 00000 or non-digits; Warning: incomplete or 12345", ) .style(Style::default().fg(Color::Gray)) .block(Block::default().borders(Borders::ALL).title("β„Ή Help")) .wrap(Wrap { trim: true }); f.render_widget(help, chunks[2]); } fn main() -> Result<(), Box> { println!("πŸ§ͺ Feature 5: External validation (UI-only) + Feature 4: Custom formatter"); println!("βœ… validation feature: ENABLED"); println!("βœ… gui feature: ENABLED"); 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 = DemoData::new(); let editor = DemoEditor::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!("πŸŽ‰ Example finished"); Ok(()) }