//! Owned, serializable versions of the ribbon vocabulary for IPC. use std::sync::Arc; use serde::{Deserialize, Serialize}; use crate::ribbon::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, StyleKey, ToolDef}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OwnedToolDef { pub id: String, pub label: String, pub icon: OwnedIconKind, pub event: ModuleEvent, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum OwnedIconKind { Glyph(String), Svg(Vec), } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum OwnedRibbonItem { Tool(OwnedToolDef), LabeledTool(OwnedToolDef), LargeTool(OwnedToolDef), Dropdown { id: String, icon: OwnedIconKind, items: Vec<(String, String, OwnedIconKind)>, default: String, }, LabeledDropdown { id: String, label: String, icon: OwnedIconKind, items: Vec<(String, String, OwnedIconKind)>, default: String, }, LargeDropdown { id: String, label: String, icon: OwnedIconKind, items: Vec<(String, String, OwnedIconKind)>, default: String, }, ToolGrid { columns: Vec>, }, LayerComboGroup { row2: Vec, row3: Vec, }, PropertiesGroup { match_prop: OwnedToolDef, }, StyleComboGroup { style_key: StyleKey, combo_id: String, manager_cmd: Option, rows: Vec>, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OwnedRibbonGroup { pub title: String, pub tools: Vec, } impl OwnedRibbonGroup { /// Every command id a user could invoke from this group's tools. The host /// feeds these into the command-line autocomplete so a loaded plugin's /// commands are discoverable by typing, not just via ribbon buttons (#272). pub fn command_ids(&self) -> Vec { let mut out = Vec::new(); for item in &self.tools { collect_item_command_ids(item, &mut out); } out } } fn push_tool_command(tool: &OwnedToolDef, out: &mut Vec) { if let ModuleEvent::Command(cmd) = &tool.event { out.push(cmd.clone()); } } fn collect_item_command_ids(item: &OwnedRibbonItem, out: &mut Vec) { match item { OwnedRibbonItem::Tool(t) | OwnedRibbonItem::LabeledTool(t) | OwnedRibbonItem::LargeTool(t) => push_tool_command(t, out), OwnedRibbonItem::Dropdown { items, .. } | OwnedRibbonItem::LabeledDropdown { items, .. } | OwnedRibbonItem::LargeDropdown { items, .. } => { // Each entry is (command id, label, icon). out.extend(items.iter().map(|(id, _, _)| id.clone())); } OwnedRibbonItem::ToolGrid { columns } => { for tool in columns.iter().flatten() { push_tool_command(tool, out); } } OwnedRibbonItem::LayerComboGroup { row2, row3 } => { for t in row2.iter().chain(row3.iter()) { push_tool_command(t, out); } } OwnedRibbonItem::PropertiesGroup { match_prop } => push_tool_command(match_prop, out), OwnedRibbonItem::StyleComboGroup { rows, manager_cmd, .. } => { for t in rows.iter().flatten() { push_tool_command(t, out); } if let Some(cmd) = manager_cmd { out.push(cmd.clone()); } } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OwnedPluginManifest { pub id: String, pub name: String, pub version: String, pub description: String, pub api_version: u32, pub ribbon_order: i32, pub xdata_apps: Vec, pub command_prefixes: Vec, } impl From for OwnedIconKind { fn from(i: IconKind) -> Self { match i { IconKind::Glyph(g) => OwnedIconKind::Glyph(g.to_string()), IconKind::Svg(b) => OwnedIconKind::Svg(b.to_vec()), } } } impl From<&IconKind> for OwnedIconKind { fn from(i: &IconKind) -> Self { match *i { IconKind::Glyph(g) => OwnedIconKind::Glyph(g.to_string()), IconKind::Svg(b) => OwnedIconKind::Svg(b.to_vec()), } } } impl OwnedIconKind { /// Leak the owned data to reconstruct an `IconKind` with `&'static` lifetime. pub fn to_static(self) -> IconKind { match self { OwnedIconKind::Glyph(g) => IconKind::Glyph(&*Box::leak(g.into_boxed_str())), OwnedIconKind::Svg(b) => IconKind::Svg(&*Box::leak(b.into_boxed_slice())), } } } impl From for OwnedToolDef { fn from(t: ToolDef) -> Self { Self { id: t.id.to_string(), label: t.label.to_string(), icon: t.icon.into(), event: t.event, } } } impl From<&ToolDef> for OwnedToolDef { fn from(t: &ToolDef) -> Self { Self { id: t.id.to_string(), label: t.label.to_string(), icon: (&t.icon).into(), event: t.event.clone(), } } } impl OwnedToolDef { pub fn to_static(self) -> ToolDef { ToolDef { id: &*Box::leak(self.id.into_boxed_str()), label: &*Box::leak(self.label.into_boxed_str()), icon: self.icon.to_static(), event: self.event, } } } impl From for OwnedRibbonItem { fn from(item: RibbonItem) -> Self { match item { RibbonItem::Tool(t) => OwnedRibbonItem::Tool(t.into()), RibbonItem::LabeledTool(t) => OwnedRibbonItem::LabeledTool(t.into()), RibbonItem::LargeTool(t) => OwnedRibbonItem::LargeTool(t.into()), RibbonItem::Dropdown { id, icon, items, default, } => OwnedRibbonItem::Dropdown { id: id.to_string(), icon: icon.into(), items: items .into_iter() .map(|(a, b, i)| (a.to_string(), b.to_string(), i.into())) .collect(), default: default.to_string(), }, RibbonItem::LabeledDropdown { id, label, icon, items, default } => { OwnedRibbonItem::LabeledDropdown { id: id.to_string(), label: label.to_string(), icon: icon.into(), items: items.into_iter().map(|(a, b, i)| { (a.to_string(), b.to_string(), i.into()) }).collect(), default: default.to_string(), } } RibbonItem::LargeDropdown { id, label, icon, items, default, } => OwnedRibbonItem::LargeDropdown { id: id.to_string(), label: label.to_string(), icon: icon.into(), items: items .into_iter() .map(|(a, b, i)| (a.to_string(), b.to_string(), i.into())) .collect(), default: default.to_string(), }, RibbonItem::ToolGrid { columns } => OwnedRibbonItem::ToolGrid { columns: columns.into_iter().map(|column| { column.into_iter().map(Into::into).collect() }).collect(), }, RibbonItem::LayerComboGroup { row2, row3 } => OwnedRibbonItem::LayerComboGroup { row2: row2.into_iter().map(Into::into).collect(), row3: row3.into_iter().map(Into::into).collect(), }, RibbonItem::PropertiesGroup { match_prop } => OwnedRibbonItem::PropertiesGroup { match_prop: match_prop.into(), }, RibbonItem::StyleComboGroup { style_key, combo_id, manager_cmd, rows, } => OwnedRibbonItem::StyleComboGroup { style_key, combo_id: combo_id.to_string(), manager_cmd: manager_cmd.map(|s| s.to_string()), rows: rows .into_iter() .map(|r| r.into_iter().map(Into::into).collect()) .collect(), }, } } } impl OwnedRibbonItem { pub fn to_static(self) -> RibbonItem { match self { OwnedRibbonItem::Tool(t) => RibbonItem::Tool(t.to_static()), OwnedRibbonItem::LabeledTool(t) => RibbonItem::LabeledTool(t.to_static()), OwnedRibbonItem::LargeTool(t) => RibbonItem::LargeTool(t.to_static()), OwnedRibbonItem::Dropdown { id, icon, items, default, } => RibbonItem::Dropdown { id: &*Box::leak(id.into_boxed_str()), icon: icon.to_static(), items: items .into_iter() .map(|(a, b, i)| { ( &*Box::leak(a.into_boxed_str()), &*Box::leak(b.into_boxed_str()), i.to_static(), ) }) .collect(), default: &*Box::leak(default.into_boxed_str()), }, OwnedRibbonItem::LabeledDropdown { id, label, icon, items, default } => { RibbonItem::LabeledDropdown { id: &*Box::leak(id.into_boxed_str()), label: &*Box::leak(label.into_boxed_str()), icon: icon.to_static(), items: items.into_iter().map(|(a, b, i)| ( &*Box::leak(a.into_boxed_str()), &*Box::leak(b.into_boxed_str()), i.to_static(), )).collect(), default: &*Box::leak(default.into_boxed_str()), } } OwnedRibbonItem::LargeDropdown { id, label, icon, items, default, } => RibbonItem::LargeDropdown { id: &*Box::leak(id.into_boxed_str()), label: &*Box::leak(label.into_boxed_str()), icon: icon.to_static(), items: items .into_iter() .map(|(a, b, i)| { ( &*Box::leak(a.into_boxed_str()), &*Box::leak(b.into_boxed_str()), i.to_static(), ) }) .collect(), default: &*Box::leak(default.into_boxed_str()), }, OwnedRibbonItem::ToolGrid { columns } => RibbonItem::ToolGrid { columns: columns.into_iter().map(|column| { column.into_iter().map(OwnedToolDef::to_static).collect() }).collect(), }, OwnedRibbonItem::LayerComboGroup { row2, row3 } => RibbonItem::LayerComboGroup { row2: row2.into_iter().map(|t| t.to_static()).collect(), row3: row3.into_iter().map(|t| t.to_static()).collect(), }, OwnedRibbonItem::PropertiesGroup { match_prop } => RibbonItem::PropertiesGroup { match_prop: match_prop.to_static(), }, OwnedRibbonItem::StyleComboGroup { style_key, combo_id, manager_cmd, rows, } => RibbonItem::StyleComboGroup { style_key, combo_id: &*Box::leak(combo_id.into_boxed_str()), manager_cmd: manager_cmd.map(|s| &*Box::leak(s.into_boxed_str())), rows: rows .into_iter() .map(|r| r.into_iter().map(|t| t.to_static()).collect()) .collect(), }, } } } impl From<&RibbonItem> for OwnedRibbonItem { fn from(item: &RibbonItem) -> Self { match item { RibbonItem::Tool(t) => OwnedRibbonItem::Tool(t.into()), RibbonItem::LabeledTool(t) => OwnedRibbonItem::LabeledTool(t.into()), RibbonItem::LargeTool(t) => OwnedRibbonItem::LargeTool(t.into()), RibbonItem::Dropdown { id, icon, items, default, } => OwnedRibbonItem::Dropdown { id: id.to_string(), icon: icon.into(), items: items .iter() .map(|(a, b, i)| (a.to_string(), b.to_string(), i.into())) .collect(), default: default.to_string(), }, RibbonItem::LabeledDropdown { id, label, icon, items, default } => { OwnedRibbonItem::LabeledDropdown { id: id.to_string(), label: label.to_string(), icon: icon.into(), items: items.iter().map(|(a, b, i)| { (a.to_string(), b.to_string(), i.into()) }).collect(), default: default.to_string(), } } RibbonItem::LargeDropdown { id, label, icon, items, default, } => OwnedRibbonItem::LargeDropdown { id: id.to_string(), label: label.to_string(), icon: icon.into(), items: items .iter() .map(|(a, b, i)| (a.to_string(), b.to_string(), i.into())) .collect(), default: default.to_string(), }, RibbonItem::ToolGrid { columns } => OwnedRibbonItem::ToolGrid { columns: columns.iter().map(|column| { column.iter().map(Into::into).collect() }).collect(), }, RibbonItem::LayerComboGroup { row2, row3 } => OwnedRibbonItem::LayerComboGroup { row2: row2.iter().map(Into::into).collect(), row3: row3.iter().map(Into::into).collect(), }, RibbonItem::PropertiesGroup { match_prop } => OwnedRibbonItem::PropertiesGroup { match_prop: match_prop.into(), }, RibbonItem::StyleComboGroup { style_key, combo_id, manager_cmd, rows, } => OwnedRibbonItem::StyleComboGroup { style_key: *style_key, combo_id: combo_id.to_string(), manager_cmd: manager_cmd.map(|s| s.to_string()), rows: rows .iter() .map(|r| r.iter().map(Into::into).collect()) .collect(), }, } } } impl From for OwnedRibbonGroup { fn from(g: RibbonGroup) -> Self { Self { title: g.title.to_string(), tools: g.tools.into_iter().map(Into::into).collect(), } } } impl From<&RibbonGroup> for OwnedRibbonGroup { fn from(g: &RibbonGroup) -> Self { Self { title: g.title.to_string(), tools: g.tools.iter().map(Into::into).collect(), } } } impl OwnedRibbonGroup { pub fn to_static(self) -> RibbonGroup { RibbonGroup { title: &*Box::leak(self.title.into_boxed_str()), tools: self.tools.into_iter().map(|t| t.to_static()).collect(), } } } /// Convert owned ribbon groups into a `CadModule` by leaking the strings once. pub fn to_module(id: String, title: String, groups: Vec) -> Box { struct M { id: &'static str, title: &'static str, groups: Vec, } impl CadModule for M { fn id(&self) -> &'static str { self.id } fn title(&self) -> &'static str { self.title } fn ribbon_groups(&self) -> &[RibbonGroup] { &self.groups } } let id = &*Box::leak(id.into_boxed_str()); let title = &*Box::leak(title.into_boxed_str()); Box::new(M { id, title, groups: groups .into_iter() .map(OwnedRibbonGroup::to_static) .collect(), }) } /// A cheaply-cloneable `CadModule` wrapper for plugin ribbon data. #[derive(Clone)] pub struct SharedCadModule(Arc); impl CadModule for SharedCadModule { fn id(&self) -> &'static str { self.0.id() } fn title(&self) -> &'static str { self.0.title() } fn ribbon_groups(&self) -> &[RibbonGroup] { self.0.ribbon_groups() } } /// Convert owned ribbon groups into a shareable `CadModule`. pub fn to_shared_module( id: String, title: String, groups: Vec, ) -> SharedCadModule { let module = to_module(id, title, groups); SharedCadModule(Arc::from(module)) } #[cfg(all(test, feature = "host"))] mod tests { use super::*; #[test] fn owned_ribbon_group_round_trips_through_static() { let owned = OwnedRibbonGroup { title: "Geometry".to_string(), tools: vec![OwnedRibbonItem::Tool(OwnedToolDef { id: "line".to_string(), label: "Line".to_string(), icon: OwnedIconKind::Glyph("L".to_string()), event: ModuleEvent::Command("LINE".to_string()), })], }; let static_group = owned.clone().to_static(); assert_eq!(static_group.title, "Geometry"); assert_eq!(static_group.tools.len(), 1); let back: OwnedRibbonGroup = static_group.into(); assert_eq!(back.title, owned.title); } #[test] fn shared_module_clones_without_re_leaking() { let owned = vec![OwnedRibbonGroup { title: "Draw".to_string(), tools: vec![OwnedRibbonItem::Tool(OwnedToolDef { id: "circle".to_string(), label: "Circle".to_string(), icon: OwnedIconKind::Glyph("C".to_string()), event: ModuleEvent::Command("CIRCLE".to_string()), })], }]; let shared = to_shared_module("opencad.demo".to_string(), "Demo".to_string(), owned); let cloned = shared.clone(); assert_eq!(shared.title(), "Demo"); assert_eq!(shared.id(), "opencad.demo"); assert_eq!(cloned.title(), shared.title()); assert_eq!(shared.ribbon_groups().len(), cloned.ribbon_groups().len()); } #[test] fn command_ids_covers_tools_and_dropdown_items() { let group = OwnedRibbonGroup { title: "Labels".to_string(), tools: vec![ OwnedRibbonItem::Tool(OwnedToolDef { id: "ls_label".to_string(), label: "Label".to_string(), icon: OwnedIconKind::Glyph("L".to_string()), event: ModuleEvent::Command("LS_LABEL".to_string()), }), OwnedRibbonItem::LargeDropdown { id: "ls_menu".to_string(), label: "More".to_string(), icon: OwnedIconKind::Glyph("M".to_string()), items: vec![( "LS_AUTOLABEL".to_string(), "Auto".to_string(), OwnedIconKind::Glyph("A".to_string()), )], default: "LS_AUTOLABEL".to_string(), }, ], }; let ids = group.command_ids(); assert!(ids.contains(&"LS_LABEL".to_string()), "got {ids:?}"); assert!(ids.contains(&"LS_AUTOLABEL".to_string()), "got {ids:?}"); } #[test] fn command_ids_covers_labeled_tools_and_tool_grids() { let tool = |id: &str| OwnedToolDef { id: id.to_lowercase(), label: id.to_string(), icon: OwnedIconKind::Glyph("·".to_string()), event: ModuleEvent::Command(id.to_string()), }; let group = OwnedRibbonGroup { title: "Constraints".to_string(), tools: vec![ OwnedRibbonItem::LabeledTool(tool("SHOW_ALL")), OwnedRibbonItem::LabeledDropdown { id: "visibility".to_string(), label: "Show/Hide".to_string(), icon: OwnedIconKind::Glyph("·".to_string()), items: vec![( "SHOW_SELECTED".to_string(), "Show".to_string(), OwnedIconKind::Glyph("·".to_string()), )], default: "SHOW_SELECTED".to_string(), }, OwnedRibbonItem::ToolGrid { columns: vec![vec![tool("PARALLEL"), tool("PERPENDICULAR")]], }, ], }; let ids = group.command_ids(); assert_eq!(ids, ["SHOW_ALL", "SHOW_SELECTED", "PARALLEL", "PERPENDICULAR"]); let round_trip: OwnedRibbonGroup = group.to_static().into(); assert_eq!(round_trip.command_ids(), ids); } }