├── .gitignore ├── .vscode └── settings.json ├── README.md ├── atom.go ├── go.mod ├── go.sum ├── hooks.go ├── input_event.go ├── maps └── maps.go ├── mount.go ├── mouse_event.go ├── node.go ├── reconcile.go ├── sets └── sets.go ├── syscall.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | example 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.toolsEnvVars": { 3 | "GOARCH":"wasm", 4 | "GOOS":"js", 5 | }, 6 | "go.installDependenciesWhenBuilding": false, 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoUI 2 | An experimental web framework for creating user interfaces. 3 | 4 | ## GoUIX 5 | Install `gouix`, a cli tool for GoUI. 6 | 7 | ``` 8 | go install github.com/goui-org/gouix@latest 9 | ``` 10 | 11 | Create a new app 12 | ``` 13 | gouix create my-app 14 | ``` 15 | 16 | Start the development server 17 | ``` 18 | gouix serve 19 | ``` 20 | 21 | Create a production build 22 | ``` 23 | gouix build 24 | ``` 25 | 26 | ## Usage 27 | ```go 28 | // main.go 29 | package main 30 | 31 | import ( 32 | "github.com/goui-org/goui" 33 | "main/app" 34 | ) 35 | 36 | func main() { 37 | goui.Mount("#root", app.App) 38 | } 39 | ``` 40 | 41 | 42 | ```go 43 | // app/app.go 44 | package app 45 | 46 | import ( 47 | "strconv" 48 | 49 | "github.com/goui-org/goui" 50 | ) 51 | 52 | func App(goui.NoProps) *goui.Node { 53 | count, setCount := goui.UseState(0) 54 | 55 | goui.UseEffect(func() goui.EffectTeardown { 56 | goui.Log("count is %d", count) 57 | return nil 58 | }, goui.Deps{count}) 59 | 60 | handleIncrement := goui.UseCallback(func(e *goui.MouseEvent) { 61 | setCount(func(c int) int { return c + 1 }) 62 | }, goui.Deps{}) 63 | 64 | return goui.Element("div", &goui.Attributes{ 65 | Class: "app", 66 | Slot: []*goui.Node{ 67 | goui.Element("button", &goui.Attributes{ 68 | Class: "app-btn", 69 | Slot: "increment", 70 | OnClick: handleIncrement, 71 | }), 72 | goui.Element("p", &goui.Attributes{ 73 | Slot: "count: " + strconv.Itoa(count), 74 | }), 75 | }, 76 | }) 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /atom.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | import ( 4 | "github.com/goui-org/goui/maps" 5 | "github.com/goui-org/goui/sets" 6 | ) 7 | 8 | // import { Dispatch, SetStateAction, UpdateStateAction } from './hooks.js'; 9 | // import { ComponentElem } from './elem.js'; 10 | // import { callComponentFuncAndReconcile } from './reconcile.js'; 11 | 12 | // export type AtomSelector = (state: T) => R 13 | 14 | type selectorRecord[T comparable] struct { 15 | selected any 16 | selector func(T) any 17 | } 18 | 19 | type Atom[T comparable] struct { 20 | value T 21 | subs *sets.Set[*Node] 22 | selectors *maps.Map[*Node, []*selectorRecord[T]] 23 | } 24 | 25 | func CreateAtom[T comparable](initialValue T) *Atom[T] { 26 | return &Atom[T]{ 27 | value: initialValue, 28 | subs: sets.New[*Node](), 29 | selectors: maps.New[*Node, []*selectorRecord[T]](), 30 | } 31 | } 32 | 33 | func (a *Atom[T]) subscribe(n *Node) { 34 | a.subs.Add(n) 35 | } 36 | 37 | func (a *Atom[T]) unsubscribe(n *Node) { 38 | a.subs.Delete(n) 39 | } 40 | 41 | func (a *Atom[T]) update(action func(T) T) { 42 | newVal := action(a.value) 43 | if newVal != a.value { 44 | a.value = newVal 45 | go a.reconcile() 46 | } 47 | } 48 | 49 | func (a *Atom[T]) reconcile() { 50 | for _, node := range a.subs.Slice() { 51 | if a.subs.Has(node) { 52 | callComponentFuncAndReconcile(node, node) 53 | } 54 | } 55 | for _, entry := range a.selectors.Slice() { 56 | for _, record := range entry.Value { 57 | if record.selected != record.selector(a.value) { 58 | callComponentFuncAndReconcile(entry.Key, entry.Key) 59 | } 60 | } 61 | } 62 | } 63 | 64 | // export interface Atom { 65 | // s: T // state 66 | // u: Dispatch> // update atom state 67 | // r: () => void // reconcile all subscribers 68 | // c: Set // component subscribers 69 | // a: Set | ReadonlyAtom> // atoms subscribed to this atom 70 | // f: Map][]> // selectors subscribed to this atom 71 | // } 72 | 73 | // interface Deriver { 74 | // d: AtomDerivation // state derivation 75 | // } 76 | 77 | // export type AtomGetter = (atom: Atom | ReadonlyAtom) => A; 78 | // export type ReadonlyAtom = Omit, 'u'> & Deriver; 79 | // export type AtomDerivation = (get: AtomGetter) => T; 80 | 81 | // type CreateAtom = { 82 | // (derivation: AtomDerivation, options?: AtomOptions): ReadonlyAtom 83 | // (initialValue: T, options?: AtomOptions): Atom 84 | // }; 85 | 86 | // export interface AtomOptions { 87 | // watch?: (prevState: T, newState: T) => void 88 | // } 89 | 90 | // export let createAtom: CreateAtom = (config: any, options?: any): any => typeof config === 'function' ? createDerivedAtom(config, options) : createStandardAtom(config, options); 91 | 92 | // let createStandardAtom = (initialValue: T, options?: AtomOptions): Atom => { 93 | // let atom: Atom = { 94 | // s: initialValue, 95 | // u: action => { 96 | // let oldState = atom.s; 97 | // atom.s = typeof action === 'function' ? (action as UpdateStateAction)(oldState) : action; 98 | // if (oldState !== atom.s) { 99 | // options?.watch?.(oldState, atom.s); 100 | // queueMicrotask(atom.r); 101 | // } 102 | // }, 103 | // r: () => updateAtomSubscribers(atom), 104 | // c: new Set(), 105 | // a: new Set(), 106 | // f: new Map(), 107 | // }; 108 | // return atom; 109 | // }; 110 | 111 | // let createDerivedAtom = (derivation: AtomDerivation, options?: AtomOptions): ReadonlyAtom => { 112 | // let atom: ReadonlyAtom = { 113 | // s: null as T, 114 | // d: derivation, 115 | // r: null as unknown as () => void, 116 | // c: new Set(), 117 | // a: new Set(), 118 | // f: new Map(), 119 | // }; 120 | // let getter: AtomGetter = (a: Atom | ReadonlyAtom): A => { 121 | // a.a.add(atom); 122 | // return a.s; 123 | // } 124 | // atom.s = derivation(getter); 125 | // atom.r = () => { 126 | // let oldState = atom.s; 127 | // atom.s = atom.d(getter); 128 | // if (atom.s !== oldState) { 129 | // options?.watch?.(oldState, atom.s); 130 | // updateAtomSubscribers(atom); 131 | // } 132 | // } 133 | // return atom; 134 | // }; 135 | 136 | // let updateAtomSubscribers = (atom: Atom | ReadonlyAtom): void => { 137 | // for (let component of [...atom.c.keys()]) { 138 | // if (atom.c.has(component)) { 139 | // callComponentFuncAndReconcile(component, component); 140 | // } 141 | // } 142 | // atom.a.forEach(a => a.r()); 143 | // for (let [component, selects] of [...atom.f.entries()]) { 144 | // for (let i = selects.length - 1; i >= 0; i--) { 145 | // let [selected, selector] = selects[i]; 146 | // if (selected !== selector(atom.s)) { 147 | // callComponentFuncAndReconcile(component, component); 148 | // } 149 | // } 150 | // } 151 | // }; 152 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/goui-org/goui 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goui-org/goui/227316c7d6a004c28dbab8ccd861e0ff2a157ac1/go.sum -------------------------------------------------------------------------------- /hooks.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | type EffectTeardown func() 4 | type Deps []any 5 | 6 | type Callback[Func any] struct { 7 | invoke Func 8 | } 9 | 10 | type effectRecord struct { 11 | deps Deps 12 | teardown EffectTeardown 13 | } 14 | 15 | func useHooks() (int, *Node) { 16 | node := currentNode 17 | cursor := node.hooksCursor 18 | node.hooksCursor++ 19 | return cursor, node 20 | } 21 | 22 | func UseState[T comparable](initialValue T) (T, func(func(T) T)) { 23 | cursor, node := useHooks() 24 | if len(node.hooks) <= cursor { 25 | node.hooks = append(node.hooks, initialValue) 26 | } 27 | setState := func(update func(T) T) { 28 | if node.unmounted { 29 | panic("bad set state") 30 | } 31 | oldVal := node.hooks[cursor].(T) 32 | newVal := update(oldVal) 33 | if newVal != oldVal { 34 | node.hooks[cursor] = newVal 35 | node.queue = append(node.queue, callComponentFunc(node)) 36 | go func() { 37 | if len(node.queue) > 0 { 38 | tip := node.queue[len(node.queue)-1] 39 | node.queue = node.queue[:0] 40 | reconcile(node.virtNode, tip) 41 | node.virtNode = tip 42 | } 43 | }() 44 | } 45 | } 46 | return node.hooks[cursor].(T), setState 47 | } 48 | 49 | func UseEffect(effect func() EffectTeardown, deps Deps) { 50 | cursor, node := useHooks() 51 | if len(node.hooks) <= cursor { 52 | record := &effectRecord{deps: deps} 53 | node.hooks = append(node.hooks, record) 54 | go func() { 55 | if !node.unmounted { 56 | record.teardown = effect() 57 | } 58 | }() 59 | return 60 | } 61 | record := node.hooks[cursor].(*effectRecord) 62 | if !areDepsEqual(deps, record.deps) { 63 | record.deps = deps 64 | go func() { 65 | if record.teardown != nil { 66 | record.teardown() 67 | } 68 | if !node.unmounted { 69 | record.teardown = effect() 70 | } 71 | }() 72 | } 73 | } 74 | 75 | func UseImmediateEffect(effect func() EffectTeardown, deps Deps) { 76 | cursor, node := useHooks() 77 | if len(node.hooks) <= cursor { 78 | node.hooks = append(node.hooks, &effectRecord{ 79 | deps: deps, 80 | teardown: effect(), 81 | }) 82 | return 83 | } 84 | record := node.hooks[cursor].(*effectRecord) 85 | if !areDepsEqual(deps, record.deps) { 86 | if record.teardown != nil { 87 | record.teardown() 88 | } 89 | record.deps = deps 90 | record.teardown = effect() 91 | } 92 | } 93 | 94 | type memoRecord[T any] struct { 95 | deps Deps 96 | val T 97 | } 98 | 99 | func UseMemo[T any](create func() T, deps Deps) T { 100 | cursor, node := useHooks() 101 | if len(node.hooks) <= cursor { 102 | m := &memoRecord[T]{ 103 | val: create(), 104 | deps: deps, 105 | } 106 | node.hooks = append(node.hooks, m) 107 | return m.val 108 | } 109 | memo := node.hooks[cursor].(*memoRecord[T]) 110 | if !areDepsEqual(deps, memo.deps) { 111 | memo.deps = deps 112 | memo.val = create() 113 | } 114 | return memo.val 115 | } 116 | 117 | func UseCallback[Func any](handlerFunc Func, deps Deps) *Callback[Func] { 118 | return UseMemo(func() *Callback[Func] { 119 | return &Callback[Func]{invoke: handlerFunc} 120 | }, deps) 121 | } 122 | 123 | type Ref[T any] struct { 124 | Value T 125 | } 126 | 127 | func UseRef[T any](initialValue T) *Ref[T] { 128 | return UseMemo[*Ref[T]](func() *Ref[T] { return &Ref[T]{Value: initialValue} }, Deps{}) 129 | } 130 | 131 | func useAtomSubscription[T comparable](atom *Atom[T]) { 132 | node := currentNode 133 | UseImmediateEffect(func() EffectTeardown { 134 | atom.subscribe(node) 135 | return func() { 136 | atom.unsubscribe(node) 137 | } 138 | }, Deps{node}) 139 | } 140 | 141 | func UseAtom[T comparable](atom *Atom[T]) (T, func(func(T) T)) { 142 | useAtomSubscription(atom) 143 | return atom.value, atom.update 144 | } 145 | 146 | func UseAtomValue[T comparable](atom *Atom[T]) T { 147 | useAtomSubscription(atom) 148 | return atom.value 149 | } 150 | 151 | func UseAtomSetter[T comparable](atom *Atom[T]) func(func(T) T) { 152 | return atom.update 153 | } 154 | 155 | func UseAtomSelector[T comparable, R any](atom *Atom[T], selector func(T) R) R { 156 | node := currentNode 157 | selected := selector(atom.value) 158 | UseImmediateEffect(func() EffectTeardown { 159 | selects, ok := atom.selectors.Get(node) 160 | record := &selectorRecord[T]{ 161 | selected: selected, 162 | selector: func(t T) any { return selector(t) }, 163 | } 164 | if ok { 165 | selects = append(selects, record) 166 | atom.selectors.Set(node, selects) 167 | } else { 168 | atom.selectors.Set(node, []*selectorRecord[T]{record}) 169 | } 170 | return func() { atom.selectors.Delete(node) } 171 | }, nil) 172 | return selected 173 | } 174 | -------------------------------------------------------------------------------- /input_event.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | // type InputEvent struct { 4 | // val js.Value 5 | // } 6 | 7 | // func newInputEvent(val js.Value) *InputEvent { 8 | // return &InputEvent{ 9 | // val: val, 10 | // } 11 | // } 12 | 13 | // func (e *InputEvent) Value() string { 14 | // return e.val.Get("target").Get("value").String() 15 | // } 16 | -------------------------------------------------------------------------------- /maps/maps.go: -------------------------------------------------------------------------------- 1 | package maps 2 | 3 | type Map[K comparable, V any] struct { 4 | m map[K]V 5 | s []K 6 | } 7 | 8 | func New[K comparable, V any]() *Map[K, V] { 9 | return &Map[K, V]{ 10 | m: make(map[K]V), 11 | } 12 | } 13 | 14 | func (m *Map[K, V]) Set(key K, value V) { 15 | if _, ok := m.m[key]; ok { 16 | m.m[key] = value 17 | return 18 | } 19 | m.m[key] = value 20 | m.s = append(m.s, key) 21 | } 22 | 23 | func (m *Map[K, V]) Has(key K) bool { 24 | _, ok := m.m[key] 25 | return ok 26 | } 27 | 28 | func (m *Map[K, V]) Get(key K) (V, bool) { 29 | v, ok := m.m[key] 30 | return v, ok 31 | } 32 | 33 | func (m *Map[K, V]) Delete(key K) { 34 | if !m.Has(key) { 35 | return 36 | } 37 | delete(m.m, key) 38 | for i := 0; i < len(m.s); i++ { 39 | if m.s[i] == key { 40 | m.s = append(m.s[:i], m.s[i+1:]...) 41 | return 42 | } 43 | } 44 | } 45 | 46 | type Entry[K comparable, V any] struct { 47 | Key K 48 | Value V 49 | } 50 | 51 | func (m *Map[K, V]) Slice() []Entry[K, V] { 52 | resp := make([]Entry[K, V], 0, len(m.s)) 53 | for k, v := range m.m { 54 | resp = append(resp, Entry[K, V]{ 55 | Key: k, 56 | Value: v, 57 | }) 58 | } 59 | return resp 60 | } 61 | -------------------------------------------------------------------------------- /mount.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | func Mount(selector string, component func(NoProps) *Node) { 4 | mount(createDom(Component(component, nil), ""), selector) 5 | select {} 6 | } 7 | -------------------------------------------------------------------------------- /mouse_event.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | import "syscall/js" 4 | 5 | // MouseEvent . 6 | type MouseEvent struct { 7 | val js.Value 8 | } 9 | 10 | func newMouseEvent(val js.Value) *MouseEvent { 11 | return &MouseEvent{ 12 | val: val, 13 | } 14 | } 15 | 16 | // PreventDefault . 17 | func (e *MouseEvent) PreventDefault() { 18 | e.val.Call("preventDefault") 19 | } 20 | 21 | // StopPropogation . 22 | func (e *MouseEvent) StopPropogation() { 23 | e.val.Call("stopPropogation") 24 | } 25 | 26 | // OffsetX . 27 | func (e *MouseEvent) OffsetX() int { 28 | return e.val.Get("offsetX").Int() 29 | } 30 | 31 | // OffsetY . 32 | func (e *MouseEvent) OffsetY() int { 33 | return e.val.Get("offsetY").Int() 34 | } 35 | 36 | // ClientX . 37 | func (e *MouseEvent) ClientX() int { 38 | return e.val.Get("clientX").Int() 39 | } 40 | 41 | // ClientY . 42 | func (e *MouseEvent) ClientY() int { 43 | return e.val.Get("clientY").Int() 44 | } 45 | 46 | // ShiftKey . 47 | func (e *MouseEvent) ShiftKey() bool { 48 | return e.val.Get("shiftKey").Bool() 49 | } 50 | 51 | // AltKey . 52 | func (e *MouseEvent) AltKey() bool { 53 | return e.val.Get("altKey").Bool() 54 | } 55 | 56 | // CtrlKey . 57 | func (e *MouseEvent) CtrlKey() bool { 58 | return e.val.Get("ctrlKey").Bool() 59 | } 60 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "syscall/js" 7 | ) 8 | 9 | type NoProps any 10 | 11 | type Node struct { 12 | tag string 13 | namespace string 14 | ptr uintptr 15 | render func() *Node 16 | key string 17 | attrs *Attributes 18 | textContent string 19 | ref *Ref[js.Value] 20 | refs []int 21 | dom int 22 | unmounted bool 23 | children []*Node 24 | 25 | virtNode *Node 26 | queue []*Node 27 | hooks []any 28 | hooksCursor int 29 | memo Deps 30 | } 31 | 32 | func (n *Node) teardown() { 33 | if n.virtNode != nil { 34 | n.unmounted = true 35 | n.queue = nil 36 | for _, h := range n.hooks { 37 | if effect, ok := h.(*effectRecord); ok { 38 | if effect.teardown != nil { 39 | effect.teardown() 40 | } 41 | } 42 | } 43 | n.virtNode.teardown() 44 | return 45 | } 46 | if n.ref != nil { 47 | n.ref.Value = js.Undefined() 48 | } 49 | for _, ch := range n.children { 50 | ch.teardown() 51 | } 52 | if n.attrs != nil && n.attrs.OnClick != nil { 53 | delete(clickListeners, n.dom) 54 | } 55 | disposeNode(n.dom) 56 | for _, n := range n.refs { 57 | disposeNode(n) 58 | } 59 | n.refs = n.refs[:0] 60 | n.dom = 0 61 | } 62 | 63 | type Keyer interface { 64 | Key() string 65 | } 66 | 67 | type Memoer interface { 68 | Memo() Deps 69 | } 70 | 71 | func Component[T any](ty func(T) *Node, props T) *Node { 72 | n := &Node{ 73 | ptr: uintptr(reflect.ValueOf(ty).UnsafePointer()), 74 | render: func() *Node { return ty(props) }, 75 | } 76 | if keyer, ok := any(props).(Keyer); ok { 77 | n.key = keyer.Key() 78 | } 79 | if memoer, ok := any(props).(Memoer); ok { 80 | n.memo = memoer.Memo() 81 | } 82 | return n 83 | } 84 | 85 | var currentNode *Node 86 | 87 | func callComponentFunc(node *Node) *Node { 88 | prev := currentNode 89 | currentNode = node 90 | node.hooksCursor = 0 91 | vd := node.render() 92 | if vd == nil { 93 | vd = &Node{} 94 | } 95 | currentNode = prev 96 | return vd 97 | } 98 | 99 | type Attributes struct { 100 | ID string 101 | Class string 102 | Disabled bool 103 | Style string 104 | Value string 105 | Key string 106 | 107 | // Slot must be string, int, *Node, []*Node, func(NoProps) *Node, or nil 108 | Slot any 109 | Type string 110 | Ref *Ref[js.Value] 111 | 112 | AriaHidden bool 113 | 114 | // Common UIEvents: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events 115 | // All Events: https://developer.mozilla.org/en-US/docs/Web/API/Event 116 | // 117 | // MouseEvent: click, dblclick, mouseup, mousedown 118 | // InputEvent: input, beforeinput 119 | // KeyboardEvent: keydown, keypress, keyup 120 | // CompositionEvent: compositionstart, compositionend, compositionupdate 121 | // WheelEvent: wheel 122 | // FocusEvent: focus, blur, focusin, and focusout 123 | 124 | OnClick *Callback[func(*MouseEvent)] 125 | // OnMouseMove *Callback[func(*MouseEvent)] 126 | // OnInput *Callback[func(*InputEvent)] 127 | } 128 | 129 | func Element(tag string, attrs *Attributes) *Node { 130 | n := &Node{ 131 | tag: tag, 132 | attrs: attrs, 133 | key: attrs.Key, 134 | ref: attrs.Ref, 135 | } 136 | if attrs.Slot != nil { 137 | switch slot := attrs.Slot.(type) { 138 | case string: 139 | n.textContent = slot 140 | case int: 141 | n.textContent = strconv.Itoa(slot) 142 | case *Node: 143 | n.children = []*Node{slot} 144 | case func(NoProps) *Node: 145 | n.children = []*Node{Component(slot, nil)} 146 | case []*Node: 147 | n.children = slot 148 | case []any: 149 | n.children = make([]*Node, len(slot)) 150 | for i := 0; i < len(slot); i++ { 151 | n.children[i] = makeNode(slot[i]) 152 | } 153 | case []func(NoProps) *Node: 154 | n.children = make([]*Node, len(slot)) 155 | for i := 0; i < len(slot); i++ { 156 | n.children[i] = Component(slot[i], nil) 157 | } 158 | case []string: 159 | n.children = make([]*Node, len(slot)) 160 | for i := 0; i < len(slot); i++ { 161 | n.children[i] = text(slot[i]) 162 | } 163 | case []int: 164 | n.children = make([]*Node, len(slot)) 165 | for i := 0; i < len(slot); i++ { 166 | n.children[i] = text(strconv.Itoa(slot[i])) 167 | } 168 | } 169 | } 170 | return n 171 | } 172 | 173 | func text(content string) *Node { 174 | return &Node{ 175 | textContent: content, 176 | } 177 | } 178 | 179 | func makeNode(v any) *Node { 180 | switch chn := v.(type) { 181 | case string: 182 | return text(chn) 183 | case int: 184 | return text(strconv.Itoa(chn)) 185 | case *Node: 186 | return chn 187 | case func(NoProps) *Node: 188 | return Component(chn, nil) 189 | } 190 | return nil 191 | } 192 | 193 | func createDom(node *Node, ns string) int { 194 | if node.dom != 0 { 195 | node.refs = append(node.refs, node.dom) 196 | node.dom = cloneNode(node.dom) 197 | return node.dom 198 | } 199 | if node.tag != "" { 200 | clicks := node.attrs.OnClick != nil 201 | if node.tag == "svg" { 202 | ns = "http://www.w3.org/2000/svg" 203 | } else if node.tag == "math" { 204 | ns = "http://www.w3.org/1998/Math/MathML" 205 | } 206 | switch node.tag { 207 | case "tr": 208 | node.dom = createTr(clicks) 209 | case "span": 210 | node.dom = createSpan(clicks) 211 | case "td": 212 | node.dom = createTd(clicks) 213 | case "a": 214 | node.dom = createA(clicks) 215 | case "h1": 216 | node.dom = createH1(clicks) 217 | case "div": 218 | node.dom = createDiv(clicks) 219 | case "table": 220 | node.dom = createTable(clicks) 221 | case "tbody": 222 | node.dom = createTbody(clicks) 223 | case "button": 224 | node.dom = createButton(clicks) 225 | default: 226 | if ns == "" { 227 | node.dom = createElement(node.tag, clicks) 228 | } else { 229 | node.dom = createElementNS(node.tag, ns, clicks) 230 | } 231 | } 232 | if node.ref != nil { 233 | node.ref.Value = getJsValue(node.dom) 234 | } 235 | if node.attrs.Disabled { 236 | setBool(node.dom, "disabled", true) 237 | } 238 | if node.attrs.Class != "" { 239 | setClass(node.dom, node.attrs.Class) 240 | } 241 | if node.attrs.Style != "" { 242 | setStr(node.dom, "style", node.attrs.Style) 243 | } 244 | if node.attrs.ID != "" { 245 | setStr(node.dom, "id", node.attrs.ID) 246 | } 247 | if node.attrs.AriaHidden { 248 | setAriaHidden(node.dom, true) 249 | } 250 | if node.attrs.Value != "" { 251 | setStr(node.dom, "value", node.attrs.Value) 252 | } 253 | if node.textContent != "" { 254 | setTextContent(node.dom, node.textContent) 255 | } 256 | if clicks { 257 | clickListeners[node.dom] = node.attrs.OnClick.invoke 258 | } 259 | node.namespace = ns 260 | for _, child := range node.children { 261 | appendChild(node.dom, createDom(child, ns)) 262 | } 263 | } else if node.render != nil { 264 | node.virtNode = callComponentFunc(node) 265 | return createDom(node.virtNode, ns) 266 | } else { 267 | node.dom = createTextNode(node.textContent) 268 | return node.dom 269 | } 270 | return node.dom 271 | } 272 | -------------------------------------------------------------------------------- /reconcile.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | import ( 4 | "syscall/js" 5 | ) 6 | 7 | func getDom(node *Node) int { 8 | for node.virtNode != nil { 9 | node = node.virtNode 10 | } 11 | return node.dom 12 | } 13 | 14 | func reconcile(oldNode *Node, newNode *Node) { 15 | newNode.unmounted = false 16 | if oldNode.tag != newNode.tag || oldNode.ptr != newNode.ptr { 17 | oldDom := getDom(oldNode) 18 | replaceWith(oldDom, createDom(newNode, oldNode.namespace)) 19 | oldNode.teardown() 20 | } else if oldNode.render != nil { 21 | reconcileComponents(oldNode, newNode) 22 | } else { 23 | newNode.dom = oldNode.dom 24 | if oldNode.tag != "" { 25 | reconcileVdomElems(oldNode, newNode) 26 | } else if oldNode.textContent != newNode.textContent { 27 | setData(newNode.dom, newNode.textContent) 28 | } 29 | } 30 | } 31 | 32 | func reconcileVdomElems(oldNode *Node, newNode *Node) { 33 | if areDepsEqual(oldNode.memo, newNode.memo) { 34 | return 35 | } 36 | if oldNode.textContent != newNode.textContent { 37 | setTextContent(newNode.dom, newNode.textContent) 38 | } 39 | reconcileAttributes(oldNode, newNode) 40 | reconcileChildren(oldNode, newNode) 41 | if newNode.ref != nil { 42 | if oldNode.ref != nil { 43 | newNode.ref.Value = oldNode.ref.Value 44 | } else { 45 | newNode.ref.Value = getJsValue(newNode.dom) 46 | } 47 | } else if oldNode.ref != nil { 48 | oldNode.ref.Value = js.Undefined() 49 | } 50 | } 51 | 52 | func reconcileComponents(oldNode *Node, newNode *Node) { 53 | newNode.hooks = oldNode.hooks 54 | if areDepsEqual(oldNode.memo, newNode.memo) { 55 | newNode.virtNode = oldNode.virtNode 56 | return 57 | } 58 | oldNode.queue = nil 59 | callComponentFuncAndReconcile(oldNode, newNode) 60 | } 61 | 62 | func reconcileStringAttribute(oldAttr string, newAttr string, name string, ref int) { 63 | if oldAttr != newAttr { 64 | if newAttr == "" { 65 | removeAttribute(ref, name) 66 | } else { 67 | setStr(ref, name, newAttr) 68 | } 69 | } 70 | } 71 | 72 | func reconcileBoolAttribute(oldAttr bool, newAttr bool, name string, ref int) { 73 | if oldAttr != newAttr { 74 | if !newAttr { 75 | removeAttribute(ref, name) 76 | } else { 77 | setBool(ref, name, true) 78 | } 79 | } 80 | } 81 | 82 | func reconcileAttributes(oldNode *Node, newNode *Node) { 83 | oldAttrs := oldNode.attrs 84 | newAttrs := newNode.attrs 85 | 86 | if oldAttrs.Class != newAttrs.Class { 87 | if newAttrs.Class == "" { 88 | removeAttribute(oldNode.dom, "class") 89 | } else { 90 | setStr(oldNode.dom, "className", newAttrs.Class) 91 | } 92 | } 93 | 94 | if oldAttrs.AriaHidden != newAttrs.AriaHidden { 95 | if !newAttrs.AriaHidden { 96 | removeAttribute(oldNode.dom, "aria-hidden") 97 | } else { 98 | setAriaHidden(oldNode.dom, true) 99 | } 100 | } 101 | reconcileBoolAttribute(oldAttrs.Disabled, newAttrs.Disabled, "disabled", oldNode.dom) 102 | reconcileStringAttribute(oldAttrs.Style, newAttrs.Style, "style", oldNode.dom) 103 | reconcileStringAttribute(oldAttrs.Class, newAttrs.Class, "class", oldNode.dom) 104 | reconcileStringAttribute(oldAttrs.ID, newAttrs.ID, "id", oldNode.dom) 105 | reconcileStringAttribute(oldAttrs.Value, newAttrs.Value, "value", oldNode.dom) 106 | if oldAttrs.OnClick != newAttrs.OnClick { 107 | clickListeners[newNode.dom] = newAttrs.OnClick.invoke 108 | } 109 | } 110 | 111 | func callComponentFuncAndReconcile(oldNode *Node, newNode *Node) { 112 | newElemVdom := callComponentFunc(newNode) 113 | reconcile(oldNode.virtNode, newElemVdom) 114 | newNode.virtNode = newElemVdom 115 | } 116 | 117 | func reconcileChildren(oldNode *Node, newNode *Node) { 118 | newChn := newNode.children 119 | oldChn := oldNode.children 120 | newLength := len(newChn) 121 | oldLength := len(oldChn) 122 | if newLength == 0 && oldLength > 0 { 123 | setTextContent(newNode.dom, "") 124 | for _, ch := range oldChn { 125 | ch.teardown() 126 | } 127 | return 128 | } 129 | start := 0 130 | 131 | // prefix 132 | for start < newLength && start < oldLength { 133 | o := oldChn[start] 134 | n := newChn[start] 135 | if n.key == "" || n.key == o.key { 136 | reconcile(o, n) 137 | } else { 138 | break 139 | } 140 | start++ 141 | } 142 | if start >= newLength { 143 | for start < oldLength { 144 | removeNode(getDom(oldChn[start])) 145 | oldChn[start].teardown() 146 | start++ 147 | } 148 | return 149 | } 150 | 151 | // suffix 152 | oldLength-- 153 | newLength-- 154 | for newLength > start && oldLength >= start { 155 | o := oldChn[oldLength] 156 | n := newChn[newLength] 157 | if n.key == "" || n.key == o.key { 158 | reconcile(o, n) 159 | } else { 160 | break 161 | } 162 | oldLength-- 163 | newLength-- 164 | } 165 | 166 | oldMap := make(map[string]*Node) 167 | for i := start; i <= oldLength; i++ { 168 | oldChd := oldChn[i] 169 | oldKey := oldChd.key 170 | noMoreNewChn := false 171 | if i >= len(newChn) { 172 | noMoreNewChn = true 173 | } 174 | if oldKey != "" && (noMoreNewChn || oldKey != newChn[i].key) { 175 | oldMap[oldKey] = oldChd 176 | } 177 | } 178 | 179 | for start <= newLength { 180 | if len(oldChn) <= start { 181 | for i := start; i <= newLength; i++ { 182 | appendChild(newNode.dom, createDom(newChn[i], newNode.namespace)) 183 | } 184 | break 185 | } 186 | 187 | newChd := newChn[start] 188 | oldChd := oldChn[start] 189 | if oldChd.key == newChd.key { 190 | reconcile(oldChd, newChd) 191 | start++ 192 | continue 193 | } 194 | var nextIsCorrect bool 195 | if len(newChn) > start+1 { 196 | nextIsCorrect = newChn[start+1].key == oldChd.key 197 | } 198 | var oldDom int 199 | if mappedOld, ok := oldMap[newChd.key]; ok { 200 | oldDom = getDom(mappedOld) 201 | reconcile(mappedOld, newChd) 202 | delete(oldMap, newChd.key) 203 | } else { 204 | oldDom = createDom(newChd, newNode.namespace) 205 | } 206 | moveBefore(newNode.dom, nextIsCorrect, start, oldDom) 207 | start++ 208 | } 209 | 210 | for _, node := range oldMap { 211 | removeNode(getDom(node)) 212 | node.teardown() 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /sets/sets.go: -------------------------------------------------------------------------------- 1 | package sets 2 | 3 | type Set[T comparable] struct { 4 | m map[T]struct{} 5 | s []T 6 | } 7 | 8 | func New[T comparable]() *Set[T] { 9 | return &Set[T]{ 10 | m: make(map[T]struct{}), 11 | } 12 | } 13 | 14 | func (s *Set[T]) Add(value T) { 15 | if _, ok := s.m[value]; ok { 16 | return 17 | } 18 | s.m[value] = struct{}{} 19 | s.s = append(s.s, value) 20 | } 21 | 22 | func (s *Set[T]) Has(value T) bool { 23 | _, ok := s.m[value] 24 | return ok 25 | } 26 | 27 | func (s *Set[T]) Delete(value T) { 28 | if !s.Has(value) { 29 | return 30 | } 31 | delete(s.m, value) 32 | for i := 0; i < len(s.s); i++ { 33 | if s.s[i] == value { 34 | s.s = append(s.s[:i], s.s[i+1:]...) 35 | return 36 | } 37 | } 38 | } 39 | 40 | func (s *Set[T]) Slice() []T { 41 | resp := make([]T, len(s.s)) 42 | copy(resp, s.s) 43 | return resp 44 | } 45 | -------------------------------------------------------------------------------- /syscall.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | import ( 4 | "syscall/js" 5 | ) 6 | 7 | var global = js.Global() 8 | var document = global.Get("document") 9 | var console = global.Get("console") 10 | 11 | func Log(args ...any) { 12 | console.Call("log", args...) 13 | } 14 | 15 | //export createElement 16 | func createElement(tag string, clicks bool) int 17 | 18 | //export createTd 19 | func createTd(clicks bool) int 20 | 21 | //export createTr 22 | func createTr(clicks bool) int 23 | 24 | //export createSpan 25 | func createSpan(clicks bool) int 26 | 27 | //export createDiv 28 | func createDiv(clicks bool) int 29 | 30 | //export createTable 31 | func createTable(clicks bool) int 32 | 33 | //export createTbody 34 | func createTbody(clicks bool) int 35 | 36 | //export createH1 37 | func createH1(clicks bool) int 38 | 39 | //export createA 40 | func createA(clicks bool) int 41 | 42 | //export createButton 43 | func createButton(clicks bool) int 44 | 45 | //export createElementNS 46 | func createElementNS(tag string, ns string, clicks bool) int 47 | 48 | //export createTextNode 49 | func createTextNode(text string) int 50 | 51 | //export appendChild 52 | func appendChild(parent int, child int) 53 | 54 | //export replaceWith 55 | func replaceWith(old, new int) 56 | 57 | //export moveBefore 58 | func moveBefore(parent int, nextKeyMatch bool, start int, movingDomNode int) 59 | 60 | //export mount 61 | func mount(child int, selector string) 62 | 63 | //export setStr 64 | func setStr(child int, prop string, val string) 65 | 66 | //export setClass 67 | func setClass(child int, val string) 68 | 69 | //export setTextContent 70 | func setTextContent(child int, val string) 71 | 72 | //export setData 73 | func setData(child int, val string) 74 | 75 | //export setAriaHidden 76 | func setAriaHidden(child int, val bool) 77 | 78 | //export setBool 79 | func setBool(child int, prop string, val bool) 80 | 81 | //export removeAttribute 82 | func removeAttribute(child int, attr string) 83 | 84 | //export removeNode 85 | func removeNode(node int) 86 | 87 | //export disposeNode 88 | func disposeNode(node int) 89 | 90 | //export cloneNode 91 | func cloneNode(node int) int 92 | 93 | var clickListeners = map[int]func(*MouseEvent){} 94 | 95 | // func init() { 96 | // var memStats runtime.MemStats 97 | // go func() { 98 | // for range time.NewTicker(time.Second).C { 99 | // runtime.ReadMemStats(&memStats) 100 | // fmt.Printf("HeapInuse=%d;Frees=%d\n", memStats.HeapInuse/1e6, memStats.Frees) 101 | // } 102 | // }() 103 | // } 104 | 105 | var _listener func(*MouseEvent) 106 | var _callClickListener = js.FuncOf(func(this js.Value, args []js.Value) any { 107 | _listener(newMouseEvent(args[0])) 108 | return nil 109 | }) 110 | 111 | //export callClickListener 112 | func callClickListener(node int) { 113 | if listener, ok := clickListeners[node]; ok { 114 | // println("listener called") 115 | // listener(newMouseEvent(global.Get("_GOUI_EVENT"))) 116 | // runtime.GC() 117 | _listener = listener 118 | _callClickListener.Invoke(global.Get("_GOUI_EVENT")) 119 | } 120 | } 121 | 122 | var elements = global.Get("_GOUI_ELEMENTS") 123 | 124 | func getJsValue(ref int) js.Value { 125 | return elements.Index(int(ref)) 126 | } 127 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | func areDepsEqual(a Deps, b Deps) bool { 4 | if a == nil || b == nil || len(a) != len(b) { 5 | return false 6 | } 7 | for i := range a { 8 | if a[i] != b[i] { 9 | return false 10 | } 11 | } 12 | return true 13 | } 14 | 15 | func Map[T any](s []T, m func(item T) *Node) []*Node { 16 | k := make([]*Node, len(s)) 17 | for i := range s { 18 | k[i] = m(s[i]) 19 | } 20 | return k 21 | } 22 | --------------------------------------------------------------------------------