// 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, } 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 { 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!("{subtotal:.2}") } } 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!("{tax_amount:.2}") } } 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!("{total:.2}") } } _ => "".to_string(), } } fn handles_field(&self, field_index: usize) -> bool { matches!(field_index, 4..=6) // Subtotal, Tax Amount, Total } fn field_dependencies(&self, field_index: usize) -> Vec { 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 { editor: FormEditor, calculator: InvoiceCalculator, debug_message: String, last_computed_values: Vec, } impl ComputedFieldsEditor { 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..=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..=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..=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_name} - {field_type} field"); } } 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_name} - {field_type} field"); } } 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!( "๐Ÿšซ {field_name} is computed (read-only) - Press Tab to move to editable fields" ); return; } self.editor.enter_edit_mode(); let field_name = self.editor.data_provider().field_name(current); self.debug_message = format!("โœ๏ธ Editing {field_name} - Type to see calculations update"); } 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!( "๐Ÿšซ {field_name} is computed (read-only) - Press Tab to move to editable fields" ); return; } self.editor.enter_append_mode(); let field_name = self.editor.data_provider().field_name(current); self.debug_message = format!("โœ๏ธ Appending to {field_name} - Type to see calculations"); } fn exit_edit_mode(&mut self) { let current_field = self.editor.current_field(); self.editor.exit_edit_mode(); if matches!(current_field, 1..=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 { let field_index = self.editor.current_field(); self.editor.data_provider().field_value(field_index) } 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, ) -> anyhow::Result { 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( terminal: &mut Terminal, mut editor: ComputedFieldsEditor, ) -> 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) { 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) { render_canvas_default(f, area, &editor.editor); } fn render_computed_status(f: &mut Frame, area: Rect, editor: &ComputedFieldsEditor) { 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> { 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(()) }