mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-03 16:35:36 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05f0dc2513 | |||
| c1d975cced | |||
| 32f15ede7b | |||
| d414116990 | |||
| 341978dbcd | |||
| 80da4d6b02 | |||
| edbf872b44 | |||
| c7c49c3258 | |||
| fdd102aaf9 | |||
| 8dc03ed655 | |||
| 1d8b54892a | |||
| 7966832536 | |||
| 36a39f7d38 | |||
| d62057a62e | |||
| ddf770b9d5 | |||
| acab582487 | |||
| 6384ab6087 | |||
| 43c47f0883 | |||
| babe7a292b | |||
| 92bc52c25c | |||
| df782ea7c5 | |||
| 74a87b1092 | |||
| 6ea4119a3c | |||
| 15031d0b52 | |||
| 5606a961f2 |
+1
-1
@@ -24,7 +24,7 @@ environment:
|
|||||||
tasks:
|
tasks:
|
||||||
- install_go: |
|
- install_go: |
|
||||||
mkdir -p /home/build/sdk
|
mkdir -p /home/build/sdk
|
||||||
curl -s https://dl.google.com/go/go1.18.9.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
|
curl -s https://dl.google.com/go/go1.19.11.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
|
||||||
- prepare_toolchain: |
|
- prepare_toolchain: |
|
||||||
mkdir -p $APPLE_TOOLCHAIN_ROOT
|
mkdir -p $APPLE_TOOLCHAIN_ROOT
|
||||||
cd $APPLE_TOOLCHAIN_ROOT
|
cd $APPLE_TOOLCHAIN_ROOT
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ environment:
|
|||||||
tasks:
|
tasks:
|
||||||
- install_go: |
|
- install_go: |
|
||||||
mkdir -p /home/build/sdk
|
mkdir -p /home/build/sdk
|
||||||
curl https://dl.google.com/go/go1.18.9.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf -
|
curl https://dl.google.com/go/go1.19.11.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf -
|
||||||
- test_gio: |
|
- test_gio: |
|
||||||
cd gio
|
cd gio
|
||||||
go test ./...
|
go test ./...
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ secrets:
|
|||||||
tasks:
|
tasks:
|
||||||
- install_go: |
|
- install_go: |
|
||||||
mkdir -p /home/build/sdk
|
mkdir -p /home/build/sdk
|
||||||
curl -s https://dl.google.com/go/go1.18.9.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
|
curl -s https://dl.google.com/go/go1.19.11.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
|
||||||
- check_gofmt: |
|
- check_gofmt: |
|
||||||
cd gio
|
cd gio
|
||||||
test -z "$(gofmt -s -l .)"
|
test -z "$(gofmt -s -l .)"
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ environment:
|
|||||||
tasks:
|
tasks:
|
||||||
- install_go: |
|
- install_go: |
|
||||||
mkdir -p /home/build/sdk
|
mkdir -p /home/build/sdk
|
||||||
curl https://dl.google.com/go/go1.18.9.src.tar.gz | tar -C /home/build/sdk -xzf -
|
curl https://dl.google.com/go/go1.19.11.src.tar.gz | tar -C /home/build/sdk -xzf -
|
||||||
cd /home/build/sdk/go/src
|
cd /home/build/sdk/go/src
|
||||||
./make.bash
|
./make.bash
|
||||||
- test_gio: |
|
- test_gio: |
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type d3d11Context struct {
|
|||||||
width, height int
|
width, height int
|
||||||
}
|
}
|
||||||
|
|
||||||
const debug = false
|
const debugDirectX = false
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
drivers = append(drivers, gpuAPI{
|
drivers = append(drivers, gpuAPI{
|
||||||
@@ -28,7 +28,7 @@ func init() {
|
|||||||
initializer: func(w *window) (context, error) {
|
initializer: func(w *window) (context, error) {
|
||||||
hwnd, _, _ := w.HWND()
|
hwnd, _, _ := w.HWND()
|
||||||
var flags uint32
|
var flags uint32
|
||||||
if debug {
|
if debugDirectX {
|
||||||
flags |= d3d11.CREATE_DEVICE_DEBUG
|
flags |= d3d11.CREATE_DEVICE_DEBUG
|
||||||
}
|
}
|
||||||
dev, ctx, _, err := d3d11.CreateDevice(
|
dev, ctx, _, err := d3d11.CreateDevice(
|
||||||
@@ -122,7 +122,7 @@ func (c *d3d11Context) Release() {
|
|||||||
d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release)
|
d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release)
|
||||||
}
|
}
|
||||||
*c = d3d11Context{}
|
*c = d3d11Context{}
|
||||||
if debug {
|
if debugDirectX {
|
||||||
d3d11.ReportLiveObjects()
|
d3d11.ReportLiveObjects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ func FuzzIME(f *testing.F) {
|
|||||||
f.Add([]byte("20007800002\x02000"))
|
f.Add([]byte("20007800002\x02000"))
|
||||||
f.Add([]byte("200A02000990\x19002\x17\x0200"))
|
f.Add([]byte("200A02000990\x19002\x17\x0200"))
|
||||||
f.Fuzz(func(t *testing.T, cmds []byte) {
|
f.Fuzz(func(t *testing.T, cmds []byte) {
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.WithCollection(gofont.Collection()))
|
||||||
e := new(widget.Editor)
|
e := new(widget.Editor)
|
||||||
e.Focus()
|
e.Focus()
|
||||||
|
|
||||||
|
|||||||
+19
-14
@@ -338,20 +338,6 @@ func (w *window) NewContext() (context, error) {
|
|||||||
func dataDir() (string, error) {
|
func dataDir() (string, error) {
|
||||||
dataDirOnce.Do(func() {
|
dataDirOnce.Do(func() {
|
||||||
dataPath = <-dataDirChan
|
dataPath = <-dataDirChan
|
||||||
// Set XDG_CACHE_HOME to make os.UserCacheDir work.
|
|
||||||
if _, exists := os.LookupEnv("XDG_CACHE_HOME"); !exists {
|
|
||||||
cachePath := filepath.Join(dataPath, "cache")
|
|
||||||
os.Setenv("XDG_CACHE_HOME", cachePath)
|
|
||||||
}
|
|
||||||
// Set XDG_CONFIG_HOME to make os.UserConfigDir work.
|
|
||||||
if _, exists := os.LookupEnv("XDG_CONFIG_HOME"); !exists {
|
|
||||||
cfgPath := filepath.Join(dataPath, "config")
|
|
||||||
os.Setenv("XDG_CONFIG_HOME", cfgPath)
|
|
||||||
}
|
|
||||||
// Set HOME to make os.UserHomeDir work.
|
|
||||||
if _, exists := os.LookupEnv("HOME"); !exists {
|
|
||||||
os.Setenv("HOME", dataPath)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return dataPath, nil
|
return dataPath, nil
|
||||||
}
|
}
|
||||||
@@ -389,6 +375,22 @@ func Java_org_gioui_Gio_runGoMain(env *C.JNIEnv, class C.jclass, jdataDir C.jbyt
|
|||||||
}
|
}
|
||||||
n := C.jni_GetArrayLength(env, jdataDir)
|
n := C.jni_GetArrayLength(env, jdataDir)
|
||||||
dataDir := C.GoStringN((*C.char)(unsafe.Pointer(dirBytes)), n)
|
dataDir := C.GoStringN((*C.char)(unsafe.Pointer(dirBytes)), n)
|
||||||
|
|
||||||
|
// Set XDG_CACHE_HOME to make os.UserCacheDir work.
|
||||||
|
if _, exists := os.LookupEnv("XDG_CACHE_HOME"); !exists {
|
||||||
|
cachePath := filepath.Join(dataDir, "cache")
|
||||||
|
os.Setenv("XDG_CACHE_HOME", cachePath)
|
||||||
|
}
|
||||||
|
// Set XDG_CONFIG_HOME to make os.UserConfigDir work.
|
||||||
|
if _, exists := os.LookupEnv("XDG_CONFIG_HOME"); !exists {
|
||||||
|
cfgPath := filepath.Join(dataDir, "config")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", cfgPath)
|
||||||
|
}
|
||||||
|
// Set HOME to make os.UserHomeDir work.
|
||||||
|
if _, exists := os.LookupEnv("HOME"); !exists {
|
||||||
|
os.Setenv("HOME", dataDir)
|
||||||
|
}
|
||||||
|
|
||||||
dataDirChan <- dataDir
|
dataDirChan <- dataDir
|
||||||
C.jni_ReleaseByteArrayElements(env, jdataDir, dirBytes)
|
C.jni_ReleaseByteArrayElements(env, jdataDir, dirBytes)
|
||||||
|
|
||||||
@@ -1150,6 +1152,7 @@ func (w *window) SetInputHint(mode key.InputHint) {
|
|||||||
TYPE_CLASS_TEXT = 1
|
TYPE_CLASS_TEXT = 1
|
||||||
TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 32
|
TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 32
|
||||||
TYPE_TEXT_VARIATION_URI = 16
|
TYPE_TEXT_VARIATION_URI = 16
|
||||||
|
TYPE_TEXT_VARIATION_PASSWORD = 128
|
||||||
TYPE_TEXT_FLAG_CAP_SENTENCES = 16384
|
TYPE_TEXT_FLAG_CAP_SENTENCES = 16384
|
||||||
TYPE_TEXT_FLAG_AUTO_CORRECT = 32768
|
TYPE_TEXT_FLAG_AUTO_CORRECT = 32768
|
||||||
|
|
||||||
@@ -1173,6 +1176,8 @@ func (w *window) SetInputHint(mode key.InputHint) {
|
|||||||
m = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_URI
|
m = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_URI
|
||||||
case key.HintTelephone:
|
case key.HintTelephone:
|
||||||
m = TYPE_CLASS_PHONE
|
m = TYPE_CLASS_PHONE
|
||||||
|
case key.HintPassword:
|
||||||
|
m = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_PASSWORD
|
||||||
default:
|
default:
|
||||||
m = TYPE_CLASS_TEXT
|
m = TYPE_CLASS_TEXT
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,6 +358,8 @@ func (w *window) keyboard(hint key.InputHint) {
|
|||||||
m = "url"
|
m = "url"
|
||||||
case key.HintTelephone:
|
case key.HintTelephone:
|
||||||
m = "tel"
|
m = "tel"
|
||||||
|
case key.HintPassword:
|
||||||
|
m = "password"
|
||||||
default:
|
default:
|
||||||
m = "text"
|
m = "text"
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -16,6 +16,7 @@ import (
|
|||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/font/gofont"
|
"gioui.org/font/gofont"
|
||||||
"gioui.org/gpu"
|
"gioui.org/gpu"
|
||||||
|
"gioui.org/internal/debug"
|
||||||
"gioui.org/internal/ops"
|
"gioui.org/internal/ops"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
@@ -25,6 +26,7 @@ import (
|
|||||||
"gioui.org/io/system"
|
"gioui.org/io/system"
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
|
"gioui.org/text"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"gioui.org/widget"
|
"gioui.org/widget"
|
||||||
"gioui.org/widget/material"
|
"gioui.org/widget/material"
|
||||||
@@ -136,9 +138,11 @@ type queue struct {
|
|||||||
// Calling NewWindow more than once is not supported on
|
// Calling NewWindow more than once is not supported on
|
||||||
// iOS, Android, WebAssembly.
|
// iOS, Android, WebAssembly.
|
||||||
func NewWindow(options ...Option) *Window {
|
func NewWindow(options ...Option) *Window {
|
||||||
|
debug.Parse()
|
||||||
// Measure decoration height.
|
// Measure decoration height.
|
||||||
deco := new(widget.Decorations)
|
deco := new(widget.Decorations)
|
||||||
theme := material.NewTheme(gofont.Regular())
|
theme := material.NewTheme()
|
||||||
|
theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))
|
||||||
decoStyle := material.Decorations(theme, deco, 0, "")
|
decoStyle := material.Decorations(theme, deco, 0, "")
|
||||||
gtx := layout.Context{
|
gtx := layout.Context{
|
||||||
Ops: new(op.Ops),
|
Ops: new(op.Ops),
|
||||||
|
|||||||
+41
-9
@@ -3,7 +3,9 @@ Package font provides type describing font faces attributes.
|
|||||||
*/
|
*/
|
||||||
package font
|
package font
|
||||||
|
|
||||||
import "github.com/go-text/typesetting/font"
|
import (
|
||||||
|
"github.com/go-text/typesetting/font"
|
||||||
|
)
|
||||||
|
|
||||||
// A FontFace is a Font and a matching Face.
|
// A FontFace is a Font and a matching Face.
|
||||||
type FontFace struct {
|
type FontFace struct {
|
||||||
@@ -20,10 +22,12 @@ type Weight int
|
|||||||
|
|
||||||
// Font specify a particular typeface variant, style and weight.
|
// Font specify a particular typeface variant, style and weight.
|
||||||
type Font struct {
|
type Font struct {
|
||||||
|
// Typeface specifies the name(s) of the the font faces to try. See [Typeface]
|
||||||
|
// for details.
|
||||||
Typeface Typeface
|
Typeface Typeface
|
||||||
Variant Variant
|
// Style specifies the kind of text style.
|
||||||
Style Style
|
Style Style
|
||||||
// Weight is the text weight. If zero, Normal is used instead.
|
// Weight is the text weight.
|
||||||
Weight Weight
|
Weight Weight
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,13 +37,41 @@ type Face interface {
|
|||||||
Face() font.Face
|
Face() font.Face
|
||||||
}
|
}
|
||||||
|
|
||||||
// Typeface identifies a particular typeface design. The empty
|
// Typeface identifies a list of font families to attempt to use for displaying
|
||||||
// string denotes the default typeface.
|
// a string. The syntax is a comma-delimited list of family names. In order to
|
||||||
|
// allow for the remote possibility of needing to express a font family name
|
||||||
|
// containing a comma, name entries may be quoted using either single or double
|
||||||
|
// quotes. Within quotes, a literal quotation mark can be expressed by escaping
|
||||||
|
// it with `\`. A literal backslash may be expressed by escaping it with another
|
||||||
|
// `\`.
|
||||||
|
//
|
||||||
|
// Here's an example Typeface:
|
||||||
|
//
|
||||||
|
// Times New Roman, Georgia, serif
|
||||||
|
//
|
||||||
|
// This is equivalent to the above:
|
||||||
|
//
|
||||||
|
// "Times New Roman", 'Georgia', serif
|
||||||
|
//
|
||||||
|
// Here are some valid uses of escape sequences:
|
||||||
|
//
|
||||||
|
// "Contains a literal \" doublequote", 'Literal \' Singlequote', "\\ Literal backslash", '\\ another'
|
||||||
|
//
|
||||||
|
// This syntax has the happy side effect that most CSS "font-family" rules are
|
||||||
|
// valid Typefaces (without the trailing semicolon).
|
||||||
|
//
|
||||||
|
// Generic CSS font families are supported, and are automatically expanded to lists
|
||||||
|
// of known font families with a matching style. The supported generic families are:
|
||||||
|
//
|
||||||
|
// - fantasy
|
||||||
|
// - math
|
||||||
|
// - emoji
|
||||||
|
// - serif
|
||||||
|
// - sans-serif
|
||||||
|
// - cursive
|
||||||
|
// - monospace
|
||||||
type Typeface string
|
type Typeface string
|
||||||
|
|
||||||
// Variant denotes a typeface variant such as "Mono" or "Smallcaps".
|
|
||||||
type Variant string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Regular Style = iota
|
Regular Style = iota
|
||||||
Italic
|
Italic
|
||||||
|
|||||||
+16
-17
@@ -37,11 +37,11 @@ var (
|
|||||||
|
|
||||||
func loadRegular() {
|
func loadRegular() {
|
||||||
regOnce.Do(func() {
|
regOnce.Do(func() {
|
||||||
face, err := opentype.Parse(goregular.TTF)
|
faces, err := opentype.ParseCollection(goregular.TTF)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to parse font: %v", err))
|
panic(fmt.Errorf("failed to parse font: %v", err))
|
||||||
}
|
}
|
||||||
reg = []font.FontFace{{Font: font.Font{Typeface: "Go"}, Face: face}}
|
reg = faces
|
||||||
collection = append(collection, reg[0])
|
collection = append(collection, reg[0])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -56,17 +56,17 @@ func Regular() []font.FontFace {
|
|||||||
func Collection() []font.FontFace {
|
func Collection() []font.FontFace {
|
||||||
loadRegular()
|
loadRegular()
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
register(font.Font{Style: font.Italic}, goitalic.TTF)
|
register(goitalic.TTF)
|
||||||
register(font.Font{Weight: font.Bold}, gobold.TTF)
|
register(gobold.TTF)
|
||||||
register(font.Font{Style: font.Italic, Weight: font.Bold}, gobolditalic.TTF)
|
register(gobolditalic.TTF)
|
||||||
register(font.Font{Weight: font.Medium}, gomedium.TTF)
|
register(gomedium.TTF)
|
||||||
register(font.Font{Weight: font.Medium, Style: font.Italic}, gomediumitalic.TTF)
|
register(gomediumitalic.TTF)
|
||||||
register(font.Font{Variant: "Mono"}, gomono.TTF)
|
register(gomono.TTF)
|
||||||
register(font.Font{Variant: "Mono", Weight: font.Bold}, gomonobold.TTF)
|
register(gomonobold.TTF)
|
||||||
register(font.Font{Variant: "Mono", Weight: font.Bold, Style: font.Italic}, gomonobolditalic.TTF)
|
register(gomonobolditalic.TTF)
|
||||||
register(font.Font{Variant: "Mono", Style: font.Italic}, gomonoitalic.TTF)
|
register(gomonoitalic.TTF)
|
||||||
register(font.Font{Variant: "Smallcaps"}, gosmallcaps.TTF)
|
register(gosmallcaps.TTF)
|
||||||
register(font.Font{Variant: "Smallcaps", Style: font.Italic}, gosmallcapsitalic.TTF)
|
register(gosmallcapsitalic.TTF)
|
||||||
// Ensure that any outside appends will not reuse the backing store.
|
// Ensure that any outside appends will not reuse the backing store.
|
||||||
n := len(collection)
|
n := len(collection)
|
||||||
collection = collection[:n:n]
|
collection = collection[:n:n]
|
||||||
@@ -74,11 +74,10 @@ func Collection() []font.FontFace {
|
|||||||
return collection
|
return collection
|
||||||
}
|
}
|
||||||
|
|
||||||
func register(fnt font.Font, ttf []byte) {
|
func register(ttf []byte) {
|
||||||
face, err := opentype.Parse(ttf)
|
faces, err := opentype.ParseCollection(ttf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to parse font: %v", err))
|
panic(fmt.Errorf("failed to parse font: %v", err))
|
||||||
}
|
}
|
||||||
fnt.Typeface = "Go"
|
collection = append(collection, faces[0])
|
||||||
collection = append(collection, font.FontFace{Font: fnt, Face: face})
|
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-31
@@ -26,10 +26,8 @@ import (
|
|||||||
// should construct a face for any given font file once, reusing it across different
|
// should construct a face for any given font file once, reusing it across different
|
||||||
// text shapers.
|
// text shapers.
|
||||||
type Face struct {
|
type Face struct {
|
||||||
face font.Font
|
face font.Font
|
||||||
aspect metadata.Aspect
|
font giofont.Font
|
||||||
family string
|
|
||||||
variant string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse constructs a Face from source bytes.
|
// Parse constructs a Face from source bytes.
|
||||||
@@ -38,15 +36,13 @@ func Parse(src []byte) (Face, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Face{}, err
|
return Face{}, err
|
||||||
}
|
}
|
||||||
font, aspect, family, variant, err := parseLoader(ld)
|
font, md, err := parseLoader(ld)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Face{}, fmt.Errorf("failed parsing truetype font: %w", err)
|
return Face{}, fmt.Errorf("failed parsing truetype font: %w", err)
|
||||||
}
|
}
|
||||||
return Face{
|
return Face{
|
||||||
face: font,
|
face: font,
|
||||||
aspect: aspect,
|
font: md,
|
||||||
family: family,
|
|
||||||
variant: variant,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,15 +59,13 @@ func ParseCollection(src []byte) ([]giofont.FontFace, error) {
|
|||||||
}
|
}
|
||||||
out := make([]giofont.FontFace, len(lds))
|
out := make([]giofont.FontFace, len(lds))
|
||||||
for i, ld := range lds {
|
for i, ld := range lds {
|
||||||
face, aspect, family, variant, err := parseLoader(ld)
|
face, md, err := parseLoader(ld)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("reading font %d of collection: %s", i, err)
|
return nil, fmt.Errorf("reading font %d of collection: %s", i, err)
|
||||||
}
|
}
|
||||||
ff := Face{
|
ff := Face{
|
||||||
face: face,
|
face: face,
|
||||||
aspect: aspect,
|
font: md,
|
||||||
family: family,
|
|
||||||
variant: variant,
|
|
||||||
}
|
}
|
||||||
out[i] = giofont.FontFace{
|
out[i] = giofont.FontFace{
|
||||||
Face: ff,
|
Face: ff,
|
||||||
@@ -82,17 +76,32 @@ func ParseCollection(src []byte) ([]giofont.FontFace, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DescriptionToFont(md metadata.Description) giofont.Font {
|
||||||
|
return giofont.Font{
|
||||||
|
Typeface: giofont.Typeface(md.Family),
|
||||||
|
Style: gioStyle(md.Aspect.Style),
|
||||||
|
Weight: gioWeight(md.Aspect.Weight),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FontToDescription(font giofont.Font) metadata.Description {
|
||||||
|
return metadata.Description{
|
||||||
|
Family: string(font.Typeface),
|
||||||
|
Aspect: metadata.Aspect{
|
||||||
|
Style: mdStyle(font.Style),
|
||||||
|
Weight: mdWeight(font.Weight),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// parseLoader parses the contents of the loader into a face and its metadata.
|
// parseLoader parses the contents of the loader into a face and its metadata.
|
||||||
func parseLoader(ld *loader.Loader) (_ font.Font, _ metadata.Aspect, family, variant string, _ error) {
|
func parseLoader(ld *loader.Loader) (font.Font, giofont.Font, error) {
|
||||||
ft, err := fontapi.NewFont(ld)
|
ft, err := fontapi.NewFont(ld)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, metadata.Aspect{}, "", "", err
|
return nil, giofont.Font{}, err
|
||||||
}
|
}
|
||||||
data := metadata.Metadata(ld)
|
data := DescriptionToFont(metadata.Metadata(ld))
|
||||||
if data.IsMonospace {
|
return ft, data, nil
|
||||||
variant = "Mono"
|
|
||||||
}
|
|
||||||
return ft, data.Aspect, data.Family, variant, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Face returns a thread-unsafe wrapper for this Face suitable for use by a single shaper.
|
// Face returns a thread-unsafe wrapper for this Face suitable for use by a single shaper.
|
||||||
@@ -107,16 +116,11 @@ func (f Face) Face() font.Face {
|
|||||||
// BUG(whereswaldon): the only Variant that can be detected automatically is
|
// BUG(whereswaldon): the only Variant that can be detected automatically is
|
||||||
// "Mono".
|
// "Mono".
|
||||||
func (f Face) Font() giofont.Font {
|
func (f Face) Font() giofont.Font {
|
||||||
return giofont.Font{
|
return f.font
|
||||||
Typeface: giofont.Typeface(f.family),
|
|
||||||
Style: f.style(),
|
|
||||||
Weight: f.weight(),
|
|
||||||
Variant: giofont.Variant(f.variant),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Face) style() giofont.Style {
|
func gioStyle(s metadata.Style) giofont.Style {
|
||||||
switch f.aspect.Style {
|
switch s {
|
||||||
case metadata.StyleItalic:
|
case metadata.StyleItalic:
|
||||||
return giofont.Italic
|
return giofont.Italic
|
||||||
case metadata.StyleNormal:
|
case metadata.StyleNormal:
|
||||||
@@ -126,8 +130,19 @@ func (f Face) style() giofont.Style {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Face) weight() giofont.Weight {
|
func mdStyle(g giofont.Style) metadata.Style {
|
||||||
switch f.aspect.Weight {
|
switch g {
|
||||||
|
case giofont.Italic:
|
||||||
|
return metadata.StyleItalic
|
||||||
|
case giofont.Regular:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return metadata.StyleNormal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gioWeight(w metadata.Weight) giofont.Weight {
|
||||||
|
switch w {
|
||||||
case metadata.WeightThin:
|
case metadata.WeightThin:
|
||||||
return giofont.Thin
|
return giofont.Thin
|
||||||
case metadata.WeightExtraLight:
|
case metadata.WeightExtraLight:
|
||||||
@@ -150,3 +165,28 @@ func (f Face) weight() giofont.Weight {
|
|||||||
return giofont.Normal
|
return giofont.Normal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mdWeight(g giofont.Weight) metadata.Weight {
|
||||||
|
switch g {
|
||||||
|
case giofont.Thin:
|
||||||
|
return metadata.WeightThin
|
||||||
|
case giofont.ExtraLight:
|
||||||
|
return metadata.WeightExtraLight
|
||||||
|
case giofont.Light:
|
||||||
|
return metadata.WeightLight
|
||||||
|
case giofont.Normal:
|
||||||
|
return metadata.WeightNormal
|
||||||
|
case giofont.Medium:
|
||||||
|
return metadata.WeightMedium
|
||||||
|
case giofont.SemiBold:
|
||||||
|
return metadata.WeightSemibold
|
||||||
|
case giofont.Bold:
|
||||||
|
return metadata.WeightBold
|
||||||
|
case giofont.ExtraBold:
|
||||||
|
return metadata.WeightExtraBold
|
||||||
|
case giofont.Black:
|
||||||
|
return metadata.WeightBlack
|
||||||
|
default:
|
||||||
|
return metadata.WeightNormal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
module gioui.org
|
module gioui.org
|
||||||
|
|
||||||
go 1.18
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
|
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
|
||||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
|
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
|
||||||
gioui.org/shader v1.0.6
|
gioui.org/shader v1.0.6
|
||||||
github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433
|
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372
|
||||||
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
|
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
|
||||||
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
|
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
|
||||||
golang.org/x/image v0.5.0
|
golang.org/x/image v0.5.0
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJG
|
|||||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||||
gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=
|
gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=
|
||||||
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
||||||
github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433 h1:Pdyvqsfi1QYgFfZa4R8otBOtgO+CGyBDMEG8cM3jwvE=
|
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
|
||||||
github.com/go-text/typesetting v0.0.0-20230602202114-9797aefac433/go.mod h1:KmrpWuSMFcO2yjmyhGpnBGQHSKAoEgMTSSzvLDzCuEA=
|
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
|
||||||
github.com/go-text/typesetting-utils v0.0.0-20230412163830-89e4bcfa3ecc h1:9Kf84pnrmmjdRzZIkomfjowmGUhHs20jkrWYw/I6CYc=
|
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
"gioui.org/op/paint"
|
"gioui.org/op/paint"
|
||||||
|
"gioui.org/text"
|
||||||
"gioui.org/widget/material"
|
"gioui.org/widget/material"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,7 +34,8 @@ func setupBenchmark(b *testing.B) (layout.Context, *headless.Window, *material.T
|
|||||||
Ops: ops,
|
Ops: ops,
|
||||||
Constraints: layout.Exact(sz),
|
Constraints: layout.Exact(sz),
|
||||||
}
|
}
|
||||||
th := material.NewTheme(gofont.Collection())
|
th := material.NewTheme()
|
||||||
|
th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
|
||||||
return gtx, w, th
|
return gtx, w, th
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// Package debug provides general debug feature management for Gio, including
|
||||||
|
// the ability to toggle debug features using the GIODEBUG environment variable.
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
debugVariable = "GIODEBUG"
|
||||||
|
textSubsystem = "text"
|
||||||
|
silentFeature = "silent"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Text controls whether the text subsystem has debug logging enabled.
|
||||||
|
var Text atomic.Bool
|
||||||
|
|
||||||
|
var parseOnce sync.Once
|
||||||
|
|
||||||
|
// Parse processes the current value of GIODEBUG. If it is unset, it does nothing.
|
||||||
|
// Otherwise it process its value, printing usage info the stderr if the value is
|
||||||
|
// not understood. Parse will be automatically invoked when the first application
|
||||||
|
// window is created, allowing applications to manipulate GIODEBUG programmatically
|
||||||
|
// before it is parsed.
|
||||||
|
func Parse() {
|
||||||
|
parseOnce.Do(func() {
|
||||||
|
val, ok := os.LookupEnv(debugVariable)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print := false
|
||||||
|
silent := false
|
||||||
|
for _, part := range strings.Split(val, ",") {
|
||||||
|
switch part {
|
||||||
|
case textSubsystem:
|
||||||
|
Text.Store(true)
|
||||||
|
case silentFeature:
|
||||||
|
silent = true
|
||||||
|
default:
|
||||||
|
print = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if print && !silent {
|
||||||
|
fmt.Fprintf(os.Stderr,
|
||||||
|
`Usage of %s:
|
||||||
|
A comma-delimited list of debug subsystems to enable. Currently recognized systems:
|
||||||
|
|
||||||
|
- %s: text debug info including system font resolution
|
||||||
|
- %s: silence this usage message even if GIODEBUG contains invalid content
|
||||||
|
`, debugVariable, textSubsystem, silentFeature)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -153,6 +153,8 @@ const (
|
|||||||
HintURL
|
HintURL
|
||||||
// HintTelephone hints that telephone number input is expected. It may activate shortcuts for 0-9, "#" and "*".
|
// HintTelephone hints that telephone number input is expected. It may activate shortcuts for 0-9, "#" and "*".
|
||||||
HintTelephone
|
HintTelephone
|
||||||
|
// HintPassword hints that password input is expected. It may disable autocorrection and enable password autofill.
|
||||||
|
HintPassword
|
||||||
)
|
)
|
||||||
|
|
||||||
// State is the state of a key during an event.
|
// State is the state of a key during an event.
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tokenKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
tokenStr tokenKind = iota
|
||||||
|
tokenComma
|
||||||
|
tokenEOF
|
||||||
|
)
|
||||||
|
|
||||||
|
type token struct {
|
||||||
|
kind tokenKind
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t token) String() string {
|
||||||
|
switch t.kind {
|
||||||
|
case tokenStr:
|
||||||
|
return t.value
|
||||||
|
case tokenComma:
|
||||||
|
return ","
|
||||||
|
case tokenEOF:
|
||||||
|
return "EOF"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type lexState func(*lexer) lexState
|
||||||
|
|
||||||
|
func lexText(l *lexer) lexState {
|
||||||
|
for {
|
||||||
|
switch r := l.next(); {
|
||||||
|
case r == -1:
|
||||||
|
l.ignore()
|
||||||
|
l.emit(tokenEOF)
|
||||||
|
return nil
|
||||||
|
case unicode.IsSpace(r):
|
||||||
|
continue
|
||||||
|
case r == ',':
|
||||||
|
l.ignore()
|
||||||
|
l.emit(tokenComma)
|
||||||
|
case r == '"':
|
||||||
|
l.ignore()
|
||||||
|
return lexDquote
|
||||||
|
case r == '\'':
|
||||||
|
l.ignore()
|
||||||
|
return lexSquote
|
||||||
|
default:
|
||||||
|
return lexBareStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lexBareStr(l *lexer) lexState {
|
||||||
|
defer l.emitProcessed(tokenStr, func(s string) (string, error) {
|
||||||
|
return strings.TrimSpace(s), nil
|
||||||
|
})
|
||||||
|
for {
|
||||||
|
if strings.HasPrefix(l.input[l.pos:], `,`) {
|
||||||
|
return lexText
|
||||||
|
}
|
||||||
|
switch r := l.next(); {
|
||||||
|
case r == -1:
|
||||||
|
return lexText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lexDquote(l *lexer) lexState {
|
||||||
|
return lexQuote(l, `"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lexSquote(l *lexer) lexState {
|
||||||
|
return lexQuote(l, `'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unescape(s string, quote rune) (string, error) {
|
||||||
|
var b strings.Builder
|
||||||
|
hitNonSpace := false
|
||||||
|
var wb strings.Builder
|
||||||
|
for i := 0; i < len(s); {
|
||||||
|
r, sz := utf8.DecodeRuneInString(s[i:])
|
||||||
|
i += sz
|
||||||
|
if unicode.IsSpace(r) {
|
||||||
|
if !hitNonSpace {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wb.WriteRune(r)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hitNonSpace = true
|
||||||
|
// If we get here, we're not looking at whitespace.
|
||||||
|
// Insert any buffered up whitespace characters from
|
||||||
|
// the gap between words.
|
||||||
|
b.WriteString(wb.String())
|
||||||
|
wb.Reset()
|
||||||
|
if r == '\\' {
|
||||||
|
r, sz := utf8.DecodeRuneInString(s[i:])
|
||||||
|
i += sz
|
||||||
|
switch r {
|
||||||
|
case '\\', quote:
|
||||||
|
b.WriteRune(r)
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("illegal escape sequence \\%c", r)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lexQuote(l *lexer, mark string) lexState {
|
||||||
|
escaping := false
|
||||||
|
for {
|
||||||
|
if isQuote := strings.HasPrefix(l.input[l.pos:], mark); isQuote && !escaping {
|
||||||
|
err := l.emitProcessed(tokenStr, func(s string) (string, error) {
|
||||||
|
return unescape(s, []rune(mark)[0])
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
l.err = err
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
l.next()
|
||||||
|
l.ignore()
|
||||||
|
return lexText
|
||||||
|
}
|
||||||
|
escaped := escaping
|
||||||
|
switch r := l.next(); {
|
||||||
|
case r == -1:
|
||||||
|
l.err = fmt.Errorf("unexpected EOF while parsing %s-quoted family", mark)
|
||||||
|
return lexText
|
||||||
|
case r == '\\':
|
||||||
|
if !escaped {
|
||||||
|
escaping = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if escaped {
|
||||||
|
escaping = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type lexer struct {
|
||||||
|
input string
|
||||||
|
pos int
|
||||||
|
tokens []token
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lexer) ignore() {
|
||||||
|
l.input = l.input[l.pos:]
|
||||||
|
l.pos = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// next decodes the next rune in the input and returns it.
|
||||||
|
func (l *lexer) next() int32 {
|
||||||
|
if l.pos >= len(l.input) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
|
||||||
|
l.pos += w
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit adds a token of the given kind.
|
||||||
|
func (l *lexer) emit(t tokenKind) {
|
||||||
|
l.emitProcessed(t, func(s string) (string, error) { return s, nil })
|
||||||
|
}
|
||||||
|
|
||||||
|
// emitProcessed adds a token of the given kind, but transforms its value
|
||||||
|
// with the provided closure first.
|
||||||
|
func (l *lexer) emitProcessed(t tokenKind, f func(string) (string, error)) error {
|
||||||
|
val, err := f(l.input[:l.pos])
|
||||||
|
l.tokens = append(l.tokens, token{
|
||||||
|
kind: t,
|
||||||
|
value: val,
|
||||||
|
})
|
||||||
|
l.ignore()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// run executes the lexer on the given input.
|
||||||
|
func (l *lexer) run(input string) ([]token, error) {
|
||||||
|
l.input = input
|
||||||
|
l.tokens = l.tokens[:0]
|
||||||
|
l.pos = 0
|
||||||
|
for state := lexText; state != nil; {
|
||||||
|
state = state(l)
|
||||||
|
}
|
||||||
|
return l.tokens, l.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parser implements a simple recursive descent parser for font family fallback
|
||||||
|
// expressions.
|
||||||
|
type parser struct {
|
||||||
|
faces []string
|
||||||
|
lexer lexer
|
||||||
|
tokens []token
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the provided rule and return the extracted font families. The returned families
|
||||||
|
// are valid only until the next call to parse. If parsing fails, an error describing the
|
||||||
|
// failure is returned instead.
|
||||||
|
func (p *parser) parse(rule string) ([]string, error) {
|
||||||
|
var err error
|
||||||
|
p.tokens, err = p.lexer.run(rule)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.faces = p.faces[:0]
|
||||||
|
return p.faces, p.parseList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse implements the production:
|
||||||
|
//
|
||||||
|
// LIST ::= <FACE> <COMMA> <LIST> | <FACE>
|
||||||
|
func (p *parser) parseList() error {
|
||||||
|
if len(p.tokens) < 0 {
|
||||||
|
return fmt.Errorf("expected family name, got EOF")
|
||||||
|
}
|
||||||
|
if head := p.tokens[0]; head.kind != tokenStr {
|
||||||
|
return fmt.Errorf("expected family name, got %s", head)
|
||||||
|
} else {
|
||||||
|
p.faces = append(p.faces, head.value)
|
||||||
|
p.tokens = p.tokens[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch head := p.tokens[0]; head.kind {
|
||||||
|
case tokenEOF:
|
||||||
|
return nil
|
||||||
|
case tokenComma:
|
||||||
|
p.tokens = p.tokens[1:]
|
||||||
|
return p.parseList()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected token %s", head)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParser(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
variantName string
|
||||||
|
input string
|
||||||
|
}
|
||||||
|
type testcase struct {
|
||||||
|
name string
|
||||||
|
inputs []scenario
|
||||||
|
expected []string
|
||||||
|
shouldErr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []testcase{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
inputs: []scenario{
|
||||||
|
{
|
||||||
|
variantName: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comma failure",
|
||||||
|
inputs: []scenario{
|
||||||
|
{
|
||||||
|
variantName: "bare single",
|
||||||
|
input: ",",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "bare multiple",
|
||||||
|
input: ",, ,,",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comma success",
|
||||||
|
inputs: []scenario{
|
||||||
|
{
|
||||||
|
variantName: "squote",
|
||||||
|
input: "','",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "dquote",
|
||||||
|
input: `","`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []string{","},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comma success multiple",
|
||||||
|
inputs: []scenario{
|
||||||
|
{
|
||||||
|
variantName: "squote",
|
||||||
|
input: "',,', ',,'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "dquote",
|
||||||
|
input: `",,", ",,"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []string{",,", ",,"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "backslashes",
|
||||||
|
inputs: []scenario{
|
||||||
|
{
|
||||||
|
variantName: "bare",
|
||||||
|
input: `\font\\`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "dquote",
|
||||||
|
input: `"\\font\\\\"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "squote",
|
||||||
|
input: `'\\font\\\\'`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []string{`\font\\`},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid backslashes",
|
||||||
|
inputs: []scenario{
|
||||||
|
{
|
||||||
|
variantName: "dquote",
|
||||||
|
input: `"\\""`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "squote",
|
||||||
|
input: `'\\''`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too many quotes",
|
||||||
|
inputs: []scenario{
|
||||||
|
{
|
||||||
|
variantName: "dquote",
|
||||||
|
input: `"""`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "squote",
|
||||||
|
input: `'''`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "serif serif's serif\"s",
|
||||||
|
inputs: []scenario{
|
||||||
|
{
|
||||||
|
variantName: "bare",
|
||||||
|
input: `serif, serif's, serif"s`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "squote",
|
||||||
|
input: `'serif', 'serif\'s', 'serif"s'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "dquote",
|
||||||
|
input: `"serif", "serif's", "serif\"s"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []string{"serif", `serif's`, `serif"s`},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex list",
|
||||||
|
inputs: []scenario{
|
||||||
|
{
|
||||||
|
variantName: "bare",
|
||||||
|
input: `Times New Roman, Georgia Common, Helvetica Neue, serif`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "squote",
|
||||||
|
input: `'Times New Roman', 'Georgia Common', 'Helvetica Neue', 'serif'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "dquote",
|
||||||
|
input: `"Times New Roman", "Georgia Common", "Helvetica Neue", "serif"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "mixed",
|
||||||
|
input: `Times New Roman, "Georgia Common", 'Helvetica Neue', "serif"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variantName: "mixed with weird spacing",
|
||||||
|
input: `Times New Roman ,"Georgia Common" , 'Helvetica Neue' ,"serif"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: []string{"Times New Roman", "Georgia Common", "Helvetica Neue", "serif"},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var p parser
|
||||||
|
for _, scen := range tc.inputs {
|
||||||
|
t.Run(scen.variantName, func(t *testing.T) {
|
||||||
|
actual, err := p.parse(scen.input)
|
||||||
|
if (err != nil) != tc.shouldErr {
|
||||||
|
t.Errorf("unexpected error state: %v", err)
|
||||||
|
}
|
||||||
|
if !slices.Equal(tc.expected, actual) {
|
||||||
|
t.Errorf("expected\n%q\ngot\n%q", tc.expected, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+234
-195
@@ -6,12 +6,15 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"sort"
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/go-text/typesetting/di"
|
"github.com/go-text/typesetting/di"
|
||||||
"github.com/go-text/typesetting/font"
|
"github.com/go-text/typesetting/font"
|
||||||
|
"github.com/go-text/typesetting/fontscan"
|
||||||
"github.com/go-text/typesetting/language"
|
"github.com/go-text/typesetting/language"
|
||||||
"github.com/go-text/typesetting/opentype/api"
|
"github.com/go-text/typesetting/opentype/api"
|
||||||
|
"github.com/go-text/typesetting/opentype/api/metadata"
|
||||||
"github.com/go-text/typesetting/shaping"
|
"github.com/go-text/typesetting/shaping"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
"golang.org/x/image/math/fixed"
|
"golang.org/x/image/math/fixed"
|
||||||
@@ -19,6 +22,8 @@ import (
|
|||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
giofont "gioui.org/font"
|
giofont "gioui.org/font"
|
||||||
|
"gioui.org/font/opentype"
|
||||||
|
"gioui.org/internal/debug"
|
||||||
"gioui.org/io/system"
|
"gioui.org/io/system"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
@@ -74,8 +79,9 @@ type line struct {
|
|||||||
// descent is the height below the baseline, including
|
// descent is the height below the baseline, including
|
||||||
// the line gap.
|
// the line gap.
|
||||||
descent fixed.Int26_6
|
descent fixed.Int26_6
|
||||||
// bounds is the visible bounds of the line.
|
// lineHeight captures the gap that should exist between the baseline of this
|
||||||
bounds fixed.Rectangle26_6
|
// line and the previous (if any).
|
||||||
|
lineHeight fixed.Int26_6
|
||||||
// direction is the dominant direction of the line. This direction will be
|
// direction is the dominant direction of the line. This direction will be
|
||||||
// used to align the text content of the line, but may not match the actual
|
// used to align the text content of the line, but may not match the actual
|
||||||
// direction of the runs of text within the line (such as an RTL sentence
|
// direction of the runs of text within the line (such as an RTL sentence
|
||||||
@@ -149,112 +155,18 @@ type runLayout struct {
|
|||||||
truncator bool
|
truncator bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// faceOrderer chooses the order in which faces should be applied to text.
|
|
||||||
type faceOrderer struct {
|
|
||||||
def giofont.Font
|
|
||||||
faceScratch []font.Face
|
|
||||||
fontDefaultOrder map[giofont.Font]int
|
|
||||||
defaultOrderedFonts []giofont.Font
|
|
||||||
faces map[giofont.Font]font.Face
|
|
||||||
faceToIndex map[font.Face]int
|
|
||||||
fonts []giofont.Font
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *faceOrderer) insert(fnt giofont.Font, face font.Face) {
|
|
||||||
if len(f.fonts) == 0 {
|
|
||||||
f.def = fnt
|
|
||||||
}
|
|
||||||
if f.fontDefaultOrder == nil {
|
|
||||||
f.fontDefaultOrder = make(map[giofont.Font]int)
|
|
||||||
}
|
|
||||||
if f.faces == nil {
|
|
||||||
f.faces = make(map[giofont.Font]font.Face)
|
|
||||||
f.faceToIndex = make(map[font.Face]int)
|
|
||||||
}
|
|
||||||
f.fontDefaultOrder[fnt] = len(f.faceScratch)
|
|
||||||
f.defaultOrderedFonts = append(f.defaultOrderedFonts, fnt)
|
|
||||||
f.faceScratch = append(f.faceScratch, face)
|
|
||||||
f.fonts = append(f.fonts, fnt)
|
|
||||||
f.faces[fnt] = face
|
|
||||||
f.faceToIndex[face] = f.fontDefaultOrder[fnt]
|
|
||||||
}
|
|
||||||
|
|
||||||
// resetFontOrder restores the fonts to a predictable order. It should be invoked
|
|
||||||
// before any operation searching the fonts.
|
|
||||||
func (c *faceOrderer) resetFontOrder() {
|
|
||||||
copy(c.fonts, c.defaultOrderedFonts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *faceOrderer) indexFor(face font.Face) int {
|
|
||||||
return c.faceToIndex[face]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *faceOrderer) faceFor(idx int) font.Face {
|
|
||||||
if idx < len(c.defaultOrderedFonts) {
|
|
||||||
return c.faces[c.defaultOrderedFonts[idx]]
|
|
||||||
}
|
|
||||||
panic("face index not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(whereswaldon): this function could sort all faces by appropriateness for the
|
|
||||||
// given font characteristics. This would ensure that (if possible) text using a
|
|
||||||
// fallback font would select similar weights and emphases to the primary font.
|
|
||||||
func (c *faceOrderer) sortedFacesForStyle(font giofont.Font) []font.Face {
|
|
||||||
c.resetFontOrder()
|
|
||||||
primary, ok := c.fontForStyle(font)
|
|
||||||
if !ok {
|
|
||||||
font.Typeface = c.def.Typeface
|
|
||||||
primary, ok = c.fontForStyle(font)
|
|
||||||
if !ok {
|
|
||||||
primary = c.def
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c.sorted(primary)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fontForStyle returns the closest existing font to the requested font within the
|
|
||||||
// same typeface.
|
|
||||||
func (c *faceOrderer) fontForStyle(font giofont.Font) (giofont.Font, bool) {
|
|
||||||
if closest, ok := closestFont(font, c.fonts); ok {
|
|
||||||
return closest, true
|
|
||||||
}
|
|
||||||
font.Style = giofont.Regular
|
|
||||||
if closest, ok := closestFont(font, c.fonts); ok {
|
|
||||||
return closest, true
|
|
||||||
}
|
|
||||||
return font, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// faces returns a slice of faces with primary as the first element and
|
|
||||||
// the remaining faces ordered by insertion order.
|
|
||||||
func (f *faceOrderer) sorted(primary giofont.Font) []font.Face {
|
|
||||||
// If we find primary, put it first, and omit it from the below sort.
|
|
||||||
lowest := 0
|
|
||||||
for i := range f.fonts {
|
|
||||||
if f.fonts[i] == primary {
|
|
||||||
if i != 0 {
|
|
||||||
f.fonts[0], f.fonts[i] = f.fonts[i], f.fonts[0]
|
|
||||||
}
|
|
||||||
lowest = 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sorting := f.fonts[lowest:]
|
|
||||||
sort.Slice(sorting, func(i, j int) bool {
|
|
||||||
a := sorting[i]
|
|
||||||
b := sorting[j]
|
|
||||||
return f.fontDefaultOrder[a] < f.fontDefaultOrder[b]
|
|
||||||
})
|
|
||||||
for i, font := range f.fonts {
|
|
||||||
f.faceScratch[i] = f.faces[font]
|
|
||||||
}
|
|
||||||
return f.faceScratch
|
|
||||||
}
|
|
||||||
|
|
||||||
// shaperImpl implements the shaping and line-wrapping of opentype fonts.
|
// shaperImpl implements the shaping and line-wrapping of opentype fonts.
|
||||||
type shaperImpl struct {
|
type shaperImpl struct {
|
||||||
// Fields for tracking fonts/faces.
|
// Fields for tracking fonts/faces.
|
||||||
orderer faceOrderer
|
fontMap *fontscan.FontMap
|
||||||
|
faces []font.Face
|
||||||
|
faceToIndex map[font.Font]int
|
||||||
|
faceMeta []giofont.Font
|
||||||
|
defaultFaces []string
|
||||||
|
logger interface {
|
||||||
|
Printf(format string, args ...any)
|
||||||
|
}
|
||||||
|
parser parser
|
||||||
|
|
||||||
// Shaping and wrapping state.
|
// Shaping and wrapping state.
|
||||||
shaper shaping.HarfbuzzShaper
|
shaper shaping.HarfbuzzShaper
|
||||||
@@ -271,11 +183,60 @@ type shaperImpl struct {
|
|||||||
bitmapGlyphCache bitmapCache
|
bitmapGlyphCache bitmapCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// debugLogger only logs messages if debug.Text is true.
|
||||||
|
type debugLogger struct {
|
||||||
|
*log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDebugLogger() debugLogger {
|
||||||
|
return debugLogger{Logger: log.New(log.Writer(), "[text] ", log.Default().Flags())}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d debugLogger) Printf(format string, args ...any) {
|
||||||
|
if debug.Text.Load() {
|
||||||
|
d.Logger.Printf(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
|
||||||
|
var shaper shaperImpl
|
||||||
|
shaper.logger = newDebugLogger()
|
||||||
|
shaper.fontMap = fontscan.NewFontMap(shaper.logger)
|
||||||
|
shaper.faceToIndex = make(map[font.Font]int)
|
||||||
|
if systemFonts {
|
||||||
|
str, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
shaper.logger.Printf("failed resolving font cache dir: %v", err)
|
||||||
|
shaper.logger.Printf("skipping system font load")
|
||||||
|
}
|
||||||
|
if err := shaper.fontMap.UseSystemFonts(str); err != nil {
|
||||||
|
shaper.logger.Printf("failed loading system fonts: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, f := range collection {
|
||||||
|
shaper.Load(f)
|
||||||
|
shaper.defaultFaces = append(shaper.defaultFaces, string(f.Font.Typeface))
|
||||||
|
}
|
||||||
|
shaper.shaper.SetFontCacheSize(32)
|
||||||
|
return &shaper
|
||||||
|
}
|
||||||
|
|
||||||
// Load registers the provided FontFace with the shaper, if it is compatible.
|
// Load registers the provided FontFace with the shaper, if it is compatible.
|
||||||
// It returns whether the face is now available for use. FontFaces are prioritized
|
// It returns whether the face is now available for use. FontFaces are prioritized
|
||||||
// in the order in which they are loaded, with the first face being the default.
|
// in the order in which they are loaded, with the first face being the default.
|
||||||
func (s *shaperImpl) Load(f FontFace) {
|
func (s *shaperImpl) Load(f FontFace) {
|
||||||
s.orderer.insert(f.Font, f.Face.Face())
|
s.fontMap.AddFace(f.Face.Face(), opentype.FontToDescription(f.Font))
|
||||||
|
s.addFace(f.Face.Face(), f.Font)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *shaperImpl) addFace(f font.Face, md giofont.Font) {
|
||||||
|
if _, ok := s.faceToIndex[f.Font]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
idx := len(s.faces)
|
||||||
|
s.faceToIndex[f.Font] = idx
|
||||||
|
s.faces = append(s.faces, f)
|
||||||
|
s.faceMeta = append(s.faceMeta, md)
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitByScript divides the inputs into new, smaller inputs on script boundaries
|
// splitByScript divides the inputs into new, smaller inputs on script boundaries
|
||||||
@@ -359,9 +320,26 @@ func (s *shaperImpl) splitBidi(input shaping.Input) []shaping.Input {
|
|||||||
return splitInputs
|
return splitInputs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveFace allows shaperImpl to implement shaping.FontMap, wrapping its fontMap
|
||||||
|
// field and ensuring that any faces loaded as part of the search are registered with
|
||||||
|
// ids so that they can be referred to by a GlyphID.
|
||||||
|
func (s *shaperImpl) ResolveFace(r rune) font.Face {
|
||||||
|
face := s.fontMap.ResolveFace(r)
|
||||||
|
if face != nil {
|
||||||
|
family, aspect := s.fontMap.FontMetadata(face.Font)
|
||||||
|
md := opentype.DescriptionToFont(metadata.Description{
|
||||||
|
Family: family,
|
||||||
|
Aspect: aspect,
|
||||||
|
})
|
||||||
|
s.addFace(face, md)
|
||||||
|
return face
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// splitByFaces divides the inputs by font coverage in the provided faces. It will use the slice provided in buf
|
// splitByFaces divides the inputs by font coverage in the provided faces. It will use the slice provided in buf
|
||||||
// as the backing storage of the returned slice if buf is non-nil.
|
// as the backing storage of the returned slice if buf is non-nil.
|
||||||
func (s *shaperImpl) splitByFaces(inputs []shaping.Input, faces []font.Face, buf []shaping.Input) []shaping.Input {
|
func (s *shaperImpl) splitByFaces(inputs []shaping.Input, buf []shaping.Input) []shaping.Input {
|
||||||
var split []shaping.Input
|
var split []shaping.Input
|
||||||
if buf == nil {
|
if buf == nil {
|
||||||
split = make([]shaping.Input, 0, len(inputs))
|
split = make([]shaping.Input, 0, len(inputs))
|
||||||
@@ -369,34 +347,78 @@ func (s *shaperImpl) splitByFaces(inputs []shaping.Input, faces []font.Face, buf
|
|||||||
split = buf
|
split = buf
|
||||||
}
|
}
|
||||||
for _, input := range inputs {
|
for _, input := range inputs {
|
||||||
split = append(split, shaping.SplitByFontGlyphs(input, faces)...)
|
split = append(split, shaping.SplitByFace(input, s)...)
|
||||||
}
|
}
|
||||||
return split
|
return split
|
||||||
}
|
}
|
||||||
|
|
||||||
// shapeText invokes the text shaper and returns the raw text data in the shaper's native
|
// shapeText invokes the text shaper and returns the raw text data in the shaper's native
|
||||||
// format. It does not wrap lines.
|
// format. It does not wrap lines.
|
||||||
func (s *shaperImpl) shapeText(faces []font.Face, ppem fixed.Int26_6, lc system.Locale, txt []rune) []shaping.Output {
|
func (s *shaperImpl) shapeText(ppem fixed.Int26_6, lc system.Locale, txt []rune) []shaping.Output {
|
||||||
if len(faces) < 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
lcfg := langConfig{
|
lcfg := langConfig{
|
||||||
Language: language.NewLanguage(lc.Language),
|
Language: language.NewLanguage(lc.Language),
|
||||||
Direction: mapDirection(lc.Direction),
|
Direction: mapDirection(lc.Direction),
|
||||||
}
|
}
|
||||||
// Create an initial input.
|
// Create an initial input.
|
||||||
input := toInput(faces[0], ppem, lcfg, txt)
|
input := toInput(nil, ppem, lcfg, txt)
|
||||||
|
if input.RunStart == input.RunEnd && len(s.faces) > 0 {
|
||||||
|
// Give the empty string a face. This is a necessary special case because
|
||||||
|
// the face splitting process works by resolving faces for each rune, and
|
||||||
|
// the empty string contains no runes.
|
||||||
|
input.Face = s.faces[0]
|
||||||
|
}
|
||||||
// Break input on font glyph coverage.
|
// Break input on font glyph coverage.
|
||||||
inputs := s.splitBidi(input)
|
inputs := s.splitBidi(input)
|
||||||
inputs = s.splitByFaces(inputs, faces, s.splitScratch1[:0])
|
inputs = s.splitByFaces(inputs, s.splitScratch1[:0])
|
||||||
inputs = splitByScript(inputs, lcfg.Direction, s.splitScratch2[:0])
|
inputs = splitByScript(inputs, lcfg.Direction, s.splitScratch2[:0])
|
||||||
// Shape all inputs.
|
// Shape all inputs.
|
||||||
if needed := len(inputs) - len(s.outScratchBuf); needed > 0 {
|
if needed := len(inputs) - len(s.outScratchBuf); needed > 0 {
|
||||||
s.outScratchBuf = slices.Grow(s.outScratchBuf, needed)
|
s.outScratchBuf = slices.Grow(s.outScratchBuf, needed)
|
||||||
}
|
}
|
||||||
s.outScratchBuf = s.outScratchBuf[:len(inputs)]
|
s.outScratchBuf = s.outScratchBuf[:0]
|
||||||
for i := range inputs {
|
for _, input := range inputs {
|
||||||
s.outScratchBuf[i] = s.shaper.Shape(inputs[i])
|
if input.Face != nil {
|
||||||
|
s.outScratchBuf = append(s.outScratchBuf, s.shaper.Shape(input))
|
||||||
|
} else {
|
||||||
|
s.outScratchBuf = append(s.outScratchBuf, shaping.Output{
|
||||||
|
// Use the text size as the advance of the entire fake run so that
|
||||||
|
// it doesn't occupy zero space.
|
||||||
|
Advance: input.Size,
|
||||||
|
Size: input.Size,
|
||||||
|
Glyphs: []shaping.Glyph{
|
||||||
|
{
|
||||||
|
Width: input.Size,
|
||||||
|
Height: input.Size,
|
||||||
|
XBearing: 0,
|
||||||
|
YBearing: 0,
|
||||||
|
XAdvance: input.Size,
|
||||||
|
YAdvance: input.Size,
|
||||||
|
XOffset: 0,
|
||||||
|
YOffset: 0,
|
||||||
|
ClusterIndex: input.RunStart,
|
||||||
|
RuneCount: input.RunEnd - input.RunStart,
|
||||||
|
GlyphCount: 1,
|
||||||
|
GlyphID: 0,
|
||||||
|
Mask: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LineBounds: shaping.Bounds{
|
||||||
|
Ascent: input.Size,
|
||||||
|
Descent: 0,
|
||||||
|
Gap: 0,
|
||||||
|
},
|
||||||
|
GlyphBounds: shaping.Bounds{
|
||||||
|
Ascent: input.Size,
|
||||||
|
Descent: 0,
|
||||||
|
Gap: 0,
|
||||||
|
},
|
||||||
|
Direction: input.Direction,
|
||||||
|
Runes: shaping.Range{
|
||||||
|
Offset: input.RunStart,
|
||||||
|
Count: input.RunEnd - input.RunStart,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return s.outScratchBuf
|
return s.outScratchBuf
|
||||||
}
|
}
|
||||||
@@ -413,22 +435,35 @@ func wrapPolicyToGoText(p WrapPolicy) shaping.LineBreakPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format.
|
// shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format.
|
||||||
func (s *shaperImpl) shapeAndWrapText(faces []font.Face, params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
|
func (s *shaperImpl) shapeAndWrapText(params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
|
||||||
wc := shaping.WrapConfig{
|
wc := shaping.WrapConfig{
|
||||||
TruncateAfterLines: params.MaxLines,
|
TruncateAfterLines: params.MaxLines,
|
||||||
TextContinues: params.forceTruncate,
|
TextContinues: params.forceTruncate,
|
||||||
BreakPolicy: wrapPolicyToGoText(params.WrapPolicy),
|
BreakPolicy: wrapPolicyToGoText(params.WrapPolicy),
|
||||||
}
|
}
|
||||||
|
families := s.defaultFaces
|
||||||
|
if params.Font.Typeface != "" {
|
||||||
|
parsed, err := s.parser.parse(string(params.Font.Typeface))
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Printf("Unable to parse typeface %q: %v", params.Font.Typeface, err)
|
||||||
|
} else {
|
||||||
|
families = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.fontMap.SetQuery(fontscan.Query{
|
||||||
|
Families: families,
|
||||||
|
Aspect: opentype.FontToDescription(params.Font).Aspect,
|
||||||
|
})
|
||||||
if wc.TruncateAfterLines > 0 {
|
if wc.TruncateAfterLines > 0 {
|
||||||
if len(params.Truncator) == 0 {
|
if len(params.Truncator) == 0 {
|
||||||
params.Truncator = "…"
|
params.Truncator = "…"
|
||||||
}
|
}
|
||||||
// We only permit a single run as the truncator, regardless of whether more were generated.
|
// We only permit a single run as the truncator, regardless of whether more were generated.
|
||||||
// Just use the first one.
|
// Just use the first one.
|
||||||
wc.Truncator = s.shapeText(faces, params.PxPerEm, params.Locale, []rune(params.Truncator))[0]
|
wc.Truncator = s.shapeText(params.PxPerEm, params.Locale, []rune(params.Truncator))[0]
|
||||||
}
|
}
|
||||||
// Wrap outputs into lines.
|
// Wrap outputs into lines.
|
||||||
return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, shaping.NewSliceIterator(s.shapeText(faces, params.PxPerEm, params.Locale, txt)))
|
return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, shaping.NewSliceIterator(s.shapeText(params.PxPerEm, params.Locale, txt)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// replaceControlCharacters replaces problematic unicode
|
// replaceControlCharacters replaces problematic unicode
|
||||||
@@ -471,25 +506,50 @@ func (s *shaperImpl) Layout(params Parameters, txt io.RuneReader) document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func calculateYOffsets(lines []line) {
|
func calculateYOffsets(lines []line) {
|
||||||
currentY := 0
|
if len(lines) < 1 {
|
||||||
prevDesc := fixed.I(0)
|
return
|
||||||
|
}
|
||||||
|
// Ceil the first value to ensure that we don't baseline it too close to the top of the
|
||||||
|
// viewport and cut off the top pixel.
|
||||||
|
currentY := lines[0].ascent.Ceil()
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
ascent, descent := lines[i].ascent, lines[i].descent
|
if i > 0 {
|
||||||
currentY += (prevDesc + ascent).Ceil()
|
currentY += lines[i].lineHeight.Round()
|
||||||
|
}
|
||||||
lines[i].yOffset = currentY
|
lines[i].yOffset = currentY
|
||||||
prevDesc = descent
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LayoutRunes shapes and wraps the text, and returns the result in Gio's shaped text format.
|
// LayoutRunes shapes and wraps the text, and returns the result in Gio's shaped text format.
|
||||||
func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
|
func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
|
||||||
hasNewline := len(txt) > 0 && txt[len(txt)-1] == '\n'
|
hasNewline := len(txt) > 0 && txt[len(txt)-1] == '\n'
|
||||||
|
var ls []shaping.Line
|
||||||
|
var truncated int
|
||||||
if hasNewline {
|
if hasNewline {
|
||||||
txt = txt[:len(txt)-1]
|
txt = txt[:len(txt)-1]
|
||||||
}
|
}
|
||||||
ls, truncated := s.shapeAndWrapText(s.orderer.sortedFacesForStyle(params.Font), params, replaceControlCharacters(txt))
|
truncatedNewline := false
|
||||||
|
if hasNewline && len(txt) == 0 {
|
||||||
|
params.forceTruncate = false
|
||||||
|
// If we only have a newline, shape a space to get line metrics.
|
||||||
|
ls, truncated = s.shapeAndWrapText(params, []rune{' '})
|
||||||
|
if truncated > 0 {
|
||||||
|
// Our space was truncated. Since our space didn't exist in any meaningful
|
||||||
|
// capacity, ensure the truncated count is zeroed out.
|
||||||
|
truncated = 0
|
||||||
|
truncatedNewline = true
|
||||||
|
} else {
|
||||||
|
// We shaped a space to get proper line metrics, but we need to drop
|
||||||
|
// the rune/glyph info since it isn't actually part of the text.
|
||||||
|
ls[0][0].Glyphs = ls[0][0].Glyphs[:0]
|
||||||
|
ls[0][0].Advance = 0
|
||||||
|
ls[0][0].Runes.Count = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt))
|
||||||
|
}
|
||||||
|
|
||||||
didTruncate := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls))
|
didTruncate := truncated > 0 || truncatedNewline || (params.forceTruncate && params.MaxLines == len(ls))
|
||||||
|
|
||||||
if didTruncate && hasNewline {
|
if didTruncate && hasNewline {
|
||||||
// We've truncated the newline, since it was at the end and we've truncated some amount of runes
|
// We've truncated the newline, since it was at the end and we've truncated some amount of runes
|
||||||
@@ -499,8 +559,12 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
|
|||||||
}
|
}
|
||||||
// Convert to Lines.
|
// Convert to Lines.
|
||||||
textLines := make([]line, len(ls))
|
textLines := make([]line, len(ls))
|
||||||
|
maxHeight := fixed.Int26_6(0)
|
||||||
for i := range ls {
|
for i := range ls {
|
||||||
otLine := toLine(&s.orderer, ls[i], params.Locale.Direction)
|
otLine := toLine(s.faceToIndex, ls[i], params.Locale.Direction)
|
||||||
|
if otLine.lineHeight > maxHeight {
|
||||||
|
maxHeight = otLine.lineHeight
|
||||||
|
}
|
||||||
isFinalLine := i == len(ls)-1
|
isFinalLine := i == len(ls)-1
|
||||||
if isFinalLine && hasNewline {
|
if isFinalLine && hasNewline {
|
||||||
// If there was a trailing newline update the rune counts to include
|
// If there was a trailing newline update the rune counts to include
|
||||||
@@ -548,6 +612,17 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
|
|||||||
}
|
}
|
||||||
textLines[i] = otLine
|
textLines[i] = otLine
|
||||||
}
|
}
|
||||||
|
if params.LineHeight != 0 {
|
||||||
|
maxHeight = params.LineHeight
|
||||||
|
}
|
||||||
|
if params.LineHeightScale == 0 {
|
||||||
|
params.LineHeightScale = 1.2
|
||||||
|
}
|
||||||
|
|
||||||
|
maxHeight = floatToFixed(fixedToFloat(maxHeight) * params.LineHeightScale)
|
||||||
|
for i := range textLines {
|
||||||
|
textLines[i].lineHeight = maxHeight
|
||||||
|
}
|
||||||
calculateYOffsets(textLines)
|
calculateYOffsets(textLines)
|
||||||
return document{
|
return document{
|
||||||
lines: textLines,
|
lines: textLines,
|
||||||
@@ -575,7 +650,13 @@ func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec {
|
|||||||
x = g.X
|
x = g.X
|
||||||
}
|
}
|
||||||
ppem, faceIdx, gid := splitGlyphID(g.ID)
|
ppem, faceIdx, gid := splitGlyphID(g.ID)
|
||||||
face := s.orderer.faceFor(faceIdx)
|
if faceIdx >= len(s.faces) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
face := s.faces[faceIdx]
|
||||||
|
if face == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
scaleFactor := fixedToFloat(ppem) / float32(face.Upem())
|
scaleFactor := fixedToFloat(ppem) / float32(face.Upem())
|
||||||
glyphData := face.GlyphData(gid)
|
glyphData := face.GlyphData(gid)
|
||||||
switch glyphData := glyphData.(type) {
|
switch glyphData := glyphData.(type) {
|
||||||
@@ -633,6 +714,10 @@ func fixedToFloat(i fixed.Int26_6) float32 {
|
|||||||
return float32(i) / 64.0
|
return float32(i) / 64.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func floatToFixed(f float32) fixed.Int26_6 {
|
||||||
|
return fixed.Int26_6(f * 64)
|
||||||
|
}
|
||||||
|
|
||||||
// Bitmaps returns an op.CallOp that will display all bitmap glyphs within gs.
|
// Bitmaps returns an op.CallOp that will display all bitmap glyphs within gs.
|
||||||
// The positioning of the bitmaps uses the same logic as Shape(), so the returned
|
// The positioning of the bitmaps uses the same logic as Shape(), so the returned
|
||||||
// CallOp can be added at the same offset as the path data returned by Shape()
|
// CallOp can be added at the same offset as the path data returned by Shape()
|
||||||
@@ -645,7 +730,13 @@ func (s *shaperImpl) Bitmaps(ops *op.Ops, gs []Glyph) op.CallOp {
|
|||||||
x = g.X
|
x = g.X
|
||||||
}
|
}
|
||||||
_, faceIdx, gid := splitGlyphID(g.ID)
|
_, faceIdx, gid := splitGlyphID(g.ID)
|
||||||
face := s.orderer.faceFor(faceIdx)
|
if faceIdx >= len(s.faces) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
face := s.faces[faceIdx]
|
||||||
|
if face == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
glyphData := face.GlyphData(gid)
|
glyphData := face.GlyphData(gid)
|
||||||
switch glyphData := glyphData.(type) {
|
switch glyphData := glyphData.(type) {
|
||||||
case api.GlyphBitmap:
|
case api.GlyphBitmap:
|
||||||
@@ -674,7 +765,7 @@ func (s *shaperImpl) Bitmaps(ops *op.Ops, gs []Glyph) op.CallOp {
|
|||||||
}
|
}
|
||||||
off := op.Affine(f32.Affine2D{}.Offset(f32.Point{
|
off := op.Affine(f32.Affine2D{}.Offset(f32.Point{
|
||||||
X: fixedToFloat((g.X - x) - g.Offset.X),
|
X: fixedToFloat((g.X - x) - g.Offset.X),
|
||||||
Y: fixedToFloat(g.Offset.Y - g.Ascent),
|
Y: fixedToFloat(g.Offset.Y + g.Bounds.Min.Y),
|
||||||
})).Push(ops)
|
})).Push(ops)
|
||||||
cl := clip.Rect{Max: imgSize}.Push(ops)
|
cl := clip.Rect{Max: imgSize}.Push(ops)
|
||||||
|
|
||||||
@@ -773,7 +864,7 @@ func toGioGlyphs(in []shaping.Glyph, ppem fixed.Int26_6, faceIdx int) []glyph {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// toLine converts the output into a Line with the provided dominant text direction.
|
// toLine converts the output into a Line with the provided dominant text direction.
|
||||||
func toLine(orderer *faceOrderer, o shaping.Line, dir system.TextDirection) line {
|
func toLine(faceToIndex map[font.Font]int, o shaping.Line, dir system.TextDirection) line {
|
||||||
if len(o) < 1 {
|
if len(o) < 1 {
|
||||||
return line{}
|
return line{}
|
||||||
}
|
}
|
||||||
@@ -781,10 +872,18 @@ func toLine(orderer *faceOrderer, o shaping.Line, dir system.TextDirection) line
|
|||||||
runs: make([]runLayout, len(o)),
|
runs: make([]runLayout, len(o)),
|
||||||
direction: dir,
|
direction: dir,
|
||||||
}
|
}
|
||||||
|
maxSize := fixed.Int26_6(0)
|
||||||
for i := range o {
|
for i := range o {
|
||||||
run := o[i]
|
run := o[i]
|
||||||
|
if run.Size > maxSize {
|
||||||
|
maxSize = run.Size
|
||||||
|
}
|
||||||
|
var font font.Font
|
||||||
|
if run.Face != nil {
|
||||||
|
font = run.Face.Font
|
||||||
|
}
|
||||||
line.runs[i] = runLayout{
|
line.runs[i] = runLayout{
|
||||||
Glyphs: toGioGlyphs(run.Glyphs, run.Size, orderer.indexFor(run.Face)),
|
Glyphs: toGioGlyphs(run.Glyphs, run.Size, faceToIndex[font]),
|
||||||
Runes: Range{
|
Runes: Range{
|
||||||
Count: run.Runes.Count,
|
Count: run.Runes.Count,
|
||||||
Offset: line.runeCount,
|
Offset: line.runeCount,
|
||||||
@@ -795,13 +894,6 @@ func toLine(orderer *faceOrderer, o shaping.Line, dir system.TextDirection) line
|
|||||||
PPEM: run.Size,
|
PPEM: run.Size,
|
||||||
}
|
}
|
||||||
line.runeCount += run.Runes.Count
|
line.runeCount += run.Runes.Count
|
||||||
if line.bounds.Min.Y > -run.LineBounds.Ascent {
|
|
||||||
line.bounds.Min.Y = -run.LineBounds.Ascent
|
|
||||||
}
|
|
||||||
if line.bounds.Max.Y < -run.LineBounds.Ascent+run.LineBounds.LineHeight() {
|
|
||||||
line.bounds.Max.Y = -run.LineBounds.Ascent + run.LineBounds.LineHeight()
|
|
||||||
}
|
|
||||||
line.bounds.Max.X += run.Advance
|
|
||||||
line.width += run.Advance
|
line.width += run.Advance
|
||||||
if line.ascent < run.LineBounds.Ascent {
|
if line.ascent < run.LineBounds.Ascent {
|
||||||
line.ascent = run.LineBounds.Ascent
|
line.ascent = run.LineBounds.Ascent
|
||||||
@@ -810,21 +902,8 @@ func toLine(orderer *faceOrderer, o shaping.Line, dir system.TextDirection) line
|
|||||||
line.descent = -run.LineBounds.Descent + run.LineBounds.Gap
|
line.descent = -run.LineBounds.Descent + run.LineBounds.Gap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
line.lineHeight = maxSize
|
||||||
computeVisualOrder(&line)
|
computeVisualOrder(&line)
|
||||||
// Account for glyphs hanging off of either side in the bounds.
|
|
||||||
if len(line.visualOrder) > 0 {
|
|
||||||
runIdx := line.visualOrder[0]
|
|
||||||
run := o[runIdx]
|
|
||||||
if len(run.Glyphs) > 0 {
|
|
||||||
line.bounds.Min.X = run.Glyphs[0].LeftSideBearing()
|
|
||||||
}
|
|
||||||
runIdx = line.visualOrder[len(line.visualOrder)-1]
|
|
||||||
run = o[runIdx]
|
|
||||||
if len(run.Glyphs) > 0 {
|
|
||||||
lastGlyphIdx := len(run.Glyphs) - 1
|
|
||||||
line.bounds.Max.X += run.Glyphs[lastGlyphIdx].RightSideBearing()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -885,43 +964,3 @@ func computeVisualOrder(l *line) {
|
|||||||
x += l.runs[runIdx].Advance
|
x += l.runs[runIdx].Advance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// closestFont returns the closest Font in available by weight.
|
|
||||||
// In case of equality the lighter weight will be returned.
|
|
||||||
func closestFont(lookup giofont.Font, available []giofont.Font) (giofont.Font, bool) {
|
|
||||||
found := false
|
|
||||||
var match giofont.Font
|
|
||||||
for _, cf := range available {
|
|
||||||
if cf == lookup {
|
|
||||||
return lookup, true
|
|
||||||
}
|
|
||||||
if cf.Typeface != lookup.Typeface || cf.Variant != lookup.Variant || cf.Style != lookup.Style {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
found = true
|
|
||||||
match = cf
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cDist := weightDistance(lookup.Weight, cf.Weight)
|
|
||||||
mDist := weightDistance(lookup.Weight, match.Weight)
|
|
||||||
if cDist < mDist {
|
|
||||||
match = cf
|
|
||||||
} else if cDist == mDist && cf.Weight < match.Weight {
|
|
||||||
match = cf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return match, found
|
|
||||||
}
|
|
||||||
|
|
||||||
// weightDistance returns the distance value between two font weights.
|
|
||||||
func weightDistance(wa giofont.Weight, wb giofont.Weight) int {
|
|
||||||
// Avoid dealing with negative Weight values.
|
|
||||||
a := int(wa) + 400
|
|
||||||
b := int(wb) + 400
|
|
||||||
diff := a - b
|
|
||||||
if diff < 0 {
|
|
||||||
return -diff
|
|
||||||
}
|
|
||||||
return diff
|
|
||||||
}
|
|
||||||
|
|||||||
+30
-129
@@ -30,11 +30,12 @@ var arabic = system.Locale{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testShaper(faces ...giofont.Face) *shaperImpl {
|
func testShaper(faces ...giofont.Face) *shaperImpl {
|
||||||
shaper := shaperImpl{}
|
ff := make([]FontFace, 0, len(faces))
|
||||||
for _, face := range faces {
|
for _, face := range faces {
|
||||||
shaper.Load(FontFace{Face: face})
|
ff = append(ff, FontFace{Face: face})
|
||||||
}
|
}
|
||||||
return &shaper
|
shaper := newShaperImpl(false, ff)
|
||||||
|
return shaper
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmptyString(t *testing.T) {
|
func TestEmptyString(t *testing.T) {
|
||||||
@@ -51,19 +52,26 @@ func TestEmptyString(t *testing.T) {
|
|||||||
t.Fatalf("Layout returned no lines for empty string; expected 1")
|
t.Fatalf("Layout returned no lines for empty string; expected 1")
|
||||||
}
|
}
|
||||||
l := lines.lines[0]
|
l := lines.lines[0]
|
||||||
exp := fixed.Rectangle26_6{
|
if expected := fixed.Int26_6(12094); l.ascent != expected {
|
||||||
Min: fixed.Point26_6{
|
t.Errorf("unexpected ascent for empty string: %v, expected %v", l.ascent, expected)
|
||||||
Y: fixed.Int26_6(-12094),
|
|
||||||
},
|
|
||||||
Max: fixed.Point26_6{
|
|
||||||
Y: fixed.Int26_6(2700),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
if got := l.bounds; got != exp {
|
if expected := fixed.Int26_6(2700); l.descent != expected {
|
||||||
t.Errorf("got bounds %+v for empty string; expected %+v", got, exp)
|
t.Errorf("unexpected descent for empty string: %v, expected %v", l.descent, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNoFaces(t *testing.T) {
|
||||||
|
ppem := fixed.I(200)
|
||||||
|
shaper := testShaper()
|
||||||
|
|
||||||
|
// Ensure shaping text with no faces does not panic.
|
||||||
|
shaper.LayoutRunes(Parameters{
|
||||||
|
PxPerEm: ppem,
|
||||||
|
MaxWidth: 2000,
|
||||||
|
Locale: english,
|
||||||
|
}, []rune("✨ⷽℎ↞⋇ⱜ⪫⢡⽛⣦␆Ⱨⳏ⳯⒛⭣╎⌞⟻⢇┃➡⬎⩱⸇ⷎ⟅▤⼶⇺⩳⎏⤬⬞ⴈ⋠⿶⢒₍☟⽂ⶦ⫰⭢⌹∼▀⾯⧂❽⩏ⓖ⟅⤔⍇␋⽓ₑ⢳⠑❂⊪⢘⽨⃯▴ⷿ"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestAlignWidth(t *testing.T) {
|
func TestAlignWidth(t *testing.T) {
|
||||||
lines := []line{
|
lines := []line{
|
||||||
{width: fixed.I(50)},
|
{width: fixed.I(50)},
|
||||||
@@ -288,13 +296,13 @@ func makeTestText(shaper *shaperImpl, primaryDir system.TextDirection, fontSize,
|
|||||||
rtlSource = string(complexRunes[:runeLimit])
|
rtlSource = string(complexRunes[:runeLimit])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
simpleText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(giofont.Font{}), Parameters{
|
simpleText, _ := shaper.shapeAndWrapText(Parameters{
|
||||||
PxPerEm: fixed.I(fontSize),
|
PxPerEm: fixed.I(fontSize),
|
||||||
MaxWidth: lineWidth,
|
MaxWidth: lineWidth,
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
}, []rune(simpleSource))
|
}, []rune(simpleSource))
|
||||||
simpleText = copyLines(simpleText)
|
simpleText = copyLines(simpleText)
|
||||||
complexText, _ := shaper.shapeAndWrapText(shaper.orderer.sortedFacesForStyle(giofont.Font{}), Parameters{
|
complexText, _ := shaper.shapeAndWrapText(Parameters{
|
||||||
PxPerEm: fixed.I(fontSize),
|
PxPerEm: fixed.I(fontSize),
|
||||||
MaxWidth: lineWidth,
|
MaxWidth: lineWidth,
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
@@ -378,13 +386,7 @@ func TestToLine(t *testing.T) {
|
|||||||
totalInputGlyphs += len(run.Glyphs)
|
totalInputGlyphs += len(run.Glyphs)
|
||||||
totalInputRunes += run.Runes.Count
|
totalInputRunes += run.Runes.Count
|
||||||
}
|
}
|
||||||
output := toLine(&shaper.orderer, input, tc.dir)
|
output := toLine(shaper.faceToIndex, input, tc.dir)
|
||||||
if output.bounds.Min == (fixed.Point26_6{}) {
|
|
||||||
t.Errorf("line %d: Bounds.Min not populated", i)
|
|
||||||
}
|
|
||||||
if output.bounds.Max == (fixed.Point26_6{}) {
|
|
||||||
t.Errorf("line %d: Bounds.Max not populated", i)
|
|
||||||
}
|
|
||||||
if output.direction != tc.dir {
|
if output.direction != tc.dir {
|
||||||
t.Errorf("line %d: expected direction %v, got %v", i, tc.dir, output.direction)
|
t.Errorf("line %d: expected direction %v, got %v", i, tc.dir, output.direction)
|
||||||
}
|
}
|
||||||
@@ -565,10 +567,10 @@ func TestComputeVisualOrder(t *testing.T) {
|
|||||||
func FuzzLayout(f *testing.F) {
|
func FuzzLayout(f *testing.F) {
|
||||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||||
rtlFace, _ := opentype.Parse(nsareg.TTF)
|
rtlFace, _ := opentype.Parse(nsareg.TTF)
|
||||||
f.Add("د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ.", true, uint8(10), uint16(200))
|
f.Add("د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ.", true, false, uint8(10), uint16(200))
|
||||||
|
|
||||||
shaper := testShaper(ltrFace, rtlFace)
|
shaper := testShaper(ltrFace, rtlFace)
|
||||||
f.Fuzz(func(t *testing.T, txt string, rtl bool, fontSize uint8, width uint16) {
|
f.Fuzz(func(t *testing.T, txt string, rtl bool, truncate bool, fontSize uint8, width uint16) {
|
||||||
locale := system.Locale{
|
locale := system.Locale{
|
||||||
Direction: system.LTR,
|
Direction: system.LTR,
|
||||||
}
|
}
|
||||||
@@ -578,9 +580,14 @@ func FuzzLayout(f *testing.F) {
|
|||||||
if fontSize < 1 {
|
if fontSize < 1 {
|
||||||
fontSize = 1
|
fontSize = 1
|
||||||
}
|
}
|
||||||
|
maxLines := 0
|
||||||
|
if truncate {
|
||||||
|
maxLines = 1
|
||||||
|
}
|
||||||
lines := shaper.LayoutRunes(Parameters{
|
lines := shaper.LayoutRunes(Parameters{
|
||||||
PxPerEm: fixed.I(int(fontSize)),
|
PxPerEm: fixed.I(int(fontSize)),
|
||||||
MaxWidth: int(width),
|
MaxWidth: int(width),
|
||||||
|
MaxLines: maxLines,
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
}, []rune(txt))
|
}, []rune(txt))
|
||||||
validateLines(t, lines.lines, len([]rune(txt)))
|
validateLines(t, lines.lines, len([]rune(txt)))
|
||||||
@@ -591,12 +598,6 @@ func validateLines(t *testing.T, lines []line, expectedRuneCount int) {
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
runesSeen := 0
|
runesSeen := 0
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
if line.bounds.Min == (fixed.Point26_6{}) {
|
|
||||||
t.Errorf("line %d: Bounds.Min not populated", i)
|
|
||||||
}
|
|
||||||
if line.bounds.Max == (fixed.Point26_6{}) {
|
|
||||||
t.Errorf("line %d: Bounds.Max not populated", i)
|
|
||||||
}
|
|
||||||
totalRunWidth := fixed.I(0)
|
totalRunWidth := fixed.I(0)
|
||||||
totalLineGlyphs := 0
|
totalLineGlyphs := 0
|
||||||
lineRunesSeen := 0
|
lineRunesSeen := 0
|
||||||
@@ -662,106 +663,6 @@ func TestTextAppend(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClosestFontByWeight(t *testing.T) {
|
|
||||||
const (
|
|
||||||
testTF1 giofont.Typeface = "MockFace"
|
|
||||||
testTF2 giofont.Typeface = "TestFace"
|
|
||||||
testTF3 giofont.Typeface = "AnotherFace"
|
|
||||||
)
|
|
||||||
fonts := []giofont.Font{
|
|
||||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Normal},
|
|
||||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
|
|
||||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Bold},
|
|
||||||
{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Thin},
|
|
||||||
}
|
|
||||||
weightOnlyTests := []struct {
|
|
||||||
Lookup giofont.Weight
|
|
||||||
Expected giofont.Weight
|
|
||||||
}{
|
|
||||||
// Test for existing weights.
|
|
||||||
{Lookup: giofont.Normal, Expected: giofont.Normal},
|
|
||||||
{Lookup: giofont.Light, Expected: giofont.Light},
|
|
||||||
{Lookup: giofont.Bold, Expected: giofont.Bold},
|
|
||||||
// Test for missing weights.
|
|
||||||
{Lookup: giofont.Thin, Expected: giofont.Light},
|
|
||||||
{Lookup: giofont.ExtraLight, Expected: giofont.Light},
|
|
||||||
{Lookup: giofont.Medium, Expected: giofont.Normal},
|
|
||||||
{Lookup: giofont.SemiBold, Expected: giofont.Bold},
|
|
||||||
{Lookup: giofont.ExtraBold, Expected: giofont.Bold},
|
|
||||||
}
|
|
||||||
for _, test := range weightOnlyTests {
|
|
||||||
got, ok := closestFont(giofont.Font{Typeface: testTF1, Weight: test.Lookup}, fonts)
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("expected closest font for %v to exist", test.Lookup)
|
|
||||||
}
|
|
||||||
if got.Weight != test.Expected {
|
|
||||||
t.Errorf("got weight %v, expected %v", got.Weight, test.Expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fonts = []giofont.Font{
|
|
||||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
|
|
||||||
{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Bold},
|
|
||||||
{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
|
||||||
{Typeface: testTF3, Style: giofont.Italic, Weight: giofont.Bold},
|
|
||||||
}
|
|
||||||
otherTests := []struct {
|
|
||||||
Lookup giofont.Font
|
|
||||||
Expected giofont.Font
|
|
||||||
ExpectedToFail bool
|
|
||||||
}{
|
|
||||||
// Test for existing fonts.
|
|
||||||
{
|
|
||||||
Lookup: giofont.Font{Typeface: testTF1, Weight: giofont.Light},
|
|
||||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Lookup: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
|
||||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
|
||||||
},
|
|
||||||
// Test for missing fonts.
|
|
||||||
{
|
|
||||||
Lookup: giofont.Font{Typeface: testTF1, Weight: giofont.Normal},
|
|
||||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Regular, Weight: giofont.Light},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Lookup: giofont.Font{Typeface: testTF3, Style: giofont.Italic, Weight: giofont.Normal},
|
|
||||||
Expected: giofont.Font{Typeface: testTF3, Style: giofont.Italic, Weight: giofont.Bold},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Lookup: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Thin},
|
|
||||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Lookup: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Bold},
|
|
||||||
Expected: giofont.Font{Typeface: testTF1, Style: giofont.Italic, Weight: giofont.Normal},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Lookup: giofont.Font{Typeface: testTF2, Weight: giofont.Normal},
|
|
||||||
ExpectedToFail: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Lookup: giofont.Font{Typeface: testTF2, Style: giofont.Italic, Weight: giofont.Normal},
|
|
||||||
ExpectedToFail: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range otherTests {
|
|
||||||
got, ok := closestFont(test.Lookup, fonts)
|
|
||||||
if test.ExpectedToFail {
|
|
||||||
if ok {
|
|
||||||
t.Errorf("expected closest font for %v to not exist", test.Lookup)
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("expected closest font for %v to exist", test.Lookup)
|
|
||||||
}
|
|
||||||
if got != test.Expected {
|
|
||||||
t.Errorf("got %v, expected %v", got, test.Expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGlyphIDPacking(t *testing.T) {
|
func TestGlyphIDPacking(t *testing.T) {
|
||||||
const maxPPem = fixed.Int26_6((1 << sizebits) - 1)
|
const maxPPem = fixed.Int26_6((1 << sizebits) - 1)
|
||||||
type testcase struct {
|
type testcase struct {
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ type layoutKey struct {
|
|||||||
font giofont.Font
|
font giofont.Font
|
||||||
forceTruncate bool
|
forceTruncate bool
|
||||||
wrapPolicy WrapPolicy
|
wrapPolicy WrapPolicy
|
||||||
|
lineHeight fixed.Int26_6
|
||||||
|
lineHeightScale float32
|
||||||
}
|
}
|
||||||
|
|
||||||
type pathKey struct {
|
type pathKey struct {
|
||||||
|
|||||||
+66
-16
@@ -62,6 +62,16 @@ type Parameters struct {
|
|||||||
// Locale provides primary direction and language information for the shaped text.
|
// Locale provides primary direction and language information for the shaped text.
|
||||||
Locale system.Locale
|
Locale system.Locale
|
||||||
|
|
||||||
|
// LineHeightScale is a scaling factor applied to the LineHeight of a paragraph. If zero, a default
|
||||||
|
// value of 1.2 will be used.
|
||||||
|
LineHeightScale float32
|
||||||
|
|
||||||
|
// LineHeight is the distance between the baselines of two lines of text. If zero, the PxPerEm
|
||||||
|
// of the any given paragraph will set the LineHeight of that paragraph. This value will be
|
||||||
|
// scaled by LineHeightScale, so applications desiring a specific fixed value
|
||||||
|
// should set LineHeightScale to 1.
|
||||||
|
LineHeight fixed.Int26_6
|
||||||
|
|
||||||
// forceTruncate controls whether the truncator string is inserted on the final line of
|
// forceTruncate controls whether the truncator string is inserted on the final line of
|
||||||
// text with a MaxLines. It is unexported because this behavior only makes sense for the
|
// text with a MaxLines. It is unexported because this behavior only makes sense for the
|
||||||
// shaper to control when it iterates paragraphs of text.
|
// shaper to control when it iterates paragraphs of text.
|
||||||
@@ -191,6 +201,11 @@ type GlyphID uint64
|
|||||||
|
|
||||||
// Shaper converts strings of text into glyphs that can be displayed.
|
// Shaper converts strings of text into glyphs that can be displayed.
|
||||||
type Shaper struct {
|
type Shaper struct {
|
||||||
|
config struct {
|
||||||
|
disableSystemFonts bool
|
||||||
|
collection []FontFace
|
||||||
|
}
|
||||||
|
initialized bool
|
||||||
shaper shaperImpl
|
shaper shaperImpl
|
||||||
pathCache pathCache
|
pathCache pathCache
|
||||||
bitmapShapeCache bitmapShapeCache
|
bitmapShapeCache bitmapShapeCache
|
||||||
@@ -213,26 +228,53 @@ type Shaper struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShaperOptions configure text shapers.
|
||||||
|
type ShaperOption func(*Shaper)
|
||||||
|
|
||||||
|
// NoSystemFonts can be used to disable system font loading.
|
||||||
|
func NoSystemFonts() ShaperOption {
|
||||||
|
return func(s *Shaper) {
|
||||||
|
s.config.disableSystemFonts = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCollection can be used to provide a collection of pre-loaded fonts to the shaper.
|
||||||
|
func WithCollection(collection []FontFace) ShaperOption {
|
||||||
|
return func(s *Shaper) {
|
||||||
|
s.config.collection = collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewShaper constructs a shaper with the provided collection of font faces
|
// NewShaper constructs a shaper with the provided collection of font faces
|
||||||
// available.
|
// available.
|
||||||
func NewShaper(collection []FontFace) *Shaper {
|
func NewShaper(options ...ShaperOption) *Shaper {
|
||||||
l := &Shaper{}
|
l := &Shaper{}
|
||||||
for _, f := range collection {
|
for _, opt := range options {
|
||||||
l.shaper.Load(f)
|
opt(l)
|
||||||
}
|
}
|
||||||
l.shaper.shaper.SetFontCacheSize(32)
|
l.init()
|
||||||
l.reader = bufio.NewReader(nil)
|
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Shaper) init() {
|
||||||
|
if l.initialized {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.initialized = true
|
||||||
|
l.reader = bufio.NewReader(nil)
|
||||||
|
l.shaper = *newShaperImpl(!l.config.disableSystemFonts, l.config.collection)
|
||||||
|
}
|
||||||
|
|
||||||
// Layout text from an io.Reader according to a set of options. Results can be retrieved by
|
// Layout text from an io.Reader according to a set of options. Results can be retrieved by
|
||||||
// iteratively calling NextGlyph.
|
// iteratively calling NextGlyph.
|
||||||
func (l *Shaper) Layout(params Parameters, txt io.Reader) {
|
func (l *Shaper) Layout(params Parameters, txt io.Reader) {
|
||||||
|
l.init()
|
||||||
l.layoutText(params, txt, "")
|
l.layoutText(params, txt, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// LayoutString is Layout for strings.
|
// LayoutString is Layout for strings.
|
||||||
func (l *Shaper) LayoutString(params Parameters, str string) {
|
func (l *Shaper) LayoutString(params Parameters, str string) {
|
||||||
|
l.init()
|
||||||
l.layoutText(params, nil, str)
|
l.layoutText(params, nil, str)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +316,9 @@ func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) {
|
|||||||
if !done {
|
if !done {
|
||||||
_, re := l.reader.ReadByte()
|
_, re := l.reader.ReadByte()
|
||||||
done = re != nil
|
done = re != nil
|
||||||
_ = l.reader.UnreadByte()
|
if !done {
|
||||||
|
_ = l.reader.UnreadByte()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
idx := strings.IndexByte(str, '\n')
|
idx := strings.IndexByte(str, '\n')
|
||||||
@@ -283,6 +327,7 @@ func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) {
|
|||||||
endByte = len(str)
|
endByte = len(str)
|
||||||
} else {
|
} else {
|
||||||
endByte = idx + 1
|
endByte = idx + 1
|
||||||
|
done = endByte == len(str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(str[:endByte]) > 0 || (len(l.paragraph) > 0 || len(l.txt.lines) == 0) {
|
if len(str[:endByte]) > 0 || (len(l.paragraph) > 0 || len(l.txt.lines) == 0) {
|
||||||
@@ -334,16 +379,18 @@ func (l *Shaper) layoutParagraph(params Parameters, asStr string, asBytes []byte
|
|||||||
}
|
}
|
||||||
// Alignment is not part of the cache key because changing it does not impact shaping.
|
// Alignment is not part of the cache key because changing it does not impact shaping.
|
||||||
lk := layoutKey{
|
lk := layoutKey{
|
||||||
ppem: params.PxPerEm,
|
ppem: params.PxPerEm,
|
||||||
maxWidth: params.MaxWidth,
|
maxWidth: params.MaxWidth,
|
||||||
minWidth: params.MinWidth,
|
minWidth: params.MinWidth,
|
||||||
maxLines: params.MaxLines,
|
maxLines: params.MaxLines,
|
||||||
truncator: params.Truncator,
|
truncator: params.Truncator,
|
||||||
locale: params.Locale,
|
locale: params.Locale,
|
||||||
font: params.Font,
|
font: params.Font,
|
||||||
forceTruncate: params.forceTruncate,
|
forceTruncate: params.forceTruncate,
|
||||||
wrapPolicy: params.WrapPolicy,
|
wrapPolicy: params.WrapPolicy,
|
||||||
str: asStr,
|
str: asStr,
|
||||||
|
lineHeight: params.LineHeight,
|
||||||
|
lineHeightScale: params.LineHeightScale,
|
||||||
}
|
}
|
||||||
if l, ok := l.layoutCache.Get(lk); ok {
|
if l, ok := l.layoutCache.Get(lk); ok {
|
||||||
return l
|
return l
|
||||||
@@ -356,6 +403,7 @@ func (l *Shaper) layoutParagraph(params Parameters, asStr string, asBytes []byte
|
|||||||
// NextGlyph returns the next glyph from the most recent shaping operation, if
|
// NextGlyph returns the next glyph from the most recent shaping operation, if
|
||||||
// any. If there are no more glyphs, ok will be false.
|
// any. If there are no more glyphs, ok will be false.
|
||||||
func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
|
func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
|
||||||
|
l.init()
|
||||||
if l.done {
|
if l.done {
|
||||||
return Glyph{}, false
|
return Glyph{}, false
|
||||||
}
|
}
|
||||||
@@ -523,6 +571,7 @@ func splitGlyphID(g GlyphID) (fixed.Int26_6, int, font.GID) {
|
|||||||
// of all vector glyphs.
|
// of all vector glyphs.
|
||||||
// All glyphs are expected to be from a single line of text (their Y offsets are ignored).
|
// All glyphs are expected to be from a single line of text (their Y offsets are ignored).
|
||||||
func (l *Shaper) Shape(gs []Glyph) clip.PathSpec {
|
func (l *Shaper) Shape(gs []Glyph) clip.PathSpec {
|
||||||
|
l.init()
|
||||||
key := l.pathCache.hashGlyphs(gs)
|
key := l.pathCache.hashGlyphs(gs)
|
||||||
shape, ok := l.pathCache.Get(key, gs)
|
shape, ok := l.pathCache.Get(key, gs)
|
||||||
if ok {
|
if ok {
|
||||||
@@ -539,6 +588,7 @@ func (l *Shaper) Shape(gs []Glyph) clip.PathSpec {
|
|||||||
// same gs slice.
|
// same gs slice.
|
||||||
// All glyphs are expected to be from a single line of text (their Y offsets are ignored).
|
// All glyphs are expected to be from a single line of text (their Y offsets are ignored).
|
||||||
func (l *Shaper) Bitmaps(gs []Glyph) op.CallOp {
|
func (l *Shaper) Bitmaps(gs []Glyph) op.CallOp {
|
||||||
|
l.init()
|
||||||
key := l.bitmapShapeCache.hashGlyphs(gs)
|
key := l.bitmapShapeCache.hashGlyphs(gs)
|
||||||
call, ok := l.bitmapShapeCache.Get(key, gs)
|
call, ok := l.bitmapShapeCache.Get(key, gs)
|
||||||
if ok {
|
if ok {
|
||||||
|
|||||||
+70
-9
@@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
nsareg "eliasnaur.com/font/noto/sans/arabic/regular"
|
nsareg "eliasnaur.com/font/noto/sans/arabic/regular"
|
||||||
|
"gioui.org/font"
|
||||||
"gioui.org/font/gofont"
|
"gioui.org/font/gofont"
|
||||||
"gioui.org/font/opentype"
|
"gioui.org/font/opentype"
|
||||||
"gioui.org/io/system"
|
"gioui.org/io/system"
|
||||||
@@ -22,7 +23,7 @@ func TestWrappingTruncation(t *testing.T) {
|
|||||||
textInput := "Lorem ipsum dolor sit amet, consectetur adipiscing elit,\nsed do eiusmod tempor incididunt ut labore et\ndolore magna aliqua.\n"
|
textInput := "Lorem ipsum dolor sit amet, consectetur adipiscing elit,\nsed do eiusmod tempor incididunt ut labore et\ndolore magna aliqua.\n"
|
||||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||||
collection := []FontFace{{Face: ltrFace}}
|
collection := []FontFace{{Face: ltrFace}}
|
||||||
cache := NewShaper(collection)
|
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||||
cache.LayoutString(Parameters{
|
cache.LayoutString(Parameters{
|
||||||
Alignment: Middle,
|
Alignment: Middle,
|
||||||
PxPerEm: fixed.I(10),
|
PxPerEm: fixed.I(10),
|
||||||
@@ -89,7 +90,7 @@ func TestWrappingForcedTruncation(t *testing.T) {
|
|||||||
textInput := "Lorem ipsum\ndolor sit\namet"
|
textInput := "Lorem ipsum\ndolor sit\namet"
|
||||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||||
collection := []FontFace{{Face: ltrFace}}
|
collection := []FontFace{{Face: ltrFace}}
|
||||||
cache := NewShaper(collection)
|
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||||
cache.LayoutString(Parameters{
|
cache.LayoutString(Parameters{
|
||||||
Alignment: Middle,
|
Alignment: Middle,
|
||||||
PxPerEm: fixed.I(10),
|
PxPerEm: fixed.I(10),
|
||||||
@@ -161,15 +162,23 @@ func TestShapingNewlineHandling(t *testing.T) {
|
|||||||
{textInput: "a\n", expectedLines: 1, expectedGlyphs: 3},
|
{textInput: "a\n", expectedLines: 1, expectedGlyphs: 3},
|
||||||
{textInput: "a\nb", expectedLines: 2, expectedGlyphs: 3},
|
{textInput: "a\nb", expectedLines: 2, expectedGlyphs: 3},
|
||||||
{textInput: "", expectedLines: 1, expectedGlyphs: 1},
|
{textInput: "", expectedLines: 1, expectedGlyphs: 1},
|
||||||
|
{textInput: "\n", expectedLines: 1, expectedGlyphs: 2},
|
||||||
|
{textInput: "\n\n", expectedLines: 2, expectedGlyphs: 3},
|
||||||
|
{textInput: "\n\n\n", expectedLines: 3, expectedGlyphs: 4},
|
||||||
} {
|
} {
|
||||||
t.Run(fmt.Sprintf("%q", tc.textInput), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%q", tc.textInput), func(t *testing.T) {
|
||||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||||
collection := []FontFace{{Face: ltrFace}}
|
collection := []FontFace{{Face: ltrFace}}
|
||||||
cache := NewShaper(collection)
|
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||||
checkGlyphs := func() {
|
checkGlyphs := func() {
|
||||||
glyphs := []Glyph{}
|
glyphs := []Glyph{}
|
||||||
|
runes := 0
|
||||||
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
|
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
|
||||||
glyphs = append(glyphs, g)
|
glyphs = append(glyphs, g)
|
||||||
|
runes += g.Runes
|
||||||
|
}
|
||||||
|
if expected := len([]rune(tc.textInput)); expected != runes {
|
||||||
|
t.Errorf("expected %d runes, got %d", expected, runes)
|
||||||
}
|
}
|
||||||
if len(glyphs) != tc.expectedGlyphs {
|
if len(glyphs) != tc.expectedGlyphs {
|
||||||
t.Errorf("expected %d glyphs, got %d", tc.expectedGlyphs, len(glyphs))
|
t.Errorf("expected %d glyphs, got %d", tc.expectedGlyphs, len(glyphs))
|
||||||
@@ -191,8 +200,8 @@ func TestShapingNewlineHandling(t *testing.T) {
|
|||||||
}
|
}
|
||||||
breakX, breakY := breakGlyph.X, breakGlyph.Y
|
breakX, breakY := breakGlyph.X, breakGlyph.Y
|
||||||
startX, startY := startGlyph.X, startGlyph.Y
|
startX, startY := startGlyph.X, startGlyph.Y
|
||||||
if breakX == startX {
|
if breakX == startX && idx != 0 {
|
||||||
t.Errorf("expected paragraph start glyph to have cursor x")
|
t.Errorf("expected paragraph start glyph to have cursor x, got %v", startX)
|
||||||
}
|
}
|
||||||
if breakY == startY {
|
if breakY == startY {
|
||||||
t.Errorf("expected paragraph start glyph to have cursor y")
|
t.Errorf("expected paragraph start glyph to have cursor y")
|
||||||
@@ -234,7 +243,7 @@ func TestShapingNewlineHandling(t *testing.T) {
|
|||||||
func TestCacheEmptyString(t *testing.T) {
|
func TestCacheEmptyString(t *testing.T) {
|
||||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||||
collection := []FontFace{{Face: ltrFace}}
|
collection := []FontFace{{Face: ltrFace}}
|
||||||
cache := NewShaper(collection)
|
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||||
cache.LayoutString(Parameters{
|
cache.LayoutString(Parameters{
|
||||||
Alignment: Middle,
|
Alignment: Middle,
|
||||||
PxPerEm: fixed.I(10),
|
PxPerEm: fixed.I(10),
|
||||||
@@ -273,7 +282,7 @@ func TestCacheEmptyString(t *testing.T) {
|
|||||||
func TestCacheAlignment(t *testing.T) {
|
func TestCacheAlignment(t *testing.T) {
|
||||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||||
collection := []FontFace{{Face: ltrFace}}
|
collection := []FontFace{{Face: ltrFace}}
|
||||||
cache := NewShaper(collection)
|
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||||
params := Parameters{
|
params := Parameters{
|
||||||
Alignment: Start,
|
Alignment: Start,
|
||||||
PxPerEm: fixed.I(10),
|
PxPerEm: fixed.I(10),
|
||||||
@@ -339,7 +348,7 @@ func TestCacheGlyphConverstion(t *testing.T) {
|
|||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cache := NewShaper(collection)
|
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||||
cache.LayoutString(Parameters{
|
cache.LayoutString(Parameters{
|
||||||
PxPerEm: fixed.I(10),
|
PxPerEm: fixed.I(10),
|
||||||
MaxWidth: 200,
|
MaxWidth: 200,
|
||||||
@@ -464,6 +473,58 @@ func TestShapeStringRuneAccounting(t *testing.T) {
|
|||||||
MaxWidth: 100,
|
MaxWidth: 100,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "newline regression",
|
||||||
|
input: "\n",
|
||||||
|
params: Parameters{
|
||||||
|
Font: font.Font{Typeface: "Go", Style: font.Regular, Weight: font.Normal},
|
||||||
|
Alignment: Start,
|
||||||
|
PxPerEm: 768,
|
||||||
|
MaxLines: 1,
|
||||||
|
Truncator: "\u200b",
|
||||||
|
WrapPolicy: WrapHeuristically,
|
||||||
|
MaxWidth: 999929,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "newline zero-width regression",
|
||||||
|
input: "\n",
|
||||||
|
params: Parameters{
|
||||||
|
Font: font.Font{Typeface: "Go", Style: font.Regular, Weight: font.Normal},
|
||||||
|
Alignment: Start,
|
||||||
|
PxPerEm: 768,
|
||||||
|
MaxLines: 1,
|
||||||
|
Truncator: "\u200b",
|
||||||
|
WrapPolicy: WrapHeuristically,
|
||||||
|
MaxWidth: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "double newline regression",
|
||||||
|
input: "\n\n",
|
||||||
|
params: Parameters{
|
||||||
|
Font: font.Font{Typeface: "Go", Style: font.Regular, Weight: font.Normal},
|
||||||
|
Alignment: Start,
|
||||||
|
PxPerEm: 768,
|
||||||
|
MaxLines: 1,
|
||||||
|
Truncator: "\u200b",
|
||||||
|
WrapPolicy: WrapHeuristically,
|
||||||
|
MaxWidth: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "triple newline regression",
|
||||||
|
input: "\n\n\n",
|
||||||
|
params: Parameters{
|
||||||
|
Font: font.Font{Typeface: "Go", Style: font.Regular, Weight: font.Normal},
|
||||||
|
Alignment: Start,
|
||||||
|
PxPerEm: 768,
|
||||||
|
MaxLines: 1,
|
||||||
|
Truncator: "\u200b",
|
||||||
|
WrapPolicy: WrapHeuristically,
|
||||||
|
MaxWidth: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
for _, setup := range []setup{
|
for _, setup := range []setup{
|
||||||
@@ -481,7 +542,7 @@ func TestShapeStringRuneAccounting(t *testing.T) {
|
|||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(setup.kind, func(t *testing.T) {
|
t.Run(setup.kind, func(t *testing.T) {
|
||||||
shaper := NewShaper(gofont.Collection())
|
shaper := NewShaper(NoSystemFonts(), WithCollection(gofont.Collection()))
|
||||||
setup.do(shaper, tc.params, tc.input)
|
setup.do(shaper, tc.params, tc.input)
|
||||||
|
|
||||||
glyphs := []Glyph{}
|
glyphs := []Glyph{}
|
||||||
|
|||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
go test fuzz v1
|
go test fuzz v1
|
||||||
string("\x1d")
|
string("\x1d")
|
||||||
bool(true)
|
bool(true)
|
||||||
|
bool(false)
|
||||||
byte('\x1c')
|
byte('\x1c')
|
||||||
uint16(227)
|
uint16(227)
|
||||||
|
|||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
go test fuzz v1
|
go test fuzz v1
|
||||||
string("0")
|
string("0")
|
||||||
bool(true)
|
bool(true)
|
||||||
|
bool(false)
|
||||||
uint8(27)
|
uint8(27)
|
||||||
uint16(200)
|
uint16(200)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
string("\n")
|
||||||
|
bool(false)
|
||||||
|
bool(true)
|
||||||
|
byte('±')
|
||||||
|
uint16(0)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
string("\n")
|
||||||
|
bool(false)
|
||||||
|
bool(false)
|
||||||
|
byte('±')
|
||||||
|
uint16(0)
|
||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
go test fuzz v1
|
go test fuzz v1
|
||||||
string("\u2029")
|
string("\u2029")
|
||||||
bool(false)
|
bool(false)
|
||||||
|
bool(false)
|
||||||
byte('*')
|
byte('*')
|
||||||
uint16(72)
|
uint16(72)
|
||||||
|
|||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
go test fuzz v1
|
go test fuzz v1
|
||||||
string("Aͮ000000000000000")
|
string("Aͮ000000000000000")
|
||||||
bool(false)
|
bool(false)
|
||||||
|
bool(false)
|
||||||
byte('\u0087')
|
byte('\u0087')
|
||||||
uint16(111)
|
uint16(111)
|
||||||
|
|||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
go test fuzz v1
|
go test fuzz v1
|
||||||
string("\x1e")
|
string("\x1e")
|
||||||
bool(true)
|
bool(true)
|
||||||
|
bool(false)
|
||||||
byte('\n')
|
byte('\n')
|
||||||
uint16(254)
|
uint16(254)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
string("000000000000000 00000000 ٰ00000")
|
||||||
|
bool(true)
|
||||||
|
bool(false)
|
||||||
|
byte('\n')
|
||||||
|
uint16(121)
|
||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
go test fuzz v1
|
go test fuzz v1
|
||||||
string("\r")
|
string("\r")
|
||||||
bool(false)
|
bool(false)
|
||||||
|
bool(false)
|
||||||
byte('T')
|
byte('T')
|
||||||
uint16(200)
|
uint16(200)
|
||||||
|
|||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
go test fuzz v1
|
go test fuzz v1
|
||||||
string("\u0085")
|
string("\u0085")
|
||||||
bool(true)
|
bool(true)
|
||||||
|
bool(false)
|
||||||
byte('\x10')
|
byte('\x10')
|
||||||
uint16(271)
|
uint16(271)
|
||||||
|
|||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
go test fuzz v1
|
go test fuzz v1
|
||||||
string("0")
|
string("0")
|
||||||
bool(false)
|
bool(false)
|
||||||
|
bool(false)
|
||||||
byte('\x00')
|
byte('\x00')
|
||||||
uint16(142)
|
uint16(142)
|
||||||
|
|||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
go test fuzz v1
|
go test fuzz v1
|
||||||
string("\n")
|
string("\n")
|
||||||
bool(true)
|
bool(true)
|
||||||
|
bool(false)
|
||||||
byte('\t')
|
byte('\t')
|
||||||
uint16(200)
|
uint16(200)
|
||||||
|
|||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
go test fuzz v1
|
go test fuzz v1
|
||||||
string("ع0 ׂ0")
|
string("ع0 ׂ0")
|
||||||
bool(false)
|
bool(false)
|
||||||
|
bool(false)
|
||||||
byte('\u0098')
|
byte('\u0098')
|
||||||
uint16(198)
|
uint16(198)
|
||||||
|
|||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
go test fuzz v1
|
go test fuzz v1
|
||||||
string("\x1c")
|
string("\x1c")
|
||||||
bool(true)
|
bool(true)
|
||||||
|
bool(false)
|
||||||
byte('\u009c')
|
byte('\u009c')
|
||||||
uint16(200)
|
uint16(200)
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ type Editor struct {
|
|||||||
text textView
|
text textView
|
||||||
// Alignment controls the alignment of text within the editor.
|
// Alignment controls the alignment of text within the editor.
|
||||||
Alignment text.Alignment
|
Alignment text.Alignment
|
||||||
|
// LineHeight determines the gap between baselines of text. If zero, a sensible
|
||||||
|
// default will be used.
|
||||||
|
LineHeight unit.Sp
|
||||||
|
// LineHeightScale is multiplied by LineHeight to determine the final gap
|
||||||
|
// between baselines. If zero, a sensible default will be used.
|
||||||
|
LineHeightScale float32
|
||||||
// SingleLine force the text to stay on a single line.
|
// SingleLine force the text to stay on a single line.
|
||||||
// SingleLine also sets the scrolling direction to
|
// SingleLine also sets the scrolling direction to
|
||||||
// horizontal.
|
// horizontal.
|
||||||
@@ -504,6 +510,8 @@ func (e *Editor) initBuffer() {
|
|||||||
e.text.SetSource(e.buffer)
|
e.text.SetSource(e.buffer)
|
||||||
}
|
}
|
||||||
e.text.Alignment = e.Alignment
|
e.text.Alignment = e.Alignment
|
||||||
|
e.text.LineHeight = e.LineHeight
|
||||||
|
e.text.LineHeightScale = e.LineHeightScale
|
||||||
e.text.SingleLine = e.SingleLine
|
e.text.SingleLine = e.SingleLine
|
||||||
e.text.Mask = e.Mask
|
e.text.Mask = e.Mask
|
||||||
e.text.WrapPolicy = e.WrapPolicy
|
e.text.WrapPolicy = e.WrapPolicy
|
||||||
|
|||||||
+21
-20
@@ -108,7 +108,7 @@ func TestEditorReadOnly(t *testing.T) {
|
|||||||
key.FocusEvent{Focus: true},
|
key.FocusEvent{Focus: true},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
e := new(Editor)
|
e := new(Editor)
|
||||||
@@ -187,7 +187,7 @@ func TestEditorConfigurations(t *testing.T) {
|
|||||||
Constraints: layout.Exact(image.Pt(300, 300)),
|
Constraints: layout.Exact(image.Pt(300, 300)),
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
sentence := "\n\n\n\n\n\n\n\n\n\n\n\nthe quick brown fox jumps over the lazy dog"
|
sentence := "\n\n\n\n\n\n\n\n\n\n\n\nthe quick brown fox jumps over the lazy dog"
|
||||||
@@ -241,7 +241,7 @@ func TestEditor(t *testing.T) {
|
|||||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
|
|
||||||
@@ -349,7 +349,7 @@ func TestEditorRTL(t *testing.T) {
|
|||||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||||
Locale: arabic,
|
Locale: arabic,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(arabicCollection)
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(arabicCollection))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
|
|
||||||
@@ -419,14 +419,14 @@ func TestEditorLigature(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skipf("failed parsing test font: %v", err)
|
t.Skipf("failed parsing test font: %v", err)
|
||||||
}
|
}
|
||||||
cache := text.NewShaper([]font.FontFace{
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{
|
||||||
{
|
{
|
||||||
Font: font.Font{
|
Font: font.Font{
|
||||||
Typeface: "Roboto",
|
Typeface: "Roboto",
|
||||||
},
|
},
|
||||||
Face: face,
|
Face: face,
|
||||||
},
|
},
|
||||||
})
|
}))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
|
|
||||||
@@ -508,13 +508,14 @@ func TestEditorLigature(t *testing.T) {
|
|||||||
// Ensure that all runes in the final cluster of a line are properly
|
// 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.
|
// decoded when moving to the end of the line. This is a regression test.
|
||||||
e.text.MoveEnd(selectionClear)
|
e.text.MoveEnd(selectionClear)
|
||||||
// The first line was broken by line wrapping, not a newline character. As such,
|
// The first line was broken by line wrapping, not a newline character, and has a trailing
|
||||||
// the cursor can reach the position after the final glyph (a space).
|
// whitespace. However, we should never be able to reach the "other side" of such a trailing
|
||||||
assertCaret(t, e, 0, 14, len("fflffl fflffl "))
|
// whitespace glyph.
|
||||||
|
assertCaret(t, e, 0, 13, len("fflffl fflffl"))
|
||||||
e.text.MoveLines(1, selectionClear)
|
e.text.MoveLines(1, selectionClear)
|
||||||
assertCaret(t, e, 1, 13, len("fflffl fflffl fflffl fflffl"))
|
assertCaret(t, e, 1, 13, len("fflffl fflffl fflffl fflffl"))
|
||||||
e.text.MoveLines(-1, selectionClear)
|
e.text.MoveLines(-1, selectionClear)
|
||||||
assertCaret(t, e, 0, 14, len("fflffl fflffl "))
|
assertCaret(t, e, 0, 13, len("fflffl fflffl"))
|
||||||
|
|
||||||
// Absurdly narrow constraints to force each ligature onto its own line.
|
// Absurdly narrow constraints to force each ligature onto its own line.
|
||||||
gtx.Constraints = layout.Exact(image.Pt(10, 10))
|
gtx.Constraints = layout.Exact(image.Pt(10, 10))
|
||||||
@@ -540,7 +541,7 @@ func TestEditorDimensions(t *testing.T) {
|
|||||||
Queue: tq,
|
Queue: tq,
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||||
@@ -587,7 +588,7 @@ func TestEditorCaretConsistency(t *testing.T) {
|
|||||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
|
for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
|
||||||
@@ -679,7 +680,7 @@ func TestEditorMoveWord(t *testing.T) {
|
|||||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
e.SetText(t)
|
e.SetText(t)
|
||||||
@@ -784,7 +785,7 @@ func TestEditorInsert(t *testing.T) {
|
|||||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
e.SetText(t)
|
e.SetText(t)
|
||||||
@@ -874,7 +875,7 @@ func TestEditorDeleteWord(t *testing.T) {
|
|||||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
e.SetText(t)
|
e.SetText(t)
|
||||||
@@ -928,7 +929,7 @@ g 2 4 6 8 g
|
|||||||
Ops: new(op.Ops),
|
Ops: new(op.Ops),
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
|
|
||||||
@@ -1026,7 +1027,7 @@ func TestSelectMove(t *testing.T) {
|
|||||||
Ops: new(op.Ops),
|
Ops: new(op.Ops),
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
|
|
||||||
@@ -1114,7 +1115,7 @@ func TestEditor_MaxLen(t *testing.T) {
|
|||||||
key.SelectionEvent{Start: 4, End: 4},
|
key.SelectionEvent{Start: 4, End: 4},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||||
@@ -1145,7 +1146,7 @@ func TestEditor_Filter(t *testing.T) {
|
|||||||
key.SelectionEvent{Start: 4, End: 4},
|
key.SelectionEvent{Start: 4, End: 4},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||||
@@ -1169,7 +1170,7 @@ func TestEditor_Submit(t *testing.T) {
|
|||||||
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"},
|
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||||
|
|||||||
+50
-38
@@ -44,12 +44,11 @@ type glyphIndex struct {
|
|||||||
prog text.Flags
|
prog text.Flags
|
||||||
// clusterAdvance accumulates the advances of glyphs in a glyph cluster.
|
// clusterAdvance accumulates the advances of glyphs in a glyph cluster.
|
||||||
clusterAdvance fixed.Int26_6
|
clusterAdvance fixed.Int26_6
|
||||||
// skipPrior controls whether a text position is inserted "before" the
|
|
||||||
// next glyph. Usually this should not happen, but the boundaries of
|
|
||||||
// lines and bidi runs require it.
|
|
||||||
skipPrior bool
|
|
||||||
// truncated indicates that the text was truncated by the shaper.
|
// truncated indicates that the text was truncated by the shaper.
|
||||||
truncated bool
|
truncated bool
|
||||||
|
// midCluster tracks whether the next glyph processed is not the first glyph in a
|
||||||
|
// cluster.
|
||||||
|
midCluster bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset prepares the index for reuse.
|
// reset prepares the index for reuse.
|
||||||
@@ -63,8 +62,8 @@ func (g *glyphIndex) reset() {
|
|||||||
g.pos = combinedPos{}
|
g.pos = combinedPos{}
|
||||||
g.prog = 0
|
g.prog = 0
|
||||||
g.clusterAdvance = 0
|
g.clusterAdvance = 0
|
||||||
g.skipPrior = false
|
|
||||||
g.truncated = false
|
g.truncated = false
|
||||||
|
g.midCluster = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// screenPos represents a character position in text line and column numbers,
|
// screenPos represents a character position in text line and column numbers,
|
||||||
@@ -113,6 +112,20 @@ func (g *glyphIndex) incrementPosition(pos combinedPos) (next combinedPos, eof b
|
|||||||
return candidate, true
|
return candidate, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *glyphIndex) insertPosition(pos combinedPos) {
|
||||||
|
lastIdx := len(g.positions) - 1
|
||||||
|
if lastIdx >= 0 {
|
||||||
|
lastPos := g.positions[lastIdx]
|
||||||
|
if lastPos.runes == pos.runes && (lastPos.y != pos.y || (lastPos.x == pos.x)) {
|
||||||
|
// If we insert a consecutive position with the same logical position,
|
||||||
|
// overwrite the previous position with the new one.
|
||||||
|
g.positions[lastIdx] = pos
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.positions = append(g.positions, pos)
|
||||||
|
}
|
||||||
|
|
||||||
// Glyph indexes the provided glyph, generating text cursor positions for it.
|
// Glyph indexes the provided glyph, generating text cursor positions for it.
|
||||||
func (g *glyphIndex) Glyph(gl text.Glyph) {
|
func (g *glyphIndex) Glyph(gl text.Glyph) {
|
||||||
g.glyphs = append(g.glyphs, gl)
|
g.glyphs = append(g.glyphs, gl)
|
||||||
@@ -128,30 +141,32 @@ func (g *glyphIndex) Glyph(gl text.Glyph) {
|
|||||||
if end := gl.X + gl.Advance; end > g.currentLineMax {
|
if end := gl.X + gl.Advance; end > g.currentLineMax {
|
||||||
g.currentLineMax = end
|
g.currentLineMax = end
|
||||||
}
|
}
|
||||||
if !g.skipPrior || gl.Flags&text.FlagTowardOrigin != g.prog || gl.Flags&text.FlagParagraphStart != 0 {
|
|
||||||
// Set the new text progression based on that of the first glyph.
|
|
||||||
g.prog = gl.Flags & text.FlagTowardOrigin
|
|
||||||
g.pos.towardOrigin = g.prog == text.FlagTowardOrigin
|
|
||||||
// Create the text position prior to the first glyph.
|
|
||||||
pos := g.pos
|
|
||||||
pos.x = gl.X
|
|
||||||
pos.y = int(gl.Y)
|
|
||||||
pos.ascent = gl.Ascent
|
|
||||||
pos.descent = gl.Descent
|
|
||||||
if pos.towardOrigin {
|
|
||||||
pos.x += gl.Advance
|
|
||||||
}
|
|
||||||
g.pos = pos
|
|
||||||
g.positions = append(g.positions, pos)
|
|
||||||
g.skipPrior = true
|
|
||||||
}
|
|
||||||
needsNewLine := gl.Flags&text.FlagLineBreak != 0
|
needsNewLine := gl.Flags&text.FlagLineBreak != 0
|
||||||
needsNewRun := gl.Flags&text.FlagRunBreak != 0
|
needsNewRun := gl.Flags&text.FlagRunBreak != 0
|
||||||
breaksParagraph := gl.Flags&text.FlagParagraphBreak != 0
|
breaksParagraph := gl.Flags&text.FlagParagraphBreak != 0
|
||||||
|
breaksCluster := gl.Flags&text.FlagClusterBreak != 0
|
||||||
// We should insert new positions if the glyph we're processing terminates
|
// We should insert new positions if the glyph we're processing terminates
|
||||||
// a glyph cluster.
|
// a glyph cluster, has nonzero runes, and is not a hard newline.
|
||||||
insertPositionAfter := gl.Flags&text.FlagClusterBreak != 0 && !breaksParagraph && gl.Runes > 0
|
insertPositionsWithin := breaksCluster && !breaksParagraph && gl.Runes > 0
|
||||||
|
|
||||||
|
// Get the text progression/direction right.
|
||||||
|
g.prog = gl.Flags & text.FlagTowardOrigin
|
||||||
|
g.pos.towardOrigin = g.prog == text.FlagTowardOrigin
|
||||||
|
if !g.midCluster {
|
||||||
|
// Create the text position prior to the glyph.
|
||||||
|
g.pos.x = gl.X
|
||||||
|
g.pos.y = int(gl.Y)
|
||||||
|
g.pos.ascent = gl.Ascent
|
||||||
|
g.pos.descent = gl.Descent
|
||||||
|
if g.pos.towardOrigin {
|
||||||
|
g.pos.x += gl.Advance
|
||||||
|
}
|
||||||
|
g.insertPosition(g.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.midCluster = !breaksCluster
|
||||||
|
|
||||||
if breaksParagraph {
|
if breaksParagraph {
|
||||||
// Paragraph breaking clusters shouldn't have positions generated for both
|
// Paragraph breaking clusters shouldn't have positions generated for both
|
||||||
// sides of them. They're always zero-width, so doing so would
|
// sides of them. They're always zero-width, so doing so would
|
||||||
@@ -164,12 +179,11 @@ func (g *glyphIndex) Glyph(gl text.Glyph) {
|
|||||||
// Always track the cumulative advance added by the glyph, even if it
|
// Always track the cumulative advance added by the glyph, even if it
|
||||||
// doesn't terminate a cluster itself.
|
// doesn't terminate a cluster itself.
|
||||||
g.clusterAdvance += gl.Advance
|
g.clusterAdvance += gl.Advance
|
||||||
if insertPositionAfter {
|
if insertPositionsWithin {
|
||||||
// Construct the text position _after_ gl.
|
// Construct the text positions _within_ gl.
|
||||||
pos := g.pos
|
g.pos.y = int(gl.Y)
|
||||||
pos.y = int(gl.Y)
|
g.pos.ascent = gl.Ascent
|
||||||
pos.ascent = gl.Ascent
|
g.pos.descent = gl.Descent
|
||||||
pos.descent = gl.Descent
|
|
||||||
width := g.clusterAdvance
|
width := g.clusterAdvance
|
||||||
positionCount := int(gl.Runes)
|
positionCount := int(gl.Runes)
|
||||||
runesPerPosition := 1
|
runesPerPosition := 1
|
||||||
@@ -181,19 +195,18 @@ func (g *glyphIndex) Glyph(gl text.Glyph) {
|
|||||||
}
|
}
|
||||||
perRune := width / fixed.Int26_6(positionCount)
|
perRune := width / fixed.Int26_6(positionCount)
|
||||||
adjust := fixed.Int26_6(0)
|
adjust := fixed.Int26_6(0)
|
||||||
if pos.towardOrigin {
|
if g.pos.towardOrigin {
|
||||||
// If RTL, subtract increments from the width of the cluster
|
// If RTL, subtract increments from the width of the cluster
|
||||||
// instead of adding.
|
// instead of adding.
|
||||||
adjust = width
|
adjust = width
|
||||||
perRune = -perRune
|
perRune = -perRune
|
||||||
}
|
}
|
||||||
for i := 1; i <= positionCount; i++ {
|
for i := 1; i <= positionCount; i++ {
|
||||||
pos.x = gl.X + adjust + perRune*fixed.Int26_6(i)
|
g.pos.x = gl.X + adjust + perRune*fixed.Int26_6(i)
|
||||||
pos.runes += runesPerPosition
|
g.pos.runes += runesPerPosition
|
||||||
pos.lineCol.col += runesPerPosition
|
g.pos.lineCol.col += runesPerPosition
|
||||||
g.positions = append(g.positions, pos)
|
g.insertPosition(g.pos)
|
||||||
}
|
}
|
||||||
g.pos = pos
|
|
||||||
g.clusterAdvance = 0
|
g.clusterAdvance = 0
|
||||||
}
|
}
|
||||||
if needsNewRun {
|
if needsNewRun {
|
||||||
@@ -214,7 +227,6 @@ func (g *glyphIndex) Glyph(gl text.Glyph) {
|
|||||||
g.currentLineMin = math.MaxInt32
|
g.currentLineMin = math.MaxInt32
|
||||||
g.currentLineMax = 0
|
g.currentLineMax = 0
|
||||||
g.currentLineGlyphs = 0
|
g.currentLineGlyphs = 0
|
||||||
g.skipPrior = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+130
-65
@@ -20,7 +20,7 @@ func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (source string
|
|||||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||||
rtlFace, _ := opentype.Parse(nsareg.TTF)
|
rtlFace, _ := opentype.Parse(nsareg.TTF)
|
||||||
|
|
||||||
shaper := text.NewShaper([]font.FontFace{
|
shaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{
|
||||||
{
|
{
|
||||||
Font: font.Font{Typeface: "LTR"},
|
Font: font.Font{Typeface: "LTR"},
|
||||||
Face: ltrFace,
|
Face: ltrFace,
|
||||||
@@ -29,12 +29,11 @@ func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (source string
|
|||||||
Font: font.Font{Typeface: "RTL"},
|
Font: font.Font{Typeface: "RTL"},
|
||||||
Face: rtlFace,
|
Face: rtlFace,
|
||||||
},
|
},
|
||||||
})
|
}))
|
||||||
// bidiSource is crafted to contain multiple consecutive RTL runs (by
|
// bidiSource is crafted to contain multiple consecutive RTL runs (by
|
||||||
// changing scripts within the RTL).
|
// changing scripts within the RTL).
|
||||||
bidiSource := "The quick سماء שלום لا fox تمط שלום غير the lazy dog."
|
bidiSource := "The quick سماء שלום لا fox تمط שלום غير the lazy dog."
|
||||||
ltrParams := text.Parameters{
|
ltrParams := text.Parameters{
|
||||||
Font: font.Font{Typeface: "LTR"},
|
|
||||||
PxPerEm: fixed.I(fontSize),
|
PxPerEm: fixed.I(fontSize),
|
||||||
MaxWidth: lineWidth,
|
MaxWidth: lineWidth,
|
||||||
MinWidth: lineWidth,
|
MinWidth: lineWidth,
|
||||||
@@ -42,7 +41,6 @@ func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (source string
|
|||||||
}
|
}
|
||||||
rtlParams := text.Parameters{
|
rtlParams := text.Parameters{
|
||||||
Alignment: text.End,
|
Alignment: text.End,
|
||||||
Font: font.Font{Typeface: "RTL"},
|
|
||||||
PxPerEm: fixed.I(fontSize),
|
PxPerEm: fixed.I(fontSize),
|
||||||
MaxWidth: lineWidth,
|
MaxWidth: lineWidth,
|
||||||
MinWidth: lineWidth,
|
MinWidth: lineWidth,
|
||||||
@@ -69,7 +67,7 @@ func makeAccountingTestText(str string, fontSize, lineWidth int) (txt []text.Gly
|
|||||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||||
rtlFace, _ := opentype.Parse(nsareg.TTF)
|
rtlFace, _ := opentype.Parse(nsareg.TTF)
|
||||||
|
|
||||||
shaper := text.NewShaper([]font.FontFace{{
|
shaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{{
|
||||||
Font: font.Font{Typeface: "LTR"},
|
Font: font.Font{Typeface: "LTR"},
|
||||||
Face: ltrFace,
|
Face: ltrFace,
|
||||||
},
|
},
|
||||||
@@ -77,7 +75,7 @@ func makeAccountingTestText(str string, fontSize, lineWidth int) (txt []text.Gly
|
|||||||
Font: font.Font{Typeface: "RTL"},
|
Font: font.Font{Typeface: "RTL"},
|
||||||
Face: rtlFace,
|
Face: rtlFace,
|
||||||
},
|
},
|
||||||
})
|
}))
|
||||||
params := text.Parameters{
|
params := text.Parameters{
|
||||||
PxPerEm: fixed.I(fontSize),
|
PxPerEm: fixed.I(fontSize),
|
||||||
MaxWidth: lineWidth,
|
MaxWidth: lineWidth,
|
||||||
@@ -95,7 +93,7 @@ func getGlyphs(fontSize, minWidth, lineWidth int, align text.Alignment, str stri
|
|||||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||||
rtlFace, _ := opentype.Parse(nsareg.TTF)
|
rtlFace, _ := opentype.Parse(nsareg.TTF)
|
||||||
|
|
||||||
shaper := text.NewShaper([]font.FontFace{{
|
shaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{{
|
||||||
Font: font.Font{Typeface: "LTR"},
|
Font: font.Font{Typeface: "LTR"},
|
||||||
Face: ltrFace,
|
Face: ltrFace,
|
||||||
},
|
},
|
||||||
@@ -103,13 +101,14 @@ func getGlyphs(fontSize, minWidth, lineWidth int, align text.Alignment, str stri
|
|||||||
Font: font.Font{Typeface: "RTL"},
|
Font: font.Font{Typeface: "RTL"},
|
||||||
Face: rtlFace,
|
Face: rtlFace,
|
||||||
},
|
},
|
||||||
})
|
}))
|
||||||
params := text.Parameters{
|
params := text.Parameters{
|
||||||
PxPerEm: fixed.I(fontSize),
|
PxPerEm: fixed.I(fontSize),
|
||||||
Alignment: align,
|
Alignment: align,
|
||||||
MinWidth: minWidth,
|
MinWidth: minWidth,
|
||||||
MaxWidth: lineWidth,
|
MaxWidth: lineWidth,
|
||||||
Locale: english,
|
Locale: english,
|
||||||
|
WrapPolicy: text.WrapWords,
|
||||||
}
|
}
|
||||||
shaper.LayoutString(params, str)
|
shaper.LayoutString(params, str)
|
||||||
for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() {
|
for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() {
|
||||||
@@ -122,30 +121,34 @@ func getGlyphs(fontSize, minWidth, lineWidth int, align text.Alignment, str stri
|
|||||||
// for empty lines and the empty string.
|
// for empty lines and the empty string.
|
||||||
func TestIndexPositionWhitespace(t *testing.T) {
|
func TestIndexPositionWhitespace(t *testing.T) {
|
||||||
type testcase struct {
|
type testcase struct {
|
||||||
name string
|
name string
|
||||||
str string
|
str string
|
||||||
align text.Alignment
|
lineWidth int
|
||||||
expected []combinedPos
|
align text.Alignment
|
||||||
|
expected []combinedPos
|
||||||
}
|
}
|
||||||
for _, tc := range []testcase{
|
for _, tc := range []testcase{
|
||||||
{
|
{
|
||||||
name: "empty string",
|
name: "empty string",
|
||||||
str: "",
|
str: "",
|
||||||
|
lineWidth: 200,
|
||||||
expected: []combinedPos{
|
expected: []combinedPos{
|
||||||
{x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)},
|
{x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "just hard newline",
|
name: "just hard newline",
|
||||||
str: "\n",
|
str: "\n",
|
||||||
|
lineWidth: 200,
|
||||||
expected: []combinedPos{
|
expected: []combinedPos{
|
||||||
{x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)},
|
{x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)},
|
||||||
{x: fixed.Int26_6(0), y: 35, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{line: 1}},
|
{x: fixed.Int26_6(0), y: 35, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{line: 1}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "trailing newline",
|
name: "trailing newline",
|
||||||
str: "a\n",
|
str: "a\n",
|
||||||
|
lineWidth: 200,
|
||||||
expected: []combinedPos{
|
expected: []combinedPos{
|
||||||
{x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)},
|
{x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)},
|
||||||
{x: fixed.Int26_6(570), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{col: 1}},
|
{x: fixed.Int26_6(570), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{col: 1}},
|
||||||
@@ -153,8 +156,9 @@ func TestIndexPositionWhitespace(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "just blank line",
|
name: "just blank line",
|
||||||
str: "\n\n",
|
str: "\n\n",
|
||||||
|
lineWidth: 200,
|
||||||
expected: []combinedPos{
|
expected: []combinedPos{
|
||||||
{x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)},
|
{x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)},
|
||||||
{x: fixed.Int26_6(0), y: 35, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{line: 1}},
|
{x: fixed.Int26_6(0), y: 35, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{line: 1}},
|
||||||
@@ -162,9 +166,10 @@ func TestIndexPositionWhitespace(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "middle aligned blank lines",
|
name: "middle aligned blank lines",
|
||||||
str: "\n\n\nabc",
|
str: "\n\n\nabc",
|
||||||
align: text.Middle,
|
align: text.Middle,
|
||||||
|
lineWidth: 200,
|
||||||
expected: []combinedPos{
|
expected: []combinedPos{
|
||||||
{x: fixed.Int26_6(832), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)},
|
{x: fixed.Int26_6(832), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)},
|
||||||
{x: fixed.Int26_6(832), y: 35, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{line: 1}},
|
{x: fixed.Int26_6(832), y: 35, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{line: 1}},
|
||||||
@@ -176,8 +181,9 @@ func TestIndexPositionWhitespace(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "blank line",
|
name: "blank line",
|
||||||
str: "a\n\nb",
|
str: "a\n\nb",
|
||||||
|
lineWidth: 200,
|
||||||
expected: []combinedPos{
|
expected: []combinedPos{
|
||||||
{x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)},
|
{x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)},
|
||||||
{x: fixed.Int26_6(570), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{col: 1}},
|
{x: fixed.Int26_6(570), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{col: 1}},
|
||||||
@@ -186,9 +192,45 @@ func TestIndexPositionWhitespace(t *testing.T) {
|
|||||||
{x: fixed.Int26_6(570), y: 54, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 4, lineCol: screenPos{line: 2, col: 1}},
|
{x: fixed.Int26_6(570), y: 54, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 4, lineCol: screenPos{line: 2, col: 1}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "soft wrap",
|
||||||
|
str: "abc def",
|
||||||
|
lineWidth: 30,
|
||||||
|
expected: []combinedPos{
|
||||||
|
{runes: 0, lineCol: screenPos{line: 0, col: 0}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 0, y: 16},
|
||||||
|
{runes: 1, lineCol: screenPos{line: 0, col: 1}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 570, y: 16},
|
||||||
|
{runes: 2, lineCol: screenPos{line: 0, col: 2}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 1140, y: 16},
|
||||||
|
{runes: 3, lineCol: screenPos{line: 0, col: 3}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 1652, y: 16},
|
||||||
|
{runes: 4, lineCol: screenPos{line: 1, col: 0}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 0, y: 35},
|
||||||
|
{runes: 5, lineCol: screenPos{line: 1, col: 1}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 570, y: 35},
|
||||||
|
{runes: 6, lineCol: screenPos{line: 1, col: 2}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 1140, y: 35},
|
||||||
|
{runes: 7, lineCol: screenPos{line: 1, col: 3}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 1425, y: 35},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "soft wrap arabic",
|
||||||
|
str: "ثنائي الاتجاه",
|
||||||
|
lineWidth: 30,
|
||||||
|
expected: []combinedPos{
|
||||||
|
{runes: 0, lineCol: screenPos{line: 0, col: 0}, ascent: 1407, descent: 756, x: 2250, y: 22, towardOrigin: true},
|
||||||
|
{runes: 1, lineCol: screenPos{line: 0, col: 1}, ascent: 1407, descent: 756, x: 1944, y: 22, towardOrigin: true},
|
||||||
|
{runes: 2, lineCol: screenPos{line: 0, col: 2}, ascent: 1407, descent: 756, x: 1593, y: 22, towardOrigin: true},
|
||||||
|
{runes: 3, lineCol: screenPos{line: 0, col: 3}, ascent: 1407, descent: 756, x: 1295, y: 22, towardOrigin: true},
|
||||||
|
{runes: 4, lineCol: screenPos{line: 0, col: 4}, ascent: 1407, descent: 756, x: 1020, y: 22, towardOrigin: true},
|
||||||
|
{runes: 5, lineCol: screenPos{line: 0, col: 5}, ascent: 1407, descent: 756, x: 266, y: 22, towardOrigin: true},
|
||||||
|
{runes: 6, lineCol: screenPos{line: 1, col: 0}, ascent: 1407, descent: 756, x: 2511, y: 41, towardOrigin: true},
|
||||||
|
{runes: 7, lineCol: screenPos{line: 1, col: 1}, ascent: 1407, descent: 756, x: 2267, y: 41, towardOrigin: true},
|
||||||
|
{runes: 8, lineCol: screenPos{line: 1, col: 2}, ascent: 1407, descent: 756, x: 1969, y: 41, towardOrigin: true},
|
||||||
|
{runes: 9, lineCol: screenPos{line: 1, col: 3}, ascent: 1407, descent: 756, x: 1671, y: 41, towardOrigin: true},
|
||||||
|
{runes: 10, lineCol: screenPos{line: 1, col: 4}, ascent: 1407, descent: 756, x: 1365, y: 41, towardOrigin: true},
|
||||||
|
{runes: 11, lineCol: screenPos{line: 1, col: 5}, ascent: 1407, descent: 756, x: 713, y: 41, towardOrigin: true},
|
||||||
|
{runes: 12, lineCol: screenPos{line: 1, col: 6}, ascent: 1407, descent: 756, x: 415, y: 41, towardOrigin: true},
|
||||||
|
{runes: 13, lineCol: screenPos{line: 1, col: 7}, ascent: 1407, descent: 756, x: 0, y: 41, towardOrigin: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
glyphs := getGlyphs(16, 0, 200, tc.align, tc.str)
|
glyphs := getGlyphs(16, 0, tc.lineWidth, tc.align, tc.str)
|
||||||
var gi glyphIndex
|
var gi glyphIndex
|
||||||
gi.reset()
|
gi.reset()
|
||||||
for _, g := range glyphs {
|
for _, g := range glyphs {
|
||||||
@@ -229,9 +271,12 @@ func TestIndexPositionBidi(t *testing.T) {
|
|||||||
name: "bidi ltr",
|
name: "bidi ltr",
|
||||||
glyphs: bidiLTRText,
|
glyphs: bidiLTRText,
|
||||||
expectedXs: []fixed.Int26_6{
|
expectedXs: []fixed.Int26_6{
|
||||||
0, 626, 1196, 1766, 2051, 2621, 3191, 3444, 3956, 4468, 4753, 7133, 6330, 5738, 5440, 5019, 4753, // Positions on line 0.
|
0, 626, 1196, 1766, 2051, 2621, 3191, 3444, 3956, 4468, 4753, 7133, 6330, 5738, 5440, 5019, // Positions on line 0.
|
||||||
3953, 3185, 2417, 1649, 881, 596, 298, 0, 3953, 4238, 4523, 5093, 5605, 5890, 7905, 7599, 7007, 6156, 5890, // Positions on line 1.
|
|
||||||
4660, 3892, 3124, 2356, 1588, 1303, 788, 406, 0, 4660, 4945, 5235, 5805, 6375, 6660, 6934, 7504, 8016, 8528, 8813, // Positions on line 2.
|
3953, 3185, 2417, 1649, 881, 596, 298, 0, 3953, 4238, 4523, 5093, 5605, 5890, 7905, 7599, 7007, 6156, // Positions on line 1.
|
||||||
|
|
||||||
|
4660, 3892, 3124, 2356, 1588, 1303, 788, 406, 0, 4660, 4945, 5235, 5805, 6375, 6660, 6934, 7504, 8016, 8528, // Positions on line 2.
|
||||||
|
|
||||||
0, 570, 1140, 1710, 2034, // Positions on line 3.
|
0, 570, 1140, 1710, 2034, // Positions on line 3.
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -239,9 +284,13 @@ func TestIndexPositionBidi(t *testing.T) {
|
|||||||
name: "bidi rtl",
|
name: "bidi rtl",
|
||||||
glyphs: bidiRTLText,
|
glyphs: bidiRTLText,
|
||||||
expectedXs: []fixed.Int26_6{
|
expectedXs: []fixed.Int26_6{
|
||||||
5368, 5994, 6564, 7134, 7419, 7989, 8559, 8812, 9324, 9836, 5368, 5102, 4299, 3707, 3409, 2988, 2722, 2108, 1494, 880, 266, 0, // Positions on line 0.
|
2665, 3291, 3861, 4431, 4716, 5286, 5856, 6109, 6621, 7133, 2665, 2380, 1577, 985, 687, 266, // Positions on line 0.
|
||||||
8801, 8503, 8205, 7939, 6572, 6857, 7427, 7939, 6572, 6306, 6000, 5408, 4557, 4291, 3677, 3063, 2449, 1835, 1569, 1054, 672, 266, 0, // Positions on line 1.
|
|
||||||
274, 564, 1134, 1704, 1989, 2263, 2833, 3345, 3857, 4142, 4712, 5282, 5852, 274, 0, // Positions on line 2.
|
7886, 7118, 6350, 5582, 4814, 4529, 4231, 3933, 3667, 2300, 2585, 3155, 3667, 2300, 2015, 1709, 1117, 266, // Positions on line 1.
|
||||||
|
|
||||||
|
8794, 8026, 7258, 6490, 5722, 5437, 4922, 4540, 4134, 3868, 0, 290, 860, 1430, 1715, 1989, 2559, 3071, 3583, // Positions on line 2.
|
||||||
|
|
||||||
|
324, 894, 1464, 2034, 324, 0, // Positions on line 3.
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
@@ -320,7 +369,7 @@ func TestIndexPositionLines(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
xOff: fixed.Int26_6(0),
|
xOff: fixed.Int26_6(0),
|
||||||
yOff: 56,
|
yOff: 41,
|
||||||
glyphs: 15,
|
glyphs: 15,
|
||||||
width: fixed.Int26_6(7905),
|
width: fixed.Int26_6(7905),
|
||||||
ascent: fixed.Int26_6(1407),
|
ascent: fixed.Int26_6(1407),
|
||||||
@@ -328,7 +377,7 @@ func TestIndexPositionLines(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
xOff: fixed.Int26_6(0),
|
xOff: fixed.Int26_6(0),
|
||||||
yOff: 90,
|
yOff: 60,
|
||||||
glyphs: 18,
|
glyphs: 18,
|
||||||
width: fixed.Int26_6(8813),
|
width: fixed.Int26_6(8813),
|
||||||
ascent: fixed.Int26_6(1407),
|
ascent: fixed.Int26_6(1407),
|
||||||
@@ -336,7 +385,7 @@ func TestIndexPositionLines(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
xOff: fixed.Int26_6(0),
|
xOff: fixed.Int26_6(0),
|
||||||
yOff: 117,
|
yOff: 79,
|
||||||
glyphs: 4,
|
glyphs: 4,
|
||||||
width: fixed.Int26_6(2034),
|
width: fixed.Int26_6(2034),
|
||||||
ascent: fixed.Int26_6(968),
|
ascent: fixed.Int26_6(968),
|
||||||
@@ -352,27 +401,35 @@ func TestIndexPositionLines(t *testing.T) {
|
|||||||
{
|
{
|
||||||
xOff: fixed.Int26_6(0),
|
xOff: fixed.Int26_6(0),
|
||||||
yOff: 22,
|
yOff: 22,
|
||||||
glyphs: 20,
|
glyphs: 15,
|
||||||
width: fixed.Int26_6(9836),
|
width: fixed.Int26_6(7133),
|
||||||
ascent: fixed.Int26_6(1407),
|
ascent: fixed.Int26_6(1407),
|
||||||
descent: fixed.Int26_6(756),
|
descent: fixed.Int26_6(756),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xOff: fixed.Int26_6(0),
|
xOff: fixed.Int26_6(0),
|
||||||
yOff: 56,
|
yOff: 41,
|
||||||
glyphs: 19,
|
glyphs: 15,
|
||||||
width: fixed.Int26_6(8801),
|
width: fixed.Int26_6(7886),
|
||||||
ascent: fixed.Int26_6(1407),
|
ascent: fixed.Int26_6(1407),
|
||||||
descent: fixed.Int26_6(756),
|
descent: fixed.Int26_6(756),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xOff: fixed.Int26_6(0),
|
xOff: fixed.Int26_6(0),
|
||||||
yOff: 90,
|
yOff: 60,
|
||||||
glyphs: 13,
|
glyphs: 18,
|
||||||
width: fixed.Int26_6(5852),
|
width: fixed.Int26_6(8794),
|
||||||
ascent: fixed.Int26_6(1407),
|
ascent: fixed.Int26_6(1407),
|
||||||
descent: fixed.Int26_6(756),
|
descent: fixed.Int26_6(756),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
xOff: fixed.Int26_6(0),
|
||||||
|
yOff: 79,
|
||||||
|
glyphs: 4,
|
||||||
|
width: fixed.Int26_6(2034),
|
||||||
|
ascent: fixed.Int26_6(968),
|
||||||
|
descent: fixed.Int26_6(216),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -390,7 +447,7 @@ func TestIndexPositionLines(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
xOff: fixed.Int26_6(2335),
|
xOff: fixed.Int26_6(2335),
|
||||||
yOff: 56,
|
yOff: 41,
|
||||||
glyphs: 15,
|
glyphs: 15,
|
||||||
width: fixed.Int26_6(7905),
|
width: fixed.Int26_6(7905),
|
||||||
ascent: fixed.Int26_6(1407),
|
ascent: fixed.Int26_6(1407),
|
||||||
@@ -398,7 +455,7 @@ func TestIndexPositionLines(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
xOff: fixed.Int26_6(1427),
|
xOff: fixed.Int26_6(1427),
|
||||||
yOff: 90,
|
yOff: 60,
|
||||||
glyphs: 18,
|
glyphs: 18,
|
||||||
width: fixed.Int26_6(8813),
|
width: fixed.Int26_6(8813),
|
||||||
ascent: fixed.Int26_6(1407),
|
ascent: fixed.Int26_6(1407),
|
||||||
@@ -406,7 +463,7 @@ func TestIndexPositionLines(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
xOff: fixed.Int26_6(8206),
|
xOff: fixed.Int26_6(8206),
|
||||||
yOff: 117,
|
yOff: 79,
|
||||||
glyphs: 4,
|
glyphs: 4,
|
||||||
width: fixed.Int26_6(2034),
|
width: fixed.Int26_6(2034),
|
||||||
ascent: fixed.Int26_6(968),
|
ascent: fixed.Int26_6(968),
|
||||||
@@ -420,29 +477,37 @@ func TestIndexPositionLines(t *testing.T) {
|
|||||||
glyphs: bidiRTLTextOpp,
|
glyphs: bidiRTLTextOpp,
|
||||||
expectedLines: []lineInfo{
|
expectedLines: []lineInfo{
|
||||||
{
|
{
|
||||||
xOff: fixed.Int26_6(404),
|
xOff: fixed.Int26_6(3107),
|
||||||
yOff: 22,
|
yOff: 22,
|
||||||
glyphs: 20,
|
glyphs: 15,
|
||||||
width: fixed.Int26_6(9836),
|
width: fixed.Int26_6(7133),
|
||||||
ascent: fixed.Int26_6(1407),
|
ascent: fixed.Int26_6(1407),
|
||||||
descent: fixed.Int26_6(756),
|
descent: fixed.Int26_6(756),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xOff: fixed.Int26_6(1439),
|
xOff: fixed.Int26_6(2354),
|
||||||
yOff: 56,
|
yOff: 41,
|
||||||
glyphs: 19,
|
glyphs: 15,
|
||||||
width: fixed.Int26_6(8801),
|
width: fixed.Int26_6(7886),
|
||||||
ascent: fixed.Int26_6(1407),
|
ascent: fixed.Int26_6(1407),
|
||||||
descent: fixed.Int26_6(756),
|
descent: fixed.Int26_6(756),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xOff: fixed.Int26_6(4388),
|
xOff: fixed.Int26_6(1446),
|
||||||
yOff: 90,
|
yOff: 60,
|
||||||
glyphs: 13,
|
glyphs: 18,
|
||||||
width: fixed.Int26_6(5852),
|
width: fixed.Int26_6(8794),
|
||||||
ascent: fixed.Int26_6(1407),
|
ascent: fixed.Int26_6(1407),
|
||||||
descent: fixed.Int26_6(756),
|
descent: fixed.Int26_6(756),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
xOff: fixed.Int26_6(8206),
|
||||||
|
yOff: 79,
|
||||||
|
glyphs: 4,
|
||||||
|
width: fixed.Int26_6(2034),
|
||||||
|
ascent: fixed.Int26_6(968),
|
||||||
|
descent: fixed.Int26_6(216),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
@@ -503,13 +568,13 @@ func TestIndexPositionRunes(t *testing.T) {
|
|||||||
{runes: 12, lineCol: screenPos{line: 1, col: 8}, runIndex: 1, towardOrigin: true},
|
{runes: 12, lineCol: screenPos{line: 1, col: 8}, runIndex: 1, towardOrigin: true},
|
||||||
{runes: 13, lineCol: screenPos{line: 1, col: 9}, runIndex: 1, towardOrigin: true},
|
{runes: 13, lineCol: screenPos{line: 1, col: 9}, runIndex: 1, towardOrigin: true},
|
||||||
{runes: 14, lineCol: screenPos{line: 1, col: 10}, runIndex: 1, towardOrigin: true},
|
{runes: 14, lineCol: screenPos{line: 1, col: 10}, runIndex: 1, towardOrigin: true},
|
||||||
{runes: 15, lineCol: screenPos{line: 1, col: 11}, runIndex: 1, towardOrigin: true},
|
{runes: 15, lineCol: screenPos{line: 1, col: 11}, runIndex: 2, towardOrigin: true},
|
||||||
{runes: 16, lineCol: screenPos{line: 1, col: 12}, runIndex: 2, towardOrigin: true},
|
{runes: 16, lineCol: screenPos{line: 1, col: 12}, runIndex: 2, towardOrigin: true},
|
||||||
{runes: 17, lineCol: screenPos{line: 1, col: 13}, runIndex: 2, towardOrigin: true},
|
{runes: 17, lineCol: screenPos{line: 1, col: 13}, runIndex: 2, towardOrigin: true},
|
||||||
{runes: 18, lineCol: screenPos{line: 2, col: 0}, runIndex: 0, towardOrigin: true},
|
{runes: 18, lineCol: screenPos{line: 2, col: 0}, runIndex: 0, towardOrigin: true},
|
||||||
{runes: 19, lineCol: screenPos{line: 2, col: 1}, runIndex: 0, towardOrigin: true},
|
{runes: 19, lineCol: screenPos{line: 2, col: 1}, runIndex: 0, towardOrigin: true},
|
||||||
{runes: 20, lineCol: screenPos{line: 2, col: 2}, runIndex: 0, towardOrigin: true},
|
{runes: 20, lineCol: screenPos{line: 2, col: 2}, runIndex: 0, towardOrigin: true},
|
||||||
{runes: 21, lineCol: screenPos{line: 2, col: 3}, runIndex: 0, towardOrigin: true},
|
{runes: 21, lineCol: screenPos{line: 2, col: 3}, runIndex: 1, towardOrigin: true},
|
||||||
{runes: 22, lineCol: screenPos{line: 2, col: 4}, runIndex: 1, towardOrigin: true},
|
{runes: 22, lineCol: screenPos{line: 2, col: 4}, runIndex: 1, towardOrigin: true},
|
||||||
{runes: 23, lineCol: screenPos{line: 2, col: 5}, runIndex: 1, towardOrigin: true},
|
{runes: 23, lineCol: screenPos{line: 2, col: 5}, runIndex: 1, towardOrigin: true},
|
||||||
{runes: 24, lineCol: screenPos{line: 2, col: 6}, runIndex: 1, towardOrigin: true},
|
{runes: 24, lineCol: screenPos{line: 2, col: 6}, runIndex: 1, towardOrigin: true},
|
||||||
@@ -521,7 +586,7 @@ func TestIndexPositionRunes(t *testing.T) {
|
|||||||
{runes: 29, lineCol: screenPos{line: 3, col: 1}, runIndex: 0, towardOrigin: true},
|
{runes: 29, lineCol: screenPos{line: 3, col: 1}, runIndex: 0, towardOrigin: true},
|
||||||
{runes: 30, lineCol: screenPos{line: 3, col: 2}, runIndex: 0, towardOrigin: true},
|
{runes: 30, lineCol: screenPos{line: 3, col: 2}, runIndex: 0, towardOrigin: true},
|
||||||
{runes: 31, lineCol: screenPos{line: 3, col: 3}, runIndex: 0, towardOrigin: true},
|
{runes: 31, lineCol: screenPos{line: 3, col: 3}, runIndex: 0, towardOrigin: true},
|
||||||
{runes: 32, lineCol: screenPos{line: 3, col: 4}, runIndex: 0, towardOrigin: true},
|
{runes: 32, lineCol: screenPos{line: 3, col: 4}, runIndex: 1, towardOrigin: true},
|
||||||
{runes: 33, lineCol: screenPos{line: 3, col: 5}, runIndex: 1, towardOrigin: true},
|
{runes: 33, lineCol: screenPos{line: 3, col: 5}, runIndex: 1, towardOrigin: true},
|
||||||
{runes: 34, lineCol: screenPos{line: 3, col: 6}, runIndex: 1, towardOrigin: true},
|
{runes: 34, lineCol: screenPos{line: 3, col: 6}, runIndex: 1, towardOrigin: true},
|
||||||
{runes: 35, lineCol: screenPos{line: 4, col: 0}, runIndex: 0, towardOrigin: true},
|
{runes: 35, lineCol: screenPos{line: 4, col: 0}, runIndex: 0, towardOrigin: true},
|
||||||
|
|||||||
+33
-9
@@ -30,6 +30,12 @@ type Label struct {
|
|||||||
Truncator string
|
Truncator string
|
||||||
// WrapPolicy configures how displayed text will be broken into lines.
|
// WrapPolicy configures how displayed text will be broken into lines.
|
||||||
WrapPolicy text.WrapPolicy
|
WrapPolicy text.WrapPolicy
|
||||||
|
// LineHeight controls the distance between the baselines of lines of text.
|
||||||
|
// If zero, a sensible default will be used.
|
||||||
|
LineHeight unit.Sp
|
||||||
|
// LineHeightScale applies a scaling factor to the LineHeight. If zero, a
|
||||||
|
// sensible default will be used.
|
||||||
|
LineHeightScale float32
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout the label with the given shaper, font, size, text, and material.
|
// Layout the label with the given shaper, font, size, text, and material.
|
||||||
@@ -49,16 +55,19 @@ type TextInfo struct {
|
|||||||
func (l Label) LayoutDetailed(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, txt string, textMaterial op.CallOp) (layout.Dimensions, TextInfo) {
|
func (l Label) LayoutDetailed(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, txt string, textMaterial op.CallOp) (layout.Dimensions, TextInfo) {
|
||||||
cs := gtx.Constraints
|
cs := gtx.Constraints
|
||||||
textSize := fixed.I(gtx.Sp(size))
|
textSize := fixed.I(gtx.Sp(size))
|
||||||
|
lineHeight := fixed.I(gtx.Sp(l.LineHeight))
|
||||||
lt.LayoutString(text.Parameters{
|
lt.LayoutString(text.Parameters{
|
||||||
Font: font,
|
Font: font,
|
||||||
PxPerEm: textSize,
|
PxPerEm: textSize,
|
||||||
MaxLines: l.MaxLines,
|
MaxLines: l.MaxLines,
|
||||||
Truncator: l.Truncator,
|
Truncator: l.Truncator,
|
||||||
Alignment: l.Alignment,
|
Alignment: l.Alignment,
|
||||||
WrapPolicy: l.WrapPolicy,
|
WrapPolicy: l.WrapPolicy,
|
||||||
MaxWidth: cs.Max.X,
|
MaxWidth: cs.Max.X,
|
||||||
MinWidth: cs.Min.X,
|
MinWidth: cs.Min.X,
|
||||||
Locale: gtx.Locale,
|
Locale: gtx.Locale,
|
||||||
|
LineHeight: lineHeight,
|
||||||
|
LineHeightScale: l.LineHeightScale,
|
||||||
}, txt)
|
}, txt)
|
||||||
m := op.Record(gtx.Ops)
|
m := op.Record(gtx.Ops)
|
||||||
viewport := image.Rectangle{Max: cs.Max}
|
viewport := image.Rectangle{Max: cs.Max}
|
||||||
@@ -141,11 +150,26 @@ func (it *textIterator) processGlyph(g text.Glyph, ok bool) (_ text.Glyph, visib
|
|||||||
// Compute the maximum extent to which glyphs overhang on the horizontal
|
// Compute the maximum extent to which glyphs overhang on the horizontal
|
||||||
// axis.
|
// axis.
|
||||||
if d := g.Bounds.Min.X.Floor(); d < it.padding.Min.X {
|
if d := g.Bounds.Min.X.Floor(); d < it.padding.Min.X {
|
||||||
|
// If the distance between the dot and the left edge of this glyph is
|
||||||
|
// less than the current padding, increase the left padding.
|
||||||
it.padding.Min.X = d
|
it.padding.Min.X = d
|
||||||
}
|
}
|
||||||
if d := (g.Bounds.Max.X - g.Advance).Ceil(); d > it.padding.Max.X {
|
if d := (g.Bounds.Max.X - g.Advance).Ceil(); d > it.padding.Max.X {
|
||||||
|
// If the distance between the dot and the right edge of this glyph
|
||||||
|
// minus the logical advance of this glyph is greater than the current
|
||||||
|
// padding, increase the right padding.
|
||||||
it.padding.Max.X = d
|
it.padding.Max.X = d
|
||||||
}
|
}
|
||||||
|
if d := (g.Bounds.Min.Y + g.Ascent).Floor(); d < it.padding.Min.Y {
|
||||||
|
// If the distance between the dot and the top of this glyph is greater
|
||||||
|
// than the ascent of the glyph, increase the top padding.
|
||||||
|
it.padding.Min.Y = d
|
||||||
|
}
|
||||||
|
if d := (g.Bounds.Max.Y - g.Descent).Ceil(); d > it.padding.Max.Y {
|
||||||
|
// If the distance between the dot and the bottom of this glyph is greater
|
||||||
|
// than the descent of the glyph, increase the bottom padding.
|
||||||
|
it.padding.Max.Y = d
|
||||||
|
}
|
||||||
logicalBounds := image.Rectangle{
|
logicalBounds := image.Rectangle{
|
||||||
Min: image.Pt(g.X.Floor(), int(g.Y)-g.Ascent.Ceil()),
|
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()),
|
Max: image.Pt((g.X + g.Advance).Ceil(), int(g.Y)+g.Descent.Ceil()),
|
||||||
|
|||||||
@@ -166,3 +166,67 @@ func TestGlyphIterator(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGlyphIteratorPadding ensures that the glyph iterator computes correct padding
|
||||||
|
// around glyphs with unusual bounding boxes.
|
||||||
|
func TestGlyphIteratorPadding(t *testing.T) {
|
||||||
|
type testcase struct {
|
||||||
|
name string
|
||||||
|
glyph text.Glyph
|
||||||
|
viewport image.Rectangle
|
||||||
|
expectedDims image.Rectangle
|
||||||
|
expectedPadding image.Rectangle
|
||||||
|
expectedBaseline int
|
||||||
|
}
|
||||||
|
for _, tc := range []testcase{
|
||||||
|
{
|
||||||
|
name: "simple",
|
||||||
|
glyph: text.Glyph{
|
||||||
|
X: 0,
|
||||||
|
Y: 50,
|
||||||
|
Advance: fixed.I(50),
|
||||||
|
Ascent: fixed.I(50),
|
||||||
|
Descent: fixed.I(50),
|
||||||
|
Bounds: fixed.Rectangle26_6{
|
||||||
|
Min: fixed.Point26_6{
|
||||||
|
X: fixed.I(-5),
|
||||||
|
Y: fixed.I(-56),
|
||||||
|
},
|
||||||
|
Max: fixed.Point26_6{
|
||||||
|
X: fixed.I(57),
|
||||||
|
Y: fixed.I(58),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)},
|
||||||
|
expectedDims: image.Rectangle{
|
||||||
|
Max: image.Point{X: 50, Y: 100},
|
||||||
|
},
|
||||||
|
expectedBaseline: 50,
|
||||||
|
expectedPadding: image.Rectangle{
|
||||||
|
Min: image.Point{
|
||||||
|
X: -5,
|
||||||
|
Y: -6,
|
||||||
|
},
|
||||||
|
Max: image.Point{
|
||||||
|
X: 7,
|
||||||
|
Y: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
it := textIterator{viewport: tc.viewport}
|
||||||
|
it.processGlyph(tc.glyph, true)
|
||||||
|
if it.bounds != tc.expectedDims {
|
||||||
|
t.Errorf("expected bounds %#+v, got %#+v", tc.expectedDims, it.bounds)
|
||||||
|
}
|
||||||
|
if it.baseline != tc.expectedBaseline {
|
||||||
|
t.Errorf("expected baseline %d, got %d", tc.expectedBaseline, it.baseline)
|
||||||
|
}
|
||||||
|
if it.padding != tc.expectedPadding {
|
||||||
|
t.Errorf("expected padding %d, got %d", tc.expectedPadding, it.padding)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ type IconButtonStyle struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Button(th *Theme, button *widget.Clickable, txt string) ButtonStyle {
|
func Button(th *Theme, button *widget.Clickable, txt string) ButtonStyle {
|
||||||
return ButtonStyle{
|
b := ButtonStyle{
|
||||||
Text: txt,
|
Text: txt,
|
||||||
Color: th.Palette.ContrastFg,
|
Color: th.Palette.ContrastFg,
|
||||||
CornerRadius: 4,
|
CornerRadius: 4,
|
||||||
@@ -64,6 +64,8 @@ func Button(th *Theme, button *widget.Clickable, txt string) ButtonStyle {
|
|||||||
Button: button,
|
Button: button,
|
||||||
shaper: th.Shaper,
|
shaper: th.Shaper,
|
||||||
}
|
}
|
||||||
|
b.Font.Typeface = th.Face
|
||||||
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func ButtonLayout(th *Theme, button *widget.Clickable) ButtonLayoutStyle {
|
func ButtonLayout(th *Theme, button *widget.Clickable) ButtonLayoutStyle {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type CheckBoxStyle struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle {
|
func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle {
|
||||||
return CheckBoxStyle{
|
c := CheckBoxStyle{
|
||||||
CheckBox: checkBox,
|
CheckBox: checkBox,
|
||||||
checkable: checkable{
|
checkable: checkable{
|
||||||
Label: label,
|
Label: label,
|
||||||
@@ -27,6 +27,8 @@ func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle {
|
|||||||
uncheckedStateIcon: th.Icon.CheckBoxUnchecked,
|
uncheckedStateIcon: th.Icon.CheckBoxUnchecked,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
c.checkable.Font.Typeface = th.Face
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout updates the checkBox and displays it.
|
// Layout updates the checkBox and displays it.
|
||||||
|
|||||||
@@ -16,8 +16,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type EditorStyle struct {
|
type EditorStyle struct {
|
||||||
Font font.Font
|
Font font.Font
|
||||||
TextSize unit.Sp
|
// LineHeight controls the distance between the baselines of lines of text.
|
||||||
|
// If zero, a sensible default will be used.
|
||||||
|
LineHeight unit.Sp
|
||||||
|
// LineHeightScale applies a scaling factor to the LineHeight. If zero, a
|
||||||
|
// sensible default will be used.
|
||||||
|
LineHeightScale float32
|
||||||
|
TextSize unit.Sp
|
||||||
// Color is the text color.
|
// Color is the text color.
|
||||||
Color color.NRGBA
|
Color color.NRGBA
|
||||||
// Hint contains the text displayed when the editor is empty.
|
// Hint contains the text displayed when the editor is empty.
|
||||||
@@ -33,7 +39,10 @@ type EditorStyle struct {
|
|||||||
|
|
||||||
func Editor(th *Theme, editor *widget.Editor, hint string) EditorStyle {
|
func Editor(th *Theme, editor *widget.Editor, hint string) EditorStyle {
|
||||||
return EditorStyle{
|
return EditorStyle{
|
||||||
Editor: editor,
|
Editor: editor,
|
||||||
|
Font: font.Font{
|
||||||
|
Typeface: th.Face,
|
||||||
|
},
|
||||||
TextSize: th.TextSize,
|
TextSize: th.TextSize,
|
||||||
Color: th.Palette.Fg,
|
Color: th.Palette.Fg,
|
||||||
shaper: th.Shaper,
|
shaper: th.Shaper,
|
||||||
@@ -61,7 +70,12 @@ func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
macro := op.Record(gtx.Ops)
|
macro := op.Record(gtx.Ops)
|
||||||
tl := widget.Label{Alignment: e.Editor.Alignment, MaxLines: maxlines}
|
tl := widget.Label{
|
||||||
|
Alignment: e.Editor.Alignment,
|
||||||
|
MaxLines: maxlines,
|
||||||
|
LineHeight: e.LineHeight,
|
||||||
|
LineHeightScale: e.LineHeightScale,
|
||||||
|
}
|
||||||
dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint, hintColor)
|
dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint, hintColor)
|
||||||
call := macro.Stop()
|
call := macro.Stop()
|
||||||
|
|
||||||
@@ -71,6 +85,8 @@ func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
if h := dims.Size.Y; gtx.Constraints.Min.Y < h {
|
if h := dims.Size.Y; gtx.Constraints.Min.Y < h {
|
||||||
gtx.Constraints.Min.Y = h
|
gtx.Constraints.Min.Y = h
|
||||||
}
|
}
|
||||||
|
e.Editor.LineHeight = e.LineHeight
|
||||||
|
e.Editor.LineHeightScale = e.LineHeightScale
|
||||||
dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize, textColor, selectionColor)
|
dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize, textColor, selectionColor)
|
||||||
if e.Editor.Len() == 0 {
|
if e.Editor.Len() == 0 {
|
||||||
call.Add(gtx.Ops)
|
call.Add(gtx.Ops)
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ type LabelStyle struct {
|
|||||||
Text string
|
Text string
|
||||||
// TextSize determines the size of the text glyphs.
|
// TextSize determines the size of the text glyphs.
|
||||||
TextSize unit.Sp
|
TextSize unit.Sp
|
||||||
|
// LineHeight controls the distance between the baselines of lines of text.
|
||||||
|
// If zero, a sensible default will be used.
|
||||||
|
LineHeight unit.Sp
|
||||||
|
// LineHeightScale applies a scaling factor to the LineHeight. If zero, a
|
||||||
|
// sensible default will be used.
|
||||||
|
LineHeightScale float32
|
||||||
|
|
||||||
// Shaper is the text shaper used to display this labe. This field is automatically
|
// Shaper is the text shaper used to display this labe. This field is automatically
|
||||||
// set using by all constructor functions. If constructing a LabelStyle literal, you
|
// set using by all constructor functions. If constructing a LabelStyle literal, you
|
||||||
@@ -105,13 +111,15 @@ func Overline(th *Theme, txt string) LabelStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Label(th *Theme, size unit.Sp, txt string) LabelStyle {
|
func Label(th *Theme, size unit.Sp, txt string) LabelStyle {
|
||||||
return LabelStyle{
|
l := LabelStyle{
|
||||||
Text: txt,
|
Text: txt,
|
||||||
Color: th.Palette.Fg,
|
Color: th.Palette.Fg,
|
||||||
SelectionColor: f32color.MulAlpha(th.Palette.ContrastBg, 0x60),
|
SelectionColor: f32color.MulAlpha(th.Palette.ContrastBg, 0x60),
|
||||||
TextSize: size,
|
TextSize: size,
|
||||||
Shaper: th.Shaper,
|
Shaper: th.Shaper,
|
||||||
}
|
}
|
||||||
|
l.Font.Typeface = th.Face
|
||||||
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
|
func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -130,13 +138,17 @@ func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
l.State.MaxLines = l.MaxLines
|
l.State.MaxLines = l.MaxLines
|
||||||
l.State.Truncator = l.Truncator
|
l.State.Truncator = l.Truncator
|
||||||
l.State.WrapPolicy = l.WrapPolicy
|
l.State.WrapPolicy = l.WrapPolicy
|
||||||
|
l.State.LineHeight = l.LineHeight
|
||||||
|
l.State.LineHeightScale = l.LineHeightScale
|
||||||
return l.State.Layout(gtx, l.Shaper, l.Font, l.TextSize, textColor, selectColor)
|
return l.State.Layout(gtx, l.Shaper, l.Font, l.TextSize, textColor, selectColor)
|
||||||
}
|
}
|
||||||
tl := widget.Label{
|
tl := widget.Label{
|
||||||
Alignment: l.Alignment,
|
Alignment: l.Alignment,
|
||||||
MaxLines: l.MaxLines,
|
MaxLines: l.MaxLines,
|
||||||
Truncator: l.Truncator,
|
Truncator: l.Truncator,
|
||||||
WrapPolicy: l.WrapPolicy,
|
WrapPolicy: l.WrapPolicy,
|
||||||
|
LineHeight: l.LineHeight,
|
||||||
|
LineHeightScale: l.LineHeightScale,
|
||||||
}
|
}
|
||||||
return tl.Layout(gtx, l.Shaper, l.Font, l.TextSize, l.Text, textColor)
|
return tl.Layout(gtx, l.Shaper, l.Font, l.TextSize, l.Text, textColor)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gioui.org/font/gofont"
|
|
||||||
"gioui.org/io/system"
|
"gioui.org/io/system"
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
@@ -45,7 +44,7 @@ func TestListAnchorStrategies(t *testing.T) {
|
|||||||
var list widget.List
|
var list widget.List
|
||||||
list.Axis = layout.Vertical
|
list.Axis = layout.Vertical
|
||||||
elements := 100
|
elements := 100
|
||||||
th := material.NewTheme(gofont.Collection())
|
th := material.NewTheme()
|
||||||
materialList := material.List(th, &list)
|
materialList := material.List(th, &list)
|
||||||
indicatorWidth := gtx.Dp(materialList.Width())
|
indicatorWidth := gtx.Dp(materialList.Width())
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type RadioButtonStyle struct {
|
|||||||
// RadioButton returns a RadioButton with a label. The key specifies
|
// RadioButton returns a RadioButton with a label. The key specifies
|
||||||
// the value for the Enum.
|
// the value for the Enum.
|
||||||
func RadioButton(th *Theme, group *widget.Enum, key, label string) RadioButtonStyle {
|
func RadioButton(th *Theme, group *widget.Enum, key, label string) RadioButtonStyle {
|
||||||
return RadioButtonStyle{
|
r := RadioButtonStyle{
|
||||||
Group: group,
|
Group: group,
|
||||||
checkable: checkable{
|
checkable: checkable{
|
||||||
Label: label,
|
Label: label,
|
||||||
@@ -32,6 +32,8 @@ func RadioButton(th *Theme, group *widget.Enum, key, label string) RadioButtonSt
|
|||||||
},
|
},
|
||||||
Key: key,
|
Key: key,
|
||||||
}
|
}
|
||||||
|
r.checkable.Font.Typeface = th.Face
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout updates enum and displays the radio button.
|
// Layout updates enum and displays the radio button.
|
||||||
|
|||||||
@@ -42,15 +42,16 @@ type Theme struct {
|
|||||||
RadioChecked *widget.Icon
|
RadioChecked *widget.Icon
|
||||||
RadioUnchecked *widget.Icon
|
RadioUnchecked *widget.Icon
|
||||||
}
|
}
|
||||||
|
// Face selects the default typeface for text.
|
||||||
|
Face font.Typeface
|
||||||
|
|
||||||
// FingerSize is the minimum touch target size.
|
// FingerSize is the minimum touch target size.
|
||||||
FingerSize unit.Dp
|
FingerSize unit.Dp
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTheme(fontCollection []font.FontFace) *Theme {
|
// NewTheme constructs a theme (and underlying text shaper).
|
||||||
t := &Theme{
|
func NewTheme() *Theme {
|
||||||
Shaper: text.NewShaper(fontCollection),
|
t := &Theme{Shaper: &text.Shaper{}}
|
||||||
}
|
|
||||||
t.Palette = Palette{
|
t.Palette = Palette{
|
||||||
Fg: rgb(0x000000),
|
Fg: rgb(0x000000),
|
||||||
Bg: rgb(0xffffff),
|
Bg: rgb(0xffffff),
|
||||||
|
|||||||
+11
-3
@@ -59,9 +59,15 @@ type Selectable struct {
|
|||||||
// if text was cut off. Defaults to "…" if left empty.
|
// if text was cut off. Defaults to "…" if left empty.
|
||||||
Truncator string
|
Truncator string
|
||||||
// WrapPolicy configures how displayed text will be broken into lines.
|
// WrapPolicy configures how displayed text will be broken into lines.
|
||||||
WrapPolicy text.WrapPolicy
|
WrapPolicy text.WrapPolicy
|
||||||
initialized bool
|
// LineHeight controls the distance between the baselines of lines of text.
|
||||||
source stringSource
|
// If zero, a sensible default will be used.
|
||||||
|
LineHeight unit.Sp
|
||||||
|
// LineHeightScale applies a scaling factor to the LineHeight. If zero, a
|
||||||
|
// sensible default will be used.
|
||||||
|
LineHeightScale float32
|
||||||
|
initialized bool
|
||||||
|
source stringSource
|
||||||
// scratch is a buffer reused to efficiently read text out of the
|
// scratch is a buffer reused to efficiently read text out of the
|
||||||
// textView.
|
// textView.
|
||||||
scratch []byte
|
scratch []byte
|
||||||
@@ -181,6 +187,8 @@ func (l *Selectable) Truncated() bool {
|
|||||||
// paint material for the text and selection rectangles, respectively.
|
// paint material for the text and selection rectangles, respectively.
|
||||||
func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectionMaterial op.CallOp) layout.Dimensions {
|
func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectionMaterial op.CallOp) layout.Dimensions {
|
||||||
l.initialize()
|
l.initialize()
|
||||||
|
l.text.LineHeight = l.LineHeight
|
||||||
|
l.text.LineHeightScale = l.LineHeightScale
|
||||||
l.text.Alignment = l.Alignment
|
l.text.Alignment = l.Alignment
|
||||||
l.text.MaxLines = l.MaxLines
|
l.text.MaxLines = l.MaxLines
|
||||||
l.text.Truncator = l.Truncator
|
l.text.Truncator = l.Truncator
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func TestSelectableMove(t *testing.T) {
|
|||||||
Ops: new(op.Ops),
|
Ops: new(op.Ops),
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fnt := font.Font{}
|
fnt := font.Font{}
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ func TestSelectableConfigurations(t *testing.T) {
|
|||||||
Constraints: layout.Exact(image.Pt(300, 300)),
|
Constraints: layout.Exact(image.Pt(300, 300)),
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(gofont.Collection())
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
sentence := "\n\n\n\n\n\n\n\n\n\n\n\nthe quick brown fox jumps over the lazy dog"
|
sentence := "\n\n\n\n\n\n\n\n\n\n\n\nthe quick brown fox jumps over the lazy dog"
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ type textSource interface {
|
|||||||
// be scrolled, and for configuring and drawing text selection boxes.
|
// be scrolled, and for configuring and drawing text selection boxes.
|
||||||
type textView struct {
|
type textView struct {
|
||||||
Alignment text.Alignment
|
Alignment text.Alignment
|
||||||
|
// LineHeight controls the distance between the baselines of lines of text.
|
||||||
|
// If zero, a sensible default will be used.
|
||||||
|
LineHeight unit.Sp
|
||||||
|
// LineHeightScale applies a scaling factor to the LineHeight. If zero, a
|
||||||
|
// sensible default will be used.
|
||||||
|
LineHeightScale float32
|
||||||
// SingleLine forces the text to stay on a single line.
|
// SingleLine forces the text to stay on a single line.
|
||||||
// SingleLine also sets the scrolling direction to
|
// SingleLine also sets the scrolling direction to
|
||||||
// horizontal.
|
// horizontal.
|
||||||
@@ -273,6 +279,14 @@ func (e *textView) Update(gtx layout.Context, lt *text.Shaper, font font.Font, s
|
|||||||
e.params.WrapPolicy = e.WrapPolicy
|
e.params.WrapPolicy = e.WrapPolicy
|
||||||
e.invalidate()
|
e.invalidate()
|
||||||
}
|
}
|
||||||
|
if lh := fixed.I(gtx.Sp(e.LineHeight)); lh != e.params.LineHeight {
|
||||||
|
e.params.LineHeight = lh
|
||||||
|
e.invalidate()
|
||||||
|
}
|
||||||
|
if e.LineHeightScale != e.params.LineHeightScale {
|
||||||
|
e.params.LineHeightScale = e.LineHeightScale
|
||||||
|
e.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
e.makeValid()
|
e.makeValid()
|
||||||
if eventHandling != nil {
|
if eventHandling != nil {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func BenchmarkLabelStatic(b *testing.B) {
|
|||||||
},
|
},
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(benchFonts)
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
|
||||||
if render {
|
if render {
|
||||||
win, _ = headless.NewWindow(size.X, size.Y)
|
win, _ = headless.NewWindow(size.X, size.Y)
|
||||||
defer win.Release()
|
defer win.Release()
|
||||||
@@ -118,7 +118,7 @@ func BenchmarkLabelDynamic(b *testing.B) {
|
|||||||
},
|
},
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(benchFonts)
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
|
||||||
if render {
|
if render {
|
||||||
win, _ = headless.NewWindow(size.X, size.Y)
|
win, _ = headless.NewWindow(size.X, size.Y)
|
||||||
defer win.Release()
|
defer win.Release()
|
||||||
@@ -153,7 +153,7 @@ func BenchmarkEditorStatic(b *testing.B) {
|
|||||||
},
|
},
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(benchFonts)
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
|
||||||
if render {
|
if render {
|
||||||
win, _ = headless.NewWindow(size.X, size.Y)
|
win, _ = headless.NewWindow(size.X, size.Y)
|
||||||
defer win.Release()
|
defer win.Release()
|
||||||
@@ -186,7 +186,7 @@ func BenchmarkEditorDynamic(b *testing.B) {
|
|||||||
},
|
},
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(benchFonts)
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
|
||||||
if render {
|
if render {
|
||||||
win, _ = headless.NewWindow(size.X, size.Y)
|
win, _ = headless.NewWindow(size.X, size.Y)
|
||||||
defer win.Release()
|
defer win.Release()
|
||||||
@@ -224,7 +224,7 @@ func FuzzEditorEditing(f *testing.F) {
|
|||||||
},
|
},
|
||||||
Locale: arabic,
|
Locale: arabic,
|
||||||
}
|
}
|
||||||
cache := text.NewShaper(benchFonts)
|
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
|
||||||
fontSize := unit.Sp(10)
|
fontSize := unit.Sp(10)
|
||||||
font := font.Font{}
|
font := font.Font{}
|
||||||
e := Editor{}
|
e := Editor{}
|
||||||
|
|||||||
Reference in New Issue
Block a user