Sasha Koshka
ff8875535d
Boxes that need their minimum size to be updated now use a map like for layout and drawing. Size set with MinimumSize is now treated as separate from the content size and the larger size is used.
302 lines
7.2 KiB
Go
302 lines
7.2 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 {
|
|
this := &textBox {
|
|
textColor: color.Black,
|
|
dotColor: color.RGBA { B: 255, G: 255, A: 255 },
|
|
}
|
|
this.box = backend.newBox(this)
|
|
return this
|
|
}
|
|
|
|
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.invalidateMinimum()
|
|
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()
|
|
this.invalidateLayout()
|
|
}
|
|
|
|
func (this *textBox) SetWrap (wrap bool) {
|
|
if this.wrap == wrap { return }
|
|
this.drawer.SetWrap(wrap)
|
|
this.invalidateMinimum()
|
|
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) contentMinimum () image.Point {
|
|
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
|
|
}
|
|
|
|
return minimum.Add(this.box.contentMinimum())
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|