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