zntg/ui/paragraph.go

131 lines
3.2 KiB
Go
Raw Normal View History

package ui
import (
"strconv"
"strings"
"opslag.de/schobers/geom"
)
type Paragraph struct {
Label
width float32
lines CachedValue
}
func BuildParagraph(text string, fn func(*Paragraph)) *Paragraph {
var p = &Paragraph{}
p.Text = text
if fn != nil {
fn(p)
}
return p
}
func fastFormatFloat32(f float32) string { return strconv.FormatFloat(float64(f), 'b', 32, 32) }
func (p *Paragraph) desiredSize(ctx Context) interface{} {
fontName := p.FontName(ctx)
font := ctx.Fonts().Font(fontName)
pad := ctx.Style().Dimensions.TextPadding
lines := p.splitInLines(ctx, p.width-2*pad)
return geom.PtF32(p.width, float32(len(lines))*font.Height()+2*pad)
}
func (p *Paragraph) hashTextArranged(ctx Context) string {
return p.FontName(ctx) + p.Text + fastFormatFloat32(p.Bounds().Dx())
}
func (p *Paragraph) hashTextDesired(ctx Context) string {
return p.FontName(ctx) + p.Text + fastFormatFloat32(p.width)
}
func (p *Paragraph) splitInLines(ctx Context, width float32) []string {
fontName := p.FontName(ctx)
font := ctx.Fonts().Font(fontName)
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
for _, line := range strings.Split(p.Text, "\n") {
if len(line) == 0 {
lines = append(lines, line)
continue
}
for len(line) > 0 {
clipped := fit(line)
lines = append(lines, clipped)
line = strings.TrimLeft(line[len(clipped):], " ")
}
}
return lines
}
func (p *Paragraph) updateLines(ctx Context) interface{} {
pad := ctx.Style().Dimensions.TextPadding
return p.splitInLines(ctx, p.Bounds().Dx()-2*pad)
}
func (p *Paragraph) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 {
// stores the given width, is used when calculating the new desired size (and thus used in the hash method as well)
p.width = size.X
return p.desired.GetContext(ctx, p.desiredSize, p.hashTextDesired).(geom.PointF32)
}
func (p *Paragraph) Render(ctx Context) {
p.RenderBackground(ctx)
pad := ctx.Style().Dimensions.TextPadding
width := p.Bounds().Dx() - 2*pad
lines := p.lines.GetContext(ctx, p.updateLines, p.hashTextArranged).([]string)
fontColor := p.TextColor(ctx)
fontName := p.FontName(ctx)
fontHeight := ctx.Fonts().Font(fontName).Height()
bounds := p.bounds.Inset(pad)
left := bounds.Min.X
switch p.TextAlignment {
case AlignRight:
left = bounds.Max.X
case AlignCenter:
left += .5 * width
}
offset := bounds.Min.Y
for _, line := range lines {
ctx.Fonts().TextAlign(fontName, geom.PtF32(left, offset), fontColor, line, p.TextAlignment)
offset += fontHeight
}
}