221 lines
5.1 KiB
Go
221 lines
5.1 KiB
Go
package x
|
|
|
|
import "image"
|
|
import "image/color"
|
|
import "golang.org/x/image/font"
|
|
import "git.tebibyte.media/tomo/tomo"
|
|
import "git.tebibyte.media/tomo/typeset"
|
|
import "git.tebibyte.media/tomo/tomo/text"
|
|
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
|
|
dot text.Dot
|
|
|
|
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,
|
|
}
|
|
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) 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 (this *textBox) drawDot (can canvas.Canvas) {
|
|
pen := can.Pen()
|
|
pen.Fill(color.Transparent)
|
|
pen.Stroke(this.textColor)
|
|
pen.StrokeWeight(1)
|
|
|
|
// TODO draw dot
|
|
if this.dot.Empty() {
|
|
metrics := this.face.Metrics()
|
|
position := this.drawer.PositionAt(this.dot.Start)
|
|
roundPos :=
|
|
image.Pt(position.X.Round(), position.Y.Round()).
|
|
Add(this.textOffset())
|
|
pen.Path (
|
|
roundPos.Add(image.Pt(0, metrics.Descent.Round())),
|
|
roundPos.Sub(image.Pt(0, metrics.Ascent.Round())))
|
|
} else {
|
|
|
|
}
|
|
}
|
|
|
|
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) 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()
|
|
}
|
|
}
|