From 069b8898f3aa4b08694bf470add1f5c581ebbf20 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 12 Jul 2023 01:17:12 -0400 Subject: [PATCH] Add partial support for text boxes --- box.go | 15 ++++-- containerbox.go | 15 ++++-- go.mod | 3 +- go.sum | 39 +++------------ textbox.go | 126 +++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 155 insertions(+), 43 deletions(-) diff --git a/box.go b/box.go index 6e39466..7277f1b 100644 --- a/box.go +++ b/box.go @@ -67,7 +67,11 @@ func (this *box) Bounds () image.Rectangle { } func (this *box) InnerBounds () image.Rectangle { - innerBounds := this.padding.Apply(this.bounds) + return this.padding.Apply(this.innerClippingBounds()) +} + +func (this *box) innerClippingBounds () image.Rectangle { + innerBounds := this.bounds for _, border := range this.border { innerBounds = border.Width.Apply(innerBounds) } @@ -97,9 +101,8 @@ func (this *box) SetMinimumSize (width, height int) { this.minSize = minSize if this.bounds.Dx() < width || this.bounds.Dy() < height { - this.invalidateLayout() + // TODO: alert the parent } - // TODO: alert the parent } func (this *box) SetPadding (padding tomo.Inset) { @@ -187,13 +190,14 @@ func (this *box) OnKeyUp (callback func(key input.Key, numberPad bool)) event.Co // -------------------------------------------------------------------------- // func (this *box) Draw (can canvas.Canvas) { - this.drawBorders(can) + if can == nil { return } pen := can.Pen() pen.Fill(this.color) pen.Rectangle(this.bounds) } func (this *box) drawBorders (can canvas.Canvas) { + if can == nil { return } pen := can.Pen() bounds := this.bounds for _, border := range this.border { @@ -229,7 +233,8 @@ func (this *box) drawBorders (can canvas.Canvas) { func (this *box) doDraw () { if this.canvas == nil { return } if this.drawer != nil { - this.drawer.Draw(this.canvas) + this.drawBorders(this.canvas) + this.drawer.Draw(this.canvas.Clip(this.innerClippingBounds())) } } diff --git a/containerbox.go b/containerbox.go index 34de3dd..8a737dc 100644 --- a/containerbox.go +++ b/containerbox.go @@ -59,6 +59,7 @@ func (this *containerBox) SetGap (gap image.Point) { if this.gap == gap { return } this.gap = gap this.invalidateLayout() + this.recalculateMinimumSize() } func (this *containerBox) Add (child tomo.Object) { @@ -68,6 +69,7 @@ func (this *containerBox) Add (child tomo.Object) { box.setParent(this) this.children = append(this.children, box) this.invalidateLayout() + this.recalculateMinimumSize() } func (this *containerBox) Delete (child tomo.Object) { @@ -78,6 +80,7 @@ func (this *containerBox) Delete (child tomo.Object) { box.setParent(nil) this.children = remove(this.children, index) this.invalidateLayout() + this.recalculateMinimumSize() } func (this *containerBox) Insert (child, before tomo.Object) { @@ -91,6 +94,7 @@ func (this *containerBox) Insert (child, before tomo.Object) { box.setParent(this) this.children = insert(this.children, index, box) this.invalidateLayout() + this.recalculateMinimumSize() } func (this *containerBox) Clear () { @@ -99,6 +103,7 @@ func (this *containerBox) Clear () { } this.children = nil this.invalidateLayout() + this.recalculateMinimumSize() } func (this *containerBox) Length () int { @@ -115,9 +120,11 @@ func (this *containerBox) At (index int) tomo.Object { func (this *containerBox) SetLayout (layout tomo.Layout) { this.layout = layout this.invalidateLayout() + this.recalculateMinimumSize() } func (this *containerBox) Draw (can canvas.Canvas) { + if can == nil { return } this.drawBorders(can) pen := can.Pen() pen.Fill(this.color) @@ -127,8 +134,6 @@ func (this *containerBox) Draw (can canvas.Canvas) { for index, box := range this.children { rocks[index] = box.Bounds() } - // TODO: use shatter algorithm here to optimize amount of pixels drawn - // and not draw over child boxes for _, tile := range canvas.Shatter(this.bounds, rocks...) { pen.Rectangle(tile) } @@ -153,6 +158,10 @@ func (this *containerBox) layoutHints () tomo.LayoutHints { } } +func (this *containerBox) recalculateMinimumSize () { + // TODO calculate minimum size and use SetMinimumSize +} + func (this *containerBox) doLayout () { this.box.doLayout() // TODO: possibly store all children as tomo.Box-es and don't allocate a @@ -163,8 +172,6 @@ func (this *containerBox) doLayout () { boxes[index] = box } if this.layout != nil { - // TODO maybe we should pass more information into Arrange such - // as overflow information and scroll this.layout.Arrange(this.layoutHints(), boxes) } if previousContentBounds != this.contentBounds { diff --git a/go.mod b/go.mod index 63b63d8..80553ad 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,8 @@ go 1.20 require ( git.tebibyte.media/tomo/ggfx v0.4.0 - git.tebibyte.media/tomo/tomo v0.10.0 + git.tebibyte.media/tomo/tomo v0.11.0 + git.tebibyte.media/tomo/typeset v0.3.0 git.tebibyte.media/tomo/xgbkb v1.0.1 github.com/jezek/xgb v1.1.0 github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 diff --git a/go.sum b/go.sum index f9da488..f719bd6 100644 --- a/go.sum +++ b/go.sum @@ -1,34 +1,14 @@ git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q= -git.tebibyte.media/tomo/ggfx v0.2.0 h1:TSWfNQgnnHewwHiGC3VPFssdOIYCfgqCcOiPX4Sgv00= -git.tebibyte.media/tomo/ggfx v0.2.0/go.mod h1:zPoz8BdVQyG2KhEmeGFQBK66V71i6Kj8oVFbrZaCwRA= -git.tebibyte.media/tomo/ggfx v0.3.0 h1:h+RfairZTt4jT76KwmJN8OcdU7Ew0vFRqMZFqz3iHaE= -git.tebibyte.media/tomo/ggfx v0.3.0/go.mod h1:zPoz8BdVQyG2KhEmeGFQBK66V71i6Kj8oVFbrZaCwRA= git.tebibyte.media/tomo/ggfx v0.4.0 h1:3aUHeGS/yYWRV/zCDubBsXnik5ygkMnj/VgrM5Z75A4= git.tebibyte.media/tomo/ggfx v0.4.0/go.mod h1:zPoz8BdVQyG2KhEmeGFQBK66V71i6Kj8oVFbrZaCwRA= -git.tebibyte.media/tomo/tomo v0.4.0 h1:nraUtsmYLSe8BZOolmeBuD+aaMk4duSxI84RqnzflCs= -git.tebibyte.media/tomo/tomo v0.4.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= -git.tebibyte.media/tomo/tomo v0.5.0 h1:bfHNExPewlt+n7nq8LvNiAbemqSllrCY/tAI08r8sAo= -git.tebibyte.media/tomo/tomo v0.5.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= -git.tebibyte.media/tomo/tomo v0.5.1 h1:APOTY+YSV8JJwNmJsKFYzBYLPUy3DqNr49rrSspOKZ8= -git.tebibyte.media/tomo/tomo v0.5.1/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= -git.tebibyte.media/tomo/tomo v0.6.0 h1:/gjY6neXEqyKQ2Ye05mZi3yIOvsRVyIKSddvCySGN2Y= -git.tebibyte.media/tomo/tomo v0.6.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= -git.tebibyte.media/tomo/tomo v0.6.1 h1:XdtHfF2xhz9pZXqyrwSsPaore/8PHVqFrnT4NwlBOhY= -git.tebibyte.media/tomo/tomo v0.6.1/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= -git.tebibyte.media/tomo/tomo v0.7.0 h1:dUYBB/gZzmkiKR8Cq/nmEQGwMqVE01CnQFtvjmInif0= -git.tebibyte.media/tomo/tomo v0.7.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= -git.tebibyte.media/tomo/tomo v0.7.1 h1:CHOBGel7Acp88cVW+5SEIx41cRdwuuP/niSSp9/CRRg= -git.tebibyte.media/tomo/tomo v0.7.1/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= -git.tebibyte.media/tomo/tomo v0.7.2 h1:15dMJm4Sm339b23o9RZSq87u99SaF2q+b5CRB5P58fA= -git.tebibyte.media/tomo/tomo v0.7.2/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= -git.tebibyte.media/tomo/tomo v0.7.3 h1:eHwuYKe+0nLWoEfPZid8njirxmWY3dFmdY+PsPp1RN0= -git.tebibyte.media/tomo/tomo v0.7.3/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= -git.tebibyte.media/tomo/tomo v0.8.0 h1:Sqvos2Huf0mSHFZ0FJrBZiH8Ro/gmQPHCvK6Qr29SBo= -git.tebibyte.media/tomo/tomo v0.8.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= -git.tebibyte.media/tomo/tomo v0.9.0 h1:Ow7LaOwPTNogkREDVbxsx827XcyHKzXq3dFSM0TttC4= -git.tebibyte.media/tomo/tomo v0.9.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= -git.tebibyte.media/tomo/tomo v0.10.0 h1:SFX4JQt1KgWeX9RnYoUQRj7MyFyb1ld8uDPHFTU2IKU= -git.tebibyte.media/tomo/tomo v0.10.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= +git.tebibyte.media/tomo/tomo v0.11.0 h1:Gh9c/6rDqvhxt/DaNQHYNUfdRmSQTuz9T3F+pb5W6BI= +git.tebibyte.media/tomo/tomo v0.11.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= +git.tebibyte.media/tomo/typeset v0.1.0 h1:ZLwQzy51vUskjg1nB4Emjag8VXn3ki2jEkE19kwVQ4c= +git.tebibyte.media/tomo/typeset v0.1.0/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g= +git.tebibyte.media/tomo/typeset v0.2.0 h1:7DcnB0sW12eL+MxkEMv99eVG2IQxsZHDDK6pz6VE1O8= +git.tebibyte.media/tomo/typeset v0.2.0/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g= +git.tebibyte.media/tomo/typeset v0.3.0 h1:9koJzy0bguBHjlesrHpXK8odIVEMmQRBIFIRXDhv7Bk= +git.tebibyte.media/tomo/typeset v0.3.0/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g= git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE= git.tebibyte.media/tomo/xgbkb v1.0.1/go.mod h1:P5Du0yo5hUsojchW08t+Mds0XPIJXwMi733ZfklzjRw= github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA= @@ -42,8 +22,6 @@ github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0/go.mod h1:AHecLyFNy6 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/image v0.8.0 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg= -golang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM= golang.org/x/image v0.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g= golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -68,7 +46,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/textbox.go b/textbox.go index 92572c6..c6dbfab 100644 --- a/textbox.go +++ b/textbox.go @@ -1,8 +1,130 @@ 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/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 + hAlign tomo.Align + + drawer typeset.Drawer + + on struct { + contentBoundsChange event.FuncBroadcaster + } +} func (backend *Backend) NewTextBox() tomo.TextBox { - // TODO - return nil + 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) SetHAlign (align tomo.Align) { + if this.hAlign == align { return } + this.hAlign = align + + switch align { + 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) SetVAlign (align tomo.Align) { + // TODO +} + +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.face == nil { return } + offset := this.InnerBounds().Min. + Sub(this.scroll). + Sub(this.drawer.LayoutBoundsSpace().Min) + this.drawer.Draw(can, this.textColor, offset) +} + +func (this *textBox) recalculateMinimumSize () { + // TODO calculate minimum size and use SetMinimumSize +} + +func (this *textBox) doLayout () { + this.box.doLayout() + previousContentBounds := this.contentBounds + + this.contentBounds = this.drawer.LayoutBoundsSpace() + this.contentBounds = this.contentBounds.Sub(this.contentBounds.Min) + this.contentBounds = this.contentBounds.Sub(this.scroll) + + if previousContentBounds != this.contentBounds { + this.on.contentBoundsChange.Broadcast() + } }