1use 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
17enum 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum FocusPanel {
39 #[default]
41 Categories,
42 Settings,
44 Footer,
46}
47
48#[derive(Debug)]
50pub struct SettingsState {
51 categories: Vec<SettingCategory>,
53 pub pages: Vec<SettingsPage>,
55 pub selected_category: usize,
57 pub selected_item: usize,
59 pub focus: FocusManager<FocusPanel>,
61 pub footer_button_index: usize,
63 pub pending_changes: HashMap<String, serde_json::Value>,
65 original_config: serde_json::Value,
67 pub visible: bool,
69 pub search_query: String,
71 pub search_active: bool,
73 pub search_results: Vec<SearchResult>,
75 pub selected_search_result: usize,
77 pub showing_confirm_dialog: bool,
79 pub confirm_dialog_selection: usize,
81 pub confirm_dialog_hover: Option<usize>,
83 pub showing_help: bool,
85 pub scroll_panel: ScrollablePanel,
87 pub sub_focus: Option<usize>,
89 pub editing_text: bool,
91 pub hover_position: Option<(u16, u16)>,
93 pub hover_hit: Option<SettingsHit>,
95 pub entry_dialog_stack: Vec<EntryDialogState>,
98 pub target_layer: ConfigLayer,
102 pub layer_sources: HashMap<String, ConfigLayer>,
106 pub pending_deletions: std::collections::HashSet<String>,
110}
111
112impl SettingsState {
113 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(); let target_layer = ConfigLayer::User; 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, 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 #[inline]
158 pub fn focus_panel(&self) -> FocusPanel {
159 self.focus.current().unwrap_or_default()
160 }
161
162 pub fn show(&mut self) {
164 self.visible = true;
165 self.focus.set(FocusPanel::Categories);
166 self.footer_button_index = 2; self.selected_category = 0;
168 self.selected_item = 0;
169 self.scroll_panel = ScrollablePanel::new();
170 self.sub_focus = None;
171 }
172
173 pub fn hide(&mut self) {
175 self.visible = false;
176 self.search_active = false;
177 self.search_query.clear();
178 }
179
180 pub fn entry_dialog(&self) -> Option<&EntryDialogState> {
182 self.entry_dialog_stack.last()
183 }
184
185 pub fn entry_dialog_mut(&mut self) -> Option<&mut EntryDialogState> {
187 self.entry_dialog_stack.last_mut()
188 }
189
190 pub fn has_entry_dialog(&self) -> bool {
192 !self.entry_dialog_stack.is_empty()
193 }
194
195 pub fn current_page(&self) -> Option<&SettingsPage> {
197 self.pages.get(self.selected_category)
198 }
199
200 pub fn current_page_mut(&mut self) -> Option<&mut SettingsPage> {
202 self.pages.get_mut(self.selected_category)
203 }
204
205 pub fn current_item(&self) -> Option<&SettingItem> {
207 self.current_page()
208 .and_then(|page| page.items.get(self.selected_item))
209 }
210
211 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 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 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 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 self.update_map_sub_focus();
255 }
256
257 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 { .. } => {} }
277 }
278 }
279
280 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 Some(match map_state.focused_entry {
287 Some(i) => 1 + i,
288 None => 1 + map_state.entries.len(), })
290 } else {
291 None
292 }
293 });
294 }
295
296 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); 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); }
308 }
309 FocusPanel::Settings => {
310 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 self.update_map_sub_focus();
322 } else if self.selected_item > 0 {
323 self.update_control_focus(false); self.selected_item -= 1;
325 self.sub_focus = None;
326 self.init_map_focus(false); self.update_control_focus(true); }
329 self.ensure_visible();
330 }
331 FocusPanel::Footer => {
332 if self.footer_button_index > 0 {
334 self.footer_button_index -= 1;
335 }
336 }
337 }
338 }
339
340 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); 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); }
352 }
353 FocusPanel::Settings => {
354 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 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); self.selected_item += 1;
373 self.sub_focus = None;
374 self.init_map_focus(true); self.update_control_focus(true); }
377 }
378 self.ensure_visible();
379 }
380 FocusPanel::Footer => {
381 if self.footer_button_index < 2 {
383 self.footer_button_index += 1;
384 }
385 }
386 }
387 }
388
389 pub fn toggle_focus(&mut self) {
391 let old_panel = self.focus_panel();
392 self.focus.focus_next();
393
394 if old_panel == FocusPanel::Settings {
396 self.update_control_focus(false);
397 }
398
399 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); self.update_control_focus(true); }
411
412 if self.focus_panel() == FocusPanel::Footer {
414 self.footer_button_index = 2; }
416
417 self.ensure_visible();
418 }
419
420 pub fn ensure_visible(&mut self) {
422 if self.focus_panel() != FocusPanel::Settings {
423 return;
424 }
425
426 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 pub fn set_pending_change(&mut self, path: &str, value: serde_json::Value) {
437 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 pub fn has_changes(&self) -> bool {
448 !self.pending_changes.is_empty() || !self.pending_deletions.is_empty()
449 }
450
451 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 pub fn discard_changes(&mut self) {
466 self.pending_changes.clear();
467 self.pending_deletions.clear();
468 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 pub fn set_target_layer(&mut self, layer: ConfigLayer) {
479 if layer != ConfigLayer::System {
480 self.target_layer = layer;
482 self.pending_changes.clear();
484 self.pending_deletions.clear();
485 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 pub fn cycle_target_layer(&mut self) {
497 self.target_layer = match self.target_layer {
498 ConfigLayer::System => ConfigLayer::User, ConfigLayer::User => ConfigLayer::Project,
500 ConfigLayer::Project => ConfigLayer::Session,
501 ConfigLayer::Session => ConfigLayer::User,
502 };
503 self.pending_changes.clear();
505 self.pending_deletions.clear();
506 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 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 pub fn set_layer_sources(&mut self, sources: HashMap<String, ConfigLayer>) {
528 self.layer_sources = sources;
529 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 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 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 pub fn reset_current_to_default(&mut self) {
565 let reset_info = self.current_item().and_then(|item| {
567 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 self.pending_deletions.insert(path.clone());
580 self.pending_changes.remove(&path);
582
583 if let Some(item) = self.current_item_mut() {
587 update_control_from_value(&mut item.control, &default);
588 item.modified = false;
589 item.layer_source = ConfigLayer::System; }
592 }
593 }
594
595 pub fn on_value_changed(&mut self) {
597 let target_layer = self.target_layer;
599
600 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 self.pending_deletions.remove(&path);
610
611 if let Some(item) = self.current_item_mut() {
613 item.modified = true; item.layer_source = target_layer; }
616 self.set_pending_change(&path, value);
617 }
618 }
619
620 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 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 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 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 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 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 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 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 pub fn jump_to_search_result(&mut self) {
705 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 self.update_control_focus(false);
717 self.selected_category = page_index;
718 self.selected_item = item_index;
719 self.focus.set(FocusPanel::Settings);
720 self.scroll_panel.scroll.offset = 0;
722 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); self.ensure_visible();
730 self.cancel_search();
731 }
732
733 pub fn current_search_result(&self) -> Option<&SearchResult> {
735 self.search_results.get(self.selected_search_result)
736 }
737
738 pub fn show_confirm_dialog(&mut self) {
740 self.showing_confirm_dialog = true;
741 self.confirm_dialog_selection = 0; }
743
744 pub fn hide_confirm_dialog(&mut self) {
746 self.showing_confirm_dialog = false;
747 self.confirm_dialog_selection = 0;
748 }
749
750 pub fn confirm_dialog_next(&mut self) {
752 self.confirm_dialog_selection = (self.confirm_dialog_selection + 1) % 3;
753 }
754
755 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 pub fn toggle_help(&mut self) {
766 self.showing_help = !self.showing_help;
767 }
768
769 pub fn hide_help(&mut self) {
771 self.showing_help = false;
772 }
773
774 pub fn showing_entry_dialog(&self) -> bool {
776 self.has_entry_dialog()
777 }
778
779 pub fn open_entry_dialog(&mut self) {
781 let Some(item) = self.current_item() else {
782 return;
783 };
784
785 let path = item.path.as_str();
787 let SettingControl::Map(map_state) = &item.control else {
788 return;
789 };
790
791 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 let Some(schema) = map_state.value_schema.as_ref() else {
801 return; };
803
804 let no_delete = map_state.no_add;
806
807 let dialog =
809 EntryDialogState::from_schema(key.clone(), value, schema, path, false, no_delete);
810 self.entry_dialog_stack.push(dialog);
811 }
812
813 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 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 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 let dialog =
854 EntryDialogState::for_array_item(None, &serde_json::json!({}), schema, &path, true);
855 self.entry_dialog_stack.push(dialog);
856 }
857
858 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 pub fn close_entry_dialog(&mut self) {
883 self.entry_dialog_stack.pop();
884 }
885
886 pub fn open_nested_entry_dialog(&mut self) {
891 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 let Some(entry_idx) = map_state.focused_entry {
901 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 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, })
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 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 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 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 pub fn save_entry_dialog(&mut self) {
978 let is_array = if self.entry_dialog_stack.len() > 1 {
982 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 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 fn save_map_entry_dialog_inner(&mut self) {
1004 let Some(dialog) = self.entry_dialog_stack.pop() else {
1005 return;
1006 };
1007
1008 let key = dialog.get_key();
1010 if key.is_empty() {
1011 return; }
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 if let Some(item) = self.current_item_mut() {
1022 if let SettingControl::Map(map_state) = &mut item.control {
1023 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 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 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 let path = format!("{}/{}", map_path, key);
1053 self.set_pending_change(&path, value);
1054 }
1055
1056 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 let is_nested = !self.entry_dialog_stack.is_empty();
1069
1070 if is_nested {
1071 let array_field = array_path.rsplit('/').next().unwrap_or("").to_string();
1074 let item_path = format!("/{}", array_field);
1075
1076 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 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 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 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 pub fn delete_entry_dialog(&mut self) {
1127 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 if is_nested {
1138 let map_field = dialog.map_path.rsplit('/').next().unwrap_or("").to_string();
1141 let item_path = format!("/{}", map_field);
1142
1143 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 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 self.set_pending_change(&path, serde_json::Value::Null);
1174 }
1175
1176 pub fn max_scroll(&self) -> u16 {
1178 self.scroll_panel.scroll.max_offset()
1179 }
1180
1181 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 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 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 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 pub fn stop_editing(&mut self) {
1219 self.editing_text = false;
1220 }
1221
1222 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 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 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 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 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 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 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 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 self.on_value_changed();
1376 }
1377
1378 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 self.on_value_changed();
1397 }
1398
1399 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 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 if opened {
1424 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 self.scroll_panel
1430 .ensure_focused_visible(&page.items, selected_item, None);
1431 }
1432 }
1433 }
1434
1435 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1754fn 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 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 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1858
1859 state.toggle_focus();
1861 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1862
1863 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 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(); 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 state.select_next();
1955 state.select_next();
1956
1957 state.dropdown_toggle();
1959 assert!(state.is_dropdown_open());
1960
1961 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 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 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 state.dropdown_toggle();
2004
2005 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 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 state.select_next();
2038
2039 assert!(!state.is_number_editing());
2041
2042 state.start_number_editing();
2044 assert!(state.is_number_editing());
2045
2046 state.number_insert('8');
2048
2049 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 state.select_next();
2063
2064 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 state.start_number_editing();
2075 state.number_backspace();
2076 state.number_insert('9');
2077 state.number_insert('9');
2078
2079 state.number_cancel();
2081 assert!(!state.is_number_editing());
2082
2083 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 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 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 assert_eq!(state.target_layer, ConfigLayer::User);
2126 assert_eq!(state.target_layer_name(), "User");
2127
2128 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 state.set_target_layer(ConfigLayer::Project);
2142 assert_eq!(state.target_layer, ConfigLayer::Project);
2143
2144 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 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
2156 assert!(state.has_changes());
2157
2158 state.cycle_target_layer();
2160 assert!(!state.has_changes());
2161 }
2162}