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
17enum NestedDialogInfo {
19 MapEntry {
20 key: String,
21 value: serde_json::Value,
22 schema: SettingSchema,
23 path: String,
24 is_new: bool,
25 no_delete: bool,
26 },
27 ArrayItem {
28 index: Option<usize>,
29 value: serde_json::Value,
30 schema: SettingSchema,
31 path: String,
32 is_new: bool,
33 },
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum FocusPanel {
39 #[default]
41 Categories,
42 Settings,
44 Footer,
46}
47
48#[derive(Debug)]
50pub struct SettingsState {
51 categories: Vec<SettingCategory>,
53 pub pages: Vec<SettingsPage>,
55 pub selected_category: usize,
57 pub selected_item: usize,
59 pub focus: FocusManager<FocusPanel>,
61 pub footer_button_index: usize,
63 pub pending_changes: HashMap<String, serde_json::Value>,
65 original_config: serde_json::Value,
67 pub visible: bool,
69 pub search_query: String,
71 pub search_active: bool,
73 pub search_results: Vec<SearchResult>,
75 pub selected_search_result: usize,
77 pub search_scroll_offset: usize,
79 pub search_max_visible: usize,
81 pub showing_confirm_dialog: bool,
83 pub confirm_dialog_selection: usize,
85 pub confirm_dialog_hover: Option<usize>,
87 pub showing_reset_dialog: bool,
89 pub reset_dialog_selection: usize,
91 pub reset_dialog_hover: Option<usize>,
93 pub showing_help: bool,
95 pub scroll_panel: ScrollablePanel,
97 pub sub_focus: Option<usize>,
99 pub editing_text: bool,
101 pub hover_position: Option<(u16, u16)>,
103 pub hover_hit: Option<SettingsHit>,
105 pub entry_dialog_stack: Vec<EntryDialogState>,
108 pub target_layer: ConfigLayer,
112 pub layer_sources: HashMap<String, ConfigLayer>,
116 pub pending_deletions: std::collections::HashSet<String>,
120 pub layout_width: u16,
124 pub item_style: super::items::ItemBoxStyle,
127 pub expanded_categories: std::collections::HashSet<usize>,
131 pub categories_scroll: ScrollablePanel,
134 pub tree_cursor_section: Option<usize>,
146}
147
148#[derive(Debug, Clone, Copy)]
155pub enum TreeRow {
156 Category {
157 idx: usize,
158 expandable: bool,
159 expanded: bool,
160 },
161 Section {
162 cat_idx: usize,
163 section_idx: usize,
164 },
165}
166
167impl crate::view::ui::ScrollItem for TreeRow {
168 fn height(&self, _width: u16) -> u16 {
169 1
170 }
171}
172
173impl SettingsState {
174 pub fn new(schema_json: &str, config: &Config) -> Result<Self, serde_json::Error> {
176 let categories = parse_schema(schema_json)?;
177 let config_value = serde_json::to_value(config)?;
178 let layer_sources = HashMap::new(); let target_layer = ConfigLayer::User; let pages =
181 super::items::build_pages(&categories, &config_value, &layer_sources, target_layer);
182
183 Ok(Self {
184 categories,
185 pages,
186 selected_category: 0,
187 selected_item: 0,
188 focus: FocusManager::new(vec![
189 FocusPanel::Categories,
190 FocusPanel::Settings,
191 FocusPanel::Footer,
192 ]),
193 footer_button_index: 2, pending_changes: HashMap::new(),
195 original_config: config_value,
196 visible: false,
197 search_query: String::new(),
198 search_active: false,
199 search_results: Vec::new(),
200 selected_search_result: 0,
201 search_scroll_offset: 0,
202 search_max_visible: 5, showing_confirm_dialog: false,
204 confirm_dialog_selection: 0,
205 confirm_dialog_hover: None,
206 showing_reset_dialog: false,
207 reset_dialog_selection: 0,
208 reset_dialog_hover: None,
209 showing_help: false,
210 scroll_panel: ScrollablePanel::new(),
211 sub_focus: None,
212 editing_text: false,
213 hover_position: None,
214 hover_hit: None,
215 entry_dialog_stack: Vec::new(),
216 target_layer,
217 layer_sources,
218 pending_deletions: std::collections::HashSet::new(),
219 layout_width: 0,
220 item_style: super::items::ItemBoxStyle::default(),
221 expanded_categories: std::collections::HashSet::new(),
222 categories_scroll: ScrollablePanel::new(),
223 tree_cursor_section: None,
224 })
225 }
226
227 #[inline]
229 pub fn focus_panel(&self) -> FocusPanel {
230 self.focus.current().unwrap_or_default()
231 }
232
233 pub fn show(&mut self) {
235 self.visible = true;
236 self.focus.set(FocusPanel::Categories);
237 self.footer_button_index = 2; self.selected_category = 0;
239 self.selected_item = 0;
240 self.scroll_panel = ScrollablePanel::new();
241 self.sub_focus = None;
242 self.showing_confirm_dialog = false;
244 self.confirm_dialog_selection = 0;
245 self.confirm_dialog_hover = None;
246 self.showing_reset_dialog = false;
247 self.reset_dialog_selection = 0;
248 self.reset_dialog_hover = None;
249 self.showing_help = false;
250 }
251
252 pub fn hide(&mut self) {
254 self.visible = false;
255 self.search_active = false;
256 self.search_query.clear();
257 }
258
259 pub fn entry_dialog(&self) -> Option<&EntryDialogState> {
261 self.entry_dialog_stack.last()
262 }
263
264 pub fn entry_dialog_mut(&mut self) -> Option<&mut EntryDialogState> {
266 self.entry_dialog_stack.last_mut()
267 }
268
269 pub fn has_entry_dialog(&self) -> bool {
271 !self.entry_dialog_stack.is_empty()
272 }
273
274 pub fn current_page(&self) -> Option<&SettingsPage> {
276 self.pages.get(self.selected_category)
277 }
278
279 pub fn current_page_mut(&mut self) -> Option<&mut SettingsPage> {
281 self.pages.get_mut(self.selected_category)
282 }
283
284 pub fn topmost_visible_item_index(&self) -> Option<usize> {
289 let page = self.pages.get(self.selected_category)?;
290 if page.items.is_empty() {
291 return None;
292 }
293 let target = self.scroll_panel.scroll.offset;
294 let width = self.layout_width;
295 let mut y: u16 = 0;
296 for (idx, item) in page.items.iter().enumerate() {
297 let h = <SettingItem as ScrollItem>::height(item, width);
298 if y + h > target {
299 return Some(idx);
300 }
301 y += h;
302 }
303 Some(page.items.len() - 1)
304 }
305
306 pub fn current_section_index(&self) -> Option<usize> {
311 let page = self.pages.get(self.selected_category)?;
312 if page.sections.is_empty() {
313 return None;
314 }
315 let item_idx = self
324 .topmost_visible_item_index()
325 .unwrap_or(self.selected_item);
326 let mut current: Option<usize> = None;
328 for (s_idx, section) in page.sections.iter().enumerate() {
329 if section.first_item_index <= item_idx {
330 current = Some(s_idx);
331 } else {
332 break;
333 }
334 }
335 current
336 }
337
338 pub fn is_category_expandable(&self, cat_idx: usize) -> bool {
342 self.pages
343 .get(cat_idx)
344 .is_some_and(|p| p.sections.len() > 1)
345 }
346
347 pub fn tree_step(&mut self, delta: i32) {
357 let rows = self.visible_tree();
358 if rows.is_empty() {
359 return;
360 }
361 let cur = self.tree_cursor_index(&rows);
362 let len = rows.len() as i32;
363 let target = (cur as i32 + delta).clamp(0, len - 1) as usize;
364 if target == cur {
365 return;
366 }
367 let prev_category = self.selected_category;
368 self.update_control_focus(false);
369 match rows[target] {
370 TreeRow::Category { idx, .. } => {
371 self.selected_category = idx;
375 self.selected_item = 0;
376 self.tree_cursor_section = None;
377 if idx != prev_category {
378 self.scroll_panel = ScrollablePanel::new();
379 }
380 self.sub_focus = None;
381 self.update_control_focus(true);
382 }
383 TreeRow::Section {
384 cat_idx,
385 section_idx,
386 } => {
387 let first = self.pages[cat_idx].sections[section_idx].first_item_index;
388 self.selected_category = cat_idx;
389 self.selected_item = first;
390 self.tree_cursor_section = Some(section_idx);
391 if cat_idx != prev_category {
392 self.scroll_panel = ScrollablePanel::new();
393 }
394 self.sub_focus = None;
395 self.init_map_focus(true);
396 self.update_control_focus(true);
397 }
398 }
399 let width = self.layout_width;
405 if let Some(page) = self.pages.get(self.selected_category) {
406 self.scroll_panel.update_content_height(&page.items, width);
407 if matches!(rows[target], TreeRow::Section { .. }) {
415 let item_y =
416 self.scroll_panel
417 .item_y_offset(&page.items, self.selected_item, width);
418 self.scroll_panel.scroll.offset = item_y;
419 } else {
420 let selected_item = self.selected_item;
421 let sub_focus = self.sub_focus;
422 self.scroll_panel.ensure_focused_visible(
423 &page.items,
424 selected_item,
425 sub_focus,
426 width,
427 );
428 }
429 }
430 let new_rows = self.visible_tree();
431 let new_cur = self.tree_cursor_index(&new_rows);
432 self.categories_scroll
433 .ensure_focused_visible(&new_rows, new_cur, None, width);
434 }
435
436 pub(super) fn tree_cursor_index(&self, rows: &[TreeRow]) -> usize {
444 let cat = self.selected_category;
445 if let Some(s_idx) = self.tree_cursor_section {
446 for (i, row) in rows.iter().enumerate() {
447 if let TreeRow::Section {
448 cat_idx,
449 section_idx,
450 } = *row
451 {
452 if cat_idx == cat && section_idx == s_idx {
453 return i;
454 }
455 }
456 }
457 }
458 for (i, row) in rows.iter().enumerate() {
459 if let TreeRow::Category { idx, .. } = *row {
460 if idx == cat {
461 return i;
462 }
463 }
464 }
465 0
466 }
467
468 pub fn auto_expand_current_category(&mut self) {
478 let idx = self.selected_category;
479 if self.is_category_expandable(idx) {
480 self.expanded_categories.insert(idx);
481 }
482 }
483
484 pub fn toggle_category_expanded(&mut self, cat_idx: usize) {
485 if !self.is_category_expandable(cat_idx) {
486 return;
487 }
488 if !self.expanded_categories.insert(cat_idx) {
489 self.expanded_categories.remove(&cat_idx);
490 }
491 }
492
493 pub fn jump_to_section(&mut self, cat_idx: usize, section_idx: usize) {
497 let Some(page) = self.pages.get(cat_idx) else {
498 return;
499 };
500 let Some(section) = page.sections.get(section_idx) else {
501 return;
502 };
503 let target_item = section.first_item_index;
504 self.update_control_focus(false);
505 self.selected_category = cat_idx;
506 self.selected_item = target_item;
507 self.tree_cursor_section = Some(section_idx);
508 self.focus.set(FocusPanel::Settings);
509 let width = self.layout_width;
510 if let Some(page) = self.pages.get(self.selected_category) {
511 self.scroll_panel.update_content_height(&page.items, width);
512 let item_y = self
519 .scroll_panel
520 .item_y_offset(&page.items, target_item, width);
521 self.scroll_panel.scroll.offset = item_y;
522 }
523 self.sub_focus = None;
524 self.init_map_focus(true);
525 self.update_control_focus(true);
526 self.auto_expand_current_category();
527 }
528
529 pub fn visible_tree(&self) -> Vec<TreeRow> {
533 let mut rows = Vec::with_capacity(self.pages.len());
534 for (idx, page) in self.pages.iter().enumerate() {
535 let expandable = page.sections.len() > 1;
536 let expanded = expandable && self.expanded_categories.contains(&idx);
537 rows.push(TreeRow::Category {
538 idx,
539 expandable,
540 expanded,
541 });
542 if expanded {
543 for section_idx in 0..page.sections.len() {
544 rows.push(TreeRow::Section {
545 cat_idx: idx,
546 section_idx,
547 });
548 }
549 }
550 }
551 rows
552 }
553
554 pub fn current_item(&self) -> Option<&SettingItem> {
556 self.current_page()
557 .and_then(|page| page.items.get(self.selected_item))
558 }
559
560 pub fn current_item_mut(&mut self) -> Option<&mut SettingItem> {
562 self.pages
563 .get_mut(self.selected_category)
564 .and_then(|page| page.items.get_mut(self.selected_item))
565 }
566
567 pub fn can_exit_text_editing(&self) -> bool {
569 self.current_item()
570 .map(|item| {
571 if let SettingControl::Text(state) = &item.control {
572 state.is_valid()
573 } else {
574 true
575 }
576 })
577 .unwrap_or(true)
578 }
579
580 pub fn entry_dialog_can_exit_text_editing(&self) -> bool {
582 self.entry_dialog()
583 .and_then(|dialog| dialog.current_item())
584 .map(|item| {
585 if let SettingControl::Text(state) = &item.control {
586 state.is_valid()
587 } else {
588 true
589 }
590 })
591 .unwrap_or(true)
592 }
593
594 fn init_map_focus(&mut self, from_above: bool) {
597 if let Some(item) = self.current_item_mut() {
598 if let SettingControl::Map(ref mut map_state) = item.control {
599 map_state.init_focus(from_above);
600 }
601 }
602 self.update_map_sub_focus();
604 }
605
606 pub(super) fn update_control_focus(&mut self, focused: bool) {
610 let focus_state = if focused {
611 FocusState::Focused
612 } else {
613 FocusState::Normal
614 };
615 if let Some(item) = self.current_item_mut() {
616 match &mut item.control {
617 SettingControl::Map(ref mut state) => state.focus = focus_state,
618 SettingControl::TextList(ref mut state) => state.focus = focus_state,
619 SettingControl::DualList(ref mut state) => state.focus = focus_state,
620 SettingControl::ObjectArray(ref mut state) => state.focus = focus_state,
621 SettingControl::Toggle(ref mut state) => state.focus = focus_state,
622 SettingControl::Number(ref mut state) => state.focus = focus_state,
623 SettingControl::Dropdown(ref mut state) => state.focus = focus_state,
624 SettingControl::Text(ref mut state) => {
625 state.focus = focus_state;
626 if !focused {
630 state.editing = false;
631 }
632 }
633 SettingControl::Json(_) | SettingControl::Complex { .. } => {} }
635 }
636 }
637
638 fn update_map_sub_focus(&mut self) {
641 self.sub_focus = self.current_item().and_then(|item| {
642 if let SettingControl::Map(ref map_state) = item.control {
643 Some(match map_state.focused_entry {
645 Some(i) => 1 + i,
646 None => 1 + map_state.entries.len(), })
648 } else {
649 None
650 }
651 });
652 }
653
654 pub fn select_prev(&mut self) {
656 match self.focus_panel() {
657 FocusPanel::Categories => {
658 self.tree_step(-1);
659 }
660 FocusPanel::Settings => {
661 let handled = self
663 .current_item_mut()
664 .and_then(|item| match &mut item.control {
665 SettingControl::Map(map_state) => Some(map_state.focus_prev()),
666 _ => None,
667 })
668 .unwrap_or(false);
669
670 if handled {
671 self.update_map_sub_focus();
673 } else if self.selected_item > 0 {
674 self.update_control_focus(false); self.selected_item -= 1;
676 self.sub_focus = None;
677 self.init_map_focus(false); self.update_control_focus(true); }
680 self.ensure_visible();
681 }
682 FocusPanel::Footer => {
683 if self.footer_button_index > 0 {
685 self.footer_button_index -= 1;
686 }
687 }
688 }
689 }
690
691 pub fn select_next(&mut self) {
693 match self.focus_panel() {
694 FocusPanel::Categories => {
695 self.tree_step(1);
696 }
697 FocusPanel::Settings => {
698 let handled = self
700 .current_item_mut()
701 .and_then(|item| match &mut item.control {
702 SettingControl::Map(map_state) => Some(map_state.focus_next()),
703 _ => None,
704 })
705 .unwrap_or(false);
706
707 if handled {
708 self.update_map_sub_focus();
710 } else {
711 let can_move = self
712 .current_page()
713 .is_some_and(|page| self.selected_item + 1 < page.items.len());
714 if can_move {
715 self.update_control_focus(false); self.selected_item += 1;
717 self.sub_focus = None;
718 self.init_map_focus(true); self.update_control_focus(true); }
721 }
722 self.ensure_visible();
723 }
724 FocusPanel::Footer => {
725 if self.footer_button_index < 2 {
727 self.footer_button_index += 1;
728 }
729 }
730 }
731 }
732
733 pub fn select_next_page(&mut self) {
735 let page_size = self.scroll_panel.viewport_height().max(1);
736 for _ in 0..page_size {
737 self.select_next();
738 }
739 }
740
741 pub fn select_prev_page(&mut self) {
743 let page_size = self.scroll_panel.viewport_height().max(1);
744 for _ in 0..page_size {
745 self.select_prev();
746 }
747 }
748
749 pub fn toggle_focus(&mut self) {
751 let old_panel = self.focus_panel();
752 self.focus.focus_next();
753 self.on_panel_changed(old_panel, true);
754 }
755
756 pub fn toggle_focus_backward(&mut self) {
758 let old_panel = self.focus_panel();
759 self.focus.focus_prev();
760 self.on_panel_changed(old_panel, false);
761 }
762
763 fn on_panel_changed(&mut self, old_panel: FocusPanel, forward: bool) {
765 if old_panel == FocusPanel::Settings {
767 self.update_control_focus(false);
768 }
769
770 if self.focus_panel() == FocusPanel::Settings
772 && self.selected_item >= self.current_page().map_or(0, |p| p.items.len())
773 {
774 self.selected_item = 0;
775 }
776 self.sub_focus = None;
777
778 if self.focus_panel() == FocusPanel::Settings {
779 self.init_map_focus(forward); self.update_control_focus(true); }
782
783 if self.focus_panel() == FocusPanel::Footer {
785 self.footer_button_index = if forward {
786 0 } else {
788 4 };
790 }
791
792 self.ensure_visible();
793 }
794
795 pub fn set_item_style(&mut self, style: super::items::ItemBoxStyle) {
803 if self.item_style == style {
804 return;
805 }
806 self.item_style = style;
807 for page in &mut self.pages {
808 for item in &mut page.items {
809 item.style = style;
810 }
811 }
812 let width = self.layout_width;
813 if let Some(page) = self.pages.get(self.selected_category) {
814 self.scroll_panel.update_content_height(&page.items, width);
815 }
816 }
817
818 pub fn ensure_visible(&mut self) {
820 if self.focus_panel() != FocusPanel::Settings {
821 return;
822 }
823
824 let selected_item = self.selected_item;
826 let sub_focus = self.sub_focus;
827 let width = self.layout_width;
828 let prev_offset = self.scroll_panel.scroll.offset;
829 if let Some(page) = self.pages.get(self.selected_category) {
830 self.scroll_panel
831 .ensure_focused_visible(&page.items, selected_item, sub_focus, width);
832 }
833 if self.scroll_panel.scroll.offset != prev_offset {
837 self.sync_tree_cursor_to_body_scroll();
838 }
839 }
840
841 pub fn set_pending_change(&mut self, path: &str, value: serde_json::Value) {
843 let original = self.original_config.pointer(path);
845 if original == Some(&value) {
846 self.pending_changes.remove(path);
847 } else {
848 self.pending_changes.insert(path.to_string(), value);
849 }
850 }
851
852 pub fn has_changes(&self) -> bool {
854 !self.pending_changes.is_empty() || !self.pending_deletions.is_empty()
855 }
856
857 pub fn apply_changes(&self, config: &Config) -> Result<Config, serde_json::Error> {
859 let mut config_value = serde_json::to_value(config)?;
860
861 for (path, value) in &self.pending_changes {
862 if let Some(target) = config_value.pointer_mut(path) {
863 *target = value.clone();
864 }
865 }
866
867 serde_json::from_value(config_value)
868 }
869
870 pub fn discard_changes(&mut self) {
872 self.pending_changes.clear();
873 self.pending_deletions.clear();
874 self.pages = super::items::build_pages(
876 &self.categories,
877 &self.original_config,
878 &self.layer_sources,
879 self.target_layer,
880 );
881 }
882
883 pub fn set_target_layer(&mut self, layer: ConfigLayer) {
885 if layer != ConfigLayer::System {
886 self.target_layer = layer;
888 self.pending_changes.clear();
890 self.pending_deletions.clear();
891 self.pages = super::items::build_pages(
893 &self.categories,
894 &self.original_config,
895 &self.layer_sources,
896 self.target_layer,
897 );
898 }
899 }
900
901 pub fn cycle_target_layer(&mut self) {
903 self.target_layer = match self.target_layer {
904 ConfigLayer::System => ConfigLayer::User, ConfigLayer::User => ConfigLayer::Project,
906 ConfigLayer::Project => ConfigLayer::Session,
907 ConfigLayer::Session => ConfigLayer::User,
908 };
909 self.pending_changes.clear();
911 self.pending_deletions.clear();
912 self.pages = super::items::build_pages(
914 &self.categories,
915 &self.original_config,
916 &self.layer_sources,
917 self.target_layer,
918 );
919 }
920
921 pub fn target_layer_name(&self) -> &'static str {
923 match self.target_layer {
924 ConfigLayer::System => "System (read-only)",
925 ConfigLayer::User => "User",
926 ConfigLayer::Project => "Project",
927 ConfigLayer::Session => "Session",
928 }
929 }
930
931 pub fn set_layer_sources(&mut self, sources: HashMap<String, ConfigLayer>) {
934 self.layer_sources = sources;
935 self.pages = super::items::build_pages(
937 &self.categories,
938 &self.original_config,
939 &self.layer_sources,
940 self.target_layer,
941 );
942 }
943
944 pub fn get_layer_source(&self, path: &str) -> ConfigLayer {
947 self.layer_sources
948 .get(path)
949 .copied()
950 .unwrap_or(ConfigLayer::System)
951 }
952
953 pub fn layer_source_label(layer: ConfigLayer) -> &'static str {
955 match layer {
956 ConfigLayer::System => "default",
957 ConfigLayer::User => "user",
958 ConfigLayer::Project => "project",
959 ConfigLayer::Session => "session",
960 }
961 }
962
963 pub fn reset_current_to_default(&mut self) {
971 let reset_info = self.current_item().and_then(|item| {
973 if !item.modified || item.is_auto_managed {
976 return None;
977 }
978 item.default
979 .as_ref()
980 .map(|default| (item.path.clone(), default.clone()))
981 });
982
983 if let Some((path, default)) = reset_info {
984 self.pending_deletions.insert(path.clone());
986 self.pending_changes.remove(&path);
988
989 if let Some(item) = self.current_item_mut() {
993 update_control_from_value(&mut item.control, &default);
994 item.modified = false;
995 item.layer_source = ConfigLayer::System; }
998 }
999 }
1000
1001 pub fn set_current_to_null(&mut self) {
1007 let target_layer = self.target_layer;
1008 let change_info = self.current_item().and_then(|item| {
1009 if !item.nullable || item.is_null || item.read_only {
1010 return None;
1011 }
1012 Some(item.path.clone())
1013 });
1014
1015 if let Some(path) = change_info {
1016 self.pending_changes
1018 .insert(path.clone(), serde_json::Value::Null);
1019 self.pending_deletions.remove(&path);
1020
1021 if let Some(item) = self.current_item_mut() {
1023 item.is_null = true;
1024 item.modified = true;
1025 item.layer_source = target_layer;
1026 }
1027 }
1028 }
1029
1030 pub fn clear_current_category(&mut self) {
1036 let target_layer = self.target_layer;
1037 let page = match self.current_page() {
1038 Some(p) if p.nullable => p,
1039 _ => return,
1040 };
1041 let page_path = page.path.clone();
1042
1043 self.pending_changes
1045 .insert(page_path.clone(), serde_json::Value::Null);
1046
1047 let prefix = format!("{}/", page_path);
1049 self.pending_changes
1050 .retain(|path, _| !path.starts_with(&prefix));
1051 self.pending_deletions
1052 .retain(|path| !path.starts_with(&prefix));
1053
1054 if let Some(page) = self.current_page_mut() {
1056 for item in &mut page.items {
1057 if item.nullable {
1058 item.is_null = true;
1059 item.modified = false;
1060 item.layer_source = target_layer;
1061 }
1062 }
1063 }
1064 }
1065
1066 pub fn current_category_has_values(&self) -> bool {
1068 match self.current_page() {
1069 Some(page) if page.nullable => {
1070 page.items.iter().any(|item| !item.is_null && item.nullable)
1071 || page.items.iter().any(|item| item.modified)
1072 }
1073 _ => false,
1074 }
1075 }
1076
1077 pub fn on_value_changed(&mut self) {
1079 let target_layer = self.target_layer;
1081
1082 let change_info = self.current_item().map(|item| {
1084 let value = control_to_value(&item.control);
1085 (item.path.clone(), value)
1086 });
1087
1088 if let Some((path, value)) = change_info {
1089 self.pending_deletions.remove(&path);
1092
1093 if let Some(item) = self.current_item_mut() {
1095 item.modified = true; item.layer_source = target_layer; item.is_null = false; }
1099 self.set_pending_change(&path, value);
1100 }
1101 }
1102
1103 pub fn update_focus_states(&mut self) {
1105 let current_focus = self.focus_panel();
1106 for (page_idx, page) in self.pages.iter_mut().enumerate() {
1107 for (item_idx, item) in page.items.iter_mut().enumerate() {
1108 let is_focused = current_focus == FocusPanel::Settings
1109 && page_idx == self.selected_category
1110 && item_idx == self.selected_item;
1111
1112 let focus = if is_focused {
1113 FocusState::Focused
1114 } else {
1115 FocusState::Normal
1116 };
1117
1118 match &mut item.control {
1119 SettingControl::Toggle(state) => state.focus = focus,
1120 SettingControl::Number(state) => state.focus = focus,
1121 SettingControl::Dropdown(state) => state.focus = focus,
1122 SettingControl::Text(state) => state.focus = focus,
1123 SettingControl::TextList(state) => state.focus = focus,
1124 SettingControl::DualList(state) => state.focus = focus,
1125 SettingControl::Map(state) => state.focus = focus,
1126 SettingControl::ObjectArray(state) => state.focus = focus,
1127 SettingControl::Json(state) => state.focus = focus,
1128 SettingControl::Complex { .. } => {}
1129 }
1130 }
1131 }
1132 }
1133
1134 pub fn start_search(&mut self) {
1136 self.search_active = true;
1137 self.search_query.clear();
1138 self.search_results.clear();
1139 self.selected_search_result = 0;
1140 self.search_scroll_offset = 0;
1141 }
1142
1143 pub fn cancel_search(&mut self) {
1145 self.search_active = false;
1146 self.search_query.clear();
1147 self.search_results.clear();
1148 self.selected_search_result = 0;
1149 self.search_scroll_offset = 0;
1150 }
1151
1152 pub fn set_search_query(&mut self, query: String) {
1154 self.search_query = query;
1155 self.search_results = search_settings(&self.pages, &self.search_query);
1156 self.selected_search_result = 0;
1157 self.search_scroll_offset = 0;
1158 }
1159
1160 pub fn search_push_char(&mut self, c: char) {
1162 self.search_query.push(c);
1163 self.search_results = search_settings(&self.pages, &self.search_query);
1164 self.selected_search_result = 0;
1165 self.search_scroll_offset = 0;
1166 }
1167
1168 pub fn search_pop_char(&mut self) {
1170 self.search_query.pop();
1171 self.search_results = search_settings(&self.pages, &self.search_query);
1172 self.selected_search_result = 0;
1173 self.search_scroll_offset = 0;
1174 }
1175
1176 pub fn search_prev(&mut self) {
1178 if !self.search_results.is_empty() && self.selected_search_result > 0 {
1179 self.selected_search_result -= 1;
1180 if self.selected_search_result < self.search_scroll_offset {
1182 self.search_scroll_offset = self.selected_search_result;
1183 }
1184 }
1185 }
1186
1187 pub fn search_next(&mut self) {
1189 if !self.search_results.is_empty()
1190 && self.selected_search_result + 1 < self.search_results.len()
1191 {
1192 self.selected_search_result += 1;
1193 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1195 self.search_scroll_offset =
1196 self.selected_search_result - self.search_max_visible + 1;
1197 }
1198 }
1199 }
1200
1201 pub fn search_scroll_up(&mut self, delta: usize) -> bool {
1203 if self.search_results.is_empty() || self.search_scroll_offset == 0 {
1204 return false;
1205 }
1206 self.search_scroll_offset = self.search_scroll_offset.saturating_sub(delta);
1207 if self.selected_search_result >= self.search_scroll_offset + self.search_max_visible {
1209 self.selected_search_result = self.search_scroll_offset + self.search_max_visible - 1;
1210 }
1211 true
1212 }
1213
1214 pub fn search_scroll_down(&mut self, delta: usize) -> bool {
1216 if self.search_results.is_empty() {
1217 return false;
1218 }
1219 let max_offset = self
1220 .search_results
1221 .len()
1222 .saturating_sub(self.search_max_visible);
1223 if self.search_scroll_offset >= max_offset {
1224 return false;
1225 }
1226 self.search_scroll_offset = (self.search_scroll_offset + delta).min(max_offset);
1227 if self.selected_search_result < self.search_scroll_offset {
1229 self.selected_search_result = self.search_scroll_offset;
1230 }
1231 true
1232 }
1233
1234 pub fn search_scroll_to_ratio(&mut self, ratio: f32) -> bool {
1236 if self.search_results.is_empty() {
1237 return false;
1238 }
1239 let max_offset = self
1240 .search_results
1241 .len()
1242 .saturating_sub(self.search_max_visible);
1243 let new_offset = (ratio * max_offset as f32) as usize;
1244 if new_offset != self.search_scroll_offset {
1245 self.search_scroll_offset = new_offset.min(max_offset);
1246 if self.selected_search_result < self.search_scroll_offset {
1248 self.selected_search_result = self.search_scroll_offset;
1249 } else if self.selected_search_result
1250 >= self.search_scroll_offset + self.search_max_visible
1251 {
1252 self.selected_search_result =
1253 self.search_scroll_offset + self.search_max_visible - 1;
1254 }
1255 return true;
1256 }
1257 false
1258 }
1259
1260 pub fn jump_to_search_result(&mut self) {
1262 let Some(result) = self
1264 .search_results
1265 .get(self.selected_search_result)
1266 .cloned()
1267 else {
1268 return;
1269 };
1270 let page_index = result.page_index;
1271 let item_index = result.item_index;
1272
1273 self.update_control_focus(false);
1275 self.selected_category = page_index;
1276 self.selected_item = item_index;
1277 self.focus.set(FocusPanel::Settings);
1278 self.scroll_panel.scroll.offset = 0;
1280 let width = self.layout_width;
1282 if let Some(page) = self.pages.get(self.selected_category) {
1283 self.scroll_panel.update_content_height(&page.items, width);
1284 }
1285 self.sub_focus = None;
1286 self.init_map_focus(true);
1287
1288 if let Some(ref deep_match) = result.deep_match {
1290 self.jump_to_deep_match(deep_match);
1291 }
1292
1293 self.update_control_focus(true); self.auto_expand_current_category();
1295 self.tree_cursor_section = self.current_section_index();
1299 self.ensure_visible();
1300 self.cancel_search();
1301 }
1302
1303 fn jump_to_deep_match(&mut self, deep_match: &DeepMatch) {
1305 match deep_match {
1306 DeepMatch::MapKey { entry_index, .. } | DeepMatch::MapValue { entry_index, .. } => {
1307 if let Some(item) = self.current_item_mut() {
1308 if let SettingControl::Map(ref mut map_state) = item.control {
1309 map_state.focused_entry = Some(*entry_index);
1310 }
1311 }
1312 self.update_map_sub_focus();
1313 }
1314 DeepMatch::TextListItem { item_index, .. } => {
1315 if let Some(item) = self.current_item_mut() {
1316 if let SettingControl::TextList(ref mut list_state) = item.control {
1317 list_state.focused_item = Some(*item_index);
1318 }
1319 }
1320 self.sub_focus = Some(1 + *item_index);
1322 }
1323 }
1324 }
1325
1326 pub fn current_search_result(&self) -> Option<&SearchResult> {
1328 self.search_results.get(self.selected_search_result)
1329 }
1330
1331 pub fn show_confirm_dialog(&mut self) {
1333 self.showing_confirm_dialog = true;
1334 self.confirm_dialog_selection = 0; }
1336
1337 pub fn hide_confirm_dialog(&mut self) {
1339 self.showing_confirm_dialog = false;
1340 self.confirm_dialog_selection = 0;
1341 }
1342
1343 pub fn confirm_dialog_next(&mut self) {
1345 self.confirm_dialog_selection = (self.confirm_dialog_selection + 1) % 3;
1346 }
1347
1348 pub fn confirm_dialog_prev(&mut self) {
1350 self.confirm_dialog_selection = if self.confirm_dialog_selection == 0 {
1351 2
1352 } else {
1353 self.confirm_dialog_selection - 1
1354 };
1355 }
1356
1357 pub fn toggle_help(&mut self) {
1359 self.showing_help = !self.showing_help;
1360 }
1361
1362 pub fn hide_help(&mut self) {
1364 self.showing_help = false;
1365 }
1366
1367 pub fn showing_entry_dialog(&self) -> bool {
1369 self.has_entry_dialog()
1370 }
1371
1372 pub fn open_entry_dialog(&mut self) {
1374 let Some(item) = self.current_item() else {
1375 return;
1376 };
1377
1378 let path = item.path.as_str();
1380 let SettingControl::Map(map_state) = &item.control else {
1381 return;
1382 };
1383
1384 let Some(entry_idx) = map_state.focused_entry else {
1386 return;
1387 };
1388 let Some((key, value)) = map_state.entries.get(entry_idx) else {
1389 return;
1390 };
1391
1392 let Some(schema) = map_state.value_schema.as_ref() else {
1394 return; };
1396
1397 let no_delete = map_state.no_add;
1399
1400 let dialog =
1402 EntryDialogState::from_schema(key.clone(), value, schema, path, false, no_delete);
1403 self.entry_dialog_stack.push(dialog);
1404 }
1405
1406 pub fn open_add_entry_dialog(&mut self) {
1408 let Some(item) = self.current_item() else {
1409 return;
1410 };
1411 let SettingControl::Map(map_state) = &item.control else {
1412 return;
1413 };
1414 let Some(schema) = map_state.value_schema.as_ref() else {
1415 return;
1416 };
1417 let path = item.path.clone();
1418
1419 let dialog = EntryDialogState::from_schema(
1422 String::new(),
1423 &serde_json::json!({}),
1424 schema,
1425 &path,
1426 true,
1427 false,
1428 );
1429 self.entry_dialog_stack.push(dialog);
1430 }
1431
1432 pub fn open_add_array_item_dialog(&mut self) {
1434 let Some(item) = self.current_item() else {
1435 return;
1436 };
1437 let SettingControl::ObjectArray(array_state) = &item.control else {
1438 return;
1439 };
1440 let Some(schema) = array_state.item_schema.as_ref() else {
1441 return;
1442 };
1443 let path = item.path.clone();
1444
1445 let dialog =
1447 EntryDialogState::for_array_item(None, &serde_json::json!({}), schema, &path, true);
1448 self.entry_dialog_stack.push(dialog);
1449 }
1450
1451 pub fn open_edit_array_item_dialog(&mut self) {
1453 let Some(item) = self.current_item() else {
1454 return;
1455 };
1456 let SettingControl::ObjectArray(array_state) = &item.control else {
1457 return;
1458 };
1459 let Some(schema) = array_state.item_schema.as_ref() else {
1460 return;
1461 };
1462 let Some(index) = array_state.focused_index else {
1463 return;
1464 };
1465 let Some(value) = array_state.bindings.get(index) else {
1466 return;
1467 };
1468 let path = item.path.clone();
1469
1470 let dialog = EntryDialogState::for_array_item(Some(index), value, schema, &path, false);
1471 self.entry_dialog_stack.push(dialog);
1472 }
1473
1474 pub fn close_entry_dialog(&mut self) {
1476 self.entry_dialog_stack.pop();
1477 }
1478
1479 pub fn open_nested_entry_dialog(&mut self) {
1484 let nested_info = self.entry_dialog().and_then(|dialog| {
1486 let item = dialog.current_item()?;
1487 let base = dialog.entry_path();
1493 let relative = item.path.trim_start_matches('/');
1494 let path = if relative.is_empty() {
1495 base
1499 } else {
1500 format!("{}/{}", base, relative)
1501 };
1502
1503 match &item.control {
1504 SettingControl::Map(map_state) => {
1505 let schema = map_state.value_schema.as_ref()?;
1506 let no_delete = map_state.no_add; if let Some(entry_idx) = map_state.focused_entry {
1508 let (key, value) = map_state.entries.get(entry_idx)?;
1510 Some(NestedDialogInfo::MapEntry {
1511 key: key.clone(),
1512 value: value.clone(),
1513 schema: schema.as_ref().clone(),
1514 path,
1515 is_new: false,
1516 no_delete,
1517 })
1518 } else {
1519 Some(NestedDialogInfo::MapEntry {
1521 key: String::new(),
1522 value: serde_json::json!({}),
1523 schema: schema.as_ref().clone(),
1524 path,
1525 is_new: true,
1526 no_delete: false, })
1528 }
1529 }
1530 SettingControl::ObjectArray(array_state) => {
1531 let schema = array_state.item_schema.as_ref()?;
1532 if let Some(index) = array_state.focused_index {
1533 let value = array_state.bindings.get(index)?;
1535 Some(NestedDialogInfo::ArrayItem {
1536 index: Some(index),
1537 value: value.clone(),
1538 schema: schema.as_ref().clone(),
1539 path,
1540 is_new: false,
1541 })
1542 } else {
1543 Some(NestedDialogInfo::ArrayItem {
1545 index: None,
1546 value: serde_json::json!({}),
1547 schema: schema.as_ref().clone(),
1548 path,
1549 is_new: true,
1550 })
1551 }
1552 }
1553 _ => None,
1554 }
1555 });
1556
1557 if let Some(info) = nested_info {
1559 let dialog = match info {
1560 NestedDialogInfo::MapEntry {
1561 key,
1562 value,
1563 schema,
1564 path,
1565 is_new,
1566 no_delete,
1567 } => EntryDialogState::from_schema(key, &value, &schema, &path, is_new, no_delete),
1568 NestedDialogInfo::ArrayItem {
1569 index,
1570 value,
1571 schema,
1572 path,
1573 is_new,
1574 } => EntryDialogState::for_array_item(index, &value, &schema, &path, is_new),
1575 };
1576 self.entry_dialog_stack.push(dialog);
1577 }
1578 }
1579
1580 pub fn save_entry_dialog(&mut self) {
1585 let is_array = if self.entry_dialog_stack.len() > 1 {
1589 self.entry_dialog_stack
1591 .get(self.entry_dialog_stack.len() - 2)
1592 .and_then(|parent| parent.current_item())
1593 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1594 .unwrap_or(false)
1595 } else {
1596 self.current_item()
1598 .map(|item| matches!(item.control, SettingControl::ObjectArray(_)))
1599 .unwrap_or(false)
1600 };
1601
1602 if is_array {
1603 self.save_array_item_dialog_inner();
1604 } else {
1605 self.save_map_entry_dialog_inner();
1606 }
1607 }
1608
1609 fn save_map_entry_dialog_inner(&mut self) {
1611 let Some(dialog) = self.entry_dialog_stack.pop() else {
1612 return;
1613 };
1614
1615 let key = dialog.get_key();
1617 if key.is_empty() {
1618 return; }
1620
1621 let value = dialog.to_value();
1622 let map_path = dialog.map_path.clone();
1623 let original_key = dialog.entry_key.clone();
1624 let is_new = dialog.is_new;
1625 let key_changed = !is_new && key != original_key;
1626
1627 if let Some(item) = self.current_item_mut() {
1629 if let SettingControl::Map(map_state) = &mut item.control {
1630 if key_changed {
1632 if let Some(idx) = map_state
1633 .entries
1634 .iter()
1635 .position(|(k, _)| k == &original_key)
1636 {
1637 map_state.entries.remove(idx);
1638 }
1639 }
1640
1641 if let Some(entry) = map_state.entries.iter_mut().find(|(k, _)| k == &key) {
1643 entry.1 = value.clone();
1644 } else {
1645 map_state.entries.push((key.clone(), value.clone()));
1646 map_state.entries.sort_by(|a, b| a.0.cmp(&b.0));
1647 }
1648 }
1649 }
1650
1651 if key_changed {
1653 let old_path = format!("{}/{}", map_path, original_key);
1654 self.pending_changes
1655 .insert(old_path, serde_json::Value::Null);
1656 }
1657
1658 let path = format!("{}/{}", map_path, key);
1660 self.set_pending_change(&path, value);
1661 }
1662
1663 fn save_array_item_dialog_inner(&mut self) {
1665 let Some(dialog) = self.entry_dialog_stack.pop() else {
1666 return;
1667 };
1668
1669 let value = dialog.to_value();
1670 let array_path = dialog.map_path.clone();
1671 let is_new = dialog.is_new;
1672 let entry_key = dialog.entry_key.clone();
1673
1674 let is_nested = !self.entry_dialog_stack.is_empty();
1676
1677 if is_nested {
1678 let parent_entry_path = self
1686 .entry_dialog_stack
1687 .last()
1688 .map(|p| p.entry_path())
1689 .unwrap_or_default();
1690 let item_path = array_path
1691 .strip_prefix(parent_entry_path.as_str())
1692 .unwrap_or(&array_path)
1693 .trim_end_matches('/')
1694 .to_string();
1695
1696 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1698 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1699 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1700 if is_new {
1701 array_state.bindings.push(value.clone());
1702 } else if let Ok(index) = entry_key.parse::<usize>() {
1703 if index < array_state.bindings.len() {
1704 array_state.bindings[index] = value.clone();
1705 }
1706 }
1707 }
1708 }
1709 }
1710
1711 if let Some(parent) = self.entry_dialog_stack.last() {
1714 if let Some(item) = parent.items.iter().find(|i| i.path == item_path) {
1715 if let SettingControl::ObjectArray(array_state) = &item.control {
1716 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1717 self.set_pending_change(&array_path, array_value);
1718 }
1719 }
1720 }
1721 } else {
1722 if let Some(item) = self.current_item_mut() {
1724 if let SettingControl::ObjectArray(array_state) = &mut item.control {
1725 if is_new {
1726 array_state.bindings.push(value.clone());
1727 } else if let Ok(index) = entry_key.parse::<usize>() {
1728 if index < array_state.bindings.len() {
1729 array_state.bindings[index] = value.clone();
1730 }
1731 }
1732 }
1733 }
1734
1735 if let Some(item) = self.current_item() {
1737 if let SettingControl::ObjectArray(array_state) = &item.control {
1738 let array_value = serde_json::Value::Array(array_state.bindings.clone());
1739 self.set_pending_change(&array_path, array_value);
1740 }
1741 }
1742 }
1743 }
1744
1745 pub fn delete_entry_dialog(&mut self) {
1747 let is_nested = self.entry_dialog_stack.len() > 1;
1749
1750 let Some(dialog) = self.entry_dialog_stack.pop() else {
1751 return;
1752 };
1753
1754 let path = format!("{}/{}", dialog.map_path, dialog.entry_key);
1755
1756 if is_nested {
1758 let map_field = dialog.map_path.rsplit('/').next().unwrap_or("").to_string();
1761 let item_path = format!("/{}", map_field);
1762
1763 if let Some(parent) = self.entry_dialog_stack.last_mut() {
1765 if let Some(item) = parent.items.iter_mut().find(|i| i.path == item_path) {
1766 if let SettingControl::Map(map_state) = &mut item.control {
1767 if let Some(idx) = map_state
1768 .entries
1769 .iter()
1770 .position(|(k, _)| k == &dialog.entry_key)
1771 {
1772 map_state.remove_entry(idx);
1773 }
1774 }
1775 }
1776 }
1777 } else {
1778 if let Some(item) = self.current_item_mut() {
1780 if let SettingControl::Map(map_state) = &mut item.control {
1781 if let Some(idx) = map_state
1782 .entries
1783 .iter()
1784 .position(|(k, _)| k == &dialog.entry_key)
1785 {
1786 map_state.remove_entry(idx);
1787 }
1788 }
1789 }
1790 }
1791
1792 self.set_pending_change(&path, serde_json::Value::Null);
1794 }
1795
1796 pub fn max_scroll(&self) -> u16 {
1798 self.scroll_panel.scroll.max_offset()
1799 }
1800
1801 pub fn scroll_up(&mut self, delta: usize) -> bool {
1804 let old = self.scroll_panel.scroll.offset;
1805 self.scroll_panel.scroll_up(delta as u16);
1806 let changed = old != self.scroll_panel.scroll.offset;
1807 if changed {
1808 self.sync_tree_cursor_to_body_scroll();
1809 }
1810 changed
1811 }
1812
1813 pub fn scroll_down(&mut self, delta: usize) -> bool {
1816 let old = self.scroll_panel.scroll.offset;
1817 self.scroll_panel.scroll_down(delta as u16);
1818 let changed = old != self.scroll_panel.scroll.offset;
1819 if changed {
1820 self.sync_tree_cursor_to_body_scroll();
1821 }
1822 changed
1823 }
1824
1825 pub fn scroll_to_ratio(&mut self, ratio: f32) -> bool {
1828 let old = self.scroll_panel.scroll.offset;
1829 self.scroll_panel.scroll_to_ratio(ratio);
1830 let changed = old != self.scroll_panel.scroll.offset;
1831 if changed {
1832 self.sync_tree_cursor_to_body_scroll();
1833 }
1834 changed
1835 }
1836
1837 pub(super) fn sync_tree_cursor_to_body_scroll(&mut self) {
1843 if let Some(section_idx) = self.current_section_index() {
1844 self.tree_cursor_section = Some(section_idx);
1845 }
1846 }
1851
1852 pub fn is_number_control(&self) -> bool {
1855 self.current_item()
1856 .is_some_and(|item| matches!(item.control, SettingControl::Number(_)))
1857 }
1858
1859 pub fn start_editing(&mut self) {
1860 if let Some(item) = self.current_item() {
1861 if matches!(
1862 item.control,
1863 SettingControl::TextList(_)
1864 | SettingControl::DualList(_)
1865 | SettingControl::Text(_)
1866 | SettingControl::Map(_)
1867 | SettingControl::Json(_)
1868 ) {
1869 self.editing_text = true;
1870 }
1871 }
1872 if let Some(item) = self.current_item_mut() {
1873 match item.control {
1874 SettingControl::DualList(ref mut dl) => {
1875 dl.editing = true;
1876 }
1877 SettingControl::Text(ref mut state) => {
1878 state.editing = true;
1879 state.arm_replace_on_type();
1884 }
1885 _ => {}
1886 }
1887 }
1888 }
1889
1890 pub fn stop_editing(&mut self) {
1892 self.editing_text = false;
1893 if let Some(item) = self.current_item_mut() {
1894 match item.control {
1895 SettingControl::DualList(ref mut dl) => {
1896 dl.editing = false;
1897 }
1898 SettingControl::Text(ref mut state) => {
1899 state.editing = false;
1900 }
1901 _ => {}
1902 }
1903 }
1904 }
1905
1906 pub fn is_editable_control(&self) -> bool {
1908 self.current_item().is_some_and(|item| {
1909 matches!(
1910 item.control,
1911 SettingControl::TextList(_)
1912 | SettingControl::DualList(_)
1913 | SettingControl::Text(_)
1914 | SettingControl::Map(_)
1915 | SettingControl::Json(_)
1916 )
1917 })
1918 }
1919
1920 pub fn is_editing_json(&self) -> bool {
1922 if !self.editing_text {
1923 return false;
1924 }
1925 self.current_item()
1926 .map(|item| matches!(&item.control, SettingControl::Json(_)))
1927 .unwrap_or(false)
1928 }
1929
1930 pub fn text_insert(&mut self, c: char) {
1932 if let Some(item) = self.current_item_mut() {
1933 match &mut item.control {
1934 SettingControl::TextList(state) => state.insert(c),
1935 SettingControl::Text(state) => state.insert(c),
1936 SettingControl::Map(state) => {
1937 state.new_key_text.insert(state.cursor, c);
1938 state.cursor += c.len_utf8();
1939 }
1940 SettingControl::Json(state) => state.insert(c),
1941 _ => {}
1942 }
1943 }
1944 }
1945
1946 pub fn text_backspace(&mut self) {
1948 if let Some(item) = self.current_item_mut() {
1949 match &mut item.control {
1950 SettingControl::TextList(state) => state.backspace(),
1951 SettingControl::Text(state) => state.backspace(),
1952 SettingControl::Map(state) => {
1953 if state.cursor > 0 {
1954 let mut char_start = state.cursor - 1;
1955 while char_start > 0 && !state.new_key_text.is_char_boundary(char_start) {
1956 char_start -= 1;
1957 }
1958 state.new_key_text.remove(char_start);
1959 state.cursor = char_start;
1960 }
1961 }
1962 SettingControl::Json(state) => state.backspace(),
1963 _ => {}
1964 }
1965 }
1966 }
1967
1968 pub fn text_move_left(&mut self) {
1970 if let Some(item) = self.current_item_mut() {
1971 match &mut item.control {
1972 SettingControl::TextList(state) => state.move_left(),
1973 SettingControl::Text(state) => state.move_left(),
1974 SettingControl::Map(state) => {
1975 if state.cursor > 0 {
1976 let mut new_pos = state.cursor - 1;
1977 while new_pos > 0 && !state.new_key_text.is_char_boundary(new_pos) {
1978 new_pos -= 1;
1979 }
1980 state.cursor = new_pos;
1981 }
1982 }
1983 SettingControl::Json(state) => state.move_left(),
1984 _ => {}
1985 }
1986 }
1987 }
1988
1989 pub fn text_move_right(&mut self) {
1991 if let Some(item) = self.current_item_mut() {
1992 match &mut item.control {
1993 SettingControl::TextList(state) => state.move_right(),
1994 SettingControl::Text(state) => state.move_right(),
1995 SettingControl::Map(state) => {
1996 if state.cursor < state.new_key_text.len() {
1997 let mut new_pos = state.cursor + 1;
1998 while new_pos < state.new_key_text.len()
1999 && !state.new_key_text.is_char_boundary(new_pos)
2000 {
2001 new_pos += 1;
2002 }
2003 state.cursor = new_pos;
2004 }
2005 }
2006 SettingControl::Json(state) => state.move_right(),
2007 _ => {}
2008 }
2009 }
2010 }
2011
2012 pub fn text_focus_prev(&mut self) {
2014 if let Some(item) = self.current_item_mut() {
2015 match &mut item.control {
2016 SettingControl::TextList(state) => state.focus_prev(),
2017 SettingControl::Map(state) => {
2018 state.focus_prev();
2019 }
2020 _ => {}
2021 }
2022 }
2023 }
2024
2025 pub fn text_focus_next(&mut self) {
2027 if let Some(item) = self.current_item_mut() {
2028 match &mut item.control {
2029 SettingControl::TextList(state) => state.focus_next(),
2030 SettingControl::Map(state) => {
2031 state.focus_next();
2032 }
2033 _ => {}
2034 }
2035 }
2036 }
2037
2038 pub fn text_add_item(&mut self) {
2040 if let Some(item) = self.current_item_mut() {
2041 match &mut item.control {
2042 SettingControl::TextList(state) => state.add_item(),
2043 SettingControl::Map(state) => state.add_entry_from_input(),
2044 _ => {}
2045 }
2046 }
2047 self.on_value_changed();
2049 }
2050
2051 pub fn text_remove_focused(&mut self) {
2053 if let Some(item) = self.current_item_mut() {
2054 match &mut item.control {
2055 SettingControl::TextList(state) => {
2056 if let Some(idx) = state.focused_item {
2057 state.remove_item(idx);
2058 }
2059 }
2060 SettingControl::Map(state) => {
2061 if let Some(idx) = state.focused_entry {
2062 state.remove_entry(idx);
2063 }
2064 }
2065 _ => {}
2066 }
2067 }
2068 self.on_value_changed();
2070 }
2071
2072 pub fn is_editing_dual_list(&self) -> bool {
2074 if !self.editing_text {
2075 return false;
2076 }
2077 self.current_item()
2078 .map(|item| matches!(&item.control, SettingControl::DualList(_)))
2079 .unwrap_or(false)
2080 }
2081
2082 pub fn with_dual_list_mut<R>(
2087 &mut self,
2088 item_idx: usize,
2089 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2090 ) -> Option<R> {
2091 let page = self.pages.get_mut(self.selected_category)?;
2092 let item = page.items.get_mut(item_idx)?;
2093 if let SettingControl::DualList(ref mut state) = item.control {
2094 Some(f(state))
2095 } else {
2096 None
2097 }
2098 }
2099
2100 pub fn with_current_dual_list_mut<R>(
2103 &mut self,
2104 f: impl FnOnce(&mut crate::view::controls::DualListState) -> R,
2105 ) -> Option<R> {
2106 if let Some(item) = self.current_item_mut() {
2107 if let SettingControl::DualList(ref mut state) = item.control {
2108 return Some(f(state));
2109 }
2110 }
2111 None
2112 }
2113
2114 pub fn refresh_dual_list_sibling(&mut self) {
2121 let (new_included, sibling_path) = {
2122 let Some(item) = self.current_item() else {
2123 return;
2124 };
2125 let SettingControl::DualList(state) = &item.control else {
2126 return;
2127 };
2128 let Some(ref sib_path) = item.dual_list_sibling else {
2129 return;
2130 };
2131 (state.included.clone(), sib_path.clone())
2132 };
2133
2134 if let Some(page) = self.pages.get_mut(self.selected_category) {
2136 for other in page.items.iter_mut() {
2137 if other.path == sibling_path {
2138 if let SettingControl::DualList(ref mut sib_state) = other.control {
2139 sib_state.excluded = new_included;
2140 }
2141 break;
2142 }
2143 }
2144 }
2145 }
2146
2147 pub fn json_cursor_up(&mut self) {
2151 if let Some(item) = self.current_item_mut() {
2152 if let SettingControl::Json(state) = &mut item.control {
2153 state.move_up();
2154 }
2155 }
2156 }
2157
2158 pub fn json_cursor_down(&mut self) {
2160 if let Some(item) = self.current_item_mut() {
2161 if let SettingControl::Json(state) = &mut item.control {
2162 state.move_down();
2163 }
2164 }
2165 }
2166
2167 pub fn json_insert_newline(&mut self) {
2169 if let Some(item) = self.current_item_mut() {
2170 if let SettingControl::Json(state) = &mut item.control {
2171 state.insert('\n');
2172 }
2173 }
2174 }
2175
2176 pub fn json_delete(&mut self) {
2178 if let Some(item) = self.current_item_mut() {
2179 if let SettingControl::Json(state) = &mut item.control {
2180 state.delete();
2181 }
2182 }
2183 }
2184
2185 pub fn json_exit_editing(&mut self) {
2187 let is_valid = self
2188 .current_item()
2189 .map(|item| {
2190 if let SettingControl::Json(state) = &item.control {
2191 state.is_valid()
2192 } else {
2193 true
2194 }
2195 })
2196 .unwrap_or(true);
2197
2198 if is_valid {
2199 if let Some(item) = self.current_item_mut() {
2200 if let SettingControl::Json(state) = &mut item.control {
2201 state.commit();
2202 }
2203 }
2204 self.on_value_changed();
2205 } else if let Some(item) = self.current_item_mut() {
2206 if let SettingControl::Json(state) = &mut item.control {
2207 state.revert();
2208 }
2209 }
2210 self.editing_text = false;
2211 }
2212
2213 pub fn json_select_all(&mut self) {
2215 if let Some(item) = self.current_item_mut() {
2216 if let SettingControl::Json(state) = &mut item.control {
2217 state.select_all();
2218 }
2219 }
2220 }
2221
2222 pub fn json_selected_text(&self) -> Option<String> {
2224 if let Some(item) = self.current_item() {
2225 if let SettingControl::Json(state) = &item.control {
2226 return state.selected_text();
2227 }
2228 }
2229 None
2230 }
2231
2232 pub fn json_cursor_up_selecting(&mut self) {
2234 if let Some(item) = self.current_item_mut() {
2235 if let SettingControl::Json(state) = &mut item.control {
2236 state.editor.move_up_selecting();
2237 }
2238 }
2239 }
2240
2241 pub fn json_cursor_down_selecting(&mut self) {
2243 if let Some(item) = self.current_item_mut() {
2244 if let SettingControl::Json(state) = &mut item.control {
2245 state.editor.move_down_selecting();
2246 }
2247 }
2248 }
2249
2250 pub fn json_cursor_left_selecting(&mut self) {
2252 if let Some(item) = self.current_item_mut() {
2253 if let SettingControl::Json(state) = &mut item.control {
2254 state.editor.move_left_selecting();
2255 }
2256 }
2257 }
2258
2259 pub fn json_cursor_right_selecting(&mut self) {
2261 if let Some(item) = self.current_item_mut() {
2262 if let SettingControl::Json(state) = &mut item.control {
2263 state.editor.move_right_selecting();
2264 }
2265 }
2266 }
2267
2268 pub fn is_dropdown_open(&self) -> bool {
2272 self.current_item().is_some_and(|item| {
2273 if let SettingControl::Dropdown(ref d) = item.control {
2274 d.open
2275 } else {
2276 false
2277 }
2278 })
2279 }
2280
2281 pub fn dropdown_toggle(&mut self) {
2283 let mut opened = false;
2284 if let Some(item) = self.current_item_mut() {
2285 if let SettingControl::Dropdown(ref mut d) = item.control {
2286 d.toggle_open();
2287 opened = d.open;
2288 }
2289 }
2290
2291 if opened {
2293 let selected_item = self.selected_item;
2295 let width = self.layout_width;
2296 if let Some(page) = self.pages.get(self.selected_category) {
2297 self.scroll_panel.update_content_height(&page.items, width);
2298 self.scroll_panel
2300 .ensure_focused_visible(&page.items, selected_item, None, width);
2301 }
2302 }
2303 }
2304
2305 pub fn dropdown_prev(&mut self) {
2307 if let Some(item) = self.current_item_mut() {
2308 if let SettingControl::Dropdown(ref mut d) = item.control {
2309 d.select_prev();
2310 }
2311 }
2312 }
2313
2314 pub fn dropdown_next(&mut self) {
2316 if let Some(item) = self.current_item_mut() {
2317 if let SettingControl::Dropdown(ref mut d) = item.control {
2318 d.select_next();
2319 }
2320 }
2321 }
2322
2323 pub fn dropdown_home(&mut self) {
2325 if let Some(item) = self.current_item_mut() {
2326 if let SettingControl::Dropdown(ref mut d) = item.control {
2327 if !d.options.is_empty() {
2328 d.selected = 0;
2329 d.ensure_visible();
2330 }
2331 }
2332 }
2333 }
2334
2335 pub fn dropdown_end(&mut self) {
2337 if let Some(item) = self.current_item_mut() {
2338 if let SettingControl::Dropdown(ref mut d) = item.control {
2339 if !d.options.is_empty() {
2340 d.selected = d.options.len() - 1;
2341 d.ensure_visible();
2342 }
2343 }
2344 }
2345 }
2346
2347 pub fn dropdown_confirm(&mut self) {
2349 if let Some(item) = self.current_item_mut() {
2350 if let SettingControl::Dropdown(ref mut d) = item.control {
2351 d.confirm();
2352 }
2353 }
2354 self.on_value_changed();
2355 }
2356
2357 pub fn dropdown_cancel(&mut self) {
2359 if let Some(item) = self.current_item_mut() {
2360 if let SettingControl::Dropdown(ref mut d) = item.control {
2361 d.cancel();
2362 }
2363 }
2364 }
2365
2366 pub fn dropdown_select(&mut self, option_idx: usize) {
2368 if let Some(item) = self.current_item_mut() {
2369 if let SettingControl::Dropdown(ref mut d) = item.control {
2370 if option_idx < d.options.len() {
2371 d.selected = option_idx;
2372 d.confirm();
2373 }
2374 }
2375 }
2376 self.on_value_changed();
2377 }
2378
2379 pub fn set_dropdown_hover(&mut self, hover_idx: Option<usize>) -> bool {
2382 if let Some(item) = self.current_item_mut() {
2383 if let SettingControl::Dropdown(ref mut d) = item.control {
2384 if d.open && d.hover_index != hover_idx {
2385 d.hover_index = hover_idx;
2386 return true;
2387 }
2388 }
2389 }
2390 false
2391 }
2392
2393 pub fn dropdown_scroll(&mut self, delta: i32) {
2395 if let Some(item) = self.current_item_mut() {
2396 if let SettingControl::Dropdown(ref mut d) = item.control {
2397 if d.open {
2398 d.scroll_by(delta);
2399 }
2400 }
2401 }
2402 }
2403
2404 pub fn is_number_editing(&self) -> bool {
2408 self.current_item().is_some_and(|item| {
2409 if let SettingControl::Number(ref n) = item.control {
2410 n.editing()
2411 } else {
2412 false
2413 }
2414 })
2415 }
2416
2417 pub fn start_number_editing(&mut self) {
2419 if let Some(item) = self.current_item_mut() {
2420 if let SettingControl::Number(ref mut n) = item.control {
2421 n.start_editing();
2422 }
2423 }
2424 }
2425
2426 pub fn number_insert(&mut self, c: char) {
2428 if let Some(item) = self.current_item_mut() {
2429 if let SettingControl::Number(ref mut n) = item.control {
2430 n.insert_char(c);
2431 }
2432 }
2433 }
2434
2435 pub fn number_backspace(&mut self) {
2437 if let Some(item) = self.current_item_mut() {
2438 if let SettingControl::Number(ref mut n) = item.control {
2439 n.backspace();
2440 }
2441 }
2442 }
2443
2444 pub fn number_confirm(&mut self) {
2446 if let Some(item) = self.current_item_mut() {
2447 if let SettingControl::Number(ref mut n) = item.control {
2448 n.confirm_editing();
2449 }
2450 }
2451 self.on_value_changed();
2452 }
2453
2454 pub fn number_cancel(&mut self) {
2456 if let Some(item) = self.current_item_mut() {
2457 if let SettingControl::Number(ref mut n) = item.control {
2458 n.cancel_editing();
2459 }
2460 }
2461 }
2462
2463 pub fn number_delete(&mut self) {
2465 if let Some(item) = self.current_item_mut() {
2466 if let SettingControl::Number(ref mut n) = item.control {
2467 n.delete();
2468 }
2469 }
2470 }
2471
2472 pub fn number_move_left(&mut self) {
2474 if let Some(item) = self.current_item_mut() {
2475 if let SettingControl::Number(ref mut n) = item.control {
2476 n.move_left();
2477 }
2478 }
2479 }
2480
2481 pub fn number_move_right(&mut self) {
2483 if let Some(item) = self.current_item_mut() {
2484 if let SettingControl::Number(ref mut n) = item.control {
2485 n.move_right();
2486 }
2487 }
2488 }
2489
2490 pub fn number_move_home(&mut self) {
2492 if let Some(item) = self.current_item_mut() {
2493 if let SettingControl::Number(ref mut n) = item.control {
2494 n.move_home();
2495 }
2496 }
2497 }
2498
2499 pub fn number_move_end(&mut self) {
2501 if let Some(item) = self.current_item_mut() {
2502 if let SettingControl::Number(ref mut n) = item.control {
2503 n.move_end();
2504 }
2505 }
2506 }
2507
2508 pub fn number_move_left_selecting(&mut self) {
2510 if let Some(item) = self.current_item_mut() {
2511 if let SettingControl::Number(ref mut n) = item.control {
2512 n.move_left_selecting();
2513 }
2514 }
2515 }
2516
2517 pub fn number_move_right_selecting(&mut self) {
2519 if let Some(item) = self.current_item_mut() {
2520 if let SettingControl::Number(ref mut n) = item.control {
2521 n.move_right_selecting();
2522 }
2523 }
2524 }
2525
2526 pub fn number_move_home_selecting(&mut self) {
2528 if let Some(item) = self.current_item_mut() {
2529 if let SettingControl::Number(ref mut n) = item.control {
2530 n.move_home_selecting();
2531 }
2532 }
2533 }
2534
2535 pub fn number_move_end_selecting(&mut self) {
2537 if let Some(item) = self.current_item_mut() {
2538 if let SettingControl::Number(ref mut n) = item.control {
2539 n.move_end_selecting();
2540 }
2541 }
2542 }
2543
2544 pub fn number_move_word_left(&mut self) {
2546 if let Some(item) = self.current_item_mut() {
2547 if let SettingControl::Number(ref mut n) = item.control {
2548 n.move_word_left();
2549 }
2550 }
2551 }
2552
2553 pub fn number_move_word_right(&mut self) {
2555 if let Some(item) = self.current_item_mut() {
2556 if let SettingControl::Number(ref mut n) = item.control {
2557 n.move_word_right();
2558 }
2559 }
2560 }
2561
2562 pub fn number_move_word_left_selecting(&mut self) {
2564 if let Some(item) = self.current_item_mut() {
2565 if let SettingControl::Number(ref mut n) = item.control {
2566 n.move_word_left_selecting();
2567 }
2568 }
2569 }
2570
2571 pub fn number_move_word_right_selecting(&mut self) {
2573 if let Some(item) = self.current_item_mut() {
2574 if let SettingControl::Number(ref mut n) = item.control {
2575 n.move_word_right_selecting();
2576 }
2577 }
2578 }
2579
2580 pub fn number_select_all(&mut self) {
2582 if let Some(item) = self.current_item_mut() {
2583 if let SettingControl::Number(ref mut n) = item.control {
2584 n.select_all();
2585 }
2586 }
2587 }
2588
2589 pub fn number_delete_word_backward(&mut self) {
2591 if let Some(item) = self.current_item_mut() {
2592 if let SettingControl::Number(ref mut n) = item.control {
2593 n.delete_word_backward();
2594 }
2595 }
2596 }
2597
2598 pub fn number_delete_word_forward(&mut self) {
2600 if let Some(item) = self.current_item_mut() {
2601 if let SettingControl::Number(ref mut n) = item.control {
2602 n.delete_word_forward();
2603 }
2604 }
2605 }
2606
2607 pub fn get_change_descriptions(&self) -> Vec<String> {
2609 let mut descriptions: Vec<String> = self
2610 .pending_changes
2611 .iter()
2612 .map(|(path, value)| {
2613 let value_str = match value {
2614 serde_json::Value::Bool(b) => b.to_string(),
2615 serde_json::Value::Number(n) => n.to_string(),
2616 serde_json::Value::String(s) => format!("\"{}\"", s),
2617 _ => value.to_string(),
2618 };
2619 format!("{}: {}", path, value_str)
2620 })
2621 .collect();
2622 for path in &self.pending_deletions {
2624 descriptions.push(format!("{}: (reset to default)", path));
2625 }
2626 descriptions.sort();
2627 descriptions
2628 }
2629}
2630
2631fn update_control_from_value(control: &mut SettingControl, value: &serde_json::Value) {
2633 match control {
2634 SettingControl::Toggle(state) => {
2635 if let Some(b) = value.as_bool() {
2636 state.checked = b;
2637 }
2638 }
2639 SettingControl::Number(state) => {
2640 if let Some(n) = value.as_i64() {
2641 state.value = n;
2642 }
2643 }
2644 SettingControl::Dropdown(state) => {
2645 if let Some(s) = value.as_str() {
2646 if let Some(idx) = state.options.iter().position(|o| o == s) {
2647 state.selected = idx;
2648 }
2649 }
2650 }
2651 SettingControl::Text(state) => {
2652 if let Some(s) = value.as_str() {
2653 state.value = s.to_string();
2654 state.cursor = state.value.len();
2655 }
2656 }
2657 SettingControl::TextList(state) => {
2658 if let Some(arr) = value.as_array() {
2659 state.items = arr
2660 .iter()
2661 .filter_map(|v| {
2662 if state.is_integer {
2663 v.as_i64()
2664 .map(|n| n.to_string())
2665 .or_else(|| v.as_u64().map(|n| n.to_string()))
2666 .or_else(|| v.as_f64().map(|n| n.to_string()))
2667 } else {
2668 v.as_str().map(String::from)
2669 }
2670 })
2671 .collect();
2672 }
2673 }
2674 SettingControl::DualList(state) => {
2675 if let Some(arr) = value.as_array() {
2676 state.included = arr
2677 .iter()
2678 .filter_map(|v| v.as_str().map(String::from))
2679 .collect();
2680 }
2681 }
2682 SettingControl::Map(state) => {
2683 if let Some(obj) = value.as_object() {
2684 state.entries = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
2685 state.entries.sort_by(|a, b| a.0.cmp(&b.0));
2686 }
2687 }
2688 SettingControl::ObjectArray(state) => {
2689 if let Some(arr) = value.as_array() {
2690 state.bindings = arr.clone();
2691 }
2692 }
2693 SettingControl::Json(state) => {
2694 let json_str =
2696 serde_json::to_string_pretty(value).unwrap_or_else(|_| "null".to_string());
2697 let json_str = if json_str.is_empty() {
2698 "null".to_string()
2699 } else {
2700 json_str
2701 };
2702 state.original_text = json_str.clone();
2703 state.editor.set_value(&json_str);
2704 state.scroll_offset = 0;
2705 }
2706 SettingControl::Complex { .. } => {}
2707 }
2708}
2709
2710#[cfg(test)]
2711mod tests {
2712 use super::*;
2713
2714 const TEST_SCHEMA: &str = r#"
2715{
2716 "type": "object",
2717 "properties": {
2718 "theme": {
2719 "type": "string",
2720 "default": "dark"
2721 },
2722 "line_numbers": {
2723 "type": "boolean",
2724 "default": true
2725 }
2726 },
2727 "$defs": {}
2728}
2729"#;
2730
2731 fn test_config() -> Config {
2732 Config::default()
2733 }
2734
2735 #[test]
2736 fn test_settings_state_creation() {
2737 let config = test_config();
2738 let state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2739
2740 assert!(!state.visible);
2741 assert_eq!(state.selected_category, 0);
2742 assert!(!state.has_changes());
2743 }
2744
2745 #[test]
2746 fn test_navigation() {
2747 let config = test_config();
2748 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2749
2750 assert_eq!(state.focus_panel(), FocusPanel::Categories);
2752
2753 state.toggle_focus();
2755 assert_eq!(state.focus_panel(), FocusPanel::Settings);
2756
2757 state.select_next();
2759 assert_eq!(state.selected_item, 1);
2760
2761 state.select_prev();
2762 assert_eq!(state.selected_item, 0);
2763 }
2764
2765 #[test]
2766 fn test_pending_changes() {
2767 let config = test_config();
2768 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2769
2770 assert!(!state.has_changes());
2771
2772 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
2773 assert!(state.has_changes());
2774
2775 state.discard_changes();
2776 assert!(!state.has_changes());
2777 }
2778
2779 #[test]
2780 fn test_show_hide() {
2781 let config = test_config();
2782 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
2783
2784 assert!(!state.visible);
2785
2786 state.show();
2787 assert!(state.visible);
2788 assert_eq!(state.focus_panel(), FocusPanel::Categories);
2789
2790 state.hide();
2791 assert!(!state.visible);
2792 }
2793
2794 const TEST_SCHEMA_CONTROLS: &str = r#"
2796{
2797 "type": "object",
2798 "properties": {
2799 "theme": {
2800 "type": "string",
2801 "enum": ["dark", "light", "high-contrast"],
2802 "default": "dark"
2803 },
2804 "tab_size": {
2805 "type": "integer",
2806 "minimum": 1,
2807 "maximum": 8,
2808 "default": 4
2809 },
2810 "line_numbers": {
2811 "type": "boolean",
2812 "default": true
2813 }
2814 },
2815 "$defs": {}
2816}
2817"#;
2818
2819 #[test]
2820 fn test_dropdown_toggle() {
2821 let config = test_config();
2822 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2823 state.show();
2824 state.toggle_focus(); state.select_next();
2829 state.select_next();
2830 assert!(!state.is_dropdown_open());
2831
2832 state.dropdown_toggle();
2833 assert!(state.is_dropdown_open());
2834
2835 state.dropdown_toggle();
2836 assert!(!state.is_dropdown_open());
2837 }
2838
2839 #[test]
2840 fn test_dropdown_cancel_restores() {
2841 let config = test_config();
2842 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2843 state.show();
2844 state.toggle_focus();
2845
2846 state.select_next();
2849 state.select_next();
2850
2851 state.dropdown_toggle();
2853 assert!(state.is_dropdown_open());
2854
2855 let initial = state.current_item().and_then(|item| {
2857 if let SettingControl::Dropdown(ref d) = item.control {
2858 Some(d.selected)
2859 } else {
2860 None
2861 }
2862 });
2863
2864 state.dropdown_next();
2866 let after_change = state.current_item().and_then(|item| {
2867 if let SettingControl::Dropdown(ref d) = item.control {
2868 Some(d.selected)
2869 } else {
2870 None
2871 }
2872 });
2873 assert_ne!(initial, after_change);
2874
2875 state.dropdown_cancel();
2877 assert!(!state.is_dropdown_open());
2878
2879 let after_cancel = state.current_item().and_then(|item| {
2880 if let SettingControl::Dropdown(ref d) = item.control {
2881 Some(d.selected)
2882 } else {
2883 None
2884 }
2885 });
2886 assert_eq!(initial, after_cancel);
2887 }
2888
2889 #[test]
2890 fn test_dropdown_confirm_keeps_selection() {
2891 let config = test_config();
2892 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2893 state.show();
2894 state.toggle_focus();
2895
2896 state.dropdown_toggle();
2898
2899 state.dropdown_next();
2901 let after_change = state.current_item().and_then(|item| {
2902 if let SettingControl::Dropdown(ref d) = item.control {
2903 Some(d.selected)
2904 } else {
2905 None
2906 }
2907 });
2908
2909 state.dropdown_confirm();
2911 assert!(!state.is_dropdown_open());
2912
2913 let after_confirm = state.current_item().and_then(|item| {
2914 if let SettingControl::Dropdown(ref d) = item.control {
2915 Some(d.selected)
2916 } else {
2917 None
2918 }
2919 });
2920 assert_eq!(after_change, after_confirm);
2921 }
2922
2923 #[test]
2924 fn test_number_editing() {
2925 let config = test_config();
2926 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2927 state.show();
2928 state.toggle_focus();
2929
2930 state.select_next();
2932
2933 assert!(!state.is_number_editing());
2935
2936 state.start_number_editing();
2938 assert!(state.is_number_editing());
2939
2940 state.number_insert('8');
2942
2943 state.number_confirm();
2945 assert!(!state.is_number_editing());
2946 }
2947
2948 #[test]
2949 fn test_number_cancel_editing() {
2950 let config = test_config();
2951 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2952 state.show();
2953 state.toggle_focus();
2954
2955 state.select_next();
2957
2958 let initial_value = state.current_item().and_then(|item| {
2960 if let SettingControl::Number(ref n) = item.control {
2961 Some(n.value)
2962 } else {
2963 None
2964 }
2965 });
2966
2967 state.start_number_editing();
2969 state.number_backspace();
2970 state.number_insert('9');
2971 state.number_insert('9');
2972
2973 state.number_cancel();
2975 assert!(!state.is_number_editing());
2976
2977 let after_cancel = state.current_item().and_then(|item| {
2979 if let SettingControl::Number(ref n) = item.control {
2980 Some(n.value)
2981 } else {
2982 None
2983 }
2984 });
2985 assert_eq!(initial_value, after_cancel);
2986 }
2987
2988 #[test]
2989 fn test_number_backspace() {
2990 let config = test_config();
2991 let mut state = SettingsState::new(TEST_SCHEMA_CONTROLS, &config).unwrap();
2992 state.show();
2993 state.toggle_focus();
2994 state.select_next();
2995
2996 state.start_number_editing();
2997 state.number_backspace();
2998
2999 let display_text = state.current_item().and_then(|item| {
3001 if let SettingControl::Number(ref n) = item.control {
3002 Some(n.display_text())
3003 } else {
3004 None
3005 }
3006 });
3007 assert_eq!(display_text, Some(String::new()));
3009
3010 state.number_cancel();
3011 }
3012
3013 #[test]
3014 fn test_layer_selection() {
3015 let config = test_config();
3016 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3017
3018 assert_eq!(state.target_layer, ConfigLayer::User);
3020 assert_eq!(state.target_layer_name(), "User");
3021
3022 state.cycle_target_layer();
3024 assert_eq!(state.target_layer, ConfigLayer::Project);
3025 assert_eq!(state.target_layer_name(), "Project");
3026
3027 state.cycle_target_layer();
3028 assert_eq!(state.target_layer, ConfigLayer::Session);
3029 assert_eq!(state.target_layer_name(), "Session");
3030
3031 state.cycle_target_layer();
3032 assert_eq!(state.target_layer, ConfigLayer::User);
3033
3034 state.set_target_layer(ConfigLayer::Project);
3036 assert_eq!(state.target_layer, ConfigLayer::Project);
3037
3038 state.set_target_layer(ConfigLayer::System);
3040 assert_eq!(state.target_layer, ConfigLayer::Project);
3041 }
3042
3043 #[test]
3044 fn test_layer_switch_clears_pending_changes() {
3045 let config = test_config();
3046 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3047
3048 state.set_pending_change("/theme", serde_json::Value::String("light".to_string()));
3050 assert!(state.has_changes());
3051
3052 state.cycle_target_layer();
3054 assert!(!state.has_changes());
3055 }
3056
3057 #[test]
3076 fn nested_array_save_records_full_entry_path() {
3077 use crate::view::settings::schema::SettingType;
3080
3081 let config = test_config();
3082 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3083
3084 let item_schema = SettingSchema {
3086 path: "/item".to_string(),
3087 name: "Server".to_string(),
3088 description: None,
3089 setting_type: SettingType::Object {
3090 properties: vec![SettingSchema {
3091 path: "/enabled".to_string(),
3092 name: "Enabled".to_string(),
3093 description: None,
3094 setting_type: SettingType::Boolean,
3095 default: Some(serde_json::json!(false)),
3096 read_only: false,
3097 section: None,
3098 order: None,
3099 nullable: false,
3100 enum_from: None,
3101 dual_list_sibling: None,
3102 }],
3103 },
3104 default: None,
3105 read_only: false,
3106 section: None,
3107 order: None,
3108 nullable: false,
3109 enum_from: None,
3110 dual_list_sibling: None,
3111 };
3112
3113 let value_schema = SettingSchema {
3118 path: String::new(),
3119 name: "value".to_string(),
3120 description: None,
3121 setting_type: SettingType::ObjectArray {
3122 item_schema: Box::new(item_schema.clone()),
3123 display_field: None,
3124 },
3125 default: None,
3126 read_only: false,
3127 section: None,
3128 order: None,
3129 nullable: false,
3130 enum_from: None,
3131 dual_list_sibling: None,
3132 };
3133
3134 let parent = EntryDialogState::from_schema(
3138 "quicklsp".to_string(),
3139 &serde_json::json!([{ "enabled": true }]),
3140 &value_schema,
3141 "/universal_lsp",
3142 false, false,
3144 );
3145
3146 assert!(
3148 parent.is_single_value,
3149 "array value_schema should trigger is_single_value path"
3150 );
3151 assert_eq!(parent.entry_path(), "/universal_lsp/quicklsp");
3152
3153 state.entry_dialog_stack.push(parent);
3154
3155 state.open_nested_entry_dialog();
3160
3161 assert_eq!(
3163 state.entry_dialog_stack.len(),
3164 2,
3165 "open_nested_entry_dialog should have pushed a nested dialog"
3166 );
3167
3168 let nested_map_path = state
3171 .entry_dialog_stack
3172 .last()
3173 .map(|d| d.map_path.clone())
3174 .unwrap();
3175 assert_eq!(
3176 nested_map_path, "/universal_lsp/quicklsp",
3177 "BUG: nested dialog's map_path dropped the 'quicklsp' key segment"
3178 );
3179
3180 state.save_entry_dialog();
3182
3183 assert_eq!(state.entry_dialog_stack.len(), 1);
3185
3186 assert!(
3189 !state.pending_changes.contains_key("/universal_lsp/"),
3190 "regression: pending change recorded under empty-key path /universal_lsp/. \
3191 All keys: {:?}",
3192 state.pending_changes.keys().collect::<Vec<_>>()
3193 );
3194 assert!(
3195 !state
3196 .pending_changes
3197 .keys()
3198 .any(|k| k.starts_with("/universal_lsp") && k.ends_with('/')),
3199 "no /universal_lsp/* path should end in a trailing slash; got {:?}",
3200 state.pending_changes.keys().collect::<Vec<_>>()
3201 );
3202 assert!(
3203 state
3204 .pending_changes
3205 .contains_key("/universal_lsp/quicklsp"),
3206 "expected pending change at /universal_lsp/quicklsp, got {:?}",
3207 state.pending_changes.keys().collect::<Vec<_>>()
3208 );
3209 }
3210
3211 #[test]
3212 fn test_refresh_dual_list_sibling_updates_excluded() {
3213 use crate::view::controls::DualListState;
3214
3215 let schema = include_str!("../../../plugins/config-schema.json");
3218 let config = test_config();
3219 let mut state = SettingsState::new(schema, &config).unwrap();
3220
3221 let editor_page_idx = state
3223 .pages
3224 .iter()
3225 .position(|p| p.path == "/editor")
3226 .expect("editor page");
3227 state.selected_category = editor_page_idx;
3228
3229 let (left_idx, right_idx) = {
3230 let page = &state.pages[editor_page_idx];
3231 let l = page
3232 .items
3233 .iter()
3234 .position(|i| i.path == "/editor/status_bar/left")
3235 .expect("left item");
3236 let r = page
3237 .items
3238 .iter()
3239 .position(|i| i.path == "/editor/status_bar/right")
3240 .expect("right item");
3241 (l, r)
3242 };
3243
3244 assert!(matches!(
3246 &state.pages[editor_page_idx].items[left_idx].control,
3247 SettingControl::DualList(_)
3248 ));
3249
3250 let default_right_items: Vec<String> =
3252 match &state.pages[editor_page_idx].items[right_idx].control {
3253 SettingControl::DualList(dl) => dl.included.clone(),
3254 _ => panic!("right should be DualList"),
3255 };
3256 let initial_left_excluded: Vec<String> =
3257 match &state.pages[editor_page_idx].items[left_idx].control {
3258 SettingControl::DualList(dl) => dl.excluded.clone(),
3259 _ => panic!("left should be DualList"),
3260 };
3261 assert_eq!(
3262 initial_left_excluded, default_right_items,
3263 "left.excluded should mirror right's included on initial build"
3264 );
3265
3266 let new_element = "{chord}".to_string();
3268 state.selected_item = left_idx;
3269 state
3270 .with_current_dual_list_mut(|dl: &mut DualListState| {
3271 if !dl.included.contains(&new_element) {
3272 dl.included.push(new_element.clone());
3273 }
3274 })
3275 .expect("current item is a DualList");
3276
3277 state.refresh_dual_list_sibling();
3279
3280 match &state.pages[editor_page_idx].items[right_idx].control {
3281 SettingControl::DualList(dl) => {
3282 assert!(
3283 dl.excluded.contains(&new_element),
3284 "right.excluded should be updated to reflect left's new inclusion"
3285 );
3286 }
3287 _ => panic!("right should be DualList"),
3288 }
3289 }
3290
3291 #[test]
3292 fn test_with_dual_list_mut_returns_none_for_non_dual_list() {
3293 let config = test_config();
3294 let mut state = SettingsState::new(TEST_SCHEMA, &config).unwrap();
3295
3296 let result = state.with_dual_list_mut(0, |_| ());
3298 assert!(result.is_none());
3299 }
3300}