7 Commits

Author SHA1 Message Date
Chris Waldon 718be79d9e app: fix automatic window decoration action processing
This commit adapts the use of the automatic window decorations to the
event processing changes introduced in v0.4.0. You must update widget
state before laying it out, not after. Doing so after (as this code used
to do) results in discarding updates.

Fixes: https://todo.sr.ht/~eliasnaur/gio/542
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-01-08 08:18:09 -05:00
Dominik Honnef 0073e1a167 widget: don't refer to non-existent method Clickable.Clicks
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2024-01-08 08:18:09 -05:00
Dominik Honnef 32ecec5538 gesture: adjust ClickKind.String for ClickType -> ClickKind rename
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2024-01-08 08:18:09 -05:00
Elias Naur 6eb33b8a56 app: [Windows] tolerate gpu.ErrDeviceLost from Refresh
Fixes: https://todo.sr.ht/~eliasnaur/gio/552
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-01-08 08:18:09 -05:00
Elias Naur 4617526e12 app: don't route internal wakeup events to the Router
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-01-08 08:18:09 -05:00
Elias Naur dbc7a900bd app: [Windows] fix restore size when leaving fullscreen
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-01-08 08:18:09 -05:00
Chris Waldon fb3ae95b28 widget/material: fix list scrollbar display
This commit fixes a visual misalignment in scrollbars resulting from subtle differences
in the semantics of layout.Stack and layout.Background. layout.Stack will position expanded
children according to their minimum constraint regardless of their returned size, whereas
layout.Background uses their returned size. This means that layout.Expanded widgets returning
zero dimensions are positioned correctly, but they break when converted to use layout.Background.

This commit fixes the problem by returning correct dimensions from the scrollbar track.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-01-08 08:18:09 -05:00
83 changed files with 4022 additions and 3978 deletions
+9 -82
View File
@@ -3,16 +3,9 @@
package app package app
import ( import (
"image"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"gioui.org/io/input"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
) )
// extraArgs contains extra arguments to append to // extraArgs contains extra arguments to append to
@@ -27,77 +20,23 @@ var extraArgs string
// On Android ID is the package property of AndroidManifest.xml, // On Android ID is the package property of AndroidManifest.xml,
// on iOS ID is the CFBundleIdentifier of the app Info.plist, // on iOS ID is the CFBundleIdentifier of the app Info.plist,
// on Wayland it is the toplevel app_id, // on Wayland it is the toplevel app_id,
// on X11 it is the X11 XClassHint. // on X11 it is the X11 XClassHint
// //
// ID is set by the [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'" . // go build -ldflags="-X 'gioui.org/app.ID=org.gioui.example.Kitchen'" .
// //
// Note that ID is treated as a constant, and that changing it at runtime // Note that ID is treated as a constant, and that changing it at runtime
// is not supported. 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 = "" var ID = ""
// A FrameEvent requests a new frame in the form of a list of func init() {
// operations that describes the window content. if extraArgs != "" {
type FrameEvent struct { args := strings.Split(extraArgs, "|")
// Now is the current animation. Use Now instead of time.Now to os.Args = append(os.Args, args...)
// 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
}
// Insets is the space taken up by
// system decoration such as translucent
// system bars and software keyboards.
type Insets struct {
// Values are in pixels.
Top, Bottom, Left, Right unit.Dp
}
// NewContext is shorthand for
//
// layout.Context{
// Ops: ops,
// Now: e.Now,
// Source: e.Source,
// Metric: e.Metric,
// Constraints: layout.Exact(e.Size),
// }
//
// NewContext calls ops.Reset and adjusts ops for e.Insets.
func NewContext(ops *op.Ops, e FrameEvent) layout.Context {
ops.Reset()
size := e.Size
if e.Insets != (Insets{}) {
left := e.Metric.Dp(e.Insets.Left)
top := e.Metric.Dp(e.Insets.Top)
op.Offset(image.Point{
X: left,
Y: top,
}).Add(ops)
size.X -= left + e.Metric.Dp(e.Insets.Right)
size.Y -= top + e.Metric.Dp(e.Insets.Bottom)
} }
if ID == "" {
return layout.Context{ ID = filepath.Base(os.Args[0])
Ops: ops,
Now: e.Now,
Source: e.Source,
Metric: e.Metric,
Constraints: layout.Exact(size),
} }
} }
@@ -124,15 +63,3 @@ func DataDir() (string, error) {
func Main() { func Main() {
osMain() 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])
}
}
+13 -5
View File
@@ -8,19 +8,21 @@ See https://gioui.org for instructions to set up and run Gio programs.
# Windows # Windows
Create a new [Window] by calling [NewWindow]. On mobile platforms or when Gio Create a new Window by calling NewWindow. On mobile platforms or when Gio
is embedded in another project, NewWindow merely connects with a previously is embedded in another project, NewWindow merely connects with a previously
created window. created window.
A Window is run by calling its NextEvent method in a loop. The most important event is A Window is run by calling NextEvent in a loop. The most important event is
[FrameEvent] that prompts an update of the window contents. FrameEvent that prompts an update of the window contents.
For example: For example:
import "gioui.org/unit"
w := app.NewWindow() w := app.NewWindow()
for { for {
e := w.NextEvent() e := w.NextEvent()
if e, ok := e.(app.FrameEvent); ok { if e, ok := e.(system.FrameEvent); ok {
ops.Reset() ops.Reset()
// Add operations to ops. // Add operations to ops.
... ...
@@ -30,7 +32,7 @@ For example:
} }
A program must keep receiving events from the event channel until A program must keep receiving events from the event channel until
[DestroyEvent] is received. DestroyEvent is received.
# Main # Main
@@ -55,6 +57,12 @@ For example, to display a blank but otherwise functional window:
app.Main() app.Main()
} }
# Event queue
A FrameEvent's Queue method returns an event.Queue implementation that distributes
incoming events to the event handlers declared in the last frame.
See the gioui.org/io/event package for more information about event handlers.
# Permissions # Permissions
The packages under gioui.org/app/permission should be imported The packages under gioui.org/app/permission should be imported
+4 -4
View File
@@ -11,8 +11,8 @@ import (
"gioui.org/font" "gioui.org/font"
"gioui.org/font/gofont" "gioui.org/font/gofont"
"gioui.org/io/input"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/router"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/text" "gioui.org/text"
@@ -31,10 +31,10 @@ func FuzzIME(f *testing.F) {
f.Fuzz(func(t *testing.T, cmds []byte) { f.Fuzz(func(t *testing.T, cmds []byte) {
cache := text.NewShaper(text.WithCollection(gofont.Collection())) cache := text.NewShaper(text.WithCollection(gofont.Collection()))
e := new(widget.Editor) e := new(widget.Editor)
e.Focus()
var r input.Router var r router.Router
gtx := layout.Context{Ops: new(op.Ops), Source: r.Source()} gtx := layout.Context{Ops: new(op.Ops), Queue: &r}
gtx.Execute(key.FocusCmd{Tag: e})
// Layout once to register focus. // Layout once to register focus.
e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops) r.Frame(gtx.Ops)
+5 -5
View File
@@ -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)) 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' { 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 { 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 <= '~' { if ' ' < s && s <= '~' {
return key.Name(rune(s)), true return string(rune(s)), true
} }
var n key.Name var n string
switch s { switch s {
case C.XKB_KEY_Escape: case C.XKB_KEY_Escape:
n = key.NameEscape n = key.NameEscape
+2 -2
View File
@@ -132,7 +132,7 @@ func (o Orientation) String() string {
} }
type frameEvent struct { type frameEvent struct {
FrameEvent system.FrameEvent
Sync bool Sync bool
} }
@@ -160,7 +160,7 @@ type driver interface {
// ReadClipboard requests the clipboard content. // ReadClipboard requests the clipboard content.
ReadClipboard() ReadClipboard()
// WriteClipboard requests a clipboard write. // WriteClipboard requests a clipboard write.
WriteClipboard(mime string, s []byte) WriteClipboard(s string)
// Configure the window. // Configure the window.
Configure([]Option) Configure([]Option)
// SetCursor updates the current cursor to name. // SetCursor updates the current cursor to name.
+29 -35
View File
@@ -123,14 +123,12 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"io"
"math" "math"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"runtime/cgo" "runtime/cgo"
"runtime/debug" "runtime/debug"
"strings"
"sync" "sync"
"time" "time"
"unicode/utf16" "unicode/utf16"
@@ -139,12 +137,12 @@ import (
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/input" "gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
) )
@@ -158,7 +156,7 @@ type window struct {
fontScale float32 fontScale float32
insets pixelInsets insets pixelInsets
stage Stage stage system.Stage
started bool started bool
animating bool animating bool
@@ -166,10 +164,10 @@ type window struct {
config Config config Config
semantic struct { semantic struct {
hoverID input.SemanticID hoverID router.SemanticID
rootID input.SemanticID rootID router.SemanticID
focusID input.SemanticID focusID router.SemanticID
diffs []input.SemanticID diffs []router.SemanticID
} }
} }
@@ -503,7 +501,7 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
w.loadConfig(env, class) w.loadConfig(env, class)
w.Configure(wopts.options) w.Configure(wopts.options)
w.SetInputHint(key.HintAny) w.SetInputHint(key.HintAny)
w.setStage(StagePaused) w.setStage(system.StagePaused)
w.callbacks.Event(ViewEvent{View: uintptr(view)}) w.callbacks.Event(ViewEvent{View: uintptr(view)})
return C.jlong(w.handle) return C.jlong(w.handle)
} }
@@ -518,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) { func Java_org_gioui_GioView_onStopView(env *C.JNIEnv, class C.jclass, handle C.jlong) {
w := cgo.Handle(handle).Value().(*window) w := cgo.Handle(handle).Value().(*window)
w.started = false w.started = false
w.setStage(StagePaused) w.setStage(system.StagePaused)
} }
//export Java_org_gioui_GioView_onStartView //export Java_org_gioui_GioView_onStartView
@@ -534,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) { func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, handle C.jlong) {
w := cgo.Handle(handle).Value().(*window) w := cgo.Handle(handle).Value().(*window)
w.win = nil w.win = nil
w.setStage(StagePaused) w.setStage(system.StagePaused)
} }
//export Java_org_gioui_GioView_onSurfaceChanged //export Java_org_gioui_GioView_onSurfaceChanged
@@ -556,7 +554,7 @@ func Java_org_gioui_GioView_onLowMemory(env *C.JNIEnv, class C.jclass) {
func Java_org_gioui_GioView_onConfigurationChanged(env *C.JNIEnv, class C.jclass, view C.jlong) { func Java_org_gioui_GioView_onConfigurationChanged(env *C.JNIEnv, class C.jclass, view C.jlong) {
w := cgo.Handle(view).Value().(*window) w := cgo.Handle(view).Value().(*window)
w.loadConfig(env, class) w.loadConfig(env, class)
if w.stage >= StageInactive { if w.stage >= system.StageInactive {
w.draw(env, true) w.draw(env, true)
} }
} }
@@ -567,7 +565,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
if !exist { if !exist {
return return
} }
if w.stage < StageInactive { if w.stage < system.StageInactive {
return return
} }
if w.animating { if w.animating {
@@ -600,7 +598,7 @@ func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C
left: int(left), left: int(left),
right: int(right), right: int(right),
} }
if w.stage >= StageInactive { if w.stage >= system.StageInactive {
w.draw(env, true) w.draw(env, true)
} }
} }
@@ -663,7 +661,7 @@ func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view
} }
} }
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 { for _, ch := range sem.Children {
err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID))) err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID)))
if err != nil { if err != nil {
@@ -706,7 +704,7 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem input.SemanticNode
panic(err) panic(err)
} }
} }
if d.Gestures&input.ClickGesture != 0 { if d.Gestures&router.ClickGesture != 0 {
addAction(ACTION_CLICK) addAction(ACTION_CLICK)
} }
clsName := android.strings.androidViewView clsName := android.strings.androidViewView
@@ -751,18 +749,19 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem input.SemanticNode
return nil 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 { if id == w.semantic.rootID {
return HOST_VIEW_ID return HOST_VIEW_ID
} }
return C.jint(id) return C.jint(id)
} }
func (w *window) semIDFor(virtID C.jint) input.SemanticID { func (w *window) semIDFor(virtID C.jint) router.SemanticID {
if virtID == HOST_VIEW_ID { if virtID == HOST_VIEW_ID {
return w.semantic.rootID return w.semantic.rootID
} }
return input.SemanticID(virtID) return router.SemanticID(virtID)
} }
func (w *window) detach(env *C.JNIEnv) { func (w *window) detach(env *C.JNIEnv) {
@@ -779,16 +778,16 @@ func (w *window) setVisible(env *C.JNIEnv) {
if width == 0 || height == 0 { if width == 0 || height == 0 {
return return
} }
w.setStage(StageRunning) w.setStage(system.StageRunning)
w.draw(env, true) w.draw(env, true)
} }
func (w *window) setStage(stage Stage) { func (w *window) setStage(stage system.Stage) {
if stage == w.stage { if stage == w.stage {
return return
} }
w.stage = stage w.stage = stage
w.callbacks.Event(StageEvent{stage}) w.callbacks.Event(system.StageEvent{stage})
} }
func (w *window) setVisual(visID int) error { func (w *window) setVisual(visID int) error {
@@ -838,14 +837,14 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
const inchPrDp = 1.0 / 160 const inchPrDp = 1.0 / 160
ppdp := float32(w.dpi) * inchPrDp ppdp := float32(w.dpi) * inchPrDp
dppp := unit.Dp(1.0 / ppdp) dppp := unit.Dp(1.0 / ppdp)
insets := Insets{ insets := system.Insets{
Top: unit.Dp(w.insets.top) * dppp, Top: unit.Dp(w.insets.top) * dppp,
Bottom: unit.Dp(w.insets.bottom) * dppp, Bottom: unit.Dp(w.insets.bottom) * dppp,
Left: unit.Dp(w.insets.left) * dppp, Left: unit.Dp(w.insets.left) * dppp,
Right: unit.Dp(w.insets.right) * dppp, Right: unit.Dp(w.insets.right) * dppp,
} }
w.callbacks.Event(frameEvent{ w.callbacks.Event(frameEvent{
FrameEvent: FrameEvent{ FrameEvent: system.FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: w.config.Size, Size: w.config.Size,
Insets: insets, Insets: insets,
@@ -899,8 +898,8 @@ func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) {
f(env) f(env)
} }
func convertKeyCode(code C.jint) (key.Name, bool) { func convertKeyCode(code C.jint) (string, bool) {
var n key.Name var n string
switch code { switch code {
case C.AKEYCODE_FORWARD_DEL: case C.AKEYCODE_FORWARD_DEL:
n = key.NameDeleteForward n = key.NameDeleteForward
@@ -1297,9 +1296,9 @@ func newWindow(window *callbacks, options []Option) error {
return <-mainWindow.errs return <-mainWindow.errs
} }
func (w *window) WriteClipboard(mime string, s []byte) { func (w *window) WriteClipboard(s string) {
runInJVM(javaVM(), func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
jstr := javaString(env, string(s)) jstr := javaString(env, s)
callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard, callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard,
jvalue(android.appCtx), jvalue(jstr)) jvalue(android.appCtx), jvalue(jstr))
}) })
@@ -1313,12 +1312,7 @@ func (w *window) ReadClipboard() {
return return
} }
content := goString(env, C.jstring(c)) content := goString(env, C.jstring(c))
w.callbacks.Event(transfer.DataEvent{ w.callbacks.Event(clipboard.Event{Text: content})
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
}) })
} }
+11 -18
View File
@@ -72,19 +72,17 @@ import "C"
import ( import (
"image" "image"
"io"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"strings"
"time" "time"
"unicode/utf16" "unicode/utf16"
"unsafe" "unsafe"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
) )
@@ -131,7 +129,7 @@ func onCreate(view, controller C.CFTypeRef) {
w.w.SetDriver(w) w.w.SetDriver(w)
views[view] = w views[view] = w
w.Configure(wopts.options) w.Configure(wopts.options)
w.w.Event(StageEvent{Stage: StagePaused}) w.w.Event(system.StageEvent{Stage: system.StagePaused})
w.w.Event(ViewEvent{ViewController: uintptr(controller)}) w.w.Event(ViewEvent{ViewController: uintptr(controller)})
} }
@@ -149,7 +147,7 @@ func (w *window) draw(sync bool) {
wasVisible := w.visible wasVisible := w.visible
w.visible = true w.visible = true
if !wasVisible { if !wasVisible {
w.w.Event(StageEvent{Stage: StageRunning}) w.w.Event(system.StageEvent{Stage: system.StageRunning})
} }
const inchPrDp = 1.0 / 163 const inchPrDp = 1.0 / 163
m := unit.Metric{ m := unit.Metric{
@@ -158,13 +156,13 @@ func (w *window) draw(sync bool) {
} }
dppp := unit.Dp(1. / m.PxPerDp) dppp := unit.Dp(1. / m.PxPerDp)
w.w.Event(frameEvent{ w.w.Event(frameEvent{
FrameEvent: FrameEvent{ FrameEvent: system.FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: image.Point{ Size: image.Point{
X: int(params.width + .5), X: int(params.width + .5),
Y: int(params.height + .5), Y: int(params.height + .5),
}, },
Insets: Insets{ Insets: system.Insets{
Top: unit.Dp(params.top) * dppp, Top: unit.Dp(params.top) * dppp,
Bottom: unit.Dp(params.bottom) * dppp, Bottom: unit.Dp(params.bottom) * dppp,
Left: unit.Dp(params.left) * dppp, Left: unit.Dp(params.left) * dppp,
@@ -180,7 +178,7 @@ func (w *window) draw(sync bool) {
func onStop(view C.CFTypeRef) { func onStop(view C.CFTypeRef) {
w := views[view] w := views[view]
w.visible = false w.visible = false
w.w.Event(StageEvent{Stage: StagePaused}) w.w.Event(system.StageEvent{Stage: system.StagePaused})
} }
//export onDestroy //export onDestroy
@@ -188,7 +186,7 @@ func onDestroy(view C.CFTypeRef) {
w := views[view] w := views[view]
delete(views, view) delete(views, view)
w.w.Event(ViewEvent{}) w.w.Event(ViewEvent{})
w.w.Event(DestroyEvent{}) w.w.Event(system.DestroyEvent{})
w.displayLink.Close() w.displayLink.Close()
w.view = 0 w.view = 0
} }
@@ -267,16 +265,11 @@ func (w *window) ReadClipboard() {
cstr := C.readClipboard() cstr := C.readClipboard()
defer C.CFRelease(cstr) defer C.CFRelease(cstr)
content := nsstringToString(cstr) content := nsstringToString(cstr)
w.w.Event(transfer.DataEvent{ w.w.Event(clipboard.Event{Text: content})
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
} }
func (w *window) WriteClipboard(mime string, s []byte) { func (w *window) WriteClipboard(s string) {
u16 := utf16.Encode([]rune(string(s))) u16 := utf16.Encode([]rune(s))
var chars *C.unichar var chars *C.unichar
if len(u16) > 0 { if len(u16) > 0 {
chars = (*C.unichar)(unsafe.Pointer(&u16[0])) chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
@@ -310,7 +303,7 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
w.cursor = windowSetCursor(w.cursor, cursor) w.cursor = windowSetCursor(w.cursor, cursor)
} }
func (w *window) onKeyCommand(name key.Name) { func (w *window) onKeyCommand(name string) {
w.w.Event(key.Event{ w.w.Event(key.Event{
Name: name, Name: name,
}) })
+14 -20
View File
@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"io"
"strings" "strings"
"syscall/js" "syscall/js"
"time" "time"
@@ -16,10 +15,10 @@ import (
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
) )
@@ -102,12 +101,7 @@ func newWindow(win *callbacks, options []Option) error {
}) })
w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} { w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
content := args[0].String() content := args[0].String()
go win.Event(transfer.DataEvent{ go win.Event(clipboard.Event{Text: content})
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
return nil return nil
}) })
w.addEventListeners() w.addEventListeners()
@@ -120,7 +114,7 @@ func newWindow(win *callbacks, options []Option) error {
w.Configure(options) w.Configure(options)
w.blur() w.blur()
w.w.Event(ViewEvent{Element: cont}) w.w.Event(ViewEvent{Element: cont})
w.w.Event(StageEvent{Stage: StageRunning}) w.w.Event(system.StageEvent{Stage: system.StageRunning})
w.resize() w.resize()
w.draw(true) w.draw(true)
for { for {
@@ -213,12 +207,12 @@ func (w *window) addEventListeners() {
return w.browserHistory.Call("back") return w.browserHistory.Call("back")
}) })
w.addEventListener(w.document, "visibilitychange", func(this js.Value, args []js.Value) interface{} { w.addEventListener(w.document, "visibilitychange", func(this js.Value, args []js.Value) interface{} {
ev := StageEvent{} ev := system.StageEvent{}
switch w.document.Get("visibilityState").String() { switch w.document.Get("visibilityState").String() {
case "hidden", "prerender", "unloaded": case "hidden", "prerender", "unloaded":
ev.Stage = StagePaused ev.Stage = system.StagePaused
default: default:
ev.Stage = StageRunning ev.Stage = system.StageRunning
} }
w.w.Event(ev) w.w.Event(ev)
return nil return nil
@@ -539,14 +533,14 @@ func (w *window) ReadClipboard() {
w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback) w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback)
} }
func (w *window) WriteClipboard(mime string, s []byte) { func (w *window) WriteClipboard(s string) {
if w.clipboard.IsUndefined() { if w.clipboard.IsUndefined() {
return return
} }
if w.clipboard.Get("writeText").IsUndefined() { if w.clipboard.Get("writeText").IsUndefined() {
return return
} }
w.clipboard.Call("writeText", string(s)) w.clipboard.Call("writeText", s)
} }
func (w *window) Configure(options []Option) { func (w *window) Configure(options []Option) {
@@ -672,7 +666,7 @@ func (w *window) draw(sync bool) {
} }
w.w.Event(frameEvent{ w.w.Event(frameEvent{
FrameEvent: FrameEvent{ FrameEvent: system.FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: size, Size: size,
Insets: insets, 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) invscale := unit.Dp(1. / w.scale)
return image.Pt(w.config.Size.X, w.config.Size.Y), return image.Pt(w.config.Size.X, w.config.Size.Y),
Insets{ system.Insets{
Bottom: unit.Dp(w.inset.Y) * invscale, Bottom: unit.Dp(w.inset.Y) * invscale,
Right: unit.Dp(w.inset.X) * invscale, Right: unit.Dp(w.inset.X) * invscale,
}, unit.Metric{ }, unit.Metric{
@@ -752,8 +746,8 @@ func osMain() {
select {} select {}
} }
func translateKey(k string) (key.Name, bool) { func translateKey(k string) (string, bool) {
var n key.Name var n string
switch k { switch k {
case "ArrowUp": case "ArrowUp":
@@ -820,7 +814,7 @@ func translateKey(k string) (key.Name, bool) {
r, s := utf8.DecodeRuneInString(k) r, s := utf8.DecodeRuneInString(k)
// If there is exactly one printable character, return that. // If there is exactly one printable character, return that.
if s == len(k) && unicode.IsPrint(r) { if s == len(k) && unicode.IsPrint(r) {
return key.Name(strings.ToUpper(k)), true return strings.ToUpper(k), true
} }
return "", false return "", false
} }
+20 -27
View File
@@ -8,18 +8,16 @@ package app
import ( import (
"errors" "errors"
"image" "image"
"io"
"runtime" "runtime"
"strings"
"time" "time"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"gioui.org/internal/f32" "gioui.org/internal/f32"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
_ "gioui.org/internal/cocoainit" _ "gioui.org/internal/cocoainit"
@@ -249,7 +247,7 @@ type ViewEvent struct {
type window struct { type window struct {
view C.CFTypeRef view C.CFTypeRef
w *callbacks w *callbacks
stage Stage stage system.Stage
displayLink *displayLink displayLink *displayLink
// redraw is a single entry channel for making sure only one // redraw is a single entry channel for making sure only one
// display link redraw request is in flight. // display link redraw request is in flight.
@@ -307,16 +305,11 @@ func (w *window) ReadClipboard() {
defer C.CFRelease(cstr) defer C.CFRelease(cstr)
} }
content := nsstringToString(cstr) content := nsstringToString(cstr)
w.w.Event(transfer.DataEvent{ w.w.Event(clipboard.Event{Text: content})
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
} }
func (w *window) WriteClipboard(mime string, s []byte) { func (w *window) WriteClipboard(s string) {
cstr := stringToNSString(string(s)) cstr := stringToNSString(s)
defer C.CFRelease(cstr) defer C.CFRelease(cstr)
C.writeClipboard(cstr) C.writeClipboard(cstr)
} }
@@ -488,12 +481,12 @@ func (w *window) runOnMain(f func()) {
}) })
} }
func (w *window) setStage(stage Stage) { func (w *window) setStage(stage system.Stage) {
if stage == w.stage { if stage == w.stage {
return return
} }
w.stage = stage w.stage = stage
w.w.Event(StageEvent{Stage: stage}) w.w.Event(system.StageEvent{Stage: stage})
} }
//export gio_onKeys //export gio_onKeys
@@ -583,11 +576,11 @@ func gio_onDraw(view C.CFTypeRef) {
func gio_onFocus(view C.CFTypeRef, focus C.int) { func gio_onFocus(view C.CFTypeRef, focus C.int) {
w := mustView(view) w := mustView(view)
w.w.Event(key.FocusEvent{Focus: focus == 1}) w.w.Event(key.FocusEvent{Focus: focus == 1})
if w.stage >= StageInactive { if w.stage >= system.StageInactive {
if focus == 0 { if focus == 0 {
w.setStage(StageInactive) w.setStage(system.StageInactive)
} else { } else {
w.setStage(StageRunning) w.setStage(system.StageRunning)
} }
} }
w.SetCursor(w.cursor) w.SetCursor(w.cursor)
@@ -782,9 +775,9 @@ func (w *window) draw() {
return return
} }
cfg := configFor(w.scale) cfg := configFor(w.scale)
w.setStage(StageRunning) w.setStage(system.StageRunning)
w.w.Event(frameEvent{ w.w.Event(frameEvent{
FrameEvent: FrameEvent{ FrameEvent: system.FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: w.config.Size, Size: w.config.Size,
Metric: cfg, Metric: cfg,
@@ -804,7 +797,7 @@ func configFor(scale float32) unit.Metric {
func gio_onClose(view C.CFTypeRef) { func gio_onClose(view C.CFTypeRef) {
w := mustView(view) w := mustView(view)
w.w.Event(ViewEvent{}) w.w.Event(ViewEvent{})
w.w.Event(DestroyEvent{}) w.w.Event(system.DestroyEvent{})
w.displayLink.Close() w.displayLink.Close()
w.displayLink = nil w.displayLink = nil
deleteView(view) deleteView(view)
@@ -815,13 +808,13 @@ func gio_onClose(view C.CFTypeRef) {
//export gio_onHide //export gio_onHide
func gio_onHide(view C.CFTypeRef) { func gio_onHide(view C.CFTypeRef) {
w := mustView(view) w := mustView(view)
w.setStage(StagePaused) w.setStage(system.StagePaused)
} }
//export gio_onShow //export gio_onShow
func gio_onShow(view C.CFTypeRef) { func gio_onShow(view C.CFTypeRef) {
w := mustView(view) w := mustView(view)
w.setStage(StageRunning) w.setStage(system.StageRunning)
} }
//export gio_onFullscreen //export gio_onFullscreen
@@ -841,14 +834,14 @@ func gio_onWindowed(view C.CFTypeRef) {
//export gio_onAppHide //export gio_onAppHide
func gio_onAppHide() { func gio_onAppHide() {
for _, w := range viewMap { for _, w := range viewMap {
w.setStage(StagePaused) w.setStage(system.StagePaused)
} }
} }
//export gio_onAppShow //export gio_onAppShow
func gio_onAppShow() { func gio_onAppShow() {
for _, w := range viewMap { for _, w := range viewMap {
w.setStage(StageRunning) w.setStage(system.StageRunning)
} }
} }
@@ -920,8 +913,8 @@ func osMain() {
C.gio_main() C.gio_main()
} }
func convertKey(k rune) (key.Name, bool) { func convertKey(k rune) (string, bool) {
var n key.Name var n string
switch k { switch k {
case 0x1b: case 0x1b:
n = key.NameEscape n = key.NameEscape
@@ -982,7 +975,7 @@ func convertKey(k rune) (key.Name, bool) {
if !unicode.IsPrint(k) { if !unicode.IsPrint(k) {
return "", false return "", false
} }
n = key.Name(k) n = string(k)
} }
return n, true return n, true
} }
+15 -19
View File
@@ -25,10 +25,10 @@ import (
"gioui.org/app/internal/xkb" "gioui.org/app/internal/xkb"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/internal/fling" "gioui.org/internal/fling"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
) )
@@ -194,7 +194,7 @@ type window struct {
dir f32.Point dir f32.Point
} }
stage Stage stage system.Stage
dead bool dead bool
lastFrameCallback *C.struct_wl_callback lastFrameCallback *C.struct_wl_callback
@@ -209,7 +209,7 @@ type window struct {
wsize image.Point // window config size before going fullscreen or maximized wsize image.Point // window config size before going fullscreen or maximized
inCompositor bool // window is moving or being resized inCompositor bool // window is moving or being resized
clipReads chan transfer.DataEvent clipReads chan clipboard.Event
wakeups chan struct{} wakeups chan struct{}
} }
@@ -277,7 +277,7 @@ func newWLWindow(callbacks *callbacks, options []Option) error {
err := w.loop() err := w.loop()
w.w.Event(WaylandViewEvent{}) w.w.Event(WaylandViewEvent{})
w.w.Event(DestroyEvent{Err: err}) w.w.Event(system.DestroyEvent{Err: err})
}() }()
return nil return nil
} }
@@ -354,7 +354,7 @@ func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) {
ppdp: ppdp, ppdp: ppdp,
ppsp: ppdp, ppsp: ppdp,
wakeups: make(chan struct{}, 1), wakeups: make(chan struct{}, 1),
clipReads: make(chan transfer.DataEvent, 1), clipReads: make(chan clipboard.Event, 1),
} }
w.surf = C.wl_compositor_create_surface(d.compositor) w.surf = C.wl_compositor_create_surface(d.compositor)
if w.surf == nil { if w.surf == nil {
@@ -551,7 +551,7 @@ func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface
w.serial = serial w.serial = serial
w.redraw = true w.redraw = true
C.xdg_surface_ack_configure(wmSurf, serial) C.xdg_surface_ack_configure(wmSurf, serial)
w.setStage(StageRunning) w.setStage(system.StageRunning)
} }
//export gio_onToplevelClose //export gio_onToplevelClose
@@ -1021,24 +1021,20 @@ func (w *window) ReadClipboard() {
r, err := w.disp.readClipboard() r, err := w.disp.readClipboard()
// Send empty responses on unavailable clipboards or errors. // Send empty responses on unavailable clipboards or errors.
if r == nil || err != nil { if r == nil || err != nil {
w.w.Event(clipboard.Event{})
return return
} }
// Don't let slow clipboard transfers block event loop. // Don't let slow clipboard transfers block event loop.
go func() { go func() {
defer r.Close() defer r.Close()
data, _ := io.ReadAll(r) data, _ := io.ReadAll(r)
w.clipReads <- transfer.DataEvent{ w.clipReads <- clipboard.Event{Text: string(data)}
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(bytes.NewReader(data))
},
}
w.Wakeup() w.Wakeup()
}() }()
} }
func (w *window) WriteClipboard(mime string, s []byte) { func (w *window) WriteClipboard(s string) {
w.disp.writeClipboard(s) w.disp.writeClipboard([]byte(s))
} }
func (w *window) Configure(options []Option) { func (w *window) Configure(options []Option) {
@@ -1677,9 +1673,9 @@ func (w *window) updateOutputs() {
w.redraw = true w.redraw = true
} }
if !found { if !found {
w.setStage(StagePaused) w.setStage(system.StagePaused)
} else { } else {
w.setStage(StageRunning) w.setStage(system.StageRunning)
w.redraw = true w.redraw = true
} }
} }
@@ -1716,7 +1712,7 @@ func (w *window) draw() {
C.wl_callback_add_listener(w.lastFrameCallback, &C.gio_callback_listener, unsafe.Pointer(w.surf)) C.wl_callback_add_listener(w.lastFrameCallback, &C.gio_callback_listener, unsafe.Pointer(w.surf))
} }
w.w.Event(frameEvent{ w.w.Event(frameEvent{
FrameEvent: FrameEvent{ FrameEvent: system.FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: w.config.Size, Size: w.config.Size,
Metric: cfg, Metric: cfg,
@@ -1725,12 +1721,12 @@ func (w *window) draw() {
}) })
} }
func (w *window) setStage(s Stage) { func (w *window) setStage(s system.Stage) {
if s == w.stage { if s == w.stage {
return return
} }
w.stage = s w.stage = s
w.w.Event(StageEvent{Stage: s}) w.w.Event(system.StageEvent{Stage: s})
} }
func (w *window) display() *C.struct_wl_display { func (w *window) display() *C.struct_wl_display {
+18 -24
View File
@@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"image" "image"
"io"
"runtime" "runtime"
"sort" "sort"
"strings" "strings"
@@ -23,10 +22,10 @@ import (
gowindows "golang.org/x/sys/windows" gowindows "golang.org/x/sys/windows"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
) )
type ViewEvent struct { type ViewEvent struct {
@@ -37,7 +36,7 @@ type window struct {
hwnd syscall.Handle hwnd syscall.Handle
hdc syscall.Handle hdc syscall.Handle
w *callbacks w *callbacks
stage Stage stage system.Stage
pointerBtns pointer.Buttons pointerBtns pointer.Buttons
// cursorIn tracks whether the cursor was inside the window according // cursorIn tracks whether the cursor was inside the window according
@@ -269,11 +268,11 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
w.focused = false w.focused = false
w.w.Event(key.FocusEvent{Focus: false}) w.w.Event(key.FocusEvent{Focus: false})
case windows.WM_NCACTIVATE: case windows.WM_NCACTIVATE:
if w.stage >= StageInactive { if w.stage >= system.StageInactive {
if wParam == windows.TRUE { if wParam == windows.TRUE {
w.setStage(StageRunning) w.setStage(system.StageRunning)
} else { } else {
w.setStage(StageInactive) w.setStage(system.StageInactive)
} }
} }
case windows.WM_NCHITTEST: case windows.WM_NCHITTEST:
@@ -302,7 +301,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
w.scrollEvent(wParam, lParam, true, getModifiers()) w.scrollEvent(wParam, lParam, true, getModifiers())
case windows.WM_DESTROY: case windows.WM_DESTROY:
w.w.Event(ViewEvent{}) w.w.Event(ViewEvent{})
w.w.Event(DestroyEvent{}) w.w.Event(system.DestroyEvent{})
if w.hdc != 0 { if w.hdc != 0 {
windows.ReleaseDC(w.hdc) windows.ReleaseDC(w.hdc)
w.hdc = 0 w.hdc = 0
@@ -339,15 +338,15 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
switch wParam { switch wParam {
case windows.SIZE_MINIMIZED: case windows.SIZE_MINIMIZED:
w.config.Mode = Minimized w.config.Mode = Minimized
w.setStage(StagePaused) w.setStage(system.StagePaused)
case windows.SIZE_MAXIMIZED: case windows.SIZE_MAXIMIZED:
w.config.Mode = Maximized w.config.Mode = Maximized
w.setStage(StageRunning) w.setStage(system.StageRunning)
case windows.SIZE_RESTORED: case windows.SIZE_RESTORED:
if w.config.Mode != Fullscreen { if w.config.Mode != Fullscreen {
w.config.Mode = Windowed w.config.Mode = Windowed
} }
w.setStage(StageRunning) w.setStage(system.StageRunning)
} }
case windows.WM_GETMINMAXINFO: case windows.WM_GETMINMAXINFO:
mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam))) mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam)))
@@ -608,10 +607,10 @@ func (w *window) Wakeup() {
} }
} }
func (w *window) setStage(s Stage) { func (w *window) setStage(s system.Stage) {
if s != w.stage { if s != w.stage {
w.stage = s w.stage = s
w.w.Event(StageEvent{Stage: s}) w.w.Event(system.StageEvent{Stage: s})
} }
} }
@@ -622,7 +621,7 @@ func (w *window) draw(sync bool) {
dpi := windows.GetWindowDPI(w.hwnd) dpi := windows.GetWindowDPI(w.hwnd)
cfg := configForDPI(dpi) cfg := configForDPI(dpi)
w.w.Event(frameEvent{ w.w.Event(frameEvent{
FrameEvent: FrameEvent{ FrameEvent: system.FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: w.config.Size, Size: w.config.Size,
Metric: cfg, Metric: cfg,
@@ -668,12 +667,7 @@ func (w *window) readClipboard() error {
} }
defer windows.GlobalUnlock(mem) defer windows.GlobalUnlock(mem)
content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr))) content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr)))
w.w.Event(transfer.DataEvent{ w.w.Event(clipboard.Event{Text: content})
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
return nil return nil
} }
@@ -741,8 +735,8 @@ func (w *window) Configure(options []Option) {
w.update() w.update()
} }
func (w *window) WriteClipboard(mime string, s []byte) { func (w *window) WriteClipboard(s string) {
w.writeClipboard(string(s)) w.writeClipboard(s)
} }
func (w *window) writeClipboard(s string) error { func (w *window) writeClipboard(s string) error {
@@ -870,11 +864,11 @@ func (w *window) raise() {
windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW) windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW)
} }
func convertKeyCode(code uintptr) (key.Name, bool) { func convertKeyCode(code uintptr) (string, bool) {
if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' { 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 { switch code {
case windows.VK_ESCAPE: case windows.VK_ESCAPE:
+10 -17
View File
@@ -30,18 +30,16 @@ import (
"errors" "errors"
"fmt" "fmt"
"image" "image"
"io"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
"unsafe" "unsafe"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
syscall "golang.org/x/sys/unix" syscall "golang.org/x/sys/unix"
@@ -93,7 +91,7 @@ type x11Window struct {
// _NET_WM_STATE_MAXIMIZED_VERT // _NET_WM_STATE_MAXIMIZED_VERT
wmStateMaximizedVert C.Atom wmStateMaximizedVert C.Atom
} }
stage Stage stage system.Stage
metric unit.Metric metric unit.Metric
notify struct { notify struct {
read, write int read, write int
@@ -153,8 +151,8 @@ func (w *x11Window) ReadClipboard() {
C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime) C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime)
} }
func (w *x11Window) WriteClipboard(mime string, s []byte) { func (w *x11Window) WriteClipboard(s string) {
w.clipboard.content = s w.clipboard.content = []byte(s)
C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime) C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime)
C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime) C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime)
} }
@@ -395,12 +393,12 @@ func (w *x11Window) window() (C.Window, int, int) {
return w.xw, w.config.Size.X, w.config.Size.Y return w.xw, w.config.Size.X, w.config.Size.Y
} }
func (w *x11Window) setStage(s Stage) { func (w *x11Window) setStage(s system.Stage) {
if s == w.stage { if s == w.stage {
return return
} }
w.stage = s w.stage = s
w.w.Event(StageEvent{Stage: s}) w.w.Event(system.StageEvent{Stage: s})
} }
func (w *x11Window) loop() { func (w *x11Window) loop() {
@@ -460,7 +458,7 @@ loop:
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 { if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
w.w.Event(frameEvent{ w.w.Event(frameEvent{
FrameEvent: FrameEvent{ FrameEvent: system.FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: w.config.Size, Size: w.config.Size,
Metric: w.metric, Metric: w.metric,
@@ -652,12 +650,7 @@ func (h *x11EventHandler) handleEvents() bool {
break break
} }
str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems)) str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
w.w.Event(transfer.DataEvent{ w.w.Event(clipboard.Event{Text: str})
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(str))
},
})
case C.SelectionRequest: case C.SelectionRequest:
cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev)) cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None { if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None {
@@ -837,10 +830,10 @@ func newX11Window(gioWin *callbacks, options []Option) error {
C.XMapWindow(dpy, win) C.XMapWindow(dpy, win)
w.Configure(options) w.Configure(options)
w.w.Event(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)}) w.w.Event(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
w.setStage(StageRunning) w.setStage(system.StageRunning)
w.loop() w.loop()
w.w.Event(X11ViewEvent{}) w.w.Event(X11ViewEvent{})
w.w.Event(DestroyEvent{Err: nil}) w.w.Event(system.DestroyEvent{Err: nil})
w.destroy() w.destroy()
}() }()
return nil return nil
-49
View File
@@ -1,49 +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
}
// 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 frames.
StagePaused Stage = iota
// StageInactive is the stage for windows that are visible, but not active.
// Inactive windows receive frames.
StageInactive
// StageRunning is for active and visible Windows.
// Running windows receive frames.
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 (StageEvent) ImplementsEvent() {}
func (DestroyEvent) ImplementsEvent() {}
+111 -83
View File
@@ -19,9 +19,10 @@ import (
"gioui.org/internal/debug" "gioui.org/internal/debug"
"gioui.org/internal/ops" "gioui.org/internal/ops"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/profile"
"gioui.org/io/router"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
@@ -67,7 +68,7 @@ type Window struct {
frameAck chan struct{} frameAck chan struct{}
destroy chan struct{} destroy chan struct{}
stage Stage stage system.Stage
animating bool animating bool
hasNextFrame bool hasNextFrame bool
nextFrame time.Time nextFrame time.Time
@@ -76,7 +77,7 @@ type Window struct {
// metric is the metric from the most recent frame. // metric is the metric from the most recent frame.
metric unit.Metric metric unit.Metric
queue input.Router queue queue
cursor pointer.Cursor cursor pointer.Cursor
decorations struct { decorations struct {
op.Ops op.Ops
@@ -101,10 +102,10 @@ type Window struct {
semantic struct { semantic struct {
// uptodate tracks whether the fields below are up to date. // uptodate tracks whether the fields below are up to date.
uptodate bool uptodate bool
root input.SemanticID root router.SemanticID
prevTree []input.SemanticNode prevTree []router.SemanticNode
tree []input.SemanticNode tree []router.SemanticNode
ids map[input.SemanticID]input.SemanticNode ids map[router.SemanticID]router.SemanticNode
} }
imeState editorState imeState editorState
@@ -121,7 +122,7 @@ type Window struct {
} }
type editorState struct { type editorState struct {
input.EditorState router.EditorState
compose key.Range compose key.Range
} }
@@ -132,6 +133,12 @@ type callbacks struct {
waitEvents []event.Event waitEvents []event.Event
} }
// queue is an event.Queue implementation that distributes system events
// to the input handlers declared in the most recent frame.
type queue struct {
q router.Router
}
// NewWindow creates a new window for a set of window // NewWindow creates a new window for a set of window
// options. The options are hints; the platform is free to // options. The options are hints; the platform is free to
// ignore or adjust them. // ignore or adjust them.
@@ -188,7 +195,7 @@ func NewWindow(options ...Option) *Window {
w.decorations.enabled = cnf.Decorated w.decorations.enabled = cnf.Decorated
w.decorations.height = decoHeight w.decorations.height = decoHeight
w.imeState.compose = key.Range{Start: -1, End: -1} w.imeState.compose = key.Range{Start: -1, End: -1}
w.semantic.ids = make(map[input.SemanticID]input.SemanticNode) w.semantic.ids = make(map[router.SemanticID]router.SemanticNode)
w.callbacks.w = w w.callbacks.w = w
w.eventState.initialOpts = options w.eventState.initialOpts = options
return w return w
@@ -273,7 +280,7 @@ func (w *Window) validateAndProcess(d driver, size image.Point, sync bool, frame
return err return err
} }
} }
w.queue.Frame(frame) w.queue.q.Frame(frame)
// Let the client continue as soon as possible, in particular before // Let the client continue as soon as possible, in particular before
// a potentially blocking Present. // a potentially blocking Present.
signal() signal()
@@ -301,25 +308,25 @@ func (w *Window) frame(frame *op.Ops, viewport image.Point) error {
return w.gpu.Frame(frame, target, viewport) return w.gpu.Frame(frame, target, viewport)
} }
func (w *Window) processFrame(d driver) { func (w *Window) processFrame(d driver, frameStart time.Time) {
for k := range w.semantic.ids { for k := range w.semantic.ids {
delete(w.semantic.ids, k) delete(w.semantic.ids, k)
} }
w.semantic.uptodate = false w.semantic.uptodate = false
q := &w.queue q := &w.queue.q
switch q.TextInputState() { switch q.TextInputState() {
case input.TextInputOpen: case router.TextInputOpen:
d.ShowTextInput(true) d.ShowTextInput(true)
case input.TextInputClose: case router.TextInputClose:
d.ShowTextInput(false) d.ShowTextInput(false)
} }
if hint, ok := q.TextInputHint(); ok { if hint, ok := q.TextInputHint(); ok {
d.SetInputHint(hint) d.SetInputHint(hint)
} }
if mime, txt, ok := q.WriteClipboard(); ok { if txt, ok := q.WriteClipboard(); ok {
d.WriteClipboard(mime, txt) d.WriteClipboard(txt)
} }
if q.ClipboardRequested() { if q.ReadClipboard() {
d.ReadClipboard() d.ReadClipboard()
} }
oldState := w.imeState oldState := w.imeState
@@ -329,18 +336,25 @@ func (w *Window) processFrame(d driver) {
w.imeState = newState w.imeState = newState
d.EditorStateChanged(oldState, newState) d.EditorStateChanged(oldState, newState)
} }
if q.Profiling() && w.gpu != nil {
frameDur := time.Since(frameStart)
frameDur = frameDur.Truncate(100 * time.Microsecond)
quantum := 100 * time.Microsecond
timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(quantum), w.gpu.Profile())
q.Queue(profile.Event{Timings: timings})
}
if t, ok := q.WakeupTime(); ok { if t, ok := q.WakeupTime(); ok {
w.setNextFrame(t) w.setNextFrame(t)
} }
w.updateAnimation(d) w.updateAnimation(d)
} }
// Invalidate the window such that a [FrameEvent] will be generated immediately. // Invalidate the window such that a FrameEvent will be generated immediately.
// If the window is inactive, the event is sent when the window becomes active. // If the window is inactive, the event is sent when the window becomes active.
// //
// Note that Invalidate is intended for externally triggered updates, such as a // Note that Invalidate is intended for externally triggered updates, such as a
// response from a network request. The [op.InvalidateCmd] command is more efficient // response from a network request. InvalidateOp is more efficient for animation
// for animation. // and similar internal updates.
// //
// Invalidate is safe for concurrent use. // Invalidate is safe for concurrent use.
func (w *Window) Invalidate() { func (w *Window) Invalidate() {
@@ -372,14 +386,21 @@ func (w *Window) Option(opts ...Option) {
} }
} }
// WriteClipboard writes a string to the clipboard.
func (w *Window) WriteClipboard(s string) {
w.driverDefer(func(d driver) {
d.WriteClipboard(s)
})
}
// Run f in the same thread as the native window event loop, and wait for f to // Run f in the same thread as the native window event loop, and wait for f to
// return or the window to close. Run is guaranteed not to deadlock if it is // return or the window to close. Run is guaranteed not to deadlock if it is
// invoked during the handling of a [ViewEvent], [FrameEvent], // invoked during the handling of a ViewEvent, system.FrameEvent,
// [StageEvent]; call Run in a separate goroutine to avoid deadlock in all // system.StageEvent; call Run in a separate goroutine to avoid deadlock in all
// other cases. // other cases.
// //
// Note that most programs should not call Run; configuring a Window with // Note that most programs should not call Run; configuring a Window with
// [CustomRenderer] is a notable exception. // CustomRenderer is a notable exception.
func (w *Window) Run(f func()) { func (w *Window) Run(f func()) {
done := make(chan struct{}) done := make(chan struct{})
w.driverDefer(func(d driver) { w.driverDefer(func(d driver) {
@@ -404,7 +425,7 @@ func (w *Window) driverDefer(f func(d driver)) {
func (w *Window) updateAnimation(d driver) { func (w *Window) updateAnimation(d driver) {
animate := false animate := false
if w.stage >= StageInactive && w.hasNextFrame { if w.stage >= system.StageInactive && w.hasNextFrame {
if dt := time.Until(w.nextFrame); dt <= 0 { if dt := time.Until(w.nextFrame); dt <= 0 {
animate = true animate = true
} else { } else {
@@ -494,19 +515,19 @@ func (c *callbacks) Event(e event.Event) bool {
} }
// SemanticRoot returns the ID of the semantic root. // SemanticRoot returns the ID of the semantic root.
func (c *callbacks) SemanticRoot() input.SemanticID { func (c *callbacks) SemanticRoot() router.SemanticID {
c.w.updateSemantics() c.w.updateSemantics()
return c.w.semantic.root return c.w.semantic.root
} }
// LookupSemantic looks up a semantic node from an ID. The zero ID denotes the root. // LookupSemantic looks up a semantic node from an ID. The zero ID denotes the root.
func (c *callbacks) LookupSemantic(semID input.SemanticID) (input.SemanticNode, bool) { func (c *callbacks) LookupSemantic(semID router.SemanticID) (router.SemanticNode, bool) {
c.w.updateSemantics() c.w.updateSemantics()
n, found := c.w.semantic.ids[semID] n, found := c.w.semantic.ids[semID]
return n, found return n, found
} }
func (c *callbacks) AppendSemanticDiffs(diffs []input.SemanticID) []input.SemanticID { func (c *callbacks) AppendSemanticDiffs(diffs []router.SemanticID) []router.SemanticID {
c.w.updateSemantics() c.w.updateSemantics()
if tree := c.w.semantic.prevTree; len(tree) > 0 { if tree := c.w.semantic.prevTree; len(tree) > 0 {
c.w.collectSemanticDiffs(&diffs, c.w.semantic.prevTree[0]) c.w.collectSemanticDiffs(&diffs, c.w.semantic.prevTree[0])
@@ -514,9 +535,9 @@ func (c *callbacks) AppendSemanticDiffs(diffs []input.SemanticID) []input.Semant
return diffs return diffs
} }
func (c *callbacks) SemanticAt(pos f32.Point) (input.SemanticID, bool) { func (c *callbacks) SemanticAt(pos f32.Point) (router.SemanticID, bool) {
c.w.updateSemantics() c.w.updateSemantics()
return c.w.queue.SemanticAt(pos) return c.w.queue.q.SemanticAt(pos)
} }
func (c *callbacks) EditorState() editorState { func (c *callbacks) EditorState() editorState {
@@ -558,38 +579,37 @@ func (c *callbacks) SetEditorSnippet(r key.Range) {
c.Event(key.SnippetEvent(r)) c.Event(key.SnippetEvent(r))
} }
func (w *Window) moveFocus(dir key.FocusDirection) { func (w *Window) moveFocus(dir router.FocusDirection, d driver) {
w.queue.MoveFocus(dir) if w.queue.q.MoveFocus(dir) {
if _, handled := w.queue.WakeupTime(); handled { w.queue.q.RevealFocus(w.viewport)
w.queue.RevealFocus(w.viewport)
} else { } else {
var v image.Point var v image.Point
switch dir { switch dir {
case key.FocusRight: case router.FocusRight:
v = image.Pt(+1, 0) v = image.Pt(+1, 0)
case key.FocusLeft: case router.FocusLeft:
v = image.Pt(-1, 0) v = image.Pt(-1, 0)
case key.FocusDown: case router.FocusDown:
v = image.Pt(0, +1) v = image.Pt(0, +1)
case key.FocusUp: case router.FocusUp:
v = image.Pt(0, -1) v = image.Pt(0, -1)
default: default:
return return
} }
const scrollABit = unit.Dp(50) const scrollABit = unit.Dp(50)
dist := v.Mul(int(w.metric.Dp(scrollABit))) dist := v.Mul(int(w.metric.Dp(scrollABit)))
w.queue.ScrollFocus(dist) w.queue.q.ScrollFocus(dist)
} }
} }
func (c *callbacks) ClickFocus() { func (c *callbacks) ClickFocus() {
c.w.queue.ClickFocus() c.w.queue.q.ClickFocus()
c.w.setNextFrame(time.Time{}) c.w.setNextFrame(time.Time{})
c.w.updateAnimation(c.d) c.w.updateAnimation(c.d)
} }
func (c *callbacks) ActionAt(p f32.Point) (system.Action, bool) { func (c *callbacks) ActionAt(p f32.Point) (system.Action, bool) {
return c.w.queue.ActionAt(p) return c.w.queue.q.ActionAt(p)
} }
func (e *editorState) Replace(r key.Range, text string) { func (e *editorState) Replace(r key.Range, text string) {
@@ -724,7 +744,7 @@ func (w *Window) destroyGPU() {
} }
} }
// waitFrame waits for the client to either call [FrameEvent.Frame] // waitFrame waits for the client to either call FrameEvent.Frame
// or to continue event handling. // or to continue event handling.
func (w *Window) waitFrame(d driver) *op.Ops { func (w *Window) waitFrame(d driver) *op.Ops {
for { for {
@@ -753,7 +773,7 @@ func (w *Window) updateSemantics() {
} }
w.semantic.uptodate = true w.semantic.uptodate = true
w.semantic.prevTree, w.semantic.tree = w.semantic.tree, w.semantic.prevTree w.semantic.prevTree, w.semantic.tree = w.semantic.tree, w.semantic.prevTree
w.semantic.tree = w.queue.AppendSemantics(w.semantic.tree[:0]) w.semantic.tree = w.queue.q.AppendSemantics(w.semantic.tree[:0])
w.semantic.root = w.semantic.tree[0].ID w.semantic.root = w.semantic.tree[0].ID
for _, n := range w.semantic.tree { for _, n := range w.semantic.tree {
w.semantic.ids[n.ID] = n w.semantic.ids[n.ID] = n
@@ -761,7 +781,7 @@ func (w *Window) updateSemantics() {
} }
// collectSemanticDiffs traverses the previous semantic tree, noting changed nodes. // collectSemanticDiffs traverses the previous semantic tree, noting changed nodes.
func (w *Window) collectSemanticDiffs(diffs *[]input.SemanticID, n input.SemanticNode) { func (w *Window) collectSemanticDiffs(diffs *[]router.SemanticID, n router.SemanticNode) {
newNode, exists := w.semantic.ids[n.ID] newNode, exists := w.semantic.ids[n.ID]
// Ignore deleted nodes, as their disappearance will be reported through an // Ignore deleted nodes, as their disappearance will be reported through an
// ancestor node. // ancestor node.
@@ -802,8 +822,8 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
default: default:
} }
switch e2 := e.(type) { switch e2 := e.(type) {
case StageEvent: case system.StageEvent:
if e2.Stage < StageInactive { if e2.Stage < system.StageInactive {
if w.gpu != nil { if w.gpu != nil {
w.ctx.Lock() w.ctx.Lock()
w.gpu.Release() w.gpu.Release()
@@ -819,14 +839,18 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
if e2.Size == (image.Point{}) { if e2.Size == (image.Point{}) {
panic(errors.New("internal error: zero-sized Draw")) panic(errors.New("internal error: zero-sized Draw"))
} }
if w.stage < StageInactive { if w.stage < system.StageInactive {
// No drawing if not visible. // No drawing if not visible.
break break
} }
w.metric = e2.Metric w.metric = e2.Metric
var frameStart time.Time
if w.queue.q.Profiling() {
frameStart = time.Now()
}
w.hasNextFrame = false w.hasNextFrame = false
e2.Frame = w.update e2.Frame = w.update
e2.Source = w.queue.Source() e2.Queue = &w.queue
// Prepare the decorations and update the frame insets. // Prepare the decorations and update the frame insets.
wrapper := &w.decorations.Ops wrapper := &w.decorations.Ops
@@ -843,7 +867,7 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
} }
// Scroll to focus if viewport is shrinking in any dimension. // Scroll to focus if viewport is shrinking in any dimension.
if old, new := w.viewport.Size(), viewport.Size(); new.X < old.X || new.Y < old.Y { if old, new := w.viewport.Size(), viewport.Size(); new.X < old.X || new.Y < old.Y {
w.queue.RevealFocus(viewport) w.queue.q.RevealFocus(viewport)
} }
w.viewport = viewport w.viewport = viewport
viewSize := e2.Size viewSize := e2.Size
@@ -863,13 +887,13 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
deco.Add(wrapper) deco.Add(wrapper)
if err := w.validateAndProcess(d, viewSize, e2.Sync, wrapper, signal); err != nil { if err := w.validateAndProcess(d, viewSize, e2.Sync, wrapper, signal); err != nil {
w.destroyGPU() w.destroyGPU()
w.out <- DestroyEvent{Err: err} w.out <- system.DestroyEvent{Err: err}
close(w.destroy) close(w.destroy)
break break
} }
w.processFrame(d) w.processFrame(d, frameStart)
w.updateCursor(d) w.updateCursor(d)
case DestroyEvent: case system.DestroyEvent:
w.destroyGPU() w.destroyGPU()
w.out <- e2 w.out <- e2
close(w.destroy) close(w.destroy)
@@ -882,37 +906,37 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
w.out <- e2 w.out <- e2
case wakeupEvent: case wakeupEvent:
case event.Event: case event.Event:
focusDir := key.FocusDirection(-1) handled := w.queue.q.Queue(e2)
if e, ok := e2.(key.Event); ok && e.State == key.Press { if e, ok := e.(key.Event); ok && !handled {
isMobile := runtime.GOOS == "ios" || runtime.GOOS == "android" if e.State == key.Press {
switch { handled = true
case e.Name == key.NameTab && e.Modifiers == 0: isMobile := runtime.GOOS == "ios" || runtime.GOOS == "android"
focusDir = key.FocusForward switch {
case e.Name == key.NameTab && e.Modifiers == key.ModShift: case e.Name == key.NameTab && e.Modifiers == 0:
focusDir = key.FocusBackward w.moveFocus(router.FocusForward, d)
case e.Name == key.NameUpArrow && e.Modifiers == 0 && isMobile: case e.Name == key.NameTab && e.Modifiers == key.ModShift:
focusDir = key.FocusUp w.moveFocus(router.FocusBackward, d)
case e.Name == key.NameDownArrow && e.Modifiers == 0 && isMobile: case e.Name == key.NameUpArrow && e.Modifiers == 0 && isMobile:
focusDir = key.FocusDown w.moveFocus(router.FocusUp, d)
case e.Name == key.NameLeftArrow && e.Modifiers == 0 && isMobile: case e.Name == key.NameDownArrow && e.Modifiers == 0 && isMobile:
focusDir = key.FocusLeft w.moveFocus(router.FocusDown, d)
case e.Name == key.NameRightArrow && e.Modifiers == 0 && isMobile: case e.Name == key.NameLeftArrow && e.Modifiers == 0 && isMobile:
focusDir = key.FocusRight w.moveFocus(router.FocusLeft, d)
case e.Name == key.NameRightArrow && e.Modifiers == 0 && isMobile:
w.moveFocus(router.FocusRight, d)
default:
handled = false
}
}
// As a special case, the top-most input handler receives all unhandled
// events.
if !handled {
handled = w.queue.q.QueueTopmost(e)
} }
}
e := e2
if focusDir != -1 {
e = input.SystemEvent{Event: e}
}
w.queue.Queue(e)
t, handled := w.queue.WakeupTime()
if focusDir != -1 && !handled {
w.moveFocus(focusDir)
t, handled = w.queue.WakeupTime()
} }
w.updateCursor(d) w.updateCursor(d)
if handled { if handled {
w.setNextFrame(t) w.setNextFrame(time.Time{})
w.updateAnimation(d) w.updateAnimation(d)
} }
return handled return handled
@@ -921,7 +945,7 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
} }
// NextEvent blocks until an event is received from the window, such as // NextEvent blocks until an event is received from the window, such as
// [FrameEvent]. It blocks forever if called after [DestroyEvent] // [io/system.FrameEvent]. It blocks forever if called after [io/system.DestroyEvent]
// has been returned. // has been returned.
func (w *Window) NextEvent() event.Event { func (w *Window) NextEvent() event.Event {
state := &w.eventState state := &w.eventState
@@ -929,7 +953,7 @@ func (w *Window) NextEvent() event.Event {
state.created = true state.created = true
if err := newWindow(&w.callbacks, state.initialOpts); err != nil { if err := newWindow(&w.callbacks, state.initialOpts); err != nil {
close(w.destroy) close(w.destroy)
return DestroyEvent{Err: err} return system.DestroyEvent{Err: err}
} }
} }
for { for {
@@ -970,7 +994,7 @@ func (w *Window) NextEvent() event.Event {
} }
func (w *Window) updateCursor(d driver) { func (w *Window) updateCursor(d driver) {
if c := w.queue.Cursor(); c != w.cursor { if c := w.queue.q.Cursor(); c != w.cursor {
w.cursor = c w.cursor = c
d.SetCursor(c) d.SetCursor(c)
} }
@@ -982,7 +1006,7 @@ func (w *Window) fallbackDecorate() bool {
} }
// decorate the window if enabled and returns the corresponding Insets. // decorate the window if enabled and returns the corresponding Insets.
func (w *Window) decorate(d driver, e FrameEvent, o *op.Ops) (size, offset image.Point) { func (w *Window) decorate(d driver, e system.FrameEvent, o *op.Ops) (size, offset image.Point) {
if !w.fallbackDecorate() { if !w.fallbackDecorate() {
return e.Size, image.Pt(0, 0) return e.Size, image.Pt(0, 0)
} }
@@ -1008,7 +1032,7 @@ func (w *Window) decorate(d driver, e FrameEvent, o *op.Ops) (size, offset image
gtx := layout.Context{ gtx := layout.Context{
Ops: o, Ops: o,
Now: e.Now, Now: e.Now,
Source: e.Source, Queue: e.Queue,
Metric: e.Metric, Metric: e.Metric,
Constraints: layout.Exact(e.Size), Constraints: layout.Exact(e.Size),
} }
@@ -1061,6 +1085,10 @@ func (w *Window) Perform(actions system.Action) {
} }
} }
func (q *queue) Events(k event.Tag) []event.Event {
return q.q.Events(k)
}
// Title sets the title of the window. // Title sets the title of the window.
func Title(t string) Option { func Title(t string) Option {
return func(_ unit.Metric, cnf *Config) { return func(_ unit.Metric, cnf *Config) {
Generated
+17 -48
View File
@@ -9,11 +9,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1701721028, "lastModified": 1659298920,
"narHash": "sha256-2z4YrdHPLoMZNWR1MPOjNZMqPg057i1eZXaYI6RTahQ=", "narHash": "sha256-LgRMge8BZUG15EN43iDJOlnEMX1dvRprB7SaoNqgibU=",
"owner": "tadfisher", "owner": "tadfisher",
"repo": "android-nixpkgs", "repo": "android-nixpkgs",
"rev": "c923f9ec0f4dd0d7dc725dc5b73fbf03658e50dd", "rev": "d4f20a3cd4ce961bb23b48447457f6810d69ae5e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -24,18 +24,21 @@
}, },
"devshell": { "devshell": {
"inputs": { "inputs": {
"nixpkgs": [ "flake-utils": [
"android", "android",
"nixpkgs" "nixpkgs"
], ],
"systems": "systems" "nixpkgs": [
"android",
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1701697687, "lastModified": 1658746384,
"narHash": "sha256-dLLE5wQBVv+pIb4bWmKFSw2DvLVyuEk0F7ng6hpZPSU=", "narHash": "sha256-CCJcoMOcXyZFrV1ag4XMTpAPjLWb4Anbv+ktXFI1ry0=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "c3bd77911391eb1638af6ce773de86da57ee6df5", "rev": "0ffc7937bb5e8141af03d462b468bd071eb18e1b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -45,15 +48,12 @@
} }
}, },
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems_2"
},
"locked": { "locked": {
"lastModified": 1701680307, "lastModified": 1656928814,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -64,16 +64,15 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1701282334, "lastModified": 1659305579,
"narHash": "sha256-MxCVrXY6v4QmfTwIysjjaX0XUhqBbxTWWB4HXtDYsdk=", "narHash": "sha256-SFeQTmh7hc9Y2fSkooHaoS8mDfPa04sfmUCtQ8MA6Pg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "057f9aecfb71c4437d2b27d3323df7f93c010b7e", "rev": "5857574d45925585baffde730369414319228a84",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "23.11",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@@ -83,36 +82,6 @@
"android": "android", "android": "android",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",
+4 -4
View File
@@ -3,7 +3,7 @@
description = "Gio build environment"; description = "Gio build environment";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/23.11"; nixpkgs.url = "github:NixOS/nixpkgs";
android.url = "github:tadfisher/android-nixpkgs"; android.url = "github:tadfisher/android-nixpkgs";
android.inputs.nixpkgs.follows = "nixpkgs"; android.inputs.nixpkgs.follows = "nixpkgs";
}; };
@@ -33,10 +33,10 @@
default = with pkgs; mkShell default = with pkgs; mkShell
({ ({
ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk"; ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk";
JAVA_HOME = jdk17.home; JAVA_HOME = jdk8.home;
packages = [ packages = [
android-sdk android-sdk
jdk17 jdk8
clang clang
] ++ (if stdenv.isLinux then [ ] ++ (if stdenv.isLinux then [
vulkan-headers vulkan-headers
@@ -46,7 +46,7 @@
xorg.libXcursor xorg.libXcursor
xorg.libXfixes xorg.libXfixes
libGL libGL
pkg-config pkgconfig
] else if stdenv.isDarwin then [ ] else if stdenv.isDarwin then [
darwin.apple_sdk_11_0.frameworks.Foundation darwin.apple_sdk_11_0.frameworks.Foundation
darwin.apple_sdk_11_0.frameworks.Metal darwin.apple_sdk_11_0.frameworks.Metal
+61 -71
View File
@@ -18,7 +18,6 @@ import (
"gioui.org/f32" "gioui.org/f32"
"gioui.org/internal/fling" "gioui.org/internal/fling"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/op" "gioui.org/op"
@@ -38,19 +37,15 @@ type Hover struct {
// Add the gesture to detect hovering over the current pointer area. // Add the gesture to detect hovering over the current pointer area.
func (h *Hover) Add(ops *op.Ops) { func (h *Hover) Add(ops *op.Ops) {
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. // Update state and report whether a pointer is inside the area.
func (h *Hover) Update(q input.Source) bool { func (h *Hover) Update(q event.Queue) bool {
for { for _, ev := range q.Events(h) {
ev, ok := q.Event(pointer.Filter{
Target: h,
Kinds: pointer.Enter | pointer.Leave | pointer.Cancel,
})
if !ok {
break
}
e, ok := ev.(pointer.Event) e, ok := ev.(pointer.Event)
if !ok { if !ok {
continue continue
@@ -112,6 +107,7 @@ type Drag struct {
pressed bool pressed bool
pid pointer.ID pid pointer.ID
start f32.Point start f32.Point
grab bool
} }
// Scroll detects scroll gestures and reduces them to // Scroll detects scroll gestures and reduces them to
@@ -119,9 +115,11 @@ type Drag struct {
// movements as well as drag and fling touch gestures. // movements as well as drag and fling touch gestures.
type Scroll struct { type Scroll struct {
dragging bool dragging bool
axis Axis
estimator fling.Extrapolation estimator fling.Extrapolation
flinger fling.Animation flinger fling.Animation
pid pointer.ID pid pointer.ID
grab bool
last int last int
// Leftover scroll. // Leftover scroll.
scroll float32 scroll float32
@@ -163,7 +161,10 @@ const touchSlop = unit.Dp(3)
// Add the handler to the operation list to receive click events. // Add the handler to the operation list to receive click events.
func (c *Click) Add(ops *op.Ops) { func (c *Click) Add(ops *op.Ops) {
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. // Hovered returns whether a pointer is inside the area.
@@ -176,16 +177,10 @@ func (c *Click) Pressed() bool {
return c.pressed return c.pressed
} }
// Update state and return the next click events, if any. // Update state and return the click events.
func (c *Click) Update(q input.Source) (ClickEvent, bool) { func (c *Click) Update(q event.Queue) []ClickEvent {
for { var events []ClickEvent
evt, ok := q.Event(pointer.Filter{ for _, evt := range q.Events(c) {
Target: c,
Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave | pointer.Cancel,
})
if !ok {
break
}
e, ok := evt.(pointer.Event) e, ok := evt.(pointer.Event)
if !ok { if !ok {
continue continue
@@ -197,15 +192,9 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
} }
c.pressed = false c.pressed = false
if !c.entered || c.hovered { if !c.entered || c.hovered {
return ClickEvent{ events = append(events, ClickEvent{Kind: KindClick, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
Kind: KindClick,
Position: e.Position.Round(),
Source: e.Source,
Modifiers: e.Modifiers,
NumClicks: c.clicks,
}, true
} else { } else {
return ClickEvent{Kind: KindCancel}, true events = append(events, ClickEvent{Kind: KindCancel})
} }
case pointer.Cancel: case pointer.Cancel:
wasPressed := c.pressed wasPressed := c.pressed
@@ -213,7 +202,7 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
c.hovered = false c.hovered = false
c.entered = false c.entered = false
if wasPressed { if wasPressed {
return ClickEvent{Kind: KindCancel}, true events = append(events, ClickEvent{Kind: KindCancel})
} }
case pointer.Press: case pointer.Press:
if c.pressed { if c.pressed {
@@ -235,7 +224,7 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
c.clicks = 1 c.clicks = 1
} }
c.clickedAt = e.Time 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: case pointer.Leave:
if !c.pressed { if !c.pressed {
c.pid = e.PointerID 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() {} func (ClickEvent) ImplementsEvent() {}
// Add the handler to the operation list to receive scroll events. // Add the handler to the operation list to receive scroll events.
// The bounds variable refers to the scrolling boundaries // The bounds variable refers to the scrolling boundaries
// as defined in [pointer.Filter]. // as defined in io/pointer.InputOp.
func (s *Scroll) Add(ops *op.Ops) { func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) {
event.Op(ops, s) 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. // Stop any remaining fling movement.
@@ -271,18 +269,13 @@ func (s *Scroll) Stop() {
} }
// Update state and report the scroll distance along axis. // Update state and report the scroll distance along axis.
func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, bounds image.Rectangle) int { func (s *Scroll) Update(cfg unit.Metric, q event.Queue, t time.Time, axis Axis) int {
total := 0 if s.axis != axis {
f := pointer.Filter{ s.axis = axis
Target: s, return 0
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
ScrollBounds: bounds,
} }
for { total := 0
evt, ok := q.Event(f) for _, evt := range q.Events(s) {
if !ok {
break
}
e, ok := evt.(pointer.Event) e, ok := evt.(pointer.Event)
if !ok { if !ok {
continue continue
@@ -299,7 +292,7 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis,
} }
s.Stop() s.Stop()
s.estimator = fling.Extrapolation{} s.estimator = fling.Extrapolation{}
v := s.val(axis, e.Position) v := s.val(e.Position)
s.last = int(math.Round(float64(v))) s.last = int(math.Round(float64(v)))
s.estimator.Sample(e.Time, v) s.estimator.Sample(e.Time, v)
s.dragging = true s.dragging = true
@@ -315,8 +308,9 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis,
fallthrough fallthrough
case pointer.Cancel: case pointer.Cancel:
s.dragging = false s.dragging = false
s.grab = false
case pointer.Scroll: case pointer.Scroll:
switch axis { switch s.axis {
case Horizontal: case Horizontal:
s.scroll += e.Scroll.X s.scroll += e.Scroll.X
case Vertical: case Vertical:
@@ -329,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 { if !s.dragging || s.pid != e.PointerID {
continue continue
} }
val := s.val(axis, e.Position) val := s.val(e.Position)
s.estimator.Sample(e.Time, val) s.estimator.Sample(e.Time, val)
v := int(math.Round(float64(val))) v := int(math.Round(float64(val)))
dist := s.last - v dist := s.last - v
if e.Priority < pointer.Grabbed { if e.Priority < pointer.Grabbed {
slop := cfg.Dp(touchSlop) slop := cfg.Dp(touchSlop)
if dist := dist; dist >= slop || -slop >= dist { if dist := dist; dist >= slop || -slop >= dist {
q.Execute(pointer.GrabCmd{Tag: s, ID: e.PointerID}) s.grab = true
} }
} else { } else {
s.last = v s.last = v
@@ -345,14 +339,11 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis,
} }
} }
total += s.flinger.Tick(t) total += s.flinger.Tick(t)
if s.flinger.Active() {
q.Execute(op.InvalidateCmd{})
}
return total return total
} }
func (s *Scroll) val(axis Axis, p f32.Point) float32 { func (s *Scroll) val(p f32.Point) float32 {
if axis == Horizontal { if s.axis == Horizontal {
return p.X return p.X
} else { } else {
return p.Y return p.Y
@@ -373,20 +364,18 @@ func (s *Scroll) State() ScrollState {
// Add the handler to the operation list to receive drag events. // Add the handler to the operation list to receive drag events.
func (d *Drag) Add(ops *op.Ops) { func (d *Drag) Add(ops *op.Ops) {
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. // Update state and return the drag events.
func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event, bool) { func (d *Drag) Update(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event {
for { var events []pointer.Event
ev, ok := q.Event(pointer.Filter{ for _, e := range q.Events(d) {
Target: d, e, ok := e.(pointer.Event)
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel,
})
if !ok {
break
}
e, ok := ev.(pointer.Event)
if !ok { if !ok {
continue continue
} }
@@ -419,7 +408,7 @@ func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event
diff := e.Position.Sub(d.start) diff := e.Position.Sub(d.start)
slop := cfg.Dp(touchSlop) slop := cfg.Dp(touchSlop)
if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) { if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
q.Execute(pointer.GrabCmd{Tag: d, ID: e.PointerID}) d.grab = true
} }
} }
case pointer.Release, pointer.Cancel: case pointer.Release, pointer.Cancel:
@@ -428,12 +417,13 @@ func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event
continue continue
} }
d.dragging = false 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. // Dragging reports whether it is currently in use.
+17 -17
View File
@@ -9,8 +9,8 @@ import (
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
) )
@@ -22,21 +22,20 @@ func TestHover(t *testing.T) {
stack := clip.Rect(rect).Push(ops) stack := clip.Rect(rect).Push(ops)
h.Add(ops) h.Add(ops)
stack.Pop() stack.Pop()
r := new(input.Router) r := new(router.Router)
h.Update(r.Source())
r.Frame(ops) r.Frame(ops)
r.Queue( r.Queue(
pointer.Event{Kind: pointer.Move, Position: f32.Pt(30, 30)}, pointer.Event{Kind: pointer.Move, Position: f32.Pt(30, 30)},
) )
if !h.Update(r.Source()) { if !h.Update(r) {
t.Fatal("expected hovered") t.Fatal("expected hovered")
} }
r.Queue( r.Queue(
pointer.Event{Kind: pointer.Move, Position: f32.Pt(50, 50)}, pointer.Event{Kind: pointer.Move, Position: f32.Pt(50, 50)},
) )
if h.Update(r.Source()) { if h.Update(r) {
t.Fatal("expected not hovered") t.Fatal("expected not hovered")
} }
} }
@@ -72,21 +71,12 @@ func TestMouseClicks(t *testing.T) {
var ops op.Ops var ops op.Ops
click.Add(&ops) click.Add(&ops)
var r input.Router var r router.Router
click.Update(r.Source())
r.Frame(&ops) r.Frame(&ops)
r.Queue(tc.events...) r.Queue(tc.events...)
var clicks []ClickEvent events := click.Update(&r)
for { clicks := filterMouseClicks(events)
ev, ok := click.Update(r.Source())
if !ok {
break
}
if ev.Kind == KindClick {
clicks = append(clicks, ev)
}
}
if got, want := len(clicks), len(tc.clicks); got != want { if got, want := len(clicks), len(tc.clicks); got != want {
t.Fatalf("got %d mouse clicks, expected %d", got, want) t.Fatalf("got %d mouse clicks, expected %d", got, want)
} }
@@ -116,3 +106,13 @@ func mouseClickEvents(times ...time.Duration) []event.Event {
} }
return events return events
} }
func filterMouseClicks(events []ClickEvent) []ClickEvent {
var clicks []ClickEvent
for _, ev := range events {
if ev.Kind == KindClick {
clicks = append(clicks, ev)
}
}
return clicks
}
+10 -15
View File
@@ -8,13 +8,8 @@ import (
"gioui.org/internal/f32" "gioui.org/internal/f32"
) )
type textureCacheKey struct { type resourceCache struct {
filter byte res map[interface{}]resourceCacheValue
handle any
}
type textureCache struct {
res map[textureCacheKey]resourceCacheValue
} }
type resourceCacheValue struct { type resourceCacheValue struct {
@@ -42,13 +37,13 @@ type opCacheValue struct {
keep bool keep bool
} }
func newTextureCache() *textureCache { func newResourceCache() *resourceCache {
return &textureCache{ return &resourceCache{
res: make(map[textureCacheKey]resourceCacheValue), 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] v, exists := r.res[key]
if !exists { if !exists {
return nil, false return nil, false
@@ -60,17 +55,17 @@ func (r *textureCache) get(key textureCacheKey) (resource, bool) {
return v.resource, exists 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] v, exists := r.res[key]
if exists && v.used { if exists && v.used {
panic(fmt.Errorf("key exists, %v", key)) panic(fmt.Errorf("key exists, %p", key))
} }
v.used = true v.used = true
v.resource = val v.resource = val
r.res[key] = v r.res[key] = v
} }
func (r *textureCache) frame() { func (r *resourceCache) frame() {
for k, v := range r.res { for k, v := range r.res {
if v.used { if v.used {
v.used = false v.used = false
@@ -82,7 +77,7 @@ func (r *textureCache) frame() {
} }
} }
func (r *textureCache) release() { func (r *resourceCache) release() {
for _, v := range r.res { for _, v := range r.res {
v.resource.release() v.resource.release()
} }
+24
View File
@@ -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
View File
@@ -93,6 +93,7 @@ type compute struct {
} }
} }
timers struct { timers struct {
profile string
t *timers t *timers
compact *timer compact *timer
render *timer render *timer
@@ -175,6 +176,7 @@ type materialUniforms struct {
type collector struct { type collector struct {
hasher maphash.Hash hasher maphash.Hash
profile bool
reader ops.Reader reader ops.Reader
states []f32.Affine2D states []f32.Affine2D
clear bool clear bool
@@ -595,7 +597,7 @@ func (g *compute) frame(target RenderTarget) error {
defer g.ctx.EndFrame() defer g.ctx.EndFrame()
t := &g.timers 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.t = newTimers(g.ctx)
t.compact = t.t.newTimer() t.compact = t.t.newTimer()
t.render = t.t.newTimer() t.render = t.t.newTimer()
@@ -629,13 +631,13 @@ func (g *compute) frame(target RenderTarget) error {
return err return err
} }
t.compact.end() 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 com, ren, blit := t.compact.Elapsed, t.render.Elapsed, t.blit.Elapsed
ft := com + ren + blit ft := com + ren + blit
q := 100 * time.Microsecond q := 100 * time.Microsecond
ft = ft.Round(q) ft = ft.Round(q)
com, ren, blit = com.Round(q), ren.Round(q), blit.Round(q) com, ren, blit = com.Round(q), ren.Round(q), blit.Round(q)
// t.profile = fmt.Sprintf("ft:%7s com: %7s ren:%7s blit:%7s", ft, com, ren, blit) t.profile = fmt.Sprintf("ft:%7s com: %7s ren:%7s blit:%7s", ft, com, ren, blit)
} }
return nil return nil
} }
@@ -659,6 +661,10 @@ func (g *compute) dumpAtlases() {
} }
} }
func (g *compute) Profile() string {
return g.timers.profile
}
func (g *compute) compactAllocs() error { func (g *compute) compactAllocs() error {
const ( const (
maxAllocAge = 3 maxAllocAge = 3
@@ -1650,6 +1656,7 @@ func (e *encoder) line(start, end f32.Point) {
func (c *collector) reset() { func (c *collector) reset() {
c.prevFrame, c.frame = c.frame, c.prevFrame c.prevFrame, c.frame = c.frame, c.prevFrame
c.profile = false
c.clipStates = c.clipStates[:0] c.clipStates = c.clipStates[:0]
c.transStack = c.transStack[:0] c.transStack = c.transStack[:0]
c.frame.reset() c.frame.reset()
@@ -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) c.addClip(&state, fview, fview, nil, ops.Key{}, 0, 0, false)
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
switch ops.OpType(encOp.Data[0]) { switch ops.OpType(encOp.Data[0]) {
case ops.TypeProfile:
c.profile = true
case ops.TypeTransform: case ops.TypeTransform:
dop, push := ops.DecodeTransform(encOp.Data) dop, push := ops.DecodeTransform(encOp.Data)
if push { if push {
+26 -14
View File
@@ -44,10 +44,14 @@ type GPU interface {
Clear(color color.NRGBA) Clear(color color.NRGBA)
// Frame draws the graphics operations from op into a viewport of target. // Frame draws the graphics operations from op into a viewport of target.
Frame(frame *op.Ops, target RenderTarget, viewport image.Point) error Frame(frame *op.Ops, target RenderTarget, viewport image.Point) error
// Profile returns the last available profiling information. Profiling
// information is requested when Frame sees an io/profile.Op, and the result
// is available through Profile at some later time.
Profile() string
} }
type gpu struct { type gpu struct {
cache *textureCache cache *resourceCache
profile string profile string
timers *timers timers *timers
@@ -69,6 +73,7 @@ type renderer struct {
} }
type drawOps struct { type drawOps struct {
profile bool
reader ops.Reader reader ops.Reader
states []f32.Affine2D states []f32.Affine2D
transStack []f32.Affine2D transStack []f32.Affine2D
@@ -354,7 +359,7 @@ func NewWithDevice(d driver.Device) (GPU, error) {
func newGPU(ctx driver.Device) (*gpu, error) { func newGPU(ctx driver.Device) (*gpu, error) {
g := &gpu{ g := &gpu{
cache: newTextureCache(), cache: newResourceCache(),
} }
g.drawOps.pathCache = newOpCache() g.drawOps.pathCache = newOpCache()
if err := g.init(ctx); err != nil { 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.renderer.pather.viewport = viewport
g.drawOps.reset(viewport) g.drawOps.reset(viewport)
g.drawOps.collect(frameOps, 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.frameStart = time.Now()
g.timers = newTimers(g.ctx) g.timers = newTimers(g.ctx)
g.stencilTimer = g.timers.newTimer() g.stencilTimer = g.timers.newTimer()
@@ -420,9 +425,9 @@ func (g *gpu) frame(target RenderTarget) error {
g.stencilTimer.end() g.stencilTimer.end()
g.coverTimer.begin() g.coverTimer.begin()
g.renderer.uploadImages(g.cache, g.drawOps.imageOps) g.renderer.uploadImages(g.cache, g.drawOps.imageOps)
g.renderer.prepareDrawOps(g.drawOps.imageOps) g.renderer.prepareDrawOps(g.cache, g.drawOps.imageOps)
g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers) 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{ d := driver.LoadDesc{
ClearColor: g.drawOps.clearColor, ClearColor: g.drawOps.clearColor,
} }
@@ -432,14 +437,14 @@ func (g *gpu) frame(target RenderTarget) error {
} }
g.ctx.BeginRenderPass(defFBO, d) g.ctx.BeginRenderPass(defFBO, d)
g.ctx.Viewport(0, 0, viewport.X, viewport.Y) g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
g.renderer.drawOps(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.coverTimer.end()
g.ctx.EndRenderPass() g.ctx.EndRenderPass()
g.cleanupTimer.begin() g.cleanupTimer.begin()
g.cache.frame() g.cache.frame()
g.drawOps.pathCache.frame() g.drawOps.pathCache.frame()
g.cleanupTimer.end() g.cleanupTimer.end()
if false && g.timers.ready() { if g.drawOps.profile && g.timers.ready() {
st, covt, cleant := g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed st, covt, cleant := g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
ft := st + covt + cleant ft := st + covt + cleant
q := 100 * time.Microsecond q := 100 * time.Microsecond
@@ -455,8 +460,12 @@ func (g *gpu) Profile() string {
return g.profile return g.profile
} }
func (r *renderer) texHandle(cache *textureCache, data imageOpData) driver.Texture { func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture {
key := textureCacheKey{ type cachekey struct {
filter byte
handle any
}
key := cachekey{
filter: data.filter, filter: data.filter,
handle: data.handle, handle: data.handle,
} }
@@ -844,7 +853,7 @@ func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer {
return layers 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 { if len(r.layers.sizes) == 0 {
return return
} }
@@ -867,7 +876,7 @@ func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
} }
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y) r.ctx.Viewport(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y)
f := r.layerFBOs.fbos[fbo] 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) sr := f32.FRect(v)
uvScale, uvOffset := texSpaceTransform(sr, f.size) uvScale, uvOffset := texSpaceTransform(sr, f.size)
uvTrans := f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset) uvTrans := f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset)
@@ -890,6 +899,7 @@ func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
} }
func (d *drawOps) reset(viewport image.Point) { func (d *drawOps) reset(viewport image.Point) {
d.profile = false
d.viewport = viewport d.viewport = viewport
d.imageOps = d.imageOps[:0] d.imageOps = d.imageOps[:0]
d.pathOps = d.pathOps[:0] d.pathOps = d.pathOps[:0]
@@ -983,6 +993,8 @@ func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) {
loop: loop:
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
switch ops.OpType(encOp.Data[0]) { switch ops.OpType(encOp.Data[0]) {
case ops.TypeProfile:
d.profile = true
case ops.TypeTransform: case ops.TypeTransform:
dop, push := ops.DecodeTransform(encOp.Data) dop, push := ops.DecodeTransform(encOp.Data)
if push { if push {
@@ -1199,7 +1211,7 @@ func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32
return m return m
} }
func (r *renderer) uploadImages(cache *textureCache, ops []imageOp) { func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) {
for i := range ops { for i := range ops {
img := &ops[i] img := &ops[i]
m := img.material m := img.material
@@ -1209,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 { for _, img := range ops {
m := img.material m := img.material
switch m.material { switch m.material {
@@ -1230,7 +1242,7 @@ func (r *renderer) prepareDrawOps(ops []imageOp) {
} }
} }
func (r *renderer) drawOps(isFBO bool, opOff image.Point, 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 var coverTex driver.Texture
for i := 0; i < len(ops); i++ { for i := 0; i < len(ops); i++ {
img := ops[i] img := ops[i]
+1 -1
View File
@@ -665,7 +665,7 @@ func (f *Functions) load(forceES bool) error {
case runtime.GOOS == "android": case runtime.GOOS == "android":
libNames = []string{"libGLESv2.so", "libGLESv3.so"} libNames = []string{"libGLESv2.so", "libGLESv3.so"}
default: default:
libNames = []string{"libGLESv2.so.2", "libGLESv2.so.3.0"} libNames = []string{"libGLESv2.so.2"}
} }
for _, lib := range libNames { for _, lib := range libNames {
if h := dlopen(lib); h != nil { if h := dlopen(lib); h != nil {
+60 -10
View File
@@ -55,19 +55,28 @@ const (
TypePopTransform TypePopTransform
TypePushOpacity TypePushOpacity
TypePopOpacity TypePopOpacity
TypeInvalidate
TypeImage TypeImage
TypePaint TypePaint
TypeColor TypeColor
TypeLinearGradient TypeLinearGradient
TypePass TypePass
TypePopPass TypePopPass
TypeInput TypePointerInput
TypeKeyInputHint TypeClipboardRead
TypeClipboardWrite
TypeSource
TypeTarget
TypeOffer
TypeKeyInput
TypeKeyFocus
TypeKeySoftKeyboard
TypeSave TypeSave
TypeLoad TypeLoad
TypeAux TypeAux
TypeClip TypeClip
TypePopClip TypePopClip
TypeProfile
TypeCursor TypeCursor
TypePath TypePath
TypeStroke TypeStroke
@@ -76,6 +85,8 @@ const (
TypeSemanticClass TypeSemanticClass
TypeSemanticSelected TypeSemanticSelected
TypeSemanticEnabled TypeSemanticEnabled
TypeSnippet
TypeSelection
TypeActionInput TypeActionInput
) )
@@ -137,13 +148,21 @@ const (
TypeLinearGradientLen = 1 + 8*2 + 4*2 TypeLinearGradientLen = 1 + 8*2 + 4*2
TypePassLen = 1 TypePassLen = 1
TypePopPassLen = 1 TypePopPassLen = 1
TypeInputLen = 1 TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4
TypeKeyInputHintLen = 1 + 1 TypeClipboardReadLen = 1
TypeClipboardWriteLen = 1
TypeSourceLen = 1
TypeTargetLen = 1
TypeOfferLen = 1
TypeKeyInputLen = 1 + 1
TypeKeyFocusLen = 1 + 1
TypeKeySoftKeyboardLen = 1 + 1
TypeSaveLen = 1 + 4 TypeSaveLen = 1 + 4
TypeLoadLen = 1 + 4 TypeLoadLen = 1 + 4
TypeAuxLen = 1 TypeAuxLen = 1
TypeClipLen = 1 + 4*4 + 1 + 1 TypeClipLen = 1 + 4*4 + 1 + 1
TypePopClipLen = 1 TypePopClipLen = 1
TypeProfileLen = 1
TypeCursorLen = 2 TypeCursorLen = 2
TypePathLen = 8 + 1 TypePathLen = 8 + 1
TypeStrokeLen = 1 + 4 TypeStrokeLen = 1 + 4
@@ -152,6 +171,8 @@ const (
TypeSemanticClassLen = 2 TypeSemanticClassLen = 2
TypeSemanticSelectedLen = 2 TypeSemanticSelectedLen = 2
TypeSemanticEnabledLen = 2 TypeSemanticEnabledLen = 2
TypeSnippetLen = 1 + 4 + 4
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
TypeActionInputLen = 1 + 1 TypeActionInputLen = 1 + 1
) )
@@ -404,19 +425,28 @@ var opProps = [0x100]opProp{
TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0}, TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0},
TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0}, TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0},
TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0}, TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0},
TypeInvalidate: {Size: TypeRedrawLen, NumRefs: 0},
TypeImage: {Size: TypeImageLen, NumRefs: 2}, TypeImage: {Size: TypeImageLen, NumRefs: 2},
TypePaint: {Size: TypePaintLen, NumRefs: 0}, TypePaint: {Size: TypePaintLen, NumRefs: 0},
TypeColor: {Size: TypeColorLen, NumRefs: 0}, TypeColor: {Size: TypeColorLen, NumRefs: 0},
TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0}, TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0},
TypePass: {Size: TypePassLen, NumRefs: 0}, TypePass: {Size: TypePassLen, NumRefs: 0},
TypePopPass: {Size: TypePopPassLen, NumRefs: 0}, TypePopPass: {Size: TypePopPassLen, NumRefs: 0},
TypeInput: {Size: TypeInputLen, NumRefs: 1}, TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1},
TypeKeyInputHint: {Size: TypeKeyInputHintLen, 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}, TypeSave: {Size: TypeSaveLen, NumRefs: 0},
TypeLoad: {Size: TypeLoadLen, NumRefs: 0}, TypeLoad: {Size: TypeLoadLen, NumRefs: 0},
TypeAux: {Size: TypeAuxLen, NumRefs: 0}, TypeAux: {Size: TypeAuxLen, NumRefs: 0},
TypeClip: {Size: TypeClipLen, NumRefs: 0}, TypeClip: {Size: TypeClipLen, NumRefs: 0},
TypePopClip: {Size: TypePopClipLen, NumRefs: 0}, TypePopClip: {Size: TypePopClipLen, NumRefs: 0},
TypeProfile: {Size: TypeProfileLen, NumRefs: 1},
TypeCursor: {Size: TypeCursorLen, NumRefs: 0}, TypeCursor: {Size: TypeCursorLen, NumRefs: 0},
TypePath: {Size: TypePathLen, NumRefs: 0}, TypePath: {Size: TypePathLen, NumRefs: 0},
TypeStroke: {Size: TypeStrokeLen, NumRefs: 0}, TypeStroke: {Size: TypeStrokeLen, NumRefs: 0},
@@ -425,6 +455,8 @@ var opProps = [0x100]opProp{
TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0}, TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0},
TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0}, TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0},
TypeSemanticEnabled: {Size: TypeSemanticEnabledLen, NumRefs: 0}, TypeSemanticEnabled: {Size: TypeSemanticEnabledLen, NumRefs: 0},
TypeSnippet: {Size: TypeSnippetLen, NumRefs: 2},
TypeSelection: {Size: TypeSelectionLen, NumRefs: 1},
TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0}, TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0},
} }
@@ -457,6 +489,8 @@ func (t OpType) String() string {
return "PushOpacity" return "PushOpacity"
case TypePopOpacity: case TypePopOpacity:
return "PopOpacity" return "PopOpacity"
case TypeInvalidate:
return "Invalidate"
case TypeImage: case TypeImage:
return "Image" return "Image"
case TypePaint: case TypePaint:
@@ -469,10 +503,24 @@ func (t OpType) String() string {
return "Pass" return "Pass"
case TypePopPass: case TypePopPass:
return "PopPass" return "PopPass"
case TypeInput: case TypePointerInput:
return "Input" return "PointerInput"
case TypeKeyInputHint: case TypeClipboardRead:
return "KeyInputHint" 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: case TypeSave:
return "Save" return "Save"
case TypeLoad: case TypeLoad:
@@ -483,6 +531,8 @@ func (t OpType) String() string {
return "Clip" return "Clip"
case TypePopClip: case TypePopClip:
return "PopClip" return "PopClip"
case TypeProfile:
return "Profile"
case TypeCursor: case TypeCursor:
return "Cursor" return "Cursor"
case TypePath: case TypePath:
+24 -11
View File
@@ -3,22 +3,35 @@
package clipboard package clipboard
import ( import (
"io" "gioui.org/internal/ops"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/op"
) )
// WriteCmd copies Text to the clipboard. // Event is generated when the clipboard content is requested.
type WriteCmd struct { type Event struct {
Type string Text string
Data io.ReadCloser
} }
// ReadCmd requests the text of the clipboard, delivered to // ReadOp requests the text of the clipboard, delivered to
// the handler through an [io/transfer.DataEvent]. // the current handler through an Event.
type ReadCmd struct { type ReadOp struct {
Tag event.Tag Tag event.Tag
} }
func (WriteCmd) ImplementsCommand() {} // WriteOp copies Text to the clipboard.
func (ReadCmd) ImplementsCommand() {} 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
View File
@@ -1,12 +1,41 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
// Package event contains types for event handling. /*
Package event contains the types for event handling.
The Queue interface is the protocol for receiving external events.
For example:
var queue event.Queue = ...
for _, e := range queue.Events(h) {
switch e.(type) {
...
}
}
In general, handlers must be declared before events become
available. Other packages such as pointer and key provide
the means for declaring handlers for specific event types.
The following example declares a handler ready for key input:
import gioui.org/io/key
ops := new(op.Ops)
var h *Handler = ...
key.InputOp{Tag: h, Filter: ...}.Add(ops)
*/
package event package event
import ( // Queue maps an event handler key to the events
"gioui.org/internal/ops" // available to the handler.
"gioui.org/op" 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. // Tag is the stable identifier for an event handler.
// For a handler h, the tag is typically &h. // For a handler h, the tag is typically &h.
@@ -16,18 +45,3 @@ type Tag interface{}
type Event interface { type Event interface {
ImplementsEvent() ImplementsEvent()
} }
// Filter represents a filter for [Event] types.
type Filter interface {
ImplementsFilter()
}
// Op declares a tag for input routing at the current transformation
// and clip area hierarchy. It panics if tag is nil.
func Op(o *op.Ops, tag Tag) {
if tag == nil {
panic("Tag must be non-nil")
}
data := ops.Write1(&o.Internal, ops.TypeInputLen, tag)
data[0] = byte(ops.TypeInput)
}
-72
View File
@@ -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
}
-125
View File
@@ -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)
}
}
-15
View File
@@ -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
View File
@@ -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")
}
}
-331
View File
@@ -1,331 +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,
ScrollBounds: image.Rect(-100, -100, 100, 100),
},
}
events(r, -1, filters...)
parent := clip.Rect(image.Rect(1, 1, 14, 39)).Push(ops)
cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops)
event.Op(ops, h)
// Test that h is scrolled even if behind another handler.
event.Op(ops, new(int))
cl.Pop()
parent.Pop()
r.Frame(ops)
r.MoveFocus(key.FocusLeft)
r.RevealFocus(image.Rect(0, 0, 15, 40))
evts := events(r, -1, filters...)
assertScrollEvent(t, evts[len(evts)-1], f32.Pt(6, -9))
}
func TestFocusClick(t *testing.T) {
ops := new(op.Ops)
r := new(Router)
h := new(int)
filters := []event.Filter{
key.FocusFilter{Target: h},
pointer.Filter{
Target: h,
Kinds: pointer.Press | pointer.Release | pointer.Cancel,
},
}
assertEventPointerTypeSequence(t, events(r, -1, filters...), pointer.Cancel)
cl := clip.Rect(image.Rect(0, 0, 10, 10)).Push(ops)
event.Op(ops, h)
cl.Pop()
r.Frame(ops)
r.MoveFocus(key.FocusLeft)
r.ClickFocus()
assertEventPointerTypeSequence(t, events(r, -1, filters...), pointer.Press, pointer.Release)
}
func TestNoFocus(t *testing.T) {
r := new(Router)
r.MoveFocus(key.FocusForward)
}
func TestKeyRouting(t *testing.T) {
r := new(Router)
h := new(int)
A, B := key.Event{Name: "A"}, key.Event{Name: "B"}
// Register filters.
events(r, -1, key.Filter{Name: "A"}, key.Filter{Name: "B"})
r.Frame(new(op.Ops))
r.Queue(A, B)
// The handler is not focused, so only B is delivered.
assertEventSequence(t, events(r, -1, key.Filter{Focus: h, Name: "A"}, key.Filter{Name: "B"}), B)
r.Source().Execute(key.FocusCmd{Tag: h})
// A is delivered to the focused handler.
assertEventSequence(t, events(r, -1, key.Filter{Focus: h, Name: "A"}, key.Filter{Name: "B"}), A)
}
func assertFocus(t *testing.T, router *Router, expected event.Tag) {
t.Helper()
if !router.Source().Focused(expected) {
t.Errorf("expected %v to be focused", expected)
}
}
func assertKeyboard(t *testing.T, router *Router, expected TextInputState) {
t.Helper()
if got := router.state().state; got != expected {
t.Errorf("expected %v keyboard, got %v", expected, got)
}
}
-882
View File
@@ -1,882 +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
processedFilter keyFilter
scratchFilter keyFilter
}
cqueue clipboardQueue
// states is the list of pending state changes resulting from
// incoming events. The first element, if present, contains the state
// and events for the current frame.
changes []stateChange
reader ops.Reader
// InvalidateCmd summary.
wakeup bool
wakeupTime time.Time
// Changes queued for next call to Frame.
commands []Command
// transfers is the pending transfer.DataEvent.Open functions.
transfers []io.ReadCloser
// deferring is set if command execution and event delivery is deferred
// to the next frame.
deferring bool
// scratchFilters is for garbage-free construction of ephemeral filters.
scratchFilters []taggedFilter
}
// Source implements the interface between a Router and user interface widgets.
// The value Source is disabled.
type Source struct {
r *Router
}
// Command represents a request such as moving the focus, or initiating a clipboard read.
// Commands are queued by calling [Source.Queue].
type Command interface {
ImplementsCommand()
}
// SemanticNode represents a node in the tree describing the components
// contained in a frame.
type SemanticNode struct {
ID SemanticID
ParentID SemanticID
Children []SemanticNode
Desc SemanticDesc
areaIdx int
}
// SemanticDesc provides a semantic description of a UI component.
type SemanticDesc struct {
Class semantic.ClassOp
Description string
Label string
Selected bool
Disabled bool
Gestures SemanticGestures
Bounds image.Rectangle
}
// SemanticGestures is a bit-set of supported gestures.
type SemanticGestures int
const (
ClickGesture SemanticGestures = 1 << iota
ScrollGesture
)
// SemanticID uniquely identifies a SemanticDescription.
//
// By convention, the zero value denotes the non-existent ID.
type SemanticID uint
// SystemEvent is a marker for events that have platform specific
// side-effects. SystemEvents are never matched by catch-all filters.
type SystemEvent struct {
Event event.Event
}
// handler contains the per-handler state tracked by a [Router].
type handler struct {
// active tracks whether the handler was active in the current
// frame. Router deletes state belonging to inactive handlers during Frame.
active bool
pointer pointerHandler
key keyHandler
// filter the handler has asked for through event handling
// in the previous frame. It is used for routing events in the
// current frame.
filter filter
// prevFilter is the filter being built in the current frame.
nextFilter filter
// processedFilter is the filters that have exhausted available events.
processedFilter filter
}
// filter is the union of a set of [io/event.Filters].
type filter struct {
pointer pointerFilter
focusable bool
}
// taggedFilter is a filter for a particular tag.
type taggedFilter struct {
tag event.Tag
filter filter
}
// stateChange represents the new state and outgoing events
// resulting from an incoming event.
type stateChange struct {
// event, if set, is the trigger for the change.
event event.Event
state inputState
events []taggedEvent
}
// inputState represent a immutable snapshot of the state required
// to route events.
type inputState struct {
clipboardState
keyState
pointerState
}
// taggedEvent represents an event and its target handler.
type taggedEvent struct {
event event.Event
tag event.Tag
}
// Source returns a Source backed by this Router.
func (q *Router) Source() Source {
return Source{r: q}
}
// Execute a command.
func (s Source) Execute(c Command) {
if !s.Enabled() {
return
}
s.r.execute(c)
}
// Enabled reports whether the source is enabled. Only enabled
// Sources deliver events and respond to commands.
func (s Source) Enabled() bool {
return s.r != nil
}
// Focused reports whether tag is focused, according to the most recent
// [key.FocusEvent] delivered.
func (s Source) Focused(tag event.Tag) bool {
if !s.Enabled() {
return false
}
return s.r.state().keyState.focus == tag
}
// Event returns the next event that matches at least one of filters.
func (s Source) Event(filters ...event.Filter) (event.Event, bool) {
if !s.Enabled() {
return nil, false
}
return s.r.Event(filters...)
}
func (q *Router) Event(filters ...event.Filter) (event.Event, bool) {
// Merge filters into scratch filters.
q.scratchFilters = q.scratchFilters[:0]
q.key.scratchFilter = q.key.scratchFilter[:0]
for _, f := range filters {
var t event.Tag
switch f := f.(type) {
case key.Filter:
q.key.scratchFilter = append(q.key.scratchFilter, f)
continue
case transfer.SourceFilter:
t = f.Target
case transfer.TargetFilter:
t = f.Target
case key.FocusFilter:
t = f.Target
case pointer.Filter:
t = f.Target
}
if t == nil {
continue
}
var filter *filter
for i := range q.scratchFilters {
s := &q.scratchFilters[i]
if s.tag == t {
filter = &s.filter
break
}
}
if filter == nil {
n := len(q.scratchFilters)
if n < cap(q.scratchFilters) {
// Re-use previously allocated filter.
q.scratchFilters = q.scratchFilters[:n+1]
tf := &q.scratchFilters[n]
tf.tag = t
filter = &tf.filter
filter.Reset()
} else {
q.scratchFilters = append(q.scratchFilters, taggedFilter{tag: t})
filter = &q.scratchFilters[n].filter
}
}
filter.Add(f)
}
for _, tf := range q.scratchFilters {
h := q.stateFor(tf.tag)
h.filter.Merge(tf.filter)
h.nextFilter.Merge(tf.filter)
}
q.key.filter = append(q.key.filter, q.key.scratchFilter...)
q.key.nextFilter = append(q.key.nextFilter, q.key.scratchFilter...)
// Deliver reset event, if any.
for _, f := range filters {
switch f := f.(type) {
case key.FocusFilter:
if f.Target == nil {
break
}
h := q.stateFor(f.Target)
if reset, ok := h.key.ResetEvent(); ok {
return reset, true
}
case pointer.Filter:
if f.Target == nil {
break
}
h := q.stateFor(f.Target)
if reset, ok := h.pointer.ResetEvent(); ok && h.filter.pointer.Matches(reset) {
return reset, true
}
}
}
if !q.deferring {
for i := range q.changes {
change := &q.changes[i]
for j, evt := range change.events {
match := false
switch e := evt.event.(type) {
case key.Event:
match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false)
default:
for _, tf := range q.scratchFilters {
if evt.tag == tf.tag && tf.filter.Matches(evt.event) {
match = true
break
}
}
}
if match {
change.events = append(change.events[:j], change.events[j+1:]...)
// Fast forward state to last matched.
q.collapseState(i)
return evt.event, true
}
}
}
}
for _, tf := range q.scratchFilters {
h := q.stateFor(tf.tag)
h.processedFilter.Merge(tf.filter)
}
q.key.processedFilter = append(q.key.processedFilter, q.key.scratchFilter...)
return nil, false
}
// collapseState in the interval [1;idx] into q.changes[0].
func (q *Router) collapseState(idx int) {
if idx == 0 {
return
}
first := &q.changes[0]
first.state = q.changes[idx].state
for i := 1; i <= idx; i++ {
first.events = append(first.events, q.changes[i].events...)
}
q.changes = append(q.changes[:1], q.changes[idx+1:]...)
}
// Frame replaces the declared handlers from the supplied
// operation list. The text input state, wakeup time and whether
// there are active profile handlers is also saved.
func (q *Router) Frame(frame *op.Ops) {
var remaining []event.Event
if n := len(q.changes); n > 0 {
if q.deferring {
// Collect events for replay.
for _, ch := range q.changes[1:] {
remaining = append(remaining, ch.event)
}
q.changes = append(q.changes[:0], stateChange{state: q.changes[0].state})
} else {
// Collapse state.
state := q.changes[n-1].state
q.changes = append(q.changes[:0], stateChange{state: state})
}
}
for _, rc := range q.transfers {
if rc != nil {
rc.Close()
}
}
q.transfers = nil
q.deferring = false
for _, h := range q.handlers {
h.filter, h.nextFilter = h.nextFilter, h.filter
h.nextFilter.Reset()
h.processedFilter.Reset()
h.pointer.Reset()
h.key.Reset()
}
q.key.filter, q.key.nextFilter = q.key.nextFilter, q.key.filter
q.key.nextFilter = q.key.nextFilter[:0]
var ops *ops.Ops
if frame != nil {
ops = &frame.Internal
}
q.reader.Reset(ops)
q.collect()
for k, h := range q.handlers {
if !h.active {
delete(q.handlers, k)
} else {
h.active = false
}
}
q.executeCommands()
q.Queue(remaining...)
st := q.lastState()
pst, evts := q.pointer.queue.Frame(q.handlers, st.pointerState)
st.pointerState = pst
st.keyState = q.key.queue.Frame(q.handlers, q.lastState().keyState)
q.changeState(nil, st, evts)
// Collapse state and events.
q.collapseState(len(q.changes) - 1)
}
// Queue events to be routed.
func (q *Router) Queue(events ...event.Event) {
for _, e := range events {
se, system := e.(SystemEvent)
if system {
e = se.Event
}
q.processEvent(e, system)
}
}
func (f *filter) Add(flt event.Filter) {
switch flt := flt.(type) {
case key.FocusFilter:
f.focusable = true
case pointer.Filter:
f.pointer.Add(flt)
case transfer.SourceFilter, transfer.TargetFilter:
f.pointer.Add(flt)
}
}
// Merge f2 into f.
func (f *filter) Merge(f2 filter) {
f.focusable = f.focusable || f2.focusable
f.pointer.Merge(f2.pointer)
}
func (f *filter) Matches(e event.Event) bool {
switch e.(type) {
case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent:
return f.focusable
default:
return f.pointer.Matches(e)
}
}
func (f *filter) Reset() {
*f = filter{
pointer: pointerFilter{
sourceMimes: f.pointer.sourceMimes[:0],
targetMimes: f.pointer.targetMimes[:0],
},
}
}
func (q *Router) processEvent(e event.Event, system bool) {
state := q.lastState()
switch e := e.(type) {
case pointer.Event:
pstate, evts := q.pointer.queue.Push(q.handlers, state.pointerState, e)
state.pointerState = pstate
q.changeState(e, state, evts)
case key.Event:
var evts []taggedEvent
if q.key.filter.Matches(state.keyState.focus, e, system) {
evts = append(evts, taggedEvent{event: e})
}
q.changeState(e, state, evts)
case key.SnippetEvent:
// Expand existing, overlapping snippet.
if r := state.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) {
if e.Start > r.Start {
e.Start = r.Start
}
if e.End < r.End {
e.End = r.End
}
}
var evts []taggedEvent
if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
q.changeState(e, state, evts)
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
var evts []taggedEvent
if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
q.changeState(e, state, evts)
case transfer.DataEvent:
cstate, evts := q.cqueue.Push(state.clipboardState, e)
state.clipboardState = cstate
q.changeState(e, state, evts)
default:
panic("unknown event type")
}
}
func (q *Router) execute(c Command) {
// The command can be executed immediately if event delivery is not frozen, and
// no event receiver has completed their event handling.
if !q.deferring {
ch := q.executeCommand(c)
immediate := true
for _, e := range ch.events {
h, ok := q.handlers[e.tag]
immediate = immediate && (!ok || !h.processedFilter.Matches(e.event))
}
if immediate {
// Hold on to the remaining events for state replay.
var evts []event.Event
for _, ch := range q.changes {
if ch.event != nil {
evts = append(evts, ch.event)
}
}
if len(q.changes) > 1 {
q.changes = q.changes[:1]
}
q.changeState(nil, ch.state, ch.events)
q.Queue(evts...)
return
}
}
q.deferring = true
q.commands = append(q.commands, c)
}
func (q *Router) state() inputState {
if len(q.changes) > 0 {
return q.changes[0].state
}
return inputState{}
}
func (q *Router) lastState() inputState {
if n := len(q.changes); n > 0 {
return q.changes[n-1].state
}
return inputState{}
}
func (q *Router) executeCommands() {
for _, c := range q.commands {
ch := q.executeCommand(c)
q.changeState(nil, ch.state, ch.events)
}
q.commands = nil
}
// executeCommand the command and return the resulting state change along with the
// tag the state change depended on, if any.
func (q *Router) executeCommand(c Command) stateChange {
state := q.state()
var evts []taggedEvent
switch req := c.(type) {
case key.SelectionCmd:
state.keyState = q.key.queue.setSelection(state.keyState, req)
case key.FocusCmd:
state.keyState, evts = q.key.queue.Focus(q.handlers, state.keyState, req.Tag)
case key.SoftKeyboardCmd:
state.keyState = state.keyState.softKeyboard(req.Show)
case key.SnippetCmd:
state.keyState = q.key.queue.setSnippet(state.keyState, req)
case transfer.OfferCmd:
state.pointerState, evts = q.pointer.queue.offerData(q.handlers, state.pointerState, req)
case clipboard.WriteCmd:
q.cqueue.ProcessWriteClipboard(req)
case clipboard.ReadCmd:
state.clipboardState = q.cqueue.ProcessReadClipboard(state.clipboardState, req.Tag)
case pointer.GrabCmd:
state.pointerState, evts = q.pointer.queue.grab(state.pointerState, req)
case op.InvalidateCmd:
if !q.wakeup || req.At.Before(q.wakeupTime) {
q.wakeup = true
q.wakeupTime = req.At
}
}
return stateChange{state: state, events: evts}
}
func (q *Router) changeState(e event.Event, state inputState, evts []taggedEvent) {
// Wrap pointer.DataEvent.Open functions to detect them not being called.
for i := range evts {
e := &evts[i]
if de, ok := e.event.(transfer.DataEvent); ok {
transferIdx := len(q.transfers)
data := de.Open()
q.transfers = append(q.transfers, data)
de.Open = func() io.ReadCloser {
q.transfers[transferIdx] = nil
return data
}
e.event = de
}
}
// Initialize the first change to contain the current state
// and events that are bound for the current frame.
if len(q.changes) == 0 {
q.changes = append(q.changes, stateChange{})
}
if e != nil && len(evts) > 0 {
// An event triggered events bound for user receivers. Add a state change to be
// able to redo the change in case of a command execution.
q.changes = append(q.changes, stateChange{event: e, state: state, events: evts})
} else {
// Otherwise, merge with previous change.
prev := &q.changes[len(q.changes)-1]
prev.state = state
prev.events = append(prev.events, evts...)
}
}
func rangeOverlaps(r1, r2 key.Range) bool {
r1 = rangeNorm(r1)
r2 = rangeNorm(r2)
return r1.Start <= r2.Start && r2.Start < r1.End ||
r1.Start <= r2.End && r2.End < r1.End
}
func rangeNorm(r key.Range) key.Range {
if r.End < r.Start {
r.End, r.Start = r.Start, r.End
}
return r
}
func (q *Router) MoveFocus(dir key.FocusDirection) {
state := q.lastState()
kstate, evts := q.key.queue.MoveFocus(q.handlers, state.keyState, dir)
state.keyState = kstate
q.changeState(nil, state, evts)
}
// RevealFocus scrolls the current focus (if any) into viewport
// if there are scrollable parent handlers.
func (q *Router) RevealFocus(viewport image.Rectangle) {
state := q.lastState()
focus := state.focus
if focus == nil {
return
}
kh := &q.handlers[focus].key
bounds := q.key.queue.BoundsFor(kh)
area := q.key.queue.AreaFor(kh)
viewport = q.pointer.queue.ClipFor(area, viewport)
topleft := bounds.Min.Sub(viewport.Min)
topleft = max(topleft, bounds.Max.Sub(viewport.Max))
topleft = min(image.Pt(0, 0), topleft)
bottomright := bounds.Max.Sub(viewport.Max)
bottomright = min(bottomright, bounds.Min.Sub(viewport.Min))
bottomright = max(image.Pt(0, 0), bottomright)
s := topleft
if s.X == 0 {
s.X = bottomright.X
}
if s.Y == 0 {
s.Y = bottomright.Y
}
q.ScrollFocus(s)
}
// ScrollFocus scrolls the focused widget, if any, by dist.
func (q *Router) ScrollFocus(dist image.Point) {
state := q.lastState()
focus := state.focus
if focus == nil {
return
}
kh := &q.handlers[focus].key
area := q.key.queue.AreaFor(kh)
q.changeState(nil, q.lastState(), q.pointer.queue.Deliver(q.handlers, area, pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Touch,
Scroll: f32internal.FPt(dist),
}))
}
func max(p1, p2 image.Point) image.Point {
m := p1
if p2.X > m.X {
m.X = p2.X
}
if p2.Y > m.Y {
m.Y = p2.Y
}
return m
}
func min(p1, p2 image.Point) image.Point {
m := p1
if p2.X < m.X {
m.X = p2.X
}
if p2.Y < m.Y {
m.Y = p2.Y
}
return m
}
func (q *Router) ActionAt(p f32.Point) (system.Action, bool) {
return q.pointer.queue.ActionAt(p)
}
func (q *Router) ClickFocus() {
focus := q.lastState().focus
if focus == nil {
return
}
kh := &q.handlers[focus].key
bounds := q.key.queue.BoundsFor(kh)
center := bounds.Max.Add(bounds.Min).Div(2)
e := pointer.Event{
Position: f32.Pt(float32(center.X), float32(center.Y)),
Source: pointer.Touch,
}
area := q.key.queue.AreaFor(kh)
e.Kind = pointer.Press
state := q.lastState()
q.changeState(nil, state, q.pointer.queue.Deliver(q.handlers, area, e))
e.Kind = pointer.Release
q.changeState(nil, state, q.pointer.queue.Deliver(q.handlers, area, e))
}
// TextInputState returns the input state from the most recent
// call to Frame.
func (q *Router) TextInputState() TextInputState {
state := q.state()
kstate, s := state.InputState()
state.keyState = kstate
q.changeState(nil, state, nil)
return s
}
// TextInputHint returns the input mode from the most recent key.InputOp.
func (q *Router) TextInputHint() (key.InputHint, bool) {
return q.key.queue.InputHint(q.handlers, q.state().keyState)
}
// WriteClipboard returns the most recent content to be copied
// to the clipboard, if any.
func (q *Router) WriteClipboard() (mime string, content []byte, ok bool) {
return q.cqueue.WriteClipboard()
}
// ClipboardRequested reports if any new handler is waiting
// to read the clipboard.
func (q *Router) ClipboardRequested() bool {
return q.cqueue.ClipboardRequested(q.lastState().clipboardState)
}
// Cursor returns the last cursor set.
func (q *Router) Cursor() pointer.Cursor {
return q.state().cursor
}
// SemanticAt returns the first semantic description under pos, if any.
func (q *Router) SemanticAt(pos f32.Point) (SemanticID, bool) {
return q.pointer.queue.SemanticAt(pos)
}
// AppendSemantics appends the semantic tree to nodes, and returns the result.
// The root node is the first added.
func (q *Router) AppendSemantics(nodes []SemanticNode) []SemanticNode {
q.pointer.collector.q = &q.pointer.queue
q.pointer.collector.ensureRoot()
return q.pointer.queue.AppendSemantics(nodes)
}
// EditorState returns the editor state for the focused handler, or the
// zero value if there is none.
func (q *Router) EditorState() EditorState {
return q.key.queue.editorState(q.handlers, q.state().keyState)
}
func (q *Router) stateFor(tag event.Tag) *handler {
if tag == nil {
panic("internal error: nil tag")
}
s, ok := q.handlers[tag]
if !ok {
s = new(handler)
if q.handlers == nil {
q.handlers = make(map[event.Tag]*handler)
}
q.handlers[tag] = s
}
s.active = true
return s
}
func (q *Router) collect() {
q.transStack = q.transStack[:0]
pc := &q.pointer.collector
pc.q = &q.pointer.queue
pc.Reset()
kq := &q.key.queue
q.key.queue.Reset()
var t f32.Affine2D
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
switch ops.OpType(encOp.Data[0]) {
case ops.TypeSave:
id := ops.DecodeSave(encOp.Data)
if extra := id - len(q.savedTrans) + 1; extra > 0 {
q.savedTrans = append(q.savedTrans, make([]f32.Affine2D, extra)...)
}
q.savedTrans[id] = t
case ops.TypeLoad:
id := ops.DecodeLoad(encOp.Data)
t = q.savedTrans[id]
pc.resetState()
pc.setTrans(t)
case ops.TypeClip:
var op ops.ClipOp
op.Decode(encOp.Data)
pc.clip(op)
case ops.TypePopClip:
pc.popArea()
case ops.TypeTransform:
t2, push := ops.DecodeTransform(encOp.Data)
if push {
q.transStack = append(q.transStack, t)
}
t = t.Mul(t2)
pc.setTrans(t)
case ops.TypePopTransform:
n := len(q.transStack)
t = q.transStack[n-1]
q.transStack = q.transStack[:n-1]
pc.setTrans(t)
case ops.TypeInput:
tag := encOp.Refs[0].(event.Tag)
s := q.stateFor(tag)
pc.inputOp(tag, &s.pointer)
a := pc.currentArea()
b := pc.currentAreaBounds()
if s.filter.focusable {
kq.inputOp(tag, &s.key, t, a, b)
}
// Pointer ops.
case ops.TypePass:
pc.pass()
case ops.TypePopPass:
pc.popPass()
case ops.TypeCursor:
name := pointer.Cursor(encOp.Data[1])
pc.cursor(name)
case ops.TypeActionInput:
act := system.Action(encOp.Data[1])
pc.actionInputOp(act)
case ops.TypeKeyInputHint:
op := key.InputHintOp{
Tag: encOp.Refs[0].(event.Tag),
Hint: key.InputHint(encOp.Data[1]),
}
s := q.stateFor(op.Tag)
s.key.inputHint(op.Hint)
// Semantic ops.
case ops.TypeSemanticLabel:
lbl := *encOp.Refs[0].(*string)
pc.semanticLabel(lbl)
case ops.TypeSemanticDesc:
desc := *encOp.Refs[0].(*string)
pc.semanticDesc(desc)
case ops.TypeSemanticClass:
class := semantic.ClassOp(encOp.Data[1])
pc.semanticClass(class)
case ops.TypeSemanticSelected:
if encOp.Data[1] != 0 {
pc.semanticSelected(true)
} else {
pc.semanticSelected(false)
}
case ops.TypeSemanticEnabled:
if encOp.Data[1] != 0 {
pc.semanticEnabled(true)
} else {
pc.semanticEnabled(false)
}
}
}
}
// WakeupTime returns the most recent time for doing another frame,
// as determined from the last call to Frame.
func (q *Router) WakeupTime() (time.Time, bool) {
t, w := q.wakeupTime, q.wakeup
q.wakeup = false
// Pending events always trigger wakeups.
if len(q.changes) > 1 || len(q.changes) == 1 && len(q.changes[0].events) > 0 {
t, w = time.Time{}, true
}
return t, w
}
func (s SemanticGestures) String() string {
var gestures []string
if s&ClickGesture != 0 {
gestures = append(gestures, "Click")
}
return strings.Join(gestures, ",")
}
func (SystemEvent) ImplementsEvent() {}
-34
View File
@@ -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
View File
@@ -1,9 +1,18 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
// Package key implements key and text events and operations. /*
Package key implements key and text events and operations.
The InputOp operations is used for declaring key input handlers. Use
an implementation of the Queue interface from package ui to receive
events.
*/
package key package key
import ( import (
"encoding/binary"
"fmt"
"math"
"strings" "strings"
"gioui.org/f32" "gioui.org/f32"
@@ -12,40 +21,59 @@ import (
"gioui.org/op" "gioui.org/op"
) )
// Filter matches any [Event] that matches the parameters. // InputOp declares a handler ready for key events.
type Filter struct { // Key events are in general only delivered to the
// Focus is the tag that must be focused for the filter to match. It has no effect // focused key handler.
// if it is nil. type InputOp struct {
Focus event.Tag Tag event.Tag
// Required is the set of modifiers that must be included in events matched. // Hint describes the type of text expected by Tag.
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
Hint InputHint 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. // Set is an expression that describes a set of key combinations, in the form
type SoftKeyboardCmd struct { // "<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 Show bool
} }
// SelectionCmd updates the selection for an input handler. // FocusOp sets or clears the keyboard focus. It replaces any previous
type SelectionCmd struct { // 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 Tag event.Tag
Range Range
Caret Caret
} }
// SnippetCmd updates the content snippet for an input handler. // SnippetOp updates the content snippet for an input handler.
type SnippetCmd struct { type SnippetOp struct {
Tag event.Tag Tag event.Tag
Snippet Snippet
} }
@@ -90,8 +118,11 @@ type FocusEvent struct {
// An Event is generated when a key is pressed. For text input // An Event is generated when a key is pressed. For text input
// use EditEvent. // use EditEvent.
type Event struct { type Event struct {
// Name of the key. // Name of the key. For letters, the upper case form is used, via
Name Name // 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 is the set of active modifiers when the key was pressed.
Modifiers Modifiers Modifiers Modifiers
// State is the state of the key when the event was fired. // State is the state of the key when the event was fired.
@@ -105,13 +136,6 @@ type EditEvent struct {
Text string Text string
} }
// FocusFilter matches any [FocusEvent], [EditEvent], [SnippetEvent],
// or [SelectionEvent] with the specified target.
type FocusFilter struct {
// Target is a tag specified in a previous event.Op.
Target event.Tag
}
// InputHint changes the on-screen-keyboard type. That hints the // InputHint changes the on-screen-keyboard type. That hints the
// type of data that might be entered by the user. // type of data that might be entered by the user.
type InputHint uint8 type InputHint uint8
@@ -165,60 +189,41 @@ const (
ModSuper ModSuper
) )
// Name is the identifier for a keyboard key.
//
// For letters, the upper case form is used, via unicode.ToUpper.
// The shift modifier is taken into account, all other
// modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1"
// combinations both give the Name "!" with the US keyboard layout.
type Name string
const ( const (
// Names for special keys. // Names for special keys.
NameLeftArrow Name = "←" NameLeftArrow = "←"
NameRightArrow Name = "→" NameRightArrow = "→"
NameUpArrow Name = "↑" NameUpArrow = "↑"
NameDownArrow Name = "↓" NameDownArrow = "↓"
NameReturn Name = "⏎" NameReturn = "⏎"
NameEnter Name = "⌤" NameEnter = "⌤"
NameEscape Name = "⎋" NameEscape = "⎋"
NameHome Name = "⇱" NameHome = "⇱"
NameEnd Name = "⇲" NameEnd = "⇲"
NameDeleteBackward Name = "⌫" NameDeleteBackward = "⌫"
NameDeleteForward Name = "⌦" NameDeleteForward = "⌦"
NamePageUp Name = "⇞" NamePageUp = "⇞"
NamePageDown Name = "⇟" NamePageDown = "⇟"
NameTab Name = "Tab" NameTab = "Tab"
NameSpace Name = "Space" NameSpace = "Space"
NameCtrl Name = "Ctrl" NameCtrl = "Ctrl"
NameShift Name = "Shift" NameShift = "Shift"
NameAlt Name = "Alt" NameAlt = "Alt"
NameSuper Name = "Super" NameSuper = "Super"
NameCommand Name = "⌘" NameCommand = "⌘"
NameF1 Name = "F1" NameF1 = "F1"
NameF2 Name = "F2" NameF2 = "F2"
NameF3 Name = "F3" NameF3 = "F3"
NameF4 Name = "F4" NameF4 = "F4"
NameF5 Name = "F5" NameF5 = "F5"
NameF6 Name = "F6" NameF6 = "F6"
NameF7 Name = "F7" NameF7 = "F7"
NameF8 Name = "F8" NameF8 = "F8"
NameF9 Name = "F9" NameF9 = "F9"
NameF10 Name = "F10" NameF10 = "F10"
NameF11 Name = "F11" NameF11 = "F11"
NameF12 Name = "F12" NameF12 = "F12"
NameBack Name = "Back" NameBack = "Back"
)
type FocusDirection int
const (
FocusRight FocusDirection = iota
FocusLeft
FocusUp
FocusDown
FocusForward
FocusBackward
) )
// Contain reports whether m contains all modifiers // Contain reports whether m contains all modifiers
@@ -227,52 +232,161 @@ func (m Modifiers) Contain(m2 Modifiers) bool {
return m&m2 == m2 return m&m2 == m2
} }
// FocusCmd requests to set or clear the keyboard focus. func (k Set) Contains(name string, mods Modifiers) bool {
type FocusCmd struct { ks := string(k)
// Tag is the new focus. The focus is cleared if Tag is nil, or if Tag for len(ks) > 0 {
// has no [event.Op] references. // Cut next key expression.
Tag event.Tag 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 { if h.Tag == nil {
panic("Tag must be non-nil") panic("Tag must be non-nil")
} }
data := ops.Write1(&o.Internal, ops.TypeKeyInputHintLen, h.Tag) data := ops.Write2String(&o.Internal, ops.TypeKeyInputLen, h.Tag, string(h.Keys))
data[0] = byte(ops.TypeKeyInputHint) data[0] = byte(ops.TypeKeyInput)
data[1] = byte(h.Hint) data[1] = byte(h.Hint)
} }
func (h SoftKeyboardOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeKeySoftKeyboardLen)
data[0] = byte(ops.TypeKeySoftKeyboard)
if h.Show {
data[1] = 1
}
}
func (h FocusOp) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeKeyFocusLen, h.Tag)
data[0] = byte(ops.TypeKeyFocus)
}
func (s SnippetOp) Add(o *op.Ops) {
data := ops.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 (EditEvent) ImplementsEvent() {}
func (Event) ImplementsEvent() {} func (Event) ImplementsEvent() {}
func (FocusEvent) ImplementsEvent() {} func (FocusEvent) ImplementsEvent() {}
func (SnippetEvent) ImplementsEvent() {} func (SnippetEvent) ImplementsEvent() {}
func (SelectionEvent) ImplementsEvent() {} func (SelectionEvent) ImplementsEvent() {}
func (FocusCmd) ImplementsCommand() {} func (e Event) String() string {
func (SoftKeyboardCmd) ImplementsCommand() {} return fmt.Sprintf("%v %v %v}", e.Name, e.Modifiers, e.State)
func (SelectionCmd) ImplementsCommand() {} }
func (SnippetCmd) ImplementsCommand() {}
func (Filter) ImplementsFilter() {}
func (FocusFilter) ImplementsFilter() {}
func (m Modifiers) String() string { func (m Modifiers) String() string {
var strs []string var strs []string
if m.Contain(ModCtrl) { if m.Contain(ModCtrl) {
strs = append(strs, string(NameCtrl)) strs = append(strs, NameCtrl)
} }
if m.Contain(ModCommand) { if m.Contain(ModCommand) {
strs = append(strs, string(NameCommand)) strs = append(strs, NameCommand)
} }
if m.Contain(ModShift) { if m.Contain(ModShift) {
strs = append(strs, string(NameShift)) strs = append(strs, NameShift)
} }
if m.Contain(ModAlt) { if m.Contain(ModAlt) {
strs = append(strs, string(NameAlt)) strs = append(strs, NameAlt)
} }
if m.Contain(ModSuper) { if m.Contain(ModSuper) {
strs = append(strs, string(NameSuper)) strs = append(strs, NameSuper)
} }
return strings.Join(strs, "-") return strings.Join(strs, "-")
} }
+35
View File
@@ -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
View File
@@ -5,19 +5,36 @@ Package pointer implements pointer events and operations.
A pointer is either a mouse controlled cursor or a touch A pointer is either a mouse controlled cursor or a touch
object such as a finger. object such as a finger.
The [event.Op] operation is used to declare a handler ready for pointer The InputOp operation is used to declare a handler ready for pointer
events. 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 # Hit areas
Clip operations from package [op/clip] are used for specifying Clip operations from package op/clip are used for specifying
hit areas where handlers may receive events. hit areas where subsequent InputOps are active.
For example, to set up a handler with a rectangular hit area: For example, to set up a handler with a rectangular hit area:
r := image.Rectangle{...} r := image.Rectangle{...}
area := clip.Rect(r).Push(ops) area := clip.Rect(r).Push(ops)
event.Op{Tag: h}.Add(ops) pointer.InputOp{Tag: h}.Add(ops)
area.Pop() area.Pop()
Note that hit areas behave similar to painting: the effective area of a stack Note that hit areas behave similar to painting: the effective area of a stack
@@ -37,11 +54,11 @@ For example:
var h1, h2 *Handler var h1, h2 *Handler
area := clip.Rect(...).Push(ops) area := clip.Rect(...).Push(ops)
event.Op{Tag: h1}.Add(Ops) pointer.InputOp{Tag: h1}.Add(Ops)
area.Pop() area.Pop()
area := clip.Rect(...).Push(ops) area := clip.Rect(...).Push(ops)
event.Op{Tag: h2}.Add(ops) pointer.InputOp{Tag: h2}.Add(ops)
area.Pop() area.Pop()
implies a tree of two inner nodes, each with one pointer handler attached. implies a tree of two inner nodes, each with one pointer handler attached.
+35 -17
View File
@@ -3,6 +3,8 @@
package pointer package pointer
import ( import (
"encoding/binary"
"fmt"
"image" "image"
"strings" "strings"
"time" "time"
@@ -54,12 +56,14 @@ type PassStack struct {
macroID uint32 macroID uint32
} }
// Filter matches every [Event] that target the Tag and whose kind is // InputOp declares an input handler ready for pointer
// included in Kinds. Note that only tags specified in [event.Op] can // events.
// be targeted by pointer events. type InputOp struct {
type Filter struct { Tag event.Tag
Target event.Tag // Grab, if set, request that the handler get
// Kinds is a bitwise-or of event types to match. // Grabbed priority.
Grab bool
// Kinds is a bitwise-or of event types to receive.
Kinds Kind Kinds Kind
// ScrollBounds describe the maximum scrollable distances in both // ScrollBounds describe the maximum scrollable distances in both
// axes. Specifically, any Event e delivered to Tag will satisfy // axes. Specifically, any Event e delivered to Tag will satisfy
@@ -69,12 +73,6 @@ type Filter struct {
ScrollBounds image.Rectangle ScrollBounds image.Rectangle
} }
// GrabCmd requests a pointer grab on the pointer identified by ID.
type GrabCmd struct {
Tag event.Tag
ID ID
}
type ID uint16 type ID uint16
// Kind of an Event. // Kind of an Event.
@@ -173,7 +171,7 @@ const (
const ( const (
// A Cancel event is generated when the current gesture is // A Cancel event is generated when the current gesture is
// interrupted by other handlers or the system. // interrupted by other handlers or the system.
Cancel Kind = 1 << iota Cancel Kind = (1 << iota) >> 1
// Press of a pointer. // Press of a pointer.
Press Press
// Release of a pointer. // Release of a pointer.
@@ -239,6 +237,30 @@ func (op Cursor) Add(o *op.Ops) {
data[1] = byte(op) data[1] = byte(op)
} }
// Add panics if the scroll range does not contain zero.
func (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 { func (t Kind) String() string {
if t == Cancel { if t == Cancel {
return "Cancel" return "Cancel"
@@ -382,7 +404,3 @@ func (c Cursor) String() string {
} }
func (Event) ImplementsEvent() {} func (Event) ImplementsEvent() {}
func (GrabCmd) ImplementsCommand() {}
func (Filter) ImplementsFilter() {}
+31
View File
@@ -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() {}
+57
View File
@@ -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
}
}
+154
View File
@@ -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)
}
}
+353
View File
@@ -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")
}
}
+427
View File
@@ -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)
}
}
+274 -313
View File
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
package input package router
import ( import (
"image" "image"
@@ -10,6 +10,7 @@ import (
f32internal "gioui.org/internal/f32" f32internal "gioui.org/internal/f32"
"gioui.org/internal/ops" "gioui.org/internal/ops"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/io/system" "gioui.org/io/system"
@@ -17,8 +18,14 @@ import (
) )
type pointerQueue struct { type pointerQueue struct {
hitTree []hitNode hitTree []hitNode
areas []areaNode areas []areaNode
cursor pointer.Cursor
handlers map[event.Tag]*pointerHandler
pointers []pointerInfo
transfers []io.ReadCloser // pending data transfers
scratch []event.Tag
semantic struct { semantic struct {
idsAssigned bool idsAssigned bool
@@ -36,15 +43,10 @@ type hitNode struct {
// For handler nodes. // For handler nodes.
tag event.Tag tag event.Tag
ktag event.Tag
pass bool pass bool
} }
// pointerState is the input state related to pointer events.
type pointerState struct {
cursor pointer.Cursor
pointers []pointerInfo
}
type pointerInfo struct { type pointerInfo struct {
id pointer.ID id pointer.ID
pressed bool pressed bool
@@ -61,21 +63,17 @@ type pointerInfo struct {
} }
type pointerHandler struct { type pointerHandler struct {
// areaPlusOne is the index into the list of pointerQueue.areas, plus 1. area int
areaPlusOne int active bool
// setup tracks whether the handler has received wantsGrab bool
// the pointer.Cancel event that resets its state. types pointer.Kind
setup bool
}
// pointerFilter represents the union of a set of pointer filters.
type pointerFilter struct {
kinds pointer.Kind
// min and max horizontal/vertical scroll // min and max horizontal/vertical scroll
scrollRange image.Rectangle scrollRange image.Rectangle
sourceMimes []string sourceMimes []string
targetMimes []string targetMimes []string
offeredMime string
data io.ReadCloser
} }
type areaOp struct { type areaOp struct {
@@ -231,18 +229,33 @@ func (c *pointerCollector) addHitNode(n hitNode) {
} }
// newHandler returns the current handler or a new one for tag. // newHandler returns the current handler or a new one for tag.
func (c *pointerCollector) newHandler(tag event.Tag, state *pointerHandler) { func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *pointerHandler {
areaID := c.currentArea() areaID := c.currentArea()
c.addHitNode(hitNode{ c.addHitNode(hitNode{
area: areaID, area: areaID,
tag: tag, tag: tag,
pass: c.state.pass > 0, pass: c.state.pass > 0,
}) })
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() { func (c *pointerCollector) keyInputOp(op key.InputOp) {
s.areaPlusOne = 0 areaID := c.currentArea()
c.addHitNode(hitNode{
area: areaID,
ktag: op.Tag,
pass: true,
})
} }
func (c *pointerCollector) actionInputOp(act system.Action) { func (c *pointerCollector) actionInputOp(act system.Action) {
@@ -251,109 +264,21 @@ func (c *pointerCollector) actionInputOp(act system.Action) {
area.action = act area.action = act
} }
func (q *pointerQueue) grab(state pointerState, req pointer.GrabCmd) (pointerState, []taggedEvent) { func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
var evts []taggedEvent
for _, p := range state.pointers {
if !p.pressed || p.id != req.ID {
continue
}
// Drop other handlers that lost their grab.
for i := len(p.handlers) - 1; i >= 0; i-- {
if tag := p.handlers[i]; tag != req.Tag {
evts = append(evts, taggedEvent{
tag: tag,
event: pointer.Event{Kind: pointer.Cancel},
})
state = dropHandler(state, tag)
}
}
break
}
return state, evts
}
func (c *pointerCollector) inputOp(tag event.Tag, state *pointerHandler) {
areaID := c.currentArea() areaID := c.currentArea()
area := &c.q.areas[areaID] area := &c.q.areas[areaID]
area.semantic.content.tag = tag area.semantic.content.tag = op.Tag
c.newHandler(tag, state) if op.Kinds&(pointer.Press|pointer.Release) != 0 {
} area.semantic.content.gestures |= ClickGesture
func (p *pointerFilter) Add(f event.Filter) {
switch f := f.(type) {
case transfer.SourceFilter:
for _, m := range p.sourceMimes {
if m == f.Type {
return
}
}
p.sourceMimes = append(p.sourceMimes, f.Type)
case transfer.TargetFilter:
for _, m := range p.targetMimes {
if m == f.Type {
return
}
}
p.targetMimes = append(p.targetMimes, f.Type)
case pointer.Filter:
p.kinds = p.kinds | f.Kinds
p.scrollRange = p.scrollRange.Union(f.ScrollBounds)
} }
} if op.Kinds&pointer.Scroll != 0 {
area.semantic.content.gestures |= ScrollGesture
func (p *pointerFilter) Matches(e event.Event) bool {
switch e := e.(type) {
case pointer.Event:
return e.Kind&p.kinds == e.Kind
case transfer.CancelEvent, transfer.InitiateEvent:
return len(p.sourceMimes) > 0 || len(p.targetMimes) > 0
case transfer.RequestEvent:
for _, t := range p.sourceMimes {
if t == e.Type {
return true
}
}
case transfer.DataEvent:
for _, t := range p.targetMimes {
if t == e.Type {
return true
}
}
} }
return false area.semantic.valid = area.semantic.content.gestures != 0
} h := c.newHandler(op.Tag, events)
h.wantsGrab = h.wantsGrab || op.Grab
func (p *pointerFilter) Merge(p2 pointerFilter) { h.types = h.types | op.Kinds
p.kinds = p.kinds | p2.kinds h.scrollRange = op.ScrollBounds
p.scrollRange = p.scrollRange.Union(p2.scrollRange)
p.sourceMimes = append(p.sourceMimes, p2.sourceMimes...)
p.targetMimes = append(p.targetMimes, p2.targetMimes...)
}
// clampScroll splits a scroll distance in the remaining scroll and the
// scroll accepted by the filter.
func (p *pointerFilter) clampScroll(scroll f32.Point) (left, scrolled f32.Point) {
left.X, scrolled.X = clampSplit(scroll.X, p.scrollRange.Min.X, p.scrollRange.Max.X)
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollRange.Min.Y, p.scrollRange.Max.Y)
return
}
func clampSplit(v float32, min, max int) (float32, float32) {
if m := float32(max); v > m {
return v - m, m
}
if m := float32(min); v < m {
return v - m, m
}
return 0, v
}
func (s *pointerHandler) ResetEvent() (event.Event, bool) {
if s.setup {
return nil, false
}
s.setup = true
return pointer.Event{Kind: pointer.Cancel}, true
} }
func (c *pointerCollector) semanticLabel(lbl string) { func (c *pointerCollector) semanticLabel(lbl string) {
@@ -397,28 +322,23 @@ func (c *pointerCollector) cursor(cursor pointer.Cursor) {
area.cursor = cursor area.cursor = cursor
} }
func (q *pointerQueue) offerData(handlers map[event.Tag]*handler, state pointerState, req transfer.OfferCmd) (pointerState, []taggedEvent) { func (c *pointerCollector) sourceOp(op transfer.SourceOp, events *handlerEvents) {
var evts []taggedEvent h := c.newHandler(op.Tag, events)
for i, p := range state.pointers { h.sourceMimes = append(h.sourceMimes, op.Type)
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) 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.q.reset()
c.resetState() c.resetState()
c.ensureRoot() c.ensureRoot()
@@ -573,6 +493,20 @@ func (q *pointerQueue) hitTest(pos f32.Point, onNode func(*hitNode) bool) pointe
return cursor 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 { func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point {
if areaIdx == -1 { if areaIdx == -1 {
return p return p
@@ -597,6 +531,17 @@ func (q *pointerQueue) hit(areaIdx int, p f32.Point) (bool, pointer.Cursor) {
} }
func (q *pointerQueue) reset() { func (q *pointerQueue) reset() {
if q.handlers == nil {
q.handlers = make(map[event.Tag]*pointerHandler)
}
for _, h := range q.handlers {
// Reset handler.
h.active = false
h.wantsGrab = false
h.types = 0
h.sourceMimes = h.sourceMimes[:0]
h.targetMimes = h.targetMimes[:0]
}
q.hitTree = q.hitTree[:0] q.hitTree = q.hitTree[:0]
q.areas = q.areas[:0] q.areas = q.areas[:0]
q.semantic.idsAssigned = false q.semantic.idsAssigned = false
@@ -614,71 +559,80 @@ func (q *pointerQueue) reset() {
delete(q.semantic.contentIDs, k) delete(q.semantic.contentIDs, k)
} }
} }
for _, rc := range q.transfers {
if rc != nil {
rc.Close()
}
}
q.transfers = nil
} }
func (q *pointerQueue) Frame(handlers map[event.Tag]*handler, state pointerState) (pointerState, []taggedEvent) { func (q *pointerQueue) Frame(events *handlerEvents) {
for _, h := range handlers { for k, h := range q.handlers {
if h.pointer.areaPlusOne != 0 { if !h.active {
area := &q.areas[h.pointer.areaPlusOne-1] q.dropHandler(nil, k)
if h.filter.pointer.kinds&(pointer.Press|pointer.Release) != 0 { delete(q.handlers, k)
area.semantic.content.gestures |= ClickGesture }
if h.wantsGrab {
for _, p := range q.pointers {
if !p.pressed {
continue
}
for i, k2 := range p.handlers {
if k2 == k {
// Drop other handlers that lost their grab.
dropped := q.scratch[:0]
dropped = append(dropped, p.handlers[:i]...)
dropped = append(dropped, p.handlers[i+1:]...)
for _, tag := range dropped {
q.dropHandler(events, tag)
}
break
}
}
} }
if h.filter.pointer.kinds&pointer.Scroll != 0 {
area.semantic.content.gestures |= ScrollGesture
}
area.semantic.valid = area.semantic.content.gestures != 0
} }
} }
var evts []taggedEvent for i := range q.pointers {
for i, p := range state.pointers { p := &q.pointers[i]
changed := false q.deliverEnterLeaveEvents(p, events, p.last)
p, evts, state.cursor, changed = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, p.last) q.deliverTransferDataEvent(p, events)
if changed {
state.pointers = append([]pointerInfo{}, state.pointers...)
state.pointers[i] = p
}
} }
return state, evts
} }
func dropHandler(state pointerState, tag event.Tag) pointerState { func (q *pointerQueue) dropHandler(events *handlerEvents, tag event.Tag) {
pointers := state.pointers if events != nil {
state.pointers = nil events.Add(tag, pointer.Event{Kind: pointer.Cancel})
for _, p := range pointers { }
handlers := p.handlers for i := range q.pointers {
p.handlers = nil p := &q.pointers[i]
for _, h := range handlers { for i := len(p.handlers) - 1; i >= 0; i-- {
if h != tag { if p.handlers[i] == tag {
p.handlers = append(p.handlers, h) p.handlers = append(p.handlers[:i], p.handlers[i+1:]...)
} }
} }
entered := p.entered for i := len(p.entered) - 1; i >= 0; i-- {
p.entered = nil if p.entered[i] == tag {
for _, h := range entered { p.entered = append(p.entered[:i], p.entered[i+1:]...)
if h != tag { }
p.entered = append(p.entered, h) }
}
}
state.pointers = append(state.pointers, p)
} }
return state
} }
// pointerOf returns the pointerInfo index corresponding to the pointer in e. // pointerOf returns the pointerInfo index corresponding to the pointer in e.
func (s pointerState) pointerOf(e pointer.Event) (pointerState, int) { func (q *pointerQueue) pointerOf(e pointer.Event) int {
for i, p := range s.pointers { for i, p := range q.pointers {
if p.id == e.PointerID { if p.id == e.PointerID {
return s, i return i
} }
} }
n := len(s.pointers) q.pointers = append(q.pointers, pointerInfo{id: e.PointerID})
s.pointers = append(s.pointers[:n:n], pointerInfo{id: e.PointerID}) return len(q.pointers) - 1
return s, len(s.pointers) - 1
} }
// Deliver is like Push, but delivers an event to a particular area. // Deliver is like Push, but delivers an event to a particular area.
func (q *pointerQueue) Deliver(handlers map[event.Tag]*handler, areaIdx int, e pointer.Event) []taggedEvent { func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEvents) {
scroll := e.Scroll var sx, sy = e.Scroll.X, e.Scroll.Y
idx := len(q.hitTree) - 1 idx := len(q.hitTree) - 1
// Locate first potential receiver. // Locate first potential receiver.
for idx != -1 { for idx != -1 {
@@ -688,28 +642,31 @@ func (q *pointerQueue) Deliver(handlers map[event.Tag]*handler, areaIdx int, e p
} }
idx-- idx--
} }
var evts []taggedEvent
for idx != -1 { for idx != -1 {
n := &q.hitTree[idx] n := &q.hitTree[idx]
idx = n.next idx = n.next
h, ok := handlers[n.tag] if n.tag == nil {
if !ok || !h.filter.pointer.Matches(e) { continue
}
h := q.handlers[n.tag]
if e.Kind&h.types == 0 {
continue continue
} }
e := e e := e
if e.Kind == pointer.Scroll { if e.Kind == pointer.Scroll {
if scroll == (f32.Point{}) { if sx == 0 && sy == 0 {
break 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) e.Position = q.invTransform(h.area, e.Position)
evts = append(evts, taggedEvent{tag: n.tag, event: e}) events.Add(n.tag, e)
if e.Kind != pointer.Scroll { if e.Kind != pointer.Scroll {
break break
} }
} }
return evts
} }
// SemanticArea returns the sematic content for area, and its parent area. // SemanticArea returns the sematic content for area, and its parent area.
@@ -725,129 +682,106 @@ func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) {
return semanticContent{}, -1 return semanticContent{}, -1
} }
func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState, e pointer.Event) (pointerState, []taggedEvent) { func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
var evts []taggedEvent
if e.Kind == pointer.Cancel { if e.Kind == pointer.Cancel {
for k := range handlers { q.pointers = q.pointers[:0]
evts = append(evts, taggedEvent{ for k := range q.handlers {
event: pointer.Event{Kind: pointer.Cancel}, q.dropHandler(events, k)
tag: k,
})
} }
state.pointers = nil return
return state, evts
} }
state, pidx := state.pointerOf(e) pidx := q.pointerOf(e)
p := state.pointers[pidx] p := &q.pointers[pidx]
p.last = e
switch e.Kind { switch e.Kind {
case pointer.Press: case pointer.Press:
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) q.deliverEnterLeaveEvents(p, events, e)
p.pressed = true p.pressed = true
evts = q.deliverEvent(handlers, p, evts, e) q.deliverEvent(p, events, e)
case pointer.Move: case pointer.Move:
if p.pressed { if p.pressed {
e.Kind = pointer.Drag e.Kind = pointer.Drag
} }
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) q.deliverEnterLeaveEvents(p, events, e)
evts = q.deliverEvent(handlers, p, evts, e) q.deliverEvent(p, events, e)
if p.pressed { if p.pressed {
p, evts = q.deliverDragEvent(handlers, p, evts) q.deliverDragEvent(p, events)
} }
case pointer.Release: case pointer.Release:
evts = q.deliverEvent(handlers, p, evts, e) q.deliverEvent(p, events, e)
p.pressed = false p.pressed = false
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) q.deliverEnterLeaveEvents(p, events, e)
p, evts = q.deliverDropEvent(handlers, p, evts) q.deliverDropEvent(p, events)
case pointer.Scroll: case pointer.Scroll:
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) q.deliverEnterLeaveEvents(p, events, e)
evts = q.deliverEvent(handlers, p, evts, e) q.deliverEvent(p, events, e)
default: default:
panic("unsupported pointer event type") panic("unsupported pointer event type")
} }
p.last = e
if !p.pressed && len(p.entered) == 0 { if !p.pressed && len(p.entered) == 0 {
// No longer need to track pointer. // No longer need to track pointer.
state.pointers = append(state.pointers[:pidx:pidx], state.pointers[pidx+1:]...) q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...)
} else {
state.pointers = append([]pointerInfo{}, state.pointers...)
state.pointers[pidx] = p
} }
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 foremost := true
if p.pressed && len(p.handlers) == 1 { if p.pressed && len(p.handlers) == 1 {
e.Priority = pointer.Grabbed e.Priority = pointer.Grabbed
foremost = false foremost = false
} }
scroll := e.Scroll var sx, sy = e.Scroll.X, e.Scroll.Y
for _, k := range p.handlers { for _, k := range p.handlers {
h, ok := handlers[k] h := q.handlers[k]
if !ok {
continue
}
f := h.filter.pointer
if !f.Matches(e) {
continue
}
if e.Kind == pointer.Scroll { if e.Kind == pointer.Scroll {
if scroll == (f32.Point{}) { if sx == 0 && sy == 0 {
return evts 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 e := e
if foremost { if foremost {
foremost = false foremost = false
e.Priority = pointer.Foremost e.Priority = pointer.Foremost
} }
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position) e.Position = q.invTransform(h.area, e.Position)
evts = append(evts, taggedEvent{event: e, tag: k}) 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) { func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) {
changed := false
var hits []event.Tag var hits []event.Tag
if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press { if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press {
// Consider non-mouse pointers leaving when they're released. // Consider non-mouse pointers leaving when they're released.
} else { } else {
var transSrc *pointerFilter hits, q.cursor = q.opHit(e.Position)
if p.dataSource != nil { if p.pressed {
transSrc = &handlers[p.dataSource].filter.pointer // Filter out non-participating handlers,
} // except potential transfer targets when a transfer has been initiated.
cursor = q.hitTest(e.Position, func(n *hitNode) bool { var hitsHaveTarget bool
h, ok := handlers[n.tag] if p.dataSource != nil {
if !ok { transferSource := q.handlers[p.dataSource]
return true for _, hit := range hits {
} if _, ok := firstMimeMatch(transferSource, q.handlers[hit]); ok {
add := true hitsHaveTarget = true
if p.pressed { break
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
} }
} }
} }
if add { for i := len(hits) - 1; i >= 0; i-- {
hits = addHandler(hits, n.tag) if _, found := searchTag(p.handlers, hits[i]); !found && !hitsHaveTarget {
hits = append(hits[:i], hits[i+1:]...)
}
} }
return true } else {
}) p.handlers = append(p.handlers[:0], hits...)
if !p.pressed {
changed = true
p.handlers = hits
} }
} }
// Deliver Leave events. // Deliver Leave events.
@@ -855,94 +789,111 @@ func (q *pointerQueue) deliverEnterLeaveEvents(handlers map[event.Tag]*handler,
if _, found := searchTag(hits, k); found { if _, found := searchTag(hits, k); found {
continue continue
} }
h, ok := handlers[k] h := q.handlers[k]
if !ok {
continue
}
changed = true
e := e
e.Kind = pointer.Leave e.Kind = pointer.Leave
if h.filter.pointer.Matches(e) { if e.Kind&h.types != 0 {
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position) e := e
evts = append(evts, taggedEvent{tag: k, event: e}) e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
} }
} }
// Deliver Enter events. // Deliver Enter events.
for _, k := range hits { for _, k := range hits {
h := q.handlers[k]
if _, found := searchTag(p.entered, k); found { if _, found := searchTag(p.entered, k); found {
continue continue
} }
h, ok := handlers[k]
if !ok {
continue
}
changed = true
e := e
e.Kind = pointer.Enter e.Kind = pointer.Enter
if h.filter.pointer.Matches(e) { if e.Kind&h.types != 0 {
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position) e := e
evts = append(evts, taggedEvent{tag: k, event: e}) e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
} }
} }
p.entered = hits p.entered = append(p.entered[:0], hits...)
return p, evts, cursor, changed
} }
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 { if p.dataSource != nil {
return p, evts return
} }
// Identify the data source. // Identify the data source.
for _, k := range p.entered { for _, k := range p.entered {
src := &handlers[k].filter.pointer src := q.handlers[k]
if len(src.sourceMimes) == 0 { if len(src.sourceMimes) == 0 {
continue continue
} }
// One data source handler per pointer. // One data source handler per pointer.
p.dataSource = k p.dataSource = k
// Notify all potential targets. // Notify all potential targets.
for k, tgt := range handlers { for k, tgt := range q.handlers {
if _, ok := firstMimeMatch(src, &tgt.filter.pointer); ok { if _, ok := firstMimeMatch(src, tgt); ok {
evts = append(evts, taggedEvent{tag: k, event: transfer.InitiateEvent{}}) events.Add(k, transfer.InitiateEvent{})
} }
} }
break 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 { if p.dataSource == nil {
return p, evts return
} }
// Request data from the source. // Request data from the source.
src := &handlers[p.dataSource].filter.pointer src := q.handlers[p.dataSource]
for _, k := range p.entered { for _, k := range p.entered {
h := handlers[k] h := q.handlers[k]
if m, ok := firstMimeMatch(src, &h.filter.pointer); ok { if m, ok := firstMimeMatch(src, h); ok {
p.dataTarget = k p.dataTarget = k
evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.RequestEvent{Type: m}}) events.Add(p.dataSource, transfer.RequestEvent{Type: m})
return p, evts return
} }
} }
// No valid target found, abort. // 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) { func (q *pointerQueue) deliverTransferDataEvent(p *pointerInfo, events *handlerEvents) {
evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.CancelEvent{}}) 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. // Cancel all potential targets.
src := &handlers[p.dataSource].filter.pointer src := q.handlers[p.dataSource]
for k, h := range handlers { for k, h := range q.handlers {
if _, ok := firstMimeMatch(src, &h.filter.pointer); ok { if _, ok := firstMimeMatch(src, h); ok {
evts = append(evts, taggedEvent{tag: k, event: transfer.CancelEvent{}}) events.Add(k, transfer.CancelEvent{})
} }
} }
src.offeredMime = ""
src.data = nil
p.dataSource = nil p.dataSource = nil
p.dataTarget = nil p.dataTarget = nil
return p, evts
} }
// ClipFor clips r to the parents of area. // ClipFor clips r to the parents of area.
@@ -977,7 +928,7 @@ func addHandler(tags []event.Tag, tag event.Tag) []event.Tag {
} }
// firstMimeMatch returns the first type match between src and tgt. // firstMimeMatch returns the first type match between src and tgt.
func firstMimeMatch(src, tgt *pointerFilter) (first string, matched bool) { func firstMimeMatch(src, tgt *pointerHandler) (first string, matched bool) {
for _, m1 := range tgt.targetMimes { for _, m1 := range tgt.targetMimes {
for _, m2 := range src.sourceMimes { for _, m2 := range src.sourceMimes {
if m1 == m2 { if m1 == m2 {
@@ -1014,3 +965,13 @@ func (a *areaNode) bounds() image.Rectangle {
Max: a.trans.Transform(f32internal.FPt(a.area.rect.Max)), Max: a.trans.Transform(f32internal.FPt(a.area.rect.Max)),
}.Round() }.Round()
} }
func setScrollEvent(scroll float32, min, max int) (left, scrolled float32) {
if v := float32(max); scroll > v {
return scroll - v, v
}
if v := float32(min); scroll < v {
return scroll - v, v
}
return 0, scroll
}
File diff suppressed because it is too large Load Diff
+634
View File
@@ -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 // SPDX-License-Identifier: Unlicense OR MIT
package input package router
import ( import (
"fmt" "fmt"
@@ -9,7 +9,6 @@ import (
"testing" "testing"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/op" "gioui.org/op"
@@ -75,19 +74,13 @@ func TestSemanticTree(t *testing.T) {
func TestSemanticDescription(t *testing.T) { func TestSemanticDescription(t *testing.T) {
var ops op.Ops var ops op.Ops
pointer.InputOp{Tag: new(int), Kinds: pointer.Press | pointer.Release}.Add(&ops)
h := new(int)
event.Op(&ops, h)
semantic.DescriptionOp("description").Add(&ops) semantic.DescriptionOp("description").Add(&ops)
semantic.LabelOp("label").Add(&ops) semantic.LabelOp("label").Add(&ops)
semantic.Button.Add(&ops) semantic.Button.Add(&ops)
semantic.EnabledOp(false).Add(&ops) semantic.EnabledOp(false).Add(&ops)
semantic.SelectedOp(true).Add(&ops) semantic.SelectedOp(true).Add(&ops)
var r Router var r Router
events(&r, -1, pointer.Filter{
Target: h,
Kinds: pointer.Press | pointer.Release,
})
r.Frame(&ops) r.Frame(&ops)
tree := r.AppendSemantics(nil) tree := r.AppendSemantics(nil)
got := tree[0].Desc got := tree[0].Desc
+89
View File
@@ -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
View File
@@ -2,11 +2,11 @@
// //
// The transfer protocol is as follows: // The transfer protocol is as follows:
// //
// - Data sources use [SourceFilter] to receive an [InitiateEvent] when a drag // - Data sources are registered with SourceOps, data targets with TargetOps.
// is initiated, and an [RequestEvent] for each initiation of a data transfer. // - A data source receives a RequestEvent when a transfer is initiated.
// Sources respond to requests with [OfferCmd]. // It must respond with an OfferOp.
// - Data targets use [TargetFilter] to receive an [DataEvent] for receiving data. // - The target receives a DataEvent when transferring to it. It must close
// The target must close the data event after use. // the event data after use.
// //
// When a user initiates a pointer-guided drag and drop transfer, the // When a user initiates a pointer-guided drag and drop transfer, the
// source as well as all potential targets receive an InitiateEvent. // source as well as all potential targets receive an InitiateEvent.
@@ -20,11 +20,29 @@ package transfer
import ( import (
"io" "io"
"gioui.org/internal/ops"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/op"
) )
// OfferCmd is used by data sources as a response to a RequestEvent. // SourceOp registers a tag as a data source for a MIME type.
type OfferCmd struct { // 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 Tag event.Tag
// Type is the MIME type of Data. // Type is the MIME type of Data.
// It must be the Type from the corresponding RequestEvent. // It must be the Type from the corresponding RequestEvent.
@@ -32,33 +50,32 @@ type OfferCmd struct {
// Data contains the offered data. It is closed when the // Data contains the offered data. It is closed when the
// transfer is complete or cancelled. // transfer is complete or cancelled.
// Data must be kept valid until closed, and it may be used from // Data must be kept valid until closed, and it may be used from
// a goroutine separate from the one processing the frame. // a goroutine separate from the one processing the frame..
Data io.ReadCloser Data io.ReadCloser
} }
func (OfferCmd) ImplementsCommand() {} func (op SourceOp) Add(o *op.Ops) {
data := ops.Write2(&o.Internal, ops.TypeSourceLen, op.Tag, op.Type)
// SourceFilter filters for any [RequestEvent] that match a MIME type data[0] = byte(ops.TypeSource)
// 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
} }
// TargetFilter filters for any [DataEvent] whose type matches a MIME type func (op TargetOp) Add(o *op.Ops) {
// as well as [CancelEvent]. Use multiple filters to accept multiple types. data := ops.Write2(&o.Internal, ops.TypeTargetLen, op.Tag, op.Type)
type TargetFilter struct { data[0] = byte(ops.TypeTarget)
// Target is a tag included in a previous event.Op. }
Target event.Tag
// Type is the MIME type accepted by this target. // Add the offer to the list of operations.
Type string // 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 // RequestEvent requests data from a data source. The source must
// respond with an OfferCmd. // respond with an OfferOp.
type RequestEvent struct { type RequestEvent struct {
// Type is the first matched type between the source and the target. // Type is the first matched type between the source and the target.
Type string Type string
@@ -90,6 +107,3 @@ type DataEvent struct {
} }
func (DataEvent) ImplementsEvent() {} func (DataEvent) ImplementsEvent() {}
func (SourceFilter) ImplementsFilter() {}
func (TargetFilter) ImplementsFilter() {}
+57 -5
View File
@@ -3,9 +3,10 @@
package layout package layout
import ( import (
"image"
"time" "time"
"gioui.org/io/input" "gioui.org/io/event"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/op" "gioui.org/op"
"gioui.org/unit" "gioui.org/unit"
@@ -20,6 +21,9 @@ type Context struct {
Constraints Constraints Constraints Constraints
Metric unit.Metric Metric unit.Metric
// By convention, a nil Queue is a signal to widgets to draw themselves
// in a disabled state.
Queue event.Queue
// Now is the animation time. // Now is the animation time.
Now time.Time Now time.Time
@@ -28,10 +32,46 @@ type Context struct {
// Interested users must look up and populate these values manually. // Interested users must look up and populate these values manually.
Locale system.Locale Locale system.Locale
input.Source
*op.Ops *op.Ops
} }
// NewContext is a shorthand for
//
// Context{
// Ops: ops,
// Now: e.Now,
// Queue: e.Queue,
// Config: e.Config,
// Constraints: Exact(e.Size),
// }
//
// NewContext calls ops.Reset and adjusts ops for e.Insets.
func NewContext(ops *op.Ops, e system.FrameEvent) Context {
ops.Reset()
size := e.Size
if e.Insets != (system.Insets{}) {
left := e.Metric.Dp(e.Insets.Left)
top := e.Metric.Dp(e.Insets.Top)
op.Offset(image.Point{
X: left,
Y: top,
}).Add(ops)
size.X -= left + e.Metric.Dp(e.Insets.Right)
size.Y -= top + e.Metric.Dp(e.Insets.Bottom)
}
return Context{
Ops: ops,
Now: e.Now,
Queue: e.Queue,
Metric: e.Metric,
Constraints: Exact(size),
}
}
// Dp converts v to pixels. // Dp converts v to pixels.
func (c Context) Dp(v unit.Dp) int { func (c Context) Dp(v unit.Dp) int {
return c.Metric.Dp(v) return c.Metric.Dp(v)
@@ -42,9 +82,21 @@ func (c Context) Sp(v unit.Sp) int {
return c.Metric.Sp(v) return c.Metric.Sp(v)
} }
// Disabled returns a copy of this context with a disabled Source, // Events returns the events available for the key. If no
// blocking widgets from changing its state and receiving events. // 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 { func (c Context) Disabled() Context {
c.Source = input.Source{} c.Queue = nil
return c return c
} }
+20 -20
View File
@@ -144,25 +144,7 @@ func (l *List) Dragging() bool {
} }
func (l *List) update(gtx Context) { func (l *List) update(gtx Context) {
min, max := int(-inf), int(inf) d := l.scroll.Update(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis))
if l.Position.First == 0 {
// Use the size of the invisible part as scroll boundary.
min = -l.Position.Offset
if min > 0 {
min = 0
}
}
if l.Position.First+l.Position.Count == l.len {
max = -l.Position.OffsetLast
if max < 0 {
max = 0
}
}
scrollRange := image.Rectangle{
Min: l.Axis.Convert(image.Pt(min, 0)),
Max: l.Axis.Convert(image.Pt(max, 0)),
}
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), scrollRange)
l.scrollDelta = d l.scrollDelta = d
l.Position.Offset += d l.Position.Offset += d
} }
@@ -350,7 +332,25 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
call := macro.Stop() call := macro.Stop()
defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop() defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop()
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) call.Add(ops)
return Dimensions{Size: dims} return Dimensions{Size: dims}
+3 -3
View File
@@ -8,8 +8,8 @@ import (
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/op" "gioui.org/op"
) )
@@ -64,13 +64,13 @@ func TestListScrollToEnd(t *testing.T) {
func TestListPosition(t *testing.T) { func TestListPosition(t *testing.T) {
_s := func(e ...event.Event) []event.Event { return e } _s := func(e ...event.Event) []event.Event { return e }
r := new(input.Router) r := new(router.Router)
gtx := Context{ gtx := Context{
Ops: new(op.Ops), Ops: new(op.Ops),
Constraints: Constraints{ Constraints: Constraints{
Max: image.Pt(20, 10), Max: image.Pt(20, 10),
}, },
Source: r.Source(), Queue: r,
} }
el := func(gtx Context, idx int) Dimensions { el := func(gtx Context, idx int) Dimensions {
return Dimensions{Size: image.Pt(10, 10)} return Dimensions{Size: image.Pt(10, 10)}
+16 -4
View File
@@ -53,6 +53,7 @@ The MacroOp records a list of operations to be executed later:
ops := new(op.Ops) ops := new(op.Ops)
macro := op.Record(ops) macro := op.Record(ops)
// Record operations by adding them. // Record operations by adding them.
op.InvalidateOp{}.Add(ops)
... ...
// End recording. // End recording.
call := macro.Stop() call := macro.Stop()
@@ -95,9 +96,9 @@ type CallOp struct {
end ops.PC 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. // the zero value to request an immediate redraw.
type InvalidateCmd struct { type InvalidateOp struct {
At time.Time At time.Time
} }
@@ -180,6 +181,19 @@ func (c CallOp) Add(o *Ops) {
ops.AddCall(&o.Internal, c.ops, c.start, c.end) ops.AddCall(&o.Internal, c.ops, c.start, c.end)
} }
func (r InvalidateOp) Add(o *Ops) {
data := ops.Write(&o.Internal, ops.TypeRedrawLen)
data[0] = byte(ops.TypeInvalidate)
bo := binary.LittleEndian
// UnixNano cannot represent the zero time.
if t := r.At; !t.IsZero() {
nanos := t.UnixNano()
if nanos > 0 {
bo.PutUint64(data[1:], uint64(nanos))
}
}
}
// Offset converts an offset to a TransformOp. // Offset converts an offset to a TransformOp.
func Offset(off image.Point) TransformOp { func Offset(off image.Point) TransformOp {
offf := f32.Pt(float32(off.X), float32(off.Y)) offf := f32.Pt(float32(off.X), float32(off.Y))
@@ -226,5 +240,3 @@ func (t TransformStack) Pop() {
data := ops.Write(t.ops, ops.TypePopTransformLen) data := ops.Write(t.ops, ops.TypePopTransformLen)
data[0] = byte(ops.TypePopTransform) data[0] = byte(ops.TypePopTransform)
} }
func (InvalidateCmd) ImplementsCommand() {}
+4
View File
@@ -163,6 +163,10 @@ type layoutKey struct {
lineHeightScale float32 lineHeightScale float32
} }
type pathKey struct {
gidHash uint64
}
const maxSize = 1000 const maxSize = 1000
func gidsEqual(a []glyphInfo, glyphs []Glyph) bool { func gidsEqual(a []glyphInfo, glyphs []Glyph) bool {
+8 -3
View File
@@ -16,7 +16,7 @@ type Bool struct {
// Update the widget state and report whether Value was changed. // Update the widget state and report whether Value was changed.
func (b *Bool) Update(gtx layout.Context) bool { func (b *Bool) Update(gtx layout.Context) bool {
changed := false changed := false
for b.clk.clicked(b, gtx) { for b.clk.Clicked(gtx) {
b.Value = !b.Value b.Value = !b.Value
changed = true changed = true
} }
@@ -33,15 +33,20 @@ func (b *Bool) Pressed() bool {
return b.clk.Pressed() return b.clk.Pressed()
} }
// Focused reports whether b has focus.
func (b *Bool) Focused() bool {
return b.clk.Focused()
}
func (b *Bool) History() []Press { func (b *Bool) History() []Press {
return b.clk.History() return b.clk.History()
} }
func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
b.Update(gtx) 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.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 w(gtx)
}) })
return dims return dims
+3
View File
@@ -11,6 +11,9 @@ import (
// editBuffer implements a gap buffer for text editing. // editBuffer implements a gap buffer for text editing.
type editBuffer struct { type editBuffer struct {
// pos is the byte position for Read and ReadRune.
pos int
// The gap start and end in bytes. // The gap start and end in bytes.
gapstart, gapend int gapstart, gapend int
text []byte text []byte
+64 -53
View File
@@ -7,7 +7,6 @@ import (
"time" "time"
"gioui.org/gesture" "gioui.org/gesture"
"gioui.org/io/event"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic" "gioui.org/io/semantic"
@@ -18,11 +17,16 @@ import (
// Clickable represents a clickable area. // Clickable represents a clickable area.
type Clickable struct { type Clickable struct {
click gesture.Click click gesture.Click
// clicks is for saved clicks to support Clicked.
clicks []Click
history []Press history []Press
keyTag struct{}
requestFocus bool
requestClicks int requestClicks int
pressedKey key.Name focused bool
pressedKey string
} }
// Click represents a click. // Click represents a click.
@@ -49,14 +53,19 @@ func (b *Clickable) Click() {
b.requestClicks++ 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 { func (b *Clickable) Clicked(gtx layout.Context) bool {
return b.clicked(b, gtx) if len(b.clicks) > 0 {
} b.clicks = b.clicks[1:]
return true
func (b *Clickable) clicked(t event.Tag, gtx layout.Context) bool { }
_, clicked := b.update(t, gtx) b.clicks = b.Update(gtx)
return clicked if len(b.clicks) > 0 {
b.clicks = b.clicks[1:]
return true
}
return false
} }
// Hovered reports whether a pointer is over the element. // Hovered reports whether a pointer is over the element.
@@ -69,6 +78,16 @@ func (b *Clickable) Pressed() bool {
return b.click.Pressed() 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 the past pointer presses useful for drawing markers.
// History is retained for a short duration (about a second). // History is retained for a short duration (about a second).
func (b *Clickable) History() []Press { func (b *Clickable) History() []Press {
@@ -77,34 +96,36 @@ func (b *Clickable) History() []Press {
// Layout and update the button state. // Layout and update the button state.
func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
return b.layout(b, gtx, w) b.Update(gtx)
}
func (b *Clickable) layout(t event.Tag, gtx layout.Context, w layout.Widget) layout.Dimensions {
for {
_, ok := b.update(t, gtx)
if !ok {
break
}
}
m := op.Record(gtx.Ops) m := op.Record(gtx.Ops)
dims := w(gtx) dims := w(gtx)
c := m.Stop() c := m.Stop()
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop() defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops) enabled := gtx.Queue != nil
semantic.EnabledOp(enabled).Add(gtx.Ops)
b.click.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) c.Add(gtx.Ops)
return dims return dims
} }
// Update the button state by processing events, and return the next // Update the button state by processing events, and return the resulting
// click, if any. // clicks, if any.
func (b *Clickable) Update(gtx layout.Context) (Click, bool) { func (b *Clickable) Update(gtx layout.Context) []Click {
return b.update(b, gtx) b.clicks = nil
} if gtx.Queue == nil {
b.focused = false
func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) { }
if b.requestFocus {
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
b.requestFocus = false
}
for len(b.history) > 0 { for len(b.history) > 0 {
c := b.history[0] c := b.history[0]
if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second { if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second {
@@ -113,26 +134,23 @@ func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
n := copy(b.history, b.history[1:]) n := copy(b.history, b.history[1:])
b.history = b.history[:n] b.history = b.history[:n]
} }
var clicks []Click
if c := b.requestClicks; c > 0 { if c := b.requestClicks; c > 0 {
b.requestClicks = 0 b.requestClicks = 0
return Click{ clicks = append(clicks, Click{
NumClicks: c, NumClicks: c,
}, true })
} }
for { for _, e := range b.click.Update(gtx) {
e, ok := b.click.Update(gtx.Source)
if !ok {
break
}
switch e.Kind { switch e.Kind {
case gesture.KindClick: case gesture.KindClick:
if l := len(b.history); l > 0 { if l := len(b.history); l > 0 {
b.history[l-1].End = gtx.Now b.history[l-1].End = gtx.Now
} }
return Click{ clicks = append(clicks, Click{
Modifiers: e.Modifiers, Modifiers: e.Modifiers,
NumClicks: e.NumClicks, NumClicks: e.NumClicks,
}, true })
case gesture.KindCancel: case gesture.KindCancel:
for i := range b.history { for i := range b.history {
b.history[i].Cancelled = true b.history[i].Cancelled = true
@@ -142,7 +160,7 @@ func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
} }
case gesture.KindPress: case gesture.KindPress:
if e.Source == pointer.Mouse { 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{ b.history = append(b.history, Press{
Position: e.Position, Position: e.Position,
@@ -150,22 +168,15 @@ func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
}) })
} }
} }
for { for _, e := range gtx.Events(&b.keyTag) {
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
}
switch e := e.(type) { switch e := e.(type) {
case key.FocusEvent: case key.FocusEvent:
if e.Focus { b.focused = e.Focus
if !b.focused {
b.pressedKey = "" b.pressedKey = ""
} }
case key.Event: case key.Event:
if !gtx.Focused(t) { if !b.focused {
break break
} }
if e.Name != key.NameReturn && e.Name != key.NameSpace { 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 // only register a key as a click if the key was pressed and released while this button was focused
b.pressedKey = "" b.pressedKey = ""
return Click{ clicks = append(clicks, Click{
Modifiers: e.Modifiers, Modifiers: e.Modifiers,
NumClicks: 1, NumClicks: 1,
}, true })
} }
} }
} }
return Click{}, false return clicks
} }
+25 -15
View File
@@ -6,8 +6,9 @@ import (
"image" "image"
"testing" "testing"
"gioui.org/io/input"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/router"
"gioui.org/io/system"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/widget" "gioui.org/widget"
@@ -15,14 +16,12 @@ import (
func TestClickable(t *testing.T) { func TestClickable(t *testing.T) {
var ( var (
r input.Router ops op.Ops
b1 widget.Clickable r router.Router
b2 widget.Clickable b1 widget.Clickable
b2 widget.Clickable
) )
gtx := layout.Context{ gtx := layout.NewContext(&ops, system.FrameEvent{Queue: &r})
Ops: new(op.Ops),
Source: r.Source(),
}
layout := func() { layout := func() {
b1.Layout(gtx, func(gtx layout.Context) layout.Dimensions { b1.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Dimensions{Size: image.Pt(100, 100)} return layout.Dimensions{Size: image.Pt(100, 100)}
@@ -33,18 +32,23 @@ func TestClickable(t *testing.T) {
}) })
} }
frame := func() { frame := func() {
gtx.Reset() ops.Reset()
layout() layout()
r.Frame(gtx.Ops) r.Frame(gtx.Ops)
} }
gtx.Execute(key.FocusCmd{Tag: &b1}) // frame: request focus for button 1
b1.Focus()
frame() frame()
if !gtx.Focused(&b1) { // frame: gain focus for button 1
frame()
if !b1.Focused() {
t.Error("button 1 did not gain focus") t.Error("button 1 did not gain focus")
} }
if gtx.Focused(&b2) { if b2.Focused() {
t.Error("button 2 should not have focus") t.Error("button 2 should not have focus")
} }
// frame: press & release return
frame()
r.Queue( r.Queue(
key.Event{ key.Event{
Name: key.NameReturn, Name: key.NameReturn,
@@ -61,30 +65,36 @@ func TestClickable(t *testing.T) {
if b2.Clicked(gtx) { if b2.Clicked(gtx) {
t.Error("button 2 got clicked when it did not have focus") t.Error("button 2 got clicked when it did not have focus")
} }
// frame: press return down
r.Queue( r.Queue(
key.Event{ key.Event{
Name: key.NameReturn, Name: key.NameReturn,
State: key.Press, State: key.Press,
}, },
) )
frame()
if b1.Clicked(gtx) { if b1.Clicked(gtx) {
t.Error("button 1 got clicked, even if it only got return press") t.Error("button 1 got clicked, even if it only got return press")
} }
// frame: request focus for button 2
b2.Focus()
frame() frame()
gtx.Execute(key.FocusCmd{Tag: &b2}) // frame: gain focus for button 2
frame() frame()
if gtx.Focused(&b1) { if b1.Focused() {
t.Error("button 1 should not have focus") t.Error("button 1 should not have focus")
} }
if !gtx.Focused(&b2) { if !b2.Focused() {
t.Error("button 2 did not gain focus") t.Error("button 2 did not gain focus")
} }
// frame: release return
r.Queue( r.Queue(
key.Event{ key.Event{
Name: key.NameReturn, Name: key.NameReturn,
State: key.Release, State: key.Release,
}, },
) )
frame()
if b1.Clicked(gtx) { if b1.Clicked(gtx) {
t.Error("button 1 got clicked, even if it had lost focus") t.Error("button 1 got clicked, even if it had lost focus")
} }
+6 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"math/bits" "math/bits"
"gioui.org/gesture"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op/clip" "gioui.org/op/clip"
@@ -11,7 +12,11 @@ import (
// Decorations handles the states of window decorations. // Decorations handles the states of window decorations.
type Decorations struct { type Decorations struct {
clicks map[int]*Clickable clicks map[int]*Clickable
resize [8]struct {
gesture.Hover
gesture.Drag
}
maximized bool maximized bool
} }
+18 -19
View File
@@ -5,7 +5,6 @@ import (
"gioui.org/f32" "gioui.org/f32"
"gioui.org/gesture" "gioui.org/gesture"
"gioui.org/io/event"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/transfer" "gioui.org/io/transfer"
"gioui.org/layout" "gioui.org/layout"
@@ -18,20 +17,24 @@ type Draggable struct {
// Type contains the MIME type and matches transfer.SourceOp. // Type contains the MIME type and matches transfer.SourceOp.
Type string Type string
drag gesture.Drag handle struct{}
click f32.Point drag gesture.Drag
pos f32.Point click f32.Point
pos f32.Point
} }
func (d *Draggable) Layout(gtx layout.Context, w, drag layout.Widget) layout.Dimensions { func (d *Draggable) Layout(gtx layout.Context, w, drag layout.Widget) layout.Dimensions {
if !gtx.Enabled() { if gtx.Queue == nil {
return w(gtx) return w(gtx)
} }
dims := w(gtx) dims := w(gtx)
stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops) stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops)
d.drag.Add(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() stack.Pop()
if drag != nil && d.drag.Pressed() { if drag != nil && d.drag.Pressed() {
@@ -53,11 +56,7 @@ func (d *Draggable) Dragging() bool {
// requested to offer data, if any // requested to offer data, if any
func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) { func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
pos := d.pos pos := d.pos
for { for _, ev := range d.drag.Update(gtx.Metric, gtx.Queue, gesture.Both) {
ev, ok := d.drag.Update(gtx.Metric, gtx.Source, gesture.Both)
if !ok {
break
}
switch ev.Kind { switch ev.Kind {
case pointer.Press: case pointer.Press:
d.click = ev.Position d.click = ev.Position
@@ -68,12 +67,8 @@ func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
} }
d.pos = pos d.pos = pos
for { for _, ev := range gtx.Queue.Events(&d.handle) {
e, ok := gtx.Event(transfer.SourceFilter{Target: d, Type: d.Type}) if e, ok := ev.(transfer.RequestEvent); ok {
if !ok {
break
}
if e, ok := e.(transfer.RequestEvent); ok {
return e.Type, true 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. // Offer the data ready for a drop. Must be called after being Requested.
// The mime must be one in the requested list. // The mime must be one in the requested list.
func (d *Draggable) Offer(gtx layout.Context, mime string, data io.ReadCloser) { func (d *Draggable) Offer(ops *op.Ops, mime string, data io.ReadCloser) {
gtx.Execute(transfer.OfferCmd{Tag: d, Type: mime, Data: data}) transfer.OfferOp{
Tag: &d.handle,
Type: mime,
Data: data,
}.Add(ops)
} }
// Pos returns the drag position relative to its initial click position. // Pos returns the drag position relative to its initial click position.
+14 -22
View File
@@ -5,9 +5,8 @@ import (
"testing" "testing"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/io/transfer" "gioui.org/io/transfer"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
@@ -15,27 +14,27 @@ import (
) )
func TestDraggable(t *testing.T) { func TestDraggable(t *testing.T) {
var r input.Router var r router.Router
gtx := layout.Context{ gtx := layout.Context{
Constraints: layout.Exact(image.Pt(100, 100)), Constraints: layout.Exact(image.Pt(100, 100)),
Source: r.Source(), Queue: &r,
Ops: new(op.Ops), Ops: new(op.Ops),
} }
drag := &Draggable{ drag := &Draggable{
Type: "file", Type: "file",
} }
tgt := new(int)
defer pointer.PassOp{}.Push(gtx.Ops).Pop() defer pointer.PassOp{}.Push(gtx.Ops).Pop()
dims := drag.Layout(gtx, func(gtx layout.Context) layout.Dimensions { dims := drag.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Dimensions{Size: gtx.Constraints.Min} return layout.Dimensions{Size: gtx.Constraints.Min}
}, nil) }, nil)
stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops) 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() stack.Pop()
drag.Update(gtx)
r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type})
r.Frame(gtx.Ops) r.Frame(gtx.Ops)
r.Queue( r.Queue(
pointer.Event{ pointer.Event{
@@ -52,28 +51,21 @@ func TestDraggable(t *testing.T) {
}, },
) )
ofr := &offer{data: "hello"} ofr := &offer{data: "hello"}
drag.Update(gtx) drag.Offer(gtx.Ops, "file", ofr)
r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) r.Frame(gtx.Ops)
drag.Offer(gtx, "file", ofr)
e, ok := r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) evs := r.Events(drag)
if !ok { if len(evs) != 1 {
t.Fatalf("expected event") 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 { if got, want := ev.Type, "file"; got != want {
t.Errorf("expected %v; got %v", got, want) t.Errorf("expected %v; got %v", got, want)
} }
if ofr.closed { if ofr.closed {
t.Error("offer closed prematurely") 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) r.Frame(gtx.Ops)
if !ofr.closed { if !ofr.closed {
t.Error("offer was not closed") t.Error("offer was not closed")
+1 -1
View File
@@ -2,5 +2,5 @@
// Package widget implements state tracking and event handling of // Package widget implements state tracking and event handling of
// common user interface controls. To draw widgets, use a theme // 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 package widget
+235 -284
View File
@@ -21,7 +21,6 @@ import (
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
@@ -70,8 +69,11 @@ type Editor struct {
buffer *editBuffer buffer *editBuffer
// scratch is a byte buffer that is reused to efficiently read portions of text // scratch is a byte buffer that is reused to efficiently read portions of text
// from the textView. // from the textView.
scratch []byte scratch []byte
blinkStart time.Time eventKey int
blinkStart time.Time
focused bool
requestFocus bool
// ime tracks the state relevant to input methods. // ime tracks the state relevant to input methods.
ime struct { ime struct {
@@ -87,14 +89,16 @@ type Editor struct {
clicker gesture.Click 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 contains undo history.
history []modification history []modification
// nextHistoryIdx is the index within the history of the next modification. This // 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 // is only not len(history) immediately after undo operations occur. It is framed as the "next" value
// to make the zero value consistent. // to make the zero value consistent.
nextHistoryIdx int nextHistoryIdx int
pending []EditorEvent
} }
type offEntry struct { type offEntry struct {
@@ -187,37 +191,30 @@ const (
maxBlinkDuration = 10 * time.Second maxBlinkDuration = 10 * time.Second
) )
func (e *Editor) processEvents(gtx layout.Context) (ev EditorEvent, ok bool) { // Events returns available editor events.
if len(e.pending) > 0 { func (e *Editor) Events() []EditorEvent {
out := e.pending[0] events := e.events
e.pending = e.pending[:copy(e.pending, e.pending[1:])] e.events = nil
return out, true e.prevEvents = 0
} return events
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
} }
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() sbounds := e.text.ScrollBounds()
var smin, smax int var smin, smax int
var axis gesture.Axis var axis gesture.Axis
@@ -228,19 +225,7 @@ func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
axis = gesture.Vertical axis = gesture.Vertical
smin, smax = sbounds.Min.Y, sbounds.Max.Y smin, smax = sbounds.Min.Y, sbounds.Max.Y
} }
var scrollRange image.Rectangle sdist := e.scroller.Update(gtx.Metric, gtx, gtx.Now, axis)
textDims := e.text.FullDimensions()
visibleDims := e.text.Dimensions()
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))
}
sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollRange)
var soff int var soff int
if e.SingleLine { if e.SingleLine {
e.text.ScrollRel(sdist, 0) e.text.ScrollRel(sdist, 0)
@@ -249,173 +234,115 @@ func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
e.text.ScrollRel(0, sdist) e.text.ScrollRel(0, sdist)
soff = e.text.ScrollOff().Y soff = e.text.ScrollOff().Y
} }
for { for _, evt := range e.clickDragEvents(gtx) {
evt, ok := e.clicker.Update(gtx.Source) switch evt := evt.(type) {
if !ok { case gesture.ClickEvent:
break switch {
} case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse,
ev, ok := e.processPointerEvent(gtx, evt) evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse:
if ok { prevCaretPos, _ := e.text.Selection()
return ev, ok e.blinkStart = gtx.Now
} e.text.MoveCoord(image.Point{
} X: int(math.Round(float64(evt.Position.X))),
for { Y: int(math.Round(float64(evt.Position.Y))),
evt, ok := e.dragger.Update(gtx.Metric, gtx.Source, gesture.Both) })
if !ok { e.requestFocus = true
break if e.scroller.State() != gesture.StateFlinging {
} e.scrollCaret = true
ev, ok := e.processPointerEvent(gtx, evt) }
if ok {
return ev, ok 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) { if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) {
e.scroller.Stop() e.scroller.Stop()
} }
return nil, false
} }
func (e *Editor) processPointerEvent(gtx layout.Context, ev event.Event) (EditorEvent, bool) { func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event {
switch evt := ev.(type) { var combinedEvents []event.Event
case gesture.ClickEvent: for _, evt := range e.clicker.Update(gtx) {
switch { combinedEvents = append(combinedEvents, evt)
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.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
}
}
}
} }
return nil, false for _, evt := range e.dragger.Update(gtx.Metric, gtx, gesture.Both) {
} combinedEvents = append(combinedEvents, evt)
func condFilter(pred bool, f key.Filter) event.Filter {
if pred {
return f
} else {
return nil
} }
return combinedEvents
} }
func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) { func (e *Editor) processKey(gtx layout.Context) {
if e.text.Changed() { if e.text.Changed() {
return ChangeEvent{}, true e.events = append(e.events, ChangeEvent{})
}
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.ModShift},
key.Filter{Focus: e, Name: key.NameEnd, Optional: 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}),
} }
// adjust keeps track of runes dropped because of MaxLen. // adjust keeps track of runes dropped because of MaxLen.
var adjust int var adjust int
for { for _, ke := range gtx.Events(&e.eventKey) {
ke, ok := gtx.Event(filters...)
if !ok {
break
}
e.blinkStart = gtx.Now e.blinkStart = gtx.Now
switch ke := ke.(type) { switch ke := ke.(type) {
case key.FocusEvent: case key.FocusEvent:
e.focused = ke.Focus
// Reset IME state. // Reset IME state.
e.ime.imeState = imeState{} e.ime.imeState = imeState{}
if ke.Focus {
gtx.Execute(key.SoftKeyboardCmd{Show: true})
}
case key.Event: case key.Event:
if !gtx.Focused(e) || ke.State != key.Press { if !e.focused || ke.State != key.Press {
break break
} }
if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) { if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
if !ke.Modifiers.Contain(key.ModShift) { if !ke.Modifiers.Contain(key.ModShift) {
e.scratch = e.text.Text(e.scratch) e.scratch = e.text.Text(e.scratch)
return SubmitEvent{ e.events = append(e.events, SubmitEvent{
Text: string(e.scratch), Text: string(e.scratch),
}, true })
continue
} }
} }
e.command(gtx, ke)
e.scrollCaret = true e.scrollCaret = true
e.scroller.Stop() e.scroller.Stop()
ev, ok := e.command(gtx, ke)
if ok {
return ev, ok
}
case key.SnippetEvent: case key.SnippetEvent:
e.updateSnippet(gtx, ke.Start, ke.End) e.updateSnippet(gtx, ke.Start, ke.End)
case key.EditEvent: case key.EditEvent:
@@ -442,26 +369,19 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
// Reset caret xoff. // Reset caret xoff.
e.text.MoveCaret(0, 0) e.text.MoveCaret(0, 0)
if submit { if submit {
e.scratch = e.text.Text(e.scratch)
submitEvent := SubmitEvent{
Text: string(e.scratch),
}
if e.text.Changed() { if e.text.Changed() {
e.pending = append(e.pending, submitEvent) e.events = append(e.events, ChangeEvent{})
return ChangeEvent{}, true
} }
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(). // Complete a paste event, initiated by Shortcut-V in Editor.command().
case transfer.DataEvent: case clipboard.Event:
e.scrollCaret = true e.scrollCaret = true
e.scroller.Stop() e.scroller.Stop()
content, err := io.ReadAll(ke.Open()) e.Insert(ke.Text)
if err == nil {
if e.Insert(string(content)) != 0 {
return ChangeEvent{}, true
}
}
case key.SelectionEvent: case key.SelectionEvent:
e.scrollCaret = true e.scrollCaret = true
e.scroller.Stop() e.scroller.Stop()
@@ -472,12 +392,11 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
} }
} }
if e.text.Changed() { 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 direction := 1
if gtx.Locale.Direction.Progression() == system.TowardOrigin { if gtx.Locale.Direction.Progression() == system.TowardOrigin {
direction = -1 direction = -1
@@ -493,17 +412,15 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
// half is in Editor.processKey() under clipboard.Event. // half is in Editor.processKey() under clipboard.Event.
case "V": case "V":
if !e.ReadOnly { 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. // Copy or Cut selection -- ignored if nothing selected.
case "C", "X": case "C", "X":
e.scratch = e.text.SelectedText(e.scratch) e.scratch = e.text.SelectedText(e.scratch)
if text := string(e.scratch); text != "" { 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 k.Name == "X" && !e.ReadOnly {
if e.Delete(1) != 0 { e.Delete(1)
return ChangeEvent{}, true
}
} }
} }
// Select all // Select all
@@ -512,47 +429,33 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
case "Z": case "Z":
if !e.ReadOnly { if !e.ReadOnly {
if k.Modifiers.Contain(key.ModShift) { if k.Modifiers.Contain(key.ModShift) {
if ev, ok := e.redo(); ok { e.redo()
return ev, ok
}
} else { } else {
if ev, ok := e.undo(); ok { e.undo()
return ev, ok
}
} }
} }
} }
return nil, false return
} }
switch k.Name { switch k.Name {
case key.NameReturn, key.NameEnter: case key.NameReturn, key.NameEnter:
if !e.ReadOnly { if !e.ReadOnly {
if e.Insert("\n") != 0 { e.Insert("\n")
return ChangeEvent{}, true
}
} }
case key.NameDeleteBackward: case key.NameDeleteBackward:
if !e.ReadOnly { if !e.ReadOnly {
if moveByWord { if moveByWord {
if e.deleteWord(-1) != 0 { e.deleteWord(-1)
return ChangeEvent{}, true
}
} else { } else {
if e.Delete(-1) != 0 { e.Delete(-1)
return ChangeEvent{}, true
}
} }
} }
case key.NameDeleteForward: case key.NameDeleteForward:
if !e.ReadOnly { if !e.ReadOnly {
if moveByWord { if moveByWord {
if e.deleteWord(1) != 0 { e.deleteWord(1)
return ChangeEvent{}, true
}
} else { } else {
if e.Delete(1) != 0 { e.Delete(1)
return ChangeEvent{}, true
}
} }
} }
case key.NameUpArrow: case key.NameUpArrow:
@@ -586,7 +489,16 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
case key.NameEnd: case key.NameEnd:
e.text.MoveEnd(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 // initBuffer should be invoked first in every exported function that accesses
@@ -605,51 +517,48 @@ func (e *Editor) initBuffer() {
e.text.WrapPolicy = e.WrapPolicy e.text.WrapPolicy = e.WrapPolicy
} }
// Update the state of the editor in response to input events. Update consumes editor // Update the state of the editor in response to input events.
// input events until there are no remaining events or an editor event is generated. func (e *Editor) Update(gtx layout.Context) {
// 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) {
e.initBuffer() e.initBuffer()
event, ok := e.processEvents(gtx) e.processEvents(gtx)
// Notify IME of selection if it changed. if e.focused {
newSel := e.ime.selection // Notify IME of selection if it changed.
start, end := e.text.Selection() newSel := e.ime.selection
newSel.rng = key.Range{ start, end := e.text.Selection()
Start: start, newSel.rng = key.Range{
End: end, Start: start,
} End: end,
caretPos, carAsc, carDesc := e.text.CaretInfo() }
newSel.caret = key.Caret{ caretPos, carAsc, carDesc := e.text.CaretInfo()
Pos: layout.FPt(caretPos), newSel.caret = key.Caret{
Ascent: float32(carAsc), Pos: layout.FPt(caretPos),
Descent: float32(carDesc), Ascent: float32(carAsc),
} Descent: float32(carDesc),
if newSel != e.ime.selection { }
e.ime.selection = newSel if newSel != e.ime.selection {
gtx.Execute(key.SelectionCmd{Tag: e, Range: newSel.rng, Caret: newSel.caret}) 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) e.updateSnippet(gtx, e.ime.start, e.ime.end)
return event, ok }
} }
// Layout lays out the editor using the provided textMaterial as the paint material // 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 // for the text glyphs+caret and the selectMaterial as the paint material for the
// selection rectangle. // selection rectangle.
func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions { func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
for { e.Update(gtx)
_, ok := e.Update(gtx)
if !ok {
break
}
}
e.text.Layout(gtx, lt, font, size) e.text.Layout(gtx, lt, font, size)
return e.layout(gtx, textMaterial, selectMaterial) 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. // have changed. off and len are in runes.
func (e *Editor) updateSnippet(gtx layout.Context, start, end int) { func (e *Editor) updateSnippet(gtx layout.Context, start, end int) {
if start > end { if start > end {
@@ -689,7 +598,10 @@ func (e *Editor) updateSnippet(gtx layout.Context, start, end int) {
return return
} }
e.ime.snippet = newSnip 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 { func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
@@ -700,35 +612,79 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
e.scrollCaret = false e.scrollCaret = false
e.text.ScrollToCaret() e.text.ScrollToCaret()
} }
textDims := e.text.FullDimensions()
visibleDims := e.text.Dimensions() visibleDims := e.text.Dimensions()
defer clip.Rect(image.Rectangle{Max: visibleDims.Size}).Push(gtx.Ops).Pop() defer clip.Rect(image.Rectangle{Max: visibleDims.Size}).Push(gtx.Ops).Pop()
pointer.CursorText.Add(gtx.Ops) pointer.CursorText.Add(gtx.Ops)
event.Op(gtx.Ops, e) var keys key.Set
key.InputHintOp{Tag: e, Hint: e.InputHint}.Add(gtx.Ops) 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.clicker.Add(gtx.Ops)
e.dragger.Add(gtx.Ops) e.dragger.Add(gtx.Ops)
e.showCaret = false e.showCaret = false
if gtx.Focused(e) { if e.focused {
now := gtx.Now now := gtx.Now
dt := now.Sub(e.blinkStart) dt := now.Sub(e.blinkStart)
blinking := dt < maxBlinkDuration blinking := dt < maxBlinkDuration
const timePerBlink = time.Second / blinksPerSecond const timePerBlink = time.Second / blinksPerSecond
nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2)) nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2))
if blinking { 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) semantic.Editor.Add(gtx.Ops)
if e.Len() > 0 { if e.Len() > 0 {
e.paintSelection(gtx, selectMaterial) e.paintSelection(gtx, selectMaterial)
e.paintText(gtx, textMaterial) e.paintText(gtx, textMaterial)
} }
if gtx.Enabled() { if !disabled {
e.paintCaret(gtx, textMaterial) e.paintCaret(gtx, textMaterial)
} }
return visibleDims return visibleDims
@@ -738,7 +694,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
// material to set the painting material for the selection. // material to set the painting material for the selection.
func (e *Editor) paintSelection(gtx layout.Context, material op.CallOp) { func (e *Editor) paintSelection(gtx layout.Context, material op.CallOp) {
e.initBuffer() e.initBuffer()
if !gtx.Focused(e) { if !e.focused {
return return
} }
e.text.PaintSelection(gtx, material) e.text.PaintSelection(gtx, material)
@@ -802,10 +758,10 @@ func (e *Editor) CaretCoords() f32.Point {
// //
// If there is a selection, it is deleted and counts as a single grapheme // If there is a selection, it is deleted and counts as a single grapheme
// cluster. // cluster.
func (e *Editor) Delete(graphemeClusters int) (deletedRunes int) { func (e *Editor) Delete(graphemeClusters int) {
e.initBuffer() e.initBuffer()
if graphemeClusters == 0 { if graphemeClusters == 0 {
return 0 return
} }
start, end := e.text.Selection() start, end := e.text.Selection()
@@ -821,10 +777,9 @@ func (e *Editor) Delete(graphemeClusters int) (deletedRunes int) {
// Reset xoff. // Reset xoff.
e.text.MoveCaret(0, 0) e.text.MoveCaret(0, 0)
e.ClearSelection() e.ClearSelection()
return end - start
} }
func (e *Editor) Insert(s string) (insertedRunes int) { func (e *Editor) Insert(s string) {
e.initBuffer() e.initBuffer()
if e.SingleLine { if e.SingleLine {
s = strings.ReplaceAll(s, "\n", " ") s = strings.ReplaceAll(s, "\n", " ")
@@ -838,7 +793,6 @@ func (e *Editor) Insert(s string) (insertedRunes int) {
e.text.MoveCaret(0, 0) e.text.MoveCaret(0, 0)
e.SetCaret(start+moves, start+moves) e.SetCaret(start+moves, start+moves)
e.scrollCaret = true e.scrollCaret = true
return moves
} }
// modification represents a change to the contents of the editor buffer. // modification represents a change to the contents of the editor buffer.
@@ -858,10 +812,10 @@ type modification struct {
// undo applies the modification at e.history[e.historyIdx] and decrements // undo applies the modification at e.history[e.historyIdx] and decrements
// e.historyIdx. // e.historyIdx.
func (e *Editor) undo() (EditorEvent, bool) { func (e *Editor) undo() {
e.initBuffer() e.initBuffer()
if len(e.history) < 1 || e.nextHistoryIdx == 0 { if len(e.history) < 1 || e.nextHistoryIdx == 0 {
return nil, false return
} }
mod := e.history[e.nextHistoryIdx-1] mod := e.history[e.nextHistoryIdx-1]
replaceEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent) replaceEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent)
@@ -869,15 +823,14 @@ func (e *Editor) undo() (EditorEvent, bool) {
caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent) caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent)
e.SetCaret(caretEnd, mod.StartRune) e.SetCaret(caretEnd, mod.StartRune)
e.nextHistoryIdx-- e.nextHistoryIdx--
return ChangeEvent{}, true
} }
// redo applies the modification at e.history[e.historyIdx] and increments // redo applies the modification at e.history[e.historyIdx] and increments
// e.historyIdx. // e.historyIdx.
func (e *Editor) redo() (EditorEvent, bool) { func (e *Editor) redo() {
e.initBuffer() e.initBuffer()
if len(e.history) < 1 || e.nextHistoryIdx == len(e.history) { if len(e.history) < 1 || e.nextHistoryIdx == len(e.history) {
return nil, false return
} }
mod := e.history[e.nextHistoryIdx] mod := e.history[e.nextHistoryIdx]
end := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent) end := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent)
@@ -885,7 +838,6 @@ func (e *Editor) redo() (EditorEvent, bool) {
caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent) caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent)
e.SetCaret(caretEnd, mod.StartRune) e.SetCaret(caretEnd, mod.StartRune)
e.nextHistoryIdx++ e.nextHistoryIdx++
return ChangeEvent{}, true
} }
// replace the text between start and end with s. Indices are in runes. // replace the text between start and end with s. Indices are in runes.
@@ -969,18 +921,18 @@ func (e *Editor) MoveCaret(startDelta, endDelta int) {
// Positive is forward, negative is backward. // Positive is forward, negative is backward.
// Absolute values greater than one will delete that many words. // Absolute values greater than one will delete that many words.
// The selection counts as a single word. // The selection counts as a single word.
func (e *Editor) deleteWord(distance int) (deletedRunes int) { func (e *Editor) deleteWord(distance int) {
if distance == 0 { if distance == 0 {
return return
} }
start, end := e.text.Selection() start, end := e.text.Selection()
if start != end { if start != end {
deletedRunes = e.Delete(1) e.Delete(1)
distance -= sign(distance) distance -= sign(distance)
} }
if distance == 0 { if distance == 0 {
return deletedRunes return
} }
// split the distance information into constituent parts to be // split the distance information into constituent parts to be
@@ -1020,8 +972,7 @@ func (e *Editor) deleteWord(distance int) (deletedRunes int) {
runes += 1 runes += 1
} }
} }
deletedRunes += e.Delete(runes * direction) e.Delete(runes * direction)
return deletedRunes
} }
// SelectionLen returns the length of the selection, in runes; it is // SelectionLen returns the length of the selection, in runes; it is
+80 -127
View File
@@ -20,7 +20,7 @@ import (
"gioui.org/font" "gioui.org/font"
"gioui.org/font/gofont" "gioui.org/font/gofont"
"gioui.org/font/opentype" "gioui.org/font/opentype"
"gioui.org/io/input" "gioui.org/io/event"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
@@ -96,14 +96,17 @@ func assertContents(t *testing.T, e *Editor, contents string, selectionStart, se
// TestEditorReadOnly ensures that mouse and keyboard interactions with readonly // TestEditorReadOnly ensures that mouse and keyboard interactions with readonly
// editors do nothing but manipulate the text selection. // editors do nothing but manipulate the text selection.
func TestEditorReadOnly(t *testing.T) { func TestEditorReadOnly(t *testing.T) {
r := new(input.Router)
gtx := layout.Context{ gtx := layout.Context{
Ops: new(op.Ops), Ops: new(op.Ops),
Constraints: layout.Constraints{ Constraints: layout.Constraints{
Max: image.Pt(100, 100), Max: image.Pt(100, 100),
}, },
Locale: english, Locale: english,
Source: r.Source(), }
gtx.Queue = &testQueue{
events: []event.Event{
key.FocusEvent{Focus: true},
},
} }
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
@@ -115,23 +118,12 @@ func TestEditorReadOnly(t *testing.T) {
if cStart != cEnd { if cStart != cEnd {
t.Errorf("unexpected initial caret positions") t.Errorf("unexpected initial caret positions")
} }
gtx.Execute(key.FocusCmd{Tag: e}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
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)
// Select everything. // Select everything.
gtx.Ops.Reset() gtx.Ops.Reset()
r.Queue(key.Event{Name: "A", Modifiers: key.ModShortcut}) gtx.Queue = &testQueue{events: []event.Event{key.Event{Name: "A", Modifiers: key.ModShortcut}}}
layoutEditor() e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
textContent := e.Text() textContent := e.Text()
cStart2, cEnd2 := e.Selection() cStart2, cEnd2 := e.Selection()
if cStart2 > cEnd2 { if cStart2 > cEnd2 {
@@ -146,7 +138,7 @@ func TestEditorReadOnly(t *testing.T) {
// Type some new characters. // Type some new characters.
gtx.Ops.Reset() 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) e.Update(gtx)
textContent2 := e.Text() textContent2 := e.Text()
if textContent2 != textContent { if textContent2 != textContent {
@@ -155,8 +147,8 @@ func TestEditorReadOnly(t *testing.T) {
// Try to delete selection. // Try to delete selection.
gtx.Ops.Reset() gtx.Ops.Reset()
r.Queue(key.Event{Name: key.NameDeleteBackward}) gtx.Queue = &testQueue{events: []event.Event{key.Event{Name: key.NameDeleteBackward}}}
dims := layoutEditor() dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
textContent2 = e.Text() textContent2 = e.Text()
if textContent2 != textContent { if textContent2 != textContent {
t.Errorf("readonly editor modified by delete key.Event") 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 // Click and drag from the middle of the first line
// to the center. // to the center.
gtx.Ops.Reset() gtx.Ops.Reset()
r.Queue( gtx.Queue = &testQueue{events: []event.Event{
pointer.Event{ pointer.Event{
Kind: pointer.Press, Kind: pointer.Press,
Buttons: pointer.ButtonPrimary, Buttons: pointer.ButtonPrimary,
Position: f32.Pt(float32(dims.Size.X)*.5, 5), Position: f32.Pt(float32(dims.Size.X)*.5, 5),
}, },
pointer.Event{ pointer.Event{
Kind: pointer.Move, Kind: pointer.Drag,
Buttons: pointer.ButtonPrimary, Buttons: pointer.ButtonPrimary,
Position: layout.FPt(dims.Size).Mul(.5), Position: layout.FPt(dims.Size).Mul(.5),
}, },
@@ -181,7 +173,7 @@ func TestEditorReadOnly(t *testing.T) {
Buttons: pointer.ButtonPrimary, Buttons: pointer.ButtonPrimary,
Position: layout.FPt(dims.Size).Mul(.5), Position: layout.FPt(dims.Size).Mul(.5),
}, },
) }}
e.Update(gtx) e.Update(gtx)
cStart3, cEnd3 := e.Selection() cStart3, cEnd3 := e.Selection()
if cStart3 == cStart2 || cEnd3 == cEnd2 { if cStart3 == cStart2 || cEnd3 == cEnd2 {
@@ -504,22 +496,22 @@ func TestEditorLigature(t *testing.T) {
func TestEditorDimensions(t *testing.T) { func TestEditorDimensions(t *testing.T) {
e := new(Editor) e := new(Editor)
r := new(input.Router) tq := &testQueue{
events: []event.Event{
key.EditEvent{Text: "A"},
},
}
gtx := layout.Context{ gtx := layout.Context{
Ops: new(op.Ops), Ops: new(op.Ops),
Constraints: layout.Constraints{Max: image.Pt(100, 100)}, Constraints: layout.Constraints{Max: image.Pt(100, 100)},
Source: r.Source(), Queue: tq,
Locale: english, Locale: english,
} }
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
font := font.Font{} font := font.Font{}
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{}) 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") t.Errorf("EditEvent was not reflected in Editor width")
} }
} }
@@ -890,11 +882,9 @@ f 2 4 6 8 f
g 2 4 6 8 g g 2 4 6 8 g
`) `)
r := new(input.Router)
gtx := layout.Context{ gtx := layout.Context{
Ops: new(op.Ops), Ops: new(op.Ops),
Locale: english, Locale: english,
Source: r.Source(),
} }
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
font := font.Font{} font := font.Font{}
@@ -902,39 +892,42 @@ g 2 4 6 8 g
var tim time.Duration var tim time.Duration
selected := func(start, end int) string { selected := func(start, end int) string {
gtx.Execute(key.FocusCmd{Tag: e})
// Layout once with no events; populate e.lines. // Layout once with no events; populate e.lines.
gtx.Queue = nil
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) 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 // Build the selection events
startPos := e.text.closestToRune(start) startPos := e.text.closestToRune(start)
endPos := e.text.closestToRune(end) endPos := e.text.closestToRune(end)
r.Queue( tq := &testQueue{
pointer.Event{ events: []event.Event{
Buttons: pointer.ButtonPrimary, pointer.Event{
Kind: pointer.Press, Buttons: pointer.ButtonPrimary,
Source: pointer.Mouse, Kind: pointer.Press,
Time: tim, Source: pointer.Mouse,
Position: f32.Pt(textWidth(e, startPos.lineCol.line, 0, startPos.lineCol.col), textBaseline(e, startPos.lineCol.line)), 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. tim += time.Second // Avoid multi-clicks.
gtx.Queue = tq
for { e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
_, ok := e.Update(gtx) // throw away any events from this layout for _, evt := range e.Events() {
if !ok { switch evt.(type) {
break case SelectEvent:
return e.SelectedText()
} }
} }
return e.SelectedText() return ""
} }
type screenPos image.Point type screenPos image.Point
logicalPosMatch := func(t *testing.T, n int, label string, expected screenPos, actual combinedPos) { logicalPosMatch := func(t *testing.T, n int, label string, expected screenPos, actual combinedPos) {
@@ -972,7 +965,7 @@ g 2 4 6 8 g
// Constrain the editor to roughly 6 columns wide and redraw // Constrain the editor to roughly 6 columns wide and redraw
gtx.Constraints = layout.Exact(image.Pt(36, 36)) gtx.Constraints = layout.Exact(image.Pt(36, 36))
// Keep existing selection // Keep existing selection
gtx = gtx.Disabled() gtx.Queue = nil
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
caretStart := e.text.closestToRune(e.text.caret.start) caretStart := e.text.closestToRune(e.text.caret.start)
@@ -987,30 +980,19 @@ func TestSelectMove(t *testing.T) {
e := new(Editor) e := new(Editor)
e.SetText(`0123456789`) e.SetText(`0123456789`)
r := new(input.Router)
gtx := layout.Context{ gtx := layout.Context{
Ops: new(op.Ops), Ops: new(op.Ops),
Locale: english, Locale: english,
Source: r.Source(),
} }
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
font := font.Font{} font := font.Font{}
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
// Layout once to populate e.lines and get focus. // 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{}) 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 // Select 345
e.SetCaret(3, 6) e.SetCaret(3, 6)
if expected, got := "345", e.SelectedText(); expected != got { if expected, got := "345", e.SelectedText(); expected != got {
@@ -1018,15 +1000,18 @@ func TestSelectMove(t *testing.T) {
} }
// Press the key // Press the key
r.Queue(key.Event{State: key.Press, Name: keyName}) gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName})
gtx.Ops.Reset()
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
if expected, got := "", e.SelectedText(); expected != got { if expected, got := "", e.SelectedText(); expected != got {
t.Errorf("KeyName %s, expected %q, got %q", keyName, 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) { func TestEditor_Read(t *testing.T) {
@@ -1079,22 +1064,17 @@ func TestEditor_MaxLen(t *testing.T) {
} }
e.SetText("2345678") e.SetText("2345678")
r := new(input.Router)
gtx := layout.Context{ gtx := layout.Context{
Ops: new(op.Ops), Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)), 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())) cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
font := font.Font{} 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{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
if got, want := e.Text(), "12345678"; got != want { if got, want := e.Text(), "12345678"; got != want {
@@ -1115,22 +1095,17 @@ func TestEditor_Filter(t *testing.T) {
} }
e.SetText("2345678") e.SetText("2345678")
r := new(input.Router)
gtx := layout.Context{ gtx := layout.Context{
Ops: new(op.Ops), Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)), 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())) cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
font := font.Font{} 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{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
if got, want := e.Text(), "12345678"; got != want { if got, want := e.Text(), "12345678"; got != want {
@@ -1145,33 +1120,22 @@ func TestEditor_Submit(t *testing.T) {
e := new(Editor) e := new(Editor)
e.Submit = true e.Submit = true
r := new(input.Router)
gtx := layout.Context{ gtx := layout.Context{
Ops: new(op.Ops), Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)), 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())) cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10) fontSize := unit.Sp(10)
font := font.Font{} font := font.Font{}
gtx.Execute(key.FocusCmd{Tag: e})
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) 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 { if got, want := e.Text(), "ab1"; got != want {
t.Errorf("editor failed to filter newline") t.Errorf("editor failed to filter newline")
} }
got := e.Events()
want := []EditorEvent{ want := []EditorEvent{
ChangeEvent{}, ChangeEvent{},
SubmitEvent{Text: e.Text()}, SubmitEvent{Text: e.Text()},
@@ -1181,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. // textWidth is a text helper for building simple selection events.
// It assumes single-run lines, which isn't safe with non-test text // It assumes single-run lines, which isn't safe with non-test text
// data. // data.
@@ -1223,3 +1164,15 @@ func textBaseline(e *Editor, lineNum int) float32 {
start := e.text.closestToLineCol(lineNum, 0) start := e.text.closestToLineCol(lineNum, 0)
return float32(start.y) 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
View File
@@ -4,7 +4,6 @@ package widget
import ( import (
"gioui.org/gesture" "gioui.org/gesture"
"gioui.org/io/event"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic" "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. // Update the state and report whether Value has changed by user interaction.
func (e *Enum) Update(gtx layout.Context) bool { func (e *Enum) Update(gtx layout.Context) bool {
if !gtx.Enabled() { if gtx.Queue == nil {
e.focused = false e.focused = false
} }
e.hovering = false e.hovering = false
changed := false changed := false
for _, state := range e.keys { for _, state := range e.keys {
for { for _, ev := range state.click.Update(gtx) {
ev, ok := state.click.Update(gtx.Source)
if !ok {
break
}
switch ev.Kind { switch ev.Kind {
case gesture.KindPress: case gesture.KindPress:
if ev.Source == pointer.Mouse { if ev.Source == pointer.Mouse {
gtx.Execute(key.FocusCmd{Tag: &state.tag}) key.FocusOp{Tag: &state.tag}.Add(gtx.Ops)
} }
case gesture.KindClick: case gesture.KindClick:
if state.key != e.Value { if state.key != e.Value {
@@ -64,15 +59,7 @@ func (e *Enum) Update(gtx layout.Context) bool {
} }
} }
} }
for { for _, ev := range gtx.Events(&state.tag) {
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
}
switch ev := ev.(type) { switch ev := ev.(type) {
case key.FocusEvent: case key.FocusEvent:
if ev.Focus { if ev.Focus {
@@ -82,7 +69,7 @@ func (e *Enum) Update(gtx layout.Context) bool {
e.focused = false e.focused = false
} }
case key.Event: case key.Event:
if ev.State != key.Release { if !e.focused || ev.State != key.Release {
break break
} }
if ev.Name != key.NameReturn && ev.Name != key.NameSpace { 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 := &state.click
clk.Add(gtx.Ops) 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.SelectedOp(k == e.Value).Add(gtx.Ops)
semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops) semantic.EnabledOp(enabled).Add(gtx.Ops)
c.Add(gtx.Ops) c.Add(gtx.Ops)
return dims return dims
+20 -20
View File
@@ -5,13 +5,10 @@ package widget_test
import ( import (
"fmt" "fmt"
"image" "image"
"io"
"strings"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/io/transfer" "gioui.org/io/transfer"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
@@ -24,11 +21,11 @@ func ExampleClickable_passthrough() {
// pointer events can be passed down for the underlying // pointer events can be passed down for the underlying
// widgets to pick them up. // widgets to pick them up.
var button1, button2 widget.Clickable var button1, button2 widget.Clickable
var r input.Router var r router.Router
gtx := layout.Context{ gtx := layout.Context{
Ops: new(op.Ops), Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)), Constraints: layout.Exact(image.Pt(100, 100)),
Source: r.Source(), Queue: &r,
} }
// widget lays out two buttons on top of each other. // widget lays out two buttons on top of each other.
@@ -74,15 +71,15 @@ func ExampleClickable_passthrough() {
} }
func ExampleDraggable_Layout() { func ExampleDraggable_Layout() {
var r input.Router var r router.Router
gtx := layout.Context{ gtx := layout.Context{
Ops: new(op.Ops), Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)), Constraints: layout.Exact(image.Pt(100, 100)),
Source: r.Source(), Queue: &r,
} }
// mime is the type used to match drag and drop operations. // mime is the type used to match drag and drop operations.
// It could be left empty in this example. // It could be left empty in this example.
const mime = "MyMime" mime := "MyMime"
drag := &widget.Draggable{Type: mime} drag := &widget.Draggable{Type: mime}
var drop int var drop int
// widget lays out the drag and drop handlers and processes // 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. // drag must respond with an Offer event when requested.
// Use the drag method for this. // Use the drag method for this.
if m, ok := drag.Update(gtx); ok { 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. // Setup the area for drops.
@@ -105,21 +102,17 @@ func ExampleDraggable_Layout() {
Min: image.Pt(20, 20), Min: image.Pt(20, 20),
Max: image.Pt(40, 40), Max: image.Pt(40, 40),
}.Push(gtx.Ops) }.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() ds.Pop()
// Check for the received data. // Check for the received data.
for { for _, ev := range gtx.Events(&drop) {
ev, ok := gtx.Event(transfer.TargetFilter{Target: &drop, Type: mime})
if !ok {
break
}
switch e := ev.(type) { switch e := ev.(type) {
case transfer.DataEvent: case transfer.DataEvent:
data := e.Open() data := e.Open()
defer data.Close() fmt.Println(data.(offer).Data)
content, _ := io.ReadAll(data)
fmt.Println(string(content))
} }
} }
} }
@@ -152,3 +145,10 @@ func ExampleDraggable_Layout() {
// Output: // Output:
// hello world // hello world
} }
type offer struct {
Data string
}
func (offer) Read([]byte) (int, error) { return 0, nil }
func (offer) Close() error { return nil }
+1 -5
View File
@@ -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. // The range of f is set by the minimum constraints main axis value.
func (f *Float) Update(gtx layout.Context) bool { func (f *Float) Update(gtx layout.Context) bool {
changed := false changed := false
for { for _, e := range f.drag.Update(gtx.Metric, gtx, gesture.Axis(f.axis)) {
e, ok := f.drag.Update(gtx.Metric, gtx.Source, gesture.Axis(f.axis))
if !ok {
break
}
if f.length > 0 && (e.Kind == pointer.Press || e.Kind == pointer.Drag) { if f.length > 0 && (e.Kind == pointer.Press || e.Kind == pointer.Drag) {
pos := e.Position.X pos := e.Position.X
if f.axis == layout.Vertical { if f.axis == layout.Vertical {
+4
View File
@@ -347,6 +347,10 @@ type Region struct {
Baseline int 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 // 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 // [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. // return results instead of allocating, provided that there is enough capacity.
+4
View File
@@ -97,6 +97,10 @@ func (l Label) LayoutDetailed(gtx layout.Context, lt *text.Shaper, font font.Fon
return dims, TextInfo{Truncated: it.truncated} return dims, TextInfo{Truncated: it.truncated}
} }
func r2p(r clip.Rect) clip.Op {
return clip.Stroke{Path: r.Path(), Width: 1}.Op()
}
// textIterator computes the bounding box of and paints text. // textIterator computes the bounding box of and paints text.
type textIterator struct { type textIterator struct {
// viewport is the rectangle of document coordinates that the iterator is // viewport is the rectangle of document coordinates that the iterator is
+8 -18
View File
@@ -29,8 +29,8 @@ type Scrollbar struct {
oldDragPos float32 oldDragPos float32
} }
// Update updates the internal state of the scrollbar based on events // Layout updates the internal state of the scrollbar based on events
// since the previous call to Update. The provided axis will be used to // since the previous call to Layout. The provided axis will be used to
// normalize input event coordinates and constraints into an axis- // normalize input event coordinates and constraints into an axis-
// independent format. viewportStart is the position of the beginning // independent format. viewportStart is the position of the beginning
// of the scrollable viewport relative to the underlying content expressed // of the scrollable viewport relative to the underlying content expressed
@@ -39,7 +39,7 @@ type Scrollbar struct {
// as a value in the range [0,1]. For example, if viewportStart is 0.25 // as a value in the range [0,1]. For example, if viewportStart is 0.25
// and viewportEnd is .5, the viewport described by the scrollbar is // and viewportEnd is .5, the viewport described by the scrollbar is
// currently showing the second quarter of the underlying content. // currently showing the second quarter of the underlying content.
func (s *Scrollbar) Update(gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) { func (s *Scrollbar) Layout(gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) layout.Dimensions {
// Calculate the length of the major axis of the scrollbar. This is // Calculate the length of the major axis of the scrollbar. This is
// the length of the track within which pointer events occur, and is // the length of the track within which pointer events occur, and is
// used to scale those interactions. // used to scale those interactions.
@@ -61,11 +61,7 @@ func (s *Scrollbar) Update(gtx layout.Context, axis layout.Axis, viewportStart,
} }
// Jump to a click in the track. // Jump to a click in the track.
for { for _, event := range s.track.Update(gtx) {
event, ok := s.track.Update(gtx.Source)
if !ok {
break
}
if event.Kind != gesture.KindClick || if event.Kind != gesture.KindClick ||
event.Modifiers != key.Modifiers(0) || event.Modifiers != key.Modifiers(0) ||
event.NumClicks > 1 { event.NumClicks > 1 {
@@ -84,11 +80,7 @@ func (s *Scrollbar) Update(gtx layout.Context, axis layout.Axis, viewportStart,
} }
// Offset to account for any drags. // Offset to account for any drags.
for { for _, event := range s.drag.Update(gtx.Metric, gtx, gesture.Axis(axis)) {
event, ok := s.drag.Update(gtx.Metric, gtx.Source, gesture.Axis(axis))
if !ok {
break
}
switch event.Kind { switch event.Kind {
case pointer.Drag: case pointer.Drag:
case pointer.Release, pointer.Cancel: case pointer.Release, pointer.Cancel:
@@ -144,11 +136,9 @@ func (s *Scrollbar) Update(gtx layout.Context, axis layout.Axis, viewportStart,
// Process events from the indicator so that hover is // Process events from the indicator so that hover is
// detected properly. // detected properly.
for { _ = s.indicator.Update(gtx)
if _, ok := s.indicator.Update(gtx.Source); !ok {
break return layout.Dimensions{}
}
}
} }
// AddTrack configures the track click listener for the scrollbar to use // AddTrack configures the track click listener for the scrollbar to use
+6 -6
View File
@@ -96,7 +96,7 @@ func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) la
return layout.Background{}.Layout(gtx, return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions { func(gtx layout.Context) layout.Dimensions {
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop() defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
if button.Hovered() || gtx.Focused(button) { if button.Hovered() || button.Focused() {
paint.Fill(gtx.Ops, f32color.Hovered(color.NRGBA{})) paint.Fill(gtx.Ops, f32color.Hovered(color.NRGBA{}))
} }
for _, c := range button.History() { for _, c := range button.History() {
@@ -133,9 +133,9 @@ func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Di
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop() defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
background := b.Background background := b.Background
switch { switch {
case !gtx.Enabled(): case gtx.Queue == nil:
background = f32color.Disabled(b.Background) background = f32color.Disabled(b.Background)
case b.Button.Hovered() || gtx.Focused(b.Button): case b.Button.Hovered() || b.Button.Focused():
background = f32color.Hovered(b.Background) background = f32color.Hovered(b.Background)
} }
paint.Fill(gtx.Ops, background) paint.Fill(gtx.Ops, background)
@@ -165,9 +165,9 @@ func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop() defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
background := b.Background background := b.Background
switch { switch {
case !gtx.Enabled(): case gtx.Queue == nil:
background = f32color.Disabled(b.Background) background = f32color.Disabled(b.Background)
case b.Button.Hovered() || gtx.Focused(b.Button): case b.Button.Hovered() || b.Button.Focused():
background = f32color.Hovered(b.Background) background = f32color.Hovered(b.Background)
} }
paint.Fill(gtx.Ops, background) paint.Fill(gtx.Ops, background)
@@ -258,7 +258,7 @@ func drawInk(gtx layout.Context, c widget.Press) {
// Animate only ended presses, and presses that are fading in. // Animate only ended presses, and presses that are fading in.
if !c.End.IsZero() || sizet <= 1.0 { if !c.End.IsZero() || sizet <= 1.0 {
gtx.Execute(op.InvalidateCmd{}) op.InvalidateOp{}.Add(gtx.Ops)
} }
if sizet > 1.0 { if sizet > 1.0 {
+1 -1
View File
@@ -60,7 +60,7 @@ func (c *checkable) layout(gtx layout.Context, checked, hovered bool) layout.Dim
return layout.UniformInset(2).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.UniformInset(2).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
size := gtx.Dp(c.Size) size := gtx.Dp(c.Size)
col := c.IconColor col := c.IconColor
if !gtx.Enabled() { if gtx.Queue == nil {
col = f32color.Disabled(col) col = f32color.Disabled(col)
} }
gtx.Constraints.Min = image.Point{X: size} gtx.Constraints.Min = image.Point{X: size}
+1 -1
View File
@@ -35,6 +35,6 @@ func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle {
func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions { func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions {
return c.CheckBox.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return c.CheckBox.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.CheckBox.Add(gtx.Ops) semantic.CheckBox.Add(gtx.Ops)
return c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered() || gtx.Focused(c.CheckBox)) return c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered() || c.CheckBox.Focused())
}) })
} }
+6 -6
View File
@@ -22,7 +22,7 @@
// //
// theme := material.NewTheme(...) // theme := material.NewTheme(...)
// //
// material.Button(theme, button, "Click me!").Layout(gtx) // material.Button(theme, "Click me!").Layout(gtx, button)
// //
// # Customization // # Customization
// //
@@ -36,22 +36,22 @@
// Theme-global parameters: For changing the look of all widgets drawn with a // Theme-global parameters: For changing the look of all widgets drawn with a
// particular theme, adjust the `Theme` fields: // particular theme, adjust the `Theme` fields:
// //
// theme.Palette.Fg = color.NRGBA{...} // theme.Color.Primary = color.NRGBA{...}
// //
// Widget-local parameters: For changing the look of a particular widget, // Widget-local parameters: For changing the look of a particular widget,
// adjust the widget specific theme object: // adjust the widget specific theme object:
// //
// btn := material.Button(theme, button, "Click me!") // btn := material.Button(theme, "Click me!")
// btn.Font.Style = text.Italic // btn.Font.Style = text.Italic
// btn.Layout(gtx) // btn.Layout(gtx, button)
// //
// Widget variants: A widget can have several distinct representations even // Widget variants: A widget can have several distinct representations even
// though the underlying state is the same. A widget.Clickable can be drawn as a // though the underlying state is the same. A widget.Clickable can be drawn as a
// round icon button: // round icon button:
// //
// icon := widget.NewIcon(...) // icon := material.NewIcon(...)
// //
// material.IconButton(theme, button, icon, "Click me!").Layout(gtx) // material.IconButton(theme, icon).Layout(gtx, button)
// //
// Specialized widgets: Theme both define a generic Label method // Specialized widgets: Theme both define a generic Label method
// that takes a text size, and specialized methods for standard text // that takes a text size, and specialized methods for standard text
+1 -1
View File
@@ -61,7 +61,7 @@ func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions {
paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops) paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops)
hintColor := hintColorMacro.Stop() hintColor := hintColorMacro.Stop()
selectionColorMacro := op.Record(gtx.Ops) selectionColorMacro := op.Record(gtx.Ops)
paint.ColorOp{Color: blendDisabledColor(!gtx.Enabled(), e.SelectionColor)}.Add(gtx.Ops) paint.ColorOp{Color: blendDisabledColor(gtx.Queue == nil, e.SelectionColor)}.Add(gtx.Ops)
selectionColor := selectionColorMacro.Stop() selectionColor := selectionColorMacro.Stop()
var maxlines int var maxlines int
+1 -1
View File
@@ -144,7 +144,7 @@ func (s ScrollbarStyle) Layout(gtx layout.Context, axis layout.Axis, viewportSta
gtx.Constraints.Min = convert(gtx.Constraints.Min) gtx.Constraints.Min = convert(gtx.Constraints.Min)
gtx.Constraints.Max = gtx.Constraints.Min gtx.Constraints.Max = gtx.Constraints.Min
s.Scrollbar.Update(gtx, axis, viewportStart, viewportEnd) s.Scrollbar.Layout(gtx, axis, viewportStart, viewportEnd)
// Darken indicator if hovered. // Darken indicator if hovered.
if s.Scrollbar.IndicatorHovered() { if s.Scrollbar.IndicatorHovered() {
+8 -5
View File
@@ -3,7 +3,9 @@ package material_test
import ( import (
"image" "image"
"testing" "testing"
"time"
"gioui.org/io/system"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/unit" "gioui.org/unit"
@@ -12,17 +14,18 @@ import (
) )
func TestListAnchorStrategies(t *testing.T) { func TestListAnchorStrategies(t *testing.T) {
gtx := layout.Context{ var ops op.Ops
Ops: new(op.Ops), gtx := layout.NewContext(&ops, system.FrameEvent{
Metric: unit.Metric{ Metric: unit.Metric{
PxPerDp: 1, PxPerDp: 1,
PxPerSp: 1, PxPerSp: 1,
}, },
Constraints: layout.Exact(image.Point{ Now: time.Now(),
Size: image.Point{
X: 500, X: 500,
Y: 500, Y: 500,
}), },
} })
gtx.Constraints.Min = image.Point{} gtx.Constraints.Min = image.Point{}
var spaceConstraints layout.Constraints var spaceConstraints layout.Constraints
+1 -1
View File
@@ -47,7 +47,7 @@ func (l LoaderStyle) Layout(gtx layout.Context) layout.Dimensions {
}.Add(gtx.Ops) }.Add(gtx.Ops)
defer op.Offset(image.Pt(-radius, -radius)).Push(gtx.Ops).Pop() defer op.Offset(image.Pt(-radius, -radius)).Push(gtx.Ops).Pop()
paint.PaintOp{}.Add(gtx.Ops) paint.PaintOp{}.Add(gtx.Ops)
gtx.Execute(op.InvalidateCmd{}) op.InvalidateOp{}.Add(gtx.Ops)
return layout.Dimensions{ return layout.Dimensions{
Size: sz, Size: sz,
} }
+5 -7
View File
@@ -15,8 +15,6 @@ import (
type ProgressBarStyle struct { type ProgressBarStyle struct {
Color color.NRGBA Color color.NRGBA
Height unit.Dp
Radius unit.Dp
TrackColor color.NRGBA TrackColor color.NRGBA
Progress float32 Progress float32
} }
@@ -24,8 +22,6 @@ type ProgressBarStyle struct {
func ProgressBar(th *Theme, progress float32) ProgressBarStyle { func ProgressBar(th *Theme, progress float32) ProgressBarStyle {
return ProgressBarStyle{ return ProgressBarStyle{
Progress: progress, Progress: progress,
Height: unit.Dp(4),
Radius: unit.Dp(2),
Color: th.Palette.ContrastBg, Color: th.Palette.ContrastBg,
TrackColor: f32color.MulAlpha(th.Palette.Fg, 0x88), TrackColor: f32color.MulAlpha(th.Palette.Fg, 0x88),
} }
@@ -33,8 +29,10 @@ func ProgressBar(th *Theme, progress float32) ProgressBarStyle {
func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions { func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions {
shader := func(width int, color color.NRGBA) layout.Dimensions { shader := func(width int, color color.NRGBA) layout.Dimensions {
d := image.Point{X: width, Y: gtx.Dp(p.Height)} const maxHeight = unit.Dp(4)
rr := gtx.Dp(p.Radius) rr := gtx.Dp(2)
d := image.Point{X: width, Y: gtx.Dp(maxHeight)}
defer clip.UniformRRect(image.Rectangle{Max: image.Pt(width, d.Y)}, rr).Push(gtx.Ops).Pop() defer clip.UniformRRect(image.Rectangle{Max: image.Pt(width, d.Y)}, rr).Push(gtx.Ops).Pop()
paint.ColorOp{Color: color}.Add(gtx.Ops) paint.ColorOp{Color: color}.Add(gtx.Ops)
@@ -51,7 +49,7 @@ func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions {
layout.Stacked(func(gtx layout.Context) layout.Dimensions { layout.Stacked(func(gtx layout.Context) layout.Dimensions {
fillWidth := int(float32(progressBarWidth) * clamp1(p.Progress)) fillWidth := int(float32(progressBarWidth) * clamp1(p.Progress))
fillColor := p.Color fillColor := p.Color
if !gtx.Enabled() { if gtx.Queue == nil {
fillColor = f32color.Disabled(fillColor) fillColor = f32color.Disabled(fillColor)
} }
return shader(fillWidth, fillColor) return shader(fillWidth, fillColor)
+1 -1
View File
@@ -56,7 +56,7 @@ func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions {
trans.Pop() trans.Pop()
color := s.Color color := s.Color
if !gtx.Enabled() { if gtx.Queue == nil {
color = f32color.Disabled(color) color = f32color.Disabled(color)
} }
+2 -2
View File
@@ -55,7 +55,7 @@ func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions {
if s.Switch.Value { if s.Switch.Value {
col = s.Color.Enabled col = s.Color.Enabled
} }
if !gtx.Enabled() { if gtx.Queue == nil {
col = f32color.Disabled(col) col = f32color.Disabled(col)
} }
trackColor := s.Color.Track trackColor := s.Color.Track
@@ -98,7 +98,7 @@ func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions {
return clip.Ellipse(b).Op(gtx.Ops) return clip.Ellipse(b).Op(gtx.Ops)
} }
// Draw hover. // Draw hover.
if s.Switch.Hovered() || gtx.Focused(s.Switch) { if s.Switch.Hovered() || s.Switch.Focused() {
r := thumbRadius * 10 / 17 r := thumbRadius * 10 / 17
background := f32color.MulAlpha(s.Color.Enabled, 70) background := f32color.MulAlpha(s.Color.Enabled, 70)
paint.FillShape(gtx.Ops, background, circle(thumbRadius, thumbRadius, r)) paint.FillShape(gtx.Ops, background, circle(thumbRadius, thumbRadius, r))
+54 -51
View File
@@ -2,7 +2,6 @@ package widget
import ( import (
"image" "image"
"io"
"math" "math"
"strings" "strings"
@@ -71,14 +70,21 @@ type Selectable struct {
source stringSource source stringSource
// scratch is a buffer reused to efficiently read text out of the // scratch is a buffer reused to efficiently read text out of the
// textView. // textView.
scratch []byte scratch []byte
lastValue string lastValue string
text textView text textView
focused bool focused bool
dragging bool requestFocus bool
dragger gesture.Drag dragging bool
dragger gesture.Drag
scroller gesture.Scroll
scrollOff image.Point
clicker gesture.Click 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
} }
// initialize must be called at the beginning of any exported method that // initialize must be called at the beginning of any exported method that
@@ -92,6 +98,11 @@ func (l *Selectable) initialize() {
} }
} }
// Focus requests the input focus for the label.
func (l *Selectable) Focus() {
l.requestFocus = true
}
// Focused returns whether the label is focused or not. // Focused returns whether the label is focused or not.
func (l *Selectable) Focused() bool { func (l *Selectable) Focused() bool {
return l.focused return l.focused
@@ -171,11 +182,10 @@ func (l *Selectable) Truncated() bool {
return l.text.Truncated() return l.text.Truncated()
} }
// Update the state of the selectable in response to input events. It returns whether the // Update the state of the selectable in response to input events.
// text selection changed during event processing. func (l *Selectable) Update(gtx layout.Context) {
func (l *Selectable) Update(gtx layout.Context) bool {
l.initialize() l.initialize()
return l.handleEvents(gtx) l.handleEvents(gtx)
} }
// Layout clips to the dimensions of the selectable, updates the shaped text, configures input handling, and paints // Layout clips to the dimensions of the selectable, updates the shaped text, configures input handling, and paints
@@ -193,7 +203,17 @@ func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font,
dims := l.text.Dimensions() dims := l.text.Dimensions()
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop() defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
pointer.CursorText.Add(gtx.Ops) pointer.CursorText.Add(gtx.Ops)
event.Op(gtx.Ops, l) var keys key.Set
if l.focused {
const keyFilter = "(ShortAlt)-(Shift)-[←,→,↑,↓]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,X,A]"
keys = keyFilter
}
key.InputOp{Tag: l, Keys: keys}.Add(gtx.Ops)
if l.requestFocus {
key.FocusOp{Tag: l}.Add(gtx.Ops)
key.SoftKeyboardOp{Show: true}.Add(gtx.Ops)
}
l.requestFocus = false
l.clicker.Add(gtx.Ops) l.clicker.Add(gtx.Ops)
l.dragger.Add(gtx.Ops) l.dragger.Add(gtx.Ops)
@@ -203,16 +223,18 @@ func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font,
return dims return dims
} }
func (l *Selectable) handleEvents(gtx layout.Context) (selectionChanged bool) { func (l *Selectable) handleEvents(gtx layout.Context) {
// Flush events from before the previous Layout.
n := copy(l.events, l.events[l.prevEvents:])
l.events = l.events[:n]
l.prevEvents = n
oldStart, oldLen := min(l.text.Selection()), l.text.SelectionLen() oldStart, oldLen := min(l.text.Selection()), l.text.SelectionLen()
defer func() {
if newStart, newLen := min(l.text.Selection()), l.text.SelectionLen(); oldStart != newStart || oldLen != newLen {
selectionChanged = true
}
}()
l.processPointer(gtx) l.processPointer(gtx)
l.processKey(gtx) l.processKey(gtx)
return selectionChanged // Queue a SelectEvent if the selection changed, including if it went away.
if newStart, newLen := min(l.text.Selection()), l.text.SelectionLen(); oldStart != newStart || oldLen != newLen {
l.events = append(l.events, SelectEvent{})
}
} }
func (e *Selectable) processPointer(gtx layout.Context) { func (e *Selectable) processPointer(gtx layout.Context) {
@@ -227,7 +249,7 @@ func (e *Selectable) processPointer(gtx layout.Context) {
X: int(math.Round(float64(evt.Position.X))), X: int(math.Round(float64(evt.Position.X))),
Y: int(math.Round(float64(evt.Position.Y))), Y: int(math.Round(float64(evt.Position.Y))),
}) })
gtx.Execute(key.FocusCmd{Tag: e}) e.requestFocus = true
if evt.Modifiers == key.ModShift { if evt.Modifiers == key.ModShift {
start, end := e.text.Selection() start, end := e.text.Selection()
// If they clicked closer to the end, then change the end to // If they clicked closer to the end, then change the end to
@@ -276,44 +298,17 @@ func (e *Selectable) processPointer(gtx layout.Context) {
func (e *Selectable) clickDragEvents(gtx layout.Context) []event.Event { func (e *Selectable) clickDragEvents(gtx layout.Context) []event.Event {
var combinedEvents []event.Event var combinedEvents []event.Event
for { for _, evt := range e.clicker.Update(gtx) {
evt, ok := e.clicker.Update(gtx.Source)
if !ok {
break
}
combinedEvents = append(combinedEvents, evt) combinedEvents = append(combinedEvents, evt)
} }
for { for _, evt := range e.dragger.Update(gtx.Metric, gtx, gesture.Both) {
evt, ok := e.dragger.Update(gtx.Metric, gtx.Source, gesture.Both)
if !ok {
break
}
combinedEvents = append(combinedEvents, evt) combinedEvents = append(combinedEvents, evt)
} }
return combinedEvents return combinedEvents
} }
func (e *Selectable) processKey(gtx layout.Context) { func (e *Selectable) processKey(gtx layout.Context) {
for { for _, ke := range gtx.Events(e) {
ke, ok := gtx.Event(
key.FocusFilter{Target: e},
key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift},
key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift},
key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShift},
key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShift},
key.Filter{Focus: e, Name: "C", Required: key.ModShortcut},
key.Filter{Focus: e, Name: "X", Required: key.ModShortcut},
key.Filter{Focus: e, Name: "A", Required: key.ModShortcut},
)
if !ok {
break
}
switch ke := ke.(type) { switch ke := ke.(type) {
case key.FocusEvent: case key.FocusEvent:
e.focused = ke.Focus e.focused = ke.Focus
@@ -342,7 +337,7 @@ func (e *Selectable) command(gtx layout.Context, k key.Event) {
case "C", "X": case "C", "X":
e.scratch = e.text.SelectedText(e.scratch) e.scratch = e.text.SelectedText(e.scratch)
if text := string(e.scratch); text != "" { 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)
} }
// Select all // Select all
case "A": case "A":
@@ -384,6 +379,14 @@ func (e *Selectable) command(gtx layout.Context, k key.Event) {
} }
} }
// Events returns available text events.
func (l *Selectable) Events() []EditorEvent {
events := l.events
l.events = nil
l.prevEvents = 0
return events
}
// Regions returns visible regions covering the rune range [start,end). // Regions returns visible regions covering the rune range [start,end).
func (l *Selectable) Regions(start, end int, regions []Region) []Region { func (l *Selectable) Regions(start, end int, regions []Region) []Region {
l.initialize() l.initialize()
+8 -14
View File
@@ -7,7 +7,6 @@ import (
"gioui.org/font" "gioui.org/font"
"gioui.org/font/gofont" "gioui.org/font/gofont"
"gioui.org/io/input"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
@@ -34,11 +33,9 @@ func TestSelectableZeroValue(t *testing.T) {
// Verify that an existing selection is dismissed when you press arrow keys. // Verify that an existing selection is dismissed when you press arrow keys.
func TestSelectableMove(t *testing.T) { func TestSelectableMove(t *testing.T) {
r := new(input.Router)
gtx := layout.Context{ gtx := layout.Context{
Ops: new(op.Ops), Ops: new(op.Ops),
Locale: english, Locale: english,
Source: r.Source(),
} }
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fnt := font.Font{} fnt := font.Font{}
@@ -47,20 +44,13 @@ func TestSelectableMove(t *testing.T) {
str := `0123456789` str := `0123456789`
// Layout once to populate e.lines and get focus. // Layout once to populate e.lines and get focus.
gtx.Queue = newQueue(key.FocusEvent{Focus: true})
s := new(Selectable) s := new(Selectable)
gtx.Execute(key.FocusCmd{Tag: s})
s.SetText(str) s.SetText(str)
// Set up selection so the Selectable filters for all 4 directional keys.
s.Layout(gtx, cache, font.Font{}, fontSize, op.CallOp{}, op.CallOp{}) s.Layout(gtx, cache, font.Font{}, fontSize, op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
s.SetCaret(3, 6)
s.Layout(gtx, cache, font.Font{}, fontSize, op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
s.Layout(gtx, cache, font.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 // Select 345
s.SetCaret(3, 6) s.SetCaret(3, 6)
if start, end := s.Selection(); start != 3 || end != 6 { if start, end := s.Selection(); start != 3 || end != 6 {
@@ -71,15 +61,19 @@ func TestSelectableMove(t *testing.T) {
} }
// Press the key // Press the key
r.Queue(key.Event{State: key.Press, Name: keyName}) gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName})
s.SetText(str) s.SetText(str)
s.Layout(gtx, cache, fnt, fontSize, op.CallOp{}, op.CallOp{}) s.Layout(gtx, cache, fnt, fontSize, op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
if expected, got := "", s.SelectedText(); expected != got { if expected, got := "", s.SelectedText(); expected != got {
t.Errorf("KeyName %s, expected %q, got %q", keyName, 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 TestSelectableConfigurations(t *testing.T) { func TestSelectableConfigurations(t *testing.T) {
+7 -8
View File
@@ -7,9 +7,10 @@ import (
"testing" "testing"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/input"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/widget" "gioui.org/widget"
@@ -17,13 +18,11 @@ import (
func TestBool(t *testing.T) { func TestBool(t *testing.T) {
var ( var (
r input.Router ops op.Ops
b widget.Bool r router.Router
b widget.Bool
) )
gtx := layout.Context{ gtx := layout.NewContext(&ops, system.FrameEvent{Queue: &r})
Ops: new(op.Ops),
Source: r.Source(),
}
layout := func() { layout := func() {
b.Layout(gtx, func(gtx layout.Context) layout.Dimensions { b.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.CheckBox.Add(gtx.Ops) semantic.CheckBox.Add(gtx.Ops)
@@ -45,7 +44,7 @@ func TestBool(t *testing.T) {
Position: f32.Pt(50, 50), Position: f32.Pt(50, 50),
}, },
) )
gtx.Reset() ops.Reset()
layout() layout()
r.Frame(gtx.Ops) r.Frame(gtx.Ops)
tree := r.AppendSemantics(nil) tree := r.AppendSemantics(nil)