tui_canvas/editor/
navigation.rs1use crate::DataProvider;
3use crate::editor::EditorCore;
4
5impl<D: DataProvider> EditorCore<D> {
6 #[cfg(feature = "computed")]
8 fn resolved_navigable_field(
9 &self,
10 requested_field: usize,
11 prev_field: usize,
12 field_count: usize,
13 ) -> usize {
14 let target_field = requested_field.min(field_count - 1);
15
16 let Some(computed_state) = &self.ui_state.computed else {
17 return target_field;
18 };
19
20 if !computed_state.is_computed_field(target_field) {
21 return target_field;
22 }
23
24 let search_forward_first = target_field >= prev_field;
25
26 let mut search_forward =
27 || ((target_field + 1)..field_count).find(|&i| !computed_state.is_computed_field(i));
28 let mut search_backward = || {
29 (0..target_field)
30 .rev()
31 .find(|&i| !computed_state.is_computed_field(i))
32 };
33
34 if search_forward_first {
35 search_forward()
36 .or_else(&mut search_backward)
37 .unwrap_or(prev_field)
38 } else {
39 search_backward()
40 .or_else(&mut search_forward)
41 .unwrap_or(prev_field)
42 }
43 }
44
45 pub fn transition_to_field(&mut self, new_field: usize) -> anyhow::Result<()> {
46 self.break_undo_coalescing();
48
49 let field_count = self.data_provider.field_count();
50 if field_count == 0 {
51 self.clamp_current_field_to_count(field_count);
52 return Ok(());
53 }
54
55 let prev_field = self.clamp_current_field_to_count(field_count).unwrap_or(0);
56
57 #[cfg(feature = "computed")]
58 let target_field = self.resolved_navigable_field(new_field, prev_field, field_count);
59 #[cfg(not(feature = "computed"))]
60 let target_field = new_field.min(field_count - 1);
61
62 if target_field == prev_field {
63 return Ok(());
64 }
65
66 #[cfg(feature = "validation")]
67 self.ui_state.validation.clear_last_switch_block();
68
69 #[cfg(feature = "validation")]
70 {
71 let current_text = self.current_text();
72 if !self
73 .ui_state
74 .validation
75 .allows_field_switch(prev_field, current_text)
76 {
77 if let Some(reason) = self
78 .ui_state
79 .validation
80 .field_switch_block_reason(prev_field, current_text)
81 {
82 self.ui_state
83 .validation
84 .set_last_switch_block(reason.clone());
85 tracing::debug!("Field switch blocked: {}", reason);
86 return Err(anyhow::anyhow!("Cannot switch fields: {}", reason));
87 }
88 }
89 }
90
91 #[cfg(feature = "validation")]
92 {
93 let text = self.data_provider.field_value(prev_field).to_string();
94 let _ = self
95 .ui_state
96 .validation
97 .validate_field_content(prev_field, &text);
98
99 if let Some(cfg) = self.ui_state.validation.get_field_config(prev_field) {
100 if cfg.external_validation_enabled && !text.is_empty() {
101 self.set_external_validation(
102 prev_field,
103 crate::validation::ExternalValidationState::Validating,
104 );
105
106 if let Some(cb) = self.external_validation_callback.as_mut() {
107 let final_state = cb(prev_field, &text);
108 self.set_external_validation(prev_field, final_state);
109 }
110 }
111 }
112 }
113
114 let ideal_cursor_column = self.ui_state.ideal_cursor_column;
115 self.ui_state.move_to_field(target_field, field_count);
116
117 let current_text = self.current_text();
118 let max_pos = current_text.chars().count();
119 self.set_cursor_for_mode(ideal_cursor_column, max_pos);
120 self.ui_state.ideal_cursor_column = ideal_cursor_column;
121
122 #[cfg(feature = "suggestions")]
123 {
124 self.dismiss_suggestions();
125 }
126
127 Ok(())
128 }
129
130 pub fn move_first_line(&mut self) -> anyhow::Result<()> {
132 self.transition_to_field(0)
133 }
134
135 pub fn move_last_line(&mut self) -> anyhow::Result<()> {
137 let last_field = self.data_provider.field_count().saturating_sub(1);
138 self.transition_to_field(last_field)
139 }
140
141 pub fn move_up(&mut self) -> bool {
144 if self.ui_state.current_field == 0 {
145 return false;
146 }
147 let before = self.ui_state.current_field;
148 let new_field = self.ui_state.current_field - 1;
149 self.transition_to_field(new_field).is_ok() && self.ui_state.current_field != before
150 }
151
152 pub fn move_down(&mut self) -> bool {
155 let last = self.data_provider.field_count().saturating_sub(1);
156 if self.ui_state.current_field >= last {
157 return false;
158 }
159 let before = self.ui_state.current_field;
160 let new_field = self.ui_state.current_field + 1;
161 self.transition_to_field(new_field).is_ok() && self.ui_state.current_field != before
162 }
163
164 pub fn move_to_next_field(&mut self) -> anyhow::Result<()> {
166 let field_count = self.data_provider.field_count();
167 if field_count == 0 {
168 return Ok(());
169 }
170 let new_field = (self.ui_state.current_field + 1) % field_count;
171 self.transition_to_field(new_field)
172 }
173
174 pub fn prev_field(&mut self) -> bool {
175 self.move_up()
176 }
177
178 pub fn next_field(&mut self) -> bool {
179 self.move_down()
180 }
181}
182
183#[cfg(all(test, feature = "computed"))]
184mod tests {
185 use super::*;
186 use crate::computed::{ComputedContext, ComputedProvider};
187
188 #[derive(Clone, Default)]
189 struct TestProvider {
190 fields: Vec<(&'static str, String)>,
191 }
192
193 impl TestProvider {
194 fn new(names: &[&'static str]) -> Self {
195 Self {
196 fields: names.iter().map(|name| (*name, String::new())).collect(),
197 }
198 }
199 }
200
201 impl DataProvider for TestProvider {
202 fn field_count(&self) -> usize {
203 self.fields.len()
204 }
205
206 fn field_name(&self, index: usize) -> &str {
207 self.fields[index].0
208 }
209
210 fn field_value(&self, index: usize) -> &str {
211 &self.fields[index].1
212 }
213
214 fn set_field_value(&mut self, index: usize, value: String) {
215 self.fields[index].1 = value;
216 }
217 }
218
219 struct TestComputedProvider;
220
221 impl ComputedProvider for TestComputedProvider {
222 fn handles_field(&self, field_index: usize) -> bool {
223 matches!(field_index, 2 | 3 | 4)
224 }
225
226 fn field_dependencies(&self, _field_index: usize) -> Vec<usize> {
227 vec![0]
228 }
229
230 fn compute_field(&mut self, _context: ComputedContext) -> String {
231 String::new()
232 }
233 }
234
235 #[test]
236 fn move_down_skips_trailing_computed_fields() {
237 let provider = TestProvider::new(&["a", "b", "c", "d", "e"]);
238 let mut editor = EditorCore::new(provider);
239 editor.register_computed_provider(&TestComputedProvider);
240
241 assert!(editor.transition_to_field(1).is_ok());
242 assert_eq!(editor.current_field(), 1);
243
244 assert!(!editor.move_down());
245 assert_eq!(editor.current_field(), 1);
246 }
247
248 #[test]
249 fn move_last_line_lands_on_last_editable_field() {
250 let provider = TestProvider::new(&["a", "b", "c", "d", "e"]);
251 let mut editor = EditorCore::new(provider);
252 editor.register_computed_provider(&TestComputedProvider);
253
254 assert!(editor.move_last_line().is_ok());
255 assert_eq!(editor.current_field(), 1);
256 }
257}