mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 02:48:11 +08:00
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:
+16
-15
@@ -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.
|
||||
|
||||
@@ -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." } }],
|
||||
|
||||
Generated
+48
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
// Destroy the WASM editor handle
|
||||
// Destroy the Wasm editor handle
|
||||
editor?.handle.free();
|
||||
});
|
||||
</script>
|
||||
|
||||
+12
-12
@@ -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 });`.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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/`
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutTarget, WidgetTable as WidgetTableData } from "@graphite/messages";
|
||||
import type { LayoutTarget, WidgetTable } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
|
||||
|
||||
export let widgetData: WidgetTableData;
|
||||
export let widgetData: WidgetTable;
|
||||
export let layoutTarget: LayoutTarget;
|
||||
export let unstyled = false;
|
||||
|
||||
$: columns = widgetData.tableWidgets.length > 0 ? widgetData.tableWidgets[0].length : 0;
|
||||
</script>
|
||||
|
||||
<table class:unstyled>
|
||||
<table class:unstyled={widgetData.unstyled}>
|
||||
<tbody>
|
||||
{#each widgetData.tableWidgets as row}
|
||||
<tr>
|
||||
{#each row as cell}
|
||||
<td colspan={row.length < columns ? columns - row.length + 1 : undefined}>
|
||||
<WidgetSpan widgetData={{ rowWidgets: [cell] }} {layoutTarget} narrow={true} />
|
||||
<WidgetSpan direction="row" widgets={[cell]} {layoutTarget} narrow={true} />
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { IconName, IconSize } from "@graphite/icons";
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { IMAGE_BASE64_STRINGS } from "@graphite/utility-functions/images";
|
||||
|
||||
let className = "";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { FrontendGraphDataType, ActionShortcut } from "@graphite/messages";
|
||||
import type { FrontendGraphDataType, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { MenuDirection, ActionShortcut, Layout, LayoutTarget } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { IconName, PopoverButtonStyle } from "@graphite/icons";
|
||||
|
||||
import type { MenuDirection, ActionShortcut, Layout, LayoutTarget } from "@graphite/messages";
|
||||
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { MenuListEntry, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { IconName } from "@graphite/icons";
|
||||
import type { MenuListEntry, ActionShortcut } from "@graphite/messages";
|
||||
|
||||
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
|
||||
import ConditionalWrapper from "@graphite/components/layout/ConditionalWrapper.svelte";
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
|
||||
// Focus the target so that keyboard inputs are sent to the dropdown
|
||||
(e.target as HTMLElement | undefined)?.focus();
|
||||
if (e.target instanceof HTMLElement) e.target.focus();
|
||||
|
||||
// Open the menu list floating menu
|
||||
if (self) self.open = true;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { IconName } from "@graphite/icons";
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
// Content
|
||||
export let checked = false;
|
||||
export let icon: IconName = "Checkmark";
|
||||
export let icon: IconName | undefined = undefined;
|
||||
export let forLabel: bigint | undefined = undefined;
|
||||
export let disabled = false;
|
||||
// Tooltips
|
||||
@@ -23,7 +23,7 @@
|
||||
let inputElement: HTMLInputElement | undefined;
|
||||
|
||||
$: id = forLabel !== undefined ? String(forLabel) : backupId;
|
||||
$: displayIcon = (!checked && icon === "Checkmark" ? "Empty12px" : icon) as IconName;
|
||||
$: displayIcon = !checked && (!icon || icon === "Checkmark") ? "Empty12px" : icon || "Checkmark";
|
||||
|
||||
export function isChecked() {
|
||||
return checked;
|
||||
@@ -34,8 +34,8 @@
|
||||
}
|
||||
|
||||
function toggleCheckboxFromLabel(e: KeyboardEvent) {
|
||||
const target = (e.target || undefined) as HTMLLabelElement | undefined;
|
||||
const previousSibling = (target?.previousSibling || undefined) as HTMLInputElement | undefined;
|
||||
const target = e.target instanceof HTMLLabelElement ? e.target : undefined;
|
||||
const previousSibling = target?.previousSibling instanceof HTMLInputElement ? target.previousSibling : undefined;
|
||||
previousSibling?.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { FillChoice, MenuDirection, ActionShortcut } from "@graphite/messages";
|
||||
import type { Color } from "@graphite/messages";
|
||||
import { contrastingOutlineFactor, isColor, isGradient, colorToHexOptionalAlpha, gradientToLinearGradientCSS } from "@graphite/utility-functions/colors";
|
||||
import type { FillChoice, MenuDirection, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { contrastingOutlineFactor, fillChoiceColor, fillChoiceGradientStops, colorToHexOptionalAlpha, gradientToLinearGradientCSS } from "@graphite/utility-functions/colors";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
@@ -27,9 +26,15 @@
|
||||
|
||||
$: outlineFactor = contrastingOutlineFactor(value, ["--color-1-nearblack", "--color-3-darkgray"], 0.01);
|
||||
$: outlined = outlineFactor > 0.0001;
|
||||
$: chosenGradient = isGradient(value) ? gradientToLinearGradientCSS(value) : `linear-gradient(${colorToHexOptionalAlpha(value)}, ${colorToHexOptionalAlpha(value)})`;
|
||||
$: none = isColor(value) ? value.none : false;
|
||||
$: transparency = isGradient(value) ? value.color.some((color: Color) => color.alpha < 1) : value.alpha < 1;
|
||||
$: gradientStops = fillChoiceGradientStops(value);
|
||||
$: solidColor = fillChoiceColor(value);
|
||||
$: chosenGradient = gradientStops
|
||||
? gradientToLinearGradientCSS(gradientStops)
|
||||
: solidColor
|
||||
? `linear-gradient(${colorToHexOptionalAlpha(solidColor)}, ${colorToHexOptionalAlpha(solidColor)})`
|
||||
: undefined;
|
||||
$: none = value === "None";
|
||||
$: transparency = gradientStops ? gradientStops.color.some((color) => color.alpha < 1) : solidColor ? solidColor.alpha < 1 : false;
|
||||
</script>
|
||||
|
||||
<LayoutCol class="color-button" classes={{ open, disabled, narrow, none, transparency, outlined, "direction-top": menuDirection === "Top" }} {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { Curve, CurveManipulatorGroup, ActionShortcut } from "@graphite/messages";
|
||||
import type { Curve, CurveManipulatorGroup, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { clamp } from "@graphite/utility-functions/math";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { MenuListEntry, ActionShortcut } from "@graphite/messages";
|
||||
import type { MenuListEntry, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const DASH_ENTRY = { value: "", label: "-" };
|
||||
const DASH_ENTRY: MenuListEntry = {
|
||||
value: "",
|
||||
label: "-",
|
||||
icon: undefined,
|
||||
disabled: false,
|
||||
children: [],
|
||||
childrenHash: 0n,
|
||||
font: undefined,
|
||||
tooltipLabel: "",
|
||||
tooltipDescription: "",
|
||||
tooltipShortcut: undefined,
|
||||
};
|
||||
|
||||
const dispatch = createEventDispatcher<{ selectedIndex: number; hoverInEntry: number; hoverOutEntry: number }>();
|
||||
|
||||
@@ -49,13 +60,13 @@
|
||||
}
|
||||
|
||||
// Called only when `selectedIndex` is changed from outside this component
|
||||
function watchSelectedIndex(_?: typeof selectedIndex) {
|
||||
function watchSelectedIndex(_: typeof selectedIndex) {
|
||||
activeEntrySkipWatcher = true;
|
||||
activeEntry = makeActiveEntry();
|
||||
}
|
||||
|
||||
// Called only when `entries` is changed from outside this component
|
||||
function watchEntries(_?: typeof entries) {
|
||||
function watchEntries(_: typeof entries) {
|
||||
activeEntrySkipWatcher = true;
|
||||
activeEntry = makeActiveEntry();
|
||||
}
|
||||
@@ -102,7 +113,7 @@
|
||||
}
|
||||
|
||||
function unFocusDropdownBox(e: FocusEvent) {
|
||||
const blurTarget = (e.target as HTMLDivElement | undefined)?.closest("[data-dropdown-input]") || undefined;
|
||||
const blurTarget = (e.target instanceof Element ? e.target.closest("[data-dropdown-input]") : undefined) || undefined;
|
||||
if (blurTarget !== self?.div?.()) open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, onDestroy, getContext } from "svelte";
|
||||
|
||||
import { evaluateMathExpression } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { evaluateMathExpression, isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { NumberInputMode, NumberInputIncrementBehavior, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/io-managers/input";
|
||||
import type { NumberInputMode, NumberInputIncrementBehavior, ActionShortcut } from "@graphite/messages";
|
||||
import { browserVersion, isDesktop } from "@graphite/utility-functions/platform";
|
||||
import { browserVersion } from "@graphite/utility-functions/platform";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.svelte";
|
||||
@@ -43,8 +43,8 @@
|
||||
export let isInteger = false;
|
||||
/// `incrementBehavior` is only applicable with a `mode` of "Increment".
|
||||
/// "Add"/"Multiply": The value is added or multiplied by `step`.
|
||||
/// "None": the increment arrows are not shown.
|
||||
/// "Callback": the functions `incrementCallbackIncrease` and `incrementCallbackDecrease` call custom behavior.
|
||||
/// "None": the increment arrows are not shown.
|
||||
export let incrementBehavior: NumberInputIncrementBehavior = "Add";
|
||||
export let displayDecimalPlaces = 2;
|
||||
export let unit = "";
|
||||
@@ -364,7 +364,7 @@
|
||||
// Because "mousemove" (and similarly, the "pointermove" event we use) is defined as not being a user-initiated "engagement gesture" event,
|
||||
// Safari never lets us to enter pointer lock while the mouse button is held down and we are awaiting movement to begin dragging the slider.
|
||||
const isSafari = browserVersion().toLowerCase().includes("safari");
|
||||
const usePointerLock = !isSafari && !isDesktop();
|
||||
const usePointerLock = !isSafari && !isPlatformNative();
|
||||
|
||||
// On Safari, we use a workaround involving an alternative strategy where we hide the cursor while it's within the web page
|
||||
// (but we can't hide it when it ventures outside the page), taking advantage of a separate (helpful) Safari bug where it
|
||||
@@ -377,7 +377,7 @@
|
||||
|
||||
// Enter dragging state
|
||||
if (usePointerLock) target.requestPointerLock();
|
||||
if (isDesktop()) {
|
||||
if (isPlatformNative()) {
|
||||
editor.handle.appWindowPointerLock();
|
||||
}
|
||||
initialValueBeforeDragging = value;
|
||||
@@ -427,11 +427,11 @@
|
||||
}
|
||||
ignoredFirstMovement = true;
|
||||
};
|
||||
// On desktop we don't get `pointermove` events while in pointer lock (cef doesn't support pointer lock).
|
||||
// On desktop we don't get `pointermove` events while in pointer lock (CEF doesn't support pointer lock).
|
||||
// We have to listen for our custom `pointerlockmove` events instead.
|
||||
const pointerLockMove = (e: Event) => {
|
||||
if (ignoredFirstMovement && initialValueBeforeDragging !== undefined && e instanceof CustomEvent) {
|
||||
const delta = (e.detail as { x: number }).x;
|
||||
const pointerLockMove = ({ detail }: WindowEventMap["pointerlockmove"]) => {
|
||||
if (ignoredFirstMovement && initialValueBeforeDragging !== undefined) {
|
||||
const delta = detail.x;
|
||||
pointerLockMoveUpdate(delta, shiftKeyDown, ctrlKeyDown, initialValueBeforeDragging);
|
||||
}
|
||||
ignoredFirstMovement = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { RadioEntryData } from "@graphite/messages";
|
||||
import type { RadioEntryData } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { ReferencePoint, ActionShortcut } from "@graphite/messages";
|
||||
import type { ReferencePoint, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
const dispatch = createEventDispatcher<{ value: ReferencePoint }>();
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
<script lang="ts" context="module">
|
||||
export type RulerDirection = "Horizontal" | "Vertical";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
@@ -10,6 +6,8 @@
|
||||
const MINOR_MARK_THICKNESS = 6;
|
||||
const MICRO_MARK_THICKNESS = 3;
|
||||
|
||||
type RulerDirection = "Horizontal" | "Vertical";
|
||||
|
||||
export let direction: RulerDirection = "Vertical";
|
||||
export let origin: number;
|
||||
export let numberInterval: number;
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
<script lang="ts" context="module">
|
||||
export type ScrollbarDirection = "Horizontal" | "Vertical";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
@@ -21,7 +17,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher<{ trackShift: number; thumbPosition: number; thumbDragStart: undefined; thumbDragEnd: undefined; thumbDragAbort: undefined }>();
|
||||
|
||||
export let direction: ScrollbarDirection = "Vertical";
|
||||
export let direction: "Horizontal" | "Vertical" = "Vertical";
|
||||
export let thumbPosition = 0.5;
|
||||
export let thumbLength = 0.5;
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
|
||||
import { evaluateGradientAtPosition } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Color, Gradient } from "@graphite/messages";
|
||||
import { createColor, colorToHexOptionalAlpha, colorToRgbCSS, gradientFirstColor, gradientLastColor, gradientToLinearGradientCSS } from "@graphite/utility-functions/colors";
|
||||
import type { Color, GradientStops } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { colorToHexOptionalAlpha, colorToRgbCSS, gradientFirstColor, gradientLastColor, gradientToLinearGradientCSS } from "@graphite/utility-functions/colors";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
@@ -17,11 +17,11 @@
|
||||
const BUTTON_LEFT = 0;
|
||||
const BUTTON_RIGHT = 2;
|
||||
|
||||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: { activeMarkerIndex: number | undefined; activeMarkerIsMidpoint: boolean }; gradient: Gradient; dragging: boolean }>();
|
||||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: { activeMarkerIndex: number | undefined; activeMarkerIsMidpoint: boolean }; gradient: GradientStops; dragging: boolean }>();
|
||||
|
||||
export let gradient: Gradient;
|
||||
export let gradient: GradientStops;
|
||||
export let disabled = false;
|
||||
export let activeMarkerIndex = 0 as number | undefined;
|
||||
export let activeMarkerIndex: number | undefined = 0;
|
||||
export let activeMarkerIsMidpoint = false;
|
||||
// export let disabled = false;
|
||||
// export let tooltipLabel: string | undefined = undefined;
|
||||
@@ -114,9 +114,7 @@
|
||||
if (index === -1) index = gradient.position.length;
|
||||
|
||||
// Determine the color of the new stop by evaluating the gradient at the position of the new stop
|
||||
type ReturnedColor = { red: number; green: number; blue: number; alpha: number };
|
||||
const evaluated = evaluateGradientAtPosition(position, new Float64Array(gradient.position), new Float64Array(gradient.midpoint), gradient.color) as ReturnedColor;
|
||||
const color = createColor(evaluated.red, evaluated.green, evaluated.blue, evaluated.alpha);
|
||||
const color: Color = evaluateGradientAtPosition(position, new Float64Array(gradient.position), new Float64Array(gradient.midpoint), gradient.color);
|
||||
|
||||
// Insert the new stop into the gradient
|
||||
gradient.position.splice(index, 0, position);
|
||||
@@ -243,7 +241,7 @@
|
||||
dispatch("gradient", gradient);
|
||||
}
|
||||
|
||||
function toMarkers(gradient: Gradient): { position: number; midpoint: number; color: Color }[] {
|
||||
function toMarkers(gradient: GradientStops): { position: number; midpoint: number; color: Color }[] {
|
||||
return gradient.position.map((position, i) => ({
|
||||
position,
|
||||
midpoint: gradient.midpoint[i],
|
||||
@@ -251,7 +249,7 @@
|
||||
}));
|
||||
}
|
||||
|
||||
function toMidpoints(gradient: Gradient): number[] {
|
||||
function toMidpoints(gradient: GradientStops): number[] {
|
||||
if (gradient.position.length < 2) return [];
|
||||
|
||||
return gradient.midpoint.slice(0, -1).map((midpoint, i) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.svelte";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.svelte";
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { Color } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { Color } from "@graphite/messages";
|
||||
import { isColor, colorToRgbaCSS } from "@graphite/utility-functions/colors";
|
||||
import { fillChoiceColor, colorToRgbaCSS } from "@graphite/utility-functions/colors";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
@@ -43,8 +43,11 @@
|
||||
<ColorPicker
|
||||
open={primaryOpen}
|
||||
on:open={({ detail }) => (primaryOpen = detail)}
|
||||
colorOrGradient={primary}
|
||||
on:colorOrGradient={({ detail }) => isColor(detail) && primaryColorChanged(detail)}
|
||||
colorOrGradient={{ Solid: primary }}
|
||||
on:colorOrGradient={({ detail }) => {
|
||||
const color = fillChoiceColor(detail);
|
||||
if (color) primaryColorChanged(color);
|
||||
}}
|
||||
direction="Right"
|
||||
/>
|
||||
</LayoutRow>
|
||||
@@ -53,8 +56,11 @@
|
||||
<ColorPicker
|
||||
open={secondaryOpen}
|
||||
on:open={({ detail }) => (secondaryOpen = detail)}
|
||||
colorOrGradient={secondary}
|
||||
on:colorOrGradient={({ detail }) => isColor(detail) && secondaryColorChanged(detail)}
|
||||
colorOrGradient={{ Solid: secondary }}
|
||||
on:colorOrGradient={({ detail }) => {
|
||||
const color = fillChoiceColor(detail);
|
||||
if (color) secondaryColorChanged(color);
|
||||
}}
|
||||
direction="Right"
|
||||
/>
|
||||
</LayoutRow>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { ICONS, ICON_SVG_STRINGS } from "@graphite/icons";
|
||||
import type { IconName } from "@graphite/icons";
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { SeparatorDirection, SeparatorStyle } from "@graphite/messages";
|
||||
import type { SeparatorDirection, SeparatorStyle } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
// Content
|
||||
export let direction: SeparatorDirection = "Horizontal";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { ActionShortcut, Key, LabeledShortcut, MouseMotion } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { IconName } from "@graphite/icons";
|
||||
import type { ActionShortcut, KeyRaw, LabeledShortcut, MouseMotion } from "@graphite/messages";
|
||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
@@ -16,7 +16,7 @@
|
||||
if (typeof labeledKeyOrMouseMotion === "string") return { mouseMotion: labeledKeyOrMouseMotion };
|
||||
|
||||
// `key` is the name of the `Key` enum in Rust, while `label` is the localized string to display (if it doesn't become an icon)
|
||||
let key = labeledKeyOrMouseMotion.key;
|
||||
let key: Key | "Option" = labeledKeyOrMouseMotion.key;
|
||||
const label = labeledKeyOrMouseMotion.label;
|
||||
|
||||
// Replace Alt and Accel keys with their Mac-specific equivalents
|
||||
@@ -57,7 +57,7 @@
|
||||
return consolidatedList;
|
||||
}
|
||||
|
||||
function keyboardHintIcon(input: KeyRaw): IconName | undefined {
|
||||
function keyboardHintIcon(input: Key | "Option"): IconName | undefined {
|
||||
switch (input) {
|
||||
case "ArrowDown":
|
||||
return "KeyboardArrowDown";
|
||||
@@ -89,7 +89,20 @@
|
||||
}
|
||||
|
||||
function mouseHintIcon(input: MouseMotion): IconName {
|
||||
return `MouseHint${input}` as IconName;
|
||||
return {
|
||||
None: "MouseHintNone" as const,
|
||||
Lmb: "MouseHintLmb" as const,
|
||||
Rmb: "MouseHintRmb" as const,
|
||||
Mmb: "MouseHintMmb" as const,
|
||||
ScrollUp: "MouseHintScrollUp" as const,
|
||||
ScrollDown: "MouseHintScrollDown" as const,
|
||||
Drag: "MouseHintDrag" as const,
|
||||
LmbDouble: "MouseHintLmbDouble" as const,
|
||||
LmbDrag: "MouseHintLmbDrag" as const,
|
||||
RmbDrag: "MouseHintRmbDrag" as const,
|
||||
RmbDouble: "MouseHintRmbDouble" as const,
|
||||
MmbDrag: "MouseHintMmbDrag" as const,
|
||||
}[input];
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
|
||||
import type { ActionShortcut } from "@graphite/messages";
|
||||
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { AppWindowState } from "@graphite/state-providers/app-window";
|
||||
import type { DialogState } from "@graphite/state-providers/dialog";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
import { isDesktop } from "@graphite/utility-functions/platform";
|
||||
|
||||
import Dialog from "@graphite/components/floating-menus/Dialog.svelte";
|
||||
import Tooltip from "@graphite/components/floating-menus/Tooltip.svelte";
|
||||
@@ -31,7 +31,7 @@
|
||||
{#if $tooltip.visible}
|
||||
<Tooltip />
|
||||
{/if}
|
||||
{#if isDesktop() && new Date() > new Date("2026-03-15")}
|
||||
{#if isPlatformNative() && new Date() > new Date("2026-03-15")}
|
||||
<LayoutCol class="release-candidate-expiry">
|
||||
<TextLabel>
|
||||
<p>
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
<script lang="ts" context="module">
|
||||
<script lang="ts">
|
||||
import { getContext, tick } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import Data from "@graphite/components/panels/Data.svelte";
|
||||
import Document from "@graphite/components/panels/Document.svelte";
|
||||
import Layers from "@graphite/components/panels/Layers.svelte";
|
||||
import Properties from "@graphite/components/panels/Properties.svelte";
|
||||
import Welcome from "@graphite/components/panels/Welcome.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
type PanelType = keyof typeof PANEL_COMPONENTS;
|
||||
|
||||
const PANEL_COMPONENTS = {
|
||||
Welcome,
|
||||
@@ -12,20 +22,6 @@
|
||||
Properties,
|
||||
Data,
|
||||
};
|
||||
type PanelType = keyof typeof PANEL_COMPONENTS;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { getContext, tick } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { isEventSupported } from "@graphite/utility-functions/platform";
|
||||
|
||||
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";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const BUTTON_LEFT = 0;
|
||||
const BUTTON_MIDDLE = 1;
|
||||
|
||||
@@ -80,16 +76,6 @@
|
||||
closeAction?.(tabIndex);
|
||||
}
|
||||
}}
|
||||
on:mouseup={(e) => {
|
||||
// Middle mouse button click fallback for Safari:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#browser_compatibility
|
||||
// The downside of using mouseup is that the mousedown didn't have to originate in the same element.
|
||||
// A possible future improvement could save the target element during mousedown and check if it's the same here.
|
||||
if (!isEventSupported("auxclick") && e.button === BUTTON_MIDDLE) {
|
||||
e.stopPropagation();
|
||||
closeAction?.(tabIndex);
|
||||
}
|
||||
}}
|
||||
bind:this={tabElements[tabIndex]}
|
||||
>
|
||||
<LayoutRow class="name">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } 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 LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } 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 type { AppWindowState } from "@graphite/state-providers/app-window";
|
||||
import type { FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
@@ -11,7 +12,6 @@
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
import { isDesktop } from "/src/utility-functions/platform";
|
||||
|
||||
const appWindow = getContext<AppWindowState>("appWindow");
|
||||
const editor = getContext<Editor>("editor");
|
||||
@@ -20,8 +20,8 @@
|
||||
|
||||
let menuBarLayout: Layout = [];
|
||||
|
||||
$: showFullscreenButton = $appWindow.platform === "Web" || $fullscreen.windowFullscreen || (isDesktop() && $appWindow.fullscreen);
|
||||
$: isFullscreen = isDesktop() ? $appWindow.fullscreen : $fullscreen.windowFullscreen;
|
||||
$: showFullscreenButton = $appWindow.platform === "Web" || $fullscreen.windowFullscreen || (isPlatformNative() && $appWindow.fullscreen);
|
||||
$: isFullscreen = isPlatformNative() ? $appWindow.fullscreen : $fullscreen.windowFullscreen;
|
||||
// On Mac, the menu bar height needs to be scaled by the inverse of the UI scale to fit its native window buttons
|
||||
$: height = $appWindow.platform === "Mac" ? 28 * (1 / $appWindow.uiScale) : 28;
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
: undefined}
|
||||
tooltipShortcut={$tooltip.fullscreenShortcut}
|
||||
on:click={() => {
|
||||
if (isDesktop()) editor.handle.appWindowFullscreen();
|
||||
if (isPlatformNative()) editor.handle.appWindowFullscreen();
|
||||
else ($fullscreen.windowFullscreen ? fullscreen.exitFullscreen : fullscreen.enterFullscreen)();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { OpenDocument } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { OpenDocument } from "@graphite/messages";
|
||||
import type { PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
@@ -18,9 +18,9 @@
|
||||
/* └─ */ details: 20,
|
||||
/* ├─ */ properties: 45,
|
||||
/* └─ */ layers: 55,
|
||||
};
|
||||
} as const;
|
||||
|
||||
let panelSizes = PANEL_SIZES;
|
||||
let panelSizes: Record<string, number> = PANEL_SIZES;
|
||||
let documentPanel: Panel | undefined;
|
||||
let gutterResizeRestore: [number, number] | undefined = undefined;
|
||||
let pointerCaptureId: number | undefined = undefined;
|
||||
@@ -40,15 +40,19 @@
|
||||
const portfolio = getContext<PortfolioState>("portfolio");
|
||||
|
||||
function resizePanel(e: PointerEvent) {
|
||||
const gutter = (e.target || undefined) as HTMLDivElement | undefined;
|
||||
const nextSibling = (gutter?.nextElementSibling || undefined) as HTMLDivElement | undefined;
|
||||
const prevSibling = (gutter?.previousElementSibling || undefined) as HTMLDivElement | undefined;
|
||||
const parentElement = (gutter?.parentElement || undefined) as HTMLDivElement | undefined;
|
||||
const gutter = e.target;
|
||||
if (!(gutter instanceof HTMLDivElement)) return;
|
||||
|
||||
const nextSiblingName = (nextSibling?.getAttribute("data-subdivision-name") || undefined) as keyof typeof PANEL_SIZES;
|
||||
const prevSiblingName = (prevSibling?.getAttribute("data-subdivision-name") || undefined) as keyof typeof PANEL_SIZES;
|
||||
const nextSibling = gutter.nextElementSibling;
|
||||
const prevSibling = gutter.previousElementSibling;
|
||||
|
||||
if (!gutter || !nextSibling || !prevSibling || !parentElement || !nextSiblingName || !prevSiblingName) return;
|
||||
const parentElement = gutter.parentElement;
|
||||
if (!(nextSibling instanceof HTMLDivElement) || !(prevSibling instanceof HTMLDivElement) || !(parentElement instanceof HTMLDivElement)) return;
|
||||
|
||||
const nextSiblingName = nextSibling.getAttribute("data-subdivision-name") || undefined;
|
||||
const prevSiblingName = prevSibling.getAttribute("data-subdivision-name") || undefined;
|
||||
|
||||
if (!nextSiblingName || !prevSiblingName || !(nextSiblingName in PANEL_SIZES) || !(prevSiblingName in PANEL_SIZES)) return;
|
||||
|
||||
// Are we resizing horizontally?
|
||||
const isHorizontal = gutter.getAttribute("data-gutter-horizontal") !== null;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// import { panicProxy } from "@graphite/utility-functions/panic-proxy";
|
||||
|
||||
import { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import init, { wasmMemory, receiveNativeMessage } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { FrontendMessages } from "@graphite/messages";
|
||||
import init, { EditorHandle, wasmMemory, receiveNativeMessage } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { FrontendMessage } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { createSubscriptionRouter } from "@graphite/subscription-router";
|
||||
import type { SubscriptionRouter } from "@graphite/subscription-router";
|
||||
import type { MessageName, SubscriptionRouter } from "@graphite/subscription-router";
|
||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
||||
|
||||
// TODO: Remove `raw`, split out `subscriptions`, and unwrap the remaining `handle` so `EditorHandle` can replace `Editor` and then it can also be renamed to `Editor` to fully remove `EditorHandle`.
|
||||
@@ -29,10 +28,8 @@ export async function initWasm() {
|
||||
}
|
||||
|
||||
wasmImport = await wasmMemory();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).imageCanvases = {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).receiveNativeMessage = receiveNativeMessage;
|
||||
window.imageCanvases = {};
|
||||
window.receiveNativeMessage = receiveNativeMessage;
|
||||
}
|
||||
|
||||
// Should be called after running `initWasm()` and its promise resolving.
|
||||
@@ -46,7 +43,7 @@ export function createEditor(): Editor {
|
||||
const randomSeed = BigInt(randomSeedFloat);
|
||||
|
||||
// Handle: object containing many functions from `editor_api.rs` that are part of the `EditorHandle` struct (generated by wasm-bindgen)
|
||||
const handle = EditorHandle.create(operatingSystem(), randomSeed, (messageType: keyof FrontendMessages, messageData: Record<string, unknown>) => {
|
||||
const handle = EditorHandle.create(operatingSystem(), randomSeed, (messageType: MessageName, messageData: FrontendMessage) => {
|
||||
// This callback is called by Wasm when a FrontendMessage is received from the Wasm wrapper `EditorHandle`
|
||||
subscriptions.handleFrontendMessage(messageType, messageData);
|
||||
});
|
||||
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable @typescript-eslint/consistent-type-definitions */
|
||||
|
||||
// Graphite's custom properties added to the global `window` object
|
||||
interface Window {
|
||||
imageCanvases: Record<string, HTMLCanvasElement>;
|
||||
receiveNativeMessage?: (buffer: ArrayBuffer) => void;
|
||||
}
|
||||
|
||||
// Graphite's custom "pointerlockmove" event dispatched by input.ts for pointer lock in the CEF desktop app
|
||||
interface WindowEventMap {
|
||||
pointerlockmove: CustomEvent<{ x: number; y: number }>;
|
||||
}
|
||||
|
||||
// Experimental Keyboard API: https://developer.mozilla.org/en-US/docs/Web/API/Keyboard
|
||||
interface Navigator {
|
||||
keyboard?: Keyboard;
|
||||
}
|
||||
interface Keyboard {
|
||||
lock(keyCodes?: string[]): Promise<void>;
|
||||
unlock(): void;
|
||||
getLayoutMap(): Promise<KeyboardLayoutMap>;
|
||||
}
|
||||
interface KeyboardLayoutMap {
|
||||
entries(): IterableIterator<[string, string]>;
|
||||
get(key: string): string | undefined;
|
||||
has(key: string): boolean;
|
||||
readonly size: number;
|
||||
}
|
||||
|
||||
// Experimental EyeDropper API: https://developer.mozilla.org/en-US/docs/Web/API/EyeDropper
|
||||
interface Window {
|
||||
EyeDropper?: typeof EyeDropper;
|
||||
}
|
||||
declare class EyeDropper {
|
||||
constructor();
|
||||
open(options?: { signal?: AbortSignal }): Promise<{ sRGBHex: string }>;
|
||||
}
|
||||
|
||||
// Experimental "clipboard-read" Permission: https://developer.mozilla.org/en-US/docs/Web/API/Permissions
|
||||
interface Permissions {
|
||||
query(permissionDesc: { name: "clipboard-read" }): Promise<PermissionStatus>;
|
||||
}
|
||||
|
||||
// Non-standard Stack Trace Limit API: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stackTraceLimit
|
||||
interface ErrorConstructor {
|
||||
stackTraceLimit?: number;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export function createFontsManager(editor: Editor) {
|
||||
// Subscribe to process backend events
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerFontCatalogLoad", async () => {
|
||||
const response = await fetch(FONT_LIST_API);
|
||||
const fontListResponse = (await response.json()) as { items: ApiResponse };
|
||||
const fontListResponse: { items: ApiResponse } = await response.json();
|
||||
const fontListData = fontListResponse.items;
|
||||
|
||||
const catalog = fontListData.map((font) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { DialogState } from "@graphite/state-providers/dialog";
|
||||
import type { DocumentState } from "@graphite/state-providers/document";
|
||||
@@ -7,7 +8,7 @@ import type { FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import type { PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
import { pasteFile } from "@graphite/utility-functions/files";
|
||||
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
|
||||
import { isDesktop, operatingSystem } from "@graphite/utility-functions/platform";
|
||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
||||
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
||||
import { stripIndents } from "@graphite/utility-functions/strip-indents";
|
||||
|
||||
@@ -28,11 +29,12 @@ type EventListenerTarget = {
|
||||
};
|
||||
|
||||
export function createInputManager(editor: Editor, dialog: DialogState, portfolio: PortfolioState, document: DocumentState, fullscreen: FullscreenState): () => void {
|
||||
const app = window.document.querySelector("[data-app-container]") as HTMLElement | undefined;
|
||||
const appElement = window.document.querySelector("[data-app-container]");
|
||||
const app = appElement instanceof HTMLElement ? appElement : null;
|
||||
app?.focus();
|
||||
|
||||
let viewportPointerInteractionOngoing = false;
|
||||
let textToolInteractiveInputElement = undefined as undefined | HTMLDivElement;
|
||||
let textToolInteractiveInputElement: HTMLDivElement | undefined = undefined;
|
||||
let canvasFocused = true;
|
||||
let inPointerLock = false;
|
||||
const shakeSamples: { x: number; y: number; time: number }[] = [];
|
||||
@@ -40,8 +42,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
|
||||
// Event listeners
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const listeners: { target: EventListenerTarget; eventName: EventName; action: (event: any) => void; options?: AddEventListenerOptions }[] = [
|
||||
const listeners: { target: EventListenerTarget; eventName: EventName; action(event: Event): void; options?: AddEventListenerOptions }[] = [
|
||||
{ target: window, eventName: "beforeunload", action: (e: BeforeUnloadEvent) => onBeforeUnload(e) },
|
||||
{ target: window, eventName: "keyup", action: (e: KeyboardEvent) => onKeyUp(e) },
|
||||
{ target: window, eventName: "keydown", action: (e: KeyboardEvent) => onKeyDown(e) },
|
||||
@@ -83,9 +84,9 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
const accelKey = operatingSystem() === "Mac" ? e.metaKey : e.ctrlKey;
|
||||
|
||||
// Cut, copy, and paste is handled in the backend on desktop
|
||||
if (isDesktop() && accelKey && ["KeyX", "KeyC", "KeyV"].includes(key)) return true;
|
||||
if (isPlatformNative() && accelKey && ["KeyX", "KeyC", "KeyV"].includes(key)) return true;
|
||||
// But on web, we want to not redirect paste
|
||||
if (!isDesktop() && key === "KeyV" && accelKey) return false;
|
||||
if (!isPlatformNative() && key === "KeyV" && accelKey) return false;
|
||||
|
||||
// Don't redirect user input from text entry into HTML elements
|
||||
if (targetIsTextField(e.target || undefined) && key !== "Escape" && !(accelKey && ["Enter", "NumpadEnter"].includes(key))) return false;
|
||||
@@ -104,7 +105,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
if (window.document.querySelector("[data-floating-menu-content]")) return false;
|
||||
|
||||
// Web-only keyboard shortcuts
|
||||
if (!isDesktop()) {
|
||||
if (!isPlatformNative()) {
|
||||
// Don't redirect a fullscreen request, but process it immediately instead
|
||||
if (((operatingSystem() !== "Mac" && key === "F11") || (operatingSystem() === "Mac" && e.ctrlKey && e.metaKey && key === "KeyF")) && e.type === "keydown" && !e.repeat) {
|
||||
e.preventDefault();
|
||||
@@ -398,8 +399,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
// Attempt to check if the clipboard permission is denied, and throw an error if that is the case
|
||||
// In Firefox, the `clipboard-read` permission isn't supported, so attempting to query it throws an error
|
||||
// In Safari, the entire Permissions API isn't supported, so the query never occurs and this block is skipped without an error and we assume we might have permission
|
||||
const clipboardRead = "clipboard-read" as PermissionName;
|
||||
const permission = await navigator.permissions?.query({ name: clipboardRead });
|
||||
const permission = await navigator.permissions?.query({ name: "clipboard-read" });
|
||||
if (permission?.state === "denied") throw new Error("Permission denied");
|
||||
|
||||
// Read the clipboard contents if the Clipboard API is available
|
||||
@@ -414,8 +414,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
const blob = await item.getType("text/plain");
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const text = reader.result as string;
|
||||
editor.handle.pasteText(text);
|
||||
if (typeof reader.result === "string") editor.handle.pasteText(reader.result);
|
||||
};
|
||||
reader.readAsText(blob);
|
||||
return true;
|
||||
@@ -429,8 +428,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
const blob = await item.getType("text/plain");
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const text = reader.result as string;
|
||||
editor.handle.pasteSvg(undefined, text);
|
||||
if (typeof reader.result === "string") editor.handle.pasteSvg(undefined, reader.result);
|
||||
};
|
||||
reader.readAsText(blob);
|
||||
return true;
|
||||
@@ -487,7 +485,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||
|
||||
// Pointer lock movement events on desktop
|
||||
editor.subscriptions.subscribeFrontendMessage("WindowPointerLockMove", (data) => {
|
||||
const event = new CustomEvent("pointerlockmove", { detail: data });
|
||||
const event = new CustomEvent("pointerlockmove", { detail: { x: data.position[0], y: data.position[1] } });
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ export function createPanicManager(editor: Editor, dialogState: DialogState) {
|
||||
// Code panic dialog and console error
|
||||
editor.subscriptions.subscribeFrontendMessage("DisplayDialogPanic", (data) => {
|
||||
// `Error.stackTraceLimit` is only available in V8/Chromium
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(Error as any).stackTraceLimit = Infinity;
|
||||
Error.stackTraceLimit = Infinity;
|
||||
const stackTrace = new Error().stack || "";
|
||||
const panicDetails = `${data.panicInfo}${stackTrace ? `\n\n${stackTrace}` : ""}`;
|
||||
|
||||
|
||||
@@ -2,11 +2,8 @@ import { createStore, del, get, set, update } from "idb-keyval";
|
||||
import { get as getFromStore } from "svelte/store";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { FrontendMessages } from "@graphite/messages";
|
||||
import type { PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
|
||||
type TriggerPersistenceWriteDocument = FrontendMessages["TriggerPersistenceWriteDocument"];
|
||||
type TriggerSavePreferences = FrontendMessages["TriggerSavePreferences"];
|
||||
import type { MessageBody } from "@graphite/subscription-router";
|
||||
|
||||
const graphiteStore = createStore("graphite", "store");
|
||||
|
||||
@@ -22,8 +19,8 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
|
||||
await set("current_document_id", String(documentId), graphiteStore);
|
||||
}
|
||||
|
||||
async function storeDocument(autoSaveDocument: TriggerPersistenceWriteDocument) {
|
||||
await update<Record<string, TriggerPersistenceWriteDocument>>(
|
||||
async function storeDocument(autoSaveDocument: MessageBody<"TriggerPersistenceWriteDocument">) {
|
||||
await update<Record<string, MessageBody<"TriggerPersistenceWriteDocument">>>(
|
||||
"documents",
|
||||
(old) => {
|
||||
const documents = old || {};
|
||||
@@ -38,7 +35,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
|
||||
}
|
||||
|
||||
async function removeDocument(id: string) {
|
||||
await update<Record<string, TriggerPersistenceWriteDocument>>(
|
||||
await update<Record<string, MessageBody<"TriggerPersistenceWriteDocument">>>(
|
||||
"documents",
|
||||
(old) => {
|
||||
const documents = old || {};
|
||||
@@ -72,13 +69,12 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
|
||||
}
|
||||
|
||||
async function loadFirstDocument() {
|
||||
const previouslySavedDocuments = await get<Record<string, TriggerPersistenceWriteDocument>>("documents", graphiteStore);
|
||||
const previouslySavedDocuments = await get<Record<string, MessageBody<"TriggerPersistenceWriteDocument">>>("documents", graphiteStore);
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if needed
|
||||
// Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if the browser is storing the old format as strings
|
||||
if (previouslySavedDocuments) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Object.values(previouslySavedDocuments).forEach((doc: any) => {
|
||||
Object.values(previouslySavedDocuments).forEach((doc) => {
|
||||
if (typeof doc.documentId === "string") doc.documentId = BigInt(doc.documentId);
|
||||
});
|
||||
}
|
||||
@@ -105,7 +101,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
|
||||
}
|
||||
|
||||
async function loadRestDocuments() {
|
||||
const previouslySavedDocuments = await get<Record<string, TriggerPersistenceWriteDocument>>("documents", graphiteStore);
|
||||
const previouslySavedDocuments = await get<Record<string, MessageBody<"TriggerPersistenceWriteDocument">>>("documents", graphiteStore);
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if needed
|
||||
@@ -154,7 +150,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
|
||||
|
||||
// PREFERENCES
|
||||
|
||||
async function savePreferences(preferences: TriggerSavePreferences["preferences"]) {
|
||||
async function savePreferences(preferences: unknown) {
|
||||
await set("preferences", preferences, graphiteStore);
|
||||
}
|
||||
|
||||
@@ -189,7 +185,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
|
||||
});
|
||||
editor.subscriptions.subscribeFrontendMessage("TriggerSaveActiveDocument", async (data) => {
|
||||
const documentId = String(data.documentId);
|
||||
const previouslySavedDocuments = await get<Record<string, TriggerPersistenceWriteDocument>>("documents", graphiteStore);
|
||||
const previouslySavedDocuments = await get<Record<string, MessageBody<"TriggerPersistenceWriteDocument">>>("documents", graphiteStore);
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if needed
|
||||
|
||||
@@ -1,766 +0,0 @@
|
||||
import type { PopoverButtonStyle, IconName, IconSize } from "@graphite/icons";
|
||||
|
||||
export type NodeGraphError = {
|
||||
position: [number, number];
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type OpenDocument = {
|
||||
id: bigint;
|
||||
details: DocumentDetails;
|
||||
};
|
||||
|
||||
type DocumentDetails = { name: string; isAutoSaved: boolean; isSaved: boolean };
|
||||
|
||||
export type Box = {
|
||||
startX: number;
|
||||
startY: number;
|
||||
endX: number;
|
||||
endY: number;
|
||||
};
|
||||
|
||||
export type FrontendClickTargets = {
|
||||
nodeClickTargets: string[];
|
||||
layerClickTargets: string[];
|
||||
connectorClickTargets: string[];
|
||||
iconClickTargets: string[];
|
||||
allNodesBoundingBox: string;
|
||||
modifyImportExport: string[];
|
||||
};
|
||||
|
||||
type ContextMenuDataCreateNode = {
|
||||
type: "CreateNode";
|
||||
data: {
|
||||
compatibleType: string | undefined;
|
||||
};
|
||||
};
|
||||
type ContextMenuDataModifyNode = {
|
||||
type: "ModifyNode";
|
||||
data: {
|
||||
nodeId: bigint;
|
||||
canBeLayer: boolean;
|
||||
currentlyIsNode: boolean;
|
||||
hasSelectedLayers: boolean;
|
||||
allSelectedLayersLocked: boolean;
|
||||
};
|
||||
};
|
||||
export type ContextMenuInformation = {
|
||||
contextMenuCoordinates: [number, number];
|
||||
contextMenuData: ContextMenuDataCreateNode | ContextMenuDataModifyNode;
|
||||
};
|
||||
|
||||
export type FrontendGraphDataType = "General" | "Number" | "Artboard" | "Graphic" | "Raster" | "Vector" | "Color" | "Invalid";
|
||||
|
||||
export type FrontendGraphInput = {
|
||||
dataType: FrontendGraphDataType;
|
||||
name: string;
|
||||
description: string;
|
||||
resolvedType: string;
|
||||
validTypes: string[];
|
||||
connectedTo: string;
|
||||
};
|
||||
|
||||
export type FrontendGraphOutput = {
|
||||
dataType: FrontendGraphDataType;
|
||||
name: string;
|
||||
description: string;
|
||||
resolvedType: string;
|
||||
connectedTo: string[];
|
||||
};
|
||||
|
||||
export type FrontendNode = {
|
||||
id: bigint;
|
||||
isLayer: boolean;
|
||||
canBeLayer: boolean;
|
||||
reference: string | undefined;
|
||||
displayName: string;
|
||||
implementationName: string;
|
||||
primaryInput: FrontendGraphInput | undefined;
|
||||
exposedInputs: FrontendGraphInput[];
|
||||
primaryOutput: FrontendGraphOutput | undefined;
|
||||
exposedOutputs: FrontendGraphOutput[];
|
||||
primaryInputConnectedToLayer: boolean;
|
||||
primaryOutputConnectedToLayer: boolean;
|
||||
position: [number, number];
|
||||
// TODO: Store field for the width of the left node chain
|
||||
previewed: boolean;
|
||||
visible: boolean;
|
||||
locked: boolean;
|
||||
};
|
||||
|
||||
export type FrontendNodeType = {
|
||||
identifier: string;
|
||||
name: string;
|
||||
category: string;
|
||||
inputTypes: string[];
|
||||
};
|
||||
|
||||
export type NodeGraphTransform = {
|
||||
scale: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type WirePath = {
|
||||
pathString: string;
|
||||
dataType: FrontendGraphDataType;
|
||||
thick: boolean;
|
||||
dashed: boolean;
|
||||
};
|
||||
|
||||
export type AppWindowPlatform = "Web" | "Windows" | "Mac" | "Linux";
|
||||
|
||||
// Rust enum `Key`
|
||||
export type KeyRaw = string;
|
||||
// Serde converts a Rust `Key` enum variant into this format with both the `Key` variant name (called `RawKey` in TS) and the localized `label` for the key
|
||||
export type MouseMotion = "None" | "Lmb" | "Rmb" | "Mmb" | "ScrollUp" | "ScrollDown" | "Drag" | "LmbDouble" | "LmbDrag" | "RmbDrag" | "RmbDouble" | "MmbDrag";
|
||||
export type LabeledShortcut = (MouseMotion | { key: KeyRaw; label: string })[];
|
||||
export type ActionShortcut = { shortcut: LabeledShortcut };
|
||||
|
||||
// All channels range are represented by 0-1, sRGB, gamma.
|
||||
export type Color = {
|
||||
red: number;
|
||||
green: number;
|
||||
blue: number;
|
||||
alpha: number;
|
||||
none: boolean;
|
||||
};
|
||||
|
||||
export type Gradient = {
|
||||
position: number[];
|
||||
midpoint: number[];
|
||||
color: Color[];
|
||||
};
|
||||
|
||||
export type FillChoice = Color | Gradient;
|
||||
|
||||
export type EyedropperPreviewImage = {
|
||||
data: Uint8Array;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type LayerStructureEntry = {
|
||||
layerId: bigint;
|
||||
children: LayerStructureEntry[];
|
||||
};
|
||||
|
||||
export type LayerPanelEntry = {
|
||||
id: bigint;
|
||||
implementationName: string;
|
||||
iconName: IconName | undefined;
|
||||
alias: string;
|
||||
inSelectedNetwork: boolean;
|
||||
childrenAllowed: boolean;
|
||||
childrenPresent: boolean;
|
||||
expanded: boolean;
|
||||
depth: number;
|
||||
visible: boolean;
|
||||
parentsVisible: boolean;
|
||||
unlocked: boolean;
|
||||
parentsUnlocked: boolean;
|
||||
parentId: bigint | undefined;
|
||||
selected: boolean;
|
||||
ancestorOfSelected: boolean;
|
||||
descendantOfSelected: boolean;
|
||||
clipped: boolean;
|
||||
clippable: boolean;
|
||||
};
|
||||
|
||||
export type Font = {
|
||||
fontFamily: string;
|
||||
fontStyle: string;
|
||||
};
|
||||
|
||||
// WIDGET PROPS
|
||||
|
||||
export type CheckboxInput = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
checked: boolean;
|
||||
icon: IconName;
|
||||
forLabel: bigint | undefined;
|
||||
disabled: boolean;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type ColorInput = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
value: FillChoice;
|
||||
allowNone: boolean;
|
||||
// allowTransparency: boolean; // TODO: Implement
|
||||
menuDirection: MenuDirection | undefined;
|
||||
disabled: boolean;
|
||||
|
||||
// Styling
|
||||
narrow: boolean;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
// An entry in the all-encompassing MenuList component which defines all types of menus (which are spawned by widgets like `TextButton` and `DropdownInput`)
|
||||
export type MenuListEntry = {
|
||||
// Content
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: IconName;
|
||||
disabled?: boolean;
|
||||
|
||||
// Children
|
||||
children?: MenuListEntry[][];
|
||||
childrenHash?: bigint;
|
||||
|
||||
// Styling
|
||||
font?: string;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel?: string;
|
||||
tooltipDescription?: string;
|
||||
tooltipShortcut?: ActionShortcut;
|
||||
};
|
||||
|
||||
export type CurveManipulatorGroup = {
|
||||
anchor: [number, number];
|
||||
handles: [[number, number], [number, number]];
|
||||
};
|
||||
|
||||
export type Curve = {
|
||||
manipulatorGroups: CurveManipulatorGroup[];
|
||||
firstHandle: [number, number];
|
||||
lastHandle: [number, number];
|
||||
};
|
||||
|
||||
export type CurveInput = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
value: Curve;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type DropdownInput = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
selectedIndex: number | undefined;
|
||||
drawIcon: boolean;
|
||||
disabled: boolean;
|
||||
|
||||
// Children
|
||||
entries: MenuListEntry[][];
|
||||
entriesHash: bigint;
|
||||
|
||||
// Styling
|
||||
narrow: boolean;
|
||||
|
||||
// Behavior
|
||||
virtualScrolling: boolean;
|
||||
interactive: boolean;
|
||||
|
||||
// Sizing
|
||||
minWidth: number;
|
||||
maxWidth: number;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type IconButton = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
icon: IconName;
|
||||
hoverIcon: IconName | undefined;
|
||||
size: IconSize;
|
||||
disabled: boolean;
|
||||
|
||||
// Styling
|
||||
emphasized: boolean;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type IconLabel = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
icon: IconName;
|
||||
disabled: boolean;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type ImageButton = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
image: IconName;
|
||||
width: string | undefined;
|
||||
height: string | undefined;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type ImageLabel = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
url: string;
|
||||
width: string | undefined;
|
||||
height: string | undefined;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type ShortcutLabel = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
shortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type NumberInputIncrementBehavior = "Add" | "Multiply" | "Callback" | "None";
|
||||
export type NumberInputMode = "Increment" | "Range";
|
||||
|
||||
export type NumberInput = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
value: number | undefined;
|
||||
label: string | undefined;
|
||||
disabled: boolean;
|
||||
|
||||
// Styling
|
||||
narrow: boolean;
|
||||
|
||||
// Behavior
|
||||
mode: NumberInputMode;
|
||||
min: number | undefined;
|
||||
max: number | undefined;
|
||||
rangeMin: number | undefined;
|
||||
rangeMax: number | undefined;
|
||||
step: number;
|
||||
isInteger: boolean;
|
||||
incrementBehavior: NumberInputIncrementBehavior;
|
||||
displayDecimalPlaces: number;
|
||||
unit: string;
|
||||
unitIsHiddenWhenEditing: boolean;
|
||||
|
||||
// Sizing
|
||||
minWidth: number;
|
||||
maxWidth: number;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type NodeCatalog = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
disabled: boolean;
|
||||
|
||||
// Behavior
|
||||
initialSearchTerm: string;
|
||||
};
|
||||
|
||||
export type PopoverButton = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
style: PopoverButtonStyle | undefined;
|
||||
icon: IconName | undefined;
|
||||
disabled: boolean;
|
||||
|
||||
// Children
|
||||
popoverLayout: Layout;
|
||||
popoverMinWidth: number | undefined;
|
||||
menuDirection: MenuDirection | undefined;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center";
|
||||
|
||||
export type RadioEntryData = {
|
||||
// Content
|
||||
value?: string;
|
||||
label?: string;
|
||||
icon?: IconName;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel?: string;
|
||||
tooltipDescription?: string;
|
||||
tooltipShortcut?: ActionShortcut;
|
||||
};
|
||||
|
||||
export type RadioInput = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
selectedIndex: number | undefined;
|
||||
disabled: boolean;
|
||||
|
||||
// Children
|
||||
entries: RadioEntryData[];
|
||||
|
||||
// Styling
|
||||
narrow: boolean;
|
||||
|
||||
// Sizing
|
||||
minWidth: number;
|
||||
};
|
||||
|
||||
export type SeparatorDirection = "Horizontal" | "Vertical";
|
||||
export type SeparatorStyle = "Related" | "Unrelated" | "Section";
|
||||
|
||||
export type Separator = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
direction: SeparatorDirection;
|
||||
style: SeparatorStyle;
|
||||
};
|
||||
|
||||
export type WorkingColorsInput = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
primary: Color;
|
||||
secondary: Color;
|
||||
};
|
||||
|
||||
export type TextAreaInput = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
value: string;
|
||||
label: string | undefined;
|
||||
disabled: boolean;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type ParameterExposeButton = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
exposed: boolean;
|
||||
dataType: FrontendGraphDataType;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type TextButton = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
label: string;
|
||||
icon: IconName | undefined;
|
||||
hoverIcon: IconName | undefined;
|
||||
disabled: boolean;
|
||||
|
||||
// Children
|
||||
menuListChildren: MenuListEntry[][];
|
||||
menuListChildrenHash: bigint;
|
||||
|
||||
// Styling
|
||||
emphasized: boolean;
|
||||
flush: boolean;
|
||||
narrow: boolean;
|
||||
|
||||
// Sizing
|
||||
minWidth: number;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type BreadcrumbTrailButtons = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
labels: string[];
|
||||
disabled: boolean;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type TextInput = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
value: string;
|
||||
label: string | undefined;
|
||||
placeholder: string | undefined;
|
||||
disabled: boolean;
|
||||
|
||||
// Styling
|
||||
narrow: boolean;
|
||||
centered: boolean;
|
||||
|
||||
// Sizing
|
||||
minWidth: number;
|
||||
maxWidth: number;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type TextLabel = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
forCheckbox: bigint | undefined;
|
||||
|
||||
// Styling
|
||||
narrow: boolean;
|
||||
bold: boolean;
|
||||
italic: boolean;
|
||||
monospace: boolean;
|
||||
multiline: boolean;
|
||||
centerAlign: boolean;
|
||||
tableAlign: boolean;
|
||||
|
||||
// Sizing
|
||||
minWidth: number;
|
||||
minWidthCharacters: number;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
export type ReferencePoint = "None" | "TopLeft" | "TopCenter" | "TopRight" | "CenterLeft" | "Center" | "CenterRight" | "BottomLeft" | "BottomCenter" | "BottomRight";
|
||||
|
||||
export type ReferencePointInput = {
|
||||
kind: WidgetPropsNames;
|
||||
|
||||
// Content
|
||||
value: ReferencePoint;
|
||||
disabled: boolean;
|
||||
|
||||
// Tooltips
|
||||
tooltipLabel: string;
|
||||
tooltipDescription: string;
|
||||
tooltipShortcut: ActionShortcut | undefined;
|
||||
};
|
||||
|
||||
// WIDGET
|
||||
|
||||
export type WidgetTypes = {
|
||||
BreadcrumbTrailButtons: BreadcrumbTrailButtons;
|
||||
CheckboxInput: CheckboxInput;
|
||||
ColorInput: ColorInput;
|
||||
CurveInput: CurveInput;
|
||||
DropdownInput: DropdownInput;
|
||||
IconButton: IconButton;
|
||||
IconLabel: IconLabel;
|
||||
ImageButton: ImageButton;
|
||||
ImageLabel: ImageLabel;
|
||||
NodeCatalog: NodeCatalog;
|
||||
NumberInput: NumberInput;
|
||||
ParameterExposeButton: ParameterExposeButton;
|
||||
PopoverButton: PopoverButton;
|
||||
RadioInput: RadioInput;
|
||||
ReferencePointInput: ReferencePointInput;
|
||||
Separator: Separator;
|
||||
ShortcutLabel: ShortcutLabel;
|
||||
TextAreaInput: TextAreaInput;
|
||||
TextButton: TextButton;
|
||||
TextInput: TextInput;
|
||||
TextLabel: TextLabel;
|
||||
WorkingColorsInput: WorkingColorsInput;
|
||||
};
|
||||
export type WidgetPropsNames = keyof WidgetTypes;
|
||||
export type WidgetPropsSet = WidgetTypes[WidgetPropsNames];
|
||||
|
||||
export type WidgetInstance = {
|
||||
widgetId: bigint;
|
||||
props: WidgetPropsSet;
|
||||
};
|
||||
|
||||
// WIDGET LAYOUT
|
||||
|
||||
export type LayoutTarget =
|
||||
| "DataPanel"
|
||||
| "DialogButtons"
|
||||
| "DialogColumn1"
|
||||
| "DialogColumn2"
|
||||
| "DocumentBar"
|
||||
| "LayersPanelBottomBar"
|
||||
| "LayersPanelControlLeftBar"
|
||||
| "LayersPanelControlRightBar"
|
||||
| "MenuBar"
|
||||
| "NodeGraphControlBar"
|
||||
| "PropertiesPanel"
|
||||
| "StatusBarHints"
|
||||
| "StatusBarInfo"
|
||||
| "ToolOptions"
|
||||
| "ToolShelf"
|
||||
| "WelcomeScreenButtons"
|
||||
| "WorkingColors";
|
||||
|
||||
export type WidgetDiff = {
|
||||
widgetPath: number[];
|
||||
newValue: { layout: Layout } | { layoutGroup: LayoutGroup } | { widget: WidgetInstance };
|
||||
};
|
||||
|
||||
export type UIItem = Layout | LayoutGroup | WidgetInstance[] | WidgetInstance;
|
||||
export type LayoutGroup = WidgetSpanRow | WidgetSpanColumn | WidgetTable | WidgetSection;
|
||||
export type Layout = LayoutGroup[];
|
||||
|
||||
export type WidgetSpanColumn = { columnWidgets: WidgetInstance[] };
|
||||
export type WidgetSpanRow = { rowWidgets: WidgetInstance[] };
|
||||
export type WidgetTable = { tableWidgets: WidgetInstance[][]; unstyled: boolean };
|
||||
export type WidgetSection = { name: string; description: string; visible: boolean; pinned: boolean; id: bigint; layout: Layout };
|
||||
|
||||
export type FrontendMessages = {
|
||||
ClearAllNodeGraphWires: Record<string, never>;
|
||||
DisplayDialog: { title: string; icon: IconName };
|
||||
DialogClose: Record<string, never>;
|
||||
DisplayDialogPanic: { panicInfo: string };
|
||||
DisplayEditableTextbox: {
|
||||
text: string;
|
||||
lineHeightRatio: number;
|
||||
fontSize: number;
|
||||
color: string;
|
||||
fontData: ArrayBuffer;
|
||||
transform: number[];
|
||||
maxWidth: undefined | number;
|
||||
maxHeight: undefined | number;
|
||||
align: "Left" | "Center" | "Right" | "JustifyLeft";
|
||||
};
|
||||
DisplayEditableTextboxTransform: { transform: number[] };
|
||||
DisplayEditableTextboxUpdateFontData: { fontData: ArrayBuffer };
|
||||
DisplayRemoveEditableTextbox: Record<string, never>;
|
||||
SendShortcutAltClick: { shortcut: ActionShortcut | undefined };
|
||||
SendShortcutFullscreen: { shortcut: ActionShortcut | undefined; shortcutMac: ActionShortcut | undefined };
|
||||
SendShortcutShiftClick: { shortcut: ActionShortcut | undefined };
|
||||
SendUIMetadata: { nodeDescriptions: [string, string][]; nodeTypes: FrontendNodeType[] };
|
||||
TriggerAboutGraphiteLocalizedCommitDate: { commitDate: string };
|
||||
TriggerClipboardRead: Record<string, never>;
|
||||
TriggerClipboardWrite: { content: string };
|
||||
TriggerDisplayThirdPartyLicensesDialog: Record<string, never>;
|
||||
TriggerExportImage: { svg: string; name: string; mime: string; size: [number, number] };
|
||||
TriggerFetchAndOpenDocument: { name: string; filename: string };
|
||||
TriggerFontCatalogLoad: Record<string, never>;
|
||||
TriggerFontDataLoad: { font: Font; url: string };
|
||||
TriggerImport: Record<string, never>;
|
||||
TriggerLoadFirstAutoSaveDocument: Record<string, never>;
|
||||
TriggerLoadPreferences: Record<string, never>;
|
||||
TriggerLoadRestAutoSaveDocuments: Record<string, never>;
|
||||
TriggerOpen: Record<string, never>;
|
||||
TriggerOpenLaunchDocuments: Record<string, never>;
|
||||
TriggerPersistenceRemoveDocument: { documentId: bigint };
|
||||
TriggerPersistenceWriteDocument: { documentId: bigint; document: string; details: DocumentDetails; version: string };
|
||||
TriggerSaveActiveDocument: { documentId: bigint };
|
||||
TriggerSaveDocument: { documentId: bigint; name: string; path: string | undefined; content: ArrayBuffer };
|
||||
TriggerSaveFile: { name: string; content: ArrayBuffer };
|
||||
TriggerSavePreferences: { preferences: Record<string, unknown> };
|
||||
TriggerSelectionRead: { cut: boolean };
|
||||
TriggerSelectionWrite: { content: string };
|
||||
TriggerTextCommit: Record<string, never>;
|
||||
TriggerVisitLink: { url: string };
|
||||
UpdateActiveDocument: { documentId: bigint };
|
||||
UpdateBox: { box: Box | undefined };
|
||||
UpdateClickTargets: { clickTargets: FrontendClickTargets | undefined };
|
||||
UpdateContextMenuInformation: { contextMenuInformation: ContextMenuInformation | undefined };
|
||||
UpdateDataPanelState: { open: boolean };
|
||||
UpdateDocumentArtwork: { svg: string };
|
||||
UpdateDocumentLayerDetails: { data: LayerPanelEntry };
|
||||
UpdateDocumentLayerStructure: { layerStructure: LayerStructureEntry[] };
|
||||
UpdateDocumentRulers: { origin: [number, number]; spacing: number; interval: number; visible: boolean };
|
||||
UpdateDocumentScrollbars: { position: [number, number]; size: [number, number]; multiplier: [number, number] };
|
||||
UpdateExportReorderIndex: { exportIndex: number | undefined };
|
||||
UpdateEyedropperSamplingState: {
|
||||
image: EyedropperPreviewImage | undefined;
|
||||
mousePosition: [number, number] | undefined;
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
setColorChoice: "Primary" | "Secondary" | undefined;
|
||||
};
|
||||
UpdateFullscreen: { fullscreen: boolean };
|
||||
UpdateGradientStopColorPickerPosition: { color: Color; x: number; y: number };
|
||||
UpdateGraphFadeArtwork: { percentage: number };
|
||||
UpdateGraphViewOverlay: { open: boolean };
|
||||
UpdateImportReorderIndex: { importIndex: number | undefined };
|
||||
UpdateImportsExports: {
|
||||
imports: (FrontendGraphOutput | undefined)[];
|
||||
exports: (FrontendGraphInput | undefined)[];
|
||||
importPosition: [number, number];
|
||||
exportPosition: [number, number];
|
||||
addImportExport: boolean;
|
||||
};
|
||||
UpdateInSelectedNetwork: { inSelectedNetwork: boolean };
|
||||
UpdateLayersPanelState: { open: boolean };
|
||||
UpdateLayerWidths: { layerWidths: Map<bigint, number>; chainWidths: Map<bigint, number>; hasLeftInputWire: Map<bigint, boolean> };
|
||||
UpdateLayout: { layoutTarget: LayoutTarget; diff: WidgetDiff[] };
|
||||
UpdateMaximized: { maximized: boolean };
|
||||
UpdateMouseCursor: { cursor: string };
|
||||
UpdateNodeGraphErrorDiagnostic: { error: NodeGraphError | undefined };
|
||||
UpdateNodeGraphNodes: { nodes: FrontendNode[] };
|
||||
UpdateNodeGraphSelection: { selected: bigint[] };
|
||||
UpdateNodeGraphTransform: { transform: NodeGraphTransform };
|
||||
UpdateNodeGraphWires: { wires: { id: bigint; inputIndex: number; wirePathUpdate: WirePath | undefined }[] };
|
||||
UpdateNodeThumbnail: { id: bigint; value: string };
|
||||
UpdateOpenDocumentsList: { openDocuments: OpenDocument[] };
|
||||
UpdatePlatform: { platform: AppWindowPlatform };
|
||||
UpdatePropertiesPanelState: { open: boolean };
|
||||
UpdateUIScale: { scale: number };
|
||||
UpdateViewportHolePunch: { active: boolean };
|
||||
UpdateViewportPhysicalBounds: { x: number; y: number; width: number; height: number };
|
||||
UpdateVisibleNodes: { nodes: bigint[] };
|
||||
UpdateWirePathInProgress: { wirePath: WirePath | undefined };
|
||||
WindowFullscreen: Record<string, never>;
|
||||
WindowPointerLockMove: { x: number; y: number };
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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: "",
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import type { FrontendMessages, LayoutTarget, WidgetDiff } from "@graphite/messages";
|
||||
import { parseWidgetDiffs } from "@graphite/utility-functions/widgets";
|
||||
import type { FrontendMessage, LayoutTarget, WidgetDiff } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type FrontendMessageCallbacks = Record<string, ((messageData: any) => void) | undefined>;
|
||||
// Type convert a union of messages into a map of messages
|
||||
export type ToMessageMap<T> = {
|
||||
[K in T extends string ? T : T extends object ? keyof T : never]: K extends T ? Record<string, never> : T extends Record<K, infer Payload> ? Payload : never;
|
||||
};
|
||||
|
||||
export type MessageMap = ToMessageMap<FrontendMessage>;
|
||||
export type MessageName = keyof MessageMap;
|
||||
export type MessageBody<T extends MessageName> = Extract<FrontendMessage, Record<T, unknown>>[T];
|
||||
|
||||
export function createSubscriptionRouter() {
|
||||
const subscriptions: FrontendMessageCallbacks = {};
|
||||
// Callbacks are wrapped at subscription time to capture their type-specific data extraction in a closure,
|
||||
// so the stored function has a uniform signature and the map doesn't need per-key generic value types.
|
||||
const subscriptions: Partial<Record<MessageName, (taggedMessage: MessageMap) => void>> = {};
|
||||
const layoutCallbacks: Partial<Record<LayoutTarget, (diffs: WidgetDiff[]) => void>> = {};
|
||||
|
||||
const subscribeFrontendMessage = <T extends keyof FrontendMessages>(messageType: T, callback: (data: FrontendMessages[T]) => void) => {
|
||||
subscriptions[messageType] = callback;
|
||||
const subscribeFrontendMessage = <T extends MessageName>(messageType: T, callback: (data: MessageMap[T]) => void) => {
|
||||
subscriptions[messageType] = (taggedMessage: MessageMap) => callback(taggedMessage[messageType]);
|
||||
};
|
||||
|
||||
const unsubscribeFrontendMessage = (messageType: keyof FrontendMessages) => {
|
||||
const unsubscribeFrontendMessage = (messageType: MessageName) => {
|
||||
delete subscriptions[messageType];
|
||||
};
|
||||
|
||||
@@ -24,43 +31,56 @@ export function createSubscriptionRouter() {
|
||||
delete layoutCallbacks[target];
|
||||
};
|
||||
|
||||
const handleFrontendMessage = (messageType: keyof FrontendMessages, messageData: Record<string, unknown>) => {
|
||||
function normalizeMessage<T extends string | object>(message: T): ToMessageMap<T>;
|
||||
function normalizeMessage(message: string | Record<string, unknown>): Record<string, unknown> {
|
||||
// If it's a bare string, convert it to an object with an empty payload
|
||||
if (typeof message === "string") {
|
||||
const result: Record<string, Record<string, never>> = { [message]: {} };
|
||||
return result;
|
||||
}
|
||||
|
||||
// If it's already an object, it matches the structure of our map
|
||||
return message;
|
||||
}
|
||||
|
||||
const handleFrontendMessage = (messageType: MessageName, messageData: FrontendMessage) => {
|
||||
// Messages with non-empty data are provided by Serde JSON as an object with one key as the message name, like: { NameOfThisMessage: { ... } }
|
||||
// Messages with empty data are provided by Serde JSON as a string with the message name, like: "NameOfThisMessage"
|
||||
// Here we extract the payload object or use an empty object depending on the situation.
|
||||
const message = messageData[messageType] || {};
|
||||
// Here we extract the payload object or create an empty payload object, as needed.
|
||||
const taggedMessage = normalizeMessage(messageData);
|
||||
|
||||
// Resolve the callback lookup and the data to pass, depending on whether this is a layout update or a regular message.
|
||||
// Resolve the dispatch thunk, depending on whether this is a layout update or a regular message.
|
||||
// UpdateLayout messages are dispatched to layout-specific callbacks based on the layout target.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let getCallback: () => ((data: any) => void) | undefined;
|
||||
let callbackData: unknown;
|
||||
let errorLabel: string;
|
||||
if (messageType === "UpdateLayout") {
|
||||
const { layoutTarget, diff } = message as FrontendMessages["UpdateLayout"];
|
||||
getCallback = () => layoutCallbacks[layoutTarget];
|
||||
callbackData = parseWidgetDiffs(diff);
|
||||
errorLabel = `UpdateLayout for layout target "${layoutTarget}"`;
|
||||
} else {
|
||||
getCallback = () => subscriptions[messageType];
|
||||
callbackData = message;
|
||||
errorLabel = messageType;
|
||||
// The thunk is re-evaluated on each retry because the callback may not be registered yet.
|
||||
let getHandler: () => ((taggedMessage: MessageMap) => void) | undefined = () => subscriptions[messageType];
|
||||
|
||||
// Handle layout updates specially to route them to layout-specific callbacks and extract the diffs as the data to pass
|
||||
let target: LayoutTarget | undefined;
|
||||
if ("UpdateLayout" in taggedMessage) {
|
||||
const { layoutTarget, diff } = taggedMessage["UpdateLayout"];
|
||||
target = layoutTarget;
|
||||
|
||||
getHandler = () => {
|
||||
const layoutCallback = layoutCallbacks[layoutTarget];
|
||||
if (!layoutCallback) return undefined;
|
||||
return () => layoutCallback(diff);
|
||||
};
|
||||
}
|
||||
|
||||
// Try to execute the callback. Due to message ordering, the callback may not be registered yet,
|
||||
// so we retry a few times on the next stack frame to give onMount a chance to run.
|
||||
let retries = 0;
|
||||
const callCallback = () => {
|
||||
const callback = getCallback();
|
||||
const handler = getHandler();
|
||||
|
||||
if (callback) {
|
||||
callback(callbackData);
|
||||
if (handler) {
|
||||
handler(taggedMessage);
|
||||
} else if (retries <= 3) {
|
||||
retries += 1;
|
||||
setTimeout(callCallback, 0);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Received a frontend message of type "${errorLabel}" but no handler was registered for it from the client.`);
|
||||
console.error(`Received a frontend message of type ${messageType}${target ? ` (${target})` : ""} but no handler was registered for it from the client.`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sampleInterpolatedGradient } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Color, FillChoice, Gradient } from "@graphite/messages";
|
||||
import type { Color, FillChoice, GradientStops } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
// Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers
|
||||
export type HSV = { h: number; s: number; v: number };
|
||||
@@ -8,11 +8,7 @@ export type RGB = { r: number; g: number; b: number };
|
||||
// COLOR FACTORY FUNCTIONS
|
||||
|
||||
export function createColor(red: number, green: number, blue: number, alpha: number): Color {
|
||||
return { red, green, blue, alpha, none: false };
|
||||
}
|
||||
|
||||
export function createNoneColor(): Color {
|
||||
return { red: 0, green: 0, blue: 0, alpha: 1, none: true };
|
||||
return { red, green, blue, alpha };
|
||||
}
|
||||
|
||||
export function createColorFromHSVA(h: number, s: number, v: number, a: number): Color {
|
||||
@@ -21,7 +17,7 @@ export function createColorFromHSVA(h: number, s: number, v: number, a: number):
|
||||
return v - v * s * Math.max(Math.min(...[k, 4 - k, 1]), 0);
|
||||
};
|
||||
|
||||
return { red: convert(5), green: convert(3), blue: convert(1), alpha: a, none: false };
|
||||
return { red: convert(5), green: convert(3), blue: convert(1), alpha: a };
|
||||
}
|
||||
|
||||
// COLOR UTILITY FUNCTIONS
|
||||
@@ -67,15 +63,13 @@ export function colorFromCSS(colorCode: string): Color | undefined {
|
||||
return createColor(r / 255, g / 255, b / 255, a / 255);
|
||||
}
|
||||
|
||||
export function colorEquals(c1: Color, c2: Color): boolean {
|
||||
if (c1.none !== c2.none) return false;
|
||||
if (c1.none && c2.none) return true;
|
||||
export function colorEquals(c1: Color | undefined, c2: Color | undefined): boolean {
|
||||
if (c1 === undefined && c2 === undefined) return true;
|
||||
if (c1 === undefined || c2 === undefined) return false;
|
||||
return Math.abs(c1.red - c2.red) < 1e-6 && Math.abs(c1.green - c2.green) < 1e-6 && Math.abs(c1.blue - c2.blue) < 1e-6 && Math.abs(c1.alpha - c2.alpha) < 1e-6;
|
||||
}
|
||||
|
||||
export function colorToHexNoAlpha(color: Color): string | undefined {
|
||||
if (color.none) return undefined;
|
||||
|
||||
export function colorToHexNoAlpha(color: Color): string {
|
||||
const r = Math.round(color.red * 255)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
@@ -89,9 +83,7 @@ export function colorToHexNoAlpha(color: Color): string | undefined {
|
||||
return `#${r}${g}${b}`;
|
||||
}
|
||||
|
||||
export function colorToHexOptionalAlpha(color: Color): string | undefined {
|
||||
if (color.none) return undefined;
|
||||
|
||||
export function colorToHexOptionalAlpha(color: Color): string {
|
||||
const hex = colorToHexNoAlpha(color);
|
||||
const a = Math.round(color.alpha * 255)
|
||||
.toString(16)
|
||||
@@ -100,9 +92,7 @@ export function colorToHexOptionalAlpha(color: Color): string | undefined {
|
||||
return a === "ff" ? hex : `${hex}${a}`;
|
||||
}
|
||||
|
||||
export function colorToRgb255(color: Color): RGB | undefined {
|
||||
if (color.none) return undefined;
|
||||
|
||||
export function colorToRgb255(color: Color): RGB {
|
||||
return {
|
||||
r: Math.round(color.red * 255),
|
||||
g: Math.round(color.green * 255),
|
||||
@@ -110,23 +100,19 @@ export function colorToRgb255(color: Color): RGB | undefined {
|
||||
};
|
||||
}
|
||||
|
||||
export function colorToRgbCSS(color: Color): string | undefined {
|
||||
export function colorToRgbCSS(color: Color): string {
|
||||
const rgb = colorToRgb255(color);
|
||||
if (!rgb) return undefined;
|
||||
|
||||
return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
|
||||
}
|
||||
|
||||
export function colorToRgbaCSS(color: Color): string | undefined {
|
||||
export function colorToRgbaCSS(color: Color): string {
|
||||
const rgb = colorToRgb255(color);
|
||||
if (!rgb) return undefined;
|
||||
|
||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${color.alpha})`;
|
||||
}
|
||||
|
||||
export function colorToHSV(color: Color): HSV | undefined {
|
||||
if (color.none) return undefined;
|
||||
|
||||
export function colorToHSV(color: Color): HSV {
|
||||
const { red: r, green: g, blue: b } = color;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
@@ -156,15 +142,11 @@ export function colorToHSV(color: Color): HSV | undefined {
|
||||
return { h, s, v };
|
||||
}
|
||||
|
||||
export function colorOpaque(color: Color): Color | undefined {
|
||||
if (color.none) return undefined;
|
||||
|
||||
export function colorOpaque(color: Color): Color {
|
||||
return createColor(color.red, color.green, color.blue, 1);
|
||||
}
|
||||
|
||||
export function colorLuminance(color: Color): number | undefined {
|
||||
if (color.none) return undefined;
|
||||
|
||||
export function colorLuminance(color: Color): number {
|
||||
// Convert alpha into white
|
||||
const r = color.red * color.alpha + (1 - color.alpha);
|
||||
const g = color.green * color.alpha + (1 - color.alpha);
|
||||
@@ -179,48 +161,51 @@ export function colorLuminance(color: Color): number | undefined {
|
||||
return linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722;
|
||||
}
|
||||
|
||||
export function colorContrastingColor(color: Color): "black" | "white" {
|
||||
if (color.none) return "black";
|
||||
export function colorContrastingColor(color: Color | undefined): "black" | "white" {
|
||||
if (!color) return "black";
|
||||
|
||||
const luminance = colorLuminance(color);
|
||||
|
||||
return luminance && luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white";
|
||||
return luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white";
|
||||
}
|
||||
|
||||
export function contrastingOutlineFactor(value: FillChoice, proximityColor: string | [string, string], proximityRange: number): number {
|
||||
const pair = Array.isArray(proximityColor) ? [proximityColor[0], proximityColor[1]] : [proximityColor, proximityColor];
|
||||
const [range1, range2] = pair.map((color) => colorFromCSS(window.getComputedStyle(document.body).getPropertyValue(color)) || createNoneColor());
|
||||
const [range1, range2] = pair.map((color) => colorFromCSS(window.getComputedStyle(document.body).getPropertyValue(color)));
|
||||
|
||||
const contrast = (color: Color): number => {
|
||||
const lum = colorLuminance(color) || 0;
|
||||
let rangeLuminance1 = colorLuminance(range1) || 0;
|
||||
let rangeLuminance2 = colorLuminance(range2) || 0;
|
||||
const contrast = (color: Color | undefined): number => {
|
||||
if (!color) return 0;
|
||||
|
||||
const lum = colorLuminance(color);
|
||||
let rangeLuminance1 = range1 ? colorLuminance(range1) : 0;
|
||||
let rangeLuminance2 = range2 ? colorLuminance(range2) : 0;
|
||||
[rangeLuminance1, rangeLuminance2] = [Math.min(rangeLuminance1, rangeLuminance2), Math.max(rangeLuminance1, rangeLuminance2)];
|
||||
|
||||
const distance = Math.max(0, rangeLuminance1 - lum, lum - rangeLuminance2);
|
||||
|
||||
return (1 - Math.min(distance / proximityRange, 1)) * (1 - (colorToHSV(color)?.s || 0));
|
||||
return (1 - Math.min(distance / proximityRange, 1)) * (1 - colorToHSV(color).s);
|
||||
};
|
||||
|
||||
if (isGradient(value)) {
|
||||
if (value.color.length === 0) return 0;
|
||||
const gradientStops = fillChoiceGradientStops(value);
|
||||
if (gradientStops) {
|
||||
if (gradientStops.color.length === 0) return 0;
|
||||
|
||||
const first = contrast(value.color[0]);
|
||||
const last = contrast(value.color[value.color.length - 1]);
|
||||
const first = contrast(gradientStops.color[0]);
|
||||
const last = contrast(gradientStops.color[gradientStops.color.length - 1]);
|
||||
|
||||
return Math.min(first, last);
|
||||
}
|
||||
|
||||
return contrast(value);
|
||||
return contrast(fillChoiceColor(value));
|
||||
}
|
||||
|
||||
// GRADIENT UTILITY FUNCTIONS
|
||||
|
||||
export function isGradient(value: unknown): value is Gradient {
|
||||
return typeof value === "object" && value !== null && "position" in value && "midpoint" in value;
|
||||
export function isGradientStops(value: unknown): value is GradientStops {
|
||||
return typeof value === "object" && value !== null && "position" in value && "midpoint" in value && "color" in value;
|
||||
}
|
||||
|
||||
export function gradientToLinearGradientCSS(gradient: Gradient): string {
|
||||
export function gradientToLinearGradientCSS(gradient: GradientStops): string {
|
||||
if (gradient.position.length === 1) {
|
||||
return `linear-gradient(to right, ${colorToHexOptionalAlpha(gradient.color[0])} 0%, ${colorToHexOptionalAlpha(gradient.color[0])} 100%)`;
|
||||
}
|
||||
@@ -229,29 +214,29 @@ export function gradientToLinearGradientCSS(gradient: Gradient): string {
|
||||
return `linear-gradient(to right, ${pieces})`;
|
||||
}
|
||||
|
||||
export function gradientFirstColor(gradient: Gradient): Color | undefined {
|
||||
export function gradientFirstColor(gradient: GradientStops): Color | undefined {
|
||||
return gradient.color[0];
|
||||
}
|
||||
|
||||
export function gradientLastColor(gradient: Gradient): Color | undefined {
|
||||
export function gradientLastColor(gradient: GradientStops): Color | undefined {
|
||||
return gradient.color[gradient.color.length - 1];
|
||||
}
|
||||
|
||||
// FILL CHOICE UTILITY FUNCTIONS
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function parseFillChoice(value: any): FillChoice {
|
||||
if (isColor(value)) return value;
|
||||
if (isGradient(value)) return value;
|
||||
export function fillChoiceColor(value: FillChoice): Color | undefined {
|
||||
if (typeof value === "object" && "Solid" in value) return value.Solid;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const gradient: Gradient | undefined = value["Gradient"];
|
||||
if (gradient) {
|
||||
const color = gradient.color.map((c) => createColor(c.red, c.green, c.blue, c.alpha));
|
||||
return { ...gradient, color };
|
||||
}
|
||||
export function fillChoiceGradientStops(value: FillChoice): GradientStops | undefined {
|
||||
if (typeof value === "object" && "Gradient" in value) return value.Gradient;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const solid = value["Solid"];
|
||||
if (solid) return createColor(solid.red, solid.green, solid.blue, solid.alpha);
|
||||
|
||||
return createNoneColor();
|
||||
export function parseFillChoice(value: unknown): FillChoice {
|
||||
if (value === "None" || value === undefined || value === null) return "None";
|
||||
if (typeof value === "object" && value !== null && "Solid" in value && isColor(value.Solid)) return { Solid: value.Solid };
|
||||
if (typeof value === "object" && value !== null && "Gradient" in value && isGradientStops(value.Gradient)) return { Gradient: value.Gradient };
|
||||
return "None";
|
||||
}
|
||||
|
||||
@@ -18,16 +18,22 @@ export function downloadFileBlob(filename: string, blob: Blob) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function downloadFile(filename: string, content: ArrayBuffer) {
|
||||
export function downloadFile(filename: string, content: Uint8Array) {
|
||||
const type = filename.endsWith(".svg") ? "image/svg+xml;charset=utf-8" : "application/octet-stream";
|
||||
|
||||
const blob = new Blob([new Uint8Array(content)], { type });
|
||||
downloadFileBlob(filename, blob);
|
||||
if (content.length > 0 && content.buffer instanceof ArrayBuffer) {
|
||||
const contentView = new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
|
||||
const blob = new Blob([contentView], { type });
|
||||
downloadFileBlob(filename, blob);
|
||||
}
|
||||
}
|
||||
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#accept for the `accept` string format
|
||||
export async function upload<T extends "text" | "data" | "both">(accept: string, textOrData: T): Promise<UploadResult<T>> {
|
||||
return new Promise<UploadResult<T>>((resolve, _) => {
|
||||
export async function upload(accept: string, textOrData: "text"): Promise<UploadResult<string>>;
|
||||
export async function upload(accept: string, textOrData: "data"): Promise<UploadResult<Uint8Array>>;
|
||||
export async function upload(accept: string, textOrData: "both"): Promise<UploadResult<{ text: string; data: Uint8Array }>>;
|
||||
export async function upload(accept: string, textOrData: "text" | "data" | "both"): Promise<UploadResult<string | Uint8Array | { text: string; data: Uint8Array }>> {
|
||||
return new Promise((resolve) => {
|
||||
const element = document.createElement("input");
|
||||
element.type = "file";
|
||||
element.accept = accept;
|
||||
@@ -40,15 +46,12 @@ export async function upload<T extends "text" | "data" | "both">(accept: string,
|
||||
|
||||
const filename = file.name;
|
||||
const type = file.type;
|
||||
const content = (
|
||||
const content =
|
||||
textOrData === "text"
|
||||
? await file.text()
|
||||
: textOrData === "data"
|
||||
? new Uint8Array(await file.arrayBuffer())
|
||||
: textOrData === "both"
|
||||
? { text: await file.text(), data: new Uint8Array(await file.arrayBuffer()) }
|
||||
: undefined
|
||||
) as UploadResultType<T>;
|
||||
: { text: await file.text(), data: new Uint8Array(await file.arrayBuffer()) };
|
||||
|
||||
resolve({ filename, type, content });
|
||||
}
|
||||
@@ -61,8 +64,7 @@ export async function upload<T extends "text" | "data" | "both">(accept: string,
|
||||
// Once `element` goes out of scope, it has no references so it gets garbage collected along with its event listener, so `removeEventListener` is not needed
|
||||
});
|
||||
}
|
||||
export type UploadResult<T> = { filename: string; type: string; content: UploadResultType<T> };
|
||||
type UploadResultType<T> = T extends "text" ? string : T extends "data" ? Uint8Array : T extends "both" ? { text: string; data: Uint8Array } : never;
|
||||
export type UploadResult<T> = { filename: string; type: string; content: T };
|
||||
|
||||
export async function pasteFile(item: DataTransferItem, editor: Editor, mouse?: [number, number], insertParentId?: bigint, insertIndex?: number) {
|
||||
const file = item.getAsFile();
|
||||
|
||||
@@ -94,10 +94,8 @@ export async function getLocalizedScanCode(e: KeyboardEvent): Promise<string> {
|
||||
// It is likely a weird symbol that isn't in the A-Z range even with accents removed.
|
||||
// It might be a symbol from an Option key combination on a Mac. Or it might be from a non-Latin alphabet like Cyrillic.
|
||||
if (!KEY_ATTRIBUTE_VALUES.has(keyText)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (navigator && "keyboard" in navigator && "getLayoutMap" in (navigator as any).keyboard) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const layout = await (navigator as any).keyboard.getLayoutMap();
|
||||
if (navigator.keyboard && "getLayoutMap" in navigator.keyboard) {
|
||||
const layout = await navigator.keyboard.getLayoutMap();
|
||||
|
||||
type KeyCode = string;
|
||||
type KeySymbol = string;
|
||||
@@ -377,7 +375,7 @@ const LOCALE_SPECIFIC_KEY_CODES = LOCALE_SPECIFIC_KEY_CODES_INFO.map((info) => i
|
||||
const WRITING_SYSTEM_SPECIAL_CHARS = Object.values(KEY_CODES)
|
||||
.filter((info) => info.category === "writing-system")
|
||||
.flatMap((info) => info.keys?.us?.split(" "))
|
||||
.filter((character) => character && !/[a-zA-Z0-9]/.test(character)) as string[];
|
||||
.filter((character): character is string => (!character ? false : !/[a-zA-Z0-9]/.test(character)));
|
||||
|
||||
const KEY_ATTRIBUTE_VALUES_INVOLVING_HANDEDNESS = ["Control", "Meta", "Shift"];
|
||||
const KEY_ATTRIBUTE_VALUES = new Set([
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
export function browserVersion(): string {
|
||||
const agent = window.navigator.userAgent;
|
||||
let match = agent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
|
||||
@@ -37,24 +35,3 @@ export function operatingSystem(): OperatingSystem {
|
||||
const userAgentOS = Object.keys(osTable).find((key) => window.navigator.userAgent.includes(key));
|
||||
return osTable[userAgentOS || "Windows"];
|
||||
}
|
||||
|
||||
export function isDesktop(): boolean {
|
||||
return isPlatformNative();
|
||||
}
|
||||
|
||||
export function isEventSupported(eventName: string) {
|
||||
const onEventName = `on${eventName}`;
|
||||
|
||||
let tag = "div";
|
||||
if (["select", "change"].includes(eventName)) tag = "select";
|
||||
if (["submit", "reset"].includes(eventName)) tag = "form";
|
||||
if (["error", "load", "abort"].includes(eventName)) tag = "img";
|
||||
const element = document.createElement(tag);
|
||||
|
||||
if (onEventName in element) return true;
|
||||
|
||||
// Check if "return;" gets converted into a function, meaning the event is supported
|
||||
element.setAttribute(eventName, "return;");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return typeof (element as Record<string, any>)[onEventName] === "function";
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ export function setupViewportResizeObserver(editor: Editor) {
|
||||
const viewports = Array.from(window.document.querySelectorAll("[data-viewport-container]"));
|
||||
if (viewports.length <= 0) return;
|
||||
|
||||
const viewport = viewports[0] as HTMLElement;
|
||||
const viewport = viewports[0];
|
||||
if (!(viewport instanceof HTMLElement)) return;
|
||||
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
|
||||
@@ -1,68 +1,36 @@
|
||||
import type { Layout, LayoutGroup, UIItem, WidgetDiff, WidgetInstance, WidgetSection, WidgetSpanColumn, WidgetSpanRow, WidgetTable } from "@graphite/messages";
|
||||
|
||||
export function isWidgetSpanColumn(layoutColumn: LayoutGroup): layoutColumn is WidgetSpanColumn {
|
||||
return Boolean((layoutColumn as WidgetSpanColumn)?.columnWidgets);
|
||||
}
|
||||
|
||||
export function isWidgetSpanRow(layoutRow: LayoutGroup): layoutRow is WidgetSpanRow {
|
||||
return Boolean((layoutRow as WidgetSpanRow)?.rowWidgets);
|
||||
}
|
||||
|
||||
export function isWidgetTable(layoutTable: LayoutGroup): layoutTable is WidgetTable {
|
||||
return Boolean((layoutTable as WidgetTable)?.tableWidgets);
|
||||
}
|
||||
|
||||
export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSection {
|
||||
return Boolean((layoutRow as WidgetSection)?.layout);
|
||||
}
|
||||
|
||||
/// Unwraps the Serde tagged enum `{ widgetId, widget: { Kind: props } }` into `{ widgetId, props: { kind, ...props } }`
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function parseWidgetInstance(widgetInstance: any): WidgetInstance {
|
||||
const widgetId = widgetInstance.widgetId;
|
||||
|
||||
const kind = Object.keys(widgetInstance.widget)[0];
|
||||
const props = widgetInstance.widget[kind];
|
||||
props.kind = kind;
|
||||
|
||||
return { widgetId, props };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function parseWidgetDiffs(rawDiffs: any): WidgetDiff[] {
|
||||
return rawDiffs.map((diff: WidgetDiff) => {
|
||||
const { widgetPath, newValue } = diff;
|
||||
|
||||
if ("layout" in newValue) return { widgetPath, newValue: newValue.layout.map(createLayoutGroup) };
|
||||
if ("layoutGroup" in newValue) return { widgetPath, newValue: createLayoutGroup(newValue.layoutGroup) };
|
||||
if ("widget" in newValue) return { widgetPath, newValue: parseWidgetInstance(newValue.widget) };
|
||||
|
||||
// This code should be unreachable
|
||||
throw new Error("DiffUpdate invalid");
|
||||
});
|
||||
}
|
||||
import type { Layout, LayoutGroup, WidgetDiff, WidgetInstance } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
|
||||
type UIItem = Layout | LayoutGroup | WidgetInstance[] | WidgetInstance;
|
||||
// Updates a widget layout based on a list of updates, giving the new layout by mutating the `layout` argument
|
||||
export function patchLayout(layout: /* &mut */ Layout, diffs: WidgetDiff[]) {
|
||||
diffs.forEach((update) => {
|
||||
// Extract the actual content from the DiffUpdate tagged enum
|
||||
const { newValue } = update;
|
||||
let newContent: Layout | LayoutGroup | WidgetInstance;
|
||||
if ("layout" in newValue) newContent = newValue.layout;
|
||||
else if ("layoutGroup" in newValue) newContent = newValue.layoutGroup;
|
||||
else if ("widget" in newValue) newContent = newValue.widget;
|
||||
else throw new Error("DiffUpdate invalid");
|
||||
|
||||
// Find the object where the diff applies to
|
||||
const diffObject = update.widgetPath.reduce((targetLayout: UIItem | undefined, index: number): UIItem | undefined => {
|
||||
if (targetLayout && "columnWidgets" in targetLayout) return targetLayout.columnWidgets[index];
|
||||
if (targetLayout && "rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
|
||||
if (targetLayout && "tableWidgets" in targetLayout) return targetLayout.tableWidgets[index];
|
||||
if (targetLayout && "layout" in targetLayout) return targetLayout.layout[index];
|
||||
if (targetLayout && "props" in targetLayout && "widgetId" in targetLayout) {
|
||||
if (targetLayout.props.kind === "PopoverButton" && "popoverLayout" in targetLayout.props && targetLayout.props.popoverLayout) {
|
||||
targetLayout.props.popoverLayout = targetLayout.props.popoverLayout.map(createLayoutGroup);
|
||||
return targetLayout.props.popoverLayout[index];
|
||||
const diffObject = update.widgetPath.reduce((targetLayout, index: bigint): UIItem | undefined => {
|
||||
const i = Number(index);
|
||||
|
||||
if (targetLayout && "Column" in targetLayout) return targetLayout.Column.columnWidgets[i];
|
||||
if (targetLayout && "Row" in targetLayout) return targetLayout.Row.rowWidgets[i];
|
||||
if (targetLayout && "Table" in targetLayout) return targetLayout.Table.tableWidgets[i];
|
||||
if (targetLayout && "Section" in targetLayout) return targetLayout.Section.layout[i];
|
||||
if (targetLayout && "widget" in targetLayout && "widgetId" in targetLayout) {
|
||||
if ("PopoverButton" in targetLayout.widget && targetLayout.widget.PopoverButton.popoverLayout) {
|
||||
return targetLayout.widget.PopoverButton.popoverLayout[i];
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Tried to index widget");
|
||||
return targetLayout;
|
||||
}
|
||||
|
||||
return targetLayout?.[index];
|
||||
}, layout as UIItem);
|
||||
return targetLayout?.[i];
|
||||
}, layout);
|
||||
|
||||
// Exit if we failed to produce a valid patch for the existing layout.
|
||||
// This means that the backend assumed an existing layout that doesn't exist in the frontend. This can happen, for
|
||||
@@ -79,53 +47,11 @@ export function patchLayout(layout: /* &mut */ Layout, diffs: WidgetDiff[]) {
|
||||
diffObject.length = 0;
|
||||
}
|
||||
// Remove all of the keys from the old object
|
||||
Object.keys(diffObject).forEach((key) => delete (diffObject as Record<string, unknown>)[key]);
|
||||
Object.keys(diffObject).forEach((key) => Reflect.deleteProperty(diffObject, key));
|
||||
|
||||
// Assign keys to the new object
|
||||
// `Object.assign` works but `diffObject = update.newValue;` doesn't.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
|
||||
Object.assign(diffObject, update.newValue);
|
||||
Object.assign(diffObject, newContent);
|
||||
});
|
||||
}
|
||||
|
||||
// Unpacking a layout group
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function createLayoutGroup(layoutGroup: any): LayoutGroup {
|
||||
// Detect if this has already been parsed and, if so, return it as-is so this function can be idempotent
|
||||
if ("columnWidgets" in layoutGroup || "rowWidgets" in layoutGroup || "tableWidgets" in layoutGroup || ("name" in layoutGroup && "layout" in layoutGroup)) return layoutGroup;
|
||||
|
||||
if (layoutGroup.column) {
|
||||
const columnWidgets = layoutGroup.column.columnWidgets.map(parseWidgetInstance);
|
||||
|
||||
const result: WidgetSpanColumn = { columnWidgets };
|
||||
return result;
|
||||
}
|
||||
|
||||
if (layoutGroup.row) {
|
||||
const result: WidgetSpanRow = { rowWidgets: layoutGroup.row.rowWidgets.map(parseWidgetInstance) };
|
||||
return result;
|
||||
}
|
||||
|
||||
if (layoutGroup.section) {
|
||||
const result: WidgetSection = {
|
||||
name: layoutGroup.section.name,
|
||||
description: layoutGroup.section.description,
|
||||
visible: layoutGroup.section.visible,
|
||||
pinned: layoutGroup.section.pinned,
|
||||
id: layoutGroup.section.id,
|
||||
layout: layoutGroup.section.layout.map(createLayoutGroup),
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
if (layoutGroup.table) {
|
||||
const result: WidgetTable = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tableWidgets: layoutGroup.table.tableWidgets.map((row: any) => row.map(parseWidgetInstance)),
|
||||
unstyled: layoutGroup.table.unstyled,
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new Error("Layout row type does not exist");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { sveltePreprocess } from "svelte-preprocess";
|
||||
|
||||
export default {
|
||||
preprocess: sveltePreprocess(),
|
||||
compilerOptions: /** @type {import("svelte/compiler").ModuleCompileOptions} */ ({
|
||||
warningFilter: (warning) => !warning.code.startsWith("a11y_") && !["css_unused_selector"].includes(warning.code),
|
||||
}),
|
||||
};
|
||||
+1
-20
@@ -3,7 +3,6 @@ import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import { sveltePreprocess } from "svelte-preprocess";
|
||||
import { defineConfig } from "vite";
|
||||
import type { PluginOption } from "vite";
|
||||
import { DynamicPublicDirectory as viteMultipleAssets } from "vite-multiple-assets";
|
||||
@@ -31,25 +30,7 @@ export default defineConfig(({ mode }) => {
|
||||
|
||||
function plugins(mode: string): PluginOption[] {
|
||||
const plugins = [
|
||||
svelte({
|
||||
preprocess: [sveltePreprocess()],
|
||||
onwarn(warning, defaultHandler) {
|
||||
const suppressed = [
|
||||
"css-unused-selector", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
"vite-plugin-svelte-css-no-scopable-elements", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
"a11y-no-static-element-interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
"a11y-no-noninteractive-element-interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
"a11y-click-events-have-key-events", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
"a11y_consider_explicit_label", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
"a11y_click_events_have_key_events", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
"a11y_no_noninteractive_element_interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
"a11y_no_static_element_interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json`
|
||||
];
|
||||
if (suppressed.includes(warning.code)) return;
|
||||
|
||||
defaultHandler?.(warning);
|
||||
},
|
||||
}),
|
||||
svelte(),
|
||||
viteMultipleAssets(
|
||||
// Additional static asset directories besides `public/`
|
||||
[
|
||||
|
||||
+11
-8
@@ -1,14 +1,17 @@
|
||||
# Overview of `/frontend/wasm/`
|
||||
|
||||
## WASM wrapper API: `src/editor_api.rs`
|
||||
Provides bindings for JS to call functions defined in this file, and for FrontendMessages to be sent from Rust back to JS in the form of a callback to the subscription router. This WASM wrapper crate, since it's written in Rust, is able to call into the Editor crate's codebase and send FrontendMessages back to JS.
|
||||
## Wasm wrapper API: `src/editor_api.rs`
|
||||
|
||||
Provides bindings for JS to call functions defined in this file, and for `FrontendMessage`s to be sent from Rust back to JS in the form of a callback to the subscription router. This Wasm wrapper crate, since it's written in Rust, is able to call into the Editor crate's codebase and send `FrontendMessage`s back to JS.
|
||||
|
||||
## WASM wrapper helper code: `src/helpers.rs`
|
||||
Assorted function and struct definitions used in the WASM wrapper.
|
||||
## Wasm wrapper helper code: `src/helpers.rs`
|
||||
|
||||
## WASM wrapper initialization: `src/lib.rs`
|
||||
Entry point for the Rust entire codebase in the WASM environment. Initializes the WASM module and persistent storage for editor and WASM wrapper instances.
|
||||
Assorted function and struct definitions used in the Wasm wrapper.
|
||||
|
||||
## WASM wrapper tests: `tests/`
|
||||
We currently have no WASM wrapper tests, but this is where they would go.
|
||||
## Native communication: `src/native_communication.rs`
|
||||
|
||||
Handles receiving serialized `FrontendMessage`s from the native desktop app via an `ArrayBuffer` and forwarding them to JS through the editor handle.
|
||||
|
||||
## Wasm wrapper initialization: `src/lib.rs`
|
||||
|
||||
Entry point for the Rust codebase in the Wasm environment. Sets up panic hooks and logging, and defines thread-local storage for the editor instance, editor handle, message buffer, and panic dialog callback.
|
||||
|
||||
@@ -69,7 +69,7 @@ pub fn is_platform_native() -> bool {
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone)]
|
||||
pub struct EditorHandle {
|
||||
/// This callback is called by the editor's dispatcher when directing FrontendMessages from Rust to JS
|
||||
/// This callback is called by the editor's dispatcher when directing `FrontendMessage`s from Rust to JS
|
||||
frontend_message_handler_callback: js_sys::Function,
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ impl EditorHandle {
|
||||
log::error!("Failed to serialize message");
|
||||
return;
|
||||
};
|
||||
crate::native_communcation::send_message_to_cef(serialized_message)
|
||||
crate::native_communication::send_message_to_cef(serialized_message)
|
||||
}
|
||||
|
||||
// Sends a FrontendMessage to JavaScript
|
||||
@@ -190,7 +190,7 @@ impl EditorHandle {
|
||||
#[wasm_bindgen(js_name = initAfterFrontendReady)]
|
||||
pub fn init_after_frontend_ready(&self) {
|
||||
#[cfg(feature = "native")]
|
||||
crate::native_communcation::initialize_native_communication();
|
||||
crate::native_communication::initialize_native_communication();
|
||||
|
||||
self.dispatch(PortfolioMessage::Init);
|
||||
|
||||
@@ -955,16 +955,11 @@ pub fn sample_interpolated_gradient(position: Vec<f64>, midpoint: Vec<f64>, colo
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = evaluateGradientAtPosition)]
|
||||
pub fn evaluate_gradient_at_position(t: f64, position: Vec<f64>, midpoint: Vec<f64>, color: Vec<JsValue>) -> Object {
|
||||
pub fn evaluate_gradient_at_position(t: f64, position: Vec<f64>, midpoint: Vec<f64>, color: Vec<JsValue>) -> JsValue {
|
||||
let color = color.into_iter().filter_map(|c| serde_wasm_bindgen::from_value(c).ok()).collect();
|
||||
let color = GradientStops { position, midpoint, color }.evaluate(t);
|
||||
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &JsValue::from_str("red"), &JsValue::from_f64(color.r() as f64)).unwrap();
|
||||
Reflect::set(&obj, &JsValue::from_str("green"), &JsValue::from_f64(color.g() as f64)).unwrap();
|
||||
Reflect::set(&obj, &JsValue::from_str("blue"), &JsValue::from_f64(color.b() as f64)).unwrap();
|
||||
Reflect::set(&obj, &JsValue::from_str("alpha"), &JsValue::from_f64(color.a() as f64)).unwrap();
|
||||
obj
|
||||
serde_wasm_bindgen::to_value(&color).unwrap()
|
||||
}
|
||||
|
||||
/// Helper function for calling JS's `requestAnimationFrame` with the given closure
|
||||
|
||||
@@ -6,7 +6,7 @@ extern crate log;
|
||||
|
||||
pub mod editor_api;
|
||||
pub mod helpers;
|
||||
pub mod native_communcation;
|
||||
pub mod native_communication;
|
||||
|
||||
use editor::messages::prelude::*;
|
||||
use std::panic;
|
||||
|
||||
Reference in New Issue
Block a user