x/textbox.go

350 lines
8.5 KiB
Go
Raw Permalink Normal View History

2023-07-02 06:52:14 +00:00
package x
2023-07-12 05:17:12 +00:00
import "image"
import "image/color"
import "golang.org/x/image/font"
2023-07-02 06:52:14 +00:00
import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/math/fixed"
2023-07-12 05:17:12 +00:00
import "git.tebibyte.media/tomo/typeset"
import "git.tebibyte.media/tomo/tomo/text"
import "git.tebibyte.media/tomo/tomo/input"
2023-07-12 05:17:12 +00:00
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
type textBox struct {
*box
2023-07-12 05:17:12 +00:00
hOverflow, vOverflow bool
contentBounds image.Rectangle
scroll image.Point
text string
textColor color.Color
face font.Face
2023-07-13 16:48:09 +00:00
wrap bool
2023-07-12 05:17:12 +00:00
hAlign tomo.Align
2024-04-24 15:40:31 +00:00
vAlign tomo.Align
selectable bool
selecting bool
selectStart int
dot text.Dot
dotColor color.Color
2023-07-12 05:17:12 +00:00
drawer typeset.Drawer
2023-07-12 05:17:12 +00:00
on struct {
contentBoundsChange event.FuncBroadcaster
dotChange event.FuncBroadcaster
2023-07-12 05:17:12 +00:00
}
}
2023-07-02 06:52:14 +00:00
func (backend *Backend) NewTextBox() tomo.TextBox {
this := &textBox {
2023-07-12 05:17:12 +00:00
textColor: color.Black,
2023-08-06 07:42:06 +00:00
dotColor: color.RGBA { B: 255, G: 255, A: 255 },
2023-07-12 05:17:12 +00:00
}
this.box = backend.newBox(this)
return this
2023-07-12 05:17:12 +00:00
}
func (this *textBox) SetOverflow (horizontal, vertical bool) {
if this.hOverflow == horizontal && this.vOverflow == vertical { return }
this.hOverflow = horizontal
this.vOverflow = vertical
this.invalidateLayout()
}
func (this *textBox) ContentBounds () image.Rectangle {
return this.contentBounds
}
func (this *textBox) ScrollTo (point image.Point) {
if this.scroll == point { return }
2023-07-12 05:17:12 +00:00
this.scroll = point
this.invalidateLayout()
}
func (this *textBox) OnContentBoundsChange (callback func()) event.Cookie {
return this.on.contentBoundsChange.Connect(callback)
}
func (this *textBox) SetText (text string) {
if this.text == text { return }
this.text = text
this.drawer.SetText([]rune(text))
this.invalidateMinimum()
2023-07-12 05:17:12 +00:00
this.invalidateLayout()
}
func (this *textBox) SetTextColor (c color.Color) {
if this.textColor == c { return }
this.textColor = c
this.invalidateDraw()
}
func (this *textBox) SetFace (face font.Face) {
if this.face == face { return }
this.face = face
this.drawer.SetFace(face)
this.invalidateMinimum()
2023-07-12 05:17:12 +00:00
this.invalidateLayout()
}
2023-07-13 16:48:09 +00:00
func (this *textBox) SetWrap (wrap bool) {
if this.wrap == wrap { return }
2023-07-16 04:42:47 +00:00
this.drawer.SetWrap(wrap)
this.invalidateMinimum()
2023-07-13 16:48:09 +00:00
this.invalidateLayout()
}
2023-07-12 05:17:12 +00:00
func (this *textBox) SetSelectable (selectable bool) {
if this.selectable == selectable { return }
this.selectable = selectable
}
2023-08-05 01:59:52 +00:00
func (this *textBox) SetDotColor (c color.Color) {
if this.dotColor == c { return }
this.dotColor = c
if !this.dot.Empty() {
this.invalidateDraw()
}
}
func (this *textBox) Select (dot text.Dot) {
if !this.selectable { return }
if this.dot == dot { return }
this.SetFocused(true)
this.dot = dot
this.scrollToDot()
this.on.dotChange.Broadcast()
this.invalidateDraw()
}
func (this *textBox) Dot () text.Dot {
return this.dot
}
func (this *textBox) OnDotChange (callback func ()) event.Cookie {
return this.on.dotChange.Connect(callback)
}
2023-07-13 16:48:09 +00:00
func (this *textBox) SetAlign (x, y tomo.Align) {
2024-04-24 15:40:31 +00:00
if this.hAlign == x && this.vAlign == y { return }
2023-07-13 16:48:09 +00:00
this.hAlign = x
2024-04-24 15:40:31 +00:00
this.vAlign = y
this.drawer.SetAlign(typeset.Align(x), typeset.Align(y))
2023-07-12 05:17:12 +00:00
this.invalidateDraw()
}
func (this *textBox) Draw (can canvas.Canvas) {
if can == nil { return }
this.drawBorders(can)
pen := can.Pen()
pen.Fill(this.color)
pen.Texture(this.texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
2023-07-12 05:17:12 +00:00
pen.Rectangle(can.Bounds())
if this.selectable && this.Focused() {
this.drawDot(can)
}
2023-07-12 05:17:12 +00:00
if this.face == nil { return }
this.drawer.Draw(can, this.textColor, this.textOffset())
}
2023-08-05 01:59:52 +00:00
func roundPt (point fixed.Point26_6) image.Point {
return image.Pt(point.X.Round(), point.Y.Round())
}
func fixPt (point image.Point) fixed.Point26_6 {
return fixed.P(point.X, point.Y)
}
func (this *textBox) drawDot (can canvas.Canvas) {
2023-08-12 05:03:34 +00:00
if this.face == nil { return }
pen := can.Pen()
pen.Fill(color.Transparent)
pen.Stroke(this.textColor)
2023-08-05 01:59:52 +00:00
bounds := this.InnerBounds()
metrics := this.face.Metrics()
dot := this.dot.Canon()
start := this.drawer.PositionAt(dot.Start).Add(fixPt(this.textOffset()))
end := this.drawer.PositionAt(dot.End ).Add(fixPt(this.textOffset()))
height := this.drawer.LineHeight().Round()
ascent := fixed.Point26_6 { Y: metrics.Descent }
descent := fixed.Point26_6 { Y: metrics.Ascent }
2023-08-05 01:59:52 +00:00
switch {
case dot.Empty():
pen.StrokeWeight(1)
2023-08-05 01:59:52 +00:00
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
2023-08-05 01:59:52 +00:00
case start.Y == end.Y:
pen.Fill(this.dotColor)
pen.StrokeWeight(0)
pen.Rectangle(image.Rectangle {
Min: roundPt(start.Add(ascent)),
Max: roundPt(end.Sub(descent)),
})
2023-08-05 01:59:52 +00:00
default:
pen.Fill(this.dotColor)
pen.StrokeWeight(0)
2023-08-05 01:59:52 +00:00
rect := image.Rectangle {
Min: roundPt(start.Add(ascent)),
Max: roundPt(start.Sub(descent)),
}
rect.Max.X = bounds.Max.X
pen.Rectangle(rect)
if end.Y - start.Y > fixed.I(height) {
rect.Min.X = bounds.Min.X
rect.Min.Y = roundPt(start.Sub(descent)).Y + height
rect.Max.X = bounds.Max.X
rect.Max.Y = roundPt(end.Add(ascent)).Y - height
pen.Rectangle(rect)
}
rect = image.Rectangle {
Min: roundPt(end.Add(ascent)),
Max: roundPt(end.Sub(descent)),
}
rect.Min.X = bounds.Min.X
pen.Rectangle(rect)
}
}
func (this *textBox) textOffset () image.Point {
return this.InnerBounds().Min.
2023-07-12 05:17:12 +00:00
Sub(this.scroll).
Sub(this.drawer.LayoutBoundsSpace().Min)
}
func (this *textBox) handleFocusLeave () {
this.dot = text.EmptyDot(0)
this.on.dotChange.Broadcast()
this.invalidateDraw()
this.box.handleFocusLeave()
2023-07-12 05:17:12 +00:00
}
func (this *textBox) handleMouseDown (button input.Button) {
if button == input.ButtonLeft {
index := this.runeUnderMouse()
this.selectStart = index
this.selecting = true
this.Select(text.Dot { Start: this.selectStart, End: index })
}
this.box.handleMouseDown(button)
}
func (this *textBox) handleMouseUp (button input.Button) {
if button == input.ButtonLeft && this.selecting {
index := this.runeUnderMouse()
this.selecting = false
this.Select(text.Dot { Start: this.selectStart, End: index })
}
this.box.handleMouseUp(button)
}
func (this *textBox) handleMouseMove () {
if this.selecting {
index := this.runeUnderMouse()
this.Select(text.Dot { Start: this.selectStart, End: index })
}
this.box.handleMouseMove()
}
func (this *textBox) runeUnderMouse () int {
position := this.MousePosition().Sub(this.textOffset())
return this.drawer.AtPosition(fixPt(position))
}
2023-07-13 16:48:09 +00:00
func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
bounds := this.drawer.LayoutBoundsSpace()
return bounds.Sub(bounds.Min)
}
func (this *textBox) contentMinimum () image.Point {
minimum := this.drawer.MinimumSize()
2023-07-13 16:48:09 +00:00
if this.hOverflow || this.wrap {
minimum.X = this.drawer.Em().Round()
2023-07-13 16:48:09 +00:00
}
if this.vOverflow {
minimum.Y = this.drawer.LineHeight().Round()
2023-07-13 16:48:09 +00:00
}
return minimum.Add(this.box.contentMinimum())
2023-07-12 05:17:12 +00:00
}
func (this *textBox) doLayout () {
this.box.doLayout()
previousContentBounds := this.contentBounds
innerBounds := this.InnerBounds()
2024-04-24 15:40:31 +00:00
this.drawer.SetWidth(innerBounds.Dx())
this.drawer.SetHeight(innerBounds.Dy())
this.contentBounds = this.normalizedLayoutBoundsSpace()
this.constrainScroll()
this.contentBounds = this.contentBounds.Add(this.scroll)
2023-07-12 05:17:12 +00:00
if previousContentBounds != this.contentBounds {
this.on.contentBoundsChange.Broadcast()
}
2023-07-02 06:52:14 +00:00
}
func (this *textBox) constrainScroll () {
innerBounds := this.InnerBounds()
width := this.contentBounds.Dx()
height := this.contentBounds.Dy()
// X
if width <= innerBounds.Dx() {
this.scroll.X = 0
} else if this.scroll.X < 0 {
this.scroll.X = 0
} else if this.scroll.X > width - innerBounds.Dx() {
this.scroll.X = width - innerBounds.Dx()
}
// Y
if height <= innerBounds.Dy() {
this.scroll.Y = 0
} else if this.scroll.Y < 0 {
this.scroll.Y = 0
} else if this.scroll.Y > height - innerBounds.Dy() {
this.scroll.Y = height - innerBounds.Dy()
}
}
func (this *textBox) scrollToDot () {
dot := roundPt(this.drawer.PositionAt(this.dot.End)).Add(this.textOffset())
innerBounds := this.InnerBounds()
scroll := this.scroll
em := this.drawer.Em().Round()
lineHeight := this.drawer.LineHeight().Round()
// X
if dot.X < innerBounds.Min.X + em {
scroll.X -= innerBounds.Min.X - dot.X + em
} else if dot.X > innerBounds.Max.X - em {
scroll.X += dot.X - innerBounds.Max.X + em
}
// Y
if dot.Y < innerBounds.Min.Y + lineHeight {
scroll.Y -= innerBounds.Min.Y - dot.Y + lineHeight
} else if dot.Y > innerBounds.Max.Y - lineHeight {
scroll.Y += dot.Y - innerBounds.Max.Y + lineHeight
}
this.ScrollTo(scroll)
}