From 1a29a4ee0b29d7e1065c5e9da58eff3393bbb50d Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 3 Nov 2024 13:12:06 -0800 Subject: [PATCH 01/10] working on vdom events --- cmd/wsh/cmd/wshcmd-html.go | 11 +++- frontend/app/view/vdom/vdom-model.tsx | 78 ++++++++++++++++----------- frontend/types/custom.d.ts | 42 --------------- frontend/types/gotypes.d.ts | 43 ++++++++++++++- package.json | 2 + pkg/vdom/vdom_root.go | 20 ++++--- pkg/vdom/vdom_test.go | 2 +- pkg/vdom/vdom_types.go | 50 +++++++++++++++-- pkg/vdom/vdomclient/vdomserverimpl.go | 4 +- yarn.lock | 31 ++++++++++- 10 files changed, 192 insertions(+), 91 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-html.go b/cmd/wsh/cmd/wshcmd-html.go index e28ac73ae0..f9b7161b1b 100644 --- a/cmd/wsh/cmd/wshcmd-html.go +++ b/cmd/wsh/cmd/wshcmd-html.go @@ -138,6 +138,10 @@ func AllBgItemsTag(ctx context.Context, props map[string]any) any { } func MakeVDom() *vdom.VDomElem { + changeHandler := func(event vdom.VDomEvent) { + log.Printf("changeHandler: %v %q\n", event, event.TargetValue) + GlobalVDomClient.SetAtomVal("inputval", event.TargetValue) + } vdomStr := `
@@ -151,9 +155,13 @@ func MakeVDom() *vdom.VDomElem {
+
+ +
text
+
` - elem := vdom.Bind(vdomStr, nil) + elem := vdom.Bind(vdomStr, map[string]any{"changeHandler": changeHandler}) return elem } @@ -178,6 +186,7 @@ func htmlRun(cmd *cobra.Command, args []string) error { client.RegisterComponent("BgItemTag", BgItemTag) client.RegisterComponent("AllBgItemsTag", AllBgItemsTag) client.RegisterFileHandler("/test.png", "~/Downloads/IMG_1939.png") + client.SetAtomVal("inputval", "start") client.SetRootElem(MakeVDom()) err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: htmlCmdNewBlock}) if err != nil { diff --git a/frontend/app/view/vdom/vdom-model.tsx b/frontend/app/view/vdom/vdom-model.tsx index 44ab2e0f66..d6ecbb1347 100644 --- a/frontend/app/view/vdom/vdom-model.tsx +++ b/frontend/app/view/vdom/vdom-model.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { BlockNodeModel } from "@/app/block/blocktypes"; -import { getBlockMetaKeyAtom, globalStore, WOS } from "@/app/store/global"; +import { getBlockMetaKeyAtom, globalStore, PLATFORM, WOS } from "@/app/store/global"; import { makeORef } from "@/app/store/wos"; import { waveEventSubscribe } from "@/app/store/wps"; import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; @@ -43,24 +43,45 @@ function makeVDomIdMap(vdom: VDomElem, idMap: Map) { } } -function convertEvent(e: React.SyntheticEvent, fromProp: string): any { - if (e == null) { - return null; - } - if (fromProp == "onClick") { - return { type: "click" }; - } - if (fromProp == "onKeyDown") { - const waveKeyEvent = adaptFromReactOrNativeKeyEvent(e as React.KeyboardEvent); - return waveKeyEvent; +function annotateEvent(event: VDomEvent, propName: string, reactEvent: React.SyntheticEvent) { + if (reactEvent == null) { + return; } - if (fromProp == "onFocus") { - return { type: "focus" }; + if (propName == "onChange") { + const changeEvent = reactEvent as React.ChangeEvent; + event.targetvalue = changeEvent.target?.value; + event.targetchecked = changeEvent.target?.checked; + } + if (propName == "onClick" || propName == "onMouseDown") { + const mouseEvent = reactEvent as React.MouseEvent; + event.mousedata = { + button: mouseEvent.button, + buttons: mouseEvent.buttons, + alt: mouseEvent.altKey, + control: mouseEvent.ctrlKey, + shift: mouseEvent.shiftKey, + meta: mouseEvent.metaKey, + clientx: mouseEvent.clientX, + clienty: mouseEvent.clientY, + pagex: mouseEvent.pageX, + pagey: mouseEvent.pageY, + screenx: mouseEvent.screenX, + screeny: mouseEvent.screenY, + movementx: mouseEvent.movementX, + movementy: mouseEvent.movementY, + }; + if (PLATFORM == "darwin") { + event.mousedata.cmd = event.mousedata.meta; + event.mousedata.option = event.mousedata.alt; + } else { + event.mousedata.cmd = event.mousedata.alt; + event.mousedata.option = event.mousedata.meta; + } } - if (fromProp == "onBlur") { - return { type: "blur" }; + if (propName == "onKeyDown") { + const waveKeyEvent = adaptFromReactOrNativeKeyEvent(reactEvent as React.KeyboardEvent); + event.keydata = waveKeyEvent; } - return { type: "unknown" }; } class VDomWshClient extends WshClient { @@ -200,7 +221,7 @@ export class VDomModel { this.batchedEvents.push({ waveid: null, eventtype: "onKeyDown", - eventdata: e, + keydata: e, }); this.queueUpdate(); return true; @@ -542,23 +563,16 @@ export class VDomModel { } } - callVDomFunc(fnDecl: VDomFunc, e: any, compId: string, propName: string) { - const eventData = convertEvent(e, propName); + callVDomFunc(fnDecl: VDomFunc, e: React.SyntheticEvent, compId: string, propName: string) { + const vdomEvent: VDomEvent = { + waveid: compId, + eventtype: propName, + }; if (fnDecl.globalevent) { - const waveEvent: VDomEvent = { - waveid: null, - eventtype: fnDecl.globalevent, - eventdata: eventData, - }; - this.batchedEvents.push(waveEvent); - } else { - const vdomEvent: VDomEvent = { - waveid: compId, - eventtype: propName, - eventdata: eventData, - }; - this.batchedEvents.push(vdomEvent); + vdomEvent.globaleventtype = fnDecl.globalevent; } + annotateEvent(vdomEvent, propName, e); + this.batchedEvents.push(vdomEvent); this.queueUpdate(); } diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 42d4dc78d3..f3fb1279b8 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -134,48 +134,6 @@ declare global { keyType: string; }; - interface WaveKeyboardEvent { - type: "keydown" | "keyup" | "keypress" | "unknown"; - /** - * Equivalent to KeyboardEvent.key. - */ - key: string; - /** - * Equivalent to KeyboardEvent.code. - */ - code: string; - /** - * Equivalent to KeyboardEvent.shiftKey. - */ - shift: boolean; - /** - * Equivalent to KeyboardEvent.controlKey. - */ - control: boolean; - /** - * Equivalent to KeyboardEvent.altKey. - */ - alt: boolean; - /** - * Equivalent to KeyboardEvent.metaKey. - */ - meta: boolean; - /** - * cmd is special, on mac it is meta, on windows it is alt - */ - cmd: boolean; - /** - * option is special, on mac it is alt, on windows it is meta - */ - option: boolean; - - repeat: boolean; - /** - * Equivalent to KeyboardEvent.location. - */ - location: number; - } - type SubjectWithRef = rxjs.Subject & { refCount: number; release: () => void }; type HeaderElem = diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 8c0f80e932..ba8aeb5532 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -684,7 +684,13 @@ declare global { type VDomEvent = { waveid: string; eventtype: string; - eventdata: any; + globaleventtype?: string; + targetvalue?: string; + targetchecked?: boolean; + targetname?: string; + targetid?: string; + keydata?: WaveKeyboardEvent; + mousedata?: WavePointerData; }; // vdom.VDomFrontendUpdate @@ -855,6 +861,21 @@ declare global { datadir: string; }; + // vdom.WaveKeyboardEvent + type WaveKeyboardEvent = { + type: "keydown"|"keyup"|"keypress"|"unknown"; + key: string; + code: string; + repeat?: boolean; + location?: number; + shift?: boolean; + control?: boolean; + alt?: boolean; + meta?: boolean; + cmd?: boolean; + option?: boolean; + }; + // wshrpc.WaveNotificationOptions type WaveNotificationOptions = { title?: string; @@ -878,6 +899,26 @@ declare global { obj?: WaveObj; }; + // vdom.WavePointerData + type WavePointerData = { + button: number; + buttons: number; + clientx?: number; + clienty?: number; + pagex?: number; + pagey?: number; + screenx?: number; + screeny?: number; + movementx?: number; + movementy?: number; + shift?: boolean; + control?: boolean; + alt?: boolean; + meta?: boolean; + cmd?: boolean; + option?: boolean; + }; + // waveobj.Window type WaveWindow = WaveObj & { workspaceid: string; diff --git a/package.json b/package.json index 96386689a9..c8f16eab37 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@types/node": "^22.8.0", "@types/papaparse": "^5", "@types/pngjs": "^6.0.5", + "@types/prop-types": "^15", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/semver": "^7", @@ -115,6 +116,7 @@ "overlayscrollbars-react": "^0.5.6", "papaparse": "^5.4.1", "pngjs": "^7.0.0", + "prop-types": "^15.8.1", "react": "^18.3.1", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", diff --git a/pkg/vdom/vdom_root.go b/pkg/vdom/vdom_root.go index 3f26bc660c..f1d58066d6 100644 --- a/pkg/vdom/vdom_root.go +++ b/pkg/vdom/vdom_root.go @@ -6,7 +6,6 @@ package vdom import ( "context" "fmt" - "log" "reflect" "github.com/google/uuid" @@ -151,11 +150,10 @@ func (r *RootElem) RegisterComponent(name string, cfunc any) error { } func (r *RootElem) Render(elem *VDomElem) { - log.Printf("Render %s\n", elem.Tag) r.render(elem, &r.Root) } -func (vdf *VDomFunc) CallFn() { +func (vdf *VDomFunc) CallFn(event VDomEvent) { if vdf.Fn == nil { return } @@ -163,10 +161,18 @@ func (vdf *VDomFunc) CallFn() { if rval.Kind() != reflect.Func { return } - rval.Call(nil) + rtype := rval.Type() + if rtype.NumIn() == 0 { + rval.Call(nil) + } + if rtype.NumIn() == 1 { + if rtype.In(0) == reflect.TypeOf((*VDomEvent)(nil)).Elem() { + rval.Call([]reflect.Value{reflect.ValueOf(event)}) + } + } } -func callVDomFn(fnVal any, data any) { +func callVDomFn(fnVal any, data VDomEvent) { if fnVal == nil { return } @@ -192,13 +198,13 @@ func callVDomFn(fnVal any, data any) { } } -func (r *RootElem) Event(id string, propName string, data any) { +func (r *RootElem) Event(id string, propName string, event VDomEvent) { comp := r.CompMap[id] if comp == nil || comp.Elem == nil { return } fnVal := comp.Elem.Props[propName] - callVDomFn(fnVal, data) + callVDomFn(fnVal, event) } // this will be called by the frontend to say the DOM has been mounted diff --git a/pkg/vdom/vdom_test.go b/pkg/vdom/vdom_test.go index 42e8214d8e..db56a4415a 100644 --- a/pkg/vdom/vdom_test.go +++ b/pkg/vdom/vdom_test.go @@ -90,7 +90,7 @@ func Test1(t *testing.T) { printVDom(root) root.RunWork() printVDom(root) - root.Event(testContext.ButtonId, "onClick", nil) + root.Event(testContext.ButtonId, "onClick", VDomEvent{EventType: "onClick"}) root.RunWork() printVDom(root) } diff --git a/pkg/vdom/vdom_types.go b/pkg/vdom/vdom_types.go index 1c09d28175..252a25fb67 100644 --- a/pkg/vdom/vdom_types.go +++ b/pkg/vdom/vdom_types.go @@ -128,9 +128,15 @@ type VDomRefPosition struct { ///// subbordinate protocol types type VDomEvent struct { - WaveId string `json:"waveid"` // empty for global events - EventType string `json:"eventtype"` - EventData any `json:"eventdata"` + WaveId string `json:"waveid"` + EventType string `json:"eventtype"` // usually the prop name (e.g. onClick, onKeyDown) + GlobalEventType string `json:"globaleventtype,omitempty"` + TargetValue string `json:"targetvalue,omitempty"` + TargetChecked bool `json:"targetchecked,omitempty"` + TargetName string `json:"targetname,omitempty"` + TargetId string `json:"targetid,omitempty"` + KeyData *WaveKeyboardEvent `json:"keydata,omitempty"` + MouseData *WavePointerData `json:"mousedata,omitempty"` } type VDomRenderContext struct { @@ -199,3 +205,41 @@ type VDomKeyboardEvent struct { Repeat bool `json:"repeat,omitempty"` Location int `json:"location,omitempty"` } + +type WaveKeyboardEvent struct { + Type string `json:"type" tstype:"\"keydown\"|\"keyup\"|\"keypress\"|\"unknown\""` + Key string `json:"key"` // KeyboardEvent.key + Code string `json:"code"` // KeyboardEvent.code + Repeat bool `json:"repeat,omitempty"` + Location int `json:"location,omitempty"` // KeyboardEvent.location + + // modifiers + Shift bool `json:"shift,omitempty"` + Control bool `json:"control,omitempty"` + Alt bool `json:"alt,omitempty"` + Meta bool `json:"meta,omitempty"` + Cmd bool `json:"cmd,omitempty"` // special (on mac it is meta, on windows/linux it is alt) + Option bool `json:"option,omitempty"` // special (on mac it is alt, on windows/linux it is meta) +} + +type WavePointerData struct { + Button int `json:"button"` + Buttons int `json:"buttons"` + + ClientX int `json:"clientx,omitempty"` + ClientY int `json:"clienty,omitempty"` + PageX int `json:"pagex,omitempty"` + PageY int `json:"pagey,omitempty"` + ScreenX int `json:"screenx,omitempty"` + ScreenY int `json:"screeny,omitempty"` + MovementX int `json:"movementx,omitempty"` + MovementY int `json:"movementy,omitempty"` + + // Modifiers + Shift bool `json:"shift,omitempty"` + Control bool `json:"control,omitempty"` + Alt bool `json:"alt,omitempty"` + Meta bool `json:"meta,omitempty"` + Cmd bool `json:"cmd,omitempty"` // special (on mac it is meta, on windows/linux it is alt) + Option bool `json:"option,omitempty"` // special (on mac it is alt, on windows/linux it is meta) +} diff --git a/pkg/vdom/vdomclient/vdomserverimpl.go b/pkg/vdom/vdomclient/vdomserverimpl.go index 2d8e352b8d..e97808d998 100644 --- a/pkg/vdom/vdomclient/vdomserverimpl.go +++ b/pkg/vdom/vdomclient/vdomserverimpl.go @@ -36,12 +36,12 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom } // run events for _, event := range feUpdate.Events { - if event.WaveId == "" { + if event.GlobalEventType != "" { if impl.Client.GlobalEventHandler != nil { impl.Client.GlobalEventHandler(impl.Client, event) } } else { - impl.Client.Root.Event(event.WaveId, event.EventType, event.EventData) + impl.Client.Root.Event(event.WaveId, event.EventType, event) } } if feUpdate.Resync { diff --git a/yarn.lock b/yarn.lock index cf7124bbad..f0dd2816d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2771,6 +2771,13 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:^15": + version: 15.7.13 + resolution: "@types/prop-types@npm:15.7.13" + checksum: 10c0/1b20fc67281902c6743379960247bc161f3f0406ffc0df8e7058745a85ea1538612109db0406290512947f9632fe9e10e7337bf0ce6338a91d6c948df16a7c61 + languageName: node + linkType: hard + "@types/react-dom@npm:^18.3.1": version: 18.3.1 resolution: "@types/react-dom@npm:18.3.1" @@ -7274,7 +7281,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.1.0": +"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -8531,6 +8538,13 @@ __metadata: languageName: node linkType: hard +"object-assign@npm:^4.1.1": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 + languageName: node + linkType: hard + "object-keys@npm:^1.1.1": version: 1.1.1 resolution: "object-keys@npm:1.1.1" @@ -8972,6 +8986,17 @@ __metadata: languageName: node linkType: hard +"prop-types@npm:^15.8.1": + version: 15.8.1 + resolution: "prop-types@npm:15.8.1" + dependencies: + loose-envify: "npm:^1.4.0" + object-assign: "npm:^4.1.1" + react-is: "npm:^16.13.1" + checksum: 10c0/59ece7ca2fb9838031d73a48d4becb9a7cc1ed10e610517c7d8f19a1e02fa47f7c27d557d8a5702bec3cfeccddc853579832b43f449e54635803f277b1c78077 + languageName: node + linkType: hard + "property-information@npm:^6.0.0": version: 6.5.0 resolution: "property-information@npm:6.5.0" @@ -9124,7 +9149,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.7.0": +"react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1 @@ -11082,6 +11107,7 @@ __metadata: "@types/node": "npm:^22.8.0" "@types/papaparse": "npm:^5" "@types/pngjs": "npm:^6.0.5" + "@types/prop-types": "npm:^15" "@types/react": "npm:^18.3.12" "@types/react-dom": "npm:^18.3.1" "@types/semver": "npm:^7" @@ -11127,6 +11153,7 @@ __metadata: prettier: "npm:^3.3.3" prettier-plugin-jsdoc: "npm:^1.3.0" prettier-plugin-organize-imports: "npm:^4.1.0" + prop-types: "npm:^15.8.1" react: "npm:^18.3.1" react-dnd: "npm:^16.0.1" react-dnd-html5-backend: "npm:^16.0.1" From 7077a207bb6d3d5edb19408109f051d6ed1a1565 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 3 Nov 2024 13:15:46 -0800 Subject: [PATCH 02/10] queue updates immediately for events --- frontend/app/view/vdom/vdom-model.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/view/vdom/vdom-model.tsx b/frontend/app/view/vdom/vdom-model.tsx index d6ecbb1347..e7db45759a 100644 --- a/frontend/app/view/vdom/vdom-model.tsx +++ b/frontend/app/view/vdom/vdom-model.tsx @@ -573,7 +573,7 @@ export class VDomModel { } annotateEvent(vdomEvent, propName, e); this.batchedEvents.push(vdomEvent); - this.queueUpdate(); + this.queueUpdate(true); } createFeUpdate(): VDomFrontendUpdate { From 92ce8294c17994218fede291c10e487ba8059170 Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 3 Nov 2024 19:39:00 -0800 Subject: [PATCH 03/10] new MapToStruct fn instead of Reunmarshal() --- pkg/util/utilfn/marshal.go | 113 +++++++++++++++++++++++++++++++++++++ pkg/util/utilfn/utilfn.go | 23 -------- pkg/vdom/vdom_root.go | 5 +- 3 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 pkg/util/utilfn/marshal.go diff --git a/pkg/util/utilfn/marshal.go b/pkg/util/utilfn/marshal.go new file mode 100644 index 0000000000..5223d0368b --- /dev/null +++ b/pkg/util/utilfn/marshal.go @@ -0,0 +1,113 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package utilfn + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + + "github.com/mitchellh/mapstructure" +) + +func ReUnmarshal(out any, in any) error { + barr, err := json.Marshal(in) + if err != nil { + return err + } + return json.Unmarshal(barr, out) +} + +// does a mapstructure using "json" tags +func DoMapStructure(out any, input any) error { + dconfig := &mapstructure.DecoderConfig{ + Result: out, + TagName: "json", + } + decoder, err := mapstructure.NewDecoder(dconfig) + if err != nil { + return err + } + return decoder.Decode(input) +} + +func MapToStruct(in map[string]any, out any) error { + // Check that out is a pointer + outValue := reflect.ValueOf(out) + if outValue.Kind() != reflect.Ptr { + return fmt.Errorf("out parameter must be a pointer, got %v", outValue.Kind()) + } + + // Get the struct it points to + elem := outValue.Elem() + if elem.Kind() != reflect.Struct { + return fmt.Errorf("out parameter must be a pointer to struct, got pointer to %v", elem.Kind()) + } + + // Get type information + typ := elem.Type() + + // For each field in the struct + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + + // Skip unexported fields + if !field.IsExported() { + continue + } + + name := getJSONName(field) + if value, ok := in[name]; ok { + if err := setValue(elem.Field(i), value); err != nil { + return fmt.Errorf("error setting field %s: %w", name, err) + } + } + } + + return nil +} + +// getJSONName returns the field name to use for JSON mapping +func getJSONName(field reflect.StructField) string { + tag := field.Tag.Get("json") + if tag == "" || tag == "-" { + return field.Name + } + return strings.Split(tag, ",")[0] +} + +// setValue attempts to set a reflect.Value with a given interface{} value +func setValue(field reflect.Value, value any) error { + if value == nil { + return nil + } + + valueRef := reflect.ValueOf(value) + + // Direct assignment if types are exactly equal + if valueRef.Type() == field.Type() { + field.Set(valueRef) + return nil + } + + // Check if types are assignable + if valueRef.Type().AssignableTo(field.Type()) { + field.Set(valueRef) + return nil + } + + // If field is pointer and value isn't already a pointer, try address + if field.Kind() == reflect.Ptr && valueRef.Kind() != reflect.Ptr { + return setValue(field, valueRef.Addr().Interface()) + } + + // Try conversion if types are convertible + if valueRef.Type().ConvertibleTo(field.Type()) { + field.Set(valueRef.Convert(field.Type())) + return nil + } + + return fmt.Errorf("cannot set value of type %v to field of type %v", valueRef.Type(), field.Type()) +} diff --git a/pkg/util/utilfn/utilfn.go b/pkg/util/utilfn/utilfn.go index 914dc4e97e..2e3acdfeb8 100644 --- a/pkg/util/utilfn/utilfn.go +++ b/pkg/util/utilfn/utilfn.go @@ -29,8 +29,6 @@ import ( "syscall" "text/template" "unicode/utf8" - - "github.com/mitchellh/mapstructure" ) var HexDigits = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'} @@ -751,27 +749,6 @@ func IndentString(indent string, str string) string { return rtn.String() } -func ReUnmarshal(out any, in any) error { - barr, err := json.Marshal(in) - if err != nil { - return err - } - return json.Unmarshal(barr, out) -} - -// does a mapstructure using "json" tags -func DoMapStructure(out any, input any) error { - dconfig := &mapstructure.DecoderConfig{ - Result: out, - TagName: "json", - } - decoder, err := mapstructure.NewDecoder(dconfig) - if err != nil { - return err - } - return decoder.Decode(input) -} - func SliceIdx[T comparable](arr []T, elem T) int { for idx, e := range arr { if e == elem { diff --git a/pkg/vdom/vdom_root.go b/pkg/vdom/vdom_root.go index f1d58066d6..59f9331d45 100644 --- a/pkg/vdom/vdom_root.go +++ b/pkg/vdom/vdom_root.go @@ -365,7 +365,10 @@ func callCFunc(cfunc any, ctx context.Context, props map[string]any) any { rval := reflect.ValueOf(cfunc) arg2Type := rval.Type().In(1) arg2Val := reflect.New(arg2Type) - utilfn.ReUnmarshal(arg2Val.Interface(), props) + err := utilfn.MapToStruct(props, arg2Val.Interface()) + if err != nil { + fmt.Printf("error unmarshalling props: %v\n", err) + } rtnVal := rval.Call([]reflect.Value{reflect.ValueOf(ctx), arg2Val.Elem()}) if len(rtnVal) == 0 { return nil From fe4f2021bfdee6b0257a587baaa034bfb133007a Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 3 Nov 2024 19:51:52 -0800 Subject: [PATCH 04/10] pass a struct as props --- pkg/util/utilfn/marshal.go | 32 ++++++++++++++++++++++++++++++++ pkg/vdom/vdom.go | 10 ++++++++++ 2 files changed, 42 insertions(+) diff --git a/pkg/util/utilfn/marshal.go b/pkg/util/utilfn/marshal.go index 5223d0368b..fca0dd3736 100644 --- a/pkg/util/utilfn/marshal.go +++ b/pkg/util/utilfn/marshal.go @@ -69,6 +69,38 @@ func MapToStruct(in map[string]any, out any) error { return nil } +func StructToMap(in any) (map[string]any, error) { + // Get value and handle pointer + val := reflect.ValueOf(in) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + // Check that we have a struct + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("input must be a struct or pointer to struct, got %v", val.Kind()) + } + + // Get type information + typ := val.Type() + out := make(map[string]any) + + // For each field in the struct + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + + // Skip unexported fields + if !field.IsExported() { + continue + } + + name := getJSONName(field) + out[name] = val.Field(i).Interface() + } + + return out, nil +} + // getJSONName returns the field name to use for JSON mapping func getJSONName(field reflect.StructField) string { tag := field.Tag.Get("json") diff --git a/pkg/vdom/vdom.go b/pkg/vdom/vdom.go index d7c66177a5..fdb1e03c38 100644 --- a/pkg/vdom/vdom.go +++ b/pkg/vdom/vdom.go @@ -11,6 +11,8 @@ import ( "strconv" "strings" "unicode" + + "github.com/wavetermdev/waveterm/pkg/util/utilfn" ) // ReactNode types = nil | string | Elem @@ -71,6 +73,14 @@ func E(tag string, parts ...any) *VDomElem { return rtn } +func Props(props any) map[string]any { + m, err := utilfn.StructToMap(props) + if err != nil { + return nil + } + return m +} + func P(propName string, propVal any) map[string]any { return map[string]any{propName: propVal} } From dced12567571995c315caa2cfc357a9b3b1e75fb Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 3 Nov 2024 20:01:19 -0800 Subject: [PATCH 05/10] fixing bugs --- frontend/app/view/vdom/vdom.tsx | 3 ++- frontend/types/gotypes.d.ts | 2 +- pkg/vdom/vdom_types.go | 2 +- pkg/vdom/vdomclient/vdomserverimpl.go | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/app/view/vdom/vdom.tsx b/frontend/app/view/vdom/vdom.tsx index 89fd6f14d5..786ba13d56 100644 --- a/frontend/app/view/vdom/vdom.tsx +++ b/frontend/app/view/vdom/vdom.tsx @@ -145,8 +145,9 @@ const SvgUrlIdAttributes = { function convertVDomFunc(model: VDomModel, fnDecl: VDomFunc, compId: string, propName: string): (e: any) => void { return (e: any) => { if ((propName == "onKeyDown" || propName == "onKeyDownCapture") && fnDecl["#keys"]) { + dlog("key event", fnDecl, e); let waveEvent = adaptFromReactOrNativeKeyEvent(e); - for (let keyDesc of fnDecl.keys || []) { + for (let keyDesc of fnDecl["#keys"] || []) { if (checkKeyPressed(waveEvent, keyDesc)) { e.preventDefault(); e.stopPropagation(); diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index ba8aeb5532..379a662050 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -714,7 +714,7 @@ declare global { stoppropagation?: boolean; preventdefault?: boolean; globalevent?: string; - keys?: string[]; + #keys?: string[]; }; // vdom.VDomMessage diff --git a/pkg/vdom/vdom_types.go b/pkg/vdom/vdom_types.go index 252a25fb67..e39da5f039 100644 --- a/pkg/vdom/vdom_types.go +++ b/pkg/vdom/vdom_types.go @@ -95,7 +95,7 @@ type VDomFunc struct { StopPropagation bool `json:"stoppropagation,omitempty"` PreventDefault bool `json:"preventdefault,omitempty"` GlobalEvent string `json:"globalevent,omitempty"` - Keys []string `json:"keys,omitempty"` // special for keyDown events a list of keys to "capture" + Keys []string `json:"#keys,omitempty"` // special for keyDown events a list of keys to "capture" } // used in props diff --git a/pkg/vdom/vdomclient/vdomserverimpl.go b/pkg/vdom/vdomclient/vdomserverimpl.go index e97808d998..69dc34c381 100644 --- a/pkg/vdom/vdomclient/vdomserverimpl.go +++ b/pkg/vdom/vdomclient/vdomserverimpl.go @@ -44,7 +44,7 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom impl.Client.Root.Event(event.WaveId, event.EventType, event) } } - if feUpdate.Resync { + if feUpdate.Resync || true { return impl.Client.fullRender() } return impl.Client.incrementalRender() From 0f1cba7dcd9548e72e7965e8d28e75e3184fd97c Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 3 Nov 2024 22:40:50 -0800 Subject: [PATCH 06/10] checkpoint, working on definecomponent --- pkg/vdom/vdom.go | 2 ++ pkg/vdom/vdom_comp.go | 8 ++--- pkg/vdom/vdom_root.go | 51 +++++++++++++++++-------------- pkg/vdom/vdomclient/vdomclient.go | 15 +++++++++ 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/pkg/vdom/vdom.go b/pkg/vdom/vdom.go index fdb1e03c38..26a62ee00d 100644 --- a/pkg/vdom/vdom.go +++ b/pkg/vdom/vdom.go @@ -27,6 +27,8 @@ type Hook struct { Deps []any } +type Component[P any] func(props P) *VDomElem + func (e *VDomElem) Key() string { keyVal, ok := e.Props[KeyPropKey] if !ok { diff --git a/pkg/vdom/vdom_comp.go b/pkg/vdom/vdom_comp.go index 118375b369..12392ab983 100644 --- a/pkg/vdom/vdom_comp.go +++ b/pkg/vdom/vdom_comp.go @@ -12,7 +12,7 @@ type ChildKey struct { Key string } -type Component struct { +type ComponentImpl struct { WaveId string Tag string Key string @@ -26,13 +26,13 @@ type Component struct { Text string // base component -- vdom, wave elem, or #fragment - Children []*Component + Children []*ComponentImpl // component -> component - Comp *Component + Comp *ComponentImpl } -func (c *Component) compMatch(tag string, key string) bool { +func (c *ComponentImpl) compMatch(tag string, key string) bool { if c == nil { return false } diff --git a/pkg/vdom/vdom_root.go b/pkg/vdom/vdom_root.go index 59f9331d45..03f9356afc 100644 --- a/pkg/vdom/vdom_root.go +++ b/pkg/vdom/vdom_root.go @@ -18,7 +18,7 @@ var vdomContextKey = vdomContextKeyType{} type VDomContextVal struct { Root *RootElem - Comp *Component + Comp *ComponentImpl HookIdx int } @@ -30,9 +30,9 @@ type Atom struct { type RootElem struct { OuterCtx context.Context - Root *Component + Root *ComponentImpl CFuncs map[string]any - CompMap map[string]*Component // component waveid -> component + CompMap map[string]*ComponentImpl // component waveid -> component EffectWorkQueue []*EffectWorkElem NeedsRenderMap map[string]bool Atoms map[string]*Atom @@ -63,7 +63,7 @@ func MakeRoot() *RootElem { return &RootElem{ Root: nil, CFuncs: make(map[string]any), - CompMap: make(map[string]*Component), + CompMap: make(map[string]*ComponentImpl), Atoms: make(map[string]*Atom), } } @@ -241,7 +241,7 @@ func (r *RootElem) RunWork() { } } -func (r *RootElem) render(elem *VDomElem, comp **Component) { +func (r *RootElem) render(elem *VDomElem, comp **ComponentImpl) { if elem == nil || elem.Tag == "" { r.unmount(comp) return @@ -270,7 +270,7 @@ func (r *RootElem) render(elem *VDomElem, comp **Component) { r.renderComponent(cfunc, elem, comp) } -func (r *RootElem) unmount(comp **Component) { +func (r *RootElem) unmount(comp **ComponentImpl) { if *comp == nil { return } @@ -293,21 +293,21 @@ func (r *RootElem) unmount(comp **Component) { *comp = nil } -func (r *RootElem) createComp(tag string, key string, comp **Component) { - *comp = &Component{WaveId: uuid.New().String(), Tag: tag, Key: key} +func (r *RootElem) createComp(tag string, key string, comp **ComponentImpl) { + *comp = &ComponentImpl{WaveId: uuid.New().String(), Tag: tag, Key: key} r.CompMap[(*comp).WaveId] = *comp } -func (r *RootElem) renderText(text string, comp **Component) { +func (r *RootElem) renderText(text string, comp **ComponentImpl) { if (*comp).Text != text { (*comp).Text = text } } -func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*Component) []*Component { - newChildren := make([]*Component, len(elems)) - curCM := make(map[ChildKey]*Component) - usedMap := make(map[*Component]bool) +func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*ComponentImpl) []*ComponentImpl { + newChildren := make([]*ComponentImpl, len(elems)) + curCM := make(map[ChildKey]*ComponentImpl) + usedMap := make(map[*ComponentImpl]bool) for idx, child := range curChildren { if child.Key != "" { curCM[ChildKey{Tag: child.Tag, Idx: 0, Key: child.Key}] = child @@ -317,7 +317,7 @@ func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*Component) [] } for idx, elem := range elems { elemKey := elem.Key() - var curChild *Component + var curChild *ComponentImpl if elemKey != "" { curChild = curCM[ChildKey{Tag: elem.Tag, Idx: 0, Key: elemKey}] } else { @@ -335,14 +335,14 @@ func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*Component) [] return newChildren } -func (r *RootElem) renderSimple(elem *VDomElem, comp **Component) { +func (r *RootElem) renderSimple(elem *VDomElem, comp **ComponentImpl) { if (*comp).Comp != nil { r.unmount(&(*comp).Comp) } (*comp).Children = r.renderChildren(elem.Children, (*comp).Children) } -func (r *RootElem) makeRenderContext(comp *Component) context.Context { +func (r *RootElem) makeRenderContext(comp *ComponentImpl) context.Context { var ctx context.Context if r.OuterCtx != nil { ctx = r.OuterCtx @@ -365,9 +365,14 @@ func callCFunc(cfunc any, ctx context.Context, props map[string]any) any { rval := reflect.ValueOf(cfunc) arg2Type := rval.Type().In(1) arg2Val := reflect.New(arg2Type) - err := utilfn.MapToStruct(props, arg2Val.Interface()) - if err != nil { - fmt.Printf("error unmarshalling props: %v\n", err) + // if arg2 is a map, just pass props + if arg2Type.Kind() == reflect.Map { + arg2Val.Elem().Set(reflect.ValueOf(props)) + } else { + err := utilfn.MapToStruct(props, arg2Val.Interface()) + if err != nil { + fmt.Printf("error unmarshalling props: %v\n", err) + } } rtnVal := rval.Call([]reflect.Value{reflect.ValueOf(ctx), arg2Val.Elem()}) if len(rtnVal) == 0 { @@ -376,7 +381,7 @@ func callCFunc(cfunc any, ctx context.Context, props map[string]any) any { return rtnVal[0].Interface() } -func (r *RootElem) renderComponent(cfunc any, elem *VDomElem, comp **Component) { +func (r *RootElem) renderComponent(cfunc any, elem *VDomElem, comp **ComponentImpl) { if (*comp).Children != nil { for _, child := range (*comp).Children { r.unmount(&child) @@ -423,7 +428,7 @@ func convertPropsToVDom(props map[string]any) map[string]any { return vdomProps } -func convertBaseToVDom(c *Component) *VDomElem { +func convertBaseToVDom(c *ComponentImpl) *VDomElem { elem := &VDomElem{WaveId: c.WaveId, Tag: c.Tag} if c.Elem != nil { elem.Props = convertPropsToVDom(c.Elem.Props) @@ -437,7 +442,7 @@ func convertBaseToVDom(c *Component) *VDomElem { return elem } -func convertToVDom(c *Component) *VDomElem { +func convertToVDom(c *ComponentImpl) *VDomElem { if c == nil { return nil } @@ -451,7 +456,7 @@ func convertToVDom(c *Component) *VDomElem { } } -func (r *RootElem) makeVDom(comp *Component) *VDomElem { +func (r *RootElem) makeVDom(comp *ComponentImpl) *VDomElem { vdomElem := convertToVDom(comp) return vdomElem } diff --git a/pkg/vdom/vdomclient/vdomclient.go b/pkg/vdom/vdomclient/vdomclient.go index b21bfdbed8..fcac91fd26 100644 --- a/pkg/vdom/vdomclient/vdomclient.go +++ b/pkg/vdom/vdomclient/vdomclient.go @@ -4,12 +4,14 @@ package vdomclient import ( + "context" "fmt" "log" "net/http" "os" "sync" "time" + "unicode" "github.com/google/uuid" "github.com/gorilla/mux" @@ -170,6 +172,19 @@ func makeNullVDom() *vdom.VDomElem { return &vdom.VDomElem{WaveId: uuid.New().String(), Tag: vdom.WaveNullTag} } +func DefineComponent[P any](client *Client, name string, renderFn func(ctx context.Context, props P) any) vdom.Component[P] { + if name == "" { + panic("Component name cannot be empty") + } + if !unicode.IsUpper(rune(name[0])) { + panic("Component name must start with an uppercase letter") + } + client.RegisterComponent(name, renderFn) + return func(props P) *vdom.VDomElem { + return vdom.E(name, vdom.Props(props)) + } +} + func (c *Client) RegisterComponent(name string, cfunc any) error { return c.Root.RegisterComponent(name, cfunc) } From 8ecfec2bf0a2485866d644ba79c78121eeef0cea Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 3 Nov 2024 22:47:28 -0800 Subject: [PATCH 07/10] separate Make from Connect for vdomclient --- cmd/wsh/cmd/wshcmd-html.go | 3 ++- pkg/vdom/vdomclient/vdomclient.go | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-html.go b/cmd/wsh/cmd/wshcmd-html.go index f9b7161b1b..4b9c70d05d 100644 --- a/cmd/wsh/cmd/wshcmd-html.go +++ b/cmd/wsh/cmd/wshcmd-html.go @@ -175,7 +175,8 @@ func GlobalEventHandler(client *vdomclient.Client, event vdom.VDomEvent) { func htmlRun(cmd *cobra.Command, args []string) error { WriteStderr("running wsh html %q\n", RpcContext.BlockId) - client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true}) + client := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true}) + err := client.Connect() if err != nil { return err } diff --git a/pkg/vdom/vdomclient/vdomclient.go b/pkg/vdom/vdomclient/vdomclient.go index fcac91fd26..00c1dca754 100644 --- a/pkg/vdom/vdomclient/vdomclient.go +++ b/pkg/vdom/vdomclient/vdomclient.go @@ -66,7 +66,7 @@ func (c *Client) SetOverrideUrlHandler(handler http.Handler) { c.OverrideUrlHandler = handler } -func MakeClient(opts *vdom.VDomBackendOpts) (*Client, error) { +func MakeClient(opts *vdom.VDomBackendOpts) *Client { client := &Client{ Lock: &sync.Mutex{}, Root: vdom.MakeRoot(), @@ -76,34 +76,38 @@ func MakeClient(opts *vdom.VDomBackendOpts) (*Client, error) { if opts != nil { client.Opts = *opts } + return client +} + +func (client *Client) Connect() error { jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName) if jwtToken == "" { - return nil, fmt.Errorf("no %s env var set", wshutil.WaveJwtTokenVarName) + return fmt.Errorf("no %s env var set", wshutil.WaveJwtTokenVarName) } rpcCtx, err := wshutil.ExtractUnverifiedRpcContext(jwtToken) if err != nil { - return nil, fmt.Errorf("error extracting rpc context from %s: %v", wshutil.WaveJwtTokenVarName, err) + return fmt.Errorf("error extracting rpc context from %s: %v", wshutil.WaveJwtTokenVarName, err) } client.RpcContext = rpcCtx if client.RpcContext == nil || client.RpcContext.BlockId == "" { - return nil, fmt.Errorf("no block id in rpc context") + return fmt.Errorf("no block id in rpc context") } client.ServerImpl = &VDomServerImpl{BlockId: client.RpcContext.BlockId, Client: client} sockName, err := wshutil.ExtractUnverifiedSocketName(jwtToken) if err != nil { - return nil, fmt.Errorf("error extracting socket name from %s: %v", wshutil.WaveJwtTokenVarName, err) + return fmt.Errorf("error extracting socket name from %s: %v", wshutil.WaveJwtTokenVarName, err) } rpcClient, err := wshutil.SetupDomainSocketRpcClient(sockName, client.ServerImpl) if err != nil { - return nil, fmt.Errorf("error setting up domain socket rpc client: %v", err) + return fmt.Errorf("error setting up domain socket rpc client: %v", err) } client.RpcClient = rpcClient authRtn, err := wshclient.AuthenticateCommand(client.RpcClient, jwtToken, &wshrpc.RpcOpts{NoResponse: true}) if err != nil { - return nil, fmt.Errorf("error authenticating rpc connection: %v", err) + return fmt.Errorf("error authenticating rpc connection: %v", err) } client.RouteId = authRtn.RouteId - return client, nil + return nil } func (c *Client) SetRootElem(elem *vdom.VDomElem) { From 2d5b880b2034cbb816a1f971a09858b474fda8e2 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 4 Nov 2024 11:45:23 -0800 Subject: [PATCH 08/10] more robust E/P props handling. rewrite wshcmd-html in the new style --- cmd/wsh/cmd/wshcmd-html.go | 324 +++++++++++++++++++------------------ pkg/vdom/vdom.go | 54 ++++++- pkg/vdom/vdom_html.go | 14 +- 3 files changed, 232 insertions(+), 160 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-html.go b/cmd/wsh/cmd/wshcmd-html.go index 4b9c70d05d..d3d06ec9bf 100644 --- a/cmd/wsh/cmd/wshcmd-html.go +++ b/cmd/wsh/cmd/wshcmd-html.go @@ -1,12 +1,8 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - package cmd import ( "context" "log" - "time" "github.com/spf13/cobra" "github.com/wavetermdev/waveterm/pkg/vdom" @@ -18,7 +14,7 @@ import ( ) var htmlCmdNewBlock bool -var GlobalVDomClient *vdomclient.Client +var HtmlVDomClient *vdomclient.Client = vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true}) func init() { htmlCmd.Flags().BoolVarP(&htmlCmdNewBlock, "newblock", "n", false, "create a new block") @@ -32,182 +28,198 @@ var htmlCmd = &cobra.Command{ RunE: htmlRun, } -func StyleTag(ctx context.Context, props map[string]any) any { - return vdom.Bind(` - - `, nil) +// Prop Types +type BgItemProps struct { + Bg string `json:"bg"` + Label string `json:"label"` + OnClick func() `json:"onClick"` } -type BgItemProps struct { - Bg string - Label string +type BgListProps struct { + Items []BgItem `json:"items"` +} + +type BgItem struct { + Bg string `json:"bg"` + Label string `json:"label"` } -func BgItemTag(ctx context.Context, props BgItemProps) any { - clickFn := func() { - log.Printf("bg item clicked %q\n", props.Bg) - blockInfo, err := wshclient.BlockInfoCommand(GlobalVDomClient.RpcClient, GlobalVDomClient.RpcContext.BlockId, nil) - if err != nil { - log.Printf("error getting block info: %v\n", err) - return +// Components +var Style = vdomclient.DefineComponent[struct{}](HtmlVDomClient, "Style", + func(ctx context.Context, _ struct{}) any { + return vdom.E("style", nil, ` + .root { + padding: 10px; + } + + .background { + display: flex; + align-items: center; + width: 100%; + } + + .background-inner { + max-width: 300px; + } + + .bg-item { + cursor: pointer; + padding: 8px 12px; + border-radius: 4px; + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + } + + .bg-item:hover { + background-color: var(--button-grey-hover-bg); + } + + .bg-preview { + width: 20px; + height: 20px; + margin-right: 10px; + border-radius: 50%; + border: 1px solid #777; + } + + .bg-label { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + `) + }, +) + +var BgItemTag = vdomclient.DefineComponent[BgItemProps](HtmlVDomClient, "BgItem", + func(ctx context.Context, props BgItemProps) any { + return vdom.E("div", + vdom.P("className", "bg-item"), + vdom.P("onClick", props.OnClick), + vdom.E("div", + vdom.P("className", "bg-preview"), + vdom.PStyle("background", props.Bg), + ), + vdom.E("div", + vdom.P("className", "bg-label"), + props.Label, + ), + ) + }, +) + +var BgList = vdomclient.DefineComponent[BgListProps](HtmlVDomClient, "BgList", + func(ctx context.Context, props BgListProps) any { + setBackground := func(bg string) func() { + return func() { + blockInfo, err := wshclient.BlockInfoCommand(HtmlVDomClient.RpcClient, HtmlVDomClient.RpcContext.BlockId, nil) + if err != nil { + log.Printf("error getting block info: %v\n", err) + return + } + err = wshclient.SetMetaCommand(HtmlVDomClient.RpcClient, wshrpc.CommandSetMetaData{ + ORef: waveobj.ORef{OType: "tab", OID: blockInfo.TabId}, + Meta: map[string]any{"bg": bg}, + }, nil) + if err != nil { + log.Printf("error setting meta: %v\n", err) + } + } } - log.Printf("block info: tabid=%q\n", blockInfo.TabId) - err = wshclient.SetMetaCommand(GlobalVDomClient.RpcClient, wshrpc.CommandSetMetaData{ - ORef: waveobj.ORef{OType: "tab", OID: blockInfo.TabId}, - Meta: map[string]any{"bg": props.Bg}, - }, nil) - if err != nil { - log.Printf("error setting meta: %v\n", err) + + items := make([]*vdom.VDomElem, 0, len(props.Items)) + for _, item := range props.Items { + items = append(items, BgItemTag(BgItemProps{ + Bg: item.Bg, + Label: item.Label, + OnClick: setBackground(item.Bg), + })) } - // wshclient.SetMetaCommand(GlobalVDomClient.RpcClient) - } - params := map[string]any{ - "bg": props.Bg, - "label": props.Label, - "clickHandler": clickFn, - } - return vdom.Bind(` -
-
-
-
`, params) -} -func AllBgItemsTag(ctx context.Context, props map[string]any) any { - items := []map[string]any{ - {"bg": nil, "label": "default"}, - {"bg": "#ff0000", "label": "red"}, - {"bg": "#00ff00", "label": "green"}, - {"bg": "#0000ff", "label": "blue"}, - } - bgElems := make([]*vdom.VDomElem, 0) - for _, item := range items { - elem := vdom.E("BgItemTag", item) - bgElems = append(bgElems, elem) - } - return vdom.Bind(` -
-
- -
-
- `, map[string]any{"bgElems": bgElems}) -} + return vdom.E("div", + vdom.P("className", "background"), + vdom.E("div", + vdom.P("className", "background-inner"), + items, + ), + ) + }, +) -func MakeVDom() *vdom.VDomElem { - changeHandler := func(event vdom.VDomEvent) { - log.Printf("changeHandler: %v %q\n", event, event.TargetValue) - GlobalVDomClient.SetAtomVal("inputval", event.TargetValue) - } - vdomStr := ` -
- -

Set Background

-
- -
-
- -
-
- -
-
- -
text
-
-
- ` - elem := vdom.Bind(vdomStr, map[string]any{"changeHandler": changeHandler}) - return elem -} +var App = vdomclient.DefineComponent[struct{}](HtmlVDomClient, "App", + func(ctx context.Context, _ struct{}) any { + inputText, setInputText := vdom.UseState(ctx, "start") -func GlobalEventHandler(client *vdomclient.Client, event vdom.VDomEvent) { - if event.EventType == "clickinc" { - client.SetAtomVal("num", client.GetAtomVal("num").(int)+1) - return - } -} + bgItems := []BgItem{ + {Bg: "", Label: "default"}, + {Bg: "#ff0000", Label: "red"}, + {Bg: "#00ff00", Label: "green"}, + {Bg: "#0000ff", Label: "blue"}, + } + + return vdom.E("div", + vdom.P("className", "root"), + Style(struct{}{}), + vdom.E("h1", nil, "Set Background"), + vdom.E("div", nil, + vdom.E("wave:markdown", + vdom.P("text", "*quick vdom application to set background colors*"), + ), + ), + vdom.E("div", nil, + BgList(BgListProps{Items: bgItems}), + ), + vdom.E("div", nil, + vdom.E("img", + vdom.P("style", "width: 100%; height: 100%; max-width: 300px; max-height: 300px; object-fit: contain;"), + vdom.P("src", "vdom:///test.png"), + ), + ), + vdom.E("div", nil, + vdom.E("input", + vdom.P("type", "text"), + vdom.P("value", inputText), + vdom.P("onChange", func(e vdom.VDomEvent) { + setInputText(e.TargetValue) + }), + ), + vdom.E("div", nil, + "text ", inputText, + ), + ), + ) + }, +) func htmlRun(cmd *cobra.Command, args []string) error { WriteStderr("running wsh html %q\n", RpcContext.BlockId) - - client := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true}) + client := HtmlVDomClient err := client.Connect() if err != nil { return err } - GlobalVDomClient = client - client.SetGlobalEventHandler(GlobalEventHandler) - log.Printf("created client: %v\n", client) - client.RegisterComponent("StyleTag", StyleTag) - client.RegisterComponent("BgItemTag", BgItemTag) - client.RegisterComponent("AllBgItemsTag", AllBgItemsTag) + + // Set up the root component + client.SetRootElem(App(struct{}{})) + + // Set up file handler client.RegisterFileHandler("/test.png", "~/Downloads/IMG_1939.png") - client.SetAtomVal("inputval", "start") - client.SetRootElem(MakeVDom()) + + // Create the VDOM context err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: htmlCmdNewBlock}) if err != nil { return err } - log.Printf("created context\n") + + // Handle shutdown go func() { <-client.DoneCh wshutil.DoShutdown("vdom closed by FE", 0, true) }() - log.Printf("created vdom context\n") - go func() { - time.Sleep(5 * time.Second) - log.Printf("updating text\n") - client.SetAtomVal("text", "updated text") - err := client.SendAsyncInitiation() - if err != nil { - log.Printf("error sending async initiation: %v\n", err) - } - }() + <-client.DoneCh return nil } diff --git a/pkg/vdom/vdom.go b/pkg/vdom/vdom.go index 26a62ee00d..a3cc04a7f9 100644 --- a/pkg/vdom/vdom.go +++ b/pkg/vdom/vdom.go @@ -7,6 +7,7 @@ import ( "context" "encoding/json" "fmt" + "log" "reflect" "strconv" "strings" @@ -29,6 +30,15 @@ type Hook struct { type Component[P any] func(props P) *VDomElem +type styleAttrWrapper struct { + StyleAttr string + Val any +} + +type styleAttrMapWrapper struct { + StyleAttrMap map[string]any +} + func (e *VDomElem) Key() string { keyVal, ok := e.Props[KeyPropKey] if !ok { @@ -58,6 +68,20 @@ func mergeProps(props *map[string]any, newProps map[string]any) { } } +func mergeStyleAttr(props *map[string]any, styleAttr styleAttrWrapper) { + if *props == nil { + *props = make(map[string]any) + } + if (*props)["style"] == nil { + (*props)["style"] = make(map[string]any) + } + styleMap, ok := (*props)["style"].(map[string]any) + if !ok { + return + } + styleMap[styleAttr.StyleAttr] = styleAttr.Val +} + func E(tag string, parts ...any) *VDomElem { rtn := &VDomElem{Tag: tag} for _, part := range parts { @@ -69,6 +93,16 @@ func E(tag string, parts ...any) *VDomElem { mergeProps(&rtn.Props, props) continue } + if styleAttr, ok := part.(styleAttrWrapper); ok { + mergeStyleAttr(&rtn.Props, styleAttr) + continue + } + if styleAttrMap, ok := part.(styleAttrMapWrapper); ok { + for k, v := range styleAttrMap.StyleAttrMap { + mergeStyleAttr(&rtn.Props, styleAttrWrapper{StyleAttr: k, Val: v}) + } + continue + } elems := partToElems(part) rtn.Children = append(rtn.Children, elems...) } @@ -83,7 +117,25 @@ func Props(props any) map[string]any { return m } -func P(propName string, propVal any) map[string]any { +func PStyle(styleAttr string, propVal any) any { + return styleAttrWrapper{StyleAttr: styleAttr, Val: propVal} +} + +func P(propName string, propVal any) any { + if propVal == nil { + return map[string]any{propName: nil} + } + if propName == "style" { + strVal, ok := propVal.(string) + if ok { + styleMap, err := styleAttrStrToStyleMap(strVal, nil) + if err == nil { + return styleAttrMapWrapper{StyleAttrMap: styleMap} + } + log.Printf("Error parsing style attribute: %v\n", err) + return nil + } + } return map[string]any{propName: propVal} } diff --git a/pkg/vdom/vdom_html.go b/pkg/vdom/vdom_html.go index 67721efc2d..dc94b7c8c9 100644 --- a/pkg/vdom/vdom_html.go +++ b/pkg/vdom/vdom_html.go @@ -273,17 +273,25 @@ func convertStyleToReactStyles(styleMap map[string]string, params map[string]any return rtn } +func styleAttrStrToStyleMap(styleText string, params map[string]any) (map[string]any, error) { + parser := cssparser.MakeParser(styleText) + m, err := parser.Parse() + if err != nil { + return nil, err + } + return convertStyleToReactStyles(m, params), nil +} + func fixStyleAttribute(elem *VDomElem, params map[string]any, elemPath []string) error { styleText, ok := elem.Props["style"].(string) if !ok { return nil } - parser := cssparser.MakeParser(styleText) - m, err := parser.Parse() + styleMap, err := styleAttrStrToStyleMap(styleText, params) if err != nil { return fmt.Errorf("%v (at %s)", err, makePathStr(elemPath)) } - elem.Props["style"] = convertStyleToReactStyles(m, params) + elem.Props["style"] = styleMap return nil } From 026bf59361eedd376a22682da8ad9d630a2f829c Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 4 Nov 2024 12:47:54 -0800 Subject: [PATCH 09/10] working on a streaming backend transfer protocol --- frontend/app/view/vdom/vdom-model.tsx | 3 ++ frontend/app/view/vdom/vdom-utils.tsx | 46 ++++++++++++++++++++++++ frontend/types/gotypes.d.ts | 1 + pkg/vdom/vdom_root.go | 50 +++++++++++++++++++++++++++ pkg/vdom/vdom_types.go | 11 ++++++ pkg/vdom/vdomclient/vdomclient.go | 1 + 6 files changed, 112 insertions(+) diff --git a/frontend/app/view/vdom/vdom-model.tsx b/frontend/app/view/vdom/vdom-model.tsx index e7db45759a..f7bdf144f8 100644 --- a/frontend/app/view/vdom/vdom-model.tsx +++ b/frontend/app/view/vdom/vdom-model.tsx @@ -561,6 +561,9 @@ export class VDomModel { } } } + if (update.haswork) { + this.queueUpdate(true); + } } callVDomFunc(fnDecl: VDomFunc, e: React.SyntheticEvent, compId: string, propName: string) { diff --git a/frontend/app/view/vdom/vdom-utils.tsx b/frontend/app/view/vdom/vdom-utils.tsx index c4bc7f2e9e..0f049c35bc 100644 --- a/frontend/app/view/vdom/vdom-utils.tsx +++ b/frontend/app/view/vdom/vdom-utils.tsx @@ -136,3 +136,49 @@ export function validateAndWrapReactStyle(model: VDomModel, style: Record { + if (!transferElem.waveid) { + return; // Skip elements without waveid + } + const elem: VDomElem = { + waveid: transferElem.tag !== "#text" ? transferElem.waveid : undefined, + tag: transferElem.tag, + props: transferElem.props, + text: transferElem.text, + children: [], // Placeholder to be populated later + }; + elemMap[transferElem.waveid] = elem; + + // Collect root elements + if (transferElem.root) { + roots.push(elem); + } + }); + + // Now populate children for each element + transferElems.forEach((transferElem) => { + if (!transferElem.waveid || !transferElem.children) return; + + const currentElem = elemMap[transferElem.waveid]; + currentElem.children = transferElem.children + .map((childId) => elemMap[childId]) + .filter((child) => child !== undefined); // Filter out any undefined children + }); + + return roots; +} diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 379a662050..44bfcc9ba0 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -650,6 +650,7 @@ declare global { ts: number; blockid: string; opts?: VDomBackendOpts; + haswork?: boolean; renderupdates?: VDomRenderUpdate[]; statesync?: VDomStateSync[]; refoperations?: VDomRefOperation[]; diff --git a/pkg/vdom/vdom_root.go b/pkg/vdom/vdom_root.go index 03f9356afc..b59f5dd37e 100644 --- a/pkg/vdom/vdom_root.go +++ b/pkg/vdom/vdom_root.go @@ -464,3 +464,53 @@ func (r *RootElem) makeVDom(comp *ComponentImpl) *VDomElem { func (r *RootElem) MakeVDom() *VDomElem { return r.makeVDom(r.Root) } + +func ConvertElemsToTransferElems(elems []VDomElem) []VDomTransferElem { + var transferElems []VDomTransferElem + textCounter := 0 // Counter for generating unique IDs for #text nodes + + // Helper function to recursively process each VDomElem in preorder + var processElem func(elem VDomElem, isRoot bool) string + processElem = func(elem VDomElem, isRoot bool) string { + // Handle #text nodes by generating a unique placeholder ID + if elem.Tag == "#text" { + textId := fmt.Sprintf("text-%d", textCounter) + textCounter++ + transferElems = append(transferElems, VDomTransferElem{ + Root: isRoot, + WaveId: textId, + Tag: elem.Tag, + Text: elem.Text, + Props: nil, + Children: nil, + }) + return textId + } + + // Convert children to WaveId references, handling potential #text nodes + childrenIds := make([]string, len(elem.Children)) + for i, child := range elem.Children { + childrenIds[i] = processElem(child, false) // Children are not roots + } + + // Create the VDomTransferElem for the current element + transferElem := VDomTransferElem{ + Root: isRoot, + WaveId: elem.WaveId, + Tag: elem.Tag, + Props: elem.Props, + Children: childrenIds, + Text: elem.Text, + } + transferElems = append(transferElems, transferElem) + + return elem.WaveId + } + + // Start processing each top-level element, marking them as roots + for _, elem := range elems { + processElem(elem, true) + } + + return transferElems +} diff --git a/pkg/vdom/vdom_types.go b/pkg/vdom/vdom_types.go index e39da5f039..e3b3c00e9f 100644 --- a/pkg/vdom/vdom_types.go +++ b/pkg/vdom/vdom_types.go @@ -31,6 +31,16 @@ type VDomElem struct { Text string `json:"text,omitempty"` } +// the over the wire format for a vdom element +type VDomTransferElem struct { + Root bool `json:"root,omitempty"` + WaveId string `json:"waveid,omitempty"` // required, except for #text nodes + Tag string `json:"tag"` + Props map[string]any `json:"props,omitempty"` + Children []string `json:"children,omitempty"` + Text string `json:"text,omitempty"` +} + //// protocol messages type VDomCreateContext struct { @@ -74,6 +84,7 @@ type VDomBackendUpdate struct { Ts int64 `json:"ts"` BlockId string `json:"blockid"` Opts *VDomBackendOpts `json:"opts,omitempty"` + HasWork bool `json:"haswork,omitempty"` RenderUpdates []VDomRenderUpdate `json:"renderupdates,omitempty"` StateSync []VDomStateSync `json:"statesync,omitempty"` RefOperations []VDomRefOperation `json:"refoperations,omitempty"` diff --git a/pkg/vdom/vdomclient/vdomclient.go b/pkg/vdom/vdomclient/vdomclient.go index 00c1dca754..97972aa433 100644 --- a/pkg/vdom/vdomclient/vdomclient.go +++ b/pkg/vdom/vdomclient/vdomclient.go @@ -204,6 +204,7 @@ func (c *Client) fullRender() (*vdom.VDomBackendUpdate, error) { Type: "backendupdate", Ts: time.Now().UnixMilli(), BlockId: c.RpcContext.BlockId, + HasWork: len(c.Root.EffectWorkQueue) > 0, Opts: &c.Opts, RenderUpdates: []vdom.VDomRenderUpdate{ {UpdateType: "root", VDom: *renderedVDom}, From 41de91384d40d775037af2097789d4ca2b0d2198 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 4 Nov 2024 12:50:33 -0800 Subject: [PATCH 10/10] add copyright back in --- cmd/wsh/cmd/wshcmd-html.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/wsh/cmd/wshcmd-html.go b/cmd/wsh/cmd/wshcmd-html.go index d3d06ec9bf..a16bc821d1 100644 --- a/cmd/wsh/cmd/wshcmd-html.go +++ b/cmd/wsh/cmd/wshcmd-html.go @@ -1,3 +1,6 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + package cmd import (