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 showing_help: bool,
142 pub scroll_panel: ScrollablePanel,
144 pub sub_focus: Option<usize>,
146 pub editing_text: bool,
148 pub hover_position: Option<(u16, u16)>,
150 pub hover_hit: Option<SettingsHit>,
152 pub entry_dialog_stack: Vec<EntryDialogState>,
155 pub target_layer: ConfigLayer,
159 available_status_bar_tokens: HashMap<String, String>,
162 pub layer_sources: HashMap<String, ConfigLayer>,
166 pub pending_deletions: std::collections::HashSet<String>,
170 pub layout_width: u16,
174 pub item_style: super::items::ItemBoxStyle,
177 pub expanded_categories: std::collections::HashSet<usize>,
181 pub categories_scroll: ScrollablePanel,
184 pub tree_cursor_section: Option<usize>,
196}
197
198#[derive(Debug, Clone, Copy)]
205pub enum TreeRow {
206 Category {
207 idx: usize,
208 expandable: bool,
209 expanded: bool,
210 },
211 Section {
212 cat_idx: usize,
213 section_idx: usize,
214 },
215}
216
217impl crate::view::ui::ScrollItem for TreeRow {
218 fn height(&self, _width: u16) -> u16 {
219 1
220 }
221}
222
223impl SettingsState {
224 pub fn new(schema_json: &str, config: &Config) -> Result<Self, serde_json::Error> {
226 Self::new_with_plugin_schemas(schema_json, config, &HashMap::new())
227 }
228
229 pub fn new_with_plugin_schemas(
233 schema_json: &str,
234 config: &Config,
235 plugin_schemas: &HashMap<String, serde_json::Value>,
236 ) -> Result<Self, serde_json::Error> {
237 let mut categories = parse_schema(schema_json)?;
238
239 let mut enabled_with_schema: Vec<String> = config
241 .plugins
242 .iter()
243 .filter_map(|(name, cfg)| {
244 if cfg.enabled && plugin_schemas.contains_key(name) {
245 Some(name.clone())
246 } else {
247 None
248 }
249 })
250 .collect();
251 enabled_with_schema.sort();
252 tracing::trace!(
253 "SettingsState built: total plugin_schemas={}, enabled_with_schema={:?}",
254 plugin_schemas.len(),
255 enabled_with_schema
256 );
257 super::schema::append_plugin_settings_category(
258 &mut categories,
259 plugin_schemas,
260 &enabled_with_schema,
261 );
262
263 let config_value = serde_json::to_value(config)?;
264 let layer_sources = HashMap::new(); let target_layer = ConfigLayer::User; let available_status_bar_tokens: HashMap<String, String> = HashMap::new();
267 let pages = super::items::build_pages(
268 &categories,
269 &config_value,
270 &layer_sources,
271 target_layer,
272 &available_status_bar_tokens,
273 );
274
275 Ok(Self {
276 categories,
277 pages,
278 selected_category: 0,
279 selected_item: 0,
280 focus: FocusManager::new(vec![
281 FocusPanel::Categories,
282 FocusPanel::Settings,
283 FocusPanel::Footer,
284 ]),
285 footer_button_index: 2, pending_changes: HashMap::new(),
287 original_config: config_value,
288 visible: false,
289 search_query: String::new(),
290 search_active: false,
291 search_results: Vec::new(),
292 selected_search_result: 0,
293 search_scroll_offset: 0,
294 search_max_visible: 5, showing_confirm_dialog: false,
296 confirm_dialog_selection: 0,
297 confirm_dialog_hover: None,
298 showing_reset_dialog: false,
299 reset_dialog_selection: 0,
300 reset_dialog_hover: None,
301 showing_entry_discard_confirm: false,
302 entry_discard_confirm_selection: 0,
303 showing_entry_delete_confirm: false,
304 entry_delete_confirm_selection: 0,
305 entry_delete_target_name: String::new(),
306 showing_help: false,
307 scroll_panel: ScrollablePanel::new(),
308 sub_focus: None,
309 editing_text: false,
310 available_status_bar_tokens,
311 hover_position: None,
312 hover_hit: None,
313 entry_dialog_stack: Vec::new(),
314 target_layer,
315 layer_sources,
316 pending_deletions: std::collections::HashSet::new(),
317 layout_width: 0,
318 item_style: super::items::ItemBoxStyle::default(),
319 expanded_categories: std::collections::HashSet::new(),
320 categories_scroll: ScrollablePanel::new(),
321 tree_cursor_section: None,
322 })
323 }
324
325 #[inline]
327 pub fn focus_panel(&self) -> FocusPanel {
328 self.focus.current().unwrap_or_default()
329 }
330
331 pub fn show(&mut self) {
333 self.visible = true;
334 self.focus.set(FocusPanel::Categories);
335 self.footer_button_index = 2; self.selected_category = 0;
337 self.selected_item = 0;
338 self.scroll_panel = ScrollablePanel::new();
339 self.sub_focus = None;
340 self.showing_confirm_dialog = false;
342 self.confirm_dialog_selection = 0;
343 self.confirm_dialog_hover = None;
344 self.showing_reset_dialog = false;
345 self.reset_dialog_selection = 0;
346 self.reset_dialog_hover = None;
347 self.showing_help = false;
348 }
349
350 fn rebuild_pages(&mut self) {
352 self.pages = super::items::build_pages(
353 &self.categories,
354 &self.original_config,
355 &self.layer_sources,
356 self.target_layer,
357 &self.available_status_bar_tokens,
358 );
359 }
360
361 pub fn hide(&mut self) {
363 self.visible = false;
364 self.search_active = false;
365 self.search_query.clear();
366 }
367
368 pub fn entry_dialog(&self) -> Option<&EntryDialogState> {
370 self.entry_dialog_stack.last()
371 }
372
373 pub fn entry_dialog_mut(&mut self) -> Option<&mut EntryDialogState> {
375 self.entry_dialog_stack.last_mut()
376 }
377
378 pub fn has_entry_dialog(&self) -> bool {
380 !self.entry_dialog_stack.is_empty()
381 }
382
383 pub fn current_page(&self) -> Option<&SettingsPage> {
385 self.pages.get(self.selected_category)
386 }
387
388 pub fn current_page_mut(&mut self) -> Option<&mut SettingsPage> {
390 self.pages.get_mut(self.selected_category)
391 }
392
393 pub fn topmost_visible_item_index(&self) -> Option<usize> {
398 let page = self.pages.get(self.selected_category)?;
399 if page.items.is_empty() {
400 return None;
401 }
402 let target = self.scroll_panel.scroll.offset;
403 let width = self.layout_width;
404 let mut y: u16 = 0;
405 for (idx, item) in page.items.iter().enumerate() {
406 let h = <SettingItem as ScrollItem>::height(item, width);
407 if y + h > target {
408 return Some(idx);
409 }
410 y += h;
411 }
412 Some(page.items.len() - 1)
413 }
414
415 pub fn current_section_index(&self) -> Option<usize> {
420 let page = self.pages.get(self.selected_category)?;
421 if page.sections.is_empty() {
422 return None;
423 }
424 let item_idx = self
433 .topmost_visible_item_index()
434 .unwrap_or(self.selected_item);
435 let mut current: Option<usize> = None;
437 for (s_idx, section) in page.sections.iter().enumerate() {
438 if section.first_item_index <= item_idx {
439 current = Some(s_idx);
440 } else {
441 break;
442 }
443 }
444 current
445 }
446
447 pub fn is_category_expandable(&self, cat_idx: usize) -> bool {
451 self.pages
452 .get(cat_idx)
453 .is_some_and(|p| p.sections.len() > 1)
454 }
455
456 pub fn tree_step(&mut self, delta: i32) {
466 let rows = self.visible_tree();
467 if rows.is_empty() {
468 return;
469 }
470 let cur = self.tree_cursor_index(&rows);
471 let len = rows.len() as i32;
472 let target = (cur as i32 + delta).clamp(0, len - 1) as usize;
473 if target == cur {
474 return;
475 }
476 let prev_category = self.selected_category;
477 self.update_control_focus(false);
478 match rows[target] {
479 TreeRow::Category { idx, .. } => {
480 self.selected_category = idx;
484 self.selected_item = 0;
485 self.tree_cursor_section = None;
486 if idx != prev_category {
487 self.scroll_panel = ScrollablePanel::new();
488 }
489 self.sub_focus = None;
490 self.update_control_focus(true);
491 }
492 TreeRow::Section {
493 cat_idx,
494 section_idx,
495 } => {
496 let first = self.pages[cat_idx].sections[section_idx].first_item_index;
497 self.selected_category = cat_idx;
498 self.selected_item = first;
499 self.tree_cursor_section = Some(section_idx);
500 if cat_idx != prev_category {
501 self.scroll_panel = ScrollablePanel::new();
502 }
503 self.sub_focus = None;
504 self.init_map_focus(true);
505 self.update_control_focus(true);
506 }
507 }
508 let width = self.layout_width;
514 if let Some(page) = self.pages.get(self.selected_category) {
515 self.scroll_panel.update_content_height(&page.items, width);
516 if matches!(rows[target], TreeRow::Section { .. }) {
524 let item_y =
525 self.scroll_panel
526 .item_y_offset(&page.items, self.selected_item, width);
527 self.scroll_panel.scroll.offset = item_y;
528 } else {
529 let selected_item = self.selected_item;
530 let sub_focus = self.sub_focus;
531 self.scroll_panel.ensure_focused_visible(
532 &page.items,
533 selected_item,
534 sub_focus,
535 width,
536 );
537 }
538 }
539 let new_rows = self.visible_tree();
540 let new_cur = self.tree_cursor_index(&new_rows);
541 self.categories_scroll
542 .ensure_focused_visible(&new_rows, new_cur, None, width);
543 }
544
545 pub(super) fn tree_cursor_index(&self, rows: &[TreeRow]) -> usize {
553 let cat = self.selected_category;
554 if let Some(s_idx) = self.tree_cursor_section {
555 for (i, row) in rows.iter().enumerate() {
556 if let TreeRow::Section {
557 cat_idx,
558 section_idx,
559 } = *row
560 {
561 if cat_idx == cat && section_idx == s_idx {
562 return i;
563 }
564 }
565 }
566 }
567 for (i, row) in rows.iter().enumerate() {
568 if let TreeRow::Category { idx, .. } = *row {
569 if idx == cat {
570 return i;
571 }
572 }
573 }
574 0
575 }
576
577 pub fn auto_expand_current_category(&mut self) {
587 let idx = self.selected_category;
588 if self.is_category_expandable(idx) {
589 self.expanded_categories.insert(idx);
590 }
591 }
592
593 pub fn toggle_category_expanded(&mut self, cat_idx: usize) {
594 if !self.is_category_expandable(cat_idx) {
595 return;
596 }
597 if !self.expanded_categories.insert(cat_idx) {
598 self.expanded_categories.remove(&cat_idx);
599 }
600 }
601
602 pub fn jump_to_section(&mut self, cat_idx: usize, section_idx: usize) {
606 let Some(page) = self.pages.get(cat_idx) else {
607 return;
608 };
609 let Some(section) = page.sections.get(section_idx) else {
610 return;
611 };
612 let target_item = section.first_item_index;
613 self.update_control_focus(false);
614 self.selected_category = cat_idx;
615 self.selected_item = target_item;
616 self.tree_cursor_section = Some(section_idx);
617 self.focus.set(FocusPanel::Settings);
618 let width = self.layout_width;
619 if let Some(page) = self.pages.get(self.selected_category) {
620 self.scroll_panel.update_content_height(&page.items, width);
621 let item_y = self
628 .scroll_panel
629 .item_y_offset(&page.items, target_item, width);
630 self.scroll_panel.scroll.offset = item_y;
631 }
632 self.sub_focus = None;
633 self.init_map_focus(true);
634 self.update_control_focus(true);
635 self.auto_expand_current_category();
636 }
637
638 pub fn visible_tree(&self) -> Vec<TreeRow> {
642 let mut rows = Vec::with_capacity(self.pages.len());
643 for (idx, page) in self.pages.iter().enumerate() {
644 let expandable = page.sections.len() > 1;
645 let expanded = expandable && self.expanded_categories.contains(&idx);
646 rows.push(TreeRow::Category {
647 idx,
648 expandable,
649 expanded,
650 });
651 if expanded {
652 for section_idx in 0..page.sections.len() {
653 rows.push(TreeRow::Section {
654 cat_idx: idx,
655 section_idx,
656 });
657 }
658 }
659 }
660 rows
661 }
662
663 pub fn current_item(&self) -> Option<&SettingItem> {
665 self.current_page()
666 .and_then(|page| page.items.get(self.selected_item))
667 }
668
669 pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
671 self.pages
672 .get_mut(self.selected_category)
673 .and_then(|page| page.items.get_mut(self.selected_item))
674 }
675
676 pub fn can_exit_text_editing(&self) -> bool {
678 self.current_item()
679 .map(|item| {
680 if let SettingControl::Text(state) = &item.control {
681 state.is_valid()
682 } else {
683 true
684 }
685 })
686 .unwrap_or(true)
687 }
688
689 pub fn entry_dialog_can_exit_text_editing(&self) -> bool {
691 self.entry_dialog()
692 .and_then(|dialog| dialog.current_item())
693 .map(|item| {
694 if let SettingControl::Text(state) = &item.control {
695 state.is_valid()
696 } else {
697 true
698 }
699 })
700 .unwrap_or(true)
701 }
702
703 fn init_map_focus(&mut self, from_above: bool) {
706 if let Some(item) = self.current_item_mut() {
707 if let SettingControl::Map(ref mut map_state) = item.control {
708 map_state.init_focus(from_above);
709 }
710 }
711 self.update_map_sub_focus();
713 }
714
715 pub(super) fn update_control_focus(&mut self, focused: bool) {
719 let focus_state = if focused {
720 FocusState::Focused
721 } else {
722 FocusState::Normal
723 };
724 if let Some(item) = self.current_item_mut() {
725 match &mut item.control {
726 SettingControl::Map(ref mut state) => state.focus = focus_state,
727 SettingControl::TextList(ref mut state) => state.focus = focus_state,
728 SettingControl::DualList(ref mut state) => state.focus = focus_state,
729 SettingControl::ObjectArray(ref mut state) => state.focus = focus_state,
730 SettingControl::Toggle(ref mut state) => state.focus = focus_state,
731 SettingControl::Number(ref mut state) => state.focus = focus_state,
732 SettingControl::Dropdown(ref mut state) => state.focus = focus_state,
733 SettingControl::Text(ref mut state) => {
734 state.focus = focus_state;
735 if !focused {
739 state.editing = false;
740 }
741 }
742 SettingControl::Json(_) | SettingControl::Complex { .. } => {} }
744 }
745 }
746
747 fn update_map_sub_focus(&mut self) {
750 self.sub_focus = self.current_item().and_then(|item| {
751 if let SettingControl::Map(ref map_state) = item.control {
752 Some(match map_state.focused_entry {
754 Some(i) => 1 + i,
755 None => 1 + map_state.entries.len(), })
757 } else {
758 None
759 }
760 });
761 }
762
763 pub fn select_prev(&mut self) {
765 match self.focus_panel() {
766 FocusPanel::Categories => {
767 self.tree_step(-1);
768 }
769 FocusPanel::Settings => {
770 let handled = self
772 .current_item_mut()
773 .and_then(|item| match &mut item.control {
774 SettingControl::Map(map_state) => Some(map_state.focus_prev()),
775 _ => None,
776 })
777 .unwrap_or(false);
778
779 if handled {
780 self.update_map_sub_focus();
782 } else if self.selected_item > 0 {
783 self.update_control_focus(false); self.selected_item -= 1;
785 self.sub_focus = None;
786 self.init_map_focus(false); self.update_control_focus(true); }
789 self.ensure_visible();
790 }
791 FocusPanel::Footer => {
792 if self.footer_button_index > 0 {
794 self.footer_button_index -= 1;
795 }
796 }
797 }
798 }
799
800 pub fn select_next(&mut self) {
802 match self.focus_panel() {
803 FocusPanel::Categories => {
804 self.tree_step(1);
805 }
806 FocusPanel::Settings => {
807 let handled = self
809 .current_item_mut()
810 .and_then(|item| match &mut item.control {
811 SettingControl::Map(map_state) => Some(map_state.focus_next()),
812 _ => None,
813 })
814 .unwrap_or(false);
815
816 if handled {
817 self.update_map_sub_focus();
819 } else {
820 let can_move = self
821 .current_page()
822 .is_some_and(|page| self.selected_item + 1 < page.items.len());
823 if can_move {
824 self.update_control_focus(false); self.selected_item += 1;
826 self.sub_focus = None;
827 self.init_map_focus(true); self.update_control_focus(true); }
830 }
831 self.ensure_visible();
832 }
833 FocusPanel::Footer => {
834 if self.footer_button_index < 2 {
836 self.footer_button_index += 1;
837 }
838 }
839 }
840 }
841
842 pub fn select_next_page(&mut self) {
844 let page_size = self.scroll_panel.viewport_height().max(1);
845 for _ in 0..page_size {
846 self.select_next();
847 }
848 }
849
850 pub fn select_prev_page(&mut self) {
852 let page_size = self.scroll_panel.viewport_height().max(1);
853 for _ in 0..page_size {
854 self.select_prev();
855 }
856 }
857
858 pub fn toggle_focus(&mut self) {
860 let old_panel = self.focus_panel();
861 self.focus.focus_next();
862 self.on_panel_changed(old_panel, true);
863 }
864
865 pub fn toggle_focus_backward(&mut self) {
867 let old_panel = self.focus_panel();
868 self.focus.focus_prev();
869 self.on_panel_changed(old_panel, false);
870 }
871
872 fn on_panel_changed(&mut self, old_panel: FocusPanel, forward: bool) {
874 if old_panel == FocusPanel::Settings {
876 self.update_control_focus(false);
877 }
878
879 if self.focus_panel() == FocusPanel::Settings
881 && self.selected_item >= self.current_page().map_or(0, |p| p.items.len())
882 {
883 self.selected_item = 0;
884 }
885 self.sub_focus = None;
886
887 if self.focus_panel() == FocusPanel::Settings {
888 self.init_map_focus(forward); self.update_control_focus(true); }
891
892 if self.focus_panel() == FocusPanel::Footer {
894 self.footer_button_index = if forward {
895 0 } else {
897 4 };
899 }
900
901 self.ensure_visible();
902 }
903
904 pub fn set_item_style(&mut self, style: super::items::ItemBoxStyle) {
912 if self.item_style == style {
913 return;
914 }
915 self.item_style = style;
916 for page in &mut self.pages {
917 for item in &mut page.items {
918 item.style = style;
919 }
920 }
921 let width = self.layout_width;
922 if let Some(page) = self.pages.get(self.selected_category) {
923 self.scroll_panel.update_content_height(&page.items, width);
924 }
925 }
926
927 pub fn ensure_visible(&mut self) {
929 if self.focus_panel() != FocusPanel::Settings {
930 return;
931 }
932
933 let selected_item = self.selected_item;
935 let sub_focus = self.sub_focus;
936 let width = self.layout_width;
937 let prev_offset = self.scroll_panel.scroll.offset;
938 if let Some(page) = self.pages.get(self.selected_category) {
939 self.scroll_panel
940 .ensure_focused_visible(&page.items, selected_item, sub_focus, width);
941 }
942 if self.scroll_panel.scroll.offset != prev_offset {
946 self.sync_tree_cursor_to_body_scroll();
947 }
948 }
949
950 pub fn set_pending_change(&mut self, path: &str, value: serde_json::Value) {
952 let original = self.original_config.pointer(path);
954 if original == Some(&value) {
955 self.pending_changes.remove(path);
956 } else {
957 self.pending_changes.insert(path.to_string(), value);
958 }
959 }
960
961 pub fn has_changes(&self) -> bool {
963 !self.pending_changes.is_empty() || !self.pending_deletions.is_empty()
964 }
965
966 pub fn apply_changes(&self, config: &Config) -> Result<Config, serde_json::Error> {
968 let mut config_value = serde_json::to_value(config)?;
969
970 for (path, value) in &self.pending_changes {
971 if let Some(target) = config_value.pointer_mut(path) {
978 *target = value.clone();
979 } else {
980 set_json_pointer_create(&mut config_value, path, value.clone());
981 }
982 }
983
984 serde_json::from_value(config_value)
985 }
986
987 pub fn discard_changes(&mut self) {
989 self.pending_changes.clear();
990 self.pending_deletions.clear();
991 self.rebuild_pages();
993 }
994
995 pub fn set_target_layer(&mut self, layer: ConfigLayer) {
997 if layer != ConfigLayer::System {
998 self.target_layer = layer;
1000 self.pending_changes.clear();
1002 self.pending_deletions.clear();
1003 self.rebuild_pages();
1005 }
1006 }
1007
1008 pub fn cycle_target_layer(&mut self) {
1010 self.target_layer = match self.target_layer {
1011 ConfigLayer::System => ConfigLayer::User, ConfigLayer::User => ConfigLayer::Project,
1013 ConfigLayer::Project => ConfigLayer::Session,
1014 ConfigLayer::Session => ConfigLayer::User,
1015 };
1016 self.pending_changes.clear();
1018 self.pending_deletions.clear();
1019 self.rebuild_pages();
1021 }
1022
1023 pub fn target_layer_name(&self) -> &'static str {
1025 match self.target_layer {
1026 ConfigLayer::System => "System (read-only)",
1027 ConfigLayer::User => "User",
1028 ConfigLayer::Project => "Project",
1029 ConfigLayer::Session => "Session",
1030 }
1031 }
1032
1033 pub fn set_layer_sources(&mut self, sources: HashMap<String, ConfigLayer>) {
1036 self.layer_sources = sources;
1037 self.rebuild_pages();
1039 }
1040
1041 pub fn set_status_bar_tokens(&mut self, tokens: HashMap<String, String>) {
1044 self.available_status_bar_tokens = tokens;
1045 self.rebuild_pages();
1046 }
1047
1048 pub fn get_layer_source(&self, path: &str) -> ConfigLayer {
1051 self.layer_sources
1052 .get(path)
1053 .copied()
1054 .unwrap_or(ConfigLayer::System)
1055 }
1056
1057 pub fn layer_source_label(layer: ConfigLayer) -> &'static str {
1059 match layer {
1060 ConfigLayer::System => "default",
1061 ConfigLayer::User => "user",
1062 ConfigLayer::Project => "project",
1063 ConfigLayer::Session => "session",
1064 }
1065 }
1066
1067 pub fn reset_focused_entry_field(&mut self) {
1082 let Some(dialog) = self.entry_dialog_mut() else {
1083 return;
1084 };
1085 if dialog.focus_on_buttons {
1086 return;
1087 }
1088 let idx = dialog.selected_item;
1089 let Some(item) = dialog.items.get_mut(idx) else {
1090 return;
1091 };
1092 if item.read_only {
1093 return;
1094 }
1095 let Some(default) = item.default.clone() else {
1096 return;
1097 };
1098 update_control_from_value(&mut item.control, &default);
1099 item.modified = false;
1100 dialog.user_edited = true;
1101 }
1102
1103 pub fn reset_current_to_default(&mut self) {
1104 let reset_info = self.current_item().and_then(|item| {
1106 if !item.modified || item.is_auto_managed {
1109 return None;
1110 }
1111 item.default
1112 .as_ref()
1113 .map(|default| (item.path.clone(), default.clone()))
1114 });
1115
1116 if let Some((path, default)) = reset_info {
1117 self.pending_deletions.insert(path.clone());
1119 self.pending_changes.remove(&path);
1121
1122 if let Some(item) = self.current_item_mut() {
1126 update_control_from_value(&mut item.control, &default);
1127 item.modified = false;
1128 item.layer_source = ConfigLayer::System; }
1131 }
1132 }
1133
1134 pub fn set_current_to_null(&mut self) {
1140 let target_layer = self.target_layer;
1141 let change_info = self.current_item().and_then(|item| {
1142 if !item.nullable || item.is_null || item.read_only {
1143 return None;
1144 }
1145 Some(item.path.clone())
1146 });
1147
1148 if let Some(path) = change_info {
1149 self.pending_changes
1151 .insert(path.clone(), serde_json::Value::Null);
1152 self.pending_deletions.remove(&path);
1153
1154 if let Some(item) = self.current_item_mut() {
1156 item.is_null = true;
1157 item.modified = true;
1158 item.layer_source = target_layer;
1159 }
1160 }
1161 }
1162
1163 pub fn clear_current_category(&mut self) {
1169 let target_layer = self.target_layer;
1170 let page = match self.current_page() {
1171 Some(p) if p.nullable => p,
1172 _ => return,
1173 };
1174 let page_path = page.path.clone();
1175
1176 self.pending_changes
1178 .insert(page_path.clone(), serde_json::Value::Null);
1179
1180 let prefix = format!("{}/", page_path);
1182 self.pending_changes
1183 .retain(|path, _| !path.starts_with(&prefix));
1184 self.pending_deletions
1185 .retain(|path| !path.starts_with(&prefix));
1186
1187 if let Some(page) = self.current_page_mut() {
1189 for item in &mut page.items {
1190 if item.nullable {
1191 item.is_null = true;
1192 item.modified = false;
1193 item.layer_source = target_layer;
1194 }
1195 }
1196 }
1197 }
1198
1199 pub fn current_category_has_values(&self) -> bool {
1201 match self.current_page() {
1202 Some(page) if page.nullable => {
1203 page.items.iter().any(|item| !item.is_null && item.nullable)
1204 || page.items.iter().any(|item| item.modified)
1205 }
1206 _ => false,
1207 }
1208 }
1209
1210 pub fn on_value_changed(&mut self) {
1212 let target_layer = self.target_layer;
1214
1215 let change_info = self.current_item().map(|item| {
1217 let value = control_to_value(&item.control);
1218 (item.path.clone(), value)
1219 });
1220
1221 if let Some((path, value)) = change_info {
1222 self.pending_deletions.remove(&path);
1225
1226 if let Some(item) = self.current_item_mut() {
1228 item.modified = true; item.layer_source = target_layer; item.is_null = false; }
1232 self.set_pending_change(&path, value);
1233 }
1234 }
1235
1236 pub fn update_focus_states(&mut self) {
1238 let current_focus = self.focus_panel();
1239 for (page_idx, page) in self.pages.iter_mut().enumerate() {
1240 for (item_idx, item) in page.items.iter_mut().enumerate() {
1241 let is_focused = current_focus == FocusPanel::Settings
1242 && page_idx == self.selected_category
1243 && item_idx == self.selected_item;
1244
1245 let focus = if is_focused {
1246 FocusState::Focused
1247 } else {
1248 FocusState::Normal
1249 };
1250
1251 match &mut item.control {
1252 SettingControl::Toggle(state) => state.focus = focus,
1253 SettingControl::Number(state) => state.focus = focus,
1254 SettingControl::Dropdown(state) => state.focus = focus,
1255 SettingControl::Text(state) => state.focus = focus,
1256 SettingControl::TextList(state) => state.focus = focus,
1257 SettingControl::DualList(state) => state.focus = focus,
1258 SettingControl::Map(state) => state.focus = focus,
1259 SettingControl::ObjectArray(state) => state.focus = focus,
1260 SettingControl::Json(state) => state.focus = focus,
1261 SettingControl::Complex { .. } => {}
1262 }
1263 }
1264 }
1265 }
1266
1267 pub fn start_search(&mut self) {
1269 self.search_active = true;
1270 self.search_query.clear();
1271 self.search_results.clear();
1272 self.selected_search_result = 0;
1273 self.search_scroll_offset = 0;
1274 }
1275
1276 pub fn cancel_search(&mut self) {
1278 self.search_active = false;
1279 self.search_query.clear();
1280 self.search_results.clear();
1281 self.selected_search_result = 0;
1282 self.search_scroll_offset = 0;
1283 }
1284
1285 pub fn set_search_query(&mut self, query: String) {
1287 self.search_query = query;
1288 self.search_results = search_settings(&self.pages, &self.search_query);
1289 self.selected_search_result = 0;
1290 self.search_scroll_offset = 0;
1291 }
1292
1293 pub fn search_push_char(&mut self, c: char) {
1295 self.search_query.push(c);
1296 self.search_results = search_settings(&self.pages, &self.search_query);
1297 self.selected_search_result = 0;
1298 self.search_scroll_offset = 0;
1299 }
1300
1301 pub fn search_pop_char(&mut self) {
1303 self.search_query.pop();
1304 self.search_results = search_settings(&self.pages, &self.search_query);
1305 self.selected_search_result = 0;
1306 self.search_scroll_offset = 0;
1307 }
1308
1309 pub fn search_prev(&mut self) {
1311 if !self.search_results.is_empty() && self.selected_search_result > 0 {
1312 self.selected_search_result -= 1;
1313 if self.selected_search_result < self.search_scroll_offset {
1315 self.search_scroll_offset = self.selected_search_result;
1316 }
1317 }
1318 }
1319
1320 pub fn search_next(&mut self) {
1322 if !self.search_results.is_empty()
1323 && self.selected_search_result + 1 < self.search_results.len()
1324 {
1325 self.selected_search_result += 1;
1326 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1328 self.search_scroll_offset =
1329 self.selected_search_result - self.search_max_visible + 1;
1330 }
1331 }
1332 }
1333
1334 pub fn search_scroll_up(&mut self, delta: usize) -> bool {
1336 if self.search_results.is_empty() || self.search_scroll_offset == 0 {
1337 return false;
1338 }
1339 self.search_scroll_offset = self.search_scroll_offset.saturating_sub(delta);
1340 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1342 self.selected_search_result = self.search_scroll_offset + self.search_max_visible - 1;
1343 }
1344 true
1345 }
1346
1347 pub fn search_scroll_down(&mut self, delta: usize) -> bool {
1349 if self.search_results.is_empty() {
1350 return false;
1351 }
1352 let max_offset = self
1353 .search_results
1354 .len()
1355 .saturating_sub(self.search_max_visible);
1356 if self.search_scroll_offset >= max_offset {
1357 return false;
1358 }
1359 self.search_scroll_offset = (self.search_scroll_offset + delta).min(max_offset);
1360 if self.selected_search_result < self.search_scroll_offset {
1362 self.selected_search_result = self.search_scroll_offset;
1363 }
1364 true
1365 }
1366
1367 pub fn search_scroll_to_ratio(&mut self, ratio: f32) -> bool {
1369 if self.search_results.is_empty() {
1370 return false;
1371 }
1372 let max_offset = self
1373 .search_results
1374 .len()
1375 .saturating_sub(self.search_max_visible);
1376 let new_offset = (ratio * max_offset as f32) as usize;
1377 if new_offset != self.search_scroll_offset {
1378 self.search_scroll_offset = new_offset.min(max_offset);
1379 if self.selected_search_result < self.search_scroll_offset {
1381 self.selected_search_result = self.search_scroll_offset;
1382 } else if self.selected_search_result
1383 >= self.search_scroll_offset + self.search_max_visible
1384 {
1385 self.selected_search_result =
1386 self.search_scroll_offset + self.search_max_visible - 1;
1387 }
1388 return true;
1389 }
1390 false
1391 }
1392
1393 pub fn jump_to_search_result(&mut self) {
1395 let Some(result) = self
1397 .search_results
1398 .get(self.selected_search_result)
1399 .cloned()
1400 else {
1401 return;
1402 };
1403 let page_index = result.page_index;
1404 let item_index = result.item_index;
1405
1406 self.update_control_focus(false);
1408 self.selected_category = page_index;
1409 self.selected_item = item_index;
1410 self.focus.set(FocusPanel::Settings);
1411 self.scroll_panel.scroll.offset = 0;
1413 let width = self.layout_width;
1415 if let Some(page) = self.pages.get(self.selected_category) {
1416 self.scroll_panel.update_content_height(&page.items, width);
1417 }
1418 self.sub_focus = None;
1419 self.init_map_focus(true);
1420
1421 if let Some(ref deep_match) = result.deep_match {
1423 self.jump_to_deep_match(deep_match);
1424 }
1425
1426 self.update_control_focus(true); self.auto_expand_current_category();
1428 self.tree_cursor_section = self.current_section_index();
1432 self.ensure_visible();
1433 self.cancel_search();
1434 }
1435
1436 fn jump_to_deep_match(&mut self, deep_match: &DeepMatch) {
1438 match deep_match {
1439 DeepMatch::MapKey { entry_index, .. } | DeepMatch::MapValue { entry_index, .. } => {
1440 if let Some(item) = self.current_item_mut() {
1441 if let SettingControl::Map(ref mut map_state) = item.control {
1442 map_state.focused_entry = Some(*entry_index);
1443 }
1444 }
1445 self.update_map_sub_focus();
1446 }
1447 DeepMatch::TextListItem { item_index, .. } => {
1448 if let Some(item) = self.current_item_mut() {
1449 if let SettingControl::TextList(ref mut list_state) = item.control {
1450 list_state.focused_item = Some(*item_index);
1451 }
1452 }
1453 self.sub_focus = Some(1 + *item_index);
1455 }
1456 }
1457 }
1458
1459 pub fn current_search_result(&self) -> Option<&SearchResult> {
1461 self.search_results.get(self.selected_search_result)
1462 }
1463
1464 pub fn show_confirm_dialog(&mut self) {
1466 self.showing_confirm_dialog = true;
1467 self.confirm_dialog_selection = 0; }
1469
1470 pub fn hide_confirm_dialog(&mut self) {
1472 self.showing_confirm_dialog = false;
1473 self.confirm_dialog_selection = 0;
1474 }
1475
1476 pub fn confirm_dialog_next(&mut self) {
1478 self.confirm_dialog_selection = (self.confirm_dialog_selection + 1) % 3;
1479 }
1480
1481 pub fn confirm_dialog_prev(&mut self) {
1483 self.confirm_dialog_selection = if self.confirm_dialog_selection == 0 {
1484 2
1485 } else {
1486 self.confirm_dialog_selection - 1
1487 };
1488 }
1489
1490 pub fn toggle_help(&mut self) {
1492 self.showing_help = !self.showing_help;
1493 }
1494
1495 pub fn hide_help(&mut self) {
1497 self.showing_help = false;
1498 }
1499
1500 pub fn showing_entry_dialog(&self) -> bool {
1502 self.has_entry_dialog()
1503 }
1504
1505 pub fn open_entry_dialog(&mut self) {
1507 let Some(item) = self.current_item() else {
1508 return;
1509 };
1510
1511 let path = item.path.as_str();
1513 let SettingControl::Map(map_state) = &item.control else {
1514 return;
1515 };
1516
1517 let Some(entry_idx) = map_state.focused_entry else {
1519 return;
1520 };
1521 let Some((key, value)) = map_state.entries.get(entry_idx) else {
1522 return;
1523 };
1524
1525 let Some(schema) = map_state.value_schema.as_ref() else {
1527 return; };
1529
1530 let no_delete = map_state.no_add;
1532
1533 let dialog = EntryDialogState::from_schema(
1535 key.clone(),
1536 value,
1537 schema,
1538 path,
1539 false,
1540 no_delete,
1541 &self.available_status_bar_tokens,
1542 );
1543 self.entry_dialog_stack.push(dialog);
1544 }
1545
1546 pub fn open_add_entry_dialog(&mut self) {
1548 let Some(item) = self.current_item() else {
1549 return;
1550 };
1551 let SettingControl::Map(map_state) = &item.control else {
1552 return;
1553 };
1554 let Some(schema) = map_state.value_schema.as_ref() else {
1555 return;
1556 };
1557 let path = item.path.clone();
1558
1559 let dialog = EntryDialogState::from_schema(
1562 String::new(),
1563 &serde_json::json!({}),
1564 schema,
1565 &path,
1566 true,
1567 false,
1568 &self.available_status_bar_tokens,
1569 );
1570 self.entry_dialog_stack.push(dialog);
1571 }
1572
1573 pub fn open_add_array_item_dialog(&mut self) {
1575 let Some(item) = self.current_item() else {
1576 return;
1577 };
1578 let SettingControl::ObjectArray(array_state) = &item.control else {
1579 return;
1580 };
1581 let Some(schema) = array_state.item_schema.as_ref() else {
1582 return;
1583 };
1584 let path = item.path.clone();
1585
1586 let dialog = EntryDialogState::for_array_item(
1588 None,
1589 &serde_json::json!({}),
1590 schema,
1591 &path,
1592 true,
1593 &self.available_status_bar_tokens,
1594 );
1595 self.entry_dialog_stack.push(dialog);
1596 }
1597
1598 pub fn open_edit_array_item_dialog(&mut self) {
1600 let Some(item) = self.current_item() else {
1601 return;
1602 };
1603 let SettingControl::ObjectArray(array_state) = &item.control else {
1604 return;
1605 };
1606 let Some(schema) = array_state.item_schema.as_ref() else {
1607 return;
1608 };
1609 let Some(index) = array_state.focused_index else {
1610 return;
1611 };
1612 let Some(value) = array_state.bindings.get(index) else {
1613 return;
1614 };
1615 let path = item.path.clone();
1616
1617 let dialog = EntryDialogState::for_array_item(
1618 Some(index),
1619 value,
1620 schema,
1621 &path,
1622 false,
1623 &self.available_status_bar_tokens,
1624 );
1625 self.entry_dialog_stack.push(dialog);
1626 }
1627
1628 pub fn close_entry_dialog(&mut self) {
1630 self.entry_dialog_stack.pop();
1631 }
1632
1633 pub fn open_nested_entry_dialog(&mut self) {
1638 let nested_info = self.entry_dialog().and_then(|dialog| {
1640 let item = dialog.current_item()?;
1641 let base = dialog.entry_path();
1647 let relative = item.path.trim_start_matches('/');
1648 let path = if relative.is_empty() {
1649 base
1653 } else {
1654 format!("{}/{}", base, relative)
1655 };
1656
1657 match &item.control {
1658 SettingControl::Map(map_state) => {
1659 let schema = map_state.value_schema.as_ref()?;
1660 let no_delete = map_state.no_add; if let Some(entry_idx) = map_state.focused_entry {
1662 let (key, value) = map_state.entries.get(entry_idx)?;
1664 Some(NestedDialogInfo::MapEntry {
1665 key: key.clone(),
1666 value: value.clone(),
1667 schema: schema.as_ref().clone(),
1668 path,
1669 is_new: false,
1670 no_delete,
1671 })
1672 } else {
1673 Some(NestedDialogInfo::MapEntry {
1675 key: String::new(),
1676 value: serde_json::json!({}),
1677 schema: schema.as_ref().clone(),
1678 path,
1679 is_new: true,
1680 no_delete: false, })
1682 }
1683 }
1684 SettingControl::ObjectArray(array_state) => {
1685 let schema = array_state.item_schema.as_ref()?;
1686 if let Some(index) = array_state.focused_index {
1687 let value = array_state.bindings.get(index)?;
1689 Some(NestedDialogInfo::ArrayItem {
1690 index: Some(index),
1691 value: value.clone(),
1692 schema: schema.as_ref().clone(),
1693 path,
1694 is_new: false,
1695 })
1696 } else {
1697 Some(NestedDialogInfo::ArrayItem {
1699 index: None,
1700 value: serde_json::json!({}),
1701 schema: schema.as_ref().clone(),
1702 path,
1703 is_new: true,
1704 })
1705 }
1706 }
1707 _ => None,
1708 }
1709 });
1710
1711 if let Some(info) = nested_info {
1713 let dialog = match info {
1714 NestedDialogInfo::MapEntry {
1715 key,
1716 value,
1717 schema,
1718 path,
1719 is_new,
1720 no_delete,
1721 } => EntryDialogState::from_schema(
1722 key,
1723 &value,
1724 &schema,
1725 &path,
1726 is_new,
1727 no_delete,
1728 &self.available_status_bar_tokens,
1729 ),
1730 NestedDialogInfo::ArrayItem {
1731 index,
1732 value,
1733 schema,
1734 path,
1735 is_new,
1736 } => EntryDialogState::for_array_item(
1737 index,
1738 &value,
1739 &schema,
1740 &path,
1741 is_new,
1742 &self.available_status_bar_tokens,
1743 ),
1744 };
1745 self.entry_dialog_stack.push(dialog);
1746 }
1747 }
1748
1749 pub fn save_entry_dialog(&mut self) {
1754 let is_array = if self.entry_dialog_stack.len() > 1 {
1758 self.entry_dialog_stack
1760 .get(self.entry_dialog_stack.len() - 2)
1761 .and_then(|parent| parent.current_item())
1762 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1763 .unwrap_or(false)
1764 } else {
1765 self.current_item()
1767 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1768 .unwrap_or(false)
1769 };
1770
1771 if is_array {
1772 self.save_array_item_dialog_inner();
1773 } else {
1774 self.save_map_entry_dialog_inner();
1775 }
1776 }
1777
1778 fn save_map_entry_dialog_inner(&mut self) {
1780 let Some(mut dialog) = self.entry_dialog_stack.pop() else {
1781 return;
1782 };
1783 dialog.commit_pending_list_drafts();
1787
1788 let key = dialog.get_key();
1790 if key.is_empty() {
1791 return; }
1793
1794 let value = dialog.to_value();
1795 let map_path = dialog.map_path.clone();
1796 let original_key = dialog.entry_key.clone();
1797 let is_new = dialog.is_new;
1798 let key_changed = !is_new && key != original_key;
1799
1800 if let Some(item) = self.current_item_mut() {
1802 if let SettingControl::Map(map_state) = &mut item.control {
1803 if key_changed {
1805 if let Some(idx) = map_state
1806 .entries
1807 .iter()
1808 .position(|(k, _)| k == &original_key)
1809 {
1810 map_state.entries.remove(idx);
1811 }
1812 }
1813
1814 if let Some(entry) = map_state.entries.iter_mut().find(|(k, _)| k == &key) {
1816 entry.1 = value.clone();
1817 } else {
1818 map_state.entries.push((key.clone(), value.clone()));
1819 map_state.entries.sort_by(|a, b| a.0.cmp(&b.0));
1820 }
1821 }
1822 }
1823
1824 if key_changed {
1826 let old_path = format!("{}/{}", map_path, original_key);
1827 self.pending_changes
1828 .insert(old_path, serde_json::Value::Null);
1829 }
1830
1831 let path = format!("{}/{}", map_path, key);
1833 self.set_pending_change(&path, value);
1834 }
1835
1836 fn save_array_item_dialog_inner(&mut self) {
1838 let Some(mut dialog) = self.entry_dialog_stack.pop() else {
1839 return;
1840 };
1841 dialog.commit_pending_list_drafts();
1843
1844 let value = dialog.to_value();
1845 let array_path = dialog.map_path.clone();
1846 let is_new = dialog.is_new;
1847 let entry_key = dialog.entry_key.clone();
1848
1849 let is_nested = !self.entry_dialog_stack.is_empty();
1851
1852 if is_nested {
1853 let parent_entry_path = self
1861 .entry_dialog_stack
1862 .last()
1863 .map(|p| p.entry_path())
1864 .unwrap_or_default();
1865 let item_path = array_path
1866 .strip_prefix(parent_entry_path.as_str())
1867 .unwrap_or(&array_path)
1868 .trim_end_matches('/')
1869 .to_string();
1870
1871 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1877 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1878 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1879 if is_new {
1880 array_state.bindings.push(value.clone());
1881 } else if let Ok(index) = entry_key.parse::<usize>() {
1882 if index < array_state.bindings.len() {
1883 array_state.bindings[index] = value.clone();
1884 }
1885 }
1886 parent.user_edited = true;
1887 }
1888 }
1889 }
1890
1891 if let Some(parent) = self.entry_dialog_stack.last() {
1894 if let Some(item) = parent.items.iter().find(|i| i.path == item_path) {
1895 if let SettingControl::ObjectArray(array_state) = &item.control {
1896 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1897 self.set_pending_change(&array_path, array_value);
1898 }
1899 }
1900 }
1901 } else {
1902 if let Some(item) = self.current_item_mut() {
1904 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1905 if is_new {
1906 array_state.bindings.push(value.clone());
1907 } else if let Ok(index) = entry_key.parse::<usize>() {
1908 if index < array_state.bindings.len() {
1909 array_state.bindings[index] = value.clone();
1910 }
1911 }
1912 }
1913 }
1914
1915 if let Some(item) = self.current_item() {
1917 if let SettingControl::ObjectArray(array_state) = &item.control {
1918 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1919 self.set_pending_change(&array_path, array_value);
1920 }
1921 }
1922 }
1923 }
1924
1925 pub fn request_entry_delete_confirm(&mut self) {
1931 let name = self
1932 .entry_dialog()
1933 .map(|d| d.entry_key.clone())
1934 .unwrap_or_default();
1935 self.entry_delete_target_name = name;
1936 self.entry_delete_confirm_selection = 0;
1937 self.showing_entry_delete_confirm = true;
1938 }
1939
1940 pub fn delete_entry_dialog(&mut self) {
1941 let is_nested = self.entry_dialog_stack.len() > 1;
1943
1944 let Some(dialog) = self.entry_dialog_stack.pop() else {
1945 return;
1946 };
1947
1948 let path = format!("{}/{}", dialog.map_path, dialog.entry_key);
1949
1950 if is_nested {
1952 let map_field = dialog.map_path.rsplit('/').next().unwrap_or("").to_string();
1955 let item_path = format!("/{}", map_field);
1956
1957 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1959 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1960 if let SettingControl::Map(map_state) = &mut item.control {
1961 if let Some(idx) = map_state
1962 .entries
1963 .iter()
1964 .position(|(k, _)| k == &dialog.entry_key)
1965 {
1966 map_state.remove_entry(idx);
1967 }
1968 }
1969 }
1970 }
1971 } else {
1972 if let Some(item) = self.current_item_mut() {
1974 if let SettingControl::Map(map_state) = &mut item.control {
1975 if let Some(idx) = map_state
1976 .entries
1977 .iter()
1978 .position(|(k, _)| k == &dialog.entry_key)
1979 {
1980 map_state.remove_entry(idx);
1981 }
1982 }
1983 }
1984 }
1985
1986 self.set_pending_change(&path, serde_json::Value::Null);
1988 }
1989
1990 pub fn max_scroll(&self) -> u16 {
1992 self.scroll_panel.scroll.max_offset()
1993 }
1994
1995 pub fn scroll_up(&mut self, delta: usize) -> bool {
1998 let old = self.scroll_panel.scroll.offset;
1999 self.scroll_panel.scroll_up(delta as u16);
2000 let changed = old != self.scroll_panel.scroll.offset;
2001 if changed {
2002 self.sync_tree_cursor_to_body_scroll();
2003 }
2004 changed
2005 }
2006
2007 pub fn scroll_down(&mut self, delta: usize) -> bool {
2010 let old = self.scroll_panel.scroll.offset;
2011 self.scroll_panel.scroll_down(delta as u16);
2012 let changed = old != self.scroll_panel.scroll.offset;
2013 if changed {
2014 self.sync_tree_cursor_to_body_scroll();
2015 }
2016 changed
2017 }
2018
2019 pub fn scroll_to_ratio(&mut self, ratio: f32) -> bool {
2022 let old = self.scroll_panel.scroll.offset;
2023 self.scroll_panel.scroll_to_ratio(ratio);
2024 let changed = old != self.scroll_panel.scroll.offset;
2025 if changed {
2026 self.sync_tree_cursor_to_body_scroll();
2027 }
2028 changed
2029 }
2030
2031 pub(super) fn sync_tree_cursor_to_body_scroll(&mut self) {
2037 if let Some(section_idx) = self.current_section_index() {
2038 self.tree_cursor_section = Some(section_idx);
2039 }
2040 }
2045
2046 pub fn is_number_control(&self) -> bool {
2049 self.current_item()
2050 .is_some_and(|item| matches!(item.control, SettingControl::Number(_)))
2051 }
2052
2053 pub fn start_editing(&mut self) {
2054 if let Some(item) = self.current_item() {
2055 if matches!(
2056 item.control,
2057 SettingControl::TextList(_)
2058 | SettingControl::DualList(_)
2059 | SettingControl::Text(_)
2060 | SettingControl::Map(_)
2061 | SettingControl::Json(_)
2062 ) {
2063 self.editing_text = true;
2064 }
2065 }
2066 if let Some(item) = self.current_item_mut() {
2067 match item.control {
2068 SettingControl::DualList(ref mut dl) => {
2069 dl.editing = true;
2070 }
2071 SettingControl::Text(ref mut state) => {
2072 state.editing = true;
2073 state.arm_replace_on_type();
2078 }
2079 _ => {}
2080 }
2081 }
2082 }
2083
2084 pub fn stop_editing(&mut self) {
2086 self.editing_text = false;
2087 if let Some(item) = self.current_item_mut() {
2088 match item.control {
2089 SettingControl::DualList(ref mut dl) => {
2090 dl.editing = false;
2091 }
2092 SettingControl::Text(ref mut state) => {
2093 state.editing = false;
2094 }
2095 _ => {}
2096 }
2097 }
2098 }
2099
2100 pub fn is_editable_control(&self) -> bool {
2102 self.current_item().is_some_and(|item| {
2103 matches!(
2104 item.control,
2105 SettingControl::TextList(_)
2106 | SettingControl::DualList(_)
2107 | SettingControl::Text(_)
2108 | SettingControl::Map(_)
2109 | SettingControl::Json(_)
2110 )
2111 })
2112 }
2113
2114 pub fn is_editing_json(&self) -> bool {
2116 if !self.editing_text {
2117 return false;
2118 }
2119 self.current_item()
2120 .map(|item| matches!(&item.control, SettingControl::Json(_)))
2121 .unwrap_or(false)
2122 }
2123
2124 pub fn text_insert(&mut self, c: char) {
2126 if let Some(item) = self.current_item_mut() {
2127 match &mut item.control {
2128 SettingControl::TextList(state) => state.insert(c),
2129 SettingControl::Text(state) => state.insert(c),
2130 SettingControl::Map(state) => {
2131 state.new_key_text.insert(state.cursor, c);
2132 state.cursor += c.len_utf8();
2133 }
2134 SettingControl::Json(state) => state.insert(c),
2135 _ => {}
2136 }
2137 }
2138 }
2139
2140 pub fn text_backspace(&mut self) {
2142 if let Some(item) = self.current_item_mut() {
2143 match &mut item.control {
2144 SettingControl::TextList(state) => state.backspace(),
2145 SettingControl::Text(state) => state.backspace(),
2146 SettingControl::Map(state) => {
2147 if state.cursor > 0 {
2148 let mut char_start = state.cursor - 1;
2149 while char_start > 0 && !state.new_key_text.is_char_boundary(char_start) {
2150 char_start -= 1;
2151 }
2152 state.new_key_text.remove(char_start);
2153 state.cursor = char_start;
2154 }
2155 }
2156 SettingControl::Json(state) => state.backspace(),
2157 _ => {}
2158 }
2159 }
2160 }
2161
2162 pub fn text_move_left(&mut self) {
2164 if let Some(item) = self.current_item_mut() {
2165 match &mut item.control {
2166 SettingControl::TextList(state) => state.move_left(),
2167 SettingControl::Text(state) => state.move_left(),
2168 SettingControl::Map(state) => {
2169 if state.cursor > 0 {
2170 let mut new_pos = state.cursor - 1;
2171 while new_pos > 0 && !state.new_key_text.is_char_boundary(new_pos) {
2172 new_pos -= 1;
2173 }
2174 state.cursor = new_pos;
2175 }
2176 }
2177 SettingControl::Json(state) => state.move_left(),
2178 _ => {}
2179 }
2180 }
2181 }
2182
2183 pub fn text_move_right(&mut self) {
2185 if let Some(item) = self.current_item_mut() {
2186 match &mut item.control {
2187 SettingControl::TextList(state) => state.move_right(),
2188 SettingControl::Text(state) => state.move_right(),
2189 SettingControl::Map(state) => {
2190 if state.cursor < state.new_key_text.len() {
2191 let mut new_pos = state.cursor + 1;
2192 while new_pos < state.new_key_text.len()
2193 && !state.new_key_text.is_char_boundary(new_pos)
2194 {
2195 new_pos += 1;
2196 }
2197 state.cursor = new_pos;
2198 }
2199 }
2200 SettingControl::Json(state) => state.move_right(),
2201 _ => {}
2202 }
2203 }
2204 }
2205
2206 pub fn text_focus_prev(&mut self) {
2208 if let Some(item) = self.current_item_mut() {
2209 match &mut item.control {
2210 SettingControl::TextList(state) => state.focus_prev(),
2211 SettingControl::Map(state) => {
2212 state.focus_prev();
2213 }
2214 _ => {}
2215 }
2216 }
2217 }
2218
2219 pub fn text_focus_next(&mut self) {
2221 if let Some(item) = self.current_item_mut() {
2222 match &mut item.control {
2223 SettingControl::TextList(state) => state.focus_next(),
2224 SettingControl::Map(state) => {
2225 state.focus_next();
2226 }
2227 _ => {}
2228 }
2229 }
2230 }
2231
2232 pub fn text_add_item(&mut self) {
2234 if let Some(item) = self.current_item_mut() {
2235 match &mut item.control {
2236 SettingControl::TextList(state) => state.add_item(),
2237 SettingControl::Map(state) => state.add_entry_from_input(),
2238 _ => {}
2239 }
2240 }
2241 self.on_value_changed();
2243 }
2244
2245 pub fn text_remove_focused(&mut self) {
2247 if let Some(item) = self.current_item_mut() {
2248 match &mut item.control {
2249 SettingControl::TextList(state) => {
2250 if let Some(idx) = state.focused_item {
2251 state.remove_item(idx);
2252 }
2253 }
2254 SettingControl::Map(state) => {
2255 if let Some(idx) = state.focused_entry {
2256 state.remove_entry(idx);
2257 }
2258 }
2259 _ => {}
2260 }
2261 }
2262 self.on_value_changed();
2264 }
2265
2266 pub fn is_editing_dual_list(&self) -> bool {
2268 if !self.editing_text {
2269 return false;
2270 }
2271 self.current_item()
2272 .map(|item| matches!(&item.control, SettingControl::DualList(_)))
2273 .unwrap_or(false)
2274 }
2275
2276 pub fn with_dual_list_mut<R>(
2281 &mut self,
2282 item_idx: usize,
2283 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2284 ) -> Option<R> {
2285 let page = self.pages.get_mut(self.selected_category)?;
2286 let item = page.items.get_mut(item_idx)?;
2287 if let SettingControl::DualList(ref mut state) = item.control {
2288 Some(f(state))
2289 } else {
2290 None
2291 }
2292 }
2293
2294 pub fn with_current_dual_list_mut<R>(
2297 &mut self,
2298 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2299 ) -> Option<R> {
2300 if let Some(item) = self.current_item_mut() {
2301 if let SettingControl::DualList(ref mut state) = item.control {
2302 return Some(f(state));
2303 }
2304 }
2305 None
2306 }
2307
2308 pub fn refresh_dual_list_sibling(&mut self) {
2315 let (new_included, sibling_path) = {
2316 let Some(item) = self.current_item() else {
2317 return;
2318 };
2319 let SettingControl::DualList(state) = &item.control else {
2320 return;
2321 };
2322 let Some(ref sib_path) = item.dual_list_sibling else {
2323 return;
2324 };
2325 (state.included.clone(), sib_path.clone())
2326 };
2327
2328 if let Some(page) = self.pages.get_mut(self.selected_category) {
2330 for other in page.items.iter_mut() {
2331 if other.path == sibling_path {
2332 if let SettingControl::DualList(ref mut sib_state) = other.control {
2333 sib_state.excluded = new_included;
2334 }
2335 break;
2336 }
2337 }
2338 }
2339 }
2340
2341 pub fn json_cursor_up(&mut self) {
2345 if let Some(item) = self.current_item_mut() {
2346 if let SettingControl::Json(state) = &mut item.control {
2347 state.move_up();
2348 }
2349 }
2350 }
2351
2352 pub fn json_cursor_down(&mut self) {
2354 if let Some(item) = self.current_item_mut() {
2355 if let SettingControl::Json(state) = &mut item.control {
2356 state.move_down();
2357 }
2358 }
2359 }
2360
2361 pub fn json_insert_newline(&mut self) {
2363 if let Some(item) = self.current_item_mut() {
2364 if let SettingControl::Json(state) = &mut item.control {
2365 state.insert('\n');
2366 }
2367 }
2368 }
2369
2370 pub fn json_delete(&mut self) {
2372 if let Some(item) = self.current_item_mut() {
2373 if let SettingControl::Json(state) = &mut item.control {
2374 state.delete();
2375 }
2376 }
2377 }
2378
2379 pub fn json_exit_editing(&mut self) {
2381 let is_valid = self
2382 .current_item()
2383 .map(|item| {
2384 if let SettingControl::Json(state) = &item.control {
2385 state.is_valid()
2386 } else {
2387 true
2388 }
2389 })
2390 .unwrap_or(true);
2391
2392 if is_valid {
2393 if let Some(item) = self.current_item_mut() {
2394 if let SettingControl::Json(state) = &mut item.control {
2395 state.commit();
2396 }
2397 }
2398 self.on_value_changed();
2399 } else if let Some(item) = self.current_item_mut() {
2400 if let SettingControl::Json(state) = &mut item.control {
2401 state.revert();
2402 }
2403 }
2404 self.editing_text = false;
2405 }
2406
2407 pub fn json_select_all(&mut self) {
2409 if let Some(item) = self.current_item_mut() {
2410 if let SettingControl::Json(state) = &mut item.control {
2411 state.select_all();
2412 }
2413 }
2414 }
2415
2416 pub fn json_selected_text(&self) -> Option<String> {
2418 if let Some(item) = self.current_item() {
2419 if let SettingControl::Json(state) = &item.control {
2420 return state.selected_text();
2421 }
2422 }
2423 None
2424 }
2425
2426 pub fn json_cursor_up_selecting(&mut self) {
2428 if let Some(item) = self.current_item_mut() {
2429 if let SettingControl::Json(state) = &mut item.control {
2430 state.editor.move_up_selecting();
2431 }
2432 }
2433 }
2434
2435 pub fn json_cursor_down_selecting(&mut self) {
2437 if let Some(item) = self.current_item_mut() {
2438 if let SettingControl::Json(state) = &mut item.control {
2439 state.editor.move_down_selecting();
2440 }
2441 }
2442 }
2443
2444 pub fn json_cursor_left_selecting(&mut self) {
2446 if let Some(item) = self.current_item_mut() {
2447 if let SettingControl::Json(state) = &mut item.control {
2448 state.editor.move_left_selecting();
2449 }
2450 }
2451 }
2452
2453 pub fn json_cursor_right_selecting(&mut self) {
2455 if let Some(item) = self.current_item_mut() {
2456 if let SettingControl::Json(state) = &mut item.control {
2457 state.editor.move_right_selecting();
2458 }
2459 }
2460 }
2461
2462 pub fn is_dropdown_open(&self) -> bool {
2466 self.current_item().is_some_and(|item| {
2467 if let SettingControl::Dropdown(ref d) = item.control {
2468 d.open
2469 } else {
2470 false
2471 }
2472 })
2473 }
2474
2475 pub fn dropdown_toggle(&mut self) {
2477 let mut opened = false;
2478 if let Some(item) = self.current_item_mut() {
2479 if let SettingControl::Dropdown(ref mut d) = item.control {
2480 d.toggle_open();
2481 opened = d.open;
2482 }
2483 }
2484
2485 if opened {
2487 let selected_item = self.selected_item;
2489 let width = self.layout_width;
2490 if let Some(page) = self.pages.get(self.selected_category) {
2491 self.scroll_panel.update_content_height(&page.items, width);
2492 self.scroll_panel
2494 .ensure_focused_visible(&page.items, selected_item, None, width);
2495 }
2496 }
2497 }
2498
2499 pub fn dropdown_prev(&mut self) {
2501 if let Some(item) = self.current_item_mut() {
2502 if let SettingControl::Dropdown(ref mut d) = item.control {
2503 d.select_prev();
2504 }
2505 }
2506 }
2507
2508 pub fn dropdown_next(&mut self) {
2510 if let Some(item) = self.current_item_mut() {
2511 if let SettingControl::Dropdown(ref mut d) = item.control {
2512 d.select_next();
2513 }
2514 }
2515 }
2516
2517 pub fn dropdown_home(&mut self) {
2519 if let Some(item) = self.current_item_mut() {
2520 if let SettingControl::Dropdown(ref mut d) = item.control {
2521 if !d.options.is_empty() {
2522 d.selected = 0;
2523 d.ensure_visible();
2524 }
2525 }
2526 }
2527 }
2528
2529 pub fn dropdown_end(&mut self) {
2531 if let Some(item) = self.current_item_mut() {
2532 if let SettingControl::Dropdown(ref mut d) = item.control {
2533 if !d.options.is_empty() {
2534 d.selected = d.options.len() - 1;
2535 d.ensure_visible();
2536 }
2537 }
2538 }
2539 }
2540
2541 pub fn dropdown_confirm(&mut self) {
2543 if let Some(item) = self.current_item_mut() {
2544 if let SettingControl::Dropdown(ref mut d) = item.control {
2545 d.confirm();
2546 }
2547 }
2548 self.on_value_changed();
2549 }
2550
2551 pub fn dropdown_cancel(&mut self) {
2553 if let Some(item) = self.current_item_mut() {
2554 if let SettingControl::Dropdown(ref mut d) = item.control {
2555 d.cancel();
2556 }
2557 }
2558 }
2559
2560 pub fn dropdown_select(&mut self, option_idx: usize) {
2562 if let Some(item) = self.current_item_mut() {
2563 if let SettingControl::Dropdown(ref mut d) = item.control {
2564 if option_idx < d.options.len() {
2565 d.selected = option_idx;
2566 d.confirm();
2567 }
2568 }
2569 }
2570 self.on_value_changed();
2571 }
2572
2573 pub fn set_dropdown_hover(&mut self, hover_idx: Option<usize>) -> bool {
2576 if let Some(item) = self.current_item_mut() {
2577 if let SettingControl::Dropdown(ref mut d) = item.control {
2578 if d.open && d.hover_index != hover_idx {
2579 d.hover_index = hover_idx;
2580 return true;
2581 }
2582 }
2583 }
2584 false
2585 }
2586
2587 pub fn dropdown_scroll(&mut self, delta: i32) {
2589 if let Some(item) = self.current_item_mut() {
2590 if let SettingControl::Dropdown(ref mut d) = item.control {
2591 if d.open {
2592 d.scroll_by(delta);
2593 }
2594 }
2595 }
2596 }
2597
2598 pub fn is_number_editing(&self) -> bool {
2602 self.current_item().is_some_and(|item| {
2603 if let SettingControl::Number(ref n) = item.control {
2604 n.editing()
2605 } else {
2606 false
2607 }
2608 })
2609 }
2610
2611 pub fn start_number_editing(&mut self) {
2613 if let Some(item) = self.current_item_mut() {
2614 if let SettingControl::Number(ref mut n) = item.control {
2615 n.start_editing();
2616 }
2617 }
2618 }
2619
2620 pub fn number_insert(&mut self, c: char) {
2622 if let Some(item) = self.current_item_mut() {
2623 if let SettingControl::Number(ref mut n) = item.control {
2624 n.insert_char(c);
2625 }
2626 }
2627 }
2628
2629 pub fn number_backspace(&mut self) {
2631 if let Some(item) = self.current_item_mut() {
2632 if let SettingControl::Number(ref mut n) = item.control {
2633 n.backspace();
2634 }
2635 }
2636 }
2637
2638 pub fn number_confirm(&mut self) {
2640 if let Some(item) = self.current_item_mut() {
2641 if let SettingControl::Number(ref mut n) = item.control {
2642 n.confirm_editing();
2643 }
2644 }
2645 self.on_value_changed();
2646 }
2647
2648 pub fn number_cancel(&mut self) {
2650 if let Some(item) = self.current_item_mut() {
2651 if let SettingControl::Number(ref mut n) = item.control {
2652 n.cancel_editing();
2653 }
2654 }
2655 }
2656
2657 pub fn number_delete(&mut self) {
2659 if let Some(item) = self.current_item_mut() {
2660 if let SettingControl::Number(ref mut n) = item.control {
2661 n.delete();
2662 }
2663 }
2664 }
2665
2666 pub fn number_move_left(&mut self) {
2668 if let Some(item) = self.current_item_mut() {
2669 if let SettingControl::Number(ref mut n) = item.control {
2670 n.move_left();
2671 }
2672 }
2673 }
2674
2675 pub fn number_move_right(&mut self) {
2677 if let Some(item) = self.current_item_mut() {
2678 if let SettingControl::Number(ref mut n) = item.control {
2679 n.move_right();
2680 }
2681 }
2682 }
2683
2684 pub fn number_move_home(&mut self) {
2686 if let Some(item) = self.current_item_mut() {
2687 if let SettingControl::Number(ref mut n) = item.control {
2688 n.move_home();
2689 }
2690 }
2691 }
2692
2693 pub fn number_move_end(&mut self) {
2695 if let Some(item) = self.current_item_mut() {
2696 if let SettingControl::Number(ref mut n) = item.control {
2697 n.move_end();
2698 }
2699 }
2700 }
2701
2702 pub fn number_move_left_selecting(&mut self) {
2704 if let Some(item) = self.current_item_mut() {
2705 if let SettingControl::Number(ref mut n) = item.control {
2706 n.move_left_selecting();
2707 }
2708 }
2709 }
2710
2711 pub fn number_move_right_selecting(&mut self) {
2713 if let Some(item) = self.current_item_mut() {
2714 if let SettingControl::Number(ref mut n) = item.control {
2715 n.move_right_selecting();
2716 }
2717 }
2718 }
2719
2720 pub fn number_move_home_selecting(&mut self) {
2722 if let Some(item) = self.current_item_mut() {
2723 if let SettingControl::Number(ref mut n) = item.control {
2724 n.move_home_selecting();
2725 }
2726 }
2727 }
2728
2729 pub fn number_move_end_selecting(&mut self) {
2731 if let Some(item) = self.current_item_mut() {
2732 if let SettingControl::Number(ref mut n) = item.control {
2733 n.move_end_selecting();
2734 }
2735 }
2736 }
2737
2738 pub fn number_move_word_left(&mut self) {
2740 if let Some(item) = self.current_item_mut() {
2741 if let SettingControl::Number(ref mut n) = item.control {
2742 n.move_word_left();
2743 }
2744 }
2745 }
2746
2747 pub fn number_move_word_right(&mut self) {
2749 if let Some(item) = self.current_item_mut() {
2750 if let SettingControl::Number(ref mut n) = item.control {
2751 n.move_word_right();
2752 }
2753 }
2754 }
2755
2756 pub fn number_move_word_left_selecting(&mut self) {
2758 if let Some(item) = self.current_item_mut() {
2759 if let SettingControl::Number(ref mut n) = item.control {
2760 n.move_word_left_selecting();
2761 }
2762 }
2763 }
2764
2765 pub fn number_move_word_right_selecting(&mut self) {
2767 if let Some(item) = self.current_item_mut() {
2768 if let SettingControl::Number(ref mut n) = item.control {
2769 n.move_word_right_selecting();
2770 }
2771 }
2772 }
2773
2774 pub fn number_select_all(&mut self) {
2776 if let Some(item) = self.current_item_mut() {
2777 if let SettingControl::Number(ref mut n) = item.control {
2778 n.select_all();
2779 }
2780 }
2781 }
2782
2783 pub fn number_delete_word_backward(&mut self) {
2785 if let Some(item) = self.current_item_mut() {
2786 if let SettingControl::Number(ref mut n) = item.control {
2787 n.delete_word_backward();
2788 }
2789 }
2790 }
2791
2792 pub fn number_delete_word_forward(&mut self) {
2794 if let Some(item) = self.current_item_mut() {
2795 if let SettingControl::Number(ref mut n) = item.control {
2796 n.delete_word_forward();
2797 }
2798 }
2799 }
2800
2801 pub fn get_change_descriptions(&self) -> Vec<String> {
2803 let mut descriptions: Vec<String> = self
2804 .pending_changes
2805 .iter()
2806 .map(|(path, value)| {
2807 let value_str = match value {
2808 serde_json::Value::Bool(b) => b.to_string(),
2809 serde_json::Value::Number(n) => n.to_string(),
2810 serde_json::Value::String(s) => format!("\"{}\"", s),
2811 _ => value.to_string(),
2812 };
2813 format!("{}: {}", path, value_str)
2814 })
2815 .collect();
2816 for path in &self.pending_deletions {
2818 descriptions.push(format!("{}: (reset to default)", path));
2819 }
2820 descriptions.sort();
2821 descriptions
2822 }
2823}
2824
2825fn update_control_from_value(control: &mut SettingControl, value: &serde_json::Value) {
2827 match control {
2828 SettingControl::Toggle(state) => {
2829 if let Some(b) = value.as_bool() {
2830 state.checked = b;
2831 }
2832 }
2833 SettingControl::Number(state) => {
2834 if let Some(n) = value.as_i64() {
2835 state.value = n;
2836 }
2837 }
2838 SettingControl::Dropdown(state) => {
2839 if let Some(s) = value.as_str() {
2840 if let Some(idx) = state.options.iter().position(|o| o == s) {
2841 state.selected = idx;
2842 }
2843 }
2844 }
2845 SettingControl::Text(state) => {
2846 if let Some(s) = value.as_str() {
2847 state.value = s.to_string();
2848 state.cursor = state.value.len();
2849 }
2850 }
2851 SettingControl::TextList(state) => {
2852 if let Some(arr) = value.as_array() {
2853 state.items = arr
2854 .iter()
2855 .filter_map(|v| {
2856 if state.is_integer {
2857 v.as_i64()
2858 .map(|n| n.to_string())
2859 .or_else(|| v.as_u64().map(|n| n.to_string()))
2860 .or_else(|| v.as_f64().map(|n| n.to_string()))
2861 } else {
2862 v.as_str().map(String::from)
2863 }
2864 })
2865 .collect();
2866 }
2867 }
2868 SettingControl::DualList(state) => {
2869 if let Some(arr) = value.as_array() {
2870 state.included = arr
2871 .iter()
2872 .filter_map(|v| v.as_str().map(String::from))
2873 .collect();
2874 }
2875 }
2876 SettingControl::Map(state) => {
2877 if let Some(obj) = value.as_object() {
2878 state.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
2879 state.entries.sort_by(|a, b| a.0.cmp(&b.0));
2880 }
2881 }
2882 SettingControl::ObjectArray(state) => {
2883 if let Some(arr) = value.as_array() {
2884 state.bindings = arr.clone();
2885 }
2886 }
2887 SettingControl::Json(state) => {
2888 let json_str =
2890 serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_string());
2891 let json_str = if json_str.is_empty() {
2892 "null".to_string()
2893 } else {
2894 json_str
2895 };
2896 state.original_text = json_str.clone();
2897 state.editor.set_value(&json_str);
2898 state.scroll_offset = 0;
2899 }
2900 SettingControl::Complex { .. } => {}
2901 }
2902}
2903
2904#[cfg(test)]
2905mod tests {
2906 use super::*;
2907
2908 const TEST_SCHEMA: &str = r#"
2909{
2910 "type": "object",
2911 "properties": {
2912 "theme": {
2913 "type": "string",
2914 "default": "dark"
2915 },
2916 "line_numbers": {
2917 "type": "boolean",
2918 "default": true
2919 }
2920 },
2921 "$defs": {}
2922}
2923"#;
2924
2925 fn test_config() -> Config {
2926 Config::default()
2927 }
2928
2929 #[test]
2930 fn test_settings_state_creation() {
2931 let config = test_config();
2932 let state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2933
2934 assert!(!state.visible);
2935 assert_eq!(state.selected_category, 0);
2936 assert!(!state.has_changes());
2937 }
2938
2939 #[test]
2940 fn test_navigation() {
2941 let config = test_config();
2942 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2943
2944 assert_eq!(state.focus_panel(), FocusPanel::Categories);
2946
2947 state.toggle_focus();
2949 assert_eq!(state.focus_panel(), FocusPanel::Settings);
2950
2951 state.select_next();
2953 assert_eq!(state.selected_item, 1);
2954
2955 state.select_prev();
2956 assert_eq!(state.selected_item, 0);
2957 }
2958
2959 #[test]
2960 fn test_pending_changes() {
2961 let config = test_config();
2962 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2963
2964 assert!(!state.has_changes());
2965
2966 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
2967 assert!(state.has_changes());
2968
2969 state.discard_changes();
2970 assert!(!state.has_changes());
2971 }
2972
2973 #[test]
2974 fn test_show_hide() {
2975 let config = test_config();
2976 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2977
2978 assert!(!state.visible);
2979
2980 state.show();
2981 assert!(state.visible);
2982 assert_eq!(state.focus_panel(), FocusPanel::Categories);
2983
2984 state.hide();
2985 assert!(!state.visible);
2986 }
2987
2988 const TEST_SCHEMA_CONTROLS: &str = r#"
2990{
2991 "type": "object",
2992 "properties": {
2993 "theme": {
2994 "type": "string",
2995 "enum": ["dark", "light", "high-contrast"],
2996 "default": "dark"
2997 },
2998 "tab_size": {
2999 "type": "integer",
3000 "minimum": 1,
3001 "maximum": 8,
3002 "default": 4
3003 },
3004 "line_numbers": {
3005 "type": "boolean",
3006 "default": true
3007 }
3008 },
3009 "$defs": {}
3010}
3011"#;
3012
3013 #[test]
3014 fn test_dropdown_toggle() {
3015 let config = test_config();
3016 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3017 state.show();
3018 state.toggle_focus(); state.select_next();
3023 state.select_next();
3024 assert!(!state.is_dropdown_open());
3025
3026 state.dropdown_toggle();
3027 assert!(state.is_dropdown_open());
3028
3029 state.dropdown_toggle();
3030 assert!(!state.is_dropdown_open());
3031 }
3032
3033 #[test]
3034 fn test_dropdown_cancel_restores() {
3035 let config = test_config();
3036 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3037 state.show();
3038 state.toggle_focus();
3039
3040 state.select_next();
3043 state.select_next();
3044
3045 state.dropdown_toggle();
3047 assert!(state.is_dropdown_open());
3048
3049 let initial = state.current_item().and_then(|item| {
3051 if let SettingControl::Dropdown(ref d) = item.control {
3052 Some(d.selected)
3053 } else {
3054 None
3055 }
3056 });
3057
3058 state.dropdown_next();
3060 let after_change = state.current_item().and_then(|item| {
3061 if let SettingControl::Dropdown(ref d) = item.control {
3062 Some(d.selected)
3063 } else {
3064 None
3065 }
3066 });
3067 assert_ne!(initial, after_change);
3068
3069 state.dropdown_cancel();
3071 assert!(!state.is_dropdown_open());
3072
3073 let after_cancel = state.current_item().and_then(|item| {
3074 if let SettingControl::Dropdown(ref d) = item.control {
3075 Some(d.selected)
3076 } else {
3077 None
3078 }
3079 });
3080 assert_eq!(initial, after_cancel);
3081 }
3082
3083 #[test]
3084 fn test_dropdown_confirm_keeps_selection() {
3085 let config = test_config();
3086 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3087 state.show();
3088 state.toggle_focus();
3089
3090 state.dropdown_toggle();
3092
3093 state.dropdown_next();
3095 let after_change = state.current_item().and_then(|item| {
3096 if let SettingControl::Dropdown(ref d) = item.control {
3097 Some(d.selected)
3098 } else {
3099 None
3100 }
3101 });
3102
3103 state.dropdown_confirm();
3105 assert!(!state.is_dropdown_open());
3106
3107 let after_confirm = state.current_item().and_then(|item| {
3108 if let SettingControl::Dropdown(ref d) = item.control {
3109 Some(d.selected)
3110 } else {
3111 None
3112 }
3113 });
3114 assert_eq!(after_change, after_confirm);
3115 }
3116
3117 #[test]
3118 fn test_number_editing() {
3119 let config = test_config();
3120 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3121 state.show();
3122 state.toggle_focus();
3123
3124 state.select_next();
3126
3127 assert!(!state.is_number_editing());
3129
3130 state.start_number_editing();
3132 assert!(state.is_number_editing());
3133
3134 state.number_insert('8');
3136
3137 state.number_confirm();
3139 assert!(!state.is_number_editing());
3140 }
3141
3142 #[test]
3143 fn test_number_cancel_editing() {
3144 let config = test_config();
3145 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3146 state.show();
3147 state.toggle_focus();
3148
3149 state.select_next();
3151
3152 let initial_value = state.current_item().and_then(|item| {
3154 if let SettingControl::Number(ref n) = item.control {
3155 Some(n.value)
3156 } else {
3157 None
3158 }
3159 });
3160
3161 state.start_number_editing();
3163 state.number_backspace();
3164 state.number_insert('9');
3165 state.number_insert('9');
3166
3167 state.number_cancel();
3169 assert!(!state.is_number_editing());
3170
3171 let after_cancel = state.current_item().and_then(|item| {
3173 if let SettingControl::Number(ref n) = item.control {
3174 Some(n.value)
3175 } else {
3176 None
3177 }
3178 });
3179 assert_eq!(initial_value, after_cancel);
3180 }
3181
3182 #[test]
3183 fn test_number_backspace() {
3184 let config = test_config();
3185 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3186 state.show();
3187 state.toggle_focus();
3188 state.select_next();
3189
3190 state.start_number_editing();
3191 state.number_backspace();
3192
3193 let display_text = state.current_item().and_then(|item| {
3195 if let SettingControl::Number(ref n) = item.control {
3196 Some(n.display_text())
3197 } else {
3198 None
3199 }
3200 });
3201 assert_eq!(display_text, Some(String::new()));
3203
3204 state.number_cancel();
3205 }
3206
3207 #[test]
3208 fn test_layer_selection() {
3209 let config = test_config();
3210 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3211
3212 assert_eq!(state.target_layer, ConfigLayer::User);
3214 assert_eq!(state.target_layer_name(), "User");
3215
3216 state.cycle_target_layer();
3218 assert_eq!(state.target_layer, ConfigLayer::Project);
3219 assert_eq!(state.target_layer_name(), "Project");
3220
3221 state.cycle_target_layer();
3222 assert_eq!(state.target_layer, ConfigLayer::Session);
3223 assert_eq!(state.target_layer_name(), "Session");
3224
3225 state.cycle_target_layer();
3226 assert_eq!(state.target_layer, ConfigLayer::User);
3227
3228 state.set_target_layer(ConfigLayer::Project);
3230 assert_eq!(state.target_layer, ConfigLayer::Project);
3231
3232 state.set_target_layer(ConfigLayer::System);
3234 assert_eq!(state.target_layer, ConfigLayer::Project);
3235 }
3236
3237 #[test]
3238 fn test_layer_switch_clears_pending_changes() {
3239 let config = test_config();
3240 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3241
3242 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
3244 assert!(state.has_changes());
3245
3246 state.cycle_target_layer();
3248 assert!(!state.has_changes());
3249 }
3250
3251 #[test]
3270 fn nested_array_save_records_full_entry_path() {
3271 use crate::view::settings::schema::SettingType;
3274
3275 let config = test_config();
3276 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3277
3278 let item_schema = SettingSchema {
3280 path: "/item".to_string(),
3281 name: "Server".to_string(),
3282 description: None,
3283 setting_type: SettingType::Object {
3284 properties: vec![SettingSchema {
3285 path: "/enabled".to_string(),
3286 name: "Enabled".to_string(),
3287 description: None,
3288 setting_type: SettingType::Boolean,
3289 default: Some(serde_json::json!(false)),
3290 read_only: false,
3291 section: None,
3292 order: None,
3293 nullable: false,
3294 enum_from: None,
3295 dual_list_sibling: None,
3296 dynamically_extendable_status_bar_elements: false,
3297 }],
3298 },
3299 default: None,
3300 read_only: false,
3301 section: None,
3302 order: None,
3303 nullable: false,
3304 enum_from: None,
3305 dual_list_sibling: None,
3306 dynamically_extendable_status_bar_elements: false,
3307 };
3308
3309 let value_schema = SettingSchema {
3314 path: String::new(),
3315 name: "value".to_string(),
3316 description: None,
3317 setting_type: SettingType::ObjectArray {
3318 item_schema: Box::new(item_schema.clone()),
3319 display_field: None,
3320 },
3321 default: None,
3322 read_only: false,
3323 section: None,
3324 order: None,
3325 nullable: false,
3326 enum_from: None,
3327 dual_list_sibling: None,
3328 dynamically_extendable_status_bar_elements: false,
3329 };
3330
3331 let parent = EntryDialogState::from_schema(
3335 "quicklsp".to_string(),
3336 &serde_json::json!([{ "enabled": true }]),
3337 &value_schema,
3338 "/universal_lsp",
3339 false, false,
3341 &HashMap::new(),
3342 );
3343
3344 assert!(
3346 parent.is_single_value,
3347 "array value_schema should trigger is_single_value path"
3348 );
3349 assert_eq!(parent.entry_path(), "/universal_lsp/quicklsp");
3350
3351 state.entry_dialog_stack.push(parent);
3352
3353 state.open_nested_entry_dialog();
3358
3359 assert_eq!(
3361 state.entry_dialog_stack.len(),
3362 2,
3363 "open_nested_entry_dialog should have pushed a nested dialog"
3364 );
3365
3366 let nested_map_path = state
3369 .entry_dialog_stack
3370 .last()
3371 .map(|d| d.map_path.clone())
3372 .unwrap();
3373 assert_eq!(
3374 nested_map_path, "/universal_lsp/quicklsp",
3375 "BUG: nested dialog's map_path dropped the 'quicklsp' key segment"
3376 );
3377
3378 state.save_entry_dialog();
3380
3381 assert_eq!(state.entry_dialog_stack.len(), 1);
3383
3384 assert!(
3387 !state.pending_changes.contains_key("/universal_lsp/"),
3388 "regression: pending change recorded under empty-key path /universal_lsp/. \
3389 All keys: {:?}",
3390 state.pending_changes.keys().collect::<Vec<_>>()
3391 );
3392 assert!(
3393 !state
3394 .pending_changes
3395 .keys()
3396 .any(|k| k.starts_with("/universal_lsp") && k.ends_with('/')),
3397 "no /universal_lsp/* path should end in a trailing slash; got {:?}",
3398 state.pending_changes.keys().collect::<Vec<_>>()
3399 );
3400 assert!(
3401 state
3402 .pending_changes
3403 .contains_key("/universal_lsp/quicklsp"),
3404 "expected pending change at /universal_lsp/quicklsp, got {:?}",
3405 state.pending_changes.keys().collect::<Vec<_>>()
3406 );
3407 }
3408
3409 #[test]
3410 fn test_refresh_dual_list_sibling_updates_excluded() {
3411 use crate::view::controls::DualListState;
3412
3413 let schema = include_str!("../../../plugins/config-schema.json");
3416 let config = test_config();
3417 let mut state = SettingsState::new(schema, &config).unwrap();
3418
3419 let editor_page_idx = state
3421 .pages
3422 .iter()
3423 .position(|p| p.path == "/editor")
3424 .expect("editor page");
3425 state.selected_category = editor_page_idx;
3426
3427 let (left_idx, right_idx) = {
3428 let page = &state.pages[editor_page_idx];
3429 let l = page
3430 .items
3431 .iter()
3432 .position(|i| i.path == "/editor/status_bar/left")
3433 .expect("left item");
3434 let r = page
3435 .items
3436 .iter()
3437 .position(|i| i.path == "/editor/status_bar/right")
3438 .expect("right item");
3439 (l, r)
3440 };
3441
3442 assert!(matches!(
3444 &state.pages[editor_page_idx].items[left_idx].control,
3445 SettingControl::DualList(_)
3446 ));
3447
3448 let default_right_items: Vec<String> =
3450 match &state.pages[editor_page_idx].items[right_idx].control {
3451 SettingControl::DualList(dl) => dl.included.clone(),
3452 _ => panic!("right should be DualList"),
3453 };
3454 let initial_left_excluded: Vec<String> =
3455 match &state.pages[editor_page_idx].items[left_idx].control {
3456 SettingControl::DualList(dl) => dl.excluded.clone(),
3457 _ => panic!("left should be DualList"),
3458 };
3459 assert_eq!(
3460 initial_left_excluded, default_right_items,
3461 "left.excluded should mirror right's included on initial build"
3462 );
3463
3464 let new_element = "{chord}".to_string();
3466 state.selected_item = left_idx;
3467 state
3468 .with_current_dual_list_mut(|dl: &mut DualListState| {
3469 if !dl.included.contains(&new_element) {
3470 dl.included.push(new_element.clone());
3471 }
3472 })
3473 .expect("current item is a DualList");
3474
3475 state.refresh_dual_list_sibling();
3477
3478 match &state.pages[editor_page_idx].items[right_idx].control {
3479 SettingControl::DualList(dl) => {
3480 assert!(
3481 dl.excluded.contains(&new_element),
3482 "right.excluded should be updated to reflect left's new inclusion"
3483 );
3484 }
3485 _ => panic!("right should be DualList"),
3486 }
3487 }
3488
3489 #[test]
3490 fn test_with_dual_list_mut_returns_none_for_non_dual_list() {
3491 let config = test_config();
3492 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3493
3494 let result = state.with_dual_list_mut(0, |_| ());
3496 assert!(result.is_none());
3497 }
3498}