backend/internal/system/textbox.go
2024-07-27 13:47:22 -04:00

450 lines
12 KiB
Go

package system
import "image"
import "image/color"
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
contentBounds image.Rectangle
scroll image.Point
attrTextColor attrHierarchy[tomo.AttrTextColor]
attrDotColor attrHierarchy[tomo.AttrDotColor]
attrFace attrHierarchy[tomo.AttrFace]
attrWrap attrHierarchy[tomo.AttrWrap]
attrAlign attrHierarchy[tomo.AttrAlign]
attrOverflow attrHierarchy[tomo.AttrOverflow]
text string
runes []rune
selectable bool
selecting bool
selectStart int
dot text.Dot
drawer typeset.Drawer
on struct {
contentBoundsChange event.FuncBroadcaster
dotChange event.FuncBroadcaster
}
}
func (this *System) NewTextBox () tomo.TextBox {
box := &textBox { }
box.box = this.newBox(box)
return box
}
// ----- public methods ----------------------------------------------------- //
func (this *textBox) ContentBounds () image.Rectangle {
return this.contentBounds
}
func (this *textBox) ScrollTo (point image.Point) {
if this.scroll == point { return }
this.scroll = point
this.invalidateLayout()
}
func (this *textBox) RecommendedHeight (width int) int {
return this.drawer.ReccomendedHeightFor(width) + this.borderAndPaddingSum().Vertical()
}
func (this *textBox) RecommendedWidth (height int) int {
// TODO maybe not the best idea?
return this.minimumSize().X
}
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.runes = []rune(text)
this.drawer.SetText(this.runes)
this.invalidateMinimum()
this.invalidateLayout()
}
func (this *textBox) SetSelectable (selectable bool) {
if this.selectable == selectable { return }
this.selectable = selectable
}
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)
}
// ----- private methods ---------------------------------------------------- //
func (this *textBox) Draw (can canvas.Canvas) {
if can == nil { return }
texture := this.attrTexture.Value().Texture
col := this.attrColor.Value().Color
if col == nil { col = color.Transparent }
this.drawBorders(can)
pen := can.Pen()
pen.Fill(col)
pen.Texture(texture)
if this.transparent() && this.parent != nil {
this.parent.drawBackgroundPart(can)
}
pen.Rectangle(can.Bounds())
if this.selectable && this.Focused() {
this.drawDot(can)
}
if this.attrFace.Value().Face != nil {
textColor := this.attrTextColor.Value().Color
if textColor == nil { textColor = color.Black }
this.drawer.Draw(can, textColor, this.textOffset())
}
}
func (this *textBox) setAttr (attr tomo.Attr, user bool) {
switch attr := attr.(type) {
case tomo.AttrTextColor:
if this.attrTextColor.Set(attr, user) && !this.dot.Empty() {
this.invalidateDraw()
}
case tomo.AttrDotColor:
if this.attrDotColor.Set(attr, user) && !this.dot.Empty() {
this.invalidateDraw()
}
case tomo.AttrFace:
if this.attrFace.Set(attr, user) {
this.drawer.SetFace(attr.Face)
this.invalidateMinimum()
this.invalidateLayout()
}
case tomo.AttrWrap:
if this.attrWrap.Set(attr, user) {
this.drawer.SetWrap(bool(attr))
this.invalidateMinimum()
this.invalidateLayout()
}
case tomo.AttrAlign:
if this.attrAlign.Set(attr, user) {
this.drawer.SetAlign (
typeset.Align(attr.X),
typeset.Align(attr.Y))
this.invalidateDraw()
}
case tomo.AttrOverflow:
if this.attrOverflow.Set(attr, user) {
this.invalidateMinimum()
this.invalidateLayout()
}
default: this.box.setAttr(attr, user)
}
}
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.attrFace.Value().Face == nil { return }
face := this.attrFace.Value().Face
textColor := this.attrTextColor.Value().Color
dotColor := this.attrDotColor.Value().Color
if textColor == nil { textColor = color.Black }
if dotColor == nil { dotColor = color.RGBA { G: 255, B: 255, A: 255 } }
pen := can.Pen()
bounds := this.InnerBounds()
metrics := 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.Stroke(textColor)
pen.StrokeWeight(1)
pen.Path(roundPt(start.Add(ascent)), roundPt(start.Sub(descent)))
case start.Y == end.Y:
pen.Fill(dotColor)
pen.StrokeWeight(0)
pen.Rectangle(image.Rectangle {
Min: roundPt(start.Add(ascent)),
Max: roundPt(end.Sub(descent)),
})
default:
pen.Fill(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.
Add(this.scroll).
Sub(this.drawer.LayoutBoundsSpace().Min)
}
func (this *textBox) handleFocusEnter () {
this.invalidateDraw()
this.box.handleFocusEnter()
}
func (this *textBox) handleFocusLeave () {
this.invalidateDraw()
this.box.handleFocusLeave()
}
func (this *textBox) handleMouseDown (button input.Button) bool {
if this.mouseButtonCanDrag(button) {
index := this.runeUnderMouse()
this.selectStart = index
this.selecting = true
this.Select(text.Dot { Start: this.selectStart, End: index })
}
return this.box.handleMouseDown(button)
}
func (this *textBox) handleMouseUp (button input.Button) bool {
if this.mouseButtonCanDrag(button) && this.selecting {
index := this.runeUnderMouse()
this.selecting = false
this.Select(text.Dot { Start: this.selectStart, End: index })
}
return this.box.handleMouseUp(button)
}
func (this *textBox) mouseButtonCanDrag (button input.Button) bool {
return button == input.ButtonLeft ||
button == input.ButtonMiddle ||
button == input.ButtonRight
}
func (this *textBox) handleMouseMove () bool {
if this.selecting {
index := this.runeUnderMouse()
this.Select(text.Dot { Start: this.selectStart, End: index })
}
return this.box.handleMouseMove()
}
func (this *textBox) runeUnderMouse () int {
window := this.Window()
if window == nil { return 0 }
position := window.MousePosition().Sub(this.textOffset())
return this.drawer.AtPosition(fixPt(position))
}
func (this *textBox) handleKeyDown (key input.Key, numberPad bool) bool {
if this.box.handleKeyDown(key, numberPad) { return true }
if !this.selectable { return false }
// because fuck you thats why!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
modifiers := this.Window().Modifiers()
dot := this.Dot()
sel := modifiers.Shift
word := modifiers.Control
switch {
case key == input.KeyHome || (modifiers.Alt && key == input.KeyLeft):
dot.End = 0
if !sel { dot.Start = dot.End }
this.Select(dot)
return true
case key == input.KeyEnd || (modifiers.Alt && key == input.KeyRight):
dot.End = len(this.text)
if !sel { dot.Start = dot.End }
this.Select(dot)
return true
case key == input.KeyLeft:
if sel {
this.Select(text.SelectLeft(this.runes, dot, word))
} else {
this.Select(text.MoveLeft(this.runes, dot, word))
}
return true
case key == input.KeyRight:
if sel {
this.Select(text.SelectRight(this.runes, dot, word))
} else {
this.Select(text.MoveRight(this.runes, dot, word))
}
return true
case key == input.Key('a') && modifiers.Control:
dot.Start = 0
dot.End = len(this.text)
this.Select(dot)
return true
default:
return false
}
}
func (this *textBox) handleKeyUp (key input.Key, numberPad bool) bool {
if this.box.handleKeyUp(key, numberPad) { return true }
if !this.selectable { return false }
modifiers := this.Window().Modifiers()
switch {
case key == input.KeyHome || (modifiers.Alt && key == input.KeyLeft):
return true
case key == input.KeyEnd || (modifiers.Alt && key == input.KeyRight):
return true
case key == input.KeyLeft:
return true
case key == input.KeyRight:
return true
case key == input.Key('a') && modifiers.Control:
return true
default:
return false
}
}
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()
if this.attrOverflow.Value().X || bool(this.attrWrap.Value()) {
minimum.X = this.drawer.Em().Round()
}
if this.attrOverflow.Value().Y {
minimum.Y = this.drawer.LineHeight().Round()
}
return minimum.Add(this.box.contentMinimum())
}
func (this *textBox) doLayout () {
this.box.doLayout()
previousContentBounds := this.contentBounds
innerBounds := this.InnerBounds()
this.drawer.SetWidth(innerBounds.Dx())
this.drawer.SetHeight(innerBounds.Dy())
this.contentBounds = this.normalizedLayoutBoundsSpace()
this.constrainScroll()
this.contentBounds = this.contentBounds.Add(this.scroll)
// println(this.InnerBounds().String(), this.contentBounds.String())
if previousContentBounds != this.contentBounds {
this.on.contentBoundsChange.Broadcast()
}
}
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 < innerBounds.Dx() - width {
this.scroll.X = innerBounds.Dx() - width
}
// Y
if height <= innerBounds.Dy() {
this.scroll.Y = 0
} else if this.scroll.Y > 0 {
this.scroll.Y = 0
} else if this.scroll.Y < innerBounds.Dy() - height {
this.scroll.Y = innerBounds.Dy() - height
}
}
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)
}