-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathengine.go
More file actions
199 lines (159 loc) · 4.59 KB
/
engine.go
File metadata and controls
199 lines (159 loc) · 4.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package rx
import (
"log/slog"
"reflect"
)
type Engine struct {
Actions chan Action
XAS chan XAS
buf XAS
free chan XAS
cnt Counter
logger *slog.Logger
genHandler *genLogHandler
// access to all below is protected by inrenderpass Javascript lock
// these remember the previous state
et etree
gen int
g0 *vctx
k0, k1 *keyedEntity
Root RootWidget
Screen Coord
CallFrame
}
type RootWidget Widget
// New initializes a rendering engine, rooted at root.
// The second value is a start function, to execute when rendering the engine:
//
// ngx, start := rx.New()
// // finish initialization with ngx
// start()
func New(root Widget, ctx ...Action) *Engine {
ng := &Engine{
XAS: make(chan XAS),
free: make(chan XAS),
Actions: make(chan Action), // protect the call frame until processed
Root: root,
genHandler: newLogHandler(),
}
ng.g0 = &vctx{kv: make(map[reflect.Type]any)}
for _, f := range ctx {
ng.g0 = f(Context{vx: ng.g0}).vx
}
ng.logger = slog.New(ng.genHandler)
go func() {
for act := range ng.Actions {
if xas := ng.turncrank(act); xas != nil {
ng.XAS <- xas
ng.buf = <-ng.free // wait for the Return of the Buffer
}
// Note about the order: the continuation must be called synchronously
// so we can set correctly drag and drop data [dnd].
// Still, we make sure that the continuation happens after the view is updated.
// This is also happening even if no rendering happens (the NoAction context).
//
// [dnd] https://html.spec.whatwg.org/multipage/dnd.html#concept-dnd-rw
if ng.Continuation != nil {
ng.Continuation <- ng.CallFrame
}
ng.CallFrame = CallFrame{} // clear allow reacting to non-UI event
}
}()
// empty action primes the loop
return ng
}
func Mouse_(ctx Context) Coord { return ctx.ng.Mouse }
func Screen_(ctx Context) Coord { return ctx.ng.Screen }
func Point_(ctx Context) int { return ctx.ng.Point }
func Entity_(ctx Context) Entity { return ctx.ng.Entity }
func Actions_(ctx Context) chan Action { return ctx.ng.Actions }
func Modifers_(ctx Context) Modifiers { return ctx.ng.Modifiers }
func Logger_(ctx Context) *slog.Logger { return ctx.ng.logger }
// Coordinate of any object in the viewport
//
// As per UI convention, X is left to right, and Y is top to bottom
type Coord struct{ X, Y int }
type Action func(Context) Context
type Widget interface{ Build(Context) *Node }
type Modifiers struct{ CTRL, SHIFT, ALT bool }
// WidgetFunc represents the simples form of a widget, without state, nor handlers.
type WidgetFunc func(Context) *Node
func (f WidgetFunc) Build(ctx Context) *Node { return f(ctx) }
var noAction Context
// turncrank executes all the systems in turn, and returns a virtual machine for the Javascript code to execute.
// The render loop is not supposed to be executed solely based on a timing (e.g. every 60ms), but instead react to intents.
func (ng *Engine) turncrank(act Action) XAS {
defer func() {
if r := recover(); r != nil {
ng.genHandler.Dump()
panic(r)
}
ng.genHandler.Discard()
}()
ctx := act(Context{ng: ng, vx: ng.g0})
if ctx == noAction {
return nil
}
nd := ng.Root.Build(ctx)
ng.buf = serialize(nd, &ng.et, &ng.cnt, ng.buf[:0]).AddInstr(OpTerm)
ng.g0 = ctx.vx
ng.et.ngen()
ng.gen++
ng.cnt = Counter(ng.gen & 1)
ng.k0, ng.k1 = nil, ng.k0
FreePool()
return ng.buf
}
// ReleaseXAS is used by the main routine to prevent too much allocations
func (ng *Engine) ReleaseXAS(buf XAS) { ng.free <- buf }
// ReactToIntent
func (ng *Engine) ReactToIntent(cf CallFrame) {
do := func(ctx Context) Context {
if cf.Gen != ng.gen {
return ctx
}
ng.CallFrame = cf
var h intentHandler
for _, nt := range ng.et.parents(cf.Entity) {
if nt.hdl[cf.IntentType] != nil {
h = nt.hdl
break
}
}
if h[cf.IntentType] == nil {
return noAction
}
return h[cf.IntentType](ctx)
}
ng.Actions <- Action(do)
}
type IntentType int
//go:generate go tool stringer -type IntentType
//go:generate go tool rxabi -type IntentType
const (
NoIntent IntentType = iota
Click
DoubleClick
DragStart
DragOver
DragEnd
Drop
EscPress
Scroll
Filter
Change
KeyUp
Blur
ChangeView
ManifestChange
ShowDebugMenu
CellSizeChange
Submit
Seppuku // must be last, used to size intentHandler
// run "go generate ./..." after updating this list
)
type Entity = uint32
type Counter Entity
func (c *Counter) Inc() Entity { *c += 2; return Entity(*c) }
// TODO(rdo) animation in renderloop
// https://www.notion.so/wiggly-trout-software/Client-Architecture-128b4ef941b644a98f28d71904632aad#14429d96ccc44cc59b8826ef4649aa0a