Fix block numbering and switching with arrow keys (#258)

This commit is contained in:
Evan Simkowitz 2024-08-21 17:43:11 -07:00 committed by GitHub
parent dedfc31344
commit d5140129cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 108 additions and 111 deletions

View File

@ -52,67 +52,36 @@ function switchBlockIdx(index: number) {
const tabId = globalStore.get(atoms.activeTabId); const tabId = globalStore.get(atoms.activeTabId);
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId)); const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
const layoutModel = getLayoutModelForTab(tabAtom); const layoutModel = getLayoutModelForTab(tabAtom);
if (layoutModel?.leafs == null) { if (!layoutModel) {
return; return;
} }
const leafsOrdered = globalStore.get(layoutModel.leafsOrdered);
const newLeafIdx = index - 1; const newLeafIdx = index - 1;
if (newLeafIdx < 0 || newLeafIdx >= layoutModel.leafs.length) { if (newLeafIdx < 0 || newLeafIdx >= leafsOrdered.length) {
return; return;
} }
const leaf = layoutModel.leafs[newLeafIdx]; const leaf = leafsOrdered[newLeafIdx];
if (leaf?.data?.blockId == null) { if (leaf?.data?.blockId == null) {
return; return;
} }
setBlockFocus(leaf.data.blockId); setBlockFocus(leaf.data.blockId);
} }
function boundsMapMaxX(m: Map<string, Bounds>): number { function getCenter(dimensions: Dimensions): Point {
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<string, Bounds>): 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;
}
return { return {
x: parseFloat(m[1]), x: dimensions.left + dimensions.width / 2,
y: parseFloat(m[2]), y: dimensions.top + dimensions.height / 2,
width: parseFloatFromCSS(fullTransform.width),
height: parseFloatFromCSS(fullTransform.height),
}; };
} }
function parseFloatFromCSS(s: string | number): number { function findBlockAtPoint(m: Map<string, Dimensions>, p: Point): string {
if (typeof s == "number") { for (const [blockId, dimension] of m.entries()) {
return s; if (
} p.x >= dimension.left &&
return parseFloat(s); p.x <= dimension.left + dimension.width &&
} p.y >= dimension.top &&
p.y <= dimension.top + dimension.height
function findBlockAtPoint(m: Map<string, Bounds>, 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) {
return blockId; return blockId;
} }
} }
@ -128,9 +97,10 @@ function switchBlock(tabId: string, offsetX: number, offsetY: number) {
const layoutModel = getLayoutModelForTab(tabAtom); const layoutModel = getLayoutModelForTab(tabAtom);
const curBlockId = globalStore.get(atoms.waveWindow)?.activeblockid; const curBlockId = globalStore.get(atoms.waveWindow)?.activeblockid;
const addlProps = globalStore.get(layoutModel.additionalProps); const addlProps = globalStore.get(layoutModel.additionalProps);
const blockPositions: Map<string, Bounds> = new Map(); const blockPositions: Map<string, Dimensions> = new Map();
for (const leaf of layoutModel.leafs) { const leafsOrdered = globalStore.get(layoutModel.leafsOrdered);
const pos = readBoundsFromTransform(addlProps[leaf.id]?.transform); for (const leaf of leafsOrdered) {
const pos = addlProps[leaf.id]?.rect;
if (pos) { if (pos) {
blockPositions.set(leaf.data.blockId, pos); blockPositions.set(leaf.data.blockId, pos);
} }
@ -140,18 +110,22 @@ function switchBlock(tabId: string, offsetX: number, offsetY: number) {
return; return;
} }
blockPositions.delete(curBlockId); blockPositions.delete(curBlockId);
const maxX = boundsMapMaxX(blockPositions); const boundingRect = layoutModel.displayContainerRef?.current.getBoundingClientRect();
const maxY = boundsMapMaxY(blockPositions); if (!boundingRect) {
return;
}
const maxX = boundingRect.left + boundingRect.width;
const maxY = boundingRect.top + boundingRect.height;
const moveAmount = 10; const moveAmount = 10;
let curX = curBlockPos.x + 1; const curPoint = getCenter(curBlockPos);
let curY = curBlockPos.y + 1;
while (true) { while (true) {
curX += offsetX * moveAmount; console.log("nextPoint", curPoint, curBlockPos);
curY += offsetY * moveAmount; curPoint.x += offsetX * moveAmount;
if (curX < 0 || curX > maxX || curY < 0 || curY > maxY) { curPoint.y += offsetY * moveAmount;
if (curPoint.x < 0 || curPoint.x > maxX || curPoint.y < 0 || curPoint.y > maxY) {
return; return;
} }
const blockId = findBlockAtPoint(blockPositions, { x: curX, y: curY }); const blockId = findBlockAtPoint(blockPositions, curPoint);
if (blockId != null) { if (blockId != null) {
setBlockFocus(blockId); setBlockFocus(blockId);
return; return;

View File

@ -255,16 +255,14 @@
} }
&.is-layoutmode .block-mask-inner { &.is-layoutmode .block-mask-inner {
margin-top: 35px; // TODO fix this magic
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
height: calc(100% - 35px); height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.bignum { .bignum {
margin-top: -15%;
font-size: 60px; font-size: 60px;
font-weight: bold; font-weight: bold;
opacity: 0.7; opacity: 0.7;

View File

@ -202,13 +202,9 @@ function BlockNum({ blockId }: { blockId: string }) {
const tabId = jotai.useAtomValue(atoms.activeTabId); const tabId = jotai.useAtomValue(atoms.activeTabId);
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId)); const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
const layoutModel = useLayoutModel(tabAtom); const layoutModel = useLayoutModel(tabAtom);
for (let idx = 0; idx < layoutModel.leafs.length; idx++) { const leafsOrdered = jotai.useAtomValue(layoutModel.leafsOrdered);
const leaf = layoutModel.leafs[idx]; const index = React.useMemo(() => leafsOrdered.findIndex((leaf) => leaf.data?.blockId == blockId), [leafsOrdered]);
if (leaf?.data?.blockId == blockId) { return index !== -1 ? index + 1 : null;
return String(idx + 1);
}
}
return null;
} }
const BlockMask = ({ blockId, preview, isFocused }: { blockId: string; preview: boolean; isFocused: boolean }) => { const BlockMask = ({ blockId, preview, isFocused }: { blockId: string; preview: boolean; isFocused: boolean }) => {

View File

@ -28,14 +28,16 @@ import type {
LayoutTreeStateSetter, LayoutTreeStateSetter,
LayoutTreeSwapNodeAction, LayoutTreeSwapNodeAction,
} from "./lib/types"; } from "./lib/types";
import { LayoutTreeActionType } from "./lib/types"; import { DropDirection, LayoutTreeActionType, NavigateDirection } from "./lib/types";
export { export {
deleteLayoutModelForTab, deleteLayoutModelForTab,
DropDirection,
getLayoutModelForTab, getLayoutModelForTab,
getLayoutModelForTabById, getLayoutModelForTabById,
LayoutModel, LayoutModel,
LayoutTreeActionType, LayoutTreeActionType,
NavigateDirection,
newLayoutNode, newLayoutNode,
TileLayout, TileLayout,
useLayoutModel, useLayoutModel,

View File

@ -139,14 +139,14 @@ interface DisplayNodesWrapperProps {
} }
const DisplayNodesWrapper = ({ layoutModel, contents }: DisplayNodesWrapperProps) => { const DisplayNodesWrapper = ({ layoutModel, contents }: DisplayNodesWrapperProps) => {
const generation = useAtomValue(layoutModel.generationAtom); const leafs = useAtomValue(layoutModel.leafs);
return useMemo( return useMemo(
() => () =>
layoutModel.leafs.map((leaf) => { leafs.map((leaf) => {
return <DisplayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} contents={contents} />; return <DisplayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} contents={contents} />;
}), }),
[generation] [leafs]
); );
}; };
@ -283,15 +283,15 @@ interface OverlayNodeWrapperProps {
} }
const OverlayNodeWrapper = ({ layoutModel }: OverlayNodeWrapperProps) => { const OverlayNodeWrapper = ({ layoutModel }: OverlayNodeWrapperProps) => {
const generation = useAtomValue(layoutModel.generationAtom); const leafs = useAtomValue(layoutModel.leafs);
const overlayTransform = useAtomValue(layoutModel.overlayTransform); const overlayTransform = useAtomValue(layoutModel.overlayTransform);
const overlayNodes = useMemo( const overlayNodes = useMemo(
() => () =>
layoutModel.leafs.map((leaf) => { leafs.map((leaf) => {
return <OverlayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} />; return <OverlayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} />;
}), }),
[generation] [leafs]
); );
return ( return (

View File

@ -19,6 +19,7 @@ import {
} from "./layoutTree"; } from "./layoutTree";
import { import {
ContentRenderer, ContentRenderer,
FlexDirection,
LayoutNode, LayoutNode,
LayoutNodeAdditionalProps, LayoutNodeAdditionalProps,
LayoutTreeAction, LayoutTreeAction,
@ -38,7 +39,7 @@ import {
TileLayoutContents, TileLayoutContents,
WritableLayoutTreeStateAtom, WritableLayoutTreeStateAtom,
} from "./types"; } from "./types";
import { FlexDirection, setTransform } from "./utils"; import { setTransform } from "./utils";
interface ResizeContext { interface ResizeContext {
handleId: string; handleId: string;
@ -86,9 +87,14 @@ export class LayoutModel {
gapSizePx: number; 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<LayoutNode[]>;
/**
* List of nodes that are leafs, ordered sequentially by placement in the tree.
*/
leafsOrdered: Atom<LayoutNode[]>;
/** /**
* Split atom containing the properties of all of the resize handles that should be placed in the layout. * 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. * True if the whole TileLayout container is being resized.
*/ */
private isContainerResizing: PrimitiveAtom<boolean>; private isContainerResizing: PrimitiveAtom<boolean>;
/** /**
* An arbitrary generation value that is incremented every time the updateTree function runs. Helps indicate to subscribers that they should update their memoized values. * 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.halfResizeHandleSizePx = this.gapSizePx > 5 ? this.gapSizePx : DefaultGapSizePx;
this.resizeHandleSizePx = 2 * this.halfResizeHandleSizePx; this.resizeHandleSizePx = 2 * this.halfResizeHandleSizePx;
this.leafs = []; this.leafs = atom([]);
this.additionalProps = 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 resizeHandleListAtom = atom((get) => {
const addlProps = get(this.additionalProps); const addlProps = get(this.additionalProps);
@ -368,7 +387,10 @@ export class LayoutModel {
else walkNodes(this.treeState.rootNode, callback); else walkNodes(this.treeState.rootNode, callback);
this.setter(this.additionalProps, newAdditionalProps); 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); this.setter(this.generationAtom, this.getter(this.generationAtom) + 1);
} }
@ -426,7 +448,9 @@ export class LayoutModel {
const additionalProps: LayoutNodeAdditionalProps = additionalPropsMap.hasOwnProperty(node.id) const additionalProps: LayoutNodeAdditionalProps = additionalPropsMap.hasOwnProperty(node.id)
? additionalPropsMap[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 nodeRect: Dimensions = node.id === this.treeState.rootNode.id ? getBoundingRect() : additionalProps.rect;
const nodeIsRow = node.flexDirection === FlexDirection.Row; const nodeIsRow = node.flexDirection === FlexDirection.Row;
@ -436,7 +460,7 @@ export class LayoutModel {
let lastChildRect: Dimensions; let lastChildRect: Dimensions;
const resizeHandles: ResizeHandleProps[] = []; const resizeHandles: ResizeHandleProps[] = [];
for (const child of node.children) { node.children.forEach((child, i) => {
const childSize = getNodeSize(child); const childSize = getNodeSize(child);
const rect: Dimensions = { const rect: Dimensions = {
top: !nodeIsRow && lastChildRect ? lastChildRect.top + lastChildRect.height : nodeRect.top, top: !nodeIsRow && lastChildRect ? lastChildRect.top + lastChildRect.height : nodeRect.top,
@ -448,6 +472,7 @@ export class LayoutModel {
additionalPropsMap[child.id] = { additionalPropsMap[child.id] = {
rect, rect,
transform, transform,
treeKey: additionalProps.treeKey + i,
}; };
// We only want the resize handles in between nodes, this ensures we have n-1 handles. // 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; lastChildRect = rect;
} });
additionalPropsMap[node.id] = { additionalPropsMap[node.id] = {
...additionalProps, ...additionalProps,
@ -713,7 +738,7 @@ export class LayoutModel {
} }
getNodeByBlockId(blockId: string) { getNodeByBlockId(blockId: string) {
for (const leaf of this.leafs) { for (const leaf of this.getter(this.leafs)) {
if (leaf.data.blockId === blockId) { if (leaf.data.blockId === blockId) {
return leaf; return leaf;
} }

View File

@ -1,8 +1,8 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { DefaultNodeSize, LayoutNode } from "./types"; import { DefaultNodeSize, FlexDirection, LayoutNode } from "./types";
import { FlexDirection, reverseFlexDirection } from "./utils"; import { reverseFlexDirection } from "./utils";
/** /**
* Creates a new node. * Creates a new node.

View File

@ -13,6 +13,8 @@ import {
} from "./layoutNode"; } from "./layoutNode";
import { import {
DefaultNodeSize, DefaultNodeSize,
DropDirection,
FlexDirection,
LayoutTreeActionType, LayoutTreeActionType,
LayoutTreeComputeMoveNodeAction, LayoutTreeComputeMoveNodeAction,
LayoutTreeDeleteNodeAction, LayoutTreeDeleteNodeAction,
@ -25,7 +27,6 @@ import {
LayoutTreeSwapNodeAction, LayoutTreeSwapNodeAction,
MoveOperation, MoveOperation,
} from "./types"; } 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. * Computes an operation for inserting a new node into the tree in the given direction relative to the specified node.

View File

@ -3,7 +3,30 @@
import { WritableAtom } from "jotai"; import { WritableAtom } from "jotai";
import { CSSProperties } from "react"; 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. * Represents an operation to insert a node into a tree.
@ -276,6 +299,7 @@ export interface ResizeHandleProps {
} }
export interface LayoutNodeAdditionalProps { export interface LayoutNodeAdditionalProps {
treeKey: string;
transform?: CSSProperties; transform?: CSSProperties;
rect?: Dimensions; rect?: Dimensions;
pixelToSizeRatio?: number; pixelToSizeRatio?: number;

View File

@ -3,23 +3,7 @@
import { CSSProperties } from "react"; import { CSSProperties } from "react";
import { XYCoord } from "react-dnd"; import { XYCoord } from "react-dnd";
import { DropDirection, FlexDirection } from "./types";
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",
}
export function reverseFlexDirection(flexDirection: FlexDirection): FlexDirection { export function reverseFlexDirection(flexDirection: FlexDirection): FlexDirection {
return flexDirection === FlexDirection.Row ? FlexDirection.Column : FlexDirection.Row; return flexDirection === FlexDirection.Row ? FlexDirection.Column : FlexDirection.Row;

View File

@ -44,13 +44,6 @@ declare global {
blockId: string; blockId: string;
}; };
type Bounds = {
x: number;
y: number;
width: number;
height: number;
};
type ElectronApi = { type ElectronApi = {
getAuthKey(): string; getAuthKey(): string;
getIsDev(): boolean; getIsDev(): boolean;