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_entry_discard_confirm: bool,
128 pub entry_discard_confirm_selection: usize,
131 pub showing_entry_delete_confirm: bool,
135 pub entry_delete_confirm_selection: usize,
138 pub entry_delete_target_name: String,
140 pub entry_delete_target_is_array_item: bool,
145 pub showing_help: bool,
147 pub scroll_panel: ScrollablePanel,
149 pub sub_focus: Option<usize>,
151 pub editing_text: bool,
153 pub hover_position: Option<(u16, u16)>,
155 pub hover_hit: Option<SettingsHit>,
157 pub entry_dialog_stack: Vec<EntryDialogState>,
160 pub target_layer: ConfigLayer,
164 available_status_bar_tokens: HashMap<String, String>,
167 pub layer_sources: HashMap<String, ConfigLayer>,
171 pub pending_deletions: std::collections::HashSet<String>,
175 pub layout_width: u16,
179 pub item_style: super::items::ItemBoxStyle,
182 pub expanded_categories: std::collections::HashSet<usize>,
186 pub categories_scroll: ScrollablePanel,
189 pub tree_cursor_section: Option<usize>,
201}
202
203#[derive(Debug, Clone, Copy)]
210pub enum TreeRow {
211 Category {
212 idx: usize,
213 expandable: bool,
214 expanded: bool,
215 },
216 Section {
217 cat_idx: usize,
218 section_idx: usize,
219 },
220}
221
222impl crate::view::ui::ScrollItem for TreeRow {
223 fn height(&self, _width: u16) -> u16 {
224 1
225 }
226}
227
228impl SettingsState {
229 pub fn new(schema_json: &str, config: &Config) -> Result<Self, serde_json::Error> {
231 Self::new_with_plugin_schemas(schema_json, config, &HashMap::new())
232 }
233
234 pub fn new_with_plugin_schemas(
238 schema_json: &str,
239 config: &Config,
240 plugin_schemas: &HashMap<String, serde_json::Value>,
241 ) -> Result<Self, serde_json::Error> {
242 let mut categories = parse_schema(schema_json)?;
243
244 let mut enabled_with_schema: Vec<String> = config
246 .plugins
247 .iter()
248 .filter_map(|(name, cfg)| {
249 if cfg.enabled && plugin_schemas.contains_key(name) {
250 Some(name.clone())
251 } else {
252 None
253 }
254 })
255 .collect();
256 enabled_with_schema.sort();
257 tracing::trace!(
258 "SettingsState built: total plugin_schemas={}, enabled_with_schema={:?}",
259 plugin_schemas.len(),
260 enabled_with_schema
261 );
262 super::schema::append_plugin_settings_category(
263 &mut categories,
264 plugin_schemas,
265 &enabled_with_schema,
266 );
267
268 let config_value = serde_json::to_value(config)?;
269 let layer_sources = HashMap::new(); let target_layer = ConfigLayer::User; let available_status_bar_tokens: HashMap<String, String> = HashMap::new();
272 let pages = super::items::build_pages(
273 &categories,
274 &config_value,
275 &layer_sources,
276 target_layer,
277 &available_status_bar_tokens,
278 );
279
280 Ok(Self {
281 categories,
282 pages,
283 selected_category: 0,
284 selected_item: 0,
285 focus: FocusManager::new(vec![
286 FocusPanel::Categories,
287 FocusPanel::Settings,
288 FocusPanel::Footer,
289 ]),
290 footer_button_index: 2, pending_changes: HashMap::new(),
292 original_config: config_value,
293 visible: false,
294 search_query: String::new(),
295 search_active: false,
296 search_results: Vec::new(),
297 selected_search_result: 0,
298 search_scroll_offset: 0,
299 search_max_visible: 5, showing_confirm_dialog: false,
301 confirm_dialog_selection: 0,
302 confirm_dialog_hover: None,
303 showing_reset_dialog: false,
304 reset_dialog_selection: 0,
305 reset_dialog_hover: None,
306 showing_entry_discard_confirm: false,
307 entry_discard_confirm_selection: 0,
308 showing_entry_delete_confirm: false,
309 entry_delete_confirm_selection: 0,
310 entry_delete_target_name: String::new(),
311 entry_delete_target_is_array_item: false,
312 showing_help: false,
313 scroll_panel: ScrollablePanel::new(),
314 sub_focus: None,
315 editing_text: false,
316 available_status_bar_tokens,
317 hover_position: None,
318 hover_hit: None,
319 entry_dialog_stack: Vec::new(),
320 target_layer,
321 layer_sources,
322 pending_deletions: std::collections::HashSet::new(),
323 layout_width: 0,
324 item_style: super::items::ItemBoxStyle::default(),
325 expanded_categories: std::collections::HashSet::new(),
326 categories_scroll: ScrollablePanel::new(),
327 tree_cursor_section: None,
328 })
329 }
330
331 #[inline]
333 pub fn focus_panel(&self) -> FocusPanel {
334 self.focus.current().unwrap_or_default()
335 }
336
337 pub fn show(&mut self) {
339 self.visible = true;
340 self.focus.set(FocusPanel::Categories);
341 self.footer_button_index = 2; self.selected_category = 0;
343 self.selected_item = 0;
344 self.scroll_panel = ScrollablePanel::new();
345 self.sub_focus = None;
346 self.showing_confirm_dialog = false;
348 self.confirm_dialog_selection = 0;
349 self.confirm_dialog_hover = None;
350 self.showing_reset_dialog = false;
351 self.reset_dialog_selection = 0;
352 self.reset_dialog_hover = None;
353 self.showing_help = false;
354 }
355
356 fn rebuild_pages(&mut self) {
358 self.pages = super::items::build_pages(
359 &self.categories,
360 &self.original_config,
361 &self.layer_sources,
362 self.target_layer,
363 &self.available_status_bar_tokens,
364 );
365 }
366
367 pub fn hide(&mut self) {
369 self.visible = false;
370 self.search_active = false;
371 self.search_query.clear();
372 }
373
374 pub fn entry_dialog(&self) -> Option<&EntryDialogState> {
376 self.entry_dialog_stack.last()
377 }
378
379 pub fn entry_dialog_mut(&mut self) -> Option<&mut EntryDialogState> {
381 self.entry_dialog_stack.last_mut()
382 }
383
384 pub fn has_entry_dialog(&self) -> bool {
386 !self.entry_dialog_stack.is_empty()
387 }
388
389 pub fn current_page(&self) -> Option<&SettingsPage> {
391 self.pages.get(self.selected_category)
392 }
393
394 pub fn current_page_mut(&mut self) -> Option<&mut SettingsPage> {
396 self.pages.get_mut(self.selected_category)
397 }
398
399 pub fn topmost_visible_item_index(&self) -> Option<usize> {
404 let page = self.pages.get(self.selected_category)?;
405 if page.items.is_empty() {
406 return None;
407 }
408 let target = self.scroll_panel.scroll.offset;
409 let width = self.layout_width;
410 let mut y: u16 = 0;
411 for (idx, item) in page.items.iter().enumerate() {
412 let h = <SettingItem as ScrollItem>::height(item, width);
413 if y + h > target {
414 return Some(idx);
415 }
416 y += h;
417 }
418 Some(page.items.len() - 1)
419 }
420
421 pub fn current_section_index(&self) -> Option<usize> {
426 let page = self.pages.get(self.selected_category)?;
427 if page.sections.is_empty() {
428 return None;
429 }
430 let item_idx = self
439 .topmost_visible_item_index()
440 .unwrap_or(self.selected_item);
441 let mut current: Option<usize> = None;
443 for (s_idx, section) in page.sections.iter().enumerate() {
444 if section.first_item_index <= item_idx {
445 current = Some(s_idx);
446 } else {
447 break;
448 }
449 }
450 current
451 }
452
453 pub fn is_category_expandable(&self, cat_idx: usize) -> bool {
457 self.pages
458 .get(cat_idx)
459 .is_some_and(|p| p.sections.len() > 1)
460 }
461
462 pub fn tree_step(&mut self, delta: i32) {
472 let rows = self.visible_tree();
473 if rows.is_empty() {
474 return;
475 }
476 let cur = self.tree_cursor_index(&rows);
477 let len = rows.len() as i32;
478 let target = (cur as i32 + delta).clamp(0, len - 1) as usize;
479 if target == cur {
480 return;
481 }
482 let prev_category = self.selected_category;
483 self.update_control_focus(false);
484 match rows[target] {
485 TreeRow::Category { idx, .. } => {
486 self.selected_category = idx;
490 self.selected_item = 0;
491 self.tree_cursor_section = None;
492 if idx != prev_category {
493 self.scroll_panel = ScrollablePanel::new();
494 }
495 self.sub_focus = None;
496 self.update_control_focus(true);
497 }
498 TreeRow::Section {
499 cat_idx,
500 section_idx,
501 } => {
502 let first = self.pages[cat_idx].sections[section_idx].first_item_index;
503 self.selected_category = cat_idx;
504 self.selected_item = first;
505 self.tree_cursor_section = Some(section_idx);
506 if cat_idx != prev_category {
507 self.scroll_panel = ScrollablePanel::new();
508 }
509 self.sub_focus = None;
510 self.init_map_focus(true);
511 self.update_control_focus(true);
512 }
513 }
514 let width = self.layout_width;
520 if let Some(page) = self.pages.get(self.selected_category) {
521 self.scroll_panel.update_content_height(&page.items, width);
522 if matches!(rows[target], TreeRow::Section { .. }) {
530 let item_y =
531 self.scroll_panel
532 .item_y_offset(&page.items, self.selected_item, width);
533 self.scroll_panel.scroll.offset = item_y;
534 } else {
535 let selected_item = self.selected_item;
536 let sub_focus = self.sub_focus;
537 self.scroll_panel.ensure_focused_visible(
538 &page.items,
539 selected_item,
540 sub_focus,
541 width,
542 );
543 }
544 }
545 let new_rows = self.visible_tree();
546 let new_cur = self.tree_cursor_index(&new_rows);
547 self.categories_scroll
548 .ensure_focused_visible(&new_rows, new_cur, None, width);
549 }
550
551 pub(super) fn tree_cursor_index(&self, rows: &[TreeRow]) -> usize {
559 let cat = self.selected_category;
560 if let Some(s_idx) = self.tree_cursor_section {
561 for (i, row) in rows.iter().enumerate() {
562 if let TreeRow::Section {
563 cat_idx,
564 section_idx,
565 } = *row
566 {
567 if cat_idx == cat && section_idx == s_idx {
568 return i;
569 }
570 }
571 }
572 }
573 for (i, row) in rows.iter().enumerate() {
574 if let TreeRow::Category { idx, .. } = *row {
575 if idx == cat {
576 return i;
577 }
578 }
579 }
580 0
581 }
582
583 pub fn auto_expand_current_category(&mut self) {
593 let idx = self.selected_category;
594 if self.is_category_expandable(idx) {
595 self.expanded_categories.insert(idx);
596 }
597 }
598
599 pub fn toggle_category_expanded(&mut self, cat_idx: usize) {
600 if !self.is_category_expandable(cat_idx) {
601 return;
602 }
603 if !self.expanded_categories.insert(cat_idx) {
604 self.expanded_categories.remove(&cat_idx);
605 }
606 }
607
608 pub fn jump_to_section(&mut self, cat_idx: usize, section_idx: usize) {
612 let Some(page) = self.pages.get(cat_idx) else {
613 return;
614 };
615 let Some(section) = page.sections.get(section_idx) else {
616 return;
617 };
618 let target_item = section.first_item_index;
619 self.update_control_focus(false);
620 self.selected_category = cat_idx;
621 self.selected_item = target_item;
622 self.tree_cursor_section = Some(section_idx);
623 self.focus.set(FocusPanel::Settings);
624 let width = self.layout_width;
625 if let Some(page) = self.pages.get(self.selected_category) {
626 self.scroll_panel.update_content_height(&page.items, width);
627 let item_y = self
634 .scroll_panel
635 .item_y_offset(&page.items, target_item, width);
636 self.scroll_panel.scroll.offset = item_y;
637 }
638 self.sub_focus = None;
639 self.init_map_focus(true);
640 self.update_control_focus(true);
641 self.auto_expand_current_category();
642 }
643
644 pub fn visible_tree(&self) -> Vec<TreeRow> {
648 let mut rows = Vec::with_capacity(self.pages.len());
649 for (idx, page) in self.pages.iter().enumerate() {
650 let expandable = page.sections.len() > 1;
651 let expanded = expandable && self.expanded_categories.contains(&idx);
652 rows.push(TreeRow::Category {
653 idx,
654 expandable,
655 expanded,
656 });
657 if expanded {
658 for section_idx in 0..page.sections.len() {
659 rows.push(TreeRow::Section {
660 cat_idx: idx,
661 section_idx,
662 });
663 }
664 }
665 }
666 rows
667 }
668
669 pub fn current_item(&self) -> Option<&SettingItem> {
671 self.current_page()
672 .and_then(|page| page.items.get(self.selected_item))
673 }
674
675 pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
677 self.pages
678 .get_mut(self.selected_category)
679 .and_then(|page| page.items.get_mut(self.selected_item))
680 }
681
682 pub fn can_exit_text_editing(&self) -> bool {
684 self.current_item()
685 .map(|item| {
686 if let SettingControl::Text(state) = &item.control {
687 state.is_valid()
688 } else {
689 true
690 }
691 })
692 .unwrap_or(true)
693 }
694
695 pub fn entry_dialog_can_exit_text_editing(&self) -> bool {
697 self.entry_dialog()
698 .and_then(|dialog| dialog.current_item())
699 .map(|item| {
700 if let SettingControl::Text(state) = &item.control {
701 state.is_valid()
702 } else {
703 true
704 }
705 })
706 .unwrap_or(true)
707 }
708
709 fn init_map_focus(&mut self, from_above: bool) {
712 if let Some(item) = self.current_item_mut() {
713 if let SettingControl::Map(ref mut map_state) = item.control {
714 map_state.init_focus(from_above);
715 }
716 }
717 self.update_map_sub_focus();
719 }
720
721 pub(super) fn update_control_focus(&mut self, focused: bool) {
725 let focus_state = if focused {
726 FocusState::Focused
727 } else {
728 FocusState::Normal
729 };
730 if let Some(item) = self.current_item_mut() {
731 match &mut item.control {
732 SettingControl::Map(ref mut state) => state.focus = focus_state,
733 SettingControl::TextList(ref mut state) => state.focus = focus_state,
734 SettingControl::DualList(ref mut state) => state.focus = focus_state,
735 SettingControl::ObjectArray(ref mut state) => state.focus = focus_state,
736 SettingControl::Toggle(ref mut state) => state.focus = focus_state,
737 SettingControl::Number(ref mut state) => state.focus = focus_state,
738 SettingControl::Dropdown(ref mut state) => state.focus = focus_state,
739 SettingControl::Text(ref mut state) => {
740 state.focus = focus_state;
741 if !focused {
745 state.editing = false;
746 }
747 }
748 SettingControl::Json(_) | SettingControl::Complex { .. } => {} }
750 }
751 }
752
753 fn update_map_sub_focus(&mut self) {
756 self.sub_focus = self.current_item().and_then(|item| {
757 if let SettingControl::Map(ref map_state) = item.control {
758 Some(match map_state.focused_entry {
760 Some(i) => 1 + i,
761 None => 1 + map_state.entries.len(), })
763 } else {
764 None
765 }
766 });
767 }
768
769 pub fn select_prev(&mut self) {
771 match self.focus_panel() {
772 FocusPanel::Categories => {
773 self.tree_step(-1);
774 }
775 FocusPanel::Settings => {
776 let handled = self
778 .current_item_mut()
779 .and_then(|item| match &mut item.control {
780 SettingControl::Map(map_state) => Some(map_state.focus_prev()),
781 _ => None,
782 })
783 .unwrap_or(false);
784
785 if handled {
786 self.update_map_sub_focus();
788 } else if self.selected_item > 0 {
789 self.update_control_focus(false); self.selected_item -= 1;
791 self.sub_focus = None;
792 self.init_map_focus(false); self.update_control_focus(true); }
795 self.ensure_visible();
796 }
797 FocusPanel::Footer => {
798 if self.footer_button_index > 0 {
800 self.footer_button_index -= 1;
801 }
802 }
803 }
804 }
805
806 pub fn select_next(&mut self) {
808 match self.focus_panel() {
809 FocusPanel::Categories => {
810 self.tree_step(1);
811 }
812 FocusPanel::Settings => {
813 let handled = self
815 .current_item_mut()
816 .and_then(|item| match &mut item.control {
817 SettingControl::Map(map_state) => Some(map_state.focus_next()),
818 _ => None,
819 })
820 .unwrap_or(false);
821
822 if handled {
823 self.update_map_sub_focus();
825 } else {
826 let can_move = self
827 .current_page()
828 .is_some_and(|page| self.selected_item + 1 < page.items.len());
829 if can_move {
830 self.update_control_focus(false); self.selected_item += 1;
832 self.sub_focus = None;
833 self.init_map_focus(true); self.update_control_focus(true); }
836 }
837 self.ensure_visible();
838 }
839 FocusPanel::Footer => {
840 if self.footer_button_index < 2 {
842 self.footer_button_index += 1;
843 }
844 }
845 }
846 }
847
848 pub fn select_next_page(&mut self) {
850 let page_size = self.scroll_panel.viewport_height().max(1);
851 for _ in 0..page_size {
852 self.select_next();
853 }
854 }
855
856 pub fn select_prev_page(&mut self) {
858 let page_size = self.scroll_panel.viewport_height().max(1);
859 for _ in 0..page_size {
860 self.select_prev();
861 }
862 }
863
864 pub fn toggle_focus(&mut self) {
866 let old_panel = self.focus_panel();
867 self.focus.focus_next();
868 self.on_panel_changed(old_panel, true);
869 }
870
871 pub fn toggle_focus_backward(&mut self) {
873 let old_panel = self.focus_panel();
874 self.focus.focus_prev();
875 self.on_panel_changed(old_panel, false);
876 }
877
878 fn on_panel_changed(&mut self, old_panel: FocusPanel, forward: bool) {
880 if old_panel == FocusPanel::Settings {
882 self.update_control_focus(false);
883 }
884
885 if self.focus_panel() == FocusPanel::Settings
887 && self.selected_item >= self.current_page().map_or(0, |p| p.items.len())
888 {
889 self.selected_item = 0;
890 }
891 self.sub_focus = None;
892
893 if self.focus_panel() == FocusPanel::Settings {
894 self.init_map_focus(forward); self.update_control_focus(true); }
897
898 if self.focus_panel() == FocusPanel::Footer {
900 self.footer_button_index = if forward {
901 0 } else {
903 4 };
905 }
906
907 self.ensure_visible();
908 }
909
910 pub fn set_item_style(&mut self, style: super::items::ItemBoxStyle) {
918 if self.item_style == style {
919 return;
920 }
921 self.item_style = style;
922 for page in &mut self.pages {
923 for item in &mut page.items {
924 item.style = style;
925 }
926 }
927 let width = self.layout_width;
928 if let Some(page) = self.pages.get(self.selected_category) {
929 self.scroll_panel.update_content_height(&page.items, width);
930 }
931 }
932
933 pub fn ensure_visible(&mut self) {
935 if self.focus_panel() != FocusPanel::Settings {
936 return;
937 }
938
939 let selected_item = self.selected_item;
941 let sub_focus = self.sub_focus;
942 let width = self.layout_width;
943 let prev_offset = self.scroll_panel.scroll.offset;
944 if let Some(page) = self.pages.get(self.selected_category) {
945 self.scroll_panel
946 .ensure_focused_visible(&page.items, selected_item, sub_focus, width);
947 }
948 if self.scroll_panel.scroll.offset != prev_offset {
952 self.sync_tree_cursor_to_body_scroll();
953 }
954 }
955
956 pub fn set_pending_change(&mut self, path: &str, value: serde_json::Value) {
958 let original = self.original_config.pointer(path);
960 if original == Some(&value) {
961 self.pending_changes.remove(path);
962 } else {
963 self.pending_changes.insert(path.to_string(), value);
964 }
965 }
966
967 pub fn has_changes(&self) -> bool {
969 !self.pending_changes.is_empty() || !self.pending_deletions.is_empty()
970 }
971
972 pub fn apply_changes(&self, config: &Config) -> Result<Config, serde_json::Error> {
974 let mut config_value = serde_json::to_value(config)?;
975
976 for path in &self.pending_deletions {
986 crate::config_io::remove_json_pointer(&mut config_value, path);
987 }
988
989 for (path, value) in &self.pending_changes {
990 if let Some(target) = config_value.pointer_mut(path) {
997 *target = value.clone();
998 } else {
999 set_json_pointer_create(&mut config_value, path, value.clone());
1000 }
1001 }
1002
1003 serde_json::from_value(config_value)
1004 }
1005
1006 pub fn discard_changes(&mut self) {
1008 self.pending_changes.clear();
1009 self.pending_deletions.clear();
1010 self.rebuild_pages();
1012 }
1013
1014 pub fn set_target_layer(&mut self, layer: ConfigLayer) {
1016 if layer != ConfigLayer::System {
1017 self.target_layer = layer;
1019 self.pending_changes.clear();
1021 self.pending_deletions.clear();
1022 self.rebuild_pages();
1024 }
1025 }
1026
1027 pub fn cycle_target_layer(&mut self) {
1029 self.target_layer = match self.target_layer {
1030 ConfigLayer::System => ConfigLayer::User, ConfigLayer::User => ConfigLayer::Project,
1032 ConfigLayer::Project => ConfigLayer::Session,
1033 ConfigLayer::Session => ConfigLayer::User,
1034 };
1035 self.pending_changes.clear();
1037 self.pending_deletions.clear();
1038 self.rebuild_pages();
1040 }
1041
1042 pub fn target_layer_name(&self) -> &'static str {
1044 match self.target_layer {
1045 ConfigLayer::System => "System (read-only)",
1046 ConfigLayer::User => "User",
1047 ConfigLayer::Project => "Project",
1048 ConfigLayer::Session => "Session",
1049 }
1050 }
1051
1052 pub fn set_layer_sources(&mut self, sources: HashMap<String, ConfigLayer>) {
1055 self.layer_sources = sources;
1056 self.rebuild_pages();
1058 }
1059
1060 pub fn set_status_bar_tokens(&mut self, tokens: HashMap<String, String>) {
1063 self.available_status_bar_tokens = tokens;
1064 self.rebuild_pages();
1065 }
1066
1067 pub fn get_layer_source(&self, path: &str) -> ConfigLayer {
1070 self.layer_sources
1071 .get(path)
1072 .copied()
1073 .unwrap_or(ConfigLayer::System)
1074 }
1075
1076 pub fn layer_source_label(layer: ConfigLayer) -> &'static str {
1078 match layer {
1079 ConfigLayer::System => "default",
1080 ConfigLayer::User => "user",
1081 ConfigLayer::Project => "project",
1082 ConfigLayer::Session => "session",
1083 }
1084 }
1085
1086 pub fn reset_focused_entry_field(&mut self) {
1101 let Some(dialog) = self.entry_dialog_mut() else {
1102 return;
1103 };
1104 if dialog.focus_on_buttons {
1105 return;
1106 }
1107 let idx = dialog.selected_item;
1108 let Some(item) = dialog.items.get_mut(idx) else {
1109 return;
1110 };
1111 if item.read_only {
1112 return;
1113 }
1114 let Some(default) = item.default.clone() else {
1115 return;
1116 };
1117 update_control_from_value(&mut item.control, &default);
1118 item.modified = false;
1119 dialog.user_edited = true;
1120 }
1121
1122 pub fn reset_current_to_default(&mut self) {
1123 let reset_info = self.current_item().and_then(|item| {
1125 if !item.modified || item.is_auto_managed {
1128 return None;
1129 }
1130 item.default
1131 .as_ref()
1132 .map(|default| (item.path.clone(), default.clone()))
1133 });
1134
1135 if let Some((path, default)) = reset_info {
1136 self.pending_deletions.insert(path.clone());
1138 self.pending_changes.remove(&path);
1140
1141 if let Some(item) = self.current_item_mut() {
1145 update_control_from_value(&mut item.control, &default);
1146 item.modified = false;
1147 item.layer_source = ConfigLayer::System; }
1150 }
1151 }
1152
1153 pub fn set_current_to_null(&mut self) {
1159 let target_layer = self.target_layer;
1160 let change_info = self.current_item().and_then(|item| {
1161 if !item.nullable || item.is_null || item.read_only {
1162 return None;
1163 }
1164 Some(item.path.clone())
1165 });
1166
1167 if let Some(path) = change_info {
1168 self.pending_changes
1170 .insert(path.clone(), serde_json::Value::Null);
1171 self.pending_deletions.remove(&path);
1172
1173 if let Some(item) = self.current_item_mut() {
1175 item.is_null = true;
1176 item.modified = true;
1177 item.layer_source = target_layer;
1178 }
1179 }
1180 }
1181
1182 pub fn clear_current_category(&mut self) {
1188 let target_layer = self.target_layer;
1189 let page = match self.current_page() {
1190 Some(p) if p.nullable => p,
1191 _ => return,
1192 };
1193 let page_path = page.path.clone();
1194
1195 self.pending_changes
1197 .insert(page_path.clone(), serde_json::Value::Null);
1198
1199 let prefix = format!("{}/", page_path);
1201 self.pending_changes
1202 .retain(|path, _| !path.starts_with(&prefix));
1203 self.pending_deletions
1204 .retain(|path| !path.starts_with(&prefix));
1205
1206 if let Some(page) = self.current_page_mut() {
1208 for item in &mut page.items {
1209 if item.nullable {
1210 item.is_null = true;
1211 item.modified = false;
1212 item.layer_source = target_layer;
1213 }
1214 }
1215 }
1216 }
1217
1218 pub fn current_category_has_values(&self) -> bool {
1220 match self.current_page() {
1221 Some(page) if page.nullable => {
1222 page.items.iter().any(|item| !item.is_null && item.nullable)
1223 || page.items.iter().any(|item| item.modified)
1224 }
1225 _ => false,
1226 }
1227 }
1228
1229 pub fn on_value_changed(&mut self) {
1231 let target_layer = self.target_layer;
1233
1234 let change_info = self.current_item().map(|item| {
1236 let value = control_to_value(&item.control);
1237 (item.path.clone(), value)
1238 });
1239
1240 if let Some((path, value)) = change_info {
1241 self.pending_deletions.remove(&path);
1244
1245 if let Some(item) = self.current_item_mut() {
1247 item.modified = true; item.layer_source = target_layer; item.is_null = false; }
1251 self.set_pending_change(&path, value);
1252 }
1253 }
1254
1255 pub fn update_focus_states(&mut self) {
1257 let current_focus = self.focus_panel();
1258 for (page_idx, page) in self.pages.iter_mut().enumerate() {
1259 for (item_idx, item) in page.items.iter_mut().enumerate() {
1260 let is_focused = current_focus == FocusPanel::Settings
1261 && page_idx == self.selected_category
1262 && item_idx == self.selected_item;
1263
1264 let focus = if is_focused {
1265 FocusState::Focused
1266 } else {
1267 FocusState::Normal
1268 };
1269
1270 match &mut item.control {
1271 SettingControl::Toggle(state) => state.focus = focus,
1272 SettingControl::Number(state) => state.focus = focus,
1273 SettingControl::Dropdown(state) => state.focus = focus,
1274 SettingControl::Text(state) => state.focus = focus,
1275 SettingControl::TextList(state) => state.focus = focus,
1276 SettingControl::DualList(state) => state.focus = focus,
1277 SettingControl::Map(state) => state.focus = focus,
1278 SettingControl::ObjectArray(state) => state.focus = focus,
1279 SettingControl::Json(state) => state.focus = focus,
1280 SettingControl::Complex { .. } => {}
1281 }
1282 }
1283 }
1284 }
1285
1286 pub fn start_search(&mut self) {
1288 self.search_active = true;
1289 self.search_query.clear();
1290 self.search_results.clear();
1291 self.selected_search_result = 0;
1292 self.search_scroll_offset = 0;
1293 }
1294
1295 pub fn cancel_search(&mut self) {
1297 self.search_active = false;
1298 self.search_query.clear();
1299 self.search_results.clear();
1300 self.selected_search_result = 0;
1301 self.search_scroll_offset = 0;
1302 }
1303
1304 pub fn set_search_query(&mut self, query: String) {
1306 self.search_query = query;
1307 self.search_results = search_settings(&self.pages, &self.search_query);
1308 self.selected_search_result = 0;
1309 self.search_scroll_offset = 0;
1310 }
1311
1312 pub fn search_push_char(&mut self, c: char) {
1314 self.search_query.push(c);
1315 self.search_results = search_settings(&self.pages, &self.search_query);
1316 self.selected_search_result = 0;
1317 self.search_scroll_offset = 0;
1318 }
1319
1320 pub fn search_pop_char(&mut self) {
1322 self.search_query.pop();
1323 self.search_results = search_settings(&self.pages, &self.search_query);
1324 self.selected_search_result = 0;
1325 self.search_scroll_offset = 0;
1326 }
1327
1328 pub fn search_prev(&mut self) {
1330 if !self.search_results.is_empty() && self.selected_search_result > 0 {
1331 self.selected_search_result -= 1;
1332 if self.selected_search_result < self.search_scroll_offset {
1334 self.search_scroll_offset = self.selected_search_result;
1335 }
1336 }
1337 }
1338
1339 pub fn search_next(&mut self) {
1341 if !self.search_results.is_empty()
1342 && self.selected_search_result + 1 < self.search_results.len()
1343 {
1344 self.selected_search_result += 1;
1345 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1347 self.search_scroll_offset =
1348 self.selected_search_result - self.search_max_visible + 1;
1349 }
1350 }
1351 }
1352
1353 pub fn search_scroll_up(&mut self, delta: usize) -> bool {
1355 if self.search_results.is_empty() || self.search_scroll_offset == 0 {
1356 return false;
1357 }
1358 self.search_scroll_offset = self.search_scroll_offset.saturating_sub(delta);
1359 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1361 self.selected_search_result = self.search_scroll_offset + self.search_max_visible - 1;
1362 }
1363 true
1364 }
1365
1366 pub fn search_scroll_down(&mut self, delta: usize) -> bool {
1368 if self.search_results.is_empty() {
1369 return false;
1370 }
1371 let max_offset = self
1372 .search_results
1373 .len()
1374 .saturating_sub(self.search_max_visible);
1375 if self.search_scroll_offset >= max_offset {
1376 return false;
1377 }
1378 self.search_scroll_offset = (self.search_scroll_offset + delta).min(max_offset);
1379 if self.selected_search_result < self.search_scroll_offset {
1381 self.selected_search_result = self.search_scroll_offset;
1382 }
1383 true
1384 }
1385
1386 pub fn search_scroll_to_ratio(&mut self, ratio: f32) -> bool {
1388 if self.search_results.is_empty() {
1389 return false;
1390 }
1391 let max_offset = self
1392 .search_results
1393 .len()
1394 .saturating_sub(self.search_max_visible);
1395 let new_offset = (ratio * max_offset as f32) as usize;
1396 if new_offset != self.search_scroll_offset {
1397 self.search_scroll_offset = new_offset.min(max_offset);
1398 if self.selected_search_result < self.search_scroll_offset {
1400 self.selected_search_result = self.search_scroll_offset;
1401 } else if self.selected_search_result
1402 >= self.search_scroll_offset + self.search_max_visible
1403 {
1404 self.selected_search_result =
1405 self.search_scroll_offset + self.search_max_visible - 1;
1406 }
1407 return true;
1408 }
1409 false
1410 }
1411
1412 pub fn jump_to_search_result(&mut self) {
1414 let Some(result) = self
1416 .search_results
1417 .get(self.selected_search_result)
1418 .cloned()
1419 else {
1420 return;
1421 };
1422 let page_index = result.page_index;
1423 let item_index = result.item_index;
1424
1425 self.update_control_focus(false);
1427 self.selected_category = page_index;
1428 self.selected_item = item_index;
1429 self.focus.set(FocusPanel::Settings);
1430 self.scroll_panel.scroll.offset = 0;
1432 let width = self.layout_width;
1434 if let Some(page) = self.pages.get(self.selected_category) {
1435 self.scroll_panel.update_content_height(&page.items, width);
1436 }
1437 self.sub_focus = None;
1438 self.init_map_focus(true);
1439
1440 if let Some(ref deep_match) = result.deep_match {
1442 self.jump_to_deep_match(deep_match);
1443 }
1444
1445 self.update_control_focus(true); self.auto_expand_current_category();
1447 self.tree_cursor_section = self.current_section_index();
1451 self.ensure_visible();
1452 self.cancel_search();
1453 }
1454
1455 fn jump_to_deep_match(&mut self, deep_match: &DeepMatch) {
1457 match deep_match {
1458 DeepMatch::MapKey { entry_index, .. } | DeepMatch::MapValue { entry_index, .. } => {
1459 if let Some(item) = self.current_item_mut() {
1460 if let SettingControl::Map(ref mut map_state) = item.control {
1461 map_state.focused_entry = Some(*entry_index);
1462 }
1463 }
1464 self.update_map_sub_focus();
1465 }
1466 DeepMatch::TextListItem { item_index, .. } => {
1467 if let Some(item) = self.current_item_mut() {
1468 if let SettingControl::TextList(ref mut list_state) = item.control {
1469 list_state.focused_item = Some(*item_index);
1470 }
1471 }
1472 self.sub_focus = Some(1 + *item_index);
1474 }
1475 }
1476 }
1477
1478 pub fn current_search_result(&self) -> Option<&SearchResult> {
1480 self.search_results.get(self.selected_search_result)
1481 }
1482
1483 pub fn show_confirm_dialog(&mut self) {
1485 self.showing_confirm_dialog = true;
1486 self.confirm_dialog_selection = 0; }
1488
1489 pub fn hide_confirm_dialog(&mut self) {
1491 self.showing_confirm_dialog = false;
1492 self.confirm_dialog_selection = 0;
1493 }
1494
1495 pub fn confirm_dialog_next(&mut self) {
1497 self.confirm_dialog_selection = (self.confirm_dialog_selection + 1) % 3;
1498 }
1499
1500 pub fn confirm_dialog_prev(&mut self) {
1502 self.confirm_dialog_selection = if self.confirm_dialog_selection == 0 {
1503 2
1504 } else {
1505 self.confirm_dialog_selection - 1
1506 };
1507 }
1508
1509 pub fn toggle_help(&mut self) {
1511 self.showing_help = !self.showing_help;
1512 }
1513
1514 pub fn hide_help(&mut self) {
1516 self.showing_help = false;
1517 }
1518
1519 pub fn showing_entry_dialog(&self) -> bool {
1521 self.has_entry_dialog()
1522 }
1523
1524 pub fn open_entry_dialog(&mut self) {
1526 let Some(item) = self.current_item() else {
1527 return;
1528 };
1529
1530 let path = item.path.as_str();
1532 let SettingControl::Map(map_state) = &item.control else {
1533 return;
1534 };
1535
1536 let Some(entry_idx) = map_state.focused_entry else {
1538 return;
1539 };
1540 let Some((key, value)) = map_state.entries.get(entry_idx) else {
1541 return;
1542 };
1543
1544 let Some(schema) = map_state.value_schema.as_ref() else {
1546 return; };
1548
1549 let no_delete = map_state.no_add;
1551
1552 let dialog = EntryDialogState::from_schema(
1554 key.clone(),
1555 value,
1556 schema,
1557 path,
1558 false,
1559 no_delete,
1560 &self.available_status_bar_tokens,
1561 );
1562 self.entry_dialog_stack.push(dialog);
1563 }
1564
1565 pub fn open_add_entry_dialog(&mut self) {
1567 let Some(item) = self.current_item() else {
1568 return;
1569 };
1570 let SettingControl::Map(map_state) = &item.control else {
1571 return;
1572 };
1573 let Some(schema) = map_state.value_schema.as_ref() else {
1574 return;
1575 };
1576 let path = item.path.clone();
1577
1578 let dialog = EntryDialogState::from_schema(
1581 String::new(),
1582 &serde_json::json!({}),
1583 schema,
1584 &path,
1585 true,
1586 false,
1587 &self.available_status_bar_tokens,
1588 );
1589 self.entry_dialog_stack.push(dialog);
1590 }
1591
1592 pub fn open_add_array_item_dialog(&mut self) {
1594 let Some(item) = self.current_item() else {
1595 return;
1596 };
1597 let SettingControl::ObjectArray(array_state) = &item.control else {
1598 return;
1599 };
1600 let Some(schema) = array_state.item_schema.as_ref() else {
1601 return;
1602 };
1603 let path = item.path.clone();
1604
1605 let dialog = EntryDialogState::for_array_item(
1607 None,
1608 &serde_json::json!({}),
1609 schema,
1610 &path,
1611 true,
1612 &self.available_status_bar_tokens,
1613 );
1614 self.entry_dialog_stack.push(dialog);
1615 }
1616
1617 pub fn open_edit_array_item_dialog(&mut self) {
1619 let Some(item) = self.current_item() else {
1620 return;
1621 };
1622 let SettingControl::ObjectArray(array_state) = &item.control else {
1623 return;
1624 };
1625 let Some(schema) = array_state.item_schema.as_ref() else {
1626 return;
1627 };
1628 let Some(index) = array_state.focused_index else {
1629 return;
1630 };
1631 let Some(value) = array_state.bindings.get(index) else {
1632 return;
1633 };
1634 let path = item.path.clone();
1635
1636 let dialog = EntryDialogState::for_array_item(
1637 Some(index),
1638 value,
1639 schema,
1640 &path,
1641 false,
1642 &self.available_status_bar_tokens,
1643 );
1644 self.entry_dialog_stack.push(dialog);
1645 }
1646
1647 pub fn close_entry_dialog(&mut self) {
1649 self.entry_dialog_stack.pop();
1650 }
1651
1652 pub fn open_nested_entry_dialog(&mut self) {
1657 let nested_info = self.entry_dialog().and_then(|dialog| {
1659 let item = dialog.current_item()?;
1660 let base = dialog.entry_path();
1666 let relative = item.path.trim_start_matches('/');
1667 let path = if relative.is_empty() {
1668 base
1672 } else {
1673 format!("{}/{}", base, relative)
1674 };
1675
1676 match &item.control {
1677 SettingControl::Map(map_state) => {
1678 let schema = map_state.value_schema.as_ref()?;
1679 let no_delete = map_state.no_add; if let Some(entry_idx) = map_state.focused_entry {
1681 let (key, value) = map_state.entries.get(entry_idx)?;
1683 Some(NestedDialogInfo::MapEntry {
1684 key: key.clone(),
1685 value: value.clone(),
1686 schema: schema.as_ref().clone(),
1687 path,
1688 is_new: false,
1689 no_delete,
1690 })
1691 } else {
1692 Some(NestedDialogInfo::MapEntry {
1694 key: String::new(),
1695 value: serde_json::json!({}),
1696 schema: schema.as_ref().clone(),
1697 path,
1698 is_new: true,
1699 no_delete: false, })
1701 }
1702 }
1703 SettingControl::ObjectArray(array_state) => {
1704 let schema = array_state.item_schema.as_ref()?;
1705 if let Some(index) = array_state.focused_index {
1706 let value = array_state.bindings.get(index)?;
1708 Some(NestedDialogInfo::ArrayItem {
1709 index: Some(index),
1710 value: value.clone(),
1711 schema: schema.as_ref().clone(),
1712 path,
1713 is_new: false,
1714 })
1715 } else {
1716 Some(NestedDialogInfo::ArrayItem {
1718 index: None,
1719 value: serde_json::json!({}),
1720 schema: schema.as_ref().clone(),
1721 path,
1722 is_new: true,
1723 })
1724 }
1725 }
1726 _ => None,
1727 }
1728 });
1729
1730 if let Some(info) = nested_info {
1732 let dialog = match info {
1733 NestedDialogInfo::MapEntry {
1734 key,
1735 value,
1736 schema,
1737 path,
1738 is_new,
1739 no_delete,
1740 } => EntryDialogState::from_schema(
1741 key,
1742 &value,
1743 &schema,
1744 &path,
1745 is_new,
1746 no_delete,
1747 &self.available_status_bar_tokens,
1748 ),
1749 NestedDialogInfo::ArrayItem {
1750 index,
1751 value,
1752 schema,
1753 path,
1754 is_new,
1755 } => EntryDialogState::for_array_item(
1756 index,
1757 &value,
1758 &schema,
1759 &path,
1760 is_new,
1761 &self.available_status_bar_tokens,
1762 ),
1763 };
1764 self.entry_dialog_stack.push(dialog);
1765 }
1766 }
1767
1768 pub fn save_entry_dialog(&mut self) {
1773 let is_array = if self.entry_dialog_stack.len() > 1 {
1777 self.entry_dialog_stack
1779 .get(self.entry_dialog_stack.len() - 2)
1780 .and_then(|parent| parent.current_item())
1781 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1782 .unwrap_or(false)
1783 } else {
1784 self.current_item()
1786 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1787 .unwrap_or(false)
1788 };
1789
1790 if is_array {
1791 self.save_array_item_dialog_inner();
1792 } else {
1793 self.save_map_entry_dialog_inner();
1794 }
1795 }
1796
1797 fn save_map_entry_dialog_inner(&mut self) {
1799 let Some(mut dialog) = self.entry_dialog_stack.pop() else {
1800 return;
1801 };
1802 dialog.commit_pending_list_drafts();
1806
1807 let key = dialog.get_key();
1809 if key.is_empty() {
1810 return; }
1812
1813 let value = dialog.to_value();
1814 let map_path = dialog.map_path.clone();
1815 let original_key = dialog.entry_key.clone();
1816 let is_new = dialog.is_new;
1817 let key_changed = !is_new && key != original_key;
1818
1819 if let Some(item) = self.current_item_mut() {
1821 if let SettingControl::Map(map_state) = &mut item.control {
1822 if key_changed {
1824 if let Some(idx) = map_state
1825 .entries
1826 .iter()
1827 .position(|(k, _)| k == &original_key)
1828 {
1829 map_state.entries.remove(idx);
1830 }
1831 }
1832
1833 if let Some(entry) = map_state.entries.iter_mut().find(|(k, _)| k == &key) {
1835 entry.1 = value.clone();
1836 } else {
1837 map_state.entries.push((key.clone(), value.clone()));
1838 map_state.entries.sort_by(|a, b| a.0.cmp(&b.0));
1839 }
1840 }
1841 }
1842
1843 if key_changed {
1845 let old_path = format!("{}/{}", map_path, original_key);
1846 self.pending_changes
1847 .insert(old_path, serde_json::Value::Null);
1848 }
1849
1850 let path = format!("{}/{}", map_path, key);
1852 self.set_pending_change(&path, value);
1853 }
1854
1855 fn save_array_item_dialog_inner(&mut self) {
1857 let Some(mut dialog) = self.entry_dialog_stack.pop() else {
1858 return;
1859 };
1860 dialog.commit_pending_list_drafts();
1862
1863 let value = dialog.to_value();
1864 let array_path = dialog.map_path.clone();
1865 let is_new = dialog.is_new;
1866 let entry_key = dialog.entry_key.clone();
1867
1868 let is_nested = !self.entry_dialog_stack.is_empty();
1870
1871 if is_nested {
1872 let parent_entry_path = self
1880 .entry_dialog_stack
1881 .last()
1882 .map(|p| p.entry_path())
1883 .unwrap_or_default();
1884 let item_path = array_path
1885 .strip_prefix(parent_entry_path.as_str())
1886 .unwrap_or(&array_path)
1887 .trim_end_matches('/')
1888 .to_string();
1889
1890 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1896 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1897 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1898 if is_new {
1899 array_state.bindings.push(value.clone());
1900 } else if let Ok(index) = entry_key.parse::<usize>() {
1901 if index < array_state.bindings.len() {
1902 array_state.bindings[index] = value.clone();
1903 }
1904 }
1905 parent.user_edited = true;
1906 }
1907 }
1908 }
1909
1910 if let Some(parent) = self.entry_dialog_stack.last() {
1913 if let Some(item) = parent.items.iter().find(|i| i.path == item_path) {
1914 if let SettingControl::ObjectArray(array_state) = &item.control {
1915 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1916 self.set_pending_change(&array_path, array_value);
1917 }
1918 }
1919 }
1920 } else {
1921 if let Some(item) = self.current_item_mut() {
1923 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1924 if is_new {
1925 array_state.bindings.push(value.clone());
1926 } else if let Ok(index) = entry_key.parse::<usize>() {
1927 if index < array_state.bindings.len() {
1928 array_state.bindings[index] = value.clone();
1929 }
1930 }
1931 }
1932 }
1933
1934 if let Some(item) = self.current_item() {
1936 if let SettingControl::ObjectArray(array_state) = &item.control {
1937 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1938 self.set_pending_change(&array_path, array_value);
1939 }
1940 }
1941 }
1942 }
1943
1944 pub fn request_entry_delete_confirm(&mut self) {
1950 let (name, is_array_item) = self
1951 .entry_dialog()
1952 .map(|d| (d.entry_key.clone(), d.is_array_item))
1953 .unwrap_or_default();
1954 self.entry_delete_target_name = if is_array_item { String::new() } else { name };
1958 self.entry_delete_target_is_array_item = is_array_item;
1959 self.entry_delete_confirm_selection = 0;
1960 self.showing_entry_delete_confirm = true;
1961 }
1962
1963 pub fn delete_entry_dialog(&mut self) {
1964 let is_nested = self.entry_dialog_stack.len() > 1;
1966
1967 let Some(dialog) = self.entry_dialog_stack.pop() else {
1968 return;
1969 };
1970
1971 let path = format!("{}/{}", dialog.map_path, dialog.entry_key);
1972
1973 if is_nested {
1975 let map_field = dialog.map_path.rsplit('/').next().unwrap_or("").to_string();
1978 let item_path = format!("/{}", map_field);
1979
1980 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1982 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1983 if let SettingControl::Map(map_state) = &mut item.control {
1984 if let Some(idx) = map_state
1985 .entries
1986 .iter()
1987 .position(|(k, _)| k == &dialog.entry_key)
1988 {
1989 map_state.remove_entry(idx);
1990 }
1991 }
1992 }
1993 }
1994 } else {
1995 if let Some(item) = self.current_item_mut() {
1997 if let SettingControl::Map(map_state) = &mut item.control {
1998 if let Some(idx) = map_state
1999 .entries
2000 .iter()
2001 .position(|(k, _)| k == &dialog.entry_key)
2002 {
2003 map_state.remove_entry(idx);
2004 }
2005 }
2006 }
2007 }
2008
2009 self.pending_changes.remove(&path);
2017 self.pending_deletions.insert(path);
2018 }
2019
2020 pub fn max_scroll(&self) -> u16 {
2022 self.scroll_panel.scroll.max_offset()
2023 }
2024
2025 pub fn scroll_up(&mut self, delta: usize) -> bool {
2028 let old = self.scroll_panel.scroll.offset;
2029 self.scroll_panel.scroll_up(delta as u16);
2030 let changed = old != self.scroll_panel.scroll.offset;
2031 if changed {
2032 self.sync_tree_cursor_to_body_scroll();
2033 }
2034 changed
2035 }
2036
2037 pub fn scroll_down(&mut self, delta: usize) -> bool {
2040 let old = self.scroll_panel.scroll.offset;
2041 self.scroll_panel.scroll_down(delta as u16);
2042 let changed = old != self.scroll_panel.scroll.offset;
2043 if changed {
2044 self.sync_tree_cursor_to_body_scroll();
2045 }
2046 changed
2047 }
2048
2049 pub fn scroll_to_ratio(&mut self, ratio: f32) -> bool {
2052 let old = self.scroll_panel.scroll.offset;
2053 self.scroll_panel.scroll_to_ratio(ratio);
2054 let changed = old != self.scroll_panel.scroll.offset;
2055 if changed {
2056 self.sync_tree_cursor_to_body_scroll();
2057 }
2058 changed
2059 }
2060
2061 pub(super) fn sync_tree_cursor_to_body_scroll(&mut self) {
2067 if let Some(section_idx) = self.current_section_index() {
2068 self.tree_cursor_section = Some(section_idx);
2069 }
2070 }
2075
2076 pub fn is_number_control(&self) -> bool {
2079 self.current_item()
2080 .is_some_and(|item| matches!(item.control, SettingControl::Number(_)))
2081 }
2082
2083 pub fn start_editing(&mut self) {
2084 if let Some(item) = self.current_item() {
2085 if matches!(
2086 item.control,
2087 SettingControl::TextList(_)
2088 | SettingControl::DualList(_)
2089 | SettingControl::Text(_)
2090 | SettingControl::Map(_)
2091 | SettingControl::Json(_)
2092 ) {
2093 self.editing_text = true;
2094 }
2095 }
2096 if let Some(item) = self.current_item_mut() {
2097 match item.control {
2098 SettingControl::DualList(ref mut dl) => {
2099 dl.editing = true;
2100 }
2101 SettingControl::Text(ref mut state) => {
2102 state.editing = true;
2103 state.arm_replace_on_type();
2108 }
2109 _ => {}
2110 }
2111 }
2112 }
2113
2114 pub fn stop_editing(&mut self) {
2116 self.editing_text = false;
2117 if let Some(item) = self.current_item_mut() {
2118 match item.control {
2119 SettingControl::DualList(ref mut dl) => {
2120 dl.editing = false;
2121 }
2122 SettingControl::Text(ref mut state) => {
2123 state.editing = false;
2124 }
2125 _ => {}
2126 }
2127 }
2128 }
2129
2130 pub fn is_editable_control(&self) -> bool {
2132 self.current_item().is_some_and(|item| {
2133 matches!(
2134 item.control,
2135 SettingControl::TextList(_)
2136 | SettingControl::DualList(_)
2137 | SettingControl::Text(_)
2138 | SettingControl::Map(_)
2139 | SettingControl::Json(_)
2140 )
2141 })
2142 }
2143
2144 pub fn is_editing_json(&self) -> bool {
2146 if !self.editing_text {
2147 return false;
2148 }
2149 self.current_item()
2150 .map(|item| matches!(&item.control, SettingControl::Json(_)))
2151 .unwrap_or(false)
2152 }
2153
2154 pub fn text_insert(&mut self, c: char) {
2156 if let Some(item) = self.current_item_mut() {
2157 match &mut item.control {
2158 SettingControl::TextList(state) => state.insert(c),
2159 SettingControl::Text(state) => state.insert(c),
2160 SettingControl::Map(state) => {
2161 state.new_key_text.insert(state.cursor, c);
2162 state.cursor += c.len_utf8();
2163 }
2164 SettingControl::Json(state) => state.insert(c),
2165 _ => {}
2166 }
2167 }
2168 }
2169
2170 pub fn text_backspace(&mut self) {
2172 if let Some(item) = self.current_item_mut() {
2173 match &mut item.control {
2174 SettingControl::TextList(state) => state.backspace(),
2175 SettingControl::Text(state) => state.backspace(),
2176 SettingControl::Map(state) => {
2177 if state.cursor > 0 {
2178 let mut char_start = state.cursor - 1;
2179 while char_start > 0 && !state.new_key_text.is_char_boundary(char_start) {
2180 char_start -= 1;
2181 }
2182 state.new_key_text.remove(char_start);
2183 state.cursor = char_start;
2184 }
2185 }
2186 SettingControl::Json(state) => state.backspace(),
2187 _ => {}
2188 }
2189 }
2190 }
2191
2192 pub fn text_move_left(&mut self) {
2194 if let Some(item) = self.current_item_mut() {
2195 match &mut item.control {
2196 SettingControl::TextList(state) => state.move_left(),
2197 SettingControl::Text(state) => state.move_left(),
2198 SettingControl::Map(state) => {
2199 if state.cursor > 0 {
2200 let mut new_pos = state.cursor - 1;
2201 while new_pos > 0 && !state.new_key_text.is_char_boundary(new_pos) {
2202 new_pos -= 1;
2203 }
2204 state.cursor = new_pos;
2205 }
2206 }
2207 SettingControl::Json(state) => state.move_left(),
2208 _ => {}
2209 }
2210 }
2211 }
2212
2213 pub fn text_move_right(&mut self) {
2215 if let Some(item) = self.current_item_mut() {
2216 match &mut item.control {
2217 SettingControl::TextList(state) => state.move_right(),
2218 SettingControl::Text(state) => state.move_right(),
2219 SettingControl::Map(state) => {
2220 if state.cursor < state.new_key_text.len() {
2221 let mut new_pos = state.cursor + 1;
2222 while new_pos < state.new_key_text.len()
2223 && !state.new_key_text.is_char_boundary(new_pos)
2224 {
2225 new_pos += 1;
2226 }
2227 state.cursor = new_pos;
2228 }
2229 }
2230 SettingControl::Json(state) => state.move_right(),
2231 _ => {}
2232 }
2233 }
2234 }
2235
2236 pub fn text_focus_prev(&mut self) {
2238 if let Some(item) = self.current_item_mut() {
2239 match &mut item.control {
2240 SettingControl::TextList(state) => state.focus_prev(),
2241 SettingControl::Map(state) => {
2242 state.focus_prev();
2243 }
2244 _ => {}
2245 }
2246 }
2247 }
2248
2249 pub fn text_focus_next(&mut self) {
2251 if let Some(item) = self.current_item_mut() {
2252 match &mut item.control {
2253 SettingControl::TextList(state) => state.focus_next(),
2254 SettingControl::Map(state) => {
2255 state.focus_next();
2256 }
2257 _ => {}
2258 }
2259 }
2260 }
2261
2262 pub fn text_add_item(&mut self) {
2264 if let Some(item) = self.current_item_mut() {
2265 match &mut item.control {
2266 SettingControl::TextList(state) => state.add_item(),
2267 SettingControl::Map(state) => state.add_entry_from_input(),
2268 _ => {}
2269 }
2270 }
2271 self.on_value_changed();
2273 }
2274
2275 pub fn text_remove_focused(&mut self) {
2277 if let Some(item) = self.current_item_mut() {
2278 match &mut item.control {
2279 SettingControl::TextList(state) => {
2280 if let Some(idx) = state.focused_item {
2281 state.remove_item(idx);
2282 }
2283 }
2284 SettingControl::Map(state) => {
2285 if let Some(idx) = state.focused_entry {
2286 state.remove_entry(idx);
2287 }
2288 }
2289 _ => {}
2290 }
2291 }
2292 self.on_value_changed();
2294 }
2295
2296 pub fn is_editing_dual_list(&self) -> bool {
2298 if !self.editing_text {
2299 return false;
2300 }
2301 self.current_item()
2302 .map(|item| matches!(&item.control, SettingControl::DualList(_)))
2303 .unwrap_or(false)
2304 }
2305
2306 pub fn with_dual_list_mut<R>(
2311 &mut self,
2312 item_idx: usize,
2313 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2314 ) -> Option<R> {
2315 let page = self.pages.get_mut(self.selected_category)?;
2316 let item = page.items.get_mut(item_idx)?;
2317 if let SettingControl::DualList(ref mut state) = item.control {
2318 Some(f(state))
2319 } else {
2320 None
2321 }
2322 }
2323
2324 pub fn with_current_dual_list_mut<R>(
2327 &mut self,
2328 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2329 ) -> Option<R> {
2330 if let Some(item) = self.current_item_mut() {
2331 if let SettingControl::DualList(ref mut state) = item.control {
2332 return Some(f(state));
2333 }
2334 }
2335 None
2336 }
2337
2338 pub fn refresh_dual_list_sibling(&mut self) {
2345 let (new_included, sibling_path) = {
2346 let Some(item) = self.current_item() else {
2347 return;
2348 };
2349 let SettingControl::DualList(state) = &item.control else {
2350 return;
2351 };
2352 let Some(ref sib_path) = item.dual_list_sibling else {
2353 return;
2354 };
2355 (state.included.clone(), sib_path.clone())
2356 };
2357
2358 if let Some(page) = self.pages.get_mut(self.selected_category) {
2360 for other in page.items.iter_mut() {
2361 if other.path == sibling_path {
2362 if let SettingControl::DualList(ref mut sib_state) = other.control {
2363 sib_state.excluded = new_included;
2364 }
2365 break;
2366 }
2367 }
2368 }
2369 }
2370
2371 pub fn json_cursor_up(&mut self) {
2375 if let Some(item) = self.current_item_mut() {
2376 if let SettingControl::Json(state) = &mut item.control {
2377 state.move_up();
2378 }
2379 }
2380 }
2381
2382 pub fn json_cursor_down(&mut self) {
2384 if let Some(item) = self.current_item_mut() {
2385 if let SettingControl::Json(state) = &mut item.control {
2386 state.move_down();
2387 }
2388 }
2389 }
2390
2391 pub fn json_insert_newline(&mut self) {
2393 if let Some(item) = self.current_item_mut() {
2394 if let SettingControl::Json(state) = &mut item.control {
2395 state.insert('\n');
2396 }
2397 }
2398 }
2399
2400 pub fn json_delete(&mut self) {
2402 if let Some(item) = self.current_item_mut() {
2403 if let SettingControl::Json(state) = &mut item.control {
2404 state.delete();
2405 }
2406 }
2407 }
2408
2409 pub fn json_exit_editing(&mut self) {
2411 let is_valid = self
2412 .current_item()
2413 .map(|item| {
2414 if let SettingControl::Json(state) = &item.control {
2415 state.is_valid()
2416 } else {
2417 true
2418 }
2419 })
2420 .unwrap_or(true);
2421
2422 if is_valid {
2423 if let Some(item) = self.current_item_mut() {
2424 if let SettingControl::Json(state) = &mut item.control {
2425 state.commit();
2426 }
2427 }
2428 self.on_value_changed();
2429 } else if let Some(item) = self.current_item_mut() {
2430 if let SettingControl::Json(state) = &mut item.control {
2431 state.revert();
2432 }
2433 }
2434 self.editing_text = false;
2435 }
2436
2437 pub fn json_select_all(&mut self) {
2439 if let Some(item) = self.current_item_mut() {
2440 if let SettingControl::Json(state) = &mut item.control {
2441 state.select_all();
2442 }
2443 }
2444 }
2445
2446 pub fn json_selected_text(&self) -> Option<String> {
2448 if let Some(item) = self.current_item() {
2449 if let SettingControl::Json(state) = &item.control {
2450 return state.selected_text();
2451 }
2452 }
2453 None
2454 }
2455
2456 pub fn json_cursor_up_selecting(&mut self) {
2458 if let Some(item) = self.current_item_mut() {
2459 if let SettingControl::Json(state) = &mut item.control {
2460 state.editor.move_up_selecting();
2461 }
2462 }
2463 }
2464
2465 pub fn json_cursor_down_selecting(&mut self) {
2467 if let Some(item) = self.current_item_mut() {
2468 if let SettingControl::Json(state) = &mut item.control {
2469 state.editor.move_down_selecting();
2470 }
2471 }
2472 }
2473
2474 pub fn json_cursor_left_selecting(&mut self) {
2476 if let Some(item) = self.current_item_mut() {
2477 if let SettingControl::Json(state) = &mut item.control {
2478 state.editor.move_left_selecting();
2479 }
2480 }
2481 }
2482
2483 pub fn json_cursor_right_selecting(&mut self) {
2485 if let Some(item) = self.current_item_mut() {
2486 if let SettingControl::Json(state) = &mut item.control {
2487 state.editor.move_right_selecting();
2488 }
2489 }
2490 }
2491
2492 pub fn is_dropdown_open(&self) -> bool {
2496 self.current_item().is_some_and(|item| {
2497 if let SettingControl::Dropdown(ref d) = item.control {
2498 d.open
2499 } else {
2500 false
2501 }
2502 })
2503 }
2504
2505 pub fn dropdown_toggle(&mut self) {
2507 let mut opened = false;
2508 if let Some(item) = self.current_item_mut() {
2509 if let SettingControl::Dropdown(ref mut d) = item.control {
2510 d.toggle_open();
2511 opened = d.open;
2512 }
2513 }
2514
2515 if opened {
2517 let selected_item = self.selected_item;
2519 let width = self.layout_width;
2520 if let Some(page) = self.pages.get(self.selected_category) {
2521 self.scroll_panel.update_content_height(&page.items, width);
2522 self.scroll_panel
2524 .ensure_focused_visible(&page.items, selected_item, None, width);
2525 }
2526 }
2527 }
2528
2529 pub fn dropdown_prev(&mut self) {
2531 if let Some(item) = self.current_item_mut() {
2532 if let SettingControl::Dropdown(ref mut d) = item.control {
2533 d.select_prev();
2534 }
2535 }
2536 }
2537
2538 pub fn dropdown_next(&mut self) {
2540 if let Some(item) = self.current_item_mut() {
2541 if let SettingControl::Dropdown(ref mut d) = item.control {
2542 d.select_next();
2543 }
2544 }
2545 }
2546
2547 pub fn dropdown_home(&mut self) {
2549 if let Some(item) = self.current_item_mut() {
2550 if let SettingControl::Dropdown(ref mut d) = item.control {
2551 if !d.options.is_empty() {
2552 d.selected = 0;
2553 d.ensure_visible();
2554 }
2555 }
2556 }
2557 }
2558
2559 pub fn dropdown_end(&mut self) {
2561 if let Some(item) = self.current_item_mut() {
2562 if let SettingControl::Dropdown(ref mut d) = item.control {
2563 if !d.options.is_empty() {
2564 d.selected = d.options.len() - 1;
2565 d.ensure_visible();
2566 }
2567 }
2568 }
2569 }
2570
2571 pub fn dropdown_confirm(&mut self) {
2573 if let Some(item) = self.current_item_mut() {
2574 if let SettingControl::Dropdown(ref mut d) = item.control {
2575 d.confirm();
2576 }
2577 }
2578 self.on_value_changed();
2579 }
2580
2581 pub fn dropdown_cancel(&mut self) {
2583 if let Some(item) = self.current_item_mut() {
2584 if let SettingControl::Dropdown(ref mut d) = item.control {
2585 d.cancel();
2586 }
2587 }
2588 }
2589
2590 pub fn dropdown_select(&mut self, option_idx: usize) {
2592 if let Some(item) = self.current_item_mut() {
2593 if let SettingControl::Dropdown(ref mut d) = item.control {
2594 if option_idx < d.options.len() {
2595 d.selected = option_idx;
2596 d.confirm();
2597 }
2598 }
2599 }
2600 self.on_value_changed();
2601 }
2602
2603 pub fn set_dropdown_hover(&mut self, hover_idx: Option<usize>) -> bool {
2606 if let Some(item) = self.current_item_mut() {
2607 if let SettingControl::Dropdown(ref mut d) = item.control {
2608 if d.open && d.hover_index != hover_idx {
2609 d.hover_index = hover_idx;
2610 return true;
2611 }
2612 }
2613 }
2614 false
2615 }
2616
2617 pub fn dropdown_scroll(&mut self, delta: i32) {
2619 if let Some(item) = self.current_item_mut() {
2620 if let SettingControl::Dropdown(ref mut d) = item.control {
2621 if d.open {
2622 d.scroll_by(delta);
2623 }
2624 }
2625 }
2626 }
2627
2628 pub fn is_number_editing(&self) -> bool {
2632 self.current_item().is_some_and(|item| {
2633 if let SettingControl::Number(ref n) = item.control {
2634 n.editing()
2635 } else {
2636 false
2637 }
2638 })
2639 }
2640
2641 pub fn start_number_editing(&mut self) {
2643 if let Some(item) = self.current_item_mut() {
2644 if let SettingControl::Number(ref mut n) = item.control {
2645 n.start_editing();
2646 }
2647 }
2648 }
2649
2650 pub fn number_insert(&mut self, c: char) {
2652 if let Some(item) = self.current_item_mut() {
2653 if let SettingControl::Number(ref mut n) = item.control {
2654 n.insert_char(c);
2655 }
2656 }
2657 }
2658
2659 pub fn number_backspace(&mut self) {
2661 if let Some(item) = self.current_item_mut() {
2662 if let SettingControl::Number(ref mut n) = item.control {
2663 n.backspace();
2664 }
2665 }
2666 }
2667
2668 pub fn number_confirm(&mut self) {
2670 if let Some(item) = self.current_item_mut() {
2671 if let SettingControl::Number(ref mut n) = item.control {
2672 n.confirm_editing();
2673 }
2674 }
2675 self.on_value_changed();
2676 }
2677
2678 pub fn number_cancel(&mut self) {
2680 if let Some(item) = self.current_item_mut() {
2681 if let SettingControl::Number(ref mut n) = item.control {
2682 n.cancel_editing();
2683 }
2684 }
2685 }
2686
2687 pub fn number_delete(&mut self) {
2689 if let Some(item) = self.current_item_mut() {
2690 if let SettingControl::Number(ref mut n) = item.control {
2691 n.delete();
2692 }
2693 }
2694 }
2695
2696 pub fn number_move_left(&mut self) {
2698 if let Some(item) = self.current_item_mut() {
2699 if let SettingControl::Number(ref mut n) = item.control {
2700 n.move_left();
2701 }
2702 }
2703 }
2704
2705 pub fn number_move_right(&mut self) {
2707 if let Some(item) = self.current_item_mut() {
2708 if let SettingControl::Number(ref mut n) = item.control {
2709 n.move_right();
2710 }
2711 }
2712 }
2713
2714 pub fn number_move_home(&mut self) {
2716 if let Some(item) = self.current_item_mut() {
2717 if let SettingControl::Number(ref mut n) = item.control {
2718 n.move_home();
2719 }
2720 }
2721 }
2722
2723 pub fn number_move_end(&mut self) {
2725 if let Some(item) = self.current_item_mut() {
2726 if let SettingControl::Number(ref mut n) = item.control {
2727 n.move_end();
2728 }
2729 }
2730 }
2731
2732 pub fn number_move_left_selecting(&mut self) {
2734 if let Some(item) = self.current_item_mut() {
2735 if let SettingControl::Number(ref mut n) = item.control {
2736 n.move_left_selecting();
2737 }
2738 }
2739 }
2740
2741 pub fn number_move_right_selecting(&mut self) {
2743 if let Some(item) = self.current_item_mut() {
2744 if let SettingControl::Number(ref mut n) = item.control {
2745 n.move_right_selecting();
2746 }
2747 }
2748 }
2749
2750 pub fn number_move_home_selecting(&mut self) {
2752 if let Some(item) = self.current_item_mut() {
2753 if let SettingControl::Number(ref mut n) = item.control {
2754 n.move_home_selecting();
2755 }
2756 }
2757 }
2758
2759 pub fn number_move_end_selecting(&mut self) {
2761 if let Some(item) = self.current_item_mut() {
2762 if let SettingControl::Number(ref mut n) = item.control {
2763 n.move_end_selecting();
2764 }
2765 }
2766 }
2767
2768 pub fn number_move_word_left(&mut self) {
2770 if let Some(item) = self.current_item_mut() {
2771 if let SettingControl::Number(ref mut n) = item.control {
2772 n.move_word_left();
2773 }
2774 }
2775 }
2776
2777 pub fn number_move_word_right(&mut self) {
2779 if let Some(item) = self.current_item_mut() {
2780 if let SettingControl::Number(ref mut n) = item.control {
2781 n.move_word_right();
2782 }
2783 }
2784 }
2785
2786 pub fn number_move_word_left_selecting(&mut self) {
2788 if let Some(item) = self.current_item_mut() {
2789 if let SettingControl::Number(ref mut n) = item.control {
2790 n.move_word_left_selecting();
2791 }
2792 }
2793 }
2794
2795 pub fn number_move_word_right_selecting(&mut self) {
2797 if let Some(item) = self.current_item_mut() {
2798 if let SettingControl::Number(ref mut n) = item.control {
2799 n.move_word_right_selecting();
2800 }
2801 }
2802 }
2803
2804 pub fn number_select_all(&mut self) {
2806 if let Some(item) = self.current_item_mut() {
2807 if let SettingControl::Number(ref mut n) = item.control {
2808 n.select_all();
2809 }
2810 }
2811 }
2812
2813 pub fn number_delete_word_backward(&mut self) {
2815 if let Some(item) = self.current_item_mut() {
2816 if let SettingControl::Number(ref mut n) = item.control {
2817 n.delete_word_backward();
2818 }
2819 }
2820 }
2821
2822 pub fn number_delete_word_forward(&mut self) {
2824 if let Some(item) = self.current_item_mut() {
2825 if let SettingControl::Number(ref mut n) = item.control {
2826 n.delete_word_forward();
2827 }
2828 }
2829 }
2830
2831 pub fn get_change_descriptions(&self) -> Vec<String> {
2833 let mut descriptions: Vec<String> = self
2834 .pending_changes
2835 .iter()
2836 .map(|(path, value)| {
2837 let value_str = match value {
2838 serde_json::Value::Bool(b) => b.to_string(),
2839 serde_json::Value::Number(n) => n.to_string(),
2840 serde_json::Value::String(s) => format!("\"{}\"", s),
2841 _ => value.to_string(),
2842 };
2843 format!("{}: {}", path, value_str)
2844 })
2845 .collect();
2846 for path in &self.pending_deletions {
2848 descriptions.push(format!("{}: (reset to default)", path));
2849 }
2850 descriptions.sort();
2851 descriptions
2852 }
2853}
2854
2855fn update_control_from_value(control: &mut SettingControl, value: &serde_json::Value) {
2857 match control {
2858 SettingControl::Toggle(state) => {
2859 if let Some(b) = value.as_bool() {
2860 state.checked = b;
2861 }
2862 }
2863 SettingControl::Number(state) => {
2864 if let Some(n) = value.as_i64() {
2865 state.value = n;
2866 }
2867 }
2868 SettingControl::Dropdown(state) => {
2869 if let Some(s) = value.as_str() {
2870 if let Some(idx) = state.options.iter().position(|o| o == s) {
2871 state.selected = idx;
2872 }
2873 }
2874 }
2875 SettingControl::Text(state) => {
2876 if let Some(s) = value.as_str() {
2877 state.value = s.to_string();
2878 state.cursor = state.value.len();
2879 }
2880 }
2881 SettingControl::TextList(state) => {
2882 if let Some(arr) = value.as_array() {
2883 state.items = arr
2884 .iter()
2885 .filter_map(|v| {
2886 if state.is_integer {
2887 v.as_i64()
2888 .map(|n| n.to_string())
2889 .or_else(|| v.as_u64().map(|n| n.to_string()))
2890 .or_else(|| v.as_f64().map(|n| n.to_string()))
2891 } else {
2892 v.as_str().map(String::from)
2893 }
2894 })
2895 .collect();
2896 }
2897 }
2898 SettingControl::DualList(state) => {
2899 if let Some(arr) = value.as_array() {
2900 state.included = arr
2901 .iter()
2902 .filter_map(|v| v.as_str().map(String::from))
2903 .collect();
2904 }
2905 }
2906 SettingControl::Map(state) => {
2907 if let Some(obj) = value.as_object() {
2908 state.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
2909 state.entries.sort_by(|a, b| a.0.cmp(&b.0));
2910 }
2911 }
2912 SettingControl::ObjectArray(state) => {
2913 if let Some(arr) = value.as_array() {
2914 state.bindings = arr.clone();
2915 }
2916 }
2917 SettingControl::Json(state) => {
2918 let json_str =
2920 serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_string());
2921 let json_str = if json_str.is_empty() {
2922 "null".to_string()
2923 } else {
2924 json_str
2925 };
2926 state.original_text = json_str.clone();
2927 state.editor.set_value(&json_str);
2928 state.scroll_offset = 0;
2929 }
2930 SettingControl::Complex { .. } => {}
2931 }
2932}
2933
2934#[cfg(test)]
2935mod tests {
2936 use super::*;
2937
2938 const TEST_SCHEMA: &str = r#"
2939{
2940 "type": "object",
2941 "properties": {
2942 "theme": {
2943 "type": "string",
2944 "default": "dark"
2945 },
2946 "line_numbers": {
2947 "type": "boolean",
2948 "default": true
2949 }
2950 },
2951 "$defs": {}
2952}
2953"#;
2954
2955 fn test_config() -> Config {
2956 Config::default()
2957 }
2958
2959 #[test]
2960 fn test_settings_state_creation() {
2961 let config = test_config();
2962 let state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2963
2964 assert!(!state.visible);
2965 assert_eq!(state.selected_category, 0);
2966 assert!(!state.has_changes());
2967 }
2968
2969 #[test]
2970 fn test_navigation() {
2971 let config = test_config();
2972 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2973
2974 assert_eq!(state.focus_panel(), FocusPanel::Categories);
2976
2977 state.toggle_focus();
2979 assert_eq!(state.focus_panel(), FocusPanel::Settings);
2980
2981 state.select_next();
2983 assert_eq!(state.selected_item, 1);
2984
2985 state.select_prev();
2986 assert_eq!(state.selected_item, 0);
2987 }
2988
2989 #[test]
2990 fn test_pending_changes() {
2991 let config = test_config();
2992 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2993
2994 assert!(!state.has_changes());
2995
2996 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
2997 assert!(state.has_changes());
2998
2999 state.discard_changes();
3000 assert!(!state.has_changes());
3001 }
3002
3003 #[test]
3004 fn test_show_hide() {
3005 let config = test_config();
3006 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3007
3008 assert!(!state.visible);
3009
3010 state.show();
3011 assert!(state.visible);
3012 assert_eq!(state.focus_panel(), FocusPanel::Categories);
3013
3014 state.hide();
3015 assert!(!state.visible);
3016 }
3017
3018 const TEST_SCHEMA_CONTROLS: &str = r#"
3020{
3021 "type": "object",
3022 "properties": {
3023 "theme": {
3024 "type": "string",
3025 "enum": ["dark", "light", "high-contrast"],
3026 "default": "dark"
3027 },
3028 "tab_size": {
3029 "type": "integer",
3030 "minimum": 1,
3031 "maximum": 8,
3032 "default": 4
3033 },
3034 "line_numbers": {
3035 "type": "boolean",
3036 "default": true
3037 }
3038 },
3039 "$defs": {}
3040}
3041"#;
3042
3043 #[test]
3044 fn test_dropdown_toggle() {
3045 let config = test_config();
3046 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3047 state.show();
3048 state.toggle_focus(); state.select_next();
3053 state.select_next();
3054 assert!(!state.is_dropdown_open());
3055
3056 state.dropdown_toggle();
3057 assert!(state.is_dropdown_open());
3058
3059 state.dropdown_toggle();
3060 assert!(!state.is_dropdown_open());
3061 }
3062
3063 #[test]
3064 fn test_dropdown_cancel_restores() {
3065 let config = test_config();
3066 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3067 state.show();
3068 state.toggle_focus();
3069
3070 state.select_next();
3073 state.select_next();
3074
3075 state.dropdown_toggle();
3077 assert!(state.is_dropdown_open());
3078
3079 let initial = state.current_item().and_then(|item| {
3081 if let SettingControl::Dropdown(ref d) = item.control {
3082 Some(d.selected)
3083 } else {
3084 None
3085 }
3086 });
3087
3088 state.dropdown_next();
3090 let after_change = state.current_item().and_then(|item| {
3091 if let SettingControl::Dropdown(ref d) = item.control {
3092 Some(d.selected)
3093 } else {
3094 None
3095 }
3096 });
3097 assert_ne!(initial, after_change);
3098
3099 state.dropdown_cancel();
3101 assert!(!state.is_dropdown_open());
3102
3103 let after_cancel = state.current_item().and_then(|item| {
3104 if let SettingControl::Dropdown(ref d) = item.control {
3105 Some(d.selected)
3106 } else {
3107 None
3108 }
3109 });
3110 assert_eq!(initial, after_cancel);
3111 }
3112
3113 #[test]
3114 fn test_dropdown_confirm_keeps_selection() {
3115 let config = test_config();
3116 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3117 state.show();
3118 state.toggle_focus();
3119
3120 state.dropdown_toggle();
3122
3123 state.dropdown_next();
3125 let after_change = state.current_item().and_then(|item| {
3126 if let SettingControl::Dropdown(ref d) = item.control {
3127 Some(d.selected)
3128 } else {
3129 None
3130 }
3131 });
3132
3133 state.dropdown_confirm();
3135 assert!(!state.is_dropdown_open());
3136
3137 let after_confirm = state.current_item().and_then(|item| {
3138 if let SettingControl::Dropdown(ref d) = item.control {
3139 Some(d.selected)
3140 } else {
3141 None
3142 }
3143 });
3144 assert_eq!(after_change, after_confirm);
3145 }
3146
3147 #[test]
3148 fn test_number_editing() {
3149 let config = test_config();
3150 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3151 state.show();
3152 state.toggle_focus();
3153
3154 state.select_next();
3156
3157 assert!(!state.is_number_editing());
3159
3160 state.start_number_editing();
3162 assert!(state.is_number_editing());
3163
3164 state.number_insert('8');
3166
3167 state.number_confirm();
3169 assert!(!state.is_number_editing());
3170 }
3171
3172 #[test]
3173 fn test_number_cancel_editing() {
3174 let config = test_config();
3175 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3176 state.show();
3177 state.toggle_focus();
3178
3179 state.select_next();
3181
3182 let initial_value = state.current_item().and_then(|item| {
3184 if let SettingControl::Number(ref n) = item.control {
3185 Some(n.value)
3186 } else {
3187 None
3188 }
3189 });
3190
3191 state.start_number_editing();
3193 state.number_backspace();
3194 state.number_insert('9');
3195 state.number_insert('9');
3196
3197 state.number_cancel();
3199 assert!(!state.is_number_editing());
3200
3201 let after_cancel = state.current_item().and_then(|item| {
3203 if let SettingControl::Number(ref n) = item.control {
3204 Some(n.value)
3205 } else {
3206 None
3207 }
3208 });
3209 assert_eq!(initial_value, after_cancel);
3210 }
3211
3212 #[test]
3213 fn test_number_backspace() {
3214 let config = test_config();
3215 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3216 state.show();
3217 state.toggle_focus();
3218 state.select_next();
3219
3220 state.start_number_editing();
3221 state.number_backspace();
3222
3223 let display_text = state.current_item().and_then(|item| {
3225 if let SettingControl::Number(ref n) = item.control {
3226 Some(n.display_text())
3227 } else {
3228 None
3229 }
3230 });
3231 assert_eq!(display_text, Some(String::new()));
3233
3234 state.number_cancel();
3235 }
3236
3237 #[test]
3238 fn test_layer_selection() {
3239 let config = test_config();
3240 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3241
3242 assert_eq!(state.target_layer, ConfigLayer::User);
3244 assert_eq!(state.target_layer_name(), "User");
3245
3246 state.cycle_target_layer();
3248 assert_eq!(state.target_layer, ConfigLayer::Project);
3249 assert_eq!(state.target_layer_name(), "Project");
3250
3251 state.cycle_target_layer();
3252 assert_eq!(state.target_layer, ConfigLayer::Session);
3253 assert_eq!(state.target_layer_name(), "Session");
3254
3255 state.cycle_target_layer();
3256 assert_eq!(state.target_layer, ConfigLayer::User);
3257
3258 state.set_target_layer(ConfigLayer::Project);
3260 assert_eq!(state.target_layer, ConfigLayer::Project);
3261
3262 state.set_target_layer(ConfigLayer::System);
3264 assert_eq!(state.target_layer, ConfigLayer::Project);
3265 }
3266
3267 #[test]
3268 fn test_layer_switch_clears_pending_changes() {
3269 let config = test_config();
3270 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3271
3272 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
3274 assert!(state.has_changes());
3275
3276 state.cycle_target_layer();
3278 assert!(!state.has_changes());
3279 }
3280
3281 #[test]
3300 fn nested_array_save_records_full_entry_path() {
3301 use crate::view::settings::schema::SettingType;
3304
3305 let config = test_config();
3306 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3307
3308 let item_schema = SettingSchema {
3310 path: "/item".to_string(),
3311 name: "Server".to_string(),
3312 description: None,
3313 setting_type: SettingType::Object {
3314 properties: vec![SettingSchema {
3315 path: "/enabled".to_string(),
3316 name: "Enabled".to_string(),
3317 description: None,
3318 setting_type: SettingType::Boolean,
3319 default: Some(serde_json::json!(false)),
3320 read_only: false,
3321 section: None,
3322 order: None,
3323 nullable: false,
3324 enum_from: None,
3325 dual_list_sibling: None,
3326 dynamically_extendable_status_bar_elements: false,
3327 }],
3328 },
3329 default: None,
3330 read_only: false,
3331 section: None,
3332 order: None,
3333 nullable: false,
3334 enum_from: None,
3335 dual_list_sibling: None,
3336 dynamically_extendable_status_bar_elements: false,
3337 };
3338
3339 let value_schema = SettingSchema {
3344 path: String::new(),
3345 name: "value".to_string(),
3346 description: None,
3347 setting_type: SettingType::ObjectArray {
3348 item_schema: Box::new(item_schema.clone()),
3349 display_field: None,
3350 },
3351 default: None,
3352 read_only: false,
3353 section: None,
3354 order: None,
3355 nullable: false,
3356 enum_from: None,
3357 dual_list_sibling: None,
3358 dynamically_extendable_status_bar_elements: false,
3359 };
3360
3361 let parent = EntryDialogState::from_schema(
3365 "quicklsp".to_string(),
3366 &serde_json::json!([{ "enabled": true }]),
3367 &value_schema,
3368 "/universal_lsp",
3369 false, false,
3371 &HashMap::new(),
3372 );
3373
3374 assert!(
3376 parent.is_single_value,
3377 "array value_schema should trigger is_single_value path"
3378 );
3379 assert_eq!(parent.entry_path(), "/universal_lsp/quicklsp");
3380
3381 state.entry_dialog_stack.push(parent);
3382
3383 state.open_nested_entry_dialog();
3388
3389 assert_eq!(
3391 state.entry_dialog_stack.len(),
3392 2,
3393 "open_nested_entry_dialog should have pushed a nested dialog"
3394 );
3395
3396 let nested_map_path = state
3399 .entry_dialog_stack
3400 .last()
3401 .map(|d| d.map_path.clone())
3402 .unwrap();
3403 assert_eq!(
3404 nested_map_path, "/universal_lsp/quicklsp",
3405 "BUG: nested dialog's map_path dropped the 'quicklsp' key segment"
3406 );
3407
3408 state.save_entry_dialog();
3410
3411 assert_eq!(state.entry_dialog_stack.len(), 1);
3413
3414 assert!(
3417 !state.pending_changes.contains_key("/universal_lsp/"),
3418 "regression: pending change recorded under empty-key path /universal_lsp/. \
3419 All keys: {:?}",
3420 state.pending_changes.keys().collect::<Vec<_>>()
3421 );
3422 assert!(
3423 !state
3424 .pending_changes
3425 .keys()
3426 .any(|k| k.starts_with("/universal_lsp") && k.ends_with('/')),
3427 "no /universal_lsp/* path should end in a trailing slash; got {:?}",
3428 state.pending_changes.keys().collect::<Vec<_>>()
3429 );
3430 assert!(
3431 state
3432 .pending_changes
3433 .contains_key("/universal_lsp/quicklsp"),
3434 "expected pending change at /universal_lsp/quicklsp, got {:?}",
3435 state.pending_changes.keys().collect::<Vec<_>>()
3436 );
3437 }
3438
3439 #[test]
3440 fn test_refresh_dual_list_sibling_updates_excluded() {
3441 use crate::view::controls::DualListState;
3442
3443 let schema = include_str!("../../../plugins/config-schema.json");
3446 let config = test_config();
3447 let mut state = SettingsState::new(schema, &config).unwrap();
3448
3449 let editor_page_idx = state
3451 .pages
3452 .iter()
3453 .position(|p| p.path == "/editor")
3454 .expect("editor page");
3455 state.selected_category = editor_page_idx;
3456
3457 let (left_idx, right_idx) = {
3458 let page = &state.pages[editor_page_idx];
3459 let l = page
3460 .items
3461 .iter()
3462 .position(|i| i.path == "/editor/status_bar/left")
3463 .expect("left item");
3464 let r = page
3465 .items
3466 .iter()
3467 .position(|i| i.path == "/editor/status_bar/right")
3468 .expect("right item");
3469 (l, r)
3470 };
3471
3472 assert!(matches!(
3474 &state.pages[editor_page_idx].items[left_idx].control,
3475 SettingControl::DualList(_)
3476 ));
3477
3478 let default_right_items: Vec<String> =
3480 match &state.pages[editor_page_idx].items[right_idx].control {
3481 SettingControl::DualList(dl) => dl.included.clone(),
3482 _ => panic!("right should be DualList"),
3483 };
3484 let initial_left_excluded: Vec<String> =
3485 match &state.pages[editor_page_idx].items[left_idx].control {
3486 SettingControl::DualList(dl) => dl.excluded.clone(),
3487 _ => panic!("left should be DualList"),
3488 };
3489 assert_eq!(
3490 initial_left_excluded, default_right_items,
3491 "left.excluded should mirror right's included on initial build"
3492 );
3493
3494 let new_element = "{chord}".to_string();
3496 state.selected_item = left_idx;
3497 state
3498 .with_current_dual_list_mut(|dl: &mut DualListState| {
3499 if !dl.included.contains(&new_element) {
3500 dl.included.push(new_element.clone());
3501 }
3502 })
3503 .expect("current item is a DualList");
3504
3505 state.refresh_dual_list_sibling();
3507
3508 match &state.pages[editor_page_idx].items[right_idx].control {
3509 SettingControl::DualList(dl) => {
3510 assert!(
3511 dl.excluded.contains(&new_element),
3512 "right.excluded should be updated to reflect left's new inclusion"
3513 );
3514 }
3515 _ => panic!("right should be DualList"),
3516 }
3517 }
3518
3519 #[test]
3520 fn test_with_dual_list_mut_returns_none_for_non_dual_list() {
3521 let config = test_config();
3522 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3523
3524 let result = state.with_dual_list_mut(0, |_| ());
3526 assert!(result.is_none());
3527 }
3528}