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 16:35:04 -07:00
parent fbd2658148
commit 52d2b38a82
199 changed files with 2265 additions and 2811 deletions
+9 -3
View File
@@ -1,11 +1,17 @@
import { writable } from "svelte/store";
import type { AppWindowPlatform } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { AppWindowPlatform } from "@graphite/messages";
export function createAppWindowState(editor: Editor) {
const { subscribe, update } = writable({
platform: "Web" as AppWindowPlatform,
const { subscribe, update } = writable<{
platform: AppWindowPlatform;
maximized: boolean;
fullscreen: boolean;
viewportHolePunch: boolean;
uiScale: number;
}>({
platform: "Web",
maximized: false,
fullscreen: false,
viewportHolePunch: false,
+14 -6
View File
@@ -1,18 +1,26 @@
import { writable } from "svelte/store";
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { IconName } from "@graphite/icons";
import type { Layout } from "@graphite/messages";
import { patchLayout } from "@graphite/utility-functions/widgets";
export function createDialogState(editor: Editor) {
const { subscribe, update } = writable({
const { subscribe, update } = writable<{
visible: boolean;
title: string;
icon: IconName | undefined;
buttons: Layout;
column1: Layout;
column2: Layout;
panicDetails: string;
}>({
visible: false,
title: "",
icon: "" as IconName,
buttons: [] as Layout,
column1: [] as Layout,
column2: [] as Layout,
icon: undefined,
buttons: [],
column1: [],
column2: [],
// Special case for the crash dialog because we cannot handle button widget callbacks from Rust once the editor has panicked
panicDetails: "",
});
+15 -9
View File
@@ -1,19 +1,25 @@
import { tick } from "svelte";
import { writable } from "svelte/store";
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";
export function createDocumentState(editor: Editor) {
const state = writable({
// Layouts
toolOptionsLayout: [] as Layout,
documentBarLayout: [] as Layout,
toolShelfLayout: [] as Layout,
workingColorsLayout: [] as Layout,
nodeGraphControlBarLayout: [] as Layout,
// Graph view overlay
const state = writable<{
toolOptionsLayout: Layout;
documentBarLayout: Layout;
toolShelfLayout: Layout;
workingColorsLayout: Layout;
nodeGraphControlBarLayout: Layout;
graphViewOverlayOpen: boolean;
fadeArtwork: number;
}>({
toolOptionsLayout: [],
documentBarLayout: [],
toolShelfLayout: [],
workingColorsLayout: [],
nodeGraphControlBarLayout: [],
graphViewOverlayOpen: false,
fadeArtwork: 100,
});
+3 -5
View File
@@ -4,8 +4,7 @@ import type { Editor } from "@graphite/editor";
export function createFullscreenState(editor: Editor) {
// Experimental Keyboard API: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/keyboard
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const keyboardLockApiSupported: Readonly<boolean> = "keyboard" in navigator && (navigator as any).keyboard && "lock" in (navigator as any).keyboard;
const keyboardLockApiSupported: Readonly<boolean> = navigator.keyboard !== undefined && "lock" in navigator.keyboard;
const { subscribe, update } = writable({
windowFullscreen: false,
@@ -24,9 +23,8 @@ export function createFullscreenState(editor: Editor) {
async function enterFullscreen() {
await document.documentElement.requestFullscreen();
if (keyboardLockApiSupported) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (navigator as any).keyboard.lock(["ControlLeft", "ControlRight"]);
if (keyboardLockApiSupported && navigator.keyboard) {
await navigator.keyboard.lock(["ControlLeft", "ControlRight"]);
update((state) => {
state.keyboardLocked = true;
+43 -23
View File
@@ -1,33 +1,53 @@
import { writable } from "svelte/store";
import type { NodeGraphErrorDiagnostic, BoxSelection, FrontendClickTargets, ContextMenuInformation, FrontendNode, FrontendNodeType, WirePath } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { NodeGraphError, Box, FrontendClickTargets, ContextMenuInformation, FrontendNode, FrontendNodeType, WirePath, FrontendMessages } from "@graphite/messages";
type UpdateImportsExports = FrontendMessages["UpdateImportsExports"];
import type { MessageBody } from "@graphite/subscription-router";
export function createNodeGraphState(editor: Editor) {
const { subscribe, update } = writable({
box: undefined as Box | undefined,
clickTargets: undefined as FrontendClickTargets | undefined,
contextMenuInformation: undefined as ContextMenuInformation | undefined,
error: undefined as NodeGraphError | undefined,
layerWidths: new Map<bigint, number>(),
chainWidths: new Map<bigint, number>(),
hasLeftInputWire: new Map<bigint, boolean>(),
updateImportsExports: undefined as UpdateImportsExports | undefined,
nodes: new Map<bigint, FrontendNode>(),
visibleNodes: new Set<bigint>(),
const { subscribe, update } = writable<{
box: BoxSelection | undefined;
clickTargets: FrontendClickTargets | undefined;
contextMenuInformation: ContextMenuInformation | undefined;
error: NodeGraphErrorDiagnostic | undefined;
layerWidths: Map<bigint, number>;
chainWidths: Map<bigint, number>;
hasLeftInputWire: Map<bigint, boolean>;
updateImportsExports: MessageBody<"UpdateImportsExports"> | undefined;
nodes: Map<bigint, FrontendNode>;
visibleNodes: Set<bigint>;
/// The index is the exposed input index. The exports have a first key value of u32::MAX.
wires: new Map<bigint, Map<number, WirePath>>(),
wirePathInProgress: undefined as WirePath | undefined,
nodeDescriptions: new Map<string, string>(),
nodeTypes: [] as FrontendNodeType[],
thumbnails: new Map<bigint, string>(),
selected: [] as bigint[],
wires: Map<bigint, Map<number, WirePath>>;
wirePathInProgress: WirePath | undefined;
nodeDescriptions: Map<string, string>;
nodeTypes: FrontendNodeType[];
thumbnails: Map<bigint, string>;
selected: bigint[];
transform: { scale: number; x: number; y: number };
inSelectedNetwork: boolean;
reorderImportIndex: number | undefined;
reorderExportIndex: number | undefined;
}>({
box: undefined,
clickTargets: undefined,
contextMenuInformation: undefined,
error: undefined,
layerWidths: new Map(),
chainWidths: new Map(),
hasLeftInputWire: new Map(),
updateImportsExports: undefined,
nodes: new Map(),
visibleNodes: new Set(),
wires: new Map(),
wirePathInProgress: undefined,
nodeDescriptions: new Map(),
nodeTypes: [],
thumbnails: new Map(),
selected: [],
transform: { scale: 1, x: 0, y: 0 },
inSelectedNetwork: true,
reorderImportIndex: undefined as number | undefined,
reorderExportIndex: undefined as number | undefined,
reorderImportIndex: undefined,
reorderExportIndex: undefined,
});
function closeContextMenu() {
@@ -148,7 +168,7 @@ export function createNodeGraphState(editor: Editor) {
});
editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphTransform", (data) => {
update((state) => {
state.transform = data.transform;
state.transform = { scale: data.scale, x: data.translation[0], y: data.translation[1] };
return state;
});
});
+10 -3
View File
@@ -1,14 +1,21 @@
import { writable } from "svelte/store";
import type { OpenDocument } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { OpenDocument } from "@graphite/messages";
import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files";
import { rasterizeSVG } from "@graphite/utility-functions/rasterization";
export function createPortfolioState(editor: Editor) {
const { subscribe, update } = writable({
const { subscribe, update } = writable<{
unsaved: boolean;
documents: OpenDocument[];
activeDocumentIndex: number;
dataPanelOpen: boolean;
propertiesPanelOpen: boolean;
layersPanelOpen: boolean;
}>({
unsaved: false,
documents: [] as OpenDocument[],
documents: [],
activeDocumentIndex: 0,
dataPanelOpen: false,
propertiesPanelOpen: true,
+13 -6
View File
@@ -1,19 +1,26 @@
import { writable } from "svelte/store";
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { ActionShortcut } from "@graphite/messages";
import { operatingSystem } from "@graphite/utility-functions/platform";
const SHOW_TOOLTIP_DELAY_MS = 500;
export function createTooltipState(editor: Editor) {
const { subscribe, update } = writable({
const { subscribe, update } = writable<{
visible: boolean;
element: Element | undefined;
position: { x: number; y: number };
shiftClickShortcut: ActionShortcut | undefined;
altClickShortcut: ActionShortcut | undefined;
fullscreenShortcut: ActionShortcut | undefined;
}>({
visible: false,
element: undefined as Element | undefined,
element: undefined,
position: { x: 0, y: 0 },
shiftClickShortcut: undefined as ActionShortcut | undefined,
altClickShortcut: undefined as ActionShortcut | undefined,
fullscreenShortcut: undefined as ActionShortcut | undefined,
shiftClickShortcut: undefined,
altClickShortcut: undefined,
fullscreenShortcut: undefined,
});
let tooltipTimeout: ReturnType<typeof setTimeout> | undefined = undefined;