|
| 1 | +// Copyright 2025, Command Line Inc. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +package engine |
| 5 | + |
| 6 | +import ( |
| 7 | + "context" |
| 8 | + "fmt" |
| 9 | + "reflect" |
| 10 | + "unicode" |
| 11 | + |
| 12 | + "github.com/google/uuid" |
| 13 | + "github.com/wavetermdev/waveterm/tsunami/rpctypes" |
| 14 | + "github.com/wavetermdev/waveterm/tsunami/util" |
| 15 | + "github.com/wavetermdev/waveterm/tsunami/vdom" |
| 16 | +) |
| 17 | + |
| 18 | +// see render.md for a complete guide to how tsunami rendering, lifecycle, and reconciliation works |
| 19 | + |
| 20 | +type RenderOpts struct { |
| 21 | + Resync bool |
| 22 | +} |
| 23 | + |
| 24 | +func (r *RootElem) Render(elem *vdom.VDomElem, opts *RenderOpts) { |
| 25 | + r.render(elem, &r.Root, opts) |
| 26 | +} |
| 27 | + |
| 28 | +func getElemKey(elem *vdom.VDomElem) string { |
| 29 | + if elem == nil { |
| 30 | + return "" |
| 31 | + } |
| 32 | + keyVal, ok := elem.Props[vdom.KeyPropKey] |
| 33 | + if !ok { |
| 34 | + return "" |
| 35 | + } |
| 36 | + return fmt.Sprint(keyVal) |
| 37 | +} |
| 38 | + |
| 39 | +func (r *RootElem) render(elem *vdom.VDomElem, comp **ComponentImpl, opts *RenderOpts) { |
| 40 | + if elem == nil || elem.Tag == "" { |
| 41 | + r.unmount(comp) |
| 42 | + return |
| 43 | + } |
| 44 | + elemKey := getElemKey(elem) |
| 45 | + if *comp == nil || !(*comp).compMatch(elem.Tag, elemKey) { |
| 46 | + r.unmount(comp) |
| 47 | + r.createComp(elem.Tag, elemKey, comp) |
| 48 | + } |
| 49 | + (*comp).Elem = elem |
| 50 | + if elem.Tag == vdom.TextTag { |
| 51 | + // Pattern 1: Text Nodes |
| 52 | + r.renderText(elem.Text, comp) |
| 53 | + return |
| 54 | + } |
| 55 | + if isBaseTag(elem.Tag) { |
| 56 | + // Pattern 2: Base elements |
| 57 | + r.renderSimple(elem, comp, opts) |
| 58 | + return |
| 59 | + } |
| 60 | + cfunc := r.CFuncs[elem.Tag] |
| 61 | + if cfunc == nil { |
| 62 | + text := fmt.Sprintf("<%s>", elem.Tag) |
| 63 | + r.renderText(text, comp) |
| 64 | + return |
| 65 | + } |
| 66 | + // Pattern 3: components |
| 67 | + r.renderComponent(cfunc, elem, comp, opts) |
| 68 | +} |
| 69 | + |
| 70 | +// Pattern 1 |
| 71 | +func (r *RootElem) renderText(text string, comp **ComponentImpl) { |
| 72 | + // No need to clear Children/Comp - text components cannot have them |
| 73 | + if (*comp).Text != text { |
| 74 | + (*comp).Text = text |
| 75 | + } |
| 76 | +} |
| 77 | + |
| 78 | +// Pattern 2 |
| 79 | +func (r *RootElem) renderSimple(elem *vdom.VDomElem, comp **ComponentImpl, opts *RenderOpts) { |
| 80 | + if (*comp).RenderedComp != nil { |
| 81 | + // Clear Comp since base elements don't use it |
| 82 | + r.unmount(&(*comp).RenderedComp) |
| 83 | + } |
| 84 | + (*comp).Children = r.renderChildren(elem.Children, (*comp).Children, opts) |
| 85 | +} |
| 86 | + |
| 87 | +// Pattern 3 |
| 88 | +func (r *RootElem) renderComponent(cfunc any, elem *vdom.VDomElem, comp **ComponentImpl, opts *RenderOpts) { |
| 89 | + if (*comp).Children != nil { |
| 90 | + // Clear Children since custom components don't use them |
| 91 | + for _, child := range (*comp).Children { |
| 92 | + r.unmount(&child) |
| 93 | + } |
| 94 | + (*comp).Children = nil |
| 95 | + } |
| 96 | + props := make(map[string]any) |
| 97 | + for k, v := range elem.Props { |
| 98 | + props[k] = v |
| 99 | + } |
| 100 | + props[ChildrenPropKey] = elem.Children |
| 101 | + vc := makeContextVal(r, *comp, opts) |
| 102 | + rtnElemArr := withGlobalCtx(vc, func() []vdom.VDomElem { |
| 103 | + ctx := r.OuterCtx |
| 104 | + if ctx == nil { |
| 105 | + ctx = context.Background() |
| 106 | + } |
| 107 | + renderedElem := callCFunc(cfunc, ctx, props) |
| 108 | + return vdom.ToElems(renderedElem) |
| 109 | + }) |
| 110 | + var rtnElem *vdom.VDomElem |
| 111 | + if len(rtnElemArr) == 0 { |
| 112 | + rtnElem = nil |
| 113 | + } else if len(rtnElemArr) == 1 { |
| 114 | + rtnElem = &rtnElemArr[0] |
| 115 | + } else { |
| 116 | + rtnElem = &vdom.VDomElem{Tag: vdom.FragmentTag, Children: rtnElemArr} |
| 117 | + } |
| 118 | + r.render(rtnElem, &(*comp).RenderedComp, opts) |
| 119 | +} |
| 120 | + |
| 121 | +func (r *RootElem) unmount(comp **ComponentImpl) { |
| 122 | + if *comp == nil { |
| 123 | + return |
| 124 | + } |
| 125 | + waveId := (*comp).WaveId |
| 126 | + for _, hook := range (*comp).Hooks { |
| 127 | + if hook.UnmountFn != nil { |
| 128 | + hook.UnmountFn() |
| 129 | + } |
| 130 | + } |
| 131 | + if (*comp).RenderedComp != nil { |
| 132 | + r.unmount(&(*comp).RenderedComp) |
| 133 | + } |
| 134 | + if (*comp).Children != nil { |
| 135 | + for _, child := range (*comp).Children { |
| 136 | + r.unmount(&child) |
| 137 | + } |
| 138 | + } |
| 139 | + delete(r.CompMap, waveId) |
| 140 | + r.cleanupUsedByForUnmount(waveId) |
| 141 | + *comp = nil |
| 142 | +} |
| 143 | + |
| 144 | +func (r *RootElem) createComp(tag string, key string, comp **ComponentImpl) { |
| 145 | + *comp = &ComponentImpl{WaveId: uuid.New().String(), Tag: tag, Key: key} |
| 146 | + r.CompMap[(*comp).WaveId] = *comp |
| 147 | +} |
| 148 | + |
| 149 | +// handles reconcilation |
| 150 | +// maps children via key or index (exclusively) |
| 151 | +func (r *RootElem) renderChildren(elems []vdom.VDomElem, curChildren []*ComponentImpl, opts *RenderOpts) []*ComponentImpl { |
| 152 | + newChildren := make([]*ComponentImpl, len(elems)) |
| 153 | + curCM := make(map[ChildKey]*ComponentImpl) |
| 154 | + usedMap := make(map[*ComponentImpl]bool) |
| 155 | + for idx, child := range curChildren { |
| 156 | + if child.Key != "" { |
| 157 | + curCM[ChildKey{Tag: child.Tag, Idx: 0, Key: child.Key}] = child |
| 158 | + } else { |
| 159 | + curCM[ChildKey{Tag: child.Tag, Idx: idx, Key: ""}] = child |
| 160 | + } |
| 161 | + } |
| 162 | + for idx, elem := range elems { |
| 163 | + elemKey := getElemKey(&elem) |
| 164 | + var curChild *ComponentImpl |
| 165 | + if elemKey != "" { |
| 166 | + curChild = curCM[ChildKey{Tag: elem.Tag, Idx: 0, Key: elemKey}] |
| 167 | + } else { |
| 168 | + curChild = curCM[ChildKey{Tag: elem.Tag, Idx: idx, Key: ""}] |
| 169 | + } |
| 170 | + usedMap[curChild] = true |
| 171 | + newChildren[idx] = curChild |
| 172 | + r.render(&elem, &newChildren[idx], opts) |
| 173 | + } |
| 174 | + for _, child := range curChildren { |
| 175 | + if !usedMap[child] { |
| 176 | + r.unmount(&child) |
| 177 | + } |
| 178 | + } |
| 179 | + return newChildren |
| 180 | +} |
| 181 | + |
| 182 | +// uses reflection to call the component function |
| 183 | +func callCFunc(cfunc any, ctx context.Context, props map[string]any) any { |
| 184 | + rval := reflect.ValueOf(cfunc) |
| 185 | + arg2Type := rval.Type().In(1) |
| 186 | + |
| 187 | + var arg2Val reflect.Value |
| 188 | + if arg2Type.Kind() == reflect.Interface && arg2Type.NumMethod() == 0 { |
| 189 | + arg2Val = reflect.New(arg2Type) |
| 190 | + } else { |
| 191 | + arg2Val = reflect.New(arg2Type) |
| 192 | + if arg2Type.Kind() == reflect.Map { |
| 193 | + arg2Val.Elem().Set(reflect.ValueOf(props)) |
| 194 | + } else { |
| 195 | + err := util.MapToStruct(props, arg2Val.Interface()) |
| 196 | + if err != nil { |
| 197 | + fmt.Printf("error unmarshalling props: %v\n", err) |
| 198 | + } |
| 199 | + } |
| 200 | + } |
| 201 | + rtnVal := rval.Call([]reflect.Value{reflect.ValueOf(ctx), arg2Val.Elem()}) |
| 202 | + if len(rtnVal) == 0 { |
| 203 | + return nil |
| 204 | + } |
| 205 | + return rtnVal[0].Interface() |
| 206 | +} |
| 207 | + |
| 208 | +func convertPropsToVDom(props map[string]any) map[string]any { |
| 209 | + if len(props) == 0 { |
| 210 | + return nil |
| 211 | + } |
| 212 | + vdomProps := make(map[string]any) |
| 213 | + for k, v := range props { |
| 214 | + if v == nil { |
| 215 | + continue |
| 216 | + } |
| 217 | + if vdomFunc, ok := v.(vdom.VDomFunc); ok { |
| 218 | + // ensure Type is set on all VDomFuncs |
| 219 | + vdomFunc.Type = vdom.ObjectType_Func |
| 220 | + vdomProps[k] = vdomFunc |
| 221 | + continue |
| 222 | + } |
| 223 | + if vdomRef, ok := v.(vdom.VDomRef); ok { |
| 224 | + // ensure Type is set on all VDomRefs |
| 225 | + vdomRef.Type = vdom.ObjectType_Ref |
| 226 | + vdomProps[k] = vdomRef |
| 227 | + continue |
| 228 | + } |
| 229 | + val := reflect.ValueOf(v) |
| 230 | + if val.Kind() == reflect.Func { |
| 231 | + // convert go functions passed to event handlers to VDomFuncs |
| 232 | + vdomProps[k] = vdom.VDomFunc{Type: vdom.ObjectType_Func} |
| 233 | + continue |
| 234 | + } |
| 235 | + vdomProps[k] = v |
| 236 | + } |
| 237 | + return vdomProps |
| 238 | +} |
| 239 | + |
| 240 | +func (r *RootElem) MakeRendered() *rpctypes.RenderedElem { |
| 241 | + if r.Root == nil { |
| 242 | + return nil |
| 243 | + } |
| 244 | + return r.convertCompToRendered(r.Root) |
| 245 | +} |
| 246 | + |
| 247 | +func (r *RootElem) convertCompToRendered(c *ComponentImpl) *rpctypes.RenderedElem { |
| 248 | + if c == nil { |
| 249 | + return nil |
| 250 | + } |
| 251 | + if c.RenderedComp != nil { |
| 252 | + return r.convertCompToRendered(c.RenderedComp) |
| 253 | + } |
| 254 | + if len(c.Children) == 0 && r.CFuncs[c.Tag] != nil { |
| 255 | + return nil |
| 256 | + } |
| 257 | + return r.convertBaseToRendered(c) |
| 258 | +} |
| 259 | + |
| 260 | +func (r *RootElem) convertBaseToRendered(c *ComponentImpl) *rpctypes.RenderedElem { |
| 261 | + elem := &rpctypes.RenderedElem{WaveId: c.WaveId, Tag: c.Tag} |
| 262 | + if c.Elem != nil { |
| 263 | + elem.Props = convertPropsToVDom(c.Elem.Props) |
| 264 | + } |
| 265 | + for _, child := range c.Children { |
| 266 | + childElem := r.convertCompToRendered(child) |
| 267 | + if childElem != nil { |
| 268 | + elem.Children = append(elem.Children, *childElem) |
| 269 | + } |
| 270 | + } |
| 271 | + if c.Tag == vdom.TextTag { |
| 272 | + elem.Text = c.Text |
| 273 | + } |
| 274 | + return elem |
| 275 | +} |
| 276 | + |
| 277 | +func isBaseTag(tag string) bool { |
| 278 | + if tag == "" { |
| 279 | + return false |
| 280 | + } |
| 281 | + if tag == vdom.TextTag || tag == vdom.WaveTextTag || tag == vdom.WaveNullTag || tag == vdom.FragmentTag { |
| 282 | + return true |
| 283 | + } |
| 284 | + if tag[0] == '#' { |
| 285 | + return true |
| 286 | + } |
| 287 | + firstChar := rune(tag[0]) |
| 288 | + return unicode.IsLower(firstChar) |
| 289 | +} |
0 commit comments