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 tabAtom = WOS.getWaveObjectAtom<Tab>(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<string, Bounds>): 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<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;
}
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<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) {
function findBlockAtPoint(m: Map<string, Dimensions>, 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<string, Bounds> = new Map();
for (const leaf of layoutModel.leafs) {
const pos = readBoundsFromTransform(addlProps[leaf.id]?.transform);
const blockPositions: Map<string, Dimensions> = 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;

View File

@ -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;

View File

@ -202,13 +202,9 @@ function BlockNum({ blockId }: { blockId: string }) {
const tabId = jotai.useAtomValue(atoms.activeTabId);
const tabAtom = WOS.getWaveObjectAtom<Tab>(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 }) => {

View File

@ -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,

View File

@ -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 <DisplayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} contents={contents} />;
}),
[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 <OverlayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} />;
}),
[generation]
[leafs]
);
return (

View File

@ -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<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.
*/
@ -163,6 +169,7 @@ export class LayoutModel {
* True if the whole TileLayout container is being resized.
*/
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.
*/
@ -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;
}

View File

@ -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.

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

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