vdom terminal toolbar (#1263)

This commit is contained in:
Mike Sawka 2024-11-11 13:11:09 -08:00 committed by GitHub
parent 83f671c7a9
commit 3fc45c63f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 181 additions and 19 deletions

View File

@ -26,6 +26,10 @@
overflow: hidden;
min-height: 0;
padding: 5px;
&.block-no-padding {
padding: 0;
}
}
.block-focuselem {

View File

@ -25,12 +25,13 @@ import {
} from "@/store/global";
import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos";
import { focusedBlockId, getElemAsStr } from "@/util/focusutil";
import { isBlank } from "@/util/util";
import { isBlank, useAtomValueSafe } from "@/util/util";
import { HelpView, HelpViewModel, makeHelpViewModel } from "@/view/helpview/helpview";
import { QuickTipsView, QuickTipsViewModel } from "@/view/quicktipsview/quicktipsview";
import { TermViewModel, TerminalView, makeTerminalModel } from "@/view/term/term";
import { WaveAi, WaveAiModel, makeWaveAiViewModel } from "@/view/waveai/waveai";
import { WebView, WebViewModel, makeWebViewModel } from "@/view/webview/webview";
import clsx from "clsx";
import { atom, useAtomValue } from "jotai";
import { Suspense, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import "./block.less";
@ -154,11 +155,12 @@ const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => {
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel),
[nodeModel.blockId, blockData?.meta?.view, viewModel]
);
const noPadding = useAtomValueSafe(viewModel.noPadding);
if (!blockData) {
return null;
}
return (
<div key="content" className="block-content" ref={contentRef}>
<div key="content" className={clsx("block-content", { "block-no-padding": noPadding })} ref={contentRef}>
<ErrorBoundary>
<Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense>
</ErrorBoundary>
@ -176,6 +178,7 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
const isFocused = useAtomValue(nodeModel.isFocused);
const disablePointerEvents = useAtomValue(nodeModel.disablePointerEvents);
const innerRect = useDebouncedNodeInnerRect(nodeModel);
const noPadding = useAtomValueSafe(viewModel.noPadding);
useLayoutEffect(() => {
setBlockClicked(isFocused);
@ -273,7 +276,12 @@ const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
onChange={() => {}}
/>
</div>
<div key="content" className="block-content" ref={contentRef} style={blockContentStyle}>
<div
key="content"
className={clsx("block-content", { "block-no-padding": noPadding })}
ref={contentRef}
style={blockContentStyle}
>
<ErrorBoundary>
<Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense>
</ErrorBoundary>

View File

@ -41,6 +41,35 @@ export class TermWshClient extends WshClient {
magnified: data.target?.magnified,
});
return oref;
} else if (data.target?.toolbar?.toolbar) {
const oldVDomBlockId = globalStore.get(this.model.vdomToolbarBlockId);
console.log("vdom:toolbar", data.target.toolbar);
globalStore.set(this.model.vdomToolbarTarget, data.target.toolbar);
const oref = await RpcApi.CreateSubBlockCommand(this, {
parentblockid: this.blockId,
blockdef: {
meta: {
view: "vdom",
"vdom:route": rh.getSource(),
},
},
});
const [_, newVDomBlockId] = splitORef(oref);
if (!isBlank(oldVDomBlockId)) {
// dispose of the old vdom block
setTimeout(() => {
RpcApi.DeleteSubBlockCommand(this, { blockid: oldVDomBlockId });
}, 500);
}
setTimeout(() => {
RpcApi.SetMetaCommand(this, {
oref: makeORef("block", this.model.blockId),
meta: {
"term:vdomtoolbarblockid": newVDomBlockId,
},
});
}, 50);
return oref;
} else {
// in the terminal
// check if there is a current active vdom block

View File

@ -13,11 +13,10 @@
.view-term {
display: flex;
flex-direction: row;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
padding-left: 4px;
position: relative;
.term-header {
@ -31,11 +30,19 @@
border-bottom: 1px solid var(--border-color);
}
.term-toolbar {
height: 20px;
border-bottom: 1px solid var(--border-color);
overflow: hidden;
}
.term-connectelem {
flex-grow: 1;
min-height: 0;
overflow: hidden;
line-height: 1;
margin: 5px;
margin-left: 4px;
}
.term-htmlelem {

View File

@ -56,8 +56,11 @@ class TermViewModel {
termWshClient: TermWshClient;
shellProcStatusRef: React.MutableRefObject<string>;
vdomBlockId: jotai.Atom<string>;
vdomToolbarBlockId: jotai.Atom<string>;
vdomToolbarTarget: jotai.PrimitiveAtom<VDomTargetToolbar>;
fontSizeAtom: jotai.Atom<number>;
termThemeNameAtom: jotai.Atom<string>;
noPadding: jotai.PrimitiveAtom<boolean>;
constructor(blockId: string, nodeModel: BlockNodeModel) {
this.viewType = "term";
@ -70,6 +73,11 @@ class TermViewModel {
const blockData = get(this.blockAtom);
return blockData?.meta?.["term:vdomblockid"];
});
this.vdomToolbarBlockId = jotai.atom((get) => {
const blockData = get(this.blockAtom);
return blockData?.meta?.["term:vdomtoolbarblockid"];
});
this.vdomToolbarTarget = jotai.atom<VDomTargetToolbar>(null) as jotai.PrimitiveAtom<VDomTargetToolbar>;
this.termMode = jotai.atom((get) => {
const blockData = get(this.blockAtom);
return blockData?.meta?.["term:mode"] ?? "term";
@ -167,6 +175,7 @@ class TermViewModel {
return blockData?.meta?.["term:theme"] ?? get(settingsKeyAtom) ?? "default-dark";
});
});
this.noPadding = jotai.atom(true);
}
setTermMode(mode: "term" | "vdom") {
@ -191,6 +200,18 @@ class TermViewModel {
return bcm.viewModel as VDomModel;
}
getVDomToolbarModel(): VDomModel {
const vdomToolbarBlockId = globalStore.get(this.vdomToolbarBlockId);
if (!vdomToolbarBlockId) {
return null;
}
const bcm = getBlockComponentModel(vdomToolbarBlockId);
if (!bcm) {
return null;
}
return bcm.viewModel as VDomModel;
}
dispose() {
DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId));
}
@ -347,6 +368,15 @@ class TermViewModel {
prtn.catch((e) => console.log("error controller resync (force restart)", e));
},
});
if (blockData?.meta?.["term:vdomtoolbarblockid"]) {
fullMenu.push({ type: "separator" });
fullMenu.push({
label: "Close Toolbar",
click: () => {
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: blockData.meta["term:vdomtoolbarblockid"] });
},
});
}
return fullMenu;
}
}
@ -382,6 +412,44 @@ const TermResyncHandler = React.memo(({ blockId, model }: TerminalViewProps) =>
return null;
});
const TermVDomToolbarNode = ({ vdomBlockId, blockId, model }: TerminalViewProps & { vdomBlockId: string }) => {
React.useEffect(() => {
const unsub = waveEventSubscribe({
eventType: "blockclose",
scope: WOS.makeORef("block", vdomBlockId),
handler: (event) => {
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", blockId),
meta: {
"term:mode": null,
"term:vdomtoolbarblockid": null,
},
});
},
});
return () => {
unsub();
};
}, []);
let vdomNodeModel = {
blockId: vdomBlockId,
isFocused: jotai.atom(false),
focusNode: () => {},
onClose: () => {
if (vdomBlockId != null) {
RpcApi.DeleteSubBlockCommand(TabRpcClient, { blockid: vdomBlockId });
}
},
};
const toolbarTarget = jotai.useAtomValue(model.vdomToolbarTarget);
const heightStr = toolbarTarget?.height ?? "1.5em";
return (
<div key="vdomToolbar" className="term-toolbar" style={{ height: heightStr }}>
<SubBlock key="vdom" nodeModel={vdomNodeModel} />
</div>
);
};
const TermVDomNodeSingleId = ({ vdomBlockId, blockId, model }: TerminalViewProps & { vdomBlockId: string }) => {
React.useEffect(() => {
const unsub = waveEventSubscribe({
@ -431,6 +499,21 @@ const TermVDomNode = ({ blockId, model }: TerminalViewProps) => {
return <TermVDomNodeSingleId key={vdomBlockId} vdomBlockId={vdomBlockId} blockId={blockId} model={model} />;
};
const TermToolbarVDomNode = ({ blockId, model }: TerminalViewProps) => {
const vdomToolbarBlockId = jotai.useAtomValue(model.vdomToolbarBlockId);
if (vdomToolbarBlockId == null) {
return null;
}
return (
<TermVDomToolbarNode
key={vdomToolbarBlockId}
vdomBlockId={vdomToolbarBlockId}
blockId={blockId}
model={model}
/>
);
};
const TerminalView = ({ blockId, model }: TerminalViewProps) => {
const viewRef = React.useRef<HTMLDivElement>(null);
const connectElemRef = React.useRef<HTMLDivElement>(null);
@ -547,14 +630,14 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
cols: termRef.current?.terminal.cols ?? 80,
blockId: blockId,
};
return (
<div className={clsx("view-term", "term-mode-" + termMode)} ref={viewRef}>
<TermResyncHandler blockId={blockId} model={model} />
<TermThemeUpdater blockId={blockId} termRef={termRef} />
<TermStickers config={stickerConfig} />
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
<TermToolbarVDomNode key="vdom-toolbar" blockId={blockId} model={model} />
<TermVDomNode key="vdom" blockId={blockId} model={model} />
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
</div>
);
};

View File

@ -134,6 +134,7 @@ export class VDomModel {
refOutputStore: Map<string, any> = new Map();
globalVersion: jotai.PrimitiveAtom<number> = jotai.atom(0);
hasBackendWork: boolean = false;
noPadding: jotai.PrimitiveAtom<boolean>;
constructor(blockId: string, nodeModel: BlockNodeModel) {
this.viewType = "vdom";
@ -147,6 +148,7 @@ export class VDomModel {
const blockData = get(WOS.getWaveObjectAtom<Block>(makeORef("block", this.blockId)));
return blockData?.meta?.["vdom:route"];
});
this.noPadding = jotai.atom(true);
this.persist = getBlockMetaKeyAtom(this.blockId, "vdom:persist");
this.wshClient = new VDomWshClient(this);
DefaultRouter.registerRoute(this.wshClient.routeId, this.wshClient);

View File

@ -229,6 +229,7 @@ declare global {
endIconButtons?: jotai.Atom<IconButtonDecl[]>;
blockBg?: jotai.Atom<MetaType>;
manageConnection?: jotai.Atom<boolean>;
noPadding?: jotai.Atom<boolean>;
onBack?: () => void;
onForward?: () => void;

View File

@ -365,6 +365,7 @@ declare global {
"term:localshellopts"?: string[];
"term:scrollback"?: number;
"term:vdomblockid"?: string;
"term:vdomtoolbarblockid"?: string;
"vdom:*"?: boolean;
"vdom:initialized"?: boolean;
"vdom:correlationid"?: string;
@ -795,6 +796,13 @@ declare global {
type VDomTarget = {
newblock?: boolean;
magnified?: boolean;
toolbar?: VDomTargetToolbar;
};
// vdom.VDomTargetToolbar
type VDomTargetToolbar = {
toolbar: boolean;
height?: string;
};
// vdom.VDomTransferElem

View File

@ -207,6 +207,12 @@ type VDomMessage struct {
type VDomTarget struct {
NewBlock bool `json:"newblock,omitempty"`
Magnified bool `json:"magnified,omitempty"`
Toolbar *VDomTargetToolbar `json:"toolbar,omitempty"`
}
type VDomTargetToolbar struct {
Toolbar bool `json:"toolbar"`
Height string `json:"height,omitempty"`
}
// matches WaveKeyboardEvent

View File

@ -33,6 +33,8 @@ type AppOpts struct {
GlobalStyles []byte
RootComponentName string // defaults to "App"
NewBlockFlag string // defaults to "n" (set to "-" to disable)
TargetNewBlock bool
TargetToolbar *vdom.VDomTargetToolbar
}
type Client struct {
@ -116,7 +118,17 @@ func (client *Client) runMainE() error {
if err != nil {
return err
}
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: client.NewBlockFlag})
target := &vdom.VDomTarget{}
if client.AppOpts.TargetNewBlock || client.NewBlockFlag {
target.NewBlock = client.NewBlockFlag
}
if client.AppOpts.TargetToolbar != nil {
target.Toolbar = client.AppOpts.TargetToolbar
}
if target.NewBlock && target.Toolbar != nil {
return fmt.Errorf("cannot specify both new block and toolbar target")
}
err = client.CreateVDomContext(target)
if err != nil {
return err
}

View File

@ -85,6 +85,7 @@ const (
MetaKey_TermLocalShellOpts = "term:localshellopts"
MetaKey_TermScrollback = "term:scrollback"
MetaKey_TermVDomSubBlockId = "term:vdomblockid"
MetaKey_TermVDomToolbarBlockId = "term:vdomtoolbarblockid"
MetaKey_VDomClear = "vdom:*"
MetaKey_VDomInitialized = "vdom:initialized"

View File

@ -86,6 +86,7 @@ type MetaTSType struct {
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` // matches settings
TermScrollback *int `json:"term:scrollback,omitempty"`
TermVDomSubBlockId string `json:"term:vdomblockid,omitempty"`
TermVDomToolbarBlockId string `json:"term:vdomtoolbarblockid,omitempty"`
VDomClear bool `json:"vdom:*,omitempty"`
VDomInitialized bool `json:"vdom:initialized,omitempty"`