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