create blockframes to replace blockheader (#59)

created two frames -- frameless and tech. frameless is used when there
is 0 or 1 blocks, otherwise tech is used.
This commit is contained in:
Mike Sawka 2024-06-18 23:44:53 -07:00 committed by GitHub
parent 9ff8cb0292
commit 15681ffa1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 213 additions and 55 deletions

View File

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

View File

@ -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<Block>(WOS.makeORef("block", blockId));
return (
<div key="header" className="block-header">
<div className="block-header-text text-fixed">
Block [{blockId.substring(0, 8)}] {blockData?.view}
</div>
<div className="block-header-text text-fixed">{getBlockHeaderText(blockData)}</div>
{onClose && (
<div className="close-button" onClick={onClose}>
<i className="fa fa-solid fa-xmark-large" />
@ -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<Block>(WOS.makeORef("block", blockId));
const isFocusedAtom = useBlockAtom<boolean>(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 (
<div
className={clsx(
"block",
"block-frame-tech",
isFocused ? "block-focused" : null,
preview ? "block-preview" : null
)}
>
<div className="block-frame-tech-header">{getBlockHeaderText(blockData)}</div>
<div className="block-frame-tech-close" onClick={onClose}>
<i className="fa fa-solid fa-xmark fa-fw " />
</div>
{preview ? <div className="block-frame-preview" /> : children}
</div>
);
};
const BlockFrame_Frameless = ({ blockId, onClose, preview, children }: BlockFrameProps) => {
const blockRef = React.useRef<HTMLDivElement>(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 (
<div className="block block-frame-frameless" ref={blockRef} onMouseLeave={mouseLeaveHandler}>
<div
className={clsx("block-header-animation-wrap", showHeader ? "is-showing" : null)}
onMouseLeave={mouseLeaveHandler}
>
<FramelessBlockHeader blockId={blockId} onClose={onClose} />
</div>
{preview ? <div className="block-frame-preview" /> : children}
</div>
);
};
const BlockFrame = (props: BlockFrameProps) => {
const blockId = props.blockId;
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(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 <BlockFrame_Frameless {...props} />;
}
return <BlockFrame_Tech {...props} />;
};
const Block = ({ blockId, onClose }: BlockProps) => {
let blockElem: JSX.Element = null;
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
if (!blockId || !blockData) return null;
@ -93,30 +176,14 @@ const Block = ({ blockId, onClose }: BlockProps) => {
blockElem = <CodeEdit text={null} filename={null} />;
}
return (
<div
className="block"
ref={blockRef}
onMouseLeave={() => {
setShowHeader(false);
hoverState.current = hoverStateOff;
}}
>
<div
className={clsx("block-header-animation-wrap", showHeader ? "is-showing" : null)}
onMouseLeave={() => {
setShowHeader(false);
hoverState.current = hoverStateOff;
}}
>
<BlockHeader blockId={blockId} onClose={onClose} />
</div>
<BlockFrame blockId={blockId} onClose={onClose} preview={false}>
<div key="content" className="block-content">
<ErrorBoundary>
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{blockElem}</React.Suspense>
</ErrorBoundary>
</div>
</div>
</BlockFrame>
);
};
export { Block, BlockHeader };
export { Block, BlockFrame };

View File

@ -56,6 +56,13 @@ const workspaceAtom: jotai.Atom<Workspace> = jotai.atom((get) => {
}
return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get);
});
const tabAtom: jotai.Atom<Tab> = 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"

View File

@ -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 <BlockHeader blockId={tabData.blockId} />;
return <BlockFrame blockId={tabData.blockId} preview={true} />;
}, []);
const onNodeDelete = useCallback((data: TabLayoutData) => {

View File

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

View File

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

View File

@ -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<T> {
className?: string;
}
const DragPreviewWidth = 300;
const DragPreviewHeight = 300;
export const TileLayout = <T,>({
layoutTreeStateAtom,
className,
@ -289,6 +292,7 @@ const DisplayNode = <T,>({
}: DisplayNodeProps<T>) => {
const tileNodeRef = useRef<HTMLDivElement>(null);
const previewRef = useRef<HTMLDivElement>(null);
const hasImagePreviewSetRef = useRef(false);
// Register the node as a draggable item.
const [{ isDragging }, drag, dragPreview] = useDrag(
@ -302,32 +306,32 @@ const DisplayNode = <T,>({
[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 (
<div className="tile-preview-container">
<div className="tile-preview" ref={previewRef}>
{previewElement}
</div>
</div>
);
const previewWidth = DragPreviewWidth;
const previewHeight = DragPreviewHeight;
const previewTransform = `scale(${1 / window.devicePixelRatio})`;
const [previewImage, setPreviewImage] = useState<HTMLImageElement>(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<HTMLImageElement>();
// 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);
img.onload = () => {
hasImagePreviewSetRef.current = true;
setPreviewImage(img);
dragPreview(img, { offsetY, offsetX });
};
});
}
}, [previewRef, previewImage, dragPreview]);
@ -364,7 +368,19 @@ const DisplayNode = <T,>({
onPointerEnter={generatePreviewImage}
>
{leafContent}
{preview}
<div key="preview" className="tile-preview-container">
<div
className="tile-preview"
ref={previewRef}
style={{
width: previewWidth,
height: previewHeight,
transform: previewTransform,
}}
>
{previewElement}
</div>
</div>
</div>
);
};

View File

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