From d5140129cdb00d2fed2f57449d249b7f277e38c6 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 21 Aug 2024 17:43:11 -0700 Subject: [PATCH] Fix block numbering and switching with arrow keys (#258) --- frontend/app/appkey.ts | 88 +++++++++++------------------- frontend/app/block/block.less | 4 +- frontend/app/block/blockframe.tsx | 10 +--- frontend/layout/index.ts | 4 +- frontend/layout/lib/TileLayout.tsx | 12 ++-- frontend/layout/lib/layoutModel.ts | 43 ++++++++++++--- frontend/layout/lib/layoutNode.ts | 4 +- frontend/layout/lib/layoutTree.ts | 3 +- frontend/layout/lib/types.ts | 26 ++++++++- frontend/layout/lib/utils.ts | 18 +----- frontend/types/custom.d.ts | 7 --- 11 files changed, 108 insertions(+), 111 deletions(-) diff --git a/frontend/app/appkey.ts b/frontend/app/appkey.ts index ddf0a70da..7b84a592b 100644 --- a/frontend/app/appkey.ts +++ b/frontend/app/appkey.ts @@ -52,67 +52,36 @@ function switchBlockIdx(index: number) { const tabId = globalStore.get(atoms.activeTabId); const tabAtom = WOS.getWaveObjectAtom(WOS.makeORef("tab", tabId)); const layoutModel = getLayoutModelForTab(tabAtom); - if (layoutModel?.leafs == null) { + if (!layoutModel) { return; } + const leafsOrdered = globalStore.get(layoutModel.leafsOrdered); const newLeafIdx = index - 1; - if (newLeafIdx < 0 || newLeafIdx >= layoutModel.leafs.length) { + if (newLeafIdx < 0 || newLeafIdx >= leafsOrdered.length) { return; } - const leaf = layoutModel.leafs[newLeafIdx]; + const leaf = leafsOrdered[newLeafIdx]; if (leaf?.data?.blockId == null) { return; } setBlockFocus(leaf.data.blockId); } -function boundsMapMaxX(m: Map): number { - let max = 0; - for (let p of m.values()) { - if (p.x + p.width > max) { - max = p.x + p.width; - } - } - return max; -} - -function boundsMapMaxY(m: Map): number { - let max = 0; - for (let p of m.values()) { - if (p.y + p.height > max) { - max = p.y + p.height; - } - } - return max; -} - -function readBoundsFromTransform(fullTransform: React.CSSProperties): Bounds { - const transformProp = fullTransform.transform; - if (transformProp == null || fullTransform.width == null || fullTransform.height == null) { - return null; - } - const m = transformRegexp.exec(transformProp); - if (m == null) { - return null; - } +function getCenter(dimensions: Dimensions): Point { return { - x: parseFloat(m[1]), - y: parseFloat(m[2]), - width: parseFloatFromCSS(fullTransform.width), - height: parseFloatFromCSS(fullTransform.height), + x: dimensions.left + dimensions.width / 2, + y: dimensions.top + dimensions.height / 2, }; } -function parseFloatFromCSS(s: string | number): number { - if (typeof s == "number") { - return s; - } - return parseFloat(s); -} - -function findBlockAtPoint(m: Map, p: Point): string { - for (let [blockId, bounds] of m.entries()) { - if (p.x >= bounds.x && p.x <= bounds.x + bounds.width && p.y >= bounds.y && p.y <= bounds.y + bounds.height) { +function findBlockAtPoint(m: Map, p: Point): string { + for (const [blockId, dimension] of m.entries()) { + if ( + p.x >= dimension.left && + p.x <= dimension.left + dimension.width && + p.y >= dimension.top && + p.y <= dimension.top + dimension.height + ) { return blockId; } } @@ -128,9 +97,10 @@ function switchBlock(tabId: string, offsetX: number, offsetY: number) { const layoutModel = getLayoutModelForTab(tabAtom); const curBlockId = globalStore.get(atoms.waveWindow)?.activeblockid; const addlProps = globalStore.get(layoutModel.additionalProps); - const blockPositions: Map = new Map(); - for (const leaf of layoutModel.leafs) { - const pos = readBoundsFromTransform(addlProps[leaf.id]?.transform); + const blockPositions: Map = new Map(); + const leafsOrdered = globalStore.get(layoutModel.leafsOrdered); + for (const leaf of leafsOrdered) { + const pos = addlProps[leaf.id]?.rect; if (pos) { blockPositions.set(leaf.data.blockId, pos); } @@ -140,18 +110,22 @@ function switchBlock(tabId: string, offsetX: number, offsetY: number) { return; } blockPositions.delete(curBlockId); - const maxX = boundsMapMaxX(blockPositions); - const maxY = boundsMapMaxY(blockPositions); + const boundingRect = layoutModel.displayContainerRef?.current.getBoundingClientRect(); + if (!boundingRect) { + return; + } + const maxX = boundingRect.left + boundingRect.width; + const maxY = boundingRect.top + boundingRect.height; const moveAmount = 10; - let curX = curBlockPos.x + 1; - let curY = curBlockPos.y + 1; + const curPoint = getCenter(curBlockPos); while (true) { - curX += offsetX * moveAmount; - curY += offsetY * moveAmount; - if (curX < 0 || curX > maxX || curY < 0 || curY > maxY) { + console.log("nextPoint", curPoint, curBlockPos); + curPoint.x += offsetX * moveAmount; + curPoint.y += offsetY * moveAmount; + if (curPoint.x < 0 || curPoint.x > maxX || curPoint.y < 0 || curPoint.y > maxY) { return; } - const blockId = findBlockAtPoint(blockPositions, { x: curX, y: curY }); + const blockId = findBlockAtPoint(blockPositions, curPoint); if (blockId != null) { setBlockFocus(blockId); return; diff --git a/frontend/app/block/block.less b/frontend/app/block/block.less index a9a27356d..50a3ace11 100644 --- a/frontend/app/block/block.less +++ b/frontend/app/block/block.less @@ -255,16 +255,14 @@ } &.is-layoutmode .block-mask-inner { - margin-top: 35px; // TODO fix this magic background-color: rgba(0, 0, 0, 0.5); - height: calc(100% - 35px); + height: 100%; width: 100%; display: flex; align-items: center; justify-content: center; .bignum { - margin-top: -15%; font-size: 60px; font-weight: bold; opacity: 0.7; diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index dbb6035ed..f84323e64 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -202,13 +202,9 @@ function BlockNum({ blockId }: { blockId: string }) { const tabId = jotai.useAtomValue(atoms.activeTabId); const tabAtom = WOS.getWaveObjectAtom(WOS.makeORef("tab", tabId)); const layoutModel = useLayoutModel(tabAtom); - for (let idx = 0; idx < layoutModel.leafs.length; idx++) { - const leaf = layoutModel.leafs[idx]; - if (leaf?.data?.blockId == blockId) { - return String(idx + 1); - } - } - return null; + const leafsOrdered = jotai.useAtomValue(layoutModel.leafsOrdered); + const index = React.useMemo(() => leafsOrdered.findIndex((leaf) => leaf.data?.blockId == blockId), [leafsOrdered]); + return index !== -1 ? index + 1 : null; } const BlockMask = ({ blockId, preview, isFocused }: { blockId: string; preview: boolean; isFocused: boolean }) => { diff --git a/frontend/layout/index.ts b/frontend/layout/index.ts index 1d214cf01..e4f4ceb92 100644 --- a/frontend/layout/index.ts +++ b/frontend/layout/index.ts @@ -28,14 +28,16 @@ import type { LayoutTreeStateSetter, LayoutTreeSwapNodeAction, } from "./lib/types"; -import { LayoutTreeActionType } from "./lib/types"; +import { DropDirection, LayoutTreeActionType, NavigateDirection } from "./lib/types"; export { deleteLayoutModelForTab, + DropDirection, getLayoutModelForTab, getLayoutModelForTabById, LayoutModel, LayoutTreeActionType, + NavigateDirection, newLayoutNode, TileLayout, useLayoutModel, diff --git a/frontend/layout/lib/TileLayout.tsx b/frontend/layout/lib/TileLayout.tsx index fa27edbe6..80d674c9e 100644 --- a/frontend/layout/lib/TileLayout.tsx +++ b/frontend/layout/lib/TileLayout.tsx @@ -139,14 +139,14 @@ interface DisplayNodesWrapperProps { } const DisplayNodesWrapper = ({ layoutModel, contents }: DisplayNodesWrapperProps) => { - const generation = useAtomValue(layoutModel.generationAtom); + const leafs = useAtomValue(layoutModel.leafs); return useMemo( () => - layoutModel.leafs.map((leaf) => { + leafs.map((leaf) => { return ; }), - [generation] + [leafs] ); }; @@ -283,15 +283,15 @@ interface OverlayNodeWrapperProps { } const OverlayNodeWrapper = ({ layoutModel }: OverlayNodeWrapperProps) => { - const generation = useAtomValue(layoutModel.generationAtom); + const leafs = useAtomValue(layoutModel.leafs); const overlayTransform = useAtomValue(layoutModel.overlayTransform); const overlayNodes = useMemo( () => - layoutModel.leafs.map((leaf) => { + leafs.map((leaf) => { return ; }), - [generation] + [leafs] ); return ( diff --git a/frontend/layout/lib/layoutModel.ts b/frontend/layout/lib/layoutModel.ts index 38a3d44bd..82a2461da 100644 --- a/frontend/layout/lib/layoutModel.ts +++ b/frontend/layout/lib/layoutModel.ts @@ -19,6 +19,7 @@ import { } from "./layoutTree"; import { ContentRenderer, + FlexDirection, LayoutNode, LayoutNodeAdditionalProps, LayoutTreeAction, @@ -38,7 +39,7 @@ import { TileLayoutContents, WritableLayoutTreeStateAtom, } from "./types"; -import { FlexDirection, setTransform } from "./utils"; +import { setTransform } from "./utils"; interface ResizeContext { handleId: string; @@ -86,9 +87,14 @@ export class LayoutModel { gapSizePx: number; /** - * List of nodes that are leafs and should be rendered as a DisplayNode + * List of nodes that are leafs and should be rendered as a DisplayNode. */ - leafs: LayoutNode[]; + leafs: PrimitiveAtom; + /** + * List of nodes that are leafs, ordered sequentially by placement in the tree. + */ + leafsOrdered: Atom; + /** * Split atom containing the properties of all of the resize handles that should be placed in the layout. */ @@ -163,6 +169,7 @@ export class LayoutModel { * True if the whole TileLayout container is being resized. */ private isContainerResizing: PrimitiveAtom; + /** * An arbitrary generation value that is incremented every time the updateTree function runs. Helps indicate to subscribers that they should update their memoized values. */ @@ -188,8 +195,20 @@ export class LayoutModel { this.halfResizeHandleSizePx = this.gapSizePx > 5 ? this.gapSizePx : DefaultGapSizePx; this.resizeHandleSizePx = 2 * this.halfResizeHandleSizePx; - this.leafs = []; + this.leafs = atom([]); this.additionalProps = atom({}); + this.leafsOrdered = atom((get) => { + const leafs = get(this.leafs); + const additionalProps = get(this.additionalProps); + console.log("additionalProps", additionalProps); + const leafsOrdered = leafs.sort((a, b) => { + const treeKeyA = additionalProps[a.id].treeKey; + const treeKeyB = additionalProps[b.id].treeKey; + return treeKeyA.localeCompare(treeKeyB); + }); + console.log("leafsOrdered", leafsOrdered); + return leafsOrdered; + }); const resizeHandleListAtom = atom((get) => { const addlProps = get(this.additionalProps); @@ -368,7 +387,10 @@ export class LayoutModel { else walkNodes(this.treeState.rootNode, callback); this.setter(this.additionalProps, newAdditionalProps); - this.leafs = newLeafs.sort((a, b) => a.id.localeCompare(b.id)); + this.setter( + this.leafs, + newLeafs.sort((a, b) => a.id.localeCompare(b.id)) + ); this.setter(this.generationAtom, this.getter(this.generationAtom) + 1); } @@ -426,7 +448,9 @@ export class LayoutModel { const additionalProps: LayoutNodeAdditionalProps = additionalPropsMap.hasOwnProperty(node.id) ? additionalPropsMap[node.id] - : {}; + : { treeKey: "0" }; + + console.log("layoutNode addlProps", node, additionalProps); const nodeRect: Dimensions = node.id === this.treeState.rootNode.id ? getBoundingRect() : additionalProps.rect; const nodeIsRow = node.flexDirection === FlexDirection.Row; @@ -436,7 +460,7 @@ export class LayoutModel { let lastChildRect: Dimensions; const resizeHandles: ResizeHandleProps[] = []; - for (const child of node.children) { + node.children.forEach((child, i) => { const childSize = getNodeSize(child); const rect: Dimensions = { top: !nodeIsRow && lastChildRect ? lastChildRect.top + lastChildRect.height : nodeRect.top, @@ -448,6 +472,7 @@ export class LayoutModel { additionalPropsMap[child.id] = { rect, transform, + treeKey: additionalProps.treeKey + i, }; // We only want the resize handles in between nodes, this ensures we have n-1 handles. @@ -475,7 +500,7 @@ export class LayoutModel { }); } lastChildRect = rect; - } + }); additionalPropsMap[node.id] = { ...additionalProps, @@ -713,7 +738,7 @@ export class LayoutModel { } getNodeByBlockId(blockId: string) { - for (const leaf of this.leafs) { + for (const leaf of this.getter(this.leafs)) { if (leaf.data.blockId === blockId) { return leaf; } diff --git a/frontend/layout/lib/layoutNode.ts b/frontend/layout/lib/layoutNode.ts index c7d0af7de..a616475f3 100644 --- a/frontend/layout/lib/layoutNode.ts +++ b/frontend/layout/lib/layoutNode.ts @@ -1,8 +1,8 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { DefaultNodeSize, LayoutNode } from "./types"; -import { FlexDirection, reverseFlexDirection } from "./utils"; +import { DefaultNodeSize, FlexDirection, LayoutNode } from "./types"; +import { reverseFlexDirection } from "./utils"; /** * Creates a new node. diff --git a/frontend/layout/lib/layoutTree.ts b/frontend/layout/lib/layoutTree.ts index be115b4dd..5be6aa050 100644 --- a/frontend/layout/lib/layoutTree.ts +++ b/frontend/layout/lib/layoutTree.ts @@ -13,6 +13,8 @@ import { } from "./layoutNode"; import { DefaultNodeSize, + DropDirection, + FlexDirection, LayoutTreeActionType, LayoutTreeComputeMoveNodeAction, LayoutTreeDeleteNodeAction, @@ -25,7 +27,6 @@ import { LayoutTreeSwapNodeAction, MoveOperation, } from "./types"; -import { DropDirection, FlexDirection } from "./utils"; /** * Computes an operation for inserting a new node into the tree in the given direction relative to the specified node. diff --git a/frontend/layout/lib/types.ts b/frontend/layout/lib/types.ts index e18001e2f..461cc8f4a 100644 --- a/frontend/layout/lib/types.ts +++ b/frontend/layout/lib/types.ts @@ -3,7 +3,30 @@ import { WritableAtom } from "jotai"; import { CSSProperties } from "react"; -import { DropDirection, FlexDirection } from "./utils.js"; + +export enum NavigateDirection { + Top = 0, + Right = 1, + Bottom = 2, + Left = 3, +} + +export enum DropDirection { + Top = 0, + Right = 1, + Bottom = 2, + Left = 3, + OuterTop = 4, + OuterRight = 5, + OuterBottom = 6, + OuterLeft = 7, + Center = 8, +} + +export enum FlexDirection { + Row = "row", + Column = "column", +} /** * Represents an operation to insert a node into a tree. @@ -276,6 +299,7 @@ export interface ResizeHandleProps { } export interface LayoutNodeAdditionalProps { + treeKey: string; transform?: CSSProperties; rect?: Dimensions; pixelToSizeRatio?: number; diff --git a/frontend/layout/lib/utils.ts b/frontend/layout/lib/utils.ts index 067629f9f..78b5b7e1e 100644 --- a/frontend/layout/lib/utils.ts +++ b/frontend/layout/lib/utils.ts @@ -3,23 +3,7 @@ import { CSSProperties } from "react"; import { XYCoord } from "react-dnd"; - -export enum DropDirection { - Top = 0, - Right = 1, - Bottom = 2, - Left = 3, - OuterTop = 4, - OuterRight = 5, - OuterBottom = 6, - OuterLeft = 7, - Center = 8, -} - -export enum FlexDirection { - Row = "row", - Column = "column", -} +import { DropDirection, FlexDirection } from "./types"; export function reverseFlexDirection(flexDirection: FlexDirection): FlexDirection { return flexDirection === FlexDirection.Row ? FlexDirection.Column : FlexDirection.Row; diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 29609c47f..2299c10b8 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -44,13 +44,6 @@ declare global { blockId: string; }; - type Bounds = { - x: number; - y: number; - width: number; - height: number; - }; - type ElectronApi = { getAuthKey(): string; getIsDev(): boolean;