Refactor the TypeScript data flow for full type safety and auto-generation of Rust types (#3865)

* Migrate Specta to Tsify to auto-generate messages.ts, working except colors and widgets

* Adopt the generated FillColor/Color/GradientStops

* Fix widget typing

* Separate WidgetGroup enum variants into wrapper structs

* Small rename

* Simplify widgets further

* Clean up message type references

* Switch type imports to the auto-generated file

* Remove lowercase serde rename

* Fix FillChoice deserialization

* Fix small regression from #3837

* Improve type safety

* Make WidgetSpan type-safe

* More cleanup and type safety

* More type safety

* More type safety

* Get the rest to type-check without errors; improve widget builder macro to have optional icons; improve Svelte 5 configs

* Cargo fmt

* Fix imports

* Update outdated readme info

* Fix lint command rename references

* Fix typos

* One more typos fix

* Remove unnecessary dep: prefix from the edited Cargo.toml files

* Remove excess parts from Cargo.toml

* Fix compiling on desktop

* Revert "Remove excess parts from Cargo.toml"

This reverts commit 6b711117b3a5d5d8a3ee20f36a43bc74930b7c82.

* Update dev docs with simpler, more accurate instructions
This commit is contained in:
Keavon Chambers 2026-03-09 14:46:26 -07:00
parent fbd2658148
commit 52d2b38a82
199 changed files with 2267 additions and 2813 deletions

View File

@ -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:

View File

@ -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

15
.vscode/settings.json vendored
View File

@ -39,25 +39,12 @@
"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,
"eslint.workingDirectories": ["./frontend", "./website"],
"eslint.validate": ["javascript", "typescript", "svelte"],
// Svelte config
"svelte.plugin.svelte.compilerWarnings": {
"css-unused-selector": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"vite-plugin-svelte-css-no-scopable-elements": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y-no-static-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y-no-noninteractive-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y-click-events-have-key-events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y_consider_explicit_label": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y_click_events_have_key_events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y_no_noninteractive_element_interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
"a11y_no_static_element_interactions": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
},
// Git Graph config
"git-graph.repository.fetchAndPrune": true,
"git-graph.repository.showRemoteHeads": false,

106
Cargo.lock generated
View File

@ -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]]

View File

@ -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 }

View File

@ -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

View File

@ -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,

View File

@ -6,7 +6,7 @@ pub(crate) mod menu {
use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, LabeledKeyOrMouseMotion, LabeledShortcut};
use graphite_editor::messages::input_mapper::utility_types::misc::ActionShortcut;
use graphite_editor::messages::layout::LayoutMessage;
use graphite_editor::messages::tool::tool_messages::tool_prelude::{Layout, LayoutGroup, LayoutTarget, MenuListEntry, Widget, WidgetId};
use graphite_editor::messages::tool::tool_messages::tool_prelude::{Layout, LayoutGroup, LayoutTarget, MenuListEntry, Widget, WidgetId, WidgetRow};
use crate::messages::{EditorMessage, KeyCode, MenuItem, Modifiers, Shortcut};
@ -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
@ -88,8 +88,8 @@ pub(crate) mod menu {
_ => None,
};
match icon.as_str() {
"CheckboxChecked" => {
match icon.as_deref() {
Some("CheckboxChecked") => {
return MenuItem::Checkbox {
id,
text,
@ -98,7 +98,7 @@ pub(crate) mod menu {
checked: true,
};
}
"CheckboxUnchecked" => {
Some("CheckboxUnchecked") => {
return MenuItem::Checkbox {
id,
text,

View File

@ -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 }

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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;

View File

@ -11,37 +11,46 @@ impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
fn process_message(&mut self, message: AppWindowMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
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<AppWindowMessage, ()> 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,

View File

@ -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),
])
}
}

View File

@ -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)])
}
}

View File

@ -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<Message>, 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<Message>, layout_target: LayoutTarget) {

View File

@ -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 {
@ -29,7 +29,7 @@ impl DialogLayoutHolder for AboutGraphiteDialog {
.into_iter()
.map(|(icon, label, url)| {
TextButton::new(label)
.icon(Some(icon.into()))
.icon(icon)
.flush(true)
.on_update(|_| FrontendMessage::TriggerVisitLink { url: url.into() }.into())
.widget_instance()
@ -40,7 +40,7 @@ impl DialogLayoutHolder for AboutGraphiteDialog {
let localized_commit_year = self.localized_commit_year.clone();
widgets.push(
TextButton::new("Licenses")
.icon(Some("License".into()))
.icon("License")
.flush(true)
.on_update(move |_| {
DialogMessage::RequestLicensesDialogWithLocalizedCommitDate {
@ -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()]),
])
}
}

View File

@ -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()]),
])
}
}

View File

@ -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()]),
])
}
}

View File

@ -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,27 +33,23 @@ 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![
TextLabel::new(
format!(
"
LayoutGroup::row(vec![TextLabel::new("Restart to apply changes?").bold(true).multiline(true).widget_instance()]),
LayoutGroup::row(vec![
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(),
]),
])
}
}

View File

@ -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)])
}
}
@ -46,7 +46,7 @@ impl LayoutHolder for DemoArtworkDialog {
let images = chunk
.iter()
.map(|(name, thumbnail, filename)| ImageButton::new(*thumbnail).width(Some("256px".into())).on_update(|_| make_dialog(name, filename)).widget_instance())
.map(|(name, thumbnail, filename)| ImageButton::new(*thumbnail).width("256px").on_update(|_| make_dialog(name, filename)).widget_instance())
.collect();
let buttons = chunk
@ -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();

View File

@ -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()]),
])
}
}

View File

@ -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 {
@ -34,10 +34,10 @@ impl DialogLayoutHolder for LicensesDialog {
];
let widgets = button_definitions
.iter()
.map(|&(icon, label, message_factory)| TextButton::new(label).icon(Some((icon).into())).flush(true).on_update(move |_| message_factory()).widget_instance())
.map(|&(icon, label, message_factory)| TextButton::new(label).icon(icon).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()]),
])
}
}

View File

@ -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![
TextLabel::new(license_text)
.monospace(true)
.multiline(true)
.min_width_characters(non_wrapping_column_width)
.widget_instance(),
],
}])
Layout(vec![LayoutGroup::row(vec![
TextLabel::new(license_text)
.monospace(true)
.multiline(true)
.min_width_characters(non_wrapping_column_width)
.widget_instance(),
])])
}
}

View File

@ -1,15 +1,16 @@
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;
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,13 +21,14 @@ 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
DisplayDialog {
title: String,
icon: String,
icon: IconName,
},
DialogClose,
DisplayDialogPanic {
@ -41,7 +43,7 @@ pub enum FrontendMessage {
font_size: f64,
color: String,
#[serde(rename = "fontData")]
font_data: Vec<u8>,
font_data: serde_bytes::ByteBuf,
transform: [f64; 6],
#[serde(rename = "maxWidth")]
max_width: Option<f64>,
@ -51,7 +53,7 @@ pub enum FrontendMessage {
},
DisplayEditableTextboxUpdateFontData {
#[serde(rename = "fontData")]
font_data: Vec<u8>,
font_data: serde_bytes::ByteBuf,
},
DisplayEditableTextboxTransform {
transform: [f64; 6],
@ -87,11 +89,11 @@ pub enum FrontendMessage {
document_id: DocumentId,
name: String,
path: Option<PathBuf>,
content: Vec<u8>,
content: serde_bytes::ByteBuf,
},
TriggerSaveFile {
name: String,
content: Vec<u8>,
content: serde_bytes::ByteBuf,
},
TriggerExportImage {
svg: String,
@ -125,6 +127,7 @@ pub enum FrontendMessage {
TriggerOpen,
TriggerImport,
TriggerSavePreferences {
#[tsify(type = "unknown")]
preferences: PreferencesMessageHandler,
},
TriggerSaveActiveDocument {
@ -152,9 +155,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 +165,10 @@ pub enum FrontendMessage {
exports: Vec<Option<FrontendGraphInput>>,
/// 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 +204,7 @@ pub enum FrontendMessage {
UpdateLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
diff: Vec<WidgetDiff>, // TODO: Align this with what's generated
},
UpdateImportReorderIndex {
#[serde(rename = "importIndex")]
@ -223,6 +225,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<Color>)>,
},
@ -253,7 +257,7 @@ pub enum FrontendMessage {
#[serde(rename = "secondaryColor")]
secondary_color: String,
#[serde(rename = "setColorChoice")]
set_color_choice: Option<String>,
set_color_choice: Option<PrimarySecondary>,
},
UpdateGraphFadeArtwork {
percentage: f64,
@ -278,7 +282,8 @@ pub enum FrontendMessage {
selected: Vec<NodeId>,
},
UpdateNodeGraphTransform {
transform: Transform,
translation: (f64, f64),
scale: f64,
},
UpdateNodeThumbnail {
id: NodeId,
@ -304,6 +309,7 @@ pub enum FrontendMessage {
UpdateViewportHolePunch {
active: bool,
},
#[cfg(not(target_family = "wasm"))]
UpdateViewportPhysicalBounds {
x: f64,
y: f64,
@ -322,18 +328,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,
}

View File

@ -4,3 +4,7 @@ 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.
// TODO: Then remove `#[widget_builder(string)]` from all icon fields.
pub type IconName = String;

View File

@ -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<PathBuf>,
@ -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,10 @@ 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<u8>,
pub data: serde_bytes::ByteBuf,
pub width: u32,
pub height: u32,
}

View File

@ -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<Key> 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<LabeledKeyOrMouseMotion>);
impl From<KeysGroup> for LabeledShortcut {

View File

@ -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,

View File

@ -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),

View File

@ -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;
@ -63,7 +62,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 +83,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
@ -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<String, serde_json::value::Value>| -> Option<Color> {
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::<FillChoice>(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)
}
};

View File

@ -10,7 +10,8 @@ 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 +20,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 +61,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 +154,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<LayoutGroup>);
impl Layout {
@ -241,19 +245,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);
}
@ -293,19 +297,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);
}
@ -316,52 +320,88 @@ 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 {
#[serde(rename = "columnWidgets")]
widgets: Vec<WidgetInstance>,
},
#[serde(rename = "row")]
Row {
#[serde(rename = "rowWidgets")]
widgets: Vec<WidgetInstance>,
},
#[serde(rename = "table")]
Table {
#[serde(rename = "tableWidgets")]
rows: Vec<Vec<WidgetInstance>>,
unstyled: bool,
},
#[serde(rename = "section")]
Section {
name: String,
description: String,
visible: bool,
pinned: bool,
id: u64,
layout: Layout,
},
Column(WidgetColumn),
Row(WidgetRow),
Table(WidgetTable),
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<WidgetInstance>,
}
#[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<WidgetInstance>,
}
#[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<Vec<WidgetInstance>>,
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<Vec<WidgetInstance>> for LayoutGroup {
fn from(widgets: Vec<WidgetInstance>) -> LayoutGroup {
LayoutGroup::Row { widgets }
LayoutGroup::Row(WidgetRow { widgets })
}
}
impl LayoutGroup {
pub fn row(widgets: Vec<WidgetInstance>) -> Self {
Self::Row(WidgetRow { widgets })
}
pub fn column(widgets: Vec<WidgetInstance>) -> Self {
Self::Column(WidgetColumn { widgets })
}
pub fn table(rows: Vec<Vec<WidgetInstance>>, unstyled: bool) -> Self {
Self::Table(WidgetTable { rows, unstyled })
}
pub fn section(name: impl Into<String>, description: impl Into<String>, 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<String>) -> 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();
@ -394,7 +434,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<'_> {
@ -413,7 +453,8 @@ impl Diffable for LayoutGroup {
fn diff(&mut self, new: Self, widget_path: &mut Vec<usize>, widget_diffs: &mut Vec<WidgetDiff>) {
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() {
@ -421,7 +462,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;
@ -434,22 +480,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
@ -469,14 +515,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 });
@ -501,14 +547,14 @@ impl Diffable for LayoutGroup {
fn collect_checkbox_ids(&self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &mut HashMap<CheckboxId, CheckboxId>) {
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);
@ -519,7 +565,7 @@ impl Diffable for LayoutGroup {
}
}
}
Self::Section { layout, .. } => {
Self::Section(WidgetSection { layout, .. }) => {
layout.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
}
}
@ -527,14 +573,14 @@ impl Diffable for LayoutGroup {
fn replace_widget_ids(&mut self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &HashMap<CheckboxId, CheckboxId>) {
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);
@ -545,14 +591,15 @@ impl Diffable for LayoutGroup {
}
}
}
Self::Section { layout, .. } => {
Self::Section(WidgetSection { layout, .. }) => {
layout.replace_widget_ids(layout_target, widget_path, checkbox_map);
}
}
}
}
#[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 +721,8 @@ impl Diffable for WidgetInstance {
}
}
#[derive(Clone, specta::Type)]
#[derive(Clone)]
pub struct WidgetCallback<T> {
#[specta(skip)]
pub callback: Arc<dyn Fn(&T) -> Message + 'static + Send + Sync>,
}
@ -692,7 +738,8 @@ impl<T> Default for WidgetCallback<T> {
}
}
#[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 +766,8 @@ 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 +780,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),

View File

@ -1,3 +1,4 @@
use crate::messages::frontend::IconName;
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::FrontendGraphDataType;
@ -6,12 +7,14 @@ 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
#[widget_builder(constructor)]
pub icon: String,
#[widget_builder(string)]
pub icon: IconName,
#[serde(rename = "hoverIcon")]
pub hover_icon: Option<String>,
#[widget_builder(constructor)]
@ -38,12 +41,14 @@ 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
pub style: Option<String>,
pub icon: Option<String>,
#[widget_builder(string)]
pub icon: Option<IconName>,
pub disabled: bool,
// Children
@ -63,7 +68,8 @@ pub struct PopoverButton {
pub tooltip_shortcut: Option<ActionShortcut>,
}
#[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 +83,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,13 +109,15 @@ 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
#[widget_builder(constructor)]
pub label: String,
pub icon: Option<String>,
#[widget_builder(string)]
pub icon: Option<IconName>,
#[serde(rename = "hoverIcon")]
pub hover_icon: Option<String>,
pub disabled: bool,
@ -146,7 +155,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 +182,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 +218,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

View File

@ -1,3 +1,4 @@
use crate::messages::frontend::IconName;
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
@ -7,14 +8,15 @@ 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
#[widget_builder(constructor)]
pub checked: bool,
#[derivative(Default(value = "\"Checkmark\".to_string()"))]
pub icon: String,
#[widget_builder(string)]
pub icon: Option<IconName>,
#[serde(rename = "forLabel")]
pub for_label: CheckboxId,
pub disabled: bool,
@ -36,6 +38,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 +52,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 +100,8 @@ pub struct DropdownInput {
pub type MenuListEntrySections = Vec<Vec<MenuListEntry>>;
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[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)]
pub struct MenuListEntry {
@ -110,7 +109,8 @@ pub struct MenuListEntry {
#[widget_builder(constructor)]
pub value: String,
pub label: String,
pub icon: String,
#[widget_builder(string)]
pub icon: Option<IconName>,
pub disabled: bool,
// Children
@ -120,7 +120,7 @@ pub struct MenuListEntry {
pub children_hash: u64,
// Styling
pub font: String,
pub font: Option<String>,
// Tooltips
#[serde(rename = "tooltipLabel")]
@ -148,7 +148,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,22 +247,30 @@ 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 {
/// The value is added by `step`.
#[default]
Add,
/// The value is multiplied by `step`.
Multiply,
/// The functions `incrementCallbackIncrease` and `incrementCallbackDecrease` call custom behavior.
Callback,
/// The increment arrows are not shown.
None,
}
#[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 +289,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 +313,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 {
@ -311,7 +322,8 @@ pub struct RadioEntryData {
#[widget_builder(constructor)]
pub value: String,
pub label: String,
pub icon: String,
#[widget_builder(string)]
pub icon: Option<IconName>,
// Tooltips
#[serde(rename = "tooltipLabel")]
@ -330,7 +342,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 +353,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 +380,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 +418,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 +443,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

View File

@ -1,13 +1,15 @@
use super::input_widgets::CheckboxId;
use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::{frontend::IconName, 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)]
pub icon: String,
#[widget_builder(string)]
pub icon: IconName,
pub disabled: bool,
// Tooltips
@ -19,7 +21,8 @@ pub struct IconLabel {
pub tooltip_shortcut: Option<ActionShortcut>,
}
#[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 +30,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 +47,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 +84,8 @@ pub struct TextLabel {
pub tooltip_shortcut: Option<ActionShortcut>,
}
#[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 +103,8 @@ pub struct ImageLabel {
pub tooltip_shortcut: Option<ActionShortcut>,
}
#[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

View File

@ -76,7 +76,7 @@ impl LayoutHolder for MenuBarMessageHandler {
TextButton::new("Graphite")
.label("")
.flush(true)
.icon(Some("GraphiteLogo".into()))
.icon("GraphiteLogo")
.on_commit(|_| FrontendMessage::TriggerVisitLink { url: "https://graphite.art".into() }.into())
.widget_instance(),
#[cfg(target_os = "macos")]
@ -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)])
}
}

View File

@ -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()

View File

@ -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<WidgetInstance> {
fn label(x: impl Into<String>) -> Vec<LayoutGroup> {
let error = vec![TextLabel::new(x).widget_instance()];
vec![LayoutGroup::Row { widgets: error }]
vec![LayoutGroup::row(error)]
}
trait TableRowLayout {
@ -234,7 +234,7 @@ impl<T: TableRowLayout> TableRowLayout for Vec<T> {
rows.insert(0, column_headings(&["", "element"]));
vec![LayoutGroup::Table { rows, unstyled: false }]
vec![LayoutGroup::table(rows, false)]
}
}
@ -276,7 +276,7 @@ impl<T: TableRowLayout> TableRowLayout for Table<T> {
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<CPU> {
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<CPU> {
});
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<GPU> {
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<f64> {
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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<LayoutGroup> {
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)]
}
}

View File

@ -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<DocumentMessage, DocumentMessageContext<'_>> 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<DocumentMessage, DocumentMessageContext<'_>> 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,
})
}
}
@ -2216,256 +2212,222 @@ 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: {
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 {
widgets: {
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 {
widgets: vec![TextLabel::new("Select Tool").widget_instance()],
},
LayoutGroup::Row {
widgets: {
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 {
widgets: {
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 {
widgets: {
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 {
widgets: {
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 {
widgets: {
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 {
widgets: {
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 {
widgets: {
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 {
widgets: {
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 {
widgets: vec![TextLabel::new("Pen & Path Tools").widget_instance()],
},
LayoutGroup::Row {
widgets: {
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 {
widgets: {
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 {
widgets: {
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(),
]
},
},
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(),
]
}),
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(),
]
}),
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(),
]
}),
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(),
]
}),
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(),
]
}),
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(),
]
}),
LayoutGroup::row({
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.origin)
.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(),
]
}),
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(),
]
}),
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(),
]
}),
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(),
]
}),
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(),
]
}),
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(),
]
}),
]))
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
@ -2484,16 +2446,12 @@ 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))
@ -2510,13 +2468,11 @@ 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))
@ -2533,7 +2489,7 @@ impl DocumentMessageHandler {
.widget_instance(),
TextLabel::new(name).tooltip_label(name).tooltip_description(description).for_checkbox(checkbox_id).widget_instance(),
]
},
})
}))
.collect(),
))
@ -2630,8 +2586,8 @@ impl DocumentMessageHandler {
widgets.extend([
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
TextButton::new("Node Graph")
.icon(Some((if self.graph_view_overlay_open { "GraphViewOpen" } else { "GraphViewClosed" }).into()))
.hover_icon(Some((if self.graph_view_overlay_open { "GraphViewClosed" } else { "GraphViewOpen" }).into()))
.icon(if self.graph_view_overlay_open { "GraphViewOpen" } else { "GraphViewClosed" })
.hover_icon(if self.graph_view_overlay_open { "GraphViewClosed" } else { "GraphViewOpen" })
.tooltip_label(if self.graph_view_overlay_open { "Hide Node Graph" } else { "Show Node Graph" })
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::GraphViewOverlayToggle))
.on_update(move |_| DocumentMessage::GraphViewOverlayToggle.into())
@ -2639,7 +2595,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);
@ -2777,25 +2733,25 @@ 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)
.hover_icon(Some((if selection_all_locked { "PadlockUnlocked" } else { "PadlockLocked" }).into()))
.hover_icon(if selection_all_locked { "PadlockUnlocked" } else { "PadlockLocked" })
.tooltip_label(if selection_all_locked { "Unlock Selected" } else { "Lock Selected" })
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::ToggleSelectedLocked))
.on_update(|_| NodeGraphMessage::ToggleSelectedLocked.into())
.disabled(!has_selection)
.widget_instance(),
IconButton::new(if selection_all_visible { "EyeVisible" } else { "EyeHidden" }, 24)
.hover_icon(Some((if selection_all_visible { "EyeHide" } else { "EyeShow" }).into()))
.hover_icon(if selection_all_visible { "EyeHide" } else { "EyeShow" })
.tooltip_label(if selection_all_visible { "Hide Selected" } else { "Show Selected" })
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::ToggleSelectedVisibility))
.on_update(|_| DocumentMessage::ToggleSelectedVisibility.into())
.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,
@ -2821,7 +2777,7 @@ impl DocumentMessageHandler {
let widgets = vec![
PopoverButton::new()
.icon(Some("Node".to_string()))
.icon("Node")
.menu_direction(Some(MenuDirection::Top))
.tooltip_description("Add an operation to the end of this layer's chain of nodes.")
.disabled(!has_selection || has_multiple_selection)
@ -2849,7 +2805,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(),
@ -2875,7 +2831,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,
});
}

View File

@ -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),
@ -2191,9 +2192,10 @@ 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(
@ -2227,10 +2229,12 @@ 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(
@ -2298,7 +2302,7 @@ fn static_input_properties() -> InputProperties {
"noise_properties_noise_type".to_string(),
Box::new(|node_id, index, context| {
let noise_type_row = enum_choice::<NoiseType>().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(
@ -2320,7 +2324,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(
@ -2408,7 +2412,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(
@ -2504,7 +2508,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
@ -2548,24 +2552,20 @@ 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)),
}])
}),
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(),
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)
}),

View File

@ -847,7 +847,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> 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<NodeGraphMessage, NodeGraphMessageContext<'a>> 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<NodeGraphMessage, NodeGraphMessageContext<'a>> 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,
});
}
@ -2181,7 +2181,7 @@ impl NodeGraphMessageHandler {
let mut widgets = vec![
PopoverButton::new()
.icon(Some("Node".to_string()))
.icon("Node")
.tooltip_label("New Node")
.tooltip_description("To add a node at the pointer location, perform the shortcut in an open area of the graph.")
.tooltip_shortcut(action_shortcut_manual!(Key::MouseRight))
@ -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(),
//
@ -2252,14 +2252,14 @@ impl NodeGraphMessageHandler {
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
//
IconButton::new(if selection_all_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24)
.hover_icon(Some((if selection_all_locked { "PadlockUnlocked" } else { "PadlockLocked" }).into()))
.hover_icon(if selection_all_locked { "PadlockUnlocked" } else { "PadlockLocked" })
.tooltip_label(if selection_all_locked { "Unlock Selected" } else { "Lock Selected" })
.tooltip_shortcut(action_shortcut!(NodeGraphMessageDiscriminant::ToggleSelectedLocked))
.on_update(|_| NodeGraphMessage::ToggleSelectedLocked.into())
.disabled(!has_selection || !selection_includes_layers)
.widget_instance(),
IconButton::new(if selection_all_visible { "EyeVisible" } else { "EyeHidden" }, 24)
.hover_icon(Some((if selection_all_visible { "EyeHide" } else { "EyeShow" }).into()))
.hover_icon(if selection_all_visible { "EyeHide" } else { "EyeShow" })
.tooltip_label(if selection_all_visible { "Hide Selected" } else { "Show Selected" })
.tooltip_shortcut(action_shortcut!(NodeGraphMessageDiscriminant::ToggleSelectedVisibility))
.on_update(|_| NodeGraphMessage::ToggleSelectedVisibility.into())
@ -2286,7 +2286,7 @@ impl NodeGraphMessageHandler {
// If only one node is selected then show the preview or stop previewing button
if let Some(node_id) = previewing {
let button = TextButton::new("End Preview")
.icon(Some("FrameAll".to_string()))
.icon("FrameAll")
.tooltip_description("Restore preview to the graph output.")
.on_update(move |_| NodeGraphMessage::TogglePreview { node_id }.into())
.widget_instance();
@ -2298,7 +2298,7 @@ impl NodeGraphMessageHandler {
.any(|export| matches!(export, NodeInput::Node { node_id: export_node_id, .. } if *export_node_id == node_id));
if selection_is_not_already_the_output && no_other_selections {
let button = TextButton::new("Preview")
.icon(Some("FrameAll".to_string()))
.icon("FrameAll")
.tooltip_label("Preview")
.tooltip_description("Temporarily set the graph output to the selected node or layer. Perform the shortcut on a node or layer for quick access.")
.tooltip_shortcut(action_shortcut_manual!(Key::Alt, Key::MouseLeft))
@ -2323,7 +2323,7 @@ impl NodeGraphMessageHandler {
]);
}
self.widgets[0] = LayoutGroup::Row { widgets };
self.widgets[0] = LayoutGroup::row(widgets);
}
fn update_graph_bar_right(
@ -2357,15 +2357,15 @@ impl NodeGraphMessageHandler {
widgets.extend([
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
TextButton::new("Node Graph")
.icon(Some("GraphViewOpen".into()))
.hover_icon(Some("GraphViewClosed".into()))
.icon("GraphViewOpen")
.hover_icon("GraphViewClosed")
.tooltip_label("Hide Node Graph")
.tooltip_shortcut(action_shortcut!(DocumentMessageDiscriminant::GraphViewOverlayToggle))
.on_update(move |_| DocumentMessage::GraphViewOverlayToggle.into())
.widget_instance(),
]);
self.widgets[1] = LayoutGroup::Row { widgets };
self.widgets[1] = LayoutGroup::row(widgets);
}
/// Collate the properties panel sections for a node graph
@ -2407,25 +2407,23 @@ impl NodeGraphMessageHandler {
let mut properties = Vec::new();
if let [node_id] = *nodes.as_slice() {
properties.push(LayoutGroup::Row {
widgets: 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(),
],
});
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(),
]));
}
properties.extend(selected_nodes);
@ -2435,18 +2433,16 @@ 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![
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 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(),
])];
let Some(network) = context.network_interface.nested_network(context.selection_network_path) else {
warn!("No network in collate_properties");
@ -2482,50 +2478,48 @@ impl NodeGraphMessageHandler {
return Vec::new();
}
let mut layer_properties = vec![LayoutGroup::Row {
widgets: 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 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("Node")
.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 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 { widgets: vec![node_chooser] }])
})
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
],
}];
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
@ -2651,7 +2645,7 @@ impl NodeGraphMessageHandler {
exposed_outputs,
primary_output_connected_to_layer,
primary_input_connected_to_layer,
position,
position: position.into(),
previewed,
visible,
locked,
@ -2695,6 +2689,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 })
}
@ -2841,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,

View File

@ -31,7 +31,7 @@ use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, Gra
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
let widget = TextLabel::new(text).widget_instance();
vec![LayoutGroup::Row { widgets: vec![widget] }]
vec![LayoutGroup::row(vec![widget])]
}
fn optionally_update_value<T>(value: impl Fn(&T) -> Option<TaggedValue> + '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<WidgetInstance> {
@ -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::<f64>::INDEX, true, context),
NumberInput::default().label("H").min(0.).unit(" px"),
),
};
let spacing = LayoutGroup::row(number_widget(
ParameterWidgetsInfo::new(node_id, SpacingInput::<f64>::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<LayoutGroup> {
@ -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<LayoutGroup> {
@ -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<W::Value> = 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);
}

View File

@ -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<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 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<String>,
}
#[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<String>,
@ -189,7 +192,8 @@ pub struct FrontendClickTargets {
pub modify_import_export: Vec<String>,
}
#[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,

View File

@ -228,42 +228,38 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
})
};
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![
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(),
],
});
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(),
]));
let mut color_widgets = vec![
TextLabel::new("Display").table_align(true).widget_instance(),
@ -288,80 +284,72 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
}))
.widget_instance(),
);
widgets.push(LayoutGroup::Row { widgets: color_widgets });
widgets.push(LayoutGroup::row(color_widgets));
widgets.push(LayoutGroup::Row {
widgets: vec![
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(),
],
});
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))
.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 {
widgets: vec![
TextLabel::new("Spacing").table_align(true).widget_instance(),
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))
.label("X")
.unit(" px")
.min(0.)
.min_width(98)
.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(spacing.y))
.label("Y")
.unit(" px")
.min(0.)
.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(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 {
widgets: 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 {
widgets: 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(),
],
});
]));
}
}

View File

@ -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<Mutex<OverlayContextInternal>>,
pub viewport: ViewportMessageHandler,
pub visibility_settings: OverlaysVisibilitySettings,

View File

@ -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,

View File

@ -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,

View File

@ -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 {

View File

@ -3,7 +3,8 @@ 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 +13,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,

View File

@ -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 {

View File

@ -1,25 +1,28 @@
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};
/// 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<LayerStructureEntry>,
}
#[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")]
pub implementation_name: String,
#[serde(rename = "iconName")]
pub icon_name: Option<String>,
pub icon_name: Option<IconName>,
pub alias: String,
#[serde(rename = "inSelectedNetwork")]
pub in_selected_network: bool,
@ -47,7 +50,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<NodeId>);
impl SelectedNodes {
@ -157,5 +161,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<LayerNodeIdentifier>);

View File

@ -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<WirePath>,
}
#[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,

View File

@ -1011,12 +1011,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> 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()))
.icon("File")
.flush(true)
.on_commit(|_| DialogMessage::RequestNewDocumentDialog.into())
.widget_instance(),
@ -1024,7 +1023,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
],
vec![
TextButton::new("Open Document")
.icon(Some("Folder".into()))
.icon("Folder")
.flush(true)
.on_commit(|_| PortfolioMessage::Open.into())
.widget_instance(),
@ -1032,20 +1031,21 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
],
vec![
TextButton::new("Open Demo Artwork")
.icon(Some("Image".into()))
.icon("Image")
.flush(true)
.on_commit(|_| DialogMessage::RequestDemoArtworkDialog.into())
.widget_instance(),
],
vec![
TextButton::new("Support the Development Fund")
.icon(Some("Heart".into()))
.icon("Heart")
.flush(true)
.on_commit(move |_| FrontendMessage::TriggerVisitLink { url: donate.to_string() }.into())
.widget_instance(),
],
],
};
true,
);
responses.add(LayoutMessage::DestroyLayout {
layout_target: LayoutTarget::WelcomeScreenButtons,
@ -1061,7 +1061,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> 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]),

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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),
@ -220,7 +223,7 @@ impl LayoutHolder for BrushTool {
.widget_instance(),
);
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
}

View File

@ -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<ToolMessage, &mut ToolActionMessageContext<'a>> for EyedropperTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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<String>,
color_choice: Option<PrimarySecondary>,
}
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<String>,
set_color_choice: Option<PrimarySecondary>,
) {
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<String>,
set_color_choice: Option<PrimarySecondary>,
) {
tool_data.preview = true;
tool_data.color_choice = set_color_choice.clone();
@ -211,7 +224,7 @@ fn update_cursor_preview_common(
image: Option<EyedropperPreviewImage>,
input: &InputPreprocessorMessageHandler,
global_tool_data: &DocumentToolData,
set_color_choice: Option<String>,
set_color_choice: Option<PrimarySecondary>,
) {
responses.add(FrontendMessage::UpdateEyedropperSamplingState {
image,

View File

@ -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,

View File

@ -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<Color>),
FillColorType(ToolColorType),
@ -151,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)])
}
}

View File

@ -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,
@ -199,7 +201,7 @@ impl LayoutHolder for GradientTool {
widgets.push(reverse_direction);
}
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
}
@ -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 });
}
}
_ => {}

View File

@ -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,

View File

@ -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 },
@ -326,7 +329,7 @@ impl LayoutHolder for PathTool {
// Works only if a single layer is selected and its type is Vector
let path_node_button = TextButton::new("Make Path Editable")
.icon(Some("NodeShape".into()))
.icon("NodeShape")
.tooltip_label("Make Path Editable")
.tooltip_description(
"Enables the Pen and Path tools to directly edit layer geometry resulting from nondestructive operations. This inserts a 'Path' node as the last operation of the selected layer.",
@ -349,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,
])])
}
}

View File

@ -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<Color>),
FillColorType(ToolColorType),
@ -238,7 +241,7 @@ impl LayoutHolder for PenTool {
.widget_instance(),
);
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
}

View File

@ -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,
@ -265,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)])
}
}

View File

@ -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<Color>),
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 },
@ -423,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)])
}
}

View File

@ -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<Color>),
FillColorType(ToolColorType),
@ -158,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)])
}
}

View File

@ -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<Color>),
FillColorType(ToolColorType),
@ -271,7 +273,7 @@ impl TextTool {
},
));
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
}
@ -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

View File

@ -128,23 +128,21 @@ pub struct DocumentToolData {
impl DocumentToolData {
pub fn update_working_colors(&self, responses: &mut VecDeque<Message>) {
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![
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(),
],
},
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(),
]),
]);
responses.add(LayoutMessage::SendLayout {
@ -308,7 +306,7 @@ impl ToolData {
.skip(1)
.collect();
Layout(vec![LayoutGroup::Row { widgets: tool_groups_layout }])
Layout(vec![LayoutGroup::row(tool_groups_layout)])
}
}
@ -360,7 +358,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 +523,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<HintGroup>);
impl HintData {
@ -558,7 +558,7 @@ impl HintData {
}
}
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
pub fn send_layout(&self, responses: &mut VecDeque<Message>) {
@ -576,10 +576,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<HintInfo>);
#[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).

View File

@ -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<Point> 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<P: Position>: 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<Point> 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<Bounds> 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,

View File

@ -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})"));

View File

@ -56,7 +56,7 @@ pub struct NodeRuntime {
thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
vector_modify: HashMap<NodeId, Vector>,
/// 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<wgpu_executor::WgpuSurface>,
}
@ -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");

View File

@ -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.
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`
Configures the Svelte compiler, including the preprocessor setup for SCSS and TypeScript support, and compiler warning filters.
## TypeScript configuration: `tsconfig.json`
Basic configuration options for the TypeScript build tool to do its job in our repository.
## 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. 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.
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.
## TypeScript configurations: `tsconfig.json`
Basic configuration options for the TypeScript build tool to do its job in our repository.
## Vite configurations: `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.

View File

@ -72,7 +72,7 @@ export default defineConfig([
],
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as", objectLiteralTypeAssertions: "never" }],
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
"@typescript-eslint/consistent-indexed-object-style": ["error", "record"],
"@typescript-eslint/consistent-generic-constructors": ["error", "constructor"],
"@typescript-eslint/no-restricted-types": ["error", { types: { null: "Use `undefined` instead." } }],

View File

@ -31,6 +31,7 @@
"process": "^0.11.10",
"sass": "^1.97.2",
"svelte": "5.47.1",
"svelte-check": "^4.4.4",
"svelte-preprocess": "^6.0.3",
"tar": "^7.5.4",
"ts-node": "^10.9.2",
@ -5277,6 +5278,16 @@
"node": ">=10"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -6135,6 +6146,19 @@
"tslib": "^2.1.0"
}
},
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"mri": "^1.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@ -6715,6 +6739,30 @@
"node": ">=18"
}
},
"node_modules/svelte-check": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.4.tgz",
"integrity": "sha512-F1pGqXc710Oi/wTI4d/x7d6lgPwwfx1U6w3Q35n4xsC2e8C/yN2sM1+mWxjlMcpAfWucjlq4vPi+P4FZ8a14sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"chokidar": "^4.0.1",
"fdir": "^6.2.0",
"picocolors": "^1.0.0",
"sade": "^1.7.4"
},
"bin": {
"svelte-check": "bin/svelte-check"
},
"engines": {
"node": ">= 18.0.0"
},
"peerDependencies": {
"svelte": "^4.0.0 || ^5.0.0-next.0",
"typescript": ">=5.0.0"
}
},
"node_modules/svelte-eslint-parser": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.4.1.tgz",

View File

@ -15,8 +15,8 @@
"build-native": "npm run setup && npm run native:build-production",
"build-native-dev": "npm run setup && npm run native:build-dev",
"---------- UTILITIES ----------": "",
"lint": "eslint . && tsc --noEmit",
"lint-fix": "eslint . --fix && tsc --noEmit",
"check": "svelte-check --fail-on-warnings && eslint",
"fix": "eslint --fix",
"---------- INTERNAL ----------": "",
"setup": "node package-installer.js && node branding-installer.js",
"native:build-dev": "wasm-pack build ./wasm --dev --target=web --no-default-features --features native && vite build --mode native",
@ -45,6 +45,7 @@
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-svelte": "^3.14.0",
"globals": "^17.0.0",
"svelte-check": "^4.4.4",
"license-checker-rseidelsohn": "^4.4.2",
"postcss": "^8.5.6",
"prettier": "^3.8.0",

View File

@ -15,7 +15,7 @@
});
onDestroy(() => {
// Destroy the WASM editor handle
// Destroy the Wasm editor handle
editor?.handle.free();
});
</script>

View File

@ -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 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>("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 });`.

View File

@ -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);

View File

@ -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/`

View File

@ -1,15 +1,14 @@
<script lang="ts">
import { getContext, onDestroy, createEventDispatcher, tick } from "svelte";
import type { FillChoice, MenuDirection } from "@graphite/messages";
import type { Color } from "@graphite/messages";
import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
import type { FillChoice, MenuDirection, Color } from "@graphite/../wasm/pkg/graphite_wasm";
import type { TooltipState } from "@graphite/state-providers/tooltip";
import {
contrastingOutlineFactor,
isColor,
isGradient,
fillChoiceColor,
fillChoiceGradientStops,
createColor,
createNoneColor,
createColorFromHSVA,
colorFromCSS,
colorToRgb255,
@ -24,10 +23,8 @@
} from "@graphite/utility-functions/colors";
import type { HSV, RGB } from "@graphite/utility-functions/colors";
import { clamp } from "@graphite/utility-functions/math";
import { isDesktop } from "@graphite/utility-functions/platform";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
import FloatingMenu, { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
@ -37,20 +34,20 @@
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
type PresetColors = "none" | "black" | "white" | "red" | "yellow" | "green" | "cyan" | "blue" | "magenta";
type PresetColors = "None" | "Black" | "White" | "Red" | "Yellow" | "Green" | "Cyan" | "Blue" | "Magenta";
const PURE_COLORS: Record<PresetColors, [number, number, number]> = {
none: [0, 0, 0],
black: [0, 0, 0],
white: [1, 1, 1],
red: [1, 0, 0],
yellow: [1, 1, 0],
green: [0, 1, 0],
cyan: [0, 1, 1],
blue: [0, 0, 1],
magenta: [1, 0, 1],
None: [0, 0, 0],
Black: [0, 0, 0],
White: [1, 1, 1],
Red: [1, 0, 0],
Yellow: [1, 1, 0],
Green: [0, 1, 0],
Cyan: [0, 1, 1],
Blue: [0, 0, 1],
Magenta: [1, 0, 1],
};
const PURE_COLORS_GRAYABLE = [
const PURE_COLORS_GRAYABLE: [PresetColors, string, string][] = [
["Red", "#ff0000", "#4c4c4c"],
["Yellow", "#ffff00", "#e3e3e3"],
["Green", "#00ff00", "#969696"],
@ -70,17 +67,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;
let activeIndex = 0 as number | undefined;
$: gradient = fillChoiceGradientStops(colorOrGradient);
let activeIndex: number | undefined = 0;
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 +114,30 @@
$: 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][];
$: hsvChannels = Object.entries(!isNone ? { h: hue * 360, s: saturation * 100, v: value * 100 } : { h: undefined, s: undefined, v: undefined }) as [keyof HSV, number | undefined][];
$: oldColor = oldIsNone ? undefined : createColorFromHSVA(oldHue, oldSaturation, oldValue, oldAlpha);
$: newColor = isNone ? undefined : createColorFromHSVA(hue, saturation, value, alpha);
$: 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, "--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 +151,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
@ -160,7 +170,7 @@
function onPointerDown(e: PointerEvent) {
if (disabled) return;
const target = (e.target || undefined) as HTMLElement | undefined;
const target = e.target instanceof HTMLElement ? e.target : undefined;
draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined;
hueBeforeDrag = hue;
@ -301,14 +311,20 @@
setColor(color);
}
function setColor(color?: Color) {
const colorToEmit = color || createColorFromHSVA(hue, saturation, value, alpha);
if (gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.position[activeIndex] !== undefined && isGradient(colorOrGradient)) {
colorOrGradient.color[activeIndex] = colorToEmit;
function setColor(color?: Color | "None") {
if (color === "None") {
dispatch("colorOrGradient", "None");
return;
}
dispatch("colorOrGradient", gradient || colorToEmit);
const colorToEmit = color || createColorFromHSVA(hue, saturation, value, alpha);
if (gradientSpectrumInputWidget && activeIndex !== undefined && gradient && gradient.position[activeIndex] !== undefined) {
const gradientStops = fillChoiceGradientStops(colorOrGradient);
if (gradientStops) gradientStops.color[activeIndex] = colorToEmit;
}
dispatch("colorOrGradient", gradient ? { Gradient: gradient } : { Solid: colorToEmit });
}
function swapNewWithOld() {
@ -323,7 +339,7 @@
setNewHSVA(oldHue, oldSaturation, oldValue, oldAlpha, oldIsNone);
setOldHSVA(tempHue, tempSaturation, tempValue, tempAlpha, tempIsNone);
setColor(old);
setColor(old || "None");
}
function setColorCode(colorCode: string) {
@ -333,7 +349,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));
@ -356,19 +372,12 @@
setColor();
}
function setColorPresetSubtile(e: MouseEvent) {
const clickedTile = e.target as HTMLDivElement | undefined;
const tileColor = clickedTile?.getAttribute("data-pure-tile") || undefined;
if (tileColor) setColorPreset(tileColor as PresetColors);
}
function setColorPreset(preset: PresetColors) {
dispatch("startHistoryTransaction");
if (preset === "none") {
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);
@ -398,18 +407,16 @@
// TODO: Replace this temporary usage of the browser eyedropper API, that only works in Chromium-based browsers, with the custom color sampler system used by the Eyedropper tool
function eyedropperSupported(): boolean {
// TODO: Implement support in the desktop app for OS-level color picking
if (isDesktop()) return false;
if (isPlatformNative()) 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 {
@ -427,8 +434,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 +450,14 @@
<FloatingMenu class="color-picker" classes={{ disabled }} {open} on:open {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}>
<LayoutRow
styles={{
"--new-color": colorToHexOptionalAlpha(newColor),
"--new-color": newColor ? colorToHexOptionalAlpha(newColor) : undefined,
"--new-color-contrasting": colorContrastingColor(newColor),
"--old-color": colorToHexOptionalAlpha(oldColor),
"--old-color": oldColor ? colorToHexOptionalAlpha(oldColor) : undefined,
"--old-color-contrasting": colorContrastingColor(oldColor),
"--hue-color": colorToRgbCSS(opaqueHueColor),
"--hue-color-contrasting": colorContrastingColor(opaqueHueColor),
"--opaque-color": colorToHexNoAlpha(colorOpaque(newColor) || createColor(0, 0, 0, 1)),
"--opaque-color-contrasting": colorContrastingColor(colorOpaque(newColor) || createColor(0, 0, 0, 1)),
"--opaque-color": colorToHexNoAlpha(newColor ? colorOpaque(newColor) : createColor(0, 0, 0, 1)),
"--opaque-color-contrasting": colorContrastingColor(newColor ? colorOpaque(newColor) : createColor(0, 0, 0, 1)),
}}
>
{@const hueDescription = "The shade along the spectrum of the rainbow."}
@ -514,7 +521,7 @@
<SpectrumInput
{gradient}
{disabled}
on:gradient={() => dispatch("colorOrGradient", gradient)}
on:gradient={() => dispatch("colorOrGradient", gradient ? { Gradient: gradient } : "None")}
on:activeMarkerIndexChange={gradientActiveMarkerIndexChange}
activeMarkerIndex={activeIndex}
activeMarkerIsMidpoint={activeIndexIsMidpoint}
@ -568,7 +575,7 @@
<Separator style="Related" />
<LayoutRow>
<TextInput
value={colorToHexOptionalAlpha(newColor) || "-"}
value={newColor ? colorToHexOptionalAlpha(newColor) : "-"}
{disabled}
on:commitText={({ detail }) => {
dispatch("startHistoryTransaction");
@ -680,7 +687,7 @@
<button
class="preset-color none"
{disabled}
on:click={() => setColorPreset("none")}
on:click={() => setColorPreset("None")}
data-tooltip-label="Set to No Color"
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
tabindex="0"
@ -690,7 +697,7 @@
<button
class="preset-color black"
{disabled}
on:click={() => setColorPreset("black")}
on:click={() => setColorPreset("Black")}
data-tooltip-label="Set to Black"
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
tabindex="0"
@ -699,19 +706,19 @@
<button
class="preset-color white"
{disabled}
on:click={() => setColorPreset("white")}
on:click={() => setColorPreset("White")}
data-tooltip-label="Set to White"
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
tabindex="0"
></button>
<Separator style="Related" />
<button class="preset-color pure" {disabled} on:click={setColorPresetSubtile} tabindex="-1">
{#each PURE_COLORS_GRAYABLE as [name, color, gray]}
<button class="preset-color pure" {disabled} tabindex="-1">
{#each PURE_COLORS_GRAYABLE as [preset, color, gray]}
<div
data-pure-tile={name.toLowerCase()}
on:click={() => setColorPreset(preset)}
style:--pure-color={color}
style:--pure-color-gray={gray}
data-tooltip-label={`Set to ${name}`}
data-tooltip-label={`Set to ${preset}`}
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
></div>
{/each}

View File

@ -20,7 +20,8 @@
onMount(() => {
// Focus the button which is marked as emphasized, or otherwise the first button, in the popup
const emphasizedOrFirstButton = (self?.div?.()?.querySelector("[data-emphasized]") || self?.div?.()?.querySelector("[data-text-button]") || undefined) as HTMLButtonElement | undefined;
const button = self?.div?.()?.querySelector("[data-emphasized]") || self?.div?.()?.querySelector("[data-text-button]");
const emphasizedOrFirstButton = button instanceof HTMLButtonElement ? button : undefined;
emphasizedOrFirstButton?.focus();
});
</script>
@ -28,8 +29,9 @@
<!-- TODO: Use https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog for improved accessibility -->
<FloatingMenu open={true} class="dialog" type="Dialog" direction="Center" bind:this={self} data-dialog>
<LayoutRow class="header-area">
<!-- `$dialog.icon` class exists to provide special sizing in CSS to specific icons -->
<IconLabel icon={$dialog.icon} class={$dialog.icon.toLowerCase()} />
{#if $dialog.icon}
<IconLabel icon={$dialog.icon} />
{/if}
<TextLabel>{$dialog.title}</TextLabel>
</LayoutRow>
<LayoutRow class={`content ${$dialog.title === "Demo Artwork" ? "center" : "" /* TODO: Replace this with a less hacky approach that's compatible with localization/translation */}`}>
@ -104,10 +106,13 @@
.icon-label {
width: 24px;
height: 24px;
+ .text-label {
margin-left: 12px;
}
}
.text-label {
margin-left: 12px;
line-height: 24px;
}
}
@ -134,7 +139,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;
}

View File

@ -3,7 +3,7 @@
<script lang="ts">
import { createEventDispatcher, tick, onDestroy, onMount } from "svelte";
import type { MenuListEntry, MenuDirection } from "@graphite/messages";
import type { MenuListEntry, MenuDirection } from "@graphite/../wasm/pkg/graphite_wasm";
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
@ -45,7 +45,7 @@
let openChildValue: string | undefined = undefined;
let search = "";
let reactiveEntries = entries;
let highlighted = activeEntry as MenuListEntry | undefined;
let highlighted: MenuListEntry | undefined = activeEntry;
let virtualScrollingEntriesStart = 0;
// `watchOpen` is called only when `open` is changed from outside this component
@ -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 {

View File

@ -2,7 +2,7 @@
import { createEventDispatcher, getContext, onMount } from "svelte";
import { SvelteMap } from "svelte/reactivity";
import type { FrontendNodeType } from "@graphite/messages";
import type { FrontendNodeType } from "@graphite/../wasm/pkg/graphite_wasm";
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { getContext } from "svelte";
import type { LabeledShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { LabeledShortcut } from "@graphite/messages";
import type { TooltipState } from "@graphite/state-providers/tooltip";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
@ -21,7 +21,10 @@
$: shortcut = ((shortcutJSON) => {
if (!shortcutJSON) return undefined;
try {
return JSON.parse(shortcutJSON) as LabeledShortcut;
const parsed: LabeledShortcut = JSON.parse(shortcutJSON);
if (!Array.isArray(parsed)) return undefined;
return parsed;
} catch {
return undefined;
}

View File

@ -1,6 +1,4 @@
<script lang="ts" context="module">
export type MenuType = "Popover" | "Tooltip" | "Dropdown" | "Dialog" | "Cursor";
/// Prevents the escape key from closing the parent floating menu of the given element.
/// This works by momentarily setting the `data-escape-does-not-close` attribute on the parent floating menu element.
/// After checking for the Escape key, it checks (in one `setTimeout`) for the attribute and ignores the key if it's present.
@ -21,7 +19,7 @@
<script lang="ts">
import { onMount, afterUpdate, createEventDispatcher, tick } from "svelte";
import type { MenuDirection } from "@graphite/messages";
import type { MenuDirection } from "@graphite/../wasm/pkg/graphite_wasm";
import { browserVersion } from "@graphite/utility-functions/platform";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@ -38,7 +36,7 @@
export { styleName as style };
export let styles: Record<string, string | number | undefined> = {};
export let open: boolean;
export let type: MenuType;
export let type: "Popover" | "Tooltip" | "Dropdown" | "Dialog" | "Cursor";
export let direction: MenuDirection = "Bottom";
export let windowEdgeMargin = 6;
export let scrollableY = false;
@ -309,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
@ -398,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

View File

@ -1,5 +1,5 @@
<script lang="ts">
import type { ActionShortcut } from "@graphite/messages";
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
let className = "";
export { className as class };

View File

@ -1,5 +1,5 @@
<script lang="ts">
import type { ActionShortcut } from "@graphite/messages";
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
let className = "";
export { className as class };

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { getContext, onMount, onDestroy } from "svelte";
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { Layout } from "@graphite/messages";
import { patchLayout } from "@graphite/utility-functions/widgets";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";

View File

@ -1,16 +1,16 @@
<script lang="ts">
import { getContext, onMount, onDestroy, tick } from "svelte";
import type { Color, MenuDirection, MouseCursorIcon } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { Color, FrontendMessages, MenuDirection } 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 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";
import { rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
import { setupViewportResizeObserver, cleanupViewportResizeObserver } from "@graphite/utility-functions/viewports";
import { isWidgetSpanRow } 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";
@ -21,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;
@ -35,7 +33,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 };
@ -93,7 +91,7 @@
$: canvasHeightScaledRoundedToEven = canvasHeightScaled && (canvasHeightScaled % 2 === 1 ? canvasHeightScaled + 1 : canvasHeightScaled);
$: toolShelfTotalToolsAndSeparators = ((layoutGroup) => {
if (!isWidgetSpanRow(layoutGroup)) return undefined;
if (!layoutGroup || !("Row" in layoutGroup)) return undefined;
let totalSeparators = 0;
let totalToolRowsFor1Columns = 0;
@ -108,8 +106,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 {
@ -176,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;
@ -295,10 +292,9 @@
}
// Update mouse cursor icon
export function updateMouseCursor(cursor: string) {
const mouseCursorIconCSSNames: Record<string, string> = {
export function updateMouseCursor(cursor: MouseCursorIcon) {
const mouseCursorIconCSSNames: Record<MouseCursorIcon, string> = {
Default: "default",
Alias: "alias",
None: "none",
ZoomIn: "zoom-in",
ZoomOut: "zoom-out",
@ -312,7 +308,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") {
@ -345,7 +341,7 @@
editor.handle.onChangeText(textCleaned, false);
}
export async function displayEditableTextbox(data: DisplayEditableTextbox) {
export async function displayEditableTextbox(data: MessageBody<"DisplayEditableTextbox">) {
showTextInput = true;
await tick();
@ -377,9 +373,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";
}
@ -423,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;
@ -473,7 +470,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 +508,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";
}
});
@ -615,11 +612,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()}

View File

@ -2,8 +2,8 @@
import { getContext, onMount, onDestroy, tick } from "svelte";
import { SvelteMap } from "svelte/reactivity";
import type { LayerPanelEntry, LayerStructureEntry, Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
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";
import { pasteFile } from "@graphite/utility-functions/files";

View File

@ -1,8 +1,8 @@
<script lang="ts">
import { getContext, onMount, onDestroy } from "svelte";
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { Layout } from "@graphite/messages";
import { patchLayout } from "@graphite/utility-functions/widgets";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";

View File

@ -1,10 +1,10 @@
<script lang="ts">
import { getContext, onMount, onDestroy } from "svelte";
import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { Layout } from "@graphite/messages";
import { pasteFile } from "@graphite/utility-functions/files";
import { isDesktop } from "@graphite/utility-functions/platform";
import { patchLayout } from "@graphite/utility-functions/widgets";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@ -51,7 +51,7 @@
</LayoutCol>
<LayoutCol class="bottom-message">
<TextLabel italic={true} disabled={true}>
{#if isDesktop()}
{#if isPlatformNative()}
You are testing Release Candidate 3 of the 1.0 desktop release. Please regularly check Discord for the next testing build and report issues you encounter.
{/if}
</TextLabel>

View File

@ -3,8 +3,8 @@
import { cubicInOut } from "svelte/easing";
import { fade } from "svelte/transition";
import type { FrontendGraphInput, FrontendGraphOutput, FrontendNode } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { FrontendGraphInput, FrontendGraphOutput, FrontendNode } from "@graphite/messages";
import type { DocumentState } from "@graphite/state-providers/document";
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
@ -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;
}

View File

@ -1,6 +1,5 @@
<script lang="ts">
import type { Layout, LayoutTarget } from "@graphite/messages";
import { isWidgetSpanColumn, isWidgetSpanRow, isWidgetTable, isWidgetSection } from "@graphite/utility-functions/widgets";
import type { Layout, LayoutTarget } from "@graphite/../wasm/pkg/graphite_wasm";
import WidgetSection from "@graphite/components/widgets/WidgetSection.svelte";
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
@ -14,12 +13,14 @@
</script>
{#each layout as layoutGroup}
{#if isWidgetSpanRow(layoutGroup) || isWidgetSpanColumn(layoutGroup)}
<WidgetSpan widgetData={layoutGroup} {layoutTarget} class={className} {classes} />
{:else if isWidgetSection(layoutGroup)}
<WidgetSection widgetData={layoutGroup} {layoutTarget} class={className} {classes} />
{:else if isWidgetTable(layoutGroup)}
<WidgetTable widgetData={layoutGroup} {layoutTarget} unstyled={layoutGroup.unstyled} />
{#if "Row" in layoutGroup}
<WidgetSpan direction="row" widgets={layoutGroup.Row.rowWidgets} {layoutTarget} class={className} {classes} />
{:else if "Column" in layoutGroup}
<WidgetSpan direction="column" widgets={layoutGroup.Column.columnWidgets} {layoutTarget} class={className} {classes} />
{:else if "Section" in layoutGroup}
<WidgetSection widgetData={layoutGroup.Section} {layoutTarget} class={className} {classes} />
{:else if "Table" in layoutGroup}
<WidgetTable widgetData={layoutGroup.Table} {layoutTarget} />
{/if}
{/each}

View File

@ -1,9 +1,8 @@
<script lang="ts">
import { getContext } from "svelte";
import type { LayoutTarget, WidgetSection as WidgetSectionData } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { WidgetSection as WidgetSectionData, LayoutTarget } from "@graphite/messages";
import { isWidgetSpanRow, isWidgetSection } from "@graphite/utility-functions/widgets";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
@ -62,10 +61,10 @@
{#if expanded}
<LayoutCol class="body" data-block-hover-transfer>
{#each widgetData.layout as layoutGroup}
{#if isWidgetSpanRow(layoutGroup)}
<WidgetSpan widgetData={layoutGroup} {layoutTarget} />
{:else if isWidgetSection(layoutGroup)}
<svelte:self widgetData={layoutGroup} {layoutTarget} />
{#if "Row" in layoutGroup}
<WidgetSpan direction="row" widgets={layoutGroup.Row.rowWidgets} {layoutTarget} />
{:else if "Section" in layoutGroup}
<svelte:self widgetData={layoutGroup.Section} {layoutTarget} />
{/if}
{/each}
</LayoutCol>

View File

@ -1,11 +1,10 @@
<script lang="ts">
import { getContext } from "svelte";
import type { LayoutTarget, Widget, WidgetInstance } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { LayoutTarget, WidgetInstance, WidgetPropsNames, WidgetPropsSet, WidgetTypes, WidgetSpanColumn, WidgetSpanRow } 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 NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte";
@ -30,9 +29,17 @@
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<infer K, unknown> ? 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<K extends WidgetKind> = Extract<Widget, Record<K, unknown>>[K];
// A Widget tagged enum unwrapped into a correlated [kind, props] tuple
type UnwrappedWidget = { [K in WidgetKind]: [kind: K, props: WidgetProps<K>] }[WidgetKind];
const editor = getContext<Editor>("editor");
export let widgetData: WidgetSpanRow | WidgetSpanColumn;
export let widgets: WidgetInstance[];
export let direction: "row" | "column";
export let layoutTarget: LayoutTarget;
let className = "";
@ -45,21 +52,6 @@
.flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" ");
$: 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 watchWidgets(widgetData: WidgetSpanRow | WidgetSpanColumn): WidgetInstance[] {
let widgets: WidgetInstance[] = [];
if (isWidgetSpanRow(widgetData)) widgets = widgetData.rowWidgets;
else if (isWidgetSpanColumn(widgetData)) widgets = widgetData.columnWidgets;
return widgets;
}
function widgetValueCommit(widgetIndex: number, value: unknown) {
editor.handle.widgetValueCommit(layoutTarget, widgets[widgetIndex].widgetId, value);
}
@ -72,32 +64,66 @@
editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function exclude(props: WidgetPropsSet, additional?: string[]): Record<string, any> {
const exclusions = new Set(["kind", ...(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;
}
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<string, any> | undefined;
getSlotContent?(props: WidgetPropsSet): string;
// 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),
};
}
// Svelte has no variance-safe base type for component constructors
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type SvelteComponentAny = any;
type WidgetConfig<K extends WidgetKind> = {
component: SvelteComponentAny;
getProps(props: WidgetProps<K>, widgetIndex: number): Record<string, unknown> | undefined;
getSlotContent?(props: WidgetProps<K>): string;
};
const widgetRegistry: Record<WidgetPropsNames, WidgetConfig> = {
// The union of all individual widget props types (distributed across each WidgetKind member)
type AnyWidgetProps = { [K in WidgetKind]: WidgetProps<K> }[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<string, unknown> | 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<K> assignable to WidgetResolver in the overload check.
function createWidgetResolvers(registry: { [K in WidgetKind]: WidgetConfig<K> }): Record<WidgetKind, WidgetResolver>;
function createWidgetResolvers(registry: Record<WidgetKind, WidgetResolver>): Record<WidgetKind, WidgetResolver> {
return registry;
}
const widgetResolvers = createWidgetResolvers({
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 +134,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 +143,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 +154,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,80 +209,80 @@
},
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: ({ value: _, ...rest }) => rest,
getSlotContent: (props) => props.value,
},
};
});
</script>
<div class={`widget-span ${className} ${extraClasses}`.trim()} class:narrow class:row={direction === "row"} class:column={direction === "column"}>
{#each widgets as widget, widgetIndex}
{@const config = widgetRegistry[widget.props.kind]}
{@const props = config?.getProps(widget.props, widgetIndex)}
{@const slot = config?.getSlotContent?.(widget.props)}
{#if props !== undefined && slot !== undefined}
<svelte:component this={config.component} {...props}>{slot}</svelte:component>
{:else if props !== undefined}
<svelte:component this={config.component} {...props} />
{@const unwrapped = unwrapWidget(widget)}
{#if unwrapped}
{@const { component, props, slot } = resolveWidget(unwrapped, widgetIndex)}
{#if props !== undefined && slot !== undefined}
<svelte:component this={component} {...props}>{slot}</svelte:component>
{:else if props !== undefined}
<svelte:component this={component} {...props} />
{/if}
{/if}
{/each}
</div>

Some files were not shown because too many files have changed in this diff Show More