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 } }