Diff simple layout changes to avoid excessive DOM replacements (#910)

* Add UI diffs to rust

* Clean up some js

* Fix lints

* Fix test

* Remove one unnecessary keyword

* Rename to widget path

* Rename new_val to new_value

* Rename newVal to layoutGroup in createLayoutGroup

* Extract get_widget_path to a function

* Base skipping on the layout rather than the target

* Rename to ResendActiveWidget

* Switch info to trace

* Add a link to the documentation about Object.assign

* knitpick js changes

* Add more comments to diff functions

Co-authored-by: mfish33 <maxmfishernj@gmail.com>
This commit is contained in:
0HyperCube
2022-12-25 18:56:35 +00:00
committed by Keavon Chambers
parent 01931939ff
commit 694890642e
12 changed files with 464 additions and 264 deletions

View File

@@ -231,6 +231,7 @@ import { textInputCleanup } from "@/utility-functions/keyboard-entry";
import { rasterizeSVGCanvas } from "@/utility-functions/rasterization";
import {
defaultWidgetLayout,
patchWidgetLayout,
type DisplayEditableTextbox,
type MouseCursorIcon,
type UpdateDocumentBarLayout,
@@ -505,19 +506,19 @@ export default defineComponent({
},
// Update layouts
updateDocumentModeLayout(updateDocumentModeLayout: UpdateDocumentModeLayout) {
this.documentModeLayout = updateDocumentModeLayout;
patchWidgetLayout(this.documentModeLayout, updateDocumentModeLayout);
},
updateToolOptionsLayout(updateToolOptionsLayout: UpdateToolOptionsLayout) {
this.toolOptionsLayout = updateToolOptionsLayout;
patchWidgetLayout(this.toolOptionsLayout, updateToolOptionsLayout);
},
updateDocumentBarLayout(updateDocumentBarLayout: UpdateDocumentBarLayout) {
this.documentBarLayout = updateDocumentBarLayout;
patchWidgetLayout(this.documentBarLayout, updateDocumentBarLayout);
},
updateToolShelfLayout(updateToolShelfLayout: UpdateToolShelfLayout) {
this.toolShelfLayout = updateToolShelfLayout;
patchWidgetLayout(this.toolShelfLayout, updateToolShelfLayout);
},
updateWorkingColorsLayout(updateWorkingColorsLayout: UpdateWorkingColorsLayout) {
this.workingColorsLayout = updateWorkingColorsLayout;
patchWidgetLayout(this.workingColorsLayout, updateWorkingColorsLayout);
},
// Resize elements to render the new viewport size
viewportResize() {

View File

@@ -277,6 +277,7 @@ import {
type LayerTypeData,
type LayerPanelEntry,
defaultWidgetLayout,
patchWidgetLayout,
UpdateDocumentLayerDetails,
UpdateDocumentLayerTreeStructureJs,
UpdateLayerTreeOptionsLayout,
@@ -523,7 +524,7 @@ export default defineComponent({
});
this.editor.subscriptions.subscribeJsMessage(UpdateLayerTreeOptionsLayout, (updateLayerTreeOptionsLayout) => {
this.layerTreeOptionsLayout = updateLayerTreeOptionsLayout;
patchWidgetLayout(this.layerTreeOptionsLayout, updateLayerTreeOptionsLayout);
});
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerDetails, (updateDocumentLayerDetails) => {

View File

@@ -32,7 +32,7 @@
<script lang="ts">
import { defineComponent } from "vue";
import { defaultWidgetLayout, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout } from "@/wasm-communication/messages";
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout } from "@/wasm-communication/messages";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
@@ -48,11 +48,11 @@ export default defineComponent({
},
mounted() {
this.editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelOptionsLayout, (updatePropertyPanelOptionsLayout) => {
this.propertiesOptionsLayout = updatePropertyPanelOptionsLayout;
patchWidgetLayout(this.propertiesOptionsLayout, updatePropertyPanelOptionsLayout);
});
this.editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {
this.propertiesSectionsLayout = updatePropertyPanelSectionsLayout;
patchWidgetLayout(this.propertiesSectionsLayout, updatePropertyPanelSectionsLayout);
});
},
components: {

View File

@@ -2,7 +2,7 @@ import { reactive, readonly } from "vue";
import { type IconName } from "@/utility-functions/icons";
import { type Editor } from "@/wasm-communication/editor";
import { type TextButtonWidget, type WidgetLayout, defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogDetails } from "@/wasm-communication/messages";
import { type TextButtonWidget, type WidgetLayout, defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogDetails, patchWidgetLayout } from "@/wasm-communication/messages";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createDialogState(editor: Editor) {
@@ -37,7 +37,7 @@ export function createDialogState(editor: Editor) {
state.icon = displayDialog.icon;
});
editor.subscriptions.subscribeJsMessage(UpdateDialogDetails, (updateDialogDetails) => {
state.widgets = updateDialogDetails;
patchWidgetLayout(state.widgets, updateDialogDetails);
state.jsCallbackBasedButtons = undefined;
});
editor.subscriptions.subscribeJsMessage(DisplayDialogDismiss, dismissDialog);

View File

@@ -1,7 +1,16 @@
import { reactive, readonly } from "vue";
import { type Editor } from "@/wasm-communication/editor";
import { type FrontendNode, type FrontendNodeLink, type FrontendNodeType, UpdateNodeGraph, UpdateNodeTypes, UpdateNodeGraphBarLayout, defaultWidgetLayout } from "@/wasm-communication/messages";
import {
type FrontendNode,
type FrontendNodeLink,
type FrontendNodeType,
UpdateNodeGraph,
UpdateNodeTypes,
UpdateNodeGraphBarLayout,
defaultWidgetLayout,
patchWidgetLayout,
} from "@/wasm-communication/messages";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createNodeGraphState(editor: Editor) {
@@ -21,7 +30,7 @@ export function createNodeGraphState(editor: Editor) {
state.nodeTypes = updateNodeTypes.nodeTypes;
});
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphBarLayout, (updateNodeGraphBarLayout) => {
state.nodeGraphBarLayout = updateNodeGraphBarLayout;
patchWidgetLayout(state.nodeGraphBarLayout, updateNodeGraphBarLayout);
});
return {

View File

@@ -1171,18 +1171,20 @@ export class Widget {
widgetId!: bigint;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function hoistWidgetHolder(widgetHolder: any): Widget {
const kind = Object.keys(widgetHolder.widget)[0];
const props = widgetHolder.widget[kind];
props.kind = kind;
const { widgetId } = widgetHolder;
return plainToClass(Widget, { props, widgetId });
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function hoistWidgetHolders(widgetHolders: any[]): Widget[] {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return widgetHolders.map((widgetHolder: any) => {
const kind = Object.keys(widgetHolder.widget)[0];
const props = widgetHolder.widget[kind];
props.kind = kind;
const { widgetId } = widgetHolder;
return plainToClass(Widget, { props, widgetId });
});
return widgetHolders.map(hoistWidgetHolder);
}
// WIDGET LAYOUT
@@ -1192,6 +1194,18 @@ export type WidgetLayout = {
layout: LayoutGroup[];
};
export class WidgetDiffUpdate extends JsMessage {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetDiff(value))
diff!: WidgetDiff[];
}
type UIItem = LayoutGroup[] | LayoutGroup | Widget | MenuBarEntry[] | MenuBarEntry;
type WidgetDiff = { widgetPath: number[]; newValue: UIItem };
export function defaultWidgetLayout(): WidgetLayout {
return {
layoutTarget: undefined,
@@ -1199,6 +1213,42 @@ export function defaultWidgetLayout(): WidgetLayout {
};
}
// Updates a widget layout based on a list of updates, returning the new layout
export function patchWidgetLayout(layout: WidgetLayout, updates: WidgetDiffUpdate): void {
layout.layoutTarget = updates.layoutTarget;
updates.diff.forEach((update) => {
// Find the object where the diff applies to
const diffObject = update.widgetPath.reduce((targetLayout, index) => {
if ("columnWidgets" in targetLayout) return targetLayout.columnWidgets[index];
if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
if ("layout" in targetLayout) return targetLayout.layout[index];
if (targetLayout instanceof Widget) {
// eslint-disable-next-line no-console
console.error("Tried to index widget");
return targetLayout;
}
// This is a path traversal so we can assume from the backend that it exists
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if ("action" in targetLayout) return targetLayout.children![index];
return targetLayout[index];
}, layout.layout as UIItem);
// If this is a list with a length, then set the length to 0 to clear the list
if ("length" in diffObject) {
diffObject.length = 0;
}
// Remove all of the keys from the old object
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Object.keys(diffObject).forEach((key) => delete (diffObject as any)[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);
});
}
export type LayoutGroup = WidgetRow | WidgetColumn | WidgetSection;
export type WidgetColumn = { columnWidgets: Widget[] };
@@ -1218,116 +1268,66 @@ export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSect
// Unpacking rust types to more usable type in the frontend
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createWidgetLayout(widgetLayout: any[]): LayoutGroup[] {
return widgetLayout.map((layoutType): LayoutGroup => {
if (layoutType.column) {
const columnWidgets = hoistWidgetHolders(layoutType.column.columnWidgets);
const result: WidgetColumn = { columnWidgets };
return result;
function createWidgetDiff(diffs: any[]): WidgetDiff[] {
return diffs.map((diff) => {
const { widgetPath, newValue } = diff;
if (newValue.subLayout) {
return { widgetPath, newValue: newValue.subLayout.map(createLayoutGroup) };
}
if (layoutType.row) {
const rowWidgets = hoistWidgetHolders(layoutType.row.rowWidgets);
const result: WidgetRow = { rowWidgets };
return result;
if (newValue.layoutGroup) {
return { widgetPath, newValue: createLayoutGroup(newValue.layoutGroup) };
}
if (layoutType.section) {
const { name } = layoutType.section;
const layout = createWidgetLayout(layoutType.section.layout);
const result: WidgetSection = { name, layout };
return result;
if (newValue.widget) {
return { widgetPath, newValue: hoistWidgetHolder(newValue.widget) };
}
throw new Error("Layout row type does not exist");
// This code should be unreachable
throw new Error("DiffUpdate invalid");
});
}
// Unpacking a layout group
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createLayoutGroup(layoutGroup: any): LayoutGroup {
if (layoutGroup.column) {
const columnWidgets = hoistWidgetHolders(layoutGroup.column.columnWidgets);
const result: WidgetColumn = { columnWidgets };
return result;
}
if (layoutGroup.row) {
const result: WidgetRow = { rowWidgets: hoistWidgetHolders(layoutGroup.row.rowWidgets) };
return result;
}
if (layoutGroup.section) {
const result: WidgetSection = { name: layoutGroup.section.name, layout: layoutGroup.section.layout.map(createLayoutGroup) };
return result;
}
throw new Error("Layout row type does not exist");
}
// WIDGET LAYOUTS
export class UpdateDialogDetails extends WidgetDiffUpdate {}
export class UpdateDialogDetails extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
export class UpdateDocumentModeLayout extends WidgetDiffUpdate {}
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdateToolOptionsLayout extends WidgetDiffUpdate {}
export class UpdateDocumentModeLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
export class UpdateDocumentBarLayout extends WidgetDiffUpdate {}
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdateToolShelfLayout extends WidgetDiffUpdate {}
export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
export class UpdateWorkingColorsLayout extends WidgetDiffUpdate {}
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdatePropertyPanelOptionsLayout extends WidgetDiffUpdate {}
export class UpdateDocumentBarLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {}
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdateLayerTreeOptionsLayout extends WidgetDiffUpdate {}
export class UpdateToolShelfLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdateWorkingColorsLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdatePropertyPanelOptionsLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdatePropertyPanelSectionsLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdateLayerTreeOptionsLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdateNodeGraphBarLayout extends WidgetDiffUpdate {}
export class UpdateMenuBarLayout extends JsMessage {
layoutTarget!: unknown;
@@ -1338,15 +1338,6 @@ export class UpdateMenuBarLayout extends JsMessage {
layout!: MenuBarEntry[];
}
export class UpdateNodeGraphBarLayout extends JsMessage {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createMenuLayout(menuBarEntry: any[]): MenuBarEntry[] {
return menuBarEntry.map((entry) => ({

View File

@@ -0,0 +1,5 @@
[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+simd128,+atomics,+bulk-memory,+mutable-globals"]
[unstable]
build-std = ["panic_abort", "std"]