1 package etk
2
3 import (
4 "fmt"
5 "image"
6 "image/color"
7 "log"
8 "math"
9 "sync"
10 "time"
11
12 "code.rocket9labs.com/tslocum/etk/messeji"
13 "github.com/hajimehoshi/ebiten/v2"
14 "github.com/hajimehoshi/ebiten/v2/inpututil"
15 "golang.org/x/image/font"
16 "golang.org/x/image/font/opentype"
17 "golang.org/x/image/font/sfnt"
18 "golang.org/x/image/math/fixed"
19 )
20
21
22 type Alignment int
23
24 const (
25
26 AlignStart Alignment = 0
27
28
29 AlignCenter Alignment = 1
30
31
32 AlignEnd Alignment = 2
33 )
34
35 var root Widget
36
37 var drawDebug bool
38
39 var (
40 lastWidth, lastHeight int
41
42 lastX, lastY = -math.MaxInt, -math.MaxInt
43
44 touchIDs []ebiten.TouchID
45 activeTouchID = ebiten.TouchID(-1)
46
47 focusedWidget Widget
48
49 pressedWidget Widget
50
51 cursorShape ebiten.CursorShapeType
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 monitor := ebiten.Monitor()
75 if monitor != nil {
76 deviceScale = monitor.DeviceScaleFactor()
77 }
78 if deviceScale <= 0 {
79 deviceScale = ebiten.DeviceScaleFactor()
80 }
81
82 }
83 return deviceScale
84 }
85
86
87
88
89 func Scale(v int) int {
90 if deviceScale == 0 {
91 monitor := ebiten.Monitor()
92 if monitor != nil {
93 deviceScale = monitor.DeviceScaleFactor()
94 }
95 if deviceScale <= 0 {
96 deviceScale = ebiten.DeviceScaleFactor()
97 }
98
99 }
100 return int(float64(v) * deviceScale)
101 }
102
103 var (
104 fontCache = make(map[string]font.Face)
105 fontCacheLock sync.Mutex
106 )
107
108
109 func FontFace(fnt *sfnt.Font, size int) font.Face {
110 id := fmt.Sprintf("%p/%d", fnt, size)
111
112 fontCacheLock.Lock()
113 defer fontCacheLock.Unlock()
114
115 f := fontCache[id]
116 if f != nil {
117 return f
118 }
119
120 const dpi = 72
121 f, err := opentype.NewFace(fnt, &opentype.FaceOptions{
122 Size: float64(size),
123 DPI: dpi,
124 Hinting: font.HintingFull,
125 })
126 if err != nil {
127 log.Fatal(err)
128 }
129
130 fontCache[id] = f
131 return f
132 }
133
134
135
136
137 func SetRoot(w Widget) {
138 root = w
139 if root != nil && (lastWidth != 0 || lastHeight != 0) {
140 root.SetRect(image.Rect(0, 0, lastWidth, lastHeight))
141 }
142 SetFocus(root)
143 }
144
145
146 func SetFocus(w Widget) {
147 lastFocused := focusedWidget
148 if w != nil && !w.SetFocus(true) {
149 return
150 }
151 if lastFocused != nil && lastFocused != w {
152 lastFocused.SetFocus(false)
153 }
154 focusedWidget = w
155 }
156
157
158 func Focused() Widget {
159 return focusedWidget
160 }
161
162 func int26ToRect(r fixed.Rectangle26_6) image.Rectangle {
163 x, y := r.Min.X, r.Min.Y
164 w, h := r.Max.X-r.Min.X, r.Max.Y-r.Min.Y
165 return image.Rect(x.Round(), y.Round(), (x + w).Round(), (y + h).Round())
166 }
167
168
169 func BoundString(f font.Face, s string) image.Rectangle {
170 fontMutex.Lock()
171 defer fontMutex.Unlock()
172
173 bounds, _ := font.BoundString(f, s)
174 return int26ToRect(bounds)
175 }
176
177
178
179 func SetDebug(debug bool) {
180 drawDebug = debug
181 }
182
183
184 func ScreenSize() (width int, height int) {
185 return lastWidth, lastHeight
186 }
187
188
189 func Layout(outsideWidth int, outsideHeight int) {
190 outsideWidth, outsideHeight = Scale(outsideWidth), Scale(outsideHeight)
191 if outsideWidth != lastWidth || outsideHeight != lastHeight {
192 lastWidth, lastHeight = outsideWidth, outsideHeight
193 }
194
195 if root == nil {
196 return
197 }
198 root.SetRect(image.Rect(0, 0, outsideWidth, outsideHeight))
199 }
200
201
202 func Update() error {
203 if root == nil {
204 return nil
205 }
206
207 var cursor image.Point
208
209
210
211 var pressed bool
212 var clicked bool
213 var touchInput bool
214
215 if activeTouchID != -1 {
216 x, y := ebiten.TouchPosition(activeTouchID)
217 if x != 0 || y != 0 {
218 cursor = image.Point{x, y}
219
220 pressed = true
221 touchInput = true
222 } else {
223 activeTouchID = -1
224 }
225 }
226
227 if activeTouchID == -1 {
228 touchIDs = inpututil.AppendJustPressedTouchIDs(touchIDs[:0])
229 for _, id := range touchIDs {
230 x, y := ebiten.TouchPosition(id)
231 if x != 0 || y != 0 {
232 cursor = image.Point{x, y}
233
234 pressed = true
235 clicked = true
236 touchInput = true
237
238 activeTouchID = id
239 break
240 }
241 }
242 }
243
244
245
246 if !touchInput {
247 x, y := ebiten.CursorPosition()
248 cursor = image.Point{x, y}
249
250 if lastX == -math.MaxInt && lastY == -math.MaxInt {
251 lastX, lastY = x, y
252 }
253 for _, binding := range Bindings.ConfirmMouse {
254 pressed = ebiten.IsMouseButtonPressed(binding)
255 if pressed {
256 break
257 }
258 }
259
260 for _, binding := range Bindings.ConfirmMouse {
261 clicked = inpututil.IsMouseButtonJustPressed(binding)
262 if clicked {
263 break
264 }
265 }
266 }
267
268 if !pressed && !clicked && pressedWidget != nil {
269 _, err := pressedWidget.HandleMouse(cursor, false, false)
270 if err != nil {
271 return err
272 }
273 pressedWidget = nil
274 }
275
276 mouseHandled, err := update(root, cursor, pressed, clicked, false)
277 if err != nil {
278 return fmt.Errorf("failed to handle widget mouse input: %s", err)
279 } else if !mouseHandled && cursorShape != ebiten.CursorShapeDefault {
280 ebiten.SetCursorShape(ebiten.CursorShapeDefault)
281 cursorShape = ebiten.CursorShapeDefault
282 }
283
284
285
286 if focusedWidget == nil {
287 return nil
288 } else if ebiten.IsKeyPressed(ebiten.KeyBackspace) {
289 if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
290 lastBackspaceRepeat = time.Now().Add(backspaceRepeatWait)
291 } else if time.Since(lastBackspaceRepeat) >= backspaceRepeatTime {
292 lastBackspaceRepeat = time.Now()
293
294 _, err := focusedWidget.HandleKeyboard(ebiten.KeyBackspace, 0)
295 if err != nil {
296 return err
297 }
298 }
299 }
300
301 keyBuffer = inpututil.AppendJustPressedKeys(keyBuffer[:0])
302 for _, key := range keyBuffer {
303 _, err := focusedWidget.HandleKeyboard(key, 0)
304 if err != nil {
305 return fmt.Errorf("failed to handle widget keyboard input: %s", err)
306 }
307 }
308
309 runeBuffer = ebiten.AppendInputChars(runeBuffer[:0])
310 INPUTCHARS:
311 for i, r := range runeBuffer {
312 if i > 0 {
313 for j, r2 := range runeBuffer {
314 if j == i {
315 break
316 } else if r2 == r {
317 continue INPUTCHARS
318 }
319 }
320 }
321 var err error
322 switch r {
323 case Bindings.ConfirmRune:
324 _, err = focusedWidget.HandleKeyboard(ebiten.KeyEnter, 0)
325 case Bindings.BackRune:
326 _, err = focusedWidget.HandleKeyboard(ebiten.KeyBackspace, 0)
327 default:
328 _, err = focusedWidget.HandleKeyboard(-1, r)
329 }
330 if err != nil {
331 return fmt.Errorf("failed to handle widget keyboard input: %s", err)
332 }
333 }
334 return nil
335 }
336
337 func at(w Widget, p image.Point) Widget {
338 if w == nil || !w.Visible() {
339 return nil
340 }
341
342 for _, child := range w.Children() {
343 result := at(child, p)
344 if result != nil {
345 return result
346 }
347 }
348
349 if p.In(w.Rect()) {
350 return w
351 }
352
353 return nil
354 }
355
356
357 func At(p image.Point) Widget {
358 return at(root, p)
359 }
360
361 func update(w Widget, cursor image.Point, pressed bool, clicked bool, mouseHandled bool) (bool, error) {
362 if w == nil {
363 return false, nil
364 }
365
366 if !w.Visible() {
367 return mouseHandled, nil
368 }
369
370 var err error
371 children := w.Children()
372 for i := len(children) - 1; i >= 0; i-- {
373 mouseHandled, err = update(children[i], cursor, pressed, clicked, mouseHandled)
374 if err != nil {
375 return false, err
376 } else if mouseHandled {
377 return true, nil
378 }
379 }
380 if !mouseHandled && cursor.In(w.Rect()) {
381 if pressed && !clicked && w != pressedWidget {
382 return mouseHandled, nil
383 }
384 mouseHandled, err = w.HandleMouse(cursor, pressed, clicked)
385 if err != nil {
386 return false, fmt.Errorf("failed to handle widget mouse input: %s", err)
387 } else if mouseHandled {
388 if clicked {
389 SetFocus(w)
390 pressedWidget = w
391 } else if pressedWidget != nil && (!pressed || pressedWidget != w) {
392 pressedWidget = nil
393 }
394 shape := w.Cursor()
395 if shape != -1 && shape != cursorShape {
396 ebiten.SetCursorShape(shape)
397 cursorShape = shape
398 }
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