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 fn paths_intersect(a: &str, b: &str) -> bool {
368 if a.is_empty() || b.is_empty() {
369 return a == b;
370 }
371 if a == b {
372 return true;
373 }
374 let a_prefix = format!("{}/", a.trim_end_matches('/'));
375 let b_prefix = format!("{}/", b.trim_end_matches('/'));
376 a.starts_with(&b_prefix) || b.starts_with(&a_prefix)
377 }
378
379 pub fn path_has_pending_change(&self, path: &str) -> bool {
383 self.pending_changes
384 .keys()
385 .any(|pending| Self::paths_intersect(path, pending))
386 || self
387 .pending_deletions
388 .iter()
389 .any(|pending| Self::paths_intersect(path, pending))
390 }
391
392 pub fn page_has_pending_changes(&self, page_idx: usize) -> bool {
393 let Some(page) = self.pages.get(page_idx) else {
394 return false;
395 };
396 (!page.path.is_empty() && self.path_has_pending_change(&page.path))
397 || page
398 .items
399 .iter()
400 .any(|item| self.path_has_pending_change(&item.path))
401 }
402
403 fn schema_default_for_path(&self, path: &str) -> Option<serde_json::Value> {
404 self.pages
405 .iter()
406 .flat_map(|page| &page.items)
407 .find(|item| item.path == path)
408 .and_then(|item| item.default.clone())
409 }
410
411 fn effective_original_value(&self, path: &str) -> Option<serde_json::Value> {
418 self.original_config
419 .pointer(path)
420 .cloned()
421 .or_else(|| self.schema_default_for_path(path))
422 }
423
424 fn value_matches_effective_original(&self, path: &str, value: &serde_json::Value) -> bool {
425 self.effective_original_value(path).as_ref() == Some(value)
426 }
427
428 pub fn hide(&mut self) {
430 self.visible = false;
431 self.search_active = false;
432 self.search_query.clear();
433 }
434
435 pub fn entry_dialog(&self) -> Option<&EntryDialogState> {
437 self.entry_dialog_stack.last()
438 }
439
440 pub fn entry_dialog_mut(&mut self) -> Option<&mut EntryDialogState> {
442 self.entry_dialog_stack.last_mut()
443 }
444
445 pub fn has_entry_dialog(&self) -> bool {
447 !self.entry_dialog_stack.is_empty()
448 }
449
450 pub fn current_page(&self) -> Option<&SettingsPage> {
452 self.pages.get(self.selected_category)
453 }
454
455 pub fn current_page_mut(&mut self) -> Option<&mut SettingsPage> {
457 self.pages.get_mut(self.selected_category)
458 }
459
460 pub fn topmost_visible_item_index(&self) -> Option<usize> {
465 let page = self.pages.get(self.selected_category)?;
466 if page.items.is_empty() {
467 return None;
468 }
469 let target = self.scroll_panel.scroll.offset;
470 let width = self.layout_width;
471 let mut y: u16 = 0;
472 for (idx, item) in page.items.iter().enumerate() {
473 let h = <SettingItem as ScrollItem>::height(item, width);
474 if y + h > target {
475 return Some(idx);
476 }
477 y += h;
478 }
479 Some(page.items.len() - 1)
480 }
481
482 pub fn current_section_index(&self) -> Option<usize> {
487 let page = self.pages.get(self.selected_category)?;
488 if page.sections.is_empty() {
489 return None;
490 }
491 let item_idx = self
500 .topmost_visible_item_index()
501 .unwrap_or(self.selected_item);
502 let mut current: Option<usize> = None;
504 for (s_idx, section) in page.sections.iter().enumerate() {
505 if section.first_item_index <= item_idx {
506 current = Some(s_idx);
507 } else {
508 break;
509 }
510 }
511 current
512 }
513
514 pub fn is_category_expandable(&self, cat_idx: usize) -> bool {
518 self.pages
519 .get(cat_idx)
520 .is_some_and(|p| p.sections.len() > 1)
521 }
522
523 pub fn tree_step(&mut self, delta: i32) {
533 let rows = self.visible_tree();
534 if rows.is_empty() {
535 return;
536 }
537 let cur = self.tree_cursor_index(&rows);
538 let len = rows.len() as i32;
539 let target = (cur as i32 + delta).clamp(0, len - 1) as usize;
540 if target == cur {
541 return;
542 }
543 let prev_category = self.selected_category;
544 self.update_control_focus(false);
545 match rows[target] {
546 TreeRow::Category { idx, .. } => {
547 self.selected_category = idx;
551 self.selected_item = 0;
552 self.tree_cursor_section = None;
553 if idx != prev_category {
554 self.scroll_panel = ScrollablePanel::new();
555 }
556 self.sub_focus = None;
557 self.update_control_focus(true);
558 }
559 TreeRow::Section {
560 cat_idx,
561 section_idx,
562 } => {
563 let first = self.pages[cat_idx].sections[section_idx].first_item_index;
564 self.selected_category = cat_idx;
565 self.selected_item = first;
566 self.tree_cursor_section = Some(section_idx);
567 if cat_idx != prev_category {
568 self.scroll_panel = ScrollablePanel::new();
569 }
570 self.sub_focus = None;
571 self.init_map_focus(true);
572 self.update_control_focus(true);
573 }
574 }
575 let width = self.layout_width;
581 if let Some(page) = self.pages.get(self.selected_category) {
582 if matches!(rows[target], TreeRow::Section { .. }) {
590 self.scroll_panel.update_content_height(&page.items, width);
591 let content_width = self.scroll_panel.content_width(width);
592 let item_y =
593 self.scroll_panel
594 .item_y_offset(&page.items, self.selected_item, content_width);
595 self.scroll_panel.scroll.offset = item_y;
596 } else {
597 let selected_item = self.selected_item;
598 let sub_focus = self.sub_focus;
599 self.scroll_panel.ensure_focused_visible(
600 &page.items,
601 selected_item,
602 sub_focus,
603 width,
604 );
605 }
606 }
607 let new_rows = self.visible_tree();
608 let new_cur = self.tree_cursor_index(&new_rows);
609 self.categories_scroll
610 .ensure_focused_visible(&new_rows, new_cur, None, width);
611 }
612
613 pub(super) fn tree_cursor_index(&self, rows: &[TreeRow]) -> usize {
621 let cat = self.selected_category;
622 if let Some(s_idx) = self.tree_cursor_section {
623 for (i, row) in rows.iter().enumerate() {
624 if let TreeRow::Section {
625 cat_idx,
626 section_idx,
627 } = *row
628 {
629 if cat_idx == cat && section_idx == s_idx {
630 return i;
631 }
632 }
633 }
634 }
635 for (i, row) in rows.iter().enumerate() {
636 if let TreeRow::Category { idx, .. } = *row {
637 if idx == cat {
638 return i;
639 }
640 }
641 }
642 0
643 }
644
645 pub fn auto_expand_current_category(&mut self) {
655 let idx = self.selected_category;
656 if self.is_category_expandable(idx) {
657 self.expanded_categories.insert(idx);
658 }
659 }
660
661 pub fn toggle_category_expanded(&mut self, cat_idx: usize) {
662 if !self.is_category_expandable(cat_idx) {
663 return;
664 }
665 if !self.expanded_categories.insert(cat_idx) {
666 self.expanded_categories.remove(&cat_idx);
667 }
668 }
669
670 pub fn jump_to_section(&mut self, cat_idx: usize, section_idx: usize) {
674 let Some(page) = self.pages.get(cat_idx) else {
675 return;
676 };
677 let Some(section) = page.sections.get(section_idx) else {
678 return;
679 };
680 let target_item = section.first_item_index;
681 self.update_control_focus(false);
682 self.selected_category = cat_idx;
683 self.selected_item = target_item;
684 self.tree_cursor_section = Some(section_idx);
685 self.focus.set(FocusPanel::Settings);
686 let width = self.layout_width;
687 if let Some(page) = self.pages.get(self.selected_category) {
688 self.scroll_panel.update_content_height(&page.items, width);
689 let content_width = self.scroll_panel.content_width(width);
690 let item_y = self
697 .scroll_panel
698 .item_y_offset(&page.items, target_item, content_width);
699 self.scroll_panel.scroll.offset = item_y;
700 }
701 self.sub_focus = None;
702 self.init_map_focus(true);
703 self.update_control_focus(true);
704 self.auto_expand_current_category();
705 }
706
707 pub fn visible_tree(&self) -> Vec<TreeRow> {
711 let mut rows = Vec::with_capacity(self.pages.len());
712 for (idx, page) in self.pages.iter().enumerate() {
713 let expandable = page.sections.len() > 1;
714 let expanded = expandable && self.expanded_categories.contains(&idx);
715 rows.push(TreeRow::Category {
716 idx,
717 expandable,
718 expanded,
719 });
720 if expanded {
721 for section_idx in 0..page.sections.len() {
722 rows.push(TreeRow::Section {
723 cat_idx: idx,
724 section_idx,
725 });
726 }
727 }
728 }
729 rows
730 }
731
732 pub fn current_item(&self) -> Option<&SettingItem> {
734 self.current_page()
735 .and_then(|page| page.items.get(self.selected_item))
736 }
737
738 pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
740 self.pages
741 .get_mut(self.selected_category)
742 .and_then(|page| page.items.get_mut(self.selected_item))
743 }
744
745 pub fn can_exit_text_editing(&self) -> bool {
747 self.current_item()
748 .map(|item| {
749 if let SettingControl::Text(state) = &item.control {
750 state.is_valid()
751 } else {
752 true
753 }
754 })
755 .unwrap_or(true)
756 }
757
758 pub fn entry_dialog_can_exit_text_editing(&self) -> bool {
760 self.entry_dialog()
761 .and_then(|dialog| dialog.current_item())
762 .map(|item| {
763 if let SettingControl::Text(state) = &item.control {
764 state.is_valid()
765 } else {
766 true
767 }
768 })
769 .unwrap_or(true)
770 }
771
772 fn init_map_focus(&mut self, from_above: bool) {
775 if let Some(item) = self.current_item_mut() {
776 if let SettingControl::Map(ref mut map_state) = item.control {
777 map_state.init_focus(from_above);
778 }
779 }
780 self.update_map_sub_focus();
782 }
783
784 pub(super) fn update_control_focus(&mut self, focused: bool) {
788 let focus_state = if focused {
789 FocusState::Focused
790 } else {
791 FocusState::Normal
792 };
793 if let Some(item) = self.current_item_mut() {
794 match &mut item.control {
795 SettingControl::Map(ref mut state) => state.focus = focus_state,
796 SettingControl::TextList(ref mut state) => state.focus = focus_state,
797 SettingControl::DualList(ref mut state) => state.focus = focus_state,
798 SettingControl::ObjectArray(ref mut state) => state.focus = focus_state,
799 SettingControl::Toggle(ref mut state) => state.focus = focus_state,
800 SettingControl::Number(ref mut state) => state.focus = focus_state,
801 SettingControl::Dropdown(ref mut state) => state.focus = focus_state,
802 SettingControl::Text(ref mut state) => {
803 state.focus = focus_state;
804 if !focused {
808 state.editing = false;
809 }
810 }
811 SettingControl::Json(_) | SettingControl::Complex { .. } => {} }
813 }
814 }
815
816 fn update_map_sub_focus(&mut self) {
819 self.sub_focus = self.current_item().and_then(|item| {
820 if let SettingControl::Map(ref map_state) = item.control {
821 Some(match map_state.focused_entry {
823 Some(i) => 1 + i,
824 None => 1 + map_state.entries.len(), })
826 } else {
827 None
828 }
829 });
830 }
831
832 pub fn select_prev(&mut self) {
834 match self.focus_panel() {
835 FocusPanel::Categories => {
836 self.tree_step(-1);
837 }
838 FocusPanel::Settings => {
839 let handled = self
841 .current_item_mut()
842 .and_then(|item| match &mut item.control {
843 SettingControl::Map(map_state) => Some(map_state.focus_prev()),
844 _ => None,
845 })
846 .unwrap_or(false);
847
848 if handled {
849 self.update_map_sub_focus();
851 } else if self.selected_item > 0 {
852 self.update_control_focus(false); self.selected_item -= 1;
854 self.sub_focus = None;
855 self.init_map_focus(false); self.update_control_focus(true); }
858 self.ensure_visible();
859 }
860 FocusPanel::Footer => {
861 if self.footer_button_index > 0 {
863 self.footer_button_index -= 1;
864 }
865 }
866 }
867 }
868
869 pub fn select_next(&mut self) {
871 match self.focus_panel() {
872 FocusPanel::Categories => {
873 self.tree_step(1);
874 }
875 FocusPanel::Settings => {
876 let handled = self
878 .current_item_mut()
879 .and_then(|item| match &mut item.control {
880 SettingControl::Map(map_state) => Some(map_state.focus_next()),
881 _ => None,
882 })
883 .unwrap_or(false);
884
885 if handled {
886 self.update_map_sub_focus();
888 } else {
889 let can_move = self
890 .current_page()
891 .is_some_and(|page| self.selected_item + 1 < page.items.len());
892 if can_move {
893 self.update_control_focus(false); self.selected_item += 1;
895 self.sub_focus = None;
896 self.init_map_focus(true); self.update_control_focus(true); }
899 }
900 self.ensure_visible();
901 }
902 FocusPanel::Footer => {
903 if self.footer_button_index < 2 {
905 self.footer_button_index += 1;
906 }
907 }
908 }
909 }
910
911 pub fn select_next_page(&mut self) {
913 let page_size = self.scroll_panel.viewport_height().max(1);
914 for _ in 0..page_size {
915 self.select_next();
916 }
917 }
918
919 pub fn select_prev_page(&mut self) {
921 let page_size = self.scroll_panel.viewport_height().max(1);
922 for _ in 0..page_size {
923 self.select_prev();
924 }
925 }
926
927 pub fn toggle_focus(&mut self) {
929 let old_panel = self.focus_panel();
930 self.focus.focus_next();
931 self.on_panel_changed(old_panel, true);
932 }
933
934 pub fn toggle_focus_backward(&mut self) {
936 let old_panel = self.focus_panel();
937 self.focus.focus_prev();
938 self.on_panel_changed(old_panel, false);
939 }
940
941 fn on_panel_changed(&mut self, old_panel: FocusPanel, forward: bool) {
943 if old_panel == FocusPanel::Settings {
945 self.update_control_focus(false);
946 }
947
948 if self.focus_panel() == FocusPanel::Settings
950 && self.selected_item >= self.current_page().map_or(0, |p| p.items.len())
951 {
952 self.selected_item = 0;
953 }
954 self.sub_focus = None;
955
956 if self.focus_panel() == FocusPanel::Settings {
957 self.init_map_focus(forward); self.update_control_focus(true); }
960
961 if self.focus_panel() == FocusPanel::Footer {
963 self.footer_button_index = if forward {
964 0 } else {
966 4 };
968 }
969
970 self.ensure_visible();
971 }
972
973 pub fn set_item_style(&mut self, style: super::items::ItemBoxStyle) {
981 if self.item_style == style {
982 return;
983 }
984 self.item_style = style;
985 for page in &mut self.pages {
986 for item in &mut page.items {
987 item.style = style;
988 }
989 }
990 }
991
992 pub fn ensure_visible(&mut self) {
994 if self.focus_panel() != FocusPanel::Settings {
995 return;
996 }
997
998 let selected_item = self.selected_item;
1000 let sub_focus = self.sub_focus;
1001 let width = self.layout_width;
1002 let prev_offset = self.scroll_panel.scroll.offset;
1003 if let Some(page) = self.pages.get(self.selected_category) {
1004 self.scroll_panel
1005 .ensure_focused_visible(&page.items, selected_item, sub_focus, width);
1006 }
1007 if self.scroll_panel.scroll.offset != prev_offset {
1011 self.sync_tree_cursor_to_body_scroll();
1012 }
1013 }
1014
1015 pub fn set_pending_change(&mut self, path: &str, value: serde_json::Value) {
1017 let value = normalize_pending_change(path, value);
1018 if self.value_matches_effective_original(path, &value) {
1019 self.pending_changes.remove(path);
1020 } else {
1021 self.pending_changes.insert(path.to_string(), value);
1022 }
1023 }
1024
1025 pub fn has_changes(&self) -> bool {
1027 !self.pending_changes.is_empty() || !self.pending_deletions.is_empty()
1028 }
1029
1030 pub fn apply_changes(&self, config: &Config) -> Result<Config, serde_json::Error> {
1032 let mut config_value = serde_json::to_value(config)?;
1033
1034 for path in &self.pending_deletions {
1044 crate::config_io::remove_json_pointer(&mut config_value, path);
1045 }
1046
1047 for (path, value) in &self.pending_changes {
1048 let value = normalize_pending_change(path, value.clone());
1049 if let Some(target) = config_value.pointer_mut(path) {
1056 *target = value;
1057 } else {
1058 set_json_pointer_create(&mut config_value, path, value);
1059 }
1060 }
1061
1062 serde_json::from_value(config_value)
1063 }
1064
1065 pub fn discard_changes(&mut self) {
1067 self.pending_changes.clear();
1068 self.pending_deletions.clear();
1069 self.rebuild_pages();
1071 }
1072
1073 pub fn set_target_layer(&mut self, layer: ConfigLayer) {
1075 if layer != ConfigLayer::System {
1076 self.target_layer = layer;
1078 self.pending_changes.clear();
1080 self.pending_deletions.clear();
1081 self.rebuild_pages();
1083 }
1084 }
1085
1086 pub fn cycle_target_layer(&mut self) {
1088 self.target_layer = match self.target_layer {
1089 ConfigLayer::System => ConfigLayer::User, ConfigLayer::User => ConfigLayer::Project,
1091 ConfigLayer::Project => ConfigLayer::Session,
1092 ConfigLayer::Session => ConfigLayer::User,
1093 };
1094 self.pending_changes.clear();
1096 self.pending_deletions.clear();
1097 self.rebuild_pages();
1099 }
1100
1101 pub fn target_layer_name(&self) -> &'static str {
1103 match self.target_layer {
1104 ConfigLayer::System => "System (read-only)",
1105 ConfigLayer::User => "User",
1106 ConfigLayer::Project => "Project",
1107 ConfigLayer::Session => "Session",
1108 }
1109 }
1110
1111 pub fn set_layer_sources(&mut self, sources: HashMap<String, ConfigLayer>) {
1114 self.layer_sources = sources;
1115 self.rebuild_pages();
1117 }
1118
1119 pub fn set_status_bar_tokens(&mut self, tokens: HashMap<String, String>) {
1122 self.available_status_bar_tokens = tokens;
1123 self.rebuild_pages();
1124 }
1125
1126 pub fn get_layer_source(&self, path: &str) -> ConfigLayer {
1129 self.layer_sources
1130 .get(path)
1131 .copied()
1132 .unwrap_or(ConfigLayer::System)
1133 }
1134
1135 pub fn layer_source_label(layer: ConfigLayer) -> &'static str {
1137 match layer {
1138 ConfigLayer::System => "default",
1139 ConfigLayer::User => "user",
1140 ConfigLayer::Project => "project",
1141 ConfigLayer::Session => "session",
1142 }
1143 }
1144
1145 pub fn entry_dialog_activate_focused_field_button(&mut self) -> bool {
1163 match self.entry_dialog_mut() {
1164 Some(dialog) => dialog.activate_focused_field_button(),
1165 None => false,
1166 }
1167 }
1168
1169 pub fn reset_current_to_default(&mut self) {
1170 let reset_info = self.current_item().and_then(|item| {
1172 if !item.modified || item.is_auto_managed {
1175 return None;
1176 }
1177 item.default
1178 .as_ref()
1179 .map(|default| (item.path.clone(), default.clone()))
1180 });
1181
1182 if let Some((path, default)) = reset_info {
1183 let original_source = self.get_layer_source(&path);
1184
1185 if original_source != self.target_layer {
1186 self.pending_changes.remove(&path);
1191 self.pending_deletions.remove(&path);
1192 let original = self.effective_original_value(&path).unwrap_or(default);
1193 if let Some(item) = self.current_item_mut() {
1194 update_control_from_value(&mut item.control, &original);
1195 item.modified = false;
1196 item.layer_source = original_source;
1197 item.is_null = item.nullable && original.is_null();
1198 }
1199 return;
1200 }
1201
1202 self.pending_deletions.insert(path.clone());
1204 self.pending_changes.remove(&path);
1206
1207 if let Some(item) = self.current_item_mut() {
1211 update_control_from_value(&mut item.control, &default);
1212 item.modified = false;
1213 item.layer_source = ConfigLayer::System; }
1216 }
1217 }
1218
1219 pub fn set_current_to_null(&mut self) {
1225 let target_layer = self.target_layer;
1226 let change_info = self.current_item().and_then(|item| {
1227 if !item.nullable || item.is_null || item.read_only {
1228 return None;
1229 }
1230 Some(item.path.clone())
1231 });
1232
1233 if let Some(path) = change_info {
1234 self.pending_changes
1236 .insert(path.clone(), serde_json::Value::Null);
1237 self.pending_deletions.remove(&path);
1238
1239 if let Some(item) = self.current_item_mut() {
1241 item.is_null = true;
1242 item.modified = true;
1243 item.layer_source = target_layer;
1244 }
1245 }
1246 }
1247
1248 pub fn clear_current_category(&mut self) {
1254 let target_layer = self.target_layer;
1255 let page = match self.current_page() {
1256 Some(p) if p.nullable => p,
1257 _ => return,
1258 };
1259 let page_path = page.path.clone();
1260
1261 self.pending_changes
1263 .insert(page_path.clone(), serde_json::Value::Null);
1264
1265 let prefix = format!("{}/", page_path);
1267 self.pending_changes
1268 .retain(|path, _| !path.starts_with(&prefix));
1269 self.pending_deletions
1270 .retain(|path| !path.starts_with(&prefix));
1271
1272 if let Some(page) = self.current_page_mut() {
1274 for item in &mut page.items {
1275 if item.nullable {
1276 item.is_null = true;
1277 item.modified = false;
1278 item.layer_source = target_layer;
1279 }
1280 }
1281 }
1282 }
1283
1284 pub fn current_category_has_values(&self) -> bool {
1286 match self.current_page() {
1287 Some(page) if page.nullable => {
1288 page.items.iter().any(|item| !item.is_null && item.nullable)
1289 || page.items.iter().any(|item| item.modified)
1290 }
1291 _ => false,
1292 }
1293 }
1294
1295 pub fn on_value_changed(&mut self) {
1297 let target_layer = self.target_layer;
1299
1300 let change_info = self.current_item().map(|item| {
1302 let value = control_to_value(&item.control);
1303 (item.path.clone(), value)
1304 });
1305
1306 if let Some((path, value)) = change_info {
1307 let original_value = self.effective_original_value(&path);
1308 let matches_original = original_value.as_ref() == Some(&value);
1309 let original_source = self.get_layer_source(&path);
1310
1311 self.pending_deletions.remove(&path);
1314 self.set_pending_change(&path, value);
1315
1316 if let Some(item) = self.current_item_mut() {
1318 if matches_original {
1319 item.modified = !item.is_auto_managed && original_source == target_layer;
1320 item.layer_source = original_source;
1321 item.is_null = item.nullable
1322 && original_value
1323 .as_ref()
1324 .map(|v| v.is_null())
1325 .unwrap_or_else(|| {
1326 item.default.as_ref().map(|d| d.is_null()).unwrap_or(true)
1327 });
1328 } else {
1329 item.modified = true; item.layer_source = target_layer; item.is_null = false; }
1333 }
1334 }
1335 }
1336
1337 pub fn update_focus_states(&mut self) {
1339 let current_focus = self.focus_panel();
1340 for (page_idx, page) in self.pages.iter_mut().enumerate() {
1341 for (item_idx, item) in page.items.iter_mut().enumerate() {
1342 let is_focused = current_focus == FocusPanel::Settings
1343 && page_idx == self.selected_category
1344 && item_idx == self.selected_item;
1345
1346 let focus = if is_focused {
1347 FocusState::Focused
1348 } else {
1349 FocusState::Normal
1350 };
1351
1352 match &mut item.control {
1353 SettingControl::Toggle(state) => state.focus = focus,
1354 SettingControl::Number(state) => state.focus = focus,
1355 SettingControl::Dropdown(state) => state.focus = focus,
1356 SettingControl::Text(state) => state.focus = focus,
1357 SettingControl::TextList(state) => state.focus = focus,
1358 SettingControl::DualList(state) => state.focus = focus,
1359 SettingControl::Map(state) => state.focus = focus,
1360 SettingControl::ObjectArray(state) => state.focus = focus,
1361 SettingControl::Json(state) => state.focus = focus,
1362 SettingControl::Complex { .. } => {}
1363 }
1364 }
1365 }
1366 }
1367
1368 pub fn start_search(&mut self) {
1370 self.search_active = true;
1371 self.search_query.clear();
1372 self.search_results.clear();
1373 self.selected_search_result = 0;
1374 self.search_scroll_offset = 0;
1375 }
1376
1377 pub fn cancel_search(&mut self) {
1379 self.search_active = false;
1380 self.search_query.clear();
1381 self.search_results.clear();
1382 self.selected_search_result = 0;
1383 self.search_scroll_offset = 0;
1384 }
1385
1386 pub fn set_search_query(&mut self, query: String) {
1388 self.search_query = query;
1389 self.search_results = search_settings(&self.pages, &self.search_query);
1390 self.selected_search_result = 0;
1391 self.search_scroll_offset = 0;
1392 }
1393
1394 pub fn search_push_char(&mut self, c: char) {
1396 self.search_query.push(c);
1397 self.search_results = search_settings(&self.pages, &self.search_query);
1398 self.selected_search_result = 0;
1399 self.search_scroll_offset = 0;
1400 }
1401
1402 pub fn search_pop_char(&mut self) {
1404 self.search_query.pop();
1405 self.search_results = search_settings(&self.pages, &self.search_query);
1406 self.selected_search_result = 0;
1407 self.search_scroll_offset = 0;
1408 }
1409
1410 pub fn search_prev(&mut self) {
1412 if !self.search_results.is_empty() && self.selected_search_result > 0 {
1413 self.selected_search_result -= 1;
1414 if self.selected_search_result < self.search_scroll_offset {
1416 self.search_scroll_offset = self.selected_search_result;
1417 }
1418 }
1419 }
1420
1421 pub fn search_next(&mut self) {
1423 if !self.search_results.is_empty()
1424 && self.selected_search_result + 1 < self.search_results.len()
1425 {
1426 self.selected_search_result += 1;
1427 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1429 self.search_scroll_offset =
1430 self.selected_search_result - self.search_max_visible + 1;
1431 }
1432 }
1433 }
1434
1435 pub fn search_scroll_up(&mut self, delta: usize) -> bool {
1437 if self.search_results.is_empty() || self.search_scroll_offset == 0 {
1438 return false;
1439 }
1440 self.search_scroll_offset = self.search_scroll_offset.saturating_sub(delta);
1441 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1443 self.selected_search_result = self.search_scroll_offset + self.search_max_visible - 1;
1444 }
1445 true
1446 }
1447
1448 pub fn search_scroll_down(&mut self, delta: usize) -> bool {
1450 if self.search_results.is_empty() {
1451 return false;
1452 }
1453 let max_offset = self
1454 .search_results
1455 .len()
1456 .saturating_sub(self.search_max_visible);
1457 if self.search_scroll_offset >= max_offset {
1458 return false;
1459 }
1460 self.search_scroll_offset = (self.search_scroll_offset + delta).min(max_offset);
1461 if self.selected_search_result < self.search_scroll_offset {
1463 self.selected_search_result = self.search_scroll_offset;
1464 }
1465 true
1466 }
1467
1468 pub fn search_scroll_to_ratio(&mut self, ratio: f32) -> bool {
1470 if self.search_results.is_empty() {
1471 return false;
1472 }
1473 let max_offset = self
1474 .search_results
1475 .len()
1476 .saturating_sub(self.search_max_visible);
1477 let new_offset = (ratio * max_offset as f32) as usize;
1478 if new_offset != self.search_scroll_offset {
1479 self.search_scroll_offset = new_offset.min(max_offset);
1480 if self.selected_search_result < self.search_scroll_offset {
1482 self.selected_search_result = self.search_scroll_offset;
1483 } else if self.selected_search_result
1484 >= self.search_scroll_offset + self.search_max_visible
1485 {
1486 self.selected_search_result =
1487 self.search_scroll_offset + self.search_max_visible - 1;
1488 }
1489 return true;
1490 }
1491 false
1492 }
1493
1494 pub fn jump_to_search_result(&mut self) {
1496 let Some(result) = self
1498 .search_results
1499 .get(self.selected_search_result)
1500 .cloned()
1501 else {
1502 return;
1503 };
1504 let page_index = result.page_index;
1505 let item_index = result.item_index;
1506
1507 self.update_control_focus(false);
1509 self.selected_category = page_index;
1510 self.selected_item = item_index;
1511 self.focus.set(FocusPanel::Settings);
1512 self.scroll_panel.scroll.offset = 0;
1514 self.sub_focus = None;
1515 self.init_map_focus(true);
1516
1517 if let Some(ref deep_match) = result.deep_match {
1519 self.jump_to_deep_match(deep_match);
1520 }
1521
1522 self.update_control_focus(true); self.auto_expand_current_category();
1524 self.tree_cursor_section = self.current_section_index();
1528 self.ensure_visible();
1529 self.cancel_search();
1530 }
1531
1532 fn jump_to_deep_match(&mut self, deep_match: &DeepMatch) {
1534 match deep_match {
1535 DeepMatch::MapKey { entry_index, .. } | DeepMatch::MapValue { entry_index, .. } => {
1536 if let Some(item) = self.current_item_mut() {
1537 if let SettingControl::Map(ref mut map_state) = item.control {
1538 map_state.focused_entry = Some(*entry_index);
1539 }
1540 }
1541 self.update_map_sub_focus();
1542 }
1543 DeepMatch::TextListItem { item_index, .. } => {
1544 if let Some(item) = self.current_item_mut() {
1545 if let SettingControl::TextList(ref mut list_state) = item.control {
1546 list_state.focused_item = Some(*item_index);
1547 }
1548 }
1549 self.sub_focus = Some(1 + *item_index);
1551 }
1552 }
1553 }
1554
1555 pub fn current_search_result(&self) -> Option<&SearchResult> {
1557 self.search_results.get(self.selected_search_result)
1558 }
1559
1560 pub fn show_confirm_dialog(&mut self) {
1562 self.showing_confirm_dialog = true;
1563 self.confirm_dialog_selection = 0; }
1565
1566 pub fn hide_confirm_dialog(&mut self) {
1568 self.showing_confirm_dialog = false;
1569 self.confirm_dialog_selection = 0;
1570 }
1571
1572 pub fn confirm_dialog_next(&mut self) {
1574 self.confirm_dialog_selection = (self.confirm_dialog_selection + 1) % 3;
1575 }
1576
1577 pub fn confirm_dialog_prev(&mut self) {
1579 self.confirm_dialog_selection = if self.confirm_dialog_selection == 0 {
1580 2
1581 } else {
1582 self.confirm_dialog_selection - 1
1583 };
1584 }
1585
1586 pub fn toggle_help(&mut self) {
1588 self.showing_help = !self.showing_help;
1589 }
1590
1591 pub fn hide_help(&mut self) {
1593 self.showing_help = false;
1594 }
1595
1596 pub fn showing_entry_dialog(&self) -> bool {
1598 self.has_entry_dialog()
1599 }
1600
1601 pub fn open_entry_dialog(&mut self) {
1603 let Some(item) = self.current_item() else {
1604 return;
1605 };
1606
1607 let path = item.path.as_str();
1609 let SettingControl::Map(map_state) = &item.control else {
1610 return;
1611 };
1612
1613 let Some(entry_idx) = map_state.focused_entry else {
1615 return;
1616 };
1617 let Some((key, value)) = map_state.entries.get(entry_idx) else {
1618 return;
1619 };
1620
1621 let Some(schema) = map_state.value_schema.as_ref() else {
1623 return; };
1625
1626 let no_delete = map_state.no_add;
1628
1629 let entry_pointer = format!("{}/{}", path, key);
1634 let key = key.clone();
1635 let value = value.clone();
1636
1637 let mut dialog = EntryDialogState::from_schema(
1639 key,
1640 &value,
1641 schema,
1642 path,
1643 false,
1644 no_delete,
1645 &self.available_status_bar_tokens,
1646 );
1647 apply_builtin_defaults(&mut dialog, &entry_pointer);
1648 dialog.inheritable_fields = inheritable_fields_for(path);
1649 self.entry_dialog_stack.push(dialog);
1650 }
1651
1652 pub fn open_add_entry_dialog(&mut self) {
1654 let Some(item) = self.current_item() else {
1655 return;
1656 };
1657 let SettingControl::Map(map_state) = &item.control else {
1658 return;
1659 };
1660 let Some(schema) = map_state.value_schema.as_ref() else {
1661 return;
1662 };
1663 let path = item.path.clone();
1664
1665 let dialog = EntryDialogState::from_schema(
1668 String::new(),
1669 &serde_json::json!({}),
1670 schema,
1671 &path,
1672 true,
1673 false,
1674 &self.available_status_bar_tokens,
1675 );
1676 self.entry_dialog_stack.push(dialog);
1677 }
1678
1679 pub fn open_add_array_item_dialog(&mut self) {
1681 let Some(item) = self.current_item() else {
1682 return;
1683 };
1684 let SettingControl::ObjectArray(array_state) = &item.control else {
1685 return;
1686 };
1687 let Some(schema) = array_state.item_schema.as_ref() else {
1688 return;
1689 };
1690 let path = item.path.clone();
1691
1692 let dialog = EntryDialogState::for_array_item(
1694 None,
1695 &serde_json::json!({}),
1696 schema,
1697 &path,
1698 true,
1699 &self.available_status_bar_tokens,
1700 );
1701 self.entry_dialog_stack.push(dialog);
1702 }
1703
1704 pub fn open_edit_array_item_dialog(&mut self) {
1706 let Some(item) = self.current_item() else {
1707 return;
1708 };
1709 let SettingControl::ObjectArray(array_state) = &item.control else {
1710 return;
1711 };
1712 let Some(schema) = array_state.item_schema.as_ref() else {
1713 return;
1714 };
1715 let Some(index) = array_state.focused_index else {
1716 return;
1717 };
1718 let Some(value) = array_state.bindings.get(index) else {
1719 return;
1720 };
1721 let path = item.path.clone();
1722
1723 let dialog = EntryDialogState::for_array_item(
1724 Some(index),
1725 value,
1726 schema,
1727 &path,
1728 false,
1729 &self.available_status_bar_tokens,
1730 );
1731 self.entry_dialog_stack.push(dialog);
1732 }
1733
1734 pub fn close_entry_dialog(&mut self) {
1736 self.entry_dialog_stack.pop();
1737 }
1738
1739 pub fn open_nested_entry_dialog(&mut self) {
1744 let nested_info = self.entry_dialog().and_then(|dialog| {
1746 let item = dialog.current_item()?;
1747 let base = dialog.entry_path();
1753 let relative = item.path.trim_start_matches('/');
1754 let path = if relative.is_empty() {
1755 base
1759 } else {
1760 format!("{}/{}", base, relative)
1761 };
1762
1763 match &item.control {
1764 SettingControl::Map(map_state) => {
1765 let schema = map_state.value_schema.as_ref()?;
1766 let no_delete = map_state.no_add; if let Some(entry_idx) = map_state.focused_entry {
1768 let (key, value) = map_state.entries.get(entry_idx)?;
1770 Some(NestedDialogInfo::MapEntry {
1771 key: key.clone(),
1772 value: value.clone(),
1773 schema: schema.as_ref().clone(),
1774 path,
1775 is_new: false,
1776 no_delete,
1777 })
1778 } else {
1779 Some(NestedDialogInfo::MapEntry {
1781 key: String::new(),
1782 value: serde_json::json!({}),
1783 schema: schema.as_ref().clone(),
1784 path,
1785 is_new: true,
1786 no_delete: false, })
1788 }
1789 }
1790 SettingControl::ObjectArray(array_state) => {
1791 let schema = array_state.item_schema.as_ref()?;
1792 if let Some(index) = array_state.focused_index {
1793 let value = array_state.bindings.get(index)?;
1795 Some(NestedDialogInfo::ArrayItem {
1796 index: Some(index),
1797 value: value.clone(),
1798 schema: schema.as_ref().clone(),
1799 path,
1800 is_new: false,
1801 })
1802 } else {
1803 Some(NestedDialogInfo::ArrayItem {
1805 index: None,
1806 value: serde_json::json!({}),
1807 schema: schema.as_ref().clone(),
1808 path,
1809 is_new: true,
1810 })
1811 }
1812 }
1813 _ => None,
1814 }
1815 });
1816
1817 if let Some(info) = nested_info {
1819 let dialog = match info {
1820 NestedDialogInfo::MapEntry {
1821 key,
1822 value,
1823 schema,
1824 path,
1825 is_new,
1826 no_delete,
1827 } => EntryDialogState::from_schema(
1828 key,
1829 &value,
1830 &schema,
1831 &path,
1832 is_new,
1833 no_delete,
1834 &self.available_status_bar_tokens,
1835 ),
1836 NestedDialogInfo::ArrayItem {
1837 index,
1838 value,
1839 schema,
1840 path,
1841 is_new,
1842 } => EntryDialogState::for_array_item(
1843 index,
1844 &value,
1845 &schema,
1846 &path,
1847 is_new,
1848 &self.available_status_bar_tokens,
1849 ),
1850 };
1851 self.entry_dialog_stack.push(dialog);
1852 }
1853 }
1854
1855 pub fn save_entry_dialog(&mut self) {
1860 let is_array = if self.entry_dialog_stack.len() > 1 {
1864 self.entry_dialog_stack
1866 .get(self.entry_dialog_stack.len() - 2)
1867 .and_then(|parent| parent.current_item())
1868 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1869 .unwrap_or(false)
1870 } else {
1871 self.current_item()
1873 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1874 .unwrap_or(false)
1875 };
1876
1877 if is_array {
1878 self.save_array_item_dialog_inner();
1879 } else {
1880 self.save_map_entry_dialog_inner();
1881 }
1882 }
1883
1884 fn save_map_entry_dialog_inner(&mut self) {
1886 let Some(mut dialog) = self.entry_dialog_stack.pop() else {
1887 return;
1888 };
1889 dialog.commit_pending_list_drafts();
1893
1894 let key = dialog.get_key();
1896 if key.is_empty() {
1897 return; }
1899
1900 let value = dialog.to_value();
1901 let map_path = dialog.map_path.clone();
1902 let original_key = dialog.entry_key.clone();
1903 let is_new = dialog.is_new;
1904 let key_changed = !is_new && key != original_key;
1905
1906 if let Some(item) = self.current_item_mut() {
1908 if let SettingControl::Map(map_state) = &mut item.control {
1909 if key_changed {
1911 if let Some(idx) = map_state
1912 .entries
1913 .iter()
1914 .position(|(k, _)| k == &original_key)
1915 {
1916 map_state.entries.remove(idx);
1917 }
1918 }
1919
1920 if let Some(entry) = map_state.entries.iter_mut().find(|(k, _)| k == &key) {
1922 entry.1 = value.clone();
1923 } else {
1924 map_state.entries.push((key.clone(), value.clone()));
1925 map_state.entries.sort_by(|a, b| a.0.cmp(&b.0));
1926 }
1927 }
1928 }
1929
1930 if key_changed {
1932 let old_path = format!("{}/{}", map_path, original_key);
1933 self.pending_changes
1934 .insert(old_path, serde_json::Value::Null);
1935 }
1936
1937 let path = format!("{}/{}", map_path, key);
1939 self.set_pending_change(&path, value);
1940 }
1941
1942 fn save_array_item_dialog_inner(&mut self) {
1944 let Some(mut dialog) = self.entry_dialog_stack.pop() else {
1945 return;
1946 };
1947 dialog.commit_pending_list_drafts();
1949
1950 let value = dialog.to_value();
1951 let array_path = dialog.map_path.clone();
1952 let is_new = dialog.is_new;
1953 let entry_key = dialog.entry_key.clone();
1954
1955 let is_nested = !self.entry_dialog_stack.is_empty();
1957
1958 if is_nested {
1959 let parent_entry_path = self
1967 .entry_dialog_stack
1968 .last()
1969 .map(|p| p.entry_path())
1970 .unwrap_or_default();
1971 let item_path = array_path
1972 .strip_prefix(parent_entry_path.as_str())
1973 .unwrap_or(&array_path)
1974 .trim_end_matches('/')
1975 .to_string();
1976
1977 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1983 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1984 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1985 if is_new {
1986 array_state.bindings.push(value.clone());
1987 } else if let Ok(index) = entry_key.parse::<usize>() {
1988 if index < array_state.bindings.len() {
1989 array_state.bindings[index] = value.clone();
1990 }
1991 }
1992 parent.user_edited = true;
1993 }
1994 }
1995 }
1996
1997 if let Some(parent) = self.entry_dialog_stack.last() {
2000 if let Some(item) = parent.items.iter().find(|i| i.path == item_path) {
2001 if let SettingControl::ObjectArray(array_state) = &item.control {
2002 let array_value = serde_json::Value::Array(array_state.bindings.clone());
2003 self.set_pending_change(&array_path, array_value);
2004 }
2005 }
2006 }
2007 } else {
2008 if let Some(item) = self.current_item_mut() {
2010 if let SettingControl::ObjectArray(array_state) = &mut item.control {
2011 if is_new {
2012 array_state.bindings.push(value.clone());
2013 } else if let Ok(index) = entry_key.parse::<usize>() {
2014 if index < array_state.bindings.len() {
2015 array_state.bindings[index] = value.clone();
2016 }
2017 }
2018 }
2019 }
2020
2021 if let Some(item) = self.current_item() {
2023 if let SettingControl::ObjectArray(array_state) = &item.control {
2024 let array_value = serde_json::Value::Array(array_state.bindings.clone());
2025 self.set_pending_change(&array_path, array_value);
2026 }
2027 }
2028 }
2029 }
2030
2031 pub fn request_entry_delete_confirm(&mut self) {
2037 let (name, is_array_item) = self
2038 .entry_dialog()
2039 .map(|d| (d.entry_key.clone(), d.is_array_item))
2040 .unwrap_or_default();
2041 self.entry_delete_target_name = if is_array_item { String::new() } else { name };
2045 self.entry_delete_target_is_array_item = is_array_item;
2046 self.entry_delete_confirm_selection = 0;
2047 self.showing_entry_delete_confirm = true;
2048 }
2049
2050 pub fn delete_entry_dialog(&mut self) {
2051 let is_nested = self.entry_dialog_stack.len() > 1;
2053
2054 let Some(dialog) = self.entry_dialog_stack.pop() else {
2055 return;
2056 };
2057
2058 let path = format!("{}/{}", dialog.map_path, dialog.entry_key);
2059
2060 if is_nested {
2062 let map_field = dialog.map_path.rsplit('/').next().unwrap_or("").to_string();
2065 let item_path = format!("/{}", map_field);
2066
2067 if let Some(parent) = self.entry_dialog_stack.last_mut() {
2069 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
2070 if let SettingControl::Map(map_state) = &mut item.control {
2071 if let Some(idx) = map_state
2072 .entries
2073 .iter()
2074 .position(|(k, _)| k == &dialog.entry_key)
2075 {
2076 map_state.remove_entry(idx);
2077 }
2078 }
2079 }
2080 }
2081 } else {
2082 if let Some(item) = self.current_item_mut() {
2084 if let SettingControl::Map(map_state) = &mut item.control {
2085 if let Some(idx) = map_state
2086 .entries
2087 .iter()
2088 .position(|(k, _)| k == &dialog.entry_key)
2089 {
2090 map_state.remove_entry(idx);
2091 }
2092 }
2093 }
2094 }
2095
2096 self.pending_changes.remove(&path);
2104 self.pending_deletions.insert(path);
2105 }
2106
2107 pub fn max_scroll(&self) -> u16 {
2109 self.scroll_panel.scroll.max_offset()
2110 }
2111
2112 pub fn scroll_up(&mut self, delta: usize) -> bool {
2115 let old = self.scroll_panel.scroll.offset;
2116 self.scroll_panel.scroll_up(delta as u16);
2117 let changed = old != self.scroll_panel.scroll.offset;
2118 if changed {
2119 self.sync_tree_cursor_to_body_scroll();
2120 }
2121 changed
2122 }
2123
2124 pub fn scroll_down(&mut self, delta: usize) -> bool {
2127 let old = self.scroll_panel.scroll.offset;
2128 self.scroll_panel.scroll_down(delta as u16);
2129 let changed = old != self.scroll_panel.scroll.offset;
2130 if changed {
2131 self.sync_tree_cursor_to_body_scroll();
2132 }
2133 changed
2134 }
2135
2136 pub fn scroll_to_ratio(&mut self, ratio: f32) -> bool {
2139 let old = self.scroll_panel.scroll.offset;
2140 self.scroll_panel.scroll_to_ratio(ratio);
2141 let changed = old != self.scroll_panel.scroll.offset;
2142 if changed {
2143 self.sync_tree_cursor_to_body_scroll();
2144 }
2145 changed
2146 }
2147
2148 pub(super) fn sync_tree_cursor_to_body_scroll(&mut self) {
2154 if let Some(section_idx) = self.current_section_index() {
2155 self.tree_cursor_section = Some(section_idx);
2156 }
2157 }
2162
2163 pub fn is_number_control(&self) -> bool {
2166 self.current_item()
2167 .is_some_and(|item| matches!(item.control, SettingControl::Number(_)))
2168 }
2169
2170 pub fn start_editing(&mut self) {
2171 if let Some(item) = self.current_item() {
2172 if matches!(
2173 item.control,
2174 SettingControl::TextList(_)
2175 | SettingControl::DualList(_)
2176 | SettingControl::Text(_)
2177 | SettingControl::Map(_)
2178 | SettingControl::Json(_)
2179 ) {
2180 self.editing_text = true;
2181 }
2182 }
2183 if let Some(item) = self.current_item_mut() {
2184 match item.control {
2185 SettingControl::DualList(ref mut dl) => {
2186 dl.editing = true;
2187 }
2188 SettingControl::Text(ref mut state) => {
2189 state.editing = true;
2190 state.arm_replace_on_type();
2195 }
2196 _ => {}
2197 }
2198 }
2199 }
2200
2201 pub fn stop_editing(&mut self) {
2203 self.editing_text = false;
2204 if let Some(item) = self.current_item_mut() {
2205 match item.control {
2206 SettingControl::DualList(ref mut dl) => {
2207 dl.editing = false;
2208 }
2209 SettingControl::Text(ref mut state) => {
2210 state.editing = false;
2211 }
2212 _ => {}
2213 }
2214 }
2215 }
2216
2217 pub fn is_editable_control(&self) -> bool {
2219 self.current_item().is_some_and(|item| {
2220 matches!(
2221 item.control,
2222 SettingControl::TextList(_)
2223 | SettingControl::DualList(_)
2224 | SettingControl::Text(_)
2225 | SettingControl::Map(_)
2226 | SettingControl::Json(_)
2227 )
2228 })
2229 }
2230
2231 pub fn is_editing_json(&self) -> bool {
2233 if !self.editing_text {
2234 return false;
2235 }
2236 self.current_item()
2237 .map(|item| matches!(&item.control, SettingControl::Json(_)))
2238 .unwrap_or(false)
2239 }
2240
2241 pub fn text_insert(&mut self, c: char) {
2243 if let Some(item) = self.current_item_mut() {
2244 match &mut item.control {
2245 SettingControl::TextList(state) => state.insert(c),
2246 SettingControl::Text(state) => state.insert(c),
2247 SettingControl::Map(state) => {
2248 state.new_key_text.insert(state.cursor, c);
2249 state.cursor += c.len_utf8();
2250 }
2251 SettingControl::Json(state) => state.insert(c),
2252 _ => {}
2253 }
2254 }
2255 }
2256
2257 pub fn text_insert_str(&mut self, s: &str) {
2261 if let Some(item) = self.current_item_mut() {
2262 match &mut item.control {
2263 SettingControl::TextList(state) => state.insert_str(s),
2264 SettingControl::Text(state) => state.insert_str(s),
2265 SettingControl::Map(state) => {
2266 for c in s.chars() {
2267 state.new_key_text.insert(state.cursor, c);
2268 state.cursor += c.len_utf8();
2269 }
2270 }
2271 SettingControl::Json(state) => state.insert_str(s),
2272 _ => {}
2273 }
2274 }
2275 }
2276
2277 pub fn paste_into_focused_text(&mut self, text: &str) -> bool {
2284 if let Some(dialog) = self.entry_dialog_mut() {
2285 if dialog.editing_text {
2286 dialog.insert_str(text);
2287 return true;
2288 }
2289 return false;
2290 }
2291 if self.editing_text {
2292 self.text_insert_str(text);
2293 return true;
2294 }
2295 false
2296 }
2297
2298 pub fn text_backspace(&mut self) {
2300 if let Some(item) = self.current_item_mut() {
2301 match &mut item.control {
2302 SettingControl::TextList(state) => state.backspace(),
2303 SettingControl::Text(state) => state.backspace(),
2304 SettingControl::Map(state) => {
2305 if state.cursor > 0 {
2306 let mut char_start = state.cursor - 1;
2307 while char_start > 0 && !state.new_key_text.is_char_boundary(char_start) {
2308 char_start -= 1;
2309 }
2310 state.new_key_text.remove(char_start);
2311 state.cursor = char_start;
2312 }
2313 }
2314 SettingControl::Json(state) => state.backspace(),
2315 _ => {}
2316 }
2317 }
2318 }
2319
2320 pub fn text_move_left(&mut self) {
2322 if let Some(item) = self.current_item_mut() {
2323 match &mut item.control {
2324 SettingControl::TextList(state) => state.move_left(),
2325 SettingControl::Text(state) => state.move_left(),
2326 SettingControl::Map(state) => {
2327 if state.cursor > 0 {
2328 let mut new_pos = state.cursor - 1;
2329 while new_pos > 0 && !state.new_key_text.is_char_boundary(new_pos) {
2330 new_pos -= 1;
2331 }
2332 state.cursor = new_pos;
2333 }
2334 }
2335 SettingControl::Json(state) => state.move_left(),
2336 _ => {}
2337 }
2338 }
2339 }
2340
2341 pub fn text_move_right(&mut self) {
2343 if let Some(item) = self.current_item_mut() {
2344 match &mut item.control {
2345 SettingControl::TextList(state) => state.move_right(),
2346 SettingControl::Text(state) => state.move_right(),
2347 SettingControl::Map(state) => {
2348 if state.cursor < state.new_key_text.len() {
2349 let mut new_pos = state.cursor + 1;
2350 while new_pos < state.new_key_text.len()
2351 && !state.new_key_text.is_char_boundary(new_pos)
2352 {
2353 new_pos += 1;
2354 }
2355 state.cursor = new_pos;
2356 }
2357 }
2358 SettingControl::Json(state) => state.move_right(),
2359 _ => {}
2360 }
2361 }
2362 }
2363
2364 pub fn text_focus_prev(&mut self) {
2366 if let Some(item) = self.current_item_mut() {
2367 match &mut item.control {
2368 SettingControl::TextList(state) => state.focus_prev(),
2369 SettingControl::Map(state) => {
2370 state.focus_prev();
2371 }
2372 _ => {}
2373 }
2374 }
2375 }
2376
2377 pub fn text_focus_next(&mut self) {
2379 if let Some(item) = self.current_item_mut() {
2380 match &mut item.control {
2381 SettingControl::TextList(state) => state.focus_next(),
2382 SettingControl::Map(state) => {
2383 state.focus_next();
2384 }
2385 _ => {}
2386 }
2387 }
2388 }
2389
2390 pub fn text_add_item(&mut self) {
2392 if let Some(item) = self.current_item_mut() {
2393 match &mut item.control {
2394 SettingControl::TextList(state) => state.add_item(),
2395 SettingControl::Map(state) => state.add_entry_from_input(),
2396 _ => {}
2397 }
2398 }
2399 self.on_value_changed();
2401 }
2402
2403 pub fn text_remove_focused(&mut self) {
2405 if let Some(item) = self.current_item_mut() {
2406 match &mut item.control {
2407 SettingControl::TextList(state) => {
2408 if let Some(idx) = state.focused_item {
2409 state.remove_item(idx);
2410 }
2411 }
2412 SettingControl::Map(state) => {
2413 if let Some(idx) = state.focused_entry {
2414 state.remove_entry(idx);
2415 }
2416 }
2417 _ => {}
2418 }
2419 }
2420 self.on_value_changed();
2422 }
2423
2424 pub fn is_editing_dual_list(&self) -> bool {
2426 if !self.editing_text {
2427 return false;
2428 }
2429 self.current_item()
2430 .map(|item| matches!(&item.control, SettingControl::DualList(_)))
2431 .unwrap_or(false)
2432 }
2433
2434 pub fn with_dual_list_mut<R>(
2439 &mut self,
2440 item_idx: usize,
2441 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2442 ) -> Option<R> {
2443 let page = self.pages.get_mut(self.selected_category)?;
2444 let item = page.items.get_mut(item_idx)?;
2445 if let SettingControl::DualList(ref mut state) = item.control {
2446 Some(f(state))
2447 } else {
2448 None
2449 }
2450 }
2451
2452 pub fn with_current_dual_list_mut<R>(
2455 &mut self,
2456 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2457 ) -> Option<R> {
2458 if let Some(item) = self.current_item_mut() {
2459 if let SettingControl::DualList(ref mut state) = item.control {
2460 return Some(f(state));
2461 }
2462 }
2463 None
2464 }
2465
2466 pub fn refresh_dual_list_sibling(&mut self) {
2473 let (new_included, sibling_path) = {
2474 let Some(item) = self.current_item() else {
2475 return;
2476 };
2477 let SettingControl::DualList(state) = &item.control else {
2478 return;
2479 };
2480 let Some(ref sib_path) = item.dual_list_sibling else {
2481 return;
2482 };
2483 (state.included.clone(), sib_path.clone())
2484 };
2485
2486 if let Some(page) = self.pages.get_mut(self.selected_category) {
2488 for other in page.items.iter_mut() {
2489 if other.path == sibling_path {
2490 if let SettingControl::DualList(ref mut sib_state) = other.control {
2491 sib_state.excluded = new_included;
2492 }
2493 break;
2494 }
2495 }
2496 }
2497 }
2498
2499 pub fn json_cursor_up(&mut self) {
2503 if let Some(item) = self.current_item_mut() {
2504 if let SettingControl::Json(state) = &mut item.control {
2505 state.move_up();
2506 }
2507 }
2508 }
2509
2510 pub fn json_cursor_down(&mut self) {
2512 if let Some(item) = self.current_item_mut() {
2513 if let SettingControl::Json(state) = &mut item.control {
2514 state.move_down();
2515 }
2516 }
2517 }
2518
2519 pub fn json_insert_newline(&mut self) {
2521 if let Some(item) = self.current_item_mut() {
2522 if let SettingControl::Json(state) = &mut item.control {
2523 state.insert('\n');
2524 }
2525 }
2526 }
2527
2528 pub fn json_delete(&mut self) {
2530 if let Some(item) = self.current_item_mut() {
2531 if let SettingControl::Json(state) = &mut item.control {
2532 state.delete();
2533 }
2534 }
2535 }
2536
2537 pub fn json_exit_editing(&mut self) {
2539 let is_valid = self
2540 .current_item()
2541 .map(|item| {
2542 if let SettingControl::Json(state) = &item.control {
2543 state.is_valid()
2544 } else {
2545 true
2546 }
2547 })
2548 .unwrap_or(true);
2549
2550 if is_valid {
2551 if let Some(item) = self.current_item_mut() {
2552 if let SettingControl::Json(state) = &mut item.control {
2553 state.commit();
2554 }
2555 }
2556 self.on_value_changed();
2557 } else if let Some(item) = self.current_item_mut() {
2558 if let SettingControl::Json(state) = &mut item.control {
2559 state.revert();
2560 }
2561 }
2562 self.editing_text = false;
2563 }
2564
2565 pub fn json_select_all(&mut self) {
2567 if let Some(item) = self.current_item_mut() {
2568 if let SettingControl::Json(state) = &mut item.control {
2569 state.select_all();
2570 }
2571 }
2572 }
2573
2574 pub fn json_selected_text(&self) -> Option<String> {
2576 if let Some(item) = self.current_item() {
2577 if let SettingControl::Json(state) = &item.control {
2578 return state.selected_text();
2579 }
2580 }
2581 None
2582 }
2583
2584 pub fn json_cursor_up_selecting(&mut self) {
2586 if let Some(item) = self.current_item_mut() {
2587 if let SettingControl::Json(state) = &mut item.control {
2588 state.editor.move_up_selecting();
2589 }
2590 }
2591 }
2592
2593 pub fn json_cursor_down_selecting(&mut self) {
2595 if let Some(item) = self.current_item_mut() {
2596 if let SettingControl::Json(state) = &mut item.control {
2597 state.editor.move_down_selecting();
2598 }
2599 }
2600 }
2601
2602 pub fn json_cursor_left_selecting(&mut self) {
2604 if let Some(item) = self.current_item_mut() {
2605 if let SettingControl::Json(state) = &mut item.control {
2606 state.editor.move_left_selecting();
2607 }
2608 }
2609 }
2610
2611 pub fn json_cursor_right_selecting(&mut self) {
2613 if let Some(item) = self.current_item_mut() {
2614 if let SettingControl::Json(state) = &mut item.control {
2615 state.editor.move_right_selecting();
2616 }
2617 }
2618 }
2619
2620 pub fn is_dropdown_open(&self) -> bool {
2624 self.current_item().is_some_and(|item| {
2625 if let SettingControl::Dropdown(ref d) = item.control {
2626 d.open
2627 } else {
2628 false
2629 }
2630 })
2631 }
2632
2633 pub fn dropdown_toggle(&mut self) {
2635 let mut opened = false;
2636 if let Some(item) = self.current_item_mut() {
2637 if let SettingControl::Dropdown(ref mut d) = item.control {
2638 d.toggle_open();
2639 opened = d.open;
2640 }
2641 }
2642
2643 if opened {
2645 let selected_item = self.selected_item;
2647 let width = self.layout_width;
2648 if let Some(page) = self.pages.get(self.selected_category) {
2649 self.scroll_panel
2651 .ensure_focused_visible(&page.items, selected_item, None, width);
2652 }
2653 }
2654 }
2655
2656 pub fn dropdown_prev(&mut self) {
2658 if let Some(item) = self.current_item_mut() {
2659 if let SettingControl::Dropdown(ref mut d) = item.control {
2660 d.select_prev();
2661 }
2662 }
2663 }
2664
2665 pub fn dropdown_next(&mut self) {
2667 if let Some(item) = self.current_item_mut() {
2668 if let SettingControl::Dropdown(ref mut d) = item.control {
2669 d.select_next();
2670 }
2671 }
2672 }
2673
2674 pub fn dropdown_home(&mut self) {
2676 if let Some(item) = self.current_item_mut() {
2677 if let SettingControl::Dropdown(ref mut d) = item.control {
2678 if !d.options.is_empty() {
2679 d.selected = 0;
2680 d.ensure_visible();
2681 }
2682 }
2683 }
2684 }
2685
2686 pub fn dropdown_end(&mut self) {
2688 if let Some(item) = self.current_item_mut() {
2689 if let SettingControl::Dropdown(ref mut d) = item.control {
2690 if !d.options.is_empty() {
2691 d.selected = d.options.len() - 1;
2692 d.ensure_visible();
2693 }
2694 }
2695 }
2696 }
2697
2698 pub fn dropdown_confirm(&mut self) {
2700 if let Some(item) = self.current_item_mut() {
2701 if let SettingControl::Dropdown(ref mut d) = item.control {
2702 d.confirm();
2703 }
2704 }
2705 self.on_value_changed();
2706 }
2707
2708 pub fn dropdown_cancel(&mut self) {
2710 if let Some(item) = self.current_item_mut() {
2711 if let SettingControl::Dropdown(ref mut d) = item.control {
2712 d.cancel();
2713 }
2714 }
2715 }
2716
2717 pub fn dropdown_select(&mut self, option_idx: usize) {
2719 if let Some(item) = self.current_item_mut() {
2720 if let SettingControl::Dropdown(ref mut d) = item.control {
2721 if option_idx < d.options.len() {
2722 d.selected = option_idx;
2723 d.confirm();
2724 }
2725 }
2726 }
2727 self.on_value_changed();
2728 }
2729
2730 pub fn set_dropdown_hover(&mut self, hover_idx: Option<usize>) -> bool {
2733 if let Some(item) = self.current_item_mut() {
2734 if let SettingControl::Dropdown(ref mut d) = item.control {
2735 if d.open && d.hover_index != hover_idx {
2736 d.hover_index = hover_idx;
2737 return true;
2738 }
2739 }
2740 }
2741 false
2742 }
2743
2744 pub fn dropdown_scroll(&mut self, delta: i32) {
2746 if let Some(item) = self.current_item_mut() {
2747 if let SettingControl::Dropdown(ref mut d) = item.control {
2748 if d.open {
2749 d.scroll_by(delta);
2750 }
2751 }
2752 }
2753 }
2754
2755 pub fn is_number_editing(&self) -> bool {
2759 self.current_item().is_some_and(|item| {
2760 if let SettingControl::Number(ref n) = item.control {
2761 n.editing()
2762 } else {
2763 false
2764 }
2765 })
2766 }
2767
2768 pub fn start_number_editing(&mut self) {
2770 if let Some(item) = self.current_item_mut() {
2771 if let SettingControl::Number(ref mut n) = item.control {
2772 n.start_editing();
2773 }
2774 }
2775 }
2776
2777 pub fn number_insert(&mut self, c: char) {
2779 if let Some(item) = self.current_item_mut() {
2780 if let SettingControl::Number(ref mut n) = item.control {
2781 n.insert_char(c);
2782 }
2783 }
2784 }
2785
2786 pub fn number_backspace(&mut self) {
2788 if let Some(item) = self.current_item_mut() {
2789 if let SettingControl::Number(ref mut n) = item.control {
2790 n.backspace();
2791 }
2792 }
2793 }
2794
2795 pub fn number_confirm(&mut self) {
2797 if let Some(item) = self.current_item_mut() {
2798 if let SettingControl::Number(ref mut n) = item.control {
2799 n.confirm_editing();
2800 }
2801 }
2802 self.on_value_changed();
2803 }
2804
2805 pub fn number_cancel(&mut self) {
2807 if let Some(item) = self.current_item_mut() {
2808 if let SettingControl::Number(ref mut n) = item.control {
2809 n.cancel_editing();
2810 }
2811 }
2812 }
2813
2814 pub fn number_delete(&mut self) {
2816 if let Some(item) = self.current_item_mut() {
2817 if let SettingControl::Number(ref mut n) = item.control {
2818 n.delete();
2819 }
2820 }
2821 }
2822
2823 pub fn number_move_left(&mut self) {
2825 if let Some(item) = self.current_item_mut() {
2826 if let SettingControl::Number(ref mut n) = item.control {
2827 n.move_left();
2828 }
2829 }
2830 }
2831
2832 pub fn number_move_right(&mut self) {
2834 if let Some(item) = self.current_item_mut() {
2835 if let SettingControl::Number(ref mut n) = item.control {
2836 n.move_right();
2837 }
2838 }
2839 }
2840
2841 pub fn number_move_home(&mut self) {
2843 if let Some(item) = self.current_item_mut() {
2844 if let SettingControl::Number(ref mut n) = item.control {
2845 n.move_home();
2846 }
2847 }
2848 }
2849
2850 pub fn number_move_end(&mut self) {
2852 if let Some(item) = self.current_item_mut() {
2853 if let SettingControl::Number(ref mut n) = item.control {
2854 n.move_end();
2855 }
2856 }
2857 }
2858
2859 pub fn number_move_left_selecting(&mut self) {
2861 if let Some(item) = self.current_item_mut() {
2862 if let SettingControl::Number(ref mut n) = item.control {
2863 n.move_left_selecting();
2864 }
2865 }
2866 }
2867
2868 pub fn number_move_right_selecting(&mut self) {
2870 if let Some(item) = self.current_item_mut() {
2871 if let SettingControl::Number(ref mut n) = item.control {
2872 n.move_right_selecting();
2873 }
2874 }
2875 }
2876
2877 pub fn number_move_home_selecting(&mut self) {
2879 if let Some(item) = self.current_item_mut() {
2880 if let SettingControl::Number(ref mut n) = item.control {
2881 n.move_home_selecting();
2882 }
2883 }
2884 }
2885
2886 pub fn number_move_end_selecting(&mut self) {
2888 if let Some(item) = self.current_item_mut() {
2889 if let SettingControl::Number(ref mut n) = item.control {
2890 n.move_end_selecting();
2891 }
2892 }
2893 }
2894
2895 pub fn number_move_word_left(&mut self) {
2897 if let Some(item) = self.current_item_mut() {
2898 if let SettingControl::Number(ref mut n) = item.control {
2899 n.move_word_left();
2900 }
2901 }
2902 }
2903
2904 pub fn number_move_word_right(&mut self) {
2906 if let Some(item) = self.current_item_mut() {
2907 if let SettingControl::Number(ref mut n) = item.control {
2908 n.move_word_right();
2909 }
2910 }
2911 }
2912
2913 pub fn number_move_word_left_selecting(&mut self) {
2915 if let Some(item) = self.current_item_mut() {
2916 if let SettingControl::Number(ref mut n) = item.control {
2917 n.move_word_left_selecting();
2918 }
2919 }
2920 }
2921
2922 pub fn number_move_word_right_selecting(&mut self) {
2924 if let Some(item) = self.current_item_mut() {
2925 if let SettingControl::Number(ref mut n) = item.control {
2926 n.move_word_right_selecting();
2927 }
2928 }
2929 }
2930
2931 pub fn number_select_all(&mut self) {
2933 if let Some(item) = self.current_item_mut() {
2934 if let SettingControl::Number(ref mut n) = item.control {
2935 n.select_all();
2936 }
2937 }
2938 }
2939
2940 pub fn number_delete_word_backward(&mut self) {
2942 if let Some(item) = self.current_item_mut() {
2943 if let SettingControl::Number(ref mut n) = item.control {
2944 n.delete_word_backward();
2945 }
2946 }
2947 }
2948
2949 pub fn number_delete_word_forward(&mut self) {
2951 if let Some(item) = self.current_item_mut() {
2952 if let SettingControl::Number(ref mut n) = item.control {
2953 n.delete_word_forward();
2954 }
2955 }
2956 }
2957
2958 pub fn get_change_descriptions(&self) -> Vec<String> {
2960 let mut descriptions: Vec<String> = self
2961 .pending_changes
2962 .iter()
2963 .map(|(path, value)| {
2964 let value_str = match value {
2965 serde_json::Value::Bool(b) => b.to_string(),
2966 serde_json::Value::Number(n) => n.to_string(),
2967 serde_json::Value::String(s) => format!("\"{}\"", s),
2968 _ => value.to_string(),
2969 };
2970 format!("{}: {}", path, value_str)
2971 })
2972 .collect();
2973 for path in &self.pending_deletions {
2975 descriptions.push(format!("{}: (reset to default)", path));
2976 }
2977 descriptions.sort();
2978 descriptions
2979 }
2980}
2981
2982fn inheritable_fields_for(map_path: &str) -> std::collections::HashSet<String> {
2990 if map_path == "/languages" {
2991 serde_json::to_value(crate::config::EditorConfig::default())
2992 .ok()
2993 .and_then(|v| {
2994 v.as_object().map(|o| {
2995 o.keys()
2996 .cloned()
2997 .collect::<std::collections::HashSet<String>>()
2998 })
2999 })
3000 .unwrap_or_default()
3001 } else {
3002 std::collections::HashSet::new()
3003 }
3004}
3005
3006fn apply_builtin_defaults(dialog: &mut EntryDialogState, entry_pointer: &str) {
3012 let Ok(default_cfg) = serde_json::to_value(Config::default()) else {
3013 return;
3014 };
3015 let Some(entry) = default_cfg.pointer(entry_pointer) else {
3016 return;
3017 };
3018 for item in &mut dialog.items {
3019 if item.path == "__key__" {
3020 continue;
3021 }
3022 let field = item.path.trim_start_matches('/');
3023 if let Some(v) = entry.get(field) {
3024 item.default = Some(v.clone());
3025 }
3026 }
3027}
3028
3029pub(crate) fn update_control_from_value(control: &mut SettingControl, value: &serde_json::Value) {
3030 match control {
3031 SettingControl::Toggle(state) => {
3032 if let Some(b) = value.as_bool() {
3033 state.checked = b;
3034 }
3035 }
3036 SettingControl::Number(state) => {
3037 if let Some(n) = value.as_i64() {
3038 state.value = n;
3039 }
3040 }
3041 SettingControl::Dropdown(state) => {
3042 if let Some(s) = value.as_str() {
3043 if let Some(idx) = state.options.iter().position(|o| o == s) {
3044 state.selected = idx;
3045 }
3046 }
3047 }
3048 SettingControl::Text(state) => {
3049 if let Some(s) = value.as_str() {
3050 state.value = s.to_string();
3051 state.cursor = state.value.len();
3052 }
3053 }
3054 SettingControl::TextList(state) => {
3055 if let Some(arr) = value.as_array() {
3056 state.items = arr
3057 .iter()
3058 .filter_map(|v| {
3059 if state.is_integer {
3060 v.as_i64()
3061 .map(|n| n.to_string())
3062 .or_else(|| v.as_u64().map(|n| n.to_string()))
3063 .or_else(|| v.as_f64().map(|n| n.to_string()))
3064 } else {
3065 v.as_str().map(String::from)
3066 }
3067 })
3068 .collect();
3069 }
3070 }
3071 SettingControl::DualList(state) => {
3072 if let Some(arr) = value.as_array() {
3073 state.included = arr
3074 .iter()
3075 .filter_map(|v| v.as_str().map(String::from))
3076 .collect();
3077 }
3078 }
3079 SettingControl::Map(state) => {
3080 if let Some(obj) = value.as_object() {
3081 state.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
3082 state.entries.sort_by(|a, b| a.0.cmp(&b.0));
3083 }
3084 }
3085 SettingControl::ObjectArray(state) => {
3086 if let Some(arr) = value.as_array() {
3087 state.bindings = arr.clone();
3088 }
3089 }
3090 SettingControl::Json(state) => {
3091 let json_str =
3093 serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_string());
3094 let json_str = if json_str.is_empty() {
3095 "null".to_string()
3096 } else {
3097 json_str
3098 };
3099 state.original_text = json_str.clone();
3100 state.editor.set_value(&json_str);
3101 state.scroll_offset = 0;
3102 }
3103 SettingControl::Complex { .. } => {}
3104 }
3105}
3106
3107fn normalize_pending_change(path: &str, value: serde_json::Value) -> serde_json::Value {
3108 if path == "/editor/indentation_guide_glyph" {
3109 if let serde_json::Value::String(value) = value {
3110 return serde_json::Value::String(crate::config::normalize_indentation_guide_glyph(
3111 &value,
3112 ));
3113 }
3114 }
3115
3116 value
3117}
3118
3119#[cfg(test)]
3120mod tests {
3121 use super::*;
3122
3123 const TEST_SCHEMA: &str = r#"
3124{
3125 "type": "object",
3126 "properties": {
3127 "theme": {
3128 "type": "string",
3129 "default": "dark"
3130 },
3131 "line_numbers": {
3132 "type": "boolean",
3133 "default": true
3134 }
3135 },
3136 "$defs": {}
3137}
3138"#;
3139
3140 fn test_config() -> Config {
3141 Config::default()
3142 }
3143
3144 #[test]
3145 fn test_settings_state_creation() {
3146 let config = test_config();
3147 let state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3148
3149 assert!(!state.visible);
3150 assert_eq!(state.selected_category, 0);
3151 assert!(!state.has_changes());
3152 }
3153
3154 #[test]
3155 fn test_navigation() {
3156 let config = test_config();
3157 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3158
3159 assert_eq!(state.focus_panel(), FocusPanel::Categories);
3161
3162 state.toggle_focus();
3164 assert_eq!(state.focus_panel(), FocusPanel::Settings);
3165
3166 state.select_next();
3168 assert_eq!(state.selected_item, 1);
3169
3170 state.select_prev();
3171 assert_eq!(state.selected_item, 0);
3172 }
3173
3174 #[test]
3175 fn test_pending_changes() {
3176 let config = test_config();
3177 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3178
3179 assert!(!state.has_changes());
3180
3181 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
3182 assert!(state.has_changes());
3183
3184 state.discard_changes();
3185 assert!(!state.has_changes());
3186 }
3187
3188 #[test]
3189 fn test_indentation_guide_glyph_pending_change_is_normalized() {
3190 let config = test_config();
3191 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3192
3193 state.set_pending_change(
3194 "/editor/indentation_guide_glyph",
3195 serde_json::Value::String(" ┊ ".to_string()),
3196 );
3197 assert_eq!(
3198 state.pending_changes.get("/editor/indentation_guide_glyph"),
3199 Some(&serde_json::Value::String("┊".to_string()))
3200 );
3201
3202 let config = state.apply_changes(&config).unwrap();
3203 assert_eq!(config.editor.indentation_guide_glyph, "┊");
3204
3205 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3206 state.set_pending_change(
3207 "/editor/indentation_guide_glyph",
3208 serde_json::Value::String(" ".to_string()),
3209 );
3210 assert_eq!(
3211 state.pending_changes.get("/editor/indentation_guide_glyph"),
3212 Some(&serde_json::Value::String("▏".to_string()))
3213 );
3214
3215 state.pending_changes.insert(
3219 "/editor/indentation_guide_glyph".to_string(),
3220 serde_json::Value::String(" A ".to_string()),
3221 );
3222 let config = state.apply_changes(&config).unwrap();
3223 assert_eq!(config.editor.indentation_guide_glyph, "A");
3224 }
3225
3226 #[test]
3227 fn test_show_hide() {
3228 let config = test_config();
3229 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3230
3231 assert!(!state.visible);
3232
3233 state.show();
3234 assert!(state.visible);
3235 assert_eq!(state.focus_panel(), FocusPanel::Categories);
3236
3237 state.hide();
3238 assert!(!state.visible);
3239 }
3240
3241 const TEST_SCHEMA_CONTROLS: &str = r#"
3243{
3244 "type": "object",
3245 "properties": {
3246 "theme": {
3247 "type": "string",
3248 "enum": ["dark", "light", "high-contrast"],
3249 "default": "dark"
3250 },
3251 "tab_size": {
3252 "type": "integer",
3253 "minimum": 1,
3254 "maximum": 8,
3255 "default": 4
3256 },
3257 "line_numbers": {
3258 "type": "boolean",
3259 "default": true
3260 }
3261 },
3262 "$defs": {}
3263}
3264"#;
3265
3266 const TEST_SCHEMA_THEME_DEFAULT: &str = r#"
3267{
3268 "type": "object",
3269 "properties": {
3270 "theme": {
3271 "type": "string",
3272 "enum": ["dark", "light", "high-contrast"],
3273 "default": "high-contrast"
3274 }
3275 },
3276 "$defs": {}
3277}
3278"#;
3279
3280 fn open_theme_dropdown_state() -> SettingsState {
3281 let config = test_config();
3282 let mut state = SettingsState::new(TEST_SCHEMA_THEME_DEFAULT, &config).unwrap();
3283 state.show();
3284 state.toggle_focus();
3285 state
3286 }
3287
3288 #[test]
3289 fn test_dropdown_toggle() {
3290 let config = test_config();
3291 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3292 state.show();
3293 state.toggle_focus(); state.select_next();
3298 state.select_next();
3299 assert!(!state.is_dropdown_open());
3300
3301 state.dropdown_toggle();
3302 assert!(state.is_dropdown_open());
3303
3304 state.dropdown_toggle();
3305 assert!(!state.is_dropdown_open());
3306 }
3307
3308 #[test]
3309 fn test_dropdown_cancel_restores() {
3310 let config = test_config();
3311 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3312 state.show();
3313 state.toggle_focus();
3314
3315 state.select_next();
3318 state.select_next();
3319
3320 state.dropdown_toggle();
3322 assert!(state.is_dropdown_open());
3323
3324 let initial = state.current_item().and_then(|item| {
3326 if let SettingControl::Dropdown(ref d) = item.control {
3327 Some(d.selected)
3328 } else {
3329 None
3330 }
3331 });
3332
3333 state.dropdown_next();
3335 let after_change = state.current_item().and_then(|item| {
3336 if let SettingControl::Dropdown(ref d) = item.control {
3337 Some(d.selected)
3338 } else {
3339 None
3340 }
3341 });
3342 assert_ne!(initial, after_change);
3343
3344 state.dropdown_cancel();
3346 assert!(!state.is_dropdown_open());
3347
3348 let after_cancel = state.current_item().and_then(|item| {
3349 if let SettingControl::Dropdown(ref d) = item.control {
3350 Some(d.selected)
3351 } else {
3352 None
3353 }
3354 });
3355 assert_eq!(initial, after_cancel);
3356 }
3357
3358 #[test]
3359 fn test_dropdown_confirm_keeps_selection() {
3360 let config = test_config();
3361 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3362 state.show();
3363 state.toggle_focus();
3364
3365 state.dropdown_toggle();
3367
3368 state.dropdown_next();
3370 let after_change = state.current_item().and_then(|item| {
3371 if let SettingControl::Dropdown(ref d) = item.control {
3372 Some(d.selected)
3373 } else {
3374 None
3375 }
3376 });
3377
3378 state.dropdown_confirm();
3380 assert!(!state.is_dropdown_open());
3381
3382 let after_confirm = state.current_item().and_then(|item| {
3383 if let SettingControl::Dropdown(ref d) = item.control {
3384 Some(d.selected)
3385 } else {
3386 None
3387 }
3388 });
3389 assert_eq!(after_change, after_confirm);
3390 }
3391
3392 #[test]
3393 fn dropdown_reverting_to_original_value_clears_pending_and_row_modified() {
3394 let mut state = open_theme_dropdown_state();
3395
3396 state.dropdown_select(0); assert!(state.has_changes());
3398 assert!(state.current_item().unwrap().modified);
3399
3400 state.dropdown_select(2); assert!(!state.has_changes());
3402 let item = state.current_item().unwrap();
3403 assert!(!item.modified);
3404 assert_eq!(item.layer_source, ConfigLayer::System);
3405 }
3406
3407 #[test]
3408 fn reset_after_unsaved_inherited_dropdown_change_cancels_pending_edit() {
3409 let mut state = open_theme_dropdown_state();
3410
3411 state.dropdown_select(1); assert!(state.has_changes());
3413 assert!(state.current_item().unwrap().modified);
3414
3415 state.reset_current_to_default();
3416 assert!(!state.has_changes());
3417 let item = state.current_item().unwrap();
3418 assert!(!item.modified);
3419 assert_eq!(item.layer_source, ConfigLayer::System);
3420 if let SettingControl::Dropdown(dropdown) = &item.control {
3421 assert_eq!(dropdown.selected_value(), Some("high-contrast"));
3422 } else {
3423 panic!("theme should render as a dropdown");
3424 }
3425 }
3426
3427 #[test]
3428 fn test_number_editing() {
3429 let config = test_config();
3430 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3431 state.show();
3432 state.toggle_focus();
3433
3434 state.select_next();
3436
3437 assert!(!state.is_number_editing());
3439
3440 state.start_number_editing();
3442 assert!(state.is_number_editing());
3443
3444 state.number_insert('8');
3446
3447 state.number_confirm();
3449 assert!(!state.is_number_editing());
3450 }
3451
3452 #[test]
3453 fn test_number_cancel_editing() {
3454 let config = test_config();
3455 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3456 state.show();
3457 state.toggle_focus();
3458
3459 state.select_next();
3461
3462 let initial_value = state.current_item().and_then(|item| {
3464 if let SettingControl::Number(ref n) = item.control {
3465 Some(n.value)
3466 } else {
3467 None
3468 }
3469 });
3470
3471 state.start_number_editing();
3473 state.number_backspace();
3474 state.number_insert('9');
3475 state.number_insert('9');
3476
3477 state.number_cancel();
3479 assert!(!state.is_number_editing());
3480
3481 let after_cancel = state.current_item().and_then(|item| {
3483 if let SettingControl::Number(ref n) = item.control {
3484 Some(n.value)
3485 } else {
3486 None
3487 }
3488 });
3489 assert_eq!(initial_value, after_cancel);
3490 }
3491
3492 #[test]
3493 fn test_number_backspace() {
3494 let config = test_config();
3495 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
3496 state.show();
3497 state.toggle_focus();
3498 state.select_next();
3499
3500 state.start_number_editing();
3501 state.number_backspace();
3502
3503 let display_text = state.current_item().and_then(|item| {
3505 if let SettingControl::Number(ref n) = item.control {
3506 Some(n.display_text())
3507 } else {
3508 None
3509 }
3510 });
3511 assert_eq!(display_text, Some(String::new()));
3513
3514 state.number_cancel();
3515 }
3516
3517 #[test]
3518 fn test_layer_selection() {
3519 let config = test_config();
3520 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3521
3522 assert_eq!(state.target_layer, ConfigLayer::User);
3524 assert_eq!(state.target_layer_name(), "User");
3525
3526 state.cycle_target_layer();
3528 assert_eq!(state.target_layer, ConfigLayer::Project);
3529 assert_eq!(state.target_layer_name(), "Project");
3530
3531 state.cycle_target_layer();
3532 assert_eq!(state.target_layer, ConfigLayer::Session);
3533 assert_eq!(state.target_layer_name(), "Session");
3534
3535 state.cycle_target_layer();
3536 assert_eq!(state.target_layer, ConfigLayer::User);
3537
3538 state.set_target_layer(ConfigLayer::Project);
3540 assert_eq!(state.target_layer, ConfigLayer::Project);
3541
3542 state.set_target_layer(ConfigLayer::System);
3544 assert_eq!(state.target_layer, ConfigLayer::Project);
3545 }
3546
3547 #[test]
3548 fn test_layer_switch_clears_pending_changes() {
3549 let config = test_config();
3550 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3551
3552 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
3554 assert!(state.has_changes());
3555
3556 state.cycle_target_layer();
3558 assert!(!state.has_changes());
3559 }
3560
3561 #[test]
3580 fn nested_array_save_records_full_entry_path() {
3581 use crate::view::settings::schema::SettingType;
3584
3585 let config = test_config();
3586 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3587
3588 let item_schema = SettingSchema {
3590 path: "/item".to_string(),
3591 name: "Server".to_string(),
3592 description: None,
3593 setting_type: SettingType::Object {
3594 properties: vec![SettingSchema {
3595 path: "/enabled".to_string(),
3596 name: "Enabled".to_string(),
3597 description: None,
3598 setting_type: SettingType::Boolean,
3599 default: Some(serde_json::json!(false)),
3600 read_only: false,
3601 section: None,
3602 order: None,
3603 nullable: false,
3604 enum_from: None,
3605 dual_list_sibling: None,
3606 dynamically_extendable_status_bar_elements: false,
3607 }],
3608 },
3609 default: None,
3610 read_only: false,
3611 section: None,
3612 order: None,
3613 nullable: false,
3614 enum_from: None,
3615 dual_list_sibling: None,
3616 dynamically_extendable_status_bar_elements: false,
3617 };
3618
3619 let value_schema = SettingSchema {
3624 path: String::new(),
3625 name: "value".to_string(),
3626 description: None,
3627 setting_type: SettingType::ObjectArray {
3628 item_schema: Box::new(item_schema.clone()),
3629 display_field: None,
3630 },
3631 default: None,
3632 read_only: false,
3633 section: None,
3634 order: None,
3635 nullable: false,
3636 enum_from: None,
3637 dual_list_sibling: None,
3638 dynamically_extendable_status_bar_elements: false,
3639 };
3640
3641 let parent = EntryDialogState::from_schema(
3645 "quicklsp".to_string(),
3646 &serde_json::json!([{ "enabled": true }]),
3647 &value_schema,
3648 "/universal_lsp",
3649 false, false,
3651 &HashMap::new(),
3652 );
3653
3654 assert!(
3656 parent.is_single_value,
3657 "array value_schema should trigger is_single_value path"
3658 );
3659 assert_eq!(parent.entry_path(), "/universal_lsp/quicklsp");
3660
3661 state.entry_dialog_stack.push(parent);
3662
3663 state.open_nested_entry_dialog();
3668
3669 assert_eq!(
3671 state.entry_dialog_stack.len(),
3672 2,
3673 "open_nested_entry_dialog should have pushed a nested dialog"
3674 );
3675
3676 let nested_map_path = state
3679 .entry_dialog_stack
3680 .last()
3681 .map(|d| d.map_path.clone())
3682 .unwrap();
3683 assert_eq!(
3684 nested_map_path, "/universal_lsp/quicklsp",
3685 "BUG: nested dialog's map_path dropped the 'quicklsp' key segment"
3686 );
3687
3688 state.save_entry_dialog();
3690
3691 assert_eq!(state.entry_dialog_stack.len(), 1);
3693
3694 assert!(
3697 !state.pending_changes.contains_key("/universal_lsp/"),
3698 "regression: pending change recorded under empty-key path /universal_lsp/. \
3699 All keys: {:?}",
3700 state.pending_changes.keys().collect::<Vec<_>>()
3701 );
3702 assert!(
3703 !state
3704 .pending_changes
3705 .keys()
3706 .any(|k| k.starts_with("/universal_lsp") && k.ends_with('/')),
3707 "no /universal_lsp/* path should end in a trailing slash; got {:?}",
3708 state.pending_changes.keys().collect::<Vec<_>>()
3709 );
3710 assert!(
3711 state
3712 .pending_changes
3713 .contains_key("/universal_lsp/quicklsp"),
3714 "expected pending change at /universal_lsp/quicklsp, got {:?}",
3715 state.pending_changes.keys().collect::<Vec<_>>()
3716 );
3717 }
3718
3719 #[test]
3720 fn test_refresh_dual_list_sibling_updates_excluded() {
3721 use crate::view::controls::DualListState;
3722
3723 let schema = include_str!("../../../plugins/config-schema.json");
3726 let config = test_config();
3727 let mut state = SettingsState::new(schema, &config).unwrap();
3728
3729 let editor_page_idx = state
3731 .pages
3732 .iter()
3733 .position(|p| p.path == "/editor")
3734 .expect("editor page");
3735 state.selected_category = editor_page_idx;
3736
3737 let (left_idx, right_idx) = {
3738 let page = &state.pages[editor_page_idx];
3739 let l = page
3740 .items
3741 .iter()
3742 .position(|i| i.path == "/editor/status_bar/left")
3743 .expect("left item");
3744 let r = page
3745 .items
3746 .iter()
3747 .position(|i| i.path == "/editor/status_bar/right")
3748 .expect("right item");
3749 (l, r)
3750 };
3751
3752 assert!(matches!(
3754 &state.pages[editor_page_idx].items[left_idx].control,
3755 SettingControl::DualList(_)
3756 ));
3757
3758 let default_right_items: Vec<String> =
3760 match &state.pages[editor_page_idx].items[right_idx].control {
3761 SettingControl::DualList(dl) => dl.included.clone(),
3762 _ => panic!("right should be DualList"),
3763 };
3764 let initial_left_excluded: Vec<String> =
3765 match &state.pages[editor_page_idx].items[left_idx].control {
3766 SettingControl::DualList(dl) => dl.excluded.clone(),
3767 _ => panic!("left should be DualList"),
3768 };
3769 assert_eq!(
3770 initial_left_excluded, default_right_items,
3771 "left.excluded should mirror right's included on initial build"
3772 );
3773
3774 let new_element = "{chord}".to_string();
3776 state.selected_item = left_idx;
3777 state
3778 .with_current_dual_list_mut(|dl: &mut DualListState| {
3779 if !dl.included.contains(&new_element) {
3780 dl.included.push(new_element.clone());
3781 }
3782 })
3783 .expect("current item is a DualList");
3784
3785 state.refresh_dual_list_sibling();
3787
3788 match &state.pages[editor_page_idx].items[right_idx].control {
3789 SettingControl::DualList(dl) => {
3790 assert!(
3791 dl.excluded.contains(&new_element),
3792 "right.excluded should be updated to reflect left's new inclusion"
3793 );
3794 }
3795 _ => panic!("right should be DualList"),
3796 }
3797 }
3798
3799 #[test]
3800 fn test_with_dual_list_mut_returns_none_for_non_dual_list() {
3801 let config = test_config();
3802 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3803
3804 let result = state.with_dual_list_mut(0, |_| ());
3806 assert!(result.is_none());
3807 }
3808}