//! Commands that JS can call on the editor. Each function maps its arguments to a `Message`. On web the generated //! stub dispatches it directly, on native the arguments are sent as an `EditorCommand` and the same body runs in //! the editor process. #![allow(clippy::too_many_arguments)] #[cfg(target_family = "wasm")] use crate::editor_wrapper::EditorWrapper; use graphite_proc_macros::editor_commands; use serde::{Deserialize, Serialize}; use tsify::Tsify; #[cfg(target_family = "wasm")] use wasm_bindgen::prelude::*; #[editor_commands] mod editor_commands { use crate::helpers::translate_key; use editor::messages::clipboard::utility_types::ClipboardContentRaw; use editor::messages::input_mapper::utility_types::keyboard::ModifierKeys; use editor::messages::input_mapper::utility_types::pointer::{EditorPointerState, ScrollDelta}; use editor::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport; use editor::messages::portfolio::utility_types::PanelGroupId; use editor::messages::prelude::*; use editor::messages::tool::tool_messages::tool_prelude::WidgetId; use graph_craft::document::NodeId; use graphene_std::raster::color::Color; use graphene_std::vector::style::FillChoice; use std::path::PathBuf; /// Re-sends all UI layouts to the frontend. Called during HMR re-mounts when the frontend has lost its layout state. fn resend_all_layouts() -> Message { LayoutMessage::ResendAllLayouts.into() } /// First message of a session, sent once the frontend is ready fn init_portfolio() -> Message { PortfolioMessage::Init.into() } /// Per-frame tick: advances the animation clock and broadcasts the animation frame event fn animation_frame(timestamp: u64) -> Message { Message::Batched { messages: Box::new([ InputPreprocessorMessage::CurrentTime { timestamp }.into(), AnimationMessage::IncrementFrameCounter.into(), // Used by auto-panning, but this could possibly be refactored in the future, see: // BroadcastMessage::TriggerEvent(EventMessage::AnimationFrame).into(), ]), } } fn auto_save_all_documents() -> Message { PortfolioMessage::AutoSaveAllDocuments.into() } fn add_primary_import() -> Message { Message::Batched { messages: Box::new([DocumentMessage::AddTransaction.into(), NodeGraphMessage::AddPrimaryImport.into()]), } } fn add_secondary_import() -> Message { Message::Batched { messages: Box::new([DocumentMessage::AddTransaction.into(), NodeGraphMessage::AddSecondaryImport.into()]), } } fn add_primary_export() -> Message { Message::Batched { messages: Box::new([DocumentMessage::AddTransaction.into(), NodeGraphMessage::AddPrimaryExport.into()]), } } fn add_secondary_export() -> Message { Message::Batched { messages: Box::new([DocumentMessage::AddTransaction.into(), NodeGraphMessage::AddSecondaryExport.into()]), } } fn app_window_direct_input(enabled: bool) -> Message { AppWindowMessage::DirectInput { enabled }.into() } /// Start Pointer Lock fn app_window_pointer_lock() -> Message { AppWindowMessage::PointerLock.into() } /// Minimizes the application window to the taskbar or dock fn app_window_minimize() -> Message { AppWindowMessage::Minimize.into() } /// Toggles minimizing or restoring down the application window fn app_window_maximize() -> Message { AppWindowMessage::Maximize.into() } fn app_window_fullscreen() -> Message { AppWindowMessage::Fullscreen.into() } /// Closes the application window fn app_window_close() -> Message { AppWindowMessage::Close.into() } /// Drag the application window fn app_window_drag() -> Message { AppWindowMessage::Drag.into() } /// Displays a dialog with an error message fn error_dialog(title: String, description: String) -> Message { DialogMessage::DisplayDialogError { title, description }.into() } /// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that) fn widget_value_update(layout_target: LayoutTarget, widget_id: u64, value: Any, resend_widget: bool) -> Message { let widget_id = WidgetId(widget_id); let update = LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value: value.cast(), }; if resend_widget { Message::Batched { messages: Box::new([update.into(), LayoutMessage::ResendActiveWidget { layout_target, widget_id }.into()]), } } else { update.into() } } /// Commit the value of a given UI widget to the history fn widget_value_commit(layout_target: LayoutTarget, widget_id: u64, value: Any) -> Message { LayoutMessage::WidgetValueCommit { layout_target, widget_id: WidgetId(widget_id), value: value.cast(), } .into() } /// Update the value of a given UI widget, and commit it to the history fn widget_value_commit_and_update(layout_target: LayoutTarget, widget_id: u64, value: Any, resend_widget: bool) -> Message { let widget_id = WidgetId(widget_id); let mut messages: Vec = vec![ LayoutMessage::WidgetValueCommit { layout_target, widget_id, value: value.cast(), } .into(), LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value: value.cast(), } .into(), ]; if resend_widget { messages.push(LayoutMessage::ResendActiveWidget { layout_target, widget_id }.into()); } // Close out a transaction that the widget's `on_commit` opened (if any), so a single click on widgets like the // NumberInput's increment buttons collapses into one history step instead of leaving the transaction in `Modified` messages.push(DocumentMessage::EndTransaction.into()); Message::Batched { messages: messages.into() } } /// Fire a widget's drag-drop action (e.g. when a draggable item is dropped on a button) fn widget_value_drag_drop(layout_target: LayoutTarget, widget_id: u64) -> Message { let widget_id = WidgetId(widget_id); LayoutMessage::WidgetValueDragDrop { layout_target, widget_id }.into() } /// Closes out the current transaction (drag-end / text-commit end), so emits during a slider drag collapse into one history step instead of N fn end_transaction() -> Message { DocumentMessage::EndTransaction.into() } fn load_preferences(preferences: Option) -> Message { let Some(preferences) = preferences else { return Message::NoOp }; let Ok(preferences) = serde_json::from_str(&preferences) else { log::error!("Failed to deserialize preferences"); return Message::NoOp; }; PreferencesMessage::Load { preferences }.into() } fn load_document_content(document_id: u64, document: String) -> Message { PersistentStateMessage::LoadDocument { document_id: DocumentId(document_id), document, } .into() } fn select_document(document_id: u64) -> Message { PortfolioMessage::SelectDocument { document_id: DocumentId(document_id) }.into() } /// Rename the currently active document. fn rename_document(new_name: String) -> Message { PortfolioMessage::RenameDocument { new_name }.into() } fn new_document_dialog() -> Message { DialogMessage::RequestNewDocumentDialog.into() } fn open_file(path: String, content: Vec) -> Message { PortfolioMessage::OpenFile { path: PathBuf::from(path), content }.into() } fn import_file(path: String, content: Vec) -> Message { PortfolioMessage::ImportFile { path: PathBuf::from(path), content }.into() } fn trigger_auto_save(document_id: u64) -> Message { PortfolioMessage::AutoSaveDocument { document_id: DocumentId(document_id) }.into() } fn reorder_document(document_id: u64, new_index: usize) -> Message { PortfolioMessage::ReorderDocument { document_id: DocumentId(document_id), new_index, } .into() } fn reorder_panel_group_tab(group: u64, old_index: usize, new_index: usize) -> Message { PortfolioMessage::ReorderPanelGroupTab { group: PanelGroupId(group), old_index, new_index, } .into() } fn move_all_panel_tabs(source_group: u64, target_group: u64, insert_index: usize) -> Message { PortfolioMessage::MoveAllPanelTabs { source_group: PanelGroupId(source_group), target_group: PanelGroupId(target_group), insert_index, } .into() } fn move_panel_tab(source_group: u64, target_group: u64, insert_index: usize) -> Message { PortfolioMessage::MovePanelTab { source_group: PanelGroupId(source_group), target_group: PanelGroupId(target_group), insert_index, } .into() } fn set_panel_group_active_tab(group: u64, tab_index: usize) -> Message { PortfolioMessage::SetPanelGroupActiveTab { group: PanelGroupId(group), tab_index, } .into() } fn split_panel_group(target_group: u64, direction: DockingSplitDirection, tabs: PanelTypes, active_tab_index: usize) -> Message { PortfolioMessage::SplitPanelGroup { target_group: PanelGroupId(target_group), direction, tabs, active_tab_index, } .into() } fn set_panel_group_sizes(split_path: Vec, sizes: Vec) -> Message { let split_path = split_path.into_iter().map(|i| i as usize).collect(); PortfolioMessage::SetPanelGroupSizes { split_path, sizes }.into() } fn close_document_with_confirmation(document_id: u64) -> Message { PortfolioMessage::CloseDocumentWithConfirmation { document_id: DocumentId(document_id) }.into() } fn request_about_graphite_dialog_with_localized_commit_date(localized_commit_date: String, localized_commit_year: String) -> Message { DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate { localized_commit_date, localized_commit_year, } .into() } fn request_licenses_third_party_dialog_with_license_text(license_text: String) -> Message { DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text }.into() } /// Send new viewport info to the backend fn update_viewport(x: f64, y: f64, width: f64, height: f64, scale: f64) -> Message { ViewportMessage::Update { x, y, width, height, scale }.into() } /// Mouse movement within the screenspace bounds of the viewport fn on_mouse_move( x: f64, y: f64, mouse_keys: u8, modifiers: u8, time: Option, pressure: Option, tilt_x: Option, tilt_y: Option, twist: Option, tangential: Option, eraser: bool, ) -> Message { let editor_mouse_state = EditorPointerState { time, pressure, tilt: match (tilt_x, tilt_y) { (Some(tilt_x), Some(tilt_y)) => Some((tilt_x, tilt_y).into()), _ => None, }, twist, wheel: tangential, eraser, ..EditorPointerState::from_keys_and_editor_position(mouse_keys, (x, y).into()) }; let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys"); InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys }.into() } /// Mouse scrolling within the screenspace bounds of the viewport fn on_wheel_scroll(x: f64, y: f64, mouse_keys: u8, wheel_delta_x: f64, wheel_delta_y: f64, wheel_delta_z: f64, modifiers: u8) -> Message { let mut editor_mouse_state = EditorPointerState::from_keys_and_editor_position(mouse_keys, (x, y).into()); editor_mouse_state.scroll_delta = ScrollDelta::new(wheel_delta_x, wheel_delta_y, wheel_delta_z); let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys"); InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys }.into() } /// A mouse button depressed within screenspace the bounds of the viewport fn on_mouse_down( x: f64, y: f64, mouse_keys: u8, modifiers: u8, time: Option, pressure: Option, tilt_x: Option, tilt_y: Option, twist: Option, tangential: Option, eraser: bool, ) -> Message { let editor_mouse_state = EditorPointerState { time, pressure, tilt: match (tilt_x, tilt_y) { (Some(tilt_x), Some(tilt_y)) => Some((tilt_x, tilt_y).into()), _ => None, }, twist, wheel: tangential, eraser, ..EditorPointerState::from_keys_and_editor_position(mouse_keys, (x, y).into()) }; let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys"); InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys }.into() } /// A mouse button released fn on_mouse_up( x: f64, y: f64, mouse_keys: u8, modifiers: u8, time: Option, pressure: Option, tilt_x: Option, tilt_y: Option, twist: Option, tangential: Option, eraser: bool, ) -> Message { let editor_mouse_state = EditorPointerState { time, pressure, tilt: match (tilt_x, tilt_y) { (Some(tilt_x), Some(tilt_y)) => Some((tilt_x, tilt_y).into()), _ => None, }, twist, wheel: tangential, eraser, ..EditorPointerState::from_keys_and_editor_position(mouse_keys, (x, y).into()) }; let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys"); InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys }.into() } /// Mouse shaken fn on_mouse_shake(x: f64, y: f64, mouse_keys: u8, modifiers: u8) -> Message { let editor_mouse_state = EditorPointerState::from_keys_and_editor_position(mouse_keys, (x, y).into()); let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys"); InputPreprocessorMessage::PointerShake { editor_mouse_state, modifier_keys }.into() } /// Mouse double clicked fn on_double_click(x: f64, y: f64, mouse_keys: u8, modifiers: u8) -> Message { let editor_mouse_state = EditorPointerState::from_keys_and_editor_position(mouse_keys, (x, y).into()); let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys"); InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys }.into() } /// A keyboard button depressed within screenspace the bounds of the viewport fn on_key_down(name: String, modifiers: u8, key_repeat: bool) -> Message { let key = translate_key(&name); let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys"); trace!("Key down {key:?}, name: {name}, modifiers: {modifiers:?}, key repeat: {key_repeat}"); InputPreprocessorMessage::KeyDown { key, key_repeat, modifier_keys }.into() } /// A keyboard button released fn on_key_up(name: String, modifiers: u8, key_repeat: bool) -> Message { let key = translate_key(&name); let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys"); trace!("Key up {key:?}, name: {name}, modifiers: {modifier_keys:?}, key repeat: {key_repeat}"); InputPreprocessorMessage::KeyUp { key, key_repeat, modifier_keys }.into() } /// A text box was committed fn on_change_text(new_text: String, is_left_or_right_click: bool) -> Message { TextToolMessage::TextChange { new_text, is_left_or_right_click }.into() } /// Dialog got dismissed fn on_dialog_dismiss() -> Message { DialogMessage::Dismiss.into() } /// A text box was changed fn update_bounds(new_text: String) -> Message { TextToolMessage::UpdateBounds { new_text }.into() } /// Update primary color from sRGB bytes (the wire format at the JS boundary). fn update_primary_color(color: SRGBA8) -> Message { ToolMessage::SelectWorkingColor { color: Color::from(color), primary: true, } .into() } /// Update secondary color from sRGB bytes (the wire format at the JS boundary). fn update_secondary_color(color: SRGBA8) -> Message { ToolMessage::SelectWorkingColor { color: Color::from(color), primary: false, } .into() } /// Initialize the Rust color picker handler with a starting value (used when the frontend `` opens). fn open_color_picker(initial_value: FillChoiceSRGBA8, allow_none: bool, disabled: bool) -> Message { ColorPickerMessage::Open { initial_value: FillChoice::from(&initial_value.0), allow_none, disabled, } .into() } /// Tell the Rust color picker handler that the popover is closing. fn close_color_picker() -> Message { ColorPickerMessage::Close.into() } /// Update the color of the currently-edited gradient stop, from sRGB bytes (the wire format at the JS boundary). fn update_gradient_stop_color(color: SRGBA8) -> Message { GradientToolMessage::UpdateStopColor { color: Color::from(color) }.into() } /// Start a new undo transaction for gradient stop color editing fn start_gradient_stop_color_transaction() -> Message { GradientToolMessage::StartTransactionForColorStop.into() } /// Commit the current gradient stop color transaction (called on pointer-up after each drag/click) fn commit_gradient_stop_color_transaction() -> Message { GradientToolMessage::CommitTransactionForColorStop.into() } /// Close the gradient stop color picker and commit any pending transaction fn close_gradient_stop_color_picker() -> Message { GradientToolMessage::CloseStopColorPicker.into() } /// Toggle clipping the alpha of a layer to the alpha of the layer below it in the layer stack fn clip_layer(id: u64) -> Message { DocumentMessage::ClipLayer { id: NodeId(id) }.into() } /// Modify the layer selection based on the layer which is clicked while holding down the Ctrl and/or Shift modifier keys used for range selection behavior fn select_layer(id: u64, ctrl: bool, shift: bool) -> Message { DocumentMessage::SelectLayer { id: NodeId(id), ctrl, shift }.into() } /// Deselect all layers fn deselect_all_layers() -> Message { DocumentMessage::DeselectAllLayers.into() } /// Move a layer to within a folder and placed down at the given index. /// If the folder is `None`, it is inserted into the document root. /// If the insert index is `None`, it is inserted at the start of the folder. fn move_layer_in_tree(insert_parent_id: Option, insert_index: Option) -> Message { let insert_parent_id = insert_parent_id.map(NodeId); let parent = insert_parent_id.map(LayerNodeIdentifier::new_unchecked).unwrap_or_default(); DocumentMessage::MoveSelectedLayersTo { parent, insert_index: insert_index.unwrap_or_default(), } .into() } /// Reorder a draggable Properties panel section to the given index among its peers. fn reorder_properties_section(node_id: u64, insert_index: usize) -> Message { DocumentMessage::ReorderPropertiesSection { node_id: NodeId(node_id), insert_index, } .into() } /// Duplicate the selected layers, placing the copies within the given folder at the given index. /// If the folder is `None`, they are inserted into the document root. /// If the insert index is `None`, they are inserted at the start of the folder. fn duplicate_layer_in_tree(insert_parent_id: Option, insert_index: Option) -> Message { DocumentMessage::DuplicateSelectedLayersTo { parent: insert_parent_id.map(NodeId).map(LayerNodeIdentifier::new_unchecked).unwrap_or_default(), insert_index: insert_index.unwrap_or_default(), } .into() } /// Set the name for the layer fn set_layer_name(id: u64, name: String) -> Message { let layer = LayerNodeIdentifier::new_unchecked(NodeId(id)); NodeGraphMessage::SetDisplayName { node_id: layer.to_node(), network_path: Vec::new(), alias: name, skip_adding_history_step: false, } .into() } /// Translates document (in viewport coords) fn pan_canvas_abort_prepare(x_not_y_axis: bool) -> Message { NavigationMessage::CanvasPanAbortPrepare { x_not_y_axis }.into() } fn pan_canvas_abort(x_not_y_axis: bool) -> Message { NavigationMessage::CanvasPanAbort { x_not_y_axis }.into() } /// Translates document (in viewport coords) fn pan_canvas(delta_x: f64, delta_y: f64) -> Message { NavigationMessage::CanvasPan { delta: (delta_x, delta_y).into() }.into() } /// Translates document (in viewport coords) fn pan_canvas_by_fraction(delta_x: f64, delta_y: f64) -> Message { NavigationMessage::CanvasPanByViewportFraction { delta: (delta_x, delta_y).into() }.into() } /// Merge the selected nodes into a subnetwork fn merge_selected_nodes() -> Message { NodeGraphMessage::MergeSelectedNodes.into() } /// Toggle lock state of all selected layers fn toggle_selected_locked() -> Message { NodeGraphMessage::ToggleSelectedLocked.into() } /// Creates a new document node in the node graph fn create_node(node_type: String, x: i32, y: i32) -> Message { let id = NodeId::new(); NodeGraphMessage::CreateNodeFromContextMenu { node_id: Some(id), node_type: DefinitionIdentifier::from_serialized(&node_type), xy: Some((x / 24, y / 24)), add_transaction: true, } .into() } /// Respond to selection read fn read_selection(content: Option, cut: bool) -> Message { ClipboardMessage::ReadSelection { content, cut }.into() } /// Paste from a serialized JSON representation fn paste_text(data: String) -> Message { ClipboardMessage::ReadClipboard { content: ClipboardContentRaw::Text(data), } .into() } /// Pastes an image fn paste_image( name: Option, image_data: Vec, width: u32, height: u32, mouse_x: Option, mouse_y: Option, insert_parent_id: Option, insert_index: Option, ) -> Message { let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y))); let image = graphene_std::raster::Image::from_image_data(&image_data, width, height); let parent_and_insert_index = if let (Some(insert_parent_id), Some(insert_index)) = (insert_parent_id, insert_index) { let insert_parent_id = NodeId(insert_parent_id); let parent = LayerNodeIdentifier::new_unchecked(insert_parent_id); Some((parent, insert_index)) } else { None }; PortfolioMessage::InsertImage { name, image, mouse, parent_and_insert_index, } .into() } /// Pastes an SVG given its string representation fn paste_svg(name: Option, svg: String, mouse_x: Option, mouse_y: Option, insert_parent_id: Option, insert_index: Option) -> Message { let mouse = mouse_x.and_then(|x| mouse_y.map(|y| (x, y))); let parent_and_insert_index = if let (Some(insert_parent_id), Some(insert_index)) = (insert_parent_id, insert_index) { let insert_parent_id = NodeId(insert_parent_id); let parent = LayerNodeIdentifier::new_unchecked(insert_parent_id); Some((parent, insert_index)) } else { None }; PortfolioMessage::InsertSvg { name, svg, mouse, parent_and_insert_index, } .into() } /// Toggle visibility of a layer or node given its node ID fn toggle_node_visibility_layer_panel(id: u64) -> Message { NodeGraphMessage::ToggleVisibility { node_id: NodeId(id), network_path: Vec::new(), } .into() } /// Pin or unpin a node given its node ID fn set_node_pinned(id: u64, pinned: bool) -> Message { DocumentMessage::SetNodePinned { node_id: NodeId(id), pinned }.into() } /// Collapse or expand a node's section in the Properties panel fn toggle_node_properties_section_expanded(id: u64) -> Message { DocumentMessage::ToggleNodePropertiesSectionExpanded { node_id: NodeId(id) }.into() } /// Delete a layer or node given its node ID fn delete_node(id: u64) -> Message { DocumentMessage::DeleteNode { node_id: NodeId(id) }.into() } /// Toggle lock state of a layer from the layer list fn toggle_layer_lock(node_id: u64) -> Message { NodeGraphMessage::ToggleLocked { node_id: NodeId(node_id), network_path: Vec::new(), } .into() } /// Toggle expansions state of a layer from the layer list fn toggle_layer_expansion(tree_path: Vec, recursive: bool) -> Message { let tree_path = tree_path.into_iter().map(NodeId).collect(); DocumentMessage::ToggleLayerExpansion { tree_path, recursive }.into() } /// Set the active panel to the most recently clicked panel fn set_active_panel(panel: String) -> Message { DocumentMessage::SetActivePanel { active_panel: panel.into() }.into() } /// Toggle display type for a layer fn set_to_node_or_layer(id: u64, is_layer: bool) -> Message { DocumentMessage::SetToNodeOrLayer { node_id: NodeId(id), is_layer }.into() } /// Set the name of an import or export fn set_import_name(index: usize, name: String) -> Message { NodeGraphMessage::SetImportExportName { name, index: ImportOrExport::Import(index), } .into() } /// Set the name of an export fn set_export_name(index: usize, name: String) -> Message { NodeGraphMessage::SetImportExportName { name, index: ImportOrExport::Export(index), } .into() } } #[cfg(feature = "editor")] #[derive(Debug, Clone, Serialize, Deserialize, Tsify)] #[tsify(from_wasm_abi)] pub struct FillChoiceSRGBA8( /// Concrete wasm boundary form of the generic [`FillChoice`], since a `#[wasm_bindgen]` argument's TS declaration names its type without the generic's argument. #[tsify(type = "FillChoice")] pub graphene_std::vector::style::FillChoice, ); #[cfg(not(feature = "editor"))] pub type FillChoiceSRGBA8 = Any; #[derive(Debug, Clone, Serialize, Deserialize, Tsify)] #[tsify(from_wasm_abi)] pub struct Any(#[tsify(type = "any")] serde_json::Value); impl Any { #[cfg(feature = "editor")] pub(crate) fn cast Deserialize<'de>>(&self) -> T { serde_json::from_value(self.0.clone()).unwrap() } } macro_rules! editor_proxy_types { ($($name:ident = $real:ty;)*) => { $( #[cfg(feature = "editor")] pub type $name = $real; #[cfg(not(feature = "editor"))] pub type $name = Any; )* }; } editor_proxy_types! { LayoutTarget = editor::messages::layout::utility_types::layout_widget::LayoutTarget; DockingSplitDirection = editor::messages::portfolio::utility_types::DockingSplitDirection; PanelTypes = Vec; SRGBA8 = graphene_std::color::SRGBA8; }