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 if matches!(rows[target], TreeRow::Section { .. }) {
529 self.scroll_panel.update_content_height(&page.items, width);
530 let content_width = self.scroll_panel.content_width(width);
531 let item_y =
532 self.scroll_panel
533 .item_y_offset(&page.items, self.selected_item, content_width);
534 self.scroll_panel.scroll.offset = item_y;
535 } else {
536 let selected_item = self.selected_item;
537 let sub_focus = self.sub_focus;
538 self.scroll_panel.ensure_focused_visible(
539 &page.items,
540 selected_item,
541 sub_focus,
542 width,
543 );
544 }
545 }
546 let new_rows = self.visible_tree();
547 let new_cur = self.tree_cursor_index(&new_rows);
548 self.categories_scroll
549 .ensure_focused_visible(&new_rows, new_cur, None, width);
550 }
551
552 pub(super) fn tree_cursor_index(&self, rows: &[TreeRow]) -> usize {
560 let cat = self.selected_category;
561 if let Some(s_idx) = self.tree_cursor_section {
562 for (i, row) in rows.iter().enumerate() {
563 if let TreeRow::Section {
564 cat_idx,
565 section_idx,
566 } = *row
567 {
568 if cat_idx == cat && section_idx == s_idx {
569 return i;
570 }
571 }
572 }
573 }
574 for (i, row) in rows.iter().enumerate() {
575 if let TreeRow::Category { idx, .. } = *row {
576 if idx == cat {
577 return i;
578 }
579 }
580 }
581 0
582 }
583
584 pub fn auto_expand_current_category(&mut self) {
594 let idx = self.selected_category;
595 if self.is_category_expandable(idx) {
596 self.expanded_categories.insert(idx);
597 }
598 }
599
600 pub fn toggle_category_expanded(&mut self, cat_idx: usize) {
601 if !self.is_category_expandable(cat_idx) {
602 return;
603 }
604 if !self.expanded_categories.insert(cat_idx) {
605 self.expanded_categories.remove(&cat_idx);
606 }
607 }
608
609 pub fn jump_to_section(&mut self, cat_idx: usize, section_idx: usize) {
613 let Some(page) = self.pages.get(cat_idx) else {
614 return;
615 };
616 let Some(section) = page.sections.get(section_idx) else {
617 return;
618 };
619 let target_item = section.first_item_index;
620 self.update_control_focus(false);
621 self.selected_category = cat_idx;
622 self.selected_item = target_item;
623 self.tree_cursor_section = Some(section_idx);
624 self.focus.set(FocusPanel::Settings);
625 let width = self.layout_width;
626 if let Some(page) = self.pages.get(self.selected_category) {
627 self.scroll_panel.update_content_height(&page.items, width);
628 let content_width = self.scroll_panel.content_width(width);
629 let item_y = self
636 .scroll_panel
637 .item_y_offset(&page.items, target_item, content_width);
638 self.scroll_panel.scroll.offset = item_y;
639 }
640 self.sub_focus = None;
641 self.init_map_focus(true);
642 self.update_control_focus(true);
643 self.auto_expand_current_category();
644 }
645
646 pub fn visible_tree(&self) -> Vec<TreeRow> {
650 let mut rows = Vec::with_capacity(self.pages.len());
651 for (idx, page) in self.pages.iter().enumerate() {
652 let expandable = page.sections.len() > 1;
653 let expanded = expandable && self.expanded_categories.contains(&idx);
654 rows.push(TreeRow::Category {
655 idx,
656 expandable,
657 expanded,
658 });
659 if expanded {
660 for section_idx in 0..page.sections.len() {
661 rows.push(TreeRow::Section {
662 cat_idx: idx,
663 section_idx,
664 });
665 }
666 }
667 }
668 rows
669 }
670
671 pub fn current_item(&self) -> Option<&SettingItem> {
673 self.current_page()
674 .and_then(|page| page.items.get(self.selected_item))
675 }
676
677 pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
679 self.pages
680 .get_mut(self.selected_category)
681 .and_then(|page| page.items.get_mut(self.selected_item))
682 }
683
684 pub fn can_exit_text_editing(&self) -> bool {
686 self.current_item()
687 .map(|item| {
688 if let SettingControl::Text(state) = &item.control {
689 state.is_valid()
690 } else {
691 true
692 }
693 })
694 .unwrap_or(true)
695 }
696
697 pub fn entry_dialog_can_exit_text_editing(&self) -> bool {
699 self.entry_dialog()
700 .and_then(|dialog| dialog.current_item())
701 .map(|item| {
702 if let SettingControl::Text(state) = &item.control {
703 state.is_valid()
704 } else {
705 true
706 }
707 })
708 .unwrap_or(true)
709 }
710
711 fn init_map_focus(&mut self, from_above: bool) {
714 if let Some(item) = self.current_item_mut() {
715 if let SettingControl::Map(ref mut map_state) = item.control {
716 map_state.init_focus(from_above);
717 }
718 }
719 self.update_map_sub_focus();
721 }
722
723 pub(super) fn update_control_focus(&mut self, focused: bool) {
727 let focus_state = if focused {
728 FocusState::Focused
729 } else {
730 FocusState::Normal
731 };
732 if let Some(item) = self.current_item_mut() {
733 match &mut item.control {
734 SettingControl::Map(ref mut state) => state.focus = focus_state,
735 SettingControl::TextList(ref mut state) => state.focus = focus_state,
736 SettingControl::DualList(ref mut state) => state.focus = focus_state,
737 SettingControl::ObjectArray(ref mut state) => state.focus = focus_state,
738 SettingControl::Toggle(ref mut state) => state.focus = focus_state,
739 SettingControl::Number(ref mut state) => state.focus = focus_state,
740 SettingControl::Dropdown(ref mut state) => state.focus = focus_state,
741 SettingControl::Text(ref mut state) => {
742 state.focus = focus_state;
743 if !focused {
747 state.editing = false;
748 }
749 }
750 SettingControl::Json(_) | SettingControl::Complex { .. } => {} }
752 }
753 }
754
755 fn update_map_sub_focus(&mut self) {
758 self.sub_focus = self.current_item().and_then(|item| {
759 if let SettingControl::Map(ref map_state) = item.control {
760 Some(match map_state.focused_entry {
762 Some(i) => 1 + i,
763 None => 1 + map_state.entries.len(), })
765 } else {
766 None
767 }
768 });
769 }
770
771 pub fn select_prev(&mut self) {
773 match self.focus_panel() {
774 FocusPanel::Categories => {
775 self.tree_step(-1);
776 }
777 FocusPanel::Settings => {
778 let handled = self
780 .current_item_mut()
781 .and_then(|item| match &mut item.control {
782 SettingControl::Map(map_state) => Some(map_state.focus_prev()),
783 _ => None,
784 })
785 .unwrap_or(false);
786
787 if handled {
788 self.update_map_sub_focus();
790 } else if self.selected_item > 0 {
791 self.update_control_focus(false); self.selected_item -= 1;
793 self.sub_focus = None;
794 self.init_map_focus(false); self.update_control_focus(true); }
797 self.ensure_visible();
798 }
799 FocusPanel::Footer => {
800 if self.footer_button_index > 0 {
802 self.footer_button_index -= 1;
803 }
804 }
805 }
806 }
807
808 pub fn select_next(&mut self) {
810 match self.focus_panel() {
811 FocusPanel::Categories => {
812 self.tree_step(1);
813 }
814 FocusPanel::Settings => {
815 let handled = self
817 .current_item_mut()
818 .and_then(|item| match &mut item.control {
819 SettingControl::Map(map_state) => Some(map_state.focus_next()),
820 _ => None,
821 })
822 .unwrap_or(false);
823
824 if handled {
825 self.update_map_sub_focus();
827 } else {
828 let can_move = self
829 .current_page()
830 .is_some_and(|page| self.selected_item + 1 < page.items.len());
831 if can_move {
832 self.update_control_focus(false); self.selected_item += 1;
834 self.sub_focus = None;
835 self.init_map_focus(true); self.update_control_focus(true); }
838 }
839 self.ensure_visible();
840 }
841 FocusPanel::Footer => {
842 if self.footer_button_index < 2 {
844 self.footer_button_index += 1;
845 }
846 }
847 }
848 }
849
850 pub fn select_next_page(&mut self) {
852 let page_size = self.scroll_panel.viewport_height().max(1);
853 for _ in 0..page_size {
854 self.select_next();
855 }
856 }
857
858 pub fn select_prev_page(&mut self) {
860 let page_size = self.scroll_panel.viewport_height().max(1);
861 for _ in 0..page_size {
862 self.select_prev();
863 }
864 }
865
866 pub fn toggle_focus(&mut self) {
868 let old_panel = self.focus_panel();
869 self.focus.focus_next();
870 self.on_panel_changed(old_panel, true);
871 }
872
873 pub fn toggle_focus_backward(&mut self) {
875 let old_panel = self.focus_panel();
876 self.focus.focus_prev();
877 self.on_panel_changed(old_panel, false);
878 }
879
880 fn on_panel_changed(&mut self, old_panel: FocusPanel, forward: bool) {
882 if old_panel == FocusPanel::Settings {
884 self.update_control_focus(false);
885 }
886
887 if self.focus_panel() == FocusPanel::Settings
889 && self.selected_item >= self.current_page().map_or(0, |p| p.items.len())
890 {
891 self.selected_item = 0;
892 }
893 self.sub_focus = None;
894
895 if self.focus_panel() == FocusPanel::Settings {
896 self.init_map_focus(forward); self.update_control_focus(true); }
899
900 if self.focus_panel() == FocusPanel::Footer {
902 self.footer_button_index = if forward {
903 0 } else {
905 4 };
907 }
908
909 self.ensure_visible();
910 }
911
912 pub fn set_item_style(&mut self, style: super::items::ItemBoxStyle) {
920 if self.item_style == style {
921 return;
922 }
923 self.item_style = style;
924 for page in &mut self.pages {
925 for item in &mut page.items {
926 item.style = style;
927 }
928 }
929 }
930
931 pub fn ensure_visible(&mut self) {
933 if self.focus_panel() != FocusPanel::Settings {
934 return;
935 }
936
937 let selected_item = self.selected_item;
939 let sub_focus = self.sub_focus;
940 let width = self.layout_width;
941 let prev_offset = self.scroll_panel.scroll.offset;
942 if let Some(page) = self.pages.get(self.selected_category) {
943 self.scroll_panel
944 .ensure_focused_visible(&page.items, selected_item, sub_focus, width);
945 }
946 if self.scroll_panel.scroll.offset != prev_offset {
950 self.sync_tree_cursor_to_body_scroll();
951 }
952 }
953
954 pub fn set_pending_change(&mut self, path: &str, value: serde_json::Value) {
956 let original = self.original_config.pointer(path);
958 if original == Some(&value) {
959 self.pending_changes.remove(path);
960 } else {
961 self.pending_changes.insert(path.to_string(), value);
962 }
963 }
964
965 pub fn has_changes(&self) -> bool {
967 !self.pending_changes.is_empty() || !self.pending_deletions.is_empty()
968 }
969
970 pub fn apply_changes(&self, config: &Config) -> Result<Config, serde_json::Error> {
972 let mut config_value = serde_json::to_value(config)?;
973
974 for path in &self.pending_deletions {
984 crate::config_io::remove_json_pointer(&mut config_value, path);
985 }
986
987 for (path, value) in &self.pending_changes {
988 if let Some(target) = config_value.pointer_mut(path) {
995 *target = value.clone();
996 } else {
997 set_json_pointer_create(&mut config_value, path, value.clone());
998 }
999 }
1000
1001 serde_json::from_value(config_value)
1002 }
1003
1004 pub fn discard_changes(&mut self) {
1006 self.pending_changes.clear();
1007 self.pending_deletions.clear();
1008 self.rebuild_pages();
1010 }
1011
1012 pub fn set_target_layer(&mut self, layer: ConfigLayer) {
1014 if layer != ConfigLayer::System {
1015 self.target_layer = layer;
1017 self.pending_changes.clear();
1019 self.pending_deletions.clear();
1020 self.rebuild_pages();
1022 }
1023 }
1024
1025 pub fn cycle_target_layer(&mut self) {
1027 self.target_layer = match self.target_layer {
1028 ConfigLayer::System => ConfigLayer::User, ConfigLayer::User => ConfigLayer::Project,
1030 ConfigLayer::Project => ConfigLayer::Session,
1031 ConfigLayer::Session => ConfigLayer::User,
1032 };
1033 self.pending_changes.clear();
1035 self.pending_deletions.clear();
1036 self.rebuild_pages();
1038 }
1039
1040 pub fn target_layer_name(&self) -> &'static str {
1042 match self.target_layer {
1043 ConfigLayer::System => "System (read-only)",
1044 ConfigLayer::User => "User",
1045 ConfigLayer::Project => "Project",
1046 ConfigLayer::Session => "Session",
1047 }
1048 }
1049
1050 pub fn set_layer_sources(&mut self, sources: HashMap<String, ConfigLayer>) {
1053 self.layer_sources = sources;
1054 self.rebuild_pages();
1056 }
1057
1058 pub fn set_status_bar_tokens(&mut self, tokens: HashMap<String, String>) {
1061 self.available_status_bar_tokens = tokens;
1062 self.rebuild_pages();
1063 }
1064
1065 pub fn get_layer_source(&self, path: &str) -> ConfigLayer {
1068 self.layer_sources
1069 .get(path)
1070 .copied()
1071 .unwrap_or(ConfigLayer::System)
1072 }
1073
1074 pub fn layer_source_label(layer: ConfigLayer) -> &'static str {
1076 match layer {
1077 ConfigLayer::System => "default",
1078 ConfigLayer::User => "user",
1079 ConfigLayer::Project => "project",
1080 ConfigLayer::Session => "session",
1081 }
1082 }
1083
1084 pub fn reset_focused_entry_field(&mut self) {
1099 let Some(dialog) = self.entry_dialog_mut() else {
1100 return;
1101 };
1102 if dialog.focus_on_buttons {
1103 return;
1104 }
1105 let idx = dialog.selected_item;
1106 let Some(item) = dialog.items.get_mut(idx) else {
1107 return;
1108 };
1109 if item.read_only {
1110 return;
1111 }
1112 let Some(default) = item.default.clone() else {
1113 return;
1114 };
1115 update_control_from_value(&mut item.control, &default);
1116 item.modified = false;
1117 dialog.user_edited = true;
1118 }
1119
1120 pub fn reset_current_to_default(&mut self) {
1121 let reset_info = self.current_item().and_then(|item| {
1123 if !item.modified || item.is_auto_managed {
1126 return None;
1127 }
1128 item.default
1129 .as_ref()
1130 .map(|default| (item.path.clone(), default.clone()))
1131 });
1132
1133 if let Some((path, default)) = reset_info {
1134 self.pending_deletions.insert(path.clone());
1136 self.pending_changes.remove(&path);
1138
1139 if let Some(item) = self.current_item_mut() {
1143 update_control_from_value(&mut item.control, &default);
1144 item.modified = false;
1145 item.layer_source = ConfigLayer::System; }
1148 }
1149 }
1150
1151 pub fn set_current_to_null(&mut self) {
1157 let target_layer = self.target_layer;
1158 let change_info = self.current_item().and_then(|item| {
1159 if !item.nullable || item.is_null || item.read_only {
1160 return None;
1161 }
1162 Some(item.path.clone())
1163 });
1164
1165 if let Some(path) = change_info {
1166 self.pending_changes
1168 .insert(path.clone(), serde_json::Value::Null);
1169 self.pending_deletions.remove(&path);
1170
1171 if let Some(item) = self.current_item_mut() {
1173 item.is_null = true;
1174 item.modified = true;
1175 item.layer_source = target_layer;
1176 }
1177 }
1178 }
1179
1180 pub fn clear_current_category(&mut self) {
1186 let target_layer = self.target_layer;
1187 let page = match self.current_page() {
1188 Some(p) if p.nullable => p,
1189 _ => return,
1190 };
1191 let page_path = page.path.clone();
1192
1193 self.pending_changes
1195 .insert(page_path.clone(), serde_json::Value::Null);
1196
1197 let prefix = format!("{}/", page_path);
1199 self.pending_changes
1200 .retain(|path, _| !path.starts_with(&prefix));
1201 self.pending_deletions
1202 .retain(|path| !path.starts_with(&prefix));
1203
1204 if let Some(page) = self.current_page_mut() {
1206 for item in &mut page.items {
1207 if item.nullable {
1208 item.is_null = true;
1209 item.modified = false;
1210 item.layer_source = target_layer;
1211 }
1212 }
1213 }
1214 }
1215
1216 pub fn current_category_has_values(&self) -> bool {
1218 match self.current_page() {
1219 Some(page) if page.nullable => {
1220 page.items.iter().any(|item| !item.is_null && item.nullable)
1221 || page.items.iter().any(|item| item.modified)
1222 }
1223 _ => false,
1224 }
1225 }
1226
1227 pub fn on_value_changed(&mut self) {
1229 let target_layer = self.target_layer;
1231
1232 let change_info = self.current_item().map(|item| {
1234 let value = control_to_value(&item.control);
1235 (item.path.clone(), value)
1236 });
1237
1238 if let Some((path, value)) = change_info {
1239 self.pending_deletions.remove(&path);
1242
1243 if let Some(item) = self.current_item_mut() {
1245 item.modified = true; item.layer_source = target_layer; item.is_null = false; }
1249 self.set_pending_change(&path, value);
1250 }
1251 }
1252
1253 pub fn update_focus_states(&mut self) {
1255 let current_focus = self.focus_panel();
1256 for (page_idx, page) in self.pages.iter_mut().enumerate() {
1257 for (item_idx, item) in page.items.iter_mut().enumerate() {
1258 let is_focused = current_focus == FocusPanel::Settings
1259 && page_idx == self.selected_category
1260 && item_idx == self.selected_item;
1261
1262 let focus = if is_focused {
1263 FocusState::Focused
1264 } else {
1265 FocusState::Normal
1266 };
1267
1268 match &mut item.control {
1269 SettingControl::Toggle(state) => state.focus = focus,
1270 SettingControl::Number(state) => state.focus = focus,
1271 SettingControl::Dropdown(state) => state.focus = focus,
1272 SettingControl::Text(state) => state.focus = focus,
1273 SettingControl::TextList(state) => state.focus = focus,
1274 SettingControl::DualList(state) => state.focus = focus,
1275 SettingControl::Map(state) => state.focus = focus,
1276 SettingControl::ObjectArray(state) => state.focus = focus,
1277 SettingControl::Json(state) => state.focus = focus,
1278 SettingControl::Complex { .. } => {}
1279 }
1280 }
1281 }
1282 }
1283
1284 pub fn start_search(&mut self) {
1286 self.search_active = true;
1287 self.search_query.clear();
1288 self.search_results.clear();
1289 self.selected_search_result = 0;
1290 self.search_scroll_offset = 0;
1291 }
1292
1293 pub fn cancel_search(&mut self) {
1295 self.search_active = false;
1296 self.search_query.clear();
1297 self.search_results.clear();
1298 self.selected_search_result = 0;
1299 self.search_scroll_offset = 0;
1300 }
1301
1302 pub fn set_search_query(&mut self, query: String) {
1304 self.search_query = query;
1305 self.search_results = search_settings(&self.pages, &self.search_query);
1306 self.selected_search_result = 0;
1307 self.search_scroll_offset = 0;
1308 }
1309
1310 pub fn search_push_char(&mut self, c: char) {
1312 self.search_query.push(c);
1313 self.search_results = search_settings(&self.pages, &self.search_query);
1314 self.selected_search_result = 0;
1315 self.search_scroll_offset = 0;
1316 }
1317
1318 pub fn search_pop_char(&mut self) {
1320 self.search_query.pop();
1321 self.search_results = search_settings(&self.pages, &self.search_query);
1322 self.selected_search_result = 0;
1323 self.search_scroll_offset = 0;
1324 }
1325
1326 pub fn search_prev(&mut self) {
1328 if !self.search_results.is_empty() && self.selected_search_result > 0 {
1329 self.selected_search_result -= 1;
1330 if self.selected_search_result < self.search_scroll_offset {
1332 self.search_scroll_offset = self.selected_search_result;
1333 }
1334 }
1335 }
1336
1337 pub fn search_next(&mut self) {
1339 if !self.search_results.is_empty()
1340 && self.selected_search_result + 1 < self.search_results.len()
1341 {
1342 self.selected_search_result += 1;
1343 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1345 self.search_scroll_offset =
1346 self.selected_search_result - self.search_max_visible + 1;
1347 }
1348 }
1349 }
1350
1351 pub fn search_scroll_up(&mut self, delta: usize) -> bool {
1353 if self.search_results.is_empty() || self.search_scroll_offset == 0 {
1354 return false;
1355 }
1356 self.search_scroll_offset = self.search_scroll_offset.saturating_sub(delta);
1357 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1359 self.selected_search_result = self.search_scroll_offset + self.search_max_visible - 1;
1360 }
1361 true
1362 }
1363
1364 pub fn search_scroll_down(&mut self, delta: usize) -> bool {
1366 if self.search_results.is_empty() {
1367 return false;
1368 }
1369 let max_offset = self
1370 .search_results
1371 .len()
1372 .saturating_sub(self.search_max_visible);
1373 if self.search_scroll_offset >= max_offset {
1374 return false;
1375 }
1376 self.search_scroll_offset = (self.search_scroll_offset + delta).min(max_offset);
1377 if self.selected_search_result < self.search_scroll_offset {
1379 self.selected_search_result = self.search_scroll_offset;
1380 }
1381 true
1382 }
1383
1384 pub fn search_scroll_to_ratio(&mut self, ratio: f32) -> bool {
1386 if self.search_results.is_empty() {
1387 return false;
1388 }
1389 let max_offset = self
1390 .search_results
1391 .len()
1392 .saturating_sub(self.search_max_visible);
1393 let new_offset = (ratio * max_offset as f32) as usize;
1394 if new_offset != self.search_scroll_offset {
1395 self.search_scroll_offset = new_offset.min(max_offset);
1396 if self.selected_search_result < self.search_scroll_offset {
1398 self.selected_search_result = self.search_scroll_offset;
1399 } else if self.selected_search_result
1400 >= self.search_scroll_offset + self.search_max_visible
1401 {
1402 self.selected_search_result =
1403 self.search_scroll_offset + self.search_max_visible - 1;
1404 }
1405 return true;
1406 }
1407 false
1408 }
1409
1410 pub fn jump_to_search_result(&mut self) {
1412 let Some(result) = self
1414 .search_results
1415 .get(self.selected_search_result)
1416 .cloned()
1417 else {
1418 return;
1419 };
1420 let page_index = result.page_index;
1421 let item_index = result.item_index;
1422
1423 self.update_control_focus(false);
1425 self.selected_category = page_index;
1426 self.selected_item = item_index;
1427 self.focus.set(FocusPanel::Settings);
1428 self.scroll_panel.scroll.offset = 0;
1430 self.sub_focus = None;
1431 self.init_map_focus(true);
1432
1433 if let Some(ref deep_match) = result.deep_match {
1435 self.jump_to_deep_match(deep_match);
1436 }
1437
1438 self.update_control_focus(true); self.auto_expand_current_category();
1440 self.tree_cursor_section = self.current_section_index();
1444 self.ensure_visible();
1445 self.cancel_search();
1446 }
1447
1448 fn jump_to_deep_match(&mut self, deep_match: &DeepMatch) {
1450 match deep_match {
1451 DeepMatch::MapKey { entry_index, .. } | DeepMatch::MapValue { entry_index, .. } => {
1452 if let Some(item) = self.current_item_mut() {
1453 if let SettingControl::Map(ref mut map_state) = item.control {
1454 map_state.focused_entry = Some(*entry_index);
1455 }
1456 }
1457 self.update_map_sub_focus();
1458 }
1459 DeepMatch::TextListItem { item_index, .. } => {
1460 if let Some(item) = self.current_item_mut() {
1461 if let SettingControl::TextList(ref mut list_state) = item.control {
1462 list_state.focused_item = Some(*item_index);
1463 }
1464 }
1465 self.sub_focus = Some(1 + *item_index);
1467 }
1468 }
1469 }
1470
1471 pub fn current_search_result(&self) -> Option<&SearchResult> {
1473 self.search_results.get(self.selected_search_result)
1474 }
1475
1476 pub fn show_confirm_dialog(&mut self) {
1478 self.showing_confirm_dialog = true;
1479 self.confirm_dialog_selection = 0; }
1481
1482 pub fn hide_confirm_dialog(&mut self) {
1484 self.showing_confirm_dialog = false;
1485 self.confirm_dialog_selection = 0;
1486 }
1487
1488 pub fn confirm_dialog_next(&mut self) {
1490 self.confirm_dialog_selection = (self.confirm_dialog_selection + 1) % 3;
1491 }
1492
1493 pub fn confirm_dialog_prev(&mut self) {
1495 self.confirm_dialog_selection = if self.confirm_dialog_selection == 0 {
1496 2
1497 } else {
1498 self.confirm_dialog_selection - 1
1499 };
1500 }
1501
1502 pub fn toggle_help(&mut self) {
1504 self.showing_help = !self.showing_help;
1505 }
1506
1507 pub fn hide_help(&mut self) {
1509 self.showing_help = false;
1510 }
1511
1512 pub fn showing_entry_dialog(&self) -> bool {
1514 self.has_entry_dialog()
1515 }
1516
1517 pub fn open_entry_dialog(&mut self) {
1519 let Some(item) = self.current_item() else {
1520 return;
1521 };
1522
1523 let path = item.path.as_str();
1525 let SettingControl::Map(map_state) = &item.control else {
1526 return;
1527 };
1528
1529 let Some(entry_idx) = map_state.focused_entry else {
1531 return;
1532 };
1533 let Some((key, value)) = map_state.entries.get(entry_idx) else {
1534 return;
1535 };
1536
1537 let Some(schema) = map_state.value_schema.as_ref() else {
1539 return; };
1541
1542 let no_delete = map_state.no_add;
1544
1545 let dialog = EntryDialogState::from_schema(
1547 key.clone(),
1548 value,
1549 schema,
1550 path,
1551 false,
1552 no_delete,
1553 &self.available_status_bar_tokens,
1554 );
1555 self.entry_dialog_stack.push(dialog);
1556 }
1557
1558 pub fn open_add_entry_dialog(&mut self) {
1560 let Some(item) = self.current_item() else {
1561 return;
1562 };
1563 let SettingControl::Map(map_state) = &item.control else {
1564 return;
1565 };
1566 let Some(schema) = map_state.value_schema.as_ref() else {
1567 return;
1568 };
1569 let path = item.path.clone();
1570
1571 let dialog = EntryDialogState::from_schema(
1574 String::new(),
1575 &serde_json::json!({}),
1576 schema,
1577 &path,
1578 true,
1579 false,
1580 &self.available_status_bar_tokens,
1581 );
1582 self.entry_dialog_stack.push(dialog);
1583 }
1584
1585 pub fn open_add_array_item_dialog(&mut self) {
1587 let Some(item) = self.current_item() else {
1588 return;
1589 };
1590 let SettingControl::ObjectArray(array_state) = &item.control else {
1591 return;
1592 };
1593 let Some(schema) = array_state.item_schema.as_ref() else {
1594 return;
1595 };
1596 let path = item.path.clone();
1597
1598 let dialog = EntryDialogState::for_array_item(
1600 None,
1601 &serde_json::json!({}),
1602 schema,
1603 &path,
1604 true,
1605 &self.available_status_bar_tokens,
1606 );
1607 self.entry_dialog_stack.push(dialog);
1608 }
1609
1610 pub fn open_edit_array_item_dialog(&mut self) {
1612 let Some(item) = self.current_item() else {
1613 return;
1614 };
1615 let SettingControl::ObjectArray(array_state) = &item.control else {
1616 return;
1617 };
1618 let Some(schema) = array_state.item_schema.as_ref() else {
1619 return;
1620 };
1621 let Some(index) = array_state.focused_index else {
1622 return;
1623 };
1624 let Some(value) = array_state.bindings.get(index) else {
1625 return;
1626 };
1627 let path = item.path.clone();
1628
1629 let dialog = EntryDialogState::for_array_item(
1630 Some(index),
1631 value,
1632 schema,
1633 &path,
1634 false,
1635 &self.available_status_bar_tokens,
1636 );
1637 self.entry_dialog_stack.push(dialog);
1638 }
1639
1640 pub fn close_entry_dialog(&mut self) {
1642 self.entry_dialog_stack.pop();
1643 }
1644
1645 pub fn open_nested_entry_dialog(&mut self) {
1650 let nested_info = self.entry_dialog().and_then(|dialog| {
1652 let item = dialog.current_item()?;
1653 let base = dialog.entry_path();
1659 let relative = item.path.trim_start_matches('/');
1660 let path = if relative.is_empty() {
1661 base
1665 } else {
1666 format!("{}/{}", base, relative)
1667 };
1668
1669 match &item.control {
1670 SettingControl::Map(map_state) => {
1671 let schema = map_state.value_schema.as_ref()?;
1672 let no_delete = map_state.no_add; if let Some(entry_idx) = map_state.focused_entry {
1674 let (key, value) = map_state.entries.get(entry_idx)?;
1676 Some(NestedDialogInfo::MapEntry {
1677 key: key.clone(),
1678 value: value.clone(),
1679 schema: schema.as_ref().clone(),
1680 path,
1681 is_new: false,
1682 no_delete,
1683 })
1684 } else {
1685 Some(NestedDialogInfo::MapEntry {
1687 key: String::new(),
1688 value: serde_json::json!({}),
1689 schema: schema.as_ref().clone(),
1690 path,
1691 is_new: true,
1692 no_delete: false, })
1694 }
1695 }
1696 SettingControl::ObjectArray(array_state) => {
1697 let schema = array_state.item_schema.as_ref()?;
1698 if let Some(index) = array_state.focused_index {
1699 let value = array_state.bindings.get(index)?;
1701 Some(NestedDialogInfo::ArrayItem {
1702 index: Some(index),
1703 value: value.clone(),
1704 schema: schema.as_ref().clone(),
1705 path,
1706 is_new: false,
1707 })
1708 } else {
1709 Some(NestedDialogInfo::ArrayItem {
1711 index: None,
1712 value: serde_json::json!({}),
1713 schema: schema.as_ref().clone(),
1714 path,
1715 is_new: true,
1716 })
1717 }
1718 }
1719 _ => None,
1720 }
1721 });
1722
1723 if let Some(info) = nested_info {
1725 let dialog = match info {
1726 NestedDialogInfo::MapEntry {
1727 key,
1728 value,
1729 schema,
1730 path,
1731 is_new,
1732 no_delete,
1733 } => EntryDialogState::from_schema(
1734 key,
1735 &value,
1736 &schema,
1737 &path,
1738 is_new,
1739 no_delete,
1740 &self.available_status_bar_tokens,
1741 ),
1742 NestedDialogInfo::ArrayItem {
1743 index,
1744 value,
1745 schema,
1746 path,
1747 is_new,
1748 } => EntryDialogState::for_array_item(
1749 index,
1750 &value,
1751 &schema,
1752 &path,
1753 is_new,
1754 &self.available_status_bar_tokens,
1755 ),
1756 };
1757 self.entry_dialog_stack.push(dialog);
1758 }
1759 }
1760
1761 pub fn save_entry_dialog(&mut self) {
1766 let is_array = if self.entry_dialog_stack.len() > 1 {
1770 self.entry_dialog_stack
1772 .get(self.entry_dialog_stack.len() - 2)
1773 .and_then(|parent| parent.current_item())
1774 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1775 .unwrap_or(false)
1776 } else {
1777 self.current_item()
1779 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1780 .unwrap_or(false)
1781 };
1782
1783 if is_array {
1784 self.save_array_item_dialog_inner();
1785 } else {
1786 self.save_map_entry_dialog_inner();
1787 }
1788 }
1789
1790 fn save_map_entry_dialog_inner(&mut self) {
1792 let Some(mut dialog) = self.entry_dialog_stack.pop() else {
1793 return;
1794 };
1795 dialog.commit_pending_list_drafts();
1799
1800 let key = dialog.get_key();
1802 if key.is_empty() {
1803 return; }
1805
1806 let value = dialog.to_value();
1807 let map_path = dialog.map_path.clone();
1808 let original_key = dialog.entry_key.clone();
1809 let is_new = dialog.is_new;
1810 let key_changed = !is_new && key != original_key;
1811
1812 if let Some(item) = self.current_item_mut() {
1814 if let SettingControl::Map(map_state) = &mut item.control {
1815 if key_changed {
1817 if let Some(idx) = map_state
1818 .entries
1819 .iter()
1820 .position(|(k, _)| k == &original_key)
1821 {
1822 map_state.entries.remove(idx);
1823 }
1824 }
1825
1826 if let Some(entry) = map_state.entries.iter_mut().find(|(k, _)| k == &key) {
1828 entry.1 = value.clone();
1829 } else {
1830 map_state.entries.push((key.clone(), value.clone()));
1831 map_state.entries.sort_by(|a, b| a.0.cmp(&b.0));
1832 }
1833 }
1834 }
1835
1836 if key_changed {
1838 let old_path = format!("{}/{}", map_path, original_key);
1839 self.pending_changes
1840 .insert(old_path, serde_json::Value::Null);
1841 }
1842
1843 let path = format!("{}/{}", map_path, key);
1845 self.set_pending_change(&path, value);
1846 }
1847
1848 fn save_array_item_dialog_inner(&mut self) {
1850 let Some(mut dialog) = self.entry_dialog_stack.pop() else {
1851 return;
1852 };
1853 dialog.commit_pending_list_drafts();
1855
1856 let value = dialog.to_value();
1857 let array_path = dialog.map_path.clone();
1858 let is_new = dialog.is_new;
1859 let entry_key = dialog.entry_key.clone();
1860
1861 let is_nested = !self.entry_dialog_stack.is_empty();
1863
1864 if is_nested {
1865 let parent_entry_path = self
1873 .entry_dialog_stack
1874 .last()
1875 .map(|p| p.entry_path())
1876 .unwrap_or_default();
1877 let item_path = array_path
1878 .strip_prefix(parent_entry_path.as_str())
1879 .unwrap_or(&array_path)
1880 .trim_end_matches('/')
1881 .to_string();
1882
1883 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1889 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1890 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1891 if is_new {
1892 array_state.bindings.push(value.clone());
1893 } else if let Ok(index) = entry_key.parse::<usize>() {
1894 if index < array_state.bindings.len() {
1895 array_state.bindings[index] = value.clone();
1896 }
1897 }
1898 parent.user_edited = true;
1899 }
1900 }
1901 }
1902
1903 if let Some(parent) = self.entry_dialog_stack.last() {
1906 if let Some(item) = parent.items.iter().find(|i| i.path == item_path) {
1907 if let SettingControl::ObjectArray(array_state) = &item.control {
1908 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1909 self.set_pending_change(&array_path, array_value);
1910 }
1911 }
1912 }
1913 } else {
1914 if let Some(item) = self.current_item_mut() {
1916 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1917 if is_new {
1918 array_state.bindings.push(value.clone());
1919 } else if let Ok(index) = entry_key.parse::<usize>() {
1920 if index < array_state.bindings.len() {
1921 array_state.bindings[index] = value.clone();
1922 }
1923 }
1924 }
1925 }
1926
1927 if let Some(item) = self.current_item() {
1929 if let SettingControl::ObjectArray(array_state) = &item.control {
1930 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1931 self.set_pending_change(&array_path, array_value);
1932 }
1933 }
1934 }
1935 }
1936
1937 pub fn request_entry_delete_confirm(&mut self) {
1943 let (name, is_array_item) = self
1944 .entry_dialog()
1945 .map(|d| (d.entry_key.clone(), d.is_array_item))
1946 .unwrap_or_default();
1947 self.entry_delete_target_name = if is_array_item { String::new() } else { name };
1951 self.entry_delete_target_is_array_item = is_array_item;
1952 self.entry_delete_confirm_selection = 0;
1953 self.showing_entry_delete_confirm = true;
1954 }
1955
1956 pub fn delete_entry_dialog(&mut self) {
1957 let is_nested = self.entry_dialog_stack.len() > 1;
1959
1960 let Some(dialog) = self.entry_dialog_stack.pop() else {
1961 return;
1962 };
1963
1964 let path = format!("{}/{}", dialog.map_path, dialog.entry_key);
1965
1966 if is_nested {
1968 let map_field = dialog.map_path.rsplit('/').next().unwrap_or("").to_string();
1971 let item_path = format!("/{}", map_field);
1972
1973 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1975 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1976 if let SettingControl::Map(map_state) = &mut item.control {
1977 if let Some(idx) = map_state
1978 .entries
1979 .iter()
1980 .position(|(k, _)| k == &dialog.entry_key)
1981 {
1982 map_state.remove_entry(idx);
1983 }
1984 }
1985 }
1986 }
1987 } else {
1988 if let Some(item) = self.current_item_mut() {
1990 if let SettingControl::Map(map_state) = &mut item.control {
1991 if let Some(idx) = map_state
1992 .entries
1993 .iter()
1994 .position(|(k, _)| k == &dialog.entry_key)
1995 {
1996 map_state.remove_entry(idx);
1997 }
1998 }
1999 }
2000 }
2001
2002 self.pending_changes.remove(&path);
2010 self.pending_deletions.insert(path);
2011 }
2012
2013 pub fn max_scroll(&self) -> u16 {
2015 self.scroll_panel.scroll.max_offset()
2016 }
2017
2018 pub fn scroll_up(&mut self, delta: usize) -> bool {
2021 let old = self.scroll_panel.scroll.offset;
2022 self.scroll_panel.scroll_up(delta as u16);
2023 let changed = old != self.scroll_panel.scroll.offset;
2024 if changed {
2025 self.sync_tree_cursor_to_body_scroll();
2026 }
2027 changed
2028 }
2029
2030 pub fn scroll_down(&mut self, delta: usize) -> bool {
2033 let old = self.scroll_panel.scroll.offset;
2034 self.scroll_panel.scroll_down(delta as u16);
2035 let changed = old != self.scroll_panel.scroll.offset;
2036 if changed {
2037 self.sync_tree_cursor_to_body_scroll();
2038 }
2039 changed
2040 }
2041
2042 pub fn scroll_to_ratio(&mut self, ratio: f32) -> bool {
2045 let old = self.scroll_panel.scroll.offset;
2046 self.scroll_panel.scroll_to_ratio(ratio);
2047 let changed = old != self.scroll_panel.scroll.offset;
2048 if changed {
2049 self.sync_tree_cursor_to_body_scroll();
2050 }
2051 changed
2052 }
2053
2054 pub(super) fn sync_tree_cursor_to_body_scroll(&mut self) {
2060 if let Some(section_idx) = self.current_section_index() {
2061 self.tree_cursor_section = Some(section_idx);
2062 }
2063 }
2068
2069 pub fn is_number_control(&self) -> bool {
2072 self.current_item()
2073 .is_some_and(|item| matches!(item.control, SettingControl::Number(_)))
2074 }
2075
2076 pub fn start_editing(&mut self) {
2077 if let Some(item) = self.current_item() {
2078 if matches!(
2079 item.control,
2080 SettingControl::TextList(_)
2081 | SettingControl::DualList(_)
2082 | SettingControl::Text(_)
2083 | SettingControl::Map(_)
2084 | SettingControl::Json(_)
2085 ) {
2086 self.editing_text = true;
2087 }
2088 }
2089 if let Some(item) = self.current_item_mut() {
2090 match item.control {
2091 SettingControl::DualList(ref mut dl) => {
2092 dl.editing = true;
2093 }
2094 SettingControl::Text(ref mut state) => {
2095 state.editing = true;
2096 state.arm_replace_on_type();
2101 }
2102 _ => {}
2103 }
2104 }
2105 }
2106
2107 pub fn stop_editing(&mut self) {
2109 self.editing_text = false;
2110 if let Some(item) = self.current_item_mut() {
2111 match item.control {
2112 SettingControl::DualList(ref mut dl) => {
2113 dl.editing = false;
2114 }
2115 SettingControl::Text(ref mut state) => {
2116 state.editing = false;
2117 }
2118 _ => {}
2119 }
2120 }
2121 }
2122
2123 pub fn is_editable_control(&self) -> bool {
2125 self.current_item().is_some_and(|item| {
2126 matches!(
2127 item.control,
2128 SettingControl::TextList(_)
2129 | SettingControl::DualList(_)
2130 | SettingControl::Text(_)
2131 | SettingControl::Map(_)
2132 | SettingControl::Json(_)
2133 )
2134 })
2135 }
2136
2137 pub fn is_editing_json(&self) -> bool {
2139 if !self.editing_text {
2140 return false;
2141 }
2142 self.current_item()
2143 .map(|item| matches!(&item.control, SettingControl::Json(_)))
2144 .unwrap_or(false)
2145 }
2146
2147 pub fn text_insert(&mut self, c: char) {
2149 if let Some(item) = self.current_item_mut() {
2150 match &mut item.control {
2151 SettingControl::TextList(state) => state.insert(c),
2152 SettingControl::Text(state) => state.insert(c),
2153 SettingControl::Map(state) => {
2154 state.new_key_text.insert(state.cursor, c);
2155 state.cursor += c.len_utf8();
2156 }
2157 SettingControl::Json(state) => state.insert(c),
2158 _ => {}
2159 }
2160 }
2161 }
2162
2163 pub fn text_insert_str(&mut self, s: &str) {
2167 if let Some(item) = self.current_item_mut() {
2168 match &mut item.control {
2169 SettingControl::TextList(state) => state.insert_str(s),
2170 SettingControl::Text(state) => state.insert_str(s),
2171 SettingControl::Map(state) => {
2172 for c in s.chars() {
2173 state.new_key_text.insert(state.cursor, c);
2174 state.cursor += c.len_utf8();
2175 }
2176 }
2177 SettingControl::Json(state) => state.insert_str(s),
2178 _ => {}
2179 }
2180 }
2181 }
2182
2183 pub fn paste_into_focused_text(&mut self, text: &str) -> bool {
2190 if let Some(dialog) = self.entry_dialog_mut() {
2191 if dialog.editing_text {
2192 dialog.insert_str(text);
2193 return true;
2194 }
2195 return false;
2196 }
2197 if self.editing_text {
2198 self.text_insert_str(text);
2199 return true;
2200 }
2201 false
2202 }
2203
2204 pub fn text_backspace(&mut self) {
2206 if let Some(item) = self.current_item_mut() {
2207 match &mut item.control {
2208 SettingControl::TextList(state) => state.backspace(),
2209 SettingControl::Text(state) => state.backspace(),
2210 SettingControl::Map(state) => {
2211 if state.cursor > 0 {
2212 let mut char_start = state.cursor - 1;
2213 while char_start > 0 && !state.new_key_text.is_char_boundary(char_start) {
2214 char_start -= 1;
2215 }
2216 state.new_key_text.remove(char_start);
2217 state.cursor = char_start;
2218 }
2219 }
2220 SettingControl::Json(state) => state.backspace(),
2221 _ => {}
2222 }
2223 }
2224 }
2225
2226 pub fn text_move_left(&mut self) {
2228 if let Some(item) = self.current_item_mut() {
2229 match &mut item.control {
2230 SettingControl::TextList(state) => state.move_left(),
2231 SettingControl::Text(state) => state.move_left(),
2232 SettingControl::Map(state) => {
2233 if state.cursor > 0 {
2234 let mut new_pos = state.cursor - 1;
2235 while new_pos > 0 && !state.new_key_text.is_char_boundary(new_pos) {
2236 new_pos -= 1;
2237 }
2238 state.cursor = new_pos;
2239 }
2240 }
2241 SettingControl::Json(state) => state.move_left(),
2242 _ => {}
2243 }
2244 }
2245 }
2246
2247 pub fn text_move_right(&mut self) {
2249 if let Some(item) = self.current_item_mut() {
2250 match &mut item.control {
2251 SettingControl::TextList(state) => state.move_right(),
2252 SettingControl::Text(state) => state.move_right(),
2253 SettingControl::Map(state) => {
2254 if state.cursor < state.new_key_text.len() {
2255 let mut new_pos = state.cursor + 1;
2256 while new_pos < state.new_key_text.len()
2257 && !state.new_key_text.is_char_boundary(new_pos)
2258 {
2259 new_pos += 1;
2260 }
2261 state.cursor = new_pos;
2262 }
2263 }
2264 SettingControl::Json(state) => state.move_right(),
2265 _ => {}
2266 }
2267 }
2268 }
2269
2270 pub fn text_focus_prev(&mut self) {
2272 if let Some(item) = self.current_item_mut() {
2273 match &mut item.control {
2274 SettingControl::TextList(state) => state.focus_prev(),
2275 SettingControl::Map(state) => {
2276 state.focus_prev();
2277 }
2278 _ => {}
2279 }
2280 }
2281 }
2282
2283 pub fn text_focus_next(&mut self) {
2285 if let Some(item) = self.current_item_mut() {
2286 match &mut item.control {
2287 SettingControl::TextList(state) => state.focus_next(),
2288 SettingControl::Map(state) => {
2289 state.focus_next();
2290 }
2291 _ => {}
2292 }
2293 }
2294 }
2295
2296 pub fn text_add_item(&mut self) {
2298 if let Some(item) = self.current_item_mut() {
2299 match &mut item.control {
2300 SettingControl::TextList(state) => state.add_item(),
2301 SettingControl::Map(state) => state.add_entry_from_input(),
2302 _ => {}
2303 }
2304 }
2305 self.on_value_changed();
2307 }
2308
2309 pub fn text_remove_focused(&mut self) {
2311 if let Some(item) = self.current_item_mut() {
2312 match &mut item.control {
2313 SettingControl::TextList(state) => {
2314 if let Some(idx) = state.focused_item {
2315 state.remove_item(idx);
2316 }
2317 }
2318 SettingControl::Map(state) => {
2319 if let Some(idx) = state.focused_entry {
2320 state.remove_entry(idx);
2321 }
2322 }
2323 _ => {}
2324 }
2325 }
2326 self.on_value_changed();
2328 }
2329
2330 pub fn is_editing_dual_list(&self) -> bool {
2332 if !self.editing_text {
2333 return false;
2334 }
2335 self.current_item()
2336 .map(|item| matches!(&item.control, SettingControl::DualList(_)))
2337 .unwrap_or(false)
2338 }
2339
2340 pub fn with_dual_list_mut<R>(
2345 &mut self,
2346 item_idx: usize,
2347 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2348 ) -> Option<R> {
2349 let page = self.pages.get_mut(self.selected_category)?;
2350 let item = page.items.get_mut(item_idx)?;
2351 if let SettingControl::DualList(ref mut state) = item.control {
2352 Some(f(state))
2353 } else {
2354 None
2355 }
2356 }
2357
2358 pub fn with_current_dual_list_mut<R>(
2361 &mut self,
2362 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2363 ) -> Option<R> {
2364 if let Some(item) = self.current_item_mut() {
2365 if let SettingControl::DualList(ref mut state) = item.control {
2366 return Some(f(state));
2367 }
2368 }
2369 None
2370 }
2371
2372 pub fn refresh_dual_list_sibling(&mut self) {
2379 let (new_included, sibling_path) = {
2380 let Some(item) = self.current_item() else {
2381 return;
2382 };
2383 let SettingControl::DualList(state) = &item.control else {
2384 return;
2385 };
2386 let Some(ref sib_path) = item.dual_list_sibling else {
2387 return;
2388 };
2389 (state.included.clone(), sib_path.clone())
2390 };
2391
2392 if let Some(page) = self.pages.get_mut(self.selected_category) {
2394 for other in page.items.iter_mut() {
2395 if other.path == sibling_path {
2396 if let SettingControl::DualList(ref mut sib_state) = other.control {
2397 sib_state.excluded = new_included;
2398 }
2399 break;
2400 }
2401 }
2402 }
2403 }
2404
2405 pub fn json_cursor_up(&mut self) {
2409 if let Some(item) = self.current_item_mut() {
2410 if let SettingControl::Json(state) = &mut item.control {
2411 state.move_up();
2412 }
2413 }
2414 }
2415
2416 pub fn json_cursor_down(&mut self) {
2418 if let Some(item) = self.current_item_mut() {
2419 if let SettingControl::Json(state) = &mut item.control {
2420 state.move_down();
2421 }
2422 }
2423 }
2424
2425 pub fn json_insert_newline(&mut self) {
2427 if let Some(item) = self.current_item_mut() {
2428 if let SettingControl::Json(state) = &mut item.control {
2429 state.insert('\n');
2430 }
2431 }
2432 }
2433
2434 pub fn json_delete(&mut self) {
2436 if let Some(item) = self.current_item_mut() {
2437 if let SettingControl::Json(state) = &mut item.control {
2438 state.delete();
2439 }
2440 }
2441 }
2442
2443 pub fn json_exit_editing(&mut self) {
2445 let is_valid = self
2446 .current_item()
2447 .map(|item| {
2448 if let SettingControl::Json(state) = &item.control {
2449 state.is_valid()
2450 } else {
2451 true
2452 }
2453 })
2454 .unwrap_or(true);
2455
2456 if is_valid {
2457 if let Some(item) = self.current_item_mut() {
2458 if let SettingControl::Json(state) = &mut item.control {
2459 state.commit();
2460 }
2461 }
2462 self.on_value_changed();
2463 } else if let Some(item) = self.current_item_mut() {
2464 if let SettingControl::Json(state) = &mut item.control {
2465 state.revert();
2466 }
2467 }
2468 self.editing_text = false;
2469 }
2470
2471 pub fn json_select_all(&mut self) {
2473 if let Some(item) = self.current_item_mut() {
2474 if let SettingControl::Json(state) = &mut item.control {
2475 state.select_all();
2476 }
2477 }
2478 }
2479
2480 pub fn json_selected_text(&self) -> Option<String> {
2482 if let Some(item) = self.current_item() {
2483 if let SettingControl::Json(state) = &item.control {
2484 return state.selected_text();
2485 }
2486 }
2487 None
2488 }
2489
2490 pub fn json_cursor_up_selecting(&mut self) {
2492 if let Some(item) = self.current_item_mut() {
2493 if let SettingControl::Json(state) = &mut item.control {
2494 state.editor.move_up_selecting();
2495 }
2496 }
2497 }
2498
2499 pub fn json_cursor_down_selecting(&mut self) {
2501 if let Some(item) = self.current_item_mut() {
2502 if let SettingControl::Json(state) = &mut item.control {
2503 state.editor.move_down_selecting();
2504 }
2505 }
2506 }
2507
2508 pub fn json_cursor_left_selecting(&mut self) {
2510 if let Some(item) = self.current_item_mut() {
2511 if let SettingControl::Json(state) = &mut item.control {
2512 state.editor.move_left_selecting();
2513 }
2514 }
2515 }
2516
2517 pub fn json_cursor_right_selecting(&mut self) {
2519 if let Some(item) = self.current_item_mut() {
2520 if let SettingControl::Json(state) = &mut item.control {
2521 state.editor.move_right_selecting();
2522 }
2523 }
2524 }
2525
2526 pub fn is_dropdown_open(&self) -> bool {
2530 self.current_item().is_some_and(|item| {
2531 if let SettingControl::Dropdown(ref d) = item.control {
2532 d.open
2533 } else {
2534 false
2535 }
2536 })
2537 }
2538
2539 pub fn dropdown_toggle(&mut self) {
2541 let mut opened = false;
2542 if let Some(item) = self.current_item_mut() {
2543 if let SettingControl::Dropdown(ref mut d) = item.control {
2544 d.toggle_open();
2545 opened = d.open;
2546 }
2547 }
2548
2549 if opened {
2551 let selected_item = self.selected_item;
2553 let width = self.layout_width;
2554 if let Some(page) = self.pages.get(self.selected_category) {
2555 self.scroll_panel
2557 .ensure_focused_visible(&page.items, selected_item, None, width);
2558 }
2559 }
2560 }
2561
2562 pub fn dropdown_prev(&mut self) {
2564 if let Some(item) = self.current_item_mut() {
2565 if let SettingControl::Dropdown(ref mut d) = item.control {
2566 d.select_prev();
2567 }
2568 }
2569 }
2570
2571 pub fn dropdown_next(&mut self) {
2573 if let Some(item) = self.current_item_mut() {
2574 if let SettingControl::Dropdown(ref mut d) = item.control {
2575 d.select_next();
2576 }
2577 }
2578 }
2579
2580 pub fn dropdown_home(&mut self) {
2582 if let Some(item) = self.current_item_mut() {
2583 if let SettingControl::Dropdown(ref mut d) = item.control {
2584 if !d.options.is_empty() {
2585 d.selected = 0;
2586 d.ensure_visible();
2587 }
2588 }
2589 }
2590 }
2591
2592 pub fn dropdown_end(&mut self) {
2594 if let Some(item) = self.current_item_mut() {
2595 if let SettingControl::Dropdown(ref mut d) = item.control {
2596 if !d.options.is_empty() {
2597 d.selected = d.options.len() - 1;
2598 d.ensure_visible();
2599 }
2600 }
2601 }
2602 }
2603
2604 pub fn dropdown_confirm(&mut self) {
2606 if let Some(item) = self.current_item_mut() {
2607 if let SettingControl::Dropdown(ref mut d) = item.control {
2608 d.confirm();
2609 }
2610 }
2611 self.on_value_changed();
2612 }
2613
2614 pub fn dropdown_cancel(&mut self) {
2616 if let Some(item) = self.current_item_mut() {
2617 if let SettingControl::Dropdown(ref mut d) = item.control {
2618 d.cancel();
2619 }
2620 }
2621 }
2622
2623 pub fn dropdown_select(&mut self, option_idx: usize) {
2625 if let Some(item) = self.current_item_mut() {
2626 if let SettingControl::Dropdown(ref mut d) = item.control {
2627 if option_idx < d.options.len() {
2628 d.selected = option_idx;
2629 d.confirm();
2630 }
2631 }
2632 }
2633 self.on_value_changed();
2634 }
2635
2636 pub fn set_dropdown_hover(&mut self, hover_idx: Option<usize>) -> bool {
2639 if let Some(item) = self.current_item_mut() {
2640 if let SettingControl::Dropdown(ref mut d) = item.control {
2641 if d.open && d.hover_index != hover_idx {
2642 d.hover_index = hover_idx;
2643 return true;
2644 }
2645 }
2646 }
2647 false
2648 }
2649
2650 pub fn dropdown_scroll(&mut self, delta: i32) {
2652 if let Some(item) = self.current_item_mut() {
2653 if let SettingControl::Dropdown(ref mut d) = item.control {
2654 if d.open {
2655 d.scroll_by(delta);
2656 }
2657 }
2658 }
2659 }
2660
2661 pub fn is_number_editing(&self) -> bool {
2665 self.current_item().is_some_and(|item| {
2666 if let SettingControl::Number(ref n) = item.control {
2667 n.editing()
2668 } else {
2669 false
2670 }
2671 })
2672 }
2673
2674 pub fn start_number_editing(&mut self) {
2676 if let Some(item) = self.current_item_mut() {
2677 if let SettingControl::Number(ref mut n) = item.control {
2678 n.start_editing();
2679 }
2680 }
2681 }
2682
2683 pub fn number_insert(&mut self, c: char) {
2685 if let Some(item) = self.current_item_mut() {
2686 if let SettingControl::Number(ref mut n) = item.control {
2687 n.insert_char(c);
2688 }
2689 }
2690 }
2691
2692 pub fn number_backspace(&mut self) {
2694 if let Some(item) = self.current_item_mut() {
2695 if let SettingControl::Number(ref mut n) = item.control {
2696 n.backspace();
2697 }
2698 }
2699 }
2700
2701 pub fn number_confirm(&mut self) {
2703 if let Some(item) = self.current_item_mut() {
2704 if let SettingControl::Number(ref mut n) = item.control {
2705 n.confirm_editing();
2706 }
2707 }
2708 self.on_value_changed();
2709 }
2710
2711 pub fn number_cancel(&mut self) {
2713 if let Some(item) = self.current_item_mut() {
2714 if let SettingControl::Number(ref mut n) = item.control {
2715 n.cancel_editing();
2716 }
2717 }
2718 }
2719
2720 pub fn number_delete(&mut self) {
2722 if let Some(item) = self.current_item_mut() {
2723 if let SettingControl::Number(ref mut n) = item.control {
2724 n.delete();
2725 }
2726 }
2727 }
2728
2729 pub fn number_move_left(&mut self) {
2731 if let Some(item) = self.current_item_mut() {
2732 if let SettingControl::Number(ref mut n) = item.control {
2733 n.move_left();
2734 }
2735 }
2736 }
2737
2738 pub fn number_move_right(&mut self) {
2740 if let Some(item) = self.current_item_mut() {
2741 if let SettingControl::Number(ref mut n) = item.control {
2742 n.move_right();
2743 }
2744 }
2745 }
2746
2747 pub fn number_move_home(&mut self) {
2749 if let Some(item) = self.current_item_mut() {
2750 if let SettingControl::Number(ref mut n) = item.control {
2751 n.move_home();
2752 }
2753 }
2754 }
2755
2756 pub fn number_move_end(&mut self) {
2758 if let Some(item) = self.current_item_mut() {
2759 if let SettingControl::Number(ref mut n) = item.control {
2760 n.move_end();
2761 }
2762 }
2763 }
2764
2765 pub fn number_move_left_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_left_selecting();
2770 }
2771 }
2772 }
2773
2774 pub fn number_move_right_selecting(&mut self) {
2776 if let Some(item) = self.current_item_mut() {
2777 if let SettingControl::Number(ref mut n) = item.control {
2778 n.move_right_selecting();
2779 }
2780 }
2781 }
2782
2783 pub fn number_move_home_selecting(&mut self) {
2785 if let Some(item) = self.current_item_mut() {
2786 if let SettingControl::Number(ref mut n) = item.control {
2787 n.move_home_selecting();
2788 }
2789 }
2790 }
2791
2792 pub fn number_move_end_selecting(&mut self) {
2794 if let Some(item) = self.current_item_mut() {
2795 if let SettingControl::Number(ref mut n) = item.control {
2796 n.move_end_selecting();
2797 }
2798 }
2799 }
2800
2801 pub fn number_move_word_left(&mut self) {
2803 if let Some(item) = self.current_item_mut() {
2804 if let SettingControl::Number(ref mut n) = item.control {
2805 n.move_word_left();
2806 }
2807 }
2808 }
2809
2810 pub fn number_move_word_right(&mut self) {
2812 if let Some(item) = self.current_item_mut() {
2813 if let SettingControl::Number(ref mut n) = item.control {
2814 n.move_word_right();
2815 }
2816 }
2817 }
2818
2819 pub fn number_move_word_left_selecting(&mut self) {
2821 if let Some(item) = self.current_item_mut() {
2822 if let SettingControl::Number(ref mut n) = item.control {
2823 n.move_word_left_selecting();
2824 }
2825 }
2826 }
2827
2828 pub fn number_move_word_right_selecting(&mut self) {
2830 if let Some(item) = self.current_item_mut() {
2831 if let SettingControl::Number(ref mut n) = item.control {
2832 n.move_word_right_selecting();
2833 }
2834 }
2835 }
2836
2837 pub fn number_select_all(&mut self) {
2839 if let Some(item) = self.current_item_mut() {
2840 if let SettingControl::Number(ref mut n) = item.control {
2841 n.select_all();
2842 }
2843 }
2844 }
2845
2846 pub fn number_delete_word_backward(&mut self) {
2848 if let Some(item) = self.current_item_mut() {
2849 if let SettingControl::Number(ref mut n) = item.control {
2850 n.delete_word_backward();
2851 }
2852 }
2853 }
2854
2855 pub fn number_delete_word_forward(&mut self) {
2857 if let Some(item) = self.current_item_mut() {
2858 if let SettingControl::Number(ref mut n) = item.control {
2859 n.delete_word_forward();
2860 }
2861 }
2862 }
2863
2864 pub fn get_change_descriptions(&self) -> Vec<String> {
2866 let mut descriptions: Vec<String> = self
2867 .pending_changes
2868 .iter()
2869 .map(|(path, value)| {
2870 let value_str = match value {
2871 serde_json::Value::Bool(b) => b.to_string(),
2872 serde_json::Value::Number(n) => n.to_string(),
2873 serde_json::Value::String(s) => format!("\"{}\"", s),
2874 _ => value.to_string(),
2875 };
2876 format!("{}: {}", path, value_str)
2877 })
2878 .collect();
2879 for path in &self.pending_deletions {
2881 descriptions.push(format!("{}: (reset to default)", path));
2882 }
2883 descriptions.sort();
2884 descriptions
2885 }
2886}
2887
2888fn update_control_from_value(control: &mut SettingControl, value: &serde_json::Value) {
2890 match control {
2891 SettingControl::Toggle(state) => {
2892 if let Some(b) = value.as_bool() {
2893 state.checked = b;
2894 }
2895 }
2896 SettingControl::Number(state) => {
2897 if let Some(n) = value.as_i64() {
2898 state.value = n;
2899 }
2900 }
2901 SettingControl::Dropdown(state) => {
2902 if let Some(s) = value.as_str() {
2903 if let Some(idx) = state.options.iter().position(|o| o == s) {
2904 state.selected = idx;
2905 }
2906 }
2907 }
2908 SettingControl::Text(state) => {
2909 if let Some(s) = value.as_str() {
2910 state.value = s.to_string();
2911 state.cursor = state.value.len();
2912 }
2913 }
2914 SettingControl::TextList(state) => {
2915 if let Some(arr) = value.as_array() {
2916 state.items = arr
2917 .iter()
2918 .filter_map(|v| {
2919 if state.is_integer {
2920 v.as_i64()
2921 .map(|n| n.to_string())
2922 .or_else(|| v.as_u64().map(|n| n.to_string()))
2923 .or_else(|| v.as_f64().map(|n| n.to_string()))
2924 } else {
2925 v.as_str().map(String::from)
2926 }
2927 })
2928 .collect();
2929 }
2930 }
2931 SettingControl::DualList(state) => {
2932 if let Some(arr) = value.as_array() {
2933 state.included = arr
2934 .iter()
2935 .filter_map(|v| v.as_str().map(String::from))
2936 .collect();
2937 }
2938 }
2939 SettingControl::Map(state) => {
2940 if let Some(obj) = value.as_object() {
2941 state.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
2942 state.entries.sort_by(|a, b| a.0.cmp(&b.0));
2943 }
2944 }
2945 SettingControl::ObjectArray(state) => {
2946 if let Some(arr) = value.as_array() {
2947 state.bindings = arr.clone();
2948 }
2949 }
2950 SettingControl::Json(state) => {
2951 let json_str =
2953 serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_string());
2954 let json_str = if json_str.is_empty() {
2955 "null".to_string()
2956 } else {
2957 json_str
2958 };
2959 state.original_text = json_str.clone();
2960 state.editor.set_value(&json_str);
2961 state.scroll_offset = 0;
2962 }
2963 SettingControl::Complex { .. } => {}
2964 }
2965}
2966
2967#[cfg(test)]
2968mod tests {
2969 use super::*;
2970
2971 const TEST_SCHEMA: &str = r#"
2972{
2973 "type": "object",
2974 "properties": {
2975 "theme": {
2976 "type": "string",
2977 "default": "dark"
2978 },
2979 "line_numbers": {
2980 "type": "boolean",
2981 "default": true
2982 }
2983 },
2984 "$defs": {}
2985}
2986"#;
2987
2988 fn test_config() -> Config {
2989 Config::default()
2990 }
2991
2992 #[test]
2993 fn test_settings_state_creation() {
2994 let config = test_config();
2995 let state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2996
2997 assert!(!state.visible);
2998 assert_eq!(state.selected_category, 0);
2999 assert!(!state.has_changes());
3000 }
3001
3002 #[test]
3003 fn test_navigation() {
3004 let config = test_config();
3005 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3006
3007 assert_eq!(state.focus_panel(), FocusPanel::Categories);
3009
3010 state.toggle_focus();
3012 assert_eq!(state.focus_panel(), FocusPanel::Settings);
3013
3014 state.select_next();
3016 assert_eq!(state.selected_item, 1);
3017
3018 state.select_prev();
3019 assert_eq!(state.selected_item, 0);
3020 }
3021
3022 #[test]
3023 fn test_pending_changes() {
3024 let config = test_config();
3025 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3026
3027 assert!(!state.has_changes());
3028
3029 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
3030 assert!(state.has_changes());
3031
3032 state.discard_changes();
3033 assert!(!state.has_changes());
3034 }
3035
3036 #[test]
3037 fn test_show_hide() {
3038 let config = test_config();
3039 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3040
3041 assert!(!state.visible);
3042
3043 state.show();
3044 assert!(state.visible);
3045 assert_eq!(state.focus_panel(), FocusPanel::Categories);
3046
3047 state.hide();
3048 assert!(!state.visible);
3049 }
3050
3051 const TEST_SCHEMA_CONTROLS: &str = r#"
3053{
3054 "type": "object",
3055 "properties": {
3056 "theme": {
3057 "type": "string",
3058 "enum": ["dark", "light", "high-contrast"],
3059 "default": "dark"
3060 },
3061 "tab_size": {
3062 "type": "integer",
3063 "minimum": 1,
3064 "maximum": 8,
3065 "default": 4
3066 },
3067 "line_numbers": {
3068 "type": "boolean",
3069 "default": true
3070 }
3071 },
3072 "$defs": {}
3073}
3074"#;
3075
3076 #[test]
3077 fn test_dropdown_toggle() {
3078 let config = test_config();
3079 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3080 state.show();
3081 state.toggle_focus(); state.select_next();
3086 state.select_next();
3087 assert!(!state.is_dropdown_open());
3088
3089 state.dropdown_toggle();
3090 assert!(state.is_dropdown_open());
3091
3092 state.dropdown_toggle();
3093 assert!(!state.is_dropdown_open());
3094 }
3095
3096 #[test]
3097 fn test_dropdown_cancel_restores() {
3098 let config = test_config();
3099 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3100 state.show();
3101 state.toggle_focus();
3102
3103 state.select_next();
3106 state.select_next();
3107
3108 state.dropdown_toggle();
3110 assert!(state.is_dropdown_open());
3111
3112 let initial = state.current_item().and_then(|item| {
3114 if let SettingControl::Dropdown(ref d) = item.control {
3115 Some(d.selected)
3116 } else {
3117 None
3118 }
3119 });
3120
3121 state.dropdown_next();
3123 let after_change = state.current_item().and_then(|item| {
3124 if let SettingControl::Dropdown(ref d) = item.control {
3125 Some(d.selected)
3126 } else {
3127 None
3128 }
3129 });
3130 assert_ne!(initial, after_change);
3131
3132 state.dropdown_cancel();
3134 assert!(!state.is_dropdown_open());
3135
3136 let after_cancel = state.current_item().and_then(|item| {
3137 if let SettingControl::Dropdown(ref d) = item.control {
3138 Some(d.selected)
3139 } else {
3140 None
3141 }
3142 });
3143 assert_eq!(initial, after_cancel);
3144 }
3145
3146 #[test]
3147 fn test_dropdown_confirm_keeps_selection() {
3148 let config = test_config();
3149 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3150 state.show();
3151 state.toggle_focus();
3152
3153 state.dropdown_toggle();
3155
3156 state.dropdown_next();
3158 let after_change = state.current_item().and_then(|item| {
3159 if let SettingControl::Dropdown(ref d) = item.control {
3160 Some(d.selected)
3161 } else {
3162 None
3163 }
3164 });
3165
3166 state.dropdown_confirm();
3168 assert!(!state.is_dropdown_open());
3169
3170 let after_confirm = state.current_item().and_then(|item| {
3171 if let SettingControl::Dropdown(ref d) = item.control {
3172 Some(d.selected)
3173 } else {
3174 None
3175 }
3176 });
3177 assert_eq!(after_change, after_confirm);
3178 }
3179
3180 #[test]
3181 fn test_number_editing() {
3182 let config = test_config();
3183 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3184 state.show();
3185 state.toggle_focus();
3186
3187 state.select_next();
3189
3190 assert!(!state.is_number_editing());
3192
3193 state.start_number_editing();
3195 assert!(state.is_number_editing());
3196
3197 state.number_insert('8');
3199
3200 state.number_confirm();
3202 assert!(!state.is_number_editing());
3203 }
3204
3205 #[test]
3206 fn test_number_cancel_editing() {
3207 let config = test_config();
3208 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3209 state.show();
3210 state.toggle_focus();
3211
3212 state.select_next();
3214
3215 let initial_value = state.current_item().and_then(|item| {
3217 if let SettingControl::Number(ref n) = item.control {
3218 Some(n.value)
3219 } else {
3220 None
3221 }
3222 });
3223
3224 state.start_number_editing();
3226 state.number_backspace();
3227 state.number_insert('9');
3228 state.number_insert('9');
3229
3230 state.number_cancel();
3232 assert!(!state.is_number_editing());
3233
3234 let after_cancel = state.current_item().and_then(|item| {
3236 if let SettingControl::Number(ref n) = item.control {
3237 Some(n.value)
3238 } else {
3239 None
3240 }
3241 });
3242 assert_eq!(initial_value, after_cancel);
3243 }
3244
3245 #[test]
3246 fn test_number_backspace() {
3247 let config = test_config();
3248 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3249 state.show();
3250 state.toggle_focus();
3251 state.select_next();
3252
3253 state.start_number_editing();
3254 state.number_backspace();
3255
3256 let display_text = state.current_item().and_then(|item| {
3258 if let SettingControl::Number(ref n) = item.control {
3259 Some(n.display_text())
3260 } else {
3261 None
3262 }
3263 });
3264 assert_eq!(display_text, Some(String::new()));
3266
3267 state.number_cancel();
3268 }
3269
3270 #[test]
3271 fn test_layer_selection() {
3272 let config = test_config();
3273 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3274
3275 assert_eq!(state.target_layer, ConfigLayer::User);
3277 assert_eq!(state.target_layer_name(), "User");
3278
3279 state.cycle_target_layer();
3281 assert_eq!(state.target_layer, ConfigLayer::Project);
3282 assert_eq!(state.target_layer_name(), "Project");
3283
3284 state.cycle_target_layer();
3285 assert_eq!(state.target_layer, ConfigLayer::Session);
3286 assert_eq!(state.target_layer_name(), "Session");
3287
3288 state.cycle_target_layer();
3289 assert_eq!(state.target_layer, ConfigLayer::User);
3290
3291 state.set_target_layer(ConfigLayer::Project);
3293 assert_eq!(state.target_layer, ConfigLayer::Project);
3294
3295 state.set_target_layer(ConfigLayer::System);
3297 assert_eq!(state.target_layer, ConfigLayer::Project);
3298 }
3299
3300 #[test]
3301 fn test_layer_switch_clears_pending_changes() {
3302 let config = test_config();
3303 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3304
3305 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
3307 assert!(state.has_changes());
3308
3309 state.cycle_target_layer();
3311 assert!(!state.has_changes());
3312 }
3313
3314 #[test]
3333 fn nested_array_save_records_full_entry_path() {
3334 use crate::view::settings::schema::SettingType;
3337
3338 let config = test_config();
3339 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3340
3341 let item_schema = SettingSchema {
3343 path: "/item".to_string(),
3344 name: "Server".to_string(),
3345 description: None,
3346 setting_type: SettingType::Object {
3347 properties: vec![SettingSchema {
3348 path: "/enabled".to_string(),
3349 name: "Enabled".to_string(),
3350 description: None,
3351 setting_type: SettingType::Boolean,
3352 default: Some(serde_json::json!(false)),
3353 read_only: false,
3354 section: None,
3355 order: None,
3356 nullable: false,
3357 enum_from: None,
3358 dual_list_sibling: None,
3359 dynamically_extendable_status_bar_elements: false,
3360 }],
3361 },
3362 default: None,
3363 read_only: false,
3364 section: None,
3365 order: None,
3366 nullable: false,
3367 enum_from: None,
3368 dual_list_sibling: None,
3369 dynamically_extendable_status_bar_elements: false,
3370 };
3371
3372 let value_schema = SettingSchema {
3377 path: String::new(),
3378 name: "value".to_string(),
3379 description: None,
3380 setting_type: SettingType::ObjectArray {
3381 item_schema: Box::new(item_schema.clone()),
3382 display_field: None,
3383 },
3384 default: None,
3385 read_only: false,
3386 section: None,
3387 order: None,
3388 nullable: false,
3389 enum_from: None,
3390 dual_list_sibling: None,
3391 dynamically_extendable_status_bar_elements: false,
3392 };
3393
3394 let parent = EntryDialogState::from_schema(
3398 "quicklsp".to_string(),
3399 &serde_json::json!([{ "enabled": true }]),
3400 &value_schema,
3401 "/universal_lsp",
3402 false, false,
3404 &HashMap::new(),
3405 );
3406
3407 assert!(
3409 parent.is_single_value,
3410 "array value_schema should trigger is_single_value path"
3411 );
3412 assert_eq!(parent.entry_path(), "/universal_lsp/quicklsp");
3413
3414 state.entry_dialog_stack.push(parent);
3415
3416 state.open_nested_entry_dialog();
3421
3422 assert_eq!(
3424 state.entry_dialog_stack.len(),
3425 2,
3426 "open_nested_entry_dialog should have pushed a nested dialog"
3427 );
3428
3429 let nested_map_path = state
3432 .entry_dialog_stack
3433 .last()
3434 .map(|d| d.map_path.clone())
3435 .unwrap();
3436 assert_eq!(
3437 nested_map_path, "/universal_lsp/quicklsp",
3438 "BUG: nested dialog's map_path dropped the 'quicklsp' key segment"
3439 );
3440
3441 state.save_entry_dialog();
3443
3444 assert_eq!(state.entry_dialog_stack.len(), 1);
3446
3447 assert!(
3450 !state.pending_changes.contains_key("/universal_lsp/"),
3451 "regression: pending change recorded under empty-key path /universal_lsp/. \
3452 All keys: {:?}",
3453 state.pending_changes.keys().collect::<Vec<_>>()
3454 );
3455 assert!(
3456 !state
3457 .pending_changes
3458 .keys()
3459 .any(|k| k.starts_with("/universal_lsp") && k.ends_with('/')),
3460 "no /universal_lsp/* path should end in a trailing slash; got {:?}",
3461 state.pending_changes.keys().collect::<Vec<_>>()
3462 );
3463 assert!(
3464 state
3465 .pending_changes
3466 .contains_key("/universal_lsp/quicklsp"),
3467 "expected pending change at /universal_lsp/quicklsp, got {:?}",
3468 state.pending_changes.keys().collect::<Vec<_>>()
3469 );
3470 }
3471
3472 #[test]
3473 fn test_refresh_dual_list_sibling_updates_excluded() {
3474 use crate::view::controls::DualListState;
3475
3476 let schema = include_str!("../../../plugins/config-schema.json");
3479 let config = test_config();
3480 let mut state = SettingsState::new(schema, &config).unwrap();
3481
3482 let editor_page_idx = state
3484 .pages
3485 .iter()
3486 .position(|p| p.path == "/editor")
3487 .expect("editor page");
3488 state.selected_category = editor_page_idx;
3489
3490 let (left_idx, right_idx) = {
3491 let page = &state.pages[editor_page_idx];
3492 let l = page
3493 .items
3494 .iter()
3495 .position(|i| i.path == "/editor/status_bar/left")
3496 .expect("left item");
3497 let r = page
3498 .items
3499 .iter()
3500 .position(|i| i.path == "/editor/status_bar/right")
3501 .expect("right item");
3502 (l, r)
3503 };
3504
3505 assert!(matches!(
3507 &state.pages[editor_page_idx].items[left_idx].control,
3508 SettingControl::DualList(_)
3509 ));
3510
3511 let default_right_items: Vec<String> =
3513 match &state.pages[editor_page_idx].items[right_idx].control {
3514 SettingControl::DualList(dl) => dl.included.clone(),
3515 _ => panic!("right should be DualList"),
3516 };
3517 let initial_left_excluded: Vec<String> =
3518 match &state.pages[editor_page_idx].items[left_idx].control {
3519 SettingControl::DualList(dl) => dl.excluded.clone(),
3520 _ => panic!("left should be DualList"),
3521 };
3522 assert_eq!(
3523 initial_left_excluded, default_right_items,
3524 "left.excluded should mirror right's included on initial build"
3525 );
3526
3527 let new_element = "{chord}".to_string();
3529 state.selected_item = left_idx;
3530 state
3531 .with_current_dual_list_mut(|dl: &mut DualListState| {
3532 if !dl.included.contains(&new_element) {
3533 dl.included.push(new_element.clone());
3534 }
3535 })
3536 .expect("current item is a DualList");
3537
3538 state.refresh_dual_list_sibling();
3540
3541 match &state.pages[editor_page_idx].items[right_idx].control {
3542 SettingControl::DualList(dl) => {
3543 assert!(
3544 dl.excluded.contains(&new_element),
3545 "right.excluded should be updated to reflect left's new inclusion"
3546 );
3547 }
3548 _ => panic!("right should be DualList"),
3549 }
3550 }
3551
3552 #[test]
3553 fn test_with_dual_list_mut_returns_none_for_non_dual_list() {
3554 let config = test_config();
3555 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3556
3557 let result = state.with_dual_list_mut(0, |_| ());
3559 assert!(result.is_none());
3560 }
3561}