computed fields are working perfectly well now
This commit is contained in:
@@ -23,6 +23,10 @@ pub struct EditorState {
|
||||
// Validation state (only available with validation feature)
|
||||
#[cfg(feature = "validation")]
|
||||
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)]
|
||||
@@ -56,6 +60,8 @@ impl EditorState {
|
||||
selection: SelectionState::None,
|
||||
#[cfg(feature = "validation")]
|
||||
validation: crate::validation::ValidationState::new(),
|
||||
#[cfg(feature = "computed")]
|
||||
computed: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +74,15 @@ impl EditorState {
|
||||
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)
|
||||
pub fn cursor_position(&self) -> usize {
|
||||
self.cursor_pos
|
||||
|
||||
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> {
|
||||
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
|
||||
|
||||
@@ -668,6 +668,45 @@ impl<D: DataProvider> FormEditor<D> {
|
||||
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)
|
||||
#[cfg(feature = "validation")]
|
||||
{
|
||||
@@ -705,6 +744,45 @@ impl<D: DataProvider> FormEditor<D> {
|
||||
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)
|
||||
#[cfg(feature = "validation")]
|
||||
{
|
||||
@@ -763,6 +841,112 @@ impl<D: DataProvider> FormEditor<D> {
|
||||
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)
|
||||
pub fn next_field(&mut self) -> Result<()> {
|
||||
self.move_down()
|
||||
@@ -960,6 +1144,15 @@ impl<D: DataProvider> FormEditor<D> {
|
||||
|
||||
/// Enter edit mode from read-only mode (vim i/a/o)
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ pub mod suggestions;
|
||||
#[cfg(feature = "validation")]
|
||||
pub mod validation;
|
||||
|
||||
// Only include computed module if feature is enabled
|
||||
#[cfg(feature = "computed")]
|
||||
pub mod computed;
|
||||
|
||||
#[cfg(feature = "cursor-style")]
|
||||
pub use canvas::CursorManager;
|
||||
|
||||
@@ -41,6 +45,10 @@ pub use validation::{
|
||||
CustomFormatter, FormattingResult, PositionMapper, DefaultPositionMapper,
|
||||
};
|
||||
|
||||
// Computed exports (only when computed feature is enabled)
|
||||
#[cfg(feature = "computed")]
|
||||
pub use computed::{ComputedProvider, ComputedContext, ComputedState};
|
||||
|
||||
// Theming and GUI
|
||||
#[cfg(feature = "gui")]
|
||||
pub use canvas::theme::{CanvasTheme, DefaultCanvasTheme};
|
||||
|
||||
Reference in New Issue
Block a user