Skip to content
Merged

VDom 10 #1206

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/wsh/cmd/wshcmd-html.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
67 changes: 27 additions & 40 deletions frontend/app/element/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ type MarkdownProps = {
onClickExecute?: (cmd: string) => void;
resolveOpts?: MarkdownResolveOpts;
scrollable?: boolean;
rehype?: boolean;
};

const Markdown = ({
Expand All @@ -190,6 +191,7 @@ const Markdown = ({
className,
resolveOpts,
scrollable = true,
rehype = true,
onClickExecute,
}: MarkdownProps) => {
const textAtomValue = useAtomValueSafe<string>(textAtom);
Expand Down Expand Up @@ -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 (
Expand All @@ -260,26 +285,7 @@ const Markdown = ({
>
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkAlert, [RemarkFlexibleToc, { tocRef: tocRef.current }]]}
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 }),
]}
rehypePlugins={rehypePlugins}
components={markdownComponents}
>
{text}
Expand All @@ -293,26 +299,7 @@ const Markdown = ({
<div className="content non-scrollable">
<ReactMarkdown
remarkPlugins={[remarkGfm, [RemarkFlexibleToc, { tocRef: tocRef.current }]]}
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 }),
]}
rehypePlugins={rehypePlugins}
components={markdownComponents}
>
{text}
Expand Down
24 changes: 22 additions & 2 deletions frontend/app/view/vdom/vdom-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -130,6 +130,9 @@ export class VDomModel {
persist: jotai.Atom<boolean>;
routeGoneUnsub: () => void;
routeConfirmed: boolean = false;
refOutputStore: Map<string, any> = new Map();
globalVersion: jotai.PrimitiveAtom<number> = jotai.atom(0);
hasBackendWork: boolean = false;

constructor(blockId: string, nodeModel: BlockNodeModel) {
this.viewType = "vdom";
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -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);
}
}
Expand Down
47 changes: 47 additions & 0 deletions frontend/app/view/vdom/vdom-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>) {
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}`);
}
}
15 changes: 13 additions & 2 deletions frontend/app/view/vdom/vdom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const AllowedSimpleTags: { [tagName: string]: boolean } = {
br: true,
pre: true,
code: true,
canvas: true,
};

const AllowedSvgTags = {
Expand Down Expand Up @@ -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 (
<Markdown text={props?.text} style={props?.style} className={props?.className} scrollable={props?.scrollable} />
<Markdown
text={props?.text}
style={props?.style}
className={props?.className}
scrollable={props?.scrollable}
rehype={props?.rehype}
/>
);
}

Expand Down Expand Up @@ -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 <div className="vdom">{rtn}</div>;
}
Expand Down
1 change: 1 addition & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@ declare global {
refid: string;
op: string;
params?: any[];
outputref?: string;
};

// vdom.VDomRefPosition
Expand Down
22 changes: 22 additions & 0 deletions pkg/vdom/vdom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions pkg/vdom/vdom_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ package vdom
import (
"context"
"fmt"
"log"
"reflect"
"strconv"
"strings"

"github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions pkg/vdom/vdom_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading