Files
Graphite/frontend/wrapper/src/editor_commands.rs
Timon 9d108421cd Desktop: Stop compiling the editor into the wasm wrapper (#4348)
* Make wasm wrapper not depend on editor on native

* Introduce pkg-native

* Fix tests

* Fix fmt

* Correct span and make cargo fmt work inside editor_commands block

* Review

* Fix lint
2026-09-10 20:13:42 +00:00

690 lines
24 KiB
Rust

//! 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::input_keyboard::ModifierKeys;
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta};
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:
// <https://github.com/GraphiteEditor/Graphite/pull/2562#discussion_r2041102786>
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()]),
}
}
/// 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<Message> = 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<String>) -> 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<u8>) -> Message {
PortfolioMessage::OpenFile { path: PathBuf::from(path), content }.into()
}
fn import_file(path: String, content: Vec<u8>) -> 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<u32>, sizes: Vec<f64>) -> 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) -> Message {
let editor_mouse_state = EditorMouseState::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 = EditorMouseState::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) -> Message {
let editor_mouse_state = EditorMouseState::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) -> Message {
let editor_mouse_state = EditorMouseState::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 = EditorMouseState::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 = EditorMouseState::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 `<ColorPicker />` opens).
fn open_color_picker(initial_value: FillChoiceUI, allow_none: bool, disabled: bool) -> Message {
ColorPickerMessage::Open {
initial_value: FillChoice::from(&initial_value),
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 <kbd>Ctrl</kbd> and/or <kbd>Shift</kbd> 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<u64>, insert_index: Option<usize>) -> 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<u64>, insert_index: Option<usize>) -> 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: Any, x: i32, y: i32) -> Message {
let id = NodeId::new();
NodeGraphMessage::CreateNodeFromContextMenu {
node_id: Some(id),
node_type: node_type.cast(),
xy: Some((x / 24, y / 24)),
add_transaction: true,
}
.into()
}
/// Respond to selection read
fn read_selection(content: Option<String>, 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<String>,
image_data: Vec<u8>,
width: u32,
height: u32,
mouse_x: Option<f64>,
mouse_y: Option<f64>,
insert_parent_id: Option<u64>,
insert_index: Option<usize>,
) -> 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<String>, svg: String, mouse_x: Option<f64>, mouse_y: Option<f64>, insert_parent_id: Option<u64>, insert_index: Option<usize>) -> 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<u64>, 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()
}
}
#[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<T: for<'de> 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<editor::messages::portfolio::utility_types::PanelType>;
SRGBA8 = graphene_std::color::SRGBA8;
FillChoiceUI = graphene_std::vector::style::FillChoiceUI;
}