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