package x import "image" import "image/color" import "golang.org/x/image/font" import "golang.org/x/image/math/fixed" 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 dotColor color.Color 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) 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) { 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) 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() } }