fresh/view/settings/
state.rs

1//! Settings state management
2//!
3//! Tracks the current state of the settings UI, pending changes,
4//! and provides methods for reading/writing config values.
5
6use super::entry_dialog::EntryDialogState;
7use super::items::{control_to_value, SettingControl, SettingItem, SettingsPage};
8use super::layout::SettingsHit;
9use super::schema::{parse_schema, SettingCategory, SettingSchema};
10use super::search::{search_settings, SearchResult};
11use crate::config::Config;
12use crate::config_io::ConfigLayer;
13use crate::view::controls::FocusState;
14use crate::view::ui::ScrollablePanel;
15use std::collections::HashMap;
16
17/// Info needed to open a nested dialog (extracted before mutable borrow)
18enum NestedDialogInfo {
19    MapEntry {
20        key: String,
21        value: serde_json::Value,
22        schema: SettingSchema,
23        path: String,
24        is_new: bool,
25        no_delete: bool,
26    },
27    ArrayItem {
28        index: Option<usize>,
29        value: serde_json::Value,
30        schema: SettingSchema,
31        path: String,
32        is_new: bool,
33    },
34}
35
36/// Which panel currently has keyboard focus
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum FocusPanel {
39    /// Category list (left panel)
40    #[default]
41    Categories,
42    /// Settings items (right panel)
43    Settings,
44    /// Footer buttons (Reset/Save/Cancel)
45    Footer,
46}
47
48/// The state of the settings UI
49#[derive(Debug)]
50pub struct SettingsState {
51    /// Parsed schema categories
52    categories: Vec<SettingCategory>,
53    /// Pages built from categories
54    pub pages: Vec<SettingsPage>,
55    /// Currently selected category index
56    pub selected_category: usize,
57    /// Currently selected item index within the category
58    pub selected_item: usize,
59    /// Which panel currently has keyboard focus
60    pub focus_panel: FocusPanel,
61    /// Selected footer button index (0=Reset, 1=Save, 2=Cancel)
62    pub footer_button_index: usize,
63    /// Pending changes (path -> new value)
64    pub pending_changes: HashMap<String, serde_json::Value>,
65    /// The original config value (for detecting changes)
66    original_config: serde_json::Value,
67    /// Whether the settings panel is visible
68    pub visible: bool,
69    /// Current search query
70    pub search_query: String,
71    /// Whether search is active
72    pub search_active: bool,
73    /// Current search results
74    pub search_results: Vec<SearchResult>,
75    /// Selected search result index
76    pub selected_search_result: usize,
77    /// Whether the unsaved changes confirmation dialog is showing
78    pub showing_confirm_dialog: bool,
79    /// Selected option in confirmation dialog (0=Save, 1=Discard, 2=Cancel)
80    pub confirm_dialog_selection: usize,
81    /// Whether the help overlay is showing
82    pub showing_help: bool,
83    /// Scrollable panel for settings items
84    pub scroll_panel: ScrollablePanel,
85    /// Sub-focus index within the selected item (for TextList/Map navigation)
86    pub sub_focus: Option<usize>,
87    /// Whether we're in text editing mode (for TextList controls)
88    pub editing_text: bool,
89    /// Current mouse hover position (for hover feedback)
90    pub hover_position: Option<(u16, u16)>,
91    /// Current hover hit result (computed from hover_position and cached layout)
92    pub hover_hit: Option<SettingsHit>,
93    /// Stack of entry dialogs (for nested editing of Maps/ObjectArrays)
94    /// The top of the stack (last element) is the currently active dialog.
95    pub entry_dialog_stack: Vec<EntryDialogState>,
96    /// Which configuration layer to save changes to.
97    /// User layer is the default (global settings).
98    /// Project layer saves to the current project's .fresh/config.json.
99    pub target_layer: ConfigLayer,
100    /// Source layer for each setting path (where the value came from).
101    /// Maps JSON pointer paths (e.g., "/editor/tab_size") to their source layer.
102    /// Values not in this map come from system defaults.
103    pub layer_sources: HashMap<String, ConfigLayer>,
104    /// Paths to be removed from the current layer on save.
105    /// When a user "resets" a setting, we remove it from the delta rather than
106    /// setting it to the schema default.
107    pub pending_deletions: std::collections::HashSet<String>,
108}
109
110impl SettingsState {
111    /// Create a new settings state from schema and current config
112    pub fn new(schema_json: &str, config: &Config) -> Result<Self, serde_json::Error> {
113        let categories = parse_schema(schema_json)?;
114        let config_value = serde_json::to_value(config)?;
115        let layer_sources = HashMap::new(); // Populated via set_layer_sources()
116        let target_layer = ConfigLayer::User; // Default to user-global settings
117        let pages =
118            super::items::build_pages(&categories, &config_value, &layer_sources, target_layer);
119
120        Ok(Self {
121            categories,
122            pages,
123            selected_category: 0,
124            selected_item: 0,
125            focus_panel: FocusPanel::Categories,
126            footer_button_index: 2, // Default to Save button (0=Layer, 1=Reset, 2=Save, 3=Cancel)
127            pending_changes: HashMap::new(),
128            original_config: config_value,
129            visible: false,
130            search_query: String::new(),
131            search_active: false,
132            search_results: Vec::new(),
133            selected_search_result: 0,
134            showing_confirm_dialog: false,
135            confirm_dialog_selection: 0,
136            showing_help: false,
137            scroll_panel: ScrollablePanel::new(),
138            sub_focus: None,
139            editing_text: false,
140            hover_position: None,
141            hover_hit: None,
142            entry_dialog_stack: Vec::new(),
143            target_layer,
144            layer_sources,
145            pending_deletions: std::collections::HashSet::new(),
146        })
147    }
148
149    /// Show the settings panel
150    pub fn show(&mut self) {
151        self.visible = true;
152        self.focus_panel = FocusPanel::Categories;
153        self.footer_button_index = 2; // Default to Save button (0=Layer, 1=Reset, 2=Save, 3=Cancel)
154        self.selected_category = 0;
155        self.selected_item = 0;
156        self.scroll_panel = ScrollablePanel::new();
157        self.sub_focus = None;
158    }
159
160    /// Hide the settings panel
161    pub fn hide(&mut self) {
162        self.visible = false;
163        self.search_active = false;
164        self.search_query.clear();
165    }
166
167    /// Get the current entry dialog (top of stack), if any
168    pub fn entry_dialog(&self) -> Option<&EntryDialogState> {
169        self.entry_dialog_stack.last()
170    }
171
172    /// Get the current entry dialog mutably (top of stack), if any
173    pub fn entry_dialog_mut(&mut self) -> Option<&mut EntryDialogState> {
174        self.entry_dialog_stack.last_mut()
175    }
176
177    /// Check if any entry dialog is open
178    pub fn has_entry_dialog(&self) -> bool {
179        !self.entry_dialog_stack.is_empty()
180    }
181
182    /// Get the currently selected page
183    pub fn current_page(&self) -> Option<&SettingsPage> {
184        self.pages.get(self.selected_category)
185    }
186
187    /// Get the currently selected page mutably
188    pub fn current_page_mut(&mut self) -> Option<&mut SettingsPage> {
189        self.pages.get_mut(self.selected_category)
190    }
191
192    /// Get the currently selected item
193    pub fn current_item(&self) -> Option<&SettingItem> {
194        self.current_page()
195            .and_then(|page| page.items.get(self.selected_item))
196    }
197
198    /// Get the currently selected item mutably
199    pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
200        self.pages
201            .get_mut(self.selected_category)
202            .and_then(|page| page.items.get_mut(self.selected_item))
203    }
204
205    /// Check if the current text field can be exited (valid JSON if required)
206    pub fn can_exit_text_editing(&self) -> bool {
207        self.current_item()
208            .map(|item| {
209                if let SettingControl::Text(state) = &item.control {
210                    state.is_valid()
211                } else {
212                    true
213                }
214            })
215            .unwrap_or(true)
216    }
217
218    /// Check if entry dialog's current text field can be exited (valid JSON if required)
219    pub fn entry_dialog_can_exit_text_editing(&self) -> bool {
220        self.entry_dialog()
221            .and_then(|dialog| dialog.current_item())
222            .map(|item| {
223                if let SettingControl::Text(state) = &item.control {
224                    state.is_valid()
225                } else {
226                    true
227                }
228            })
229            .unwrap_or(true)
230    }
231
232    /// Initialize map focus when entering a Map control.
233    /// `from_above`: true = start at first entry, false = start at add-new field
234    fn init_map_focus(&mut self, from_above: bool) {
235        if let Some(item) = self.current_item_mut() {
236            if let SettingControl::Map(ref mut map_state) = item.control {
237                map_state.init_focus(from_above);
238            }
239        }
240    }
241
242    /// Move selection up
243    pub fn select_prev(&mut self) {
244        match self.focus_panel {
245            FocusPanel::Categories => {
246                if self.selected_category > 0 {
247                    self.selected_category -= 1;
248                    self.selected_item = 0;
249                    self.scroll_panel = ScrollablePanel::new();
250                    self.sub_focus = None;
251                }
252            }
253            FocusPanel::Settings => {
254                // Try to navigate within current Map control first
255                let handled = self
256                    .current_item_mut()
257                    .and_then(|item| match &mut item.control {
258                        SettingControl::Map(map_state) => Some(map_state.focus_prev()),
259                        _ => None,
260                    })
261                    .unwrap_or(false);
262
263                if !handled && self.selected_item > 0 {
264                    self.selected_item -= 1;
265                    self.sub_focus = None;
266                    self.init_map_focus(false); // entering from below
267                }
268                self.ensure_visible();
269            }
270            FocusPanel::Footer => {
271                // Navigate between footer buttons (left)
272                if self.footer_button_index > 0 {
273                    self.footer_button_index -= 1;
274                }
275            }
276        }
277    }
278
279    /// Move selection down
280    pub fn select_next(&mut self) {
281        match self.focus_panel {
282            FocusPanel::Categories => {
283                if self.selected_category + 1 < self.pages.len() {
284                    self.selected_category += 1;
285                    self.selected_item = 0;
286                    self.scroll_panel = ScrollablePanel::new();
287                    self.sub_focus = None;
288                }
289            }
290            FocusPanel::Settings => {
291                // Try to navigate within current Map control first
292                let handled = self
293                    .current_item_mut()
294                    .and_then(|item| match &mut item.control {
295                        SettingControl::Map(map_state) => Some(map_state.focus_next()),
296                        _ => None,
297                    })
298                    .unwrap_or(false);
299
300                if !handled {
301                    let can_move = self
302                        .current_page()
303                        .is_some_and(|page| self.selected_item + 1 < page.items.len());
304                    if can_move {
305                        self.selected_item += 1;
306                        self.sub_focus = None;
307                        self.init_map_focus(true); // entering from above
308                    }
309                }
310                self.ensure_visible();
311            }
312            FocusPanel::Footer => {
313                // Navigate between footer buttons (right)
314                if self.footer_button_index < 2 {
315                    self.footer_button_index += 1;
316                }
317            }
318        }
319    }
320
321    /// Switch focus between panels: Categories -> Settings -> Footer -> Categories
322    pub fn toggle_focus(&mut self) {
323        self.focus_panel = match self.focus_panel {
324            FocusPanel::Categories => FocusPanel::Settings,
325            FocusPanel::Settings => FocusPanel::Footer,
326            FocusPanel::Footer => FocusPanel::Categories,
327        };
328
329        // Reset item selection when switching to settings
330        if self.focus_panel == FocusPanel::Settings
331            && self.selected_item >= self.current_page().map_or(0, |p| p.items.len())
332        {
333            self.selected_item = 0;
334        }
335        self.sub_focus = None;
336
337        if self.focus_panel == FocusPanel::Settings {
338            self.init_map_focus(true); // entering from above
339        }
340
341        // Reset footer button to Save when entering Footer panel
342        if self.focus_panel == FocusPanel::Footer {
343            self.footer_button_index = 2; // Save button (0=Layer, 1=Reset, 2=Save, 3=Cancel)
344        }
345
346        self.ensure_visible();
347    }
348
349    /// Ensure the selected item is visible in the viewport
350    pub fn ensure_visible(&mut self) {
351        if self.focus_panel != FocusPanel::Settings {
352            return;
353        }
354
355        // Need to avoid borrowing self for both page and scroll_panel
356        let selected_item = self.selected_item;
357        let sub_focus = self.sub_focus;
358        if let Some(page) = self.pages.get(self.selected_category) {
359            self.scroll_panel
360                .ensure_focused_visible(&page.items, selected_item, sub_focus);
361        }
362    }
363
364    /// Record a pending change for a setting
365    pub fn set_pending_change(&mut self, path: &str, value: serde_json::Value) {
366        // Check if this is the same as the original value
367        let original = self.original_config.pointer(path);
368        if original == Some(&value) {
369            self.pending_changes.remove(path);
370        } else {
371            self.pending_changes.insert(path.to_string(), value);
372        }
373    }
374
375    /// Check if there are unsaved changes
376    pub fn has_changes(&self) -> bool {
377        !self.pending_changes.is_empty() || !self.pending_deletions.is_empty()
378    }
379
380    /// Apply pending changes to a config
381    pub fn apply_changes(&self, config: &Config) -> Result<Config, serde_json::Error> {
382        let mut config_value = serde_json::to_value(config)?;
383
384        for (path, value) in &self.pending_changes {
385            if let Some(target) = config_value.pointer_mut(path) {
386                *target = value.clone();
387            }
388        }
389
390        serde_json::from_value(config_value)
391    }
392
393    /// Discard all pending changes
394    pub fn discard_changes(&mut self) {
395        self.pending_changes.clear();
396        self.pending_deletions.clear();
397        // Rebuild pages from original config with layer info
398        self.pages = super::items::build_pages(
399            &self.categories,
400            &self.original_config,
401            &self.layer_sources,
402            self.target_layer,
403        );
404    }
405
406    /// Set the target layer for saving changes.
407    pub fn set_target_layer(&mut self, layer: ConfigLayer) {
408        if layer != ConfigLayer::System {
409            // Cannot target System layer (read-only)
410            self.target_layer = layer;
411            // Clear pending changes when switching layers
412            self.pending_changes.clear();
413            self.pending_deletions.clear();
414            // Rebuild pages with new target layer (affects "modified" indicators)
415            self.pages = super::items::build_pages(
416                &self.categories,
417                &self.original_config,
418                &self.layer_sources,
419                self.target_layer,
420            );
421        }
422    }
423
424    /// Cycle through writable layers: User -> Project -> Session -> User
425    pub fn cycle_target_layer(&mut self) {
426        self.target_layer = match self.target_layer {
427            ConfigLayer::System => ConfigLayer::User, // Should never be System, but handle it
428            ConfigLayer::User => ConfigLayer::Project,
429            ConfigLayer::Project => ConfigLayer::Session,
430            ConfigLayer::Session => ConfigLayer::User,
431        };
432        // Clear pending changes when switching layers
433        self.pending_changes.clear();
434        self.pending_deletions.clear();
435        // Rebuild pages with new target layer (affects "modified" indicators)
436        self.pages = super::items::build_pages(
437            &self.categories,
438            &self.original_config,
439            &self.layer_sources,
440            self.target_layer,
441        );
442    }
443
444    /// Get a display name for the current target layer.
445    pub fn target_layer_name(&self) -> &'static str {
446        match self.target_layer {
447            ConfigLayer::System => "System (read-only)",
448            ConfigLayer::User => "User",
449            ConfigLayer::Project => "Project",
450            ConfigLayer::Session => "Session",
451        }
452    }
453
454    /// Set the layer sources map (called by Editor when opening settings).
455    /// This also rebuilds pages to update modified indicators.
456    pub fn set_layer_sources(&mut self, sources: HashMap<String, ConfigLayer>) {
457        self.layer_sources = sources;
458        // Rebuild pages with new layer sources (affects "modified" indicators)
459        self.pages = super::items::build_pages(
460            &self.categories,
461            &self.original_config,
462            &self.layer_sources,
463            self.target_layer,
464        );
465    }
466
467    /// Get the source layer for a setting path.
468    /// Returns the layer where this value was defined, or System if it's the default.
469    pub fn get_layer_source(&self, path: &str) -> ConfigLayer {
470        self.layer_sources
471            .get(path)
472            .copied()
473            .unwrap_or(ConfigLayer::System)
474    }
475
476    /// Get a short label for a layer source (for UI display).
477    pub fn layer_source_label(layer: ConfigLayer) -> &'static str {
478        match layer {
479            ConfigLayer::System => "default",
480            ConfigLayer::User => "user",
481            ConfigLayer::Project => "project",
482            ConfigLayer::Session => "session",
483        }
484    }
485
486    /// Reset the current item by removing it from the target layer.
487    ///
488    /// NEW SEMANTICS: Instead of setting to schema default, we remove the value
489    /// from the current layer's delta. The value then falls back to inherited
490    /// (from lower-precedence layers) or to the schema default.
491    ///
492    /// Only items defined in the target layer can be reset.
493    pub fn reset_current_to_default(&mut self) {
494        // Get the info we need first, then release the borrow
495        let reset_info = self.current_item().and_then(|item| {
496            // Only allow reset if the item is defined in the target layer
497            // (i.e., if it's "modified" in the new semantics)
498            if !item.modified || item.is_auto_managed {
499                return None;
500            }
501            item.default
502                .as_ref()
503                .map(|default| (item.path.clone(), default.clone()))
504        });
505
506        if let Some((path, default)) = reset_info {
507            // Mark this path for deletion from the target layer
508            self.pending_deletions.insert(path.clone());
509            // Remove any pending change for this path
510            self.pending_changes.remove(&path);
511
512            // Update the control state to show the inherited value.
513            // Since we don't have access to other layers' values here,
514            // we use the schema default as the fallback display value.
515            if let Some(item) = self.current_item_mut() {
516                update_control_from_value(&mut item.control, &default);
517                item.modified = false;
518                // Update layer source to show where value now comes from
519                item.layer_source = ConfigLayer::System; // Falls back to default
520            }
521        }
522    }
523
524    /// Handle a value change from user interaction
525    pub fn on_value_changed(&mut self) {
526        // Capture target_layer before any borrows
527        let target_layer = self.target_layer;
528
529        // Get value and path first, then release borrow
530        let change_info = self.current_item().map(|item| {
531            let value = control_to_value(&item.control);
532            (item.path.clone(), value)
533        });
534
535        if let Some((path, value)) = change_info {
536            // When user changes a value, it becomes "modified" (defined in target layer)
537            // Remove from pending deletions if it was scheduled for removal
538            self.pending_deletions.remove(&path);
539
540            // Update the item's state
541            if let Some(item) = self.current_item_mut() {
542                item.modified = true; // New semantic: value is now defined in target layer
543                item.layer_source = target_layer; // Value now comes from target layer
544            }
545            self.set_pending_change(&path, value);
546        }
547    }
548
549    /// Update focus states for rendering
550    pub fn update_focus_states(&mut self) {
551        for (page_idx, page) in self.pages.iter_mut().enumerate() {
552            for (item_idx, item) in page.items.iter_mut().enumerate() {
553                let is_focused = self.focus_panel == FocusPanel::Settings
554                    && page_idx == self.selected_category
555                    && item_idx == self.selected_item;
556
557                let focus = if is_focused {
558                    FocusState::Focused
559                } else {
560                    FocusState::Normal
561                };
562
563                match &mut item.control {
564                    SettingControl::Toggle(state) => state.focus = focus,
565                    SettingControl::Number(state) => state.focus = focus,
566                    SettingControl::Dropdown(state) => state.focus = focus,
567                    SettingControl::Text(state) => state.focus = focus,
568                    SettingControl::TextList(state) => state.focus = focus,
569                    SettingControl::Map(state) => state.focus = focus,
570                    SettingControl::ObjectArray(state) => state.focus = focus,
571                    SettingControl::Json(state) => state.focus = focus,
572                    SettingControl::Complex { .. } => {}
573                }
574            }
575        }
576    }
577
578    /// Start search mode
579    pub fn start_search(&mut self) {
580        self.search_active = true;
581        self.search_query.clear();
582        self.search_results.clear();
583        self.selected_search_result = 0;
584    }
585
586    /// Cancel search mode
587    pub fn cancel_search(&mut self) {
588        self.search_active = false;
589        self.search_query.clear();
590        self.search_results.clear();
591        self.selected_search_result = 0;
592    }
593
594    /// Update search query and refresh results
595    pub fn set_search_query(&mut self, query: String) {
596        self.search_query = query;
597        self.search_results = search_settings(&self.pages, &self.search_query);
598        self.selected_search_result = 0;
599    }
600
601    /// Add a character to the search query
602    pub fn search_push_char(&mut self, c: char) {
603        self.search_query.push(c);
604        self.search_results = search_settings(&self.pages, &self.search_query);
605        self.selected_search_result = 0;
606    }
607
608    /// Remove the last character from the search query
609    pub fn search_pop_char(&mut self) {
610        self.search_query.pop();
611        self.search_results = search_settings(&self.pages, &self.search_query);
612        self.selected_search_result = 0;
613    }
614
615    /// Navigate to previous search result
616    pub fn search_prev(&mut self) {
617        if !self.search_results.is_empty() && self.selected_search_result > 0 {
618            self.selected_search_result -= 1;
619        }
620    }
621
622    /// Navigate to next search result
623    pub fn search_next(&mut self) {
624        if !self.search_results.is_empty()
625            && self.selected_search_result + 1 < self.search_results.len()
626        {
627            self.selected_search_result += 1;
628        }
629    }
630
631    /// Jump to the currently selected search result
632    pub fn jump_to_search_result(&mut self) {
633        if let Some(result) = self.search_results.get(self.selected_search_result) {
634            self.selected_category = result.page_index;
635            self.selected_item = result.item_index;
636            self.focus_panel = FocusPanel::Settings;
637            // Reset scroll offset but preserve viewport for ensure_visible
638            self.scroll_panel.scroll.offset = 0;
639            // Update content height for the new category's items
640            if let Some(page) = self.pages.get(self.selected_category) {
641                self.scroll_panel.update_content_height(&page.items);
642            }
643            self.sub_focus = None;
644            self.init_map_focus(true);
645            self.ensure_visible();
646            self.cancel_search();
647        }
648    }
649
650    /// Get the currently selected search result
651    pub fn current_search_result(&self) -> Option<&SearchResult> {
652        self.search_results.get(self.selected_search_result)
653    }
654
655    /// Show the unsaved changes confirmation dialog
656    pub fn show_confirm_dialog(&mut self) {
657        self.showing_confirm_dialog = true;
658        self.confirm_dialog_selection = 0; // Default to "Save and Exit"
659    }
660
661    /// Hide the confirmation dialog
662    pub fn hide_confirm_dialog(&mut self) {
663        self.showing_confirm_dialog = false;
664        self.confirm_dialog_selection = 0;
665    }
666
667    /// Move to next option in confirmation dialog
668    pub fn confirm_dialog_next(&mut self) {
669        self.confirm_dialog_selection = (self.confirm_dialog_selection + 1) % 3;
670    }
671
672    /// Move to previous option in confirmation dialog
673    pub fn confirm_dialog_prev(&mut self) {
674        self.confirm_dialog_selection = if self.confirm_dialog_selection == 0 {
675            2
676        } else {
677            self.confirm_dialog_selection - 1
678        };
679    }
680
681    /// Toggle the help overlay
682    pub fn toggle_help(&mut self) {
683        self.showing_help = !self.showing_help;
684    }
685
686    /// Hide the help overlay
687    pub fn hide_help(&mut self) {
688        self.showing_help = false;
689    }
690
691    /// Check if the entry dialog is showing
692    pub fn showing_entry_dialog(&self) -> bool {
693        self.has_entry_dialog()
694    }
695
696    /// Open the entry dialog for the currently focused map entry
697    pub fn open_entry_dialog(&mut self) {
698        let Some(item) = self.current_item() else {
699            return;
700        };
701
702        // Determine what type of entry we're editing based on the path
703        let path = item.path.as_str();
704        let SettingControl::Map(map_state) = &item.control else {
705            return;
706        };
707
708        // Get the focused entry
709        let Some(entry_idx) = map_state.focused_entry else {
710            return;
711        };
712        let Some((key, value)) = map_state.entries.get(entry_idx) else {
713            return;
714        };
715
716        // Get the value schema for this map
717        let Some(schema) = map_state.value_schema.as_ref() else {
718            return; // No schema available, can't create dialog
719        };
720
721        // If the map doesn't allow adding, it also doesn't allow deleting (auto-managed entries)
722        let no_delete = map_state.no_add;
723
724        // Create dialog from schema
725        let dialog =
726            EntryDialogState::from_schema(key.clone(), value, schema, path, false, no_delete);
727        self.entry_dialog_stack.push(dialog);
728    }
729
730    /// Open entry dialog for adding a new entry (with empty key)
731    pub fn open_add_entry_dialog(&mut self) {
732        let Some(item) = self.current_item() else {
733            return;
734        };
735        let SettingControl::Map(map_state) = &item.control else {
736            return;
737        };
738        let Some(schema) = map_state.value_schema.as_ref() else {
739            return;
740        };
741        let path = item.path.clone();
742
743        // Create dialog with empty key - user will fill it in
744        // no_delete is false for new entries (Delete button is not shown anyway for new entries)
745        let dialog = EntryDialogState::from_schema(
746            String::new(),
747            &serde_json::json!({}),
748            schema,
749            &path,
750            true,
751            false,
752        );
753        self.entry_dialog_stack.push(dialog);
754    }
755
756    /// Open dialog for adding a new array item
757    pub fn open_add_array_item_dialog(&mut self) {
758        let Some(item) = self.current_item() else {
759            return;
760        };
761        let SettingControl::ObjectArray(array_state) = &item.control else {
762            return;
763        };
764        let Some(schema) = array_state.item_schema.as_ref() else {
765            return;
766        };
767        let path = item.path.clone();
768
769        // Create dialog with empty value - user will fill it in
770        let dialog =
771            EntryDialogState::for_array_item(None, &serde_json::json!({}), schema, &path, true);
772        self.entry_dialog_stack.push(dialog);
773    }
774
775    /// Open dialog for editing an existing array item
776    pub fn open_edit_array_item_dialog(&mut self) {
777        let Some(item) = self.current_item() else {
778            return;
779        };
780        let SettingControl::ObjectArray(array_state) = &item.control else {
781            return;
782        };
783        let Some(schema) = array_state.item_schema.as_ref() else {
784            return;
785        };
786        let Some(index) = array_state.focused_index else {
787            return;
788        };
789        let Some(value) = array_state.bindings.get(index) else {
790            return;
791        };
792        let path = item.path.clone();
793
794        let dialog = EntryDialogState::for_array_item(Some(index), value, schema, &path, false);
795        self.entry_dialog_stack.push(dialog);
796    }
797
798    /// Close the entry dialog without saving (pops from stack)
799    pub fn close_entry_dialog(&mut self) {
800        self.entry_dialog_stack.pop();
801    }
802
803    /// Open a nested entry dialog for a Map or ObjectArray field within the current dialog
804    ///
805    /// This enables recursive editing: if a dialog field is itself a Map or ObjectArray,
806    /// pressing Enter will open a new dialog on top of the stack for that nested structure.
807    pub fn open_nested_entry_dialog(&mut self) {
808        // Get info from the current dialog's focused field
809        let nested_info = self.entry_dialog().and_then(|dialog| {
810            let item = dialog.current_item()?;
811            let path = format!("{}/{}", dialog.map_path, item.path.trim_start_matches('/'));
812
813            match &item.control {
814                SettingControl::Map(map_state) => {
815                    let schema = map_state.value_schema.as_ref()?;
816                    let no_delete = map_state.no_add; // If can't add, can't delete either
817                    if let Some(entry_idx) = map_state.focused_entry {
818                        // Edit existing entry
819                        let (key, value) = map_state.entries.get(entry_idx)?;
820                        Some(NestedDialogInfo::MapEntry {
821                            key: key.clone(),
822                            value: value.clone(),
823                            schema: schema.as_ref().clone(),
824                            path,
825                            is_new: false,
826                            no_delete,
827                        })
828                    } else {
829                        // Add new entry
830                        Some(NestedDialogInfo::MapEntry {
831                            key: String::new(),
832                            value: serde_json::json!({}),
833                            schema: schema.as_ref().clone(),
834                            path,
835                            is_new: true,
836                            no_delete: false, // New entries don't show Delete anyway
837                        })
838                    }
839                }
840                SettingControl::ObjectArray(array_state) => {
841                    let schema = array_state.item_schema.as_ref()?;
842                    if let Some(index) = array_state.focused_index {
843                        // Edit existing item
844                        let value = array_state.bindings.get(index)?;
845                        Some(NestedDialogInfo::ArrayItem {
846                            index: Some(index),
847                            value: value.clone(),
848                            schema: schema.as_ref().clone(),
849                            path,
850                            is_new: false,
851                        })
852                    } else {
853                        // Add new item
854                        Some(NestedDialogInfo::ArrayItem {
855                            index: None,
856                            value: serde_json::json!({}),
857                            schema: schema.as_ref().clone(),
858                            path,
859                            is_new: true,
860                        })
861                    }
862                }
863                _ => None,
864            }
865        });
866
867        // Now create and push the dialog (outside the borrow)
868        if let Some(info) = nested_info {
869            let dialog = match info {
870                NestedDialogInfo::MapEntry {
871                    key,
872                    value,
873                    schema,
874                    path,
875                    is_new,
876                    no_delete,
877                } => EntryDialogState::from_schema(key, &value, &schema, &path, is_new, no_delete),
878                NestedDialogInfo::ArrayItem {
879                    index,
880                    value,
881                    schema,
882                    path,
883                    is_new,
884                } => EntryDialogState::for_array_item(index, &value, &schema, &path, is_new),
885            };
886            self.entry_dialog_stack.push(dialog);
887        }
888    }
889
890    /// Save the entry dialog and apply changes
891    ///
892    /// Automatically detects whether this is a Map or ObjectArray dialog
893    /// and handles saving appropriately.
894    pub fn save_entry_dialog(&mut self) {
895        // Determine if this is an array dialog by checking where we need to save
896        // For nested dialogs (stack len > 1), check the parent dialog's item type
897        // For top-level dialogs (stack len == 1), check current_item()
898        let is_array = if self.entry_dialog_stack.len() > 1 {
899            // Nested dialog - check parent dialog's focused item
900            self.entry_dialog_stack
901                .get(self.entry_dialog_stack.len() - 2)
902                .and_then(|parent| parent.current_item())
903                .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
904                .unwrap_or(false)
905        } else {
906            // Top-level dialog - check main settings page item
907            self.current_item()
908                .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
909                .unwrap_or(false)
910        };
911
912        if is_array {
913            self.save_array_item_dialog_inner();
914        } else {
915            self.save_map_entry_dialog_inner();
916        }
917    }
918
919    /// Save a Map entry dialog
920    fn save_map_entry_dialog_inner(&mut self) {
921        let Some(dialog) = self.entry_dialog_stack.pop() else {
922            return;
923        };
924
925        // Get key from the dialog's key field (may have been edited)
926        let key = dialog.get_key();
927        if key.is_empty() {
928            return; // Can't save with empty key
929        }
930
931        let value = dialog.to_value();
932        let map_path = dialog.map_path.clone();
933        let original_key = dialog.entry_key.clone();
934        let is_new = dialog.is_new;
935        let key_changed = !is_new && key != original_key;
936
937        // Update the map control with the new value
938        if let Some(item) = self.current_item_mut() {
939            if let SettingControl::Map(map_state) = &mut item.control {
940                // If key was changed, remove old entry first
941                if key_changed {
942                    if let Some(idx) = map_state
943                        .entries
944                        .iter()
945                        .position(|(k, _)| k == &original_key)
946                    {
947                        map_state.entries.remove(idx);
948                    }
949                }
950
951                // Find or add the entry with the (possibly new) key
952                if let Some(entry) = map_state.entries.iter_mut().find(|(k, _)| k == &key) {
953                    entry.1 = value.clone();
954                } else {
955                    map_state.entries.push((key.clone(), value.clone()));
956                    map_state.entries.sort_by(|a, b| a.0.cmp(&b.0));
957                }
958            }
959        }
960
961        // Record deletion of old key if key was changed
962        if key_changed {
963            let old_path = format!("{}/{}", map_path, original_key);
964            self.pending_changes
965                .insert(old_path, serde_json::Value::Null);
966        }
967
968        // Record the pending change
969        let path = format!("{}/{}", map_path, key);
970        self.set_pending_change(&path, value);
971    }
972
973    /// Save an ObjectArray item dialog
974    fn save_array_item_dialog_inner(&mut self) {
975        let Some(dialog) = self.entry_dialog_stack.pop() else {
976            return;
977        };
978
979        let value = dialog.to_value();
980        let array_path = dialog.map_path.clone();
981        let is_new = dialog.is_new;
982        let entry_key = dialog.entry_key.clone();
983
984        // Determine if this is a nested dialog (parent still in stack)
985        let is_nested = !self.entry_dialog_stack.is_empty();
986
987        if is_nested {
988            // Nested dialog - update the parent dialog's ObjectArray item
989            // Extract the array field name from the path (last segment)
990            let array_field = array_path.rsplit('/').next().unwrap_or("").to_string();
991            let item_path = format!("/{}", array_field);
992
993            // Find and update the ObjectArray in the parent dialog
994            if let Some(parent) = self.entry_dialog_stack.last_mut() {
995                if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
996                    if let SettingControl::ObjectArray(array_state) = &mut item.control {
997                        if is_new {
998                            array_state.bindings.push(value.clone());
999                        } else if let Ok(index) = entry_key.parse::<usize>() {
1000                            if index < array_state.bindings.len() {
1001                                array_state.bindings[index] = value.clone();
1002                            }
1003                        }
1004                    }
1005                }
1006            }
1007
1008            // For nested arrays, the pending change will be recorded when parent dialog saves
1009            // We still record a pending change so the value persists
1010            if let Some(parent) = self.entry_dialog_stack.last() {
1011                if let Some(item) = parent.items.iter().find(|i| i.path == item_path) {
1012                    if let SettingControl::ObjectArray(array_state) = &item.control {
1013                        let array_value = serde_json::Value::Array(array_state.bindings.clone());
1014                        self.set_pending_change(&array_path, array_value);
1015                    }
1016                }
1017            }
1018        } else {
1019            // Top-level dialog - update the main settings page item
1020            if let Some(item) = self.current_item_mut() {
1021                if let SettingControl::ObjectArray(array_state) = &mut item.control {
1022                    if is_new {
1023                        array_state.bindings.push(value.clone());
1024                    } else if let Ok(index) = entry_key.parse::<usize>() {
1025                        if index < array_state.bindings.len() {
1026                            array_state.bindings[index] = value.clone();
1027                        }
1028                    }
1029                }
1030            }
1031
1032            // Record the pending change for the entire array
1033            if let Some(item) = self.current_item() {
1034                if let SettingControl::ObjectArray(array_state) = &item.control {
1035                    let array_value = serde_json::Value::Array(array_state.bindings.clone());
1036                    self.set_pending_change(&array_path, array_value);
1037                }
1038            }
1039        }
1040    }
1041
1042    /// Delete the entry from the map and close the dialog
1043    pub fn delete_entry_dialog(&mut self) {
1044        // Check if this is a nested dialog BEFORE popping
1045        let is_nested = self.entry_dialog_stack.len() > 1;
1046
1047        let Some(dialog) = self.entry_dialog_stack.pop() else {
1048            return;
1049        };
1050
1051        let path = format!("{}/{}", dialog.map_path, dialog.entry_key);
1052
1053        // Remove from the map control
1054        if is_nested {
1055            // Nested dialog - update the parent dialog's Map item
1056            // Extract the map field name from the path (last segment of map_path)
1057            let map_field = dialog.map_path.rsplit('/').next().unwrap_or("").to_string();
1058            let item_path = format!("/{}", map_field);
1059
1060            // Find and update the Map in the parent dialog
1061            if let Some(parent) = self.entry_dialog_stack.last_mut() {
1062                if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1063                    if let SettingControl::Map(map_state) = &mut item.control {
1064                        if let Some(idx) = map_state
1065                            .entries
1066                            .iter()
1067                            .position(|(k, _)| k == &dialog.entry_key)
1068                        {
1069                            map_state.remove_entry(idx);
1070                        }
1071                    }
1072                }
1073            }
1074        } else {
1075            // Top-level dialog - remove from the main settings page item
1076            if let Some(item) = self.current_item_mut() {
1077                if let SettingControl::Map(map_state) = &mut item.control {
1078                    if let Some(idx) = map_state
1079                        .entries
1080                        .iter()
1081                        .position(|(k, _)| k == &dialog.entry_key)
1082                    {
1083                        map_state.remove_entry(idx);
1084                    }
1085                }
1086            }
1087        }
1088
1089        // Record the pending change (null value signals deletion)
1090        self.set_pending_change(&path, serde_json::Value::Null);
1091    }
1092
1093    /// Get the maximum scroll offset for the current page (in rows)
1094    pub fn max_scroll(&self) -> u16 {
1095        self.scroll_panel.scroll.max_offset()
1096    }
1097
1098    /// Scroll up by a given number of rows
1099    /// Returns true if the scroll offset changed
1100    pub fn scroll_up(&mut self, delta: usize) -> bool {
1101        let old = self.scroll_panel.scroll.offset;
1102        self.scroll_panel.scroll_up(delta as u16);
1103        old != self.scroll_panel.scroll.offset
1104    }
1105
1106    /// Scroll down by a given number of rows
1107    /// Returns true if the scroll offset changed
1108    pub fn scroll_down(&mut self, delta: usize) -> bool {
1109        let old = self.scroll_panel.scroll.offset;
1110        self.scroll_panel.scroll_down(delta as u16);
1111        old != self.scroll_panel.scroll.offset
1112    }
1113
1114    /// Scroll to a position based on a ratio (0.0 to 1.0)
1115    /// Returns true if the scroll offset changed
1116    pub fn scroll_to_ratio(&mut self, ratio: f32) -> bool {
1117        let old = self.scroll_panel.scroll.offset;
1118        self.scroll_panel.scroll_to_ratio(ratio);
1119        old != self.scroll_panel.scroll.offset
1120    }
1121
1122    /// Start text editing mode for TextList, Text, or Map controls
1123    pub fn start_editing(&mut self) {
1124        if let Some(item) = self.current_item() {
1125            if matches!(
1126                item.control,
1127                SettingControl::TextList(_) | SettingControl::Text(_) | SettingControl::Map(_)
1128            ) {
1129                self.editing_text = true;
1130            }
1131        }
1132    }
1133
1134    /// Stop text editing mode
1135    pub fn stop_editing(&mut self) {
1136        self.editing_text = false;
1137    }
1138
1139    /// Check if the current item is editable (TextList, Text, or Map)
1140    pub fn is_editable_control(&self) -> bool {
1141        self.current_item().is_some_and(|item| {
1142            matches!(
1143                item.control,
1144                SettingControl::TextList(_) | SettingControl::Text(_) | SettingControl::Map(_)
1145            )
1146        })
1147    }
1148
1149    /// Insert a character into the current editable control
1150    pub fn text_insert(&mut self, c: char) {
1151        if let Some(item) = self.current_item_mut() {
1152            match &mut item.control {
1153                SettingControl::TextList(state) => state.insert(c),
1154                SettingControl::Text(state) => {
1155                    state.value.insert(state.cursor, c);
1156                    state.cursor += c.len_utf8();
1157                }
1158                SettingControl::Map(state) => {
1159                    state.new_key_text.insert(state.cursor, c);
1160                    state.cursor += c.len_utf8();
1161                }
1162                _ => {}
1163            }
1164        }
1165    }
1166
1167    /// Backspace in the current editable control
1168    pub fn text_backspace(&mut self) {
1169        if let Some(item) = self.current_item_mut() {
1170            match &mut item.control {
1171                SettingControl::TextList(state) => state.backspace(),
1172                SettingControl::Text(state) => {
1173                    if state.cursor > 0 {
1174                        let mut char_start = state.cursor - 1;
1175                        while char_start > 0 && !state.value.is_char_boundary(char_start) {
1176                            char_start -= 1;
1177                        }
1178                        state.value.remove(char_start);
1179                        state.cursor = char_start;
1180                    }
1181                }
1182                SettingControl::Map(state) => {
1183                    if state.cursor > 0 {
1184                        let mut char_start = state.cursor - 1;
1185                        while char_start > 0 && !state.new_key_text.is_char_boundary(char_start) {
1186                            char_start -= 1;
1187                        }
1188                        state.new_key_text.remove(char_start);
1189                        state.cursor = char_start;
1190                    }
1191                }
1192                _ => {}
1193            }
1194        }
1195    }
1196
1197    /// Move cursor left in the current editable control
1198    pub fn text_move_left(&mut self) {
1199        if let Some(item) = self.current_item_mut() {
1200            match &mut item.control {
1201                SettingControl::TextList(state) => state.move_left(),
1202                SettingControl::Text(state) => {
1203                    if state.cursor > 0 {
1204                        let mut new_pos = state.cursor - 1;
1205                        while new_pos > 0 && !state.value.is_char_boundary(new_pos) {
1206                            new_pos -= 1;
1207                        }
1208                        state.cursor = new_pos;
1209                    }
1210                }
1211                SettingControl::Map(state) => {
1212                    if state.cursor > 0 {
1213                        let mut new_pos = state.cursor - 1;
1214                        while new_pos > 0 && !state.new_key_text.is_char_boundary(new_pos) {
1215                            new_pos -= 1;
1216                        }
1217                        state.cursor = new_pos;
1218                    }
1219                }
1220                _ => {}
1221            }
1222        }
1223    }
1224
1225    /// Move cursor right in the current editable control
1226    pub fn text_move_right(&mut self) {
1227        if let Some(item) = self.current_item_mut() {
1228            match &mut item.control {
1229                SettingControl::TextList(state) => state.move_right(),
1230                SettingControl::Text(state) => {
1231                    if state.cursor < state.value.len() {
1232                        let mut new_pos = state.cursor + 1;
1233                        while new_pos < state.value.len() && !state.value.is_char_boundary(new_pos)
1234                        {
1235                            new_pos += 1;
1236                        }
1237                        state.cursor = new_pos;
1238                    }
1239                }
1240                SettingControl::Map(state) => {
1241                    if state.cursor < state.new_key_text.len() {
1242                        let mut new_pos = state.cursor + 1;
1243                        while new_pos < state.new_key_text.len()
1244                            && !state.new_key_text.is_char_boundary(new_pos)
1245                        {
1246                            new_pos += 1;
1247                        }
1248                        state.cursor = new_pos;
1249                    }
1250                }
1251                _ => {}
1252            }
1253        }
1254    }
1255
1256    /// Move focus to previous item in TextList/Map (wraps within control)
1257    pub fn text_focus_prev(&mut self) {
1258        if let Some(item) = self.current_item_mut() {
1259            match &mut item.control {
1260                SettingControl::TextList(state) => state.focus_prev(),
1261                SettingControl::Map(state) => {
1262                    state.focus_prev();
1263                }
1264                _ => {}
1265            }
1266        }
1267    }
1268
1269    /// Move focus to next item in TextList/Map (wraps within control)
1270    pub fn text_focus_next(&mut self) {
1271        if let Some(item) = self.current_item_mut() {
1272            match &mut item.control {
1273                SettingControl::TextList(state) => state.focus_next(),
1274                SettingControl::Map(state) => {
1275                    state.focus_next();
1276                }
1277                _ => {}
1278            }
1279        }
1280    }
1281
1282    /// Add new item in TextList/Map (from the new item field)
1283    pub fn text_add_item(&mut self) {
1284        if let Some(item) = self.current_item_mut() {
1285            match &mut item.control {
1286                SettingControl::TextList(state) => state.add_item(),
1287                SettingControl::Map(state) => state.add_entry_from_input(),
1288                _ => {}
1289            }
1290        }
1291        // Record the change
1292        self.on_value_changed();
1293    }
1294
1295    /// Remove the currently focused item in TextList/Map
1296    pub fn text_remove_focused(&mut self) {
1297        if let Some(item) = self.current_item_mut() {
1298            match &mut item.control {
1299                SettingControl::TextList(state) => {
1300                    if let Some(idx) = state.focused_item {
1301                        state.remove_item(idx);
1302                    }
1303                }
1304                SettingControl::Map(state) => {
1305                    if let Some(idx) = state.focused_entry {
1306                        state.remove_entry(idx);
1307                    }
1308                }
1309                _ => {}
1310            }
1311        }
1312        // Record the change
1313        self.on_value_changed();
1314    }
1315
1316    // =========== Dropdown methods ===========
1317
1318    /// Check if current item is a dropdown with menu open
1319    pub fn is_dropdown_open(&self) -> bool {
1320        self.current_item().is_some_and(|item| {
1321            if let SettingControl::Dropdown(ref d) = item.control {
1322                d.open
1323            } else {
1324                false
1325            }
1326        })
1327    }
1328
1329    /// Toggle dropdown open/closed
1330    pub fn dropdown_toggle(&mut self) {
1331        let mut opened = false;
1332        if let Some(item) = self.current_item_mut() {
1333            if let SettingControl::Dropdown(ref mut d) = item.control {
1334                d.toggle_open();
1335                opened = d.open;
1336            }
1337        }
1338
1339        // When dropdown opens, update content height and ensure it's visible
1340        if opened {
1341            // Update content height since item is now taller
1342            let selected_item = self.selected_item;
1343            if let Some(page) = self.pages.get(self.selected_category) {
1344                self.scroll_panel.update_content_height(&page.items);
1345                // Ensure the dropdown item is visible with its new expanded height
1346                self.scroll_panel
1347                    .ensure_focused_visible(&page.items, selected_item, None);
1348            }
1349        }
1350    }
1351
1352    /// Select previous option in dropdown
1353    pub fn dropdown_prev(&mut self) {
1354        if let Some(item) = self.current_item_mut() {
1355            if let SettingControl::Dropdown(ref mut d) = item.control {
1356                d.select_prev();
1357            }
1358        }
1359    }
1360
1361    /// Select next option in dropdown
1362    pub fn dropdown_next(&mut self) {
1363        if let Some(item) = self.current_item_mut() {
1364            if let SettingControl::Dropdown(ref mut d) = item.control {
1365                d.select_next();
1366            }
1367        }
1368    }
1369
1370    /// Jump to first option in dropdown
1371    pub fn dropdown_home(&mut self) {
1372        if let Some(item) = self.current_item_mut() {
1373            if let SettingControl::Dropdown(ref mut d) = item.control {
1374                if !d.options.is_empty() {
1375                    d.selected = 0;
1376                    d.ensure_visible();
1377                }
1378            }
1379        }
1380    }
1381
1382    /// Jump to last option in dropdown
1383    pub fn dropdown_end(&mut self) {
1384        if let Some(item) = self.current_item_mut() {
1385            if let SettingControl::Dropdown(ref mut d) = item.control {
1386                if !d.options.is_empty() {
1387                    d.selected = d.options.len() - 1;
1388                    d.ensure_visible();
1389                }
1390            }
1391        }
1392    }
1393
1394    /// Confirm dropdown selection (close and record change)
1395    pub fn dropdown_confirm(&mut self) {
1396        if let Some(item) = self.current_item_mut() {
1397            if let SettingControl::Dropdown(ref mut d) = item.control {
1398                d.confirm();
1399            }
1400        }
1401        self.on_value_changed();
1402    }
1403
1404    /// Cancel dropdown (restore original value and close)
1405    pub fn dropdown_cancel(&mut self) {
1406        if let Some(item) = self.current_item_mut() {
1407            if let SettingControl::Dropdown(ref mut d) = item.control {
1408                d.cancel();
1409            }
1410        }
1411    }
1412
1413    /// Scroll open dropdown by delta (positive = down, negative = up)
1414    pub fn dropdown_scroll(&mut self, delta: i32) {
1415        if let Some(item) = self.current_item_mut() {
1416            if let SettingControl::Dropdown(ref mut d) = item.control {
1417                if d.open {
1418                    d.scroll_by(delta);
1419                }
1420            }
1421        }
1422    }
1423
1424    // =========== Number editing methods ===========
1425
1426    /// Check if current item is a number input being edited
1427    pub fn is_number_editing(&self) -> bool {
1428        self.current_item().is_some_and(|item| {
1429            if let SettingControl::Number(ref n) = item.control {
1430                n.editing()
1431            } else {
1432                false
1433            }
1434        })
1435    }
1436
1437    /// Start number editing mode
1438    pub fn start_number_editing(&mut self) {
1439        if let Some(item) = self.current_item_mut() {
1440            if let SettingControl::Number(ref mut n) = item.control {
1441                n.start_editing();
1442            }
1443        }
1444    }
1445
1446    /// Insert a character into number input
1447    pub fn number_insert(&mut self, c: char) {
1448        if let Some(item) = self.current_item_mut() {
1449            if let SettingControl::Number(ref mut n) = item.control {
1450                n.insert_char(c);
1451            }
1452        }
1453    }
1454
1455    /// Backspace in number input
1456    pub fn number_backspace(&mut self) {
1457        if let Some(item) = self.current_item_mut() {
1458            if let SettingControl::Number(ref mut n) = item.control {
1459                n.backspace();
1460            }
1461        }
1462    }
1463
1464    /// Confirm number editing
1465    pub fn number_confirm(&mut self) {
1466        if let Some(item) = self.current_item_mut() {
1467            if let SettingControl::Number(ref mut n) = item.control {
1468                n.confirm_editing();
1469            }
1470        }
1471        self.on_value_changed();
1472    }
1473
1474    /// Cancel number editing
1475    pub fn number_cancel(&mut self) {
1476        if let Some(item) = self.current_item_mut() {
1477            if let SettingControl::Number(ref mut n) = item.control {
1478                n.cancel_editing();
1479            }
1480        }
1481    }
1482
1483    /// Delete character forward in number input
1484    pub fn number_delete(&mut self) {
1485        if let Some(item) = self.current_item_mut() {
1486            if let SettingControl::Number(ref mut n) = item.control {
1487                n.delete();
1488            }
1489        }
1490    }
1491
1492    /// Move cursor left in number input
1493    pub fn number_move_left(&mut self) {
1494        if let Some(item) = self.current_item_mut() {
1495            if let SettingControl::Number(ref mut n) = item.control {
1496                n.move_left();
1497            }
1498        }
1499    }
1500
1501    /// Move cursor right in number input
1502    pub fn number_move_right(&mut self) {
1503        if let Some(item) = self.current_item_mut() {
1504            if let SettingControl::Number(ref mut n) = item.control {
1505                n.move_right();
1506            }
1507        }
1508    }
1509
1510    /// Move cursor to start of number input
1511    pub fn number_move_home(&mut self) {
1512        if let Some(item) = self.current_item_mut() {
1513            if let SettingControl::Number(ref mut n) = item.control {
1514                n.move_home();
1515            }
1516        }
1517    }
1518
1519    /// Move cursor to end of number input
1520    pub fn number_move_end(&mut self) {
1521        if let Some(item) = self.current_item_mut() {
1522            if let SettingControl::Number(ref mut n) = item.control {
1523                n.move_end();
1524            }
1525        }
1526    }
1527
1528    /// Move cursor left selecting in number input
1529    pub fn number_move_left_selecting(&mut self) {
1530        if let Some(item) = self.current_item_mut() {
1531            if let SettingControl::Number(ref mut n) = item.control {
1532                n.move_left_selecting();
1533            }
1534        }
1535    }
1536
1537    /// Move cursor right selecting in number input
1538    pub fn number_move_right_selecting(&mut self) {
1539        if let Some(item) = self.current_item_mut() {
1540            if let SettingControl::Number(ref mut n) = item.control {
1541                n.move_right_selecting();
1542            }
1543        }
1544    }
1545
1546    /// Move cursor to start selecting in number input
1547    pub fn number_move_home_selecting(&mut self) {
1548        if let Some(item) = self.current_item_mut() {
1549            if let SettingControl::Number(ref mut n) = item.control {
1550                n.move_home_selecting();
1551            }
1552        }
1553    }
1554
1555    /// Move cursor to end selecting in number input
1556    pub fn number_move_end_selecting(&mut self) {
1557        if let Some(item) = self.current_item_mut() {
1558            if let SettingControl::Number(ref mut n) = item.control {
1559                n.move_end_selecting();
1560            }
1561        }
1562    }
1563
1564    /// Move word left in number input
1565    pub fn number_move_word_left(&mut self) {
1566        if let Some(item) = self.current_item_mut() {
1567            if let SettingControl::Number(ref mut n) = item.control {
1568                n.move_word_left();
1569            }
1570        }
1571    }
1572
1573    /// Move word right in number input
1574    pub fn number_move_word_right(&mut self) {
1575        if let Some(item) = self.current_item_mut() {
1576            if let SettingControl::Number(ref mut n) = item.control {
1577                n.move_word_right();
1578            }
1579        }
1580    }
1581
1582    /// Move word left selecting in number input
1583    pub fn number_move_word_left_selecting(&mut self) {
1584        if let Some(item) = self.current_item_mut() {
1585            if let SettingControl::Number(ref mut n) = item.control {
1586                n.move_word_left_selecting();
1587            }
1588        }
1589    }
1590
1591    /// Move word right selecting in number input
1592    pub fn number_move_word_right_selecting(&mut self) {
1593        if let Some(item) = self.current_item_mut() {
1594            if let SettingControl::Number(ref mut n) = item.control {
1595                n.move_word_right_selecting();
1596            }
1597        }
1598    }
1599
1600    /// Select all text in number input
1601    pub fn number_select_all(&mut self) {
1602        if let Some(item) = self.current_item_mut() {
1603            if let SettingControl::Number(ref mut n) = item.control {
1604                n.select_all();
1605            }
1606        }
1607    }
1608
1609    /// Delete word backward in number input
1610    pub fn number_delete_word_backward(&mut self) {
1611        if let Some(item) = self.current_item_mut() {
1612            if let SettingControl::Number(ref mut n) = item.control {
1613                n.delete_word_backward();
1614            }
1615        }
1616    }
1617
1618    /// Delete word forward in number input
1619    pub fn number_delete_word_forward(&mut self) {
1620        if let Some(item) = self.current_item_mut() {
1621            if let SettingControl::Number(ref mut n) = item.control {
1622                n.delete_word_forward();
1623            }
1624        }
1625    }
1626
1627    /// Get list of pending changes for display
1628    pub fn get_change_descriptions(&self) -> Vec<String> {
1629        self.pending_changes
1630            .iter()
1631            .map(|(path, value)| {
1632                let value_str = match value {
1633                    serde_json::Value::Bool(b) => b.to_string(),
1634                    serde_json::Value::Number(n) => n.to_string(),
1635                    serde_json::Value::String(s) => format!("\"{}\"", s),
1636                    _ => value.to_string(),
1637                };
1638                format!("{}: {}", path, value_str)
1639            })
1640            .collect()
1641    }
1642}
1643
1644/// Update a control's state from a JSON value
1645fn update_control_from_value(control: &mut SettingControl, value: &serde_json::Value) {
1646    match control {
1647        SettingControl::Toggle(state) => {
1648            if let Some(b) = value.as_bool() {
1649                state.checked = b;
1650            }
1651        }
1652        SettingControl::Number(state) => {
1653            if let Some(n) = value.as_i64() {
1654                state.value = n;
1655            }
1656        }
1657        SettingControl::Dropdown(state) => {
1658            if let Some(s) = value.as_str() {
1659                if let Some(idx) = state.options.iter().position(|o| o == s) {
1660                    state.selected = idx;
1661                }
1662            }
1663        }
1664        SettingControl::Text(state) => {
1665            if let Some(s) = value.as_str() {
1666                state.value = s.to_string();
1667                state.cursor = state.value.len();
1668            }
1669        }
1670        SettingControl::TextList(state) => {
1671            if let Some(arr) = value.as_array() {
1672                state.items = arr
1673                    .iter()
1674                    .filter_map(|v| v.as_str().map(String::from))
1675                    .collect();
1676            }
1677        }
1678        SettingControl::Map(state) => {
1679            if let Some(obj) = value.as_object() {
1680                state.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1681                state.entries.sort_by(|a, b| a.0.cmp(&b.0));
1682            }
1683        }
1684        SettingControl::ObjectArray(state) => {
1685            if let Some(arr) = value.as_array() {
1686                state.bindings = arr.clone();
1687            }
1688        }
1689        SettingControl::Json(state) => {
1690            // Re-create from value with pretty printing
1691            let json_str =
1692                serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_string());
1693            let json_str = if json_str.is_empty() {
1694                "null".to_string()
1695            } else {
1696                json_str
1697            };
1698            state.original_text = json_str.clone();
1699            state.editor.set_value(&json_str);
1700            state.scroll_offset = 0;
1701        }
1702        SettingControl::Complex { .. } => {}
1703    }
1704}
1705
1706#[cfg(test)]
1707mod tests {
1708    use super::*;
1709
1710    const TEST_SCHEMA: &str = r#"
1711{
1712  "type": "object",
1713  "properties": {
1714    "theme": {
1715      "type": "string",
1716      "default": "dark"
1717    },
1718    "line_numbers": {
1719      "type": "boolean",
1720      "default": true
1721    }
1722  },
1723  "$defs": {}
1724}
1725"#;
1726
1727    fn test_config() -> Config {
1728        Config::default()
1729    }
1730
1731    #[test]
1732    fn test_settings_state_creation() {
1733        let config = test_config();
1734        let state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
1735
1736        assert!(!state.visible);
1737        assert_eq!(state.selected_category, 0);
1738        assert!(!state.has_changes());
1739    }
1740
1741    #[test]
1742    fn test_navigation() {
1743        let config = test_config();
1744        let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
1745
1746        // Start in category focus
1747        assert_eq!(state.focus_panel, FocusPanel::Categories);
1748
1749        // Toggle to settings
1750        state.toggle_focus();
1751        assert_eq!(state.focus_panel, FocusPanel::Settings);
1752
1753        // Navigate items
1754        state.select_next();
1755        assert_eq!(state.selected_item, 1);
1756
1757        state.select_prev();
1758        assert_eq!(state.selected_item, 0);
1759    }
1760
1761    #[test]
1762    fn test_pending_changes() {
1763        let config = test_config();
1764        let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
1765
1766        assert!(!state.has_changes());
1767
1768        state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
1769        assert!(state.has_changes());
1770
1771        state.discard_changes();
1772        assert!(!state.has_changes());
1773    }
1774
1775    #[test]
1776    fn test_show_hide() {
1777        let config = test_config();
1778        let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
1779
1780        assert!(!state.visible);
1781
1782        state.show();
1783        assert!(state.visible);
1784        assert_eq!(state.focus_panel, FocusPanel::Categories);
1785
1786        state.hide();
1787        assert!(!state.visible);
1788    }
1789
1790    // Schema with dropdown (enum) and number controls for testing
1791    const TEST_SCHEMA_CONTROLS: &str = r#"
1792{
1793  "type": "object",
1794  "properties": {
1795    "theme": {
1796      "type": "string",
1797      "enum": ["dark", "light", "high-contrast"],
1798      "default": "dark"
1799    },
1800    "tab_size": {
1801      "type": "integer",
1802      "minimum": 1,
1803      "maximum": 8,
1804      "default": 4
1805    },
1806    "line_numbers": {
1807      "type": "boolean",
1808      "default": true
1809    }
1810  },
1811  "$defs": {}
1812}
1813"#;
1814
1815    #[test]
1816    fn test_dropdown_toggle() {
1817        let config = test_config();
1818        let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
1819        state.show();
1820        state.toggle_focus(); // Move to settings
1821
1822        // Items are sorted alphabetically: line_numbers, tab_size, theme
1823        // Navigate to theme (dropdown) at index 2
1824        state.select_next();
1825        state.select_next();
1826        assert!(!state.is_dropdown_open());
1827
1828        state.dropdown_toggle();
1829        assert!(state.is_dropdown_open());
1830
1831        state.dropdown_toggle();
1832        assert!(!state.is_dropdown_open());
1833    }
1834
1835    #[test]
1836    fn test_dropdown_cancel_restores() {
1837        let config = test_config();
1838        let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
1839        state.show();
1840        state.toggle_focus();
1841
1842        // Items are sorted alphabetically: line_numbers, tab_size, theme
1843        // Navigate to theme (dropdown) at index 2
1844        state.select_next();
1845        state.select_next();
1846
1847        // Open dropdown
1848        state.dropdown_toggle();
1849        assert!(state.is_dropdown_open());
1850
1851        // Get initial selection
1852        let initial = state.current_item().and_then(|item| {
1853            if let SettingControl::Dropdown(ref d) = item.control {
1854                Some(d.selected)
1855            } else {
1856                None
1857            }
1858        });
1859
1860        // Change selection
1861        state.dropdown_next();
1862        let after_change = state.current_item().and_then(|item| {
1863            if let SettingControl::Dropdown(ref d) = item.control {
1864                Some(d.selected)
1865            } else {
1866                None
1867            }
1868        });
1869        assert_ne!(initial, after_change);
1870
1871        // Cancel - should restore
1872        state.dropdown_cancel();
1873        assert!(!state.is_dropdown_open());
1874
1875        let after_cancel = state.current_item().and_then(|item| {
1876            if let SettingControl::Dropdown(ref d) = item.control {
1877                Some(d.selected)
1878            } else {
1879                None
1880            }
1881        });
1882        assert_eq!(initial, after_cancel);
1883    }
1884
1885    #[test]
1886    fn test_dropdown_confirm_keeps_selection() {
1887        let config = test_config();
1888        let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
1889        state.show();
1890        state.toggle_focus();
1891
1892        // Open dropdown
1893        state.dropdown_toggle();
1894
1895        // Change selection
1896        state.dropdown_next();
1897        let after_change = state.current_item().and_then(|item| {
1898            if let SettingControl::Dropdown(ref d) = item.control {
1899                Some(d.selected)
1900            } else {
1901                None
1902            }
1903        });
1904
1905        // Confirm - should keep new selection
1906        state.dropdown_confirm();
1907        assert!(!state.is_dropdown_open());
1908
1909        let after_confirm = state.current_item().and_then(|item| {
1910            if let SettingControl::Dropdown(ref d) = item.control {
1911                Some(d.selected)
1912            } else {
1913                None
1914            }
1915        });
1916        assert_eq!(after_change, after_confirm);
1917    }
1918
1919    #[test]
1920    fn test_number_editing() {
1921        let config = test_config();
1922        let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
1923        state.show();
1924        state.toggle_focus();
1925
1926        // Navigate to tab_size (second item)
1927        state.select_next();
1928
1929        // Should not be editing yet
1930        assert!(!state.is_number_editing());
1931
1932        // Start editing
1933        state.start_number_editing();
1934        assert!(state.is_number_editing());
1935
1936        // Insert characters
1937        state.number_insert('8');
1938
1939        // Confirm
1940        state.number_confirm();
1941        assert!(!state.is_number_editing());
1942    }
1943
1944    #[test]
1945    fn test_number_cancel_editing() {
1946        let config = test_config();
1947        let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
1948        state.show();
1949        state.toggle_focus();
1950
1951        // Navigate to tab_size
1952        state.select_next();
1953
1954        // Get initial value
1955        let initial_value = state.current_item().and_then(|item| {
1956            if let SettingControl::Number(ref n) = item.control {
1957                Some(n.value)
1958            } else {
1959                None
1960            }
1961        });
1962
1963        // Start editing and make changes
1964        state.start_number_editing();
1965        state.number_backspace();
1966        state.number_insert('9');
1967        state.number_insert('9');
1968
1969        // Cancel
1970        state.number_cancel();
1971        assert!(!state.is_number_editing());
1972
1973        // Value should be unchanged (edit text was just cleared)
1974        let after_cancel = state.current_item().and_then(|item| {
1975            if let SettingControl::Number(ref n) = item.control {
1976                Some(n.value)
1977            } else {
1978                None
1979            }
1980        });
1981        assert_eq!(initial_value, after_cancel);
1982    }
1983
1984    #[test]
1985    fn test_number_backspace() {
1986        let config = test_config();
1987        let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
1988        state.show();
1989        state.toggle_focus();
1990        state.select_next();
1991
1992        state.start_number_editing();
1993        state.number_backspace();
1994
1995        // Check edit text was modified
1996        let display_text = state.current_item().and_then(|item| {
1997            if let SettingControl::Number(ref n) = item.control {
1998                Some(n.display_text())
1999            } else {
2000                None
2001            }
2002        });
2003        // Original "4" should have last char removed, leaving ""
2004        assert_eq!(display_text, Some(String::new()));
2005
2006        state.number_cancel();
2007    }
2008
2009    #[test]
2010    fn test_layer_selection() {
2011        let config = test_config();
2012        let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2013
2014        // Default is User layer
2015        assert_eq!(state.target_layer, ConfigLayer::User);
2016        assert_eq!(state.target_layer_name(), "User");
2017
2018        // Cycle through layers
2019        state.cycle_target_layer();
2020        assert_eq!(state.target_layer, ConfigLayer::Project);
2021        assert_eq!(state.target_layer_name(), "Project");
2022
2023        state.cycle_target_layer();
2024        assert_eq!(state.target_layer, ConfigLayer::Session);
2025        assert_eq!(state.target_layer_name(), "Session");
2026
2027        state.cycle_target_layer();
2028        assert_eq!(state.target_layer, ConfigLayer::User);
2029
2030        // Set directly
2031        state.set_target_layer(ConfigLayer::Project);
2032        assert_eq!(state.target_layer, ConfigLayer::Project);
2033
2034        // Setting to System should be ignored (read-only)
2035        state.set_target_layer(ConfigLayer::System);
2036        assert_eq!(state.target_layer, ConfigLayer::Project);
2037    }
2038
2039    #[test]
2040    fn test_layer_switch_clears_pending_changes() {
2041        let config = test_config();
2042        let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2043
2044        // Add a pending change
2045        state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
2046        assert!(state.has_changes());
2047
2048        // Switching layers clears pending changes
2049        state.cycle_target_layer();
2050        assert!(!state.has_changes());
2051    }
2052}