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