diff --git a/cmd/wsh/cmd/wshcmd-html.go b/cmd/wsh/cmd/wshcmd-html.go
index b17119f21..c7af59493 100644
--- a/cmd/wsh/cmd/wshcmd-html.go
+++ b/cmd/wsh/cmd/wshcmd-html.go
@@ -130,6 +130,8 @@ var App = vdomclient.DefineComponent[struct{}](HtmlVDomClient, "App",
vdom.E("div", nil,
vdom.E("wave:markdown",
vdom.P("text", "*quick vdom application to set background colors*"),
+ vdom.P("scrollable", false),
+ vdom.P("rehype", false),
),
),
vdom.E("div", nil,
diff --git a/frontend/app/element/markdown.tsx b/frontend/app/element/markdown.tsx
index ccbdf1004..fe2386bd2 100644
--- a/frontend/app/element/markdown.tsx
+++ b/frontend/app/element/markdown.tsx
@@ -180,6 +180,7 @@ type MarkdownProps = {
onClickExecute?: (cmd: string) => void;
resolveOpts?: MarkdownResolveOpts;
scrollable?: boolean;
+ rehype?: boolean;
};
const Markdown = ({
@@ -190,6 +191,7 @@ const Markdown = ({
className,
resolveOpts,
scrollable = true,
+ rehype = true,
onClickExecute,
}: MarkdownProps) => {
const textAtomValue = useAtomValueSafe(textAtom);
@@ -250,6 +252,29 @@ const Markdown = ({
}, [showToc, tocRef]);
text = textAtomValue ?? text;
+ let rehypePlugins = null;
+ if (rehype) {
+ rehypePlugins = [
+ rehypeRaw,
+ rehypeHighlight,
+ () =>
+ rehypeSanitize({
+ ...defaultSchema,
+ attributes: {
+ ...defaultSchema.attributes,
+ span: [
+ ...(defaultSchema.attributes?.span || []),
+ // Allow all class names starting with `hljs-`.
+ ["className", /^hljs-./],
+ // Alternatively, to allow only certain class names:
+ // ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
+ ],
+ },
+ tagNames: [...(defaultSchema.tagNames || []), "span"],
+ }),
+ () => rehypeSlug({ prefix: idPrefix }),
+ ];
+ }
const ScrollableMarkdown = () => {
return (
@@ -260,26 +285,7 @@ const Markdown = ({
>
- rehypeSanitize({
- ...defaultSchema,
- attributes: {
- ...defaultSchema.attributes,
- span: [
- ...(defaultSchema.attributes?.span || []),
- // Allow all class names starting with `hljs-`.
- ["className", /^hljs-./],
- // Alternatively, to allow only certain class names:
- // ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
- ],
- },
- tagNames: [...(defaultSchema.tagNames || []), "span"],
- }),
- () => rehypeSlug({ prefix: idPrefix }),
- ]}
+ rehypePlugins={rehypePlugins}
components={markdownComponents}
>
{text}
@@ -293,26 +299,7 @@ const Markdown = ({
- rehypeSanitize({
- ...defaultSchema,
- attributes: {
- ...defaultSchema.attributes,
- span: [
- ...(defaultSchema.attributes?.span || []),
- // Allow all class names starting with `hljs-`.
- ["className", /^hljs-./],
- // Alternatively, to allow only certain class names:
- // ['className', 'hljs-number', 'hljs-title', 'hljs-variable']
- ],
- },
- tagNames: [...(defaultSchema.tagNames || []), "span"],
- }),
- () => rehypeSlug({ prefix: idPrefix }),
- ]}
+ rehypePlugins={rehypePlugins}
components={markdownComponents}
>
{text}
diff --git a/frontend/app/view/vdom/vdom-model.tsx b/frontend/app/view/vdom/vdom-model.tsx
index 6e2ffe3f9..1d3520688 100644
--- a/frontend/app/view/vdom/vdom-model.tsx
+++ b/frontend/app/view/vdom/vdom-model.tsx
@@ -9,7 +9,7 @@ import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
import { RpcApi } from "@/app/store/wshclientapi";
import { makeFeBlockRouteId } from "@/app/store/wshrouter";
import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil";
-import { mergeBackendUpdates, restoreVDomElems } from "@/app/view/vdom/vdom-utils";
+import { applyCanvasOp, mergeBackendUpdates, restoreVDomElems } from "@/app/view/vdom/vdom-utils";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import debug from "debug";
import * as jotai from "jotai";
@@ -94,7 +94,7 @@ class VDomWshClient extends WshClient {
}
handle_vdomasyncinitiation(rh: RpcResponseHelper, data: VDomAsyncInitiationRequest) {
- console.log("async-initiation", rh.getSource(), data);
+ dlog("async-initiation", rh.getSource(), data);
this.model.queueUpdate(true);
}
}
@@ -130,6 +130,9 @@ export class VDomModel {
persist: jotai.Atom;
routeGoneUnsub: () => void;
routeConfirmed: boolean = false;
+ refOutputStore: Map = new Map();
+ globalVersion: jotai.PrimitiveAtom = jotai.atom(0);
+ hasBackendWork: boolean = false;
constructor(blockId: string, nodeModel: BlockNodeModel) {
this.viewType = "vdom";
@@ -201,6 +204,9 @@ export class VDomModel {
this.needsImmediateUpdate = false;
this.lastUpdateTs = 0;
this.queuedUpdate = null;
+ this.refOutputStore.clear();
+ this.globalVersion = jotai.atom(0);
+ this.hasBackendWork = false;
globalStore.set(this.contextActive, false);
}
@@ -537,6 +543,10 @@ export class VDomModel {
this.addErrorMessage(`Could not find ref with id ${refOp.refid}`);
continue;
}
+ if (elem instanceof HTMLCanvasElement) {
+ applyCanvasOp(elem, refOp, this.refOutputStore);
+ continue;
+ }
if (refOp.op == "focus") {
if (elem == null) {
this.addErrorMessage(`Could not focus ref with id ${refOp.refid}: elem is null`);
@@ -575,7 +585,17 @@ export class VDomModel {
}
}
}
+ globalStore.set(this.globalVersion, globalStore.get(this.globalVersion) + 1);
if (update.haswork) {
+ this.hasBackendWork = true;
+ }
+ }
+
+ renderDone(version: number) {
+ // called when the render is done
+ dlog("renderDone", version);
+ if (this.hasRefUpdates() || this.hasBackendWork) {
+ this.hasBackendWork = false;
this.queueUpdate(true);
}
}
diff --git a/frontend/app/view/vdom/vdom-utils.tsx b/frontend/app/view/vdom/vdom-utils.tsx
index 17a8e4475..ce6052def 100644
--- a/frontend/app/view/vdom/vdom-utils.tsx
+++ b/frontend/app/view/vdom/vdom-utils.tsx
@@ -197,3 +197,50 @@ export function mergeBackendUpdates(baseUpdate: VDomBackendUpdate, nextUpdate: V
baseUpdate.statesync.push(...nextUpdate.statesync);
}
}
+
+export function applyCanvasOp(canvas: HTMLCanvasElement, canvasOp: VDomRefOperation, refStore: Map) {
+ const ctx = canvas.getContext("2d");
+ if (!ctx) {
+ console.error("Canvas 2D context not available.");
+ return;
+ }
+
+ let { op, params, outputref } = canvasOp;
+ if (params == null) {
+ params = [];
+ }
+ if (op == null || op == "") {
+ return;
+ }
+ // Resolve any reference parameters in params
+ const resolvedParams: any[] = [];
+ params.forEach((param) => {
+ if (typeof param === "string" && param.startsWith("#ref:")) {
+ const refId = param.slice(5); // Remove "#ref:" prefix
+ resolvedParams.push(refStore.get(refId));
+ } else if (typeof param === "string" && param.startsWith("#spreadRef:")) {
+ const refId = param.slice(11); // Remove "#spreadRef:" prefix
+ const arrayRef = refStore.get(refId);
+ if (Array.isArray(arrayRef)) {
+ resolvedParams.push(...arrayRef); // Spread array elements
+ } else {
+ console.error(`Reference ${refId} is not an array and cannot be spread.`);
+ }
+ } else {
+ resolvedParams.push(param);
+ }
+ });
+
+ // Apply the operation on the canvas context
+ if (op === "dropRef" && params.length > 0 && typeof params[0] === "string") {
+ refStore.delete(params[0]);
+ } else if (op === "addRef" && outputref) {
+ refStore.set(outputref, resolvedParams[0]);
+ } else if (typeof ctx[op as keyof CanvasRenderingContext2D] === "function") {
+ (ctx[op as keyof CanvasRenderingContext2D] as Function).apply(ctx, resolvedParams);
+ } else if (op in ctx) {
+ (ctx as any)[op] = resolvedParams[0];
+ } else {
+ console.error(`Unsupported canvas operation: ${op}`);
+ }
+}
diff --git a/frontend/app/view/vdom/vdom.tsx b/frontend/app/view/vdom/vdom.tsx
index c4098dfcd..19c3fc4b3 100644
--- a/frontend/app/view/vdom/vdom.tsx
+++ b/frontend/app/view/vdom/vdom.tsx
@@ -72,6 +72,7 @@ const AllowedSimpleTags: { [tagName: string]: boolean } = {
br: true,
pre: true,
code: true,
+ canvas: true,
};
const AllowedSvgTags = {
@@ -350,7 +351,13 @@ function useVDom(model: VDomModel, elem: VDomElem): GenericPropsType {
function WaveMarkdown({ elem, model }: { elem: VDomElem; model: VDomModel }) {
const props = useVDom(model, elem);
return (
-
+
);
}
@@ -452,11 +459,15 @@ const testVDom: VDomElem = {
};
function VDomRoot({ model }: { model: VDomModel }) {
+ let version = jotai.useAtomValue(model.globalVersion);
let rootNode = jotai.useAtomValue(model.vdomRoot);
+ React.useEffect(() => {
+ model.renderDone(version);
+ }, [version]);
if (model.viewRef.current == null || rootNode == null) {
return null;
}
- dlog("render", rootNode);
+ dlog("render", version, rootNode);
let rtn = convertElemToTag(rootNode, model);
return {rtn}
;
}
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts
index d59b5c028..2954c8f8d 100644
--- a/frontend/types/gotypes.d.ts
+++ b/frontend/types/gotypes.d.ts
@@ -741,6 +741,7 @@ declare global {
refid: string;
op: string;
params?: any[];
+ outputref?: string;
};
// vdom.VDomRefPosition
diff --git a/pkg/vdom/vdom.go b/pkg/vdom/vdom.go
index 945dbdcb5..4266ddf60 100644
--- a/pkg/vdom/vdom.go
+++ b/pkg/vdom/vdom.go
@@ -346,6 +346,28 @@ func UseId(ctx context.Context) string {
return vc.Comp.WaveId
}
+func UseRenderTs(ctx context.Context) int64 {
+ vc := getRenderContext(ctx)
+ if vc == nil {
+ panic("UseRenderTs must be called within a component (no context)")
+ }
+ return vc.Root.RenderTs
+}
+
+func QueueRefOp(ctx context.Context, ref *VDomRef, op VDomRefOperation) {
+ if ref == nil || !ref.HasCurrent {
+ return
+ }
+ vc := getRenderContext(ctx)
+ if vc == nil {
+ panic("QueueRefOp must be called within a component (no context)")
+ }
+ if op.RefId == "" {
+ op.RefId = ref.RefId
+ }
+ vc.Root.QueueRefOp(op)
+}
+
func depsEqual(deps1 []any, deps2 []any) bool {
if len(deps1) != len(deps2) {
return false
diff --git a/pkg/vdom/vdom_root.go b/pkg/vdom/vdom_root.go
index 954edd9af..9bcbf7844 100644
--- a/pkg/vdom/vdom_root.go
+++ b/pkg/vdom/vdom_root.go
@@ -6,7 +6,10 @@ package vdom
import (
"context"
"fmt"
+ "log"
"reflect"
+ "strconv"
+ "strings"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
@@ -36,11 +39,13 @@ type Atom struct {
type RootElem struct {
OuterCtx context.Context
Root *ComponentImpl
+ RenderTs int64
CFuncs map[string]any
CompMap map[string]*ComponentImpl // component waveid -> component
EffectWorkQueue []*EffectWorkElem
NeedsRenderMap map[string]bool
Atoms map[string]*Atom
+ RefOperations []VDomRefOperation
}
const (
@@ -414,6 +419,49 @@ func (r *RootElem) renderComponent(cfunc any, elem *VDomElem, comp **ComponentIm
r.render(rtnElem, &(*comp).Comp)
}
+func (r *RootElem) UpdateRef(updateRef VDomRefUpdate) {
+ refId := updateRef.RefId
+ split := strings.SplitN(refId, ":", 2)
+ if len(split) != 2 {
+ log.Printf("invalid ref id: %s\n", refId)
+ return
+ }
+ waveId := split[0]
+ hookIdx, err := strconv.Atoi(split[1])
+ if err != nil {
+ log.Printf("invalid ref id (bad hook idx): %s\n", refId)
+ return
+ }
+ comp := r.CompMap[waveId]
+ if comp == nil {
+ return
+ }
+ if hookIdx < 0 || hookIdx >= len(comp.Hooks) {
+ return
+ }
+ hook := comp.Hooks[hookIdx]
+ if hook == nil {
+ return
+ }
+ ref, ok := hook.Val.(*VDomRef)
+ if !ok {
+ return
+ }
+ ref.HasCurrent = updateRef.HasCurrent
+ ref.Position = updateRef.Position
+ r.AddRenderWork(waveId)
+}
+
+func (r *RootElem) QueueRefOp(op VDomRefOperation) {
+ r.RefOperations = append(r.RefOperations, op)
+}
+
+func (r *RootElem) GetRefOperations() []VDomRefOperation {
+ ops := r.RefOperations
+ r.RefOperations = nil
+ return ops
+}
+
func convertPropsToVDom(props map[string]any) map[string]any {
if len(props) == 0 {
return nil
diff --git a/pkg/vdom/vdom_types.go b/pkg/vdom/vdom_types.go
index ae77fcd8a..a2d1ea6f9 100644
--- a/pkg/vdom/vdom_types.go
+++ b/pkg/vdom/vdom_types.go
@@ -188,9 +188,10 @@ type VDomRenderUpdate struct {
}
type VDomRefOperation struct {
- RefId string `json:"refid"`
- Op string `json:"op" tsype:"\"focus\""`
- Params []any `json:"params,omitempty"`
+ RefId string `json:"refid"`
+ Op string `json:"op"`
+ Params []any `json:"params,omitempty"`
+ OutputRef string `json:"outputref,omitempty"`
}
type VDomMessage struct {
diff --git a/pkg/vdom/vdomclient/vdomclient.go b/pkg/vdom/vdomclient/vdomclient.go
index ee26e0ecc..47a79c7e6 100644
--- a/pkg/vdom/vdomclient/vdomclient.go
+++ b/pkg/vdom/vdomclient/vdomclient.go
@@ -211,7 +211,8 @@ func (c *Client) fullRender() (*vdom.VDomBackendUpdate, error) {
RenderUpdates: []vdom.VDomRenderUpdate{
{UpdateType: "root", VDom: renderedVDom},
},
- StateSync: c.Root.GetStateSync(true),
+ RefOperations: c.Root.GetRefOperations(),
+ StateSync: c.Root.GetStateSync(true),
}, nil
}
@@ -228,7 +229,8 @@ func (c *Client) incrementalRender() (*vdom.VDomBackendUpdate, error) {
RenderUpdates: []vdom.VDomRenderUpdate{
{UpdateType: "root", VDom: renderedVDom},
},
- StateSync: c.Root.GetStateSync(false),
+ RefOperations: c.Root.GetRefOperations(),
+ StateSync: c.Root.GetStateSync(false),
}, nil
}
diff --git a/pkg/vdom/vdomclient/vdomserverimpl.go b/pkg/vdom/vdomclient/vdomserverimpl.go
index f79584e17..e14c2f476 100644
--- a/pkg/vdom/vdomclient/vdomserverimpl.go
+++ b/pkg/vdom/vdomclient/vdomserverimpl.go
@@ -46,6 +46,8 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
return respChan
}
+ impl.Client.Root.RenderTs = feUpdate.Ts
+
// set atoms
for _, ss := range feUpdate.StateSync {
impl.Client.Root.SetAtomVal(ss.Atom, ss.Value, false)
@@ -60,6 +62,10 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
impl.Client.Root.Event(event.WaveId, event.EventType, event)
}
}
+ // update refs
+ for _, ref := range feUpdate.RefUpdates {
+ impl.Client.Root.UpdateRef(ref)
+ }
var update *vdom.VDomBackendUpdate
var err error