mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
Unified node model to pass data from layout to blocks (#259)
This adds a new NodeModel, which can be passed from the TileLayout to contained blocks. It contains all the layout data that the block should care about, including focus status, whether a drag operation is underway, whether the node is magnified, etc. This also adds a focus stack for the layout, which will let the focus switch to the last-focused node when the currently-focused one is closed. This also addresses a regression in the resize handles that caused them to be offset from the cursor when dragged. --------- Co-authored-by: sawka <mike.sawka@gmail.com>
This commit is contained in:
+27
-125
@@ -1,14 +1,19 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { atoms, createBlock, getViewModel, globalStore, setBlockFocus, WOS } from "@/app/store/global";
|
import { atoms, createBlock, globalStore, WOS } from "@/app/store/global";
|
||||||
import { deleteLayoutModelForTab, getLayoutModelForTab } from "@/layout/index";
|
import {
|
||||||
|
deleteLayoutModelForTab,
|
||||||
|
getLayoutModelForActiveTab,
|
||||||
|
getLayoutModelForTab,
|
||||||
|
getLayoutModelForTabById,
|
||||||
|
NavigateDirection,
|
||||||
|
} from "@/layout/index";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import * as keyutil from "@/util/keyutil";
|
import * as keyutil from "@/util/keyutil";
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
|
|
||||||
const simpleControlShiftAtom = jotai.atom(false);
|
const simpleControlShiftAtom = jotai.atom(false);
|
||||||
const transformRegexp = /translate3d\(\s*([0-9.]+)px\s*,\s*([0-9.]+)px,\s*0\)/;
|
|
||||||
|
|
||||||
function setControlShift() {
|
function setControlShift() {
|
||||||
globalStore.set(simpleControlShiftAtom, true);
|
globalStore.set(simpleControlShiftAtom, true);
|
||||||
@@ -38,99 +43,21 @@ function genericClose(tabId: string) {
|
|||||||
deleteLayoutModelForTab(tabId);
|
deleteLayoutModelForTab(tabId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// close block
|
|
||||||
const activeBlockId = globalStore.get(atoms.waveWindow)?.activeblockid;
|
|
||||||
if (activeBlockId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const layoutModel = getLayoutModelForTab(tabAtom);
|
const layoutModel = getLayoutModelForTab(tabAtom);
|
||||||
const curBlockLeafId = layoutModel.getNodeByBlockId(activeBlockId)?.id;
|
layoutModel.closeFocusedNode();
|
||||||
layoutModel.closeNodeById(curBlockLeafId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchBlockIdx(index: number) {
|
function switchBlockByBlockNum(index: number) {
|
||||||
const tabId = globalStore.get(atoms.activeTabId);
|
const layoutModel = getLayoutModelForActiveTab();
|
||||||
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
|
|
||||||
const layoutModel = getLayoutModelForTab(tabAtom);
|
|
||||||
if (!layoutModel) {
|
if (!layoutModel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const leafsOrdered = globalStore.get(layoutModel.leafsOrdered);
|
layoutModel.switchNodeFocusByBlockNum(index);
|
||||||
const newLeafIdx = index - 1;
|
|
||||||
if (newLeafIdx < 0 || newLeafIdx >= leafsOrdered.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const leaf = leafsOrdered[newLeafIdx];
|
|
||||||
if (leaf?.data?.blockId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setBlockFocus(leaf.data.blockId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCenter(dimensions: Dimensions): Point {
|
function switchBlockInDirection(tabId: string, direction: NavigateDirection) {
|
||||||
return {
|
const layoutModel = getLayoutModelForTabById(tabId);
|
||||||
x: dimensions.left + dimensions.width / 2,
|
layoutModel.switchNodeFocusInDirection(direction);
|
||||||
y: dimensions.top + dimensions.height / 2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchBlock(tabId: string, offsetX: number, offsetY: number) {
|
|
||||||
console.log("switch block", offsetX, offsetY);
|
|
||||||
if (offsetY == 0 && offsetX == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
|
|
||||||
const layoutModel = getLayoutModelForTab(tabAtom);
|
|
||||||
const curBlockId = globalStore.get(atoms.waveWindow)?.activeblockid;
|
|
||||||
const addlProps = globalStore.get(layoutModel.additionalProps);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const curBlockPos = blockPositions.get(curBlockId);
|
|
||||||
if (!curBlockPos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
blockPositions.delete(curBlockId);
|
|
||||||
const boundingRect = layoutModel.displayContainerRef?.current.getBoundingClientRect();
|
|
||||||
if (!boundingRect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const maxX = boundingRect.left + boundingRect.width;
|
|
||||||
const maxY = boundingRect.top + boundingRect.height;
|
|
||||||
const moveAmount = 10;
|
|
||||||
const curPoint = getCenter(curBlockPos);
|
|
||||||
while (true) {
|
|
||||||
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, curPoint);
|
|
||||||
if (blockId != null) {
|
|
||||||
setBlockFocus(blockId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchTabAbs(index: number) {
|
function switchTabAbs(index: number) {
|
||||||
@@ -158,7 +85,6 @@ function switchTab(offset: number) {
|
|||||||
}
|
}
|
||||||
const newTabIdx = (tabIdx + offset + ws.tabids.length) % ws.tabids.length;
|
const newTabIdx = (tabIdx + offset + ws.tabids.length) % ws.tabids.length;
|
||||||
const newActiveTabId = ws.tabids[newTabIdx];
|
const newActiveTabId = ws.tabids[newTabIdx];
|
||||||
console.log("switching tabs", tabIdx, newTabIdx, activeTabId, newActiveTabId, ws.tabids);
|
|
||||||
services.ObjectService.SetActiveTab(newActiveTabId);
|
services.ObjectService.SetActiveTab(newActiveTabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,17 +100,17 @@ function appHandleKeyUp(event: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCmdT() {
|
async function handleCmdN() {
|
||||||
const termBlockDef: BlockDef = {
|
const termBlockDef: BlockDef = {
|
||||||
meta: {
|
meta: {
|
||||||
view: "term",
|
view: "term",
|
||||||
controller: "shell",
|
controller: "shell",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const tabId = globalStore.get(atoms.activeTabId);
|
const layoutModel = getLayoutModelForActiveTab();
|
||||||
const win = globalStore.get(atoms.waveWindow);
|
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
||||||
if (win?.activeblockid != null) {
|
if (focusedNode != null) {
|
||||||
const blockAtom = WOS.getWaveObjectAtom<Block>(WOS.makeORef("block", win.activeblockid));
|
const blockAtom = WOS.getWaveObjectAtom<Block>(WOS.makeORef("block", focusedNode.data?.blockId));
|
||||||
const blockData = globalStore.get(blockAtom);
|
const blockData = globalStore.get(blockAtom);
|
||||||
if (blockData?.meta?.view == "term") {
|
if (blockData?.meta?.view == "term") {
|
||||||
if (blockData?.meta?.["cmd:cwd"] != null) {
|
if (blockData?.meta?.["cmd:cwd"] != null) {
|
||||||
@@ -195,27 +121,7 @@ async function handleCmdT() {
|
|||||||
termBlockDef.meta.connection = blockData.meta.connection;
|
termBlockDef.meta.connection = blockData.meta.connection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const newBlockId = await createBlock(termBlockDef);
|
await createBlock(termBlockDef);
|
||||||
setBlockFocus(newBlockId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCmdI() {
|
|
||||||
const waveWindow = globalStore.get(atoms.waveWindow);
|
|
||||||
if (waveWindow == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let activeBlockId = waveWindow.activeblockid;
|
|
||||||
if (activeBlockId == null) {
|
|
||||||
// get the first block
|
|
||||||
const tabData = globalStore.get(atoms.tabAtom);
|
|
||||||
const firstBlockId = tabData.blockids?.length == 0 ? null : tabData.blockids[0];
|
|
||||||
if (firstBlockId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
activeBlockId = firstBlockId;
|
|
||||||
}
|
|
||||||
const viewModel = getViewModel(activeBlockId);
|
|
||||||
viewModel?.giveFocus?.();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
|
function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
|
||||||
@@ -241,11 +147,7 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:n")) {
|
if (keyutil.checkKeyPressed(waveEvent, "Cmd:n")) {
|
||||||
handleCmdT();
|
handleCmdN();
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:i")) {
|
|
||||||
handleCmdI();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:t")) {
|
if (keyutil.checkKeyPressed(waveEvent, "Cmd:t")) {
|
||||||
@@ -265,24 +167,24 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
|
|||||||
keyutil.checkKeyPressed(waveEvent, `Ctrl:Shift:c{Digit${idx}}`) ||
|
keyutil.checkKeyPressed(waveEvent, `Ctrl:Shift:c{Digit${idx}}`) ||
|
||||||
keyutil.checkKeyPressed(waveEvent, `Ctrl:Shift:c{Numpad${idx}}`)
|
keyutil.checkKeyPressed(waveEvent, `Ctrl:Shift:c{Numpad${idx}}`)
|
||||||
) {
|
) {
|
||||||
switchBlockIdx(idx);
|
switchBlockByBlockNum(idx);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowUp")) {
|
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowUp")) {
|
||||||
switchBlock(tabId, 0, -1);
|
switchBlockInDirection(tabId, NavigateDirection.Up);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowDown")) {
|
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowDown")) {
|
||||||
switchBlock(tabId, 0, 1);
|
switchBlockInDirection(tabId, NavigateDirection.Down);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowLeft")) {
|
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowLeft")) {
|
||||||
switchBlock(tabId, -1, 0);
|
switchBlockInDirection(tabId, NavigateDirection.Left);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowRight")) {
|
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowRight")) {
|
||||||
switchBlock(tabId, 1, 0);
|
switchBlockInDirection(tabId, NavigateDirection.Right);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:w")) {
|
if (keyutil.checkKeyPressed(waveEvent, "Cmd:w")) {
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { BlockComponentModel, BlockProps, LayoutComponentModel } from "@/app/block/blocktypes";
|
import { BlockComponentModel, BlockProps } from "@/app/block/blocktypes";
|
||||||
import { PlotView } from "@/app/view/plotview/plotview";
|
import { PlotView } from "@/app/view/plotview/plotview";
|
||||||
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
|
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
|
||||||
import { ErrorBoundary } from "@/element/errorboundary";
|
import { ErrorBoundary } from "@/element/errorboundary";
|
||||||
import { CenteredDiv } from "@/element/quickelems";
|
import { CenteredDiv } from "@/element/quickelems";
|
||||||
import {
|
import { NodeModel } from "@/layout/index";
|
||||||
atoms,
|
import { counterInc, getViewModel, registerViewModel, unregisterViewModel } from "@/store/global";
|
||||||
counterInc,
|
|
||||||
getViewModel,
|
|
||||||
registerViewModel,
|
|
||||||
setBlockFocus,
|
|
||||||
unregisterViewModel,
|
|
||||||
useBlockAtom,
|
|
||||||
} from "@/store/global";
|
|
||||||
import * as WOS from "@/store/wos";
|
import * as WOS from "@/store/wos";
|
||||||
import * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
import { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel } from "@/view/cpuplot/cpuplot";
|
import { CpuPlotView, CpuPlotViewModel, makeCpuPlotViewModel } from "@/view/cpuplot/cpuplot";
|
||||||
@@ -24,15 +17,13 @@ import { WaveAi, WaveAiModel, makeWaveAiViewModel } from "@/view/waveai/waveai";
|
|||||||
import { WebView, WebViewModel, makeWebViewModel } from "@/view/webview/webview";
|
import { WebView, WebViewModel, makeWebViewModel } from "@/view/webview/webview";
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import "./block.less";
|
||||||
import { BlockFrame } from "./blockframe";
|
import { BlockFrame } from "./blockframe";
|
||||||
import { blockViewToIcon, blockViewToName } from "./blockutil";
|
import { blockViewToIcon, blockViewToName } from "./blockutil";
|
||||||
|
|
||||||
import "./block.less";
|
|
||||||
|
|
||||||
type FullBlockProps = {
|
type FullBlockProps = {
|
||||||
blockId: string;
|
|
||||||
preview: boolean;
|
preview: boolean;
|
||||||
layoutModel: LayoutComponentModel;
|
nodeModel: NodeModel;
|
||||||
viewModel: ViewModel;
|
viewModel: ViewModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,16 +96,15 @@ function makeDefaultViewModel(blockId: string, viewType: string): ViewModel {
|
|||||||
return viewModel;
|
return viewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BlockPreview = React.memo(({ blockId, layoutModel, viewModel }: FullBlockProps) => {
|
const BlockPreview = React.memo(({ nodeModel, viewModel }: FullBlockProps) => {
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
|
||||||
if (!blockData) {
|
if (!blockData) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<BlockFrame
|
<BlockFrame
|
||||||
key={blockId}
|
key={nodeModel.blockId}
|
||||||
blockId={blockId}
|
nodeModel={nodeModel}
|
||||||
layoutModel={layoutModel}
|
|
||||||
preview={true}
|
preview={true}
|
||||||
blockModel={null}
|
blockModel={null}
|
||||||
viewModel={viewModel}
|
viewModel={viewModel}
|
||||||
@@ -122,20 +112,16 @@ const BlockPreview = React.memo(({ blockId, layoutModel, viewModel }: FullBlockP
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const BlockFull = React.memo(({ blockId, layoutModel, viewModel }: FullBlockProps) => {
|
const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => {
|
||||||
counterInc("render-BlockFull");
|
counterInc("render-BlockFull");
|
||||||
const focusElemRef = React.useRef<HTMLInputElement>(null);
|
const focusElemRef = React.useRef<HTMLInputElement>(null);
|
||||||
const blockRef = React.useRef<HTMLDivElement>(null);
|
const blockRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [blockClicked, setBlockClicked] = React.useState(false);
|
const [blockClicked, setBlockClicked] = React.useState(false);
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
|
||||||
const [focusedChild, setFocusedChild] = React.useState(null);
|
const [focusedChild, setFocusedChild] = React.useState(null);
|
||||||
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
|
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
|
||||||
return jotai.atom((get) => {
|
const disablePointerEvents = jotai.useAtomValue(nodeModel.disablePointerEvents);
|
||||||
const winData = get(atoms.waveWindow);
|
const addlProps = jotai.useAtomValue(nodeModel.additionalProps);
|
||||||
return winData.activeblockid === blockId;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const isFocused = jotai.useAtomValue(isFocusedAtom);
|
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
setBlockClicked(isFocused);
|
setBlockClicked(isFocused);
|
||||||
@@ -150,24 +136,24 @@ const BlockFull = React.memo(({ blockId, layoutModel, viewModel }: FullBlockProp
|
|||||||
if (!focusWithin) {
|
if (!focusWithin) {
|
||||||
setFocusTarget();
|
setFocusTarget();
|
||||||
}
|
}
|
||||||
setBlockFocus(blockId);
|
nodeModel.focusNode();
|
||||||
}, [blockClicked]);
|
}, [blockClicked]);
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
if (focusedChild == null) {
|
if (focusedChild == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setBlockFocus(blockId);
|
nodeModel.focusNode();
|
||||||
}, [focusedChild, blockId]);
|
}, [focusedChild]);
|
||||||
|
|
||||||
// treat the block as clicked on creation
|
// treat the block as clicked on creation
|
||||||
const setBlockClickedTrue = React.useCallback(() => {
|
const setBlockClickedTrue = React.useCallback(() => {
|
||||||
setBlockClicked(true);
|
setBlockClicked(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let viewElem = React.useMemo(
|
const viewElem = React.useMemo(
|
||||||
() => getViewElem(blockId, blockData?.meta?.view, viewModel),
|
() => getViewElem(nodeModel.blockId, blockData?.meta?.view, viewModel),
|
||||||
[blockId, blockData?.meta?.view, viewModel]
|
[nodeModel.blockId, blockData?.meta?.view, viewModel]
|
||||||
);
|
);
|
||||||
|
|
||||||
const determineFocusedChild = React.useCallback(
|
const determineFocusedChild = React.useCallback(
|
||||||
@@ -193,20 +179,29 @@ const BlockFull = React.memo(({ blockId, layoutModel, viewModel }: FullBlockProp
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BlockFrame
|
<BlockFrame
|
||||||
key={blockId}
|
key={nodeModel.blockId}
|
||||||
blockId={blockId}
|
nodeModel={nodeModel}
|
||||||
layoutModel={layoutModel}
|
|
||||||
preview={false}
|
preview={false}
|
||||||
blockModel={blockModel}
|
blockModel={blockModel}
|
||||||
viewModel={viewModel}
|
viewModel={viewModel}
|
||||||
>
|
>
|
||||||
<div key="focuselem" className="block-focuselem">
|
<div key="focuselem" className="block-focuselem">
|
||||||
<input type="text" value="" ref={focusElemRef} id={`${blockId}-dummy-focus`} onChange={() => {}} />
|
<input
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
ref={focusElemRef}
|
||||||
|
id={`${nodeModel.blockId}-dummy-focus`}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
key="content"
|
key="content"
|
||||||
className="block-content"
|
className="block-content"
|
||||||
style={{ pointerEvents: layoutModel?.disablePointerEvents ? "none" : undefined }}
|
style={{
|
||||||
|
pointerEvents: disablePointerEvents ? "none" : undefined,
|
||||||
|
width: addlProps?.transform?.width,
|
||||||
|
height: addlProps?.transform?.height,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</React.Suspense>
|
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</React.Suspense>
|
||||||
@@ -218,19 +213,19 @@ const BlockFull = React.memo(({ blockId, layoutModel, viewModel }: FullBlockProp
|
|||||||
|
|
||||||
const Block = React.memo((props: BlockProps) => {
|
const Block = React.memo((props: BlockProps) => {
|
||||||
counterInc("render-Block");
|
counterInc("render-Block");
|
||||||
counterInc("render-Block-" + props.blockId.substring(0, 8));
|
counterInc("render-Block-" + props.nodeModel.blockId.substring(0, 8));
|
||||||
const [blockData, loading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", props.blockId));
|
const [blockData, loading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", props.nodeModel.blockId));
|
||||||
let viewModel = getViewModel(props.blockId);
|
let viewModel = getViewModel(props.nodeModel.blockId);
|
||||||
if (viewModel == null || viewModel.viewType != blockData?.meta?.view) {
|
if (viewModel == null || viewModel.viewType != blockData?.meta?.view) {
|
||||||
viewModel = makeViewModel(props.blockId, blockData?.meta?.view);
|
viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view);
|
||||||
registerViewModel(props.blockId, viewModel);
|
registerViewModel(props.nodeModel.blockId, viewModel);
|
||||||
}
|
}
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
unregisterViewModel(props.blockId);
|
unregisterViewModel(props.nodeModel.blockId);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
if (loading || util.isBlank(props.blockId) || blockData == null) {
|
if (loading || util.isBlank(props.nodeModel.blockId) || blockData == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (props.preview) {
|
if (props.preview) {
|
||||||
|
|||||||
@@ -11,21 +11,22 @@ import {
|
|||||||
} from "@/app/block/blockutil";
|
} from "@/app/block/blockutil";
|
||||||
import { Button } from "@/app/element/button";
|
import { Button } from "@/app/element/button";
|
||||||
import { ContextMenuModel } from "@/app/store/contextmenu";
|
import { ContextMenuModel } from "@/app/store/contextmenu";
|
||||||
import { atoms, globalStore, useBlockAtom, WOS } from "@/app/store/global";
|
import { atoms, globalStore, WOS } from "@/app/store/global";
|
||||||
import * as services from "@/app/store/services";
|
import * as services from "@/app/store/services";
|
||||||
import { MagnifyIcon } from "@/element/magnify";
|
import { MagnifyIcon } from "@/element/magnify";
|
||||||
import { useLayoutModel } from "@/layout/index";
|
import { NodeModel } from "@/layout/index";
|
||||||
import { checkKeyPressed, keydownWrapper } from "@/util/keyutil";
|
import { checkKeyPressed, keydownWrapper } from "@/util/keyutil";
|
||||||
import * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { BlockFrameProps, LayoutComponentModel } from "./blocktypes";
|
import { BlockFrameProps } from "./blocktypes";
|
||||||
|
|
||||||
function handleHeaderContextMenu(
|
function handleHeaderContextMenu(
|
||||||
e: React.MouseEvent<HTMLDivElement>,
|
e: React.MouseEvent<HTMLDivElement>,
|
||||||
blockData: Block,
|
blockData: Block,
|
||||||
viewModel: ViewModel,
|
viewModel: ViewModel,
|
||||||
|
magnified: boolean,
|
||||||
onMagnifyToggle: () => void,
|
onMagnifyToggle: () => void,
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
) {
|
) {
|
||||||
@@ -33,7 +34,7 @@ function handleHeaderContextMenu(
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
let menu: ContextMenuItem[] = [
|
let menu: ContextMenuItem[] = [
|
||||||
{
|
{
|
||||||
label: "Magnify Block",
|
label: magnified ? "Un-magnify Block" : "Magnify Block",
|
||||||
click: () => {
|
click: () => {
|
||||||
onMagnifyToggle();
|
onMagnifyToggle();
|
||||||
},
|
},
|
||||||
@@ -78,20 +79,27 @@ function getViewIconElem(viewIconUnion: string | HeaderIconButton, blockData: Bl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const OptMagnifyButton = React.memo(({ layoutCompModel }: { layoutCompModel: LayoutComponentModel }) => {
|
const OptMagnifyButton = React.memo(
|
||||||
const magnifyDecl: HeaderIconButton = {
|
({ magnified, toggleMagnify }: { magnified: boolean; toggleMagnify: () => void }) => {
|
||||||
elemtype: "iconbutton",
|
const magnifyDecl: HeaderIconButton = {
|
||||||
icon: <MagnifyIcon enabled={layoutCompModel?.isMagnified} />,
|
elemtype: "iconbutton",
|
||||||
title: layoutCompModel?.isMagnified ? "Minimize" : "Magnify",
|
icon: <MagnifyIcon enabled={magnified} />,
|
||||||
click: layoutCompModel?.onMagnifyToggle,
|
title: magnified ? "Minimize" : "Magnify",
|
||||||
};
|
click: toggleMagnify,
|
||||||
return <IconButton key="magnify" decl={magnifyDecl} className="block-frame-magnify" />;
|
};
|
||||||
});
|
return <IconButton key="magnify" decl={magnifyDecl} className="block-frame-magnify" />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function computeEndIcons(blockData: Block, viewModel: ViewModel, layoutModel: LayoutComponentModel): JSX.Element[] {
|
function computeEndIcons(
|
||||||
|
viewModel: ViewModel,
|
||||||
|
magnified: boolean,
|
||||||
|
toggleMagnify: () => void,
|
||||||
|
onClose: () => void,
|
||||||
|
onContextMenu: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||||
|
): JSX.Element[] {
|
||||||
const endIconsElem: JSX.Element[] = [];
|
const endIconsElem: JSX.Element[] = [];
|
||||||
const endIconButtons = util.useAtomValueSafe(viewModel.endIconButtons);
|
const endIconButtons = util.useAtomValueSafe(viewModel.endIconButtons);
|
||||||
|
|
||||||
if (endIconButtons && endIconButtons.length > 0) {
|
if (endIconButtons && endIconButtons.length > 0) {
|
||||||
endIconsElem.push(...endIconButtons.map((button, idx) => <IconButton key={idx} decl={button} />));
|
endIconsElem.push(...endIconButtons.map((button, idx) => <IconButton key={idx} decl={button} />));
|
||||||
}
|
}
|
||||||
@@ -99,30 +107,44 @@ function computeEndIcons(blockData: Block, viewModel: ViewModel, layoutModel: La
|
|||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
icon: "cog",
|
icon: "cog",
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
click: (e) =>
|
click: onContextMenu,
|
||||||
handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onMagnifyToggle, layoutModel?.onClose),
|
|
||||||
};
|
};
|
||||||
endIconsElem.push(<IconButton key="settings" decl={settingsDecl} className="block-frame-settings" />);
|
endIconsElem.push(<IconButton key="settings" decl={settingsDecl} className="block-frame-settings" />);
|
||||||
endIconsElem.push(<OptMagnifyButton key="unmagnify" layoutCompModel={layoutModel} />);
|
endIconsElem.push(<OptMagnifyButton key="unmagnify" magnified={magnified} toggleMagnify={toggleMagnify} />);
|
||||||
const closeDecl: HeaderIconButton = {
|
const closeDecl: HeaderIconButton = {
|
||||||
elemtype: "iconbutton",
|
elemtype: "iconbutton",
|
||||||
icon: "xmark-large",
|
icon: "xmark-large",
|
||||||
title: "Close",
|
title: "Close",
|
||||||
click: layoutModel?.onClose,
|
click: onClose,
|
||||||
};
|
};
|
||||||
endIconsElem.push(<IconButton key="close" decl={closeDecl} className="block-frame-default-close" />);
|
endIconsElem.push(<IconButton key="close" decl={closeDecl} className="block-frame-default-close" />);
|
||||||
return endIconsElem;
|
return endIconsElem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BlockFrame_Header = ({ blockId, layoutModel, viewModel }: BlockFrameProps) => {
|
const BlockFrame_Header = ({ nodeModel, viewModel, preview }: BlockFrameProps) => {
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
|
||||||
const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.meta?.view);
|
const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.meta?.view);
|
||||||
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
|
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
|
||||||
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
|
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
|
||||||
const preIconButton = util.useAtomValueSafe(viewModel.preIconButton);
|
const preIconButton = util.useAtomValueSafe(viewModel.preIconButton);
|
||||||
const headerTextUnion = util.useAtomValueSafe(viewModel.viewText);
|
const headerTextUnion = util.useAtomValueSafe(viewModel.viewText);
|
||||||
|
const magnified = jotai.useAtomValue(nodeModel.isMagnified);
|
||||||
|
const dragHandleRef = preview ? null : nodeModel.dragHandleRef;
|
||||||
|
|
||||||
const endIconsElem = computeEndIcons(blockData, viewModel, layoutModel);
|
const onContextMenu = React.useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
handleHeaderContextMenu(e, blockData, viewModel, magnified, nodeModel.toggleMagnify, nodeModel.onClose);
|
||||||
|
},
|
||||||
|
[magnified]
|
||||||
|
);
|
||||||
|
|
||||||
|
const endIconsElem = computeEndIcons(
|
||||||
|
viewModel,
|
||||||
|
magnified,
|
||||||
|
nodeModel.toggleMagnify,
|
||||||
|
nodeModel.onClose,
|
||||||
|
onContextMenu
|
||||||
|
);
|
||||||
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
|
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
|
||||||
let preIconButtonElem: JSX.Element = null;
|
let preIconButtonElem: JSX.Element = null;
|
||||||
if (preIconButton) {
|
if (preIconButton) {
|
||||||
@@ -139,23 +161,17 @@ const BlockFrame_Header = ({ blockId, layoutModel, viewModel }: BlockFrameProps)
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (Array.isArray(headerTextUnion)) {
|
} else if (Array.isArray(headerTextUnion)) {
|
||||||
headerTextElems.push(...renderHeaderElements(headerTextUnion));
|
headerTextElems.push(...renderHeaderElements(headerTextUnion, preview));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="block-frame-default-header" ref={dragHandleRef} onContextMenu={onContextMenu}>
|
||||||
className="block-frame-default-header"
|
|
||||||
ref={layoutModel?.dragHandleRef}
|
|
||||||
onContextMenu={(e) =>
|
|
||||||
handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onMagnifyToggle, layoutModel?.onClose)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{preIconButtonElem}
|
{preIconButtonElem}
|
||||||
<div className="block-frame-default-header-iconview">
|
<div className="block-frame-default-header-iconview">
|
||||||
{viewIconElem}
|
{viewIconElem}
|
||||||
<div className="block-frame-view-type">{viewName}</div>
|
<div className="block-frame-view-type">{viewName}</div>
|
||||||
{settingsConfig?.blockheader?.showblockids && (
|
{settingsConfig?.blockheader?.showblockids && (
|
||||||
<div className="block-frame-blockid">[{blockId.substring(0, 8)}]</div>
|
<div className="block-frame-blockid">[{nodeModel.blockId.substring(0, 8)}]</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="block-frame-textelems-wrapper">{headerTextElems}</div>
|
<div className="block-frame-textelems-wrapper">{headerTextElems}</div>
|
||||||
@@ -164,11 +180,11 @@ const BlockFrame_Header = ({ blockId, layoutModel, viewModel }: BlockFrameProps)
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeaderTextElem = React.memo(({ elem }: { elem: HeaderElem }) => {
|
const HeaderTextElem = React.memo(({ elem, preview }: { elem: HeaderElem; preview: boolean }) => {
|
||||||
if (elem.elemtype == "iconbutton") {
|
if (elem.elemtype == "iconbutton") {
|
||||||
return <IconButton decl={elem} className={clsx("block-frame-header-iconbutton", elem.className)} />;
|
return <IconButton decl={elem} className={clsx("block-frame-header-iconbutton", elem.className)} />;
|
||||||
} else if (elem.elemtype == "input") {
|
} else if (elem.elemtype == "input") {
|
||||||
return <Input decl={elem} className={clsx("block-frame-input", elem.className)} />;
|
return <Input decl={elem} className={clsx("block-frame-input", elem.className)} preview={preview} />;
|
||||||
} else if (elem.elemtype == "text") {
|
} else if (elem.elemtype == "text") {
|
||||||
return <div className="block-frame-text">{elem.text}</div>;
|
return <div className="block-frame-text">{elem.text}</div>;
|
||||||
} else if (elem.elemtype == "textbutton") {
|
} else if (elem.elemtype == "textbutton") {
|
||||||
@@ -187,7 +203,7 @@ const HeaderTextElem = React.memo(({ elem }: { elem: HeaderElem }) => {
|
|||||||
onMouseOut={elem.onMouseOut}
|
onMouseOut={elem.onMouseOut}
|
||||||
>
|
>
|
||||||
{elem.children.map((child, childIdx) => (
|
{elem.children.map((child, childIdx) => (
|
||||||
<HeaderTextElem elem={child} key={childIdx} />
|
<HeaderTextElem elem={child} key={childIdx} preview={preview} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -195,11 +211,11 @@ const HeaderTextElem = React.memo(({ elem }: { elem: HeaderElem }) => {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderHeaderElements(headerTextUnion: HeaderElem[]): JSX.Element[] {
|
function renderHeaderElements(headerTextUnion: HeaderElem[], preview: boolean): JSX.Element[] {
|
||||||
const headerTextElems: JSX.Element[] = [];
|
const headerTextElems: JSX.Element[] = [];
|
||||||
for (let idx = 0; idx < headerTextUnion.length; idx++) {
|
for (let idx = 0; idx < headerTextUnion.length; idx++) {
|
||||||
const elem = headerTextUnion[idx];
|
const elem = headerTextUnion[idx];
|
||||||
const renderedElement = <HeaderTextElem elem={elem} key={idx} />;
|
const renderedElement = <HeaderTextElem elem={elem} key={idx} preview={preview} />;
|
||||||
if (renderedElement) {
|
if (renderedElement) {
|
||||||
headerTextElems.push(renderedElement);
|
headerTextElems.push(renderedElement);
|
||||||
}
|
}
|
||||||
@@ -207,18 +223,11 @@ function renderHeaderElements(headerTextUnion: HeaderElem[]): JSX.Element[] {
|
|||||||
return headerTextElems;
|
return headerTextElems;
|
||||||
}
|
}
|
||||||
|
|
||||||
function BlockNum({ blockId }: { blockId: string }) {
|
const BlockMask = ({ nodeModel }: { nodeModel: NodeModel }) => {
|
||||||
const tabId = jotai.useAtomValue(atoms.activeTabId);
|
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
|
||||||
const tabAtom = WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", tabId));
|
const blockNum = jotai.useAtomValue(nodeModel.blockNum);
|
||||||
const layoutModel = useLayoutModel(tabAtom);
|
|
||||||
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 }) => {
|
|
||||||
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
|
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
|
||||||
|
|
||||||
const style: React.CSSProperties = {};
|
const style: React.CSSProperties = {};
|
||||||
if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
|
if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
|
||||||
@@ -231,9 +240,7 @@ const BlockMask = ({ blockId, preview, isFocused }: { blockId: string; preview:
|
|||||||
if (isLayoutMode) {
|
if (isLayoutMode) {
|
||||||
innerElem = (
|
innerElem = (
|
||||||
<div className="block-mask-inner">
|
<div className="block-mask-inner">
|
||||||
<div className="bignum">
|
<div className="bignum">{blockNum}</div>
|
||||||
<BlockNum blockId={blockId} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -245,27 +252,17 @@ const BlockMask = ({ blockId, preview, isFocused }: { blockId: string; preview:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
||||||
const { blockId, layoutModel, viewModel, blockModel, preview, numBlocksInTab, children } = props;
|
const { nodeModel, viewModel, blockModel, preview, numBlocksInTab, children } = props;
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
|
||||||
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
|
const isFocused = jotai.useAtomValue(nodeModel.isFocused);
|
||||||
return jotai.atom((get) => {
|
|
||||||
const winData = get(atoms.waveWindow);
|
|
||||||
return winData?.activeblockid === blockId;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
|
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
|
||||||
const customBg = util.useAtomValueSafe(viewModel.blockBg);
|
const customBg = util.useAtomValueSafe(viewModel.blockBg);
|
||||||
|
|
||||||
let isFocused = jotai.useAtomValue(isFocusedAtom);
|
|
||||||
if (preview) {
|
|
||||||
isFocused = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
|
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
|
||||||
|
|
||||||
function handleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
|
function handleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
|
||||||
if (checkKeyPressed(waveEvent, "Cmd:m")) {
|
if (checkKeyPressed(waveEvent, "Cmd:m")) {
|
||||||
layoutModel?.onMagnifyToggle();
|
nodeModel.toggleMagnify();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (viewModel?.keyDownHandler) {
|
if (viewModel?.keyDownHandler) {
|
||||||
@@ -286,20 +283,17 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
|||||||
const previewElem = <div className="block-frame-preview">{viewIconElem}</div>;
|
const previewElem = <div className="block-frame-preview">{viewIconElem}</div>;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx("block", "block-frame-default", "block-" + nodeModel.blockId, {
|
||||||
"block",
|
"block-focused": isFocused || preview,
|
||||||
"block-frame-default",
|
"block-preview": preview,
|
||||||
isFocused ? "block-focused" : null,
|
"block-no-highlight": numBlocksInTab === 1,
|
||||||
preview ? "block-preview" : null,
|
})}
|
||||||
numBlocksInTab == 1 ? "block-no-highlight" : null,
|
|
||||||
"block-" + blockId
|
|
||||||
)}
|
|
||||||
onClick={blockModel?.onClick}
|
onClick={blockModel?.onClick}
|
||||||
onFocusCapture={blockModel?.onFocusCapture}
|
onFocusCapture={blockModel?.onFocusCapture}
|
||||||
ref={blockModel?.blockRef}
|
ref={blockModel?.blockRef}
|
||||||
onKeyDown={keydownWrapper(handleKeyDown)}
|
onKeyDown={keydownWrapper(handleKeyDown)}
|
||||||
>
|
>
|
||||||
<BlockMask blockId={blockId} preview={preview} isFocused={isFocused} />
|
<BlockMask nodeModel={nodeModel} />
|
||||||
<div className="block-frame-default-inner" style={innerStyle}>
|
<div className="block-frame-default-inner" style={innerStyle}>
|
||||||
<BlockFrame_Header {...props} />
|
<BlockFrame_Header {...props} />
|
||||||
{preview ? previewElem : children}
|
{preview ? previewElem : children}
|
||||||
@@ -311,7 +305,7 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
|||||||
const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component;
|
const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component;
|
||||||
|
|
||||||
const BlockFrame = React.memo((props: BlockFrameProps) => {
|
const BlockFrame = React.memo((props: BlockFrameProps) => {
|
||||||
const blockId = props.blockId;
|
const blockId = props.nodeModel.blockId;
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||||
const tabData = jotai.useAtomValue(atoms.tabAtom);
|
const tabData = jotai.useAtomValue(atoms.tabAtom);
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
export interface LayoutComponentModel {
|
import { NodeModel } from "@/layout/index";
|
||||||
disablePointerEvents: boolean;
|
|
||||||
onClose?: () => void;
|
|
||||||
onMagnifyToggle?: () => void;
|
|
||||||
isMagnified: boolean;
|
|
||||||
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlockProps {
|
export interface BlockProps {
|
||||||
blockId: string;
|
|
||||||
preview: boolean;
|
preview: boolean;
|
||||||
layoutModel: LayoutComponentModel;
|
nodeModel: NodeModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BlockComponentModel {
|
export interface BlockComponentModel {
|
||||||
@@ -22,9 +14,8 @@ export interface BlockComponentModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BlockFrameProps {
|
export interface BlockFrameProps {
|
||||||
blockId: string;
|
|
||||||
blockModel?: BlockComponentModel;
|
blockModel?: BlockComponentModel;
|
||||||
layoutModel?: LayoutComponentModel;
|
nodeModel?: NodeModel;
|
||||||
viewModel?: ViewModel;
|
viewModel?: ViewModel;
|
||||||
preview: boolean;
|
preview: boolean;
|
||||||
numBlocksInTab?: number;
|
numBlocksInTab?: number;
|
||||||
|
|||||||
@@ -196,20 +196,26 @@ export const ConnectionButton = React.memo(({ decl }: { decl: ConnectionButton }
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Input = React.memo(({ decl, className }: { decl: HeaderInput; className: string }) => {
|
export const Input = React.memo(
|
||||||
const { value, ref, isDisabled, onChange, onKeyDown, onFocus, onBlur } = decl;
|
({ decl, className, preview }: { decl: HeaderInput; className: string; preview: boolean }) => {
|
||||||
return (
|
const { value, ref, isDisabled, onChange, onKeyDown, onFocus, onBlur } = decl;
|
||||||
<div className="input-wrapper">
|
return (
|
||||||
<input
|
<div className="input-wrapper">
|
||||||
ref={ref}
|
<input
|
||||||
disabled={isDisabled}
|
ref={
|
||||||
className={className}
|
!preview
|
||||||
value={value}
|
? ref
|
||||||
onChange={(e) => onChange(e)}
|
: undefined /* don't wire up the input field if the preview block is being rendered */
|
||||||
onKeyDown={(e) => onKeyDown(e)}
|
}
|
||||||
onFocus={(e) => onFocus(e)}
|
disabled={isDisabled}
|
||||||
onBlur={(e) => onBlur(e)}
|
className={className}
|
||||||
/>
|
value={value}
|
||||||
</div>
|
onChange={(e) => onChange(e)}
|
||||||
);
|
onKeyDown={(e) => onKeyDown(e)}
|
||||||
});
|
onFocus={(e) => onFocus(e)}
|
||||||
|
onBlur={(e) => onBlur(e)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import { handleIncomingRpcMessage, sendRawRpcMessage } from "@/app/store/wshrpc";
|
import { handleIncomingRpcMessage, sendRawRpcMessage } from "@/app/store/wshrpc";
|
||||||
import {
|
import {
|
||||||
|
getLayoutModelForActiveTab,
|
||||||
getLayoutModelForTabById,
|
getLayoutModelForTabById,
|
||||||
LayoutTreeActionType,
|
LayoutTreeActionType,
|
||||||
LayoutTreeInsertNodeAction,
|
LayoutTreeInsertNodeAction,
|
||||||
@@ -12,7 +13,7 @@ import {
|
|||||||
import { getWebServerEndpoint, getWSServerEndpoint } from "@/util/endpoints";
|
import { getWebServerEndpoint, getWSServerEndpoint } from "@/util/endpoints";
|
||||||
import { fetch } from "@/util/fetchutil";
|
import { fetch } from "@/util/fetchutil";
|
||||||
import * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
import { produce } from "immer";
|
import { fireAndForget } from "@/util/util";
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import * as rxjs from "rxjs";
|
import * as rxjs from "rxjs";
|
||||||
import { modalsModel } from "./modalmodel";
|
import { modalsModel } from "./modalmodel";
|
||||||
@@ -60,7 +61,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
|||||||
const isFullScreenAtom = jotai.atom(false) as jotai.PrimitiveAtom<boolean>;
|
const isFullScreenAtom = jotai.atom(false) as jotai.PrimitiveAtom<boolean>;
|
||||||
try {
|
try {
|
||||||
getApi().onFullScreenChange((isFullScreen) => {
|
getApi().onFullScreenChange((isFullScreen) => {
|
||||||
console.log("fullscreen change", isFullScreen);
|
|
||||||
globalStore.set(isFullScreenAtom, isFullScreen);
|
globalStore.set(isFullScreenAtom, isFullScreen);
|
||||||
});
|
});
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -118,7 +118,6 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
|||||||
try {
|
try {
|
||||||
globalStore.set(updaterStatusAtom, getApi().getUpdaterStatus());
|
globalStore.set(updaterStatusAtom, getApi().getUpdaterStatus());
|
||||||
getApi().onUpdaterStatusChange((status) => {
|
getApi().onUpdaterStatusChange((status) => {
|
||||||
console.log("updater status change", status);
|
|
||||||
globalStore.set(updaterStatusAtom, status);
|
globalStore.set(updaterStatusAtom, status);
|
||||||
});
|
});
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -336,7 +335,7 @@ function handleWaveEvent(event: WaveEvent) {
|
|||||||
|
|
||||||
function handleWSEventMessage(msg: WSEventType) {
|
function handleWSEventMessage(msg: WSEventType) {
|
||||||
if (msg.eventtype == null) {
|
if (msg.eventtype == null) {
|
||||||
console.log("unsupported event", msg);
|
console.warn("unsupported WSEvent", msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg.eventtype == "config") {
|
if (msg.eventtype == "config") {
|
||||||
@@ -380,7 +379,7 @@ function handleWSEventMessage(msg: WSEventType) {
|
|||||||
case LayoutTreeActionType.DeleteNode: {
|
case LayoutTreeActionType.DeleteNode: {
|
||||||
const leaf = layoutModel?.getNodeByBlockId(layoutAction.blockid);
|
const leaf = layoutModel?.getNodeByBlockId(layoutAction.blockid);
|
||||||
if (leaf) {
|
if (leaf) {
|
||||||
layoutModel.closeNode(leaf);
|
fireAndForget(() => layoutModel.closeNode(leaf.id));
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
"Cannot apply eventbus layout action DeleteNode, could not find leaf node with blockId",
|
"Cannot apply eventbus layout action DeleteNode, could not find leaf node with blockId",
|
||||||
@@ -406,7 +405,7 @@ function handleWSEventMessage(msg: WSEventType) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
console.log("unsupported layout action", layoutAction);
|
console.warn("unsupported layout action", layoutAction);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -459,12 +458,13 @@ function getApi(): ElectronApi {
|
|||||||
return (window as any).api;
|
return (window as any).api;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createBlock(blockDef: BlockDef): Promise<string> {
|
async function createBlock(blockDef: BlockDef, magnified = false): Promise<string> {
|
||||||
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
|
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
|
||||||
const blockId = await services.ObjectService.CreateBlock(blockDef, rtOpts);
|
const blockId = await services.ObjectService.CreateBlock(blockDef, rtOpts);
|
||||||
const insertNodeAction: LayoutTreeInsertNodeAction = {
|
const insertNodeAction: LayoutTreeInsertNodeAction = {
|
||||||
type: LayoutTreeActionType.InsertNode,
|
type: LayoutTreeActionType.InsertNode,
|
||||||
node: newLayoutNode(undefined, undefined, undefined, { blockId }),
|
node: newLayoutNode(undefined, undefined, undefined, { blockId }),
|
||||||
|
magnified,
|
||||||
};
|
};
|
||||||
const activeTabId = globalStore.get(atoms.uiContext).activetabid;
|
const activeTabId = globalStore.get(atoms.uiContext).activetabid;
|
||||||
const layoutModel = getLayoutModelForTabById(activeTabId);
|
const layoutModel = getLayoutModelForTabById(activeTabId);
|
||||||
@@ -503,18 +503,9 @@ async function fetchWaveFile(
|
|||||||
return { data: new Uint8Array(data), fileInfo };
|
return { data: new Uint8Array(data), fileInfo };
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBlockFocus(blockId: string) {
|
function setNodeFocus(nodeId: string) {
|
||||||
let winData = globalStore.get(atoms.waveWindow);
|
const layoutModel = getLayoutModelForActiveTab();
|
||||||
if (winData == null) {
|
layoutModel.focusNode(nodeId);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (winData.activeblockid === blockId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
winData = produce(winData, (draft) => {
|
|
||||||
draft.activeblockid = blockId;
|
|
||||||
});
|
|
||||||
WOS.setObjectValue(winData, globalStore.set, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const objectIdWeakMap = new WeakMap();
|
const objectIdWeakMap = new WeakMap();
|
||||||
@@ -635,7 +626,7 @@ export {
|
|||||||
PLATFORM,
|
PLATFORM,
|
||||||
registerViewModel,
|
registerViewModel,
|
||||||
sendWSCommand,
|
sendWSCommand,
|
||||||
setBlockFocus,
|
setNodeFocus,
|
||||||
setPlatform,
|
setPlatform,
|
||||||
subscribeToConnEvents,
|
subscribeToConnEvents,
|
||||||
unregisterViewModel,
|
unregisterViewModel,
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { Block } from "@/app/block/block";
|
import { Block } from "@/app/block/block";
|
||||||
import { LayoutComponentModel } from "@/app/block/blocktypes";
|
|
||||||
import { CenteredDiv } from "@/element/quickelems";
|
import { CenteredDiv } from "@/element/quickelems";
|
||||||
import { ContentRenderer, TileLayout } from "@/layout/index";
|
import { ContentRenderer, NodeModel, PreviewRenderer, TileLayout } from "@/layout/index";
|
||||||
import { getApi } from "@/store/global";
|
import { getApi } from "@/store/global";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import * as WOS from "@/store/wos";
|
import * as WOS from "@/store/wos";
|
||||||
@@ -21,34 +20,13 @@ const TabContent = React.memo(({ tabId }: { tabId: string }) => {
|
|||||||
const tabData = useAtomValue(tabAtom);
|
const tabData = useAtomValue(tabAtom);
|
||||||
|
|
||||||
const tileLayoutContents = useMemo(() => {
|
const tileLayoutContents = useMemo(() => {
|
||||||
const renderBlock: ContentRenderer = (
|
const renderBlock: ContentRenderer = (nodeModel: NodeModel) => {
|
||||||
blockData: TabLayoutData,
|
return <Block key={nodeModel.blockId} nodeModel={nodeModel} preview={false} />;
|
||||||
ready: boolean,
|
|
||||||
isMagnified: boolean,
|
|
||||||
disablePointerEvents: boolean,
|
|
||||||
onMagnifyToggle: () => void,
|
|
||||||
onClose: () => void,
|
|
||||||
dragHandleRef: React.RefObject<HTMLDivElement>
|
|
||||||
) => {
|
|
||||||
if (!blockData.blockId || !ready) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const layoutModel: LayoutComponentModel = {
|
|
||||||
disablePointerEvents,
|
|
||||||
onClose,
|
|
||||||
onMagnifyToggle,
|
|
||||||
dragHandleRef,
|
|
||||||
isMagnified,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Block key={blockData.blockId} blockId={blockData.blockId} layoutModel={layoutModel} preview={false} />
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderPreview(tabData: TabLayoutData) {
|
const renderPreview: PreviewRenderer = (nodeModel: NodeModel) => {
|
||||||
if (!tabData) return;
|
return <Block key={nodeModel.blockId} nodeModel={nodeModel} preview={true} />;
|
||||||
return <Block key={tabData.blockId} blockId={tabData.blockId} layoutModel={null} preview={true} />;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
function onNodeDelete(data: TabLayoutData) {
|
function onNodeDelete(data: TabLayoutData) {
|
||||||
return services.ObjectService.DeleteBlock(data.blockId);
|
return services.ObjectService.DeleteBlock(data.blockId);
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import * as services from "@/store/services";
|
|||||||
import * as keyutil from "@/util/keyutil";
|
import * as keyutil from "@/util/keyutil";
|
||||||
import * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { produce } from "immer";
|
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import "public/xterm.css";
|
import "public/xterm.css";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@@ -105,17 +104,6 @@ const testVDom: VDomElem = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
function setBlockFocus(blockId: string) {
|
|
||||||
let winData = globalStore.get(atoms.waveWindow);
|
|
||||||
if (winData == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
winData = produce(winData, (draft) => {
|
|
||||||
draft.activeblockid = blockId;
|
|
||||||
});
|
|
||||||
WOS.setObjectValue(winData, globalStore.set, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
class TermViewModel {
|
class TermViewModel {
|
||||||
viewType: string;
|
viewType: string;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
@@ -256,17 +244,10 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
|
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
|
||||||
model.htmlElemFocusRef = htmlElemFocusRef;
|
model.htmlElemFocusRef = htmlElemFocusRef;
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||||
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
|
|
||||||
return jotai.atom((get) => {
|
|
||||||
const winData = get(atoms.waveWindow);
|
|
||||||
return winData?.activeblockid === blockId;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const termSettingsAtom = useSettingsAtom<TerminalConfigType>("term", (settings: SettingsConfigType) => {
|
const termSettingsAtom = useSettingsAtom<TerminalConfigType>("term", (settings: SettingsConfigType) => {
|
||||||
return settings?.term;
|
return settings?.term;
|
||||||
});
|
});
|
||||||
const termSettings = jotai.useAtomValue(termSettingsAtom);
|
const termSettings = jotai.useAtomValue(termSettingsAtom);
|
||||||
const isFocused = jotai.useAtomValue(isFocusedAtom);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function handleTerminalKeydown(event: KeyboardEvent): boolean {
|
function handleTerminalKeydown(event: KeyboardEvent): boolean {
|
||||||
@@ -323,9 +304,6 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
);
|
);
|
||||||
(window as any).term = termWrap;
|
(window as any).term = termWrap;
|
||||||
termRef.current = termWrap;
|
termRef.current = termWrap;
|
||||||
termWrap.addFocusListener(() => {
|
|
||||||
setBlockFocus(blockId);
|
|
||||||
});
|
|
||||||
const rszObs = new ResizeObserver(() => {
|
const rszObs = new ResizeObserver(() => {
|
||||||
termWrap.handleResize_debounced();
|
termWrap.handleResize_debounced();
|
||||||
});
|
});
|
||||||
@@ -358,16 +336,6 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
termMode = "term";
|
termMode = "term";
|
||||||
}
|
}
|
||||||
|
|
||||||
// set initial focus
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (isFocused && termMode == "term") {
|
|
||||||
termRef.current?.terminal.focus();
|
|
||||||
}
|
|
||||||
if (isFocused && termMode == "html") {
|
|
||||||
htmlElemFocusRef.current?.focus();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// set intitial controller status, and then subscribe for updates
|
// set intitial controller status, and then subscribe for updates
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function updateShellProcStatus(status: string) {
|
function updateShellProcStatus(status: string) {
|
||||||
@@ -455,11 +423,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={clsx("view-term", "term-mode-" + termMode)} onKeyDown={handleKeyDown} ref={viewRef}>
|
||||||
className={clsx("view-term", "term-mode-" + termMode, isFocused ? "is-focused" : null)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
ref={viewRef}
|
|
||||||
>
|
|
||||||
{typeAhead[blockId] && (
|
{typeAhead[blockId] && (
|
||||||
<TypeAheadModal
|
<TypeAheadModal
|
||||||
anchor={viewRef}
|
anchor={viewRef}
|
||||||
@@ -483,7 +447,6 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
if (htmlElemFocusRef.current != null) {
|
if (htmlElemFocusRef.current != null) {
|
||||||
htmlElemFocusRef.current.focus();
|
htmlElemFocusRef.current.focus();
|
||||||
}
|
}
|
||||||
setBlockFocus(blockId);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div key="htmlElemFocus" className="term-htmlelem-focus">
|
<div key="htmlElemFocus" className="term-htmlelem-focus">
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { TileLayout } from "./lib/TileLayout";
|
|||||||
import { LayoutModel } from "./lib/layoutModel";
|
import { LayoutModel } from "./lib/layoutModel";
|
||||||
import {
|
import {
|
||||||
deleteLayoutModelForTab,
|
deleteLayoutModelForTab,
|
||||||
|
getLayoutModelForActiveTab,
|
||||||
getLayoutModelForTab,
|
getLayoutModelForTab,
|
||||||
getLayoutModelForTabById,
|
getLayoutModelForTabById,
|
||||||
useLayoutModel,
|
useLayoutModel,
|
||||||
useLayoutNode,
|
|
||||||
} from "./lib/layoutModelHooks";
|
} from "./lib/layoutModelHooks";
|
||||||
import { newLayoutNode } from "./lib/layoutNode";
|
import { newLayoutNode } from "./lib/layoutNode";
|
||||||
import type {
|
import type {
|
||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
LayoutTreeCommitPendingAction,
|
LayoutTreeCommitPendingAction,
|
||||||
LayoutTreeComputeMoveNodeAction,
|
LayoutTreeComputeMoveNodeAction,
|
||||||
LayoutTreeDeleteNodeAction,
|
LayoutTreeDeleteNodeAction,
|
||||||
|
LayoutTreeFocusNodeAction,
|
||||||
LayoutTreeInsertNodeAction,
|
LayoutTreeInsertNodeAction,
|
||||||
LayoutTreeInsertNodeAtIndexAction,
|
LayoutTreeInsertNodeAtIndexAction,
|
||||||
LayoutTreeMagnifyNodeToggleAction,
|
LayoutTreeMagnifyNodeToggleAction,
|
||||||
@@ -27,12 +28,15 @@ import type {
|
|||||||
LayoutTreeSetPendingAction,
|
LayoutTreeSetPendingAction,
|
||||||
LayoutTreeStateSetter,
|
LayoutTreeStateSetter,
|
||||||
LayoutTreeSwapNodeAction,
|
LayoutTreeSwapNodeAction,
|
||||||
|
NodeModel,
|
||||||
|
PreviewRenderer,
|
||||||
} from "./lib/types";
|
} from "./lib/types";
|
||||||
import { DropDirection, LayoutTreeActionType, NavigateDirection } from "./lib/types";
|
import { DropDirection, LayoutTreeActionType, NavigateDirection } from "./lib/types";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
deleteLayoutModelForTab,
|
deleteLayoutModelForTab,
|
||||||
DropDirection,
|
DropDirection,
|
||||||
|
getLayoutModelForActiveTab,
|
||||||
getLayoutModelForTab,
|
getLayoutModelForTab,
|
||||||
getLayoutModelForTabById,
|
getLayoutModelForTabById,
|
||||||
LayoutModel,
|
LayoutModel,
|
||||||
@@ -41,7 +45,6 @@ export {
|
|||||||
newLayoutNode,
|
newLayoutNode,
|
||||||
TileLayout,
|
TileLayout,
|
||||||
useLayoutModel,
|
useLayoutModel,
|
||||||
useLayoutNode,
|
|
||||||
};
|
};
|
||||||
export type {
|
export type {
|
||||||
ContentRenderer,
|
ContentRenderer,
|
||||||
@@ -51,6 +54,7 @@ export type {
|
|||||||
LayoutTreeCommitPendingAction,
|
LayoutTreeCommitPendingAction,
|
||||||
LayoutTreeComputeMoveNodeAction,
|
LayoutTreeComputeMoveNodeAction,
|
||||||
LayoutTreeDeleteNodeAction,
|
LayoutTreeDeleteNodeAction,
|
||||||
|
LayoutTreeFocusNodeAction,
|
||||||
LayoutTreeInsertNodeAction,
|
LayoutTreeInsertNodeAction,
|
||||||
LayoutTreeInsertNodeAtIndexAction,
|
LayoutTreeInsertNodeAtIndexAction,
|
||||||
LayoutTreeMagnifyNodeToggleAction,
|
LayoutTreeMagnifyNodeToggleAction,
|
||||||
@@ -59,4 +63,6 @@ export type {
|
|||||||
LayoutTreeSetPendingAction,
|
LayoutTreeSetPendingAction,
|
||||||
LayoutTreeStateSetter,
|
LayoutTreeStateSetter,
|
||||||
LayoutTreeSwapNodeAction,
|
LayoutTreeSwapNodeAction,
|
||||||
|
NodeModel,
|
||||||
|
PreviewRenderer,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { DropTargetMonitor, XYCoord, useDrag, useDragLayer, useDrop } from "reac
|
|||||||
import { debounce, throttle } from "throttle-debounce";
|
import { debounce, throttle } from "throttle-debounce";
|
||||||
import { useDevicePixelRatio } from "use-device-pixel-ratio";
|
import { useDevicePixelRatio } from "use-device-pixel-ratio";
|
||||||
import { LayoutModel } from "./layoutModel";
|
import { LayoutModel } from "./layoutModel";
|
||||||
import { useLayoutNode, useTileLayout } from "./layoutModelHooks";
|
import { useNodeModel, useTileLayout } from "./layoutModelHooks";
|
||||||
import "./tilelayout.less";
|
import "./tilelayout.less";
|
||||||
import {
|
import {
|
||||||
LayoutNode,
|
LayoutNode,
|
||||||
@@ -53,7 +53,6 @@ const DragPreviewHeight = 300;
|
|||||||
|
|
||||||
function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutProps) {
|
function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutProps) {
|
||||||
const layoutModel = useTileLayout(tabAtom, contents);
|
const layoutModel = useTileLayout(tabAtom, contents);
|
||||||
const generation = useAtomValue(layoutModel.generationAtom);
|
|
||||||
const overlayTransform = useAtomValue(layoutModel.overlayTransform);
|
const overlayTransform = useAtomValue(layoutModel.overlayTransform);
|
||||||
const setActiveDrag = useSetAtom(layoutModel.activeDrag);
|
const setActiveDrag = useSetAtom(layoutModel.activeDrag);
|
||||||
const setReady = useSetAtom(layoutModel.ready);
|
const setReady = useSetAtom(layoutModel.ready);
|
||||||
@@ -85,7 +84,7 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
[getCursorPoint, generation]
|
[getCursorPoint]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Effect to detect when the cursor leaves the TileLayout hit trap so we can remove any placeholders. This cannot be done using pointer capture
|
// Effect to detect when the cursor leaves the TileLayout hit trap so we can remove any placeholders. This cannot be done using pointer capture
|
||||||
@@ -115,7 +114,7 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
|
|||||||
>
|
>
|
||||||
<div key="display" ref={layoutModel.displayContainerRef} className="display-container">
|
<div key="display" ref={layoutModel.displayContainerRef} className="display-container">
|
||||||
<ResizeHandleWrapper layoutModel={layoutModel} />
|
<ResizeHandleWrapper layoutModel={layoutModel} />
|
||||||
<DisplayNodesWrapper contents={contents} layoutModel={layoutModel} />
|
<DisplayNodesWrapper layoutModel={layoutModel} />
|
||||||
</div>
|
</div>
|
||||||
<Placeholder key="placeholder" layoutModel={layoutModel} style={{ top: 10000, ...overlayTransform }} />
|
<Placeholder key="placeholder" layoutModel={layoutModel} style={{ top: 10000, ...overlayTransform }} />
|
||||||
<OverlayNodeWrapper layoutModel={layoutModel} />
|
<OverlayNodeWrapper layoutModel={layoutModel} />
|
||||||
@@ -131,19 +130,15 @@ interface DisplayNodesWrapperProps {
|
|||||||
* The layout tree state.
|
* The layout tree state.
|
||||||
*/
|
*/
|
||||||
layoutModel: LayoutModel;
|
layoutModel: LayoutModel;
|
||||||
/**
|
|
||||||
* contains callbacks and information about the contents (or styling) of of the TileLayout
|
|
||||||
*/
|
|
||||||
contents: TileLayoutContents;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DisplayNodesWrapper = ({ layoutModel, contents }: DisplayNodesWrapperProps) => {
|
const DisplayNodesWrapper = ({ layoutModel }: DisplayNodesWrapperProps) => {
|
||||||
const leafs = useAtomValue(layoutModel.leafs);
|
const leafs = useAtomValue(layoutModel.leafs);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
leafs.map((leaf) => {
|
leafs.map((node) => {
|
||||||
return <DisplayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} contents={contents} />;
|
return <DisplayNode key={node.id} layoutModel={layoutModel} node={node} />;
|
||||||
}),
|
}),
|
||||||
[leafs]
|
[leafs]
|
||||||
);
|
);
|
||||||
@@ -154,12 +149,7 @@ interface DisplayNodeProps {
|
|||||||
/**
|
/**
|
||||||
* The leaf node object, containing the data needed to display the leaf contents to the user.
|
* The leaf node object, containing the data needed to display the leaf contents to the user.
|
||||||
*/
|
*/
|
||||||
layoutNode: LayoutNode;
|
node: LayoutNode;
|
||||||
|
|
||||||
/**
|
|
||||||
* contains callbacks and information about the contents (or styling) of of the TileLayout
|
|
||||||
*/
|
|
||||||
contents: TileLayoutContents;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dragItemType = "TILE_ITEM";
|
const dragItemType = "TILE_ITEM";
|
||||||
@@ -167,26 +157,23 @@ const dragItemType = "TILE_ITEM";
|
|||||||
/**
|
/**
|
||||||
* The draggable and displayable portion of a leaf node in a layout tree.
|
* The draggable and displayable portion of a leaf node in a layout tree.
|
||||||
*/
|
*/
|
||||||
const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) => {
|
const DisplayNode = ({ layoutModel, node }: DisplayNodeProps) => {
|
||||||
|
const nodeModel = useNodeModel(layoutModel, node);
|
||||||
const tileNodeRef = useRef<HTMLDivElement>(null);
|
const tileNodeRef = useRef<HTMLDivElement>(null);
|
||||||
const dragHandleRef = useRef<HTMLDivElement>(null);
|
|
||||||
const previewRef = useRef<HTMLDivElement>(null);
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
const addlProps = useLayoutNode(layoutModel, layoutNode);
|
const addlProps = useAtomValue(nodeModel.additionalProps);
|
||||||
const activeDrag = useAtomValue(layoutModel.activeDrag);
|
|
||||||
const globalReady = useAtomValue(layoutModel.ready);
|
|
||||||
|
|
||||||
const devicePixelRatio = useDevicePixelRatio();
|
const devicePixelRatio = useDevicePixelRatio();
|
||||||
|
|
||||||
const [{ isDragging }, drag, dragPreview] = useDrag(
|
const [{ isDragging }, drag, dragPreview] = useDrag(
|
||||||
() => ({
|
() => ({
|
||||||
type: dragItemType,
|
type: dragItemType,
|
||||||
item: () => layoutNode,
|
item: () => node,
|
||||||
canDrag: () => !addlProps?.isMagnifiedNode,
|
canDrag: () => !addlProps?.isMagnifiedNode,
|
||||||
collect: (monitor) => ({
|
collect: (monitor) => ({
|
||||||
isDragging: monitor.isDragging(),
|
isDragging: monitor.isDragging(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
[layoutNode, addlProps]
|
[node, addlProps]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [previewElementGeneration, setPreviewElementGeneration] = useState(0);
|
const [previewElementGeneration, setPreviewElementGeneration] = useState(0);
|
||||||
@@ -203,11 +190,11 @@ const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) =>
|
|||||||
transform: `scale(${1 / devicePixelRatio})`,
|
transform: `scale(${1 / devicePixelRatio})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{contents.renderPreview?.(layoutNode.data)}
|
{layoutModel.renderPreview?.(nodeModel)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [contents.renderPreview, devicePixelRatio, layoutNode.data]);
|
}, [devicePixelRatio, nodeModel]);
|
||||||
|
|
||||||
const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null);
|
const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null);
|
||||||
const [previewImageGeneration, setPreviewImageGeneration] = useState(0);
|
const [previewImageGeneration, setPreviewImageGeneration] = useState(0);
|
||||||
@@ -232,31 +219,20 @@ const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) =>
|
|||||||
previewImageGeneration,
|
previewImageGeneration,
|
||||||
previewImage,
|
previewImage,
|
||||||
devicePixelRatio,
|
devicePixelRatio,
|
||||||
layoutNode.data,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Register the display node as a draggable item
|
|
||||||
useEffect(() => {
|
|
||||||
drag(dragHandleRef);
|
|
||||||
}, [drag, dragHandleRef.current]);
|
|
||||||
|
|
||||||
const leafContent = useMemo(() => {
|
const leafContent = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
layoutNode.data && (
|
<div key="leaf" className="tile-leaf">
|
||||||
<div key="leaf" className="tile-leaf">
|
{layoutModel.renderContent(nodeModel)}
|
||||||
{contents.renderContent(
|
</div>
|
||||||
layoutNode.data,
|
|
||||||
globalReady,
|
|
||||||
addlProps?.isMagnifiedNode ?? false,
|
|
||||||
activeDrag,
|
|
||||||
() => layoutModel.magnifyNodeToggle(layoutNode),
|
|
||||||
() => layoutModel.closeNode(layoutNode),
|
|
||||||
dragHandleRef
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}, [layoutNode, globalReady, activeDrag, addlProps]);
|
}, [nodeModel]);
|
||||||
|
|
||||||
|
// Register the display node as a draggable item
|
||||||
|
useEffect(() => {
|
||||||
|
drag(nodeModel.dragHandleRef);
|
||||||
|
}, [drag, nodeModel.dragHandleRef.current]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -266,7 +242,7 @@ const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) =>
|
|||||||
"last-magnified": addlProps?.isLastMagnifiedNode,
|
"last-magnified": addlProps?.isLastMagnifiedNode,
|
||||||
})}
|
})}
|
||||||
ref={tileNodeRef}
|
ref={tileNodeRef}
|
||||||
id={layoutNode.id}
|
id={node.id}
|
||||||
style={addlProps?.transform}
|
style={addlProps?.transform}
|
||||||
onPointerEnter={generatePreviewImage}
|
onPointerEnter={generatePreviewImage}
|
||||||
onPointerOver={(event) => event.stopPropagation()}
|
onPointerOver={(event) => event.stopPropagation()}
|
||||||
@@ -281,14 +257,14 @@ interface OverlayNodeWrapperProps {
|
|||||||
layoutModel: LayoutModel;
|
layoutModel: LayoutModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OverlayNodeWrapper = ({ layoutModel }: OverlayNodeWrapperProps) => {
|
const OverlayNodeWrapper = memo(({ layoutModel }: OverlayNodeWrapperProps) => {
|
||||||
const leafs = useAtomValue(layoutModel.leafs);
|
const leafs = useAtomValue(layoutModel.leafs);
|
||||||
const overlayTransform = useAtomValue(layoutModel.overlayTransform);
|
const overlayTransform = useAtomValue(layoutModel.overlayTransform);
|
||||||
|
|
||||||
const overlayNodes = useMemo(
|
const overlayNodes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
leafs.map((leaf) => {
|
leafs.map((node) => {
|
||||||
return <OverlayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} />;
|
return <OverlayNode key={node.id} layoutModel={layoutModel} node={node} />;
|
||||||
}),
|
}),
|
||||||
[leafs]
|
[leafs]
|
||||||
);
|
);
|
||||||
@@ -298,34 +274,31 @@ const OverlayNodeWrapper = ({ layoutModel }: OverlayNodeWrapperProps) => {
|
|||||||
{overlayNodes}
|
{overlayNodes}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
interface OverlayNodeProps {
|
interface OverlayNodeProps {
|
||||||
/**
|
|
||||||
* The layout node object corresponding to the OverlayNode.
|
|
||||||
*/
|
|
||||||
layoutNode: LayoutNode;
|
|
||||||
/**
|
/**
|
||||||
* The layout tree state.
|
* The layout tree state.
|
||||||
*/
|
*/
|
||||||
layoutModel: LayoutModel;
|
layoutModel: LayoutModel;
|
||||||
|
node: LayoutNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An overlay representing the true flexbox layout of the LayoutTreeState. This holds the drop targets for moving around nodes and is used to calculate the
|
* An overlay representing the true flexbox layout of the LayoutTreeState. This holds the drop targets for moving around nodes and is used to calculate the
|
||||||
* dimensions of the corresponding DisplayNode for each LayoutTreeState leaf.
|
* dimensions of the corresponding DisplayNode for each LayoutTreeState leaf.
|
||||||
*/
|
*/
|
||||||
const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
|
const OverlayNode = memo(({ node, layoutModel }: OverlayNodeProps) => {
|
||||||
const additionalProps = useLayoutNode(layoutModel, layoutNode);
|
const nodeModel = useNodeModel(layoutModel, node);
|
||||||
|
const additionalProps = useAtomValue(nodeModel.additionalProps);
|
||||||
const overlayRef = useRef<HTMLDivElement>(null);
|
const overlayRef = useRef<HTMLDivElement>(null);
|
||||||
const generation = useAtomValue(layoutModel.generationAtom);
|
|
||||||
|
|
||||||
const [, drop] = useDrop(
|
const [, drop] = useDrop(
|
||||||
() => ({
|
() => ({
|
||||||
accept: dragItemType,
|
accept: dragItemType,
|
||||||
canDrop: (_, monitor) => {
|
canDrop: (_, monitor) => {
|
||||||
const dragItem = monitor.getItem<LayoutNode>();
|
const dragItem = monitor.getItem<LayoutNode>();
|
||||||
if (monitor.isOver({ shallow: true }) && dragItem?.id !== layoutNode.id) {
|
if (monitor.isOver({ shallow: true }) && dragItem?.id !== node.id) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -346,7 +319,7 @@ const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
|
|||||||
offset.y -= containerRect.y;
|
offset.y -= containerRect.y;
|
||||||
layoutModel.treeReducer({
|
layoutModel.treeReducer({
|
||||||
type: LayoutTreeActionType.ComputeMove,
|
type: LayoutTreeActionType.ComputeMove,
|
||||||
node: layoutNode,
|
node: node,
|
||||||
nodeToMove: dragItem,
|
nodeToMove: dragItem,
|
||||||
direction: determineDropDirection(additionalProps.rect, offset),
|
direction: determineDropDirection(additionalProps.rect, offset),
|
||||||
} as LayoutTreeComputeMoveNodeAction);
|
} as LayoutTreeComputeMoveNodeAction);
|
||||||
@@ -358,7 +331,7 @@ const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
[layoutNode, generation, additionalProps, layoutModel.displayContainerRef]
|
[node, additionalProps, layoutModel.displayContainerRef]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Register the overlay node as a drop target
|
// Register the overlay node as a drop target
|
||||||
@@ -366,27 +339,27 @@ const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
|
|||||||
drop(overlayRef);
|
drop(overlayRef);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <div ref={overlayRef} className="overlay-node" id={layoutNode.id} style={additionalProps?.transform} />;
|
return <div ref={overlayRef} className="overlay-node" id={node.id} style={additionalProps?.transform} />;
|
||||||
};
|
});
|
||||||
|
|
||||||
interface ResizeHandleWrapperProps {
|
interface ResizeHandleWrapperProps {
|
||||||
layoutModel: LayoutModel;
|
layoutModel: LayoutModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ResizeHandleWrapper = ({ layoutModel }: ResizeHandleWrapperProps) => {
|
const ResizeHandleWrapper = memo(({ layoutModel }: ResizeHandleWrapperProps) => {
|
||||||
const resizeHandles = useAtomValue(layoutModel.resizeHandles) as Atom<ResizeHandleProps>[];
|
const resizeHandles = useAtomValue(layoutModel.resizeHandles) as Atom<ResizeHandleProps>[];
|
||||||
|
|
||||||
return resizeHandles.map((resizeHandleAtom, i) => (
|
return resizeHandles.map((resizeHandleAtom, i) => (
|
||||||
<ResizeHandle key={`resize-handle-${i}`} layoutModel={layoutModel} resizeHandleAtom={resizeHandleAtom} />
|
<ResizeHandle key={`resize-handle-${i}`} layoutModel={layoutModel} resizeHandleAtom={resizeHandleAtom} />
|
||||||
));
|
));
|
||||||
};
|
});
|
||||||
|
|
||||||
interface ResizeHandleComponentProps {
|
interface ResizeHandleComponentProps {
|
||||||
resizeHandleAtom: Atom<ResizeHandleProps>;
|
resizeHandleAtom: Atom<ResizeHandleProps>;
|
||||||
layoutModel: LayoutModel;
|
layoutModel: LayoutModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ResizeHandle = ({ resizeHandleAtom, layoutModel }: ResizeHandleComponentProps) => {
|
const ResizeHandle = memo(({ resizeHandleAtom, layoutModel }: ResizeHandleComponentProps) => {
|
||||||
const resizeHandleProps = useAtomValue(resizeHandleAtom);
|
const resizeHandleProps = useAtomValue(resizeHandleAtom);
|
||||||
const resizeHandleRef = useRef<HTMLDivElement>(null);
|
const resizeHandleRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -436,7 +409,7 @@ const ResizeHandle = ({ resizeHandleAtom, layoutModel }: ResizeHandleComponentPr
|
|||||||
<div className="line" />
|
<div className="line" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
interface PlaceholderProps {
|
interface PlaceholderProps {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ function getLayoutStateAtomFromTab(tabAtom: Atom<Tab>, get: Getter): WritableWav
|
|||||||
|
|
||||||
export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayoutTreeStateAtom {
|
export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayoutTreeStateAtom {
|
||||||
if (layoutStateAtomMap.has(tabAtom)) {
|
if (layoutStateAtomMap.has(tabAtom)) {
|
||||||
// console.log("found atom");
|
|
||||||
return layoutStateAtomMap.get(tabAtom);
|
return layoutStateAtomMap.get(tabAtom);
|
||||||
}
|
}
|
||||||
const generationAtom = atom(1);
|
const generationAtom = atom(1);
|
||||||
@@ -26,9 +25,9 @@ export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayo
|
|||||||
const stateAtom = getLayoutStateAtomFromTab(tabAtom, get);
|
const stateAtom = getLayoutStateAtomFromTab(tabAtom, get);
|
||||||
if (!stateAtom) return;
|
if (!stateAtom) return;
|
||||||
const layoutStateData = get(stateAtom);
|
const layoutStateData = get(stateAtom);
|
||||||
// console.log("layoutStateData", layoutStateData);
|
|
||||||
const layoutTreeState: LayoutTreeState = {
|
const layoutTreeState: LayoutTreeState = {
|
||||||
rootNode: layoutStateData?.rootnode,
|
rootNode: layoutStateData?.rootnode,
|
||||||
|
focusedNodeId: layoutStateData?.focusednodeid,
|
||||||
magnifiedNodeId: layoutStateData?.magnifiednodeid,
|
magnifiedNodeId: layoutStateData?.magnifiednodeid,
|
||||||
generation: get(generationAtom),
|
generation: get(generationAtom),
|
||||||
};
|
};
|
||||||
@@ -37,12 +36,11 @@ export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayo
|
|||||||
(get, set, value) => {
|
(get, set, value) => {
|
||||||
if (get(generationAtom) < value.generation) {
|
if (get(generationAtom) < value.generation) {
|
||||||
const stateAtom = getLayoutStateAtomFromTab(tabAtom, get);
|
const stateAtom = getLayoutStateAtomFromTab(tabAtom, get);
|
||||||
// console.log("setting new atom val", value);
|
|
||||||
if (!stateAtom) return;
|
if (!stateAtom) return;
|
||||||
const waveObjVal = get(stateAtom);
|
const waveObjVal = get(stateAtom);
|
||||||
// console.log("waveObjVal", waveObjVal);
|
|
||||||
waveObjVal.rootnode = value.rootNode;
|
waveObjVal.rootnode = value.rootNode;
|
||||||
waveObjVal.magnifiednodeid = value.magnifiedNodeId;
|
waveObjVal.magnifiednodeid = value.magnifiedNodeId;
|
||||||
|
waveObjVal.focusednodeid = value.focusedNodeId;
|
||||||
set(generationAtom, value.generation);
|
set(generationAtom, value.generation);
|
||||||
set(stateAtom, waveObjVal);
|
set(stateAtom, waveObjVal);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { globalStore, WOS } from "@/app/store/global";
|
import { atoms, globalStore, WOS } from "@/app/store/global";
|
||||||
import useResizeObserver from "@react-hook/resize-observer";
|
import useResizeObserver from "@react-hook/resize-observer";
|
||||||
import { Atom, useAtomValue } from "jotai";
|
import { Atom, useAtomValue } from "jotai";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { withLayoutTreeStateAtomFromTab } from "./layoutAtom";
|
import { withLayoutTreeStateAtomFromTab } from "./layoutAtom";
|
||||||
import { LayoutModel } from "./layoutModel";
|
import { LayoutModel } from "./layoutModel";
|
||||||
import { LayoutNode, LayoutNodeAdditionalProps, TileLayoutContents } from "./types";
|
import { LayoutNode, NodeModel, TileLayoutContents } from "./types";
|
||||||
|
|
||||||
const layoutModelMap: Map<string, LayoutModel> = new Map();
|
const layoutModelMap: Map<string, LayoutModel> = new Map();
|
||||||
|
|
||||||
@@ -34,6 +34,11 @@ export function getLayoutModelForTabById(tabId: string) {
|
|||||||
return getLayoutModelForTab(tabAtom);
|
return getLayoutModelForTab(tabAtom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLayoutModelForActiveTab() {
|
||||||
|
const tabId = globalStore.get(atoms.activeTabId);
|
||||||
|
return getLayoutModelForTabById(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteLayoutModelForTab(tabId: string) {
|
export function deleteLayoutModelForTab(tabId: string) {
|
||||||
if (layoutModelMap.has(tabId)) layoutModelMap.delete(tabId);
|
if (layoutModelMap.has(tabId)) layoutModelMap.delete(tabId);
|
||||||
}
|
}
|
||||||
@@ -51,8 +56,6 @@ export function useTileLayout(tabAtom: Atom<Tab>, tileContent: TileLayoutContent
|
|||||||
return layoutModel;
|
return layoutModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLayoutNode(layoutModel: LayoutModel, layoutNode: LayoutNode): LayoutNodeAdditionalProps {
|
export function useNodeModel(layoutModel: LayoutModel, layoutNode: LayoutNode): NodeModel {
|
||||||
const [addlPropsAtom] = useState(layoutModel.getNodeAdditionalPropertiesAtom(layoutNode.id));
|
return layoutModel.getNodeModel(layoutNode);
|
||||||
const addlProps = useAtomValue(addlPropsAtom);
|
|
||||||
return addlProps;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
LayoutTreeActionType,
|
LayoutTreeActionType,
|
||||||
LayoutTreeComputeMoveNodeAction,
|
LayoutTreeComputeMoveNodeAction,
|
||||||
LayoutTreeDeleteNodeAction,
|
LayoutTreeDeleteNodeAction,
|
||||||
|
LayoutTreeFocusNodeAction,
|
||||||
LayoutTreeInsertNodeAction,
|
LayoutTreeInsertNodeAction,
|
||||||
LayoutTreeInsertNodeAtIndexAction,
|
LayoutTreeInsertNodeAtIndexAction,
|
||||||
LayoutTreeMagnifyNodeToggleAction,
|
LayoutTreeMagnifyNodeToggleAction,
|
||||||
@@ -37,7 +38,6 @@ import {
|
|||||||
export function computeMoveNode(layoutState: LayoutTreeState, computeInsertAction: LayoutTreeComputeMoveNodeAction) {
|
export function computeMoveNode(layoutState: LayoutTreeState, computeInsertAction: LayoutTreeComputeMoveNodeAction) {
|
||||||
const rootNode = layoutState.rootNode;
|
const rootNode = layoutState.rootNode;
|
||||||
const { node, nodeToMove, direction } = computeInsertAction;
|
const { node, nodeToMove, direction } = computeInsertAction;
|
||||||
// console.log("computeInsertOperation start", layoutState.rootNode, node, nodeToMove, direction);
|
|
||||||
if (direction === undefined) {
|
if (direction === undefined) {
|
||||||
console.warn("No direction provided for insertItemInDirection");
|
console.warn("No direction provided for insertItemInDirection");
|
||||||
return;
|
return;
|
||||||
@@ -179,14 +179,12 @@ export function computeMoveNode(layoutState: LayoutTreeState, computeInsertActio
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case DropDirection.Center:
|
case DropDirection.Center:
|
||||||
// console.log("center drop", rootNode, node, nodeToMove);
|
|
||||||
if (node.id !== rootNode.id && nodeToMove.id !== rootNode.id) {
|
if (node.id !== rootNode.id && nodeToMove.id !== rootNode.id) {
|
||||||
const swapAction: LayoutTreeSwapNodeAction = {
|
const swapAction: LayoutTreeSwapNodeAction = {
|
||||||
type: LayoutTreeActionType.Swap,
|
type: LayoutTreeActionType.Swap,
|
||||||
node1Id: node.id,
|
node1Id: node.id,
|
||||||
node2Id: nodeToMove.id,
|
node2Id: nodeToMove.id,
|
||||||
};
|
};
|
||||||
// console.log("swapAction", swapAction);
|
|
||||||
return swapAction;
|
return swapAction;
|
||||||
} else {
|
} else {
|
||||||
console.warn("cannot swap");
|
console.warn("cannot swap");
|
||||||
@@ -209,7 +207,6 @@ export function computeMoveNode(layoutState: LayoutTreeState, computeInsertActio
|
|||||||
|
|
||||||
export function moveNode(layoutState: LayoutTreeState, action: LayoutTreeMoveNodeAction) {
|
export function moveNode(layoutState: LayoutTreeState, action: LayoutTreeMoveNodeAction) {
|
||||||
const rootNode = layoutState.rootNode;
|
const rootNode = layoutState.rootNode;
|
||||||
// console.log("moveNode", action, layoutState.rootNode);
|
|
||||||
if (!action) {
|
if (!action) {
|
||||||
console.error("no move node action provided");
|
console.error("no move node action provided");
|
||||||
return;
|
return;
|
||||||
@@ -223,8 +220,6 @@ export function moveNode(layoutState: LayoutTreeState, action: LayoutTreeMoveNod
|
|||||||
const parent = findNode(rootNode, action.parentId);
|
const parent = findNode(rootNode, action.parentId);
|
||||||
const oldParent = findParent(rootNode, action.node.id);
|
const oldParent = findParent(rootNode, action.node.id);
|
||||||
|
|
||||||
// console.log(node, parent, oldParent);
|
|
||||||
|
|
||||||
let startingIndex = 0;
|
let startingIndex = 0;
|
||||||
|
|
||||||
// If moving under the same parent, we need to make sure that we are removing the child from its old position, not its new one.
|
// If moving under the same parent, we need to make sure that we are removing the child from its old position, not its new one.
|
||||||
@@ -256,6 +251,7 @@ export function moveNode(layoutState: LayoutTreeState, action: LayoutTreeMoveNod
|
|||||||
if (oldParent) {
|
if (oldParent) {
|
||||||
removeChild(oldParent, node, startingIndex);
|
removeChild(oldParent, node, startingIndex);
|
||||||
}
|
}
|
||||||
|
layoutState.generation++;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertNode(layoutState: LayoutTreeState, action: LayoutTreeInsertNodeAction) {
|
export function insertNode(layoutState: LayoutTreeState, action: LayoutTreeInsertNodeAction) {
|
||||||
@@ -265,13 +261,15 @@ export function insertNode(layoutState: LayoutTreeState, action: LayoutTreeInser
|
|||||||
}
|
}
|
||||||
if (!layoutState.rootNode) {
|
if (!layoutState.rootNode) {
|
||||||
layoutState.rootNode = action.node;
|
layoutState.rootNode = action.node;
|
||||||
return;
|
} else {
|
||||||
}
|
const insertLoc = findNextInsertLocation(layoutState.rootNode, 5);
|
||||||
const insertLoc = findNextInsertLocation(layoutState.rootNode, 5);
|
addChildAt(insertLoc.node, insertLoc.index, action.node);
|
||||||
addChildAt(insertLoc.node, insertLoc.index, action.node);
|
if (action.magnified) {
|
||||||
if (action.magnified) {
|
layoutState.magnifiedNodeId = action.node.id;
|
||||||
layoutState.magnifiedNodeId = action.node.id;
|
}
|
||||||
|
layoutState.focusedNodeId = action.node.id;
|
||||||
}
|
}
|
||||||
|
layoutState.generation++;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertNodeAtIndex(layoutState: LayoutTreeState, action: LayoutTreeInsertNodeAtIndexAction) {
|
export function insertNodeAtIndex(layoutState: LayoutTreeState, action: LayoutTreeInsertNodeAtIndexAction) {
|
||||||
@@ -281,22 +279,22 @@ export function insertNodeAtIndex(layoutState: LayoutTreeState, action: LayoutTr
|
|||||||
}
|
}
|
||||||
if (!layoutState.rootNode) {
|
if (!layoutState.rootNode) {
|
||||||
layoutState.rootNode = action.node;
|
layoutState.rootNode = action.node;
|
||||||
return;
|
} else {
|
||||||
}
|
const insertLoc = findInsertLocationFromIndexArr(layoutState.rootNode, action.indexArr);
|
||||||
const insertLoc = findInsertLocationFromIndexArr(layoutState.rootNode, action.indexArr);
|
if (!insertLoc) {
|
||||||
if (!insertLoc) {
|
console.error("insertNodeAtIndex unable to find insert location");
|
||||||
console.error("insertNodeAtIndex unable to find insert location");
|
return;
|
||||||
return;
|
}
|
||||||
}
|
addChildAt(insertLoc.node, insertLoc.index + 1, action.node);
|
||||||
addChildAt(insertLoc.node, insertLoc.index + 1, action.node);
|
if (action.magnified) {
|
||||||
if (action.magnified) {
|
layoutState.magnifiedNodeId = action.node.id;
|
||||||
layoutState.magnifiedNodeId = action.node.id;
|
}
|
||||||
|
layoutState.focusedNodeId = action.node.id;
|
||||||
}
|
}
|
||||||
|
layoutState.generation++;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function swapNode(layoutState: LayoutTreeState, action: LayoutTreeSwapNodeAction) {
|
export function swapNode(layoutState: LayoutTreeState, action: LayoutTreeSwapNodeAction) {
|
||||||
// console.log("swapNode", layoutState, action);
|
|
||||||
|
|
||||||
if (!action.node1Id || !action.node2Id) {
|
if (!action.node1Id || !action.node2Id) {
|
||||||
console.error("invalid swapNode action, both node1 and node2 must be defined");
|
console.error("invalid swapNode action, both node1 and node2 must be defined");
|
||||||
return;
|
return;
|
||||||
@@ -325,10 +323,10 @@ export function swapNode(layoutState: LayoutTreeState, action: LayoutTreeSwapNod
|
|||||||
|
|
||||||
parentNode1.children[parentNode1Index] = node2;
|
parentNode1.children[parentNode1Index] = node2;
|
||||||
parentNode2.children[parentNode2Index] = node1;
|
parentNode2.children[parentNode2Index] = node1;
|
||||||
|
layoutState.generation++;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteNode(layoutState: LayoutTreeState, action: LayoutTreeDeleteNodeAction) {
|
export function deleteNode(layoutState: LayoutTreeState, action: LayoutTreeDeleteNodeAction) {
|
||||||
// console.log("deleteNode", layoutState, action);
|
|
||||||
if (!action?.nodeId) {
|
if (!action?.nodeId) {
|
||||||
console.error("no delete node action provided");
|
console.error("no delete node action provided");
|
||||||
return;
|
return;
|
||||||
@@ -339,20 +337,23 @@ export function deleteNode(layoutState: LayoutTreeState, action: LayoutTreeDelet
|
|||||||
}
|
}
|
||||||
if (layoutState.rootNode.id === action.nodeId) {
|
if (layoutState.rootNode.id === action.nodeId) {
|
||||||
layoutState.rootNode = undefined;
|
layoutState.rootNode = undefined;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const parent = findParent(layoutState.rootNode, action.nodeId);
|
|
||||||
if (parent) {
|
|
||||||
const node = parent.children.find((child) => child.id === action.nodeId);
|
|
||||||
removeChild(parent, node);
|
|
||||||
// console.log("node deleted", parent, node);
|
|
||||||
} else {
|
} else {
|
||||||
console.error("unable to delete node, not found in tree");
|
const parent = findParent(layoutState.rootNode, action.nodeId);
|
||||||
|
if (parent) {
|
||||||
|
const node = parent.children.find((child) => child.id === action.nodeId);
|
||||||
|
removeChild(parent, node);
|
||||||
|
if (layoutState.focusedNodeId === node.id) {
|
||||||
|
layoutState.focusedNodeId = undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("unable to delete node, not found in tree");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layoutState.generation++;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resizeNode(layoutState: LayoutTreeState, action: LayoutTreeResizeNodeAction) {
|
export function resizeNode(layoutState: LayoutTreeState, action: LayoutTreeResizeNodeAction) {
|
||||||
// console.log("resizeNode", layoutState, action);
|
|
||||||
if (!action.resizeOperations) {
|
if (!action.resizeOperations) {
|
||||||
console.error("invalid resizeNode operation. nodeSizes array must be defined.");
|
console.error("invalid resizeNode operation. nodeSizes array must be defined.");
|
||||||
}
|
}
|
||||||
@@ -364,10 +365,20 @@ export function resizeNode(layoutState: LayoutTreeState, action: LayoutTreeResiz
|
|||||||
const node = findNode(layoutState.rootNode, resize.nodeId);
|
const node = findNode(layoutState.rootNode, resize.nodeId);
|
||||||
node.size = resize.size;
|
node.size = resize.size;
|
||||||
}
|
}
|
||||||
|
layoutState.generation++;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function focusNode(layoutState: LayoutTreeState, action: LayoutTreeFocusNodeAction) {
|
||||||
|
if (!action.nodeId) {
|
||||||
|
console.error("invalid focusNode operation, nodeId must be defined.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutState.focusedNodeId = action.nodeId;
|
||||||
|
layoutState.generation++;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function magnifyNodeToggle(layoutState: LayoutTreeState, action: LayoutTreeMagnifyNodeToggleAction) {
|
export function magnifyNodeToggle(layoutState: LayoutTreeState, action: LayoutTreeMagnifyNodeToggleAction) {
|
||||||
// console.log("magnifyNodeToggle", layoutState, action);
|
|
||||||
if (!action.nodeId) {
|
if (!action.nodeId) {
|
||||||
console.error("invalid magnifyNodeToggle operation. nodeId must be defined.");
|
console.error("invalid magnifyNodeToggle operation. nodeId must be defined.");
|
||||||
return;
|
return;
|
||||||
@@ -380,5 +391,7 @@ export function magnifyNodeToggle(layoutState: LayoutTreeState, action: LayoutTr
|
|||||||
layoutState.magnifiedNodeId = undefined;
|
layoutState.magnifiedNodeId = undefined;
|
||||||
} else {
|
} else {
|
||||||
layoutState.magnifiedNodeId = action.nodeId;
|
layoutState.magnifiedNodeId = action.nodeId;
|
||||||
|
layoutState.focusedNodeId = action.nodeId;
|
||||||
}
|
}
|
||||||
|
layoutState.generation++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import { WritableAtom } from "jotai";
|
import { Atom, WritableAtom } from "jotai";
|
||||||
import { CSSProperties } from "react";
|
import { CSSProperties } from "react";
|
||||||
|
|
||||||
export enum NavigateDirection {
|
export enum NavigateDirection {
|
||||||
Top = 0,
|
Up = 0,
|
||||||
Right = 1,
|
Right = 1,
|
||||||
Bottom = 2,
|
Down = 2,
|
||||||
Left = 3,
|
Left = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ export enum LayoutTreeActionType {
|
|||||||
InsertNode = "insert",
|
InsertNode = "insert",
|
||||||
InsertNodeAtIndex = "insertatindex",
|
InsertNodeAtIndex = "insertatindex",
|
||||||
DeleteNode = "delete",
|
DeleteNode = "delete",
|
||||||
|
FocusNode = "focus",
|
||||||
MagnifyNodeToggle = "magnify",
|
MagnifyNodeToggle = "magnify",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +208,18 @@ export interface LayoutTreeResizeNodeAction extends LayoutTreeAction {
|
|||||||
resizeOperations: ResizeNodeOperation[];
|
resizeOperations: ResizeNodeOperation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action for focusing a node from the layout tree.
|
||||||
|
*/
|
||||||
|
export interface LayoutTreeFocusNodeAction extends LayoutTreeAction {
|
||||||
|
type: LayoutTreeActionType.FocusNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the node to focus;
|
||||||
|
*/
|
||||||
|
nodeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action for toggling magnification of a node from the layout tree.
|
* Action for toggling magnification of a node from the layout tree.
|
||||||
*/
|
*/
|
||||||
@@ -234,23 +247,16 @@ export type LayoutTreeStateSetter = (value: LayoutState) => void;
|
|||||||
|
|
||||||
export type LayoutTreeState = {
|
export type LayoutTreeState = {
|
||||||
rootNode: LayoutNode;
|
rootNode: LayoutNode;
|
||||||
|
focusedNodeId?: string;
|
||||||
magnifiedNodeId?: string;
|
magnifiedNodeId?: string;
|
||||||
generation: number;
|
generation: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WritableLayoutTreeStateAtom = WritableAtom<LayoutTreeState, [value: LayoutTreeState], void>;
|
export type WritableLayoutTreeStateAtom = WritableAtom<LayoutTreeState, [value: LayoutTreeState], void>;
|
||||||
|
|
||||||
export type ContentRenderer = (
|
export type ContentRenderer = (nodeModel: NodeModel) => React.ReactNode;
|
||||||
data: TabLayoutData,
|
|
||||||
ready: boolean,
|
|
||||||
isMagnified: boolean,
|
|
||||||
disablePointerEvents: boolean,
|
|
||||||
onMagnifyToggle: () => void,
|
|
||||||
onClose: () => void,
|
|
||||||
dragHandleRef: React.RefObject<HTMLDivElement>
|
|
||||||
) => React.ReactNode;
|
|
||||||
|
|
||||||
export type PreviewRenderer = (data: TabLayoutData) => React.ReactElement;
|
export type PreviewRenderer = (nodeModel: NodeModel) => React.ReactElement;
|
||||||
|
|
||||||
export const DefaultNodeSize = 10;
|
export const DefaultNodeSize = 10;
|
||||||
|
|
||||||
@@ -307,3 +313,18 @@ export interface LayoutNodeAdditionalProps {
|
|||||||
isMagnifiedNode?: boolean;
|
isMagnifiedNode?: boolean;
|
||||||
isLastMagnifiedNode?: boolean;
|
isLastMagnifiedNode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NodeModel {
|
||||||
|
additionalProps: Atom<LayoutNodeAdditionalProps>;
|
||||||
|
blockNum: Atom<number>;
|
||||||
|
nodeId: string;
|
||||||
|
blockId: string;
|
||||||
|
isFocused: Atom<boolean>;
|
||||||
|
isMagnified: Atom<boolean>;
|
||||||
|
ready: Atom<boolean>;
|
||||||
|
disablePointerEvents: Atom<boolean>;
|
||||||
|
toggleMagnify: () => void;
|
||||||
|
focusNode: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +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";
|
import { DropDirection, FlexDirection, NavigateDirection } from "./types";
|
||||||
|
|
||||||
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;
|
||||||
@@ -82,3 +82,23 @@ export function setTransform(
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCenter(dimensions: Dimensions): Point {
|
||||||
|
return {
|
||||||
|
x: dimensions.left + dimensions.width / 2,
|
||||||
|
y: dimensions.top + dimensions.height / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function navigateDirectionToOffset(direction: NavigateDirection): Point {
|
||||||
|
switch (direction) {
|
||||||
|
case NavigateDirection.Up:
|
||||||
|
return { x: 0, y: -1 };
|
||||||
|
case NavigateDirection.Down:
|
||||||
|
return { x: 0, y: 1 };
|
||||||
|
case NavigateDirection.Left:
|
||||||
|
return { x: -1, y: 0 };
|
||||||
|
case NavigateDirection.Right:
|
||||||
|
return { x: 1, y: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+1
-2
@@ -212,6 +212,7 @@ declare global {
|
|||||||
type LayoutState = WaveObj & {
|
type LayoutState = WaveObj & {
|
||||||
rootnode?: any;
|
rootnode?: any;
|
||||||
magnifiednodeid?: string;
|
magnifiednodeid?: string;
|
||||||
|
focusednodeid?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// waveobj.MetaTSType
|
// waveobj.MetaTSType
|
||||||
@@ -585,8 +586,6 @@ declare global {
|
|||||||
type WaveWindow = WaveObj & {
|
type WaveWindow = WaveObj & {
|
||||||
workspaceid: string;
|
workspaceid: string;
|
||||||
activetabid: string;
|
activetabid: string;
|
||||||
activeblockid?: string;
|
|
||||||
activeblockmap: {[key: string]: string};
|
|
||||||
pos: Point;
|
pos: Point;
|
||||||
winsize: WinSize;
|
winsize: WinSize;
|
||||||
lastfocusts: number;
|
lastfocusts: number;
|
||||||
|
|||||||
+9
-10
@@ -126,16 +126,14 @@ func (*Client) GetOType() string {
|
|||||||
// stores the ui-context of the window
|
// stores the ui-context of the window
|
||||||
// workspaceid, active tab, active block within each tab, window size, etc.
|
// workspaceid, active tab, active block within each tab, window size, etc.
|
||||||
type Window struct {
|
type Window struct {
|
||||||
OID string `json:"oid"`
|
OID string `json:"oid"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
WorkspaceId string `json:"workspaceid"`
|
WorkspaceId string `json:"workspaceid"`
|
||||||
ActiveTabId string `json:"activetabid"`
|
ActiveTabId string `json:"activetabid"`
|
||||||
ActiveBlockId string `json:"activeblockid,omitempty"`
|
Pos Point `json:"pos"`
|
||||||
ActiveBlockMap map[string]string `json:"activeblockmap"` // map from tabid to blockid
|
WinSize WinSize `json:"winsize"`
|
||||||
Pos Point `json:"pos"`
|
LastFocusTs int64 `json:"lastfocusts"`
|
||||||
WinSize WinSize `json:"winsize"`
|
Meta MetaMapType `json:"meta"`
|
||||||
LastFocusTs int64 `json:"lastfocusts"`
|
|
||||||
Meta MetaMapType `json:"meta"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Window) GetOType() string {
|
func (*Window) GetOType() string {
|
||||||
@@ -180,6 +178,7 @@ type LayoutState struct {
|
|||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
RootNode any `json:"rootnode,omitempty"`
|
RootNode any `json:"rootnode,omitempty"`
|
||||||
MagnifiedNodeId string `json:"magnifiednodeid,omitempty"`
|
MagnifiedNodeId string `json:"magnifiednodeid,omitempty"`
|
||||||
|
FocusedNodeId string `json:"focusednodeid,omitempty"`
|
||||||
Meta MetaMapType `json:"meta,omitempty"`
|
Meta MetaMapType `json:"meta,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -199,9 +199,8 @@ func CreateWindow(ctx context.Context, winSize *waveobj.WinSize) (*waveobj.Windo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
window := &waveobj.Window{
|
window := &waveobj.Window{
|
||||||
OID: windowId,
|
OID: windowId,
|
||||||
WorkspaceId: workspaceId,
|
WorkspaceId: workspaceId,
|
||||||
ActiveBlockMap: make(map[string]string),
|
|
||||||
Pos: waveobj.Point{
|
Pos: waveobj.Point{
|
||||||
X: 100,
|
X: 100,
|
||||||
Y: 100,
|
Y: 100,
|
||||||
|
|||||||
Reference in New Issue
Block a user