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 { box := &textBox { box: backend.NewBox().(*box), textColor: color.Black, dotColor: color.RGBA { B: 255, G: 255, A: 255 }, } 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) { 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) 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() } }