Skip to main content

tui_canvas/
data_provider.rs

1// src/data_provider.rs
2//! Simplified user interface - only business data, no UI state
3
4/// Defines when suggestions should be shown for a field
5#[cfg(feature = "suggestions")]
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum SuggestionTrigger {
8    /// No suggestions for this field
9    None,
10    /// Show suggestions when field starts (becomes non-empty)
11    WhenFieldStarts,
12    /// Show suggestions when field starts with this special character
13    SpecialChar(char),
14}
15
16#[cfg(feature = "suggestions")]
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct SuggestionQuery {
19    pub query: String,
20    pub replace_range: Option<(usize, usize)>,
21}
22
23#[cfg(feature = "suggestions")]
24impl SuggestionQuery {
25    pub fn whole_field(query: impl Into<String>) -> Self {
26        Self {
27            query: query.into(),
28            replace_range: None,
29        }
30    }
31
32    pub fn with_replace_range(query: impl Into<String>, replace_range: (usize, usize)) -> Self {
33        Self {
34            query: query.into(),
35            replace_range: Some(replace_range),
36        }
37    }
38}
39
40/// User implements this - only business data, no UI state
41pub trait DataProvider {
42    /// How many fields in the form
43    fn field_count(&self) -> usize;
44
45    /// Get field label/name
46    fn field_name(&self, index: usize) -> &str;
47
48    /// Get field value
49    fn field_value(&self, index: usize) -> &str;
50
51    /// Set field value (library calls this when text changes)
52    fn set_field_value(&mut self, index: usize, value: String);
53
54    /// Capture the full editable content as a flat list of field values, for
55    /// undo/redo history. The default collects every field value in order.
56    ///
57    /// Providers whose field/line count varies (e.g. a rope-backed textarea)
58    /// can rely on this default for capture but must override
59    /// [`DataProvider::restore_content`] so the structure can be rebuilt.
60    fn capture_content(&self) -> Vec<String> {
61        (0..self.field_count())
62            .map(|i| self.field_value(i).to_string())
63            .collect()
64    }
65
66    /// Restore content previously produced by [`DataProvider::capture_content`].
67    ///
68    /// The default writes each value back by index and assumes a stable
69    /// `field_count`. Providers whose field/line count can change must override
70    /// this to rebuild their structure (add/remove fields or lines as needed).
71    fn restore_content(&mut self, fields: &[String]) {
72        let count = self.field_count();
73        for (i, value) in fields.iter().enumerate() {
74            if i < count {
75                self.set_field_value(i, value.clone());
76            }
77        }
78    }
79
80    /// Check if field supports suggestions (optional)
81    fn supports_suggestions(&self, _field_index: usize) -> bool {
82        false
83    }
84
85    /// When should suggestions be triggered for a field? (optional)
86    /// Only used when suggestions feature is enabled
87    #[cfg(feature = "suggestions")]
88    fn suggestion_trigger(&self, _field_index: usize) -> SuggestionTrigger {
89        SuggestionTrigger::None
90    }
91
92    /// Build the active suggestion query for the current field/cursor.
93    ///
94    /// Default behavior uses the whole current field value and replaces the
95    /// whole field on accept. More advanced editors can return a token-local
96    /// query and replace range.
97    #[cfg(feature = "suggestions")]
98    fn suggestion_query(&self, field_index: usize, _cursor_char: usize) -> Option<SuggestionQuery> {
99        Some(SuggestionQuery::whole_field(self.field_value(field_index)))
100    }
101
102    /// Fetch suggestions synchronously (for auto-trigger feature)
103    /// Returns empty vec by default. Override to enable auto-trigger.
104    #[cfg(feature = "suggestions")]
105    fn fetch_suggestions_sync(&self, _field_index: usize, _query: &str) -> Vec<SuggestionItem> {
106        Vec::new()
107    }
108
109    /// Apply the selected suggestion to the underlying field content and
110    /// return the desired cursor character position after insertion.
111    #[cfg(feature = "suggestions")]
112    fn accept_suggestion(
113        &mut self,
114        field_index: usize,
115        _cursor_char: usize,
116        suggestion: &SuggestionItem,
117        _query: &SuggestionQuery,
118    ) -> usize {
119        let value = suggestion.value_to_store.clone();
120        let cursor = value.chars().count();
121        self.set_field_value(field_index, value);
122        cursor
123    }
124
125    /// Get display value (for password masking, etc.) - optional
126    fn display_value(&self, _index: usize) -> Option<&str> {
127        None // Default: use actual value
128    }
129
130    /// Get validation configuration for a field (optional)
131    /// Only available when the 'validation' feature is enabled
132    #[cfg(feature = "validation")]
133    fn validation_config(
134        &self,
135        _field_index: usize,
136    ) -> Option<crate::validation::ValidationConfig> {
137        None
138    }
139
140    /// Check if field is computed (display-only, skip in navigation)
141    /// Default: not computed
142    #[cfg(feature = "computed")]
143    fn is_computed_field(&self, _field_index: usize) -> bool {
144        false
145    }
146
147    /// Get computed field value if this is a computed field.
148    /// Returns None for regular fields. Default: not computed.
149    #[cfg(feature = "computed")]
150    fn computed_field_value(&self, _field_index: usize) -> Option<String> {
151        None
152    }
153}
154
155#[cfg(feature = "suggestions")]
156#[derive(Debug, Clone)]
157pub struct SuggestionItem {
158    pub display_text: String,
159    pub value_to_store: String,
160}
161
162#[cfg(feature = "suggestions")]
163impl SuggestionItem {
164    pub fn new(display: impl Into<String>, value: impl Into<String>) -> Self {
165        Self {
166            display_text: display.into(),
167            value_to_store: value.into(),
168        }
169    }
170}