mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-03 08:25:34 +00:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 718be79d9e | |||
| 0073e1a167 | |||
| 32ecec5538 | |||
| 6eb33b8a56 | |||
| 4617526e12 | |||
| dbc7a900bd | |||
| fb3ae95b28 | |||
| c458eb30f0 | |||
| d96c954769 | |||
| f39245df99 | |||
| 8097df9930 | |||
| fc6e51deba | |||
| 5fa94ff67b | |||
| 23b6f06e3e | |||
| c8801fe233 | |||
| 3fde0c0061 | |||
| 9d89f7c8b1 | |||
| 48bd5952b1 | |||
| df8a8789a3 | |||
| 62edabe137 | |||
| 49296bd0ca | |||
| d078bf0ed7 | |||
| ea58aacde2 | |||
| ae2b1f42b2 | |||
| 63fea3d2bd | |||
| ce8475a0b9 | |||
| 37717d0df9 | |||
| 7550d85447 | |||
| c756986d9e | |||
| dc170033cd | |||
| d42dae73f0 | |||
| 23e44292bb | |||
| fe85136f99 | |||
| b9837def5c | |||
| dc97871122 | |||
| 4a4fe5a69b | |||
| 1686874d07 | |||
| 650ccea28d | |||
| e1b3928819 | |||
| b66dcc436c | |||
| 526db27c75 | |||
| 27193ae8e8 | |||
| 313c488ec3 | |||
| f30e936d9a | |||
| ae3bd2a1e1 | |||
| ae43d18ced | |||
| b4d93379c4 | |||
| b9654eb4eb | |||
| 89d20c7d99 | |||
| 14bab8efae | |||
| f437aaf359 | |||
| cf5ae4aad9 | |||
| 8679f49fff | |||
| 83202263b9 | |||
| 7fde80e805 | |||
| e9d0619641 | |||
| 2e524200ab | |||
| cc477e9ca6 | |||
| 290b5fe821 | |||
| e9cb0b326d | |||
| 0e77a2b521 | |||
| 63550cc81e | |||
| 03c21dc1b5 | |||
| 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 |
+17
-7
@@ -8,23 +8,28 @@ packages:
|
|||||||
- libxml2-dev
|
- libxml2-dev
|
||||||
- libssl-dev
|
- libssl-dev
|
||||||
- libz-dev
|
- libz-dev
|
||||||
- llvm-dev # for cctools
|
- llvm-dev # cctools
|
||||||
- uuid-dev ## for cctools
|
- uuid-dev # cctools
|
||||||
|
- ninja-build # cctools
|
||||||
|
- systemtap-sdt-dev # cctools
|
||||||
|
- libbsd-dev # cctools
|
||||||
|
- linux-libc-dev # cctools
|
||||||
- libplist-utils # for gogio
|
- libplist-utils # for gogio
|
||||||
sources:
|
sources:
|
||||||
- https://git.sr.ht/~eliasnaur/applesdks
|
- https://git.sr.ht/~eliasnaur/applesdks
|
||||||
- https://git.sr.ht/~eliasnaur/gio
|
- https://git.sr.ht/~eliasnaur/gio
|
||||||
- https://git.sr.ht/~eliasnaur/giouiorg
|
- https://git.sr.ht/~eliasnaur/giouiorg
|
||||||
- https://github.com/tpoechtrager/cctools-port.git
|
- https://github.com/tpoechtrager/cctools-port
|
||||||
- https://github.com/tpoechtrager/apple-libtapi.git
|
- https://github.com/tpoechtrager/apple-libtapi
|
||||||
- https://github.com/mackyle/xar.git
|
- https://github.com/tpoechtrager/apple-libdispatch
|
||||||
|
- https://github.com/mackyle/xar
|
||||||
environment:
|
environment:
|
||||||
APPLE_TOOLCHAIN_ROOT: /home/build/appletools
|
APPLE_TOOLCHAIN_ROOT: /home/build/appletools
|
||||||
PATH: /home/build/sdk/go/bin:/home/build/go/bin:/usr/bin
|
PATH: /home/build/sdk/go/bin:/home/build/go/bin:/usr/bin
|
||||||
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
|
||||||
@@ -42,6 +47,11 @@ tasks:
|
|||||||
- install_appletoolchain: |
|
- install_appletoolchain: |
|
||||||
cd giouiorg
|
cd giouiorg
|
||||||
go build -o $APPLE_TOOLCHAIN_ROOT/tools ./cmd/appletoolchain
|
go build -o $APPLE_TOOLCHAIN_ROOT/tools ./cmd/appletoolchain
|
||||||
|
- build_libdispatch: |
|
||||||
|
cd apple-libdispatch
|
||||||
|
cmake -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_INSTALL_PREFIX=$APPLE_TOOLCHAIN_ROOT/libdispatch .
|
||||||
|
ninja
|
||||||
|
ninja install
|
||||||
- build_xar: |
|
- build_xar: |
|
||||||
cd xar/xar
|
cd xar/xar
|
||||||
ac_cv_lib_crypto_OpenSSL_add_all_ciphers=yes CC=clang ./autogen.sh --prefix=/usr
|
ac_cv_lib_crypto_OpenSSL_add_all_ciphers=yes CC=clang ./autogen.sh --prefix=/usr
|
||||||
@@ -53,7 +63,7 @@ tasks:
|
|||||||
./install.sh
|
./install.sh
|
||||||
- build_cctools: |
|
- build_cctools: |
|
||||||
cd cctools-port/cctools
|
cd cctools-port/cctools
|
||||||
./configure --prefix $APPLE_TOOLCHAIN_ROOT/toolchain --with-libtapi=$APPLE_TOOLCHAIN_ROOT/libtapi --target=x86_64-apple-darwin19
|
./configure --target=x86_64-apple-darwin19 --prefix $APPLE_TOOLCHAIN_ROOT/toolchain --with-libtapi=$APPLE_TOOLCHAIN_ROOT/libtapi --with-libdispatch=$APPLE_TOOLCHAIN_ROOT/libdispatch --with-libblocksruntime=$APPLE_TOOLCHAIN_ROOT/libdispatch
|
||||||
make install
|
make install
|
||||||
- test_macos: |
|
- test_macos: |
|
||||||
cd gio
|
cd gio
|
||||||
|
|||||||
+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
-6
@@ -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 .)"
|
||||||
@@ -67,11 +67,6 @@ tasks:
|
|||||||
CGO_ENABLED=1 GOARCH=386 go test ./...
|
CGO_ENABLED=1 GOARCH=386 go test ./...
|
||||||
GOOS=windows go test -exec=wine ./...
|
GOOS=windows go test -exec=wine ./...
|
||||||
GOOS=js GOARCH=wasm go build -o /dev/null ./...
|
GOOS=js GOARCH=wasm go build -o /dev/null ./...
|
||||||
- install_chrome: |
|
|
||||||
curl -s https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
|
||||||
sudo sh -c 'echo "deb [arch=amd64] https://dl-ssl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
|
|
||||||
sudo apt-get -qq update
|
|
||||||
sudo apt-get -qq install -y google-chrome-stable
|
|
||||||
- install_jdk8: |
|
- install_jdk8: |
|
||||||
curl -so jdk.deb "https://cdn.azul.com/zulu/bin/zulu8.42.0.21-ca-jdk8.0.232-linux_amd64.deb"
|
curl -so jdk.deb "https://cdn.azul.com/zulu/bin/zulu8.42.0.21-ca-jdk8.0.232-linux_amd64.deb"
|
||||||
sudo apt-get -qq install -y -f ./jdk.deb
|
sudo apt-get -qq install -y -f ./jdk.deb
|
||||||
|
|||||||
+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(
|
||||||
@@ -60,10 +60,10 @@ func (c *d3d11Context) RenderTarget() (gpu.RenderTarget, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *d3d11Context) Present() error {
|
func (c *d3d11Context) Present() error {
|
||||||
err := c.swchain.Present(1, 0)
|
return wrapErr(c.swchain.Present(1, 0))
|
||||||
if err == nil {
|
}
|
||||||
return nil
|
|
||||||
}
|
func wrapErr(err error) error {
|
||||||
if err, ok := err.(d3d11.ErrorCode); ok {
|
if err, ok := err.(d3d11.ErrorCode); ok {
|
||||||
switch err.Code {
|
switch err.Code {
|
||||||
case d3d11.DXGI_STATUS_OCCLUDED:
|
case d3d11.DXGI_STATUS_OCCLUDED:
|
||||||
@@ -84,7 +84,7 @@ func (c *d3d11Context) Refresh() error {
|
|||||||
}
|
}
|
||||||
c.releaseFBO()
|
c.releaseFBO()
|
||||||
if err := c.swchain.ResizeBuffers(0, 0, 0, d3d11.DXGI_FORMAT_UNKNOWN, 0); err != nil {
|
if err := c.swchain.ResizeBuffers(0, 0, 0, d3d11.DXGI_FORMAT_UNKNOWN, 0); err != nil {
|
||||||
return err
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
c.width = width
|
c.width = width
|
||||||
c.height = height
|
c.height = height
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-5
@@ -12,16 +12,16 @@ Create a new Window by calling NewWindow. On mobile platforms or when Gio
|
|||||||
is embedded in another project, NewWindow merely connects with a previously
|
is embedded in another project, NewWindow merely connects with a previously
|
||||||
created window.
|
created window.
|
||||||
|
|
||||||
A Window is run by receiving events from its Events channel. The most
|
A Window is run by calling NextEvent in a loop. The most important event is
|
||||||
important event is FrameEvent that prompts an update of the window
|
FrameEvent that prompts an update of the window contents.
|
||||||
contents and state.
|
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
import "gioui.org/unit"
|
import "gioui.org/unit"
|
||||||
|
|
||||||
w := app.NewWindow()
|
w := app.NewWindow()
|
||||||
for e := range w.Events() {
|
for {
|
||||||
|
e := w.NextEvent()
|
||||||
if e, ok := e.(system.FrameEvent); ok {
|
if e, ok := e.(system.FrameEvent); ok {
|
||||||
ops.Reset()
|
ops.Reset()
|
||||||
// Add operations to ops.
|
// Add operations to ops.
|
||||||
@@ -50,7 +50,8 @@ For example, to display a blank but otherwise functional window:
|
|||||||
func main() {
|
func main() {
|
||||||
go func() {
|
go func() {
|
||||||
w := app.NewWindow()
|
w := app.NewWindow()
|
||||||
for range w.Events() {
|
for {
|
||||||
|
w.NextEvent()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
app.Main()
|
app.Main()
|
||||||
|
|||||||
+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()
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ type WndClassEx struct {
|
|||||||
HIconSm syscall.Handle
|
HIconSm syscall.Handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Margins struct {
|
||||||
|
CxLeftWidth int32
|
||||||
|
CxRightWidth int32
|
||||||
|
CyTopHeight int32
|
||||||
|
CyBottomHeight int32
|
||||||
|
}
|
||||||
|
|
||||||
type Msg struct {
|
type Msg struct {
|
||||||
Hwnd syscall.Handle
|
Hwnd syscall.Handle
|
||||||
Message uint32
|
Message uint32
|
||||||
@@ -69,6 +76,21 @@ type MinMaxInfo struct {
|
|||||||
PtMaxTrackSize Point
|
PtMaxTrackSize Point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NCCalcSizeParams struct {
|
||||||
|
Rgrc [3]Rect
|
||||||
|
LpPos *WindowPos
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowPos struct {
|
||||||
|
HWND syscall.Handle
|
||||||
|
HWNDInsertAfter syscall.Handle
|
||||||
|
x int32
|
||||||
|
y int32
|
||||||
|
cx int32
|
||||||
|
cy int32
|
||||||
|
flags uint32
|
||||||
|
}
|
||||||
|
|
||||||
type WindowPlacement struct {
|
type WindowPlacement struct {
|
||||||
length uint32
|
length uint32
|
||||||
flags uint32
|
flags uint32
|
||||||
@@ -245,6 +267,7 @@ const (
|
|||||||
WM_MOUSEHWHEEL = 0x020E
|
WM_MOUSEHWHEEL = 0x020E
|
||||||
WM_NCACTIVATE = 0x0086
|
WM_NCACTIVATE = 0x0086
|
||||||
WM_NCHITTEST = 0x0084
|
WM_NCHITTEST = 0x0084
|
||||||
|
WM_NCCALCSIZE = 0x0083
|
||||||
WM_PAINT = 0x000F
|
WM_PAINT = 0x000F
|
||||||
WM_QUIT = 0x0012
|
WM_QUIT = 0x0012
|
||||||
WM_SETCURSOR = 0x0020
|
WM_SETCURSOR = 0x0020
|
||||||
@@ -323,6 +346,7 @@ var (
|
|||||||
_DispatchMessage = user32.NewProc("DispatchMessageW")
|
_DispatchMessage = user32.NewProc("DispatchMessageW")
|
||||||
_EmptyClipboard = user32.NewProc("EmptyClipboard")
|
_EmptyClipboard = user32.NewProc("EmptyClipboard")
|
||||||
_GetWindowRect = user32.NewProc("GetWindowRect")
|
_GetWindowRect = user32.NewProc("GetWindowRect")
|
||||||
|
_GetClientRect = user32.NewProc("GetClientRect")
|
||||||
_GetClipboardData = user32.NewProc("GetClipboardData")
|
_GetClipboardData = user32.NewProc("GetClipboardData")
|
||||||
_GetDC = user32.NewProc("GetDC")
|
_GetDC = user32.NewProc("GetDC")
|
||||||
_GetDpiForWindow = user32.NewProc("GetDpiForWindow")
|
_GetDpiForWindow = user32.NewProc("GetDpiForWindow")
|
||||||
@@ -379,6 +403,9 @@ var (
|
|||||||
_ImmReleaseContext = imm32.NewProc("ImmReleaseContext")
|
_ImmReleaseContext = imm32.NewProc("ImmReleaseContext")
|
||||||
_ImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow")
|
_ImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow")
|
||||||
_ImmSetCompositionWindow = imm32.NewProc("ImmSetCompositionWindow")
|
_ImmSetCompositionWindow = imm32.NewProc("ImmSetCompositionWindow")
|
||||||
|
|
||||||
|
dwmapi = syscall.NewLazySystemDLL("dwmapi")
|
||||||
|
_DwmExtendFrameIntoClientArea = dwmapi.NewProc("DwmExtendFrameIntoClientArea")
|
||||||
)
|
)
|
||||||
|
|
||||||
func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) {
|
func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) {
|
||||||
@@ -430,6 +457,14 @@ func DispatchMessage(m *Msg) {
|
|||||||
_DispatchMessage.Call(uintptr(unsafe.Pointer(m)))
|
_DispatchMessage.Call(uintptr(unsafe.Pointer(m)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DwmExtendFrameIntoClientArea(hwnd syscall.Handle, margins Margins) error {
|
||||||
|
r, _, _ := _DwmExtendFrameIntoClientArea.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&margins)))
|
||||||
|
if r != 0 {
|
||||||
|
return fmt.Errorf("DwmExtendFrameIntoClientArea: %#x", r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func EmptyClipboard() error {
|
func EmptyClipboard() error {
|
||||||
r, _, err := _EmptyClipboard.Call()
|
r, _, err := _EmptyClipboard.Call()
|
||||||
if r == 0 {
|
if r == 0 {
|
||||||
@@ -444,6 +479,12 @@ func GetWindowRect(hwnd syscall.Handle) Rect {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetClientRect(hwnd syscall.Handle) Rect {
|
||||||
|
var r Rect
|
||||||
|
_GetClientRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&r)))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func GetClipboardData(format uint32) (syscall.Handle, error) {
|
func GetClipboardData(format uint32) (syscall.Handle, error) {
|
||||||
r, _, err := _GetClipboardData.Call(uintptr(format))
|
r, _, err := _GetClipboardData.Call(uintptr(format))
|
||||||
if r == 0 {
|
if r == 0 {
|
||||||
|
|||||||
@@ -242,6 +242,9 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
|
|||||||
if 'a' <= s && s <= 'z' {
|
if 'a' <= s && s <= 'z' {
|
||||||
return string(rune(s - 'a' + 'A')), true
|
return string(rune(s - 'a' + 'A')), true
|
||||||
}
|
}
|
||||||
|
if C.XKB_KEY_KP_0 <= s && s <= C.XKB_KEY_KP_9 {
|
||||||
|
return string(rune(s - C.XKB_KEY_KP_0 + '0')), true
|
||||||
|
}
|
||||||
if ' ' < s && s <= '~' {
|
if ' ' < s && s <= '~' {
|
||||||
return string(rune(s)), true
|
return string(rune(s)), true
|
||||||
}
|
}
|
||||||
@@ -255,8 +258,6 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
|
|||||||
n = key.NameRightArrow
|
n = key.NameRightArrow
|
||||||
case C.XKB_KEY_Return:
|
case C.XKB_KEY_Return:
|
||||||
n = key.NameReturn
|
n = key.NameReturn
|
||||||
case C.XKB_KEY_KP_Enter:
|
|
||||||
n = key.NameEnter
|
|
||||||
case C.XKB_KEY_Up:
|
case C.XKB_KEY_Up:
|
||||||
n = key.NameUpArrow
|
n = key.NameUpArrow
|
||||||
case C.XKB_KEY_Down:
|
case C.XKB_KEY_Down:
|
||||||
@@ -297,9 +298,9 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
|
|||||||
n = key.NameF11
|
n = key.NameF11
|
||||||
case C.XKB_KEY_F12:
|
case C.XKB_KEY_F12:
|
||||||
n = key.NameF12
|
n = key.NameF12
|
||||||
case C.XKB_KEY_Tab, C.XKB_KEY_KP_Tab, C.XKB_KEY_ISO_Left_Tab:
|
case C.XKB_KEY_Tab, C.XKB_KEY_ISO_Left_Tab:
|
||||||
n = key.NameTab
|
n = key.NameTab
|
||||||
case 0x20, C.XKB_KEY_KP_Space:
|
case 0x20:
|
||||||
n = key.NameSpace
|
n = key.NameSpace
|
||||||
case C.XKB_KEY_Control_L, C.XKB_KEY_Control_R:
|
case C.XKB_KEY_Control_L, C.XKB_KEY_Control_R:
|
||||||
n = key.NameCtrl
|
n = key.NameCtrl
|
||||||
@@ -309,6 +310,64 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
|
|||||||
n = key.NameAlt
|
n = key.NameAlt
|
||||||
case C.XKB_KEY_Super_L, C.XKB_KEY_Super_R:
|
case C.XKB_KEY_Super_L, C.XKB_KEY_Super_R:
|
||||||
n = key.NameSuper
|
n = key.NameSuper
|
||||||
|
|
||||||
|
case C.XKB_KEY_KP_Space:
|
||||||
|
n = key.NameSpace
|
||||||
|
case C.XKB_KEY_KP_Tab:
|
||||||
|
n = key.NameTab
|
||||||
|
case C.XKB_KEY_KP_Enter:
|
||||||
|
n = key.NameEnter
|
||||||
|
case C.XKB_KEY_KP_F1:
|
||||||
|
n = key.NameF1
|
||||||
|
case C.XKB_KEY_KP_F2:
|
||||||
|
n = key.NameF2
|
||||||
|
case C.XKB_KEY_KP_F3:
|
||||||
|
n = key.NameF3
|
||||||
|
case C.XKB_KEY_KP_F4:
|
||||||
|
n = key.NameF4
|
||||||
|
case C.XKB_KEY_KP_Home:
|
||||||
|
n = key.NameHome
|
||||||
|
case C.XKB_KEY_KP_Left:
|
||||||
|
n = key.NameLeftArrow
|
||||||
|
case C.XKB_KEY_KP_Up:
|
||||||
|
n = key.NameUpArrow
|
||||||
|
case C.XKB_KEY_KP_Right:
|
||||||
|
n = key.NameRightArrow
|
||||||
|
case C.XKB_KEY_KP_Down:
|
||||||
|
n = key.NameDownArrow
|
||||||
|
case C.XKB_KEY_KP_Prior:
|
||||||
|
// not supported
|
||||||
|
return "", false
|
||||||
|
case C.XKB_KEY_KP_Next:
|
||||||
|
// not supported
|
||||||
|
return "", false
|
||||||
|
case C.XKB_KEY_KP_End:
|
||||||
|
n = key.NameEnd
|
||||||
|
case C.XKB_KEY_KP_Begin:
|
||||||
|
n = key.NameHome
|
||||||
|
case C.XKB_KEY_KP_Insert:
|
||||||
|
// not supported
|
||||||
|
return "", false
|
||||||
|
case C.XKB_KEY_KP_Delete:
|
||||||
|
n = key.NameDeleteForward
|
||||||
|
case C.XKB_KEY_KP_Multiply:
|
||||||
|
n = "*"
|
||||||
|
case C.XKB_KEY_KP_Add:
|
||||||
|
n = "+"
|
||||||
|
case C.XKB_KEY_KP_Separator:
|
||||||
|
// not supported
|
||||||
|
return "", false
|
||||||
|
case C.XKB_KEY_KP_Subtract:
|
||||||
|
n = "-"
|
||||||
|
case C.XKB_KEY_KP_Decimal:
|
||||||
|
// TODO(dh): does a German keyboard layout also translate the numpad key to XKB_KEY_KP_DECIMAL? Because in
|
||||||
|
// German, the decimal is a comma, not a period.
|
||||||
|
n = "."
|
||||||
|
case C.XKB_KEY_KP_Divide:
|
||||||
|
n = "/"
|
||||||
|
case C.XKB_KEY_KP_Equal:
|
||||||
|
n = "="
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-21
@@ -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)
|
||||||
|
|
||||||
@@ -951,18 +953,18 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
|
|||||||
//export Java_org_gioui_GioView_onTouchEvent
|
//export Java_org_gioui_GioView_onTouchEvent
|
||||||
func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, action, pointerID, tool C.jint, x, y, scrollX, scrollY C.jfloat, jbtns C.jint, t C.jlong) {
|
func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, action, pointerID, tool C.jint, x, y, scrollX, scrollY C.jfloat, jbtns C.jint, t C.jlong) {
|
||||||
w := cgo.Handle(handle).Value().(*window)
|
w := cgo.Handle(handle).Value().(*window)
|
||||||
var typ pointer.Type
|
var kind pointer.Kind
|
||||||
switch action {
|
switch action {
|
||||||
case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN:
|
case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN:
|
||||||
typ = pointer.Press
|
kind = pointer.Press
|
||||||
case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP:
|
case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP:
|
||||||
typ = pointer.Release
|
kind = pointer.Release
|
||||||
case C.AMOTION_EVENT_ACTION_CANCEL:
|
case C.AMOTION_EVENT_ACTION_CANCEL:
|
||||||
typ = pointer.Cancel
|
kind = pointer.Cancel
|
||||||
case C.AMOTION_EVENT_ACTION_MOVE:
|
case C.AMOTION_EVENT_ACTION_MOVE:
|
||||||
typ = pointer.Move
|
kind = pointer.Move
|
||||||
case C.AMOTION_EVENT_ACTION_SCROLL:
|
case C.AMOTION_EVENT_ACTION_SCROLL:
|
||||||
typ = pointer.Scroll
|
kind = pointer.Scroll
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -992,7 +994,7 @@ func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.callbacks.Event(pointer.Event{
|
w.callbacks.Event(pointer.Event{
|
||||||
Type: typ,
|
Kind: kind,
|
||||||
Source: src,
|
Source: src,
|
||||||
Buttons: btns,
|
Buttons: btns,
|
||||||
PointerID: pointer.ID(pointerID),
|
PointerID: pointer.ID(pointerID),
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-12
@@ -6,7 +6,7 @@ package app
|
|||||||
#include <Foundation/Foundation.h>
|
#include <Foundation/Foundation.h>
|
||||||
|
|
||||||
__attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void);
|
__attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void);
|
||||||
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(uintptr_t handle);
|
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(void);
|
||||||
__attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl);
|
__attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl);
|
||||||
__attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl);
|
__attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl);
|
||||||
__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl);
|
__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl);
|
||||||
@@ -42,7 +42,7 @@ static CFTypeRef newNSString(unichar *chars, NSUInteger length) {
|
|||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"runtime/cgo"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf16"
|
"unicode/utf16"
|
||||||
@@ -70,6 +70,9 @@ type displayLink struct {
|
|||||||
running uint32
|
running uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// displayLinks maps CFTypeRefs to *displayLinks.
|
||||||
|
var displayLinks sync.Map
|
||||||
|
|
||||||
var mainFuncs = make(chan func(), 1)
|
var mainFuncs = make(chan func(), 1)
|
||||||
|
|
||||||
// runOnMain runs the function on the main thread.
|
// runOnMain runs the function on the main thread.
|
||||||
@@ -121,25 +124,25 @@ func stringToNSString(str string) C.CFTypeRef {
|
|||||||
return C.newNSString(chars, C.NSUInteger(len(u16)))
|
return C.newNSString(chars, C.NSUInteger(len(u16)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDisplayLink(callback func()) (*displayLink, error) {
|
func newDisplayLink(callback func()) (*displayLink, error) {
|
||||||
d := &displayLink{
|
d := &displayLink{
|
||||||
callback: callback,
|
callback: callback,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
states: make(chan bool),
|
states: make(chan bool),
|
||||||
dids: make(chan uint64),
|
dids: make(chan uint64),
|
||||||
}
|
}
|
||||||
h := cgo.NewHandle(d)
|
dl := C.gio_createDisplayLink()
|
||||||
dl := C.gio_createDisplayLink(C.uintptr_t(h))
|
|
||||||
if dl == 0 {
|
if dl == 0 {
|
||||||
return nil, errors.New("app: failed to create display link")
|
return nil, errors.New("app: failed to create display link")
|
||||||
}
|
}
|
||||||
go d.run(dl, h)
|
go d.run(dl)
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *displayLink) run(dl C.CFTypeRef, h cgo.Handle) {
|
func (d *displayLink) run(dl C.CFTypeRef) {
|
||||||
defer C.gio_releaseDisplayLink(dl)
|
defer C.gio_releaseDisplayLink(dl)
|
||||||
defer h.Delete()
|
displayLinks.Store(dl, d)
|
||||||
|
defer displayLinks.Delete(dl)
|
||||||
var stopTimer *time.Timer
|
var stopTimer *time.Timer
|
||||||
var tchan <-chan time.Time
|
var tchan <-chan time.Time
|
||||||
started := false
|
started := false
|
||||||
@@ -200,10 +203,14 @@ func (d *displayLink) SetDisplayID(did uint64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onFrameCallback
|
//export gio_onFrameCallback
|
||||||
func gio_onFrameCallback(dl C.CFTypeRef, handle C.uintptr_t) {
|
func gio_onFrameCallback(ref C.CFTypeRef) {
|
||||||
d := cgo.Handle(handle).Value().(*displayLink)
|
d, exists := displayLinks.Load(ref)
|
||||||
if atomic.LoadUint32(&d.running) != 0 {
|
if !exists {
|
||||||
d.callback()
|
return
|
||||||
|
}
|
||||||
|
dl := d.(*displayLink)
|
||||||
|
if atomic.LoadUint32(&dl.running) != 0 {
|
||||||
|
dl.callback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -117,7 +117,7 @@ func onCreate(view, controller C.CFTypeRef) {
|
|||||||
w := &window{
|
w := &window{
|
||||||
view: view,
|
view: view,
|
||||||
}
|
}
|
||||||
dl, err := NewDisplayLink(func() {
|
dl, err := newDisplayLink(func() {
|
||||||
w.draw(false)
|
w.draw(false)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -236,16 +236,16 @@ func onText(view, str C.CFTypeRef) {
|
|||||||
|
|
||||||
//export onTouch
|
//export onTouch
|
||||||
func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) {
|
func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) {
|
||||||
var typ pointer.Type
|
var kind pointer.Kind
|
||||||
switch phase {
|
switch phase {
|
||||||
case C.UITouchPhaseBegan:
|
case C.UITouchPhaseBegan:
|
||||||
typ = pointer.Press
|
kind = pointer.Press
|
||||||
case C.UITouchPhaseMoved:
|
case C.UITouchPhaseMoved:
|
||||||
typ = pointer.Move
|
kind = pointer.Move
|
||||||
case C.UITouchPhaseEnded:
|
case C.UITouchPhaseEnded:
|
||||||
typ = pointer.Release
|
kind = pointer.Release
|
||||||
case C.UITouchPhaseCancelled:
|
case C.UITouchPhaseCancelled:
|
||||||
typ = pointer.Cancel
|
kind = pointer.Cancel
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -253,7 +253,7 @@ func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.C
|
|||||||
t := time.Duration(float64(ti) * float64(time.Second))
|
t := time.Duration(float64(ti) * float64(time.Second))
|
||||||
p := f32.Point{X: float32(x), Y: float32(y)}
|
p := f32.Point{X: float32(x), Y: float32(y)}
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: typ,
|
Kind: kind,
|
||||||
Source: pointer.Touch,
|
Source: pointer.Touch,
|
||||||
PointerID: w.lookupTouch(last != 0, touchRef),
|
PointerID: w.lookupTouch(last != 0, touchRef),
|
||||||
Position: p,
|
Position: p,
|
||||||
|
|||||||
+5
-17
@@ -123,6 +123,9 @@ static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIE
|
|||||||
|
|
||||||
@implementation GioView
|
@implementation GioView
|
||||||
NSArray<UIKeyCommand *> *_keyCommands;
|
NSArray<UIKeyCommand *> *_keyCommands;
|
||||||
|
+ (void)onFrameCallback:(CADisplayLink *)link {
|
||||||
|
gio_onFrameCallback((__bridge CFTypeRef)link);
|
||||||
|
}
|
||||||
+ (Class)layerClass {
|
+ (Class)layerClass {
|
||||||
return gio_layerClass();
|
return gio_layerClass();
|
||||||
}
|
}
|
||||||
@@ -227,23 +230,8 @@ NSArray<UIKeyCommand *> *_keyCommands;
|
|||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface DisplayLinkHandle : NSObject {
|
CFTypeRef gio_createDisplayLink(void) {
|
||||||
}
|
CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:[GioView class] selector:@selector(onFrameCallback:)];
|
||||||
@property uintptr_t handle;
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation DisplayLinkHandle {
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)onFrameCallback:(CADisplayLink *)link {
|
|
||||||
gio_onFrameCallback((__bridge CFTypeRef)link, _handle);
|
|
||||||
}
|
|
||||||
@end
|
|
||||||
|
|
||||||
CFTypeRef gio_createDisplayLink(uintptr_t handle) {
|
|
||||||
DisplayLinkHandle *h = [DisplayLinkHandle alloc];
|
|
||||||
h.handle = handle;
|
|
||||||
CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:h selector:@selector(onFrameCallback:)];
|
|
||||||
dl.paused = YES;
|
dl.paused = YES;
|
||||||
NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
|
NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
|
||||||
[dl addToRunLoop:runLoop forMode:[runLoop currentMode]];
|
[dl addToRunLoop:runLoop forMode:[runLoop currentMode]];
|
||||||
|
|||||||
+7
-5
@@ -275,7 +275,7 @@ func (w *window) addEventListeners() {
|
|||||||
}
|
}
|
||||||
w.touches = w.touches[:0]
|
w.touches = w.touches[:0]
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: pointer.Cancel,
|
Kind: pointer.Cancel,
|
||||||
Source: pointer.Touch,
|
Source: pointer.Touch,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -396,7 +398,7 @@ func modifiersFor(e js.Value) key.Modifiers {
|
|||||||
return mods
|
return mods
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) touchEvent(typ pointer.Type, e js.Value) {
|
func (w *window) touchEvent(kind pointer.Kind, e js.Value) {
|
||||||
e.Call("preventDefault")
|
e.Call("preventDefault")
|
||||||
t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond
|
t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond
|
||||||
changedTouches := e.Get("changedTouches")
|
changedTouches := e.Get("changedTouches")
|
||||||
@@ -424,7 +426,7 @@ func (w *window) touchEvent(typ pointer.Type, e js.Value) {
|
|||||||
Y: float32(y) * scale,
|
Y: float32(y) * scale,
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: typ,
|
Kind: kind,
|
||||||
Source: pointer.Touch,
|
Source: pointer.Touch,
|
||||||
Position: pos,
|
Position: pos,
|
||||||
PointerID: pid,
|
PointerID: pid,
|
||||||
@@ -446,7 +448,7 @@ func (w *window) touchIDFor(touch js.Value) pointer.ID {
|
|||||||
return pid
|
return pid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
|
func (w *window) pointerEvent(kind pointer.Kind, dx, dy float32, e js.Value) {
|
||||||
e.Call("preventDefault")
|
e.Call("preventDefault")
|
||||||
x, y := e.Get("clientX").Float(), e.Get("clientY").Float()
|
x, y := e.Get("clientX").Float(), e.Get("clientY").Float()
|
||||||
rect := w.cnv.Call("getBoundingClientRect")
|
rect := w.cnv.Call("getBoundingClientRect")
|
||||||
@@ -474,7 +476,7 @@ func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
|
|||||||
btns |= pointer.ButtonTertiary
|
btns |= pointer.ButtonTertiary
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: typ,
|
Kind: kind,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: btns,
|
Buttons: btns,
|
||||||
Position: pos,
|
Position: pos,
|
||||||
|
|||||||
+18
-10
@@ -192,6 +192,10 @@ static CFTypeRef windowForView(CFTypeRef viewRef) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void raiseWindow(CFTypeRef windowRef) {
|
static void raiseWindow(CFTypeRef windowRef) {
|
||||||
|
NSRunningApplication *currentApp = [NSRunningApplication currentApplication];
|
||||||
|
if (![currentApp isActive]) {
|
||||||
|
[currentApp activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
|
||||||
|
}
|
||||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||||
[window makeKeyAndOrderFront:nil];
|
[window makeKeyAndOrderFront:nil];
|
||||||
}
|
}
|
||||||
@@ -297,7 +301,9 @@ func (w *window) contextView() C.CFTypeRef {
|
|||||||
|
|
||||||
func (w *window) ReadClipboard() {
|
func (w *window) ReadClipboard() {
|
||||||
cstr := C.readClipboard()
|
cstr := C.readClipboard()
|
||||||
defer C.CFRelease(cstr)
|
if cstr != 0 {
|
||||||
|
defer C.CFRelease(cstr)
|
||||||
|
}
|
||||||
content := nsstringToString(cstr)
|
content := nsstringToString(cstr)
|
||||||
w.w.Event(clipboard.Event{Text: content})
|
w.w.Event(clipboard.Event{Text: content})
|
||||||
}
|
}
|
||||||
@@ -365,11 +371,11 @@ func (w *window) Configure(options []Option) {
|
|||||||
case Minimized:
|
case Minimized:
|
||||||
C.unhideWindow(window)
|
C.unhideWindow(window)
|
||||||
case Maximized:
|
case Maximized:
|
||||||
|
if C.isWindowZoomed(window) != 0 {
|
||||||
|
C.zoomWindow(window)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
w.config.Mode = Windowed
|
w.config.Mode = Windowed
|
||||||
if C.isWindowZoomed(window) != 0 {
|
|
||||||
C.zoomWindow(window)
|
|
||||||
}
|
|
||||||
w.setTitle(prev, cnf)
|
w.setTitle(prev, cnf)
|
||||||
if prev.Size != cnf.Size {
|
if prev.Size != cnf.Size {
|
||||||
w.config.Size = cnf.Size
|
w.config.Size = cnf.Size
|
||||||
@@ -523,8 +529,10 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
|
|||||||
btn = pointer.ButtonPrimary
|
btn = pointer.ButtonPrimary
|
||||||
case 1:
|
case 1:
|
||||||
btn = pointer.ButtonSecondary
|
btn = pointer.ButtonSecondary
|
||||||
|
case 2:
|
||||||
|
btn = pointer.ButtonTertiary
|
||||||
}
|
}
|
||||||
var typ pointer.Type
|
var typ pointer.Kind
|
||||||
switch cdir {
|
switch cdir {
|
||||||
case C.MOUSE_MOVE:
|
case C.MOUSE_MOVE:
|
||||||
typ = pointer.Move
|
typ = pointer.Move
|
||||||
@@ -548,7 +556,7 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
|
|||||||
panic("invalid direction")
|
panic("invalid direction")
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: typ,
|
Kind: typ,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Time: t,
|
Time: t,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
@@ -788,13 +796,13 @@ func configFor(scale float32) unit.Metric {
|
|||||||
//export gio_onClose
|
//export gio_onClose
|
||||||
func gio_onClose(view C.CFTypeRef) {
|
func gio_onClose(view C.CFTypeRef) {
|
||||||
w := mustView(view)
|
w := mustView(view)
|
||||||
w.displayLink.Close()
|
|
||||||
w.w.Event(ViewEvent{})
|
w.w.Event(ViewEvent{})
|
||||||
deleteView(view)
|
|
||||||
w.w.Event(system.DestroyEvent{})
|
w.w.Event(system.DestroyEvent{})
|
||||||
|
w.displayLink.Close()
|
||||||
|
w.displayLink = nil
|
||||||
|
deleteView(view)
|
||||||
C.CFRelease(w.view)
|
C.CFRelease(w.view)
|
||||||
w.view = 0
|
w.view = 0
|
||||||
w.displayLink = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onHide
|
//export gio_onHide
|
||||||
@@ -882,7 +890,7 @@ func newOSWindow() (*window, error) {
|
|||||||
scale: scale,
|
scale: scale,
|
||||||
redraw: make(chan struct{}, 1),
|
redraw: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
dl, err := NewDisplayLink(func() {
|
dl, err := newDisplayLink(func() {
|
||||||
select {
|
select {
|
||||||
case w.redraw <- struct{}{}:
|
case w.redraw <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
|
|||||||
+15
-9
@@ -92,24 +92,30 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
|||||||
- (void)mouseUp:(NSEvent *)event {
|
- (void)mouseUp:(NSEvent *)event {
|
||||||
handleMouse(self, event, MOUSE_UP, 0, 0);
|
handleMouse(self, event, MOUSE_UP, 0, 0);
|
||||||
}
|
}
|
||||||
- (void)middleMouseDown:(NSEvent *)event {
|
|
||||||
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
|
||||||
}
|
|
||||||
- (void)middleMouseUp:(NSEvent *)event {
|
|
||||||
handleMouse(self, event, MOUSE_UP, 0, 0);
|
|
||||||
}
|
|
||||||
- (void)rightMouseDown:(NSEvent *)event {
|
- (void)rightMouseDown:(NSEvent *)event {
|
||||||
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
||||||
}
|
}
|
||||||
- (void)rightMouseUp:(NSEvent *)event {
|
- (void)rightMouseUp:(NSEvent *)event {
|
||||||
handleMouse(self, event, MOUSE_UP, 0, 0);
|
handleMouse(self, event, MOUSE_UP, 0, 0);
|
||||||
}
|
}
|
||||||
|
- (void)otherMouseDown:(NSEvent *)event {
|
||||||
|
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
||||||
|
}
|
||||||
|
- (void)otherMouseUp:(NSEvent *)event {
|
||||||
|
handleMouse(self, event, MOUSE_UP, 0, 0);
|
||||||
|
}
|
||||||
- (void)mouseMoved:(NSEvent *)event {
|
- (void)mouseMoved:(NSEvent *)event {
|
||||||
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
||||||
}
|
}
|
||||||
- (void)mouseDragged:(NSEvent *)event {
|
- (void)mouseDragged:(NSEvent *)event {
|
||||||
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
||||||
}
|
}
|
||||||
|
- (void)rightMouseDragged:(NSEvent *)event {
|
||||||
|
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
||||||
|
}
|
||||||
|
- (void)otherMouseDragged:(NSEvent *)event {
|
||||||
|
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
||||||
|
}
|
||||||
- (void)scrollWheel:(NSEvent *)event {
|
- (void)scrollWheel:(NSEvent *)event {
|
||||||
CGFloat dx = -event.scrollingDeltaX;
|
CGFloat dx = -event.scrollingDeltaX;
|
||||||
CGFloat dy = -event.scrollingDeltaY;
|
CGFloat dy = -event.scrollingDeltaY;
|
||||||
@@ -193,14 +199,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
|||||||
static GioWindowDelegate *globalWindowDel;
|
static GioWindowDelegate *globalWindowDel;
|
||||||
|
|
||||||
static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *handle) {
|
static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *handle) {
|
||||||
gio_onFrameCallback(dl, (uintptr_t)handle);
|
gio_onFrameCallback(dl);
|
||||||
return kCVReturnSuccess;
|
return kCVReturnSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
CFTypeRef gio_createDisplayLink(uintptr_t handle) {
|
CFTypeRef gio_createDisplayLink(void) {
|
||||||
CVDisplayLinkRef dl;
|
CVDisplayLinkRef dl;
|
||||||
CVDisplayLinkCreateWithActiveCGDisplays(&dl);
|
CVDisplayLinkCreateWithActiveCGDisplays(&dl);
|
||||||
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, (void *)(handle));
|
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, nil);
|
||||||
return dl;
|
return dl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -791,7 +791,7 @@ func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.
|
|||||||
Y: fromFixed(y) * float32(w.scale),
|
Y: fromFixed(y) * float32(w.scale),
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Source: pointer.Touch,
|
Source: pointer.Touch,
|
||||||
Position: w.lastTouch,
|
Position: w.lastTouch,
|
||||||
PointerID: pointer.ID(id),
|
PointerID: pointer.ID(id),
|
||||||
@@ -807,7 +807,7 @@ func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.ui
|
|||||||
w := s.touchFoci[id]
|
w := s.touchFoci[id]
|
||||||
delete(s.touchFoci, id)
|
delete(s.touchFoci, id)
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Source: pointer.Touch,
|
Source: pointer.Touch,
|
||||||
Position: w.lastTouch,
|
Position: w.lastTouch,
|
||||||
PointerID: pointer.ID(id),
|
PointerID: pointer.ID(id),
|
||||||
@@ -825,7 +825,7 @@ func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32
|
|||||||
Y: fromFixed(y) * float32(w.scale),
|
Y: fromFixed(y) * float32(w.scale),
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: w.lastTouch,
|
Position: w.lastTouch,
|
||||||
Source: pointer.Touch,
|
Source: pointer.Touch,
|
||||||
PointerID: pointer.ID(id),
|
PointerID: pointer.ID(id),
|
||||||
@@ -844,7 +844,7 @@ func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) {
|
|||||||
for id, w := range s.touchFoci {
|
for id, w := range s.touchFoci {
|
||||||
delete(s.touchFoci, id)
|
delete(s.touchFoci, id)
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: pointer.Cancel,
|
Kind: pointer.Cancel,
|
||||||
Source: pointer.Touch,
|
Source: pointer.Touch,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -869,7 +869,7 @@ func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.ui
|
|||||||
s.serial = serial
|
s.serial = serial
|
||||||
if w.inCompositor {
|
if w.inCompositor {
|
||||||
w.inCompositor = false
|
w.inCompositor = false
|
||||||
w.w.Event(pointer.Event{Type: pointer.Cancel})
|
w.w.Event(pointer.Event{Kind: pointer.Cancel})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,21 +917,21 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var typ pointer.Type
|
var kind pointer.Kind
|
||||||
switch state {
|
switch state {
|
||||||
case 0:
|
case 0:
|
||||||
w.pointerBtns &^= btn
|
w.pointerBtns &^= btn
|
||||||
typ = pointer.Release
|
kind = pointer.Release
|
||||||
// Move or resize gestures no longer applies.
|
// Move or resize gestures no longer applies.
|
||||||
w.inCompositor = false
|
w.inCompositor = false
|
||||||
case 1:
|
case 1:
|
||||||
w.pointerBtns |= btn
|
w.pointerBtns |= btn
|
||||||
typ = pointer.Press
|
kind = pointer.Press
|
||||||
}
|
}
|
||||||
w.flushScroll()
|
w.flushScroll()
|
||||||
w.resetFling()
|
w.resetFling()
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: typ,
|
Kind: kind,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
Position: w.lastPos,
|
Position: w.lastPos,
|
||||||
@@ -1573,7 +1573,7 @@ func (w *window) flushScroll() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: pointer.Scroll,
|
Kind: pointer.Scroll,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
Position: w.lastPos,
|
Position: w.lastPos,
|
||||||
@@ -1596,7 +1596,7 @@ func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
|
|||||||
Y: fromFixed(y) * float32(w.scale),
|
Y: fromFixed(y) * float32(w.scale),
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: w.lastPos,
|
Position: w.lastPos,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
|
|||||||
+72
-58
@@ -32,11 +32,6 @@ type ViewEvent struct {
|
|||||||
HWND uintptr
|
HWND uintptr
|
||||||
}
|
}
|
||||||
|
|
||||||
type winDeltas struct {
|
|
||||||
width int32
|
|
||||||
height int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type window struct {
|
type window struct {
|
||||||
hwnd syscall.Handle
|
hwnd syscall.Handle
|
||||||
hdc syscall.Handle
|
hdc syscall.Handle
|
||||||
@@ -55,7 +50,6 @@ type window struct {
|
|||||||
animating bool
|
animating bool
|
||||||
focused bool
|
focused bool
|
||||||
|
|
||||||
deltas winDeltas
|
|
||||||
borderSize image.Point
|
borderSize image.Point
|
||||||
config Config
|
config Config
|
||||||
}
|
}
|
||||||
@@ -192,22 +186,12 @@ func createNativeWindow() (*window, error) {
|
|||||||
// It reads the window style and size/position and updates w.config.
|
// It reads the window style and size/position and updates w.config.
|
||||||
// If anything has changed it emits a ConfigEvent to notify the application.
|
// If anything has changed it emits a ConfigEvent to notify the application.
|
||||||
func (w *window) update() {
|
func (w *window) update() {
|
||||||
r := windows.GetWindowRect(w.hwnd)
|
cr := windows.GetClientRect(w.hwnd)
|
||||||
size := image.Point{
|
w.config.Size = image.Point{
|
||||||
X: int(r.Right - r.Left - w.deltas.width),
|
X: int(cr.Right - cr.Left),
|
||||||
Y: int(r.Bottom - r.Top - w.deltas.height),
|
Y: int(cr.Bottom - cr.Top),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the window mode.
|
|
||||||
style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE)
|
|
||||||
if style&windows.WS_OVERLAPPEDWINDOW == 0 {
|
|
||||||
size = image.Point{
|
|
||||||
X: int(r.Right - r.Left),
|
|
||||||
Y: int(r.Bottom - r.Top),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.config.Size = size
|
|
||||||
|
|
||||||
w.borderSize = image.Pt(
|
w.borderSize = image.Pt(
|
||||||
windows.GetSystemMetrics(windows.SM_CXSIZEFRAME),
|
windows.GetSystemMetrics(windows.SM_CXSIZEFRAME),
|
||||||
windows.GetSystemMetrics(windows.SM_CYSIZEFRAME),
|
windows.GetSystemMetrics(windows.SM_CYSIZEFRAME),
|
||||||
@@ -275,7 +259,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers())
|
w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers())
|
||||||
case windows.WM_CANCELMODE:
|
case windows.WM_CANCELMODE:
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: pointer.Cancel,
|
Kind: pointer.Cancel,
|
||||||
})
|
})
|
||||||
case windows.WM_SETFOCUS:
|
case windows.WM_SETFOCUS:
|
||||||
w.focused = true
|
w.focused = true
|
||||||
@@ -304,7 +288,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
x, y := coordsFromlParam(lParam)
|
x, y := coordsFromlParam(lParam)
|
||||||
p := f32.Point{X: float32(x), Y: float32(y)}
|
p := f32.Point{X: float32(x), Y: float32(y)}
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Position: p,
|
Position: p,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
@@ -325,6 +309,28 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
// The system destroys the HWND for us.
|
// The system destroys the HWND for us.
|
||||||
w.hwnd = 0
|
w.hwnd = 0
|
||||||
windows.PostQuitMessage(0)
|
windows.PostQuitMessage(0)
|
||||||
|
case windows.WM_NCCALCSIZE:
|
||||||
|
if w.config.Decorated {
|
||||||
|
// Let Windows handle decorations.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// No client areas; we draw decorations ourselves.
|
||||||
|
if wParam != 1 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// lParam contains an NCCALCSIZE_PARAMS for us to adjust.
|
||||||
|
place := windows.GetWindowPlacement(w.hwnd)
|
||||||
|
if !place.IsMaximized() {
|
||||||
|
// Nothing do adjust.
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// Adjust window position to avoid the extra padding in maximized
|
||||||
|
// state. See https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543.
|
||||||
|
// Note that trying to do the adjustment in WM_GETMINMAXINFO is ignored by Windows.
|
||||||
|
szp := (*windows.NCCalcSizeParams)(unsafe.Pointer(uintptr(lParam)))
|
||||||
|
mi := windows.GetMonitorInfo(w.hwnd)
|
||||||
|
szp.Rgrc[0] = mi.WorkArea
|
||||||
|
return 0
|
||||||
case windows.WM_PAINT:
|
case windows.WM_PAINT:
|
||||||
w.draw(true)
|
w.draw(true)
|
||||||
case windows.WM_SIZE:
|
case windows.WM_SIZE:
|
||||||
@@ -344,18 +350,26 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
}
|
}
|
||||||
case windows.WM_GETMINMAXINFO:
|
case windows.WM_GETMINMAXINFO:
|
||||||
mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam)))
|
mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam)))
|
||||||
|
var bw, bh int32
|
||||||
|
if w.config.Decorated {
|
||||||
|
r := windows.GetWindowRect(w.hwnd)
|
||||||
|
cr := windows.GetClientRect(w.hwnd)
|
||||||
|
bw = r.Right - r.Left - (cr.Right - cr.Left)
|
||||||
|
bh = r.Bottom - r.Top - (cr.Bottom - cr.Top)
|
||||||
|
}
|
||||||
if p := w.config.MinSize; p.X > 0 || p.Y > 0 {
|
if p := w.config.MinSize; p.X > 0 || p.Y > 0 {
|
||||||
mm.PtMinTrackSize = windows.Point{
|
mm.PtMinTrackSize = windows.Point{
|
||||||
X: int32(p.X) + w.deltas.width,
|
X: int32(p.X) + bw,
|
||||||
Y: int32(p.Y) + w.deltas.height,
|
Y: int32(p.Y) + bh,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p := w.config.MaxSize; p.X > 0 || p.Y > 0 {
|
if p := w.config.MaxSize; p.X > 0 || p.Y > 0 {
|
||||||
mm.PtMaxTrackSize = windows.Point{
|
mm.PtMaxTrackSize = windows.Point{
|
||||||
X: int32(p.X) + w.deltas.width,
|
X: int32(p.X) + bw,
|
||||||
Y: int32(p.Y) + w.deltas.height,
|
Y: int32(p.Y) + bh,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 0
|
||||||
case windows.WM_SETCURSOR:
|
case windows.WM_SETCURSOR:
|
||||||
w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT
|
w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT
|
||||||
if w.cursorIn {
|
if w.cursorIn {
|
||||||
@@ -446,23 +460,18 @@ func (w *window) hitTest(x, y int) uintptr {
|
|||||||
if w.config.Mode == Fullscreen {
|
if w.config.Mode == Fullscreen {
|
||||||
return windows.HTCLIENT
|
return windows.HTCLIENT
|
||||||
}
|
}
|
||||||
p := f32.Pt(float32(x), float32(y))
|
|
||||||
if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove {
|
|
||||||
return windows.HTCAPTION
|
|
||||||
}
|
|
||||||
if w.config.Mode != Windowed {
|
if w.config.Mode != Windowed {
|
||||||
// Only windowed mode should allow resizing.
|
// Only windowed mode should allow resizing.
|
||||||
return windows.HTCLIENT
|
return windows.HTCLIENT
|
||||||
}
|
}
|
||||||
|
// Check for resize handle before system actions; otherwise it can be impossible to
|
||||||
|
// resize a custom-decorations window when the system move area is flush with the
|
||||||
|
// edge of the window.
|
||||||
top := y <= w.borderSize.Y
|
top := y <= w.borderSize.Y
|
||||||
bottom := y >= w.config.Size.Y-w.borderSize.Y
|
bottom := y >= w.config.Size.Y-w.borderSize.Y
|
||||||
left := x <= w.borderSize.X
|
left := x <= w.borderSize.X
|
||||||
right := x >= w.config.Size.X-w.borderSize.X
|
right := x >= w.config.Size.X-w.borderSize.X
|
||||||
switch {
|
switch {
|
||||||
default:
|
|
||||||
fallthrough
|
|
||||||
case !top && !bottom && !left && !right:
|
|
||||||
return windows.HTCLIENT
|
|
||||||
case top && left:
|
case top && left:
|
||||||
return windows.HTTOPLEFT
|
return windows.HTTOPLEFT
|
||||||
case top && right:
|
case top && right:
|
||||||
@@ -480,6 +489,11 @@ func (w *window) hitTest(x, y int) uintptr {
|
|||||||
case right:
|
case right:
|
||||||
return windows.HTRIGHT
|
return windows.HTRIGHT
|
||||||
}
|
}
|
||||||
|
p := f32.Pt(float32(x), float32(y))
|
||||||
|
if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove {
|
||||||
|
return windows.HTCAPTION
|
||||||
|
}
|
||||||
|
return windows.HTCLIENT
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, kmods key.Modifiers) {
|
func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, kmods key.Modifiers) {
|
||||||
@@ -487,15 +501,15 @@ func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr,
|
|||||||
windows.SetFocus(w.hwnd)
|
windows.SetFocus(w.hwnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
var typ pointer.Type
|
var kind pointer.Kind
|
||||||
if press {
|
if press {
|
||||||
typ = pointer.Press
|
kind = pointer.Press
|
||||||
if w.pointerBtns == 0 {
|
if w.pointerBtns == 0 {
|
||||||
windows.SetCapture(w.hwnd)
|
windows.SetCapture(w.hwnd)
|
||||||
}
|
}
|
||||||
w.pointerBtns |= btn
|
w.pointerBtns |= btn
|
||||||
} else {
|
} else {
|
||||||
typ = pointer.Release
|
kind = pointer.Release
|
||||||
w.pointerBtns &^= btn
|
w.pointerBtns &^= btn
|
||||||
if w.pointerBtns == 0 {
|
if w.pointerBtns == 0 {
|
||||||
windows.ReleaseCapture()
|
windows.ReleaseCapture()
|
||||||
@@ -504,7 +518,7 @@ func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr,
|
|||||||
x, y := coordsFromlParam(lParam)
|
x, y := coordsFromlParam(lParam)
|
||||||
p := f32.Point{X: float32(x), Y: float32(y)}
|
p := f32.Point{X: float32(x), Y: float32(y)}
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: typ,
|
Kind: kind,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Position: p,
|
Position: p,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
@@ -539,7 +553,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: pointer.Scroll,
|
Kind: pointer.Scroll,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Position: p,
|
Position: p,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
@@ -669,9 +683,6 @@ func (w *window) Configure(options []Option) {
|
|||||||
swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED)
|
swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED)
|
||||||
winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW)
|
winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW)
|
||||||
style &^= winStyle
|
style &^= winStyle
|
||||||
if !w.config.Decorated {
|
|
||||||
winStyle = 0
|
|
||||||
}
|
|
||||||
switch w.config.Mode {
|
switch w.config.Mode {
|
||||||
case Minimized:
|
case Minimized:
|
||||||
style |= winStyle
|
style |= winStyle
|
||||||
@@ -684,41 +695,44 @@ func (w *window) Configure(options []Option) {
|
|||||||
showMode = windows.SW_SHOWMAXIMIZED
|
showMode = windows.SW_SHOWMAXIMIZED
|
||||||
|
|
||||||
case Windowed:
|
case Windowed:
|
||||||
windows.SetWindowText(w.hwnd, w.config.Title)
|
|
||||||
style |= winStyle
|
style |= winStyle
|
||||||
showMode = windows.SW_SHOWNORMAL
|
showMode = windows.SW_SHOWNORMAL
|
||||||
// Get target for client areaa size.
|
// Get target for client area size.
|
||||||
width = int32(w.config.Size.X)
|
width = int32(w.config.Size.X)
|
||||||
height = int32(w.config.Size.Y)
|
height = int32(w.config.Size.Y)
|
||||||
// Get the current window size and position.
|
// Get the current window size and position.
|
||||||
wr := windows.GetWindowRect(w.hwnd)
|
wr := windows.GetWindowRect(w.hwnd)
|
||||||
// Set desired window size.
|
|
||||||
wr.Right = wr.Left + width
|
|
||||||
wr.Bottom = wr.Top + height
|
|
||||||
// Convert from client size to window size.
|
|
||||||
r := wr
|
|
||||||
windows.AdjustWindowRectEx(&r, uint32(style), 0, dwExStyle)
|
|
||||||
// Calculate difference between client and full window sizes.
|
|
||||||
w.deltas.width = r.Right - wr.Right + wr.Left - r.Left
|
|
||||||
w.deltas.height = r.Bottom - wr.Bottom + wr.Top - r.Top
|
|
||||||
// Set new window size and position.
|
|
||||||
x = wr.Left
|
x = wr.Left
|
||||||
y = wr.Top
|
y = wr.Top
|
||||||
width = r.Right - r.Left
|
if w.config.Decorated {
|
||||||
height = r.Bottom - r.Top
|
// Compute client size and position. Note that the client size is
|
||||||
|
// equal to the window size when we are in control of decorations.
|
||||||
|
r := windows.Rect{
|
||||||
|
Right: width,
|
||||||
|
Bottom: height,
|
||||||
|
}
|
||||||
|
windows.AdjustWindowRectEx(&r, uint32(style), 0, dwExStyle)
|
||||||
|
width = r.Right - r.Left
|
||||||
|
height = r.Bottom - r.Top
|
||||||
|
}
|
||||||
|
if !w.config.Decorated {
|
||||||
|
// Enable drop shadows when we draw decorations.
|
||||||
|
windows.DwmExtendFrameIntoClientArea(w.hwnd, windows.Margins{-1, -1, -1, -1})
|
||||||
|
}
|
||||||
|
|
||||||
case Fullscreen:
|
case Fullscreen:
|
||||||
|
swpStyle |= windows.SWP_NOMOVE | windows.SWP_NOSIZE
|
||||||
mi := windows.GetMonitorInfo(w.hwnd)
|
mi := windows.GetMonitorInfo(w.hwnd)
|
||||||
x, y = mi.Monitor.Left, mi.Monitor.Top
|
x, y = mi.Monitor.Left, mi.Monitor.Top
|
||||||
width = mi.Monitor.Right - mi.Monitor.Left
|
width = mi.Monitor.Right - mi.Monitor.Left
|
||||||
height = mi.Monitor.Bottom - mi.Monitor.Top
|
height = mi.Monitor.Bottom - mi.Monitor.Top
|
||||||
showMode = windows.SW_SHOW
|
showMode = windows.SW_SHOWMAXIMIZED
|
||||||
}
|
}
|
||||||
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style)
|
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style)
|
||||||
windows.SetWindowPos(w.hwnd, 0, x, y, width, height, swpStyle)
|
windows.SetWindowPos(w.hwnd, 0, x, y, width, height, swpStyle)
|
||||||
windows.ShowWindow(w.hwnd, showMode)
|
windows.ShowWindow(w.hwnd, showMode)
|
||||||
|
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) WriteClipboard(s string) {
|
func (w *window) WriteClipboard(s string) {
|
||||||
|
|||||||
+7
-7
@@ -547,7 +547,7 @@ func (h *x11EventHandler) handleEvents() bool {
|
|||||||
case C.ButtonPress, C.ButtonRelease:
|
case C.ButtonPress, C.ButtonRelease:
|
||||||
bevt := (*C.XButtonEvent)(unsafe.Pointer(xev))
|
bevt := (*C.XButtonEvent)(unsafe.Pointer(xev))
|
||||||
ev := pointer.Event{
|
ev := pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Position: f32.Point{
|
Position: f32.Point{
|
||||||
X: float32(bevt.x),
|
X: float32(bevt.x),
|
||||||
@@ -557,7 +557,7 @@ func (h *x11EventHandler) handleEvents() bool {
|
|||||||
Modifiers: w.xkb.Modifiers(),
|
Modifiers: w.xkb.Modifiers(),
|
||||||
}
|
}
|
||||||
if bevt._type == C.ButtonRelease {
|
if bevt._type == C.ButtonRelease {
|
||||||
ev.Type = pointer.Release
|
ev.Kind = pointer.Release
|
||||||
}
|
}
|
||||||
var btn pointer.Buttons
|
var btn pointer.Buttons
|
||||||
const scrollScale = 10
|
const scrollScale = 10
|
||||||
@@ -569,7 +569,7 @@ func (h *x11EventHandler) handleEvents() bool {
|
|||||||
case C.Button3:
|
case C.Button3:
|
||||||
btn = pointer.ButtonSecondary
|
btn = pointer.ButtonSecondary
|
||||||
case C.Button4:
|
case C.Button4:
|
||||||
ev.Type = pointer.Scroll
|
ev.Kind = pointer.Scroll
|
||||||
// scroll up or left (if shift is pressed).
|
// scroll up or left (if shift is pressed).
|
||||||
if ev.Modifiers == key.ModShift {
|
if ev.Modifiers == key.ModShift {
|
||||||
ev.Scroll.X = -scrollScale
|
ev.Scroll.X = -scrollScale
|
||||||
@@ -578,7 +578,7 @@ func (h *x11EventHandler) handleEvents() bool {
|
|||||||
}
|
}
|
||||||
case C.Button5:
|
case C.Button5:
|
||||||
// scroll down or right (if shift is pressed).
|
// scroll down or right (if shift is pressed).
|
||||||
ev.Type = pointer.Scroll
|
ev.Kind = pointer.Scroll
|
||||||
if ev.Modifiers == key.ModShift {
|
if ev.Modifiers == key.ModShift {
|
||||||
ev.Scroll.X = +scrollScale
|
ev.Scroll.X = +scrollScale
|
||||||
} else {
|
} else {
|
||||||
@@ -587,11 +587,11 @@ func (h *x11EventHandler) handleEvents() bool {
|
|||||||
case 6:
|
case 6:
|
||||||
// http://xahlee.info/linux/linux_x11_mouse_button_number.html
|
// http://xahlee.info/linux/linux_x11_mouse_button_number.html
|
||||||
// scroll left.
|
// scroll left.
|
||||||
ev.Type = pointer.Scroll
|
ev.Kind = pointer.Scroll
|
||||||
ev.Scroll.X = -scrollScale * 2
|
ev.Scroll.X = -scrollScale * 2
|
||||||
case 7:
|
case 7:
|
||||||
// scroll right
|
// scroll right
|
||||||
ev.Type = pointer.Scroll
|
ev.Kind = pointer.Scroll
|
||||||
ev.Scroll.X = +scrollScale * 2
|
ev.Scroll.X = +scrollScale * 2
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
@@ -607,7 +607,7 @@ func (h *x11EventHandler) handleEvents() bool {
|
|||||||
case C.MotionNotify:
|
case C.MotionNotify:
|
||||||
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
|
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
|
||||||
w.w.Event(pointer.Event{
|
w.w.Event(pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
Position: f32.Point{
|
Position: f32.Point{
|
||||||
|
|||||||
+61
-32
@@ -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"
|
||||||
@@ -59,6 +61,8 @@ type Window struct {
|
|||||||
// actions are the actions waiting to be performed.
|
// actions are the actions waiting to be performed.
|
||||||
actions chan system.Action
|
actions chan system.Action
|
||||||
|
|
||||||
|
// out is where the platform backend delivers events bound for the
|
||||||
|
// user program.
|
||||||
out chan event.Event
|
out chan event.Event
|
||||||
frames chan *op.Ops
|
frames chan *op.Ops
|
||||||
frameAck chan struct{}
|
frameAck chan struct{}
|
||||||
@@ -105,6 +109,16 @@ type Window struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
imeState editorState
|
imeState editorState
|
||||||
|
|
||||||
|
// event stores the state required for processing and delivering events
|
||||||
|
// from NextEvent. If we had support for range over func, this would
|
||||||
|
// be the iterator state.
|
||||||
|
eventState struct {
|
||||||
|
created bool
|
||||||
|
initialOpts []Option
|
||||||
|
wakeup func()
|
||||||
|
timer *time.Timer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type editorState struct {
|
type editorState struct {
|
||||||
@@ -136,9 +150,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),
|
||||||
@@ -181,7 +197,7 @@ func NewWindow(options ...Option) *Window {
|
|||||||
w.imeState.compose = key.Range{Start: -1, End: -1}
|
w.imeState.compose = key.Range{Start: -1, End: -1}
|
||||||
w.semantic.ids = make(map[router.SemanticID]router.SemanticNode)
|
w.semantic.ids = make(map[router.SemanticID]router.SemanticNode)
|
||||||
w.callbacks.w = w
|
w.callbacks.w = w
|
||||||
go w.run(options)
|
w.eventState.initialOpts = options
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,11 +207,6 @@ func decoHeightOpt(h unit.Dp) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events returns the channel where events are delivered.
|
|
||||||
func (w *Window) Events() <-chan event.Event {
|
|
||||||
return w.out
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the window contents, input operations declare input handlers,
|
// update the window contents, input operations declare input handlers,
|
||||||
// and so on. The supplied operations list completely replaces the window state
|
// and so on. The supplied operations list completely replaces the window state
|
||||||
// from previous calls.
|
// from previous calls.
|
||||||
@@ -709,7 +720,7 @@ func (w *Window) waitAck(d driver) {
|
|||||||
select {
|
select {
|
||||||
case f := <-w.driverFuncs:
|
case f := <-w.driverFuncs:
|
||||||
f(d)
|
f(d)
|
||||||
case w.out <- event.Event(nil):
|
case w.out <- theFlushEvent:
|
||||||
// A dummy event went through, so we know the application has processed the previous event.
|
// A dummy event went through, so we know the application has processed the previous event.
|
||||||
return
|
return
|
||||||
case <-w.immediateRedraws:
|
case <-w.immediateRedraws:
|
||||||
@@ -743,7 +754,7 @@ func (w *Window) waitFrame(d driver) *op.Ops {
|
|||||||
case frame := <-w.frames:
|
case frame := <-w.frames:
|
||||||
// The client called FrameEvent.Frame.
|
// The client called FrameEvent.Frame.
|
||||||
return frame
|
return frame
|
||||||
case w.out <- event.Event(nil):
|
case w.out <- theFlushEvent:
|
||||||
// The client ignored FrameEvent and continued processing
|
// The client ignored FrameEvent and continued processing
|
||||||
// events.
|
// events.
|
||||||
return nil
|
return nil
|
||||||
@@ -877,7 +888,6 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
|
|||||||
if err := w.validateAndProcess(d, viewSize, e2.Sync, wrapper, signal); err != nil {
|
if err := w.validateAndProcess(d, viewSize, e2.Sync, wrapper, signal); err != nil {
|
||||||
w.destroyGPU()
|
w.destroyGPU()
|
||||||
w.out <- system.DestroyEvent{Err: err}
|
w.out <- system.DestroyEvent{Err: err}
|
||||||
close(w.out)
|
|
||||||
close(w.destroy)
|
close(w.destroy)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -886,7 +896,6 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
|
|||||||
case system.DestroyEvent:
|
case system.DestroyEvent:
|
||||||
w.destroyGPU()
|
w.destroyGPU()
|
||||||
w.out <- e2
|
w.out <- e2
|
||||||
close(w.out)
|
|
||||||
close(w.destroy)
|
close(w.destroy)
|
||||||
case ViewEvent:
|
case ViewEvent:
|
||||||
w.out <- e2
|
w.out <- e2
|
||||||
@@ -895,6 +904,7 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
|
|||||||
w.decorations.Config = e2.Config
|
w.decorations.Config = e2.Config
|
||||||
e2.Config = w.effectiveConfig()
|
e2.Config = w.effectiveConfig()
|
||||||
w.out <- e2
|
w.out <- e2
|
||||||
|
case wakeupEvent:
|
||||||
case event.Event:
|
case event.Event:
|
||||||
handled := w.queue.q.Queue(e2)
|
handled := w.queue.q.Queue(e2)
|
||||||
if e, ok := e.(key.Event); ok && !handled {
|
if e, ok := e.(key.Event); ok && !handled {
|
||||||
@@ -934,43 +944,51 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Window) run(options []Option) {
|
// NextEvent blocks until an event is received from the window, such as
|
||||||
if err := newWindow(&w.callbacks, options); err != nil {
|
// [io/system.FrameEvent]. It blocks forever if called after [io/system.DestroyEvent]
|
||||||
w.out <- system.DestroyEvent{Err: err}
|
// has been returned.
|
||||||
close(w.out)
|
func (w *Window) NextEvent() event.Event {
|
||||||
close(w.destroy)
|
state := &w.eventState
|
||||||
return
|
if !state.created {
|
||||||
|
state.created = true
|
||||||
|
if err := newWindow(&w.callbacks, state.initialOpts); err != nil {
|
||||||
|
close(w.destroy)
|
||||||
|
return system.DestroyEvent{Err: err}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var wakeup func()
|
|
||||||
var timer *time.Timer
|
|
||||||
for {
|
for {
|
||||||
var (
|
var (
|
||||||
wakeups <-chan struct{}
|
wakeups <-chan struct{}
|
||||||
timeC <-chan time.Time
|
timeC <-chan time.Time
|
||||||
)
|
)
|
||||||
if wakeup != nil {
|
if state.wakeup != nil {
|
||||||
wakeups = w.wakeups
|
wakeups = w.wakeups
|
||||||
if timer != nil {
|
if state.timer != nil {
|
||||||
timeC = timer.C
|
timeC = state.timer.C
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case t := <-w.scheduledRedraws:
|
case t := <-w.scheduledRedraws:
|
||||||
if timer != nil {
|
if state.timer != nil {
|
||||||
timer.Stop()
|
state.timer.Stop()
|
||||||
}
|
}
|
||||||
timer = time.NewTimer(time.Until(t))
|
state.timer = time.NewTimer(time.Until(t))
|
||||||
case <-w.destroy:
|
case e := <-w.out:
|
||||||
return
|
// Receiving a flushEvent indicates to the platform backend that
|
||||||
|
// all previous events have been processed by the user program.
|
||||||
|
if _, ok := e.(flushEvent); ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return e
|
||||||
case <-timeC:
|
case <-timeC:
|
||||||
select {
|
select {
|
||||||
case w.redraws <- struct{}{}:
|
case w.redraws <- struct{}{}:
|
||||||
wakeup()
|
state.wakeup()
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
case <-wakeups:
|
case <-wakeups:
|
||||||
wakeup()
|
state.wakeup()
|
||||||
case wakeup = <-w.wakeupFuncs:
|
case state.wakeup = <-w.wakeupFuncs:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1018,9 +1036,9 @@ func (w *Window) decorate(d driver, e system.FrameEvent, o *op.Ops) (size, offse
|
|||||||
Metric: e.Metric,
|
Metric: e.Metric,
|
||||||
Constraints: layout.Exact(e.Size),
|
Constraints: layout.Exact(e.Size),
|
||||||
}
|
}
|
||||||
style.Layout(gtx)
|
|
||||||
// Update the window based on the actions on the decorations.
|
// Update the window based on the actions on the decorations.
|
||||||
w.Perform(deco.Actions())
|
w.Perform(deco.Update(gtx))
|
||||||
|
style.Layout(gtx)
|
||||||
// Offset to place the frame content below the decorations.
|
// Offset to place the frame content below the decorations.
|
||||||
decoHeight := gtx.Dp(w.decorations.Config.decoHeight)
|
decoHeight := gtx.Dp(w.decorations.Config.decoHeight)
|
||||||
if w.decorations.currentHeight != decoHeight {
|
if w.decorations.currentHeight != decoHeight {
|
||||||
@@ -1161,3 +1179,14 @@ func Decorated(enabled bool) Option {
|
|||||||
cnf.Decorated = enabled
|
cnf.Decorated = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// flushEvent is sent to detect when the user program
|
||||||
|
// has completed processing of all prior events. Its an
|
||||||
|
// [io/event.Event] but only for internal use.
|
||||||
|
type flushEvent struct{}
|
||||||
|
|
||||||
|
func (t flushEvent) ImplementsEvent() {}
|
||||||
|
|
||||||
|
// theFlushEvent avoids allocating garbage when sending
|
||||||
|
// flushEvents.
|
||||||
|
var theFlushEvent flushEvent
|
||||||
|
|||||||
+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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+38
-39
@@ -39,18 +39,18 @@ type Hover struct {
|
|||||||
func (h *Hover) Add(ops *op.Ops) {
|
func (h *Hover) Add(ops *op.Ops) {
|
||||||
pointer.InputOp{
|
pointer.InputOp{
|
||||||
Tag: h,
|
Tag: h,
|
||||||
Types: pointer.Enter | pointer.Leave,
|
Kinds: pointer.Enter | pointer.Leave,
|
||||||
}.Add(ops)
|
}.Add(ops)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hovered returns whether a pointer is inside the area.
|
// Update state and report whether a pointer is inside the area.
|
||||||
func (h *Hover) Hovered(q event.Queue) bool {
|
func (h *Hover) Update(q event.Queue) bool {
|
||||||
for _, ev := range q.Events(h) {
|
for _, ev := range q.Events(h) {
|
||||||
e, ok := ev.(pointer.Event)
|
e, ok := ev.(pointer.Event)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch e.Type {
|
switch e.Kind {
|
||||||
case pointer.Leave, pointer.Cancel:
|
case pointer.Leave, pointer.Cancel:
|
||||||
if h.entered && h.pid == e.PointerID {
|
if h.entered && h.pid == e.PointerID {
|
||||||
h.entered = false
|
h.entered = false
|
||||||
@@ -87,10 +87,10 @@ type Click struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ClickEvent represent a click action, either a
|
// ClickEvent represent a click action, either a
|
||||||
// TypePress for the beginning of a click or a
|
// KindPress for the beginning of a click or a
|
||||||
// TypeClick for a completed click.
|
// KindClick for a completed click.
|
||||||
type ClickEvent struct {
|
type ClickEvent struct {
|
||||||
Type ClickType
|
Kind ClickKind
|
||||||
Position image.Point
|
Position image.Point
|
||||||
Source pointer.Source
|
Source pointer.Source
|
||||||
Modifiers key.Modifiers
|
Modifiers key.Modifiers
|
||||||
@@ -99,7 +99,7 @@ type ClickEvent struct {
|
|||||||
NumClicks int
|
NumClicks int
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClickType uint8
|
type ClickKind uint8
|
||||||
|
|
||||||
// Drag detects drag gestures in the form of pointer.Drag events.
|
// Drag detects drag gestures in the form of pointer.Drag events.
|
||||||
type Drag struct {
|
type Drag struct {
|
||||||
@@ -136,15 +136,15 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// TypePress is reported for the first pointer
|
// KindPress is reported for the first pointer
|
||||||
// press.
|
// press.
|
||||||
TypePress ClickType = iota
|
KindPress ClickKind = iota
|
||||||
// TypeClick is reported when a click action
|
// KindClick is reported when a click action
|
||||||
// is complete.
|
// is complete.
|
||||||
TypeClick
|
KindClick
|
||||||
// TypeCancel is reported when the gesture is
|
// KindCancel is reported when the gesture is
|
||||||
// cancelled.
|
// cancelled.
|
||||||
TypeCancel
|
KindCancel
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -163,7 +163,7 @@ const touchSlop = unit.Dp(3)
|
|||||||
func (c *Click) Add(ops *op.Ops) {
|
func (c *Click) Add(ops *op.Ops) {
|
||||||
pointer.InputOp{
|
pointer.InputOp{
|
||||||
Tag: c,
|
Tag: c,
|
||||||
Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
|
Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
|
||||||
}.Add(ops)
|
}.Add(ops)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,24 +177,24 @@ func (c *Click) Pressed() bool {
|
|||||||
return c.pressed
|
return c.pressed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events returns the next click events, if any.
|
// Update state and return the click events.
|
||||||
func (c *Click) Events(q event.Queue) []ClickEvent {
|
func (c *Click) Update(q event.Queue) []ClickEvent {
|
||||||
var events []ClickEvent
|
var events []ClickEvent
|
||||||
for _, evt := range q.Events(c) {
|
for _, evt := range q.Events(c) {
|
||||||
e, ok := evt.(pointer.Event)
|
e, ok := evt.(pointer.Event)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch e.Type {
|
switch e.Kind {
|
||||||
case pointer.Release:
|
case pointer.Release:
|
||||||
if !c.pressed || c.pid != e.PointerID {
|
if !c.pressed || c.pid != e.PointerID {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
c.pressed = false
|
c.pressed = false
|
||||||
if !c.entered || c.hovered {
|
if !c.entered || c.hovered {
|
||||||
events = append(events, ClickEvent{Type: TypeClick, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
|
events = append(events, ClickEvent{Kind: KindClick, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
|
||||||
} else {
|
} else {
|
||||||
events = append(events, ClickEvent{Type: TypeCancel})
|
events = append(events, ClickEvent{Kind: KindCancel})
|
||||||
}
|
}
|
||||||
case pointer.Cancel:
|
case pointer.Cancel:
|
||||||
wasPressed := c.pressed
|
wasPressed := c.pressed
|
||||||
@@ -202,7 +202,7 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
|
|||||||
c.hovered = false
|
c.hovered = false
|
||||||
c.entered = false
|
c.entered = false
|
||||||
if wasPressed {
|
if wasPressed {
|
||||||
events = append(events, ClickEvent{Type: TypeCancel})
|
events = append(events, ClickEvent{Kind: KindCancel})
|
||||||
}
|
}
|
||||||
case pointer.Press:
|
case pointer.Press:
|
||||||
if c.pressed {
|
if c.pressed {
|
||||||
@@ -224,7 +224,7 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
|
|||||||
c.clicks = 1
|
c.clicks = 1
|
||||||
}
|
}
|
||||||
c.clickedAt = e.Time
|
c.clickedAt = e.Time
|
||||||
events = append(events, ClickEvent{Type: TypePress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
|
events = append(events, ClickEvent{Kind: KindPress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
|
||||||
case pointer.Leave:
|
case pointer.Leave:
|
||||||
if !c.pressed {
|
if !c.pressed {
|
||||||
c.pid = e.PointerID
|
c.pid = e.PointerID
|
||||||
@@ -254,7 +254,7 @@ func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) {
|
|||||||
oph := pointer.InputOp{
|
oph := pointer.InputOp{
|
||||||
Tag: s,
|
Tag: s,
|
||||||
Grab: s.grab,
|
Grab: s.grab,
|
||||||
Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
|
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
|
||||||
ScrollBounds: bounds,
|
ScrollBounds: bounds,
|
||||||
}
|
}
|
||||||
oph.Add(ops)
|
oph.Add(ops)
|
||||||
@@ -268,9 +268,8 @@ func (s *Scroll) Stop() {
|
|||||||
s.flinger = fling.Animation{}
|
s.flinger = fling.Animation{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll detects the scrolling distance from the available events and
|
// Update state and report the scroll distance along axis.
|
||||||
// ongoing fling gestures.
|
func (s *Scroll) Update(cfg unit.Metric, q event.Queue, t time.Time, axis Axis) int {
|
||||||
func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis) int {
|
|
||||||
if s.axis != axis {
|
if s.axis != axis {
|
||||||
s.axis = axis
|
s.axis = axis
|
||||||
return 0
|
return 0
|
||||||
@@ -281,7 +280,7 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch e.Type {
|
switch e.Kind {
|
||||||
case pointer.Press:
|
case pointer.Press:
|
||||||
if s.dragging {
|
if s.dragging {
|
||||||
break
|
break
|
||||||
@@ -368,12 +367,12 @@ func (d *Drag) Add(ops *op.Ops) {
|
|||||||
pointer.InputOp{
|
pointer.InputOp{
|
||||||
Tag: d,
|
Tag: d,
|
||||||
Grab: d.grab,
|
Grab: d.grab,
|
||||||
Types: pointer.Press | pointer.Drag | pointer.Release,
|
Kinds: pointer.Press | pointer.Drag | pointer.Release,
|
||||||
}.Add(ops)
|
}.Add(ops)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events returns the next drag events, if any.
|
// Update state and return the drag events.
|
||||||
func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event {
|
func (d *Drag) Update(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event {
|
||||||
var events []pointer.Event
|
var events []pointer.Event
|
||||||
for _, e := range q.Events(d) {
|
for _, e := range q.Events(d) {
|
||||||
e, ok := e.(pointer.Event)
|
e, ok := e.(pointer.Event)
|
||||||
@@ -381,7 +380,7 @@ func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch e.Type {
|
switch e.Kind {
|
||||||
case pointer.Press:
|
case pointer.Press:
|
||||||
if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) {
|
if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) {
|
||||||
continue
|
continue
|
||||||
@@ -444,16 +443,16 @@ func (a Axis) String() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ct ClickType) String() string {
|
func (ct ClickKind) String() string {
|
||||||
switch ct {
|
switch ct {
|
||||||
case TypePress:
|
case KindPress:
|
||||||
return "TypePress"
|
return "KindPress"
|
||||||
case TypeClick:
|
case KindClick:
|
||||||
return "TypeClick"
|
return "KindClick"
|
||||||
case TypeCancel:
|
case KindCancel:
|
||||||
return "TypeCancel"
|
return "KindCancel"
|
||||||
default:
|
default:
|
||||||
panic("invalid ClickType")
|
panic("invalid ClickKind")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,16 +26,16 @@ func TestHover(t *testing.T) {
|
|||||||
r.Frame(ops)
|
r.Frame(ops)
|
||||||
|
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{Type: pointer.Move, Position: f32.Pt(30, 30)},
|
pointer.Event{Kind: pointer.Move, Position: f32.Pt(30, 30)},
|
||||||
)
|
)
|
||||||
if !h.Hovered(r) {
|
if !h.Update(r) {
|
||||||
t.Fatal("expected hovered")
|
t.Fatal("expected hovered")
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{Type: pointer.Move, Position: f32.Pt(50, 50)},
|
pointer.Event{Kind: pointer.Move, Position: f32.Pt(50, 50)},
|
||||||
)
|
)
|
||||||
if h.Hovered(r) {
|
if h.Update(r) {
|
||||||
t.Fatal("expected not hovered")
|
t.Fatal("expected not hovered")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ func TestMouseClicks(t *testing.T) {
|
|||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(tc.events...)
|
r.Queue(tc.events...)
|
||||||
|
|
||||||
events := click.Events(&r)
|
events := click.Update(&r)
|
||||||
clicks := filterMouseClicks(events)
|
clicks := filterMouseClicks(events)
|
||||||
if got, want := len(clicks), len(tc.clicks); got != want {
|
if got, want := len(clicks), len(tc.clicks); got != want {
|
||||||
t.Fatalf("got %d mouse clicks, expected %d", got, want)
|
t.Fatalf("got %d mouse clicks, expected %d", got, want)
|
||||||
@@ -92,7 +92,7 @@ func TestMouseClicks(t *testing.T) {
|
|||||||
|
|
||||||
func mouseClickEvents(times ...time.Duration) []event.Event {
|
func mouseClickEvents(times ...time.Duration) []event.Event {
|
||||||
press := pointer.Event{
|
press := pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ func mouseClickEvents(times ...time.Duration) []event.Event {
|
|||||||
press := press
|
press := press
|
||||||
press.Time = t
|
press.Time = t
|
||||||
release := press
|
release := press
|
||||||
release.Type = pointer.Release
|
release.Kind = pointer.Release
|
||||||
events = append(events, press, release)
|
events = append(events, press, release)
|
||||||
}
|
}
|
||||||
return events
|
return events
|
||||||
@@ -110,7 +110,7 @@ func mouseClickEvents(times ...time.Duration) []event.Event {
|
|||||||
func filterMouseClicks(events []ClickEvent) []ClickEvent {
|
func filterMouseClicks(events []ClickEvent) []ClickEvent {
|
||||||
var clicks []ClickEvent
|
var clicks []ClickEvent
|
||||||
for _, ev := range events {
|
for _, ev := range events {
|
||||||
if ev.Type == TypeClick {
|
if ev.Kind == KindClick {
|
||||||
clicks = append(clicks, ev)
|
clicks = append(clicks, ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.8
|
||||||
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
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8v
|
|||||||
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
|
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
|
||||||
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.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
|
||||||
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
gioui.org/shader v1.0.8/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=
|
||||||
|
|||||||
+226
-46
@@ -68,22 +68,40 @@ type renderer struct {
|
|||||||
pather *pather
|
pather *pather
|
||||||
packer packer
|
packer packer
|
||||||
intersections packer
|
intersections packer
|
||||||
|
layers packer
|
||||||
|
layerFBOs fboSet
|
||||||
}
|
}
|
||||||
|
|
||||||
type drawOps struct {
|
type drawOps struct {
|
||||||
profile bool
|
profile bool
|
||||||
reader ops.Reader
|
reader ops.Reader
|
||||||
states []f32.Affine2D
|
states []f32.Affine2D
|
||||||
transStack []f32.Affine2D
|
transStack []f32.Affine2D
|
||||||
vertCache []byte
|
layers []opacityLayer
|
||||||
viewport image.Point
|
opacityStack []int
|
||||||
clear bool
|
vertCache []byte
|
||||||
clearColor f32color.RGBA
|
viewport image.Point
|
||||||
imageOps []imageOp
|
clear bool
|
||||||
pathOps []*pathOp
|
clearColor f32color.RGBA
|
||||||
pathOpCache []pathOp
|
imageOps []imageOp
|
||||||
qs quadSplitter
|
pathOps []*pathOp
|
||||||
pathCache *opCache
|
pathOpCache []pathOp
|
||||||
|
qs quadSplitter
|
||||||
|
pathCache *opCache
|
||||||
|
}
|
||||||
|
|
||||||
|
type opacityLayer struct {
|
||||||
|
opacity float32
|
||||||
|
parent int
|
||||||
|
// depth of the opacity stack. Layers of equal depth are
|
||||||
|
// independent and may be packed into one atlas.
|
||||||
|
depth int
|
||||||
|
// opStart and opEnd denote the range of drawOps.imageOps
|
||||||
|
// that belong to the layer.
|
||||||
|
opStart, opEnd int
|
||||||
|
// clip of the layer operations.
|
||||||
|
clip image.Rectangle
|
||||||
|
place placement
|
||||||
}
|
}
|
||||||
|
|
||||||
type drawState struct {
|
type drawState struct {
|
||||||
@@ -127,7 +145,12 @@ type imageOp struct {
|
|||||||
clip image.Rectangle
|
clip image.Rectangle
|
||||||
material material
|
material material
|
||||||
clipType clipType
|
clipType clipType
|
||||||
place placement
|
// place is either a placement in the path fbos or intersection fbos,
|
||||||
|
// depending on clipType.
|
||||||
|
place placement
|
||||||
|
// layerOps is the number of operations this
|
||||||
|
// operation replaces.
|
||||||
|
layerOps int
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeStrokeOp(data []byte) float32 {
|
func decodeStrokeOp(data []byte) float32 {
|
||||||
@@ -154,17 +177,25 @@ type material struct {
|
|||||||
// For materialTypeColor.
|
// For materialTypeColor.
|
||||||
color f32color.RGBA
|
color f32color.RGBA
|
||||||
// For materialTypeLinearGradient.
|
// For materialTypeLinearGradient.
|
||||||
color1 f32color.RGBA
|
color1 f32color.RGBA
|
||||||
color2 f32color.RGBA
|
color2 f32color.RGBA
|
||||||
|
opacity float32
|
||||||
// For materialTypeTexture.
|
// For materialTypeTexture.
|
||||||
data imageOpData
|
data imageOpData
|
||||||
|
tex driver.Texture
|
||||||
uvTrans f32.Affine2D
|
uvTrans f32.Affine2D
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
filterLinear = 0
|
||||||
|
filterNearest = 1
|
||||||
|
)
|
||||||
|
|
||||||
// imageOpData is the shadow of paint.ImageOp.
|
// imageOpData is the shadow of paint.ImageOp.
|
||||||
type imageOpData struct {
|
type imageOpData struct {
|
||||||
src *image.RGBA
|
src *image.RGBA
|
||||||
handle interface{}
|
handle interface{}
|
||||||
|
filter byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type linearGradientOpData struct {
|
type linearGradientOpData struct {
|
||||||
@@ -182,6 +213,7 @@ func decodeImageOp(data []byte, refs []interface{}) imageOpData {
|
|||||||
return imageOpData{
|
return imageOpData{
|
||||||
src: refs[0].(*image.RGBA),
|
src: refs[0].(*image.RGBA),
|
||||||
handle: handle,
|
handle: handle,
|
||||||
|
filter: data[1],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,8 +254,6 @@ func decodeLinearGradientOp(data []byte) linearGradientOpData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type clipType uint8
|
|
||||||
|
|
||||||
type resource interface {
|
type resource interface {
|
||||||
release()
|
release()
|
||||||
}
|
}
|
||||||
@@ -273,6 +303,9 @@ type blitUniforms struct {
|
|||||||
transform [4]float32
|
transform [4]float32
|
||||||
uvTransformR1 [4]float32
|
uvTransformR1 [4]float32
|
||||||
uvTransformR2 [4]float32
|
uvTransformR2 [4]float32
|
||||||
|
opacity float32
|
||||||
|
fbo float32
|
||||||
|
_ [2]float32
|
||||||
}
|
}
|
||||||
|
|
||||||
type colorUniforms struct {
|
type colorUniforms struct {
|
||||||
@@ -284,7 +317,7 @@ type gradientUniforms struct {
|
|||||||
color2 f32color.RGBA
|
color2 f32color.RGBA
|
||||||
}
|
}
|
||||||
|
|
||||||
type materialType uint8
|
type clipType uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
clipTypeNone clipType = iota
|
clipTypeNone clipType = iota
|
||||||
@@ -292,6 +325,8 @@ const (
|
|||||||
clipTypeIntersection
|
clipTypeIntersection
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type materialType uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
materialColor materialType = iota
|
materialColor materialType = iota
|
||||||
materialLinearGradient
|
materialLinearGradient
|
||||||
@@ -391,6 +426,8 @@ func (g *gpu) frame(target RenderTarget) error {
|
|||||||
g.coverTimer.begin()
|
g.coverTimer.begin()
|
||||||
g.renderer.uploadImages(g.cache, g.drawOps.imageOps)
|
g.renderer.uploadImages(g.cache, g.drawOps.imageOps)
|
||||||
g.renderer.prepareDrawOps(g.cache, g.drawOps.imageOps)
|
g.renderer.prepareDrawOps(g.cache, g.drawOps.imageOps)
|
||||||
|
g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers)
|
||||||
|
g.renderer.drawLayers(g.cache, g.drawOps.layers, g.drawOps.imageOps)
|
||||||
d := driver.LoadDesc{
|
d := driver.LoadDesc{
|
||||||
ClearColor: g.drawOps.clearColor,
|
ClearColor: g.drawOps.clearColor,
|
||||||
}
|
}
|
||||||
@@ -400,7 +437,7 @@ func (g *gpu) frame(target RenderTarget) error {
|
|||||||
}
|
}
|
||||||
g.ctx.BeginRenderPass(defFBO, d)
|
g.ctx.BeginRenderPass(defFBO, d)
|
||||||
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
|
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
|
||||||
g.renderer.drawOps(g.cache, g.drawOps.imageOps)
|
g.renderer.drawOps(g.cache, false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps)
|
||||||
g.coverTimer.end()
|
g.coverTimer.end()
|
||||||
g.ctx.EndRenderPass()
|
g.ctx.EndRenderPass()
|
||||||
g.cleanupTimer.begin()
|
g.cleanupTimer.begin()
|
||||||
@@ -424,19 +461,41 @@ func (g *gpu) Profile() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture {
|
func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture {
|
||||||
|
type cachekey struct {
|
||||||
|
filter byte
|
||||||
|
handle any
|
||||||
|
}
|
||||||
|
key := cachekey{
|
||||||
|
filter: data.filter,
|
||||||
|
handle: data.handle,
|
||||||
|
}
|
||||||
|
|
||||||
var tex *texture
|
var tex *texture
|
||||||
t, exists := cache.get(data.handle)
|
t, exists := cache.get(key)
|
||||||
if !exists {
|
if !exists {
|
||||||
t = &texture{
|
t = &texture{
|
||||||
src: data.src,
|
src: data.src,
|
||||||
}
|
}
|
||||||
cache.put(data.handle, t)
|
cache.put(key, t)
|
||||||
}
|
}
|
||||||
tex = t.(*texture)
|
tex = t.(*texture)
|
||||||
if tex.tex != nil {
|
if tex.tex != nil {
|
||||||
return tex.tex
|
return tex.tex
|
||||||
}
|
}
|
||||||
handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA, data.src.Bounds().Dx(), data.src.Bounds().Dy(), driver.FilterLinearMipmapLinear, driver.FilterLinear, driver.BufferBindingTexture)
|
|
||||||
|
var minFilter, magFilter driver.TextureFilter
|
||||||
|
switch data.filter {
|
||||||
|
case filterLinear:
|
||||||
|
minFilter, magFilter = driver.FilterLinearMipmapLinear, driver.FilterLinear
|
||||||
|
case filterNearest:
|
||||||
|
minFilter, magFilter = driver.FilterNearest, driver.FilterNearest
|
||||||
|
}
|
||||||
|
|
||||||
|
handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA,
|
||||||
|
data.src.Bounds().Dx(), data.src.Bounds().Dy(),
|
||||||
|
minFilter, magFilter,
|
||||||
|
driver.BufferBindingTexture,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -464,15 +523,18 @@ func newRenderer(ctx driver.Device) *renderer {
|
|||||||
if cap := 8192; maxDim > cap {
|
if cap := 8192; maxDim > cap {
|
||||||
maxDim = cap
|
maxDim = cap
|
||||||
}
|
}
|
||||||
|
d := image.Pt(maxDim, maxDim)
|
||||||
|
|
||||||
r.packer.maxDims = image.Pt(maxDim, maxDim)
|
r.packer.maxDims = d
|
||||||
r.intersections.maxDims = image.Pt(maxDim, maxDim)
|
r.intersections.maxDims = d
|
||||||
|
r.layers.maxDims = d
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) release() {
|
func (r *renderer) release() {
|
||||||
r.pather.release()
|
r.pather.release()
|
||||||
r.blitter.release()
|
r.blitter.release()
|
||||||
|
r.layerFBOs.delete(r.ctx, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBlitter(ctx driver.Device) *blitter {
|
func newBlitter(ctx driver.Device) *blitter {
|
||||||
@@ -747,8 +809,7 @@ func (r *renderer) packStencils(pops *[]*pathOp) {
|
|||||||
ops = ops[:len(ops)-1]
|
ops = ops[:len(ops)-1]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()}
|
place, ok := r.packer.add(p.clip.Size())
|
||||||
place, ok := r.packer.add(sz)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
// The clip area is at most the entire screen. Hopefully no
|
// The clip area is at most the entire screen. Hopefully no
|
||||||
// screen is larger than GL_MAX_TEXTURE_SIZE.
|
// screen is larger than GL_MAX_TEXTURE_SIZE.
|
||||||
@@ -760,6 +821,83 @@ func (r *renderer) packStencils(pops *[]*pathOp) {
|
|||||||
*pops = ops
|
*pops = ops
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer {
|
||||||
|
// Make every layer bounds contain nested layers; cull empty layers.
|
||||||
|
for i := len(layers) - 1; i >= 0; i-- {
|
||||||
|
l := layers[i]
|
||||||
|
if l.parent != -1 {
|
||||||
|
b := layers[l.parent].clip
|
||||||
|
layers[l.parent].clip = b.Union(l.clip)
|
||||||
|
}
|
||||||
|
if l.clip.Empty() {
|
||||||
|
layers = append(layers[:i], layers[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Pack layers.
|
||||||
|
r.layers.clear()
|
||||||
|
depth := 0
|
||||||
|
for i := range layers {
|
||||||
|
l := &layers[i]
|
||||||
|
// Only layers of the same depth may be packed together.
|
||||||
|
if l.depth != depth {
|
||||||
|
r.layers.newPage()
|
||||||
|
}
|
||||||
|
place, ok := r.layers.add(l.clip.Size())
|
||||||
|
if !ok {
|
||||||
|
// The layer area is at most the entire screen. Hopefully no
|
||||||
|
// screen is larger than GL_MAX_TEXTURE_SIZE.
|
||||||
|
panic(fmt.Errorf("layer size %v is larger than maximum texture size %v", l.clip.Size(), r.layers.maxDims))
|
||||||
|
}
|
||||||
|
l.place = place
|
||||||
|
}
|
||||||
|
return layers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *renderer) drawLayers(cache *resourceCache, layers []opacityLayer, ops []imageOp) {
|
||||||
|
if len(r.layers.sizes) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fbo := -1
|
||||||
|
r.layerFBOs.resize(r.ctx, driver.TextureFormatSRGBA, r.layers.sizes)
|
||||||
|
for i := len(layers) - 1; i >= 0; i-- {
|
||||||
|
l := layers[i]
|
||||||
|
if fbo != l.place.Idx {
|
||||||
|
if fbo != -1 {
|
||||||
|
r.ctx.EndRenderPass()
|
||||||
|
r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex)
|
||||||
|
}
|
||||||
|
fbo = l.place.Idx
|
||||||
|
f := r.layerFBOs.fbos[fbo]
|
||||||
|
r.ctx.BeginRenderPass(f.tex, driver.LoadDesc{Action: driver.LoadActionClear})
|
||||||
|
}
|
||||||
|
v := image.Rectangle{
|
||||||
|
Min: l.place.Pos,
|
||||||
|
Max: l.place.Pos.Add(l.clip.Size()),
|
||||||
|
}
|
||||||
|
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y)
|
||||||
|
f := r.layerFBOs.fbos[fbo]
|
||||||
|
r.drawOps(cache, true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
|
||||||
|
sr := f32.FRect(v)
|
||||||
|
uvScale, uvOffset := texSpaceTransform(sr, f.size)
|
||||||
|
uvTrans := f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset)
|
||||||
|
// Replace layer ops with one textured op.
|
||||||
|
ops[l.opStart] = imageOp{
|
||||||
|
clip: l.clip,
|
||||||
|
material: material{
|
||||||
|
material: materialTexture,
|
||||||
|
tex: f.tex,
|
||||||
|
uvTrans: uvTrans,
|
||||||
|
opacity: l.opacity,
|
||||||
|
},
|
||||||
|
layerOps: l.opEnd - l.opStart - 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fbo != -1 {
|
||||||
|
r.ctx.EndRenderPass()
|
||||||
|
r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *drawOps) reset(viewport image.Point) {
|
func (d *drawOps) reset(viewport image.Point) {
|
||||||
d.profile = false
|
d.profile = false
|
||||||
d.viewport = viewport
|
d.viewport = viewport
|
||||||
@@ -768,6 +906,8 @@ func (d *drawOps) reset(viewport image.Point) {
|
|||||||
d.pathOpCache = d.pathOpCache[:0]
|
d.pathOpCache = d.pathOpCache[:0]
|
||||||
d.vertCache = d.vertCache[:0]
|
d.vertCache = d.vertCache[:0]
|
||||||
d.transStack = d.transStack[:0]
|
d.transStack = d.transStack[:0]
|
||||||
|
d.layers = d.layers[:0]
|
||||||
|
d.opacityStack = d.opacityStack[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *drawOps) collect(root *op.Ops, viewport image.Point) {
|
func (d *drawOps) collect(root *op.Ops, viewport image.Point) {
|
||||||
@@ -866,6 +1006,27 @@ loop:
|
|||||||
state.t = d.transStack[n-1]
|
state.t = d.transStack[n-1]
|
||||||
d.transStack = d.transStack[:n-1]
|
d.transStack = d.transStack[:n-1]
|
||||||
|
|
||||||
|
case ops.TypePushOpacity:
|
||||||
|
opacity := ops.DecodeOpacity(encOp.Data)
|
||||||
|
parent := -1
|
||||||
|
depth := len(d.opacityStack)
|
||||||
|
if depth > 0 {
|
||||||
|
parent = d.opacityStack[depth-1]
|
||||||
|
}
|
||||||
|
lidx := len(d.layers)
|
||||||
|
d.layers = append(d.layers, opacityLayer{
|
||||||
|
opacity: opacity,
|
||||||
|
parent: parent,
|
||||||
|
depth: depth,
|
||||||
|
opStart: len(d.imageOps),
|
||||||
|
})
|
||||||
|
d.opacityStack = append(d.opacityStack, lidx)
|
||||||
|
case ops.TypePopOpacity:
|
||||||
|
n := len(d.opacityStack)
|
||||||
|
idx := d.opacityStack[n-1]
|
||||||
|
d.layers[idx].opEnd = len(d.imageOps)
|
||||||
|
d.opacityStack = d.opacityStack[:n-1]
|
||||||
|
|
||||||
case ops.TypeStroke:
|
case ops.TypeStroke:
|
||||||
quads.key.strokeWidth = decodeStrokeOp(encOp.Data)
|
quads.key.strokeWidth = decodeStrokeOp(encOp.Data)
|
||||||
|
|
||||||
@@ -958,7 +1119,7 @@ loop:
|
|||||||
mat := state.materialFor(bnd, off, partialTrans, bounds)
|
mat := state.materialFor(bnd, off, partialTrans, bounds)
|
||||||
|
|
||||||
rect := state.cpath == nil || state.cpath.rect
|
rect := state.cpath == nil || state.cpath.rect
|
||||||
if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) {
|
if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) && len(d.opacityStack) == 0 {
|
||||||
// The image is a uniform opaque color and takes up the whole screen.
|
// The image is a uniform opaque color and takes up the whole screen.
|
||||||
// Scrap images up to and including this image and set clear color.
|
// Scrap images up to and including this image and set clear color.
|
||||||
d.imageOps = d.imageOps[:0]
|
d.imageOps = d.imageOps[:0]
|
||||||
@@ -971,6 +1132,15 @@ loop:
|
|||||||
clip: bounds,
|
clip: bounds,
|
||||||
material: mat,
|
material: mat,
|
||||||
}
|
}
|
||||||
|
if n := len(d.opacityStack); n > 0 {
|
||||||
|
idx := d.opacityStack[n-1]
|
||||||
|
lb := d.layers[idx].clip
|
||||||
|
if lb.Empty() {
|
||||||
|
d.layers[idx].clip = img.clip
|
||||||
|
} else {
|
||||||
|
d.layers[idx].clip = lb.Union(img.clip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
d.imageOps = append(d.imageOps, img)
|
d.imageOps = append(d.imageOps, img)
|
||||||
if clipData != nil {
|
if clipData != nil {
|
||||||
@@ -1000,7 +1170,9 @@ func expandPathOp(p *pathOp, clip image.Rectangle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32.Affine2D, clip image.Rectangle) material {
|
func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32.Affine2D, clip image.Rectangle) material {
|
||||||
var m material
|
m := material{
|
||||||
|
opacity: 1.,
|
||||||
|
}
|
||||||
switch d.matType {
|
switch d.matType {
|
||||||
case materialColor:
|
case materialColor:
|
||||||
m.material = materialColor
|
m.material = materialColor
|
||||||
@@ -1040,10 +1212,11 @@ func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) {
|
func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) {
|
||||||
for _, img := range ops {
|
for i := range ops {
|
||||||
|
img := &ops[i]
|
||||||
m := img.material
|
m := img.material
|
||||||
if m.material == materialTexture {
|
if m.material == materialTexture {
|
||||||
r.texHandle(cache, m.data)
|
img.material.tex = r.texHandle(cache, m.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1053,10 +1226,10 @@ func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) {
|
|||||||
m := img.material
|
m := img.material
|
||||||
switch m.material {
|
switch m.material {
|
||||||
case materialTexture:
|
case materialTexture:
|
||||||
r.ctx.PrepareTexture(r.texHandle(cache, m.data))
|
r.ctx.PrepareTexture(m.tex)
|
||||||
}
|
}
|
||||||
|
|
||||||
var fbo stencilFBO
|
var fbo FBO
|
||||||
switch img.clipType {
|
switch img.clipType {
|
||||||
case clipTypeNone:
|
case clipTypeNone:
|
||||||
continue
|
continue
|
||||||
@@ -1069,24 +1242,26 @@ func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
|
func (r *renderer) drawOps(cache *resourceCache, isFBO bool, opOff image.Point, viewport image.Point, ops []imageOp) {
|
||||||
var coverTex driver.Texture
|
var coverTex driver.Texture
|
||||||
for _, img := range ops {
|
for i := 0; i < len(ops); i++ {
|
||||||
|
img := ops[i]
|
||||||
|
i += img.layerOps
|
||||||
m := img.material
|
m := img.material
|
||||||
switch m.material {
|
switch m.material {
|
||||||
case materialTexture:
|
case materialTexture:
|
||||||
r.ctx.BindTexture(0, r.texHandle(cache, m.data))
|
r.ctx.BindTexture(0, m.tex)
|
||||||
}
|
}
|
||||||
drc := img.clip
|
drc := img.clip.Add(opOff)
|
||||||
|
|
||||||
scale, off := clipSpaceTransform(drc, r.blitter.viewport)
|
scale, off := clipSpaceTransform(drc, viewport)
|
||||||
var fbo stencilFBO
|
var fbo FBO
|
||||||
switch img.clipType {
|
switch img.clipType {
|
||||||
case clipTypeNone:
|
case clipTypeNone:
|
||||||
p := r.blitter.pipelines[m.material]
|
p := r.blitter.pipelines[m.material]
|
||||||
r.ctx.BindPipeline(p.pipeline)
|
r.ctx.BindPipeline(p.pipeline)
|
||||||
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
||||||
r.blitter.blit(m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans)
|
r.blitter.blit(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.opacity, m.uvTrans)
|
||||||
continue
|
continue
|
||||||
case clipTypePath:
|
case clipTypePath:
|
||||||
fbo = r.pather.stenciler.cover(img.place.Idx)
|
fbo = r.pather.stenciler.cover(img.place.Idx)
|
||||||
@@ -1105,11 +1280,11 @@ func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
|
|||||||
p := r.pather.coverer.pipelines[m.material]
|
p := r.pather.coverer.pipelines[m.material]
|
||||||
r.ctx.BindPipeline(p.pipeline)
|
r.ctx.BindPipeline(p.pipeline)
|
||||||
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
||||||
r.pather.cover(m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
|
r.pather.cover(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) {
|
func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, opacity float32, uvTrans f32.Affine2D) {
|
||||||
p := b.pipelines[mat]
|
p := b.pipelines[mat]
|
||||||
b.ctx.BindPipeline(p.pipeline)
|
b.ctx.BindPipeline(p.pipeline)
|
||||||
var uniforms *blitUniforms
|
var uniforms *blitUniforms
|
||||||
@@ -1119,18 +1294,23 @@ func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color.
|
|||||||
uniforms = &b.colUniforms.blitUniforms
|
uniforms = &b.colUniforms.blitUniforms
|
||||||
case materialTexture:
|
case materialTexture:
|
||||||
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
||||||
b.texUniforms.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
|
||||||
b.texUniforms.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
|
||||||
uniforms = &b.texUniforms.blitUniforms
|
uniforms = &b.texUniforms.blitUniforms
|
||||||
|
uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
||||||
|
uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
||||||
case materialLinearGradient:
|
case materialLinearGradient:
|
||||||
b.linearGradientUniforms.color1 = col1
|
b.linearGradientUniforms.color1 = col1
|
||||||
b.linearGradientUniforms.color2 = col2
|
b.linearGradientUniforms.color2 = col2
|
||||||
|
|
||||||
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
||||||
b.linearGradientUniforms.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
|
||||||
b.linearGradientUniforms.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
|
||||||
uniforms = &b.linearGradientUniforms.blitUniforms
|
uniforms = &b.linearGradientUniforms.blitUniforms
|
||||||
|
uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
||||||
|
uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
||||||
}
|
}
|
||||||
|
uniforms.fbo = 0
|
||||||
|
if fbo {
|
||||||
|
uniforms.fbo = 1
|
||||||
|
}
|
||||||
|
uniforms.opacity = opacity
|
||||||
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
|
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
|
||||||
p.UploadUniforms(b.ctx)
|
p.UploadUniforms(b.ctx)
|
||||||
b.ctx.DrawArrays(0, 4)
|
b.ctx.DrawArrays(0, 4)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 379 B |
Binary file not shown.
|
After Width: | Height: | Size: 334 B |
@@ -359,6 +359,73 @@ func TestImageRGBA(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImageRGBA_ScaleLinear(t *testing.T) {
|
||||||
|
run(t, func(o *op.Ops) {
|
||||||
|
w := newWindow(t, 128, 128)
|
||||||
|
defer clip.Rect{Max: image.Pt(128, 128)}.Push(o).Pop()
|
||||||
|
op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(64, 64))).Add(o)
|
||||||
|
|
||||||
|
im := image.NewRGBA(image.Rect(0, 0, 2, 2))
|
||||||
|
im.Set(0, 0, colornames.Red)
|
||||||
|
im.Set(1, 0, colornames.Green)
|
||||||
|
im.Set(0, 1, colornames.White)
|
||||||
|
im.Set(1, 1, colornames.Black)
|
||||||
|
|
||||||
|
op := paint.NewImageOp(im)
|
||||||
|
op.Filter = paint.FilterLinear
|
||||||
|
op.Add(o)
|
||||||
|
|
||||||
|
paint.PaintOp{}.Add(o)
|
||||||
|
|
||||||
|
if err := w.Frame(o); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}, func(r result) {
|
||||||
|
r.expect(0, 0, colornames.Red)
|
||||||
|
r.expect(8, 8, colornames.Red)
|
||||||
|
|
||||||
|
// TODO: this currently seems to do srgb scaling
|
||||||
|
// instead of linear rgb scaling,
|
||||||
|
r.expect(64-4, 0, color.RGBA{R: 197, G: 87, B: 0, A: 255})
|
||||||
|
r.expect(64+4, 0, color.RGBA{R: 175, G: 98, B: 0, A: 255})
|
||||||
|
|
||||||
|
r.expect(127, 0, colornames.Green)
|
||||||
|
r.expect(127-8, 8, colornames.Green)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageRGBA_ScaleNearest(t *testing.T) {
|
||||||
|
run(t, func(o *op.Ops) {
|
||||||
|
w := newWindow(t, 128, 128)
|
||||||
|
op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(64, 64))).Add(o)
|
||||||
|
|
||||||
|
im := image.NewRGBA(image.Rect(0, 0, 2, 2))
|
||||||
|
im.Set(0, 0, colornames.Red)
|
||||||
|
im.Set(1, 0, colornames.Green)
|
||||||
|
im.Set(0, 1, colornames.White)
|
||||||
|
im.Set(1, 1, colornames.Black)
|
||||||
|
|
||||||
|
op := paint.NewImageOp(im)
|
||||||
|
op.Filter = paint.FilterNearest
|
||||||
|
op.Add(o)
|
||||||
|
|
||||||
|
paint.PaintOp{}.Add(o)
|
||||||
|
|
||||||
|
if err := w.Frame(o); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}, func(r result) {
|
||||||
|
r.expect(0, 0, colornames.Red)
|
||||||
|
r.expect(8, 8, colornames.Red)
|
||||||
|
|
||||||
|
r.expect(64-4, 0, colornames.Red)
|
||||||
|
r.expect(64+4, 0, colornames.Green)
|
||||||
|
|
||||||
|
r.expect(127, 0, colornames.Green)
|
||||||
|
r.expect(127-8, 8, colornames.Green)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestGapsInPath(t *testing.T) {
|
func TestGapsInPath(t *testing.T) {
|
||||||
ops := new(op.Ops)
|
ops := new(op.Ops)
|
||||||
var p clip.Path
|
var p clip.Path
|
||||||
@@ -413,6 +480,22 @@ func TestGapsInPath(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpacity(t *testing.T) {
|
||||||
|
run(t, func(ops *op.Ops) {
|
||||||
|
opc1 := paint.PushOpacity(ops, .3)
|
||||||
|
// Fill screen to exercize the glClear optimization.
|
||||||
|
paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, clip.Rect{Max: image.Pt(1024, 1024)}.Op())
|
||||||
|
opc2 := paint.PushOpacity(ops, .6)
|
||||||
|
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(20, 10), Max: image.Pt(64, 128)}.Op())
|
||||||
|
opc2.Pop()
|
||||||
|
opc1.Pop()
|
||||||
|
opc3 := paint.PushOpacity(ops, .6)
|
||||||
|
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(50+20, 10), Max: image.Pt(50+64, 128)}.Op())
|
||||||
|
opc3.Pop()
|
||||||
|
}, func(r result) {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// lerp calculates linear interpolation with color b and p.
|
// lerp calculates linear interpolation with color b and p.
|
||||||
func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
|
func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
|
||||||
return f32color.RGBA{
|
return f32color.RGBA{
|
||||||
|
|||||||
+16
-12
@@ -58,7 +58,7 @@ type coverUniforms struct {
|
|||||||
uvCoverTransform [4]float32
|
uvCoverTransform [4]float32
|
||||||
uvTransformR1 [4]float32
|
uvTransformR1 [4]float32
|
||||||
uvTransformR2 [4]float32
|
uvTransformR2 [4]float32
|
||||||
_ float32
|
fbo float32
|
||||||
}
|
}
|
||||||
|
|
||||||
type stenciler struct {
|
type stenciler struct {
|
||||||
@@ -90,10 +90,10 @@ type intersectUniforms struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type fboSet struct {
|
type fboSet struct {
|
||||||
fbos []stencilFBO
|
fbos []FBO
|
||||||
}
|
}
|
||||||
|
|
||||||
type stencilFBO struct {
|
type FBO struct {
|
||||||
size image.Point
|
size image.Point
|
||||||
tex driver.Texture
|
tex driver.Texture
|
||||||
}
|
}
|
||||||
@@ -247,10 +247,10 @@ func newStenciler(ctx driver.Device) *stenciler {
|
|||||||
return st
|
return st
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) {
|
func (s *fboSet) resize(ctx driver.Device, format driver.TextureFormat, sizes []image.Point) {
|
||||||
// Add fbos.
|
// Add fbos.
|
||||||
for i := len(s.fbos); i < len(sizes); i++ {
|
for i := len(s.fbos); i < len(sizes); i++ {
|
||||||
s.fbos = append(s.fbos, stencilFBO{})
|
s.fbos = append(s.fbos, FBO{})
|
||||||
}
|
}
|
||||||
// Resize fbos.
|
// Resize fbos.
|
||||||
for i, sz := range sizes {
|
for i, sz := range sizes {
|
||||||
@@ -273,7 +273,7 @@ func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) {
|
|||||||
if sz.X > max {
|
if sz.X > max {
|
||||||
sz.X = max
|
sz.X = max
|
||||||
}
|
}
|
||||||
tex, err := ctx.NewTexture(driver.TextureFormatFloat, sz.X, sz.Y, driver.FilterNearest, driver.FilterNearest,
|
tex, err := ctx.NewTexture(format, sz.X, sz.Y, driver.FilterNearest, driver.FilterNearest,
|
||||||
driver.BufferBindingTexture|driver.BufferBindingFramebuffer)
|
driver.BufferBindingTexture|driver.BufferBindingFramebuffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -340,15 +340,15 @@ func (s *stenciler) beginIntersect(sizes []image.Point) {
|
|||||||
// 8 bit coverage is enough, but OpenGL ES only supports single channel
|
// 8 bit coverage is enough, but OpenGL ES only supports single channel
|
||||||
// floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if
|
// floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if
|
||||||
// no floating point support is available.
|
// no floating point support is available.
|
||||||
s.intersections.resize(s.ctx, sizes)
|
s.intersections.resize(s.ctx, driver.TextureFormatFloat, sizes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stenciler) cover(idx int) stencilFBO {
|
func (s *stenciler) cover(idx int) FBO {
|
||||||
return s.fbos.fbos[idx]
|
return s.fbos.fbos[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stenciler) begin(sizes []image.Point) {
|
func (s *stenciler) begin(sizes []image.Point) {
|
||||||
s.fbos.resize(s.ctx, sizes)
|
s.fbos.resize(s.ctx, driver.TextureFormatFloat, sizes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) {
|
func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) {
|
||||||
@@ -375,11 +375,11 @@ func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv ima
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pather) cover(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
|
func (p *pather) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
|
||||||
p.coverer.cover(mat, col, col1, col2, scale, off, uvTrans, coverScale, coverOff)
|
p.coverer.cover(mat, isFBO, col, col1, col2, scale, off, uvTrans, coverScale, coverOff)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *coverer) cover(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
|
func (c *coverer) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
|
||||||
var uniforms *coverUniforms
|
var uniforms *coverUniforms
|
||||||
switch mat {
|
switch mat {
|
||||||
case materialColor:
|
case materialColor:
|
||||||
@@ -399,6 +399,10 @@ func (c *coverer) cover(mat materialType, col f32color.RGBA, col1, col2 f32color
|
|||||||
c.texUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
c.texUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
||||||
uniforms = &c.texUniforms.coverUniforms
|
uniforms = &c.texUniforms.coverUniforms
|
||||||
}
|
}
|
||||||
|
uniforms.fbo = 0
|
||||||
|
if isFBO {
|
||||||
|
uniforms.fbo = 1
|
||||||
|
}
|
||||||
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
|
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
|
||||||
uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
|
uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
|
||||||
c.pipelines[mat].UploadUniforms(c.ctx)
|
c.pipelines[mat].UploadUniforms(c.ctx)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ func (c *Context) Release() {
|
|||||||
eglDestroyContext(c.disp, c.eglCtx.ctx)
|
eglDestroyContext(c.disp, c.eglCtx.ctx)
|
||||||
c.eglCtx = nil
|
c.eglCtx = nil
|
||||||
}
|
}
|
||||||
|
eglTerminate(c.disp)
|
||||||
c.disp = nilEGLDisplay
|
c.disp = nilEGLDisplay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+69
-26
@@ -14,14 +14,25 @@ import (
|
|||||||
|
|
||||||
type Ops struct {
|
type Ops struct {
|
||||||
// version is incremented at each Reset.
|
// version is incremented at each Reset.
|
||||||
version int
|
version uint32
|
||||||
// data contains the serialized operations.
|
// data contains the serialized operations.
|
||||||
data []byte
|
data []byte
|
||||||
// refs hold external references for operations.
|
// refs hold external references for operations.
|
||||||
refs []interface{}
|
refs []interface{}
|
||||||
|
// stringRefs provides space for string references, pointers to which will
|
||||||
|
// be stored in refs. Storing a string directly in refs would cause a heap
|
||||||
|
// allocation, to store the string header in an interface value. The backing
|
||||||
|
// array of stringRefs, on the other hand, gets reused between calls to
|
||||||
|
// reset, making string references free on average.
|
||||||
|
//
|
||||||
|
// Appending to stringRefs might reallocate the backing array, which will
|
||||||
|
// leave pointers to the old array in refs. This temporarily causes a slight
|
||||||
|
// increase in memory usage, but this, too, amortizes away as the capacity
|
||||||
|
// of stringRefs approaches its stable maximum.
|
||||||
|
stringRefs []string
|
||||||
// nextStateID is the id allocated for the next
|
// nextStateID is the id allocated for the next
|
||||||
// StateOp.
|
// StateOp.
|
||||||
nextStateID int
|
nextStateID uint32
|
||||||
// multipOp indicates a multi-op such as clip.Path is being added.
|
// multipOp indicates a multi-op such as clip.Path is being added.
|
||||||
multipOp bool
|
multipOp bool
|
||||||
|
|
||||||
@@ -40,9 +51,10 @@ const (
|
|||||||
TypeMacro OpType = iota + firstOpIndex
|
TypeMacro OpType = iota + firstOpIndex
|
||||||
TypeCall
|
TypeCall
|
||||||
TypeDefer
|
TypeDefer
|
||||||
TypePushTransform
|
|
||||||
TypeTransform
|
TypeTransform
|
||||||
TypePopTransform
|
TypePopTransform
|
||||||
|
TypePushOpacity
|
||||||
|
TypePopOpacity
|
||||||
TypeInvalidate
|
TypeInvalidate
|
||||||
TypeImage
|
TypeImage
|
||||||
TypePaint
|
TypePaint
|
||||||
@@ -72,30 +84,30 @@ const (
|
|||||||
TypeSemanticDesc
|
TypeSemanticDesc
|
||||||
TypeSemanticClass
|
TypeSemanticClass
|
||||||
TypeSemanticSelected
|
TypeSemanticSelected
|
||||||
TypeSemanticDisabled
|
TypeSemanticEnabled
|
||||||
TypeSnippet
|
TypeSnippet
|
||||||
TypeSelection
|
TypeSelection
|
||||||
TypeActionInput
|
TypeActionInput
|
||||||
)
|
)
|
||||||
|
|
||||||
type StackID struct {
|
type StackID struct {
|
||||||
id int
|
id uint32
|
||||||
prev int
|
prev uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateOp represents a saved operation snapshot to be restored
|
// StateOp represents a saved operation snapshot to be restored
|
||||||
// later.
|
// later.
|
||||||
type StateOp struct {
|
type StateOp struct {
|
||||||
id int
|
id uint32
|
||||||
macroID int
|
macroID uint32
|
||||||
ops *Ops
|
ops *Ops
|
||||||
}
|
}
|
||||||
|
|
||||||
// stack tracks the integer identities of stack operations to ensure correct
|
// stack tracks the integer identities of stack operations to ensure correct
|
||||||
// pairing of their push and pop methods.
|
// pairing of their push and pop methods.
|
||||||
type stack struct {
|
type stack struct {
|
||||||
currentID int
|
currentID uint32
|
||||||
nextID int
|
nextID uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type StackKind uint8
|
type StackKind uint8
|
||||||
@@ -111,6 +123,7 @@ const (
|
|||||||
ClipStack StackKind = iota
|
ClipStack StackKind = iota
|
||||||
TransStack
|
TransStack
|
||||||
PassStack
|
PassStack
|
||||||
|
OpacityStack
|
||||||
_StackKind
|
_StackKind
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,11 +137,12 @@ const (
|
|||||||
TypeMacroLen = 1 + 4 + 4
|
TypeMacroLen = 1 + 4 + 4
|
||||||
TypeCallLen = 1 + 4 + 4 + 4 + 4
|
TypeCallLen = 1 + 4 + 4 + 4 + 4
|
||||||
TypeDeferLen = 1
|
TypeDeferLen = 1
|
||||||
TypePushTransformLen = 1 + 4*6
|
|
||||||
TypeTransformLen = 1 + 1 + 4*6
|
TypeTransformLen = 1 + 1 + 4*6
|
||||||
TypePopTransformLen = 1
|
TypePopTransformLen = 1
|
||||||
|
TypePushOpacityLen = 1 + 4
|
||||||
|
TypePopOpacityLen = 1
|
||||||
TypeRedrawLen = 1 + 8
|
TypeRedrawLen = 1 + 8
|
||||||
TypeImageLen = 1
|
TypeImageLen = 1 + 1
|
||||||
TypePaintLen = 1
|
TypePaintLen = 1
|
||||||
TypeColorLen = 1 + 4
|
TypeColorLen = 1 + 4
|
||||||
TypeLinearGradientLen = 1 + 8*2 + 4*2
|
TypeLinearGradientLen = 1 + 8*2 + 4*2
|
||||||
@@ -156,7 +170,7 @@ const (
|
|||||||
TypeSemanticDescLen = 1
|
TypeSemanticDescLen = 1
|
||||||
TypeSemanticClassLen = 2
|
TypeSemanticClassLen = 2
|
||||||
TypeSemanticSelectedLen = 2
|
TypeSemanticSelectedLen = 2
|
||||||
TypeSemanticDisabledLen = 2
|
TypeSemanticEnabledLen = 2
|
||||||
TypeSnippetLen = 1 + 4 + 4
|
TypeSnippetLen = 1 + 4 + 4
|
||||||
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
|
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
|
||||||
TypeActionInputLen = 1 + 1
|
TypeActionInputLen = 1 + 1
|
||||||
@@ -183,8 +197,12 @@ func Reset(o *Ops) {
|
|||||||
for i := range o.refs {
|
for i := range o.refs {
|
||||||
o.refs[i] = nil
|
o.refs[i] = nil
|
||||||
}
|
}
|
||||||
|
for i := range o.stringRefs {
|
||||||
|
o.stringRefs[i] = ""
|
||||||
|
}
|
||||||
o.data = o.data[:0]
|
o.data = o.data[:0]
|
||||||
o.refs = o.refs[:0]
|
o.refs = o.refs[:0]
|
||||||
|
o.stringRefs = o.stringRefs[:0]
|
||||||
o.nextStateID = 0
|
o.nextStateID = 0
|
||||||
o.version++
|
o.version++
|
||||||
}
|
}
|
||||||
@@ -248,11 +266,11 @@ func AddCall(o *Ops, callOps *Ops, pc PC, end PC) {
|
|||||||
bo.PutUint32(data[13:], uint32(end.refs))
|
bo.PutUint32(data[13:], uint32(end.refs))
|
||||||
}
|
}
|
||||||
|
|
||||||
func PushOp(o *Ops, kind StackKind) (StackID, int) {
|
func PushOp(o *Ops, kind StackKind) (StackID, uint32) {
|
||||||
return o.stacks[kind].push(), o.macroStack.currentID
|
return o.stacks[kind].push(), o.macroStack.currentID
|
||||||
}
|
}
|
||||||
|
|
||||||
func PopOp(o *Ops, kind StackKind, sid StackID, macroID int) {
|
func PopOp(o *Ops, kind StackKind, sid StackID, macroID uint32) {
|
||||||
if o.macroStack.currentID != macroID {
|
if o.macroStack.currentID != macroID {
|
||||||
panic("stack push and pop must not cross macro boundary")
|
panic("stack push and pop must not cross macro boundary")
|
||||||
}
|
}
|
||||||
@@ -265,12 +283,26 @@ func Write1(o *Ops, n int, ref1 interface{}) []byte {
|
|||||||
return o.data[len(o.data)-n:]
|
return o.data[len(o.data)-n:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Write1String(o *Ops, n int, ref1 string) []byte {
|
||||||
|
o.data = append(o.data, make([]byte, n)...)
|
||||||
|
o.stringRefs = append(o.stringRefs, ref1)
|
||||||
|
o.refs = append(o.refs, &o.stringRefs[len(o.stringRefs)-1])
|
||||||
|
return o.data[len(o.data)-n:]
|
||||||
|
}
|
||||||
|
|
||||||
func Write2(o *Ops, n int, ref1, ref2 interface{}) []byte {
|
func Write2(o *Ops, n int, ref1, ref2 interface{}) []byte {
|
||||||
o.data = append(o.data, make([]byte, n)...)
|
o.data = append(o.data, make([]byte, n)...)
|
||||||
o.refs = append(o.refs, ref1, ref2)
|
o.refs = append(o.refs, ref1, ref2)
|
||||||
return o.data[len(o.data)-n:]
|
return o.data[len(o.data)-n:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Write2String(o *Ops, n int, ref1 interface{}, ref2 string) []byte {
|
||||||
|
o.data = append(o.data, make([]byte, n)...)
|
||||||
|
o.stringRefs = append(o.stringRefs, ref2)
|
||||||
|
o.refs = append(o.refs, ref1, &o.stringRefs[len(o.stringRefs)-1])
|
||||||
|
return o.data[len(o.data)-n:]
|
||||||
|
}
|
||||||
|
|
||||||
func Write3(o *Ops, n int, ref1, ref2, ref3 interface{}) []byte {
|
func Write3(o *Ops, n int, ref1, ref2, ref3 interface{}) []byte {
|
||||||
o.data = append(o.data, make([]byte, n)...)
|
o.data = append(o.data, make([]byte, n)...)
|
||||||
o.refs = append(o.refs, ref1, ref2, ref3)
|
o.refs = append(o.refs, ref1, ref2, ref3)
|
||||||
@@ -278,7 +310,7 @@ func Write3(o *Ops, n int, ref1, ref2, ref3 interface{}) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func PCFor(o *Ops) PC {
|
func PCFor(o *Ops) PC {
|
||||||
return PC{data: len(o.data), refs: len(o.refs)}
|
return PC{data: uint32(len(o.data)), refs: uint32(len(o.refs))}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stack) push() StackID {
|
func (s *stack) push() StackID {
|
||||||
@@ -354,6 +386,14 @@ func DecodeTransform(data []byte) (t f32.Affine2D, push bool) {
|
|||||||
return f32.NewAffine2D(a, b, c, d, e, f), push
|
return f32.NewAffine2D(a, b, c, d, e, f), push
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DecodeOpacity(data []byte) float32 {
|
||||||
|
if OpType(data[0]) != TypePushOpacity {
|
||||||
|
panic("invalid op")
|
||||||
|
}
|
||||||
|
bo := binary.LittleEndian
|
||||||
|
return math.Float32frombits(bo.Uint32(data[1:]))
|
||||||
|
}
|
||||||
|
|
||||||
// DecodeSave decodes the state id of a save op.
|
// DecodeSave decodes the state id of a save op.
|
||||||
func DecodeSave(data []byte) int {
|
func DecodeSave(data []byte) int {
|
||||||
if OpType(data[0]) != TypeSave {
|
if OpType(data[0]) != TypeSave {
|
||||||
@@ -381,9 +421,10 @@ var opProps = [0x100]opProp{
|
|||||||
TypeMacro: {Size: TypeMacroLen, NumRefs: 0},
|
TypeMacro: {Size: TypeMacroLen, NumRefs: 0},
|
||||||
TypeCall: {Size: TypeCallLen, NumRefs: 1},
|
TypeCall: {Size: TypeCallLen, NumRefs: 1},
|
||||||
TypeDefer: {Size: TypeDeferLen, NumRefs: 0},
|
TypeDefer: {Size: TypeDeferLen, NumRefs: 0},
|
||||||
TypePushTransform: {Size: TypePushTransformLen, NumRefs: 0},
|
|
||||||
TypeTransform: {Size: TypeTransformLen, NumRefs: 0},
|
TypeTransform: {Size: TypeTransformLen, NumRefs: 0},
|
||||||
TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0},
|
TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0},
|
||||||
|
TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0},
|
||||||
|
TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0},
|
||||||
TypeInvalidate: {Size: TypeRedrawLen, NumRefs: 0},
|
TypeInvalidate: {Size: TypeRedrawLen, NumRefs: 0},
|
||||||
TypeImage: {Size: TypeImageLen, NumRefs: 2},
|
TypeImage: {Size: TypeImageLen, NumRefs: 2},
|
||||||
TypePaint: {Size: TypePaintLen, NumRefs: 0},
|
TypePaint: {Size: TypePaintLen, NumRefs: 0},
|
||||||
@@ -413,23 +454,23 @@ var opProps = [0x100]opProp{
|
|||||||
TypeSemanticDesc: {Size: TypeSemanticDescLen, NumRefs: 1},
|
TypeSemanticDesc: {Size: TypeSemanticDescLen, NumRefs: 1},
|
||||||
TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0},
|
TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0},
|
||||||
TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0},
|
TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0},
|
||||||
TypeSemanticDisabled: {Size: TypeSemanticDisabledLen, NumRefs: 0},
|
TypeSemanticEnabled: {Size: TypeSemanticEnabledLen, NumRefs: 0},
|
||||||
TypeSnippet: {Size: TypeSnippetLen, NumRefs: 2},
|
TypeSnippet: {Size: TypeSnippetLen, NumRefs: 2},
|
||||||
TypeSelection: {Size: TypeSelectionLen, NumRefs: 1},
|
TypeSelection: {Size: TypeSelectionLen, NumRefs: 1},
|
||||||
TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0},
|
TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t OpType) props() (size, numRefs int) {
|
func (t OpType) props() (size, numRefs uint32) {
|
||||||
v := opProps[t]
|
v := opProps[t]
|
||||||
return int(v.Size), int(v.NumRefs)
|
return uint32(v.Size), uint32(v.NumRefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t OpType) Size() int {
|
func (t OpType) Size() uint32 {
|
||||||
return int(opProps[t].Size)
|
return uint32(opProps[t].Size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t OpType) NumRefs() int {
|
func (t OpType) NumRefs() uint32 {
|
||||||
return int(opProps[t].NumRefs)
|
return uint32(opProps[t].NumRefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t OpType) String() string {
|
func (t OpType) String() string {
|
||||||
@@ -440,12 +481,14 @@ func (t OpType) String() string {
|
|||||||
return "Call"
|
return "Call"
|
||||||
case TypeDefer:
|
case TypeDefer:
|
||||||
return "Defer"
|
return "Defer"
|
||||||
case TypePushTransform:
|
|
||||||
return "PushTransform"
|
|
||||||
case TypeTransform:
|
case TypeTransform:
|
||||||
return "Transform"
|
return "Transform"
|
||||||
case TypePopTransform:
|
case TypePopTransform:
|
||||||
return "PopTransform"
|
return "PopTransform"
|
||||||
|
case TypePushOpacity:
|
||||||
|
return "PushOpacity"
|
||||||
|
case TypePopOpacity:
|
||||||
|
return "PopOpacity"
|
||||||
case TypeInvalidate:
|
case TypeInvalidate:
|
||||||
return "Invalidate"
|
return "Invalidate"
|
||||||
case TypeImage:
|
case TypeImage:
|
||||||
|
|||||||
+13
-13
@@ -26,8 +26,8 @@ type EncodedOp struct {
|
|||||||
// Key is a unique key for a given op.
|
// Key is a unique key for a given op.
|
||||||
type Key struct {
|
type Key struct {
|
||||||
ops *Ops
|
ops *Ops
|
||||||
pc int
|
pc uint32
|
||||||
version int
|
version uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shadow of op.MacroOp.
|
// Shadow of op.MacroOp.
|
||||||
@@ -39,8 +39,8 @@ type macroOp struct {
|
|||||||
|
|
||||||
// PC is an instruction counter for an operation list.
|
// PC is an instruction counter for an operation list.
|
||||||
type PC struct {
|
type PC struct {
|
||||||
data int
|
data uint32
|
||||||
refs int
|
refs uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type macro struct {
|
type macro struct {
|
||||||
@@ -128,7 +128,7 @@ func (r *Reader) Decode() (EncodedOp, bool) {
|
|||||||
if nrefs != 1 {
|
if nrefs != 1 {
|
||||||
panic("internal error: unexpected number of macro refs")
|
panic("internal error: unexpected number of macro refs")
|
||||||
}
|
}
|
||||||
deferData := Write1(&r.deferOps, n, refs[0])
|
deferData := Write1(&r.deferOps, int(n), refs[0])
|
||||||
copy(deferData, data)
|
copy(deferData, data)
|
||||||
r.pc.data += n
|
r.pc.data += n
|
||||||
r.pc.refs += nrefs
|
r.pc.refs += nrefs
|
||||||
@@ -154,8 +154,8 @@ func (r *Reader) Decode() (EncodedOp, bool) {
|
|||||||
r.pc = op.endpc
|
r.pc = op.endpc
|
||||||
} else {
|
} else {
|
||||||
// Treat an incomplete macro as containing all remaining ops.
|
// Treat an incomplete macro as containing all remaining ops.
|
||||||
r.pc.data = len(r.ops.data)
|
r.pc.data = uint32(len(r.ops.data))
|
||||||
r.pc.refs = len(r.ops.refs)
|
r.pc.refs = uint32(len(r.ops.refs))
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -171,8 +171,8 @@ func (op *opMacroDef) decode(data []byte) {
|
|||||||
}
|
}
|
||||||
bo := binary.LittleEndian
|
bo := binary.LittleEndian
|
||||||
data = data[:TypeMacroLen]
|
data = data[:TypeMacroLen]
|
||||||
op.endpc.data = int(int32(bo.Uint32(data[1:])))
|
op.endpc.data = bo.Uint32(data[1:])
|
||||||
op.endpc.refs = int(int32(bo.Uint32(data[5:])))
|
op.endpc.refs = bo.Uint32(data[5:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *macroOp) decode(data []byte, refs []interface{}) {
|
func (m *macroOp) decode(data []byte, refs []interface{}) {
|
||||||
@@ -183,8 +183,8 @@ func (m *macroOp) decode(data []byte, refs []interface{}) {
|
|||||||
data = data[:TypeCallLen]
|
data = data[:TypeCallLen]
|
||||||
|
|
||||||
m.ops = refs[0].(*Ops)
|
m.ops = refs[0].(*Ops)
|
||||||
m.start.data = int(int32(bo.Uint32(data[1:])))
|
m.start.data = bo.Uint32(data[1:])
|
||||||
m.start.refs = int(int32(bo.Uint32(data[5:])))
|
m.start.refs = bo.Uint32(data[5:])
|
||||||
m.end.data = int(int32(bo.Uint32(data[9:])))
|
m.end.data = bo.Uint32(data[9:])
|
||||||
m.end.refs = int(int32(bo.Uint32(data[13:])))
|
m.end.refs = bo.Uint32(data[13:])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func (h ReadOp) Add(o *op.Ops) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h WriteOp) Add(o *op.Ops) {
|
func (h WriteOp) Add(o *op.Ops) {
|
||||||
data := ops.Write1(&o.Internal, ops.TypeClipboardWriteLen, &h.Text)
|
data := ops.Write1String(&o.Internal, ops.TypeClipboardWriteLen, h.Text)
|
||||||
data[0] = byte(ops.TypeClipboardWrite)
|
data[0] = byte(ops.TypeClipboardWrite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -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.
|
||||||
@@ -321,8 +323,7 @@ func (h InputOp) Add(o *op.Ops) {
|
|||||||
if h.Tag == nil {
|
if h.Tag == nil {
|
||||||
panic("Tag must be non-nil")
|
panic("Tag must be non-nil")
|
||||||
}
|
}
|
||||||
filter := h.Keys
|
data := ops.Write2String(&o.Internal, ops.TypeKeyInputLen, h.Tag, string(h.Keys))
|
||||||
data := ops.Write2(&o.Internal, ops.TypeKeyInputLen, h.Tag, &filter)
|
|
||||||
data[0] = byte(ops.TypeKeyInput)
|
data[0] = byte(ops.TypeKeyInput)
|
||||||
data[1] = byte(h.Hint)
|
data[1] = byte(h.Hint)
|
||||||
}
|
}
|
||||||
@@ -341,7 +342,7 @@ func (h FocusOp) Add(o *op.Ops) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s SnippetOp) Add(o *op.Ops) {
|
func (s SnippetOp) Add(o *op.Ops) {
|
||||||
data := ops.Write2(&o.Internal, ops.TypeSnippetLen, s.Tag, &s.Text)
|
data := ops.Write2String(&o.Internal, ops.TypeSnippetLen, s.Tag, s.Text)
|
||||||
data[0] = byte(ops.TypeSnippet)
|
data[0] = byte(ops.TypeSnippet)
|
||||||
bo := binary.LittleEndian
|
bo := binary.LittleEndian
|
||||||
bo.PutUint32(data[1:], uint32(s.Range.Start))
|
bo.PutUint32(data[1:], uint32(s.Range.Start))
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,7 @@ object such as a finger.
|
|||||||
The InputOp operation is used to declare a handler ready for pointer
|
The InputOp operation is used to declare a handler ready for pointer
|
||||||
events. Use an event.Queue to receive events.
|
events. Use an event.Queue to receive events.
|
||||||
|
|
||||||
# Types
|
# Kinds
|
||||||
|
|
||||||
Only events that match a specified list of types are delivered to a handler.
|
Only events that match a specified list of types are delivered to a handler.
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ Leave, or Scroll):
|
|||||||
|
|
||||||
pointer.InputOp{
|
pointer.InputOp{
|
||||||
Tag: h,
|
Tag: h,
|
||||||
Types: pointer.Press | pointer.Drag | pointer.Release,
|
Kinds: pointer.Press | pointer.Drag | pointer.Release,
|
||||||
}.Add(ops)
|
}.Add(ops)
|
||||||
|
|
||||||
Cancel events are always delivered.
|
Cancel events are always delivered.
|
||||||
|
|||||||
+16
-14
@@ -18,7 +18,7 @@ import (
|
|||||||
|
|
||||||
// Event is a pointer event.
|
// Event is a pointer event.
|
||||||
type Event struct {
|
type Event struct {
|
||||||
Type Type
|
Kind Kind
|
||||||
Source Source
|
Source Source
|
||||||
// PointerID is the id for the pointer and can be used
|
// PointerID is the id for the pointer and can be used
|
||||||
// to track a particular pointer from Press to
|
// to track a particular pointer from Press to
|
||||||
@@ -32,8 +32,10 @@ type Event struct {
|
|||||||
Time time.Duration
|
Time time.Duration
|
||||||
// Buttons are the set of pressed mouse buttons for this event.
|
// Buttons are the set of pressed mouse buttons for this event.
|
||||||
Buttons Buttons
|
Buttons Buttons
|
||||||
// Position is the position of the event, relative to
|
// Position is the coordinates of the event in the local coordinate
|
||||||
// the current transformation, as set by op.TransformOp.
|
// system of the receiving tag. The transformation from global window
|
||||||
|
// coordinates to local coordinates is performed by the inverse of
|
||||||
|
// the effective transformation of the tag.
|
||||||
Position f32.Point
|
Position f32.Point
|
||||||
// Scroll is the scroll amount, if any.
|
// Scroll is the scroll amount, if any.
|
||||||
Scroll f32.Point
|
Scroll f32.Point
|
||||||
@@ -51,7 +53,7 @@ type PassOp struct {
|
|||||||
type PassStack struct {
|
type PassStack struct {
|
||||||
ops *ops.Ops
|
ops *ops.Ops
|
||||||
id ops.StackID
|
id ops.StackID
|
||||||
macroID int
|
macroID uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// InputOp declares an input handler ready for pointer
|
// InputOp declares an input handler ready for pointer
|
||||||
@@ -61,8 +63,8 @@ type InputOp struct {
|
|||||||
// Grab, if set, request that the handler get
|
// Grab, if set, request that the handler get
|
||||||
// Grabbed priority.
|
// Grabbed priority.
|
||||||
Grab bool
|
Grab bool
|
||||||
// Types is a bitwise-or of event types to receive.
|
// Kinds is a bitwise-or of event types to receive.
|
||||||
Types Type
|
Kinds Kind
|
||||||
// ScrollBounds describe the maximum scrollable distances in both
|
// ScrollBounds describe the maximum scrollable distances in both
|
||||||
// axes. Specifically, any Event e delivered to Tag will satisfy
|
// axes. Specifically, any Event e delivered to Tag will satisfy
|
||||||
//
|
//
|
||||||
@@ -73,8 +75,8 @@ type InputOp struct {
|
|||||||
|
|
||||||
type ID uint16
|
type ID uint16
|
||||||
|
|
||||||
// Type of an Event.
|
// Kind of an Event.
|
||||||
type Type uint
|
type Kind uint
|
||||||
|
|
||||||
// Priority of an Event.
|
// Priority of an Event.
|
||||||
type Priority uint8
|
type Priority uint8
|
||||||
@@ -169,7 +171,7 @@ const (
|
|||||||
const (
|
const (
|
||||||
// A Cancel event is generated when the current gesture is
|
// A Cancel event is generated when the current gesture is
|
||||||
// interrupted by other handlers or the system.
|
// interrupted by other handlers or the system.
|
||||||
Cancel Type = (1 << iota) >> 1
|
Cancel Kind = (1 << iota) >> 1
|
||||||
// Press of a pointer.
|
// Press of a pointer.
|
||||||
Press
|
Press
|
||||||
// Release of a pointer.
|
// Release of a pointer.
|
||||||
@@ -243,7 +245,7 @@ func (op InputOp) Add(o *op.Ops) {
|
|||||||
if b := op.ScrollBounds; b.Min.X > 0 || b.Max.X < 0 || b.Min.Y > 0 || b.Max.Y < 0 {
|
if b := op.ScrollBounds; b.Min.X > 0 || b.Max.X < 0 || b.Min.Y > 0 || b.Max.Y < 0 {
|
||||||
panic(fmt.Errorf("invalid scroll range value %v", b))
|
panic(fmt.Errorf("invalid scroll range value %v", b))
|
||||||
}
|
}
|
||||||
if op.Types>>16 > 0 {
|
if op.Kinds>>16 > 0 {
|
||||||
panic(fmt.Errorf("value in Types overflows uint16"))
|
panic(fmt.Errorf("value in Types overflows uint16"))
|
||||||
}
|
}
|
||||||
data := ops.Write1(&o.Internal, ops.TypePointerInputLen, op.Tag)
|
data := ops.Write1(&o.Internal, ops.TypePointerInputLen, op.Tag)
|
||||||
@@ -252,19 +254,19 @@ func (op InputOp) Add(o *op.Ops) {
|
|||||||
data[1] = 1
|
data[1] = 1
|
||||||
}
|
}
|
||||||
bo := binary.LittleEndian
|
bo := binary.LittleEndian
|
||||||
bo.PutUint16(data[2:], uint16(op.Types))
|
bo.PutUint16(data[2:], uint16(op.Kinds))
|
||||||
bo.PutUint32(data[4:], uint32(op.ScrollBounds.Min.X))
|
bo.PutUint32(data[4:], uint32(op.ScrollBounds.Min.X))
|
||||||
bo.PutUint32(data[8:], uint32(op.ScrollBounds.Min.Y))
|
bo.PutUint32(data[8:], uint32(op.ScrollBounds.Min.Y))
|
||||||
bo.PutUint32(data[12:], uint32(op.ScrollBounds.Max.X))
|
bo.PutUint32(data[12:], uint32(op.ScrollBounds.Max.X))
|
||||||
bo.PutUint32(data[16:], uint32(op.ScrollBounds.Max.Y))
|
bo.PutUint32(data[16:], uint32(op.ScrollBounds.Max.Y))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Type) String() string {
|
func (t Kind) String() string {
|
||||||
if t == Cancel {
|
if t == Cancel {
|
||||||
return "Cancel"
|
return "Cancel"
|
||||||
}
|
}
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
for tt := Type(1); tt > 0; tt <<= 1 {
|
for tt := Kind(1); tt > 0; tt <<= 1 {
|
||||||
if t&tt > 0 {
|
if t&tt > 0 {
|
||||||
if buf.Len() > 0 {
|
if buf.Len() > 0 {
|
||||||
buf.WriteByte('|')
|
buf.WriteByte('|')
|
||||||
@@ -275,7 +277,7 @@ func (t Type) String() string {
|
|||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Type) string() string {
|
func (t Kind) string() string {
|
||||||
switch t {
|
switch t {
|
||||||
case Press:
|
case Press:
|
||||||
return "Press"
|
return "Press"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
func TestTypeString(t *testing.T) {
|
func TestTypeString(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
typ Type
|
typ Kind
|
||||||
res string
|
res string
|
||||||
}{
|
}{
|
||||||
{Cancel, "Cancel"},
|
{Cancel, "Cancel"},
|
||||||
|
|||||||
@@ -30,11 +30,6 @@ func TestKeyWakeup(t *testing.T) {
|
|||||||
if evts := r.Events(handler); len(evts) != 1 {
|
if evts := r.Events(handler); len(evts) != 1 {
|
||||||
t.Errorf("no Focus event for newly registered key.InputOp")
|
t.Errorf("no Focus event for newly registered key.InputOp")
|
||||||
}
|
}
|
||||||
// Verify that r.Events does trigger a redraw.
|
|
||||||
r.Frame(&ops)
|
|
||||||
if _, wake := r.WakeupTime(); !wake {
|
|
||||||
t.Errorf("key.FocusEvent event didn't trigger a redraw")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeyMultiples(t *testing.T) {
|
func TestKeyMultiples(t *testing.T) {
|
||||||
@@ -279,7 +274,7 @@ func TestFocusScroll(t *testing.T) {
|
|||||||
key.InputOp{Tag: h}.Add(ops)
|
key.InputOp{Tag: h}.Add(ops)
|
||||||
pointer.InputOp{
|
pointer.InputOp{
|
||||||
Tag: h,
|
Tag: h,
|
||||||
Types: pointer.Scroll,
|
Kinds: pointer.Scroll,
|
||||||
ScrollBounds: image.Rect(-100, -100, 100, 100),
|
ScrollBounds: image.Rect(-100, -100, 100, 100),
|
||||||
}.Add(ops)
|
}.Add(ops)
|
||||||
// Test that h is scrolled even if behind another handler.
|
// Test that h is scrolled even if behind another handler.
|
||||||
@@ -305,7 +300,7 @@ func TestFocusClick(t *testing.T) {
|
|||||||
key.InputOp{Tag: h}.Add(ops)
|
key.InputOp{Tag: h}.Add(ops)
|
||||||
pointer.InputOp{
|
pointer.InputOp{
|
||||||
Tag: h,
|
Tag: h,
|
||||||
Types: pointer.Press | pointer.Release,
|
Kinds: pointer.Press | pointer.Release,
|
||||||
}.Add(ops)
|
}.Add(ops)
|
||||||
cl.Pop()
|
cl.Pop()
|
||||||
r.Frame(ops)
|
r.Frame(ops)
|
||||||
|
|||||||
+58
-44
@@ -66,7 +66,7 @@ type pointerHandler struct {
|
|||||||
area int
|
area int
|
||||||
active bool
|
active bool
|
||||||
wantsGrab bool
|
wantsGrab bool
|
||||||
types pointer.Type
|
types pointer.Kind
|
||||||
// min and max horizontal/vertical scroll
|
// min and max horizontal/vertical scroll
|
||||||
scrollRange image.Rectangle
|
scrollRange image.Rectangle
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *poi
|
|||||||
c.q.handlers[tag] = h
|
c.q.handlers[tag] = h
|
||||||
// Cancel handlers on (each) first appearance, but don't
|
// Cancel handlers on (each) first appearance, but don't
|
||||||
// trigger redraw.
|
// trigger redraw.
|
||||||
events.AddNoRedraw(tag, pointer.Event{Type: pointer.Cancel})
|
events.AddNoRedraw(tag, pointer.Event{Kind: pointer.Cancel})
|
||||||
}
|
}
|
||||||
h.active = true
|
h.active = true
|
||||||
h.area = areaID
|
h.area = areaID
|
||||||
@@ -268,16 +268,16 @@ func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
|
|||||||
areaID := c.currentArea()
|
areaID := c.currentArea()
|
||||||
area := &c.q.areas[areaID]
|
area := &c.q.areas[areaID]
|
||||||
area.semantic.content.tag = op.Tag
|
area.semantic.content.tag = op.Tag
|
||||||
if op.Types&(pointer.Press|pointer.Release) != 0 {
|
if op.Kinds&(pointer.Press|pointer.Release) != 0 {
|
||||||
area.semantic.content.gestures |= ClickGesture
|
area.semantic.content.gestures |= ClickGesture
|
||||||
}
|
}
|
||||||
if op.Types&pointer.Scroll != 0 {
|
if op.Kinds&pointer.Scroll != 0 {
|
||||||
area.semantic.content.gestures |= ScrollGesture
|
area.semantic.content.gestures |= ScrollGesture
|
||||||
}
|
}
|
||||||
area.semantic.valid = area.semantic.content.gestures != 0
|
area.semantic.valid = area.semantic.content.gestures != 0
|
||||||
h := c.newHandler(op.Tag, events)
|
h := c.newHandler(op.Tag, events)
|
||||||
h.wantsGrab = h.wantsGrab || op.Grab
|
h.wantsGrab = h.wantsGrab || op.Grab
|
||||||
h.types = h.types | op.Types
|
h.types = h.types | op.Kinds
|
||||||
h.scrollRange = op.ScrollBounds
|
h.scrollRange = op.ScrollBounds
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,11 +309,11 @@ func (c *pointerCollector) semanticSelected(selected bool) {
|
|||||||
area.semantic.content.selected = selected
|
area.semantic.content.selected = selected
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *pointerCollector) semanticDisabled(disabled bool) {
|
func (c *pointerCollector) semanticEnabled(enabled bool) {
|
||||||
areaID := c.currentArea()
|
areaID := c.currentArea()
|
||||||
area := &c.q.areas[areaID]
|
area := &c.q.areas[areaID]
|
||||||
area.semantic.valid = true
|
area.semantic.valid = true
|
||||||
area.semantic.content.disabled = disabled
|
area.semantic.content.disabled = !enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *pointerCollector) cursor(cursor pointer.Cursor) {
|
func (c *pointerCollector) cursor(cursor pointer.Cursor) {
|
||||||
@@ -432,39 +432,42 @@ func (q *pointerQueue) semanticIDFor(content semanticContent) SemanticID {
|
|||||||
return id.id
|
return id.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) ActionAt(pos f32.Point) (system.Action, bool) {
|
func (q *pointerQueue) ActionAt(pos f32.Point) (action system.Action, hasAction bool) {
|
||||||
for i := len(q.hitTree) - 1; i >= 0; i-- {
|
q.hitTest(pos, func(n *hitNode) bool {
|
||||||
n := &q.hitTree[i]
|
|
||||||
hit, _ := q.hit(n.area, pos)
|
|
||||||
if !hit {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
area := q.areas[n.area]
|
area := q.areas[n.area]
|
||||||
return area.action, area.action != 0
|
if area.action != 0 {
|
||||||
}
|
action = area.action
|
||||||
return 0, false
|
hasAction = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return action, hasAction
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) SemanticAt(pos f32.Point) (SemanticID, bool) {
|
func (q *pointerQueue) SemanticAt(pos f32.Point) (semID SemanticID, hasSemID bool) {
|
||||||
q.assignSemIDs()
|
q.assignSemIDs()
|
||||||
for i := len(q.hitTree) - 1; i >= 0; i-- {
|
q.hitTest(pos, func(n *hitNode) bool {
|
||||||
n := &q.hitTree[i]
|
|
||||||
hit, _ := q.hit(n.area, pos)
|
|
||||||
if !hit {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
area := q.areas[n.area]
|
area := q.areas[n.area]
|
||||||
if area.semantic.id != 0 {
|
if area.semantic.id != 0 {
|
||||||
return area.semantic.id, true
|
semID = area.semantic.id
|
||||||
|
hasSemID = true
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
return 0, false
|
})
|
||||||
|
return semID, hasSemID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
|
// hitTest searches the hit tree for nodes matching pos. Any node matching pos will
|
||||||
|
// have the onNode func invoked on it to allow the caller to extract whatever information
|
||||||
|
// is necessary for further processing. onNode may return false to terminate the walk of
|
||||||
|
// the hit tree, or true to continue. Providing this algorithm in this generic way
|
||||||
|
// allows normal event routing and system action event routing to share the same traversal
|
||||||
|
// logic even though they are interested in different aspects of hit nodes.
|
||||||
|
func (q *pointerQueue) hitTest(pos f32.Point, onNode func(*hitNode) bool) pointer.Cursor {
|
||||||
// Track whether we're passing through hits.
|
// Track whether we're passing through hits.
|
||||||
pass := true
|
pass := true
|
||||||
hits := q.scratch[:0]
|
|
||||||
idx := len(q.hitTree) - 1
|
idx := len(q.hitTree) - 1
|
||||||
cursor := pointer.CursorDefault
|
cursor := pointer.CursorDefault
|
||||||
for idx >= 0 {
|
for idx >= 0 {
|
||||||
@@ -483,12 +486,23 @@ func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
|
|||||||
} else {
|
} else {
|
||||||
idx = n.next
|
idx = n.next
|
||||||
}
|
}
|
||||||
|
if !onNode(n) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
|
||||||
|
hits := q.scratch[:0]
|
||||||
|
cursor := q.hitTest(pos, func(n *hitNode) bool {
|
||||||
if n.tag != nil {
|
if n.tag != nil {
|
||||||
if _, exists := q.handlers[n.tag]; exists {
|
if _, exists := q.handlers[n.tag]; exists {
|
||||||
hits = addHandler(hits, n.tag)
|
hits = addHandler(hits, n.tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
|
})
|
||||||
q.scratch = hits[:0]
|
q.scratch = hits[:0]
|
||||||
return hits, cursor
|
return hits, cursor
|
||||||
}
|
}
|
||||||
@@ -588,7 +602,7 @@ func (q *pointerQueue) Frame(events *handlerEvents) {
|
|||||||
|
|
||||||
func (q *pointerQueue) dropHandler(events *handlerEvents, tag event.Tag) {
|
func (q *pointerQueue) dropHandler(events *handlerEvents, tag event.Tag) {
|
||||||
if events != nil {
|
if events != nil {
|
||||||
events.Add(tag, pointer.Event{Type: pointer.Cancel})
|
events.Add(tag, pointer.Event{Kind: pointer.Cancel})
|
||||||
}
|
}
|
||||||
for i := range q.pointers {
|
for i := range q.pointers {
|
||||||
p := &q.pointers[i]
|
p := &q.pointers[i]
|
||||||
@@ -635,11 +649,11 @@ func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEven
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
h := q.handlers[n.tag]
|
h := q.handlers[n.tag]
|
||||||
if e.Type&h.types == 0 {
|
if e.Kind&h.types == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
e := e
|
e := e
|
||||||
if e.Type == pointer.Scroll {
|
if e.Kind == pointer.Scroll {
|
||||||
if sx == 0 && sy == 0 {
|
if sx == 0 && sy == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -649,7 +663,7 @@ func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEven
|
|||||||
}
|
}
|
||||||
e.Position = q.invTransform(h.area, e.Position)
|
e.Position = q.invTransform(h.area, e.Position)
|
||||||
events.Add(n.tag, e)
|
events.Add(n.tag, e)
|
||||||
if e.Type != pointer.Scroll {
|
if e.Kind != pointer.Scroll {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -669,7 +683,7 @@ func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
|
func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
|
||||||
if e.Type == pointer.Cancel {
|
if e.Kind == pointer.Cancel {
|
||||||
q.pointers = q.pointers[:0]
|
q.pointers = q.pointers[:0]
|
||||||
for k := range q.handlers {
|
for k := range q.handlers {
|
||||||
q.dropHandler(events, k)
|
q.dropHandler(events, k)
|
||||||
@@ -680,14 +694,14 @@ func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
|
|||||||
p := &q.pointers[pidx]
|
p := &q.pointers[pidx]
|
||||||
p.last = e
|
p.last = e
|
||||||
|
|
||||||
switch e.Type {
|
switch e.Kind {
|
||||||
case pointer.Press:
|
case pointer.Press:
|
||||||
q.deliverEnterLeaveEvents(p, events, e)
|
q.deliverEnterLeaveEvents(p, events, e)
|
||||||
p.pressed = true
|
p.pressed = true
|
||||||
q.deliverEvent(p, events, e)
|
q.deliverEvent(p, events, e)
|
||||||
case pointer.Move:
|
case pointer.Move:
|
||||||
if p.pressed {
|
if p.pressed {
|
||||||
e.Type = pointer.Drag
|
e.Kind = pointer.Drag
|
||||||
}
|
}
|
||||||
q.deliverEnterLeaveEvents(p, events, e)
|
q.deliverEnterLeaveEvents(p, events, e)
|
||||||
q.deliverEvent(p, events, e)
|
q.deliverEvent(p, events, e)
|
||||||
@@ -721,7 +735,7 @@ func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e poi
|
|||||||
var sx, sy = e.Scroll.X, e.Scroll.Y
|
var sx, sy = e.Scroll.X, e.Scroll.Y
|
||||||
for _, k := range p.handlers {
|
for _, k := range p.handlers {
|
||||||
h := q.handlers[k]
|
h := q.handlers[k]
|
||||||
if e.Type == pointer.Scroll {
|
if e.Kind == pointer.Scroll {
|
||||||
if sx == 0 && sy == 0 {
|
if sx == 0 && sy == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -729,7 +743,7 @@ func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e poi
|
|||||||
sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X, h.scrollRange.Max.X)
|
sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X, h.scrollRange.Max.X)
|
||||||
sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y, h.scrollRange.Max.Y)
|
sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y, h.scrollRange.Max.Y)
|
||||||
}
|
}
|
||||||
if e.Type&h.types == 0 {
|
if e.Kind&h.types == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
e := e
|
e := e
|
||||||
@@ -744,7 +758,7 @@ func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e poi
|
|||||||
|
|
||||||
func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) {
|
func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) {
|
||||||
var hits []event.Tag
|
var hits []event.Tag
|
||||||
if e.Source != pointer.Mouse && !p.pressed && e.Type != pointer.Press {
|
if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press {
|
||||||
// Consider non-mouse pointers leaving when they're released.
|
// Consider non-mouse pointers leaving when they're released.
|
||||||
} else {
|
} else {
|
||||||
hits, q.cursor = q.opHit(e.Position)
|
hits, q.cursor = q.opHit(e.Position)
|
||||||
@@ -776,9 +790,9 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
h := q.handlers[k]
|
h := q.handlers[k]
|
||||||
e.Type = pointer.Leave
|
e.Kind = pointer.Leave
|
||||||
|
|
||||||
if e.Type&h.types != 0 {
|
if e.Kind&h.types != 0 {
|
||||||
e := e
|
e := e
|
||||||
e.Position = q.invTransform(h.area, e.Position)
|
e.Position = q.invTransform(h.area, e.Position)
|
||||||
events.Add(k, e)
|
events.Add(k, e)
|
||||||
@@ -790,9 +804,9 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv
|
|||||||
if _, found := searchTag(p.entered, k); found {
|
if _, found := searchTag(p.entered, k); found {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
e.Type = pointer.Enter
|
e.Kind = pointer.Enter
|
||||||
|
|
||||||
if e.Type&h.types != 0 {
|
if e.Kind&h.types != 0 {
|
||||||
e := e
|
e := e
|
||||||
e.Position = q.invTransform(h.area, e.Position)
|
e.Position = q.invTransform(h.area, e.Position)
|
||||||
events.Add(k, e)
|
events.Add(k, e)
|
||||||
|
|||||||
+149
-105
@@ -14,6 +14,7 @@ import (
|
|||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
|
"gioui.org/io/system"
|
||||||
"gioui.org/io/transfer"
|
"gioui.org/io/transfer"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
@@ -32,11 +33,6 @@ func TestPointerWakeup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// However, adding a handler queues a Cancel event.
|
// However, adding a handler queues a Cancel event.
|
||||||
assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel)
|
assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel)
|
||||||
// Verify that r.Events does trigger a redraw.
|
|
||||||
r.Frame(&ops)
|
|
||||||
if _, wake := r.WakeupTime(); !wake {
|
|
||||||
t.Errorf("pointer.Cancel event didn't trigger a redraw")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPointerDrag(t *testing.T) {
|
func TestPointerDrag(t *testing.T) {
|
||||||
@@ -49,12 +45,12 @@ func TestPointerDrag(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
// Press.
|
// Press.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
// Move outside the area.
|
// Move outside the area.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(150, 150),
|
Position: f32.Pt(150, 150),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -71,12 +67,12 @@ func TestPointerDragNegative(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
// Press.
|
// Press.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: f32.Pt(-50, -50),
|
Position: f32.Pt(-50, -50),
|
||||||
},
|
},
|
||||||
// Move outside the area.
|
// Move outside the area.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(-150, -150),
|
Position: f32.Pt(-150, -150),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -91,15 +87,15 @@ func TestPointerGrab(t *testing.T) {
|
|||||||
|
|
||||||
types := pointer.Press | pointer.Release
|
types := pointer.Press | pointer.Release
|
||||||
|
|
||||||
pointer.InputOp{Tag: handler1, Types: types, Grab: true}.Add(&ops)
|
pointer.InputOp{Tag: handler1, Kinds: types, Grab: true}.Add(&ops)
|
||||||
pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
|
pointer.InputOp{Tag: handler2, Kinds: types}.Add(&ops)
|
||||||
pointer.InputOp{Tag: handler3, Types: types}.Add(&ops)
|
pointer.InputOp{Tag: handler3, Kinds: types}.Add(&ops)
|
||||||
|
|
||||||
var r Router
|
var r Router
|
||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -109,7 +105,7 @@ func TestPointerGrab(t *testing.T) {
|
|||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -125,15 +121,15 @@ func TestPointerGrabSameHandlerTwice(t *testing.T) {
|
|||||||
|
|
||||||
types := pointer.Press | pointer.Release
|
types := pointer.Press | pointer.Release
|
||||||
|
|
||||||
pointer.InputOp{Tag: handler1, Types: types, Grab: true}.Add(&ops)
|
pointer.InputOp{Tag: handler1, Kinds: types, Grab: true}.Add(&ops)
|
||||||
pointer.InputOp{Tag: handler1, Types: types}.Add(&ops)
|
pointer.InputOp{Tag: handler1, Kinds: types}.Add(&ops)
|
||||||
pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
|
pointer.InputOp{Tag: handler2, Kinds: types}.Add(&ops)
|
||||||
|
|
||||||
var r Router
|
var r Router
|
||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -142,7 +138,7 @@ func TestPointerGrabSameHandlerTwice(t *testing.T) {
|
|||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -159,10 +155,10 @@ func TestPointerMove(t *testing.T) {
|
|||||||
|
|
||||||
// Handler 1 area: (0, 0) - (100, 100)
|
// Handler 1 area: (0, 0) - (100, 100)
|
||||||
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||||
pointer.InputOp{Tag: handler1, Types: types}.Add(&ops)
|
pointer.InputOp{Tag: handler1, Kinds: types}.Add(&ops)
|
||||||
// Handler 2 area: (50, 50) - (100, 100) (areas intersect).
|
// Handler 2 area: (50, 50) - (100, 100) (areas intersect).
|
||||||
r2 := clip.Rect(image.Rect(50, 50, 200, 200)).Push(&ops)
|
r2 := clip.Rect(image.Rect(50, 50, 200, 200)).Push(&ops)
|
||||||
pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
|
pointer.InputOp{Tag: handler2, Kinds: types}.Add(&ops)
|
||||||
r2.Pop()
|
r2.Pop()
|
||||||
r1.Pop()
|
r1.Pop()
|
||||||
|
|
||||||
@@ -171,21 +167,21 @@ func TestPointerMove(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
// Hit both handlers.
|
// Hit both handlers.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
// Hit handler 1.
|
// Hit handler 1.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(49, 50),
|
Position: f32.Pt(49, 50),
|
||||||
},
|
},
|
||||||
// Hit no handlers.
|
// Hit no handlers.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(100, 50),
|
Position: f32.Pt(100, 50),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Cancel,
|
Kind: pointer.Cancel,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, pointer.Move, pointer.Move, pointer.Leave, pointer.Cancel)
|
assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, pointer.Move, pointer.Move, pointer.Leave, pointer.Cancel)
|
||||||
@@ -198,7 +194,7 @@ func TestPointerTypes(t *testing.T) {
|
|||||||
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||||
pointer.InputOp{
|
pointer.InputOp{
|
||||||
Tag: handler,
|
Tag: handler,
|
||||||
Types: pointer.Press | pointer.Release,
|
Kinds: pointer.Press | pointer.Release,
|
||||||
}.Add(&ops)
|
}.Add(&ops)
|
||||||
r1.Pop()
|
r1.Pop()
|
||||||
|
|
||||||
@@ -206,21 +202,58 @@ func TestPointerTypes(t *testing.T) {
|
|||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(150, 150),
|
Position: f32.Pt(150, 150),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Position: f32.Pt(150, 150),
|
Position: f32.Pt(150, 150),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel, pointer.Press, pointer.Release)
|
assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel, pointer.Press, pointer.Release)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPointerSystemAction(t *testing.T) {
|
||||||
|
t.Run("simple", func(t *testing.T) {
|
||||||
|
var ops op.Ops
|
||||||
|
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||||
|
system.ActionInputOp(system.ActionMove).Add(&ops)
|
||||||
|
r1.Pop()
|
||||||
|
|
||||||
|
var r Router
|
||||||
|
r.Frame(&ops)
|
||||||
|
assertActionAt(t, r, f32.Pt(50, 50), system.ActionMove)
|
||||||
|
})
|
||||||
|
t.Run("covered by another clip", func(t *testing.T) {
|
||||||
|
var ops op.Ops
|
||||||
|
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||||
|
system.ActionInputOp(system.ActionMove).Add(&ops)
|
||||||
|
clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop()
|
||||||
|
r1.Pop()
|
||||||
|
|
||||||
|
var r Router
|
||||||
|
r.Frame(&ops)
|
||||||
|
assertActionAt(t, r, f32.Pt(50, 50), system.ActionMove)
|
||||||
|
})
|
||||||
|
t.Run("uses topmost action op", func(t *testing.T) {
|
||||||
|
var ops op.Ops
|
||||||
|
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||||
|
system.ActionInputOp(system.ActionMove).Add(&ops)
|
||||||
|
r2 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||||
|
system.ActionInputOp(system.ActionClose).Add(&ops)
|
||||||
|
r2.Pop()
|
||||||
|
r1.Pop()
|
||||||
|
|
||||||
|
var r Router
|
||||||
|
r.Frame(&ops)
|
||||||
|
assertActionAt(t, r, f32.Pt(50, 50), system.ActionClose)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestPointerPriority(t *testing.T) {
|
func TestPointerPriority(t *testing.T) {
|
||||||
handler1 := new(int)
|
handler1 := new(int)
|
||||||
handler2 := new(int)
|
handler2 := new(int)
|
||||||
@@ -230,14 +263,14 @@ func TestPointerPriority(t *testing.T) {
|
|||||||
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||||
pointer.InputOp{
|
pointer.InputOp{
|
||||||
Tag: handler1,
|
Tag: handler1,
|
||||||
Types: pointer.Scroll,
|
Kinds: pointer.Scroll,
|
||||||
ScrollBounds: image.Rectangle{Max: image.Point{X: 100}},
|
ScrollBounds: image.Rectangle{Max: image.Point{X: 100}},
|
||||||
}.Add(&ops)
|
}.Add(&ops)
|
||||||
|
|
||||||
r2 := clip.Rect(image.Rect(0, 0, 100, 50)).Push(&ops)
|
r2 := clip.Rect(image.Rect(0, 0, 100, 50)).Push(&ops)
|
||||||
pointer.InputOp{
|
pointer.InputOp{
|
||||||
Tag: handler2,
|
Tag: handler2,
|
||||||
Types: pointer.Scroll,
|
Kinds: pointer.Scroll,
|
||||||
ScrollBounds: image.Rectangle{Max: image.Point{X: 20}},
|
ScrollBounds: image.Rectangle{Max: image.Point{X: 20}},
|
||||||
}.Add(&ops)
|
}.Add(&ops)
|
||||||
r2.Pop()
|
r2.Pop()
|
||||||
@@ -246,7 +279,7 @@ func TestPointerPriority(t *testing.T) {
|
|||||||
r3 := clip.Rect(image.Rect(0, 100, 100, 200)).Push(&ops)
|
r3 := clip.Rect(image.Rect(0, 100, 100, 200)).Push(&ops)
|
||||||
pointer.InputOp{
|
pointer.InputOp{
|
||||||
Tag: handler3,
|
Tag: handler3,
|
||||||
Types: pointer.Scroll,
|
Kinds: pointer.Scroll,
|
||||||
ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}},
|
ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}},
|
||||||
}.Add(&ops)
|
}.Add(&ops)
|
||||||
r3.Pop()
|
r3.Pop()
|
||||||
@@ -256,25 +289,25 @@ func TestPointerPriority(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
// Hit handler 1 and 2.
|
// Hit handler 1 and 2.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Scroll,
|
Kind: pointer.Scroll,
|
||||||
Position: f32.Pt(50, 25),
|
Position: f32.Pt(50, 25),
|
||||||
Scroll: f32.Pt(50, 0),
|
Scroll: f32.Pt(50, 0),
|
||||||
},
|
},
|
||||||
// Hit handler 1.
|
// Hit handler 1.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Scroll,
|
Kind: pointer.Scroll,
|
||||||
Position: f32.Pt(50, 75),
|
Position: f32.Pt(50, 75),
|
||||||
Scroll: f32.Pt(50, 50),
|
Scroll: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
// Hit handler 3.
|
// Hit handler 3.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Scroll,
|
Kind: pointer.Scroll,
|
||||||
Position: f32.Pt(50, 150),
|
Position: f32.Pt(50, 150),
|
||||||
Scroll: f32.Pt(-30, -30),
|
Scroll: f32.Pt(-30, -30),
|
||||||
},
|
},
|
||||||
// Hit no handlers.
|
// Hit no handlers.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Scroll,
|
Kind: pointer.Scroll,
|
||||||
Position: f32.Pt(50, 225),
|
Position: f32.Pt(50, 225),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -310,7 +343,7 @@ func TestPointerEnterLeave(t *testing.T) {
|
|||||||
// Hit both handlers.
|
// Hit both handlers.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -323,7 +356,7 @@ func TestPointerEnterLeave(t *testing.T) {
|
|||||||
// Leave the second area by moving into the first.
|
// Leave the second area by moving into the first.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(45, 45),
|
Position: f32.Pt(45, 45),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -334,7 +367,7 @@ func TestPointerEnterLeave(t *testing.T) {
|
|||||||
// Move, but stay within the same hit area.
|
// Move, but stay within the same hit area.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(40, 40),
|
Position: f32.Pt(40, 40),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -344,7 +377,7 @@ func TestPointerEnterLeave(t *testing.T) {
|
|||||||
// Move outside of both inputs.
|
// Move outside of both inputs.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(300, 300),
|
Position: f32.Pt(300, 300),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -354,7 +387,7 @@ func TestPointerEnterLeave(t *testing.T) {
|
|||||||
// Check that a Press event generates Enter Events.
|
// Check that a Press event generates Enter Events.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: f32.Pt(125, 125),
|
Position: f32.Pt(125, 125),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -365,12 +398,12 @@ func TestPointerEnterLeave(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
// Leave
|
// Leave
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(25, 25),
|
Position: f32.Pt(25, 25),
|
||||||
},
|
},
|
||||||
// Enter
|
// Enter
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -380,7 +413,7 @@ func TestPointerEnterLeave(t *testing.T) {
|
|||||||
// Check that a Release event generates Enter/Leave Events.
|
// Check that a Release event generates Enter/Leave Events.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Position: f32.Pt(25,
|
Position: f32.Pt(25,
|
||||||
25),
|
25),
|
||||||
},
|
},
|
||||||
@@ -408,15 +441,15 @@ func TestMultipleAreas(t *testing.T) {
|
|||||||
// Hit first area, then second area, then both.
|
// Hit first area, then second area, then both.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(25, 25),
|
Position: f32.Pt(25, 25),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(150, 150),
|
Position: f32.Pt(150, 150),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -432,11 +465,11 @@ func TestPointerEnterLeaveNested(t *testing.T) {
|
|||||||
|
|
||||||
// Handler 1 area: (0, 0) - (100, 100)
|
// Handler 1 area: (0, 0) - (100, 100)
|
||||||
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||||
pointer.InputOp{Tag: handler1, Types: types}.Add(&ops)
|
pointer.InputOp{Tag: handler1, Kinds: types}.Add(&ops)
|
||||||
|
|
||||||
// Handler 2 area: (25, 25) - (75, 75) (nested within first).
|
// Handler 2 area: (25, 25) - (75, 75) (nested within first).
|
||||||
r2 := clip.Rect(image.Rect(25, 25, 75, 75)).Push(&ops)
|
r2 := clip.Rect(image.Rect(25, 25, 75, 75)).Push(&ops)
|
||||||
pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
|
pointer.InputOp{Tag: handler2, Kinds: types}.Add(&ops)
|
||||||
r2.Pop()
|
r2.Pop()
|
||||||
r1.Pop()
|
r1.Pop()
|
||||||
|
|
||||||
@@ -445,7 +478,7 @@ func TestPointerEnterLeaveNested(t *testing.T) {
|
|||||||
// Hit both handlers.
|
// Hit both handlers.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -457,7 +490,7 @@ func TestPointerEnterLeaveNested(t *testing.T) {
|
|||||||
// Leave the second area by moving into the first.
|
// Leave the second area by moving into the first.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(20, 20),
|
Position: f32.Pt(20, 20),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -467,7 +500,7 @@ func TestPointerEnterLeaveNested(t *testing.T) {
|
|||||||
// Move, but stay within the same hit area.
|
// Move, but stay within the same hit area.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -477,7 +510,7 @@ func TestPointerEnterLeaveNested(t *testing.T) {
|
|||||||
// Move outside of both inputs.
|
// Move outside of both inputs.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(200, 200),
|
Position: f32.Pt(200, 200),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -487,7 +520,7 @@ func TestPointerEnterLeaveNested(t *testing.T) {
|
|||||||
// Check that a Press event generates Enter Events.
|
// Check that a Press event generates Enter Events.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -497,7 +530,7 @@ func TestPointerEnterLeaveNested(t *testing.T) {
|
|||||||
// Check that a Release event generates Enter/Leave Events.
|
// Check that a Release event generates Enter/Leave Events.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Position: f32.Pt(20, 20),
|
Position: f32.Pt(20, 20),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -516,7 +549,7 @@ func TestPointerActiveInputDisappears(t *testing.T) {
|
|||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(25, 25),
|
Position: f32.Pt(25, 25),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -527,7 +560,7 @@ func TestPointerActiveInputDisappears(t *testing.T) {
|
|||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(25, 25),
|
Position: f32.Pt(25, 25),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -549,21 +582,21 @@ func TestMultitouch(t *testing.T) {
|
|||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: h1pt,
|
Position: h1pt,
|
||||||
PointerID: p1,
|
PointerID: p1,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: h2pt,
|
Position: h2pt,
|
||||||
PointerID: p2,
|
PointerID: p2,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Position: h2pt,
|
Position: h2pt,
|
||||||
PointerID: p2,
|
PointerID: p2,
|
||||||
},
|
},
|
||||||
@@ -596,7 +629,7 @@ func TestCursor(t *testing.T) {
|
|||||||
|
|
||||||
_at := func(x, y float32) pointer.Event {
|
_at := func(x, y float32) pointer.Event {
|
||||||
return pointer.Event{
|
return pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Position: f32.Pt(x, y),
|
Position: f32.Pt(x, y),
|
||||||
@@ -696,14 +729,14 @@ func TestPassOp(t *testing.T) {
|
|||||||
h1, h2, h3, h4 := new(int), new(int), new(int), new(int)
|
h1, h2, h3, h4 := new(int), new(int), new(int), new(int)
|
||||||
area := clip.Rect(image.Rect(0, 0, 100, 100))
|
area := clip.Rect(image.Rect(0, 0, 100, 100))
|
||||||
root := area.Push(&ops)
|
root := area.Push(&ops)
|
||||||
pointer.InputOp{Tag: h1, Types: pointer.Press}.Add(&ops)
|
pointer.InputOp{Tag: h1, Kinds: pointer.Press}.Add(&ops)
|
||||||
child1 := area.Push(&ops)
|
child1 := area.Push(&ops)
|
||||||
pointer.InputOp{Tag: h2, Types: pointer.Press}.Add(&ops)
|
pointer.InputOp{Tag: h2, Kinds: pointer.Press}.Add(&ops)
|
||||||
child1.Pop()
|
child1.Pop()
|
||||||
child2 := area.Push(&ops)
|
child2 := area.Push(&ops)
|
||||||
pass := pointer.PassOp{}.Push(&ops)
|
pass := pointer.PassOp{}.Push(&ops)
|
||||||
pointer.InputOp{Tag: h3, Types: pointer.Press}.Add(&ops)
|
pointer.InputOp{Tag: h3, Kinds: pointer.Press}.Add(&ops)
|
||||||
pointer.InputOp{Tag: h4, Types: pointer.Press}.Add(&ops)
|
pointer.InputOp{Tag: h4, Kinds: pointer.Press}.Add(&ops)
|
||||||
pass.Pop()
|
pass.Pop()
|
||||||
child2.Pop()
|
child2.Pop()
|
||||||
root.Pop()
|
root.Pop()
|
||||||
@@ -712,7 +745,7 @@ func TestPassOp(t *testing.T) {
|
|||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventPointerTypeSequence(t, r.Events(h1), pointer.Cancel, pointer.Press)
|
assertEventPointerTypeSequence(t, r.Events(h1), pointer.Cancel, pointer.Press)
|
||||||
@@ -725,13 +758,13 @@ func TestAreaPassthrough(t *testing.T) {
|
|||||||
var ops op.Ops
|
var ops op.Ops
|
||||||
|
|
||||||
h := new(int)
|
h := new(int)
|
||||||
pointer.InputOp{Tag: h, Types: pointer.Press}.Add(&ops)
|
pointer.InputOp{Tag: h, Kinds: pointer.Press}.Add(&ops)
|
||||||
clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop()
|
clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop()
|
||||||
var r Router
|
var r Router
|
||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventPointerTypeSequence(t, r.Events(h), pointer.Cancel, pointer.Press)
|
assertEventPointerTypeSequence(t, r.Events(h), pointer.Cancel, pointer.Press)
|
||||||
@@ -742,7 +775,7 @@ func TestEllipse(t *testing.T) {
|
|||||||
|
|
||||||
h := new(int)
|
h := new(int)
|
||||||
cl := clip.Ellipse(image.Rect(0, 0, 100, 100)).Push(&ops)
|
cl := clip.Ellipse(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||||
pointer.InputOp{Tag: h, Types: pointer.Press}.Add(&ops)
|
pointer.InputOp{Tag: h, Kinds: pointer.Press}.Add(&ops)
|
||||||
cl.Pop()
|
cl.Pop()
|
||||||
var r Router
|
var r Router
|
||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
@@ -750,15 +783,15 @@ func TestEllipse(t *testing.T) {
|
|||||||
// Outside ellipse.
|
// Outside ellipse.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
},
|
},
|
||||||
// Inside ellipse.
|
// Inside ellipse.
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventPointerTypeSequence(t, r.Events(h), pointer.Cancel, pointer.Press)
|
assertEventPointerTypeSequence(t, r.Events(h), pointer.Cancel, pointer.Press)
|
||||||
@@ -787,7 +820,7 @@ func TestTransfer(t *testing.T) {
|
|||||||
return src, tgt
|
return src, tgt
|
||||||
}
|
}
|
||||||
// Cancel is received when the pointer is first seen.
|
// Cancel is received when the pointer is first seen.
|
||||||
cancel := pointer.Event{Type: pointer.Cancel}
|
cancel := pointer.Event{Kind: pointer.Cancel}
|
||||||
|
|
||||||
t.Run("transfer.Offer should panic on nil Data", func(t *testing.T) {
|
t.Run("transfer.Offer should panic on nil Data", func(t *testing.T) {
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -807,11 +840,11 @@ func TestTransfer(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventSequence(t, r.Events(src), cancel)
|
assertEventSequence(t, r.Events(src), cancel)
|
||||||
@@ -821,11 +854,11 @@ func TestTransfer(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(30, 10),
|
Position: f32.Pt(30, 10),
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(30, 10),
|
Position: f32.Pt(30, 10),
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventSequence(t, r.Events(src), transfer.CancelEvent{})
|
assertEventSequence(t, r.Events(src), transfer.CancelEvent{})
|
||||||
@@ -848,11 +881,11 @@ func TestTransfer(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventSequence(t, r.Events(src), cancel)
|
assertEventSequence(t, r.Events(src), cancel)
|
||||||
@@ -869,11 +902,11 @@ func TestTransfer(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventSequence(t, r.Events(src), cancel)
|
assertEventSequence(t, r.Events(src), cancel)
|
||||||
@@ -883,7 +916,7 @@ func TestTransfer(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(40, 10),
|
Position: f32.Pt(40, 10),
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventSequence(t, r.Events(src), transfer.CancelEvent{})
|
assertEventSequence(t, r.Events(src), transfer.CancelEvent{})
|
||||||
@@ -906,11 +939,11 @@ func TestTransfer(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventSequence(t, r.Events(src), cancel)
|
assertEventSequence(t, r.Events(src), cancel)
|
||||||
@@ -920,7 +953,7 @@ func TestTransfer(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(40, 10),
|
Position: f32.Pt(40, 10),
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventSequence(t, r.Events(src), transfer.RequestEvent{Type: "file"})
|
assertEventSequence(t, r.Events(src), transfer.RequestEvent{Type: "file"})
|
||||||
@@ -973,15 +1006,15 @@ func TestTransfer(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(40, 10),
|
Position: f32.Pt(40, 10),
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
ofr := &offer{data: "hello"}
|
ofr := &offer{data: "hello"}
|
||||||
@@ -1016,15 +1049,15 @@ func TestTransfer(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(40, 10),
|
Position: f32.Pt(40, 10),
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventPointerTypeSequence(t, r.Events(&hover), pointer.Cancel, pointer.Enter)
|
assertEventPointerTypeSequence(t, r.Events(&hover), pointer.Cancel, pointer.Enter)
|
||||||
@@ -1033,7 +1066,7 @@ func TestTransfer(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(40, 10),
|
Position: f32.Pt(40, 10),
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1064,15 +1097,15 @@ func TestTransfer(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(40, 10),
|
Position: f32.Pt(40, 10),
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertEventPointerTypeSequence(t, r.Events(&hover), pointer.Cancel)
|
assertEventPointerTypeSequence(t, r.Events(&hover), pointer.Cancel)
|
||||||
@@ -1081,7 +1114,7 @@ func TestTransfer(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(40, 10),
|
Position: f32.Pt(40, 10),
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1129,7 +1162,7 @@ func TestPassCursor(t *testing.T) {
|
|||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(pointer.Event{
|
r.Queue(pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
})
|
})
|
||||||
if got := r.Cursor(); want != got {
|
if got := r.Cursor(); want != got {
|
||||||
t.Errorf("got cursor %v, want %v", got, want)
|
t.Errorf("got cursor %v, want %v", got, want)
|
||||||
@@ -1154,18 +1187,18 @@ func addPointerHandler(ops *op.Ops, tag event.Tag, area image.Rectangle) {
|
|||||||
defer clip.Rect(area).Push(ops).Pop()
|
defer clip.Rect(area).Push(ops).Pop()
|
||||||
pointer.InputOp{
|
pointer.InputOp{
|
||||||
Tag: tag,
|
Tag: tag,
|
||||||
Types: pointer.Press | pointer.Release | pointer.Move | pointer.Drag | pointer.Enter | pointer.Leave,
|
Kinds: pointer.Press | pointer.Release | pointer.Move | pointer.Drag | pointer.Enter | pointer.Leave,
|
||||||
}.Add(ops)
|
}.Add(ops)
|
||||||
}
|
}
|
||||||
|
|
||||||
// pointerTypes converts a sequence of event.Event to their pointer.Types. It assumes
|
// pointerTypes converts a sequence of event.Event to their pointer.Types. It assumes
|
||||||
// that all input events are of underlying type pointer.Event, and thus will
|
// that all input events are of underlying type pointer.Event, and thus will
|
||||||
// panic if some are not.
|
// panic if some are not.
|
||||||
func pointerTypes(events []event.Event) []pointer.Type {
|
func pointerTypes(events []event.Event) []pointer.Kind {
|
||||||
var types []pointer.Type
|
var types []pointer.Kind
|
||||||
for _, e := range events {
|
for _, e := range events {
|
||||||
if e, ok := e.(pointer.Event); ok {
|
if e, ok := e.(pointer.Event); ok {
|
||||||
types = append(types, e.Type)
|
types = append(types, e.Kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return types
|
return types
|
||||||
@@ -1173,7 +1206,7 @@ func pointerTypes(events []event.Event) []pointer.Type {
|
|||||||
|
|
||||||
// assertEventPointerTypeSequence checks that the provided events match the expected pointer event types
|
// assertEventPointerTypeSequence checks that the provided events match the expected pointer event types
|
||||||
// in the provided order.
|
// in the provided order.
|
||||||
func assertEventPointerTypeSequence(t *testing.T, events []event.Event, expected ...pointer.Type) {
|
func assertEventPointerTypeSequence(t *testing.T, events []event.Event, expected ...pointer.Kind) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
got := pointerTypes(events)
|
got := pointerTypes(events)
|
||||||
if !reflect.DeepEqual(got, expected) {
|
if !reflect.DeepEqual(got, expected) {
|
||||||
@@ -1201,7 +1234,7 @@ func eventsToString(evs []event.Event) string {
|
|||||||
for _, ev := range evs {
|
for _, ev := range evs {
|
||||||
switch e := ev.(type) {
|
switch e := ev.(type) {
|
||||||
case pointer.Event:
|
case pointer.Event:
|
||||||
s = append(s, fmt.Sprintf("%T{%s}", e, e.Type.String()))
|
s = append(s, fmt.Sprintf("%T{%s}", e, e.Kind.String()))
|
||||||
default:
|
default:
|
||||||
s = append(s, fmt.Sprintf("{%T}", e))
|
s = append(s, fmt.Sprintf("{%T}", e))
|
||||||
}
|
}
|
||||||
@@ -1231,6 +1264,17 @@ func assertScrollEvent(t *testing.T, ev event.Event, scroll f32.Point) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assertActionAt checks that the router has a system action of the expected type at point.
|
||||||
|
func assertActionAt(t *testing.T, q Router, point f32.Point, expected system.Action) {
|
||||||
|
t.Helper()
|
||||||
|
action, ok := q.ActionAt(point)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("expected action %v at %v, got no action", expected, point)
|
||||||
|
} else if action != expected {
|
||||||
|
t.Errorf("expected action %v at %v, got %v", expected, point, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkRouterAdd(b *testing.B) {
|
func BenchmarkRouterAdd(b *testing.B) {
|
||||||
// Set this to the number of overlapping handlers that you want to
|
// Set this to the number of overlapping handlers that you want to
|
||||||
// evaluate performance for. Typical values for the example applications
|
// evaluate performance for. Typical values for the example applications
|
||||||
@@ -1259,7 +1303,7 @@ func BenchmarkRouterAdd(b *testing.B) {
|
|||||||
Push(&ops)
|
Push(&ops)
|
||||||
pointer.InputOp{
|
pointer.InputOp{
|
||||||
Tag: handlers[i],
|
Tag: handlers[i],
|
||||||
Types: pointer.Move,
|
Kinds: pointer.Move,
|
||||||
}.Add(&ops)
|
}.Add(&ops)
|
||||||
}
|
}
|
||||||
var r Router
|
var r Router
|
||||||
@@ -1269,7 +1313,7 @@ func BenchmarkRouterAdd(b *testing.B) {
|
|||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
+11
-21
@@ -273,7 +273,7 @@ func (q *Router) ScrollFocus(dist image.Point) {
|
|||||||
}
|
}
|
||||||
area := q.key.queue.AreaFor(focus)
|
area := q.key.queue.AreaFor(focus)
|
||||||
q.pointer.queue.Deliver(area, pointer.Event{
|
q.pointer.queue.Deliver(area, pointer.Event{
|
||||||
Type: pointer.Scroll,
|
Kind: pointer.Scroll,
|
||||||
Source: pointer.Touch,
|
Source: pointer.Touch,
|
||||||
Scroll: f32internal.FPt(dist),
|
Scroll: f32internal.FPt(dist),
|
||||||
}, &q.handlers)
|
}, &q.handlers)
|
||||||
@@ -317,9 +317,9 @@ func (q *Router) ClickFocus() {
|
|||||||
Source: pointer.Touch,
|
Source: pointer.Touch,
|
||||||
}
|
}
|
||||||
area := q.key.queue.AreaFor(focus)
|
area := q.key.queue.AreaFor(focus)
|
||||||
e.Type = pointer.Press
|
e.Kind = pointer.Press
|
||||||
q.pointer.queue.Deliver(area, e, &q.handlers)
|
q.pointer.queue.Deliver(area, e, &q.handlers)
|
||||||
e.Type = pointer.Release
|
e.Kind = pointer.Release
|
||||||
q.pointer.queue.Deliver(area, e, &q.handlers)
|
q.pointer.queue.Deliver(area, e, &q.handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,7 +438,7 @@ func (q *Router) collect() {
|
|||||||
op := pointer.InputOp{
|
op := pointer.InputOp{
|
||||||
Tag: encOp.Refs[0].(event.Tag),
|
Tag: encOp.Refs[0].(event.Tag),
|
||||||
Grab: encOp.Data[1] != 0,
|
Grab: encOp.Data[1] != 0,
|
||||||
Types: pointer.Type(bo.Uint16(encOp.Data[2:])),
|
Kinds: pointer.Kind(bo.Uint16(encOp.Data[2:])),
|
||||||
ScrollBounds: image.Rectangle{
|
ScrollBounds: image.Rectangle{
|
||||||
Min: image.Point{
|
Min: image.Point{
|
||||||
X: int(int32(bo.Uint32(encOp.Data[4:]))),
|
X: int(int32(bo.Uint32(encOp.Data[4:]))),
|
||||||
@@ -490,11 +490,11 @@ func (q *Router) collect() {
|
|||||||
}
|
}
|
||||||
kc.softKeyboard(op.Show)
|
kc.softKeyboard(op.Show)
|
||||||
case ops.TypeKeyInput:
|
case ops.TypeKeyInput:
|
||||||
filter := encOp.Refs[1].(*key.Set)
|
filter := key.Set(*encOp.Refs[1].(*string))
|
||||||
op := key.InputOp{
|
op := key.InputOp{
|
||||||
Tag: encOp.Refs[0].(event.Tag),
|
Tag: encOp.Refs[0].(event.Tag),
|
||||||
Hint: key.InputHint(encOp.Data[1]),
|
Hint: key.InputHint(encOp.Data[1]),
|
||||||
Keys: *filter,
|
Keys: filter,
|
||||||
}
|
}
|
||||||
a := pc.currentArea()
|
a := pc.currentArea()
|
||||||
b := pc.currentAreaBounds()
|
b := pc.currentAreaBounds()
|
||||||
@@ -532,10 +532,10 @@ func (q *Router) collect() {
|
|||||||
|
|
||||||
// Semantic ops.
|
// Semantic ops.
|
||||||
case ops.TypeSemanticLabel:
|
case ops.TypeSemanticLabel:
|
||||||
lbl := encOp.Refs[0].(string)
|
lbl := *encOp.Refs[0].(*string)
|
||||||
pc.semanticLabel(lbl)
|
pc.semanticLabel(lbl)
|
||||||
case ops.TypeSemanticDesc:
|
case ops.TypeSemanticDesc:
|
||||||
desc := encOp.Refs[0].(string)
|
desc := *encOp.Refs[0].(*string)
|
||||||
pc.semanticDesc(desc)
|
pc.semanticDesc(desc)
|
||||||
case ops.TypeSemanticClass:
|
case ops.TypeSemanticClass:
|
||||||
class := semantic.ClassOp(encOp.Data[1])
|
class := semantic.ClassOp(encOp.Data[1])
|
||||||
@@ -546,11 +546,11 @@ func (q *Router) collect() {
|
|||||||
} else {
|
} else {
|
||||||
pc.semanticSelected(false)
|
pc.semanticSelected(false)
|
||||||
}
|
}
|
||||||
case ops.TypeSemanticDisabled:
|
case ops.TypeSemanticEnabled:
|
||||||
if encOp.Data[1] != 0 {
|
if encOp.Data[1] != 0 {
|
||||||
pc.semanticDisabled(true)
|
pc.semanticEnabled(true)
|
||||||
} else {
|
} else {
|
||||||
pc.semanticDisabled(false)
|
pc.semanticEnabled(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -593,16 +593,6 @@ func (h *handlerEvents) HadEvents() bool {
|
|||||||
func (h *handlerEvents) Events(k event.Tag) []event.Event {
|
func (h *handlerEvents) Events(k event.Tag) []event.Event {
|
||||||
if events, ok := h.handlers[k]; ok {
|
if events, ok := h.handlers[k]; ok {
|
||||||
h.handlers[k] = h.handlers[k][:0]
|
h.handlers[k] = h.handlers[k][:0]
|
||||||
// Schedule another frame if we delivered events to the user
|
|
||||||
// to flush half-updated state. This is important when an
|
|
||||||
// event changes UI state that has already been laid out. In
|
|
||||||
// the worst case, we waste a frame, increasing power usage.
|
|
||||||
//
|
|
||||||
// Gio is expected to grow the ability to construct
|
|
||||||
// frame-to-frame differences and only render to changed
|
|
||||||
// areas. In that case, the waste of a spurious frame should
|
|
||||||
// be minimal.
|
|
||||||
h.hadEvents = h.hadEvents || len(events) > 0
|
|
||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -74,11 +74,11 @@ func TestSemanticTree(t *testing.T) {
|
|||||||
|
|
||||||
func TestSemanticDescription(t *testing.T) {
|
func TestSemanticDescription(t *testing.T) {
|
||||||
var ops op.Ops
|
var ops op.Ops
|
||||||
pointer.InputOp{Tag: new(int), Types: pointer.Press | pointer.Release}.Add(&ops)
|
pointer.InputOp{Tag: new(int), Kinds: pointer.Press | pointer.Release}.Add(&ops)
|
||||||
semantic.DescriptionOp("description").Add(&ops)
|
semantic.DescriptionOp("description").Add(&ops)
|
||||||
semantic.LabelOp("label").Add(&ops)
|
semantic.LabelOp("label").Add(&ops)
|
||||||
semantic.Button.Add(&ops)
|
semantic.Button.Add(&ops)
|
||||||
semantic.DisabledOp(true).Add(&ops)
|
semantic.EnabledOp(false).Add(&ops)
|
||||||
semantic.SelectedOp(true).Add(&ops)
|
semantic.SelectedOp(true).Add(&ops)
|
||||||
var r Router
|
var r Router
|
||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
|
|||||||
@@ -36,16 +36,16 @@ const (
|
|||||||
// boolean state.
|
// boolean state.
|
||||||
type SelectedOp bool
|
type SelectedOp bool
|
||||||
|
|
||||||
// DisabledOp describes the disabled state.
|
// EnabledOp describes the enabled state.
|
||||||
type DisabledOp bool
|
type EnabledOp bool
|
||||||
|
|
||||||
func (l LabelOp) Add(o *op.Ops) {
|
func (l LabelOp) Add(o *op.Ops) {
|
||||||
data := ops.Write1(&o.Internal, ops.TypeSemanticLabelLen, string(l))
|
data := ops.Write1String(&o.Internal, ops.TypeSemanticLabelLen, string(l))
|
||||||
data[0] = byte(ops.TypeSemanticLabel)
|
data[0] = byte(ops.TypeSemanticLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d DescriptionOp) Add(o *op.Ops) {
|
func (d DescriptionOp) Add(o *op.Ops) {
|
||||||
data := ops.Write1(&o.Internal, ops.TypeSemanticDescLen, string(d))
|
data := ops.Write1String(&o.Internal, ops.TypeSemanticDescLen, string(d))
|
||||||
data[0] = byte(ops.TypeSemanticDesc)
|
data[0] = byte(ops.TypeSemanticDesc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,10 +63,10 @@ func (s SelectedOp) Add(o *op.Ops) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d DisabledOp) Add(o *op.Ops) {
|
func (e EnabledOp) Add(o *op.Ops) {
|
||||||
data := ops.Write(&o.Internal, ops.TypeSemanticDisabledLen)
|
data := ops.Write(&o.Internal, ops.TypeSemanticEnabledLen)
|
||||||
data[0] = byte(ops.TypeSemanticDisabled)
|
data[0] = byte(ops.TypeSemanticEnabled)
|
||||||
if d {
|
if e {
|
||||||
data[1] = 1
|
data[1] = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,30 @@ func ExampleStack() {
|
|||||||
// Expand: {(50,50) (100,100)}
|
// Expand: {(50,50) (100,100)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExampleBackground() {
|
||||||
|
gtx := layout.Context{
|
||||||
|
Ops: new(op.Ops),
|
||||||
|
Constraints: layout.Constraints{
|
||||||
|
Max: image.Point{X: 100, Y: 100},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.Background{}.Layout(gtx,
|
||||||
|
// Force widget to the same size as the second.
|
||||||
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
|
fmt.Printf("Expand: %v\n", gtx.Constraints)
|
||||||
|
return layoutWidget(gtx, 10, 10)
|
||||||
|
},
|
||||||
|
// Rigid 50x50 widget.
|
||||||
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layoutWidget(gtx, 50, 50)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Expand: {(50,50) (100,100)}
|
||||||
|
}
|
||||||
|
|
||||||
func ExampleList() {
|
func ExampleList() {
|
||||||
gtx := layout.Context{
|
gtx := layout.Context{
|
||||||
Ops: new(op.Ops),
|
Ops: new(op.Ops),
|
||||||
|
|||||||
+1
-1
@@ -144,7 +144,7 @@ func (l *List) Dragging() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *List) update(gtx Context) {
|
func (l *List) update(gtx Context) {
|
||||||
d := l.scroll.Scroll(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis))
|
d := l.scroll.Update(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis))
|
||||||
l.scrollDelta = d
|
l.scrollDelta = d
|
||||||
l.Position.Offset += d
|
l.Position.Offset += d
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-9
@@ -93,18 +93,18 @@ func TestListPosition(t *testing.T) {
|
|||||||
pointer.Event{
|
pointer.Event{
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: f32.Pt(0, 0),
|
Position: f32.Pt(0, 0),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Type: pointer.Scroll,
|
Kind: pointer.Scroll,
|
||||||
Scroll: f32.Pt(5, 0),
|
Scroll: f32.Pt(5, 0),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Position: f32.Pt(5, 0),
|
Position: f32.Pt(5, 0),
|
||||||
},
|
},
|
||||||
)},
|
)},
|
||||||
@@ -113,18 +113,18 @@ func TestListPosition(t *testing.T) {
|
|||||||
pointer.Event{
|
pointer.Event{
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: f32.Pt(0, 0),
|
Position: f32.Pt(0, 0),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Type: pointer.Scroll,
|
Kind: pointer.Scroll,
|
||||||
Scroll: f32.Pt(3, 0),
|
Scroll: f32.Pt(3, 0),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Position: f32.Pt(5, 0),
|
Position: f32.Pt(5, 0),
|
||||||
},
|
},
|
||||||
)},
|
)},
|
||||||
@@ -133,18 +133,18 @@ func TestListPosition(t *testing.T) {
|
|||||||
pointer.Event{
|
pointer.Event{
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: f32.Pt(0, 0),
|
Position: f32.Pt(0, 0),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Type: pointer.Scroll,
|
Kind: pointer.Scroll,
|
||||||
Scroll: f32.Pt(10, 0),
|
Scroll: f32.Pt(10, 0),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Position: f32.Pt(15, 0),
|
Position: f32.Pt(15, 0),
|
||||||
},
|
},
|
||||||
)},
|
)},
|
||||||
|
|||||||
@@ -118,3 +118,36 @@ func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions {
|
|||||||
Baseline: baseline,
|
Baseline: baseline,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Background lays out single child widget on top of a background,
|
||||||
|
// centering, if necessary.
|
||||||
|
type Background struct{}
|
||||||
|
|
||||||
|
// Layout a widget and then add a background to it.
|
||||||
|
func (Background) Layout(gtx Context, background, widget Widget) Dimensions {
|
||||||
|
macro := op.Record(gtx.Ops)
|
||||||
|
wdims := widget(gtx)
|
||||||
|
baseline := wdims.Baseline
|
||||||
|
call := macro.Stop()
|
||||||
|
|
||||||
|
cgtx := gtx
|
||||||
|
cgtx.Constraints.Min = gtx.Constraints.Constrain(wdims.Size)
|
||||||
|
bdims := background(cgtx)
|
||||||
|
|
||||||
|
if bdims.Size != wdims.Size {
|
||||||
|
p := image.Point{
|
||||||
|
X: (bdims.Size.X - wdims.Size.X) / 2,
|
||||||
|
Y: (bdims.Size.Y - wdims.Size.Y) / 2,
|
||||||
|
}
|
||||||
|
baseline += (bdims.Size.Y - wdims.Size.Y) / 2
|
||||||
|
trans := op.Offset(p).Push(gtx.Ops)
|
||||||
|
defer trans.Pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
call.Add(gtx.Ops)
|
||||||
|
|
||||||
|
return Dimensions{
|
||||||
|
Size: bdims.Size,
|
||||||
|
Baseline: baseline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package layout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gioui.org/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkStack(b *testing.B) {
|
||||||
|
gtx := Context{
|
||||||
|
Ops: new(op.Ops),
|
||||||
|
Constraints: Constraints{
|
||||||
|
Max: image.Point{X: 100, Y: 100},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
gtx.Ops.Reset()
|
||||||
|
|
||||||
|
Stack{}.Layout(gtx,
|
||||||
|
Expanded(emptyWidget{
|
||||||
|
Size: image.Point{X: 60, Y: 60},
|
||||||
|
}.Layout),
|
||||||
|
Stacked(emptyWidget{
|
||||||
|
Size: image.Point{X: 30, Y: 30},
|
||||||
|
}.Layout),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBackground(b *testing.B) {
|
||||||
|
gtx := Context{
|
||||||
|
Ops: new(op.Ops),
|
||||||
|
Constraints: Constraints{
|
||||||
|
Max: image.Point{X: 100, Y: 100},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
gtx.Ops.Reset()
|
||||||
|
|
||||||
|
Background{}.Layout(gtx,
|
||||||
|
emptyWidget{
|
||||||
|
Size: image.Point{X: 60, Y: 60},
|
||||||
|
}.Layout,
|
||||||
|
emptyWidget{
|
||||||
|
Size: image.Point{X: 30, Y: 30},
|
||||||
|
}.Layout,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type emptyWidget struct {
|
||||||
|
Size image.Point
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w emptyWidget) Layout(gtx Context) Dimensions {
|
||||||
|
return Dimensions{Size: w.Size}
|
||||||
|
}
|
||||||
+7
-1
@@ -29,7 +29,7 @@ type Op struct {
|
|||||||
type Stack struct {
|
type Stack struct {
|
||||||
ops *ops.Ops
|
ops *ops.Ops
|
||||||
id ops.StackID
|
id ops.StackID
|
||||||
macroID int
|
macroID uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
var pathSeed maphash.Seed
|
var pathSeed maphash.Seed
|
||||||
@@ -204,6 +204,9 @@ func (p *Path) Line(delta f32.Point) {
|
|||||||
|
|
||||||
// LineTo moves the pen to the absolute point specified, recording a line.
|
// LineTo moves the pen to the absolute point specified, recording a line.
|
||||||
func (p *Path) LineTo(to f32.Point) {
|
func (p *Path) LineTo(to f32.Point) {
|
||||||
|
if to == p.pen {
|
||||||
|
return
|
||||||
|
}
|
||||||
data := ops.WriteMulti(p.ops, scene.CommandSize+4)
|
data := ops.WriteMulti(p.ops, scene.CommandSize+4)
|
||||||
bo := binary.LittleEndian
|
bo := binary.LittleEndian
|
||||||
bo.PutUint32(data[0:], uint32(p.contour))
|
bo.PutUint32(data[0:], uint32(p.contour))
|
||||||
@@ -250,6 +253,9 @@ func (p *Path) Quad(ctrl, to f32.Point) {
|
|||||||
// QuadTo records a quadratic Bézier from the pen to end
|
// QuadTo records a quadratic Bézier from the pen to end
|
||||||
// with the control point ctrl, with absolute coordinates.
|
// with the control point ctrl, with absolute coordinates.
|
||||||
func (p *Path) QuadTo(ctrl, to f32.Point) {
|
func (p *Path) QuadTo(ctrl, to f32.Point) {
|
||||||
|
if ctrl == p.pen && to == p.pen {
|
||||||
|
return
|
||||||
|
}
|
||||||
data := ops.WriteMulti(p.ops, scene.CommandSize+4)
|
data := ops.WriteMulti(p.ops, scene.CommandSize+4)
|
||||||
bo := binary.LittleEndian
|
bo := binary.LittleEndian
|
||||||
bo.PutUint32(data[0:], uint32(p.contour))
|
bo.PutUint32(data[0:], uint32(p.contour))
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ type TransformOp struct {
|
|||||||
// TransformStack represents a TransformOp pushed on the transformation stack.
|
// TransformStack represents a TransformOp pushed on the transformation stack.
|
||||||
type TransformStack struct {
|
type TransformStack struct {
|
||||||
id ops.StackID
|
id ops.StackID
|
||||||
macroID int
|
macroID uint32
|
||||||
ops *ops.Ops
|
ops *ops.Ops
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,20 @@ import (
|
|||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ImageFilter is the scaling filter for images.
|
||||||
|
type ImageFilter byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FilterLinear uses linear interpolation for scaling.
|
||||||
|
FilterLinear ImageFilter = iota
|
||||||
|
// FilterNearest uses nearest neighbor interpolation for scaling.
|
||||||
|
FilterNearest
|
||||||
|
)
|
||||||
|
|
||||||
// ImageOp sets the brush to an image.
|
// ImageOp sets the brush to an image.
|
||||||
type ImageOp struct {
|
type ImageOp struct {
|
||||||
|
Filter ImageFilter
|
||||||
|
|
||||||
uniform bool
|
uniform bool
|
||||||
color color.NRGBA
|
color color.NRGBA
|
||||||
src *image.RGBA
|
src *image.RGBA
|
||||||
@@ -44,6 +56,14 @@ type LinearGradientOp struct {
|
|||||||
type PaintOp struct {
|
type PaintOp struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpacityStack represents an opacity applied to all painting operations
|
||||||
|
// until Pop is called.
|
||||||
|
type OpacityStack struct {
|
||||||
|
id ops.StackID
|
||||||
|
macroID uint32
|
||||||
|
ops *ops.Ops
|
||||||
|
}
|
||||||
|
|
||||||
// NewImageOp creates an ImageOp backed by src.
|
// NewImageOp creates an ImageOp backed by src.
|
||||||
//
|
//
|
||||||
// NewImageOp assumes the backing image is immutable, and may cache a
|
// NewImageOp assumes the backing image is immutable, and may cache a
|
||||||
@@ -95,6 +115,7 @@ func (i ImageOp) Add(o *op.Ops) {
|
|||||||
}
|
}
|
||||||
data := ops.Write2(&o.Internal, ops.TypeImageLen, i.src, i.handle)
|
data := ops.Write2(&o.Internal, ops.TypeImageLen, i.src, i.handle)
|
||||||
data[0] = byte(ops.TypeImage)
|
data[0] = byte(ops.TypeImage)
|
||||||
|
data[1] = byte(i.Filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c ColorOp) Add(o *op.Ops) {
|
func (c ColorOp) Add(o *op.Ops) {
|
||||||
@@ -145,3 +166,31 @@ func Fill(ops *op.Ops, c color.NRGBA) {
|
|||||||
ColorOp{Color: c}.Add(ops)
|
ColorOp{Color: c}.Add(ops)
|
||||||
PaintOp{}.Add(ops)
|
PaintOp{}.Add(ops)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PushOpacity creates a drawing layer with an opacity in the range [0;1].
|
||||||
|
// The layer includes every subsequent drawing operation until [OpacityStack.Pop]
|
||||||
|
// is called.
|
||||||
|
//
|
||||||
|
// The layer is drawn in two steps. First, the layer operations are
|
||||||
|
// drawn to a separate image. Then, the image is blended on top of
|
||||||
|
// the frame, with the opacity used as the blending factor.
|
||||||
|
func PushOpacity(o *op.Ops, opacity float32) OpacityStack {
|
||||||
|
if opacity > 1 {
|
||||||
|
opacity = 1
|
||||||
|
}
|
||||||
|
if opacity < 0 {
|
||||||
|
opacity = 0
|
||||||
|
}
|
||||||
|
id, macroID := ops.PushOp(&o.Internal, ops.OpacityStack)
|
||||||
|
data := ops.Write(&o.Internal, ops.TypePushOpacityLen)
|
||||||
|
bo := binary.LittleEndian
|
||||||
|
data[0] = byte(ops.TypePushOpacity)
|
||||||
|
bo.PutUint32(data[1:], math.Float32bits(opacity))
|
||||||
|
return OpacityStack{ops: &o.Internal, id: id, macroID: macroID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t OpacityStack) Pop() {
|
||||||
|
ops.PopOp(t.ops, ops.OpacityStack, t.id, t.macroID)
|
||||||
|
data := ops.Write(t.ops, ops.TypePopOpacityLen)
|
||||||
|
data[0] = byte(ops.TypePopOpacity)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+282
-242
@@ -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"
|
||||||
@@ -31,7 +36,8 @@ type document struct {
|
|||||||
lines []line
|
lines []line
|
||||||
alignment Alignment
|
alignment Alignment
|
||||||
// alignWidth is the width used when aligning text.
|
// alignWidth is the width used when aligning text.
|
||||||
alignWidth int
|
alignWidth int
|
||||||
|
unreadRuneCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// append adds the lines of other to the end of l and ensures they
|
// append adds the lines of other to the end of l and ensures they
|
||||||
@@ -47,6 +53,7 @@ func (l *document) reset() {
|
|||||||
l.lines = l.lines[:0]
|
l.lines = l.lines[:0]
|
||||||
l.alignment = Start
|
l.alignment = Start
|
||||||
l.alignWidth = 0
|
l.alignWidth = 0
|
||||||
|
l.unreadRuneCount = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func max(a, b int) int {
|
func max(a, b int) int {
|
||||||
@@ -74,8 +81,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
|
||||||
@@ -87,6 +95,55 @@ type line struct {
|
|||||||
yOffset int
|
yOffset int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// insertTrailingSyntheticNewline adds a synthetic newline to the final logical run of the line
|
||||||
|
// with the given shaping cluster index.
|
||||||
|
func (l *line) insertTrailingSyntheticNewline(newLineClusterIdx int) {
|
||||||
|
// If there was a newline at the end of this paragraph, insert a synthetic glyph representing it.
|
||||||
|
finalContentRun := len(l.runs) - 1
|
||||||
|
// If there was a trailing newline update the rune counts to include
|
||||||
|
// it on the last line of the paragraph.
|
||||||
|
l.runeCount += 1
|
||||||
|
l.runs[finalContentRun].Runes.Count += 1
|
||||||
|
|
||||||
|
syntheticGlyph := glyph{
|
||||||
|
id: 0,
|
||||||
|
clusterIndex: newLineClusterIdx,
|
||||||
|
glyphCount: 0,
|
||||||
|
runeCount: 1,
|
||||||
|
xAdvance: 0,
|
||||||
|
yAdvance: 0,
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: 0,
|
||||||
|
}
|
||||||
|
// Inset the synthetic newline glyph on the proper end of the run.
|
||||||
|
if l.runs[finalContentRun].Direction.Progression() == system.FromOrigin {
|
||||||
|
l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, syntheticGlyph)
|
||||||
|
} else {
|
||||||
|
// Ensure capacity.
|
||||||
|
l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, glyph{})
|
||||||
|
copy(l.runs[finalContentRun].Glyphs[1:], l.runs[finalContentRun].Glyphs)
|
||||||
|
l.runs[finalContentRun].Glyphs[0] = syntheticGlyph
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *line) setTruncatedCount(truncatedCount int) {
|
||||||
|
// If we've truncated the text with a truncator, adjust the rune counts within the
|
||||||
|
// truncator to make it represent the truncated text.
|
||||||
|
finalRunIdx := len(l.runs) - 1
|
||||||
|
l.runs[finalRunIdx].truncator = true
|
||||||
|
finalGlyphIdx := len(l.runs[finalRunIdx].Glyphs) - 1
|
||||||
|
// The run represents all of the truncated text.
|
||||||
|
l.runs[finalRunIdx].Runes.Count = truncatedCount
|
||||||
|
// Only the final glyph represents any runes, and it represents all truncated text.
|
||||||
|
for i := range l.runs[finalRunIdx].Glyphs {
|
||||||
|
if i == finalGlyphIdx {
|
||||||
|
l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = truncatedCount
|
||||||
|
} else {
|
||||||
|
l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Range describes the position and quantity of a range of text elements
|
// Range describes the position and quantity of a range of text elements
|
||||||
// within a larger slice. The unit is usually runes of unicode data or
|
// within a larger slice. The unit is usually runes of unicode data or
|
||||||
// glyphs of shaped font data.
|
// glyphs of shaped font data.
|
||||||
@@ -149,112 +206,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 +234,61 @@ 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
|
||||||
|
}
|
||||||
|
s.logger.Printf("loaded face %s(style:%s, weight:%d)", md.Typeface, md.Style, md.Weight)
|
||||||
|
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 +372,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 +399,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 +487,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,83 +558,72 @@ 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))
|
if params.MaxLines != 0 && hasNewline {
|
||||||
|
// If we might end up truncating a trailing newline, we must insert the truncator symbol
|
||||||
|
// on the final line (if we hit the limit).
|
||||||
|
params.forceTruncate = true
|
||||||
|
}
|
||||||
|
ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt))
|
||||||
|
|
||||||
didTruncate := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls))
|
hasTruncator := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls))
|
||||||
|
if hasTruncator && hasNewline {
|
||||||
if didTruncate && hasNewline {
|
// We have a truncator at the end of the line, so the newline is logically
|
||||||
// We've truncated the newline, since it was at the end and we've truncated some amount of runes
|
// truncated as well.
|
||||||
// before it.
|
|
||||||
truncated++
|
truncated++
|
||||||
hasNewline = false
|
hasNewline = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
isFinalLine := i == len(ls)-1
|
if otLine.lineHeight > maxHeight {
|
||||||
if isFinalLine && hasNewline {
|
maxHeight = otLine.lineHeight
|
||||||
// If there was a trailing newline update the rune counts to include
|
|
||||||
// it on the last line of the paragraph.
|
|
||||||
finalRunIdx := len(otLine.runs) - 1
|
|
||||||
otLine.runeCount += 1
|
|
||||||
otLine.runs[finalRunIdx].Runes.Count += 1
|
|
||||||
|
|
||||||
syntheticGlyph := glyph{
|
|
||||||
id: 0,
|
|
||||||
clusterIndex: len(txt),
|
|
||||||
glyphCount: 0,
|
|
||||||
runeCount: 1,
|
|
||||||
xAdvance: 0,
|
|
||||||
yAdvance: 0,
|
|
||||||
xOffset: 0,
|
|
||||||
yOffset: 0,
|
|
||||||
}
|
|
||||||
// Inset the synthetic newline glyph on the proper end of the run.
|
|
||||||
if otLine.runs[finalRunIdx].Direction.Progression() == system.FromOrigin {
|
|
||||||
otLine.runs[finalRunIdx].Glyphs = append(otLine.runs[finalRunIdx].Glyphs, syntheticGlyph)
|
|
||||||
} else {
|
|
||||||
// Ensure capacity.
|
|
||||||
otLine.runs[finalRunIdx].Glyphs = append(otLine.runs[finalRunIdx].Glyphs, glyph{})
|
|
||||||
copy(otLine.runs[finalRunIdx].Glyphs[1:], otLine.runs[finalRunIdx].Glyphs)
|
|
||||||
otLine.runs[finalRunIdx].Glyphs[0] = syntheticGlyph
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if isFinalLine && didTruncate {
|
if isFinalLine := i == len(ls)-1; isFinalLine {
|
||||||
// If we've truncated the text with a truncator, adjust the rune counts within the
|
if hasNewline {
|
||||||
// truncator to make it represent the truncated text.
|
otLine.insertTrailingSyntheticNewline(len(txt))
|
||||||
finalRunIdx := len(otLine.runs) - 1
|
}
|
||||||
otLine.runs[finalRunIdx].truncator = true
|
if hasTruncator {
|
||||||
finalGlyphIdx := len(otLine.runs[finalRunIdx].Glyphs) - 1
|
otLine.setTruncatedCount(truncated)
|
||||||
// The run represents all of the truncated text.
|
|
||||||
otLine.runs[finalRunIdx].Runes.Count = truncated
|
|
||||||
// Only the final glyph represents any runes, and it represents all truncated text.
|
|
||||||
for i := range otLine.runs[finalRunIdx].Glyphs {
|
|
||||||
if i == finalGlyphIdx {
|
|
||||||
otLine.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = truncated
|
|
||||||
} else {
|
|
||||||
otLine.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 +651,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 +715,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 +731,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 +766,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 +865,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 +873,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 +895,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 +903,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 +965,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 {
|
||||||
|
|||||||
+21
-20
@@ -3,9 +3,8 @@
|
|||||||
package text
|
package text
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"hash/maphash"
|
|
||||||
"image"
|
"image"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
giofont "gioui.org/font"
|
giofont "gioui.org/font"
|
||||||
"gioui.org/io/system"
|
"gioui.org/io/system"
|
||||||
@@ -88,32 +87,32 @@ type glyphValue[V any] struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type glyphLRU[V any] struct {
|
type glyphLRU[V any] struct {
|
||||||
seed maphash.Seed
|
seed uint64
|
||||||
cache lru[uint64, glyphValue[V]]
|
cache lru[uint64, glyphValue[V]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var seed uint32
|
||||||
|
|
||||||
// hashGlyphs computes a hash key based on the ID and X offset of
|
// hashGlyphs computes a hash key based on the ID and X offset of
|
||||||
// every glyph in the slice.
|
// every glyph in the slice.
|
||||||
func (c *glyphLRU[V]) hashGlyphs(gs []Glyph) uint64 {
|
func (c *glyphLRU[V]) hashGlyphs(gs []Glyph) uint64 {
|
||||||
if c.seed == (maphash.Seed{}) {
|
if c.seed == 0 {
|
||||||
c.seed = maphash.MakeSeed()
|
c.seed = uint64(atomic.AddUint32(&seed, 3900798947))
|
||||||
}
|
}
|
||||||
var h maphash.Hash
|
if len(gs) == 0 {
|
||||||
h.SetSeed(c.seed)
|
return 0
|
||||||
var b [8]byte
|
|
||||||
firstX := fixed.Int26_6(0)
|
|
||||||
for i, g := range gs {
|
|
||||||
if i == 0 {
|
|
||||||
firstX = g.X
|
|
||||||
}
|
|
||||||
// Cache glyph X offsets relative to the first glyph.
|
|
||||||
binary.LittleEndian.PutUint32(b[:4], uint32(g.X-firstX))
|
|
||||||
h.Write(b[:4])
|
|
||||||
binary.LittleEndian.PutUint64(b[:], uint64(g.ID))
|
|
||||||
h.Write(b[:])
|
|
||||||
}
|
}
|
||||||
sum := h.Sum64()
|
|
||||||
return sum
|
h := c.seed
|
||||||
|
firstX := gs[0].X
|
||||||
|
for _, g := range gs {
|
||||||
|
h += uint64(g.X - firstX)
|
||||||
|
h *= 6585573582091643
|
||||||
|
h += uint64(g.ID)
|
||||||
|
h *= 3650802748644053
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *glyphLRU[V]) Get(key uint64, gs []Glyph) (V, bool) {
|
func (c *glyphLRU[V]) Get(key uint64, gs []Glyph) (V, bool) {
|
||||||
@@ -160,6 +159,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 {
|
||||||
|
|||||||
+78
-26
@@ -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.
|
||||||
@@ -111,7 +121,7 @@ type Glyph struct {
|
|||||||
// belongs to. If Flags does not contain FlagClusterBreak, this value will
|
// belongs to. If Flags does not contain FlagClusterBreak, this value will
|
||||||
// always be zero. The final glyph in the cluster contains the runes count
|
// always be zero. The final glyph in the cluster contains the runes count
|
||||||
// for the entire cluster.
|
// for the entire cluster.
|
||||||
Runes int
|
Runes uint16
|
||||||
// Flags encode special properties of this glyph.
|
// Flags encode special properties of this glyph.
|
||||||
Flags Flags
|
Flags Flags
|
||||||
}
|
}
|
||||||
@@ -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,56 @@ type Shaper struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewShaper constructs a shaper with the provided collection of font faces
|
// ShaperOptions configure text shapers.
|
||||||
// available.
|
type ShaperOption func(*Shaper)
|
||||||
func NewShaper(collection []FontFace) *Shaper {
|
|
||||||
l := &Shaper{}
|
// NoSystemFonts can be used to disable system font loading.
|
||||||
for _, f := range collection {
|
func NoSystemFonts() ShaperOption {
|
||||||
l.shaper.Load(f)
|
return func(s *Shaper) {
|
||||||
|
s.config.disableSystemFonts = true
|
||||||
}
|
}
|
||||||
l.shaper.shaper.SetFontCacheSize(32)
|
}
|
||||||
l.reader = bufio.NewReader(nil)
|
|
||||||
|
// 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 options.
|
||||||
|
//
|
||||||
|
// NewShaper must be called after [app.NewWindow], unless the [NoSystemFonts]
|
||||||
|
// option is specified. This is an unfortunate restriction caused by some platforms
|
||||||
|
// such as Android.
|
||||||
|
func NewShaper(options ...ShaperOption) *Shaper {
|
||||||
|
l := &Shaper{}
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(l)
|
||||||
|
}
|
||||||
|
l.init()
|
||||||
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 +319,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 +330,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) {
|
||||||
@@ -306,11 +354,7 @@ func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) {
|
|||||||
unreadRunes++
|
unreadRunes++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastLineIdx := len(lines.lines) - 1
|
l.txt.unreadRuneCount = unreadRunes
|
||||||
lastRunIdx := len(lines.lines[lastLineIdx].runs) - 1
|
|
||||||
lastGlyphIdx := len(lines.lines[lastLineIdx].runs[lastRunIdx].Glyphs) - 1
|
|
||||||
lines.lines[lastLineIdx].runs[lastRunIdx].Runes.Count += unreadRunes
|
|
||||||
lines.lines[lastLineIdx].runs[lastRunIdx].Glyphs[lastGlyphIdx].runeCount += unreadRunes
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
l.txt.append(lines)
|
l.txt.append(lines)
|
||||||
@@ -334,16 +378,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 +402,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
|
||||||
}
|
}
|
||||||
@@ -422,7 +469,7 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
|
|||||||
Ascent: line.ascent,
|
Ascent: line.ascent,
|
||||||
Descent: line.descent,
|
Descent: line.descent,
|
||||||
Advance: g.xAdvance,
|
Advance: g.xAdvance,
|
||||||
Runes: g.runeCount,
|
Runes: uint16(g.runeCount),
|
||||||
Offset: fixed.Point26_6{
|
Offset: fixed.Point26_6{
|
||||||
X: g.xOffset,
|
X: g.xOffset,
|
||||||
Y: g.yOffset,
|
Y: g.yOffset,
|
||||||
@@ -457,6 +504,9 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
|
|||||||
}
|
}
|
||||||
if endOfCluster {
|
if endOfCluster {
|
||||||
glyph.Flags |= FlagClusterBreak
|
glyph.Flags |= FlagClusterBreak
|
||||||
|
if run.truncator {
|
||||||
|
glyph.Runes += uint16(l.txt.unreadRuneCount)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
glyph.Runes = 0
|
glyph.Runes = 0
|
||||||
}
|
}
|
||||||
@@ -523,6 +573,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 +590,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 {
|
||||||
|
|||||||
+109
-28
@@ -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),
|
||||||
@@ -50,9 +51,9 @@ func TestWrappingTruncation(t *testing.T) {
|
|||||||
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)
|
||||||
if g.Flags&FlagTruncator != 0 && g.Flags&FlagClusterBreak != 0 {
|
if g.Flags&FlagTruncator != 0 && g.Flags&FlagClusterBreak != 0 {
|
||||||
truncatedRunes += g.Runes
|
truncatedRunes += int(g.Runes)
|
||||||
} else {
|
} else {
|
||||||
untruncatedRunes += g.Runes
|
untruncatedRunes += int(g.Runes)
|
||||||
}
|
}
|
||||||
if g.Flags&FlagLineBreak != 0 {
|
if g.Flags&FlagLineBreak != 0 {
|
||||||
lineCount++
|
lineCount++
|
||||||
@@ -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),
|
||||||
@@ -116,9 +117,9 @@ func TestWrappingForcedTruncation(t *testing.T) {
|
|||||||
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)
|
||||||
if g.Flags&FlagTruncator != 0 && g.Flags&FlagClusterBreak != 0 {
|
if g.Flags&FlagTruncator != 0 && g.Flags&FlagClusterBreak != 0 {
|
||||||
truncatedRunes += g.Runes
|
truncatedRunes += int(g.Runes)
|
||||||
} else {
|
} else {
|
||||||
untruncatedRunes += g.Runes
|
untruncatedRunes += int(g.Runes)
|
||||||
}
|
}
|
||||||
if g.Flags&FlagLineBreak != 0 {
|
if g.Flags&FlagLineBreak != 0 {
|
||||||
lineCount++
|
lineCount++
|
||||||
@@ -153,23 +154,53 @@ func TestWrappingForcedTruncation(t *testing.T) {
|
|||||||
// consistently and does not create spurious lines of text.
|
// consistently and does not create spurious lines of text.
|
||||||
func TestShapingNewlineHandling(t *testing.T) {
|
func TestShapingNewlineHandling(t *testing.T) {
|
||||||
type testcase struct {
|
type testcase struct {
|
||||||
textInput string
|
textInput string
|
||||||
expectedLines int
|
expectedLines int
|
||||||
expectedGlyphs int
|
expectedGlyphs int
|
||||||
|
maxLines int
|
||||||
|
expectedTruncated int
|
||||||
}
|
}
|
||||||
for _, tc := range []testcase{
|
for _, tc := range []testcase{
|
||||||
{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},
|
||||||
|
{textInput: "\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 1},
|
||||||
|
{textInput: "\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 2},
|
||||||
|
{textInput: "\n\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 3},
|
||||||
|
{textInput: "a\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 1},
|
||||||
|
{textInput: "a\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 2},
|
||||||
|
{textInput: "a\n\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 3},
|
||||||
|
{textInput: "\n", expectedLines: 1, maxLines: 2, expectedGlyphs: 2},
|
||||||
|
{textInput: "\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 2, expectedTruncated: 1},
|
||||||
|
{textInput: "\n\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 2, expectedTruncated: 2},
|
||||||
|
{textInput: "a\n", expectedLines: 1, maxLines: 2, expectedGlyphs: 3},
|
||||||
|
{textInput: "a\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 3, expectedTruncated: 1},
|
||||||
|
{textInput: "a\n\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 3, expectedTruncated: 2},
|
||||||
} {
|
} {
|
||||||
t.Run(fmt.Sprintf("%q", tc.textInput), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%q-maxLines%d", tc.textInput, tc.maxLines), 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
|
||||||
|
truncated := 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)
|
||||||
|
if g.Flags&FlagTruncator == 0 {
|
||||||
|
runes += int(g.Runes)
|
||||||
|
} else {
|
||||||
|
truncated += int(g.Runes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if expected := len([]rune(tc.textInput)) - tc.expectedTruncated; expected != runes {
|
||||||
|
t.Errorf("expected %d runes, got %d", expected, runes)
|
||||||
|
}
|
||||||
|
if truncated != tc.expectedTruncated {
|
||||||
|
t.Errorf("expected %d truncated runes, got %d", tc.expectedTruncated, truncated)
|
||||||
}
|
}
|
||||||
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,36 +222,34 @@ 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if count := strings.Count(tc.textInput, "\n"); found != count {
|
if count := strings.Count(tc.textInput, "\n"); found != count && tc.maxLines == 0 {
|
||||||
t.Errorf("expected %d paragraph breaks, found %d", count, found)
|
t.Errorf("expected %d paragraph breaks, found %d", count, found)
|
||||||
|
} else if tc.maxLines > 0 && found > tc.maxLines {
|
||||||
|
t.Errorf("expected %d paragraph breaks due to truncation, found %d", tc.maxLines, found)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache.LayoutString(Parameters{
|
params := Parameters{
|
||||||
Alignment: Middle,
|
Alignment: Middle,
|
||||||
PxPerEm: fixed.I(10),
|
PxPerEm: fixed.I(10),
|
||||||
MinWidth: 200,
|
MinWidth: 200,
|
||||||
MaxWidth: 200,
|
MaxWidth: 200,
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}, tc.textInput)
|
MaxLines: tc.maxLines,
|
||||||
|
}
|
||||||
|
cache.LayoutString(params, tc.textInput)
|
||||||
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
|
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
|
||||||
t.Errorf("shaping string %q created %d lines", tc.textInput, lineCount)
|
t.Errorf("shaping string %q created %d lines", tc.textInput, lineCount)
|
||||||
}
|
}
|
||||||
checkGlyphs()
|
checkGlyphs()
|
||||||
|
|
||||||
cache.Layout(Parameters{
|
cache.Layout(params, strings.NewReader(tc.textInput))
|
||||||
Alignment: Middle,
|
|
||||||
PxPerEm: fixed.I(10),
|
|
||||||
MinWidth: 200,
|
|
||||||
MaxWidth: 200,
|
|
||||||
Locale: english,
|
|
||||||
}, strings.NewReader(tc.textInput))
|
|
||||||
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
|
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
|
||||||
t.Errorf("shaping reader %q created %d lines", tc.textInput, lineCount)
|
t.Errorf("shaping reader %q created %d lines", tc.textInput, lineCount)
|
||||||
}
|
}
|
||||||
@@ -234,7 +263,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 +302,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 +368,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 +493,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 +562,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{}
|
||||||
@@ -490,7 +571,7 @@ func TestShapeStringRuneAccounting(t *testing.T) {
|
|||||||
}
|
}
|
||||||
totalRunes := 0
|
totalRunes := 0
|
||||||
for _, g := range glyphs {
|
for _, g := range glyphs {
|
||||||
totalRunes += g.Runes
|
totalRunes += int(g.Runes)
|
||||||
}
|
}
|
||||||
if inputRunes := len([]rune(tc.input)); totalRunes != inputRunes {
|
if inputRunes := len([]rune(tc.input)); totalRunes != inputRunes {
|
||||||
t.Errorf("input contained %d runes, but glyphs contained %d", inputRunes, totalRunes)
|
t.Errorf("input contained %d runes, but glyphs contained %d", inputRunes, totalRunes)
|
||||||
|
|||||||
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)
|
||||||
|
|||||||
+9
-12
@@ -11,15 +11,15 @@ type Bool struct {
|
|||||||
Value bool
|
Value bool
|
||||||
|
|
||||||
clk Clickable
|
clk Clickable
|
||||||
|
|
||||||
changed bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changed reports whether Value has changed since the last
|
// Update the widget state and report whether Value was changed.
|
||||||
// call to Changed.
|
func (b *Bool) Update(gtx layout.Context) bool {
|
||||||
func (b *Bool) Changed() bool {
|
changed := false
|
||||||
changed := b.changed
|
for b.clk.Clicked(gtx) {
|
||||||
b.changed = false
|
b.Value = !b.Value
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
return changed
|
return changed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,13 +43,10 @@ func (b *Bool) History() []Press {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
||||||
|
b.Update(gtx)
|
||||||
dims := b.clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
dims := b.clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
for b.clk.Clicked() {
|
|
||||||
b.Value = !b.Value
|
|
||||||
b.changed = true
|
|
||||||
}
|
|
||||||
semantic.SelectedOp(b.Value).Add(gtx.Ops)
|
semantic.SelectedOp(b.Value).Add(gtx.Ops)
|
||||||
semantic.DisabledOp(gtx.Queue == nil).Add(gtx.Ops)
|
semantic.EnabledOp(gtx.Queue != nil).Add(gtx.Ops)
|
||||||
return w(gtx)
|
return w(gtx)
|
||||||
})
|
})
|
||||||
return dims
|
return dims
|
||||||
|
|||||||
+76
-68
@@ -17,17 +17,16 @@ import (
|
|||||||
|
|
||||||
// Clickable represents a clickable area.
|
// Clickable represents a clickable area.
|
||||||
type Clickable struct {
|
type Clickable struct {
|
||||||
click gesture.Click
|
click gesture.Click
|
||||||
clicks []Click
|
// clicks is for saved clicks to support Clicked.
|
||||||
// prevClicks is the index into clicks that marks the clicks
|
clicks []Click
|
||||||
// from the most recent Layout call. prevClicks is used to keep
|
history []Press
|
||||||
// clicks bounded.
|
|
||||||
prevClicks int
|
|
||||||
history []Press
|
|
||||||
|
|
||||||
keyTag struct{}
|
keyTag struct{}
|
||||||
requestFocus bool
|
requestFocus bool
|
||||||
focused bool
|
requestClicks int
|
||||||
|
focused bool
|
||||||
|
pressedKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click represents a click.
|
// Click represents a click.
|
||||||
@@ -49,26 +48,24 @@ type Press struct {
|
|||||||
Cancelled bool
|
Cancelled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click executes a simple programmatic click
|
// Click executes a simple programmatic click.
|
||||||
func (b *Clickable) Click() {
|
func (b *Clickable) Click() {
|
||||||
b.clicks = append(b.clicks, Click{
|
b.requestClicks++
|
||||||
Modifiers: 0,
|
|
||||||
NumClicks: 1,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clicked reports whether there are pending clicks as would be
|
// Clicked reports whether there are pending clicks. If so, Clicked
|
||||||
// reported by Clicks. If so, Clicked removes the earliest click.
|
// removes the earliest click.
|
||||||
func (b *Clickable) Clicked() bool {
|
func (b *Clickable) Clicked(gtx layout.Context) bool {
|
||||||
if len(b.clicks) == 0 {
|
if len(b.clicks) > 0 {
|
||||||
return false
|
b.clicks = b.clicks[1:]
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
n := copy(b.clicks, b.clicks[1:])
|
b.clicks = b.Update(gtx)
|
||||||
b.clicks = b.clicks[:n]
|
if len(b.clicks) > 0 {
|
||||||
if b.prevClicks > 0 {
|
b.clicks = b.clicks[1:]
|
||||||
b.prevClicks--
|
return true
|
||||||
}
|
}
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hovered reports whether a pointer is over the element.
|
// Hovered reports whether a pointer is over the element.
|
||||||
@@ -91,44 +88,44 @@ func (b *Clickable) Focused() bool {
|
|||||||
return b.focused
|
return b.focused
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clicks returns and clear the clicks since the last call to Clicks.
|
|
||||||
func (b *Clickable) Clicks() []Click {
|
|
||||||
clicks := b.clicks
|
|
||||||
b.clicks = nil
|
|
||||||
b.prevClicks = 0
|
|
||||||
return clicks
|
|
||||||
}
|
|
||||||
|
|
||||||
// History is the past pointer presses useful for drawing markers.
|
// History is the past pointer presses useful for drawing markers.
|
||||||
// History is retained for a short duration (about a second).
|
// History is retained for a short duration (about a second).
|
||||||
func (b *Clickable) History() []Press {
|
func (b *Clickable) History() []Press {
|
||||||
return b.history
|
return b.history
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout and update the button state
|
// Layout and update the button state.
|
||||||
func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
||||||
b.update(gtx)
|
b.Update(gtx)
|
||||||
m := op.Record(gtx.Ops)
|
m := op.Record(gtx.Ops)
|
||||||
dims := w(gtx)
|
dims := w(gtx)
|
||||||
c := m.Stop()
|
c := m.Stop()
|
||||||
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
|
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
|
||||||
disabled := gtx.Queue == nil
|
enabled := gtx.Queue != nil
|
||||||
semantic.DisabledOp(disabled).Add(gtx.Ops)
|
semantic.EnabledOp(enabled).Add(gtx.Ops)
|
||||||
b.click.Add(gtx.Ops)
|
b.click.Add(gtx.Ops)
|
||||||
if !disabled {
|
if enabled {
|
||||||
keys := key.Set("⏎|Space")
|
keys := key.Set("⏎|Space")
|
||||||
if !b.focused {
|
if !b.focused {
|
||||||
keys = ""
|
keys = ""
|
||||||
}
|
}
|
||||||
key.InputOp{Tag: &b.keyTag, Keys: keys}.Add(gtx.Ops)
|
key.InputOp{Tag: &b.keyTag, Keys: keys}.Add(gtx.Ops)
|
||||||
if b.requestFocus {
|
|
||||||
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
|
|
||||||
b.requestFocus = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
b.focused = false
|
|
||||||
}
|
}
|
||||||
c.Add(gtx.Ops)
|
c.Add(gtx.Ops)
|
||||||
|
return dims
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the button state by processing events, and return the resulting
|
||||||
|
// clicks, if any.
|
||||||
|
func (b *Clickable) Update(gtx layout.Context) []Click {
|
||||||
|
b.clicks = nil
|
||||||
|
if gtx.Queue == nil {
|
||||||
|
b.focused = false
|
||||||
|
}
|
||||||
|
if b.requestFocus {
|
||||||
|
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
|
||||||
|
b.requestFocus = false
|
||||||
|
}
|
||||||
for len(b.history) > 0 {
|
for len(b.history) > 0 {
|
||||||
c := b.history[0]
|
c := b.history[0]
|
||||||
if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second {
|
if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second {
|
||||||
@@ -137,34 +134,31 @@ func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimension
|
|||||||
n := copy(b.history, b.history[1:])
|
n := copy(b.history, b.history[1:])
|
||||||
b.history = b.history[:n]
|
b.history = b.history[:n]
|
||||||
}
|
}
|
||||||
return dims
|
var clicks []Click
|
||||||
}
|
if c := b.requestClicks; c > 0 {
|
||||||
|
b.requestClicks = 0
|
||||||
// update the button state by processing events.
|
clicks = append(clicks, Click{
|
||||||
func (b *Clickable) update(gtx layout.Context) {
|
NumClicks: c,
|
||||||
// Flush clicks from before the last update.
|
})
|
||||||
n := copy(b.clicks, b.clicks[b.prevClicks:])
|
}
|
||||||
b.clicks = b.clicks[:n]
|
for _, e := range b.click.Update(gtx) {
|
||||||
b.prevClicks = n
|
switch e.Kind {
|
||||||
|
case gesture.KindClick:
|
||||||
for _, e := range b.click.Events(gtx) {
|
|
||||||
switch e.Type {
|
|
||||||
case gesture.TypeClick:
|
|
||||||
b.clicks = append(b.clicks, Click{
|
|
||||||
Modifiers: e.Modifiers,
|
|
||||||
NumClicks: e.NumClicks,
|
|
||||||
})
|
|
||||||
if l := len(b.history); l > 0 {
|
if l := len(b.history); l > 0 {
|
||||||
b.history[l-1].End = gtx.Now
|
b.history[l-1].End = gtx.Now
|
||||||
}
|
}
|
||||||
case gesture.TypeCancel:
|
clicks = append(clicks, Click{
|
||||||
|
Modifiers: e.Modifiers,
|
||||||
|
NumClicks: e.NumClicks,
|
||||||
|
})
|
||||||
|
case gesture.KindCancel:
|
||||||
for i := range b.history {
|
for i := range b.history {
|
||||||
b.history[i].Cancelled = true
|
b.history[i].Cancelled = true
|
||||||
if b.history[i].End.IsZero() {
|
if b.history[i].End.IsZero() {
|
||||||
b.history[i].End = gtx.Now
|
b.history[i].End = gtx.Now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case gesture.TypePress:
|
case gesture.KindPress:
|
||||||
if e.Source == pointer.Mouse {
|
if e.Source == pointer.Mouse {
|
||||||
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
|
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
|
||||||
}
|
}
|
||||||
@@ -178,17 +172,31 @@ func (b *Clickable) update(gtx layout.Context) {
|
|||||||
switch e := e.(type) {
|
switch e := e.(type) {
|
||||||
case key.FocusEvent:
|
case key.FocusEvent:
|
||||||
b.focused = e.Focus
|
b.focused = e.Focus
|
||||||
|
if !b.focused {
|
||||||
|
b.pressedKey = ""
|
||||||
|
}
|
||||||
case key.Event:
|
case key.Event:
|
||||||
if !b.focused || e.State != key.Release {
|
if !b.focused {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if e.Name != key.NameReturn && e.Name != key.NameSpace {
|
if e.Name != key.NameReturn && e.Name != key.NameSpace {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
b.clicks = append(b.clicks, Click{
|
switch e.State {
|
||||||
Modifiers: e.Modifiers,
|
case key.Press:
|
||||||
NumClicks: 1,
|
b.pressedKey = e.Name
|
||||||
})
|
case key.Release:
|
||||||
|
if b.pressedKey != e.Name {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// only register a key as a click if the key was pressed and released while this button was focused
|
||||||
|
b.pressedKey = ""
|
||||||
|
clicks = append(clicks, Click{
|
||||||
|
Modifiers: e.Modifiers,
|
||||||
|
NumClicks: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return clicks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package widget_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gioui.org/io/key"
|
||||||
|
"gioui.org/io/router"
|
||||||
|
"gioui.org/io/system"
|
||||||
|
"gioui.org/layout"
|
||||||
|
"gioui.org/op"
|
||||||
|
"gioui.org/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClickable(t *testing.T) {
|
||||||
|
var (
|
||||||
|
ops op.Ops
|
||||||
|
r router.Router
|
||||||
|
b1 widget.Clickable
|
||||||
|
b2 widget.Clickable
|
||||||
|
)
|
||||||
|
gtx := layout.NewContext(&ops, system.FrameEvent{Queue: &r})
|
||||||
|
layout := func() {
|
||||||
|
b1.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Dimensions{Size: image.Pt(100, 100)}
|
||||||
|
})
|
||||||
|
// buttons are on top of each other but we only use focus and keyevents, so this is fine
|
||||||
|
b2.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Dimensions{Size: image.Pt(100, 100)}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
frame := func() {
|
||||||
|
ops.Reset()
|
||||||
|
layout()
|
||||||
|
r.Frame(gtx.Ops)
|
||||||
|
}
|
||||||
|
// frame: request focus for button 1
|
||||||
|
b1.Focus()
|
||||||
|
frame()
|
||||||
|
// frame: gain focus for button 1
|
||||||
|
frame()
|
||||||
|
if !b1.Focused() {
|
||||||
|
t.Error("button 1 did not gain focus")
|
||||||
|
}
|
||||||
|
if b2.Focused() {
|
||||||
|
t.Error("button 2 should not have focus")
|
||||||
|
}
|
||||||
|
// frame: press & release return
|
||||||
|
frame()
|
||||||
|
r.Queue(
|
||||||
|
key.Event{
|
||||||
|
Name: key.NameReturn,
|
||||||
|
State: key.Press,
|
||||||
|
},
|
||||||
|
key.Event{
|
||||||
|
Name: key.NameReturn,
|
||||||
|
State: key.Release,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if !b1.Clicked(gtx) {
|
||||||
|
t.Error("button 1 did not get clicked when it got return press & release")
|
||||||
|
}
|
||||||
|
if b2.Clicked(gtx) {
|
||||||
|
t.Error("button 2 got clicked when it did not have focus")
|
||||||
|
}
|
||||||
|
// frame: press return down
|
||||||
|
r.Queue(
|
||||||
|
key.Event{
|
||||||
|
Name: key.NameReturn,
|
||||||
|
State: key.Press,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
frame()
|
||||||
|
if b1.Clicked(gtx) {
|
||||||
|
t.Error("button 1 got clicked, even if it only got return press")
|
||||||
|
}
|
||||||
|
// frame: request focus for button 2
|
||||||
|
b2.Focus()
|
||||||
|
frame()
|
||||||
|
// frame: gain focus for button 2
|
||||||
|
frame()
|
||||||
|
if b1.Focused() {
|
||||||
|
t.Error("button 1 should not have focus")
|
||||||
|
}
|
||||||
|
if !b2.Focused() {
|
||||||
|
t.Error("button 2 did not gain focus")
|
||||||
|
}
|
||||||
|
// frame: release return
|
||||||
|
r.Queue(
|
||||||
|
key.Event{
|
||||||
|
Name: key.NameReturn,
|
||||||
|
State: key.Release,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
frame()
|
||||||
|
if b1.Clicked(gtx) {
|
||||||
|
t.Error("button 1 got clicked, even if it had lost focus")
|
||||||
|
}
|
||||||
|
if b2.Clicked(gtx) {
|
||||||
|
t.Error("button 2 should not have been clicked, as it only got return release")
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
-22
@@ -12,12 +12,11 @@ import (
|
|||||||
|
|
||||||
// Decorations handles the states of window decorations.
|
// Decorations handles the states of window decorations.
|
||||||
type Decorations struct {
|
type Decorations struct {
|
||||||
clicks []Clickable
|
clicks map[int]*Clickable
|
||||||
resize [8]struct {
|
resize [8]struct {
|
||||||
gesture.Hover
|
gesture.Hover
|
||||||
gesture.Drag
|
gesture.Drag
|
||||||
}
|
}
|
||||||
actions system.Action
|
|
||||||
maximized bool
|
maximized bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,22 +34,13 @@ func (d *Decorations) Clickable(action system.Action) *Clickable {
|
|||||||
panic(fmt.Errorf("not a single action"))
|
panic(fmt.Errorf("not a single action"))
|
||||||
}
|
}
|
||||||
idx := bits.TrailingZeros(uint(action))
|
idx := bits.TrailingZeros(uint(action))
|
||||||
if n := idx - len(d.clicks); n >= 0 {
|
click, found := d.clicks[idx]
|
||||||
d.clicks = append(d.clicks, make([]Clickable, n+1)...)
|
if !found {
|
||||||
}
|
click = new(Clickable)
|
||||||
click := &d.clicks[idx]
|
if d.clicks == nil {
|
||||||
if click.Clicked() {
|
d.clicks = make(map[int]*Clickable)
|
||||||
if action == system.ActionMaximize {
|
|
||||||
if d.maximized {
|
|
||||||
d.maximized = false
|
|
||||||
d.actions |= system.ActionUnmaximize
|
|
||||||
} else {
|
|
||||||
d.maximized = true
|
|
||||||
d.actions |= system.ActionMaximize
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
d.actions |= action
|
|
||||||
}
|
}
|
||||||
|
d.clicks[idx] = click
|
||||||
}
|
}
|
||||||
return click
|
return click
|
||||||
}
|
}
|
||||||
@@ -66,11 +56,27 @@ func (d *Decorations) Perform(actions system.Action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions returns the set of actions activated by the user.
|
// Update the state and return the set of actions activated by the user.
|
||||||
func (d *Decorations) Actions() system.Action {
|
func (d *Decorations) Update(gtx layout.Context) system.Action {
|
||||||
a := d.actions
|
var actions system.Action
|
||||||
d.actions = 0
|
for idx, clk := range d.clicks {
|
||||||
return a
|
if !clk.Clicked(gtx) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
action := system.Action(1 << idx)
|
||||||
|
switch {
|
||||||
|
case action == system.ActionMaximize && d.maximized:
|
||||||
|
action = system.ActionUnmaximize
|
||||||
|
case action == system.ActionUnmaximize && !d.maximized:
|
||||||
|
action = system.ActionMaximize
|
||||||
|
}
|
||||||
|
switch action {
|
||||||
|
case system.ActionMaximize, system.ActionUnmaximize:
|
||||||
|
d.maximized = !d.maximized
|
||||||
|
}
|
||||||
|
actions |= action
|
||||||
|
}
|
||||||
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maximized returns whether the window is maximized.
|
// Maximized returns whether the window is maximized.
|
||||||
|
|||||||
+26
-37
@@ -17,41 +17,16 @@ type Draggable struct {
|
|||||||
// Type contains the MIME type and matches transfer.SourceOp.
|
// Type contains the MIME type and matches transfer.SourceOp.
|
||||||
Type string
|
Type string
|
||||||
|
|
||||||
handle struct{}
|
handle struct{}
|
||||||
drag gesture.Drag
|
drag gesture.Drag
|
||||||
click f32.Point
|
click f32.Point
|
||||||
pos f32.Point
|
pos f32.Point
|
||||||
requested bool
|
|
||||||
request string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Draggable) Layout(gtx layout.Context, w, drag layout.Widget) layout.Dimensions {
|
func (d *Draggable) Layout(gtx layout.Context, w, drag layout.Widget) layout.Dimensions {
|
||||||
if gtx.Queue == nil {
|
if gtx.Queue == nil {
|
||||||
return w(gtx)
|
return w(gtx)
|
||||||
}
|
}
|
||||||
pos := d.pos
|
|
||||||
for _, ev := range d.drag.Events(gtx.Metric, gtx.Queue, gesture.Both) {
|
|
||||||
switch ev.Type {
|
|
||||||
case pointer.Press:
|
|
||||||
d.click = ev.Position
|
|
||||||
pos = f32.Point{}
|
|
||||||
case pointer.Drag, pointer.Release:
|
|
||||||
pos = ev.Position.Sub(d.click)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.pos = pos
|
|
||||||
|
|
||||||
for _, ev := range gtx.Queue.Events(&d.handle) {
|
|
||||||
switch e := ev.(type) {
|
|
||||||
case transfer.RequestEvent:
|
|
||||||
d.requested = true
|
|
||||||
d.request = e.Type
|
|
||||||
case transfer.CancelEvent:
|
|
||||||
d.requested = false
|
|
||||||
d.request = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dims := w(gtx)
|
dims := w(gtx)
|
||||||
|
|
||||||
stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops)
|
stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops)
|
||||||
@@ -64,7 +39,7 @@ func (d *Draggable) Layout(gtx layout.Context, w, drag layout.Widget) layout.Dim
|
|||||||
|
|
||||||
if drag != nil && d.drag.Pressed() {
|
if drag != nil && d.drag.Pressed() {
|
||||||
rec := op.Record(gtx.Ops)
|
rec := op.Record(gtx.Ops)
|
||||||
op.Offset(pos.Round()).Add(gtx.Ops)
|
op.Offset(d.pos.Round()).Add(gtx.Ops)
|
||||||
drag(gtx)
|
drag(gtx)
|
||||||
op.Defer(gtx.Ops, rec.Stop())
|
op.Defer(gtx.Ops, rec.Stop())
|
||||||
}
|
}
|
||||||
@@ -77,13 +52,27 @@ func (d *Draggable) Dragging() bool {
|
|||||||
return d.drag.Dragging()
|
return d.drag.Dragging()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Requested returns the MIME type, if any, for which the Draggable was requested to offer data.
|
// Update the draggable and returns the MIME type for which the Draggable was
|
||||||
func (d *Draggable) Requested() (mime string, requested bool) {
|
// requested to offer data, if any
|
||||||
mime = d.request
|
func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
|
||||||
requested = d.requested
|
pos := d.pos
|
||||||
d.requested = false
|
for _, ev := range d.drag.Update(gtx.Metric, gtx.Queue, gesture.Both) {
|
||||||
d.request = ""
|
switch ev.Kind {
|
||||||
return
|
case pointer.Press:
|
||||||
|
d.click = ev.Position
|
||||||
|
pos = f32.Point{}
|
||||||
|
case pointer.Drag, pointer.Release:
|
||||||
|
pos = ev.Position.Sub(d.click)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.pos = pos
|
||||||
|
|
||||||
|
for _, ev := range gtx.Queue.Events(&d.handle) {
|
||||||
|
if e, ok := ev.(transfer.RequestEvent); ok {
|
||||||
|
return e.Type, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Offer the data ready for a drop. Must be called after being Requested.
|
// Offer the data ready for a drop. Must be called after being Requested.
|
||||||
|
|||||||
+3
-3
@@ -39,15 +39,15 @@ func TestDraggable(t *testing.T) {
|
|||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(10, 10),
|
Position: f32.Pt(10, 10),
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(20, 10),
|
Position: f32.Pt(20, 10),
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Position: f32.Pt(20, 10),
|
Position: f32.Pt(20, 10),
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
ofr := &offer{data: "hello"}
|
ofr := &offer{data: "hello"}
|
||||||
|
|||||||
+27
-16
@@ -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.
|
||||||
@@ -219,7 +225,7 @@ func (e *Editor) processPointer(gtx layout.Context) {
|
|||||||
axis = gesture.Vertical
|
axis = gesture.Vertical
|
||||||
smin, smax = sbounds.Min.Y, sbounds.Max.Y
|
smin, smax = sbounds.Min.Y, sbounds.Max.Y
|
||||||
}
|
}
|
||||||
sdist := e.scroller.Scroll(gtx.Metric, gtx, gtx.Now, axis)
|
sdist := e.scroller.Update(gtx.Metric, gtx, gtx.Now, axis)
|
||||||
var soff int
|
var soff int
|
||||||
if e.SingleLine {
|
if e.SingleLine {
|
||||||
e.text.ScrollRel(sdist, 0)
|
e.text.ScrollRel(sdist, 0)
|
||||||
@@ -232,8 +238,8 @@ func (e *Editor) processPointer(gtx layout.Context) {
|
|||||||
switch evt := evt.(type) {
|
switch evt := evt.(type) {
|
||||||
case gesture.ClickEvent:
|
case gesture.ClickEvent:
|
||||||
switch {
|
switch {
|
||||||
case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse,
|
case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse,
|
||||||
evt.Type == gesture.TypeClick && evt.Source != pointer.Mouse:
|
evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse:
|
||||||
prevCaretPos, _ := e.text.Selection()
|
prevCaretPos, _ := e.text.Selection()
|
||||||
e.blinkStart = gtx.Now
|
e.blinkStart = gtx.Now
|
||||||
e.text.MoveCoord(image.Point{
|
e.text.MoveCoord(image.Point{
|
||||||
@@ -272,10 +278,10 @@ func (e *Editor) processPointer(gtx layout.Context) {
|
|||||||
case pointer.Event:
|
case pointer.Event:
|
||||||
release := false
|
release := false
|
||||||
switch {
|
switch {
|
||||||
case evt.Type == pointer.Release && evt.Source == pointer.Mouse:
|
case evt.Kind == pointer.Release && evt.Source == pointer.Mouse:
|
||||||
release = true
|
release = true
|
||||||
fallthrough
|
fallthrough
|
||||||
case evt.Type == pointer.Drag && evt.Source == pointer.Mouse:
|
case evt.Kind == pointer.Drag && evt.Source == pointer.Mouse:
|
||||||
if e.dragging {
|
if e.dragging {
|
||||||
e.blinkStart = gtx.Now
|
e.blinkStart = gtx.Now
|
||||||
e.text.MoveCoord(image.Point{
|
e.text.MoveCoord(image.Point{
|
||||||
@@ -299,10 +305,10 @@ func (e *Editor) processPointer(gtx layout.Context) {
|
|||||||
|
|
||||||
func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event {
|
func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event {
|
||||||
var combinedEvents []event.Event
|
var combinedEvents []event.Event
|
||||||
for _, evt := range e.clicker.Events(gtx) {
|
for _, evt := range e.clicker.Update(gtx) {
|
||||||
combinedEvents = append(combinedEvents, evt)
|
combinedEvents = append(combinedEvents, evt)
|
||||||
}
|
}
|
||||||
for _, evt := range e.dragger.Events(gtx.Metric, gtx, gesture.Both) {
|
for _, evt := range e.dragger.Update(gtx.Metric, gtx, gesture.Both) {
|
||||||
combinedEvents = append(combinedEvents, evt)
|
combinedEvents = append(combinedEvents, evt)
|
||||||
}
|
}
|
||||||
return combinedEvents
|
return combinedEvents
|
||||||
@@ -504,20 +510,17 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout lays out the editor using the provided textMaterial as the paint material
|
// Update the state of the editor in response to input events.
|
||||||
// for the text glyphs+caret and the selectMaterial as the paint material for the
|
func (e *Editor) Update(gtx layout.Context) {
|
||||||
// selection rectangle.
|
|
||||||
func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
|
|
||||||
e.initBuffer()
|
e.initBuffer()
|
||||||
e.text.Update(gtx, lt, font, size, e.processEvents)
|
e.processEvents(gtx)
|
||||||
|
|
||||||
dims := e.layout(gtx, textMaterial, selectMaterial)
|
|
||||||
|
|
||||||
if e.focused {
|
if e.focused {
|
||||||
// Notify IME of selection if it changed.
|
// Notify IME of selection if it changed.
|
||||||
newSel := e.ime.selection
|
newSel := e.ime.selection
|
||||||
@@ -543,8 +546,16 @@ func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, siz
|
|||||||
|
|
||||||
e.updateSnippet(gtx, e.ime.start, e.ime.end)
|
e.updateSnippet(gtx, e.ime.start, e.ime.end)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return dims
|
// Layout lays out the editor using the provided textMaterial as the paint material
|
||||||
|
// for the text glyphs+caret and the selectMaterial as the paint material for the
|
||||||
|
// selection rectangle.
|
||||||
|
func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
|
||||||
|
e.Update(gtx)
|
||||||
|
|
||||||
|
e.text.Layout(gtx, lt, font, size)
|
||||||
|
return e.layout(gtx, textMaterial, selectMaterial)
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateSnippet adds a key.SnippetOp if the snippet content or position
|
// updateSnippet adds a key.SnippetOp if the snippet content or position
|
||||||
|
|||||||
+29
-71
@@ -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)
|
||||||
@@ -139,7 +139,7 @@ func TestEditorReadOnly(t *testing.T) {
|
|||||||
// Type some new characters.
|
// Type some new characters.
|
||||||
gtx.Ops.Reset()
|
gtx.Ops.Reset()
|
||||||
gtx.Queue = &testQueue{events: []event.Event{key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"}}}
|
gtx.Queue = &testQueue{events: []event.Event{key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"}}}
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
e.Update(gtx)
|
||||||
textContent2 := e.Text()
|
textContent2 := e.Text()
|
||||||
if textContent2 != textContent {
|
if textContent2 != textContent {
|
||||||
t.Errorf("readonly editor modified by key.EditEvent")
|
t.Errorf("readonly editor modified by key.EditEvent")
|
||||||
@@ -159,22 +159,22 @@ func TestEditorReadOnly(t *testing.T) {
|
|||||||
gtx.Ops.Reset()
|
gtx.Ops.Reset()
|
||||||
gtx.Queue = &testQueue{events: []event.Event{
|
gtx.Queue = &testQueue{events: []event.Event{
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Position: f32.Pt(float32(dims.Size.X)*.5, 5),
|
Position: f32.Pt(float32(dims.Size.X)*.5, 5),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Drag,
|
Kind: pointer.Drag,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Position: layout.FPt(dims.Size).Mul(.5),
|
Position: layout.FPt(dims.Size).Mul(.5),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Position: layout.FPt(dims.Size).Mul(.5),
|
Position: layout.FPt(dims.Size).Mul(.5),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
e.Update(gtx)
|
||||||
cStart3, cEnd3 := e.Selection()
|
cStart3, cEnd3 := e.Selection()
|
||||||
if cStart3 == cStart2 || cEnd3 == cEnd2 {
|
if cStart3 == cStart2 || cEnd3 == cEnd2 {
|
||||||
t.Errorf("expected mouse interaction to change selection.")
|
t.Errorf("expected mouse interaction to change selection.")
|
||||||
@@ -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{}
|
||||||
|
|
||||||
@@ -285,44 +285,10 @@ func TestEditor(t *testing.T) {
|
|||||||
e.MoveCaret(-3, -3)
|
e.MoveCaret(-3, -3)
|
||||||
assertCaret(t, e, 1, 1, len("æbc\na"))
|
assertCaret(t, e, 1, 1, len("æbc\na"))
|
||||||
e.text.Mask = '*'
|
e.text.Mask = '*'
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
e.Update(gtx)
|
||||||
assertCaret(t, e, 1, 1, len("æbc\na"))
|
assertCaret(t, e, 1, 1, len("æbc\na"))
|
||||||
e.MoveCaret(-3, -3)
|
e.MoveCaret(-3, -3)
|
||||||
assertCaret(t, e, 0, 2, len("æb"))
|
assertCaret(t, e, 0, 2, len("æb"))
|
||||||
/*
|
|
||||||
NOTE(whereswaldon): it isn't possible to check the raw glyph data
|
|
||||||
like this anymore. How should we handle this?
|
|
||||||
e.Mask = '\U0001F92B'
|
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{},op.CallOp{})
|
|
||||||
e.moveEnd(selectionClear)
|
|
||||||
assertCaret(t, e, 0, 3, len("æbc"))
|
|
||||||
|
|
||||||
// When a password mask is applied, it should replace all visible glyphs
|
|
||||||
spaces := 0
|
|
||||||
for _, r := range textSample {
|
|
||||||
if unicode.IsSpace(r) {
|
|
||||||
spaces++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nonSpaces := len([]rune(textSample)) - spaces
|
|
||||||
glyphCounts := make(map[int]int)
|
|
||||||
// This loop assumes a single-run text, which we know is safe here.
|
|
||||||
for _, line := range e.lines {
|
|
||||||
for _, glyph := range line.Runs[0].Glyphs {
|
|
||||||
glyphCounts[int(glyph.ID)]++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(glyphCounts) > 2 {
|
|
||||||
t.Errorf("masked text contained glyphs other than mask and whitespace")
|
|
||||||
}
|
|
||||||
|
|
||||||
for gid, count := range glyphCounts {
|
|
||||||
if count != spaces && count != nonSpaces {
|
|
||||||
t.Errorf("glyph with id %d occurred %d times, expected either %d or %d", gid, count, spaces, nonSpaces)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
// Test that moveLine applies x offsets from previous moves.
|
// Test that moveLine applies x offsets from previous moves.
|
||||||
e.SetText("long line\nshort")
|
e.SetText("long line\nshort")
|
||||||
e.SetCaret(0, 0)
|
e.SetCaret(0, 0)
|
||||||
@@ -349,7 +315,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 +385,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 +474,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 +507,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 +554,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,11 +646,8 @@ 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())
|
|
||||||
fontSize := unit.Sp(10)
|
|
||||||
font := font.Font{}
|
|
||||||
e.SetText(t)
|
e.SetText(t)
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
e.Update(gtx)
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
for ii, tt := range tests {
|
for ii, tt := range tests {
|
||||||
@@ -784,11 +748,8 @@ 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())
|
|
||||||
fontSize := unit.Sp(10)
|
|
||||||
font := font.Font{}
|
|
||||||
e.SetText(t)
|
e.SetText(t)
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
e.Update(gtx)
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
for ii, tt := range tests {
|
for ii, tt := range tests {
|
||||||
@@ -874,11 +835,8 @@ 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())
|
|
||||||
fontSize := unit.Sp(10)
|
|
||||||
font := font.Font{}
|
|
||||||
e.SetText(t)
|
e.SetText(t)
|
||||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
e.Update(gtx)
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
for ii, tt := range tests {
|
for ii, tt := range tests {
|
||||||
@@ -928,7 +886,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)
|
||||||
|
|
||||||
@@ -946,13 +904,13 @@ g 2 4 6 8 g
|
|||||||
events: []event.Event{
|
events: []event.Event{
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Time: tim,
|
Time: tim,
|
||||||
Position: f32.Pt(textWidth(e, startPos.lineCol.line, 0, startPos.lineCol.col), textBaseline(e, startPos.lineCol.line)),
|
Position: f32.Pt(textWidth(e, startPos.lineCol.line, 0, startPos.lineCol.col), textBaseline(e, startPos.lineCol.line)),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Time: tim,
|
Time: tim,
|
||||||
Position: f32.Pt(textWidth(e, endPos.lineCol.line, 0, endPos.lineCol.col), textBaseline(e, endPos.lineCol.line)),
|
Position: f32.Pt(textWidth(e, endPos.lineCol.line, 0, endPos.lineCol.col), textBaseline(e, endPos.lineCol.line)),
|
||||||
@@ -1026,7 +984,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 +1072,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 +1103,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 +1127,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{})
|
||||||
|
|||||||
+53
-54
@@ -20,8 +20,6 @@ type Enum struct {
|
|||||||
focus string
|
focus string
|
||||||
focused bool
|
focused bool
|
||||||
|
|
||||||
changed bool
|
|
||||||
|
|
||||||
keys []*enumKey
|
keys []*enumKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,11 +38,55 @@ func (e *Enum) index(k string) *enumKey {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changed reports whether Value has changed by user interaction since the last
|
// Update the state and report whether Value has changed by user interaction.
|
||||||
// call to Changed.
|
func (e *Enum) Update(gtx layout.Context) bool {
|
||||||
func (e *Enum) Changed() bool {
|
if gtx.Queue == nil {
|
||||||
changed := e.changed
|
e.focused = false
|
||||||
e.changed = false
|
}
|
||||||
|
e.hovering = false
|
||||||
|
changed := false
|
||||||
|
for _, state := range e.keys {
|
||||||
|
for _, ev := range state.click.Update(gtx) {
|
||||||
|
switch ev.Kind {
|
||||||
|
case gesture.KindPress:
|
||||||
|
if ev.Source == pointer.Mouse {
|
||||||
|
key.FocusOp{Tag: &state.tag}.Add(gtx.Ops)
|
||||||
|
}
|
||||||
|
case gesture.KindClick:
|
||||||
|
if state.key != e.Value {
|
||||||
|
e.Value = state.key
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ev := range gtx.Events(&state.tag) {
|
||||||
|
switch ev := ev.(type) {
|
||||||
|
case key.FocusEvent:
|
||||||
|
if ev.Focus {
|
||||||
|
e.focused = true
|
||||||
|
e.focus = state.key
|
||||||
|
} else if state.key == e.focus {
|
||||||
|
e.focused = false
|
||||||
|
}
|
||||||
|
case key.Event:
|
||||||
|
if !e.focused || ev.State != key.Release {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ev.Name != key.NameReturn && ev.Name != key.NameSpace {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if state.key != e.Value {
|
||||||
|
e.Value = state.key
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if state.click.Hovered() {
|
||||||
|
e.hovered = state.key
|
||||||
|
e.hovering = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +102,7 @@ func (e *Enum) Focused() (string, bool) {
|
|||||||
|
|
||||||
// Layout adds the event handler for the key k.
|
// Layout adds the event handler for the key k.
|
||||||
func (e *Enum) Layout(gtx layout.Context, k string, content layout.Widget) layout.Dimensions {
|
func (e *Enum) Layout(gtx layout.Context, k string, content layout.Widget) layout.Dimensions {
|
||||||
|
e.Update(gtx)
|
||||||
m := op.Record(gtx.Ops)
|
m := op.Record(gtx.Ops)
|
||||||
dims := content(gtx)
|
dims := content(gtx)
|
||||||
c := m.Stop()
|
c := m.Stop()
|
||||||
@@ -73,57 +116,13 @@ func (e *Enum) Layout(gtx layout.Context, k string, content layout.Widget) layou
|
|||||||
e.keys = append(e.keys, state)
|
e.keys = append(e.keys, state)
|
||||||
}
|
}
|
||||||
clk := &state.click
|
clk := &state.click
|
||||||
for _, ev := range clk.Events(gtx) {
|
|
||||||
switch ev.Type {
|
|
||||||
case gesture.TypePress:
|
|
||||||
if ev.Source == pointer.Mouse {
|
|
||||||
key.FocusOp{Tag: &state.tag}.Add(gtx.Ops)
|
|
||||||
}
|
|
||||||
case gesture.TypeClick:
|
|
||||||
if state.key != e.Value {
|
|
||||||
e.Value = state.key
|
|
||||||
e.changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, ev := range gtx.Events(&state.tag) {
|
|
||||||
switch ev := ev.(type) {
|
|
||||||
case key.FocusEvent:
|
|
||||||
if ev.Focus {
|
|
||||||
e.focused = true
|
|
||||||
e.focus = state.key
|
|
||||||
} else if state.key == e.focus {
|
|
||||||
e.focused = false
|
|
||||||
}
|
|
||||||
case key.Event:
|
|
||||||
if !e.focused || ev.State != key.Release {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if ev.Name != key.NameReturn && ev.Name != key.NameSpace {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if state.key != e.Value {
|
|
||||||
e.Value = state.key
|
|
||||||
e.changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if clk.Hovered() {
|
|
||||||
e.hovered = k
|
|
||||||
e.hovering = true
|
|
||||||
} else if e.hovered == k {
|
|
||||||
e.hovering = false
|
|
||||||
}
|
|
||||||
|
|
||||||
clk.Add(gtx.Ops)
|
clk.Add(gtx.Ops)
|
||||||
disabled := gtx.Queue == nil
|
enabled := gtx.Queue != nil
|
||||||
if !disabled {
|
if enabled {
|
||||||
key.InputOp{Tag: &state.tag, Keys: "⏎|Space"}.Add(gtx.Ops)
|
key.InputOp{Tag: &state.tag, Keys: "⏎|Space"}.Add(gtx.Ops)
|
||||||
} else if e.focus == k {
|
|
||||||
e.focused = false
|
|
||||||
}
|
}
|
||||||
semantic.SelectedOp(k == e.Value).Add(gtx.Ops)
|
semantic.SelectedOp(k == e.Value).Add(gtx.Ops)
|
||||||
semantic.DisabledOp(disabled).Add(gtx.Ops)
|
semantic.EnabledOp(enabled).Add(gtx.Ops)
|
||||||
c.Add(gtx.Ops)
|
c.Add(gtx.Ops)
|
||||||
|
|
||||||
return dims
|
return dims
|
||||||
|
|||||||
+8
-10
@@ -47,23 +47,21 @@ func ExampleClickable_passthrough() {
|
|||||||
pointer.Event{
|
pointer.Event{
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: pointer.ButtonPrimary,
|
Buttons: pointer.ButtonPrimary,
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Position: f32.Pt(50, 50),
|
Position: f32.Pt(50, 50),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
// The second layout ensures that the click event is registered by the buttons.
|
|
||||||
widget()
|
|
||||||
|
|
||||||
if button1.Clicked() {
|
if button1.Clicked(gtx) {
|
||||||
fmt.Println("button1 clicked!")
|
fmt.Println("button1 clicked!")
|
||||||
}
|
}
|
||||||
if button2.Clicked() {
|
if button2.Clicked(gtx) {
|
||||||
fmt.Println("button2 clicked!")
|
fmt.Println("button2 clicked!")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +93,7 @@ func ExampleDraggable_Layout() {
|
|||||||
drag.Layout(gtx, w, w)
|
drag.Layout(gtx, w, w)
|
||||||
// drag must respond with an Offer event when requested.
|
// drag must respond with an Offer event when requested.
|
||||||
// Use the drag method for this.
|
// Use the drag method for this.
|
||||||
if m, ok := drag.Requested(); ok {
|
if m, ok := drag.Update(gtx); ok {
|
||||||
drag.Offer(gtx.Ops, m, offer{Data: "hello world"})
|
drag.Offer(gtx.Ops, m, offer{Data: "hello world"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,15 +123,15 @@ func ExampleDraggable_Layout() {
|
|||||||
// Send drag and drop gesture events.
|
// Send drag and drop gesture events.
|
||||||
r.Queue(
|
r.Queue(
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Press,
|
Kind: pointer.Press,
|
||||||
Position: f32.Pt(5, 5), // in the drag area
|
Position: f32.Pt(5, 5), // in the drag area
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Position: f32.Pt(5, 5), // in the drop area
|
Position: f32.Pt(5, 5), // in the drop area
|
||||||
},
|
},
|
||||||
pointer.Event{
|
pointer.Event{
|
||||||
Type: pointer.Release,
|
Kind: pointer.Release,
|
||||||
Position: f32.Pt(30, 30), // in the drop area
|
Position: f32.Pt(30, 30), // in the drop area
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
+29
-63
@@ -9,59 +9,29 @@ import (
|
|||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
|
"gioui.org/unit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Float is for selecting a value in a range.
|
// Float is for selecting a value in a range.
|
||||||
type Float struct {
|
type Float struct {
|
||||||
|
// Value is the value of the Float, in the [0; 1] range.
|
||||||
Value float32
|
Value float32
|
||||||
|
|
||||||
drag gesture.Drag
|
drag gesture.Drag
|
||||||
pos float32 // position normalized to [0, 1]
|
axis layout.Axis
|
||||||
length float32
|
length float32
|
||||||
changed bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dragging returns whether the value is being interacted with.
|
// Dragging returns whether the value is being interacted with.
|
||||||
func (f *Float) Dragging() bool { return f.drag.Dragging() }
|
func (f *Float) Dragging() bool { return f.drag.Dragging() }
|
||||||
|
|
||||||
// Layout updates the value according to drag events along the f's main axis.
|
func (f *Float) Layout(gtx layout.Context, axis layout.Axis, pointerMargin unit.Dp) layout.Dimensions {
|
||||||
//
|
f.Update(gtx)
|
||||||
// The range of f is set by the minimum constraints main axis value.
|
|
||||||
func (f *Float) Layout(gtx layout.Context, axis layout.Axis, min, max float32, invert bool, pointerMargin int) layout.Dimensions {
|
|
||||||
size := gtx.Constraints.Min
|
size := gtx.Constraints.Min
|
||||||
f.length = float32(axis.Convert(size).X)
|
f.length = float32(axis.Convert(size).X)
|
||||||
|
f.axis = axis
|
||||||
|
|
||||||
var de *pointer.Event
|
margin := axis.Convert(image.Pt(gtx.Dp(pointerMargin), 0))
|
||||||
for _, e := range f.drag.Events(gtx.Metric, gtx, gesture.Axis(axis)) {
|
|
||||||
if e.Type == pointer.Press || e.Type == pointer.Drag {
|
|
||||||
de = &e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
value := f.Value
|
|
||||||
if de != nil {
|
|
||||||
xy := de.Position.X
|
|
||||||
if axis == layout.Vertical {
|
|
||||||
xy = f.length - de.Position.Y
|
|
||||||
}
|
|
||||||
if invert {
|
|
||||||
xy = f.length - xy
|
|
||||||
}
|
|
||||||
f.pos = xy / f.length
|
|
||||||
value = min + (max-min)*f.pos
|
|
||||||
} else if min != max {
|
|
||||||
f.pos = (value - min) / (max - min)
|
|
||||||
}
|
|
||||||
// Unconditionally call setValue in case min, max, or value changed.
|
|
||||||
f.setValue(value, min, max)
|
|
||||||
|
|
||||||
if f.pos < 0 {
|
|
||||||
f.pos = 0
|
|
||||||
} else if f.pos > 1 {
|
|
||||||
f.pos = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
margin := axis.Convert(image.Pt(pointerMargin, 0))
|
|
||||||
rect := image.Rectangle{
|
rect := image.Rectangle{
|
||||||
Min: margin.Mul(-1),
|
Min: margin.Mul(-1),
|
||||||
Max: size.Add(margin),
|
Max: size.Add(margin),
|
||||||
@@ -72,30 +42,26 @@ func (f *Float) Layout(gtx layout.Context, axis layout.Axis, min, max float32, i
|
|||||||
return layout.Dimensions{Size: size}
|
return layout.Dimensions{Size: size}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Float) setValue(value, min, max float32) {
|
// Update the Value according to drag events along the f's main axis.
|
||||||
if min > max {
|
// The return value reports whether the value was changed.
|
||||||
min, max = max, min
|
//
|
||||||
|
// The range of f is set by the minimum constraints main axis value.
|
||||||
|
func (f *Float) Update(gtx layout.Context) bool {
|
||||||
|
changed := false
|
||||||
|
for _, e := range f.drag.Update(gtx.Metric, gtx, gesture.Axis(f.axis)) {
|
||||||
|
if f.length > 0 && (e.Kind == pointer.Press || e.Kind == pointer.Drag) {
|
||||||
|
pos := e.Position.X
|
||||||
|
if f.axis == layout.Vertical {
|
||||||
|
pos = f.length - e.Position.Y
|
||||||
|
}
|
||||||
|
f.Value = pos / f.length
|
||||||
|
if f.Value < 0 {
|
||||||
|
f.Value = 0
|
||||||
|
} else if f.Value > 1 {
|
||||||
|
f.Value = 1
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if value < min {
|
|
||||||
value = min
|
|
||||||
} else if value > max {
|
|
||||||
value = max
|
|
||||||
}
|
|
||||||
if f.Value != value {
|
|
||||||
f.Value = value
|
|
||||||
f.changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pos reports the selected position.
|
|
||||||
func (f *Float) Pos() float32 {
|
|
||||||
return f.pos * f.length
|
|
||||||
}
|
|
||||||
|
|
||||||
// Changed reports whether the value has changed since
|
|
||||||
// the last call to Changed.
|
|
||||||
func (f *Float) Changed() bool {
|
|
||||||
changed := f.changed
|
|
||||||
f.changed = false
|
|
||||||
return changed
|
return changed
|
||||||
}
|
}
|
||||||
|
|||||||
+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},
|
||||||
|
|||||||
+38
-14
@@ -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}
|
||||||
@@ -125,27 +134,42 @@ type textIterator struct {
|
|||||||
|
|
||||||
// processGlyph checks whether the glyph is visible within the iterator's configured
|
// processGlyph checks whether the glyph is visible within the iterator's configured
|
||||||
// viewport and (if so) updates the iterator's text dimensions to include the glyph.
|
// viewport and (if so) updates the iterator's text dimensions to include the glyph.
|
||||||
func (it *textIterator) processGlyph(g text.Glyph, ok bool) (_ text.Glyph, visibleOrBefore bool) {
|
func (it *textIterator) processGlyph(g text.Glyph, ok bool) (visibleOrBefore bool) {
|
||||||
if it.maxLines > 0 {
|
if it.maxLines > 0 {
|
||||||
if g.Flags&text.FlagTruncator != 0 && g.Flags&text.FlagClusterBreak != 0 {
|
if g.Flags&text.FlagTruncator != 0 && g.Flags&text.FlagClusterBreak != 0 {
|
||||||
// A glyph carrying both of these flags provides the count of truncated runes.
|
// A glyph carrying both of these flags provides the count of truncated runes.
|
||||||
it.truncated = g.Runes
|
it.truncated = int(g.Runes)
|
||||||
}
|
}
|
||||||
if g.Flags&text.FlagLineBreak != 0 {
|
if g.Flags&text.FlagLineBreak != 0 {
|
||||||
it.linesSeen++
|
it.linesSeen++
|
||||||
}
|
}
|
||||||
if it.linesSeen == it.maxLines && g.Flags&text.FlagParagraphBreak != 0 {
|
if it.linesSeen == it.maxLines && g.Flags&text.FlagParagraphBreak != 0 {
|
||||||
return g, false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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()),
|
||||||
@@ -167,7 +191,7 @@ func (it *textIterator) processGlyph(g text.Glyph, ok bool) (_ text.Glyph, visib
|
|||||||
it.bounds.Max.X = max(it.bounds.Max.X, logicalBounds.Max.X)
|
it.bounds.Max.X = max(it.bounds.Max.X, logicalBounds.Max.X)
|
||||||
it.bounds.Max.Y = max(it.bounds.Max.Y, logicalBounds.Max.Y)
|
it.bounds.Max.Y = max(it.bounds.Max.Y, logicalBounds.Max.Y)
|
||||||
}
|
}
|
||||||
return g, ok && !below
|
return ok && !below
|
||||||
}
|
}
|
||||||
|
|
||||||
func fixedToFloat(i fixed.Int26_6) float32 {
|
func fixedToFloat(i fixed.Int26_6) float32 {
|
||||||
@@ -182,7 +206,7 @@ func fixedToFloat(i fixed.Int26_6) float32 {
|
|||||||
// This design is awkward, but prevents the line slice from escaping
|
// This design is awkward, but prevents the line slice from escaping
|
||||||
// to the heap.
|
// to the heap.
|
||||||
func (it *textIterator) paintGlyph(gtx layout.Context, shaper *text.Shaper, glyph text.Glyph, line []text.Glyph) ([]text.Glyph, bool) {
|
func (it *textIterator) paintGlyph(gtx layout.Context, shaper *text.Shaper, glyph text.Glyph, line []text.Glyph) ([]text.Glyph, bool) {
|
||||||
_, visibleOrBefore := it.processGlyph(glyph, true)
|
visibleOrBefore := it.processGlyph(glyph, true)
|
||||||
if it.visible {
|
if it.visible {
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
it.lineOff = f32.Point{X: fixedToFloat(glyph.X), Y: float32(glyph.Y)}.Sub(layout.FPt(it.viewport.Min))
|
it.lineOff = f32.Point{X: fixedToFloat(glyph.X), Y: float32(glyph.Y)}.Sub(layout.FPt(it.viewport.Min))
|
||||||
|
|||||||
+65
-4
@@ -146,10 +146,7 @@ func TestGlyphIterator(t *testing.T) {
|
|||||||
glyphs := getGlyphs(16, 0, maxWidth, text.Start, tc.str)
|
glyphs := getGlyphs(16, 0, maxWidth, text.Start, tc.str)
|
||||||
it := textIterator{viewport: tc.viewport, maxLines: tc.maxLines}
|
it := textIterator{viewport: tc.viewport, maxLines: tc.maxLines}
|
||||||
for i, g := range glyphs {
|
for i, g := range glyphs {
|
||||||
gOut, ok := it.processGlyph(g, true)
|
ok := it.processGlyph(g, true)
|
||||||
if gOut != g {
|
|
||||||
t.Errorf("textIterator modified glyphs[%d], original:\n%#+v, modified:\n%#+v", i, g, gOut)
|
|
||||||
}
|
|
||||||
if !ok && i != tc.stopAtGlyph {
|
if !ok && i != tc.stopAtGlyph {
|
||||||
t.Errorf("expected iterator to stop at glyph %d, stopped at %d", tc.stopAtGlyph, i)
|
t.Errorf("expected iterator to stop at glyph %d, stopped at %d", tc.stopAtGlyph, i)
|
||||||
}
|
}
|
||||||
@@ -166,3 +163,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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+5
-5
@@ -61,8 +61,8 @@ func (s *Scrollbar) Layout(gtx layout.Context, axis layout.Axis, viewportStart,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Jump to a click in the track.
|
// Jump to a click in the track.
|
||||||
for _, event := range s.track.Events(gtx) {
|
for _, event := range s.track.Update(gtx) {
|
||||||
if event.Type != gesture.TypeClick ||
|
if event.Kind != gesture.KindClick ||
|
||||||
event.Modifiers != key.Modifiers(0) ||
|
event.Modifiers != key.Modifiers(0) ||
|
||||||
event.NumClicks > 1 {
|
event.NumClicks > 1 {
|
||||||
continue
|
continue
|
||||||
@@ -80,8 +80,8 @@ func (s *Scrollbar) Layout(gtx layout.Context, axis layout.Axis, viewportStart,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Offset to account for any drags.
|
// Offset to account for any drags.
|
||||||
for _, event := range s.drag.Events(gtx.Metric, gtx, gesture.Axis(axis)) {
|
for _, event := range s.drag.Update(gtx.Metric, gtx, gesture.Axis(axis)) {
|
||||||
switch event.Type {
|
switch event.Kind {
|
||||||
case pointer.Drag:
|
case pointer.Drag:
|
||||||
case pointer.Release, pointer.Cancel:
|
case pointer.Release, pointer.Cancel:
|
||||||
s.dragging = false
|
s.dragging = false
|
||||||
@@ -136,7 +136,7 @@ func (s *Scrollbar) Layout(gtx layout.Context, axis layout.Axis, viewportStart,
|
|||||||
|
|
||||||
// Process events from the indicator so that hover is
|
// Process events from the indicator so that hover is
|
||||||
// detected properly.
|
// detected properly.
|
||||||
_ = s.indicator.Events(gtx)
|
_ = s.indicator.Update(gtx)
|
||||||
|
|
||||||
return layout.Dimensions{}
|
return layout.Dimensions{}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-19
@@ -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 {
|
||||||
@@ -91,9 +93,8 @@ func IconButton(th *Theme, button *widget.Clickable, icon *widget.Icon, descript
|
|||||||
func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) layout.Dimensions {
|
func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) layout.Dimensions {
|
||||||
return button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
semantic.Button.Add(gtx.Ops)
|
semantic.Button.Add(gtx.Ops)
|
||||||
constraints := gtx.Constraints
|
return layout.Background{}.Layout(gtx,
|
||||||
return layout.Stack{}.Layout(gtx,
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
|
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
|
||||||
if button.Hovered() || button.Focused() {
|
if button.Hovered() || button.Focused() {
|
||||||
paint.Fill(gtx.Ops, f32color.Hovered(color.NRGBA{}))
|
paint.Fill(gtx.Ops, f32color.Hovered(color.NRGBA{}))
|
||||||
@@ -102,11 +103,8 @@ func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) la
|
|||||||
drawInk(gtx, c)
|
drawInk(gtx, c)
|
||||||
}
|
}
|
||||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||||
}),
|
},
|
||||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
w,
|
||||||
gtx.Constraints = constraints
|
|
||||||
return w(gtx)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -129,8 +127,8 @@ func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Di
|
|||||||
min := gtx.Constraints.Min
|
min := gtx.Constraints.Min
|
||||||
return b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
semantic.Button.Add(gtx.Ops)
|
semantic.Button.Add(gtx.Ops)
|
||||||
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
return layout.Background{}.Layout(gtx,
|
||||||
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
rr := gtx.Dp(b.CornerRadius)
|
rr := gtx.Dp(b.CornerRadius)
|
||||||
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
|
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
|
||||||
background := b.Background
|
background := b.Background
|
||||||
@@ -145,11 +143,11 @@ func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Di
|
|||||||
drawInk(gtx, c)
|
drawInk(gtx, c)
|
||||||
}
|
}
|
||||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||||
}),
|
},
|
||||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
gtx.Constraints.Min = min
|
gtx.Constraints.Min = min
|
||||||
return layout.Center.Layout(gtx, w)
|
return layout.Center.Layout(gtx, w)
|
||||||
}),
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -161,8 +159,8 @@ func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
if d := b.Description; d != "" {
|
if d := b.Description; d != "" {
|
||||||
semantic.DescriptionOp(b.Description).Add(gtx.Ops)
|
semantic.DescriptionOp(b.Description).Add(gtx.Ops)
|
||||||
}
|
}
|
||||||
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
return layout.Background{}.Layout(gtx,
|
||||||
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
rr := (gtx.Constraints.Min.X + gtx.Constraints.Min.Y) / 4
|
rr := (gtx.Constraints.Min.X + gtx.Constraints.Min.Y) / 4
|
||||||
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
|
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
|
||||||
background := b.Background
|
background := b.Background
|
||||||
@@ -177,8 +175,8 @@ func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
drawInk(gtx, c)
|
drawInk(gtx, c)
|
||||||
}
|
}
|
||||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||||
}),
|
},
|
||||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
size := gtx.Dp(b.Size)
|
size := gtx.Dp(b.Size)
|
||||||
if b.Icon != nil {
|
if b.Icon != nil {
|
||||||
@@ -189,7 +187,7 @@ func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
|
|||||||
Size: image.Point{X: size, Y: size},
|
Size: image.Point{X: size, Y: size},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}),
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
c := m.Stop()
|
c := m.Stop()
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -88,18 +88,18 @@ func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimension
|
|||||||
cl := d.Decorations.Clickable(a)
|
cl := d.Decorations.Clickable(a)
|
||||||
dims := cl.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
dims := cl.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
semantic.Button.Add(gtx.Ops)
|
semantic.Button.Add(gtx.Ops)
|
||||||
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
return layout.Background{}.Layout(gtx,
|
||||||
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
|
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
|
||||||
for _, c := range cl.History() {
|
for _, c := range cl.History() {
|
||||||
drawInk(gtx, c)
|
drawInk(gtx, c)
|
||||||
}
|
}
|
||||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||||
}),
|
},
|
||||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops)
|
paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops)
|
||||||
return inset.Layout(gtx, w)
|
return inset.Layout(gtx, w)
|
||||||
}),
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
size.X += dims.Size.X
|
size.X += dims.Size.X
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-10
@@ -165,12 +165,9 @@ func (s ScrollbarStyle) layout(gtx layout.Context, axis layout.Axis, viewportSta
|
|||||||
if axis == layout.Horizontal {
|
if axis == layout.Horizontal {
|
||||||
inset.Top, inset.Bottom, inset.Left, inset.Right = inset.Left, inset.Right, inset.Top, inset.Bottom
|
inset.Top, inset.Bottom, inset.Left, inset.Right = inset.Left, inset.Right, inset.Top, inset.Bottom
|
||||||
}
|
}
|
||||||
// Capture the outer constraints because layout.Stack will reset
|
|
||||||
// the minimum to zero.
|
|
||||||
outerConstraints := gtx.Constraints
|
|
||||||
|
|
||||||
return layout.Stack{}.Layout(gtx,
|
return layout.Background{}.Layout(gtx,
|
||||||
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
// Lay out the draggable track underneath the scroll indicator.
|
// Lay out the draggable track underneath the scroll indicator.
|
||||||
area := image.Rectangle{
|
area := image.Rectangle{
|
||||||
Max: gtx.Constraints.Min,
|
Max: gtx.Constraints.Min,
|
||||||
@@ -186,10 +183,9 @@ func (s ScrollbarStyle) layout(gtx layout.Context, axis layout.Axis, viewportSta
|
|||||||
s.Scrollbar.AddTrack(gtx.Ops)
|
s.Scrollbar.AddTrack(gtx.Ops)
|
||||||
|
|
||||||
paint.FillShape(gtx.Ops, s.Track.Color, clip.Rect(area).Op())
|
paint.FillShape(gtx.Ops, s.Track.Color, clip.Rect(area).Op())
|
||||||
return layout.Dimensions{}
|
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||||
}),
|
},
|
||||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
func(gtx layout.Context) layout.Dimensions {
|
||||||
gtx.Constraints = outerConstraints
|
|
||||||
return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
// Use axis-independent constraints.
|
// Use axis-independent constraints.
|
||||||
gtx.Constraints.Min = axis.Convert(gtx.Constraints.Min)
|
gtx.Constraints.Min = axis.Convert(gtx.Constraints.Min)
|
||||||
@@ -231,7 +227,7 @@ func (s ScrollbarStyle) layout(gtx layout.Context, axis layout.Axis, viewportSta
|
|||||||
|
|
||||||
return layout.Dimensions{Size: axis.Convert(gtx.Constraints.Min)}
|
return layout.Dimensions{Size: axis.Convert(gtx.Constraints.Min)}
|
||||||
})
|
})
|
||||||
}),
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user