From 0c9ac66fca0c239bcbf5869a14b732b6761f526a Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 01:25:13 -0800 Subject: [PATCH 01/28] Migrate Specta to Tsify to auto-generate messages.ts, working except colors and widgets --- .vscode/settings.json | 3 +- Cargo.lock | 106 ++- Cargo.toml | 8 +- deny.toml | 2 +- editor/Cargo.toml | 3 +- editor/src/generate_ts_types.rs | 34 - editor/src/lib.rs | 1 - .../app_window/app_window_message_handler.rs | 14 +- .../src/messages/frontend/frontend_message.rs | 47 +- editor/src/messages/frontend/utility_types.rs | 21 +- .../utility_types/input_keyboard.rs | 15 +- .../input_mapper/utility_types/input_mouse.rs | 3 +- .../input_mapper/utility_types/misc.rs | 4 +- .../layout/utility_types/layout_widget.rs | 30 +- .../utility_types/widgets/button_widgets.rs | 24 +- .../utility_types/widgets/input_widgets.rs | 49 +- .../utility_types/widgets/label_widgets.rs | 21 +- editor/src/messages/message.rs | 8 - .../document/document_message_handler.rs | 10 +- .../node_graph/document_node_definitions.rs | 3 +- .../node_graph/node_graph_message_handler.rs | 11 +- .../document/node_graph/utility_types.rs | 52 +- .../document/overlays/utility_types_native.rs | 10 +- .../document/overlays/utility_types_web.rs | 10 +- .../document/utility_types/clipboards.rs | 3 +- .../utility_types/document_metadata.rs | 3 +- .../portfolio/document/utility_types/misc.rs | 10 +- .../utility_types/network_interface.rs | 9 +- .../portfolio/document/utility_types/nodes.rs | 12 +- .../portfolio/document/utility_types/wires.rs | 9 +- .../preferences_message_handler.rs | 3 +- .../src/messages/preferences/utility_types.rs | 3 +- .../common_functionality/color_selector.rs | 3 +- .../tool/common_functionality/pivot.rs | 6 +- .../shapes/shape_utility.rs | 3 +- .../tool/tool_messages/artboard_tool.rs | 3 +- .../messages/tool/tool_messages/brush_tool.rs | 9 +- .../tool/tool_messages/eyedropper_tool.rs | 29 +- .../messages/tool/tool_messages/fill_tool.rs | 3 +- .../tool/tool_messages/freehand_tool.rs | 6 +- .../tool/tool_messages/gradient_tool.rs | 17 +- .../tool/tool_messages/navigate_tool.rs | 3 +- .../messages/tool/tool_messages/path_tool.rs | 9 +- .../messages/tool/tool_messages/pen_tool.rs | 9 +- .../tool/tool_messages/select_tool.rs | 12 +- .../messages/tool/tool_messages/shape_tool.rs | 6 +- .../tool/tool_messages/spline_tool.rs | 6 +- .../messages/tool/tool_messages/text_tool.rs | 10 +- editor/src/messages/tool/utility_types.rs | 12 +- .../viewport/viewport_message_handler.rs | 21 +- editor/src/node_graph_executor.rs | 7 +- .../src/components/panels/Document.svelte | 25 +- frontend/src/components/panels/Layers.svelte | 3 +- .../widgets/labels/ShortcutLabel.svelte | 4 +- frontend/src/editor.ts | 4 +- frontend/src/io-managers/input.ts | 2 +- frontend/src/io-managers/persistence.ts | 3 +- frontend/src/messages.ts | 769 +---------------- frontend/src/messages_old.ts | 776 ++++++++++++++++++ frontend/src/state-providers/dialog.ts | 2 +- frontend/src/state-providers/node-graph.ts | 8 +- frontend/src/subscription-router.ts | 66 +- frontend/src/utility-functions/colors.ts | 37 +- frontend/src/utility-functions/files.ts | 9 +- frontend/src/utility-functions/widgets.ts | 16 +- node-graph/graph-craft/Cargo.toml | 10 +- node-graph/graph-craft/src/document.rs | 2 +- node-graph/graph-craft/src/proto.rs | 4 +- .../graph-craft/src/wasm_application_io.rs | 3 +- node-graph/libraries/core-types/Cargo.toml | 4 +- node-graph/libraries/core-types/src/lib.rs | 3 +- node-graph/libraries/core-types/src/text.rs | 3 +- node-graph/libraries/core-types/src/types.rs | 10 +- node-graph/libraries/core-types/src/uuid.rs | 13 +- node-graph/libraries/graphic-types/Cargo.toml | 8 +- node-graph/libraries/no-std-types/Cargo.toml | 7 +- .../libraries/no-std-types/src/blending.rs | 6 +- .../no-std-types/src/color/color_types.rs | 9 +- node-graph/libraries/raster-types/Cargo.toml | 4 +- .../libraries/raster-types/src/image.rs | 14 +- node-graph/libraries/vector-types/Cargo.toml | 4 +- .../libraries/vector-types/src/gradient.rs | 9 +- .../libraries/vector-types/src/vector/misc.rs | 24 +- .../src/vector/reference_point.rs | 3 +- .../vector-types/src/vector/style.rs | 30 +- node-graph/nodes/gcore/Cargo.toml | 10 +- node-graph/nodes/gcore/src/extract_xy.rs | 3 +- node-graph/nodes/gstd/Cargo.toml | 8 + node-graph/nodes/path-bool/Cargo.toml | 8 +- node-graph/nodes/path-bool/src/lib.rs | 6 +- node-graph/nodes/raster/Cargo.toml | 19 +- node-graph/nodes/raster/src/adjustments.rs | 30 +- node-graph/nodes/raster/src/curve.rs | 6 +- node-graph/nodes/text/Cargo.toml | 3 + node-graph/nodes/text/src/font_cache.rs | 6 +- node-graph/nodes/text/src/lib.rs | 6 +- node-graph/nodes/vector/Cargo.toml | 3 + .../nodes/vector/src/generator_nodes.rs | 5 +- 98 files changed, 1493 insertions(+), 1229 deletions(-) delete mode 100644 editor/src/generate_ts_types.rs create mode 100644 frontend/src/messages_old.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index d1c8a4ae1d..dcb0005d38 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -39,8 +39,7 @@ "rust-analyzer.check.command": "clippy", "rust-analyzer.cargo.allTargets": false, "rust-analyzer.procMacro.ignored": { - "serde_derive": ["Serialize", "Deserialize"], - "specta_macros": ["Type"] // Disabled because of: https://github.com/specta-rs/specta/issues/387 + "serde_derive": ["Serialize", "Deserialize"] }, // ESLint config "eslint.format.enable": true, diff --git a/Cargo.lock b/Cargo.lock index 2fd2e3d2b0..853d170aa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - [[package]] name = "ab_glyph" version = "0.2.31" @@ -1157,9 +1151,10 @@ dependencies = [ "serde", "serde_json", "skrifa 0.40.0", - "specta", "tinyvec", "tokio", + "tsify", + "wasm-bindgen", ] [[package]] @@ -2290,9 +2285,9 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "serde_json", - "specta", "text-nodes", "tokio", + "tsify", "url", "vector-nodes", "wasm-bindgen", @@ -2348,7 +2343,8 @@ dependencies = [ "raster-types", "serde", "serde_json", - "specta", + "tsify", + "wasm-bindgen", ] [[package]] @@ -2414,8 +2410,8 @@ dependencies = [ "raster-types", "serde", "serde_json", - "specta", "vector-types", + "wasm-bindgen", ] [[package]] @@ -2538,11 +2534,12 @@ dependencies = [ "once_cell", "preprocessor", "serde", + "serde_bytes", "serde_json", - "specta", "spin", "thiserror 2.0.18", "tokio", + "tsify", "usvg", "vello", "wasm-bindgen", @@ -3740,8 +3737,9 @@ dependencies = [ "num-traits", "num_enum", "serde", - "specta", "spirv-std", + "tsify", + "wasm-bindgen", ] [[package]] @@ -4295,8 +4293,9 @@ dependencies = [ "node-macro", "path-bool", "serde", - "specta", + "tsify", "vector-types", + "wasm-bindgen", ] [[package]] @@ -4919,10 +4918,11 @@ dependencies = [ "raster-nodes-shaders", "raster-types", "serde", - "specta", "spirv-std", "tokio", + "tsify", "vector-types", + "wasm-bindgen", "wgpu-executor", ] @@ -4955,7 +4955,8 @@ dependencies = [ "node-macro", "serde", "serde_json", - "specta", + "tsify", + "wasm-bindgen", "wgpu", ] @@ -5639,6 +5640,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -5659,6 +5670,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "serde_json" version = "1.0.143" @@ -5905,29 +5927,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "specta" -version = "2.0.0-rc.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971" -dependencies = [ - "glam", - "specta-macros", - "thiserror 1.0.69", -] - -[[package]] -name = "specta-macros" -version = "2.0.0-rc.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0074b9e30ed84c6924eb63ad8d2fe71cdc82628525d84b1fcb1f2fd40676517" -dependencies = [ - "Inflector", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "spin" version = "0.10.0" @@ -6245,7 +6244,9 @@ dependencies = [ "parley", "serde", "skrifa 0.40.0", + "tsify", "vector-types", + "wasm-bindgen", ] [[package]] @@ -6665,6 +6666,30 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tsify" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ec91b85e6c6592ed28636cb1dd1fac377ecbbeb170ff1d79f97aac5e38926d" +dependencies = [ + "serde", + "serde-wasm-bindgen", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc2c44dc9fe4baf55b88e032621b7a11b215a1f0a7de8d0aa04367207d915bc" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.106", +] + [[package]] name = "ttf-parser" version = "0.25.1" @@ -6909,7 +6934,9 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "tokio", + "tsify", "vector-types", + "wasm-bindgen", ] [[package]] @@ -6931,8 +6958,9 @@ dependencies = [ "polycool", "rustc-hash 2.1.1", "serde", - "specta", "tinyvec", + "tsify", + "wasm-bindgen", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2277419d99..a7bd74b4f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,6 +98,7 @@ rustc-hash = "2.0" bytemuck = { version = "1.13", features = ["derive", "min_const_generics"] } serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" +serde_bytes = "0.11" serde-wasm-bindgen = "0.6" reqwest = { version = "0.13", features = ["blocking", "json"] } futures = "0.3" @@ -176,11 +177,7 @@ fern = { version = "0.7", features = ["colored"] } num_enum = { version = "0.7", default-features = false } num-derive = "0.4" num-traits = { version = "0.2", default-features = false, features = ["libm"] } -specta = { version = "2.0.0-rc.22", features = [ - "glam", - "derive", - # "typescript", -] } +tsify = { version = "0.5", default-features = false, features = ["js"] } syn = { version = "2.0", default-features = false, features = [ "full", "derive", @@ -230,7 +227,6 @@ graphite-proc-macros = { opt-level = 1 } image = { opt-level = 2 } rustc-hash = { opt-level = 3 } serde_derive = { opt-level = 1 } -specta-macros = { opt-level = 1 } syn = { opt-level = 1 } node-macro = { opt-level = 2 } diff --git a/deny.toml b/deny.toml index ca81b234ac..b411f461d1 100644 --- a/deny.toml +++ b/deny.toml @@ -183,7 +183,7 @@ allow-git = [] [sources.allow-org] # 1 or more github.com organizations to allow git sources for -github = ["linebender", "Rust-GPU", "specta-rs"] +github = ["linebender", "Rust-GPU"] # 1 or more gitlab.com organizations to allow git sources for #gitlab = [""] # 1 or more bitbucket.org organizations to allow git sources for diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 4472507149..4719d5430a 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -29,12 +29,13 @@ log = { workspace = true } bitflags = { workspace = true } thiserror = { workspace = true } serde = { workspace = true } +serde_bytes = { workspace = true } serde_json = { workspace = true } kurbo = { workspace = true } futures = { workspace = true } glam = { workspace = true } derivative = { workspace = true } -specta = { workspace = true } +tsify = { workspace = true } dyn-any = { workspace = true } num_enum = { workspace = true } usvg = { workspace = true } diff --git a/editor/src/generate_ts_types.rs b/editor/src/generate_ts_types.rs deleted file mode 100644 index 659b860f16..0000000000 --- a/editor/src/generate_ts_types.rs +++ /dev/null @@ -1,34 +0,0 @@ -/// Running this test will generate a `types.ts` file at the root of the repo, -/// containing every type annotated with `specta::Type` -// #[cfg(all(test, feature = "specta-export"))] -#[ignore] -#[test] -fn generate_ts_types() { - // TODO: Un-comment this out when we figure out how to reenable the "typescript` Specta feature flag - - // use crate::messages::prelude::FrontendMessage; - // use specta::ts::{export_named_datatype, BigIntExportBehavior, ExportConfig}; - // use specta::{NamedType, TypeMap}; - // use std::fs::File; - // use std::io::Write; - - // let config = ExportConfig::new().bigint(BigIntExportBehavior::Number); - - // let mut type_map = TypeMap::default(); - - // let datatype = FrontendMessage::definition_named_data_type(&mut type_map); - - // let mut export = String::new(); - - // export += &export_named_datatype(&config, &datatype, &type_map).unwrap(); - - // type_map - // .iter() - // .map(|(_, v)| v) - // .flat_map(|v| export_named_datatype(&config, v, &type_map)) - // .for_each(|e| export += &format!("\n\n{e}")); - - // let mut file = File::create("../types.ts").unwrap(); - - // write!(file, "{export}").ok(); -} diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 2c59945136..df7382343f 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -3,7 +3,6 @@ extern crate graphite_proc_macros; // `macro_use` puts these macros into scope for all descendant code files #[macro_use] mod macros; -mod generate_ts_types; #[macro_use] extern crate log; diff --git a/editor/src/messages/app_window/app_window_message_handler.rs b/editor/src/messages/app_window/app_window_message_handler.rs index fd7b05f630..3c4d4ad656 100644 --- a/editor/src/messages/app_window/app_window_message_handler.rs +++ b/editor/src/messages/app_window/app_window_message_handler.rs @@ -11,37 +11,46 @@ impl MessageHandler for AppWindowMessageHandler { fn process_message(&mut self, message: AppWindowMessage, responses: &mut std::collections::VecDeque, _: ()) { match message { AppWindowMessage::PointerLock => { + #[cfg(not(target_family = "wasm"))] responses.add(FrontendMessage::WindowPointerLock); } AppWindowMessage::PointerLockMove { x, y } => { - responses.add(FrontendMessage::WindowPointerLockMove { x, y }); + responses.add(FrontendMessage::WindowPointerLockMove { position: (x, y) }); } AppWindowMessage::Close => { + #[cfg(not(target_family = "wasm"))] responses.add(FrontendMessage::WindowClose); } AppWindowMessage::Minimize => { + #[cfg(not(target_family = "wasm"))] responses.add(FrontendMessage::WindowMinimize); } AppWindowMessage::Maximize => { + #[cfg(not(target_family = "wasm"))] responses.add(FrontendMessage::WindowMaximize); } AppWindowMessage::Fullscreen => { responses.add(FrontendMessage::WindowFullscreen); } AppWindowMessage::Drag => { + #[cfg(not(target_family = "wasm"))] responses.add(FrontendMessage::WindowDrag); } AppWindowMessage::Hide => { + #[cfg(not(target_family = "wasm"))] responses.add(FrontendMessage::WindowHide); } AppWindowMessage::HideOthers => { + #[cfg(not(target_family = "wasm"))] responses.add(FrontendMessage::WindowHideOthers); } AppWindowMessage::ShowAll => { + #[cfg(not(target_family = "wasm"))] responses.add(FrontendMessage::WindowShowAll); } AppWindowMessage::Restart => { responses.add(PortfolioMessage::AutoSaveAllDocuments); + #[cfg(not(target_family = "wasm"))] responses.add(FrontendMessage::WindowRestart); } } @@ -57,7 +66,8 @@ impl MessageHandler for AppWindowMessageHandler { ); } -#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)] pub enum AppWindowPlatform { #[default] Web, diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 5126b9ccb7..dbba09371d 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -4,12 +4,12 @@ use crate::messages::frontend::utility_types::EyedropperPreviewImage; use crate::messages::input_mapper::utility_types::misc::ActionShortcut; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::{ - BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, NodeGraphErrorDiagnostic, Transform, + BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, NodeGraphErrorDiagnostic, }; use crate::messages::portfolio::document::utility_types::nodes::{LayerPanelEntry, LayerStructureEntry}; use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; use crate::messages::prelude::*; -use glam::IVec2; +use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary; use graph_craft::document::NodeId; use graphene_std::raster::Image; use graphene_std::raster::color::Color; @@ -20,7 +20,8 @@ use std::path::PathBuf; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; #[impl_message(Message, Frontend)] -#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize)] #[derivative(Debug, PartialEq)] pub enum FrontendMessage { // Display prefix: make the frontend show something, like a dialog @@ -41,7 +42,7 @@ pub enum FrontendMessage { font_size: f64, color: String, #[serde(rename = "fontData")] - font_data: Vec, + font_data: serde_bytes::ByteBuf, transform: [f64; 6], #[serde(rename = "maxWidth")] max_width: Option, @@ -51,7 +52,7 @@ pub enum FrontendMessage { }, DisplayEditableTextboxUpdateFontData { #[serde(rename = "fontData")] - font_data: Vec, + font_data: serde_bytes::ByteBuf, }, DisplayEditableTextboxTransform { transform: [f64; 6], @@ -87,11 +88,11 @@ pub enum FrontendMessage { document_id: DocumentId, name: String, path: Option, - content: Vec, + content: serde_bytes::ByteBuf, }, TriggerSaveFile { name: String, - content: Vec, + content: serde_bytes::ByteBuf, }, TriggerExportImage { svg: String, @@ -125,6 +126,7 @@ pub enum FrontendMessage { TriggerOpen, TriggerImport, TriggerSavePreferences { + #[tsify(type = "unknown")] preferences: PreferencesMessageHandler, }, TriggerSaveActiveDocument { @@ -152,9 +154,8 @@ pub enum FrontendMessage { document_id: DocumentId, }, UpdateGradientStopColorPickerPosition { - color: Color, - x: f64, - y: f64, + color: Color, // TODO: Color (without `none`) -> Color (with `none`) + position: (f64, f64), }, UpdateImportsExports { /// If the primary import is not visible, then it is None. @@ -163,10 +164,10 @@ pub enum FrontendMessage { exports: Vec>, /// The primary import location. #[serde(rename = "importPosition")] - import_position: IVec2, + import_position: (i32, i32), /// The primary export location. #[serde(rename = "exportPosition")] - export_position: IVec2, + export_position: (i32, i32), /// The document network does not have an add import or export button. #[serde(rename = "addImportExport")] add_import_export: bool, @@ -202,7 +203,7 @@ pub enum FrontendMessage { UpdateLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, - diff: Vec, + diff: Vec, // TODO: Align this with what's generated }, UpdateImportReorderIndex { #[serde(rename = "importIndex")] @@ -223,6 +224,8 @@ pub enum FrontendMessage { UpdateDocumentArtwork { svg: String, }, + // This message is intercepted before being sent to the frontend + #[serde(skip)] UpdateImageData { image_data: Vec<(u64, Image)>, }, @@ -253,7 +256,7 @@ pub enum FrontendMessage { #[serde(rename = "secondaryColor")] secondary_color: String, #[serde(rename = "setColorChoice")] - set_color_choice: Option, + set_color_choice: Option, }, UpdateGraphFadeArtwork { percentage: f64, @@ -278,7 +281,8 @@ pub enum FrontendMessage { selected: Vec, }, UpdateNodeGraphTransform { - transform: Transform, + translation: (f64, f64), + scale: f64, }, UpdateNodeThumbnail { id: NodeId, @@ -304,6 +308,7 @@ pub enum FrontendMessage { UpdateViewportHolePunch { active: bool, }, + #[cfg(not(target_family = "wasm"))] UpdateViewportPhysicalBounds { x: f64, y: f64, @@ -322,18 +327,26 @@ pub enum FrontendMessage { }, // Window prefix: cause the application window to do something + #[cfg(not(target_family = "wasm"))] WindowPointerLock, WindowPointerLockMove { - x: f64, - y: f64, + position: (f64, f64), }, + #[cfg(not(target_family = "wasm"))] WindowClose, + #[cfg(not(target_family = "wasm"))] WindowMinimize, + #[cfg(not(target_family = "wasm"))] WindowMaximize, WindowFullscreen, + #[cfg(not(target_family = "wasm"))] WindowDrag, + #[cfg(not(target_family = "wasm"))] WindowHide, + #[cfg(not(target_family = "wasm"))] WindowHideOthers, + #[cfg(not(target_family = "wasm"))] WindowShowAll, + #[cfg(not(target_family = "wasm"))] WindowRestart, } diff --git a/editor/src/messages/frontend/utility_types.rs b/editor/src/messages/frontend/utility_types.rs index 31d2866fd1..60463c4729 100644 --- a/editor/src/messages/frontend/utility_types.rs +++ b/editor/src/messages/frontend/utility_types.rs @@ -3,13 +3,15 @@ use std::path::PathBuf; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::prelude::*; -#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct OpenDocument { pub id: DocumentId, pub details: DocumentDetails, } -#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DocumentDetails { pub name: String, pub path: Option, @@ -19,7 +21,8 @@ pub struct DocumentDetails { pub is_auto_saved: bool, } -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub enum MouseCursorIcon { #[default] Default, @@ -37,7 +40,8 @@ pub enum MouseCursorIcon { Rotate, } -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub enum FileType { #[default] Png, @@ -55,7 +59,8 @@ impl FileType { } } -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub enum ExportBounds { #[default] AllArtwork, @@ -63,9 +68,11 @@ pub enum ExportBounds { Artboard(LayerNodeIdentifier), } -#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[tsify(large_number_types_as_bigints)] +#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct EyedropperPreviewImage { - pub data: Vec, + pub data: serde_bytes::ByteBuf, pub width: u32, pub height: u32, } diff --git a/editor/src/messages/input_mapper/utility_types/input_keyboard.rs b/editor/src/messages/input_mapper/utility_types/input_keyboard.rs index e8c9bc533f..b6a2424c52 100644 --- a/editor/src/messages/input_mapper/utility_types/input_keyboard.rs +++ b/editor/src/messages/input_mapper/utility_types/input_keyboard.rs @@ -67,7 +67,8 @@ bitflags! { // (although we ignore the shift key, so the user doesn't have to press `Ctrl Shift +` on a US keyboard), even if the keyboard layout // is for a different locale where the `+` key is somewhere entirely different, shifted or not. This would then also work for numpad `+`. #[impl_message(Message, InputMapperMessage, KeyDown)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type, num_enum::TryFromPrimitive)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, num_enum::TryFromPrimitive)] #[repr(u8)] pub enum Key { // Writing system keys @@ -379,7 +380,8 @@ impl fmt::Display for KeysGroup { // LabeledKey // ========== -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct LabeledKey { key: Key, label: String, @@ -395,7 +397,8 @@ impl LabeledKey { // MouseMotion // =========== -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum MouseMotion { None, Lmb, @@ -415,7 +418,8 @@ pub enum MouseMotion { // LabeledKeyOrMouseMotion // ======================= -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[serde(untagged)] pub enum LabeledKeyOrMouseMotion { Key(LabeledKey), @@ -437,7 +441,8 @@ impl From for LabeledKeyOrMouseMotion { // LabeledShortcut // =============== -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct LabeledShortcut(pub Vec); impl From for LabeledShortcut { diff --git a/editor/src/messages/input_mapper/utility_types/input_mouse.rs b/editor/src/messages/input_mapper/utility_types/input_mouse.rs index 6fba502779..420a3bdd46 100644 --- a/editor/src/messages/input_mapper/utility_types/input_mouse.rs +++ b/editor/src/messages/input_mapper/utility_types/input_mouse.rs @@ -110,7 +110,8 @@ bitflags! { } #[impl_message(Message, InputMapperMessage, DoubleClick)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type, num_enum::TryFromPrimitive)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, num_enum::TryFromPrimitive)] #[repr(u8)] pub enum MouseButton { Left, diff --git a/editor/src/messages/input_mapper/utility_types/misc.rs b/editor/src/messages/input_mapper/utility_types/misc.rs index 2470e7a719..ea0bfc0049 100644 --- a/editor/src/messages/input_mapper/utility_types/misc.rs +++ b/editor/src/messages/input_mapper/utility_types/misc.rs @@ -106,8 +106,10 @@ pub struct MappingEntry { pub disabled: bool, } -#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub enum ActionShortcut { + #[serde(skip)] Action(MessageDiscriminant), #[serde(rename = "shortcut")] Shortcut(LabeledShortcut), diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index fef1061f27..55db2189df 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -10,7 +10,9 @@ use std::hash::{Hash, Hasher}; use std::sync::Arc; #[repr(transparent)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[tsify(large_number_types_as_bigints)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] pub struct WidgetId(pub u64); impl core::fmt::Display for WidgetId { @@ -19,7 +21,8 @@ impl core::fmt::Display for WidgetId { } } -#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, serde::Serialize, serde::Deserialize)] #[repr(u8)] pub enum LayoutTarget { /// The spreadsheet panel allows for the visualisation of data in the graph. @@ -59,6 +62,7 @@ pub enum LayoutTarget { // KEEP THIS ENUM LAST // This is a marker that is used to define an array that is used to hold widgets + #[serde(skip)] _LayoutTargetLength, } @@ -151,7 +155,8 @@ fn compute_checkbox_id(layout_target: LayoutTarget, widget_path: &[usize], widge } /// Contains an arrangement of widgets mounted somewhere specific in the frontend. -#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct Layout(pub Vec); impl Layout { @@ -316,7 +321,8 @@ impl<'a> Iterator for WidgetIterMut<'a> { } } -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum LayoutGroup { #[serde(rename = "column")] Column { @@ -552,7 +558,8 @@ impl Diffable for LayoutGroup { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct WidgetInstance { #[serde(rename = "widgetId")] pub widget_id: WidgetId, @@ -674,9 +681,8 @@ impl Diffable for WidgetInstance { } } -#[derive(Clone, specta::Type)] +#[derive(Clone)] pub struct WidgetCallback { - #[specta(skip)] pub callback: Arc Message + 'static + Send + Sync>, } @@ -692,7 +698,8 @@ impl Default for WidgetCallback { } } -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum Widget { BreadcrumbTrailButtons(BreadcrumbTrailButtons), CheckboxInput(CheckboxInput), @@ -719,7 +726,9 @@ pub enum Widget { } /// A single change to part of the UI, containing the location of the change and the new value. -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[tsify(large_number_types_as_bigints)] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct WidgetDiff { /// A path to the change /// e.g. [0, 1, 2] in the properties panel is the first section, second row and third widget. @@ -732,7 +741,8 @@ pub struct WidgetDiff { } /// The new value of the UI, sent as part of a diff. -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum DiffUpdate { #[serde(rename = "layout")] Layout(Layout), diff --git a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs index 54a1e5ba1d..c27ff2b3d4 100644 --- a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs @@ -6,7 +6,8 @@ use derivative::*; use graphene_std::vector::style::FillChoice; use graphite_proc_macros::WidgetBuilder; -#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct IconButton { // Content @@ -38,7 +39,8 @@ pub struct IconButton { pub on_commit: WidgetCallback<()>, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct PopoverButton { // Content @@ -63,7 +65,8 @@ pub struct PopoverButton { pub tooltip_shortcut: Option, } -#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub enum MenuDirection { Top, #[default] @@ -77,7 +80,8 @@ pub enum MenuDirection { Center, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct ParameterExposeButton { // Content @@ -102,7 +106,8 @@ pub struct ParameterExposeButton { pub on_commit: WidgetCallback<()>, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct TextButton { // Content @@ -146,7 +151,8 @@ pub struct TextButton { pub on_commit: WidgetCallback<()>, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct ImageButton { // Content @@ -172,7 +178,8 @@ pub struct ImageButton { pub on_commit: WidgetCallback<()>, } -#[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct ColorInput { // Content @@ -207,7 +214,8 @@ pub struct ColorInput { pub on_commit: WidgetCallback<()>, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct BreadcrumbTrailButtons { // Content diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index c4b8773a09..5e1d80816f 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -7,7 +7,8 @@ use graphene_std::raster::curve::Curve; use graphene_std::transform::ReferencePoint; use graphite_proc_macros::WidgetBuilder; -#[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder)] #[derivative(Debug, Default, PartialEq)] pub struct CheckboxInput { // Content @@ -36,6 +37,7 @@ pub struct CheckboxInput { pub on_commit: WidgetCallback<()>, } +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] pub struct CheckboxId(pub u64); @@ -49,14 +51,9 @@ impl Default for CheckboxId { Self::new() } } -impl specta::Type for CheckboxId { - fn inline(_type_map: &mut specta::TypeCollection, _generics: specta::Generics) -> specta::datatype::DataType { - // TODO: This might not be right, but it works for now. We just need the type `bigint | undefined`. - specta::datatype::DataType::Primitive(specta::datatype::PrimitiveType::u64) - } -} -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct DropdownInput { // Content @@ -102,7 +99,8 @@ pub struct DropdownInput { pub type MenuListEntrySections = Vec>; -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] #[widget_builder(not_widget_instance)] pub struct MenuListEntry { @@ -148,7 +146,8 @@ impl std::hash::Hash for MenuListEntry { } } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct NumberInput { // Content @@ -246,7 +245,8 @@ impl NumberInput { } } -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, Default, PartialEq, Eq, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, Default, PartialEq, Eq)] pub enum NumberInputIncrementBehavior { #[default] Add, @@ -254,14 +254,16 @@ pub enum NumberInputIncrementBehavior { Callback, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, Default, PartialEq, Eq, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, Default, PartialEq, Eq)] pub enum NumberInputMode { #[default] Increment, Range, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct NodeCatalog { // Content @@ -280,7 +282,8 @@ pub struct NodeCatalog { pub on_commit: WidgetCallback<()>, } -#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct RadioInput { // Content @@ -303,7 +306,8 @@ pub struct RadioInput { // Callbacks exists on the `RadioEntryData` children, not this parent `RadioInput` } -#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder)] #[derivative(Debug, PartialEq)] #[widget_builder(not_widget_instance)] pub struct RadioEntryData { @@ -330,7 +334,8 @@ pub struct RadioEntryData { pub on_commit: WidgetCallback<()>, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct WorkingColorsInput { // Content @@ -340,7 +345,8 @@ pub struct WorkingColorsInput { pub secondary: Color, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct TextAreaInput { // Content @@ -366,7 +372,8 @@ pub struct TextAreaInput { pub on_commit: WidgetCallback<()>, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct TextInput { // Content @@ -403,7 +410,8 @@ pub struct TextInput { pub on_commit: WidgetCallback<()>, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder)] #[derivative(Debug, PartialEq, Default)] pub struct CurveInput { // Content @@ -427,7 +435,8 @@ pub struct CurveInput { pub on_commit: WidgetCallback<()>, } -#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct ReferencePointInput { // Content diff --git a/editor/src/messages/layout/utility_types/widgets/label_widgets.rs b/editor/src/messages/layout/utility_types/widgets/label_widgets.rs index deea81e0b1..fde97daec8 100644 --- a/editor/src/messages/layout/utility_types/widgets/label_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/label_widgets.rs @@ -3,7 +3,8 @@ use crate::messages::input_mapper::utility_types::misc::ActionShortcut; use derivative::*; use graphite_proc_macros::WidgetBuilder; -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Debug, Default, PartialEq, Eq, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Debug, Default, PartialEq, Eq, WidgetBuilder)] pub struct IconLabel { // Content #[widget_builder(constructor)] @@ -19,7 +20,8 @@ pub struct IconLabel { pub tooltip_shortcut: Option, } -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, WidgetBuilder)] pub struct Separator { // Content pub direction: SeparatorDirection, @@ -27,14 +29,16 @@ pub struct Separator { pub style: SeparatorStyle, } -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum SeparatorDirection { #[default] Horizontal, Vertical, } -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum SeparatorStyle { Related, #[default] @@ -42,7 +46,8 @@ pub enum SeparatorStyle { Section, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Debug, Eq, Default, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Debug, Eq, Default, WidgetBuilder)] #[derivative(PartialEq)] pub struct TextLabel { // Content @@ -78,7 +83,8 @@ pub struct TextLabel { pub tooltip_shortcut: Option, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct ImageLabel { // Content @@ -96,7 +102,8 @@ pub struct ImageLabel { pub tooltip_shortcut: Option, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] pub struct ShortcutLabel { // Content diff --git a/editor/src/messages/message.rs b/editor/src/messages/message.rs index cc24a00afc..385b2b455d 100644 --- a/editor/src/messages/message.rs +++ b/editor/src/messages/message.rs @@ -45,14 +45,6 @@ pub enum Message { NoOp, } -/// Provides an impl of `specta::Type` for `MessageDiscriminant`, the struct created by `impl_message`. -/// Specta isn't integrated with `impl_message`, so a remote impl must be provided using this struct. -impl specta::Type for MessageDiscriminant { - fn inline(_type_map: &mut specta::TypeCollection, _generics: specta::Generics) -> specta::DataType { - specta::DataType::Any - } -} - impl Message { pub fn message_tree() -> DebugMessageTree { Self::build_message_tree() diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 76fbfa4828..bef3980ddd 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1,5 +1,4 @@ use super::node_graph::document_node_definitions; -use super::node_graph::utility_types::Transform; use super::utility_types::error::EditorError; use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS, SnappingOptions, SnappingState}; use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus}; @@ -848,7 +847,7 @@ impl MessageHandler> for DocumentMes document_id, name: format!("{}.{}", self.name.clone(), FILE_EXTENSION), path: self.path.clone(), - content: self.serialize_document().into_bytes(), + content: self.serialize_document().into_bytes().into(), }) } DocumentMessage::SavedDocument { path } => { @@ -1332,11 +1331,8 @@ impl MessageHandler> for DocumentMes responses.add(NodeGraphMessage::UpdateImportsExports); responses.add(FrontendMessage::UpdateNodeGraphTransform { - transform: Transform { - scale: transform.matrix2.x_axis.x, - x: transform.translation.x, - y: transform.translation.y, - }, + translation: transform.translation.into(), + scale: transform.matrix2.x_axis.x, }) } } diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index ed7f440839..84a462e537 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -57,7 +57,8 @@ impl NodePropertiesContext<'_> { /// The key used to access definitions for a network node or proto node. /// For proto nodes, this is their [`ProtoNodeIdentifier`]. /// For network nodes, it doesn't necessarily have to be the same as the network's display name, but it often is. -#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(tag = "type", content = "data")] pub enum DefinitionIdentifier { ProtoNode(ProtoNodeIdentifier), diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 6945296c25..aae3db3e9e 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -847,7 +847,7 @@ impl<'a> MessageHandler> for NodeG }; self.context_menu = Some(ContextMenuInformation { - context_menu_coordinates: (node_graph_point + node_graph_shift).as_ivec2(), + context_menu_coordinates: (node_graph_point + node_graph_shift).as_ivec2().into(), context_menu_data, }); @@ -1280,7 +1280,7 @@ impl<'a> MessageHandler> for NodeG let compatible_type = network_interface.output_type(&output_connector, selection_network_path).add_node_string(); self.context_menu = Some(ContextMenuInformation { - context_menu_coordinates: (point + node_graph_shift).as_ivec2(), + context_menu_coordinates: (point + node_graph_shift).as_ivec2().into(), context_menu_data: ContextMenuData::CreateNode { compatible_type }, }); @@ -2050,8 +2050,8 @@ impl<'a> MessageHandler> for NodeG responses.add(FrontendMessage::UpdateImportsExports { imports, exports, - import_position, - export_position, + import_position: import_position.into(), + export_position: export_position.into(), add_import_export, }); } @@ -2651,7 +2651,7 @@ impl NodeGraphMessageHandler { exposed_outputs, primary_output_connected_to_layer, primary_input_connected_to_layer, - position, + position: position.into(), previewed, visible, locked, @@ -2695,6 +2695,7 @@ impl NodeGraphMessageHandler { if network_interface.is_layer(&error_node, breadcrumb_network_path) { position += IVec2::new(12, -12) } + let position = position.into(); Some(NodeGraphErrorDiagnostic { position, error }) } diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index 620481a2c1..67ac7a1197 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -1,9 +1,9 @@ -use glam::IVec2; use graph_craft::document::NodeId; use graph_craft::document::value::TaggedValue; use graphene_std::Type; -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] pub enum FrontendGraphDataType { #[default] General, @@ -42,7 +42,8 @@ impl FrontendGraphDataType { } } -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct FrontendGraphInput { #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, @@ -57,21 +58,23 @@ pub struct FrontendGraphInput { pub connected_to: String, } -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct FrontendGraphOutput { #[serde(rename = "dataType")] pub data_type: FrontendGraphDataType, pub name: String, + pub description: String, #[serde(rename = "resolvedType")] pub resolved_type: String, - pub description: String, /// If connected to an export, it is "export index {index}". /// If connected to a node, it is "{node name} input {input_index}". #[serde(rename = "connectedTo")] pub connected_to: Vec, } -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct FrontendNode { pub id: graph_craft::document::NodeId, #[serde(rename = "isLayer")] @@ -95,13 +98,14 @@ pub struct FrontendNode { pub primary_input_connected_to_layer: bool, #[serde(rename = "primaryOutputConnectedToLayer")] pub primary_output_connected_to_layer: bool, - pub position: IVec2, + pub position: (i32, i32), pub previewed: bool, pub visible: bool, pub locked: bool, } -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct FrontendNodeType { pub identifier: String, pub name: String, @@ -110,7 +114,8 @@ pub struct FrontendNodeType { pub input_types: Vec, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct DragStart { pub start_x: f64, pub start_y: f64, @@ -118,14 +123,8 @@ pub struct DragStart { pub round_y: i32, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct Transform { - pub scale: f64, - pub x: f64, - pub y: f64, -} - -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct BoxSelection { #[serde(rename = "startX")] pub start_x: u32, @@ -137,7 +136,8 @@ pub struct BoxSelection { pub end_y: u32, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(tag = "type", content = "data")] pub enum ContextMenuData { ModifyNode { @@ -158,22 +158,25 @@ pub enum ContextMenuData { }, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ContextMenuInformation { // Stores whether the context menu is open and its position in graph coordinates #[serde(rename = "contextMenuCoordinates")] - pub context_menu_coordinates: IVec2, + pub context_menu_coordinates: (i32, i32), #[serde(rename = "contextMenuData")] pub context_menu_data: ContextMenuData, } -#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize)] pub struct NodeGraphErrorDiagnostic { - pub position: IVec2, + pub position: (i32, i32), pub error: String, } -#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize)] pub struct FrontendClickTargets { #[serde(rename = "nodeClickTargets")] pub node_click_targets: Vec, @@ -189,7 +192,8 @@ pub struct FrontendClickTargets { pub modify_import_export: Vec, } -#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub enum Direction { Up, Down, diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index 5f854a7796..9991d39c57 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -42,7 +42,8 @@ pub enum GizmoEmphasis { // TODO Remove duplicated definition of this in `utility_types_web.rs` /// Types of overlays used by DocumentMessage to enable/disable the selected set of viewport overlays. -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum OverlaysType { ArtboardName, CompassRose, @@ -60,7 +61,8 @@ pub enum OverlaysType { } // TODO Remove duplicated definition of this in `utility_types_web.rs` -#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] #[serde(default)] pub struct OverlaysVisibilitySettings { pub all: bool, @@ -160,11 +162,11 @@ impl OverlaysVisibilitySettings { } } -#[derive(serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(serde::Serialize, serde::Deserialize)] pub struct OverlayContext { // Serde functionality isn't used but is required by the message system macros #[serde(skip)] - #[specta(skip)] internal: Arc>, pub viewport: ViewportMessageHandler, pub visibility_settings: OverlaysVisibilitySettings, diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs index 3c249dcbf2..c03ba387d3 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs @@ -35,7 +35,8 @@ pub enum GizmoEmphasis { } /// Types of overlays used by DocumentMessage to enable/disable the selected set of viewport overlays. -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum OverlaysType { ArtboardName, CompassRose, @@ -52,7 +53,8 @@ pub enum OverlaysType { Handles, } -#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] #[serde(default)] pub struct OverlaysVisibilitySettings { pub all: bool, @@ -150,11 +152,11 @@ impl OverlaysVisibilitySettings { } } -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct OverlayContext { // Serde functionality isn't used but is required by the message system macros #[serde(skip, default = "overlay_canvas_context")] - #[specta(skip)] pub render_context: web_sys::CanvasRenderingContext2d, pub viewport: ViewportMessageHandler, pub visibility_settings: OverlaysVisibilitySettings, diff --git a/editor/src/messages/portfolio/document/utility_types/clipboards.rs b/editor/src/messages/portfolio/document/utility_types/clipboards.rs index ef4b00ae7c..12d9b54654 100644 --- a/editor/src/messages/portfolio/document/utility_types/clipboards.rs +++ b/editor/src/messages/portfolio/document/utility_types/clipboards.rs @@ -2,7 +2,8 @@ use super::network_interface::NodeTemplate; use graph_craft::document::NodeId; #[repr(u8)] -#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq, Debug, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq, Debug)] pub enum Clipboard { Internal, Device, diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index bc7b838eeb..328477a3c7 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -234,7 +234,8 @@ impl DocumentMetadata { // =================== /// ID of a layer node -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)] pub struct LayerNodeIdentifier(NonZeroU64); impl core::fmt::Debug for LayerNodeIdentifier { diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index ef363ae108..3daca8bef4 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -3,7 +3,9 @@ use glam::DVec2; use std::fmt; #[repr(transparent)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[tsify(large_number_types_as_bigints)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] pub struct DocumentId(pub u64); #[derive(PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Hash)] @@ -12,13 +14,15 @@ pub enum FlipAxis { Y, } -#[derive(PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Hash, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Hash)] pub enum AlignAxis { X, Y, } -#[derive(PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Hash, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Hash)] pub enum AlignAggregate { Min, Max, diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 5a1722d53d..ac95dfce84 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -5723,14 +5723,16 @@ impl Iterator for FlowIter<'_> { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum ImportOrExport { Import(usize), Export(usize), } /// Represents an input connector with index based on the [`DocumentNode::inputs`] index, not the visible input index -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum InputConnector { #[serde(rename = "node")] Node { @@ -5770,7 +5772,8 @@ impl InputConnector { } /// Represents an output connector -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum OutputConnector { #[serde(rename = "node")] Node { diff --git a/editor/src/messages/portfolio/document/utility_types/nodes.rs b/editor/src/messages/portfolio/document/utility_types/nodes.rs index 3f4e56715e..85fd1bd0f0 100644 --- a/editor/src/messages/portfolio/document/utility_types/nodes.rs +++ b/editor/src/messages/portfolio/document/utility_types/nodes.rs @@ -6,14 +6,16 @@ use graph_craft::document::{NodeId, NodeNetwork}; /// Represents an entry in the layer tree hierarchy, sent to the frontend. /// Each entry contains its layer ID and a list of its visible children. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct LayerStructureEntry { #[serde(rename = "layerId")] pub layer_id: NodeId, pub children: Vec, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct LayerPanelEntry { pub id: NodeId, #[serde(rename = "implementationName")] @@ -47,7 +49,8 @@ pub struct LayerPanelEntry { } /// IMPORTANT: the same node may appear multiple times. -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct SelectedNodes(pub Vec); impl SelectedNodes { @@ -157,5 +160,6 @@ impl SelectedNodes { } } -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct CollapsedLayers(pub Vec); diff --git a/editor/src/messages/portfolio/document/utility_types/wires.rs b/editor/src/messages/portfolio/document/utility_types/wires.rs index 7aad6977db..439962ad98 100644 --- a/editor/src/messages/portfolio/document/utility_types/wires.rs +++ b/editor/src/messages/portfolio/document/utility_types/wires.rs @@ -3,7 +3,8 @@ use glam::{DVec2, IVec2}; use graphene_std::{uuid::NodeId, vector::misc::dvec2_to_point}; use kurbo::{BezPath, DEFAULT_ACCURACY, Line, Point, Shape}; -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct WirePath { #[serde(rename = "pathString")] pub path_string: String, @@ -13,7 +14,8 @@ pub struct WirePath { pub dashed: bool, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct WirePathUpdate { pub id: NodeId, #[serde(rename = "inputIndex")] @@ -23,7 +25,8 @@ pub struct WirePathUpdate { pub wire_path_update: Option, } -#[derive(Copy, Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Copy, Clone, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize)] pub enum GraphWireStyle { #[default] Direct = 0, diff --git a/editor/src/messages/preferences/preferences_message_handler.rs b/editor/src/messages/preferences/preferences_message_handler.rs index 9fb3b4cd16..8431934b4e 100644 --- a/editor/src/messages/preferences/preferences_message_handler.rs +++ b/editor/src/messages/preferences/preferences_message_handler.rs @@ -11,7 +11,8 @@ pub struct PreferencesMessageContext<'a> { pub tool_message_handler: &'a ToolMessageHandler, } -#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type, ExtractField)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, ExtractField)] #[serde(default)] pub struct PreferencesMessageHandler { pub selection_mode: SelectionMode, diff --git a/editor/src/messages/preferences/utility_types.rs b/editor/src/messages/preferences/utility_types.rs index dabcb5b7e4..2cada9cced 100644 --- a/editor/src/messages/preferences/utility_types.rs +++ b/editor/src/messages/preferences/utility_types.rs @@ -1,4 +1,5 @@ -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type, Hash)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash)] pub enum SelectionMode { #[default] Touched = 0, diff --git a/editor/src/messages/tool/common_functionality/color_selector.rs b/editor/src/messages/tool/common_functionality/color_selector.rs index 5b65c8c3a6..c9ac746768 100644 --- a/editor/src/messages/tool/common_functionality/color_selector.rs +++ b/editor/src/messages/tool/common_functionality/color_selector.rs @@ -4,7 +4,8 @@ use crate::messages::prelude::*; use graphene_std::Color; use graphene_std::vector::style::FillChoice; -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum ToolColorType { Primary, Secondary, diff --git a/editor/src/messages/tool/common_functionality/pivot.rs b/editor/src/messages/tool/common_functionality/pivot.rs index 7ba3e7c760..072a743545 100644 --- a/editor/src/messages/tool/common_functionality/pivot.rs +++ b/editor/src/messages/tool/common_functionality/pivot.rs @@ -150,7 +150,8 @@ impl PivotGizmo { } } -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize)] pub enum PivotGizmoType { // Pivot #[default] @@ -161,7 +162,8 @@ pub enum PivotGizmoType { // TODO: Add "Individual" } -#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Hash, serde::Serialize, serde::Deserialize)] pub struct PivotGizmoState { pub enabled: bool, pub gizmo_type: PivotGizmoType, diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index 46baf0f1d4..6e1d3f22dc 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -23,7 +23,8 @@ use kurbo::{BezPath, PathEl, Shape}; use std::collections::VecDeque; use std::f64::consts::{PI, TAU}; -#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, serde::Serialize, serde::Deserialize)] pub enum ShapeType { #[default] Polygon = 0, diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index e257dc359c..0df694050e 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -22,7 +22,8 @@ pub struct ArtboardTool { } #[impl_message(Message, ToolMessage, Artboard)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum ArtboardToolMessage { // Standard messages Abort, diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index d1b84b49a4..e8f8159e0c 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -13,7 +13,8 @@ use graphene_std::raster::BlendMode; const BRUSH_MAX_SIZE: f64 = 5000.; -#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum DrawMode { Draw = 0, Erase, @@ -52,7 +53,8 @@ impl Default for BrushOptions { } #[impl_message(Message, ToolMessage, Brush)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum BrushToolMessage { // Standard messages Abort, @@ -65,7 +67,8 @@ pub enum BrushToolMessage { UpdateOptions { options: BrushToolMessageOptionsUpdate }, } -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum BrushToolMessageOptionsUpdate { BlendMode(BlendMode), ChangeDiameter(f64), diff --git a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs index eddb7cb863..c18abc1362 100644 --- a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs +++ b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs @@ -9,7 +9,8 @@ pub struct EyedropperTool { } #[impl_message(Message, ToolMessage, Eyedropper)] -#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)] pub enum EyedropperToolMessage { // Standard messages Abort, @@ -46,9 +47,9 @@ impl LayoutHolder for EyedropperTool { impl<'a> MessageHandler> for EyedropperTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, context: &mut ToolActionMessageContext<'a>) { if let ToolMessage::Eyedropper(EyedropperToolMessage::PreviewImage { data, width, height }) = message { - let image = EyedropperPreviewImage { data, width, height }; + let image = EyedropperPreviewImage { data: data.into(), width, height }; - update_cursor_preview_common(responses, Some(image), context.input, context.global_tool_data, self.data.color_choice.clone()); + update_cursor_preview_common(responses, Some(image), context.input, context.global_tool_data, self.data.color_choice); if !self.data.preview { disable_cursor_preview(responses, &mut self.data); @@ -87,10 +88,18 @@ enum EyedropperToolFsmState { SamplingSecondary, } +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum PrimarySecondary { + #[default] + Primary, + Secondary, +} + #[derive(Clone, Debug, Default)] struct EyedropperToolData { preview: bool, - color_choice: Option, + color_choice: Option, } impl Fsm for EyedropperToolFsmState { @@ -127,7 +136,11 @@ impl Fsm for EyedropperToolFsmState { } // Sampling -> Ready (EyedropperToolFsmState::SamplingPrimary, EyedropperToolMessage::SamplePrimaryColorEnd) | (EyedropperToolFsmState::SamplingSecondary, EyedropperToolMessage::SampleSecondaryColorEnd) => { - let set_color_choice = if self == EyedropperToolFsmState::SamplingPrimary { "Primary" } else { "Secondary" }.to_string(); + let set_color_choice = match self { + EyedropperToolFsmState::SamplingPrimary => PrimarySecondary::Primary, + EyedropperToolFsmState::SamplingSecondary => PrimarySecondary::Secondary, + _ => unreachable!(), + }; update_cursor_preview(responses, tool_data, input, global_tool_data, Some(set_color_choice)); disable_cursor_preview(responses, tool_data); @@ -185,7 +198,7 @@ fn update_cursor_preview( tool_data: &mut EyedropperToolData, _input: &InputPreprocessorMessageHandler, _global_tool_data: &DocumentToolData, - set_color_choice: Option, + set_color_choice: Option, ) { tool_data.preview = true; tool_data.color_choice = set_color_choice; @@ -198,7 +211,7 @@ fn update_cursor_preview( tool_data: &mut EyedropperToolData, input: &InputPreprocessorMessageHandler, global_tool_data: &DocumentToolData, - set_color_choice: Option, + set_color_choice: Option, ) { tool_data.preview = true; tool_data.color_choice = set_color_choice.clone(); @@ -211,7 +224,7 @@ fn update_cursor_preview_common( image: Option, input: &InputPreprocessorMessageHandler, global_tool_data: &DocumentToolData, - set_color_choice: Option, + set_color_choice: Option, ) { responses.add(FrontendMessage::UpdateEyedropperSamplingState { image, diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 9a069683be..c1a2a8fe75 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -9,7 +9,8 @@ pub struct FillTool { } #[impl_message(Message, ToolMessage, Fill)] -#[derive(PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)] pub enum FillToolMessage { // Standard messages Abort, diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs index b3812b87cd..c6d152417f 100644 --- a/editor/src/messages/tool/tool_messages/freehand_tool.rs +++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs @@ -37,7 +37,8 @@ impl Default for FreehandOptions { } #[impl_message(Message, ToolMessage, Freehand)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum FreehandToolMessage { // Standard messages Overlays { context: OverlayContext }, @@ -51,7 +52,8 @@ pub enum FreehandToolMessage { UpdateOptions { options: FreehandOptionsUpdate }, } -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum FreehandOptionsUpdate { FillColor(Option), FillColorType(ToolColorType), diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 1a3b11a545..c3c8344bf2 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -24,7 +24,8 @@ pub struct GradientOptions { } #[impl_message(Message, ToolMessage, Gradient)] -#[derive(PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)] pub enum GradientToolMessage { // Standard messages Abort, @@ -46,7 +47,8 @@ pub enum GradientToolMessage { UpdateOptions { options: GradientOptionsUpdate }, } -#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)] pub enum GradientOptionsUpdate { Type(GradientType), ReverseStops, @@ -770,8 +772,8 @@ impl Fsm for GradientToolFsmState { if stop_index < gradient.stops.position.len() { let color = gradient.stops.color[stop_index].to_gamma_srgb(); let position = gradient.stops.position[stop_index]; - let DVec2 { x, y } = transform.transform_point2(gradient.start.lerp(gradient.end, position)); - responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color, x, y }); + let position = transform.transform_point2(gradient.start.lerp(gradient.end, position)).into(); + responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color, position }); } } @@ -824,13 +826,10 @@ impl Fsm for GradientToolFsmState { let viewport_pos = selected_gradient .transform .transform_point2(selected_gradient.gradient.start.lerp(selected_gradient.gradient.end, stop_pos)); + let position = viewport_pos.into(); let color = selected_gradient.gradient.stops.color[stop_index].to_gamma_srgb(); tool_data.color_picker_editing_color_stop = Some(stop_index); - responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { - color, - x: viewport_pos.x, - y: viewport_pos.y, - }); + responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color, position }); } } _ => {} diff --git a/editor/src/messages/tool/tool_messages/navigate_tool.rs b/editor/src/messages/tool/tool_messages/navigate_tool.rs index 8809ccdebd..22021a3792 100644 --- a/editor/src/messages/tool/tool_messages/navigate_tool.rs +++ b/editor/src/messages/tool/tool_messages/navigate_tool.rs @@ -7,7 +7,8 @@ pub struct NavigateTool { } #[impl_message(Message, ToolMessage, Navigate)] -#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)] pub enum NavigateToolMessage { // Standard messages Abort, diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 9b46386e6e..145d8975f2 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -50,7 +50,8 @@ pub struct PathToolOptions { } #[impl_message(Message, ToolMessage, Path)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum PathToolMessage { // Standard messages Abort, @@ -155,7 +156,8 @@ pub enum PathToolMessage { ToggleSegmentEditing, } -#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize)] pub enum PathOverlayMode { AllHandles = 0, #[default] @@ -178,7 +180,8 @@ impl Default for PathEditingMode { } } -#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)] pub enum PathOptionsUpdate { OverlayModeType(PathOverlayMode), PointEditingMode { enabled: bool }, diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index ceab12bad3..ba5c4dde27 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -44,7 +44,8 @@ impl Default for PenOptions { } #[impl_message(Message, ToolMessage, Pen)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum PenToolMessage { // Standard messages Abort, @@ -107,13 +108,15 @@ enum PenToolFsmState { GRSHandle, } -#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum PenOverlayMode { AllHandles = 0, FrontierHandles = 1, } -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum PenOptionsUpdate { FillColor(Option), FillColorType(ToolColorType), diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 61012e6a0c..81042a586a 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -42,7 +42,8 @@ pub struct SelectOptions { nested_selection_behavior: NestedSelectionBehavior, } -#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)] pub enum SelectOptionsUpdate { NestedSelectionBehavior(NestedSelectionBehavior), PivotGizmoType(PivotGizmoType), @@ -50,7 +51,8 @@ pub enum SelectOptionsUpdate { TogglePivotPinned, } -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize)] pub enum NestedSelectionBehavior { #[default] Shallowest, @@ -66,7 +68,8 @@ impl fmt::Display for NestedSelectionBehavior { } } -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct SelectToolPointerKeys { pub axis_align: Key, pub snap_angle: Key, @@ -75,7 +78,8 @@ pub struct SelectToolPointerKeys { } #[impl_message(Message, ToolMessage, Select)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum SelectToolMessage { // Standard messages Abort, diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 43895d0d70..61dd308295 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -68,7 +68,8 @@ impl Default for ShapeToolOptions { } } -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum ShapeOptionsUpdate { FillColor(Option), FillColorType(ToolColorType), @@ -88,7 +89,8 @@ pub enum ShapeOptionsUpdate { } #[impl_message(Message, ToolMessage, Shape)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum ShapeToolMessage { // Standard messages Overlays { context: OverlayContext }, diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index 345b5a058e..7726b3413c 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -38,7 +38,8 @@ impl Default for SplineOptions { } #[impl_message(Message, ToolMessage, Spline)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum SplineToolMessage { // Standard messages Overlays { context: OverlayContext }, @@ -65,7 +66,8 @@ enum SplineToolFsmState { MergingEndpoints, } -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum SplineOptionsUpdate { FillColor(Option), FillColorType(ToolColorType), diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index c85b94a4aa..0ca804136f 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -55,7 +55,8 @@ impl Default for TextOptions { } #[impl_message(Message, ToolMessage, Text)] -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum TextToolMessage { // Standard messages Abort, @@ -75,7 +76,8 @@ pub enum TextToolMessage { RefreshEditingFontData, } -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum TextOptionsUpdate { FillColor(Option), FillColorType(ToolColorType), @@ -413,7 +415,7 @@ impl TextToolData { line_height_ratio: editing_text.typesetting.line_height_ratio, font_size: editing_text.typesetting.font_size, color: editing_text.color.map_or("#000000".to_string(), |color| format!("#{}", color.to_rgba_hex_srgb())), - font_data: font_cache.get(&editing_text.font).map(|(data, _)| data.clone()).unwrap_or_default(), + font_data: font_cache.get(&editing_text.font).map(|(data, _)| data.clone()).unwrap_or_default().into(), transform: editing_text.transform.to_cols_array(), max_width: editing_text.typesetting.max_width, max_height: editing_text.typesetting.max_height, @@ -925,7 +927,7 @@ impl Fsm for TextToolFsmState { (TextToolFsmState::Editing, TextToolMessage::RefreshEditingFontData) => { let font = Font::new(tool_options.font.font_family.clone(), tool_options.font.font_style.clone()); responses.add(FrontendMessage::DisplayEditableTextboxUpdateFontData { - font_data: font_cache.get(&font).map(|(data, _)| data.clone()).unwrap_or_default(), + font_data: font_cache.get(&font).map(|(data, _)| data.clone()).unwrap_or_default().into(), }); TextToolFsmState::Editing diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index ead8c52214..3b30777ca9 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -360,7 +360,8 @@ impl ToolFsmState { } #[repr(usize)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default)] pub enum ToolType { // General tool group #[default] @@ -524,7 +525,8 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis } } -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct HintData(pub Vec); impl HintData { @@ -576,10 +578,12 @@ impl HintData { } } -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct HintGroup(pub Vec); -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct HintInfo { /// A `KeysGroup` specifies all the keys pressed simultaneously to perform an action (like "Ctrl C" to copy). /// Usually at most one is given, but less commonly, multiple can be used to describe additional hotkeys not used simultaneously (like the four different arrow keys to nudge a layer). diff --git a/editor/src/messages/viewport/viewport_message_handler.rs b/editor/src/messages/viewport/viewport_message_handler.rs index a060a14efa..8a3196119b 100644 --- a/editor/src/messages/viewport/viewport_message_handler.rs +++ b/editor/src/messages/viewport/viewport_message_handler.rs @@ -3,7 +3,8 @@ use std::ops::{Add, Div, Mul, Sub}; use crate::messages::prelude::*; use crate::messages::tool::tool_messages::tool_prelude::DVec2; -#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type, ExtractField)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize, ExtractField)] pub struct ViewportMessageHandler { bounds: Bounds, // Ratio of logical pixels to physical pixels @@ -157,7 +158,8 @@ pub trait Position { fn y(&self) -> f64; } -#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize)] struct Point { x: f64, y: f64, @@ -183,7 +185,8 @@ impl Position for Point { } } -#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize)] pub struct LogicalPoint { inner: Point, scale: f64, @@ -217,7 +220,8 @@ impl FromWithScale for LogicalPoint { } } -#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize)] pub struct PhysicalPoint { inner: Point, scale: f64, @@ -258,7 +262,8 @@ pub trait Rect: Position { fn height(&self) -> f64; } -#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type, ExtractField)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize, ExtractField)] struct Bounds { offset: Point, size: Point, @@ -286,7 +291,8 @@ impl Rect for Bounds { } } -#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize)] pub struct LogicalBounds { offset: Point, size: Point, @@ -338,7 +344,8 @@ impl FromWithScale for LogicalBounds { } } -#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize)] pub struct PhysicalBounds { offset: Point, size: Point, diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 175540b05b..ce07aead89 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -450,7 +450,10 @@ impl NodeGraphExecutor { .. }) => { if file_type == FileType::Svg { - responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() }); + responses.add(FrontendMessage::TriggerSaveFile { + name, + content: svg.into_bytes().into(), + }); } else { let mime = file_type.to_mime().to_string(); let size = (size * scale_factor).into(); @@ -496,7 +499,7 @@ impl NodeGraphExecutor { } } - responses.add(FrontendMessage::TriggerSaveFile { name, content: encoded }); + responses.add(FrontendMessage::TriggerSaveFile { name, content: encoded.into() }); } _ => { return Err(format!("Incorrect render type for exporting to an SVG ({file_type:?}, {node_graph_output})")); diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 3414876105..f1c9d9d873 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -2,7 +2,7 @@ import { getContext, onMount, onDestroy, tick } from "svelte"; import type { Editor } from "@graphite/editor"; - import type { Color, FrontendMessages, MenuDirection } from "@graphite/messages"; + import type { Color, FrontendMessages, MenuDirection, MouseCursorIcon } from "@graphite/messages"; import type { AppWindowState } from "@graphite/state-providers/app-window"; import type { DocumentState } from "@graphite/state-providers/document"; import { isColor, createColor } from "@graphite/utility-functions/colors"; @@ -35,7 +35,7 @@ // Interactive text editing let textInput: undefined | HTMLDivElement = undefined; let showTextInput: boolean; - let textInputMatrix: number[]; + let textInputMatrix: [number, number, number, number, number, number]; // Scrollbars let scrollbarPos = { x: 0.5, y: 0.5 }; @@ -295,10 +295,9 @@ } // Update mouse cursor icon - export function updateMouseCursor(cursor: string) { - const mouseCursorIconCSSNames: Record = { + export function updateMouseCursor(cursor: MouseCursorIcon) { + const mouseCursorIconCSSNames: Record = { Default: "default", - Alias: "alias", None: "none", ZoomIn: "zoom-in", ZoomOut: "zoom-out", @@ -312,7 +311,7 @@ NWSEResize: "nwse-resize", Rotate: "custom-rotate", }; - let cursorString = mouseCursorIconCSSNames[cursor] || mouseCursorIconCSSNames["Alias"]; + let cursorString = mouseCursorIconCSSNames[cursor] || "alias"; // This isn't very clean but it's good enough for now until we need more icons, then we can build something more robust (consider blob URLs) if (cursor === "Rotate") { @@ -377,9 +376,9 @@ textInputMatrix = data.transform; - const bytes = new Uint8Array(data.fontData); - if (bytes.length > 0) { - window.document.fonts.add(new FontFace("text-font", bytes)); + if (data.fontData.length > 0 && data.fontData.buffer instanceof ArrayBuffer) { + const fontView = new Uint8Array(data.fontData.buffer, data.fontData.byteOffset, data.fontData.byteLength); + window.document.fonts.add(new FontFace("text-font", fontView)); textInput.style.fontFamily = "text-font"; } @@ -473,7 +472,7 @@ // Gradient stop color picker editor.subscriptions.subscribeFrontendMessage("UpdateGradientStopColorPickerPosition", (data) => { gradientStopPickerColor = data.color; - gradientStopPickerPosition = { x: data.x, y: data.y }; + gradientStopPickerPosition = { x: data.position[0], y: data.position[1] }; }); // Update scrollbars and rulers @@ -511,9 +510,9 @@ editor.subscriptions.subscribeFrontendMessage("DisplayEditableTextboxUpdateFontData", async (data) => { await tick(); - const fontData = new Uint8Array(data.fontData); - if (fontData.length > 0 && textInput) { - window.document.fonts.add(new FontFace("text-font", fontData)); + if (textInput && data.fontData.length > 0 && data.fontData.buffer instanceof ArrayBuffer) { + const fontView = new Uint8Array(data.fontData.buffer, data.fontData.byteOffset, data.fontData.byteLength); + window.document.fonts.add(new FontFace("text-font", fontView)); textInput.style.fontFamily = "text-font"; } }); diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 1c6fc6640c..e03d643f03 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -3,6 +3,7 @@ import { SvelteMap } from "svelte/reactivity"; import type { Editor } from "@graphite/editor"; + import type { IconName } from "@graphite/icons"; import type { LayerPanelEntry, LayerStructureEntry, Layout } from "@graphite/messages"; import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { TooltipState } from "@graphite/state-providers/tooltip"; @@ -600,7 +601,7 @@ {/if} {#if listing.entry.iconName} - + {/if} onEditLayerName(listing)}> import type { IconName } from "@graphite/icons"; - import type { ActionShortcut, KeyRaw, LabeledShortcut, MouseMotion } from "@graphite/messages"; + import type { ActionShortcut, Key, LabeledShortcut, MouseMotion } from "@graphite/messages"; import { operatingSystem } from "@graphite/utility-functions/platform"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; @@ -57,7 +57,7 @@ return consolidatedList; } - function keyboardHintIcon(input: KeyRaw): IconName | undefined { + function keyboardHintIcon(input: Key): IconName | undefined { switch (input) { case "ArrowDown": return "KeyboardArrowDown"; diff --git a/frontend/src/editor.ts b/frontend/src/editor.ts index 6c6b161a49..8a24429ed0 100644 --- a/frontend/src/editor.ts +++ b/frontend/src/editor.ts @@ -2,7 +2,7 @@ import { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm"; import init, { wasmMemory, receiveNativeMessage } from "@graphite/../wasm/pkg/graphite_wasm"; -import type { FrontendMessages } from "@graphite/messages"; +import type { FrontendMessage, FrontendMessages } from "@graphite/messages"; import { createSubscriptionRouter } from "@graphite/subscription-router"; import type { SubscriptionRouter } from "@graphite/subscription-router"; import { operatingSystem } from "@graphite/utility-functions/platform"; @@ -46,7 +46,7 @@ export function createEditor(): Editor { const randomSeed = BigInt(randomSeedFloat); // Handle: object containing many functions from `editor_api.rs` that are part of the `EditorHandle` struct (generated by wasm-bindgen) - const handle = EditorHandle.create(operatingSystem(), randomSeed, (messageType: keyof FrontendMessages, messageData: Record) => { + const handle = EditorHandle.create(operatingSystem(), randomSeed, (messageType: keyof FrontendMessages, messageData: FrontendMessage) => { // This callback is called by Wasm when a FrontendMessage is received from the Wasm wrapper `EditorHandle` subscriptions.handleFrontendMessage(messageType, messageData); }); diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 5e59559973..a2bb5257df 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -487,7 +487,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli // Pointer lock movement events on desktop editor.subscriptions.subscribeFrontendMessage("WindowPointerLockMove", (data) => { - const event = new CustomEvent("pointerlockmove", { detail: data }); + const event = new CustomEvent("pointerlockmove", { detail: { x: data.position[0], y: data.position[1] } }); window.dispatchEvent(event); }); diff --git a/frontend/src/io-managers/persistence.ts b/frontend/src/io-managers/persistence.ts index fb0fbf6725..7a5d8508b3 100644 --- a/frontend/src/io-managers/persistence.ts +++ b/frontend/src/io-managers/persistence.ts @@ -6,7 +6,6 @@ import type { FrontendMessages } from "@graphite/messages"; import type { PortfolioState } from "@graphite/state-providers/portfolio"; type TriggerPersistenceWriteDocument = FrontendMessages["TriggerPersistenceWriteDocument"]; -type TriggerSavePreferences = FrontendMessages["TriggerSavePreferences"]; const graphiteStore = createStore("graphite", "store"); @@ -154,7 +153,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta // PREFERENCES - async function savePreferences(preferences: TriggerSavePreferences["preferences"]) { + async function savePreferences(preferences: unknown) { await set("preferences", preferences, graphiteStore); } diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 0818492ad7..df3fdf949e 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -1,766 +1,9 @@ -import type { PopoverButtonStyle, IconName, IconSize } from "@graphite/icons"; +export * from "@graphite/../wasm/pkg/graphite_wasm"; -export type NodeGraphError = { - position: [number, number]; - error: string; +// Type convert a union of messages into a map of messages +export type ToMessageMap = { + [K in T extends string ? T : T extends object ? keyof T : never]: K extends T ? Record : T extends Record ? Payload : never; }; -export type OpenDocument = { - id: bigint; - details: DocumentDetails; -}; - -type DocumentDetails = { name: string; isAutoSaved: boolean; isSaved: boolean }; - -export type Box = { - startX: number; - startY: number; - endX: number; - endY: number; -}; - -export type FrontendClickTargets = { - nodeClickTargets: string[]; - layerClickTargets: string[]; - connectorClickTargets: string[]; - iconClickTargets: string[]; - allNodesBoundingBox: string; - modifyImportExport: string[]; -}; - -type ContextMenuDataCreateNode = { - type: "CreateNode"; - data: { - compatibleType: string | undefined; - }; -}; -type ContextMenuDataModifyNode = { - type: "ModifyNode"; - data: { - nodeId: bigint; - canBeLayer: boolean; - currentlyIsNode: boolean; - hasSelectedLayers: boolean; - allSelectedLayersLocked: boolean; - }; -}; -export type ContextMenuInformation = { - contextMenuCoordinates: [number, number]; - contextMenuData: ContextMenuDataCreateNode | ContextMenuDataModifyNode; -}; - -export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic" | "Raster" | "Vector" | "Color" | "Invalid"; - -export type FrontendGraphInput = { - dataType: FrontendGraphDataType; - name: string; - description: string; - resolvedType: string; - validTypes: string[]; - connectedTo: string; -}; - -export type FrontendGraphOutput = { - dataType: FrontendGraphDataType; - name: string; - description: string; - resolvedType: string; - connectedTo: string[]; -}; - -export type FrontendNode = { - id: bigint; - isLayer: boolean; - canBeLayer: boolean; - reference: string | undefined; - displayName: string; - implementationName: string; - primaryInput: FrontendGraphInput | undefined; - exposedInputs: FrontendGraphInput[]; - primaryOutput: FrontendGraphOutput | undefined; - exposedOutputs: FrontendGraphOutput[]; - primaryInputConnectedToLayer: boolean; - primaryOutputConnectedToLayer: boolean; - position: [number, number]; - // TODO: Store field for the width of the left node chain - previewed: boolean; - visible: boolean; - locked: boolean; -}; - -export type FrontendNodeType = { - identifier: string; - name: string; - category: string; - inputTypes: string[]; -}; - -export type NodeGraphTransform = { - scale: number; - x: number; - y: number; -}; - -export type WirePath = { - pathString: string; - dataType: FrontendGraphDataType; - thick: boolean; - dashed: boolean; -}; - -export type AppWindowPlatform = "Web" | "Windows" | "Mac" | "Linux"; - -// Rust enum `Key` -export type KeyRaw = string; -// Serde converts a Rust `Key` enum variant into this format with both the `Key` variant name (called `RawKey` in TS) and the localized `label` for the key -export type MouseMotion = "None" | "Lmb" | "Rmb" | "Mmb" | "ScrollUp" | "ScrollDown" | "Drag" | "LmbDouble" | "LmbDrag" | "RmbDrag" | "RmbDouble" | "MmbDrag"; -export type LabeledShortcut = (MouseMotion | { key: KeyRaw; label: string })[]; -export type ActionShortcut = { shortcut: LabeledShortcut }; - -// All channels range are represented by 0-1, sRGB, gamma. -export type Color = { - red: number; - green: number; - blue: number; - alpha: number; - none: boolean; -}; - -export type Gradient = { - position: number[]; - midpoint: number[]; - color: Color[]; -}; - -export type FillChoice = Color | Gradient; - -export type EyedropperPreviewImage = { - data: Uint8Array; - width: number; - height: number; -}; - -export type LayerStructureEntry = { - layerId: bigint; - children: LayerStructureEntry[]; -}; - -export type LayerPanelEntry = { - id: bigint; - implementationName: string; - iconName: IconName | undefined; - alias: string; - inSelectedNetwork: boolean; - childrenAllowed: boolean; - childrenPresent: boolean; - expanded: boolean; - depth: number; - visible: boolean; - parentsVisible: boolean; - unlocked: boolean; - parentsUnlocked: boolean; - parentId: bigint | undefined; - selected: boolean; - ancestorOfSelected: boolean; - descendantOfSelected: boolean; - clipped: boolean; - clippable: boolean; -}; - -export type Font = { - fontFamily: string; - fontStyle: string; -}; - -// WIDGET PROPS - -export type CheckboxInput = { - kind: WidgetPropsNames; - - // Content - checked: boolean; - icon: IconName; - forLabel: bigint | undefined; - disabled: boolean; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type ColorInput = { - kind: WidgetPropsNames; - - // Content - value: FillChoice; - allowNone: boolean; - // allowTransparency: boolean; // TODO: Implement - menuDirection: MenuDirection | undefined; - disabled: boolean; - - // Styling - narrow: boolean; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -// An entry in the all-encompassing MenuList component which defines all types of menus (which are spawned by widgets like `TextButton` and `DropdownInput`) -export type MenuListEntry = { - // Content - value: string; - label: string; - icon?: IconName; - disabled?: boolean; - - // Children - children?: MenuListEntry[][]; - childrenHash?: bigint; - - // Styling - font?: string; - - // Tooltips - tooltipLabel?: string; - tooltipDescription?: string; - tooltipShortcut?: ActionShortcut; -}; - -export type CurveManipulatorGroup = { - anchor: [number, number]; - handles: [[number, number], [number, number]]; -}; - -export type Curve = { - manipulatorGroups: CurveManipulatorGroup[]; - firstHandle: [number, number]; - lastHandle: [number, number]; -}; - -export type CurveInput = { - kind: WidgetPropsNames; - - // Content - value: Curve; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type DropdownInput = { - kind: WidgetPropsNames; - - // Content - selectedIndex: number | undefined; - drawIcon: boolean; - disabled: boolean; - - // Children - entries: MenuListEntry[][]; - entriesHash: bigint; - - // Styling - narrow: boolean; - - // Behavior - virtualScrolling: boolean; - interactive: boolean; - - // Sizing - minWidth: number; - maxWidth: number; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type IconButton = { - kind: WidgetPropsNames; - - // Content - icon: IconName; - hoverIcon: IconName | undefined; - size: IconSize; - disabled: boolean; - - // Styling - emphasized: boolean; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type IconLabel = { - kind: WidgetPropsNames; - - // Content - icon: IconName; - disabled: boolean; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type ImageButton = { - kind: WidgetPropsNames; - - // Content - image: IconName; - width: string | undefined; - height: string | undefined; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type ImageLabel = { - kind: WidgetPropsNames; - - // Content - url: string; - width: string | undefined; - height: string | undefined; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type ShortcutLabel = { - kind: WidgetPropsNames; - - // Content - shortcut: ActionShortcut | undefined; -}; - -export type NumberInputIncrementBehavior = "Add" | "Multiply" | "Callback" | "None"; -export type NumberInputMode = "Increment" | "Range"; - -export type NumberInput = { - kind: WidgetPropsNames; - - // Content - value: number | undefined; - label: string | undefined; - disabled: boolean; - - // Styling - narrow: boolean; - - // Behavior - mode: NumberInputMode; - min: number | undefined; - max: number | undefined; - rangeMin: number | undefined; - rangeMax: number | undefined; - step: number; - isInteger: boolean; - incrementBehavior: NumberInputIncrementBehavior; - displayDecimalPlaces: number; - unit: string; - unitIsHiddenWhenEditing: boolean; - - // Sizing - minWidth: number; - maxWidth: number; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type NodeCatalog = { - kind: WidgetPropsNames; - - // Content - disabled: boolean; - - // Behavior - initialSearchTerm: string; -}; - -export type PopoverButton = { - kind: WidgetPropsNames; - - // Content - style: PopoverButtonStyle | undefined; - icon: IconName | undefined; - disabled: boolean; - - // Children - popoverLayout: Layout; - popoverMinWidth: number | undefined; - menuDirection: MenuDirection | undefined; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center"; - -export type RadioEntryData = { - // Content - value?: string; - label?: string; - icon?: IconName; - - // Tooltips - tooltipLabel?: string; - tooltipDescription?: string; - tooltipShortcut?: ActionShortcut; -}; - -export type RadioInput = { - kind: WidgetPropsNames; - - // Content - selectedIndex: number | undefined; - disabled: boolean; - - // Children - entries: RadioEntryData[]; - - // Styling - narrow: boolean; - - // Sizing - minWidth: number; -}; - -export type SeparatorDirection = "Horizontal" | "Vertical"; -export type SeparatorStyle = "Related" | "Unrelated" | "Section"; - -export type Separator = { - kind: WidgetPropsNames; - - // Content - direction: SeparatorDirection; - style: SeparatorStyle; -}; - -export type WorkingColorsInput = { - kind: WidgetPropsNames; - - // Content - primary: Color; - secondary: Color; -}; - -export type TextAreaInput = { - kind: WidgetPropsNames; - - // Content - value: string; - label: string | undefined; - disabled: boolean; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type ParameterExposeButton = { - kind: WidgetPropsNames; - - // Content - exposed: boolean; - dataType: FrontendGraphDataType; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type TextButton = { - kind: WidgetPropsNames; - - // Content - label: string; - icon: IconName | undefined; - hoverIcon: IconName | undefined; - disabled: boolean; - - // Children - menuListChildren: MenuListEntry[][]; - menuListChildrenHash: bigint; - - // Styling - emphasized: boolean; - flush: boolean; - narrow: boolean; - - // Sizing - minWidth: number; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type BreadcrumbTrailButtons = { - kind: WidgetPropsNames; - - // Content - labels: string[]; - disabled: boolean; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type TextInput = { - kind: WidgetPropsNames; - - // Content - value: string; - label: string | undefined; - placeholder: string | undefined; - disabled: boolean; - - // Styling - narrow: boolean; - centered: boolean; - - // Sizing - minWidth: number; - maxWidth: number; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type TextLabel = { - kind: WidgetPropsNames; - - // Content - value: string; - disabled: boolean; - forCheckbox: bigint | undefined; - - // Styling - narrow: boolean; - bold: boolean; - italic: boolean; - monospace: boolean; - multiline: boolean; - centerAlign: boolean; - tableAlign: boolean; - - // Sizing - minWidth: number; - minWidthCharacters: number; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -export type ReferencePoint = "None" | "TopLeft" | "TopCenter" | "TopRight" | "CenterLeft" | "Center" | "CenterRight" | "BottomLeft" | "BottomCenter" | "BottomRight"; - -export type ReferencePointInput = { - kind: WidgetPropsNames; - - // Content - value: ReferencePoint; - disabled: boolean; - - // Tooltips - tooltipLabel: string; - tooltipDescription: string; - tooltipShortcut: ActionShortcut | undefined; -}; - -// WIDGET - -export type WidgetTypes = { - BreadcrumbTrailButtons: BreadcrumbTrailButtons; - CheckboxInput: CheckboxInput; - ColorInput: ColorInput; - CurveInput: CurveInput; - DropdownInput: DropdownInput; - IconButton: IconButton; - IconLabel: IconLabel; - ImageButton: ImageButton; - ImageLabel: ImageLabel; - NodeCatalog: NodeCatalog; - NumberInput: NumberInput; - ParameterExposeButton: ParameterExposeButton; - PopoverButton: PopoverButton; - RadioInput: RadioInput; - ReferencePointInput: ReferencePointInput; - Separator: Separator; - ShortcutLabel: ShortcutLabel; - TextAreaInput: TextAreaInput; - TextButton: TextButton; - TextInput: TextInput; - TextLabel: TextLabel; - WorkingColorsInput: WorkingColorsInput; -}; -export type WidgetPropsNames = keyof WidgetTypes; -export type WidgetPropsSet = WidgetTypes[WidgetPropsNames]; - -export type WidgetInstance = { - widgetId: bigint; - props: WidgetPropsSet; -}; - -// WIDGET LAYOUT - -export type LayoutTarget = - | "DataPanel" - | "DialogButtons" - | "DialogColumn1" - | "DialogColumn2" - | "DocumentBar" - | "LayersPanelBottomBar" - | "LayersPanelControlLeftBar" - | "LayersPanelControlRightBar" - | "MenuBar" - | "NodeGraphControlBar" - | "PropertiesPanel" - | "StatusBarHints" - | "StatusBarInfo" - | "ToolOptions" - | "ToolShelf" - | "WelcomeScreenButtons" - | "WorkingColors"; - -export type WidgetDiff = { - widgetPath: number[]; - newValue: { layout: Layout } | { layoutGroup: LayoutGroup } | { widget: WidgetInstance }; -}; - -export type UIItem = Layout | LayoutGroup | WidgetInstance[] | WidgetInstance; -export type LayoutGroup = WidgetSpanRow | WidgetSpanColumn | WidgetTable | WidgetSection; -export type Layout = LayoutGroup[]; - -export type WidgetSpanColumn = { columnWidgets: WidgetInstance[] }; -export type WidgetSpanRow = { rowWidgets: WidgetInstance[] }; -export type WidgetTable = { tableWidgets: WidgetInstance[][]; unstyled: boolean }; -export type WidgetSection = { name: string; description: string; visible: boolean; pinned: boolean; id: bigint; layout: Layout }; - -export type FrontendMessages = { - ClearAllNodeGraphWires: Record; - DisplayDialog: { title: string; icon: IconName }; - DialogClose: Record; - DisplayDialogPanic: { panicInfo: string }; - DisplayEditableTextbox: { - text: string; - lineHeightRatio: number; - fontSize: number; - color: string; - fontData: ArrayBuffer; - transform: number[]; - maxWidth: undefined | number; - maxHeight: undefined | number; - align: "Left" | "Center" | "Right" | "JustifyLeft"; - }; - DisplayEditableTextboxTransform: { transform: number[] }; - DisplayEditableTextboxUpdateFontData: { fontData: ArrayBuffer }; - DisplayRemoveEditableTextbox: Record; - SendShortcutAltClick: { shortcut: ActionShortcut | undefined }; - SendShortcutFullscreen: { shortcut: ActionShortcut | undefined; shortcutMac: ActionShortcut | undefined }; - SendShortcutShiftClick: { shortcut: ActionShortcut | undefined }; - SendUIMetadata: { nodeDescriptions: [string, string][]; nodeTypes: FrontendNodeType[] }; - TriggerAboutGraphiteLocalizedCommitDate: { commitDate: string }; - TriggerClipboardRead: Record; - TriggerClipboardWrite: { content: string }; - TriggerDisplayThirdPartyLicensesDialog: Record; - TriggerExportImage: { svg: string; name: string; mime: string; size: [number, number] }; - TriggerFetchAndOpenDocument: { name: string; filename: string }; - TriggerFontCatalogLoad: Record; - TriggerFontDataLoad: { font: Font; url: string }; - TriggerImport: Record; - TriggerLoadFirstAutoSaveDocument: Record; - TriggerLoadPreferences: Record; - TriggerLoadRestAutoSaveDocuments: Record; - TriggerOpen: Record; - TriggerOpenLaunchDocuments: Record; - TriggerPersistenceRemoveDocument: { documentId: bigint }; - TriggerPersistenceWriteDocument: { documentId: bigint; document: string; details: DocumentDetails; version: string }; - TriggerSaveActiveDocument: { documentId: bigint }; - TriggerSaveDocument: { documentId: bigint; name: string; path: string | undefined; content: ArrayBuffer }; - TriggerSaveFile: { name: string; content: ArrayBuffer }; - TriggerSavePreferences: { preferences: Record }; - TriggerSelectionRead: { cut: boolean }; - TriggerSelectionWrite: { content: string }; - TriggerTextCommit: Record; - TriggerVisitLink: { url: string }; - UpdateActiveDocument: { documentId: bigint }; - UpdateBox: { box: Box | undefined }; - UpdateClickTargets: { clickTargets: FrontendClickTargets | undefined }; - UpdateContextMenuInformation: { contextMenuInformation: ContextMenuInformation | undefined }; - UpdateDataPanelState: { open: boolean }; - UpdateDocumentArtwork: { svg: string }; - UpdateDocumentLayerDetails: { data: LayerPanelEntry }; - UpdateDocumentLayerStructure: { layerStructure: LayerStructureEntry[] }; - UpdateDocumentRulers: { origin: [number, number]; spacing: number; interval: number; visible: boolean }; - UpdateDocumentScrollbars: { position: [number, number]; size: [number, number]; multiplier: [number, number] }; - UpdateExportReorderIndex: { exportIndex: number | undefined }; - UpdateEyedropperSamplingState: { - image: EyedropperPreviewImage | undefined; - mousePosition: [number, number] | undefined; - primaryColor: string; - secondaryColor: string; - setColorChoice: "Primary" | "Secondary" | undefined; - }; - UpdateFullscreen: { fullscreen: boolean }; - UpdateGradientStopColorPickerPosition: { color: Color; x: number; y: number }; - UpdateGraphFadeArtwork: { percentage: number }; - UpdateGraphViewOverlay: { open: boolean }; - UpdateImportReorderIndex: { importIndex: number | undefined }; - UpdateImportsExports: { - imports: (FrontendGraphOutput | undefined)[]; - exports: (FrontendGraphInput | undefined)[]; - importPosition: [number, number]; - exportPosition: [number, number]; - addImportExport: boolean; - }; - UpdateInSelectedNetwork: { inSelectedNetwork: boolean }; - UpdateLayersPanelState: { open: boolean }; - UpdateLayerWidths: { layerWidths: Map; chainWidths: Map; hasLeftInputWire: Map }; - UpdateLayout: { layoutTarget: LayoutTarget; diff: WidgetDiff[] }; - UpdateMaximized: { maximized: boolean }; - UpdateMouseCursor: { cursor: string }; - UpdateNodeGraphErrorDiagnostic: { error: NodeGraphError | undefined }; - UpdateNodeGraphNodes: { nodes: FrontendNode[] }; - UpdateNodeGraphSelection: { selected: bigint[] }; - UpdateNodeGraphTransform: { transform: NodeGraphTransform }; - UpdateNodeGraphWires: { wires: { id: bigint; inputIndex: number; wirePathUpdate: WirePath | undefined }[] }; - UpdateNodeThumbnail: { id: bigint; value: string }; - UpdateOpenDocumentsList: { openDocuments: OpenDocument[] }; - UpdatePlatform: { platform: AppWindowPlatform }; - UpdatePropertiesPanelState: { open: boolean }; - UpdateUIScale: { scale: number }; - UpdateViewportHolePunch: { active: boolean }; - UpdateViewportPhysicalBounds: { x: number; y: number; width: number; height: number }; - UpdateVisibleNodes: { nodes: bigint[] }; - UpdateWirePathInProgress: { wirePath: WirePath | undefined }; - WindowFullscreen: Record; - WindowPointerLockMove: { x: number; y: number }; -}; +import type { FrontendMessage } from "@graphite/../wasm/pkg/graphite_wasm"; +export type FrontendMessages = ToMessageMap; diff --git a/frontend/src/messages_old.ts b/frontend/src/messages_old.ts new file mode 100644 index 0000000000..fb029e3ec6 --- /dev/null +++ b/frontend/src/messages_old.ts @@ -0,0 +1,776 @@ +import type { PopoverButtonStyle, IconName, IconSize } from "@graphite/icons"; + +export type NodeGraphErrorDiagnostic = { + position: [number, number]; + error: string; +}; + +export type OpenDocument = { + id: bigint; + details: DocumentDetails; +}; + +type DocumentDetails = { + name: string; + path: string | undefined; + isSaved: boolean; + isAutoSaved: boolean; +}; + +export type BoxSelection = { + startX: number; + startY: number; + endX: number; + endY: number; +}; + +export type FrontendClickTargets = { + nodeClickTargets: string[]; + layerClickTargets: string[]; + connectorClickTargets: string[]; + iconClickTargets: string[]; + allNodesBoundingBox: string; + modifyImportExport: string[]; +}; + +type ContextMenuDataCreateNode = { + type: "CreateNode"; + data: { + compatibleType: string | undefined; + }; +}; +type ContextMenuDataModifyNode = { + type: "ModifyNode"; + data: { + nodeId: bigint; + canBeLayer: boolean; + currentlyIsNode: boolean; + hasSelectedLayers: boolean; + allSelectedLayersLocked: boolean; + }; +}; +export type ContextMenuInformation = { + contextMenuCoordinates: [number, number]; + contextMenuData: ContextMenuDataCreateNode | ContextMenuDataModifyNode; +}; + +export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic" | "Raster" | "Vector" | "Color" | "Invalid"; + +export type FrontendGraphInput = { + dataType: FrontendGraphDataType; + name: string; + description: string; + resolvedType: string; + validTypes: string[]; + connectedTo: string; +}; + +export type FrontendGraphOutput = { + dataType: FrontendGraphDataType; + name: string; + description: string; + resolvedType: string; + connectedTo: string[]; +}; + +export type FrontendNode = { + id: bigint; + isLayer: boolean; + canBeLayer: boolean; + reference: string | undefined; + displayName: string; + implementationName: string; + primaryInput: FrontendGraphInput | undefined; + exposedInputs: FrontendGraphInput[]; + primaryOutput: FrontendGraphOutput | undefined; + exposedOutputs: FrontendGraphOutput[]; + primaryInputConnectedToLayer: boolean; + primaryOutputConnectedToLayer: boolean; + position: [number, number]; + // TODO: Store field for the width of the left node chain + previewed: boolean; + visible: boolean; + locked: boolean; +}; + +export type FrontendNodeType = { + identifier: string; + name: string; + category: string; + inputTypes: string[]; +}; + +export type WirePath = { + pathString: string; + dataType: FrontendGraphDataType; + thick: boolean; + dashed: boolean; +}; + +export type AppWindowPlatform = "Web" | "Windows" | "Mac" | "Linux"; + +// Rust enum `Key` +export type Key = string; +// Serde converts a Rust `Key` enum variant into this format with both the `Key` variant name (called `RawKey` in TS) and the localized `label` for the key +export type MouseMotion = "None" | "Lmb" | "Rmb" | "Mmb" | "ScrollUp" | "ScrollDown" | "Drag" | "LmbDouble" | "LmbDrag" | "RmbDrag" | "RmbDouble" | "MmbDrag"; +export type LabeledKey = { key: Key; label: string }; +export type LabeledShortcutOrMouseMotion = LabeledKey | MouseMotion; +export type LabeledShortcut = LabeledShortcutOrMouseMotion[]; +export type ActionShortcut = { shortcut: LabeledShortcut }; + +// All channels range are represented by 0-1, sRGB, gamma. +export type Color = { + red: number; + green: number; + blue: number; + alpha: number; + none: boolean; +}; + +export type Gradient = { + position: number[]; + midpoint: number[]; + color: Color[]; +}; + +export type FillChoice = Color | Gradient; + +export type EyedropperPreviewImage = { + data: Uint8Array; + width: number; + height: number; +}; + +export type LayerStructureEntry = { + layerId: bigint; + children: LayerStructureEntry[]; +}; + +export type LayerPanelEntry = { + id: bigint; + implementationName: string; + iconName: IconName | undefined; + alias: string; + inSelectedNetwork: boolean; + childrenAllowed: boolean; + childrenPresent: boolean; + expanded: boolean; + depth: number; + visible: boolean; + parentsVisible: boolean; + unlocked: boolean; + parentsUnlocked: boolean; + parentId: bigint | undefined; + selected: boolean; + ancestorOfSelected: boolean; + descendantOfSelected: boolean; + clipped: boolean; + clippable: boolean; +}; + +export type Font = { + fontFamily: string; + fontStyle: string; +}; + +// WIDGET PROPS + +export type CheckboxInput = { + kind: WidgetPropsNames; + + // Content + checked: boolean; + icon: IconName; + forLabel: bigint | undefined; + disabled: boolean; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type ColorInput = { + kind: WidgetPropsNames; + + // Content + value: FillChoice; + allowNone: boolean; + // allowTransparency: boolean; // TODO: Implement + menuDirection: MenuDirection | undefined; + disabled: boolean; + + // Styling + narrow: boolean; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +// An entry in the all-encompassing MenuList component which defines all types of menus (which are spawned by widgets like `TextButton` and `DropdownInput`) +export type MenuListEntry = { + // Content + value: string; + label: string; + icon?: IconName; + disabled?: boolean; + + // Children + children?: MenuListEntry[][]; + childrenHash?: bigint; + + // Styling + font?: string; + + // Tooltips + tooltipLabel?: string; + tooltipDescription?: string; + tooltipShortcut?: ActionShortcut; +}; + +export type CurveManipulatorGroup = { + anchor: [number, number]; + handles: [[number, number], [number, number]]; +}; + +export type Curve = { + manipulatorGroups: CurveManipulatorGroup[]; + firstHandle: [number, number]; + lastHandle: [number, number]; +}; + +export type CurveInput = { + kind: WidgetPropsNames; + + // Content + value: Curve; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type DropdownInput = { + kind: WidgetPropsNames; + + // Content + selectedIndex: number | undefined; + drawIcon: boolean; + disabled: boolean; + + // Children + entries: MenuListEntry[][]; + entriesHash: bigint; + + // Styling + narrow: boolean; + + // Behavior + virtualScrolling: boolean; + interactive: boolean; + + // Sizing + minWidth: number; + maxWidth: number; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type IconButton = { + kind: WidgetPropsNames; + + // Content + icon: IconName; + hoverIcon: IconName | undefined; + size: IconSize; + disabled: boolean; + + // Styling + emphasized: boolean; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type IconLabel = { + kind: WidgetPropsNames; + + // Content + icon: IconName; + disabled: boolean; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type ImageButton = { + kind: WidgetPropsNames; + + // Content + image: IconName; + width: string | undefined; + height: string | undefined; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type ImageLabel = { + kind: WidgetPropsNames; + + // Content + url: string; + width: string | undefined; + height: string | undefined; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type ShortcutLabel = { + kind: WidgetPropsNames; + + // Content + shortcut: ActionShortcut | undefined; +}; + +export type NumberInputIncrementBehavior = "Add" | "Multiply" | "Callback" | "None"; +export type NumberInputMode = "Increment" | "Range"; + +export type NumberInput = { + kind: WidgetPropsNames; + + // Content + value: number | undefined; + label: string | undefined; + disabled: boolean; + + // Styling + narrow: boolean; + + // Behavior + mode: NumberInputMode; + min: number | undefined; + max: number | undefined; + rangeMin: number | undefined; + rangeMax: number | undefined; + step: number; + isInteger: boolean; + incrementBehavior: NumberInputIncrementBehavior; + displayDecimalPlaces: number; + unit: string; + unitIsHiddenWhenEditing: boolean; + + // Sizing + minWidth: number; + maxWidth: number; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type NodeCatalog = { + kind: WidgetPropsNames; + + // Content + disabled: boolean; + + // Behavior + initialSearchTerm: string; +}; + +export type PopoverButton = { + kind: WidgetPropsNames; + + // Content + style: PopoverButtonStyle | undefined; + icon: IconName | undefined; + disabled: boolean; + + // Children + popoverLayout: Layout; + popoverMinWidth: number | undefined; + menuDirection: MenuDirection | undefined; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center"; + +export type RadioEntryData = { + // Content + value?: string; + label?: string; + icon?: IconName; + + // Tooltips + tooltipLabel?: string; + tooltipDescription?: string; + tooltipShortcut?: ActionShortcut; +}; + +export type RadioInput = { + kind: WidgetPropsNames; + + // Content + selectedIndex: number | undefined; + disabled: boolean; + + // Children + entries: RadioEntryData[]; + + // Styling + narrow: boolean; + + // Sizing + minWidth: number; +}; + +export type SeparatorDirection = "Horizontal" | "Vertical"; +export type SeparatorStyle = "Related" | "Unrelated" | "Section"; + +export type Separator = { + kind: WidgetPropsNames; + + // Content + direction: SeparatorDirection; + style: SeparatorStyle; +}; + +export type WorkingColorsInput = { + kind: WidgetPropsNames; + + // Content + primary: Color; + secondary: Color; +}; + +export type TextAreaInput = { + kind: WidgetPropsNames; + + // Content + value: string; + label: string | undefined; + disabled: boolean; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type ParameterExposeButton = { + kind: WidgetPropsNames; + + // Content + exposed: boolean; + dataType: FrontendGraphDataType; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type TextButton = { + kind: WidgetPropsNames; + + // Content + label: string; + icon: IconName | undefined; + hoverIcon: IconName | undefined; + disabled: boolean; + + // Children + menuListChildren: MenuListEntry[][]; + menuListChildrenHash: bigint; + + // Styling + emphasized: boolean; + flush: boolean; + narrow: boolean; + + // Sizing + minWidth: number; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type BreadcrumbTrailButtons = { + kind: WidgetPropsNames; + + // Content + labels: string[]; + disabled: boolean; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type TextInput = { + kind: WidgetPropsNames; + + // Content + value: string; + label: string | undefined; + placeholder: string | undefined; + disabled: boolean; + + // Styling + narrow: boolean; + centered: boolean; + + // Sizing + minWidth: number; + maxWidth: number; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type TextLabel = { + kind: WidgetPropsNames; + + // Content + value: string; + disabled: boolean; + forCheckbox: bigint | undefined; + + // Styling + narrow: boolean; + bold: boolean; + italic: boolean; + monospace: boolean; + multiline: boolean; + centerAlign: boolean; + tableAlign: boolean; + + // Sizing + minWidth: number; + minWidthCharacters: number; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type MouseCursorIcon = "Default" | "None" | "ZoomIn" | "ZoomOut" | "Grabbing" | "Crosshair" | "Text" | "Move" | "NSResize" | "EWResize" | "NESWResize" | "NWSEResize" | "Rotate"; + +export type ReferencePoint = "None" | "TopLeft" | "TopCenter" | "TopRight" | "CenterLeft" | "Center" | "CenterRight" | "BottomLeft" | "BottomCenter" | "BottomRight"; + +export type ReferencePointInput = { + kind: WidgetPropsNames; + + // Content + value: ReferencePoint; + disabled: boolean; + + // Tooltips + tooltipLabel: string; + tooltipDescription: string; + tooltipShortcut: ActionShortcut | undefined; +}; + +export type WirePathUpdate = { + id: bigint; + inputIndex: number; + wirePathUpdate: WirePath | undefined; +}; + +type TextAlign = "Left" | "Center" | "Right" | "JustifyLeft"; + +// WIDGET + +export type WidgetTypes = { + BreadcrumbTrailButtons: BreadcrumbTrailButtons; + CheckboxInput: CheckboxInput; + ColorInput: ColorInput; + CurveInput: CurveInput; + DropdownInput: DropdownInput; + IconButton: IconButton; + IconLabel: IconLabel; + ImageButton: ImageButton; + ImageLabel: ImageLabel; + NodeCatalog: NodeCatalog; + NumberInput: NumberInput; + ParameterExposeButton: ParameterExposeButton; + PopoverButton: PopoverButton; + RadioInput: RadioInput; + ReferencePointInput: ReferencePointInput; + Separator: Separator; + ShortcutLabel: ShortcutLabel; + TextAreaInput: TextAreaInput; + TextButton: TextButton; + TextInput: TextInput; + TextLabel: TextLabel; + WorkingColorsInput: WorkingColorsInput; +}; +export type WidgetPropsNames = keyof WidgetTypes; +export type WidgetPropsSet = WidgetTypes[WidgetPropsNames]; + +export type WidgetInstance = { + widgetId: bigint; + props: WidgetPropsSet; // TODO: Make this be: `widget: Widget;` where `Widget` is https://files.keavon.com/-/SkyblueWorriedGlowworm/capture.png +}; + +// WIDGET LAYOUT + +export type LayoutTarget = + | "DataPanel" + | "DialogButtons" + | "DialogColumn1" + | "DialogColumn2" + | "DocumentBar" + | "LayersPanelBottomBar" + | "LayersPanelControlLeftBar" + | "LayersPanelControlRightBar" + | "MenuBar" + | "NodeGraphControlBar" + | "PropertiesPanel" + | "StatusBarHints" + | "StatusBarInfo" + | "ToolOptions" + | "ToolShelf" + | "WelcomeScreenButtons" + | "WorkingColors"; + +export type WidgetDiff = { + widgetPath: bigint[]; + newValue: { layout: Layout } | { layoutGroup: LayoutGroup } | { widget: WidgetInstance }; +}; + +export type UIItem = Layout | LayoutGroup | WidgetInstance[] | WidgetInstance; +export type LayoutGroup = WidgetSpanRow | WidgetSpanColumn | WidgetTable | WidgetSection; +export type Layout = LayoutGroup[]; + +export type WidgetSpanColumn = { columnWidgets: WidgetInstance[] }; +export type WidgetSpanRow = { rowWidgets: WidgetInstance[] }; +export type WidgetTable = { tableWidgets: WidgetInstance[][]; unstyled: boolean }; +export type WidgetSection = { name: string; description: string; visible: boolean; pinned: boolean; id: bigint; layout: Layout }; + +export type FrontendMessages = { + ClearAllNodeGraphWires: Record; + DisplayDialog: { title: string; icon: string }; + DialogClose: Record; + DisplayDialogPanic: { panicInfo: string }; + DisplayEditableTextbox: { + text: string; + lineHeightRatio: number; + fontSize: number; + color: string; + fontData: Uint8Array; + transform: [number, number, number, number, number, number]; + maxWidth: undefined | number; + maxHeight: undefined | number; + align: TextAlign; + }; + DisplayEditableTextboxTransform: { transform: [number, number, number, number, number, number] }; + DisplayEditableTextboxUpdateFontData: { fontData: Uint8Array }; + DisplayRemoveEditableTextbox: Record; + SendShortcutAltClick: { shortcut: ActionShortcut | undefined }; + SendShortcutFullscreen: { shortcut: ActionShortcut | undefined; shortcutMac: ActionShortcut | undefined }; + SendShortcutShiftClick: { shortcut: ActionShortcut | undefined }; + SendUIMetadata: { nodeDescriptions: [string, string][]; nodeTypes: FrontendNodeType[] }; + TriggerAboutGraphiteLocalizedCommitDate: { commitDate: string }; + TriggerClipboardRead: Record; + TriggerClipboardWrite: { content: string }; + TriggerDisplayThirdPartyLicensesDialog: Record; + TriggerExportImage: { svg: string; name: string; mime: string; size: [number, number] }; + TriggerFetchAndOpenDocument: { name: string; filename: string }; + TriggerFontCatalogLoad: Record; + TriggerFontDataLoad: { font: Font; url: string }; + TriggerImport: Record; + TriggerLoadFirstAutoSaveDocument: Record; + TriggerLoadPreferences: Record; + TriggerLoadRestAutoSaveDocuments: Record; + TriggerOpen: Record; + TriggerOpenLaunchDocuments: Record; + TriggerPersistenceRemoveDocument: { documentId: bigint }; + TriggerPersistenceWriteDocument: { documentId: bigint; document: string; details: DocumentDetails }; + TriggerSaveActiveDocument: { documentId: bigint }; + TriggerSaveDocument: { documentId: bigint; name: string; path: string | undefined; content: Uint8Array }; + TriggerSaveFile: { name: string; content: Uint8Array }; + TriggerSavePreferences: { preferences: unknown }; + TriggerSelectionRead: { cut: boolean }; + TriggerSelectionWrite: { content: string }; + TriggerTextCommit: Record; + TriggerVisitLink: { url: string }; + UpdateActiveDocument: { documentId: bigint }; + UpdateBox: { box: BoxSelection | undefined }; + UpdateClickTargets: { clickTargets: FrontendClickTargets | undefined }; + UpdateContextMenuInformation: { contextMenuInformation: ContextMenuInformation | undefined }; + UpdateDataPanelState: { open: boolean }; + UpdateDocumentArtwork: { svg: string }; + UpdateDocumentLayerDetails: { data: LayerPanelEntry }; + UpdateDocumentLayerStructure: { layerStructure: LayerStructureEntry[] }; + UpdateDocumentRulers: { origin: [number, number]; spacing: number; interval: number; visible: boolean }; + UpdateDocumentScrollbars: { position: [number, number]; size: [number, number]; multiplier: [number, number] }; + UpdateExportReorderIndex: { exportIndex: number | undefined }; + UpdateEyedropperSamplingState: { + image: EyedropperPreviewImage | undefined; + mousePosition: [number, number] | undefined; + primaryColor: string; + secondaryColor: string; + setColorChoice: "Primary" | "Secondary" | undefined; + }; + UpdateFullscreen: { fullscreen: boolean }; + UpdateGradientStopColorPickerPosition: { color: Color; position: [number, number] }; + UpdateGraphFadeArtwork: { percentage: number }; + UpdateGraphViewOverlay: { open: boolean }; + UpdateImportReorderIndex: { importIndex: number | undefined }; + UpdateImportsExports: { + imports: (FrontendGraphOutput | undefined)[]; + exports: (FrontendGraphInput | undefined)[]; + importPosition: [number, number]; + exportPosition: [number, number]; + addImportExport: boolean; + }; + UpdateInSelectedNetwork: { inSelectedNetwork: boolean }; + UpdateLayersPanelState: { open: boolean }; + UpdateLayerWidths: { layerWidths: Map; chainWidths: Map; hasLeftInputWire: Map }; + UpdateLayout: { layoutTarget: LayoutTarget; diff: WidgetDiff[] }; + UpdateMaximized: { maximized: boolean }; + UpdateMouseCursor: { cursor: MouseCursorIcon }; + UpdateNodeGraphErrorDiagnostic: { error: NodeGraphErrorDiagnostic | undefined }; + UpdateNodeGraphNodes: { nodes: FrontendNode[] }; + UpdateNodeGraphSelection: { selected: bigint[] }; + UpdateNodeGraphTransform: { translation: [number, number]; scale: number }; + UpdateNodeGraphWires: { wires: WirePathUpdate[] }; + UpdateNodeThumbnail: { id: bigint; value: string }; + UpdateOpenDocumentsList: { openDocuments: OpenDocument[] }; + UpdatePlatform: { platform: AppWindowPlatform }; + UpdatePropertiesPanelState: { open: boolean }; + UpdateUIScale: { scale: number }; + UpdateViewportHolePunch: { active: boolean }; + UpdateVisibleNodes: { nodes: bigint[] }; + UpdateWirePathInProgress: { wirePath: WirePath | undefined }; + WindowFullscreen: Record; + WindowPointerLockMove: { position: [number, number] }; +}; diff --git a/frontend/src/state-providers/dialog.ts b/frontend/src/state-providers/dialog.ts index 04419b112b..bee7643603 100644 --- a/frontend/src/state-providers/dialog.ts +++ b/frontend/src/state-providers/dialog.ts @@ -50,7 +50,7 @@ export function createDialogState(editor: Editor) { state.visible = true; state.title = data.title; - state.icon = data.icon; + state.icon = data.icon as IconName; return state; }); diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index 9878ede439..2fdd0f3f35 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -1,16 +1,16 @@ import { writable } from "svelte/store"; import type { Editor } from "@graphite/editor"; -import type { NodeGraphError, Box, FrontendClickTargets, ContextMenuInformation, FrontendNode, FrontendNodeType, WirePath, FrontendMessages } from "@graphite/messages"; +import type { NodeGraphErrorDiagnostic, BoxSelection, FrontendClickTargets, ContextMenuInformation, FrontendNode, FrontendNodeType, WirePath, FrontendMessages } from "@graphite/messages"; type UpdateImportsExports = FrontendMessages["UpdateImportsExports"]; export function createNodeGraphState(editor: Editor) { const { subscribe, update } = writable({ - box: undefined as Box | undefined, + box: undefined as BoxSelection | undefined, clickTargets: undefined as FrontendClickTargets | undefined, contextMenuInformation: undefined as ContextMenuInformation | undefined, - error: undefined as NodeGraphError | undefined, + error: undefined as NodeGraphErrorDiagnostic | undefined, layerWidths: new Map(), chainWidths: new Map(), hasLeftInputWire: new Map(), @@ -148,7 +148,7 @@ export function createNodeGraphState(editor: Editor) { }); editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphTransform", (data) => { update((state) => { - state.transform = data.transform; + state.transform = { scale: data.scale, x: data.translation[0], y: data.translation[1] }; return state; }); }); diff --git a/frontend/src/subscription-router.ts b/frontend/src/subscription-router.ts index f81499b2c1..5084c03077 100644 --- a/frontend/src/subscription-router.ts +++ b/frontend/src/subscription-router.ts @@ -1,15 +1,14 @@ -import type { FrontendMessages, LayoutTarget, WidgetDiff } from "@graphite/messages"; +import type { FrontendMessage, FrontendMessages, LayoutTarget, WidgetDiff, ToMessageMap } from "@graphite/messages"; import { parseWidgetDiffs } from "@graphite/utility-functions/widgets"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type FrontendMessageCallbacks = Record void) | undefined>; - export function createSubscriptionRouter() { - const subscriptions: FrontendMessageCallbacks = {}; + // Callbacks are wrapped at subscription time to capture their type-specific data extraction in a closure, + // so the stored function has a uniform signature and the map doesn't need per-key generic value types. + const subscriptions: Partial void>> = {}; const layoutCallbacks: Partial void>> = {}; const subscribeFrontendMessage = (messageType: T, callback: (data: FrontendMessages[T]) => void) => { - subscriptions[messageType] = callback; + subscriptions[messageType] = (taggedMessage: FrontendMessages) => callback(taggedMessage[messageType]); }; const unsubscribeFrontendMessage = (messageType: keyof FrontendMessages) => { @@ -24,43 +23,56 @@ export function createSubscriptionRouter() { delete layoutCallbacks[target]; }; - const handleFrontendMessage = (messageType: keyof FrontendMessages, messageData: Record) => { + function normalizeMessage(message: T): ToMessageMap; + function normalizeMessage(message: string | Record): Record { + // If it's a bare string, convert it to an object with an empty payload + if (typeof message === "string") { + const result: Record> = { [message]: {} }; + return result; + } + + // If it's already an object, it matches the structure of our map + return message; + } + + const handleFrontendMessage = (messageType: keyof FrontendMessages, messageData: FrontendMessage) => { // Messages with non-empty data are provided by Serde JSON as an object with one key as the message name, like: { NameOfThisMessage: { ... } } // Messages with empty data are provided by Serde JSON as a string with the message name, like: "NameOfThisMessage" - // Here we extract the payload object or use an empty object depending on the situation. - const message = messageData[messageType] || {}; + // Here we extract the payload object or create an empty payload object, as needed. + const taggedMessage = normalizeMessage(messageData); - // Resolve the callback lookup and the data to pass, depending on whether this is a layout update or a regular message. + // Resolve the dispatch thunk, depending on whether this is a layout update or a regular message. // UpdateLayout messages are dispatched to layout-specific callbacks based on the layout target. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let getCallback: () => ((data: any) => void) | undefined; - let callbackData: unknown; - let errorLabel: string; - if (messageType === "UpdateLayout") { - const { layoutTarget, diff } = message as FrontendMessages["UpdateLayout"]; - getCallback = () => layoutCallbacks[layoutTarget]; - callbackData = parseWidgetDiffs(diff); - errorLabel = `UpdateLayout for layout target "${layoutTarget}"`; - } else { - getCallback = () => subscriptions[messageType]; - callbackData = message; - errorLabel = messageType; + // The thunk is re-evaluated on each retry because the callback may not be registered yet. + let getHandler: () => ((taggedMessage: FrontendMessages) => void) | undefined = () => subscriptions[messageType]; + + // Handle layout updates specially to route them to layout-specific callbacks and extract the diffs as the data to pass + let target: LayoutTarget | undefined; + if ("UpdateLayout" in taggedMessage) { + const { layoutTarget, diff } = taggedMessage["UpdateLayout"]; + target = layoutTarget; + + getHandler = () => { + const layoutCallback = layoutCallbacks[layoutTarget]; + if (!layoutCallback) return undefined; + return () => layoutCallback(parseWidgetDiffs(diff)); + }; } // Try to execute the callback. Due to message ordering, the callback may not be registered yet, // so we retry a few times on the next stack frame to give onMount a chance to run. let retries = 0; const callCallback = () => { - const callback = getCallback(); + const handler = getHandler(); - if (callback) { - callback(callbackData); + if (handler) { + handler(taggedMessage); } else if (retries <= 3) { retries += 1; setTimeout(callCallback, 0); } else { // eslint-disable-next-line no-console - console.error(`Received a frontend message of type "${errorLabel}" but no handler was registered for it from the client.`); + console.error(`Received a frontend message of type ${messageType}${target ? ` (${target})` : ""} but no handler was registered for it from the client.`); } }; diff --git a/frontend/src/utility-functions/colors.ts b/frontend/src/utility-functions/colors.ts index 170684228c..033657366c 100644 --- a/frontend/src/utility-functions/colors.ts +++ b/frontend/src/utility-functions/colors.ts @@ -4,18 +4,19 @@ import type { Color, FillChoice, Gradient } from "@graphite/messages"; // Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers export type HSV = { h: number; s: number; v: number }; export type RGB = { r: number; g: number; b: number }; +export type OptionalColor = Color & { none: boolean }; // COLOR FACTORY FUNCTIONS -export function createColor(red: number, green: number, blue: number, alpha: number): Color { +export function createColor(red: number, green: number, blue: number, alpha: number): OptionalColor { return { red, green, blue, alpha, none: false }; } -export function createNoneColor(): Color { +export function createNoneColor(): OptionalColor { return { red: 0, green: 0, blue: 0, alpha: 1, none: true }; } -export function createColorFromHSVA(h: number, s: number, v: number, a: number): Color { +export function createColorFromHSVA(h: number, s: number, v: number, a: number): OptionalColor { const convert = (n: number): number => { const k = (n + h * 6) % 6; return v - v * s * Math.max(Math.min(...[k, 4 - k, 1]), 0); @@ -26,11 +27,11 @@ export function createColorFromHSVA(h: number, s: number, v: number, a: number): // COLOR UTILITY FUNCTIONS -export function isColor(value: unknown): value is Color { +export function isColor(value: unknown): value is OptionalColor { return typeof value === "object" && value !== null && "red" in value; } -export function colorFromCSS(colorCode: string): Color | undefined { +export function colorFromCSS(colorCode: string): OptionalColor | undefined { // Allow single-digit hex value inputs let colorValue = colorCode.trim(); if (colorValue.length === 2 && colorValue.charAt(0) === "#" && /[0-9a-f]/i.test(colorValue.charAt(1))) { @@ -67,13 +68,13 @@ export function colorFromCSS(colorCode: string): Color | undefined { return createColor(r / 255, g / 255, b / 255, a / 255); } -export function colorEquals(c1: Color, c2: Color): boolean { +export function colorEquals(c1: OptionalColor, c2: OptionalColor): boolean { if (c1.none !== c2.none) return false; if (c1.none && c2.none) return true; return Math.abs(c1.red - c2.red) < 1e-6 && Math.abs(c1.green - c2.green) < 1e-6 && Math.abs(c1.blue - c2.blue) < 1e-6 && Math.abs(c1.alpha - c2.alpha) < 1e-6; } -export function colorToHexNoAlpha(color: Color): string | undefined { +export function colorToHexNoAlpha(color: OptionalColor): string | undefined { if (color.none) return undefined; const r = Math.round(color.red * 255) @@ -89,7 +90,7 @@ export function colorToHexNoAlpha(color: Color): string | undefined { return `#${r}${g}${b}`; } -export function colorToHexOptionalAlpha(color: Color): string | undefined { +export function colorToHexOptionalAlpha(color: OptionalColor): string | undefined { if (color.none) return undefined; const hex = colorToHexNoAlpha(color); @@ -100,7 +101,7 @@ export function colorToHexOptionalAlpha(color: Color): string | undefined { return a === "ff" ? hex : `${hex}${a}`; } -export function colorToRgb255(color: Color): RGB | undefined { +export function colorToRgb255(color: OptionalColor): RGB | undefined { if (color.none) return undefined; return { @@ -110,21 +111,21 @@ export function colorToRgb255(color: Color): RGB | undefined { }; } -export function colorToRgbCSS(color: Color): string | undefined { +export function colorToRgbCSS(color: OptionalColor): string | undefined { const rgb = colorToRgb255(color); if (!rgb) return undefined; return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; } -export function colorToRgbaCSS(color: Color): string | undefined { +export function colorToRgbaCSS(color: OptionalColor): string | undefined { const rgb = colorToRgb255(color); if (!rgb) return undefined; return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${color.alpha})`; } -export function colorToHSV(color: Color): HSV | undefined { +export function colorToHSV(color: OptionalColor): HSV | undefined { if (color.none) return undefined; const { red: r, green: g, blue: b } = color; @@ -156,13 +157,13 @@ export function colorToHSV(color: Color): HSV | undefined { return { h, s, v }; } -export function colorOpaque(color: Color): Color | undefined { +export function colorOpaque(color: OptionalColor): OptionalColor | undefined { if (color.none) return undefined; return createColor(color.red, color.green, color.blue, 1); } -export function colorLuminance(color: Color): number | undefined { +export function colorLuminance(color: OptionalColor): number | undefined { if (color.none) return undefined; // Convert alpha into white @@ -179,7 +180,7 @@ export function colorLuminance(color: Color): number | undefined { return linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722; } -export function colorContrastingColor(color: Color): "black" | "white" { +export function colorContrastingColor(color: OptionalColor): "black" | "white" { if (color.none) return "black"; const luminance = colorLuminance(color); @@ -191,7 +192,7 @@ export function contrastingOutlineFactor(value: FillChoice, proximityColor: stri const pair = Array.isArray(proximityColor) ? [proximityColor[0], proximityColor[1]] : [proximityColor, proximityColor]; const [range1, range2] = pair.map((color) => colorFromCSS(window.getComputedStyle(document.body).getPropertyValue(color)) || createNoneColor()); - const contrast = (color: Color): number => { + const contrast = (color: OptionalColor): number => { const lum = colorLuminance(color) || 0; let rangeLuminance1 = colorLuminance(range1) || 0; let rangeLuminance2 = colorLuminance(range2) || 0; @@ -229,11 +230,11 @@ export function gradientToLinearGradientCSS(gradient: Gradient): string { return `linear-gradient(to right, ${pieces})`; } -export function gradientFirstColor(gradient: Gradient): Color | undefined { +export function gradientFirstColor(gradient: Gradient): OptionalColor | undefined { return gradient.color[0]; } -export function gradientLastColor(gradient: Gradient): Color | undefined { +export function gradientLastColor(gradient: Gradient): OptionalColor | undefined { return gradient.color[gradient.color.length - 1]; } diff --git a/frontend/src/utility-functions/files.ts b/frontend/src/utility-functions/files.ts index b09523f6cc..7581da8c4d 100644 --- a/frontend/src/utility-functions/files.ts +++ b/frontend/src/utility-functions/files.ts @@ -18,11 +18,14 @@ export function downloadFileBlob(filename: string, blob: Blob) { URL.revokeObjectURL(url); } -export function downloadFile(filename: string, content: ArrayBuffer) { +export function downloadFile(filename: string, content: Uint8Array) { const type = filename.endsWith(".svg") ? "image/svg+xml;charset=utf-8" : "application/octet-stream"; - const blob = new Blob([new Uint8Array(content)], { type }); - downloadFileBlob(filename, blob); + if (content.length > 0 && content.buffer instanceof ArrayBuffer) { + const contentView = new Uint8Array(content.buffer, content.byteOffset, content.byteLength); + const blob = new Blob([contentView], { type }); + downloadFileBlob(filename, blob); + } } // See https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#accept for the `accept` string format diff --git a/frontend/src/utility-functions/widgets.ts b/frontend/src/utility-functions/widgets.ts index 8d9f3ecd76..19d5a13197 100644 --- a/frontend/src/utility-functions/widgets.ts +++ b/frontend/src/utility-functions/widgets.ts @@ -46,22 +46,24 @@ export function parseWidgetDiffs(rawDiffs: any): WidgetDiff[] { export function patchLayout(layout: /* &mut */ Layout, diffs: WidgetDiff[]) { diffs.forEach((update) => { // Find the object where the diff applies to - const diffObject = update.widgetPath.reduce((targetLayout: UIItem | undefined, index: number): UIItem | undefined => { - if (targetLayout && "columnWidgets" in targetLayout) return targetLayout.columnWidgets[index]; - if (targetLayout && "rowWidgets" in targetLayout) return targetLayout.rowWidgets[index]; - if (targetLayout && "tableWidgets" in targetLayout) return targetLayout.tableWidgets[index]; - if (targetLayout && "layout" in targetLayout) return targetLayout.layout[index]; + const diffObject = update.widgetPath.reduce((targetLayout: UIItem | undefined, index: bigint): UIItem | undefined => { + const i = Number(index); + + if (targetLayout && "columnWidgets" in targetLayout) return targetLayout.columnWidgets[i]; + if (targetLayout && "rowWidgets" in targetLayout) return targetLayout.rowWidgets[i]; + if (targetLayout && "tableWidgets" in targetLayout) return targetLayout.tableWidgets[i]; + if (targetLayout && "layout" in targetLayout) return targetLayout.layout[i]; if (targetLayout && "props" in targetLayout && "widgetId" in targetLayout) { if (targetLayout.props.kind === "PopoverButton" && "popoverLayout" in targetLayout.props && targetLayout.props.popoverLayout) { targetLayout.props.popoverLayout = targetLayout.props.popoverLayout.map(createLayoutGroup); - return targetLayout.props.popoverLayout[index]; + return targetLayout.props.popoverLayout[i]; } // eslint-disable-next-line no-console console.error("Tried to index widget"); return targetLayout; } - return targetLayout?.[index]; + return targetLayout?.[i]; }, layout as UIItem); // Exit if we failed to produce a valid patch for the existing layout. diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml index 9906a2afa5..db25bf7756 100644 --- a/node-graph/graph-craft/Cargo.toml +++ b/node-graph/graph-craft/Cargo.toml @@ -11,6 +11,13 @@ dealloc_nodes = ["core-types/dealloc_nodes"] wgpu = ["wgpu-executor"] tokio = ["dep:tokio"] loading = ["serde_json"] +wasm = [ + "core-types/wasm", + "graphic-types/wasm", + "text-nodes/wasm", + "dep:tsify", + "dep:wasm-bindgen", +] [dependencies] # Local dependencies @@ -29,7 +36,6 @@ text-nodes = { workspace = true } # Workspace dependencies log = { workspace = true } glam = { workspace = true } -specta = { workspace = true } rustc-hash = { workspace = true } url = { workspace = true } reqwest = { workspace = true } @@ -39,6 +45,8 @@ serde = { workspace = true } wgpu-executor = { workspace = true, optional = true } tokio = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } +tsify = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } # Workspace dependencies [target.'cfg(target_family = "wasm")'.dependencies] diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index b54d56780d..0cb33bf3ff 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -452,7 +452,7 @@ pub struct OldDocumentNode { } // TODO: Eventually remove this document upgrade code -#[derive(Clone, Debug, PartialEq, Default, specta::Type, Hash, DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, Default, Hash, DynAny, serde::Serialize, serde::Deserialize)] /// Metadata about the node including its position in the graph UI pub struct OldDocumentNodeMetadata { pub position: IVec2, diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index e9f05053bc..7e3250590a 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -619,8 +619,8 @@ impl GraphError { } impl Debug for GraphError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NodeGraphError") - .field("path", &self.node_path.iter().map(|id| id.0).collect::>()) + f.debug_struct("GraphError") + .field("node_path", &self.node_path.iter().map(|id| id.0).collect::>()) .field("identifier", &self.identifier.to_string()) .field("error", &self.error) .finish() diff --git a/node-graph/graph-craft/src/wasm_application_io.rs b/node-graph/graph-craft/src/wasm_application_io.rs index b02b8014e2..311bf532e1 100644 --- a/node-graph/graph-craft/src/wasm_application_io.rs +++ b/node-graph/graph-craft/src/wasm_application_io.rs @@ -336,7 +336,8 @@ pub type WasmSurfaceHandle = SurfaceHandle; #[cfg(feature = "wgpu")] pub type WasmSurfaceHandleFrame = graphene_application_io::SurfaceHandleFrame; -#[derive(Clone, Debug, PartialEq, Hash, specta::Type, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, PartialEq, Hash, serde::Serialize, serde::Deserialize)] pub struct EditorPreferences { /// Maximum render region size in pixels along one dimension of the square area. pub max_render_region_size: u32, diff --git a/node-graph/libraries/core-types/Cargo.toml b/node-graph/libraries/core-types/Cargo.toml index a2bbaae8ae..d6bc01c4db 100644 --- a/node-graph/libraries/core-types/Cargo.toml +++ b/node-graph/libraries/core-types/Cargo.toml @@ -11,6 +11,7 @@ default = ["serde"] nightly = [] type_id_logging = [] dealloc_nodes = [] +wasm = ["tsify", "wasm-bindgen", "no-std-types/wasm"] [dependencies] # Local dependencies @@ -29,7 +30,6 @@ rustc-hash = { workspace = true } dyn-any = { workspace = true } ctor = { workspace = true } rand_chacha = { workspace = true } -specta = { workspace = true } image = { workspace = true } tinyvec = { workspace = true } parley = { workspace = true } @@ -42,6 +42,8 @@ polycool = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } +tsify = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } [dev-dependencies] # Workspace dependencies diff --git a/node-graph/libraries/core-types/src/lib.rs b/node-graph/libraries/core-types/src/lib.rs index e514ba8561..1d2f540a40 100644 --- a/node-graph/libraries/core-types/src/lib.rs +++ b/node-graph/libraries/core-types/src/lib.rs @@ -28,10 +28,11 @@ pub use no_std_types::choice_type; pub use no_std_types::color; pub use no_std_types::shaders; pub use num_traits; -pub use specta; use std::any::TypeId; use std::future::Future; use std::pin::Pin; +#[cfg(feature = "wasm")] +pub use tsify; pub use types::Cow; // pub trait Node: for<'n> NodeIO<'n> { diff --git a/node-graph/libraries/core-types/src/text.rs b/node-graph/libraries/core-types/src/text.rs index b58a05a6e0..29917694b6 100644 --- a/node-graph/libraries/core-types/src/text.rs +++ b/node-graph/libraries/core-types/src/text.rs @@ -10,7 +10,8 @@ pub use to_path::*; /// Alignment of lines of type within a text block. #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum TextAlign { #[default] diff --git a/node-graph/libraries/core-types/src/types.rs b/node-graph/libraries/core-types/src/types.rs index 38e8f09ffa..ac0c5a6687 100644 --- a/node-graph/libraries/core-types/src/types.rs +++ b/node-graph/libraries/core-types/src/types.rs @@ -125,7 +125,8 @@ impl std::fmt::Debug for NodeIOTypes { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash, specta::Type, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct ProtoNodeIdentifier { name: Cow<'static, str>, } @@ -178,10 +179,10 @@ fn migrate_type_descriptor_names<'de, D: serde::Deserializer<'de>>(deserializer: Ok(Cow::Owned(name)) } -#[derive(Clone, Debug, Eq, specta::Type, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, Eq, serde::Serialize, serde::Deserialize)] pub struct TypeDescriptor { #[serde(skip)] - #[specta(skip)] pub id: Option, #[serde(deserialize_with = "migrate_type_descriptor_names")] pub name: Cow<'static, str>, @@ -220,7 +221,8 @@ impl PartialEq for TypeDescriptor { } /// Graph runtime type information used for type inference. -#[derive(Clone, PartialEq, Eq, Hash, specta::Type, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum Type { /// A wrapper for some type variable used within the inference system. Resolved at inference time and replaced with a concrete type. Generic(Cow<'static, str>), diff --git a/node-graph/libraries/core-types/src/uuid.rs b/node-graph/libraries/core-types/src/uuid.rs index 3df5007e08..0f8e54aed1 100644 --- a/node-graph/libraries/core-types/src/uuid.rs +++ b/node-graph/libraries/core-types/src/uuid.rs @@ -1,12 +1,9 @@ use dyn_any::DynAny; pub use uuid_generation::*; -#[derive(Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct Uuid( - #[serde(with = "u64_string")] - #[specta(type = String)] - u64, -); +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Copy, serde::Serialize, serde::Deserialize)] +pub struct Uuid(#[serde(with = "u64_string")] u64); mod u64_string { use serde::{self, Deserialize, Deserializer, Serializer}; @@ -70,7 +67,9 @@ mod uuid_generation { } #[repr(transparent)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, specta::Type, DynAny)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[tsify(large_number_types_as_bigints)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, DynAny)] pub struct NodeId(pub u64); impl NodeId { diff --git a/node-graph/libraries/graphic-types/Cargo.toml b/node-graph/libraries/graphic-types/Cargo.toml index 6d3cdeb413..1dac611f07 100644 --- a/node-graph/libraries/graphic-types/Cargo.toml +++ b/node-graph/libraries/graphic-types/Cargo.toml @@ -8,6 +8,12 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] +wasm = [ + "core-types/wasm", + "vector-types/wasm", + "raster-types/wasm", + "wasm-bindgen", +] [dependencies] # Local dependencies @@ -19,8 +25,8 @@ node-macro = { workspace = true } # Workspace dependencies dyn-any = { workspace = true } glam = { workspace = true } -specta = { workspace = true } serde_json = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } diff --git a/node-graph/libraries/no-std-types/Cargo.toml b/node-graph/libraries/no-std-types/Cargo.toml index 6a9a10930d..c7df6bf757 100644 --- a/node-graph/libraries/no-std-types/Cargo.toml +++ b/node-graph/libraries/no-std-types/Cargo.toml @@ -15,7 +15,6 @@ license = "MIT OR Apache-2.0" std = [ "dep:dyn-any", "dep:serde", - "dep:specta", "dep:log", "glam/debug-glam-assert", "glam/std", @@ -25,6 +24,7 @@ std = [ "num-traits/std", "num_enum/std", ] +wasm = ["dep:tsify", "dep:wasm-bindgen"] [dependencies] # Local dependencies @@ -44,9 +44,12 @@ spirv-std = { workspace = true } # Workspace std dependencies serde = { workspace = true, optional = true } -specta = { workspace = true, optional = true } log = { workspace = true, optional = true } +# Workspace wasm dependencies +tsify = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } + [dev-dependencies] core-types = { workspace = true } diff --git a/node-graph/libraries/no-std-types/src/blending.rs b/node-graph/libraries/no-std-types/src/blending.rs index 747c4ba0a8..f6bb2965af 100644 --- a/node-graph/libraries/no-std-types/src/blending.rs +++ b/node-graph/libraries/no-std-types/src/blending.rs @@ -6,7 +6,8 @@ use num_enum::{FromPrimitive, IntoPrimitive}; use num_traits::float::Float; #[derive(Debug, Clone, Copy, PartialEq, BufferStruct)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "std", serde(default))] pub struct AlphaBlending { pub blend_mode: BlendMode, @@ -68,8 +69,9 @@ impl AlphaBlending { } #[repr(i32)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, BufferStruct, FromPrimitive, IntoPrimitive)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] pub enum BlendMode { // Basic group #[default] diff --git a/node-graph/libraries/no-std-types/src/color/color_types.rs b/node-graph/libraries/no-std-types/src/color/color_types.rs index 1c5ce2ef3d..ebb654b3d3 100644 --- a/node-graph/libraries/no-std-types/src/color/color_types.rs +++ b/node-graph/libraries/no-std-types/src/color/color_types.rs @@ -94,8 +94,9 @@ impl Alpha for RGBA16F { impl Pixel for RGBA16F {} #[repr(C)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] pub struct SRGBA8 { red: u8, green: u8, @@ -175,8 +176,9 @@ impl Alpha for SRGBA8 { impl Pixel for SRGBA8 {} #[repr(C)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] pub struct Luma(pub f32); impl Luminance for Luma { @@ -216,8 +218,9 @@ impl Pixel for Luma {} /// The other components (RGB) are stored as `f32` that range from `0.0` up to `f32::MAX`, /// the values encode the brightness of each channel proportional to the light intensity in cd/m² (nits) in HDR, and `0.0` (black) to `1.0` (white) in SDR color. #[repr(C)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Default, Clone, Copy, Pod, Zeroable, BufferStruct)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] pub struct Color { red: f32, green: f32, diff --git a/node-graph/libraries/raster-types/Cargo.toml b/node-graph/libraries/raster-types/Cargo.toml index 8d8e8583b2..e8efb64d1d 100644 --- a/node-graph/libraries/raster-types/Cargo.toml +++ b/node-graph/libraries/raster-types/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] wgpu = ["dep:wgpu"] +wasm = ["core-types/wasm", "dep:tsify", "dep:wasm-bindgen"] [dependencies] # Local dependencies @@ -20,10 +21,11 @@ dyn-any = { workspace = true } glam = { workspace = true } base64 = { workspace = true } bytemuck = { workspace = true } -specta = { workspace = true } image = { workspace = true } serde_json = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } +tsify = { workspace = true, optional = true } wgpu = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } diff --git a/node-graph/libraries/raster-types/src/image.rs b/node-graph/libraries/raster-types/src/image.rs index 8c07e46567..83de9edba4 100644 --- a/node-graph/libraries/raster-types/src/image.rs +++ b/node-graph/libraries/raster-types/src/image.rs @@ -39,7 +39,8 @@ mod base64_serde { } } -#[derive(Clone, Eq, Default, specta::Type, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Eq, Default, serde::Serialize, serde::Deserialize)] pub struct Image { pub width: u32, pub height: u32, @@ -59,7 +60,8 @@ impl PartialEq for Image

{ } } -#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize)] pub struct TransformImage(pub DAffine2); impl Hash for TransformImage { @@ -247,7 +249,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> RasterFrame(RasterFrame), } - #[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ImageFrame { pub image: Image

, } @@ -275,7 +277,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> type Static = ImageFrame; } - #[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct OldImageFrame { image: Image

, transform: DAffine2, @@ -401,7 +403,7 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D RasterFrame(RasterFrame), } - #[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ImageFrame { pub image: Image

, } @@ -429,7 +431,7 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D type Static = ImageFrame; } - #[derive(Clone, Default, Debug, PartialEq, specta::Type, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct OldImageFrame { image: Image

, transform: DAffine2, diff --git a/node-graph/libraries/vector-types/Cargo.toml b/node-graph/libraries/vector-types/Cargo.toml index b7c6704f14..55de1324aa 100644 --- a/node-graph/libraries/vector-types/Cargo.toml +++ b/node-graph/libraries/vector-types/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] +wasm = ["core-types/wasm", "dep:tsify", "dep:wasm-bindgen"] [dependencies] # Local dependencies @@ -22,7 +23,6 @@ glam = { workspace = true } kurbo = { workspace = true } lyon_geom = { workspace = true } dyn-any = { workspace = true } -specta = { workspace = true } log = { workspace = true } petgraph = { workspace = true } rustc-hash = { workspace = true } @@ -31,4 +31,6 @@ tinyvec = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } +tsify = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } fixedbitset = "0.5.7" diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index bcfc2b46cd..653ca6bc5d 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -2,7 +2,8 @@ use core_types::{Color, render_complexity::RenderComplexity}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; -#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum GradientType { #[default] @@ -13,7 +14,8 @@ pub enum GradientType { // TODO: Someday we could switch this to a Box[T] to avoid over-allocation // TODO: Use linear not gamma colors /// A list of colors associated with positions (in the range 0 to 1) along a gradient. -#[derive(Debug, Clone, PartialEq, serde::Serialize, DynAny, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, PartialEq, serde::Serialize, DynAny)] pub struct GradientStops { /// The position of this stop, a factor from 0-1 along the length of the full gradient. pub position: Vec, @@ -336,7 +338,8 @@ impl GradientStops { /// /// Contains the start and end points, along with the colors at varying points along the length. #[repr(C)] -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny)] pub struct Gradient { pub stops: GradientStops, pub gradient_type: GradientType, diff --git a/node-graph/libraries/vector-types/src/vector/misc.rs b/node-graph/libraries/vector-types/src/vector/misc.rs index 17902b6068..1d74519c12 100644 --- a/node-graph/libraries/vector-types/src/vector/misc.rs +++ b/node-graph/libraries/vector-types/src/vector/misc.rs @@ -8,7 +8,8 @@ use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point use std::ops::Sub; /// Represents different geometric interpretations of calculating the centroid (center of mass). -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum CentroidType { /// The center of mass for the area of a solid shape's interior, as if made out of an infinitely flat material. @@ -19,7 +20,8 @@ pub enum CentroidType { } #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum RowsOrColumns { #[default] @@ -65,7 +67,8 @@ impl AsI64 for f64 { } } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum GridType { #[default] @@ -74,7 +77,8 @@ pub enum GridType { } #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum ArcType { #[default] @@ -84,7 +88,8 @@ pub enum ArcType { } #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum MergeByDistanceAlgorithm { #[default] @@ -93,7 +98,8 @@ pub enum MergeByDistanceAlgorithm { } #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum ExtrudeJoiningAlgorithm { All, @@ -103,7 +109,8 @@ pub enum ExtrudeJoiningAlgorithm { } #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum PointSpacingType { #[default] @@ -522,7 +529,8 @@ impl HandleId { } } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Dropdown)] pub enum SpiralType { #[default] diff --git a/node-graph/libraries/vector-types/src/vector/reference_point.rs b/node-graph/libraries/vector-types/src/vector/reference_point.rs index eb03412f5b..094155918c 100644 --- a/node-graph/libraries/vector-types/src/vector/reference_point.rs +++ b/node-graph/libraries/vector-types/src/vector/reference_point.rs @@ -1,7 +1,8 @@ use core_types::math::bbox::AxisAlignedBbox; use glam::DVec2; -#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub enum ReferencePoint { #[default] None, diff --git a/node-graph/libraries/vector-types/src/vector/style.rs b/node-graph/libraries/vector-types/src/vector/style.rs index b4e942c200..24fe6bd390 100644 --- a/node-graph/libraries/vector-types/src/vector/style.rs +++ b/node-graph/libraries/vector-types/src/vector/style.rs @@ -13,7 +13,8 @@ use glam::DAffine2; /// /// In the future we'll probably also add a pattern fill. This will probably be named "Paint" in the future. #[repr(C)] -#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash)] pub enum Fill { #[default] None, @@ -157,7 +158,8 @@ impl From for Fill { /// /// In the future we'll probably also add a pattern fill. #[repr(C)] -#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash)] pub enum FillChoice { #[default] None, @@ -204,7 +206,8 @@ impl From for FillChoice { } #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, node_macro::ChoiceType)] #[widget(Radio)] pub enum FillType { #[default] @@ -214,7 +217,8 @@ pub enum FillType { /// The stroke (outline) style of an SVG element. #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum StrokeCap { #[default] @@ -234,7 +238,8 @@ impl StrokeCap { } #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum StrokeJoin { #[default] @@ -254,7 +259,8 @@ impl StrokeJoin { } #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum StrokeAlign { #[default] @@ -270,7 +276,8 @@ impl StrokeAlign { } #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum PaintOrder { #[default] @@ -289,7 +296,8 @@ fn daffine2_identity() -> DAffine2 { } #[repr(C)] -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny)] #[serde(default)] pub struct Stroke { /// Stroke color @@ -481,7 +489,8 @@ impl Default for Stroke { } #[repr(C)] -#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny)] pub struct PathStyle { pub stroke: Option, pub fill: Fill, @@ -648,7 +657,8 @@ impl PathStyle { } /// Ways the user can choose to view the artwork in the viewport. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny)] pub enum RenderMode { /// Render with normal coloration at the current viewport resolution #[default] diff --git a/node-graph/nodes/gcore/Cargo.toml b/node-graph/nodes/gcore/Cargo.toml index fa59deed1a..3d013e302d 100644 --- a/node-graph/nodes/gcore/Cargo.toml +++ b/node-graph/nodes/gcore/Cargo.toml @@ -8,6 +8,13 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] +wasm = [ + "core-types/wasm", + "raster-types/wasm", + "graphic-types/wasm", + "dep:tsify", + "dep:wasm-bindgen", +] [dependencies] # Local dependencies @@ -21,7 +28,8 @@ dyn-any = { workspace = true } glam = { workspace = true } log = { workspace = true } serde_json = { workspace = true } -specta = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } +tsify = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } diff --git a/node-graph/nodes/gcore/src/extract_xy.rs b/node-graph/nodes/gcore/src/extract_xy.rs index 63d4922ce0..ffe13e26f2 100644 --- a/node-graph/nodes/gcore/src/extract_xy.rs +++ b/node-graph/nodes/gcore/src/extract_xy.rs @@ -14,7 +14,8 @@ fn extract_xy>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2 } /// The X or Y component of a vec2. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, specta::Type, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)] #[widget(Radio)] pub enum XY { #[default] diff --git a/node-graph/nodes/gstd/Cargo.toml b/node-graph/nodes/gstd/Cargo.toml index 012c8417e3..ba1d2d62f9 100644 --- a/node-graph/nodes/gstd/Cargo.toml +++ b/node-graph/nodes/gstd/Cargo.toml @@ -16,6 +16,14 @@ wasm = [ "web-sys", "graphene-application-io/wasm", "image/png", + "core-types/wasm", + "vector-types/wasm", + "graphic-types/wasm", + "text-nodes/wasm", + "raster-nodes/wasm", + "vector-nodes/wasm", + "graphene-core/wasm", + "graph-craft/wasm", ] image-compare = [] vello = ["gpu"] diff --git a/node-graph/nodes/path-bool/Cargo.toml b/node-graph/nodes/path-bool/Cargo.toml index bb4537a6cf..0d8b31f9c7 100644 --- a/node-graph/nodes/path-bool/Cargo.toml +++ b/node-graph/nodes/path-bool/Cargo.toml @@ -6,6 +6,9 @@ description = "Path boolean operation nodes for Graphene" authors = ["Graphite Authors "] license = "MIT OR Apache-2.0" +[features] +wasm = ["core-types/wasm", "dep:tsify", "dep:wasm-bindgen"] + [dependencies] # Local dependencies dyn-any = { workspace = true } @@ -13,8 +16,11 @@ core-types = { workspace = true } graphic-types = { workspace = true } node-macro = { workspace = true } glam = { workspace = true } -specta = { workspace = true } log = { workspace = true } path-bool = { workspace = true } serde = { workspace = true } vector-types = { workspace = true } + +# Optional workspace dependencies +tsify = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 3c984b78f9..49ab3004b4 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -11,14 +11,12 @@ pub use path_bool as path_bool_lib; use path_bool::{FillRule, PathBooleanOperation}; use std::ops::Mul; -// Import specta so derive macros can find it -use core_types::specta; - // TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls, // TODO: since before we used a Vec of single-row tables and now we use a single table // TODO: with multiple rows while still assuming a single row for the boolean operations. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum BooleanOperation { #[default] diff --git a/node-graph/nodes/raster/Cargo.toml b/node-graph/nodes/raster/Cargo.toml index f5084eb55c..d079da09e2 100644 --- a/node-graph/nodes/raster/Cargo.toml +++ b/node-graph/nodes/raster/Cargo.toml @@ -11,11 +11,7 @@ workspace = true [features] default = ["std"] -shader-nodes = [ - "std", - "dep:raster-nodes-shaders", - "dep:wgpu-executor", -] +shader-nodes = ["std", "dep:raster-nodes-shaders", "dep:wgpu-executor"] std = [ "dep:core-types", "dep:dyn-any", @@ -27,9 +23,15 @@ std = [ "dep:rand_chacha", "dep:fastnoise-lite", "dep:serde", - "dep:specta", "dep:kurbo", ] +wasm = [ + "core-types/wasm", + "raster-types/wasm", + "vector-types/wasm", + "dep:tsify", + "dep:wasm-bindgen", +] [dependencies] # Local dependencies @@ -52,7 +54,6 @@ num-traits = { workspace = true } num_enum = { workspace = true } # Workspace std dependencies -specta = { workspace = true, optional = true } image = { workspace = true, optional = true } ndarray = { workspace = true, optional = true } rand = { workspace = true, optional = true } @@ -61,6 +62,10 @@ fastnoise-lite = { workspace = true, optional = true } serde = { workspace = true, optional = true } kurbo = { workspace = true, optional = true } +# Workspace wasm dependencies +tsify = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } + [dev-dependencies] tokio = { workspace = true } futures = { workspace = true } diff --git a/node-graph/nodes/raster/src/adjustments.rs b/node-graph/nodes/raster/src/adjustments.rs index 87e20f255e..ea729094b7 100644 --- a/node-graph/nodes/raster/src/adjustments.rs +++ b/node-graph/nodes/raster/src/adjustments.rs @@ -33,8 +33,9 @@ use vector_types::GradientStops; // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27clrL%27%20%3D%20Color%20Lookup // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Color%20Lookup%20(Photoshop%20CS6 +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[widget(Dropdown)] #[repr(u32)] pub enum LuminanceCalculation { @@ -563,8 +564,9 @@ fn vibrance>( } #[repr(u32)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum RedGreenBlue { #[default] @@ -573,8 +575,9 @@ pub enum RedGreenBlue { Blue, } +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[widget(Radio)] #[repr(u32)] pub enum RedGreenBlueAlpha { @@ -586,8 +589,9 @@ pub enum RedGreenBlueAlpha { } /// Style of noise pattern. +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[widget(Dropdown)] pub enum NoiseType { #[default] @@ -603,8 +607,9 @@ pub enum NoiseType { } /// Style of layered levels of the noise pattern. +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] pub enum FractalType { #[default] None, @@ -619,8 +624,9 @@ pub enum FractalType { } /// Distance function used by the cellular noise. +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] pub enum CellularDistanceFunction { #[default] Euclidean, @@ -630,8 +636,9 @@ pub enum CellularDistanceFunction { Hybrid, } +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] pub enum CellularReturnType { CellValue, #[default] @@ -649,8 +656,9 @@ pub enum CellularReturnType { Division, } +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[widget(Dropdown)] pub enum DomainWarpType { #[default] @@ -762,8 +770,9 @@ fn channel_mixer>( } #[repr(u32)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] #[widget(Radio)] pub enum RelativeAbsolute { #[default] @@ -772,8 +781,9 @@ pub enum RelativeAbsolute { } #[repr(u32)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)] -#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))] pub enum SelectiveColorChoice { #[default] Reds, diff --git a/node-graph/nodes/raster/src/curve.rs b/node-graph/nodes/raster/src/curve.rs index 5aa49bbd37..2ba1d84cbf 100644 --- a/node-graph/nodes/raster/src/curve.rs +++ b/node-graph/nodes/raster/src/curve.rs @@ -4,7 +4,8 @@ use dyn_any::{DynAny, StaticType, StaticTypeSized}; use std::hash::{Hash, Hasher}; use std::ops::{Add, Mul, Sub}; -#[derive(Debug, Clone, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub struct Curve { #[serde(rename = "manipulatorGroups")] pub manipulator_groups: Vec, @@ -31,7 +32,8 @@ impl Hash for Curve { } } -#[derive(Debug, Clone, Copy, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub struct CurveManipulatorGroup { pub anchor: [f32; 2], pub handles: [[f32; 2]; 2], diff --git a/node-graph/nodes/text/Cargo.toml b/node-graph/nodes/text/Cargo.toml index fb22be08c9..3e542ced88 100644 --- a/node-graph/nodes/text/Cargo.toml +++ b/node-graph/nodes/text/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] +wasm = ["core-types/wasm", "dep:tsify", "dep:wasm-bindgen"] [dependencies] # Local dependencies @@ -24,3 +25,5 @@ log = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } +tsify = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } diff --git a/node-graph/nodes/text/src/font_cache.rs b/node-graph/nodes/text/src/font_cache.rs index 8875448897..58111bda21 100644 --- a/node-graph/nodes/text/src/font_cache.rs +++ b/node-graph/nodes/text/src/font_cache.rs @@ -3,11 +3,9 @@ use parley::fontique::Blob; use std::collections::HashMap; use std::sync::Arc; -// Import specta so derive macros can find it -use core_types::specta; - /// A font type (storing font family and font style and an optional preview URL) -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, DynAny, core_types::specta::Type)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, DynAny)] pub struct Font { #[serde(rename = "fontFamily")] pub font_family: String, diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index b4ac6f4566..ca4738ffa1 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -12,12 +12,10 @@ pub use to_path::*; pub use core_types as gcore; pub use vector_types; -// Import specta so derive macros can find it -use core_types::specta; - /// Alignment of lines of type within a text block. #[repr(C)] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, core_types::specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum TextAlign { #[default] diff --git a/node-graph/nodes/vector/Cargo.toml b/node-graph/nodes/vector/Cargo.toml index 47ccb0de84..0057e0db9c 100644 --- a/node-graph/nodes/vector/Cargo.toml +++ b/node-graph/nodes/vector/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] +wasm = ["core-types/wasm", "dep:tsify", "dep:wasm-bindgen"] [dependencies] # Local dependencies @@ -28,6 +29,8 @@ qrcodegen = { workspace = true } # Optional workspace dependencies serde = { workspace = true, optional = true } +tsify = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } [dev-dependencies] graphene-core = { workspace = true } diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 6527c9c3a6..5a1297c0ac 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -1,6 +1,6 @@ use core_types::registry::types::{Angle, PixelSize}; use core_types::table::Table; -use core_types::{Ctx, specta}; +use core_types::Ctx; use dyn_any::DynAny; use glam::DVec2; use graphic_types::Vector; @@ -187,7 +187,8 @@ fn star( Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter))) } -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] #[widget(Radio)] pub enum QRCodeErrorCorrectionLevel { /// Allows recovery from up to 7% data loss. From fd930d96f6bb5928562c4c7dfc7180d54e5e2956 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 03:21:04 -0800 Subject: [PATCH 02/28] Adopt the generated FillColor/Color/GradientStops --- .../floating-menus/ColorPicker.svelte | 69 +++++----- .../src/components/panels/Document.svelte | 9 +- .../widgets/inputs/ColorInput.svelte | 15 ++- .../widgets/inputs/SpectrumInput.svelte | 10 +- .../widgets/inputs/WorkingColorsInput.svelte | 16 ++- frontend/src/utility-functions/colors.ts | 122 ++++++++---------- 6 files changed, 120 insertions(+), 121 deletions(-) diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index ac6ccb3180..7ddc65326c 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -6,10 +6,9 @@ import type { TooltipState } from "@graphite/state-providers/tooltip"; import { contrastingOutlineFactor, - isColor, - isGradient, + fillChoiceColor, + fillChoiceGradientStops, createColor, - createNoneColor, createColorFromHSVA, colorFromCSS, colorToRgb255, @@ -70,17 +69,19 @@ // TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed export let open: boolean; - const colorForHSVA = isColor(colorOrGradient) ? colorOrGradient : gradientFirstColor(colorOrGradient); + const initSolidColor = fillChoiceColor(colorOrGradient); + const initGradientStops = fillChoiceGradientStops(colorOrGradient); + const colorForHSVA = initSolidColor || (initGradientStops ? gradientFirstColor(initGradientStops) : undefined); const hsvOrNone = colorForHSVA ? colorToHSV(colorForHSVA) : undefined; const hsv = hsvOrNone || { h: 0, s: 0, v: 0 }; // Gradient color stops - $: gradient = isGradient(colorOrGradient) ? colorOrGradient : undefined; + $: gradient = fillChoiceGradientStops(colorOrGradient); let activeIndex = 0 as number | undefined; let activeIndexIsMidpoint = false; - $: selectedGradientColor = (activeIndex !== undefined && gradient?.color[activeIndex]) || (colorFromCSS("black") as Color); + $: selectedGradientColor = (activeIndex !== undefined && gradient?.color[activeIndex]) || colorFromCSS("black") || createColor(0, 0, 0, 1); // Currently viewed color - $: color = isColor(colorOrGradient) ? colorOrGradient : selectedGradientColor; + $: color = fillChoiceColor(colorOrGradient) || selectedGradientColor; // New color components let hue = hsv.h; let saturation = hsv.s; @@ -115,14 +116,17 @@ $: watchOpen(open); $: watchColor(color); - $: oldColor = oldIsNone ? createNoneColor() : createColorFromHSVA(oldHue, oldSaturation, oldValue, oldAlpha); - $: newColor = isNone ? createNoneColor() : createColorFromHSVA(hue, saturation, value, alpha); - $: rgbChannels = Object.entries(colorToRgb255(newColor) || { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][]; + $: oldColor = oldIsNone ? undefined : createColorFromHSVA(oldHue, oldSaturation, oldValue, oldAlpha); + $: newColor = isNone ? undefined : createColorFromHSVA(hue, saturation, value, alpha); + $: rgbChannels = Object.entries(newColor ? colorToRgb255(newColor) : { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][]; $: hsvChannels = Object.entries(!isNone ? { h: hue * 360, s: saturation * 100, v: value * 100 } : { h: undefined, s: undefined, v: undefined }) as [keyof HSV, number | undefined][]; $: opaqueHueColor = createColorFromHSVA(hue, 1, 1, 1); - $: outlineFactor = Math.max(contrastingOutlineFactor(newColor, "--color-2-mildblack", 0.01), contrastingOutlineFactor(oldColor, "--color-2-mildblack", 0.01)); + $: outlineFactor = Math.max( + contrastingOutlineFactor(newColor ? { Solid: newColor } : ("None" as const), "--color-2-mildblack", 0.01), + contrastingOutlineFactor(oldColor ? { Solid: oldColor } : ("None" as const), "--color-2-mildblack", 0.01), + ); $: outlined = outlineFactor > 0.0001; - $: transparency = newColor.alpha < 1 || oldColor.alpha < 1; + $: transparency = (newColor?.alpha ?? 1) < 1 || (oldColor?.alpha ?? 1) < 1; async function watchOpen(open: boolean) { if (open) { @@ -136,11 +140,6 @@ function watchColor(color: Color) { const hsv = colorToHSV(color); - if (hsv === undefined) { - setNewHSVA(0, 0, 0, 1, true); - return; - } - // Update the hue, but only if it is necessary so we don't: // - ...jump the user's hue from 360° (top) to the equivalent 0° (bottom) // - ...reset the hue to 0° if the color is fully desaturated, where all hues are equivalent @@ -301,14 +300,20 @@ setColor(color); } - function setColor(color?: Color) { + function setColor(color?: Color | "None") { + if (color === "None") { + dispatch("colorOrGradient", "None"); + return; + } + const colorToEmit = color || createColorFromHSVA(hue, saturation, value, alpha); - if (gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.position[activeIndex] !== undefined && isGradient(colorOrGradient)) { - colorOrGradient.color[activeIndex] = colorToEmit; + if (gradientSpectrumInputWidget && activeIndex !== undefined && gradient && gradient.position[activeIndex] !== undefined) { + const gradientStops = fillChoiceGradientStops(colorOrGradient); + if (gradientStops) gradientStops.color[activeIndex] = colorToEmit; } - dispatch("colorOrGradient", gradient || colorToEmit); + dispatch("colorOrGradient", gradient ? { Gradient: gradient } : { Solid: colorToEmit }); } function swapNewWithOld() { @@ -323,7 +328,7 @@ setNewHSVA(oldHue, oldSaturation, oldValue, oldAlpha, oldIsNone); setOldHSVA(tempHue, tempSaturation, tempValue, tempAlpha, tempIsNone); - setColor(old); + setColor(old || "None"); } function setColorCode(colorCode: string) { @@ -333,7 +338,7 @@ function setColorRGB(channel: keyof RGB, strength: number | undefined) { // Do nothing if the given value is undefined - if (strength === undefined) return undefined; + if (strength === undefined || !newColor) return undefined; // Set the specified channel to the given value else if (channel === "r") setColor(createColor(strength / 255, newColor.green, newColor.blue, newColor.alpha)); else if (channel === "g") setColor(createColor(newColor.red, strength / 255, newColor.blue, newColor.alpha)); @@ -368,7 +373,7 @@ if (preset === "none") { setNewHSVA(0, 0, 0, 1, true); - setColor(createNoneColor()); + setColor("None"); } else { const presetColor = createColor(...PURE_COLORS[preset], 1); const hsv = colorToHSV(presetColor); @@ -427,8 +432,8 @@ setColor(color); - setNewHSVA(hsv.h, hsv.s, hsv.v, color.alpha, color.none); - setOldHSVA(hsv.h, hsv.s, hsv.v, color.alpha, color.none); + setNewHSVA(hsv.h, hsv.s, hsv.v, color.alpha, false); + setOldHSVA(hsv.h, hsv.s, hsv.v, color.alpha, false); } export function div(): HTMLDivElement | undefined { @@ -443,14 +448,14 @@ {@const hueDescription = "The shade along the spectrum of the rainbow."} @@ -514,7 +519,7 @@ dispatch("colorOrGradient", gradient)} + on:gradient={() => dispatch("colorOrGradient", gradient ? { Gradient: gradient } : "None")} on:activeMarkerIndexChange={gradientActiveMarkerIndexChange} activeMarkerIndex={activeIndex} activeMarkerIsMidpoint={activeIndexIsMidpoint} @@ -568,7 +573,7 @@ { dispatch("startHistoryTransaction"); diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index f1c9d9d873..51b91e07b3 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -5,7 +5,7 @@ import type { Color, FrontendMessages, MenuDirection, MouseCursorIcon } from "@graphite/messages"; import type { AppWindowState } from "@graphite/state-providers/app-window"; import type { DocumentState } from "@graphite/state-providers/document"; - import { isColor, createColor } from "@graphite/utility-functions/colors"; + import { fillChoiceColor, createColor } from "@graphite/utility-functions/colors"; import { pasteFile } from "@graphite/utility-functions/files"; import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry"; import { rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization"; @@ -614,11 +614,10 @@ gradientStopPickerColor = undefined; } }} - colorOrGradient={gradientStopPickerColor || createColor(0, 0, 0, 1)} + colorOrGradient={{ Solid: gradientStopPickerColor || createColor(0, 0, 0, 1) }} on:colorOrGradient={({ detail }) => { - if (isColor(detail)) { - editor.handle.updateGradientStopColor(detail.red, detail.green, detail.blue, detail.alpha); - } + const color = fillChoiceColor(detail); + if (color) editor.handle.updateGradientStopColor(color.red, color.green, color.blue, color.alpha); }} on:startHistoryTransaction={() => editor.handle.startGradientStopColorTransaction()} on:commitHistoryTransaction={() => editor.handle.commitGradientStopColorTransaction()} diff --git a/frontend/src/components/widgets/inputs/ColorInput.svelte b/frontend/src/components/widgets/inputs/ColorInput.svelte index 1e0b401de8..9ce6ae3675 100644 --- a/frontend/src/components/widgets/inputs/ColorInput.svelte +++ b/frontend/src/components/widgets/inputs/ColorInput.svelte @@ -2,8 +2,7 @@ import { createEventDispatcher } from "svelte"; import type { FillChoice, MenuDirection, ActionShortcut } from "@graphite/messages"; - import type { Color } from "@graphite/messages"; - import { contrastingOutlineFactor, isColor, isGradient, colorToHexOptionalAlpha, gradientToLinearGradientCSS } from "@graphite/utility-functions/colors"; + import { contrastingOutlineFactor, fillChoiceColor, fillChoiceGradientStops, colorToHexOptionalAlpha, gradientToLinearGradientCSS } from "@graphite/utility-functions/colors"; import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; @@ -27,9 +26,15 @@ $: outlineFactor = contrastingOutlineFactor(value, ["--color-1-nearblack", "--color-3-darkgray"], 0.01); $: outlined = outlineFactor > 0.0001; - $: chosenGradient = isGradient(value) ? gradientToLinearGradientCSS(value) : `linear-gradient(${colorToHexOptionalAlpha(value)}, ${colorToHexOptionalAlpha(value)})`; - $: none = isColor(value) ? value.none : false; - $: transparency = isGradient(value) ? value.color.some((color: Color) => color.alpha < 1) : value.alpha < 1; + $: gradientStops = fillChoiceGradientStops(value); + $: solidColor = fillChoiceColor(value); + $: chosenGradient = gradientStops + ? gradientToLinearGradientCSS(gradientStops) + : solidColor + ? `linear-gradient(${colorToHexOptionalAlpha(solidColor)}, ${colorToHexOptionalAlpha(solidColor)})` + : undefined; + $: none = value === "None"; + $: transparency = gradientStops ? gradientStops.color.some((color) => color.alpha < 1) : solidColor ? solidColor.alpha < 1 : false; diff --git a/frontend/src/components/widgets/inputs/SpectrumInput.svelte b/frontend/src/components/widgets/inputs/SpectrumInput.svelte index ac7a8d40ab..f3b101fa7a 100644 --- a/frontend/src/components/widgets/inputs/SpectrumInput.svelte +++ b/frontend/src/components/widgets/inputs/SpectrumInput.svelte @@ -7,7 +7,7 @@ import { createEventDispatcher, onDestroy } from "svelte"; import { evaluateGradientAtPosition } from "@graphite/../wasm/pkg/graphite_wasm"; - import type { Color, Gradient } from "@graphite/messages"; + import type { Color, GradientStops } from "@graphite/messages"; import { createColor, colorToHexOptionalAlpha, colorToRgbCSS, gradientFirstColor, gradientLastColor, gradientToLinearGradientCSS } from "@graphite/utility-functions/colors"; import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte"; @@ -17,9 +17,9 @@ const BUTTON_LEFT = 0; const BUTTON_RIGHT = 2; - const dispatch = createEventDispatcher<{ activeMarkerIndexChange: { activeMarkerIndex: number | undefined; activeMarkerIsMidpoint: boolean }; gradient: Gradient; dragging: boolean }>(); + const dispatch = createEventDispatcher<{ activeMarkerIndexChange: { activeMarkerIndex: number | undefined; activeMarkerIsMidpoint: boolean }; gradient: GradientStops; dragging: boolean }>(); - export let gradient: Gradient; + export let gradient: GradientStops; export let disabled = false; export let activeMarkerIndex = 0 as number | undefined; export let activeMarkerIsMidpoint = false; @@ -243,7 +243,7 @@ dispatch("gradient", gradient); } - function toMarkers(gradient: Gradient): { position: number; midpoint: number; color: Color }[] { + function toMarkers(gradient: GradientStops): { position: number; midpoint: number; color: Color }[] { return gradient.position.map((position, i) => ({ position, midpoint: gradient.midpoint[i], @@ -251,7 +251,7 @@ })); } - function toMidpoints(gradient: Gradient): number[] { + function toMidpoints(gradient: GradientStops): number[] { if (gradient.position.length < 2) return []; return gradient.midpoint.slice(0, -1).map((midpoint, i) => { diff --git a/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte b/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte index 60346be8a8..87f43d5d17 100644 --- a/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte +++ b/frontend/src/components/widgets/inputs/WorkingColorsInput.svelte @@ -3,7 +3,7 @@ import type { Editor } from "@graphite/editor"; import type { Color } from "@graphite/messages"; - import { isColor, colorToRgbaCSS } from "@graphite/utility-functions/colors"; + import { fillChoiceColor, colorToRgbaCSS } from "@graphite/utility-functions/colors"; import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; @@ -43,8 +43,11 @@ (primaryOpen = detail)} - colorOrGradient={primary} - on:colorOrGradient={({ detail }) => isColor(detail) && primaryColorChanged(detail)} + colorOrGradient={{ Solid: primary }} + on:colorOrGradient={({ detail }) => { + const color = fillChoiceColor(detail); + if (color) primaryColorChanged(color); + }} direction="Right" /> @@ -53,8 +56,11 @@ (secondaryOpen = detail)} - colorOrGradient={secondary} - on:colorOrGradient={({ detail }) => isColor(detail) && secondaryColorChanged(detail)} + colorOrGradient={{ Solid: secondary }} + on:colorOrGradient={({ detail }) => { + const color = fillChoiceColor(detail); + if (color) secondaryColorChanged(color); + }} direction="Right" /> diff --git a/frontend/src/utility-functions/colors.ts b/frontend/src/utility-functions/colors.ts index 033657366c..04cf835218 100644 --- a/frontend/src/utility-functions/colors.ts +++ b/frontend/src/utility-functions/colors.ts @@ -1,37 +1,32 @@ import { sampleInterpolatedGradient } from "@graphite/../wasm/pkg/graphite_wasm"; -import type { Color, FillChoice, Gradient } from "@graphite/messages"; +import type { Color, FillChoice, GradientStops } from "@graphite/messages"; // Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers export type HSV = { h: number; s: number; v: number }; export type RGB = { r: number; g: number; b: number }; -export type OptionalColor = Color & { none: boolean }; // COLOR FACTORY FUNCTIONS -export function createColor(red: number, green: number, blue: number, alpha: number): OptionalColor { - return { red, green, blue, alpha, none: false }; +export function createColor(red: number, green: number, blue: number, alpha: number): Color { + return { red, green, blue, alpha }; } -export function createNoneColor(): OptionalColor { - return { red: 0, green: 0, blue: 0, alpha: 1, none: true }; -} - -export function createColorFromHSVA(h: number, s: number, v: number, a: number): OptionalColor { +export function createColorFromHSVA(h: number, s: number, v: number, a: number): Color { const convert = (n: number): number => { const k = (n + h * 6) % 6; return v - v * s * Math.max(Math.min(...[k, 4 - k, 1]), 0); }; - return { red: convert(5), green: convert(3), blue: convert(1), alpha: a, none: false }; + return { red: convert(5), green: convert(3), blue: convert(1), alpha: a }; } // COLOR UTILITY FUNCTIONS -export function isColor(value: unknown): value is OptionalColor { +export function isColor(value: unknown): value is Color { return typeof value === "object" && value !== null && "red" in value; } -export function colorFromCSS(colorCode: string): OptionalColor | undefined { +export function colorFromCSS(colorCode: string): Color | undefined { // Allow single-digit hex value inputs let colorValue = colorCode.trim(); if (colorValue.length === 2 && colorValue.charAt(0) === "#" && /[0-9a-f]/i.test(colorValue.charAt(1))) { @@ -68,15 +63,13 @@ export function colorFromCSS(colorCode: string): OptionalColor | undefined { return createColor(r / 255, g / 255, b / 255, a / 255); } -export function colorEquals(c1: OptionalColor, c2: OptionalColor): boolean { - if (c1.none !== c2.none) return false; - if (c1.none && c2.none) return true; +export function colorEquals(c1: Color | undefined, c2: Color | undefined): boolean { + if (c1 === undefined && c2 === undefined) return true; + if (c1 === undefined || c2 === undefined) return false; return Math.abs(c1.red - c2.red) < 1e-6 && Math.abs(c1.green - c2.green) < 1e-6 && Math.abs(c1.blue - c2.blue) < 1e-6 && Math.abs(c1.alpha - c2.alpha) < 1e-6; } -export function colorToHexNoAlpha(color: OptionalColor): string | undefined { - if (color.none) return undefined; - +export function colorToHexNoAlpha(color: Color): string { const r = Math.round(color.red * 255) .toString(16) .padStart(2, "0"); @@ -90,9 +83,7 @@ export function colorToHexNoAlpha(color: OptionalColor): string | undefined { return `#${r}${g}${b}`; } -export function colorToHexOptionalAlpha(color: OptionalColor): string | undefined { - if (color.none) return undefined; - +export function colorToHexOptionalAlpha(color: Color): string { const hex = colorToHexNoAlpha(color); const a = Math.round(color.alpha * 255) .toString(16) @@ -101,9 +92,7 @@ export function colorToHexOptionalAlpha(color: OptionalColor): string | undefine return a === "ff" ? hex : `${hex}${a}`; } -export function colorToRgb255(color: OptionalColor): RGB | undefined { - if (color.none) return undefined; - +export function colorToRgb255(color: Color): RGB { return { r: Math.round(color.red * 255), g: Math.round(color.green * 255), @@ -111,23 +100,19 @@ export function colorToRgb255(color: OptionalColor): RGB | undefined { }; } -export function colorToRgbCSS(color: OptionalColor): string | undefined { +export function colorToRgbCSS(color: Color): string { const rgb = colorToRgb255(color); - if (!rgb) return undefined; return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; } -export function colorToRgbaCSS(color: OptionalColor): string | undefined { +export function colorToRgbaCSS(color: Color): string { const rgb = colorToRgb255(color); - if (!rgb) return undefined; return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${color.alpha})`; } -export function colorToHSV(color: OptionalColor): HSV | undefined { - if (color.none) return undefined; - +export function colorToHSV(color: Color): HSV { const { red: r, green: g, blue: b } = color; const max = Math.max(r, g, b); @@ -157,15 +142,11 @@ export function colorToHSV(color: OptionalColor): HSV | undefined { return { h, s, v }; } -export function colorOpaque(color: OptionalColor): OptionalColor | undefined { - if (color.none) return undefined; - +export function colorOpaque(color: Color): Color { return createColor(color.red, color.green, color.blue, 1); } -export function colorLuminance(color: OptionalColor): number | undefined { - if (color.none) return undefined; - +export function colorLuminance(color: Color): number { // Convert alpha into white const r = color.red * color.alpha + (1 - color.alpha); const g = color.green * color.alpha + (1 - color.alpha); @@ -180,48 +161,51 @@ export function colorLuminance(color: OptionalColor): number | undefined { return linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722; } -export function colorContrastingColor(color: OptionalColor): "black" | "white" { - if (color.none) return "black"; +export function colorContrastingColor(color: Color | undefined): "black" | "white" { + if (!color) return "black"; const luminance = colorLuminance(color); - return luminance && luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white"; + return luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white"; } export function contrastingOutlineFactor(value: FillChoice, proximityColor: string | [string, string], proximityRange: number): number { const pair = Array.isArray(proximityColor) ? [proximityColor[0], proximityColor[1]] : [proximityColor, proximityColor]; - const [range1, range2] = pair.map((color) => colorFromCSS(window.getComputedStyle(document.body).getPropertyValue(color)) || createNoneColor()); + const [range1, range2] = pair.map((color) => colorFromCSS(window.getComputedStyle(document.body).getPropertyValue(color))); + + const contrast = (color: Color | undefined): number => { + if (!color) return 0; - const contrast = (color: OptionalColor): number => { - const lum = colorLuminance(color) || 0; - let rangeLuminance1 = colorLuminance(range1) || 0; - let rangeLuminance2 = colorLuminance(range2) || 0; + const lum = colorLuminance(color); + let rangeLuminance1 = range1 ? colorLuminance(range1) : 0; + let rangeLuminance2 = range2 ? colorLuminance(range2) : 0; [rangeLuminance1, rangeLuminance2] = [Math.min(rangeLuminance1, rangeLuminance2), Math.max(rangeLuminance1, rangeLuminance2)]; const distance = Math.max(0, rangeLuminance1 - lum, lum - rangeLuminance2); - return (1 - Math.min(distance / proximityRange, 1)) * (1 - (colorToHSV(color)?.s || 0)); + return (1 - Math.min(distance / proximityRange, 1)) * (1 - colorToHSV(color).s); }; - if (isGradient(value)) { - if (value.color.length === 0) return 0; + const gradientStops = fillChoiceGradientStops(value); + if (gradientStops) { + if (gradientStops.color.length === 0) return 0; - const first = contrast(value.color[0]); - const last = contrast(value.color[value.color.length - 1]); + const first = contrast(gradientStops.color[0]); + const last = contrast(gradientStops.color[gradientStops.color.length - 1]); return Math.min(first, last); } - return contrast(value); + return contrast(fillChoiceColor(value)); } // GRADIENT UTILITY FUNCTIONS -export function isGradient(value: unknown): value is Gradient { - return typeof value === "object" && value !== null && "position" in value && "midpoint" in value; +export function isGradientStops(value: unknown): value is GradientStops { + return typeof value === "object" && value !== null && "position" in value && "midpoint" in value && "color" in value; } -export function gradientToLinearGradientCSS(gradient: Gradient): string { +export function gradientToLinearGradientCSS(gradient: GradientStops): string { if (gradient.position.length === 1) { return `linear-gradient(to right, ${colorToHexOptionalAlpha(gradient.color[0])} 0%, ${colorToHexOptionalAlpha(gradient.color[0])} 100%)`; } @@ -230,29 +214,29 @@ export function gradientToLinearGradientCSS(gradient: Gradient): string { return `linear-gradient(to right, ${pieces})`; } -export function gradientFirstColor(gradient: Gradient): OptionalColor | undefined { +export function gradientFirstColor(gradient: GradientStops): Color | undefined { return gradient.color[0]; } -export function gradientLastColor(gradient: Gradient): OptionalColor | undefined { +export function gradientLastColor(gradient: GradientStops): Color | undefined { return gradient.color[gradient.color.length - 1]; } // FILL CHOICE UTILITY FUNCTIONS -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function parseFillChoice(value: any): FillChoice { - if (isColor(value)) return value; - if (isGradient(value)) return value; - - const gradient: Gradient | undefined = value["Gradient"]; - if (gradient) { - const color = gradient.color.map((c) => createColor(c.red, c.green, c.blue, c.alpha)); - return { ...gradient, color }; - } +export function fillChoiceColor(value: FillChoice): Color | undefined { + if (typeof value === "object" && "Solid" in value) return value.Solid; + return undefined; +} - const solid = value["Solid"]; - if (solid) return createColor(solid.red, solid.green, solid.blue, solid.alpha); +export function fillChoiceGradientStops(value: FillChoice): GradientStops | undefined { + if (typeof value === "object" && "Gradient" in value) return value.Gradient; + return undefined; +} - return createNoneColor(); +export function parseFillChoice(value: unknown): FillChoice { + if (value === "None" || value === undefined || value === null) return "None"; + if (typeof value === "object" && value !== null && "Solid" in value && isColor(value.Solid)) return { Solid: value.Solid }; + if (typeof value === "object" && value !== null && "Gradient" in value && isGradientStops(value.Gradient)) return { Gradient: value.Gradient }; + return "None"; } From 092485d4e3347f510cf188934f1db5beaf65f27d Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 14:25:07 -0800 Subject: [PATCH 03/28] Fix widget typing --- .../src/components/panels/Document.svelte | 6 +- .../components/widgets/WidgetLayout.svelte | 2 +- .../components/widgets/WidgetSection.svelte | 25 ++-- .../src/components/widgets/WidgetSpan.svelte | 113 ++++++++-------- .../src/components/widgets/WidgetTable.svelte | 9 +- frontend/src/messages_old.ts | 4 +- frontend/src/subscription-router.ts | 3 +- frontend/src/utility-functions/widgets.ts | 121 +++++------------- 8 files changed, 120 insertions(+), 163 deletions(-) diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 51b91e07b3..6c359ca245 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -93,7 +93,7 @@ $: canvasHeightScaledRoundedToEven = canvasHeightScaled && (canvasHeightScaled % 2 === 1 ? canvasHeightScaled + 1 : canvasHeightScaled); $: toolShelfTotalToolsAndSeparators = ((layoutGroup) => { - if (!isWidgetSpanRow(layoutGroup)) return undefined; + if (!layoutGroup || !isWidgetSpanRow(layoutGroup)) return undefined; let totalSeparators = 0; let totalToolRowsFor1Columns = 0; @@ -108,8 +108,8 @@ }; let toolsInCurrentGroup = 0; - layoutGroup.rowWidgets.forEach((widget) => { - if (widget.props.kind === "Separator") { + layoutGroup.row.rowWidgets.forEach((widget) => { + if ("Separator" in widget.widget) { totalSeparators += 1; tally(); } else { diff --git a/frontend/src/components/widgets/WidgetLayout.svelte b/frontend/src/components/widgets/WidgetLayout.svelte index 136c81dea2..f224f0d6ff 100644 --- a/frontend/src/components/widgets/WidgetLayout.svelte +++ b/frontend/src/components/widgets/WidgetLayout.svelte @@ -19,7 +19,7 @@ {:else if isWidgetSection(layoutGroup)} {:else if isWidgetTable(layoutGroup)} - + {/if} {/each} diff --git a/frontend/src/components/widgets/WidgetSection.svelte b/frontend/src/components/widgets/WidgetSection.svelte index c71a562c45..b4cc75503a 100644 --- a/frontend/src/components/widgets/WidgetSection.svelte +++ b/frontend/src/components/widgets/WidgetSection.svelte @@ -2,7 +2,8 @@ import { getContext } from "svelte"; import type { Editor } from "@graphite/editor"; - import type { WidgetSection as WidgetSectionData, LayoutTarget } from "@graphite/messages"; + import type { LayoutTarget } from "@graphite/messages"; + import type { WidgetSection as WidgetSectionData } from "@graphite/utility-functions/widgets"; import { isWidgetSpanRow, isWidgetSection } from "@graphite/utility-functions/widgets"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; @@ -26,13 +27,13 @@ {#if expanded} - {#each widgetData.layout as layoutGroup} + {#each widgetData.section.layout as layoutGroup} {#if isWidgetSpanRow(layoutGroup)} {:else if isWidgetSection(layoutGroup)} diff --git a/frontend/src/components/widgets/WidgetSpan.svelte b/frontend/src/components/widgets/WidgetSpan.svelte index f9d0e87862..6ae52e6601 100644 --- a/frontend/src/components/widgets/WidgetSpan.svelte +++ b/frontend/src/components/widgets/WidgetSpan.svelte @@ -2,10 +2,11 @@ import { getContext } from "svelte"; import type { Editor } from "@graphite/editor"; - import type { LayoutTarget, WidgetInstance, WidgetPropsNames, WidgetPropsSet, WidgetTypes, WidgetSpanColumn, WidgetSpanRow } from "@graphite/messages"; + import type { LayoutTarget, WidgetInstance } from "@graphite/messages"; import { parseFillChoice } from "@graphite/utility-functions/colors"; import { debouncer } from "@graphite/utility-functions/debounce"; - import { isWidgetSpanColumn, isWidgetSpanRow, createLayoutGroup } from "@graphite/utility-functions/widgets"; + import type { WidgetSpanColumn, WidgetSpanRow, WidgetKind } from "@graphite/utility-functions/widgets"; + import { isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/utility-functions/widgets"; import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte"; import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte"; @@ -55,8 +56,8 @@ function watchWidgets(widgetData: WidgetSpanRow | WidgetSpanColumn): WidgetInstance[] { let widgets: WidgetInstance[] = []; - if (isWidgetSpanRow(widgetData)) widgets = widgetData.rowWidgets; - else if (isWidgetSpanColumn(widgetData)) widgets = widgetData.columnWidgets; + if (isWidgetSpanRow(widgetData)) widgets = widgetData.row.rowWidgets; + else if (isWidgetSpanColumn(widgetData)) widgets = widgetData.column.columnWidgets; return widgets; } @@ -73,31 +74,37 @@ } // eslint-disable-next-line @typescript-eslint/no-explicit-any - function exclude(props: WidgetPropsSet, additional?: string[]): Record { - const exclusions = new Set(["kind", ...(additional || [])]); + function exclude(props: Record, additional: string[]): Record { + const exclusions = new Set(additional); return Object.fromEntries(Object.entries(props).filter(([key]) => !exclusions.has(key))); } + // Extracts the kind name and props from a Widget tagged enum (e.g. `{ TextButton: { label: "..." } }` -> `["TextButton", { label: "..." }]`) + function unwrapWidget(widgetInstance: WidgetInstance): [WidgetKind, Record] { + const entries = Object.entries(widgetInstance.widget); + return entries[0] as [WidgetKind, Record]; + } + type WidgetConfig = { // eslint-disable-next-line @typescript-eslint/no-explicit-any component: any; // eslint-disable-next-line @typescript-eslint/no-explicit-any - getProps(props: WidgetPropsSet, widgetIndex: number): Record | undefined; - getSlotContent?(props: WidgetPropsSet): string; + getProps(props: Record, widgetIndex: number): Record | undefined; + getSlotContent?(props: Record): string; }; - const widgetRegistry: Record = { + const widgetRegistry: Record = { CheckboxInput: { component: CheckboxInput, - getProps: (props: WidgetTypes["CheckboxInput"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, $$events: { checked: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) }, }), }, ColorInput: { component: ColorInput, - getProps: (props: WidgetTypes["ColorInput"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, value: parseFillChoice(props.value), $$events: { value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false), @@ -108,8 +115,8 @@ CurveInput: { // TODO: CurvesInput is currently unused component: CurveInput, - getProps: (props: WidgetTypes["CurveInput"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, $$events: { value: (e: CustomEvent) => debouncer((value: unknown) => widgetValueCommitAndUpdate(index, value, false), { debounceTime: 120 }).debounceUpdateValue(e.detail), }, @@ -117,8 +124,8 @@ }, DropdownInput: { component: DropdownInput, - getProps: (props: WidgetTypes["DropdownInput"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, $$events: { hoverInEntry: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false), hoverOutEntry: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false), @@ -128,51 +135,51 @@ }, ParameterExposeButton: { component: ParameterExposeButton, - getProps: (props: WidgetTypes["ParameterExposeButton"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, action: () => widgetValueCommitAndUpdate(index, undefined, true), }), }, IconButton: { component: IconButton, - getProps: (props: WidgetTypes["IconButton"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, action: () => widgetValueCommitAndUpdate(index, undefined, true), }), }, IconLabel: { component: IconLabel, - getProps: (props: WidgetTypes["IconLabel"]) => exclude(props), + getProps: (props) => ({ ...props }), }, ShortcutLabel: { component: ShortcutLabel, - getProps: (props: WidgetTypes["ShortcutLabel"]) => { + getProps: (props) => { if (!props.shortcut) return undefined; - return exclude(props); + return { ...props }; }, }, ImageLabel: { component: ImageLabel, - getProps: (props: WidgetTypes["ImageLabel"]) => exclude(props), + getProps: (props) => ({ ...props }), }, ImageButton: { component: ImageButton, - getProps: (props: WidgetTypes["ImageButton"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, action: () => widgetValueCommitAndUpdate(index, undefined, true), }), }, NodeCatalog: { component: NodeCatalog, - getProps: (props: WidgetTypes["NodeCatalog"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, $$events: { selectNodeType: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false) }, }), }, NumberInput: { component: NumberInput, - getProps: (props: WidgetTypes["NumberInput"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, incrementCallbackIncrease: () => widgetValueCommitAndUpdate(index, "Increment", false), incrementCallbackDecrease: () => widgetValueCommitAndUpdate(index, "Decrement", false), $$events: { @@ -183,76 +190,76 @@ }, ReferencePointInput: { component: ReferencePointInput, - getProps: (props: WidgetTypes["ReferencePointInput"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, $$events: { value: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) }, }), }, PopoverButton: { component: PopoverButton, - getProps: (props: WidgetTypes["PopoverButton"]) => ({ - ...exclude(props), + getProps: (props) => ({ + ...props, layoutTarget, - popoverLayout: props.popoverLayout.map(createLayoutGroup), }), }, RadioInput: { component: RadioInput, - getProps: (props: WidgetTypes["RadioInput"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, $$events: { selectedIndex: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) }, }), }, Separator: { component: Separator, - getProps: (props: WidgetTypes["Separator"]) => exclude(props), + getProps: (props) => ({ ...props }), }, WorkingColorsInput: { component: WorkingColorsInput, - getProps: (props: WidgetTypes["WorkingColorsInput"]) => exclude(props), + getProps: (props) => ({ ...props }), }, TextAreaInput: { component: TextAreaInput, - getProps: (props: WidgetTypes["TextAreaInput"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, $$events: { commitText: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false) }, }), }, TextButton: { component: TextButton, - getProps: (props: WidgetTypes["TextButton"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, action: () => widgetValueCommitAndUpdate(index, [], true), $$events: { selectedEntryValuePath: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false) }, }), }, BreadcrumbTrailButtons: { component: BreadcrumbTrailButtons, - getProps: (props: WidgetTypes["BreadcrumbTrailButtons"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, action: (breadcrumbIndex: number) => widgetValueCommitAndUpdate(index, breadcrumbIndex, true), }), }, TextInput: { component: TextInput, - getProps: (props: WidgetTypes["TextInput"], index) => ({ - ...exclude(props), + getProps: (props, index) => ({ + ...props, $$events: { commitText: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) }, }), }, TextLabel: { component: TextLabel, - getProps: (props: WidgetTypes["TextLabel"]) => exclude(props, ["value"]), - getSlotContent: (props: WidgetTypes["TextLabel"]) => props.value, + getProps: (props) => exclude(props, ["value"]), + getSlotContent: (props) => props.value as string, }, };

{#each widgets as widget, widgetIndex} - {@const config = widgetRegistry[widget.props.kind]} - {@const props = config?.getProps(widget.props, widgetIndex)} - {@const slot = config?.getSlotContent?.(widget.props)} + {@const [kind, widgetProps] = unwrapWidget(widget)} + {@const config = widgetRegistry[kind]} + {@const props = config?.getProps(widgetProps, widgetIndex)} + {@const slot = config?.getSlotContent?.(widgetProps)} {#if props !== undefined && slot !== undefined} {slot} {:else if props !== undefined} diff --git a/frontend/src/components/widgets/WidgetTable.svelte b/frontend/src/components/widgets/WidgetTable.svelte index 009d649b11..8187cba90e 100644 --- a/frontend/src/components/widgets/WidgetTable.svelte +++ b/frontend/src/components/widgets/WidgetTable.svelte @@ -1,5 +1,6 @@ - {#each widgetData.tableWidgets as row} + {#each widgetData.table.tableWidgets as row} {#each row as cell} {/each} diff --git a/frontend/src/messages_old.ts b/frontend/src/messages_old.ts index fb029e3ec6..4e99b679a4 100644 --- a/frontend/src/messages_old.ts +++ b/frontend/src/messages_old.ts @@ -661,9 +661,11 @@ export type LayoutTarget = | "WelcomeScreenButtons" | "WorkingColors"; +export type DiffUpdate = { layout: Layout } | { layoutGroup: LayoutGroup } | { widget: WidgetInstance }; + export type WidgetDiff = { widgetPath: bigint[]; - newValue: { layout: Layout } | { layoutGroup: LayoutGroup } | { widget: WidgetInstance }; + newValue: DiffUpdate; }; export type UIItem = Layout | LayoutGroup | WidgetInstance[] | WidgetInstance; diff --git a/frontend/src/subscription-router.ts b/frontend/src/subscription-router.ts index 5084c03077..f1ce31c574 100644 --- a/frontend/src/subscription-router.ts +++ b/frontend/src/subscription-router.ts @@ -1,5 +1,4 @@ import type { FrontendMessage, FrontendMessages, LayoutTarget, WidgetDiff, ToMessageMap } from "@graphite/messages"; -import { parseWidgetDiffs } from "@graphite/utility-functions/widgets"; export function createSubscriptionRouter() { // Callbacks are wrapped at subscription time to capture their type-specific data extraction in a closure, @@ -55,7 +54,7 @@ export function createSubscriptionRouter() { getHandler = () => { const layoutCallback = layoutCallbacks[layoutTarget]; if (!layoutCallback) return undefined; - return () => layoutCallback(parseWidgetDiffs(diff)); + return () => layoutCallback(diff); }; } diff --git a/frontend/src/utility-functions/widgets.ts b/frontend/src/utility-functions/widgets.ts index 19d5a13197..dcb2309b5f 100644 --- a/frontend/src/utility-functions/widgets.ts +++ b/frontend/src/utility-functions/widgets.ts @@ -1,62 +1,51 @@ -import type { Layout, LayoutGroup, UIItem, WidgetDiff, WidgetInstance, WidgetSection, WidgetSpanColumn, WidgetSpanRow, WidgetTable } from "@graphite/messages"; - -export function isWidgetSpanColumn(layoutColumn: LayoutGroup): layoutColumn is WidgetSpanColumn { - return Boolean((layoutColumn as WidgetSpanColumn)?.columnWidgets); -} - -export function isWidgetSpanRow(layoutRow: LayoutGroup): layoutRow is WidgetSpanRow { - return Boolean((layoutRow as WidgetSpanRow)?.rowWidgets); -} - -export function isWidgetTable(layoutTable: LayoutGroup): layoutTable is WidgetTable { - return Boolean((layoutTable as WidgetTable)?.tableWidgets); +import type { Layout, LayoutGroup, Widget, WidgetDiff, WidgetInstance } from "@graphite/messages"; + +type UIItem = Layout | LayoutGroup | WidgetInstance[] | WidgetInstance; +export type WidgetSpanColumn = Extract; +export type WidgetSpanRow = Extract; +export type WidgetTable = Extract; +export type WidgetSection = Extract; +type ExtractWidgetKind = T extends Record ? K & string : never; +export type WidgetKind = ExtractWidgetKind; + +export function isWidgetSpanColumn(layoutGroup: LayoutGroup): layoutGroup is WidgetSpanColumn { + return "column" in layoutGroup; } -export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSection { - return Boolean((layoutRow as WidgetSection)?.layout); +export function isWidgetSpanRow(layoutGroup: LayoutGroup): layoutGroup is WidgetSpanRow { + return "row" in layoutGroup; } -/// Unwraps the Serde tagged enum `{ widgetId, widget: { Kind: props } }` into `{ widgetId, props: { kind, ...props } }` -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function parseWidgetInstance(widgetInstance: any): WidgetInstance { - const widgetId = widgetInstance.widgetId; - - const kind = Object.keys(widgetInstance.widget)[0]; - const props = widgetInstance.widget[kind]; - props.kind = kind; - - return { widgetId, props }; +export function isWidgetTable(layoutGroup: LayoutGroup): layoutGroup is WidgetTable { + return "table" in layoutGroup; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function parseWidgetDiffs(rawDiffs: any): WidgetDiff[] { - return rawDiffs.map((diff: WidgetDiff) => { - const { widgetPath, newValue } = diff; - - if ("layout" in newValue) return { widgetPath, newValue: newValue.layout.map(createLayoutGroup) }; - if ("layoutGroup" in newValue) return { widgetPath, newValue: createLayoutGroup(newValue.layoutGroup) }; - if ("widget" in newValue) return { widgetPath, newValue: parseWidgetInstance(newValue.widget) }; - - // This code should be unreachable - throw new Error("DiffUpdate invalid"); - }); +export function isWidgetSection(layoutGroup: LayoutGroup): layoutGroup is WidgetSection { + return "section" in layoutGroup; } // Updates a widget layout based on a list of updates, giving the new layout by mutating the `layout` argument export function patchLayout(layout: /* &mut */ Layout, diffs: WidgetDiff[]) { diffs.forEach((update) => { + // Extract the actual content from the DiffUpdate tagged enum + const { newValue } = update; + let newContent: Layout | LayoutGroup | WidgetInstance; + if ("layout" in newValue) newContent = newValue.layout; + else if ("layoutGroup" in newValue) newContent = newValue.layoutGroup; + else if ("widget" in newValue) newContent = newValue.widget; + else throw new Error("DiffUpdate invalid"); + // Find the object where the diff applies to const diffObject = update.widgetPath.reduce((targetLayout: UIItem | undefined, index: bigint): UIItem | undefined => { const i = Number(index); - if (targetLayout && "columnWidgets" in targetLayout) return targetLayout.columnWidgets[i]; - if (targetLayout && "rowWidgets" in targetLayout) return targetLayout.rowWidgets[i]; - if (targetLayout && "tableWidgets" in targetLayout) return targetLayout.tableWidgets[i]; - if (targetLayout && "layout" in targetLayout) return targetLayout.layout[i]; - if (targetLayout && "props" in targetLayout && "widgetId" in targetLayout) { - if (targetLayout.props.kind === "PopoverButton" && "popoverLayout" in targetLayout.props && targetLayout.props.popoverLayout) { - targetLayout.props.popoverLayout = targetLayout.props.popoverLayout.map(createLayoutGroup); - return targetLayout.props.popoverLayout[i]; + if (targetLayout && "column" in targetLayout) return targetLayout.column.columnWidgets[i]; + if (targetLayout && "row" in targetLayout) return targetLayout.row.rowWidgets[i]; + if (targetLayout && "table" in targetLayout) return targetLayout.table.tableWidgets[i]; + if (targetLayout && "section" in targetLayout) return targetLayout.section.layout[i]; + if (targetLayout && "widget" in targetLayout && "widgetId" in targetLayout) { + if ("PopoverButton" in targetLayout.widget && targetLayout.widget.PopoverButton.popoverLayout) { + return targetLayout.widget.PopoverButton.popoverLayout[i]; } // eslint-disable-next-line no-console console.error("Tried to index widget"); @@ -86,48 +75,6 @@ export function patchLayout(layout: /* &mut */ Layout, diffs: WidgetDiff[]) { // Assign keys to the new object // `Object.assign` works but `diffObject = update.newValue;` doesn't. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign - Object.assign(diffObject, update.newValue); + Object.assign(diffObject, newContent); }); } - -// Unpacking a layout group -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createLayoutGroup(layoutGroup: any): LayoutGroup { - // Detect if this has already been parsed and, if so, return it as-is so this function can be idempotent - if ("columnWidgets" in layoutGroup || "rowWidgets" in layoutGroup || "tableWidgets" in layoutGroup || ("name" in layoutGroup && "layout" in layoutGroup)) return layoutGroup; - - if (layoutGroup.column) { - const columnWidgets = layoutGroup.column.columnWidgets.map(parseWidgetInstance); - - const result: WidgetSpanColumn = { columnWidgets }; - return result; - } - - if (layoutGroup.row) { - const result: WidgetSpanRow = { rowWidgets: layoutGroup.row.rowWidgets.map(parseWidgetInstance) }; - return result; - } - - if (layoutGroup.section) { - const result: WidgetSection = { - name: layoutGroup.section.name, - description: layoutGroup.section.description, - visible: layoutGroup.section.visible, - pinned: layoutGroup.section.pinned, - id: layoutGroup.section.id, - layout: layoutGroup.section.layout.map(createLayoutGroup), - }; - return result; - } - - if (layoutGroup.table) { - const result: WidgetTable = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - tableWidgets: layoutGroup.table.tableWidgets.map((row: any) => row.map(parseWidgetInstance)), - unstyled: layoutGroup.table.unstyled, - }; - return result; - } - - throw new Error("Layout row type does not exist"); -} From a9f1cb79037bd8d45c15d4642a31cdfa21576096 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 14:25:41 -0800 Subject: [PATCH 04/28] Separate WidgetGroup enum variants into wrapper structs --- desktop/wrapper/src/utils.rs | 2 +- editor/src/dispatcher.rs | 2 +- .../export_dialog_message_handler.rs | 10 +- .../new_document_dialog_message_handler.rs | 4 +- .../preferences_dialog_message_handler.rs | 4 +- .../simple_dialogs/about_graphite_dialog.rs | 16 +- .../close_all_documents_dialog.rs | 10 +- .../simple_dialogs/close_document_dialog.rs | 10 +- .../simple_dialogs/confirm_restart_dialog.rs | 12 +- .../simple_dialogs/demo_artwork_dialog.rs | 4 +- .../dialog/simple_dialogs/error_dialog.rs | 10 +- .../dialog/simple_dialogs/licenses_dialog.rs | 12 +- .../licenses_third_party_dialog.rs | 8 +- .../messages/layout/layout_message_handler.rs | 6 +- .../layout/utility_types/layout_widget.rs | 142 ++++++++++----- .../menu_bar/menu_bar_message_handler.rs | 2 +- .../data_panel/data_panel_message_handler.rs | 40 ++--- .../document/document_message_handler.rs | 128 +++++--------- .../node_graph/document_node_definitions.rs | 28 ++- .../node_graph/node_graph_message_handler.rs | 28 ++- .../document/node_graph/node_properties.rs | 164 ++++++++---------- .../document/overlays/grid_overlays.rs | 36 ++-- .../portfolio/portfolio_message_handler.rs | 10 +- .../messages/tool/tool_messages/brush_tool.rs | 2 +- .../tool/tool_messages/freehand_tool.rs | 2 +- .../tool/tool_messages/gradient_tool.rs | 2 +- .../messages/tool/tool_messages/path_tool.rs | 50 +++--- .../messages/tool/tool_messages/pen_tool.rs | 2 +- .../tool/tool_messages/select_tool.rs | 2 +- .../messages/tool/tool_messages/shape_tool.rs | 2 +- .../tool/tool_messages/spline_tool.rs | 2 +- .../messages/tool/tool_messages/text_tool.rs | 2 +- editor/src/messages/tool/utility_types.rs | 14 +- 33 files changed, 345 insertions(+), 423 deletions(-) diff --git a/desktop/wrapper/src/utils.rs b/desktop/wrapper/src/utils.rs index ca246dfadc..03ecadce77 100644 --- a/desktop/wrapper/src/utils.rs +++ b/desktop/wrapper/src/utils.rs @@ -15,7 +15,7 @@ pub(crate) mod menu { [layout_group] => layout_group, _ => panic!("Menu bar layout is supposed to have exactly one layout group"), }; - let LayoutGroup::Row { widgets } = layout_group else { + let LayoutGroup::Row(WidgetRow { widgets }) = layout_group else { panic!("Menu bar layout group is supposed to be a row"); }; widgets diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 4088322347..6c312ed6a2 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -596,7 +596,7 @@ mod test { } = response { if let DiffUpdate::Layout(sub_layout) = &diff[0].new_value { - if let LayoutGroup::Row { widgets } = &sub_layout.0[0] { + if let LayoutGroup::Row(WidgetRow { widgets }) = &sub_layout.0[0] { if let Widget::TextLabel(TextLabel { value, .. }) = &*widgets[0].widget { print_problem_to_terminal_on_failure(value); } diff --git a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs index 5eb151d11a..a99962d269 100644 --- a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs +++ b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs @@ -85,7 +85,7 @@ impl DialogLayoutHolder for ExportDialogMessageHandler { TextButton::new("Cancel").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(), ]; - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } @@ -170,10 +170,10 @@ impl LayoutHolder for ExportDialogMessageHandler { ]; Layout(vec![ - LayoutGroup::Row { widgets: export_type }, - LayoutGroup::Row { widgets: resolution }, - LayoutGroup::Row { widgets: export_area }, - LayoutGroup::Row { widgets: transparent_background }, + LayoutGroup::row(export_type), + LayoutGroup::row(resolution), + LayoutGroup::row(export_area), + LayoutGroup::row(transparent_background), ]) } } diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index a1fd90583b..e16a2120f6 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -71,7 +71,7 @@ impl DialogLayoutHolder for NewDocumentDialogMessageHandler { TextButton::new("Cancel").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(), ]; - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } @@ -122,6 +122,6 @@ impl LayoutHolder for NewDocumentDialogMessageHandler { .widget_instance(), ]; - Layout(vec![LayoutGroup::Row { widgets: name }, LayoutGroup::Row { widgets: infinite }, LayoutGroup::Row { widgets: scale }]) + Layout(vec![LayoutGroup::row(name), LayoutGroup::row(infinite), LayoutGroup::row(scale)]) } } diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs index dd114bc713..e4d50a86d3 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs @@ -389,7 +389,7 @@ impl PreferencesDialogMessageHandler { } } - Layout(rows.into_iter().map(|r| LayoutGroup::Row { widgets: r }).collect()) + Layout(rows.into_iter().map(|r| LayoutGroup::row(r)).collect()) } pub fn send_layout(&self, responses: &mut VecDeque, layout_target: LayoutTarget, preferences: &PreferencesMessageHandler) { @@ -416,7 +416,7 @@ impl PreferencesDialogMessageHandler { TextButton::new("Reset to Defaults").on_update(|_| PreferencesMessage::ResetToDefaults.into()).widget_instance(), ]; - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } fn send_layout_buttons(&self, responses: &mut VecDeque, layout_target: LayoutTarget) { diff --git a/editor/src/messages/dialog/simple_dialogs/about_graphite_dialog.rs b/editor/src/messages/dialog/simple_dialogs/about_graphite_dialog.rs index f9817dcf37..2dc168c77a 100644 --- a/editor/src/messages/dialog/simple_dialogs/about_graphite_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/about_graphite_dialog.rs @@ -15,7 +15,7 @@ impl DialogLayoutHolder for AboutGraphiteDialog { fn layout_buttons(&self) -> Layout { let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()]; - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } fn layout_column_2(&self) -> Layout { @@ -51,22 +51,16 @@ impl DialogLayoutHolder for AboutGraphiteDialog { .widget_instance(), ); - Layout(vec![LayoutGroup::Column { widgets }]) + Layout(vec![LayoutGroup::column(widgets)]) } } impl LayoutHolder for AboutGraphiteDialog { fn layout(&self) -> Layout { Layout(vec![ - LayoutGroup::Row { - widgets: vec![TextLabel::new("About this release").bold(true).widget_instance()], - }, - LayoutGroup::Row { - widgets: vec![TextLabel::new(commit_info_localized(&self.localized_commit_date)).multiline(true).widget_instance()], - }, - LayoutGroup::Row { - widgets: vec![TextLabel::new(format!("Copyright © {} Graphite contributors", self.localized_commit_year)).widget_instance()], - }, + LayoutGroup::row(vec![TextLabel::new("About this release").bold(true).widget_instance()]), + LayoutGroup::row(vec![TextLabel::new(commit_info_localized(&self.localized_commit_date)).multiline(true).widget_instance()]), + LayoutGroup::row(vec![TextLabel::new(format!("Copyright © {} Graphite contributors", self.localized_commit_year)).widget_instance()]), ]) } } diff --git a/editor/src/messages/dialog/simple_dialogs/close_all_documents_dialog.rs b/editor/src/messages/dialog/simple_dialogs/close_all_documents_dialog.rs index b02cb5c3d9..262250d75c 100644 --- a/editor/src/messages/dialog/simple_dialogs/close_all_documents_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/close_all_documents_dialog.rs @@ -24,7 +24,7 @@ impl DialogLayoutHolder for CloseAllDocumentsDialog { TextButton::new("Cancel").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(), ]; - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } @@ -33,12 +33,8 @@ impl LayoutHolder for CloseAllDocumentsDialog { let unsaved_list = "• ".to_string() + &self.unsaved_document_names.join("\n• "); Layout(vec![ - LayoutGroup::Row { - widgets: vec![TextLabel::new("Save documents before closing them?").bold(true).multiline(true).widget_instance()], - }, - LayoutGroup::Row { - widgets: vec![TextLabel::new(format!("Documents with unsaved changes:\n{unsaved_list}")).multiline(true).widget_instance()], - }, + LayoutGroup::row(vec![TextLabel::new("Save documents before closing them?").bold(true).multiline(true).widget_instance()]), + LayoutGroup::row(vec![TextLabel::new(format!("Documents with unsaved changes:\n{unsaved_list}")).multiline(true).widget_instance()]), ]) } } diff --git a/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs b/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs index 9e9f56d812..6f8cf66260 100644 --- a/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/close_document_dialog.rs @@ -35,7 +35,7 @@ impl DialogLayoutHolder for CloseDocumentDialog { TextButton::new("Cancel").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(), ]; - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } @@ -52,12 +52,8 @@ impl LayoutHolder for CloseDocumentDialog { let break_lines = if self.document_name.len() > max_one_line_length { '\n' } else { ' ' }; Layout(vec![ - LayoutGroup::Row { - widgets: vec![TextLabel::new("Save document before closing it?").bold(true).widget_instance()], - }, - LayoutGroup::Row { - widgets: vec![TextLabel::new(format!("\"{name}{ellipsis}\"{break_lines}has unsaved changes")).multiline(true).widget_instance()], - }, + LayoutGroup::row(vec![TextLabel::new("Save document before closing it?").bold(true).widget_instance()]), + LayoutGroup::row(vec![TextLabel::new(format!("\"{name}{ellipsis}\"{break_lines}has unsaved changes")).multiline(true).widget_instance()]), ]) } } diff --git a/editor/src/messages/dialog/simple_dialogs/confirm_restart_dialog.rs b/editor/src/messages/dialog/simple_dialogs/confirm_restart_dialog.rs index 17856f9b8c..efb77c30fd 100644 --- a/editor/src/messages/dialog/simple_dialogs/confirm_restart_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/confirm_restart_dialog.rs @@ -24,7 +24,7 @@ impl DialogLayoutHolder for ConfirmRestartDialog { TextButton::new("Later").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(), ]; - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } @@ -33,11 +33,8 @@ impl LayoutHolder for ConfirmRestartDialog { let changed_settings = "• ".to_string() + &self.changed_settings.join("\n• "); Layout(vec![ - LayoutGroup::Row { - widgets: vec![TextLabel::new("Restart to apply changes?").bold(true).multiline(true).widget_instance()], - }, - LayoutGroup::Row { - widgets: vec![ + LayoutGroup::row(vec![TextLabel::new("Restart to apply changes?").bold(true).multiline(true).widget_instance()]), + LayoutGroup::row(vec![ TextLabel::new( format!( " @@ -52,8 +49,7 @@ impl LayoutHolder for ConfirmRestartDialog { ) .multiline(true) .widget_instance(), - ], - }, + ]), ]) } } diff --git a/editor/src/messages/dialog/simple_dialogs/demo_artwork_dialog.rs b/editor/src/messages/dialog/simple_dialogs/demo_artwork_dialog.rs index 7b261b5382..effb8530cc 100644 --- a/editor/src/messages/dialog/simple_dialogs/demo_artwork_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/demo_artwork_dialog.rs @@ -22,7 +22,7 @@ impl DialogLayoutHolder for DemoArtworkDialog { fn layout_buttons(&self) -> Layout { let widgets = vec![TextButton::new("Close").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()]; - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } @@ -54,7 +54,7 @@ impl LayoutHolder for DemoArtworkDialog { .map(|(name, _, filename)| TextButton::new(*name).min_width(256).flush(true).on_update(|_| make_dialog(name, filename)).widget_instance()) .collect(); - vec![LayoutGroup::Row { widgets: images }, LayoutGroup::Row { widgets: buttons }, LayoutGroup::Row { widgets: vec![] }] + vec![LayoutGroup::row(images), LayoutGroup::row(buttons), LayoutGroup::row(vec![])] }) .collect(); let _ = rows_of_images_with_buttons.pop(); diff --git a/editor/src/messages/dialog/simple_dialogs/error_dialog.rs b/editor/src/messages/dialog/simple_dialogs/error_dialog.rs index a47c676acf..e92541a28b 100644 --- a/editor/src/messages/dialog/simple_dialogs/error_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/error_dialog.rs @@ -14,19 +14,15 @@ impl DialogLayoutHolder for ErrorDialog { fn layout_buttons(&self) -> Layout { let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()]; - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } impl LayoutHolder for ErrorDialog { fn layout(&self) -> Layout { Layout(vec![ - LayoutGroup::Row { - widgets: vec![TextLabel::new(&self.title).bold(true).widget_instance()], - }, - LayoutGroup::Row { - widgets: vec![TextLabel::new(&self.description).multiline(true).widget_instance()], - }, + LayoutGroup::row(vec![TextLabel::new(&self.title).bold(true).widget_instance()]), + LayoutGroup::row(vec![TextLabel::new(&self.description).multiline(true).widget_instance()]), ]) } } diff --git a/editor/src/messages/dialog/simple_dialogs/licenses_dialog.rs b/editor/src/messages/dialog/simple_dialogs/licenses_dialog.rs index ed74893241..bb8538e5fe 100644 --- a/editor/src/messages/dialog/simple_dialogs/licenses_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/licenses_dialog.rs @@ -12,7 +12,7 @@ impl DialogLayoutHolder for LicensesDialog { fn layout_buttons(&self) -> Layout { let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()]; - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } fn layout_column_2(&self) -> Layout { @@ -37,7 +37,7 @@ impl DialogLayoutHolder for LicensesDialog { .map(|&(icon, label, message_factory)| TextButton::new(label).icon(Some((icon).into())).flush(true).on_update(move |_| message_factory()).widget_instance()) .collect(); - Layout(vec![LayoutGroup::Column { widgets }]) + Layout(vec![LayoutGroup::column(widgets)]) } } @@ -56,12 +56,8 @@ impl LayoutHolder for LicensesDialog { let description = description.trim(); Layout(vec![ - LayoutGroup::Row { - widgets: vec![TextLabel::new("Graphite is free, open source software").bold(true).widget_instance()], - }, - LayoutGroup::Row { - widgets: vec![TextLabel::new(description).multiline(true).widget_instance()], - }, + LayoutGroup::row(vec![TextLabel::new("Graphite is free, open source software").bold(true).widget_instance()]), + LayoutGroup::row(vec![TextLabel::new(description).multiline(true).widget_instance()]), ]) } } diff --git a/editor/src/messages/dialog/simple_dialogs/licenses_third_party_dialog.rs b/editor/src/messages/dialog/simple_dialogs/licenses_third_party_dialog.rs index 890803bb00..c15d5c8d02 100644 --- a/editor/src/messages/dialog/simple_dialogs/licenses_third_party_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/licenses_third_party_dialog.rs @@ -12,7 +12,7 @@ impl DialogLayoutHolder for LicensesThirdPartyDialog { fn layout_buttons(&self) -> Layout { let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()]; - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } @@ -31,14 +31,12 @@ impl LayoutHolder for LicensesThirdPartyDialog { // Two characters (one before, one after) the sequence of underscore characters, plus one additional column to provide a space between the text and the scrollbar let non_wrapping_column_width = license_text.split('\n').map(|line| line.chars().filter(|&c| c == '_').count() as u32).max().unwrap_or(0) + 2 + 1; - Layout(vec![LayoutGroup::Row { - widgets: vec![ + Layout(vec![LayoutGroup::row(vec![ TextLabel::new(license_text) .monospace(true) .multiline(true) .min_width_characters(non_wrapping_column_width) .widget_instance(), - ], - }]) + ])]) } } diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index de3d1ab396..5f2c647a4a 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -63,7 +63,7 @@ impl LayoutMessageHandler { while let Some((mut widget_path, layout_group)) = stack.pop() { match layout_group { // Check if any of the widgets in the current column or row have the correct id - LayoutGroup::Column { widgets } | LayoutGroup::Row { widgets } => { + LayoutGroup::Column(WidgetColumn { widgets }) | LayoutGroup::Row(WidgetRow { widgets }) => { for (index, widget) in widgets.iter().enumerate() { // Return if this is the correct ID if widget.widget_id == widget_id { @@ -84,10 +84,10 @@ impl LayoutMessageHandler { } } // A section contains more LayoutGroups which we add to the stack. - LayoutGroup::Section { layout, .. } => { + LayoutGroup::Section(WidgetSection { layout, .. }) => { stack.extend(layout.0.iter().enumerate().map(|(index, val)| ([widget_path.as_slice(), &[index]].concat(), val))); } - LayoutGroup::Table { rows, .. } => { + LayoutGroup::Table(WidgetTable { rows, .. }) => { for (row_index, row) in rows.iter().enumerate() { for (cell_index, cell) in row.iter().enumerate() { // Return if this is the correct ID diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 55db2189df..9492f73413 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -246,19 +246,19 @@ impl<'a> Iterator for WidgetIter<'a> { } match self.stack.pop() { - Some(LayoutGroup::Column { widgets }) => { + Some(LayoutGroup::Column(WidgetColumn { widgets })) => { self.current_slice = Some(widgets); self.next() } - Some(LayoutGroup::Row { widgets }) => { + Some(LayoutGroup::Row(WidgetRow { widgets })) => { self.current_slice = Some(widgets); self.next() } - Some(LayoutGroup::Table { rows, .. }) => { + Some(LayoutGroup::Table(WidgetTable { rows, .. })) => { self.table.extend(rows.iter().flatten().rev()); self.next() } - Some(LayoutGroup::Section { layout, .. }) => { + Some(LayoutGroup::Section(WidgetSection { layout, .. })) => { for layout_row in &layout.0 { self.stack.push(layout_row); } @@ -298,19 +298,19 @@ impl<'a> Iterator for WidgetIterMut<'a> { } match self.stack.pop() { - Some(LayoutGroup::Column { widgets }) => { + Some(LayoutGroup::Column(WidgetColumn { widgets })) => { self.current_slice = Some(widgets); self.next() } - Some(LayoutGroup::Row { widgets }) => { + Some(LayoutGroup::Row(WidgetRow { widgets })) => { self.current_slice = Some(widgets); self.next() } - Some(LayoutGroup::Table { rows, .. }) => { + Some(LayoutGroup::Table(WidgetTable { rows, .. })) => { self.table.extend(rows.iter_mut().flatten().rev()); self.next() } - Some(LayoutGroup::Section { layout, .. }) => { + Some(LayoutGroup::Section(WidgetSection { layout, .. })) => { for layout_row in &mut layout.0 { self.stack.push(layout_row); } @@ -325,49 +325,89 @@ impl<'a> Iterator for WidgetIterMut<'a> { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum LayoutGroup { #[serde(rename = "column")] - Column { - #[serde(rename = "columnWidgets")] - widgets: Vec, - }, + Column(WidgetColumn), #[serde(rename = "row")] - Row { - #[serde(rename = "rowWidgets")] - widgets: Vec, - }, + Row(WidgetRow), #[serde(rename = "table")] - Table { - #[serde(rename = "tableWidgets")] - rows: Vec>, - unstyled: bool, - }, + Table(WidgetTable), #[serde(rename = "section")] - Section { - name: String, - description: String, - visible: bool, - pinned: bool, - id: u64, - layout: Layout, - }, + Section(WidgetSection), +} + +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct WidgetColumn { + #[serde(rename = "columnWidgets")] + pub widgets: Vec, +} + +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct WidgetRow { + #[serde(rename = "rowWidgets")] + pub widgets: Vec, +} + +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct WidgetTable { + #[serde(rename = "tableWidgets")] + pub rows: Vec>, + pub unstyled: bool, +} + +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[tsify(large_number_types_as_bigints)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct WidgetSection { + pub name: String, + pub description: String, + pub visible: bool, + pub pinned: bool, + pub id: u64, + pub layout: Layout, } impl Default for LayoutGroup { fn default() -> Self { - Self::Row { widgets: Vec::new() } + Self::Row(Default::default()) } } impl From> for LayoutGroup { fn from(widgets: Vec) -> LayoutGroup { - LayoutGroup::Row { widgets } + LayoutGroup::Row(WidgetRow { widgets }) } } impl LayoutGroup { + pub fn row(widgets: Vec) -> Self { + Self::Row(WidgetRow { widgets }) + } + + pub fn column(widgets: Vec) -> Self { + Self::Column(WidgetColumn { widgets }) + } + + pub fn table(rows: Vec>, unstyled: bool) -> Self { + Self::Table(WidgetTable { rows, unstyled }) + } + + pub fn section(name: impl Into, description: impl Into, visible: bool, pinned: bool, id: u64, layout: Layout) -> Self { + Self::Section(WidgetSection { + name: name.into(), + description: description.into(), + visible, + pinned, + id, + layout, + }) + } + /// Applies a tooltip description to all widgets without a tooltip in this row or column. pub fn with_tooltip_description(self, description: impl Into) -> Self { let (is_col, mut widgets) = match self { - LayoutGroup::Column { widgets } => (true, widgets), - LayoutGroup::Row { widgets } => (false, widgets), + LayoutGroup::Column(WidgetColumn { widgets }) => (true, widgets), + LayoutGroup::Row(WidgetRow { widgets }) => (false, widgets), _ => unimplemented!(), }; let description = description.into(); @@ -400,7 +440,7 @@ impl LayoutGroup { val.clone_from(&description); } } - if is_col { Self::Column { widgets } } else { Self::Row { widgets } } + if is_col { Self::Column(WidgetColumn { widgets }) } else { Self::Row(WidgetRow { widgets }) } } pub fn iter_mut(&mut self) -> WidgetIterMut<'_> { @@ -419,7 +459,8 @@ impl Diffable for LayoutGroup { fn diff(&mut self, new: Self, widget_path: &mut Vec, widget_diffs: &mut Vec) { let is_column = matches!(new, Self::Column { .. }); match (self, new) { - (Self::Column { widgets: current_widgets }, Self::Column { widgets: new_widgets }) | (Self::Row { widgets: current_widgets }, Self::Row { widgets: new_widgets }) => { + (Self::Column(WidgetColumn { widgets: current_widgets }), Self::Column(WidgetColumn { widgets: new_widgets })) + | (Self::Row(WidgetRow { widgets: current_widgets }), Self::Row(WidgetRow { widgets: new_widgets })) => { // If the lengths are different then resend the entire panel // TODO: Diff insersion and deletion of items if current_widgets.len() != new_widgets.len() { @@ -427,7 +468,12 @@ impl Diffable for LayoutGroup { current_widgets.clone_from(&new_widgets); // Push back a LayoutGroup update to the diff - let new_value = (if is_column { Self::Column { widgets: new_widgets } } else { Self::Row { widgets: new_widgets } }).into_diff_update(); + let new_value = (if is_column { + Self::Column(WidgetColumn { widgets: new_widgets }) + } else { + Self::Row(WidgetRow { widgets: new_widgets }) + }) + .into_diff_update(); let widget_path = widget_path.to_vec(); widget_diffs.push(WidgetDiff { widget_path, new_value }); return; @@ -440,22 +486,22 @@ impl Diffable for LayoutGroup { } } ( - Self::Section { + Self::Section(WidgetSection { name: current_name, description: current_description, visible: current_visible, pinned: current_pinned, id: current_id, layout: current_layout, - }, - Self::Section { + }), + Self::Section(WidgetSection { name: new_name, description: new_description, visible: new_visible, pinned: new_pinned, id: new_id, layout: new_layout, - }, + }), ) => { // Resend the entire panel if the lengths, names, visibility, or node IDs are different // TODO: Diff insersion and deletion of items @@ -475,14 +521,14 @@ impl Diffable for LayoutGroup { current_layout.clone_from(&new_layout); // Push an update layout group to the diff - let new_value = Self::Section { + let new_value = Self::Section(WidgetSection { name: new_name, description: new_description, visible: new_visible, pinned: new_pinned, id: new_id, layout: new_layout, - } + }) .into_diff_update(); let widget_path = widget_path.to_vec(); widget_diffs.push(WidgetDiff { widget_path, new_value }); @@ -507,14 +553,14 @@ impl Diffable for LayoutGroup { fn collect_checkbox_ids(&self, layout_target: LayoutTarget, widget_path: &mut Vec, checkbox_map: &mut HashMap) { match self { - Self::Column { widgets } | Self::Row { widgets } => { + Self::Column(WidgetColumn { widgets }) | Self::Row(WidgetRow { widgets }) => { for (index, widget) in widgets.iter().enumerate() { widget_path.push(index); widget.collect_checkbox_ids(layout_target, widget_path, checkbox_map); widget_path.pop(); } } - Self::Table { rows, .. } => { + Self::Table(WidgetTable { rows, .. }) => { for (row_idx, row) in rows.iter().enumerate() { for (col_idx, widget) in row.iter().enumerate() { widget_path.push(row_idx); @@ -525,7 +571,7 @@ impl Diffable for LayoutGroup { } } } - Self::Section { layout, .. } => { + Self::Section(WidgetSection { layout, .. }) => { layout.collect_checkbox_ids(layout_target, widget_path, checkbox_map); } } @@ -533,14 +579,14 @@ impl Diffable for LayoutGroup { fn replace_widget_ids(&mut self, layout_target: LayoutTarget, widget_path: &mut Vec, checkbox_map: &HashMap) { match self { - Self::Column { widgets } | Self::Row { widgets } => { + Self::Column(WidgetColumn { widgets }) | Self::Row(WidgetRow { widgets }) => { for (index, widget) in widgets.iter_mut().enumerate() { widget_path.push(index); widget.replace_widget_ids(layout_target, widget_path, checkbox_map); widget_path.pop(); } } - Self::Table { rows, .. } => { + Self::Table(WidgetTable { rows, .. }) => { for (row_idx, row) in rows.iter_mut().enumerate() { for (col_idx, widget) in row.iter_mut().enumerate() { widget_path.push(row_idx); @@ -551,7 +597,7 @@ impl Diffable for LayoutGroup { } } } - Self::Section { layout, .. } => { + Self::Section(WidgetSection { layout, .. }) => { layout.replace_widget_ids(layout_target, widget_path, checkbox_map); } } diff --git a/editor/src/messages/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/menu_bar/menu_bar_message_handler.rs index b52cc25907..7e7dadf8f5 100644 --- a/editor/src/messages/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/menu_bar/menu_bar_message_handler.rs @@ -750,6 +750,6 @@ impl LayoutHolder for MenuBarMessageHandler { .widget_instance(), ]; - Layout(vec![LayoutGroup::Row { widgets: menu_bar_buttons }]) + Layout(vec![LayoutGroup::row(menu_bar_buttons)]) } } diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index b0b9bacacb..b767b60305 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -128,7 +128,7 @@ impl DataPanelMessageHandler { } if !widgets.is_empty() { - layout.0.insert(0, LayoutGroup::Row { widgets }); + layout.0.insert(0, LayoutGroup::row(widgets)); } responses.add(LayoutMessage::SendLayout { @@ -185,7 +185,7 @@ fn column_headings(value: &[&str]) -> Vec { fn label(x: impl Into) -> Vec { let error = vec![TextLabel::new(x).widget_instance()]; - vec![LayoutGroup::Row { widgets: error }] + vec![LayoutGroup::row(error)] } trait TableRowLayout { @@ -234,7 +234,7 @@ impl TableRowLayout for Vec { rows.insert(0, column_headings(&["", "element"])); - vec![LayoutGroup::Table { rows, unstyled: false }] + vec![LayoutGroup::table(rows, false)] } } @@ -276,7 +276,7 @@ impl TableRowLayout for Table { rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"])); - vec![LayoutGroup::Table { rows, unstyled: false }] + vec![LayoutGroup::table(rows, false)] } } @@ -488,7 +488,7 @@ impl TableRowLayout for Vector { } } - vec![LayoutGroup::Row { widgets: table_tabs }, LayoutGroup::Table { rows: table_rows, unstyled: false }] + vec![LayoutGroup::row(table_tabs), LayoutGroup::table(table_rows, false)] } } @@ -504,7 +504,7 @@ impl TableRowLayout for Raster { if raster.width == 0 || raster.height == 0 { let widgets = vec![TextLabel::new("Image has no area").widget_instance()]; - return vec![LayoutGroup::Row { widgets }]; + return vec![LayoutGroup::row(widgets)]; } let base64_string = raster.base64_string.clone().unwrap_or_else(|| { @@ -519,7 +519,7 @@ impl TableRowLayout for Raster { }); let widgets = vec![ImageLabel::new(base64_string).widget_instance()]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -532,7 +532,7 @@ impl TableRowLayout for Raster { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let widgets = vec![TextLabel::new("Raster is a texture on the GPU and cannot currently be displayed here").widget_instance()]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -552,7 +552,7 @@ impl TableRowLayout for Color { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let widgets = vec![self.element_widget(0)]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -572,7 +572,7 @@ impl TableRowLayout for GradientStops { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let widgets = vec![self.element_widget(0)]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -585,7 +585,7 @@ impl TableRowLayout for f64 { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let widgets = vec![TextLabel::new(self.to_string()).widget_instance()]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -598,7 +598,7 @@ impl TableRowLayout for u32 { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let widgets = vec![TextLabel::new(self.to_string()).widget_instance()]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -611,7 +611,7 @@ impl TableRowLayout for u64 { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let widgets = vec![TextLabel::new(self.to_string()).widget_instance()]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -624,7 +624,7 @@ impl TableRowLayout for bool { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let widgets = vec![TextLabel::new(self.to_string()).widget_instance()]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -643,7 +643,7 @@ impl TableRowLayout for String { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let widgets = vec![TextAreaInput::new(self.to_string()).disabled(true).widget_instance()]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -656,7 +656,7 @@ impl TableRowLayout for Option { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let widgets = vec![TextLabel::new(format!("{self:?}")).widget_instance()]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -669,7 +669,7 @@ impl TableRowLayout for DVec2 { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let widgets = vec![TextLabel::new(format!("({}, {})", self.x, self.y)).widget_instance()]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -682,7 +682,7 @@ impl TableRowLayout for Vec2 { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let widgets = vec![TextLabel::new(format!("({}, {})", self.x, self.y)).widget_instance()]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -695,7 +695,7 @@ impl TableRowLayout for DAffine2 { } fn element_page(&self, _data: &mut LayoutData) -> Vec { let widgets = vec![TextLabel::new(format_transform_matrix(self)).widget_instance()]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } @@ -709,7 +709,7 @@ impl TableRowLayout for Affine2 { fn element_page(&self, _data: &mut LayoutData) -> Vec { let matrix = DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64)); let widgets = vec![TextLabel::new(format_transform_matrix(&matrix)).widget_instance()]; - vec![LayoutGroup::Row { widgets }] + vec![LayoutGroup::row(widgets)] } } diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index bef3980ddd..c28c5518ab 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2212,14 +2212,9 @@ impl DocumentMessageHandler { .widget_instance(), PopoverButton::new() .popover_layout(Layout(vec![ - LayoutGroup::Row { - widgets: vec![TextLabel::new("Overlays").bold(true).widget_instance()], - }, - LayoutGroup::Row { - widgets: vec![TextLabel::new("General").widget_instance()], - }, - LayoutGroup::Row { - widgets: { + LayoutGroup::row(vec![TextLabel::new("Overlays").bold(true).widget_instance()]), + LayoutGroup::row(vec![TextLabel::new("General").widget_instance()]), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.artboard_name) @@ -2234,10 +2229,8 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new("Artboard Name".to_string()).for_checkbox(checkbox_id).widget_instance(), ] - }, - }, - LayoutGroup::Row { - widgets: { + }), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.transform_measurement) @@ -2252,13 +2245,9 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new("G/R/S Measurement".to_string()).for_checkbox(checkbox_id).widget_instance(), ] - }, - }, - LayoutGroup::Row { - widgets: vec![TextLabel::new("Select Tool").widget_instance()], - }, - LayoutGroup::Row { - widgets: { + }), + LayoutGroup::row(vec![TextLabel::new("Select Tool").widget_instance()]), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.quick_measurement) @@ -2273,10 +2262,8 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new("Quick Measurement".to_string()).for_checkbox(checkbox_id).widget_instance(), ] - }, - }, - LayoutGroup::Row { - widgets: { + }), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.transform_cage) @@ -2291,10 +2278,8 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new("Transform Cage".to_string()).for_checkbox(checkbox_id).widget_instance(), ] - }, - }, - LayoutGroup::Row { - widgets: { + }), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.compass_rose) @@ -2309,10 +2294,8 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new("Transform Dial".to_string()).for_checkbox(checkbox_id).widget_instance(), ] - }, - }, - LayoutGroup::Row { - widgets: { + }), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.pivot) @@ -2327,10 +2310,8 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new("Transform Pivot".to_string()).for_checkbox(checkbox_id).widget_instance(), ] - }, - }, - LayoutGroup::Row { - widgets: { + }), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.pivot) @@ -2345,10 +2326,8 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new("Transform Origin".to_string()).for_checkbox(checkbox_id).widget_instance(), ] - }, - }, - LayoutGroup::Row { - widgets: { + }), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.hover_outline) @@ -2363,10 +2342,8 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new("Hover Outline".to_string()).for_checkbox(checkbox_id).widget_instance(), ] - }, - }, - LayoutGroup::Row { - widgets: { + }), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.selection_outline) @@ -2381,10 +2358,8 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new("Selection Outline".to_string()).for_checkbox(checkbox_id).widget_instance(), ] - }, - }, - LayoutGroup::Row { - widgets: { + }), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.layer_origin_cross) @@ -2399,13 +2374,9 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new("Layer Origin".to_string()).for_checkbox(checkbox_id).widget_instance(), ] - }, - }, - LayoutGroup::Row { - widgets: vec![TextLabel::new("Pen & Path Tools").widget_instance()], - }, - LayoutGroup::Row { - widgets: { + }), + LayoutGroup::row(vec![TextLabel::new("Pen & Path Tools").widget_instance()]), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.path) @@ -2420,10 +2391,8 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new("Path".to_string()).for_checkbox(checkbox_id).widget_instance(), ] - }, - }, - LayoutGroup::Row { - widgets: { + }), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.anchors) @@ -2438,10 +2407,8 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new("Anchors".to_string()).for_checkbox(checkbox_id).widget_instance(), ] - }, - }, - LayoutGroup::Row { - widgets: { + }), + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(self.overlays_visibility_settings.handles) @@ -2460,8 +2427,7 @@ impl DocumentMessageHandler { .for_checkbox(checkbox_id) .widget_instance(), ] - }, - }, + }), ])) .widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), @@ -2480,16 +2446,11 @@ impl DocumentMessageHandler { PopoverButton::new() .popover_layout(Layout( [ - LayoutGroup::Row { - widgets: vec![TextLabel::new("Snapping").bold(true).widget_instance()], - }, - LayoutGroup::Row { - widgets: vec![TextLabel::new(SnappingOptions::BoundingBoxes.to_string()).widget_instance()], - }, + LayoutGroup::row(vec![TextLabel::new("Snapping").bold(true).widget_instance()]), + LayoutGroup::row(vec![TextLabel::new(SnappingOptions::BoundingBoxes.to_string()).widget_instance()]), ] .into_iter() - .chain(SNAP_FUNCTIONS_FOR_BOUNDING_BOXES.into_iter().map(|(name, closure, description)| LayoutGroup::Row { - widgets: { + .chain(SNAP_FUNCTIONS_FOR_BOUNDING_BOXES.into_iter().map(|(name, closure, description)| LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(*closure(&mut snapping_state)) @@ -2506,13 +2467,9 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new(name).tooltip_label(name).tooltip_description(description).for_checkbox(checkbox_id).widget_instance(), ] - }, - })) - .chain([LayoutGroup::Row { - widgets: vec![TextLabel::new(SnappingOptions::Paths.to_string()).widget_instance()], - }]) - .chain(SNAP_FUNCTIONS_FOR_PATHS.into_iter().map(|(name, closure, description)| LayoutGroup::Row { - widgets: { + }))) + .chain([LayoutGroup::row(vec![TextLabel::new(SnappingOptions::Paths.to_string()).widget_instance()])]) + .chain(SNAP_FUNCTIONS_FOR_PATHS.into_iter().map(|(name, closure, description)| LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(*closure(&mut snapping_state2)) @@ -2529,8 +2486,7 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new(name).tooltip_label(name).tooltip_description(description).for_checkbox(checkbox_id).widget_instance(), ] - }, - })) + }))) .collect(), )) .widget_instance(), @@ -2635,7 +2591,7 @@ impl DocumentMessageHandler { ]); responses.add(LayoutMessage::SendLayout { - layout: Layout(vec![LayoutGroup::Row { widgets }]), + layout: Layout(vec![LayoutGroup::row(widgets)]), layout_target: LayoutTarget::DocumentBar, }); responses.add(NodeGraphMessage::RunDocumentGraph); @@ -2773,7 +2729,7 @@ impl DocumentMessageHandler { .tooltip_label("Fill") .widget_instance(), ]; - let layers_panel_control_bar_left = Layout(vec![LayoutGroup::Row { widgets }]); + let layers_panel_control_bar_left = Layout(vec![LayoutGroup::row(widgets)]); let widgets = vec![ IconButton::new(if selection_all_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24) @@ -2791,7 +2747,7 @@ impl DocumentMessageHandler { .disabled(!has_selection) .widget_instance(), ]; - let layers_panel_control_bar_right = Layout(vec![LayoutGroup::Row { widgets }]); + let layers_panel_control_bar_right = Layout(vec![LayoutGroup::row(widgets)]); responses.add(LayoutMessage::SendLayout { layout: layers_panel_control_bar_left, @@ -2845,7 +2801,7 @@ impl DocumentMessageHandler { } }) .widget_instance(); - Layout(vec![LayoutGroup::Row { widgets: vec![node_chooser] }]) + Layout(vec![LayoutGroup::row(vec![node_chooser])]) }) .widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), @@ -2871,7 +2827,7 @@ impl DocumentMessageHandler { .widget_instance(), ]; responses.add(LayoutMessage::SendLayout { - layout: Layout(vec![LayoutGroup::Row { widgets }]), + layout: Layout(vec![LayoutGroup::row(widgets)]), layout_target: LayoutTarget::LayersPanelBottomBar, }); } diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 84a462e537..7889bfa2d3 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -2192,9 +2192,7 @@ fn static_input_properties() -> InputProperties { true }); - Ok(vec![LayoutGroup::Row { - widgets: node_properties::number_widget(ParameterWidgetsInfo::new(node_id, index, blank_assist, context), number_input), - }]) + Ok(vec![LayoutGroup::row(node_properties::number_widget(ParameterWidgetsInfo::new(node_id, index, blank_assist, context), number_input))]) }), ); map.insert( @@ -2228,10 +2226,8 @@ fn static_input_properties() -> InputProperties { number_input = number_input.step(number_step); } }; - Ok(vec![LayoutGroup::Row { - // NOTE: The bool input MUST be at the input index directly before the f64 input! - widgets: node_properties::optional_f64_widget(ParameterWidgetsInfo::new(node_id, index, false, context), index - 1, number_input), - }]) + // NOTE: The bool input MUST be at the input index directly before the f64 input! + Ok(vec![LayoutGroup::row(node_properties::optional_f64_widget(ParameterWidgetsInfo::new(node_id, index, false, context), index - 1, number_input))]) }), ); map.insert( @@ -2299,7 +2295,7 @@ fn static_input_properties() -> InputProperties { "noise_properties_noise_type".to_string(), Box::new(|node_id, index, context| { let noise_type_row = enum_choice::().for_socket(ParameterWidgetsInfo::new(node_id, index, true, context)).property_row(); - Ok(vec![noise_type_row, LayoutGroup::Row { widgets: Vec::new() }]) + Ok(vec![noise_type_row, LayoutGroup::row(Vec::new())]) }), ); map.insert( @@ -2321,7 +2317,7 @@ fn static_input_properties() -> InputProperties { ParameterWidgetsInfo::new(node_id, index, true, context), NumberInput::default().min(0.).disabled(!coherent_noise_active || !domain_warp_active), ); - Ok(vec![domain_warp_amplitude.into(), LayoutGroup::Row { widgets: Vec::new() }]) + Ok(vec![domain_warp_amplitude.into(), LayoutGroup::row(Vec::new())]) }), ); map.insert( @@ -2409,7 +2405,7 @@ fn static_input_properties() -> InputProperties { .range_max(Some(10.)) .disabled(!ping_pong_active || !coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active), ); - Ok(vec![fractal_ping_pong_strength.into(), LayoutGroup::Row { widgets: Vec::new() }]) + Ok(vec![fractal_ping_pong_strength.into(), LayoutGroup::row(Vec::new())]) }), ); map.insert( @@ -2505,7 +2501,7 @@ fn static_input_properties() -> InputProperties { ]); } - Ok(vec![LayoutGroup::Row { widgets }]) + Ok(vec![LayoutGroup::row(widgets)]) }), ); // Skew has a custom override that maps to degrees @@ -2549,24 +2545,22 @@ fn static_input_properties() -> InputProperties { ]); } - Ok(vec![LayoutGroup::Row { widgets }]) + Ok(vec![LayoutGroup::row(widgets)]) }), ); map.insert( "text_area".to_string(), Box::new(|node_id, index, context| { - Ok(vec![LayoutGroup::Row { - widgets: node_properties::text_area_widget(ParameterWidgetsInfo::new(node_id, index, true, context)), - }]) + Ok(vec![LayoutGroup::row(node_properties::text_area_widget(ParameterWidgetsInfo::new(node_id, index, true, context)))]) }), ); map.insert( "text_font".to_string(), Box::new(|node_id, index, context| { let (font, style) = node_properties::font_inputs(ParameterWidgetsInfo::new(node_id, index, true, context)); - let mut result = vec![LayoutGroup::Row { widgets: font }]; + let mut result = vec![LayoutGroup::row(font)]; if let Some(style) = style { - result.push(LayoutGroup::Row { widgets: style }); + result.push(LayoutGroup::row(style)); } Ok(result) }), diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index aae3db3e9e..c4c67c9865 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -2222,7 +2222,7 @@ impl NodeGraphMessageHandler { } }) .widget_instance(); - Layout(vec![LayoutGroup::Row { widgets: vec![node_chooser] }]) + Layout(vec![LayoutGroup::row(vec![node_chooser])]) }) .widget_instance(), // @@ -2323,7 +2323,7 @@ impl NodeGraphMessageHandler { ]); } - self.widgets[0] = LayoutGroup::Row { widgets }; + self.widgets[0] = LayoutGroup::row(widgets); } fn update_graph_bar_right( @@ -2365,7 +2365,7 @@ impl NodeGraphMessageHandler { .widget_instance(), ]); - self.widgets[1] = LayoutGroup::Row { widgets }; + self.widgets[1] = LayoutGroup::row(widgets); } /// Collate the properties panel sections for a node graph @@ -2407,8 +2407,7 @@ impl NodeGraphMessageHandler { let mut properties = Vec::new(); if let [node_id] = *nodes.as_slice() { - properties.push(LayoutGroup::Row { - widgets: vec![ + properties.push(LayoutGroup::row(vec![ Separator::new(SeparatorStyle::Related).widget_instance(), IconLabel::new("Node").tooltip_description("Name of the selected node.").widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), @@ -2424,8 +2423,7 @@ impl NodeGraphMessageHandler { }) .widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), - ], - }); + ])); } properties.extend(selected_nodes); @@ -2435,8 +2433,7 @@ impl NodeGraphMessageHandler { // TODO: Display properties for encapsulating node when no nodes are selected in a nested network // This may require store a separate path for the properties panel - let mut properties = vec![LayoutGroup::Row { - widgets: vec![ + let mut properties = vec![LayoutGroup::row(vec![ Separator::new(SeparatorStyle::Related).widget_instance(), IconLabel::new("File").tooltip_description("Name of the current document.").widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), @@ -2445,8 +2442,7 @@ impl NodeGraphMessageHandler { .on_update(|text_input| DocumentMessage::RenameDocument { new_name: text_input.value.clone() }.into()) .widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), - ], - }]; + ])]; let Some(network) = context.network_interface.nested_network(context.selection_network_path) else { warn!("No network in collate_properties"); @@ -2482,8 +2478,7 @@ impl NodeGraphMessageHandler { return Vec::new(); } - let mut layer_properties = vec![LayoutGroup::Row { - widgets: vec![ + let mut layer_properties = vec![LayoutGroup::row(vec![ Separator::new(SeparatorStyle::Related).widget_instance(), IconLabel::new("Layer").tooltip_description("Name of the selected layer.").widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), @@ -2520,12 +2515,11 @@ impl NodeGraphMessageHandler { .into() }) .widget_instance(); - Layout(vec![LayoutGroup::Row { widgets: vec![node_chooser] }]) + Layout(vec![LayoutGroup::row(vec![node_chooser])]) }) .widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), - ], - }]; + ])]; // Iterate through all the upstream nodes, but stop when we reach another layer (since that's a point where we switch from horizontal to vertical flow) let node_properties = context @@ -2842,7 +2836,7 @@ impl Default for NodeGraphMessageHandler { Self { network: Vec::new(), has_selection: false, - widgets: [LayoutGroup::Row { widgets: Vec::new() }, LayoutGroup::Row { widgets: Vec::new() }], + widgets: [LayoutGroup::row(Vec::new()), LayoutGroup::row(Vec::new())], drag_start: None, begin_dragging: false, node_has_moved_in_drag: false, diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 09677f3364..efa508b3ff 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -31,7 +31,7 @@ use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, Gra pub(crate) fn string_properties(text: &str) -> Vec { let widget = TextLabel::new(text).widget_instance(); - vec![LayoutGroup::Row { widgets: vec![widget] }] + vec![LayoutGroup::row(vec![widget])] } fn optionally_update_value(value: impl Fn(&T) -> Option + 'static + Send + Sync, node_id: NodeId, input_index: usize) -> impl Fn(&T) -> Message + 'static + Send + Sync { @@ -538,11 +538,7 @@ pub fn footprint_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg ); } - let widgets = [ - LayoutGroup::Row { widgets: location_widgets }, - LayoutGroup::Row { widgets: scale_widgets }, - LayoutGroup::Row { widgets: resolution_widgets }, - ]; + let widgets = [LayoutGroup::row(location_widgets), LayoutGroup::row(scale_widgets), LayoutGroup::row(resolution_widgets)]; let (last, rest) = widgets.split_last().expect("Footprint widget should return multiple rows"); *extra_widgets = rest.to_vec(); last.clone() @@ -651,13 +647,9 @@ pub fn transform_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg .widget_instance(), ]); - vec![ - LayoutGroup::Row { widgets: location_widgets }, - LayoutGroup::Row { widgets: rotation_widgets }, - LayoutGroup::Row { widgets: scale_widgets }, - ] + vec![LayoutGroup::row(location_widgets), LayoutGroup::row(rotation_widgets), LayoutGroup::row(scale_widgets)] } else { - vec![LayoutGroup::Row { widgets: location_widgets }] + vec![LayoutGroup::row(location_widgets)] }; if let Some((last, rest)) = widgets.split_last() { @@ -676,7 +668,7 @@ pub fn vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &st let Some(document_node) = document_node else { return LayoutGroup::default() }; let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; + return LayoutGroup::row(vec![]); }; match input.as_non_exposed_value() { Some(&TaggedValue::DVec2(dvec2)) => { @@ -730,7 +722,7 @@ pub fn vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &st _ => {} } - LayoutGroup::Row { widgets } + LayoutGroup::row(widgets) } pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text_input: TextInput) -> Vec { @@ -1101,7 +1093,7 @@ pub fn blend_mode_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Layout let Some(document_node) = document_node else { return LayoutGroup::default() }; let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; + return LayoutGroup::row(vec![]); }; if let Some(&TaggedValue::BlendMode(blend_mode)) = input.as_non_exposed_value() { let entries = BlendMode::list_svg_subset() @@ -1126,7 +1118,7 @@ pub fn blend_mode_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Layout .widget_instance(), ]); } - LayoutGroup::Row { widgets }.with_tooltip_description("Formula used for blending.") + LayoutGroup::row(widgets).with_tooltip_description("Formula used for blending.") } pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button: ColorInput) -> LayoutGroup { @@ -1137,7 +1129,7 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button: let Some(document_node) = document_node else { return LayoutGroup::default() }; // Return early with just the label if the input is exposed to the graph, meaning we don't want to show the color picker widget in the Properties panel let NodeInput::Value { tagged_value, exposed: false } = &document_node.inputs[index] else { - return LayoutGroup::Row { widgets }; + return LayoutGroup::row(widgets); }; // Add a separator @@ -1176,7 +1168,7 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button: x => warn!("Color {x:?}"), } - LayoutGroup::Row { widgets } + LayoutGroup::row(widgets) } pub fn font_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup { @@ -1192,7 +1184,7 @@ pub fn curve_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup let Some(document_node) = document_node else { return LayoutGroup::default() }; let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; + return LayoutGroup::row(vec![]); }; if let Some(TaggedValue::Curve(curve)) = &input.as_non_exposed_value() { widgets.extend_from_slice(&[ @@ -1203,7 +1195,7 @@ pub fn curve_widget(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup .widget_instance(), ]) } - LayoutGroup::Row { widgets } + LayoutGroup::row(widgets) } pub fn get_document_node<'a>(node_id: NodeId, context: &'a NodePropertiesContext<'a>) -> Result<&'a DocumentNode, String> { @@ -1306,10 +1298,10 @@ pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut Node .range_max(Some(100.)), ); - let mut layout = vec![LayoutGroup::Row { widgets: brightness }, LayoutGroup::Row { widgets: contrast }]; + let mut layout = vec![LayoutGroup::row(brightness), LayoutGroup::row(contrast)]; if includes_use_classic { // TODO: When we no longer use this function in the temporary "Brightness/Contrast Classic" node, remove this conditional pushing and just always include this - layout.push(LayoutGroup::Row { widgets: use_classic }); + layout.push(LayoutGroup::row(use_classic)); } layout @@ -1358,18 +1350,13 @@ pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodeProper let constant = number_widget(ParameterWidgetsInfo::new(node_id, constant_output_index, true, context), number_input); // Monochrome - let mut layout = vec![LayoutGroup::Row { widgets: is_monochrome }]; + let mut layout = vec![LayoutGroup::row(is_monochrome)]; // Output channel choice if !is_monochrome_value { layout.push(output_channel); } // Channel values - layout.extend([ - LayoutGroup::Row { widgets: red }, - LayoutGroup::Row { widgets: green }, - LayoutGroup::Row { widgets: blue }, - LayoutGroup::Row { widgets: constant }, - ]); + layout.extend([LayoutGroup::row(red), LayoutGroup::row(green), LayoutGroup::row(blue), LayoutGroup::row(constant)]); layout } @@ -1422,10 +1409,10 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp // Colors choice colors, // CMYK - LayoutGroup::Row { widgets: cyan }, - LayoutGroup::Row { widgets: magenta }, - LayoutGroup::Row { widgets: yellow }, - LayoutGroup::Row { widgets: black }, + LayoutGroup::row(cyan), + LayoutGroup::row(magenta), + LayoutGroup::row(yellow), + LayoutGroup::row(black), // Mode mode, ] @@ -1458,12 +1445,10 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte widgets.push(spacing); } GridType::Isometric => { - let spacing = LayoutGroup::Row { - widgets: number_widget( - ParameterWidgetsInfo::new(node_id, SpacingInput::::INDEX, true, context), - NumberInput::default().label("H").min(0.).unit(" px"), - ), - }; + let spacing = LayoutGroup::row(number_widget( + ParameterWidgetsInfo::new(node_id, SpacingInput::::INDEX, true, context), + NumberInput::default().label("H").min(0.).unit(" px"), + )); let angles = vec2_widget(ParameterWidgetsInfo::new(node_id, AnglesInput::INDEX, true, context), "", "", "°", None, false); widgets.extend([spacing, angles]); } @@ -1473,7 +1458,7 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte let columns = number_widget(ParameterWidgetsInfo::new(node_id, ColumnsInput::INDEX, true, context), NumberInput::default().min(1.)); let rows = number_widget(ParameterWidgetsInfo::new(node_id, RowsInput::INDEX, true, context), NumberInput::default().min(1.)); - widgets.extend([LayoutGroup::Row { widgets: columns }, LayoutGroup::Row { widgets: rows }]); + widgets.extend([LayoutGroup::row(columns), LayoutGroup::row(rows)]); widgets } @@ -1487,7 +1472,7 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon let turns = number_widget(ParameterWidgetsInfo::new(node_id, TurnsInput::INDEX, true, context), NumberInput::default().min(0.1)); let start_angle = number_widget(ParameterWidgetsInfo::new(node_id, StartAngleInput::INDEX, true, context), NumberInput::default().unit("°")); - let mut widgets = vec![spiral_type, LayoutGroup::Row { widgets: turns }, LayoutGroup::Row { widgets: start_angle }]; + let mut widgets = vec![spiral_type, LayoutGroup::row(turns), LayoutGroup::row(start_angle)]; let document_node = match get_document_node(node_id, context) { Ok(document_node) => document_node, @@ -1504,24 +1489,28 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon if let Some(&TaggedValue::SpiralType(spiral_type)) = spiral_type_input.as_non_exposed_value() { match spiral_type { SpiralType::Archimedean => { - let inner_radius = LayoutGroup::Row { - widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.).unit(" px")), - }; + let inner_radius = LayoutGroup::row(number_widget( + ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), + NumberInput::default().min(0.).unit(" px"), + )); - let outer_radius = LayoutGroup::Row { - widgets: number_widget(ParameterWidgetsInfo::new(node_id, OuterRadiusInput::INDEX, true, context), NumberInput::default().unit(" px")), - }; + let outer_radius = LayoutGroup::row(number_widget( + ParameterWidgetsInfo::new(node_id, OuterRadiusInput::INDEX, true, context), + NumberInput::default().unit(" px"), + )); widgets.extend([inner_radius, outer_radius]); } SpiralType::Logarithmic => { - let inner_radius = LayoutGroup::Row { - widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.).unit(" px")), - }; + let inner_radius = LayoutGroup::row(number_widget( + ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), + NumberInput::default().min(0.).unit(" px"), + )); - let outer_radius = LayoutGroup::Row { - widgets: number_widget(ParameterWidgetsInfo::new(node_id, OuterRadiusInput::INDEX, true, context), NumberInput::default().min(0.1).unit(" px")), - }; + let outer_radius = LayoutGroup::row(number_widget( + ParameterWidgetsInfo::new(node_id, OuterRadiusInput::INDEX, true, context), + NumberInput::default().min(0.1).unit(" px"), + )); widgets.extend([inner_radius, outer_radius]); } @@ -1533,7 +1522,7 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon NumberInput::default().min(1.).max(180.).unit("°"), ); - widgets.push(LayoutGroup::Row { widgets: angular_resolution }); + widgets.push(LayoutGroup::row(angular_resolution)); widgets } @@ -1574,13 +1563,13 @@ pub(crate) fn sample_polyline_properties(node_id: NodeId, context: &mut NodeProp vec![ spacing.with_tooltip_description(SAMPLE_POLYLINE_DESCRIPTION_SPACING), match current_spacing { - Some(TaggedValue::PointSpacingType(PointSpacingType::Separation)) => LayoutGroup::Row { widgets: separation }.with_tooltip_description(SAMPLE_POLYLINE_DESCRIPTION_SEPARATION), - Some(TaggedValue::PointSpacingType(PointSpacingType::Quantity)) => LayoutGroup::Row { widgets: quantity }.with_tooltip_description(SAMPLE_POLYLINE_DESCRIPTION_QUANTITY), - _ => LayoutGroup::Row { widgets: vec![] }, + Some(TaggedValue::PointSpacingType(PointSpacingType::Separation)) => LayoutGroup::row(separation).with_tooltip_description(SAMPLE_POLYLINE_DESCRIPTION_SEPARATION), + Some(TaggedValue::PointSpacingType(PointSpacingType::Quantity)) => LayoutGroup::row(quantity).with_tooltip_description(SAMPLE_POLYLINE_DESCRIPTION_QUANTITY), + _ => LayoutGroup::row(vec![]), }, - LayoutGroup::Row { widgets: start_offset }.with_tooltip_description(SAMPLE_POLYLINE_DESCRIPTION_START_OFFSET), - LayoutGroup::Row { widgets: stop_offset }.with_tooltip_description(SAMPLE_POLYLINE_DESCRIPTION_STOP_OFFSET), - LayoutGroup::Row { widgets: adaptive_spacing }.with_tooltip_description(SAMPLE_POLYLINE_DESCRIPTION_ADAPTIVE_SPACING), + LayoutGroup::row(start_offset).with_tooltip_description(SAMPLE_POLYLINE_DESCRIPTION_START_OFFSET), + LayoutGroup::row(stop_offset).with_tooltip_description(SAMPLE_POLYLINE_DESCRIPTION_STOP_OFFSET), + LayoutGroup::row(adaptive_spacing).with_tooltip_description(SAMPLE_POLYLINE_DESCRIPTION_ADAPTIVE_SPACING), ] } @@ -1594,11 +1583,7 @@ pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesC NumberInput::default().min(0.01).max(9.99).increment_step(0.1), ); - vec![ - LayoutGroup::Row { widgets: exposure }, - LayoutGroup::Row { widgets: offset }, - LayoutGroup::Row { widgets: gamma_correction }, - ] + vec![LayoutGroup::row(exposure), LayoutGroup::row(offset), LayoutGroup::row(gamma_correction)] } pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { @@ -1722,11 +1707,11 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties let clamped = bool_widget(ParameterWidgetsInfo::new(node_id, ClampedInput::INDEX, true, context), CheckboxInput::default()); vec![ - LayoutGroup::Row { widgets: size_x }, - LayoutGroup::Row { widgets: size_y }, - LayoutGroup::Row { widgets: corner_radius_row_1 }, - LayoutGroup::Row { widgets: corner_radius_row_2 }, - LayoutGroup::Row { widgets: clamped }, + LayoutGroup::row(size_x), + LayoutGroup::row(size_y), + LayoutGroup::row(corner_radius_row_1), + LayoutGroup::row(corner_radius_row_2), + LayoutGroup::row(clamped), ] } @@ -1825,14 +1810,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper let visible = context.network_interface.is_visible(&node_id, context.selection_network_path); let pinned = context.network_interface.is_pinned(&node_id, context.selection_network_path); - LayoutGroup::Section { - name, - description, - visible, - pinned, - id: node_id.0, - layout: Layout(layout), - } + LayoutGroup::section(name, description, visible, pinned, node_id.0, Layout(layout)) } /// Fill Node Widgets LayoutGroup @@ -1856,7 +1834,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte ) { (fill, backup_color, backup_gradient) } else { - return vec![LayoutGroup::Row { widgets: widgets_first_row }]; + return vec![LayoutGroup::row(widgets_first_row)]; }; let fill2 = fill.clone(); let backup_color_fill: Fill = backup_color.clone().into(); @@ -1899,7 +1877,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte .on_commit(commit_value) .widget_instance(), ); - let mut widgets = vec![LayoutGroup::Row { widgets: widgets_first_row }]; + let mut widgets = vec![LayoutGroup::row(widgets_first_row)]; let fill_type_switch = { let mut row = vec![TextLabel::new("").widget_instance()]; @@ -1942,7 +1920,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte RadioInput::new(entries).selected_index(Some(if fill.as_gradient().is_some() { 1 } else { 0 })).widget_instance(), ]); - LayoutGroup::Row { widgets: row } + LayoutGroup::row(row) }; widgets.push(fill_type_switch); @@ -2011,7 +1989,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte RadioInput::new(entries).selected_index(Some(gradient.gradient_type as u32)).widget_instance(), ]); - widgets.push(LayoutGroup::Row { widgets: row }); + widgets.push(LayoutGroup::row(row)); } widgets @@ -2069,14 +2047,14 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) - vec![ color, - LayoutGroup::Row { widgets: weight }, + LayoutGroup::row(weight), align, cap, join, - LayoutGroup::Row { widgets: miter_limit }, + LayoutGroup::row(miter_limit), paint_order, - LayoutGroup::Row { widgets: dash_lengths }, - LayoutGroup::Row { widgets: dash_offset }, + LayoutGroup::row(dash_lengths), + LayoutGroup::row(dash_offset), ] } @@ -2106,7 +2084,7 @@ pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesConte }); let miter_limit = number_widget(ParameterWidgetsInfo::new(node_id, MiterLimitInput::INDEX, true, context), number_input); - vec![LayoutGroup::Row { widgets: distance }, join, LayoutGroup::Row { widgets: miter_limit }] + vec![LayoutGroup::row(distance), join, LayoutGroup::row(miter_limit)] } pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { @@ -2158,9 +2136,9 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> let operand_a_hint = vec![TextLabel::new("(Operand A is the primary input)").widget_instance()]; vec![ - LayoutGroup::Row { widgets: expression }.with_tooltip_description(r#"A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2"."#), - LayoutGroup::Row { widgets: operand_b }.with_tooltip_description(r#"The value of "B" when calculating the expression."#), - LayoutGroup::Row { widgets: operand_a_hint }.with_tooltip_description(r#""A" is fed by the value from the previous node in the primary data flow, or it is 0 if disconnected."#), + LayoutGroup::row(expression).with_tooltip_description(r#"A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2"."#), + LayoutGroup::row(operand_b).with_tooltip_description(r#"The value of "B" when calculating the expression."#), + LayoutGroup::row(operand_a_hint).with_tooltip_description(r#""A" is fed by the value from the previous node in the primary data flow, or it is 0 if disconnected."#), ] } @@ -2348,14 +2326,14 @@ pub mod choice { let ParameterWidgetsInfo { document_node, node_id, index, .. } = self.parameter_info; let Some(document_node) = document_node else { log::error!("Could not get document node when building property row for node {node_id:?}"); - return LayoutGroup::Row { widgets: Vec::new() }; + return LayoutGroup::row(Vec::new()); }; let mut widgets = super::start_widgets(self.parameter_info); let Some(input) = document_node.inputs.get(index) else { log::warn!("A widget failed to be built because its node's input index is invalid."); - return LayoutGroup::Row { widgets: vec![] }; + return LayoutGroup::row(vec![]); }; let input: Option = input.as_non_exposed_value().and_then(|v| <&W::Value as TryFrom<&TaggedValue>>::try_from(v).ok()).cloned(); @@ -2367,7 +2345,7 @@ pub mod choice { widgets.extend_from_slice(&[Separator::new(SeparatorStyle::Unrelated).widget_instance(), widget]); } - let mut row = LayoutGroup::Row { widgets }; + let mut row = LayoutGroup::row(widgets); if let Some(desc) = self.widget_factory.description() { row = row.with_tooltip_description(desc); } diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 899fd767c9..0a9270e066 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -228,12 +228,9 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { }) }; - widgets.push(LayoutGroup::Row { - widgets: vec![TextLabel::new("Grid").bold(true).widget_instance()], - }); + widgets.push(LayoutGroup::row(vec![TextLabel::new("Grid").bold(true).widget_instance()])); - widgets.push(LayoutGroup::Row { - widgets: vec![ + widgets.push(LayoutGroup::row(vec![ TextLabel::new("Type").table_align(true).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), RadioInput::new(vec![ @@ -262,8 +259,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { GridType::Isometric { .. } => 1, })) .widget_instance(), - ], - }); + ])); let mut color_widgets = vec![ TextLabel::new("Display").table_align(true).widget_instance(), @@ -288,10 +284,9 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { })) .widget_instance(), ); - widgets.push(LayoutGroup::Row { widgets: color_widgets }); + widgets.push(LayoutGroup::row(color_widgets)); - widgets.push(LayoutGroup::Row { - widgets: vec![ + widgets.push(LayoutGroup::row(vec![ TextLabel::new("Origin").table_align(true).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), NumberInput::new(Some(grid.origin.x)) @@ -307,12 +302,10 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { .min_width(98) .on_update(update_origin(grid, |grid| Some(&mut grid.origin.y))) .widget_instance(), - ], - }); + ])); match grid.grid_type { - GridType::Rectangular { spacing } => widgets.push(LayoutGroup::Row { - widgets: vec![ + GridType::Rectangular { spacing } => widgets.push(LayoutGroup::row(vec![ TextLabel::new("Spacing").table_align(true).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), NumberInput::new(Some(spacing.x)) @@ -330,11 +323,9 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { .min_width(98) .on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.y))) .widget_instance(), - ], - }), + ])), GridType::Isometric { y_axis_spacing, angle_a, angle_b } => { - widgets.push(LayoutGroup::Row { - widgets: vec![ + widgets.push(LayoutGroup::row(vec![ TextLabel::new("Y Spacing").table_align(true).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), NumberInput::new(Some(y_axis_spacing)) @@ -343,10 +334,8 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { .min_width(200) .on_update(update_origin(grid, |grid| grid.grid_type.isometric_y_spacing())) .widget_instance(), - ], - }); - widgets.push(LayoutGroup::Row { - widgets: vec![ + ])); + widgets.push(LayoutGroup::row(vec![ TextLabel::new("Angles").table_align(true).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), NumberInput::new(Some(angle_a)) @@ -360,8 +349,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { .min_width(98) .on_update(update_origin(grid, |grid| grid.grid_type.angle_b())) .widget_instance(), - ], - }); + ])); } } diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index e73eb4ef98..cfd3b6c8b0 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1011,9 +1011,8 @@ impl MessageHandler> for Portfolio PortfolioMessage::RequestWelcomeScreenButtonsLayout => { let donate = "https://graphite.art/donate/"; - let table = LayoutGroup::Table { - unstyled: true, - rows: vec![ + let table = LayoutGroup::table( + vec![ vec![ TextButton::new("New Document") .icon(Some("File".into())) @@ -1045,7 +1044,8 @@ impl MessageHandler> for Portfolio .widget_instance(), ], ], - }; + true, + ); responses.add(LayoutMessage::DestroyLayout { layout_target: LayoutTarget::WelcomeScreenButtons, @@ -1061,7 +1061,7 @@ impl MessageHandler> for Portfolio #[cfg(target_family = "wasm")] let widgets = vec![]; - let row = LayoutGroup::Row { widgets }; + let row = LayoutGroup::row(widgets); responses.add(LayoutMessage::SendLayout { layout: Layout(vec![row]), diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index e8f8159e0c..5319e21aa4 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -223,7 +223,7 @@ impl LayoutHolder for BrushTool { .widget_instance(), ); - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs index c6d152417f..f6fc27ed09 100644 --- a/editor/src/messages/tool/tool_messages/freehand_tool.rs +++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs @@ -153,7 +153,7 @@ impl LayoutHolder for FreehandTool { widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); widgets.push(create_weight_widget(self.options.line_weight)); - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index c3c8344bf2..8f16b2a226 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -201,7 +201,7 @@ impl LayoutHolder for GradientTool { widgets.push(reverse_direction); } - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 145d8975f2..3ad8a2d461 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -352,32 +352,30 @@ impl LayoutHolder for PathTool { let _pin_pivot = pin_pivot_widget(self.tool_data.pivot_gizmo.pin_active(), false, PivotToolSource::Path); - Layout(vec![LayoutGroup::Row { - widgets: vec![ - x_location, - related_seperator.clone(), - y_location, - unrelated_seperator.clone(), - colinear_handle_checkbox, - related_seperator.clone(), - colinear_handles_label, - unrelated_seperator.clone(), - point_editing_mode, - related_seperator.clone(), - segment_editing_mode, - unrelated_seperator.clone(), - path_overlay_mode_widget, - unrelated_seperator.clone(), - path_node_button, - // checkbox.clone(), - // related_seperator.clone(), - // dropdown.clone(), - // unrelated_seperator, - // pivot_reference, - // related_seperator.clone(), - // pin_pivot, - ], - }]) + Layout(vec![LayoutGroup::row(vec![ + x_location, + related_seperator.clone(), + y_location, + unrelated_seperator.clone(), + colinear_handle_checkbox, + related_seperator.clone(), + colinear_handles_label, + unrelated_seperator.clone(), + point_editing_mode, + related_seperator.clone(), + segment_editing_mode, + unrelated_seperator.clone(), + path_overlay_mode_widget, + unrelated_seperator.clone(), + path_node_button, + // checkbox.clone(), + // related_seperator.clone(), + // dropdown.clone(), + // unrelated_seperator, + // pivot_reference, + // related_seperator.clone(), + // pin_pivot, + ])]) } } diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index ba5c4dde27..b83bda1817 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -241,7 +241,7 @@ impl LayoutHolder for PenTool { .widget_instance(), ); - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 81042a586a..2ddff084e5 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -269,7 +269,7 @@ impl LayoutHolder for SelectTool { widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); widgets.extend(self.boolean_widgets(self.tool_data.selected_layers_count)); - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 61dd308295..8e0ac4bca9 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -425,7 +425,7 @@ impl LayoutHolder for ShapeTool { widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); widgets.push(create_weight_widget(self.options.line_weight)); - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index 7726b3413c..b5ef6c6b7a 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -160,7 +160,7 @@ impl LayoutHolder for SplineTool { widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); widgets.push(create_weight_widget(self.options.line_weight)); - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index 0ca804136f..c2a35bb6cf 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -273,7 +273,7 @@ impl TextTool { }, )); - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } } diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index 3b30777ca9..2b633a3c8e 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -128,11 +128,8 @@ pub struct DocumentToolData { impl DocumentToolData { pub fn update_working_colors(&self, responses: &mut VecDeque) { let layout = Layout(vec![ - LayoutGroup::Row { - widgets: vec![WorkingColorsInput::new(self.primary_color.to_gamma_srgb(), self.secondary_color.to_gamma_srgb()).widget_instance()], - }, - LayoutGroup::Row { - widgets: vec![ + LayoutGroup::row(vec![WorkingColorsInput::new(self.primary_color.to_gamma_srgb(), self.secondary_color.to_gamma_srgb()).widget_instance()]), + LayoutGroup::row(vec![ IconButton::new("SwapVertical", 16) .tooltip_label("Swap Working Colors") .tooltip_shortcut(action_shortcut!(ToolMessageDiscriminant::SwapColors)) @@ -143,8 +140,7 @@ impl DocumentToolData { .tooltip_shortcut(action_shortcut!(ToolMessageDiscriminant::ResetColors)) .on_update(|_| ToolMessage::ResetColors.into()) .widget_instance(), - ], - }, + ]), ]); responses.add(LayoutMessage::SendLayout { @@ -308,7 +304,7 @@ impl ToolData { .skip(1) .collect(); - Layout(vec![LayoutGroup::Row { widgets: tool_groups_layout }]) + Layout(vec![LayoutGroup::row(tool_groups_layout)]) } } @@ -560,7 +556,7 @@ impl HintData { } } - Layout(vec![LayoutGroup::Row { widgets }]) + Layout(vec![LayoutGroup::row(widgets)]) } pub fn send_layout(&self, responses: &mut VecDeque) { From dad51d2ec7ab4e0d1a997a5e107e84abd7ec945d Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 14:34:34 -0800 Subject: [PATCH 05/28] Small rename --- frontend/src/components/panels/Document.svelte | 4 ++-- .../src/components/widgets/WidgetLayout.svelte | 4 ++-- .../components/widgets/WidgetSection.svelte | 4 ++-- .../src/components/widgets/WidgetSpan.svelte | 18 +++++++++--------- frontend/src/messages_old.ts | 6 +++--- frontend/src/utility-functions/widgets.ts | 8 ++++---- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 6c359ca245..91f5914a30 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -10,7 +10,7 @@ import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry"; import { rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization"; import { setupViewportResizeObserver, cleanupViewportResizeObserver } from "@graphite/utility-functions/viewports"; - import { isWidgetSpanRow } from "@graphite/utility-functions/widgets"; + import { isWidgetRow } from "@graphite/utility-functions/widgets"; import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte"; import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte"; @@ -93,7 +93,7 @@ $: canvasHeightScaledRoundedToEven = canvasHeightScaled && (canvasHeightScaled % 2 === 1 ? canvasHeightScaled + 1 : canvasHeightScaled); $: toolShelfTotalToolsAndSeparators = ((layoutGroup) => { - if (!layoutGroup || !isWidgetSpanRow(layoutGroup)) return undefined; + if (!layoutGroup || !isWidgetRow(layoutGroup)) return undefined; let totalSeparators = 0; let totalToolRowsFor1Columns = 0; diff --git a/frontend/src/components/widgets/WidgetLayout.svelte b/frontend/src/components/widgets/WidgetLayout.svelte index f224f0d6ff..50ed8c60f3 100644 --- a/frontend/src/components/widgets/WidgetLayout.svelte +++ b/frontend/src/components/widgets/WidgetLayout.svelte @@ -1,6 +1,6 @@ {#each layout as layoutGroup} - {#if isWidgetSpanRow(layoutGroup) || isWidgetSpanColumn(layoutGroup)} + {#if isWidgetRow(layoutGroup) || isWidgetColumn(layoutGroup)} {:else if isWidgetSection(layoutGroup)} diff --git a/frontend/src/components/widgets/WidgetSection.svelte b/frontend/src/components/widgets/WidgetSection.svelte index b4cc75503a..5774497cd1 100644 --- a/frontend/src/components/widgets/WidgetSection.svelte +++ b/frontend/src/components/widgets/WidgetSection.svelte @@ -4,7 +4,7 @@ import type { Editor } from "@graphite/editor"; import type { LayoutTarget } from "@graphite/messages"; import type { WidgetSection as WidgetSectionData } from "@graphite/utility-functions/widgets"; - import { isWidgetSpanRow, isWidgetSection } from "@graphite/utility-functions/widgets"; + import { isWidgetRow, isWidgetSection } from "@graphite/utility-functions/widgets"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte"; @@ -63,7 +63,7 @@ {#if expanded} {#each widgetData.section.layout as layoutGroup} - {#if isWidgetSpanRow(layoutGroup)} + {#if isWidgetRow(layoutGroup)} {:else if isWidgetSection(layoutGroup)} diff --git a/frontend/src/components/widgets/WidgetSpan.svelte b/frontend/src/components/widgets/WidgetSpan.svelte index 6ae52e6601..5e48106768 100644 --- a/frontend/src/components/widgets/WidgetSpan.svelte +++ b/frontend/src/components/widgets/WidgetSpan.svelte @@ -5,8 +5,8 @@ import type { LayoutTarget, WidgetInstance } from "@graphite/messages"; import { parseFillChoice } from "@graphite/utility-functions/colors"; import { debouncer } from "@graphite/utility-functions/debounce"; - import type { WidgetSpanColumn, WidgetSpanRow, WidgetKind } from "@graphite/utility-functions/widgets"; - import { isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/utility-functions/widgets"; + import type { WidgetColumn, WidgetRow, WidgetKind } from "@graphite/utility-functions/widgets"; + import { isWidgetColumn, isWidgetRow } from "@graphite/utility-functions/widgets"; import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte"; import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte"; @@ -33,7 +33,7 @@ const editor = getContext("editor"); - export let widgetData: WidgetSpanRow | WidgetSpanColumn; + export let widgetData: WidgetRow | WidgetColumn; export let layoutTarget: LayoutTarget; let className = ""; @@ -49,15 +49,15 @@ $: direction = watchDirection(widgetData); $: widgets = watchWidgets(widgetData); - function watchDirection(widgetData: WidgetSpanRow | WidgetSpanColumn): "row" | "column" | undefined { - if (isWidgetSpanRow(widgetData)) return "row"; - if (isWidgetSpanColumn(widgetData)) return "column"; + function watchDirection(widgetData: WidgetRow | WidgetColumn): "row" | "column" | undefined { + if (isWidgetRow(widgetData)) return "row"; + if (isWidgetColumn(widgetData)) return "column"; } - function watchWidgets(widgetData: WidgetSpanRow | WidgetSpanColumn): WidgetInstance[] { + function watchWidgets(widgetData: WidgetRow | WidgetColumn): WidgetInstance[] { let widgets: WidgetInstance[] = []; - if (isWidgetSpanRow(widgetData)) widgets = widgetData.row.rowWidgets; - else if (isWidgetSpanColumn(widgetData)) widgets = widgetData.column.columnWidgets; + if (isWidgetRow(widgetData)) widgets = widgetData.row.rowWidgets; + else if (isWidgetColumn(widgetData)) widgets = widgetData.column.columnWidgets; return widgets; } diff --git a/frontend/src/messages_old.ts b/frontend/src/messages_old.ts index 4e99b679a4..24614a2986 100644 --- a/frontend/src/messages_old.ts +++ b/frontend/src/messages_old.ts @@ -669,11 +669,11 @@ export type WidgetDiff = { }; export type UIItem = Layout | LayoutGroup | WidgetInstance[] | WidgetInstance; -export type LayoutGroup = WidgetSpanRow | WidgetSpanColumn | WidgetTable | WidgetSection; +export type LayoutGroup = WidgetColumn | WidgetRow | WidgetTable | WidgetSection; export type Layout = LayoutGroup[]; -export type WidgetSpanColumn = { columnWidgets: WidgetInstance[] }; -export type WidgetSpanRow = { rowWidgets: WidgetInstance[] }; +export type WidgetColumn = { columnWidgets: WidgetInstance[] }; +export type WidgetRow = { rowWidgets: WidgetInstance[] }; export type WidgetTable = { tableWidgets: WidgetInstance[][]; unstyled: boolean }; export type WidgetSection = { name: string; description: string; visible: boolean; pinned: boolean; id: bigint; layout: Layout }; diff --git a/frontend/src/utility-functions/widgets.ts b/frontend/src/utility-functions/widgets.ts index dcb2309b5f..6230e6c277 100644 --- a/frontend/src/utility-functions/widgets.ts +++ b/frontend/src/utility-functions/widgets.ts @@ -1,18 +1,18 @@ import type { Layout, LayoutGroup, Widget, WidgetDiff, WidgetInstance } from "@graphite/messages"; type UIItem = Layout | LayoutGroup | WidgetInstance[] | WidgetInstance; -export type WidgetSpanColumn = Extract; -export type WidgetSpanRow = Extract; +export type WidgetColumn = Extract; +export type WidgetRow = Extract; export type WidgetTable = Extract; export type WidgetSection = Extract; type ExtractWidgetKind = T extends Record ? K & string : never; export type WidgetKind = ExtractWidgetKind; -export function isWidgetSpanColumn(layoutGroup: LayoutGroup): layoutGroup is WidgetSpanColumn { +export function isWidgetColumn(layoutGroup: LayoutGroup): layoutGroup is WidgetColumn { return "column" in layoutGroup; } -export function isWidgetSpanRow(layoutGroup: LayoutGroup): layoutGroup is WidgetSpanRow { +export function isWidgetRow(layoutGroup: LayoutGroup): layoutGroup is WidgetRow { return "row" in layoutGroup; } From 51bb7e5e206deba62ae2da62ddf18e1aeda5ec80 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 15:22:52 -0800 Subject: [PATCH 06/28] Simplify widgets further --- .../src/components/panels/Document.svelte | 3 +- .../components/widgets/WidgetLayout.svelte | 15 ++++---- .../components/widgets/WidgetSection.svelte | 34 +++++++++---------- .../src/components/widgets/WidgetSpan.svelte | 24 +++---------- .../src/components/widgets/WidgetTable.svelte | 14 ++++---- frontend/src/utility-functions/widgets.ts | 25 +------------- 6 files changed, 37 insertions(+), 78 deletions(-) diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 91f5914a30..a11d166a04 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -10,7 +10,6 @@ import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry"; import { rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization"; import { setupViewportResizeObserver, cleanupViewportResizeObserver } from "@graphite/utility-functions/viewports"; - import { isWidgetRow } from "@graphite/utility-functions/widgets"; import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte"; import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte"; @@ -93,7 +92,7 @@ $: canvasHeightScaledRoundedToEven = canvasHeightScaled && (canvasHeightScaled % 2 === 1 ? canvasHeightScaled + 1 : canvasHeightScaled); $: toolShelfTotalToolsAndSeparators = ((layoutGroup) => { - if (!layoutGroup || !isWidgetRow(layoutGroup)) return undefined; + if (!layoutGroup || !("row" in layoutGroup)) return undefined; let totalSeparators = 0; let totalToolRowsFor1Columns = 0; diff --git a/frontend/src/components/widgets/WidgetLayout.svelte b/frontend/src/components/widgets/WidgetLayout.svelte index 50ed8c60f3..25c28d1a21 100644 --- a/frontend/src/components/widgets/WidgetLayout.svelte +++ b/frontend/src/components/widgets/WidgetLayout.svelte @@ -1,6 +1,5 @@ {#each layout as layoutGroup} - {#if isWidgetRow(layoutGroup) || isWidgetColumn(layoutGroup)} - - {:else if isWidgetSection(layoutGroup)} - - {:else if isWidgetTable(layoutGroup)} - + {#if "row" in layoutGroup} + + {:else if "column" in layoutGroup} + + {:else if "section" in layoutGroup} + + {:else if "table" in layoutGroup} + {/if} {/each} diff --git a/frontend/src/components/widgets/WidgetSection.svelte b/frontend/src/components/widgets/WidgetSection.svelte index 5774497cd1..7129fd43e7 100644 --- a/frontend/src/components/widgets/WidgetSection.svelte +++ b/frontend/src/components/widgets/WidgetSection.svelte @@ -2,9 +2,7 @@ import { getContext } from "svelte"; import type { Editor } from "@graphite/editor"; - import type { LayoutTarget } from "@graphite/messages"; - import type { WidgetSection as WidgetSectionData } from "@graphite/utility-functions/widgets"; - import { isWidgetRow, isWidgetSection } from "@graphite/utility-functions/widgets"; + import type { LayoutTarget, WidgetSection as WidgetSectionData } from "@graphite/messages"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte"; @@ -27,13 +25,13 @@ {#if expanded} - {#each widgetData.section.layout as layoutGroup} - {#if isWidgetRow(layoutGroup)} - - {:else if isWidgetSection(layoutGroup)} - + {#each widgetData.layout as layoutGroup} + {#if "row" in layoutGroup} + + {:else if "section" in layoutGroup} + {/if} {/each} diff --git a/frontend/src/components/widgets/WidgetSpan.svelte b/frontend/src/components/widgets/WidgetSpan.svelte index 5e48106768..cd3dfd63df 100644 --- a/frontend/src/components/widgets/WidgetSpan.svelte +++ b/frontend/src/components/widgets/WidgetSpan.svelte @@ -2,11 +2,9 @@ import { getContext } from "svelte"; import type { Editor } from "@graphite/editor"; - import type { LayoutTarget, WidgetInstance } from "@graphite/messages"; + import type { LayoutTarget, Widget, WidgetInstance } from "@graphite/messages"; import { parseFillChoice } from "@graphite/utility-functions/colors"; import { debouncer } from "@graphite/utility-functions/debounce"; - import type { WidgetColumn, WidgetRow, WidgetKind } from "@graphite/utility-functions/widgets"; - import { isWidgetColumn, isWidgetRow } from "@graphite/utility-functions/widgets"; import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte"; import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte"; @@ -31,9 +29,12 @@ import ShortcutLabel from "@graphite/components/widgets/labels/ShortcutLabel.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; + type WidgetKind = Widget extends infer T ? (T extends Record ? K & string : never) : never; + const editor = getContext("editor"); - export let widgetData: WidgetRow | WidgetColumn; + export let widgets: WidgetInstance[]; + export let direction: "row" | "column"; export let layoutTarget: LayoutTarget; let className = ""; @@ -46,21 +47,6 @@ .flatMap(([className, stateName]) => (stateName ? [className] : [])) .join(" "); - $: direction = watchDirection(widgetData); - $: widgets = watchWidgets(widgetData); - - function watchDirection(widgetData: WidgetRow | WidgetColumn): "row" | "column" | undefined { - if (isWidgetRow(widgetData)) return "row"; - if (isWidgetColumn(widgetData)) return "column"; - } - - function watchWidgets(widgetData: WidgetRow | WidgetColumn): WidgetInstance[] { - let widgets: WidgetInstance[] = []; - if (isWidgetRow(widgetData)) widgets = widgetData.row.rowWidgets; - else if (isWidgetColumn(widgetData)) widgets = widgetData.column.columnWidgets; - return widgets; - } - function widgetValueCommit(widgetIndex: number, value: unknown) { editor.handle.widgetValueCommit(layoutTarget, widgets[widgetIndex].widgetId, value); } diff --git a/frontend/src/components/widgets/WidgetTable.svelte b/frontend/src/components/widgets/WidgetTable.svelte index 8187cba90e..6c2a83c956 100644 --- a/frontend/src/components/widgets/WidgetTable.svelte +++ b/frontend/src/components/widgets/WidgetTable.svelte @@ -1,23 +1,21 @@ -
- +
+
- {#each widgetData.table.tableWidgets as row} + {#each widgetData.tableWidgets as row} {#each row as cell} {/each} diff --git a/frontend/src/utility-functions/widgets.ts b/frontend/src/utility-functions/widgets.ts index 6230e6c277..b210fc6def 100644 --- a/frontend/src/utility-functions/widgets.ts +++ b/frontend/src/utility-functions/widgets.ts @@ -1,29 +1,6 @@ -import type { Layout, LayoutGroup, Widget, WidgetDiff, WidgetInstance } from "@graphite/messages"; +import type { Layout, LayoutGroup, WidgetDiff, WidgetInstance } from "@graphite/messages"; type UIItem = Layout | LayoutGroup | WidgetInstance[] | WidgetInstance; -export type WidgetColumn = Extract; -export type WidgetRow = Extract; -export type WidgetTable = Extract; -export type WidgetSection = Extract; -type ExtractWidgetKind = T extends Record ? K & string : never; -export type WidgetKind = ExtractWidgetKind; - -export function isWidgetColumn(layoutGroup: LayoutGroup): layoutGroup is WidgetColumn { - return "column" in layoutGroup; -} - -export function isWidgetRow(layoutGroup: LayoutGroup): layoutGroup is WidgetRow { - return "row" in layoutGroup; -} - -export function isWidgetTable(layoutGroup: LayoutGroup): layoutGroup is WidgetTable { - return "table" in layoutGroup; -} - -export function isWidgetSection(layoutGroup: LayoutGroup): layoutGroup is WidgetSection { - return "section" in layoutGroup; -} - // Updates a widget layout based on a list of updates, giving the new layout by mutating the `layout` argument export function patchLayout(layout: /* &mut */ Layout, diffs: WidgetDiff[]) { diffs.forEach((update) => { From e7cc94dee03ee0828b38a6fc975b36297276d3ac Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 16:02:48 -0800 Subject: [PATCH 07/28] Clean up message type references --- .../src/components/panels/Document.svelte | 7 +++--- frontend/src/editor.ts | 6 ++--- frontend/src/io-managers/persistence.ts | 18 +++++++-------- frontend/src/messages.ts | 8 ------- frontend/src/messages_old.ts | 2 +- frontend/src/state-providers/node-graph.ts | 7 +++--- frontend/src/subscription-router.ts | 23 +++++++++++++------ frontend/wasm/README.md | 2 +- frontend/wasm/src/editor_api.rs | 2 +- .../guide/codebase-overview/_index.md | 2 +- 10 files changed, 37 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index a11d166a04..fb2ea6927d 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -2,9 +2,10 @@ import { getContext, onMount, onDestroy, tick } from "svelte"; import type { Editor } from "@graphite/editor"; - import type { Color, FrontendMessages, MenuDirection, MouseCursorIcon } from "@graphite/messages"; + import type { Color, MenuDirection, MouseCursorIcon } from "@graphite/messages"; import type { AppWindowState } from "@graphite/state-providers/app-window"; import type { DocumentState } from "@graphite/state-providers/document"; + import type { MessageBody } from "@graphite/subscription-router"; import { fillChoiceColor, createColor } from "@graphite/utility-functions/colors"; import { pasteFile } from "@graphite/utility-functions/files"; import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry"; @@ -20,8 +21,6 @@ import ScrollbarInput from "@graphite/components/widgets/inputs/ScrollbarInput.svelte"; import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte"; - type DisplayEditableTextbox = FrontendMessages["DisplayEditableTextbox"]; - let rulerHorizontal: RulerInput | undefined; let rulerVertical: RulerInput | undefined; let viewport: HTMLDivElement | undefined; @@ -343,7 +342,7 @@ editor.handle.onChangeText(textCleaned, false); } - export async function displayEditableTextbox(data: DisplayEditableTextbox) { + export async function displayEditableTextbox(data: MessageBody<"DisplayEditableTextbox">) { showTextInput = true; await tick(); diff --git a/frontend/src/editor.ts b/frontend/src/editor.ts index 8a24429ed0..30f2b50cfb 100644 --- a/frontend/src/editor.ts +++ b/frontend/src/editor.ts @@ -2,9 +2,9 @@ import { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm"; import init, { wasmMemory, receiveNativeMessage } from "@graphite/../wasm/pkg/graphite_wasm"; -import type { FrontendMessage, FrontendMessages } from "@graphite/messages"; +import type { FrontendMessage } from "@graphite/messages"; import { createSubscriptionRouter } from "@graphite/subscription-router"; -import type { SubscriptionRouter } from "@graphite/subscription-router"; +import type { MessageName, SubscriptionRouter } from "@graphite/subscription-router"; import { operatingSystem } from "@graphite/utility-functions/platform"; // TODO: Remove `raw`, split out `subscriptions`, and unwrap the remaining `handle` so `EditorHandle` can replace `Editor` and then it can also be renamed to `Editor` to fully remove `EditorHandle`. @@ -46,7 +46,7 @@ export function createEditor(): Editor { const randomSeed = BigInt(randomSeedFloat); // Handle: object containing many functions from `editor_api.rs` that are part of the `EditorHandle` struct (generated by wasm-bindgen) - const handle = EditorHandle.create(operatingSystem(), randomSeed, (messageType: keyof FrontendMessages, messageData: FrontendMessage) => { + const handle = EditorHandle.create(operatingSystem(), randomSeed, (messageType: MessageName, messageData: FrontendMessage) => { // This callback is called by Wasm when a FrontendMessage is received from the Wasm wrapper `EditorHandle` subscriptions.handleFrontendMessage(messageType, messageData); }); diff --git a/frontend/src/io-managers/persistence.ts b/frontend/src/io-managers/persistence.ts index 7a5d8508b3..d17a42226a 100644 --- a/frontend/src/io-managers/persistence.ts +++ b/frontend/src/io-managers/persistence.ts @@ -2,10 +2,8 @@ import { createStore, del, get, set, update } from "idb-keyval"; import { get as getFromStore } from "svelte/store"; import type { Editor } from "@graphite/editor"; -import type { FrontendMessages } from "@graphite/messages"; import type { PortfolioState } from "@graphite/state-providers/portfolio"; - -type TriggerPersistenceWriteDocument = FrontendMessages["TriggerPersistenceWriteDocument"]; +import type { MessageBody } from "@graphite/subscription-router"; const graphiteStore = createStore("graphite", "store"); @@ -21,8 +19,8 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta await set("current_document_id", String(documentId), graphiteStore); } - async function storeDocument(autoSaveDocument: TriggerPersistenceWriteDocument) { - await update>( + async function storeDocument(autoSaveDocument: MessageBody<"TriggerPersistenceWriteDocument">) { + await update>>( "documents", (old) => { const documents = old || {}; @@ -37,7 +35,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta } async function removeDocument(id: string) { - await update>( + await update>>( "documents", (old) => { const documents = old || {}; @@ -71,10 +69,10 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta } async function loadFirstDocument() { - const previouslySavedDocuments = await get>("documents", graphiteStore); + const previouslySavedDocuments = await get>>("documents", graphiteStore); // TODO: Eventually remove this document upgrade code - // Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if needed + // Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if the browser is storing the old format as strings if (previouslySavedDocuments) { // eslint-disable-next-line @typescript-eslint/no-explicit-any Object.values(previouslySavedDocuments).forEach((doc: any) => { @@ -104,7 +102,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta } async function loadRestDocuments() { - const previouslySavedDocuments = await get>("documents", graphiteStore); + const previouslySavedDocuments = await get>>("documents", graphiteStore); // TODO: Eventually remove this document upgrade code // Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if needed @@ -188,7 +186,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta }); editor.subscriptions.subscribeFrontendMessage("TriggerSaveActiveDocument", async (data) => { const documentId = String(data.documentId); - const previouslySavedDocuments = await get>("documents", graphiteStore); + const previouslySavedDocuments = await get>>("documents", graphiteStore); // TODO: Eventually remove this document upgrade code // Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if needed diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index df3fdf949e..05d3083467 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -1,9 +1 @@ export * from "@graphite/../wasm/pkg/graphite_wasm"; - -// Type convert a union of messages into a map of messages -export type ToMessageMap = { - [K in T extends string ? T : T extends object ? keyof T : never]: K extends T ? Record : T extends Record ? Payload : never; -}; - -import type { FrontendMessage } from "@graphite/../wasm/pkg/graphite_wasm"; -export type FrontendMessages = ToMessageMap; diff --git a/frontend/src/messages_old.ts b/frontend/src/messages_old.ts index 24614a2986..fdd1f49c4d 100644 --- a/frontend/src/messages_old.ts +++ b/frontend/src/messages_old.ts @@ -677,7 +677,7 @@ export type WidgetRow = { rowWidgets: WidgetInstance[] }; export type WidgetTable = { tableWidgets: WidgetInstance[][]; unstyled: boolean }; export type WidgetSection = { name: string; description: string; visible: boolean; pinned: boolean; id: bigint; layout: Layout }; -export type FrontendMessages = { +export type FrontendMessage = { ClearAllNodeGraphWires: Record; DisplayDialog: { title: string; icon: string }; DialogClose: Record; diff --git a/frontend/src/state-providers/node-graph.ts b/frontend/src/state-providers/node-graph.ts index 2fdd0f3f35..ececf8a7cd 100644 --- a/frontend/src/state-providers/node-graph.ts +++ b/frontend/src/state-providers/node-graph.ts @@ -1,9 +1,8 @@ import { writable } from "svelte/store"; import type { Editor } from "@graphite/editor"; -import type { NodeGraphErrorDiagnostic, BoxSelection, FrontendClickTargets, ContextMenuInformation, FrontendNode, FrontendNodeType, WirePath, FrontendMessages } from "@graphite/messages"; - -type UpdateImportsExports = FrontendMessages["UpdateImportsExports"]; +import type { NodeGraphErrorDiagnostic, BoxSelection, FrontendClickTargets, ContextMenuInformation, FrontendNode, FrontendNodeType, WirePath } from "@graphite/messages"; +import type { MessageBody } from "@graphite/subscription-router"; export function createNodeGraphState(editor: Editor) { const { subscribe, update } = writable({ @@ -14,7 +13,7 @@ export function createNodeGraphState(editor: Editor) { layerWidths: new Map(), chainWidths: new Map(), hasLeftInputWire: new Map(), - updateImportsExports: undefined as UpdateImportsExports | undefined, + updateImportsExports: undefined as MessageBody<"UpdateImportsExports"> | undefined, nodes: new Map(), visibleNodes: new Set(), /// The index is the exposed input index. The exports have a first key value of u32::MAX. diff --git a/frontend/src/subscription-router.ts b/frontend/src/subscription-router.ts index f1ce31c574..81505f47e9 100644 --- a/frontend/src/subscription-router.ts +++ b/frontend/src/subscription-router.ts @@ -1,16 +1,25 @@ -import type { FrontendMessage, FrontendMessages, LayoutTarget, WidgetDiff, ToMessageMap } from "@graphite/messages"; +import type { FrontendMessage, LayoutTarget, WidgetDiff } from "@graphite/messages"; + +// Type convert a union of messages into a map of messages +export type ToMessageMap = { + [K in T extends string ? T : T extends object ? keyof T : never]: K extends T ? Record : T extends Record ? Payload : never; +}; + +export type MessageMap = ToMessageMap; +export type MessageName = keyof MessageMap; +export type MessageBody = Extract>[T]; export function createSubscriptionRouter() { // Callbacks are wrapped at subscription time to capture their type-specific data extraction in a closure, // so the stored function has a uniform signature and the map doesn't need per-key generic value types. - const subscriptions: Partial void>> = {}; + const subscriptions: Partial void>> = {}; const layoutCallbacks: Partial void>> = {}; - const subscribeFrontendMessage = (messageType: T, callback: (data: FrontendMessages[T]) => void) => { - subscriptions[messageType] = (taggedMessage: FrontendMessages) => callback(taggedMessage[messageType]); + const subscribeFrontendMessage = (messageType: T, callback: (data: MessageMap[T]) => void) => { + subscriptions[messageType] = (taggedMessage: MessageMap) => callback(taggedMessage[messageType]); }; - const unsubscribeFrontendMessage = (messageType: keyof FrontendMessages) => { + const unsubscribeFrontendMessage = (messageType: MessageName) => { delete subscriptions[messageType]; }; @@ -34,7 +43,7 @@ export function createSubscriptionRouter() { return message; } - const handleFrontendMessage = (messageType: keyof FrontendMessages, messageData: FrontendMessage) => { + const handleFrontendMessage = (messageType: MessageName, messageData: FrontendMessage) => { // Messages with non-empty data are provided by Serde JSON as an object with one key as the message name, like: { NameOfThisMessage: { ... } } // Messages with empty data are provided by Serde JSON as a string with the message name, like: "NameOfThisMessage" // Here we extract the payload object or create an empty payload object, as needed. @@ -43,7 +52,7 @@ export function createSubscriptionRouter() { // Resolve the dispatch thunk, depending on whether this is a layout update or a regular message. // UpdateLayout messages are dispatched to layout-specific callbacks based on the layout target. // The thunk is re-evaluated on each retry because the callback may not be registered yet. - let getHandler: () => ((taggedMessage: FrontendMessages) => void) | undefined = () => subscriptions[messageType]; + let getHandler: () => ((taggedMessage: MessageMap) => void) | undefined = () => subscriptions[messageType]; // Handle layout updates specially to route them to layout-specific callbacks and extract the diffs as the data to pass let target: LayoutTarget | undefined; diff --git a/frontend/wasm/README.md b/frontend/wasm/README.md index 3e0c03b76b..539ff0d1b5 100644 --- a/frontend/wasm/README.md +++ b/frontend/wasm/README.md @@ -1,7 +1,7 @@ # Overview of `/frontend/wasm/` ## WASM wrapper API: `src/editor_api.rs` -Provides bindings for JS to call functions defined in this file, and for FrontendMessages to be sent from Rust back to JS in the form of a callback to the subscription router. This WASM wrapper crate, since it's written in Rust, is able to call into the Editor crate's codebase and send FrontendMessages back to JS. +Provides bindings for JS to call functions defined in this file, and for `FrontendMessage`s to be sent from Rust back to JS in the form of a callback to the subscription router. This WASM wrapper crate, since it's written in Rust, is able to call into the Editor crate's codebase and send `FrontendMessage`s back to JS. ## WASM wrapper helper code: `src/helpers.rs` diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index fba8e36bac..5fe142b462 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -69,7 +69,7 @@ pub fn is_platform_native() -> bool { #[wasm_bindgen] #[derive(Clone)] pub struct EditorHandle { - /// This callback is called by the editor's dispatcher when directing FrontendMessages from Rust to JS + /// This callback is called by the editor's dispatcher when directing `FrontendMessage`s from Rust to JS frontend_message_handler_callback: js_sys::Function, } diff --git a/website/content/volunteer/guide/codebase-overview/_index.md b/website/content/volunteer/guide/codebase-overview/_index.md index 4a2fb50017..e9b13d34bf 100644 --- a/website/content/volunteer/guide/codebase-overview/_index.md +++ b/website/content/volunteer/guide/codebase-overview/_index.md @@ -44,5 +44,5 @@ Frontend-to-backend communication is achieved through a thin Rust translation la Backend-to-frontend communication happens by sending a queue of messages to the frontend message dispatcher. After the TS has called any wrapper API function to get into backend code execution, the editor's business logic runs and queues up each [`FrontendMessage`](https://github.com/GraphiteEditor/Graphite/tree/master/editor/src/messages/frontend/frontend_message.rs) which get mapped from Rust to JavaScript data structures in [`/frontend/src/messages.ts`](https://github.com/GraphiteEditor/Graphite/tree/master/frontend/src/messages.ts). Various TS code subscribes to these messages by calling: ```rs -subscribeFrontendMessage(MessageName, (messageData) => { /* callback code */ }); +subscribeFrontendMessage(NameOfMessage, (messageData) => { /* callback code */ }); ``` From 3cfa4ce61cf7a9533683699b922aeaf14bb286da Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 16:10:19 -0800 Subject: [PATCH 08/28] Switch type imports to the auto-generated file --- .../floating-menus/ColorPicker.svelte | 6 +- .../components/floating-menus/MenuList.svelte | 2 +- .../floating-menus/NodeCatalog.svelte | 2 +- .../components/floating-menus/Tooltip.svelte | 2 +- .../src/components/layout/FloatingMenu.svelte | 2 +- .../src/components/layout/LayoutCol.svelte | 2 +- .../src/components/layout/LayoutRow.svelte | 2 +- frontend/src/components/panels/Data.svelte | 2 +- .../src/components/panels/Document.svelte | 2 +- frontend/src/components/panels/Layers.svelte | 2 +- .../src/components/panels/Properties.svelte | 2 +- frontend/src/components/panels/Welcome.svelte | 2 +- frontend/src/components/views/Graph.svelte | 2 +- .../components/widgets/WidgetLayout.svelte | 2 +- .../components/widgets/WidgetSection.svelte | 2 +- .../src/components/widgets/WidgetSpan.svelte | 2 +- .../src/components/widgets/WidgetTable.svelte | 2 +- .../buttons/BreadcrumbTrailButtons.svelte | 2 +- .../widgets/buttons/IconButton.svelte | 2 +- .../widgets/buttons/ImageButton.svelte | 2 +- .../buttons/ParameterExposeButton.svelte | 2 +- .../widgets/buttons/PopoverButton.svelte | 3 +- .../widgets/buttons/TextButton.svelte | 2 +- .../widgets/inputs/CheckboxInput.svelte | 2 +- .../widgets/inputs/ColorInput.svelte | 2 +- .../widgets/inputs/CurveInput.svelte | 2 +- .../widgets/inputs/DropdownInput.svelte | 2 +- .../widgets/inputs/FieldInput.svelte | 2 +- .../widgets/inputs/NumberInput.svelte | 2 +- .../widgets/inputs/RadioInput.svelte | 2 +- .../widgets/inputs/ReferencePointInput.svelte | 2 +- .../widgets/inputs/SpectrumInput.svelte | 2 +- .../widgets/inputs/TextAreaInput.svelte | 2 +- .../widgets/inputs/TextInput.svelte | 2 +- .../widgets/inputs/WorkingColorsInput.svelte | 2 +- .../widgets/labels/IconLabel.svelte | 2 +- .../widgets/labels/ImageLabel.svelte | 2 +- .../widgets/labels/Separator.svelte | 2 +- .../widgets/labels/ShortcutLabel.svelte | 2 +- .../widgets/labels/TextLabel.svelte | 2 +- .../src/components/window/StatusBar.svelte | 2 +- .../src/components/window/TitleBar.svelte | 2 +- .../src/components/window/Workspace.svelte | 2 +- frontend/src/editor.ts | 5 +- frontend/src/messages.ts | 1 - frontend/src/messages_old.ts | 778 ------------------ frontend/src/state-providers/app-window.ts | 2 +- frontend/src/state-providers/dialog.ts | 2 +- frontend/src/state-providers/document.ts | 2 +- frontend/src/state-providers/node-graph.ts | 2 +- frontend/src/state-providers/portfolio.ts | 2 +- frontend/src/state-providers/tooltip.ts | 2 +- frontend/src/subscription-router.ts | 2 +- frontend/src/utility-functions/colors.ts | 2 +- frontend/src/utility-functions/widgets.ts | 2 +- 55 files changed, 55 insertions(+), 838 deletions(-) delete mode 100644 frontend/src/messages.ts delete mode 100644 frontend/src/messages_old.ts diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index 7ddc65326c..aaf7ee837d 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -1,8 +1,7 @@ {#each layout as layoutGroup} - {#if "row" in layoutGroup} - - {:else if "column" in layoutGroup} - - {:else if "section" in layoutGroup} - - {:else if "table" in layoutGroup} - + {#if "Row" in layoutGroup} + + {:else if "Column" in layoutGroup} + + {:else if "Section" in layoutGroup} + + {:else if "Table" in layoutGroup} + {/if} {/each} diff --git a/frontend/src/components/widgets/WidgetSection.svelte b/frontend/src/components/widgets/WidgetSection.svelte index 393ab37395..0314eb3114 100644 --- a/frontend/src/components/widgets/WidgetSection.svelte +++ b/frontend/src/components/widgets/WidgetSection.svelte @@ -61,10 +61,10 @@ {#if expanded} {#each widgetData.layout as layoutGroup} - {#if "row" in layoutGroup} - - {:else if "section" in layoutGroup} - + {#if "Row" in layoutGroup} + + {:else if "Section" in layoutGroup} + {/if} {/each} diff --git a/frontend/src/utility-functions/widgets.ts b/frontend/src/utility-functions/widgets.ts index 22da52c539..7d40826c9a 100644 --- a/frontend/src/utility-functions/widgets.ts +++ b/frontend/src/utility-functions/widgets.ts @@ -16,10 +16,10 @@ export function patchLayout(layout: /* &mut */ Layout, diffs: WidgetDiff[]) { const diffObject = update.widgetPath.reduce((targetLayout: UIItem | undefined, index: bigint): UIItem | undefined => { const i = Number(index); - if (targetLayout && "column" in targetLayout) return targetLayout.column.columnWidgets[i]; - if (targetLayout && "row" in targetLayout) return targetLayout.row.rowWidgets[i]; - if (targetLayout && "table" in targetLayout) return targetLayout.table.tableWidgets[i]; - if (targetLayout && "section" in targetLayout) return targetLayout.section.layout[i]; + if (targetLayout && "Column" in targetLayout) return targetLayout.Column.columnWidgets[i]; + if (targetLayout && "Row" in targetLayout) return targetLayout.Row.rowWidgets[i]; + if (targetLayout && "Table" in targetLayout) return targetLayout.Table.tableWidgets[i]; + if (targetLayout && "Section" in targetLayout) return targetLayout.Section.layout[i]; if (targetLayout && "widget" in targetLayout && "widgetId" in targetLayout) { if ("PopoverButton" in targetLayout.widget && targetLayout.widget.PopoverButton.popoverLayout) { return targetLayout.widget.PopoverButton.popoverLayout[i]; From ec78397eea8fc5debd96e7ec496ddbe0618cee63 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 16:33:54 -0800 Subject: [PATCH 10/28] Fix FillChoice deserialization --- .../messages/layout/layout_message_handler.rs | 61 ++----------------- .../components/floating-menus/Dialog.svelte | 2 +- 2 files changed, 7 insertions(+), 56 deletions(-) diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 5f2c647a4a..73dfa2ae9d 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -1,8 +1,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; -use graphene_std::raster::color::Color; -use graphene_std::vector::style::{FillChoice, GradientStop, GradientStops}; +use graphene_std::vector::style::FillChoice; use serde_json::Value; use std::collections::HashMap; @@ -158,60 +157,12 @@ impl LayoutMessageHandler { let callback_message = match action { WidgetValueAction::Commit => (color_button.on_commit.callback)(&()), WidgetValueAction::Update => { - // Decodes the colors in gamma, not linear - let decode_color = |color: &serde_json::map::Map| -> Option { - let red = color.get("red").and_then(|x| x.as_f64()).map(|x| x as f32); - let green = color.get("green").and_then(|x| x.as_f64()).map(|x| x as f32); - let blue = color.get("blue").and_then(|x| x.as_f64()).map(|x| x as f32); - let alpha = color.get("alpha").and_then(|x| x.as_f64()).map(|x| x as f32); - - if let (Some(red), Some(green), Some(blue), Some(alpha)) = (red, green, blue, alpha) - && let Some(color) = Color::from_rgbaf32(red, green, blue, alpha) - { - return Some(color); - } - None + let Ok(fill_choice) = serde_json::from_value::(value) else { + warn!("ColorInput update was not able to be parsed as FillChoice: {color_button:?}"); + return; }; - - (|| { - let Some(update_value) = value.as_object() else { - warn!("ColorInput update was not of type: object"); - return Message::NoOp; - }; - - // None - let is_none = update_value.get("none").and_then(|x| x.as_bool()); - if is_none == Some(true) { - color_button.value = FillChoice::None; - return (color_button.on_update.callback)(color_button); - } - - // Solid - if let Some(color) = decode_color(update_value) { - color_button.value = FillChoice::Solid(color); - return (color_button.on_update.callback)(color_button); - } - - // Gradient - let positions = update_value.get("position").and_then(|x| x.as_array()); - let midpoints = update_value.get("midpoint").and_then(|x| x.as_array()); - let colors = update_value.get("color").and_then(|x| x.as_array()); - - if let (Some(positions), Some(midpoints), Some(colors)) = (positions, midpoints, colors) { - let gradient_stops = positions.iter().zip(midpoints.iter()).zip(colors.iter()).filter_map(|((pos, mid), col)| { - let position = pos.as_f64()?; - let midpoint = mid.as_f64()?; - let color = col.as_object().and_then(decode_color)?; - Some(GradientStop { position, midpoint, color }) - }); - - color_button.value = FillChoice::Gradient(GradientStops::new(gradient_stops)); - return (color_button.on_update.callback)(color_button); - } - - warn!("ColorInput update was not able to be parsed with color data: {color_button:?}"); - Message::NoOp - })() + color_button.value = fill_choice; + (color_button.on_update.callback)(color_button) } }; diff --git a/frontend/src/components/floating-menus/Dialog.svelte b/frontend/src/components/floating-menus/Dialog.svelte index ce06b22626..b6ee5ad990 100644 --- a/frontend/src/components/floating-menus/Dialog.svelte +++ b/frontend/src/components/floating-menus/Dialog.svelte @@ -134,7 +134,7 @@ } .text-label.multiline { - -webkit-user-select: text; // Still required by Safari as of 2025 + -webkit-user-select: text; // Still required by Safari as of 2026 user-select: text; } From d8a986f0d2b1260a48d8bac64d2cee52c5cb1118 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 16:57:39 -0800 Subject: [PATCH 11/28] Fix small regression from #3837 --- node-graph/graph-craft/src/document/value.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 1bc10ec050..86c7b1397e 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -401,6 +401,8 @@ impl TaggedValue { () if ty == TypeId::of::() => FromStr::from_str(string).map(TaggedValue::U32).ok()?, () if ty == TypeId::of::() => to_dvec2(string).map(TaggedValue::DVec2)?, () if ty == TypeId::of::() => FromStr::from_str(string).map(TaggedValue::Bool).ok()?, + // `Color` (not in a table) is still currently needed by `BlackAndWhiteNode` and `ColorOverlayNode` GPU `shader_node(PerPixelAdjust)` variants + () if ty == TypeId::of::() => to_color(string).map(|color| TaggedValue::Color(Table::new_from_element(color)))?, () if ty == TypeId::of::>() => to_color(string).map(|color| TaggedValue::Color(Table::new_from_element(color)))?, () if ty == TypeId::of::>() => to_gradient(string).map(|color| TaggedValue::GradientTable(Table::new_from_element(color)))?, () if ty == TypeId::of::() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?, From ab3806ec7e49f2ce0e969a6f31924ea5a507a04f Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 18:13:21 -0800 Subject: [PATCH 12/28] Improve type safety --- .../src/messages/frontend/frontend_message.rs | 3 +- editor/src/messages/frontend/mod.rs | 3 + .../node_graph/node_graph_message_handler.rs | 132 +++++++++--------- .../portfolio/document/utility_types/nodes.rs | 3 +- .../floating-menus/ColorPicker.svelte | 23 ++- .../components/floating-menus/Tooltip.svelte | 5 +- .../src/components/panels/Document.svelte | 3 +- frontend/src/components/panels/Layers.svelte | 3 +- .../src/components/widgets/WidgetSpan.svelte | 27 ++-- .../widgets/inputs/CheckboxInput.svelte | 2 +- .../widgets/labels/ShortcutLabel.svelte | 15 +- frontend/src/editor.ts | 6 +- frontend/src/global.d.ts | 37 +++++ frontend/src/io-managers/input.ts | 6 +- frontend/src/io-managers/panic.ts | 3 +- frontend/src/state-providers/dialog.ts | 2 +- frontend/src/state-providers/fullscreen.ts | 8 +- .../src/utility-functions/keyboard-entry.ts | 6 +- 18 files changed, 174 insertions(+), 113 deletions(-) create mode 100644 frontend/src/global.d.ts diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index dbba09371d..ba5484a64b 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -1,3 +1,4 @@ +use super::IconName; use super::utility_types::{DocumentDetails, MouseCursorIcon, OpenDocument}; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::frontend::utility_types::EyedropperPreviewImage; @@ -27,7 +28,7 @@ pub enum FrontendMessage { // Display prefix: make the frontend show something, like a dialog DisplayDialog { title: String, - icon: String, + icon: IconName, }, DialogClose, DisplayDialogPanic { diff --git a/editor/src/messages/frontend/mod.rs b/editor/src/messages/frontend/mod.rs index 81d963f56a..1796932029 100644 --- a/editor/src/messages/frontend/mod.rs +++ b/editor/src/messages/frontend/mod.rs @@ -4,3 +4,6 @@ pub mod utility_types; #[doc(inline)] pub use frontend_message::{FrontendMessage, FrontendMessageDiscriminant}; + +// TODO: Make this an enum with the actual icon names, somehow derived from or tied to the frontend icon set +pub type IconName = String; diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index c4c67c9865..cce30e6ac9 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -2408,22 +2408,22 @@ impl NodeGraphMessageHandler { if let [node_id] = *nodes.as_slice() { properties.push(LayoutGroup::row(vec![ - Separator::new(SeparatorStyle::Related).widget_instance(), - IconLabel::new("Node").tooltip_description("Name of the selected node.").widget_instance(), - Separator::new(SeparatorStyle::Related).widget_instance(), - TextInput::new(context.network_interface.display_name(&node_id, context.selection_network_path)) - .tooltip_description("Name of the selected node.") - .on_update(move |text_input| { - NodeGraphMessage::SetDisplayName { - node_id, - alias: text_input.value.clone(), - skip_adding_history_step: false, - } - .into() - }) - .widget_instance(), - Separator::new(SeparatorStyle::Related).widget_instance(), - ])); + Separator::new(SeparatorStyle::Related).widget_instance(), + IconLabel::new("Node").tooltip_description("Name of the selected node.").widget_instance(), + Separator::new(SeparatorStyle::Related).widget_instance(), + TextInput::new(context.network_interface.display_name(&node_id, context.selection_network_path)) + .tooltip_description("Name of the selected node.") + .on_update(move |text_input| { + NodeGraphMessage::SetDisplayName { + node_id, + alias: text_input.value.clone(), + skip_adding_history_step: false, + } + .into() + }) + .widget_instance(), + Separator::new(SeparatorStyle::Related).widget_instance(), + ])); } properties.extend(selected_nodes); @@ -2434,15 +2434,15 @@ impl NodeGraphMessageHandler { // TODO: Display properties for encapsulating node when no nodes are selected in a nested network // This may require store a separate path for the properties panel let mut properties = vec![LayoutGroup::row(vec![ - Separator::new(SeparatorStyle::Related).widget_instance(), - IconLabel::new("File").tooltip_description("Name of the current document.").widget_instance(), - Separator::new(SeparatorStyle::Related).widget_instance(), - TextInput::new(context.document_name) - .tooltip_description("Name of the current document.") - .on_update(|text_input| DocumentMessage::RenameDocument { new_name: text_input.value.clone() }.into()) - .widget_instance(), - Separator::new(SeparatorStyle::Related).widget_instance(), - ])]; + Separator::new(SeparatorStyle::Related).widget_instance(), + IconLabel::new("File").tooltip_description("Name of the current document.").widget_instance(), + Separator::new(SeparatorStyle::Related).widget_instance(), + TextInput::new(context.document_name) + .tooltip_description("Name of the current document.") + .on_update(|text_input| DocumentMessage::RenameDocument { new_name: text_input.value.clone() }.into()) + .widget_instance(), + Separator::new(SeparatorStyle::Related).widget_instance(), + ])]; let Some(network) = context.network_interface.nested_network(context.selection_network_path) else { warn!("No network in collate_properties"); @@ -2479,47 +2479,47 @@ impl NodeGraphMessageHandler { } let mut layer_properties = vec![LayoutGroup::row(vec![ - Separator::new(SeparatorStyle::Related).widget_instance(), - IconLabel::new("Layer").tooltip_description("Name of the selected layer.").widget_instance(), - Separator::new(SeparatorStyle::Related).widget_instance(), - TextInput::new(context.network_interface.display_name(&layer, context.selection_network_path)) - .tooltip_description("Name of the selected layer.") - .on_update(move |text_input| { - NodeGraphMessage::SetDisplayName { - node_id: layer, - alias: text_input.value.clone(), - skip_adding_history_step: false, - } - .into() - }) - .widget_instance(), - Separator::new(SeparatorStyle::Related).widget_instance(), - PopoverButton::new() - .icon(Some("Node".to_string())) - .tooltip_description("Add an operation to the end of this layer's chain of nodes.") - .popover_layout({ - let compatible_type = context - .network_interface - .upstream_output_connector(&InputConnector::node(layer, 1), &[]) - .and_then(|upstream_output| context.network_interface.output_type(&upstream_output, &[]).add_node_string()); - - let mut node_chooser = NodeCatalog::new(); - node_chooser.intial_search = compatible_type.unwrap_or("".to_string()); - - let node_chooser = node_chooser - .on_update(move |node_type| { - NodeGraphMessage::CreateNodeInLayerWithTransaction { - node_type: node_type.clone(), - layer: LayerNodeIdentifier::new_unchecked(layer), - } - .into() - }) - .widget_instance(); - Layout(vec![LayoutGroup::row(vec![node_chooser])]) - }) - .widget_instance(), - Separator::new(SeparatorStyle::Related).widget_instance(), - ])]; + Separator::new(SeparatorStyle::Related).widget_instance(), + IconLabel::new("Layer").tooltip_description("Name of the selected layer.").widget_instance(), + Separator::new(SeparatorStyle::Related).widget_instance(), + TextInput::new(context.network_interface.display_name(&layer, context.selection_network_path)) + .tooltip_description("Name of the selected layer.") + .on_update(move |text_input| { + NodeGraphMessage::SetDisplayName { + node_id: layer, + alias: text_input.value.clone(), + skip_adding_history_step: false, + } + .into() + }) + .widget_instance(), + Separator::new(SeparatorStyle::Related).widget_instance(), + PopoverButton::new() + .icon(Some("Node".to_string())) + .tooltip_description("Add an operation to the end of this layer's chain of nodes.") + .popover_layout({ + let compatible_type = context + .network_interface + .upstream_output_connector(&InputConnector::node(layer, 1), &[]) + .and_then(|upstream_output| context.network_interface.output_type(&upstream_output, &[]).add_node_string()); + + let mut node_chooser = NodeCatalog::new(); + node_chooser.intial_search = compatible_type.unwrap_or("".to_string()); + + let node_chooser = node_chooser + .on_update(move |node_type| { + NodeGraphMessage::CreateNodeInLayerWithTransaction { + node_type: node_type.clone(), + layer: LayerNodeIdentifier::new_unchecked(layer), + } + .into() + }) + .widget_instance(); + Layout(vec![LayoutGroup::row(vec![node_chooser])]) + }) + .widget_instance(), + Separator::new(SeparatorStyle::Related).widget_instance(), + ])]; // Iterate through all the upstream nodes, but stop when we reach another layer (since that's a point where we switch from horizontal to vertical flow) let node_properties = context diff --git a/editor/src/messages/portfolio/document/utility_types/nodes.rs b/editor/src/messages/portfolio/document/utility_types/nodes.rs index 85fd1bd0f0..bfa94ee2e5 100644 --- a/editor/src/messages/portfolio/document/utility_types/nodes.rs +++ b/editor/src/messages/portfolio/document/utility_types/nodes.rs @@ -1,5 +1,6 @@ use super::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use super::network_interface::NodeNetworkInterface; +use crate::messages::frontend::IconName; use crate::messages::tool::common_functionality::graph_modification_utils; use glam::DVec2; use graph_craft::document::{NodeId, NodeNetwork}; @@ -21,7 +22,7 @@ pub struct LayerPanelEntry { #[serde(rename = "implementationName")] pub implementation_name: String, #[serde(rename = "iconName")] - pub icon_name: Option, + pub icon_name: Option, pub alias: String, #[serde(rename = "inSelectedNetwork")] pub in_selected_network: bool, diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index aaf7ee837d..ca0b8fc074 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -116,8 +116,21 @@ $: oldColor = oldIsNone ? undefined : createColorFromHSVA(oldHue, oldSaturation, oldValue, oldAlpha); $: newColor = isNone ? undefined : createColorFromHSVA(hue, saturation, value, alpha); - $: rgbChannels = Object.entries(newColor ? colorToRgb255(newColor) : { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][]; - $: hsvChannels = Object.entries(!isNone ? { h: hue * 360, s: saturation * 100, v: value * 100 } : { h: undefined, s: undefined, v: undefined }) as [keyof HSV, number | undefined][]; + $: rgbChannels = ((): [keyof RGB, number | undefined][] => { + const rgb = newColor ? colorToRgb255(newColor) : undefined; + return [ + ["r", rgb?.r], + ["g", rgb?.g], + ["b", rgb?.b], + ]; + })(); + $: hsvChannels = ((): [keyof HSV, number | undefined][] => { + return [ + ["h", isNone ? undefined : hue * 360], + ["s", isNone ? undefined : saturation * 100], + ["v", isNone ? undefined : value * 100], + ]; + })(); $: opaqueHueColor = createColorFromHSVA(hue, 1, 1, 1); $: outlineFactor = Math.max( contrastingOutlineFactor(newColor ? { Solid: newColor } : ("None" as const), "--color-2-mildblack", 0.01), @@ -403,16 +416,14 @@ // TODO: Implement support in the desktop app for OS-level color picking if (isDesktop()) return false; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return Boolean((window as any).EyeDropper); + return window.EyeDropper !== undefined; } async function activateEyedropperSample() { if (!eyedropperSupported()) return; try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await new (window as any).EyeDropper().open(); + const result = await new EyeDropper().open(); dispatch("startHistoryTransaction"); setColorCode(result.sRGBHex); } catch { diff --git a/frontend/src/components/floating-menus/Tooltip.svelte b/frontend/src/components/floating-menus/Tooltip.svelte index 39cced24a8..adc51716db 100644 --- a/frontend/src/components/floating-menus/Tooltip.svelte +++ b/frontend/src/components/floating-menus/Tooltip.svelte @@ -21,7 +21,10 @@ $: shortcut = ((shortcutJSON) => { if (!shortcutJSON) return undefined; try { - return JSON.parse(shortcutJSON) as LabeledShortcut; + const parsed: unknown = JSON.parse(shortcutJSON); + if (!Array.isArray(parsed)) return undefined; + + return parsed as LabeledShortcut; } catch { return undefined; } diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 468af0d26d..fe16582a8a 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -174,8 +174,7 @@ const canvasName = placeholder.getAttribute("data-canvas-placeholder"); if (!canvasName) return; // Get the canvas element from the global storage - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let canvas = (window as any).imageCanvases[canvasName]; + let canvas = window.imageCanvases[canvasName]; // Get logical dimensions from foreignObject parent (set by backend) const foreignObject = placeholder.parentElement; diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 4b2c8fdaf5..286b8a8384 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -4,7 +4,6 @@ import type { LayerPanelEntry, LayerStructureEntry, Layout } from "@graphite/../wasm/pkg/graphite_wasm"; import type { Editor } from "@graphite/editor"; - import type { IconName } from "@graphite/icons"; import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { TooltipState } from "@graphite/state-providers/tooltip"; import { pasteFile } from "@graphite/utility-functions/files"; @@ -601,7 +600,7 @@ {/if} {#if listing.entry.iconName} - + {/if} onEditLayerName(listing)}> !exclusions.has(key))); } - // Extracts the kind name and props from a Widget tagged enum (e.g. `{ TextButton: { label: "..." } }` -> `["TextButton", { label: "..." }]`) - function unwrapWidget(widgetInstance: WidgetInstance): [WidgetKind, Record] { - const entries = Object.entries(widgetInstance.widget); - return entries[0] as [WidgetKind, Record]; + function unwrapWidget(widgetInstance: WidgetInstance): [WidgetKind, Record] | undefined { + const entry = Object.entries(widgetInstance.widget)[0]; + if (!entry || !(entry[0] in widgetRegistry)) return undefined; + return entry as [WidgetKind, Record]; } type WidgetConfig = { @@ -242,14 +242,17 @@
{#each widgets as widget, widgetIndex} - {@const [kind, widgetProps] = unwrapWidget(widget)} - {@const config = widgetRegistry[kind]} - {@const props = config?.getProps(widgetProps, widgetIndex)} - {@const slot = config?.getSlotContent?.(widgetProps)} - {#if props !== undefined && slot !== undefined} - {slot} - {:else if props !== undefined} - + {@const unwrapped = unwrapWidget(widget)} + {#if unwrapped} + {@const [kind, widgetProps] = unwrapped} + {@const config = widgetRegistry[kind]} + {@const props = config?.getProps(widgetProps, widgetIndex)} + {@const slot = config?.getSlotContent?.(widgetProps)} + {#if props !== undefined && slot !== undefined} + {slot} + {:else if props !== undefined} + + {/if} {/if} {/each}
diff --git a/frontend/src/components/widgets/inputs/CheckboxInput.svelte b/frontend/src/components/widgets/inputs/CheckboxInput.svelte index fca47532f8..2cc226fdeb 100644 --- a/frontend/src/components/widgets/inputs/CheckboxInput.svelte +++ b/frontend/src/components/widgets/inputs/CheckboxInput.svelte @@ -23,7 +23,7 @@ let inputElement: HTMLInputElement | undefined; $: id = forLabel !== undefined ? String(forLabel) : backupId; - $: displayIcon = (!checked && icon === "Checkmark" ? "Empty12px" : icon) as IconName; + $: displayIcon = !checked && icon === "Checkmark" ? "Empty12px" : icon; export function isChecked() { return checked; diff --git a/frontend/src/components/widgets/labels/ShortcutLabel.svelte b/frontend/src/components/widgets/labels/ShortcutLabel.svelte index 93858f7f7a..a4a10e43ee 100644 --- a/frontend/src/components/widgets/labels/ShortcutLabel.svelte +++ b/frontend/src/components/widgets/labels/ShortcutLabel.svelte @@ -89,7 +89,20 @@ } function mouseHintIcon(input: MouseMotion): IconName { - return `MouseHint${input}` as IconName; + return { + None: "MouseHintNone" as const, + Lmb: "MouseHintLmb" as const, + Rmb: "MouseHintRmb" as const, + Mmb: "MouseHintMmb" as const, + ScrollUp: "MouseHintScrollUp" as const, + ScrollDown: "MouseHintScrollDown" as const, + Drag: "MouseHintDrag" as const, + LmbDouble: "MouseHintLmbDouble" as const, + LmbDrag: "MouseHintLmbDrag" as const, + RmbDrag: "MouseHintRmbDrag" as const, + RmbDouble: "MouseHintRmbDouble" as const, + MmbDrag: "MouseHintMmbDrag" as const, + }[input]; } diff --git a/frontend/src/editor.ts b/frontend/src/editor.ts index 6d5b0e9626..5bc0b95167 100644 --- a/frontend/src/editor.ts +++ b/frontend/src/editor.ts @@ -28,10 +28,8 @@ export async function initWasm() { } wasmImport = await wasmMemory(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any).imageCanvases = {}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any).receiveNativeMessage = receiveNativeMessage; + window.imageCanvases = {}; + window.receiveNativeMessage = receiveNativeMessage; } // Should be called after running `initWasm()` and its promise resolving. diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts new file mode 100644 index 0000000000..822010d328 --- /dev/null +++ b/frontend/src/global.d.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +// Graphite's custom properties added to the global `window` object +interface Window { + imageCanvases: Record; + receiveNativeMessage?: (buffer: ArrayBuffer) => void; +} + +// Experimental Keyboard API: https://developer.mozilla.org/en-US/docs/Web/API/Keyboard +interface Navigator { + keyboard?: Keyboard; +} +interface Keyboard { + lock(keyCodes?: string[]): Promise; + unlock(): void; + getLayoutMap(): Promise; +} +interface KeyboardLayoutMap { + entries(): IterableIterator<[string, string]>; + get(key: string): string | undefined; + has(key: string): boolean; + readonly size: number; +} + +// Experimental EyeDropper API: https://developer.mozilla.org/en-US/docs/Web/API/EyeDropper +interface Window { + EyeDropper?: typeof EyeDropper; +} +declare class EyeDropper { + constructor(); + open(options?: { signal?: AbortSignal }): Promise<{ sRGBHex: string }>; +} + +// Non-standard Stack Trace Limit API: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stackTraceLimit +interface ErrorConstructor { + stackTraceLimit?: number; +} diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index a2bb5257df..ab3ee4846a 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -414,8 +414,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli const blob = await item.getType("text/plain"); const reader = new FileReader(); reader.onload = () => { - const text = reader.result as string; - editor.handle.pasteText(text); + if (typeof reader.result === "string") editor.handle.pasteText(reader.result); }; reader.readAsText(blob); return true; @@ -429,8 +428,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli const blob = await item.getType("text/plain"); const reader = new FileReader(); reader.onload = () => { - const text = reader.result as string; - editor.handle.pasteSvg(undefined, text); + if (typeof reader.result === "string") editor.handle.pasteSvg(undefined, reader.result); }; reader.readAsText(blob); return true; diff --git a/frontend/src/io-managers/panic.ts b/frontend/src/io-managers/panic.ts index b8e1635265..da3cc1df3f 100644 --- a/frontend/src/io-managers/panic.ts +++ b/frontend/src/io-managers/panic.ts @@ -7,8 +7,7 @@ export function createPanicManager(editor: Editor, dialogState: DialogState) { // Code panic dialog and console error editor.subscriptions.subscribeFrontendMessage("DisplayDialogPanic", (data) => { // `Error.stackTraceLimit` is only available in V8/Chromium - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (Error as any).stackTraceLimit = Infinity; + Error.stackTraceLimit = Infinity; const stackTrace = new Error().stack || ""; const panicDetails = `${data.panicInfo}${stackTrace ? `\n\n${stackTrace}` : ""}`; diff --git a/frontend/src/state-providers/dialog.ts b/frontend/src/state-providers/dialog.ts index 7f77121cd8..73ab73e3ab 100644 --- a/frontend/src/state-providers/dialog.ts +++ b/frontend/src/state-providers/dialog.ts @@ -50,7 +50,7 @@ export function createDialogState(editor: Editor) { state.visible = true; state.title = data.title; - state.icon = data.icon as IconName; + state.icon = data.icon; return state; }); diff --git a/frontend/src/state-providers/fullscreen.ts b/frontend/src/state-providers/fullscreen.ts index e13fcaab48..aeac8b5646 100644 --- a/frontend/src/state-providers/fullscreen.ts +++ b/frontend/src/state-providers/fullscreen.ts @@ -4,8 +4,7 @@ import type { Editor } from "@graphite/editor"; export function createFullscreenState(editor: Editor) { // Experimental Keyboard API: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/keyboard - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const keyboardLockApiSupported: Readonly = "keyboard" in navigator && (navigator as any).keyboard && "lock" in (navigator as any).keyboard; + const keyboardLockApiSupported: Readonly = navigator.keyboard !== undefined && "lock" in navigator.keyboard; const { subscribe, update } = writable({ windowFullscreen: false, @@ -24,9 +23,8 @@ export function createFullscreenState(editor: Editor) { async function enterFullscreen() { await document.documentElement.requestFullscreen(); - if (keyboardLockApiSupported) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (navigator as any).keyboard.lock(["ControlLeft", "ControlRight"]); + if (keyboardLockApiSupported && navigator.keyboard) { + await navigator.keyboard.lock(["ControlLeft", "ControlRight"]); update((state) => { state.keyboardLocked = true; diff --git a/frontend/src/utility-functions/keyboard-entry.ts b/frontend/src/utility-functions/keyboard-entry.ts index 1749f53c0a..bcbad8310d 100644 --- a/frontend/src/utility-functions/keyboard-entry.ts +++ b/frontend/src/utility-functions/keyboard-entry.ts @@ -94,10 +94,8 @@ export async function getLocalizedScanCode(e: KeyboardEvent): Promise { // It is likely a weird symbol that isn't in the A-Z range even with accents removed. // It might be a symbol from an Option key combination on a Mac. Or it might be from a non-Latin alphabet like Cyrillic. if (!KEY_ATTRIBUTE_VALUES.has(keyText)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (navigator && "keyboard" in navigator && "getLayoutMap" in (navigator as any).keyboard) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const layout = await (navigator as any).keyboard.getLayoutMap(); + if (navigator.keyboard && "getLayoutMap" in navigator.keyboard) { + const layout = await navigator.keyboard.getLayoutMap(); type KeyCode = string; type KeySymbol = string; From 3619170a49f7fa03bb1c833c431bb35f0868349c Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 19:32:49 -0800 Subject: [PATCH 13/28] Make WidgetSpan type-safe --- .../src/components/widgets/WidgetSpan.svelte | 78 +++++++++++++------ 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/widgets/WidgetSpan.svelte b/frontend/src/components/widgets/WidgetSpan.svelte index fa11458e63..b293efe290 100644 --- a/frontend/src/components/widgets/WidgetSpan.svelte +++ b/frontend/src/components/widgets/WidgetSpan.svelte @@ -29,7 +29,12 @@ import ShortcutLabel from "@graphite/components/widgets/labels/ShortcutLabel.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; + // Extract the discriminant key names from the Widget tagged enum union (e.g. "TextButton" | "CheckboxInput" | ...) type WidgetKind = Widget extends infer T ? (T extends Record ? K & string : never) : never; + // Extract the props type for a specific widget kind (e.g. WidgetProps<"TextButton"> gives the WASM-generated TextButton interface) + type WidgetProps = Extract>[K]; + // A Widget tagged enum unwrapped into a correlated [kind, props] tuple + type UnwrappedWidget = { [K in WidgetKind]: [kind: K, props: WidgetProps] }[WidgetKind]; const editor = getContext("editor"); @@ -59,27 +64,55 @@ editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function exclude(props: Record, additional: string[]): Record { - const exclusions = new Set(additional); - return Object.fromEntries(Object.entries(props).filter(([key]) => !exclusions.has(key))); + // Extracts the kind and props from a Widget tagged enum, validated against the widget registry. + // The overload declares the precise correlated return type while the implementation uses broader types. + function unwrapWidget(widgetInstance: WidgetInstance): UnwrappedWidget | undefined; + function unwrapWidget(widgetInstance: WidgetInstance) { + const entry = Object.entries(widgetInstance.widget)[0]; + if (!entry || !(entry[0] in widgetResolvers)) return undefined; + return entry; } - function unwrapWidget(widgetInstance: WidgetInstance): [WidgetKind, Record] | undefined { - const entry = Object.entries(widgetInstance.widget)[0]; - if (!entry || !(entry[0] in widgetRegistry)) return undefined; - return entry as [WidgetKind, Record]; + // Resolves the unwrapped widget through the registry to get its Svelte component and computed props. + function resolveWidget([kind, widgetProps]: UnwrappedWidget, widgetIndex: number) { + const config = widgetResolvers[kind]; + return { + component: config.component, + props: config.getProps(widgetProps, widgetIndex), + slot: config.getSlotContent?.(widgetProps), + }; } - type WidgetConfig = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - component: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getProps(props: Record, widgetIndex: number): Record | undefined; - getSlotContent?(props: Record): string; + // Svelte has no variance-safe base type for component constructors + // eslint-disable-next-line @typescript-eslint/no-explicit-any + type SvelteComponentAny = any; + + type WidgetConfig = { + component: SvelteComponentAny; + getProps(props: WidgetProps, widgetIndex: number): Record | undefined; + getSlotContent?(props: WidgetProps): string; }; - const widgetRegistry: Record = { + // The union of all individual widget props types (distributed across each WidgetKind member) + type AnyWidgetProps = { [K in WidgetKind]: WidgetProps }[WidgetKind]; + + // Uniform view for runtime lookup — widens the per-kind config types to a single type that + // accepts any widget props, avoiding the correlated unions problem at the call site + type WidgetResolver = { + component: SvelteComponentAny; + getProps(props: AnyWidgetProps, widgetIndex: number): Record | undefined; + getSlotContent?(props: AnyWidgetProps): string; + }; + + // Overload: callers provide the precise mapped type (preserving per-entry type inference). + // Implementation: receives/returns the widened uniform type (no cast needed). + // Method syntax bivariance makes WidgetConfig assignable to WidgetResolver in the overload check. + function createWidgetResolvers(registry: { [K in WidgetKind]: WidgetConfig }): Record; + function createWidgetResolvers(registry: Record): Record { + return registry; + } + + const widgetResolvers = createWidgetResolvers({ CheckboxInput: { component: CheckboxInput, getProps: (props, index) => ({ @@ -234,24 +267,21 @@ }, TextLabel: { component: TextLabel, - getProps: (props) => exclude(props, ["value"]), - getSlotContent: (props) => props.value as string, + getProps: ({ value: _, ...rest }) => rest, + getSlotContent: (props) => props.value, }, - }; + });
{#each widgets as widget, widgetIndex} {@const unwrapped = unwrapWidget(widget)} {#if unwrapped} - {@const [kind, widgetProps] = unwrapped} - {@const config = widgetRegistry[kind]} - {@const props = config?.getProps(widgetProps, widgetIndex)} - {@const slot = config?.getSlotContent?.(widgetProps)} + {@const { component, props, slot } = resolveWidget(unwrapped, widgetIndex)} {#if props !== undefined && slot !== undefined} - {slot} + {slot} {:else if props !== undefined} - + {/if} {/if} {/each} From 299ed5c21d3b1a0ab1f87b88fd2ac68b1080f292 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Mar 2026 20:21:00 -0800 Subject: [PATCH 14/28] More cleanup and type safety --- .../floating-menus/ColorPicker.svelte | 4 +-- .../src/components/layout/FloatingMenu.svelte | 4 +-- frontend/src/components/panels/Welcome.svelte | 4 +-- .../widgets/inputs/NumberInput.svelte | 8 ++--- .../widgets/inputs/RulerInput.svelte | 6 ++-- .../widgets/inputs/ScrollbarInput.svelte | 6 +--- .../src/components/window/MainWindow.svelte | 4 +-- frontend/src/components/window/Panel.svelte | 36 ++++++------------- .../src/components/window/TitleBar.svelte | 8 ++--- frontend/src/io-managers/input.ts | 12 +++---- frontend/src/io-managers/persistence.ts | 3 +- frontend/src/utility-functions/platform.ts | 23 ------------ 12 files changed, 36 insertions(+), 82 deletions(-) diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index ca0b8fc074..4d9e9f5b7c 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -1,6 +1,7 @@ - - - - diff --git a/frontend/src/components/floating-menus/MenuList.svelte b/frontend/src/components/floating-menus/MenuList.svelte index f243a771c7..aeb196f736 100644 --- a/frontend/src/components/floating-menus/MenuList.svelte +++ b/frontend/src/components/floating-menus/MenuList.svelte @@ -154,7 +154,7 @@ function onScroll(e: Event) { if (!virtualScrollingEntryHeight) return; - virtualScrollingEntriesStart = (e.target as HTMLElement)?.scrollTop || 0; + virtualScrollingEntriesStart = e.target instanceof HTMLElement ? e.target.scrollTop : 0; } function getChildReference(menuListEntry: MenuListEntry): MenuList | undefined { diff --git a/frontend/src/components/layout/FloatingMenu.svelte b/frontend/src/components/layout/FloatingMenu.svelte index f3df2d7ebb..ba69a4bab0 100644 --- a/frontend/src/components/layout/FloatingMenu.svelte +++ b/frontend/src/components/layout/FloatingMenu.svelte @@ -307,7 +307,7 @@ function pointerMoveHandler(e: PointerEvent) { // This element and the element being hovered over - const target = e.target as HTMLElement | undefined; + const target = e.target instanceof HTMLElement ? e.target : undefined; // Get the spawner element (that which is clicked to spawn this floating menu) // Assumes the spawner is a sibling of this FloatingMenu component @@ -396,9 +396,9 @@ else { const foundTarget = filteredListOfDescendantSpawners.find((item: Element): boolean => item === targetSpawner); // If the currently hovered spawner is one of the found valid hover-transferrable spawners, swap to it by clicking on it - if (foundTarget) { + if (foundTarget instanceof HTMLElement) { dispatch("open", false); - (foundTarget as HTMLElement).click(); + foundTarget.click(); } // In either case, we are done searching diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index fe16582a8a..3dc412250e 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -419,7 +419,8 @@ } function gradientStopPickerDirection(position: { x: number; y: number } | undefined, viewport: HTMLDivElement | undefined): MenuDirection { - const picker = (gradientStopPicker?.div()?.querySelector("[data-floating-menu-content]") || undefined) as HTMLElement | undefined; + const element = gradientStopPicker?.div()?.querySelector("[data-floating-menu-content]"); + const picker = element instanceof HTMLElement ? element : undefined; if (!picker || !position || !viewport) return "Bottom"; const roomRight = position.x + picker.offsetWidth - viewport.clientWidth; diff --git a/frontend/src/components/views/Graph.svelte b/frontend/src/components/views/Graph.svelte index 78db77f7b3..646c621c8e 100644 --- a/frontend/src/components/views/Graph.svelte +++ b/frontend/src/components/views/Graph.svelte @@ -80,7 +80,8 @@ function setEditingImportName(event: Event) { if (editingNameImportIndex !== undefined) { - let text = (event.target as HTMLInputElement)?.value; + if (!(event.target instanceof HTMLInputElement)) return; + let text = event.target.value; editor.handle.setImportName(editingNameImportIndex, text); editingNameImportIndex = undefined; } @@ -88,7 +89,8 @@ function setEditingExportName(event: Event) { if (editingNameExportIndex !== undefined) { - let text = (event.target as HTMLInputElement)?.value; + if (!(event.target instanceof HTMLInputElement)) return; + let text = event.target.value; editor.handle.setExportName(editingNameExportIndex, text); editingNameExportIndex = undefined; } diff --git a/frontend/src/components/widgets/buttons/TextButton.svelte b/frontend/src/components/widgets/buttons/TextButton.svelte index 0030db9d17..9242344980 100644 --- a/frontend/src/components/widgets/buttons/TextButton.svelte +++ b/frontend/src/components/widgets/buttons/TextButton.svelte @@ -53,7 +53,7 @@ } // Focus the target so that keyboard inputs are sent to the dropdown - (e.target as HTMLElement | undefined)?.focus(); + if (e.target instanceof HTMLElement) e.target.focus(); // Open the menu list floating menu if (self) self.open = true; diff --git a/frontend/src/components/widgets/inputs/CheckboxInput.svelte b/frontend/src/components/widgets/inputs/CheckboxInput.svelte index 2cc226fdeb..0f48c3e539 100644 --- a/frontend/src/components/widgets/inputs/CheckboxInput.svelte +++ b/frontend/src/components/widgets/inputs/CheckboxInput.svelte @@ -12,7 +12,7 @@ // Content export let checked = false; - export let icon: IconName = "Checkmark"; + export let icon: IconName | undefined = undefined; export let forLabel: bigint | undefined = undefined; export let disabled = false; // Tooltips @@ -23,7 +23,7 @@ let inputElement: HTMLInputElement | undefined; $: id = forLabel !== undefined ? String(forLabel) : backupId; - $: displayIcon = !checked && icon === "Checkmark" ? "Empty12px" : icon; + $: displayIcon = !checked && (!icon || icon === "Checkmark") ? "Empty12px" : icon || "Checkmark"; export function isChecked() { return checked; @@ -34,8 +34,8 @@ } function toggleCheckboxFromLabel(e: KeyboardEvent) { - const target = (e.target || undefined) as HTMLLabelElement | undefined; - const previousSibling = (target?.previousSibling || undefined) as HTMLInputElement | undefined; + const target = e.target instanceof HTMLLabelElement ? e.target : undefined; + const previousSibling = target?.previousSibling instanceof HTMLInputElement ? target.previousSibling : undefined; previousSibling?.click(); } diff --git a/frontend/src/components/widgets/inputs/DropdownInput.svelte b/frontend/src/components/widgets/inputs/DropdownInput.svelte index 0f4774b91e..59a81fe8ae 100644 --- a/frontend/src/components/widgets/inputs/DropdownInput.svelte +++ b/frontend/src/components/widgets/inputs/DropdownInput.svelte @@ -8,7 +8,18 @@ import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; - const DASH_ENTRY = { value: "", label: "-" }; + const DASH_ENTRY: MenuListEntry = { + value: "", + label: "-", + icon: undefined, + disabled: false, + children: [], + childrenHash: 0n, + font: undefined, + tooltipLabel: "", + tooltipDescription: "", + tooltipShortcut: undefined, + }; const dispatch = createEventDispatcher<{ selectedIndex: number; hoverInEntry: number; hoverOutEntry: number }>(); @@ -49,13 +60,13 @@ } // Called only when `selectedIndex` is changed from outside this component - function watchSelectedIndex(_?: typeof selectedIndex) { + function watchSelectedIndex(_: typeof selectedIndex) { activeEntrySkipWatcher = true; activeEntry = makeActiveEntry(); } // Called only when `entries` is changed from outside this component - function watchEntries(_?: typeof entries) { + function watchEntries(_: typeof entries) { activeEntrySkipWatcher = true; activeEntry = makeActiveEntry(); } @@ -102,7 +113,7 @@ } function unFocusDropdownBox(e: FocusEvent) { - const blurTarget = (e.target as HTMLDivElement | undefined)?.closest("[data-dropdown-input]") || undefined; + const blurTarget = (e.target instanceof Element ? e.target.closest("[data-dropdown-input]") : undefined) || undefined; if (blurTarget !== self?.div?.()) open = false; } diff --git a/frontend/src/components/widgets/labels/ShortcutLabel.svelte b/frontend/src/components/widgets/labels/ShortcutLabel.svelte index a4a10e43ee..6651c20647 100644 --- a/frontend/src/components/widgets/labels/ShortcutLabel.svelte +++ b/frontend/src/components/widgets/labels/ShortcutLabel.svelte @@ -16,7 +16,7 @@ if (typeof labeledKeyOrMouseMotion === "string") return { mouseMotion: labeledKeyOrMouseMotion }; // `key` is the name of the `Key` enum in Rust, while `label` is the localized string to display (if it doesn't become an icon) - let key = labeledKeyOrMouseMotion.key; + let key: Key | "Option" = labeledKeyOrMouseMotion.key; const label = labeledKeyOrMouseMotion.label; // Replace Alt and Accel keys with their Mac-specific equivalents @@ -57,7 +57,7 @@ return consolidatedList; } - function keyboardHintIcon(input: Key): IconName | undefined { + function keyboardHintIcon(input: Key | "Option"): IconName | undefined { switch (input) { case "ArrowDown": return "KeyboardArrowDown"; diff --git a/frontend/src/components/window/Workspace.svelte b/frontend/src/components/window/Workspace.svelte index 8244f5504b..53c394ac5c 100644 --- a/frontend/src/components/window/Workspace.svelte +++ b/frontend/src/components/window/Workspace.svelte @@ -40,15 +40,19 @@ const portfolio = getContext("portfolio"); function resizePanel(e: PointerEvent) { - const gutter = (e.target || undefined) as HTMLDivElement | undefined; - const nextSibling = (gutter?.nextElementSibling || undefined) as HTMLDivElement | undefined; - const prevSibling = (gutter?.previousElementSibling || undefined) as HTMLDivElement | undefined; - const parentElement = (gutter?.parentElement || undefined) as HTMLDivElement | undefined; + const gutter = e.target; + if (!(gutter instanceof HTMLDivElement)) return; - const nextSiblingName = nextSibling?.getAttribute("data-subdivision-name") || undefined; - const prevSiblingName = prevSibling?.getAttribute("data-subdivision-name") || undefined; + const nextSibling = gutter.nextElementSibling; + const prevSibling = gutter.previousElementSibling; - if (!gutter || !nextSibling || !prevSibling || !parentElement || !nextSiblingName || !prevSiblingName || !(nextSiblingName in PANEL_SIZES) || !(prevSiblingName in PANEL_SIZES)) return; + const parentElement = gutter.parentElement; + if (!(nextSibling instanceof HTMLDivElement) || !(prevSibling instanceof HTMLDivElement) || !(parentElement instanceof HTMLDivElement)) return; + + const nextSiblingName = nextSibling.getAttribute("data-subdivision-name") || undefined; + const prevSiblingName = prevSibling.getAttribute("data-subdivision-name") || undefined; + + if (!nextSiblingName || !prevSiblingName || !(nextSiblingName in PANEL_SIZES) || !(prevSiblingName in PANEL_SIZES)) return; // Are we resizing horizontally? const isHorizontal = gutter.getAttribute("data-gutter-horizontal") !== null; diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index 491d5e9cbb..3032398e34 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -29,7 +29,8 @@ type EventListenerTarget = { }; export function createInputManager(editor: Editor, dialog: DialogState, portfolio: PortfolioState, document: DocumentState, fullscreen: FullscreenState): () => void { - const app = window.document.querySelector("[data-app-container]") as HTMLElement | undefined; + const appElement = window.document.querySelector("[data-app-container]"); + const app = appElement instanceof HTMLElement ? appElement : null; app?.focus(); let viewportPointerInteractionOngoing = false; diff --git a/frontend/src/utility-functions/viewports.ts b/frontend/src/utility-functions/viewports.ts index 370d5567f8..670f2b1bea 100644 --- a/frontend/src/utility-functions/viewports.ts +++ b/frontend/src/utility-functions/viewports.ts @@ -11,7 +11,8 @@ export function setupViewportResizeObserver(editor: Editor) { const viewports = Array.from(window.document.querySelectorAll("[data-viewport-container]")); if (viewports.length <= 0) return; - const viewport = viewports[0] as HTMLElement; + const viewport = viewports[0]; + if (!(viewport instanceof HTMLElement)) return; resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 0000000000..e5bfbadbc8 --- /dev/null +++ b/frontend/svelte.config.js @@ -0,0 +1,8 @@ +import { sveltePreprocess } from "svelte-preprocess"; + +export default { + preprocess: sveltePreprocess(), + compilerOptions: /** @type {import("svelte/compiler").ModuleCompileOptions} */ ({ + warningFilter: (warning) => !warning.code.startsWith("a11y_") && !["css_unused_selector"].includes(warning.code), + }), +}; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 6615471470..9b4da63605 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,7 +3,6 @@ import { readFileSync } from "fs"; import path from "path"; import { svelte } from "@sveltejs/vite-plugin-svelte"; -import { sveltePreprocess } from "svelte-preprocess"; import { defineConfig } from "vite"; import type { PluginOption } from "vite"; import { DynamicPublicDirectory as viteMultipleAssets } from "vite-multiple-assets"; @@ -31,25 +30,7 @@ export default defineConfig(({ mode }) => { function plugins(mode: string): PluginOption[] { const plugins = [ - svelte({ - preprocess: [sveltePreprocess()], - onwarn(warning, defaultHandler) { - const suppressed = [ - "css-unused-selector", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` - "vite-plugin-svelte-css-no-scopable-elements", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` - "a11y-no-static-element-interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` - "a11y-no-noninteractive-element-interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` - "a11y-click-events-have-key-events", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` - "a11y_consider_explicit_label", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` - "a11y_click_events_have_key_events", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` - "a11y_no_noninteractive_element_interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` - "a11y_no_static_element_interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` - ]; - if (suppressed.includes(warning.code)) return; - - defaultHandler?.(warning); - }, - }), + svelte(), viteMultipleAssets( // Additional static asset directories besides `public/` [ diff --git a/proc-macros/src/widget_builder.rs b/proc-macros/src/widget_builder.rs index 601ec12083..a1ec05b0ea 100644 --- a/proc-macros/src/widget_builder.rs +++ b/proc-macros/src/widget_builder.rs @@ -14,18 +14,36 @@ fn has_attribute(attrs: &[Attribute], target: &str) -> bool { /// Make setting strings easier by allowing all types that `impl Into` /// /// Returns the new input type and a conversion to the original. -fn easier_string_assignment(field_ty: &Type, field_ident: &Ident) -> (TokenStream2, TokenStream2) { - if let Type::Path(type_path) = field_ty { - if let Some(last_segment) = type_path.path.segments.last() { - // Check if this type is a `String` - // Based on https://stackoverflow.com/questions/66906261/rust-proc-macro-derive-how-do-i-check-if-a-field-is-of-a-primitive-type-like-b - if last_segment.ident == Ident::new("String", last_segment.ident.span()) { +fn easier_string_assignment(field: &Field, field_ty: &Type, field_ident: &Ident) -> (TokenStream2, TokenStream2) { + let has_string_attr = has_attribute(&field.attrs, "string"); + + if let Type::Path(type_path) = field_ty + && let Some(last_segment) = type_path.path.segments.last() + { + // Check for `Option` or `Option` with `#[widget_builder(string)]` + if last_segment.ident == Ident::new("Option", last_segment.ident.span()) + && let PathArguments::AngleBracketed(generic_args) = &last_segment.arguments + { + let inner_is_string = generic_args.args.first().is_some_and(|arg| { + matches!(arg, syn::GenericArgument::Type(Type::Path(inner_path)) + if inner_path.path.segments.last().is_some_and(|seg| seg.ident == Ident::new("String", seg.ident.span()))) + }); + if inner_is_string || has_string_attr { return ( - quote::quote_spanned!(type_path.span()=> impl Into), - quote::quote_spanned!(field_ident.span()=> #field_ident.into()), + quote::quote_spanned!(field_ty.span()=> impl Into), + quote::quote_spanned!(field_ident.span()=> Some(#field_ident.into())), ); } } + + // Check if this type is a `String` + // Based on https://stackoverflow.com/questions/66906261/rust-proc-macro-derive-how-do-i-check-if-a-field-is-of-a-primitive-type-like-b + if last_segment.ident == Ident::new("String", last_segment.ident.span()) || has_string_attr { + return ( + quote::quote_spanned!(field_ty.span()=> impl Into), + quote::quote_spanned!(field_ident.span()=> #field_ident.into()), + ); + } } (quote::quote_spanned!(field_ty.span()=> #field_ty), quote::quote_spanned!(field_ident.span()=> #field_ident)) } @@ -45,21 +63,18 @@ fn find_type_and_assignment(field: &Field) -> syn::Result<(TokenStream2, TokenSt let field_ty = &field.ty; let field_ident = extract_ident(field)?; - let (mut function_input_ty, mut assignment) = easier_string_assignment(field_ty, field_ident); + let (mut function_input_ty, mut assignment) = easier_string_assignment(field, field_ty, field_ident); // Check if type is `WidgetCallback` - if let Type::Path(type_path) = field_ty { - if let Some(last_segment) = type_path.path.segments.last() { - if let PathArguments::AngleBracketed(generic_args) = &last_segment.arguments { - if let Some(first_generic) = generic_args.args.first() { - if last_segment.ident == Ident::new("WidgetCallback", last_segment.ident.span()) { - // Assign builder pattern to assign the closure directly - function_input_ty = quote::quote_spanned!(field_ty.span()=> impl Fn(&#first_generic) -> crate::messages::message::Message + 'static + Send + Sync); - assignment = quote::quote_spanned!(field_ident.span()=> crate::messages::layout::utility_types::layout_widget::WidgetCallback::new(#field_ident)); - } - } - } - } + if let Type::Path(type_path) = field_ty + && let Some(last_segment) = type_path.path.segments.last() + && let PathArguments::AngleBracketed(generic_args) = &last_segment.arguments + && let Some(first_generic) = generic_args.args.first() + && last_segment.ident == Ident::new("WidgetCallback", last_segment.ident.span()) + { + // Assign builder pattern to assign the closure directly + function_input_ty = quote::quote_spanned!(field_ty.span()=> impl Fn(&#first_generic) -> crate::messages::message::Message + 'static + Send + Sync); + assignment = quote::quote_spanned!(field_ident.span()=> crate::messages::layout::utility_types::layout_widget::WidgetCallback::new(#field_ident)); } Ok((function_input_ty, assignment)) } From 385e4dc559145ad2d1504fce5c87b4e843e4ccee Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 7 Mar 2026 01:41:25 -0800 Subject: [PATCH 18/28] Cargo fmt --- .../simple_dialogs/confirm_restart_dialog.rs | 16 +- .../licenses_third_party_dialog.rs | 12 +- .../document/document_message_handler.rs | 410 +++++++++--------- .../node_graph/document_node_definitions.rs | 15 +- .../document/overlays/grid_overlays.rs | 148 +++---- editor/src/messages/tool/utility_types.rs | 26 +- .../nodes/vector/src/generator_nodes.rs | 2 +- 7 files changed, 320 insertions(+), 309 deletions(-) diff --git a/editor/src/messages/dialog/simple_dialogs/confirm_restart_dialog.rs b/editor/src/messages/dialog/simple_dialogs/confirm_restart_dialog.rs index efb77c30fd..5cf55b4874 100644 --- a/editor/src/messages/dialog/simple_dialogs/confirm_restart_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/confirm_restart_dialog.rs @@ -35,21 +35,21 @@ impl LayoutHolder for ConfirmRestartDialog { Layout(vec![ LayoutGroup::row(vec![TextLabel::new("Restart to apply changes?").bold(true).multiline(true).widget_instance()]), LayoutGroup::row(vec![ - TextLabel::new( - format!( - " + TextLabel::new( + format!( + " Settings that only take effect on next launch:\n\ {changed_settings}\n\ \n\ This only takes a few seconds. Open documents,\n\ even unsaved ones, will be automatically restored. " - ) - .trim(), ) - .multiline(true) - .widget_instance(), - ]), + .trim(), + ) + .multiline(true) + .widget_instance(), + ]), ]) } } diff --git a/editor/src/messages/dialog/simple_dialogs/licenses_third_party_dialog.rs b/editor/src/messages/dialog/simple_dialogs/licenses_third_party_dialog.rs index c15d5c8d02..842e185745 100644 --- a/editor/src/messages/dialog/simple_dialogs/licenses_third_party_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/licenses_third_party_dialog.rs @@ -32,11 +32,11 @@ impl LayoutHolder for LicensesThirdPartyDialog { let non_wrapping_column_width = license_text.split('\n').map(|line| line.chars().filter(|&c| c == '_').count() as u32).max().unwrap_or(0) + 2 + 1; Layout(vec![LayoutGroup::row(vec![ - TextLabel::new(license_text) - .monospace(true) - .multiline(true) - .min_width_characters(non_wrapping_column_width) - .widget_instance(), - ])]) + TextLabel::new(license_text) + .monospace(true) + .multiline(true) + .min_width_characters(non_wrapping_column_width) + .widget_instance(), + ])]) } } diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 9006f4b0c3..5631b3f1f8 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2215,219 +2215,219 @@ impl DocumentMessageHandler { LayoutGroup::row(vec![TextLabel::new("Overlays").bold(true).widget_instance()]), LayoutGroup::row(vec![TextLabel::new("General").widget_instance()]), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.artboard_name) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::ArtboardName), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Artboard Name".to_string()).for_checkbox(checkbox_id).widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.artboard_name) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::ArtboardName), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Artboard Name".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.transform_measurement) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::TransformMeasurement), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("G/R/S Measurement".to_string()).for_checkbox(checkbox_id).widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.transform_measurement) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::TransformMeasurement), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("G/R/S Measurement".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), LayoutGroup::row(vec![TextLabel::new("Select Tool").widget_instance()]), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.quick_measurement) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::QuickMeasurement), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Quick Measurement".to_string()).for_checkbox(checkbox_id).widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.quick_measurement) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::QuickMeasurement), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Quick Measurement".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.transform_cage) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::TransformCage), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Transform Cage".to_string()).for_checkbox(checkbox_id).widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.transform_cage) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::TransformCage), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Transform Cage".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.compass_rose) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::CompassRose), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Transform Dial".to_string()).for_checkbox(checkbox_id).widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.compass_rose) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::CompassRose), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Transform Dial".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.pivot) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::Pivot), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Transform Pivot".to_string()).for_checkbox(checkbox_id).widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.pivot) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::Pivot), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Transform Pivot".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.pivot) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::Origin), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Transform Origin".to_string()).for_checkbox(checkbox_id).widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.pivot) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::Origin), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Transform Origin".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.hover_outline) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::HoverOutline), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Hover Outline".to_string()).for_checkbox(checkbox_id).widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.hover_outline) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::HoverOutline), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Hover Outline".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.selection_outline) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::SelectionOutline), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Selection Outline".to_string()).for_checkbox(checkbox_id).widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.selection_outline) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::SelectionOutline), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Selection Outline".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.layer_origin_cross) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::LayerOriginCross), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Layer Origin".to_string()).for_checkbox(checkbox_id).widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.layer_origin_cross) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::LayerOriginCross), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Layer Origin".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), LayoutGroup::row(vec![TextLabel::new("Pen & Path Tools").widget_instance()]), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.path) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::Path), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Path".to_string()).for_checkbox(checkbox_id).widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.path) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::Path), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Path".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.anchors) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::Anchors), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Anchors".to_string()).for_checkbox(checkbox_id).widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.anchors) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::Anchors), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Anchors".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), LayoutGroup::row({ - let checkbox_id = CheckboxId::new(); - vec![ - CheckboxInput::new(self.overlays_visibility_settings.handles) - .disabled(!self.overlays_visibility_settings.anchors) - .on_update(|optional_input: &CheckboxInput| { - DocumentMessage::SetOverlaysVisibility { - visible: optional_input.checked, - overlays_type: Some(OverlaysType::Handles), - } - .into() - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Handles".to_string()) - .disabled(!self.overlays_visibility_settings.anchors) - .for_checkbox(checkbox_id) - .widget_instance(), - ] - }), + let checkbox_id = CheckboxId::new(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.handles) + .disabled(!self.overlays_visibility_settings.anchors) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::Handles), + } + .into() + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Handles".to_string()) + .disabled(!self.overlays_visibility_settings.anchors) + .for_checkbox(checkbox_id) + .widget_instance(), + ] + }), ])) .widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), @@ -2450,7 +2450,8 @@ impl DocumentMessageHandler { LayoutGroup::row(vec![TextLabel::new(SnappingOptions::BoundingBoxes.to_string()).widget_instance()]), ] .into_iter() - .chain(SNAP_FUNCTIONS_FOR_BOUNDING_BOXES.into_iter().map(|(name, closure, description)| LayoutGroup::row({ + .chain(SNAP_FUNCTIONS_FOR_BOUNDING_BOXES.into_iter().map(|(name, closure, description)| { + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(*closure(&mut snapping_state)) @@ -2467,9 +2468,11 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new(name).tooltip_label(name).tooltip_description(description).for_checkbox(checkbox_id).widget_instance(), ] - }))) + }) + })) .chain([LayoutGroup::row(vec![TextLabel::new(SnappingOptions::Paths.to_string()).widget_instance()])]) - .chain(SNAP_FUNCTIONS_FOR_PATHS.into_iter().map(|(name, closure, description)| LayoutGroup::row({ + .chain(SNAP_FUNCTIONS_FOR_PATHS.into_iter().map(|(name, closure, description)| { + LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ CheckboxInput::new(*closure(&mut snapping_state2)) @@ -2486,7 +2489,8 @@ impl DocumentMessageHandler { .widget_instance(), TextLabel::new(name).tooltip_label(name).tooltip_description(description).for_checkbox(checkbox_id).widget_instance(), ] - }))) + }) + })) .collect(), )) .widget_instance(), diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 7889bfa2d3..06c64faa64 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -2192,7 +2192,10 @@ fn static_input_properties() -> InputProperties { true }); - Ok(vec![LayoutGroup::row(node_properties::number_widget(ParameterWidgetsInfo::new(node_id, index, blank_assist, context), number_input))]) + Ok(vec![LayoutGroup::row(node_properties::number_widget( + ParameterWidgetsInfo::new(node_id, index, blank_assist, context), + number_input, + ))]) }), ); map.insert( @@ -2227,7 +2230,11 @@ fn static_input_properties() -> InputProperties { } }; // NOTE: The bool input MUST be at the input index directly before the f64 input! - Ok(vec![LayoutGroup::row(node_properties::optional_f64_widget(ParameterWidgetsInfo::new(node_id, index, false, context), index - 1, number_input))]) + Ok(vec![LayoutGroup::row(node_properties::optional_f64_widget( + ParameterWidgetsInfo::new(node_id, index, false, context), + index - 1, + number_input, + ))]) }), ); map.insert( @@ -2550,9 +2557,7 @@ fn static_input_properties() -> InputProperties { ); map.insert( "text_area".to_string(), - Box::new(|node_id, index, context| { - Ok(vec![LayoutGroup::row(node_properties::text_area_widget(ParameterWidgetsInfo::new(node_id, index, true, context)))]) - }), + Box::new(|node_id, index, context| Ok(vec![LayoutGroup::row(node_properties::text_area_widget(ParameterWidgetsInfo::new(node_id, index, true, context)))])), ); map.insert( "text_font".to_string(), diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 0a9270e066..f3683c24da 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -231,35 +231,35 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { widgets.push(LayoutGroup::row(vec![TextLabel::new("Grid").bold(true).widget_instance()])); widgets.push(LayoutGroup::row(vec![ - TextLabel::new("Type").table_align(true).widget_instance(), - Separator::new(SeparatorStyle::Unrelated).widget_instance(), - RadioInput::new(vec![ - RadioEntryData::new("rectangular").label("Rectangular").on_update(update_val(grid, |grid, _| { - if let GridType::Isometric { y_axis_spacing, angle_a, angle_b } = grid.grid_type { - grid.isometric_y_spacing = y_axis_spacing; - grid.isometric_angle_a = angle_a; - grid.isometric_angle_b = angle_b; - } - grid.grid_type = GridType::Rectangular { spacing: grid.rectangular_spacing }; - })), - RadioEntryData::new("isometric").label("Isometric").on_update(update_val(grid, |grid, _| { - if let GridType::Rectangular { spacing } = grid.grid_type { - grid.rectangular_spacing = spacing; - } - grid.grid_type = GridType::Isometric { - y_axis_spacing: grid.isometric_y_spacing, - angle_a: grid.isometric_angle_a, - angle_b: grid.isometric_angle_b, - }; - })), - ]) - .min_width(200) - .selected_index(Some(match grid.grid_type { - GridType::Rectangular { .. } => 0, - GridType::Isometric { .. } => 1, - })) - .widget_instance(), - ])); + TextLabel::new("Type").table_align(true).widget_instance(), + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + RadioInput::new(vec![ + RadioEntryData::new("rectangular").label("Rectangular").on_update(update_val(grid, |grid, _| { + if let GridType::Isometric { y_axis_spacing, angle_a, angle_b } = grid.grid_type { + grid.isometric_y_spacing = y_axis_spacing; + grid.isometric_angle_a = angle_a; + grid.isometric_angle_b = angle_b; + } + grid.grid_type = GridType::Rectangular { spacing: grid.rectangular_spacing }; + })), + RadioEntryData::new("isometric").label("Isometric").on_update(update_val(grid, |grid, _| { + if let GridType::Rectangular { spacing } = grid.grid_type { + grid.rectangular_spacing = spacing; + } + grid.grid_type = GridType::Isometric { + y_axis_spacing: grid.isometric_y_spacing, + angle_a: grid.isometric_angle_a, + angle_b: grid.isometric_angle_b, + }; + })), + ]) + .min_width(200) + .selected_index(Some(match grid.grid_type { + GridType::Rectangular { .. } => 0, + GridType::Isometric { .. } => 1, + })) + .widget_instance(), + ])); let mut color_widgets = vec![ TextLabel::new("Display").table_align(true).widget_instance(), @@ -287,69 +287,69 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { widgets.push(LayoutGroup::row(color_widgets)); widgets.push(LayoutGroup::row(vec![ - TextLabel::new("Origin").table_align(true).widget_instance(), + TextLabel::new("Origin").table_align(true).widget_instance(), + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + NumberInput::new(Some(grid.origin.x)) + .label("X") + .unit(" px") + .min_width(98) + .on_update(update_origin(grid, |grid| Some(&mut grid.origin.x))) + .widget_instance(), + Separator::new(SeparatorStyle::Related).widget_instance(), + NumberInput::new(Some(grid.origin.y)) + .label("Y") + .unit(" px") + .min_width(98) + .on_update(update_origin(grid, |grid| Some(&mut grid.origin.y))) + .widget_instance(), + ])); + + match grid.grid_type { + GridType::Rectangular { spacing } => widgets.push(LayoutGroup::row(vec![ + TextLabel::new("Spacing").table_align(true).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), - NumberInput::new(Some(grid.origin.x)) + NumberInput::new(Some(spacing.x)) .label("X") .unit(" px") + .min(0.) .min_width(98) - .on_update(update_origin(grid, |grid| Some(&mut grid.origin.x))) + .on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.x))) .widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), - NumberInput::new(Some(grid.origin.y)) + NumberInput::new(Some(spacing.y)) .label("Y") .unit(" px") + .min(0.) .min_width(98) - .on_update(update_origin(grid, |grid| Some(&mut grid.origin.y))) + .on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.y))) .widget_instance(), - ])); - - match grid.grid_type { - GridType::Rectangular { spacing } => widgets.push(LayoutGroup::row(vec![ - TextLabel::new("Spacing").table_align(true).widget_instance(), + ])), + GridType::Isometric { y_axis_spacing, angle_a, angle_b } => { + widgets.push(LayoutGroup::row(vec![ + TextLabel::new("Y Spacing").table_align(true).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), - NumberInput::new(Some(spacing.x)) - .label("X") + NumberInput::new(Some(y_axis_spacing)) .unit(" px") .min(0.) + .min_width(200) + .on_update(update_origin(grid, |grid| grid.grid_type.isometric_y_spacing())) + .widget_instance(), + ])); + widgets.push(LayoutGroup::row(vec![ + TextLabel::new("Angles").table_align(true).widget_instance(), + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + NumberInput::new(Some(angle_a)) + .unit("°") .min_width(98) - .on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.x))) + .on_update(update_origin(grid, |grid| grid.grid_type.angle_a())) .widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), - NumberInput::new(Some(spacing.y)) - .label("Y") - .unit(" px") - .min(0.) + NumberInput::new(Some(angle_b)) + .unit("°") .min_width(98) - .on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.y))) + .on_update(update_origin(grid, |grid| grid.grid_type.angle_b())) .widget_instance(), - ])), - GridType::Isometric { y_axis_spacing, angle_a, angle_b } => { - widgets.push(LayoutGroup::row(vec![ - TextLabel::new("Y Spacing").table_align(true).widget_instance(), - Separator::new(SeparatorStyle::Unrelated).widget_instance(), - NumberInput::new(Some(y_axis_spacing)) - .unit(" px") - .min(0.) - .min_width(200) - .on_update(update_origin(grid, |grid| grid.grid_type.isometric_y_spacing())) - .widget_instance(), - ])); - widgets.push(LayoutGroup::row(vec![ - TextLabel::new("Angles").table_align(true).widget_instance(), - Separator::new(SeparatorStyle::Unrelated).widget_instance(), - NumberInput::new(Some(angle_a)) - .unit("°") - .min_width(98) - .on_update(update_origin(grid, |grid| grid.grid_type.angle_a())) - .widget_instance(), - Separator::new(SeparatorStyle::Related).widget_instance(), - NumberInput::new(Some(angle_b)) - .unit("°") - .min_width(98) - .on_update(update_origin(grid, |grid| grid.grid_type.angle_b())) - .widget_instance(), - ])); + ])); } } diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index 2b633a3c8e..c54f3c1cd3 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -128,19 +128,21 @@ pub struct DocumentToolData { impl DocumentToolData { pub fn update_working_colors(&self, responses: &mut VecDeque) { let layout = Layout(vec![ - LayoutGroup::row(vec![WorkingColorsInput::new(self.primary_color.to_gamma_srgb(), self.secondary_color.to_gamma_srgb()).widget_instance()]), LayoutGroup::row(vec![ - IconButton::new("SwapVertical", 16) - .tooltip_label("Swap Working Colors") - .tooltip_shortcut(action_shortcut!(ToolMessageDiscriminant::SwapColors)) - .on_update(|_| ToolMessage::SwapColors.into()) - .widget_instance(), - IconButton::new("WorkingColors", 16) - .tooltip_label("Reset Working Colors") - .tooltip_shortcut(action_shortcut!(ToolMessageDiscriminant::ResetColors)) - .on_update(|_| ToolMessage::ResetColors.into()) - .widget_instance(), - ]), + WorkingColorsInput::new(self.primary_color.to_gamma_srgb(), self.secondary_color.to_gamma_srgb()).widget_instance(), + ]), + LayoutGroup::row(vec![ + IconButton::new("SwapVertical", 16) + .tooltip_label("Swap Working Colors") + .tooltip_shortcut(action_shortcut!(ToolMessageDiscriminant::SwapColors)) + .on_update(|_| ToolMessage::SwapColors.into()) + .widget_instance(), + IconButton::new("WorkingColors", 16) + .tooltip_label("Reset Working Colors") + .tooltip_shortcut(action_shortcut!(ToolMessageDiscriminant::ResetColors)) + .on_update(|_| ToolMessage::ResetColors.into()) + .widget_instance(), + ]), ]); responses.add(LayoutMessage::SendLayout { diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 5a1297c0ac..41ed3311e6 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -1,6 +1,6 @@ +use core_types::Ctx; use core_types::registry::types::{Angle, PixelSize}; use core_types::table::Table; -use core_types::Ctx; use dyn_any::DynAny; use glam::DVec2; use graphic_types::Vector; From 4647c5f00e756a59c9aa52ad8e6fbff4e93e29a9 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 7 Mar 2026 02:00:57 -0800 Subject: [PATCH 19/28] Fix imports --- editor/src/messages/frontend/utility_types.rs | 3 +-- .../src/messages/layout/utility_types/layout_widget.rs | 9 +++------ .../layout/utility_types/widgets/input_widgets.rs | 3 +-- .../portfolio/document/document_message_handler.rs | 2 +- .../messages/portfolio/document/utility_types/misc.rs | 3 +-- node-graph/libraries/core-types/src/uuid.rs | 3 +-- 6 files changed, 8 insertions(+), 15 deletions(-) diff --git a/editor/src/messages/frontend/utility_types.rs b/editor/src/messages/frontend/utility_types.rs index 60463c4729..979a3d0221 100644 --- a/editor/src/messages/frontend/utility_types.rs +++ b/editor/src/messages/frontend/utility_types.rs @@ -68,8 +68,7 @@ pub enum ExportBounds { Artboard(LayerNodeIdentifier), } -#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[tsify(large_number_types_as_bigints)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))] #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct EyedropperPreviewImage { pub data: serde_bytes::ByteBuf, diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 6b0b3b9b10..ac136311d2 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -10,8 +10,7 @@ use std::hash::{Hash, Hasher}; use std::sync::Arc; #[repr(transparent)] -#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[tsify(large_number_types_as_bigints)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] pub struct WidgetId(pub u64); @@ -352,8 +351,7 @@ pub struct WidgetTable { pub unstyled: bool, } -#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[tsify(large_number_types_as_bigints)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))] #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct WidgetSection { pub name: String, @@ -768,8 +766,7 @@ pub enum Widget { } /// A single change to part of the UI, containing the location of the change and the new value. -#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[tsify(large_number_types_as_bigints)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct WidgetDiff { /// A path to the change diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index 1cdb65ef95..a1062f9cde 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -100,8 +100,7 @@ pub struct DropdownInput { pub type MenuListEntrySections = Vec>; -#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[tsify(large_number_types_as_bigints)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))] #[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)] #[derivative(Debug, PartialEq)] #[widget_builder(not_widget_instance)] diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 5631b3f1f8..0f2739789a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2314,7 +2314,7 @@ impl DocumentMessageHandler { LayoutGroup::row({ let checkbox_id = CheckboxId::new(); vec![ - CheckboxInput::new(self.overlays_visibility_settings.pivot) + CheckboxInput::new(self.overlays_visibility_settings.origin) .on_update(|optional_input: &CheckboxInput| { DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked, diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 3daca8bef4..a41e0ca957 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -3,8 +3,7 @@ use glam::DVec2; use std::fmt; #[repr(transparent)] -#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[tsify(large_number_types_as_bigints)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] pub struct DocumentId(pub u64); diff --git a/node-graph/libraries/core-types/src/uuid.rs b/node-graph/libraries/core-types/src/uuid.rs index 0f8e54aed1..9ddab56d4c 100644 --- a/node-graph/libraries/core-types/src/uuid.rs +++ b/node-graph/libraries/core-types/src/uuid.rs @@ -67,8 +67,7 @@ mod uuid_generation { } #[repr(transparent)] -#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[tsify(large_number_types_as_bigints)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, DynAny)] pub struct NodeId(pub u64); From 10ef0e1e65c084e25c4c484264b62f8e8f0af2e6 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 7 Mar 2026 02:40:29 -0800 Subject: [PATCH 20/28] Update outdated readme info --- frontend/README.md | 29 ++++++++++--------- frontend/src/App.svelte | 2 +- frontend/src/README.md | 24 +++++++-------- frontend/src/components/Editor.svelte | 2 +- frontend/src/components/README.md | 8 +++-- .../src/components/widgets/WidgetSpan.svelte | 2 +- frontend/wasm/README.md | 19 +++++++----- node-graph/README.md | 2 +- ...buted-computing-in-the-graphene-runtime.md | 2 +- 9 files changed, 49 insertions(+), 41 deletions(-) diff --git a/frontend/README.md b/frontend/README.md index 3a75c83380..b1ee9ab878 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -4,11 +4,7 @@ The Graphite frontend is a web app that provides the presentation for the editor ## Bundled assets: `assets/` -Icons and images that are used in components and embedded into the application bundle by the build system. - -## Public assets: `public/` - -Static content like favicons that are copied directly into the root of the build output by the build system. +Images that are used in components and embedded into the application bundle by the build system. ## Svelte/TypeScript source: `src/` @@ -18,22 +14,27 @@ Source code for the web app in the form of Svelte components and [TypeScript](ht Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for the web app to use as an entry point, unburdened by Rust's complex data types that are incompatible with JS data types. Bindings (JS functions that call into the Wasm module) are provided by [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/) in concert with [wasm-pack](https://github.com/rustwasm/wasm-pack). -## ESLint configurations: `.eslintrc.cjs` +## ESLint configuration: `eslint.config.js` [ESLint](https://eslint.org/) is the tool which enforces style rules on the JS, TS, and Svelte files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when (in VS Code) the file is saved or `npm run lint` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Svelte support and [Prettier](https://prettier.io/)'s role as a code formatter. -## npm ecosystem packages: `package.json` - -While we don't use Node.js as a JS-based server, we do rely on its ecosystem of packages for our build system toolchain. If you're just getting started, make sure to install the latest LTS copy of [Node.js](https://nodejs.org/en/download). Our project's philosophy on third-party packages is to keep our dependency tree as light as possible, so adding anything new to our `package.json` should have overwhelming justification. Most of the packages are just development tooling (TypeScript, Vite, ESLint, Prettier, and [Sass](https://sass-lang.com/)) that run in your terminal during the build process. +## Svelte configuration: `svelte.config.js` -## npm package installed versions: `package-lock.json` - -Specifies the exact versions of packages installed in the npm dependency tree. While `package.json` specifies which packages to install and their minimum/maximum acceptable version numbers, `package-lock.json` represents the exact versions of each dependency and sub-dependency. Running `npm ci` will grab these exact versions to ensure you are using the same packages as everyone else working on Graphite. `npm update` will modify `package-lock.json` to specify newer versions of any updated (sub-)dependencies and download those, as long as they don't exceed the maximum version allowed in `package.json`. To check for newer versions that exceed the max version, run `npm outdated` to see a list. Unless you know why you are doing it, try to avoid committing updates to `package-lock.json` by mistake if your code changes don't pertain to package updates. And never manually modify the file. +Configures the Svelte compiler, including the preprocessor setup for SCSS and TypeScript support, and compiler warning filters. -## TypeScript configurations: `tsconfig.json` +## TypeScript configuration: `tsconfig.json` Basic configuration options for the TypeScript build tool to do its job in our repository. -## Vite configurations: `vite.config.ts` +## Vite configuration: `vite.config.ts` We use the [Vite](https://vitejs.dev/) bundler/build system. This file is where we configure Vite to set up plugins (like the third-party license checker/generator). Part of the license checker plugin setup includes some functions to format web package licenses, as well as Rust package licenses provided by [cargo-about](https://github.com/EmbarkStudios/cargo-about), into a text file that's distributed with the application to provide license notices for third-party code. + +## npm ecosystem packages: `package.json` + +While we don't use Node.js as a JS-based server, we do rely on its ecosystem of packages for our build system toolchain. Our project's philosophy on third-party packages is to keep our dependency tree as light as possible, so adding anything new to our `package.json` should have overwhelming justification. Most of the packages are just development tooling (TypeScript, Vite, ESLint, Prettier, Sass, etc.) that run in your terminal during the build process. + +## npm package installed versions: `package-lock.json` + +Specifies the exact versions of packages installed in the npm dependency tree. While `package.json` specifies which packages to install and their minimum/maximum acceptable version numbers, `package-lock.json` represents the exact versions of each dependency and sub-dependency. Running `npm ci` will grab these exact versions to ensure you are using the same packages as everyone else working on Graphite. `npm update` will modify `package-lock.json` to specify newer versions of any updated (sub-)dependencies and download those, as long as they don't exceed the maximum version allowed in `package.json`. To check for newer versions that exceed the max version, run `npm outdated` to see a list. Unless you know why you are doing it, try to avoid committing updates to `package-lock.json` by mistake if your code changes don't pertain to package updates. And never manually modify the file. + diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 8e103c5464..6e98a39bc5 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -15,7 +15,7 @@ }); onDestroy(() => { - // Destroy the WASM editor handle + // Destroy the Wasm editor handle editor?.handle.free(); }); diff --git a/frontend/src/README.md b/frontend/src/README.md index 6206153ed5..41d46098b6 100644 --- a/frontend/src/README.md +++ b/frontend/src/README.md @@ -2,7 +2,7 @@ ## Svelte components: `components/` -Svelte components that build the Graphite editor GUI, which are mounted in `App.svelte`. These each contain a Svelte-templated HTML section, an SCSS (Stylus CSS) section, and a script section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur. +Svelte components that build the Graphite editor GUI. These each contain a a TypeScript section, a Svelte-templated HTML template section, and an SCSS stylesheet section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur. ## I/O managers: `io-managers/` @@ -18,27 +18,23 @@ TypeScript files which provide reactive state and importable functions to Svelte In `Editor.svelte`, an instance of each of these are given to Svelte's `setContext()` function. This allows any component to access the state provider instance using `const exampleStateProvider = getContext("exampleStateProvider");`. -## _I/O managers vs. state providers_ +## *I/O managers vs. state providers* -_Some state providers, similarly to I/O managers, may subscribe to backend events, call functions from `editor_api.rs` into the backend, and interact with browser APIs and user input. The difference is that state providers are meant to be made available to components via `getContext()` to use them for reactive state, while I/O managers are meant to be self-contained systems that operate for the lifetime of the application and aren't touched by Svelte components._ +*Some state providers, similarly to I/O managers, may subscribe to backend events, call functions from `editor_api.rs` into the backend, and interact with browser APIs and user input. The difference is that state providers are meant to be made available to components via `getContext()` to use them for reactive state, while I/O managers are meant to be self-contained systems that operate for the lifetime of the application and aren't touched by Svelte components.* ## Utility functions: `utility-functions/` TypeScript files which define and `export` individual helper functions for use elsewhere in the codebase. These files should not persist state outside each function. -## WASM editor: `editor.ts` +## Wasm editor: `editor.ts` -Instantiates the WASM and editor backend instances. The function `initWasm()` asynchronously constructs and initializes an instance of the WASM bindings JS module provided by wasm-bindgen/wasm-pack. The function `createEditor()` constructs an instance of the editor backend. In theory there could be multiple editor instances sharing the same WASM module instance. The function returns an object where `raw` is the WASM module, `instance` is the editor, and `subscriptions` is the subscription router (described below). +Instantiates the Wasm and editor backend instances. The function `initWasm()` asynchronously constructs and initializes an instance of the Wasm bindings JS module provided by wasm-bindgen/wasm-pack. The function `createEditor()` constructs an instance of the editor backend. In theory there could be multiple editor instances sharing the same Wasm module instance. The function returns an object where `raw` is the Wasm memory, `handle` provides access to callable backend functions, and `subscriptions` is the subscription router (described below). -`initWasm()` occurs in `main.ts` right before the Svelte application exists, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the state providers described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.raw`, `editor.handle`, or `editor.subscriptions`. - -## Message definitions: `messages.ts` - -Defines the message formats and data types received from the backend. Since Rust and JS support different styles of data representation, this bridges the gap from Rust into JS land. Messages (and the data contained within) are serialized in Rust by `serde` into JSON, and these definitions are manually kept up-to-date to parallel the message structs and their data types. (However, directives like `#[serde(skip)]` or `#[serde(rename = "someOtherName")]` may cause the TypeScript format to look slightly different from the Rust structs.) These definitions are basically just for the sake of TypeScript to understand the format, although in some cases we may perform data conversion here using translation functions that we can provide. +`initWasm()` occurs in `main.ts` right before the Svelte application is mounted, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the state providers described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.handle` or `editor.subscriptions`. ## Subscription router: `subscription-router.ts` -Associates messages from the backend with subscribers in the frontend, and routes messages to subscriber callbacks. This module provides a `subscribeFrontendMessage(messageType, callback)` function which JS code throughout the frontend can call to be registered as the exclusive handler for a chosen message type. This file's other exported function, `handleFrontendMessage(messageType, messageData, wasm, instance)`, is called in `editor.ts` by the associated editor instance when the backend sends a `FrontendMessage`. When this occurs, the subscription router delivers the message to the subscriber for given `messageType` by executing its registered `callback` function. As an argument to the function, it provides the `messageData` payload transformed into its TypeScript-friendly format defined in `messages.ts`. +Associates messages from the backend with subscribers in the frontend, and routes messages to subscriber callbacks. This module provides a `subscribeFrontendMessage(messageType, callback)` function which JS code throughout the frontend can call to be registered as the exclusive handler for a chosen message type. The router's other function, `handleFrontendMessage(messageType, messageData)`, is called via the callback passed to `EditorHandle.create()` in `editor.ts` when the backend sends a `FrontendMessage`. When this occurs, the subscription router delivers the message to the subscriber by executing its registered `callback` function. ## Svelte app entry point: `App.svelte` @@ -48,6 +44,10 @@ The entry point for the Svelte application. This is where we define global CSS style rules, create/destroy the editor instance, construct/destruct the I/O managers, and construct and `setContext()` the state providers. +## Global type augmentations: `global.d.ts` + +Extends built-in browser type definitions using TypeScript's interface merging. This includes Graphite's custom properties on the `window` object, custom events like `pointerlockmove`, and experimental browser APIs not yet in TypeScript's standard library. New custom events or non-standard browser APIs used by the frontend should be declared here. + ## JS bundle entry point: `main.ts` -The entry point for the entire project's code bundle. Here we simply initialize the Svelte application with `export default new App({ target: document.body });`. +The entry point for the entire project's code bundle. Here we simply mount the Svelte application with `export default mount(App, { target: document.body });`. diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index 307f215ac1..0f14408298 100644 --- a/frontend/src/components/Editor.svelte +++ b/frontend/src/components/Editor.svelte @@ -19,7 +19,7 @@ import MainWindow from "@graphite/components/window/MainWindow.svelte"; - // Graphite WASM editor + // Graphite Wasm editor export let editor: Editor; setContext("editor", editor); diff --git a/frontend/src/components/README.md b/frontend/src/components/README.md index bcf02e639d..6669aaba17 100644 --- a/frontend/src/components/README.md +++ b/frontend/src/components/README.md @@ -1,6 +1,6 @@ # Overview of `/frontend/src/components/` -Each component represents a (usually reusable) part of the Graphite editor GUI. These all get mounted in `Editor.svelte` (in the `/src` directory above this one). +Each component represents a (usually reusable) part of the Graphite editor GUI. ## Floating Menus: `floating-menus/` @@ -12,7 +12,11 @@ Useful containers that control the flow of content held within. ## Panels: `panels/` -The dockable tabbed regions like the Document, Properties, Layers, and Node Graph panels. +The dockable tabbed regions like the Document, Properties, Layers, Data, and Welcome panels. + +## Views: `views/` + +Content views rendered within panels, such as the node graph. ## Widgets: `widgets/` diff --git a/frontend/src/components/widgets/WidgetSpan.svelte b/frontend/src/components/widgets/WidgetSpan.svelte index b293efe290..6a402b067e 100644 --- a/frontend/src/components/widgets/WidgetSpan.svelte +++ b/frontend/src/components/widgets/WidgetSpan.svelte @@ -31,7 +31,7 @@ // Extract the discriminant key names from the Widget tagged enum union (e.g. "TextButton" | "CheckboxInput" | ...) type WidgetKind = Widget extends infer T ? (T extends Record ? K & string : never) : never; - // Extract the props type for a specific widget kind (e.g. WidgetProps<"TextButton"> gives the WASM-generated TextButton interface) + // Extract the props type for a specific widget kind (e.g. WidgetProps<"TextButton"> gives the Wasm-generated TextButton interface) type WidgetProps = Extract>[K]; // A Widget tagged enum unwrapped into a correlated [kind, props] tuple type UnwrappedWidget = { [K in WidgetKind]: [kind: K, props: WidgetProps] }[WidgetKind]; diff --git a/frontend/wasm/README.md b/frontend/wasm/README.md index 539ff0d1b5..0bdd28bab6 100644 --- a/frontend/wasm/README.md +++ b/frontend/wasm/README.md @@ -1,14 +1,17 @@ # Overview of `/frontend/wasm/` -## WASM wrapper API: `src/editor_api.rs` -Provides bindings for JS to call functions defined in this file, and for `FrontendMessage`s to be sent from Rust back to JS in the form of a callback to the subscription router. This WASM wrapper crate, since it's written in Rust, is able to call into the Editor crate's codebase and send `FrontendMessage`s back to JS. +## Wasm wrapper API: `src/editor_api.rs` +Provides bindings for JS to call functions defined in this file, and for `FrontendMessage`s to be sent from Rust back to JS in the form of a callback to the subscription router. This Wasm wrapper crate, since it's written in Rust, is able to call into the Editor crate's codebase and send `FrontendMessage`s back to JS. -## WASM wrapper helper code: `src/helpers.rs` -Assorted function and struct definitions used in the WASM wrapper. +## Wasm wrapper helper code: `src/helpers.rs` -## WASM wrapper initialization: `src/lib.rs` -Entry point for the Rust entire codebase in the WASM environment. Initializes the WASM module and persistent storage for editor and WASM wrapper instances. +Assorted function and struct definitions used in the Wasm wrapper. -## WASM wrapper tests: `tests/` -We currently have no WASM wrapper tests, but this is where they would go. +## Native communication: `src/native_communcation.rs` + +Handles receiving serialized `FrontendMessage`s from the native desktop app via an `ArrayBuffer` and forwarding them to JS through the editor handle. + +## Wasm wrapper initialization: `src/lib.rs` + +Entry point for the Rust codebase in the Wasm environment. Sets up panic hooks and logging, and defines thread-local storage for the editor instance, editor handle, message buffer, and panic dialog callback. diff --git a/node-graph/README.md b/node-graph/README.md index cf2c1e9bc1..985929a395 100644 --- a/node-graph/README.md +++ b/node-graph/README.md @@ -142,7 +142,7 @@ The definition for the constructor of a node that applies the opacity transforma }) }, // Defines the call argument, return value, and inputs. - NodeIOTypes::new(concrete!(Image), concrete!(Image), vec![fn_type!((), f64))]), + NodeIOTypes::new(concrete!(Image), concrete!(Image), vec![fn_type!((), f64)]), ), ``` diff --git a/website/content/blog/2022-05-12-distributed-computing-in-the-graphene-runtime.md b/website/content/blog/2022-05-12-distributed-computing-in-the-graphene-runtime.md index 9cb873c3af..32197a3ce7 100644 --- a/website/content/blog/2022-05-12-distributed-computing-in-the-graphene-runtime.md +++ b/website/content/blog/2022-05-12-distributed-computing-in-the-graphene-runtime.md @@ -39,7 +39,7 @@ Nodes are implemented by us as part of a built-in library, and by some users who ### Sandboxing -For security and portability, user-authored nodes are compiled into WebAssembly (WASM) modules and run in a sandbox. Built-in nodes provided with Graphite run natively to avoid the nominal performance penalty of the sandbox. When the entire editor is running in a web browser, all nodes use the browser's WASM executor. When running in a distributed compute cluster on cloud machines, the infrastructure provider may be able to offer sandboxing to sufficiently address the security concerns of running untrusted code natively. +For security and portability, user-authored nodes are compiled into WebAssembly (Wasm) modules and run in a sandbox. Built-in nodes provided with Graphite run natively to avoid the nominal performance penalty of the sandbox. When the entire editor is running in a web browser, all nodes use the browser's Wasm executor. When running in a distributed compute cluster on cloud machines, the infrastructure provider may be able to offer sandboxing to sufficiently address the security concerns of running untrusted code natively. ## The Graphene distributed runtime From b5b357c33e78b036ec2c33da523153aae554d51b Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 7 Mar 2026 02:55:39 -0800 Subject: [PATCH 21/28] Fix lint command rename references --- .github/workflows/ci.yml | 2 +- .github/workflows/website.yml | 2 +- editor/src/node_graph_executor/runtime.rs | 4 ++-- frontend/README.md | 2 +- website/content/volunteer/guide/project-setup/_index.md | 4 ++-- website/package.json | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4985abfe20..51a8feed24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,7 +117,7 @@ jobs: NODE_ENV: production run: | cd frontend - npm run lint + npm run check # Run the Rust tests on the self-hosted native runner test: diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 3b8fb54eeb..e4bb6d1a8e 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -94,7 +94,7 @@ jobs: run: | cd website npm ci - npm run lint + npm run check zola --config config.toml build --minify - name: 📤 Publish to Cloudflare Pages diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 60b04c13e3..4c7b196d72 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -56,7 +56,7 @@ pub struct NodeRuntime { thumbnail_renders: HashMap>, vector_modify: HashMap, - /// Cached surface for WASM viewport rendering (reused across frames) + /// Cached surface for Wasm viewport rendering (reused across frames) #[cfg(all(target_family = "wasm", feature = "gpu"))] wasm_viewport_surface: Option, } @@ -309,7 +309,7 @@ impl NodeRuntime { data: RenderOutputType::Texture(image_texture), metadata, })) if !render_config.for_export => { - // On WASM, for viewport rendering, blit the texture to a surface and return a CanvasFrame + // On Wasm, for viewport rendering, blit the texture to a surface and return a CanvasFrame let app_io = self.editor_api.application_io.as_ref().unwrap(); let executor = app_io.gpu_executor().expect("GPU executor should be available when we receive a texture"); diff --git a/frontend/README.md b/frontend/README.md index b1ee9ab878..7d7b6f4c97 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -16,7 +16,7 @@ Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for ## ESLint configuration: `eslint.config.js` -[ESLint](https://eslint.org/) is the tool which enforces style rules on the JS, TS, and Svelte files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when (in VS Code) the file is saved or `npm run lint` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Svelte support and [Prettier](https://prettier.io/)'s role as a code formatter. +When you use `npm run check`, it [ESLint](https://eslint.org/) checks the code in the frontend project for code quality. (The command also reports TS and Svelte errors.) The tool enforces style rules on the JS, TS, and Svelte (including its HTML and SCSS) files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when the file is saved in VS Code, or manually when `npm run fix` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Svelte support and [Prettier](https://prettier.io/)'s role as a code formatter. ## Svelte configuration: `svelte.config.js` diff --git a/website/content/volunteer/guide/project-setup/_index.md b/website/content/volunteer/guide/project-setup/_index.md index 6c16070a31..b0bf40a34c 100644 --- a/website/content/volunteer/guide/project-setup/_index.md +++ b/website/content/volunteer/guide/project-setup/_index.md @@ -53,6 +53,6 @@ We provide default configurations for VS Code users. When you open the project, ### Checking, linting, and formatting -While developing Rust code, `cargo check`, `cargo clippy`, and `cargo fmt` terminal commands may be run from the root directory. For web code, formatting issues can be linted using `npm run lint` (to view) and `npm run lint-fix` (to fix) if run from the `/frontend` directory. +While developing Rust code: `cargo check`, `cargo clippy`, and `cargo fmt` terminal commands may be run from the root directory. For web code: errors, code quality lints, and formatting issues can be checked using `npm run check` (to view them) and `npm run fix` (to fix them) if run from the `/frontend` directory. -If you don't use VS Code and its format-on-save feature, please remember to format before committing or [set up a `pre-commit` hook](https://githooks.com/) to do that automatically. Disabling VS Code's *Auto Save* files feature is recommended to ensure you actually save (and thus format) file changes. +If you don't use VS Code and its format-on-save feature, please remember to format before committing or [set up a `pre-commit` hook](https://githooks.com/) to do that automatically. Disabling VS Code's *Auto Save* files feature is recommended to ensure you actually save (and thus format) file changes. CI will enforce that everything passes these checks before your PR can be merged. diff --git a/website/package.json b/website/package.json index 88901b0877..6993956a16 100644 --- a/website/package.json +++ b/website/package.json @@ -13,8 +13,8 @@ "scripts": { "postinstall": "node .build-scripts/install.ts", "generate-editor-structure": "node .build-scripts/generate-editor-structure.ts generated/hierarchical_message_system_tree.txt generated/hierarchical_message_system_tree.html", - "lint": "eslint . && tsc --noEmit", - "lint-fix": "eslint . --fix && tsc --noEmit" + "check": "tsc --noEmit && eslint", + "fix": "eslint --fix" }, "devDependencies": { "@eslint/compat": "^2.0.1", From 55df87cd40eb10201ae0823d1b120d17978b1fc4 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 7 Mar 2026 02:57:45 -0800 Subject: [PATCH 22/28] Fix typos --- frontend/src/README.md | 2 +- frontend/wasm/README.md | 2 +- frontend/wasm/src/editor_api.rs | 4 ++-- frontend/wasm/src/lib.rs | 2 +- .../src/{native_communcation.rs => native_communication.rs} | 0 5 files changed, 5 insertions(+), 5 deletions(-) rename frontend/wasm/src/{native_communcation.rs => native_communication.rs} (100%) diff --git a/frontend/src/README.md b/frontend/src/README.md index 41d46098b6..7195b05372 100644 --- a/frontend/src/README.md +++ b/frontend/src/README.md @@ -2,7 +2,7 @@ ## Svelte components: `components/` -Svelte components that build the Graphite editor GUI. These each contain a a TypeScript section, a Svelte-templated HTML template section, and an SCSS stylesheet section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur. +Svelte components that build the Graphite editor GUI. These each contain a TypeScript section, a Svelte-templated HTML template section, and an SCSS stylesheet section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur. ## I/O managers: `io-managers/` diff --git a/frontend/wasm/README.md b/frontend/wasm/README.md index 0bdd28bab6..820759e633 100644 --- a/frontend/wasm/README.md +++ b/frontend/wasm/README.md @@ -8,7 +8,7 @@ Provides bindings for JS to call functions defined in this file, and for `Fronte Assorted function and struct definitions used in the Wasm wrapper. -## Native communication: `src/native_communcation.rs` +## Native communication: `src/native_communication.rs` Handles receiving serialized `FrontendMessage`s from the native desktop app via an `ArrayBuffer` and forwarding them to JS through the editor handle. diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index a095396663..e399e4c2d3 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -154,7 +154,7 @@ impl EditorHandle { log::error!("Failed to serialize message"); return; }; - crate::native_communcation::send_message_to_cef(serialized_message) + crate::native_communication::send_message_to_cef(serialized_message) } // Sends a FrontendMessage to JavaScript @@ -190,7 +190,7 @@ impl EditorHandle { #[wasm_bindgen(js_name = initAfterFrontendReady)] pub fn init_after_frontend_ready(&self) { #[cfg(feature = "native")] - crate::native_communcation::initialize_native_communication(); + crate::native_communication::initialize_native_communication(); self.dispatch(PortfolioMessage::Init); diff --git a/frontend/wasm/src/lib.rs b/frontend/wasm/src/lib.rs index d9e9ab2ea0..34c591fded 100644 --- a/frontend/wasm/src/lib.rs +++ b/frontend/wasm/src/lib.rs @@ -6,7 +6,7 @@ extern crate log; pub mod editor_api; pub mod helpers; -pub mod native_communcation; +pub mod native_communication; use editor::messages::prelude::*; use std::panic; diff --git a/frontend/wasm/src/native_communcation.rs b/frontend/wasm/src/native_communication.rs similarity index 100% rename from frontend/wasm/src/native_communcation.rs rename to frontend/wasm/src/native_communication.rs From bb88d9fadb092f3ba9677cbf4d29d3be8e2ab8cf Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 7 Mar 2026 02:59:23 -0800 Subject: [PATCH 23/28] One more typos fix --- frontend/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/README.md b/frontend/README.md index 7d7b6f4c97..c285691ff4 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -16,7 +16,7 @@ Wraps the editor backend codebase (`/editor`) and provides a JS-centric API for ## ESLint configuration: `eslint.config.js` -When you use `npm run check`, it [ESLint](https://eslint.org/) checks the code in the frontend project for code quality. (The command also reports TS and Svelte errors.) The tool enforces style rules on the JS, TS, and Svelte (including its HTML and SCSS) files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when the file is saved in VS Code, or manually when `npm run fix` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Svelte support and [Prettier](https://prettier.io/)'s role as a code formatter. +When you use `npm run check`, [ESLint](https://eslint.org/) checks the code in the frontend project for code quality. (The command also reports TS and Svelte errors.) The tool enforces style rules on the JS, TS, and Svelte (including its HTML and SCSS) files in our frontend codebase. As it is set up in this config file, ESLint will complain about bad practices and often help reformat code automatically when the file is saved in VS Code, or manually when `npm run fix` is executed. (If you don't use VS Code, remember to run this command before committing!) This config file for ESLint sets our style preferences and configures our usage of extensions/plugins for Svelte support and [Prettier](https://prettier.io/)'s role as a code formatter. ## Svelte configuration: `svelte.config.js` From a967c4915a1e8d447543c70dd0cd784ef8015b5d Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 7 Mar 2026 03:57:41 -0800 Subject: [PATCH 24/28] Remove unnecessary dep: prefix from the edited Cargo.toml files --- node-graph/graph-craft/Cargo.toml | 4 ++-- node-graph/libraries/no-std-types/Cargo.toml | 2 +- node-graph/libraries/raster-types/Cargo.toml | 2 +- node-graph/libraries/vector-types/Cargo.toml | 2 +- node-graph/nodes/gcore/Cargo.toml | 4 ++-- node-graph/nodes/path-bool/Cargo.toml | 2 +- node-graph/nodes/raster/Cargo.toml | 4 ++-- node-graph/nodes/text/Cargo.toml | 2 +- node-graph/nodes/vector/Cargo.toml | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml index db25bf7756..492c4997be 100644 --- a/node-graph/graph-craft/Cargo.toml +++ b/node-graph/graph-craft/Cargo.toml @@ -15,8 +15,8 @@ wasm = [ "core-types/wasm", "graphic-types/wasm", "text-nodes/wasm", - "dep:tsify", - "dep:wasm-bindgen", + "tsify", + "wasm-bindgen", ] [dependencies] diff --git a/node-graph/libraries/no-std-types/Cargo.toml b/node-graph/libraries/no-std-types/Cargo.toml index c7df6bf757..10d8bf67e1 100644 --- a/node-graph/libraries/no-std-types/Cargo.toml +++ b/node-graph/libraries/no-std-types/Cargo.toml @@ -24,7 +24,7 @@ std = [ "num-traits/std", "num_enum/std", ] -wasm = ["dep:tsify", "dep:wasm-bindgen"] +wasm = ["tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/libraries/raster-types/Cargo.toml b/node-graph/libraries/raster-types/Cargo.toml index e8efb64d1d..0039481667 100644 --- a/node-graph/libraries/raster-types/Cargo.toml +++ b/node-graph/libraries/raster-types/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] wgpu = ["dep:wgpu"] -wasm = ["core-types/wasm", "dep:tsify", "dep:wasm-bindgen"] +wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/libraries/vector-types/Cargo.toml b/node-graph/libraries/vector-types/Cargo.toml index 55de1324aa..5212fb2386 100644 --- a/node-graph/libraries/vector-types/Cargo.toml +++ b/node-graph/libraries/vector-types/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = ["core-types/wasm", "dep:tsify", "dep:wasm-bindgen"] +wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/nodes/gcore/Cargo.toml b/node-graph/nodes/gcore/Cargo.toml index 3d013e302d..9d2fbabb03 100644 --- a/node-graph/nodes/gcore/Cargo.toml +++ b/node-graph/nodes/gcore/Cargo.toml @@ -12,8 +12,8 @@ wasm = [ "core-types/wasm", "raster-types/wasm", "graphic-types/wasm", - "dep:tsify", - "dep:wasm-bindgen", + "tsify", + "wasm-bindgen", ] [dependencies] diff --git a/node-graph/nodes/path-bool/Cargo.toml b/node-graph/nodes/path-bool/Cargo.toml index 0d8b31f9c7..d755adcb75 100644 --- a/node-graph/nodes/path-bool/Cargo.toml +++ b/node-graph/nodes/path-bool/Cargo.toml @@ -7,7 +7,7 @@ authors = ["Graphite Authors "] license = "MIT OR Apache-2.0" [features] -wasm = ["core-types/wasm", "dep:tsify", "dep:wasm-bindgen"] +wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/nodes/raster/Cargo.toml b/node-graph/nodes/raster/Cargo.toml index d079da09e2..9fad331852 100644 --- a/node-graph/nodes/raster/Cargo.toml +++ b/node-graph/nodes/raster/Cargo.toml @@ -29,8 +29,8 @@ wasm = [ "core-types/wasm", "raster-types/wasm", "vector-types/wasm", - "dep:tsify", - "dep:wasm-bindgen", + "tsify", + "wasm-bindgen", ] [dependencies] diff --git a/node-graph/nodes/text/Cargo.toml b/node-graph/nodes/text/Cargo.toml index 3e542ced88..4537425a80 100644 --- a/node-graph/nodes/text/Cargo.toml +++ b/node-graph/nodes/text/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = ["core-types/wasm", "dep:tsify", "dep:wasm-bindgen"] +wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/nodes/vector/Cargo.toml b/node-graph/nodes/vector/Cargo.toml index 0057e0db9c..bd976b84e7 100644 --- a/node-graph/nodes/vector/Cargo.toml +++ b/node-graph/nodes/vector/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = ["core-types/wasm", "dep:tsify", "dep:wasm-bindgen"] +wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies From d0af724b625c913b853816902df29fa30525c3c4 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 7 Mar 2026 04:26:12 -0800 Subject: [PATCH 25/28] Remove excess parts from Cargo.toml --- node-graph/graph-craft/Cargo.toml | 8 +------- node-graph/libraries/core-types/Cargo.toml | 2 +- node-graph/libraries/graphic-types/Cargo.toml | 7 +------ node-graph/libraries/raster-types/Cargo.toml | 2 +- node-graph/libraries/vector-types/Cargo.toml | 2 +- node-graph/nodes/gcore/Cargo.toml | 8 +------- node-graph/nodes/gstd/Cargo.toml | 8 -------- node-graph/nodes/path-bool/Cargo.toml | 2 +- node-graph/nodes/raster/Cargo.toml | 8 +------- node-graph/nodes/text/Cargo.toml | 2 +- node-graph/nodes/vector/Cargo.toml | 2 +- 11 files changed, 10 insertions(+), 41 deletions(-) diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml index 492c4997be..14b7f98023 100644 --- a/node-graph/graph-craft/Cargo.toml +++ b/node-graph/graph-craft/Cargo.toml @@ -11,13 +11,7 @@ dealloc_nodes = ["core-types/dealloc_nodes"] wgpu = ["wgpu-executor"] tokio = ["dep:tokio"] loading = ["serde_json"] -wasm = [ - "core-types/wasm", - "graphic-types/wasm", - "text-nodes/wasm", - "tsify", - "wasm-bindgen", -] +wasm = ["tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/libraries/core-types/Cargo.toml b/node-graph/libraries/core-types/Cargo.toml index d6bc01c4db..f159805fe8 100644 --- a/node-graph/libraries/core-types/Cargo.toml +++ b/node-graph/libraries/core-types/Cargo.toml @@ -11,7 +11,7 @@ default = ["serde"] nightly = [] type_id_logging = [] dealloc_nodes = [] -wasm = ["tsify", "wasm-bindgen", "no-std-types/wasm"] +wasm = ["tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/libraries/graphic-types/Cargo.toml b/node-graph/libraries/graphic-types/Cargo.toml index 1dac611f07..272d49b08a 100644 --- a/node-graph/libraries/graphic-types/Cargo.toml +++ b/node-graph/libraries/graphic-types/Cargo.toml @@ -8,12 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = [ - "core-types/wasm", - "vector-types/wasm", - "raster-types/wasm", - "wasm-bindgen", -] +wasm = ["wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/libraries/raster-types/Cargo.toml b/node-graph/libraries/raster-types/Cargo.toml index 0039481667..cd3928a50b 100644 --- a/node-graph/libraries/raster-types/Cargo.toml +++ b/node-graph/libraries/raster-types/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] wgpu = ["dep:wgpu"] -wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] +wasm = ["tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/libraries/vector-types/Cargo.toml b/node-graph/libraries/vector-types/Cargo.toml index 5212fb2386..a8c9d97dff 100644 --- a/node-graph/libraries/vector-types/Cargo.toml +++ b/node-graph/libraries/vector-types/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] +wasm = ["tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/nodes/gcore/Cargo.toml b/node-graph/nodes/gcore/Cargo.toml index 9d2fbabb03..6da0fb6f3e 100644 --- a/node-graph/nodes/gcore/Cargo.toml +++ b/node-graph/nodes/gcore/Cargo.toml @@ -8,13 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = [ - "core-types/wasm", - "raster-types/wasm", - "graphic-types/wasm", - "tsify", - "wasm-bindgen", -] +wasm = ["tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/nodes/gstd/Cargo.toml b/node-graph/nodes/gstd/Cargo.toml index ba1d2d62f9..012c8417e3 100644 --- a/node-graph/nodes/gstd/Cargo.toml +++ b/node-graph/nodes/gstd/Cargo.toml @@ -16,14 +16,6 @@ wasm = [ "web-sys", "graphene-application-io/wasm", "image/png", - "core-types/wasm", - "vector-types/wasm", - "graphic-types/wasm", - "text-nodes/wasm", - "raster-nodes/wasm", - "vector-nodes/wasm", - "graphene-core/wasm", - "graph-craft/wasm", ] image-compare = [] vello = ["gpu"] diff --git a/node-graph/nodes/path-bool/Cargo.toml b/node-graph/nodes/path-bool/Cargo.toml index d755adcb75..c8bfd779c9 100644 --- a/node-graph/nodes/path-bool/Cargo.toml +++ b/node-graph/nodes/path-bool/Cargo.toml @@ -7,7 +7,7 @@ authors = ["Graphite Authors "] license = "MIT OR Apache-2.0" [features] -wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] +wasm = ["tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/nodes/raster/Cargo.toml b/node-graph/nodes/raster/Cargo.toml index 9fad331852..61c6b2a806 100644 --- a/node-graph/nodes/raster/Cargo.toml +++ b/node-graph/nodes/raster/Cargo.toml @@ -25,13 +25,7 @@ std = [ "dep:serde", "dep:kurbo", ] -wasm = [ - "core-types/wasm", - "raster-types/wasm", - "vector-types/wasm", - "tsify", - "wasm-bindgen", -] +wasm = ["tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/nodes/text/Cargo.toml b/node-graph/nodes/text/Cargo.toml index 4537425a80..4637fa123f 100644 --- a/node-graph/nodes/text/Cargo.toml +++ b/node-graph/nodes/text/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] +wasm = ["tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/nodes/vector/Cargo.toml b/node-graph/nodes/vector/Cargo.toml index bd976b84e7..b4b811ff4e 100644 --- a/node-graph/nodes/vector/Cargo.toml +++ b/node-graph/nodes/vector/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] +wasm = ["tsify", "wasm-bindgen"] [dependencies] # Local dependencies From f4bc9a3b13152498133bd316aaa8d55d5c151867 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 7 Mar 2026 04:39:17 -0800 Subject: [PATCH 26/28] Fix compiling on desktop --- desktop/wrapper/src/intercept_frontend_message.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index fe430ad44a..f9c2bd1df1 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -26,6 +26,7 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD }); } FrontendMessage::TriggerSaveDocument { document_id, name, path, content } => { + let content = content.into_vec(); if let Some(path) = path { dispatcher.respond(DesktopFrontendMessage::WriteFile { path, content }); } else { @@ -42,6 +43,7 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD } } FrontendMessage::TriggerSaveFile { name, content } => { + let content = content.into_vec(); dispatcher.respond(DesktopFrontendMessage::SaveFileDialog { title: "Save File".to_string(), default_filename: name, From e0c7c9dbbeeae1a13ab64e67cfcee0830c39f0d0 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sat, 7 Mar 2026 05:02:42 -0800 Subject: [PATCH 27/28] Revert "Remove excess parts from Cargo.toml" This reverts commit 6b711117b3a5d5d8a3ee20f36a43bc74930b7c82. --- node-graph/graph-craft/Cargo.toml | 8 +++++++- node-graph/libraries/core-types/Cargo.toml | 2 +- node-graph/libraries/graphic-types/Cargo.toml | 7 ++++++- node-graph/libraries/raster-types/Cargo.toml | 2 +- node-graph/libraries/vector-types/Cargo.toml | 2 +- node-graph/nodes/gcore/Cargo.toml | 8 +++++++- node-graph/nodes/gstd/Cargo.toml | 8 ++++++++ node-graph/nodes/path-bool/Cargo.toml | 2 +- node-graph/nodes/raster/Cargo.toml | 8 +++++++- node-graph/nodes/text/Cargo.toml | 2 +- node-graph/nodes/vector/Cargo.toml | 2 +- 11 files changed, 41 insertions(+), 10 deletions(-) diff --git a/node-graph/graph-craft/Cargo.toml b/node-graph/graph-craft/Cargo.toml index 14b7f98023..492c4997be 100644 --- a/node-graph/graph-craft/Cargo.toml +++ b/node-graph/graph-craft/Cargo.toml @@ -11,7 +11,13 @@ dealloc_nodes = ["core-types/dealloc_nodes"] wgpu = ["wgpu-executor"] tokio = ["dep:tokio"] loading = ["serde_json"] -wasm = ["tsify", "wasm-bindgen"] +wasm = [ + "core-types/wasm", + "graphic-types/wasm", + "text-nodes/wasm", + "tsify", + "wasm-bindgen", +] [dependencies] # Local dependencies diff --git a/node-graph/libraries/core-types/Cargo.toml b/node-graph/libraries/core-types/Cargo.toml index f159805fe8..d6bc01c4db 100644 --- a/node-graph/libraries/core-types/Cargo.toml +++ b/node-graph/libraries/core-types/Cargo.toml @@ -11,7 +11,7 @@ default = ["serde"] nightly = [] type_id_logging = [] dealloc_nodes = [] -wasm = ["tsify", "wasm-bindgen"] +wasm = ["tsify", "wasm-bindgen", "no-std-types/wasm"] [dependencies] # Local dependencies diff --git a/node-graph/libraries/graphic-types/Cargo.toml b/node-graph/libraries/graphic-types/Cargo.toml index 272d49b08a..1dac611f07 100644 --- a/node-graph/libraries/graphic-types/Cargo.toml +++ b/node-graph/libraries/graphic-types/Cargo.toml @@ -8,7 +8,12 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = ["wasm-bindgen"] +wasm = [ + "core-types/wasm", + "vector-types/wasm", + "raster-types/wasm", + "wasm-bindgen", +] [dependencies] # Local dependencies diff --git a/node-graph/libraries/raster-types/Cargo.toml b/node-graph/libraries/raster-types/Cargo.toml index cd3928a50b..0039481667 100644 --- a/node-graph/libraries/raster-types/Cargo.toml +++ b/node-graph/libraries/raster-types/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] wgpu = ["dep:wgpu"] -wasm = ["tsify", "wasm-bindgen"] +wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/libraries/vector-types/Cargo.toml b/node-graph/libraries/vector-types/Cargo.toml index a8c9d97dff..5212fb2386 100644 --- a/node-graph/libraries/vector-types/Cargo.toml +++ b/node-graph/libraries/vector-types/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = ["tsify", "wasm-bindgen"] +wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/nodes/gcore/Cargo.toml b/node-graph/nodes/gcore/Cargo.toml index 6da0fb6f3e..9d2fbabb03 100644 --- a/node-graph/nodes/gcore/Cargo.toml +++ b/node-graph/nodes/gcore/Cargo.toml @@ -8,7 +8,13 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = ["tsify", "wasm-bindgen"] +wasm = [ + "core-types/wasm", + "raster-types/wasm", + "graphic-types/wasm", + "tsify", + "wasm-bindgen", +] [dependencies] # Local dependencies diff --git a/node-graph/nodes/gstd/Cargo.toml b/node-graph/nodes/gstd/Cargo.toml index 012c8417e3..ba1d2d62f9 100644 --- a/node-graph/nodes/gstd/Cargo.toml +++ b/node-graph/nodes/gstd/Cargo.toml @@ -16,6 +16,14 @@ wasm = [ "web-sys", "graphene-application-io/wasm", "image/png", + "core-types/wasm", + "vector-types/wasm", + "graphic-types/wasm", + "text-nodes/wasm", + "raster-nodes/wasm", + "vector-nodes/wasm", + "graphene-core/wasm", + "graph-craft/wasm", ] image-compare = [] vello = ["gpu"] diff --git a/node-graph/nodes/path-bool/Cargo.toml b/node-graph/nodes/path-bool/Cargo.toml index c8bfd779c9..d755adcb75 100644 --- a/node-graph/nodes/path-bool/Cargo.toml +++ b/node-graph/nodes/path-bool/Cargo.toml @@ -7,7 +7,7 @@ authors = ["Graphite Authors "] license = "MIT OR Apache-2.0" [features] -wasm = ["tsify", "wasm-bindgen"] +wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/nodes/raster/Cargo.toml b/node-graph/nodes/raster/Cargo.toml index 61c6b2a806..9fad331852 100644 --- a/node-graph/nodes/raster/Cargo.toml +++ b/node-graph/nodes/raster/Cargo.toml @@ -25,7 +25,13 @@ std = [ "dep:serde", "dep:kurbo", ] -wasm = ["tsify", "wasm-bindgen"] +wasm = [ + "core-types/wasm", + "raster-types/wasm", + "vector-types/wasm", + "tsify", + "wasm-bindgen", +] [dependencies] # Local dependencies diff --git a/node-graph/nodes/text/Cargo.toml b/node-graph/nodes/text/Cargo.toml index 4637fa123f..4537425a80 100644 --- a/node-graph/nodes/text/Cargo.toml +++ b/node-graph/nodes/text/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = ["tsify", "wasm-bindgen"] +wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies diff --git a/node-graph/nodes/vector/Cargo.toml b/node-graph/nodes/vector/Cargo.toml index b4b811ff4e..bd976b84e7 100644 --- a/node-graph/nodes/vector/Cargo.toml +++ b/node-graph/nodes/vector/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [features] default = ["serde"] -wasm = ["tsify", "wasm-bindgen"] +wasm = ["core-types/wasm", "tsify", "wasm-bindgen"] [dependencies] # Local dependencies From c07adb45a1d8614e6b2b60c36869be93fdad8233 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 8 Mar 2026 03:22:07 -0700 Subject: [PATCH 28/28] Update dev docs with simpler, more accurate instructions --- .../guide/codebase-overview/debugging-tips.md | 6 ++++++ .../volunteer/guide/project-setup/_index.md | 15 +++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/website/content/volunteer/guide/codebase-overview/debugging-tips.md b/website/content/volunteer/guide/codebase-overview/debugging-tips.md index d340281101..2f6b25f1e7 100644 --- a/website/content/volunteer/guide/codebase-overview/debugging-tips.md +++ b/website/content/volunteer/guide/codebase-overview/debugging-tips.md @@ -32,3 +32,9 @@ To also view logs of the messages dispatched by the message system, activate *He ## Node/layer and document IDs In debug mode, hover over a layer's name in the Layers panel, or a layer/node in the node graph, to view a tooltip with its ID. Likewise, document IDs may be read from their tab tooltips. + +## Performance profiling + +Be aware that having your browser's developer tools open will significantly impact performance in both debug and release builds, so it's best to close that when not in use. + +The *Performance* tab of the browser developer tools lets you record and analyze performance profiles, and this is a useful way to track down bottlenecks. The Firefox profiler has some additional features missing from the Chromium debugger, so if you are digging deep into a performance issue, it can be worth giving Firefox a try for that purpose. Be sure to use debug builds while profiling, otherwise inlined functions and other optimizations may produce a misleading view of where time is being spent. The live deployed web app (production and dev) and build links hosted by our CI infrastructure are all built with release optimizations. diff --git a/website/content/volunteer/guide/project-setup/_index.md b/website/content/volunteer/guide/project-setup/_index.md index b0bf40a34c..23ff433dbe 100644 --- a/website/content/volunteer/guide/project-setup/_index.md +++ b/website/content/volunteer/guide/project-setup/_index.md @@ -26,26 +26,21 @@ git clone https://github.com/GraphiteEditor/Graphite.git ## Development builds -From either the `/` (root) or `/frontend` directories, you can run the project by executing: +In the project directory, run the build system by executing: ```sh cargo run ``` -This spins up the dev server at with a file watcher that performs hot reloading of the web page. You should be able to start the server, edit and save web and Rust code, and shut it down by double pressing CtrlC. TypeScript and HTML changes require a manual page reload to fix broken state. +This will check for the required system dependency versions, help you install any that are missing, and spin up the dev server at serving the web app with debug optimizations. A file watcher hot-reloads the web app when you save a code file. Shut down the dev server by double pressing CtrlC. -This method compiles Graphite code in debug mode which includes debug symbols for viewing function names in stack traces. But be aware, it runs slower and the Wasm binary is much larger. (Having your browser's developer tools open will also significantly impact performance in both debug and release builds, so it's best to close that when not in use.) - -
-Dev server optimized build instructions: click here - -On rare occasions (like while running advanced performance profiles or proxying the dev server connection over a slow network where the >100 MB unoptimized binary size would pose an issue), you may need to run the dev server with release optimizations. To do that while keeping debug symbols: +For additional build commands, see: ```sh -cargo run release +cargo run help ``` -
+For example, if you must proxy the dev server connection over a slow network where the >100 MB unoptimized binary size would pose an issue, you may need to run with release optimizations using `cargo run release`. ## Development tooling
- +