1use crate::app::Editor;
15use fresh_core::LeafId;
16use ratatui::layout::Rect;
17use serde::Serialize;
18use std::collections::HashMap;
19
20#[derive(Debug, Clone, Copy, Serialize)]
23pub struct RectView {
24 pub x: u16,
25 pub y: u16,
26 pub w: u16,
27 pub h: u16,
28}
29
30impl From<Rect> for RectView {
31 fn from(r: Rect) -> Self {
32 RectView {
33 x: r.x,
34 y: r.y,
35 w: r.width,
36 h: r.height,
37 }
38 }
39}
40
41#[derive(Debug, Clone, Serialize)]
45#[serde(tag = "kind", rename_all = "lowercase")]
46pub enum MenuItemView {
47 Action {
48 label: String,
49 action: String,
50 #[serde(skip_serializing_if = "HashMap::is_empty")]
51 args: HashMap<String, serde_json::Value>,
52 accel: Option<String>,
53 enabled: bool,
54 checked: Option<bool>,
55 },
56 Sep,
57 Submenu {
58 label: String,
59 items: Vec<MenuItemView>,
60 },
61 Label {
62 label: String,
63 },
64}
65
66#[derive(Debug, Clone, Serialize)]
69pub struct MenuEntry {
70 pub label: String,
71 pub visible: bool,
75 pub x: Option<u16>,
76 pub w: Option<u16>,
77 pub items: Vec<MenuItemView>,
78}
79
80#[derive(Debug, Clone, Serialize)]
84pub struct DropdownView {
85 pub rect: Option<RectView>,
86 pub items: Vec<ItemArea>,
87 pub submenus: Vec<SubmenuArea>,
88}
89
90#[derive(Debug, Clone, Serialize)]
91pub struct ItemArea {
92 pub index: usize,
93 pub rect: RectView,
94}
95
96#[derive(Debug, Clone, Serialize)]
97pub struct SubmenuArea {
98 pub depth: usize,
99 pub index: usize,
100 pub rect: RectView,
101}
102
103#[derive(Debug, Clone, Serialize)]
107#[serde(rename_all = "camelCase")]
108pub struct MenuView {
109 pub menus: Vec<MenuEntry>,
110 pub menu_open: Option<usize>,
111 pub menu_highlight: Option<usize>,
112 pub submenu_path: Vec<usize>,
113 pub dropdown: Option<DropdownView>,
114}
115
116fn item_view(editor: &Editor, item: &fresh_core::menu::MenuItem) -> MenuItemView {
117 use fresh_core::menu::MenuItem::*;
118 match item {
119 Separator { .. } => MenuItemView::Sep,
120 Action {
121 label,
122 action,
123 args,
124 when: _,
125 checkbox,
126 } => MenuItemView::Action {
127 label: label.clone(),
128 action: action.clone(),
129 args: args.clone(),
130 accel: editor.accelerator_for(action),
131 enabled: crate::view::ui::menu::is_menu_item_enabled(
133 item,
134 &editor.menu_state().context,
135 ),
136 checked: checkbox.as_ref().map(|_| {
137 crate::view::ui::menu::is_checkbox_checked(checkbox, &editor.menu_state().context)
138 }),
139 },
140 Submenu { label, items } => MenuItemView::Submenu {
141 label: label.clone(),
142 items: items.iter().map(|i| item_view(editor, i)).collect(),
143 },
144 DynamicSubmenu { label, .. } => MenuItemView::Submenu {
145 label: label.clone(),
146 items: Vec::new(),
147 },
148 Label { info } => MenuItemView::Label {
149 label: info.clone(),
150 },
151 }
152}
153
154fn union_rect(rects: &[Rect]) -> Option<Rect> {
155 let mut acc: Option<Rect> = None;
156 for r in rects {
157 acc = Some(match acc {
158 None => *r,
159 Some(a) => {
160 let x0 = a.x.min(r.x);
161 let y0 = a.y.min(r.y);
162 let x1 = (a.x + a.width).max(r.x + r.width);
163 let y1 = (a.y + a.height).max(r.y + r.height);
164 Rect::new(x0, y0, x1 - x0, y1 - y0)
165 }
166 });
167 }
168 acc
169}
170
171impl Editor {
172 pub fn menu_view(&self) -> MenuView {
179 let chrome = self.active_chrome();
180 let menu_areas: HashMap<usize, Rect> = chrome
181 .menu_layout
182 .as_ref()
183 .map(|m| m.menu_areas.iter().cloned().collect())
184 .unwrap_or_default();
185
186 let menus: Vec<MenuEntry> = self
189 .all_menus_expanded()
190 .iter()
191 .enumerate()
192 .map(|(i, m)| MenuEntry {
193 label: m.label.clone(),
194 visible: crate::view::ui::menu::is_menu_visible(m, &self.menu_state().context),
195 x: menu_areas.get(&i).map(|r| r.x),
196 w: menu_areas.get(&i).map(|r| r.width),
197 items: m.items.iter().map(|it| item_view(self, it)).collect(),
198 })
199 .collect();
200
201 let dropdown = chrome.menu_layout.as_ref().and_then(|ml| {
202 if ml.item_areas.is_empty() {
203 return None;
204 }
205 let rects: Vec<Rect> = ml.item_areas.iter().map(|(_, r)| *r).collect();
206 Some(DropdownView {
207 rect: union_rect(&rects).map(RectView::from),
208 items: ml
209 .item_areas
210 .iter()
211 .map(|(index, r)| ItemArea {
212 index: *index,
213 rect: RectView::from(*r),
214 })
215 .collect(),
216 submenus: ml
217 .submenu_areas
218 .iter()
219 .map(|(depth, index, r)| SubmenuArea {
220 depth: *depth,
221 index: *index,
222 rect: RectView::from(*r),
223 })
224 .collect(),
225 })
226 });
227
228 let ms = self.menu_state();
229 MenuView {
230 menus,
231 menu_open: ms.active_menu,
232 menu_highlight: ms.highlighted_item,
233 submenu_path: ms.submenu_path.clone(),
234 dropdown,
235 }
236 }
237}
238
239#[derive(Debug, Clone, Serialize)]
244#[serde(rename_all = "camelCase")]
245pub struct TabView {
246 pub buffer_id: Option<usize>,
247 pub label: String,
248 pub active: bool,
249 pub modified: bool,
250 pub rect: RectView,
251 pub close_rect: RectView,
252}
253
254#[derive(Debug, Clone, Default, Serialize)]
256pub struct TabBarView {
257 pub bar: Option<RectView>,
258 pub tabs: Vec<TabView>,
259}
260
261#[derive(Debug, Clone, Serialize)]
264pub struct StatusSegment {
265 pub name: &'static str,
266 pub key: Option<String>,
267 pub text: String,
268 pub x: u16,
269 pub w: u16,
270 pub side: &'static str,
271}
272
273#[derive(Debug, Clone, Serialize)]
274pub struct StatusView {
275 pub rect: RectView,
276 pub segments: Vec<StatusSegment>,
277}
278
279#[derive(Debug, Clone, Serialize)]
282pub struct SuggestionView {
283 pub text: String,
284 pub description: Option<String>,
285 pub keybinding: Option<String>,
286 pub disabled: bool,
287}
288
289#[derive(Debug, Clone, Serialize)]
290#[serde(rename_all = "camelCase")]
291pub struct PaletteView {
292 pub query: String,
293 pub message: String,
294 pub prompt_type: &'static str,
295 pub overlay: bool,
296 pub title: String,
297 pub status: String,
298 pub selected: Option<usize>,
299 pub scroll_start: usize,
300 pub visible_count: usize,
301 pub total: usize,
302 pub outer_rect: Option<RectView>,
303 pub list_rect: Option<RectView>,
304 #[serde(skip_serializing_if = "Option::is_none")]
309 pub preview_rect: Option<RectView>,
310 pub suggestions: Vec<SuggestionView>,
311 #[serde(skip_serializing_if = "Option::is_none")]
315 pub toolbar: Option<fresh_core::api::WidgetSpec>,
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub toolbar_focus: Option<String>,
318}
319
320fn prompt_type_tag(t: &crate::view::prompt::PromptType) -> &'static str {
322 use crate::view::prompt::PromptType::*;
323 match t {
324 QuickOpen => "quickopen",
325 LiveGrep => "livegrep",
326 Search | ReplaceSearch | QueryReplaceSearch => "search",
327 OpenFile | OpenFileWithEncoding { .. } => "openfile",
328 SaveFileAs => "saveas",
329 GotoLine | GotoByteOffset => "goto",
330 _ => "input",
331 }
332}
333
334impl Editor {
335 pub fn tab_bar_view(&self, leaf: LeafId) -> TabBarView {
338 let active = self.active_buffer();
339 let layout = self.active_layout();
340 match layout.tab_layouts.get(&leaf) {
341 None => TabBarView::default(),
342 Some(tl) => TabBarView {
343 bar: Some(RectView::from(tl.bar_area)),
344 tabs: tl
345 .tabs
346 .iter()
347 .map(|tab| {
348 let bid = tab.target.as_buffer();
349 TabView {
350 buffer_id: bid.map(|b| b.0),
351 label: bid
352 .and_then(|b| self.buffer_display_name(b))
353 .unwrap_or_else(|| "untitled".into()),
354 active: bid == Some(active),
355 modified: bid.map(|b| self.buffer_is_modified(b)).unwrap_or(false),
356 rect: RectView::from(tab.tab_area),
357 close_rect: RectView::from(tab.close_area),
358 }
359 })
360 .collect(),
361 },
362 }
363 }
364
365 pub fn status_view(&self) -> Option<StatusView> {
370 let chrome = self.active_chrome();
371 let (sy, sx, sw) = chrome.status_bar_area?;
372
373 let segments: Vec<StatusSegment> = chrome
378 .status_bar_segments
379 .iter()
380 .filter(|s| !s.text.trim().is_empty())
381 .map(|s| StatusSegment {
382 name: s.name,
383 key: s.key.clone(),
384 text: s.text.trim().to_string(),
385 x: s.x,
386 w: s.w,
387 side: s.side,
388 })
389 .collect();
390
391 Some(StatusView {
392 rect: RectView {
393 x: sx,
394 y: sy,
395 w: sw,
396 h: 1,
397 },
398 segments,
399 })
400 }
401
402 pub fn palette_view(&self) -> Option<PaletteView> {
406 let chrome = self.active_chrome();
407 let sugg_outer = chrome.suggestions_outer_area;
408 let sugg_area = chrome.suggestions_area;
409 let prompt_results = chrome.prompt_results_area;
410 let p = self.active_window().prompt.as_ref()?;
411 if p.suggestions.is_empty() && !p.overlay {
412 return None;
413 }
414 let (scroll_start, visible, total) = sugg_area.map(|(_, s, v, t)| (s, v, t)).unwrap_or((
415 p.scroll_offset,
416 p.suggestions.len(),
417 p.suggestions.len(),
418 ));
419 Some(PaletteView {
420 query: p.input.clone(),
421 message: p.message.clone(),
422 prompt_type: prompt_type_tag(&p.prompt_type),
423 overlay: p.overlay,
424 title: p.title.iter().map(|t| t.text.as_str()).collect(),
425 status: p.status.clone(),
426 selected: p.selected_suggestion,
427 scroll_start,
428 visible_count: visible,
429 total,
430 outer_rect: sugg_outer.map(RectView::from),
431 list_rect: sugg_area
432 .map(|(r, _, _, _)| r)
433 .or(prompt_results)
434 .map(RectView::from),
435 preview_rect: chrome.prompt_preview_area.and_then(|r| {
439 (r.width > 1 && r.height > 0).then(|| {
440 RectView::from(Rect::new(
441 r.x.saturating_add(1),
442 r.y,
443 r.width.saturating_sub(1),
444 r.height,
445 ))
446 })
447 }),
448 suggestions: p
449 .suggestions
450 .iter()
451 .map(|s| SuggestionView {
452 text: s.text.clone(),
453 description: s.description.clone(),
454 keybinding: s.keybinding.clone(),
455 disabled: s.disabled,
456 })
457 .collect(),
458 toolbar: p.toolbar_widget.clone(),
459 toolbar_focus: p.toolbar_focus.clone(),
460 })
461 }
462}
463
464#[derive(Debug, Clone, Serialize)]
467pub struct PopupItemView {
468 pub text: String,
469 pub detail: Option<String>,
470 pub icon: Option<String>,
471 pub disabled: bool,
472}
473
474#[derive(Debug, Clone, Serialize)]
475#[serde(tag = "type", rename_all = "lowercase")]
476pub enum PopupContentView {
477 List {
478 items: Vec<PopupItemView>,
479 selected: usize,
480 },
481 Lines {
482 lines: Vec<String>,
483 },
484}
485
486#[derive(Debug, Clone, Serialize)]
491#[serde(rename_all = "camelCase")]
492pub struct ScenePopup {
493 pub kind: &'static str,
494 pub title: Option<String>,
495 pub description: Option<String>,
496 pub rect: RectView,
497 pub content_rect: RectView,
498 pub scroll_offset: usize,
499 pub content: PopupContentView,
500}
501
502fn project_popup(
503 p: &crate::view::popup::Popup,
504 outer: Rect,
505 inner: Rect,
506 scroll: usize,
507) -> ScenePopup {
508 use crate::view::popup::{PopupContent, PopupKind};
509 let kind = match p.kind {
510 PopupKind::Completion => "completion",
511 PopupKind::Hover => "hover",
512 PopupKind::Action => "action",
513 PopupKind::List => "list",
514 PopupKind::Text => "text",
515 };
516 let content = match &p.content {
517 PopupContent::List { items, selected } => PopupContentView::List {
518 items: items
519 .iter()
520 .map(|i| PopupItemView {
521 text: i.text.clone(),
522 detail: i.detail.clone(),
523 icon: i.icon.clone(),
524 disabled: i.disabled,
525 })
526 .collect(),
527 selected: *selected,
528 },
529 PopupContent::Text(lines) | PopupContent::Custom(lines) => PopupContentView::Lines {
530 lines: lines.clone(),
531 },
532 PopupContent::Markdown(styled) => PopupContentView::Lines {
533 lines: styled
534 .iter()
535 .map(|l| l.spans.iter().map(|s| s.text.as_str()).collect::<String>())
536 .collect(),
537 },
538 };
539 ScenePopup {
540 kind,
541 title: p.title.clone(),
542 description: p.description.clone(),
543 rect: RectView::from(outer),
544 content_rect: RectView::from(inner),
545 scroll_offset: scroll,
546 content,
547 }
548}
549
550impl Editor {
551 pub fn popups_view(&self) -> Vec<ScenePopup> {
556 let chrome = self.active_chrome();
557 let mut out = Vec::new();
558 let locals = self.active_state().popups.all();
559 for (idx, outer, inner, scroll, _n, _sb, _t) in &chrome.popup_areas {
560 if let Some(p) = locals.get(*idx) {
561 out.push(project_popup(p, *outer, *inner, *scroll));
562 }
563 }
564 let globals = self.global_popups.all();
565 for (idx, outer, inner, scroll, _n) in &chrome.global_popup_areas {
566 if let Some(p) = globals.get(*idx) {
567 out.push(project_popup(p, *outer, *inner, *scroll));
568 }
569 }
570 out
571 }
572}
573
574#[derive(Debug, Clone, Serialize)]
577#[serde(rename_all = "camelCase")]
578pub struct FileRow {
579 pub name: String,
580 pub depth: usize,
581 pub is_dir: bool,
582 pub expanded: bool,
583}
584
585#[derive(Debug, Clone, Serialize)]
586#[serde(rename_all = "camelCase")]
587pub struct FileExplorerView {
588 pub rect: RectView,
589 pub title: String,
590 pub scroll_offset: usize,
591 pub viewport_height: usize,
592 pub selected: Option<usize>,
593 pub rows: Vec<FileRow>,
594}
595
596impl Editor {
597 pub fn file_explorer_view(&self) -> Option<FileExplorerView> {
603 let rect = self.active_layout().file_explorer_area?;
604 let view = self.file_explorer()?;
605 let tree = view.tree();
606 let rows = view
607 .get_display_nodes()
608 .into_iter()
609 .filter_map(|(id, indent)| {
610 tree.get_node(id).map(|n| FileRow {
611 name: n.entry.name.clone(),
612 depth: indent,
613 is_dir: n.is_dir(),
614 expanded: n.is_expanded(),
615 })
616 })
617 .collect();
618 let title = tree
619 .get_node(tree.root_id())
620 .map(|n| n.entry.name.clone())
621 .unwrap_or_default();
622 Some(FileExplorerView {
623 rect: RectView::from(rect),
624 title,
625 scroll_offset: view.get_scroll_offset(),
626 viewport_height: view.viewport_height,
627 selected: view.get_selected_index(),
628 rows,
629 })
630 }
631}
632
633#[derive(Debug, Clone, Serialize)]
636pub struct TrustOptionView {
637 pub label: String,
638 pub description: String,
639 pub selected: bool,
640 pub data: &'static str,
641 pub rect: RectView,
642}
643
644#[derive(Debug, Clone, Serialize)]
645#[serde(rename_all = "camelCase")]
646pub struct TrustDialogView {
647 pub dialog: RectView,
648 pub title: String,
649 pub path: String,
650 pub triggers: String,
651 pub cancellable: bool,
652 pub options: Vec<TrustOptionView>,
653 pub ok: RectView,
654 pub ok_label: String,
655 pub quit: RectView,
656 pub quit_label: String,
657}
658
659impl Editor {
660 pub fn trust_dialog_view(&self) -> Option<TrustDialogView> {
665 let layout = self.active_chrome().workspace_trust_dialog.clone()?;
666 let selected = self.current_workspace_trust_selection();
667 let data = ["trusted", "restricted", "blocked"];
668 let options = crate::view::workspace_trust_dialog::options()
669 .into_iter()
670 .enumerate()
671 .map(|(i, o)| TrustOptionView {
672 label: o.label,
673 description: o.description,
674 selected: i == selected,
675 data: data.get(i).copied().unwrap_or("restricted"),
676 rect: RectView::from(layout.radios[i]),
677 })
678 .collect();
679 let quit_label = if self.workspace_trust_cancellable() {
680 rust_i18n::t!("trust.dialog.btn_cancel").into_owned()
681 } else {
682 rust_i18n::t!("trust.dialog.btn_quit").into_owned()
683 };
684 Some(TrustDialogView {
685 dialog: RectView::from(layout.dialog),
686 title: rust_i18n::t!("trust.dialog.security_warning").into_owned(),
687 path: self.working_dir().display().to_string(),
688 triggers: self.workspace_trust_markers().join(", "),
689 cancellable: self.workspace_trust_cancellable(),
690 options,
691 ok: RectView::from(layout.ok),
692 ok_label: rust_i18n::t!("trust.dialog.btn_ok").into_owned(),
693 quit: RectView::from(layout.quit),
694 quit_label,
695 })
696 }
697}
698
699#[derive(Debug, Clone, Serialize)]
702#[serde(rename_all = "camelCase")]
703pub struct WidgetHitView {
704 pub index: usize,
707 pub widget_key: String,
708 pub widget_kind: String,
709 pub event_type: String,
710 pub payload: serde_json::Value,
711}
712
713#[derive(Debug, Clone, Serialize)]
716#[serde(rename_all = "camelCase")]
717pub struct WidgetInstanceView {
718 pub selected_index: Option<i32>,
719 pub scroll_offset: Option<u32>,
720 #[serde(skip_serializing_if = "Vec::is_empty")]
721 pub expanded_keys: Vec<String>,
722}
723
724#[derive(Debug, Clone, Serialize)]
725#[serde(rename_all = "camelCase")]
726pub struct WidgetSurfaceView {
727 pub kind: &'static str,
729 pub plugin: String,
730 pub panel_id: u64,
731 pub rect: RectView,
732 pub focus_key: String,
733 pub spec: fresh_core::api::WidgetSpec,
735 pub instances: HashMap<String, WidgetInstanceView>,
736 pub hits: Vec<WidgetHitView>,
737}
738
739impl Editor {
740 pub fn widgets_view(&self) -> Vec<WidgetSurfaceView> {
747 let mut out = Vec::new();
748 for (kind, slot) in [
749 ("dock", self.dock.as_ref()),
750 ("floatingModal", self.floating_widget_panel.as_ref()),
751 ] {
752 let Some(fwp) = slot else { continue };
753 let Some(rect) = fwp.last_inner_rect else {
754 continue;
755 };
756 let Some(panel) = self.widget_registry.get(&fwp.panel_key) else {
757 continue;
758 };
759 let mut instances = HashMap::new();
760 for (key, st) in &panel.instance_states {
761 use crate::widgets::WidgetInstanceState as W;
762 let view = match st {
763 W::List {
764 scroll_offset,
765 selected_index,
766 ..
767 } => WidgetInstanceView {
768 selected_index: Some(*selected_index),
769 scroll_offset: Some(*scroll_offset),
770 expanded_keys: Vec::new(),
771 },
772 W::Tree {
773 scroll_offset,
774 selected_index,
775 expanded_keys,
776 } => WidgetInstanceView {
777 selected_index: Some(*selected_index),
778 scroll_offset: Some(*scroll_offset),
779 expanded_keys: expanded_keys.iter().cloned().collect(),
780 },
781 _ => continue,
782 };
783 instances.insert(key.clone(), view);
784 }
785 let hits = panel
786 .hits
787 .iter()
788 .enumerate()
789 .map(|(index, h)| WidgetHitView {
790 index,
791 widget_key: h.widget_key.clone(),
792 widget_kind: h.widget_kind.to_string(),
793 event_type: h.event_type.to_string(),
794 payload: h.payload.clone(),
795 })
796 .collect();
797 out.push(WidgetSurfaceView {
798 kind,
799 plugin: fwp.panel_key.plugin.clone(),
800 panel_id: fwp.panel_key.id,
801 rect: RectView::from(rect),
802 focus_key: panel.focus_key.clone(),
803 spec: panel.spec.clone(),
804 instances,
805 hits,
806 });
807 }
808 out
809 }
810}
811
812#[derive(Debug, Clone, Serialize)]
815#[serde(rename_all = "camelCase")]
816pub struct ContextMenuView {
817 pub kind: &'static str,
819 pub x: u16,
820 pub y: u16,
821 pub highlighted: usize,
822 pub items: Vec<String>,
823}
824
825impl Editor {
826 pub fn context_menu_view(&self) -> Option<ContextMenuView> {
831 let w = self.active_window();
832 let chrome = self.active_chrome();
833 if let Some(m) = &w.file_explorer_context_menu {
834 let (x, y) = m.clamped_position(chrome.last_frame_width, chrome.last_frame_height);
835 return Some(ContextMenuView {
836 kind: "fileExplorer",
837 x,
838 y,
839 highlighted: m.highlighted,
840 items: m.items().iter().map(|i| i.label()).collect(),
841 });
842 }
843 if let Some(m) = &w.new_tab_menu {
844 return Some(ContextMenuView {
845 kind: "newTab",
846 x: m.position.0,
847 y: m.position.1,
848 highlighted: m.highlighted,
849 items: crate::app::types::NewTabMenuItem::all()
850 .iter()
851 .map(|i| i.label())
852 .collect(),
853 });
854 }
855 if let Some(m) = &w.tab_context_menu {
856 return Some(ContextMenuView {
857 kind: "tab",
858 x: m.position.0,
859 y: m.position.1,
860 highlighted: m.highlighted,
861 items: crate::app::types::TabContextMenuItem::all()
862 .iter()
863 .map(|i| i.label())
864 .collect(),
865 });
866 }
867 None
868 }
869}
870
871#[derive(Debug, Clone, Serialize)]
874#[serde(rename_all = "camelCase")]
875pub struct AuxLine {
876 pub text: String,
877 pub selected: bool,
878}
879
880#[derive(Debug, Clone, Serialize)]
886#[serde(rename_all = "camelCase")]
887pub struct AuxModalView {
888 pub kind: &'static str,
889 pub title: String,
890 pub rect: Option<RectView>,
891 pub lines: Vec<AuxLine>,
892 pub footer: Option<String>,
893}
894
895impl Editor {
896 pub fn aux_modals_view(&self) -> Option<AuxModalView> {
900 let w = self.active_window();
906 if let Some(ed) = &w.event_debug {
908 let mut lines: Vec<AuxLine> = ed
909 .history
910 .iter()
911 .map(|r| AuxLine {
912 text: r.description.clone(),
913 selected: false,
914 })
915 .collect();
916 if lines.is_empty() {
917 lines.push(AuxLine {
918 text: rust_i18n::t!("event_debug.no_events").into_owned(),
919 selected: false,
920 });
921 }
922 return Some(AuxModalView {
923 kind: "eventDebug",
924 title: rust_i18n::t!("event_debug.title").into_owned(),
925 rect: None,
926 lines,
927 footer: Some(rust_i18n::t!("event_debug.help_text").into_owned()),
928 });
929 }
930 if let Some(ti) = &w.theme_info_popup {
932 fn color_str(c: ratatui::style::Color) -> String {
933 match c {
934 ratatui::style::Color::Rgb(r, g, b) => format!("#{r:02x}{g:02x}{b:02x}"),
935 other => format!("{other:?}"),
936 }
937 }
938 let info = &ti.info;
939 let mut lines = vec![AuxLine {
940 text: format!("Region: {}", info.region),
941 selected: false,
942 }];
943 if let Some(k) = &info.fg_key {
944 let c = info
945 .fg_color
946 .map(|c| format!(" {}", color_str(c)))
947 .unwrap_or_default();
948 lines.push(AuxLine {
949 text: format!("Foreground: {k}{c}"),
950 selected: false,
951 });
952 }
953 if let Some(k) = &info.bg_key {
954 let c = info
955 .bg_color
956 .map(|c| format!(" {}", color_str(c)))
957 .unwrap_or_default();
958 lines.push(AuxLine {
959 text: format!("Background: {k}{c}"),
960 selected: false,
961 });
962 }
963 if let Some(cat) = &info.syntax_category {
964 lines.push(AuxLine {
965 text: format!("Category: {cat}"),
966 selected: false,
967 });
968 }
969 return Some(AuxModalView {
970 kind: "themeInfo",
971 title: "Theme".to_string(),
972 rect: Some(RectView {
973 x: ti.position.0,
974 y: ti.position.1,
975 w: 0,
976 h: 0,
977 }),
978 lines,
979 footer: None,
980 });
981 }
982 None
983 }
984}
985
986#[derive(Debug, Clone, Serialize)]
989#[serde(rename_all = "camelCase")]
990pub struct KbSearchView {
991 pub active: bool,
992 pub focused: bool,
993 pub mode: &'static str, pub query: String,
995 pub key_display: String,
996}
997
998#[derive(Debug, Clone, Serialize)]
999#[serde(tag = "type", rename_all = "camelCase")]
1000pub enum KbRow {
1001 Section {
1002 name: String,
1003 collapsed: bool,
1004 count: usize,
1005 selected: bool,
1006 },
1007 Binding {
1008 key: String,
1009 action: String,
1010 description: String,
1011 context: String,
1012 source: &'static str, selected: bool,
1014 },
1015}
1016
1017#[derive(Debug, Clone, Serialize)]
1018#[serde(rename_all = "camelCase")]
1019pub struct KbEditDialog {
1020 pub title: String,
1021 pub focus_area: usize, pub key_display: String,
1023 pub key_capturing: bool,
1024 pub action_text: String,
1025 pub action_error: Option<String>,
1026 pub autocomplete: Vec<String>,
1027 pub autocomplete_selected: Option<usize>,
1028 pub context: String,
1029 pub context_options: Vec<String>,
1030 pub conflicts: Vec<String>,
1031 pub save_focused: bool,
1032 pub cancel_focused: bool,
1033}
1034
1035#[derive(Debug, Clone, Serialize)]
1036#[serde(rename_all = "camelCase")]
1037pub struct KbConfirm {
1038 pub buttons: Vec<String>,
1039 pub selected: usize,
1040}
1041
1042#[derive(Debug, Clone, Serialize)]
1043#[serde(rename_all = "camelCase")]
1044pub struct KeybindingEditorView {
1045 pub title: String,
1046 pub config_path: String,
1047 pub keymaps: Vec<String>,
1048 pub search: KbSearchView,
1049 pub context_filter: String,
1050 pub context_filtered: bool,
1051 pub source_filter: String,
1052 pub source_filtered: bool,
1053 pub count: String,
1054 pub has_changes: bool,
1055 pub rows: Vec<KbRow>,
1056 pub selected: usize,
1057 pub scroll_offset: u16,
1058 pub viewport: u16,
1059 pub showing_help: bool,
1060 pub edit_dialog: Option<KbEditDialog>,
1061 pub confirm: Option<KbConfirm>,
1062}
1063
1064impl Editor {
1065 pub fn keybinding_editor_view(&self) -> Option<KeybindingEditorView> {
1070 use crate::app::keybinding_editor::{
1071 BindingSource, ContextFilter, DisplayRow, SearchMode, SourceFilter,
1072 };
1073 let kb = self.keybinding_editor.as_ref()?;
1074
1075 let rows = kb
1076 .display_rows
1077 .iter()
1078 .enumerate()
1079 .map(|(i, dr)| {
1080 let selected = i == kb.selected;
1081 match dr {
1082 DisplayRow::SectionHeader {
1083 plugin_name,
1084 collapsed,
1085 binding_count,
1086 } => KbRow::Section {
1087 name: plugin_name.clone().unwrap_or_else(|| "Builtin".to_string()),
1088 collapsed: *collapsed,
1089 count: *binding_count,
1090 selected,
1091 },
1092 DisplayRow::Binding(bi) => {
1093 let b = &kb.bindings[*bi];
1094 KbRow::Binding {
1095 key: b.key_display.clone(),
1096 action: b.action.clone(),
1097 description: b.action_display.clone(),
1098 context: b.context.clone(),
1099 source: match b.source {
1100 BindingSource::Keymap => "keymap",
1101 BindingSource::Custom => "custom",
1102 BindingSource::Plugin => "plugin",
1103 BindingSource::Unbound => "",
1104 },
1105 selected,
1106 }
1107 }
1108 }
1109 })
1110 .collect();
1111
1112 let (context_filter, context_filtered) = match &kb.context_filter {
1113 ContextFilter::All => ("All".to_string(), false),
1114 ContextFilter::Specific(s) => (s.clone(), true),
1115 };
1116 let (source_filter, source_filtered) = match kb.source_filter {
1117 SourceFilter::All => ("All", false),
1118 SourceFilter::KeymapOnly => ("Keymap", true),
1119 SourceFilter::CustomOnly => ("Custom", true),
1120 SourceFilter::PluginOnly => ("Plugin", true),
1121 };
1122
1123 let edit_dialog = kb.edit_dialog.as_ref().map(|d| KbEditDialog {
1124 title: if d.editing_index.is_some() {
1125 "Edit Binding".to_string()
1126 } else {
1127 "Add Binding".to_string()
1128 },
1129 focus_area: d.focus_area,
1130 key_display: d.key_display.clone(),
1131 key_capturing: d.capturing_special,
1132 action_text: d.action_text.clone(),
1133 action_error: d.action_error.clone(),
1134 autocomplete: if d.autocomplete_visible {
1135 d.autocomplete_suggestions.clone()
1136 } else {
1137 Vec::new()
1138 },
1139 autocomplete_selected: d.autocomplete_selected,
1140 context: d.context.clone(),
1141 context_options: d.context_options.clone(),
1142 conflicts: d.conflicts.clone(),
1143 save_focused: d.focus_area == 3 && d.selected_button == 0,
1144 cancel_focused: d.focus_area == 3 && d.selected_button == 1,
1145 });
1146
1147 let confirm = kb.showing_confirm_dialog.then(|| KbConfirm {
1148 buttons: vec!["Save".into(), "Discard".into(), "Cancel".into()],
1149 selected: kb.confirm_selection,
1150 });
1151
1152 Some(KeybindingEditorView {
1153 title: format!("Keybindings — {}", kb.active_keymap),
1154 config_path: kb.config_file_path.clone(),
1155 keymaps: kb.keymap_names.clone(),
1156 search: KbSearchView {
1157 active: kb.search_active,
1158 focused: kb.search_focused,
1159 mode: match kb.search_mode {
1160 SearchMode::Text => "text",
1161 SearchMode::RecordKey => "recordKey",
1162 },
1163 query: kb.search_query.clone(),
1164 key_display: kb.search_key_display.clone(),
1165 },
1166 context_filter,
1167 context_filtered,
1168 source_filter: source_filter.to_string(),
1169 source_filtered,
1170 count: format!("{} / {}", kb.filtered_indices.len(), kb.bindings.len()),
1171 has_changes: kb.has_changes,
1172 rows,
1173 selected: kb.selected,
1174 scroll_offset: kb.scroll.offset,
1175 viewport: kb.scroll.viewport,
1176 showing_help: kb.showing_help,
1177 edit_dialog,
1178 confirm,
1179 })
1180 }
1181}
1182
1183#[derive(Debug, Clone, Serialize)]
1186#[serde(tag = "kind", rename_all = "camelCase")]
1187pub enum SettingControlView {
1188 Toggle {
1189 checked: bool,
1190 },
1191 Number {
1192 value: i64,
1193 min: Option<i64>,
1194 max: Option<i64>,
1195 },
1196 Dropdown {
1197 selected: usize,
1198 options: Vec<String>,
1199 open: bool,
1200 },
1201 Text {
1202 value: String,
1203 editing: bool,
1204 placeholder: String,
1205 },
1206 TextList {
1207 items: Vec<String>,
1208 focused: Option<usize>,
1209 },
1210 #[serde(rename_all = "camelCase")]
1214 DualList {
1215 included: Vec<String>,
1216 available: Vec<String>,
1217 included_cursor: usize,
1221 available_cursor: usize,
1222 active_column: &'static str, },
1224 Map {
1225 entries: Vec<MapEntryView>,
1226 },
1227 ObjectArray {
1228 entries: Vec<String>,
1229 },
1230 Json {
1231 value: String,
1232 },
1233 Complex {
1234 type_name: String,
1235 },
1236}
1237
1238#[derive(Debug, Clone, Serialize)]
1239#[serde(rename_all = "camelCase")]
1240pub struct MapEntryView {
1241 pub key: String,
1242 pub display: String,
1243}
1244
1245#[derive(Debug, Clone, Serialize)]
1246#[serde(rename_all = "camelCase")]
1247pub struct SettingItemView {
1248 pub index: usize,
1249 pub path: String,
1250 pub name: String,
1251 pub description: Option<String>,
1252 pub section: Option<String>,
1253 pub section_start: bool,
1254 pub modified: bool,
1255 pub read_only: bool,
1256 pub nullable: bool,
1257 pub is_null: bool,
1258 pub selected: bool,
1259 pub control: SettingControlView,
1260}
1261
1262#[derive(Debug, Clone, Serialize)]
1263#[serde(rename_all = "camelCase")]
1264pub struct SettingsCategoryView {
1265 pub index: usize,
1266 pub name: String,
1267 pub selected: bool,
1268 pub expandable: bool,
1269 pub expanded: bool,
1270 pub sections: Vec<String>,
1271}
1272
1273#[derive(Debug, Clone, Serialize)]
1274#[serde(rename_all = "camelCase")]
1275pub struct SettingsSearchResultView {
1276 pub name: String,
1277 pub category: String,
1278}
1279
1280#[derive(Debug, Clone, Serialize)]
1281#[serde(rename_all = "camelCase")]
1282pub struct EntryDialogView {
1283 pub title: String,
1284 pub is_new: bool,
1285 pub items: Vec<SettingItemView>,
1286 pub selected_item: usize,
1287 pub focus_on_buttons: bool,
1288 pub focused_button: usize,
1289 pub no_delete: bool,
1290}
1291
1292#[derive(Debug, Clone, Serialize)]
1293#[serde(rename_all = "camelCase")]
1294pub struct SettingsView {
1295 pub title: String,
1296 pub focus: &'static str, pub target_layer: String,
1298 pub categories: Vec<SettingsCategoryView>,
1299 pub items: Vec<SettingItemView>,
1300 pub footer_buttons: Vec<String>,
1301 pub footer_selected: usize,
1302 pub search_active: bool,
1303 pub search_query: String,
1304 pub search_results: Vec<SettingsSearchResultView>,
1305 pub search_selected: usize,
1306 pub entry_dialog: Option<EntryDialogView>,
1307 pub showing_help: bool,
1308 pub showing_confirm: bool,
1309 pub showing_reset: bool,
1310}
1311
1312fn setting_control_view(c: &crate::view::settings::items::SettingControl) -> SettingControlView {
1313 use crate::view::settings::items::SettingControl as C;
1314 match c {
1315 C::Toggle(s) => SettingControlView::Toggle { checked: s.checked },
1316 C::Number(s) => SettingControlView::Number {
1317 value: s.value,
1318 min: s.min,
1319 max: s.max,
1320 },
1321 C::Dropdown(s) => SettingControlView::Dropdown {
1322 selected: s.selected,
1323 options: s.options.clone(),
1324 open: s.open,
1325 },
1326 C::Text(s) => SettingControlView::Text {
1327 value: s.value.clone(),
1328 editing: s.editing,
1329 placeholder: s.placeholder.clone(),
1330 },
1331 C::TextList(s) => SettingControlView::TextList {
1332 items: s.items.clone(),
1333 focused: s.focused_item,
1334 },
1335 C::DualList(s) => SettingControlView::DualList {
1336 included: s
1340 .included_items()
1341 .iter()
1342 .map(|(_, n)| n.to_string())
1343 .collect(),
1344 available: s.available_items().iter().map(|(_, n)| n.clone()).collect(),
1345 included_cursor: s.included_cursor,
1346 available_cursor: s.available_cursor,
1347 active_column: match s.active_column {
1348 crate::view::controls::DualListColumn::Included => "included",
1349 crate::view::controls::DualListColumn::Available => "available",
1350 },
1351 },
1352 C::Map(s) => SettingControlView::Map {
1353 entries: s
1354 .entries
1355 .iter()
1356 .map(|(k, v)| MapEntryView {
1357 key: k.clone(),
1358 display: v
1359 .as_str()
1360 .map(|x| x.to_string())
1361 .unwrap_or_else(|| v.to_string()),
1362 })
1363 .collect(),
1364 },
1365 C::ObjectArray(s) => SettingControlView::ObjectArray {
1366 entries: s
1367 .bindings
1368 .iter()
1369 .map(|v| {
1370 s.display_field
1371 .as_ref()
1372 .and_then(|f| v.pointer(f))
1373 .and_then(|x| x.as_str())
1374 .map(|x| x.to_string())
1375 .unwrap_or_else(|| v.to_string())
1376 })
1377 .collect(),
1378 },
1379 C::Json(s) => SettingControlView::Json { value: s.value() },
1380 C::Complex { type_name } => SettingControlView::Complex {
1381 type_name: type_name.clone(),
1382 },
1383 }
1384}
1385
1386fn setting_item_view(
1387 item: &crate::view::settings::items::SettingItem,
1388 i: usize,
1389 selected: bool,
1390) -> SettingItemView {
1391 SettingItemView {
1392 index: i,
1393 path: item.path.clone(),
1394 name: item.name.clone(),
1395 description: item.description.clone(),
1396 section: item.section.clone(),
1397 section_start: item.is_section_start,
1398 modified: item.modified,
1399 read_only: item.read_only,
1400 nullable: item.nullable,
1401 is_null: item.is_null,
1402 selected,
1403 control: setting_control_view(&item.control),
1404 }
1405}
1406
1407impl Editor {
1408 pub fn settings_view(&self) -> Option<SettingsView> {
1413 use crate::view::settings::state::FocusPanel;
1414 let st = self.settings_state.as_ref()?;
1415 if !st.visible {
1416 return None;
1417 }
1418
1419 let categories = st
1420 .pages
1421 .iter()
1422 .enumerate()
1423 .map(|(i, p)| SettingsCategoryView {
1424 index: i,
1425 name: p.name.clone(),
1426 selected: i == st.selected_category,
1427 expandable: !p.subpages.is_empty() || p.sections.len() > 1,
1428 expanded: st.expanded_categories.contains(&i),
1429 sections: p.sections.iter().map(|s| s.name.clone()).collect(),
1430 })
1431 .collect();
1432
1433 let items = st
1434 .pages
1435 .get(st.selected_category)
1436 .map(|p| {
1437 p.items
1438 .iter()
1439 .enumerate()
1440 .map(|(i, it)| setting_item_view(it, i, i == st.selected_item))
1441 .collect()
1442 })
1443 .unwrap_or_default();
1444
1445 let entry_dialog = st.entry_dialog_stack.last().map(|d| EntryDialogView {
1446 title: d.title.clone(),
1447 is_new: d.is_new,
1448 items: d
1449 .items
1450 .iter()
1451 .enumerate()
1452 .map(|(i, it)| setting_item_view(it, i, i == d.selected_item))
1453 .collect(),
1454 selected_item: d.selected_item,
1455 focus_on_buttons: d.focus_on_buttons,
1456 focused_button: d.focused_button,
1457 no_delete: d.no_delete,
1458 });
1459
1460 Some(SettingsView {
1461 title: "Settings".to_string(),
1462 focus: match st.focus.current() {
1463 Some(FocusPanel::Settings) => "settings",
1464 Some(FocusPanel::Footer) => "footer",
1465 _ => "categories",
1466 },
1467 target_layer: format!("{:?}", st.target_layer),
1468 categories,
1469 items,
1470 footer_buttons: vec![
1471 format!("{:?}", st.target_layer),
1472 "Reset".into(),
1473 "Save".into(),
1474 "Cancel".into(),
1475 ],
1476 footer_selected: st.footer_button_index,
1477 search_active: st.search_active,
1478 search_query: st.search_query.clone(),
1479 search_results: st
1480 .search_results
1481 .iter()
1482 .map(|r| SettingsSearchResultView {
1483 name: r.item.name.clone(),
1484 category: r.breadcrumb.clone(),
1485 })
1486 .collect(),
1487 search_selected: st.selected_search_result,
1488 entry_dialog,
1489 showing_help: st.showing_help,
1490 showing_confirm: st.showing_confirm_dialog,
1491 showing_reset: st.showing_reset_dialog,
1492 })
1493 }
1494}