diff --git a/go.mod b/go.mod index 07c230894f..b7918fc3fb 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/internal/painter/drawer.go b/internal/painter/drawer.go new file mode 100644 index 0000000000..47be08ee6a --- /dev/null +++ b/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 +} diff --git a/internal/painter/font.go b/internal/painter/font.go index 034f4dec5a..2b777a6268 100644 --- a/internal/painter/font.go +++ b/internal/painter/font.go @@ -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())) } diff --git a/internal/painter/gl/gl_common.go b/internal/painter/gl/gl_common.go index 03a16b197b..066239dd7c 100644 --- a/internal/painter/gl/gl_common.go +++ b/internal/painter/gl/gl_common.go @@ -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" @@ -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) } diff --git a/internal/painter/software/draw.go b/internal/painter/software/draw.go index 2dc8819f38..2f905c0e7b 100644 --- a/internal/painter/software/draw.go +++ b/internal/painter/software/draw.go @@ -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 { @@ -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) diff --git a/internal/test/util_test.go b/internal/test/util_test.go index 938046b370..141c303c24 100644 --- a/internal/test/util_test.go +++ b/internal/test/util_test.go @@ -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" @@ -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{} diff --git a/test/testdriver.go b/test/testdriver.go index 6c26e49465..e8b36bd89d 100644 --- a/test/testdriver.go +++ b/test/testdriver.go @@ -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 @@ -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) diff --git a/text.go b/text.go index 2e55be92dd..d12ed0854f 100644 --- a/text.go +++ b/text.go @@ -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. diff --git a/widget/entry_internal_test.go b/widget/entry_internal_test.go index 8bce6d8afa..1e2cd71c69 100644 --- a/widget/entry_internal_test.go +++ b/widget/entry_internal_test.go @@ -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)) diff --git a/widget/text.go b/widget/text.go index 94e68192b8..7448da7215 100644 --- a/widget/text.go +++ b/widget/text.go @@ -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 @@ -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()) @@ -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 diff --git a/widget/textgrid.go b/widget/textgrid.go index 3788ee775f..344e89a10d 100644 --- a/widget/textgrid.go +++ b/widget/textgrid.go @@ -67,6 +67,7 @@ type TextGrid struct { ShowLineNumbers bool ShowWhitespace bool + TabWidth int // If set to 0 the fyne.DefaultTabWidth is used } // MinSize returns the smallest size this widget can shrink to @@ -84,25 +85,22 @@ func (t *TextGrid) Resize(size fyne.Size) { // SetText updates the buffer of this textgrid to contain the specified text. // New lines and columns will be added as required. Lines are separated by '\n'. // The grid will use default text style and any previous content and style will be removed. +// Tab characters are padded with spaces to the next tab stop. func (t *TextGrid) SetText(text string) { lines := strings.Split(text, "\n") rows := make([]TextGridRow, len(lines)) for i, line := range lines { - spaced := strings.ReplaceAll(line, "\t", textTabIndent) - - cells := make([]TextGridCell, len(spaced)) - extras := 0 - for j, r := range line { - cells[j+extras] = TextGridCell{Rune: r} - + cells := make([]TextGridCell, 0, len(line)) + for _, r := range line { + cells = append(cells, TextGridCell{Rune: r}) if r == '\t' { - for k := j + extras + 1; k < j+extras+len(textTabIndent); k++ { - cells[k] = TextGridCell{Rune: ' '} + col := len(cells) + next := nextTab(col-1, t.tabWidth()) + for i := col; i < next; i++ { + cells = append(cells, TextGridCell{Rune: ' '}) } - extras += len(textTabIndent) - 1 } } - rows[i] = TextGridRow{Cells: cells} } @@ -112,6 +110,7 @@ func (t *TextGrid) SetText(text string) { // Text returns the contents of the buffer as a single string (with no style information). // It reconstructs the lines by joining with a `\n` character. +// Tab characters have padded spaces removed. func (t *TextGrid) Text() string { count := len(t.Rows) - 1 // newlines for _, row := range t.Rows { @@ -122,31 +121,25 @@ func (t *TextGrid) Text() string { return "" } - runes := make([]rune, count) - c := 0 - skipped := 0 + runes := make([]rune, 0, count) + for i, row := range t.Rows { - skip := 0 - for _, r := range row.Cells { - if skip > 0 { - skip-- + next := 0 + for col, cell := range row.Cells { + if col < next { continue } - runes[c] = r.Rune - c++ - if r.Rune == '\t' { - skip = len(textTabIndent) - 1 - skipped += skip + runes = append(runes, cell.Rune) + if cell.Rune == '\t' { + next = nextTab(col, t.tabWidth()) } } - if i < len(t.Rows)-1 { - runes[c] = '\n' - c++ + runes = append(runes, '\n') } } - return string(runes[:len(runes)-skipped]) + return string(runes) } // Row returns a copy of the content in a specified row as a TextGridRow. @@ -163,18 +156,30 @@ func (t *TextGrid) Row(row int) TextGridRow { // If the index is out of bounds it returns an empty string. func (t *TextGrid) RowText(row int) string { rowData := t.Row(row) - runes := make([]rune, len(rowData.Cells)) - c := 0 - for _, r := range rowData.Cells { - runes[c] = r.Rune - c++ + count := len(rowData.Cells) + + if count <= 0 { + return "" } + runes := make([]rune, 0, count) + + next := 0 + for col, cell := range rowData.Cells { + if col < next { + continue + } + runes = append(runes, cell.Rune) + if cell.Rune == '\t' { + next = nextTab(col, t.tabWidth()) + } + } return string(runes) } // SetRow updates the specified row of the grid's contents using the specified content and style and then refreshes. // If the row is beyond the end of the current buffer it will be expanded. +// Tab characters are not padded with spaces. func (t *TextGrid) SetRow(row int, content TextGridRow) { if row < 0 { return @@ -314,6 +319,12 @@ func NewTextGridFromString(content string) *TextGrid { return grid } +// nextTab finds the column of the next tab stop for the given column +func nextTab(column int, tabWidth int) int { + tabStop, _ := math.Modf(float64(column+tabWidth) / float64(tabWidth)) + return tabWidth * int(tabStop) +} + type textGridRenderer struct { text *TextGrid @@ -436,6 +447,14 @@ func (t *textGridRenderer) refreshGrid() { } } +// tabWidth either returns the set tab width or if not set the returns the DefaultTabWidth +func (t *TextGrid) tabWidth() int { + if t.TabWidth == 0 { + return fyne.DefaultTabWidth + } + return t.TabWidth +} + func (t *textGridRenderer) lineNumberWidth() int { return len(fmt.Sprintf("%d", t.rows+1)) }