mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 718be79d9e | |||
| 0073e1a167 | |||
| 32ecec5538 | |||
| 6eb33b8a56 | |||
| 4617526e12 | |||
| dbc7a900bd | |||
| fb3ae95b28 |
+1
-1
@@ -29,7 +29,7 @@ environment:
|
||||
tasks:
|
||||
- install_go: |
|
||||
mkdir -p /home/build/sdk
|
||||
curl -s https://dl.google.com/go/go1.22.2.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: |
|
||||
mkdir -p $APPLE_TOOLCHAIN_ROOT
|
||||
cd $APPLE_TOOLCHAIN_ROOT
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ environment:
|
||||
tasks:
|
||||
- install_go: |
|
||||
mkdir -p /home/build/sdk
|
||||
curl https://dl.google.com/go/go1.22.2.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: |
|
||||
cd gio
|
||||
go test ./...
|
||||
|
||||
+1
-3
@@ -40,7 +40,7 @@ secrets:
|
||||
tasks:
|
||||
- install_go: |
|
||||
mkdir -p /home/build/sdk
|
||||
curl -s https://dl.google.com/go/go1.22.2.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: |
|
||||
cd gio
|
||||
test -z "$(gofmt -s -l .)"
|
||||
@@ -80,8 +80,6 @@ tasks:
|
||||
unzip -q ndk.zip
|
||||
rm ndk.zip
|
||||
mv android-ndk-* ndk-bundle
|
||||
# sdkmanager needs lots of file descriptors
|
||||
ulimit -n 10000
|
||||
yes|sdkmanager --licenses
|
||||
sdkmanager "platforms;android-31" "build-tools;32.0.0"
|
||||
- test_android: |
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ environment:
|
||||
tasks:
|
||||
- install_go: |
|
||||
mkdir -p /home/build/sdk
|
||||
curl https://dl.google.com/go/go1.22.2.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
|
||||
./make.bash
|
||||
- test_gio: |
|
||||
|
||||
+1
-1
@@ -65,8 +65,8 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
|
||||
private final InputMethodManager imm;
|
||||
private final float scrollXScale;
|
||||
private final float scrollYScale;
|
||||
private final AccessibilityManager accessManager;
|
||||
private int keyboardHint;
|
||||
private AccessibilityManager accessManager;
|
||||
|
||||
private long nhandle;
|
||||
|
||||
|
||||
+9
-93
@@ -3,16 +3,9 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
|
||||
// extraArgs contains extra arguments to append to
|
||||
@@ -27,88 +20,23 @@ var extraArgs string
|
||||
// On Android ID is the package property of AndroidManifest.xml,
|
||||
// on iOS ID is the CFBundleIdentifier of the app Info.plist,
|
||||
// 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 [gioui.org/cmd/gogio] tool or manually with the -X linker flag. For example,
|
||||
// ID is set by the gogio tool or manually with the -X linker flag. For example,
|
||||
//
|
||||
// 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
|
||||
// is not supported. The default value of ID is filepath.Base(os.Args[0]).
|
||||
// is not supported. Default value of ID is filepath.Base(os.Args[0]).
|
||||
var ID = ""
|
||||
|
||||
// A FrameEvent requests a new frame in the form of a list of
|
||||
// operations that describes the window content.
|
||||
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)
|
||||
// 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()
|
||||
// Valid will return true when the ViewEvent does contains valid handles.
|
||||
// If a window receives an invalid ViewEvent, it should deinitialize any
|
||||
// state referring to handles from a previous ViewEvent.
|
||||
Valid() bool
|
||||
}
|
||||
|
||||
// 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)
|
||||
func init() {
|
||||
if extraArgs != "" {
|
||||
args := strings.Split(extraArgs, "|")
|
||||
os.Args = append(os.Args, args...)
|
||||
}
|
||||
|
||||
return layout.Context{
|
||||
Ops: ops,
|
||||
Now: e.Now,
|
||||
Source: e.Source,
|
||||
Metric: e.Metric,
|
||||
Constraints: layout.Exact(size),
|
||||
if ID == "" {
|
||||
ID = filepath.Base(os.Args[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,15 +63,3 @@ func DataDir() (string, error) {
|
||||
func Main() {
|
||||
osMain()
|
||||
}
|
||||
|
||||
func (FrameEvent) ImplementsEvent() {}
|
||||
|
||||
func init() {
|
||||
if extraArgs != "" {
|
||||
args := strings.Split(extraArgs, "|")
|
||||
os.Args = append(os.Args, args...)
|
||||
}
|
||||
if ID == "" {
|
||||
ID = filepath.Base(os.Args[0])
|
||||
}
|
||||
}
|
||||
|
||||
+18
-11
@@ -8,20 +8,21 @@ See https://gioui.org for instructions to set up and run Gio programs.
|
||||
|
||||
# Windows
|
||||
|
||||
A Window is run by calling its Event method in a loop. The first time a
|
||||
method on Window is called, a new GUI window is created and shown. On mobile
|
||||
platforms or when Gio is embedded in another project, Window merely connects
|
||||
with a previously created GUI window.
|
||||
Create a new Window by calling NewWindow. On mobile platforms or when Gio
|
||||
is embedded in another project, NewWindow merely connects with a previously
|
||||
created window.
|
||||
|
||||
The most important event is [FrameEvent] that prompts an update of the window
|
||||
contents.
|
||||
A Window is run by calling NextEvent in a loop. The most important event is
|
||||
FrameEvent that prompts an update of the window contents.
|
||||
|
||||
For example:
|
||||
|
||||
w := new(app.Window)
|
||||
import "gioui.org/unit"
|
||||
|
||||
w := app.NewWindow()
|
||||
for {
|
||||
e := w.Event()
|
||||
if e, ok := e.(app.FrameEvent); ok {
|
||||
e := w.NextEvent()
|
||||
if e, ok := e.(system.FrameEvent); ok {
|
||||
ops.Reset()
|
||||
// Add operations to ops.
|
||||
...
|
||||
@@ -31,7 +32,7 @@ For example:
|
||||
}
|
||||
|
||||
A program must keep receiving events from the event channel until
|
||||
[DestroyEvent] is received.
|
||||
DestroyEvent is received.
|
||||
|
||||
# Main
|
||||
|
||||
@@ -50,12 +51,18 @@ For example, to display a blank but otherwise functional window:
|
||||
go func() {
|
||||
w := app.NewWindow()
|
||||
for {
|
||||
w.Event()
|
||||
w.NextEvent()
|
||||
}
|
||||
}()
|
||||
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
|
||||
|
||||
The packages under gioui.org/app/permission should be imported
|
||||
|
||||
+6
-4
@@ -17,8 +17,9 @@ import (
|
||||
)
|
||||
|
||||
type androidContext struct {
|
||||
win *window
|
||||
eglSurf egl.NativeWindowType
|
||||
win *window
|
||||
eglSurf egl.NativeWindowType
|
||||
width, height int
|
||||
*egl.Context
|
||||
}
|
||||
|
||||
@@ -44,8 +45,9 @@ func (c *androidContext) Refresh() error {
|
||||
if err := c.win.setVisual(c.Context.VisualID()); err != nil {
|
||||
return err
|
||||
}
|
||||
win, _, _ := c.win.nativeWindow()
|
||||
win, width, height := c.win.nativeWindow()
|
||||
c.eglSurf = egl.NativeWindowType(unsafe.Pointer(win))
|
||||
c.width, c.height = width, height
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -53,7 +55,7 @@ func (c *androidContext) Lock() error {
|
||||
// The Android emulator creates a broken surface if it is not
|
||||
// created on the same thread as the context is made current.
|
||||
if c.eglSurf != nil {
|
||||
if err := c.Context.CreateSurface(c.eglSurf); err != nil {
|
||||
if err := c.Context.CreateSurface(c.eglSurf, c.width, c.height); err != nil {
|
||||
return err
|
||||
}
|
||||
c.eglSurf = nil
|
||||
|
||||
+1
-11
@@ -69,17 +69,7 @@ func (c *wlContext) Refresh() error {
|
||||
}
|
||||
c.eglWin = eglWin
|
||||
eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin)))
|
||||
if err := c.Context.CreateSurface(eglSurf); 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
|
||||
return c.Context.CreateSurface(eglSurf, width, height)
|
||||
}
|
||||
|
||||
func (c *wlContext) Lock() error {
|
||||
|
||||
+17
-12
@@ -5,6 +5,8 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"gioui.org/internal/egl"
|
||||
)
|
||||
|
||||
@@ -22,18 +24,6 @@ func init() {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
win, _, _ := w.HWND()
|
||||
eglSurf := egl.NativeWindowType(win)
|
||||
if err := ctx.CreateSurface(eglSurf); err != nil {
|
||||
ctx.Release()
|
||||
return nil, err
|
||||
}
|
||||
if err := ctx.MakeCurrent(); err != nil {
|
||||
ctx.Release()
|
||||
return nil, err
|
||||
}
|
||||
defer ctx.ReleaseCurrent()
|
||||
ctx.EnableVSync(true)
|
||||
return &glContext{win: w, Context: ctx}, nil
|
||||
},
|
||||
})
|
||||
@@ -47,6 +37,21 @@ func (c *glContext) Release() {
|
||||
}
|
||||
|
||||
func (c *glContext) Refresh() error {
|
||||
c.Context.ReleaseSurface()
|
||||
var (
|
||||
win windows.Handle
|
||||
width, height int
|
||||
)
|
||||
win, width, height = c.win.HWND()
|
||||
eglSurf := egl.NativeWindowType(win)
|
||||
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Context.MakeCurrent(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Context.EnableVSync(true)
|
||||
c.Context.ReleaseCurrent()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+11
-12
@@ -25,18 +25,6 @@ func init() {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
win, _, _ := w.window()
|
||||
eglSurf := egl.NativeWindowType(uintptr(win))
|
||||
if err := ctx.CreateSurface(eglSurf); err != nil {
|
||||
ctx.Release()
|
||||
return nil, err
|
||||
}
|
||||
if err := ctx.MakeCurrent(); err != nil {
|
||||
ctx.Release()
|
||||
return nil, err
|
||||
}
|
||||
defer ctx.ReleaseCurrent()
|
||||
ctx.EnableVSync(true)
|
||||
return &x11Context{win: w, Context: ctx}, nil
|
||||
}
|
||||
}
|
||||
@@ -49,6 +37,17 @@ func (c *x11Context) Release() {
|
||||
}
|
||||
|
||||
func (c *x11Context) Refresh() error {
|
||||
c.Context.ReleaseSurface()
|
||||
win, width, height := c.win.window()
|
||||
eglSurf := egl.NativeWindowType(uintptr(win))
|
||||
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Context.MakeCurrent(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Context.EnableVSync(true)
|
||||
c.Context.ReleaseCurrent()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
#include <OpenGL/OpenGL.h>
|
||||
#include "_cgo_export.h"
|
||||
|
||||
CALayer *gio_layerFactory(BOOL presentWithTrans) {
|
||||
CALayer *gio_layerFactory(void) {
|
||||
@autoreleasepool {
|
||||
return [CALayer layer];
|
||||
}
|
||||
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
+4
-4
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/font/gofont"
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/router"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/text"
|
||||
@@ -31,10 +31,10 @@ func FuzzIME(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, cmds []byte) {
|
||||
cache := text.NewShaper(text.WithCollection(gofont.Collection()))
|
||||
e := new(widget.Editor)
|
||||
e.Focus()
|
||||
|
||||
var r input.Router
|
||||
gtx := layout.Context{Ops: new(op.Ops), Source: r.Source()}
|
||||
gtx.Execute(key.FocusCmd{Tag: e})
|
||||
var r router.Router
|
||||
gtx := layout.Context{Ops: new(op.Ops), Queue: &r}
|
||||
// Layout once to register focus.
|
||||
e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
|
||||
r.Frame(gtx.Ops)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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"
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package app
|
||||
package log
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -llog
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
// 1024 is the truncation limit from android/log.h, plus a \n.
|
||||
const logLineLimit = 1024
|
||||
|
||||
var logTag = C.CString(ID)
|
||||
var logTag = C.CString(appID)
|
||||
|
||||
func init() {
|
||||
// Android's logcat already includes timestamps.
|
||||
@@ -3,7 +3,7 @@
|
||||
//go:build darwin && ios
|
||||
// +build darwin,ios
|
||||
|
||||
package app
|
||||
package log
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package app
|
||||
package log
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -266,7 +266,6 @@ const (
|
||||
WM_MOUSEWHEEL = 0x020A
|
||||
WM_MOUSEHWHEEL = 0x020E
|
||||
WM_NCACTIVATE = 0x0086
|
||||
WM_NCLBUTTONDBLCLK = 0x00A3
|
||||
WM_NCHITTEST = 0x0084
|
||||
WM_NCCALCSIZE = 0x0083
|
||||
WM_PAINT = 0x000F
|
||||
|
||||
@@ -238,17 +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))
|
||||
}
|
||||
|
||||
func convertKeysym(s C.xkb_keysym_t) (key.Name, bool) {
|
||||
func convertKeysym(s C.xkb_keysym_t) (string, bool) {
|
||||
if 'a' <= s && s <= 'z' {
|
||||
return key.Name(rune(s - 'a' + 'A')), true
|
||||
return string(rune(s - 'a' + 'A')), true
|
||||
}
|
||||
if C.XKB_KEY_KP_0 <= s && s <= C.XKB_KEY_KP_9 {
|
||||
return key.Name(rune(s - C.XKB_KEY_KP_0 + '0')), true
|
||||
return string(rune(s - C.XKB_KEY_KP_0 + '0')), true
|
||||
}
|
||||
if ' ' < s && s <= '~' {
|
||||
return key.Name(rune(s)), true
|
||||
return string(rune(s)), true
|
||||
}
|
||||
var n key.Name
|
||||
var n string
|
||||
switch s {
|
||||
case C.XKB_KEY_Escape:
|
||||
n = key.NameEscape
|
||||
|
||||
+1
-2
@@ -60,9 +60,8 @@ static void presentDrawable(CFTypeRef queueRef, CFTypeRef drawableRef) {
|
||||
id<MTLDrawable> drawable = (__bridge id<MTLDrawable>)drawableRef;
|
||||
id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef;
|
||||
id<MTLCommandBuffer> cmdBuffer = [queue commandBuffer];
|
||||
[cmdBuffer presentDrawable:drawable];
|
||||
[cmdBuffer commit];
|
||||
[cmdBuffer waitUntilScheduled];
|
||||
[drawable present];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -21,10 +21,7 @@ Class gio_layerClass(void) {
|
||||
static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
UIView *view = (__bridge UIView *)viewRef;
|
||||
CAMetalLayer *l = (CAMetalLayer *)view.layer;
|
||||
l.needsDisplayOnBoundsChange = YES;
|
||||
l.presentsWithTransaction = YES;
|
||||
return CFBridgingRetain(l);
|
||||
return CFBridgingRetain(view.layer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-6
@@ -12,13 +12,9 @@ package app
|
||||
#import <QuartzCore/CAMetalLayer.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
CALayer *gio_layerFactory(BOOL presentWithTrans) {
|
||||
CALayer *gio_layerFactory(void) {
|
||||
@autoreleasepool {
|
||||
CAMetalLayer *l = [CAMetalLayer layer];
|
||||
l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable;
|
||||
l.needsDisplayOnBoundsChange = YES;
|
||||
l.presentsWithTransaction = presentWithTrans;
|
||||
return l;
|
||||
return [CAMetalLayer layer];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/op"
|
||||
|
||||
"gioui.org/gpu"
|
||||
"gioui.org/io/pointer"
|
||||
@@ -45,8 +43,6 @@ type Config struct {
|
||||
CustomRenderer bool
|
||||
// Decorated reports whether window decorations are provided automatically.
|
||||
Decorated bool
|
||||
// Focused reports whether has the keyboard focus.
|
||||
Focused bool
|
||||
// decoHeight is the height of the fallback decoration for platforms such
|
||||
// as Wayland that may need fallback client-side decorations.
|
||||
decoHeight unit.Dp
|
||||
@@ -135,30 +131,8 @@ func (o Orientation) String() string {
|
||||
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 {
|
||||
FrameEvent
|
||||
system.FrameEvent
|
||||
|
||||
Sync bool
|
||||
}
|
||||
@@ -173,13 +147,9 @@ type context interface {
|
||||
Unlock()
|
||||
}
|
||||
|
||||
// driver is the interface for the platform implementation
|
||||
// Driver is the interface for the platform implementation
|
||||
// of a window.
|
||||
type driver interface {
|
||||
// Event blocks until an event is available and returns it.
|
||||
Event() event.Event
|
||||
// Invalidate requests a FrameEvent.
|
||||
Invalidate()
|
||||
// SetAnimating sets the animation flag. When the window is animating,
|
||||
// FrameEvents are delivered as fast as the display can handle them.
|
||||
SetAnimating(anim bool)
|
||||
@@ -190,29 +160,23 @@ type driver interface {
|
||||
// ReadClipboard requests the clipboard content.
|
||||
ReadClipboard()
|
||||
// WriteClipboard requests a clipboard write.
|
||||
WriteClipboard(mime string, s []byte)
|
||||
WriteClipboard(s string)
|
||||
// Configure the window.
|
||||
Configure([]Option)
|
||||
// SetCursor updates the current cursor to name.
|
||||
SetCursor(cursor pointer.Cursor)
|
||||
// Wakeup wakes up the event loop and sends a WakeupEvent.
|
||||
// Wakeup()
|
||||
Wakeup()
|
||||
// Perform actions on the window.
|
||||
Perform(system.Action)
|
||||
// EditorStateChanged notifies the driver that the editor state changed.
|
||||
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 {
|
||||
in chan windowAndConfig
|
||||
out chan windowAndConfig
|
||||
windows chan struct{}
|
||||
in chan windowAndConfig
|
||||
out chan windowAndConfig
|
||||
errs chan error
|
||||
}
|
||||
|
||||
type windowAndConfig struct {
|
||||
@@ -222,137 +186,32 @@ type windowAndConfig struct {
|
||||
|
||||
func newWindowRendezvous() *windowRendezvous {
|
||||
wr := &windowRendezvous{
|
||||
in: make(chan windowAndConfig),
|
||||
out: make(chan windowAndConfig),
|
||||
windows: make(chan struct{}),
|
||||
in: make(chan windowAndConfig),
|
||||
out: make(chan windowAndConfig),
|
||||
errs: make(chan error),
|
||||
}
|
||||
go func() {
|
||||
in := wr.in
|
||||
var window windowAndConfig
|
||||
var main windowAndConfig
|
||||
var out chan windowAndConfig
|
||||
for {
|
||||
select {
|
||||
case w := <-in:
|
||||
window = w
|
||||
case w := <-wr.in:
|
||||
var err error
|
||||
if main.window != nil {
|
||||
err = errors.New("multiple windows are not supported")
|
||||
}
|
||||
wr.errs <- err
|
||||
main = w
|
||||
out = wr.out
|
||||
case out <- window:
|
||||
case out <- main:
|
||||
}
|
||||
}
|
||||
}()
|
||||
return wr
|
||||
}
|
||||
|
||||
func newEventLoop(w *callbacks, wakeup func()) *eventLoop {
|
||||
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 (wakeupEvent) ImplementsEvent() {}
|
||||
func (ConfigEvent) ImplementsEvent() {}
|
||||
|
||||
func walkActions(actions system.Action, do func(system.Action)) {
|
||||
for a := system.Action(1); actions != 0; a <<= 1 {
|
||||
@@ -362,6 +221,3 @@ func walkActions(actions system.Action, do func(system.Action)) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wakeupEvent) ImplementsEvent() {}
|
||||
func (ConfigEvent) ImplementsEvent() {}
|
||||
|
||||
+94
-137
@@ -123,36 +123,31 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
|
||||
"gioui.org/internal/f32color"
|
||||
"gioui.org/op"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/router"
|
||||
"gioui.org/io/semantic"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
|
||||
type window struct {
|
||||
callbacks *callbacks
|
||||
loop *eventLoop
|
||||
|
||||
view C.jobject
|
||||
handle cgo.Handle
|
||||
@@ -161,19 +156,18 @@ type window struct {
|
||||
fontScale float32
|
||||
insets pixelInsets
|
||||
|
||||
visible bool
|
||||
stage system.Stage
|
||||
started bool
|
||||
animating bool
|
||||
|
||||
win *C.ANativeWindow
|
||||
config Config
|
||||
inputHint key.InputHint
|
||||
win *C.ANativeWindow
|
||||
config Config
|
||||
|
||||
semantic struct {
|
||||
hoverID input.SemanticID
|
||||
rootID input.SemanticID
|
||||
focusID input.SemanticID
|
||||
diffs []input.SemanticID
|
||||
hoverID router.SemanticID
|
||||
rootID router.SemanticID
|
||||
focusID router.SemanticID
|
||||
diffs []router.SemanticID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,9 +199,9 @@ type pixelInsets struct {
|
||||
top, bottom, left, right int
|
||||
}
|
||||
|
||||
// AndroidViewEvent is sent whenever the Window's underlying Android view
|
||||
// ViewEvent is sent whenever the Window's underlying Android view
|
||||
// changes.
|
||||
type AndroidViewEvent struct {
|
||||
type ViewEvent struct {
|
||||
// View is a JNI global reference to the android.view.View
|
||||
// instance backing the Window. The reference is valid until
|
||||
// the next ViewEvent is received.
|
||||
@@ -491,30 +485,24 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
|
||||
})
|
||||
view = C.jni_NewGlobalRef(env, view)
|
||||
wopts := <-mainWindow.out
|
||||
var cnf Config
|
||||
w, ok := windows[wopts.window]
|
||||
if !ok {
|
||||
w = &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
|
||||
} else {
|
||||
cnf = w.config
|
||||
}
|
||||
mainWindow.windows <- struct{}{}
|
||||
if w.view != 0 {
|
||||
w.detach(env)
|
||||
}
|
||||
w.view = view
|
||||
w.visible = false
|
||||
w.handle = cgo.NewHandle(w)
|
||||
w.callbacks.SetDriver(w)
|
||||
w.loadConfig(env, class)
|
||||
w.setConfig(env, cnf)
|
||||
w.SetInputHint(w.inputHint)
|
||||
w.processEvent(AndroidViewEvent{View: uintptr(view)})
|
||||
w.Configure(wopts.options)
|
||||
w.SetInputHint(key.HintAny)
|
||||
w.setStage(system.StagePaused)
|
||||
w.callbacks.Event(ViewEvent{View: uintptr(view)})
|
||||
return C.jlong(w.handle)
|
||||
}
|
||||
|
||||
@@ -528,7 +516,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) {
|
||||
w := cgo.Handle(handle).Value().(*window)
|
||||
w.started = false
|
||||
w.visible = false
|
||||
w.setStage(system.StagePaused)
|
||||
}
|
||||
|
||||
//export Java_org_gioui_GioView_onStartView
|
||||
@@ -544,7 +532,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) {
|
||||
w := cgo.Handle(handle).Value().(*window)
|
||||
w.win = nil
|
||||
w.visible = false
|
||||
w.setStage(system.StagePaused)
|
||||
}
|
||||
|
||||
//export Java_org_gioui_GioView_onSurfaceChanged
|
||||
@@ -566,7 +554,9 @@ 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) {
|
||||
w := cgo.Handle(view).Value().(*window)
|
||||
w.loadConfig(env, class)
|
||||
w.draw(env, true)
|
||||
if w.stage >= system.StageInactive {
|
||||
w.draw(env, true)
|
||||
}
|
||||
}
|
||||
|
||||
//export Java_org_gioui_GioView_onFrameCallback
|
||||
@@ -575,7 +565,10 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
if w.visible && w.animating {
|
||||
if w.stage < system.StageInactive {
|
||||
return
|
||||
}
|
||||
if w.animating {
|
||||
w.draw(env, false)
|
||||
callVoidMethod(env, w.view, gioView.postFrameCallback)
|
||||
}
|
||||
@@ -584,7 +577,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
|
||||
//export Java_org_gioui_GioView_onBack
|
||||
func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong) C.jboolean {
|
||||
w := cgo.Handle(view).Value().(*window)
|
||||
if w.processEvent(key.Event{Name: key.NameBack}) {
|
||||
if w.callbacks.Event(key.Event{Name: key.NameBack}) {
|
||||
return C.JNI_TRUE
|
||||
}
|
||||
return C.JNI_FALSE
|
||||
@@ -593,8 +586,7 @@ func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong)
|
||||
//export Java_org_gioui_GioView_onFocusChange
|
||||
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.config.Focused = focus == C.JNI_TRUE
|
||||
w.processEvent(ConfigEvent{Config: w.config})
|
||||
w.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE})
|
||||
}
|
||||
|
||||
//export Java_org_gioui_GioView_onWindowInsets
|
||||
@@ -606,7 +598,9 @@ func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C
|
||||
left: int(left),
|
||||
right: int(right),
|
||||
}
|
||||
w.draw(env, true)
|
||||
if w.stage >= system.StageInactive {
|
||||
w.draw(env, true)
|
||||
}
|
||||
}
|
||||
|
||||
//export Java_org_gioui_GioView_initializeAccessibilityNodeInfo
|
||||
@@ -667,35 +661,7 @@ func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNode, off image.Point, info C.jobject) error {
|
||||
for _, ch := range sem.Children {
|
||||
err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID)))
|
||||
if err != nil {
|
||||
@@ -738,7 +704,7 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem input.SemanticNode
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
if d.Gestures&input.ClickGesture != 0 {
|
||||
if d.Gestures&router.ClickGesture != 0 {
|
||||
addAction(ACTION_CLICK)
|
||||
}
|
||||
clsName := android.strings.androidViewView
|
||||
@@ -783,23 +749,25 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem input.SemanticNode
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *window) virtualIDFor(id input.SemanticID) C.jint {
|
||||
func (w *window) virtualIDFor(id router.SemanticID) C.jint {
|
||||
// TODO: Android virtual IDs are 32-bit Java integers, but childID is a int64.
|
||||
if id == w.semantic.rootID {
|
||||
return HOST_VIEW_ID
|
||||
}
|
||||
return C.jint(id)
|
||||
}
|
||||
|
||||
func (w *window) semIDFor(virtID C.jint) input.SemanticID {
|
||||
func (w *window) semIDFor(virtID C.jint) router.SemanticID {
|
||||
if virtID == HOST_VIEW_ID {
|
||||
return w.semantic.rootID
|
||||
}
|
||||
return input.SemanticID(virtID)
|
||||
return router.SemanticID(virtID)
|
||||
}
|
||||
|
||||
func (w *window) detach(env *C.JNIEnv) {
|
||||
callVoidMethod(env, w.view, gioView.unregister)
|
||||
w.processEvent(AndroidViewEvent{})
|
||||
w.callbacks.Event(ViewEvent{})
|
||||
w.callbacks.SetDriver(nil)
|
||||
w.handle.Delete()
|
||||
C.jni_DeleteGlobalRef(env, w.view)
|
||||
w.view = 0
|
||||
@@ -810,10 +778,18 @@ func (w *window) setVisible(env *C.JNIEnv) {
|
||||
if width == 0 || height == 0 {
|
||||
return
|
||||
}
|
||||
w.visible = true
|
||||
w.setStage(system.StageRunning)
|
||||
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 {
|
||||
if C.ANativeWindow_setBuffersGeometry(w.win, 0, 0, C.int32_t(visID)) != 0 {
|
||||
return errors.New("ANativeWindow_setBuffersGeometry failed")
|
||||
@@ -850,13 +826,10 @@ func (w *window) SetAnimating(anim 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)))
|
||||
if size != w.config.Size {
|
||||
w.config.Size = size
|
||||
w.processEvent(ConfigEvent{Config: w.config})
|
||||
w.callbacks.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
if size.X == 0 || size.Y == 0 {
|
||||
return
|
||||
@@ -864,14 +837,14 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
|
||||
const inchPrDp = 1.0 / 160
|
||||
ppdp := float32(w.dpi) * inchPrDp
|
||||
dppp := unit.Dp(1.0 / ppdp)
|
||||
insets := Insets{
|
||||
insets := system.Insets{
|
||||
Top: unit.Dp(w.insets.top) * dppp,
|
||||
Bottom: unit.Dp(w.insets.bottom) * dppp,
|
||||
Left: unit.Dp(w.insets.left) * dppp,
|
||||
Right: unit.Dp(w.insets.right) * dppp,
|
||||
}
|
||||
w.processEvent(frameEvent{
|
||||
FrameEvent: FrameEvent{
|
||||
w.callbacks.Event(frameEvent{
|
||||
FrameEvent: system.FrameEvent{
|
||||
Now: time.Now(),
|
||||
Size: w.config.Size,
|
||||
Insets: insets,
|
||||
@@ -925,8 +898,8 @@ func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) {
|
||||
f(env)
|
||||
}
|
||||
|
||||
func convertKeyCode(code C.jint) (key.Name, bool) {
|
||||
var n key.Name
|
||||
func convertKeyCode(code C.jint) (string, bool) {
|
||||
var n string
|
||||
switch code {
|
||||
case C.AKEYCODE_FORWARD_DEL:
|
||||
n = key.NameDeleteForward
|
||||
@@ -970,7 +943,7 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
|
||||
if pressed == C.JNI_TRUE {
|
||||
state = key.Press
|
||||
}
|
||||
w.processEvent(key.Event{Name: n, State: state})
|
||||
w.callbacks.Event(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).
|
||||
w.callbacks.EditorInsert(string(rune(r)))
|
||||
@@ -1020,7 +993,7 @@ func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C
|
||||
default:
|
||||
return
|
||||
}
|
||||
w.processEvent(pointer.Event{
|
||||
w.callbacks.Event(pointer.Event{
|
||||
Kind: kind,
|
||||
Source: src,
|
||||
Buttons: btns,
|
||||
@@ -1172,8 +1145,6 @@ func (w *window) ShowTextInput(show bool) {
|
||||
}
|
||||
|
||||
func (w *window) SetInputHint(mode key.InputHint) {
|
||||
w.inputHint = mode
|
||||
|
||||
// Constants defined at https://developer.android.com/reference/android/text/InputType.
|
||||
const (
|
||||
TYPE_NULL = 0
|
||||
@@ -1320,14 +1291,14 @@ func findClass(env *C.JNIEnv, name string) C.jclass {
|
||||
func osMain() {
|
||||
}
|
||||
|
||||
func newWindow(window *callbacks, options []Option) {
|
||||
func newWindow(window *callbacks, options []Option) error {
|
||||
mainWindow.in <- windowAndConfig{window, options}
|
||||
<-mainWindow.windows
|
||||
return <-mainWindow.errs
|
||||
}
|
||||
|
||||
func (w *window) WriteClipboard(mime string, s []byte) {
|
||||
func (w *window) WriteClipboard(s string) {
|
||||
runInJVM(javaVM(), func(env *C.JNIEnv) {
|
||||
jstr := javaString(env, string(s))
|
||||
jstr := javaString(env, s)
|
||||
callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard,
|
||||
jvalue(android.appCtx), jvalue(jstr))
|
||||
})
|
||||
@@ -1341,54 +1312,45 @@ func (w *window) ReadClipboard() {
|
||||
return
|
||||
}
|
||||
content := goString(env, C.jstring(c))
|
||||
w.processEvent(transfer.DataEvent{
|
||||
Type: "application/text",
|
||||
Open: func() io.ReadCloser {
|
||||
return io.NopCloser(strings.NewReader(content))
|
||||
},
|
||||
})
|
||||
w.callbacks.Event(clipboard.Event{Text: content})
|
||||
})
|
||||
}
|
||||
|
||||
func (w *window) Configure(options []Option) {
|
||||
cnf := w.config
|
||||
cnf.apply(unit.Metric{}, options)
|
||||
runInJVM(javaVM(), func(env *C.JNIEnv) {
|
||||
w.setConfig(env, cnf)
|
||||
})
|
||||
}
|
||||
prev := w.config
|
||||
cnf := w.config
|
||||
cnf.apply(unit.Metric{}, options)
|
||||
// Decorations are never disabled.
|
||||
cnf.Decorated = true
|
||||
|
||||
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 prev.Orientation != cnf.Orientation {
|
||||
w.config.Orientation = cnf.Orientation
|
||||
setOrientation(env, w.view, cnf.Orientation)
|
||||
}
|
||||
}
|
||||
if cnf.Decorated != prev.Decorated {
|
||||
w.config.Decorated = cnf.Decorated
|
||||
}
|
||||
w.processEvent(ConfigEvent{Config: w.config})
|
||||
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) Perform(system.Action) {}
|
||||
@@ -1399,10 +1361,9 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
|
||||
})
|
||||
}
|
||||
|
||||
func (w *window) wakeup() {
|
||||
func (w *window) Wakeup() {
|
||||
runOnMain(func(env *C.JNIEnv) {
|
||||
w.loop.Wakeup()
|
||||
w.loop.FlushEvents()
|
||||
w.callbacks.Event(wakeupEvent{})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1493,8 +1454,4 @@ func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {
|
||||
}
|
||||
}
|
||||
|
||||
func (AndroidViewEvent) implementsViewEvent() {}
|
||||
func (AndroidViewEvent) ImplementsEvent() {}
|
||||
func (a AndroidViewEvent) Valid() bool {
|
||||
return a != (AndroidViewEvent{})
|
||||
}
|
||||
func (_ ViewEvent) ImplementsEvent() {}
|
||||
|
||||
+3
-8
@@ -75,13 +75,9 @@ var displayLinks sync.Map
|
||||
|
||||
var mainFuncs = make(chan func(), 1)
|
||||
|
||||
func isMainThread() bool {
|
||||
return bool(C.isMainThread())
|
||||
}
|
||||
|
||||
// runOnMain runs the function on the main thread.
|
||||
func runOnMain(f func()) {
|
||||
if isMainThread() {
|
||||
if C.isMainThread() {
|
||||
f()
|
||||
return
|
||||
}
|
||||
@@ -264,9 +260,8 @@ func windowSetCursor(from, to pointer.Cursor) pointer.Cursor {
|
||||
return to
|
||||
}
|
||||
|
||||
func (w *window) wakeup() {
|
||||
func (w *window) Wakeup() {
|
||||
runOnMain(func() {
|
||||
w.loop.Wakeup()
|
||||
w.loop.FlushEvents()
|
||||
w.w.Event(wakeupEvent{})
|
||||
})
|
||||
}
|
||||
|
||||
+65
-152
@@ -12,9 +12,6 @@ package app
|
||||
#include <UIKit/UIKit.h>
|
||||
#include <stdint.h>
|
||||
|
||||
__attribute__ ((visibility ("hidden"))) int gio_applicationMain(int argc, char *argv[]);
|
||||
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
|
||||
|
||||
struct drawParams {
|
||||
CGFloat dpi, sdpi;
|
||||
CGFloat width, height;
|
||||
@@ -22,7 +19,6 @@ struct drawParams {
|
||||
};
|
||||
|
||||
static void writeClipboard(unichar *chars, NSUInteger length) {
|
||||
#if !TARGET_OS_TV
|
||||
@autoreleasepool {
|
||||
NSString *s = [NSString string];
|
||||
if (length > 0) {
|
||||
@@ -31,18 +27,13 @@ static void writeClipboard(unichar *chars, NSUInteger length) {
|
||||
UIPasteboard *p = UIPasteboard.generalPasteboard;
|
||||
p.string = s;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static CFTypeRef readClipboard(void) {
|
||||
#if !TARGET_OS_TV
|
||||
@autoreleasepool {
|
||||
UIPasteboard *p = UIPasteboard.generalPasteboard;
|
||||
return (__bridge_retained CFTypeRef)p.string;
|
||||
}
|
||||
#else
|
||||
return nil;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void showTextInput(CFTypeRef viewRef) {
|
||||
@@ -81,27 +72,21 @@ import "C"
|
||||
|
||||
import (
|
||||
"image"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
|
||||
type UIKitViewEvent struct {
|
||||
type ViewEvent struct {
|
||||
// ViewController is a CFTypeRef for the UIViewController backing a Window.
|
||||
ViewController uintptr
|
||||
}
|
||||
@@ -110,17 +95,18 @@ type window struct {
|
||||
view C.CFTypeRef
|
||||
w *callbacks
|
||||
displayLink *displayLink
|
||||
loop *eventLoop
|
||||
|
||||
hidden bool
|
||||
cursor pointer.Cursor
|
||||
config Config
|
||||
visible bool
|
||||
cursor pointer.Cursor
|
||||
config Config
|
||||
|
||||
pointerMap []C.CFTypeRef
|
||||
}
|
||||
|
||||
var mainWindow = newWindowRendezvous()
|
||||
|
||||
var views = make(map[C.CFTypeRef]*window)
|
||||
|
||||
func init() {
|
||||
// Darwin requires UI operations happen on the main thread only.
|
||||
runtime.LockOSThread()
|
||||
@@ -128,59 +114,55 @@ func init() {
|
||||
|
||||
//export onCreate
|
||||
func onCreate(view, controller C.CFTypeRef) {
|
||||
wopts := <-mainWindow.out
|
||||
w := &window{
|
||||
view: view,
|
||||
w: wopts.window,
|
||||
}
|
||||
w.loop = newEventLoop(w.w, w.wakeup)
|
||||
w.w.SetDriver(w)
|
||||
mainWindow.windows <- struct{}{}
|
||||
dl, err := newDisplayLink(func() {
|
||||
w.draw(false)
|
||||
})
|
||||
if err != nil {
|
||||
w.w.ProcessEvent(DestroyEvent{Err: err})
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
w.displayLink = dl
|
||||
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
|
||||
wopts := <-mainWindow.out
|
||||
w.w = wopts.window
|
||||
w.w.SetDriver(w)
|
||||
views[view] = w
|
||||
w.Configure(wopts.options)
|
||||
w.ProcessEvent(UIKitViewEvent{ViewController: uintptr(controller)})
|
||||
}
|
||||
|
||||
func viewFor(h C.uintptr_t) *window {
|
||||
return cgo.Handle(h).Value().(*window)
|
||||
w.w.Event(system.StageEvent{Stage: system.StagePaused})
|
||||
w.w.Event(ViewEvent{ViewController: uintptr(controller)})
|
||||
}
|
||||
|
||||
//export gio_onDraw
|
||||
func gio_onDraw(h C.uintptr_t) {
|
||||
w := viewFor(h)
|
||||
func gio_onDraw(view C.CFTypeRef) {
|
||||
w := views[view]
|
||||
w.draw(true)
|
||||
}
|
||||
|
||||
func (w *window) draw(sync bool) {
|
||||
if w.hidden {
|
||||
return
|
||||
}
|
||||
params := C.viewDrawParams(w.view)
|
||||
if params.width == 0 || params.height == 0 {
|
||||
return
|
||||
}
|
||||
wasVisible := w.visible
|
||||
w.visible = true
|
||||
if !wasVisible {
|
||||
w.w.Event(system.StageEvent{Stage: system.StageRunning})
|
||||
}
|
||||
const inchPrDp = 1.0 / 163
|
||||
m := unit.Metric{
|
||||
PxPerDp: float32(params.dpi) * inchPrDp,
|
||||
PxPerSp: float32(params.sdpi) * inchPrDp,
|
||||
}
|
||||
dppp := unit.Dp(1. / m.PxPerDp)
|
||||
w.ProcessEvent(frameEvent{
|
||||
FrameEvent: FrameEvent{
|
||||
w.w.Event(frameEvent{
|
||||
FrameEvent: system.FrameEvent{
|
||||
Now: time.Now(),
|
||||
Size: image.Point{
|
||||
X: int(params.width + .5),
|
||||
Y: int(params.height + .5),
|
||||
},
|
||||
Insets: Insets{
|
||||
Insets: system.Insets{
|
||||
Top: unit.Dp(params.top) * dppp,
|
||||
Bottom: unit.Dp(params.bottom) * dppp,
|
||||
Left: unit.Dp(params.left) * dppp,
|
||||
@@ -193,34 +175,26 @@ func (w *window) draw(sync bool) {
|
||||
}
|
||||
|
||||
//export onStop
|
||||
func onStop(h C.uintptr_t) {
|
||||
w := viewFor(h)
|
||||
w.hidden = true
|
||||
}
|
||||
|
||||
//export onStart
|
||||
func onStart(h C.uintptr_t) {
|
||||
w := viewFor(h)
|
||||
w.hidden = false
|
||||
w.draw(true)
|
||||
func onStop(view C.CFTypeRef) {
|
||||
w := views[view]
|
||||
w.visible = false
|
||||
w.w.Event(system.StageEvent{Stage: system.StagePaused})
|
||||
}
|
||||
|
||||
//export onDestroy
|
||||
func onDestroy(h C.uintptr_t) {
|
||||
w := viewFor(h)
|
||||
w.ProcessEvent(UIKitViewEvent{})
|
||||
w.ProcessEvent(DestroyEvent{})
|
||||
func onDestroy(view C.CFTypeRef) {
|
||||
w := views[view]
|
||||
delete(views, view)
|
||||
w.w.Event(ViewEvent{})
|
||||
w.w.Event(system.DestroyEvent{})
|
||||
w.displayLink.Close()
|
||||
w.displayLink = nil
|
||||
cgo.Handle(h).Delete()
|
||||
w.view = 0
|
||||
}
|
||||
|
||||
//export onFocus
|
||||
func onFocus(h C.uintptr_t, focus int) {
|
||||
w := viewFor(h)
|
||||
w.config.Focused = focus != 0
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
func onFocus(view C.CFTypeRef, focus int) {
|
||||
w := views[view]
|
||||
w.w.Event(key.FocusEvent{Focus: focus != 0})
|
||||
}
|
||||
|
||||
//export onLowMemory
|
||||
@@ -230,38 +204,38 @@ func onLowMemory() {
|
||||
}
|
||||
|
||||
//export onUpArrow
|
||||
func onUpArrow(h C.uintptr_t) {
|
||||
viewFor(h).onKeyCommand(key.NameUpArrow)
|
||||
func onUpArrow(view C.CFTypeRef) {
|
||||
views[view].onKeyCommand(key.NameUpArrow)
|
||||
}
|
||||
|
||||
//export onDownArrow
|
||||
func onDownArrow(h C.uintptr_t) {
|
||||
viewFor(h).onKeyCommand(key.NameDownArrow)
|
||||
func onDownArrow(view C.CFTypeRef) {
|
||||
views[view].onKeyCommand(key.NameDownArrow)
|
||||
}
|
||||
|
||||
//export onLeftArrow
|
||||
func onLeftArrow(h C.uintptr_t) {
|
||||
viewFor(h).onKeyCommand(key.NameLeftArrow)
|
||||
func onLeftArrow(view C.CFTypeRef) {
|
||||
views[view].onKeyCommand(key.NameLeftArrow)
|
||||
}
|
||||
|
||||
//export onRightArrow
|
||||
func onRightArrow(h C.uintptr_t) {
|
||||
viewFor(h).onKeyCommand(key.NameRightArrow)
|
||||
func onRightArrow(view C.CFTypeRef) {
|
||||
views[view].onKeyCommand(key.NameRightArrow)
|
||||
}
|
||||
|
||||
//export onDeleteBackward
|
||||
func onDeleteBackward(h C.uintptr_t) {
|
||||
viewFor(h).onKeyCommand(key.NameDeleteBackward)
|
||||
func onDeleteBackward(view C.CFTypeRef) {
|
||||
views[view].onKeyCommand(key.NameDeleteBackward)
|
||||
}
|
||||
|
||||
//export onText
|
||||
func onText(h C.uintptr_t, str C.CFTypeRef) {
|
||||
w := viewFor(h)
|
||||
func onText(view, str C.CFTypeRef) {
|
||||
w := views[view]
|
||||
w.w.EditorInsert(nsstringToString(str))
|
||||
}
|
||||
|
||||
//export onTouch
|
||||
func onTouch(h C.uintptr_t, last C.int, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) {
|
||||
func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) {
|
||||
var kind pointer.Kind
|
||||
switch phase {
|
||||
case C.UITouchPhaseBegan:
|
||||
@@ -275,10 +249,10 @@ func onTouch(h C.uintptr_t, last C.int, touchRef C.CFTypeRef, phase C.NSInteger,
|
||||
default:
|
||||
return
|
||||
}
|
||||
w := viewFor(h)
|
||||
w := views[view]
|
||||
t := time.Duration(float64(ti) * float64(time.Second))
|
||||
p := f32.Point{X: float32(x), Y: float32(y)}
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: kind,
|
||||
Source: pointer.Touch,
|
||||
PointerID: w.lookupTouch(last != 0, touchRef),
|
||||
@@ -291,16 +265,11 @@ func (w *window) ReadClipboard() {
|
||||
cstr := C.readClipboard()
|
||||
defer C.CFRelease(cstr)
|
||||
content := nsstringToString(cstr)
|
||||
w.ProcessEvent(transfer.DataEvent{
|
||||
Type: "application/text",
|
||||
Open: func() io.ReadCloser {
|
||||
return io.NopCloser(strings.NewReader(content))
|
||||
},
|
||||
})
|
||||
w.w.Event(clipboard.Event{Text: content})
|
||||
}
|
||||
|
||||
func (w *window) WriteClipboard(mime string, s []byte) {
|
||||
u16 := utf16.Encode([]rune(string(s)))
|
||||
func (w *window) WriteClipboard(s string) {
|
||||
u16 := utf16.Encode([]rune(s))
|
||||
var chars *C.unichar
|
||||
if len(u16) > 0 {
|
||||
chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
|
||||
@@ -311,7 +280,7 @@ func (w *window) WriteClipboard(mime string, s []byte) {
|
||||
func (w *window) Configure([]Option) {
|
||||
// Decorations are never disabled.
|
||||
w.config.Decorated = true
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
|
||||
func (w *window) EditorStateChanged(old, new editorState) {}
|
||||
@@ -319,6 +288,10 @@ func (w *window) EditorStateChanged(old, new editorState) {}
|
||||
func (w *window) Perform(system.Action) {}
|
||||
|
||||
func (w *window) SetAnimating(anim bool) {
|
||||
v := w.view
|
||||
if v == 0 {
|
||||
return
|
||||
}
|
||||
if anim {
|
||||
w.displayLink.Start()
|
||||
} else {
|
||||
@@ -330,8 +303,8 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
|
||||
w.cursor = windowSetCursor(w.cursor, cursor)
|
||||
}
|
||||
|
||||
func (w *window) onKeyCommand(name key.Name) {
|
||||
w.ProcessEvent(key.Event{
|
||||
func (w *window) onKeyCommand(name string) {
|
||||
w.w.Event(key.Event{
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
@@ -370,77 +343,17 @@ func (w *window) ShowTextInput(show bool) {
|
||||
|
||||
func (w *window) SetInputHint(_ key.InputHint) {}
|
||||
|
||||
func (w *window) ProcessEvent(e event.Event) {
|
||||
w.w.ProcessEvent(e)
|
||||
w.loop.FlushEvents()
|
||||
}
|
||||
|
||||
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) {
|
||||
func newWindow(win *callbacks, options []Option) error {
|
||||
mainWindow.in <- windowAndConfig{win, options}
|
||||
<-mainWindow.windows
|
||||
return <-mainWindow.errs
|
||||
}
|
||||
|
||||
var mainMode = mainModeUndefined
|
||||
|
||||
const (
|
||||
mainModeUndefined = iota
|
||||
mainModeExe
|
||||
mainModeLibrary
|
||||
)
|
||||
|
||||
func osMain() {
|
||||
if !isMainThread() {
|
||||
panic("app.Main must be run on the main goroutine")
|
||||
}
|
||||
switch mainMode {
|
||||
case mainModeUndefined:
|
||||
mainMode = mainModeExe
|
||||
var argv []*C.char
|
||||
for _, arg := range os.Args {
|
||||
a := C.CString(arg)
|
||||
defer C.free(unsafe.Pointer(a))
|
||||
argv = append(argv, a)
|
||||
}
|
||||
C.gio_applicationMain(C.int(len(argv)), unsafe.SliceData(argv))
|
||||
case mainModeExe:
|
||||
panic("app.Main may be called only once")
|
||||
case mainModeLibrary:
|
||||
// Do nothing, we're embedded as a library.
|
||||
}
|
||||
}
|
||||
|
||||
//export gio_runMain
|
||||
func gio_runMain() {
|
||||
if !isMainThread() {
|
||||
panic("app.Main must be run on the main goroutine")
|
||||
}
|
||||
switch mainMode {
|
||||
case mainModeUndefined:
|
||||
mainMode = mainModeLibrary
|
||||
runMain()
|
||||
case mainModeExe:
|
||||
// Do nothing, main has already been called.
|
||||
}
|
||||
runMain()
|
||||
}
|
||||
|
||||
func (UIKitViewEvent) implementsViewEvent() {}
|
||||
func (UIKitViewEvent) ImplementsEvent() {}
|
||||
func (u UIKitViewEvent) Valid() bool {
|
||||
return u != (UIKitViewEvent{})
|
||||
}
|
||||
func (_ ViewEvent) ImplementsEvent() {}
|
||||
|
||||
+22
-51
@@ -11,7 +11,6 @@
|
||||
__attribute__ ((visibility ("hidden"))) Class gio_layerClass(void);
|
||||
|
||||
@interface GioView: UIView <UIKeyInput>
|
||||
@property uintptr_t handle;
|
||||
@end
|
||||
|
||||
@implementation GioViewController
|
||||
@@ -26,13 +25,12 @@ CGFloat _keyboardHeight;
|
||||
self.view.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
|
||||
UIView *drawView = [[GioView alloc] initWithFrame:zeroFrame];
|
||||
[self.view addSubview: drawView];
|
||||
#if !TARGET_OS_TV
|
||||
#ifndef TARGET_OS_TV
|
||||
drawView.multipleTouchEnabled = YES;
|
||||
#endif
|
||||
drawView.preservesSuperviewLayoutMargins = YES;
|
||||
drawView.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
|
||||
onCreate((__bridge CFTypeRef)drawView, (__bridge CFTypeRef)self);
|
||||
#if !TARGET_OS_TV
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(keyboardWillChange:)
|
||||
name:UIKeyboardWillShowNotification
|
||||
@@ -45,7 +43,6 @@ CGFloat _keyboardHeight;
|
||||
selector:@selector(keyboardWillHide:)
|
||||
name:UIKeyboardWillHideNotification
|
||||
object:nil];
|
||||
#endif
|
||||
[[NSNotificationCenter defaultCenter] addObserver: self
|
||||
selector: @selector(applicationDidEnterBackground:)
|
||||
name: UIApplicationDidEnterBackgroundNotification
|
||||
@@ -57,33 +54,33 @@ CGFloat _keyboardHeight;
|
||||
}
|
||||
|
||||
- (void)applicationWillEnterForeground:(UIApplication *)application {
|
||||
GioView *view = (GioView *)self.view.subviews[0];
|
||||
if (view != nil) {
|
||||
onStart(view.handle);
|
||||
UIView *drawView = self.view.subviews[0];
|
||||
if (drawView != nil) {
|
||||
gio_onDraw((__bridge CFTypeRef)drawView);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||
GioView *view = (GioView *)self.view.subviews[0];
|
||||
if (view != nil) {
|
||||
onStop(view.handle);
|
||||
UIView *drawView = self.view.subviews[0];
|
||||
if (drawView != nil) {
|
||||
onStop((__bridge CFTypeRef)drawView);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated {
|
||||
[super viewDidDisappear:animated];
|
||||
GioView *view = (GioView *)self.view.subviews[0];
|
||||
onDestroy(view.handle);
|
||||
CFTypeRef viewRef = (__bridge CFTypeRef)self.view.subviews[0];
|
||||
onDestroy(viewRef);
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
GioView *view = (GioView *)self.view.subviews[0];
|
||||
UIView *view = self.view.subviews[0];
|
||||
CGRect frame = self.view.bounds;
|
||||
// Adjust view bounds to make room for the keyboard.
|
||||
frame.size.height -= _keyboardHeight;
|
||||
view.frame = frame;
|
||||
gio_onDraw(view.handle);
|
||||
gio_onDraw((__bridge CFTypeRef)view);
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
@@ -91,7 +88,6 @@ CGFloat _keyboardHeight;
|
||||
[super didReceiveMemoryWarning];
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (void)keyboardWillChange:(NSNotification *)note {
|
||||
NSDictionary *userInfo = note.userInfo;
|
||||
CGRect f = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
@@ -103,13 +99,13 @@ CGFloat _keyboardHeight;
|
||||
_keyboardHeight = 0.0;
|
||||
[self.view setNeedsLayout];
|
||||
}
|
||||
#endif
|
||||
@end
|
||||
|
||||
static void handleTouches(int last, GioView *view, NSSet<UITouch *> *touches, UIEvent *event) {
|
||||
static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIEvent *event) {
|
||||
CGFloat scale = view.contentScaleFactor;
|
||||
NSUInteger i = 0;
|
||||
NSUInteger n = [touches count];
|
||||
CFTypeRef viewRef = (__bridge CFTypeRef)view;
|
||||
for (UITouch *touch in touches) {
|
||||
CFTypeRef touchRef = (__bridge CFTypeRef)touch;
|
||||
i++;
|
||||
@@ -120,7 +116,7 @@ static void handleTouches(int last, GioView *view, NSSet<UITouch *> *touches, UI
|
||||
CGPoint loc = [coalescedTouch locationInView:view];
|
||||
j++;
|
||||
int lastTouch = last && i == n && j == m;
|
||||
onTouch(view.handle, lastTouch, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]);
|
||||
onTouch(lastTouch, viewRef, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,13 +151,13 @@ NSArray<UIKeyCommand *> *_keyCommands;
|
||||
|
||||
- (void)onWindowDidBecomeKey:(NSNotification *)note {
|
||||
if (self.isFirstResponder) {
|
||||
onFocus(self.handle, YES);
|
||||
onFocus((__bridge CFTypeRef)self, YES);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onWindowDidResignKey:(NSNotification *)note {
|
||||
if (self.isFirstResponder) {
|
||||
onFocus(self.handle, NO);
|
||||
onFocus((__bridge CFTypeRef)self, NO);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +178,7 @@ NSArray<UIKeyCommand *> *_keyCommands;
|
||||
}
|
||||
|
||||
- (void)insertText:(NSString *)text {
|
||||
onText(self.handle, (__bridge CFTypeRef)text);
|
||||
onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)text);
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeFirstResponder {
|
||||
@@ -194,23 +190,23 @@ NSArray<UIKeyCommand *> *_keyCommands;
|
||||
}
|
||||
|
||||
- (void)deleteBackward {
|
||||
onDeleteBackward(self.handle);
|
||||
onDeleteBackward((__bridge CFTypeRef)self);
|
||||
}
|
||||
|
||||
- (void)onUpArrow {
|
||||
onUpArrow(self.handle);
|
||||
onUpArrow((__bridge CFTypeRef)self);
|
||||
}
|
||||
|
||||
- (void)onDownArrow {
|
||||
onDownArrow(self.handle);
|
||||
onDownArrow((__bridge CFTypeRef)self);
|
||||
}
|
||||
|
||||
- (void)onLeftArrow {
|
||||
onLeftArrow(self.handle);
|
||||
onLeftArrow((__bridge CFTypeRef)self);
|
||||
}
|
||||
|
||||
- (void)onRightArrow {
|
||||
onRightArrow(self.handle);
|
||||
onRightArrow((__bridge CFTypeRef)self);
|
||||
}
|
||||
|
||||
- (NSArray<UIKeyCommand *> *)keyCommands {
|
||||
@@ -275,28 +271,3 @@ void gio_showCursor() {
|
||||
void gio_setCursor(NSUInteger curID) {
|
||||
// Not supported.
|
||||
}
|
||||
|
||||
void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
|
||||
GioView *v = (__bridge GioView *)viewRef;
|
||||
v.handle = handle;
|
||||
}
|
||||
|
||||
@interface _gioAppDelegate : UIResponder <UIApplicationDelegate>
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
@end
|
||||
|
||||
@implementation _gioAppDelegate
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil];
|
||||
self.window.rootViewController = controller;
|
||||
[self.window makeKeyAndVisible];
|
||||
return YES;
|
||||
}
|
||||
@end
|
||||
|
||||
int gio_applicationMain(int argc, char *argv[]) {
|
||||
@autoreleasepool {
|
||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([_gioAppDelegate class]));
|
||||
}
|
||||
}
|
||||
|
||||
+96
-99
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"strings"
|
||||
"syscall/js"
|
||||
"time"
|
||||
@@ -14,18 +13,16 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"gioui.org/internal/f32color"
|
||||
"gioui.org/op"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
|
||||
type JSViewEvent struct {
|
||||
type ViewEvent struct {
|
||||
Element js.Value
|
||||
}
|
||||
|
||||
@@ -56,6 +53,9 @@ type window struct {
|
||||
composing bool
|
||||
requestFocus bool
|
||||
|
||||
chanAnimation chan struct{}
|
||||
chanRedraw chan struct{}
|
||||
|
||||
config Config
|
||||
inset f32.Point
|
||||
scale float32
|
||||
@@ -68,7 +68,7 @@ type window struct {
|
||||
contextStatus contextStatus
|
||||
}
|
||||
|
||||
func newWindow(win *callbacks, options []Option) {
|
||||
func newWindow(win *callbacks, options []Option) error {
|
||||
doc := js.Global().Get("document")
|
||||
cont := getContainer(doc)
|
||||
cnv := createCanvas(doc)
|
||||
@@ -83,9 +83,7 @@ func newWindow(win *callbacks, options []Option) {
|
||||
head: doc.Get("head"),
|
||||
clipboard: js.Global().Get("navigator").Get("clipboard"),
|
||||
wakeups: make(chan struct{}, 1),
|
||||
w: win,
|
||||
}
|
||||
w.w.SetDriver(w)
|
||||
w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
|
||||
w.browserHistory = w.window.Get("history")
|
||||
w.visualViewport = w.window.Get("visualViewport")
|
||||
@@ -95,28 +93,42 @@ func newWindow(win *callbacks, options []Option) {
|
||||
if screen := w.window.Get("screen"); screen.Truthy() {
|
||||
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.draw(false)
|
||||
w.chanAnimation <- struct{}{}
|
||||
return nil
|
||||
})
|
||||
w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
|
||||
content := args[0].String()
|
||||
w.processEvent(transfer.DataEvent{
|
||||
Type: "application/text",
|
||||
Open: func() io.ReadCloser {
|
||||
return io.NopCloser(strings.NewReader(content))
|
||||
},
|
||||
})
|
||||
go win.Event(clipboard.Event{Text: content})
|
||||
return nil
|
||||
})
|
||||
w.addEventListeners()
|
||||
w.addHistory()
|
||||
w.w = win
|
||||
|
||||
w.Configure(options)
|
||||
w.blur()
|
||||
w.processEvent(JSViewEvent{Element: cont})
|
||||
w.resize()
|
||||
w.draw(true)
|
||||
go func() {
|
||||
defer w.cleanup()
|
||||
w.w.SetDriver(w)
|
||||
w.Configure(options)
|
||||
w.blur()
|
||||
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 {
|
||||
@@ -176,12 +188,12 @@ func (w *window) addEventListeners() {
|
||||
w.cnv.Set("width", 0)
|
||||
w.cnv.Set("height", 0)
|
||||
w.resize()
|
||||
w.draw(true)
|
||||
w.requestRedraw()
|
||||
return nil
|
||||
})
|
||||
w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} {
|
||||
w.resize()
|
||||
w.draw(true)
|
||||
w.requestRedraw()
|
||||
return nil
|
||||
})
|
||||
w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} {
|
||||
@@ -189,11 +201,22 @@ func (w *window) addEventListeners() {
|
||||
return nil
|
||||
})
|
||||
w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} {
|
||||
if w.processEvent(key.Event{Name: key.NameBack}) {
|
||||
if w.w.Event(key.Event{Name: key.NameBack}) {
|
||||
return w.browserHistory.Call("forward")
|
||||
}
|
||||
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.pointerEvent(pointer.Move, 0, 0, args[0])
|
||||
return nil
|
||||
@@ -251,20 +274,18 @@ func (w *window) addEventListeners() {
|
||||
w.touches[i] = js.Null()
|
||||
}
|
||||
w.touches = w.touches[:0]
|
||||
w.processEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: pointer.Cancel,
|
||||
Source: pointer.Touch,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} {
|
||||
w.config.Focused = true
|
||||
w.processEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(key.FocusEvent{Focus: true})
|
||||
return nil
|
||||
})
|
||||
w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
|
||||
w.config.Focused = false
|
||||
w.processEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(key.FocusEvent{Focus: false})
|
||||
w.blur()
|
||||
return nil
|
||||
})
|
||||
@@ -353,50 +374,10 @@ func (w *window) keyEvent(e js.Value, ks key.State) {
|
||||
Modifiers: modifiersFor(e),
|
||||
State: ks,
|
||||
}
|
||||
w.processEvent(cmd)
|
||||
w.w.Event(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
|
||||
// KeyEvent.
|
||||
func modifiersFor(e js.Value) key.Modifiers {
|
||||
@@ -444,7 +425,7 @@ func (w *window) touchEvent(kind pointer.Kind, e js.Value) {
|
||||
X: float32(x) * scale,
|
||||
Y: float32(y) * scale,
|
||||
}
|
||||
w.processEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: kind,
|
||||
Source: pointer.Touch,
|
||||
Position: pos,
|
||||
@@ -494,7 +475,7 @@ func (w *window) pointerEvent(kind pointer.Kind, dx, dy float32, e js.Value) {
|
||||
if jbtns&4 != 0 {
|
||||
btns |= pointer.ButtonTertiary
|
||||
}
|
||||
w.processEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: kind,
|
||||
Source: pointer.Mouse,
|
||||
Buttons: btns,
|
||||
@@ -521,6 +502,17 @@ func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.F
|
||||
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) SetAnimating(anim bool) {
|
||||
@@ -541,14 +533,14 @@ func (w *window) ReadClipboard() {
|
||||
w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback)
|
||||
}
|
||||
|
||||
func (w *window) WriteClipboard(mime string, s []byte) {
|
||||
func (w *window) WriteClipboard(s string) {
|
||||
if w.clipboard.IsUndefined() {
|
||||
return
|
||||
}
|
||||
if w.clipboard.Get("writeText").IsUndefined() {
|
||||
return
|
||||
}
|
||||
w.clipboard.Call("writeText", string(s))
|
||||
w.clipboard.Call("writeText", s)
|
||||
}
|
||||
|
||||
func (w *window) Configure(options []Option) {
|
||||
@@ -576,7 +568,7 @@ func (w *window) Configure(options []Option) {
|
||||
if cnf.Decorated != prev.Decorated {
|
||||
w.config.Decorated = cnf.Decorated
|
||||
}
|
||||
w.processEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
|
||||
func (w *window) Perform(system.Action) {}
|
||||
@@ -615,14 +607,23 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
|
||||
style.Set("cursor", webCursor[cursor])
|
||||
}
|
||||
|
||||
func (w *window) Wakeup() {
|
||||
select {
|
||||
case w.wakeups <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) ShowTextInput(show bool) {
|
||||
// Run in a goroutine to avoid a deadlock if the
|
||||
// focus change result in an event.
|
||||
if show {
|
||||
w.focus()
|
||||
} else {
|
||||
w.blur()
|
||||
}
|
||||
go func() {
|
||||
if show {
|
||||
w.focus()
|
||||
} else {
|
||||
w.blur()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (w *window) SetInputHint(mode key.InputHint) {
|
||||
@@ -639,7 +640,7 @@ func (w *window) resize() {
|
||||
}
|
||||
if size != w.config.Size {
|
||||
w.config.Size = size
|
||||
w.processEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
|
||||
if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() {
|
||||
@@ -659,20 +660,13 @@ func (w *window) draw(sync bool) {
|
||||
if w.contextStatus == contextStatusLost {
|
||||
return
|
||||
}
|
||||
anim := w.animating
|
||||
w.animRequested = anim
|
||||
if anim {
|
||||
w.requestAnimationFrame.Invoke(w.redraw)
|
||||
} else if !sync {
|
||||
return
|
||||
}
|
||||
size, insets, metric := w.getConfig()
|
||||
if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
w.processEvent(frameEvent{
|
||||
FrameEvent: FrameEvent{
|
||||
w.w.Event(frameEvent{
|
||||
FrameEvent: system.FrameEvent{
|
||||
Now: time.Now(),
|
||||
Size: size,
|
||||
Insets: insets,
|
||||
@@ -682,10 +676,10 @@ func (w *window) draw(sync bool) {
|
||||
})
|
||||
}
|
||||
|
||||
func (w *window) getConfig() (image.Point, Insets, unit.Metric) {
|
||||
func (w *window) getConfig() (image.Point, system.Insets, unit.Metric) {
|
||||
invscale := unit.Dp(1. / w.scale)
|
||||
return image.Pt(w.config.Size.X, w.config.Size.Y),
|
||||
Insets{
|
||||
system.Insets{
|
||||
Bottom: unit.Dp(w.inset.Y) * invscale,
|
||||
Right: unit.Dp(w.inset.X) * invscale,
|
||||
}, unit.Metric{
|
||||
@@ -741,12 +735,19 @@ func (w *window) navigationColor(c color.NRGBA) {
|
||||
theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B}))
|
||||
}
|
||||
|
||||
func (w *window) requestRedraw() {
|
||||
select {
|
||||
case w.chanRedraw <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func osMain() {
|
||||
select {}
|
||||
}
|
||||
|
||||
func translateKey(k string) (key.Name, bool) {
|
||||
var n key.Name
|
||||
func translateKey(k string) (string, bool) {
|
||||
var n string
|
||||
|
||||
switch k {
|
||||
case "ArrowUp":
|
||||
@@ -813,15 +814,11 @@ func translateKey(k string) (key.Name, bool) {
|
||||
r, s := utf8.DecodeRuneInString(k)
|
||||
// If there is exactly one printable character, return that.
|
||||
if s == len(k) && unicode.IsPrint(r) {
|
||||
return key.Name(strings.ToUpper(k)), true
|
||||
return strings.ToUpper(k), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
|
||||
func (JSViewEvent) implementsViewEvent() {}
|
||||
func (JSViewEvent) ImplementsEvent() {}
|
||||
func (j JSViewEvent) Valid() bool {
|
||||
return !(j.Element.IsNull() || j.Element.IsUndefined())
|
||||
}
|
||||
func (_ ViewEvent) ImplementsEvent() {}
|
||||
|
||||
+213
-292
@@ -8,21 +8,16 @@ package app
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"io"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"gioui.org/internal/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
|
||||
_ "gioui.org/internal/cocoainit"
|
||||
@@ -40,9 +35,8 @@ import (
|
||||
#define MOUSE_SCROLL 4
|
||||
|
||||
__attribute__ ((visibility ("hidden"))) void gio_main(void);
|
||||
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(int presentWithTrans);
|
||||
__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"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
|
||||
|
||||
static void writeClipboard(CFTypeRef str) {
|
||||
@autoreleasepool {
|
||||
@@ -62,212 +56,148 @@ static CFTypeRef readClipboard(void) {
|
||||
}
|
||||
|
||||
static CGFloat viewHeight(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
return [view bounds].size.height;
|
||||
}
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
return [view bounds].size.height;
|
||||
}
|
||||
|
||||
static CGFloat viewWidth(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
return [view bounds].size.width;
|
||||
}
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
return [view bounds].size.width;
|
||||
}
|
||||
|
||||
static CGFloat getScreenBackingScale(void) {
|
||||
@autoreleasepool {
|
||||
return [NSScreen.mainScreen backingScaleFactor];
|
||||
}
|
||||
return [NSScreen.mainScreen backingScaleFactor];
|
||||
}
|
||||
|
||||
static CGFloat getViewBackingScale(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
return [view.window backingScaleFactor];
|
||||
}
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
return [view.window backingScaleFactor];
|
||||
}
|
||||
|
||||
static void setNeedsDisplay(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
[view setNeedsDisplay:YES];
|
||||
}
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
[view setNeedsDisplay:YES];
|
||||
}
|
||||
|
||||
static NSPoint cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) {
|
||||
@autoreleasepool {
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
return [window cascadeTopLeftFromPoint:topLeft];
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
return [window cascadeTopLeftFromPoint:topLeft];
|
||||
}
|
||||
|
||||
static void makeKeyAndOrderFront(CFTypeRef windowRef) {
|
||||
@autoreleasepool {
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
[window makeKeyAndOrderFront:nil];
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
[window makeKeyAndOrderFront:nil];
|
||||
}
|
||||
|
||||
static void toggleFullScreen(CFTypeRef windowRef) {
|
||||
@autoreleasepool {
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
[window toggleFullScreen:nil];
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
[window toggleFullScreen:nil];
|
||||
}
|
||||
|
||||
static NSWindowStyleMask getWindowStyleMask(CFTypeRef windowRef) {
|
||||
@autoreleasepool {
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
return [window styleMask];
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
return [window styleMask];
|
||||
}
|
||||
|
||||
static void setWindowStyleMask(CFTypeRef windowRef, NSWindowStyleMask mask) {
|
||||
@autoreleasepool {
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
window.styleMask = mask;
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
window.styleMask = mask;
|
||||
}
|
||||
|
||||
static void setWindowTitleVisibility(CFTypeRef windowRef, NSWindowTitleVisibility state) {
|
||||
@autoreleasepool {
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
window.titleVisibility = state;
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
window.titleVisibility = state;
|
||||
}
|
||||
|
||||
static void setWindowTitlebarAppearsTransparent(CFTypeRef windowRef, int transparent) {
|
||||
@autoreleasepool {
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
window.titlebarAppearsTransparent = (BOOL)transparent;
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
window.titlebarAppearsTransparent = (BOOL)transparent;
|
||||
}
|
||||
|
||||
static void setWindowStandardButtonHidden(CFTypeRef windowRef, NSWindowButton btn, int hide) {
|
||||
@autoreleasepool {
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
[window standardWindowButton:btn].hidden = (BOOL)hide;
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
[window standardWindowButton:btn].hidden = (BOOL)hide;
|
||||
}
|
||||
|
||||
static void performWindowDragWithEvent(CFTypeRef windowRef, CFTypeRef evt) {
|
||||
@autoreleasepool {
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
[window performWindowDragWithEvent:(__bridge NSEvent*)evt];
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
[window performWindowDragWithEvent:(__bridge NSEvent*)evt];
|
||||
}
|
||||
|
||||
static void closeWindow(CFTypeRef windowRef) {
|
||||
@autoreleasepool {
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
[window performClose:nil];
|
||||
}
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
[window performClose:nil];
|
||||
}
|
||||
|
||||
static void setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
|
||||
@autoreleasepool {
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
NSSize size = NSMakeSize(width, height);
|
||||
[window setContentSize:size];
|
||||
}
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
NSSize size = NSMakeSize(width, height);
|
||||
[window setContentSize:size];
|
||||
}
|
||||
|
||||
static void setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
|
||||
@autoreleasepool {
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
window.contentMinSize = NSMakeSize(width, height);
|
||||
}
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
window.contentMinSize = NSMakeSize(width, height);
|
||||
}
|
||||
|
||||
static void setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
|
||||
@autoreleasepool {
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
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) {
|
||||
@autoreleasepool {
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
NSRect r = NSMakeRect(x, y, w, h);
|
||||
[window setFrame:r display:YES];
|
||||
}
|
||||
}
|
||||
|
||||
static void resetLayerFrame(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
NSView* view = (__bridge NSView *)viewRef;
|
||||
NSRect r = view.frame;
|
||||
view.layer.frame = r;
|
||||
}
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
NSRect r = NSMakeRect(x, y, w, h);
|
||||
[window setFrame:r display:YES];
|
||||
}
|
||||
|
||||
static void hideWindow(CFTypeRef windowRef) {
|
||||
@autoreleasepool {
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
[window miniaturize:window];
|
||||
}
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
[window miniaturize:window];
|
||||
}
|
||||
|
||||
static void unhideWindow(CFTypeRef windowRef) {
|
||||
@autoreleasepool {
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
[window deminiaturize:window];
|
||||
}
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
[window deminiaturize:window];
|
||||
}
|
||||
|
||||
static NSRect getScreenFrame(CFTypeRef windowRef) {
|
||||
@autoreleasepool {
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
return [[window screen] frame];
|
||||
}
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
return [[window screen] frame];
|
||||
}
|
||||
|
||||
static void setTitle(CFTypeRef windowRef, CFTypeRef titleRef) {
|
||||
@autoreleasepool {
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
window.title = (__bridge NSString *)titleRef;
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
window.title = (__bridge NSString *)titleRef;
|
||||
}
|
||||
|
||||
static int isWindowZoomed(CFTypeRef windowRef) {
|
||||
@autoreleasepool {
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
return window.zoomed ? 1 : 0;
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
return window.zoomed ? 1 : 0;
|
||||
}
|
||||
|
||||
static void zoomWindow(CFTypeRef windowRef) {
|
||||
@autoreleasepool {
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
[window zoom:nil];
|
||||
}
|
||||
NSWindow *window = (__bridge NSWindow *)windowRef;
|
||||
[window zoom:nil];
|
||||
}
|
||||
|
||||
static CFTypeRef layerForView(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
return (__bridge CFTypeRef)view.layer;
|
||||
}
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
return (__bridge CFTypeRef)view.layer;
|
||||
}
|
||||
|
||||
static CFTypeRef windowForView(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
return (__bridge CFTypeRef)view.window;
|
||||
}
|
||||
NSView *view = (__bridge NSView *)viewRef;
|
||||
return (__bridge CFTypeRef)view.window;
|
||||
}
|
||||
|
||||
static void raiseWindow(CFTypeRef windowRef) {
|
||||
@autoreleasepool {
|
||||
NSRunningApplication *currentApp = [NSRunningApplication currentApplication];
|
||||
if (![currentApp isActive]) {
|
||||
[currentApp activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
|
||||
}
|
||||
NSWindow* window = (__bridge NSWindow *)windowRef;
|
||||
[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) {
|
||||
@@ -279,23 +209,23 @@ static CFTypeRef createInputContext(CFTypeRef clientRef) {
|
||||
}
|
||||
|
||||
static void discardMarkedText(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
@autoreleasepool {
|
||||
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
|
||||
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
|
||||
if (view == [ctx client]) {
|
||||
[ctx discardMarkedText];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void invalidateCharacterCoordinates(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
@autoreleasepool {
|
||||
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
|
||||
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
|
||||
if (view == [ctx client]) {
|
||||
[ctx invalidateCharacterCoordinates];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
@@ -305,9 +235,9 @@ func init() {
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
// AppKitViewEvent notifies the client of changes to the window AppKit handles.
|
||||
// The handles are retained until another AppKitViewEvent is sent.
|
||||
type AppKitViewEvent struct {
|
||||
// ViewEvent notified the client of changes to the window AppKit handles.
|
||||
// The handles are retained until another ViewEvent is sent.
|
||||
type ViewEvent struct {
|
||||
// View is a CFTypeRef for the NSView for the window.
|
||||
View uintptr
|
||||
// Layer is a CFTypeRef of the CALayer of View.
|
||||
@@ -317,20 +247,21 @@ type AppKitViewEvent struct {
|
||||
type window struct {
|
||||
view C.CFTypeRef
|
||||
w *callbacks
|
||||
anim bool
|
||||
visible bool
|
||||
stage system.Stage
|
||||
displayLink *displayLink
|
||||
// redraw is a single entry channel for making sure only one
|
||||
// display link redraw request is in flight.
|
||||
redraw chan struct{}
|
||||
cursor pointer.Cursor
|
||||
pointerBtns pointer.Buttons
|
||||
loop *eventLoop
|
||||
|
||||
scale float32
|
||||
config Config
|
||||
}
|
||||
|
||||
// viewMap is the mapping from Cocoa NSViews to Go windows.
|
||||
var viewMap = make(map[C.CFTypeRef]*window)
|
||||
|
||||
// launched is closed when applicationDidFinishLaunching is called.
|
||||
var launched = make(chan struct{})
|
||||
|
||||
@@ -338,8 +269,30 @@ var launched = make(chan struct{})
|
||||
// cascadeTopLeftFromPoint.
|
||||
var nextTopLeft C.NSPoint
|
||||
|
||||
func windowFor(h C.uintptr_t) *window {
|
||||
return cgo.Handle(h).Value().(*window)
|
||||
// mustView is like lookupView, except that it panics
|
||||
// if the view isn't mapped.
|
||||
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) {
|
||||
w, exists := viewMap[view]
|
||||
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 {
|
||||
@@ -352,16 +305,11 @@ func (w *window) ReadClipboard() {
|
||||
defer C.CFRelease(cstr)
|
||||
}
|
||||
content := nsstringToString(cstr)
|
||||
w.ProcessEvent(transfer.DataEvent{
|
||||
Type: "application/text",
|
||||
Open: func() io.ReadCloser {
|
||||
return io.NopCloser(strings.NewReader(content))
|
||||
},
|
||||
})
|
||||
w.w.Event(clipboard.Event{Text: content})
|
||||
}
|
||||
|
||||
func (w *window) WriteClipboard(mime string, s []byte) {
|
||||
cstr := stringToNSString(string(s))
|
||||
func (w *window) WriteClipboard(s string) {
|
||||
cstr := stringToNSString(s)
|
||||
defer C.CFRelease(cstr)
|
||||
C.writeClipboard(cstr)
|
||||
}
|
||||
@@ -464,11 +412,8 @@ func (w *window) Configure(options []Option) {
|
||||
C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans)
|
||||
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
|
||||
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
|
||||
// When toggling the titlebar, the layer doesn't update its frame
|
||||
// until the next resize. Force it.
|
||||
C.resetLayerFrame(w.view)
|
||||
}
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
|
||||
func (w *window) setTitle(prev, cnf Config) {
|
||||
@@ -519,8 +464,7 @@ func (w *window) ShowTextInput(show bool) {}
|
||||
func (w *window) SetInputHint(_ key.InputHint) {}
|
||||
|
||||
func (w *window) SetAnimating(anim bool) {
|
||||
w.anim = anim
|
||||
if w.anim && w.visible {
|
||||
if anim {
|
||||
w.displayLink.Start()
|
||||
} else {
|
||||
w.displayLink.Stop()
|
||||
@@ -537,18 +481,26 @@ 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
|
||||
func gio_onKeys(h C.uintptr_t, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) {
|
||||
func gio_onKeys(view, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) {
|
||||
str := nsstringToString(cstr)
|
||||
kmods := convertMods(mods)
|
||||
ks := key.Release
|
||||
if keyDown {
|
||||
ks = key.Press
|
||||
}
|
||||
w := windowFor(h)
|
||||
w := mustView(view)
|
||||
for _, k := range str {
|
||||
if n, ok := convertKey(k); ok {
|
||||
w.ProcessEvent(key.Event{
|
||||
w.w.Event(key.Event{
|
||||
Name: n,
|
||||
Modifiers: kmods,
|
||||
State: ks,
|
||||
@@ -558,15 +510,15 @@ func gio_onKeys(h C.uintptr_t, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger,
|
||||
}
|
||||
|
||||
//export gio_onText
|
||||
func gio_onText(h C.uintptr_t, cstr C.CFTypeRef) {
|
||||
func gio_onText(view, cstr C.CFTypeRef) {
|
||||
str := nsstringToString(cstr)
|
||||
w := windowFor(h)
|
||||
w := mustView(view)
|
||||
w.w.EditorInsert(str)
|
||||
}
|
||||
|
||||
//export gio_onMouse
|
||||
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 := windowFor(h)
|
||||
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) {
|
||||
w := mustView(view)
|
||||
t := time.Duration(float64(ti)*float64(time.Second) + .5)
|
||||
xf, yf := float32(x)*w.scale, float32(y)*w.scale
|
||||
dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale
|
||||
@@ -603,7 +555,7 @@ func gio_onMouse(h C.uintptr_t, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x
|
||||
default:
|
||||
panic("invalid direction")
|
||||
}
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: typ,
|
||||
Source: pointer.Mouse,
|
||||
Time: t,
|
||||
@@ -615,29 +567,35 @@ func gio_onMouse(h C.uintptr_t, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x
|
||||
}
|
||||
|
||||
//export gio_onDraw
|
||||
func gio_onDraw(h C.uintptr_t) {
|
||||
w := windowFor(h)
|
||||
func gio_onDraw(view C.CFTypeRef) {
|
||||
w := mustView(view)
|
||||
w.draw()
|
||||
}
|
||||
|
||||
//export gio_onFocus
|
||||
func gio_onFocus(h C.uintptr_t, focus C.int) {
|
||||
w := windowFor(h)
|
||||
func gio_onFocus(view C.CFTypeRef, focus C.int) {
|
||||
w := mustView(view)
|
||||
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.config.Focused = focus == 1
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
}
|
||||
|
||||
//export gio_onChangeScreen
|
||||
func gio_onChangeScreen(h C.uintptr_t, did uint64) {
|
||||
w := windowFor(h)
|
||||
func gio_onChangeScreen(view C.CFTypeRef, did uint64) {
|
||||
w := mustView(view)
|
||||
w.displayLink.SetDisplayID(did)
|
||||
C.setNeedsDisplay(w.view)
|
||||
}
|
||||
|
||||
//export gio_hasMarkedText
|
||||
func gio_hasMarkedText(h C.uintptr_t) C.int {
|
||||
w := windowFor(h)
|
||||
func gio_hasMarkedText(view C.CFTypeRef) C.int {
|
||||
w := mustView(view)
|
||||
state := w.w.EditorState()
|
||||
if state.compose.Start != -1 {
|
||||
return 1
|
||||
@@ -646,8 +604,8 @@ func gio_hasMarkedText(h C.uintptr_t) C.int {
|
||||
}
|
||||
|
||||
//export gio_markedRange
|
||||
func gio_markedRange(h C.uintptr_t) C.NSRange {
|
||||
w := windowFor(h)
|
||||
func gio_markedRange(view C.CFTypeRef) C.NSRange {
|
||||
w := mustView(view)
|
||||
state := w.w.EditorState()
|
||||
rng := state.compose
|
||||
start, end := rng.Start, rng.End
|
||||
@@ -662,8 +620,8 @@ func gio_markedRange(h C.uintptr_t) C.NSRange {
|
||||
}
|
||||
|
||||
//export gio_selectedRange
|
||||
func gio_selectedRange(h C.uintptr_t) C.NSRange {
|
||||
w := windowFor(h)
|
||||
func gio_selectedRange(view C.CFTypeRef) C.NSRange {
|
||||
w := mustView(view)
|
||||
state := w.w.EditorState()
|
||||
rng := state.Selection
|
||||
start, end := rng.Start, rng.End
|
||||
@@ -678,14 +636,14 @@ func gio_selectedRange(h C.uintptr_t) C.NSRange {
|
||||
}
|
||||
|
||||
//export gio_unmarkText
|
||||
func gio_unmarkText(h C.uintptr_t) {
|
||||
w := windowFor(h)
|
||||
func gio_unmarkText(view C.CFTypeRef) {
|
||||
w := mustView(view)
|
||||
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
|
||||
}
|
||||
|
||||
//export gio_setMarkedText
|
||||
func gio_setMarkedText(h C.uintptr_t, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) {
|
||||
w := windowFor(h)
|
||||
func gio_setMarkedText(view, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) {
|
||||
w := mustView(view)
|
||||
str := nsstringToString(cstr)
|
||||
state := w.w.EditorState()
|
||||
rng := state.compose
|
||||
@@ -724,8 +682,8 @@ func gio_setMarkedText(h C.uintptr_t, cstr C.CFTypeRef, selRange C.NSRange, repl
|
||||
}
|
||||
|
||||
//export gio_substringForProposedRange
|
||||
func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef {
|
||||
w := windowFor(h)
|
||||
func gio_substringForProposedRange(view C.CFTypeRef, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef {
|
||||
w := mustView(view)
|
||||
state := w.w.EditorState()
|
||||
start, end := state.Snippet.Start, state.Snippet.End
|
||||
if start > end {
|
||||
@@ -745,8 +703,8 @@ func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRan
|
||||
}
|
||||
|
||||
//export gio_insertText
|
||||
func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) {
|
||||
w := windowFor(h)
|
||||
func gio_insertText(view, cstr C.CFTypeRef, crng C.NSRange) {
|
||||
w := mustView(view)
|
||||
state := w.w.EditorState()
|
||||
rng := state.compose
|
||||
if rng.Start == -1 {
|
||||
@@ -770,13 +728,13 @@ func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) {
|
||||
}
|
||||
|
||||
//export gio_characterIndexForPoint
|
||||
func gio_characterIndexForPoint(h C.uintptr_t, p C.NSPoint) C.NSUInteger {
|
||||
func gio_characterIndexForPoint(view C.CFTypeRef, p C.NSPoint) C.NSUInteger {
|
||||
return C.NSNotFound
|
||||
}
|
||||
|
||||
//export gio_firstRectForCharacterRange
|
||||
func gio_firstRectForCharacterRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.NSRect {
|
||||
w := windowFor(h)
|
||||
func gio_firstRectForCharacterRange(view C.CFTypeRef, crng C.NSRange, actual C.NSRangePointer) C.NSRect {
|
||||
w := mustView(view)
|
||||
state := w.w.EditorState()
|
||||
sel := state.Selection
|
||||
u16start := state.UTF16Index(sel.Start)
|
||||
@@ -803,10 +761,6 @@ func (w *window) draw() {
|
||||
case <-w.redraw:
|
||||
default:
|
||||
}
|
||||
w.visible = true
|
||||
if w.anim {
|
||||
w.SetAnimating(w.anim)
|
||||
}
|
||||
w.scale = float32(C.getViewBackingScale(w.view))
|
||||
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
|
||||
sz := image.Point{
|
||||
@@ -815,14 +769,15 @@ func (w *window) draw() {
|
||||
}
|
||||
if sz != w.config.Size {
|
||||
w.config.Size = sz
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
if sz.X == 0 || sz.Y == 0 {
|
||||
return
|
||||
}
|
||||
cfg := configFor(w.scale)
|
||||
w.ProcessEvent(frameEvent{
|
||||
FrameEvent: FrameEvent{
|
||||
w.setStage(system.StageRunning)
|
||||
w.w.Event(frameEvent{
|
||||
FrameEvent: system.FrameEvent{
|
||||
Now: time.Now(),
|
||||
Size: w.config.Size,
|
||||
Metric: cfg,
|
||||
@@ -831,27 +786,6 @@ func (w *window) draw() {
|
||||
})
|
||||
}
|
||||
|
||||
func (w *window) ProcessEvent(e event.Event) {
|
||||
w.w.ProcessEvent(e)
|
||||
w.loop.FlushEvents()
|
||||
}
|
||||
|
||||
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 configFor(scale float32) unit.Metric {
|
||||
return unit.Metric{
|
||||
PxPerDp: scale,
|
||||
@@ -859,54 +793,56 @@ func configFor(scale float32) unit.Metric {
|
||||
}
|
||||
}
|
||||
|
||||
//export gio_onAttached
|
||||
func gio_onAttached(h C.uintptr_t, attached C.int) {
|
||||
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{})
|
||||
//export gio_onClose
|
||||
func gio_onClose(view C.CFTypeRef) {
|
||||
w := mustView(view)
|
||||
w.w.Event(ViewEvent{})
|
||||
w.w.Event(system.DestroyEvent{})
|
||||
w.displayLink.Close()
|
||||
w.displayLink = nil
|
||||
cgo.Handle(h).Delete()
|
||||
deleteView(view)
|
||||
C.CFRelease(w.view)
|
||||
w.view = 0
|
||||
}
|
||||
|
||||
//export gio_onHide
|
||||
func gio_onHide(h C.uintptr_t) {
|
||||
w := windowFor(h)
|
||||
w.visible = false
|
||||
w.SetAnimating(w.anim)
|
||||
func gio_onHide(view C.CFTypeRef) {
|
||||
w := mustView(view)
|
||||
w.setStage(system.StagePaused)
|
||||
}
|
||||
|
||||
//export gio_onShow
|
||||
func gio_onShow(h C.uintptr_t) {
|
||||
w := windowFor(h)
|
||||
w.draw()
|
||||
func gio_onShow(view C.CFTypeRef) {
|
||||
w := mustView(view)
|
||||
w.setStage(system.StageRunning)
|
||||
}
|
||||
|
||||
//export gio_onFullscreen
|
||||
func gio_onFullscreen(h C.uintptr_t) {
|
||||
w := windowFor(h)
|
||||
func gio_onFullscreen(view C.CFTypeRef) {
|
||||
w := mustView(view)
|
||||
w.config.Mode = Fullscreen
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
|
||||
//export gio_onWindowed
|
||||
func gio_onWindowed(h C.uintptr_t) {
|
||||
w := windowFor(h)
|
||||
func gio_onWindowed(view C.CFTypeRef) {
|
||||
w := mustView(view)
|
||||
w.config.Mode = Windowed
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
|
||||
//export gio_onAppHide
|
||||
func gio_onAppHide() {
|
||||
for _, w := range viewMap {
|
||||
w.setStage(system.StagePaused)
|
||||
}
|
||||
}
|
||||
|
||||
//export gio_onAppShow
|
||||
func gio_onAppShow() {
|
||||
for _, w := range viewMap {
|
||||
w.setStage(system.StageRunning)
|
||||
}
|
||||
}
|
||||
|
||||
//export gio_onFinishLaunching
|
||||
@@ -914,27 +850,20 @@ func gio_onFinishLaunching() {
|
||||
close(launched)
|
||||
}
|
||||
|
||||
func newWindow(win *callbacks, options []Option) {
|
||||
func newWindow(win *callbacks, options []Option) error {
|
||||
<-launched
|
||||
res := make(chan struct{})
|
||||
errch := make(chan error)
|
||||
runOnMain(func() {
|
||||
w := &window{
|
||||
redraw: make(chan struct{}, 1),
|
||||
w: win,
|
||||
}
|
||||
w.loop = newEventLoop(w.w, w.wakeup)
|
||||
win.SetDriver(w)
|
||||
res <- struct{}{}
|
||||
var cnf Config
|
||||
cnf.apply(unit.Metric{}, options)
|
||||
if err := w.init(cnf.CustomRenderer); err != nil {
|
||||
w.ProcessEvent(DestroyEvent{Err: err})
|
||||
w, err := newOSWindow()
|
||||
if err != nil {
|
||||
errch <- err
|
||||
return
|
||||
}
|
||||
errch <- nil
|
||||
w.w = win
|
||||
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()
|
||||
win.SetDriver(w)
|
||||
w.Configure(options)
|
||||
if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
|
||||
// cascadeTopLeftFromPoint treats (0, 0) as a no-op,
|
||||
@@ -944,21 +873,23 @@ func newWindow(win *callbacks, options []Option) {
|
||||
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
|
||||
// makeKeyAndOrderFront assumes ownership of our window reference.
|
||||
C.makeKeyAndOrderFront(window)
|
||||
layer := C.layerForView(w.view)
|
||||
w.w.Event(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
|
||||
})
|
||||
<-res
|
||||
return <-errch
|
||||
}
|
||||
|
||||
func (w *window) init(customRenderer bool) error {
|
||||
presentWithTrans := 1
|
||||
if customRenderer {
|
||||
presentWithTrans = 0
|
||||
}
|
||||
view := C.gio_createView(C.int(presentWithTrans))
|
||||
func newOSWindow() (*window, error) {
|
||||
view := C.gio_createView()
|
||||
if view == 0 {
|
||||
return errors.New("newOSWindow: failed to create view")
|
||||
return nil, errors.New("newOSWindows: failed to create view")
|
||||
}
|
||||
scale := float32(C.getViewBackingScale(view))
|
||||
w.scale = scale
|
||||
w := &window{
|
||||
view: view,
|
||||
scale: scale,
|
||||
redraw: make(chan struct{}, 1),
|
||||
}
|
||||
dl, err := newDisplayLink(func() {
|
||||
select {
|
||||
case w.redraw <- struct{}{}:
|
||||
@@ -966,30 +897,24 @@ func (w *window) init(customRenderer bool) error {
|
||||
return
|
||||
}
|
||||
w.runOnMain(func() {
|
||||
if w.visible {
|
||||
C.setNeedsDisplay(w.view)
|
||||
}
|
||||
C.setNeedsDisplay(w.view)
|
||||
})
|
||||
})
|
||||
w.displayLink = dl
|
||||
if err != nil {
|
||||
C.CFRelease(view)
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
|
||||
w.view = view
|
||||
return nil
|
||||
insertView(view, w)
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func osMain() {
|
||||
if !isMainThread() {
|
||||
panic("app.Main must run on the main goroutine")
|
||||
}
|
||||
C.gio_main()
|
||||
}
|
||||
|
||||
func convertKey(k rune) (key.Name, bool) {
|
||||
var n key.Name
|
||||
func convertKey(k rune) (string, bool) {
|
||||
var n string
|
||||
switch k {
|
||||
case 0x1b:
|
||||
n = key.NameEscape
|
||||
@@ -1050,7 +975,7 @@ func convertKey(k rune) (key.Name, bool) {
|
||||
if !unicode.IsPrint(k) {
|
||||
return "", false
|
||||
}
|
||||
n = key.Name(k)
|
||||
n = string(k)
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
@@ -1072,8 +997,4 @@ func convertMods(mods C.NSUInteger) key.Modifiers {
|
||||
return kmods
|
||||
}
|
||||
|
||||
func (AppKitViewEvent) implementsViewEvent() {}
|
||||
func (AppKitViewEvent) ImplementsEvent() {}
|
||||
func (a AppKitViewEvent) Valid() bool {
|
||||
return a != (AppKitViewEvent{})
|
||||
}
|
||||
func (_ ViewEvent) ImplementsEvent() {}
|
||||
|
||||
+53
-89
@@ -6,7 +6,7 @@
|
||||
|
||||
#include "_cgo_export.h"
|
||||
|
||||
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWithTrans);
|
||||
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
|
||||
|
||||
@interface GioAppDelegate : NSObject<NSApplicationDelegate>
|
||||
@end
|
||||
@@ -14,55 +14,40 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWi
|
||||
@interface GioWindowDelegate : NSObject<NSWindowDelegate>
|
||||
@end
|
||||
|
||||
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
|
||||
@property uintptr_t handle;
|
||||
@property BOOL presentWithTrans;
|
||||
@end
|
||||
|
||||
@implementation GioWindowDelegate
|
||||
- (void)windowWillMiniaturize:(NSNotification *)notification {
|
||||
NSWindow *window = (NSWindow *)[notification object];
|
||||
GioView *view = (GioView *)window.contentView;
|
||||
gio_onHide(view.handle);
|
||||
gio_onHide((__bridge CFTypeRef)window.contentView);
|
||||
}
|
||||
- (void)windowDidDeminiaturize:(NSNotification *)notification {
|
||||
NSWindow *window = (NSWindow *)[notification object];
|
||||
GioView *view = (GioView *)window.contentView;
|
||||
gio_onShow(view.handle);
|
||||
gio_onShow((__bridge CFTypeRef)window.contentView);
|
||||
}
|
||||
- (void)windowWillEnterFullScreen:(NSNotification *)notification {
|
||||
NSWindow *window = (NSWindow *)[notification object];
|
||||
GioView *view = (GioView *)window.contentView;
|
||||
gio_onFullscreen(view.handle);
|
||||
gio_onFullscreen((__bridge CFTypeRef)window.contentView);
|
||||
}
|
||||
- (void)windowWillExitFullScreen:(NSNotification *)notification {
|
||||
NSWindow *window = (NSWindow *)[notification object];
|
||||
GioView *view = (GioView *)window.contentView;
|
||||
gio_onWindowed(view.handle);
|
||||
gio_onWindowed((__bridge CFTypeRef)window.contentView);
|
||||
}
|
||||
- (void)windowDidChangeScreen:(NSNotification *)notification {
|
||||
NSWindow *window = (NSWindow *)[notification object];
|
||||
CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue];
|
||||
GioView *view = (GioView *)window.contentView;
|
||||
gio_onChangeScreen(view.handle, dispID);
|
||||
CFTypeRef view = (__bridge CFTypeRef)window.contentView;
|
||||
gio_onChangeScreen(view, dispID);
|
||||
}
|
||||
- (void)windowDidBecomeKey:(NSNotification *)notification {
|
||||
NSWindow *window = (NSWindow *)[notification object];
|
||||
GioView *view = (GioView *)window.contentView;
|
||||
if ([window firstResponder] == view) {
|
||||
gio_onFocus(view.handle, 1);
|
||||
}
|
||||
gio_onFocus((__bridge CFTypeRef)window.contentView, 1);
|
||||
}
|
||||
- (void)windowDidResignKey:(NSNotification *)notification {
|
||||
NSWindow *window = (NSWindow *)[notification object];
|
||||
GioView *view = (GioView *)window.contentView;
|
||||
if ([window firstResponder] == view) {
|
||||
gio_onFocus(view.handle, 0);
|
||||
}
|
||||
gio_onFocus((__bridge CFTypeRef)window.contentView, 0);
|
||||
}
|
||||
@end
|
||||
|
||||
static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
|
||||
static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
|
||||
NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil];
|
||||
if (!event.hasPreciseScrollingDeltas) {
|
||||
// dx and dy are in rows and columns.
|
||||
@@ -71,9 +56,12 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
|
||||
}
|
||||
// Origin is in the lower left corner. Convert to upper left.
|
||||
CGFloat height = view.bounds.size.height;
|
||||
gio_onMouse(view.handle, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
|
||||
gio_onMouse((__bridge CFTypeRef)view, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
|
||||
}
|
||||
|
||||
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
|
||||
@end
|
||||
|
||||
@implementation GioView
|
||||
- (void)setFrameSize:(NSSize)newSize {
|
||||
[super setFrameSize:newSize];
|
||||
@@ -82,19 +70,21 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
|
||||
// drawRect is called when OpenGL is used, displayLayer otherwise.
|
||||
// Don't know why.
|
||||
- (void)drawRect:(NSRect)r {
|
||||
gio_onDraw(self.handle);
|
||||
gio_onDraw((__bridge CFTypeRef)self);
|
||||
}
|
||||
- (void)displayLayer:(CALayer *)layer {
|
||||
layer.contentsScale = self.window.backingScaleFactor;
|
||||
gio_onDraw(self.handle);
|
||||
gio_onDraw((__bridge CFTypeRef)self);
|
||||
}
|
||||
- (CALayer *)makeBackingLayer {
|
||||
CALayer *layer = gio_layerFactory(self.presentWithTrans);
|
||||
CALayer *layer = gio_layerFactory();
|
||||
layer.delegate = self;
|
||||
return layer;
|
||||
}
|
||||
- (void)viewDidMoveToWindow {
|
||||
gio_onAttached(self.handle, self.window != nil ? 1 : 0);
|
||||
if (self.window == nil) {
|
||||
gio_onClose((__bridge CFTypeRef)self);
|
||||
}
|
||||
}
|
||||
- (void)mouseDown:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
||||
@@ -134,14 +124,14 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
|
||||
- (void)keyDown:(NSEvent *)event {
|
||||
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
|
||||
NSString *keys = [event charactersIgnoringModifiers];
|
||||
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
|
||||
gio_onKeys((__bridge CFTypeRef)self, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
|
||||
}
|
||||
- (void)keyUp:(NSEvent *)event {
|
||||
NSString *keys = [event charactersIgnoringModifiers];
|
||||
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
|
||||
gio_onKeys((__bridge CFTypeRef)self, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
|
||||
}
|
||||
- (void)insertText:(id)string {
|
||||
gio_onText(self.handle, (__bridge CFTypeRef)string);
|
||||
gio_onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)string);
|
||||
}
|
||||
- (void)doCommandBySelector:(SEL)sel {
|
||||
// Don't pass commands up the responder chain.
|
||||
@@ -149,17 +139,17 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
|
||||
}
|
||||
|
||||
- (BOOL)hasMarkedText {
|
||||
int res = gio_hasMarkedText(self.handle);
|
||||
int res = gio_hasMarkedText((__bridge CFTypeRef)self);
|
||||
return res ? YES : NO;
|
||||
}
|
||||
- (NSRange)markedRange {
|
||||
return gio_markedRange(self.handle);
|
||||
return gio_markedRange((__bridge CFTypeRef)self);
|
||||
}
|
||||
- (NSRange)selectedRange {
|
||||
return gio_selectedRange(self.handle);
|
||||
return gio_selectedRange((__bridge CFTypeRef)self);
|
||||
}
|
||||
- (void)unmarkText {
|
||||
gio_unmarkText(self.handle);
|
||||
gio_unmarkText((__bridge CFTypeRef)self);
|
||||
}
|
||||
- (void)setMarkedText:(id)string
|
||||
selectedRange:(NSRange)selRange
|
||||
@@ -171,14 +161,14 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
|
||||
} else {
|
||||
str = string;
|
||||
}
|
||||
gio_setMarkedText(self.handle, (__bridge CFTypeRef)str, selRange, replaceRange);
|
||||
gio_setMarkedText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, selRange, replaceRange);
|
||||
}
|
||||
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
|
||||
return nil;
|
||||
}
|
||||
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range
|
||||
actualRange:(NSRangePointer)actualRange {
|
||||
NSString *str = CFBridgingRelease(gio_substringForProposedRange(self.handle, range, actualRange));
|
||||
NSString *str = CFBridgingRelease(gio_substringForProposedRange((__bridge CFTypeRef)self, range, actualRange));
|
||||
return [[NSAttributedString alloc] initWithString:str attributes:nil];
|
||||
}
|
||||
- (void)insertText:(id)string
|
||||
@@ -190,34 +180,17 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
|
||||
} else {
|
||||
str = string;
|
||||
}
|
||||
gio_insertText(self.handle, (__bridge CFTypeRef)str, replaceRange);
|
||||
gio_insertText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, replaceRange);
|
||||
}
|
||||
- (NSUInteger)characterIndexForPoint:(NSPoint)p {
|
||||
return gio_characterIndexForPoint(self.handle, p);
|
||||
return gio_characterIndexForPoint((__bridge CFTypeRef)self, p);
|
||||
}
|
||||
- (NSRect)firstRectForCharacterRange:(NSRange)rng
|
||||
actualRange:(NSRangePointer)actual {
|
||||
NSRect r = gio_firstRectForCharacterRange(self.handle, rng, actual);
|
||||
NSRect r = gio_firstRectForCharacterRange((__bridge CFTypeRef)self, rng, actual);
|
||||
r = [self convertRect:r toView:nil];
|
||||
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);
|
||||
}
|
||||
- (BOOL) becomeFirstResponder {
|
||||
gio_onFocus(self.handle, 1);
|
||||
return [super becomeFirstResponder];
|
||||
}
|
||||
- (BOOL) resignFirstResponder {
|
||||
gio_onFocus(self.handle, 0);
|
||||
return [super resignFirstResponder];
|
||||
}
|
||||
@end
|
||||
|
||||
// Delegates are weakly referenced from their peers. Nothing
|
||||
@@ -267,7 +240,7 @@ void gio_showCursor() {
|
||||
|
||||
// some cursors are not public, this tries to use a private cursor
|
||||
// and uses fallback when the use of private cursor fails.
|
||||
static void trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
|
||||
void gio_trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
|
||||
if ([NSCursor respondsToSelector:cursorName]) {
|
||||
id object = [NSCursor performSelector:cursorName];
|
||||
if ([object isKindOfClass:[NSCursor class]]) {
|
||||
@@ -299,7 +272,7 @@ void gio_setCursor(NSUInteger curID) {
|
||||
break;
|
||||
case 6: // pointer.CursorAllScroll
|
||||
// For some reason, using _moveCursor fails on Monterey.
|
||||
// trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
|
||||
// gio_trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
|
||||
[NSCursor.arrowCursor set];
|
||||
break;
|
||||
case 7: // pointer.CursorColResize
|
||||
@@ -309,31 +282,33 @@ void gio_setCursor(NSUInteger curID) {
|
||||
[NSCursor.resizeUpDownCursor set];
|
||||
break;
|
||||
case 9: // pointer.CursorGrab
|
||||
[NSCursor.openHandCursor set];
|
||||
// [NSCursor.openHandCursor set];
|
||||
gio_trySetPrivateCursor(@selector(openHandCursor), NSCursor.arrowCursor);
|
||||
break;
|
||||
case 10: // pointer.CursorGrabbing
|
||||
[NSCursor.closedHandCursor set];
|
||||
// [NSCursor.closedHandCursor set];
|
||||
gio_trySetPrivateCursor(@selector(closedHandCursor), NSCursor.arrowCursor);
|
||||
break;
|
||||
case 11: // pointer.CursorNotAllowed
|
||||
[NSCursor.operationNotAllowedCursor set];
|
||||
break;
|
||||
case 12: // pointer.CursorWait
|
||||
trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
|
||||
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
|
||||
break;
|
||||
case 13: // pointer.CursorProgress
|
||||
trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
|
||||
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
|
||||
break;
|
||||
case 14: // pointer.CursorNorthWestResize
|
||||
trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
|
||||
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
|
||||
break;
|
||||
case 15: // pointer.CursorNorthEastResize
|
||||
trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
|
||||
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
|
||||
break;
|
||||
case 16: // pointer.CursorSouthWestResize
|
||||
trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
|
||||
gio_trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
|
||||
break;
|
||||
case 17: // pointer.CursorSouthEastResize
|
||||
trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
|
||||
gio_trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
|
||||
break;
|
||||
case 18: // pointer.CursorNorthSouthResize
|
||||
[NSCursor.resizeUpDownCursor set];
|
||||
@@ -354,10 +329,10 @@ void gio_setCursor(NSUInteger curID) {
|
||||
[NSCursor.resizeDownCursor set];
|
||||
break;
|
||||
case 24: // pointer.CursorNorthEastSouthWestResize
|
||||
trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
|
||||
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
|
||||
break;
|
||||
case 25: // pointer.CursorNorthWestSouthEastResize
|
||||
trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
|
||||
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
|
||||
break;
|
||||
default:
|
||||
[NSCursor.arrowCursor set];
|
||||
@@ -393,39 +368,28 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF
|
||||
}
|
||||
}
|
||||
|
||||
CFTypeRef gio_createView(int presentWithTrans) {
|
||||
CFTypeRef gio_createView(void) {
|
||||
@autoreleasepool {
|
||||
NSRect frame = NSMakeRect(0, 0, 0, 0);
|
||||
GioView* view = [[GioView alloc] initWithFrame:frame];
|
||||
view.presentWithTrans = presentWithTrans ? YES : NO;
|
||||
view.wantsLayer = YES;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
|
||||
@autoreleasepool {
|
||||
GioView *v = (__bridge GioView *)viewRef;
|
||||
v.handle = handle;
|
||||
}
|
||||
}
|
||||
|
||||
@implementation GioAppDelegate
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
gio_onFinishLaunching();
|
||||
}
|
||||
- (void)applicationDidHide:(NSNotification *)aNotification {
|
||||
gio_onAppHide();
|
||||
}
|
||||
- (void)applicationWillUnhide:(NSNotification *)notification {
|
||||
gio_onAppShow();
|
||||
}
|
||||
@end
|
||||
|
||||
void gio_main() {
|
||||
|
||||
+12
-11
@@ -12,6 +12,13 @@ import (
|
||||
"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 {
|
||||
// Display is a pointer to the X11 Display created by XOpenDisplay.
|
||||
Display unsafe.Pointer
|
||||
@@ -21,9 +28,6 @@ type X11ViewEvent struct {
|
||||
|
||||
func (X11ViewEvent) implementsViewEvent() {}
|
||||
func (X11ViewEvent) ImplementsEvent() {}
|
||||
func (x X11ViewEvent) Valid() bool {
|
||||
return x != (X11ViewEvent{})
|
||||
}
|
||||
|
||||
type WaylandViewEvent struct {
|
||||
// Display is the *wl_display returned by wl_display_connect.
|
||||
@@ -34,9 +38,6 @@ type WaylandViewEvent struct {
|
||||
|
||||
func (WaylandViewEvent) implementsViewEvent() {}
|
||||
func (WaylandViewEvent) ImplementsEvent() {}
|
||||
func (w WaylandViewEvent) Valid() bool {
|
||||
return w != (WaylandViewEvent{})
|
||||
}
|
||||
|
||||
func osMain() {
|
||||
select {}
|
||||
@@ -48,7 +49,7 @@ type windowDriver func(*callbacks, []Option) error
|
||||
// let each driver initialize these variables with their own version of createWindow.
|
||||
var wlDriver, x11Driver windowDriver
|
||||
|
||||
func newWindow(window *callbacks, options []Option) {
|
||||
func newWindow(window *callbacks, options []Option) error {
|
||||
var errFirst error
|
||||
for _, d := range []windowDriver{wlDriver, x11Driver} {
|
||||
if d == nil {
|
||||
@@ -56,16 +57,16 @@ func newWindow(window *callbacks, options []Option) {
|
||||
}
|
||||
err := d(window, options)
|
||||
if err == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if errFirst == nil {
|
||||
errFirst = err
|
||||
}
|
||||
}
|
||||
if errFirst == nil {
|
||||
errFirst = errors.New("app: no window driver available")
|
||||
if errFirst != nil {
|
||||
return errFirst
|
||||
}
|
||||
window.ProcessEvent(DestroyEvent{Err: errFirst})
|
||||
return errors.New("app: no window driver available")
|
||||
}
|
||||
|
||||
// xCursor contains mapping from pointer.Cursor to XCursor.
|
||||
|
||||
+117
-176
@@ -15,7 +15,6 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -26,12 +25,10 @@ import (
|
||||
"gioui.org/app/internal/xkb"
|
||||
"gioui.org/f32"
|
||||
"gioui.org/internal/fling"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
|
||||
@@ -100,9 +97,7 @@ type wlDisplay struct {
|
||||
read, write int
|
||||
}
|
||||
|
||||
repeat repeatState
|
||||
poller poller
|
||||
readClipClose chan struct{}
|
||||
repeat repeatState
|
||||
}
|
||||
|
||||
type wlSeat struct {
|
||||
@@ -142,7 +137,7 @@ type repeatState struct {
|
||||
delay time.Duration
|
||||
|
||||
key uint32
|
||||
win *window
|
||||
win *callbacks
|
||||
stopC chan struct{}
|
||||
|
||||
start time.Duration
|
||||
@@ -199,10 +194,12 @@ type window struct {
|
||||
dir f32.Point
|
||||
}
|
||||
|
||||
configured bool
|
||||
stage system.Stage
|
||||
dead bool
|
||||
lastFrameCallback *C.struct_wl_callback
|
||||
|
||||
animating bool
|
||||
redraw bool
|
||||
// The most recent configure serial waiting to be ack'ed.
|
||||
serial C.uint32_t
|
||||
scale int
|
||||
@@ -212,11 +209,9 @@ type window struct {
|
||||
wsize image.Point // window config size before going fullscreen or maximized
|
||||
inCompositor bool // window is moving or being resized
|
||||
|
||||
clipReads chan transfer.DataEvent
|
||||
clipReads chan clipboard.Event
|
||||
|
||||
wakeups chan struct{}
|
||||
|
||||
closing bool
|
||||
}
|
||||
|
||||
type poller struct {
|
||||
@@ -265,17 +260,25 @@ func newWLWindow(callbacks *callbacks, options []Option) error {
|
||||
return err
|
||||
}
|
||||
w.w = callbacks
|
||||
w.w.SetDriver(w)
|
||||
go func() {
|
||||
defer d.destroy()
|
||||
defer w.destroy()
|
||||
|
||||
// Finish and commit setup from createNativeWindow.
|
||||
w.Configure(options)
|
||||
w.draw(true)
|
||||
C.wl_surface_commit(w.surf)
|
||||
w.w.SetDriver(w)
|
||||
|
||||
w.ProcessEvent(WaylandViewEvent{
|
||||
Display: unsafe.Pointer(w.display()),
|
||||
Surface: unsafe.Pointer(w.surf),
|
||||
})
|
||||
// Finish and commit setup from createNativeWindow.
|
||||
w.Configure(options)
|
||||
C.wl_surface_commit(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
|
||||
}
|
||||
|
||||
@@ -351,7 +354,7 @@ func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) {
|
||||
ppdp: ppdp,
|
||||
ppsp: ppdp,
|
||||
wakeups: make(chan struct{}, 1),
|
||||
clipReads: make(chan transfer.DataEvent, 1),
|
||||
clipReads: make(chan clipboard.Event, 1),
|
||||
}
|
||||
w.surf = C.wl_compositor_create_surface(d.compositor)
|
||||
if w.surf == nil {
|
||||
@@ -546,15 +549,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) {
|
||||
w := callbackLoad(data).(*window)
|
||||
w.serial = serial
|
||||
w.redraw = true
|
||||
C.xdg_surface_ack_configure(wmSurf, serial)
|
||||
w.configured = true
|
||||
w.draw(true)
|
||||
w.setStage(system.StageRunning)
|
||||
}
|
||||
|
||||
//export gio_onToplevelClose
|
||||
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
|
||||
w := callbackLoad(data).(*window)
|
||||
w.closing = true
|
||||
w.dead = true
|
||||
}
|
||||
|
||||
//export gio_onToplevelConfigure
|
||||
@@ -583,8 +586,8 @@ func gio_onToplevelDecorationConfigure(data unsafe.Pointer, deco *C.struct_zxdg_
|
||||
} else {
|
||||
w.size.Y += int(w.config.decoHeight)
|
||||
}
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.draw(true)
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
w.redraw = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,7 +645,7 @@ func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output *
|
||||
if w.config.Mode == Minimized {
|
||||
// Minimized window got brought back up: it is no longer so.
|
||||
w.config.Mode = Windowed
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,7 +790,7 @@ func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.
|
||||
X: fromFixed(x) * float32(w.scale),
|
||||
Y: fromFixed(y) * float32(w.scale),
|
||||
}
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: pointer.Press,
|
||||
Source: pointer.Touch,
|
||||
Position: w.lastTouch,
|
||||
@@ -803,7 +806,7 @@ func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.ui
|
||||
s.serial = serial
|
||||
w := s.touchFoci[id]
|
||||
delete(s.touchFoci, id)
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: pointer.Release,
|
||||
Source: pointer.Touch,
|
||||
Position: w.lastTouch,
|
||||
@@ -821,7 +824,7 @@ func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32
|
||||
X: fromFixed(x) * float32(w.scale),
|
||||
Y: fromFixed(y) * float32(w.scale),
|
||||
}
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: pointer.Move,
|
||||
Position: w.lastTouch,
|
||||
Source: pointer.Touch,
|
||||
@@ -840,7 +843,7 @@ func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) {
|
||||
s := callbackLoad(data).(*wlSeat)
|
||||
for id, w := range s.touchFoci {
|
||||
delete(s.touchFoci, id)
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: pointer.Cancel,
|
||||
Source: pointer.Touch,
|
||||
})
|
||||
@@ -866,7 +869,7 @@ func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.ui
|
||||
s.serial = serial
|
||||
if w.inCompositor {
|
||||
w.inCompositor = false
|
||||
w.ProcessEvent(pointer.Event{Kind: pointer.Cancel})
|
||||
w.w.Event(pointer.Event{Kind: pointer.Cancel})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,7 +930,7 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t,
|
||||
}
|
||||
w.flushScroll()
|
||||
w.resetFling()
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: kind,
|
||||
Source: pointer.Mouse,
|
||||
Buttons: w.pointerBtns,
|
||||
@@ -1015,34 +1018,23 @@ func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, axis
|
||||
}
|
||||
|
||||
func (w *window) ReadClipboard() {
|
||||
if w.disp.readClipClose != nil {
|
||||
return
|
||||
}
|
||||
w.disp.readClipClose = make(chan struct{})
|
||||
r, err := w.disp.readClipboard()
|
||||
// Send empty responses on unavailable clipboards or errors.
|
||||
if r == nil || err != nil {
|
||||
w.w.Event(clipboard.Event{})
|
||||
return
|
||||
}
|
||||
// Don't let slow clipboard transfers block event loop.
|
||||
go func() {
|
||||
defer r.Close()
|
||||
data, _ := io.ReadAll(r)
|
||||
e := transfer.DataEvent{
|
||||
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:
|
||||
}
|
||||
w.clipReads <- clipboard.Event{Text: string(data)}
|
||||
w.Wakeup()
|
||||
}()
|
||||
}
|
||||
|
||||
func (w *window) WriteClipboard(mime string, s []byte) {
|
||||
w.disp.writeClipboard(s)
|
||||
func (w *window) WriteClipboard(s string) {
|
||||
w.disp.writeClipboard([]byte(s))
|
||||
}
|
||||
|
||||
func (w *window) Configure(options []Option) {
|
||||
@@ -1100,7 +1092,8 @@ func (w *window) Configure(options []Option) {
|
||||
w.config.MaxSize = cnf.MaxSize
|
||||
w.setWindowConstraints()
|
||||
}
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
w.redraw = true
|
||||
}
|
||||
|
||||
func (w *window) setWindowConstraints() {
|
||||
@@ -1137,7 +1130,7 @@ func (w *window) Perform(actions system.Action) {
|
||||
walkActions(actions, func(action system.Action) {
|
||||
switch action {
|
||||
case system.ActionClose:
|
||||
w.closing = true
|
||||
w.dead = true
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1220,8 +1213,7 @@ func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
|
||||
w := callbackLoad(unsafe.Pointer(surf)).(*window)
|
||||
s.keyboardFocus = w
|
||||
s.disp.repeat.Stop(0)
|
||||
w.config.Focused = true
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(key.FocusEvent{Focus: true})
|
||||
}
|
||||
|
||||
//export gio_onKeyboardLeave
|
||||
@@ -1230,8 +1222,7 @@ func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
|
||||
s.serial = serial
|
||||
s.disp.repeat.Stop(0)
|
||||
w := s.keyboardFocus
|
||||
w.config.Focused = false
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(key.FocusEvent{Focus: false})
|
||||
}
|
||||
|
||||
//export gio_onKeyboardKey
|
||||
@@ -1249,7 +1240,7 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri
|
||||
// There's no support for IME yet.
|
||||
w.w.EditorInsert(ee.Text)
|
||||
} else {
|
||||
w.ProcessEvent(e)
|
||||
w.w.Event(e)
|
||||
}
|
||||
}
|
||||
if state != C.WL_KEYBOARD_KEY_STATE_PRESSED {
|
||||
@@ -1284,7 +1275,7 @@ func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) {
|
||||
r.now = 0
|
||||
r.stopC = stopC
|
||||
r.key = keyCode
|
||||
r.win = w
|
||||
r.win = w.w
|
||||
rate, delay := r.rate, r.delay
|
||||
go func() {
|
||||
timer := time.NewTimer(delay)
|
||||
@@ -1342,9 +1333,9 @@ func (r *repeatState) Repeat(d *wlDisplay) {
|
||||
for _, e := range d.xkb.DispatchKey(r.key, key.Press) {
|
||||
if ee, ok := e.(key.EditEvent); ok {
|
||||
// There's no support for IME yet.
|
||||
r.win.w.EditorInsert(ee.Text)
|
||||
r.win.EditorInsert(ee.Text)
|
||||
} else {
|
||||
r.win.ProcessEvent(e)
|
||||
r.win.Event(e)
|
||||
}
|
||||
}
|
||||
r.last += delay
|
||||
@@ -1357,68 +1348,28 @@ func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.ui
|
||||
w := callbackLoad(data).(*window)
|
||||
if w.lastFrameCallback == callback {
|
||||
w.lastFrameCallback = nil
|
||||
w.draw(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) close(err error) {
|
||||
w.ProcessEvent(WaylandViewEvent{})
|
||||
w.ProcessEvent(DestroyEvent{Err: err})
|
||||
w.destroy()
|
||||
w.disp.destroy()
|
||||
w.disp = nil
|
||||
}
|
||||
|
||||
func (w *window) dispatch() {
|
||||
if w.disp == nil {
|
||||
<-w.wakeups
|
||||
w.w.Invalidate()
|
||||
return
|
||||
}
|
||||
if err := w.disp.dispatch(); err != nil || w.closing {
|
||||
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 {
|
||||
func (w *window) loop() error {
|
||||
var p poller
|
||||
for {
|
||||
evt, ok := w.w.nextEvent()
|
||||
if !ok {
|
||||
w.dispatch()
|
||||
continue
|
||||
if err := w.disp.dispatch(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
return evt
|
||||
select {
|
||||
case e := <-w.clipReads:
|
||||
w.w.Event(e)
|
||||
case <-w.wakeups:
|
||||
w.w.Event(wakeupEvent{})
|
||||
default:
|
||||
}
|
||||
if w.dead {
|
||||
break
|
||||
}
|
||||
w.draw()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) Invalidate() {
|
||||
select {
|
||||
case w.wakeups <- struct{}{}:
|
||||
default:
|
||||
return
|
||||
}
|
||||
w.disp.wakeup()
|
||||
}
|
||||
|
||||
func (w *window) Run(f func()) {
|
||||
f()
|
||||
}
|
||||
|
||||
func (w *window) Frame(frame *op.Ops) {
|
||||
w.w.ProcessFrame(frame, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// bindDataDevice initializes the dataDev field if and only if both
|
||||
@@ -1434,21 +1385,13 @@ func (d *wlDisplay) bindDataDevice() {
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
func (d *wlDisplay) dispatch(p *poller) error {
|
||||
dispfd := C.wl_display_get_fd(d.disp)
|
||||
// Poll for events and notifications.
|
||||
pollfds := append(d.poller.pollfds[:0],
|
||||
pollfds := append(p.pollfds[:0],
|
||||
syscall.PollFd{Fd: int32(dispfd), 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]
|
||||
if ret, err := C.wl_display_flush(d.disp); ret < 0 {
|
||||
if err != syscall.EAGAIN {
|
||||
@@ -1459,25 +1402,11 @@ func (d *wlDisplay) dispatch() error {
|
||||
dispFd.Events |= syscall.POLLOUT
|
||||
}
|
||||
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)
|
||||
}
|
||||
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.
|
||||
for {
|
||||
_, err := syscall.Read(d.notify.read, d.poller.buf[:])
|
||||
_, err := syscall.Read(d.notify.read, p.buf[:])
|
||||
if err == syscall.EAGAIN {
|
||||
break
|
||||
}
|
||||
@@ -1485,15 +1414,29 @@ func (d *wlDisplay) dispatch() error {
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *window) Wakeup() {
|
||||
select {
|
||||
case w.wakeups <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
w.disp.wakeup()
|
||||
}
|
||||
|
||||
func (w *window) SetAnimating(anim bool) {
|
||||
w.animating = anim
|
||||
if anim {
|
||||
w.draw(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Wakeup wakes up the event loop through the notification pipe.
|
||||
@@ -1505,10 +1448,6 @@ func (d *wlDisplay) wakeup() {
|
||||
}
|
||||
|
||||
func (w *window) destroy() {
|
||||
if w.lastFrameCallback != nil {
|
||||
C.wl_callback_destroy(w.lastFrameCallback)
|
||||
w.lastFrameCallback = nil
|
||||
}
|
||||
if w.cursor.surf != nil {
|
||||
C.wl_surface_destroy(w.cursor.surf)
|
||||
}
|
||||
@@ -1633,15 +1572,7 @@ func (w *window) flushScroll() {
|
||||
if total == (f32.Point{}) {
|
||||
return
|
||||
}
|
||||
if w.scroll.steps == (image.Point{}) {
|
||||
w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X)
|
||||
w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y)
|
||||
}
|
||||
// Zero scroll distance prior to calling ProcessEvent, otherwise we may recursively
|
||||
// re-process the scroll distance.
|
||||
w.scroll.dist = f32.Point{}
|
||||
w.scroll.steps = image.Point{}
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: pointer.Scroll,
|
||||
Source: pointer.Mouse,
|
||||
Buttons: w.pointerBtns,
|
||||
@@ -1650,6 +1581,12 @@ func (w *window) flushScroll() {
|
||||
Time: w.scroll.time,
|
||||
Modifiers: w.disp.xkb.Modifiers(),
|
||||
})
|
||||
if w.scroll.steps == (image.Point{}) {
|
||||
w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X)
|
||||
w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y)
|
||||
}
|
||||
w.scroll.dist = f32.Point{}
|
||||
w.scroll.steps = image.Point{}
|
||||
}
|
||||
|
||||
func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
|
||||
@@ -1658,7 +1595,7 @@ func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
|
||||
X: fromFixed(x) * float32(w.scale),
|
||||
Y: fromFixed(y) * float32(w.scale),
|
||||
}
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: pointer.Move,
|
||||
Position: w.lastPos,
|
||||
Buttons: w.pointerBtns,
|
||||
@@ -1679,8 +1616,7 @@ func (w *window) systemGesture() (*C.struct_wl_cursor, C.uint32_t) {
|
||||
if w.config.Mode != Windowed || w.config.Decorated {
|
||||
return nil, 0
|
||||
}
|
||||
_, cfg := w.getConfig()
|
||||
border := cfg.Dp(3)
|
||||
border := w.w.w.metric.Dp(3)
|
||||
x, y, size := int(w.lastPos.X), int(w.lastPos.Y), w.config.Size
|
||||
north := y <= border
|
||||
south := y >= size.Y-border
|
||||
@@ -1734,10 +1670,13 @@ func (w *window) updateOutputs() {
|
||||
if found && scale != w.scale {
|
||||
w.scale = scale
|
||||
C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale))
|
||||
w.draw(true)
|
||||
w.redraw = true
|
||||
}
|
||||
if found {
|
||||
w.draw(true)
|
||||
if !found {
|
||||
w.setStage(system.StagePaused)
|
||||
} else {
|
||||
w.setStage(system.StageRunning)
|
||||
w.redraw = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1749,10 +1688,7 @@ func (w *window) getConfig() (image.Point, unit.Metric) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) draw(sync bool) {
|
||||
if !w.configured {
|
||||
return
|
||||
}
|
||||
func (w *window) draw() {
|
||||
w.flushScroll()
|
||||
size, cfg := w.getConfig()
|
||||
if cfg == (unit.Metric{}) {
|
||||
@@ -1760,9 +1696,11 @@ func (w *window) draw(sync bool) {
|
||||
}
|
||||
if size != w.config.Size {
|
||||
w.config.Size = size
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
anim := w.animating || w.fling.anim.Active()
|
||||
sync := w.redraw
|
||||
w.redraw = false
|
||||
// Draw animation only when not waiting for frame callback.
|
||||
redrawAnim := anim && w.lastFrameCallback == nil
|
||||
if !redrawAnim && !sync {
|
||||
@@ -1773,8 +1711,8 @@ func (w *window) draw(sync bool) {
|
||||
// Use the surface as listener data for gio_onFrameDone.
|
||||
C.wl_callback_add_listener(w.lastFrameCallback, &C.gio_callback_listener, unsafe.Pointer(w.surf))
|
||||
}
|
||||
w.ProcessEvent(frameEvent{
|
||||
FrameEvent: FrameEvent{
|
||||
w.w.Event(frameEvent{
|
||||
FrameEvent: system.FrameEvent{
|
||||
Now: time.Now(),
|
||||
Size: w.config.Size,
|
||||
Metric: cfg,
|
||||
@@ -1783,6 +1721,14 @@ func (w *window) draw(sync bool) {
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
return w.disp.disp
|
||||
}
|
||||
@@ -1874,10 +1820,6 @@ func newWLDisplay() (*wlDisplay, error) {
|
||||
}
|
||||
|
||||
func (d *wlDisplay) destroy() {
|
||||
if d.readClipClose != nil {
|
||||
close(d.readClipClose)
|
||||
d.readClipClose = nil
|
||||
}
|
||||
if d.notify.write != 0 {
|
||||
syscall.Close(d.notify.write)
|
||||
d.notify.write = 0
|
||||
@@ -1919,7 +1861,6 @@ func (d *wlDisplay) destroy() {
|
||||
if d.disp != nil {
|
||||
C.wl_display_disconnect(d.disp)
|
||||
callbackDelete(unsafe.Pointer(d.disp))
|
||||
d.disp = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+71
-92
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -19,19 +18,17 @@ import (
|
||||
syscall "golang.org/x/sys/windows"
|
||||
|
||||
"gioui.org/app/internal/windows"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
gowindows "golang.org/x/sys/windows"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
)
|
||||
|
||||
type Win32ViewEvent struct {
|
||||
type ViewEvent struct {
|
||||
HWND uintptr
|
||||
}
|
||||
|
||||
@@ -39,6 +36,7 @@ type window struct {
|
||||
hwnd syscall.Handle
|
||||
hdc syscall.Handle
|
||||
w *callbacks
|
||||
stage system.Stage
|
||||
pointerBtns pointer.Buttons
|
||||
|
||||
// cursorIn tracks whether the cursor was inside the window according
|
||||
@@ -50,10 +48,10 @@ type window struct {
|
||||
placement *windows.WindowPlacement
|
||||
|
||||
animating bool
|
||||
focused bool
|
||||
|
||||
borderSize image.Point
|
||||
config Config
|
||||
loop *eventLoop
|
||||
}
|
||||
|
||||
const _WM_WAKEUP = windows.WM_USER + iota
|
||||
@@ -86,38 +84,36 @@ func osMain() {
|
||||
select {}
|
||||
}
|
||||
|
||||
func newWindow(win *callbacks, options []Option) {
|
||||
done := make(chan struct{})
|
||||
func newWindow(window *callbacks, options []Option) error {
|
||||
cerr := make(chan error)
|
||||
go func() {
|
||||
// GetMessage and PeekMessage can filter on a window HWND, but
|
||||
// then thread-specific messages such as WM_QUIT are ignored.
|
||||
// Instead lock the thread so window messages arrive through
|
||||
// unfiltered GetMessage calls.
|
||||
runtime.LockOSThread()
|
||||
|
||||
w := &window{
|
||||
w: win,
|
||||
}
|
||||
w.loop = newEventLoop(w.w, w.wakeup)
|
||||
w.w.SetDriver(w)
|
||||
err := w.init()
|
||||
done <- struct{}{}
|
||||
w, err := createNativeWindow()
|
||||
if err != nil {
|
||||
w.ProcessEvent(DestroyEvent{Err: err})
|
||||
cerr <- err
|
||||
return
|
||||
}
|
||||
cerr <- nil
|
||||
winMap.Store(w.hwnd, w)
|
||||
defer winMap.Delete(w.hwnd)
|
||||
w.ProcessEvent(Win32ViewEvent{HWND: uintptr(w.hwnd)})
|
||||
w.w = window
|
||||
w.w.SetDriver(w)
|
||||
w.w.Event(ViewEvent{HWND: uintptr(w.hwnd)})
|
||||
w.Configure(options)
|
||||
windows.SetForegroundWindow(w.hwnd)
|
||||
windows.SetFocus(w.hwnd)
|
||||
// Since the window class for the cursor is null,
|
||||
// set it here to show the cursor.
|
||||
w.SetCursor(pointer.CursorDefault)
|
||||
w.runLoop()
|
||||
if err := w.loop(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
<-done
|
||||
return <-cerr
|
||||
}
|
||||
|
||||
// initResources initializes the resources global.
|
||||
@@ -152,13 +148,13 @@ func initResources() error {
|
||||
|
||||
const dwExStyle = windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE
|
||||
|
||||
func (w *window) init() error {
|
||||
func createNativeWindow() (*window, error) {
|
||||
var resErr error
|
||||
resources.once.Do(func() {
|
||||
resErr = initResources()
|
||||
})
|
||||
if resErr != nil {
|
||||
return resErr
|
||||
return nil, resErr
|
||||
}
|
||||
const dwStyle = windows.WS_OVERLAPPEDWINDOW
|
||||
|
||||
@@ -174,15 +170,16 @@ func (w *window) init() error {
|
||||
resources.handle,
|
||||
0)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
w := &window{
|
||||
hwnd: hwnd,
|
||||
}
|
||||
w.hdc, err = windows.GetDC(hwnd)
|
||||
if err != nil {
|
||||
windows.DestroyWindow(hwnd)
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
w.hwnd = hwnd
|
||||
return nil
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// update() handles changes done by the user, and updates the configuration.
|
||||
@@ -199,7 +196,7 @@ func (w *window) update() {
|
||||
windows.GetSystemMetrics(windows.SM_CXSIZEFRAME),
|
||||
windows.GetSystemMetrics(windows.SM_CYSIZEFRAME),
|
||||
)
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
|
||||
func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr {
|
||||
@@ -240,7 +237,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
||||
e.State = key.Release
|
||||
}
|
||||
|
||||
w.ProcessEvent(e)
|
||||
w.w.Event(e)
|
||||
|
||||
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
|
||||
@@ -261,15 +258,23 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
||||
case windows.WM_MBUTTONUP:
|
||||
w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers())
|
||||
case windows.WM_CANCELMODE:
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: pointer.Cancel,
|
||||
})
|
||||
case windows.WM_SETFOCUS:
|
||||
w.config.Focused = true
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.focused = true
|
||||
w.w.Event(key.FocusEvent{Focus: true})
|
||||
case windows.WM_KILLFOCUS:
|
||||
w.config.Focused = false
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.focused = false
|
||||
w.w.Event(key.FocusEvent{Focus: false})
|
||||
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:
|
||||
if w.config.Decorated {
|
||||
// Let the system handle it.
|
||||
@@ -282,7 +287,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
||||
case windows.WM_MOUSEMOVE:
|
||||
x, y := coordsFromlParam(lParam)
|
||||
p := f32.Point{X: float32(x), Y: float32(y)}
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: pointer.Move,
|
||||
Source: pointer.Mouse,
|
||||
Position: p,
|
||||
@@ -295,8 +300,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
||||
case windows.WM_MOUSEHWHEEL:
|
||||
w.scrollEvent(wParam, lParam, true, getModifiers())
|
||||
case windows.WM_DESTROY:
|
||||
w.ProcessEvent(Win32ViewEvent{})
|
||||
w.ProcessEvent(DestroyEvent{})
|
||||
w.w.Event(ViewEvent{})
|
||||
w.w.Event(system.DestroyEvent{})
|
||||
if w.hdc != 0 {
|
||||
windows.ReleaseDC(w.hdc)
|
||||
w.hdc = 0
|
||||
@@ -322,16 +327,10 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
||||
// 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))
|
||||
szp := (*windows.NCCalcSizeParams)(unsafe.Pointer(uintptr(lParam)))
|
||||
mi := windows.GetMonitorInfo(w.hwnd)
|
||||
szp.Rgrc[0] = mi.WorkArea
|
||||
return 0
|
||||
case windows.WM_NCLBUTTONDBLCLK:
|
||||
if !w.config.Decorated {
|
||||
// Override Windows behaviour when we
|
||||
// draw decorations.
|
||||
return 0
|
||||
}
|
||||
case windows.WM_PAINT:
|
||||
w.draw(true)
|
||||
case windows.WM_SIZE:
|
||||
@@ -339,15 +338,18 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
||||
switch wParam {
|
||||
case windows.SIZE_MINIMIZED:
|
||||
w.config.Mode = Minimized
|
||||
w.setStage(system.StagePaused)
|
||||
case windows.SIZE_MAXIMIZED:
|
||||
w.config.Mode = Maximized
|
||||
w.setStage(system.StageRunning)
|
||||
case windows.SIZE_RESTORED:
|
||||
if w.config.Mode != Fullscreen {
|
||||
w.config.Mode = Windowed
|
||||
}
|
||||
w.setStage(system.StageRunning)
|
||||
}
|
||||
case windows.WM_GETMINMAXINFO:
|
||||
mm := (*windows.MinMaxInfo)(unsafe.Pointer(lParam))
|
||||
mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam)))
|
||||
var bw, bh int32
|
||||
if w.config.Decorated {
|
||||
r := windows.GetWindowRect(w.hwnd)
|
||||
@@ -375,8 +377,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
||||
return windows.TRUE
|
||||
}
|
||||
case _WM_WAKEUP:
|
||||
w.loop.Wakeup()
|
||||
w.loop.FlushEvents()
|
||||
w.w.Event(wakeupEvent{})
|
||||
case windows.WM_IME_STARTCOMPOSITION:
|
||||
imc := windows.ImmGetContext(w.hwnd)
|
||||
if imc == 0 {
|
||||
@@ -496,7 +497,7 @@ func (w *window) hitTest(x, y int) uintptr {
|
||||
}
|
||||
|
||||
func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, kmods key.Modifiers) {
|
||||
if !w.config.Focused {
|
||||
if !w.focused {
|
||||
windows.SetFocus(w.hwnd)
|
||||
}
|
||||
|
||||
@@ -516,7 +517,7 @@ func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr,
|
||||
}
|
||||
x, y := coordsFromlParam(lParam)
|
||||
p := f32.Point{X: float32(x), Y: float32(y)}
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: kind,
|
||||
Source: pointer.Mouse,
|
||||
Position: p,
|
||||
@@ -551,7 +552,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
|
||||
sp.Y = -dist
|
||||
}
|
||||
}
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: pointer.Scroll,
|
||||
Source: pointer.Mouse,
|
||||
Position: p,
|
||||
@@ -563,7 +564,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
|
||||
}
|
||||
|
||||
// Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/
|
||||
func (w *window) runLoop() {
|
||||
func (w *window) loop() error {
|
||||
msg := new(windows.Msg)
|
||||
loop:
|
||||
for {
|
||||
@@ -574,7 +575,7 @@ loop:
|
||||
}
|
||||
switch ret := windows.GetMessage(msg, 0, 0, 0); ret {
|
||||
case -1:
|
||||
panic(errors.New("GetMessage failed"))
|
||||
return errors.New("GetMessage failed")
|
||||
case 0:
|
||||
// WM_QUIT received.
|
||||
break loop
|
||||
@@ -582,6 +583,7 @@ loop:
|
||||
windows.TranslateMessage(msg)
|
||||
windows.DispatchMessage(msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *window) EditorStateChanged(old, new editorState) {
|
||||
@@ -599,41 +601,27 @@ func (w *window) SetAnimating(anim bool) {
|
||||
w.animating = anim
|
||||
}
|
||||
|
||||
func (w *window) ProcessEvent(e event.Event) {
|
||||
w.w.ProcessEvent(e)
|
||||
w.loop.FlushEvents()
|
||||
}
|
||||
|
||||
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) wakeup() {
|
||||
func (w *window) Wakeup() {
|
||||
if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) setStage(s system.Stage) {
|
||||
if s != w.stage {
|
||||
w.stage = s
|
||||
w.w.Event(system.StageEvent{Stage: s})
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) draw(sync bool) {
|
||||
if w.config.Size.X == 0 || w.config.Size.Y == 0 {
|
||||
return
|
||||
}
|
||||
dpi := windows.GetWindowDPI(w.hwnd)
|
||||
cfg := configForDPI(dpi)
|
||||
w.ProcessEvent(frameEvent{
|
||||
FrameEvent: FrameEvent{
|
||||
w.w.Event(frameEvent{
|
||||
FrameEvent: system.FrameEvent{
|
||||
Now: time.Now(),
|
||||
Size: w.config.Size,
|
||||
Metric: cfg,
|
||||
@@ -679,12 +667,7 @@ func (w *window) readClipboard() error {
|
||||
}
|
||||
defer windows.GlobalUnlock(mem)
|
||||
content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr)))
|
||||
w.ProcessEvent(transfer.DataEvent{
|
||||
Type: "application/text",
|
||||
Open: func() io.ReadCloser {
|
||||
return io.NopCloser(strings.NewReader(content))
|
||||
},
|
||||
})
|
||||
w.w.Event(clipboard.Event{Text: content})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -752,8 +735,8 @@ func (w *window) Configure(options []Option) {
|
||||
w.update()
|
||||
}
|
||||
|
||||
func (w *window) WriteClipboard(mime string, s []byte) {
|
||||
w.writeClipboard(string(s))
|
||||
func (w *window) WriteClipboard(s string) {
|
||||
w.writeClipboard(s)
|
||||
}
|
||||
|
||||
func (w *window) writeClipboard(s string) error {
|
||||
@@ -881,11 +864,11 @@ func (w *window) raise() {
|
||||
windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW)
|
||||
}
|
||||
|
||||
func convertKeyCode(code uintptr) (key.Name, bool) {
|
||||
func convertKeyCode(code uintptr) (string, bool) {
|
||||
if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' {
|
||||
return key.Name(rune(code)), true
|
||||
return string(rune(code)), true
|
||||
}
|
||||
var r key.Name
|
||||
var r string
|
||||
|
||||
switch code {
|
||||
case windows.VK_ESCAPE:
|
||||
@@ -985,8 +968,4 @@ func configForDPI(dpi int) unit.Metric {
|
||||
}
|
||||
}
|
||||
|
||||
func (Win32ViewEvent) implementsViewEvent() {}
|
||||
func (Win32ViewEvent) ImplementsEvent() {}
|
||||
func (w Win32ViewEvent) Valid() bool {
|
||||
return w != (Win32ViewEvent{})
|
||||
}
|
||||
func (_ ViewEvent) ImplementsEvent() {}
|
||||
|
||||
+83
-115
@@ -30,20 +30,16 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
|
||||
syscall "golang.org/x/sys/unix"
|
||||
@@ -95,10 +91,12 @@ type x11Window struct {
|
||||
// _NET_WM_STATE_MAXIMIZED_VERT
|
||||
wmStateMaximizedVert C.Atom
|
||||
}
|
||||
stage system.Stage
|
||||
metric unit.Metric
|
||||
notify struct {
|
||||
read, write int
|
||||
}
|
||||
dead bool
|
||||
|
||||
animating bool
|
||||
|
||||
@@ -111,8 +109,6 @@ type x11Window struct {
|
||||
config Config
|
||||
|
||||
wakeups chan struct{}
|
||||
handler x11EventHandler
|
||||
buf [100]byte
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -155,8 +151,8 @@ func (w *x11Window) ReadClipboard() {
|
||||
C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime)
|
||||
}
|
||||
|
||||
func (w *x11Window) WriteClipboard(mime string, s []byte) {
|
||||
w.clipboard.content = s
|
||||
func (w *x11Window) WriteClipboard(s string) {
|
||||
w.clipboard.content = []byte(s)
|
||||
C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime)
|
||||
C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime)
|
||||
}
|
||||
@@ -236,7 +232,7 @@ func (w *x11Window) Configure(options []Option) {
|
||||
if cnf.Decorated != prev.Decorated {
|
||||
w.config.Decorated = cnf.Decorated
|
||||
}
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
|
||||
func (w *x11Window) setTitle(prev, cnf Config) {
|
||||
@@ -379,36 +375,7 @@ func (w *x11Window) sendWMStateEvent(action C.long, atom1, atom2 C.ulong) {
|
||||
|
||||
var x11OneByte = make([]byte, 1)
|
||||
|
||||
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})
|
||||
w.destroy()
|
||||
}
|
||||
|
||||
func (w *x11Window) Event() event.Event {
|
||||
for {
|
||||
evt, ok := w.w.nextEvent()
|
||||
if !ok {
|
||||
w.dispatch()
|
||||
continue
|
||||
}
|
||||
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() {
|
||||
func (w *x11Window) Wakeup() {
|
||||
select {
|
||||
case w.wakeups <- struct{}{}:
|
||||
default:
|
||||
@@ -426,20 +393,16 @@ func (w *x11Window) window() (C.Window, int, int) {
|
||||
return w.xw, w.config.Size.X, w.config.Size.Y
|
||||
}
|
||||
|
||||
func (w *x11Window) dispatch() {
|
||||
if w.x == nil {
|
||||
// Only Invalidate can wake us up.
|
||||
<-w.wakeups
|
||||
w.w.Invalidate()
|
||||
func (w *x11Window) setStage(s system.Stage) {
|
||||
if s == w.stage {
|
||||
return
|
||||
}
|
||||
w.stage = s
|
||||
w.w.Event(system.StageEvent{Stage: s})
|
||||
}
|
||||
|
||||
select {
|
||||
case <-w.wakeups:
|
||||
w.w.Invalidate()
|
||||
default:
|
||||
}
|
||||
|
||||
func (w *x11Window) loop() {
|
||||
h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
|
||||
xfd := C.XConnectionNumber(w.x)
|
||||
|
||||
// Poll for events and notifications.
|
||||
@@ -449,54 +412,60 @@ func (w *x11Window) dispatch() {
|
||||
}
|
||||
xEvents := &pollfds[0].Revents
|
||||
// Plenty of room for a backlog of notifications.
|
||||
buf := make([]byte, 100)
|
||||
|
||||
var syn, anim bool
|
||||
// Check for pending draw events before checking animation or blocking.
|
||||
// This fixes an issue on Xephyr where on startup XPending() > 0 but
|
||||
// poll will still block. This also prevents no-op calls to poll.
|
||||
syn = w.handler.handleEvents()
|
||||
if w.x == nil {
|
||||
// handleEvents received a close request and destroyed the window.
|
||||
return
|
||||
}
|
||||
if !syn {
|
||||
anim = w.animating
|
||||
if !anim {
|
||||
// Clear poll events.
|
||||
*xEvents = 0
|
||||
// Wait for X event or gio notification.
|
||||
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:
|
||||
syn = w.handler.handleEvents()
|
||||
if w.x == nil {
|
||||
return
|
||||
loop:
|
||||
for !w.dead {
|
||||
var syn, anim bool
|
||||
// Check for pending draw events before checking animation or blocking.
|
||||
// This fixes an issue on Xephyr where on startup XPending() > 0 but
|
||||
// poll will still block. This also prevents no-op calls to poll.
|
||||
if syn = h.handleEvents(); !syn {
|
||||
anim = w.animating
|
||||
if !anim {
|
||||
// Clear poll events.
|
||||
*xEvents = 0
|
||||
// Wait for X event or gio notification.
|
||||
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:
|
||||
syn = h.handleEvents()
|
||||
if w.dead {
|
||||
break loop
|
||||
}
|
||||
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
|
||||
break loop
|
||||
}
|
||||
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear notifications.
|
||||
for {
|
||||
_, err := syscall.Read(w.notify.read, w.buf[:])
|
||||
if err == syscall.EAGAIN {
|
||||
break
|
||||
// Clear notifications.
|
||||
for {
|
||||
_, err := syscall.Read(w.notify.read, buf)
|
||||
if err == syscall.EAGAIN {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
|
||||
select {
|
||||
case <-w.wakeups:
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,7 +484,6 @@ func (w *x11Window) destroy() {
|
||||
}
|
||||
C.XDestroyWindow(w.x, w.xw)
|
||||
C.XCloseDisplay(w.x)
|
||||
w.x = nil
|
||||
}
|
||||
|
||||
// atom is a wrapper around XInternAtom. Callers should cache the result
|
||||
@@ -573,7 +541,7 @@ func (h *x11EventHandler) handleEvents() bool {
|
||||
// There's no support for IME yet.
|
||||
w.w.EditorInsert(ee.Text)
|
||||
} else {
|
||||
w.ProcessEvent(e)
|
||||
w.w.Event(e)
|
||||
}
|
||||
}
|
||||
case C.ButtonPress, C.ButtonRelease:
|
||||
@@ -635,10 +603,10 @@ func (h *x11EventHandler) handleEvents() bool {
|
||||
w.pointerBtns &^= btn
|
||||
}
|
||||
ev.Buttons = w.pointerBtns
|
||||
w.ProcessEvent(ev)
|
||||
w.w.Event(ev)
|
||||
case C.MotionNotify:
|
||||
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
|
||||
w.ProcessEvent(pointer.Event{
|
||||
w.w.Event(pointer.Event{
|
||||
Kind: pointer.Move,
|
||||
Source: pointer.Mouse,
|
||||
Buttons: w.pointerBtns,
|
||||
@@ -653,16 +621,14 @@ func (h *x11EventHandler) handleEvents() bool {
|
||||
// redraw only on the last expose event
|
||||
redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0
|
||||
case C.FocusIn:
|
||||
w.config.Focused = true
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(key.FocusEvent{Focus: true})
|
||||
case C.FocusOut:
|
||||
w.config.Focused = false
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(key.FocusEvent{Focus: false})
|
||||
case C.ConfigureNotify: // window configuration change
|
||||
cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev))
|
||||
if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size {
|
||||
w.config.Size = sz
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
w.w.Event(ConfigEvent{Config: w.config})
|
||||
}
|
||||
// redraw will be done by a later expose event
|
||||
case C.SelectionNotify:
|
||||
@@ -684,12 +650,7 @@ func (h *x11EventHandler) handleEvents() bool {
|
||||
break
|
||||
}
|
||||
str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
|
||||
w.ProcessEvent(transfer.DataEvent{
|
||||
Type: "application/text",
|
||||
Open: func() io.ReadCloser {
|
||||
return io.NopCloser(strings.NewReader(str))
|
||||
},
|
||||
})
|
||||
w.w.Event(clipboard.Event{Text: str})
|
||||
case C.SelectionRequest:
|
||||
cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
|
||||
if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None {
|
||||
@@ -743,7 +704,7 @@ func (h *x11EventHandler) handleEvents() bool {
|
||||
cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev))
|
||||
switch *(*C.long)(unsafe.Pointer(&cevt.data)) {
|
||||
case C.long(w.atoms.evDelWindow):
|
||||
w.shutdown(nil)
|
||||
w.dead = true
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -825,10 +786,8 @@ func newX11Window(gioWin *callbacks, options []Option) error {
|
||||
wakeups: make(chan struct{}, 1),
|
||||
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.write = pipe[1]
|
||||
w.w.SetDriver(w)
|
||||
|
||||
if err := w.updateXkbKeymap(); err != nil {
|
||||
w.destroy()
|
||||
@@ -864,10 +823,19 @@ func newX11Window(gioWin *callbacks, options []Option) error {
|
||||
// extensions
|
||||
C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1)
|
||||
|
||||
// make the window visible on the screen
|
||||
C.XMapWindow(dpy, win)
|
||||
w.Configure(options)
|
||||
w.ProcessEvent(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
|
||||
go func() {
|
||||
w.w.SetDriver(w)
|
||||
|
||||
// make the window visible on the screen
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// 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() {}
|
||||
+607
-390
File diff suppressed because it is too large
Load Diff
Generated
+17
-48
@@ -9,11 +9,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701721028,
|
||||
"narHash": "sha256-2z4YrdHPLoMZNWR1MPOjNZMqPg057i1eZXaYI6RTahQ=",
|
||||
"lastModified": 1659298920,
|
||||
"narHash": "sha256-LgRMge8BZUG15EN43iDJOlnEMX1dvRprB7SaoNqgibU=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "c923f9ec0f4dd0d7dc725dc5b73fbf03658e50dd",
|
||||
"rev": "d4f20a3cd4ce961bb23b48447457f6810d69ae5e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -24,18 +24,21 @@
|
||||
},
|
||||
"devshell": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"flake-utils": [
|
||||
"android",
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems"
|
||||
"nixpkgs": [
|
||||
"android",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701697687,
|
||||
"narHash": "sha256-dLLE5wQBVv+pIb4bWmKFSw2DvLVyuEk0F7ng6hpZPSU=",
|
||||
"lastModified": 1658746384,
|
||||
"narHash": "sha256-CCJcoMOcXyZFrV1ag4XMTpAPjLWb4Anbv+ktXFI1ry0=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "c3bd77911391eb1638af6ce773de86da57ee6df5",
|
||||
"rev": "0ffc7937bb5e8141af03d462b468bd071eb18e1b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -45,15 +48,12 @@
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"lastModified": 1656928814,
|
||||
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -64,16 +64,15 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1701282334,
|
||||
"narHash": "sha256-MxCVrXY6v4QmfTwIysjjaX0XUhqBbxTWWB4HXtDYsdk=",
|
||||
"lastModified": 1659305579,
|
||||
"narHash": "sha256-SFeQTmh7hc9Y2fSkooHaoS8mDfPa04sfmUCtQ8MA6Pg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "057f9aecfb71c4437d2b27d3323df7f93c010b7e",
|
||||
"rev": "5857574d45925585baffde730369414319228a84",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "23.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
@@ -83,36 +82,6 @@
|
||||
"android": "android",
|
||||
"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",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
description = "Gio build environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/23.11";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs";
|
||||
android.url = "github:tadfisher/android-nixpkgs";
|
||||
android.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
@@ -33,10 +33,10 @@
|
||||
default = with pkgs; mkShell
|
||||
({
|
||||
ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk";
|
||||
JAVA_HOME = jdk17.home;
|
||||
JAVA_HOME = jdk8.home;
|
||||
packages = [
|
||||
android-sdk
|
||||
jdk17
|
||||
jdk8
|
||||
clang
|
||||
] ++ (if stdenv.isLinux then [
|
||||
vulkan-headers
|
||||
@@ -46,7 +46,7 @@
|
||||
xorg.libXcursor
|
||||
xorg.libXfixes
|
||||
libGL
|
||||
pkg-config
|
||||
pkgconfig
|
||||
] else if stdenv.isDarwin then [
|
||||
darwin.apple_sdk_11_0.frameworks.Foundation
|
||||
darwin.apple_sdk_11_0.frameworks.Metal
|
||||
|
||||
+61
-72
@@ -18,7 +18,6 @@ import (
|
||||
"gioui.org/f32"
|
||||
"gioui.org/internal/fling"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/op"
|
||||
@@ -38,19 +37,15 @@ type Hover struct {
|
||||
|
||||
// Add the gesture to detect hovering over the current pointer area.
|
||||
func (h *Hover) Add(ops *op.Ops) {
|
||||
event.Op(ops, h)
|
||||
pointer.InputOp{
|
||||
Tag: h,
|
||||
Kinds: pointer.Enter | pointer.Leave,
|
||||
}.Add(ops)
|
||||
}
|
||||
|
||||
// Update state and report whether a pointer is inside the area.
|
||||
func (h *Hover) Update(q input.Source) bool {
|
||||
for {
|
||||
ev, ok := q.Event(pointer.Filter{
|
||||
Target: h,
|
||||
Kinds: pointer.Enter | pointer.Leave | pointer.Cancel,
|
||||
})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
func (h *Hover) Update(q event.Queue) bool {
|
||||
for _, ev := range q.Events(h) {
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
@@ -112,6 +107,7 @@ type Drag struct {
|
||||
pressed bool
|
||||
pid pointer.ID
|
||||
start f32.Point
|
||||
grab bool
|
||||
}
|
||||
|
||||
// Scroll detects scroll gestures and reduces them to
|
||||
@@ -119,9 +115,11 @@ type Drag struct {
|
||||
// movements as well as drag and fling touch gestures.
|
||||
type Scroll struct {
|
||||
dragging bool
|
||||
axis Axis
|
||||
estimator fling.Extrapolation
|
||||
flinger fling.Animation
|
||||
pid pointer.ID
|
||||
grab bool
|
||||
last int
|
||||
// Leftover scroll.
|
||||
scroll float32
|
||||
@@ -163,7 +161,10 @@ const touchSlop = unit.Dp(3)
|
||||
|
||||
// Add the handler to the operation list to receive click events.
|
||||
func (c *Click) Add(ops *op.Ops) {
|
||||
event.Op(ops, c)
|
||||
pointer.InputOp{
|
||||
Tag: c,
|
||||
Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
|
||||
}.Add(ops)
|
||||
}
|
||||
|
||||
// Hovered returns whether a pointer is inside the area.
|
||||
@@ -176,16 +177,10 @@ func (c *Click) Pressed() bool {
|
||||
return c.pressed
|
||||
}
|
||||
|
||||
// Update state and return the next click events, if any.
|
||||
func (c *Click) Update(q input.Source) (ClickEvent, bool) {
|
||||
for {
|
||||
evt, ok := q.Event(pointer.Filter{
|
||||
Target: c,
|
||||
Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave | pointer.Cancel,
|
||||
})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
// Update state and return the click events.
|
||||
func (c *Click) Update(q event.Queue) []ClickEvent {
|
||||
var events []ClickEvent
|
||||
for _, evt := range q.Events(c) {
|
||||
e, ok := evt.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
@@ -197,15 +192,9 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
|
||||
}
|
||||
c.pressed = false
|
||||
if !c.entered || c.hovered {
|
||||
return ClickEvent{
|
||||
Kind: KindClick,
|
||||
Position: e.Position.Round(),
|
||||
Source: e.Source,
|
||||
Modifiers: e.Modifiers,
|
||||
NumClicks: c.clicks,
|
||||
}, true
|
||||
events = append(events, ClickEvent{Kind: KindClick, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
|
||||
} else {
|
||||
return ClickEvent{Kind: KindCancel}, true
|
||||
events = append(events, ClickEvent{Kind: KindCancel})
|
||||
}
|
||||
case pointer.Cancel:
|
||||
wasPressed := c.pressed
|
||||
@@ -213,7 +202,7 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
|
||||
c.hovered = false
|
||||
c.entered = false
|
||||
if wasPressed {
|
||||
return ClickEvent{Kind: KindCancel}, true
|
||||
events = append(events, ClickEvent{Kind: KindCancel})
|
||||
}
|
||||
case pointer.Press:
|
||||
if c.pressed {
|
||||
@@ -235,7 +224,7 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
|
||||
c.clicks = 1
|
||||
}
|
||||
c.clickedAt = e.Time
|
||||
return ClickEvent{Kind: KindPress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks}, true
|
||||
events = append(events, ClickEvent{Kind: KindPress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
|
||||
case pointer.Leave:
|
||||
if !c.pressed {
|
||||
c.pid = e.PointerID
|
||||
@@ -253,16 +242,25 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return ClickEvent{}, false
|
||||
return events
|
||||
}
|
||||
|
||||
func (ClickEvent) ImplementsEvent() {}
|
||||
|
||||
// Add the handler to the operation list to receive scroll events.
|
||||
// The bounds variable refers to the scrolling boundaries
|
||||
// as defined in [pointer.Filter].
|
||||
func (s *Scroll) Add(ops *op.Ops) {
|
||||
event.Op(ops, s)
|
||||
// as defined in io/pointer.InputOp.
|
||||
func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) {
|
||||
oph := pointer.InputOp{
|
||||
Tag: s,
|
||||
Grab: s.grab,
|
||||
Kinds: 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.
|
||||
@@ -271,19 +269,13 @@ func (s *Scroll) Stop() {
|
||||
}
|
||||
|
||||
// Update state and report the scroll distance along axis.
|
||||
func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, scrollx, scrolly pointer.ScrollRange) int {
|
||||
total := 0
|
||||
f := pointer.Filter{
|
||||
Target: s,
|
||||
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
|
||||
ScrollX: scrollx,
|
||||
ScrollY: scrolly,
|
||||
func (s *Scroll) Update(cfg unit.Metric, q event.Queue, t time.Time, axis Axis) int {
|
||||
if s.axis != axis {
|
||||
s.axis = axis
|
||||
return 0
|
||||
}
|
||||
for {
|
||||
evt, ok := q.Event(f)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
total := 0
|
||||
for _, evt := range q.Events(s) {
|
||||
e, ok := evt.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
@@ -300,7 +292,7 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis,
|
||||
}
|
||||
s.Stop()
|
||||
s.estimator = fling.Extrapolation{}
|
||||
v := s.val(axis, e.Position)
|
||||
v := s.val(e.Position)
|
||||
s.last = int(math.Round(float64(v)))
|
||||
s.estimator.Sample(e.Time, v)
|
||||
s.dragging = true
|
||||
@@ -316,8 +308,9 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis,
|
||||
fallthrough
|
||||
case pointer.Cancel:
|
||||
s.dragging = false
|
||||
s.grab = false
|
||||
case pointer.Scroll:
|
||||
switch axis {
|
||||
switch s.axis {
|
||||
case Horizontal:
|
||||
s.scroll += e.Scroll.X
|
||||
case Vertical:
|
||||
@@ -330,14 +323,14 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis,
|
||||
if !s.dragging || s.pid != e.PointerID {
|
||||
continue
|
||||
}
|
||||
val := s.val(axis, e.Position)
|
||||
val := s.val(e.Position)
|
||||
s.estimator.Sample(e.Time, val)
|
||||
v := int(math.Round(float64(val)))
|
||||
dist := s.last - v
|
||||
if e.Priority < pointer.Grabbed {
|
||||
slop := cfg.Dp(touchSlop)
|
||||
if dist := dist; dist >= slop || -slop >= dist {
|
||||
q.Execute(pointer.GrabCmd{Tag: s, ID: e.PointerID})
|
||||
s.grab = true
|
||||
}
|
||||
} else {
|
||||
s.last = v
|
||||
@@ -346,14 +339,11 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis,
|
||||
}
|
||||
}
|
||||
total += s.flinger.Tick(t)
|
||||
if s.flinger.Active() {
|
||||
q.Execute(op.InvalidateCmd{})
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func (s *Scroll) val(axis Axis, p f32.Point) float32 {
|
||||
if axis == Horizontal {
|
||||
func (s *Scroll) val(p f32.Point) float32 {
|
||||
if s.axis == Horizontal {
|
||||
return p.X
|
||||
} else {
|
||||
return p.Y
|
||||
@@ -374,20 +364,18 @@ func (s *Scroll) State() ScrollState {
|
||||
|
||||
// Add the handler to the operation list to receive drag events.
|
||||
func (d *Drag) Add(ops *op.Ops) {
|
||||
event.Op(ops, d)
|
||||
pointer.InputOp{
|
||||
Tag: d,
|
||||
Grab: d.grab,
|
||||
Kinds: pointer.Press | pointer.Drag | pointer.Release,
|
||||
}.Add(ops)
|
||||
}
|
||||
|
||||
// Update state and return the next drag event, if any.
|
||||
func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event, bool) {
|
||||
for {
|
||||
ev, ok := q.Event(pointer.Filter{
|
||||
Target: d,
|
||||
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel,
|
||||
})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
e, ok := ev.(pointer.Event)
|
||||
// Update state and return the drag events.
|
||||
func (d *Drag) Update(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event {
|
||||
var events []pointer.Event
|
||||
for _, e := range q.Events(d) {
|
||||
e, ok := e.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
@@ -420,7 +408,7 @@ func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event
|
||||
diff := e.Position.Sub(d.start)
|
||||
slop := cfg.Dp(touchSlop)
|
||||
if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
|
||||
q.Execute(pointer.GrabCmd{Tag: d, ID: e.PointerID})
|
||||
d.grab = true
|
||||
}
|
||||
}
|
||||
case pointer.Release, pointer.Cancel:
|
||||
@@ -429,12 +417,13 @@ func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event
|
||||
continue
|
||||
}
|
||||
d.dragging = false
|
||||
d.grab = false
|
||||
}
|
||||
|
||||
return e, true
|
||||
events = append(events, e)
|
||||
}
|
||||
|
||||
return pointer.Event{}, false
|
||||
return events
|
||||
}
|
||||
|
||||
// Dragging reports whether it is currently in use.
|
||||
|
||||
+17
-17
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/router"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
)
|
||||
@@ -22,21 +22,20 @@ func TestHover(t *testing.T) {
|
||||
stack := clip.Rect(rect).Push(ops)
|
||||
h.Add(ops)
|
||||
stack.Pop()
|
||||
r := new(input.Router)
|
||||
h.Update(r.Source())
|
||||
r := new(router.Router)
|
||||
r.Frame(ops)
|
||||
|
||||
r.Queue(
|
||||
pointer.Event{Kind: pointer.Move, Position: f32.Pt(30, 30)},
|
||||
)
|
||||
if !h.Update(r.Source()) {
|
||||
if !h.Update(r) {
|
||||
t.Fatal("expected hovered")
|
||||
}
|
||||
|
||||
r.Queue(
|
||||
pointer.Event{Kind: pointer.Move, Position: f32.Pt(50, 50)},
|
||||
)
|
||||
if h.Update(r.Source()) {
|
||||
if h.Update(r) {
|
||||
t.Fatal("expected not hovered")
|
||||
}
|
||||
}
|
||||
@@ -72,21 +71,12 @@ func TestMouseClicks(t *testing.T) {
|
||||
var ops op.Ops
|
||||
click.Add(&ops)
|
||||
|
||||
var r input.Router
|
||||
click.Update(r.Source())
|
||||
var r router.Router
|
||||
r.Frame(&ops)
|
||||
r.Queue(tc.events...)
|
||||
|
||||
var clicks []ClickEvent
|
||||
for {
|
||||
ev, ok := click.Update(r.Source())
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if ev.Kind == KindClick {
|
||||
clicks = append(clicks, ev)
|
||||
}
|
||||
}
|
||||
events := click.Update(&r)
|
||||
clicks := filterMouseClicks(events)
|
||||
if got, want := len(clicks), len(tc.clicks); got != want {
|
||||
t.Fatalf("got %d mouse clicks, expected %d", got, want)
|
||||
}
|
||||
@@ -116,3 +106,13 @@ func mouseClickEvents(times ...time.Duration) []event.Event {
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func filterMouseClicks(events []ClickEvent) []ClickEvent {
|
||||
var clicks []ClickEvent
|
||||
for _, ev := range events {
|
||||
if ev.Kind == KindClick {
|
||||
clicks = append(clicks, ev)
|
||||
}
|
||||
}
|
||||
return clicks
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
module gioui.org
|
||||
|
||||
go 1.21
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
|
||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
|
||||
gioui.org/shader v1.0.8
|
||||
github.com/go-text/typesetting v0.1.1
|
||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372
|
||||
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
|
||||
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
|
||||
golang.org/x/image v0.5.0
|
||||
golang.org/x/sys v0.5.0
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64
|
||||
)
|
||||
|
||||
require golang.org/x/text v0.9.0
|
||||
require golang.org/x/text v0.7.0
|
||||
|
||||
@@ -5,10 +5,9 @@ gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJG
|
||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
|
||||
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
||||
github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
|
||||
github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
|
||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
|
||||
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=
|
||||
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=
|
||||
@@ -29,16 +28,15 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
||||
+10
-15
@@ -8,13 +8,8 @@ import (
|
||||
"gioui.org/internal/f32"
|
||||
)
|
||||
|
||||
type textureCacheKey struct {
|
||||
filter byte
|
||||
handle any
|
||||
}
|
||||
|
||||
type textureCache struct {
|
||||
res map[textureCacheKey]resourceCacheValue
|
||||
type resourceCache struct {
|
||||
res map[interface{}]resourceCacheValue
|
||||
}
|
||||
|
||||
type resourceCacheValue struct {
|
||||
@@ -42,13 +37,13 @@ type opCacheValue struct {
|
||||
keep bool
|
||||
}
|
||||
|
||||
func newTextureCache() *textureCache {
|
||||
return &textureCache{
|
||||
res: make(map[textureCacheKey]resourceCacheValue),
|
||||
func newResourceCache() *resourceCache {
|
||||
return &resourceCache{
|
||||
res: make(map[interface{}]resourceCacheValue),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *textureCache) get(key textureCacheKey) (resource, bool) {
|
||||
func (r *resourceCache) get(key interface{}) (resource, bool) {
|
||||
v, exists := r.res[key]
|
||||
if !exists {
|
||||
return nil, false
|
||||
@@ -60,17 +55,17 @@ func (r *textureCache) get(key textureCacheKey) (resource, bool) {
|
||||
return v.resource, exists
|
||||
}
|
||||
|
||||
func (r *textureCache) put(key textureCacheKey, val resource) {
|
||||
func (r *resourceCache) put(key interface{}, val resource) {
|
||||
v, exists := r.res[key]
|
||||
if exists && v.used {
|
||||
panic(fmt.Errorf("key exists, %v", key))
|
||||
panic(fmt.Errorf("key exists, %p", key))
|
||||
}
|
||||
v.used = true
|
||||
v.resource = val
|
||||
r.res[key] = v
|
||||
}
|
||||
|
||||
func (r *textureCache) frame() {
|
||||
func (r *resourceCache) frame() {
|
||||
for k, v := range r.res {
|
||||
if v.used {
|
||||
v.used = false
|
||||
@@ -82,7 +77,7 @@ func (r *textureCache) frame() {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *textureCache) release() {
|
||||
func (r *resourceCache) release() {
|
||||
for _, v := range r.res {
|
||||
v.resource.release()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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() {}
|
||||
+12
-3
@@ -93,6 +93,7 @@ type compute struct {
|
||||
}
|
||||
}
|
||||
timers struct {
|
||||
profile string
|
||||
t *timers
|
||||
compact *timer
|
||||
render *timer
|
||||
@@ -175,6 +176,7 @@ type materialUniforms struct {
|
||||
|
||||
type collector struct {
|
||||
hasher maphash.Hash
|
||||
profile bool
|
||||
reader ops.Reader
|
||||
states []f32.Affine2D
|
||||
clear bool
|
||||
@@ -595,7 +597,7 @@ func (g *compute) frame(target RenderTarget) error {
|
||||
defer g.ctx.EndFrame()
|
||||
|
||||
t := &g.timers
|
||||
if false && t.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
|
||||
if g.collector.profile && t.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
|
||||
t.t = newTimers(g.ctx)
|
||||
t.compact = t.t.newTimer()
|
||||
t.render = t.t.newTimer()
|
||||
@@ -629,13 +631,13 @@ func (g *compute) frame(target RenderTarget) error {
|
||||
return err
|
||||
}
|
||||
t.compact.end()
|
||||
if false && t.t.ready() {
|
||||
if g.collector.profile && t.t.ready() {
|
||||
com, ren, blit := t.compact.Elapsed, t.render.Elapsed, t.blit.Elapsed
|
||||
ft := com + ren + blit
|
||||
q := 100 * time.Microsecond
|
||||
ft = ft.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
|
||||
}
|
||||
@@ -659,6 +661,10 @@ func (g *compute) dumpAtlases() {
|
||||
}
|
||||
}
|
||||
|
||||
func (g *compute) Profile() string {
|
||||
return g.timers.profile
|
||||
}
|
||||
|
||||
func (g *compute) compactAllocs() error {
|
||||
const (
|
||||
maxAllocAge = 3
|
||||
@@ -1650,6 +1656,7 @@ func (e *encoder) line(start, end f32.Point) {
|
||||
|
||||
func (c *collector) reset() {
|
||||
c.prevFrame, c.frame = c.frame, c.prevFrame
|
||||
c.profile = false
|
||||
c.clipStates = c.clipStates[:0]
|
||||
c.transStack = c.transStack[:0]
|
||||
c.frame.reset()
|
||||
@@ -1729,6 +1736,8 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur
|
||||
c.addClip(&state, fview, fview, nil, ops.Key{}, 0, 0, false)
|
||||
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
|
||||
switch ops.OpType(encOp.Data[0]) {
|
||||
case ops.TypeProfile:
|
||||
c.profile = true
|
||||
case ops.TypeTransform:
|
||||
dop, push := ops.DecodeTransform(encOp.Data)
|
||||
if push {
|
||||
|
||||
+114
-112
@@ -44,10 +44,14 @@ type GPU interface {
|
||||
Clear(color color.NRGBA)
|
||||
// Frame draws the graphics operations from op into a viewport of target.
|
||||
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 {
|
||||
cache *textureCache
|
||||
cache *resourceCache
|
||||
|
||||
profile string
|
||||
timers *timers
|
||||
@@ -69,6 +73,7 @@ type renderer struct {
|
||||
}
|
||||
|
||||
type drawOps struct {
|
||||
profile bool
|
||||
reader ops.Reader
|
||||
states []f32.Affine2D
|
||||
transStack []f32.Affine2D
|
||||
@@ -261,7 +266,7 @@ type texture struct {
|
||||
type blitter struct {
|
||||
ctx driver.Device
|
||||
viewport image.Point
|
||||
pipelines [2][3]*pipeline
|
||||
pipelines [3]*pipeline
|
||||
colUniforms *blitColUniforms
|
||||
texUniforms *blitTexUniforms
|
||||
linearGradientUniforms *blitLinearGradientUniforms
|
||||
@@ -354,7 +359,7 @@ func NewWithDevice(d driver.Device) (GPU, error) {
|
||||
|
||||
func newGPU(ctx driver.Device) (*gpu, error) {
|
||||
g := &gpu{
|
||||
cache: newTextureCache(),
|
||||
cache: newResourceCache(),
|
||||
}
|
||||
g.drawOps.pathCache = newOpCache()
|
||||
if err := g.init(ctx); err != nil {
|
||||
@@ -394,7 +399,7 @@ func (g *gpu) collect(viewport image.Point, frameOps *op.Ops) {
|
||||
g.renderer.pather.viewport = viewport
|
||||
g.drawOps.reset(viewport)
|
||||
g.drawOps.collect(frameOps, viewport)
|
||||
if false && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
|
||||
if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
|
||||
g.frameStart = time.Now()
|
||||
g.timers = newTimers(g.ctx)
|
||||
g.stencilTimer = g.timers.newTimer()
|
||||
@@ -420,9 +425,9 @@ func (g *gpu) frame(target RenderTarget) error {
|
||||
g.stencilTimer.end()
|
||||
g.coverTimer.begin()
|
||||
g.renderer.uploadImages(g.cache, g.drawOps.imageOps)
|
||||
g.renderer.prepareDrawOps(g.drawOps.imageOps)
|
||||
g.renderer.prepareDrawOps(g.cache, g.drawOps.imageOps)
|
||||
g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers)
|
||||
g.renderer.drawLayers(g.drawOps.layers, g.drawOps.imageOps)
|
||||
g.renderer.drawLayers(g.cache, g.drawOps.layers, g.drawOps.imageOps)
|
||||
d := driver.LoadDesc{
|
||||
ClearColor: g.drawOps.clearColor,
|
||||
}
|
||||
@@ -432,14 +437,14 @@ func (g *gpu) frame(target RenderTarget) error {
|
||||
}
|
||||
g.ctx.BeginRenderPass(defFBO, d)
|
||||
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
|
||||
g.renderer.drawOps(false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps)
|
||||
g.renderer.drawOps(g.cache, false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps)
|
||||
g.coverTimer.end()
|
||||
g.ctx.EndRenderPass()
|
||||
g.cleanupTimer.begin()
|
||||
g.cache.frame()
|
||||
g.drawOps.pathCache.frame()
|
||||
g.cleanupTimer.end()
|
||||
if false && g.timers.ready() {
|
||||
if g.drawOps.profile && g.timers.ready() {
|
||||
st, covt, cleant := g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
|
||||
ft := st + covt + cleant
|
||||
q := 100 * time.Microsecond
|
||||
@@ -455,8 +460,12 @@ func (g *gpu) Profile() string {
|
||||
return g.profile
|
||||
}
|
||||
|
||||
func (r *renderer) texHandle(cache *textureCache, data imageOpData) driver.Texture {
|
||||
key := textureCacheKey{
|
||||
func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture {
|
||||
type cachekey struct {
|
||||
filter byte
|
||||
handle any
|
||||
}
|
||||
key := cachekey{
|
||||
filter: data.filter,
|
||||
handle: data.handle,
|
||||
}
|
||||
@@ -560,24 +569,12 @@ func newBlitter(ctx driver.Device) *blitter {
|
||||
func (b *blitter) release() {
|
||||
b.quadVerts.Release()
|
||||
for _, p := range b.pipelines {
|
||||
for _, p := range p {
|
||||
p.Release()
|
||||
}
|
||||
p.Release()
|
||||
}
|
||||
}
|
||||
|
||||
func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]interface{}) (pipelines [2][3]*pipeline, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
for _, p := range pipelines {
|
||||
for _, p := range p {
|
||||
if p != nil {
|
||||
p.Release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]interface{}) ([3]*pipeline, error) {
|
||||
var pipelines [3]*pipeline
|
||||
blend := driver.BlendDesc{
|
||||
Enable: true,
|
||||
SrcFactor: driver.BlendFactorOne,
|
||||
@@ -595,76 +592,86 @@ func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.
|
||||
return pipelines, err
|
||||
}
|
||||
defer vsh.Release()
|
||||
for i, format := range []driver.TextureFormat{driver.TextureFormatOutput, driver.TextureFormatSRGBA} {
|
||||
{
|
||||
fsh, err := b.NewFragmentShader(fsSrc[materialTexture])
|
||||
if err != nil {
|
||||
return pipelines, err
|
||||
}
|
||||
defer fsh.Release()
|
||||
pipe, err := b.NewPipeline(driver.PipelineDesc{
|
||||
VertexShader: vsh,
|
||||
FragmentShader: fsh,
|
||||
BlendDesc: blend,
|
||||
VertexLayout: layout,
|
||||
PixelFormat: format,
|
||||
Topology: driver.TopologyTriangleStrip,
|
||||
})
|
||||
if err != nil {
|
||||
return pipelines, err
|
||||
}
|
||||
var vertBuffer *uniformBuffer
|
||||
if u := uniforms[materialTexture]; u != nil {
|
||||
vertBuffer = newUniformBuffer(b, u)
|
||||
}
|
||||
pipelines[i][materialTexture] = &pipeline{pipe, vertBuffer}
|
||||
{
|
||||
fsh, err := b.NewFragmentShader(fsSrc[materialTexture])
|
||||
if err != nil {
|
||||
return pipelines, err
|
||||
}
|
||||
{
|
||||
var vertBuffer *uniformBuffer
|
||||
fsh, err := b.NewFragmentShader(fsSrc[materialColor])
|
||||
if err != nil {
|
||||
return pipelines, err
|
||||
}
|
||||
defer fsh.Release()
|
||||
pipe, err := b.NewPipeline(driver.PipelineDesc{
|
||||
VertexShader: vsh,
|
||||
FragmentShader: fsh,
|
||||
BlendDesc: blend,
|
||||
VertexLayout: layout,
|
||||
PixelFormat: format,
|
||||
Topology: driver.TopologyTriangleStrip,
|
||||
})
|
||||
if err != nil {
|
||||
return pipelines, err
|
||||
}
|
||||
if u := uniforms[materialColor]; u != nil {
|
||||
vertBuffer = newUniformBuffer(b, u)
|
||||
}
|
||||
pipelines[i][materialColor] = &pipeline{pipe, vertBuffer}
|
||||
defer fsh.Release()
|
||||
pipe, err := b.NewPipeline(driver.PipelineDesc{
|
||||
VertexShader: vsh,
|
||||
FragmentShader: fsh,
|
||||
BlendDesc: blend,
|
||||
VertexLayout: layout,
|
||||
PixelFormat: driver.TextureFormatOutput,
|
||||
Topology: driver.TopologyTriangleStrip,
|
||||
})
|
||||
if err != nil {
|
||||
return pipelines, err
|
||||
}
|
||||
{
|
||||
var vertBuffer *uniformBuffer
|
||||
fsh, err := b.NewFragmentShader(fsSrc[materialLinearGradient])
|
||||
if err != nil {
|
||||
return pipelines, err
|
||||
}
|
||||
defer fsh.Release()
|
||||
pipe, err := b.NewPipeline(driver.PipelineDesc{
|
||||
VertexShader: vsh,
|
||||
FragmentShader: fsh,
|
||||
BlendDesc: blend,
|
||||
VertexLayout: layout,
|
||||
PixelFormat: format,
|
||||
Topology: driver.TopologyTriangleStrip,
|
||||
})
|
||||
if err != nil {
|
||||
return pipelines, err
|
||||
}
|
||||
if u := uniforms[materialLinearGradient]; u != nil {
|
||||
vertBuffer = newUniformBuffer(b, u)
|
||||
}
|
||||
pipelines[i][materialLinearGradient] = &pipeline{pipe, vertBuffer}
|
||||
var vertBuffer *uniformBuffer
|
||||
if u := uniforms[materialTexture]; u != nil {
|
||||
vertBuffer = newUniformBuffer(b, u)
|
||||
}
|
||||
pipelines[materialTexture] = &pipeline{pipe, vertBuffer}
|
||||
}
|
||||
{
|
||||
var vertBuffer *uniformBuffer
|
||||
fsh, err := b.NewFragmentShader(fsSrc[materialColor])
|
||||
if err != nil {
|
||||
pipelines[materialTexture].Release()
|
||||
return pipelines, err
|
||||
}
|
||||
defer fsh.Release()
|
||||
pipe, err := b.NewPipeline(driver.PipelineDesc{
|
||||
VertexShader: vsh,
|
||||
FragmentShader: fsh,
|
||||
BlendDesc: blend,
|
||||
VertexLayout: layout,
|
||||
PixelFormat: driver.TextureFormatOutput,
|
||||
Topology: driver.TopologyTriangleStrip,
|
||||
})
|
||||
if err != nil {
|
||||
pipelines[materialTexture].Release()
|
||||
return pipelines, err
|
||||
}
|
||||
if u := uniforms[materialColor]; u != nil {
|
||||
vertBuffer = newUniformBuffer(b, u)
|
||||
}
|
||||
pipelines[materialColor] = &pipeline{pipe, vertBuffer}
|
||||
}
|
||||
{
|
||||
var vertBuffer *uniformBuffer
|
||||
fsh, err := b.NewFragmentShader(fsSrc[materialLinearGradient])
|
||||
if err != nil {
|
||||
pipelines[materialTexture].Release()
|
||||
pipelines[materialColor].Release()
|
||||
return pipelines, err
|
||||
}
|
||||
defer fsh.Release()
|
||||
pipe, err := b.NewPipeline(driver.PipelineDesc{
|
||||
VertexShader: vsh,
|
||||
FragmentShader: fsh,
|
||||
BlendDesc: blend,
|
||||
VertexLayout: layout,
|
||||
PixelFormat: driver.TextureFormatOutput,
|
||||
Topology: driver.TopologyTriangleStrip,
|
||||
})
|
||||
if err != nil {
|
||||
pipelines[materialTexture].Release()
|
||||
pipelines[materialColor].Release()
|
||||
return pipelines, err
|
||||
}
|
||||
if u := uniforms[materialLinearGradient]; u != nil {
|
||||
vertBuffer = newUniformBuffer(b, u)
|
||||
}
|
||||
pipelines[materialLinearGradient] = &pipeline{pipe, vertBuffer}
|
||||
}
|
||||
if err != nil {
|
||||
for _, p := range pipelines {
|
||||
p.Release()
|
||||
}
|
||||
return pipelines, err
|
||||
}
|
||||
return pipelines, nil
|
||||
}
|
||||
@@ -846,7 +853,7 @@ func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer {
|
||||
return layers
|
||||
}
|
||||
|
||||
func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
|
||||
func (r *renderer) drawLayers(cache *resourceCache, layers []opacityLayer, ops []imageOp) {
|
||||
if len(r.layers.sizes) == 0 {
|
||||
return
|
||||
}
|
||||
@@ -867,9 +874,9 @@ func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
|
||||
Min: l.place.Pos,
|
||||
Max: l.place.Pos.Add(l.clip.Size()),
|
||||
}
|
||||
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Dx(), v.Dy())
|
||||
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])
|
||||
r.drawOps(cache, true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
|
||||
sr := f32.FRect(v)
|
||||
uvScale, uvOffset := texSpaceTransform(sr, f.size)
|
||||
uvTrans := f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset)
|
||||
@@ -892,6 +899,7 @@ func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
|
||||
}
|
||||
|
||||
func (d *drawOps) reset(viewport image.Point) {
|
||||
d.profile = false
|
||||
d.viewport = viewport
|
||||
d.imageOps = d.imageOps[:0]
|
||||
d.pathOps = d.pathOps[:0]
|
||||
@@ -932,7 +940,7 @@ func (d *drawOps) newPathOp() *pathOp {
|
||||
return &d.pathOpCache[len(d.pathOpCache)-1]
|
||||
}
|
||||
|
||||
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds f32.Rectangle, off f32.Point) {
|
||||
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds f32.Rectangle, off f32.Point, push bool) {
|
||||
npath := d.newPathOp()
|
||||
*npath = pathOp{
|
||||
parent: state.cpath,
|
||||
@@ -985,6 +993,8 @@ func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) {
|
||||
loop:
|
||||
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
|
||||
switch ops.OpType(encOp.Data[0]) {
|
||||
case ops.TypeProfile:
|
||||
d.profile = true
|
||||
case ops.TypeTransform:
|
||||
dop, push := ops.DecodeTransform(encOp.Data)
|
||||
if push {
|
||||
@@ -1057,7 +1067,7 @@ loop:
|
||||
quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans)
|
||||
quads.key = opKey{Key: encOp.Key}
|
||||
}
|
||||
d.addClipPath(&state, quads.aux, quads.key, bounds, off)
|
||||
d.addClipPath(&state, quads.aux, quads.key, bounds, off, true)
|
||||
quads = quadsOp{}
|
||||
case ops.TypePopClip:
|
||||
state.cpath = state.cpath.parent
|
||||
@@ -1102,7 +1112,7 @@ loop:
|
||||
// this transformed rectangle.
|
||||
k := opKey{Key: encOp.Key}
|
||||
k.SetTransform(t) // TODO: This call has no effect.
|
||||
d.addClipPath(&state, clipData, k, bnd, off)
|
||||
d.addClipPath(&state, clipData, k, bnd, off, false)
|
||||
}
|
||||
|
||||
bounds := cl.Round()
|
||||
@@ -1201,7 +1211,7 @@ func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32
|
||||
return m
|
||||
}
|
||||
|
||||
func (r *renderer) uploadImages(cache *textureCache, ops []imageOp) {
|
||||
func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) {
|
||||
for i := range ops {
|
||||
img := &ops[i]
|
||||
m := img.material
|
||||
@@ -1211,7 +1221,7 @@ func (r *renderer) uploadImages(cache *textureCache, ops []imageOp) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *renderer) prepareDrawOps(ops []imageOp) {
|
||||
func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) {
|
||||
for _, img := range ops {
|
||||
m := img.material
|
||||
switch m.material {
|
||||
@@ -1232,7 +1242,7 @@ func (r *renderer) prepareDrawOps(ops []imageOp) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageOp) {
|
||||
func (r *renderer) drawOps(cache *resourceCache, isFBO bool, opOff image.Point, viewport image.Point, ops []imageOp) {
|
||||
var coverTex driver.Texture
|
||||
for i := 0; i < len(ops); i++ {
|
||||
img := ops[i]
|
||||
@@ -1246,13 +1256,9 @@ func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageO
|
||||
|
||||
scale, off := clipSpaceTransform(drc, viewport)
|
||||
var fbo FBO
|
||||
fboIdx := 0
|
||||
if isFBO {
|
||||
fboIdx = 1
|
||||
}
|
||||
switch img.clipType {
|
||||
case clipTypeNone:
|
||||
p := r.blitter.pipelines[fboIdx][m.material]
|
||||
p := r.blitter.pipelines[m.material]
|
||||
r.ctx.BindPipeline(p.pipeline)
|
||||
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
||||
r.blitter.blit(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.opacity, m.uvTrans)
|
||||
@@ -1271,7 +1277,7 @@ func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageO
|
||||
Max: img.place.Pos.Add(drc.Size()),
|
||||
}
|
||||
coverScale, coverOff := texSpaceTransform(f32.FRect(uv), fbo.size)
|
||||
p := r.pather.coverer.pipelines[fboIdx][m.material]
|
||||
p := r.pather.coverer.pipelines[m.material]
|
||||
r.ctx.BindPipeline(p.pipeline)
|
||||
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
||||
r.pather.cover(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
|
||||
@@ -1279,11 +1285,7 @@ func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageO
|
||||
}
|
||||
|
||||
func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, opacity float32, uvTrans f32.Affine2D) {
|
||||
fboIdx := 0
|
||||
if fbo {
|
||||
fboIdx = 1
|
||||
}
|
||||
p := b.pipelines[fboIdx][mat]
|
||||
p := b.pipelines[mat]
|
||||
b.ctx.BindPipeline(p.pipeline)
|
||||
var uniforms *blitUniforms
|
||||
switch mat {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 334 B |
@@ -483,14 +483,14 @@ func TestGapsInPath(t *testing.T) {
|
||||
func TestOpacity(t *testing.T) {
|
||||
run(t, func(ops *op.Ops) {
|
||||
opc1 := paint.PushOpacity(ops, .3)
|
||||
// Fill screen to exercise the glClear optimization.
|
||||
// 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{B: 255, A: 255}, clip.Ellipse(image.Rectangle{Min: image.Pt(20+20, 10), Max: image.Pt(50+64, 128)}).Op(ops))
|
||||
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) {
|
||||
})
|
||||
|
||||
+3
-9
@@ -30,7 +30,7 @@ type pather struct {
|
||||
|
||||
type coverer struct {
|
||||
ctx driver.Device
|
||||
pipelines [2][3]*pipeline
|
||||
pipelines [3]*pipeline
|
||||
texUniforms *coverTexUniforms
|
||||
colUniforms *coverColUniforms
|
||||
linearGradientUniforms *coverLinearGradientUniforms
|
||||
@@ -309,9 +309,7 @@ func (p *pather) release() {
|
||||
|
||||
func (c *coverer) release() {
|
||||
for _, p := range c.pipelines {
|
||||
for _, p := range p {
|
||||
p.Release()
|
||||
}
|
||||
p.Release()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,11 +405,7 @@ func (c *coverer) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, c
|
||||
}
|
||||
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
|
||||
uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
|
||||
fboIdx := 0
|
||||
if isFBO {
|
||||
fboIdx = 1
|
||||
}
|
||||
c.pipelines[fboIdx][mat].UploadUniforms(c.ctx)
|
||||
c.pipelines[mat].UploadUniforms(c.ctx)
|
||||
c.ctx.DrawArrays(0, 4)
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -15,9 +15,10 @@ import (
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
disp _EGLDisplay
|
||||
eglCtx *eglContext
|
||||
eglSurf _EGLSurface
|
||||
disp _EGLDisplay
|
||||
eglCtx *eglContext
|
||||
eglSurf _EGLSurface
|
||||
width, height int
|
||||
}
|
||||
|
||||
type eglContext struct {
|
||||
@@ -120,9 +121,11 @@ func (c *Context) VisualID() int {
|
||||
return c.eglCtx.visualID
|
||||
}
|
||||
|
||||
func (c *Context) CreateSurface(win NativeWindowType) error {
|
||||
func (c *Context) CreateSurface(win NativeWindowType, width, height int) error {
|
||||
eglSurf, err := createSurface(c.disp, c.eglCtx, win)
|
||||
c.eglSurf = eglSurf
|
||||
c.width = width
|
||||
c.height = height
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -665,7 +665,7 @@ func (f *Functions) load(forceES bool) error {
|
||||
case runtime.GOOS == "android":
|
||||
libNames = []string{"libGLESv2.so", "libGLESv3.so"}
|
||||
default:
|
||||
libNames = []string{"libGLESv2.so.2", "libGLESv2.so.3.0"}
|
||||
libNames = []string{"libGLESv2.so.2"}
|
||||
}
|
||||
for _, lib := range libNames {
|
||||
if h := dlopen(lib); h != nil {
|
||||
|
||||
@@ -361,9 +361,6 @@ func (c *Functions) GetProgrami(p Program, pname Enum) int {
|
||||
}
|
||||
func (c *Functions) GetProgramInfoLog(p Program) string {
|
||||
n := c.GetProgrami(p, INFO_LOG_LENGTH)
|
||||
if n == 0 {
|
||||
return ""
|
||||
}
|
||||
buf := make([]byte, n)
|
||||
syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0)
|
||||
return string(buf)
|
||||
|
||||
+60
-10
@@ -55,19 +55,28 @@ const (
|
||||
TypePopTransform
|
||||
TypePushOpacity
|
||||
TypePopOpacity
|
||||
TypeInvalidate
|
||||
TypeImage
|
||||
TypePaint
|
||||
TypeColor
|
||||
TypeLinearGradient
|
||||
TypePass
|
||||
TypePopPass
|
||||
TypeInput
|
||||
TypeKeyInputHint
|
||||
TypePointerInput
|
||||
TypeClipboardRead
|
||||
TypeClipboardWrite
|
||||
TypeSource
|
||||
TypeTarget
|
||||
TypeOffer
|
||||
TypeKeyInput
|
||||
TypeKeyFocus
|
||||
TypeKeySoftKeyboard
|
||||
TypeSave
|
||||
TypeLoad
|
||||
TypeAux
|
||||
TypeClip
|
||||
TypePopClip
|
||||
TypeProfile
|
||||
TypeCursor
|
||||
TypePath
|
||||
TypeStroke
|
||||
@@ -76,6 +85,8 @@ const (
|
||||
TypeSemanticClass
|
||||
TypeSemanticSelected
|
||||
TypeSemanticEnabled
|
||||
TypeSnippet
|
||||
TypeSelection
|
||||
TypeActionInput
|
||||
)
|
||||
|
||||
@@ -137,13 +148,21 @@ const (
|
||||
TypeLinearGradientLen = 1 + 8*2 + 4*2
|
||||
TypePassLen = 1
|
||||
TypePopPassLen = 1
|
||||
TypeInputLen = 1
|
||||
TypeKeyInputHintLen = 1 + 1
|
||||
TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4
|
||||
TypeClipboardReadLen = 1
|
||||
TypeClipboardWriteLen = 1
|
||||
TypeSourceLen = 1
|
||||
TypeTargetLen = 1
|
||||
TypeOfferLen = 1
|
||||
TypeKeyInputLen = 1 + 1
|
||||
TypeKeyFocusLen = 1 + 1
|
||||
TypeKeySoftKeyboardLen = 1 + 1
|
||||
TypeSaveLen = 1 + 4
|
||||
TypeLoadLen = 1 + 4
|
||||
TypeAuxLen = 1
|
||||
TypeClipLen = 1 + 4*4 + 1 + 1
|
||||
TypePopClipLen = 1
|
||||
TypeProfileLen = 1
|
||||
TypeCursorLen = 2
|
||||
TypePathLen = 8 + 1
|
||||
TypeStrokeLen = 1 + 4
|
||||
@@ -152,6 +171,8 @@ const (
|
||||
TypeSemanticClassLen = 2
|
||||
TypeSemanticSelectedLen = 2
|
||||
TypeSemanticEnabledLen = 2
|
||||
TypeSnippetLen = 1 + 4 + 4
|
||||
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
|
||||
TypeActionInputLen = 1 + 1
|
||||
)
|
||||
|
||||
@@ -404,19 +425,28 @@ var opProps = [0x100]opProp{
|
||||
TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0},
|
||||
TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0},
|
||||
TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0},
|
||||
TypeInvalidate: {Size: TypeRedrawLen, NumRefs: 0},
|
||||
TypeImage: {Size: TypeImageLen, NumRefs: 2},
|
||||
TypePaint: {Size: TypePaintLen, NumRefs: 0},
|
||||
TypeColor: {Size: TypeColorLen, NumRefs: 0},
|
||||
TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0},
|
||||
TypePass: {Size: TypePassLen, NumRefs: 0},
|
||||
TypePopPass: {Size: TypePopPassLen, NumRefs: 0},
|
||||
TypeInput: {Size: TypeInputLen, NumRefs: 1},
|
||||
TypeKeyInputHint: {Size: TypeKeyInputHintLen, NumRefs: 1},
|
||||
TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1},
|
||||
TypeClipboardRead: {Size: TypeClipboardReadLen, 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},
|
||||
TypeLoad: {Size: TypeLoadLen, NumRefs: 0},
|
||||
TypeAux: {Size: TypeAuxLen, NumRefs: 0},
|
||||
TypeClip: {Size: TypeClipLen, NumRefs: 0},
|
||||
TypePopClip: {Size: TypePopClipLen, NumRefs: 0},
|
||||
TypeProfile: {Size: TypeProfileLen, NumRefs: 1},
|
||||
TypeCursor: {Size: TypeCursorLen, NumRefs: 0},
|
||||
TypePath: {Size: TypePathLen, NumRefs: 0},
|
||||
TypeStroke: {Size: TypeStrokeLen, NumRefs: 0},
|
||||
@@ -425,6 +455,8 @@ var opProps = [0x100]opProp{
|
||||
TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0},
|
||||
TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0},
|
||||
TypeSemanticEnabled: {Size: TypeSemanticEnabledLen, NumRefs: 0},
|
||||
TypeSnippet: {Size: TypeSnippetLen, NumRefs: 2},
|
||||
TypeSelection: {Size: TypeSelectionLen, NumRefs: 1},
|
||||
TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0},
|
||||
}
|
||||
|
||||
@@ -457,6 +489,8 @@ func (t OpType) String() string {
|
||||
return "PushOpacity"
|
||||
case TypePopOpacity:
|
||||
return "PopOpacity"
|
||||
case TypeInvalidate:
|
||||
return "Invalidate"
|
||||
case TypeImage:
|
||||
return "Image"
|
||||
case TypePaint:
|
||||
@@ -469,10 +503,24 @@ func (t OpType) String() string {
|
||||
return "Pass"
|
||||
case TypePopPass:
|
||||
return "PopPass"
|
||||
case TypeInput:
|
||||
return "Input"
|
||||
case TypeKeyInputHint:
|
||||
return "KeyInputHint"
|
||||
case TypePointerInput:
|
||||
return "PointerInput"
|
||||
case TypeClipboardRead:
|
||||
return "ClipboardRead"
|
||||
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:
|
||||
return "Save"
|
||||
case TypeLoad:
|
||||
@@ -483,6 +531,8 @@ func (t OpType) String() string {
|
||||
return "Clip"
|
||||
case TypePopClip:
|
||||
return "PopClip"
|
||||
case TypeProfile:
|
||||
return "Profile"
|
||||
case TypeCursor:
|
||||
return "Cursor"
|
||||
case TypePath:
|
||||
|
||||
@@ -327,9 +327,6 @@ func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
|
||||
func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) }
|
||||
|
||||
func normPt(p f32.Point, l float32) f32.Point {
|
||||
if (p.X == 0 && p.Y == l) || (p.Y == 0 && p.X == l) {
|
||||
return f32.Point{X: p.X, Y: p.Y}
|
||||
}
|
||||
d := math.Hypot(float64(p.X), float64(p.Y))
|
||||
l64 := float64(l)
|
||||
if math.Abs(d-l64) < 1e-10 {
|
||||
|
||||
+24
-11
@@ -3,22 +3,35 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"gioui.org/internal/ops"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/op"
|
||||
)
|
||||
|
||||
// WriteCmd copies Text to the clipboard.
|
||||
type WriteCmd struct {
|
||||
Type string
|
||||
Data io.ReadCloser
|
||||
// Event is generated when the clipboard content is requested.
|
||||
type Event struct {
|
||||
Text string
|
||||
}
|
||||
|
||||
// ReadCmd requests the text of the clipboard, delivered to
|
||||
// the handler through an [io/transfer.DataEvent].
|
||||
type ReadCmd struct {
|
||||
// ReadOp requests the text of the clipboard, delivered to
|
||||
// the current handler through an Event.
|
||||
type ReadOp struct {
|
||||
Tag event.Tag
|
||||
}
|
||||
|
||||
func (WriteCmd) ImplementsCommand() {}
|
||||
func (ReadCmd) ImplementsCommand() {}
|
||||
// WriteOp copies Text to the clipboard.
|
||||
type WriteOp struct {
|
||||
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.Write1String(&o.Internal, ops.TypeClipboardWriteLen, h.Text)
|
||||
data[0] = byte(ops.TypeClipboardWrite)
|
||||
}
|
||||
|
||||
func (Event) ImplementsEvent() {}
|
||||
|
||||
+34
-20
@@ -1,12 +1,41 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"gioui.org/internal/ops"
|
||||
"gioui.org/op"
|
||||
)
|
||||
// Queue maps an event handler key to the events
|
||||
// available to the handler.
|
||||
type Queue interface {
|
||||
// Events returns the available events for an
|
||||
// event handler tag.
|
||||
Events(t Tag) []Event
|
||||
}
|
||||
|
||||
// Tag is the stable identifier for an event handler.
|
||||
// For a handler h, the tag is typically &h.
|
||||
@@ -16,18 +45,3 @@ type Tag interface{}
|
||||
type Event interface {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// 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
@@ -1,364 +0,0 @@
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
// 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,
|
||||
ScrollX: pointer.ScrollRange{Min: -100, Max: +100},
|
||||
ScrollY: pointer.ScrollRange{Min: -100, Max: +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,881 +0,0 @@
|
||||
// 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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range q.changes {
|
||||
if q.deferring && i > 0 {
|
||||
break
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 _, ch := range q.changes[1 : idx+1] {
|
||||
first.events = append(first.events, ch.events...)
|
||||
}
|
||||
q.changes = append(q.changes[:1], q.changes[idx+1:]...)
|
||||
}
|
||||
|
||||
// Frame completes the current frame and starts a new with the
|
||||
// handlers from the frame argument. Remaining events are discarded,
|
||||
// unless they were deferred by a command.
|
||||
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() {}
|
||||
@@ -1,34 +0,0 @@
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
+219
-105
@@ -1,9 +1,18 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"gioui.org/f32"
|
||||
@@ -12,40 +21,59 @@ import (
|
||||
"gioui.org/op"
|
||||
)
|
||||
|
||||
// Filter matches any [Event] that matches the parameters.
|
||||
type Filter struct {
|
||||
// Focus is the tag that must be focused for the filter to match. It has no effect
|
||||
// if it is nil.
|
||||
Focus event.Tag
|
||||
// Required is the set of modifiers that must be included in events matched.
|
||||
Required Modifiers
|
||||
// Optional is the set of modifiers that may be included in events matched.
|
||||
Optional Modifiers
|
||||
// Name of the key to be matched. As a special case, the empty
|
||||
// Name matches every key not matched by any other filter.
|
||||
Name Name
|
||||
}
|
||||
|
||||
// InputHintOp describes the type of text expected by a tag.
|
||||
type InputHintOp struct {
|
||||
Tag event.Tag
|
||||
// InputOp declares a handler ready for key events.
|
||||
// Key events are in general only delivered to the
|
||||
// focused key handler.
|
||||
type InputOp struct {
|
||||
Tag event.Tag
|
||||
// Hint describes the type of text expected by Tag.
|
||||
Hint InputHint
|
||||
// Keys is the set of keys Tag can handle. That is, Tag will only
|
||||
// receive an Event if its key and modifiers are accepted by Keys.Contains.
|
||||
// As a special case, the topmost (first added) InputOp handler receives all
|
||||
// unhandled events.
|
||||
Keys Set
|
||||
}
|
||||
|
||||
// SoftKeyboardCmd shows or hides the on-screen keyboard, if available.
|
||||
type SoftKeyboardCmd struct {
|
||||
// Set is an expression that describes a set of key combinations, in the form
|
||||
// "<modifiers>-<keyset>|...". Modifiers are separated by dashes, optional
|
||||
// modifiers are enclosed by parentheses. A key set is either a literal key
|
||||
// name or a list of key names separated by commas and enclosed in brackets.
|
||||
//
|
||||
// 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.
|
||||
// It replaces any previous SoftKeyboardOp.
|
||||
type SoftKeyboardOp struct {
|
||||
Show bool
|
||||
}
|
||||
|
||||
// SelectionCmd updates the selection for an input handler.
|
||||
type SelectionCmd struct {
|
||||
// FocusOp sets or clears the keyboard focus. It replaces any previous
|
||||
// FocusOp in the same frame.
|
||||
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
|
||||
Range
|
||||
Caret
|
||||
}
|
||||
|
||||
// SnippetCmd updates the content snippet for an input handler.
|
||||
type SnippetCmd struct {
|
||||
// SnippetOp updates the content snippet for an input handler.
|
||||
type SnippetOp struct {
|
||||
Tag event.Tag
|
||||
Snippet
|
||||
}
|
||||
@@ -90,8 +118,11 @@ type FocusEvent struct {
|
||||
// An Event is generated when a key is pressed. For text input
|
||||
// use EditEvent.
|
||||
type Event struct {
|
||||
// Name of the key.
|
||||
Name Name
|
||||
// Name of the 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.
|
||||
Name string
|
||||
// Modifiers is the set of active modifiers when the key was pressed.
|
||||
Modifiers Modifiers
|
||||
// State is the state of the key when the event was fired.
|
||||
@@ -105,13 +136,6 @@ type EditEvent struct {
|
||||
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
|
||||
// type of data that might be entered by the user.
|
||||
type InputHint uint8
|
||||
@@ -165,60 +189,41 @@ const (
|
||||
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 (
|
||||
// Names for special keys.
|
||||
NameLeftArrow Name = "←"
|
||||
NameRightArrow Name = "→"
|
||||
NameUpArrow Name = "↑"
|
||||
NameDownArrow Name = "↓"
|
||||
NameReturn Name = "⏎"
|
||||
NameEnter Name = "⌤"
|
||||
NameEscape Name = "⎋"
|
||||
NameHome Name = "⇱"
|
||||
NameEnd Name = "⇲"
|
||||
NameDeleteBackward Name = "⌫"
|
||||
NameDeleteForward Name = "⌦"
|
||||
NamePageUp Name = "⇞"
|
||||
NamePageDown Name = "⇟"
|
||||
NameTab Name = "Tab"
|
||||
NameSpace Name = "Space"
|
||||
NameCtrl Name = "Ctrl"
|
||||
NameShift Name = "Shift"
|
||||
NameAlt Name = "Alt"
|
||||
NameSuper Name = "Super"
|
||||
NameCommand Name = "⌘"
|
||||
NameF1 Name = "F1"
|
||||
NameF2 Name = "F2"
|
||||
NameF3 Name = "F3"
|
||||
NameF4 Name = "F4"
|
||||
NameF5 Name = "F5"
|
||||
NameF6 Name = "F6"
|
||||
NameF7 Name = "F7"
|
||||
NameF8 Name = "F8"
|
||||
NameF9 Name = "F9"
|
||||
NameF10 Name = "F10"
|
||||
NameF11 Name = "F11"
|
||||
NameF12 Name = "F12"
|
||||
NameBack Name = "Back"
|
||||
)
|
||||
|
||||
type FocusDirection int
|
||||
|
||||
const (
|
||||
FocusRight FocusDirection = iota
|
||||
FocusLeft
|
||||
FocusUp
|
||||
FocusDown
|
||||
FocusForward
|
||||
FocusBackward
|
||||
NameLeftArrow = "←"
|
||||
NameRightArrow = "→"
|
||||
NameUpArrow = "↑"
|
||||
NameDownArrow = "↓"
|
||||
NameReturn = "⏎"
|
||||
NameEnter = "⌤"
|
||||
NameEscape = "⎋"
|
||||
NameHome = "⇱"
|
||||
NameEnd = "⇲"
|
||||
NameDeleteBackward = "⌫"
|
||||
NameDeleteForward = "⌦"
|
||||
NamePageUp = "⇞"
|
||||
NamePageDown = "⇟"
|
||||
NameTab = "Tab"
|
||||
NameSpace = "Space"
|
||||
NameCtrl = "Ctrl"
|
||||
NameShift = "Shift"
|
||||
NameAlt = "Alt"
|
||||
NameSuper = "Super"
|
||||
NameCommand = "⌘"
|
||||
NameF1 = "F1"
|
||||
NameF2 = "F2"
|
||||
NameF3 = "F3"
|
||||
NameF4 = "F4"
|
||||
NameF5 = "F5"
|
||||
NameF6 = "F6"
|
||||
NameF7 = "F7"
|
||||
NameF8 = "F8"
|
||||
NameF9 = "F9"
|
||||
NameF10 = "F10"
|
||||
NameF11 = "F11"
|
||||
NameF12 = "F12"
|
||||
NameBack = "Back"
|
||||
)
|
||||
|
||||
// Contain reports whether m contains all modifiers
|
||||
@@ -227,52 +232,161 @@ func (m Modifiers) Contain(m2 Modifiers) bool {
|
||||
return m&m2 == m2
|
||||
}
|
||||
|
||||
// FocusCmd requests to set or clear the keyboard focus.
|
||||
type FocusCmd struct {
|
||||
// Tag is the new focus. The focus is cleared if Tag is nil, or if Tag
|
||||
// has no [event.Op] references.
|
||||
Tag event.Tag
|
||||
func (k Set) Contains(name string, mods Modifiers) bool {
|
||||
ks := string(k)
|
||||
for len(ks) > 0 {
|
||||
// Cut next key expression.
|
||||
chord, rest, _ := cut(ks, "|")
|
||||
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 (h InputHintOp) Add(o *op.Ops) {
|
||||
func keySetContains(keySet, name string) bool {
|
||||
// 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 {
|
||||
panic("Tag must be non-nil")
|
||||
}
|
||||
data := ops.Write1(&o.Internal, ops.TypeKeyInputHintLen, h.Tag)
|
||||
data[0] = byte(ops.TypeKeyInputHint)
|
||||
data := ops.Write2String(&o.Internal, ops.TypeKeyInputLen, h.Tag, string(h.Keys))
|
||||
data[0] = byte(ops.TypeKeyInput)
|
||||
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.Write2String(&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 (Event) ImplementsEvent() {}
|
||||
func (FocusEvent) ImplementsEvent() {}
|
||||
func (SnippetEvent) ImplementsEvent() {}
|
||||
func (SelectionEvent) ImplementsEvent() {}
|
||||
|
||||
func (FocusCmd) ImplementsCommand() {}
|
||||
func (SoftKeyboardCmd) ImplementsCommand() {}
|
||||
func (SelectionCmd) ImplementsCommand() {}
|
||||
func (SnippetCmd) ImplementsCommand() {}
|
||||
|
||||
func (Filter) ImplementsFilter() {}
|
||||
func (FocusFilter) ImplementsFilter() {}
|
||||
func (e Event) String() string {
|
||||
return fmt.Sprintf("%v %v %v}", e.Name, e.Modifiers, e.State)
|
||||
}
|
||||
|
||||
func (m Modifiers) String() string {
|
||||
var strs []string
|
||||
if m.Contain(ModCtrl) {
|
||||
strs = append(strs, string(NameCtrl))
|
||||
strs = append(strs, NameCtrl)
|
||||
}
|
||||
if m.Contain(ModCommand) {
|
||||
strs = append(strs, string(NameCommand))
|
||||
strs = append(strs, NameCommand)
|
||||
}
|
||||
if m.Contain(ModShift) {
|
||||
strs = append(strs, string(NameShift))
|
||||
strs = append(strs, NameShift)
|
||||
}
|
||||
if m.Contain(ModAlt) {
|
||||
strs = append(strs, string(NameAlt))
|
||||
strs = append(strs, NameAlt)
|
||||
}
|
||||
if m.Contain(ModSuper) {
|
||||
strs = append(strs, string(NameSuper))
|
||||
strs = append(strs, NameSuper)
|
||||
}
|
||||
return strings.Join(strs, "-")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-7
@@ -5,19 +5,36 @@ Package pointer implements pointer events and operations.
|
||||
A pointer is either a mouse controlled cursor or a touch
|
||||
object such as a finger.
|
||||
|
||||
The [event.Op] operation is used to declare a handler ready for pointer
|
||||
events.
|
||||
The InputOp operation is used to declare a handler ready for pointer
|
||||
events. Use an event.Queue to receive events.
|
||||
|
||||
# Kinds
|
||||
|
||||
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,
|
||||
Kinds: pointer.Press | pointer.Drag | pointer.Release,
|
||||
}.Add(ops)
|
||||
|
||||
Cancel events are always delivered.
|
||||
|
||||
# Hit areas
|
||||
|
||||
Clip operations from package [op/clip] are used for specifying
|
||||
hit areas where handlers may receive events.
|
||||
Clip operations from package op/clip are used for specifying
|
||||
hit areas where subsequent InputOps are active.
|
||||
|
||||
For example, to set up a handler with a rectangular hit area:
|
||||
|
||||
r := image.Rectangle{...}
|
||||
area := clip.Rect(r).Push(ops)
|
||||
event.Op{Tag: h}.Add(ops)
|
||||
pointer.InputOp{Tag: h}.Add(ops)
|
||||
area.Pop()
|
||||
|
||||
Note that hit areas behave similar to painting: the effective area of a stack
|
||||
@@ -37,11 +54,11 @@ For example:
|
||||
var h1, h2 *Handler
|
||||
|
||||
area := clip.Rect(...).Push(ops)
|
||||
event.Op(Ops, h1)
|
||||
pointer.InputOp{Tag: h1}.Add(Ops)
|
||||
area.Pop()
|
||||
|
||||
area := clip.Rect(...).Push(ops)
|
||||
event.Op(Ops, h2)
|
||||
pointer.InputOp{Tag: h2}.Add(ops)
|
||||
area.Pop()
|
||||
|
||||
implies a tree of two inner nodes, each with one pointer handler attached.
|
||||
|
||||
+42
-37
@@ -3,6 +3,9 @@
|
||||
package pointer
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -53,32 +56,21 @@ type PassStack struct {
|
||||
macroID uint32
|
||||
}
|
||||
|
||||
// Filter matches every [Event] that target the Tag and whose kind is
|
||||
// included in Kinds. Note that only tags specified in [event.Op] can
|
||||
// be targeted by pointer events.
|
||||
type Filter struct {
|
||||
Target event.Tag
|
||||
// Kinds is a bitwise-or of event types to match.
|
||||
Kinds Kind
|
||||
// ScrollX and ScrollY constrain the range of scrolling events delivered
|
||||
// to Target. Specifically, any Event e delivered to Tag will satisfy
|
||||
//
|
||||
// ScrollX.Min <= e.Scroll.X <= ScrollX.Max (horizontal axis)
|
||||
// ScrollY.Min <= e.Scroll.Y <= ScrollY.Max (vertical axis)
|
||||
ScrollX ScrollRange
|
||||
ScrollY ScrollRange
|
||||
}
|
||||
|
||||
// ScrollRange describes the range of scrolling distances in an
|
||||
// axis.
|
||||
type ScrollRange struct {
|
||||
Min, Max int
|
||||
}
|
||||
|
||||
// GrabCmd requests a pointer grab on the pointer identified by ID.
|
||||
type GrabCmd struct {
|
||||
// InputOp declares an input handler ready for pointer
|
||||
// events.
|
||||
type InputOp struct {
|
||||
Tag event.Tag
|
||||
ID ID
|
||||
// Grab, if set, request that the handler get
|
||||
// Grabbed priority.
|
||||
Grab bool
|
||||
// Kinds is a bitwise-or of event types to receive.
|
||||
Kinds Kind
|
||||
// ScrollBounds describe the maximum scrollable distances in both
|
||||
// axes. Specifically, any Event e delivered to Tag will satisfy
|
||||
//
|
||||
// ScrollBounds.Min.X <= e.Scroll.X <= ScrollBounds.Max.X (horizontal axis)
|
||||
// ScrollBounds.Min.Y <= e.Scroll.Y <= ScrollBounds.Max.Y (vertical axis)
|
||||
ScrollBounds image.Rectangle
|
||||
}
|
||||
|
||||
type ID uint16
|
||||
@@ -179,7 +171,7 @@ const (
|
||||
const (
|
||||
// A Cancel event is generated when the current gesture is
|
||||
// interrupted by other handlers or the system.
|
||||
Cancel Kind = 1 << iota
|
||||
Cancel Kind = (1 << iota) >> 1
|
||||
// Press of a pointer.
|
||||
Press
|
||||
// Release of a pointer.
|
||||
@@ -225,13 +217,6 @@ const (
|
||||
ButtonTertiary
|
||||
)
|
||||
|
||||
func (s ScrollRange) Union(s2 ScrollRange) ScrollRange {
|
||||
return ScrollRange{
|
||||
Min: min(s.Min, s2.Min),
|
||||
Max: max(s.Max, s2.Max),
|
||||
}
|
||||
}
|
||||
|
||||
// Push the current pass mode to the pass stack and set the pass mode.
|
||||
func (p PassOp) Push(o *op.Ops) PassStack {
|
||||
id, mid := ops.PushOp(&o.Internal, ops.PassStack)
|
||||
@@ -252,6 +237,30 @@ func (op Cursor) Add(o *op.Ops) {
|
||||
data[1] = byte(op)
|
||||
}
|
||||
|
||||
// Add panics if the scroll range does not contain zero.
|
||||
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.Kinds>>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.Kinds))
|
||||
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 Kind) String() string {
|
||||
if t == Cancel {
|
||||
return "Cancel"
|
||||
@@ -395,7 +404,3 @@ func (c Cursor) String() string {
|
||||
}
|
||||
|
||||
func (Event) ImplementsEvent() {}
|
||||
|
||||
func (GrabCmd) ImplementsCommand() {}
|
||||
|
||||
func (Filter) ImplementsFilter() {}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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() {}
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
Kinds: 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,
|
||||
Kinds: 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)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package input
|
||||
package router
|
||||
|
||||
import (
|
||||
"image"
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
f32internal "gioui.org/internal/f32"
|
||||
"gioui.org/internal/ops"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/semantic"
|
||||
"gioui.org/io/system"
|
||||
@@ -17,8 +18,14 @@ import (
|
||||
)
|
||||
|
||||
type pointerQueue struct {
|
||||
hitTree []hitNode
|
||||
areas []areaNode
|
||||
hitTree []hitNode
|
||||
areas []areaNode
|
||||
cursor pointer.Cursor
|
||||
handlers map[event.Tag]*pointerHandler
|
||||
pointers []pointerInfo
|
||||
transfers []io.ReadCloser // pending data transfers
|
||||
|
||||
scratch []event.Tag
|
||||
|
||||
semantic struct {
|
||||
idsAssigned bool
|
||||
@@ -36,15 +43,10 @@ type hitNode struct {
|
||||
|
||||
// For handler nodes.
|
||||
tag event.Tag
|
||||
ktag event.Tag
|
||||
pass bool
|
||||
}
|
||||
|
||||
// pointerState is the input state related to pointer events.
|
||||
type pointerState struct {
|
||||
cursor pointer.Cursor
|
||||
pointers []pointerInfo
|
||||
}
|
||||
|
||||
type pointerInfo struct {
|
||||
id pointer.ID
|
||||
pressed bool
|
||||
@@ -61,21 +63,17 @@ type pointerInfo struct {
|
||||
}
|
||||
|
||||
type pointerHandler struct {
|
||||
// areaPlusOne is the index into the list of pointerQueue.areas, plus 1.
|
||||
areaPlusOne int
|
||||
// setup tracks whether the handler has received
|
||||
// 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
|
||||
area int
|
||||
active bool
|
||||
wantsGrab bool
|
||||
types pointer.Kind
|
||||
// min and max horizontal/vertical scroll
|
||||
scrollX, scrollY pointer.ScrollRange
|
||||
scrollRange image.Rectangle
|
||||
|
||||
sourceMimes []string
|
||||
targetMimes []string
|
||||
offeredMime string
|
||||
data io.ReadCloser
|
||||
}
|
||||
|
||||
type areaOp struct {
|
||||
@@ -231,18 +229,33 @@ func (c *pointerCollector) addHitNode(n hitNode) {
|
||||
}
|
||||
|
||||
// newHandler returns the current handler or a new one for tag.
|
||||
func (c *pointerCollector) newHandler(tag event.Tag, state *pointerHandler) {
|
||||
func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *pointerHandler {
|
||||
areaID := c.currentArea()
|
||||
c.addHitNode(hitNode{
|
||||
area: areaID,
|
||||
tag: tag,
|
||||
pass: c.state.pass > 0,
|
||||
})
|
||||
state.areaPlusOne = areaID + 1
|
||||
h, ok := c.q.handlers[tag]
|
||||
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{Kind: pointer.Cancel})
|
||||
}
|
||||
h.active = true
|
||||
h.area = areaID
|
||||
return h
|
||||
}
|
||||
|
||||
func (s *pointerHandler) Reset() {
|
||||
s.areaPlusOne = 0
|
||||
func (c *pointerCollector) keyInputOp(op key.InputOp) {
|
||||
areaID := c.currentArea()
|
||||
c.addHitNode(hitNode{
|
||||
area: areaID,
|
||||
ktag: op.Tag,
|
||||
pass: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *pointerCollector) actionInputOp(act system.Action) {
|
||||
@@ -251,111 +264,21 @@ func (c *pointerCollector) actionInputOp(act system.Action) {
|
||||
area.action = act
|
||||
}
|
||||
|
||||
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) {
|
||||
func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
|
||||
areaID := c.currentArea()
|
||||
area := &c.q.areas[areaID]
|
||||
area.semantic.content.tag = tag
|
||||
c.newHandler(tag, state)
|
||||
}
|
||||
|
||||
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.scrollX = p.scrollX.Union(f.ScrollX)
|
||||
p.scrollY = p.scrollY.Union(f.ScrollY)
|
||||
area.semantic.content.tag = op.Tag
|
||||
if op.Kinds&(pointer.Press|pointer.Release) != 0 {
|
||||
area.semantic.content.gestures |= ClickGesture
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
if op.Kinds&pointer.Scroll != 0 {
|
||||
area.semantic.content.gestures |= ScrollGesture
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *pointerFilter) Merge(p2 pointerFilter) {
|
||||
p.kinds = p.kinds | p2.kinds
|
||||
p.scrollX = p.scrollX.Union(p2.scrollX)
|
||||
p.scrollY = p.scrollY.Union(p2.scrollY)
|
||||
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.scrollX.Min, p.scrollX.Max)
|
||||
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollY.Min, p.scrollY.Max)
|
||||
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
|
||||
area.semantic.valid = area.semantic.content.gestures != 0
|
||||
h := c.newHandler(op.Tag, events)
|
||||
h.wantsGrab = h.wantsGrab || op.Grab
|
||||
h.types = h.types | op.Kinds
|
||||
h.scrollRange = op.ScrollBounds
|
||||
}
|
||||
|
||||
func (c *pointerCollector) semanticLabel(lbl string) {
|
||||
@@ -399,28 +322,23 @@ func (c *pointerCollector) cursor(cursor pointer.Cursor) {
|
||||
area.cursor = cursor
|
||||
}
|
||||
|
||||
func (q *pointerQueue) offerData(handlers map[event.Tag]*handler, state pointerState, req transfer.OfferCmd) (pointerState, []taggedEvent) {
|
||||
var evts []taggedEvent
|
||||
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) sourceOp(op transfer.SourceOp, events *handlerEvents) {
|
||||
h := c.newHandler(op.Tag, events)
|
||||
h.sourceMimes = append(h.sourceMimes, op.Type)
|
||||
}
|
||||
|
||||
func (c *pointerCollector) Reset() {
|
||||
func (c *pointerCollector) targetOp(op transfer.TargetOp, events *handlerEvents) {
|
||||
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.resetState()
|
||||
c.ensureRoot()
|
||||
@@ -575,6 +493,20 @@ func (q *pointerQueue) hitTest(pos f32.Point, onNode func(*hitNode) bool) pointe
|
||||
return cursor
|
||||
}
|
||||
|
||||
func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
|
||||
hits := q.scratch[:0]
|
||||
cursor := q.hitTest(pos, func(n *hitNode) bool {
|
||||
if n.tag != nil {
|
||||
if _, exists := q.handlers[n.tag]; exists {
|
||||
hits = addHandler(hits, n.tag)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
q.scratch = hits[:0]
|
||||
return hits, cursor
|
||||
}
|
||||
|
||||
func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point {
|
||||
if areaIdx == -1 {
|
||||
return p
|
||||
@@ -599,6 +531,17 @@ func (q *pointerQueue) hit(areaIdx int, p f32.Point) (bool, pointer.Cursor) {
|
||||
}
|
||||
|
||||
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.areas = q.areas[:0]
|
||||
q.semantic.idsAssigned = false
|
||||
@@ -616,71 +559,80 @@ func (q *pointerQueue) reset() {
|
||||
delete(q.semantic.contentIDs, k)
|
||||
}
|
||||
}
|
||||
for _, rc := range q.transfers {
|
||||
if rc != nil {
|
||||
rc.Close()
|
||||
}
|
||||
}
|
||||
q.transfers = nil
|
||||
}
|
||||
|
||||
func (q *pointerQueue) Frame(handlers map[event.Tag]*handler, state pointerState) (pointerState, []taggedEvent) {
|
||||
for _, h := range handlers {
|
||||
if h.pointer.areaPlusOne != 0 {
|
||||
area := &q.areas[h.pointer.areaPlusOne-1]
|
||||
if h.filter.pointer.kinds&(pointer.Press|pointer.Release) != 0 {
|
||||
area.semantic.content.gestures |= ClickGesture
|
||||
func (q *pointerQueue) Frame(events *handlerEvents) {
|
||||
for k, h := range q.handlers {
|
||||
if !h.active {
|
||||
q.dropHandler(nil, k)
|
||||
delete(q.handlers, k)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
var evts []taggedEvent
|
||||
for i, p := range state.pointers {
|
||||
changed := false
|
||||
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
|
||||
}
|
||||
for i := range q.pointers {
|
||||
p := &q.pointers[i]
|
||||
q.deliverEnterLeaveEvents(p, events, p.last)
|
||||
q.deliverTransferDataEvent(p, events)
|
||||
}
|
||||
return state, evts
|
||||
}
|
||||
|
||||
func dropHandler(state pointerState, tag event.Tag) pointerState {
|
||||
pointers := state.pointers
|
||||
state.pointers = nil
|
||||
for _, p := range pointers {
|
||||
handlers := p.handlers
|
||||
p.handlers = nil
|
||||
for _, h := range handlers {
|
||||
if h != tag {
|
||||
p.handlers = append(p.handlers, h)
|
||||
}
|
||||
}
|
||||
entered := p.entered
|
||||
p.entered = nil
|
||||
for _, h := range entered {
|
||||
if h != tag {
|
||||
p.entered = append(p.entered, h)
|
||||
}
|
||||
}
|
||||
state.pointers = append(state.pointers, p)
|
||||
func (q *pointerQueue) dropHandler(events *handlerEvents, tag event.Tag) {
|
||||
if events != nil {
|
||||
events.Add(tag, pointer.Event{Kind: pointer.Cancel})
|
||||
}
|
||||
for i := range q.pointers {
|
||||
p := &q.pointers[i]
|
||||
for i := len(p.handlers) - 1; i >= 0; i-- {
|
||||
if p.handlers[i] == tag {
|
||||
p.handlers = append(p.handlers[:i], p.handlers[i+1:]...)
|
||||
}
|
||||
}
|
||||
for i := len(p.entered) - 1; i >= 0; i-- {
|
||||
if p.entered[i] == tag {
|
||||
p.entered = append(p.entered[:i], p.entered[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// pointerOf returns the pointerInfo index corresponding to the pointer in e.
|
||||
func (s pointerState) pointerOf(e pointer.Event) (pointerState, int) {
|
||||
for i, p := range s.pointers {
|
||||
func (q *pointerQueue) pointerOf(e pointer.Event) int {
|
||||
for i, p := range q.pointers {
|
||||
if p.id == e.PointerID {
|
||||
return s, i
|
||||
return i
|
||||
}
|
||||
}
|
||||
n := len(s.pointers)
|
||||
s.pointers = append(s.pointers[:n:n], pointerInfo{id: e.PointerID})
|
||||
return s, len(s.pointers) - 1
|
||||
q.pointers = append(q.pointers, pointerInfo{id: e.PointerID})
|
||||
return len(q.pointers) - 1
|
||||
}
|
||||
|
||||
// Deliver is like Push, but delivers an event to a particular area.
|
||||
func (q *pointerQueue) Deliver(handlers map[event.Tag]*handler, areaIdx int, e pointer.Event) []taggedEvent {
|
||||
scroll := e.Scroll
|
||||
func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEvents) {
|
||||
var sx, sy = e.Scroll.X, e.Scroll.Y
|
||||
idx := len(q.hitTree) - 1
|
||||
// Locate first potential receiver.
|
||||
for idx != -1 {
|
||||
@@ -690,28 +642,31 @@ func (q *pointerQueue) Deliver(handlers map[event.Tag]*handler, areaIdx int, e p
|
||||
}
|
||||
idx--
|
||||
}
|
||||
var evts []taggedEvent
|
||||
for idx != -1 {
|
||||
n := &q.hitTree[idx]
|
||||
idx = n.next
|
||||
h, ok := handlers[n.tag]
|
||||
if !ok || !h.filter.pointer.Matches(e) {
|
||||
if n.tag == nil {
|
||||
continue
|
||||
}
|
||||
h := q.handlers[n.tag]
|
||||
if e.Kind&h.types == 0 {
|
||||
continue
|
||||
}
|
||||
e := e
|
||||
if e.Kind == pointer.Scroll {
|
||||
if scroll == (f32.Point{}) {
|
||||
if sx == 0 && sy == 0 {
|
||||
break
|
||||
}
|
||||
scroll, e.Scroll = h.filter.pointer.clampScroll(scroll)
|
||||
// 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)
|
||||
}
|
||||
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
|
||||
evts = append(evts, taggedEvent{tag: n.tag, event: e})
|
||||
e.Position = q.invTransform(h.area, e.Position)
|
||||
events.Add(n.tag, e)
|
||||
if e.Kind != pointer.Scroll {
|
||||
break
|
||||
}
|
||||
}
|
||||
return evts
|
||||
}
|
||||
|
||||
// SemanticArea returns the sematic content for area, and its parent area.
|
||||
@@ -727,129 +682,106 @@ func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) {
|
||||
return semanticContent{}, -1
|
||||
}
|
||||
|
||||
func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState, e pointer.Event) (pointerState, []taggedEvent) {
|
||||
var evts []taggedEvent
|
||||
func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
|
||||
if e.Kind == pointer.Cancel {
|
||||
for k := range handlers {
|
||||
evts = append(evts, taggedEvent{
|
||||
event: pointer.Event{Kind: pointer.Cancel},
|
||||
tag: k,
|
||||
})
|
||||
q.pointers = q.pointers[:0]
|
||||
for k := range q.handlers {
|
||||
q.dropHandler(events, k)
|
||||
}
|
||||
state.pointers = nil
|
||||
return state, evts
|
||||
return
|
||||
}
|
||||
state, pidx := state.pointerOf(e)
|
||||
p := state.pointers[pidx]
|
||||
pidx := q.pointerOf(e)
|
||||
p := &q.pointers[pidx]
|
||||
p.last = e
|
||||
|
||||
switch e.Kind {
|
||||
case pointer.Press:
|
||||
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
|
||||
q.deliverEnterLeaveEvents(p, events, e)
|
||||
p.pressed = true
|
||||
evts = q.deliverEvent(handlers, p, evts, e)
|
||||
q.deliverEvent(p, events, e)
|
||||
case pointer.Move:
|
||||
if p.pressed {
|
||||
e.Kind = pointer.Drag
|
||||
}
|
||||
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
|
||||
evts = q.deliverEvent(handlers, p, evts, e)
|
||||
q.deliverEnterLeaveEvents(p, events, e)
|
||||
q.deliverEvent(p, events, e)
|
||||
if p.pressed {
|
||||
p, evts = q.deliverDragEvent(handlers, p, evts)
|
||||
q.deliverDragEvent(p, events)
|
||||
}
|
||||
case pointer.Release:
|
||||
evts = q.deliverEvent(handlers, p, evts, e)
|
||||
q.deliverEvent(p, events, e)
|
||||
p.pressed = false
|
||||
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
|
||||
p, evts = q.deliverDropEvent(handlers, p, evts)
|
||||
q.deliverEnterLeaveEvents(p, events, e)
|
||||
q.deliverDropEvent(p, events)
|
||||
case pointer.Scroll:
|
||||
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
|
||||
evts = q.deliverEvent(handlers, p, evts, e)
|
||||
q.deliverEnterLeaveEvents(p, events, e)
|
||||
q.deliverEvent(p, events, e)
|
||||
default:
|
||||
panic("unsupported pointer event type")
|
||||
}
|
||||
|
||||
p.last = e
|
||||
|
||||
if !p.pressed && len(p.entered) == 0 {
|
||||
// No longer need to track pointer.
|
||||
state.pointers = append(state.pointers[:pidx:pidx], state.pointers[pidx+1:]...)
|
||||
} else {
|
||||
state.pointers = append([]pointerInfo{}, state.pointers...)
|
||||
state.pointers[pidx] = p
|
||||
q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...)
|
||||
}
|
||||
return state, evts
|
||||
}
|
||||
|
||||
func (q *pointerQueue) deliverEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent, e pointer.Event) []taggedEvent {
|
||||
func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e pointer.Event) {
|
||||
foremost := true
|
||||
if p.pressed && len(p.handlers) == 1 {
|
||||
e.Priority = pointer.Grabbed
|
||||
foremost = false
|
||||
}
|
||||
scroll := e.Scroll
|
||||
var sx, sy = e.Scroll.X, e.Scroll.Y
|
||||
for _, k := range p.handlers {
|
||||
h, ok := handlers[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
f := h.filter.pointer
|
||||
if !f.Matches(e) {
|
||||
continue
|
||||
}
|
||||
h := q.handlers[k]
|
||||
if e.Kind == pointer.Scroll {
|
||||
if scroll == (f32.Point{}) {
|
||||
return evts
|
||||
if sx == 0 && sy == 0 {
|
||||
return
|
||||
}
|
||||
scroll, e.Scroll = f.clampScroll(scroll)
|
||||
// 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.Kind&h.types == 0 {
|
||||
continue
|
||||
}
|
||||
e := e
|
||||
if foremost {
|
||||
foremost = false
|
||||
e.Priority = pointer.Foremost
|
||||
}
|
||||
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
|
||||
evts = append(evts, taggedEvent{event: e, tag: k})
|
||||
e.Position = q.invTransform(h.area, e.Position)
|
||||
events.Add(k, e)
|
||||
}
|
||||
return evts
|
||||
}
|
||||
|
||||
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
|
||||
func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) {
|
||||
var hits []event.Tag
|
||||
if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press {
|
||||
// Consider non-mouse pointers leaving when they're released.
|
||||
} else {
|
||||
var transSrc *pointerFilter
|
||||
if p.dataSource != nil {
|
||||
transSrc = &handlers[p.dataSource].filter.pointer
|
||||
}
|
||||
cursor = q.hitTest(e.Position, func(n *hitNode) bool {
|
||||
h, ok := handlers[n.tag]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
add := true
|
||||
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
|
||||
hits, q.cursor = q.opHit(e.Position)
|
||||
if p.pressed {
|
||||
// Filter out non-participating handlers,
|
||||
// except potential transfer targets when a transfer has been initiated.
|
||||
var hitsHaveTarget bool
|
||||
if p.dataSource != nil {
|
||||
transferSource := q.handlers[p.dataSource]
|
||||
for _, hit := range hits {
|
||||
if _, ok := firstMimeMatch(transferSource, q.handlers[hit]); ok {
|
||||
hitsHaveTarget = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if add {
|
||||
hits = addHandler(hits, n.tag)
|
||||
for i := len(hits) - 1; i >= 0; i-- {
|
||||
if _, found := searchTag(p.handlers, hits[i]); !found && !hitsHaveTarget {
|
||||
hits = append(hits[:i], hits[i+1:]...)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if !p.pressed {
|
||||
changed = true
|
||||
p.handlers = hits
|
||||
} else {
|
||||
p.handlers = append(p.handlers[:0], hits...)
|
||||
}
|
||||
}
|
||||
// Deliver Leave events.
|
||||
@@ -857,94 +789,111 @@ func (q *pointerQueue) deliverEnterLeaveEvents(handlers map[event.Tag]*handler,
|
||||
if _, found := searchTag(hits, k); found {
|
||||
continue
|
||||
}
|
||||
h, ok := handlers[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
changed = true
|
||||
e := e
|
||||
h := q.handlers[k]
|
||||
e.Kind = pointer.Leave
|
||||
|
||||
if h.filter.pointer.Matches(e) {
|
||||
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
|
||||
evts = append(evts, taggedEvent{tag: k, event: e})
|
||||
if e.Kind&h.types != 0 {
|
||||
e := e
|
||||
e.Position = q.invTransform(h.area, e.Position)
|
||||
events.Add(k, e)
|
||||
}
|
||||
}
|
||||
// Deliver Enter events.
|
||||
for _, k := range hits {
|
||||
h := q.handlers[k]
|
||||
if _, found := searchTag(p.entered, k); found {
|
||||
continue
|
||||
}
|
||||
h, ok := handlers[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
changed = true
|
||||
e := e
|
||||
e.Kind = pointer.Enter
|
||||
|
||||
if h.filter.pointer.Matches(e) {
|
||||
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
|
||||
evts = append(evts, taggedEvent{tag: k, event: e})
|
||||
if e.Kind&h.types != 0 {
|
||||
e := e
|
||||
e.Position = q.invTransform(h.area, e.Position)
|
||||
events.Add(k, e)
|
||||
}
|
||||
}
|
||||
p.entered = hits
|
||||
return p, evts, cursor, changed
|
||||
p.entered = append(p.entered[:0], hits...)
|
||||
}
|
||||
|
||||
func (q *pointerQueue) deliverDragEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
|
||||
func (q *pointerQueue) deliverDragEvent(p *pointerInfo, events *handlerEvents) {
|
||||
if p.dataSource != nil {
|
||||
return p, evts
|
||||
return
|
||||
}
|
||||
// Identify the data source.
|
||||
for _, k := range p.entered {
|
||||
src := &handlers[k].filter.pointer
|
||||
src := q.handlers[k]
|
||||
if len(src.sourceMimes) == 0 {
|
||||
continue
|
||||
}
|
||||
// One data source handler per pointer.
|
||||
p.dataSource = k
|
||||
// Notify all potential targets.
|
||||
for k, tgt := range handlers {
|
||||
if _, ok := firstMimeMatch(src, &tgt.filter.pointer); ok {
|
||||
evts = append(evts, taggedEvent{tag: k, event: transfer.InitiateEvent{}})
|
||||
for k, tgt := range q.handlers {
|
||||
if _, ok := firstMimeMatch(src, tgt); ok {
|
||||
events.Add(k, transfer.InitiateEvent{})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return p, evts
|
||||
}
|
||||
|
||||
func (q *pointerQueue) deliverDropEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
|
||||
func (q *pointerQueue) deliverDropEvent(p *pointerInfo, events *handlerEvents) {
|
||||
if p.dataSource == nil {
|
||||
return p, evts
|
||||
return
|
||||
}
|
||||
// Request data from the source.
|
||||
src := &handlers[p.dataSource].filter.pointer
|
||||
src := q.handlers[p.dataSource]
|
||||
for _, k := range p.entered {
|
||||
h := handlers[k]
|
||||
if m, ok := firstMimeMatch(src, &h.filter.pointer); ok {
|
||||
h := q.handlers[k]
|
||||
if m, ok := firstMimeMatch(src, h); ok {
|
||||
p.dataTarget = k
|
||||
evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.RequestEvent{Type: m}})
|
||||
return p, evts
|
||||
events.Add(p.dataSource, transfer.RequestEvent{Type: m})
|
||||
return
|
||||
}
|
||||
}
|
||||
// No valid target found, abort.
|
||||
return q.deliverTransferCancelEvent(handlers, p, evts)
|
||||
q.deliverTransferCancelEvent(p, events)
|
||||
}
|
||||
|
||||
func (q *pointerQueue) deliverTransferCancelEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
|
||||
evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.CancelEvent{}})
|
||||
func (q *pointerQueue) deliverTransferDataEvent(p *pointerInfo, events *handlerEvents) {
|
||||
if p.dataSource == nil {
|
||||
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.
|
||||
src := &handlers[p.dataSource].filter.pointer
|
||||
for k, h := range handlers {
|
||||
if _, ok := firstMimeMatch(src, &h.filter.pointer); ok {
|
||||
evts = append(evts, taggedEvent{tag: k, event: transfer.CancelEvent{}})
|
||||
src := q.handlers[p.dataSource]
|
||||
for k, h := range q.handlers {
|
||||
if _, ok := firstMimeMatch(src, h); ok {
|
||||
events.Add(k, transfer.CancelEvent{})
|
||||
}
|
||||
}
|
||||
src.offeredMime = ""
|
||||
src.data = nil
|
||||
p.dataSource = nil
|
||||
p.dataTarget = nil
|
||||
return p, evts
|
||||
}
|
||||
|
||||
// ClipFor clips r to the parents of area.
|
||||
@@ -979,7 +928,7 @@ func addHandler(tags []event.Tag, tag event.Tag) []event.Tag {
|
||||
}
|
||||
|
||||
// firstMimeMatch returns the first type match between src and tgt.
|
||||
func firstMimeMatch(src, tgt *pointerFilter) (first string, matched bool) {
|
||||
func firstMimeMatch(src, tgt *pointerHandler) (first string, matched bool) {
|
||||
for _, m1 := range tgt.targetMimes {
|
||||
for _, m2 := range src.sourceMimes {
|
||||
if m1 == m2 {
|
||||
@@ -1016,3 +965,13 @@ func (a *areaNode) bounds() image.Rectangle {
|
||||
Max: a.trans.Transform(f32internal.FPt(a.area.rect.Max)),
|
||||
}.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,634 @@
|
||||
// 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{
|
||||
Kind: 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.Kind = pointer.Press
|
||||
q.pointer.queue.Deliver(area, e, &q.handlers)
|
||||
e.Kind = 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,
|
||||
Kinds: pointer.Kind(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 := key.Set(*encOp.Refs[1].(*string))
|
||||
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.TypeSemanticEnabled:
|
||||
if encOp.Data[1] != 0 {
|
||||
pc.semanticEnabled(true)
|
||||
} else {
|
||||
pc.semanticEnabled(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]
|
||||
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, ",")
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package input
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/semantic"
|
||||
"gioui.org/op"
|
||||
@@ -75,19 +74,13 @@ func TestSemanticTree(t *testing.T) {
|
||||
|
||||
func TestSemanticDescription(t *testing.T) {
|
||||
var ops op.Ops
|
||||
|
||||
h := new(int)
|
||||
event.Op(&ops, h)
|
||||
pointer.InputOp{Tag: new(int), Kinds: pointer.Press | pointer.Release}.Add(&ops)
|
||||
semantic.DescriptionOp("description").Add(&ops)
|
||||
semantic.LabelOp("label").Add(&ops)
|
||||
semantic.Button.Add(&ops)
|
||||
semantic.EnabledOp(false).Add(&ops)
|
||||
semantic.SelectedOp(true).Add(&ops)
|
||||
var r Router
|
||||
events(&r, -1, pointer.Filter{
|
||||
Target: h,
|
||||
Kinds: pointer.Press | pointer.Release,
|
||||
})
|
||||
r.Frame(&ops)
|
||||
tree := r.AppendSemantics(nil)
|
||||
got := tree[0].Desc
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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() {}
|
||||
+43
-29
@@ -2,11 +2,11 @@
|
||||
//
|
||||
// The transfer protocol is as follows:
|
||||
//
|
||||
// - Data sources use [SourceFilter] to receive an [InitiateEvent] when a drag
|
||||
// is initiated, and an [RequestEvent] for each initiation of a data transfer.
|
||||
// Sources respond to requests with [OfferCmd].
|
||||
// - Data targets use [TargetFilter] to receive an [DataEvent] for receiving data.
|
||||
// The target must close the data event after use.
|
||||
// - Data sources are registered with SourceOps, data targets with TargetOps.
|
||||
// - A data source receives a RequestEvent when a transfer is initiated.
|
||||
// It must respond with an OfferOp.
|
||||
// - The target receives a DataEvent when transferring to it. It must close
|
||||
// the event data after use.
|
||||
//
|
||||
// When a user initiates a pointer-guided drag and drop transfer, the
|
||||
// source as well as all potential targets receive an InitiateEvent.
|
||||
@@ -20,11 +20,29 @@ package transfer
|
||||
import (
|
||||
"io"
|
||||
|
||||
"gioui.org/internal/ops"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/op"
|
||||
)
|
||||
|
||||
// OfferCmd is used by data sources as a response to a RequestEvent.
|
||||
type OfferCmd struct {
|
||||
// SourceOp registers a tag as a data source for a MIME type.
|
||||
// Use multiple SourceOps if a tag supports multiple types.
|
||||
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
|
||||
// Type is the MIME type of Data.
|
||||
// It must be the Type from the corresponding RequestEvent.
|
||||
@@ -32,33 +50,32 @@ type OfferCmd struct {
|
||||
// Data contains the offered data. It is closed when the
|
||||
// transfer is complete or cancelled.
|
||||
// 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
|
||||
}
|
||||
|
||||
func (OfferCmd) ImplementsCommand() {}
|
||||
|
||||
// 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 SourceOp) Add(o *op.Ops) {
|
||||
data := ops.Write2(&o.Internal, ops.TypeSourceLen, op.Tag, op.Type)
|
||||
data[0] = byte(ops.TypeSource)
|
||||
}
|
||||
|
||||
// TargetFilter filters for any [DataEvent] whose type matches a MIME type
|
||||
// as well as [CancelEvent]. Use multiple filters to accept multiple types.
|
||||
type TargetFilter struct {
|
||||
// Target is a tag included in a previous event.Op.
|
||||
Target event.Tag
|
||||
// Type is the MIME type accepted by this target.
|
||||
Type string
|
||||
func (op TargetOp) Add(o *op.Ops) {
|
||||
data := ops.Write2(&o.Internal, ops.TypeTargetLen, op.Tag, op.Type)
|
||||
data[0] = byte(ops.TypeTarget)
|
||||
}
|
||||
|
||||
// Add the offer to the list of operations.
|
||||
// It panics if the Data field is not set.
|
||||
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
|
||||
// respond with an OfferCmd.
|
||||
// respond with an OfferOp.
|
||||
type RequestEvent struct {
|
||||
// Type is the first matched type between the source and the target.
|
||||
Type string
|
||||
@@ -90,6 +107,3 @@ type DataEvent struct {
|
||||
}
|
||||
|
||||
func (DataEvent) ImplementsEvent() {}
|
||||
|
||||
func (SourceFilter) ImplementsFilter() {}
|
||||
func (TargetFilter) ImplementsFilter() {}
|
||||
|
||||
+57
-5
@@ -3,9 +3,10 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
"image"
|
||||
"time"
|
||||
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
@@ -20,6 +21,9 @@ type Context struct {
|
||||
Constraints Constraints
|
||||
|
||||
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 time.Time
|
||||
|
||||
@@ -28,10 +32,46 @@ type Context struct {
|
||||
// Interested users must look up and populate these values manually.
|
||||
Locale system.Locale
|
||||
|
||||
input.Source
|
||||
*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.
|
||||
func (c Context) Dp(v unit.Dp) int {
|
||||
return c.Metric.Dp(v)
|
||||
@@ -42,9 +82,21 @@ func (c Context) Sp(v unit.Sp) int {
|
||||
return c.Metric.Sp(v)
|
||||
}
|
||||
|
||||
// Disabled returns a copy of this context with a disabled Source,
|
||||
// blocking widgets from changing its state and receiving events.
|
||||
// Events returns the events available for the key. If no
|
||||
// queue is configured, Events returns nil.
|
||||
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 {
|
||||
c.Source = input.Source{}
|
||||
c.Queue = nil
|
||||
return c
|
||||
}
|
||||
|
||||
+20
-22
@@ -7,7 +7,6 @@ import (
|
||||
"math"
|
||||
|
||||
"gioui.org/gesture"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
)
|
||||
@@ -145,26 +144,7 @@ func (l *List) Dragging() bool {
|
||||
}
|
||||
|
||||
func (l *List) update(gtx Context) {
|
||||
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
|
||||
}
|
||||
}
|
||||
xrange := pointer.ScrollRange{Min: min, Max: max}
|
||||
yrange := pointer.ScrollRange{}
|
||||
if l.Axis == Vertical {
|
||||
xrange, yrange = yrange, xrange
|
||||
}
|
||||
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), xrange, yrange)
|
||||
d := l.scroll.Update(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis))
|
||||
l.scrollDelta = d
|
||||
l.Position.Offset += d
|
||||
}
|
||||
@@ -352,7 +332,25 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
|
||||
call := macro.Stop()
|
||||
defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop()
|
||||
|
||||
l.scroll.Add(ops)
|
||||
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)),
|
||||
}
|
||||
l.scroll.Add(ops, scrollRange)
|
||||
|
||||
call.Add(ops)
|
||||
return Dimensions{Size: dims}
|
||||
|
||||
+3
-3
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/router"
|
||||
"gioui.org/op"
|
||||
)
|
||||
|
||||
@@ -64,13 +64,13 @@ func TestListScrollToEnd(t *testing.T) {
|
||||
|
||||
func TestListPosition(t *testing.T) {
|
||||
_s := func(e ...event.Event) []event.Event { return e }
|
||||
r := new(input.Router)
|
||||
r := new(router.Router)
|
||||
gtx := Context{
|
||||
Ops: new(op.Ops),
|
||||
Constraints: Constraints{
|
||||
Max: image.Pt(20, 10),
|
||||
},
|
||||
Source: r.Source(),
|
||||
Queue: r,
|
||||
}
|
||||
el := func(gtx Context, idx int) Dimensions {
|
||||
return Dimensions{Size: image.Pt(10, 10)}
|
||||
|
||||
@@ -138,9 +138,6 @@ type Path struct {
|
||||
func (p *Path) Pos() f32.Point { return p.pen }
|
||||
|
||||
// Begin the path, storing the path data and final Op into ops.
|
||||
//
|
||||
// Caller must also call End to finish the drawing.
|
||||
// Forgetting to call it will result in a "panic: cannot mix multi ops with single ones".
|
||||
func (p *Path) Begin(o *op.Ops) {
|
||||
*p = Path{
|
||||
ops: &o.Internal,
|
||||
|
||||
@@ -53,6 +53,7 @@ The MacroOp records a list of operations to be executed later:
|
||||
ops := new(op.Ops)
|
||||
macro := op.Record(ops)
|
||||
// Record operations by adding them.
|
||||
op.InvalidateOp{}.Add(ops)
|
||||
...
|
||||
// End recording.
|
||||
call := macro.Stop()
|
||||
@@ -95,9 +96,9 @@ type CallOp struct {
|
||||
end ops.PC
|
||||
}
|
||||
|
||||
// InvalidateCmd requests a redraw at the given time. Use
|
||||
// InvalidateOp requests a redraw at the given time. Use
|
||||
// the zero value to request an immediate redraw.
|
||||
type InvalidateCmd struct {
|
||||
type InvalidateOp struct {
|
||||
At time.Time
|
||||
}
|
||||
|
||||
@@ -180,6 +181,19 @@ func (c CallOp) Add(o *Ops) {
|
||||
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.
|
||||
func Offset(off image.Point) TransformOp {
|
||||
offf := f32.Pt(float32(off.X), float32(off.Y))
|
||||
@@ -226,5 +240,3 @@ func (t TransformStack) Pop() {
|
||||
data := ops.Write(t.ops, ops.TypePopTransformLen)
|
||||
data[0] = byte(ops.TypePopTransform)
|
||||
}
|
||||
|
||||
func (InvalidateCmd) ImplementsCommand() {}
|
||||
|
||||
+1
-3
@@ -4,7 +4,6 @@ package text
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"log"
|
||||
@@ -277,8 +276,7 @@ func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
|
||||
// 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.
|
||||
func (s *shaperImpl) Load(f FontFace) {
|
||||
desc := opentype.FontToDescription(f.Font)
|
||||
s.fontMap.AddFace(f.Face.Face(), fontscan.Location{File: fmt.Sprint(desc)}, desc)
|
||||
s.fontMap.AddFace(f.Face.Face(), opentype.FontToDescription(f.Font))
|
||||
s.addFace(f.Face.Face(), f.Font)
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,10 @@ type layoutKey struct {
|
||||
lineHeightScale float32
|
||||
}
|
||||
|
||||
type pathKey struct {
|
||||
gidHash uint64
|
||||
}
|
||||
|
||||
const maxSize = 1000
|
||||
|
||||
func gidsEqual(a []glyphInfo, glyphs []Glyph) bool {
|
||||
|
||||
+8
-3
@@ -16,7 +16,7 @@ type Bool struct {
|
||||
// Update the widget state and report whether Value was changed.
|
||||
func (b *Bool) Update(gtx layout.Context) bool {
|
||||
changed := false
|
||||
for b.clk.clicked(b, gtx) {
|
||||
for b.clk.Clicked(gtx) {
|
||||
b.Value = !b.Value
|
||||
changed = true
|
||||
}
|
||||
@@ -33,15 +33,20 @@ func (b *Bool) Pressed() bool {
|
||||
return b.clk.Pressed()
|
||||
}
|
||||
|
||||
// Focused reports whether b has focus.
|
||||
func (b *Bool) Focused() bool {
|
||||
return b.clk.Focused()
|
||||
}
|
||||
|
||||
func (b *Bool) History() []Press {
|
||||
return b.clk.History()
|
||||
}
|
||||
|
||||
func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
||||
b.Update(gtx)
|
||||
dims := b.clk.layout(b, gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
dims := b.clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
semantic.SelectedOp(b.Value).Add(gtx.Ops)
|
||||
semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops)
|
||||
semantic.EnabledOp(gtx.Queue != nil).Add(gtx.Ops)
|
||||
return w(gtx)
|
||||
})
|
||||
return dims
|
||||
|
||||
@@ -11,6 +11,9 @@ import (
|
||||
|
||||
// editBuffer implements a gap buffer for text editing.
|
||||
type editBuffer struct {
|
||||
// pos is the byte position for Read and ReadRune.
|
||||
pos int
|
||||
|
||||
// The gap start and end in bytes.
|
||||
gapstart, gapend int
|
||||
text []byte
|
||||
|
||||
+64
-53
@@ -7,7 +7,6 @@ import (
|
||||
"time"
|
||||
|
||||
"gioui.org/gesture"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/semantic"
|
||||
@@ -18,11 +17,16 @@ import (
|
||||
|
||||
// Clickable represents a clickable area.
|
||||
type Clickable struct {
|
||||
click gesture.Click
|
||||
click gesture.Click
|
||||
// clicks is for saved clicks to support Clicked.
|
||||
clicks []Click
|
||||
history []Press
|
||||
|
||||
keyTag struct{}
|
||||
requestFocus bool
|
||||
requestClicks int
|
||||
pressedKey key.Name
|
||||
focused bool
|
||||
pressedKey string
|
||||
}
|
||||
|
||||
// Click represents a click.
|
||||
@@ -49,14 +53,19 @@ func (b *Clickable) Click() {
|
||||
b.requestClicks++
|
||||
}
|
||||
|
||||
// Clicked calls Update and reports whether a click was registered.
|
||||
// Clicked reports whether there are pending clicks. If so, Clicked
|
||||
// removes the earliest click.
|
||||
func (b *Clickable) Clicked(gtx layout.Context) bool {
|
||||
return b.clicked(b, gtx)
|
||||
}
|
||||
|
||||
func (b *Clickable) clicked(t event.Tag, gtx layout.Context) bool {
|
||||
_, clicked := b.update(t, gtx)
|
||||
return clicked
|
||||
if len(b.clicks) > 0 {
|
||||
b.clicks = b.clicks[1:]
|
||||
return true
|
||||
}
|
||||
b.clicks = b.Update(gtx)
|
||||
if len(b.clicks) > 0 {
|
||||
b.clicks = b.clicks[1:]
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Hovered reports whether a pointer is over the element.
|
||||
@@ -69,6 +78,16 @@ func (b *Clickable) Pressed() bool {
|
||||
return b.click.Pressed()
|
||||
}
|
||||
|
||||
// Focus requests the input focus for the element.
|
||||
func (b *Clickable) Focus() {
|
||||
b.requestFocus = true
|
||||
}
|
||||
|
||||
// Focused reports whether b has focus.
|
||||
func (b *Clickable) Focused() bool {
|
||||
return b.focused
|
||||
}
|
||||
|
||||
// History is the past pointer presses useful for drawing markers.
|
||||
// History is retained for a short duration (about a second).
|
||||
func (b *Clickable) History() []Press {
|
||||
@@ -77,34 +96,36 @@ func (b *Clickable) History() []Press {
|
||||
|
||||
// Layout and update the button state.
|
||||
func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
||||
return b.layout(b, gtx, w)
|
||||
}
|
||||
|
||||
func (b *Clickable) layout(t event.Tag, gtx layout.Context, w layout.Widget) layout.Dimensions {
|
||||
for {
|
||||
_, ok := b.update(t, gtx)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
b.Update(gtx)
|
||||
m := op.Record(gtx.Ops)
|
||||
dims := w(gtx)
|
||||
c := m.Stop()
|
||||
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
|
||||
semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops)
|
||||
enabled := gtx.Queue != nil
|
||||
semantic.EnabledOp(enabled).Add(gtx.Ops)
|
||||
b.click.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, t)
|
||||
if enabled {
|
||||
keys := key.Set("⏎|Space")
|
||||
if !b.focused {
|
||||
keys = ""
|
||||
}
|
||||
key.InputOp{Tag: &b.keyTag, Keys: keys}.Add(gtx.Ops)
|
||||
}
|
||||
c.Add(gtx.Ops)
|
||||
return dims
|
||||
}
|
||||
|
||||
// Update the button state by processing events, and return the next
|
||||
// click, if any.
|
||||
func (b *Clickable) Update(gtx layout.Context) (Click, bool) {
|
||||
return b.update(b, gtx)
|
||||
}
|
||||
|
||||
func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
|
||||
// Update the button state by processing events, and return the resulting
|
||||
// clicks, if any.
|
||||
func (b *Clickable) Update(gtx layout.Context) []Click {
|
||||
b.clicks = nil
|
||||
if gtx.Queue == nil {
|
||||
b.focused = false
|
||||
}
|
||||
if b.requestFocus {
|
||||
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
|
||||
b.requestFocus = false
|
||||
}
|
||||
for len(b.history) > 0 {
|
||||
c := b.history[0]
|
||||
if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second {
|
||||
@@ -113,26 +134,23 @@ func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
|
||||
n := copy(b.history, b.history[1:])
|
||||
b.history = b.history[:n]
|
||||
}
|
||||
var clicks []Click
|
||||
if c := b.requestClicks; c > 0 {
|
||||
b.requestClicks = 0
|
||||
return Click{
|
||||
clicks = append(clicks, Click{
|
||||
NumClicks: c,
|
||||
}, true
|
||||
})
|
||||
}
|
||||
for {
|
||||
e, ok := b.click.Update(gtx.Source)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
for _, e := range b.click.Update(gtx) {
|
||||
switch e.Kind {
|
||||
case gesture.KindClick:
|
||||
if l := len(b.history); l > 0 {
|
||||
b.history[l-1].End = gtx.Now
|
||||
}
|
||||
return Click{
|
||||
clicks = append(clicks, Click{
|
||||
Modifiers: e.Modifiers,
|
||||
NumClicks: e.NumClicks,
|
||||
}, true
|
||||
})
|
||||
case gesture.KindCancel:
|
||||
for i := range b.history {
|
||||
b.history[i].Cancelled = true
|
||||
@@ -142,7 +160,7 @@ func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
|
||||
}
|
||||
case gesture.KindPress:
|
||||
if e.Source == pointer.Mouse {
|
||||
gtx.Execute(key.FocusCmd{Tag: t})
|
||||
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
|
||||
}
|
||||
b.history = append(b.history, Press{
|
||||
Position: e.Position,
|
||||
@@ -150,22 +168,15 @@ func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
|
||||
})
|
||||
}
|
||||
}
|
||||
for {
|
||||
e, ok := gtx.Event(
|
||||
key.FocusFilter{Target: t},
|
||||
key.Filter{Focus: t, Name: key.NameReturn},
|
||||
key.Filter{Focus: t, Name: key.NameSpace},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
for _, e := range gtx.Events(&b.keyTag) {
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
if e.Focus {
|
||||
b.focused = e.Focus
|
||||
if !b.focused {
|
||||
b.pressedKey = ""
|
||||
}
|
||||
case key.Event:
|
||||
if !gtx.Focused(t) {
|
||||
if !b.focused {
|
||||
break
|
||||
}
|
||||
if e.Name != key.NameReturn && e.Name != key.NameSpace {
|
||||
@@ -180,12 +191,12 @@ func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
|
||||
}
|
||||
// only register a key as a click if the key was pressed and released while this button was focused
|
||||
b.pressedKey = ""
|
||||
return Click{
|
||||
clicks = append(clicks, Click{
|
||||
Modifiers: e.Modifiers,
|
||||
NumClicks: 1,
|
||||
}, true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return Click{}, false
|
||||
return clicks
|
||||
}
|
||||
|
||||
+25
-15
@@ -6,8 +6,9 @@ import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/router"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/widget"
|
||||
@@ -15,14 +16,12 @@ import (
|
||||
|
||||
func TestClickable(t *testing.T) {
|
||||
var (
|
||||
r input.Router
|
||||
b1 widget.Clickable
|
||||
b2 widget.Clickable
|
||||
ops op.Ops
|
||||
r router.Router
|
||||
b1 widget.Clickable
|
||||
b2 widget.Clickable
|
||||
)
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Source: r.Source(),
|
||||
}
|
||||
gtx := layout.NewContext(&ops, system.FrameEvent{Queue: &r})
|
||||
layout := func() {
|
||||
b1.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Dimensions{Size: image.Pt(100, 100)}
|
||||
@@ -33,18 +32,23 @@ func TestClickable(t *testing.T) {
|
||||
})
|
||||
}
|
||||
frame := func() {
|
||||
gtx.Reset()
|
||||
ops.Reset()
|
||||
layout()
|
||||
r.Frame(gtx.Ops)
|
||||
}
|
||||
gtx.Execute(key.FocusCmd{Tag: &b1})
|
||||
// frame: request focus for button 1
|
||||
b1.Focus()
|
||||
frame()
|
||||
if !gtx.Focused(&b1) {
|
||||
// frame: gain focus for button 1
|
||||
frame()
|
||||
if !b1.Focused() {
|
||||
t.Error("button 1 did not gain focus")
|
||||
}
|
||||
if gtx.Focused(&b2) {
|
||||
if b2.Focused() {
|
||||
t.Error("button 2 should not have focus")
|
||||
}
|
||||
// frame: press & release return
|
||||
frame()
|
||||
r.Queue(
|
||||
key.Event{
|
||||
Name: key.NameReturn,
|
||||
@@ -61,30 +65,36 @@ func TestClickable(t *testing.T) {
|
||||
if b2.Clicked(gtx) {
|
||||
t.Error("button 2 got clicked when it did not have focus")
|
||||
}
|
||||
// frame: press return down
|
||||
r.Queue(
|
||||
key.Event{
|
||||
Name: key.NameReturn,
|
||||
State: key.Press,
|
||||
},
|
||||
)
|
||||
frame()
|
||||
if b1.Clicked(gtx) {
|
||||
t.Error("button 1 got clicked, even if it only got return press")
|
||||
}
|
||||
// frame: request focus for button 2
|
||||
b2.Focus()
|
||||
frame()
|
||||
gtx.Execute(key.FocusCmd{Tag: &b2})
|
||||
// frame: gain focus for button 2
|
||||
frame()
|
||||
if gtx.Focused(&b1) {
|
||||
if b1.Focused() {
|
||||
t.Error("button 1 should not have focus")
|
||||
}
|
||||
if !gtx.Focused(&b2) {
|
||||
if !b2.Focused() {
|
||||
t.Error("button 2 did not gain focus")
|
||||
}
|
||||
// frame: release return
|
||||
r.Queue(
|
||||
key.Event{
|
||||
Name: key.NameReturn,
|
||||
State: key.Release,
|
||||
},
|
||||
)
|
||||
frame()
|
||||
if b1.Clicked(gtx) {
|
||||
t.Error("button 1 got clicked, even if it had lost focus")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
|
||||
"gioui.org/gesture"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
@@ -11,7 +12,11 @@ import (
|
||||
|
||||
// Decorations handles the states of window decorations.
|
||||
type Decorations struct {
|
||||
clicks map[int]*Clickable
|
||||
clicks map[int]*Clickable
|
||||
resize [8]struct {
|
||||
gesture.Hover
|
||||
gesture.Drag
|
||||
}
|
||||
maximized bool
|
||||
}
|
||||
|
||||
|
||||
+18
-19
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/gesture"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/layout"
|
||||
@@ -18,20 +17,24 @@ type Draggable struct {
|
||||
// Type contains the MIME type and matches transfer.SourceOp.
|
||||
Type string
|
||||
|
||||
drag gesture.Drag
|
||||
click f32.Point
|
||||
pos f32.Point
|
||||
handle struct{}
|
||||
drag gesture.Drag
|
||||
click f32.Point
|
||||
pos f32.Point
|
||||
}
|
||||
|
||||
func (d *Draggable) Layout(gtx layout.Context, w, drag layout.Widget) layout.Dimensions {
|
||||
if !gtx.Enabled() {
|
||||
if gtx.Queue == nil {
|
||||
return w(gtx)
|
||||
}
|
||||
dims := w(gtx)
|
||||
|
||||
stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops)
|
||||
d.drag.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, d)
|
||||
transfer.SourceOp{
|
||||
Tag: &d.handle,
|
||||
Type: d.Type,
|
||||
}.Add(gtx.Ops)
|
||||
stack.Pop()
|
||||
|
||||
if drag != nil && d.drag.Pressed() {
|
||||
@@ -53,11 +56,7 @@ func (d *Draggable) Dragging() bool {
|
||||
// requested to offer data, if any
|
||||
func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
|
||||
pos := d.pos
|
||||
for {
|
||||
ev, ok := d.drag.Update(gtx.Metric, gtx.Source, gesture.Both)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
for _, ev := range d.drag.Update(gtx.Metric, gtx.Queue, gesture.Both) {
|
||||
switch ev.Kind {
|
||||
case pointer.Press:
|
||||
d.click = ev.Position
|
||||
@@ -68,12 +67,8 @@ func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
|
||||
}
|
||||
d.pos = pos
|
||||
|
||||
for {
|
||||
e, ok := gtx.Event(transfer.SourceFilter{Target: d, Type: d.Type})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if e, ok := e.(transfer.RequestEvent); ok {
|
||||
for _, ev := range gtx.Queue.Events(&d.handle) {
|
||||
if e, ok := ev.(transfer.RequestEvent); ok {
|
||||
return e.Type, true
|
||||
}
|
||||
}
|
||||
@@ -82,8 +77,12 @@ func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
|
||||
|
||||
// Offer the data ready for a drop. Must be called after being Requested.
|
||||
// The mime must be one in the requested list.
|
||||
func (d *Draggable) Offer(gtx layout.Context, mime string, data io.ReadCloser) {
|
||||
gtx.Execute(transfer.OfferCmd{Tag: d, Type: mime, Data: data})
|
||||
func (d *Draggable) Offer(ops *op.Ops, mime string, data io.ReadCloser) {
|
||||
transfer.OfferOp{
|
||||
Tag: &d.handle,
|
||||
Type: mime,
|
||||
Data: data,
|
||||
}.Add(ops)
|
||||
}
|
||||
|
||||
// Pos returns the drag position relative to its initial click position.
|
||||
|
||||
+14
-22
@@ -5,9 +5,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/router"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
@@ -15,27 +14,27 @@ import (
|
||||
)
|
||||
|
||||
func TestDraggable(t *testing.T) {
|
||||
var r input.Router
|
||||
var r router.Router
|
||||
gtx := layout.Context{
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Source: r.Source(),
|
||||
Queue: &r,
|
||||
Ops: new(op.Ops),
|
||||
}
|
||||
|
||||
drag := &Draggable{
|
||||
Type: "file",
|
||||
}
|
||||
tgt := new(int)
|
||||
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
|
||||
dims := drag.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
}, nil)
|
||||
stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops)
|
||||
event.Op(gtx.Ops, tgt)
|
||||
transfer.TargetOp{
|
||||
Tag: drag,
|
||||
Type: drag.Type,
|
||||
}.Add(gtx.Ops)
|
||||
stack.Pop()
|
||||
|
||||
drag.Update(gtx)
|
||||
r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type})
|
||||
r.Frame(gtx.Ops)
|
||||
r.Queue(
|
||||
pointer.Event{
|
||||
@@ -52,28 +51,21 @@ func TestDraggable(t *testing.T) {
|
||||
},
|
||||
)
|
||||
ofr := &offer{data: "hello"}
|
||||
drag.Update(gtx)
|
||||
r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type})
|
||||
drag.Offer(gtx, "file", ofr)
|
||||
drag.Offer(gtx.Ops, "file", ofr)
|
||||
r.Frame(gtx.Ops)
|
||||
|
||||
e, ok := r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type})
|
||||
if !ok {
|
||||
t.Fatalf("expected event")
|
||||
evs := r.Events(drag)
|
||||
if len(evs) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(evs))
|
||||
}
|
||||
ev := e.(transfer.DataEvent)
|
||||
ev := evs[0].(transfer.DataEvent)
|
||||
ev.Open = nil
|
||||
if got, want := ev.Type, "file"; got != want {
|
||||
t.Errorf("expected %v; got %v", got, want)
|
||||
}
|
||||
if ofr.closed {
|
||||
t.Error("offer closed prematurely")
|
||||
}
|
||||
e, ok = r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type})
|
||||
if !ok {
|
||||
t.Fatalf("expected event")
|
||||
}
|
||||
if _, ok := e.(transfer.CancelEvent); !ok {
|
||||
t.Fatalf("expected transfer.CancelEvent event")
|
||||
}
|
||||
r.Frame(gtx.Ops)
|
||||
if !ofr.closed {
|
||||
t.Error("offer was not closed")
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
|
||||
// Package widget implements state tracking and event handling of
|
||||
// common user interface controls. To draw widgets, use a theme
|
||||
// packages such as package [gioui.org/widget/material].
|
||||
// packages such as package gioui.org/widget/material.
|
||||
package widget
|
||||
|
||||
+237
-293
@@ -21,7 +21,6 @@ import (
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/semantic"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
@@ -70,8 +69,11 @@ type Editor struct {
|
||||
buffer *editBuffer
|
||||
// scratch is a byte buffer that is reused to efficiently read portions of text
|
||||
// from the textView.
|
||||
scratch []byte
|
||||
blinkStart time.Time
|
||||
scratch []byte
|
||||
eventKey int
|
||||
blinkStart time.Time
|
||||
focused bool
|
||||
requestFocus bool
|
||||
|
||||
// ime tracks the state relevant to input methods.
|
||||
ime struct {
|
||||
@@ -87,14 +89,16 @@ type Editor struct {
|
||||
|
||||
clicker gesture.Click
|
||||
|
||||
// events is the list of events not yet processed.
|
||||
events []EditorEvent
|
||||
// prevEvents is the number of events from the previous frame.
|
||||
prevEvents int
|
||||
// history contains undo history.
|
||||
history []modification
|
||||
// nextHistoryIdx is the index within the history of the next modification. This
|
||||
// is only not len(history) immediately after undo operations occur. It is framed as the "next" value
|
||||
// to make the zero value consistent.
|
||||
nextHistoryIdx int
|
||||
|
||||
pending []EditorEvent
|
||||
}
|
||||
|
||||
type offEntry struct {
|
||||
@@ -187,37 +191,30 @@ const (
|
||||
maxBlinkDuration = 10 * time.Second
|
||||
)
|
||||
|
||||
func (e *Editor) processEvents(gtx layout.Context) (ev EditorEvent, ok bool) {
|
||||
if len(e.pending) > 0 {
|
||||
out := e.pending[0]
|
||||
e.pending = e.pending[:copy(e.pending, e.pending[1:])]
|
||||
return out, true
|
||||
}
|
||||
selStart, selEnd := e.Selection()
|
||||
defer func() {
|
||||
afterSelStart, afterSelEnd := e.Selection()
|
||||
if selStart != afterSelStart || selEnd != afterSelEnd {
|
||||
if ok {
|
||||
e.pending = append(e.pending, SelectEvent{})
|
||||
} else {
|
||||
ev = SelectEvent{}
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ev, ok = e.processPointer(gtx)
|
||||
if ok {
|
||||
return ev, ok
|
||||
}
|
||||
ev, ok = e.processKey(gtx)
|
||||
if ok {
|
||||
return ev, ok
|
||||
}
|
||||
return nil, false
|
||||
// Events returns available editor events.
|
||||
func (e *Editor) Events() []EditorEvent {
|
||||
events := e.events
|
||||
e.events = nil
|
||||
e.prevEvents = 0
|
||||
return events
|
||||
}
|
||||
|
||||
func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
|
||||
func (e *Editor) processEvents(gtx layout.Context) {
|
||||
// Flush events from before the previous Layout.
|
||||
n := copy(e.events, e.events[e.prevEvents:])
|
||||
e.events = e.events[:n]
|
||||
e.prevEvents = n
|
||||
|
||||
oldStart, oldLen := min(e.text.Selection()), e.text.SelectionLen()
|
||||
e.processPointer(gtx)
|
||||
e.processKey(gtx)
|
||||
// Queue a SelectEvent if the selection changed, including if it went away.
|
||||
if newStart, newLen := min(e.text.Selection()), e.text.SelectionLen(); oldStart != newStart || oldLen != newLen {
|
||||
e.events = append(e.events, SelectEvent{})
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Editor) processPointer(gtx layout.Context) {
|
||||
sbounds := e.text.ScrollBounds()
|
||||
var smin, smax int
|
||||
var axis gesture.Axis
|
||||
@@ -228,19 +225,7 @@ func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
|
||||
axis = gesture.Vertical
|
||||
smin, smax = sbounds.Min.Y, sbounds.Max.Y
|
||||
}
|
||||
var scrollX, scrollY pointer.ScrollRange
|
||||
textDims := e.text.FullDimensions()
|
||||
visibleDims := e.text.Dimensions()
|
||||
if e.SingleLine {
|
||||
scrollOffX := e.text.ScrollOff().X
|
||||
scrollX.Min = min(-scrollOffX, 0)
|
||||
scrollX.Max = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
|
||||
} else {
|
||||
scrollOffY := e.text.ScrollOff().Y
|
||||
scrollY.Min = -scrollOffY
|
||||
scrollY.Max = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y))
|
||||
}
|
||||
sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollX, scrollY)
|
||||
sdist := e.scroller.Update(gtx.Metric, gtx, gtx.Now, axis)
|
||||
var soff int
|
||||
if e.SingleLine {
|
||||
e.text.ScrollRel(sdist, 0)
|
||||
@@ -249,176 +234,115 @@ func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
|
||||
e.text.ScrollRel(0, sdist)
|
||||
soff = e.text.ScrollOff().Y
|
||||
}
|
||||
for {
|
||||
evt, ok := e.clicker.Update(gtx.Source)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
ev, ok := e.processPointerEvent(gtx, evt)
|
||||
if ok {
|
||||
return ev, ok
|
||||
}
|
||||
}
|
||||
for {
|
||||
evt, ok := e.dragger.Update(gtx.Metric, gtx.Source, gesture.Both)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
ev, ok := e.processPointerEvent(gtx, evt)
|
||||
if ok {
|
||||
return ev, ok
|
||||
for _, evt := range e.clickDragEvents(gtx) {
|
||||
switch evt := evt.(type) {
|
||||
case gesture.ClickEvent:
|
||||
switch {
|
||||
case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse,
|
||||
evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse:
|
||||
prevCaretPos, _ := e.text.Selection()
|
||||
e.blinkStart = gtx.Now
|
||||
e.text.MoveCoord(image.Point{
|
||||
X: int(math.Round(float64(evt.Position.X))),
|
||||
Y: int(math.Round(float64(evt.Position.Y))),
|
||||
})
|
||||
e.requestFocus = true
|
||||
if e.scroller.State() != gesture.StateFlinging {
|
||||
e.scrollCaret = true
|
||||
}
|
||||
|
||||
if evt.Modifiers == key.ModShift {
|
||||
start, end := e.text.Selection()
|
||||
// If they clicked closer to the end, then change the end to
|
||||
// where the caret used to be (effectively swapping start & end).
|
||||
if abs(end-start) < abs(start-prevCaretPos) {
|
||||
e.text.SetCaret(start, prevCaretPos)
|
||||
}
|
||||
} else {
|
||||
e.text.ClearSelection()
|
||||
}
|
||||
e.dragging = true
|
||||
|
||||
// Process multi-clicks.
|
||||
switch {
|
||||
case evt.NumClicks == 2:
|
||||
e.text.MoveWord(-1, selectionClear)
|
||||
e.text.MoveWord(1, selectionExtend)
|
||||
e.dragging = false
|
||||
case evt.NumClicks >= 3:
|
||||
e.text.MoveStart(selectionClear)
|
||||
e.text.MoveEnd(selectionExtend)
|
||||
e.dragging = false
|
||||
}
|
||||
}
|
||||
case pointer.Event:
|
||||
release := false
|
||||
switch {
|
||||
case evt.Kind == pointer.Release && evt.Source == pointer.Mouse:
|
||||
release = true
|
||||
fallthrough
|
||||
case evt.Kind == pointer.Drag && evt.Source == pointer.Mouse:
|
||||
if e.dragging {
|
||||
e.blinkStart = gtx.Now
|
||||
e.text.MoveCoord(image.Point{
|
||||
X: int(math.Round(float64(evt.Position.X))),
|
||||
Y: int(math.Round(float64(evt.Position.Y))),
|
||||
})
|
||||
e.scrollCaret = true
|
||||
|
||||
if release {
|
||||
e.dragging = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) {
|
||||
e.scroller.Stop()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (e *Editor) processPointerEvent(gtx layout.Context, ev event.Event) (EditorEvent, bool) {
|
||||
switch evt := ev.(type) {
|
||||
case gesture.ClickEvent:
|
||||
switch {
|
||||
case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse,
|
||||
evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse:
|
||||
prevCaretPos, _ := e.text.Selection()
|
||||
e.blinkStart = gtx.Now
|
||||
e.text.MoveCoord(image.Point{
|
||||
X: int(math.Round(float64(evt.Position.X))),
|
||||
Y: int(math.Round(float64(evt.Position.Y))),
|
||||
})
|
||||
gtx.Execute(key.FocusCmd{Tag: e})
|
||||
if !e.ReadOnly {
|
||||
gtx.Execute(key.SoftKeyboardCmd{Show: true})
|
||||
}
|
||||
if e.scroller.State() != gesture.StateFlinging {
|
||||
e.scrollCaret = true
|
||||
}
|
||||
|
||||
if evt.Modifiers == key.ModShift {
|
||||
start, end := e.text.Selection()
|
||||
// If they clicked closer to the end, then change the end to
|
||||
// where the caret used to be (effectively swapping start & end).
|
||||
if abs(end-start) < abs(start-prevCaretPos) {
|
||||
e.text.SetCaret(start, prevCaretPos)
|
||||
}
|
||||
} else {
|
||||
e.text.ClearSelection()
|
||||
}
|
||||
e.dragging = true
|
||||
|
||||
// Process multi-clicks.
|
||||
switch {
|
||||
case evt.NumClicks == 2:
|
||||
e.text.MoveWord(-1, selectionClear)
|
||||
e.text.MoveWord(1, selectionExtend)
|
||||
e.dragging = false
|
||||
case evt.NumClicks >= 3:
|
||||
e.text.MoveLineStart(selectionClear)
|
||||
e.text.MoveLineEnd(selectionExtend)
|
||||
e.dragging = false
|
||||
}
|
||||
}
|
||||
case pointer.Event:
|
||||
release := false
|
||||
switch {
|
||||
case evt.Kind == pointer.Release && evt.Source == pointer.Mouse:
|
||||
release = true
|
||||
fallthrough
|
||||
case evt.Kind == pointer.Drag && evt.Source == pointer.Mouse:
|
||||
if e.dragging {
|
||||
e.blinkStart = gtx.Now
|
||||
e.text.MoveCoord(image.Point{
|
||||
X: int(math.Round(float64(evt.Position.X))),
|
||||
Y: int(math.Round(float64(evt.Position.Y))),
|
||||
})
|
||||
e.scrollCaret = true
|
||||
|
||||
if release {
|
||||
e.dragging = false
|
||||
}
|
||||
}
|
||||
}
|
||||
func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event {
|
||||
var combinedEvents []event.Event
|
||||
for _, evt := range e.clicker.Update(gtx) {
|
||||
combinedEvents = append(combinedEvents, evt)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func condFilter(pred bool, f key.Filter) event.Filter {
|
||||
if pred {
|
||||
return f
|
||||
} else {
|
||||
return nil
|
||||
for _, evt := range e.dragger.Update(gtx.Metric, gtx, gesture.Both) {
|
||||
combinedEvents = append(combinedEvents, evt)
|
||||
}
|
||||
return combinedEvents
|
||||
}
|
||||
|
||||
func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
|
||||
func (e *Editor) processKey(gtx layout.Context) {
|
||||
if e.text.Changed() {
|
||||
return ChangeEvent{}, true
|
||||
}
|
||||
caret, _ := e.text.Selection()
|
||||
atBeginning := caret == 0
|
||||
atEnd := caret == e.text.Len()
|
||||
if gtx.Locale.Direction.Progression() != system.FromOrigin {
|
||||
atEnd, atBeginning = atBeginning, atEnd
|
||||
}
|
||||
filters := []event.Filter{
|
||||
key.FocusFilter{Target: e},
|
||||
transfer.TargetFilter{Target: e, Type: "application/text"},
|
||||
key.Filter{Focus: e, Name: key.NameEnter, Optional: key.ModShift},
|
||||
key.Filter{Focus: e, Name: key.NameReturn, Optional: key.ModShift},
|
||||
|
||||
key.Filter{Focus: e, Name: "Z", Required: key.ModShortcut, Optional: key.ModShift},
|
||||
key.Filter{Focus: e, Name: "C", Required: key.ModShortcut},
|
||||
key.Filter{Focus: e, Name: "V", Required: key.ModShortcut},
|
||||
key.Filter{Focus: e, Name: "X", Required: key.ModShortcut},
|
||||
key.Filter{Focus: e, Name: "A", Required: key.ModShortcut},
|
||||
|
||||
key.Filter{Focus: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift},
|
||||
key.Filter{Focus: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift},
|
||||
|
||||
key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShortcut | key.ModShift},
|
||||
key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShortcut | key.ModShift},
|
||||
key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift},
|
||||
key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift},
|
||||
condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}),
|
||||
condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}),
|
||||
condFilter(!atEnd, key.Filter{Focus: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}),
|
||||
condFilter(!atEnd, key.Filter{Focus: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}),
|
||||
e.events = append(e.events, ChangeEvent{})
|
||||
}
|
||||
// adjust keeps track of runes dropped because of MaxLen.
|
||||
var adjust int
|
||||
for {
|
||||
ke, ok := gtx.Event(filters...)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
for _, ke := range gtx.Events(&e.eventKey) {
|
||||
e.blinkStart = gtx.Now
|
||||
switch ke := ke.(type) {
|
||||
case key.FocusEvent:
|
||||
e.focused = ke.Focus
|
||||
// Reset IME state.
|
||||
e.ime.imeState = imeState{}
|
||||
if ke.Focus && !e.ReadOnly {
|
||||
gtx.Execute(key.SoftKeyboardCmd{Show: true})
|
||||
}
|
||||
case key.Event:
|
||||
if !gtx.Focused(e) || ke.State != key.Press {
|
||||
if !e.focused || ke.State != key.Press {
|
||||
break
|
||||
}
|
||||
if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
|
||||
if !ke.Modifiers.Contain(key.ModShift) {
|
||||
e.scratch = e.text.Text(e.scratch)
|
||||
return SubmitEvent{
|
||||
e.events = append(e.events, SubmitEvent{
|
||||
Text: string(e.scratch),
|
||||
}, true
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
e.command(gtx, ke)
|
||||
e.scrollCaret = true
|
||||
e.scroller.Stop()
|
||||
ev, ok := e.command(gtx, ke)
|
||||
if ok {
|
||||
return ev, ok
|
||||
}
|
||||
case key.SnippetEvent:
|
||||
e.updateSnippet(gtx, ke.Start, ke.End)
|
||||
case key.EditEvent:
|
||||
@@ -445,26 +369,19 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
|
||||
// Reset caret xoff.
|
||||
e.text.MoveCaret(0, 0)
|
||||
if submit {
|
||||
e.scratch = e.text.Text(e.scratch)
|
||||
submitEvent := SubmitEvent{
|
||||
Text: string(e.scratch),
|
||||
}
|
||||
if e.text.Changed() {
|
||||
e.pending = append(e.pending, submitEvent)
|
||||
return ChangeEvent{}, true
|
||||
e.events = append(e.events, ChangeEvent{})
|
||||
}
|
||||
return submitEvent, true
|
||||
e.scratch = e.text.Text(e.scratch)
|
||||
e.events = append(e.events, SubmitEvent{
|
||||
Text: string(e.scratch),
|
||||
})
|
||||
}
|
||||
// Complete a paste event, initiated by Shortcut-V in Editor.command().
|
||||
case transfer.DataEvent:
|
||||
case clipboard.Event:
|
||||
e.scrollCaret = true
|
||||
e.scroller.Stop()
|
||||
content, err := io.ReadAll(ke.Open())
|
||||
if err == nil {
|
||||
if e.Insert(string(content)) != 0 {
|
||||
return ChangeEvent{}, true
|
||||
}
|
||||
}
|
||||
e.Insert(ke.Text)
|
||||
case key.SelectionEvent:
|
||||
e.scrollCaret = true
|
||||
e.scroller.Stop()
|
||||
@@ -475,12 +392,11 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
|
||||
}
|
||||
}
|
||||
if e.text.Changed() {
|
||||
return ChangeEvent{}, true
|
||||
e.events = append(e.events, ChangeEvent{})
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
|
||||
func (e *Editor) command(gtx layout.Context, k key.Event) {
|
||||
direction := 1
|
||||
if gtx.Locale.Direction.Progression() == system.TowardOrigin {
|
||||
direction = -1
|
||||
@@ -496,17 +412,15 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
|
||||
// half is in Editor.processKey() under clipboard.Event.
|
||||
case "V":
|
||||
if !e.ReadOnly {
|
||||
gtx.Execute(clipboard.ReadCmd{Tag: e})
|
||||
clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops)
|
||||
}
|
||||
// Copy or Cut selection -- ignored if nothing selected.
|
||||
case "C", "X":
|
||||
e.scratch = e.text.SelectedText(e.scratch)
|
||||
if text := string(e.scratch); text != "" {
|
||||
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))})
|
||||
clipboard.WriteOp{Text: text}.Add(gtx.Ops)
|
||||
if k.Name == "X" && !e.ReadOnly {
|
||||
if e.Delete(1) != 0 {
|
||||
return ChangeEvent{}, true
|
||||
}
|
||||
e.Delete(1)
|
||||
}
|
||||
}
|
||||
// Select all
|
||||
@@ -515,51 +429,33 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
|
||||
case "Z":
|
||||
if !e.ReadOnly {
|
||||
if k.Modifiers.Contain(key.ModShift) {
|
||||
if ev, ok := e.redo(); ok {
|
||||
return ev, ok
|
||||
}
|
||||
e.redo()
|
||||
} else {
|
||||
if ev, ok := e.undo(); ok {
|
||||
return ev, ok
|
||||
}
|
||||
e.undo()
|
||||
}
|
||||
}
|
||||
case key.NameHome:
|
||||
e.text.MoveTextStart(selAct)
|
||||
case key.NameEnd:
|
||||
e.text.MoveTextEnd(selAct)
|
||||
}
|
||||
return nil, false
|
||||
return
|
||||
}
|
||||
switch k.Name {
|
||||
case key.NameReturn, key.NameEnter:
|
||||
if !e.ReadOnly {
|
||||
if e.Insert("\n") != 0 {
|
||||
return ChangeEvent{}, true
|
||||
}
|
||||
e.Insert("\n")
|
||||
}
|
||||
case key.NameDeleteBackward:
|
||||
if !e.ReadOnly {
|
||||
if moveByWord {
|
||||
if e.deleteWord(-1) != 0 {
|
||||
return ChangeEvent{}, true
|
||||
}
|
||||
e.deleteWord(-1)
|
||||
} else {
|
||||
if e.Delete(-1) != 0 {
|
||||
return ChangeEvent{}, true
|
||||
}
|
||||
e.Delete(-1)
|
||||
}
|
||||
}
|
||||
case key.NameDeleteForward:
|
||||
if !e.ReadOnly {
|
||||
if moveByWord {
|
||||
if e.deleteWord(1) != 0 {
|
||||
return ChangeEvent{}, true
|
||||
}
|
||||
e.deleteWord(1)
|
||||
} else {
|
||||
if e.Delete(1) != 0 {
|
||||
return ChangeEvent{}, true
|
||||
}
|
||||
e.Delete(1)
|
||||
}
|
||||
}
|
||||
case key.NameUpArrow:
|
||||
@@ -589,11 +485,20 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
|
||||
case key.NamePageDown:
|
||||
e.text.MovePages(+1, selAct)
|
||||
case key.NameHome:
|
||||
e.text.MoveLineStart(selAct)
|
||||
e.text.MoveStart(selAct)
|
||||
case key.NameEnd:
|
||||
e.text.MoveLineEnd(selAct)
|
||||
e.text.MoveEnd(selAct)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Focus requests the input focus for the Editor.
|
||||
func (e *Editor) Focus() {
|
||||
e.requestFocus = true
|
||||
}
|
||||
|
||||
// Focused returns whether the editor is focused or not.
|
||||
func (e *Editor) Focused() bool {
|
||||
return e.focused
|
||||
}
|
||||
|
||||
// initBuffer should be invoked first in every exported function that accesses
|
||||
@@ -612,51 +517,48 @@ func (e *Editor) initBuffer() {
|
||||
e.text.WrapPolicy = e.WrapPolicy
|
||||
}
|
||||
|
||||
// Update the state of the editor in response to input events. Update consumes editor
|
||||
// input events until there are no remaining events or an editor event is generated.
|
||||
// To fully update the state of the editor, callers should call Update until it returns
|
||||
// false.
|
||||
func (e *Editor) Update(gtx layout.Context) (EditorEvent, bool) {
|
||||
// Update the state of the editor in response to input events.
|
||||
func (e *Editor) Update(gtx layout.Context) {
|
||||
e.initBuffer()
|
||||
event, ok := e.processEvents(gtx)
|
||||
// Notify IME of selection if it changed.
|
||||
newSel := e.ime.selection
|
||||
start, end := e.text.Selection()
|
||||
newSel.rng = key.Range{
|
||||
Start: start,
|
||||
End: end,
|
||||
}
|
||||
caretPos, carAsc, carDesc := e.text.CaretInfo()
|
||||
newSel.caret = key.Caret{
|
||||
Pos: layout.FPt(caretPos),
|
||||
Ascent: float32(carAsc),
|
||||
Descent: float32(carDesc),
|
||||
}
|
||||
if newSel != e.ime.selection {
|
||||
e.ime.selection = newSel
|
||||
gtx.Execute(key.SelectionCmd{Tag: e, Range: newSel.rng, Caret: newSel.caret})
|
||||
}
|
||||
e.processEvents(gtx)
|
||||
if e.focused {
|
||||
// Notify IME of selection if it changed.
|
||||
newSel := e.ime.selection
|
||||
start, end := e.text.Selection()
|
||||
newSel.rng = key.Range{
|
||||
Start: start,
|
||||
End: end,
|
||||
}
|
||||
caretPos, carAsc, carDesc := e.text.CaretInfo()
|
||||
newSel.caret = key.Caret{
|
||||
Pos: layout.FPt(caretPos),
|
||||
Ascent: float32(carAsc),
|
||||
Descent: float32(carDesc),
|
||||
}
|
||||
if newSel != e.ime.selection {
|
||||
e.ime.selection = newSel
|
||||
key.SelectionOp{
|
||||
Tag: &e.eventKey,
|
||||
Range: newSel.rng,
|
||||
Caret: newSel.caret,
|
||||
}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
e.updateSnippet(gtx, e.ime.start, e.ime.end)
|
||||
return event, ok
|
||||
e.updateSnippet(gtx, e.ime.start, e.ime.end)
|
||||
}
|
||||
}
|
||||
|
||||
// Layout lays out the editor using the provided textMaterial as the paint material
|
||||
// for the text glyphs+caret and the selectMaterial as the paint material for the
|
||||
// selection rectangle.
|
||||
func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
|
||||
for {
|
||||
_, ok := e.Update(gtx)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
e.Update(gtx)
|
||||
|
||||
e.text.Layout(gtx, lt, font, size)
|
||||
return e.layout(gtx, textMaterial, selectMaterial)
|
||||
}
|
||||
|
||||
// updateSnippet queues a key.SnippetCmd if the snippet content or position
|
||||
// updateSnippet adds a key.SnippetOp if the snippet content or position
|
||||
// have changed. off and len are in runes.
|
||||
func (e *Editor) updateSnippet(gtx layout.Context, start, end int) {
|
||||
if start > end {
|
||||
@@ -696,7 +598,10 @@ func (e *Editor) updateSnippet(gtx layout.Context, start, end int) {
|
||||
return
|
||||
}
|
||||
e.ime.snippet = newSnip
|
||||
gtx.Execute(key.SnippetCmd{Tag: e, Snippet: newSnip})
|
||||
key.SnippetOp{
|
||||
Tag: &e.eventKey,
|
||||
Snippet: newSnip,
|
||||
}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
|
||||
@@ -707,35 +612,79 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
|
||||
e.scrollCaret = false
|
||||
e.text.ScrollToCaret()
|
||||
}
|
||||
textDims := e.text.FullDimensions()
|
||||
visibleDims := e.text.Dimensions()
|
||||
|
||||
defer clip.Rect(image.Rectangle{Max: visibleDims.Size}).Push(gtx.Ops).Pop()
|
||||
pointer.CursorText.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, e)
|
||||
key.InputHintOp{Tag: e, Hint: e.InputHint}.Add(gtx.Ops)
|
||||
var keys key.Set
|
||||
if e.focused {
|
||||
const keyFilterNoLeftUp = "(ShortAlt)-(Shift)-[→,↓]|(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z"
|
||||
const keyFilterNoRightDown = "(ShortAlt)-(Shift)-[←,↑]|(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z"
|
||||
const keyFilterNoArrows = "(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z"
|
||||
const keyFilterAllArrows = "(ShortAlt)-(Shift)-[←,→,↑,↓]|(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z"
|
||||
caret, _ := e.text.Selection()
|
||||
switch {
|
||||
case caret == 0 && caret == e.text.Len():
|
||||
keys = keyFilterNoArrows
|
||||
case caret == 0:
|
||||
if gtx.Locale.Direction.Progression() == system.FromOrigin {
|
||||
keys = keyFilterNoLeftUp
|
||||
} else {
|
||||
keys = keyFilterNoRightDown
|
||||
}
|
||||
case caret == e.text.Len():
|
||||
if gtx.Locale.Direction.Progression() == system.FromOrigin {
|
||||
keys = keyFilterNoRightDown
|
||||
} else {
|
||||
keys = keyFilterNoLeftUp
|
||||
}
|
||||
default:
|
||||
keys = keyFilterAllArrows
|
||||
}
|
||||
}
|
||||
key.InputOp{Tag: &e.eventKey, Hint: e.InputHint, Keys: keys}.Add(gtx.Ops)
|
||||
if e.requestFocus {
|
||||
key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops)
|
||||
key.SoftKeyboardOp{Show: true}.Add(gtx.Ops)
|
||||
}
|
||||
e.requestFocus = false
|
||||
|
||||
e.scroller.Add(gtx.Ops)
|
||||
var scrollRange image.Rectangle
|
||||
if e.SingleLine {
|
||||
scrollOffX := e.text.ScrollOff().X
|
||||
scrollRange.Min.X = min(-scrollOffX, 0)
|
||||
scrollRange.Max.X = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
|
||||
} else {
|
||||
scrollOffY := e.text.ScrollOff().Y
|
||||
scrollRange.Min.Y = -scrollOffY
|
||||
scrollRange.Max.Y = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y))
|
||||
}
|
||||
e.scroller.Add(gtx.Ops, scrollRange)
|
||||
|
||||
e.clicker.Add(gtx.Ops)
|
||||
e.dragger.Add(gtx.Ops)
|
||||
e.showCaret = false
|
||||
if gtx.Focused(e) {
|
||||
if e.focused {
|
||||
now := gtx.Now
|
||||
dt := now.Sub(e.blinkStart)
|
||||
blinking := dt < maxBlinkDuration
|
||||
const timePerBlink = time.Second / blinksPerSecond
|
||||
nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2))
|
||||
if blinking {
|
||||
gtx.Execute(op.InvalidateCmd{At: nextBlink})
|
||||
redraw := op.InvalidateOp{At: nextBlink}
|
||||
redraw.Add(gtx.Ops)
|
||||
}
|
||||
e.showCaret = !blinking || dt%timePerBlink < timePerBlink/2
|
||||
e.showCaret = e.focused && (!blinking || dt%timePerBlink < timePerBlink/2)
|
||||
}
|
||||
disabled := gtx.Queue == nil
|
||||
|
||||
semantic.Editor.Add(gtx.Ops)
|
||||
if e.Len() > 0 {
|
||||
e.paintSelection(gtx, selectMaterial)
|
||||
e.paintText(gtx, textMaterial)
|
||||
}
|
||||
if gtx.Enabled() {
|
||||
if !disabled {
|
||||
e.paintCaret(gtx, textMaterial)
|
||||
}
|
||||
return visibleDims
|
||||
@@ -745,7 +694,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
|
||||
// material to set the painting material for the selection.
|
||||
func (e *Editor) paintSelection(gtx layout.Context, material op.CallOp) {
|
||||
e.initBuffer()
|
||||
if !gtx.Focused(e) {
|
||||
if !e.focused {
|
||||
return
|
||||
}
|
||||
e.text.PaintSelection(gtx, material)
|
||||
@@ -809,10 +758,10 @@ func (e *Editor) CaretCoords() f32.Point {
|
||||
//
|
||||
// If there is a selection, it is deleted and counts as a single grapheme
|
||||
// cluster.
|
||||
func (e *Editor) Delete(graphemeClusters int) (deletedRunes int) {
|
||||
func (e *Editor) Delete(graphemeClusters int) {
|
||||
e.initBuffer()
|
||||
if graphemeClusters == 0 {
|
||||
return 0
|
||||
return
|
||||
}
|
||||
|
||||
start, end := e.text.Selection()
|
||||
@@ -828,10 +777,9 @@ func (e *Editor) Delete(graphemeClusters int) (deletedRunes int) {
|
||||
// Reset xoff.
|
||||
e.text.MoveCaret(0, 0)
|
||||
e.ClearSelection()
|
||||
return end - start
|
||||
}
|
||||
|
||||
func (e *Editor) Insert(s string) (insertedRunes int) {
|
||||
func (e *Editor) Insert(s string) {
|
||||
e.initBuffer()
|
||||
if e.SingleLine {
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
@@ -845,7 +793,6 @@ func (e *Editor) Insert(s string) (insertedRunes int) {
|
||||
e.text.MoveCaret(0, 0)
|
||||
e.SetCaret(start+moves, start+moves)
|
||||
e.scrollCaret = true
|
||||
return moves
|
||||
}
|
||||
|
||||
// modification represents a change to the contents of the editor buffer.
|
||||
@@ -865,10 +812,10 @@ type modification struct {
|
||||
|
||||
// undo applies the modification at e.history[e.historyIdx] and decrements
|
||||
// e.historyIdx.
|
||||
func (e *Editor) undo() (EditorEvent, bool) {
|
||||
func (e *Editor) undo() {
|
||||
e.initBuffer()
|
||||
if len(e.history) < 1 || e.nextHistoryIdx == 0 {
|
||||
return nil, false
|
||||
return
|
||||
}
|
||||
mod := e.history[e.nextHistoryIdx-1]
|
||||
replaceEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent)
|
||||
@@ -876,15 +823,14 @@ func (e *Editor) undo() (EditorEvent, bool) {
|
||||
caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent)
|
||||
e.SetCaret(caretEnd, mod.StartRune)
|
||||
e.nextHistoryIdx--
|
||||
return ChangeEvent{}, true
|
||||
}
|
||||
|
||||
// redo applies the modification at e.history[e.historyIdx] and increments
|
||||
// e.historyIdx.
|
||||
func (e *Editor) redo() (EditorEvent, bool) {
|
||||
func (e *Editor) redo() {
|
||||
e.initBuffer()
|
||||
if len(e.history) < 1 || e.nextHistoryIdx == len(e.history) {
|
||||
return nil, false
|
||||
return
|
||||
}
|
||||
mod := e.history[e.nextHistoryIdx]
|
||||
end := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent)
|
||||
@@ -892,7 +838,6 @@ func (e *Editor) redo() (EditorEvent, bool) {
|
||||
caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent)
|
||||
e.SetCaret(caretEnd, mod.StartRune)
|
||||
e.nextHistoryIdx++
|
||||
return ChangeEvent{}, true
|
||||
}
|
||||
|
||||
// replace the text between start and end with s. Indices are in runes.
|
||||
@@ -976,18 +921,18 @@ func (e *Editor) MoveCaret(startDelta, endDelta int) {
|
||||
// Positive is forward, negative is backward.
|
||||
// Absolute values greater than one will delete that many words.
|
||||
// The selection counts as a single word.
|
||||
func (e *Editor) deleteWord(distance int) (deletedRunes int) {
|
||||
func (e *Editor) deleteWord(distance int) {
|
||||
if distance == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
start, end := e.text.Selection()
|
||||
if start != end {
|
||||
deletedRunes = e.Delete(1)
|
||||
e.Delete(1)
|
||||
distance -= sign(distance)
|
||||
}
|
||||
if distance == 0 {
|
||||
return deletedRunes
|
||||
return
|
||||
}
|
||||
|
||||
// split the distance information into constituent parts to be
|
||||
@@ -1027,8 +972,7 @@ func (e *Editor) deleteWord(distance int) (deletedRunes int) {
|
||||
runes += 1
|
||||
}
|
||||
}
|
||||
deletedRunes += e.Delete(runes * direction)
|
||||
return deletedRunes
|
||||
e.Delete(runes * direction)
|
||||
}
|
||||
|
||||
// SelectionLen returns the length of the selection, in runes; it is
|
||||
|
||||
+97
-204
@@ -20,7 +20,7 @@ import (
|
||||
"gioui.org/font"
|
||||
"gioui.org/font/gofont"
|
||||
"gioui.org/font/opentype"
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/system"
|
||||
@@ -96,14 +96,17 @@ func assertContents(t *testing.T, e *Editor, contents string, selectionStart, se
|
||||
// TestEditorReadOnly ensures that mouse and keyboard interactions with readonly
|
||||
// editors do nothing but manipulate the text selection.
|
||||
func TestEditorReadOnly(t *testing.T) {
|
||||
r := new(input.Router)
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Constraints: layout.Constraints{
|
||||
Max: image.Pt(100, 100),
|
||||
},
|
||||
Locale: english,
|
||||
Source: r.Source(),
|
||||
}
|
||||
gtx.Queue = &testQueue{
|
||||
events: []event.Event{
|
||||
key.FocusEvent{Focus: true},
|
||||
},
|
||||
}
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
@@ -115,23 +118,12 @@ func TestEditorReadOnly(t *testing.T) {
|
||||
if cStart != cEnd {
|
||||
t.Errorf("unexpected initial caret positions")
|
||||
}
|
||||
gtx.Execute(key.FocusCmd{Tag: e})
|
||||
layoutEditor := func() layout.Dimensions {
|
||||
return e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
}
|
||||
layoutEditor()
|
||||
r.Frame(gtx.Ops)
|
||||
gtx.Ops.Reset()
|
||||
layoutEditor()
|
||||
r.Frame(gtx.Ops)
|
||||
gtx.Ops.Reset()
|
||||
layoutEditor()
|
||||
r.Frame(gtx.Ops)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
// Select everything.
|
||||
gtx.Ops.Reset()
|
||||
r.Queue(key.Event{Name: "A", Modifiers: key.ModShortcut})
|
||||
layoutEditor()
|
||||
gtx.Queue = &testQueue{events: []event.Event{key.Event{Name: "A", Modifiers: key.ModShortcut}}}
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
textContent := e.Text()
|
||||
cStart2, cEnd2 := e.Selection()
|
||||
if cStart2 > cEnd2 {
|
||||
@@ -146,7 +138,7 @@ func TestEditorReadOnly(t *testing.T) {
|
||||
|
||||
// Type some new characters.
|
||||
gtx.Ops.Reset()
|
||||
r.Queue(key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"})
|
||||
gtx.Queue = &testQueue{events: []event.Event{key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"}}}
|
||||
e.Update(gtx)
|
||||
textContent2 := e.Text()
|
||||
if textContent2 != textContent {
|
||||
@@ -155,8 +147,8 @@ func TestEditorReadOnly(t *testing.T) {
|
||||
|
||||
// Try to delete selection.
|
||||
gtx.Ops.Reset()
|
||||
r.Queue(key.Event{Name: key.NameDeleteBackward})
|
||||
dims := layoutEditor()
|
||||
gtx.Queue = &testQueue{events: []event.Event{key.Event{Name: key.NameDeleteBackward}}}
|
||||
dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
textContent2 = e.Text()
|
||||
if textContent2 != textContent {
|
||||
t.Errorf("readonly editor modified by delete key.Event")
|
||||
@@ -165,14 +157,14 @@ func TestEditorReadOnly(t *testing.T) {
|
||||
// Click and drag from the middle of the first line
|
||||
// to the center.
|
||||
gtx.Ops.Reset()
|
||||
r.Queue(
|
||||
gtx.Queue = &testQueue{events: []event.Event{
|
||||
pointer.Event{
|
||||
Kind: pointer.Press,
|
||||
Buttons: pointer.ButtonPrimary,
|
||||
Position: f32.Pt(float32(dims.Size.X)*.5, 5),
|
||||
},
|
||||
pointer.Event{
|
||||
Kind: pointer.Move,
|
||||
Kind: pointer.Drag,
|
||||
Buttons: pointer.ButtonPrimary,
|
||||
Position: layout.FPt(dims.Size).Mul(.5),
|
||||
},
|
||||
@@ -181,7 +173,7 @@ func TestEditorReadOnly(t *testing.T) {
|
||||
Buttons: pointer.ButtonPrimary,
|
||||
Position: layout.FPt(dims.Size).Mul(.5),
|
||||
},
|
||||
)
|
||||
}}
|
||||
e.Update(gtx)
|
||||
cStart3, cEnd3 := e.Selection()
|
||||
if cStart3 == cStart2 || cEnd3 == cEnd2 {
|
||||
@@ -256,7 +248,7 @@ func TestEditor(t *testing.T) {
|
||||
// Regression test for bad in-cluster rune offset math.
|
||||
e.SetText("æbc")
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
e.text.MoveLineEnd(selectionClear)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 0, 3, len("æbc"))
|
||||
|
||||
textSample := "æbc\naøå••"
|
||||
@@ -268,7 +260,7 @@ func TestEditor(t *testing.T) {
|
||||
}
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
assertCaret(t, e, 0, 0, 0)
|
||||
e.text.MoveLineEnd(selectionClear)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 0, 3, len("æbc"))
|
||||
e.MoveCaret(+1, +1)
|
||||
assertCaret(t, e, 1, 0, len("æbc\n"))
|
||||
@@ -276,7 +268,7 @@ func TestEditor(t *testing.T) {
|
||||
assertCaret(t, e, 0, 3, len("æbc"))
|
||||
e.text.MoveLines(+1, selectionClear)
|
||||
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
|
||||
e.text.MoveLineEnd(selectionClear)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 1, 5, len("æbc\naøå••"))
|
||||
e.MoveCaret(+1, +1)
|
||||
assertCaret(t, e, 1, 5, len("æbc\naøå••"))
|
||||
@@ -300,7 +292,7 @@ func TestEditor(t *testing.T) {
|
||||
// Test that moveLine applies x offsets from previous moves.
|
||||
e.SetText("long line\nshort")
|
||||
e.SetCaret(0, 0)
|
||||
e.text.MoveLineEnd(selectionClear)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
e.text.MoveLines(+1, selectionClear)
|
||||
e.text.MoveLines(-1, selectionClear)
|
||||
assertCaret(t, e, 0, utf8.RuneCountInString("long line"), len("long line"))
|
||||
@@ -342,14 +334,14 @@ func TestEditorRTL(t *testing.T) {
|
||||
e.MoveCaret(+1, +1)
|
||||
assertCaret(t, e, 0, 3, len("الح"))
|
||||
// Move to the "end" of the line. This moves to the left edge of the line.
|
||||
e.text.MoveLineEnd(selectionClear)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 0, 4, len("الحب"))
|
||||
|
||||
sentence := "الحب سماء لا\nتمط غير الأحلام"
|
||||
e.SetText(sentence)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
assertCaret(t, e, 0, 0, 0)
|
||||
e.text.MoveLineEnd(selectionClear)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 0, 12, len("الحب سماء لا"))
|
||||
e.MoveCaret(+1, +1)
|
||||
assertCaret(t, e, 1, 0, len("الحب سماء لا\n"))
|
||||
@@ -361,7 +353,7 @@ func TestEditorRTL(t *testing.T) {
|
||||
assertCaret(t, e, 0, 12, len("الحب سماء لا"))
|
||||
e.text.MoveLines(+1, selectionClear)
|
||||
assertCaret(t, e, 1, 14, len("الحب سماء لا\nتمط غير الأحلا"))
|
||||
e.text.MoveLineEnd(selectionClear)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
|
||||
e.MoveCaret(+1, +1)
|
||||
assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
|
||||
@@ -417,7 +409,7 @@ func TestEditorLigature(t *testing.T) {
|
||||
assertCaret(t, e, 0, 0, 0)
|
||||
e.SetText("fl") // just a ligature
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
e.text.MoveLineEnd(selectionClear)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 0, 2, len("fl"))
|
||||
e.MoveCaret(-1, -1)
|
||||
assertCaret(t, e, 0, 1, len("f"))
|
||||
@@ -428,7 +420,7 @@ func TestEditorLigature(t *testing.T) {
|
||||
e.SetText("flaffl•ffi\n•fflfi") // 3 ligatures on line 0, 2 on line 1
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
assertCaret(t, e, 0, 0, 0)
|
||||
e.text.MoveLineEnd(selectionClear)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
assertCaret(t, e, 0, 10, len("ffaffl•ffi"))
|
||||
e.MoveCaret(+1, +1)
|
||||
assertCaret(t, e, 1, 0, len("ffaffl•ffi\n"))
|
||||
@@ -481,7 +473,7 @@ func TestEditorLigature(t *testing.T) {
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
// Ensure that all runes in the final cluster of a line are properly
|
||||
// decoded when moving to the end of the line. This is a regression test.
|
||||
e.text.MoveLineEnd(selectionClear)
|
||||
e.text.MoveEnd(selectionClear)
|
||||
// The first line was broken by line wrapping, not a newline character, and has a trailing
|
||||
// whitespace. However, we should never be able to reach the "other side" of such a trailing
|
||||
// whitespace glyph.
|
||||
@@ -504,22 +496,22 @@ func TestEditorLigature(t *testing.T) {
|
||||
|
||||
func TestEditorDimensions(t *testing.T) {
|
||||
e := new(Editor)
|
||||
r := new(input.Router)
|
||||
tq := &testQueue{
|
||||
events: []event.Event{
|
||||
key.EditEvent{Text: "A"},
|
||||
},
|
||||
}
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Constraints: layout.Constraints{Max: image.Pt(100, 100)},
|
||||
Source: r.Source(),
|
||||
Queue: tq,
|
||||
Locale: english,
|
||||
}
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
gtx.Execute(key.FocusCmd{Tag: e})
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
r.Frame(gtx.Ops)
|
||||
r.Queue(key.EditEvent{Text: "A"})
|
||||
dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
if dims.Size.X < 5 {
|
||||
if dims.Size.X == 0 {
|
||||
t.Errorf("EditEvent was not reflected in Editor width")
|
||||
}
|
||||
}
|
||||
@@ -548,10 +540,8 @@ const (
|
||||
moveRune
|
||||
moveLine
|
||||
movePage
|
||||
moveTextStart
|
||||
moveTextEnd
|
||||
moveLineStart
|
||||
moveLineEnd
|
||||
moveStart
|
||||
moveEnd
|
||||
moveCoord
|
||||
moveWord
|
||||
deleteWord
|
||||
@@ -601,14 +591,10 @@ func TestEditorCaretConsistency(t *testing.T) {
|
||||
e.text.MoveLines(int(distance), selectionClear)
|
||||
case movePage:
|
||||
e.text.MovePages(int(distance), selectionClear)
|
||||
case moveLineStart:
|
||||
e.text.MoveLineStart(selectionClear)
|
||||
case moveLineEnd:
|
||||
e.text.MoveLineEnd(selectionClear)
|
||||
case moveTextStart:
|
||||
e.text.MoveTextStart(selectionClear)
|
||||
case moveTextEnd:
|
||||
e.text.MoveTextEnd(selectionClear)
|
||||
case moveStart:
|
||||
e.text.MoveStart(selectionClear)
|
||||
case moveEnd:
|
||||
e.text.MoveEnd(selectionClear)
|
||||
case moveCoord:
|
||||
e.text.MoveCoord(image.Pt(int(x), int(y)))
|
||||
case moveWord:
|
||||
@@ -885,7 +871,7 @@ func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
// to make it much narrower (which makes the lines in the editor reflow), and
|
||||
// then verifies that the updated (col, line) positions of the selected text
|
||||
// are where we expect.
|
||||
func TestEditorSelectReflow(t *testing.T) {
|
||||
func TestEditorSelect(t *testing.T) {
|
||||
e := new(Editor)
|
||||
e.SetText(`a 2 4 6 8 a
|
||||
b 2 4 6 8 b
|
||||
@@ -896,11 +882,9 @@ f 2 4 6 8 f
|
||||
g 2 4 6 8 g
|
||||
`)
|
||||
|
||||
r := new(input.Router)
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Locale: english,
|
||||
Source: r.Source(),
|
||||
}
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
font := font.Font{}
|
||||
@@ -908,39 +892,42 @@ g 2 4 6 8 g
|
||||
|
||||
var tim time.Duration
|
||||
selected := func(start, end int) string {
|
||||
gtx.Execute(key.FocusCmd{Tag: e})
|
||||
// Layout once with no events; populate e.lines.
|
||||
gtx.Queue = nil
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
_ = e.Events() // throw away any events from this layout
|
||||
|
||||
r.Frame(gtx.Ops)
|
||||
gtx.Source = r.Source()
|
||||
// Build the selection events
|
||||
startPos := e.text.closestToRune(start)
|
||||
endPos := e.text.closestToRune(end)
|
||||
r.Queue(
|
||||
pointer.Event{
|
||||
Buttons: pointer.ButtonPrimary,
|
||||
Kind: pointer.Press,
|
||||
Source: pointer.Mouse,
|
||||
Time: tim,
|
||||
Position: f32.Pt(textWidth(e, startPos.lineCol.line, 0, startPos.lineCol.col), textBaseline(e, startPos.lineCol.line)),
|
||||
tq := &testQueue{
|
||||
events: []event.Event{
|
||||
pointer.Event{
|
||||
Buttons: pointer.ButtonPrimary,
|
||||
Kind: pointer.Press,
|
||||
Source: pointer.Mouse,
|
||||
Time: tim,
|
||||
Position: f32.Pt(textWidth(e, startPos.lineCol.line, 0, startPos.lineCol.col), textBaseline(e, startPos.lineCol.line)),
|
||||
},
|
||||
pointer.Event{
|
||||
Kind: pointer.Release,
|
||||
Source: pointer.Mouse,
|
||||
Time: tim,
|
||||
Position: f32.Pt(textWidth(e, endPos.lineCol.line, 0, endPos.lineCol.col), textBaseline(e, endPos.lineCol.line)),
|
||||
},
|
||||
},
|
||||
pointer.Event{
|
||||
Kind: pointer.Release,
|
||||
Source: pointer.Mouse,
|
||||
Time: tim,
|
||||
Position: f32.Pt(textWidth(e, endPos.lineCol.line, 0, endPos.lineCol.col), textBaseline(e, endPos.lineCol.line)),
|
||||
},
|
||||
)
|
||||
}
|
||||
tim += time.Second // Avoid multi-clicks.
|
||||
gtx.Queue = tq
|
||||
|
||||
for {
|
||||
_, ok := e.Update(gtx) // throw away any events from this layout
|
||||
if !ok {
|
||||
break
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
for _, evt := range e.Events() {
|
||||
switch evt.(type) {
|
||||
case SelectEvent:
|
||||
return e.SelectedText()
|
||||
}
|
||||
}
|
||||
return e.SelectedText()
|
||||
return ""
|
||||
}
|
||||
type screenPos image.Point
|
||||
logicalPosMatch := func(t *testing.T, n int, label string, expected screenPos, actual combinedPos) {
|
||||
@@ -978,7 +965,7 @@ g 2 4 6 8 g
|
||||
// Constrain the editor to roughly 6 columns wide and redraw
|
||||
gtx.Constraints = layout.Exact(image.Pt(36, 36))
|
||||
// Keep existing selection
|
||||
gtx = gtx.Disabled()
|
||||
gtx.Queue = nil
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
caretStart := e.text.closestToRune(e.text.caret.start)
|
||||
@@ -988,89 +975,24 @@ g 2 4 6 8 g
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorSelectShortcuts(t *testing.T) {
|
||||
tFont := font.Font{}
|
||||
tFontSize := unit.Sp(10)
|
||||
tShaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
var tEditor = &Editor{
|
||||
SingleLine: false,
|
||||
ReadOnly: true,
|
||||
}
|
||||
lines := "abc abc abc\ndef def def\nghi ghi ghi"
|
||||
tEditor.SetText(lines)
|
||||
type testCase struct {
|
||||
// Initial text selection.
|
||||
startPos, endPos int
|
||||
// Keyboard shortcut to execute.
|
||||
keyEvent key.Event
|
||||
// Expected text selection.
|
||||
selection string
|
||||
}
|
||||
|
||||
pos1, pos2 := 14, 21
|
||||
for n, tst := range []testCase{
|
||||
{pos1, pos2, key.Event{Name: "A", Modifiers: key.ModShortcut}, lines},
|
||||
{pos2, pos1, key.Event{Name: "A", Modifiers: key.ModShortcut}, lines},
|
||||
{pos1, pos2, key.Event{Name: key.NameHome, Modifiers: key.ModShift}, "def def d"},
|
||||
{pos1, pos2, key.Event{Name: key.NameEnd, Modifiers: key.ModShift}, "ef"},
|
||||
{pos2, pos1, key.Event{Name: key.NameHome, Modifiers: key.ModShift}, "de"},
|
||||
{pos2, pos1, key.Event{Name: key.NameEnd, Modifiers: key.ModShift}, "f def def"},
|
||||
{pos1, pos2, key.Event{Name: key.NameHome, Modifiers: key.ModShortcut | key.ModShift}, "abc abc abc\ndef def d"},
|
||||
{pos1, pos2, key.Event{Name: key.NameEnd, Modifiers: key.ModShortcut | key.ModShift}, "ef\nghi ghi ghi"},
|
||||
{pos2, pos1, key.Event{Name: key.NameHome, Modifiers: key.ModShortcut | key.ModShift}, "abc abc abc\nde"},
|
||||
{pos2, pos1, key.Event{Name: key.NameEnd, Modifiers: key.ModShortcut | key.ModShift}, "f def def\nghi ghi ghi"},
|
||||
} {
|
||||
tRouter := new(input.Router)
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Locale: english,
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Source: tRouter.Source(),
|
||||
}
|
||||
gtx.Execute(key.FocusCmd{Tag: tEditor})
|
||||
tEditor.Layout(gtx, tShaper, tFont, tFontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
tEditor.SetCaret(tst.startPos, tst.endPos)
|
||||
if cStart, cEnd := tEditor.Selection(); cStart != tst.startPos || cEnd != tst.endPos {
|
||||
t.Errorf("TestEditorSelect %d: initial selection", n)
|
||||
}
|
||||
tRouter.Queue(tst.keyEvent)
|
||||
tEditor.Update(gtx)
|
||||
if got := tEditor.SelectedText(); got != tst.selection {
|
||||
t.Errorf("TestEditorSelect %d: Expected %q, got %q", n, tst.selection, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that an existing selection is dismissed when you press arrow keys.
|
||||
func TestSelectMove(t *testing.T) {
|
||||
e := new(Editor)
|
||||
e.SetText(`0123456789`)
|
||||
|
||||
r := new(input.Router)
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Locale: english,
|
||||
Source: r.Source(),
|
||||
}
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
font := font.Font{}
|
||||
fontSize := unit.Sp(10)
|
||||
|
||||
// Layout once to populate e.lines and get focus.
|
||||
gtx.Execute(key.FocusCmd{Tag: e})
|
||||
gtx.Queue = newQueue(key.FocusEvent{Focus: true})
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
r.Frame(gtx.Ops)
|
||||
// Set up selecton so the Editor key handler filters for all 4 directional keys.
|
||||
e.SetCaret(3, 6)
|
||||
gtx.Ops.Reset()
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
r.Frame(gtx.Ops)
|
||||
gtx.Ops.Reset()
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
r.Frame(gtx.Ops)
|
||||
|
||||
for _, keyName := range []key.Name{key.NameLeftArrow, key.NameRightArrow, key.NameUpArrow, key.NameDownArrow} {
|
||||
testKey := func(keyName string) {
|
||||
// Select 345
|
||||
e.SetCaret(3, 6)
|
||||
if expected, got := "345", e.SelectedText(); expected != got {
|
||||
@@ -1078,15 +1000,18 @@ func TestSelectMove(t *testing.T) {
|
||||
}
|
||||
|
||||
// Press the key
|
||||
r.Queue(key.Event{State: key.Press, Name: keyName})
|
||||
gtx.Ops.Reset()
|
||||
gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName})
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
r.Frame(gtx.Ops)
|
||||
|
||||
if expected, got := "", e.SelectedText(); expected != got {
|
||||
t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
testKey(key.NameLeftArrow)
|
||||
testKey(key.NameRightArrow)
|
||||
testKey(key.NameUpArrow)
|
||||
testKey(key.NameDownArrow)
|
||||
}
|
||||
|
||||
func TestEditor_Read(t *testing.T) {
|
||||
@@ -1139,22 +1064,17 @@ func TestEditor_MaxLen(t *testing.T) {
|
||||
}
|
||||
|
||||
e.SetText("2345678")
|
||||
r := new(input.Router)
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Source: r.Source(),
|
||||
Queue: newQueue(
|
||||
key.EditEvent{Range: key.Range{Start: 0, End: 2}, Text: "1234"},
|
||||
key.SelectionEvent{Start: 4, End: 4},
|
||||
),
|
||||
}
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
gtx.Execute(key.FocusCmd{Tag: e})
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
r.Frame(gtx.Ops)
|
||||
r.Queue(
|
||||
key.EditEvent{Range: key.Range{Start: 0, End: 2}, Text: "1234"},
|
||||
key.SelectionEvent{Start: 4, End: 4},
|
||||
)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
if got, want := e.Text(), "12345678"; got != want {
|
||||
@@ -1175,22 +1095,17 @@ func TestEditor_Filter(t *testing.T) {
|
||||
}
|
||||
|
||||
e.SetText("2345678")
|
||||
r := new(input.Router)
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Source: r.Source(),
|
||||
Queue: newQueue(
|
||||
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1"},
|
||||
key.SelectionEvent{Start: 4, End: 4},
|
||||
),
|
||||
}
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
gtx.Execute(key.FocusCmd{Tag: e})
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
r.Frame(gtx.Ops)
|
||||
r.Queue(
|
||||
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1"},
|
||||
key.SelectionEvent{Start: 4, End: 4},
|
||||
)
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
|
||||
if got, want := e.Text(), "12345678"; got != want {
|
||||
@@ -1205,33 +1120,22 @@ func TestEditor_Submit(t *testing.T) {
|
||||
e := new(Editor)
|
||||
e.Submit = true
|
||||
|
||||
r := new(input.Router)
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Source: r.Source(),
|
||||
Queue: newQueue(
|
||||
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"},
|
||||
),
|
||||
}
|
||||
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
|
||||
fontSize := unit.Sp(10)
|
||||
font := font.Font{}
|
||||
gtx.Execute(key.FocusCmd{Tag: e})
|
||||
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
|
||||
r.Frame(gtx.Ops)
|
||||
r.Queue(
|
||||
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"},
|
||||
)
|
||||
|
||||
got := []EditorEvent{}
|
||||
for {
|
||||
ev, ok := e.Update(gtx)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
got = append(got, ev)
|
||||
}
|
||||
if got, want := e.Text(), "ab1"; got != want {
|
||||
t.Errorf("editor failed to filter newline")
|
||||
}
|
||||
got := e.Events()
|
||||
want := []EditorEvent{
|
||||
ChangeEvent{},
|
||||
SubmitEvent{Text: e.Text()},
|
||||
@@ -1241,29 +1145,6 @@ func TestEditor_Submit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoFilterAllocs(t *testing.T) {
|
||||
b := testing.Benchmark(func(b *testing.B) {
|
||||
r := new(input.Router)
|
||||
e := new(Editor)
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Constraints: layout.Constraints{
|
||||
Max: image.Pt(100, 100),
|
||||
},
|
||||
Locale: english,
|
||||
Source: r.Source(),
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
e.Update(gtx)
|
||||
}
|
||||
})
|
||||
if allocs := b.AllocsPerOp(); allocs != 0 {
|
||||
t.Fatalf("expected 0 AllocsPerOp, got %d", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
// textWidth is a text helper for building simple selection events.
|
||||
// It assumes single-run lines, which isn't safe with non-test text
|
||||
// data.
|
||||
@@ -1283,3 +1164,15 @@ func textBaseline(e *Editor, lineNum int) float32 {
|
||||
start := e.text.closestToLineCol(lineNum, 0)
|
||||
return float32(start.y)
|
||||
}
|
||||
|
||||
type testQueue struct {
|
||||
events []event.Event
|
||||
}
|
||||
|
||||
func newQueue(e ...event.Event) *testQueue {
|
||||
return &testQueue{events: e}
|
||||
}
|
||||
|
||||
func (q *testQueue) Events(_ event.Tag) []event.Event {
|
||||
return q.events
|
||||
}
|
||||
|
||||
+10
-20
@@ -4,7 +4,6 @@ package widget
|
||||
|
||||
import (
|
||||
"gioui.org/gesture"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/semantic"
|
||||
@@ -41,21 +40,17 @@ func (e *Enum) index(k string) *enumKey {
|
||||
|
||||
// Update the state and report whether Value has changed by user interaction.
|
||||
func (e *Enum) Update(gtx layout.Context) bool {
|
||||
if !gtx.Enabled() {
|
||||
if gtx.Queue == nil {
|
||||
e.focused = false
|
||||
}
|
||||
e.hovering = false
|
||||
changed := false
|
||||
for _, state := range e.keys {
|
||||
for {
|
||||
ev, ok := state.click.Update(gtx.Source)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
for _, ev := range state.click.Update(gtx) {
|
||||
switch ev.Kind {
|
||||
case gesture.KindPress:
|
||||
if ev.Source == pointer.Mouse {
|
||||
gtx.Execute(key.FocusCmd{Tag: &state.tag})
|
||||
key.FocusOp{Tag: &state.tag}.Add(gtx.Ops)
|
||||
}
|
||||
case gesture.KindClick:
|
||||
if state.key != e.Value {
|
||||
@@ -64,15 +59,7 @@ func (e *Enum) Update(gtx layout.Context) bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
for {
|
||||
ev, ok := gtx.Event(
|
||||
key.FocusFilter{Target: &state.tag},
|
||||
key.Filter{Focus: &state.tag, Name: key.NameReturn},
|
||||
key.Filter{Focus: &state.tag, Name: key.NameSpace},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
for _, ev := range gtx.Events(&state.tag) {
|
||||
switch ev := ev.(type) {
|
||||
case key.FocusEvent:
|
||||
if ev.Focus {
|
||||
@@ -82,7 +69,7 @@ func (e *Enum) Update(gtx layout.Context) bool {
|
||||
e.focused = false
|
||||
}
|
||||
case key.Event:
|
||||
if ev.State != key.Release {
|
||||
if !e.focused || ev.State != key.Release {
|
||||
break
|
||||
}
|
||||
if ev.Name != key.NameReturn && ev.Name != key.NameSpace {
|
||||
@@ -130,9 +117,12 @@ func (e *Enum) Layout(gtx layout.Context, k string, content layout.Widget) layou
|
||||
}
|
||||
clk := &state.click
|
||||
clk.Add(gtx.Ops)
|
||||
event.Op(gtx.Ops, &state.tag)
|
||||
enabled := gtx.Queue != nil
|
||||
if enabled {
|
||||
key.InputOp{Tag: &state.tag, Keys: "⏎|Space"}.Add(gtx.Ops)
|
||||
}
|
||||
semantic.SelectedOp(k == e.Value).Add(gtx.Ops)
|
||||
semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops)
|
||||
semantic.EnabledOp(enabled).Add(gtx.Ops)
|
||||
c.Add(gtx.Ops)
|
||||
|
||||
return dims
|
||||
|
||||
+20
-20
@@ -5,13 +5,10 @@ package widget_test
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/input"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/router"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
@@ -24,11 +21,11 @@ func ExampleClickable_passthrough() {
|
||||
// pointer events can be passed down for the underlying
|
||||
// widgets to pick them up.
|
||||
var button1, button2 widget.Clickable
|
||||
var r input.Router
|
||||
var r router.Router
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Source: r.Source(),
|
||||
Queue: &r,
|
||||
}
|
||||
|
||||
// widget lays out two buttons on top of each other.
|
||||
@@ -74,15 +71,15 @@ func ExampleClickable_passthrough() {
|
||||
}
|
||||
|
||||
func ExampleDraggable_Layout() {
|
||||
var r input.Router
|
||||
var r router.Router
|
||||
gtx := layout.Context{
|
||||
Ops: new(op.Ops),
|
||||
Constraints: layout.Exact(image.Pt(100, 100)),
|
||||
Source: r.Source(),
|
||||
Queue: &r,
|
||||
}
|
||||
// mime is the type used to match drag and drop operations.
|
||||
// It could be left empty in this example.
|
||||
const mime = "MyMime"
|
||||
mime := "MyMime"
|
||||
drag := &widget.Draggable{Type: mime}
|
||||
var drop int
|
||||
// widget lays out the drag and drop handlers and processes
|
||||
@@ -97,7 +94,7 @@ func ExampleDraggable_Layout() {
|
||||
// drag must respond with an Offer event when requested.
|
||||
// Use the drag method for this.
|
||||
if m, ok := drag.Update(gtx); ok {
|
||||
drag.Offer(gtx, m, io.NopCloser(strings.NewReader("hello world")))
|
||||
drag.Offer(gtx.Ops, m, offer{Data: "hello world"})
|
||||
}
|
||||
|
||||
// Setup the area for drops.
|
||||
@@ -105,21 +102,17 @@ func ExampleDraggable_Layout() {
|
||||
Min: image.Pt(20, 20),
|
||||
Max: image.Pt(40, 40),
|
||||
}.Push(gtx.Ops)
|
||||
event.Op(gtx.Ops, &drop)
|
||||
transfer.TargetOp{
|
||||
Tag: &drop,
|
||||
Type: mime, // this must match the drag Type for the drop to succeed
|
||||
}.Add(gtx.Ops)
|
||||
ds.Pop()
|
||||
|
||||
// Check for the received data.
|
||||
for {
|
||||
ev, ok := gtx.Event(transfer.TargetFilter{Target: &drop, Type: mime})
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
for _, ev := range gtx.Events(&drop) {
|
||||
switch e := ev.(type) {
|
||||
case transfer.DataEvent:
|
||||
data := e.Open()
|
||||
defer data.Close()
|
||||
content, _ := io.ReadAll(data)
|
||||
fmt.Println(string(content))
|
||||
fmt.Println(data.(offer).Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,3 +145,10 @@ func ExampleDraggable_Layout() {
|
||||
// Output:
|
||||
// hello world
|
||||
}
|
||||
|
||||
type offer struct {
|
||||
Data string
|
||||
}
|
||||
|
||||
func (offer) Read([]byte) (int, error) { return 0, nil }
|
||||
func (offer) Close() error { return nil }
|
||||
|
||||
+1
-5
@@ -48,11 +48,7 @@ func (f *Float) Layout(gtx layout.Context, axis layout.Axis, pointerMargin unit.
|
||||
// The range of f is set by the minimum constraints main axis value.
|
||||
func (f *Float) Update(gtx layout.Context) bool {
|
||||
changed := false
|
||||
for {
|
||||
e, ok := f.drag.Update(gtx.Metric, gtx.Source, gesture.Axis(f.axis))
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
for _, e := range f.drag.Update(gtx.Metric, gtx, gesture.Axis(f.axis)) {
|
||||
if f.length > 0 && (e.Kind == pointer.Press || e.Kind == pointer.Drag) {
|
||||
pos := e.Position.X
|
||||
if f.axis == layout.Vertical {
|
||||
|
||||
@@ -347,6 +347,10 @@ type Region struct {
|
||||
Baseline int
|
||||
}
|
||||
|
||||
// region is identical to Region except that its coordinates are in document
|
||||
// space instead of a widget coordinate space.
|
||||
type region = Region
|
||||
|
||||
// locate returns highlight regions covering the glyphs that represent the runes in
|
||||
// [startRune,endRune). If the rects parameter is non-nil, locate will use it to
|
||||
// return results instead of allocating, provided that there is enough capacity.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user