diff --git a/frontend/app/block/block.less b/frontend/app/block/block.less index 448e7c97..54af5a40 100644 --- a/frontend/app/block/block.less +++ b/frontend/app/block/block.less @@ -37,6 +37,71 @@ max-height: 30px; } } + + &.block-frame-tech { + border: 2px solid var(--border-color); + border-radius: 4px; + margin: 10px 2px 2px 2px; + padding: 10px 2px 2px 2px; + height: calc(100% - 12px); + width: calc(100% - 4px); + overflow: visible; + + &.block-preview { + background-color: var(--main-bg-color); + } + + &.block-focused { + border: 2px solid var(--accent-color); + + .block-frame-tech-header { + color: var(--main-text-color); + } + + .block-frame-tech-close { + display: none; + } + } + + .block-frame-tech-header { + position: absolute; + max-width: 85%; + left: 50%; + transform: translate(-50%, 0); + overflow: hidden; + text-overflow: ellipsis; + top: -11px; + padding: 4px 6px 4px 6px; + border-radius: 4px; + background-color: var(--main-bg-color); + font: var(--fixed-font); + color: var(--secondary-text-color); + white-space: nowrap; + } + + .block-frame-tech-close { + position: absolute; + top: 0; + right: -2px; + padding: 0 0 1px 1px; + border-radius: 4px; + background-color: var(--panel-bg-color); + cursor: pointer; + color: var(--grey-text-color); + opacity: 0; + + &:hover { + color: var(--secondary-text-color); + opacity: 1; + } + } + } + + .block-frame-preview { + background-color: var(--main-bg-color); + width: 100%; + flex-grow: 1; + } } .block-header { @@ -61,6 +126,7 @@ .block-header-text { padding: 0 5px; flex-grow: 1; + white-space: nowrap; } .close-button { diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index d209f942..c65ad096 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -7,8 +7,10 @@ import { PreviewView } from "@/app/view/preview"; import { TerminalView } from "@/app/view/term/term"; import { ErrorBoundary } from "@/element/errorboundary"; import { CenteredDiv } from "@/element/quickelems"; +import { atoms, useBlockAtom } from "@/store/global"; import * as WOS from "@/store/wos"; import clsx from "clsx"; +import * as jotai from "jotai"; import * as React from "react"; import "./block.less"; @@ -21,14 +23,19 @@ interface BlockProps { onClose?: () => void; } -const BlockHeader = ({ blockId, onClose }: BlockProps) => { +function getBlockHeaderText(blockData: Block): string { + if (!blockData) { + return "no block data"; + } + return `${blockData?.view} [${blockData.oid.substring(0, 8)}]`; +} + +const FramelessBlockHeader = ({ blockId, onClose }: BlockProps) => { const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); return (
-
- Block [{blockId.substring(0, 8)}] {blockData?.view} -
+
{getBlockHeaderText(blockData)}
{onClose && (
@@ -42,12 +49,52 @@ const hoverStateOff = "off"; const hoverStatePending = "pending"; const hoverStateOn = "on"; -const Block = ({ blockId, onClose }: BlockProps) => { +interface BlockFrameProps { + blockId: string; + onClose?: () => void; + preview: boolean; + children?: React.ReactNode; +} + +const BlockFrame_Tech = ({ blockId, onClose, preview, children }: BlockFrameProps) => { + const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + const isFocusedAtom = useBlockAtom(blockId, "isFocused", () => { + return jotai.atom((get) => { + const winData = get(atoms.waveWindow); + return winData.activeblockid === blockId; + }); + }); + let isFocused = jotai.useAtomValue(isFocusedAtom); + if (preview) { + isFocused = true; + } + return ( +
+
{getBlockHeaderText(blockData)}
+
+ +
+ {preview ?
: children} +
+ ); +}; + +const BlockFrame_Frameless = ({ blockId, onClose, preview, children }: BlockFrameProps) => { const blockRef = React.useRef(null); + const [showHeader, setShowHeader] = React.useState(preview ? true : false); const hoverState = React.useRef(hoverStateOff); - const [showHeader, setShowHeader] = React.useState(false); React.useEffect(() => { + if (preview) { + return; + } const block = blockRef.current; let hoverTimeout: NodeJS.Timeout = null; const handleMouseMove = (event) => { @@ -77,7 +124,43 @@ const Block = ({ blockId, onClose }: BlockProps) => { block.removeEventListener("mousemove", handleMouseMove); }; }); + let mouseLeaveHandler = () => { + if (preview) { + return; + } + setShowHeader(false); + hoverState.current = hoverStateOff; + }; + return ( +
+
+ +
+ {preview ?
: children} +
+ ); +}; +const BlockFrame = (props: BlockFrameProps) => { + const blockId = props.blockId; + const [blockData, blockDataLoading] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + const tabData = jotai.useAtomValue(atoms.tabAtom); + + if (!blockId || !blockData) { + return null; + } + // if 0 or 1 blocks, use frameless, otherwise use tech + const numBlocks = tabData?.blockids?.length ?? 0; + if (numBlocks <= 1) { + return ; + } + return ; +}; + +const Block = ({ blockId, onClose }: BlockProps) => { let blockElem: JSX.Element = null; const [blockData, blockDataLoading] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); if (!blockId || !blockData) return null; @@ -93,30 +176,14 @@ const Block = ({ blockId, onClose }: BlockProps) => { blockElem = ; } return ( -
{ - setShowHeader(false); - hoverState.current = hoverStateOff; - }} - > -
{ - setShowHeader(false); - hoverState.current = hoverStateOff; - }} - > - -
+
Loading...}>{blockElem}
-
+ ); }; -export { Block, BlockHeader }; +export { Block, BlockFrame }; diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 3b46d296..01e0196b 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -56,6 +56,13 @@ const workspaceAtom: jotai.Atom = jotai.atom((get) => { } return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get); }); +const tabAtom: jotai.Atom = jotai.atom((get) => { + const windowData = get(windowDataAtom); + if (windowData == null) { + return null; + } + return WOS.getObjectValue(WOS.makeORef("tab", windowData.activetabid), get); +}); const atoms = { // initialized in wave.ts (will not be null inside of application) @@ -65,6 +72,7 @@ const atoms = { client: clientAtom, waveWindow: windowDataAtom, workspace: workspaceAtom, + tabAtom: tabAtom, }; // key is "eventType" or "eventType|oref" diff --git a/frontend/app/tab/tabcontent.tsx b/frontend/app/tab/tabcontent.tsx index 618a3cc4..206ebc5b 100644 --- a/frontend/app/tab/tabcontent.tsx +++ b/frontend/app/tab/tabcontent.tsx @@ -1,7 +1,7 @@ // Copyright 2023, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { Block, BlockHeader } from "@/app/block/block"; +import { Block, BlockFrame } from "@/app/block/block"; import * as services from "@/store/services"; import * as WOS from "@/store/wos"; @@ -30,7 +30,7 @@ const TabContent = ({ tabId }: { tabId: string }) => { const renderPreview = useCallback((tabData: TabLayoutData) => { console.log("renderPreview", tabData); - return ; + return ; }, []); const onNodeDelete = useCallback((data: TabLayoutData) => { diff --git a/frontend/app/theme.less b/frontend/app/theme.less index 25312904..ac91fff2 100644 --- a/frontend/app/theme.less +++ b/frontend/app/theme.less @@ -5,6 +5,7 @@ --main-text-color: #f7f7f7; --title-font-size: 18px; --secondary-text-color: rgb(195, 200, 194); + --grey-text-color: #666; --main-bg-color: #000000; --border-color: #333333; --base-font: normal 15px / normal "Lato", sans-serif; diff --git a/frontend/app/view/term/term.less b/frontend/app/view/term/term.less index 7ef1d0b4..424bb22e 100644 --- a/frontend/app/view/term/term.less +++ b/frontend/app/view/term/term.less @@ -11,10 +11,6 @@ padding-left: 4px; position: relative; - &:focus-within { - border-left: 4px solid var(--accent-color); - } - .term-header { display: flex; flex-direction: row; diff --git a/frontend/faraday/lib/TileLayout.tsx b/frontend/faraday/lib/TileLayout.tsx index a7ade31a..e5a537cf 100644 --- a/frontend/faraday/lib/TileLayout.tsx +++ b/frontend/faraday/lib/TileLayout.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import clsx from "clsx"; +import { toPng } from "html-to-image"; import React, { CSSProperties, ReactNode, @@ -17,7 +18,6 @@ import React, { import { useDrag, useDragLayer, useDrop } from "react-dnd"; import useResizeObserver from "@react-hook/resize-observer"; -import { toPng } from "html-to-image"; import { useLayoutTreeStateReducerAtom } from "./layoutAtom"; import { findNode } from "./layoutNode"; import { @@ -60,6 +60,9 @@ export interface TileLayoutProps { className?: string; } +const DragPreviewWidth = 300; +const DragPreviewHeight = 300; + export const TileLayout = ({ layoutTreeStateAtom, className, @@ -289,6 +292,7 @@ const DisplayNode = ({ }: DisplayNodeProps) => { const tileNodeRef = useRef(null); const previewRef = useRef(null); + const hasImagePreviewSetRef = useRef(false); // Register the node as a draggable item. const [{ isDragging }, drag, dragPreview] = useDrag( @@ -302,32 +306,32 @@ const DisplayNode = ({ [layoutNode] ); - // Generate a preview div using the provided renderPreview function. This will be placed in the DOM so we can render an image from it, but it is pushed out of view so the user will not see it. - // No-op if not provided, meaning React-DnD will attempt to generate a preview from the DOM, which is very slow. - const preview = useMemo(() => { - const previewElement = renderPreview?.(layoutNode.data); - return ( -
-
- {previewElement} -
-
- ); + const previewElement = renderPreview?.(layoutNode.data); + const previewWidth = DragPreviewWidth; + const previewHeight = DragPreviewHeight; + const previewTransform = `scale(${1 / window.devicePixelRatio})`; + const [previewImage, setPreviewImage] = useState(null); + // we set the drag preview on load to be the HTML element + // later, on pointerenter, we generate a static png preview to use instead (for performance) + useEffect(() => { + if (!hasImagePreviewSetRef.current) { + dragPreview(previewRef.current); + } }, []); - - // Cache the preview image after we generate it - const [previewImage, setPreviewImage] = useState(); - - // When a user first mouses over a node, generate a preview image and set it as the drag preview. const generatePreviewImage = useCallback(() => { - if (previewImage) { - dragPreview(previewImage); + let offsetX = (DragPreviewWidth * window.devicePixelRatio - DragPreviewWidth) / 2 + 10; + let offsetY = (DragPreviewHeight * window.devicePixelRatio - DragPreviewHeight) / 2 + 10; + if (previewImage != null) { + dragPreview(previewImage, { offsetY, offsetX }); } else if (previewRef.current) { toPng(previewRef.current).then((url) => { const img = new Image(); img.src = url; - img.onload = () => dragPreview(img); - setPreviewImage(img); + img.onload = () => { + hasImagePreviewSetRef.current = true; + setPreviewImage(img); + dragPreview(img, { offsetY, offsetX }); + }; }); } }, [previewRef, previewImage, dragPreview]); @@ -364,7 +368,19 @@ const DisplayNode = ({ onPointerEnter={generatePreviewImage} > {leafContent} - {preview} +
+
+ {previewElement} +
+
); }; diff --git a/frontend/faraday/lib/tilelayout.less b/frontend/faraday/lib/tilelayout.less index 4f52258c..21bd249c 100644 --- a/frontend/faraday/lib/tilelayout.less +++ b/frontend/faraday/lib/tilelayout.less @@ -46,13 +46,16 @@ } .tile-preview-container { - height: fit-content !important; - width: fit-content !important; position: absolute; top: 10000px; white-space: nowrap !important; user-select: none; -webkit-user-select: none; + + .tile-preview { + width: 100%; + height: 100%; + } } } @@ -75,6 +78,7 @@ .tile-leaf { border: 1px solid black; + overflow: hidden; } .placeholder {