Added text overflow to label.

Generalised text fitting (to width) and implemented binary search.
This commit is contained in:
Sander Schobers 2021-07-19 17:55:10 +02:00
parent 5dcecb8cc1
commit de8ce3e7bc
3 changed files with 88 additions and 38 deletions

View File

@ -6,10 +6,18 @@ import (
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
) )
type TextOverflow int
const (
TextOverflowClip TextOverflow = iota
TextOverflowEllipsis
)
type Label struct { type Label struct {
ControlBase ControlBase
Text string Text string
TextOverflow TextOverflow
DropShadow color.Color DropShadow color.Color
desired CachedValue desired CachedValue
@ -55,10 +63,15 @@ func (l *Label) getLabelTopLeft(ctx Context) geom.PointF32 {
func (l *Label) Render(ctx Context) { func (l *Label) Render(ctx Context) {
l.RenderBackground(ctx) l.RenderBackground(ctx)
fontColor := l.TextColor(ctx) fontColor := l.TextColor(ctx)
fontName := l.FontName(ctx) font := l.ActualFont(ctx)
topLeft := l.getLabelTopLeft(ctx) topLeft := l.getLabelTopLeft(ctx)
if l.DropShadow != nil { text := l.Text
ctx.Fonts().TextAlign(fontName, topLeft.Add2D(1, 1), l.DropShadow, l.Text, l.TextAlignment) availableWidth := l.bounds.Dx()
if l.TextOverflow == TextOverflowEllipsis {
text = fitTextEllipsis(font, text, availableWidth)
} }
ctx.Fonts().TextAlign(fontName, topLeft, fontColor, l.Text, l.TextAlignment) if l.DropShadow != nil {
ctx.Renderer().TextAlign(font, topLeft.Add2D(1, 1), l.DropShadow, text, l.TextAlignment)
}
ctx.Renderer().TextAlign(font, topLeft, fontColor, text, l.TextAlignment)
} }

View File

@ -43,37 +43,6 @@ func (p *Paragraph) hashTextDesired(ctx Context) string {
func (p *Paragraph) splitInLines(ctx Context, width float32) []string { func (p *Paragraph) splitInLines(ctx Context, width float32) []string {
font := p.ActualFont(ctx) font := p.ActualFont(ctx)
spaces := func(s string) []int { // creates a slice with indices where spaces can be found in string s
var spaces []int
offset := 0
for {
space := strings.Index(s[offset:], " ")
if space == -1 {
return spaces
}
offset += space
spaces = append(spaces, offset)
offset++
}
}
fit := func(s string) string { // tries to fit as much of string s in width space.
if font.WidthOf(s) < width {
return s
}
spaces := spaces(s)
// removes one word (delimited by spaces) at a time and tries until the result fits.
for split := len(spaces) - 1; split >= 0; split-- {
clipped := s[:spaces[split]]
if font.WidthOf(clipped) < width {
return clipped
}
}
// nothing fits (returns the whole string)...
return s
}
var lines []string var lines []string
for _, line := range strings.Split(p.Text, "\n") { for _, line := range strings.Split(p.Text, "\n") {
if len(line) == 0 { if len(line) == 0 {
@ -82,7 +51,7 @@ func (p *Paragraph) splitInLines(ctx Context, width float32) []string {
} }
for len(line) > 0 { for len(line) > 0 {
clipped := fit(line) clipped := fitTextWord(font, line, width)
lines = append(lines, clipped) lines = append(lines, clipped)
line = strings.TrimLeft(line[len(clipped):], " ") line = strings.TrimLeft(line[len(clipped):], " ")
} }

68
ui/text.go Normal file
View File

@ -0,0 +1,68 @@
package ui
import "strings"
// findOptimalFit tries to find the optimal (largest) fit for the interval [0..n] by doing a binary search.
func findOptimalFit(n int, fits func(i int) bool) int {
if n < 0 || fits(n) {
return n
}
min, max := 0, n
for {
if min == max {
if min == 0 && !fits(min) {
return -1
}
return min
}
middle := (max + min + 1) / 2
if fits(middle) {
min = middle
} else {
max = middle - 1
}
}
}
// findSpaces creates a slice with indices where spaces can be found in string s
func findSpaces(s string) []int {
var spaces []int
offset := 0
for {
space := strings.Index(s[offset:], " ")
if space == -1 {
return spaces
}
offset += space
spaces = append(spaces, offset)
offset++
}
}
func fitTextEllipsis(font Font, text string, availableWidth float32) string {
if font.WidthOf(text) <= availableWidth {
return text
}
ellipsis := "..."
availableWidth -= font.WidthOf(ellipsis)
cut := findOptimalFit(len(text), func(i int) bool { return font.WidthOf(text[:i]) <= availableWidth })
if cut == -1 {
return ""
}
return text[:cut] + ellipsis
}
// fitTextWord tries to fit as much of string s in width space.
func fitTextWord(font Font, s string, availableWidth float32) string {
if font.WidthOf(s) < availableWidth {
return s
}
spaces := findSpaces(s)
split := findOptimalFit(len(spaces)-1, func(i int) bool {
return font.WidthOf(s[:spaces[i]]) <= availableWidth
})
if split == -1 {
return s
}
return s[:spaces[split]]
}