WIP: Defer message

This commit is contained in:
Adam
2025-09-08 19:40:37 -07:00
parent 8caf9317a5
commit c994cdeced
15 changed files with 401 additions and 1047 deletions
+9 -800
View File
@@ -180,33 +180,6 @@
return `M-2,-2 L${nodeWidth + 2},-2 L${nodeWidth + 2},${nodeHeight + 2} L-2,${nodeHeight + 2}z ${rectangles.join(" ")}`;
}
function inputTooltip(value: FrontendGraphInput): string {
return dataTypeTooltip(value) + "\n\n" + inputConnectedToText(value) + "\n\n";
}
function outputTooltip(value: FrontendGraphOutput): string {
return dataTypeTooltip(value) + "\n\n" + outputConnectedToText(value);
}
function dataTypeTooltip(value: FrontendGraphInput | FrontendGraphOutput): string {
return `Data Type: ${value.resolvedType}`;
}
// function validTypesText(value: FrontendGraphInput): string {
// const validTypes = value.validTypes.length > 0 ? value.validTypes.map((x) => `• ${x}`).join("\n") : "None";
// return `Valid Types:\n${validTypes}`;
// }
function outputConnectedToText(output: FrontendGraphOutput): string {
if (output.connectedTo.length === 0) return "Connected to nothing";
return `Connected to:\n${output.connectedTo.join("\n")}`;
}
function inputConnectedToText(input: FrontendGraphInput): string {
return `Connected to:\n${input.connectedToString}`;
}
function collectExposedInputsOutputs(
inputs: (FrontendGraphInput | undefined)[],
outputs: (FrontendGraphOutput | undefined)[],
@@ -326,7 +299,6 @@
style:--offset-left={($nodeGraph.updateImportsExports.importPosition.x - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.importPosition.y - 8) / 24 + index}
>
<title>{outputTooltip(frontendOutput)}</title>
{#if frontendOutput.connectedTo.length > 0}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{:else}
@@ -407,7 +379,6 @@
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition.x - 8) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition.y - 8) / 24 + index}
>
<title>{inputTooltip(frontendInput)}</title>
{#if frontendInput.connectedTo !== "nothing"}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{:else}
@@ -511,6 +482,15 @@
</div>
</div>
<!-- Input/Output tooltip widget -->
{#if $nodeGraph.tooltipPosition}
<div class="connector-tooltip">
<div class="connector-tooltip-div" style:--offset-left={`${$nodeGraph.tooltipPosition.x + 10}px`} style:--offset-top={`${$nodeGraph.tooltipPosition.y + 10}px`}>
{$nodeGraph.tooltipText}
</div>
</div>
{/if}
<!-- Box selection widget -->
{#if $nodeGraph.selectionBox}
<div
@@ -523,33 +503,6 @@
{/if}
<style lang="scss" global>
.graph-background {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: var(--color-2-mildblack);
opacity: var(--fade-artwork);
// We're displaying the dotted grid in a pseudo-element because `image-rendering` is an inherited property and we don't want it to apply to child elements
&::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
background-size: var(--grid-spacing) var(--grid-spacing);
background-position: calc(var(--grid-offset-x) - var(--grid-dot-radius)) calc(var(--grid-offset-y) - var(--grid-dot-radius));
background-image: radial-gradient(circle at var(--grid-dot-radius) var(--grid-dot-radius), var(--color-3-darkgray) var(--grid-dot-radius), transparent 0);
background-repeat: repeat;
image-rendering: pixelated;
mix-blend-mode: screen;
opacity: var(--fade-artwork);
}
}
.native-node-graph-ui {
position: absolute;
top: 0;
@@ -745,750 +698,6 @@
}
}
.breadcrumb-trail-buttons {
margin-top: 8px;
margin-left: 8px;
}
.context-menu {
width: max-content;
position: absolute;
box-sizing: border-box;
padding: 5px;
z-index: 3;
background-color: var(--color-3-darkgray);
border-radius: 4px;
.toggle-layer-or-node .text-label {
line-height: 24px;
margin-right: 8px;
}
.merge-selected-nodes {
justify-content: center;
}
}
.click-targets {
position: absolute;
pointer-events: none;
width: 100%;
height: 100%;
z-index: 10;
svg {
overflow: visible;
width: 100%;
height: 100%;
stroke-width: 1;
fill: none;
.layer {
stroke: yellow;
}
.node {
stroke: blue;
}
.connector {
stroke: green;
}
.visibility {
stroke: red;
}
.all-nodes-bounding-box {
stroke: purple;
}
.modify-import-export {
stroke: orange;
}
}
}
.wires {
pointer-events: none;
position: absolute;
width: 100%;
height: 100%;
svg {
width: 100%;
height: 100%;
overflow: visible;
path {
fill: none;
stroke: var(--data-color-dim);
stroke-width: var(--data-line-width);
stroke-dasharray: var(--data-dasharray);
}
}
}
.imports-and-exports {
width: 100%;
height: 100%;
position: absolute;
// Keeps the connectors above the wires
z-index: 1;
.connector {
position: absolute;
width: 8px;
height: 8px;
margin-top: 4px;
margin-left: 5px;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
}
.reorder-bar {
position: absolute;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
width: 50px;
height: 2px;
background: white;
}
.plus {
position: absolute;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
}
.edit-import-export {
position: absolute;
display: flex;
align-items: center;
top: calc(var(--offset-top) * 24px);
margin-top: -5px;
height: 24px;
&.separator-bottom::after,
&.separator-top::before {
content: "";
position: absolute;
background: var(--color-8-uppergray);
height: 1px;
left: -4px;
right: -4px;
}
&.separator-bottom::after {
bottom: -1px;
}
&.separator-top::before {
top: 0;
}
&.import {
right: calc(100% - var(--offset-left) * 24px);
}
&.export {
left: calc(var(--offset-left) * 24px + 17px);
}
.import-text {
text-align: right;
text-wrap: nowrap;
}
.export-text {
text-wrap: nowrap;
}
.import-text-input {
text-align: right;
}
.remove-button-import {
margin-left: 3px;
}
.remove-button-export {
margin-right: 3px;
}
.reorder-drag-grip {
width: 8px;
height: 24px;
background-position: 2px 8px;
border-radius: 2px;
margin: -6px 0;
background-image: var(--icon-drag-grip-hover);
}
}
}
}
}
.layers-and-nodes {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
.layer,
.node {
position: absolute;
display: flex;
left: calc(var(--offset-left) * 24px);
top: calc(var(--offset-top) * 24px);
// TODO: Reenable the `transition` property below after dealing with all edge cases where the wires need to be updated until the transition is complete
// transition: top 0.1s cubic-bezier(0, 0, 0.2, 1), left 0.1s cubic-bezier(0, 0, 0.2, 1); // Update `DRAG_SMOOTHING_TIME` in the JS above.
// TODO: Reenable the `backdrop-filter` property once a solution can be found for the black whole-page flickering problems it causes in Chrome.
// TODO: Additionally, find a solution for this having no effect in Firefox due to a browser bug caused when the two
// ancestor elements, `.graph` and `.panel`, each have the simultaneous pairing of `overflow: hidden` and `border-radius`.
// See: https://stackoverflow.com/questions/75137879/bug-with-backdrop-filter-in-firefox
// backdrop-filter: blur(4px);
background: rgba(var(--color-0-black-rgb), 0.33);
.node-error {
position: absolute;
width: max-content;
white-space: pre-wrap;
max-width: 600px;
line-height: 18px;
color: var(--color-2-mildblack);
background: var(--color-error-red);
padding: 8px;
border-radius: 4px;
bottom: calc(100% + 12px);
z-index: -1;
transition: opacity 0.2s;
opacity: 0.5;
// Tail
&::after {
content: "";
position: absolute;
left: 6px;
bottom: -8px;
width: 0;
height: 0;
border-style: solid;
border-width: 8px 6px 0 6px;
border-color: var(--color-error-red) transparent transparent transparent;
}
&.hover {
opacity: 0;
z-index: 1;
pointer-events: none;
}
&.faded:hover + .hover {
opacity: 1;
}
&.faded:hover {
z-index: 2;
opacity: 1;
-webkit-user-select: text;
user-select: text;
transition:
opacity 0.2s,
z-index 0s 0.2s;
&::selection {
background-color: var(--color-e-nearwhite);
// Target only Safari
@supports (background: -webkit-named-image(i)) {
& {
// Setting an alpha value opts out of Safari's "fancy" (but not visible on dark backgrounds) selection highlight rendering
// https://stackoverflow.com/a/71753552/775283
background-color: rgba(var(--color-e-nearwhite-rgb), calc(254 / 255));
}
}
}
}
}
&::after {
content: "";
position: absolute;
box-sizing: border-box;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
clip-path: var(--clip-path-id);
}
.border-mask {
position: absolute;
top: 0;
}
&.disabled {
background: rgba(var(--color-4-dimgray-rgb), 0.33);
color: var(--color-a-softgray);
.icon-label {
fill: var(--color-a-softgray);
}
}
&.previewed::after {
border: 1px dashed var(--data-color);
}
.connectors {
position: absolute;
// Keeps the connectors above the wires
z-index: 1;
margin-top: -24px;
&.input {
left: -3px;
}
&.output {
right: -5px;
}
}
.connector {
// Double the intended value because of margin collapsing, but for the first and last we divide it by two as intended
margin: calc(24px - 8px) 0;
width: 8px;
height: 8px;
}
.text-label {
overflow: hidden;
text-overflow: ellipsis;
}
}
.layer {
border-radius: 8px;
--extra-width-to-reach-grid-multiple: 8px;
--node-chain-area-left-extension: 0;
// Keep this equation in sync with the equivalent one in the Svelte template `<clipPath><path d="layerBorderMask(...)" /></clipPath>` above, as well as the `left` connector offset CSS rule above in `.connectors.input` above.
width: calc((var(--layer-area-width) - 0.5) * 24px);
padding-left: calc(var(--node-chain-area-left-extension) * 24px);
margin-left: calc((0.5 - var(--node-chain-area-left-extension)) * 24px);
&::after {
border: 1px solid var(--color-5-dullgray);
border-radius: 8px;
}
&.selected {
background: rgba(var(--color-5-dullgray-rgb), 0.33);
&.in-selected-network {
background: rgba(var(--color-6-lowergray-rgb), 0.33);
}
}
.thumbnail {
background: var(--color-2-mildblack);
border: 1px solid var(--data-color-dim);
border-radius: 2px;
position: relative;
box-sizing: border-box;
height: 48px;
// We shorten the width by 1px on the left and right so the inner thumbnail graphic maintains a perfect 3:2 aspect ratio
width: calc(72px - 2px);
margin: 0 1px;
&::before {
content: "";
background-image: var(--color-transparent-checkered-background);
background-size: var(--color-transparent-checkered-background-size);
background-position: var(--color-transparent-checkered-background-position);
background-repeat: var(--color-transparent-checkered-background-repeat);
}
&::before,
svg:not(.connector) {
pointer-events: none;
position: absolute;
margin: auto;
top: 1px;
left: 1px;
width: calc(100% - 2px);
height: calc(100% - 2px);
}
.connector {
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
height: 12px;
&.top {
top: -13px;
}
&.bottom {
bottom: -13px;
}
}
}
.details {
margin: 0 8px;
.text-label {
white-space: nowrap;
line-height: 48px;
}
}
.solo-drag-grip {
width: 8px;
height: 24px;
background-position: 2px 8px;
right: calc(-12px + 24px);
border-radius: 2px;
}
.solo-drag-grip:hover,
&.selected .solo-drag-grip {
background-image: var(--icon-drag-grip);
&:hover {
background-image: var(--icon-drag-grip-hover);
}
}
.visibility {
position: absolute;
right: -12px;
}
.input.connectors {
left: calc(-3px + var(--node-chain-area-left-extension) * 24px - 36px);
}
.solo-drag-grip,
.visibility,
.input.connectors,
.input.connectors .connector {
position: absolute;
margin: auto 0;
top: 0;
bottom: 0;
}
.input.connectors .connector {
left: 24px;
}
}
.node {
flex-direction: column;
border-radius: 2px;
width: 120px;
top: calc((var(--offset-top) + 0.5) * 24px);
&::after {
border: 1px solid var(--data-color-dim);
border-radius: 2px;
}
&.selected {
.primary {
background: rgba(var(--color-f-white-rgb), 0.15);
&.in-selected-network {
background: rgba(var(--color-f-white-rgb), 0.2);
}
}
.secondary {
background: rgba(var(--color-f-white-rgb), 0.1);
&.in-selected-network {
background: rgba(var(--color-f-white-rgb), 0.15);
}
}
}
.connector {
&:first-of-type {
margin-top: calc((24px - 8px) / 2);
&:not(.primary-connector) {
margin-top: calc((24px - 8px) / 2 + 24px);
}
}
&:last-of-type {
margin-bottom: calc((24px - 8px) / 2);
}
}
.primary {
display: flex;
align-items: center;
position: relative;
width: 100%;
height: 24px;
border-radius: 2px 2px 0 0;
background: rgba(var(--color-f-white-rgb), 0.05);
&.no-secondary-section {
border-radius: 2px;
}
.icon-label {
display: none; // Remove after we have unique icons for the nodes
margin: 0 8px;
}
.text-label {
// margin-right: 4px; // Restore after reenabling icon-label
margin: 0 8px;
}
}
.secondary {
display: flex;
flex-direction: column;
width: 100%;
position: relative;
.secondary-row {
position: relative;
display: flex;
align-items: center;
margin: 0 8px;
width: calc(100% - 8px - 8px);
height: 24px;
&:last-of-type {
border-radius: 0 0 2px 2px;
}
.text-label {
width: 100%;
}
&.output {
flex-direction: row-reverse;
text-align: right;
svg {
width: 30px;
height: 20px;
}
}
}
&::before {
left: 0;
}
&::after {
right: 0;
}
}
}
}
.wire {
position: absolute;
overflow: visible;
top: 0;
left: 0;
path {
fill: none;
stroke: var(--data-color-dim);
stroke-width: var(--data-line-width);
stroke-dasharray: var(--data-dasharray);
}
}
.graph {
position: relative;
overflow: hidden;
display: flex;
flex-direction: row;
flex-grow: 1;
> img {
position: absolute;
bottom: 0;
}
.breadcrumb-trail-buttons {
margin-top: 8px;
margin-left: 8px;
}
.context-menu {
width: max-content;
position: absolute;
box-sizing: border-box;
padding: 5px;
z-index: 3;
background-color: var(--color-3-darkgray);
border-radius: 4px;
.toggle-layer-or-node .text-label {
line-height: 24px;
margin-right: 8px;
}
.merge-selected-nodes {
justify-content: center;
}
}
.click-targets {
position: absolute;
pointer-events: none;
width: 100%;
height: 100%;
z-index: 10;
svg {
overflow: visible;
width: 100%;
height: 100%;
stroke-width: 1;
fill: none;
.layer {
stroke: yellow;
}
.node {
stroke: blue;
}
.connector {
stroke: green;
}
.visibility {
stroke: red;
}
.all-nodes-bounding-box {
stroke: purple;
}
.modify-import-export {
stroke: orange;
}
}
}
.imports-and-exports {
width: 100%;
height: 100%;
position: absolute;
// Keeps the connectors above the wires
z-index: 1;
.connector {
position: absolute;
width: 8px;
height: 8px;
margin-top: 4px;
margin-left: 5px;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
}
.reorder-bar {
position: absolute;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
width: 50px;
height: 2px;
background: white;
}
.plus {
position: absolute;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
}
.edit-import-export {
position: absolute;
display: flex;
align-items: center;
top: calc(var(--offset-top) * 24px);
margin-top: -5px;
height: 24px;
&.separator-bottom::after,
&.separator-top::before {
content: "";
position: absolute;
background: var(--color-8-uppergray);
height: 1px;
left: -4px;
right: -4px;
}
&.separator-bottom::after {
bottom: -1px;
}
&.separator-top::before {
top: 0;
}
&.import {
right: calc(100% - var(--offset-left) * 24px);
}
&.export {
left: calc(var(--offset-left) * 24px + 17px);
}
.import-text {
text-align: right;
text-wrap: nowrap;
}
.export-text {
text-wrap: nowrap;
}
.import-text-input {
text-align: right;
}
.remove-button-import {
margin-left: 3px;
}
.remove-button-export {
margin-right: 3px;
}
.reorder-drag-grip {
width: 8px;
height: 24px;
background-position: 2px 8px;
border-radius: 2px;
margin: -6px 0;
background-image: var(--icon-drag-grip-hover);
}
}
}
}
.box-selection {
position: absolute;
pointer-events: none;
+35 -45
View File
@@ -128,9 +128,14 @@ export class UpdateNodeThumbnail extends JsMessage {
readonly value!: string;
}
export class UpdateTooltip extends JsMessage {
readonly position!: XY | undefined;
readonly text!: string;
}
export class UpdateOpenDocumentsList extends JsMessage {
@Type(() => OpenDocument)
readonly openDocuments!: OpenDocument[];
@Type(() => FrontendDocumentDetails)
readonly openDocuments!: FrontendDocumentDetails[];
}
export class WirePathInProgress {
@@ -143,54 +148,29 @@ export class UpdateWirePathInProgress extends JsMessage {
readonly wirePathInProgress!: WirePathInProgress | undefined;
}
export class OpenDocument {
readonly id!: bigint;
@Type(() => DocumentDetails)
readonly details!: DocumentDetails;
get displayName(): string {
return this.details.displayName;
}
}
export class DocumentDetails {
// Allows the auto save system to use a string for the id rather than a BigInt.
// IndexedDb does not allow for BigInts as primary keys.
// TypeScript does not allow subclasses to change the type of class variables in subclasses.
// It is an abstract class to point out that it should not be instantiated directly.
export abstract class DocumentDetails {
readonly name!: string;
readonly isAutoSaved!: boolean;
readonly isSaved!: boolean;
// This field must be provided by the subclass implementation
// readonly id!: bigint | string;
get displayName(): string {
return `${this.name}${this.isSaved ? "" : "*"}`;
}
}
<<<<<<< HEAD
export class Box {
readonly startX!: number;
readonly startY!: number;
readonly endX!: number;
readonly endY!: number;
export class FrontendDocumentDetails extends DocumentDetails {
readonly id!: bigint;
}
export type FrontendClickTargets = {
readonly nodeClickTargets: string[];
readonly layerClickTargets: string[];
readonly connectorClickTargets: string[];
readonly iconClickTargets: string[];
readonly allNodesBoundingBox: string;
readonly importExportsBoundingBox: string;
readonly modifyImportExport: string[];
};
export type ContextMenuInformation = {
contextMenuCoordinates: XY;
contextMenuData: "CreateNode" | { type: "CreateNode"; compatibleType: string } | { nodeId: bigint; currentlyIsNode: boolean };
};
=======
export class FrontendDocumentDetails extends DocumentDetails {
readonly id!: bigint;
@@ -342,20 +322,21 @@ export class WireUpdate {
readonly wirePathUpdate!: WirePath | undefined;
}
export class TriggerPersistenceWriteDocument extends JsMessage {
// Use a string since IndexedDB can not use BigInts for keys
export class IndexedDbDocumentDetails extends DocumentDetails {
@Transform(({ value }: { value: bigint }) => value.toString())
documentId!: string;
id!: string;
}
export class TriggerIndexedDbWriteDocument extends JsMessage {
document!: string;
@Type(() => DocumentDetails)
details!: DocumentDetails;
@Type(() => IndexedDbDocumentDetails)
details!: IndexedDbDocumentDetails;
version!: string;
}
export class TriggerPersistenceRemoveDocument extends JsMessage {
export class TriggerIndexedDbRemoveDocument extends JsMessage {
// Use a string since IndexedDB can not use BigInts for keys
@Transform(({ value }: { value: bigint }) => value.toString())
documentId!: string;
@@ -1488,6 +1469,7 @@ export class WidgetDiffUpdate extends JsMessage {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
@Transform(({ value }: { value: any }) => createWidgetDiff(value))
diff!: WidgetDiff[];
}
@@ -1522,6 +1504,7 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
return targetLayout;
}
// This is a path traversal so we can assume from the backend that it exists
if (targetLayout && "action" in targetLayout) return targetLayout.children![index];
return targetLayout?.[index];
@@ -1542,6 +1525,7 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
diffObject.length = 0;
}
// Remove all of the keys from the old object
Object.keys(diffObject).forEach((key) => delete (diffObject as any)[key]);
// Assign keys to the new object
@@ -1574,6 +1558,7 @@ export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSect
}
// Unpacking rust types to more usable type in the frontend
function createWidgetDiff(diffs: any[]): WidgetDiff[] {
return diffs.map((diff) => {
const { widgetPath, newValue } = diff;
@@ -1592,6 +1577,7 @@ function createWidgetDiff(diffs: any[]): WidgetDiff[] {
}
// Unpacking a layout group
function createLayoutGroup(layoutGroup: any): LayoutGroup {
if (layoutGroup.column) {
const columnWidgets = hoistWidgetHolders(layoutGroup.column.columnWidgets);
@@ -1649,6 +1635,7 @@ export class UpdateMenuBarLayout extends JsMessage {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
@Transform(({ value }: { value: any }) => createMenuLayout(value))
layout!: MenuBarEntry[];
}
@@ -1671,6 +1658,7 @@ function createMenuLayout(menuBarEntry: any[]): MenuBarEntry[] {
children: createMenuLayoutRecursive(entry.children),
}));
}
function createMenuLayoutRecursive(children: any[][]): MenuBarEntry[][] {
return children.map((groups) =>
groups.map((entry) => ({
@@ -1683,6 +1671,7 @@ function createMenuLayoutRecursive(children: any[][]): MenuBarEntry[][] {
}
// `any` is used since the type of the object should be known from the Rust side
type JSMessageFactory = (data: any, wasm: WebAssembly.Memory, handle: EditorHandle) => JsMessage;
type MessageMaker = typeof JsMessage | JSMessageFactory;
@@ -1702,8 +1691,8 @@ export const messageMakers: Record<string, MessageMaker> = {
TriggerFetchAndOpenDocument,
TriggerFontLoad,
TriggerImport,
TriggerPersistenceRemoveDocument,
TriggerPersistenceWriteDocument,
TriggerIndexedDbRemoveDocument,
TriggerIndexedDbWriteDocument,
TriggerLoadFirstAutoSaveDocument,
TriggerLoadPreferences,
TriggerLoadRestAutoSaveDocuments,
@@ -1754,6 +1743,7 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateLayersPanelState,
UpdateToolOptionsLayout,
UpdateToolShelfLayout,
UpdateTooltip,
UpdateViewportHolePunch,
UpdateWirePathInProgress,
UpdateWorkingColorsLayout,
+135 -100
View File
@@ -5,8 +5,11 @@
// on the dispatcher messaging system and more complex Rust data types.
//
use crate::helpers::translate_key;
use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER};
#[cfg(not(feature = "native"))]
use crate::wasm_node_graph_ui_executor::WasmNodeGraphUIExecutor;
use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER, WASM_NODE_GRAPH_EXECUTOR};
use editor::consts::FILE_EXTENSION;
use editor::dispatcher::EditorOutput;
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@@ -134,80 +137,9 @@ pub struct EditorHandle {
frontend_message_handler_callback: js_sys::Function,
}
// Defined separately from the `impl` block below since this `impl` block lacks the `#[wasm_bindgen]` attribute.
// Quirks in wasm-bindgen prevent functions in `#[wasm_bindgen]` `impl` blocks from being made publicly accessible from Rust.
impl EditorHandle {
pub fn send_frontend_message_to_js_rust_proxy(&self, message: FrontendMessage) {
self.send_frontend_message_to_js(message);
}
}
#[wasm_bindgen]
impl EditorHandle {
#[cfg(not(feature = "native"))]
#[wasm_bindgen(constructor)]
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
let editor = Editor::new();
let editor_handle = EditorHandle { frontend_message_handler_callback };
if EDITOR.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor))).is_none() {
log::error!("Attempted to initialize the editor more than once");
}
if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() {
log::error!("Attempted to initialize the editor handle more than once");
}
editor_handle
}
#[cfg(feature = "native")]
#[wasm_bindgen(constructor)]
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
let editor_handle = EditorHandle { frontend_message_handler_callback };
if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() {
log::error!("Attempted to initialize the editor handle more than once");
}
editor_handle
}
// Sends a message to the dispatcher in the Editor Backend
#[cfg(not(feature = "native"))]
fn dispatch<T: Into<Message>>(&self, message: T) {
// Process no further messages after a crash to avoid spamming the console
use crate::MESSAGE_BUFFER;
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
return;
}
// Get the editor, dispatch the message, and store the `FrontendMessage` queue response
let frontend_messages = EDITOR.with(|editor| {
let mut guard = editor.try_lock();
let Ok(Some(editor)) = guard.as_deref_mut() else {
// Enqueue messages which can't be procssed currently
MESSAGE_BUFFER.with_borrow_mut(|buffer| buffer.push(message.into()));
return vec![];
};
editor.handle_message(message)
});
// Send each `FrontendMessage` to the JavaScript frontend
for message in frontend_messages.into_iter() {
self.send_frontend_message_to_js(message);
}
}
#[cfg(feature = "native")]
fn dispatch<T: Into<Message>>(&self, message: T) {
let message: Message = message.into();
let Ok(serialized_message) = ron::to_string(&message) else {
log::error!("Failed to serialize message");
return;
};
crate::native_communcation::send_message_to_cef(serialized_message)
}
// Sends a FrontendMessage to JavaScript
fn send_frontend_message_to_js(&self, mut message: FrontendMessage) {
pub fn send_frontend_message_to_js(&self, mut message: FrontendMessage) {
if let FrontendMessage::UpdateImageData { ref image_data } = message {
let new_hash = calculate_hash(image_data);
let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed);
@@ -234,6 +166,90 @@ impl EditorHandle {
error!("While handling FrontendMessage {:?}, JavaScript threw an error:\n{:?}", message.to_discriminant().local_name(), error,)
}
}
}
#[wasm_bindgen]
impl EditorHandle {
#[cfg(not(feature = "native"))]
#[wasm_bindgen(constructor)]
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
let editor = Editor::new();
let editor_handle = EditorHandle { frontend_message_handler_callback };
let node_graph_executor = WasmNodeGraphUIExecutor::new();
if EDITOR.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor))).is_none() {
log::error!("Attempted to initialize the editor more than once");
}
if WASM_NODE_GRAPH_EXECUTOR.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(node_graph_executor))).is_none() {
log::error!("Attempted to initialize the editor more than once");
}
if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() {
log::error!("Attempted to initialize the editor handle more than once");
}
editor_handle
}
#[cfg(feature = "native")]
#[wasm_bindgen(constructor)]
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
let editor_handle = EditorHandle { frontend_message_handler_callback };
if EDITOR_HANDLE.with(|handle| handle.lock().ok().map(|mut guard| *guard = Some(editor_handle.clone()))).is_none() {
log::error!("Attempted to initialize the editor handle more than once");
}
editor_handle
}
// Sends a message to the dispatcher in the Editor Backend
#[cfg(not(feature = "native"))]
fn dispatch<T: Into<Message>>(&self, message: T) {
// Process no further messages after a crash to avoid spamming the console
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
return;
}
let _ = editor(|editor| {
self.process_messages(std::iter::once(message.into()), editor);
});
}
#[cfg(feature = "native")]
fn dispatch<T: Into<Message>>(&self, message: T) {
let message: Message = message.into();
let Ok(serialized_message) = ron::to_string(&message) else {
log::error!("Failed to serialize message");
return;
};
crate::native_communcation::send_message_to_cef(serialized_message)
}
// Messages can come from the runtime, browser, or a timed callback. This processes them in the editor and does all the side effects
// Like updating the frontend and node graph ui network. Some side effects are deduplicated and produce other side effects.
fn process_messages(&self, messages: impl IntoIterator<Item = Message>, editor_param: &mut Editor) {
// Get the editor, dispatch the message, and store the `FrontendMessage` queue response
for output in messages.into_iter().flat_map(|message| editor_param.handle_message(message)).collect::<Vec<_>>() {
match output {
EditorOutput::RequestNativeNodeGraphRender { compilation_request } => {
let res = executor(|executor| executor.compilation_request(compilation_request));
if let Err(_) = res {
log::error!("Could not borrow executor in process_messages_in_editor");
}
}
EditorOutput::RequestDeferredMessage { message, timeout } => {
let callback = Closure::once_into_js(move || {
editor_and_handle(|editor, handle| {
handle.process_messages(std::iter::once(*message), editor);
});
});
window()
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(callback.as_ref().unchecked_ref(), timeout.as_millis() as i32)
.unwrap();
}
EditorOutput::FrontendMessage { frontend_message } => {
self.send_frontend_message_to_js(frontend_message);
}
}
}
}
// ========================================================================
// Add additional JS -> Rust wrapper functions below as needed for calling
@@ -264,6 +280,20 @@ impl EditorHandle {
#[cfg(not(feature = "native"))]
wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation());
// Poll the UI node graph
#[cfg(not(feature = "native"))]
let result = editor(|editor| {
let node_graph_response = executor(|executor| executor.poll_node_graph_ui_evaluation(editor));
match node_graph_response {
Ok(node_graph_ui_messages) => handle(|handle| handle.process_messages(node_graph_ui_messages, editor)),
Err(_) => log::error!("Could not get executor in frame loop"),
}
});
if let Err(_) = result {
log::error!("Could not get editor in frame loop");
}
if !EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
handle(|handle| {
// Process all messages that have been queued up
@@ -277,7 +307,7 @@ impl EditorHandle {
timestamp: js_sys::Date::now() as u64,
});
handle.dispatch(AnimationMessage::IncrementFrameCounter);
handle.dispatch(DeferMessage::CheckDeferredMessages);
// Used by auto-panning, but this could possibly be refactored in the future, see:
// <https://github.com/GraphiteEditor/Graphite/pull/2562#discussion_r2041102786>
handle.dispatch(BroadcastMessage::TriggerEvent(EventMessage::AnimationFrame));
@@ -466,7 +496,6 @@ impl EditorHandle {
document_is_saved,
document_serialized_content,
to_front,
select_after_open: false,
};
self.dispatch(message);
}
@@ -971,39 +1000,49 @@ fn set_timeout(f: &Closure<dyn FnMut()>, delay: Duration) {
/// Provides access to the `Editor` by calling the given closure with it as an argument.
#[cfg(not(feature = "native"))]
fn editor<T: Default>(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> T {
fn editor<T>(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> Result<T, ()> {
EDITOR.with(|editor| {
let mut guard = editor.try_lock();
let Ok(Some(editor)) = guard.as_deref_mut() else {
log::error!("Failed to borrow editor");
return T::default();
return Err(());
};
Ok(callback(editor))
})
}
#[cfg(not(feature = "native"))]
fn executor<T>(callback: impl FnOnce(&mut WasmNodeGraphUIExecutor) -> T) -> Result<T, ()> {
WASM_NODE_GRAPH_EXECUTOR.with(|executor| {
let mut guard = executor.try_lock();
let Ok(Some(executor)) = guard.as_deref_mut() else {
return Err(());
};
callback(editor)
Ok(callback(executor))
})
}
/// Provides access to the `EditorHandle` by calling the given closure with them as arguments.
pub(crate) fn handle(callback: impl FnOnce(&mut EditorHandle)) {
EDITOR_HANDLE.with(|editor_handle| {
let mut guard = editor_handle.try_lock();
let Ok(Some(editor_handle)) = guard.as_deref_mut() else {
return log::error!("Failed to borrow handle");
};
// Call the closure with the editor and its handle
callback(editor_handle)
})
}
/// Provides access to the `Editor` and its `EditorHandle` by calling the given closure with them as arguments.
#[cfg(not(feature = "native"))]
pub(crate) fn editor_and_handle(callback: impl FnOnce(&mut Editor, &mut EditorHandle)) {
handle(|editor_handle| {
editor(|editor| {
let _ = handle(|editor_handle| {
let _ = editor(|editor| {
// Call the closure with the editor and its handle
callback(editor, editor_handle);
})
});
}
/// Provides access to the `EditorHandle` by calling the given closure with them as arguments.
pub(crate) fn handle(callback: impl FnOnce(&mut EditorHandle)) {
EDITOR_HANDLE.with(|editor_handle| {
let mut guard = editor_handle.try_lock();
let Ok(Some(editor_handle)) = guard.as_deref_mut() else {
log::error!("Failed to borrow editor handle");
return;
};
// Call the closure with the editor and its handle
callback(editor_handle);
});
});
}
@@ -1032,15 +1071,11 @@ async fn poll_node_graph_evaluation() {
crate::NODE_GRAPH_ERROR_DISPLAYED.store(false, Ordering::SeqCst);
}
// Send each `FrontendMessage` to the JavaScript frontend
for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) {
handle.send_frontend_message_to_js(response);
}
handle.process_messages(messages, editor);
// If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches
});
}
fn auto_save_all_documents() {
// Process no further messages after a crash to avoid spamming the console
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {