Add ephemeral block support (#1275)

Ephemeral blocks can now be added to the LayoutModel for a tab. Only one
ephemeral block can exist at a time. It is placed above all other
blocks, including the magnified blocks.

Updates how magnified and ephemeral blocks overlay the other blocks.
Now, there's a blurred backdrop behind them that will obscure the other
blocks. As a result of this, the overlayed blocks are now translucent.
This commit is contained in:
Evan Simkowitz 2024-11-13 18:00:13 -08:00 committed by GitHub
parent 31d0aa114d
commit 3fcf209b52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 272 additions and 56 deletions

View File

@ -61,7 +61,7 @@
} }
&.block-preview.block-frame-default .block-frame-default-inner .block-frame-default-header { &.block-preview.block-frame-default .block-frame-default-inner .block-frame-default-header {
background-color: rgba(0, 0, 0, 0.7); background-color: rgb(from var(--block-bg-color) r g b / 70%);
} }
&.block-frame-default { &.block-frame-default {
@ -254,7 +254,7 @@
} }
.block-frame-preview { .block-frame-preview {
background-color: rgba(0, 0, 0, 0.7); background-color: rgb(from var(--block-bg-color) r g b / 70%);
width: 100%; width: 100%;
flex-grow: 1; flex-grow: 1;
border-bottom-left-radius: var(--block-border-radius); border-bottom-left-radius: var(--block-border-radius);
@ -271,6 +271,12 @@
} }
} }
&.magnified,
&.ephemeral {
background-color: rgb(from var(--block-bg-color) r g b / 60%);
backdrop-filter: blur(10px);
}
.connstatus-overlay { .connstatus-overlay {
position: absolute; position: absolute;
top: calc(var(--header-height) + 6px); top: calc(var(--header-height) + 6px);
@ -385,7 +391,7 @@
&.show-block-mask .block-mask-inner { &.show-block-mask .block-mask-inner {
margin-top: var(--header-height); // TODO fix this magic margin-top: var(--header-height); // TODO fix this magic
background-color: rgba(0, 0, 0, 0.5); background-color: rgb(from var(--block-bg-color) r g b / 50%);
height: calc(100% - var(--header-height)); height: calc(100% - var(--header-height));
width: 100%; width: 100%;
display: flex; display: flex;

View File

@ -120,6 +120,7 @@ function computeEndIcons(
const endIconsElem: JSX.Element[] = []; const endIconsElem: JSX.Element[] = [];
const endIconButtons = util.useAtomValueSafe(viewModel?.endIconButtons); const endIconButtons = util.useAtomValueSafe(viewModel?.endIconButtons);
const magnified = jotai.useAtomValue(nodeModel.isMagnified); const magnified = jotai.useAtomValue(nodeModel.isMagnified);
const ephemeral = jotai.useAtomValue(nodeModel.isEphemeral);
const numLeafs = jotai.useAtomValue(nodeModel.numLeafs); const numLeafs = jotai.useAtomValue(nodeModel.numLeafs);
const magnifyDisabled = numLeafs <= 1; const magnifyDisabled = numLeafs <= 1;
@ -133,14 +134,27 @@ function computeEndIcons(
click: onContextMenu, click: onContextMenu,
}; };
endIconsElem.push(<IconButton key="settings" decl={settingsDecl} className="block-frame-settings" />); endIconsElem.push(<IconButton key="settings" decl={settingsDecl} className="block-frame-settings" />);
endIconsElem.push( if (ephemeral) {
<OptMagnifyButton const addToLayoutDecl: IconButtonDecl = {
key="unmagnify" elemtype: "iconbutton",
magnified={magnified} icon: "circle-plus",
toggleMagnify={nodeModel.toggleMagnify} title: "Add to Layout",
disabled={magnifyDisabled} click: () => {
/> nodeModel.addEphemeralNodeToLayout();
); },
};
endIconsElem.push(<IconButton key="add-to-layout" decl={addToLayoutDecl} />);
} else {
endIconsElem.push(
<OptMagnifyButton
key="unmagnify"
magnified={magnified}
toggleMagnify={nodeModel.toggleMagnify}
disabled={magnifyDisabled}
/>
);
}
const closeDecl: IconButtonDecl = { const closeDecl: IconButtonDecl = {
elemtype: "iconbutton", elemtype: "iconbutton",
icon: "xmark-large", icon: "xmark-large",
@ -166,6 +180,7 @@ const BlockFrame_Header = ({
const preIconButton = util.useAtomValueSafe(viewModel?.preIconButton); const preIconButton = util.useAtomValueSafe(viewModel?.preIconButton);
let headerTextUnion = util.useAtomValueSafe(viewModel?.viewText); let headerTextUnion = util.useAtomValueSafe(viewModel?.viewText);
const magnified = jotai.useAtomValue(nodeModel.isMagnified); const magnified = jotai.useAtomValue(nodeModel.isMagnified);
const ephemeral = jotai.useAtomValue(nodeModel.isEphemeral);
const manageConnection = util.useAtomValueSafe(viewModel?.manageConnection); const manageConnection = util.useAtomValueSafe(viewModel?.manageConnection);
const dragHandleRef = preview ? null : nodeModel.dragHandleRef; const dragHandleRef = preview ? null : nodeModel.dragHandleRef;
@ -221,7 +236,12 @@ const BlockFrame_Header = ({
} }
return ( return (
<div className="block-frame-default-header" ref={dragHandleRef} onContextMenu={onContextMenu}> <div
className="block-frame-default-header"
ref={dragHandleRef}
onContextMenu={onContextMenu}
draggable={!(preview || magnified || ephemeral)}
>
{preIconButtonElem} {preIconButtonElem}
<div className="block-frame-default-header-iconview"> <div className="block-frame-default-header-iconview">
{viewIconElem} {viewIconElem}
@ -309,7 +329,6 @@ const ConnStatusOverlay = React.memo(
const [overlayRefCallback, _, domRect] = useDimensionsWithCallbackRef(30); const [overlayRefCallback, _, domRect] = useDimensionsWithCallbackRef(30);
const width = domRect?.width; const width = domRect?.width;
const [showError, setShowError] = React.useState(false); const [showError, setShowError] = React.useState(false);
const blockNum = jotai.useAtomValue(nodeModel.blockNum);
React.useEffect(() => { React.useEffect(() => {
if (width) { if (width) {
@ -421,6 +440,8 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
return jotai.atom(false); return jotai.atom(false);
}) as jotai.PrimitiveAtom<boolean>; }) as jotai.PrimitiveAtom<boolean>;
const connModalOpen = jotai.useAtomValue(changeConnModalAtom); const connModalOpen = jotai.useAtomValue(changeConnModalAtom);
const isMagnified = jotai.useAtomValue(nodeModel.isMagnified);
const isEphemeral = jotai.useAtomValue(nodeModel.isEphemeral);
const connBtnRef = React.useRef<HTMLDivElement>(); const connBtnRef = React.useRef<HTMLDivElement>();
React.useEffect(() => { React.useEffect(() => {
@ -476,6 +497,8 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
"block-focused": isFocused || preview, "block-focused": isFocused || preview,
"block-preview": preview, "block-preview": preview,
"block-no-highlight": numBlocksInTab === 1, "block-no-highlight": numBlocksInTab === 1,
ephemeral: isEphemeral,
magnified: isMagnified,
})} })}
data-blockid={nodeModel.blockId} data-blockid={nodeModel.blockId}
onClick={blockModel?.onClick} onClick={blockModel?.onClick}

View File

@ -341,17 +341,21 @@ function getApi(): ElectronApi {
return (window as any).api; return (window as any).api;
} }
async function createBlock(blockDef: BlockDef, magnified = false): Promise<string> { async function createBlock(blockDef: BlockDef, magnified = false, ephemeral = false): Promise<string> {
const tabId = globalStore.get(atoms.staticTabId);
const layoutModel = getLayoutModelForTabById(tabId);
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } }; const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
const blockId = await ObjectService.CreateBlock(blockDef, rtOpts); const blockId = await ObjectService.CreateBlock(blockDef, rtOpts);
if (ephemeral) {
layoutModel.newEphemeralNode(blockId);
return blockId;
}
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, magnified,
focused: true, focused: true,
}; };
const tabId = globalStore.get(atoms.staticTabId);
const layoutModel = getLayoutModelForTabById(tabId);
layoutModel.treeReducer(insertNodeAction); layoutModel.treeReducer(insertNodeAction);
return blockId; return blockId;
} }

View File

@ -56,10 +56,14 @@
--zindex-tab-name: 3; --zindex-tab-name: 3;
--zindex-layout-display-container: 0; --zindex-layout-display-container: 0;
--zindex-layout-last-magnified-node: 1; --zindex-layout-last-magnified-node: 1;
--zindex-layout-resize-handle: 2; --zindex-layout-last-ephemeral-node: 2;
--zindex-layout-placeholder-container: 3; --zindex-layout-resize-handle: 3;
--zindex-layout-overlay-container: 4; --zindex-layout-placeholder-container: 4;
--zindex-layout-magnified-node: 5; --zindex-layout-overlay-container: 5;
--zindex-layout-magnified-node-backdrop: 6;
--zindex-layout-magnified-node: 7;
--zindex-layout-ephemeral-node-backdrop: 8;
--zindex-layout-ephemeral-node: 9;
--zindex-block-mask-inner: 10; --zindex-block-mask-inner: 10;
--zindex-flash-error-container: 550; --zindex-flash-error-container: 550;
--zindex-app-background: -1; --zindex-app-background: -1;

View File

@ -240,7 +240,7 @@ export class WaveAiModel implements ViewModel {
file: path, file: path,
}, },
}; };
await createBlock(blockDef, true); await createBlock(blockDef, false, true);
}); });
}, },
}); });

View File

@ -57,6 +57,7 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
const setActiveDrag = useSetAtom(layoutModel.activeDrag); const setActiveDrag = useSetAtom(layoutModel.activeDrag);
const setReady = useSetAtom(layoutModel.ready); const setReady = useSetAtom(layoutModel.ready);
const isResizing = useAtomValue(layoutModel.isResizing); const isResizing = useAtomValue(layoutModel.isResizing);
const ephemeralNode = useAtomValue(layoutModel.ephemeralNode);
const { activeDrag, dragClientOffset } = useDragLayer((monitor) => ({ const { activeDrag, dragClientOffset } = useDragLayer((monitor) => ({
activeDrag: monitor.isDragging(), activeDrag: monitor.isDragging(),
@ -121,6 +122,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 layoutModel={layoutModel} /> <DisplayNodesWrapper layoutModel={layoutModel} />
<NodeBackdrops 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} />
@ -130,6 +132,55 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
} }
export const TileLayout = memo(TileLayoutComponent) as typeof TileLayoutComponent; export const TileLayout = memo(TileLayoutComponent) as typeof TileLayoutComponent;
function NodeBackdrops({ layoutModel }: { layoutModel: LayoutModel }) {
const ephemeralNode = useAtomValue(layoutModel.ephemeralNode);
const magnifiedNodeId = useAtomValue(layoutModel.treeStateAtom).magnifiedNodeId;
const [showMagnifiedBackdrop, setShowMagnifiedBackdrop] = useState(!!ephemeralNode);
const [showEphemeralBackdrop, setShowEphemeralBackdrop] = useState(!!magnifiedNodeId);
const debouncedCallback = useCallback(
debounce(100, (callback: () => void) => callback()),
[]
);
useEffect(() => {
if (magnifiedNodeId && !showMagnifiedBackdrop) {
debouncedCallback(() => setShowMagnifiedBackdrop(true));
}
if (!magnifiedNodeId) {
setShowMagnifiedBackdrop(false);
}
if (ephemeralNode && !showEphemeralBackdrop) {
debouncedCallback(() => setShowEphemeralBackdrop(true));
}
if (!ephemeralNode) {
setShowEphemeralBackdrop(false);
}
}, [ephemeralNode, magnifiedNodeId]);
return (
<>
{showMagnifiedBackdrop && (
<div
className="magnified-node-backdrop"
onClick={() => {
layoutModel.magnifyNodeToggle(magnifiedNodeId);
}}
/>
)}
{showEphemeralBackdrop && (
<div
className="ephemeral-node-backdrop"
onClick={() => {
layoutModel.closeNode(ephemeralNode?.id);
}}
/>
)}
</>
);
}
interface DisplayNodesWrapperProps { interface DisplayNodesWrapperProps {
/** /**
* The layout tree state. * The layout tree state.
@ -173,7 +224,6 @@ const DisplayNode = ({ layoutModel, node }: DisplayNodeProps) => {
() => ({ () => ({
type: dragItemType, type: dragItemType,
item: () => node, item: () => node,
canDrag: () => !addlProps?.isMagnifiedNode,
collect: (monitor) => ({ collect: (monitor) => ({
isDragging: monitor.isDragging(), isDragging: monitor.isDragging(),
}), }),
@ -243,8 +293,6 @@ const DisplayNode = ({ layoutModel, node }: DisplayNodeProps) => {
<div <div
className={clsx("tile-node", { className={clsx("tile-node", {
dragging: isDragging, dragging: isDragging,
magnified: addlProps?.isMagnifiedNode,
"last-magnified": addlProps?.isLastMagnifiedNode,
})} })}
key={node.id} key={node.id}
ref={tileNodeRef} ref={tileNodeRef}

View File

@ -171,6 +171,8 @@ export class LayoutModel {
* Atom pointing to the currently focused node. * Atom pointing to the currently focused node.
*/ */
focusedNode: Atom<LayoutNode>; focusedNode: Atom<LayoutNode>;
// TODO: Nodes that need to be placed at higher z-indices should probably be handled by an ordered list, rather than individual properties.
/** /**
* The currently magnified node. * The currently magnified node.
*/ */
@ -179,6 +181,14 @@ export class LayoutModel {
* The last node to be magnified, other than the current magnified node, if set. This node should sit at a higher z-index than the others so that it floats above the other nodes as it returns to its original position. * The last node to be magnified, other than the current magnified node, if set. This node should sit at a higher z-index than the others so that it floats above the other nodes as it returns to its original position.
*/ */
lastMagnifiedNodeId: string; lastMagnifiedNodeId: string;
/**
* Atom holding an ephemeral node that is not part of the layout tree. This node displays above all other nodes.
*/
ephemeralNode: PrimitiveAtom<LayoutNode>;
/**
* The last node to be an ephemeral node. This node should sit at a higher z-index than the others so that it floats above the other nodes as it returns to its original position.
*/
lastEphemeralNodeId: string;
/** /**
* The size of the resize handles, in CSS pixels. * The size of the resize handles, in CSS pixels.
@ -267,6 +277,8 @@ export class LayoutModel {
} }
}); });
this.ephemeralNode = atom();
this.focusedNode = atom((get) => { this.focusedNode = atom((get) => {
const treeState = get(this.treeStateAtom); const treeState = get(this.treeStateAtom);
if (treeState.focusedNodeId == null) { if (treeState.focusedNodeId == null) {
@ -366,6 +378,7 @@ export class LayoutModel {
if (this.lastTreeStateGeneration < this.treeState.generation) { if (this.lastTreeStateGeneration < this.treeState.generation) {
if (this.magnifiedNodeId !== this.treeState.magnifiedNodeId) { if (this.magnifiedNodeId !== this.treeState.magnifiedNodeId) {
this.lastMagnifiedNodeId = this.magnifiedNodeId; this.lastMagnifiedNodeId = this.magnifiedNodeId;
this.lastEphemeralNodeId = undefined;
this.magnifiedNodeId = this.treeState.magnifiedNodeId; this.magnifiedNodeId = this.treeState.magnifiedNodeId;
} }
this.updateTree(); this.updateTree();
@ -486,11 +499,28 @@ export class LayoutModel {
? (pendingAction as LayoutTreeResizeNodeAction) ? (pendingAction as LayoutTreeResizeNodeAction)
: null; : null;
const resizeHandleSizePx = this.getter(this.resizeHandleSizePx); const resizeHandleSizePx = this.getter(this.resizeHandleSizePx);
const boundingRect = this.getBoundingRect();
const callback = (node: LayoutNode) => const callback = (node: LayoutNode) =>
this.updateTreeHelper(node, newAdditionalProps, newLeafs, resizeHandleSizePx, resizeAction); this.updateTreeHelper(
node,
newAdditionalProps,
newLeafs,
resizeHandleSizePx,
boundingRect,
resizeAction
);
if (balanceTree) this.treeState.rootNode = balanceNode(this.treeState.rootNode, callback); if (balanceTree) this.treeState.rootNode = balanceNode(this.treeState.rootNode, callback);
else walkNodes(this.treeState.rootNode, callback); else walkNodes(this.treeState.rootNode, callback);
// Process ephemeral node, if present.
const ephemeralNode = this.getter(this.ephemeralNode);
if (ephemeralNode) {
console.log("updateTree ephemeralNode", ephemeralNode);
this.updateEphemeralNodeProps(ephemeralNode, newAdditionalProps, newLeafs, boundingRect);
}
this.treeState.leafOrder = getLeafOrder(newLeafs, newAdditionalProps); this.treeState.leafOrder = getLeafOrder(newLeafs, newAdditionalProps);
this.validateFocusedNode(this.treeState.leafOrder); this.validateFocusedNode(this.treeState.leafOrder);
this.validateMagnifiedNode(this.treeState.leafOrder, newAdditionalProps); this.validateMagnifiedNode(this.treeState.leafOrder, newAdditionalProps);
@ -516,23 +546,14 @@ export class LayoutModel {
additionalPropsMap: Record<string, LayoutNodeAdditionalProps>, additionalPropsMap: Record<string, LayoutNodeAdditionalProps>,
leafs: LayoutNode[], leafs: LayoutNode[],
resizeHandleSizePx: number, resizeHandleSizePx: number,
boundingRect: Dimensions,
resizeAction?: LayoutTreeResizeNodeAction resizeAction?: LayoutTreeResizeNodeAction
) { ) {
/**
* Gets normalized dimensions for the TileLayout container.
* @returns The normalized dimensions for the TileLayout container.
*/
const getBoundingRect: () => Dimensions = () => {
const boundingRect = this.displayContainerRef.current.getBoundingClientRect();
return { top: 0, left: 0, width: boundingRect.width, height: boundingRect.height };
};
if (!node.children?.length) { if (!node.children?.length) {
leafs.push(node); leafs.push(node);
const addlProps = additionalPropsMap[node.id]; const addlProps = additionalPropsMap[node.id];
if (addlProps) { if (addlProps) {
if (this.magnifiedNodeId === node.id) { if (this.magnifiedNodeId === node.id) {
const boundingRect = getBoundingRect();
const transform = setTransform( const transform = setTransform(
{ {
top: boundingRect.height * 0.05, top: boundingRect.height * 0.05,
@ -540,12 +561,17 @@ export class LayoutModel {
width: boundingRect.width * 0.9, width: boundingRect.width * 0.9,
height: boundingRect.height * 0.9, height: boundingRect.height * 0.9,
}, },
true true,
true,
"var(--zindex-layout-magnified-node)"
); );
addlProps.transform = transform; addlProps.transform = transform;
addlProps.isMagnifiedNode = true;
} }
addlProps.isLastMagnifiedNode = this.lastMagnifiedNodeId === node.id; if (this.lastMagnifiedNodeId === node.id) {
addlProps.transform.zIndex = "var(--zindex-layout-last-magnified-node)";
} else if (this.lastEphemeralNodeId === node.id) {
addlProps.transform.zIndex = "var(--zindex-layout-last-ephemeral-node)";
}
} }
return; return;
} }
@ -558,7 +584,7 @@ export class LayoutModel {
? additionalPropsMap[node.id] ? additionalPropsMap[node.id]
: { treeKey: "0" }; : { treeKey: "0" };
const nodeRect: Dimensions = node.id === this.treeState.rootNode.id ? getBoundingRect() : additionalProps.rect; const nodeRect: Dimensions = node.id === this.treeState.rootNode.id ? boundingRect : additionalProps.rect;
const nodeIsRow = node.flexDirection === FlexDirection.Row; const nodeIsRow = node.flexDirection === FlexDirection.Row;
const nodePixels = nodeIsRow ? nodeRect.width : nodeRect.height; const nodePixels = nodeIsRow ? nodeRect.width : nodeRect.height;
const totalChildrenSize = node.children.reduce((acc, child) => acc + getNodeSize(child), 0); const totalChildrenSize = node.children.reduce((acc, child) => acc + getNodeSize(child), 0);
@ -615,6 +641,15 @@ export class LayoutModel {
}; };
} }
/**
* Gets normalized dimensions for the TileLayout container.
* @returns The normalized dimensions for the TileLayout container.
*/
getBoundingRect: () => Dimensions = () => {
const boundingRect = this.displayContainerRef.current.getBoundingClientRect();
return { top: 0, left: 0, width: boundingRect.width, height: boundingRect.height };
};
/** /**
* The id of the focused node in the layout. * The id of the focused node in the layout.
*/ */
@ -627,6 +662,7 @@ export class LayoutModel {
* @param leafOrder The new leaf order array to use when searching for stale nodes in the stack. * @param leafOrder The new leaf order array to use when searching for stale nodes in the stack.
*/ */
private validateFocusedNode(leafOrder: LeafOrderEntry[]) { private validateFocusedNode(leafOrder: LeafOrderEntry[]) {
console.log("validateFocusedNode", this.treeState.focusedNodeId, this.focusedNodeId, this.focusedNodeIdStack);
if (this.treeState.focusedNodeId !== this.focusedNodeId) { if (this.treeState.focusedNodeId !== this.focusedNodeId) {
// Remove duplicates and stale entries from focus stack. // Remove duplicates and stale entries from focus stack.
const newFocusedNodeIdStack: string[] = []; const newFocusedNodeIdStack: string[] = [];
@ -794,6 +830,11 @@ export class LayoutModel {
const treeState = get(this.treeStateAtom); const treeState = get(this.treeStateAtom);
return treeState.magnifiedNodeId === nodeid; return treeState.magnifiedNodeId === nodeid;
}), }),
isEphemeral: atom((get) => {
const ephemeralNode = get(this.ephemeralNode);
return ephemeralNode?.id === nodeid;
}),
addEphemeralNodeToLayout: () => this.addEphemeralNodeToLayout(),
animationTimeS: this.animationTimeS, animationTimeS: this.animationTimeS,
ready: this.ready, ready: this.ready,
disablePointerEvents: this.activeDrag, disablePointerEvents: this.activeDrag,
@ -907,10 +948,15 @@ export class LayoutModel {
*/ */
focusNode(nodeId: string) { focusNode(nodeId: string) {
if (this.focusedNodeId === nodeId) return; if (this.focusedNodeId === nodeId) return;
const layoutNode = findNode(this.treeState?.rootNode, nodeId); let layoutNode = findNode(this.treeState?.rootNode, nodeId);
if (!layoutNode) { if (!layoutNode) {
console.error("unable to focus node, cannot find it in tree", nodeId); const ephemeralNode = this.getter(this.ephemeralNode);
return; if (ephemeralNode?.id === nodeId) {
layoutNode = ephemeralNode;
} else {
console.error("unable to focus node, cannot find it in tree", nodeId);
return;
}
} }
const action: LayoutTreeFocusNodeAction = { const action: LayoutTreeFocusNodeAction = {
type: LayoutTreeActionType.FocusNode, type: LayoutTreeActionType.FocusNode,
@ -931,13 +977,16 @@ export class LayoutModel {
* Toggle magnification of a given node. * Toggle magnification of a given node.
* @param nodeId The id of the node that is being magnified. * @param nodeId The id of the node that is being magnified.
*/ */
magnifyNodeToggle(nodeId: string) { magnifyNodeToggle(nodeId: string, setState = true) {
const action: LayoutTreeMagnifyNodeToggleAction = { const action: LayoutTreeMagnifyNodeToggleAction = {
type: LayoutTreeActionType.MagnifyNodeToggle, type: LayoutTreeActionType.MagnifyNodeToggle,
nodeId: nodeId, nodeId: nodeId,
}; };
this.treeReducer(action); // Unset the last ephemeral node id to ensure the magnify animation sits on top of the layout.
this.lastEphemeralNodeId = undefined;
this.treeReducer(action, setState);
} }
/** /**
@ -947,9 +996,23 @@ export class LayoutModel {
async closeNode(nodeId: string) { async closeNode(nodeId: string) {
const nodeToDelete = findNode(this.treeState.rootNode, nodeId); const nodeToDelete = findNode(this.treeState.rootNode, nodeId);
if (!nodeToDelete) { if (!nodeToDelete) {
// TODO: clean up the ephemeral node handling
// The ephemeral node is not in the tree, so we need to handle it separately.
const ephemeralNode = this.getter(this.ephemeralNode);
if (ephemeralNode?.id === nodeId) {
this.setter(this.ephemeralNode, undefined);
this.treeState.focusedNodeId = undefined;
this.updateTree(false);
this.setTreeStateAtom(true);
await this.onNodeDelete?.(ephemeralNode.data);
return;
}
console.error("unable to close node, cannot find it in tree", nodeId); console.error("unable to close node, cannot find it in tree", nodeId);
return; return;
} }
if (nodeId === this.magnifiedNodeId) {
this.magnifyNodeToggle(nodeId);
}
const deleteAction: LayoutTreeDeleteNodeAction = { const deleteAction: LayoutTreeDeleteNodeAction = {
type: LayoutTreeActionType.DeleteNode, type: LayoutTreeActionType.DeleteNode,
nodeId: nodeId, nodeId: nodeId,
@ -965,6 +1028,61 @@ export class LayoutModel {
await this.closeNode(this.focusedNodeId); await this.closeNode(this.focusedNodeId);
} }
newEphemeralNode(blockId: string) {
if (this.getter(this.ephemeralNode)) {
this.closeNode(this.getter(this.ephemeralNode).id);
}
const ephemeralNode = newLayoutNode(undefined, undefined, undefined, { blockId });
this.setter(this.ephemeralNode, ephemeralNode);
const addlProps = this.getter(this.additionalProps);
const leafs = this.getter(this.leafs);
const boundingRect = this.getBoundingRect();
this.updateEphemeralNodeProps(ephemeralNode, addlProps, leafs, boundingRect);
this.setter(this.additionalProps, addlProps);
this.focusNode(ephemeralNode.id);
}
addEphemeralNodeToLayout() {
const ephemeralNode = this.getter(this.ephemeralNode);
this.setter(this.ephemeralNode, undefined);
if (this.magnifiedNodeId) {
this.magnifyNodeToggle(this.magnifiedNodeId, false);
}
this.lastEphemeralNodeId = ephemeralNode.id;
if (ephemeralNode) {
const action: LayoutTreeInsertNodeAction = {
type: LayoutTreeActionType.InsertNode,
node: ephemeralNode,
magnified: false,
focused: false,
};
this.treeReducer(action);
}
}
updateEphemeralNodeProps(
node: LayoutNode,
addlPropsMap: Record<string, LayoutNodeAdditionalProps>,
leafs: LayoutNode[],
boundingRect: Dimensions
) {
const transform = setTransform(
{
top: boundingRect.height * 0.075,
left: boundingRect.width * 0.075,
width: boundingRect.width * 0.85,
height: boundingRect.height * 0.85,
},
true,
true,
"var(--zindex-layout-ephemeral-node)"
);
addlPropsMap[node.id] = { treeKey: "-1", transform };
leafs.push(node);
}
/** /**
* Callback that is invoked when a drag operation completes and the pending action should be committed. * Callback that is invoked when a drag operation completes and the pending action should be committed.
*/ */

View File

@ -84,14 +84,6 @@
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
&.magnified {
background-color: var(--block-bg-solid-color);
z-index: var(--zindex-layout-magnified-node);
}
&.last-magnified {
z-index: var(--zindex-layout-last-magnified-node);
}
.tile-leaf { .tile-leaf {
overflow: hidden; overflow: hidden;
} }
@ -109,11 +101,29 @@
} }
} }
&:not(:only-child, .magnified) .tile-leaf { &:not(:only-child) .tile-leaf {
padding: calc(var(--gap-size-px) / 2); padding: calc(var(--gap-size-px) / 2);
} }
} }
.magnified-node-backdrop,
.ephemeral-node-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
backdrop-filter: blur(2px);
}
.magnified-node-backdrop {
z-index: var(--zindex-layout-magnified-node-backdrop);
}
.ephemeral-node-backdrop {
z-index: var(--zindex-layout-ephemeral-node-backdrop);
}
&.animate { &.animate {
.tile-node, .tile-node,
.placeholder { .placeholder {

View File

@ -332,8 +332,7 @@ export interface LayoutNodeAdditionalProps {
rect?: Dimensions; rect?: Dimensions;
pixelToSizeRatio?: number; pixelToSizeRatio?: number;
resizeHandles?: ResizeHandleProps[]; resizeHandles?: ResizeHandleProps[];
isMagnifiedNode?: boolean; isLastEphemeralNode?: boolean;
isLastMagnifiedNode?: boolean;
} }
export interface NodeModel { export interface NodeModel {
@ -343,10 +342,12 @@ export interface NodeModel {
numLeafs: Atom<number>; numLeafs: Atom<number>;
nodeId: string; nodeId: string;
blockId: string; blockId: string;
addEphemeralNodeToLayout: () => void;
animationTimeS: Atom<number>; animationTimeS: Atom<number>;
isResizing: Atom<boolean>; isResizing: Atom<boolean>;
isFocused: Atom<boolean>; isFocused: Atom<boolean>;
isMagnified: Atom<boolean>; isMagnified: Atom<boolean>;
isEphemeral: Atom<boolean>;
ready: Atom<boolean>; ready: Atom<boolean>;
disablePointerEvents: Atom<boolean>; disablePointerEvents: Atom<boolean>;
toggleMagnify: () => void; toggleMagnify: () => void;

View File

@ -61,7 +61,8 @@ export function determineDropDirection(dimensions?: Dimensions, offset?: XYCoord
export function setTransform( export function setTransform(
{ top, left, width, height }: Dimensions, { top, left, width, height }: Dimensions,
setSize = true, setSize = true,
roundVals = true roundVals = true,
zIndex?: number | string
): CSSProperties { ): CSSProperties {
// Replace unitless items with px // Replace unitless items with px
const topRounded = roundVals ? Math.floor(top) : top; const topRounded = roundVals ? Math.floor(top) : top;
@ -80,6 +81,7 @@ export function setTransform(
width: setSize ? `${widthRounded}px` : undefined, width: setSize ? `${widthRounded}px` : undefined,
height: setSize ? `${heightRounded}px` : undefined, height: setSize ? `${heightRounded}px` : undefined,
position: "absolute", position: "absolute",
zIndex: zIndex,
}; };
} }