Skip to content

Commit

Permalink
Merge pull request #1836 from adrianre12/tabs_support
Browse files Browse the repository at this point in the history
Tabs support
  • Loading branch information
andydotxyz committed May 8, 2021
2 parents 6af63b0 + 310d8f6 commit f7c9f60
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 55 deletions.
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
// 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 @@ -77,12 +76,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
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
}

// 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
}
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 @@ -333,7 +333,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

0 comments on commit f7c9f60

Please sign in to comment.