Files
komp_ac/canvas/examples/validation_5.rs
2025-08-07 00:03:11 +02:00

449 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<canvas::ValidationConfig> {
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<D: DataProvider> {
editor: FormEditor<D>,
debug_message: String,
show_raw_hint: bool, // panel-only toggle, does not affect canvas
}
impl<D: DataProvider> DemoEditor<D> {
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<B: Backend>(
terminal: &mut Terminal<B>,
mut editor: DemoEditor<DemoData>,
) -> 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<DemoData>) {
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<DemoData>) {
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<dyn std::error::Error>> {
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(())
}