Skip to main content

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