mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
6ab3ff40a6
This commit supports rendering opentype glyphs containing bitmap data instead of
color data. In order to support returning the shaped bitmap glyphs from the Shaper's
Shape() method, it has gained a second return parameter, an op.CallOp. Adding
that CallOp immediately after or immediately before painting the returned path
will display the bitmap glyphs.
The consequences of supporting colored glyphs forced changes upon the widget APIs
for widgets that display text. Previously text always had a fixed paint material,
so we could rely upon the caller setting the material (e.g. adding a paint.ColorOp)
before painting the glyphs and everything would work. Now that we display image-
based glyphs, we end up changing the painting material to an image midway through
displaying text. This is an awkward consequence of how we currently manage the
painting material, and to work around it widgets now accept an op.CallOp that
is expected to set the proper paint material. Text widgets will use that op.CallOp
before painting text (or other paint operations) to ensure that they are painting
with the proper materials.
This, in turn, changed the APIs for laying out widget.Editor, widget.Label, and
widget.Selectable, and eliminated the need for them to accept a callback (the
callback was only really to set the colors). Dropping that callback function
allowed me to consolidate widget.Label to only need one exported Layout method,
and allowed me to unexport the PaintText, PaintCaret, and PaintSelection methods
from widget.Editor and widget.Selectable. Those methods are useless in the public
API now that they don't need to be invoked after applying a color operation.
Callers of the raw text shaper API will need to make the following changes:
- Where before you used:
var ops *op.Ops // Assume we have an operation list.
var shaper *text.Shaper // Assume we have a shaper.
var col color.NRGBA // Assume we have a text color.
var glyphs []text.Glyph // Assume we have already filled a slice of glyphs.
shape := shaper.Shape(glyphs)
paint.FillShape(ops, col, clip.Outline{Path:shape}.Op())
- Now you should do:
shape, call := shaper.Shape(glyphs)
paint.FillShape(ops, col, clip.Outline{Path:shape}.Op())
call.Add(ops)
Callers of the widget.{Label,Selectable,Editor} APIs will need to make the
following changes:
- Where before you used:
var gtx layout.Context // Assume we have an operation list.
var shaper *text.Shaper // Assume we have a shaper.
var textCol color.NRGBA // Assume we have a text color.
var selectCol color.NRGBA // Assume we have a selection color.
var ed widget.Editor // Assume we have an editor.
var sel widget.Selectable // Assume we have a selectable.
// Lay out an editor.
ed.Layout(gtx, shaper, text.Font{}, unit.Sp(30), func(layout.Context) layout.Dimensions {
// Paint the editor.
})
// Lay out a selectable.
sel.Layout(gtx, shaper, text.Font{}, unit.Sp(30), func(layout.Context) layout.Dimensions {
// Paint the selectable.
})
// Lay out an interactive label.
widget.Label{}.LayoutSelectable(gtx, shaper, text.Font{}, unit.Sp(30), "hello", func(layout.Context) layout.Dimensions {
// Paint the label.
})
// Lay out a non-interactive label.
widget.Label{}.Layout(gtx, shaper, text.Font{}, unit.Sp(30), "hello")
- Now you should do:
// Capture setting the text paint material in a macro.
textColMacro := op.Record(gtx.Ops)
paint.ColorOp{Color: textCol}.Add(gtx.Ops)
textMaterial := textColMacro.Stop()
// Capture setting the selection paint material in a macro.
selectColMacro := op.Record(gtx.Ops)
paint.ColorOp{Color: selectCol}.Add(gtx.Ops)
selectMaterial := selectColMacro.Stop()
// Lay out an editor.
ed.Layout(gtx, shaper, text.Font{}, unit.Sp(30), textMaterial, selectMaterial)
// Lay out a selectable.
sel.Layout(gtx, shaper, text.Font{}, unit.Sp(30), textMaterial, selectMaterial)
// Lay out a label (no difference between interactive and non-interactive)
widget.Label{}.Layout(gtx, shaper, text.Font{}, unit.Sp(30), "hello", textMaterial, selectMaterial)
Callers of the material package API do not need to make any changes.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
188 lines
6.4 KiB
Go
188 lines
6.4 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package widget
|
|
|
|
import (
|
|
"image"
|
|
|
|
"gioui.org/io/semantic"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/op/clip"
|
|
"gioui.org/op/paint"
|
|
"gioui.org/text"
|
|
"gioui.org/unit"
|
|
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
// Label is a widget for laying out and drawing text.
|
|
type Label struct {
|
|
// Alignment specifies the text alignment.
|
|
Alignment text.Alignment
|
|
// MaxLines limits the number of lines. Zero means no limit.
|
|
MaxLines int
|
|
// Selectable optionally provides text selection state. If nil,
|
|
// text will not be selectable.
|
|
Selectable *Selectable
|
|
}
|
|
|
|
// Layout the label with the given shaper, font, size, text, and materials. If the Selectable field is
|
|
// populated, the label will support text selection. Otherwise, it will be non-interactive. The textMaterial
|
|
// and selectionMaterial op.CallOps are responsible for setting the painting material for the text glyphs
|
|
// and the text selection rectangles, respectively.
|
|
func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, txt string, textMaterial, selectionMaterial op.CallOp) layout.Dimensions {
|
|
if l.Selectable == nil {
|
|
return l.layout(gtx, lt, font, size, txt, textMaterial, selectionMaterial)
|
|
}
|
|
l.Selectable.text.Alignment = l.Alignment
|
|
l.Selectable.text.MaxLines = l.MaxLines
|
|
l.Selectable.SetText(txt)
|
|
return l.Selectable.Layout(gtx, lt, font, size, textMaterial, selectionMaterial)
|
|
}
|
|
|
|
// layout the text as non-interactive.
|
|
func (l Label) layout(gtx layout.Context, lt *text.Shaper, font text.Font, size unit.Sp, txt string, textMaterial, selectionMaterial op.CallOp) layout.Dimensions {
|
|
cs := gtx.Constraints
|
|
textSize := fixed.I(gtx.Sp(size))
|
|
lt.LayoutString(text.Parameters{
|
|
Font: font,
|
|
PxPerEm: textSize,
|
|
MaxLines: l.MaxLines,
|
|
Alignment: l.Alignment,
|
|
}, cs.Min.X, cs.Max.X, gtx.Locale, txt)
|
|
m := op.Record(gtx.Ops)
|
|
viewport := image.Rectangle{Max: cs.Max}
|
|
it := textIterator{
|
|
viewport: viewport,
|
|
maxLines: l.MaxLines,
|
|
material: textMaterial,
|
|
}
|
|
semantic.LabelOp(txt).Add(gtx.Ops)
|
|
var glyphs [32]text.Glyph
|
|
line := glyphs[:0]
|
|
for g, ok := lt.NextGlyph(); ok; g, ok = lt.NextGlyph() {
|
|
var ok bool
|
|
if line, ok = it.paintGlyph(gtx, lt, g, line); !ok {
|
|
break
|
|
}
|
|
}
|
|
call := m.Stop()
|
|
viewport.Min = viewport.Min.Add(it.padding.Min)
|
|
viewport.Max = viewport.Max.Add(it.padding.Max)
|
|
clipStack := clip.Rect(viewport).Push(gtx.Ops)
|
|
call.Add(gtx.Ops)
|
|
dims := layout.Dimensions{Size: it.bounds.Size()}
|
|
dims.Size = cs.Constrain(dims.Size)
|
|
dims.Baseline = dims.Size.Y - it.baseline
|
|
clipStack.Pop()
|
|
return dims
|
|
}
|
|
|
|
func r2p(r clip.Rect) clip.Op {
|
|
return clip.Stroke{Path: r.Path(), Width: 1}.Op()
|
|
}
|
|
|
|
// textIterator computes the bounding box of and paints text.
|
|
type textIterator struct {
|
|
// viewport is the rectangle of document coordinates that the iterator is
|
|
// trying to fill with text.
|
|
viewport image.Rectangle
|
|
// maxLines is the maximum number of text lines that should be displayed.
|
|
maxLines int
|
|
// material sets the paint material for the text glyphs. If none is provided
|
|
// the glyphs will be invisible.
|
|
material op.CallOp
|
|
|
|
// linesSeen tracks the quantity of line endings this iterator has seen.
|
|
linesSeen int
|
|
// lineOff tracks the origin for the glyphs in the current line.
|
|
lineOff image.Point
|
|
// padding is the space needed outside of the bounds of the text to ensure no
|
|
// part of a glyph is clipped.
|
|
padding image.Rectangle
|
|
// bounds is the logical bounding box of the text.
|
|
bounds image.Rectangle
|
|
// visible tracks whether the most recently iterated glyph is visible within
|
|
// the viewport.
|
|
visible bool
|
|
// first tracks whether the iterator has processed a glyph yet.
|
|
first bool
|
|
// baseline tracks the location of the first line of text's baseline.
|
|
baseline int
|
|
}
|
|
|
|
// processGlyph checks whether the glyph is visible within the iterator's configured
|
|
// viewport and (if so) updates the iterator's text dimensions to include the glyph.
|
|
func (it *textIterator) processGlyph(g text.Glyph, ok bool) (_ text.Glyph, visibleOrBefore bool) {
|
|
if it.maxLines > 0 {
|
|
if g.Flags&text.FlagLineBreak != 0 {
|
|
it.linesSeen++
|
|
}
|
|
if it.linesSeen == it.maxLines && g.Flags&text.FlagParagraphBreak != 0 {
|
|
return g, false
|
|
}
|
|
}
|
|
// Compute the maximum extent to which glyphs overhang on the horizontal
|
|
// axis.
|
|
if d := g.Bounds.Min.X.Floor(); d < it.padding.Min.X {
|
|
it.padding.Min.X = d
|
|
}
|
|
if d := (g.Bounds.Max.X - g.Advance).Ceil(); d > it.padding.Max.X {
|
|
it.padding.Max.X = d
|
|
}
|
|
logicalBounds := image.Rectangle{
|
|
Min: image.Pt(g.X.Floor(), int(g.Y)-g.Ascent.Ceil()),
|
|
Max: image.Pt((g.X + g.Advance).Ceil(), int(g.Y)+g.Descent.Ceil()),
|
|
}
|
|
if !it.first {
|
|
it.first = true
|
|
it.baseline = int(g.Y)
|
|
it.bounds = logicalBounds
|
|
}
|
|
|
|
above := logicalBounds.Max.Y < it.viewport.Min.Y
|
|
below := logicalBounds.Min.Y > it.viewport.Max.Y
|
|
left := logicalBounds.Max.X < it.viewport.Min.X
|
|
right := logicalBounds.Min.X > it.viewport.Max.X
|
|
it.visible = !above && !below && !left && !right
|
|
if it.visible {
|
|
it.bounds.Min.X = min(it.bounds.Min.X, logicalBounds.Min.X)
|
|
it.bounds.Min.Y = min(it.bounds.Min.Y, logicalBounds.Min.Y)
|
|
it.bounds.Max.X = max(it.bounds.Max.X, logicalBounds.Max.X)
|
|
it.bounds.Max.Y = max(it.bounds.Max.Y, logicalBounds.Max.Y)
|
|
}
|
|
return g, ok && !below
|
|
}
|
|
|
|
// paintGlyph buffers up and paints text glyphs. It should be invoked iteratively upon each glyph
|
|
// until it returns false. The line parameter should be a slice with
|
|
// a backing array of sufficient size to buffer multiple glyphs.
|
|
// A modified slice will be returned with each invocation, and is
|
|
// expected to be passed back in on the following invocation.
|
|
// This design is awkward, but prevents the line slice from escaping
|
|
// to the heap.
|
|
func (it *textIterator) paintGlyph(gtx layout.Context, shaper *text.Shaper, glyph text.Glyph, line []text.Glyph) ([]text.Glyph, bool) {
|
|
_, visibleOrBefore := it.processGlyph(glyph, true)
|
|
if it.visible {
|
|
if len(line) == 0 {
|
|
it.lineOff = image.Point{X: glyph.X.Floor(), Y: int(glyph.Y)}.Sub(it.viewport.Min)
|
|
}
|
|
line = append(line, glyph)
|
|
}
|
|
if glyph.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 || !visibleOrBefore {
|
|
t := op.Offset(it.lineOff).Push(gtx.Ops)
|
|
path := shaper.Shape(line)
|
|
outline := clip.Outline{Path: path}.Op().Push(gtx.Ops)
|
|
it.material.Add(gtx.Ops)
|
|
paint.PaintOp{}.Add(gtx.Ops)
|
|
outline.Pop()
|
|
if call := shaper.Bitmaps(line); call != (op.CallOp{}) {
|
|
call.Add(gtx.Ops)
|
|
}
|
|
t.Pop()
|
|
line = line[:0]
|
|
}
|
|
return line, visibleOrBefore
|
|
}
|