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:
Evan Simkowitz
2024-08-26 11:56:00 -07:00
committed by GitHub
co-authored by sawka
parent 8e5a4a457c
commit 164afeeb66
19 changed files with 596 additions and 573 deletions
+27 -125
View File
@@ -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")) {
+41 -46
View File
@@ -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) {
+68 -74
View File
@@ -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);
+3 -12
View File
@@ -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;
+23 -17
View File
@@ -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>
);
}
);
+11 -20
View File
@@ -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,
+6 -28
View File
@@ -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);
+1 -38
View File
@@ -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">
+8 -2
View File
@@ -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,
}; };
+41 -68
View File
@@ -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 {
/** /**
+2 -4
View File
@@ -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
+10 -7
View File
@@ -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;
} }
+48 -35
View File
@@ -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++;
} }
+34 -13
View File
@@ -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>;
}
+21 -1
View File
@@ -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 };
}
}
+1 -2
View File
@@ -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
View File
@@ -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"`
} }
+2 -3
View File
@@ -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,