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, DeepMatch, SearchResult};
11use crate::config::Config;
12use crate::config_io::ConfigLayer;
13use crate::view::controls::FocusState;
14use crate::view::ui::{FocusManager, ScrollItem, ScrollablePanel};
15use std::collections::HashMap;
16
17fn set_json_pointer_create(root: &mut serde_json::Value, pointer: &str, value: serde_json::Value) {
20 if pointer.is_empty() || pointer == "/" {
21 *root = value;
22 return;
23 }
24 let parts: Vec<&str> = pointer.trim_start_matches('/').split('/').collect();
25 let mut current = root;
26 for (i, part) in parts.iter().enumerate() {
27 if i == parts.len() - 1 {
28 if let serde_json::Value::Object(map) = current {
29 map.insert(part.to_string(), value);
30 }
31 return;
32 }
33 if let serde_json::Value::Object(map) = current {
34 if !map.contains_key(*part) {
35 map.insert(
36 part.to_string(),
37 serde_json::Value::Object(Default::default()),
38 );
39 }
40 current = map.get_mut(*part).unwrap();
41 } else {
42 return;
43 }
44 }
45}
46
47enum NestedDialogInfo {
49 MapEntry {
50 key: String,
51 value: serde_json::Value,
52 schema: SettingSchema,
53 path: String,
54 is_new: bool,
55 no_delete: bool,
56 },
57 ArrayItem {
58 index: Option<usize>,
59 value: serde_json::Value,
60 schema: SettingSchema,
61 path: String,
62 is_new: bool,
63 },
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68pub enum FocusPanel {
69 #[default]
71 Categories,
72 Settings,
74 Footer,
76}
77
78#[derive(Debug)]
80pub struct SettingsState {
81 categories: Vec<SettingCategory>,
83 pub pages: Vec<SettingsPage>,
85 pub selected_category: usize,
87 pub selected_item: usize,
89 pub focus: FocusManager<FocusPanel>,
91 pub footer_button_index: usize,
93 pub pending_changes: HashMap<String, serde_json::Value>,
95 original_config: serde_json::Value,
97 pub visible: bool,
99 pub search_query: String,
101 pub search_active: bool,
103 pub search_results: Vec<SearchResult>,
105 pub selected_search_result: usize,
107 pub search_scroll_offset: usize,
109 pub search_max_visible: usize,
111 pub showing_confirm_dialog: bool,
113 pub confirm_dialog_selection: usize,
115 pub confirm_dialog_hover: Option<usize>,
117 pub showing_reset_dialog: bool,
119 pub reset_dialog_selection: usize,
121 pub reset_dialog_hover: Option<usize>,
123 pub showing_help: bool,
125 pub scroll_panel: ScrollablePanel,
127 pub sub_focus: Option<usize>,
129 pub editing_text: bool,
131 pub hover_position: Option<(u16, u16)>,
133 pub hover_hit: Option<SettingsHit>,
135 pub entry_dialog_stack: Vec<EntryDialogState>,
138 pub target_layer: ConfigLayer,
142 available_status_bar_tokens: HashMap<String, String>,
145 pub layer_sources: HashMap<String, ConfigLayer>,
149 pub pending_deletions: std::collections::HashSet<String>,
153 pub layout_width: u16,
157 pub item_style: super::items::ItemBoxStyle,
160 pub expanded_categories: std::collections::HashSet<usize>,
164 pub categories_scroll: ScrollablePanel,
167 pub tree_cursor_section: Option<usize>,
179}
180
181#[derive(Debug, Clone, Copy)]
188pub enum TreeRow {
189 Category {
190 idx: usize,
191 expandable: bool,
192 expanded: bool,
193 },
194 Section {
195 cat_idx: usize,
196 section_idx: usize,
197 },
198}
199
200impl crate::view::ui::ScrollItem for TreeRow {
201 fn height(&self, _width: u16) -> u16 {
202 1
203 }
204}
205
206impl SettingsState {
207 pub fn new(schema_json: &str, config: &Config) -> Result<Self, serde_json::Error> {
209 Self::new_with_plugin_schemas(schema_json, config, &HashMap::new())
210 }
211
212 pub fn new_with_plugin_schemas(
216 schema_json: &str,
217 config: &Config,
218 plugin_schemas: &HashMap<String, serde_json::Value>,
219 ) -> Result<Self, serde_json::Error> {
220 let mut categories = parse_schema(schema_json)?;
221
222 let mut enabled_with_schema: Vec<String> = config
224 .plugins
225 .iter()
226 .filter_map(|(name, cfg)| {
227 if cfg.enabled && plugin_schemas.contains_key(name) {
228 Some(name.clone())
229 } else {
230 None
231 }
232 })
233 .collect();
234 enabled_with_schema.sort();
235 tracing::trace!(
236 "SettingsState built: total plugin_schemas={}, enabled_with_schema={:?}",
237 plugin_schemas.len(),
238 enabled_with_schema
239 );
240 super::schema::append_plugin_settings_category(
241 &mut categories,
242 plugin_schemas,
243 &enabled_with_schema,
244 );
245
246 let config_value = serde_json::to_value(config)?;
247 let layer_sources = HashMap::new(); let target_layer = ConfigLayer::User; let available_status_bar_tokens: HashMap<String, String> = HashMap::new();
250 let pages = super::items::build_pages(
251 &categories,
252 &config_value,
253 &layer_sources,
254 target_layer,
255 &available_status_bar_tokens,
256 );
257
258 Ok(Self {
259 categories,
260 pages,
261 selected_category: 0,
262 selected_item: 0,
263 focus: FocusManager::new(vec![
264 FocusPanel::Categories,
265 FocusPanel::Settings,
266 FocusPanel::Footer,
267 ]),
268 footer_button_index: 2, pending_changes: HashMap::new(),
270 original_config: config_value,
271 visible: false,
272 search_query: String::new(),
273 search_active: false,
274 search_results: Vec::new(),
275 selected_search_result: 0,
276 search_scroll_offset: 0,
277 search_max_visible: 5, showing_confirm_dialog: false,
279 confirm_dialog_selection: 0,
280 confirm_dialog_hover: None,
281 showing_reset_dialog: false,
282 reset_dialog_selection: 0,
283 reset_dialog_hover: None,
284 showing_help: false,
285 scroll_panel: ScrollablePanel::new(),
286 sub_focus: None,
287 editing_text: false,
288 available_status_bar_tokens,
289 hover_position: None,
290 hover_hit: None,
291 entry_dialog_stack: Vec::new(),
292 target_layer,
293 layer_sources,
294 pending_deletions: std::collections::HashSet::new(),
295 layout_width: 0,
296 item_style: super::items::ItemBoxStyle::default(),
297 expanded_categories: std::collections::HashSet::new(),
298 categories_scroll: ScrollablePanel::new(),
299 tree_cursor_section: None,
300 })
301 }
302
303 #[inline]
305 pub fn focus_panel(&self) -> FocusPanel {
306 self.focus.current().unwrap_or_default()
307 }
308
309 pub fn show(&mut self) {
311 self.visible = true;
312 self.focus.set(FocusPanel::Categories);
313 self.footer_button_index = 2; self.selected_category = 0;
315 self.selected_item = 0;
316 self.scroll_panel = ScrollablePanel::new();
317 self.sub_focus = None;
318 self.showing_confirm_dialog = false;
320 self.confirm_dialog_selection = 0;
321 self.confirm_dialog_hover = None;
322 self.showing_reset_dialog = false;
323 self.reset_dialog_selection = 0;
324 self.reset_dialog_hover = None;
325 self.showing_help = false;
326 }
327
328 fn rebuild_pages(&mut self) {
330 self.pages = super::items::build_pages(
331 &self.categories,
332 &self.original_config,
333 &self.layer_sources,
334 self.target_layer,
335 &self.available_status_bar_tokens,
336 );
337 }
338
339 pub fn hide(&mut self) {
341 self.visible = false;
342 self.search_active = false;
343 self.search_query.clear();
344 }
345
346 pub fn entry_dialog(&self) -> Option<&EntryDialogState> {
348 self.entry_dialog_stack.last()
349 }
350
351 pub fn entry_dialog_mut(&mut self) -> Option<&mut EntryDialogState> {
353 self.entry_dialog_stack.last_mut()
354 }
355
356 pub fn has_entry_dialog(&self) -> bool {
358 !self.entry_dialog_stack.is_empty()
359 }
360
361 pub fn current_page(&self) -> Option<&SettingsPage> {
363 self.pages.get(self.selected_category)
364 }
365
366 pub fn current_page_mut(&mut self) -> Option<&mut SettingsPage> {
368 self.pages.get_mut(self.selected_category)
369 }
370
371 pub fn topmost_visible_item_index(&self) -> Option<usize> {
376 let page = self.pages.get(self.selected_category)?;
377 if page.items.is_empty() {
378 return None;
379 }
380 let target = self.scroll_panel.scroll.offset;
381 let width = self.layout_width;
382 let mut y: u16 = 0;
383 for (idx, item) in page.items.iter().enumerate() {
384 let h = <SettingItem as ScrollItem>::height(item, width);
385 if y + h > target {
386 return Some(idx);
387 }
388 y += h;
389 }
390 Some(page.items.len() - 1)
391 }
392
393 pub fn current_section_index(&self) -> Option<usize> {
398 let page = self.pages.get(self.selected_category)?;
399 if page.sections.is_empty() {
400 return None;
401 }
402 let item_idx = self
411 .topmost_visible_item_index()
412 .unwrap_or(self.selected_item);
413 let mut current: Option<usize> = None;
415 for (s_idx, section) in page.sections.iter().enumerate() {
416 if section.first_item_index <= item_idx {
417 current = Some(s_idx);
418 } else {
419 break;
420 }
421 }
422 current
423 }
424
425 pub fn is_category_expandable(&self, cat_idx: usize) -> bool {
429 self.pages
430 .get(cat_idx)
431 .is_some_and(|p| p.sections.len() > 1)
432 }
433
434 pub fn tree_step(&mut self, delta: i32) {
444 let rows = self.visible_tree();
445 if rows.is_empty() {
446 return;
447 }
448 let cur = self.tree_cursor_index(&rows);
449 let len = rows.len() as i32;
450 let target = (cur as i32 + delta).clamp(0, len - 1) as usize;
451 if target == cur {
452 return;
453 }
454 let prev_category = self.selected_category;
455 self.update_control_focus(false);
456 match rows[target] {
457 TreeRow::Category { idx, .. } => {
458 self.selected_category = idx;
462 self.selected_item = 0;
463 self.tree_cursor_section = None;
464 if idx != prev_category {
465 self.scroll_panel = ScrollablePanel::new();
466 }
467 self.sub_focus = None;
468 self.update_control_focus(true);
469 }
470 TreeRow::Section {
471 cat_idx,
472 section_idx,
473 } => {
474 let first = self.pages[cat_idx].sections[section_idx].first_item_index;
475 self.selected_category = cat_idx;
476 self.selected_item = first;
477 self.tree_cursor_section = Some(section_idx);
478 if cat_idx != prev_category {
479 self.scroll_panel = ScrollablePanel::new();
480 }
481 self.sub_focus = None;
482 self.init_map_focus(true);
483 self.update_control_focus(true);
484 }
485 }
486 let width = self.layout_width;
492 if let Some(page) = self.pages.get(self.selected_category) {
493 self.scroll_panel.update_content_height(&page.items, width);
494 if matches!(rows[target], TreeRow::Section { .. }) {
502 let item_y =
503 self.scroll_panel
504 .item_y_offset(&page.items, self.selected_item, width);
505 self.scroll_panel.scroll.offset = item_y;
506 } else {
507 let selected_item = self.selected_item;
508 let sub_focus = self.sub_focus;
509 self.scroll_panel.ensure_focused_visible(
510 &page.items,
511 selected_item,
512 sub_focus,
513 width,
514 );
515 }
516 }
517 let new_rows = self.visible_tree();
518 let new_cur = self.tree_cursor_index(&new_rows);
519 self.categories_scroll
520 .ensure_focused_visible(&new_rows, new_cur, None, width);
521 }
522
523 pub(super) fn tree_cursor_index(&self, rows: &[TreeRow]) -> usize {
531 let cat = self.selected_category;
532 if let Some(s_idx) = self.tree_cursor_section {
533 for (i, row) in rows.iter().enumerate() {
534 if let TreeRow::Section {
535 cat_idx,
536 section_idx,
537 } = *row
538 {
539 if cat_idx == cat && section_idx == s_idx {
540 return i;
541 }
542 }
543 }
544 }
545 for (i, row) in rows.iter().enumerate() {
546 if let TreeRow::Category { idx, .. } = *row {
547 if idx == cat {
548 return i;
549 }
550 }
551 }
552 0
553 }
554
555 pub fn auto_expand_current_category(&mut self) {
565 let idx = self.selected_category;
566 if self.is_category_expandable(idx) {
567 self.expanded_categories.insert(idx);
568 }
569 }
570
571 pub fn toggle_category_expanded(&mut self, cat_idx: usize) {
572 if !self.is_category_expandable(cat_idx) {
573 return;
574 }
575 if !self.expanded_categories.insert(cat_idx) {
576 self.expanded_categories.remove(&cat_idx);
577 }
578 }
579
580 pub fn jump_to_section(&mut self, cat_idx: usize, section_idx: usize) {
584 let Some(page) = self.pages.get(cat_idx) else {
585 return;
586 };
587 let Some(section) = page.sections.get(section_idx) else {
588 return;
589 };
590 let target_item = section.first_item_index;
591 self.update_control_focus(false);
592 self.selected_category = cat_idx;
593 self.selected_item = target_item;
594 self.tree_cursor_section = Some(section_idx);
595 self.focus.set(FocusPanel::Settings);
596 let width = self.layout_width;
597 if let Some(page) = self.pages.get(self.selected_category) {
598 self.scroll_panel.update_content_height(&page.items, width);
599 let item_y = self
606 .scroll_panel
607 .item_y_offset(&page.items, target_item, width);
608 self.scroll_panel.scroll.offset = item_y;
609 }
610 self.sub_focus = None;
611 self.init_map_focus(true);
612 self.update_control_focus(true);
613 self.auto_expand_current_category();
614 }
615
616 pub fn visible_tree(&self) -> Vec<TreeRow> {
620 let mut rows = Vec::with_capacity(self.pages.len());
621 for (idx, page) in self.pages.iter().enumerate() {
622 let expandable = page.sections.len() > 1;
623 let expanded = expandable && self.expanded_categories.contains(&idx);
624 rows.push(TreeRow::Category {
625 idx,
626 expandable,
627 expanded,
628 });
629 if expanded {
630 for section_idx in 0..page.sections.len() {
631 rows.push(TreeRow::Section {
632 cat_idx: idx,
633 section_idx,
634 });
635 }
636 }
637 }
638 rows
639 }
640
641 pub fn current_item(&self) -> Option<&SettingItem> {
643 self.current_page()
644 .and_then(|page| page.items.get(self.selected_item))
645 }
646
647 pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
649 self.pages
650 .get_mut(self.selected_category)
651 .and_then(|page| page.items.get_mut(self.selected_item))
652 }
653
654 pub fn can_exit_text_editing(&self) -> bool {
656 self.current_item()
657 .map(|item| {
658 if let SettingControl::Text(state) = &item.control {
659 state.is_valid()
660 } else {
661 true
662 }
663 })
664 .unwrap_or(true)
665 }
666
667 pub fn entry_dialog_can_exit_text_editing(&self) -> bool {
669 self.entry_dialog()
670 .and_then(|dialog| dialog.current_item())
671 .map(|item| {
672 if let SettingControl::Text(state) = &item.control {
673 state.is_valid()
674 } else {
675 true
676 }
677 })
678 .unwrap_or(true)
679 }
680
681 fn init_map_focus(&mut self, from_above: bool) {
684 if let Some(item) = self.current_item_mut() {
685 if let SettingControl::Map(ref mut map_state) = item.control {
686 map_state.init_focus(from_above);
687 }
688 }
689 self.update_map_sub_focus();
691 }
692
693 pub(super) fn update_control_focus(&mut self, focused: bool) {
697 let focus_state = if focused {
698 FocusState::Focused
699 } else {
700 FocusState::Normal
701 };
702 if let Some(item) = self.current_item_mut() {
703 match &mut item.control {
704 SettingControl::Map(ref mut state) => state.focus = focus_state,
705 SettingControl::TextList(ref mut state) => state.focus = focus_state,
706 SettingControl::DualList(ref mut state) => state.focus = focus_state,
707 SettingControl::ObjectArray(ref mut state) => state.focus = focus_state,
708 SettingControl::Toggle(ref mut state) => state.focus = focus_state,
709 SettingControl::Number(ref mut state) => state.focus = focus_state,
710 SettingControl::Dropdown(ref mut state) => state.focus = focus_state,
711 SettingControl::Text(ref mut state) => {
712 state.focus = focus_state;
713 if !focused {
717 state.editing = false;
718 }
719 }
720 SettingControl::Json(_) | SettingControl::Complex { .. } => {} }
722 }
723 }
724
725 fn update_map_sub_focus(&mut self) {
728 self.sub_focus = self.current_item().and_then(|item| {
729 if let SettingControl::Map(ref map_state) = item.control {
730 Some(match map_state.focused_entry {
732 Some(i) => 1 + i,
733 None => 1 + map_state.entries.len(), })
735 } else {
736 None
737 }
738 });
739 }
740
741 pub fn select_prev(&mut self) {
743 match self.focus_panel() {
744 FocusPanel::Categories => {
745 self.tree_step(-1);
746 }
747 FocusPanel::Settings => {
748 let handled = self
750 .current_item_mut()
751 .and_then(|item| match &mut item.control {
752 SettingControl::Map(map_state) => Some(map_state.focus_prev()),
753 _ => None,
754 })
755 .unwrap_or(false);
756
757 if handled {
758 self.update_map_sub_focus();
760 } else if self.selected_item > 0 {
761 self.update_control_focus(false); self.selected_item -= 1;
763 self.sub_focus = None;
764 self.init_map_focus(false); self.update_control_focus(true); }
767 self.ensure_visible();
768 }
769 FocusPanel::Footer => {
770 if self.footer_button_index > 0 {
772 self.footer_button_index -= 1;
773 }
774 }
775 }
776 }
777
778 pub fn select_next(&mut self) {
780 match self.focus_panel() {
781 FocusPanel::Categories => {
782 self.tree_step(1);
783 }
784 FocusPanel::Settings => {
785 let handled = self
787 .current_item_mut()
788 .and_then(|item| match &mut item.control {
789 SettingControl::Map(map_state) => Some(map_state.focus_next()),
790 _ => None,
791 })
792 .unwrap_or(false);
793
794 if handled {
795 self.update_map_sub_focus();
797 } else {
798 let can_move = self
799 .current_page()
800 .is_some_and(|page| self.selected_item + 1 < page.items.len());
801 if can_move {
802 self.update_control_focus(false); self.selected_item += 1;
804 self.sub_focus = None;
805 self.init_map_focus(true); self.update_control_focus(true); }
808 }
809 self.ensure_visible();
810 }
811 FocusPanel::Footer => {
812 if self.footer_button_index < 2 {
814 self.footer_button_index += 1;
815 }
816 }
817 }
818 }
819
820 pub fn select_next_page(&mut self) {
822 let page_size = self.scroll_panel.viewport_height().max(1);
823 for _ in 0..page_size {
824 self.select_next();
825 }
826 }
827
828 pub fn select_prev_page(&mut self) {
830 let page_size = self.scroll_panel.viewport_height().max(1);
831 for _ in 0..page_size {
832 self.select_prev();
833 }
834 }
835
836 pub fn toggle_focus(&mut self) {
838 let old_panel = self.focus_panel();
839 self.focus.focus_next();
840 self.on_panel_changed(old_panel, true);
841 }
842
843 pub fn toggle_focus_backward(&mut self) {
845 let old_panel = self.focus_panel();
846 self.focus.focus_prev();
847 self.on_panel_changed(old_panel, false);
848 }
849
850 fn on_panel_changed(&mut self, old_panel: FocusPanel, forward: bool) {
852 if old_panel == FocusPanel::Settings {
854 self.update_control_focus(false);
855 }
856
857 if self.focus_panel() == FocusPanel::Settings
859 && self.selected_item >= self.current_page().map_or(0, |p| p.items.len())
860 {
861 self.selected_item = 0;
862 }
863 self.sub_focus = None;
864
865 if self.focus_panel() == FocusPanel::Settings {
866 self.init_map_focus(forward); self.update_control_focus(true); }
869
870 if self.focus_panel() == FocusPanel::Footer {
872 self.footer_button_index = if forward {
873 0 } else {
875 4 };
877 }
878
879 self.ensure_visible();
880 }
881
882 pub fn set_item_style(&mut self, style: super::items::ItemBoxStyle) {
890 if self.item_style == style {
891 return;
892 }
893 self.item_style = style;
894 for page in &mut self.pages {
895 for item in &mut page.items {
896 item.style = style;
897 }
898 }
899 let width = self.layout_width;
900 if let Some(page) = self.pages.get(self.selected_category) {
901 self.scroll_panel.update_content_height(&page.items, width);
902 }
903 }
904
905 pub fn ensure_visible(&mut self) {
907 if self.focus_panel() != FocusPanel::Settings {
908 return;
909 }
910
911 let selected_item = self.selected_item;
913 let sub_focus = self.sub_focus;
914 let width = self.layout_width;
915 let prev_offset = self.scroll_panel.scroll.offset;
916 if let Some(page) = self.pages.get(self.selected_category) {
917 self.scroll_panel
918 .ensure_focused_visible(&page.items, selected_item, sub_focus, width);
919 }
920 if self.scroll_panel.scroll.offset != prev_offset {
924 self.sync_tree_cursor_to_body_scroll();
925 }
926 }
927
928 pub fn set_pending_change(&mut self, path: &str, value: serde_json::Value) {
930 let original = self.original_config.pointer(path);
932 if original == Some(&value) {
933 self.pending_changes.remove(path);
934 } else {
935 self.pending_changes.insert(path.to_string(), value);
936 }
937 }
938
939 pub fn has_changes(&self) -> bool {
941 !self.pending_changes.is_empty() || !self.pending_deletions.is_empty()
942 }
943
944 pub fn apply_changes(&self, config: &Config) -> Result<Config, serde_json::Error> {
946 let mut config_value = serde_json::to_value(config)?;
947
948 for (path, value) in &self.pending_changes {
949 if let Some(target) = config_value.pointer_mut(path) {
956 *target = value.clone();
957 } else {
958 set_json_pointer_create(&mut config_value, path, value.clone());
959 }
960 }
961
962 serde_json::from_value(config_value)
963 }
964
965 pub fn discard_changes(&mut self) {
967 self.pending_changes.clear();
968 self.pending_deletions.clear();
969 self.rebuild_pages();
971 }
972
973 pub fn set_target_layer(&mut self, layer: ConfigLayer) {
975 if layer != ConfigLayer::System {
976 self.target_layer = layer;
978 self.pending_changes.clear();
980 self.pending_deletions.clear();
981 self.rebuild_pages();
983 }
984 }
985
986 pub fn cycle_target_layer(&mut self) {
988 self.target_layer = match self.target_layer {
989 ConfigLayer::System => ConfigLayer::User, ConfigLayer::User => ConfigLayer::Project,
991 ConfigLayer::Project => ConfigLayer::Session,
992 ConfigLayer::Session => ConfigLayer::User,
993 };
994 self.pending_changes.clear();
996 self.pending_deletions.clear();
997 self.rebuild_pages();
999 }
1000
1001 pub fn target_layer_name(&self) -> &'static str {
1003 match self.target_layer {
1004 ConfigLayer::System => "System (read-only)",
1005 ConfigLayer::User => "User",
1006 ConfigLayer::Project => "Project",
1007 ConfigLayer::Session => "Session",
1008 }
1009 }
1010
1011 pub fn set_layer_sources(&mut self, sources: HashMap<String, ConfigLayer>) {
1014 self.layer_sources = sources;
1015 self.rebuild_pages();
1017 }
1018
1019 pub fn set_status_bar_tokens(&mut self, tokens: HashMap<String, String>) {
1022 self.available_status_bar_tokens = tokens;
1023 self.rebuild_pages();
1024 }
1025
1026 pub fn get_layer_source(&self, path: &str) -> ConfigLayer {
1029 self.layer_sources
1030 .get(path)
1031 .copied()
1032 .unwrap_or(ConfigLayer::System)
1033 }
1034
1035 pub fn layer_source_label(layer: ConfigLayer) -> &'static str {
1037 match layer {
1038 ConfigLayer::System => "default",
1039 ConfigLayer::User => "user",
1040 ConfigLayer::Project => "project",
1041 ConfigLayer::Session => "session",
1042 }
1043 }
1044
1045 pub fn reset_current_to_default(&mut self) {
1053 let reset_info = self.current_item().and_then(|item| {
1055 if !item.modified || item.is_auto_managed {
1058 return None;
1059 }
1060 item.default
1061 .as_ref()
1062 .map(|default| (item.path.clone(), default.clone()))
1063 });
1064
1065 if let Some((path, default)) = reset_info {
1066 self.pending_deletions.insert(path.clone());
1068 self.pending_changes.remove(&path);
1070
1071 if let Some(item) = self.current_item_mut() {
1075 update_control_from_value(&mut item.control, &default);
1076 item.modified = false;
1077 item.layer_source = ConfigLayer::System; }
1080 }
1081 }
1082
1083 pub fn set_current_to_null(&mut self) {
1089 let target_layer = self.target_layer;
1090 let change_info = self.current_item().and_then(|item| {
1091 if !item.nullable || item.is_null || item.read_only {
1092 return None;
1093 }
1094 Some(item.path.clone())
1095 });
1096
1097 if let Some(path) = change_info {
1098 self.pending_changes
1100 .insert(path.clone(), serde_json::Value::Null);
1101 self.pending_deletions.remove(&path);
1102
1103 if let Some(item) = self.current_item_mut() {
1105 item.is_null = true;
1106 item.modified = true;
1107 item.layer_source = target_layer;
1108 }
1109 }
1110 }
1111
1112 pub fn clear_current_category(&mut self) {
1118 let target_layer = self.target_layer;
1119 let page = match self.current_page() {
1120 Some(p) if p.nullable => p,
1121 _ => return,
1122 };
1123 let page_path = page.path.clone();
1124
1125 self.pending_changes
1127 .insert(page_path.clone(), serde_json::Value::Null);
1128
1129 let prefix = format!("{}/", page_path);
1131 self.pending_changes
1132 .retain(|path, _| !path.starts_with(&prefix));
1133 self.pending_deletions
1134 .retain(|path| !path.starts_with(&prefix));
1135
1136 if let Some(page) = self.current_page_mut() {
1138 for item in &mut page.items {
1139 if item.nullable {
1140 item.is_null = true;
1141 item.modified = false;
1142 item.layer_source = target_layer;
1143 }
1144 }
1145 }
1146 }
1147
1148 pub fn current_category_has_values(&self) -> bool {
1150 match self.current_page() {
1151 Some(page) if page.nullable => {
1152 page.items.iter().any(|item| !item.is_null && item.nullable)
1153 || page.items.iter().any(|item| item.modified)
1154 }
1155 _ => false,
1156 }
1157 }
1158
1159 pub fn on_value_changed(&mut self) {
1161 let target_layer = self.target_layer;
1163
1164 let change_info = self.current_item().map(|item| {
1166 let value = control_to_value(&item.control);
1167 (item.path.clone(), value)
1168 });
1169
1170 if let Some((path, value)) = change_info {
1171 self.pending_deletions.remove(&path);
1174
1175 if let Some(item) = self.current_item_mut() {
1177 item.modified = true; item.layer_source = target_layer; item.is_null = false; }
1181 self.set_pending_change(&path, value);
1182 }
1183 }
1184
1185 pub fn update_focus_states(&mut self) {
1187 let current_focus = self.focus_panel();
1188 for (page_idx, page) in self.pages.iter_mut().enumerate() {
1189 for (item_idx, item) in page.items.iter_mut().enumerate() {
1190 let is_focused = current_focus == FocusPanel::Settings
1191 && page_idx == self.selected_category
1192 && item_idx == self.selected_item;
1193
1194 let focus = if is_focused {
1195 FocusState::Focused
1196 } else {
1197 FocusState::Normal
1198 };
1199
1200 match &mut item.control {
1201 SettingControl::Toggle(state) => state.focus = focus,
1202 SettingControl::Number(state) => state.focus = focus,
1203 SettingControl::Dropdown(state) => state.focus = focus,
1204 SettingControl::Text(state) => state.focus = focus,
1205 SettingControl::TextList(state) => state.focus = focus,
1206 SettingControl::DualList(state) => state.focus = focus,
1207 SettingControl::Map(state) => state.focus = focus,
1208 SettingControl::ObjectArray(state) => state.focus = focus,
1209 SettingControl::Json(state) => state.focus = focus,
1210 SettingControl::Complex { .. } => {}
1211 }
1212 }
1213 }
1214 }
1215
1216 pub fn start_search(&mut self) {
1218 self.search_active = true;
1219 self.search_query.clear();
1220 self.search_results.clear();
1221 self.selected_search_result = 0;
1222 self.search_scroll_offset = 0;
1223 }
1224
1225 pub fn cancel_search(&mut self) {
1227 self.search_active = false;
1228 self.search_query.clear();
1229 self.search_results.clear();
1230 self.selected_search_result = 0;
1231 self.search_scroll_offset = 0;
1232 }
1233
1234 pub fn set_search_query(&mut self, query: String) {
1236 self.search_query = query;
1237 self.search_results = search_settings(&self.pages, &self.search_query);
1238 self.selected_search_result = 0;
1239 self.search_scroll_offset = 0;
1240 }
1241
1242 pub fn search_push_char(&mut self, c: char) {
1244 self.search_query.push(c);
1245 self.search_results = search_settings(&self.pages, &self.search_query);
1246 self.selected_search_result = 0;
1247 self.search_scroll_offset = 0;
1248 }
1249
1250 pub fn search_pop_char(&mut self) {
1252 self.search_query.pop();
1253 self.search_results = search_settings(&self.pages, &self.search_query);
1254 self.selected_search_result = 0;
1255 self.search_scroll_offset = 0;
1256 }
1257
1258 pub fn search_prev(&mut self) {
1260 if !self.search_results.is_empty() && self.selected_search_result > 0 {
1261 self.selected_search_result -= 1;
1262 if self.selected_search_result < self.search_scroll_offset {
1264 self.search_scroll_offset = self.selected_search_result;
1265 }
1266 }
1267 }
1268
1269 pub fn search_next(&mut self) {
1271 if !self.search_results.is_empty()
1272 && self.selected_search_result + 1 < self.search_results.len()
1273 {
1274 self.selected_search_result += 1;
1275 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1277 self.search_scroll_offset =
1278 self.selected_search_result - self.search_max_visible + 1;
1279 }
1280 }
1281 }
1282
1283 pub fn search_scroll_up(&mut self, delta: usize) -> bool {
1285 if self.search_results.is_empty() || self.search_scroll_offset == 0 {
1286 return false;
1287 }
1288 self.search_scroll_offset = self.search_scroll_offset.saturating_sub(delta);
1289 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1291 self.selected_search_result = self.search_scroll_offset + self.search_max_visible - 1;
1292 }
1293 true
1294 }
1295
1296 pub fn search_scroll_down(&mut self, delta: usize) -> bool {
1298 if self.search_results.is_empty() {
1299 return false;
1300 }
1301 let max_offset = self
1302 .search_results
1303 .len()
1304 .saturating_sub(self.search_max_visible);
1305 if self.search_scroll_offset >= max_offset {
1306 return false;
1307 }
1308 self.search_scroll_offset = (self.search_scroll_offset + delta).min(max_offset);
1309 if self.selected_search_result < self.search_scroll_offset {
1311 self.selected_search_result = self.search_scroll_offset;
1312 }
1313 true
1314 }
1315
1316 pub fn search_scroll_to_ratio(&mut self, ratio: f32) -> bool {
1318 if self.search_results.is_empty() {
1319 return false;
1320 }
1321 let max_offset = self
1322 .search_results
1323 .len()
1324 .saturating_sub(self.search_max_visible);
1325 let new_offset = (ratio * max_offset as f32) as usize;
1326 if new_offset != self.search_scroll_offset {
1327 self.search_scroll_offset = new_offset.min(max_offset);
1328 if self.selected_search_result < self.search_scroll_offset {
1330 self.selected_search_result = self.search_scroll_offset;
1331 } else if self.selected_search_result
1332 >= self.search_scroll_offset + self.search_max_visible
1333 {
1334 self.selected_search_result =
1335 self.search_scroll_offset + self.search_max_visible - 1;
1336 }
1337 return true;
1338 }
1339 false
1340 }
1341
1342 pub fn jump_to_search_result(&mut self) {
1344 let Some(result) = self
1346 .search_results
1347 .get(self.selected_search_result)
1348 .cloned()
1349 else {
1350 return;
1351 };
1352 let page_index = result.page_index;
1353 let item_index = result.item_index;
1354
1355 self.update_control_focus(false);
1357 self.selected_category = page_index;
1358 self.selected_item = item_index;
1359 self.focus.set(FocusPanel::Settings);
1360 self.scroll_panel.scroll.offset = 0;
1362 let width = self.layout_width;
1364 if let Some(page) = self.pages.get(self.selected_category) {
1365 self.scroll_panel.update_content_height(&page.items, width);
1366 }
1367 self.sub_focus = None;
1368 self.init_map_focus(true);
1369
1370 if let Some(ref deep_match) = result.deep_match {
1372 self.jump_to_deep_match(deep_match);
1373 }
1374
1375 self.update_control_focus(true); self.auto_expand_current_category();
1377 self.tree_cursor_section = self.current_section_index();
1381 self.ensure_visible();
1382 self.cancel_search();
1383 }
1384
1385 fn jump_to_deep_match(&mut self, deep_match: &DeepMatch) {
1387 match deep_match {
1388 DeepMatch::MapKey { entry_index, .. } | DeepMatch::MapValue { entry_index, .. } => {
1389 if let Some(item) = self.current_item_mut() {
1390 if let SettingControl::Map(ref mut map_state) = item.control {
1391 map_state.focused_entry = Some(*entry_index);
1392 }
1393 }
1394 self.update_map_sub_focus();
1395 }
1396 DeepMatch::TextListItem { item_index, .. } => {
1397 if let Some(item) = self.current_item_mut() {
1398 if let SettingControl::TextList(ref mut list_state) = item.control {
1399 list_state.focused_item = Some(*item_index);
1400 }
1401 }
1402 self.sub_focus = Some(1 + *item_index);
1404 }
1405 }
1406 }
1407
1408 pub fn current_search_result(&self) -> Option<&SearchResult> {
1410 self.search_results.get(self.selected_search_result)
1411 }
1412
1413 pub fn show_confirm_dialog(&mut self) {
1415 self.showing_confirm_dialog = true;
1416 self.confirm_dialog_selection = 0; }
1418
1419 pub fn hide_confirm_dialog(&mut self) {
1421 self.showing_confirm_dialog = false;
1422 self.confirm_dialog_selection = 0;
1423 }
1424
1425 pub fn confirm_dialog_next(&mut self) {
1427 self.confirm_dialog_selection = (self.confirm_dialog_selection + 1) % 3;
1428 }
1429
1430 pub fn confirm_dialog_prev(&mut self) {
1432 self.confirm_dialog_selection = if self.confirm_dialog_selection == 0 {
1433 2
1434 } else {
1435 self.confirm_dialog_selection - 1
1436 };
1437 }
1438
1439 pub fn toggle_help(&mut self) {
1441 self.showing_help = !self.showing_help;
1442 }
1443
1444 pub fn hide_help(&mut self) {
1446 self.showing_help = false;
1447 }
1448
1449 pub fn showing_entry_dialog(&self) -> bool {
1451 self.has_entry_dialog()
1452 }
1453
1454 pub fn open_entry_dialog(&mut self) {
1456 let Some(item) = self.current_item() else {
1457 return;
1458 };
1459
1460 let path = item.path.as_str();
1462 let SettingControl::Map(map_state) = &item.control else {
1463 return;
1464 };
1465
1466 let Some(entry_idx) = map_state.focused_entry else {
1468 return;
1469 };
1470 let Some((key, value)) = map_state.entries.get(entry_idx) else {
1471 return;
1472 };
1473
1474 let Some(schema) = map_state.value_schema.as_ref() else {
1476 return; };
1478
1479 let no_delete = map_state.no_add;
1481
1482 let dialog = EntryDialogState::from_schema(
1484 key.clone(),
1485 value,
1486 schema,
1487 path,
1488 false,
1489 no_delete,
1490 &self.available_status_bar_tokens,
1491 );
1492 self.entry_dialog_stack.push(dialog);
1493 }
1494
1495 pub fn open_add_entry_dialog(&mut self) {
1497 let Some(item) = self.current_item() else {
1498 return;
1499 };
1500 let SettingControl::Map(map_state) = &item.control else {
1501 return;
1502 };
1503 let Some(schema) = map_state.value_schema.as_ref() else {
1504 return;
1505 };
1506 let path = item.path.clone();
1507
1508 let dialog = EntryDialogState::from_schema(
1511 String::new(),
1512 &serde_json::json!({}),
1513 schema,
1514 &path,
1515 true,
1516 false,
1517 &self.available_status_bar_tokens,
1518 );
1519 self.entry_dialog_stack.push(dialog);
1520 }
1521
1522 pub fn open_add_array_item_dialog(&mut self) {
1524 let Some(item) = self.current_item() else {
1525 return;
1526 };
1527 let SettingControl::ObjectArray(array_state) = &item.control else {
1528 return;
1529 };
1530 let Some(schema) = array_state.item_schema.as_ref() else {
1531 return;
1532 };
1533 let path = item.path.clone();
1534
1535 let dialog = EntryDialogState::for_array_item(
1537 None,
1538 &serde_json::json!({}),
1539 schema,
1540 &path,
1541 true,
1542 &self.available_status_bar_tokens,
1543 );
1544 self.entry_dialog_stack.push(dialog);
1545 }
1546
1547 pub fn open_edit_array_item_dialog(&mut self) {
1549 let Some(item) = self.current_item() else {
1550 return;
1551 };
1552 let SettingControl::ObjectArray(array_state) = &item.control else {
1553 return;
1554 };
1555 let Some(schema) = array_state.item_schema.as_ref() else {
1556 return;
1557 };
1558 let Some(index) = array_state.focused_index else {
1559 return;
1560 };
1561 let Some(value) = array_state.bindings.get(index) else {
1562 return;
1563 };
1564 let path = item.path.clone();
1565
1566 let dialog = EntryDialogState::for_array_item(
1567 Some(index),
1568 value,
1569 schema,
1570 &path,
1571 false,
1572 &self.available_status_bar_tokens,
1573 );
1574 self.entry_dialog_stack.push(dialog);
1575 }
1576
1577 pub fn close_entry_dialog(&mut self) {
1579 self.entry_dialog_stack.pop();
1580 }
1581
1582 pub fn open_nested_entry_dialog(&mut self) {
1587 let nested_info = self.entry_dialog().and_then(|dialog| {
1589 let item = dialog.current_item()?;
1590 let base = dialog.entry_path();
1596 let relative = item.path.trim_start_matches('/');
1597 let path = if relative.is_empty() {
1598 base
1602 } else {
1603 format!("{}/{}", base, relative)
1604 };
1605
1606 match &item.control {
1607 SettingControl::Map(map_state) => {
1608 let schema = map_state.value_schema.as_ref()?;
1609 let no_delete = map_state.no_add; if let Some(entry_idx) = map_state.focused_entry {
1611 let (key, value) = map_state.entries.get(entry_idx)?;
1613 Some(NestedDialogInfo::MapEntry {
1614 key: key.clone(),
1615 value: value.clone(),
1616 schema: schema.as_ref().clone(),
1617 path,
1618 is_new: false,
1619 no_delete,
1620 })
1621 } else {
1622 Some(NestedDialogInfo::MapEntry {
1624 key: String::new(),
1625 value: serde_json::json!({}),
1626 schema: schema.as_ref().clone(),
1627 path,
1628 is_new: true,
1629 no_delete: false, })
1631 }
1632 }
1633 SettingControl::ObjectArray(array_state) => {
1634 let schema = array_state.item_schema.as_ref()?;
1635 if let Some(index) = array_state.focused_index {
1636 let value = array_state.bindings.get(index)?;
1638 Some(NestedDialogInfo::ArrayItem {
1639 index: Some(index),
1640 value: value.clone(),
1641 schema: schema.as_ref().clone(),
1642 path,
1643 is_new: false,
1644 })
1645 } else {
1646 Some(NestedDialogInfo::ArrayItem {
1648 index: None,
1649 value: serde_json::json!({}),
1650 schema: schema.as_ref().clone(),
1651 path,
1652 is_new: true,
1653 })
1654 }
1655 }
1656 _ => None,
1657 }
1658 });
1659
1660 if let Some(info) = nested_info {
1662 let dialog = match info {
1663 NestedDialogInfo::MapEntry {
1664 key,
1665 value,
1666 schema,
1667 path,
1668 is_new,
1669 no_delete,
1670 } => EntryDialogState::from_schema(
1671 key,
1672 &value,
1673 &schema,
1674 &path,
1675 is_new,
1676 no_delete,
1677 &self.available_status_bar_tokens,
1678 ),
1679 NestedDialogInfo::ArrayItem {
1680 index,
1681 value,
1682 schema,
1683 path,
1684 is_new,
1685 } => EntryDialogState::for_array_item(
1686 index,
1687 &value,
1688 &schema,
1689 &path,
1690 is_new,
1691 &self.available_status_bar_tokens,
1692 ),
1693 };
1694 self.entry_dialog_stack.push(dialog);
1695 }
1696 }
1697
1698 pub fn save_entry_dialog(&mut self) {
1703 let is_array = if self.entry_dialog_stack.len() > 1 {
1707 self.entry_dialog_stack
1709 .get(self.entry_dialog_stack.len() - 2)
1710 .and_then(|parent| parent.current_item())
1711 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1712 .unwrap_or(false)
1713 } else {
1714 self.current_item()
1716 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1717 .unwrap_or(false)
1718 };
1719
1720 if is_array {
1721 self.save_array_item_dialog_inner();
1722 } else {
1723 self.save_map_entry_dialog_inner();
1724 }
1725 }
1726
1727 fn save_map_entry_dialog_inner(&mut self) {
1729 let Some(dialog) = self.entry_dialog_stack.pop() else {
1730 return;
1731 };
1732
1733 let key = dialog.get_key();
1735 if key.is_empty() {
1736 return; }
1738
1739 let value = dialog.to_value();
1740 let map_path = dialog.map_path.clone();
1741 let original_key = dialog.entry_key.clone();
1742 let is_new = dialog.is_new;
1743 let key_changed = !is_new && key != original_key;
1744
1745 if let Some(item) = self.current_item_mut() {
1747 if let SettingControl::Map(map_state) = &mut item.control {
1748 if key_changed {
1750 if let Some(idx) = map_state
1751 .entries
1752 .iter()
1753 .position(|(k, _)| k == &original_key)
1754 {
1755 map_state.entries.remove(idx);
1756 }
1757 }
1758
1759 if let Some(entry) = map_state.entries.iter_mut().find(|(k, _)| k == &key) {
1761 entry.1 = value.clone();
1762 } else {
1763 map_state.entries.push((key.clone(), value.clone()));
1764 map_state.entries.sort_by(|a, b| a.0.cmp(&b.0));
1765 }
1766 }
1767 }
1768
1769 if key_changed {
1771 let old_path = format!("{}/{}", map_path, original_key);
1772 self.pending_changes
1773 .insert(old_path, serde_json::Value::Null);
1774 }
1775
1776 let path = format!("{}/{}", map_path, key);
1778 self.set_pending_change(&path, value);
1779 }
1780
1781 fn save_array_item_dialog_inner(&mut self) {
1783 let Some(dialog) = self.entry_dialog_stack.pop() else {
1784 return;
1785 };
1786
1787 let value = dialog.to_value();
1788 let array_path = dialog.map_path.clone();
1789 let is_new = dialog.is_new;
1790 let entry_key = dialog.entry_key.clone();
1791
1792 let is_nested = !self.entry_dialog_stack.is_empty();
1794
1795 if is_nested {
1796 let parent_entry_path = self
1804 .entry_dialog_stack
1805 .last()
1806 .map(|p| p.entry_path())
1807 .unwrap_or_default();
1808 let item_path = array_path
1809 .strip_prefix(parent_entry_path.as_str())
1810 .unwrap_or(&array_path)
1811 .trim_end_matches('/')
1812 .to_string();
1813
1814 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1816 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1817 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1818 if is_new {
1819 array_state.bindings.push(value.clone());
1820 } else if let Ok(index) = entry_key.parse::<usize>() {
1821 if index < array_state.bindings.len() {
1822 array_state.bindings[index] = value.clone();
1823 }
1824 }
1825 }
1826 }
1827 }
1828
1829 if let Some(parent) = self.entry_dialog_stack.last() {
1832 if let Some(item) = parent.items.iter().find(|i| i.path == item_path) {
1833 if let SettingControl::ObjectArray(array_state) = &item.control {
1834 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1835 self.set_pending_change(&array_path, array_value);
1836 }
1837 }
1838 }
1839 } else {
1840 if let Some(item) = self.current_item_mut() {
1842 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1843 if is_new {
1844 array_state.bindings.push(value.clone());
1845 } else if let Ok(index) = entry_key.parse::<usize>() {
1846 if index < array_state.bindings.len() {
1847 array_state.bindings[index] = value.clone();
1848 }
1849 }
1850 }
1851 }
1852
1853 if let Some(item) = self.current_item() {
1855 if let SettingControl::ObjectArray(array_state) = &item.control {
1856 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1857 self.set_pending_change(&array_path, array_value);
1858 }
1859 }
1860 }
1861 }
1862
1863 pub fn delete_entry_dialog(&mut self) {
1865 let is_nested = self.entry_dialog_stack.len() > 1;
1867
1868 let Some(dialog) = self.entry_dialog_stack.pop() else {
1869 return;
1870 };
1871
1872 let path = format!("{}/{}", dialog.map_path, dialog.entry_key);
1873
1874 if is_nested {
1876 let map_field = dialog.map_path.rsplit('/').next().unwrap_or("").to_string();
1879 let item_path = format!("/{}", map_field);
1880
1881 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1883 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1884 if let SettingControl::Map(map_state) = &mut item.control {
1885 if let Some(idx) = map_state
1886 .entries
1887 .iter()
1888 .position(|(k, _)| k == &dialog.entry_key)
1889 {
1890 map_state.remove_entry(idx);
1891 }
1892 }
1893 }
1894 }
1895 } else {
1896 if let Some(item) = self.current_item_mut() {
1898 if let SettingControl::Map(map_state) = &mut item.control {
1899 if let Some(idx) = map_state
1900 .entries
1901 .iter()
1902 .position(|(k, _)| k == &dialog.entry_key)
1903 {
1904 map_state.remove_entry(idx);
1905 }
1906 }
1907 }
1908 }
1909
1910 self.set_pending_change(&path, serde_json::Value::Null);
1912 }
1913
1914 pub fn max_scroll(&self) -> u16 {
1916 self.scroll_panel.scroll.max_offset()
1917 }
1918
1919 pub fn scroll_up(&mut self, delta: usize) -> bool {
1922 let old = self.scroll_panel.scroll.offset;
1923 self.scroll_panel.scroll_up(delta as u16);
1924 let changed = old != self.scroll_panel.scroll.offset;
1925 if changed {
1926 self.sync_tree_cursor_to_body_scroll();
1927 }
1928 changed
1929 }
1930
1931 pub fn scroll_down(&mut self, delta: usize) -> bool {
1934 let old = self.scroll_panel.scroll.offset;
1935 self.scroll_panel.scroll_down(delta as u16);
1936 let changed = old != self.scroll_panel.scroll.offset;
1937 if changed {
1938 self.sync_tree_cursor_to_body_scroll();
1939 }
1940 changed
1941 }
1942
1943 pub fn scroll_to_ratio(&mut self, ratio: f32) -> bool {
1946 let old = self.scroll_panel.scroll.offset;
1947 self.scroll_panel.scroll_to_ratio(ratio);
1948 let changed = old != self.scroll_panel.scroll.offset;
1949 if changed {
1950 self.sync_tree_cursor_to_body_scroll();
1951 }
1952 changed
1953 }
1954
1955 pub(super) fn sync_tree_cursor_to_body_scroll(&mut self) {
1961 if let Some(section_idx) = self.current_section_index() {
1962 self.tree_cursor_section = Some(section_idx);
1963 }
1964 }
1969
1970 pub fn is_number_control(&self) -> bool {
1973 self.current_item()
1974 .is_some_and(|item| matches!(item.control, SettingControl::Number(_)))
1975 }
1976
1977 pub fn start_editing(&mut self) {
1978 if let Some(item) = self.current_item() {
1979 if matches!(
1980 item.control,
1981 SettingControl::TextList(_)
1982 | SettingControl::DualList(_)
1983 | SettingControl::Text(_)
1984 | SettingControl::Map(_)
1985 | SettingControl::Json(_)
1986 ) {
1987 self.editing_text = true;
1988 }
1989 }
1990 if let Some(item) = self.current_item_mut() {
1991 match item.control {
1992 SettingControl::DualList(ref mut dl) => {
1993 dl.editing = true;
1994 }
1995 SettingControl::Text(ref mut state) => {
1996 state.editing = true;
1997 state.arm_replace_on_type();
2002 }
2003 _ => {}
2004 }
2005 }
2006 }
2007
2008 pub fn stop_editing(&mut self) {
2010 self.editing_text = false;
2011 if let Some(item) = self.current_item_mut() {
2012 match item.control {
2013 SettingControl::DualList(ref mut dl) => {
2014 dl.editing = false;
2015 }
2016 SettingControl::Text(ref mut state) => {
2017 state.editing = false;
2018 }
2019 _ => {}
2020 }
2021 }
2022 }
2023
2024 pub fn is_editable_control(&self) -> bool {
2026 self.current_item().is_some_and(|item| {
2027 matches!(
2028 item.control,
2029 SettingControl::TextList(_)
2030 | SettingControl::DualList(_)
2031 | SettingControl::Text(_)
2032 | SettingControl::Map(_)
2033 | SettingControl::Json(_)
2034 )
2035 })
2036 }
2037
2038 pub fn is_editing_json(&self) -> bool {
2040 if !self.editing_text {
2041 return false;
2042 }
2043 self.current_item()
2044 .map(|item| matches!(&item.control, SettingControl::Json(_)))
2045 .unwrap_or(false)
2046 }
2047
2048 pub fn text_insert(&mut self, c: char) {
2050 if let Some(item) = self.current_item_mut() {
2051 match &mut item.control {
2052 SettingControl::TextList(state) => state.insert(c),
2053 SettingControl::Text(state) => state.insert(c),
2054 SettingControl::Map(state) => {
2055 state.new_key_text.insert(state.cursor, c);
2056 state.cursor += c.len_utf8();
2057 }
2058 SettingControl::Json(state) => state.insert(c),
2059 _ => {}
2060 }
2061 }
2062 }
2063
2064 pub fn text_backspace(&mut self) {
2066 if let Some(item) = self.current_item_mut() {
2067 match &mut item.control {
2068 SettingControl::TextList(state) => state.backspace(),
2069 SettingControl::Text(state) => state.backspace(),
2070 SettingControl::Map(state) => {
2071 if state.cursor > 0 {
2072 let mut char_start = state.cursor - 1;
2073 while char_start > 0 && !state.new_key_text.is_char_boundary(char_start) {
2074 char_start -= 1;
2075 }
2076 state.new_key_text.remove(char_start);
2077 state.cursor = char_start;
2078 }
2079 }
2080 SettingControl::Json(state) => state.backspace(),
2081 _ => {}
2082 }
2083 }
2084 }
2085
2086 pub fn text_move_left(&mut self) {
2088 if let Some(item) = self.current_item_mut() {
2089 match &mut item.control {
2090 SettingControl::TextList(state) => state.move_left(),
2091 SettingControl::Text(state) => state.move_left(),
2092 SettingControl::Map(state) => {
2093 if state.cursor > 0 {
2094 let mut new_pos = state.cursor - 1;
2095 while new_pos > 0 && !state.new_key_text.is_char_boundary(new_pos) {
2096 new_pos -= 1;
2097 }
2098 state.cursor = new_pos;
2099 }
2100 }
2101 SettingControl::Json(state) => state.move_left(),
2102 _ => {}
2103 }
2104 }
2105 }
2106
2107 pub fn text_move_right(&mut self) {
2109 if let Some(item) = self.current_item_mut() {
2110 match &mut item.control {
2111 SettingControl::TextList(state) => state.move_right(),
2112 SettingControl::Text(state) => state.move_right(),
2113 SettingControl::Map(state) => {
2114 if state.cursor < state.new_key_text.len() {
2115 let mut new_pos = state.cursor + 1;
2116 while new_pos < state.new_key_text.len()
2117 && !state.new_key_text.is_char_boundary(new_pos)
2118 {
2119 new_pos += 1;
2120 }
2121 state.cursor = new_pos;
2122 }
2123 }
2124 SettingControl::Json(state) => state.move_right(),
2125 _ => {}
2126 }
2127 }
2128 }
2129
2130 pub fn text_focus_prev(&mut self) {
2132 if let Some(item) = self.current_item_mut() {
2133 match &mut item.control {
2134 SettingControl::TextList(state) => state.focus_prev(),
2135 SettingControl::Map(state) => {
2136 state.focus_prev();
2137 }
2138 _ => {}
2139 }
2140 }
2141 }
2142
2143 pub fn text_focus_next(&mut self) {
2145 if let Some(item) = self.current_item_mut() {
2146 match &mut item.control {
2147 SettingControl::TextList(state) => state.focus_next(),
2148 SettingControl::Map(state) => {
2149 state.focus_next();
2150 }
2151 _ => {}
2152 }
2153 }
2154 }
2155
2156 pub fn text_add_item(&mut self) {
2158 if let Some(item) = self.current_item_mut() {
2159 match &mut item.control {
2160 SettingControl::TextList(state) => state.add_item(),
2161 SettingControl::Map(state) => state.add_entry_from_input(),
2162 _ => {}
2163 }
2164 }
2165 self.on_value_changed();
2167 }
2168
2169 pub fn text_remove_focused(&mut self) {
2171 if let Some(item) = self.current_item_mut() {
2172 match &mut item.control {
2173 SettingControl::TextList(state) => {
2174 if let Some(idx) = state.focused_item {
2175 state.remove_item(idx);
2176 }
2177 }
2178 SettingControl::Map(state) => {
2179 if let Some(idx) = state.focused_entry {
2180 state.remove_entry(idx);
2181 }
2182 }
2183 _ => {}
2184 }
2185 }
2186 self.on_value_changed();
2188 }
2189
2190 pub fn is_editing_dual_list(&self) -> bool {
2192 if !self.editing_text {
2193 return false;
2194 }
2195 self.current_item()
2196 .map(|item| matches!(&item.control, SettingControl::DualList(_)))
2197 .unwrap_or(false)
2198 }
2199
2200 pub fn with_dual_list_mut<R>(
2205 &mut self,
2206 item_idx: usize,
2207 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2208 ) -> Option<R> {
2209 let page = self.pages.get_mut(self.selected_category)?;
2210 let item = page.items.get_mut(item_idx)?;
2211 if let SettingControl::DualList(ref mut state) = item.control {
2212 Some(f(state))
2213 } else {
2214 None
2215 }
2216 }
2217
2218 pub fn with_current_dual_list_mut<R>(
2221 &mut self,
2222 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2223 ) -> Option<R> {
2224 if let Some(item) = self.current_item_mut() {
2225 if let SettingControl::DualList(ref mut state) = item.control {
2226 return Some(f(state));
2227 }
2228 }
2229 None
2230 }
2231
2232 pub fn refresh_dual_list_sibling(&mut self) {
2239 let (new_included, sibling_path) = {
2240 let Some(item) = self.current_item() else {
2241 return;
2242 };
2243 let SettingControl::DualList(state) = &item.control else {
2244 return;
2245 };
2246 let Some(ref sib_path) = item.dual_list_sibling else {
2247 return;
2248 };
2249 (state.included.clone(), sib_path.clone())
2250 };
2251
2252 if let Some(page) = self.pages.get_mut(self.selected_category) {
2254 for other in page.items.iter_mut() {
2255 if other.path == sibling_path {
2256 if let SettingControl::DualList(ref mut sib_state) = other.control {
2257 sib_state.excluded = new_included;
2258 }
2259 break;
2260 }
2261 }
2262 }
2263 }
2264
2265 pub fn json_cursor_up(&mut self) {
2269 if let Some(item) = self.current_item_mut() {
2270 if let SettingControl::Json(state) = &mut item.control {
2271 state.move_up();
2272 }
2273 }
2274 }
2275
2276 pub fn json_cursor_down(&mut self) {
2278 if let Some(item) = self.current_item_mut() {
2279 if let SettingControl::Json(state) = &mut item.control {
2280 state.move_down();
2281 }
2282 }
2283 }
2284
2285 pub fn json_insert_newline(&mut self) {
2287 if let Some(item) = self.current_item_mut() {
2288 if let SettingControl::Json(state) = &mut item.control {
2289 state.insert('\n');
2290 }
2291 }
2292 }
2293
2294 pub fn json_delete(&mut self) {
2296 if let Some(item) = self.current_item_mut() {
2297 if let SettingControl::Json(state) = &mut item.control {
2298 state.delete();
2299 }
2300 }
2301 }
2302
2303 pub fn json_exit_editing(&mut self) {
2305 let is_valid = self
2306 .current_item()
2307 .map(|item| {
2308 if let SettingControl::Json(state) = &item.control {
2309 state.is_valid()
2310 } else {
2311 true
2312 }
2313 })
2314 .unwrap_or(true);
2315
2316 if is_valid {
2317 if let Some(item) = self.current_item_mut() {
2318 if let SettingControl::Json(state) = &mut item.control {
2319 state.commit();
2320 }
2321 }
2322 self.on_value_changed();
2323 } else if let Some(item) = self.current_item_mut() {
2324 if let SettingControl::Json(state) = &mut item.control {
2325 state.revert();
2326 }
2327 }
2328 self.editing_text = false;
2329 }
2330
2331 pub fn json_select_all(&mut self) {
2333 if let Some(item) = self.current_item_mut() {
2334 if let SettingControl::Json(state) = &mut item.control {
2335 state.select_all();
2336 }
2337 }
2338 }
2339
2340 pub fn json_selected_text(&self) -> Option<String> {
2342 if let Some(item) = self.current_item() {
2343 if let SettingControl::Json(state) = &item.control {
2344 return state.selected_text();
2345 }
2346 }
2347 None
2348 }
2349
2350 pub fn json_cursor_up_selecting(&mut self) {
2352 if let Some(item) = self.current_item_mut() {
2353 if let SettingControl::Json(state) = &mut item.control {
2354 state.editor.move_up_selecting();
2355 }
2356 }
2357 }
2358
2359 pub fn json_cursor_down_selecting(&mut self) {
2361 if let Some(item) = self.current_item_mut() {
2362 if let SettingControl::Json(state) = &mut item.control {
2363 state.editor.move_down_selecting();
2364 }
2365 }
2366 }
2367
2368 pub fn json_cursor_left_selecting(&mut self) {
2370 if let Some(item) = self.current_item_mut() {
2371 if let SettingControl::Json(state) = &mut item.control {
2372 state.editor.move_left_selecting();
2373 }
2374 }
2375 }
2376
2377 pub fn json_cursor_right_selecting(&mut self) {
2379 if let Some(item) = self.current_item_mut() {
2380 if let SettingControl::Json(state) = &mut item.control {
2381 state.editor.move_right_selecting();
2382 }
2383 }
2384 }
2385
2386 pub fn is_dropdown_open(&self) -> bool {
2390 self.current_item().is_some_and(|item| {
2391 if let SettingControl::Dropdown(ref d) = item.control {
2392 d.open
2393 } else {
2394 false
2395 }
2396 })
2397 }
2398
2399 pub fn dropdown_toggle(&mut self) {
2401 let mut opened = false;
2402 if let Some(item) = self.current_item_mut() {
2403 if let SettingControl::Dropdown(ref mut d) = item.control {
2404 d.toggle_open();
2405 opened = d.open;
2406 }
2407 }
2408
2409 if opened {
2411 let selected_item = self.selected_item;
2413 let width = self.layout_width;
2414 if let Some(page) = self.pages.get(self.selected_category) {
2415 self.scroll_panel.update_content_height(&page.items, width);
2416 self.scroll_panel
2418 .ensure_focused_visible(&page.items, selected_item, None, width);
2419 }
2420 }
2421 }
2422
2423 pub fn dropdown_prev(&mut self) {
2425 if let Some(item) = self.current_item_mut() {
2426 if let SettingControl::Dropdown(ref mut d) = item.control {
2427 d.select_prev();
2428 }
2429 }
2430 }
2431
2432 pub fn dropdown_next(&mut self) {
2434 if let Some(item) = self.current_item_mut() {
2435 if let SettingControl::Dropdown(ref mut d) = item.control {
2436 d.select_next();
2437 }
2438 }
2439 }
2440
2441 pub fn dropdown_home(&mut self) {
2443 if let Some(item) = self.current_item_mut() {
2444 if let SettingControl::Dropdown(ref mut d) = item.control {
2445 if !d.options.is_empty() {
2446 d.selected = 0;
2447 d.ensure_visible();
2448 }
2449 }
2450 }
2451 }
2452
2453 pub fn dropdown_end(&mut self) {
2455 if let Some(item) = self.current_item_mut() {
2456 if let SettingControl::Dropdown(ref mut d) = item.control {
2457 if !d.options.is_empty() {
2458 d.selected = d.options.len() - 1;
2459 d.ensure_visible();
2460 }
2461 }
2462 }
2463 }
2464
2465 pub fn dropdown_confirm(&mut self) {
2467 if let Some(item) = self.current_item_mut() {
2468 if let SettingControl::Dropdown(ref mut d) = item.control {
2469 d.confirm();
2470 }
2471 }
2472 self.on_value_changed();
2473 }
2474
2475 pub fn dropdown_cancel(&mut self) {
2477 if let Some(item) = self.current_item_mut() {
2478 if let SettingControl::Dropdown(ref mut d) = item.control {
2479 d.cancel();
2480 }
2481 }
2482 }
2483
2484 pub fn dropdown_select(&mut self, option_idx: usize) {
2486 if let Some(item) = self.current_item_mut() {
2487 if let SettingControl::Dropdown(ref mut d) = item.control {
2488 if option_idx < d.options.len() {
2489 d.selected = option_idx;
2490 d.confirm();
2491 }
2492 }
2493 }
2494 self.on_value_changed();
2495 }
2496
2497 pub fn set_dropdown_hover(&mut self, hover_idx: Option<usize>) -> bool {
2500 if let Some(item) = self.current_item_mut() {
2501 if let SettingControl::Dropdown(ref mut d) = item.control {
2502 if d.open && d.hover_index != hover_idx {
2503 d.hover_index = hover_idx;
2504 return true;
2505 }
2506 }
2507 }
2508 false
2509 }
2510
2511 pub fn dropdown_scroll(&mut self, delta: i32) {
2513 if let Some(item) = self.current_item_mut() {
2514 if let SettingControl::Dropdown(ref mut d) = item.control {
2515 if d.open {
2516 d.scroll_by(delta);
2517 }
2518 }
2519 }
2520 }
2521
2522 pub fn is_number_editing(&self) -> bool {
2526 self.current_item().is_some_and(|item| {
2527 if let SettingControl::Number(ref n) = item.control {
2528 n.editing()
2529 } else {
2530 false
2531 }
2532 })
2533 }
2534
2535 pub fn start_number_editing(&mut self) {
2537 if let Some(item) = self.current_item_mut() {
2538 if let SettingControl::Number(ref mut n) = item.control {
2539 n.start_editing();
2540 }
2541 }
2542 }
2543
2544 pub fn number_insert(&mut self, c: char) {
2546 if let Some(item) = self.current_item_mut() {
2547 if let SettingControl::Number(ref mut n) = item.control {
2548 n.insert_char(c);
2549 }
2550 }
2551 }
2552
2553 pub fn number_backspace(&mut self) {
2555 if let Some(item) = self.current_item_mut() {
2556 if let SettingControl::Number(ref mut n) = item.control {
2557 n.backspace();
2558 }
2559 }
2560 }
2561
2562 pub fn number_confirm(&mut self) {
2564 if let Some(item) = self.current_item_mut() {
2565 if let SettingControl::Number(ref mut n) = item.control {
2566 n.confirm_editing();
2567 }
2568 }
2569 self.on_value_changed();
2570 }
2571
2572 pub fn number_cancel(&mut self) {
2574 if let Some(item) = self.current_item_mut() {
2575 if let SettingControl::Number(ref mut n) = item.control {
2576 n.cancel_editing();
2577 }
2578 }
2579 }
2580
2581 pub fn number_delete(&mut self) {
2583 if let Some(item) = self.current_item_mut() {
2584 if let SettingControl::Number(ref mut n) = item.control {
2585 n.delete();
2586 }
2587 }
2588 }
2589
2590 pub fn number_move_left(&mut self) {
2592 if let Some(item) = self.current_item_mut() {
2593 if let SettingControl::Number(ref mut n) = item.control {
2594 n.move_left();
2595 }
2596 }
2597 }
2598
2599 pub fn number_move_right(&mut self) {
2601 if let Some(item) = self.current_item_mut() {
2602 if let SettingControl::Number(ref mut n) = item.control {
2603 n.move_right();
2604 }
2605 }
2606 }
2607
2608 pub fn number_move_home(&mut self) {
2610 if let Some(item) = self.current_item_mut() {
2611 if let SettingControl::Number(ref mut n) = item.control {
2612 n.move_home();
2613 }
2614 }
2615 }
2616
2617 pub fn number_move_end(&mut self) {
2619 if let Some(item) = self.current_item_mut() {
2620 if let SettingControl::Number(ref mut n) = item.control {
2621 n.move_end();
2622 }
2623 }
2624 }
2625
2626 pub fn number_move_left_selecting(&mut self) {
2628 if let Some(item) = self.current_item_mut() {
2629 if let SettingControl::Number(ref mut n) = item.control {
2630 n.move_left_selecting();
2631 }
2632 }
2633 }
2634
2635 pub fn number_move_right_selecting(&mut self) {
2637 if let Some(item) = self.current_item_mut() {
2638 if let SettingControl::Number(ref mut n) = item.control {
2639 n.move_right_selecting();
2640 }
2641 }
2642 }
2643
2644 pub fn number_move_home_selecting(&mut self) {
2646 if let Some(item) = self.current_item_mut() {
2647 if let SettingControl::Number(ref mut n) = item.control {
2648 n.move_home_selecting();
2649 }
2650 }
2651 }
2652
2653 pub fn number_move_end_selecting(&mut self) {
2655 if let Some(item) = self.current_item_mut() {
2656 if let SettingControl::Number(ref mut n) = item.control {
2657 n.move_end_selecting();
2658 }
2659 }
2660 }
2661
2662 pub fn number_move_word_left(&mut self) {
2664 if let Some(item) = self.current_item_mut() {
2665 if let SettingControl::Number(ref mut n) = item.control {
2666 n.move_word_left();
2667 }
2668 }
2669 }
2670
2671 pub fn number_move_word_right(&mut self) {
2673 if let Some(item) = self.current_item_mut() {
2674 if let SettingControl::Number(ref mut n) = item.control {
2675 n.move_word_right();
2676 }
2677 }
2678 }
2679
2680 pub fn number_move_word_left_selecting(&mut self) {
2682 if let Some(item) = self.current_item_mut() {
2683 if let SettingControl::Number(ref mut n) = item.control {
2684 n.move_word_left_selecting();
2685 }
2686 }
2687 }
2688
2689 pub fn number_move_word_right_selecting(&mut self) {
2691 if let Some(item) = self.current_item_mut() {
2692 if let SettingControl::Number(ref mut n) = item.control {
2693 n.move_word_right_selecting();
2694 }
2695 }
2696 }
2697
2698 pub fn number_select_all(&mut self) {
2700 if let Some(item) = self.current_item_mut() {
2701 if let SettingControl::Number(ref mut n) = item.control {
2702 n.select_all();
2703 }
2704 }
2705 }
2706
2707 pub fn number_delete_word_backward(&mut self) {
2709 if let Some(item) = self.current_item_mut() {
2710 if let SettingControl::Number(ref mut n) = item.control {
2711 n.delete_word_backward();
2712 }
2713 }
2714 }
2715
2716 pub fn number_delete_word_forward(&mut self) {
2718 if let Some(item) = self.current_item_mut() {
2719 if let SettingControl::Number(ref mut n) = item.control {
2720 n.delete_word_forward();
2721 }
2722 }
2723 }
2724
2725 pub fn get_change_descriptions(&self) -> Vec<String> {
2727 let mut descriptions: Vec<String> = self
2728 .pending_changes
2729 .iter()
2730 .map(|(path, value)| {
2731 let value_str = match value {
2732 serde_json::Value::Bool(b) => b.to_string(),
2733 serde_json::Value::Number(n) => n.to_string(),
2734 serde_json::Value::String(s) => format!("\"{}\"", s),
2735 _ => value.to_string(),
2736 };
2737 format!("{}: {}", path, value_str)
2738 })
2739 .collect();
2740 for path in &self.pending_deletions {
2742 descriptions.push(format!("{}: (reset to default)", path));
2743 }
2744 descriptions.sort();
2745 descriptions
2746 }
2747}
2748
2749fn update_control_from_value(control: &mut SettingControl, value: &serde_json::Value) {
2751 match control {
2752 SettingControl::Toggle(state) => {
2753 if let Some(b) = value.as_bool() {
2754 state.checked = b;
2755 }
2756 }
2757 SettingControl::Number(state) => {
2758 if let Some(n) = value.as_i64() {
2759 state.value = n;
2760 }
2761 }
2762 SettingControl::Dropdown(state) => {
2763 if let Some(s) = value.as_str() {
2764 if let Some(idx) = state.options.iter().position(|o| o == s) {
2765 state.selected = idx;
2766 }
2767 }
2768 }
2769 SettingControl::Text(state) => {
2770 if let Some(s) = value.as_str() {
2771 state.value = s.to_string();
2772 state.cursor = state.value.len();
2773 }
2774 }
2775 SettingControl::TextList(state) => {
2776 if let Some(arr) = value.as_array() {
2777 state.items = arr
2778 .iter()
2779 .filter_map(|v| {
2780 if state.is_integer {
2781 v.as_i64()
2782 .map(|n| n.to_string())
2783 .or_else(|| v.as_u64().map(|n| n.to_string()))
2784 .or_else(|| v.as_f64().map(|n| n.to_string()))
2785 } else {
2786 v.as_str().map(String::from)
2787 }
2788 })
2789 .collect();
2790 }
2791 }
2792 SettingControl::DualList(state) => {
2793 if let Some(arr) = value.as_array() {
2794 state.included = arr
2795 .iter()
2796 .filter_map(|v| v.as_str().map(String::from))
2797 .collect();
2798 }
2799 }
2800 SettingControl::Map(state) => {
2801 if let Some(obj) = value.as_object() {
2802 state.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
2803 state.entries.sort_by(|a, b| a.0.cmp(&b.0));
2804 }
2805 }
2806 SettingControl::ObjectArray(state) => {
2807 if let Some(arr) = value.as_array() {
2808 state.bindings = arr.clone();
2809 }
2810 }
2811 SettingControl::Json(state) => {
2812 let json_str =
2814 serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_string());
2815 let json_str = if json_str.is_empty() {
2816 "null".to_string()
2817 } else {
2818 json_str
2819 };
2820 state.original_text = json_str.clone();
2821 state.editor.set_value(&json_str);
2822 state.scroll_offset = 0;
2823 }
2824 SettingControl::Complex { .. } => {}
2825 }
2826}
2827
2828#[cfg(test)]
2829mod tests {
2830 use super::*;
2831
2832 const TEST_SCHEMA: &str = r#"
2833{
2834 "type": "object",
2835 "properties": {
2836 "theme": {
2837 "type": "string",
2838 "default": "dark"
2839 },
2840 "line_numbers": {
2841 "type": "boolean",
2842 "default": true
2843 }
2844 },
2845 "$defs": {}
2846}
2847"#;
2848
2849 fn test_config() -> Config {
2850 Config::default()
2851 }
2852
2853 #[test]
2854 fn test_settings_state_creation() {
2855 let config = test_config();
2856 let state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2857
2858 assert!(!state.visible);
2859 assert_eq!(state.selected_category, 0);
2860 assert!(!state.has_changes());
2861 }
2862
2863 #[test]
2864 fn test_navigation() {
2865 let config = test_config();
2866 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2867
2868 assert_eq!(state.focus_panel(), FocusPanel::Categories);
2870
2871 state.toggle_focus();
2873 assert_eq!(state.focus_panel(), FocusPanel::Settings);
2874
2875 state.select_next();
2877 assert_eq!(state.selected_item, 1);
2878
2879 state.select_prev();
2880 assert_eq!(state.selected_item, 0);
2881 }
2882
2883 #[test]
2884 fn test_pending_changes() {
2885 let config = test_config();
2886 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2887
2888 assert!(!state.has_changes());
2889
2890 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
2891 assert!(state.has_changes());
2892
2893 state.discard_changes();
2894 assert!(!state.has_changes());
2895 }
2896
2897 #[test]
2898 fn test_show_hide() {
2899 let config = test_config();
2900 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2901
2902 assert!(!state.visible);
2903
2904 state.show();
2905 assert!(state.visible);
2906 assert_eq!(state.focus_panel(), FocusPanel::Categories);
2907
2908 state.hide();
2909 assert!(!state.visible);
2910 }
2911
2912 const TEST_SCHEMA_CONTROLS: &str = r#"
2914{
2915 "type": "object",
2916 "properties": {
2917 "theme": {
2918 "type": "string",
2919 "enum": ["dark", "light", "high-contrast"],
2920 "default": "dark"
2921 },
2922 "tab_size": {
2923 "type": "integer",
2924 "minimum": 1,
2925 "maximum": 8,
2926 "default": 4
2927 },
2928 "line_numbers": {
2929 "type": "boolean",
2930 "default": true
2931 }
2932 },
2933 "$defs": {}
2934}
2935"#;
2936
2937 #[test]
2938 fn test_dropdown_toggle() {
2939 let config = test_config();
2940 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2941 state.show();
2942 state.toggle_focus(); state.select_next();
2947 state.select_next();
2948 assert!(!state.is_dropdown_open());
2949
2950 state.dropdown_toggle();
2951 assert!(state.is_dropdown_open());
2952
2953 state.dropdown_toggle();
2954 assert!(!state.is_dropdown_open());
2955 }
2956
2957 #[test]
2958 fn test_dropdown_cancel_restores() {
2959 let config = test_config();
2960 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2961 state.show();
2962 state.toggle_focus();
2963
2964 state.select_next();
2967 state.select_next();
2968
2969 state.dropdown_toggle();
2971 assert!(state.is_dropdown_open());
2972
2973 let initial = state.current_item().and_then(|item| {
2975 if let SettingControl::Dropdown(ref d) = item.control {
2976 Some(d.selected)
2977 } else {
2978 None
2979 }
2980 });
2981
2982 state.dropdown_next();
2984 let after_change = state.current_item().and_then(|item| {
2985 if let SettingControl::Dropdown(ref d) = item.control {
2986 Some(d.selected)
2987 } else {
2988 None
2989 }
2990 });
2991 assert_ne!(initial, after_change);
2992
2993 state.dropdown_cancel();
2995 assert!(!state.is_dropdown_open());
2996
2997 let after_cancel = state.current_item().and_then(|item| {
2998 if let SettingControl::Dropdown(ref d) = item.control {
2999 Some(d.selected)
3000 } else {
3001 None
3002 }
3003 });
3004 assert_eq!(initial, after_cancel);
3005 }
3006
3007 #[test]
3008 fn test_dropdown_confirm_keeps_selection() {
3009 let config = test_config();
3010 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3011 state.show();
3012 state.toggle_focus();
3013
3014 state.dropdown_toggle();
3016
3017 state.dropdown_next();
3019 let after_change = state.current_item().and_then(|item| {
3020 if let SettingControl::Dropdown(ref d) = item.control {
3021 Some(d.selected)
3022 } else {
3023 None
3024 }
3025 });
3026
3027 state.dropdown_confirm();
3029 assert!(!state.is_dropdown_open());
3030
3031 let after_confirm = state.current_item().and_then(|item| {
3032 if let SettingControl::Dropdown(ref d) = item.control {
3033 Some(d.selected)
3034 } else {
3035 None
3036 }
3037 });
3038 assert_eq!(after_change, after_confirm);
3039 }
3040
3041 #[test]
3042 fn test_number_editing() {
3043 let config = test_config();
3044 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3045 state.show();
3046 state.toggle_focus();
3047
3048 state.select_next();
3050
3051 assert!(!state.is_number_editing());
3053
3054 state.start_number_editing();
3056 assert!(state.is_number_editing());
3057
3058 state.number_insert('8');
3060
3061 state.number_confirm();
3063 assert!(!state.is_number_editing());
3064 }
3065
3066 #[test]
3067 fn test_number_cancel_editing() {
3068 let config = test_config();
3069 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3070 state.show();
3071 state.toggle_focus();
3072
3073 state.select_next();
3075
3076 let initial_value = state.current_item().and_then(|item| {
3078 if let SettingControl::Number(ref n) = item.control {
3079 Some(n.value)
3080 } else {
3081 None
3082 }
3083 });
3084
3085 state.start_number_editing();
3087 state.number_backspace();
3088 state.number_insert('9');
3089 state.number_insert('9');
3090
3091 state.number_cancel();
3093 assert!(!state.is_number_editing());
3094
3095 let after_cancel = state.current_item().and_then(|item| {
3097 if let SettingControl::Number(ref n) = item.control {
3098 Some(n.value)
3099 } else {
3100 None
3101 }
3102 });
3103 assert_eq!(initial_value, after_cancel);
3104 }
3105
3106 #[test]
3107 fn test_number_backspace() {
3108 let config = test_config();
3109 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3110 state.show();
3111 state.toggle_focus();
3112 state.select_next();
3113
3114 state.start_number_editing();
3115 state.number_backspace();
3116
3117 let display_text = state.current_item().and_then(|item| {
3119 if let SettingControl::Number(ref n) = item.control {
3120 Some(n.display_text())
3121 } else {
3122 None
3123 }
3124 });
3125 assert_eq!(display_text, Some(String::new()));
3127
3128 state.number_cancel();
3129 }
3130
3131 #[test]
3132 fn test_layer_selection() {
3133 let config = test_config();
3134 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3135
3136 assert_eq!(state.target_layer, ConfigLayer::User);
3138 assert_eq!(state.target_layer_name(), "User");
3139
3140 state.cycle_target_layer();
3142 assert_eq!(state.target_layer, ConfigLayer::Project);
3143 assert_eq!(state.target_layer_name(), "Project");
3144
3145 state.cycle_target_layer();
3146 assert_eq!(state.target_layer, ConfigLayer::Session);
3147 assert_eq!(state.target_layer_name(), "Session");
3148
3149 state.cycle_target_layer();
3150 assert_eq!(state.target_layer, ConfigLayer::User);
3151
3152 state.set_target_layer(ConfigLayer::Project);
3154 assert_eq!(state.target_layer, ConfigLayer::Project);
3155
3156 state.set_target_layer(ConfigLayer::System);
3158 assert_eq!(state.target_layer, ConfigLayer::Project);
3159 }
3160
3161 #[test]
3162 fn test_layer_switch_clears_pending_changes() {
3163 let config = test_config();
3164 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3165
3166 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
3168 assert!(state.has_changes());
3169
3170 state.cycle_target_layer();
3172 assert!(!state.has_changes());
3173 }
3174
3175 #[test]
3194 fn nested_array_save_records_full_entry_path() {
3195 use crate::view::settings::schema::SettingType;
3198
3199 let config = test_config();
3200 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3201
3202 let item_schema = SettingSchema {
3204 path: "/item".to_string(),
3205 name: "Server".to_string(),
3206 description: None,
3207 setting_type: SettingType::Object {
3208 properties: vec![SettingSchema {
3209 path: "/enabled".to_string(),
3210 name: "Enabled".to_string(),
3211 description: None,
3212 setting_type: SettingType::Boolean,
3213 default: Some(serde_json::json!(false)),
3214 read_only: false,
3215 section: None,
3216 order: None,
3217 nullable: false,
3218 enum_from: None,
3219 dual_list_sibling: None,
3220 dynamically_extendable_status_bar_elements: false,
3221 }],
3222 },
3223 default: None,
3224 read_only: false,
3225 section: None,
3226 order: None,
3227 nullable: false,
3228 enum_from: None,
3229 dual_list_sibling: None,
3230 dynamically_extendable_status_bar_elements: false,
3231 };
3232
3233 let value_schema = SettingSchema {
3238 path: String::new(),
3239 name: "value".to_string(),
3240 description: None,
3241 setting_type: SettingType::ObjectArray {
3242 item_schema: Box::new(item_schema.clone()),
3243 display_field: None,
3244 },
3245 default: None,
3246 read_only: false,
3247 section: None,
3248 order: None,
3249 nullable: false,
3250 enum_from: None,
3251 dual_list_sibling: None,
3252 dynamically_extendable_status_bar_elements: false,
3253 };
3254
3255 let parent = EntryDialogState::from_schema(
3259 "quicklsp".to_string(),
3260 &serde_json::json!([{ "enabled": true }]),
3261 &value_schema,
3262 "/universal_lsp",
3263 false, false,
3265 &HashMap::new(),
3266 );
3267
3268 assert!(
3270 parent.is_single_value,
3271 "array value_schema should trigger is_single_value path"
3272 );
3273 assert_eq!(parent.entry_path(), "/universal_lsp/quicklsp");
3274
3275 state.entry_dialog_stack.push(parent);
3276
3277 state.open_nested_entry_dialog();
3282
3283 assert_eq!(
3285 state.entry_dialog_stack.len(),
3286 2,
3287 "open_nested_entry_dialog should have pushed a nested dialog"
3288 );
3289
3290 let nested_map_path = state
3293 .entry_dialog_stack
3294 .last()
3295 .map(|d| d.map_path.clone())
3296 .unwrap();
3297 assert_eq!(
3298 nested_map_path, "/universal_lsp/quicklsp",
3299 "BUG: nested dialog's map_path dropped the 'quicklsp' key segment"
3300 );
3301
3302 state.save_entry_dialog();
3304
3305 assert_eq!(state.entry_dialog_stack.len(), 1);
3307
3308 assert!(
3311 !state.pending_changes.contains_key("/universal_lsp/"),
3312 "regression: pending change recorded under empty-key path /universal_lsp/. \
3313 All keys: {:?}",
3314 state.pending_changes.keys().collect::<Vec<_>>()
3315 );
3316 assert!(
3317 !state
3318 .pending_changes
3319 .keys()
3320 .any(|k| k.starts_with("/universal_lsp") && k.ends_with('/')),
3321 "no /universal_lsp/* path should end in a trailing slash; got {:?}",
3322 state.pending_changes.keys().collect::<Vec<_>>()
3323 );
3324 assert!(
3325 state
3326 .pending_changes
3327 .contains_key("/universal_lsp/quicklsp"),
3328 "expected pending change at /universal_lsp/quicklsp, got {:?}",
3329 state.pending_changes.keys().collect::<Vec<_>>()
3330 );
3331 }
3332
3333 #[test]
3334 fn test_refresh_dual_list_sibling_updates_excluded() {
3335 use crate::view::controls::DualListState;
3336
3337 let schema = include_str!("../../../plugins/config-schema.json");
3340 let config = test_config();
3341 let mut state = SettingsState::new(schema, &config).unwrap();
3342
3343 let editor_page_idx = state
3345 .pages
3346 .iter()
3347 .position(|p| p.path == "/editor")
3348 .expect("editor page");
3349 state.selected_category = editor_page_idx;
3350
3351 let (left_idx, right_idx) = {
3352 let page = &state.pages[editor_page_idx];
3353 let l = page
3354 .items
3355 .iter()
3356 .position(|i| i.path == "/editor/status_bar/left")
3357 .expect("left item");
3358 let r = page
3359 .items
3360 .iter()
3361 .position(|i| i.path == "/editor/status_bar/right")
3362 .expect("right item");
3363 (l, r)
3364 };
3365
3366 assert!(matches!(
3368 &state.pages[editor_page_idx].items[left_idx].control,
3369 SettingControl::DualList(_)
3370 ));
3371
3372 let default_right_items: Vec<String> =
3374 match &state.pages[editor_page_idx].items[right_idx].control {
3375 SettingControl::DualList(dl) => dl.included.clone(),
3376 _ => panic!("right should be DualList"),
3377 };
3378 let initial_left_excluded: Vec<String> =
3379 match &state.pages[editor_page_idx].items[left_idx].control {
3380 SettingControl::DualList(dl) => dl.excluded.clone(),
3381 _ => panic!("left should be DualList"),
3382 };
3383 assert_eq!(
3384 initial_left_excluded, default_right_items,
3385 "left.excluded should mirror right's included on initial build"
3386 );
3387
3388 let new_element = "{chord}".to_string();
3390 state.selected_item = left_idx;
3391 state
3392 .with_current_dual_list_mut(|dl: &mut DualListState| {
3393 if !dl.included.contains(&new_element) {
3394 dl.included.push(new_element.clone());
3395 }
3396 })
3397 .expect("current item is a DualList");
3398
3399 state.refresh_dual_list_sibling();
3401
3402 match &state.pages[editor_page_idx].items[right_idx].control {
3403 SettingControl::DualList(dl) => {
3404 assert!(
3405 dl.excluded.contains(&new_element),
3406 "right.excluded should be updated to reflect left's new inclusion"
3407 );
3408 }
3409 _ => panic!("right should be DualList"),
3410 }
3411 }
3412
3413 #[test]
3414 fn test_with_dual_list_mut_returns_none_for_non_dual_list() {
3415 let config = test_config();
3416 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3417
3418 let result = state.with_dual_list_mut(0, |_| ());
3420 assert!(result.is_none());
3421 }
3422}