M go.mod => go.mod +4 -4
@@ 3,13 3,14 @@ module gioui.org/x
go 1.18
require (
- gioui.org v0.0.0-20220830130127-276b7eefdd65
+ gioui.org v0.0.0-20221216233230-5d1d1df2061c
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0
github.com/andybalholm/stroke v0.0.0-20220316233208-2609e58d58a5
github.com/esiqveland/notify v0.11.0
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/godbus/dbus/v5 v5.0.6
github.com/yuin/goldmark v1.4.0
+ golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64
@@ 19,8 20,7 @@ require (
require (
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect
gioui.org/shader v1.0.6 // indirect
- github.com/benoitkugler/textlayout v0.1.3 // indirect
- github.com/gioui/uax v0.2.1-0.20220819135011-cda973fac06d // indirect
- github.com/go-text/typesetting v0.0.0-20220411150340-35994bc27a7b // indirect
+ github.com/benoitkugler/textlayout v0.3.0 // indirect
+ github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
)
M go.sum => go.sum +10 -10
@@ 1,6 1,6 @@
eliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3 h1:djFprmHZgrSepsHAIRMp5UJn3PzsoTg9drI+BDmif5Q=
-gioui.org v0.0.0-20220830130127-276b7eefdd65 h1:mX+A86TwTyHZNqDxekUukiAmtYNUOq4CnrRZHxUrlo8=
-gioui.org v0.0.0-20220830130127-276b7eefdd65/go.mod h1:GN091SCcGAfHfQiSOetXx7Abdy+8nmONj0ZN63Xxf7w=
+gioui.org v0.0.0-20221216233230-5d1d1df2061c h1:5sweSGj5cjHLJgK4cGI5wNhghp/+JQF+MM0oen8HQA8=
+gioui.org v0.0.0-20221216233230-5d1d1df2061c/go.mod h1:3lLo7xMHYnnHTrgKNNctBjEKKH3wQCO2Sn7ti5Jy8mU=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
@@ 11,16 11,14 @@ git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73
github.com/andybalholm/stroke v0.0.0-20220316233208-2609e58d58a5 h1:xxGjYr3zFgmxumkDiXyxiDD1Pdv1kmO0hewvTEOMlzQ=
github.com/andybalholm/stroke v0.0.0-20220316233208-2609e58d58a5/go.mod h1:ccdDYaY5+gO+cbnQdFxEXqfy0RkoV25H3jLXUDNM3wg=
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
-github.com/benoitkugler/textlayout v0.0.5/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8=
-github.com/benoitkugler/textlayout v0.1.3 h1:Jv0E28xDkke3KrWle90yOLtBmZsUqXLBy70lZRfbKN0=
-github.com/benoitkugler/textlayout v0.1.3/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
+github.com/benoitkugler/textlayout v0.3.0 h1:2ehWXEkgb6RUokTjXh1LzdGwG4dRP6X3dqhYYDYhUVk=
+github.com/benoitkugler/textlayout v0.3.0/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
github.com/benoitkugler/textlayout-testdata v0.1.1 h1:AvFxBxpfrQd8v55qH59mZOJOQjtD6K2SFe9/HvnIbJk=
+github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo=
github.com/esiqveland/notify v0.11.0 h1:0WJ/xW+3Ln8uRBYntG7f0XihXxnlOaQTdha1yyzXz30=
github.com/esiqveland/notify v0.11.0/go.mod h1:63UbVSaeJwF0LVJARHFuPgUAoM7o1BEvCZyknsuonBc=
-github.com/gioui/uax v0.2.1-0.20220819135011-cda973fac06d h1:ro1W5kY1pVBLHy4GokZUfr9cl7ewZhAiT5WsXqFDYE4=
-github.com/gioui/uax v0.2.1-0.20220819135011-cda973fac06d/go.mod h1:b6uGh9ySJPVQG/RdiI88bE5sUGDk6vzzRujv1BAeuJc=
-github.com/go-text/typesetting v0.0.0-20220411150340-35994bc27a7b h1:WINlj3ANt+CVrO2B4NGDHRlPvEWZPxjhb7z+JKypwXI=
-github.com/go-text/typesetting v0.0.0-20220411150340-35994bc27a7b/go.mod h1:ZNYu5saGoMOqtkVH5T8onTwhzenDUVszI+5WFHJRaxQ=
+github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5 h1:iOA0HmtpANn48hX2nlDNMu0VVaNza35HJG0WeetBVzQ=
+github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5/go.mod h1:/cmOXaoTiO+lbCwkTZBgCvevJpbFsZ5reXIpEJVh5MI=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ 30,10 28,12 @@ github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/yuin/goldmark v1.4.0 h1:OtISOGfH6sOWa1/qXqqAiOIAO6Z5J3AEAE18WAq6BiQ=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg=
+golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0=
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8=
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
M richtext/richtext.go => richtext/richtext.go +2 -2
@@ 184,11 184,11 @@ type TextStyle struct {
State *InteractiveText
Styles []SpanStyle
Alignment text.Alignment
- text.Shaper
+ *text.Shaper
}
// Text constructs a TextStyle.
-func Text(state *InteractiveText, shaper text.Shaper, styles ...SpanStyle) TextStyle {
+func Text(state *InteractiveText, shaper *text.Shaper, styles ...SpanStyle) TextStyle {
return TextStyle{
State: state,
Styles: styles,
A styledtext/iterator.go => styledtext/iterator.go +141 -0
@@ 0,0 1,141 @@
+package styledtext
+
+import (
+ "image"
+
+ "gioui.org/layout"
+ "gioui.org/op"
+ "gioui.org/op/clip"
+ "gioui.org/op/paint"
+ "gioui.org/text"
+ "golang.org/x/exp/constraints"
+ "golang.org/x/image/math/fixed"
+)
+
+// textIterator computes the bounding box of and paints text. This iterator is
+// specialized to laying out single lines of text.
+type textIterator struct {
+ // viewport is the rectangle of document coordinates that the iterator is
+ // trying to fill with text.
+ viewport image.Rectangle
+ // maxLines tracks the maximum allowed number of glyphs with FlagLineBreak.
+ maxLines int
+
+ // linesSeen tracks the number of FlagLineBreak glyphs we have seen.
+ linesSeen int
+ // init tracks whether the iterator has processed any glyphs.
+ init bool
+ // firstX tracks the x offset of the first processed glyph. This is subtracted
+ // from all glyph x offsets in order to ensure that the text is rendered at
+ // x=0.
+ firstX fixed.Int26_6
+ // hasNewline tracks whether the processed glyphs contained a synthetic newline
+ // character.
+ hasNewline bool
+ // lineOff tracks the origin for the glyphs in the current line.
+ lineOff image.Point
+ // padding is the space needed outside of the bounds of the text to ensure no
+ // part of a glyph is clipped.
+ padding image.Rectangle
+ // bounds is the logical bounding box of the text.
+ bounds image.Rectangle
+ // runes is the count of runes represented by the processed glyphs.
+ runes int
+ // visible tracks whether the most recently iterated glyph is visible within
+ // the viewport.
+ visible bool
+ // first tracks whether the iterator has processed a glyph yet.
+ first bool
+ // baseline tracks the location of the first line of text's baseline.
+ baseline int
+}
+
+// processGlyph checks whether the glyph is visible within the iterator's configured
+// viewport and (if so) updates the iterator's text dimensions to include the glyph.
+func (it *textIterator) processGlyph(g text.Glyph, ok bool) (_ text.Glyph, visibleOrBefore bool) {
+ it.runes += int(g.Runes)
+ it.hasNewline = it.hasNewline || (g.Flags&text.FlagLineBreak > 0 && g.Flags&text.FlagParagraphBreak > 0)
+ if it.maxLines > 0 {
+ if g.Flags&text.FlagLineBreak != 0 {
+ it.linesSeen++
+ }
+ if it.linesSeen == it.maxLines && g.Flags&text.FlagParagraphBreak != 0 {
+ return g, false
+ }
+ }
+ // Compute the maximum extent to which glyphs overhang on the horizontal
+ // axis.
+ if d := g.Bounds.Min.X.Floor(); d < it.padding.Min.X {
+ it.padding.Min.X = d
+ }
+ if d := (g.Bounds.Max.X - g.Advance).Ceil(); d > it.padding.Max.X {
+ it.padding.Max.X = d
+ }
+ logicalBounds := image.Rectangle{
+ Min: image.Pt(g.X.Floor(), int(g.Y)-g.Ascent.Ceil()),
+ Max: image.Pt((g.X + g.Advance).Ceil(), int(g.Y)+g.Descent.Ceil()),
+ }
+ if !it.first {
+ it.first = true
+ it.baseline = int(g.Y)
+ it.bounds = logicalBounds
+ }
+
+ above := logicalBounds.Max.Y < it.viewport.Min.Y
+ below := logicalBounds.Min.Y > it.viewport.Max.Y
+ left := logicalBounds.Max.X < it.viewport.Min.X
+ right := logicalBounds.Min.X > it.viewport.Max.X
+ it.visible = !above && !below && !left && !right
+ if it.visible {
+ it.bounds.Min.X = min(it.bounds.Min.X, logicalBounds.Min.X)
+ it.bounds.Min.Y = min(it.bounds.Min.Y, logicalBounds.Min.Y)
+ it.bounds.Max.X = max(it.bounds.Max.X, logicalBounds.Max.X)
+ it.bounds.Max.Y = max(it.bounds.Max.Y, logicalBounds.Max.Y)
+ }
+ return g, ok && !below
+
+}
+
+func min[T constraints.Ordered](a, b T) T {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+func max[T constraints.Ordered](a, b T) T {
+ if a > b {
+ return a
+ }
+ return b
+}
+
+// paintGlyph buffers up and paints text glyphs. It should be invoked iteratively upon each glyph
+// until it returns false. The line parameter should be a slice with
+// a backing array of sufficient size to buffer multiple glyphs.
+// A modified slice will be returned with each invocation, and is
+// expected to be passed back in on the following invocation.
+// This design is awkward, but prevents the line slice from escaping
+// to the heap.
+func (it *textIterator) paintGlyph(gtx layout.Context, shaper *text.Shaper, glyph text.Glyph, line []text.Glyph) ([]text.Glyph, bool) {
+ _, visibleOrBefore := it.processGlyph(glyph, true)
+ if it.visible {
+ if !it.init {
+ it.firstX = glyph.X
+ it.init = true
+ }
+ if len(line) == 0 {
+ it.lineOff = image.Point{X: (glyph.X - it.firstX).Floor(), Y: int(glyph.Y)}.Sub(it.viewport.Min)
+ }
+ line = append(line, glyph)
+ }
+ if glyph.Flags&text.FlagLineBreak > 0 || cap(line)-len(line) == 0 || !visibleOrBefore {
+ t := op.Offset(it.lineOff).Push(gtx.Ops)
+ op := clip.Outline{Path: shaper.Shape(line)}.Op().Push(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ op.Pop()
+ t.Pop()
+ line = line[:0]
+ }
+ return line, visibleOrBefore
+}
M styledtext/styledtext.go => styledtext/styledtext.go +42 -24
@@ 8,7 8,6 @@ import (
"gioui.org/layout"
"gioui.org/op"
- "gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
@@ 28,17 27,16 @@ type SpanStyle struct {
// spanShape describes the text shaping of a single span.
type spanShape struct {
offset image.Point
- layout text.Layout
+ call op.CallOp
size image.Point
ascent int
}
// Layout renders the span using the provided text shaping.
-func (ss SpanStyle) Layout(gtx layout.Context, s text.Shaper, shape spanShape) layout.Dimensions {
+func (ss SpanStyle) Layout(gtx layout.Context, shape spanShape) layout.Dimensions {
paint.ColorOp{Color: ss.Color}.Add(gtx.Ops)
defer op.Offset(shape.offset).Push(gtx.Ops).Pop()
- defer clip.Outline{Path: s.Shape(ss.Font, fixed.I(gtx.Sp(ss.Size)), shape.layout)}.Op().Push(gtx.Ops).Pop()
- paint.PaintOp{}.Add(gtx.Ops)
+ shape.call.Add(gtx.Ops)
return layout.Dimensions{Size: shape.size}
}
@@ 46,11 44,11 @@ func (ss SpanStyle) Layout(gtx layout.Context, s text.Shaper, shape spanShape) l
type TextStyle struct {
Styles []SpanStyle
Alignment text.Alignment
- text.Shaper
+ *text.Shaper
}
// Text constructs a TextStyle.
-func Text(shaper text.Shaper, styles ...SpanStyle) TextStyle {
+func Text(shaper *text.Shaper, styles ...SpanStyle) TextStyle {
return TextStyle{
Styles: styles,
Shaper: shaper,
@@ 80,6 78,7 @@ func (t TextStyle) Layout(gtx layout.Context, spanFn func(gtx layout.Context, id
overallSize image.Point
lineShapes []spanShape
lineStartIndex int
+ glyphs [32]text.Glyph
)
for i := 0; i < len(spans); i++ {
@@ 90,13 89,33 @@ func (t TextStyle) Layout(gtx layout.Context, spanFn func(gtx layout.Context, id
maxWidth := gtx.Constraints.Max.X - lineDims.X
// shape the text of the current span
- lines := t.Shaper.LayoutString(span.Font, fixed.I(gtx.Sp(span.Size)), maxWidth, gtx.Locale, span.Content)
+ macro := op.Record(gtx.Ops)
+ paint.ColorOp{Color: span.Color}.Add(gtx.Ops)
+ t.Shaper.LayoutString(text.Parameters{
+ Font: span.Font,
+ PxPerEm: fixed.I(gtx.Sp(span.Size)),
+ MaxLines: 1,
+ }, 0, maxWidth, gtx.Locale, span.Content)
+ ti := textIterator{
+ viewport: image.Rectangle{Max: gtx.Constraints.Max},
+ maxLines: 1,
+ }
+
+ line := glyphs[:0]
+ for g, ok := t.Shaper.NextGlyph(); ok; g, ok = t.Shaper.NextGlyph() {
+ line, ok = ti.paintGlyph(gtx, t.Shaper, g, line)
+ if !ok {
+ break
+ }
+ }
+ call := macro.Stop()
+ runesDisplayed := ti.runes
+ multiLine := runesDisplayed < utf8.RuneCountInString(span.Content)
// grab the first line of the result and compute its dimensions
- firstLine := lines[0]
- spanWidth := firstLine.Width.Ceil()
- spanHeight := (firstLine.Ascent + firstLine.Descent).Ceil()
- spanAscent := firstLine.Ascent.Ceil()
+ spanWidth := ti.bounds.Dx()
+ spanHeight := ti.bounds.Dy()
+ spanAscent := ti.baseline
// forceToNextLine handles the case in which the first segment of the new span does not fit
// AND there is already content on the current line. If there is no content on the line,
@@ 109,7 128,7 @@ func (t TextStyle) Layout(gtx layout.Context, spanFn func(gtx layout.Context, id
lineShapes = append(lineShapes, spanShape{
offset: image.Point{X: lineDims.X},
size: image.Point{X: spanWidth, Y: spanHeight},
- layout: firstLine.Layout,
+ call: call,
ascent: spanAscent,
})
// update the dimensions of the current line
@@ 130,19 149,17 @@ func (t TextStyle) Layout(gtx layout.Context, spanFn func(gtx layout.Context, id
// if we are breaking the current span across lines or we are on the
// last span, lay out all of the spans for the line.
- if len(lines) > 1 || i == len(spans)-1 || forceToNextLine {
+ if multiLine || ti.hasNewline || i == len(spans)-1 || forceToNextLine {
lineMacro := op.Record(gtx.Ops)
for i, shape := range lineShapes {
// lay out this span
span = spans[i+lineStartIndex]
- shape.offset.Y = overallSize.Y + lineAscent
- span.Layout(gtx, t.Shaper, shape)
+ shape.offset.Y = overallSize.Y
+ span.Layout(gtx, shape)
if spanFn == nil {
continue
}
- // set this offset to the upper corner of the text, not the lower
- shape.offset.Y -= shape.ascent
offStack := op.Offset(shape.offset).Push(gtx.Ops)
fnGtx := gtx
fnGtx.Constraints.Min = image.Point{}
@@ 175,14 192,14 @@ func (t TextStyle) Layout(gtx layout.Context, spanFn func(gtx layout.Context, id
// reset line shaping data and update overall vertical dimensions
lineShapes = lineShapes[:0]
overallSize.Y += lineDims.Y
+ lineDims = image.Point{}
+ lineAscent = 0
}
// if the current span breaks across lines
- if len(lines) > 1 && !forceToNextLine {
+ if multiLine && !forceToNextLine {
// mark where the next line to be laid out starts
lineStartIndex = i + 1
- lineDims = image.Point{}
- lineAscent = 0
// ensure the spans slice has room for another span
spans = append(spans, SpanStyle{})
@@ 192,7 209,7 @@ func (t TextStyle) Layout(gtx layout.Context, spanFn func(gtx layout.Context, id
}
// synthesize and insert a new span
byteLen := 0
- for i := 0; i < firstLine.Layout.Runes.Count; i++ {
+ for i := 0; i < runesDisplayed; i++ {
_, n := utf8.DecodeRuneInString(span.Content[byteLen:])
byteLen += n
}
@@ 201,9 218,10 @@ func (t TextStyle) Layout(gtx layout.Context, spanFn func(gtx layout.Context, id
} else if forceToNextLine {
// mark where the next line to be laid out starts
lineStartIndex = i
- lineDims = image.Point{}
- lineAscent = 0
i--
+ } else if ti.hasNewline {
+ // mark where the next line to be laid out starts
+ lineStartIndex = i + 1
}
}