package messeji import ( "image" "image/color" "strings" "sync" "unicode" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/text" "golang.org/x/image/font" ) const ( initialPadding = 2 initialScrollWidth = 32 ) var ( initialForeground = color.RGBA{0, 0, 0, 255} initialBackground = color.RGBA{255, 255, 255, 255} ) // TextField is a text display field. Call Update and Draw when your Game's // Update and Draw methods are called. // // Note: A position and size must be set via SetRect before the field will appear. // Keyboard events are not handled by default, and may be enabled via SetHandleKeyboard. type TextField struct { // r specifies the position and size of the field. r image.Rectangle // buffer is the actual content of the field. buffer string // prefix is the text shown before the content of the field. prefix string // suffix is the text shown after the content of the field. suffix string // wordWrap determines whether content is wrapped at word boundaries. wordWrap bool // bufferWrapped is the content of the field as it appears on the screen. bufferWrapped []string // bufferSize is the size (in pixels) of the entire text buffer. When single // line mode is enabled, bufferSize int // singleLine is whether the field displays all text on a single line. singleLine bool // face is the font face of the text within the field. face font.Face // lineHeight is the height of a single line of text. lineHeight int // overrideLineHeight is the custom height for a line of text, or 0 to disable. overrideLineHeight int // textColor is the color of the text within the field. textColor color.Color // backgroundColor is the color of the background of the field. backgroundColor color.Color // padding is the amount of padding around the text within the field. padding int // follow determines whether the field should automatically scroll to the // end when content is added to the buffer. follow bool // overflow is whether the content of the field is currently larger than the field. overflow bool // offset is the current view offset of the text within the field. offset int // handleKeyboard is a flag which, when enabled, causes keyboard input to be handled. handleKeyboard bool // modified is a flag which, when enabled, causes bufferModified to be called // during the next Draw call. modified bool // scrollRect specifies the position and size of the scrolling area. scrollRect image.Rectangle // scrollWidth is the width of the scroll bar. scrollWidth int // scrollVisible is whether the scroll bar is visible on the screen. scrollVisible bool // scrollAutoHide is whether the scroll bar should be automatically hidden // when the entire text buffer fits within the screen. scrollAutoHide bool // scrollDrag is whether the scroll bar is currently being dragged. scrollDrag bool // img is the image of the field. img *ebiten.Image // visible is whether the field is visible on the screen. visible bool sync.Mutex } // NewTextField returns a new TextField. See type documentation for more info. func NewTextField(face font.Face) *TextField { f := &TextField{ face: face, textColor: initialForeground, backgroundColor: initialBackground, padding: initialPadding, scrollWidth: initialScrollWidth, follow: true, wordWrap: true, scrollVisible: true, scrollAutoHide: true, visible: true, } f.setDefaultLineHeight() return f } // Rect returns the position and size of the field. func (f *TextField) Rect() image.Rectangle { f.Lock() defer f.Unlock() return f.r } // SetRect sets the position and size of the field. func (f *TextField) SetRect(r image.Rectangle) { f.Lock() defer f.Unlock() f.r = r f.drawImage() } // Text returns the text in the field. func (f *TextField) Text() string { f.Lock() defer f.Unlock() return f.buffer } // SetText sets the text in the field. func (f *TextField) SetText(text string) { f.Lock() defer f.Unlock() f.buffer = text f.modified = true } // SetPrefix sets the text shown before the content of the field. func (f *TextField) SetPrefix(text string) { f.Lock() defer f.Unlock() f.prefix = text f.drawImage() } // SetSuffix sets the text shown before the content of the field. func (f *TextField) SetSuffix(text string) { f.Lock() defer f.Unlock() f.suffix = text f.drawImage() } // SetFollow sets whether the field should automatically scroll to the end when // content is added to the buffer. func (f *TextField) SetFollow(follow bool) { f.Lock() defer f.Unlock() f.follow = follow } // SetSingleLine sets whether the field displays all text on a single line. // When enabled, the field scrolls horizontally. Otherwise, it scrolls vertically. func (f *TextField) SetSingleLine(single bool) { f.Lock() defer f.Unlock() if f.singleLine == single { return } f.singleLine = single f.bufferModified() } // LineHeight returns the line height for the field. func (f *TextField) LineHeight() int { f.Lock() defer f.Unlock() if f.overrideLineHeight != 0 { return f.overrideLineHeight } return f.lineHeight } // SetLineHeight sets a custom line height for the field. Setting a line // height of 0 restores the automatic line height detection based on the font. func (f *TextField) SetLineHeight(height int) { f.Lock() defer f.Unlock() f.overrideLineHeight = height } // SetForegroundColor sets the color of the text within the field. func (f *TextField) SetForegroundColor(c color.Color) { f.Lock() defer f.Unlock() f.textColor = c } // SetBackgroundColor sets the color of the background of the field. func (f *TextField) SetBackgroundColor(c color.Color) { f.Lock() defer f.Unlock() f.backgroundColor = c } // SetFont sets the font face of the text within the field. func (f *TextField) SetFont(face font.Face) { f.Lock() defer f.Unlock() f.face = face f.setDefaultLineHeight() } // Padding returns the amount of padding around the text within the field. func (f *TextField) Padding() int { f.Lock() defer f.Unlock() return f.padding } // SetPadding sets the amount of padding around the text within the field. func (f *TextField) SetPadding(padding int) { f.Lock() defer f.Unlock() f.padding = padding } // Visible returns whether the field is currently visible on the screen. func (f *TextField) Visible() bool { return f.visible } // SetVisible sets whether the field is visible on the screen. func (f *TextField) SetVisible(visible bool) { f.Lock() defer f.Unlock() if f.visible == visible { return } f.visible = visible if visible { f.drawImage() } } // SetScrollBarWidth sets the width of the scroll bar. func (f *TextField) SetScrollBarWidth(width int) { f.Lock() defer f.Unlock() if f.scrollWidth == width { return } f.scrollWidth = width f.drawImage() } // SetScrollBarVisible sets whether the scroll bar is visible on the screen. func (f *TextField) SetScrollBarVisible(scrollVisible bool) { f.Lock() defer f.Unlock() if f.scrollVisible == scrollVisible { return } f.scrollVisible = scrollVisible f.drawImage() } // SetAutoHideScrollBar sets whether the scroll bar is automatically hidden // when the entire text buffer is visible. func (f *TextField) SetAutoHideScrollBar(autoHide bool) { f.Lock() defer f.Unlock() if f.scrollAutoHide == autoHide { return } f.scrollAutoHide = autoHide f.drawImage() } // WordWrap returns the current text wrap mode. func (f *TextField) WordWrap() bool { f.Lock() defer f.Unlock() return f.wordWrap } // SetWordWrap sets a flag which, when enabled, causes text to wrap without breaking words. func (f *TextField) SetWordWrap(wrap bool) { f.Lock() defer f.Unlock() if f.wordWrap == wrap { return } f.wordWrap = wrap f.drawImage() } // SetHandleKeyboard sets a flag controlling whether keyboard input should be handled // by the field. This can be used to facilitate focus changes between multiple inputs. func (f *TextField) SetHandleKeyboard(handle bool) { f.Lock() defer f.Unlock() f.handleKeyboard = handle } // Write writes to the field's buffer. func (f *TextField) Write(p []byte) (n int, err error) { f.Lock() defer f.Unlock() f.buffer += string(p) f.modified = true return len(p), nil } // Update updates the field. This function should be called when // Game.Update is called. func (f *TextField) Update() error { f.Lock() defer f.Unlock() if !f.visible || rectIsZero(f.r) { return nil } var redraw bool // Handle keyboard PageUp/PageDown. if f.handleKeyboard { offsetAmount := 0 if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) { offsetAmount = -100 } else if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) { offsetAmount = 100 } if offsetAmount != 0 { f.offset += offsetAmount f.clampOffset() redraw = true } } // Handle mouse wheel. _, scrollY := ebiten.Wheel() if scrollY != 0 { x, y := ebiten.CursorPosition() p := image.Point{x, y} if p.In(f.r) { const offsetAmount = 25 f.offset -= int(scrollY * offsetAmount) f.clampOffset() redraw = true } } // Handle scroll bar click (and drag). if f.showScrollBar() { if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) || f.scrollDrag { x, y := ebiten.CursorPosition() p := image.Point{x - f.r.Min.X, y - f.r.Min.Y} if f.scrollDrag || p.In(f.scrollRect) { dragY := y - f.r.Min.Y - f.scrollWidth/4 if dragY < 0 { dragY = 0 } else if dragY > f.scrollRect.Dy() { dragY = f.scrollRect.Dy() } pct := float64(dragY) / float64(f.scrollRect.Dy()-f.scrollWidth/2) if pct > 1 { pct = 1 } h := f.r.Dy() f.offset = int(float64(f.bufferSize-h) * pct) redraw = true f.scrollDrag = true } if !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { f.scrollDrag = false } } } if redraw { f.drawImage() } return nil } // Draw draws the field on the screen. This function should be called // when Game.Draw is called. func (f *TextField) Draw(screen *ebiten.Image) { f.Lock() defer f.Unlock() if f.modified { f.bufferModified() f.modified = false } if !f.visible || rectIsZero(f.r) || f.img == nil { return } op := &ebiten.DrawImageOptions{} op.GeoM.Translate(float64(f.r.Min.X), float64(f.r.Min.Y)) screen.DrawImage(f.img, op) } func (f *TextField) setDefaultLineHeight() { lineBounds := text.BoundString(f.face, "ATZgpq.") f.lineHeight = lineBounds.Dy() + 5 } func (f *TextField) wrapContent(withScrollBar bool) { buffer := f.prefix + f.buffer + f.suffix if f.singleLine { f.bufferWrapped = []string{strings.ReplaceAll(buffer, "\n", "")} return } w := f.r.Dx() if withScrollBar { w -= f.scrollWidth } f.bufferWrapped = f.bufferWrapped[:0] for _, line := range strings.Split(buffer, "\n") { l := len(line) var start int var end int for start < l { for end = l; end > start; end-- { bounds := text.BoundString(f.face, line[start:end]) if bounds.Dx() < w-(f.padding*2)-2 { if f.wordWrap { if end < l && !unicode.IsSpace(rune(line[end])) { for endOffset := 0; endOffset < end-start; endOffset++ { if unicode.IsSpace(rune(line[end-endOffset])) { end = end - endOffset break } } } } f.bufferWrapped = append(f.bufferWrapped, line[start:end]) break } } start = end } } } // drawContent draws the text buffer to img. func (f *TextField) drawContent() (overflow bool) { f.img.Fill(f.backgroundColor) h := f.r.Dy() lineHeight := f.overrideLineHeight if lineHeight == 0 { lineHeight = f.lineHeight } lineOffset := 7 f.bufferSize = 0 for i, line := range f.bufferWrapped { lineX := f.padding lineY := f.padding lineY += lineHeight*(i+1) - lineOffset if f.singleLine { bounds := text.BoundString(f.face, line) f.bufferSize = bounds.Dx() + f.padding*4 } else { f.bufferSize = lineY + lineOffset + f.padding } if lineY < 0 || lineY >= h-(f.padding*2) { overflow = true } // Skip drawing off-screen lines. if lineY < 0 || lineY-lineHeight > f.offset+h { continue } if f.singleLine { lineX -= f.offset } else { lineY -= f.offset } text.Draw(f.img, line, f.face, lineX, lineY, f.textColor) } return overflow } func (f *TextField) clampOffset() { fieldSize := f.r.Dy() if f.singleLine { fieldSize = f.r.Dx() } if f.offset > f.bufferSize-fieldSize { f.offset = f.bufferSize - fieldSize } if f.offset < 0 { f.offset = 0 } } func (f *TextField) showScrollBar() bool { return !f.singleLine && f.scrollVisible && (f.overflow || !f.scrollAutoHide) } // drawImage draws the field to img (caching it for future draws). func (f *TextField) drawImage() { if rectIsZero(f.r) { f.img = nil return } w, h := f.r.Dx(), f.r.Dy() var newImage bool if f.img == nil { newImage = true } else { imgRect := f.img.Bounds() imgW, imgH := imgRect.Dx(), imgRect.Dy() newImage = imgW != w || imgH != h } if newImage { f.img = ebiten.NewImage(w, h) } f.wrapContent(false) f.overflow = f.drawContent() if f.showScrollBar() { f.wrapContent(true) f.drawContent() } scrollBarW := 32 // Draw scrollbar. if f.showScrollBar() { scrollAreaX, scrollAreaY := w-scrollBarW, 0 f.scrollRect = image.Rect(scrollAreaX, scrollAreaY, scrollAreaX+scrollBarW, h) scrollBarH := f.scrollWidth / 2 if scrollBarH < 4 { scrollBarH = 4 } scrollX, scrollY := w-scrollBarW, 0 pct := float64(f.offset) / float64(f.bufferSize-h) scrollY += int(float64(h-scrollBarH) * pct) scrollBarRect := image.Rect(scrollX, scrollY, scrollX+scrollBarW, scrollY+scrollBarH) f.img.SubImage(f.scrollRect).(*ebiten.Image).Fill(color.RGBA{200, 200, 200, 255}) f.img.SubImage(scrollBarRect).(*ebiten.Image).Fill(color.RGBA{108, 108, 108, 255}) } } func (f *TextField) bufferModified() { f.drawImage() if !f.follow { return } fieldSize := f.r.Dy() if f.singleLine { fieldSize = f.r.Dx() } offset := f.bufferSize - fieldSize if offset < 0 { offset = 0 } if offset != f.offset { f.offset = offset f.drawImage() } } func rectIsZero(r image.Rectangle) bool { return r == image.Rectangle{} }