This repository has been archived on 2024-06-03. You can view files and clone it, but cannot push or open issues or pull requests.
x/textbox.go

271 lines
6.4 KiB
Go
Raw 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-08-05 01:59:52 +00:00
import "golang.org/x/image/math/fixed"
2023-07-02 06:52:14 +00:00
import "git.tebibyte.media/tomo/tomo"
2023-07-12 05:17:12 +00:00
import "git.tebibyte.media/tomo/typeset"
import "git.tebibyte.media/tomo/tomo/text"
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
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
selectable bool
dot text.Dot
2023-08-05 01:59:52 +00:00
dotColor color.Color
2023-07-12 05:17:12 +00:00
drawer typeset.Drawer
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 {
2023-07-12 05:17:12 +00:00
box := &textBox {
box: backend.NewBox().(*box),
textColor: color.Black,
}
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()
}
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)
2023-07-13 16:48:09 +00:00
this.recalculateMinimumSize()
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.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) {
if this.hAlign == x { return }
this.hAlign = x
switch x {
2023-07-12 05:17:12 +00:00
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)
}
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) {
pen := can.Pen()
pen.Fill(color.Transparent)
pen.Stroke(this.textColor)
pen.StrokeWeight(1)
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.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)
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
}
2023-07-13 16:48:09 +00:00
func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
bounds := this.drawer.LayoutBoundsSpace()
return bounds.Sub(bounds.Min)
}
2023-07-12 05:17:12 +00:00
func (this *textBox) recalculateMinimumSize () {
2023-07-13 16:48:09 +00:00
minimum := image.Pt (
this.drawer.Em().Round(),
this.drawer.LineHeight().Round())
2023-07-16 04:42:47 +00:00
textSize := this.drawer.MinimumSize()
2023-07-13 16:48:09 +00:00
if !this.hOverflow && !this.wrap {
2023-07-16 04:42:47 +00:00
minimum.X = textSize.X
2023-07-13 16:48:09 +00:00
}
if !this.vOverflow {
2023-07-16 04:42:47 +00:00
minimum.Y = textSize.Y
2023-07-13 16:48:09 +00:00
}
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
this.SetMinimumSize(minimum)
2023-07-12 05:17:12 +00:00
}
func (this *textBox) doLayout () {
this.box.doLayout()
previousContentBounds := this.contentBounds
innerBounds := this.InnerBounds()
2023-07-16 04:42:47 +00:00
this.drawer.SetMaxWidth(innerBounds.Dx())
this.drawer.SetMaxHeight(innerBounds.Dy())
2023-07-12 05:17:12 +00:00
2023-07-13 16:48:09 +00:00
this.contentBounds = this.normalizedLayoutBoundsSpace().Sub(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
}