mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-02 07:57:29 +00:00
font/opentype,text,widget{,/material}: [API] support bitmap glyph rendering
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>
This commit is contained in:
+31
-31
@@ -117,12 +117,12 @@ func TestEditorReadOnly(t *testing.T) {
|
||||
if cStart != cEnd {
|
||||
t.Errorf("unexpected initial caret positions")
|
||||
}
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
// Select everything.
|
||||
gtx.Ops.Reset()
|
||||
gtx.Queue = &testQueue{events: []event.Event{key.Event{Name: "A", Modifiers: key.ModShortcut}}}
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
textContent := e.Text()
|
||||
cStart2, cEnd2 := e.Selection()
|
||||
if cStart2 > cEnd2 {
|
||||
@@ -138,7 +138,7 @@ func TestEditorReadOnly(t *testing.T) {
|
||||
// Type some new characters.
|
||||
gtx.Ops.Reset()
|
||||
gtx.Queue = &testQueue{events: []event.Event{key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"}}}
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
textContent2 := e.Text()
|
||||
if textContent2 != textContent {
|
||||
t.Errorf("readonly editor modified by key.EditEvent")
|
||||
@@ -147,7 +147,7 @@ func TestEditorReadOnly(t *testing.T) {
|
||||
// Try to delete selection.
|
||||
gtx.Ops.Reset()
|
||||
gtx.Queue = &testQueue{events: []event.Event{key.Event{Name: key.NameDeleteBackward}}}
|
||||
dims := e.Layout(gtx, cache, font, fontSize, nil)
|
||||
dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
textContent2 = e.Text()
|
||||
if textContent2 != textContent {
|
||||
t.Errorf("readonly editor modified by delete key.Event")
|
||||
@@ -173,7 +173,7 @@ func TestEditorReadOnly(t *testing.T) {
|
||||
Position: layout.FPt(dims.Size).Mul(.5),
|
||||
},
|
||||
}}
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
cStart3, cEnd3 := e.Selection()
|
||||
if cStart3 == cStart2 || cEnd3 == cEnd2 {
|
||||
t.Errorf("expected mouse interaction to change selection.")
|
||||
@@ -213,7 +213,7 @@ func TestEditorConfigurations(t *testing.T) {
|
||||
e.Alignment = alignment
|
||||
e.SetText(sentence)
|
||||
e.SetCaret(0, 0)
|
||||
dims := e.Layout(gtx, cache, font, fontSize, nil)
|
||||
dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
if dims.Size.X < gtx.Constraints.Min.X || dims.Size.Y < gtx.Constraints.Min.Y {
|
||||
t.Errorf("expected min size %#+v, got %#+v", gtx.Constraints.Min, dims.Size)
|
||||
}
|
||||
@@ -222,7 +222,7 @@ func TestEditorConfigurations(t *testing.T) {
|
||||
t.Errorf("expected caret X to be %f, got %f", halfway, coords.X)
|
||||
}
|
||||
e.SetCaret(runes, runes)
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
coords = e.CaretCoords()
|
||||
if int(coords.X) > gtx.Constraints.Max.X || int(coords.Y) > gtx.Constraints.Max.Y {
|
||||
t.Errorf("caret coordinates %v exceed constraints %v", coords, gtx.Constraints.Max)
|
||||
@@ -246,7 +246,7 @@ func TestEditor(t *testing.T) {
|
||||
|
||||
// Regression test for bad in-cluster rune offset math.
|
||||
e.SetText("æbc")
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 0, 3, len("æbc"))
|
||||
|
||||
@@ -257,7 +257,7 @@ func TestEditor(t *testing.T) {
|
||||
if got, exp := e.Len(), utf8.RuneCountInString(e.Text()); got != exp {
|
||||
t.Errorf("got length %d, expected %d", got, exp)
|
||||
}
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
assertCaret(t, e, 0, 0, 0)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 0, 3, len("æbc"))
|
||||
@@ -284,7 +284,7 @@ func TestEditor(t *testing.T) {
|
||||
e.MoveCaret(-3, -3)
|
||||
assertCaret(t, e, 1, 1, len("æbc\na"))
|
||||
e.text.Mask = '*'
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
assertCaret(t, e, 1, 1, len("æbc\na"))
|
||||
e.MoveCaret(-3, -3)
|
||||
assertCaret(t, e, 0, 2, len("æb"))
|
||||
@@ -292,7 +292,7 @@ func TestEditor(t *testing.T) {
|
||||
NOTE(whereswaldon): it isn't possible to check the raw glyph data
|
||||
like this anymore. How should we handle this?
|
||||
e.Mask = '\U0001F92B'
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{},op.CallOp{})
|
||||
e.moveEnd(selectionClear)
|
||||
assertCaret(t, e, 0, 3, len("æbc"))
|
||||
|
||||
@@ -358,7 +358,7 @@ func TestEditorRTL(t *testing.T) {
|
||||
// Set the text to a single RTL word. The caret should start at 0 column
|
||||
// zero, but this is the first column on the right.
|
||||
e.SetText("الحب")
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
assertCaret(t, e, 0, 0, 0)
|
||||
e.MoveCaret(+1, +1)
|
||||
assertCaret(t, e, 0, 1, len("ا"))
|
||||
@@ -372,7 +372,7 @@ func TestEditorRTL(t *testing.T) {
|
||||
|
||||
sentence := "الحب سماء لا\nتمط غير الأحلام"
|
||||
e.SetText(sentence)
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
assertCaret(t, e, 0, 0, 0)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 0, 12, len("الحب سماء لا"))
|
||||
@@ -440,7 +440,7 @@ func TestEditorLigature(t *testing.T) {
|
||||
e.SetCaret(0, 0) // shouldn't panic
|
||||
assertCaret(t, e, 0, 0, 0)
|
||||
e.SetText("fl") // just a ligature
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 0, 2, len("fl"))
|
||||
e.MoveCaret(-1, -1)
|
||||
@@ -450,7 +450,7 @@ func TestEditorLigature(t *testing.T) {
|
||||
e.MoveCaret(+2, +2)
|
||||
assertCaret(t, e, 0, 2, len("fl"))
|
||||
e.SetText("flaffl•ffi\n•fflfi") // 3 ligatures on line 0, 2 on line 1
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
assertCaret(t, e, 0, 0, 0)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 0, 10, len("ffaffl•ffi"))
|
||||
@@ -502,7 +502,7 @@ func TestEditorLigature(t *testing.T) {
|
||||
assertCaret(t, e, 0, 0, 0)
|
||||
gtx.Constraints = layout.Exact(image.Pt(50, 50))
|
||||
e.SetText("fflffl fflffl fflffl fflffl") // Many ligatures broken across lines.
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
// Ensure that all runes in the final cluster of a line are properly
|
||||
// decoded when moving to the end of the line. This is a regression test.
|
||||
e.text.MoveEnd(selectionClear)
|
||||
@@ -517,7 +517,7 @@ func TestEditorLigature(t *testing.T) {
|
||||
// Absurdly narrow constraints to force each ligature onto its own line.
|
||||
gtx.Constraints = layout.Exact(image.Pt(10, 10))
|
||||
e.SetText("ffl ffl") // Two ligatures on separate lines.
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
assertCaret(t, e, 0, 0, 0)
|
||||
e.MoveCaret(1, 1) // Move the caret into the first ligature.
|
||||
assertCaret(t, e, 0, 1, len("f"))
|
||||
@@ -541,7 +541,7 @@ func TestEditorDimensions(t *testing.T) {
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
fontSize := unit.Sp(10)
|
||||
font := text.Font{}
|
||||
dims := e.Layout(gtx, cache, font, fontSize, nil)
|
||||
dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
if dims.Size.X == 0 {
|
||||
t.Errorf("EditEvent was not reflected in Editor width")
|
||||
}
|
||||
@@ -591,7 +591,7 @@ func TestEditorCaretConsistency(t *testing.T) {
|
||||
for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
|
||||
e := &Editor{}
|
||||
e.Alignment = a
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
consistent := func() error {
|
||||
t.Helper()
|
||||
@@ -615,7 +615,7 @@ func TestEditorCaretConsistency(t *testing.T) {
|
||||
switch mutation {
|
||||
case setText:
|
||||
e.SetText(str)
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
case moveRune:
|
||||
e.MoveCaret(int(distance), int(distance))
|
||||
case moveLine:
|
||||
@@ -681,7 +681,7 @@ func TestEditorMoveWord(t *testing.T) {
|
||||
fontSize := unit.Sp(10)
|
||||
font := text.Font{}
|
||||
e.SetText(t)
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
return e
|
||||
}
|
||||
for ii, tt := range tests {
|
||||
@@ -786,7 +786,7 @@ func TestEditorInsert(t *testing.T) {
|
||||
fontSize := unit.Sp(10)
|
||||
font := text.Font{}
|
||||
e.SetText(t)
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
return e
|
||||
}
|
||||
for ii, tt := range tests {
|
||||
@@ -876,7 +876,7 @@ func TestEditorDeleteWord(t *testing.T) {
|
||||
fontSize := unit.Sp(10)
|
||||
font := text.Font{}
|
||||
e.SetText(t)
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
return e
|
||||
}
|
||||
for ii, tt := range tests {
|
||||
@@ -934,7 +934,7 @@ g 2 4 6 8 g
|
||||
selected := func(start, end int) string {
|
||||
// Layout once with no events; populate e.lines.
|
||||
gtx.Queue = nil
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
_ = e.Events() // throw away any events from this layout
|
||||
|
||||
// Build the selection events
|
||||
@@ -960,7 +960,7 @@ g 2 4 6 8 g
|
||||
tim += time.Second // Avoid multi-clicks.
|
||||
gtx.Queue = tq
|
||||
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
for _, evt := range e.Events() {
|
||||
switch evt.(type) {
|
||||
case SelectEvent:
|
||||
@@ -1006,7 +1006,7 @@ g 2 4 6 8 g
|
||||
gtx.Constraints = layout.Exact(image.Pt(36, 36))
|
||||
// Keep existing selection
|
||||
gtx.Queue = nil
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
caretStart := e.text.closestToRune(e.text.caret.start)
|
||||
caretEnd := e.text.closestToRune(e.text.caret.end)
|
||||
@@ -1030,7 +1030,7 @@ func TestSelectMove(t *testing.T) {
|
||||
|
||||
// Layout once to populate e.lines and get focus.
|
||||
gtx.Queue = newQueue(key.FocusEvent{Focus: true})
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
testKey := func(keyName string) {
|
||||
// Select 345
|
||||
@@ -1041,7 +1041,7 @@ func TestSelectMove(t *testing.T) {
|
||||
|
||||
// Press the key
|
||||
gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName})
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
if expected, got := "", e.SelectedText(); expected != got {
|
||||
t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got)
|
||||
@@ -1115,7 +1115,7 @@ func TestEditor_MaxLen(t *testing.T) {
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
fontSize := unit.Sp(10)
|
||||
font := text.Font{}
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
if got, want := e.Text(), "12345678"; got != want {
|
||||
t.Errorf("editor failed to cap EditEvent")
|
||||
@@ -1146,7 +1146,7 @@ func TestEditor_Filter(t *testing.T) {
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
fontSize := unit.Sp(10)
|
||||
font := text.Font{}
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
if got, want := e.Text(), "12345678"; got != want {
|
||||
t.Errorf("editor failed to filter EditEvent")
|
||||
@@ -1170,7 +1170,7 @@ func TestEditor_Submit(t *testing.T) {
|
||||
cache := text.NewShaper(gofont.Collection())
|
||||
fontSize := unit.Sp(10)
|
||||
font := text.Font{}
|
||||
e.Layout(gtx, cache, font, fontSize, nil)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
if got, want := e.Text(), "ab1"; got != want {
|
||||
t.Errorf("editor failed to filter newline")
|
||||
|
||||
Reference in New Issue
Block a user