Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tabs support #1836

Merged
merged 17 commits into from May 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 1 addition & 2 deletions go.mod
Expand Up @@ -5,7 +5,7 @@ go 1.12
require (
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9
github.com/akavel/rsrc v0.8.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3
github.com/fsnotify/fsnotify v1.4.9
github.com/fyne-io/mobile v0.1.2
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7
Expand All @@ -28,7 +28,6 @@ require (
golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3
)

replace github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3 => github.com/fyne-io/glfw/v3.3/glfw v0.0.0-20201123143003-f2279069162d
83 changes: 83 additions & 0 deletions internal/painter/drawer.go
@@ -0,0 +1,83 @@
package painter

import (
"image"
"image/draw"
"log"
"math"

"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)

// FontDrawer extends "golang.org/x/image/font" to add support for tabs
// FontDrawer draws text on a destination image.
//
// A FontDrawer is not safe for concurrent use by multiple goroutines, since its
// Face is not.
type FontDrawer struct {
font.Drawer
}

func tabStop(f font.Face, x fixed.Int26_6, tabWidth int) fixed.Int26_6 {
spacew, ok := f.GlyphAdvance(' ')
if !ok {
log.Print("Failed to find space width for tab")
return x
}
tabw := spacew * fixed.Int26_6(tabWidth)
tabs, _ := math.Modf(float64((x + tabw) / tabw))
return tabw * fixed.Int26_6(tabs)
}

// DrawString draws s at the dot and advances the dot's location.
// Tabs are translated into a dot location change.
func (d *FontDrawer) DrawString(s string, tabWidth int) {
prevC := rune(-1)
for _, c := range s {
if prevC >= 0 {
d.Dot.X += d.Face.Kern(prevC, c)
}
if c == '\t' {
d.Dot.X = tabStop(d.Face, d.Dot.X, tabWidth)
} else {
dr, mask, maskp, a, ok := d.Face.Glyph(d.Dot, c)
if !ok {
// TODO: is falling back on the U+FFFD glyph the responsibility of
adrianre12 marked this conversation as resolved.
Show resolved Hide resolved
// the Drawer or the Face?
// TODO: set prevC = '\ufffd'?
continue
}
draw.DrawMask(d.Dst, dr, d.Src, image.Point{}, mask, maskp, draw.Over)
d.Dot.X += a
}

prevC = c
}
}

// MeasureString returns how far dot would advance by drawing s with f.
// Tabs are translated into a dot location change.
func MeasureString(f font.Face, s string, tabWidth int) (advance fixed.Int26_6) {
prevC := rune(-1)
for _, c := range s {
if prevC >= 0 {
advance += f.Kern(prevC, c)
}
if c == '\t' {
advance = tabStop(f, advance, tabWidth)
} else {
a, ok := f.GlyphAdvance(c)
if !ok {
// TODO: is falling back on the U+FFFD glyph the responsibility of
// the Drawer or the Face?
// TODO: set prevC = '\ufffd'?
continue
}
advance += a
}

prevC = c
}
return advance
}
2 changes: 1 addition & 1 deletion internal/painter/font.go
Expand Up @@ -31,7 +31,7 @@ func RenderedTextSize(text string, size float32, style fyne.TextStyle) fyne.Size
opts.DPI = TextDPI

face := CachedFontFace(style, &opts)
advance := font.MeasureString(face, text)
advance := MeasureString(face, text, style.TabWidth())

return fyne.NewSize(float32(advance.Ceil()), float32(face.Metrics().Height.Ceil()))
}
Expand Down
5 changes: 2 additions & 3 deletions internal/painter/gl/gl_common.go
Expand Up @@ -7,7 +7,6 @@ import (

"github.com/goki/freetype"
"github.com/goki/freetype/truetype"
"golang.org/x/image/font"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
Expand Down Expand Up @@ -84,12 +83,12 @@ func (p *glPainter) newGlTextTexture(obj fyne.CanvasObject) Texture {
opts.DPI = float64(painter.TextDPI * p.texScale)
face := painter.CachedFontFace(text.TextStyle, &opts)

d := font.Drawer{}
d := painter.FontDrawer{}
d.Dst = img
d.Src = &image.Uniform{C: text.Color}
d.Face = face
d.Dot = freetype.Pt(0, height-face.Metrics().Descent.Ceil())
d.DrawString(text.Text)
d.DrawString(text.Text, text.TextStyle.TabWidth())

return p.imgToTexture(img, canvas.ImageScaleSmooth)
}
Expand Down
5 changes: 2 additions & 3 deletions internal/painter/software/draw.go
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/goki/freetype"
"github.com/goki/freetype/truetype"
"golang.org/x/image/draw"
"golang.org/x/image/font"
)

type gradient interface {
Expand Down Expand Up @@ -145,12 +144,12 @@ func drawText(c fyne.Canvas, text *canvas.Text, pos fyne.Position, base *image.N
opts.DPI = painter.TextDPI
face := painter.CachedFontFace(text.TextStyle, &opts)

d := font.Drawer{}
d := painter.FontDrawer{}
d.Dst = txtImg
d.Src = &image.Uniform{C: text.Color}
d.Face = face
d.Dot = freetype.Pt(0, height-face.Metrics().Descent.Ceil())
d.DrawString(text.Text)
d.DrawString(text.Text, text.TextStyle.TabWidth())

size := text.Size()
offsetX := float32(0)
Expand Down
17 changes: 9 additions & 8 deletions internal/test/util_test.go
Expand Up @@ -8,10 +8,10 @@ import (
"os"
"testing"

"fyne.io/fyne/v2/internal/painter"
"github.com/goki/freetype"
"github.com/goki/freetype/truetype"
"github.com/stretchr/testify/require"
"golang.org/x/image/font"

"fyne.io/fyne/v2/internal/test"
"fyne.io/fyne/v2/theme"
Expand All @@ -28,13 +28,14 @@ func TestAssertImageMatches(t *testing.T) {
opts := truetype.Options{Size: 20, DPI: 96}
f, _ := truetype.Parse(theme.TextFont().Content())
face := truetype.NewFace(f, &opts)
d := font.Drawer{
Dst: txtImg,
Src: image.NewUniform(color.Black),
Face: face,
Dot: freetype.Pt(0, 50-face.Metrics().Descent.Ceil()),
}
d.DrawString("Hello!")

d := painter.FontDrawer{}
d.Dst = txtImg
d.Src = image.NewUniform(color.Black)
d.Face = face
d.Dot = freetype.Pt(0, 50-face.Metrics().Descent.Ceil())

d.DrawString("Hello!", 4)
draw.Draw(img, bounds, txtImg, image.Point{}, draw.Over)

tt := &testing.T{}
Expand Down
3 changes: 1 addition & 2 deletions test/testdriver.go
Expand Up @@ -13,7 +13,6 @@ import (
"fyne.io/fyne/v2/storage/repository"

"github.com/goki/freetype/truetype"
"golang.org/x/image/font"
)

// SoftwarePainter describes a simple type that can render canvases
Expand Down Expand Up @@ -107,7 +106,7 @@ func (d *testDriver) RenderedTextSize(text string, size float32, style fyne.Text
opts.DPI = painter.TextDPI

face := painter.CachedFontFace(style, &opts)
advance := font.MeasureString(face, text)
advance := painter.MeasureString(face, text, style.TabWidth())

sws := fyne.NewSize(float32(advance.Ceil()), float32(face.Metrics().Height.Ceil()))
gls := painter.RenderedTextSize(text, size, style)
Expand Down
21 changes: 21 additions & 0 deletions text.go
Expand Up @@ -30,12 +30,33 @@ const (
TextWrapWord
)

const (
// DefaultTabWidth is the default width in spaces
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API introduced needs a since header:

//
// Since: 2.1

However - I don't think this API is necessary. If the defaulting was handled in the renderer and widget packages we could hide it in an internal package until we decide if this detail should be exposed...

DefaultTabWidth = 4
)

// TextStyle represents the styles that can be applied to a text canvas object
// or text based widget.
type TextStyle struct {
Bold bool // Should text be bold
Italic bool // Should text be italic
Monospace bool // Use the system monospace font instead of regular
tabWidth int // Width of tabs in spaces
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just used a public field like the other items? the check for 0 can be done inside the text code instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is done. The default code below is what causes the exposed public API above - if we just use a public field as suggested then the default handling would be in painter / widget and would avoid the additional API requirements.

}

// TabWidth either returns the set tab width or if not set the returns the DefaultTabWidth
func (ts *TextStyle) TabWidth() int {
if ts.tabWidth == 0 {
return DefaultTabWidth
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither this nor the setter are required if the field is public

}
return ts.tabWidth
}

// SetTabWidth sets the tab width if the supplied value is greater than 0
func (ts *TextStyle) SetTabWidth(tabWidth int) {
if tabWidth > 0 {
ts.tabWidth = tabWidth
}
}

// MeasureText uses the current driver to calculate the size of text when rendered.
Expand Down
2 changes: 1 addition & 1 deletion widget/entry_internal_test.go
Expand Up @@ -241,7 +241,7 @@ func TestEntry_Tab(t *testing.T) {
r := cache.Renderer(e.textProvider()).(*textRenderer)
assert.Equal(t, 3, len(r.texts))
assert.Equal(t, "a", r.texts[0].Text)
assert.Equal(t, textTabIndent+"b", r.texts[1].Text)
assert.Equal(t, "\tb", r.texts[1].Text)

w := test.NewWindow(e)
w.Resize(fyne.NewSize(86, 86))
Expand Down
6 changes: 1 addition & 5 deletions widget/text.go
Expand Up @@ -14,8 +14,6 @@ import (

const (
passwordChar = "•"
// TODO move to complete tab handling, for now we just indent this far statically
textTabIndent = " "
)

// textPresenter provides the widget specific information to a generic text provider
Expand Down Expand Up @@ -236,8 +234,6 @@ func (t *textProvider) lineSizeToColumn(col, row int) fyne.Size {
measureText := string(line[0:col])
if t.presenter.concealed() {
measureText = strings.Repeat(passwordChar, col)
} else {
measureText = strings.ReplaceAll(measureText, "\t", textTabIndent)
}

label := canvas.NewText(measureText, theme.ForegroundColor())
Expand Down Expand Up @@ -331,7 +327,7 @@ func (r *textRenderer) Refresh() {
if concealed {
line = strings.Repeat(passwordChar, len(row))
} else {
line = strings.ReplaceAll(string(row), "\t", textTabIndent)
line = string(row)
}

var textCanvas *canvas.Text
Expand Down