This repository has been archived on 2024-06-02. You can view files and clone it, but cannot push or open issues or pull requests.
x/textbox.go
2023-08-12 01:03:34 -04:00

309 lines
7.5 KiB
Go

package x
import "image"
import "image/color"
import "golang.org/x/image/font"
import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/tomo/typeset"
import "git.tebibyte.media/tomo/tomo/text"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas"
type textBox struct {
*box
hOverflow, vOverflow bool
contentBounds image.Rectangle
scroll image.Point
text string
textColor color.Color
face font.Face
wrap bool
hAlign tomo.Align
selectable bool
selecting bool
selectStart int
dot text.Dot
dotColor color.Color
drawer typeset.Drawer
on struct {
contentBoundsChange event.FuncBroadcaster
dotChange event.FuncBroadcaster
}
}
func (backend *Backend) NewTextBox() tomo.TextBox {
box := &textBox {
box: backend.NewBox().(*box),
textColor: color.Black,
dotColor: color.RGBA { B: 255, G: 255, A: 255 },
}
box.box.drawer = box
box.outer = box
return box
}
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) {
// TODO: constrain scroll
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.recalculateMinimumSize()
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.recalculateMinimumSize()
this.invalidateLayout()
}
func (this *textBox) SetWrap (wrap bool) {
if this.wrap == wrap { return }
this.drawer.SetWrap(wrap)
this.recalculateMinimumSize()
this.invalidateLayout()
}
func (this *textBox) SetSelectable (selectable bool) {
if this.selectable == selectable { return }
this.selectable = selectable
}
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.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)
}
func (this *textBox) SetAlign (x, y tomo.Align) {
if this.hAlign == x { return }
this.hAlign = x
switch x {
case tomo.AlignStart: this.drawer.SetAlign(typeset.AlignLeft)
case tomo.AlignMiddle: this.drawer.SetAlign(typeset.AlignCenter)
case tomo.AlignEnd: this.drawer.SetAlign(typeset.AlignRight)
case tomo.AlignEven: this.drawer.SetAlign(typeset.AlignJustify)
}
this.invalidateDraw()
}
func (this *textBox) Draw (can canvas.Canvas) {
if can == nil { return }
this.drawBorders(can)
pen := can.Pen()
pen.Fill(this.color)
pen.Rectangle(can.Bounds())
if this.selectable && this.Focused() {
this.drawDot(can)
}
if this.face == nil { return }
this.drawer.Draw(can, this.textColor, this.textOffset())
}
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) {
if this.face == nil { return }
pen := can.Pen()
pen.Fill(color.Transparent)
pen.Stroke(this.textColor)
pen.StrokeWeight(1)
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 }
switch {
case dot.Empty():
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
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)),
})
default:
pen.Fill(this.dotColor)
pen.StrokeWeight(0)
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.
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()
}
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))
}
func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
bounds := this.drawer.LayoutBoundsSpace()
return bounds.Sub(bounds.Min)
}
func (this *textBox) recalculateMinimumSize () {
minimum := image.Pt (
this.drawer.Em().Round(),
this.drawer.LineHeight().Round())
textSize := this.drawer.MinimumSize()
if !this.hOverflow && !this.wrap {
minimum.X = textSize.X
}
if !this.vOverflow {
minimum.Y = textSize.Y
}
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
this.SetMinimumSize(minimum)
}
func (this *textBox) doLayout () {
this.box.doLayout()
previousContentBounds := this.contentBounds
innerBounds := this.InnerBounds()
this.drawer.SetMaxWidth(innerBounds.Dx())
this.drawer.SetMaxHeight(innerBounds.Dy())
this.contentBounds = this.normalizedLayoutBoundsSpace().Sub(this.scroll)
if previousContentBounds != this.contentBounds {
this.on.contentBoundsChange.Broadcast()
}
}