package system 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 vAlign 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 (this *System) NewTextBox () tomo.TextBox { box := &textBox { textColor: color.Black, dotColor: color.RGBA { B: 255, G: 255, A: 255 }, } box.box = this.newBox(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) { if this.scroll == point { return } 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.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) } func (this *textBox) SetAlign (x, y tomo.Align) { if this.hAlign == x && this.vAlign == y { return } this.hAlign = x this.vAlign = y this.drawer.SetAlign(typeset.Align(x), typeset.Align(y)) this.invalidateDraw() } func (this *textBox) Draw (can canvas.Canvas) { if can == nil { return } this.drawBorders(can) pen := can.Pen() pen.Fill(this.color) pen.Texture(this.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.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) 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.StrokeWeight(1) 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. Add(this.scroll). Sub(this.drawer.LayoutBoundsSpace().Min) } func (this *textBox) handleFocusLeave () { 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 := this.drawer.MinimumSize() if this.hOverflow || this.wrap { minimum.X = this.drawer.Em().Round() } if this.vOverflow { 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) }