1 package etk
2
3 import (
4 "fmt"
5 "image"
6 "image/color"
7 "io"
8 "log"
9 "math"
10 "sync"
11 "time"
12
13 "code.rocket9labs.com/tslocum/etk/messeji"
14 "github.com/hajimehoshi/ebiten/v2"
15 "github.com/hajimehoshi/ebiten/v2/inpututil"
16 "golang.org/x/image/font"
17 "golang.org/x/image/font/opentype"
18 "golang.org/x/image/font/sfnt"
19 "golang.org/x/image/math/fixed"
20 )
21
22
23 type Alignment int
24
25 const (
26
27 AlignStart Alignment = 0
28
29
30 AlignCenter Alignment = 1
31
32
33 AlignEnd Alignment = 2
34 )
35
36
37
38 var DebounceResize = 250 * time.Millisecond
39
40 var root Widget
41
42 var drawDebug bool
43
44 var (
45 lastWidth, lastHeight int
46
47 lastX, lastY = -math.MaxInt, -math.MaxInt
48
49 touchIDs []ebiten.TouchID
50 activeTouchID = ebiten.TouchID(-1)
51
52 focusedWidget Widget
53
54 pressedWidget Widget
55
56 cursorShape ebiten.CursorShapeType
57
58 foundFocused bool
59
60 lastResize time.Time
61
62 lastBackspaceRepeat time.Time
63
64 keyBuffer []ebiten.Key
65 runeBuffer []rune
66
67 fontMutex = &sync.Mutex{}
68 )
69
70 const maxScroll = 3
71
72 var debugColor = color.RGBA{0, 0, 255, 255}
73
74 const (
75 backspaceRepeatWait = 500 * time.Millisecond
76 backspaceRepeatTime = 75 * time.Millisecond
77 )
78
79 var deviceScale float64
80
81
82
83 func ScaleFactor() float64 {
84 if deviceScale == 0 {
85 monitor := ebiten.Monitor()
86 if monitor != nil {
87 deviceScale = monitor.DeviceScaleFactor()
88 }
89 if deviceScale <= 0 {
90 deviceScale = ebiten.DeviceScaleFactor()
91 }
92
93 }
94 return deviceScale
95 }
96
97
98
99
100 func Scale(v int) int {
101 if deviceScale == 0 {
102 monitor := ebiten.Monitor()
103 if monitor != nil {
104 deviceScale = monitor.DeviceScaleFactor()
105 }
106 if deviceScale <= 0 {
107 deviceScale = ebiten.DeviceScaleFactor()
108 }
109
110 }
111 return int(float64(v) * deviceScale)
112 }
113
114 var (
115 fontCache = make(map[string]font.Face)
116 fontCacheLock sync.Mutex
117 )
118
119
120 func FontFace(fnt *sfnt.Font, size int) font.Face {
121 id := fmt.Sprintf("%p/%d", fnt, size)
122
123 fontCacheLock.Lock()
124 defer fontCacheLock.Unlock()
125
126 f := fontCache[id]
127 if f != nil {
128 return f
129 }
130
131 const dpi = 72
132 f, err := opentype.NewFace(fnt, &opentype.FaceOptions{
133 Size: float64(size),
134 DPI: dpi,
135 Hinting: font.HintingFull,
136 })
137 if err != nil {
138 log.Fatal(err)
139 }
140
141 fontCache[id] = f
142 return f
143 }
144
145
146
147
148 func SetRoot(w Widget) {
149 root = w
150 if root != nil && (lastWidth != 0 || lastHeight != 0) {
151 root.SetRect(image.Rect(0, 0, lastWidth, lastHeight))
152 }
153 SetFocus(root)
154 }
155
156
157 func SetFocus(w Widget) {
158 lastFocused := focusedWidget
159 if w != nil && !w.SetFocus(true) {
160 return
161 }
162 if lastFocused != nil && lastFocused != w {
163 lastFocused.SetFocus(false)
164 }
165 focusedWidget = w
166 }
167
168
169 func Focused() Widget {
170 return focusedWidget
171 }
172
173 func int26ToRect(r fixed.Rectangle26_6) image.Rectangle {
174 x, y := r.Min.X, r.Min.Y
175 w, h := r.Max.X-r.Min.X, r.Max.Y-r.Min.Y
176 return image.Rect(x.Round(), y.Round(), (x + w).Round(), (y + h).Round())
177 }
178
179
180 func BoundString(f font.Face, s string) image.Rectangle {
181 fontMutex.Lock()
182 defer fontMutex.Unlock()
183
184 bounds, _ := font.BoundString(f, s)
185 return int26ToRect(bounds)
186 }
187
188
189
190 func SetDebug(debug bool) {
191 drawDebug = debug
192 }
193
194
195 func ScreenSize() (width int, height int) {
196 return lastWidth, lastHeight
197 }
198
199
200 func Layout(outsideWidth int, outsideHeight int) {
201 if !lastResize.IsZero() && time.Since(lastResize) < DebounceResize && outsideWidth != 0 && outsideHeight != 0 {
202 return
203 }
204
205 outsideWidth, outsideHeight = Scale(outsideWidth), Scale(outsideHeight)
206 if outsideWidth != lastWidth || outsideHeight != lastHeight {
207 lastWidth, lastHeight = outsideWidth, outsideHeight
208 }
209
210 if root == nil {
211 return
212 }
213 root.SetRect(image.Rect(0, 0, outsideWidth, outsideHeight))
214 }
215
216
217 func Update() error {
218 if root == nil {
219 return nil
220 }
221
222 var cursor image.Point
223
224
225
226 var pressed bool
227 var clicked bool
228 var touchInput bool
229
230 if activeTouchID != -1 {
231 x, y := ebiten.TouchPosition(activeTouchID)
232 if x != 0 || y != 0 {
233 cursor = image.Point{x, y}
234
235 pressed = true
236 touchInput = true
237 } else {
238 activeTouchID = -1
239 }
240 }
241
242 if activeTouchID == -1 {
243 touchIDs = inpututil.AppendJustPressedTouchIDs(touchIDs[:0])
244 for _, id := range touchIDs {
245 x, y := ebiten.TouchPosition(id)
246 if x != 0 || y != 0 {
247 cursor = image.Point{x, y}
248
249 pressed = true
250 clicked = true
251 touchInput = true
252
253 activeTouchID = id
254 break
255 }
256 }
257 }
258
259
260
261 if !touchInput {
262 x, y := ebiten.CursorPosition()
263 cursor = image.Point{x, y}
264
265 if lastX == -math.MaxInt && lastY == -math.MaxInt {
266 lastX, lastY = x, y
267 }
268 for _, binding := range Bindings.ConfirmMouse {
269 pressed = ebiten.IsMouseButtonPressed(binding)
270 if pressed {
271 break
272 }
273 }
274
275 for _, binding := range Bindings.ConfirmMouse {
276 clicked = inpututil.IsMouseButtonJustPressed(binding)
277 if clicked {
278 break
279 }
280 }
281 }
282
283 if !pressed && !clicked && pressedWidget != nil {
284 _, err := pressedWidget.HandleMouse(cursor, false, false)
285 if err != nil {
286 return err
287 }
288 pressedWidget = nil
289 }
290
291 mouseHandled, err := update(root, cursor, pressed, clicked, false)
292 if err != nil {
293 return fmt.Errorf("failed to handle widget mouse input: %s", err)
294 } else if !mouseHandled && cursorShape != ebiten.CursorShapeDefault {
295 ebiten.SetCursorShape(ebiten.CursorShapeDefault)
296 cursorShape = ebiten.CursorShapeDefault
297 }
298
299
300
301 if focusedWidget == nil {
302 return nil
303 } else if ebiten.IsKeyPressed(ebiten.KeyBackspace) {
304 if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
305 lastBackspaceRepeat = time.Now().Add(backspaceRepeatWait)
306 } else if time.Since(lastBackspaceRepeat) >= backspaceRepeatTime {
307 lastBackspaceRepeat = time.Now()
308
309 _, err := focusedWidget.HandleKeyboard(ebiten.KeyBackspace, 0)
310 if err != nil {
311 return err
312 }
313 }
314 }
315
316
317 if inpututil.IsKeyJustPressed(ebiten.KeyV) && ebiten.IsKeyPressed(ebiten.KeyControl) {
318 focused := Focused()
319 if focused != nil {
320 writer, ok := focused.(io.Writer)
321 if ok {
322 _, err := writer.Write(clipboardBuffer())
323 return err
324 }
325 }
326 }
327
328 keyBuffer = inpututil.AppendJustPressedKeys(keyBuffer[:0])
329 for _, key := range keyBuffer {
330 _, err := focusedWidget.HandleKeyboard(key, 0)
331 if err != nil {
332 return fmt.Errorf("failed to handle widget keyboard input: %s", err)
333 }
334 }
335
336 runeBuffer = ebiten.AppendInputChars(runeBuffer[:0])
337 INPUTCHARS:
338 for i, r := range runeBuffer {
339 if i > 0 {
340 for j, r2 := range runeBuffer {
341 if j == i {
342 break
343 } else if r2 == r {
344 continue INPUTCHARS
345 }
346 }
347 }
348 var err error
349 switch r {
350 case Bindings.ConfirmRune:
351 _, err = focusedWidget.HandleKeyboard(ebiten.KeyEnter, 0)
352 case Bindings.BackRune:
353 _, err = focusedWidget.HandleKeyboard(ebiten.KeyBackspace, 0)
354 default:
355 _, err = focusedWidget.HandleKeyboard(-1, r)
356 }
357 if err != nil {
358 return fmt.Errorf("failed to handle widget keyboard input: %s", err)
359 }
360 }
361 return nil
362 }
363
364 func at(w Widget, p image.Point) Widget {
365 if w == nil || !w.Visible() {
366 return nil
367 }
368
369 for _, child := range w.Children() {
370 result := at(child, p)
371 if result != nil {
372 return result
373 }
374 }
375
376 if p.In(w.Rect()) {
377 return w
378 }
379
380 return nil
381 }
382
383
384 func At(p image.Point) Widget {
385 return at(root, p)
386 }
387
388 func update(w Widget, cursor image.Point, pressed bool, clicked bool, mouseHandled bool) (bool, error) {
389 if w == nil {
390 return false, nil
391 } else if !w.Visible() {
392 return mouseHandled, nil
393 }
394
395 var err error
396 children := w.Children()
397 for i := len(children) - 1; i >= 0; i-- {
398 mouseHandled, err = update(children[i], cursor, pressed, clicked, mouseHandled)
399 if err != nil {
400 return false, err
401 } else if mouseHandled {
402 return true, nil
403 }
404 }
405 if !mouseHandled && cursor.In(w.Rect()) {
406 if pressed && !clicked && w != pressedWidget {
407 return mouseHandled, nil
408 }
409 mouseHandled, err = w.HandleMouse(cursor, pressed, clicked)
410 if err != nil {
411 return false, fmt.Errorf("failed to handle widget mouse input: %s", err)
412 } else if mouseHandled {
413 if clicked {
414 SetFocus(w)
415 pressedWidget = w
416 } else if pressedWidget != nil && (!pressed || pressedWidget != w) {
417 pressedWidget = nil
418 }
419 shape := w.Cursor()
420 if shape == -1 {
421 shape = ebiten.CursorShapeDefault
422 }
423 if shape != cursorShape {
424 ebiten.SetCursorShape(shape)
425 cursorShape = shape
426 }
427 }
428 }
429 return mouseHandled, nil
430 }
431
432
433 func Draw(screen *ebiten.Image) error {
434 foundFocused = false
435 err := draw(root, screen)
436 if err != nil {
437 return err
438 } else if focusedWidget != nil && !foundFocused {
439 SetFocus(nil)
440 }
441 return nil
442 }
443
444 func draw(w Widget, screen *ebiten.Image) error {
445 if w == nil || !w.Visible() {
446 return nil
447 }
448
449 r := w.Rect()
450 subScreen := screen.SubImage(r).(*ebiten.Image)
451
452 background := w.Background()
453 if background.A > 0 {
454 subScreen.Fill(background)
455 }
456
457 err := w.Draw(subScreen)
458 if err != nil {
459 return fmt.Errorf("failed to draw widget: %s", err)
460 }
461
462 if drawDebug && !r.Empty() {
463 x, y := r.Min.X, r.Min.Y
464 w, h := r.Dx(), r.Dy()
465 screen.SubImage(image.Rect(x, y, x+w, y+1)).(*ebiten.Image).Fill(debugColor)
466 screen.SubImage(image.Rect(x, y+h-1, x+w, y+h)).(*ebiten.Image).Fill(debugColor)
467 screen.SubImage(image.Rect(x, y, x+1, y+h)).(*ebiten.Image).Fill(debugColor)
468 screen.SubImage(image.Rect(x+w-1, y, x+w, y+h)).(*ebiten.Image).Fill(debugColor)
469 }
470
471 children := w.Children()
472 for _, child := range children {
473 err = draw(child, subScreen)
474 if err != nil {
475 return fmt.Errorf("failed to draw widget: %s", err)
476 }
477 }
478
479 if w == focusedWidget {
480 foundFocused = true
481 }
482 return nil
483 }
484
485 func newText() *messeji.TextField {
486 f := messeji.NewTextField(FontFace(Style.TextFont, Scale(Style.TextSize)), fontMutex)
487 f.SetForegroundColor(Style.TextColorLight)
488 f.SetBackgroundColor(transparent)
489 f.SetScrollBarColors(Style.ScrollAreaColor, Style.ScrollHandleColor)
490 f.SetScrollBorderSize(Scale(Style.ScrollBorderSize))
491 f.SetScrollBorderColors(Style.ScrollBorderTop, Style.ScrollBorderRight, Style.ScrollBorderBottom, Style.ScrollBorderLeft)
492 return f
493 }
494
495 func rectAtOrigin(r image.Rectangle) image.Rectangle {
496 r.Max.X, r.Max.Y = r.Dx(), r.Dy()
497 r.Min.X, r.Min.Y = 0, 0
498 return r
499 }
500
501 func _clamp(x int, y int, boundary image.Rectangle) (int, int) {
502 if x < boundary.Min.X {
503 x = boundary.Min.X
504 } else if y > boundary.Max.X {
505 x = boundary.Max.X
506 }
507 if y < boundary.Min.Y {
508 y = boundary.Min.Y
509 } else if y > boundary.Max.Y {
510 y = boundary.Max.Y
511 }
512 return x, y
513 }
514
515 func clampRect(r image.Rectangle, boundary image.Rectangle) image.Rectangle {
516 r.Min.X, r.Min.Y = _clamp(r.Min.X, r.Min.Y, boundary)
517 r.Max.X, r.Max.Y = _clamp(r.Max.X, r.Max.Y, boundary)
518 if r.Min.X == r.Max.X || r.Min.Y == r.Max.Y {
519 return image.Rectangle{}
520 }
521 return r
522 }
523
View as plain text