Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e3c85991c | ||
|
|
d3e5418221 | ||
|
|
0d0e54032c | ||
|
|
a8de16f66d | ||
|
|
5b2e0e976f | ||
|
|
d601134535 |
@@ -34,6 +34,7 @@ gui = ["ratatui", "crossterm"]
|
|||||||
suggestions = ["tokio"]
|
suggestions = ["tokio"]
|
||||||
cursor-style = ["crossterm"]
|
cursor-style = ["crossterm"]
|
||||||
validation = ["regex"]
|
validation = ["regex"]
|
||||||
|
computed = []
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "suggestions"
|
name = "suggestions"
|
||||||
@@ -42,7 +43,7 @@ path = "examples/suggestions.rs"
|
|||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "canvas_gui_demo"
|
name = "canvas_gui_demo"
|
||||||
required-features = ["gui"]
|
required-features = ["gui", "cursor-style"]
|
||||||
path = "examples/canvas_gui_demo.rs"
|
path = "examples/canvas_gui_demo.rs"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
@@ -64,3 +65,7 @@ required-features = ["gui", "validation"]
|
|||||||
[[example]]
|
[[example]]
|
||||||
name = "validation_5"
|
name = "validation_5"
|
||||||
required-features = ["gui", "validation"]
|
required-features = ["gui", "validation"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "computed_fields"
|
||||||
|
required-features = ["gui", "computed"]
|
||||||
|
|||||||
620
canvas/examples/computed_fields.rs
Normal file
620
canvas/examples/computed_fields.rs
Normal file
@@ -0,0 +1,620 @@
|
|||||||
|
// examples/computed_fields.rs - COMPLETE WORKING VERSION
|
||||||
|
//! Demonstrates computed fields with the canvas library - Invoice Calculator Example
|
||||||
|
//!
|
||||||
|
//! This example REQUIRES the `computed` feature to compile.
|
||||||
|
//!
|
||||||
|
//! Run with:
|
||||||
|
//! cargo run --example computed_fields --features "gui,computed"
|
||||||
|
|
||||||
|
#[cfg(not(feature = "computed"))]
|
||||||
|
compile_error!(
|
||||||
|
"This example requires the 'computed' feature. \
|
||||||
|
Run with: cargo run --example computed_fields --features \"gui,computed\""
|
||||||
|
);
|
||||||
|
|
||||||
|
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, Modifier},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, Borders, Paragraph, Wrap},
|
||||||
|
Frame, Terminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
use canvas::{
|
||||||
|
canvas::{gui::render_canvas_default, modes::AppMode},
|
||||||
|
DataProvider, FormEditor,
|
||||||
|
computed::{ComputedProvider, ComputedContext},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Invoice data with computed fields
|
||||||
|
struct InvoiceData {
|
||||||
|
fields: Vec<(String, String)>,
|
||||||
|
computed_indices: std::collections::HashSet<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InvoiceData {
|
||||||
|
fn new() -> Self {
|
||||||
|
let mut computed_indices = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
// Mark computed fields (read-only, calculated)
|
||||||
|
computed_indices.insert(4); // Subtotal
|
||||||
|
computed_indices.insert(5); // Tax Amount
|
||||||
|
computed_indices.insert(6); // Total
|
||||||
|
|
||||||
|
Self {
|
||||||
|
fields: vec![
|
||||||
|
("📦 Product Name".to_string(), "".to_string()),
|
||||||
|
("🔢 Quantity".to_string(), "".to_string()),
|
||||||
|
("💰 Unit Price ($)".to_string(), "".to_string()),
|
||||||
|
("📊 Tax Rate (%)".to_string(), "".to_string()),
|
||||||
|
("➕ Subtotal ($)".to_string(), "".to_string()), // COMPUTED
|
||||||
|
("🧾 Tax Amount ($)".to_string(), "".to_string()), // COMPUTED
|
||||||
|
("💳 Total ($)".to_string(), "".to_string()), // COMPUTED
|
||||||
|
("📝 Notes".to_string(), "".to_string()),
|
||||||
|
],
|
||||||
|
computed_indices,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataProvider for InvoiceData {
|
||||||
|
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) {
|
||||||
|
// 🔥 FIXED: Allow computed fields to be updated for display purposes
|
||||||
|
// The editing protection happens at the editor level, not here
|
||||||
|
self.fields[index].1 = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_suggestions(&self, _field_index: usize) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_value(&self, _index: usize) -> Option<&str> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark computed fields
|
||||||
|
fn is_computed_field(&self, field_index: usize) -> bool {
|
||||||
|
self.computed_indices.contains(&field_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get computed field values
|
||||||
|
fn computed_field_value(&self, field_index: usize) -> Option<String> {
|
||||||
|
if self.computed_indices.contains(&field_index) {
|
||||||
|
Some(self.fields[field_index].1.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invoice calculator - computes totals based on input fields
|
||||||
|
struct InvoiceCalculator;
|
||||||
|
|
||||||
|
impl ComputedProvider for InvoiceCalculator {
|
||||||
|
fn compute_field(&mut self, context: ComputedContext) -> String {
|
||||||
|
// Helper to parse field values safely
|
||||||
|
let parse_field = |index: usize| -> f64 {
|
||||||
|
let value = context.field_values[index].trim();
|
||||||
|
if value.is_empty() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
value.parse().unwrap_or(0.0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match context.target_field {
|
||||||
|
4 => {
|
||||||
|
// Subtotal = Quantity × Unit Price
|
||||||
|
let qty = parse_field(1);
|
||||||
|
let price = parse_field(2);
|
||||||
|
let subtotal = qty * price;
|
||||||
|
|
||||||
|
if qty == 0.0 || price == 0.0 {
|
||||||
|
"".to_string() // Show empty if no meaningful calculation
|
||||||
|
} else {
|
||||||
|
format!("{:.2}", subtotal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
5 => {
|
||||||
|
// Tax Amount = Subtotal × (Tax Rate / 100)
|
||||||
|
let qty = parse_field(1);
|
||||||
|
let price = parse_field(2);
|
||||||
|
let tax_rate = parse_field(3);
|
||||||
|
let subtotal = qty * price;
|
||||||
|
let tax_amount = subtotal * (tax_rate / 100.0);
|
||||||
|
|
||||||
|
if subtotal == 0.0 || tax_rate == 0.0 {
|
||||||
|
"".to_string()
|
||||||
|
} else {
|
||||||
|
format!("{:.2}", tax_amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
6 => {
|
||||||
|
// Total = Subtotal + Tax Amount
|
||||||
|
let qty = parse_field(1);
|
||||||
|
let price = parse_field(2);
|
||||||
|
let tax_rate = parse_field(3);
|
||||||
|
let subtotal = qty * price;
|
||||||
|
|
||||||
|
if subtotal == 0.0 {
|
||||||
|
"".to_string()
|
||||||
|
} else {
|
||||||
|
let tax_amount = subtotal * (tax_rate / 100.0);
|
||||||
|
let total = subtotal + tax_amount;
|
||||||
|
format!("{:.2}", total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handles_field(&self, field_index: usize) -> bool {
|
||||||
|
matches!(field_index, 4 | 5 | 6) // Subtotal, Tax Amount, Total
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_dependencies(&self, field_index: usize) -> Vec<usize> {
|
||||||
|
match field_index {
|
||||||
|
4 => vec![1, 2], // Subtotal depends on Quantity, Unit Price
|
||||||
|
5 => vec![1, 2, 3], // Tax Amount depends on Quantity, Unit Price, Tax Rate
|
||||||
|
6 => vec![1, 2, 3], // Total depends on Quantity, Unit Price, Tax Rate
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enhanced editor with computed fields
|
||||||
|
struct ComputedFieldsEditor<D: DataProvider> {
|
||||||
|
editor: FormEditor<D>,
|
||||||
|
calculator: InvoiceCalculator,
|
||||||
|
debug_message: String,
|
||||||
|
last_computed_values: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: DataProvider> ComputedFieldsEditor<D> {
|
||||||
|
fn new(data_provider: D) -> Self {
|
||||||
|
let mut editor = FormEditor::new(data_provider);
|
||||||
|
editor.set_computed_provider(InvoiceCalculator);
|
||||||
|
|
||||||
|
let calculator = InvoiceCalculator;
|
||||||
|
let last_computed_values = vec!["".to_string(); 8];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
editor,
|
||||||
|
calculator,
|
||||||
|
debug_message: "💰 Invoice Calculator - Start typing in fields to see calculations!".to_string(),
|
||||||
|
last_computed_values,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_computed_field(&self, field_index: usize) -> bool {
|
||||||
|
self.editor.ui_state().is_computed_field(field_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_computed_fields(&mut self) {
|
||||||
|
// Trigger recomputation of all computed fields
|
||||||
|
self.editor.recompute_all_fields(&mut self.calculator);
|
||||||
|
|
||||||
|
// 🔥 CRITICAL FIX: Sync computed values to DataProvider so GUI shows them!
|
||||||
|
for i in [4, 5, 6] { // Computed field indices
|
||||||
|
let computed_value = self.editor.effective_field_value(i);
|
||||||
|
self.editor.data_provider_mut().set_field_value(i, computed_value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if values changed to show feedback
|
||||||
|
let mut changed = false;
|
||||||
|
let mut has_calculations = false;
|
||||||
|
|
||||||
|
for i in [4, 5, 6] {
|
||||||
|
let new_value = self.editor.effective_field_value(i);
|
||||||
|
if new_value != self.last_computed_values[i] {
|
||||||
|
changed = true;
|
||||||
|
self.last_computed_values[i] = new_value.clone();
|
||||||
|
}
|
||||||
|
if !new_value.is_empty() {
|
||||||
|
has_calculations = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
if has_calculations {
|
||||||
|
let subtotal = &self.last_computed_values[4];
|
||||||
|
let tax = &self.last_computed_values[5];
|
||||||
|
let total = &self.last_computed_values[6];
|
||||||
|
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
if !subtotal.is_empty() {
|
||||||
|
parts.push(format!("Subtotal=${}", subtotal));
|
||||||
|
}
|
||||||
|
if !tax.is_empty() {
|
||||||
|
parts.push(format!("Tax=${}", tax));
|
||||||
|
}
|
||||||
|
if !total.is_empty() {
|
||||||
|
parts.push(format!("Total=${}", total));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parts.is_empty() {
|
||||||
|
self.debug_message = format!("🧮 Calculated: {}", parts.join(", "));
|
||||||
|
} else {
|
||||||
|
self.debug_message = "💰 Enter Quantity and Unit Price to see calculations".to_string();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.debug_message = "💰 Enter Quantity and Unit Price to see calculations".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_char(&mut self, ch: char) -> anyhow::Result<()> {
|
||||||
|
let current_field = self.editor.current_field();
|
||||||
|
let result = self.editor.insert_char(ch);
|
||||||
|
|
||||||
|
if result.is_ok() && matches!(current_field, 1 | 2 | 3) {
|
||||||
|
self.editor.on_field_changed(&mut self.calculator, current_field);
|
||||||
|
self.update_computed_fields();
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_backward(&mut self) -> anyhow::Result<()> {
|
||||||
|
let current_field = self.editor.current_field();
|
||||||
|
let result = self.editor.delete_backward();
|
||||||
|
|
||||||
|
if result.is_ok() && matches!(current_field, 1 | 2 | 3) {
|
||||||
|
self.editor.on_field_changed(&mut self.calculator, current_field);
|
||||||
|
self.update_computed_fields();
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_forward(&mut self) -> anyhow::Result<()> {
|
||||||
|
let current_field = self.editor.current_field();
|
||||||
|
let result = self.editor.delete_forward();
|
||||||
|
|
||||||
|
if result.is_ok() && matches!(current_field, 1 | 2 | 3) {
|
||||||
|
self.editor.on_field_changed(&mut self.calculator, current_field);
|
||||||
|
self.update_computed_fields();
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_field(&mut self) {
|
||||||
|
let old_field = self.editor.current_field();
|
||||||
|
let _ = self.editor.next_field();
|
||||||
|
let new_field = self.editor.current_field();
|
||||||
|
|
||||||
|
if old_field != new_field {
|
||||||
|
let field_name = self.editor.data_provider().field_name(new_field);
|
||||||
|
let field_type = if self.is_computed_field(new_field) {
|
||||||
|
"computed (read-only)"
|
||||||
|
} else {
|
||||||
|
"editable"
|
||||||
|
};
|
||||||
|
self.debug_message = format!("→ {} - {} field", field_name, field_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev_field(&mut self) {
|
||||||
|
let old_field = self.editor.current_field();
|
||||||
|
let _ = self.editor.prev_field();
|
||||||
|
let new_field = self.editor.current_field();
|
||||||
|
|
||||||
|
if old_field != new_field {
|
||||||
|
let field_name = self.editor.data_provider().field_name(new_field);
|
||||||
|
let field_type = if self.is_computed_field(new_field) {
|
||||||
|
"computed (read-only)"
|
||||||
|
} else {
|
||||||
|
"editable"
|
||||||
|
};
|
||||||
|
self.debug_message = format!("← {} - {} field", field_name, field_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enter_edit_mode(&mut self) {
|
||||||
|
let current = self.editor.current_field();
|
||||||
|
|
||||||
|
// Double protection: check both ways
|
||||||
|
if self.editor.data_provider().is_computed_field(current) || self.is_computed_field(current) {
|
||||||
|
let field_name = self.editor.data_provider().field_name(current);
|
||||||
|
self.debug_message = format!(
|
||||||
|
"🚫 {} is computed (read-only) - Press Tab to move to editable fields",
|
||||||
|
field_name
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.editor.enter_edit_mode();
|
||||||
|
let field_name = self.editor.data_provider().field_name(current);
|
||||||
|
self.debug_message = format!("✏️ Editing {} - Type to see calculations update", field_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enter_append_mode(&mut self) {
|
||||||
|
let current = self.editor.current_field();
|
||||||
|
|
||||||
|
if self.editor.data_provider().is_computed_field(current) || self.is_computed_field(current) {
|
||||||
|
let field_name = self.editor.data_provider().field_name(current);
|
||||||
|
self.debug_message = format!(
|
||||||
|
"🚫 {} is computed (read-only) - Press Tab to move to editable fields",
|
||||||
|
field_name
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.editor.enter_append_mode();
|
||||||
|
let field_name = self.editor.data_provider().field_name(current);
|
||||||
|
self.debug_message = format!("✏️ Appending to {} - Type to see calculations", field_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit_edit_mode(&mut self) {
|
||||||
|
let current_field = self.editor.current_field();
|
||||||
|
self.editor.exit_edit_mode();
|
||||||
|
|
||||||
|
if matches!(current_field, 1 | 2 | 3) {
|
||||||
|
self.editor.on_field_changed(&mut self.calculator, current_field);
|
||||||
|
self.update_computed_fields();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.debug_message = "🔒 Normal mode - Press 'i' to edit fields".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate methods
|
||||||
|
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 move_left(&mut self) { self.editor.move_left(); }
|
||||||
|
fn move_right(&mut self) { self.editor.move_right(); }
|
||||||
|
fn move_up(&mut self) { let _ = self.editor.move_up(); }
|
||||||
|
fn move_down(&mut self) { let _ = self.editor.move_down(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key_press(
|
||||||
|
key: KeyCode,
|
||||||
|
modifiers: KeyModifiers,
|
||||||
|
editor: &mut ComputedFieldsEditor<InvoiceData>,
|
||||||
|
) -> anyhow::Result<bool> {
|
||||||
|
let mode = editor.mode();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
(AppMode::ReadOnly, KeyCode::Char('i'), _) => {
|
||||||
|
editor.enter_edit_mode();
|
||||||
|
}
|
||||||
|
(AppMode::ReadOnly, KeyCode::Char('a'), _) => {
|
||||||
|
editor.enter_append_mode();
|
||||||
|
}
|
||||||
|
(AppMode::ReadOnly, KeyCode::Char('A'), _) => {
|
||||||
|
editor.editor.move_line_end();
|
||||||
|
editor.enter_edit_mode();
|
||||||
|
}
|
||||||
|
(_, KeyCode::Esc, _) => {
|
||||||
|
if mode == AppMode::Edit {
|
||||||
|
editor.exit_edit_mode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Movement
|
||||||
|
(AppMode::ReadOnly, KeyCode::Char('h'), _) | (AppMode::ReadOnly, KeyCode::Left, _) => {
|
||||||
|
editor.move_left();
|
||||||
|
}
|
||||||
|
(AppMode::ReadOnly, KeyCode::Char('l'), _) | (AppMode::ReadOnly, KeyCode::Right, _) => {
|
||||||
|
editor.move_right();
|
||||||
|
}
|
||||||
|
(AppMode::ReadOnly, KeyCode::Char('j'), _) | (AppMode::ReadOnly, KeyCode::Down, _) => {
|
||||||
|
editor.move_down();
|
||||||
|
}
|
||||||
|
(AppMode::ReadOnly, KeyCode::Char('k'), _) | (AppMode::ReadOnly, KeyCode::Up, _) => {
|
||||||
|
editor.move_up();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(); }
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
(_, 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()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug info
|
||||||
|
(AppMode::ReadOnly, KeyCode::Char('?'), _) => {
|
||||||
|
let current = editor.current_field();
|
||||||
|
let field_name = editor.data_provider().field_name(current);
|
||||||
|
let field_type = if editor.is_computed_field(current) {
|
||||||
|
"COMPUTED (read-only)"
|
||||||
|
} else {
|
||||||
|
"EDITABLE"
|
||||||
|
};
|
||||||
|
editor.debug_message = format!(
|
||||||
|
"{} - {} - Position {} - Mode: {:?}",
|
||||||
|
field_name, field_type, editor.cursor_position(), mode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_app<B: Backend>(
|
||||||
|
terminal: &mut Terminal<B>,
|
||||||
|
mut editor: ComputedFieldsEditor<InvoiceData>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
editor.update_computed_fields(); // Initial computation
|
||||||
|
|
||||||
|
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: &ComputedFieldsEditor<InvoiceData>) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(8), Constraint::Length(10)])
|
||||||
|
.split(f.area());
|
||||||
|
|
||||||
|
render_enhanced_canvas(f, chunks[0], editor);
|
||||||
|
render_computed_status(f, chunks[1], editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_enhanced_canvas(f: &mut Frame, area: Rect, editor: &ComputedFieldsEditor<InvoiceData>) {
|
||||||
|
render_canvas_default(f, area, &editor.editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_computed_status(f: &mut Frame, area: Rect, editor: &ComputedFieldsEditor<InvoiceData>) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Length(7)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let mode_text = match editor.mode() {
|
||||||
|
AppMode::Edit => "INSERT",
|
||||||
|
AppMode::ReadOnly => "NORMAL",
|
||||||
|
_ => "OTHER",
|
||||||
|
};
|
||||||
|
|
||||||
|
let current = editor.current_field();
|
||||||
|
let field_status = if editor.is_computed_field(current) {
|
||||||
|
"📊 COMPUTED FIELD (read-only)"
|
||||||
|
} else {
|
||||||
|
"✏️ EDITABLE FIELD"
|
||||||
|
};
|
||||||
|
|
||||||
|
let status_text = format!("-- {} -- {} | {}", mode_text, field_status, editor.debug_message);
|
||||||
|
|
||||||
|
let status = Paragraph::new(Line::from(Span::raw(status_text)))
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("💰 Invoice Calculator"));
|
||||||
|
|
||||||
|
f.render_widget(status, chunks[0]);
|
||||||
|
|
||||||
|
let help_text = match editor.mode() {
|
||||||
|
AppMode::ReadOnly => {
|
||||||
|
"💰 COMPUTED FIELDS DEMO: Real-time invoice calculations!\n\
|
||||||
|
🔢 EDITABLE: Product, Quantity, Unit Price, Tax Rate, Notes\n\
|
||||||
|
📊 COMPUTED: Subtotal, Tax Amount, Total (calculated automatically)\n\
|
||||||
|
\n\
|
||||||
|
🚀 START: Press 'i' to edit Quantity, type '5', Tab to Unit Price, type '19.99'\n\
|
||||||
|
Watch Subtotal and Total appear! Add Tax Rate to see tax calculations.\n\
|
||||||
|
Navigation: Tab/Shift+Tab skips computed fields automatically"
|
||||||
|
}
|
||||||
|
AppMode::Edit => {
|
||||||
|
"✏️ EDIT MODE: Type numbers to see calculations appear!\n\
|
||||||
|
\n\
|
||||||
|
💡 EXAMPLE: Type '5' in Quantity, then Tab to Unit Price and type '19.99'\n\
|
||||||
|
• Subtotal appears: $99.95\n\
|
||||||
|
• Total appears: $99.95\n\
|
||||||
|
• Add Tax Rate (like '10') to see tax: $9.99, Total: $109.94\n\
|
||||||
|
\n\
|
||||||
|
Esc=normal, Tab=next field (auto-skips computed fields)"
|
||||||
|
}
|
||||||
|
_ => "💰 Invoice Calculator with Computed Fields"
|
||||||
|
};
|
||||||
|
|
||||||
|
let help_style = if editor.is_computed_field(editor.current_field()) {
|
||||||
|
Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::Gray)
|
||||||
|
};
|
||||||
|
|
||||||
|
let help = Paragraph::new(help_text)
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("🚀 Try It Now!"))
|
||||||
|
.style(help_style)
|
||||||
|
.wrap(Wrap { trim: true });
|
||||||
|
|
||||||
|
f.render_widget(help, chunks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("💰 Canvas Computed Fields Demo - Invoice Calculator");
|
||||||
|
println!("✅ computed feature: ENABLED");
|
||||||
|
println!("🚀 QUICK TEST:");
|
||||||
|
println!(" 1. Press 'i' to edit Quantity");
|
||||||
|
println!(" 2. Type '5' and press Tab");
|
||||||
|
println!(" 3. Type '19.99' in Unit Price");
|
||||||
|
println!(" 4. Watch Subtotal ($99.95) and Total ($99.95) appear!");
|
||||||
|
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 = InvoiceData::new();
|
||||||
|
let editor = ComputedFieldsEditor::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!("💰 Demo completed! Computed fields should have updated in real-time!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
1084
canvas/examples/suggestions2.rs
Normal file
1084
canvas/examples/suggestions2.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
// examples/validation_1.rs
|
// examples/validation_1.rs
|
||||||
//! Demonstrates field validation with the canvas library
|
//! Demonstrates field validation with the canvas library
|
||||||
//!
|
//!
|
||||||
//! This example REQUIRES the `validation` feature to compile.
|
//! This example REQUIRES the `validation` and `cursor-style` features to compile.
|
||||||
//!
|
//!
|
||||||
//! Run with:
|
//! Run with:
|
||||||
//! cargo run --example validation_1 --features "gui,validation"
|
//! cargo run --example validation_1 --features "gui,validation"
|
||||||
@@ -10,10 +10,10 @@
|
|||||||
//! cargo run --example validation_1 --features "gui"
|
//! cargo run --example validation_1 --features "gui"
|
||||||
|
|
||||||
// REQUIRE validation feature - example won't compile without it
|
// REQUIRE validation feature - example won't compile without it
|
||||||
#[cfg(not(feature = "validation"))]
|
#[cfg(not(all(feature = "validation", feature = "cursor-style")))]
|
||||||
compile_error!(
|
compile_error!(
|
||||||
"This example requires the 'validation' feature. \
|
"This example requires the 'validation' and 'cursor-style' features. \
|
||||||
Run with: cargo run --example validation_1 --features \"gui,validation\""
|
Run with: cargo run --example validation_1 --features \"gui,validation,cursor-style\""
|
||||||
);
|
);
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -39,6 +39,7 @@ use canvas::{
|
|||||||
canvas::{
|
canvas::{
|
||||||
gui::render_canvas_default,
|
gui::render_canvas_default,
|
||||||
modes::AppMode,
|
modes::AppMode,
|
||||||
|
CursorManager,
|
||||||
},
|
},
|
||||||
DataProvider, FormEditor,
|
DataProvider, FormEditor,
|
||||||
ValidationConfig, ValidationConfigBuilder, CharacterLimits, ValidationResult,
|
ValidationConfig, ValidationConfigBuilder, CharacterLimits, ValidationResult,
|
||||||
@@ -269,18 +270,21 @@ impl<D: DataProvider> ValidationFormEditor<D> {
|
|||||||
|
|
||||||
// === MODE TRANSITIONS ===
|
// === MODE TRANSITIONS ===
|
||||||
fn enter_edit_mode(&mut self) {
|
fn enter_edit_mode(&mut self) {
|
||||||
|
// Library will automatically update cursor to bar | in insert mode
|
||||||
self.editor.enter_edit_mode();
|
self.editor.enter_edit_mode();
|
||||||
self.debug_message = "✏️ INSERT MODE - Type to test validation".to_string();
|
self.debug_message = "✏️ INSERT MODE - Cursor: Steady Bar | - Type to test validation".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enter_append_mode(&mut self) {
|
fn enter_append_mode(&mut self) {
|
||||||
|
// Library will automatically update cursor to bar | in insert mode
|
||||||
self.editor.enter_append_mode();
|
self.editor.enter_append_mode();
|
||||||
self.debug_message = "✏️ INSERT (append) - Validation active".to_string();
|
self.debug_message = "✏️ INSERT (append) - Cursor: Steady Bar | - Validation active".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit_edit_mode(&mut self) {
|
fn exit_edit_mode(&mut self) {
|
||||||
|
// Library will automatically update cursor to block █ in normal mode
|
||||||
self.editor.exit_edit_mode();
|
self.editor.exit_edit_mode();
|
||||||
self.debug_message = "🔒 NORMAL MODE - Press 'v' to validate current field".to_string();
|
self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █ - Press 'v' to validate current field".to_string();
|
||||||
self.update_field_validation_status();
|
self.update_field_validation_status();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,6 +366,7 @@ impl<D: DataProvider> ValidationFormEditor<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_mode(&mut self, mode: AppMode) {
|
fn set_mode(&mut self, mode: AppMode) {
|
||||||
|
// Library automatically updates cursor for the mode
|
||||||
self.editor.set_mode(mode);
|
self.editor.set_mode(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,9 +699,9 @@ fn render_validation_status(
|
|||||||
|
|
||||||
// Status bar with validation information
|
// Status bar with validation information
|
||||||
let mode_text = match editor.mode() {
|
let mode_text = match editor.mode() {
|
||||||
AppMode::Edit => "INSERT",
|
AppMode::Edit => "INSERT | (bar cursor)",
|
||||||
AppMode::ReadOnly => "NORMAL",
|
AppMode::ReadOnly => "NORMAL █ (block cursor)",
|
||||||
_ => "OTHER",
|
_ => "NORMAL █ (block cursor)",
|
||||||
};
|
};
|
||||||
|
|
||||||
let validation_status = editor.get_validation_status();
|
let validation_status = editor.get_validation_status();
|
||||||
@@ -765,21 +770,21 @@ fn render_validation_status(
|
|||||||
// Enhanced help text
|
// Enhanced help text
|
||||||
let help_text = match editor.mode() {
|
let help_text = match editor.mode() {
|
||||||
AppMode::ReadOnly => {
|
AppMode::ReadOnly => {
|
||||||
"🔍 VALIDATION DEMO: Different fields have different limits!\n\
|
"🎯 CURSOR-STYLE: Normal █ | Insert |\n\
|
||||||
Fields with MINIMUM requirements will block field switching if too short!\n\
|
🔍 VALIDATION: Different fields have different limits (some block field switching)!\n\
|
||||||
Movement: hjkl/arrows=move, Tab/Shift+Tab=fields\n\
|
Movement: hjkl/arrows=move, Tab/Shift+Tab=fields\n\
|
||||||
Edit: i/a/A=insert modes, Esc=normal\n\
|
Edit: i/a/A=insert modes, Esc=normal\n\
|
||||||
Validation: v=validate current, V=validate all, c=clear results, F1=toggle\n\
|
Validation: v=validate current, V=validate all, c=clear results, F1=toggle\n\
|
||||||
?=info, Ctrl+C/Ctrl+Q=quit"
|
?=info, Ctrl+C/Ctrl+Q=quit"
|
||||||
}
|
}
|
||||||
AppMode::Edit => {
|
AppMode::Edit => {
|
||||||
"✏️ INSERT MODE - Type to test validation limits!\n\
|
"🎯 INSERT MODE - Cursor: | (bar)\n\
|
||||||
Some fields have MINIMUM character requirements!\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\
|
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\
|
arrows=move, Backspace/Del=delete, Esc=normal, Tab=next field\n\
|
||||||
Field switching may be BLOCKED if minimum requirements not met!"
|
Field switching may be BLOCKED if minimum requirements not met!"
|
||||||
}
|
}
|
||||||
_ => "🔍 Validation Demo Active!"
|
_ => "🎯 Watch the cursor change automatically while validating!"
|
||||||
};
|
};
|
||||||
|
|
||||||
let help = Paragraph::new(help_text)
|
let help = Paragraph::new(help_text)
|
||||||
@@ -810,10 +815,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
let data = ValidationDemoData::new();
|
let data = ValidationDemoData::new();
|
||||||
let editor = ValidationFormEditor::new(data);
|
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);
|
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()?;
|
disable_raw_mode()?;
|
||||||
execute!(
|
execute!(
|
||||||
terminal.backend_mut(),
|
terminal.backend_mut(),
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
//! This example showcases the full potential of the pattern validation system
|
//! This example showcases the full potential of the pattern validation system
|
||||||
//! with creative real-world scenarios and edge cases.
|
//! with creative real-world scenarios and edge cases.
|
||||||
//!
|
//!
|
||||||
//! Run with: cargo run --example validation_advanced_patterns --features "validation,gui"
|
//! Run with: cargo run --example validation_advanced_patterns --features "validation,gui,cursor-style"
|
||||||
|
|
||||||
// REQUIRE validation and gui features
|
// REQUIRE validation, gui and cursor-style features
|
||||||
#[cfg(not(all(feature = "validation", feature = "gui")))]
|
#[cfg(not(all(feature = "validation", feature = "gui", feature = "cursor-style")))]
|
||||||
compile_error!(
|
compile_error!(
|
||||||
"This example requires the 'validation' and 'gui' features. \
|
"This example requires the 'validation', 'gui' and 'cursor-style' features. \
|
||||||
Run with: cargo run --example validation_advanced_patterns --features \"validation,gui\""
|
Run with: cargo run --example validation_advanced_patterns --features \"validation,gui,cursor-style\""
|
||||||
);
|
);
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -38,6 +38,7 @@ use canvas::{
|
|||||||
canvas::{
|
canvas::{
|
||||||
gui::render_canvas_default,
|
gui::render_canvas_default,
|
||||||
modes::AppMode,
|
modes::AppMode,
|
||||||
|
CursorManager,
|
||||||
},
|
},
|
||||||
DataProvider, FormEditor,
|
DataProvider, FormEditor,
|
||||||
ValidationConfig, ValidationConfigBuilder, PatternFilters, PositionFilter, PositionRange, CharacterFilter,
|
ValidationConfig, ValidationConfigBuilder, PatternFilters, PositionFilter, PositionRange, CharacterFilter,
|
||||||
@@ -107,18 +108,21 @@ impl<D: DataProvider> AdvancedPatternFormEditor<D> {
|
|||||||
fn move_line_end(&mut self) { self.editor.move_line_end(); }
|
fn move_line_end(&mut self) { self.editor.move_line_end(); }
|
||||||
|
|
||||||
fn enter_edit_mode(&mut self) {
|
fn enter_edit_mode(&mut self) {
|
||||||
|
// Library will automatically update cursor to bar | in insert mode
|
||||||
self.editor.enter_edit_mode();
|
self.editor.enter_edit_mode();
|
||||||
self.debug_message = "✏️ INSERT MODE - Testing advanced pattern validation".to_string();
|
self.debug_message = "✏️ INSERT MODE - Cursor: Steady Bar | - Testing advanced pattern validation".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enter_append_mode(&mut self) {
|
fn enter_append_mode(&mut self) {
|
||||||
|
// Library will automatically update cursor to bar | in insert mode
|
||||||
self.editor.enter_append_mode();
|
self.editor.enter_append_mode();
|
||||||
self.debug_message = "✏️ INSERT (append) - Advanced patterns active".to_string();
|
self.debug_message = "✏️ INSERT (append) - Cursor: Steady Bar | - Advanced patterns active".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit_edit_mode(&mut self) {
|
fn exit_edit_mode(&mut self) {
|
||||||
|
// Library will automatically update cursor to block █ in normal mode
|
||||||
self.editor.exit_edit_mode();
|
self.editor.exit_edit_mode();
|
||||||
self.debug_message = "🔒 NORMAL MODE".to_string();
|
self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █".to_string();
|
||||||
self.update_field_validation_status();
|
self.update_field_validation_status();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,9 +526,9 @@ fn render_advanced_validation_status(
|
|||||||
|
|
||||||
// Status bar
|
// Status bar
|
||||||
let mode_text = match editor.mode() {
|
let mode_text = match editor.mode() {
|
||||||
AppMode::Edit => "INSERT",
|
AppMode::Edit => "INSERT | (bar cursor)",
|
||||||
AppMode::ReadOnly => "NORMAL",
|
AppMode::ReadOnly => "NORMAL █ (block cursor)",
|
||||||
_ => "OTHER",
|
_ => "NORMAL █ (block cursor)",
|
||||||
};
|
};
|
||||||
|
|
||||||
let validation_status = editor.get_validation_status();
|
let validation_status = editor.get_validation_status();
|
||||||
@@ -613,6 +617,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
println!("🚀 Canvas Advanced Pattern Validation Demo");
|
println!("🚀 Canvas Advanced Pattern Validation Demo");
|
||||||
println!("✅ validation feature: ENABLED");
|
println!("✅ validation feature: ENABLED");
|
||||||
println!("✅ gui feature: ENABLED");
|
println!("✅ gui feature: ENABLED");
|
||||||
|
println!("✅ cursor-style feature: ENABLED");
|
||||||
println!("🎯 Advanced pattern filtering: ACTIVE");
|
println!("🎯 Advanced pattern filtering: ACTIVE");
|
||||||
println!("🧪 Edge cases and complex patterns: READY");
|
println!("🧪 Edge cases and complex patterns: READY");
|
||||||
println!("💡 Each field showcases different validation capabilities!");
|
println!("💡 Each field showcases different validation capabilities!");
|
||||||
@@ -625,10 +630,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
let data = AdvancedPatternData::new();
|
let data = AdvancedPatternData::new();
|
||||||
let editor = AdvancedPatternFormEditor::new(data);
|
let mut editor = AdvancedPatternFormEditor::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);
|
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()?;
|
disable_raw_mode()?;
|
||||||
execute!(
|
execute!(
|
||||||
terminal.backend_mut(),
|
terminal.backend_mut(),
|
||||||
|
|||||||
@@ -18,13 +18,13 @@
|
|||||||
//! Each mask's input position count EXACTLY matches its character limit to prevent
|
//! Each mask's input position count EXACTLY matches its character limit to prevent
|
||||||
//! the critical bug where users can type more characters than they can see.
|
//! the critical bug where users can type more characters than they can see.
|
||||||
//!
|
//!
|
||||||
//! Run with: cargo run --example validation_3 --features "gui,validation"
|
//! Run with: cargo run --example validation_3 --features "gui,validation,cursor-style"
|
||||||
|
|
||||||
// REQUIRE validation and gui features for mask functionality
|
// REQUIRE validation, gui and cursor-style features for mask functionality
|
||||||
#[cfg(not(all(feature = "validation", feature = "gui")))]
|
#[cfg(not(all(feature = "validation", feature = "gui", feature = "cursor-style")))]
|
||||||
compile_error!(
|
compile_error!(
|
||||||
"This example requires the 'validation' and 'gui' features. \
|
"This example requires the 'validation', 'gui' and 'cursor-style' features. \
|
||||||
Run with: cargo run --example validation_3 --features \"gui,validation\""
|
Run with: cargo run --example validation_3 --features \"gui,validation,cursor-style\""
|
||||||
);
|
);
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -50,6 +50,7 @@ use canvas::{
|
|||||||
canvas::{
|
canvas::{
|
||||||
gui::render_canvas_default,
|
gui::render_canvas_default,
|
||||||
modes::AppMode,
|
modes::AppMode,
|
||||||
|
CursorManager,
|
||||||
},
|
},
|
||||||
DataProvider, FormEditor,
|
DataProvider, FormEditor,
|
||||||
ValidationConfig, ValidationConfigBuilder, DisplayMask,
|
ValidationConfig, ValidationConfigBuilder, DisplayMask,
|
||||||
@@ -183,18 +184,21 @@ impl<D: DataProvider> MaskDemoFormEditor<D> {
|
|||||||
|
|
||||||
// === MODE TRANSITIONS ===
|
// === MODE TRANSITIONS ===
|
||||||
fn enter_edit_mode(&mut self) {
|
fn enter_edit_mode(&mut self) {
|
||||||
|
// Library will automatically update cursor to bar | in insert mode
|
||||||
self.editor.enter_edit_mode();
|
self.editor.enter_edit_mode();
|
||||||
self.debug_message = "✏️ INSERT MODE - Type to see mask formatting in real-time".to_string();
|
self.debug_message = "✏️ INSERT MODE - Cursor: Steady Bar | - Type to see mask formatting in real-time".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enter_append_mode(&mut self) {
|
fn enter_append_mode(&mut self) {
|
||||||
|
// Library will automatically update cursor to bar | in insert mode
|
||||||
self.editor.enter_append_mode();
|
self.editor.enter_append_mode();
|
||||||
self.debug_message = "✏️ INSERT (append) - Mask formatting active".to_string();
|
self.debug_message = "✏️ INSERT (append) - Cursor: Steady Bar | - Mask formatting active".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit_edit_mode(&mut self) {
|
fn exit_edit_mode(&mut self) {
|
||||||
|
// Library will automatically update cursor to block █ in normal mode
|
||||||
self.editor.exit_edit_mode();
|
self.editor.exit_edit_mode();
|
||||||
self.debug_message = "🔒 NORMAL MODE - Press 'r' to see raw data, 'm' for mask info".to_string();
|
self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █ - Press 'r' to see raw data, 'm' for mask info".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_char(&mut self, ch: char) -> anyhow::Result<()> {
|
fn insert_char(&mut self, ch: char) -> anyhow::Result<()> {
|
||||||
@@ -236,7 +240,10 @@ impl<D: DataProvider> MaskDemoFormEditor<D> {
|
|||||||
fn current_text(&self) -> &str { self.editor.current_text() }
|
fn current_text(&self) -> &str { self.editor.current_text() }
|
||||||
fn data_provider(&self) -> &D { self.editor.data_provider() }
|
fn data_provider(&self) -> &D { self.editor.data_provider() }
|
||||||
fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() }
|
fn ui_state(&self) -> &canvas::EditorState { self.editor.ui_state() }
|
||||||
fn set_mode(&mut self, mode: AppMode) { self.editor.set_mode(mode); }
|
fn set_mode(&mut self, mode: AppMode) {
|
||||||
|
// Library automatically updates cursor for the mode
|
||||||
|
self.editor.set_mode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
fn next_field(&mut self) {
|
fn next_field(&mut self) {
|
||||||
match self.editor.next_field() {
|
match self.editor.next_field() {
|
||||||
@@ -582,9 +589,9 @@ fn render_mask_status(
|
|||||||
|
|
||||||
// Status bar with mask information
|
// Status bar with mask information
|
||||||
let mode_text = match editor.mode() {
|
let mode_text = match editor.mode() {
|
||||||
AppMode::Edit => "INSERT",
|
AppMode::Edit => "INSERT | (bar cursor)",
|
||||||
AppMode::ReadOnly => "NORMAL",
|
AppMode::ReadOnly => "NORMAL █ (block cursor)",
|
||||||
_ => "OTHER",
|
_ => "NORMAL █ (block cursor)",
|
||||||
};
|
};
|
||||||
|
|
||||||
let mask_status = editor.get_mask_status();
|
let mask_status = editor.get_mask_status();
|
||||||
@@ -634,7 +641,8 @@ fn render_mask_status(
|
|||||||
// Enhanced help text
|
// Enhanced help text
|
||||||
let help_text = match editor.mode() {
|
let help_text = match editor.mode() {
|
||||||
AppMode::ReadOnly => {
|
AppMode::ReadOnly => {
|
||||||
"🎭 MASK DEMO: See how visual formatting keeps business logic clean!\n\
|
"🎯 CURSOR-STYLE: Normal █ | Insert |\n\
|
||||||
|
🎭 MASK DEMO: Visual formatting keeps business logic clean!\n\
|
||||||
\n\
|
\n\
|
||||||
📱 Try different fields to see various mask patterns:\n\
|
📱 Try different fields to see various mask patterns:\n\
|
||||||
• Dynamic vs Template modes • Custom separators • Different input chars\n\
|
• Dynamic vs Template modes • Custom separators • Different input chars\n\
|
||||||
@@ -644,7 +652,8 @@ fn render_mask_status(
|
|||||||
?=detailed info, Ctrl+C=quit"
|
?=detailed info, Ctrl+C=quit"
|
||||||
}
|
}
|
||||||
AppMode::Edit => {
|
AppMode::Edit => {
|
||||||
"✏️ INSERT MODE - Type to see real-time mask formatting!\n\
|
"🎯 INSERT MODE - Cursor: | (bar)\n\
|
||||||
|
✏️ Type to see real-time mask formatting!\n\
|
||||||
\n\
|
\n\
|
||||||
🔥 Key Features in Action:\n\
|
🔥 Key Features in Action:\n\
|
||||||
• Separators auto-appear as you type • Cursor skips over separators\n\
|
• Separators auto-appear as you type • Cursor skips over separators\n\
|
||||||
@@ -670,6 +679,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
println!("✅ validation feature: ENABLED");
|
println!("✅ validation feature: ENABLED");
|
||||||
println!("✅ gui feature: ENABLED");
|
println!("✅ gui feature: ENABLED");
|
||||||
println!("🎭 Display masks: ACTIVE");
|
println!("🎭 Display masks: ACTIVE");
|
||||||
|
println!("✅ cursor-style feature: ENABLED");
|
||||||
println!("🔥 Key Benefits Demonstrated:");
|
println!("🔥 Key Benefits Demonstrated:");
|
||||||
println!(" • Clean separation: Visual formatting ≠ Business logic");
|
println!(" • Clean separation: Visual formatting ≠ Business logic");
|
||||||
println!(" • User-friendly: Pretty displays with automatic cursor handling");
|
println!(" • User-friendly: Pretty displays with automatic cursor handling");
|
||||||
@@ -690,7 +700,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
let data = MaskDemoData::new();
|
let data = MaskDemoData::new();
|
||||||
let editor = MaskDemoFormEditor::new(data);
|
let mut editor = MaskDemoFormEditor::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);
|
let res = run_app(&mut terminal, editor);
|
||||||
|
|
||||||
@@ -702,6 +718,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
)?;
|
)?;
|
||||||
terminal.show_cursor()?;
|
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 {
|
if let Err(err) = res {
|
||||||
println!("{:?}", err);
|
println!("{:?}", err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
|
|
||||||
#![allow(clippy::needless_return)]
|
#![allow(clippy::needless_return)]
|
||||||
|
|
||||||
#[cfg(not(all(feature = "validation", feature = "gui")))]
|
#[cfg(not(all(feature = "validation", feature = "gui", feature = "cursor-style")))]
|
||||||
compile_error!(
|
compile_error!(
|
||||||
"This example requires the 'validation' and 'gui' features. \
|
"This example requires the 'validation', 'gui' and 'cursor-style' features. \
|
||||||
Run with: cargo run --example validation_4 --features \"gui,validation\""
|
Run with: cargo run --example validation_4 --features \"gui,validation,cursor-style\""
|
||||||
);
|
);
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -39,7 +39,7 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use canvas::{
|
use canvas::{
|
||||||
canvas::{gui::render_canvas_default, modes::AppMode},
|
canvas::{gui::render_canvas_default, modes::AppMode, CursorManager},
|
||||||
DataProvider, FormEditor,
|
DataProvider, FormEditor,
|
||||||
ValidationConfig, ValidationConfigBuilder,
|
ValidationConfig, ValidationConfigBuilder,
|
||||||
CustomFormatter, FormattingResult,
|
CustomFormatter, FormattingResult,
|
||||||
@@ -403,21 +403,23 @@ impl<D: DataProvider> EnhancedDemoEditor<D> {
|
|||||||
|
|
||||||
// Delegate methods with enhanced feedback
|
// Delegate methods with enhanced feedback
|
||||||
fn enter_edit_mode(&mut self) {
|
fn enter_edit_mode(&mut self) {
|
||||||
|
// Library will automatically update cursor to bar | in insert mode
|
||||||
self.editor.enter_edit_mode();
|
self.editor.enter_edit_mode();
|
||||||
let field_type = self.current_field_type();
|
let field_type = self.current_field_type();
|
||||||
let rules = self.get_input_rules();
|
let rules = self.get_input_rules();
|
||||||
self.debug_message = format!("✏️ EDITING {} - {}", field_type, rules);
|
self.debug_message = format!("✏️ INSERT MODE - Cursor: Steady Bar | - {} - {}", field_type, rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit_edit_mode(&mut self) {
|
fn exit_edit_mode(&mut self) {
|
||||||
|
// Library will automatically update cursor to block █ in normal mode
|
||||||
self.editor.exit_edit_mode();
|
self.editor.exit_edit_mode();
|
||||||
let (raw, display, _, warning) = self.get_current_field_analysis();
|
let (raw, display, _, warning) = self.get_current_field_analysis();
|
||||||
if let Some(warn) = warning {
|
if let Some(warn) = warning {
|
||||||
self.debug_message = format!("🔒 NORMAL - {} | ⚠️ {}", self.current_field_type(), warn);
|
self.debug_message = format!("🔒 NORMAL - Cursor: Steady Block █ - {} | ⚠️ {}", self.current_field_type(), warn);
|
||||||
} else if raw != display {
|
} else if raw != display {
|
||||||
self.debug_message = format!("🔒 NORMAL - {} formatted successfully", self.current_field_type());
|
self.debug_message = format!("🔒 NORMAL - Cursor: Steady Block █ - {} formatted successfully", self.current_field_type());
|
||||||
} else {
|
} else {
|
||||||
self.debug_message = "🔒 NORMAL MODE".to_string();
|
self.debug_message = "🔒 NORMAL MODE - Cursor: Steady Block █".to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,9 +590,9 @@ fn render_enhanced_status(
|
|||||||
|
|
||||||
// Status bar
|
// Status bar
|
||||||
let mode_text = match editor.mode() {
|
let mode_text = match editor.mode() {
|
||||||
AppMode::Edit => "INSERT",
|
AppMode::Edit => "INSERT | (bar cursor)",
|
||||||
AppMode::ReadOnly => "NORMAL",
|
AppMode::ReadOnly => "NORMAL █ (block cursor)",
|
||||||
_ => "OTHER",
|
_ => "NORMAL █ (block cursor)",
|
||||||
};
|
};
|
||||||
|
|
||||||
let formatter_count = (0..editor.data_provider().field_count())
|
let formatter_count = (0..editor.data_provider().field_count())
|
||||||
@@ -660,7 +662,8 @@ fn render_enhanced_status(
|
|||||||
// Enhanced help
|
// Enhanced help
|
||||||
let help_text = match editor.mode() {
|
let help_text = match editor.mode() {
|
||||||
AppMode::ReadOnly => {
|
AppMode::ReadOnly => {
|
||||||
"🧩 ENHANCED CUSTOM FORMATTER DEMO\n\
|
"🎯 CURSOR-STYLE: Normal █ | Insert |\n\
|
||||||
|
🧩 ENHANCED CUSTOM FORMATTER DEMO\n\
|
||||||
\n\
|
\n\
|
||||||
Try these formatters:
|
Try these formatters:
|
||||||
• PSC: 01001 → 010 01 | Phone: 1234567890 → (123) 456-7890 | Card: 1234567890123456 → 1234 5678 9012 3456
|
• PSC: 01001 → 010 01 | Phone: 1234567890 → (123) 456-7890 | Card: 1234567890123456 → 1234 5678 9012 3456
|
||||||
@@ -671,7 +674,8 @@ fn render_enhanced_status(
|
|||||||
Ctrl+C/F10=quit"
|
Ctrl+C/F10=quit"
|
||||||
}
|
}
|
||||||
AppMode::Edit => {
|
AppMode::Edit => {
|
||||||
"✏️ INSERT MODE - Real-time formatting as you type!\n\
|
"🎯 INSERT MODE - Cursor: | (bar)\n\
|
||||||
|
✏️ Real-time formatting as you type!\n\
|
||||||
\n\
|
\n\
|
||||||
Current field rules: {}\n\
|
Current field rules: {}\n\
|
||||||
• Raw input is authoritative (what gets stored)\n\
|
• Raw input is authoritative (what gets stored)\n\
|
||||||
@@ -701,6 +705,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
println!("🧩 Enhanced Canvas Custom Formatter Demo (Feature 4)");
|
println!("🧩 Enhanced Canvas Custom Formatter Demo (Feature 4)");
|
||||||
println!("✅ validation feature: ENABLED");
|
println!("✅ validation feature: ENABLED");
|
||||||
println!("✅ gui feature: ENABLED");
|
println!("✅ gui feature: ENABLED");
|
||||||
|
println!("✅ cursor-style feature: ENABLED");
|
||||||
println!("🧩 Enhanced features:");
|
println!("🧩 Enhanced features:");
|
||||||
println!(" • 5 different custom formatters with edge cases");
|
println!(" • 5 different custom formatters with edge cases");
|
||||||
println!(" • Real-time format preview and validation");
|
println!(" • Real-time format preview and validation");
|
||||||
@@ -716,7 +721,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
let data = MultiFormatterDemoData::new();
|
let data = MultiFormatterDemoData::new();
|
||||||
let editor = EnhancedDemoEditor::new(data);
|
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);
|
let res = run_app(&mut terminal, editor);
|
||||||
|
|
||||||
@@ -724,6 +735,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
|
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
|
||||||
terminal.show_cursor()?;
|
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 {
|
if let Err(err) = res {
|
||||||
println!("{:?}", err);
|
println!("{:?}", err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,14 +26,14 @@
|
|||||||
//! - F1: toggle external validation globally
|
//! - F1: toggle external validation globally
|
||||||
//! - F10/Ctrl+C: quit
|
//! - F10/Ctrl+C: quit
|
||||||
//!
|
//!
|
||||||
//! Run: cargo run --example validation_5 --features "gui,validation"
|
//! Run: cargo run --example validation_5 --features "gui,validation,cursor-style"
|
||||||
|
|
||||||
#![allow(clippy::needless_return)]
|
#![allow(clippy::needless_return)]
|
||||||
|
|
||||||
#[cfg(not(all(feature = "validation", feature = "gui")))]
|
#[cfg(not(all(feature = "validation", feature = "gui", feature = "cursor-style")))]
|
||||||
compile_error!(
|
compile_error!(
|
||||||
"This example requires the 'validation' and 'gui' features. \
|
"This example requires the 'validation', 'gui' and 'cursor-style' features. \
|
||||||
Run with: cargo run --example validation_5 --features \"gui,validation\""
|
Run with: cargo run --example validation_5 --features \"gui,validation,cursor-style\""
|
||||||
);
|
);
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -59,7 +59,7 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use canvas::{
|
use canvas::{
|
||||||
canvas::{gui::render_canvas_default, modes::AppMode},
|
canvas::{gui::render_canvas_default, modes::AppMode, CursorManager},
|
||||||
DataProvider, FormEditor,
|
DataProvider, FormEditor,
|
||||||
ValidationConfigBuilder, CustomFormatter, FormattingResult,
|
ValidationConfigBuilder, CustomFormatter, FormattingResult,
|
||||||
validation::ExternalValidationState,
|
validation::ExternalValidationState,
|
||||||
@@ -762,7 +762,7 @@ impl<D: DataProvider> ValidationDemoEditor<D> {
|
|||||||
fn enter_edit_mode(&mut self) {
|
fn enter_edit_mode(&mut self) {
|
||||||
self.editor.enter_edit_mode();
|
self.editor.enter_edit_mode();
|
||||||
let rules = self.field_validation_rules();
|
let rules = self.field_validation_rules();
|
||||||
self.debug_message = format!("✏️ EDITING {} - {}", self.field_type(), rules);
|
self.debug_message = format!("✏️ INSERT MODE - Cursor: Steady Bar | - {} - {}", self.field_type(), rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit_edit_mode(&mut self) {
|
fn exit_edit_mode(&mut self) {
|
||||||
@@ -774,7 +774,7 @@ impl<D: DataProvider> ValidationDemoEditor<D> {
|
|||||||
self.validate_field(current_field);
|
self.validate_field(current_field);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.debug_message = format!("🔒 NORMAL - {}", self.field_type());
|
self.debug_message = format!("🔒 NORMAL - Cursor: Steady Block █ - {}", self.field_type());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_field(&mut self) {
|
fn next_field(&mut self) {
|
||||||
@@ -915,9 +915,9 @@ fn render_validation_panel(
|
|||||||
|
|
||||||
// Status bar
|
// Status bar
|
||||||
let mode_text = match editor.mode() {
|
let mode_text = match editor.mode() {
|
||||||
AppMode::Edit => "INSERT",
|
AppMode::Edit => "INSERT | (bar cursor)",
|
||||||
AppMode::ReadOnly => "NORMAL",
|
AppMode::ReadOnly => "NORMAL █ (block cursor)",
|
||||||
_ => "OTHER",
|
_ => "NORMAL █ (block cursor)",
|
||||||
};
|
};
|
||||||
|
|
||||||
let summary = editor.get_validation_summary();
|
let summary = editor.get_validation_summary();
|
||||||
@@ -1019,7 +1019,8 @@ fn render_validation_panel(
|
|||||||
} else {
|
} else {
|
||||||
let help_text = match editor.mode() {
|
let help_text = match editor.mode() {
|
||||||
AppMode::ReadOnly => {
|
AppMode::ReadOnly => {
|
||||||
"🧪 EXTERNAL VALIDATION DEMO - Multiple validation types with async simulation\n\
|
"🎯 CURSOR-STYLE: Normal █ | Insert |\n\
|
||||||
|
🧪 EXTERNAL VALIDATION DEMO - Multiple validation types with async simulation\n\
|
||||||
\n\
|
\n\
|
||||||
Commands: v=validate current, V=validate all, c=clear current, C=clear all\n\
|
Commands: v=validate current, V=validate all, c=clear current, C=clear all\n\
|
||||||
e=cycle examples, r=toggle history, h=field help, F1=toggle validation\n\
|
e=cycle examples, r=toggle history, h=field help, F1=toggle validation\n\
|
||||||
@@ -1028,7 +1029,8 @@ fn render_validation_panel(
|
|||||||
Try different values to see validation in action!"
|
Try different values to see validation in action!"
|
||||||
}
|
}
|
||||||
AppMode::Edit => {
|
AppMode::Edit => {
|
||||||
"✏️ EDITING MODE - Type to see validation on field blur\n\
|
"🎯 INSERT MODE - Cursor: | (bar)\n\
|
||||||
|
✏️ Type to see validation on field blur\n\
|
||||||
\n\
|
\n\
|
||||||
Current field validation will trigger when you:\n\
|
Current field validation will trigger when you:\n\
|
||||||
• Press Esc (exit edit mode)\n\
|
• Press Esc (exit edit mode)\n\
|
||||||
@@ -1052,6 +1054,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
println!("🧪 Enhanced External Validation Demo (Feature 5)");
|
println!("🧪 Enhanced External Validation Demo (Feature 5)");
|
||||||
println!("✅ validation feature: ENABLED");
|
println!("✅ validation feature: ENABLED");
|
||||||
println!("✅ gui feature: ENABLED");
|
println!("✅ gui feature: ENABLED");
|
||||||
|
println!("✅ cursor-style feature: ENABLED");
|
||||||
println!("🧪 Enhanced features:");
|
println!("🧪 Enhanced features:");
|
||||||
println!(" • 5 different external validation types with realistic scenarios");
|
println!(" • 5 different external validation types with realistic scenarios");
|
||||||
println!(" • Validation caching and performance metrics");
|
println!(" • Validation caching and performance metrics");
|
||||||
@@ -1067,7 +1070,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
let data = ValidationDemoData::new();
|
let data = ValidationDemoData::new();
|
||||||
let editor = ValidationDemoEditor::new(data);
|
let mut editor = ValidationDemoEditor::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);
|
let res = run_app(&mut terminal, editor);
|
||||||
|
|
||||||
@@ -1075,6 +1084,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
|
execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
|
||||||
terminal.show_cursor()?;
|
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 {
|
if let Err(err) = res {
|
||||||
println!("{:?}", err);
|
println!("{:?}", err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,15 @@ pub fn render_canvas_with_highlight<T: CanvasTheme, D: DataProvider>(
|
|||||||
let current_field_idx = ui_state.current_field();
|
let current_field_idx = ui_state.current_field();
|
||||||
let is_edit_mode = matches!(ui_state.mode(), crate::canvas::modes::AppMode::Edit);
|
let is_edit_mode = matches!(ui_state.mode(), crate::canvas::modes::AppMode::Edit);
|
||||||
|
|
||||||
|
// Precompute completion for active field
|
||||||
|
let active_completion = if ui_state.is_suggestions_active()
|
||||||
|
&& ui_state.suggestions.active_field == Some(current_field_idx)
|
||||||
|
{
|
||||||
|
ui_state.suggestions.completion_text.clone()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
render_canvas_fields(
|
render_canvas_fields(
|
||||||
f,
|
f,
|
||||||
area,
|
area,
|
||||||
@@ -111,6 +120,14 @@ pub fn render_canvas_with_highlight<T: CanvasTheme, D: DataProvider>(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// NEW: provide completion for the active field
|
||||||
|
|i| {
|
||||||
|
if i == current_field_idx {
|
||||||
|
active_completion.clone()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +145,7 @@ fn convert_selection_to_highlight(selection: &crate::canvas::state::SelectionSta
|
|||||||
|
|
||||||
/// Core canvas field rendering
|
/// Core canvas field rendering
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn render_canvas_fields<T: CanvasTheme, F1, F2>(
|
fn render_canvas_fields<T: CanvasTheme, F1, F2, F3>(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
fields: &[&str],
|
fields: &[&str],
|
||||||
@@ -141,10 +158,12 @@ fn render_canvas_fields<T: CanvasTheme, F1, F2>(
|
|||||||
has_unsaved_changes: bool,
|
has_unsaved_changes: bool,
|
||||||
get_display_value: F1,
|
get_display_value: F1,
|
||||||
has_display_override: F2,
|
has_display_override: F2,
|
||||||
|
get_completion: F3,
|
||||||
) -> Option<Rect>
|
) -> Option<Rect>
|
||||||
where
|
where
|
||||||
F1: Fn(usize) -> String,
|
F1: Fn(usize) -> String,
|
||||||
F2: Fn(usize) -> bool,
|
F2: Fn(usize) -> bool,
|
||||||
|
F3: Fn(usize) -> Option<String>,
|
||||||
{
|
{
|
||||||
// Create layout
|
// Create layout
|
||||||
let columns = Layout::default()
|
let columns = Layout::default()
|
||||||
@@ -198,6 +217,7 @@ where
|
|||||||
current_cursor_pos,
|
current_cursor_pos,
|
||||||
get_display_value,
|
get_display_value,
|
||||||
has_display_override,
|
has_display_override,
|
||||||
|
get_completion,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +249,7 @@ fn render_field_labels<T: CanvasTheme>(
|
|||||||
|
|
||||||
/// Render field values with highlighting
|
/// Render field values with highlighting
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
fn render_field_values<T: CanvasTheme, F1, F2>(
|
fn render_field_values<T: CanvasTheme, F1, F2, F3>(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
input_rows: Vec<Rect>,
|
input_rows: Vec<Rect>,
|
||||||
inputs: &[String],
|
inputs: &[String],
|
||||||
@@ -239,35 +259,54 @@ fn render_field_values<T: CanvasTheme, F1, F2>(
|
|||||||
current_cursor_pos: usize,
|
current_cursor_pos: usize,
|
||||||
get_display_value: F1,
|
get_display_value: F1,
|
||||||
has_display_override: F2,
|
has_display_override: F2,
|
||||||
|
get_completion: F3,
|
||||||
) -> Option<Rect>
|
) -> Option<Rect>
|
||||||
where
|
where
|
||||||
F1: Fn(usize) -> String,
|
F1: Fn(usize) -> String,
|
||||||
F2: Fn(usize) -> bool,
|
F2: Fn(usize) -> bool,
|
||||||
|
F3: Fn(usize) -> Option<String>,
|
||||||
{
|
{
|
||||||
let mut active_field_input_rect = None;
|
let mut active_field_input_rect = None;
|
||||||
|
|
||||||
for (i, _input) in inputs.iter().enumerate() {
|
for (i, _input) in inputs.iter().enumerate() {
|
||||||
let is_active = i == *current_field_idx;
|
let is_active = i == *current_field_idx;
|
||||||
let text = get_display_value(i);
|
let typed_text = get_display_value(i);
|
||||||
|
|
||||||
// Apply highlighting
|
let line = if is_active {
|
||||||
let line = apply_highlighting(
|
// Compose typed + gray completion for the active field
|
||||||
&text,
|
let normal_style = Style::default().fg(theme.fg());
|
||||||
i,
|
let gray_style = Style::default().fg(theme.suggestion_gray());
|
||||||
current_field_idx,
|
|
||||||
current_cursor_pos,
|
let mut spans: Vec<Span> = Vec::new();
|
||||||
highlight_state,
|
spans.push(Span::styled(typed_text.clone(), normal_style));
|
||||||
theme,
|
|
||||||
is_active,
|
if let Some(completion) = get_completion(i) {
|
||||||
);
|
if !completion.is_empty() {
|
||||||
|
spans.push(Span::styled(completion, gray_style));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Line::from(spans)
|
||||||
|
} else {
|
||||||
|
// Non-active fields: keep existing highlighting logic
|
||||||
|
apply_highlighting(
|
||||||
|
&typed_text,
|
||||||
|
i,
|
||||||
|
current_field_idx,
|
||||||
|
current_cursor_pos,
|
||||||
|
highlight_state,
|
||||||
|
theme,
|
||||||
|
is_active,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
let input_display = Paragraph::new(line).alignment(Alignment::Left);
|
let input_display = Paragraph::new(line).alignment(Alignment::Left);
|
||||||
f.render_widget(input_display, input_rows[i]);
|
f.render_widget(input_display, input_rows[i]);
|
||||||
|
|
||||||
// Set cursor for active field
|
// Set cursor for active field at end of typed text (not after completion)
|
||||||
if is_active {
|
if is_active {
|
||||||
active_field_input_rect = Some(input_rows[i]);
|
active_field_input_rect = Some(input_rows[i]);
|
||||||
set_cursor_position(f, input_rows[i], &text, current_cursor_pos, has_display_override(i));
|
set_cursor_position(f, input_rows[i], &typed_text, current_cursor_pos, has_display_override(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,14 @@ pub struct EditorState {
|
|||||||
|
|
||||||
// Selection state (for vim visual mode)
|
// Selection state (for vim visual mode)
|
||||||
pub(crate) selection: SelectionState,
|
pub(crate) selection: SelectionState,
|
||||||
|
|
||||||
// Validation state (only available with validation feature)
|
// Validation state (only available with validation feature)
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
pub(crate) validation: crate::validation::ValidationState,
|
pub(crate) validation: crate::validation::ValidationState,
|
||||||
|
|
||||||
|
/// Computed fields state (only when computed feature is enabled)
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
pub(crate) computed: Option<crate::computed::ComputedState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -31,6 +35,7 @@ pub struct SuggestionsUIState {
|
|||||||
pub(crate) is_loading: bool,
|
pub(crate) is_loading: bool,
|
||||||
pub(crate) selected_index: Option<usize>,
|
pub(crate) selected_index: Option<usize>,
|
||||||
pub(crate) active_field: Option<usize>,
|
pub(crate) active_field: Option<usize>,
|
||||||
|
pub(crate) completion_text: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -52,10 +57,13 @@ impl EditorState {
|
|||||||
is_loading: false,
|
is_loading: false,
|
||||||
selected_index: None,
|
selected_index: None,
|
||||||
active_field: None,
|
active_field: None,
|
||||||
|
completion_text: None,
|
||||||
},
|
},
|
||||||
selection: SelectionState::None,
|
selection: SelectionState::None,
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
validation: crate::validation::ValidationState::new(),
|
validation: crate::validation::ValidationState::new(),
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
computed: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +76,15 @@ impl EditorState {
|
|||||||
self.current_field
|
self.current_field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if field is computed
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
pub fn is_computed_field(&self, field_index: usize) -> bool {
|
||||||
|
self.computed
|
||||||
|
.as_ref()
|
||||||
|
.map(|state| state.is_computed_field(field_index))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get current cursor position (for user's business logic)
|
/// Get current cursor position (for user's business logic)
|
||||||
pub fn cursor_position(&self) -> usize {
|
pub fn cursor_position(&self) -> usize {
|
||||||
self.cursor_pos
|
self.cursor_pos
|
||||||
@@ -97,7 +114,7 @@ impl EditorState {
|
|||||||
pub fn selection_state(&self) -> &SelectionState {
|
pub fn selection_state(&self) -> &SelectionState {
|
||||||
&self.selection
|
&self.selection
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get validation state (for user's business logic)
|
/// Get validation state (for user's business logic)
|
||||||
/// Only available when the 'validation' feature is enabled
|
/// Only available when the 'validation' feature is enabled
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
@@ -117,7 +134,12 @@ impl EditorState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_cursor(&mut self, position: usize, max_position: usize, for_edit_mode: bool) {
|
pub(crate) fn set_cursor(
|
||||||
|
&mut self,
|
||||||
|
position: usize,
|
||||||
|
max_position: usize,
|
||||||
|
for_edit_mode: bool,
|
||||||
|
) {
|
||||||
if for_edit_mode {
|
if for_edit_mode {
|
||||||
// Edit mode: can go past end for insertion
|
// Edit mode: can go past end for insertion
|
||||||
self.cursor_pos = position.min(max_position);
|
self.cursor_pos = position.min(max_position);
|
||||||
@@ -128,18 +150,40 @@ impl EditorState {
|
|||||||
self.ideal_cursor_column = self.cursor_pos;
|
self.ideal_cursor_column = self.cursor_pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Legacy internal activation (still used internally if needed)
|
||||||
pub(crate) fn activate_suggestions(&mut self, field_index: usize) {
|
pub(crate) fn activate_suggestions(&mut self, field_index: usize) {
|
||||||
self.suggestions.is_active = true;
|
self.suggestions.is_active = true;
|
||||||
self.suggestions.is_loading = true;
|
self.suggestions.is_loading = true;
|
||||||
self.suggestions.active_field = Some(field_index);
|
self.suggestions.active_field = Some(field_index);
|
||||||
self.suggestions.selected_index = None;
|
self.suggestions.selected_index = None;
|
||||||
|
self.suggestions.completion_text = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Legacy internal deactivation
|
||||||
pub(crate) fn deactivate_suggestions(&mut self) {
|
pub(crate) fn deactivate_suggestions(&mut self) {
|
||||||
self.suggestions.is_active = false;
|
self.suggestions.is_active = false;
|
||||||
self.suggestions.is_loading = false;
|
self.suggestions.is_loading = false;
|
||||||
self.suggestions.active_field = None;
|
self.suggestions.active_field = None;
|
||||||
self.suggestions.selected_index = None;
|
self.suggestions.selected_index = None;
|
||||||
|
self.suggestions.completion_text = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly open suggestions — should only be called on Tab
|
||||||
|
pub(crate) fn open_suggestions(&mut self, field_index: usize) {
|
||||||
|
self.suggestions.is_active = true;
|
||||||
|
self.suggestions.is_loading = true;
|
||||||
|
self.suggestions.active_field = Some(field_index);
|
||||||
|
self.suggestions.selected_index = None;
|
||||||
|
self.suggestions.completion_text = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly close suggestions — should be called on Esc or field change
|
||||||
|
pub(crate) fn close_suggestions(&mut self) {
|
||||||
|
self.suggestions.is_active = false;
|
||||||
|
self.suggestions.is_loading = false;
|
||||||
|
self.suggestions.active_field = None;
|
||||||
|
self.suggestions.selected_index = None;
|
||||||
|
self.suggestions.completion_text = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub trait CanvasTheme {
|
|||||||
fn highlight(&self) -> Color;
|
fn highlight(&self) -> Color;
|
||||||
fn highlight_bg(&self) -> Color;
|
fn highlight_bg(&self) -> Color;
|
||||||
fn warning(&self) -> Color;
|
fn warning(&self) -> Color;
|
||||||
|
fn suggestion_gray(&self) -> Color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -47,4 +48,7 @@ impl CanvasTheme for DefaultCanvasTheme {
|
|||||||
fn warning(&self) -> Color {
|
fn warning(&self) -> Color {
|
||||||
Color::Red
|
Color::Red
|
||||||
}
|
}
|
||||||
|
fn suggestion_gray(&self) -> Color {
|
||||||
|
Color::DarkGray
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
canvas/src/computed/mod.rs
Normal file
5
canvas/src/computed/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod provider;
|
||||||
|
pub mod state;
|
||||||
|
|
||||||
|
pub use provider::{ComputedContext, ComputedProvider};
|
||||||
|
pub use state::ComputedState;
|
||||||
31
canvas/src/computed/provider.rs
Normal file
31
canvas/src/computed/provider.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// ================================================================================================
|
||||||
|
// COMPUTED FIELDS - Provider and Context
|
||||||
|
// ================================================================================================
|
||||||
|
|
||||||
|
/// Context information provided to computed field calculations
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ComputedContext<'a> {
|
||||||
|
/// All field values in the form (index -> value)
|
||||||
|
pub field_values: &'a [&'a str],
|
||||||
|
/// The field index being computed
|
||||||
|
pub target_field: usize,
|
||||||
|
/// Current field that user is editing (if any)
|
||||||
|
pub current_field: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User implements this to provide computed field logic
|
||||||
|
pub trait ComputedProvider {
|
||||||
|
/// Compute value for a field based on other field values.
|
||||||
|
/// Called automatically when any field changes.
|
||||||
|
fn compute_field(&mut self, context: ComputedContext) -> String;
|
||||||
|
|
||||||
|
/// Check if this provider handles the given field.
|
||||||
|
fn handles_field(&self, field_index: usize) -> bool;
|
||||||
|
|
||||||
|
/// Get list of field dependencies for optimization.
|
||||||
|
/// If field A depends on fields [1, 3], only recompute A when fields 1 or 3 change.
|
||||||
|
/// Default: depend on all fields (always recompute) with a reasonable upper bound.
|
||||||
|
fn field_dependencies(&self, _field_index: usize) -> Vec<usize> {
|
||||||
|
(0..100).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
88
canvas/src/computed/state.rs
Normal file
88
canvas/src/computed/state.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/* file: canvas/src/computed/state.rs */
|
||||||
|
/*
|
||||||
|
Add computed state module file implementing caching and dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ================================================================================================
|
||||||
|
// COMPUTED FIELDS - State: caching and dependencies
|
||||||
|
// ================================================================================================
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
/// Internal state for computed field management
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ComputedState {
|
||||||
|
/// Cached computed values (field_index -> computed_value)
|
||||||
|
computed_values: HashMap<usize, String>,
|
||||||
|
/// Field dependency graph (field_index -> depends_on_fields)
|
||||||
|
dependencies: HashMap<usize, Vec<usize>>,
|
||||||
|
/// Track which fields are computed (display-only)
|
||||||
|
computed_fields: HashSet<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComputedState {
|
||||||
|
/// Create a new, empty computed state
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
computed_values: HashMap::new(),
|
||||||
|
dependencies: HashMap::new(),
|
||||||
|
computed_fields: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a field as computed with its dependencies
|
||||||
|
///
|
||||||
|
/// - `field_index`: the field that is computed (display-only)
|
||||||
|
/// - `dependencies`: indices of fields this computed field depends on
|
||||||
|
pub fn register_computed_field(&mut self, field_index: usize, mut dependencies: Vec<usize>) {
|
||||||
|
// Deduplicate dependencies to keep graph lean
|
||||||
|
dependencies.sort_unstable();
|
||||||
|
dependencies.dedup();
|
||||||
|
|
||||||
|
self.computed_fields.insert(field_index);
|
||||||
|
self.dependencies.insert(field_index, dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a field is computed (read-only, skip editing/navigation)
|
||||||
|
pub fn is_computed_field(&self, field_index: usize) -> bool {
|
||||||
|
self.computed_fields.contains(&field_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get cached computed value for a field, if available
|
||||||
|
pub fn get_computed_value(&self, field_index: usize) -> Option<&String> {
|
||||||
|
self.computed_values.get(&field_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update cached computed value for a field
|
||||||
|
pub fn set_computed_value(&mut self, field_index: usize, value: String) {
|
||||||
|
self.computed_values.insert(field_index, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get fields that should be recomputed when `changed_field` changed
|
||||||
|
///
|
||||||
|
/// This scans the dependency graph and returns all computed fields
|
||||||
|
/// that list `changed_field` as a dependency.
|
||||||
|
pub fn fields_to_recompute(&self, changed_field: usize) -> Vec<usize> {
|
||||||
|
self.dependencies
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(field, deps)| {
|
||||||
|
if deps.contains(&changed_field) {
|
||||||
|
Some(*field)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over all computed field indices
|
||||||
|
pub fn computed_fields(&self) -> impl Iterator<Item = usize> + '_ {
|
||||||
|
self.computed_fields.iter().copied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ComputedState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,20 @@ pub trait DataProvider {
|
|||||||
fn validation_config(&self, _field_index: usize) -> Option<crate::validation::ValidationConfig> {
|
fn validation_config(&self, _field_index: usize) -> Option<crate::validation::ValidationConfig> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if field is computed (display-only, skip in navigation)
|
||||||
|
/// Default: not computed
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
fn is_computed_field(&self, _field_index: usize) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get computed field value if this is a computed field.
|
||||||
|
/// Returns None for regular fields. Default: not computed.
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
fn computed_field_value(&self, _field_index: usize) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Optional: User implements this for suggestions data
|
/// Optional: User implements this for suggestions data
|
||||||
|
|||||||
@@ -40,6 +40,24 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
editor
|
editor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute inline completion for current selection and current text.
|
||||||
|
fn compute_current_completion(&self) -> Option<String> {
|
||||||
|
let typed = self.current_text();
|
||||||
|
let idx = self.ui_state.suggestions.selected_index?;
|
||||||
|
let sugg = self.suggestions.get(idx)?;
|
||||||
|
if let Some(rest) = sugg.value_to_store.strip_prefix(typed) {
|
||||||
|
if !rest.is_empty() {
|
||||||
|
return Some(rest.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update UI state's completion text from current selection
|
||||||
|
pub fn update_inline_completion(&mut self) {
|
||||||
|
self.ui_state.suggestions.completion_text = self.compute_current_completion();
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize validation configurations from data provider
|
/// Initialize validation configurations from data provider
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
fn initialize_validation(&mut self) {
|
fn initialize_validation(&mut self) {
|
||||||
@@ -134,6 +152,22 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
&self.ui_state
|
&self.ui_state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mutable access to UI state for internal crate use only.
|
||||||
|
pub(crate) fn ui_state_mut(&mut self) -> &mut EditorState {
|
||||||
|
&mut self.ui_state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the suggestions UI for `field_index` (UI-only; does not fetch).
|
||||||
|
pub fn open_suggestions(&mut self, field_index: usize) {
|
||||||
|
self.ui_state.open_suggestions(field_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close suggestions UI and clear the current suggestion results.
|
||||||
|
pub fn close_suggestions(&mut self) {
|
||||||
|
self.ui_state.close_suggestions();
|
||||||
|
self.suggestions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/// Set external validation state for a field (Feature 5)
|
/// Set external validation state for a field (Feature 5)
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
pub fn set_external_validation(
|
pub fn set_external_validation(
|
||||||
@@ -605,6 +639,8 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
self.ui_state.suggestions.is_loading = false;
|
self.ui_state.suggestions.is_loading = false;
|
||||||
if !self.suggestions.is_empty() {
|
if !self.suggestions.is_empty() {
|
||||||
self.ui_state.suggestions.selected_index = Some(0);
|
self.ui_state.suggestions.selected_index = Some(0);
|
||||||
|
// Compute initial inline completion from first suggestion
|
||||||
|
self.update_inline_completion();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -619,6 +655,9 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
let current = self.ui_state.suggestions.selected_index.unwrap_or(0);
|
let current = self.ui_state.suggestions.selected_index.unwrap_or(0);
|
||||||
let next = (current + 1) % self.suggestions.len();
|
let next = (current + 1) % self.suggestions.len();
|
||||||
self.ui_state.suggestions.selected_index = Some(next);
|
self.ui_state.suggestions.selected_index = Some(next);
|
||||||
|
|
||||||
|
// Update inline completion to reflect new highlighted item
|
||||||
|
self.update_inline_completion();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply selected suggestion
|
/// Apply selected suggestion
|
||||||
@@ -668,6 +707,45 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip computed fields during navigation when feature enabled
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
{
|
||||||
|
if let Some(computed_state) = &self.ui_state.computed {
|
||||||
|
// Find previous non-computed field
|
||||||
|
let mut candidate = self.ui_state.current_field;
|
||||||
|
for _ in 0..field_count {
|
||||||
|
candidate = candidate.saturating_sub(1);
|
||||||
|
if !computed_state.is_computed_field(candidate) {
|
||||||
|
// Validate and move as usual
|
||||||
|
#[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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
|
{
|
||||||
|
let current_text = self.current_text().to_string();
|
||||||
|
let _validation_result = self.ui_state.validation.validate_field_content(
|
||||||
|
self.ui_state.current_field,
|
||||||
|
¤t_text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.ui_state.move_to_field(candidate, field_count);
|
||||||
|
self.clamp_cursor_to_current_field();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if candidate == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if field switching is allowed (minimum character enforcement)
|
// Check if field switching is allowed (minimum character enforcement)
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
{
|
{
|
||||||
@@ -705,6 +783,45 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip computed fields during navigation when feature enabled
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
{
|
||||||
|
if let Some(computed_state) = &self.ui_state.computed {
|
||||||
|
// Find next non-computed field
|
||||||
|
let mut candidate = self.ui_state.current_field;
|
||||||
|
for _ in 0..field_count {
|
||||||
|
candidate = (candidate + 1).min(field_count - 1);
|
||||||
|
if !computed_state.is_computed_field(candidate) {
|
||||||
|
// Validate and move as usual
|
||||||
|
#[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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
|
{
|
||||||
|
let current_text = self.current_text().to_string();
|
||||||
|
let _validation_result = self.ui_state.validation.validate_field_content(
|
||||||
|
self.ui_state.current_field,
|
||||||
|
¤t_text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.ui_state.move_to_field(candidate, field_count);
|
||||||
|
self.clamp_cursor_to_current_field();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if candidate == field_count - 1 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if field switching is allowed (minimum character enforcement)
|
// Check if field switching is allowed (minimum character enforcement)
|
||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
{
|
{
|
||||||
@@ -763,6 +880,112 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
self.move_up()
|
self.move_up()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================================================================================================
|
||||||
|
// COMPUTED FIELDS (behind 'computed' feature)
|
||||||
|
// ================================================================================================
|
||||||
|
|
||||||
|
/// Initialize computed fields from provider and computed provider
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
pub fn set_computed_provider<C>(&mut self, mut provider: C)
|
||||||
|
where
|
||||||
|
C: crate::computed::ComputedProvider,
|
||||||
|
{
|
||||||
|
// Initialize computed state
|
||||||
|
self.ui_state.computed = Some(crate::computed::ComputedState::new());
|
||||||
|
|
||||||
|
// Register computed fields and their dependencies
|
||||||
|
let field_count = self.data_provider.field_count();
|
||||||
|
for field_index in 0..field_count {
|
||||||
|
if provider.handles_field(field_index) {
|
||||||
|
let deps = provider.field_dependencies(field_index);
|
||||||
|
if let Some(computed_state) = &mut self.ui_state.computed {
|
||||||
|
computed_state.register_computed_field(field_index, deps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial computation of all computed fields
|
||||||
|
self.recompute_all_fields(&mut provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recompute specific computed fields
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
pub fn recompute_fields<C>(&mut self, provider: &mut C, field_indices: &[usize])
|
||||||
|
where
|
||||||
|
C: crate::computed::ComputedProvider,
|
||||||
|
{
|
||||||
|
if let Some(computed_state) = &mut self.ui_state.computed {
|
||||||
|
// Collect all field values for context
|
||||||
|
let field_values: Vec<String> = (0..self.data_provider.field_count())
|
||||||
|
.map(|i| {
|
||||||
|
if computed_state.is_computed_field(i) {
|
||||||
|
// Use cached computed value
|
||||||
|
computed_state
|
||||||
|
.get_computed_value(i)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
// Use regular field value
|
||||||
|
self.data_provider.field_value(i).to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let field_refs: Vec<&str> = field_values.iter().map(|s| s.as_str()).collect();
|
||||||
|
|
||||||
|
// Recompute specified fields
|
||||||
|
for &field_index in field_indices {
|
||||||
|
if provider.handles_field(field_index) {
|
||||||
|
let context = crate::computed::ComputedContext {
|
||||||
|
field_values: &field_refs,
|
||||||
|
target_field: field_index,
|
||||||
|
current_field: Some(self.ui_state.current_field),
|
||||||
|
};
|
||||||
|
|
||||||
|
let computed_value = provider.compute_field(context);
|
||||||
|
computed_state.set_computed_value(field_index, computed_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recompute all computed fields
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
pub fn recompute_all_fields<C>(&mut self, provider: &mut C)
|
||||||
|
where
|
||||||
|
C: crate::computed::ComputedProvider,
|
||||||
|
{
|
||||||
|
if let Some(computed_state) = &self.ui_state.computed {
|
||||||
|
let computed_fields: Vec<usize> = computed_state.computed_fields().collect();
|
||||||
|
self.recompute_fields(provider, &computed_fields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trigger recomputation when field changes (call this after set_field_value)
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
pub fn on_field_changed<C>(&mut self, provider: &mut C, changed_field: usize)
|
||||||
|
where
|
||||||
|
C: crate::computed::ComputedProvider,
|
||||||
|
{
|
||||||
|
if let Some(computed_state) = &self.ui_state.computed {
|
||||||
|
let fields_to_update = computed_state.fields_to_recompute(changed_field);
|
||||||
|
if !fields_to_update.is_empty() {
|
||||||
|
self.recompute_fields(provider, &fields_to_update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enhanced getter that returns computed values for computed fields when available
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
pub fn effective_field_value(&self, field_index: usize) -> String {
|
||||||
|
if let Some(computed_state) = &self.ui_state.computed {
|
||||||
|
if let Some(computed_value) = computed_state.get_computed_value(field_index) {
|
||||||
|
return computed_value.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.data_provider.field_value(field_index).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// Move to next field (alternative to move_down)
|
/// Move to next field (alternative to move_down)
|
||||||
pub fn next_field(&mut self) -> Result<()> {
|
pub fn next_field(&mut self) -> Result<()> {
|
||||||
self.move_down()
|
self.move_down()
|
||||||
@@ -960,6 +1183,15 @@ impl<D: DataProvider> FormEditor<D> {
|
|||||||
|
|
||||||
/// Enter edit mode from read-only mode (vim i/a/o)
|
/// Enter edit mode from read-only mode (vim i/a/o)
|
||||||
pub fn enter_edit_mode(&mut self) {
|
pub fn enter_edit_mode(&mut self) {
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
{
|
||||||
|
if let Some(computed_state) = &self.ui_state.computed {
|
||||||
|
if computed_state.is_computed_field(self.ui_state.current_field) {
|
||||||
|
// Can't edit computed fields - silently ignore
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
self.set_mode(AppMode::Edit);
|
self.set_mode(AppMode::Edit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ pub mod suggestions;
|
|||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
pub mod validation;
|
pub mod validation;
|
||||||
|
|
||||||
|
// Only include computed module if feature is enabled
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
pub mod computed;
|
||||||
|
|
||||||
#[cfg(feature = "cursor-style")]
|
#[cfg(feature = "cursor-style")]
|
||||||
pub use canvas::CursorManager;
|
pub use canvas::CursorManager;
|
||||||
|
|
||||||
@@ -41,6 +45,10 @@ pub use validation::{
|
|||||||
CustomFormatter, FormattingResult, PositionMapper, DefaultPositionMapper,
|
CustomFormatter, FormattingResult, PositionMapper, DefaultPositionMapper,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Computed exports (only when computed feature is enabled)
|
||||||
|
#[cfg(feature = "computed")]
|
||||||
|
pub use computed::{ComputedProvider, ComputedContext, ComputedState};
|
||||||
|
|
||||||
// Theming and GUI
|
// Theming and GUI
|
||||||
#[cfg(feature = "gui")]
|
#[cfg(feature = "gui")]
|
||||||
pub use canvas::theme::{CanvasTheme, DefaultCanvasTheme};
|
pub use canvas::theme::{CanvasTheme, DefaultCanvasTheme};
|
||||||
|
|||||||
Reference in New Issue
Block a user