use crate::app::Editor;
use fresh_core::LeafId;
use ratatui::layout::Rect;
use serde::Serialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, Serialize)]
pub struct RectView {
pub x: u16,
pub y: u16,
pub w: u16,
pub h: u16,
}
impl From<Rect> for RectView {
fn from(r: Rect) -> Self {
RectView {
x: r.x,
y: r.y,
w: r.width,
h: r.height,
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum MenuItemView {
Action {
label: String,
action: String,
#[serde(skip_serializing_if = "HashMap::is_empty")]
args: HashMap<String, serde_json::Value>,
accel: Option<String>,
enabled: bool,
checked: Option<bool>,
},
Sep,
Submenu {
label: String,
items: Vec<MenuItemView>,
},
Label {
label: String,
},
}
#[derive(Debug, Clone, Serialize)]
pub struct MenuEntry {
pub label: String,
pub visible: bool,
pub x: Option<u16>,
pub w: Option<u16>,
pub items: Vec<MenuItemView>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DropdownView {
pub rect: Option<RectView>,
pub items: Vec<ItemArea>,
pub submenus: Vec<SubmenuArea>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ItemArea {
pub index: usize,
pub rect: RectView,
}
#[derive(Debug, Clone, Serialize)]
pub struct SubmenuArea {
pub depth: usize,
pub index: usize,
pub rect: RectView,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MenuView {
pub menus: Vec<MenuEntry>,
pub menu_open: Option<usize>,
pub menu_highlight: Option<usize>,
pub submenu_path: Vec<usize>,
pub dropdown: Option<DropdownView>,
}
fn item_view(editor: &Editor, item: &fresh_core::menu::MenuItem) -> MenuItemView {
use fresh_core::menu::MenuItem::*;
match item {
Separator { .. } => MenuItemView::Sep,
Action {
label,
action,
args,
when: _,
checkbox,
} => MenuItemView::Action {
label: label.clone(),
action: action.clone(),
args: args.clone(),
accel: editor.accelerator_for(action),
enabled: crate::view::ui::menu::is_menu_item_enabled(
item,
&editor.menu_state().context,
),
checked: checkbox.as_ref().map(|_| {
crate::view::ui::menu::is_checkbox_checked(checkbox, &editor.menu_state().context)
}),
},
Submenu { label, items } => MenuItemView::Submenu {
label: label.clone(),
items: items.iter().map(|i| item_view(editor, i)).collect(),
},
DynamicSubmenu { label, .. } => MenuItemView::Submenu {
label: label.clone(),
items: Vec::new(),
},
Label { info } => MenuItemView::Label {
label: info.clone(),
},
}
}
fn union_rect(rects: &[Rect]) -> Option<Rect> {
let mut acc: Option<Rect> = None;
for r in rects {
acc = Some(match acc {
None => *r,
Some(a) => {
let x0 = a.x.min(r.x);
let y0 = a.y.min(r.y);
let x1 = (a.x + a.width).max(r.x + r.width);
let y1 = (a.y + a.height).max(r.y + r.height);
Rect::new(x0, y0, x1 - x0, y1 - y0)
}
});
}
acc
}
impl Editor {
pub fn menu_view(&self) -> MenuView {
let chrome = self.active_chrome();
let menu_areas: HashMap<usize, Rect> = chrome
.menu_layout
.as_ref()
.map(|m| m.menu_areas.iter().cloned().collect())
.unwrap_or_default();
let menus: Vec<MenuEntry> = self
.all_menus_expanded()
.iter()
.enumerate()
.map(|(i, m)| MenuEntry {
label: m.label.clone(),
visible: crate::view::ui::menu::is_menu_visible(m, &self.menu_state().context),
x: menu_areas.get(&i).map(|r| r.x),
w: menu_areas.get(&i).map(|r| r.width),
items: m.items.iter().map(|it| item_view(self, it)).collect(),
})
.collect();
let dropdown = chrome.menu_layout.as_ref().and_then(|ml| {
if ml.item_areas.is_empty() {
return None;
}
let rects: Vec<Rect> = ml.item_areas.iter().map(|(_, r)| *r).collect();
Some(DropdownView {
rect: union_rect(&rects).map(RectView::from),
items: ml
.item_areas
.iter()
.map(|(index, r)| ItemArea {
index: *index,
rect: RectView::from(*r),
})
.collect(),
submenus: ml
.submenu_areas
.iter()
.map(|(depth, index, r)| SubmenuArea {
depth: *depth,
index: *index,
rect: RectView::from(*r),
})
.collect(),
})
});
let ms = self.menu_state();
MenuView {
menus,
menu_open: ms.active_menu,
menu_highlight: ms.highlighted_item,
submenu_path: ms.submenu_path.clone(),
dropdown,
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TabView {
pub buffer_id: Option<usize>,
pub label: String,
pub active: bool,
pub modified: bool,
pub rect: RectView,
pub close_rect: RectView,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct TabBarView {
pub bar: Option<RectView>,
pub tabs: Vec<TabView>,
}
#[derive(Debug, Clone, Serialize)]
pub struct StatusSegment {
pub name: &'static str,
pub key: Option<String>,
pub text: String,
pub x: u16,
pub w: u16,
pub side: &'static str,
}
#[derive(Debug, Clone, Serialize)]
pub struct StatusView {
pub rect: RectView,
pub segments: Vec<StatusSegment>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SuggestionView {
pub text: String,
pub description: Option<String>,
pub keybinding: Option<String>,
pub disabled: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PaletteView {
pub query: String,
pub message: String,
pub prompt_type: &'static str,
pub overlay: bool,
pub title: String,
pub status: String,
pub selected: Option<usize>,
pub scroll_start: usize,
pub visible_count: usize,
pub total: usize,
pub outer_rect: Option<RectView>,
pub list_rect: Option<RectView>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview_rect: Option<RectView>,
pub suggestions: Vec<SuggestionView>,
#[serde(skip_serializing_if = "Option::is_none")]
pub toolbar: Option<fresh_core::api::WidgetSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub toolbar_focus: Option<String>,
}
fn prompt_type_tag(t: &crate::view::prompt::PromptType) -> &'static str {
use crate::view::prompt::PromptType::*;
match t {
QuickOpen => "quickopen",
LiveGrep => "livegrep",
Search | ReplaceSearch | QueryReplaceSearch => "search",
OpenFile | OpenFileWithEncoding { .. } => "openfile",
SaveFileAs => "saveas",
GotoLine | GotoByteOffset => "goto",
_ => "input",
}
}
impl Editor {
pub fn tab_bar_view(&self, leaf: LeafId) -> TabBarView {
let active = self.active_buffer();
let layout = self.active_layout();
match layout.tab_layouts.get(&leaf) {
None => TabBarView::default(),
Some(tl) => TabBarView {
bar: Some(RectView::from(tl.bar_area)),
tabs: tl
.tabs
.iter()
.map(|tab| {
let bid = tab.target.as_buffer();
TabView {
buffer_id: bid.map(|b| b.0),
label: bid
.and_then(|b| self.buffer_display_name(b))
.unwrap_or_else(|| "untitled".into()),
active: bid == Some(active),
modified: bid.map(|b| self.buffer_is_modified(b)).unwrap_or(false),
rect: RectView::from(tab.tab_area),
close_rect: RectView::from(tab.close_area),
}
})
.collect(),
},
}
}
pub fn status_view(&self) -> Option<StatusView> {
let chrome = self.active_chrome();
let (sy, sx, sw) = chrome.status_bar.area?;
let segments: Vec<StatusSegment> = chrome
.status_bar
.segments
.iter()
.filter(|s| !s.text.trim().is_empty())
.map(|s| StatusSegment {
name: s.name,
key: s.key.clone(),
text: s.text.trim().to_string(),
x: s.x,
w: s.w,
side: s.side,
})
.collect();
Some(StatusView {
rect: RectView {
x: sx,
y: sy,
w: sw,
h: 1,
},
segments,
})
}
pub fn palette_view(&self) -> Option<PaletteView> {
let chrome = self.active_chrome();
let sugg_outer = chrome.suggestions_outer_area;
let sugg_area = chrome.suggestions_area;
let prompt_results = chrome.prompt_results_area;
let p = self.active_window().prompt.as_ref()?;
if p.suggestions.is_empty() && !p.overlay {
return None;
}
let (scroll_start, visible, total) = sugg_area.map(|(_, s, v, t)| (s, v, t)).unwrap_or((
p.scroll_offset,
p.suggestions.len(),
p.suggestions.len(),
));
Some(PaletteView {
query: p.input.clone(),
message: p.message.clone(),
prompt_type: prompt_type_tag(&p.prompt_type),
overlay: p.overlay,
title: p.title.iter().map(|t| t.text.as_str()).collect(),
status: p.status.clone(),
selected: p.selected_suggestion,
scroll_start,
visible_count: visible,
total,
outer_rect: sugg_outer.map(RectView::from),
list_rect: sugg_area
.map(|(r, _, _, _)| r)
.or(prompt_results)
.map(RectView::from),
preview_rect: chrome.prompt_preview_area.and_then(|r| {
(r.width > 1 && r.height > 0).then(|| {
RectView::from(Rect::new(
r.x.saturating_add(1),
r.y,
r.width.saturating_sub(1),
r.height,
))
})
}),
suggestions: p
.suggestions
.iter()
.map(|s| SuggestionView {
text: s.text.clone(),
description: s.description.clone(),
keybinding: s.keybinding.clone(),
disabled: s.disabled,
})
.collect(),
toolbar: p.toolbar_widget.clone(),
toolbar_focus: p.toolbar_focus.clone(),
})
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PopupItemView {
pub text: String,
pub detail: Option<String>,
pub icon: Option<String>,
pub disabled: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum PopupContentView {
List {
items: Vec<PopupItemView>,
selected: usize,
},
Lines {
lines: Vec<String>,
},
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ScenePopup {
pub kind: &'static str,
pub title: Option<String>,
pub description: Option<String>,
pub rect: RectView,
pub content_rect: RectView,
pub scroll_offset: usize,
pub content: PopupContentView,
}
fn project_popup(
p: &crate::view::popup::Popup,
outer: Rect,
inner: Rect,
scroll: usize,
) -> ScenePopup {
use crate::view::popup::{PopupContent, PopupKind};
let kind = match p.kind {
PopupKind::Completion => "completion",
PopupKind::Hover => "hover",
PopupKind::Action => "action",
PopupKind::List => "list",
PopupKind::Text => "text",
};
let content = match &p.content {
PopupContent::List { items, selected } => PopupContentView::List {
items: items
.iter()
.map(|i| PopupItemView {
text: i.text.clone(),
detail: i.detail.clone(),
icon: i.icon.clone(),
disabled: i.disabled,
})
.collect(),
selected: *selected,
},
PopupContent::Text(lines) | PopupContent::Custom(lines) => PopupContentView::Lines {
lines: lines.clone(),
},
PopupContent::Markdown(styled) => PopupContentView::Lines {
lines: styled
.iter()
.map(|l| l.spans.iter().map(|s| s.text.as_str()).collect::<String>())
.collect(),
},
};
ScenePopup {
kind,
title: p.title.clone(),
description: p.description.clone(),
rect: RectView::from(outer),
content_rect: RectView::from(inner),
scroll_offset: scroll,
content,
}
}
impl Editor {
pub fn popups_view(&self) -> Vec<ScenePopup> {
let chrome = self.active_chrome();
let mut out = Vec::new();
let locals = self.active_state().popups.all();
for (idx, outer, inner, scroll, _n, _sb, _t) in &chrome.popup_areas {
if let Some(p) = locals.get(*idx) {
out.push(project_popup(p, *outer, *inner, *scroll));
}
}
let globals = self.global_popups.all();
for (idx, outer, inner, scroll, _n) in &chrome.global_popup_areas {
if let Some(p) = globals.get(*idx) {
out.push(project_popup(p, *outer, *inner, *scroll));
}
}
out
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FileRow {
pub name: String,
pub depth: usize,
pub is_dir: bool,
pub expanded: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FileExplorerView {
pub rect: RectView,
pub title: String,
pub scroll_offset: usize,
pub viewport_height: usize,
pub selected: Option<usize>,
pub rows: Vec<FileRow>,
}
impl Editor {
pub fn file_explorer_view(&self) -> Option<FileExplorerView> {
let rect = self.active_layout().file_explorer_area?;
let view = self.file_explorer()?;
let tree = view.tree();
let rows = view
.get_display_nodes()
.into_iter()
.filter_map(|(id, indent)| {
tree.get_node(id).map(|n| FileRow {
name: n.entry.name.clone(),
depth: indent,
is_dir: n.is_dir(),
expanded: n.is_expanded(),
})
})
.collect();
let title = tree
.get_node(tree.root_id())
.map(|n| n.entry.name.clone())
.unwrap_or_default();
Some(FileExplorerView {
rect: RectView::from(rect),
title,
scroll_offset: view.get_scroll_offset(),
viewport_height: view.viewport_height,
selected: view.get_selected_index(),
rows,
})
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TrustOptionView {
pub label: String,
pub description: String,
pub selected: bool,
pub data: &'static str,
pub rect: RectView,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TrustDialogView {
pub dialog: RectView,
pub title: String,
pub path: String,
pub triggers: String,
pub cancellable: bool,
pub options: Vec<TrustOptionView>,
pub ok: RectView,
pub ok_label: String,
pub quit: RectView,
pub quit_label: String,
}
impl Editor {
pub fn trust_dialog_view(&self) -> Option<TrustDialogView> {
let layout = self.active_chrome().workspace_trust_dialog.clone()?;
let selected = self.current_workspace_trust_selection();
let data = ["trusted", "restricted", "blocked"];
let options = crate::view::workspace_trust_dialog::options()
.into_iter()
.enumerate()
.map(|(i, o)| TrustOptionView {
label: o.label,
description: o.description,
selected: i == selected,
data: data.get(i).copied().unwrap_or("restricted"),
rect: RectView::from(layout.radios[i]),
})
.collect();
let quit_label = if self.workspace_trust_cancellable() {
rust_i18n::t!("trust.dialog.btn_cancel").into_owned()
} else {
rust_i18n::t!("trust.dialog.btn_quit").into_owned()
};
Some(TrustDialogView {
dialog: RectView::from(layout.dialog),
title: rust_i18n::t!("trust.dialog.security_warning").into_owned(),
path: self.working_dir().display().to_string(),
triggers: self.workspace_trust_markers().join(", "),
cancellable: self.workspace_trust_cancellable(),
options,
ok: RectView::from(layout.ok),
ok_label: rust_i18n::t!("trust.dialog.btn_ok").into_owned(),
quit: RectView::from(layout.quit),
quit_label,
})
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WidgetHitView {
pub index: usize,
pub widget_key: String,
pub widget_kind: String,
pub event_type: String,
pub payload: serde_json::Value,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WidgetInstanceView {
pub selected_index: Option<i32>,
pub scroll_offset: Option<u32>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub expanded_keys: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WidgetSurfaceView {
pub kind: &'static str,
pub plugin: String,
pub panel_id: u64,
pub rect: RectView,
pub focus_key: String,
pub spec: fresh_core::api::WidgetSpec,
pub instances: HashMap<String, WidgetInstanceView>,
pub hits: Vec<WidgetHitView>,
}
impl Editor {
pub fn widgets_view(&self) -> Vec<WidgetSurfaceView> {
let mut out = Vec::new();
for (kind, slot) in [
("dock", self.dock.as_ref()),
("floatingModal", self.floating_widget_panel.as_ref()),
] {
let Some(fwp) = slot else { continue };
let Some(rect) = fwp.last_inner_rect else {
continue;
};
let Some(panel) = self.widget_registry.get(&fwp.panel_key) else {
continue;
};
let mut instances = HashMap::new();
for (key, st) in &panel.instance_states {
use crate::widgets::WidgetInstanceState as W;
let view = match st {
W::List {
scroll_offset,
selected_index,
..
} => WidgetInstanceView {
selected_index: Some(*selected_index),
scroll_offset: Some(*scroll_offset),
expanded_keys: Vec::new(),
},
W::Tree {
scroll_offset,
selected_index,
expanded_keys,
} => WidgetInstanceView {
selected_index: Some(*selected_index),
scroll_offset: Some(*scroll_offset),
expanded_keys: expanded_keys.iter().cloned().collect(),
},
_ => continue,
};
instances.insert(key.clone(), view);
}
let hits = panel
.hits
.iter()
.enumerate()
.map(|(index, h)| WidgetHitView {
index,
widget_key: h.widget_key.clone(),
widget_kind: h.widget_kind.to_string(),
event_type: h.event_type.to_string(),
payload: h.payload.clone(),
})
.collect();
out.push(WidgetSurfaceView {
kind,
plugin: fwp.panel_key.plugin.clone(),
panel_id: fwp.panel_key.id,
rect: RectView::from(rect),
focus_key: panel.focus_key.clone(),
spec: panel.spec.clone(),
instances,
hits,
});
}
out
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ContextMenuView {
pub kind: &'static str,
pub x: u16,
pub y: u16,
pub highlighted: usize,
pub items: Vec<String>,
}
impl Editor {
pub fn context_menu_view(&self) -> Option<ContextMenuView> {
let w = self.active_window();
let chrome = self.active_chrome();
if let Some(m) = &w.file_explorer_context_menu {
let (x, y) = m.clamped_position(chrome.last_frame.width, chrome.last_frame.height);
return Some(ContextMenuView {
kind: "fileExplorer",
x,
y,
highlighted: m.highlighted,
items: m.items().iter().map(|i| i.label()).collect(),
});
}
if let Some(m) = &w.new_tab_menu {
return Some(ContextMenuView {
kind: "newTab",
x: m.position.0,
y: m.position.1,
highlighted: m.highlighted,
items: crate::app::types::NewTabMenuItem::all()
.iter()
.map(|i| i.label())
.collect(),
});
}
if let Some(m) = &w.tab_context_menu {
return Some(ContextMenuView {
kind: "tab",
x: m.position.0,
y: m.position.1,
highlighted: m.highlighted,
items: crate::app::types::TabContextMenuItem::all()
.iter()
.map(|i| i.label())
.collect(),
});
}
None
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuxLine {
pub text: String,
pub selected: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuxModalView {
pub kind: &'static str,
pub title: String,
pub rect: Option<RectView>,
pub lines: Vec<AuxLine>,
pub footer: Option<String>,
}
impl Editor {
pub fn aux_modals_view(&self) -> Option<AuxModalView> {
let w = self.active_window();
if let Some(ed) = &w.event_debug {
let mut lines: Vec<AuxLine> = ed
.history
.iter()
.map(|r| AuxLine {
text: r.description.clone(),
selected: false,
})
.collect();
if lines.is_empty() {
lines.push(AuxLine {
text: rust_i18n::t!("event_debug.no_events").into_owned(),
selected: false,
});
}
return Some(AuxModalView {
kind: "eventDebug",
title: rust_i18n::t!("event_debug.title").into_owned(),
rect: None,
lines,
footer: Some(rust_i18n::t!("event_debug.help_text").into_owned()),
});
}
if let Some(ti) = &w.theme_info_popup {
fn color_str(c: ratatui::style::Color) -> String {
match c {
ratatui::style::Color::Rgb(r, g, b) => format!("#{r:02x}{g:02x}{b:02x}"),
other => format!("{other:?}"),
}
}
let info = &ti.info;
let mut lines = vec![AuxLine {
text: format!("Region: {}", info.region),
selected: false,
}];
if let Some(k) = &info.fg_key {
let c = info
.fg_color
.map(|c| format!(" {}", color_str(c)))
.unwrap_or_default();
lines.push(AuxLine {
text: format!("Foreground: {k}{c}"),
selected: false,
});
}
if let Some(k) = &info.bg_key {
let c = info
.bg_color
.map(|c| format!(" {}", color_str(c)))
.unwrap_or_default();
lines.push(AuxLine {
text: format!("Background: {k}{c}"),
selected: false,
});
}
if let Some(cat) = &info.syntax_category {
lines.push(AuxLine {
text: format!("Category: {cat}"),
selected: false,
});
}
return Some(AuxModalView {
kind: "themeInfo",
title: "Theme".to_string(),
rect: Some(RectView {
x: ti.position.0,
y: ti.position.1,
w: 0,
h: 0,
}),
lines,
footer: None,
});
}
None
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct KbSearchView {
pub active: bool,
pub focused: bool,
pub mode: &'static str, pub query: String,
pub key_display: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum KbRow {
Section {
name: String,
collapsed: bool,
count: usize,
selected: bool,
},
Binding {
key: String,
action: String,
description: String,
context: String,
source: &'static str, selected: bool,
},
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct KbEditDialog {
pub title: String,
pub focus_area: usize, pub key_display: String,
pub key_capturing: bool,
pub action_text: String,
pub action_error: Option<String>,
pub autocomplete: Vec<String>,
pub autocomplete_selected: Option<usize>,
pub context: String,
pub context_options: Vec<String>,
pub conflicts: Vec<String>,
pub save_focused: bool,
pub cancel_focused: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct KbConfirm {
pub buttons: Vec<String>,
pub selected: usize,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct KeybindingEditorView {
pub title: String,
pub config_path: String,
pub keymaps: Vec<String>,
pub search: KbSearchView,
pub context_filter: String,
pub context_filtered: bool,
pub source_filter: String,
pub source_filtered: bool,
pub count: String,
pub has_changes: bool,
pub rows: Vec<KbRow>,
pub selected: usize,
pub scroll_offset: u16,
pub viewport: u16,
pub showing_help: bool,
pub edit_dialog: Option<KbEditDialog>,
pub confirm: Option<KbConfirm>,
}
impl Editor {
pub fn keybinding_editor_view(&self) -> Option<KeybindingEditorView> {
use crate::app::keybinding_editor::{
BindingSource, ContextFilter, DisplayRow, SearchMode, SourceFilter,
};
let kb = self.keybinding_editor.as_ref()?;
let rows = kb
.display_rows
.iter()
.enumerate()
.map(|(i, dr)| {
let selected = i == kb.selected;
match dr {
DisplayRow::SectionHeader {
plugin_name,
collapsed,
binding_count,
} => KbRow::Section {
name: plugin_name.clone().unwrap_or_else(|| "Builtin".to_string()),
collapsed: *collapsed,
count: *binding_count,
selected,
},
DisplayRow::Binding(bi) => {
let b = &kb.bindings[*bi];
KbRow::Binding {
key: b.key_display.clone(),
action: b.action.clone(),
description: b.action_display.clone(),
context: b.context.clone(),
source: match b.source {
BindingSource::Keymap => "keymap",
BindingSource::Custom => "custom",
BindingSource::Plugin => "plugin",
BindingSource::Unbound => "",
},
selected,
}
}
}
})
.collect();
let (context_filter, context_filtered) = match &kb.context_filter {
ContextFilter::All => ("All".to_string(), false),
ContextFilter::Specific(s) => (s.clone(), true),
};
let (source_filter, source_filtered) = match kb.source_filter {
SourceFilter::All => ("All", false),
SourceFilter::KeymapOnly => ("Keymap", true),
SourceFilter::CustomOnly => ("Custom", true),
SourceFilter::PluginOnly => ("Plugin", true),
};
let edit_dialog = kb.edit_dialog.as_ref().map(|d| KbEditDialog {
title: if d.editing_index.is_some() {
"Edit Binding".to_string()
} else {
"Add Binding".to_string()
},
focus_area: d.focus_area,
key_display: d.key_display.clone(),
key_capturing: d.capturing_special,
action_text: d.action_text.clone(),
action_error: d.action_error.clone(),
autocomplete: if d.autocomplete_visible {
d.autocomplete_suggestions.clone()
} else {
Vec::new()
},
autocomplete_selected: d.autocomplete_selected,
context: d.context.clone(),
context_options: d.context_options.clone(),
conflicts: d.conflicts.clone(),
save_focused: d.focus_area == 3 && d.selected_button == 0,
cancel_focused: d.focus_area == 3 && d.selected_button == 1,
});
let confirm = kb.showing_confirm_dialog.then(|| KbConfirm {
buttons: vec!["Save".into(), "Discard".into(), "Cancel".into()],
selected: kb.confirm_selection,
});
Some(KeybindingEditorView {
title: format!("Keybindings — {}", kb.active_keymap),
config_path: kb.config_file_path.clone(),
keymaps: kb.keymap_names.clone(),
search: KbSearchView {
active: kb.search_active,
focused: kb.search_focused,
mode: match kb.search_mode {
SearchMode::Text => "text",
SearchMode::RecordKey => "recordKey",
},
query: kb.search_query.clone(),
key_display: kb.search_key_display.clone(),
},
context_filter,
context_filtered,
source_filter: source_filter.to_string(),
source_filtered,
count: format!("{} / {}", kb.filtered_indices.len(), kb.bindings.len()),
has_changes: kb.has_changes,
rows,
selected: kb.selected,
scroll_offset: kb.scroll.offset,
viewport: kb.scroll.viewport,
showing_help: kb.showing_help,
edit_dialog,
confirm,
})
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum SettingControlView {
Toggle {
checked: bool,
},
Number {
value: i64,
min: Option<i64>,
max: Option<i64>,
},
Dropdown {
selected: usize,
options: Vec<String>,
open: bool,
},
Text {
value: String,
editing: bool,
placeholder: String,
},
TextList {
items: Vec<String>,
focused: Option<usize>,
},
#[serde(rename_all = "camelCase")]
DualList {
included: Vec<String>,
available: Vec<String>,
included_cursor: usize,
available_cursor: usize,
active_column: &'static str, },
Map {
entries: Vec<MapEntryView>,
},
ObjectArray {
entries: Vec<String>,
},
Json {
value: String,
},
Complex {
type_name: String,
},
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MapEntryView {
pub key: String,
pub display: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SettingItemView {
pub index: usize,
pub path: String,
pub name: String,
pub description: Option<String>,
pub section: Option<String>,
pub section_start: bool,
pub modified: bool,
pub read_only: bool,
pub nullable: bool,
pub is_null: bool,
pub selected: bool,
pub control: SettingControlView,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SettingsCategoryView {
pub index: usize,
pub name: String,
pub selected: bool,
pub expandable: bool,
pub expanded: bool,
pub sections: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SettingsSearchResultView {
pub name: String,
pub category: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EntryDialogView {
pub title: String,
pub is_new: bool,
pub items: Vec<SettingItemView>,
pub selected_item: usize,
pub focus_on_buttons: bool,
pub focused_button: usize,
pub no_delete: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SettingsView {
pub title: String,
pub focus: &'static str, pub target_layer: String,
pub categories: Vec<SettingsCategoryView>,
pub items: Vec<SettingItemView>,
pub footer_buttons: Vec<String>,
pub footer_selected: usize,
pub search_active: bool,
pub search_query: String,
pub search_results: Vec<SettingsSearchResultView>,
pub search_selected: usize,
pub entry_dialog: Option<EntryDialogView>,
pub showing_help: bool,
pub showing_confirm: bool,
pub showing_reset: bool,
}
fn setting_control_view(c: &crate::view::settings::items::SettingControl) -> SettingControlView {
use crate::view::settings::items::SettingControl as C;
match c {
C::Toggle(s) => SettingControlView::Toggle { checked: s.checked },
C::Number(s) => SettingControlView::Number {
value: s.value,
min: s.min,
max: s.max,
},
C::Dropdown(s) => SettingControlView::Dropdown {
selected: s.selected,
options: s.options.clone(),
open: s.open,
},
C::Text(s) => SettingControlView::Text {
value: s.value.clone(),
editing: s.editing,
placeholder: s.placeholder.clone(),
},
C::TextList(s) => SettingControlView::TextList {
items: s.items.clone(),
focused: s.focused_item,
},
C::DualList(s) => SettingControlView::DualList {
included: s
.included_items()
.iter()
.map(|(_, n)| n.to_string())
.collect(),
available: s.available_items().iter().map(|(_, n)| n.clone()).collect(),
included_cursor: s.included_cursor,
available_cursor: s.available_cursor,
active_column: match s.active_column {
crate::view::controls::DualListColumn::Included => "included",
crate::view::controls::DualListColumn::Available => "available",
},
},
C::Map(s) => SettingControlView::Map {
entries: s
.entries
.iter()
.map(|(k, v)| MapEntryView {
key: k.clone(),
display: v
.as_str()
.map(|x| x.to_string())
.unwrap_or_else(|| v.to_string()),
})
.collect(),
},
C::ObjectArray(s) => SettingControlView::ObjectArray {
entries: s
.bindings
.iter()
.map(|v| {
s.display_field
.as_ref()
.and_then(|f| v.pointer(f))
.and_then(|x| x.as_str())
.map(|x| x.to_string())
.unwrap_or_else(|| v.to_string())
})
.collect(),
},
C::Json(s) => SettingControlView::Json { value: s.value() },
C::Complex { type_name } => SettingControlView::Complex {
type_name: type_name.clone(),
},
}
}
fn setting_item_view(
item: &crate::view::settings::items::SettingItem,
i: usize,
selected: bool,
) -> SettingItemView {
SettingItemView {
index: i,
path: item.path.clone(),
name: item.name.clone(),
description: item.description.clone(),
section: item.section.clone(),
section_start: item.is_section_start,
modified: item.modified,
read_only: item.read_only,
nullable: item.nullable,
is_null: item.is_null,
selected,
control: setting_control_view(&item.control),
}
}
impl Editor {
pub fn settings_view(&self) -> Option<SettingsView> {
use crate::view::settings::state::FocusPanel;
let st = self.settings_state.as_ref()?;
if !st.visible {
return None;
}
let categories = st
.pages
.iter()
.enumerate()
.map(|(i, p)| SettingsCategoryView {
index: i,
name: p.name.clone(),
selected: i == st.selected_category,
expandable: !p.subpages.is_empty() || p.sections.len() > 1,
expanded: st.expanded_categories.contains(&i),
sections: p.sections.iter().map(|s| s.name.clone()).collect(),
})
.collect();
let items = st
.pages
.get(st.selected_category)
.map(|p| {
p.items
.iter()
.enumerate()
.map(|(i, it)| setting_item_view(it, i, i == st.selected_item))
.collect()
})
.unwrap_or_default();
let entry_dialog = st.entry_dialog_stack.last().map(|d| EntryDialogView {
title: d.title.clone(),
is_new: d.is_new,
items: d
.items
.iter()
.enumerate()
.map(|(i, it)| setting_item_view(it, i, i == d.selected_item))
.collect(),
selected_item: d.selected_item,
focus_on_buttons: d.focus_on_buttons,
focused_button: d.focused_button,
no_delete: d.no_delete,
});
Some(SettingsView {
title: "Settings".to_string(),
focus: match st.focus.current() {
Some(FocusPanel::Settings) => "settings",
Some(FocusPanel::Footer) => "footer",
_ => "categories",
},
target_layer: format!("{:?}", st.target_layer),
categories,
items,
footer_buttons: vec![
format!("{:?}", st.target_layer),
"Reset".into(),
"Save".into(),
"Cancel".into(),
],
footer_selected: st.footer_button_index,
search_active: st.search_active,
search_query: st.search_query.clone(),
search_results: st
.search_results
.iter()
.map(|r| SettingsSearchResultView {
name: r.item.name.clone(),
category: r.breadcrumb.clone(),
})
.collect(),
search_selected: st.selected_search_result,
entry_dialog,
showing_help: st.showing_help,
showing_confirm: st.showing_confirm_dialog,
showing_reset: st.showing_reset_dialog,
})
}
}