mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
f77bf9a42c
This commit adds back support for loading font collections, which we
lost when switching to the harfbuzz-based shaper last January. In
addition, this commit takes advantage of our new font loading library's
metadata facilities to automatically construct text.FontFaces for all
fonts within a collection. This is significantly more ergonomic for
users, and can be used to load single fonts with automatic metadata
detection as well.
I've exposed a opentype.Face.Font() method that can be used to get the
font metadata for a given face as well, though you have to type assert to
see it:
var myFace text.Face
if asOpentype, ok := myFace.(opentype.Face); ok {
myFont := asOpentype.Font()
}
The one problem with this approach is that the font variant field always
be automatically populated. Mono font detection is supported, but
other variants like SmallCaps are more complicated and may need to be
expressed differently in the future (smallcaps is a feature that any font
file can have, not necessarily a separate font file). See this [0] upstream
issue for details.
Additionally, in order to avoid import cycles, I've moved the declarations
of font attributes to package font. You can fix your code automatically to
refer to the new definitions by running the following:
gofmt -w -r 'text.FontFace -> font.FontFace' .
gofmt -w -r 'text.Variant -> font.Variant' .
gofmt -w -r 'text.Style -> font.Style' .
gofmt -w -r 'text.Typeface -> font.Typeface' .
gofmt -w -r 'text.Font -> font.Font' .
gofmt -w -r 'text.Regular -> font.Regular' .
gofmt -w -r 'text.Italic -> font.Italic' .
gofmt -w -r 'text.Thin -> font.Thin' .
gofmt -w -r 'text.ExtraLight -> font.ExtraLight' .
gofmt -w -r 'text.Light -> font.Light' .
gofmt -w -r 'text.Normal -> font.Normal' .
gofmt -w -r 'text.Medium -> font.Medium' .
gofmt -w -r 'text.SemiBold -> font.SemiBold' .
gofmt -w -r 'text.Bold -> font.Bold' .
gofmt -w -r 'text.ExtraBold -> font.ExtraBold' .
gofmt -w -r 'text.Black -> font.Black' .
gofmt -w -r 'text.Hairline -> font.Thin' .
gofmt -w -r 'text.UltraLight -> font.ExtraLight' .
gofmt -w -r 'text.DemiBold -> font.SemiBold' .
gofmt -w -r 'text.UltraBold -> font.ExtraBold' .
gofmt -w -r 'text.Heavy -> font.Black' .
gofmt -w -r 'text.ExtraBlack -> font.Black+50' .
gofmt -w -r 'text.UltraBlack -> font.ExtraBlack' .
Make sure each affected file imports gioui.org/font.
[0] https://github.com/go-text/typesetting/issues/57
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
164 lines
4.0 KiB
Go
164 lines
4.0 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
//go:build go1.18
|
|
// +build go1.18
|
|
|
|
package app
|
|
|
|
import (
|
|
"testing"
|
|
"unicode/utf8"
|
|
|
|
"gioui.org/font"
|
|
"gioui.org/font/gofont"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/router"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/text"
|
|
"gioui.org/unit"
|
|
"gioui.org/widget"
|
|
)
|
|
|
|
func FuzzIME(f *testing.F) {
|
|
runes := []rune("Hello, 世界! 🤬 علي،الحسنب北查爾斯頓工廠的安全漏洞已")
|
|
f.Add([]byte("20\x0010"))
|
|
f.Add([]byte("80000"))
|
|
f.Add([]byte("2008\"80\r00"))
|
|
f.Add([]byte("20007900002\x02000"))
|
|
f.Add([]byte("20007800002\x02000"))
|
|
f.Add([]byte("200A02000990\x19002\x17\x0200"))
|
|
f.Fuzz(func(t *testing.T, cmds []byte) {
|
|
cache := text.NewShaper(gofont.Collection())
|
|
e := new(widget.Editor)
|
|
e.Focus()
|
|
|
|
var r router.Router
|
|
gtx := layout.Context{Ops: new(op.Ops), Queue: &r}
|
|
// Layout once to register focus.
|
|
e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
|
|
r.Frame(gtx.Ops)
|
|
|
|
var state editorState
|
|
const (
|
|
cmdReplace = iota
|
|
cmdSelect
|
|
cmdSnip
|
|
maxCmd
|
|
)
|
|
const cmdLen = 5
|
|
for len(cmds) >= cmdLen {
|
|
n := e.Len()
|
|
rng := key.Range{
|
|
Start: int(cmds[1]) % (n + 1),
|
|
End: int(cmds[2]) % (n + 1),
|
|
}
|
|
switch cmds[0] % cmdLen {
|
|
case cmdReplace:
|
|
rstart := int(cmds[3]) % len(runes)
|
|
rend := int(cmds[4]) % len(runes)
|
|
if rstart > rend {
|
|
rstart, rend = rend, rstart
|
|
}
|
|
replacement := string(runes[rstart:rend])
|
|
state.Replace(rng, replacement)
|
|
r.Queue(key.EditEvent{Range: rng, Text: replacement})
|
|
r.Queue(key.SnippetEvent(state.Snippet.Range))
|
|
case cmdSelect:
|
|
r.Queue(key.SelectionEvent(rng))
|
|
runes := []rune(e.Text())
|
|
if rng.Start < 0 {
|
|
rng.Start = 0
|
|
}
|
|
if rng.End < 0 {
|
|
rng.End = 0
|
|
}
|
|
if rng.Start > len(runes) {
|
|
rng.Start = len(runes)
|
|
}
|
|
if rng.End > len(runes) {
|
|
rng.End = len(runes)
|
|
}
|
|
state.Selection.Range = rng
|
|
case cmdSnip:
|
|
r.Queue(key.SnippetEvent(rng))
|
|
runes := []rune(e.Text())
|
|
if rng.Start > rng.End {
|
|
rng.Start, rng.End = rng.End, rng.Start
|
|
}
|
|
if rng.Start < 0 {
|
|
rng.Start = 0
|
|
}
|
|
if rng.End < 0 {
|
|
rng.End = 0
|
|
}
|
|
if rng.Start > len(runes) {
|
|
rng.Start = len(runes)
|
|
}
|
|
if rng.End > len(runes) {
|
|
rng.End = len(runes)
|
|
}
|
|
state.Snippet = key.Snippet{
|
|
Range: rng,
|
|
Text: string(runes[rng.Start:rng.End]),
|
|
}
|
|
}
|
|
cmds = cmds[cmdLen:]
|
|
e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
|
|
r.Frame(gtx.Ops)
|
|
newState := r.EditorState()
|
|
// We don't track caret position.
|
|
state.Selection.Caret = newState.Selection.Caret
|
|
// Expanded snippets are ok.
|
|
their, our := newState.Snippet, state.EditorState.Snippet
|
|
beforeLen := 0
|
|
for before := our.Start - their.Start; before > 0; before-- {
|
|
_, n := utf8.DecodeRuneInString(their.Text[beforeLen:])
|
|
beforeLen += n
|
|
}
|
|
afterLen := 0
|
|
for after := their.End - our.End; after > 0; after-- {
|
|
_, n := utf8.DecodeLastRuneInString(their.Text[:len(their.Text)-afterLen])
|
|
afterLen += n
|
|
}
|
|
if beforeLen > 0 {
|
|
our.Text = their.Text[:beforeLen] + our.Text
|
|
our.Start = their.Start
|
|
}
|
|
if afterLen > 0 {
|
|
our.Text = our.Text + their.Text[len(their.Text)-afterLen:]
|
|
our.End = their.End
|
|
}
|
|
state.EditorState.Snippet = our
|
|
if newState != state.EditorState {
|
|
t.Errorf("IME state: %+v\neditor state: %+v", state.EditorState, newState)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestEditorIndices(t *testing.T) {
|
|
var s editorState
|
|
const str = "Hello, 😀"
|
|
s.Snippet = key.Snippet{
|
|
Text: str,
|
|
Range: key.Range{
|
|
Start: 10,
|
|
End: utf8.RuneCountInString(str),
|
|
},
|
|
}
|
|
utf16Indices := [...]struct {
|
|
Runes, UTF16 int
|
|
}{
|
|
{0, 0}, {10, 10}, {17, 17}, {18, 19}, {30, 31},
|
|
}
|
|
for _, p := range utf16Indices {
|
|
if want, got := p.UTF16, s.UTF16Index(p.Runes); want != got {
|
|
t.Errorf("UTF16Index(%d) = %d, wanted %d", p.Runes, got, want)
|
|
}
|
|
if want, got := p.Runes, s.RunesIndex(p.UTF16); want != got {
|
|
t.Errorf("RunesIndex(%d) = %d, wanted %d", p.UTF16, got, want)
|
|
}
|
|
}
|
|
}
|