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