mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-02 16:06:19 +00:00
Compare commits
178 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3879921b80 | |||
| b1f84da679 | |||
| 43024fcca2 | |||
| 9fe8b684e2 | |||
| 2a18a0c135 | |||
| 93ac0b03f1 | |||
| d58d386b9b | |||
| f3fc0d62b8 | |||
| 5e5d164929 | |||
| 1527e91a02 | |||
| caba422d9c | |||
| 390242f214 | |||
| fe1df00d02 | |||
| 0d7f00c634 | |||
| d7528a8338 | |||
| 9bca5bfdcf | |||
| a880d6403d | |||
| 6879a30582 | |||
| 5cda660e6e | |||
| 8cb06ffa30 | |||
| 297c03925d | |||
| c645c2ec8e | |||
| 95ca7b5b59 | |||
| 5a843bee61 | |||
| dbc10056f9 | |||
| eae39d8556 | |||
| e59f91dfd0 | |||
| c3f2abebca | |||
| 77ff21605c | |||
| f5aa745038 | |||
| 1fc646a8c2 | |||
| 33f9a850c8 | |||
| 5fcfc40ab8 | |||
| 20c28ef282 | |||
| ed0d5d5767 | |||
| d9a007586c | |||
| 496fc3cc82 | |||
| 8e209fd2eb | |||
| ab9f42c820 | |||
| 6dcebf205f | |||
| 75314fcee2 | |||
| c515b7804e | |||
| 0fab08bd6c | |||
| 88f5ac9cb9 | |||
| bce1dbd654 | |||
| fc208248b7 | |||
| 67b58a6006 | |||
| 4d8caba6c9 | |||
| 9dfada745c | |||
| 651094d692 | |||
| 3ba5fc557c | |||
| d25912678c | |||
| 27ef6dd7a2 | |||
| 73c3849da4 | |||
| 12a0ad7038 | |||
| ef8171b971 | |||
| d2085ab7c5 | |||
| d7636ea273 | |||
| be86450ea5 | |||
| 1bcbaa8137 | |||
| 676b670119 | |||
| d51aea553f | |||
| a3c539b3c2 | |||
| eed93aaffe | |||
| 813d836641 | |||
| 627e028d3c | |||
| 9de80749e1 | |||
| 8334d2abb4 | |||
| 5dd41f74d3 | |||
| be36fc88aa | |||
| a11f35fe0d | |||
| 6027517949 | |||
| c319f3c214 | |||
| 4fcd96ac4b | |||
| d5a0d2cf60 | |||
| 99399184ac | |||
| dd36ec5e07 | |||
| cb1e605203 | |||
| 60bfb9e064 | |||
| 3648bdc02a | |||
| e19a248815 | |||
| 7cfd226b57 | |||
| 05d28ad76a | |||
| 40706d3782 | |||
| adba14c062 | |||
| ab021c4566 | |||
| fe2a164d30 | |||
| 4eca2c7d26 | |||
| 7ea432fa13 | |||
| e666ef35ca | |||
| a8ec3968d9 | |||
| 2128f7adea | |||
| a454d5fa38 | |||
| 7d1ea02267 | |||
| f7aa4b5c81 | |||
| 52987e53f6 | |||
| e32417353a | |||
| 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: |
|
||||||
|
|||||||
+1
-1
@@ -65,8 +65,8 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
|
|||||||
private final InputMethodManager imm;
|
private final InputMethodManager imm;
|
||||||
private final float scrollXScale;
|
private final float scrollXScale;
|
||||||
private final float scrollYScale;
|
private final float scrollYScale;
|
||||||
|
private final AccessibilityManager accessManager;
|
||||||
private int keyboardHint;
|
private int keyboardHint;
|
||||||
private AccessibilityManager accessManager;
|
|
||||||
|
|
||||||
private long nhandle;
|
private long nhandle;
|
||||||
|
|
||||||
|
|||||||
+87
-19
@@ -3,9 +3,16 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"image"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gioui.org/io/input"
|
||||||
|
"gioui.org/layout"
|
||||||
|
"gioui.org/op"
|
||||||
|
"gioui.org/unit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// extraArgs contains extra arguments to append to
|
// extraArgs contains extra arguments to append to
|
||||||
@@ -20,23 +27,84 @@ var extraArgs string
|
|||||||
// On Android ID is the package property of AndroidManifest.xml,
|
// On Android ID is the package property of AndroidManifest.xml,
|
||||||
// on iOS ID is the CFBundleIdentifier of the app Info.plist,
|
// on iOS ID is the CFBundleIdentifier of the app Info.plist,
|
||||||
// on Wayland it is the toplevel app_id,
|
// on Wayland it is the toplevel app_id,
|
||||||
// on X11 it is the X11 XClassHint
|
// on X11 it is the X11 XClassHint.
|
||||||
//
|
//
|
||||||
// ID is set by the gogio tool or manually with the -X linker flag. For example,
|
// ID is set by the [gioui.org/cmd/gogio] tool or manually with the -X linker flag. For example,
|
||||||
//
|
//
|
||||||
// go build -ldflags="-X 'gioui.org/app.ID=org.gioui.example.Kitchen'" .
|
// go build -ldflags="-X 'gioui.org/app.ID=org.gioui.example.Kitchen'" .
|
||||||
//
|
//
|
||||||
// Note that ID is treated as a constant, and that changing it at runtime
|
// Note that ID is treated as a constant, and that changing it at runtime
|
||||||
// is not supported. Default value of ID is filepath.Base(os.Args[0]).
|
// is not supported. The default value of ID is filepath.Base(os.Args[0]).
|
||||||
var ID = ""
|
var ID = ""
|
||||||
|
|
||||||
func init() {
|
// A FrameEvent requests a new frame in the form of a list of
|
||||||
if extraArgs != "" {
|
// operations that describes the window content.
|
||||||
args := strings.Split(extraArgs, "|")
|
type FrameEvent struct {
|
||||||
os.Args = append(os.Args, args...)
|
// Now is the current animation. Use Now instead of time.Now to
|
||||||
|
// synchronize animation and to avoid the time.Now call overhead.
|
||||||
|
Now time.Time
|
||||||
|
// Metric converts device independent dp and sp to device pixels.
|
||||||
|
Metric unit.Metric
|
||||||
|
// Size is the dimensions of the window.
|
||||||
|
Size image.Point
|
||||||
|
// Insets represent the space occupied by system decorations and controls.
|
||||||
|
Insets Insets
|
||||||
|
// Frame completes the FrameEvent by drawing the graphical operations
|
||||||
|
// from ops into the window.
|
||||||
|
Frame func(frame *op.Ops)
|
||||||
|
// Source is the interface between the window and widgets.
|
||||||
|
Source input.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewEvent provides handles to the underlying window objects for the
|
||||||
|
// current display protocol.
|
||||||
|
type ViewEvent interface {
|
||||||
|
implementsViewEvent()
|
||||||
|
ImplementsEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insets is the space taken up by
|
||||||
|
// system decoration such as translucent
|
||||||
|
// system bars and software keyboards.
|
||||||
|
type Insets struct {
|
||||||
|
// Values are in pixels.
|
||||||
|
Top, Bottom, Left, Right unit.Dp
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContext is shorthand for
|
||||||
|
//
|
||||||
|
// layout.Context{
|
||||||
|
// Ops: ops,
|
||||||
|
// Now: e.Now,
|
||||||
|
// Source: e.Source,
|
||||||
|
// Metric: e.Metric,
|
||||||
|
// Constraints: layout.Exact(e.Size),
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// NewContext calls ops.Reset and adjusts ops for e.Insets.
|
||||||
|
func NewContext(ops *op.Ops, e FrameEvent) layout.Context {
|
||||||
|
ops.Reset()
|
||||||
|
|
||||||
|
size := e.Size
|
||||||
|
|
||||||
|
if e.Insets != (Insets{}) {
|
||||||
|
left := e.Metric.Dp(e.Insets.Left)
|
||||||
|
top := e.Metric.Dp(e.Insets.Top)
|
||||||
|
op.Offset(image.Point{
|
||||||
|
X: left,
|
||||||
|
Y: top,
|
||||||
|
}).Add(ops)
|
||||||
|
|
||||||
|
size.X -= left + e.Metric.Dp(e.Insets.Right)
|
||||||
|
size.Y -= top + e.Metric.Dp(e.Insets.Bottom)
|
||||||
}
|
}
|
||||||
if ID == "" {
|
|
||||||
ID = filepath.Base(os.Args[0])
|
return layout.Context{
|
||||||
|
Ops: ops,
|
||||||
|
Now: e.Now,
|
||||||
|
Source: e.Source,
|
||||||
|
Metric: e.Metric,
|
||||||
|
Constraints: layout.Exact(size),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,14 +120,14 @@ func DataDir() (string, error) {
|
|||||||
return dataDir()
|
return dataDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main must be called last from the program main function.
|
func (FrameEvent) ImplementsEvent() {}
|
||||||
// On most platforms Main blocks forever, for Android and
|
|
||||||
// iOS it returns immediately to give control of the main
|
func init() {
|
||||||
// thread back to the system.
|
if extraArgs != "" {
|
||||||
//
|
args := strings.Split(extraArgs, "|")
|
||||||
// Calling Main is necessary because some operating systems
|
os.Args = append(os.Args, args...)
|
||||||
// require control of the main thread of the program for
|
}
|
||||||
// running windows.
|
if ID == "" {
|
||||||
func Main() {
|
ID = filepath.Base(os.Args[0])
|
||||||
osMain()
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-38
@@ -8,21 +8,20 @@ See https://gioui.org for instructions to set up and run Gio programs.
|
|||||||
|
|
||||||
# Windows
|
# Windows
|
||||||
|
|
||||||
Create a new Window by calling NewWindow. On mobile platforms or when Gio
|
A Window is run by calling its Event method in a loop. The first time a
|
||||||
is embedded in another project, NewWindow merely connects with a previously
|
method on Window is called, a new GUI window is created and shown. On mobile
|
||||||
created window.
|
platforms or when Gio is embedded in another project, Window merely connects
|
||||||
|
with a previously created GUI window.
|
||||||
|
|
||||||
A Window is run by receiving events from its Events channel. The most
|
The most important event is [FrameEvent] that prompts an update of the window
|
||||||
important event is FrameEvent that prompts an update of the window
|
contents.
|
||||||
contents and state.
|
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
import "gioui.org/unit"
|
w := new(app.Window)
|
||||||
|
for {
|
||||||
w := app.NewWindow()
|
e := w.Event()
|
||||||
for e := range w.Events() {
|
if e, ok := e.(app.FrameEvent); ok {
|
||||||
if e, ok := e.(system.FrameEvent); ok {
|
|
||||||
ops.Reset()
|
ops.Reset()
|
||||||
// Add operations to ops.
|
// Add operations to ops.
|
||||||
...
|
...
|
||||||
@@ -32,35 +31,14 @@ For example:
|
|||||||
}
|
}
|
||||||
|
|
||||||
A program must keep receiving events from the event channel until
|
A program must keep receiving events from the event channel until
|
||||||
DestroyEvent is received.
|
[DestroyEvent] is received.
|
||||||
|
|
||||||
# Main
|
# Main Thread
|
||||||
|
|
||||||
The Main function must be called from a program's main function, to hand over
|
Some GUI platform need access to the main thread of the program. To avoid a
|
||||||
control of the main thread to operating systems that need it.
|
deadlock on such platforms, at least one Window must have its Event method
|
||||||
|
called by the main goroutine. It doesn't have to be any particular Window;
|
||||||
Because Main is also blocking on some platforms, the event loop of a Window must run in a goroutine.
|
even a destroyed Window suffices.
|
||||||
|
|
||||||
For example, to display a blank but otherwise functional window:
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "gioui.org/app"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
go func() {
|
|
||||||
w := app.NewWindow()
|
|
||||||
for range w.Events() {
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
app.Main()
|
|
||||||
}
|
|
||||||
|
|
||||||
# Event queue
|
|
||||||
|
|
||||||
A FrameEvent's Queue method returns an event.Queue implementation that distributes
|
|
||||||
incoming events to the event handlers declared in the last frame.
|
|
||||||
See the gioui.org/io/event package for more information about event handlers.
|
|
||||||
|
|
||||||
# Permissions
|
# Permissions
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -69,7 +69,17 @@ func (c *wlContext) Refresh() error {
|
|||||||
}
|
}
|
||||||
c.eglWin = eglWin
|
c.eglWin = eglWin
|
||||||
eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin)))
|
eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin)))
|
||||||
return c.Context.CreateSurface(eglSurf, width, height)
|
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.Context.MakeCurrent(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer c.Context.ReleaseCurrent()
|
||||||
|
// We're in charge of the frame callbacks, don't let eglSwapBuffers
|
||||||
|
// wait for callbacks that may never arrive.
|
||||||
|
c.Context.EnableVSync(false)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *wlContext) Lock() error {
|
func (c *wlContext) Lock() error {
|
||||||
|
|||||||
+1
-1
@@ -46,8 +46,8 @@ func (c *x11Context) Refresh() error {
|
|||||||
if err := c.Context.MakeCurrent(); err != nil {
|
if err := c.Context.MakeCurrent(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer c.Context.ReleaseCurrent()
|
||||||
c.Context.EnableVSync(true)
|
c.Context.EnableVSync(true)
|
||||||
c.Context.ReleaseCurrent()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf16"
|
||||||
|
|
||||||
|
"gioui.org/io/input"
|
||||||
|
"gioui.org/io/key"
|
||||||
|
)
|
||||||
|
|
||||||
|
type editorState struct {
|
||||||
|
input.EditorState
|
||||||
|
compose key.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *editorState) Replace(r key.Range, text string) {
|
||||||
|
if r.Start > r.End {
|
||||||
|
r.Start, r.End = r.End, r.Start
|
||||||
|
}
|
||||||
|
runes := []rune(text)
|
||||||
|
newEnd := r.Start + len(runes)
|
||||||
|
adjust := func(pos int) int {
|
||||||
|
switch {
|
||||||
|
case newEnd < pos && pos <= r.End:
|
||||||
|
return newEnd
|
||||||
|
case r.End < pos:
|
||||||
|
diff := newEnd - r.End
|
||||||
|
return pos + diff
|
||||||
|
}
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
e.Selection.Start = adjust(e.Selection.Start)
|
||||||
|
e.Selection.End = adjust(e.Selection.End)
|
||||||
|
if e.compose.Start != -1 {
|
||||||
|
e.compose.Start = adjust(e.compose.Start)
|
||||||
|
e.compose.End = adjust(e.compose.End)
|
||||||
|
}
|
||||||
|
s := e.Snippet
|
||||||
|
if r.End < s.Start || r.Start > s.End {
|
||||||
|
// Discard snippet if it doesn't overlap with replacement.
|
||||||
|
s = key.Snippet{
|
||||||
|
Range: key.Range{
|
||||||
|
Start: r.Start,
|
||||||
|
End: r.Start,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var newSnippet []rune
|
||||||
|
snippet := []rune(s.Text)
|
||||||
|
// Append first part of existing snippet.
|
||||||
|
if end := r.Start - s.Start; end > 0 {
|
||||||
|
newSnippet = append(newSnippet, snippet[:end]...)
|
||||||
|
}
|
||||||
|
// Append replacement.
|
||||||
|
newSnippet = append(newSnippet, runes...)
|
||||||
|
// Append last part of existing snippet.
|
||||||
|
if start := r.End; start < s.End {
|
||||||
|
newSnippet = append(newSnippet, snippet[start-s.Start:]...)
|
||||||
|
}
|
||||||
|
// Adjust snippet range to include replacement.
|
||||||
|
if r.Start < s.Start {
|
||||||
|
s.Start = r.Start
|
||||||
|
}
|
||||||
|
s.End = s.Start + len(newSnippet)
|
||||||
|
s.Text = string(newSnippet)
|
||||||
|
e.Snippet = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTF16Index converts the given index in runes into an index in utf16 characters.
|
||||||
|
func (e *editorState) UTF16Index(runes int) int {
|
||||||
|
if runes == -1 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if runes < e.Snippet.Start {
|
||||||
|
// Assume runes before sippet are one UTF-16 character each.
|
||||||
|
return runes
|
||||||
|
}
|
||||||
|
chars := e.Snippet.Start
|
||||||
|
runes -= e.Snippet.Start
|
||||||
|
for _, r := range e.Snippet.Text {
|
||||||
|
if runes == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
runes--
|
||||||
|
chars++
|
||||||
|
if r1, _ := utf16.EncodeRune(r); r1 != unicode.ReplacementChar {
|
||||||
|
chars++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Assume runes after snippets are one UTF-16 character each.
|
||||||
|
return chars + runes
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunesIndex converts the given index in utf16 characters to an index in runes.
|
||||||
|
func (e *editorState) RunesIndex(chars int) int {
|
||||||
|
if chars == -1 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if chars < e.Snippet.Start {
|
||||||
|
// Assume runes before offset are one UTF-16 character each.
|
||||||
|
return chars
|
||||||
|
}
|
||||||
|
runes := e.Snippet.Start
|
||||||
|
chars -= e.Snippet.Start
|
||||||
|
for _, r := range e.Snippet.Text {
|
||||||
|
if chars == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
chars--
|
||||||
|
runes++
|
||||||
|
if r1, _ := utf16.EncodeRune(r); r1 != unicode.ReplacementChar {
|
||||||
|
chars--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Assume runes after snippets are one UTF-16 character each.
|
||||||
|
return runes + chars
|
||||||
|
}
|
||||||
+5
-5
@@ -11,8 +11,8 @@ import (
|
|||||||
|
|
||||||
"gioui.org/font"
|
"gioui.org/font"
|
||||||
"gioui.org/font/gofont"
|
"gioui.org/font/gofont"
|
||||||
|
"gioui.org/io/input"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
"gioui.org/io/router"
|
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"gioui.org/text"
|
"gioui.org/text"
|
||||||
@@ -29,12 +29,12 @@ 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()
|
|
||||||
|
|
||||||
var r router.Router
|
var r input.Router
|
||||||
gtx := layout.Context{Ops: new(op.Ops), Queue: &r}
|
gtx := layout.Context{Ops: new(op.Ops), Source: r.Source()}
|
||||||
|
gtx.Execute(key.FocusCmd{Tag: e})
|
||||||
// Layout once to register focus.
|
// Layout once to register focus.
|
||||||
e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
|
e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
|
||||||
r.Frame(gtx.Ops)
|
r.Frame(gtx.Ops)
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
|
||||||
|
|
||||||
// Package points standard output, standard error and the standard
|
|
||||||
// library package log to the platform logger.
|
|
||||||
package log
|
|
||||||
|
|
||||||
var appID = "gio"
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -238,14 +238,17 @@ func (x *Context) UpdateMask(depressed, latched, locked, depressedGroup, latched
|
|||||||
C.xkb_layout_index_t(depressedGroup), C.xkb_layout_index_t(latchedGroup), C.xkb_layout_index_t(lockedGroup))
|
C.xkb_layout_index_t(depressedGroup), C.xkb_layout_index_t(latchedGroup), C.xkb_layout_index_t(lockedGroup))
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertKeysym(s C.xkb_keysym_t) (string, bool) {
|
func convertKeysym(s C.xkb_keysym_t) (key.Name, bool) {
|
||||||
if 'a' <= s && s <= 'z' {
|
if 'a' <= s && s <= 'z' {
|
||||||
return string(rune(s - 'a' + 'A')), true
|
return key.Name(rune(s - 'a' + 'A')), true
|
||||||
|
}
|
||||||
|
if C.XKB_KEY_KP_0 <= s && s <= C.XKB_KEY_KP_9 {
|
||||||
|
return key.Name(rune(s - C.XKB_KEY_KP_0 + '0')), true
|
||||||
}
|
}
|
||||||
if ' ' < s && s <= '~' {
|
if ' ' < s && s <= '~' {
|
||||||
return string(rune(s)), true
|
return key.Name(rune(s)), true
|
||||||
}
|
}
|
||||||
var n string
|
var n key.Name
|
||||||
switch s {
|
switch s {
|
||||||
case C.XKB_KEY_Escape:
|
case C.XKB_KEY_Escape:
|
||||||
n = key.NameEscape
|
n = key.NameEscape
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
package log
|
package app
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#cgo LDFLAGS: -llog
|
#cgo LDFLAGS: -llog
|
||||||
@@ -22,7 +22,7 @@ import (
|
|||||||
// 1024 is the truncation limit from android/log.h, plus a \n.
|
// 1024 is the truncation limit from android/log.h, plus a \n.
|
||||||
const logLineLimit = 1024
|
const logLineLimit = 1024
|
||||||
|
|
||||||
var logTag = C.CString(appID)
|
var logTag = C.CString(ID)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Android's logcat already includes timestamps.
|
// Android's logcat already includes timestamps.
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
//go:build darwin && ios
|
//go:build darwin && ios
|
||||||
// +build darwin,ios
|
// +build darwin,ios
|
||||||
|
|
||||||
package log
|
package app
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c
|
#cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
package log
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
+2
-1
@@ -60,8 +60,9 @@ static void presentDrawable(CFTypeRef queueRef, CFTypeRef drawableRef) {
|
|||||||
id<MTLDrawable> drawable = (__bridge id<MTLDrawable>)drawableRef;
|
id<MTLDrawable> drawable = (__bridge id<MTLDrawable>)drawableRef;
|
||||||
id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef;
|
id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef;
|
||||||
id<MTLCommandBuffer> cmdBuffer = [queue commandBuffer];
|
id<MTLCommandBuffer> cmdBuffer = [queue commandBuffer];
|
||||||
[cmdBuffer presentDrawable:drawable];
|
|
||||||
[cmdBuffer commit];
|
[cmdBuffer commit];
|
||||||
|
[cmdBuffer waitUntilScheduled];
|
||||||
|
[drawable present];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -21,7 +21,10 @@ Class gio_layerClass(void) {
|
|||||||
static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
|
static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
UIView *view = (__bridge UIView *)viewRef;
|
UIView *view = (__bridge UIView *)viewRef;
|
||||||
return CFBridgingRetain(view.layer);
|
CAMetalLayer *l = (CAMetalLayer *)view.layer;
|
||||||
|
l.needsDisplayOnBoundsChange = YES;
|
||||||
|
l.presentsWithTransaction = YES;
|
||||||
|
return CFBridgingRetain(l);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -14,7 +14,11 @@ package app
|
|||||||
|
|
||||||
CALayer *gio_layerFactory(void) {
|
CALayer *gio_layerFactory(void) {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
return [CAMetalLayer layer];
|
CAMetalLayer *l = [CAMetalLayer layer];
|
||||||
|
l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable;
|
||||||
|
l.needsDisplayOnBoundsChange = YES;
|
||||||
|
l.presentsWithTransaction = YES;
|
||||||
|
return l;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
|
||||||
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
|
"gioui.org/op"
|
||||||
|
|
||||||
"gioui.org/gpu"
|
"gioui.org/gpu"
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
@@ -43,6 +45,8 @@ type Config struct {
|
|||||||
CustomRenderer bool
|
CustomRenderer bool
|
||||||
// Decorated reports whether window decorations are provided automatically.
|
// Decorated reports whether window decorations are provided automatically.
|
||||||
Decorated bool
|
Decorated bool
|
||||||
|
// Focused reports whether has the keyboard focus.
|
||||||
|
Focused bool
|
||||||
// decoHeight is the height of the fallback decoration for platforms such
|
// decoHeight is the height of the fallback decoration for platforms such
|
||||||
// as Wayland that may need fallback client-side decorations.
|
// as Wayland that may need fallback client-side decorations.
|
||||||
decoHeight unit.Dp
|
decoHeight unit.Dp
|
||||||
@@ -131,8 +135,30 @@ func (o Orientation) String() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eventLoop implements the functionality required for drivers where
|
||||||
|
// window event loops must run on a separate thread.
|
||||||
|
type eventLoop struct {
|
||||||
|
win *callbacks
|
||||||
|
// wakeup is the callback to wake up the event loop.
|
||||||
|
wakeup func()
|
||||||
|
// driverFuncs is a channel of functions to run the next
|
||||||
|
// time the window loop waits for events.
|
||||||
|
driverFuncs chan func()
|
||||||
|
// invalidates is notified when an invalidate is requested by the client.
|
||||||
|
invalidates chan struct{}
|
||||||
|
// immediateInvalidates is an optimistic invalidates that doesn't require a wakeup.
|
||||||
|
immediateInvalidates chan struct{}
|
||||||
|
// events is where the platform backend delivers events bound for the
|
||||||
|
// user program.
|
||||||
|
events chan event.Event
|
||||||
|
frames chan *op.Ops
|
||||||
|
frameAck chan struct{}
|
||||||
|
// delivering avoids re-entrant event delivery.
|
||||||
|
delivering bool
|
||||||
|
}
|
||||||
|
|
||||||
type frameEvent struct {
|
type frameEvent struct {
|
||||||
system.FrameEvent
|
FrameEvent
|
||||||
|
|
||||||
Sync bool
|
Sync bool
|
||||||
}
|
}
|
||||||
@@ -147,9 +173,19 @@ type context interface {
|
|||||||
Unlock()
|
Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Driver is the interface for the platform implementation
|
// basicDriver is the subset of [driver] that may be called even after
|
||||||
|
// a window is destroyed.
|
||||||
|
type basicDriver interface {
|
||||||
|
// Event blocks until an even is available and returns it.
|
||||||
|
Event() event.Event
|
||||||
|
// Invalidate requests a FrameEvent.
|
||||||
|
Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// driver is the interface for the platform implementation
|
||||||
// of a window.
|
// of a window.
|
||||||
type driver interface {
|
type driver interface {
|
||||||
|
basicDriver
|
||||||
// SetAnimating sets the animation flag. When the window is animating,
|
// SetAnimating sets the animation flag. When the window is animating,
|
||||||
// FrameEvents are delivered as fast as the display can handle them.
|
// FrameEvents are delivered as fast as the display can handle them.
|
||||||
SetAnimating(anim bool)
|
SetAnimating(anim bool)
|
||||||
@@ -160,23 +196,29 @@ type driver interface {
|
|||||||
// ReadClipboard requests the clipboard content.
|
// ReadClipboard requests the clipboard content.
|
||||||
ReadClipboard()
|
ReadClipboard()
|
||||||
// WriteClipboard requests a clipboard write.
|
// WriteClipboard requests a clipboard write.
|
||||||
WriteClipboard(s string)
|
WriteClipboard(mime string, s []byte)
|
||||||
// Configure the window.
|
// Configure the window.
|
||||||
Configure([]Option)
|
Configure([]Option)
|
||||||
// SetCursor updates the current cursor to name.
|
// SetCursor updates the current cursor to name.
|
||||||
SetCursor(cursor pointer.Cursor)
|
SetCursor(cursor pointer.Cursor)
|
||||||
// Wakeup wakes up the event loop and sends a WakeupEvent.
|
// Wakeup wakes up the event loop and sends a WakeupEvent.
|
||||||
Wakeup()
|
// Wakeup()
|
||||||
// Perform actions on the window.
|
// Perform actions on the window.
|
||||||
Perform(system.Action)
|
Perform(system.Action)
|
||||||
// EditorStateChanged notifies the driver that the editor state changed.
|
// EditorStateChanged notifies the driver that the editor state changed.
|
||||||
EditorStateChanged(old, new editorState)
|
EditorStateChanged(old, new editorState)
|
||||||
|
// Run a function on the window thread.
|
||||||
|
Run(f func())
|
||||||
|
// Frame receives a frame.
|
||||||
|
Frame(frame *op.Ops)
|
||||||
|
// ProcessEvent processes an event.
|
||||||
|
ProcessEvent(e event.Event)
|
||||||
}
|
}
|
||||||
|
|
||||||
type windowRendezvous struct {
|
type windowRendezvous struct {
|
||||||
in chan windowAndConfig
|
in chan windowAndConfig
|
||||||
out chan windowAndConfig
|
out chan windowAndConfig
|
||||||
errs chan error
|
windows chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type windowAndConfig struct {
|
type windowAndConfig struct {
|
||||||
@@ -186,32 +228,137 @@ type windowAndConfig struct {
|
|||||||
|
|
||||||
func newWindowRendezvous() *windowRendezvous {
|
func newWindowRendezvous() *windowRendezvous {
|
||||||
wr := &windowRendezvous{
|
wr := &windowRendezvous{
|
||||||
in: make(chan windowAndConfig),
|
in: make(chan windowAndConfig),
|
||||||
out: make(chan windowAndConfig),
|
out: make(chan windowAndConfig),
|
||||||
errs: make(chan error),
|
windows: make(chan struct{}),
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
var main windowAndConfig
|
in := wr.in
|
||||||
|
var window windowAndConfig
|
||||||
var out chan windowAndConfig
|
var out chan windowAndConfig
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case w := <-wr.in:
|
case w := <-in:
|
||||||
var err error
|
window = w
|
||||||
if main.window != nil {
|
|
||||||
err = errors.New("multiple windows are not supported")
|
|
||||||
}
|
|
||||||
wr.errs <- err
|
|
||||||
main = w
|
|
||||||
out = wr.out
|
out = wr.out
|
||||||
case out <- main:
|
case out <- window:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return wr
|
return wr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wakeupEvent) ImplementsEvent() {}
|
func newEventLoop(w *callbacks, wakeup func()) *eventLoop {
|
||||||
func (ConfigEvent) ImplementsEvent() {}
|
return &eventLoop{
|
||||||
|
win: w,
|
||||||
|
wakeup: wakeup,
|
||||||
|
events: make(chan event.Event),
|
||||||
|
invalidates: make(chan struct{}, 1),
|
||||||
|
immediateInvalidates: make(chan struct{}),
|
||||||
|
frames: make(chan *op.Ops),
|
||||||
|
frameAck: make(chan struct{}),
|
||||||
|
driverFuncs: make(chan func(), 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame receives a frame and waits for its processing. It is called by
|
||||||
|
// the client goroutine.
|
||||||
|
func (e *eventLoop) Frame(frame *op.Ops) {
|
||||||
|
e.frames <- frame
|
||||||
|
<-e.frameAck
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event returns the next available event. It is called by the client
|
||||||
|
// goroutine.
|
||||||
|
func (e *eventLoop) Event() event.Event {
|
||||||
|
for {
|
||||||
|
evt := <-e.events
|
||||||
|
// Receiving a flushEvent indicates to the platform backend that
|
||||||
|
// all previous events have been processed by the user program.
|
||||||
|
if _, ok := evt.(flushEvent); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate requests invalidation of the window. It is called by the client
|
||||||
|
// goroutine.
|
||||||
|
func (e *eventLoop) Invalidate() {
|
||||||
|
select {
|
||||||
|
case e.immediateInvalidates <- struct{}{}:
|
||||||
|
// The event loop was waiting, no need for a wakeup.
|
||||||
|
case e.invalidates <- struct{}{}:
|
||||||
|
// The event loop is sleeping, wake it up.
|
||||||
|
e.wakeup()
|
||||||
|
default:
|
||||||
|
// A redraw is pending.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run f in the window loop thread. It is called by the client goroutine.
|
||||||
|
func (e *eventLoop) Run(f func()) {
|
||||||
|
e.driverFuncs <- f
|
||||||
|
e.wakeup()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlushEvents delivers pending events to the client.
|
||||||
|
func (e *eventLoop) FlushEvents() {
|
||||||
|
if e.delivering {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.delivering = true
|
||||||
|
defer func() { e.delivering = false }()
|
||||||
|
for {
|
||||||
|
evt, ok := e.win.nextEvent()
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
e.deliverEvent(evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *eventLoop) deliverEvent(evt event.Event) {
|
||||||
|
var frames <-chan *op.Ops
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case f := <-e.driverFuncs:
|
||||||
|
f()
|
||||||
|
case frame := <-frames:
|
||||||
|
// The client called FrameEvent.Frame.
|
||||||
|
frames = nil
|
||||||
|
e.win.ProcessFrame(frame, e.frameAck)
|
||||||
|
case e.events <- evt:
|
||||||
|
switch evt.(type) {
|
||||||
|
case flushEvent, DestroyEvent:
|
||||||
|
// DestroyEvents are not flushed.
|
||||||
|
return
|
||||||
|
case FrameEvent:
|
||||||
|
frames = e.frames
|
||||||
|
}
|
||||||
|
evt = theFlushEvent
|
||||||
|
case <-e.invalidates:
|
||||||
|
e.win.Invalidate()
|
||||||
|
case <-e.immediateInvalidates:
|
||||||
|
e.win.Invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *eventLoop) Wakeup() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case f := <-e.driverFuncs:
|
||||||
|
f()
|
||||||
|
case <-e.invalidates:
|
||||||
|
e.win.Invalidate()
|
||||||
|
case <-e.immediateInvalidates:
|
||||||
|
e.win.Invalidate()
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func walkActions(actions system.Action, do func(system.Action)) {
|
func walkActions(actions system.Action, do func(system.Action)) {
|
||||||
for a := system.Action(1); actions != 0; a <<= 1 {
|
for a := system.Action(1); actions != 0; a <<= 1 {
|
||||||
@@ -221,3 +368,6 @@ func walkActions(actions system.Action, do func(system.Action)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wakeupEvent) ImplementsEvent() {}
|
||||||
|
func (ConfigEvent) ImplementsEvent() {}
|
||||||
|
|||||||
+161
-119
@@ -123,31 +123,36 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/cgo"
|
"runtime/cgo"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf16"
|
"unicode/utf16"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"gioui.org/internal/f32color"
|
"gioui.org/internal/f32color"
|
||||||
|
"gioui.org/op"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/io/clipboard"
|
"gioui.org/io/event"
|
||||||
|
"gioui.org/io/input"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
"gioui.org/io/router"
|
|
||||||
"gioui.org/io/semantic"
|
"gioui.org/io/semantic"
|
||||||
"gioui.org/io/system"
|
"gioui.org/io/system"
|
||||||
|
"gioui.org/io/transfer"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type window struct {
|
type window struct {
|
||||||
callbacks *callbacks
|
callbacks *callbacks
|
||||||
|
loop *eventLoop
|
||||||
|
|
||||||
view C.jobject
|
view C.jobject
|
||||||
handle cgo.Handle
|
handle cgo.Handle
|
||||||
@@ -156,18 +161,19 @@ type window struct {
|
|||||||
fontScale float32
|
fontScale float32
|
||||||
insets pixelInsets
|
insets pixelInsets
|
||||||
|
|
||||||
stage system.Stage
|
visible bool
|
||||||
started bool
|
started bool
|
||||||
animating bool
|
animating bool
|
||||||
|
|
||||||
win *C.ANativeWindow
|
win *C.ANativeWindow
|
||||||
config Config
|
config Config
|
||||||
|
inputHint key.InputHint
|
||||||
|
|
||||||
semantic struct {
|
semantic struct {
|
||||||
hoverID router.SemanticID
|
hoverID input.SemanticID
|
||||||
rootID router.SemanticID
|
rootID input.SemanticID
|
||||||
focusID router.SemanticID
|
focusID input.SemanticID
|
||||||
diffs []router.SemanticID
|
diffs []input.SemanticID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,9 +205,9 @@ type pixelInsets struct {
|
|||||||
top, bottom, left, right int
|
top, bottom, left, right int
|
||||||
}
|
}
|
||||||
|
|
||||||
// ViewEvent is sent whenever the Window's underlying Android view
|
// AndroidViewEvent is sent whenever the Window's underlying Android view
|
||||||
// changes.
|
// changes.
|
||||||
type ViewEvent struct {
|
type AndroidViewEvent struct {
|
||||||
// View is a JNI global reference to the android.view.View
|
// View is a JNI global reference to the android.view.View
|
||||||
// instance backing the Window. The reference is valid until
|
// instance backing the Window. The reference is valid until
|
||||||
// the next ViewEvent is received.
|
// the next ViewEvent is received.
|
||||||
@@ -338,20 +344,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 +381,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)
|
||||||
|
|
||||||
@@ -483,24 +491,30 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
|
|||||||
})
|
})
|
||||||
view = C.jni_NewGlobalRef(env, view)
|
view = C.jni_NewGlobalRef(env, view)
|
||||||
wopts := <-mainWindow.out
|
wopts := <-mainWindow.out
|
||||||
|
var cnf Config
|
||||||
w, ok := windows[wopts.window]
|
w, ok := windows[wopts.window]
|
||||||
if !ok {
|
if !ok {
|
||||||
w = &window{
|
w = &window{
|
||||||
callbacks: wopts.window,
|
callbacks: wopts.window,
|
||||||
}
|
}
|
||||||
|
w.loop = newEventLoop(w.callbacks, w.wakeup)
|
||||||
|
w.callbacks.SetDriver(w)
|
||||||
|
cnf.apply(unit.Metric{}, wopts.options)
|
||||||
windows[wopts.window] = w
|
windows[wopts.window] = w
|
||||||
|
} else {
|
||||||
|
cnf = w.config
|
||||||
}
|
}
|
||||||
|
mainWindow.windows <- struct{}{}
|
||||||
if w.view != 0 {
|
if w.view != 0 {
|
||||||
w.detach(env)
|
w.detach(env)
|
||||||
}
|
}
|
||||||
w.view = view
|
w.view = view
|
||||||
|
w.visible = false
|
||||||
w.handle = cgo.NewHandle(w)
|
w.handle = cgo.NewHandle(w)
|
||||||
w.callbacks.SetDriver(w)
|
|
||||||
w.loadConfig(env, class)
|
w.loadConfig(env, class)
|
||||||
w.Configure(wopts.options)
|
w.setConfig(env, cnf)
|
||||||
w.SetInputHint(key.HintAny)
|
w.SetInputHint(w.inputHint)
|
||||||
w.setStage(system.StagePaused)
|
w.processEvent(AndroidViewEvent{View: uintptr(view)})
|
||||||
w.callbacks.Event(ViewEvent{View: uintptr(view)})
|
|
||||||
return C.jlong(w.handle)
|
return C.jlong(w.handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,7 +528,7 @@ func Java_org_gioui_GioView_onDestroyView(env *C.JNIEnv, class C.jclass, handle
|
|||||||
func Java_org_gioui_GioView_onStopView(env *C.JNIEnv, class C.jclass, handle C.jlong) {
|
func Java_org_gioui_GioView_onStopView(env *C.JNIEnv, class C.jclass, handle C.jlong) {
|
||||||
w := cgo.Handle(handle).Value().(*window)
|
w := cgo.Handle(handle).Value().(*window)
|
||||||
w.started = false
|
w.started = false
|
||||||
w.setStage(system.StagePaused)
|
w.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
//export Java_org_gioui_GioView_onStartView
|
//export Java_org_gioui_GioView_onStartView
|
||||||
@@ -530,7 +544,7 @@ func Java_org_gioui_GioView_onStartView(env *C.JNIEnv, class C.jclass, handle C.
|
|||||||
func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, handle C.jlong) {
|
func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, handle C.jlong) {
|
||||||
w := cgo.Handle(handle).Value().(*window)
|
w := cgo.Handle(handle).Value().(*window)
|
||||||
w.win = nil
|
w.win = nil
|
||||||
w.setStage(system.StagePaused)
|
w.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
//export Java_org_gioui_GioView_onSurfaceChanged
|
//export Java_org_gioui_GioView_onSurfaceChanged
|
||||||
@@ -552,9 +566,7 @@ func Java_org_gioui_GioView_onLowMemory(env *C.JNIEnv, class C.jclass) {
|
|||||||
func Java_org_gioui_GioView_onConfigurationChanged(env *C.JNIEnv, class C.jclass, view C.jlong) {
|
func Java_org_gioui_GioView_onConfigurationChanged(env *C.JNIEnv, class C.jclass, view C.jlong) {
|
||||||
w := cgo.Handle(view).Value().(*window)
|
w := cgo.Handle(view).Value().(*window)
|
||||||
w.loadConfig(env, class)
|
w.loadConfig(env, class)
|
||||||
if w.stage >= system.StageInactive {
|
w.draw(env, true)
|
||||||
w.draw(env, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//export Java_org_gioui_GioView_onFrameCallback
|
//export Java_org_gioui_GioView_onFrameCallback
|
||||||
@@ -563,10 +575,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
|
|||||||
if !exist {
|
if !exist {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if w.stage < system.StageInactive {
|
if w.visible && w.animating {
|
||||||
return
|
|
||||||
}
|
|
||||||
if w.animating {
|
|
||||||
w.draw(env, false)
|
w.draw(env, false)
|
||||||
callVoidMethod(env, w.view, gioView.postFrameCallback)
|
callVoidMethod(env, w.view, gioView.postFrameCallback)
|
||||||
}
|
}
|
||||||
@@ -575,7 +584,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
|
|||||||
//export Java_org_gioui_GioView_onBack
|
//export Java_org_gioui_GioView_onBack
|
||||||
func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong) C.jboolean {
|
func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong) C.jboolean {
|
||||||
w := cgo.Handle(view).Value().(*window)
|
w := cgo.Handle(view).Value().(*window)
|
||||||
if w.callbacks.Event(key.Event{Name: key.NameBack}) {
|
if w.processEvent(key.Event{Name: key.NameBack}) {
|
||||||
return C.JNI_TRUE
|
return C.JNI_TRUE
|
||||||
}
|
}
|
||||||
return C.JNI_FALSE
|
return C.JNI_FALSE
|
||||||
@@ -584,7 +593,8 @@ func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong)
|
|||||||
//export Java_org_gioui_GioView_onFocusChange
|
//export Java_org_gioui_GioView_onFocusChange
|
||||||
func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) {
|
func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) {
|
||||||
w := cgo.Handle(view).Value().(*window)
|
w := cgo.Handle(view).Value().(*window)
|
||||||
w.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE})
|
w.config.Focused = focus == C.JNI_TRUE
|
||||||
|
w.processEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
//export Java_org_gioui_GioView_onWindowInsets
|
//export Java_org_gioui_GioView_onWindowInsets
|
||||||
@@ -596,9 +606,7 @@ func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C
|
|||||||
left: int(left),
|
left: int(left),
|
||||||
right: int(right),
|
right: int(right),
|
||||||
}
|
}
|
||||||
if w.stage >= system.StageInactive {
|
w.draw(env, true)
|
||||||
w.draw(env, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//export Java_org_gioui_GioView_initializeAccessibilityNodeInfo
|
//export Java_org_gioui_GioView_initializeAccessibilityNodeInfo
|
||||||
@@ -659,7 +667,35 @@ func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNode, off image.Point, info C.jobject) error {
|
func (w *window) ProcessEvent(e event.Event) {
|
||||||
|
w.processEvent(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) processEvent(e event.Event) bool {
|
||||||
|
if !w.callbacks.ProcessEvent(e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
w.loop.FlushEvents()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Event() event.Event {
|
||||||
|
return w.loop.Event()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Invalidate() {
|
||||||
|
w.loop.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Run(f func()) {
|
||||||
|
w.loop.Run(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Frame(frame *op.Ops) {
|
||||||
|
w.loop.Frame(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem input.SemanticNode, off image.Point, info C.jobject) error {
|
||||||
for _, ch := range sem.Children {
|
for _, ch := range sem.Children {
|
||||||
err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID)))
|
err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -702,7 +738,7 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNod
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if d.Gestures&router.ClickGesture != 0 {
|
if d.Gestures&input.ClickGesture != 0 {
|
||||||
addAction(ACTION_CLICK)
|
addAction(ACTION_CLICK)
|
||||||
}
|
}
|
||||||
clsName := android.strings.androidViewView
|
clsName := android.strings.androidViewView
|
||||||
@@ -747,25 +783,23 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNod
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) virtualIDFor(id router.SemanticID) C.jint {
|
func (w *window) virtualIDFor(id input.SemanticID) C.jint {
|
||||||
// TODO: Android virtual IDs are 32-bit Java integers, but childID is a int64.
|
|
||||||
if id == w.semantic.rootID {
|
if id == w.semantic.rootID {
|
||||||
return HOST_VIEW_ID
|
return HOST_VIEW_ID
|
||||||
}
|
}
|
||||||
return C.jint(id)
|
return C.jint(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) semIDFor(virtID C.jint) router.SemanticID {
|
func (w *window) semIDFor(virtID C.jint) input.SemanticID {
|
||||||
if virtID == HOST_VIEW_ID {
|
if virtID == HOST_VIEW_ID {
|
||||||
return w.semantic.rootID
|
return w.semantic.rootID
|
||||||
}
|
}
|
||||||
return router.SemanticID(virtID)
|
return input.SemanticID(virtID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) detach(env *C.JNIEnv) {
|
func (w *window) detach(env *C.JNIEnv) {
|
||||||
callVoidMethod(env, w.view, gioView.unregister)
|
callVoidMethod(env, w.view, gioView.unregister)
|
||||||
w.callbacks.Event(ViewEvent{})
|
w.processEvent(AndroidViewEvent{})
|
||||||
w.callbacks.SetDriver(nil)
|
|
||||||
w.handle.Delete()
|
w.handle.Delete()
|
||||||
C.jni_DeleteGlobalRef(env, w.view)
|
C.jni_DeleteGlobalRef(env, w.view)
|
||||||
w.view = 0
|
w.view = 0
|
||||||
@@ -776,18 +810,10 @@ func (w *window) setVisible(env *C.JNIEnv) {
|
|||||||
if width == 0 || height == 0 {
|
if width == 0 || height == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.setStage(system.StageRunning)
|
w.visible = true
|
||||||
w.draw(env, true)
|
w.draw(env, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) setStage(stage system.Stage) {
|
|
||||||
if stage == w.stage {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.stage = stage
|
|
||||||
w.callbacks.Event(system.StageEvent{stage})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *window) setVisual(visID int) error {
|
func (w *window) setVisual(visID int) error {
|
||||||
if C.ANativeWindow_setBuffersGeometry(w.win, 0, 0, C.int32_t(visID)) != 0 {
|
if C.ANativeWindow_setBuffersGeometry(w.win, 0, 0, C.int32_t(visID)) != 0 {
|
||||||
return errors.New("ANativeWindow_setBuffersGeometry failed")
|
return errors.New("ANativeWindow_setBuffersGeometry failed")
|
||||||
@@ -824,10 +850,13 @@ func (w *window) SetAnimating(anim bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) draw(env *C.JNIEnv, sync bool) {
|
func (w *window) draw(env *C.JNIEnv, sync bool) {
|
||||||
|
if !w.visible {
|
||||||
|
return
|
||||||
|
}
|
||||||
size := image.Pt(int(C.ANativeWindow_getWidth(w.win)), int(C.ANativeWindow_getHeight(w.win)))
|
size := image.Pt(int(C.ANativeWindow_getWidth(w.win)), int(C.ANativeWindow_getHeight(w.win)))
|
||||||
if size != w.config.Size {
|
if size != w.config.Size {
|
||||||
w.config.Size = size
|
w.config.Size = size
|
||||||
w.callbacks.Event(ConfigEvent{Config: w.config})
|
w.processEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
if size.X == 0 || size.Y == 0 {
|
if size.X == 0 || size.Y == 0 {
|
||||||
return
|
return
|
||||||
@@ -835,14 +864,14 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
|
|||||||
const inchPrDp = 1.0 / 160
|
const inchPrDp = 1.0 / 160
|
||||||
ppdp := float32(w.dpi) * inchPrDp
|
ppdp := float32(w.dpi) * inchPrDp
|
||||||
dppp := unit.Dp(1.0 / ppdp)
|
dppp := unit.Dp(1.0 / ppdp)
|
||||||
insets := system.Insets{
|
insets := Insets{
|
||||||
Top: unit.Dp(w.insets.top) * dppp,
|
Top: unit.Dp(w.insets.top) * dppp,
|
||||||
Bottom: unit.Dp(w.insets.bottom) * dppp,
|
Bottom: unit.Dp(w.insets.bottom) * dppp,
|
||||||
Left: unit.Dp(w.insets.left) * dppp,
|
Left: unit.Dp(w.insets.left) * dppp,
|
||||||
Right: unit.Dp(w.insets.right) * dppp,
|
Right: unit.Dp(w.insets.right) * dppp,
|
||||||
}
|
}
|
||||||
w.callbacks.Event(frameEvent{
|
w.processEvent(frameEvent{
|
||||||
FrameEvent: system.FrameEvent{
|
FrameEvent: FrameEvent{
|
||||||
Now: time.Now(),
|
Now: time.Now(),
|
||||||
Size: w.config.Size,
|
Size: w.config.Size,
|
||||||
Insets: insets,
|
Insets: insets,
|
||||||
@@ -896,8 +925,8 @@ func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) {
|
|||||||
f(env)
|
f(env)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertKeyCode(code C.jint) (string, bool) {
|
func convertKeyCode(code C.jint) (key.Name, bool) {
|
||||||
var n string
|
var n key.Name
|
||||||
switch code {
|
switch code {
|
||||||
case C.AKEYCODE_FORWARD_DEL:
|
case C.AKEYCODE_FORWARD_DEL:
|
||||||
n = key.NameDeleteForward
|
n = key.NameDeleteForward
|
||||||
@@ -941,7 +970,7 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
|
|||||||
if pressed == C.JNI_TRUE {
|
if pressed == C.JNI_TRUE {
|
||||||
state = key.Press
|
state = key.Press
|
||||||
}
|
}
|
||||||
w.callbacks.Event(key.Event{Name: n, State: state})
|
w.processEvent(key.Event{Name: n, State: state})
|
||||||
}
|
}
|
||||||
if pressed == C.JNI_TRUE && r != 0 && r != '\n' { // Checking for "\n" to prevent duplication with key.NameEnter (gio#224).
|
if pressed == C.JNI_TRUE && r != 0 && r != '\n' { // Checking for "\n" to prevent duplication with key.NameEnter (gio#224).
|
||||||
w.callbacks.EditorInsert(string(rune(r)))
|
w.callbacks.EditorInsert(string(rune(r)))
|
||||||
@@ -951,18 +980,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
|
||||||
}
|
}
|
||||||
@@ -991,8 +1020,8 @@ func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C
|
|||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.callbacks.Event(pointer.Event{
|
w.processEvent(pointer.Event{
|
||||||
Type: typ,
|
Kind: kind,
|
||||||
Source: src,
|
Source: src,
|
||||||
Buttons: btns,
|
Buttons: btns,
|
||||||
PointerID: pointer.ID(pointerID),
|
PointerID: pointer.ID(pointerID),
|
||||||
@@ -1143,6 +1172,8 @@ func (w *window) ShowTextInput(show bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) SetInputHint(mode key.InputHint) {
|
func (w *window) SetInputHint(mode key.InputHint) {
|
||||||
|
w.inputHint = mode
|
||||||
|
|
||||||
// Constants defined at https://developer.android.com/reference/android/text/InputType.
|
// Constants defined at https://developer.android.com/reference/android/text/InputType.
|
||||||
const (
|
const (
|
||||||
TYPE_NULL = 0
|
TYPE_NULL = 0
|
||||||
@@ -1150,6 +1181,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 +1205,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
|
||||||
}
|
}
|
||||||
@@ -1283,17 +1317,14 @@ func findClass(env *C.JNIEnv, name string) C.jclass {
|
|||||||
return C.jni_FindClass(env, cn)
|
return C.jni_FindClass(env, cn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func osMain() {
|
func newWindow(window *callbacks, options []Option) {
|
||||||
}
|
|
||||||
|
|
||||||
func newWindow(window *callbacks, options []Option) error {
|
|
||||||
mainWindow.in <- windowAndConfig{window, options}
|
mainWindow.in <- windowAndConfig{window, options}
|
||||||
return <-mainWindow.errs
|
<-mainWindow.windows
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) WriteClipboard(s string) {
|
func (w *window) WriteClipboard(mime string, s []byte) {
|
||||||
runInJVM(javaVM(), func(env *C.JNIEnv) {
|
runInJVM(javaVM(), func(env *C.JNIEnv) {
|
||||||
jstr := javaString(env, s)
|
jstr := javaString(env, string(s))
|
||||||
callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard,
|
callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard,
|
||||||
jvalue(android.appCtx), jvalue(jstr))
|
jvalue(android.appCtx), jvalue(jstr))
|
||||||
})
|
})
|
||||||
@@ -1307,47 +1338,56 @@ func (w *window) ReadClipboard() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
content := goString(env, C.jstring(c))
|
content := goString(env, C.jstring(c))
|
||||||
w.callbacks.Event(clipboard.Event{Text: content})
|
w.processEvent(transfer.DataEvent{
|
||||||
|
Type: "application/text",
|
||||||
|
Open: func() io.ReadCloser {
|
||||||
|
return io.NopCloser(strings.NewReader(content))
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) Configure(options []Option) {
|
func (w *window) Configure(options []Option) {
|
||||||
|
cnf := w.config
|
||||||
|
cnf.apply(unit.Metric{}, options)
|
||||||
runInJVM(javaVM(), func(env *C.JNIEnv) {
|
runInJVM(javaVM(), func(env *C.JNIEnv) {
|
||||||
prev := w.config
|
w.setConfig(env, cnf)
|
||||||
cnf := w.config
|
|
||||||
cnf.apply(unit.Metric{}, options)
|
|
||||||
// Decorations are never disabled.
|
|
||||||
cnf.Decorated = true
|
|
||||||
|
|
||||||
if prev.Orientation != cnf.Orientation {
|
|
||||||
w.config.Orientation = cnf.Orientation
|
|
||||||
setOrientation(env, w.view, cnf.Orientation)
|
|
||||||
}
|
|
||||||
if prev.NavigationColor != cnf.NavigationColor {
|
|
||||||
w.config.NavigationColor = cnf.NavigationColor
|
|
||||||
setNavigationColor(env, w.view, cnf.NavigationColor)
|
|
||||||
}
|
|
||||||
if prev.StatusColor != cnf.StatusColor {
|
|
||||||
w.config.StatusColor = cnf.StatusColor
|
|
||||||
setStatusColor(env, w.view, cnf.StatusColor)
|
|
||||||
}
|
|
||||||
if prev.Mode != cnf.Mode {
|
|
||||||
switch cnf.Mode {
|
|
||||||
case Fullscreen:
|
|
||||||
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE)
|
|
||||||
w.config.Mode = Fullscreen
|
|
||||||
case Windowed:
|
|
||||||
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE)
|
|
||||||
w.config.Mode = Windowed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cnf.Decorated != prev.Decorated {
|
|
||||||
w.config.Decorated = cnf.Decorated
|
|
||||||
}
|
|
||||||
w.callbacks.Event(ConfigEvent{Config: w.config})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *window) setConfig(env *C.JNIEnv, cnf Config) {
|
||||||
|
prev := w.config
|
||||||
|
// Decorations are never disabled.
|
||||||
|
cnf.Decorated = true
|
||||||
|
|
||||||
|
if prev.Orientation != cnf.Orientation {
|
||||||
|
w.config.Orientation = cnf.Orientation
|
||||||
|
setOrientation(env, w.view, cnf.Orientation)
|
||||||
|
}
|
||||||
|
if prev.NavigationColor != cnf.NavigationColor {
|
||||||
|
w.config.NavigationColor = cnf.NavigationColor
|
||||||
|
setNavigationColor(env, w.view, cnf.NavigationColor)
|
||||||
|
}
|
||||||
|
if prev.StatusColor != cnf.StatusColor {
|
||||||
|
w.config.StatusColor = cnf.StatusColor
|
||||||
|
setStatusColor(env, w.view, cnf.StatusColor)
|
||||||
|
}
|
||||||
|
if prev.Mode != cnf.Mode {
|
||||||
|
switch cnf.Mode {
|
||||||
|
case Fullscreen:
|
||||||
|
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE)
|
||||||
|
w.config.Mode = Fullscreen
|
||||||
|
case Windowed:
|
||||||
|
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE)
|
||||||
|
w.config.Mode = Windowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cnf.Decorated != prev.Decorated {
|
||||||
|
w.config.Decorated = cnf.Decorated
|
||||||
|
}
|
||||||
|
w.processEvent(ConfigEvent{Config: w.config})
|
||||||
|
}
|
||||||
|
|
||||||
func (w *window) Perform(system.Action) {}
|
func (w *window) Perform(system.Action) {}
|
||||||
|
|
||||||
func (w *window) SetCursor(cursor pointer.Cursor) {
|
func (w *window) SetCursor(cursor pointer.Cursor) {
|
||||||
@@ -1356,9 +1396,10 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) Wakeup() {
|
func (w *window) wakeup() {
|
||||||
runOnMain(func(env *C.JNIEnv) {
|
runOnMain(func(env *C.JNIEnv) {
|
||||||
w.callbacks.Event(wakeupEvent{})
|
w.loop.Wakeup()
|
||||||
|
w.loop.FlushEvents()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1449,4 +1490,5 @@ func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_ ViewEvent) ImplementsEvent() {}
|
func (AndroidViewEvent) implementsViewEvent() {}
|
||||||
|
func (AndroidViewEvent) ImplementsEvent() {}
|
||||||
|
|||||||
+26
-21
@@ -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);
|
||||||
@@ -15,8 +15,8 @@ __attribute__ ((visibility ("hidden"))) void gio_hideCursor();
|
|||||||
__attribute__ ((visibility ("hidden"))) void gio_showCursor();
|
__attribute__ ((visibility ("hidden"))) void gio_showCursor();
|
||||||
__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID);
|
__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID);
|
||||||
|
|
||||||
static bool isMainThread() {
|
static int isMainThread() {
|
||||||
return [NSThread isMainThread];
|
return [NSThread isMainThread] ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static NSUInteger nsstringLength(CFTypeRef cstr) {
|
static NSUInteger nsstringLength(CFTypeRef cstr) {
|
||||||
@@ -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,11 +70,14 @@ 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.
|
||||||
func runOnMain(f func()) {
|
func runOnMain(f func()) {
|
||||||
if C.isMainThread() {
|
if isMainThread() {
|
||||||
f()
|
f()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -84,6 +87,10 @@ func runOnMain(f func()) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isMainThread() bool {
|
||||||
|
return C.isMainThread() != 0
|
||||||
|
}
|
||||||
|
|
||||||
//export gio_dispatchMainFuncs
|
//export gio_dispatchMainFuncs
|
||||||
func gio_dispatchMainFuncs() {
|
func gio_dispatchMainFuncs() {
|
||||||
for {
|
for {
|
||||||
@@ -121,25 +128,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 +207,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,9 +263,3 @@ func windowSetCursor(from, to pointer.Cursor) pointer.Cursor {
|
|||||||
C.gio_setCursor(C.NSUInteger(macosCursorID[to]))
|
C.gio_setCursor(C.NSUInteger(macosCursorID[to]))
|
||||||
return to
|
return to
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) Wakeup() {
|
|
||||||
runOnMain(func() {
|
|
||||||
w.w.Event(wakeupEvent{})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
#include "_cgo_export.h"
|
|
||||||
|
|
||||||
void gio_wakeupMainThread(void) {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
gio_dispatchMainFuncs();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+118
-73
@@ -12,6 +12,8 @@ package app
|
|||||||
#include <UIKit/UIKit.h>
|
#include <UIKit/UIKit.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
|
||||||
|
|
||||||
struct drawParams {
|
struct drawParams {
|
||||||
CGFloat dpi, sdpi;
|
CGFloat dpi, sdpi;
|
||||||
CGFloat width, height;
|
CGFloat width, height;
|
||||||
@@ -72,21 +74,26 @@ import "C"
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
|
"io"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"runtime/cgo"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf16"
|
"unicode/utf16"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/io/clipboard"
|
"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/system"
|
||||||
|
"gioui.org/io/transfer"
|
||||||
|
"gioui.org/op"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ViewEvent struct {
|
type UIKitViewEvent struct {
|
||||||
// ViewController is a CFTypeRef for the UIViewController backing a Window.
|
// ViewController is a CFTypeRef for the UIViewController backing a Window.
|
||||||
ViewController uintptr
|
ViewController uintptr
|
||||||
}
|
}
|
||||||
@@ -95,18 +102,17 @@ type window struct {
|
|||||||
view C.CFTypeRef
|
view C.CFTypeRef
|
||||||
w *callbacks
|
w *callbacks
|
||||||
displayLink *displayLink
|
displayLink *displayLink
|
||||||
|
loop *eventLoop
|
||||||
|
|
||||||
visible bool
|
hidden bool
|
||||||
cursor pointer.Cursor
|
cursor pointer.Cursor
|
||||||
config Config
|
config Config
|
||||||
|
|
||||||
pointerMap []C.CFTypeRef
|
pointerMap []C.CFTypeRef
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainWindow = newWindowRendezvous()
|
var mainWindow = newWindowRendezvous()
|
||||||
|
|
||||||
var views = make(map[C.CFTypeRef]*window)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Darwin requires UI operations happen on the main thread only.
|
// Darwin requires UI operations happen on the main thread only.
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
@@ -114,55 +120,59 @@ func init() {
|
|||||||
|
|
||||||
//export onCreate
|
//export onCreate
|
||||||
func onCreate(view, controller C.CFTypeRef) {
|
func onCreate(view, controller C.CFTypeRef) {
|
||||||
|
wopts := <-mainWindow.out
|
||||||
w := &window{
|
w := &window{
|
||||||
view: view,
|
view: view,
|
||||||
|
w: wopts.window,
|
||||||
}
|
}
|
||||||
dl, err := NewDisplayLink(func() {
|
w.loop = newEventLoop(w.w, w.wakeup)
|
||||||
|
w.w.SetDriver(w)
|
||||||
|
mainWindow.windows <- struct{}{}
|
||||||
|
dl, err := newDisplayLink(func() {
|
||||||
w.draw(false)
|
w.draw(false)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
w.w.ProcessEvent(DestroyEvent{Err: err})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
w.displayLink = dl
|
w.displayLink = dl
|
||||||
wopts := <-mainWindow.out
|
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
|
||||||
w.w = wopts.window
|
|
||||||
w.w.SetDriver(w)
|
|
||||||
views[view] = w
|
|
||||||
w.Configure(wopts.options)
|
w.Configure(wopts.options)
|
||||||
w.w.Event(system.StageEvent{Stage: system.StagePaused})
|
w.ProcessEvent(UIKitViewEvent{ViewController: uintptr(controller)})
|
||||||
w.w.Event(ViewEvent{ViewController: uintptr(controller)})
|
}
|
||||||
|
|
||||||
|
func viewFor(h C.uintptr_t) *window {
|
||||||
|
return cgo.Handle(h).Value().(*window)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onDraw
|
//export gio_onDraw
|
||||||
func gio_onDraw(view C.CFTypeRef) {
|
func gio_onDraw(h C.uintptr_t) {
|
||||||
w := views[view]
|
w := viewFor(h)
|
||||||
w.draw(true)
|
w.draw(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) draw(sync bool) {
|
func (w *window) draw(sync bool) {
|
||||||
|
if w.hidden {
|
||||||
|
return
|
||||||
|
}
|
||||||
params := C.viewDrawParams(w.view)
|
params := C.viewDrawParams(w.view)
|
||||||
if params.width == 0 || params.height == 0 {
|
if params.width == 0 || params.height == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
wasVisible := w.visible
|
|
||||||
w.visible = true
|
|
||||||
if !wasVisible {
|
|
||||||
w.w.Event(system.StageEvent{Stage: system.StageRunning})
|
|
||||||
}
|
|
||||||
const inchPrDp = 1.0 / 163
|
const inchPrDp = 1.0 / 163
|
||||||
m := unit.Metric{
|
m := unit.Metric{
|
||||||
PxPerDp: float32(params.dpi) * inchPrDp,
|
PxPerDp: float32(params.dpi) * inchPrDp,
|
||||||
PxPerSp: float32(params.sdpi) * inchPrDp,
|
PxPerSp: float32(params.sdpi) * inchPrDp,
|
||||||
}
|
}
|
||||||
dppp := unit.Dp(1. / m.PxPerDp)
|
dppp := unit.Dp(1. / m.PxPerDp)
|
||||||
w.w.Event(frameEvent{
|
w.ProcessEvent(frameEvent{
|
||||||
FrameEvent: system.FrameEvent{
|
FrameEvent: FrameEvent{
|
||||||
Now: time.Now(),
|
Now: time.Now(),
|
||||||
Size: image.Point{
|
Size: image.Point{
|
||||||
X: int(params.width + .5),
|
X: int(params.width + .5),
|
||||||
Y: int(params.height + .5),
|
Y: int(params.height + .5),
|
||||||
},
|
},
|
||||||
Insets: system.Insets{
|
Insets: Insets{
|
||||||
Top: unit.Dp(params.top) * dppp,
|
Top: unit.Dp(params.top) * dppp,
|
||||||
Bottom: unit.Dp(params.bottom) * dppp,
|
Bottom: unit.Dp(params.bottom) * dppp,
|
||||||
Left: unit.Dp(params.left) * dppp,
|
Left: unit.Dp(params.left) * dppp,
|
||||||
@@ -175,26 +185,34 @@ func (w *window) draw(sync bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//export onStop
|
//export onStop
|
||||||
func onStop(view C.CFTypeRef) {
|
func onStop(h C.uintptr_t) {
|
||||||
w := views[view]
|
w := viewFor(h)
|
||||||
w.visible = false
|
w.hidden = true
|
||||||
w.w.Event(system.StageEvent{Stage: system.StagePaused})
|
}
|
||||||
|
|
||||||
|
//export onStart
|
||||||
|
func onStart(h C.uintptr_t) {
|
||||||
|
w := viewFor(h)
|
||||||
|
w.hidden = false
|
||||||
|
w.draw(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export onDestroy
|
//export onDestroy
|
||||||
func onDestroy(view C.CFTypeRef) {
|
func onDestroy(h C.uintptr_t) {
|
||||||
w := views[view]
|
w := viewFor(h)
|
||||||
delete(views, view)
|
w.ProcessEvent(UIKitViewEvent{})
|
||||||
w.w.Event(ViewEvent{})
|
w.ProcessEvent(DestroyEvent{})
|
||||||
w.w.Event(system.DestroyEvent{})
|
|
||||||
w.displayLink.Close()
|
w.displayLink.Close()
|
||||||
|
w.displayLink = nil
|
||||||
|
cgo.Handle(h).Delete()
|
||||||
w.view = 0
|
w.view = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
//export onFocus
|
//export onFocus
|
||||||
func onFocus(view C.CFTypeRef, focus int) {
|
func onFocus(h C.uintptr_t, focus int) {
|
||||||
w := views[view]
|
w := viewFor(h)
|
||||||
w.w.Event(key.FocusEvent{Focus: focus != 0})
|
w.config.Focused = focus != 0
|
||||||
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
//export onLowMemory
|
//export onLowMemory
|
||||||
@@ -204,56 +222,56 @@ func onLowMemory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//export onUpArrow
|
//export onUpArrow
|
||||||
func onUpArrow(view C.CFTypeRef) {
|
func onUpArrow(h C.uintptr_t) {
|
||||||
views[view].onKeyCommand(key.NameUpArrow)
|
viewFor(h).onKeyCommand(key.NameUpArrow)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export onDownArrow
|
//export onDownArrow
|
||||||
func onDownArrow(view C.CFTypeRef) {
|
func onDownArrow(h C.uintptr_t) {
|
||||||
views[view].onKeyCommand(key.NameDownArrow)
|
viewFor(h).onKeyCommand(key.NameDownArrow)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export onLeftArrow
|
//export onLeftArrow
|
||||||
func onLeftArrow(view C.CFTypeRef) {
|
func onLeftArrow(h C.uintptr_t) {
|
||||||
views[view].onKeyCommand(key.NameLeftArrow)
|
viewFor(h).onKeyCommand(key.NameLeftArrow)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export onRightArrow
|
//export onRightArrow
|
||||||
func onRightArrow(view C.CFTypeRef) {
|
func onRightArrow(h C.uintptr_t) {
|
||||||
views[view].onKeyCommand(key.NameRightArrow)
|
viewFor(h).onKeyCommand(key.NameRightArrow)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export onDeleteBackward
|
//export onDeleteBackward
|
||||||
func onDeleteBackward(view C.CFTypeRef) {
|
func onDeleteBackward(h C.uintptr_t) {
|
||||||
views[view].onKeyCommand(key.NameDeleteBackward)
|
viewFor(h).onKeyCommand(key.NameDeleteBackward)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export onText
|
//export onText
|
||||||
func onText(view, str C.CFTypeRef) {
|
func onText(h C.uintptr_t, str C.CFTypeRef) {
|
||||||
w := views[view]
|
w := viewFor(h)
|
||||||
w.w.EditorInsert(nsstringToString(str))
|
w.w.EditorInsert(nsstringToString(str))
|
||||||
}
|
}
|
||||||
|
|
||||||
//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(h C.uintptr_t, last C.int, 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
|
||||||
}
|
}
|
||||||
w := views[view]
|
w := viewFor(h)
|
||||||
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.ProcessEvent(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,
|
||||||
@@ -265,11 +283,16 @@ func (w *window) ReadClipboard() {
|
|||||||
cstr := C.readClipboard()
|
cstr := C.readClipboard()
|
||||||
defer C.CFRelease(cstr)
|
defer C.CFRelease(cstr)
|
||||||
content := nsstringToString(cstr)
|
content := nsstringToString(cstr)
|
||||||
w.w.Event(clipboard.Event{Text: content})
|
w.ProcessEvent(transfer.DataEvent{
|
||||||
|
Type: "application/text",
|
||||||
|
Open: func() io.ReadCloser {
|
||||||
|
return io.NopCloser(strings.NewReader(content))
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) WriteClipboard(s string) {
|
func (w *window) WriteClipboard(mime string, s []byte) {
|
||||||
u16 := utf16.Encode([]rune(s))
|
u16 := utf16.Encode([]rune(string(s)))
|
||||||
var chars *C.unichar
|
var chars *C.unichar
|
||||||
if len(u16) > 0 {
|
if len(u16) > 0 {
|
||||||
chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
|
chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
|
||||||
@@ -280,7 +303,7 @@ func (w *window) WriteClipboard(s string) {
|
|||||||
func (w *window) Configure([]Option) {
|
func (w *window) Configure([]Option) {
|
||||||
// Decorations are never disabled.
|
// Decorations are never disabled.
|
||||||
w.config.Decorated = true
|
w.config.Decorated = true
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) EditorStateChanged(old, new editorState) {}
|
func (w *window) EditorStateChanged(old, new editorState) {}
|
||||||
@@ -288,10 +311,6 @@ func (w *window) EditorStateChanged(old, new editorState) {}
|
|||||||
func (w *window) Perform(system.Action) {}
|
func (w *window) Perform(system.Action) {}
|
||||||
|
|
||||||
func (w *window) SetAnimating(anim bool) {
|
func (w *window) SetAnimating(anim bool) {
|
||||||
v := w.view
|
|
||||||
if v == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if anim {
|
if anim {
|
||||||
w.displayLink.Start()
|
w.displayLink.Start()
|
||||||
} else {
|
} else {
|
||||||
@@ -303,8 +322,8 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
|
|||||||
w.cursor = windowSetCursor(w.cursor, cursor)
|
w.cursor = windowSetCursor(w.cursor, cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) onKeyCommand(name string) {
|
func (w *window) onKeyCommand(name key.Name) {
|
||||||
w.w.Event(key.Event{
|
w.ProcessEvent(key.Event{
|
||||||
Name: name,
|
Name: name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -343,12 +362,30 @@ func (w *window) ShowTextInput(show bool) {
|
|||||||
|
|
||||||
func (w *window) SetInputHint(_ key.InputHint) {}
|
func (w *window) SetInputHint(_ key.InputHint) {}
|
||||||
|
|
||||||
func newWindow(win *callbacks, options []Option) error {
|
func (w *window) ProcessEvent(e event.Event) {
|
||||||
mainWindow.in <- windowAndConfig{win, options}
|
w.w.ProcessEvent(e)
|
||||||
return <-mainWindow.errs
|
w.loop.FlushEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
func osMain() {
|
func (w *window) Event() event.Event {
|
||||||
|
return w.loop.Event()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Invalidate() {
|
||||||
|
w.loop.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Run(f func()) {
|
||||||
|
w.loop.Run(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Frame(frame *op.Ops) {
|
||||||
|
w.loop.Frame(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWindow(win *callbacks, options []Option) {
|
||||||
|
mainWindow.in <- windowAndConfig{win, options}
|
||||||
|
<-mainWindow.windows
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_runMain
|
//export gio_runMain
|
||||||
@@ -356,4 +393,12 @@ func gio_runMain() {
|
|||||||
runMain()
|
runMain()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_ ViewEvent) ImplementsEvent() {}
|
func (w *window) wakeup() {
|
||||||
|
runOnMain(func() {
|
||||||
|
w.loop.Wakeup()
|
||||||
|
w.loop.FlushEvents()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UIKitViewEvent) implementsViewEvent() {}
|
||||||
|
func (UIKitViewEvent) ImplementsEvent() {}
|
||||||
|
|||||||
+37
-38
@@ -11,6 +11,7 @@
|
|||||||
__attribute__ ((visibility ("hidden"))) Class gio_layerClass(void);
|
__attribute__ ((visibility ("hidden"))) Class gio_layerClass(void);
|
||||||
|
|
||||||
@interface GioView: UIView <UIKeyInput>
|
@interface GioView: UIView <UIKeyInput>
|
||||||
|
@property uintptr_t handle;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation GioViewController
|
@implementation GioViewController
|
||||||
@@ -54,33 +55,33 @@ CGFloat _keyboardHeight;
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)applicationWillEnterForeground:(UIApplication *)application {
|
- (void)applicationWillEnterForeground:(UIApplication *)application {
|
||||||
UIView *drawView = self.view.subviews[0];
|
GioView *view = (GioView *)self.view.subviews[0];
|
||||||
if (drawView != nil) {
|
if (view != nil) {
|
||||||
gio_onDraw((__bridge CFTypeRef)drawView);
|
onStart(view.handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||||
UIView *drawView = self.view.subviews[0];
|
GioView *view = (GioView *)self.view.subviews[0];
|
||||||
if (drawView != nil) {
|
if (view != nil) {
|
||||||
onStop((__bridge CFTypeRef)drawView);
|
onStop(view.handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewDidDisappear:(BOOL)animated {
|
- (void)viewDidDisappear:(BOOL)animated {
|
||||||
[super viewDidDisappear:animated];
|
[super viewDidDisappear:animated];
|
||||||
CFTypeRef viewRef = (__bridge CFTypeRef)self.view.subviews[0];
|
GioView *view = (GioView *)self.view.subviews[0];
|
||||||
onDestroy(viewRef);
|
onDestroy(view.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewDidLayoutSubviews {
|
- (void)viewDidLayoutSubviews {
|
||||||
[super viewDidLayoutSubviews];
|
[super viewDidLayoutSubviews];
|
||||||
UIView *view = self.view.subviews[0];
|
GioView *view = (GioView *)self.view.subviews[0];
|
||||||
CGRect frame = self.view.bounds;
|
CGRect frame = self.view.bounds;
|
||||||
// Adjust view bounds to make room for the keyboard.
|
// Adjust view bounds to make room for the keyboard.
|
||||||
frame.size.height -= _keyboardHeight;
|
frame.size.height -= _keyboardHeight;
|
||||||
view.frame = frame;
|
view.frame = frame;
|
||||||
gio_onDraw((__bridge CFTypeRef)view);
|
gio_onDraw(view.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)didReceiveMemoryWarning {
|
- (void)didReceiveMemoryWarning {
|
||||||
@@ -101,11 +102,10 @@ CGFloat _keyboardHeight;
|
|||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIEvent *event) {
|
static void handleTouches(int last, GioView *view, NSSet<UITouch *> *touches, UIEvent *event) {
|
||||||
CGFloat scale = view.contentScaleFactor;
|
CGFloat scale = view.contentScaleFactor;
|
||||||
NSUInteger i = 0;
|
NSUInteger i = 0;
|
||||||
NSUInteger n = [touches count];
|
NSUInteger n = [touches count];
|
||||||
CFTypeRef viewRef = (__bridge CFTypeRef)view;
|
|
||||||
for (UITouch *touch in touches) {
|
for (UITouch *touch in touches) {
|
||||||
CFTypeRef touchRef = (__bridge CFTypeRef)touch;
|
CFTypeRef touchRef = (__bridge CFTypeRef)touch;
|
||||||
i++;
|
i++;
|
||||||
@@ -116,13 +116,16 @@ static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIE
|
|||||||
CGPoint loc = [coalescedTouch locationInView:view];
|
CGPoint loc = [coalescedTouch locationInView:view];
|
||||||
j++;
|
j++;
|
||||||
int lastTouch = last && i == n && j == m;
|
int lastTouch = last && i == n && j == m;
|
||||||
onTouch(lastTouch, viewRef, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]);
|
onTouch(view.handle, lastTouch, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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();
|
||||||
}
|
}
|
||||||
@@ -148,13 +151,13 @@ NSArray<UIKeyCommand *> *_keyCommands;
|
|||||||
|
|
||||||
- (void)onWindowDidBecomeKey:(NSNotification *)note {
|
- (void)onWindowDidBecomeKey:(NSNotification *)note {
|
||||||
if (self.isFirstResponder) {
|
if (self.isFirstResponder) {
|
||||||
onFocus((__bridge CFTypeRef)self, YES);
|
onFocus(self.handle, YES);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onWindowDidResignKey:(NSNotification *)note {
|
- (void)onWindowDidResignKey:(NSNotification *)note {
|
||||||
if (self.isFirstResponder) {
|
if (self.isFirstResponder) {
|
||||||
onFocus((__bridge CFTypeRef)self, NO);
|
onFocus(self.handle, NO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +178,7 @@ NSArray<UIKeyCommand *> *_keyCommands;
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)insertText:(NSString *)text {
|
- (void)insertText:(NSString *)text {
|
||||||
onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)text);
|
onText(self.handle, (__bridge CFTypeRef)text);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)canBecomeFirstResponder {
|
- (BOOL)canBecomeFirstResponder {
|
||||||
@@ -187,23 +190,23 @@ NSArray<UIKeyCommand *> *_keyCommands;
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)deleteBackward {
|
- (void)deleteBackward {
|
||||||
onDeleteBackward((__bridge CFTypeRef)self);
|
onDeleteBackward(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onUpArrow {
|
- (void)onUpArrow {
|
||||||
onUpArrow((__bridge CFTypeRef)self);
|
onUpArrow(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onDownArrow {
|
- (void)onDownArrow {
|
||||||
onDownArrow((__bridge CFTypeRef)self);
|
onDownArrow(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onLeftArrow {
|
- (void)onLeftArrow {
|
||||||
onLeftArrow((__bridge CFTypeRef)self);
|
onLeftArrow(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)onRightArrow {
|
- (void)onRightArrow {
|
||||||
onRightArrow((__bridge CFTypeRef)self);
|
onRightArrow(self.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSArray<UIKeyCommand *> *)keyCommands {
|
- (NSArray<UIKeyCommand *> *)keyCommands {
|
||||||
@@ -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]];
|
||||||
@@ -283,3 +271,14 @@ void gio_showCursor() {
|
|||||||
void gio_setCursor(NSUInteger curID) {
|
void gio_setCursor(NSUInteger curID) {
|
||||||
// Not supported.
|
// Not supported.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
|
||||||
|
GioView *v = (__bridge GioView *)viewRef;
|
||||||
|
v.handle = handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
void gio_wakeupMainThread(void) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
gio_dispatchMainFuncs();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+103
-105
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
"time"
|
"time"
|
||||||
@@ -13,16 +14,18 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"gioui.org/internal/f32color"
|
"gioui.org/internal/f32color"
|
||||||
|
"gioui.org/op"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/io/clipboard"
|
"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/system"
|
||||||
|
"gioui.org/io/transfer"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ViewEvent struct {
|
type JSViewEvent struct {
|
||||||
Element js.Value
|
Element js.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,9 +56,6 @@ type window struct {
|
|||||||
composing bool
|
composing bool
|
||||||
requestFocus bool
|
requestFocus bool
|
||||||
|
|
||||||
chanAnimation chan struct{}
|
|
||||||
chanRedraw chan struct{}
|
|
||||||
|
|
||||||
config Config
|
config Config
|
||||||
inset f32.Point
|
inset f32.Point
|
||||||
scale float32
|
scale float32
|
||||||
@@ -68,7 +68,7 @@ type window struct {
|
|||||||
contextStatus contextStatus
|
contextStatus contextStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWindow(win *callbacks, options []Option) error {
|
func newWindow(win *callbacks, options []Option) {
|
||||||
doc := js.Global().Get("document")
|
doc := js.Global().Get("document")
|
||||||
cont := getContainer(doc)
|
cont := getContainer(doc)
|
||||||
cnv := createCanvas(doc)
|
cnv := createCanvas(doc)
|
||||||
@@ -83,7 +83,9 @@ func newWindow(win *callbacks, options []Option) error {
|
|||||||
head: doc.Get("head"),
|
head: doc.Get("head"),
|
||||||
clipboard: js.Global().Get("navigator").Get("clipboard"),
|
clipboard: js.Global().Get("navigator").Get("clipboard"),
|
||||||
wakeups: make(chan struct{}, 1),
|
wakeups: make(chan struct{}, 1),
|
||||||
|
w: win,
|
||||||
}
|
}
|
||||||
|
w.w.SetDriver(w)
|
||||||
w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
|
w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
|
||||||
w.browserHistory = w.window.Get("history")
|
w.browserHistory = w.window.Get("history")
|
||||||
w.visualViewport = w.window.Get("visualViewport")
|
w.visualViewport = w.window.Get("visualViewport")
|
||||||
@@ -93,42 +95,28 @@ func newWindow(win *callbacks, options []Option) error {
|
|||||||
if screen := w.window.Get("screen"); screen.Truthy() {
|
if screen := w.window.Get("screen"); screen.Truthy() {
|
||||||
w.screenOrientation = screen.Get("orientation")
|
w.screenOrientation = screen.Get("orientation")
|
||||||
}
|
}
|
||||||
w.chanAnimation = make(chan struct{}, 1)
|
|
||||||
w.chanRedraw = make(chan struct{}, 1)
|
|
||||||
w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
|
w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
w.chanAnimation <- struct{}{}
|
w.draw(false)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
|
w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
content := args[0].String()
|
content := args[0].String()
|
||||||
go win.Event(clipboard.Event{Text: content})
|
w.processEvent(transfer.DataEvent{
|
||||||
|
Type: "application/text",
|
||||||
|
Open: func() io.ReadCloser {
|
||||||
|
return io.NopCloser(strings.NewReader(content))
|
||||||
|
},
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
w.addEventListeners()
|
w.addEventListeners()
|
||||||
w.addHistory()
|
w.addHistory()
|
||||||
w.w = win
|
|
||||||
|
|
||||||
go func() {
|
w.Configure(options)
|
||||||
defer w.cleanup()
|
w.blur()
|
||||||
w.w.SetDriver(w)
|
w.processEvent(JSViewEvent{Element: cont})
|
||||||
w.Configure(options)
|
w.resize()
|
||||||
w.blur()
|
w.draw(true)
|
||||||
w.w.Event(ViewEvent{Element: cont})
|
|
||||||
w.w.Event(system.StageEvent{Stage: system.StageRunning})
|
|
||||||
w.resize()
|
|
||||||
w.draw(true)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-w.wakeups:
|
|
||||||
w.w.Event(wakeupEvent{})
|
|
||||||
case <-w.chanAnimation:
|
|
||||||
w.animCallback()
|
|
||||||
case <-w.chanRedraw:
|
|
||||||
w.draw(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getContainer(doc js.Value) js.Value {
|
func getContainer(doc js.Value) js.Value {
|
||||||
@@ -188,12 +176,12 @@ func (w *window) addEventListeners() {
|
|||||||
w.cnv.Set("width", 0)
|
w.cnv.Set("width", 0)
|
||||||
w.cnv.Set("height", 0)
|
w.cnv.Set("height", 0)
|
||||||
w.resize()
|
w.resize()
|
||||||
w.requestRedraw()
|
w.draw(true)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} {
|
w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} {
|
||||||
w.resize()
|
w.resize()
|
||||||
w.requestRedraw()
|
w.draw(true)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} {
|
w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} {
|
||||||
@@ -201,22 +189,11 @@ func (w *window) addEventListeners() {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} {
|
w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} {
|
||||||
if w.w.Event(key.Event{Name: key.NameBack}) {
|
if w.processEvent(key.Event{Name: key.NameBack}) {
|
||||||
return w.browserHistory.Call("forward")
|
return w.browserHistory.Call("forward")
|
||||||
}
|
}
|
||||||
return w.browserHistory.Call("back")
|
return w.browserHistory.Call("back")
|
||||||
})
|
})
|
||||||
w.addEventListener(w.document, "visibilitychange", func(this js.Value, args []js.Value) interface{} {
|
|
||||||
ev := system.StageEvent{}
|
|
||||||
switch w.document.Get("visibilityState").String() {
|
|
||||||
case "hidden", "prerender", "unloaded":
|
|
||||||
ev.Stage = system.StagePaused
|
|
||||||
default:
|
|
||||||
ev.Stage = system.StageRunning
|
|
||||||
}
|
|
||||||
w.w.Event(ev)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} {
|
w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} {
|
||||||
w.pointerEvent(pointer.Move, 0, 0, args[0])
|
w.pointerEvent(pointer.Move, 0, 0, args[0])
|
||||||
return nil
|
return nil
|
||||||
@@ -274,18 +251,20 @@ func (w *window) addEventListeners() {
|
|||||||
w.touches[i] = js.Null()
|
w.touches[i] = js.Null()
|
||||||
}
|
}
|
||||||
w.touches = w.touches[:0]
|
w.touches = w.touches[:0]
|
||||||
w.w.Event(pointer.Event{
|
w.processEvent(pointer.Event{
|
||||||
Type: pointer.Cancel,
|
Kind: pointer.Cancel,
|
||||||
Source: pointer.Touch,
|
Source: pointer.Touch,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} {
|
w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} {
|
||||||
w.w.Event(key.FocusEvent{Focus: true})
|
w.config.Focused = true
|
||||||
|
w.processEvent(ConfigEvent{Config: w.config})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
|
w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
|
||||||
w.w.Event(key.FocusEvent{Focus: false})
|
w.config.Focused = false
|
||||||
|
w.processEvent(ConfigEvent{Config: w.config})
|
||||||
w.blur()
|
w.blur()
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -358,6 +337,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"
|
||||||
}
|
}
|
||||||
@@ -372,10 +353,50 @@ func (w *window) keyEvent(e js.Value, ks key.State) {
|
|||||||
Modifiers: modifiersFor(e),
|
Modifiers: modifiersFor(e),
|
||||||
State: ks,
|
State: ks,
|
||||||
}
|
}
|
||||||
w.w.Event(cmd)
|
w.processEvent(cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *window) ProcessEvent(e event.Event) {
|
||||||
|
w.processEvent(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) processEvent(e event.Event) bool {
|
||||||
|
if !w.w.ProcessEvent(e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case w.wakeups <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Event() event.Event {
|
||||||
|
for {
|
||||||
|
evt, ok := w.w.nextEvent()
|
||||||
|
if ok {
|
||||||
|
if _, destroy := evt.(DestroyEvent); destroy {
|
||||||
|
w.cleanup()
|
||||||
|
}
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
<-w.wakeups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Invalidate() {
|
||||||
|
w.w.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Run(f func()) {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Frame(frame *op.Ops) {
|
||||||
|
w.w.ProcessFrame(frame, nil)
|
||||||
|
}
|
||||||
|
|
||||||
// modifiersFor returns the modifier set for a DOM MouseEvent or
|
// modifiersFor returns the modifier set for a DOM MouseEvent or
|
||||||
// KeyEvent.
|
// KeyEvent.
|
||||||
func modifiersFor(e js.Value) key.Modifiers {
|
func modifiersFor(e js.Value) key.Modifiers {
|
||||||
@@ -396,7 +417,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")
|
||||||
@@ -423,8 +444,8 @@ func (w *window) touchEvent(typ pointer.Type, e js.Value) {
|
|||||||
X: float32(x) * scale,
|
X: float32(x) * scale,
|
||||||
Y: float32(y) * scale,
|
Y: float32(y) * scale,
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.processEvent(pointer.Event{
|
||||||
Type: typ,
|
Kind: kind,
|
||||||
Source: pointer.Touch,
|
Source: pointer.Touch,
|
||||||
Position: pos,
|
Position: pos,
|
||||||
PointerID: pid,
|
PointerID: pid,
|
||||||
@@ -446,7 +467,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")
|
||||||
@@ -473,8 +494,8 @@ func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
|
|||||||
if jbtns&4 != 0 {
|
if jbtns&4 != 0 {
|
||||||
btns |= pointer.ButtonTertiary
|
btns |= pointer.ButtonTertiary
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.processEvent(pointer.Event{
|
||||||
Type: typ,
|
Kind: kind,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: btns,
|
Buttons: btns,
|
||||||
Position: pos,
|
Position: pos,
|
||||||
@@ -500,17 +521,6 @@ func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.F
|
|||||||
return jsf
|
return jsf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) animCallback() {
|
|
||||||
anim := w.animating
|
|
||||||
w.animRequested = anim
|
|
||||||
if anim {
|
|
||||||
w.requestAnimationFrame.Invoke(w.redraw)
|
|
||||||
}
|
|
||||||
if anim {
|
|
||||||
w.draw(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *window) EditorStateChanged(old, new editorState) {}
|
func (w *window) EditorStateChanged(old, new editorState) {}
|
||||||
|
|
||||||
func (w *window) SetAnimating(anim bool) {
|
func (w *window) SetAnimating(anim bool) {
|
||||||
@@ -531,14 +541,14 @@ func (w *window) ReadClipboard() {
|
|||||||
w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback)
|
w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) WriteClipboard(s string) {
|
func (w *window) WriteClipboard(mime string, s []byte) {
|
||||||
if w.clipboard.IsUndefined() {
|
if w.clipboard.IsUndefined() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if w.clipboard.Get("writeText").IsUndefined() {
|
if w.clipboard.Get("writeText").IsUndefined() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.clipboard.Call("writeText", s)
|
w.clipboard.Call("writeText", string(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) Configure(options []Option) {
|
func (w *window) Configure(options []Option) {
|
||||||
@@ -566,7 +576,7 @@ func (w *window) Configure(options []Option) {
|
|||||||
if cnf.Decorated != prev.Decorated {
|
if cnf.Decorated != prev.Decorated {
|
||||||
w.config.Decorated = cnf.Decorated
|
w.config.Decorated = cnf.Decorated
|
||||||
}
|
}
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.processEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) Perform(system.Action) {}
|
func (w *window) Perform(system.Action) {}
|
||||||
@@ -605,23 +615,14 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
|
|||||||
style.Set("cursor", webCursor[cursor])
|
style.Set("cursor", webCursor[cursor])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) Wakeup() {
|
|
||||||
select {
|
|
||||||
case w.wakeups <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *window) ShowTextInput(show bool) {
|
func (w *window) ShowTextInput(show bool) {
|
||||||
// Run in a goroutine to avoid a deadlock if the
|
// Run in a goroutine to avoid a deadlock if the
|
||||||
// focus change result in an event.
|
// focus change result in an event.
|
||||||
go func() {
|
if show {
|
||||||
if show {
|
w.focus()
|
||||||
w.focus()
|
} else {
|
||||||
} else {
|
w.blur()
|
||||||
w.blur()
|
}
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) SetInputHint(mode key.InputHint) {
|
func (w *window) SetInputHint(mode key.InputHint) {
|
||||||
@@ -638,7 +639,7 @@ func (w *window) resize() {
|
|||||||
}
|
}
|
||||||
if size != w.config.Size {
|
if size != w.config.Size {
|
||||||
w.config.Size = size
|
w.config.Size = size
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.processEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() {
|
if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() {
|
||||||
@@ -658,13 +659,20 @@ func (w *window) draw(sync bool) {
|
|||||||
if w.contextStatus == contextStatusLost {
|
if w.contextStatus == contextStatusLost {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
anim := w.animating
|
||||||
|
w.animRequested = anim
|
||||||
|
if anim {
|
||||||
|
w.requestAnimationFrame.Invoke(w.redraw)
|
||||||
|
} else if !sync {
|
||||||
|
return
|
||||||
|
}
|
||||||
size, insets, metric := w.getConfig()
|
size, insets, metric := w.getConfig()
|
||||||
if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 {
|
if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.w.Event(frameEvent{
|
w.processEvent(frameEvent{
|
||||||
FrameEvent: system.FrameEvent{
|
FrameEvent: FrameEvent{
|
||||||
Now: time.Now(),
|
Now: time.Now(),
|
||||||
Size: size,
|
Size: size,
|
||||||
Insets: insets,
|
Insets: insets,
|
||||||
@@ -674,10 +682,10 @@ func (w *window) draw(sync bool) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) getConfig() (image.Point, system.Insets, unit.Metric) {
|
func (w *window) getConfig() (image.Point, Insets, unit.Metric) {
|
||||||
invscale := unit.Dp(1. / w.scale)
|
invscale := unit.Dp(1. / w.scale)
|
||||||
return image.Pt(w.config.Size.X, w.config.Size.Y),
|
return image.Pt(w.config.Size.X, w.config.Size.Y),
|
||||||
system.Insets{
|
Insets{
|
||||||
Bottom: unit.Dp(w.inset.Y) * invscale,
|
Bottom: unit.Dp(w.inset.Y) * invscale,
|
||||||
Right: unit.Dp(w.inset.X) * invscale,
|
Right: unit.Dp(w.inset.X) * invscale,
|
||||||
}, unit.Metric{
|
}, unit.Metric{
|
||||||
@@ -733,19 +741,8 @@ func (w *window) navigationColor(c color.NRGBA) {
|
|||||||
theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B}))
|
theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) requestRedraw() {
|
func translateKey(k string) (key.Name, bool) {
|
||||||
select {
|
var n key.Name
|
||||||
case w.chanRedraw <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func osMain() {
|
|
||||||
select {}
|
|
||||||
}
|
|
||||||
|
|
||||||
func translateKey(k string) (string, bool) {
|
|
||||||
var n string
|
|
||||||
|
|
||||||
switch k {
|
switch k {
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
@@ -812,11 +809,12 @@ func translateKey(k string) (string, bool) {
|
|||||||
r, s := utf8.DecodeRuneInString(k)
|
r, s := utf8.DecodeRuneInString(k)
|
||||||
// If there is exactly one printable character, return that.
|
// If there is exactly one printable character, return that.
|
||||||
if s == len(k) && unicode.IsPrint(r) {
|
if s == len(k) && unicode.IsPrint(r) {
|
||||||
return strings.ToUpper(k), true
|
return key.Name(strings.ToUpper(k)), true
|
||||||
}
|
}
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
return n, true
|
return n, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_ ViewEvent) ImplementsEvent() {}
|
func (JSViewEvent) implementsViewEvent() {}
|
||||||
|
func (JSViewEvent) ImplementsEvent() {}
|
||||||
|
|||||||
+342
-225
@@ -8,16 +8,21 @@ package app
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
|
"io"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"runtime/cgo"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"gioui.org/internal/f32"
|
"gioui.org/internal/f32"
|
||||||
"gioui.org/io/clipboard"
|
"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/system"
|
||||||
|
"gioui.org/io/transfer"
|
||||||
|
"gioui.org/op"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
|
|
||||||
_ "gioui.org/internal/cocoainit"
|
_ "gioui.org/internal/cocoainit"
|
||||||
@@ -34,9 +39,10 @@ import (
|
|||||||
#define MOUSE_DOWN 3
|
#define MOUSE_DOWN 3
|
||||||
#define MOUSE_SCROLL 4
|
#define MOUSE_SCROLL 4
|
||||||
|
|
||||||
__attribute__ ((visibility ("hidden"))) void gio_main(void);
|
__attribute__ ((visibility ("hidden"))) void gio_initApp(void);
|
||||||
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void);
|
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void);
|
||||||
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
|
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
|
||||||
|
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
|
||||||
|
|
||||||
static void writeClipboard(CFTypeRef str) {
|
static void writeClipboard(CFTypeRef str) {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
@@ -56,144 +62,204 @@ static CFTypeRef readClipboard(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static CGFloat viewHeight(CFTypeRef viewRef) {
|
static CGFloat viewHeight(CFTypeRef viewRef) {
|
||||||
NSView *view = (__bridge NSView *)viewRef;
|
@autoreleasepool {
|
||||||
return [view bounds].size.height;
|
NSView *view = (__bridge NSView *)viewRef;
|
||||||
|
return [view bounds].size.height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static CGFloat viewWidth(CFTypeRef viewRef) {
|
static CGFloat viewWidth(CFTypeRef viewRef) {
|
||||||
NSView *view = (__bridge NSView *)viewRef;
|
@autoreleasepool {
|
||||||
return [view bounds].size.width;
|
NSView *view = (__bridge NSView *)viewRef;
|
||||||
|
return [view bounds].size.width;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static CGFloat getScreenBackingScale(void) {
|
static CGFloat getScreenBackingScale(void) {
|
||||||
return [NSScreen.mainScreen backingScaleFactor];
|
@autoreleasepool {
|
||||||
|
return [NSScreen.mainScreen backingScaleFactor];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static CGFloat getViewBackingScale(CFTypeRef viewRef) {
|
static CGFloat getViewBackingScale(CFTypeRef viewRef) {
|
||||||
NSView *view = (__bridge NSView *)viewRef;
|
@autoreleasepool {
|
||||||
return [view.window backingScaleFactor];
|
NSView *view = (__bridge NSView *)viewRef;
|
||||||
|
return [view.window backingScaleFactor];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setNeedsDisplay(CFTypeRef viewRef) {
|
static void setNeedsDisplay(CFTypeRef viewRef) {
|
||||||
NSView *view = (__bridge NSView *)viewRef;
|
@autoreleasepool {
|
||||||
[view setNeedsDisplay:YES];
|
NSView *view = (__bridge NSView *)viewRef;
|
||||||
|
[view setNeedsDisplay:YES];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static NSPoint cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) {
|
static NSPoint cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) {
|
||||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
return [window cascadeTopLeftFromPoint:topLeft];
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||||
|
return [window cascadeTopLeftFromPoint:topLeft];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void makeKeyAndOrderFront(CFTypeRef windowRef) {
|
static void makeKeyAndOrderFront(CFTypeRef windowRef) {
|
||||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
[window makeKeyAndOrderFront:nil];
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||||
|
[window makeKeyAndOrderFront:nil];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void toggleFullScreen(CFTypeRef windowRef) {
|
static void toggleFullScreen(CFTypeRef windowRef) {
|
||||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
[window toggleFullScreen:nil];
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||||
|
[window toggleFullScreen:nil];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static NSWindowStyleMask getWindowStyleMask(CFTypeRef windowRef) {
|
static NSWindowStyleMask getWindowStyleMask(CFTypeRef windowRef) {
|
||||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
return [window styleMask];
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||||
|
return [window styleMask];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setWindowStyleMask(CFTypeRef windowRef, NSWindowStyleMask mask) {
|
static void setWindowStyleMask(CFTypeRef windowRef, NSWindowStyleMask mask) {
|
||||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
window.styleMask = mask;
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||||
|
window.styleMask = mask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setWindowTitleVisibility(CFTypeRef windowRef, NSWindowTitleVisibility state) {
|
static void setWindowTitleVisibility(CFTypeRef windowRef, NSWindowTitleVisibility state) {
|
||||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
window.titleVisibility = state;
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||||
|
window.titleVisibility = state;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setWindowTitlebarAppearsTransparent(CFTypeRef windowRef, int transparent) {
|
static void setWindowTitlebarAppearsTransparent(CFTypeRef windowRef, int transparent) {
|
||||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
window.titlebarAppearsTransparent = (BOOL)transparent;
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||||
|
window.titlebarAppearsTransparent = (BOOL)transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setWindowStandardButtonHidden(CFTypeRef windowRef, NSWindowButton btn, int hide) {
|
static void setWindowStandardButtonHidden(CFTypeRef windowRef, NSWindowButton btn, int hide) {
|
||||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
[window standardWindowButton:btn].hidden = (BOOL)hide;
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||||
|
[window standardWindowButton:btn].hidden = (BOOL)hide;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void performWindowDragWithEvent(CFTypeRef windowRef, CFTypeRef evt) {
|
static void performWindowDragWithEvent(CFTypeRef windowRef, CFTypeRef evt) {
|
||||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
[window performWindowDragWithEvent:(__bridge NSEvent*)evt];
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||||
|
[window performWindowDragWithEvent:(__bridge NSEvent*)evt];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void closeWindow(CFTypeRef windowRef) {
|
static void closeWindow(CFTypeRef windowRef) {
|
||||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
[window performClose:nil];
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||||
|
[window performClose:nil];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
|
static void setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
|
||||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
NSSize size = NSMakeSize(width, height);
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||||
[window setContentSize:size];
|
NSSize size = NSMakeSize(width, height);
|
||||||
|
[window setContentSize:size];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
|
static void setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
|
||||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
window.contentMinSize = NSMakeSize(width, height);
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||||
|
window.contentMinSize = NSMakeSize(width, height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
|
static void setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
|
||||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
window.contentMaxSize = NSMakeSize(width, height);
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||||
|
window.contentMaxSize = NSMakeSize(width, height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setScreenFrame(CFTypeRef windowRef, CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
|
static void setScreenFrame(CFTypeRef windowRef, CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
|
||||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
NSRect r = NSMakeRect(x, y, w, h);
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||||
[window setFrame:r display:YES];
|
NSRect r = NSMakeRect(x, y, w, h);
|
||||||
|
[window setFrame:r display:YES];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void hideWindow(CFTypeRef windowRef) {
|
static void hideWindow(CFTypeRef windowRef) {
|
||||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
[window miniaturize:window];
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||||
|
[window miniaturize:window];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void unhideWindow(CFTypeRef windowRef) {
|
static void unhideWindow(CFTypeRef windowRef) {
|
||||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
[window deminiaturize:window];
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||||
|
[window deminiaturize:window];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static NSRect getScreenFrame(CFTypeRef windowRef) {
|
static NSRect getScreenFrame(CFTypeRef windowRef) {
|
||||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
return [[window screen] frame];
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||||
|
return [[window screen] frame];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setTitle(CFTypeRef windowRef, CFTypeRef titleRef) {
|
static void setTitle(CFTypeRef windowRef, CFTypeRef titleRef) {
|
||||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
window.title = (__bridge NSString *)titleRef;
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||||
|
window.title = (__bridge NSString *)titleRef;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static int isWindowZoomed(CFTypeRef windowRef) {
|
static int isWindowZoomed(CFTypeRef windowRef) {
|
||||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
return window.zoomed ? 1 : 0;
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||||
|
return window.zoomed ? 1 : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void zoomWindow(CFTypeRef windowRef) {
|
static void zoomWindow(CFTypeRef windowRef) {
|
||||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
[window zoom:nil];
|
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||||
|
[window zoom:nil];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static CFTypeRef layerForView(CFTypeRef viewRef) {
|
static CFTypeRef layerForView(CFTypeRef viewRef) {
|
||||||
NSView *view = (__bridge NSView *)viewRef;
|
@autoreleasepool {
|
||||||
return (__bridge CFTypeRef)view.layer;
|
NSView *view = (__bridge NSView *)viewRef;
|
||||||
|
return (__bridge CFTypeRef)view.layer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static CFTypeRef windowForView(CFTypeRef viewRef) {
|
static CFTypeRef windowForView(CFTypeRef viewRef) {
|
||||||
NSView *view = (__bridge NSView *)viewRef;
|
@autoreleasepool {
|
||||||
return (__bridge CFTypeRef)view.window;
|
NSView *view = (__bridge NSView *)viewRef;
|
||||||
|
return (__bridge CFTypeRef)view.window;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void raiseWindow(CFTypeRef windowRef) {
|
static void raiseWindow(CFTypeRef windowRef) {
|
||||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
@autoreleasepool {
|
||||||
[window makeKeyAndOrderFront:nil];
|
NSRunningApplication *currentApp = [NSRunningApplication currentApplication];
|
||||||
|
if (![currentApp isActive]) {
|
||||||
|
[currentApp activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
|
||||||
|
}
|
||||||
|
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||||
|
[window makeKeyAndOrderFront:nil];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static CFTypeRef createInputContext(CFTypeRef clientRef) {
|
static CFTypeRef createInputContext(CFTypeRef clientRef) {
|
||||||
@@ -205,23 +271,33 @@ static CFTypeRef createInputContext(CFTypeRef clientRef) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void discardMarkedText(CFTypeRef viewRef) {
|
static void discardMarkedText(CFTypeRef viewRef) {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
|
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
|
||||||
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
|
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
|
||||||
if (view == [ctx client]) {
|
if (view == [ctx client]) {
|
||||||
[ctx discardMarkedText];
|
[ctx discardMarkedText];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void invalidateCharacterCoordinates(CFTypeRef viewRef) {
|
static void invalidateCharacterCoordinates(CFTypeRef viewRef) {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
|
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
|
||||||
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
|
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
|
||||||
if (view == [ctx client]) {
|
if (view == [ctx client]) {
|
||||||
[ctx invalidateCharacterCoordinates];
|
[ctx invalidateCharacterCoordinates];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dispatchEvent(void) {
|
||||||
|
@autoreleasepool {
|
||||||
|
NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
|
||||||
|
untilDate:[NSDate distantFuture]
|
||||||
|
inMode:NSDefaultRunLoopMode
|
||||||
|
dequeue:YES];
|
||||||
|
[NSApp sendEvent:event];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
@@ -231,9 +307,9 @@ func init() {
|
|||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ViewEvent notified the client of changes to the window AppKit handles.
|
// AppKitViewEvent notifies the client of changes to the window AppKit handles.
|
||||||
// The handles are retained until another ViewEvent is sent.
|
// The handles are retained until another AppKitViewEvent is sent.
|
||||||
type ViewEvent struct {
|
type AppKitViewEvent struct {
|
||||||
// View is a CFTypeRef for the NSView for the window.
|
// View is a CFTypeRef for the NSView for the window.
|
||||||
View uintptr
|
View uintptr
|
||||||
// Layer is a CFTypeRef of the CALayer of View.
|
// Layer is a CFTypeRef of the CALayer of View.
|
||||||
@@ -243,52 +319,32 @@ type ViewEvent struct {
|
|||||||
type window struct {
|
type window struct {
|
||||||
view C.CFTypeRef
|
view C.CFTypeRef
|
||||||
w *callbacks
|
w *callbacks
|
||||||
stage system.Stage
|
anim bool
|
||||||
|
visible bool
|
||||||
displayLink *displayLink
|
displayLink *displayLink
|
||||||
// redraw is a single entry channel for making sure only one
|
// redraw is a single entry channel for making sure only one
|
||||||
// display link redraw request is in flight.
|
// display link redraw request is in flight.
|
||||||
redraw chan struct{}
|
redraw chan struct{}
|
||||||
cursor pointer.Cursor
|
cursor pointer.Cursor
|
||||||
pointerBtns pointer.Buttons
|
pointerBtns pointer.Buttons
|
||||||
|
loop *eventLoop
|
||||||
|
|
||||||
scale float32
|
scale float32
|
||||||
config Config
|
config Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// viewMap is the mapping from Cocoa NSViews to Go windows.
|
// launched is closed after gio_initApp returns.
|
||||||
var viewMap = make(map[C.CFTypeRef]*window)
|
|
||||||
|
|
||||||
// launched is closed when applicationDidFinishLaunching is called.
|
|
||||||
var launched = make(chan struct{})
|
var launched = make(chan struct{})
|
||||||
|
|
||||||
// nextTopLeft is the offset to use for the next window's call to
|
// nextTopLeft is the offset to use for the next window's call to
|
||||||
// cascadeTopLeftFromPoint.
|
// cascadeTopLeftFromPoint.
|
||||||
var nextTopLeft C.NSPoint
|
var nextTopLeft C.NSPoint
|
||||||
|
|
||||||
// mustView is like lookupView, except that it panics
|
// mainThreadWindow is the window currently in control of the main thread.
|
||||||
// if the view isn't mapped.
|
var mainThreadWindow *window
|
||||||
func mustView(view C.CFTypeRef) *window {
|
|
||||||
w, ok := lookupView(view)
|
|
||||||
if !ok {
|
|
||||||
panic("no window for view")
|
|
||||||
}
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupView(view C.CFTypeRef) (*window, bool) {
|
func windowFor(h C.uintptr_t) *window {
|
||||||
w, exists := viewMap[view]
|
return cgo.Handle(h).Value().(*window)
|
||||||
if !exists {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return w, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteView(view C.CFTypeRef) {
|
|
||||||
delete(viewMap, view)
|
|
||||||
}
|
|
||||||
|
|
||||||
func insertView(view C.CFTypeRef, w *window) {
|
|
||||||
viewMap[view] = w
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) contextView() C.CFTypeRef {
|
func (w *window) contextView() C.CFTypeRef {
|
||||||
@@ -297,13 +353,20 @@ 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.ProcessEvent(transfer.DataEvent{
|
||||||
|
Type: "application/text",
|
||||||
|
Open: func() io.ReadCloser {
|
||||||
|
return io.NopCloser(strings.NewReader(content))
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) WriteClipboard(s string) {
|
func (w *window) WriteClipboard(mime string, s []byte) {
|
||||||
cstr := stringToNSString(s)
|
cstr := stringToNSString(string(s))
|
||||||
defer C.CFRelease(cstr)
|
defer C.CFRelease(cstr)
|
||||||
C.writeClipboard(cstr)
|
C.writeClipboard(cstr)
|
||||||
}
|
}
|
||||||
@@ -365,11 +428,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
|
||||||
@@ -407,7 +470,7 @@ func (w *window) Configure(options []Option) {
|
|||||||
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
|
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
|
||||||
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
|
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
|
||||||
}
|
}
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) setTitle(prev, cnf Config) {
|
func (w *window) setTitle(prev, cnf Config) {
|
||||||
@@ -458,7 +521,8 @@ func (w *window) ShowTextInput(show bool) {}
|
|||||||
func (w *window) SetInputHint(_ key.InputHint) {}
|
func (w *window) SetInputHint(_ key.InputHint) {}
|
||||||
|
|
||||||
func (w *window) SetAnimating(anim bool) {
|
func (w *window) SetAnimating(anim bool) {
|
||||||
if anim {
|
w.anim = anim
|
||||||
|
if w.anim && w.visible {
|
||||||
w.displayLink.Start()
|
w.displayLink.Start()
|
||||||
} else {
|
} else {
|
||||||
w.displayLink.Stop()
|
w.displayLink.Stop()
|
||||||
@@ -475,26 +539,18 @@ func (w *window) runOnMain(f func()) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) setStage(stage system.Stage) {
|
|
||||||
if stage == w.stage {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.stage = stage
|
|
||||||
w.w.Event(system.StageEvent{Stage: stage})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export gio_onKeys
|
//export gio_onKeys
|
||||||
func gio_onKeys(view, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) {
|
func gio_onKeys(h C.uintptr_t, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) {
|
||||||
str := nsstringToString(cstr)
|
str := nsstringToString(cstr)
|
||||||
kmods := convertMods(mods)
|
kmods := convertMods(mods)
|
||||||
ks := key.Release
|
ks := key.Release
|
||||||
if keyDown {
|
if keyDown {
|
||||||
ks = key.Press
|
ks = key.Press
|
||||||
}
|
}
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
for _, k := range str {
|
for _, k := range str {
|
||||||
if n, ok := convertKey(k); ok {
|
if n, ok := convertKey(k); ok {
|
||||||
w.w.Event(key.Event{
|
w.ProcessEvent(key.Event{
|
||||||
Name: n,
|
Name: n,
|
||||||
Modifiers: kmods,
|
Modifiers: kmods,
|
||||||
State: ks,
|
State: ks,
|
||||||
@@ -504,15 +560,15 @@ func gio_onKeys(view, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown
|
|||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onText
|
//export gio_onText
|
||||||
func gio_onText(view, cstr C.CFTypeRef) {
|
func gio_onText(h C.uintptr_t, cstr C.CFTypeRef) {
|
||||||
str := nsstringToString(cstr)
|
str := nsstringToString(cstr)
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
w.w.EditorInsert(str)
|
w.w.EditorInsert(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onMouse
|
//export gio_onMouse
|
||||||
func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) {
|
func gio_onMouse(h C.uintptr_t, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
t := time.Duration(float64(ti)*float64(time.Second) + .5)
|
t := time.Duration(float64(ti)*float64(time.Second) + .5)
|
||||||
xf, yf := float32(x)*w.scale, float32(y)*w.scale
|
xf, yf := float32(x)*w.scale, float32(y)*w.scale
|
||||||
dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale
|
dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale
|
||||||
@@ -523,8 +579,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
|
||||||
@@ -547,8 +605,8 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
|
|||||||
default:
|
default:
|
||||||
panic("invalid direction")
|
panic("invalid direction")
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.ProcessEvent(pointer.Event{
|
||||||
Type: typ,
|
Kind: typ,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Time: t,
|
Time: t,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
@@ -559,35 +617,29 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
|
|||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onDraw
|
//export gio_onDraw
|
||||||
func gio_onDraw(view C.CFTypeRef) {
|
func gio_onDraw(h C.uintptr_t) {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
w.draw()
|
w.draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onFocus
|
//export gio_onFocus
|
||||||
func gio_onFocus(view C.CFTypeRef, focus C.int) {
|
func gio_onFocus(h C.uintptr_t, focus C.int) {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
w.w.Event(key.FocusEvent{Focus: focus == 1})
|
|
||||||
if w.stage >= system.StageInactive {
|
|
||||||
if focus == 0 {
|
|
||||||
w.setStage(system.StageInactive)
|
|
||||||
} else {
|
|
||||||
w.setStage(system.StageRunning)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.SetCursor(w.cursor)
|
w.SetCursor(w.cursor)
|
||||||
|
w.config.Focused = focus == 1
|
||||||
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onChangeScreen
|
//export gio_onChangeScreen
|
||||||
func gio_onChangeScreen(view C.CFTypeRef, did uint64) {
|
func gio_onChangeScreen(h C.uintptr_t, did uint64) {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
w.displayLink.SetDisplayID(did)
|
w.displayLink.SetDisplayID(did)
|
||||||
C.setNeedsDisplay(w.view)
|
C.setNeedsDisplay(w.view)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_hasMarkedText
|
//export gio_hasMarkedText
|
||||||
func gio_hasMarkedText(view C.CFTypeRef) C.int {
|
func gio_hasMarkedText(h C.uintptr_t) C.int {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
state := w.w.EditorState()
|
state := w.w.EditorState()
|
||||||
if state.compose.Start != -1 {
|
if state.compose.Start != -1 {
|
||||||
return 1
|
return 1
|
||||||
@@ -596,8 +648,8 @@ func gio_hasMarkedText(view C.CFTypeRef) C.int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//export gio_markedRange
|
//export gio_markedRange
|
||||||
func gio_markedRange(view C.CFTypeRef) C.NSRange {
|
func gio_markedRange(h C.uintptr_t) C.NSRange {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
state := w.w.EditorState()
|
state := w.w.EditorState()
|
||||||
rng := state.compose
|
rng := state.compose
|
||||||
start, end := rng.Start, rng.End
|
start, end := rng.Start, rng.End
|
||||||
@@ -612,8 +664,8 @@ func gio_markedRange(view C.CFTypeRef) C.NSRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//export gio_selectedRange
|
//export gio_selectedRange
|
||||||
func gio_selectedRange(view C.CFTypeRef) C.NSRange {
|
func gio_selectedRange(h C.uintptr_t) C.NSRange {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
state := w.w.EditorState()
|
state := w.w.EditorState()
|
||||||
rng := state.Selection
|
rng := state.Selection
|
||||||
start, end := rng.Start, rng.End
|
start, end := rng.Start, rng.End
|
||||||
@@ -628,14 +680,14 @@ func gio_selectedRange(view C.CFTypeRef) C.NSRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//export gio_unmarkText
|
//export gio_unmarkText
|
||||||
func gio_unmarkText(view C.CFTypeRef) {
|
func gio_unmarkText(h C.uintptr_t) {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
|
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_setMarkedText
|
//export gio_setMarkedText
|
||||||
func gio_setMarkedText(view, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) {
|
func gio_setMarkedText(h C.uintptr_t, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
str := nsstringToString(cstr)
|
str := nsstringToString(cstr)
|
||||||
state := w.w.EditorState()
|
state := w.w.EditorState()
|
||||||
rng := state.compose
|
rng := state.compose
|
||||||
@@ -674,8 +726,8 @@ func gio_setMarkedText(view, cstr C.CFTypeRef, selRange C.NSRange, replaceRange
|
|||||||
}
|
}
|
||||||
|
|
||||||
//export gio_substringForProposedRange
|
//export gio_substringForProposedRange
|
||||||
func gio_substringForProposedRange(view C.CFTypeRef, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef {
|
func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
state := w.w.EditorState()
|
state := w.w.EditorState()
|
||||||
start, end := state.Snippet.Start, state.Snippet.End
|
start, end := state.Snippet.Start, state.Snippet.End
|
||||||
if start > end {
|
if start > end {
|
||||||
@@ -695,8 +747,8 @@ func gio_substringForProposedRange(view C.CFTypeRef, crng C.NSRange, actual C.NS
|
|||||||
}
|
}
|
||||||
|
|
||||||
//export gio_insertText
|
//export gio_insertText
|
||||||
func gio_insertText(view, cstr C.CFTypeRef, crng C.NSRange) {
|
func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
state := w.w.EditorState()
|
state := w.w.EditorState()
|
||||||
rng := state.compose
|
rng := state.compose
|
||||||
if rng.Start == -1 {
|
if rng.Start == -1 {
|
||||||
@@ -720,13 +772,13 @@ func gio_insertText(view, cstr C.CFTypeRef, crng C.NSRange) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//export gio_characterIndexForPoint
|
//export gio_characterIndexForPoint
|
||||||
func gio_characterIndexForPoint(view C.CFTypeRef, p C.NSPoint) C.NSUInteger {
|
func gio_characterIndexForPoint(h C.uintptr_t, p C.NSPoint) C.NSUInteger {
|
||||||
return C.NSNotFound
|
return C.NSNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_firstRectForCharacterRange
|
//export gio_firstRectForCharacterRange
|
||||||
func gio_firstRectForCharacterRange(view C.CFTypeRef, crng C.NSRange, actual C.NSRangePointer) C.NSRect {
|
func gio_firstRectForCharacterRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.NSRect {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
state := w.w.EditorState()
|
state := w.w.EditorState()
|
||||||
sel := state.Selection
|
sel := state.Selection
|
||||||
u16start := state.UTF16Index(sel.Start)
|
u16start := state.UTF16Index(sel.Start)
|
||||||
@@ -753,6 +805,10 @@ func (w *window) draw() {
|
|||||||
case <-w.redraw:
|
case <-w.redraw:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
w.visible = true
|
||||||
|
if w.anim {
|
||||||
|
w.SetAnimating(w.anim)
|
||||||
|
}
|
||||||
w.scale = float32(C.getViewBackingScale(w.view))
|
w.scale = float32(C.getViewBackingScale(w.view))
|
||||||
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
|
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
|
||||||
sz := image.Point{
|
sz := image.Point{
|
||||||
@@ -761,15 +817,14 @@ func (w *window) draw() {
|
|||||||
}
|
}
|
||||||
if sz != w.config.Size {
|
if sz != w.config.Size {
|
||||||
w.config.Size = sz
|
w.config.Size = sz
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
if sz.X == 0 || sz.Y == 0 {
|
if sz.X == 0 || sz.Y == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cfg := configFor(w.scale)
|
cfg := configFor(w.scale)
|
||||||
w.setStage(system.StageRunning)
|
w.ProcessEvent(frameEvent{
|
||||||
w.w.Event(frameEvent{
|
FrameEvent: FrameEvent{
|
||||||
FrameEvent: system.FrameEvent{
|
|
||||||
Now: time.Now(),
|
Now: time.Now(),
|
||||||
Size: w.config.Size,
|
Size: w.config.Size,
|
||||||
Metric: cfg,
|
Metric: cfg,
|
||||||
@@ -778,6 +833,55 @@ func (w *window) draw() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *window) ProcessEvent(e event.Event) {
|
||||||
|
w.w.ProcessEvent(e)
|
||||||
|
// The main thread window deliver events in Event.
|
||||||
|
if w != mainThreadWindow {
|
||||||
|
w.loop.FlushEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Event() event.Event {
|
||||||
|
if !isMainThread() {
|
||||||
|
return w.loop.Event()
|
||||||
|
}
|
||||||
|
mainThreadWindow = w
|
||||||
|
defer func() { mainThreadWindow = nil }()
|
||||||
|
for {
|
||||||
|
if evt, ok := w.loop.win.nextEvent(); ok {
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
C.dispatchEvent()
|
||||||
|
gio_dispatchMainFuncs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Invalidate() {
|
||||||
|
if isMainThread() {
|
||||||
|
mainThreadWindow = w
|
||||||
|
defer func() { mainThreadWindow = nil }()
|
||||||
|
}
|
||||||
|
w.loop.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Run(f func()) {
|
||||||
|
if isMainThread() {
|
||||||
|
mainThreadWindow = w
|
||||||
|
defer func() { mainThreadWindow = nil }()
|
||||||
|
}
|
||||||
|
w.loop.Run(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Frame(frame *op.Ops) {
|
||||||
|
if !isMainThread() {
|
||||||
|
w.loop.Frame(frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mainThreadWindow = w
|
||||||
|
defer func() { mainThreadWindow = nil }()
|
||||||
|
w.loop.win.ProcessFrame(frame, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func configFor(scale float32) unit.Metric {
|
func configFor(scale float32) unit.Metric {
|
||||||
return unit.Metric{
|
return unit.Metric{
|
||||||
PxPerDp: scale,
|
PxPerDp: scale,
|
||||||
@@ -785,77 +889,87 @@ func configFor(scale float32) unit.Metric {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onClose
|
//export gio_onAttached
|
||||||
func gio_onClose(view C.CFTypeRef) {
|
func gio_onAttached(h C.uintptr_t, attached C.int) {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
|
if attached != 0 {
|
||||||
|
layer := C.layerForView(w.view)
|
||||||
|
w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
|
||||||
|
} else {
|
||||||
|
w.ProcessEvent(AppKitViewEvent{})
|
||||||
|
w.visible = false
|
||||||
|
w.SetAnimating(w.anim)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export gio_onDestroy
|
||||||
|
func gio_onDestroy(h C.uintptr_t) {
|
||||||
|
w := windowFor(h)
|
||||||
|
w.ProcessEvent(DestroyEvent{})
|
||||||
w.displayLink.Close()
|
w.displayLink.Close()
|
||||||
w.w.Event(ViewEvent{})
|
|
||||||
deleteView(view)
|
|
||||||
w.w.Event(system.DestroyEvent{})
|
|
||||||
C.CFRelease(w.view)
|
|
||||||
w.view = 0
|
|
||||||
w.displayLink = nil
|
w.displayLink = nil
|
||||||
|
cgo.Handle(h).Delete()
|
||||||
|
w.view = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onHide
|
//export gio_onHide
|
||||||
func gio_onHide(view C.CFTypeRef) {
|
func gio_onHide(h C.uintptr_t) {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
w.setStage(system.StagePaused)
|
w.visible = false
|
||||||
|
w.SetAnimating(w.anim)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onShow
|
//export gio_onShow
|
||||||
func gio_onShow(view C.CFTypeRef) {
|
func gio_onShow(h C.uintptr_t) {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
w.setStage(system.StageRunning)
|
w.draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onFullscreen
|
//export gio_onFullscreen
|
||||||
func gio_onFullscreen(view C.CFTypeRef) {
|
func gio_onFullscreen(h C.uintptr_t) {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
w.config.Mode = Fullscreen
|
w.config.Mode = Fullscreen
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onWindowed
|
//export gio_onWindowed
|
||||||
func gio_onWindowed(view C.CFTypeRef) {
|
func gio_onWindowed(h C.uintptr_t) {
|
||||||
w := mustView(view)
|
w := windowFor(h)
|
||||||
w.config.Mode = Windowed
|
w.config.Mode = Windowed
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onAppHide
|
func newWindow(win *callbacks, options []Option) {
|
||||||
func gio_onAppHide() {
|
w := &window{
|
||||||
for _, w := range viewMap {
|
redraw: make(chan struct{}, 1),
|
||||||
w.setStage(system.StagePaused)
|
w: win,
|
||||||
}
|
}
|
||||||
}
|
w.loop = newEventLoop(w.w, w.wakeup)
|
||||||
|
if isMainThread() {
|
||||||
//export gio_onAppShow
|
mainThreadWindow = w
|
||||||
func gio_onAppShow() {
|
defer func() { mainThreadWindow = nil }()
|
||||||
for _, w := range viewMap {
|
select {
|
||||||
w.setStage(system.StageRunning)
|
case <-launched:
|
||||||
|
default:
|
||||||
|
// If we're the main thread, initialize the GUI.
|
||||||
|
C.gio_initApp()
|
||||||
|
close(launched)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<-launched
|
||||||
}
|
}
|
||||||
}
|
res := make(chan struct{}, 1)
|
||||||
|
|
||||||
//export gio_onFinishLaunching
|
|
||||||
func gio_onFinishLaunching() {
|
|
||||||
close(launched)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWindow(win *callbacks, options []Option) error {
|
|
||||||
<-launched
|
|
||||||
errch := make(chan error)
|
|
||||||
runOnMain(func() {
|
runOnMain(func() {
|
||||||
w, err := newOSWindow()
|
win.SetDriver(w)
|
||||||
if err != nil {
|
res <- struct{}{}
|
||||||
errch <- err
|
if err := w.init(); err != nil {
|
||||||
|
w.ProcessEvent(DestroyEvent{Err: err})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
errch <- nil
|
|
||||||
w.w = win
|
|
||||||
window := C.gio_createWindow(w.view, 0, 0, 0, 0, 0, 0)
|
window := C.gio_createWindow(w.view, 0, 0, 0, 0, 0, 0)
|
||||||
|
// Release our reference now that the NSWindow has it.
|
||||||
|
C.CFRelease(w.view)
|
||||||
w.updateWindowMode()
|
w.updateWindowMode()
|
||||||
win.SetDriver(w)
|
|
||||||
w.Configure(options)
|
w.Configure(options)
|
||||||
if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
|
if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
|
||||||
// cascadeTopLeftFromPoint treats (0, 0) as a no-op,
|
// cascadeTopLeftFromPoint treats (0, 0) as a no-op,
|
||||||
@@ -865,48 +979,41 @@ func newWindow(win *callbacks, options []Option) error {
|
|||||||
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
|
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
|
||||||
// makeKeyAndOrderFront assumes ownership of our window reference.
|
// makeKeyAndOrderFront assumes ownership of our window reference.
|
||||||
C.makeKeyAndOrderFront(window)
|
C.makeKeyAndOrderFront(window)
|
||||||
layer := C.layerForView(w.view)
|
|
||||||
w.w.Event(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
|
|
||||||
})
|
})
|
||||||
return <-errch
|
<-res
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOSWindow() (*window, error) {
|
func (w *window) init() error {
|
||||||
view := C.gio_createView()
|
view := C.gio_createView()
|
||||||
if view == 0 {
|
if view == 0 {
|
||||||
return nil, errors.New("newOSWindows: failed to create view")
|
return errors.New("newOSWindow: failed to create view")
|
||||||
}
|
}
|
||||||
scale := float32(C.getViewBackingScale(view))
|
scale := float32(C.getViewBackingScale(view))
|
||||||
w := &window{
|
w.scale = scale
|
||||||
view: view,
|
dl, err := newDisplayLink(func() {
|
||||||
scale: scale,
|
|
||||||
redraw: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
dl, err := NewDisplayLink(func() {
|
|
||||||
select {
|
select {
|
||||||
case w.redraw <- struct{}{}:
|
case w.redraw <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.runOnMain(func() {
|
w.runOnMain(func() {
|
||||||
C.setNeedsDisplay(w.view)
|
if w.visible {
|
||||||
|
C.setNeedsDisplay(w.view)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
w.displayLink = dl
|
w.displayLink = dl
|
||||||
if err != nil {
|
if err != nil {
|
||||||
C.CFRelease(view)
|
C.CFRelease(view)
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
insertView(view, w)
|
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
|
||||||
return w, nil
|
w.view = view
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func osMain() {
|
func convertKey(k rune) (key.Name, bool) {
|
||||||
C.gio_main()
|
var n key.Name
|
||||||
}
|
|
||||||
|
|
||||||
func convertKey(k rune) (string, bool) {
|
|
||||||
var n string
|
|
||||||
switch k {
|
switch k {
|
||||||
case 0x1b:
|
case 0x1b:
|
||||||
n = key.NameEscape
|
n = key.NameEscape
|
||||||
@@ -967,7 +1074,7 @@ func convertKey(k rune) (string, bool) {
|
|||||||
if !unicode.IsPrint(k) {
|
if !unicode.IsPrint(k) {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
n = string(k)
|
n = key.Name(k)
|
||||||
}
|
}
|
||||||
return n, true
|
return n, true
|
||||||
}
|
}
|
||||||
@@ -989,4 +1096,14 @@ func convertMods(mods C.NSUInteger) key.Modifiers {
|
|||||||
return kmods
|
return kmods
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_ ViewEvent) ImplementsEvent() {}
|
func (w *window) wakeup() {
|
||||||
|
runOnMain(func() {
|
||||||
|
w.loop.Wakeup()
|
||||||
|
if w != mainThreadWindow {
|
||||||
|
w.loop.FlushEvents()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AppKitViewEvent) implementsViewEvent() {}
|
||||||
|
func (AppKitViewEvent) ImplementsEvent() {}
|
||||||
|
|||||||
+326
-69
@@ -14,40 +14,50 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
|
|||||||
@interface GioWindowDelegate : NSObject<NSWindowDelegate>
|
@interface GioWindowDelegate : NSObject<NSWindowDelegate>
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
|
||||||
|
@property uintptr_t handle;
|
||||||
|
@end
|
||||||
|
|
||||||
@implementation GioWindowDelegate
|
@implementation GioWindowDelegate
|
||||||
- (void)windowWillMiniaturize:(NSNotification *)notification {
|
- (void)windowWillMiniaturize:(NSNotification *)notification {
|
||||||
NSWindow *window = (NSWindow *)[notification object];
|
NSWindow *window = (NSWindow *)[notification object];
|
||||||
gio_onHide((__bridge CFTypeRef)window.contentView);
|
GioView *view = (GioView *)window.contentView;
|
||||||
|
gio_onHide(view.handle);
|
||||||
}
|
}
|
||||||
- (void)windowDidDeminiaturize:(NSNotification *)notification {
|
- (void)windowDidDeminiaturize:(NSNotification *)notification {
|
||||||
NSWindow *window = (NSWindow *)[notification object];
|
NSWindow *window = (NSWindow *)[notification object];
|
||||||
gio_onShow((__bridge CFTypeRef)window.contentView);
|
GioView *view = (GioView *)window.contentView;
|
||||||
|
gio_onShow(view.handle);
|
||||||
}
|
}
|
||||||
- (void)windowWillEnterFullScreen:(NSNotification *)notification {
|
- (void)windowWillEnterFullScreen:(NSNotification *)notification {
|
||||||
NSWindow *window = (NSWindow *)[notification object];
|
NSWindow *window = (NSWindow *)[notification object];
|
||||||
gio_onFullscreen((__bridge CFTypeRef)window.contentView);
|
GioView *view = (GioView *)window.contentView;
|
||||||
|
gio_onFullscreen(view.handle);
|
||||||
}
|
}
|
||||||
- (void)windowWillExitFullScreen:(NSNotification *)notification {
|
- (void)windowWillExitFullScreen:(NSNotification *)notification {
|
||||||
NSWindow *window = (NSWindow *)[notification object];
|
NSWindow *window = (NSWindow *)[notification object];
|
||||||
gio_onWindowed((__bridge CFTypeRef)window.contentView);
|
GioView *view = (GioView *)window.contentView;
|
||||||
|
gio_onWindowed(view.handle);
|
||||||
}
|
}
|
||||||
- (void)windowDidChangeScreen:(NSNotification *)notification {
|
- (void)windowDidChangeScreen:(NSNotification *)notification {
|
||||||
NSWindow *window = (NSWindow *)[notification object];
|
NSWindow *window = (NSWindow *)[notification object];
|
||||||
CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue];
|
CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue];
|
||||||
CFTypeRef view = (__bridge CFTypeRef)window.contentView;
|
GioView *view = (GioView *)window.contentView;
|
||||||
gio_onChangeScreen(view, dispID);
|
gio_onChangeScreen(view.handle, dispID);
|
||||||
}
|
}
|
||||||
- (void)windowDidBecomeKey:(NSNotification *)notification {
|
- (void)windowDidBecomeKey:(NSNotification *)notification {
|
||||||
NSWindow *window = (NSWindow *)[notification object];
|
NSWindow *window = (NSWindow *)[notification object];
|
||||||
gio_onFocus((__bridge CFTypeRef)window.contentView, 1);
|
GioView *view = (GioView *)window.contentView;
|
||||||
|
gio_onFocus(view.handle, 1);
|
||||||
}
|
}
|
||||||
- (void)windowDidResignKey:(NSNotification *)notification {
|
- (void)windowDidResignKey:(NSNotification *)notification {
|
||||||
NSWindow *window = (NSWindow *)[notification object];
|
NSWindow *window = (NSWindow *)[notification object];
|
||||||
gio_onFocus((__bridge CFTypeRef)window.contentView, 0);
|
GioView *view = (GioView *)window.contentView;
|
||||||
|
gio_onFocus(view.handle, 0);
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
|
static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
|
||||||
NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil];
|
NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil];
|
||||||
if (!event.hasPreciseScrollingDeltas) {
|
if (!event.hasPreciseScrollingDeltas) {
|
||||||
// dx and dy are in rows and columns.
|
// dx and dy are in rows and columns.
|
||||||
@@ -56,10 +66,213 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
|||||||
}
|
}
|
||||||
// Origin is in the lower left corner. Convert to upper left.
|
// Origin is in the lower left corner. Convert to upper left.
|
||||||
CGFloat height = view.bounds.size.height;
|
CGFloat height = view.bounds.size.height;
|
||||||
gio_onMouse((__bridge CFTypeRef)view, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
|
gio_onMouse(view.handle, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
|
@interface GioApplication: NSApplication
|
||||||
|
@end
|
||||||
|
|
||||||
|
// Variables for tracking resizes.
|
||||||
|
static struct {
|
||||||
|
NSPoint dir;
|
||||||
|
NSEvent *lastMouseDown;
|
||||||
|
NSPoint off;
|
||||||
|
} resizeState = {};
|
||||||
|
|
||||||
|
static NSBitmapImageRep *nsImageBitmap(NSImage *img) {
|
||||||
|
NSArray<NSImageRep *> *reps = img.representations;
|
||||||
|
if ([reps count] == 0) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSImageRep *rep = reps[0];
|
||||||
|
if (![rep isKindOfClass:[NSBitmapImageRep class]]) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
return (NSBitmapImageRep *)rep;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSCursor *lookupPrivateNSCursor(SEL name) {
|
||||||
|
if (![NSCursor respondsToSelector:name]) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
id obj = [NSCursor performSelector:name];
|
||||||
|
if (![obj isKindOfClass:[NSCursor class]]) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
return (NSCursor *)obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL isEqualNSCursor(NSCursor *c1, SEL name2) {
|
||||||
|
NSCursor *c2 = lookupPrivateNSCursor(name2);
|
||||||
|
if (c2 == nil || !NSEqualPoints(c1.hotSpot, c2.hotSpot)) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
NSImage *img1 = c1.image;
|
||||||
|
NSImage *img2 = c2.image;
|
||||||
|
if (!NSEqualSizes(img1.size, img2.size)) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
NSBitmapImageRep *bit1 = nsImageBitmap(img1);
|
||||||
|
NSBitmapImageRep *bit2 = nsImageBitmap(img2);
|
||||||
|
if (bit1 == nil || bit2 == nil) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
NSInteger n1 = bit1.numberOfPlanes*bit1.bytesPerPlane;
|
||||||
|
NSInteger n2 = bit1.numberOfPlanes*bit1.bytesPerPlane;
|
||||||
|
if (n1 != n2) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
if (memcmp(bit1.bitmapData, bit2.bitmapData, n1) != 0) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@implementation GioApplication
|
||||||
|
- (NSEvent *)nextEventMatchingMask:(NSEventMask)mask
|
||||||
|
untilDate:(NSDate *)expiration
|
||||||
|
inMode:(NSRunLoopMode)mode
|
||||||
|
dequeue:(BOOL)deqFlag {
|
||||||
|
if ([mode isEqualToString:NSEventTrackingRunLoopMode]) {
|
||||||
|
NSEvent *l = resizeState.lastMouseDown;
|
||||||
|
if (l != nil) {
|
||||||
|
//lastMouseDown = nil;
|
||||||
|
NSCursor *cur = [NSCursor currentSystemCursor];
|
||||||
|
NSPoint dir = {};
|
||||||
|
NSPoint off = {};
|
||||||
|
NSSize wsz = [l window].frame.size;
|
||||||
|
NSPoint center = NSMakePoint(wsz.width/2, wsz.height/2);
|
||||||
|
NSPoint p = [l locationInWindow];
|
||||||
|
if (p.x >= center.x) {
|
||||||
|
dir.x = 1;
|
||||||
|
off.x = p.x - wsz.width;
|
||||||
|
} else {
|
||||||
|
dir.x = -1;
|
||||||
|
off.x = p.x;
|
||||||
|
}
|
||||||
|
if (p.y >= center.y) {
|
||||||
|
dir.y = 1;
|
||||||
|
off.y = p.y - wsz.height;
|
||||||
|
} else {
|
||||||
|
dir.y = -1;
|
||||||
|
off.y = p.y;
|
||||||
|
}
|
||||||
|
// The button down coordinate distinguish the four quadrants. Use the
|
||||||
|
// cursor image to determine the precise direction.
|
||||||
|
SEL nw = @selector(_windowResizeNorthWestCursor);
|
||||||
|
SEL n = @selector(_windowResizeNorthCursor);
|
||||||
|
SEL ne = @selector(_windowResizeNorthEastCursor);
|
||||||
|
SEL e = @selector(_windowResizeEastCursor);
|
||||||
|
SEL se = @selector(_windowResizeSouthEastCursor);
|
||||||
|
SEL s = @selector(_windowResizeSouthCursor);
|
||||||
|
SEL sw = @selector(_windowResizeSouthWestCursor);
|
||||||
|
SEL w = @selector(_windowResizeWestCursor);
|
||||||
|
SEL ns = @selector(_windowResizeNorthSouthCursor);
|
||||||
|
SEL ew = @selector(_windowResizeEastWestCursor);
|
||||||
|
SEL nwse = @selector(_windowResizeNorthWestSouthEastCursor);
|
||||||
|
SEL nesw = @selector(_windowResizeNorthEastSouthWestCursor);
|
||||||
|
BOOL match = YES;
|
||||||
|
if (dir.x != 0 && (isEqualNSCursor(cur, ew) || isEqualNSCursor(cur, w) || isEqualNSCursor(cur, e))) {
|
||||||
|
dir.y = 0;
|
||||||
|
}
|
||||||
|
if (dir.y != 0 && (isEqualNSCursor(cur, ns) || isEqualNSCursor(cur, s) || isEqualNSCursor(cur, n))) {
|
||||||
|
dir.x = 0;
|
||||||
|
}
|
||||||
|
// If none of the cursors matched, we may deduce that the resize
|
||||||
|
// direction is one of the corners. However, to ensure that at least
|
||||||
|
// one cursor matches, check the corner cursors.
|
||||||
|
if (dir.x == 1 && dir.y == 1) {
|
||||||
|
if (!isEqualNSCursor(cur, nesw) && !isEqualNSCursor(cur, sw)) {
|
||||||
|
dir = NSZeroPoint;
|
||||||
|
}
|
||||||
|
} else if (dir.x == 1 && dir.y == -1) {
|
||||||
|
if (!isEqualNSCursor(cur, nwse) && !isEqualNSCursor(cur, nw)) {
|
||||||
|
dir = NSZeroPoint;
|
||||||
|
}
|
||||||
|
} else if (dir.x == -1 && dir.y == 1) {
|
||||||
|
if (!isEqualNSCursor(cur, nwse) && !isEqualNSCursor(cur, se)) {
|
||||||
|
dir = NSZeroPoint;
|
||||||
|
}
|
||||||
|
} else if (dir.x == -1 && dir.y == -1) {
|
||||||
|
if (!isEqualNSCursor(cur, nesw) && !isEqualNSCursor(cur, ne)) {
|
||||||
|
dir = NSZeroPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!NSEqualPoints(dir, NSZeroPoint)) {
|
||||||
|
NSEvent *cancel = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp
|
||||||
|
location:l.locationInWindow
|
||||||
|
modifierFlags:l.modifierFlags
|
||||||
|
timestamp:l.timestamp
|
||||||
|
windowNumber:l.windowNumber
|
||||||
|
context:l.context
|
||||||
|
eventNumber:l.eventNumber
|
||||||
|
clickCount:l.clickCount
|
||||||
|
pressure:l.pressure];
|
||||||
|
resizeState.off = off;
|
||||||
|
resizeState.dir = dir;
|
||||||
|
return cancel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [super nextEventMatchingMask:mask untilDate:expiration inMode:mode dequeue:deqFlag];
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface GioWindow: NSWindow
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation GioWindow
|
||||||
|
- (void)sendEvent:(NSEvent *)evt {
|
||||||
|
if (evt.type == NSEventTypeLeftMouseDown) {
|
||||||
|
resizeState.lastMouseDown = evt;
|
||||||
|
}
|
||||||
|
NSPoint dir = resizeState.dir;
|
||||||
|
if (NSEqualPoints(dir, NSZeroPoint)) {
|
||||||
|
[super sendEvent:evt];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (evt.type) {
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
case NSEventTypeLeftMouseUp:
|
||||||
|
resizeState.dir = NSZeroPoint;
|
||||||
|
resizeState.lastMouseDown = nil;
|
||||||
|
return;
|
||||||
|
case NSEventTypeLeftMouseDragged:
|
||||||
|
// Ok to proceed.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
NSPoint loc = evt.locationInWindow;
|
||||||
|
NSPoint off = resizeState.off;
|
||||||
|
loc.x -= off.x;
|
||||||
|
loc.y -= off.y;
|
||||||
|
NSRect frame = [self frame];
|
||||||
|
NSSize min = [self minSize];
|
||||||
|
NSSize max = [self maxSize];
|
||||||
|
CGFloat width = frame.size.width;
|
||||||
|
if (dir.x > 0) {
|
||||||
|
width = loc.x;
|
||||||
|
} else if (dir.x < 0) {
|
||||||
|
width -= loc.x;
|
||||||
|
}
|
||||||
|
width = MIN(max.width, MAX(min.width, width));
|
||||||
|
if (dir.x < 0) {
|
||||||
|
frame.origin.x += frame.size.width - width;
|
||||||
|
}
|
||||||
|
frame.size.width = width;
|
||||||
|
CGFloat height = frame.size.height;
|
||||||
|
if (dir.y > 0) {
|
||||||
|
height = loc.y;
|
||||||
|
} else if (dir.y < 0) {
|
||||||
|
height -= loc.y;
|
||||||
|
}
|
||||||
|
height = MIN(max.height, MAX(min.height, height));
|
||||||
|
if (dir.y < 0) {
|
||||||
|
frame.origin.y += frame.size.height - height;
|
||||||
|
}
|
||||||
|
frame.size.height = height;
|
||||||
|
[self setFrame:frame display:YES animate:NO];
|
||||||
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation GioView
|
@implementation GioView
|
||||||
@@ -70,11 +283,11 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
|||||||
// drawRect is called when OpenGL is used, displayLayer otherwise.
|
// drawRect is called when OpenGL is used, displayLayer otherwise.
|
||||||
// Don't know why.
|
// Don't know why.
|
||||||
- (void)drawRect:(NSRect)r {
|
- (void)drawRect:(NSRect)r {
|
||||||
gio_onDraw((__bridge CFTypeRef)self);
|
gio_onDraw(self.handle);
|
||||||
}
|
}
|
||||||
- (void)displayLayer:(CALayer *)layer {
|
- (void)displayLayer:(CALayer *)layer {
|
||||||
layer.contentsScale = self.window.backingScaleFactor;
|
layer.contentsScale = self.window.backingScaleFactor;
|
||||||
gio_onDraw((__bridge CFTypeRef)self);
|
gio_onDraw(self.handle);
|
||||||
}
|
}
|
||||||
- (CALayer *)makeBackingLayer {
|
- (CALayer *)makeBackingLayer {
|
||||||
CALayer *layer = gio_layerFactory();
|
CALayer *layer = gio_layerFactory();
|
||||||
@@ -82,9 +295,7 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
|||||||
return layer;
|
return layer;
|
||||||
}
|
}
|
||||||
- (void)viewDidMoveToWindow {
|
- (void)viewDidMoveToWindow {
|
||||||
if (self.window == nil) {
|
gio_onAttached(self.handle, self.window != nil ? 1 : 0);
|
||||||
gio_onClose((__bridge CFTypeRef)self);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
- (void)mouseDown:(NSEvent *)event {
|
- (void)mouseDown:(NSEvent *)event {
|
||||||
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
||||||
@@ -92,24 +303,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;
|
||||||
@@ -118,32 +335,31 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
|||||||
- (void)keyDown:(NSEvent *)event {
|
- (void)keyDown:(NSEvent *)event {
|
||||||
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
|
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
|
||||||
NSString *keys = [event charactersIgnoringModifiers];
|
NSString *keys = [event charactersIgnoringModifiers];
|
||||||
gio_onKeys((__bridge CFTypeRef)self, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
|
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
|
||||||
}
|
}
|
||||||
- (void)keyUp:(NSEvent *)event {
|
- (void)keyUp:(NSEvent *)event {
|
||||||
NSString *keys = [event charactersIgnoringModifiers];
|
NSString *keys = [event charactersIgnoringModifiers];
|
||||||
gio_onKeys((__bridge CFTypeRef)self, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
|
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
|
||||||
}
|
}
|
||||||
- (void)insertText:(id)string {
|
- (void)insertText:(id)string {
|
||||||
gio_onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)string);
|
gio_onText(self.handle, (__bridge CFTypeRef)string);
|
||||||
}
|
}
|
||||||
- (void)doCommandBySelector:(SEL)sel {
|
- (void)doCommandBySelector:(SEL)sel {
|
||||||
// Don't pass commands up the responder chain.
|
// Don't pass commands up the responder chain.
|
||||||
// They will end up in a beep.
|
// They will end up in a beep.
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)hasMarkedText {
|
- (BOOL)hasMarkedText {
|
||||||
int res = gio_hasMarkedText((__bridge CFTypeRef)self);
|
int res = gio_hasMarkedText(self.handle);
|
||||||
return res ? YES : NO;
|
return res ? YES : NO;
|
||||||
}
|
}
|
||||||
- (NSRange)markedRange {
|
- (NSRange)markedRange {
|
||||||
return gio_markedRange((__bridge CFTypeRef)self);
|
return gio_markedRange(self.handle);
|
||||||
}
|
}
|
||||||
- (NSRange)selectedRange {
|
- (NSRange)selectedRange {
|
||||||
return gio_selectedRange((__bridge CFTypeRef)self);
|
return gio_selectedRange(self.handle);
|
||||||
}
|
}
|
||||||
- (void)unmarkText {
|
- (void)unmarkText {
|
||||||
gio_unmarkText((__bridge CFTypeRef)self);
|
gio_unmarkText(self.handle);
|
||||||
}
|
}
|
||||||
- (void)setMarkedText:(id)string
|
- (void)setMarkedText:(id)string
|
||||||
selectedRange:(NSRange)selRange
|
selectedRange:(NSRange)selRange
|
||||||
@@ -155,14 +371,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
|||||||
} else {
|
} else {
|
||||||
str = string;
|
str = string;
|
||||||
}
|
}
|
||||||
gio_setMarkedText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, selRange, replaceRange);
|
gio_setMarkedText(self.handle, (__bridge CFTypeRef)str, selRange, replaceRange);
|
||||||
}
|
}
|
||||||
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
|
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range
|
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range
|
||||||
actualRange:(NSRangePointer)actualRange {
|
actualRange:(NSRangePointer)actualRange {
|
||||||
NSString *str = CFBridgingRelease(gio_substringForProposedRange((__bridge CFTypeRef)self, range, actualRange));
|
NSString *str = CFBridgingRelease(gio_substringForProposedRange(self.handle, range, actualRange));
|
||||||
return [[NSAttributedString alloc] initWithString:str attributes:nil];
|
return [[NSAttributedString alloc] initWithString:str attributes:nil];
|
||||||
}
|
}
|
||||||
- (void)insertText:(id)string
|
- (void)insertText:(id)string
|
||||||
@@ -174,17 +390,26 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
|||||||
} else {
|
} else {
|
||||||
str = string;
|
str = string;
|
||||||
}
|
}
|
||||||
gio_insertText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, replaceRange);
|
gio_insertText(self.handle, (__bridge CFTypeRef)str, replaceRange);
|
||||||
}
|
}
|
||||||
- (NSUInteger)characterIndexForPoint:(NSPoint)p {
|
- (NSUInteger)characterIndexForPoint:(NSPoint)p {
|
||||||
return gio_characterIndexForPoint((__bridge CFTypeRef)self, p);
|
return gio_characterIndexForPoint(self.handle, p);
|
||||||
}
|
}
|
||||||
- (NSRect)firstRectForCharacterRange:(NSRange)rng
|
- (NSRect)firstRectForCharacterRange:(NSRange)rng
|
||||||
actualRange:(NSRangePointer)actual {
|
actualRange:(NSRangePointer)actual {
|
||||||
NSRect r = gio_firstRectForCharacterRange((__bridge CFTypeRef)self, rng, actual);
|
NSRect r = gio_firstRectForCharacterRange(self.handle, rng, actual);
|
||||||
r = [self convertRect:r toView:nil];
|
r = [self convertRect:r toView:nil];
|
||||||
return [[self window] convertRectToScreen:r];
|
return [[self window] convertRectToScreen:r];
|
||||||
}
|
}
|
||||||
|
- (void)applicationWillUnhide:(NSNotification *)notification {
|
||||||
|
gio_onShow(self.handle);
|
||||||
|
}
|
||||||
|
- (void)applicationDidHide:(NSNotification *)notification {
|
||||||
|
gio_onHide(self.handle);
|
||||||
|
}
|
||||||
|
- (void)dealloc {
|
||||||
|
gio_onDestroy(self.handle);
|
||||||
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
// Delegates are weakly referenced from their peers. Nothing
|
// Delegates are weakly referenced from their peers. Nothing
|
||||||
@@ -193,14 +418,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,15 +459,12 @@ void gio_showCursor() {
|
|||||||
|
|
||||||
// some cursors are not public, this tries to use a private cursor
|
// some cursors are not public, this tries to use a private cursor
|
||||||
// and uses fallback when the use of private cursor fails.
|
// and uses fallback when the use of private cursor fails.
|
||||||
void gio_trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
|
static void trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
|
||||||
if ([NSCursor respondsToSelector:cursorName]) {
|
NSCursor *cur = lookupPrivateNSCursor(cursorName);
|
||||||
id object = [NSCursor performSelector:cursorName];
|
if (cur == nil) {
|
||||||
if ([object isKindOfClass:[NSCursor class]]) {
|
cur = fallback;
|
||||||
[(NSCursor*)object set];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
[fallback set];
|
[cur set];
|
||||||
}
|
}
|
||||||
|
|
||||||
void gio_setCursor(NSUInteger curID) {
|
void gio_setCursor(NSUInteger curID) {
|
||||||
@@ -266,7 +488,7 @@ void gio_setCursor(NSUInteger curID) {
|
|||||||
break;
|
break;
|
||||||
case 6: // pointer.CursorAllScroll
|
case 6: // pointer.CursorAllScroll
|
||||||
// For some reason, using _moveCursor fails on Monterey.
|
// For some reason, using _moveCursor fails on Monterey.
|
||||||
// gio_trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
|
// trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
|
||||||
[NSCursor.arrowCursor set];
|
[NSCursor.arrowCursor set];
|
||||||
break;
|
break;
|
||||||
case 7: // pointer.CursorColResize
|
case 7: // pointer.CursorColResize
|
||||||
@@ -276,33 +498,31 @@ void gio_setCursor(NSUInteger curID) {
|
|||||||
[NSCursor.resizeUpDownCursor set];
|
[NSCursor.resizeUpDownCursor set];
|
||||||
break;
|
break;
|
||||||
case 9: // pointer.CursorGrab
|
case 9: // pointer.CursorGrab
|
||||||
// [NSCursor.openHandCursor set];
|
[NSCursor.openHandCursor set];
|
||||||
gio_trySetPrivateCursor(@selector(openHandCursor), NSCursor.arrowCursor);
|
|
||||||
break;
|
break;
|
||||||
case 10: // pointer.CursorGrabbing
|
case 10: // pointer.CursorGrabbing
|
||||||
// [NSCursor.closedHandCursor set];
|
[NSCursor.closedHandCursor set];
|
||||||
gio_trySetPrivateCursor(@selector(closedHandCursor), NSCursor.arrowCursor);
|
|
||||||
break;
|
break;
|
||||||
case 11: // pointer.CursorNotAllowed
|
case 11: // pointer.CursorNotAllowed
|
||||||
[NSCursor.operationNotAllowedCursor set];
|
[NSCursor.operationNotAllowedCursor set];
|
||||||
break;
|
break;
|
||||||
case 12: // pointer.CursorWait
|
case 12: // pointer.CursorWait
|
||||||
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
|
trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
|
||||||
break;
|
break;
|
||||||
case 13: // pointer.CursorProgress
|
case 13: // pointer.CursorProgress
|
||||||
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
|
trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
|
||||||
break;
|
break;
|
||||||
case 14: // pointer.CursorNorthWestResize
|
case 14: // pointer.CursorNorthWestResize
|
||||||
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
|
trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
|
||||||
break;
|
break;
|
||||||
case 15: // pointer.CursorNorthEastResize
|
case 15: // pointer.CursorNorthEastResize
|
||||||
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
|
trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
|
||||||
break;
|
break;
|
||||||
case 16: // pointer.CursorSouthWestResize
|
case 16: // pointer.CursorSouthWestResize
|
||||||
gio_trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
|
trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
|
||||||
break;
|
break;
|
||||||
case 17: // pointer.CursorSouthEastResize
|
case 17: // pointer.CursorSouthEastResize
|
||||||
gio_trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
|
trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
|
||||||
break;
|
break;
|
||||||
case 18: // pointer.CursorNorthSouthResize
|
case 18: // pointer.CursorNorthSouthResize
|
||||||
[NSCursor.resizeUpDownCursor set];
|
[NSCursor.resizeUpDownCursor set];
|
||||||
@@ -323,10 +543,10 @@ void gio_setCursor(NSUInteger curID) {
|
|||||||
[NSCursor.resizeDownCursor set];
|
[NSCursor.resizeDownCursor set];
|
||||||
break;
|
break;
|
||||||
case 24: // pointer.CursorNorthEastSouthWestResize
|
case 24: // pointer.CursorNorthEastSouthWestResize
|
||||||
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
|
trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
|
||||||
break;
|
break;
|
||||||
case 25: // pointer.CursorNorthWestSouthEastResize
|
case 25: // pointer.CursorNorthWestSouthEastResize
|
||||||
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
|
trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
[NSCursor.arrowCursor set];
|
[NSCursor.arrowCursor set];
|
||||||
@@ -343,7 +563,7 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF
|
|||||||
NSMiniaturizableWindowMask |
|
NSMiniaturizableWindowMask |
|
||||||
NSClosableWindowMask;
|
NSClosableWindowMask;
|
||||||
|
|
||||||
NSWindow* window = [[NSWindow alloc] initWithContentRect:rect
|
GioWindow* window = [[GioWindow alloc] initWithContentRect:rect
|
||||||
styleMask:styleMask
|
styleMask:styleMask
|
||||||
backing:NSBackingStoreBuffered
|
backing:NSBackingStoreBuffered
|
||||||
defer:NO];
|
defer:NO];
|
||||||
@@ -368,27 +588,48 @@ CFTypeRef gio_createView(void) {
|
|||||||
GioView* view = [[GioView alloc] initWithFrame:frame];
|
GioView* view = [[GioView alloc] initWithFrame:frame];
|
||||||
view.wantsLayer = YES;
|
view.wantsLayer = YES;
|
||||||
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
|
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
|
||||||
|
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:view
|
||||||
|
selector:@selector(applicationWillUnhide:)
|
||||||
|
name:NSApplicationWillUnhideNotification
|
||||||
|
object:nil];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:view
|
||||||
|
selector:@selector(applicationDidHide:)
|
||||||
|
name:NSApplicationDidHideNotification
|
||||||
|
object:nil];
|
||||||
return CFBridgingRetain(view);
|
return CFBridgingRetain(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
|
||||||
|
@autoreleasepool {
|
||||||
|
GioView *v = (__bridge GioView *)viewRef;
|
||||||
|
v.handle = handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@implementation GioAppDelegate
|
@implementation GioAppDelegate
|
||||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||||
[NSApp activateIgnoringOtherApps:YES];
|
[NSApp activateIgnoringOtherApps:YES];
|
||||||
gio_onFinishLaunching();
|
// Force the [NSApp run] call to return.
|
||||||
}
|
[NSApp stop:nil];
|
||||||
- (void)applicationDidHide:(NSNotification *)aNotification {
|
NSEvent *dummy = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
|
||||||
gio_onAppHide();
|
location:NSZeroPoint
|
||||||
}
|
modifierFlags:0
|
||||||
- (void)applicationWillUnhide:(NSNotification *)notification {
|
timestamp:0
|
||||||
gio_onAppShow();
|
windowNumber:0
|
||||||
|
context:nil
|
||||||
|
subtype:0
|
||||||
|
data1:0
|
||||||
|
data2:0];
|
||||||
|
[NSApp postEvent:dummy atStart:YES];
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
void gio_main() {
|
void gio_initApp() {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
[NSApplication sharedApplication];
|
[GioApplication sharedApplication];
|
||||||
GioAppDelegate *del = [[GioAppDelegate alloc] init];
|
GioAppDelegate *del = [[GioAppDelegate alloc] init];
|
||||||
[NSApp setDelegate:del];
|
[NSApp setDelegate:del];
|
||||||
|
|
||||||
@@ -410,6 +651,22 @@ void gio_main() {
|
|||||||
|
|
||||||
globalWindowDel = [[GioWindowDelegate alloc] init];
|
globalWindowDel = [[GioWindowDelegate alloc] init];
|
||||||
|
|
||||||
|
// Runs until stopped by applicationDidFinishLaunching.
|
||||||
[NSApp run];
|
[NSApp run];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void gio_wakeupMainThread(void) {
|
||||||
|
@autoreleasepool {
|
||||||
|
NSEvent *dummy = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
|
||||||
|
location:NSZeroPoint
|
||||||
|
modifierFlags:0
|
||||||
|
timestamp:0
|
||||||
|
windowNumber:0
|
||||||
|
context:nil
|
||||||
|
subtype:0
|
||||||
|
data1:0
|
||||||
|
data2:0];
|
||||||
|
[NSApp postEvent:dummy atStart:YES];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+29
-16
@@ -9,16 +9,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ViewEvent provides handles to the underlying window objects for the
|
|
||||||
// current display protocol.
|
|
||||||
type ViewEvent interface {
|
|
||||||
implementsViewEvent()
|
|
||||||
ImplementsEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
type X11ViewEvent struct {
|
type X11ViewEvent struct {
|
||||||
// Display is a pointer to the X11 Display created by XOpenDisplay.
|
// Display is a pointer to the X11 Display created by XOpenDisplay.
|
||||||
Display unsafe.Pointer
|
Display unsafe.Pointer
|
||||||
@@ -39,17 +33,13 @@ type WaylandViewEvent struct {
|
|||||||
func (WaylandViewEvent) implementsViewEvent() {}
|
func (WaylandViewEvent) implementsViewEvent() {}
|
||||||
func (WaylandViewEvent) ImplementsEvent() {}
|
func (WaylandViewEvent) ImplementsEvent() {}
|
||||||
|
|
||||||
func osMain() {
|
|
||||||
select {}
|
|
||||||
}
|
|
||||||
|
|
||||||
type windowDriver func(*callbacks, []Option) error
|
type windowDriver func(*callbacks, []Option) error
|
||||||
|
|
||||||
// Instead of creating files with build tags for each combination of wayland +/- x11
|
// Instead of creating files with build tags for each combination of wayland +/- x11
|
||||||
// let each driver initialize these variables with their own version of createWindow.
|
// let each driver initialize these variables with their own version of createWindow.
|
||||||
var wlDriver, x11Driver windowDriver
|
var wlDriver, x11Driver windowDriver
|
||||||
|
|
||||||
func newWindow(window *callbacks, options []Option) error {
|
func newWindow(window *callbacks, options []Option) {
|
||||||
var errFirst error
|
var errFirst error
|
||||||
for _, d := range []windowDriver{wlDriver, x11Driver} {
|
for _, d := range []windowDriver{wlDriver, x11Driver} {
|
||||||
if d == nil {
|
if d == nil {
|
||||||
@@ -57,16 +47,39 @@ func newWindow(window *callbacks, options []Option) error {
|
|||||||
}
|
}
|
||||||
err := d(window, options)
|
err := d(window, options)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
if errFirst == nil {
|
if errFirst == nil {
|
||||||
errFirst = err
|
errFirst = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errFirst != nil {
|
window.SetDriver(&dummyDriver{
|
||||||
return errFirst
|
win: window,
|
||||||
|
wakeups: make(chan event.Event, 1),
|
||||||
|
})
|
||||||
|
if errFirst == nil {
|
||||||
|
errFirst = errors.New("app: no window driver available")
|
||||||
|
}
|
||||||
|
window.ProcessEvent(DestroyEvent{Err: errFirst})
|
||||||
|
}
|
||||||
|
|
||||||
|
type dummyDriver struct {
|
||||||
|
win *callbacks
|
||||||
|
wakeups chan event.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyDriver) Event() event.Event {
|
||||||
|
if e, ok := d.win.nextEvent(); ok {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return <-d.wakeups
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyDriver) Invalidate() {
|
||||||
|
select {
|
||||||
|
case d.wakeups <- wakeupEvent{}:
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
return errors.New("app: no window driver available")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// xCursor contains mapping from pointer.Cursor to XCursor.
|
// xCursor contains mapping from pointer.Cursor to XCursor.
|
||||||
|
|||||||
+186
-123
@@ -15,6 +15,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -25,10 +26,12 @@ import (
|
|||||||
"gioui.org/app/internal/xkb"
|
"gioui.org/app/internal/xkb"
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/internal/fling"
|
"gioui.org/internal/fling"
|
||||||
"gioui.org/io/clipboard"
|
"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/system"
|
||||||
|
"gioui.org/io/transfer"
|
||||||
|
"gioui.org/op"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -97,7 +100,9 @@ type wlDisplay struct {
|
|||||||
read, write int
|
read, write int
|
||||||
}
|
}
|
||||||
|
|
||||||
repeat repeatState
|
repeat repeatState
|
||||||
|
poller poller
|
||||||
|
readClipClose chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type wlSeat struct {
|
type wlSeat struct {
|
||||||
@@ -137,7 +142,7 @@ type repeatState struct {
|
|||||||
delay time.Duration
|
delay time.Duration
|
||||||
|
|
||||||
key uint32
|
key uint32
|
||||||
win *callbacks
|
win *window
|
||||||
stopC chan struct{}
|
stopC chan struct{}
|
||||||
|
|
||||||
start time.Duration
|
start time.Duration
|
||||||
@@ -194,12 +199,10 @@ type window struct {
|
|||||||
dir f32.Point
|
dir f32.Point
|
||||||
}
|
}
|
||||||
|
|
||||||
stage system.Stage
|
configured bool
|
||||||
dead bool
|
|
||||||
lastFrameCallback *C.struct_wl_callback
|
lastFrameCallback *C.struct_wl_callback
|
||||||
|
|
||||||
animating bool
|
animating bool
|
||||||
redraw bool
|
|
||||||
// The most recent configure serial waiting to be ack'ed.
|
// The most recent configure serial waiting to be ack'ed.
|
||||||
serial C.uint32_t
|
serial C.uint32_t
|
||||||
scale int
|
scale int
|
||||||
@@ -209,9 +212,13 @@ type window struct {
|
|||||||
wsize image.Point // window config size before going fullscreen or maximized
|
wsize image.Point // window config size before going fullscreen or maximized
|
||||||
inCompositor bool // window is moving or being resized
|
inCompositor bool // window is moving or being resized
|
||||||
|
|
||||||
clipReads chan clipboard.Event
|
clipReads chan transfer.DataEvent
|
||||||
|
|
||||||
wakeups chan struct{}
|
wakeups chan struct{}
|
||||||
|
|
||||||
|
// invMu avoids the race between the destruction of disp and
|
||||||
|
// Invalidate waking it up.
|
||||||
|
invMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type poller struct {
|
type poller struct {
|
||||||
@@ -260,25 +267,17 @@ func newWLWindow(callbacks *callbacks, options []Option) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.w = callbacks
|
w.w = callbacks
|
||||||
go func() {
|
w.w.SetDriver(w)
|
||||||
defer d.destroy()
|
|
||||||
defer w.destroy()
|
|
||||||
|
|
||||||
w.w.SetDriver(w)
|
// Finish and commit setup from createNativeWindow.
|
||||||
|
w.Configure(options)
|
||||||
|
w.draw(true)
|
||||||
|
C.wl_surface_commit(w.surf)
|
||||||
|
|
||||||
// Finish and commit setup from createNativeWindow.
|
w.ProcessEvent(WaylandViewEvent{
|
||||||
w.Configure(options)
|
Display: unsafe.Pointer(w.display()),
|
||||||
C.wl_surface_commit(w.surf)
|
Surface: unsafe.Pointer(w.surf),
|
||||||
|
})
|
||||||
w.w.Event(WaylandViewEvent{
|
|
||||||
Display: unsafe.Pointer(w.display()),
|
|
||||||
Surface: unsafe.Pointer(w.surf),
|
|
||||||
})
|
|
||||||
|
|
||||||
err := w.loop()
|
|
||||||
w.w.Event(WaylandViewEvent{})
|
|
||||||
w.w.Event(system.DestroyEvent{Err: err})
|
|
||||||
}()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +353,7 @@ func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) {
|
|||||||
ppdp: ppdp,
|
ppdp: ppdp,
|
||||||
ppsp: ppdp,
|
ppsp: ppdp,
|
||||||
wakeups: make(chan struct{}, 1),
|
wakeups: make(chan struct{}, 1),
|
||||||
clipReads: make(chan clipboard.Event, 1),
|
clipReads: make(chan transfer.DataEvent, 1),
|
||||||
}
|
}
|
||||||
w.surf = C.wl_compositor_create_surface(d.compositor)
|
w.surf = C.wl_compositor_create_surface(d.compositor)
|
||||||
if w.surf == nil {
|
if w.surf == nil {
|
||||||
@@ -549,15 +548,15 @@ func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) {
|
|||||||
func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) {
|
func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) {
|
||||||
w := callbackLoad(data).(*window)
|
w := callbackLoad(data).(*window)
|
||||||
w.serial = serial
|
w.serial = serial
|
||||||
w.redraw = true
|
|
||||||
C.xdg_surface_ack_configure(wmSurf, serial)
|
C.xdg_surface_ack_configure(wmSurf, serial)
|
||||||
w.setStage(system.StageRunning)
|
w.configured = true
|
||||||
|
w.draw(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onToplevelClose
|
//export gio_onToplevelClose
|
||||||
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
|
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
|
||||||
w := callbackLoad(data).(*window)
|
w := callbackLoad(data).(*window)
|
||||||
w.dead = true
|
w.close(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onToplevelConfigure
|
//export gio_onToplevelConfigure
|
||||||
@@ -586,8 +585,8 @@ func gio_onToplevelDecorationConfigure(data unsafe.Pointer, deco *C.struct_zxdg_
|
|||||||
} else {
|
} else {
|
||||||
w.size.Y += int(w.config.decoHeight)
|
w.size.Y += int(w.config.decoHeight)
|
||||||
}
|
}
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
w.redraw = true
|
w.draw(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,7 +644,7 @@ func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output *
|
|||||||
if w.config.Mode == Minimized {
|
if w.config.Mode == Minimized {
|
||||||
// Minimized window got brought back up: it is no longer so.
|
// Minimized window got brought back up: it is no longer so.
|
||||||
w.config.Mode = Windowed
|
w.config.Mode = Windowed
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -790,8 +789,8 @@ func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.
|
|||||||
X: fromFixed(x) * float32(w.scale),
|
X: fromFixed(x) * float32(w.scale),
|
||||||
Y: fromFixed(y) * float32(w.scale),
|
Y: fromFixed(y) * float32(w.scale),
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.ProcessEvent(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),
|
||||||
@@ -806,8 +805,8 @@ func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.ui
|
|||||||
s.serial = serial
|
s.serial = serial
|
||||||
w := s.touchFoci[id]
|
w := s.touchFoci[id]
|
||||||
delete(s.touchFoci, id)
|
delete(s.touchFoci, id)
|
||||||
w.w.Event(pointer.Event{
|
w.ProcessEvent(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),
|
||||||
@@ -824,8 +823,8 @@ func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32
|
|||||||
X: fromFixed(x) * float32(w.scale),
|
X: fromFixed(x) * float32(w.scale),
|
||||||
Y: fromFixed(y) * float32(w.scale),
|
Y: fromFixed(y) * float32(w.scale),
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.ProcessEvent(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),
|
||||||
@@ -843,8 +842,8 @@ func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) {
|
|||||||
s := callbackLoad(data).(*wlSeat)
|
s := callbackLoad(data).(*wlSeat)
|
||||||
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.ProcessEvent(pointer.Event{
|
||||||
Type: pointer.Cancel,
|
Kind: pointer.Cancel,
|
||||||
Source: pointer.Touch,
|
Source: pointer.Touch,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -869,7 +868,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.ProcessEvent(pointer.Event{Kind: pointer.Cancel})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,21 +916,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.ProcessEvent(pointer.Event{
|
||||||
Type: typ,
|
Kind: kind,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
Position: w.lastPos,
|
Position: w.lastPos,
|
||||||
@@ -1018,23 +1017,34 @@ func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, axis
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) ReadClipboard() {
|
func (w *window) ReadClipboard() {
|
||||||
|
if w.disp.readClipClose != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.disp.readClipClose = make(chan struct{})
|
||||||
r, err := w.disp.readClipboard()
|
r, err := w.disp.readClipboard()
|
||||||
// Send empty responses on unavailable clipboards or errors.
|
|
||||||
if r == nil || err != nil {
|
if r == nil || err != nil {
|
||||||
w.w.Event(clipboard.Event{})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Don't let slow clipboard transfers block event loop.
|
// Don't let slow clipboard transfers block event loop.
|
||||||
go func() {
|
go func() {
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
data, _ := io.ReadAll(r)
|
data, _ := io.ReadAll(r)
|
||||||
w.clipReads <- clipboard.Event{Text: string(data)}
|
e := transfer.DataEvent{
|
||||||
w.Wakeup()
|
Type: "application/text",
|
||||||
|
Open: func() io.ReadCloser {
|
||||||
|
return io.NopCloser(bytes.NewReader(data))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case w.clipReads <- e:
|
||||||
|
w.disp.wakeup()
|
||||||
|
case <-w.disp.readClipClose:
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) WriteClipboard(s string) {
|
func (w *window) WriteClipboard(mime string, s []byte) {
|
||||||
w.disp.writeClipboard([]byte(s))
|
w.disp.writeClipboard(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) Configure(options []Option) {
|
func (w *window) Configure(options []Option) {
|
||||||
@@ -1092,8 +1102,7 @@ func (w *window) Configure(options []Option) {
|
|||||||
w.config.MaxSize = cnf.MaxSize
|
w.config.MaxSize = cnf.MaxSize
|
||||||
w.setWindowConstraints()
|
w.setWindowConstraints()
|
||||||
}
|
}
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
w.redraw = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) setWindowConstraints() {
|
func (w *window) setWindowConstraints() {
|
||||||
@@ -1130,7 +1139,7 @@ func (w *window) Perform(actions system.Action) {
|
|||||||
walkActions(actions, func(action system.Action) {
|
walkActions(actions, func(action system.Action) {
|
||||||
switch action {
|
switch action {
|
||||||
case system.ActionClose:
|
case system.ActionClose:
|
||||||
w.dead = true
|
w.close(nil)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1213,7 +1222,8 @@ func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
|
|||||||
w := callbackLoad(unsafe.Pointer(surf)).(*window)
|
w := callbackLoad(unsafe.Pointer(surf)).(*window)
|
||||||
s.keyboardFocus = w
|
s.keyboardFocus = w
|
||||||
s.disp.repeat.Stop(0)
|
s.disp.repeat.Stop(0)
|
||||||
w.w.Event(key.FocusEvent{Focus: true})
|
w.config.Focused = true
|
||||||
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onKeyboardLeave
|
//export gio_onKeyboardLeave
|
||||||
@@ -1222,7 +1232,8 @@ func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
|
|||||||
s.serial = serial
|
s.serial = serial
|
||||||
s.disp.repeat.Stop(0)
|
s.disp.repeat.Stop(0)
|
||||||
w := s.keyboardFocus
|
w := s.keyboardFocus
|
||||||
w.w.Event(key.FocusEvent{Focus: false})
|
w.config.Focused = false
|
||||||
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onKeyboardKey
|
//export gio_onKeyboardKey
|
||||||
@@ -1240,7 +1251,7 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri
|
|||||||
// There's no support for IME yet.
|
// There's no support for IME yet.
|
||||||
w.w.EditorInsert(ee.Text)
|
w.w.EditorInsert(ee.Text)
|
||||||
} else {
|
} else {
|
||||||
w.w.Event(e)
|
w.ProcessEvent(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if state != C.WL_KEYBOARD_KEY_STATE_PRESSED {
|
if state != C.WL_KEYBOARD_KEY_STATE_PRESSED {
|
||||||
@@ -1275,7 +1286,7 @@ func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) {
|
|||||||
r.now = 0
|
r.now = 0
|
||||||
r.stopC = stopC
|
r.stopC = stopC
|
||||||
r.key = keyCode
|
r.key = keyCode
|
||||||
r.win = w.w
|
r.win = w
|
||||||
rate, delay := r.rate, r.delay
|
rate, delay := r.rate, r.delay
|
||||||
go func() {
|
go func() {
|
||||||
timer := time.NewTimer(delay)
|
timer := time.NewTimer(delay)
|
||||||
@@ -1333,9 +1344,9 @@ func (r *repeatState) Repeat(d *wlDisplay) {
|
|||||||
for _, e := range d.xkb.DispatchKey(r.key, key.Press) {
|
for _, e := range d.xkb.DispatchKey(r.key, key.Press) {
|
||||||
if ee, ok := e.(key.EditEvent); ok {
|
if ee, ok := e.(key.EditEvent); ok {
|
||||||
// There's no support for IME yet.
|
// There's no support for IME yet.
|
||||||
r.win.EditorInsert(ee.Text)
|
r.win.w.EditorInsert(ee.Text)
|
||||||
} else {
|
} else {
|
||||||
r.win.Event(e)
|
r.win.ProcessEvent(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.last += delay
|
r.last += delay
|
||||||
@@ -1348,28 +1359,76 @@ func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.ui
|
|||||||
w := callbackLoad(data).(*window)
|
w := callbackLoad(data).(*window)
|
||||||
if w.lastFrameCallback == callback {
|
if w.lastFrameCallback == callback {
|
||||||
w.lastFrameCallback = nil
|
w.lastFrameCallback = nil
|
||||||
|
w.draw(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) loop() error {
|
func (w *window) close(err error) {
|
||||||
var p poller
|
w.ProcessEvent(WaylandViewEvent{})
|
||||||
for {
|
w.ProcessEvent(DestroyEvent{Err: err})
|
||||||
if err := w.disp.dispatch(&p); err != nil {
|
}
|
||||||
return err
|
|
||||||
}
|
func (w *window) dispatch() {
|
||||||
select {
|
if w.disp == nil {
|
||||||
case e := <-w.clipReads:
|
<-w.wakeups
|
||||||
w.w.Event(e)
|
w.w.Invalidate()
|
||||||
case <-w.wakeups:
|
return
|
||||||
w.w.Event(wakeupEvent{})
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
if w.dead {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
w.draw()
|
|
||||||
}
|
}
|
||||||
return nil
|
if err := w.disp.dispatch(); err != nil {
|
||||||
|
w.close(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case e := <-w.clipReads:
|
||||||
|
w.disp.readClipClose = nil
|
||||||
|
w.ProcessEvent(e)
|
||||||
|
case <-w.wakeups:
|
||||||
|
w.w.Invalidate()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) ProcessEvent(e event.Event) {
|
||||||
|
w.w.ProcessEvent(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Event() event.Event {
|
||||||
|
for {
|
||||||
|
evt, ok := w.w.nextEvent()
|
||||||
|
if !ok {
|
||||||
|
w.dispatch()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, destroy := evt.(DestroyEvent); destroy {
|
||||||
|
w.destroy()
|
||||||
|
w.invMu.Lock()
|
||||||
|
w.disp.destroy()
|
||||||
|
w.disp = nil
|
||||||
|
w.invMu.Unlock()
|
||||||
|
}
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Invalidate() {
|
||||||
|
select {
|
||||||
|
case w.wakeups <- struct{}{}:
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.invMu.Lock()
|
||||||
|
defer w.invMu.Unlock()
|
||||||
|
if w.disp != nil {
|
||||||
|
w.disp.wakeup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Run(f func()) {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Frame(frame *op.Ops) {
|
||||||
|
w.w.ProcessFrame(frame, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// bindDataDevice initializes the dataDev field if and only if both
|
// bindDataDevice initializes the dataDev field if and only if both
|
||||||
@@ -1385,13 +1444,21 @@ func (d *wlDisplay) bindDataDevice() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *wlDisplay) dispatch(p *poller) error {
|
func (d *wlDisplay) dispatch() error {
|
||||||
|
// wl_display_prepare_read records the current thread for
|
||||||
|
// use in wl_display_read_events or wl_display_cancel_events.
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
dispfd := C.wl_display_get_fd(d.disp)
|
dispfd := C.wl_display_get_fd(d.disp)
|
||||||
// Poll for events and notifications.
|
// Poll for events and notifications.
|
||||||
pollfds := append(p.pollfds[:0],
|
pollfds := append(d.poller.pollfds[:0],
|
||||||
syscall.PollFd{Fd: int32(dispfd), Events: syscall.POLLIN | syscall.POLLERR},
|
syscall.PollFd{Fd: int32(dispfd), Events: syscall.POLLIN | syscall.POLLERR},
|
||||||
syscall.PollFd{Fd: int32(d.notify.read), Events: syscall.POLLIN | syscall.POLLERR},
|
syscall.PollFd{Fd: int32(d.notify.read), Events: syscall.POLLIN | syscall.POLLERR},
|
||||||
)
|
)
|
||||||
|
for C.wl_display_prepare_read(d.disp) != 0 {
|
||||||
|
C.wl_display_dispatch_pending(d.disp)
|
||||||
|
}
|
||||||
dispFd := &pollfds[0]
|
dispFd := &pollfds[0]
|
||||||
if ret, err := C.wl_display_flush(d.disp); ret < 0 {
|
if ret, err := C.wl_display_flush(d.disp); ret < 0 {
|
||||||
if err != syscall.EAGAIN {
|
if err != syscall.EAGAIN {
|
||||||
@@ -1402,11 +1469,25 @@ func (d *wlDisplay) dispatch(p *poller) error {
|
|||||||
dispFd.Events |= syscall.POLLOUT
|
dispFd.Events |= syscall.POLLOUT
|
||||||
}
|
}
|
||||||
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
|
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
|
||||||
|
C.wl_display_cancel_read(d.disp)
|
||||||
return fmt.Errorf("wayland: poll failed: %v", err)
|
return fmt.Errorf("wayland: poll failed: %v", err)
|
||||||
}
|
}
|
||||||
|
if dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0 {
|
||||||
|
C.wl_display_cancel_read(d.disp)
|
||||||
|
return errors.New("wayland: display file descriptor gone")
|
||||||
|
}
|
||||||
|
// Handle events.
|
||||||
|
if dispFd.Revents&syscall.POLLIN != 0 {
|
||||||
|
if ret, err := C.wl_display_read_events(d.disp); ret < 0 {
|
||||||
|
return fmt.Errorf("wayland: wl_display_read_events failed: %v", err)
|
||||||
|
}
|
||||||
|
C.wl_display_dispatch_pending(d.disp)
|
||||||
|
} else {
|
||||||
|
C.wl_display_cancel_read(d.disp)
|
||||||
|
}
|
||||||
// Clear notifications.
|
// Clear notifications.
|
||||||
for {
|
for {
|
||||||
_, err := syscall.Read(d.notify.read, p.buf[:])
|
_, err := syscall.Read(d.notify.read, d.poller.buf[:])
|
||||||
if err == syscall.EAGAIN {
|
if err == syscall.EAGAIN {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -1414,29 +1495,15 @@ func (d *wlDisplay) dispatch(p *poller) error {
|
|||||||
return fmt.Errorf("wayland: read from notify pipe failed: %v", err)
|
return fmt.Errorf("wayland: read from notify pipe failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle events
|
|
||||||
switch {
|
|
||||||
case dispFd.Revents&syscall.POLLIN != 0:
|
|
||||||
if ret, err := C.wl_display_dispatch(d.disp); ret < 0 {
|
|
||||||
return fmt.Errorf("wayland: wl_display_dispatch failed: %v", err)
|
|
||||||
}
|
|
||||||
case dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0:
|
|
||||||
return errors.New("wayland: display file descriptor gone")
|
|
||||||
}
|
|
||||||
d.repeat.Repeat(d)
|
d.repeat.Repeat(d)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) Wakeup() {
|
|
||||||
select {
|
|
||||||
case w.wakeups <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
w.disp.wakeup()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *window) SetAnimating(anim bool) {
|
func (w *window) SetAnimating(anim bool) {
|
||||||
w.animating = anim
|
w.animating = anim
|
||||||
|
if anim {
|
||||||
|
w.draw(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wakeup wakes up the event loop through the notification pipe.
|
// Wakeup wakes up the event loop through the notification pipe.
|
||||||
@@ -1572,8 +1639,8 @@ func (w *window) flushScroll() {
|
|||||||
if total == (f32.Point{}) {
|
if total == (f32.Point{}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.ProcessEvent(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,
|
||||||
@@ -1595,8 +1662,8 @@ func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
|
|||||||
X: fromFixed(x) * float32(w.scale),
|
X: fromFixed(x) * float32(w.scale),
|
||||||
Y: fromFixed(y) * float32(w.scale),
|
Y: fromFixed(y) * float32(w.scale),
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.ProcessEvent(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,
|
||||||
@@ -1616,7 +1683,8 @@ func (w *window) systemGesture() (*C.struct_wl_cursor, C.uint32_t) {
|
|||||||
if w.config.Mode != Windowed || w.config.Decorated {
|
if w.config.Mode != Windowed || w.config.Decorated {
|
||||||
return nil, 0
|
return nil, 0
|
||||||
}
|
}
|
||||||
border := w.w.w.metric.Dp(3)
|
_, cfg := w.getConfig()
|
||||||
|
border := cfg.Dp(3)
|
||||||
x, y, size := int(w.lastPos.X), int(w.lastPos.Y), w.config.Size
|
x, y, size := int(w.lastPos.X), int(w.lastPos.Y), w.config.Size
|
||||||
north := y <= border
|
north := y <= border
|
||||||
south := y >= size.Y-border
|
south := y >= size.Y-border
|
||||||
@@ -1670,13 +1738,10 @@ func (w *window) updateOutputs() {
|
|||||||
if found && scale != w.scale {
|
if found && scale != w.scale {
|
||||||
w.scale = scale
|
w.scale = scale
|
||||||
C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale))
|
C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale))
|
||||||
w.redraw = true
|
w.draw(true)
|
||||||
}
|
}
|
||||||
if !found {
|
if found {
|
||||||
w.setStage(system.StagePaused)
|
w.draw(true)
|
||||||
} else {
|
|
||||||
w.setStage(system.StageRunning)
|
|
||||||
w.redraw = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1688,7 +1753,10 @@ func (w *window) getConfig() (image.Point, unit.Metric) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) draw() {
|
func (w *window) draw(sync bool) {
|
||||||
|
if !w.configured {
|
||||||
|
return
|
||||||
|
}
|
||||||
w.flushScroll()
|
w.flushScroll()
|
||||||
size, cfg := w.getConfig()
|
size, cfg := w.getConfig()
|
||||||
if cfg == (unit.Metric{}) {
|
if cfg == (unit.Metric{}) {
|
||||||
@@ -1696,11 +1764,9 @@ func (w *window) draw() {
|
|||||||
}
|
}
|
||||||
if size != w.config.Size {
|
if size != w.config.Size {
|
||||||
w.config.Size = size
|
w.config.Size = size
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
anim := w.animating || w.fling.anim.Active()
|
anim := w.animating || w.fling.anim.Active()
|
||||||
sync := w.redraw
|
|
||||||
w.redraw = false
|
|
||||||
// Draw animation only when not waiting for frame callback.
|
// Draw animation only when not waiting for frame callback.
|
||||||
redrawAnim := anim && w.lastFrameCallback == nil
|
redrawAnim := anim && w.lastFrameCallback == nil
|
||||||
if !redrawAnim && !sync {
|
if !redrawAnim && !sync {
|
||||||
@@ -1711,8 +1777,8 @@ func (w *window) draw() {
|
|||||||
// Use the surface as listener data for gio_onFrameDone.
|
// Use the surface as listener data for gio_onFrameDone.
|
||||||
C.wl_callback_add_listener(w.lastFrameCallback, &C.gio_callback_listener, unsafe.Pointer(w.surf))
|
C.wl_callback_add_listener(w.lastFrameCallback, &C.gio_callback_listener, unsafe.Pointer(w.surf))
|
||||||
}
|
}
|
||||||
w.w.Event(frameEvent{
|
w.ProcessEvent(frameEvent{
|
||||||
FrameEvent: system.FrameEvent{
|
FrameEvent: FrameEvent{
|
||||||
Now: time.Now(),
|
Now: time.Now(),
|
||||||
Size: w.config.Size,
|
Size: w.config.Size,
|
||||||
Metric: cfg,
|
Metric: cfg,
|
||||||
@@ -1721,14 +1787,6 @@ func (w *window) draw() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) setStage(s system.Stage) {
|
|
||||||
if s == w.stage {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.stage = s
|
|
||||||
w.w.Event(system.StageEvent{Stage: s})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *window) display() *C.struct_wl_display {
|
func (w *window) display() *C.struct_wl_display {
|
||||||
return w.disp.disp
|
return w.disp.disp
|
||||||
}
|
}
|
||||||
@@ -1820,6 +1878,10 @@ func newWLDisplay() (*wlDisplay, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *wlDisplay) destroy() {
|
func (d *wlDisplay) destroy() {
|
||||||
|
if d.readClipClose != nil {
|
||||||
|
close(d.readClipClose)
|
||||||
|
d.readClipClose = nil
|
||||||
|
}
|
||||||
if d.notify.write != 0 {
|
if d.notify.write != 0 {
|
||||||
syscall.Close(d.notify.write)
|
syscall.Close(d.notify.write)
|
||||||
d.notify.write = 0
|
d.notify.write = 0
|
||||||
@@ -1861,6 +1923,7 @@ func (d *wlDisplay) destroy() {
|
|||||||
if d.disp != nil {
|
if d.disp != nil {
|
||||||
C.wl_display_disconnect(d.disp)
|
C.wl_display_disconnect(d.disp)
|
||||||
callbackDelete(unsafe.Pointer(d.disp))
|
callbackDelete(unsafe.Pointer(d.disp))
|
||||||
|
d.disp = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+166
-132
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"io"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,30 +19,26 @@ import (
|
|||||||
syscall "golang.org/x/sys/windows"
|
syscall "golang.org/x/sys/windows"
|
||||||
|
|
||||||
"gioui.org/app/internal/windows"
|
"gioui.org/app/internal/windows"
|
||||||
|
"gioui.org/op"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
gowindows "golang.org/x/sys/windows"
|
gowindows "golang.org/x/sys/windows"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/io/clipboard"
|
"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/system"
|
||||||
|
"gioui.org/io/transfer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ViewEvent struct {
|
type Win32ViewEvent 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
|
||||||
w *callbacks
|
w *callbacks
|
||||||
stage system.Stage
|
|
||||||
pointerBtns pointer.Buttons
|
pointerBtns pointer.Buttons
|
||||||
|
|
||||||
// cursorIn tracks whether the cursor was inside the window according
|
// cursorIn tracks whether the cursor was inside the window according
|
||||||
@@ -53,11 +50,13 @@ type window struct {
|
|||||||
placement *windows.WindowPlacement
|
placement *windows.WindowPlacement
|
||||||
|
|
||||||
animating bool
|
animating bool
|
||||||
focused bool
|
|
||||||
|
|
||||||
deltas winDeltas
|
|
||||||
borderSize image.Point
|
borderSize image.Point
|
||||||
config Config
|
config Config
|
||||||
|
loop *eventLoop
|
||||||
|
|
||||||
|
// invMu avoids the race between destroying the window and Invalidate.
|
||||||
|
invMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
const _WM_WAKEUP = windows.WM_USER + iota
|
const _WM_WAKEUP = windows.WM_USER + iota
|
||||||
@@ -86,40 +85,38 @@ var resources struct {
|
|||||||
cursor syscall.Handle
|
cursor syscall.Handle
|
||||||
}
|
}
|
||||||
|
|
||||||
func osMain() {
|
func newWindow(win *callbacks, options []Option) {
|
||||||
select {}
|
done := make(chan struct{})
|
||||||
}
|
|
||||||
|
|
||||||
func newWindow(window *callbacks, options []Option) error {
|
|
||||||
cerr := make(chan error)
|
|
||||||
go func() {
|
go func() {
|
||||||
// GetMessage and PeekMessage can filter on a window HWND, but
|
// GetMessage and PeekMessage can filter on a window HWND, but
|
||||||
// then thread-specific messages such as WM_QUIT are ignored.
|
// then thread-specific messages such as WM_QUIT are ignored.
|
||||||
// Instead lock the thread so window messages arrive through
|
// Instead lock the thread so window messages arrive through
|
||||||
// unfiltered GetMessage calls.
|
// unfiltered GetMessage calls.
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
w, err := createNativeWindow()
|
|
||||||
|
w := &window{
|
||||||
|
w: win,
|
||||||
|
}
|
||||||
|
w.loop = newEventLoop(w.w, w.wakeup)
|
||||||
|
w.w.SetDriver(w)
|
||||||
|
err := w.init()
|
||||||
|
done <- struct{}{}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cerr <- err
|
w.ProcessEvent(DestroyEvent{Err: err})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cerr <- nil
|
|
||||||
winMap.Store(w.hwnd, w)
|
winMap.Store(w.hwnd, w)
|
||||||
defer winMap.Delete(w.hwnd)
|
defer winMap.Delete(w.hwnd)
|
||||||
w.w = window
|
w.ProcessEvent(Win32ViewEvent{HWND: uintptr(w.hwnd)})
|
||||||
w.w.SetDriver(w)
|
|
||||||
w.w.Event(ViewEvent{HWND: uintptr(w.hwnd)})
|
|
||||||
w.Configure(options)
|
w.Configure(options)
|
||||||
windows.SetForegroundWindow(w.hwnd)
|
windows.SetForegroundWindow(w.hwnd)
|
||||||
windows.SetFocus(w.hwnd)
|
windows.SetFocus(w.hwnd)
|
||||||
// Since the window class for the cursor is null,
|
// Since the window class for the cursor is null,
|
||||||
// set it here to show the cursor.
|
// set it here to show the cursor.
|
||||||
w.SetCursor(pointer.CursorDefault)
|
w.SetCursor(pointer.CursorDefault)
|
||||||
if err := w.loop(); err != nil {
|
w.runLoop()
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
return <-cerr
|
<-done
|
||||||
}
|
}
|
||||||
|
|
||||||
// initResources initializes the resources global.
|
// initResources initializes the resources global.
|
||||||
@@ -154,13 +151,13 @@ func initResources() error {
|
|||||||
|
|
||||||
const dwExStyle = windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE
|
const dwExStyle = windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE
|
||||||
|
|
||||||
func createNativeWindow() (*window, error) {
|
func (w *window) init() error {
|
||||||
var resErr error
|
var resErr error
|
||||||
resources.once.Do(func() {
|
resources.once.Do(func() {
|
||||||
resErr = initResources()
|
resErr = initResources()
|
||||||
})
|
})
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return nil, resErr
|
return resErr
|
||||||
}
|
}
|
||||||
const dwStyle = windows.WS_OVERLAPPEDWINDOW
|
const dwStyle = windows.WS_OVERLAPPEDWINDOW
|
||||||
|
|
||||||
@@ -176,43 +173,32 @@ func createNativeWindow() (*window, error) {
|
|||||||
resources.handle,
|
resources.handle,
|
||||||
0)
|
0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
|
||||||
w := &window{
|
|
||||||
hwnd: hwnd,
|
|
||||||
}
|
}
|
||||||
w.hdc, err = windows.GetDC(hwnd)
|
w.hdc, err = windows.GetDC(hwnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
windows.DestroyWindow(hwnd)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return w, nil
|
w.hwnd = hwnd
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// update() handles changes done by the user, and updates the configuration.
|
// update() handles changes done by the user, and updates the configuration.
|
||||||
// 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),
|
||||||
)
|
)
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr {
|
func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr {
|
||||||
@@ -253,7 +239,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
e.State = key.Release
|
e.State = key.Release
|
||||||
}
|
}
|
||||||
|
|
||||||
w.w.Event(e)
|
w.ProcessEvent(e)
|
||||||
|
|
||||||
if (wParam == windows.VK_F10) && (msg == windows.WM_SYSKEYDOWN || msg == windows.WM_SYSKEYUP) {
|
if (wParam == windows.VK_F10) && (msg == windows.WM_SYSKEYDOWN || msg == windows.WM_SYSKEYUP) {
|
||||||
// Reserve F10 for ourselves, and don't let it open the system menu. Other Windows programs
|
// Reserve F10 for ourselves, and don't let it open the system menu. Other Windows programs
|
||||||
@@ -274,23 +260,15 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
case windows.WM_MBUTTONUP:
|
case windows.WM_MBUTTONUP:
|
||||||
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.ProcessEvent(pointer.Event{
|
||||||
Type: pointer.Cancel,
|
Kind: pointer.Cancel,
|
||||||
})
|
})
|
||||||
case windows.WM_SETFOCUS:
|
case windows.WM_SETFOCUS:
|
||||||
w.focused = true
|
w.config.Focused = true
|
||||||
w.w.Event(key.FocusEvent{Focus: true})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
case windows.WM_KILLFOCUS:
|
case windows.WM_KILLFOCUS:
|
||||||
w.focused = false
|
w.config.Focused = false
|
||||||
w.w.Event(key.FocusEvent{Focus: false})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
case windows.WM_NCACTIVATE:
|
|
||||||
if w.stage >= system.StageInactive {
|
|
||||||
if wParam == windows.TRUE {
|
|
||||||
w.setStage(system.StageRunning)
|
|
||||||
} else {
|
|
||||||
w.setStage(system.StageInactive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case windows.WM_NCHITTEST:
|
case windows.WM_NCHITTEST:
|
||||||
if w.config.Decorated {
|
if w.config.Decorated {
|
||||||
// Let the system handle it.
|
// Let the system handle it.
|
||||||
@@ -303,8 +281,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
case windows.WM_MOUSEMOVE:
|
case windows.WM_MOUSEMOVE:
|
||||||
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.ProcessEvent(pointer.Event{
|
||||||
Type: pointer.Move,
|
Kind: pointer.Move,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Position: p,
|
Position: p,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
@@ -316,15 +294,39 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
case windows.WM_MOUSEHWHEEL:
|
case windows.WM_MOUSEHWHEEL:
|
||||||
w.scrollEvent(wParam, lParam, true, getModifiers())
|
w.scrollEvent(wParam, lParam, true, getModifiers())
|
||||||
case windows.WM_DESTROY:
|
case windows.WM_DESTROY:
|
||||||
w.w.Event(ViewEvent{})
|
w.ProcessEvent(Win32ViewEvent{})
|
||||||
w.w.Event(system.DestroyEvent{})
|
w.ProcessEvent(DestroyEvent{})
|
||||||
if w.hdc != 0 {
|
if w.hdc != 0 {
|
||||||
windows.ReleaseDC(w.hdc)
|
windows.ReleaseDC(w.hdc)
|
||||||
w.hdc = 0
|
w.hdc = 0
|
||||||
}
|
}
|
||||||
|
w.invMu.Lock()
|
||||||
// The system destroys the HWND for us.
|
// The system destroys the HWND for us.
|
||||||
w.hwnd = 0
|
w.hwnd = 0
|
||||||
|
w.invMu.Unlock()
|
||||||
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(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:
|
||||||
@@ -332,30 +334,35 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
switch wParam {
|
switch wParam {
|
||||||
case windows.SIZE_MINIMIZED:
|
case windows.SIZE_MINIMIZED:
|
||||||
w.config.Mode = Minimized
|
w.config.Mode = Minimized
|
||||||
w.setStage(system.StagePaused)
|
|
||||||
case windows.SIZE_MAXIMIZED:
|
case windows.SIZE_MAXIMIZED:
|
||||||
w.config.Mode = Maximized
|
w.config.Mode = Maximized
|
||||||
w.setStage(system.StageRunning)
|
|
||||||
case windows.SIZE_RESTORED:
|
case windows.SIZE_RESTORED:
|
||||||
if w.config.Mode != Fullscreen {
|
if w.config.Mode != Fullscreen {
|
||||||
w.config.Mode = Windowed
|
w.config.Mode = Windowed
|
||||||
}
|
}
|
||||||
w.setStage(system.StageRunning)
|
|
||||||
}
|
}
|
||||||
case windows.WM_GETMINMAXINFO:
|
case windows.WM_GETMINMAXINFO:
|
||||||
mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam)))
|
mm := (*windows.MinMaxInfo)(unsafe.Pointer(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 {
|
||||||
@@ -363,7 +370,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
return windows.TRUE
|
return windows.TRUE
|
||||||
}
|
}
|
||||||
case _WM_WAKEUP:
|
case _WM_WAKEUP:
|
||||||
w.w.Event(wakeupEvent{})
|
w.loop.Wakeup()
|
||||||
|
w.loop.FlushEvents()
|
||||||
case windows.WM_IME_STARTCOMPOSITION:
|
case windows.WM_IME_STARTCOMPOSITION:
|
||||||
imc := windows.ImmGetContext(w.hwnd)
|
imc := windows.ImmGetContext(w.hwnd)
|
||||||
if imc == 0 {
|
if imc == 0 {
|
||||||
@@ -446,23 +454,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,22 +483,27 @@ 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) {
|
||||||
if !w.focused {
|
if !w.config.Focused {
|
||||||
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()
|
||||||
@@ -503,8 +511,8 @@ 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.ProcessEvent(pointer.Event{
|
||||||
Type: typ,
|
Kind: kind,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Position: p,
|
Position: p,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
@@ -538,8 +546,8 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
|
|||||||
sp.Y = -dist
|
sp.Y = -dist
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.w.Event(pointer.Event{
|
w.ProcessEvent(pointer.Event{
|
||||||
Type: pointer.Scroll,
|
Kind: pointer.Scroll,
|
||||||
Source: pointer.Mouse,
|
Source: pointer.Mouse,
|
||||||
Position: p,
|
Position: p,
|
||||||
Buttons: w.pointerBtns,
|
Buttons: w.pointerBtns,
|
||||||
@@ -550,7 +558,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/
|
// Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/
|
||||||
func (w *window) loop() error {
|
func (w *window) runLoop() {
|
||||||
msg := new(windows.Msg)
|
msg := new(windows.Msg)
|
||||||
loop:
|
loop:
|
||||||
for {
|
for {
|
||||||
@@ -561,7 +569,7 @@ loop:
|
|||||||
}
|
}
|
||||||
switch ret := windows.GetMessage(msg, 0, 0, 0); ret {
|
switch ret := windows.GetMessage(msg, 0, 0, 0); ret {
|
||||||
case -1:
|
case -1:
|
||||||
return errors.New("GetMessage failed")
|
panic(errors.New("GetMessage failed"))
|
||||||
case 0:
|
case 0:
|
||||||
// WM_QUIT received.
|
// WM_QUIT received.
|
||||||
break loop
|
break loop
|
||||||
@@ -569,7 +577,6 @@ loop:
|
|||||||
windows.TranslateMessage(msg)
|
windows.TranslateMessage(msg)
|
||||||
windows.DispatchMessage(msg)
|
windows.DispatchMessage(msg)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) EditorStateChanged(old, new editorState) {
|
func (w *window) EditorStateChanged(old, new editorState) {
|
||||||
@@ -587,16 +594,37 @@ func (w *window) SetAnimating(anim bool) {
|
|||||||
w.animating = anim
|
w.animating = anim
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) Wakeup() {
|
func (w *window) ProcessEvent(e event.Event) {
|
||||||
if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil {
|
w.w.ProcessEvent(e)
|
||||||
panic(err)
|
w.loop.FlushEvents()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) setStage(s system.Stage) {
|
func (w *window) Event() event.Event {
|
||||||
if s != w.stage {
|
return w.loop.Event()
|
||||||
w.stage = s
|
}
|
||||||
w.w.Event(system.StageEvent{Stage: s})
|
|
||||||
|
func (w *window) Invalidate() {
|
||||||
|
w.loop.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Run(f func()) {
|
||||||
|
w.loop.Run(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) Frame(frame *op.Ops) {
|
||||||
|
w.loop.Frame(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *window) wakeup() {
|
||||||
|
w.invMu.Lock()
|
||||||
|
defer w.invMu.Unlock()
|
||||||
|
if w.hwnd == 0 {
|
||||||
|
w.loop.Wakeup()
|
||||||
|
w.loop.FlushEvents()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil {
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,8 +634,8 @@ func (w *window) draw(sync bool) {
|
|||||||
}
|
}
|
||||||
dpi := windows.GetWindowDPI(w.hwnd)
|
dpi := windows.GetWindowDPI(w.hwnd)
|
||||||
cfg := configForDPI(dpi)
|
cfg := configForDPI(dpi)
|
||||||
w.w.Event(frameEvent{
|
w.ProcessEvent(frameEvent{
|
||||||
FrameEvent: system.FrameEvent{
|
FrameEvent: FrameEvent{
|
||||||
Now: time.Now(),
|
Now: time.Now(),
|
||||||
Size: w.config.Size,
|
Size: w.config.Size,
|
||||||
Metric: cfg,
|
Metric: cfg,
|
||||||
@@ -653,7 +681,12 @@ func (w *window) readClipboard() error {
|
|||||||
}
|
}
|
||||||
defer windows.GlobalUnlock(mem)
|
defer windows.GlobalUnlock(mem)
|
||||||
content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr)))
|
content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr)))
|
||||||
w.w.Event(clipboard.Event{Text: content})
|
w.ProcessEvent(transfer.DataEvent{
|
||||||
|
Type: "application/text",
|
||||||
|
Open: func() io.ReadCloser {
|
||||||
|
return io.NopCloser(strings.NewReader(content))
|
||||||
|
},
|
||||||
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,9 +702,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,45 +714,48 @@ 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(mime string, s []byte) {
|
||||||
w.writeClipboard(s)
|
w.writeClipboard(string(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) writeClipboard(s string) error {
|
func (w *window) writeClipboard(s string) error {
|
||||||
@@ -850,11 +883,11 @@ func (w *window) raise() {
|
|||||||
windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW)
|
windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertKeyCode(code uintptr) (string, bool) {
|
func convertKeyCode(code uintptr) (key.Name, bool) {
|
||||||
if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' {
|
if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' {
|
||||||
return string(rune(code)), true
|
return key.Name(rune(code)), true
|
||||||
}
|
}
|
||||||
var r string
|
var r key.Name
|
||||||
|
|
||||||
switch code {
|
switch code {
|
||||||
case windows.VK_ESCAPE:
|
case windows.VK_ESCAPE:
|
||||||
@@ -954,4 +987,5 @@ func configForDPI(dpi int) unit.Metric {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_ ViewEvent) ImplementsEvent() {}
|
func (Win32ViewEvent) implementsViewEvent() {}
|
||||||
|
func (Win32ViewEvent) ImplementsEvent() {}
|
||||||
|
|||||||
+127
-91
@@ -30,16 +30,20 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/io/clipboard"
|
"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/system"
|
||||||
|
"gioui.org/io/transfer"
|
||||||
|
"gioui.org/op"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
|
|
||||||
syscall "golang.org/x/sys/unix"
|
syscall "golang.org/x/sys/unix"
|
||||||
@@ -91,12 +95,10 @@ type x11Window struct {
|
|||||||
// _NET_WM_STATE_MAXIMIZED_VERT
|
// _NET_WM_STATE_MAXIMIZED_VERT
|
||||||
wmStateMaximizedVert C.Atom
|
wmStateMaximizedVert C.Atom
|
||||||
}
|
}
|
||||||
stage system.Stage
|
|
||||||
metric unit.Metric
|
metric unit.Metric
|
||||||
notify struct {
|
notify struct {
|
||||||
read, write int
|
read, write int
|
||||||
}
|
}
|
||||||
dead bool
|
|
||||||
|
|
||||||
animating bool
|
animating bool
|
||||||
|
|
||||||
@@ -109,6 +111,11 @@ type x11Window struct {
|
|||||||
config Config
|
config Config
|
||||||
|
|
||||||
wakeups chan struct{}
|
wakeups chan struct{}
|
||||||
|
handler x11EventHandler
|
||||||
|
buf [100]byte
|
||||||
|
|
||||||
|
// invMy avoids the race between destroy and Invalidate.
|
||||||
|
invMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -151,8 +158,8 @@ func (w *x11Window) ReadClipboard() {
|
|||||||
C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime)
|
C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *x11Window) WriteClipboard(s string) {
|
func (w *x11Window) WriteClipboard(mime string, s []byte) {
|
||||||
w.clipboard.content = []byte(s)
|
w.clipboard.content = s
|
||||||
C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime)
|
C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime)
|
||||||
C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime)
|
C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime)
|
||||||
}
|
}
|
||||||
@@ -232,7 +239,7 @@ func (w *x11Window) Configure(options []Option) {
|
|||||||
if cnf.Decorated != prev.Decorated {
|
if cnf.Decorated != prev.Decorated {
|
||||||
w.config.Decorated = cnf.Decorated
|
w.config.Decorated = cnf.Decorated
|
||||||
}
|
}
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *x11Window) setTitle(prev, cnf Config) {
|
func (w *x11Window) setTitle(prev, cnf Config) {
|
||||||
@@ -375,11 +382,47 @@ func (w *x11Window) sendWMStateEvent(action C.long, atom1, atom2 C.ulong) {
|
|||||||
|
|
||||||
var x11OneByte = make([]byte, 1)
|
var x11OneByte = make([]byte, 1)
|
||||||
|
|
||||||
func (w *x11Window) Wakeup() {
|
func (w *x11Window) ProcessEvent(e event.Event) {
|
||||||
|
w.w.ProcessEvent(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *x11Window) shutdown(err error) {
|
||||||
|
w.ProcessEvent(X11ViewEvent{})
|
||||||
|
w.ProcessEvent(DestroyEvent{Err: err})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *x11Window) Event() event.Event {
|
||||||
|
for {
|
||||||
|
evt, ok := w.w.nextEvent()
|
||||||
|
if !ok {
|
||||||
|
w.dispatch()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, destroy := evt.(DestroyEvent); destroy {
|
||||||
|
w.destroy()
|
||||||
|
}
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *x11Window) Run(f func()) {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *x11Window) Frame(frame *op.Ops) {
|
||||||
|
w.w.ProcessFrame(frame, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *x11Window) Invalidate() {
|
||||||
select {
|
select {
|
||||||
case w.wakeups <- struct{}{}:
|
case w.wakeups <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
w.invMu.Lock()
|
||||||
|
defer w.invMu.Unlock()
|
||||||
|
if w.x == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
|
if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
|
||||||
panic(fmt.Errorf("failed to write to pipe: %v", err))
|
panic(fmt.Errorf("failed to write to pipe: %v", err))
|
||||||
}
|
}
|
||||||
@@ -393,16 +436,20 @@ func (w *x11Window) window() (C.Window, int, int) {
|
|||||||
return w.xw, w.config.Size.X, w.config.Size.Y
|
return w.xw, w.config.Size.X, w.config.Size.Y
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *x11Window) setStage(s system.Stage) {
|
func (w *x11Window) dispatch() {
|
||||||
if s == w.stage {
|
if w.x == nil {
|
||||||
|
// Only Invalidate can wake us up.
|
||||||
|
<-w.wakeups
|
||||||
|
w.w.Invalidate()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.stage = s
|
|
||||||
w.w.Event(system.StageEvent{Stage: s})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *x11Window) loop() {
|
select {
|
||||||
h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
|
case <-w.wakeups:
|
||||||
|
w.w.Invalidate()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
xfd := C.XConnectionNumber(w.x)
|
xfd := C.XConnectionNumber(w.x)
|
||||||
|
|
||||||
// Poll for events and notifications.
|
// Poll for events and notifications.
|
||||||
@@ -412,64 +459,52 @@ func (w *x11Window) loop() {
|
|||||||
}
|
}
|
||||||
xEvents := &pollfds[0].Revents
|
xEvents := &pollfds[0].Revents
|
||||||
// Plenty of room for a backlog of notifications.
|
// Plenty of room for a backlog of notifications.
|
||||||
buf := make([]byte, 100)
|
|
||||||
|
|
||||||
loop:
|
var syn, anim bool
|
||||||
for !w.dead {
|
// Check for pending draw events before checking animation or blocking.
|
||||||
var syn, anim bool
|
// This fixes an issue on Xephyr where on startup XPending() > 0 but
|
||||||
// Check for pending draw events before checking animation or blocking.
|
// poll will still block. This also prevents no-op calls to poll.
|
||||||
// This fixes an issue on Xephyr where on startup XPending() > 0 but
|
if syn = w.handler.handleEvents(); !syn {
|
||||||
// poll will still block. This also prevents no-op calls to poll.
|
anim = w.animating
|
||||||
if syn = h.handleEvents(); !syn {
|
if !anim {
|
||||||
anim = w.animating
|
// Clear poll events.
|
||||||
if !anim {
|
*xEvents = 0
|
||||||
// Clear poll events.
|
// Wait for X event or gio notification.
|
||||||
*xEvents = 0
|
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
|
||||||
// Wait for X event or gio notification.
|
panic(fmt.Errorf("x11 loop: poll failed: %w", err))
|
||||||
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
|
}
|
||||||
panic(fmt.Errorf("x11 loop: poll failed: %w", err))
|
switch {
|
||||||
}
|
case *xEvents&syscall.POLLIN != 0:
|
||||||
switch {
|
syn = w.handler.handleEvents()
|
||||||
case *xEvents&syscall.POLLIN != 0:
|
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
|
||||||
syn = h.handleEvents()
|
|
||||||
if w.dead {
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Clear notifications.
|
}
|
||||||
for {
|
// Clear notifications.
|
||||||
_, err := syscall.Read(w.notify.read, buf)
|
for {
|
||||||
if err == syscall.EAGAIN {
|
_, err := syscall.Read(w.notify.read, w.buf[:])
|
||||||
break
|
if err == syscall.EAGAIN {
|
||||||
}
|
break
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
select {
|
if err != nil {
|
||||||
case <-w.wakeups:
|
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
|
||||||
w.w.Event(wakeupEvent{})
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
|
|
||||||
w.w.Event(frameEvent{
|
|
||||||
FrameEvent: system.FrameEvent{
|
|
||||||
Now: time.Now(),
|
|
||||||
Size: w.config.Size,
|
|
||||||
Metric: w.metric,
|
|
||||||
},
|
|
||||||
Sync: syn,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
|
||||||
|
w.ProcessEvent(frameEvent{
|
||||||
|
FrameEvent: FrameEvent{
|
||||||
|
Now: time.Now(),
|
||||||
|
Size: w.config.Size,
|
||||||
|
Metric: w.metric,
|
||||||
|
},
|
||||||
|
Sync: syn,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *x11Window) destroy() {
|
func (w *x11Window) destroy() {
|
||||||
|
w.invMu.Lock()
|
||||||
|
defer w.invMu.Unlock()
|
||||||
if w.notify.write != 0 {
|
if w.notify.write != 0 {
|
||||||
syscall.Close(w.notify.write)
|
syscall.Close(w.notify.write)
|
||||||
w.notify.write = 0
|
w.notify.write = 0
|
||||||
@@ -484,6 +519,7 @@ func (w *x11Window) destroy() {
|
|||||||
}
|
}
|
||||||
C.XDestroyWindow(w.x, w.xw)
|
C.XDestroyWindow(w.x, w.xw)
|
||||||
C.XCloseDisplay(w.x)
|
C.XCloseDisplay(w.x)
|
||||||
|
w.x = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// atom is a wrapper around XInternAtom. Callers should cache the result
|
// atom is a wrapper around XInternAtom. Callers should cache the result
|
||||||
@@ -541,13 +577,13 @@ func (h *x11EventHandler) handleEvents() bool {
|
|||||||
// There's no support for IME yet.
|
// There's no support for IME yet.
|
||||||
w.w.EditorInsert(ee.Text)
|
w.w.EditorInsert(ee.Text)
|
||||||
} else {
|
} else {
|
||||||
w.w.Event(e)
|
w.ProcessEvent(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 +593,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 +605,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 +614,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 +623,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
|
||||||
@@ -603,11 +639,11 @@ func (h *x11EventHandler) handleEvents() bool {
|
|||||||
w.pointerBtns &^= btn
|
w.pointerBtns &^= btn
|
||||||
}
|
}
|
||||||
ev.Buttons = w.pointerBtns
|
ev.Buttons = w.pointerBtns
|
||||||
w.w.Event(ev)
|
w.ProcessEvent(ev)
|
||||||
case C.MotionNotify:
|
case C.MotionNotify:
|
||||||
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
|
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
|
||||||
w.w.Event(pointer.Event{
|
w.ProcessEvent(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{
|
||||||
@@ -621,14 +657,16 @@ func (h *x11EventHandler) handleEvents() bool {
|
|||||||
// redraw only on the last expose event
|
// redraw only on the last expose event
|
||||||
redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0
|
redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0
|
||||||
case C.FocusIn:
|
case C.FocusIn:
|
||||||
w.w.Event(key.FocusEvent{Focus: true})
|
w.config.Focused = true
|
||||||
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
case C.FocusOut:
|
case C.FocusOut:
|
||||||
w.w.Event(key.FocusEvent{Focus: false})
|
w.config.Focused = false
|
||||||
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
case C.ConfigureNotify: // window configuration change
|
case C.ConfigureNotify: // window configuration change
|
||||||
cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev))
|
cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev))
|
||||||
if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size {
|
if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size {
|
||||||
w.config.Size = sz
|
w.config.Size = sz
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||||
}
|
}
|
||||||
// redraw will be done by a later expose event
|
// redraw will be done by a later expose event
|
||||||
case C.SelectionNotify:
|
case C.SelectionNotify:
|
||||||
@@ -650,7 +688,12 @@ func (h *x11EventHandler) handleEvents() bool {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
|
str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
|
||||||
w.w.Event(clipboard.Event{Text: str})
|
w.ProcessEvent(transfer.DataEvent{
|
||||||
|
Type: "application/text",
|
||||||
|
Open: func() io.ReadCloser {
|
||||||
|
return io.NopCloser(strings.NewReader(str))
|
||||||
|
},
|
||||||
|
})
|
||||||
case C.SelectionRequest:
|
case C.SelectionRequest:
|
||||||
cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
|
cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
|
||||||
if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None {
|
if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None {
|
||||||
@@ -704,7 +747,7 @@ func (h *x11EventHandler) handleEvents() bool {
|
|||||||
cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev))
|
cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev))
|
||||||
switch *(*C.long)(unsafe.Pointer(&cevt.data)) {
|
switch *(*C.long)(unsafe.Pointer(&cevt.data)) {
|
||||||
case C.long(w.atoms.evDelWindow):
|
case C.long(w.atoms.evDelWindow):
|
||||||
w.dead = true
|
w.shutdown(nil)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -786,8 +829,10 @@ func newX11Window(gioWin *callbacks, options []Option) error {
|
|||||||
wakeups: make(chan struct{}, 1),
|
wakeups: make(chan struct{}, 1),
|
||||||
config: Config{Size: cnf.Size},
|
config: Config{Size: cnf.Size},
|
||||||
}
|
}
|
||||||
|
w.handler = x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
|
||||||
w.notify.read = pipe[0]
|
w.notify.read = pipe[0]
|
||||||
w.notify.write = pipe[1]
|
w.notify.write = pipe[1]
|
||||||
|
w.w.SetDriver(w)
|
||||||
|
|
||||||
if err := w.updateXkbKeymap(); err != nil {
|
if err := w.updateXkbKeymap(); err != nil {
|
||||||
w.destroy()
|
w.destroy()
|
||||||
@@ -823,19 +868,10 @@ func newX11Window(gioWin *callbacks, options []Option) error {
|
|||||||
// extensions
|
// extensions
|
||||||
C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1)
|
C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1)
|
||||||
|
|
||||||
go func() {
|
// make the window visible on the screen
|
||||||
w.w.SetDriver(w)
|
C.XMapWindow(dpy, win)
|
||||||
|
w.Configure(options)
|
||||||
// make the window visible on the screen
|
w.ProcessEvent(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
|
||||||
C.XMapWindow(dpy, win)
|
|
||||||
w.Configure(options)
|
|
||||||
w.w.Event(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
|
|
||||||
w.setStage(system.StageRunning)
|
|
||||||
w.loop()
|
|
||||||
w.w.Event(X11ViewEvent{})
|
|
||||||
w.w.Event(system.DestroyEvent{Err: nil})
|
|
||||||
w.destroy()
|
|
||||||
}()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -25,6 +25,6 @@ func runMain() {
|
|||||||
// Indirect call, since the linker does not know the address of main when
|
// Indirect call, since the linker does not know the address of main when
|
||||||
// laying down this package.
|
// laying down this package.
|
||||||
fn := mainMain
|
fn := mainMain
|
||||||
fn()
|
go fn()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
// DestroyEvent is the last event sent through
|
||||||
|
// a window event channel.
|
||||||
|
type DestroyEvent struct {
|
||||||
|
// Err is nil for normal window closures. If a
|
||||||
|
// window is prematurely closed, Err is the cause.
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DestroyEvent) ImplementsEvent() {}
|
||||||
+368
-596
File diff suppressed because it is too large
Load Diff
Generated
+48
-17
@@ -9,11 +9,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1659298920,
|
"lastModified": 1701721028,
|
||||||
"narHash": "sha256-LgRMge8BZUG15EN43iDJOlnEMX1dvRprB7SaoNqgibU=",
|
"narHash": "sha256-2z4YrdHPLoMZNWR1MPOjNZMqPg057i1eZXaYI6RTahQ=",
|
||||||
"owner": "tadfisher",
|
"owner": "tadfisher",
|
||||||
"repo": "android-nixpkgs",
|
"repo": "android-nixpkgs",
|
||||||
"rev": "d4f20a3cd4ce961bb23b48447457f6810d69ae5e",
|
"rev": "c923f9ec0f4dd0d7dc725dc5b73fbf03658e50dd",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -24,21 +24,18 @@
|
|||||||
},
|
},
|
||||||
"devshell": {
|
"devshell": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": [
|
|
||||||
"android",
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"android",
|
"android",
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
],
|
||||||
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1658746384,
|
"lastModified": 1701697687,
|
||||||
"narHash": "sha256-CCJcoMOcXyZFrV1ag4XMTpAPjLWb4Anbv+ktXFI1ry0=",
|
"narHash": "sha256-dLLE5wQBVv+pIb4bWmKFSw2DvLVyuEk0F7ng6hpZPSU=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "devshell",
|
"repo": "devshell",
|
||||||
"rev": "0ffc7937bb5e8141af03d462b468bd071eb18e1b",
|
"rev": "c3bd77911391eb1638af6ce773de86da57ee6df5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -48,12 +45,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems_2"
|
||||||
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1656928814,
|
"lastModified": 1701680307,
|
||||||
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
|
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
|
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -64,15 +64,16 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1659305579,
|
"lastModified": 1701282334,
|
||||||
"narHash": "sha256-SFeQTmh7hc9Y2fSkooHaoS8mDfPa04sfmUCtQ8MA6Pg=",
|
"narHash": "sha256-MxCVrXY6v4QmfTwIysjjaX0XUhqBbxTWWB4HXtDYsdk=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "5857574d45925585baffde730369414319228a84",
|
"rev": "057f9aecfb71c4437d2b27d3323df7f93c010b7e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
|
"ref": "23.11",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
@@ -82,6 +83,36 @@
|
|||||||
"android": "android",
|
"android": "android",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
description = "Gio build environment";
|
description = "Gio build environment";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs";
|
nixpkgs.url = "github:NixOS/nixpkgs/23.11";
|
||||||
android.url = "github:tadfisher/android-nixpkgs";
|
android.url = "github:tadfisher/android-nixpkgs";
|
||||||
android.inputs.nixpkgs.follows = "nixpkgs";
|
android.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
@@ -33,10 +33,10 @@
|
|||||||
default = with pkgs; mkShell
|
default = with pkgs; mkShell
|
||||||
({
|
({
|
||||||
ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk";
|
ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk";
|
||||||
JAVA_HOME = jdk8.home;
|
JAVA_HOME = jdk17.home;
|
||||||
packages = [
|
packages = [
|
||||||
android-sdk
|
android-sdk
|
||||||
jdk8
|
jdk17
|
||||||
clang
|
clang
|
||||||
] ++ (if stdenv.isLinux then [
|
] ++ (if stdenv.isLinux then [
|
||||||
vulkan-headers
|
vulkan-headers
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
xorg.libXcursor
|
xorg.libXcursor
|
||||||
xorg.libXfixes
|
xorg.libXfixes
|
||||||
libGL
|
libGL
|
||||||
pkgconfig
|
pkg-config
|
||||||
] else if stdenv.isDarwin then [
|
] else if stdenv.isDarwin then [
|
||||||
darwin.apple_sdk_11_0.frameworks.Foundation
|
darwin.apple_sdk_11_0.frameworks.Foundation
|
||||||
darwin.apple_sdk_11_0.frameworks.Metal
|
darwin.apple_sdk_11_0.frameworks.Metal
|
||||||
|
|||||||
+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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+95
-86
@@ -18,6 +18,7 @@ import (
|
|||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/internal/fling"
|
"gioui.org/internal/fling"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
|
"gioui.org/io/input"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
@@ -37,20 +38,24 @@ type Hover struct {
|
|||||||
|
|
||||||
// Add the gesture to detect hovering over the current pointer area.
|
// Add the gesture to detect hovering over the current pointer area.
|
||||||
func (h *Hover) Add(ops *op.Ops) {
|
func (h *Hover) Add(ops *op.Ops) {
|
||||||
pointer.InputOp{
|
event.Op(ops, h)
|
||||||
Tag: h,
|
|
||||||
Types: pointer.Enter | pointer.Leave,
|
|
||||||
}.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 input.Source) bool {
|
||||||
for _, ev := range q.Events(h) {
|
for {
|
||||||
|
ev, ok := q.Event(pointer.Filter{
|
||||||
|
Target: h,
|
||||||
|
Kinds: pointer.Enter | pointer.Leave | pointer.Cancel,
|
||||||
|
})
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
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 +92,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 +104,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 {
|
||||||
@@ -107,7 +112,6 @@ type Drag struct {
|
|||||||
pressed bool
|
pressed bool
|
||||||
pid pointer.ID
|
pid pointer.ID
|
||||||
start f32.Point
|
start f32.Point
|
||||||
grab bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll detects scroll gestures and reduces them to
|
// Scroll detects scroll gestures and reduces them to
|
||||||
@@ -115,11 +119,9 @@ type Drag struct {
|
|||||||
// movements as well as drag and fling touch gestures.
|
// movements as well as drag and fling touch gestures.
|
||||||
type Scroll struct {
|
type Scroll struct {
|
||||||
dragging bool
|
dragging bool
|
||||||
axis Axis
|
|
||||||
estimator fling.Extrapolation
|
estimator fling.Extrapolation
|
||||||
flinger fling.Animation
|
flinger fling.Animation
|
||||||
pid pointer.ID
|
pid pointer.ID
|
||||||
grab bool
|
|
||||||
last int
|
last int
|
||||||
// Leftover scroll.
|
// Leftover scroll.
|
||||||
scroll float32
|
scroll float32
|
||||||
@@ -136,15 +138,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 (
|
||||||
@@ -161,10 +163,7 @@ const touchSlop = unit.Dp(3)
|
|||||||
|
|
||||||
// Add the handler to the operation list to receive click events.
|
// Add the handler to the operation list to receive click events.
|
||||||
func (c *Click) Add(ops *op.Ops) {
|
func (c *Click) Add(ops *op.Ops) {
|
||||||
pointer.InputOp{
|
event.Op(ops, c)
|
||||||
Tag: c,
|
|
||||||
Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
|
|
||||||
}.Add(ops)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hovered returns whether a pointer is inside the area.
|
// Hovered returns whether a pointer is inside the area.
|
||||||
@@ -177,24 +176,36 @@ func (c *Click) Pressed() bool {
|
|||||||
return c.pressed
|
return c.pressed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events returns the next click events, if any.
|
// Update state and return the next click events, if any.
|
||||||
func (c *Click) Events(q event.Queue) []ClickEvent {
|
func (c *Click) Update(q input.Source) (ClickEvent, bool) {
|
||||||
var events []ClickEvent
|
for {
|
||||||
for _, evt := range q.Events(c) {
|
evt, ok := q.Event(pointer.Filter{
|
||||||
|
Target: c,
|
||||||
|
Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave | pointer.Cancel,
|
||||||
|
})
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
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})
|
return ClickEvent{
|
||||||
|
Kind: KindClick,
|
||||||
|
Position: e.Position.Round(),
|
||||||
|
Source: e.Source,
|
||||||
|
Modifiers: e.Modifiers,
|
||||||
|
NumClicks: c.clicks,
|
||||||
|
}, true
|
||||||
} else {
|
} else {
|
||||||
events = append(events, ClickEvent{Type: TypeCancel})
|
return ClickEvent{Kind: KindCancel}, true
|
||||||
}
|
}
|
||||||
case pointer.Cancel:
|
case pointer.Cancel:
|
||||||
wasPressed := c.pressed
|
wasPressed := c.pressed
|
||||||
@@ -202,7 +213,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})
|
return ClickEvent{Kind: KindCancel}, true
|
||||||
}
|
}
|
||||||
case pointer.Press:
|
case pointer.Press:
|
||||||
if c.pressed {
|
if c.pressed {
|
||||||
@@ -224,7 +235,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})
|
return ClickEvent{Kind: KindPress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks}, true
|
||||||
case pointer.Leave:
|
case pointer.Leave:
|
||||||
if !c.pressed {
|
if !c.pressed {
|
||||||
c.pid = e.PointerID
|
c.pid = e.PointerID
|
||||||
@@ -242,25 +253,16 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return events
|
return ClickEvent{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ClickEvent) ImplementsEvent() {}
|
func (ClickEvent) ImplementsEvent() {}
|
||||||
|
|
||||||
// Add the handler to the operation list to receive scroll events.
|
// Add the handler to the operation list to receive scroll events.
|
||||||
// The bounds variable refers to the scrolling boundaries
|
// The bounds variable refers to the scrolling boundaries
|
||||||
// as defined in io/pointer.InputOp.
|
// as defined in [pointer.Filter].
|
||||||
func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) {
|
func (s *Scroll) Add(ops *op.Ops) {
|
||||||
oph := pointer.InputOp{
|
event.Op(ops, s)
|
||||||
Tag: s,
|
|
||||||
Grab: s.grab,
|
|
||||||
Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
|
|
||||||
ScrollBounds: bounds,
|
|
||||||
}
|
|
||||||
oph.Add(ops)
|
|
||||||
if s.flinger.Active() {
|
|
||||||
op.InvalidateOp{}.Add(ops)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop any remaining fling movement.
|
// Stop any remaining fling movement.
|
||||||
@@ -268,20 +270,24 @@ 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 input.Source, t time.Time, axis Axis, bounds image.Rectangle) int {
|
||||||
func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis) int {
|
|
||||||
if s.axis != axis {
|
|
||||||
s.axis = axis
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
total := 0
|
total := 0
|
||||||
for _, evt := range q.Events(s) {
|
f := pointer.Filter{
|
||||||
|
Target: s,
|
||||||
|
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
|
||||||
|
ScrollBounds: bounds,
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
evt, ok := q.Event(f)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
e, ok := evt.(pointer.Event)
|
e, ok := evt.(pointer.Event)
|
||||||
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
|
||||||
@@ -293,7 +299,7 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
|
|||||||
}
|
}
|
||||||
s.Stop()
|
s.Stop()
|
||||||
s.estimator = fling.Extrapolation{}
|
s.estimator = fling.Extrapolation{}
|
||||||
v := s.val(e.Position)
|
v := s.val(axis, e.Position)
|
||||||
s.last = int(math.Round(float64(v)))
|
s.last = int(math.Round(float64(v)))
|
||||||
s.estimator.Sample(e.Time, v)
|
s.estimator.Sample(e.Time, v)
|
||||||
s.dragging = true
|
s.dragging = true
|
||||||
@@ -309,9 +315,8 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
|
|||||||
fallthrough
|
fallthrough
|
||||||
case pointer.Cancel:
|
case pointer.Cancel:
|
||||||
s.dragging = false
|
s.dragging = false
|
||||||
s.grab = false
|
|
||||||
case pointer.Scroll:
|
case pointer.Scroll:
|
||||||
switch s.axis {
|
switch axis {
|
||||||
case Horizontal:
|
case Horizontal:
|
||||||
s.scroll += e.Scroll.X
|
s.scroll += e.Scroll.X
|
||||||
case Vertical:
|
case Vertical:
|
||||||
@@ -324,14 +329,14 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
|
|||||||
if !s.dragging || s.pid != e.PointerID {
|
if !s.dragging || s.pid != e.PointerID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val := s.val(e.Position)
|
val := s.val(axis, e.Position)
|
||||||
s.estimator.Sample(e.Time, val)
|
s.estimator.Sample(e.Time, val)
|
||||||
v := int(math.Round(float64(val)))
|
v := int(math.Round(float64(val)))
|
||||||
dist := s.last - v
|
dist := s.last - v
|
||||||
if e.Priority < pointer.Grabbed {
|
if e.Priority < pointer.Grabbed {
|
||||||
slop := cfg.Dp(touchSlop)
|
slop := cfg.Dp(touchSlop)
|
||||||
if dist := dist; dist >= slop || -slop >= dist {
|
if dist := dist; dist >= slop || -slop >= dist {
|
||||||
s.grab = true
|
q.Execute(pointer.GrabCmd{Tag: s, ID: e.PointerID})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
s.last = v
|
s.last = v
|
||||||
@@ -340,11 +345,14 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
total += s.flinger.Tick(t)
|
total += s.flinger.Tick(t)
|
||||||
|
if s.flinger.Active() {
|
||||||
|
q.Execute(op.InvalidateCmd{})
|
||||||
|
}
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Scroll) val(p f32.Point) float32 {
|
func (s *Scroll) val(axis Axis, p f32.Point) float32 {
|
||||||
if s.axis == Horizontal {
|
if axis == Horizontal {
|
||||||
return p.X
|
return p.X
|
||||||
} else {
|
} else {
|
||||||
return p.Y
|
return p.Y
|
||||||
@@ -365,23 +373,25 @@ func (s *Scroll) State() ScrollState {
|
|||||||
|
|
||||||
// Add the handler to the operation list to receive drag events.
|
// Add the handler to the operation list to receive drag events.
|
||||||
func (d *Drag) Add(ops *op.Ops) {
|
func (d *Drag) Add(ops *op.Ops) {
|
||||||
pointer.InputOp{
|
event.Op(ops, d)
|
||||||
Tag: d,
|
|
||||||
Grab: d.grab,
|
|
||||||
Types: pointer.Press | pointer.Drag | pointer.Release,
|
|
||||||
}.Add(ops)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events returns the next drag events, if any.
|
// Update state and return the next drag event, if any.
|
||||||
func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event {
|
func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event, bool) {
|
||||||
var events []pointer.Event
|
for {
|
||||||
for _, e := range q.Events(d) {
|
ev, ok := q.Event(pointer.Filter{
|
||||||
e, ok := e.(pointer.Event)
|
Target: d,
|
||||||
|
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel,
|
||||||
|
})
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
e, ok := ev.(pointer.Event)
|
||||||
if !ok {
|
if !ok {
|
||||||
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
|
||||||
@@ -409,7 +419,7 @@ func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event
|
|||||||
diff := e.Position.Sub(d.start)
|
diff := e.Position.Sub(d.start)
|
||||||
slop := cfg.Dp(touchSlop)
|
slop := cfg.Dp(touchSlop)
|
||||||
if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
|
if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
|
||||||
d.grab = true
|
q.Execute(pointer.GrabCmd{Tag: d, ID: e.PointerID})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case pointer.Release, pointer.Cancel:
|
case pointer.Release, pointer.Cancel:
|
||||||
@@ -418,13 +428,12 @@ func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
d.dragging = false
|
d.dragging = false
|
||||||
d.grab = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
events = append(events, e)
|
return e, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return events
|
return pointer.Event{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dragging reports whether it is currently in use.
|
// Dragging reports whether it is currently in use.
|
||||||
@@ -444,16 +453,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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-21
@@ -9,8 +9,8 @@ import (
|
|||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
|
"gioui.org/io/input"
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
"gioui.org/io/router"
|
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
)
|
)
|
||||||
@@ -22,20 +22,21 @@ func TestHover(t *testing.T) {
|
|||||||
stack := clip.Rect(rect).Push(ops)
|
stack := clip.Rect(rect).Push(ops)
|
||||||
h.Add(ops)
|
h.Add(ops)
|
||||||
stack.Pop()
|
stack.Pop()
|
||||||
r := new(router.Router)
|
r := new(input.Router)
|
||||||
|
h.Update(r.Source())
|
||||||
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.Source()) {
|
||||||
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.Source()) {
|
||||||
t.Fatal("expected not hovered")
|
t.Fatal("expected not hovered")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,12 +72,21 @@ func TestMouseClicks(t *testing.T) {
|
|||||||
var ops op.Ops
|
var ops op.Ops
|
||||||
click.Add(&ops)
|
click.Add(&ops)
|
||||||
|
|
||||||
var r router.Router
|
var r input.Router
|
||||||
|
click.Update(r.Source())
|
||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
r.Queue(tc.events...)
|
r.Queue(tc.events...)
|
||||||
|
|
||||||
events := click.Events(&r)
|
var clicks []ClickEvent
|
||||||
clicks := filterMouseClicks(events)
|
for {
|
||||||
|
ev, ok := click.Update(r.Source())
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ev.Kind == KindClick {
|
||||||
|
clicks = append(clicks, ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
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 +102,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,18 +111,8 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterMouseClicks(events []ClickEvent) []ClickEvent {
|
|
||||||
var clicks []ClickEvent
|
|
||||||
for _, ev := range events {
|
|
||||||
if ev.Type == TypeClick {
|
|
||||||
clicks = append(clicks, ev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return clicks
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
+15
-10
@@ -8,8 +8,13 @@ import (
|
|||||||
"gioui.org/internal/f32"
|
"gioui.org/internal/f32"
|
||||||
)
|
)
|
||||||
|
|
||||||
type resourceCache struct {
|
type textureCacheKey struct {
|
||||||
res map[interface{}]resourceCacheValue
|
filter byte
|
||||||
|
handle any
|
||||||
|
}
|
||||||
|
|
||||||
|
type textureCache struct {
|
||||||
|
res map[textureCacheKey]resourceCacheValue
|
||||||
}
|
}
|
||||||
|
|
||||||
type resourceCacheValue struct {
|
type resourceCacheValue struct {
|
||||||
@@ -37,13 +42,13 @@ type opCacheValue struct {
|
|||||||
keep bool
|
keep bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newResourceCache() *resourceCache {
|
func newTextureCache() *textureCache {
|
||||||
return &resourceCache{
|
return &textureCache{
|
||||||
res: make(map[interface{}]resourceCacheValue),
|
res: make(map[textureCacheKey]resourceCacheValue),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resourceCache) get(key interface{}) (resource, bool) {
|
func (r *textureCache) get(key textureCacheKey) (resource, bool) {
|
||||||
v, exists := r.res[key]
|
v, exists := r.res[key]
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, false
|
return nil, false
|
||||||
@@ -55,17 +60,17 @@ func (r *resourceCache) get(key interface{}) (resource, bool) {
|
|||||||
return v.resource, exists
|
return v.resource, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resourceCache) put(key interface{}, val resource) {
|
func (r *textureCache) put(key textureCacheKey, val resource) {
|
||||||
v, exists := r.res[key]
|
v, exists := r.res[key]
|
||||||
if exists && v.used {
|
if exists && v.used {
|
||||||
panic(fmt.Errorf("key exists, %p", key))
|
panic(fmt.Errorf("key exists, %v", key))
|
||||||
}
|
}
|
||||||
v.used = true
|
v.used = true
|
||||||
v.resource = val
|
v.resource = val
|
||||||
r.res[key] = v
|
r.res[key] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resourceCache) frame() {
|
func (r *textureCache) frame() {
|
||||||
for k, v := range r.res {
|
for k, v := range r.res {
|
||||||
if v.used {
|
if v.used {
|
||||||
v.used = false
|
v.used = false
|
||||||
@@ -77,7 +82,7 @@ func (r *resourceCache) frame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resourceCache) release() {
|
func (r *textureCache) release() {
|
||||||
for _, v := range r.res {
|
for _, v := range r.res {
|
||||||
v.resource.release()
|
v.resource.release()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
|
||||||
|
|
||||||
package gpu
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func BenchmarkResourceCache(b *testing.B) {
|
|
||||||
offset := 0
|
|
||||||
const N = 100
|
|
||||||
|
|
||||||
cache := newResourceCache()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
// half are the same and half updated
|
|
||||||
for k := 0; k < N; k++ {
|
|
||||||
cache.put(offset+k, nullResource{})
|
|
||||||
}
|
|
||||||
cache.frame()
|
|
||||||
offset += N / 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type nullResource struct{}
|
|
||||||
|
|
||||||
func (nullResource) release() {}
|
|
||||||
+3
-12
@@ -93,7 +93,6 @@ type compute struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
timers struct {
|
timers struct {
|
||||||
profile string
|
|
||||||
t *timers
|
t *timers
|
||||||
compact *timer
|
compact *timer
|
||||||
render *timer
|
render *timer
|
||||||
@@ -176,7 +175,6 @@ type materialUniforms struct {
|
|||||||
|
|
||||||
type collector struct {
|
type collector struct {
|
||||||
hasher maphash.Hash
|
hasher maphash.Hash
|
||||||
profile bool
|
|
||||||
reader ops.Reader
|
reader ops.Reader
|
||||||
states []f32.Affine2D
|
states []f32.Affine2D
|
||||||
clear bool
|
clear bool
|
||||||
@@ -597,7 +595,7 @@ func (g *compute) frame(target RenderTarget) error {
|
|||||||
defer g.ctx.EndFrame()
|
defer g.ctx.EndFrame()
|
||||||
|
|
||||||
t := &g.timers
|
t := &g.timers
|
||||||
if g.collector.profile && t.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
|
if false && t.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
|
||||||
t.t = newTimers(g.ctx)
|
t.t = newTimers(g.ctx)
|
||||||
t.compact = t.t.newTimer()
|
t.compact = t.t.newTimer()
|
||||||
t.render = t.t.newTimer()
|
t.render = t.t.newTimer()
|
||||||
@@ -631,13 +629,13 @@ func (g *compute) frame(target RenderTarget) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.compact.end()
|
t.compact.end()
|
||||||
if g.collector.profile && t.t.ready() {
|
if false && t.t.ready() {
|
||||||
com, ren, blit := t.compact.Elapsed, t.render.Elapsed, t.blit.Elapsed
|
com, ren, blit := t.compact.Elapsed, t.render.Elapsed, t.blit.Elapsed
|
||||||
ft := com + ren + blit
|
ft := com + ren + blit
|
||||||
q := 100 * time.Microsecond
|
q := 100 * time.Microsecond
|
||||||
ft = ft.Round(q)
|
ft = ft.Round(q)
|
||||||
com, ren, blit = com.Round(q), ren.Round(q), blit.Round(q)
|
com, ren, blit = com.Round(q), ren.Round(q), blit.Round(q)
|
||||||
t.profile = fmt.Sprintf("ft:%7s com: %7s ren:%7s blit:%7s", ft, com, ren, blit)
|
// t.profile = fmt.Sprintf("ft:%7s com: %7s ren:%7s blit:%7s", ft, com, ren, blit)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -661,10 +659,6 @@ func (g *compute) dumpAtlases() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *compute) Profile() string {
|
|
||||||
return g.timers.profile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *compute) compactAllocs() error {
|
func (g *compute) compactAllocs() error {
|
||||||
const (
|
const (
|
||||||
maxAllocAge = 3
|
maxAllocAge = 3
|
||||||
@@ -1656,7 +1650,6 @@ func (e *encoder) line(start, end f32.Point) {
|
|||||||
|
|
||||||
func (c *collector) reset() {
|
func (c *collector) reset() {
|
||||||
c.prevFrame, c.frame = c.frame, c.prevFrame
|
c.prevFrame, c.frame = c.frame, c.prevFrame
|
||||||
c.profile = false
|
|
||||||
c.clipStates = c.clipStates[:0]
|
c.clipStates = c.clipStates[:0]
|
||||||
c.transStack = c.transStack[:0]
|
c.transStack = c.transStack[:0]
|
||||||
c.frame.reset()
|
c.frame.reset()
|
||||||
@@ -1736,8 +1729,6 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur
|
|||||||
c.addClip(&state, fview, fview, nil, ops.Key{}, 0, 0, false)
|
c.addClip(&state, fview, fview, nil, ops.Key{}, 0, 0, false)
|
||||||
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
|
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
|
||||||
switch ops.OpType(encOp.Data[0]) {
|
switch ops.OpType(encOp.Data[0]) {
|
||||||
case ops.TypeProfile:
|
|
||||||
c.profile = true
|
|
||||||
case ops.TypeTransform:
|
case ops.TypeTransform:
|
||||||
dop, push := ops.DecodeTransform(encOp.Data)
|
dop, push := ops.DecodeTransform(encOp.Data)
|
||||||
if push {
|
if push {
|
||||||
|
|||||||
+229
-61
@@ -44,14 +44,10 @@ type GPU interface {
|
|||||||
Clear(color color.NRGBA)
|
Clear(color color.NRGBA)
|
||||||
// Frame draws the graphics operations from op into a viewport of target.
|
// Frame draws the graphics operations from op into a viewport of target.
|
||||||
Frame(frame *op.Ops, target RenderTarget, viewport image.Point) error
|
Frame(frame *op.Ops, target RenderTarget, viewport image.Point) error
|
||||||
// Profile returns the last available profiling information. Profiling
|
|
||||||
// information is requested when Frame sees an io/profile.Op, and the result
|
|
||||||
// is available through Profile at some later time.
|
|
||||||
Profile() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type gpu struct {
|
type gpu struct {
|
||||||
cache *resourceCache
|
cache *textureCache
|
||||||
|
|
||||||
profile string
|
profile string
|
||||||
timers *timers
|
timers *timers
|
||||||
@@ -68,22 +64,39 @@ 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
|
reader ops.Reader
|
||||||
reader ops.Reader
|
states []f32.Affine2D
|
||||||
states []f32.Affine2D
|
transStack []f32.Affine2D
|
||||||
transStack []f32.Affine2D
|
layers []opacityLayer
|
||||||
vertCache []byte
|
opacityStack []int
|
||||||
viewport image.Point
|
vertCache []byte
|
||||||
clear bool
|
viewport image.Point
|
||||||
clearColor f32color.RGBA
|
clear bool
|
||||||
imageOps []imageOp
|
clearColor f32color.RGBA
|
||||||
pathOps []*pathOp
|
imageOps []imageOp
|
||||||
pathOpCache []pathOp
|
pathOps []*pathOp
|
||||||
qs quadSplitter
|
pathOpCache []pathOp
|
||||||
pathCache *opCache
|
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 +140,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 +172,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 +208,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 +249,6 @@ func decodeLinearGradientOp(data []byte) linearGradientOpData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type clipType uint8
|
|
||||||
|
|
||||||
type resource interface {
|
type resource interface {
|
||||||
release()
|
release()
|
||||||
}
|
}
|
||||||
@@ -273,6 +298,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 +312,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 +320,8 @@ const (
|
|||||||
clipTypeIntersection
|
clipTypeIntersection
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type materialType uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
materialColor materialType = iota
|
materialColor materialType = iota
|
||||||
materialLinearGradient
|
materialLinearGradient
|
||||||
@@ -324,7 +354,7 @@ func NewWithDevice(d driver.Device) (GPU, error) {
|
|||||||
|
|
||||||
func newGPU(ctx driver.Device) (*gpu, error) {
|
func newGPU(ctx driver.Device) (*gpu, error) {
|
||||||
g := &gpu{
|
g := &gpu{
|
||||||
cache: newResourceCache(),
|
cache: newTextureCache(),
|
||||||
}
|
}
|
||||||
g.drawOps.pathCache = newOpCache()
|
g.drawOps.pathCache = newOpCache()
|
||||||
if err := g.init(ctx); err != nil {
|
if err := g.init(ctx); err != nil {
|
||||||
@@ -364,7 +394,7 @@ func (g *gpu) collect(viewport image.Point, frameOps *op.Ops) {
|
|||||||
g.renderer.pather.viewport = viewport
|
g.renderer.pather.viewport = viewport
|
||||||
g.drawOps.reset(viewport)
|
g.drawOps.reset(viewport)
|
||||||
g.drawOps.collect(frameOps, viewport)
|
g.drawOps.collect(frameOps, viewport)
|
||||||
if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
|
if false && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
|
||||||
g.frameStart = time.Now()
|
g.frameStart = time.Now()
|
||||||
g.timers = newTimers(g.ctx)
|
g.timers = newTimers(g.ctx)
|
||||||
g.stencilTimer = g.timers.newTimer()
|
g.stencilTimer = g.timers.newTimer()
|
||||||
@@ -390,7 +420,9 @@ func (g *gpu) frame(target RenderTarget) error {
|
|||||||
g.stencilTimer.end()
|
g.stencilTimer.end()
|
||||||
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.drawOps.imageOps)
|
||||||
|
g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers)
|
||||||
|
g.renderer.drawLayers(g.drawOps.layers, g.drawOps.imageOps)
|
||||||
d := driver.LoadDesc{
|
d := driver.LoadDesc{
|
||||||
ClearColor: g.drawOps.clearColor,
|
ClearColor: g.drawOps.clearColor,
|
||||||
}
|
}
|
||||||
@@ -400,14 +432,14 @@ 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(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()
|
||||||
g.cache.frame()
|
g.cache.frame()
|
||||||
g.drawOps.pathCache.frame()
|
g.drawOps.pathCache.frame()
|
||||||
g.cleanupTimer.end()
|
g.cleanupTimer.end()
|
||||||
if g.drawOps.profile && g.timers.ready() {
|
if false && g.timers.ready() {
|
||||||
st, covt, cleant := g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
|
st, covt, cleant := g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
|
||||||
ft := st + covt + cleant
|
ft := st + covt + cleant
|
||||||
q := 100 * time.Microsecond
|
q := 100 * time.Microsecond
|
||||||
@@ -423,20 +455,38 @@ func (g *gpu) Profile() string {
|
|||||||
return g.profile
|
return g.profile
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture {
|
func (r *renderer) texHandle(cache *textureCache, data imageOpData) driver.Texture {
|
||||||
|
key := textureCacheKey{
|
||||||
|
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 +514,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 +800,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,14 +812,92 @@ 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(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(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.viewport = viewport
|
d.viewport = viewport
|
||||||
d.imageOps = d.imageOps[:0]
|
d.imageOps = d.imageOps[:0]
|
||||||
d.pathOps = d.pathOps[:0]
|
d.pathOps = d.pathOps[:0]
|
||||||
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) {
|
||||||
@@ -853,8 +983,6 @@ func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) {
|
|||||||
loop:
|
loop:
|
||||||
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
|
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
|
||||||
switch ops.OpType(encOp.Data[0]) {
|
switch ops.OpType(encOp.Data[0]) {
|
||||||
case ops.TypeProfile:
|
|
||||||
d.profile = true
|
|
||||||
case ops.TypeTransform:
|
case ops.TypeTransform:
|
||||||
dop, push := ops.DecodeTransform(encOp.Data)
|
dop, push := ops.DecodeTransform(encOp.Data)
|
||||||
if push {
|
if push {
|
||||||
@@ -866,6 +994,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 +1107,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 +1120,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 +1158,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
|
||||||
@@ -1039,24 +1199,25 @@ func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) {
|
func (r *renderer) uploadImages(cache *textureCache, 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) {
|
func (r *renderer) prepareDrawOps(ops []imageOp) {
|
||||||
for _, img := range ops {
|
for _, img := range ops {
|
||||||
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 +1230,26 @@ func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
|
func (r *renderer) drawOps(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 +1268,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 +1282,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -665,7 +665,7 @@ func (f *Functions) load(forceES bool) error {
|
|||||||
case runtime.GOOS == "android":
|
case runtime.GOOS == "android":
|
||||||
libNames = []string{"libGLESv2.so", "libGLESv3.so"}
|
libNames = []string{"libGLESv2.so", "libGLESv3.so"}
|
||||||
default:
|
default:
|
||||||
libNames = []string{"libGLESv2.so.2"}
|
libNames = []string{"libGLESv2.so.2", "libGLESv2.so.3.0"}
|
||||||
}
|
}
|
||||||
for _, lib := range libNames {
|
for _, lib := range libNames {
|
||||||
if h := dlopen(lib); h != nil {
|
if h := dlopen(lib); h != nil {
|
||||||
|
|||||||
+79
-86
@@ -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,31 +51,23 @@ const (
|
|||||||
TypeMacro OpType = iota + firstOpIndex
|
TypeMacro OpType = iota + firstOpIndex
|
||||||
TypeCall
|
TypeCall
|
||||||
TypeDefer
|
TypeDefer
|
||||||
TypePushTransform
|
|
||||||
TypeTransform
|
TypeTransform
|
||||||
TypePopTransform
|
TypePopTransform
|
||||||
TypeInvalidate
|
TypePushOpacity
|
||||||
|
TypePopOpacity
|
||||||
TypeImage
|
TypeImage
|
||||||
TypePaint
|
TypePaint
|
||||||
TypeColor
|
TypeColor
|
||||||
TypeLinearGradient
|
TypeLinearGradient
|
||||||
TypePass
|
TypePass
|
||||||
TypePopPass
|
TypePopPass
|
||||||
TypePointerInput
|
TypeInput
|
||||||
TypeClipboardRead
|
TypeKeyInputHint
|
||||||
TypeClipboardWrite
|
|
||||||
TypeSource
|
|
||||||
TypeTarget
|
|
||||||
TypeOffer
|
|
||||||
TypeKeyInput
|
|
||||||
TypeKeyFocus
|
|
||||||
TypeKeySoftKeyboard
|
|
||||||
TypeSave
|
TypeSave
|
||||||
TypeLoad
|
TypeLoad
|
||||||
TypeAux
|
TypeAux
|
||||||
TypeClip
|
TypeClip
|
||||||
TypePopClip
|
TypePopClip
|
||||||
TypeProfile
|
|
||||||
TypeCursor
|
TypeCursor
|
||||||
TypePath
|
TypePath
|
||||||
TypeStroke
|
TypeStroke
|
||||||
@@ -72,30 +75,28 @@ const (
|
|||||||
TypeSemanticDesc
|
TypeSemanticDesc
|
||||||
TypeSemanticClass
|
TypeSemanticClass
|
||||||
TypeSemanticSelected
|
TypeSemanticSelected
|
||||||
TypeSemanticDisabled
|
TypeSemanticEnabled
|
||||||
TypeSnippet
|
|
||||||
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 +112,7 @@ const (
|
|||||||
ClipStack StackKind = iota
|
ClipStack StackKind = iota
|
||||||
TransStack
|
TransStack
|
||||||
PassStack
|
PassStack
|
||||||
|
OpacityStack
|
||||||
_StackKind
|
_StackKind
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,31 +126,24 @@ 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
|
||||||
TypePassLen = 1
|
TypePassLen = 1
|
||||||
TypePopPassLen = 1
|
TypePopPassLen = 1
|
||||||
TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4
|
TypeInputLen = 1
|
||||||
TypeClipboardReadLen = 1
|
TypeKeyInputHintLen = 1 + 1
|
||||||
TypeClipboardWriteLen = 1
|
|
||||||
TypeSourceLen = 1
|
|
||||||
TypeTargetLen = 1
|
|
||||||
TypeOfferLen = 1
|
|
||||||
TypeKeyInputLen = 1 + 1
|
|
||||||
TypeKeyFocusLen = 1 + 1
|
|
||||||
TypeKeySoftKeyboardLen = 1 + 1
|
|
||||||
TypeSaveLen = 1 + 4
|
TypeSaveLen = 1 + 4
|
||||||
TypeLoadLen = 1 + 4
|
TypeLoadLen = 1 + 4
|
||||||
TypeAuxLen = 1
|
TypeAuxLen = 1
|
||||||
TypeClipLen = 1 + 4*4 + 1 + 1
|
TypeClipLen = 1 + 4*4 + 1 + 1
|
||||||
TypePopClipLen = 1
|
TypePopClipLen = 1
|
||||||
TypeProfileLen = 1
|
|
||||||
TypeCursorLen = 2
|
TypeCursorLen = 2
|
||||||
TypePathLen = 8 + 1
|
TypePathLen = 8 + 1
|
||||||
TypeStrokeLen = 1 + 4
|
TypeStrokeLen = 1 + 4
|
||||||
@@ -156,9 +151,7 @@ const (
|
|||||||
TypeSemanticDescLen = 1
|
TypeSemanticDescLen = 1
|
||||||
TypeSemanticClassLen = 2
|
TypeSemanticClassLen = 2
|
||||||
TypeSemanticSelectedLen = 2
|
TypeSemanticSelectedLen = 2
|
||||||
TypeSemanticDisabledLen = 2
|
TypeSemanticEnabledLen = 2
|
||||||
TypeSnippetLen = 1 + 4 + 4
|
|
||||||
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
|
|
||||||
TypeActionInputLen = 1 + 1
|
TypeActionInputLen = 1 + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -183,8 +176,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 +245,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 +262,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 +289,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 +365,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,31 +400,23 @@ 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},
|
||||||
TypeInvalidate: {Size: TypeRedrawLen, NumRefs: 0},
|
TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0},
|
||||||
|
TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0},
|
||||||
TypeImage: {Size: TypeImageLen, NumRefs: 2},
|
TypeImage: {Size: TypeImageLen, NumRefs: 2},
|
||||||
TypePaint: {Size: TypePaintLen, NumRefs: 0},
|
TypePaint: {Size: TypePaintLen, NumRefs: 0},
|
||||||
TypeColor: {Size: TypeColorLen, NumRefs: 0},
|
TypeColor: {Size: TypeColorLen, NumRefs: 0},
|
||||||
TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0},
|
TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0},
|
||||||
TypePass: {Size: TypePassLen, NumRefs: 0},
|
TypePass: {Size: TypePassLen, NumRefs: 0},
|
||||||
TypePopPass: {Size: TypePopPassLen, NumRefs: 0},
|
TypePopPass: {Size: TypePopPassLen, NumRefs: 0},
|
||||||
TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1},
|
TypeInput: {Size: TypeInputLen, NumRefs: 1},
|
||||||
TypeClipboardRead: {Size: TypeClipboardReadLen, NumRefs: 1},
|
TypeKeyInputHint: {Size: TypeKeyInputHintLen, NumRefs: 1},
|
||||||
TypeClipboardWrite: {Size: TypeClipboardWriteLen, NumRefs: 1},
|
|
||||||
TypeSource: {Size: TypeSourceLen, NumRefs: 2},
|
|
||||||
TypeTarget: {Size: TypeTargetLen, NumRefs: 2},
|
|
||||||
TypeOffer: {Size: TypeOfferLen, NumRefs: 3},
|
|
||||||
TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2},
|
|
||||||
TypeKeyFocus: {Size: TypeKeyFocusLen, NumRefs: 1},
|
|
||||||
TypeKeySoftKeyboard: {Size: TypeKeySoftKeyboardLen, NumRefs: 0},
|
|
||||||
TypeSave: {Size: TypeSaveLen, NumRefs: 0},
|
TypeSave: {Size: TypeSaveLen, NumRefs: 0},
|
||||||
TypeLoad: {Size: TypeLoadLen, NumRefs: 0},
|
TypeLoad: {Size: TypeLoadLen, NumRefs: 0},
|
||||||
TypeAux: {Size: TypeAuxLen, NumRefs: 0},
|
TypeAux: {Size: TypeAuxLen, NumRefs: 0},
|
||||||
TypeClip: {Size: TypeClipLen, NumRefs: 0},
|
TypeClip: {Size: TypeClipLen, NumRefs: 0},
|
||||||
TypePopClip: {Size: TypePopClipLen, NumRefs: 0},
|
TypePopClip: {Size: TypePopClipLen, NumRefs: 0},
|
||||||
TypeProfile: {Size: TypeProfileLen, NumRefs: 1},
|
|
||||||
TypeCursor: {Size: TypeCursorLen, NumRefs: 0},
|
TypeCursor: {Size: TypeCursorLen, NumRefs: 0},
|
||||||
TypePath: {Size: TypePathLen, NumRefs: 0},
|
TypePath: {Size: TypePathLen, NumRefs: 0},
|
||||||
TypeStroke: {Size: TypeStrokeLen, NumRefs: 0},
|
TypeStroke: {Size: TypeStrokeLen, NumRefs: 0},
|
||||||
@@ -413,23 +424,21 @@ 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},
|
|
||||||
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,14 +449,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 TypeInvalidate:
|
case TypePushOpacity:
|
||||||
return "Invalidate"
|
return "PushOpacity"
|
||||||
|
case TypePopOpacity:
|
||||||
|
return "PopOpacity"
|
||||||
case TypeImage:
|
case TypeImage:
|
||||||
return "Image"
|
return "Image"
|
||||||
case TypePaint:
|
case TypePaint:
|
||||||
@@ -460,24 +469,10 @@ func (t OpType) String() string {
|
|||||||
return "Pass"
|
return "Pass"
|
||||||
case TypePopPass:
|
case TypePopPass:
|
||||||
return "PopPass"
|
return "PopPass"
|
||||||
case TypePointerInput:
|
case TypeInput:
|
||||||
return "PointerInput"
|
return "Input"
|
||||||
case TypeClipboardRead:
|
case TypeKeyInputHint:
|
||||||
return "ClipboardRead"
|
return "KeyInputHint"
|
||||||
case TypeClipboardWrite:
|
|
||||||
return "ClipboardWrite"
|
|
||||||
case TypeSource:
|
|
||||||
return "Source"
|
|
||||||
case TypeTarget:
|
|
||||||
return "Target"
|
|
||||||
case TypeOffer:
|
|
||||||
return "Offer"
|
|
||||||
case TypeKeyInput:
|
|
||||||
return "KeyInput"
|
|
||||||
case TypeKeyFocus:
|
|
||||||
return "KeyFocus"
|
|
||||||
case TypeKeySoftKeyboard:
|
|
||||||
return "KeySoftKeyboard"
|
|
||||||
case TypeSave:
|
case TypeSave:
|
||||||
return "Save"
|
return "Save"
|
||||||
case TypeLoad:
|
case TypeLoad:
|
||||||
@@ -488,8 +483,6 @@ func (t OpType) String() string {
|
|||||||
return "Clip"
|
return "Clip"
|
||||||
case TypePopClip:
|
case TypePopClip:
|
||||||
return "PopClip"
|
return "PopClip"
|
||||||
case TypeProfile:
|
|
||||||
return "Profile"
|
|
||||||
case TypeCursor:
|
case TypeCursor:
|
||||||
return "Cursor"
|
return "Cursor"
|
||||||
case TypePath:
|
case TypePath:
|
||||||
|
|||||||
+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:])
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-24
@@ -3,35 +3,22 @@
|
|||||||
package clipboard
|
package clipboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gioui.org/internal/ops"
|
"io"
|
||||||
|
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
"gioui.org/op"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event is generated when the clipboard content is requested.
|
// WriteCmd copies Text to the clipboard.
|
||||||
type Event struct {
|
type WriteCmd struct {
|
||||||
Text string
|
Type string
|
||||||
|
Data io.ReadCloser
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadOp requests the text of the clipboard, delivered to
|
// ReadCmd requests the text of the clipboard, delivered to
|
||||||
// the current handler through an Event.
|
// the handler through an [io/transfer.DataEvent].
|
||||||
type ReadOp struct {
|
type ReadCmd struct {
|
||||||
Tag event.Tag
|
Tag event.Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteOp copies Text to the clipboard.
|
func (WriteCmd) ImplementsCommand() {}
|
||||||
type WriteOp struct {
|
func (ReadCmd) ImplementsCommand() {}
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h ReadOp) Add(o *op.Ops) {
|
|
||||||
data := ops.Write1(&o.Internal, ops.TypeClipboardReadLen, h.Tag)
|
|
||||||
data[0] = byte(ops.TypeClipboardRead)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h WriteOp) Add(o *op.Ops) {
|
|
||||||
data := ops.Write1(&o.Internal, ops.TypeClipboardWriteLen, &h.Text)
|
|
||||||
data[0] = byte(ops.TypeClipboardWrite)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Event) ImplementsEvent() {}
|
|
||||||
|
|||||||
+20
-34
@@ -1,41 +1,12 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
/*
|
// Package event contains types for event handling.
|
||||||
Package event contains the types for event handling.
|
|
||||||
|
|
||||||
The Queue interface is the protocol for receiving external events.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
var queue event.Queue = ...
|
|
||||||
|
|
||||||
for _, e := range queue.Events(h) {
|
|
||||||
switch e.(type) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
In general, handlers must be declared before events become
|
|
||||||
available. Other packages such as pointer and key provide
|
|
||||||
the means for declaring handlers for specific event types.
|
|
||||||
|
|
||||||
The following example declares a handler ready for key input:
|
|
||||||
|
|
||||||
import gioui.org/io/key
|
|
||||||
|
|
||||||
ops := new(op.Ops)
|
|
||||||
var h *Handler = ...
|
|
||||||
key.InputOp{Tag: h, Filter: ...}.Add(ops)
|
|
||||||
*/
|
|
||||||
package event
|
package event
|
||||||
|
|
||||||
// Queue maps an event handler key to the events
|
import (
|
||||||
// available to the handler.
|
"gioui.org/internal/ops"
|
||||||
type Queue interface {
|
"gioui.org/op"
|
||||||
// Events returns the available events for an
|
)
|
||||||
// event handler tag.
|
|
||||||
Events(t Tag) []Event
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tag is the stable identifier for an event handler.
|
// Tag is the stable identifier for an event handler.
|
||||||
// For a handler h, the tag is typically &h.
|
// For a handler h, the tag is typically &h.
|
||||||
@@ -45,3 +16,18 @@ type Tag interface{}
|
|||||||
type Event interface {
|
type Event interface {
|
||||||
ImplementsEvent()
|
ImplementsEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter represents a filter for [Event] types.
|
||||||
|
type Filter interface {
|
||||||
|
ImplementsFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Op declares a tag for input routing at the current transformation
|
||||||
|
// and clip area hierarchy. It panics if tag is nil.
|
||||||
|
func Op(o *op.Ops, tag Tag) {
|
||||||
|
if tag == nil {
|
||||||
|
panic("Tag must be non-nil")
|
||||||
|
}
|
||||||
|
data := ops.Write1(&o.Internal, ops.TypeInputLen, tag)
|
||||||
|
data[0] = byte(ops.TypeInput)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"gioui.org/io/clipboard"
|
||||||
|
"gioui.org/io/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
// clipboardState contains the state for clipboard event routing.
|
||||||
|
type clipboardState struct {
|
||||||
|
receivers []event.Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
type clipboardQueue struct {
|
||||||
|
// request avoid read clipboard every frame while waiting.
|
||||||
|
requested bool
|
||||||
|
mime string
|
||||||
|
text []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteClipboard returns the most recent data to be copied
|
||||||
|
// to the clipboard, if any.
|
||||||
|
func (q *clipboardQueue) WriteClipboard() (mime string, content []byte, ok bool) {
|
||||||
|
if q.text == nil {
|
||||||
|
return "", nil, false
|
||||||
|
}
|
||||||
|
content = q.text
|
||||||
|
q.text = nil
|
||||||
|
return q.mime, content, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClipboardRequested reports if any new handler is waiting
|
||||||
|
// to read the clipboard.
|
||||||
|
func (q *clipboardQueue) ClipboardRequested(state clipboardState) bool {
|
||||||
|
req := len(state.receivers) > 0 && q.requested
|
||||||
|
q.requested = false
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *clipboardQueue) Push(state clipboardState, e event.Event) (clipboardState, []taggedEvent) {
|
||||||
|
var evts []taggedEvent
|
||||||
|
for _, r := range state.receivers {
|
||||||
|
evts = append(evts, taggedEvent{tag: r, event: e})
|
||||||
|
}
|
||||||
|
state.receivers = nil
|
||||||
|
return state, evts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) {
|
||||||
|
defer req.Data.Close()
|
||||||
|
content, err := io.ReadAll(req.Data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q.mime = req.Type
|
||||||
|
q.text = content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *clipboardQueue) ProcessReadClipboard(state clipboardState, tag event.Tag) clipboardState {
|
||||||
|
for _, k := range state.receivers {
|
||||||
|
if k == tag {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n := len(state.receivers)
|
||||||
|
state.receivers = append(state.receivers[:n:n], tag)
|
||||||
|
q.requested = true
|
||||||
|
return state
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gioui.org/io/clipboard"
|
||||||
|
"gioui.org/io/transfer"
|
||||||
|
"gioui.org/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClipboardDuplicateEvent(t *testing.T) {
|
||||||
|
ops, r, handlers := new(op.Ops), new(Router), make([]int, 2)
|
||||||
|
|
||||||
|
// Both must receive the event once.
|
||||||
|
r.Source().Execute(clipboard.ReadCmd{Tag: &handlers[0]})
|
||||||
|
r.Source().Execute(clipboard.ReadCmd{Tag: &handlers[1]})
|
||||||
|
|
||||||
|
event := transfer.DataEvent{
|
||||||
|
Type: "application/text",
|
||||||
|
Open: func() io.ReadCloser {
|
||||||
|
return io.NopCloser(strings.NewReader("Test"))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r.Queue(event)
|
||||||
|
for i := range handlers {
|
||||||
|
f := transfer.TargetFilter{Target: &handlers[i], Type: "application/text"}
|
||||||
|
assertEventTypeSequence(t, events(r, -1, f), transfer.DataEvent{})
|
||||||
|
}
|
||||||
|
assertClipboardReadCmd(t, r, 0)
|
||||||
|
|
||||||
|
r.Source().Execute(clipboard.ReadCmd{Tag: &handlers[0]})
|
||||||
|
|
||||||
|
r.Frame(ops)
|
||||||
|
// No ClipboardEvent sent
|
||||||
|
assertClipboardReadCmd(t, r, 1)
|
||||||
|
for i := range handlers {
|
||||||
|
f := transfer.TargetFilter{Target: &handlers[i]}
|
||||||
|
assertEventTypeSequence(t, events(r, -1, f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueueProcessReadClipboard(t *testing.T) {
|
||||||
|
ops, r, handler := new(op.Ops), new(Router), make([]int, 2)
|
||||||
|
|
||||||
|
// Request read
|
||||||
|
r.Source().Execute(clipboard.ReadCmd{Tag: &handler[0]})
|
||||||
|
|
||||||
|
assertClipboardReadCmd(t, r, 1)
|
||||||
|
ops.Reset()
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
// No ReadCmd
|
||||||
|
// One receiver must still wait for response
|
||||||
|
|
||||||
|
r.Frame(ops)
|
||||||
|
assertClipboardReadDuplicated(t, r, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the clipboard event
|
||||||
|
event := transfer.DataEvent{
|
||||||
|
Type: "application/text",
|
||||||
|
Open: func() io.ReadCloser {
|
||||||
|
return io.NopCloser(strings.NewReader("Text 2"))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r.Queue(event)
|
||||||
|
assertEventTypeSequence(t, events(r, -1, transfer.TargetFilter{Target: &handler[0], Type: "application/text"}), transfer.DataEvent{})
|
||||||
|
assertClipboardReadCmd(t, r, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueueProcessWriteClipboard(t *testing.T) {
|
||||||
|
r := new(Router)
|
||||||
|
|
||||||
|
const mime = "application/text"
|
||||||
|
r.Source().Execute(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 1"))})
|
||||||
|
|
||||||
|
assertClipboardWriteCmd(t, r, mime, "Write 1")
|
||||||
|
assertClipboardWriteCmd(t, r, "", "")
|
||||||
|
|
||||||
|
r.Source().Execute(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 2"))})
|
||||||
|
|
||||||
|
assertClipboardReadCmd(t, r, 0)
|
||||||
|
assertClipboardWriteCmd(t, r, mime, "Write 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertClipboardReadCmd(t *testing.T, router *Router, expected int) {
|
||||||
|
t.Helper()
|
||||||
|
if got := len(router.state().receivers); got != expected {
|
||||||
|
t.Errorf("unexpected %d receivers, got %d", expected, got)
|
||||||
|
}
|
||||||
|
if router.ClipboardRequested() != (expected > 0) {
|
||||||
|
t.Error("missing requests")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertClipboardReadDuplicated(t *testing.T, router *Router, expected int) {
|
||||||
|
t.Helper()
|
||||||
|
if len(router.state().receivers) != expected {
|
||||||
|
t.Error("receivers removed")
|
||||||
|
}
|
||||||
|
if router.ClipboardRequested() != false {
|
||||||
|
t.Error("duplicated requests")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertClipboardWriteCmd(t *testing.T, router *Router, mimeExp, expected string) {
|
||||||
|
t.Helper()
|
||||||
|
if (router.cqueue.text != nil) != (expected != "") {
|
||||||
|
t.Error("text not defined")
|
||||||
|
}
|
||||||
|
mime, text, ok := router.cqueue.WriteClipboard()
|
||||||
|
if ok != (expected != "") {
|
||||||
|
t.Error("duplicated requests")
|
||||||
|
}
|
||||||
|
if string(mime) != mimeExp {
|
||||||
|
t.Errorf("got MIME type %s, expected %s", mime, mimeExp)
|
||||||
|
}
|
||||||
|
if string(text) != expected {
|
||||||
|
t.Errorf("got text %s, expected %s", text, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package input implements input routing and tracking of interface
|
||||||
|
state for a window.
|
||||||
|
|
||||||
|
The [Source] is the interface between the window and the widgets
|
||||||
|
of a user interface and is exposed by [gioui.org/app.FrameEvent]
|
||||||
|
received from windows.
|
||||||
|
|
||||||
|
The [Router] is used by [gioui.org/app.Window] to track window state and route
|
||||||
|
events from the platform to event handlers. It is otherwise only
|
||||||
|
useful for using Gio with external window implementations.
|
||||||
|
*/
|
||||||
|
package input
|
||||||
+364
@@ -0,0 +1,364 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"gioui.org/f32"
|
||||||
|
"gioui.org/io/event"
|
||||||
|
"gioui.org/io/key"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EditorState represents the state of an editor needed by input handlers.
|
||||||
|
type EditorState struct {
|
||||||
|
Selection struct {
|
||||||
|
Transform f32.Affine2D
|
||||||
|
key.Range
|
||||||
|
key.Caret
|
||||||
|
}
|
||||||
|
Snippet key.Snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextInputState uint8
|
||||||
|
|
||||||
|
type keyQueue struct {
|
||||||
|
order []event.Tag
|
||||||
|
dirOrder []dirFocusEntry
|
||||||
|
hint key.InputHint
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyState is the input state related to key events.
|
||||||
|
type keyState struct {
|
||||||
|
focus event.Tag
|
||||||
|
state TextInputState
|
||||||
|
content EditorState
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyHandler struct {
|
||||||
|
// visible will be true if the InputOp is present
|
||||||
|
// in the current frame.
|
||||||
|
visible bool
|
||||||
|
// reset tracks whether the handler has seen a
|
||||||
|
// focus reset.
|
||||||
|
reset bool
|
||||||
|
hint key.InputHint
|
||||||
|
orderPlusOne int
|
||||||
|
dirOrder int
|
||||||
|
trans f32.Affine2D
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyFilter []key.Filter
|
||||||
|
|
||||||
|
type dirFocusEntry struct {
|
||||||
|
tag event.Tag
|
||||||
|
row int
|
||||||
|
area int
|
||||||
|
bounds image.Rectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TextInputKeep TextInputState = iota
|
||||||
|
TextInputClose
|
||||||
|
TextInputOpen
|
||||||
|
)
|
||||||
|
|
||||||
|
func (k *keyHandler) inputHint(hint key.InputHint) {
|
||||||
|
k.hint = hint
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputState returns the input state and returns a state
|
||||||
|
// reset to [TextInputKeep].
|
||||||
|
func (s keyState) InputState() (keyState, TextInputState) {
|
||||||
|
state := s.state
|
||||||
|
s.state = TextInputKeep
|
||||||
|
return s, state
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputHint returns the input hint from the focused handler and whether it was
|
||||||
|
// changed since the last call.
|
||||||
|
func (q *keyQueue) InputHint(handlers map[event.Tag]*handler, state keyState) (key.InputHint, bool) {
|
||||||
|
focused, ok := handlers[state.focus]
|
||||||
|
if !ok {
|
||||||
|
return q.hint, false
|
||||||
|
}
|
||||||
|
old := q.hint
|
||||||
|
q.hint = focused.key.hint
|
||||||
|
return q.hint, old != q.hint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *keyHandler) Reset() {
|
||||||
|
k.visible = false
|
||||||
|
k.orderPlusOne = 0
|
||||||
|
k.hint = key.HintAny
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *keyQueue) Reset() {
|
||||||
|
q.order = q.order[:0]
|
||||||
|
q.dirOrder = q.dirOrder[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *keyHandler) ResetEvent() (event.Event, bool) {
|
||||||
|
if k.reset {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
k.reset = true
|
||||||
|
return key.FocusEvent{Focus: false}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *keyQueue) Frame(handlers map[event.Tag]*handler, state keyState) keyState {
|
||||||
|
if state.focus != nil {
|
||||||
|
if h, ok := handlers[state.focus]; !ok || !h.filter.focusable || !h.key.visible {
|
||||||
|
// Remove focus from the handler that is no longer focusable.
|
||||||
|
state.focus = nil
|
||||||
|
state.state = TextInputClose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q.updateFocusLayout(handlers)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateFocusLayout partitions input handlers handlers into rows
|
||||||
|
// for directional focus moves.
|
||||||
|
//
|
||||||
|
// The approach is greedy: pick the topmost handler and create a row
|
||||||
|
// containing it. Then, extend the handler bounds to a horizontal beam
|
||||||
|
// and add to the row every handler whose center intersect it. Repeat
|
||||||
|
// until no handlers remain.
|
||||||
|
func (q *keyQueue) updateFocusLayout(handlers map[event.Tag]*handler) {
|
||||||
|
order := q.dirOrder
|
||||||
|
// Sort by ascending y position.
|
||||||
|
sort.SliceStable(order, func(i, j int) bool {
|
||||||
|
return order[i].bounds.Min.Y < order[j].bounds.Min.Y
|
||||||
|
})
|
||||||
|
row := 0
|
||||||
|
for len(order) > 0 {
|
||||||
|
h := &order[0]
|
||||||
|
h.row = row
|
||||||
|
bottom := h.bounds.Max.Y
|
||||||
|
end := 1
|
||||||
|
for ; end < len(order); end++ {
|
||||||
|
h := &order[end]
|
||||||
|
center := (h.bounds.Min.Y + h.bounds.Max.Y) / 2
|
||||||
|
if center > bottom {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h.row = row
|
||||||
|
}
|
||||||
|
// Sort row by ascending x position.
|
||||||
|
sort.SliceStable(order[:end], func(i, j int) bool {
|
||||||
|
return order[i].bounds.Min.X < order[j].bounds.Min.X
|
||||||
|
})
|
||||||
|
order = order[end:]
|
||||||
|
row++
|
||||||
|
}
|
||||||
|
for i, o := range q.dirOrder {
|
||||||
|
handlers[o.tag].key.dirOrder = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveFocus attempts to move the focus in the direction of dir.
|
||||||
|
func (q *keyQueue) MoveFocus(handlers map[event.Tag]*handler, state keyState, dir key.FocusDirection) (keyState, []taggedEvent) {
|
||||||
|
if len(q.dirOrder) == 0 {
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
order := 0
|
||||||
|
if state.focus != nil {
|
||||||
|
order = handlers[state.focus].key.dirOrder
|
||||||
|
}
|
||||||
|
focus := q.dirOrder[order]
|
||||||
|
switch dir {
|
||||||
|
case key.FocusForward, key.FocusBackward:
|
||||||
|
if len(q.order) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
order := 0
|
||||||
|
if dir == key.FocusBackward {
|
||||||
|
order = -1
|
||||||
|
}
|
||||||
|
if state.focus != nil {
|
||||||
|
order = handlers[state.focus].key.orderPlusOne - 1
|
||||||
|
if dir == key.FocusForward {
|
||||||
|
order++
|
||||||
|
} else {
|
||||||
|
order--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
order = (order + len(q.order)) % len(q.order)
|
||||||
|
return q.Focus(handlers, state, q.order[order])
|
||||||
|
case key.FocusRight, key.FocusLeft:
|
||||||
|
next := order
|
||||||
|
if state.focus != nil {
|
||||||
|
next = order + 1
|
||||||
|
if dir == key.FocusLeft {
|
||||||
|
next = order - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if 0 <= next && next < len(q.dirOrder) {
|
||||||
|
newFocus := q.dirOrder[next]
|
||||||
|
if newFocus.row == focus.row {
|
||||||
|
return q.Focus(handlers, state, newFocus.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case key.FocusUp, key.FocusDown:
|
||||||
|
delta := +1
|
||||||
|
if dir == key.FocusUp {
|
||||||
|
delta = -1
|
||||||
|
}
|
||||||
|
nextRow := 0
|
||||||
|
if state.focus != nil {
|
||||||
|
nextRow = focus.row + delta
|
||||||
|
}
|
||||||
|
var closest event.Tag
|
||||||
|
dist := int(1e6)
|
||||||
|
center := (focus.bounds.Min.X + focus.bounds.Max.X) / 2
|
||||||
|
loop:
|
||||||
|
for 0 <= order && order < len(q.dirOrder) {
|
||||||
|
next := q.dirOrder[order]
|
||||||
|
switch next.row {
|
||||||
|
case nextRow:
|
||||||
|
nextCenter := (next.bounds.Min.X + next.bounds.Max.X) / 2
|
||||||
|
d := center - nextCenter
|
||||||
|
if d < 0 {
|
||||||
|
d = -d
|
||||||
|
}
|
||||||
|
if d > dist {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
dist = d
|
||||||
|
closest = next.tag
|
||||||
|
case nextRow + delta:
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
order += delta
|
||||||
|
}
|
||||||
|
if closest != nil {
|
||||||
|
return q.Focus(handlers, state, closest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *keyQueue) BoundsFor(k *keyHandler) image.Rectangle {
|
||||||
|
order := k.dirOrder
|
||||||
|
return q.dirOrder[order].bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *keyQueue) AreaFor(k *keyHandler) int {
|
||||||
|
order := k.dirOrder
|
||||||
|
return q.dirOrder[order].area
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *keyFilter) Matches(focus event.Tag, e key.Event, system bool) bool {
|
||||||
|
for _, f := range *k {
|
||||||
|
if keyFilterMatch(focus, f, e, system) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyFilterMatch(focus event.Tag, f key.Filter, e key.Event, system bool) bool {
|
||||||
|
if f.Focus != nil && f.Focus != focus {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (f.Name != "" || system) && f.Name != e.Name {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if e.Modifiers&f.Required != f.Required {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if e.Modifiers&^(f.Required|f.Optional) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *keyQueue) Focus(handlers map[event.Tag]*handler, state keyState, focus event.Tag) (keyState, []taggedEvent) {
|
||||||
|
if focus == state.focus {
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
state.content = EditorState{}
|
||||||
|
var evts []taggedEvent
|
||||||
|
if state.focus != nil {
|
||||||
|
evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: false}})
|
||||||
|
}
|
||||||
|
state.focus = focus
|
||||||
|
if state.focus != nil {
|
||||||
|
evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: true}})
|
||||||
|
}
|
||||||
|
if state.focus == nil || state.state == TextInputKeep {
|
||||||
|
state.state = TextInputClose
|
||||||
|
}
|
||||||
|
return state, evts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s keyState) softKeyboard(show bool) keyState {
|
||||||
|
if show {
|
||||||
|
s.state = TextInputOpen
|
||||||
|
} else {
|
||||||
|
s.state = TextInputClose
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *keyFilter) Add(f key.Filter) {
|
||||||
|
for _, f2 := range *k {
|
||||||
|
if f == f2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*k = append(*k, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *keyFilter) Merge(k2 keyFilter) {
|
||||||
|
*k = append(*k, k2...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *keyQueue) inputOp(tag event.Tag, state *keyHandler, t f32.Affine2D, area int, bounds image.Rectangle) {
|
||||||
|
state.visible = true
|
||||||
|
if state.orderPlusOne == 0 {
|
||||||
|
state.orderPlusOne = len(q.order) + 1
|
||||||
|
q.order = append(q.order, tag)
|
||||||
|
q.dirOrder = append(q.dirOrder, dirFocusEntry{tag: tag, area: area, bounds: bounds})
|
||||||
|
}
|
||||||
|
state.trans = t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *keyQueue) setSelection(state keyState, req key.SelectionCmd) keyState {
|
||||||
|
if req.Tag != state.focus {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
state.content.Selection.Range = req.Range
|
||||||
|
state.content.Selection.Caret = req.Caret
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *keyQueue) editorState(handlers map[event.Tag]*handler, state keyState) EditorState {
|
||||||
|
s := state.content
|
||||||
|
if f := state.focus; f != nil {
|
||||||
|
s.Selection.Transform = handlers[f].key.trans
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *keyQueue) setSnippet(state keyState, req key.SnippetCmd) keyState {
|
||||||
|
if req.Tag == state.focus {
|
||||||
|
state.content.Snippet = req.Snippet
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TextInputState) String() string {
|
||||||
|
switch t {
|
||||||
|
case TextInputKeep:
|
||||||
|
return "Keep"
|
||||||
|
case TextInputClose:
|
||||||
|
return "Close"
|
||||||
|
case TextInputOpen:
|
||||||
|
return "Open"
|
||||||
|
default:
|
||||||
|
panic("unexpected value")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gioui.org/f32"
|
||||||
|
"gioui.org/io/event"
|
||||||
|
"gioui.org/io/key"
|
||||||
|
"gioui.org/io/pointer"
|
||||||
|
"gioui.org/op"
|
||||||
|
"gioui.org/op/clip"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAllMatchKeyFilter(t *testing.T) {
|
||||||
|
r := new(Router)
|
||||||
|
r.Event(key.Filter{})
|
||||||
|
ke := key.Event{Name: "A"}
|
||||||
|
r.Queue(ke)
|
||||||
|
// Catch-all gets all non-system events.
|
||||||
|
assertEventSequence(t, events(r, -1, key.Filter{}), ke)
|
||||||
|
|
||||||
|
r = new(Router)
|
||||||
|
r.Event(key.Filter{Name: "A"})
|
||||||
|
r.Queue(SystemEvent{ke})
|
||||||
|
if _, handled := r.WakeupTime(); !handled {
|
||||||
|
t.Errorf("system event was unexpectedly ignored")
|
||||||
|
}
|
||||||
|
// Only specific filters match system events.
|
||||||
|
assertEventSequence(t, events(r, -1, key.Filter{Name: "A"}), ke)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputHint(t *testing.T) {
|
||||||
|
r := new(Router)
|
||||||
|
if hint, changed := r.TextInputHint(); hint != key.HintAny || changed {
|
||||||
|
t.Fatal("unexpected hint")
|
||||||
|
}
|
||||||
|
ops := new(op.Ops)
|
||||||
|
h := new(int)
|
||||||
|
key.InputHintOp{Tag: h, Hint: key.HintEmail}.Add(ops)
|
||||||
|
r.Frame(ops)
|
||||||
|
if hint, changed := r.TextInputHint(); hint != key.HintAny || changed {
|
||||||
|
t.Fatal("unexpected hint")
|
||||||
|
}
|
||||||
|
r.Source().Execute(key.FocusCmd{Tag: h})
|
||||||
|
if hint, changed := r.TextInputHint(); hint != key.HintEmail || !changed {
|
||||||
|
t.Fatal("unexpected hint")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeferred(t *testing.T) {
|
||||||
|
r := new(Router)
|
||||||
|
h := new(int)
|
||||||
|
f := []event.Filter{
|
||||||
|
key.FocusFilter{Target: h},
|
||||||
|
key.Filter{Name: "A"},
|
||||||
|
}
|
||||||
|
// Provoke deferring by exhausting events for h.
|
||||||
|
events(r, -1, f...)
|
||||||
|
r.Source().Execute(key.FocusCmd{Tag: h})
|
||||||
|
ke := key.Event{Name: "A"}
|
||||||
|
r.Queue(ke)
|
||||||
|
// All events are deferred at this point.
|
||||||
|
assertEventSequence(t, events(r, -1, f...))
|
||||||
|
r.Frame(new(op.Ops))
|
||||||
|
// But delivered after a frame.
|
||||||
|
assertEventSequence(t, events(r, -1, f...), key.FocusEvent{Focus: true}, ke)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputWakeup(t *testing.T) {
|
||||||
|
handler := new(int)
|
||||||
|
var ops op.Ops
|
||||||
|
// InputOps shouldn't trigger redraws.
|
||||||
|
event.Op(&ops, handler)
|
||||||
|
|
||||||
|
var r Router
|
||||||
|
// Reset events shouldn't either.
|
||||||
|
evts := events(&r, -1, key.FocusFilter{Target: new(int)}, key.Filter{Name: "A"})
|
||||||
|
assertEventSequence(t, evts, key.FocusEvent{Focus: false})
|
||||||
|
r.Frame(&ops)
|
||||||
|
if _, wake := r.WakeupTime(); wake {
|
||||||
|
t.Errorf("InputOp or the resetting FocusEvent triggered a wakeup")
|
||||||
|
}
|
||||||
|
// And neither does events that don't match anything.
|
||||||
|
r.Queue(key.SnippetEvent{})
|
||||||
|
if _, handled := r.WakeupTime(); handled {
|
||||||
|
t.Errorf("a not-matching event triggered a wakeup")
|
||||||
|
}
|
||||||
|
// However, events that does match should trigger wakeup.
|
||||||
|
r.Queue(key.Event{Name: "A"})
|
||||||
|
if _, handled := r.WakeupTime(); !handled {
|
||||||
|
t.Errorf("a key.Event didn't trigger redraw")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyMultiples(t *testing.T) {
|
||||||
|
handlers := make([]int, 3)
|
||||||
|
r := new(Router)
|
||||||
|
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
|
||||||
|
for i := range handlers {
|
||||||
|
assertEventSequence(t, events(r, 1, key.FocusFilter{Target: &handlers[i]}), key.FocusEvent{Focus: false})
|
||||||
|
}
|
||||||
|
r.Source().Execute(key.FocusCmd{Tag: &handlers[2]})
|
||||||
|
assertEventSequence(t, events(r, -1, key.FocusFilter{Target: &handlers[2]}), key.FocusEvent{Focus: true})
|
||||||
|
assertFocus(t, r, &handlers[2])
|
||||||
|
|
||||||
|
assertKeyboard(t, r, TextInputOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeySoftKeyboardNoFocus(t *testing.T) {
|
||||||
|
r := new(Router)
|
||||||
|
|
||||||
|
// It's possible to open the keyboard
|
||||||
|
// without any active focus:
|
||||||
|
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
|
||||||
|
|
||||||
|
assertFocus(t, r, nil)
|
||||||
|
assertKeyboard(t, r, TextInputOpen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyRemoveFocus(t *testing.T) {
|
||||||
|
handlers := make([]int, 2)
|
||||||
|
r := new(Router)
|
||||||
|
|
||||||
|
filters := func(h event.Tag) []event.Filter {
|
||||||
|
return []event.Filter{
|
||||||
|
key.FocusFilter{Target: h},
|
||||||
|
key.Filter{Focus: h, Name: key.NameTab, Required: key.ModShortcut},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var all []event.Filter
|
||||||
|
for i := range handlers {
|
||||||
|
all = append(all, filters(&handlers[i])...)
|
||||||
|
}
|
||||||
|
assertEventSequence(t, events(r, len(handlers), all...), key.FocusEvent{}, key.FocusEvent{})
|
||||||
|
r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
|
||||||
|
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
|
||||||
|
|
||||||
|
evt := key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press}
|
||||||
|
r.Queue(evt)
|
||||||
|
|
||||||
|
assertEventSequence(t, events(r, 2, filters(&handlers[0])...), key.FocusEvent{Focus: true}, evt)
|
||||||
|
assertFocus(t, r, &handlers[0])
|
||||||
|
assertKeyboard(t, r, TextInputOpen)
|
||||||
|
|
||||||
|
// Frame removes focus from tags that don't filter for focus events nor mentioned in an InputOp.
|
||||||
|
r.Source().Execute(key.FocusCmd{Tag: new(int)})
|
||||||
|
r.Frame(new(op.Ops))
|
||||||
|
|
||||||
|
assertEventSequence(t, events(r, -1, filters(&handlers[1])...))
|
||||||
|
assertFocus(t, r, nil)
|
||||||
|
assertKeyboard(t, r, TextInputClose)
|
||||||
|
|
||||||
|
// Set focus to InputOp which already
|
||||||
|
// exists in the previous frame:
|
||||||
|
r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
|
||||||
|
assertFocus(t, r, &handlers[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyFocusedInvisible(t *testing.T) {
|
||||||
|
handlers := make([]int, 2)
|
||||||
|
ops := new(op.Ops)
|
||||||
|
r := new(Router)
|
||||||
|
|
||||||
|
for i := range handlers {
|
||||||
|
assertEventSequence(t, events(r, 1, key.FocusFilter{Target: &handlers[i]}), key.FocusEvent{Focus: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new InputOp with focus:
|
||||||
|
r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
|
||||||
|
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
|
||||||
|
|
||||||
|
assertEventSequence(t, events(r, 1, key.FocusFilter{Target: &handlers[0]}), key.FocusEvent{Focus: true})
|
||||||
|
assertFocus(t, r, &handlers[0])
|
||||||
|
assertKeyboard(t, r, TextInputOpen)
|
||||||
|
|
||||||
|
// Frame will clear the focus because the handler is not visible.
|
||||||
|
r.Frame(ops)
|
||||||
|
|
||||||
|
for i := range handlers {
|
||||||
|
assertEventSequence(t, events(r, -1, key.FocusFilter{Target: &handlers[i]}))
|
||||||
|
}
|
||||||
|
assertFocus(t, r, nil)
|
||||||
|
assertKeyboard(t, r, TextInputClose)
|
||||||
|
|
||||||
|
r.Frame(ops)
|
||||||
|
r.Frame(ops)
|
||||||
|
|
||||||
|
ops.Reset()
|
||||||
|
|
||||||
|
// Respawn the first element:
|
||||||
|
// It must receive one `Event{Focus: false}`.
|
||||||
|
event.Op(ops, &handlers[0])
|
||||||
|
|
||||||
|
assertEventSequence(t, events(r, -1, key.FocusFilter{Target: &handlers[0]}), key.FocusEvent{Focus: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoOps(t *testing.T) {
|
||||||
|
r := new(Router)
|
||||||
|
r.Frame(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirectionalFocus(t *testing.T) {
|
||||||
|
ops := new(op.Ops)
|
||||||
|
r := new(Router)
|
||||||
|
handlers := []image.Rectangle{
|
||||||
|
image.Rect(10, 10, 50, 50),
|
||||||
|
image.Rect(50, 20, 100, 80),
|
||||||
|
image.Rect(20, 26, 60, 80),
|
||||||
|
image.Rect(10, 60, 50, 100),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, bounds := range handlers {
|
||||||
|
cl := clip.Rect(bounds).Push(ops)
|
||||||
|
event.Op(ops, &handlers[i])
|
||||||
|
cl.Pop()
|
||||||
|
events(r, -1, key.FocusFilter{Target: &handlers[i]})
|
||||||
|
}
|
||||||
|
r.Frame(ops)
|
||||||
|
|
||||||
|
r.MoveFocus(key.FocusLeft)
|
||||||
|
assertFocus(t, r, &handlers[0])
|
||||||
|
r.MoveFocus(key.FocusLeft)
|
||||||
|
assertFocus(t, r, &handlers[0])
|
||||||
|
r.MoveFocus(key.FocusRight)
|
||||||
|
assertFocus(t, r, &handlers[1])
|
||||||
|
r.MoveFocus(key.FocusRight)
|
||||||
|
assertFocus(t, r, &handlers[1])
|
||||||
|
r.MoveFocus(key.FocusDown)
|
||||||
|
assertFocus(t, r, &handlers[2])
|
||||||
|
r.MoveFocus(key.FocusDown)
|
||||||
|
assertFocus(t, r, &handlers[2])
|
||||||
|
r.MoveFocus(key.FocusLeft)
|
||||||
|
assertFocus(t, r, &handlers[3])
|
||||||
|
r.MoveFocus(key.FocusUp)
|
||||||
|
assertFocus(t, r, &handlers[0])
|
||||||
|
|
||||||
|
r.MoveFocus(key.FocusForward)
|
||||||
|
assertFocus(t, r, &handlers[1])
|
||||||
|
r.MoveFocus(key.FocusBackward)
|
||||||
|
assertFocus(t, r, &handlers[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFocusScroll(t *testing.T) {
|
||||||
|
ops := new(op.Ops)
|
||||||
|
r := new(Router)
|
||||||
|
h := new(int)
|
||||||
|
|
||||||
|
filters := []event.Filter{
|
||||||
|
key.FocusFilter{Target: h},
|
||||||
|
pointer.Filter{
|
||||||
|
Target: h,
|
||||||
|
Kinds: pointer.Scroll,
|
||||||
|
ScrollBounds: image.Rect(-100, -100, 100, 100),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
events(r, -1, filters...)
|
||||||
|
parent := clip.Rect(image.Rect(1, 1, 14, 39)).Push(ops)
|
||||||
|
cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops)
|
||||||
|
event.Op(ops, h)
|
||||||
|
// Test that h is scrolled even if behind another handler.
|
||||||
|
event.Op(ops, new(int))
|
||||||
|
cl.Pop()
|
||||||
|
parent.Pop()
|
||||||
|
r.Frame(ops)
|
||||||
|
|
||||||
|
r.MoveFocus(key.FocusLeft)
|
||||||
|
r.RevealFocus(image.Rect(0, 0, 15, 40))
|
||||||
|
evts := events(r, -1, filters...)
|
||||||
|
assertScrollEvent(t, evts[len(evts)-1], f32.Pt(6, -9))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFocusClick(t *testing.T) {
|
||||||
|
ops := new(op.Ops)
|
||||||
|
r := new(Router)
|
||||||
|
h := new(int)
|
||||||
|
|
||||||
|
filters := []event.Filter{
|
||||||
|
key.FocusFilter{Target: h},
|
||||||
|
pointer.Filter{
|
||||||
|
Target: h,
|
||||||
|
Kinds: pointer.Press | pointer.Release | pointer.Cancel,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assertEventPointerTypeSequence(t, events(r, -1, filters...), pointer.Cancel)
|
||||||
|
cl := clip.Rect(image.Rect(0, 0, 10, 10)).Push(ops)
|
||||||
|
event.Op(ops, h)
|
||||||
|
cl.Pop()
|
||||||
|
r.Frame(ops)
|
||||||
|
|
||||||
|
r.MoveFocus(key.FocusLeft)
|
||||||
|
r.ClickFocus()
|
||||||
|
|
||||||
|
assertEventPointerTypeSequence(t, events(r, -1, filters...), pointer.Press, pointer.Release)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoFocus(t *testing.T) {
|
||||||
|
r := new(Router)
|
||||||
|
r.MoveFocus(key.FocusForward)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyRouting(t *testing.T) {
|
||||||
|
r := new(Router)
|
||||||
|
h := new(int)
|
||||||
|
A, B := key.Event{Name: "A"}, key.Event{Name: "B"}
|
||||||
|
// Register filters.
|
||||||
|
events(r, -1, key.Filter{Name: "A"}, key.Filter{Name: "B"})
|
||||||
|
r.Frame(new(op.Ops))
|
||||||
|
r.Queue(A, B)
|
||||||
|
// The handler is not focused, so only B is delivered.
|
||||||
|
assertEventSequence(t, events(r, -1, key.Filter{Focus: h, Name: "A"}, key.Filter{Name: "B"}), B)
|
||||||
|
r.Source().Execute(key.FocusCmd{Tag: h})
|
||||||
|
// A is delivered to the focused handler.
|
||||||
|
assertEventSequence(t, events(r, -1, key.Filter{Focus: h, Name: "A"}, key.Filter{Name: "B"}), A)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertFocus(t *testing.T, router *Router, expected event.Tag) {
|
||||||
|
t.Helper()
|
||||||
|
if !router.Source().Focused(expected) {
|
||||||
|
t.Errorf("expected %v to be focused", expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertKeyboard(t *testing.T, router *Router, expected TextInputState) {
|
||||||
|
t.Helper()
|
||||||
|
if got := router.state().state; got != expected {
|
||||||
|
t.Errorf("expected %v keyboard, got %v", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
package router
|
package input
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
f32internal "gioui.org/internal/f32"
|
f32internal "gioui.org/internal/f32"
|
||||||
"gioui.org/internal/ops"
|
"gioui.org/internal/ops"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/key"
|
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
"gioui.org/io/semantic"
|
"gioui.org/io/semantic"
|
||||||
"gioui.org/io/system"
|
"gioui.org/io/system"
|
||||||
@@ -18,14 +17,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type pointerQueue struct {
|
type pointerQueue struct {
|
||||||
hitTree []hitNode
|
hitTree []hitNode
|
||||||
areas []areaNode
|
areas []areaNode
|
||||||
cursor pointer.Cursor
|
|
||||||
handlers map[event.Tag]*pointerHandler
|
|
||||||
pointers []pointerInfo
|
|
||||||
transfers []io.ReadCloser // pending data transfers
|
|
||||||
|
|
||||||
scratch []event.Tag
|
|
||||||
|
|
||||||
semantic struct {
|
semantic struct {
|
||||||
idsAssigned bool
|
idsAssigned bool
|
||||||
@@ -43,10 +36,15 @@ type hitNode struct {
|
|||||||
|
|
||||||
// For handler nodes.
|
// For handler nodes.
|
||||||
tag event.Tag
|
tag event.Tag
|
||||||
ktag event.Tag
|
|
||||||
pass bool
|
pass bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pointerState is the input state related to pointer events.
|
||||||
|
type pointerState struct {
|
||||||
|
cursor pointer.Cursor
|
||||||
|
pointers []pointerInfo
|
||||||
|
}
|
||||||
|
|
||||||
type pointerInfo struct {
|
type pointerInfo struct {
|
||||||
id pointer.ID
|
id pointer.ID
|
||||||
pressed bool
|
pressed bool
|
||||||
@@ -63,17 +61,21 @@ type pointerInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type pointerHandler struct {
|
type pointerHandler struct {
|
||||||
area int
|
// areaPlusOne is the index into the list of pointerQueue.areas, plus 1.
|
||||||
active bool
|
areaPlusOne int
|
||||||
wantsGrab bool
|
// setup tracks whether the handler has received
|
||||||
types pointer.Type
|
// the pointer.Cancel event that resets its state.
|
||||||
|
setup bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// pointerFilter represents the union of a set of pointer filters.
|
||||||
|
type pointerFilter struct {
|
||||||
|
kinds pointer.Kind
|
||||||
// min and max horizontal/vertical scroll
|
// min and max horizontal/vertical scroll
|
||||||
scrollRange image.Rectangle
|
scrollRange image.Rectangle
|
||||||
|
|
||||||
sourceMimes []string
|
sourceMimes []string
|
||||||
targetMimes []string
|
targetMimes []string
|
||||||
offeredMime string
|
|
||||||
data io.ReadCloser
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type areaOp struct {
|
type areaOp struct {
|
||||||
@@ -229,33 +231,18 @@ func (c *pointerCollector) addHitNode(n hitNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newHandler returns the current handler or a new one for tag.
|
// newHandler returns the current handler or a new one for tag.
|
||||||
func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *pointerHandler {
|
func (c *pointerCollector) newHandler(tag event.Tag, state *pointerHandler) {
|
||||||
areaID := c.currentArea()
|
areaID := c.currentArea()
|
||||||
c.addHitNode(hitNode{
|
c.addHitNode(hitNode{
|
||||||
area: areaID,
|
area: areaID,
|
||||||
tag: tag,
|
tag: tag,
|
||||||
pass: c.state.pass > 0,
|
pass: c.state.pass > 0,
|
||||||
})
|
})
|
||||||
h, ok := c.q.handlers[tag]
|
state.areaPlusOne = areaID + 1
|
||||||
if !ok {
|
|
||||||
h = new(pointerHandler)
|
|
||||||
c.q.handlers[tag] = h
|
|
||||||
// Cancel handlers on (each) first appearance, but don't
|
|
||||||
// trigger redraw.
|
|
||||||
events.AddNoRedraw(tag, pointer.Event{Type: pointer.Cancel})
|
|
||||||
}
|
|
||||||
h.active = true
|
|
||||||
h.area = areaID
|
|
||||||
return h
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *pointerCollector) keyInputOp(op key.InputOp) {
|
func (s *pointerHandler) Reset() {
|
||||||
areaID := c.currentArea()
|
s.areaPlusOne = 0
|
||||||
c.addHitNode(hitNode{
|
|
||||||
area: areaID,
|
|
||||||
ktag: op.Tag,
|
|
||||||
pass: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *pointerCollector) actionInputOp(act system.Action) {
|
func (c *pointerCollector) actionInputOp(act system.Action) {
|
||||||
@@ -264,21 +251,109 @@ func (c *pointerCollector) actionInputOp(act system.Action) {
|
|||||||
area.action = act
|
area.action = act
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
|
func (q *pointerQueue) grab(state pointerState, req pointer.GrabCmd) (pointerState, []taggedEvent) {
|
||||||
|
var evts []taggedEvent
|
||||||
|
for _, p := range state.pointers {
|
||||||
|
if !p.pressed || p.id != req.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Drop other handlers that lost their grab.
|
||||||
|
for i := len(p.handlers) - 1; i >= 0; i-- {
|
||||||
|
if tag := p.handlers[i]; tag != req.Tag {
|
||||||
|
evts = append(evts, taggedEvent{
|
||||||
|
tag: tag,
|
||||||
|
event: pointer.Event{Kind: pointer.Cancel},
|
||||||
|
})
|
||||||
|
state = dropHandler(state, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return state, evts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pointerCollector) inputOp(tag event.Tag, state *pointerHandler) {
|
||||||
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 = tag
|
||||||
if op.Types&(pointer.Press|pointer.Release) != 0 {
|
c.newHandler(tag, state)
|
||||||
area.semantic.content.gestures |= ClickGesture
|
}
|
||||||
|
|
||||||
|
func (p *pointerFilter) Add(f event.Filter) {
|
||||||
|
switch f := f.(type) {
|
||||||
|
case transfer.SourceFilter:
|
||||||
|
for _, m := range p.sourceMimes {
|
||||||
|
if m == f.Type {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.sourceMimes = append(p.sourceMimes, f.Type)
|
||||||
|
case transfer.TargetFilter:
|
||||||
|
for _, m := range p.targetMimes {
|
||||||
|
if m == f.Type {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.targetMimes = append(p.targetMimes, f.Type)
|
||||||
|
case pointer.Filter:
|
||||||
|
p.kinds = p.kinds | f.Kinds
|
||||||
|
p.scrollRange = p.scrollRange.Union(f.ScrollBounds)
|
||||||
}
|
}
|
||||||
if op.Types&pointer.Scroll != 0 {
|
}
|
||||||
area.semantic.content.gestures |= ScrollGesture
|
|
||||||
|
func (p *pointerFilter) Matches(e event.Event) bool {
|
||||||
|
switch e := e.(type) {
|
||||||
|
case pointer.Event:
|
||||||
|
return e.Kind&p.kinds == e.Kind
|
||||||
|
case transfer.CancelEvent, transfer.InitiateEvent:
|
||||||
|
return len(p.sourceMimes) > 0 || len(p.targetMimes) > 0
|
||||||
|
case transfer.RequestEvent:
|
||||||
|
for _, t := range p.sourceMimes {
|
||||||
|
if t == e.Type {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case transfer.DataEvent:
|
||||||
|
for _, t := range p.targetMimes {
|
||||||
|
if t == e.Type {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
area.semantic.valid = area.semantic.content.gestures != 0
|
return false
|
||||||
h := c.newHandler(op.Tag, events)
|
}
|
||||||
h.wantsGrab = h.wantsGrab || op.Grab
|
|
||||||
h.types = h.types | op.Types
|
func (p *pointerFilter) Merge(p2 pointerFilter) {
|
||||||
h.scrollRange = op.ScrollBounds
|
p.kinds = p.kinds | p2.kinds
|
||||||
|
p.scrollRange = p.scrollRange.Union(p2.scrollRange)
|
||||||
|
p.sourceMimes = append(p.sourceMimes, p2.sourceMimes...)
|
||||||
|
p.targetMimes = append(p.targetMimes, p2.targetMimes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clampScroll splits a scroll distance in the remaining scroll and the
|
||||||
|
// scroll accepted by the filter.
|
||||||
|
func (p *pointerFilter) clampScroll(scroll f32.Point) (left, scrolled f32.Point) {
|
||||||
|
left.X, scrolled.X = clampSplit(scroll.X, p.scrollRange.Min.X, p.scrollRange.Max.X)
|
||||||
|
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollRange.Min.Y, p.scrollRange.Max.Y)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func clampSplit(v float32, min, max int) (float32, float32) {
|
||||||
|
if m := float32(max); v > m {
|
||||||
|
return v - m, m
|
||||||
|
}
|
||||||
|
if m := float32(min); v < m {
|
||||||
|
return v - m, m
|
||||||
|
}
|
||||||
|
return 0, v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *pointerHandler) ResetEvent() (event.Event, bool) {
|
||||||
|
if s.setup {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
s.setup = true
|
||||||
|
return pointer.Event{Kind: pointer.Cancel}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *pointerCollector) semanticLabel(lbl string) {
|
func (c *pointerCollector) semanticLabel(lbl string) {
|
||||||
@@ -309,11 +384,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) {
|
||||||
@@ -322,23 +397,28 @@ func (c *pointerCollector) cursor(cursor pointer.Cursor) {
|
|||||||
area.cursor = cursor
|
area.cursor = cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *pointerCollector) sourceOp(op transfer.SourceOp, events *handlerEvents) {
|
func (q *pointerQueue) offerData(handlers map[event.Tag]*handler, state pointerState, req transfer.OfferCmd) (pointerState, []taggedEvent) {
|
||||||
h := c.newHandler(op.Tag, events)
|
var evts []taggedEvent
|
||||||
h.sourceMimes = append(h.sourceMimes, op.Type)
|
for i, p := range state.pointers {
|
||||||
|
if p.dataSource != req.Tag {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p.dataTarget != nil {
|
||||||
|
evts = append(evts, taggedEvent{tag: p.dataTarget, event: transfer.DataEvent{
|
||||||
|
Type: req.Type,
|
||||||
|
Open: func() io.ReadCloser {
|
||||||
|
return req.Data
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
state.pointers = append([]pointerInfo{}, state.pointers...)
|
||||||
|
state.pointers[i], evts = q.deliverTransferCancelEvent(handlers, p, evts)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return state, evts
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *pointerCollector) targetOp(op transfer.TargetOp, events *handlerEvents) {
|
func (c *pointerCollector) Reset() {
|
||||||
h := c.newHandler(op.Tag, events)
|
|
||||||
h.targetMimes = append(h.targetMimes, op.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *pointerCollector) offerOp(op transfer.OfferOp, events *handlerEvents) {
|
|
||||||
h := c.newHandler(op.Tag, events)
|
|
||||||
h.offeredMime = op.Type
|
|
||||||
h.data = op.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *pointerCollector) reset() {
|
|
||||||
c.q.reset()
|
c.q.reset()
|
||||||
c.resetState()
|
c.resetState()
|
||||||
c.ensureRoot()
|
c.ensureRoot()
|
||||||
@@ -432,39 +512,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,14 +566,11 @@ func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
|
|||||||
} else {
|
} else {
|
||||||
idx = n.next
|
idx = n.next
|
||||||
}
|
}
|
||||||
if n.tag != nil {
|
if !onNode(n) {
|
||||||
if _, exists := q.handlers[n.tag]; exists {
|
break
|
||||||
hits = addHandler(hits, n.tag)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
q.scratch = hits[:0]
|
return cursor
|
||||||
return hits, cursor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point {
|
func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point {
|
||||||
@@ -517,17 +597,6 @@ func (q *pointerQueue) hit(areaIdx int, p f32.Point) (bool, pointer.Cursor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) reset() {
|
func (q *pointerQueue) reset() {
|
||||||
if q.handlers == nil {
|
|
||||||
q.handlers = make(map[event.Tag]*pointerHandler)
|
|
||||||
}
|
|
||||||
for _, h := range q.handlers {
|
|
||||||
// Reset handler.
|
|
||||||
h.active = false
|
|
||||||
h.wantsGrab = false
|
|
||||||
h.types = 0
|
|
||||||
h.sourceMimes = h.sourceMimes[:0]
|
|
||||||
h.targetMimes = h.targetMimes[:0]
|
|
||||||
}
|
|
||||||
q.hitTree = q.hitTree[:0]
|
q.hitTree = q.hitTree[:0]
|
||||||
q.areas = q.areas[:0]
|
q.areas = q.areas[:0]
|
||||||
q.semantic.idsAssigned = false
|
q.semantic.idsAssigned = false
|
||||||
@@ -545,80 +614,71 @@ func (q *pointerQueue) reset() {
|
|||||||
delete(q.semantic.contentIDs, k)
|
delete(q.semantic.contentIDs, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, rc := range q.transfers {
|
|
||||||
if rc != nil {
|
|
||||||
rc.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
q.transfers = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) Frame(events *handlerEvents) {
|
func (q *pointerQueue) Frame(handlers map[event.Tag]*handler, state pointerState) (pointerState, []taggedEvent) {
|
||||||
for k, h := range q.handlers {
|
for _, h := range handlers {
|
||||||
if !h.active {
|
if h.pointer.areaPlusOne != 0 {
|
||||||
q.dropHandler(nil, k)
|
area := &q.areas[h.pointer.areaPlusOne-1]
|
||||||
delete(q.handlers, k)
|
if h.filter.pointer.kinds&(pointer.Press|pointer.Release) != 0 {
|
||||||
}
|
area.semantic.content.gestures |= ClickGesture
|
||||||
if h.wantsGrab {
|
|
||||||
for _, p := range q.pointers {
|
|
||||||
if !p.pressed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for i, k2 := range p.handlers {
|
|
||||||
if k2 == k {
|
|
||||||
// Drop other handlers that lost their grab.
|
|
||||||
dropped := q.scratch[:0]
|
|
||||||
dropped = append(dropped, p.handlers[:i]...)
|
|
||||||
dropped = append(dropped, p.handlers[i+1:]...)
|
|
||||||
for _, tag := range dropped {
|
|
||||||
q.dropHandler(events, tag)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if h.filter.pointer.kinds&pointer.Scroll != 0 {
|
||||||
|
area.semantic.content.gestures |= ScrollGesture
|
||||||
|
}
|
||||||
|
area.semantic.valid = area.semantic.content.gestures != 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := range q.pointers {
|
var evts []taggedEvent
|
||||||
p := &q.pointers[i]
|
for i, p := range state.pointers {
|
||||||
q.deliverEnterLeaveEvents(p, events, p.last)
|
changed := false
|
||||||
q.deliverTransferDataEvent(p, events)
|
p, evts, state.cursor, changed = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, p.last)
|
||||||
|
if changed {
|
||||||
|
state.pointers = append([]pointerInfo{}, state.pointers...)
|
||||||
|
state.pointers[i] = p
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return state, evts
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) dropHandler(events *handlerEvents, tag event.Tag) {
|
func dropHandler(state pointerState, tag event.Tag) pointerState {
|
||||||
if events != nil {
|
pointers := state.pointers
|
||||||
events.Add(tag, pointer.Event{Type: pointer.Cancel})
|
state.pointers = nil
|
||||||
}
|
for _, p := range pointers {
|
||||||
for i := range q.pointers {
|
handlers := p.handlers
|
||||||
p := &q.pointers[i]
|
p.handlers = nil
|
||||||
for i := len(p.handlers) - 1; i >= 0; i-- {
|
for _, h := range handlers {
|
||||||
if p.handlers[i] == tag {
|
if h != tag {
|
||||||
p.handlers = append(p.handlers[:i], p.handlers[i+1:]...)
|
p.handlers = append(p.handlers, h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := len(p.entered) - 1; i >= 0; i-- {
|
entered := p.entered
|
||||||
if p.entered[i] == tag {
|
p.entered = nil
|
||||||
p.entered = append(p.entered[:i], p.entered[i+1:]...)
|
for _, h := range entered {
|
||||||
|
if h != tag {
|
||||||
|
p.entered = append(p.entered, h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
state.pointers = append(state.pointers, p)
|
||||||
}
|
}
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
// pointerOf returns the pointerInfo index corresponding to the pointer in e.
|
// pointerOf returns the pointerInfo index corresponding to the pointer in e.
|
||||||
func (q *pointerQueue) pointerOf(e pointer.Event) int {
|
func (s pointerState) pointerOf(e pointer.Event) (pointerState, int) {
|
||||||
for i, p := range q.pointers {
|
for i, p := range s.pointers {
|
||||||
if p.id == e.PointerID {
|
if p.id == e.PointerID {
|
||||||
return i
|
return s, i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
q.pointers = append(q.pointers, pointerInfo{id: e.PointerID})
|
n := len(s.pointers)
|
||||||
return len(q.pointers) - 1
|
s.pointers = append(s.pointers[:n:n], pointerInfo{id: e.PointerID})
|
||||||
|
return s, len(s.pointers) - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deliver is like Push, but delivers an event to a particular area.
|
// Deliver is like Push, but delivers an event to a particular area.
|
||||||
func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEvents) {
|
func (q *pointerQueue) Deliver(handlers map[event.Tag]*handler, areaIdx int, e pointer.Event) []taggedEvent {
|
||||||
var sx, sy = e.Scroll.X, e.Scroll.Y
|
scroll := e.Scroll
|
||||||
idx := len(q.hitTree) - 1
|
idx := len(q.hitTree) - 1
|
||||||
// Locate first potential receiver.
|
// Locate first potential receiver.
|
||||||
for idx != -1 {
|
for idx != -1 {
|
||||||
@@ -628,31 +688,28 @@ func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEven
|
|||||||
}
|
}
|
||||||
idx--
|
idx--
|
||||||
}
|
}
|
||||||
|
var evts []taggedEvent
|
||||||
for idx != -1 {
|
for idx != -1 {
|
||||||
n := &q.hitTree[idx]
|
n := &q.hitTree[idx]
|
||||||
idx = n.next
|
idx = n.next
|
||||||
if n.tag == nil {
|
h, ok := handlers[n.tag]
|
||||||
continue
|
if !ok || !h.filter.pointer.Matches(e) {
|
||||||
}
|
|
||||||
h := q.handlers[n.tag]
|
|
||||||
if e.Type&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 scroll == (f32.Point{}) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// Distribute the scroll to the handler based on its ScrollRange.
|
scroll, e.Scroll = h.filter.pointer.clampScroll(scroll)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
e.Position = q.invTransform(h.area, e.Position)
|
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
|
||||||
events.Add(n.tag, e)
|
evts = append(evts, taggedEvent{tag: n.tag, event: e})
|
||||||
if e.Type != pointer.Scroll {
|
if e.Kind != pointer.Scroll {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return evts
|
||||||
}
|
}
|
||||||
|
|
||||||
// SemanticArea returns the sematic content for area, and its parent area.
|
// SemanticArea returns the sematic content for area, and its parent area.
|
||||||
@@ -668,106 +725,129 @@ func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) {
|
|||||||
return semanticContent{}, -1
|
return semanticContent{}, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
|
func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState, e pointer.Event) (pointerState, []taggedEvent) {
|
||||||
if e.Type == pointer.Cancel {
|
var evts []taggedEvent
|
||||||
q.pointers = q.pointers[:0]
|
if e.Kind == pointer.Cancel {
|
||||||
for k := range q.handlers {
|
for k := range handlers {
|
||||||
q.dropHandler(events, k)
|
evts = append(evts, taggedEvent{
|
||||||
|
event: pointer.Event{Kind: pointer.Cancel},
|
||||||
|
tag: k,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return
|
state.pointers = nil
|
||||||
|
return state, evts
|
||||||
}
|
}
|
||||||
pidx := q.pointerOf(e)
|
state, pidx := state.pointerOf(e)
|
||||||
p := &q.pointers[pidx]
|
p := state.pointers[pidx]
|
||||||
p.last = e
|
|
||||||
|
|
||||||
switch e.Type {
|
switch e.Kind {
|
||||||
case pointer.Press:
|
case pointer.Press:
|
||||||
q.deliverEnterLeaveEvents(p, events, e)
|
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
|
||||||
p.pressed = true
|
p.pressed = true
|
||||||
q.deliverEvent(p, events, e)
|
evts = q.deliverEvent(handlers, p, evts, 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)
|
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
|
||||||
q.deliverEvent(p, events, e)
|
evts = q.deliverEvent(handlers, p, evts, e)
|
||||||
if p.pressed {
|
if p.pressed {
|
||||||
q.deliverDragEvent(p, events)
|
p, evts = q.deliverDragEvent(handlers, p, evts)
|
||||||
}
|
}
|
||||||
case pointer.Release:
|
case pointer.Release:
|
||||||
q.deliverEvent(p, events, e)
|
evts = q.deliverEvent(handlers, p, evts, e)
|
||||||
p.pressed = false
|
p.pressed = false
|
||||||
q.deliverEnterLeaveEvents(p, events, e)
|
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
|
||||||
q.deliverDropEvent(p, events)
|
p, evts = q.deliverDropEvent(handlers, p, evts)
|
||||||
case pointer.Scroll:
|
case pointer.Scroll:
|
||||||
q.deliverEnterLeaveEvents(p, events, e)
|
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
|
||||||
q.deliverEvent(p, events, e)
|
evts = q.deliverEvent(handlers, p, evts, e)
|
||||||
default:
|
default:
|
||||||
panic("unsupported pointer event type")
|
panic("unsupported pointer event type")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.last = e
|
||||||
|
|
||||||
if !p.pressed && len(p.entered) == 0 {
|
if !p.pressed && len(p.entered) == 0 {
|
||||||
// No longer need to track pointer.
|
// No longer need to track pointer.
|
||||||
q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...)
|
state.pointers = append(state.pointers[:pidx:pidx], state.pointers[pidx+1:]...)
|
||||||
|
} else {
|
||||||
|
state.pointers = append([]pointerInfo{}, state.pointers...)
|
||||||
|
state.pointers[pidx] = p
|
||||||
}
|
}
|
||||||
|
return state, evts
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e pointer.Event) {
|
func (q *pointerQueue) deliverEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent, e pointer.Event) []taggedEvent {
|
||||||
foremost := true
|
foremost := true
|
||||||
if p.pressed && len(p.handlers) == 1 {
|
if p.pressed && len(p.handlers) == 1 {
|
||||||
e.Priority = pointer.Grabbed
|
e.Priority = pointer.Grabbed
|
||||||
foremost = false
|
foremost = false
|
||||||
}
|
}
|
||||||
var sx, sy = e.Scroll.X, e.Scroll.Y
|
scroll := e.Scroll
|
||||||
for _, k := range p.handlers {
|
for _, k := range p.handlers {
|
||||||
h := q.handlers[k]
|
h, ok := handlers[k]
|
||||||
if e.Type == pointer.Scroll {
|
if !ok {
|
||||||
if sx == 0 && sy == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Distribute the scroll to the handler based on its ScrollRange.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
if e.Type&h.types == 0 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
f := h.filter.pointer
|
||||||
|
if !f.Matches(e) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e.Kind == pointer.Scroll {
|
||||||
|
if scroll == (f32.Point{}) {
|
||||||
|
return evts
|
||||||
|
}
|
||||||
|
scroll, e.Scroll = f.clampScroll(scroll)
|
||||||
|
}
|
||||||
e := e
|
e := e
|
||||||
if foremost {
|
if foremost {
|
||||||
foremost = false
|
foremost = false
|
||||||
e.Priority = pointer.Foremost
|
e.Priority = pointer.Foremost
|
||||||
}
|
}
|
||||||
e.Position = q.invTransform(h.area, e.Position)
|
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
|
||||||
events.Add(k, e)
|
evts = append(evts, taggedEvent{event: e, tag: k})
|
||||||
}
|
}
|
||||||
|
return evts
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) {
|
func (q *pointerQueue) deliverEnterLeaveEvents(handlers map[event.Tag]*handler, cursor pointer.Cursor, p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent, pointer.Cursor, bool) {
|
||||||
|
changed := false
|
||||||
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)
|
var transSrc *pointerFilter
|
||||||
if p.pressed {
|
if p.dataSource != nil {
|
||||||
// Filter out non-participating handlers,
|
transSrc = &handlers[p.dataSource].filter.pointer
|
||||||
// except potential transfer targets when a transfer has been initiated.
|
}
|
||||||
var hitsHaveTarget bool
|
cursor = q.hitTest(e.Position, func(n *hitNode) bool {
|
||||||
if p.dataSource != nil {
|
h, ok := handlers[n.tag]
|
||||||
transferSource := q.handlers[p.dataSource]
|
if !ok {
|
||||||
for _, hit := range hits {
|
return true
|
||||||
if _, ok := firstMimeMatch(transferSource, q.handlers[hit]); ok {
|
}
|
||||||
hitsHaveTarget = true
|
add := true
|
||||||
break
|
if p.pressed {
|
||||||
|
add = false
|
||||||
|
// Filter out non-participating handlers,
|
||||||
|
// except potential transfer targets when a transfer has been initiated.
|
||||||
|
if _, found := searchTag(p.handlers, n.tag); found {
|
||||||
|
add = true
|
||||||
|
}
|
||||||
|
if transSrc != nil {
|
||||||
|
if _, ok := firstMimeMatch(transSrc, &h.filter.pointer); ok {
|
||||||
|
add = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := len(hits) - 1; i >= 0; i-- {
|
if add {
|
||||||
if _, found := searchTag(p.handlers, hits[i]); !found && !hitsHaveTarget {
|
hits = addHandler(hits, n.tag)
|
||||||
hits = append(hits[:i], hits[i+1:]...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
return true
|
||||||
p.handlers = append(p.handlers[:0], hits...)
|
})
|
||||||
|
if !p.pressed {
|
||||||
|
changed = true
|
||||||
|
p.handlers = hits
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Deliver Leave events.
|
// Deliver Leave events.
|
||||||
@@ -775,111 +855,94 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv
|
|||||||
if _, found := searchTag(hits, k); found {
|
if _, found := searchTag(hits, k); found {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
h := q.handlers[k]
|
h, ok := handlers[k]
|
||||||
e.Type = pointer.Leave
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
e := e
|
||||||
|
e.Kind = pointer.Leave
|
||||||
|
|
||||||
if e.Type&h.types != 0 {
|
if h.filter.pointer.Matches(e) {
|
||||||
e := e
|
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
|
||||||
e.Position = q.invTransform(h.area, e.Position)
|
evts = append(evts, taggedEvent{tag: k, event: e})
|
||||||
events.Add(k, e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Deliver Enter events.
|
// Deliver Enter events.
|
||||||
for _, k := range hits {
|
for _, k := range hits {
|
||||||
h := q.handlers[k]
|
|
||||||
if _, found := searchTag(p.entered, k); found {
|
if _, found := searchTag(p.entered, k); found {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
e.Type = pointer.Enter
|
h, ok := handlers[k]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
e := e
|
||||||
|
e.Kind = pointer.Enter
|
||||||
|
|
||||||
if e.Type&h.types != 0 {
|
if h.filter.pointer.Matches(e) {
|
||||||
e := e
|
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
|
||||||
e.Position = q.invTransform(h.area, e.Position)
|
evts = append(evts, taggedEvent{tag: k, event: e})
|
||||||
events.Add(k, e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.entered = append(p.entered[:0], hits...)
|
p.entered = hits
|
||||||
|
return p, evts, cursor, changed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) deliverDragEvent(p *pointerInfo, events *handlerEvents) {
|
func (q *pointerQueue) deliverDragEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
|
||||||
if p.dataSource != nil {
|
if p.dataSource != nil {
|
||||||
return
|
return p, evts
|
||||||
}
|
}
|
||||||
// Identify the data source.
|
// Identify the data source.
|
||||||
for _, k := range p.entered {
|
for _, k := range p.entered {
|
||||||
src := q.handlers[k]
|
src := &handlers[k].filter.pointer
|
||||||
if len(src.sourceMimes) == 0 {
|
if len(src.sourceMimes) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// One data source handler per pointer.
|
// One data source handler per pointer.
|
||||||
p.dataSource = k
|
p.dataSource = k
|
||||||
// Notify all potential targets.
|
// Notify all potential targets.
|
||||||
for k, tgt := range q.handlers {
|
for k, tgt := range handlers {
|
||||||
if _, ok := firstMimeMatch(src, tgt); ok {
|
if _, ok := firstMimeMatch(src, &tgt.filter.pointer); ok {
|
||||||
events.Add(k, transfer.InitiateEvent{})
|
evts = append(evts, taggedEvent{tag: k, event: transfer.InitiateEvent{}})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
return p, evts
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) deliverDropEvent(p *pointerInfo, events *handlerEvents) {
|
func (q *pointerQueue) deliverDropEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
|
||||||
if p.dataSource == nil {
|
if p.dataSource == nil {
|
||||||
return
|
return p, evts
|
||||||
}
|
}
|
||||||
// Request data from the source.
|
// Request data from the source.
|
||||||
src := q.handlers[p.dataSource]
|
src := &handlers[p.dataSource].filter.pointer
|
||||||
for _, k := range p.entered {
|
for _, k := range p.entered {
|
||||||
h := q.handlers[k]
|
h := handlers[k]
|
||||||
if m, ok := firstMimeMatch(src, h); ok {
|
if m, ok := firstMimeMatch(src, &h.filter.pointer); ok {
|
||||||
p.dataTarget = k
|
p.dataTarget = k
|
||||||
events.Add(p.dataSource, transfer.RequestEvent{Type: m})
|
evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.RequestEvent{Type: m}})
|
||||||
return
|
return p, evts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// No valid target found, abort.
|
// No valid target found, abort.
|
||||||
q.deliverTransferCancelEvent(p, events)
|
return q.deliverTransferCancelEvent(handlers, p, evts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) deliverTransferDataEvent(p *pointerInfo, events *handlerEvents) {
|
func (q *pointerQueue) deliverTransferCancelEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
|
||||||
if p.dataSource == nil {
|
evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.CancelEvent{}})
|
||||||
return
|
|
||||||
}
|
|
||||||
src := q.handlers[p.dataSource]
|
|
||||||
if src.data == nil {
|
|
||||||
// Data not received yet.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if p.dataTarget == nil {
|
|
||||||
q.deliverTransferCancelEvent(p, events)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Send the offered data to the target.
|
|
||||||
transferIdx := len(q.transfers)
|
|
||||||
events.Add(p.dataTarget, transfer.DataEvent{
|
|
||||||
Type: src.offeredMime,
|
|
||||||
Open: func() io.ReadCloser {
|
|
||||||
q.transfers[transferIdx] = nil
|
|
||||||
return src.data
|
|
||||||
},
|
|
||||||
})
|
|
||||||
q.transfers = append(q.transfers, src.data)
|
|
||||||
p.dataTarget = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *pointerQueue) deliverTransferCancelEvent(p *pointerInfo, events *handlerEvents) {
|
|
||||||
events.Add(p.dataSource, transfer.CancelEvent{})
|
|
||||||
// Cancel all potential targets.
|
// Cancel all potential targets.
|
||||||
src := q.handlers[p.dataSource]
|
src := &handlers[p.dataSource].filter.pointer
|
||||||
for k, h := range q.handlers {
|
for k, h := range handlers {
|
||||||
if _, ok := firstMimeMatch(src, h); ok {
|
if _, ok := firstMimeMatch(src, &h.filter.pointer); ok {
|
||||||
events.Add(k, transfer.CancelEvent{})
|
evts = append(evts, taggedEvent{tag: k, event: transfer.CancelEvent{}})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
src.offeredMime = ""
|
|
||||||
src.data = nil
|
|
||||||
p.dataSource = nil
|
p.dataSource = nil
|
||||||
p.dataTarget = nil
|
p.dataTarget = nil
|
||||||
|
return p, evts
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClipFor clips r to the parents of area.
|
// ClipFor clips r to the parents of area.
|
||||||
@@ -914,7 +977,7 @@ func addHandler(tags []event.Tag, tag event.Tag) []event.Tag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// firstMimeMatch returns the first type match between src and tgt.
|
// firstMimeMatch returns the first type match between src and tgt.
|
||||||
func firstMimeMatch(src, tgt *pointerHandler) (first string, matched bool) {
|
func firstMimeMatch(src, tgt *pointerFilter) (first string, matched bool) {
|
||||||
for _, m1 := range tgt.targetMimes {
|
for _, m1 := range tgt.targetMimes {
|
||||||
for _, m2 := range src.sourceMimes {
|
for _, m2 := range src.sourceMimes {
|
||||||
if m1 == m2 {
|
if m1 == m2 {
|
||||||
@@ -951,13 +1014,3 @@ func (a *areaNode) bounds() image.Rectangle {
|
|||||||
Max: a.trans.Transform(f32internal.FPt(a.area.rect.Max)),
|
Max: a.trans.Transform(f32internal.FPt(a.area.rect.Max)),
|
||||||
}.Round()
|
}.Round()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setScrollEvent(scroll float32, min, max int) (left, scrolled float32) {
|
|
||||||
if v := float32(max); scroll > v {
|
|
||||||
return scroll - v, v
|
|
||||||
}
|
|
||||||
if v := float32(min); scroll < v {
|
|
||||||
return scroll - v, v
|
|
||||||
}
|
|
||||||
return 0, scroll
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,882 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gioui.org/f32"
|
||||||
|
f32internal "gioui.org/internal/f32"
|
||||||
|
"gioui.org/internal/ops"
|
||||||
|
"gioui.org/io/clipboard"
|
||||||
|
"gioui.org/io/event"
|
||||||
|
"gioui.org/io/key"
|
||||||
|
"gioui.org/io/pointer"
|
||||||
|
"gioui.org/io/semantic"
|
||||||
|
"gioui.org/io/system"
|
||||||
|
"gioui.org/io/transfer"
|
||||||
|
"gioui.org/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Router tracks the [io/event.Tag] identifiers of user interface widgets
|
||||||
|
// and routes events to them. [Source] is its interface exposed to widgets.
|
||||||
|
type Router struct {
|
||||||
|
savedTrans []f32.Affine2D
|
||||||
|
transStack []f32.Affine2D
|
||||||
|
handlers map[event.Tag]*handler
|
||||||
|
pointer struct {
|
||||||
|
queue pointerQueue
|
||||||
|
collector pointerCollector
|
||||||
|
}
|
||||||
|
key struct {
|
||||||
|
queue keyQueue
|
||||||
|
// The following fields have the same purpose as the fields in
|
||||||
|
// type handler, but for key.Events.
|
||||||
|
filter keyFilter
|
||||||
|
nextFilter keyFilter
|
||||||
|
processedFilter keyFilter
|
||||||
|
scratchFilter keyFilter
|
||||||
|
}
|
||||||
|
cqueue clipboardQueue
|
||||||
|
// states is the list of pending state changes resulting from
|
||||||
|
// incoming events. The first element, if present, contains the state
|
||||||
|
// and events for the current frame.
|
||||||
|
changes []stateChange
|
||||||
|
reader ops.Reader
|
||||||
|
// InvalidateCmd summary.
|
||||||
|
wakeup bool
|
||||||
|
wakeupTime time.Time
|
||||||
|
// Changes queued for next call to Frame.
|
||||||
|
commands []Command
|
||||||
|
// transfers is the pending transfer.DataEvent.Open functions.
|
||||||
|
transfers []io.ReadCloser
|
||||||
|
// deferring is set if command execution and event delivery is deferred
|
||||||
|
// to the next frame.
|
||||||
|
deferring bool
|
||||||
|
// scratchFilters is for garbage-free construction of ephemeral filters.
|
||||||
|
scratchFilters []taggedFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source implements the interface between a Router and user interface widgets.
|
||||||
|
// The value Source is disabled.
|
||||||
|
type Source struct {
|
||||||
|
r *Router
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command represents a request such as moving the focus, or initiating a clipboard read.
|
||||||
|
// Commands are queued by calling [Source.Queue].
|
||||||
|
type Command interface {
|
||||||
|
ImplementsCommand()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SemanticNode represents a node in the tree describing the components
|
||||||
|
// contained in a frame.
|
||||||
|
type SemanticNode struct {
|
||||||
|
ID SemanticID
|
||||||
|
ParentID SemanticID
|
||||||
|
Children []SemanticNode
|
||||||
|
Desc SemanticDesc
|
||||||
|
|
||||||
|
areaIdx int
|
||||||
|
}
|
||||||
|
|
||||||
|
// SemanticDesc provides a semantic description of a UI component.
|
||||||
|
type SemanticDesc struct {
|
||||||
|
Class semantic.ClassOp
|
||||||
|
Description string
|
||||||
|
Label string
|
||||||
|
Selected bool
|
||||||
|
Disabled bool
|
||||||
|
Gestures SemanticGestures
|
||||||
|
Bounds image.Rectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
// SemanticGestures is a bit-set of supported gestures.
|
||||||
|
type SemanticGestures int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClickGesture SemanticGestures = 1 << iota
|
||||||
|
ScrollGesture
|
||||||
|
)
|
||||||
|
|
||||||
|
// SemanticID uniquely identifies a SemanticDescription.
|
||||||
|
//
|
||||||
|
// By convention, the zero value denotes the non-existent ID.
|
||||||
|
type SemanticID uint
|
||||||
|
|
||||||
|
// SystemEvent is a marker for events that have platform specific
|
||||||
|
// side-effects. SystemEvents are never matched by catch-all filters.
|
||||||
|
type SystemEvent struct {
|
||||||
|
Event event.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// handler contains the per-handler state tracked by a [Router].
|
||||||
|
type handler struct {
|
||||||
|
// active tracks whether the handler was active in the current
|
||||||
|
// frame. Router deletes state belonging to inactive handlers during Frame.
|
||||||
|
active bool
|
||||||
|
pointer pointerHandler
|
||||||
|
key keyHandler
|
||||||
|
// filter the handler has asked for through event handling
|
||||||
|
// in the previous frame. It is used for routing events in the
|
||||||
|
// current frame.
|
||||||
|
filter filter
|
||||||
|
// prevFilter is the filter being built in the current frame.
|
||||||
|
nextFilter filter
|
||||||
|
// processedFilter is the filters that have exhausted available events.
|
||||||
|
processedFilter filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter is the union of a set of [io/event.Filters].
|
||||||
|
type filter struct {
|
||||||
|
pointer pointerFilter
|
||||||
|
focusable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// taggedFilter is a filter for a particular tag.
|
||||||
|
type taggedFilter struct {
|
||||||
|
tag event.Tag
|
||||||
|
filter filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// stateChange represents the new state and outgoing events
|
||||||
|
// resulting from an incoming event.
|
||||||
|
type stateChange struct {
|
||||||
|
// event, if set, is the trigger for the change.
|
||||||
|
event event.Event
|
||||||
|
state inputState
|
||||||
|
events []taggedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputState represent a immutable snapshot of the state required
|
||||||
|
// to route events.
|
||||||
|
type inputState struct {
|
||||||
|
clipboardState
|
||||||
|
keyState
|
||||||
|
pointerState
|
||||||
|
}
|
||||||
|
|
||||||
|
// taggedEvent represents an event and its target handler.
|
||||||
|
type taggedEvent struct {
|
||||||
|
event event.Event
|
||||||
|
tag event.Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source returns a Source backed by this Router.
|
||||||
|
func (q *Router) Source() Source {
|
||||||
|
return Source{r: q}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute a command.
|
||||||
|
func (s Source) Execute(c Command) {
|
||||||
|
if !s.Enabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.r.execute(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled reports whether the source is enabled. Only enabled
|
||||||
|
// Sources deliver events and respond to commands.
|
||||||
|
func (s Source) Enabled() bool {
|
||||||
|
return s.r != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focused reports whether tag is focused, according to the most recent
|
||||||
|
// [key.FocusEvent] delivered.
|
||||||
|
func (s Source) Focused(tag event.Tag) bool {
|
||||||
|
if !s.Enabled() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.r.state().keyState.focus == tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event returns the next event that matches at least one of filters.
|
||||||
|
func (s Source) Event(filters ...event.Filter) (event.Event, bool) {
|
||||||
|
if !s.Enabled() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return s.r.Event(filters...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Router) Event(filters ...event.Filter) (event.Event, bool) {
|
||||||
|
// Merge filters into scratch filters.
|
||||||
|
q.scratchFilters = q.scratchFilters[:0]
|
||||||
|
q.key.scratchFilter = q.key.scratchFilter[:0]
|
||||||
|
for _, f := range filters {
|
||||||
|
var t event.Tag
|
||||||
|
switch f := f.(type) {
|
||||||
|
case key.Filter:
|
||||||
|
q.key.scratchFilter = append(q.key.scratchFilter, f)
|
||||||
|
continue
|
||||||
|
case transfer.SourceFilter:
|
||||||
|
t = f.Target
|
||||||
|
case transfer.TargetFilter:
|
||||||
|
t = f.Target
|
||||||
|
case key.FocusFilter:
|
||||||
|
t = f.Target
|
||||||
|
case pointer.Filter:
|
||||||
|
t = f.Target
|
||||||
|
}
|
||||||
|
if t == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var filter *filter
|
||||||
|
for i := range q.scratchFilters {
|
||||||
|
s := &q.scratchFilters[i]
|
||||||
|
if s.tag == t {
|
||||||
|
filter = &s.filter
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filter == nil {
|
||||||
|
n := len(q.scratchFilters)
|
||||||
|
if n < cap(q.scratchFilters) {
|
||||||
|
// Re-use previously allocated filter.
|
||||||
|
q.scratchFilters = q.scratchFilters[:n+1]
|
||||||
|
tf := &q.scratchFilters[n]
|
||||||
|
tf.tag = t
|
||||||
|
filter = &tf.filter
|
||||||
|
filter.Reset()
|
||||||
|
} else {
|
||||||
|
q.scratchFilters = append(q.scratchFilters, taggedFilter{tag: t})
|
||||||
|
filter = &q.scratchFilters[n].filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter.Add(f)
|
||||||
|
}
|
||||||
|
for _, tf := range q.scratchFilters {
|
||||||
|
h := q.stateFor(tf.tag)
|
||||||
|
h.filter.Merge(tf.filter)
|
||||||
|
h.nextFilter.Merge(tf.filter)
|
||||||
|
}
|
||||||
|
q.key.filter = append(q.key.filter, q.key.scratchFilter...)
|
||||||
|
q.key.nextFilter = append(q.key.nextFilter, q.key.scratchFilter...)
|
||||||
|
// Deliver reset event, if any.
|
||||||
|
for _, f := range filters {
|
||||||
|
switch f := f.(type) {
|
||||||
|
case key.FocusFilter:
|
||||||
|
if f.Target == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h := q.stateFor(f.Target)
|
||||||
|
if reset, ok := h.key.ResetEvent(); ok {
|
||||||
|
return reset, true
|
||||||
|
}
|
||||||
|
case pointer.Filter:
|
||||||
|
if f.Target == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h := q.stateFor(f.Target)
|
||||||
|
if reset, ok := h.pointer.ResetEvent(); ok && h.filter.pointer.Matches(reset) {
|
||||||
|
return reset, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !q.deferring {
|
||||||
|
for i := range q.changes {
|
||||||
|
change := &q.changes[i]
|
||||||
|
for j, evt := range change.events {
|
||||||
|
match := false
|
||||||
|
switch e := evt.event.(type) {
|
||||||
|
case key.Event:
|
||||||
|
match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false)
|
||||||
|
default:
|
||||||
|
for _, tf := range q.scratchFilters {
|
||||||
|
if evt.tag == tf.tag && tf.filter.Matches(evt.event) {
|
||||||
|
match = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if match {
|
||||||
|
change.events = append(change.events[:j], change.events[j+1:]...)
|
||||||
|
// Fast forward state to last matched.
|
||||||
|
q.collapseState(i)
|
||||||
|
return evt.event, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, tf := range q.scratchFilters {
|
||||||
|
h := q.stateFor(tf.tag)
|
||||||
|
h.processedFilter.Merge(tf.filter)
|
||||||
|
}
|
||||||
|
q.key.processedFilter = append(q.key.processedFilter, q.key.scratchFilter...)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// collapseState in the interval [1;idx] into q.changes[0].
|
||||||
|
func (q *Router) collapseState(idx int) {
|
||||||
|
if idx == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
first := &q.changes[0]
|
||||||
|
first.state = q.changes[idx].state
|
||||||
|
for i := 1; i <= idx; i++ {
|
||||||
|
first.events = append(first.events, q.changes[i].events...)
|
||||||
|
}
|
||||||
|
q.changes = append(q.changes[:1], q.changes[idx+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame replaces the declared handlers from the supplied
|
||||||
|
// operation list. The text input state, wakeup time and whether
|
||||||
|
// there are active profile handlers is also saved.
|
||||||
|
func (q *Router) Frame(frame *op.Ops) {
|
||||||
|
var remaining []event.Event
|
||||||
|
if n := len(q.changes); n > 0 {
|
||||||
|
if q.deferring {
|
||||||
|
// Collect events for replay.
|
||||||
|
for _, ch := range q.changes[1:] {
|
||||||
|
remaining = append(remaining, ch.event)
|
||||||
|
}
|
||||||
|
q.changes = append(q.changes[:0], stateChange{state: q.changes[0].state})
|
||||||
|
} else {
|
||||||
|
// Collapse state.
|
||||||
|
state := q.changes[n-1].state
|
||||||
|
q.changes = append(q.changes[:0], stateChange{state: state})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, rc := range q.transfers {
|
||||||
|
if rc != nil {
|
||||||
|
rc.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q.transfers = nil
|
||||||
|
q.deferring = false
|
||||||
|
for _, h := range q.handlers {
|
||||||
|
h.filter, h.nextFilter = h.nextFilter, h.filter
|
||||||
|
h.nextFilter.Reset()
|
||||||
|
h.processedFilter.Reset()
|
||||||
|
h.pointer.Reset()
|
||||||
|
h.key.Reset()
|
||||||
|
}
|
||||||
|
q.key.filter, q.key.nextFilter = q.key.nextFilter, q.key.filter
|
||||||
|
q.key.nextFilter = q.key.nextFilter[:0]
|
||||||
|
var ops *ops.Ops
|
||||||
|
if frame != nil {
|
||||||
|
ops = &frame.Internal
|
||||||
|
}
|
||||||
|
q.reader.Reset(ops)
|
||||||
|
q.collect()
|
||||||
|
for k, h := range q.handlers {
|
||||||
|
if !h.active {
|
||||||
|
delete(q.handlers, k)
|
||||||
|
} else {
|
||||||
|
h.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q.executeCommands()
|
||||||
|
q.Queue(remaining...)
|
||||||
|
st := q.lastState()
|
||||||
|
pst, evts := q.pointer.queue.Frame(q.handlers, st.pointerState)
|
||||||
|
st.pointerState = pst
|
||||||
|
st.keyState = q.key.queue.Frame(q.handlers, q.lastState().keyState)
|
||||||
|
q.changeState(nil, st, evts)
|
||||||
|
|
||||||
|
// Collapse state and events.
|
||||||
|
q.collapseState(len(q.changes) - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue events to be routed.
|
||||||
|
func (q *Router) Queue(events ...event.Event) {
|
||||||
|
for _, e := range events {
|
||||||
|
se, system := e.(SystemEvent)
|
||||||
|
if system {
|
||||||
|
e = se.Event
|
||||||
|
}
|
||||||
|
q.processEvent(e, system)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *filter) Add(flt event.Filter) {
|
||||||
|
switch flt := flt.(type) {
|
||||||
|
case key.FocusFilter:
|
||||||
|
f.focusable = true
|
||||||
|
case pointer.Filter:
|
||||||
|
f.pointer.Add(flt)
|
||||||
|
case transfer.SourceFilter, transfer.TargetFilter:
|
||||||
|
f.pointer.Add(flt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge f2 into f.
|
||||||
|
func (f *filter) Merge(f2 filter) {
|
||||||
|
f.focusable = f.focusable || f2.focusable
|
||||||
|
f.pointer.Merge(f2.pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *filter) Matches(e event.Event) bool {
|
||||||
|
switch e.(type) {
|
||||||
|
case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent:
|
||||||
|
return f.focusable
|
||||||
|
default:
|
||||||
|
return f.pointer.Matches(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *filter) Reset() {
|
||||||
|
*f = filter{
|
||||||
|
pointer: pointerFilter{
|
||||||
|
sourceMimes: f.pointer.sourceMimes[:0],
|
||||||
|
targetMimes: f.pointer.targetMimes[:0],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Router) processEvent(e event.Event, system bool) {
|
||||||
|
state := q.lastState()
|
||||||
|
switch e := e.(type) {
|
||||||
|
case pointer.Event:
|
||||||
|
pstate, evts := q.pointer.queue.Push(q.handlers, state.pointerState, e)
|
||||||
|
state.pointerState = pstate
|
||||||
|
q.changeState(e, state, evts)
|
||||||
|
case key.Event:
|
||||||
|
var evts []taggedEvent
|
||||||
|
if q.key.filter.Matches(state.keyState.focus, e, system) {
|
||||||
|
evts = append(evts, taggedEvent{event: e})
|
||||||
|
}
|
||||||
|
q.changeState(e, state, evts)
|
||||||
|
case key.SnippetEvent:
|
||||||
|
// Expand existing, overlapping snippet.
|
||||||
|
if r := state.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) {
|
||||||
|
if e.Start > r.Start {
|
||||||
|
e.Start = r.Start
|
||||||
|
}
|
||||||
|
if e.End < r.End {
|
||||||
|
e.End = r.End
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var evts []taggedEvent
|
||||||
|
if f := state.focus; f != nil {
|
||||||
|
evts = append(evts, taggedEvent{tag: f, event: e})
|
||||||
|
}
|
||||||
|
q.changeState(e, state, evts)
|
||||||
|
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
|
||||||
|
var evts []taggedEvent
|
||||||
|
if f := state.focus; f != nil {
|
||||||
|
evts = append(evts, taggedEvent{tag: f, event: e})
|
||||||
|
}
|
||||||
|
q.changeState(e, state, evts)
|
||||||
|
case transfer.DataEvent:
|
||||||
|
cstate, evts := q.cqueue.Push(state.clipboardState, e)
|
||||||
|
state.clipboardState = cstate
|
||||||
|
q.changeState(e, state, evts)
|
||||||
|
default:
|
||||||
|
panic("unknown event type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Router) execute(c Command) {
|
||||||
|
// The command can be executed immediately if event delivery is not frozen, and
|
||||||
|
// no event receiver has completed their event handling.
|
||||||
|
if !q.deferring {
|
||||||
|
ch := q.executeCommand(c)
|
||||||
|
immediate := true
|
||||||
|
for _, e := range ch.events {
|
||||||
|
h, ok := q.handlers[e.tag]
|
||||||
|
immediate = immediate && (!ok || !h.processedFilter.Matches(e.event))
|
||||||
|
}
|
||||||
|
if immediate {
|
||||||
|
// Hold on to the remaining events for state replay.
|
||||||
|
var evts []event.Event
|
||||||
|
for _, ch := range q.changes {
|
||||||
|
if ch.event != nil {
|
||||||
|
evts = append(evts, ch.event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(q.changes) > 1 {
|
||||||
|
q.changes = q.changes[:1]
|
||||||
|
}
|
||||||
|
q.changeState(nil, ch.state, ch.events)
|
||||||
|
q.Queue(evts...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q.deferring = true
|
||||||
|
q.commands = append(q.commands, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Router) state() inputState {
|
||||||
|
if len(q.changes) > 0 {
|
||||||
|
return q.changes[0].state
|
||||||
|
}
|
||||||
|
return inputState{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Router) lastState() inputState {
|
||||||
|
if n := len(q.changes); n > 0 {
|
||||||
|
return q.changes[n-1].state
|
||||||
|
}
|
||||||
|
return inputState{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Router) executeCommands() {
|
||||||
|
for _, c := range q.commands {
|
||||||
|
ch := q.executeCommand(c)
|
||||||
|
q.changeState(nil, ch.state, ch.events)
|
||||||
|
}
|
||||||
|
q.commands = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeCommand the command and return the resulting state change along with the
|
||||||
|
// tag the state change depended on, if any.
|
||||||
|
func (q *Router) executeCommand(c Command) stateChange {
|
||||||
|
state := q.state()
|
||||||
|
var evts []taggedEvent
|
||||||
|
switch req := c.(type) {
|
||||||
|
case key.SelectionCmd:
|
||||||
|
state.keyState = q.key.queue.setSelection(state.keyState, req)
|
||||||
|
case key.FocusCmd:
|
||||||
|
state.keyState, evts = q.key.queue.Focus(q.handlers, state.keyState, req.Tag)
|
||||||
|
case key.SoftKeyboardCmd:
|
||||||
|
state.keyState = state.keyState.softKeyboard(req.Show)
|
||||||
|
case key.SnippetCmd:
|
||||||
|
state.keyState = q.key.queue.setSnippet(state.keyState, req)
|
||||||
|
case transfer.OfferCmd:
|
||||||
|
state.pointerState, evts = q.pointer.queue.offerData(q.handlers, state.pointerState, req)
|
||||||
|
case clipboard.WriteCmd:
|
||||||
|
q.cqueue.ProcessWriteClipboard(req)
|
||||||
|
case clipboard.ReadCmd:
|
||||||
|
state.clipboardState = q.cqueue.ProcessReadClipboard(state.clipboardState, req.Tag)
|
||||||
|
case pointer.GrabCmd:
|
||||||
|
state.pointerState, evts = q.pointer.queue.grab(state.pointerState, req)
|
||||||
|
case op.InvalidateCmd:
|
||||||
|
if !q.wakeup || req.At.Before(q.wakeupTime) {
|
||||||
|
q.wakeup = true
|
||||||
|
q.wakeupTime = req.At
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stateChange{state: state, events: evts}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Router) changeState(e event.Event, state inputState, evts []taggedEvent) {
|
||||||
|
// Wrap pointer.DataEvent.Open functions to detect them not being called.
|
||||||
|
for i := range evts {
|
||||||
|
e := &evts[i]
|
||||||
|
if de, ok := e.event.(transfer.DataEvent); ok {
|
||||||
|
transferIdx := len(q.transfers)
|
||||||
|
data := de.Open()
|
||||||
|
q.transfers = append(q.transfers, data)
|
||||||
|
de.Open = func() io.ReadCloser {
|
||||||
|
q.transfers[transferIdx] = nil
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
e.event = de
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Initialize the first change to contain the current state
|
||||||
|
// and events that are bound for the current frame.
|
||||||
|
if len(q.changes) == 0 {
|
||||||
|
q.changes = append(q.changes, stateChange{})
|
||||||
|
}
|
||||||
|
if e != nil && len(evts) > 0 {
|
||||||
|
// An event triggered events bound for user receivers. Add a state change to be
|
||||||
|
// able to redo the change in case of a command execution.
|
||||||
|
q.changes = append(q.changes, stateChange{event: e, state: state, events: evts})
|
||||||
|
} else {
|
||||||
|
// Otherwise, merge with previous change.
|
||||||
|
prev := &q.changes[len(q.changes)-1]
|
||||||
|
prev.state = state
|
||||||
|
prev.events = append(prev.events, evts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func rangeOverlaps(r1, r2 key.Range) bool {
|
||||||
|
r1 = rangeNorm(r1)
|
||||||
|
r2 = rangeNorm(r2)
|
||||||
|
return r1.Start <= r2.Start && r2.Start < r1.End ||
|
||||||
|
r1.Start <= r2.End && r2.End < r1.End
|
||||||
|
}
|
||||||
|
|
||||||
|
func rangeNorm(r key.Range) key.Range {
|
||||||
|
if r.End < r.Start {
|
||||||
|
r.End, r.Start = r.Start, r.End
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Router) MoveFocus(dir key.FocusDirection) {
|
||||||
|
state := q.lastState()
|
||||||
|
kstate, evts := q.key.queue.MoveFocus(q.handlers, state.keyState, dir)
|
||||||
|
state.keyState = kstate
|
||||||
|
q.changeState(nil, state, evts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevealFocus scrolls the current focus (if any) into viewport
|
||||||
|
// if there are scrollable parent handlers.
|
||||||
|
func (q *Router) RevealFocus(viewport image.Rectangle) {
|
||||||
|
state := q.lastState()
|
||||||
|
focus := state.focus
|
||||||
|
if focus == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kh := &q.handlers[focus].key
|
||||||
|
bounds := q.key.queue.BoundsFor(kh)
|
||||||
|
area := q.key.queue.AreaFor(kh)
|
||||||
|
viewport = q.pointer.queue.ClipFor(area, viewport)
|
||||||
|
|
||||||
|
topleft := bounds.Min.Sub(viewport.Min)
|
||||||
|
topleft = max(topleft, bounds.Max.Sub(viewport.Max))
|
||||||
|
topleft = min(image.Pt(0, 0), topleft)
|
||||||
|
bottomright := bounds.Max.Sub(viewport.Max)
|
||||||
|
bottomright = min(bottomright, bounds.Min.Sub(viewport.Min))
|
||||||
|
bottomright = max(image.Pt(0, 0), bottomright)
|
||||||
|
s := topleft
|
||||||
|
if s.X == 0 {
|
||||||
|
s.X = bottomright.X
|
||||||
|
}
|
||||||
|
if s.Y == 0 {
|
||||||
|
s.Y = bottomright.Y
|
||||||
|
}
|
||||||
|
q.ScrollFocus(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollFocus scrolls the focused widget, if any, by dist.
|
||||||
|
func (q *Router) ScrollFocus(dist image.Point) {
|
||||||
|
state := q.lastState()
|
||||||
|
focus := state.focus
|
||||||
|
if focus == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kh := &q.handlers[focus].key
|
||||||
|
area := q.key.queue.AreaFor(kh)
|
||||||
|
q.changeState(nil, q.lastState(), q.pointer.queue.Deliver(q.handlers, area, pointer.Event{
|
||||||
|
Kind: pointer.Scroll,
|
||||||
|
Source: pointer.Touch,
|
||||||
|
Scroll: f32internal.FPt(dist),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(p1, p2 image.Point) image.Point {
|
||||||
|
m := p1
|
||||||
|
if p2.X > m.X {
|
||||||
|
m.X = p2.X
|
||||||
|
}
|
||||||
|
if p2.Y > m.Y {
|
||||||
|
m.Y = p2.Y
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(p1, p2 image.Point) image.Point {
|
||||||
|
m := p1
|
||||||
|
if p2.X < m.X {
|
||||||
|
m.X = p2.X
|
||||||
|
}
|
||||||
|
if p2.Y < m.Y {
|
||||||
|
m.Y = p2.Y
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Router) ActionAt(p f32.Point) (system.Action, bool) {
|
||||||
|
return q.pointer.queue.ActionAt(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Router) ClickFocus() {
|
||||||
|
focus := q.lastState().focus
|
||||||
|
if focus == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kh := &q.handlers[focus].key
|
||||||
|
bounds := q.key.queue.BoundsFor(kh)
|
||||||
|
center := bounds.Max.Add(bounds.Min).Div(2)
|
||||||
|
e := pointer.Event{
|
||||||
|
Position: f32.Pt(float32(center.X), float32(center.Y)),
|
||||||
|
Source: pointer.Touch,
|
||||||
|
}
|
||||||
|
area := q.key.queue.AreaFor(kh)
|
||||||
|
e.Kind = pointer.Press
|
||||||
|
state := q.lastState()
|
||||||
|
q.changeState(nil, state, q.pointer.queue.Deliver(q.handlers, area, e))
|
||||||
|
e.Kind = pointer.Release
|
||||||
|
q.changeState(nil, state, q.pointer.queue.Deliver(q.handlers, area, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextInputState returns the input state from the most recent
|
||||||
|
// call to Frame.
|
||||||
|
func (q *Router) TextInputState() TextInputState {
|
||||||
|
state := q.state()
|
||||||
|
kstate, s := state.InputState()
|
||||||
|
state.keyState = kstate
|
||||||
|
q.changeState(nil, state, nil)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextInputHint returns the input mode from the most recent key.InputOp.
|
||||||
|
func (q *Router) TextInputHint() (key.InputHint, bool) {
|
||||||
|
return q.key.queue.InputHint(q.handlers, q.state().keyState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteClipboard returns the most recent content to be copied
|
||||||
|
// to the clipboard, if any.
|
||||||
|
func (q *Router) WriteClipboard() (mime string, content []byte, ok bool) {
|
||||||
|
return q.cqueue.WriteClipboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClipboardRequested reports if any new handler is waiting
|
||||||
|
// to read the clipboard.
|
||||||
|
func (q *Router) ClipboardRequested() bool {
|
||||||
|
return q.cqueue.ClipboardRequested(q.lastState().clipboardState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor returns the last cursor set.
|
||||||
|
func (q *Router) Cursor() pointer.Cursor {
|
||||||
|
return q.state().cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
// SemanticAt returns the first semantic description under pos, if any.
|
||||||
|
func (q *Router) SemanticAt(pos f32.Point) (SemanticID, bool) {
|
||||||
|
return q.pointer.queue.SemanticAt(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendSemantics appends the semantic tree to nodes, and returns the result.
|
||||||
|
// The root node is the first added.
|
||||||
|
func (q *Router) AppendSemantics(nodes []SemanticNode) []SemanticNode {
|
||||||
|
q.pointer.collector.q = &q.pointer.queue
|
||||||
|
q.pointer.collector.ensureRoot()
|
||||||
|
return q.pointer.queue.AppendSemantics(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditorState returns the editor state for the focused handler, or the
|
||||||
|
// zero value if there is none.
|
||||||
|
func (q *Router) EditorState() EditorState {
|
||||||
|
return q.key.queue.editorState(q.handlers, q.state().keyState)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Router) stateFor(tag event.Tag) *handler {
|
||||||
|
if tag == nil {
|
||||||
|
panic("internal error: nil tag")
|
||||||
|
}
|
||||||
|
s, ok := q.handlers[tag]
|
||||||
|
if !ok {
|
||||||
|
s = new(handler)
|
||||||
|
if q.handlers == nil {
|
||||||
|
q.handlers = make(map[event.Tag]*handler)
|
||||||
|
}
|
||||||
|
q.handlers[tag] = s
|
||||||
|
}
|
||||||
|
s.active = true
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Router) collect() {
|
||||||
|
q.transStack = q.transStack[:0]
|
||||||
|
pc := &q.pointer.collector
|
||||||
|
pc.q = &q.pointer.queue
|
||||||
|
pc.Reset()
|
||||||
|
kq := &q.key.queue
|
||||||
|
q.key.queue.Reset()
|
||||||
|
var t f32.Affine2D
|
||||||
|
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
|
||||||
|
switch ops.OpType(encOp.Data[0]) {
|
||||||
|
case ops.TypeSave:
|
||||||
|
id := ops.DecodeSave(encOp.Data)
|
||||||
|
if extra := id - len(q.savedTrans) + 1; extra > 0 {
|
||||||
|
q.savedTrans = append(q.savedTrans, make([]f32.Affine2D, extra)...)
|
||||||
|
}
|
||||||
|
q.savedTrans[id] = t
|
||||||
|
case ops.TypeLoad:
|
||||||
|
id := ops.DecodeLoad(encOp.Data)
|
||||||
|
t = q.savedTrans[id]
|
||||||
|
pc.resetState()
|
||||||
|
pc.setTrans(t)
|
||||||
|
|
||||||
|
case ops.TypeClip:
|
||||||
|
var op ops.ClipOp
|
||||||
|
op.Decode(encOp.Data)
|
||||||
|
pc.clip(op)
|
||||||
|
case ops.TypePopClip:
|
||||||
|
pc.popArea()
|
||||||
|
case ops.TypeTransform:
|
||||||
|
t2, push := ops.DecodeTransform(encOp.Data)
|
||||||
|
if push {
|
||||||
|
q.transStack = append(q.transStack, t)
|
||||||
|
}
|
||||||
|
t = t.Mul(t2)
|
||||||
|
pc.setTrans(t)
|
||||||
|
case ops.TypePopTransform:
|
||||||
|
n := len(q.transStack)
|
||||||
|
t = q.transStack[n-1]
|
||||||
|
q.transStack = q.transStack[:n-1]
|
||||||
|
pc.setTrans(t)
|
||||||
|
|
||||||
|
case ops.TypeInput:
|
||||||
|
tag := encOp.Refs[0].(event.Tag)
|
||||||
|
s := q.stateFor(tag)
|
||||||
|
pc.inputOp(tag, &s.pointer)
|
||||||
|
a := pc.currentArea()
|
||||||
|
b := pc.currentAreaBounds()
|
||||||
|
if s.filter.focusable {
|
||||||
|
kq.inputOp(tag, &s.key, t, a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointer ops.
|
||||||
|
case ops.TypePass:
|
||||||
|
pc.pass()
|
||||||
|
case ops.TypePopPass:
|
||||||
|
pc.popPass()
|
||||||
|
case ops.TypeCursor:
|
||||||
|
name := pointer.Cursor(encOp.Data[1])
|
||||||
|
pc.cursor(name)
|
||||||
|
case ops.TypeActionInput:
|
||||||
|
act := system.Action(encOp.Data[1])
|
||||||
|
pc.actionInputOp(act)
|
||||||
|
case ops.TypeKeyInputHint:
|
||||||
|
op := key.InputHintOp{
|
||||||
|
Tag: encOp.Refs[0].(event.Tag),
|
||||||
|
Hint: key.InputHint(encOp.Data[1]),
|
||||||
|
}
|
||||||
|
s := q.stateFor(op.Tag)
|
||||||
|
s.key.inputHint(op.Hint)
|
||||||
|
|
||||||
|
// Semantic ops.
|
||||||
|
case ops.TypeSemanticLabel:
|
||||||
|
lbl := *encOp.Refs[0].(*string)
|
||||||
|
pc.semanticLabel(lbl)
|
||||||
|
case ops.TypeSemanticDesc:
|
||||||
|
desc := *encOp.Refs[0].(*string)
|
||||||
|
pc.semanticDesc(desc)
|
||||||
|
case ops.TypeSemanticClass:
|
||||||
|
class := semantic.ClassOp(encOp.Data[1])
|
||||||
|
pc.semanticClass(class)
|
||||||
|
case ops.TypeSemanticSelected:
|
||||||
|
if encOp.Data[1] != 0 {
|
||||||
|
pc.semanticSelected(true)
|
||||||
|
} else {
|
||||||
|
pc.semanticSelected(false)
|
||||||
|
}
|
||||||
|
case ops.TypeSemanticEnabled:
|
||||||
|
if encOp.Data[1] != 0 {
|
||||||
|
pc.semanticEnabled(true)
|
||||||
|
} else {
|
||||||
|
pc.semanticEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WakeupTime returns the most recent time for doing another frame,
|
||||||
|
// as determined from the last call to Frame.
|
||||||
|
func (q *Router) WakeupTime() (time.Time, bool) {
|
||||||
|
t, w := q.wakeupTime, q.wakeup
|
||||||
|
q.wakeup = false
|
||||||
|
// Pending events always trigger wakeups.
|
||||||
|
if len(q.changes) > 1 || len(q.changes) == 1 && len(q.changes[0].events) > 0 {
|
||||||
|
t, w = time.Time{}, true
|
||||||
|
}
|
||||||
|
return t, w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SemanticGestures) String() string {
|
||||||
|
var gestures []string
|
||||||
|
if s&ClickGesture != 0 {
|
||||||
|
gestures = append(gestures, "Click")
|
||||||
|
}
|
||||||
|
return strings.Join(gestures, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SystemEvent) ImplementsEvent() {}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package input
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gioui.org/io/pointer"
|
||||||
|
"gioui.org/op"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNoFilterAllocs(t *testing.T) {
|
||||||
|
b := testing.Benchmark(func(b *testing.B) {
|
||||||
|
var r Router
|
||||||
|
s := r.Source()
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
s.Event(pointer.Filter{})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if allocs := b.AllocsPerOp(); allocs != 0 {
|
||||||
|
t.Fatalf("expected 0 AllocsPerOp, got %d", allocs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterWakeup(t *testing.T) {
|
||||||
|
r := new(Router)
|
||||||
|
r.Source().Execute(op.InvalidateCmd{})
|
||||||
|
r.Frame(new(op.Ops))
|
||||||
|
if _, wake := r.WakeupTime(); !wake {
|
||||||
|
t.Errorf("InvalidateCmd did not trigger a redraw")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
package router
|
package input
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
"gioui.org/io/semantic"
|
"gioui.org/io/semantic"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
@@ -74,13 +75,19 @@ 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)
|
|
||||||
|
h := new(int)
|
||||||
|
event.Op(&ops, h)
|
||||||
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
|
||||||
|
events(&r, -1, pointer.Filter{
|
||||||
|
Target: h,
|
||||||
|
Kinds: pointer.Press | pointer.Release,
|
||||||
|
})
|
||||||
r.Frame(&ops)
|
r.Frame(&ops)
|
||||||
tree := r.AppendSemantics(nil)
|
tree := r.AppendSemantics(nil)
|
||||||
got := tree[0].Desc
|
got := tree[0].Desc
|
||||||
+107
-220
@@ -1,18 +1,9 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
/*
|
// Package key implements key and text events and operations.
|
||||||
Package key implements key and text events and operations.
|
|
||||||
|
|
||||||
The InputOp operations is used for declaring key input handlers. Use
|
|
||||||
an implementation of the Queue interface from package ui to receive
|
|
||||||
events.
|
|
||||||
*/
|
|
||||||
package key
|
package key
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
@@ -21,59 +12,40 @@ import (
|
|||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InputOp declares a handler ready for key events.
|
// Filter matches any [Event] that matches the parameters.
|
||||||
// Key events are in general only delivered to the
|
type Filter struct {
|
||||||
// focused key handler.
|
// Focus is the tag that must be focused for the filter to match. It has no effect
|
||||||
type InputOp struct {
|
// if it is nil.
|
||||||
Tag event.Tag
|
Focus event.Tag
|
||||||
// Hint describes the type of text expected by Tag.
|
// Required is the set of modifiers that must be included in events matched.
|
||||||
Hint InputHint
|
Required Modifiers
|
||||||
// Keys is the set of keys Tag can handle. That is, Tag will only
|
// Optional is the set of modifiers that may be included in events matched.
|
||||||
// receive an Event if its key and modifiers are accepted by Keys.Contains.
|
Optional Modifiers
|
||||||
// As a special case, the topmost (first added) InputOp handler receives all
|
// Name of the key to be matched. As a special case, the empty
|
||||||
// unhandled events.
|
// Name matches every key not matched by any other filter.
|
||||||
Keys Set
|
Name Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set is an expression that describes a set of key combinations, in the form
|
// InputHintOp describes the type of text expected by a tag.
|
||||||
// "<modifiers>-<keyset>|...". Modifiers are separated by dashes, optional
|
type InputHintOp struct {
|
||||||
// modifiers are enclosed by parentheses. A key set is either a literal key
|
Tag event.Tag
|
||||||
// name or a list of key names separated by commas and enclosed in brackets.
|
Hint InputHint
|
||||||
//
|
}
|
||||||
// The "Short" modifier matches the shortcut modifier (ModShortcut) and
|
|
||||||
// "ShortAlt" matches the alternative modifier (ModShortcutAlt).
|
|
||||||
//
|
|
||||||
// Examples:
|
|
||||||
//
|
|
||||||
// - A|B matches the A and B keys
|
|
||||||
// - [A,B] also matches the A and B keys
|
|
||||||
// - Shift-A matches A key if shift is pressed, and no other modifier.
|
|
||||||
// - Shift-(Ctrl)-A matches A if shift is pressed, and optionally ctrl.
|
|
||||||
type Set string
|
|
||||||
|
|
||||||
// SoftKeyboardOp shows or hide the on-screen keyboard, if available.
|
// SoftKeyboardCmd shows or hides the on-screen keyboard, if available.
|
||||||
// It replaces any previous SoftKeyboardOp.
|
type SoftKeyboardCmd struct {
|
||||||
type SoftKeyboardOp struct {
|
|
||||||
Show bool
|
Show bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// FocusOp sets or clears the keyboard focus. It replaces any previous
|
// SelectionCmd updates the selection for an input handler.
|
||||||
// FocusOp in the same frame.
|
type SelectionCmd struct {
|
||||||
type FocusOp struct {
|
|
||||||
// Tag is the new focus. The focus is cleared if Tag is nil, or if Tag
|
|
||||||
// has no InputOp in the same frame.
|
|
||||||
Tag event.Tag
|
|
||||||
}
|
|
||||||
|
|
||||||
// SelectionOp updates the selection for an input handler.
|
|
||||||
type SelectionOp struct {
|
|
||||||
Tag event.Tag
|
Tag event.Tag
|
||||||
Range
|
Range
|
||||||
Caret
|
Caret
|
||||||
}
|
}
|
||||||
|
|
||||||
// SnippetOp updates the content snippet for an input handler.
|
// SnippetCmd updates the content snippet for an input handler.
|
||||||
type SnippetOp struct {
|
type SnippetCmd struct {
|
||||||
Tag event.Tag
|
Tag event.Tag
|
||||||
Snippet
|
Snippet
|
||||||
}
|
}
|
||||||
@@ -118,11 +90,8 @@ type FocusEvent struct {
|
|||||||
// An Event is generated when a key is pressed. For text input
|
// An Event is generated when a key is pressed. For text input
|
||||||
// use EditEvent.
|
// use EditEvent.
|
||||||
type Event struct {
|
type Event struct {
|
||||||
// Name of the key. For letters, the upper case form is used, via
|
// Name of the key.
|
||||||
// unicode.ToUpper. The shift modifier is taken into account, all other
|
Name Name
|
||||||
// modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1"
|
|
||||||
// combinations both give the Name "!" with the US keyboard layout.
|
|
||||||
Name string
|
|
||||||
// Modifiers is the set of active modifiers when the key was pressed.
|
// Modifiers is the set of active modifiers when the key was pressed.
|
||||||
Modifiers Modifiers
|
Modifiers Modifiers
|
||||||
// State is the state of the key when the event was fired.
|
// State is the state of the key when the event was fired.
|
||||||
@@ -136,6 +105,13 @@ type EditEvent struct {
|
|||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FocusFilter matches any [FocusEvent], [EditEvent], [SnippetEvent],
|
||||||
|
// or [SelectionEvent] with the specified target.
|
||||||
|
type FocusFilter struct {
|
||||||
|
// Target is a tag specified in a previous event.Op.
|
||||||
|
Target event.Tag
|
||||||
|
}
|
||||||
|
|
||||||
// InputHint changes the on-screen-keyboard type. That hints the
|
// InputHint changes the on-screen-keyboard type. That hints the
|
||||||
// type of data that might be entered by the user.
|
// type of data that might be entered by the user.
|
||||||
type InputHint uint8
|
type InputHint uint8
|
||||||
@@ -153,6 +129,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.
|
||||||
@@ -187,41 +165,60 @@ const (
|
|||||||
ModSuper
|
ModSuper
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Name is the identifier for a keyboard key.
|
||||||
|
//
|
||||||
|
// For letters, the upper case form is used, via unicode.ToUpper.
|
||||||
|
// The shift modifier is taken into account, all other
|
||||||
|
// modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1"
|
||||||
|
// combinations both give the Name "!" with the US keyboard layout.
|
||||||
|
type Name string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Names for special keys.
|
// Names for special keys.
|
||||||
NameLeftArrow = "←"
|
NameLeftArrow Name = "←"
|
||||||
NameRightArrow = "→"
|
NameRightArrow Name = "→"
|
||||||
NameUpArrow = "↑"
|
NameUpArrow Name = "↑"
|
||||||
NameDownArrow = "↓"
|
NameDownArrow Name = "↓"
|
||||||
NameReturn = "⏎"
|
NameReturn Name = "⏎"
|
||||||
NameEnter = "⌤"
|
NameEnter Name = "⌤"
|
||||||
NameEscape = "⎋"
|
NameEscape Name = "⎋"
|
||||||
NameHome = "⇱"
|
NameHome Name = "⇱"
|
||||||
NameEnd = "⇲"
|
NameEnd Name = "⇲"
|
||||||
NameDeleteBackward = "⌫"
|
NameDeleteBackward Name = "⌫"
|
||||||
NameDeleteForward = "⌦"
|
NameDeleteForward Name = "⌦"
|
||||||
NamePageUp = "⇞"
|
NamePageUp Name = "⇞"
|
||||||
NamePageDown = "⇟"
|
NamePageDown Name = "⇟"
|
||||||
NameTab = "Tab"
|
NameTab Name = "Tab"
|
||||||
NameSpace = "Space"
|
NameSpace Name = "Space"
|
||||||
NameCtrl = "Ctrl"
|
NameCtrl Name = "Ctrl"
|
||||||
NameShift = "Shift"
|
NameShift Name = "Shift"
|
||||||
NameAlt = "Alt"
|
NameAlt Name = "Alt"
|
||||||
NameSuper = "Super"
|
NameSuper Name = "Super"
|
||||||
NameCommand = "⌘"
|
NameCommand Name = "⌘"
|
||||||
NameF1 = "F1"
|
NameF1 Name = "F1"
|
||||||
NameF2 = "F2"
|
NameF2 Name = "F2"
|
||||||
NameF3 = "F3"
|
NameF3 Name = "F3"
|
||||||
NameF4 = "F4"
|
NameF4 Name = "F4"
|
||||||
NameF5 = "F5"
|
NameF5 Name = "F5"
|
||||||
NameF6 = "F6"
|
NameF6 Name = "F6"
|
||||||
NameF7 = "F7"
|
NameF7 Name = "F7"
|
||||||
NameF8 = "F8"
|
NameF8 Name = "F8"
|
||||||
NameF9 = "F9"
|
NameF9 Name = "F9"
|
||||||
NameF10 = "F10"
|
NameF10 Name = "F10"
|
||||||
NameF11 = "F11"
|
NameF11 Name = "F11"
|
||||||
NameF12 = "F12"
|
NameF12 Name = "F12"
|
||||||
NameBack = "Back"
|
NameBack Name = "Back"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FocusDirection int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FocusRight FocusDirection = iota
|
||||||
|
FocusLeft
|
||||||
|
FocusUp
|
||||||
|
FocusDown
|
||||||
|
FocusForward
|
||||||
|
FocusBackward
|
||||||
)
|
)
|
||||||
|
|
||||||
// Contain reports whether m contains all modifiers
|
// Contain reports whether m contains all modifiers
|
||||||
@@ -230,162 +227,52 @@ func (m Modifiers) Contain(m2 Modifiers) bool {
|
|||||||
return m&m2 == m2
|
return m&m2 == m2
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k Set) Contains(name string, mods Modifiers) bool {
|
// FocusCmd requests to set or clear the keyboard focus.
|
||||||
ks := string(k)
|
type FocusCmd struct {
|
||||||
for len(ks) > 0 {
|
// Tag is the new focus. The focus is cleared if Tag is nil, or if Tag
|
||||||
// Cut next key expression.
|
// has no [event.Op] references.
|
||||||
chord, rest, _ := cut(ks, "|")
|
Tag event.Tag
|
||||||
ks = rest
|
|
||||||
// Separate key set and modifier set.
|
|
||||||
var modSet, keySet string
|
|
||||||
sep := strings.LastIndex(chord, "-")
|
|
||||||
if sep != -1 {
|
|
||||||
modSet, keySet = chord[:sep], chord[sep+1:]
|
|
||||||
} else {
|
|
||||||
modSet, keySet = "", chord
|
|
||||||
}
|
|
||||||
if !keySetContains(keySet, name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if modSetContains(modSet, mods) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func keySetContains(keySet, name string) bool {
|
func (h InputHintOp) Add(o *op.Ops) {
|
||||||
// Check for single key match.
|
|
||||||
if keySet == name {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// Check for set match.
|
|
||||||
if len(keySet) < 2 || keySet[0] != '[' || keySet[len(keySet)-1] != ']' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
keySet = keySet[1 : len(keySet)-1]
|
|
||||||
for len(keySet) > 0 {
|
|
||||||
key, rest, _ := cut(keySet, ",")
|
|
||||||
keySet = rest
|
|
||||||
if key == name {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func modSetContains(modSet string, mods Modifiers) bool {
|
|
||||||
var smods Modifiers
|
|
||||||
for len(modSet) > 0 {
|
|
||||||
mod, rest, _ := cut(modSet, "-")
|
|
||||||
modSet = rest
|
|
||||||
if len(mod) >= 2 && mod[0] == '(' && mod[len(mod)-1] == ')' {
|
|
||||||
mods &^= modFor(mod[1 : len(mod)-1])
|
|
||||||
} else {
|
|
||||||
smods |= modFor(mod)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mods == smods
|
|
||||||
}
|
|
||||||
|
|
||||||
// cut is a copy of the standard library strings.Cut.
|
|
||||||
// TODO: remove when Go 1.18 is our minimum.
|
|
||||||
func cut(s, sep string) (before, after string, found bool) {
|
|
||||||
if i := strings.Index(s, sep); i >= 0 {
|
|
||||||
return s[:i], s[i+len(sep):], true
|
|
||||||
}
|
|
||||||
return s, "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func modFor(name string) Modifiers {
|
|
||||||
switch name {
|
|
||||||
case NameCtrl:
|
|
||||||
return ModCtrl
|
|
||||||
case NameShift:
|
|
||||||
return ModShift
|
|
||||||
case NameAlt:
|
|
||||||
return ModAlt
|
|
||||||
case NameSuper:
|
|
||||||
return ModSuper
|
|
||||||
case NameCommand:
|
|
||||||
return ModCommand
|
|
||||||
case "Short":
|
|
||||||
return ModShortcut
|
|
||||||
case "ShortAlt":
|
|
||||||
return ModShortcutAlt
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Write1(&o.Internal, ops.TypeKeyInputHintLen, h.Tag)
|
||||||
data := ops.Write2(&o.Internal, ops.TypeKeyInputLen, h.Tag, &filter)
|
data[0] = byte(ops.TypeKeyInputHint)
|
||||||
data[0] = byte(ops.TypeKeyInput)
|
|
||||||
data[1] = byte(h.Hint)
|
data[1] = byte(h.Hint)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h SoftKeyboardOp) Add(o *op.Ops) {
|
|
||||||
data := ops.Write(&o.Internal, ops.TypeKeySoftKeyboardLen)
|
|
||||||
data[0] = byte(ops.TypeKeySoftKeyboard)
|
|
||||||
if h.Show {
|
|
||||||
data[1] = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h FocusOp) Add(o *op.Ops) {
|
|
||||||
data := ops.Write1(&o.Internal, ops.TypeKeyFocusLen, h.Tag)
|
|
||||||
data[0] = byte(ops.TypeKeyFocus)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SnippetOp) Add(o *op.Ops) {
|
|
||||||
data := ops.Write2(&o.Internal, ops.TypeSnippetLen, s.Tag, &s.Text)
|
|
||||||
data[0] = byte(ops.TypeSnippet)
|
|
||||||
bo := binary.LittleEndian
|
|
||||||
bo.PutUint32(data[1:], uint32(s.Range.Start))
|
|
||||||
bo.PutUint32(data[5:], uint32(s.Range.End))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SelectionOp) Add(o *op.Ops) {
|
|
||||||
data := ops.Write1(&o.Internal, ops.TypeSelectionLen, s.Tag)
|
|
||||||
data[0] = byte(ops.TypeSelection)
|
|
||||||
bo := binary.LittleEndian
|
|
||||||
bo.PutUint32(data[1:], uint32(s.Start))
|
|
||||||
bo.PutUint32(data[5:], uint32(s.End))
|
|
||||||
bo.PutUint32(data[9:], math.Float32bits(s.Pos.X))
|
|
||||||
bo.PutUint32(data[13:], math.Float32bits(s.Pos.Y))
|
|
||||||
bo.PutUint32(data[17:], math.Float32bits(s.Ascent))
|
|
||||||
bo.PutUint32(data[21:], math.Float32bits(s.Descent))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (EditEvent) ImplementsEvent() {}
|
func (EditEvent) ImplementsEvent() {}
|
||||||
func (Event) ImplementsEvent() {}
|
func (Event) ImplementsEvent() {}
|
||||||
func (FocusEvent) ImplementsEvent() {}
|
func (FocusEvent) ImplementsEvent() {}
|
||||||
func (SnippetEvent) ImplementsEvent() {}
|
func (SnippetEvent) ImplementsEvent() {}
|
||||||
func (SelectionEvent) ImplementsEvent() {}
|
func (SelectionEvent) ImplementsEvent() {}
|
||||||
|
|
||||||
func (e Event) String() string {
|
func (FocusCmd) ImplementsCommand() {}
|
||||||
return fmt.Sprintf("%v %v %v}", e.Name, e.Modifiers, e.State)
|
func (SoftKeyboardCmd) ImplementsCommand() {}
|
||||||
}
|
func (SelectionCmd) ImplementsCommand() {}
|
||||||
|
func (SnippetCmd) ImplementsCommand() {}
|
||||||
|
|
||||||
|
func (Filter) ImplementsFilter() {}
|
||||||
|
func (FocusFilter) ImplementsFilter() {}
|
||||||
|
|
||||||
func (m Modifiers) String() string {
|
func (m Modifiers) String() string {
|
||||||
var strs []string
|
var strs []string
|
||||||
if m.Contain(ModCtrl) {
|
if m.Contain(ModCtrl) {
|
||||||
strs = append(strs, NameCtrl)
|
strs = append(strs, string(NameCtrl))
|
||||||
}
|
}
|
||||||
if m.Contain(ModCommand) {
|
if m.Contain(ModCommand) {
|
||||||
strs = append(strs, NameCommand)
|
strs = append(strs, string(NameCommand))
|
||||||
}
|
}
|
||||||
if m.Contain(ModShift) {
|
if m.Contain(ModShift) {
|
||||||
strs = append(strs, NameShift)
|
strs = append(strs, string(NameShift))
|
||||||
}
|
}
|
||||||
if m.Contain(ModAlt) {
|
if m.Contain(ModAlt) {
|
||||||
strs = append(strs, NameAlt)
|
strs = append(strs, string(NameAlt))
|
||||||
}
|
}
|
||||||
if m.Contain(ModSuper) {
|
if m.Contain(ModSuper) {
|
||||||
strs = append(strs, NameSuper)
|
strs = append(strs, string(NameSuper))
|
||||||
}
|
}
|
||||||
return strings.Join(strs, "-")
|
return strings.Join(strs, "-")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
|
||||||
|
|
||||||
package key
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestKeySet(t *testing.T) {
|
|
||||||
const allMods = ModAlt | ModShift | ModSuper | ModCtrl | ModCommand
|
|
||||||
tests := []struct {
|
|
||||||
Set Set
|
|
||||||
Matches []Event
|
|
||||||
Mismatches []Event
|
|
||||||
}{
|
|
||||||
{"A", []Event{{Name: "A"}}, []Event{{Name: "A", Modifiers: ModShift}}},
|
|
||||||
{"[A,B,C]", []Event{{Name: "A"}, {Name: "B"}}, []Event{}},
|
|
||||||
{"Short-A", []Event{{Name: "A", Modifiers: ModShortcut}}, []Event{{Name: "A", Modifiers: ModShift}}},
|
|
||||||
{"(Ctrl)-A", []Event{{Name: "A", Modifiers: ModCtrl}, {Name: "A"}}, []Event{{Name: "A", Modifiers: ModShift}}},
|
|
||||||
{"Shift-[A,B,C]", []Event{{Name: "A", Modifiers: ModShift}}, []Event{{Name: "B", Modifiers: ModShift | ModCtrl}}},
|
|
||||||
{Set(allMods.String() + "-A"), []Event{{Name: "A", Modifiers: allMods}}, []Event{}},
|
|
||||||
}
|
|
||||||
for _, tst := range tests {
|
|
||||||
for _, e := range tst.Matches {
|
|
||||||
if !tst.Set.Contains(e.Name, e.Modifiers) {
|
|
||||||
t.Errorf("key set %q didn't contain %+v", tst.Set, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, e := range tst.Mismatches {
|
|
||||||
if tst.Set.Contains(e.Name, e.Modifiers) {
|
|
||||||
t.Errorf("key set %q contains %+v", tst.Set, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+7
-24
@@ -5,36 +5,19 @@ Package pointer implements pointer events and operations.
|
|||||||
A pointer is either a mouse controlled cursor or a touch
|
A pointer is either a mouse controlled cursor or a touch
|
||||||
object such as a finger.
|
object such as a finger.
|
||||||
|
|
||||||
The InputOp operation is used to declare a handler ready for pointer
|
The [event.Op] operation is used to declare a handler ready for pointer
|
||||||
events. Use an event.Queue to receive events.
|
events.
|
||||||
|
|
||||||
# Types
|
|
||||||
|
|
||||||
Only events that match a specified list of types are delivered to a handler.
|
|
||||||
|
|
||||||
For example, to receive Press, Drag, and Release events (but not Move, Enter,
|
|
||||||
Leave, or Scroll):
|
|
||||||
|
|
||||||
var ops op.Ops
|
|
||||||
var h *Handler = ...
|
|
||||||
|
|
||||||
pointer.InputOp{
|
|
||||||
Tag: h,
|
|
||||||
Types: pointer.Press | pointer.Drag | pointer.Release,
|
|
||||||
}.Add(ops)
|
|
||||||
|
|
||||||
Cancel events are always delivered.
|
|
||||||
|
|
||||||
# Hit areas
|
# Hit areas
|
||||||
|
|
||||||
Clip operations from package op/clip are used for specifying
|
Clip operations from package [op/clip] are used for specifying
|
||||||
hit areas where subsequent InputOps are active.
|
hit areas where handlers may receive events.
|
||||||
|
|
||||||
For example, to set up a handler with a rectangular hit area:
|
For example, to set up a handler with a rectangular hit area:
|
||||||
|
|
||||||
r := image.Rectangle{...}
|
r := image.Rectangle{...}
|
||||||
area := clip.Rect(r).Push(ops)
|
area := clip.Rect(r).Push(ops)
|
||||||
pointer.InputOp{Tag: h}.Add(ops)
|
event.Op{Tag: h}.Add(ops)
|
||||||
area.Pop()
|
area.Pop()
|
||||||
|
|
||||||
Note that hit areas behave similar to painting: the effective area of a stack
|
Note that hit areas behave similar to painting: the effective area of a stack
|
||||||
@@ -54,11 +37,11 @@ For example:
|
|||||||
var h1, h2 *Handler
|
var h1, h2 *Handler
|
||||||
|
|
||||||
area := clip.Rect(...).Push(ops)
|
area := clip.Rect(...).Push(ops)
|
||||||
pointer.InputOp{Tag: h1}.Add(Ops)
|
event.Op{Tag: h1}.Add(Ops)
|
||||||
area.Pop()
|
area.Pop()
|
||||||
|
|
||||||
area := clip.Rect(...).Push(ops)
|
area := clip.Rect(...).Push(ops)
|
||||||
pointer.InputOp{Tag: h2}.Add(ops)
|
event.Op{Tag: h2}.Add(ops)
|
||||||
area.Pop()
|
area.Pop()
|
||||||
|
|
||||||
implies a tree of two inner nodes, each with one pointer handler attached.
|
implies a tree of two inner nodes, each with one pointer handler attached.
|
||||||
|
|||||||
+29
-45
@@ -3,8 +3,6 @@
|
|||||||
package pointer
|
package pointer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
"image"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -18,7 +16,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 +30,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,18 +51,16 @@ 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
|
// Filter matches every [Event] that target the Tag and whose kind is
|
||||||
// events.
|
// included in Kinds. Note that only tags specified in [event.Op] can
|
||||||
type InputOp struct {
|
// be targeted by pointer events.
|
||||||
Tag event.Tag
|
type Filter struct {
|
||||||
// Grab, if set, request that the handler get
|
Target event.Tag
|
||||||
// Grabbed priority.
|
// Kinds is a bitwise-or of event types to match.
|
||||||
Grab bool
|
Kinds Kind
|
||||||
// Types is a bitwise-or of event types to receive.
|
|
||||||
Types Type
|
|
||||||
// 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
|
||||||
//
|
//
|
||||||
@@ -71,10 +69,16 @@ type InputOp struct {
|
|||||||
ScrollBounds image.Rectangle
|
ScrollBounds image.Rectangle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GrabCmd requests a pointer grab on the pointer identified by ID.
|
||||||
|
type GrabCmd struct {
|
||||||
|
Tag event.Tag
|
||||||
|
ID ID
|
||||||
|
}
|
||||||
|
|
||||||
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 +173,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
|
||||||
// Press of a pointer.
|
// Press of a pointer.
|
||||||
Press
|
Press
|
||||||
// Release of a pointer.
|
// Release of a pointer.
|
||||||
@@ -235,36 +239,12 @@ func (op Cursor) Add(o *op.Ops) {
|
|||||||
data[1] = byte(op)
|
data[1] = byte(op)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add panics if the scroll range does not contain zero.
|
func (t Kind) String() string {
|
||||||
func (op InputOp) Add(o *op.Ops) {
|
|
||||||
if op.Tag == nil {
|
|
||||||
panic("Tag must be non-nil")
|
|
||||||
}
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
if op.Types>>16 > 0 {
|
|
||||||
panic(fmt.Errorf("value in Types overflows uint16"))
|
|
||||||
}
|
|
||||||
data := ops.Write1(&o.Internal, ops.TypePointerInputLen, op.Tag)
|
|
||||||
data[0] = byte(ops.TypePointerInput)
|
|
||||||
if op.Grab {
|
|
||||||
data[1] = 1
|
|
||||||
}
|
|
||||||
bo := binary.LittleEndian
|
|
||||||
bo.PutUint16(data[2:], uint16(op.Types))
|
|
||||||
bo.PutUint32(data[4:], uint32(op.ScrollBounds.Min.X))
|
|
||||||
bo.PutUint32(data[8:], uint32(op.ScrollBounds.Min.Y))
|
|
||||||
bo.PutUint32(data[12:], uint32(op.ScrollBounds.Max.X))
|
|
||||||
bo.PutUint32(data[16:], uint32(op.ScrollBounds.Max.Y))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t Type) 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 +255,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"
|
||||||
@@ -402,3 +382,7 @@ func (c Cursor) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (Event) ImplementsEvent() {}
|
func (Event) ImplementsEvent() {}
|
||||||
|
|
||||||
|
func (GrabCmd) ImplementsCommand() {}
|
||||||
|
|
||||||
|
func (Filter) ImplementsFilter() {}
|
||||||
|
|||||||
@@ -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"},
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
|
||||||
|
|
||||||
// Package profiles provides access to rendering
|
|
||||||
// profiles.
|
|
||||||
package profile
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gioui.org/internal/ops"
|
|
||||||
"gioui.org/io/event"
|
|
||||||
"gioui.org/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Op registers a handler for receiving
|
|
||||||
// Events.
|
|
||||||
type Op struct {
|
|
||||||
Tag event.Tag
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event contains profile data from a single
|
|
||||||
// rendered frame.
|
|
||||||
type Event struct {
|
|
||||||
// Timings. Very likely to change.
|
|
||||||
Timings string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Op) Add(o *op.Ops) {
|
|
||||||
data := ops.Write1(&o.Internal, ops.TypeProfileLen, p.Tag)
|
|
||||||
data[0] = byte(ops.TypeProfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Event) ImplementsEvent() {}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
|
||||||
|
|
||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gioui.org/io/event"
|
|
||||||
)
|
|
||||||
|
|
||||||
type clipboardQueue struct {
|
|
||||||
receivers map[event.Tag]struct{}
|
|
||||||
// request avoid read clipboard every frame while waiting.
|
|
||||||
requested bool
|
|
||||||
text *string
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteClipboard returns the most recent text to be copied
|
|
||||||
// to the clipboard, if any.
|
|
||||||
func (q *clipboardQueue) WriteClipboard() (string, bool) {
|
|
||||||
if q.text == nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
text := *q.text
|
|
||||||
q.text = nil
|
|
||||||
return text, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadClipboard reports if any new handler is waiting
|
|
||||||
// to read the clipboard.
|
|
||||||
func (q *clipboardQueue) ReadClipboard() bool {
|
|
||||||
if len(q.receivers) == 0 || q.requested {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
q.requested = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) {
|
|
||||||
for r := range q.receivers {
|
|
||||||
events.Add(r, e)
|
|
||||||
delete(q.receivers, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *clipboardQueue) ProcessWriteClipboard(refs []interface{}) {
|
|
||||||
q.text = refs[0].(*string)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *clipboardQueue) ProcessReadClipboard(refs []interface{}) {
|
|
||||||
if q.receivers == nil {
|
|
||||||
q.receivers = make(map[event.Tag]struct{})
|
|
||||||
}
|
|
||||||
tag := refs[0].(event.Tag)
|
|
||||||
if _, ok := q.receivers[tag]; !ok {
|
|
||||||
q.receivers[tag] = struct{}{}
|
|
||||||
q.requested = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gioui.org/io/clipboard"
|
|
||||||
"gioui.org/io/event"
|
|
||||||
"gioui.org/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestClipboardDuplicateEvent(t *testing.T) {
|
|
||||||
ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
|
|
||||||
|
|
||||||
// Both must receive the event once
|
|
||||||
clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
|
|
||||||
clipboard.ReadOp{Tag: &handler[1]}.Add(ops)
|
|
||||||
|
|
||||||
router.Frame(ops)
|
|
||||||
event := clipboard.Event{Text: "Test"}
|
|
||||||
router.Queue(event)
|
|
||||||
assertClipboardReadOp(t, router, 0)
|
|
||||||
assertClipboardEvent(t, router.Events(&handler[0]), true)
|
|
||||||
assertClipboardEvent(t, router.Events(&handler[1]), true)
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
// No ReadOp
|
|
||||||
|
|
||||||
router.Frame(ops)
|
|
||||||
assertClipboardReadOp(t, router, 0)
|
|
||||||
assertClipboardEvent(t, router.Events(&handler[0]), false)
|
|
||||||
assertClipboardEvent(t, router.Events(&handler[1]), false)
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
|
|
||||||
|
|
||||||
router.Frame(ops)
|
|
||||||
// No ClipboardEvent sent
|
|
||||||
assertClipboardReadOp(t, router, 1)
|
|
||||||
assertClipboardEvent(t, router.Events(&handler[0]), false)
|
|
||||||
assertClipboardEvent(t, router.Events(&handler[1]), false)
|
|
||||||
ops.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQueueProcessReadClipboard(t *testing.T) {
|
|
||||||
ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
// Request read
|
|
||||||
clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
|
|
||||||
|
|
||||||
router.Frame(ops)
|
|
||||||
assertClipboardReadOp(t, router, 1)
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
// No ReadOp
|
|
||||||
// One receiver must still wait for response
|
|
||||||
|
|
||||||
router.Frame(ops)
|
|
||||||
assertClipboardReadOpDuplicated(t, router, 1)
|
|
||||||
ops.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
router.Frame(ops)
|
|
||||||
// Send the clipboard event
|
|
||||||
event := clipboard.Event{Text: "Text 2"}
|
|
||||||
router.Queue(event)
|
|
||||||
assertClipboardReadOp(t, router, 0)
|
|
||||||
assertClipboardEvent(t, router.Events(&handler[0]), true)
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
// No ReadOp
|
|
||||||
// There's no receiver waiting
|
|
||||||
|
|
||||||
router.Frame(ops)
|
|
||||||
assertClipboardReadOp(t, router, 0)
|
|
||||||
assertClipboardEvent(t, router.Events(&handler[0]), false)
|
|
||||||
ops.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQueueProcessWriteClipboard(t *testing.T) {
|
|
||||||
ops, router := new(op.Ops), new(Router)
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
clipboard.WriteOp{Text: "Write 1"}.Add(ops)
|
|
||||||
|
|
||||||
router.Frame(ops)
|
|
||||||
assertClipboardWriteOp(t, router, "Write 1")
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
// No WriteOp
|
|
||||||
|
|
||||||
router.Frame(ops)
|
|
||||||
assertClipboardWriteOp(t, router, "")
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
clipboard.WriteOp{Text: "Write 2"}.Add(ops)
|
|
||||||
|
|
||||||
router.Frame(ops)
|
|
||||||
assertClipboardReadOp(t, router, 0)
|
|
||||||
assertClipboardWriteOp(t, router, "Write 2")
|
|
||||||
ops.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) {
|
|
||||||
t.Helper()
|
|
||||||
var evtClipboard int
|
|
||||||
for _, e := range events {
|
|
||||||
switch e.(type) {
|
|
||||||
case clipboard.Event:
|
|
||||||
evtClipboard++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if evtClipboard <= 0 && expected {
|
|
||||||
t.Error("expected to receive some event")
|
|
||||||
}
|
|
||||||
if evtClipboard > 0 && !expected {
|
|
||||||
t.Error("unexpected event received")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertClipboardReadOp(t *testing.T, router *Router, expected int) {
|
|
||||||
t.Helper()
|
|
||||||
if len(router.cqueue.receivers) != expected {
|
|
||||||
t.Error("unexpected number of receivers")
|
|
||||||
}
|
|
||||||
if router.cqueue.ReadClipboard() != (expected > 0) {
|
|
||||||
t.Error("missing requests")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertClipboardReadOpDuplicated(t *testing.T, router *Router, expected int) {
|
|
||||||
t.Helper()
|
|
||||||
if len(router.cqueue.receivers) != expected {
|
|
||||||
t.Error("receivers removed")
|
|
||||||
}
|
|
||||||
if router.cqueue.ReadClipboard() != false {
|
|
||||||
t.Error("duplicated requests")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertClipboardWriteOp(t *testing.T, router *Router, expected string) {
|
|
||||||
t.Helper()
|
|
||||||
if (router.cqueue.text != nil) != (expected != "") {
|
|
||||||
t.Error("text not defined")
|
|
||||||
}
|
|
||||||
text, ok := router.cqueue.WriteClipboard()
|
|
||||||
if ok != (expected != "") {
|
|
||||||
t.Error("duplicated requests")
|
|
||||||
}
|
|
||||||
if text != expected {
|
|
||||||
t.Errorf("got text %s, expected %s", text, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
|
||||||
|
|
||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"gioui.org/f32"
|
|
||||||
"gioui.org/io/event"
|
|
||||||
"gioui.org/io/key"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EditorState represents the state of an editor needed by input handlers.
|
|
||||||
type EditorState struct {
|
|
||||||
Selection struct {
|
|
||||||
Transform f32.Affine2D
|
|
||||||
key.Range
|
|
||||||
key.Caret
|
|
||||||
}
|
|
||||||
Snippet key.Snippet
|
|
||||||
}
|
|
||||||
|
|
||||||
type TextInputState uint8
|
|
||||||
|
|
||||||
type keyQueue struct {
|
|
||||||
focus event.Tag
|
|
||||||
order []event.Tag
|
|
||||||
dirOrder []dirFocusEntry
|
|
||||||
handlers map[event.Tag]*keyHandler
|
|
||||||
state TextInputState
|
|
||||||
hint key.InputHint
|
|
||||||
content EditorState
|
|
||||||
}
|
|
||||||
|
|
||||||
type keyHandler struct {
|
|
||||||
// visible will be true if the InputOp is present
|
|
||||||
// in the current frame.
|
|
||||||
visible bool
|
|
||||||
new bool
|
|
||||||
hint key.InputHint
|
|
||||||
order int
|
|
||||||
dirOrder int
|
|
||||||
filter key.Set
|
|
||||||
}
|
|
||||||
|
|
||||||
// keyCollector tracks state required to update a keyQueue
|
|
||||||
// from key ops.
|
|
||||||
type keyCollector struct {
|
|
||||||
q *keyQueue
|
|
||||||
focus event.Tag
|
|
||||||
changed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type dirFocusEntry struct {
|
|
||||||
tag event.Tag
|
|
||||||
row int
|
|
||||||
area int
|
|
||||||
bounds image.Rectangle
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
TextInputKeep TextInputState = iota
|
|
||||||
TextInputClose
|
|
||||||
TextInputOpen
|
|
||||||
)
|
|
||||||
|
|
||||||
type FocusDirection int
|
|
||||||
|
|
||||||
const (
|
|
||||||
FocusRight FocusDirection = iota
|
|
||||||
FocusLeft
|
|
||||||
FocusUp
|
|
||||||
FocusDown
|
|
||||||
FocusForward
|
|
||||||
FocusBackward
|
|
||||||
)
|
|
||||||
|
|
||||||
// InputState returns the last text input state as
|
|
||||||
// determined in Frame.
|
|
||||||
func (q *keyQueue) InputState() TextInputState {
|
|
||||||
state := q.state
|
|
||||||
q.state = TextInputKeep
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
// InputHint returns the input mode from the most recent key.InputOp.
|
|
||||||
func (q *keyQueue) InputHint() (key.InputHint, bool) {
|
|
||||||
if q.focus == nil {
|
|
||||||
return q.hint, false
|
|
||||||
}
|
|
||||||
focused, ok := q.handlers[q.focus]
|
|
||||||
if !ok {
|
|
||||||
return q.hint, false
|
|
||||||
}
|
|
||||||
old := q.hint
|
|
||||||
q.hint = focused.hint
|
|
||||||
return q.hint, old != q.hint
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *keyQueue) Reset() {
|
|
||||||
if q.handlers == nil {
|
|
||||||
q.handlers = make(map[event.Tag]*keyHandler)
|
|
||||||
}
|
|
||||||
for _, h := range q.handlers {
|
|
||||||
h.visible, h.new = false, false
|
|
||||||
h.order = -1
|
|
||||||
}
|
|
||||||
q.order = q.order[:0]
|
|
||||||
q.dirOrder = q.dirOrder[:0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *keyQueue) Frame(events *handlerEvents, collector keyCollector) {
|
|
||||||
changed, focus := collector.changed, collector.focus
|
|
||||||
for k, h := range q.handlers {
|
|
||||||
if !h.visible {
|
|
||||||
delete(q.handlers, k)
|
|
||||||
if q.focus == k {
|
|
||||||
// Remove focus from the handler that is no longer visible.
|
|
||||||
q.focus = nil
|
|
||||||
q.state = TextInputClose
|
|
||||||
}
|
|
||||||
} else if h.new && k != focus {
|
|
||||||
// Reset the handler on (each) first appearance, but don't trigger redraw.
|
|
||||||
events.AddNoRedraw(k, key.FocusEvent{Focus: false})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if changed {
|
|
||||||
q.setFocus(focus, events)
|
|
||||||
}
|
|
||||||
q.updateFocusLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateFocusLayout partitions input handlers handlers into rows
|
|
||||||
// for directional focus moves.
|
|
||||||
//
|
|
||||||
// The approach is greedy: pick the topmost handler and create a row
|
|
||||||
// containing it. Then, extend the handler bounds to a horizontal beam
|
|
||||||
// and add to the row every handler whose center intersect it. Repeat
|
|
||||||
// until no handlers remain.
|
|
||||||
func (q *keyQueue) updateFocusLayout() {
|
|
||||||
order := q.dirOrder
|
|
||||||
// Sort by ascending y position.
|
|
||||||
sort.SliceStable(order, func(i, j int) bool {
|
|
||||||
return order[i].bounds.Min.Y < order[j].bounds.Min.Y
|
|
||||||
})
|
|
||||||
row := 0
|
|
||||||
for len(order) > 0 {
|
|
||||||
h := &order[0]
|
|
||||||
h.row = row
|
|
||||||
bottom := h.bounds.Max.Y
|
|
||||||
end := 1
|
|
||||||
for ; end < len(order); end++ {
|
|
||||||
h := &order[end]
|
|
||||||
center := (h.bounds.Min.Y + h.bounds.Max.Y) / 2
|
|
||||||
if center > bottom {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
h.row = row
|
|
||||||
}
|
|
||||||
// Sort row by ascending x position.
|
|
||||||
sort.SliceStable(order[:end], func(i, j int) bool {
|
|
||||||
return order[i].bounds.Min.X < order[j].bounds.Min.X
|
|
||||||
})
|
|
||||||
order = order[end:]
|
|
||||||
row++
|
|
||||||
}
|
|
||||||
for i, o := range q.dirOrder {
|
|
||||||
q.handlers[o.tag].dirOrder = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MoveFocus attempts to move the focus in the direction of dir, returning true if it succeeds.
|
|
||||||
func (q *keyQueue) MoveFocus(dir FocusDirection, events *handlerEvents) bool {
|
|
||||||
if len(q.dirOrder) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
order := 0
|
|
||||||
if q.focus != nil {
|
|
||||||
order = q.handlers[q.focus].dirOrder
|
|
||||||
}
|
|
||||||
focus := q.dirOrder[order]
|
|
||||||
switch dir {
|
|
||||||
case FocusForward, FocusBackward:
|
|
||||||
if len(q.order) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
order := 0
|
|
||||||
if dir == FocusBackward {
|
|
||||||
order = -1
|
|
||||||
}
|
|
||||||
if q.focus != nil {
|
|
||||||
order = q.handlers[q.focus].order
|
|
||||||
if dir == FocusForward {
|
|
||||||
order++
|
|
||||||
} else {
|
|
||||||
order--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
order = (order + len(q.order)) % len(q.order)
|
|
||||||
q.setFocus(q.order[order], events)
|
|
||||||
return true
|
|
||||||
case FocusRight, FocusLeft:
|
|
||||||
next := order
|
|
||||||
if q.focus != nil {
|
|
||||||
next = order + 1
|
|
||||||
if dir == FocusLeft {
|
|
||||||
next = order - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if 0 <= next && next < len(q.dirOrder) {
|
|
||||||
newFocus := q.dirOrder[next]
|
|
||||||
if newFocus.row == focus.row {
|
|
||||||
q.setFocus(newFocus.tag, events)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case FocusUp, FocusDown:
|
|
||||||
delta := +1
|
|
||||||
if dir == FocusUp {
|
|
||||||
delta = -1
|
|
||||||
}
|
|
||||||
nextRow := 0
|
|
||||||
if q.focus != nil {
|
|
||||||
nextRow = focus.row + delta
|
|
||||||
}
|
|
||||||
var closest event.Tag
|
|
||||||
dist := int(1e6)
|
|
||||||
center := (focus.bounds.Min.X + focus.bounds.Max.X) / 2
|
|
||||||
loop:
|
|
||||||
for 0 <= order && order < len(q.dirOrder) {
|
|
||||||
next := q.dirOrder[order]
|
|
||||||
switch next.row {
|
|
||||||
case nextRow:
|
|
||||||
nextCenter := (next.bounds.Min.X + next.bounds.Max.X) / 2
|
|
||||||
d := center - nextCenter
|
|
||||||
if d < 0 {
|
|
||||||
d = -d
|
|
||||||
}
|
|
||||||
if d > dist {
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
dist = d
|
|
||||||
closest = next.tag
|
|
||||||
case nextRow + delta:
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
order += delta
|
|
||||||
}
|
|
||||||
if closest != nil {
|
|
||||||
q.setFocus(closest, events)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *keyQueue) BoundsFor(t event.Tag) image.Rectangle {
|
|
||||||
order := q.handlers[t].dirOrder
|
|
||||||
return q.dirOrder[order].bounds
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *keyQueue) AreaFor(t event.Tag) int {
|
|
||||||
order := q.handlers[t].dirOrder
|
|
||||||
return q.dirOrder[order].area
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *keyQueue) Accepts(t event.Tag, e key.Event) bool {
|
|
||||||
return q.handlers[t].filter.Contains(e.Name, e.Modifiers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *keyQueue) setFocus(focus event.Tag, events *handlerEvents) {
|
|
||||||
if focus != nil {
|
|
||||||
if _, exists := q.handlers[focus]; !exists {
|
|
||||||
focus = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if focus == q.focus {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
q.content = EditorState{}
|
|
||||||
if q.focus != nil {
|
|
||||||
events.Add(q.focus, key.FocusEvent{Focus: false})
|
|
||||||
}
|
|
||||||
q.focus = focus
|
|
||||||
if q.focus != nil {
|
|
||||||
events.Add(q.focus, key.FocusEvent{Focus: true})
|
|
||||||
}
|
|
||||||
if q.focus == nil || q.state == TextInputKeep {
|
|
||||||
q.state = TextInputClose
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *keyCollector) focusOp(tag event.Tag) {
|
|
||||||
k.focus = tag
|
|
||||||
k.changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *keyCollector) softKeyboard(show bool) {
|
|
||||||
if show {
|
|
||||||
k.q.state = TextInputOpen
|
|
||||||
} else {
|
|
||||||
k.q.state = TextInputClose
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *keyCollector) handlerFor(tag event.Tag, area int, bounds image.Rectangle) *keyHandler {
|
|
||||||
h, ok := k.q.handlers[tag]
|
|
||||||
if !ok {
|
|
||||||
h = &keyHandler{new: true, order: -1}
|
|
||||||
k.q.handlers[tag] = h
|
|
||||||
}
|
|
||||||
if h.order == -1 {
|
|
||||||
h.order = len(k.q.order)
|
|
||||||
k.q.order = append(k.q.order, tag)
|
|
||||||
k.q.dirOrder = append(k.q.dirOrder, dirFocusEntry{tag: tag, area: area, bounds: bounds})
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *keyCollector) inputOp(op key.InputOp, area int, bounds image.Rectangle) {
|
|
||||||
h := k.handlerFor(op.Tag, area, bounds)
|
|
||||||
h.visible = true
|
|
||||||
h.hint = op.Hint
|
|
||||||
h.filter = op.Keys
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *keyCollector) selectionOp(t f32.Affine2D, op key.SelectionOp) {
|
|
||||||
if op.Tag == k.q.focus {
|
|
||||||
k.q.content.Selection.Range = op.Range
|
|
||||||
k.q.content.Selection.Caret = op.Caret
|
|
||||||
k.q.content.Selection.Transform = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *keyCollector) snippetOp(op key.SnippetOp) {
|
|
||||||
if op.Tag == k.q.focus {
|
|
||||||
k.q.content.Snippet = op.Snippet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t TextInputState) String() string {
|
|
||||||
switch t {
|
|
||||||
case TextInputKeep:
|
|
||||||
return "Keep"
|
|
||||||
case TextInputClose:
|
|
||||||
return "Close"
|
|
||||||
case TextInputOpen:
|
|
||||||
return "Open"
|
|
||||||
default:
|
|
||||||
panic("unexpected value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,432 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
|
||||||
|
|
||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gioui.org/f32"
|
|
||||||
"gioui.org/io/event"
|
|
||||||
"gioui.org/io/key"
|
|
||||||
"gioui.org/io/pointer"
|
|
||||||
"gioui.org/op"
|
|
||||||
"gioui.org/op/clip"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestKeyWakeup(t *testing.T) {
|
|
||||||
handler := new(int)
|
|
||||||
var ops op.Ops
|
|
||||||
key.InputOp{Tag: handler}.Add(&ops)
|
|
||||||
|
|
||||||
var r Router
|
|
||||||
// Test that merely adding a handler doesn't trigger redraw.
|
|
||||||
r.Frame(&ops)
|
|
||||||
if _, wake := r.WakeupTime(); wake {
|
|
||||||
t.Errorf("adding key.InputOp triggered a redraw")
|
|
||||||
}
|
|
||||||
// However, adding a handler queues a Focus(false) event.
|
|
||||||
if evts := r.Events(handler); len(evts) != 1 {
|
|
||||||
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) {
|
|
||||||
handlers := make([]int, 3)
|
|
||||||
ops := new(op.Ops)
|
|
||||||
r := new(Router)
|
|
||||||
|
|
||||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
|
||||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
|
||||||
key.FocusOp{Tag: &handlers[2]}.Add(ops)
|
|
||||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
|
||||||
|
|
||||||
// The last one must be focused:
|
|
||||||
key.InputOp{Tag: &handlers[2]}.Add(ops)
|
|
||||||
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[0]), false)
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[1]), false)
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[2]), true)
|
|
||||||
assertFocus(t, r, &handlers[2])
|
|
||||||
assertKeyboard(t, r, TextInputOpen)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeyStacked(t *testing.T) {
|
|
||||||
handlers := make([]int, 4)
|
|
||||||
ops := new(op.Ops)
|
|
||||||
r := new(Router)
|
|
||||||
|
|
||||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
|
||||||
key.FocusOp{Tag: nil}.Add(ops)
|
|
||||||
key.SoftKeyboardOp{Show: false}.Add(ops)
|
|
||||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
|
||||||
key.FocusOp{Tag: &handlers[1]}.Add(ops)
|
|
||||||
key.InputOp{Tag: &handlers[2]}.Add(ops)
|
|
||||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
|
||||||
key.InputOp{Tag: &handlers[3]}.Add(ops)
|
|
||||||
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[0]), false)
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[1]), true)
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[2]), false)
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[3]), false)
|
|
||||||
assertFocus(t, r, &handlers[1])
|
|
||||||
assertKeyboard(t, r, TextInputOpen)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeySoftKeyboardNoFocus(t *testing.T) {
|
|
||||||
ops := new(op.Ops)
|
|
||||||
r := new(Router)
|
|
||||||
|
|
||||||
// It's possible to open the keyboard
|
|
||||||
// without any active focus:
|
|
||||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
|
||||||
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
assertFocus(t, r, nil)
|
|
||||||
assertKeyboard(t, r, TextInputOpen)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeyRemoveFocus(t *testing.T) {
|
|
||||||
handlers := make([]int, 2)
|
|
||||||
ops := new(op.Ops)
|
|
||||||
r := new(Router)
|
|
||||||
|
|
||||||
// New InputOp with Focus and Keyboard:
|
|
||||||
key.InputOp{Tag: &handlers[0], Keys: "Short-Tab"}.Add(ops)
|
|
||||||
key.FocusOp{Tag: &handlers[0]}.Add(ops)
|
|
||||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
|
||||||
|
|
||||||
// New InputOp without any focus:
|
|
||||||
key.InputOp{Tag: &handlers[1], Keys: "Short-Tab"}.Add(ops)
|
|
||||||
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
// Add some key events:
|
|
||||||
event := event.Event(key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press})
|
|
||||||
r.Queue(event)
|
|
||||||
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[0]), true, event)
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[1]), false)
|
|
||||||
assertFocus(t, r, &handlers[0])
|
|
||||||
assertKeyboard(t, r, TextInputOpen)
|
|
||||||
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
// Will get the focus removed:
|
|
||||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
|
||||||
|
|
||||||
// Unchanged:
|
|
||||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
|
||||||
|
|
||||||
// Remove focus by focusing on a tag that don't exist.
|
|
||||||
key.FocusOp{Tag: new(int)}.Add(ops)
|
|
||||||
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
|
|
||||||
assertFocus(t, r, nil)
|
|
||||||
assertKeyboard(t, r, TextInputClose)
|
|
||||||
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
|
||||||
|
|
||||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
|
||||||
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
assertKeyEventUnexpected(t, r.Events(&handlers[0]))
|
|
||||||
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
|
|
||||||
assertFocus(t, r, nil)
|
|
||||||
assertKeyboard(t, r, TextInputClose)
|
|
||||||
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
// Set focus to InputOp which already
|
|
||||||
// exists in the previous frame:
|
|
||||||
key.FocusOp{Tag: &handlers[0]}.Add(ops)
|
|
||||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
|
||||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
|
||||||
|
|
||||||
// Remove focus.
|
|
||||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
|
||||||
key.FocusOp{Tag: nil}.Add(ops)
|
|
||||||
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
|
|
||||||
assertFocus(t, r, nil)
|
|
||||||
assertKeyboard(t, r, TextInputOpen)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeyFocusedInvisible(t *testing.T) {
|
|
||||||
handlers := make([]int, 2)
|
|
||||||
ops := new(op.Ops)
|
|
||||||
r := new(Router)
|
|
||||||
|
|
||||||
// Set new InputOp with focus:
|
|
||||||
key.FocusOp{Tag: &handlers[0]}.Add(ops)
|
|
||||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
|
||||||
key.SoftKeyboardOp{Show: true}.Add(ops)
|
|
||||||
|
|
||||||
// Set new InputOp without focus:
|
|
||||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
|
||||||
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[0]), true)
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[1]), false)
|
|
||||||
assertFocus(t, r, &handlers[0])
|
|
||||||
assertKeyboard(t, r, TextInputOpen)
|
|
||||||
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
//
|
|
||||||
// Removed first (focused) element!
|
|
||||||
//
|
|
||||||
|
|
||||||
// Unchanged:
|
|
||||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
|
||||||
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
assertKeyEventUnexpected(t, r.Events(&handlers[0]))
|
|
||||||
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
|
|
||||||
assertFocus(t, r, nil)
|
|
||||||
assertKeyboard(t, r, TextInputClose)
|
|
||||||
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
// Respawn the first element:
|
|
||||||
// It must receive one `Event{Focus: false}`.
|
|
||||||
key.InputOp{Tag: &handlers[0]}.Add(ops)
|
|
||||||
|
|
||||||
// Unchanged
|
|
||||||
key.InputOp{Tag: &handlers[1]}.Add(ops)
|
|
||||||
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[0]), false)
|
|
||||||
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
|
|
||||||
assertFocus(t, r, nil)
|
|
||||||
assertKeyboard(t, r, TextInputClose)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNoOps(t *testing.T) {
|
|
||||||
r := new(Router)
|
|
||||||
r.Frame(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDirectionalFocus(t *testing.T) {
|
|
||||||
ops := new(op.Ops)
|
|
||||||
r := new(Router)
|
|
||||||
handlers := []image.Rectangle{
|
|
||||||
image.Rect(10, 10, 50, 50),
|
|
||||||
image.Rect(50, 20, 100, 80),
|
|
||||||
image.Rect(20, 26, 60, 80),
|
|
||||||
image.Rect(10, 60, 50, 100),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, bounds := range handlers {
|
|
||||||
cl := clip.Rect(bounds).Push(ops)
|
|
||||||
key.InputOp{Tag: &handlers[i]}.Add(ops)
|
|
||||||
cl.Pop()
|
|
||||||
}
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
r.MoveFocus(FocusLeft)
|
|
||||||
assertFocus(t, r, &handlers[0])
|
|
||||||
r.MoveFocus(FocusLeft)
|
|
||||||
assertFocus(t, r, &handlers[0])
|
|
||||||
r.MoveFocus(FocusRight)
|
|
||||||
assertFocus(t, r, &handlers[1])
|
|
||||||
r.MoveFocus(FocusRight)
|
|
||||||
assertFocus(t, r, &handlers[1])
|
|
||||||
r.MoveFocus(FocusDown)
|
|
||||||
assertFocus(t, r, &handlers[2])
|
|
||||||
r.MoveFocus(FocusDown)
|
|
||||||
assertFocus(t, r, &handlers[2])
|
|
||||||
r.MoveFocus(FocusLeft)
|
|
||||||
assertFocus(t, r, &handlers[3])
|
|
||||||
r.MoveFocus(FocusUp)
|
|
||||||
assertFocus(t, r, &handlers[0])
|
|
||||||
|
|
||||||
r.MoveFocus(FocusForward)
|
|
||||||
assertFocus(t, r, &handlers[1])
|
|
||||||
r.MoveFocus(FocusBackward)
|
|
||||||
assertFocus(t, r, &handlers[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFocusScroll(t *testing.T) {
|
|
||||||
ops := new(op.Ops)
|
|
||||||
r := new(Router)
|
|
||||||
h := new(int)
|
|
||||||
|
|
||||||
parent := clip.Rect(image.Rect(1, 1, 14, 39)).Push(ops)
|
|
||||||
cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops)
|
|
||||||
key.InputOp{Tag: h}.Add(ops)
|
|
||||||
pointer.InputOp{
|
|
||||||
Tag: h,
|
|
||||||
Types: pointer.Scroll,
|
|
||||||
ScrollBounds: image.Rect(-100, -100, 100, 100),
|
|
||||||
}.Add(ops)
|
|
||||||
// Test that h is scrolled even if behind another handler.
|
|
||||||
pointer.InputOp{
|
|
||||||
Tag: new(int),
|
|
||||||
}.Add(ops)
|
|
||||||
cl.Pop()
|
|
||||||
parent.Pop()
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
r.MoveFocus(FocusLeft)
|
|
||||||
r.RevealFocus(image.Rect(0, 0, 15, 40))
|
|
||||||
evts := r.Events(h)
|
|
||||||
assertScrollEvent(t, evts[len(evts)-1], f32.Pt(6, -9))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFocusClick(t *testing.T) {
|
|
||||||
ops := new(op.Ops)
|
|
||||||
r := new(Router)
|
|
||||||
h := new(int)
|
|
||||||
|
|
||||||
cl := clip.Rect(image.Rect(0, 0, 10, 10)).Push(ops)
|
|
||||||
key.InputOp{Tag: h}.Add(ops)
|
|
||||||
pointer.InputOp{
|
|
||||||
Tag: h,
|
|
||||||
Types: pointer.Press | pointer.Release,
|
|
||||||
}.Add(ops)
|
|
||||||
cl.Pop()
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
r.MoveFocus(FocusLeft)
|
|
||||||
r.ClickFocus()
|
|
||||||
assertEventPointerTypeSequence(t, r.Events(h), pointer.Cancel, pointer.Press, pointer.Release)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNoFocus(t *testing.T) {
|
|
||||||
r := new(Router)
|
|
||||||
r.MoveFocus(FocusForward)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeyRouting(t *testing.T) {
|
|
||||||
handlers := make([]int, 5)
|
|
||||||
ops := new(op.Ops)
|
|
||||||
macroOps := new(op.Ops)
|
|
||||||
r := new(Router)
|
|
||||||
|
|
||||||
rect := clip.Rect{Max: image.Pt(10, 10)}
|
|
||||||
|
|
||||||
macro := op.Record(macroOps)
|
|
||||||
key.InputOp{Tag: &handlers[0], Keys: "A"}.Add(ops)
|
|
||||||
cl1 := rect.Push(ops)
|
|
||||||
key.InputOp{Tag: &handlers[1], Keys: "B"}.Add(ops)
|
|
||||||
key.InputOp{Tag: &handlers[2], Keys: "A"}.Add(ops)
|
|
||||||
cl1.Pop()
|
|
||||||
cl2 := rect.Push(ops)
|
|
||||||
key.InputOp{Tag: &handlers[3]}.Add(ops)
|
|
||||||
key.InputOp{Tag: &handlers[4], Keys: "A"}.Add(ops)
|
|
||||||
cl2.Pop()
|
|
||||||
call := macro.Stop()
|
|
||||||
call.Add(ops)
|
|
||||||
|
|
||||||
r.Frame(ops)
|
|
||||||
|
|
||||||
A, B := key.Event{Name: "A"}, key.Event{Name: "B"}
|
|
||||||
r.Queue(A, B)
|
|
||||||
|
|
||||||
// With no focus, the events should traverse the final branch of the hit tree
|
|
||||||
// searching for handlers.
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[4]), false, A)
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[3]), false)
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[2]), false)
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[1]), false, B)
|
|
||||||
assertKeyEvent(t, r.Events(&handlers[0]), false)
|
|
||||||
|
|
||||||
r2 := new(Router)
|
|
||||||
|
|
||||||
call.Add(ops)
|
|
||||||
key.FocusOp{Tag: &handlers[3]}.Add(ops)
|
|
||||||
r2.Frame(ops)
|
|
||||||
|
|
||||||
r2.Queue(A, B)
|
|
||||||
|
|
||||||
// With focus, the events should traverse the branch of the hit tree
|
|
||||||
// containing the focused element.
|
|
||||||
assertKeyEvent(t, r2.Events(&handlers[4]), false)
|
|
||||||
assertKeyEvent(t, r2.Events(&handlers[3]), true)
|
|
||||||
assertKeyEvent(t, r2.Events(&handlers[2]), false)
|
|
||||||
assertKeyEvent(t, r2.Events(&handlers[1]), false)
|
|
||||||
assertKeyEvent(t, r2.Events(&handlers[0]), false, A)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertKeyEvent(t *testing.T, events []event.Event, expectedFocus bool, expectedInputs ...event.Event) {
|
|
||||||
t.Helper()
|
|
||||||
var evtFocus int
|
|
||||||
var evtKeyPress int
|
|
||||||
for _, e := range events {
|
|
||||||
switch ev := e.(type) {
|
|
||||||
case key.FocusEvent:
|
|
||||||
if ev.Focus != expectedFocus {
|
|
||||||
t.Errorf("focus is expected to be %v, got %v", expectedFocus, ev.Focus)
|
|
||||||
}
|
|
||||||
evtFocus++
|
|
||||||
case key.Event, key.EditEvent:
|
|
||||||
if len(expectedInputs) <= evtKeyPress {
|
|
||||||
t.Fatalf("unexpected key events")
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) {
|
|
||||||
t.Errorf("expected %v events, got %v", expectedInputs[evtKeyPress], ev)
|
|
||||||
}
|
|
||||||
evtKeyPress++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if evtFocus <= 0 {
|
|
||||||
t.Errorf("expected focus event")
|
|
||||||
}
|
|
||||||
if evtFocus > 1 {
|
|
||||||
t.Errorf("expected single focus event")
|
|
||||||
}
|
|
||||||
if evtKeyPress != len(expectedInputs) {
|
|
||||||
t.Errorf("expected key events")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertKeyEventUnexpected(t *testing.T, events []event.Event) {
|
|
||||||
t.Helper()
|
|
||||||
var evtFocus int
|
|
||||||
for _, e := range events {
|
|
||||||
switch e.(type) {
|
|
||||||
case key.FocusEvent:
|
|
||||||
evtFocus++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if evtFocus > 1 {
|
|
||||||
t.Errorf("unexpected focus event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertFocus(t *testing.T, router *Router, expected event.Tag) {
|
|
||||||
t.Helper()
|
|
||||||
if got := router.key.queue.focus; got != expected {
|
|
||||||
t.Errorf("expected %v to be focused, got %v", expected, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertKeyboard(t *testing.T, router *Router, expected TextInputState) {
|
|
||||||
t.Helper()
|
|
||||||
if got := router.key.queue.state; got != expected {
|
|
||||||
t.Errorf("expected %v keyboard, got %v", expected, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,644 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
|
||||||
|
|
||||||
/*
|
|
||||||
Package router implements Router, a event.Queue implementation
|
|
||||||
that that disambiguates and routes events to handlers declared
|
|
||||||
in operation lists.
|
|
||||||
|
|
||||||
Router is used by app.Window and is otherwise only useful for
|
|
||||||
using Gio with external window implementations.
|
|
||||||
*/
|
|
||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"image"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gioui.org/f32"
|
|
||||||
f32internal "gioui.org/internal/f32"
|
|
||||||
"gioui.org/internal/ops"
|
|
||||||
"gioui.org/io/clipboard"
|
|
||||||
"gioui.org/io/event"
|
|
||||||
"gioui.org/io/key"
|
|
||||||
"gioui.org/io/pointer"
|
|
||||||
"gioui.org/io/profile"
|
|
||||||
"gioui.org/io/semantic"
|
|
||||||
"gioui.org/io/system"
|
|
||||||
"gioui.org/io/transfer"
|
|
||||||
"gioui.org/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Router is a Queue implementation that routes events
|
|
||||||
// to handlers declared in operation lists.
|
|
||||||
type Router struct {
|
|
||||||
savedTrans []f32.Affine2D
|
|
||||||
transStack []f32.Affine2D
|
|
||||||
pointer struct {
|
|
||||||
queue pointerQueue
|
|
||||||
collector pointerCollector
|
|
||||||
}
|
|
||||||
key struct {
|
|
||||||
queue keyQueue
|
|
||||||
collector keyCollector
|
|
||||||
}
|
|
||||||
cqueue clipboardQueue
|
|
||||||
|
|
||||||
handlers handlerEvents
|
|
||||||
|
|
||||||
reader ops.Reader
|
|
||||||
|
|
||||||
// InvalidateOp summary.
|
|
||||||
wakeup bool
|
|
||||||
wakeupTime time.Time
|
|
||||||
|
|
||||||
// ProfileOp summary.
|
|
||||||
profHandlers map[event.Tag]struct{}
|
|
||||||
profile profile.Event
|
|
||||||
}
|
|
||||||
|
|
||||||
// SemanticNode represents a node in the tree describing the components
|
|
||||||
// contained in a frame.
|
|
||||||
type SemanticNode struct {
|
|
||||||
ID SemanticID
|
|
||||||
ParentID SemanticID
|
|
||||||
Children []SemanticNode
|
|
||||||
Desc SemanticDesc
|
|
||||||
|
|
||||||
areaIdx int
|
|
||||||
}
|
|
||||||
|
|
||||||
// SemanticDesc provides a semantic description of a UI component.
|
|
||||||
type SemanticDesc struct {
|
|
||||||
Class semantic.ClassOp
|
|
||||||
Description string
|
|
||||||
Label string
|
|
||||||
Selected bool
|
|
||||||
Disabled bool
|
|
||||||
Gestures SemanticGestures
|
|
||||||
Bounds image.Rectangle
|
|
||||||
}
|
|
||||||
|
|
||||||
// SemanticGestures is a bit-set of supported gestures.
|
|
||||||
type SemanticGestures int
|
|
||||||
|
|
||||||
const (
|
|
||||||
ClickGesture SemanticGestures = 1 << iota
|
|
||||||
ScrollGesture
|
|
||||||
)
|
|
||||||
|
|
||||||
// SemanticID uniquely identifies a SemanticDescription.
|
|
||||||
//
|
|
||||||
// By convention, the zero value denotes the non-existent ID.
|
|
||||||
type SemanticID uint64
|
|
||||||
|
|
||||||
type handlerEvents struct {
|
|
||||||
handlers map[event.Tag][]event.Event
|
|
||||||
hadEvents bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Events returns the available events for the handler key.
|
|
||||||
func (q *Router) Events(k event.Tag) []event.Event {
|
|
||||||
events := q.handlers.Events(k)
|
|
||||||
if _, isprof := q.profHandlers[k]; isprof {
|
|
||||||
delete(q.profHandlers, k)
|
|
||||||
events = append(events, q.profile)
|
|
||||||
}
|
|
||||||
return events
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frame replaces the declared handlers from the supplied
|
|
||||||
// operation list. The text input state, wakeup time and whether
|
|
||||||
// there are active profile handlers is also saved.
|
|
||||||
func (q *Router) Frame(frame *op.Ops) {
|
|
||||||
q.handlers.Clear()
|
|
||||||
q.wakeup = false
|
|
||||||
for k := range q.profHandlers {
|
|
||||||
delete(q.profHandlers, k)
|
|
||||||
}
|
|
||||||
var ops *ops.Ops
|
|
||||||
if frame != nil {
|
|
||||||
ops = &frame.Internal
|
|
||||||
}
|
|
||||||
q.reader.Reset(ops)
|
|
||||||
q.collect()
|
|
||||||
|
|
||||||
q.pointer.queue.Frame(&q.handlers)
|
|
||||||
q.key.queue.Frame(&q.handlers, q.key.collector)
|
|
||||||
if q.handlers.HadEvents() {
|
|
||||||
q.wakeup = true
|
|
||||||
q.wakeupTime = time.Time{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue key events to the topmost handler.
|
|
||||||
func (q *Router) QueueTopmost(events ...key.Event) bool {
|
|
||||||
var topmost event.Tag
|
|
||||||
pq := &q.pointer.queue
|
|
||||||
for _, h := range pq.hitTree {
|
|
||||||
if h.ktag != nil {
|
|
||||||
topmost = h.ktag
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if topmost == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, e := range events {
|
|
||||||
q.handlers.Add(topmost, e)
|
|
||||||
}
|
|
||||||
return q.handlers.HadEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue events and report whether at least one handler had an event queued.
|
|
||||||
func (q *Router) Queue(events ...event.Event) bool {
|
|
||||||
for _, e := range events {
|
|
||||||
switch e := e.(type) {
|
|
||||||
case profile.Event:
|
|
||||||
q.profile = e
|
|
||||||
case pointer.Event:
|
|
||||||
q.pointer.queue.Push(e, &q.handlers)
|
|
||||||
case key.Event:
|
|
||||||
q.queueKeyEvent(e)
|
|
||||||
case key.SnippetEvent:
|
|
||||||
// Expand existing, overlapping snippet.
|
|
||||||
if r := q.key.queue.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) {
|
|
||||||
if e.Start > r.Start {
|
|
||||||
e.Start = r.Start
|
|
||||||
}
|
|
||||||
if e.End < r.End {
|
|
||||||
e.End = r.End
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if f := q.key.queue.focus; f != nil {
|
|
||||||
q.handlers.Add(f, e)
|
|
||||||
}
|
|
||||||
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
|
|
||||||
if f := q.key.queue.focus; f != nil {
|
|
||||||
q.handlers.Add(f, e)
|
|
||||||
}
|
|
||||||
case clipboard.Event:
|
|
||||||
q.cqueue.Push(e, &q.handlers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return q.handlers.HadEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
func rangeOverlaps(r1, r2 key.Range) bool {
|
|
||||||
r1 = rangeNorm(r1)
|
|
||||||
r2 = rangeNorm(r2)
|
|
||||||
return r1.Start <= r2.Start && r2.Start < r1.End ||
|
|
||||||
r1.Start <= r2.End && r2.End < r1.End
|
|
||||||
}
|
|
||||||
|
|
||||||
func rangeNorm(r key.Range) key.Range {
|
|
||||||
if r.End < r.Start {
|
|
||||||
r.End, r.Start = r.Start, r.End
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Router) queueKeyEvent(e key.Event) {
|
|
||||||
kq := &q.key.queue
|
|
||||||
f := q.key.queue.focus
|
|
||||||
if f != nil && kq.Accepts(f, e) {
|
|
||||||
q.handlers.Add(f, e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pq := &q.pointer.queue
|
|
||||||
idx := len(pq.hitTree) - 1
|
|
||||||
focused := f != nil
|
|
||||||
if focused {
|
|
||||||
// If there is a focused tag, traverse its ancestry through the
|
|
||||||
// hit tree to search for handlers.
|
|
||||||
for ; pq.hitTree[idx].ktag != f; idx-- {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for idx != -1 {
|
|
||||||
n := &pq.hitTree[idx]
|
|
||||||
if focused {
|
|
||||||
idx = n.next
|
|
||||||
} else {
|
|
||||||
idx--
|
|
||||||
}
|
|
||||||
if n.ktag == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if kq.Accepts(n.ktag, e) {
|
|
||||||
q.handlers.Add(n.ktag, e)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Router) MoveFocus(dir FocusDirection) bool {
|
|
||||||
return q.key.queue.MoveFocus(dir, &q.handlers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RevealFocus scrolls the current focus (if any) into viewport
|
|
||||||
// if there are scrollable parent handlers.
|
|
||||||
func (q *Router) RevealFocus(viewport image.Rectangle) {
|
|
||||||
focus := q.key.queue.focus
|
|
||||||
if focus == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bounds := q.key.queue.BoundsFor(focus)
|
|
||||||
area := q.key.queue.AreaFor(focus)
|
|
||||||
viewport = q.pointer.queue.ClipFor(area, viewport)
|
|
||||||
|
|
||||||
topleft := bounds.Min.Sub(viewport.Min)
|
|
||||||
topleft = max(topleft, bounds.Max.Sub(viewport.Max))
|
|
||||||
topleft = min(image.Pt(0, 0), topleft)
|
|
||||||
bottomright := bounds.Max.Sub(viewport.Max)
|
|
||||||
bottomright = min(bottomright, bounds.Min.Sub(viewport.Min))
|
|
||||||
bottomright = max(image.Pt(0, 0), bottomright)
|
|
||||||
s := topleft
|
|
||||||
if s.X == 0 {
|
|
||||||
s.X = bottomright.X
|
|
||||||
}
|
|
||||||
if s.Y == 0 {
|
|
||||||
s.Y = bottomright.Y
|
|
||||||
}
|
|
||||||
q.ScrollFocus(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScrollFocus scrolls the focused widget, if any, by dist.
|
|
||||||
func (q *Router) ScrollFocus(dist image.Point) {
|
|
||||||
focus := q.key.queue.focus
|
|
||||||
if focus == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
area := q.key.queue.AreaFor(focus)
|
|
||||||
q.pointer.queue.Deliver(area, pointer.Event{
|
|
||||||
Type: pointer.Scroll,
|
|
||||||
Source: pointer.Touch,
|
|
||||||
Scroll: f32internal.FPt(dist),
|
|
||||||
}, &q.handlers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func max(p1, p2 image.Point) image.Point {
|
|
||||||
m := p1
|
|
||||||
if p2.X > m.X {
|
|
||||||
m.X = p2.X
|
|
||||||
}
|
|
||||||
if p2.Y > m.Y {
|
|
||||||
m.Y = p2.Y
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func min(p1, p2 image.Point) image.Point {
|
|
||||||
m := p1
|
|
||||||
if p2.X < m.X {
|
|
||||||
m.X = p2.X
|
|
||||||
}
|
|
||||||
if p2.Y < m.Y {
|
|
||||||
m.Y = p2.Y
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Router) ActionAt(p f32.Point) (system.Action, bool) {
|
|
||||||
return q.pointer.queue.ActionAt(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Router) ClickFocus() {
|
|
||||||
focus := q.key.queue.focus
|
|
||||||
if focus == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bounds := q.key.queue.BoundsFor(focus)
|
|
||||||
center := bounds.Max.Add(bounds.Min).Div(2)
|
|
||||||
e := pointer.Event{
|
|
||||||
Position: f32.Pt(float32(center.X), float32(center.Y)),
|
|
||||||
Source: pointer.Touch,
|
|
||||||
}
|
|
||||||
area := q.key.queue.AreaFor(focus)
|
|
||||||
e.Type = pointer.Press
|
|
||||||
q.pointer.queue.Deliver(area, e, &q.handlers)
|
|
||||||
e.Type = pointer.Release
|
|
||||||
q.pointer.queue.Deliver(area, e, &q.handlers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextInputState returns the input state from the most recent
|
|
||||||
// call to Frame.
|
|
||||||
func (q *Router) TextInputState() TextInputState {
|
|
||||||
return q.key.queue.InputState()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextInputHint returns the input mode from the most recent key.InputOp.
|
|
||||||
func (q *Router) TextInputHint() (key.InputHint, bool) {
|
|
||||||
return q.key.queue.InputHint()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteClipboard returns the most recent text to be copied
|
|
||||||
// to the clipboard, if any.
|
|
||||||
func (q *Router) WriteClipboard() (string, bool) {
|
|
||||||
return q.cqueue.WriteClipboard()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadClipboard reports if any new handler is waiting
|
|
||||||
// to read the clipboard.
|
|
||||||
func (q *Router) ReadClipboard() bool {
|
|
||||||
return q.cqueue.ReadClipboard()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cursor returns the last cursor set.
|
|
||||||
func (q *Router) Cursor() pointer.Cursor {
|
|
||||||
return q.pointer.queue.cursor
|
|
||||||
}
|
|
||||||
|
|
||||||
// SemanticAt returns the first semantic description under pos, if any.
|
|
||||||
func (q *Router) SemanticAt(pos f32.Point) (SemanticID, bool) {
|
|
||||||
return q.pointer.queue.SemanticAt(pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendSemantics appends the semantic tree to nodes, and returns the result.
|
|
||||||
// The root node is the first added.
|
|
||||||
func (q *Router) AppendSemantics(nodes []SemanticNode) []SemanticNode {
|
|
||||||
q.pointer.collector.q = &q.pointer.queue
|
|
||||||
q.pointer.collector.ensureRoot()
|
|
||||||
return q.pointer.queue.AppendSemantics(nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EditorState returns the editor state for the focused handler, or the
|
|
||||||
// zero value if there is none.
|
|
||||||
func (q *Router) EditorState() EditorState {
|
|
||||||
return q.key.queue.content
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Router) collect() {
|
|
||||||
q.transStack = q.transStack[:0]
|
|
||||||
pc := &q.pointer.collector
|
|
||||||
pc.q = &q.pointer.queue
|
|
||||||
pc.reset()
|
|
||||||
kc := &q.key.collector
|
|
||||||
*kc = keyCollector{q: &q.key.queue}
|
|
||||||
q.key.queue.Reset()
|
|
||||||
var t f32.Affine2D
|
|
||||||
bo := binary.LittleEndian
|
|
||||||
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
|
|
||||||
switch ops.OpType(encOp.Data[0]) {
|
|
||||||
case ops.TypeInvalidate:
|
|
||||||
op := decodeInvalidateOp(encOp.Data)
|
|
||||||
if !q.wakeup || op.At.Before(q.wakeupTime) {
|
|
||||||
q.wakeup = true
|
|
||||||
q.wakeupTime = op.At
|
|
||||||
}
|
|
||||||
case ops.TypeProfile:
|
|
||||||
op := decodeProfileOp(encOp.Data, encOp.Refs)
|
|
||||||
if q.profHandlers == nil {
|
|
||||||
q.profHandlers = make(map[event.Tag]struct{})
|
|
||||||
}
|
|
||||||
q.profHandlers[op.Tag] = struct{}{}
|
|
||||||
case ops.TypeClipboardRead:
|
|
||||||
q.cqueue.ProcessReadClipboard(encOp.Refs)
|
|
||||||
case ops.TypeClipboardWrite:
|
|
||||||
q.cqueue.ProcessWriteClipboard(encOp.Refs)
|
|
||||||
case ops.TypeSave:
|
|
||||||
id := ops.DecodeSave(encOp.Data)
|
|
||||||
if extra := id - len(q.savedTrans) + 1; extra > 0 {
|
|
||||||
q.savedTrans = append(q.savedTrans, make([]f32.Affine2D, extra)...)
|
|
||||||
}
|
|
||||||
q.savedTrans[id] = t
|
|
||||||
case ops.TypeLoad:
|
|
||||||
id := ops.DecodeLoad(encOp.Data)
|
|
||||||
t = q.savedTrans[id]
|
|
||||||
pc.resetState()
|
|
||||||
pc.setTrans(t)
|
|
||||||
|
|
||||||
case ops.TypeClip:
|
|
||||||
var op ops.ClipOp
|
|
||||||
op.Decode(encOp.Data)
|
|
||||||
pc.clip(op)
|
|
||||||
case ops.TypePopClip:
|
|
||||||
pc.popArea()
|
|
||||||
case ops.TypeTransform:
|
|
||||||
t2, push := ops.DecodeTransform(encOp.Data)
|
|
||||||
if push {
|
|
||||||
q.transStack = append(q.transStack, t)
|
|
||||||
}
|
|
||||||
t = t.Mul(t2)
|
|
||||||
pc.setTrans(t)
|
|
||||||
case ops.TypePopTransform:
|
|
||||||
n := len(q.transStack)
|
|
||||||
t = q.transStack[n-1]
|
|
||||||
q.transStack = q.transStack[:n-1]
|
|
||||||
pc.setTrans(t)
|
|
||||||
|
|
||||||
// Pointer ops.
|
|
||||||
case ops.TypePass:
|
|
||||||
pc.pass()
|
|
||||||
case ops.TypePopPass:
|
|
||||||
pc.popPass()
|
|
||||||
case ops.TypePointerInput:
|
|
||||||
op := pointer.InputOp{
|
|
||||||
Tag: encOp.Refs[0].(event.Tag),
|
|
||||||
Grab: encOp.Data[1] != 0,
|
|
||||||
Types: pointer.Type(bo.Uint16(encOp.Data[2:])),
|
|
||||||
ScrollBounds: image.Rectangle{
|
|
||||||
Min: image.Point{
|
|
||||||
X: int(int32(bo.Uint32(encOp.Data[4:]))),
|
|
||||||
Y: int(int32(bo.Uint32(encOp.Data[8:]))),
|
|
||||||
},
|
|
||||||
Max: image.Point{
|
|
||||||
X: int(int32(bo.Uint32(encOp.Data[12:]))),
|
|
||||||
Y: int(int32(bo.Uint32(encOp.Data[16:]))),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
pc.inputOp(op, &q.handlers)
|
|
||||||
case ops.TypeCursor:
|
|
||||||
name := pointer.Cursor(encOp.Data[1])
|
|
||||||
pc.cursor(name)
|
|
||||||
case ops.TypeSource:
|
|
||||||
op := transfer.SourceOp{
|
|
||||||
Tag: encOp.Refs[0].(event.Tag),
|
|
||||||
Type: encOp.Refs[1].(string),
|
|
||||||
}
|
|
||||||
pc.sourceOp(op, &q.handlers)
|
|
||||||
case ops.TypeTarget:
|
|
||||||
op := transfer.TargetOp{
|
|
||||||
Tag: encOp.Refs[0].(event.Tag),
|
|
||||||
Type: encOp.Refs[1].(string),
|
|
||||||
}
|
|
||||||
pc.targetOp(op, &q.handlers)
|
|
||||||
case ops.TypeOffer:
|
|
||||||
op := transfer.OfferOp{
|
|
||||||
Tag: encOp.Refs[0].(event.Tag),
|
|
||||||
Type: encOp.Refs[1].(string),
|
|
||||||
Data: encOp.Refs[2].(io.ReadCloser),
|
|
||||||
}
|
|
||||||
pc.offerOp(op, &q.handlers)
|
|
||||||
case ops.TypeActionInput:
|
|
||||||
act := system.Action(encOp.Data[1])
|
|
||||||
pc.actionInputOp(act)
|
|
||||||
|
|
||||||
// Key ops.
|
|
||||||
case ops.TypeKeyFocus:
|
|
||||||
tag, _ := encOp.Refs[0].(event.Tag)
|
|
||||||
op := key.FocusOp{
|
|
||||||
Tag: tag,
|
|
||||||
}
|
|
||||||
kc.focusOp(op.Tag)
|
|
||||||
case ops.TypeKeySoftKeyboard:
|
|
||||||
op := key.SoftKeyboardOp{
|
|
||||||
Show: encOp.Data[1] != 0,
|
|
||||||
}
|
|
||||||
kc.softKeyboard(op.Show)
|
|
||||||
case ops.TypeKeyInput:
|
|
||||||
filter := encOp.Refs[1].(*key.Set)
|
|
||||||
op := key.InputOp{
|
|
||||||
Tag: encOp.Refs[0].(event.Tag),
|
|
||||||
Hint: key.InputHint(encOp.Data[1]),
|
|
||||||
Keys: *filter,
|
|
||||||
}
|
|
||||||
a := pc.currentArea()
|
|
||||||
b := pc.currentAreaBounds()
|
|
||||||
pc.keyInputOp(op)
|
|
||||||
kc.inputOp(op, a, b)
|
|
||||||
case ops.TypeSnippet:
|
|
||||||
op := key.SnippetOp{
|
|
||||||
Tag: encOp.Refs[0].(event.Tag),
|
|
||||||
Snippet: key.Snippet{
|
|
||||||
Range: key.Range{
|
|
||||||
Start: int(int32(bo.Uint32(encOp.Data[1:]))),
|
|
||||||
End: int(int32(bo.Uint32(encOp.Data[5:]))),
|
|
||||||
},
|
|
||||||
Text: *(encOp.Refs[1].(*string)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
kc.snippetOp(op)
|
|
||||||
case ops.TypeSelection:
|
|
||||||
op := key.SelectionOp{
|
|
||||||
Tag: encOp.Refs[0].(event.Tag),
|
|
||||||
Range: key.Range{
|
|
||||||
Start: int(int32(bo.Uint32(encOp.Data[1:]))),
|
|
||||||
End: int(int32(bo.Uint32(encOp.Data[5:]))),
|
|
||||||
},
|
|
||||||
Caret: key.Caret{
|
|
||||||
Pos: f32.Point{
|
|
||||||
X: math.Float32frombits(bo.Uint32(encOp.Data[9:])),
|
|
||||||
Y: math.Float32frombits(bo.Uint32(encOp.Data[13:])),
|
|
||||||
},
|
|
||||||
Ascent: math.Float32frombits(bo.Uint32(encOp.Data[17:])),
|
|
||||||
Descent: math.Float32frombits(bo.Uint32(encOp.Data[21:])),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
kc.selectionOp(t, op)
|
|
||||||
|
|
||||||
// Semantic ops.
|
|
||||||
case ops.TypeSemanticLabel:
|
|
||||||
lbl := encOp.Refs[0].(string)
|
|
||||||
pc.semanticLabel(lbl)
|
|
||||||
case ops.TypeSemanticDesc:
|
|
||||||
desc := encOp.Refs[0].(string)
|
|
||||||
pc.semanticDesc(desc)
|
|
||||||
case ops.TypeSemanticClass:
|
|
||||||
class := semantic.ClassOp(encOp.Data[1])
|
|
||||||
pc.semanticClass(class)
|
|
||||||
case ops.TypeSemanticSelected:
|
|
||||||
if encOp.Data[1] != 0 {
|
|
||||||
pc.semanticSelected(true)
|
|
||||||
} else {
|
|
||||||
pc.semanticSelected(false)
|
|
||||||
}
|
|
||||||
case ops.TypeSemanticDisabled:
|
|
||||||
if encOp.Data[1] != 0 {
|
|
||||||
pc.semanticDisabled(true)
|
|
||||||
} else {
|
|
||||||
pc.semanticDisabled(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Profiling reports whether there was profile handlers in the
|
|
||||||
// most recent Frame call.
|
|
||||||
func (q *Router) Profiling() bool {
|
|
||||||
return len(q.profHandlers) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// WakeupTime returns the most recent time for doing another frame,
|
|
||||||
// as determined from the last call to Frame.
|
|
||||||
func (q *Router) WakeupTime() (time.Time, bool) {
|
|
||||||
return q.wakeupTime, q.wakeup
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handlerEvents) init() {
|
|
||||||
if h.handlers == nil {
|
|
||||||
h.handlers = make(map[event.Tag][]event.Event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handlerEvents) AddNoRedraw(k event.Tag, e event.Event) {
|
|
||||||
h.init()
|
|
||||||
h.handlers[k] = append(h.handlers[k], e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handlerEvents) Add(k event.Tag, e event.Event) {
|
|
||||||
h.AddNoRedraw(k, e)
|
|
||||||
h.hadEvents = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handlerEvents) HadEvents() bool {
|
|
||||||
u := h.hadEvents
|
|
||||||
h.hadEvents = false
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handlerEvents) Events(k event.Tag) []event.Event {
|
|
||||||
if events, ok := h.handlers[k]; ok {
|
|
||||||
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 nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handlerEvents) Clear() {
|
|
||||||
for k := range h.handlers {
|
|
||||||
delete(h.handlers, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeProfileOp(d []byte, refs []interface{}) profile.Op {
|
|
||||||
if ops.OpType(d[0]) != ops.TypeProfile {
|
|
||||||
panic("invalid op")
|
|
||||||
}
|
|
||||||
return profile.Op{
|
|
||||||
Tag: refs[0].(event.Tag),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeInvalidateOp(d []byte) op.InvalidateOp {
|
|
||||||
bo := binary.LittleEndian
|
|
||||||
if ops.OpType(d[0]) != ops.TypeInvalidate {
|
|
||||||
panic("invalid op")
|
|
||||||
}
|
|
||||||
var o op.InvalidateOp
|
|
||||||
if nanos := bo.Uint64(d[1:]); nanos > 0 {
|
|
||||||
o.At = time.Unix(0, int64(nanos))
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SemanticGestures) String() string {
|
|
||||||
var gestures []string
|
|
||||||
if s&ClickGesture != 0 {
|
|
||||||
gestures = append(gestures, "Click")
|
|
||||||
}
|
|
||||||
return strings.Join(gestures, ",")
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
|
||||||
|
|
||||||
// Package system contains events usually handled at the top-level
|
|
||||||
// program level.
|
|
||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gioui.org/io/event"
|
|
||||||
"gioui.org/op"
|
|
||||||
"gioui.org/unit"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A FrameEvent requests a new frame in the form of a list of
|
|
||||||
// operations that describes what to display and how to handle
|
|
||||||
// input.
|
|
||||||
type FrameEvent struct {
|
|
||||||
// Now is the current animation. Use Now instead of time.Now to
|
|
||||||
// synchronize animation and to avoid the time.Now call overhead.
|
|
||||||
Now time.Time
|
|
||||||
// Metric converts device independent dp and sp to device pixels.
|
|
||||||
Metric unit.Metric
|
|
||||||
// Size is the dimensions of the window.
|
|
||||||
Size image.Point
|
|
||||||
// Insets represent the space occupied by system decorations and controls.
|
|
||||||
Insets Insets
|
|
||||||
// Frame completes the FrameEvent by drawing the graphical operations
|
|
||||||
// from ops into the window.
|
|
||||||
Frame func(frame *op.Ops)
|
|
||||||
// Queue supplies the events for event handlers.
|
|
||||||
Queue event.Queue
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestroyEvent is the last event sent through
|
|
||||||
// a window event channel.
|
|
||||||
type DestroyEvent struct {
|
|
||||||
// Err is nil for normal window closures. If a
|
|
||||||
// window is prematurely closed, Err is the cause.
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insets is the space taken up by
|
|
||||||
// system decoration such as translucent
|
|
||||||
// system bars and software keyboards.
|
|
||||||
type Insets struct {
|
|
||||||
// Values are in pixels.
|
|
||||||
Top, Bottom, Left, Right unit.Dp
|
|
||||||
}
|
|
||||||
|
|
||||||
// A StageEvent is generated whenever the stage of a
|
|
||||||
// Window changes.
|
|
||||||
type StageEvent struct {
|
|
||||||
Stage Stage
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stage of a Window.
|
|
||||||
type Stage uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
// StagePaused is the stage for windows that have no on-screen representation.
|
|
||||||
// Paused windows don't receive FrameEvent.
|
|
||||||
StagePaused Stage = iota
|
|
||||||
// StageInactive is the stage for windows that are visible, but not active.
|
|
||||||
// Inactive windows receive FrameEvent.
|
|
||||||
StageInactive
|
|
||||||
// StageRunning is for active and visible Windows.
|
|
||||||
// Running windows receive FrameEvent.
|
|
||||||
StageRunning
|
|
||||||
)
|
|
||||||
|
|
||||||
// String implements fmt.Stringer.
|
|
||||||
func (l Stage) String() string {
|
|
||||||
switch l {
|
|
||||||
case StagePaused:
|
|
||||||
return "StagePaused"
|
|
||||||
case StageInactive:
|
|
||||||
return "StageInactive"
|
|
||||||
case StageRunning:
|
|
||||||
return "StageRunning"
|
|
||||||
default:
|
|
||||||
panic("unexpected Stage value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (FrameEvent) ImplementsEvent() {}
|
|
||||||
func (StageEvent) ImplementsEvent() {}
|
|
||||||
func (DestroyEvent) ImplementsEvent() {}
|
|
||||||
+29
-43
@@ -2,11 +2,11 @@
|
|||||||
//
|
//
|
||||||
// The transfer protocol is as follows:
|
// The transfer protocol is as follows:
|
||||||
//
|
//
|
||||||
// - Data sources are registered with SourceOps, data targets with TargetOps.
|
// - Data sources use [SourceFilter] to receive an [InitiateEvent] when a drag
|
||||||
// - A data source receives a RequestEvent when a transfer is initiated.
|
// is initiated, and an [RequestEvent] for each initiation of a data transfer.
|
||||||
// It must respond with an OfferOp.
|
// Sources respond to requests with [OfferCmd].
|
||||||
// - The target receives a DataEvent when transferring to it. It must close
|
// - Data targets use [TargetFilter] to receive an [DataEvent] for receiving data.
|
||||||
// the event data after use.
|
// The target must close the data event after use.
|
||||||
//
|
//
|
||||||
// When a user initiates a pointer-guided drag and drop transfer, the
|
// When a user initiates a pointer-guided drag and drop transfer, the
|
||||||
// source as well as all potential targets receive an InitiateEvent.
|
// source as well as all potential targets receive an InitiateEvent.
|
||||||
@@ -20,29 +20,11 @@ package transfer
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gioui.org/internal/ops"
|
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
"gioui.org/op"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SourceOp registers a tag as a data source for a MIME type.
|
// OfferCmd is used by data sources as a response to a RequestEvent.
|
||||||
// Use multiple SourceOps if a tag supports multiple types.
|
type OfferCmd struct {
|
||||||
type SourceOp struct {
|
|
||||||
Tag event.Tag
|
|
||||||
// Type is the MIME type supported by this source.
|
|
||||||
Type string
|
|
||||||
}
|
|
||||||
|
|
||||||
// TargetOp registers a tag as a data target.
|
|
||||||
// Use multiple TargetOps if a tag supports multiple types.
|
|
||||||
type TargetOp struct {
|
|
||||||
Tag event.Tag
|
|
||||||
// Type is the MIME type accepted by this target.
|
|
||||||
Type string
|
|
||||||
}
|
|
||||||
|
|
||||||
// OfferOp is used by data sources as a response to a RequestEvent.
|
|
||||||
type OfferOp struct {
|
|
||||||
Tag event.Tag
|
Tag event.Tag
|
||||||
// Type is the MIME type of Data.
|
// Type is the MIME type of Data.
|
||||||
// It must be the Type from the corresponding RequestEvent.
|
// It must be the Type from the corresponding RequestEvent.
|
||||||
@@ -50,32 +32,33 @@ type OfferOp struct {
|
|||||||
// Data contains the offered data. It is closed when the
|
// Data contains the offered data. It is closed when the
|
||||||
// transfer is complete or cancelled.
|
// transfer is complete or cancelled.
|
||||||
// Data must be kept valid until closed, and it may be used from
|
// Data must be kept valid until closed, and it may be used from
|
||||||
// a goroutine separate from the one processing the frame..
|
// a goroutine separate from the one processing the frame.
|
||||||
Data io.ReadCloser
|
Data io.ReadCloser
|
||||||
}
|
}
|
||||||
|
|
||||||
func (op SourceOp) Add(o *op.Ops) {
|
func (OfferCmd) ImplementsCommand() {}
|
||||||
data := ops.Write2(&o.Internal, ops.TypeSourceLen, op.Tag, op.Type)
|
|
||||||
data[0] = byte(ops.TypeSource)
|
// SourceFilter filters for any [RequestEvent] that match a MIME type
|
||||||
|
// as well as [InitiateEvent] and [CancelEvent].
|
||||||
|
// Use multiple filters to offer multiple types.
|
||||||
|
type SourceFilter struct {
|
||||||
|
// Target is a tag included in a previous event.Op.
|
||||||
|
Target event.Tag
|
||||||
|
// Type is the MIME type supported by this source.
|
||||||
|
Type string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (op TargetOp) Add(o *op.Ops) {
|
// TargetFilter filters for any [DataEvent] whose type matches a MIME type
|
||||||
data := ops.Write2(&o.Internal, ops.TypeTargetLen, op.Tag, op.Type)
|
// as well as [CancelEvent]. Use multiple filters to accept multiple types.
|
||||||
data[0] = byte(ops.TypeTarget)
|
type TargetFilter struct {
|
||||||
}
|
// Target is a tag included in a previous event.Op.
|
||||||
|
Target event.Tag
|
||||||
// Add the offer to the list of operations.
|
// Type is the MIME type accepted by this target.
|
||||||
// It panics if the Data field is not set.
|
Type string
|
||||||
func (op OfferOp) Add(o *op.Ops) {
|
|
||||||
if op.Data == nil {
|
|
||||||
panic("invalid nil data in OfferOp")
|
|
||||||
}
|
|
||||||
data := ops.Write3(&o.Internal, ops.TypeOfferLen, op.Tag, op.Type, op.Data)
|
|
||||||
data[0] = byte(ops.TypeOffer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestEvent requests data from a data source. The source must
|
// RequestEvent requests data from a data source. The source must
|
||||||
// respond with an OfferOp.
|
// respond with an OfferCmd.
|
||||||
type RequestEvent struct {
|
type RequestEvent struct {
|
||||||
// Type is the first matched type between the source and the target.
|
// Type is the first matched type between the source and the target.
|
||||||
Type string
|
Type string
|
||||||
@@ -107,3 +90,6 @@ type DataEvent struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (DataEvent) ImplementsEvent() {}
|
func (DataEvent) ImplementsEvent() {}
|
||||||
|
|
||||||
|
func (SourceFilter) ImplementsFilter() {}
|
||||||
|
func (TargetFilter) ImplementsFilter() {}
|
||||||
|
|||||||
+5
-57
@@ -3,10 +3,9 @@
|
|||||||
package layout
|
package layout
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/input"
|
||||||
"gioui.org/io/system"
|
"gioui.org/io/system"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
@@ -21,9 +20,6 @@ type Context struct {
|
|||||||
Constraints Constraints
|
Constraints Constraints
|
||||||
|
|
||||||
Metric unit.Metric
|
Metric unit.Metric
|
||||||
// By convention, a nil Queue is a signal to widgets to draw themselves
|
|
||||||
// in a disabled state.
|
|
||||||
Queue event.Queue
|
|
||||||
// Now is the animation time.
|
// Now is the animation time.
|
||||||
Now time.Time
|
Now time.Time
|
||||||
|
|
||||||
@@ -32,46 +28,10 @@ type Context struct {
|
|||||||
// Interested users must look up and populate these values manually.
|
// Interested users must look up and populate these values manually.
|
||||||
Locale system.Locale
|
Locale system.Locale
|
||||||
|
|
||||||
|
input.Source
|
||||||
*op.Ops
|
*op.Ops
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContext is a shorthand for
|
|
||||||
//
|
|
||||||
// Context{
|
|
||||||
// Ops: ops,
|
|
||||||
// Now: e.Now,
|
|
||||||
// Queue: e.Queue,
|
|
||||||
// Config: e.Config,
|
|
||||||
// Constraints: Exact(e.Size),
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// NewContext calls ops.Reset and adjusts ops for e.Insets.
|
|
||||||
func NewContext(ops *op.Ops, e system.FrameEvent) Context {
|
|
||||||
ops.Reset()
|
|
||||||
|
|
||||||
size := e.Size
|
|
||||||
|
|
||||||
if e.Insets != (system.Insets{}) {
|
|
||||||
left := e.Metric.Dp(e.Insets.Left)
|
|
||||||
top := e.Metric.Dp(e.Insets.Top)
|
|
||||||
op.Offset(image.Point{
|
|
||||||
X: left,
|
|
||||||
Y: top,
|
|
||||||
}).Add(ops)
|
|
||||||
|
|
||||||
size.X -= left + e.Metric.Dp(e.Insets.Right)
|
|
||||||
size.Y -= top + e.Metric.Dp(e.Insets.Bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Context{
|
|
||||||
Ops: ops,
|
|
||||||
Now: e.Now,
|
|
||||||
Queue: e.Queue,
|
|
||||||
Metric: e.Metric,
|
|
||||||
Constraints: Exact(size),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dp converts v to pixels.
|
// Dp converts v to pixels.
|
||||||
func (c Context) Dp(v unit.Dp) int {
|
func (c Context) Dp(v unit.Dp) int {
|
||||||
return c.Metric.Dp(v)
|
return c.Metric.Dp(v)
|
||||||
@@ -82,21 +42,9 @@ func (c Context) Sp(v unit.Sp) int {
|
|||||||
return c.Metric.Sp(v)
|
return c.Metric.Sp(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events returns the events available for the key. If no
|
// Disabled returns a copy of this context with a disabled Source,
|
||||||
// queue is configured, Events returns nil.
|
// blocking widgets from changing its state and receiving events.
|
||||||
func (c Context) Events(k event.Tag) []event.Event {
|
|
||||||
if c.Queue == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return c.Queue.Events(k)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disabled returns a copy of this context with a nil Queue,
|
|
||||||
// blocking events to widgets using it.
|
|
||||||
//
|
|
||||||
// By convention, a nil Queue is a signal to widgets to draw themselves
|
|
||||||
// in a disabled state.
|
|
||||||
func (c Context) Disabled() Context {
|
func (c Context) Disabled() Context {
|
||||||
c.Queue = nil
|
c.Source = input.Source{}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
+20
-20
@@ -144,7 +144,25 @@ 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))
|
min, max := int(-inf), int(inf)
|
||||||
|
if l.Position.First == 0 {
|
||||||
|
// Use the size of the invisible part as scroll boundary.
|
||||||
|
min = -l.Position.Offset
|
||||||
|
if min > 0 {
|
||||||
|
min = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.Position.First+l.Position.Count == l.len {
|
||||||
|
max = -l.Position.OffsetLast
|
||||||
|
if max < 0 {
|
||||||
|
max = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scrollRange := image.Rectangle{
|
||||||
|
Min: l.Axis.Convert(image.Pt(min, 0)),
|
||||||
|
Max: l.Axis.Convert(image.Pt(max, 0)),
|
||||||
|
}
|
||||||
|
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), scrollRange)
|
||||||
l.scrollDelta = d
|
l.scrollDelta = d
|
||||||
l.Position.Offset += d
|
l.Position.Offset += d
|
||||||
}
|
}
|
||||||
@@ -332,25 +350,7 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
|
|||||||
call := macro.Stop()
|
call := macro.Stop()
|
||||||
defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop()
|
defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop()
|
||||||
|
|
||||||
min, max := int(-inf), int(inf)
|
l.scroll.Add(ops)
|
||||||
if l.Position.First == 0 {
|
|
||||||
// Use the size of the invisible part as scroll boundary.
|
|
||||||
min = -l.Position.Offset
|
|
||||||
if min > 0 {
|
|
||||||
min = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if l.Position.First+l.Position.Count == l.len {
|
|
||||||
max = -l.Position.OffsetLast
|
|
||||||
if max < 0 {
|
|
||||||
max = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scrollRange := image.Rectangle{
|
|
||||||
Min: l.Axis.Convert(image.Pt(min, 0)),
|
|
||||||
Max: l.Axis.Convert(image.Pt(max, 0)),
|
|
||||||
}
|
|
||||||
l.scroll.Add(ops, scrollRange)
|
|
||||||
|
|
||||||
call.Add(ops)
|
call.Add(ops)
|
||||||
return Dimensions{Size: dims}
|
return Dimensions{Size: dims}
|
||||||
|
|||||||
+12
-12
@@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
|
"gioui.org/io/input"
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
"gioui.org/io/router"
|
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,13 +64,13 @@ func TestListScrollToEnd(t *testing.T) {
|
|||||||
|
|
||||||
func TestListPosition(t *testing.T) {
|
func TestListPosition(t *testing.T) {
|
||||||
_s := func(e ...event.Event) []event.Event { return e }
|
_s := func(e ...event.Event) []event.Event { return e }
|
||||||
r := new(router.Router)
|
r := new(input.Router)
|
||||||
gtx := Context{
|
gtx := Context{
|
||||||
Ops: new(op.Ops),
|
Ops: new(op.Ops),
|
||||||
Constraints: Constraints{
|
Constraints: Constraints{
|
||||||
Max: image.Pt(20, 10),
|
Max: image.Pt(20, 10),
|
||||||
},
|
},
|
||||||
Queue: r,
|
Source: r.Source(),
|
||||||
}
|
}
|
||||||
el := func(gtx Context, idx int) Dimensions {
|
el := func(gtx Context, idx int) Dimensions {
|
||||||
return Dimensions{Size: image.Pt(10, 10)}
|
return Dimensions{Size: image.Pt(10, 10)}
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ The MacroOp records a list of operations to be executed later:
|
|||||||
ops := new(op.Ops)
|
ops := new(op.Ops)
|
||||||
macro := op.Record(ops)
|
macro := op.Record(ops)
|
||||||
// Record operations by adding them.
|
// Record operations by adding them.
|
||||||
op.InvalidateOp{}.Add(ops)
|
|
||||||
...
|
...
|
||||||
// End recording.
|
// End recording.
|
||||||
call := macro.Stop()
|
call := macro.Stop()
|
||||||
@@ -96,9 +95,9 @@ type CallOp struct {
|
|||||||
end ops.PC
|
end ops.PC
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvalidateOp requests a redraw at the given time. Use
|
// InvalidateCmd requests a redraw at the given time. Use
|
||||||
// the zero value to request an immediate redraw.
|
// the zero value to request an immediate redraw.
|
||||||
type InvalidateOp struct {
|
type InvalidateCmd struct {
|
||||||
At time.Time
|
At time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +110,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,19 +180,6 @@ func (c CallOp) Add(o *Ops) {
|
|||||||
ops.AddCall(&o.Internal, c.ops, c.start, c.end)
|
ops.AddCall(&o.Internal, c.ops, c.start, c.end)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r InvalidateOp) Add(o *Ops) {
|
|
||||||
data := ops.Write(&o.Internal, ops.TypeRedrawLen)
|
|
||||||
data[0] = byte(ops.TypeInvalidate)
|
|
||||||
bo := binary.LittleEndian
|
|
||||||
// UnixNano cannot represent the zero time.
|
|
||||||
if t := r.At; !t.IsZero() {
|
|
||||||
nanos := t.UnixNano()
|
|
||||||
if nanos > 0 {
|
|
||||||
bo.PutUint64(data[1:], uint64(nanos))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offset converts an offset to a TransformOp.
|
// Offset converts an offset to a TransformOp.
|
||||||
func Offset(off image.Point) TransformOp {
|
func Offset(off image.Point) TransformOp {
|
||||||
offf := f32.Pt(float32(off.X), float32(off.Y))
|
offf := f32.Pt(float32(off.X), float32(off.Y))
|
||||||
@@ -240,3 +226,5 @@ func (t TransformStack) Pop() {
|
|||||||
data := ops.Write(t.ops, ops.TypePopTransformLen)
|
data := ops.Write(t.ops, ops.TypePopTransformLen)
|
||||||
data[0] = byte(ops.TypePopTransform)
|
data[0] = byte(ops.TypePopTransform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (InvalidateCmd) ImplementsCommand() {}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user