package system import "image" import "image/color" 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 contentBounds image.Rectangle scroll image.Point attrTextColor attrHierarchy[tomo.AttrTextColor] attrDotColor attrHierarchy[tomo.AttrDotColor] attrFace attrHierarchy[tomo.AttrFace] attrWrap attrHierarchy[tomo.AttrWrap] attrAlign attrHierarchy[tomo.AttrAlign] attrOverflow attrHierarchy[tomo.AttrOverflow] text string selectable bool selecting bool selectStart int dot text.Dot drawer typeset.Drawer on struct { contentBoundsChange event.FuncBroadcaster dotChange event.FuncBroadcaster } } func (this *System) NewTextBox () tomo.TextBox { box := &textBox { } box.box = this.newBox(box) return box } // ----- public methods ----------------------------------------------------- // 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) RecommendedHeight (width int) int { return this.drawer.ReccomendedHeightFor(width) + this.borderAndPaddingSum().Vertical() } func (this *textBox) RecommendedWidth (height int) int { // TODO maybe not the best idea? return this.minimumSize().X } 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) 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.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) } // ----- private methods ---------------------------------------------------- // func (this *textBox) Draw (can canvas.Canvas) { if can == nil { return } texture := this.attrTexture.Value().Texture col := this.attrColor.Value().Color if col == nil { col = color.Transparent } this.drawBorders(can) pen := can.Pen() pen.Fill(col) pen.Texture(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.attrFace.Value().Face != nil { textColor := this.attrTextColor.Value().Color if textColor == nil { textColor = color.Black } this.drawer.Draw(can, textColor, this.textOffset()) } } func (this *textBox) setAttr (attr tomo.Attr, user bool) { switch attr := attr.(type) { case tomo.AttrTextColor: if this.attrTextColor.Set(attr, user) && !this.dot.Empty() { this.invalidateDraw() } case tomo.AttrDotColor: if this.attrDotColor.Set(attr, user) && !this.dot.Empty() { this.invalidateDraw() } case tomo.AttrFace: if this.attrFace.Set(attr, user) { this.drawer.SetFace(attr.Face) this.invalidateMinimum() this.invalidateLayout() } case tomo.AttrWrap: if this.attrWrap.Set(attr, user) { this.invalidateMinimum() this.invalidateLayout() } case tomo.AttrAlign: if this.attrAlign.Set(attr, user) { this.invalidateDraw() } case tomo.AttrOverflow: if this.attrOverflow.Set(attr, user) { this.invalidateLayout() } default: this.box.setAttr(attr, user) } } 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.attrFace.Value().Face == nil { return } face := this.attrFace.Value().Face textColor := this.attrTextColor.Value().Color dotColor := this.attrDotColor.Value().Color if textColor == nil { textColor = color.Transparent } if dotColor == nil { dotColor = color.RGBA { G: 255, B: 255, A: 255 } } pen := can.Pen() pen.Fill(color.Transparent) pen.Stroke(textColor) bounds := this.InnerBounds() metrics := 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(dotColor) pen.StrokeWeight(0) pen.Rectangle(image.Rectangle { Min: roundPt(start.Add(ascent)), Max: roundPt(end.Sub(descent)), }) default: pen.Fill(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) bool { if button == input.ButtonLeft { index := this.runeUnderMouse() this.selectStart = index this.selecting = true this.Select(text.Dot { Start: this.selectStart, End: index }) } return this.box.handleMouseDown(button) } func (this *textBox) handleMouseUp (button input.Button) bool { if button == input.ButtonLeft && this.selecting { index := this.runeUnderMouse() this.selecting = false this.Select(text.Dot { Start: this.selectStart, End: index }) } return this.box.handleMouseUp(button) } func (this *textBox) handleMouseMove () bool { if this.selecting { index := this.runeUnderMouse() this.Select(text.Dot { Start: this.selectStart, End: index }) } return this.box.handleMouseMove() } func (this *textBox) runeUnderMouse () int { window := this.Window() if window == nil { return 0 } position := window.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.attrOverflow.Value().X || bool(this.attrWrap.Value()) { minimum.X = this.drawer.Em().Round() } if this.attrOverflow.Value().Y { 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) }