-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathwidget.go
531 lines (461 loc) · 13.5 KB
/
widget.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
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
package ui
import (
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui/theme"
)
// BorderStyle options for widget.SetBorderStyle()
type BorderStyle string
// Styles for a widget border.
const (
BorderNone BorderStyle = ""
BorderSolid BorderStyle = "solid"
BorderRaised = "raised"
BorderSunken = "sunken"
)
// Widget is a user interface element.
type Widget interface {
ID() string // Get the widget's string ID.
IDFunc(func() string) // Set a function that returns the widget's ID.
String() string
Point() render.Point
MoveTo(render.Point)
MoveBy(render.Point)
Size() render.Rect // Return the Width and Height of the widget.
FixedSize() bool // Return whether the size is fixed (true) or automatic (false)
BoxSize() render.Rect // Return the full size including the border and outline.
Resize(render.Rect)
ResizeBy(render.Rect)
ResizeAuto(render.Rect)
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
Handle(Event, func(EventData) error)
Event(Event, EventData) error // called internally to trigger an event
// Thickness of the padding + border + outline.
BoxThickness(multiplier int) int
DrawBox(render.Engine, render.Point)
// Widget configuration getters.
Margin() int // Margin away from other widgets
SetMargin(int) //
Background() render.Color // Background color
SetBackground(render.Color) //
Foreground() render.Color // Foreground color
SetForeground(render.Color) //
BorderStyle() BorderStyle // Border style: none, raised, sunken
SetBorderStyle(BorderStyle) //
BorderColor() render.Color // Border color (default is Background)
SetBorderColor(render.Color) //
BorderSize() int // Border size (default 0)
SetBorderSize(int) //
OutlineColor() render.Color // Outline color (default Invisible)
SetOutlineColor(render.Color) //
OutlineSize() int // Outline size (default 0)
SetOutlineSize(int) //
// Visibility
Hide()
Show()
Hidden() bool
// Container widgets like Frames can wire up associations between the
// child widgets and the parent.
Parent() (parent Widget, ok bool)
SetParent(parent Widget) // for the container to assign itself the parent
Children() []Widget // for containers to return their children
// Run any render computations; by the end the widget must know its
// Width and Height. For example the Label widget will render itself onto
// an SDL Surface and then it will know its bounding box, but not before.
Compute(render.Engine)
// Render the final widget onto the drawing engine.
Present(render.Engine, render.Point)
// Destroy: implement this if you have resources to free up on teardown.
Destroy()
}
// Config holds common base widget configs for quick configuration.
type Config struct {
// Size management. If you provide a non-zero value for Width and Height,
// the widget will be resized and the "fixedSize" flag is set, meaning it
// will not re-compute its size dynamically. To set the size while also
// keeping the auto-resize property, pass AutoResize=true too. This is
// mainly used internally when widgets are calculating their automatic sizes.
AutoResize bool
Width int
Height int
Margin int
MarginX int
MarginY int
Background render.Color
Foreground render.Color
BorderSize int
BorderStyle BorderStyle
BorderColor render.Color
OutlineSize int
OutlineColor render.Color
}
// BaseWidget holds common functionality for all widgets, such as managing
// their widths and heights.
type BaseWidget struct {
id string
idFunc func() string
fixedSize bool
hidden bool
width int
height int
point render.Point
margin int
background render.Color
foreground render.Color
borderStyle BorderStyle
borderColor render.Color
borderSize int
outlineColor render.Color
outlineSize int
handlers map[Event][]func(EventData) error
hasParent bool
parent Widget
}
// SetID sets a string name for your widget, helpful for debugging purposes.
func (w *BaseWidget) SetID(id string) {
w.id = id
}
// ID returns the ID that the widget calls itself by.
func (w *BaseWidget) ID() string {
if w.idFunc == nil {
w.IDFunc(func() string {
return "Widget<Untitled>"
})
}
return w.idFunc()
}
// IDFunc sets an ID function.
func (w *BaseWidget) IDFunc(fn func() string) {
w.idFunc = fn
}
func (w *BaseWidget) String() string {
return w.ID()
}
// Configure the base widget with all the common properties at once. Any
// property left as the zero value will not update the widget.
func (w *BaseWidget) Configure(c Config) {
if c.Width != 0 || c.Height != 0 {
w.fixedSize = !c.AutoResize
if c.Width != 0 {
w.width = c.Width
}
if c.Height != 0 {
w.height = c.Height
}
}
if c.Margin != 0 {
w.margin = c.Margin
}
if c.Background != render.Invisible {
w.background = c.Background
}
if c.Foreground != render.Invisible {
w.foreground = c.Foreground
}
if c.BorderColor != render.Invisible {
w.borderColor = c.BorderColor
}
if c.OutlineColor != render.Invisible {
w.outlineColor = c.OutlineColor
}
if c.BorderSize != 0 {
w.borderSize = c.BorderSize
}
if c.BorderStyle != BorderNone {
w.borderStyle = c.BorderStyle
}
if c.OutlineSize != 0 {
w.outlineSize = c.OutlineSize
}
}
// Rect returns the widget's absolute rectangle, the combined Size and Point.
func (w *BaseWidget) Rect() render.Rect {
return render.Rect{
X: w.point.X,
Y: w.point.Y,
W: w.width,
H: w.height,
}
}
// Point returns the X,Y position of the widget on the window.
func (w *BaseWidget) Point() render.Point {
return w.point
}
// MoveTo updates the X,Y position to the new point.
func (w *BaseWidget) MoveTo(v render.Point) {
w.point = v
}
// MoveBy adds the X,Y values to the widget's current position.
func (w *BaseWidget) MoveBy(v render.Point) {
w.point.X += v.X
w.point.Y += v.Y
}
// Size returns the box with W and H attributes containing the size of the
// widget. The X,Y attributes of the box are ignored and zero.
func (w *BaseWidget) Size() render.Rect {
return render.Rect{
W: w.width,
H: w.height,
}
}
// BoxSize returns the full rendered size of the widget including its box
// thickness (border, padding and outline).
func (w *BaseWidget) BoxSize() render.Rect {
return render.Rect{
W: w.width + w.BoxThickness(2),
H: w.height + w.BoxThickness(2),
}
}
// FixedSize returns whether the widget's size has been hard-coded by the user
// (true) or if it automatically resizes based on its contents (false).
func (w *BaseWidget) FixedSize() bool {
return w.fixedSize
}
// Resize sets the size of the widget to the .W and .H attributes of a rect.
func (w *BaseWidget) Resize(v render.Rect) {
w.fixedSize = true
w.width = v.W
w.height = v.H
}
// ResizeBy resizes by a relative amount.
func (w *BaseWidget) ResizeBy(v render.Rect) {
w.fixedSize = true
w.width += v.W
w.height += v.H
}
// ResizeAuto sets the size of the widget but doesn't set the fixedSize flag.
func (w *BaseWidget) ResizeAuto(v render.Rect) {
w.width = v.W
w.height = v.H
}
// BoxThickness returns the full sum of the padding, border and outline.
// m = multiplier, i.e., 1 or 2. If m=1 this returns the box thickness of one
// edge of the widget, if m=2 it would account for both edges of the widget.
func (w *BaseWidget) BoxThickness(m int) int {
if m == 0 {
m = 1
}
return (w.Margin() * m) + (w.BorderSize() * m) + (w.OutlineSize() * m)
}
// Parent returns the parent widget, like a Frame, and a boolean indicating
// whether the widget had a parent.
func (w *BaseWidget) Parent() (Widget, bool) {
return w.parent, w.hasParent
}
// SetParent sets the widget's parent. This function is called by container
// widgets like Frame when they add a child widget to their care.
// Pass a nil parent to unset the parent.
func (w *BaseWidget) SetParent(parent Widget) {
if parent == nil {
w.hasParent = false
w.parent = nil
} else {
w.hasParent = true
w.parent = parent
}
}
// Children returns the widget's children, to be implemented by containers.
// The default implementation returns an empty slice.
func (w *BaseWidget) Children() []Widget {
return []Widget{}
}
// Hide the widget from being rendered.
func (w *BaseWidget) Hide() {
w.hidden = true
}
// Show the widget.
func (w *BaseWidget) Show() {
w.hidden = false
}
// Hidden returns whether the widget is hidden. If this widget is not hidden,
// but it has a parent, this will recursively crawl the parents to see if any
// of them are hidden.
func (w *BaseWidget) Hidden() bool {
if w.hidden {
return true
}
// Return if any parents are hidden.
parent, ok := w.Parent()
for ok {
if parent.Hidden() {
return true
}
parent, ok = parent.Parent()
}
return false
}
// DrawBox draws the border and outline.
func (w *BaseWidget) DrawBox(e render.Engine, P render.Point) {
var (
S = w.Size()
outline = w.OutlineSize()
border = w.BorderSize()
borderColor = w.BorderColor()
highlight = borderColor.Lighten(theme.BorderColorOffset)
shadow = borderColor.Darken(theme.BorderColorOffset)
color render.Color
box = render.Rect{
X: P.X,
Y: P.Y,
W: S.W,
H: S.H,
}
)
if borderColor == render.Invisible {
borderColor = render.Red
}
// Draw the outline layer as the full size of the widget.
if outline > 0 && w.OutlineColor() != render.Invisible {
e.DrawBox(w.OutlineColor(), render.Rect{
X: P.X,
Y: P.Y,
W: S.W,
H: S.H,
})
}
box.X += outline
box.Y += outline
box.W -= outline * 2
box.H -= outline * 2
// Highlight on the top left edge.
if border > 0 {
if w.BorderStyle() == BorderRaised {
color = highlight
} else if w.BorderStyle() == BorderSunken {
color = shadow
} else {
color = borderColor
}
e.DrawBox(color, box)
}
// Shadow on the bottom right edge.
box.X += border
box.Y += border
box.W -= border
box.H -= border
if w.BorderSize() > 0 {
if w.BorderStyle() == BorderRaised {
color = shadow
} else if w.BorderStyle() == BorderSunken {
color = highlight
} else {
color = borderColor
}
e.DrawBox(color, box)
}
// Background color of the button.
box.W -= border
box.H -= border
if w.Background() != render.Invisible {
e.DrawBox(w.Background(), box)
}
}
// Margin returns the margin width.
func (w *BaseWidget) Margin() int {
return w.margin
}
// SetMargin sets the margin width.
func (w *BaseWidget) SetMargin(v int) {
w.margin = v
}
// Background returns the background color.
func (w *BaseWidget) Background() render.Color {
return w.background
}
// SetBackground sets the color.
func (w *BaseWidget) SetBackground(c render.Color) {
w.background = c
}
// Foreground returns the foreground color.
func (w *BaseWidget) Foreground() render.Color {
return w.foreground
}
// SetForeground sets the color.
func (w *BaseWidget) SetForeground(c render.Color) {
w.foreground = c
}
// BorderStyle returns the border style.
func (w *BaseWidget) BorderStyle() BorderStyle {
return w.borderStyle
}
// SetBorderStyle sets the border style.
func (w *BaseWidget) SetBorderStyle(v BorderStyle) {
w.borderStyle = v
}
// BorderColor returns the border color, or defaults to the background color.
func (w *BaseWidget) BorderColor() render.Color {
if w.borderColor == render.Invisible {
return w.Background()
}
return w.borderColor
}
// SetBorderColor sets the border color.
func (w *BaseWidget) SetBorderColor(c render.Color) {
w.borderColor = c
}
// BorderSize returns the border thickness.
func (w *BaseWidget) BorderSize() int {
return w.borderSize
}
// SetBorderSize sets the border thickness.
func (w *BaseWidget) SetBorderSize(v int) {
w.borderSize = v
}
// OutlineColor returns the background color.
func (w *BaseWidget) OutlineColor() render.Color {
return w.outlineColor
}
// SetOutlineColor sets the color.
func (w *BaseWidget) SetOutlineColor(c render.Color) {
w.outlineColor = c
}
// OutlineSize returns the outline thickness.
func (w *BaseWidget) OutlineSize() int {
return w.outlineSize
}
// SetOutlineSize sets the outline thickness.
func (w *BaseWidget) SetOutlineSize(v int) {
w.outlineSize = v
}
// Compute calls the base widget's Compute function, which just triggers
// events on widgets that want to be notified when the widget computes.
func (w *BaseWidget) Compute(e render.Engine) {
w.Event(Compute, EventData{
Engine: e,
})
}
// Present calls the base widget's Present function, which just triggers
// events on widgets that want to be notified when the widget presents.
func (w *BaseWidget) Present(e render.Engine, p render.Point) {
w.Event(Present, EventData{
Point: p,
Engine: e,
})
}
// Event is called internally by Doodle to trigger an event.
// Handlers can return ErrStopPropagation to prevent further widgets being
// notified of events.
func (w *BaseWidget) Event(event Event, e EventData) error {
if handlers, ok := w.handlers[event]; ok {
for _, fn := range handlers {
res := fn(e)
if res == ErrStopPropagation {
return res
}
}
}
return ErrNoEventHandler
}
// Handle an event in the widget.
func (w *BaseWidget) Handle(event Event, fn func(EventData) error) {
if w.handlers == nil {
w.handlers = map[Event][]func(EventData) error{}
}
if _, ok := w.handlers[event]; !ok {
w.handlers[event] = []func(EventData) error{}
}
w.handlers[event] = append(w.handlers[event], fn)
}
// OnMouseOut should be overridden on widgets who want this event.
func (w *BaseWidget) OnMouseOut(render.Point) {}
// Destroy does nothing on the base widget. Implement it for widgets which need it.
func (w *BaseWidget) Destroy() {}