/
uiCore.go
501 lines (453 loc) · 16.7 KB
/
uiCore.go
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
package framework
import (
"fmt"
"github.com/lexisother/frenyard"
)
// FocusEvent is an event type specific to the UI framework that represents focusing/unfocusing the receiving element.
type FocusEvent struct {
// True if this was a focus, false if this was an unfocus.
Focused bool
}
// FyVRoute implements NormalEvent.FyVRoute
func (fe FocusEvent) FyVRoute() frenyard.NormalEventRoute {
return frenyard.NormalEventRouteStop
}
// FyVOffset implements NormalEvent.FyVOffset
func (fe FocusEvent) FyVOffset(o frenyard.Vec2i) frenyard.NormalEvent {
return fe
}
/*
* EXPLAINATION: EnterWindowEvent is sent when the root element is attached to the binding.
* It's set to NormalEventRouteStructuralBroadcast. It is intended to go EVERYWHERE.
* It is rebroadcast by panels when their contents change.
* As panels and proxies are the only elements involved in event routing, this essentially means it is guaranteed to reach every element.
*/
// EnterWindowEvent is an event type specific to the UI framework that represents the element being attached to a window.
type EnterWindowEvent struct {
Window frenyard.Window
}
// FyVRoute implements NormalEvent.FyVRoute
func (fe EnterWindowEvent) FyVRoute() frenyard.NormalEventRoute {
return frenyard.NormalEventRouteStructuralBroadcast
}
// FyVOffset implements NormalEvent.FyVOffset
func (fe EnterWindowEvent) FyVOffset(o frenyard.Vec2i) frenyard.NormalEvent {
return fe
}
/*
* This is the core UIElement type without layout capabilities.
* Simply put, if it's being drawn, it's this type.
*/
// UIElement is the core UI element type (no layout capabilities). An implementation must contain UIElementComponent or UIProxy.
type UIElement interface {
FyENormalEvent(ev frenyard.NormalEvent)
FyEMouseEvent(ev frenyard.MouseEvent)
// Updates the element.
FyETick(deltaTime float64)
/*
* Drawing occurs in two passes: The 'under' pass and the main pass.
* If the element has a shadow, it draws that in the 'under' pass.
* As such, if an element has a background, it draws the shadow for that (if any) in the 'under' pass,
* and splits its main pass into, in order: background, sub-element 'under' pass, sub-element main pass.
*/
FyEDraw(target frenyard.Renderer, under bool)
/*
* Sets FyESize.
* FyESize MUST NOT change without FyEResize being used.
* FyEResize MUST ONLY be called if:
* 1. the parent element/window to which this element is attached is doing it
* 2. there is no parent element/window (setting default)
* 3. the parameter is equal to FyESize() (relayout)
* FyESize SHOULD default to a reasonable default size for the element.
*/
FyEResize(size frenyard.Vec2i)
FyESize() frenyard.Vec2i
// Attempts to find an element relative to Area2iOfSize(FyESize()). Does not check for itself. Returns an empty area for 'not found'.
FyEFindElement(target UIElement) frenyard.Area2i
}
/*
* A correct implementation of FyEResize & FyESize.
* Part of core so it can't possibly get broken.
*/
// UIElementComponent implements the resizing logic for UIElement and default method implementations.
type UIElementComponent struct {
// SUPER DUPER PRIVATE! DO NOT ACCESS OUTSIDE OF MEMBER METHODS.
_fyUIElementComponentSize frenyard.Vec2i
}
// NewUIElementComponent creates a new UIElementComponent.
func NewUIElementComponent(size frenyard.Vec2i) UIElementComponent {
return UIElementComponent{size}
}
// FyEResize implements UIElement.FyEResize
func (es *UIElementComponent) FyEResize(size frenyard.Vec2i) {
es._fyUIElementComponentSize = size
}
// FyESize implements UIElement.FyESize
func (es *UIElementComponent) FyESize() frenyard.Vec2i {
return es._fyUIElementComponentSize
}
// FyEFindElement implements UIElement.FyEFindElement
func (es *UIElementComponent) FyEFindElement(target UIElement) frenyard.Area2i {
return frenyard.Area2i{}
}
type fyWindowElementBinding struct {
window frenyard.Window
clearColour uint32
element UIElement
}
// CreateBoundWindow creates a window that is bound to an element.
func CreateBoundWindow(title string, vsync bool, clearColour uint32, e UIElement) (frenyard.Window, error) {
return frenyard.GlobalBackend.CreateWindow(title, e.FyESize(), vsync, &fyWindowElementBinding{
nil,
clearColour,
e,
})
}
// FyRStart implements WindowReceiver.FyRStart
func (web *fyWindowElementBinding) FyRStart(w frenyard.Window) {
web.window = w
web.element.FyENormalEvent(EnterWindowEvent{w})
web.element.FyENormalEvent(FocusEvent{true})
}
// FyRTick implements WindowReceiver.FyRTick
func (web *fyWindowElementBinding) FyRTick(f float64) {
if !web.window.Size().Eq(web.element.FyESize()) {
web.element.FyEResize(web.window.Size())
}
web.element.FyETick(f)
web.window.Reset(web.clearColour)
web.element.FyEDraw(web.window, true)
web.element.FyEDraw(web.window, false)
web.window.Present()
}
// FyRNormalEvent implements WindowReceiver.FyRNormalEvent
func (web *fyWindowElementBinding) FyRNormalEvent(ev frenyard.NormalEvent) {
web.element.FyENormalEvent(ev)
}
// FyRMouseEvent implements WindowReceiver.FyRMouseEvent
func (web *fyWindowElementBinding) FyRMouseEvent(ev frenyard.MouseEvent) {
web.element.FyEMouseEvent(ev)
}
// FyRClose implements WindowReceiver.FyRClose
func (web *fyWindowElementBinding) FyRClose() {
}
// PanelFixedElement describes an element attached to a panel.
type PanelFixedElement struct {
Pos frenyard.Vec2i
// Setting this to false is useful if you want an element to still tick but want to remove the drawing overhead.
Visible bool
// Setting this to true 'locks' the element. The element still participates in hit-tests but fails to focus and events are NOT forwarded.
Locked bool
Element UIElement
}
/*
* Basic "set it and forget it" stateful panel that does not transmit or receive layout data.
* This is part of core because it's responsible for implementing several UI rules, such as cursor forwarding.
*/
// UIPanel is a "set it and forget it" stateful panel for placing multiple elements into.
type UIPanel struct {
UIElementComponent
ThisUIPanelDetails UIPanelDetails
}
// UIPanelDetails contains the details of a UIPanel otherwise accessible only by it's owner.
type UIPanelDetails struct {
// Enables/disables clipping
Clipping bool
// This is a bitfield
_buttonsDown uint16
// Mouse event receiver. Be aware: Focus outside of Content is not a very good idea, except -1 (None)
_focus int
// Content (As far as I can tell there is no way to change the length of a slice without replacing it.)
_content []PanelFixedElement
// Cached window
_window frenyard.Window
}
// NewPanel creates a UIPanel.
func NewPanel(size frenyard.Vec2i) UIPanel {
return UIPanel{
NewUIElementComponent(size),
UIPanelDetails{
false,
0,
-1,
make([]PanelFixedElement, 0),
nil,
},
}
}
// SetContent sets the contents of the panel.
func (pan *UIPanelDetails) SetContent(content []PanelFixedElement) {
// Before we touch anything, broadcast EnterWindowEvent. Panels that were already in the same window will drop this.
if pan._window != nil {
for _, v := range content {
v.Element.FyENormalEvent(EnterWindowEvent{
pan._window,
})
}
}
// Is this actually a change we need to worry about?
// DO BE WARNED: THIS IS A LOAD-BEARING OPTIMIZATION. DISABLE IT AND BUTTONS DON'T WORK PROPERLY
// Reason: Clicking a button changes the button content which causes a layout rebuild.
// Layout rebuilds destroying focus also destroys the evidence the button was pressed.
changeCanBeIgnored := true
if len(content) != len(pan._content) {
changeCanBeIgnored = false
} else {
// Lengths are the same; if the elements are the same, we can just roll with it
for k, v := range content {
if pan._content[k].Element != v.Element {
changeCanBeIgnored = false
}
}
}
if !changeCanBeIgnored {
if pan._focus != -1 {
// Ensure the focus has been notified.
focusElement := pan._content[pan._focus]
// Has to occur before the buttons get removed or ordering issues occur.
pan._focus = -1
// And we've successfully delivered the MOUSEDOWNs to the *new* element, -1, by default
for button := (uint)(0); button < (uint)(frenyard.MouseButtonLength); button++ {
if pan._buttonsDown&(1<<button) != 0 {
focusElement.Element.FyEMouseEvent(frenyard.MouseEvent{
frenyard.Vec2i{0, 0},
frenyard.MouseEventUp,
(frenyard.MouseButton)(button),
})
}
}
focusElement.Element.FyENormalEvent(FocusEvent{false})
}
}
pan._content = content
}
// FyENormalEvent implements UIElement.FyENormalEvent
func (pan *UIPanel) FyENormalEvent(ev frenyard.NormalEvent) {
switch xev := ev.(type) {
case EnterWindowEvent:
if pan.ThisUIPanelDetails._window == xev.Window {
// Drop the event, it's redundant
return
}
pan.ThisUIPanelDetails._window = xev.Window
}
route := ev.FyVRoute()
if (route == frenyard.NormalEventRouteBroadcast) || (route == frenyard.NormalEventRouteStructuralBroadcast) {
for _, v := range pan.ThisUIPanelDetails._content {
if (route == frenyard.NormalEventRouteStructuralBroadcast) || (v.Visible && !v.Locked) {
v.Element.FyENormalEvent(ev.FyVOffset(v.Pos.Negate()))
}
}
} else {
if pan.ThisUIPanelDetails._focus != -1 {
elem := pan.ThisUIPanelDetails._content[pan.ThisUIPanelDetails._focus]
if elem.Visible && !elem.Locked {
elem.Element.FyENormalEvent(ev)
}
}
}
}
func (pan *UIPanel) _fyUIPanelForwardMouseEvent(target PanelFixedElement, ev frenyard.MouseEvent) {
ev = ev.Offset(target.Pos.Negate())
// Problematic mouse events are prevented from reaching locked targets via the hit-test logic.
target.Element.FyEMouseEvent(ev)
}
// FyEMouseEvent implements UIElement.FyEMouseEvent
func (pan *UIPanel) FyEMouseEvent(ev frenyard.MouseEvent) {
// Useful for debugging if any of the warnings come up
// if ev.ID != MouseEventMove { fmt.Printf("ui_core.go/Panel (%p)/FyEMouseEvent %v %v (%v, %v)\n", pan, ev.ID, ev.Button, ev.Pos.X, ev.Pos.Y) }
invalid := false
hittest := -1
buttonMask := (uint16)(0)
if ev.Button != -1 {
buttonMask = (uint16)(1 << (uint)(ev.Button))
}
// Hit-test goes in reverse so that the element drawn last wins.
for keyRev := range pan.ThisUIPanelDetails._content {
key := len(pan.ThisUIPanelDetails._content) - (keyRev + 1)
val := pan.ThisUIPanelDetails._content[key]
if !val.Visible {
continue
}
if frenyard.Area2iFromVecs(val.Pos, val.Element.FyESize()).Contains(ev.Pos) {
//fmt.Printf(" Hit index %v\n", key)
hittest = key
if val.Locked {
hittest = -1
}
break
}
}
switch ev.ID {
case frenyard.MouseEventMove:
// Mouse-move events go everywhere.
for _, val := range pan.ThisUIPanelDetails._content {
pan._fyUIPanelForwardMouseEvent(val, ev)
}
invalid = true
case frenyard.MouseEventUp:
if pan.ThisUIPanelDetails._buttonsDown&buttonMask == 0 {
fmt.Printf("ui_core.go/Panel (%p)/FyEMouseEvent warning: Button removal on non-existent button %v\n", pan, ev.Button)
invalid = true
} else {
pan.ThisUIPanelDetails._buttonsDown &= 0xFFFF ^ buttonMask
}
case frenyard.MouseEventDown:
if pan.ThisUIPanelDetails._buttonsDown == 0 {
/*
* FOCUS REASONING DESCRIPTION
* If focusing on a subelement of an unfocused panel
* the parent focuses the panel
* the panel gets & forwards focus message to old interior focus
* the panel gets the mouse event
* the panel creates & forwards unfocus message to old interior focus
* the panel creates & forwards focus message to new interior focus
* If changing the subelement of a focused panel
* the panel creates & forwards unfocus message to old interior focus
* the panel creates & forwards focus message to new interior focus
* If unfocusing a panel
* the panel gets & forwards unfocus message to interior focus
*/
if pan.ThisUIPanelDetails._focus != hittest {
// Note that this only happens when all other buttons have been released.
// This prevents having to create fake release events.
// The details of the order here are to do with issues when elements start modifying things in reaction to events.
// Hence, the element that is being focused gets to run first so it will always receive an unfocus event after it has been focused.
// While the element being unfocused is unlikely to get refocused under sane circumstances.
// If worst comes to worst, make this stop sending focus events so nobody has to worry about focus state atomicity.
oldFocus := pan.ThisUIPanelDetails._focus
pan.ThisUIPanelDetails._focus = hittest
newFocusFixed := PanelFixedElement{}
if pan.ThisUIPanelDetails._focus != -1 {
newFocusFixed = pan.ThisUIPanelDetails._content[pan.ThisUIPanelDetails._focus]
}
// Since a mouse event came in in the first place, we know the panel's focused.
// Focus the newly focused element.
if newFocusFixed.Element != nil {
newFocusFixed.Element.FyENormalEvent(FocusEvent{true})
}
// Unfocus the existing focused element, if any.
if oldFocus != -1 {
pan.ThisUIPanelDetails._content[oldFocus].Element.FyENormalEvent(FocusEvent{false})
}
}
}
if pan.ThisUIPanelDetails._buttonsDown&buttonMask != 0 {
fmt.Println("ui_core.go/Panel/FyEMouseEvent warning: Button added when it was already added")
invalid = true
} else {
pan.ThisUIPanelDetails._buttonsDown |= buttonMask
}
}
// Yes, focus gets to receive mouse-move events out of bounds even if there are no buttons.
// All the state is updated, forward the event
if !invalid && pan.ThisUIPanelDetails._focus != -1 {
pan._fyUIPanelForwardMouseEvent(pan.ThisUIPanelDetails._content[pan.ThisUIPanelDetails._focus], ev)
}
}
// FyEDraw implements UIElement.FyEDraw
func (pan *UIPanel) FyEDraw(target frenyard.Renderer, under bool) {
if pan.ThisUIPanelDetails.Clipping {
// Clipping: everything is inside panel bounds
if under {
return
}
oldClip := target.Clip()
newClip := oldClip.Intersect(frenyard.Area2iOfSize(pan.FyESize()))
if newClip.Empty() {
return
}
target.SetClip(newClip)
defer target.SetClip(oldClip)
for pass := 0; pass < 2; pass++ {
for _, val := range pan.ThisUIPanelDetails._content {
if !val.Visible {
continue
}
target.Translate(val.Pos)
val.Element.FyEDraw(target, pass == 0)
target.Translate(val.Pos.Negate())
}
}
} else {
// Not clipping; this simply arranges a bunch of elements
for _, val := range pan.ThisUIPanelDetails._content {
if !val.Visible {
continue
}
target.Translate(val.Pos)
val.Element.FyEDraw(target, under)
target.Translate(val.Pos.Negate())
}
}
}
// FyETick implements UIElement.FyETick
func (pan *UIPanel) FyETick(f float64) {
for _, val := range pan.ThisUIPanelDetails._content {
if !val.Visible {
continue
}
val.Element.FyETick(f)
}
}
// FyEFindElement implements UIElement.FyEFindElement
func (pan *UIPanel) FyEFindElement(target UIElement) frenyard.Area2i {
for _, val := range pan.ThisUIPanelDetails._content {
if val.Element == target {
return frenyard.Area2iFromVecs(val.Pos, val.Element.FyESize())
}
area := val.Element.FyEFindElement(target)
if !area.Empty() {
return area.Translate(val.Pos)
}
}
return frenyard.Area2i{}
}
// UIProxyHost is used to 'drill down' to the UIProxy within an element.
type UIProxyHost interface {
// Returns the *UIProxy within this element.
fyGetUIProxy() *UIProxy
}
// UIProxy is a "proxy" element. Useful to use another element as a base class without including it via inheritance.
type UIProxy struct {
// This element is semi-private: it may be read by UIProxy and UILayoutProxy but nothing else.
fyUIProxyTarget UIElement
}
func (px *UIProxy) fyGetUIProxy() *UIProxy {
return px
}
// InitUIProxy initializes a UIProxy, setting the target.
func InitUIProxy(proxy UIProxyHost, target UIElement) {
proxy.fyGetUIProxy().fyUIProxyTarget = target
}
// FyENormalEvent implements UIElement.FyENormalEvent
func (px *UIProxy) FyENormalEvent(ev frenyard.NormalEvent) {
px.fyUIProxyTarget.FyENormalEvent(ev)
}
// FyEMouseEvent implements UIElement.FyEMouseEvent
func (px *UIProxy) FyEMouseEvent(ev frenyard.MouseEvent) {
px.fyUIProxyTarget.FyEMouseEvent(ev)
}
// FyEDraw implements UIElement.FyEDraw
func (px *UIProxy) FyEDraw(target frenyard.Renderer, under bool) {
px.fyUIProxyTarget.FyEDraw(target, under)
}
// FyETick implements UIElement.FyETick
func (px *UIProxy) FyETick(f float64) {
px.fyUIProxyTarget.FyETick(f)
}
// FyEResize implements UIElement.FyEResize
func (px *UIProxy) FyEResize(v frenyard.Vec2i) {
px.fyUIProxyTarget.FyEResize(v)
}
// FyESize implements UIElement.FyESize
func (px *UIProxy) FyESize() frenyard.Vec2i {
return px.fyUIProxyTarget.FyESize()
}
// FyEFindElement implements UIElement.FyEFindElement
func (px *UIProxy) FyEFindElement(target UIElement) frenyard.Area2i {
if px.fyUIProxyTarget == target {
return frenyard.Area2iOfSize(px.FyESize())
}
return px.fyUIProxyTarget.FyEFindElement(target)
}