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