2023-07-02 00:52:14 -06:00
|
|
|
package x
|
|
|
|
|
2023-07-11 23:17:12 -06:00
|
|
|
import "image"
|
|
|
|
import "image/color"
|
|
|
|
import "golang.org/x/image/font"
|
2023-07-02 00:52:14 -06:00
|
|
|
import "git.tebibyte.media/tomo/tomo"
|
2023-08-06 01:38:12 -06:00
|
|
|
import "golang.org/x/image/math/fixed"
|
2023-07-11 23:17:12 -06:00
|
|
|
import "git.tebibyte.media/tomo/typeset"
|
2023-08-03 10:45:21 -06:00
|
|
|
import "git.tebibyte.media/tomo/tomo/text"
|
2023-08-06 01:38:12 -06:00
|
|
|
import "git.tebibyte.media/tomo/tomo/input"
|
2023-07-11 23:17:12 -06:00
|
|
|
import "git.tebibyte.media/tomo/tomo/event"
|
|
|
|
import "git.tebibyte.media/tomo/tomo/canvas"
|
|
|
|
|
|
|
|
type textBox struct {
|
|
|
|
*box
|
2023-08-24 15:15:34 -06:00
|
|
|
|
2023-07-11 23:17:12 -06:00
|
|
|
hOverflow, vOverflow bool
|
|
|
|
contentBounds image.Rectangle
|
|
|
|
scroll image.Point
|
|
|
|
|
|
|
|
text string
|
|
|
|
textColor color.Color
|
|
|
|
face font.Face
|
2023-07-13 10:48:09 -06:00
|
|
|
wrap bool
|
2023-07-11 23:17:12 -06:00
|
|
|
hAlign tomo.Align
|
2024-04-24 09:40:31 -06:00
|
|
|
vAlign tomo.Align
|
2023-08-03 10:45:21 -06:00
|
|
|
|
2023-08-06 01:38:12 -06:00
|
|
|
selectable bool
|
|
|
|
selecting bool
|
|
|
|
selectStart int
|
|
|
|
dot text.Dot
|
|
|
|
dotColor color.Color
|
2023-08-24 15:15:34 -06:00
|
|
|
|
2023-07-11 23:17:12 -06:00
|
|
|
drawer typeset.Drawer
|
2023-08-24 15:15:34 -06:00
|
|
|
|
2023-07-11 23:17:12 -06:00
|
|
|
on struct {
|
|
|
|
contentBoundsChange event.FuncBroadcaster
|
2023-08-03 10:45:21 -06:00
|
|
|
dotChange event.FuncBroadcaster
|
2023-07-11 23:17:12 -06:00
|
|
|
}
|
|
|
|
}
|
2023-07-02 00:52:14 -06:00
|
|
|
|
|
|
|
func (backend *Backend) NewTextBox() tomo.TextBox {
|
2023-08-17 21:20:08 -06:00
|
|
|
this := &textBox {
|
2023-07-11 23:17:12 -06:00
|
|
|
textColor: color.Black,
|
2023-08-06 01:42:06 -06:00
|
|
|
dotColor: color.RGBA { B: 255, G: 255, A: 255 },
|
2023-07-11 23:17:12 -06:00
|
|
|
}
|
2023-08-17 21:20:08 -06:00
|
|
|
this.box = backend.newBox(this)
|
|
|
|
return this
|
2023-07-11 23:17:12 -06: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) {
|
2023-09-09 18:22:10 -06:00
|
|
|
if this.scroll == point { return }
|
2023-07-11 23:17:12 -06: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))
|
2023-08-17 21:20:08 -06:00
|
|
|
this.invalidateMinimum()
|
2023-07-11 23:17:12 -06: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)
|
2023-08-17 21:20:08 -06:00
|
|
|
this.invalidateMinimum()
|
2023-07-11 23:17:12 -06:00
|
|
|
this.invalidateLayout()
|
|
|
|
}
|
|
|
|
|
2023-07-13 10:48:09 -06:00
|
|
|
func (this *textBox) SetWrap (wrap bool) {
|
|
|
|
if this.wrap == wrap { return }
|
2023-07-15 22:42:47 -06:00
|
|
|
this.drawer.SetWrap(wrap)
|
2023-08-17 21:20:08 -06:00
|
|
|
this.invalidateMinimum()
|
2023-07-13 10:48:09 -06:00
|
|
|
this.invalidateLayout()
|
|
|
|
}
|
2023-07-11 23:17:12 -06:00
|
|
|
|
2023-08-03 10:45:21 -06:00
|
|
|
func (this *textBox) SetSelectable (selectable bool) {
|
|
|
|
if this.selectable == selectable { return }
|
|
|
|
this.selectable = selectable
|
|
|
|
}
|
|
|
|
|
2023-08-04 19:59:52 -06:00
|
|
|
func (this *textBox) SetDotColor (c color.Color) {
|
|
|
|
if this.dotColor == c { return }
|
|
|
|
this.dotColor = c
|
|
|
|
if !this.dot.Empty() {
|
|
|
|
this.invalidateDraw()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-03 10:45:21 -06:00
|
|
|
func (this *textBox) Select (dot text.Dot) {
|
|
|
|
if !this.selectable { return }
|
|
|
|
if this.dot == dot { return }
|
|
|
|
this.SetFocused(true)
|
|
|
|
this.dot = dot
|
2023-09-09 18:22:10 -06:00
|
|
|
this.scrollToDot()
|
2023-08-03 10:45:21 -06:00
|
|
|
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 10:48:09 -06:00
|
|
|
func (this *textBox) SetAlign (x, y tomo.Align) {
|
2024-04-24 09:40:31 -06:00
|
|
|
if this.hAlign == x && this.vAlign == y { return }
|
2023-07-13 10:48:09 -06:00
|
|
|
this.hAlign = x
|
2024-04-24 09:40:31 -06:00
|
|
|
this.vAlign = y
|
|
|
|
this.drawer.SetAlign(typeset.Align(x), typeset.Align(y))
|
2023-07-11 23:17:12 -06:00
|
|
|
this.invalidateDraw()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *textBox) Draw (can canvas.Canvas) {
|
|
|
|
if can == nil { return }
|
|
|
|
this.drawBorders(can)
|
|
|
|
pen := can.Pen()
|
|
|
|
pen.Fill(this.color)
|
2023-08-25 00:51:07 -06:00
|
|
|
pen.Texture(this.texture)
|
|
|
|
|
|
|
|
if this.transparent() && this.parent != nil {
|
|
|
|
this.parent.drawBackgroundPart(can)
|
|
|
|
}
|
2023-07-11 23:17:12 -06:00
|
|
|
pen.Rectangle(can.Bounds())
|
|
|
|
|
2023-08-03 10:45:21 -06:00
|
|
|
if this.selectable && this.Focused() {
|
|
|
|
this.drawDot(can)
|
|
|
|
}
|
|
|
|
|
2023-07-11 23:17:12 -06:00
|
|
|
if this.face == nil { return }
|
2023-08-03 10:45:21 -06:00
|
|
|
this.drawer.Draw(can, this.textColor, this.textOffset())
|
|
|
|
}
|
|
|
|
|
2023-08-04 19:59:52 -06: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)
|
|
|
|
}
|
|
|
|
|
2023-08-03 10:45:21 -06:00
|
|
|
func (this *textBox) drawDot (can canvas.Canvas) {
|
2023-08-11 23:03:34 -06:00
|
|
|
if this.face == nil { return }
|
|
|
|
|
2023-08-03 10:45:21 -06:00
|
|
|
pen := can.Pen()
|
|
|
|
pen.Fill(color.Transparent)
|
|
|
|
pen.Stroke(this.textColor)
|
2023-08-04 19:59:52 -06: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-24 15:15:34 -06:00
|
|
|
|
2023-08-04 19:59:52 -06:00
|
|
|
switch {
|
|
|
|
case dot.Empty():
|
2023-08-24 15:15:34 -06:00
|
|
|
pen.StrokeWeight(1)
|
2023-08-04 19:59:52 -06:00
|
|
|
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
|
2023-08-24 15:15:34 -06:00
|
|
|
|
2023-08-04 19:59:52 -06: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-24 15:15:34 -06:00
|
|
|
|
2023-08-04 19:59:52 -06:00
|
|
|
default:
|
|
|
|
pen.Fill(this.dotColor)
|
|
|
|
pen.StrokeWeight(0)
|
2023-08-24 15:15:34 -06:00
|
|
|
|
2023-08-04 19:59:52 -06: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)
|
2023-08-03 10:45:21 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *textBox) textOffset () image.Point {
|
|
|
|
return this.InnerBounds().Min.
|
2024-05-13 17:39:36 -06:00
|
|
|
Add(this.scroll).
|
2023-07-11 23:17:12 -06:00
|
|
|
Sub(this.drawer.LayoutBoundsSpace().Min)
|
2023-08-03 10:45:21 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (this *textBox) handleFocusLeave () {
|
|
|
|
this.on.dotChange.Broadcast()
|
|
|
|
this.invalidateDraw()
|
|
|
|
this.box.handleFocusLeave()
|
2023-07-11 23:17:12 -06:00
|
|
|
}
|
|
|
|
|
2023-08-06 01:38:12 -06: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 10:48:09 -06:00
|
|
|
func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
|
|
|
|
bounds := this.drawer.LayoutBoundsSpace()
|
|
|
|
return bounds.Sub(bounds.Min)
|
|
|
|
}
|
|
|
|
|
2023-08-17 21:20:08 -06:00
|
|
|
func (this *textBox) contentMinimum () image.Point {
|
2023-09-09 18:22:10 -06:00
|
|
|
minimum := this.drawer.MinimumSize()
|
2023-07-13 10:48:09 -06:00
|
|
|
|
2023-09-09 18:22:10 -06:00
|
|
|
if this.hOverflow || this.wrap {
|
|
|
|
minimum.X = this.drawer.Em().Round()
|
2023-07-13 10:48:09 -06:00
|
|
|
}
|
2023-09-09 18:22:10 -06:00
|
|
|
if this.vOverflow {
|
|
|
|
minimum.Y = this.drawer.LineHeight().Round()
|
2023-07-13 10:48:09 -06:00
|
|
|
}
|
2023-08-17 21:20:08 -06:00
|
|
|
|
|
|
|
return minimum.Add(this.box.contentMinimum())
|
2023-07-11 23:17:12 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (this *textBox) doLayout () {
|
|
|
|
this.box.doLayout()
|
|
|
|
previousContentBounds := this.contentBounds
|
2023-07-12 16:36:11 -06:00
|
|
|
|
|
|
|
innerBounds := this.InnerBounds()
|
2024-04-24 09:40:31 -06:00
|
|
|
this.drawer.SetWidth(innerBounds.Dx())
|
|
|
|
this.drawer.SetHeight(innerBounds.Dy())
|
2023-08-24 15:15:34 -06:00
|
|
|
|
2023-09-09 18:22:10 -06:00
|
|
|
this.contentBounds = this.normalizedLayoutBoundsSpace()
|
|
|
|
this.constrainScroll()
|
|
|
|
this.contentBounds = this.contentBounds.Add(this.scroll)
|
2024-05-13 17:39:36 -06:00
|
|
|
// println(this.InnerBounds().String(), this.contentBounds.String())
|
2023-08-24 15:15:34 -06:00
|
|
|
|
2023-07-11 23:17:12 -06:00
|
|
|
if previousContentBounds != this.contentBounds {
|
|
|
|
this.on.contentBoundsChange.Broadcast()
|
|
|
|
}
|
2023-07-02 00:52:14 -06:00
|
|
|
}
|
2023-09-09 18:22:10 -06: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
|
2024-05-13 17:39:36 -06:00
|
|
|
} else if this.scroll.X > 0 {
|
2023-09-09 18:22:10 -06:00
|
|
|
this.scroll.X = 0
|
2024-05-13 17:39:36 -06:00
|
|
|
} else if this.scroll.X < innerBounds.Dx() - width {
|
|
|
|
this.scroll.X = innerBounds.Dx() - width
|
2023-09-09 18:22:10 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// Y
|
|
|
|
if height <= innerBounds.Dy() {
|
|
|
|
this.scroll.Y = 0
|
2024-05-13 17:39:36 -06:00
|
|
|
} else if this.scroll.Y > 0 {
|
2023-09-09 18:22:10 -06:00
|
|
|
this.scroll.Y = 0
|
2024-05-13 17:39:36 -06:00
|
|
|
} else if this.scroll.Y < innerBounds.Dy() - height {
|
|
|
|
this.scroll.Y = innerBounds.Dy() - height
|
2023-09-09 18:22:10 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2024-05-13 17:39:36 -06:00
|
|
|
scroll.X += innerBounds.Min.X - dot.X + em
|
2023-09-09 18:22:10 -06:00
|
|
|
} else if dot.X > innerBounds.Max.X - em {
|
2024-05-13 17:39:36 -06:00
|
|
|
scroll.X -= dot.X - innerBounds.Max.X + em
|
2023-09-09 18:22:10 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// Y
|
|
|
|
if dot.Y < innerBounds.Min.Y + lineHeight {
|
2024-05-13 17:39:36 -06:00
|
|
|
scroll.Y += innerBounds.Min.Y - dot.Y + lineHeight
|
2023-09-09 18:22:10 -06:00
|
|
|
} else if dot.Y > innerBounds.Max.Y - lineHeight {
|
2024-05-13 17:39:36 -06:00
|
|
|
scroll.Y -= dot.Y - innerBounds.Max.Y + lineHeight
|
2023-09-09 18:22:10 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
this.ScrollTo(scroll)
|
|
|
|
}
|