1 package messeji
2
3 import (
4 "image"
5 "image/color"
6 "strings"
7 "sync"
8 "unicode"
9
10 "github.com/hajimehoshi/ebiten/v2"
11 "github.com/hajimehoshi/ebiten/v2/inpututil"
12 "github.com/hajimehoshi/ebiten/v2/text"
13 "golang.org/x/image/font"
14 )
15
16
17 type Alignment int
18
19 const (
20
21 AlignStart Alignment = 0
22
23
24 AlignCenter Alignment = 1
25
26
27 AlignEnd Alignment = 2
28 )
29
30 const (
31 initialPadding = 5
32 initialScrollWidth = 32
33 )
34
35 var (
36 initialForeground = color.RGBA{0, 0, 0, 255}
37 initialBackground = color.RGBA{255, 255, 255, 255}
38 )
39
40
41
42
43
44
45 type TextField struct {
46
47 r image.Rectangle
48
49
50 buffer string
51
52
53 prefix string
54
55
56 suffix string
57
58
59 wordWrap bool
60
61
62 bufferWrapped []string
63
64
65
66 bufferSize int
67
68
69 lineWidths []int
70
71
72 singleLine bool
73
74
75 horizontal Alignment
76
77
78 vertical Alignment
79
80
81 face font.Face
82
83
84 lineHeight int
85
86
87 overrideLineHeight int
88
89
90 lineOffset int
91
92
93 textColor color.Color
94
95
96 backgroundColor color.Color
97
98
99 padding int
100
101
102
103 follow bool
104
105
106 overflow bool
107
108
109 offset int
110
111
112 handleKeyboard bool
113
114
115
116 modified bool
117
118
119 scrollRect image.Rectangle
120
121
122 scrollWidth int
123
124
125 scrollVisible bool
126
127
128
129 scrollAutoHide bool
130
131
132 scrollDrag bool
133
134
135 img *ebiten.Image
136
137
138 visible bool
139
140 sync.Mutex
141 }
142
143
144 func NewTextField(face font.Face) *TextField {
145 f := &TextField{
146 face: face,
147 textColor: initialForeground,
148 backgroundColor: initialBackground,
149 padding: initialPadding,
150 scrollWidth: initialScrollWidth,
151 follow: true,
152 wordWrap: true,
153 scrollVisible: true,
154 scrollAutoHide: true,
155 visible: true,
156 }
157 f.fontUpdated()
158 return f
159 }
160
161
162 func (f *TextField) Rect() image.Rectangle {
163 f.Lock()
164 defer f.Unlock()
165
166 return f.r
167 }
168
169
170 func (f *TextField) SetRect(r image.Rectangle) {
171 f.Lock()
172 defer f.Unlock()
173
174 f.r = r
175 f.drawImage()
176 }
177
178
179 func (f *TextField) Text() string {
180 f.Lock()
181 defer f.Unlock()
182
183 return f.buffer
184 }
185
186
187 func (f *TextField) SetText(text string) {
188 f.Lock()
189 defer f.Unlock()
190
191 f.buffer = text
192 f.modified = true
193 }
194
195
196 func (f *TextField) SetPrefix(text string) {
197 f.Lock()
198 defer f.Unlock()
199
200 f.prefix = text
201 f.drawImage()
202 }
203
204
205 func (f *TextField) SetSuffix(text string) {
206 f.Lock()
207 defer f.Unlock()
208
209 f.suffix = text
210 f.drawImage()
211 }
212
213
214
215 func (f *TextField) SetFollow(follow bool) {
216 f.Lock()
217 defer f.Unlock()
218
219 f.follow = follow
220 }
221
222
223
224 func (f *TextField) SetSingleLine(single bool) {
225 f.Lock()
226 defer f.Unlock()
227
228 if f.singleLine == single {
229 return
230 }
231
232 f.singleLine = single
233 f.bufferModified()
234 }
235
236
237 func (f *TextField) SetHorizontal(h Alignment) {
238 f.Lock()
239 defer f.Unlock()
240
241 if f.horizontal == h {
242 return
243 }
244
245 f.horizontal = h
246 f.bufferModified()
247 }
248
249
250 func (f *TextField) SetVertical(v Alignment) {
251 f.Lock()
252 defer f.Unlock()
253
254 if f.vertical == v {
255 return
256 }
257
258 f.vertical = v
259 f.bufferModified()
260 }
261
262
263 func (f *TextField) LineHeight() int {
264 f.Lock()
265 defer f.Unlock()
266
267 if f.overrideLineHeight != 0 {
268 return f.overrideLineHeight
269 }
270 return f.lineHeight
271 }
272
273
274
275 func (f *TextField) SetLineHeight(height int) {
276 f.Lock()
277 defer f.Unlock()
278
279 f.overrideLineHeight = height
280 }
281
282
283 func (f *TextField) SetForegroundColor(c color.Color) {
284 f.Lock()
285 defer f.Unlock()
286
287 f.textColor = c
288 }
289
290
291 func (f *TextField) SetBackgroundColor(c color.Color) {
292 f.Lock()
293 defer f.Unlock()
294
295 f.backgroundColor = c
296 }
297
298
299 func (f *TextField) SetFont(face font.Face) {
300 f.Lock()
301 defer f.Unlock()
302
303 f.face = face
304 f.fontUpdated()
305 }
306
307
308 func (f *TextField) Padding() int {
309 f.Lock()
310 defer f.Unlock()
311
312 return f.padding
313 }
314
315
316 func (f *TextField) SetPadding(padding int) {
317 f.Lock()
318 defer f.Unlock()
319
320 f.padding = padding
321 }
322
323
324 func (f *TextField) Visible() bool {
325 return f.visible
326 }
327
328
329 func (f *TextField) SetVisible(visible bool) {
330 f.Lock()
331 defer f.Unlock()
332
333 if f.visible == visible {
334 return
335 }
336
337 f.visible = visible
338 if visible {
339 f.drawImage()
340 }
341 }
342
343
344 func (f *TextField) SetScrollBarWidth(width int) {
345 f.Lock()
346 defer f.Unlock()
347
348 if f.scrollWidth == width {
349 return
350 }
351
352 f.scrollWidth = width
353 f.drawImage()
354 }
355
356
357 func (f *TextField) SetScrollBarVisible(scrollVisible bool) {
358 f.Lock()
359 defer f.Unlock()
360
361 if f.scrollVisible == scrollVisible {
362 return
363 }
364
365 f.scrollVisible = scrollVisible
366 f.drawImage()
367 }
368
369
370
371 func (f *TextField) SetAutoHideScrollBar(autoHide bool) {
372 f.Lock()
373 defer f.Unlock()
374
375 if f.scrollAutoHide == autoHide {
376 return
377 }
378
379 f.scrollAutoHide = autoHide
380 f.drawImage()
381 }
382
383
384 func (f *TextField) WordWrap() bool {
385 f.Lock()
386 defer f.Unlock()
387
388 return f.wordWrap
389 }
390
391
392 func (f *TextField) SetWordWrap(wrap bool) {
393 f.Lock()
394 defer f.Unlock()
395
396 if f.wordWrap == wrap {
397 return
398 }
399
400 f.wordWrap = wrap
401 f.drawImage()
402 }
403
404
405
406 func (f *TextField) SetHandleKeyboard(handle bool) {
407 f.Lock()
408 defer f.Unlock()
409
410 f.handleKeyboard = handle
411 }
412
413
414 func (f *TextField) Write(p []byte) (n int, err error) {
415 f.Lock()
416 defer f.Unlock()
417
418 f.buffer += string(p)
419 f.modified = true
420 return len(p), nil
421 }
422
423
424
425 func (f *TextField) Update() error {
426 f.Lock()
427 defer f.Unlock()
428
429 if !f.visible || rectIsZero(f.r) {
430 return nil
431 }
432
433 var redraw bool
434
435
436 if f.handleKeyboard {
437 offsetAmount := 0
438 if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) {
439 offsetAmount = -100
440 } else if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) {
441 offsetAmount = 100
442 }
443 if offsetAmount != 0 {
444 f.offset += offsetAmount
445 f.clampOffset()
446 redraw = true
447 }
448 }
449
450
451 _, scrollY := ebiten.Wheel()
452 if scrollY != 0 {
453 x, y := ebiten.CursorPosition()
454 p := image.Point{x, y}
455 if p.In(f.r) {
456 const offsetAmount = 25
457 f.offset -= int(scrollY * offsetAmount)
458 f.clampOffset()
459 redraw = true
460 }
461 }
462
463
464 if f.showScrollBar() {
465 if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) || f.scrollDrag {
466 x, y := ebiten.CursorPosition()
467 p := image.Point{x - f.r.Min.X, y - f.r.Min.Y}
468 if f.scrollDrag || p.In(f.scrollRect) {
469 dragY := y - f.r.Min.Y - f.scrollWidth/4
470 if dragY < 0 {
471 dragY = 0
472 } else if dragY > f.scrollRect.Dy() {
473 dragY = f.scrollRect.Dy()
474 }
475 pct := float64(dragY) / float64(f.scrollRect.Dy()-f.scrollWidth/2)
476 if pct > 1 {
477 pct = 1
478 }
479 h := f.r.Dy()
480 f.offset = int(float64(f.bufferSize-h) * pct)
481 redraw = true
482 f.scrollDrag = true
483 }
484 if !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
485 f.scrollDrag = false
486 }
487 }
488 }
489
490 if redraw {
491 f.drawImage()
492 }
493 return nil
494 }
495
496
497
498 func (f *TextField) Draw(screen *ebiten.Image) {
499 f.Lock()
500 defer f.Unlock()
501
502 if f.modified {
503 f.bufferModified()
504 f.modified = false
505 }
506
507 if !f.visible || rectIsZero(f.r) || f.img == nil {
508 return
509 }
510
511 op := &ebiten.DrawImageOptions{}
512 op.GeoM.Translate(float64(f.r.Min.X), float64(f.r.Min.Y))
513 screen.DrawImage(f.img, op)
514 }
515
516 func (f *TextField) fontUpdated() {
517 m := f.face.Metrics()
518 f.lineHeight = m.Height.Round()
519 f.lineOffset = m.Ascent.Round()
520 }
521
522 func (f *TextField) wrapContent(withScrollBar bool) {
523 f.lineWidths = f.lineWidths[:0]
524 buffer := f.prefix + f.buffer + f.suffix
525
526 if f.singleLine {
527 buffer = strings.ReplaceAll(buffer, "\n", "")
528 bounds := text.BoundString(f.face, buffer)
529
530 f.bufferWrapped = []string{buffer}
531 f.lineWidths = append(f.lineWidths, bounds.Dx())
532 return
533 }
534
535 w := f.r.Dx()
536 if withScrollBar {
537 w -= f.scrollWidth
538 }
539 f.bufferWrapped = f.bufferWrapped[:0]
540 for _, line := range strings.Split(buffer, "\n") {
541
542 if strings.TrimSpace(line) == "" {
543 f.bufferWrapped = append(f.bufferWrapped, "")
544 f.lineWidths = append(f.lineWidths, 0)
545 continue
546 }
547
548 l := len(line)
549 availableWidth := w - (f.padding * 2)
550 var start int
551 var end int
552 var initialEnd int
553 for start < l {
554 for end = l; end > start; end-- {
555 initialEnd = end
556
557 bounds := text.BoundString(f.face, line[start:end])
558 if bounds.Dx() > availableWidth {
559 continue
560 }
561
562 if f.wordWrap && end < l && !unicode.IsSpace(rune(line[end])) {
563 for endOffset := 0; endOffset < end-start; endOffset++ {
564 if unicode.IsSpace(rune(line[end-endOffset])) {
565 end = end - endOffset
566 if end < l-1 {
567 end++
568 }
569 break
570 }
571 }
572 }
573
574 if end != initialEnd && f.horizontal != AlignStart {
575 bounds = text.BoundString(f.face, line[start:end])
576 }
577
578 f.bufferWrapped = append(f.bufferWrapped, line[start:end])
579 f.lineWidths = append(f.lineWidths, bounds.Dx())
580 break
581 }
582 start = end
583 }
584 }
585 }
586
587
588 func (f *TextField) drawContent() (overflow bool) {
589 f.img.Fill(f.backgroundColor)
590
591 fieldWidth := f.r.Dx()
592 fieldHeight := f.r.Dy()
593 if f.showScrollBar() {
594 fieldWidth -= f.scrollWidth
595 }
596 lines := len(f.bufferWrapped)
597
598 h := f.r.Dy()
599 lineHeight := f.overrideLineHeight
600 if lineHeight == 0 {
601 lineHeight = f.lineHeight
602 }
603
604 f.bufferSize = 0
605 for i, line := range f.bufferWrapped {
606 lineX := f.padding
607 lineY := f.lineOffset + lineHeight*i
608
609
610 if f.singleLine {
611 bounds := text.BoundString(f.face, line)
612 f.bufferSize = bounds.Dx() + f.padding*2
613 } else {
614 f.bufferSize = lineY + f.padding*2
615 }
616
617
618 lineOverflows := lineY < 0 || lineY >= h-(f.padding*2)
619 if lineOverflows {
620 overflow = true
621 }
622
623
624 if lineY < 0 || lineY-lineHeight > f.offset+h {
625 continue
626 }
627
628
629 if f.singleLine {
630 lineX -= f.offset
631 } else {
632 lineY -= f.offset
633 }
634
635
636 if f.horizontal == AlignCenter {
637 lineX = (fieldWidth - f.lineWidths[i]) / 2
638 } else if f.horizontal == AlignEnd {
639 lineX = (fieldWidth - f.lineWidths[i]) - f.padding*2
640 }
641
642
643 if f.vertical == AlignCenter && lineHeight*lines <= h {
644 lineY = (fieldHeight-(lineHeight*lines))/2 + lineHeight*(i+1) - f.lineOffset
645 } else if f.vertical == AlignEnd && lineHeight*lines <= h {
646 lineY = (fieldHeight - lineHeight*i) - f.padding*2
647 }
648
649
650 text.Draw(f.img, line, f.face, lineX, lineY, f.textColor)
651 }
652
653 return overflow
654 }
655
656 func (f *TextField) clampOffset() {
657 fieldSize := f.r.Dy()
658 if f.singleLine {
659 fieldSize = f.r.Dx()
660 }
661
662 if f.offset > f.bufferSize-fieldSize {
663 f.offset = f.bufferSize - fieldSize
664 }
665 if f.offset < 0 {
666 f.offset = 0
667 }
668 }
669
670 func (f *TextField) showScrollBar() bool {
671 return !f.singleLine && f.scrollVisible && (f.overflow || !f.scrollAutoHide)
672 }
673
674
675 func (f *TextField) drawImage() {
676 if rectIsZero(f.r) {
677 f.img = nil
678 return
679 }
680
681 w, h := f.r.Dx(), f.r.Dy()
682 var newImage bool
683 if f.img == nil {
684 newImage = true
685 } else {
686 imgRect := f.img.Bounds()
687 imgW, imgH := imgRect.Dx(), imgRect.Dy()
688 newImage = imgW != w || imgH != h
689 }
690 if newImage {
691 f.img = ebiten.NewImage(w, h)
692 }
693
694 f.wrapContent(false)
695 f.overflow = f.drawContent()
696 if f.showScrollBar() {
697 f.wrapContent(true)
698 f.drawContent()
699 }
700
701
702 if f.showScrollBar() {
703 scrollAreaX, scrollAreaY := w-f.scrollWidth, 0
704 f.scrollRect = image.Rect(scrollAreaX, scrollAreaY, scrollAreaX+f.scrollWidth, h)
705
706 scrollBarH := f.scrollWidth / 2
707 if scrollBarH < 4 {
708 scrollBarH = 4
709 }
710
711 scrollX, scrollY := w-f.scrollWidth, 0
712 pct := float64(f.offset) / float64(f.bufferSize-h)
713 scrollY += int(float64(h-scrollBarH) * pct)
714 scrollBarRect := image.Rect(scrollX, scrollY, scrollX+f.scrollWidth, scrollY+scrollBarH)
715
716 f.img.SubImage(f.scrollRect).(*ebiten.Image).Fill(color.RGBA{200, 200, 200, 255})
717 f.img.SubImage(scrollBarRect).(*ebiten.Image).Fill(color.RGBA{108, 108, 108, 255})
718 }
719 }
720
721 func (f *TextField) bufferModified() {
722 f.drawImage()
723
724 if !f.follow {
725 return
726 }
727 fieldSize := f.r.Dy()
728 if f.singleLine {
729 fieldSize = f.r.Dx()
730 }
731 offset := f.bufferSize - fieldSize
732 if offset < 0 {
733 offset = 0
734 }
735 if offset != f.offset {
736 f.offset = offset
737 f.drawImage()
738 }
739 }
740
741 func rectIsZero(r image.Rectangle) bool {
742 return r == image.Rectangle{}
743 }
744
View as plain text