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
102 changed files with 5281 additions and 5535 deletions
+1 -1
View File
@@ -65,8 +65,8 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
private final InputMethodManager imm;
private final float scrollXScale;
private final float scrollYScale;
private final AccessibilityManager accessManager;
private int keyboardHint;
private AccessibilityManager accessManager;
private long nhandle;
+9 -89
View File
@@ -3,16 +3,9 @@
package app
import (
"image"
"os"
"path/filepath"
"strings"
"time"
"gioui.org/io/input"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
)
// extraArgs contains extra arguments to append to
@@ -27,84 +20,23 @@ var extraArgs string
// On Android ID is the package property of AndroidManifest.xml,
// on iOS ID is the CFBundleIdentifier of the app Info.plist,
// on Wayland it is the toplevel app_id,
// on X11 it is the X11 XClassHint.
// on X11 it is the X11 XClassHint
//
// ID is set by the [gioui.org/cmd/gogio] tool or manually with the -X linker flag. For example,
// ID is set by the gogio tool or manually with the -X linker flag. For example,
//
// go build -ldflags="-X 'gioui.org/app.ID=org.gioui.example.Kitchen'" .
//
// Note that ID is treated as a constant, and that changing it at runtime
// is not supported. The default value of ID is filepath.Base(os.Args[0]).
// is not supported. Default value of ID is filepath.Base(os.Args[0]).
var ID = ""
// A FrameEvent requests a new frame in the form of a list of
// operations that describes the window content.
type FrameEvent struct {
// Now is the current animation. Use Now instead of time.Now to
// synchronize animation and to avoid the time.Now call overhead.
Now time.Time
// Metric converts device independent dp and sp to device pixels.
Metric unit.Metric
// Size is the dimensions of the window.
Size image.Point
// Insets represent the space occupied by system decorations and controls.
Insets Insets
// Frame completes the FrameEvent by drawing the graphical operations
// from ops into the window.
Frame func(frame *op.Ops)
// Source is the interface between the window and widgets.
Source input.Source
}
// ViewEvent provides handles to the underlying window objects for the
// current display protocol.
type ViewEvent interface {
implementsViewEvent()
ImplementsEvent()
}
// Insets is the space taken up by
// system decoration such as translucent
// system bars and software keyboards.
type Insets struct {
// Values are in pixels.
Top, Bottom, Left, Right unit.Dp
}
// NewContext is shorthand for
//
// layout.Context{
// Ops: ops,
// Now: e.Now,
// Source: e.Source,
// Metric: e.Metric,
// Constraints: layout.Exact(e.Size),
// }
//
// NewContext calls ops.Reset and adjusts ops for e.Insets.
func NewContext(ops *op.Ops, e FrameEvent) layout.Context {
ops.Reset()
size := e.Size
if e.Insets != (Insets{}) {
left := e.Metric.Dp(e.Insets.Left)
top := e.Metric.Dp(e.Insets.Top)
op.Offset(image.Point{
X: left,
Y: top,
}).Add(ops)
size.X -= left + e.Metric.Dp(e.Insets.Right)
size.Y -= top + e.Metric.Dp(e.Insets.Bottom)
func init() {
if extraArgs != "" {
args := strings.Split(extraArgs, "|")
os.Args = append(os.Args, args...)
}
return layout.Context{
Ops: ops,
Now: e.Now,
Source: e.Source,
Metric: e.Metric,
Constraints: layout.Exact(size),
if ID == "" {
ID = filepath.Base(os.Args[0])
}
}
@@ -131,15 +63,3 @@ func DataDir() (string, error) {
func Main() {
osMain()
}
func (FrameEvent) ImplementsEvent() {}
func init() {
if extraArgs != "" {
args := strings.Split(extraArgs, "|")
os.Args = append(os.Args, args...)
}
if ID == "" {
ID = filepath.Base(os.Args[0])
}
}
+18 -11
View File
@@ -8,20 +8,21 @@ See https://gioui.org for instructions to set up and run Gio programs.
# Windows
A Window is run by calling its Event method in a loop. The first time a
method on Window is called, a new GUI window is created and shown. On mobile
platforms or when Gio is embedded in another project, Window merely connects
with a previously created GUI window.
Create a new Window by calling NewWindow. On mobile platforms or when Gio
is embedded in another project, NewWindow merely connects with a previously
created window.
The most important event is [FrameEvent] that prompts an update of the window
contents.
A Window is run by calling NextEvent in a loop. The most important event is
FrameEvent that prompts an update of the window contents.
For example:
w := new(app.Window)
import "gioui.org/unit"
w := app.NewWindow()
for {
e := w.Event()
if e, ok := e.(app.FrameEvent); ok {
e := w.NextEvent()
if e, ok := e.(system.FrameEvent); ok {
ops.Reset()
// Add operations to ops.
...
@@ -31,7 +32,7 @@ For example:
}
A program must keep receiving events from the event channel until
[DestroyEvent] is received.
DestroyEvent is received.
# Main
@@ -50,12 +51,18 @@ For example, to display a blank but otherwise functional window:
go func() {
w := app.NewWindow()
for {
w.Event()
w.NextEvent()
}
}()
app.Main()
}
# Event queue
A FrameEvent's Queue method returns an event.Queue implementation that distributes
incoming events to the event handlers declared in the last frame.
See the gioui.org/io/event package for more information about event handlers.
# Permissions
The packages under gioui.org/app/permission should be imported
+1 -11
View File
@@ -69,17 +69,7 @@ func (c *wlContext) Refresh() error {
}
c.eglWin = eglWin
eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin)))
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
return err
}
if err := c.Context.MakeCurrent(); err != nil {
return err
}
defer c.Context.ReleaseCurrent()
// We're in charge of the frame callbacks, don't let eglSwapBuffers
// wait for callbacks that may never arrive.
c.Context.EnableVSync(false)
return nil
return c.Context.CreateSurface(eglSurf, width, height)
}
func (c *wlContext) Lock() error {
+1 -1
View File
@@ -46,8 +46,8 @@ func (c *x11Context) Refresh() error {
if err := c.Context.MakeCurrent(); err != nil {
return err
}
defer c.Context.ReleaseCurrent()
c.Context.EnableVSync(true)
c.Context.ReleaseCurrent()
return nil
}
-118
View File
@@ -1,118 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package app
import (
"unicode"
"unicode/utf16"
"gioui.org/io/input"
"gioui.org/io/key"
)
type editorState struct {
input.EditorState
compose key.Range
}
func (e *editorState) Replace(r key.Range, text string) {
if r.Start > r.End {
r.Start, r.End = r.End, r.Start
}
runes := []rune(text)
newEnd := r.Start + len(runes)
adjust := func(pos int) int {
switch {
case newEnd < pos && pos <= r.End:
return newEnd
case r.End < pos:
diff := newEnd - r.End
return pos + diff
}
return pos
}
e.Selection.Start = adjust(e.Selection.Start)
e.Selection.End = adjust(e.Selection.End)
if e.compose.Start != -1 {
e.compose.Start = adjust(e.compose.Start)
e.compose.End = adjust(e.compose.End)
}
s := e.Snippet
if r.End < s.Start || r.Start > s.End {
// Discard snippet if it doesn't overlap with replacement.
s = key.Snippet{
Range: key.Range{
Start: r.Start,
End: r.Start,
},
}
}
var newSnippet []rune
snippet := []rune(s.Text)
// Append first part of existing snippet.
if end := r.Start - s.Start; end > 0 {
newSnippet = append(newSnippet, snippet[:end]...)
}
// Append replacement.
newSnippet = append(newSnippet, runes...)
// Append last part of existing snippet.
if start := r.End; start < s.End {
newSnippet = append(newSnippet, snippet[start-s.Start:]...)
}
// Adjust snippet range to include replacement.
if r.Start < s.Start {
s.Start = r.Start
}
s.End = s.Start + len(newSnippet)
s.Text = string(newSnippet)
e.Snippet = s
}
// UTF16Index converts the given index in runes into an index in utf16 characters.
func (e *editorState) UTF16Index(runes int) int {
if runes == -1 {
return -1
}
if runes < e.Snippet.Start {
// Assume runes before sippet are one UTF-16 character each.
return runes
}
chars := e.Snippet.Start
runes -= e.Snippet.Start
for _, r := range e.Snippet.Text {
if runes == 0 {
break
}
runes--
chars++
if r1, _ := utf16.EncodeRune(r); r1 != unicode.ReplacementChar {
chars++
}
}
// Assume runes after snippets are one UTF-16 character each.
return chars + runes
}
// RunesIndex converts the given index in utf16 characters to an index in runes.
func (e *editorState) RunesIndex(chars int) int {
if chars == -1 {
return -1
}
if chars < e.Snippet.Start {
// Assume runes before offset are one UTF-16 character each.
return chars
}
runes := e.Snippet.Start
chars -= e.Snippet.Start
for _, r := range e.Snippet.Text {
if chars == 0 {
break
}
chars--
runes++
if r1, _ := utf16.EncodeRune(r); r1 != unicode.ReplacementChar {
chars--
}
}
// Assume runes after snippets are one UTF-16 character each.
return runes + chars
}
+4 -4
View File
@@ -11,8 +11,8 @@ import (
"gioui.org/font"
"gioui.org/font/gofont"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/router"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
@@ -31,10 +31,10 @@ func FuzzIME(f *testing.F) {
f.Fuzz(func(t *testing.T, cmds []byte) {
cache := text.NewShaper(text.WithCollection(gofont.Collection()))
e := new(widget.Editor)
e.Focus()
var r input.Router
gtx := layout.Context{Ops: new(op.Ops), Source: r.Source()}
gtx.Execute(key.FocusCmd{Tag: e})
var r router.Router
gtx := layout.Context{Ops: new(op.Ops), Queue: &r}
// Layout once to register focus.
e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
+7
View File
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package points standard output, standard error and the standard
// library package log to the platform logger.
package log
var appID = "gio"
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
package app
package log
/*
#cgo LDFLAGS: -llog
@@ -22,7 +22,7 @@ import (
// 1024 is the truncation limit from android/log.h, plus a \n.
const logLineLimit = 1024
var logTag = C.CString(ID)
var logTag = C.CString(appID)
func init() {
// Android's logcat already includes timestamps.
@@ -3,7 +3,7 @@
//go:build darwin && ios
// +build darwin,ios
package app
package log
/*
#cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
package app
package log
import (
"log"
+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))
}
func convertKeysym(s C.xkb_keysym_t) (key.Name, bool) {
func convertKeysym(s C.xkb_keysym_t) (string, bool) {
if 'a' <= s && s <= 'z' {
return key.Name(rune(s - 'a' + 'A')), true
return string(rune(s - 'a' + 'A')), true
}
if C.XKB_KEY_KP_0 <= s && s <= C.XKB_KEY_KP_9 {
return key.Name(rune(s - C.XKB_KEY_KP_0 + '0')), true
return string(rune(s - C.XKB_KEY_KP_0 + '0')), true
}
if ' ' < s && s <= '~' {
return key.Name(rune(s)), true
return string(rune(s)), true
}
var n key.Name
var n string
switch s {
case C.XKB_KEY_Escape:
n = key.NameEscape
+1 -2
View File
@@ -60,9 +60,8 @@ static void presentDrawable(CFTypeRef queueRef, CFTypeRef drawableRef) {
id<MTLDrawable> drawable = (__bridge id<MTLDrawable>)drawableRef;
id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef;
id<MTLCommandBuffer> cmdBuffer = [queue commandBuffer];
[cmdBuffer presentDrawable:drawable];
[cmdBuffer commit];
[cmdBuffer waitUntilScheduled];
[drawable present];
}
}
+1 -4
View File
@@ -21,10 +21,7 @@ Class gio_layerClass(void) {
static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
@autoreleasepool {
UIView *view = (__bridge UIView *)viewRef;
CAMetalLayer *l = (CAMetalLayer *)view.layer;
l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = YES;
return CFBridgingRetain(l);
return CFBridgingRetain(view.layer);
}
}
+1 -5
View File
@@ -14,11 +14,7 @@ package app
CALayer *gio_layerFactory(void) {
@autoreleasepool {
CAMetalLayer *l = [CAMetalLayer layer];
l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable;
l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = YES;
return l;
return [CAMetalLayer layer];
}
}
+21 -171
View File
@@ -7,9 +7,7 @@ import (
"image"
"image/color"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/op"
"gioui.org/gpu"
"gioui.org/io/pointer"
@@ -45,8 +43,6 @@ type Config struct {
CustomRenderer bool
// Decorated reports whether window decorations are provided automatically.
Decorated bool
// Focused reports whether has the keyboard focus.
Focused bool
// decoHeight is the height of the fallback decoration for platforms such
// as Wayland that may need fallback client-side decorations.
decoHeight unit.Dp
@@ -135,30 +131,8 @@ func (o Orientation) String() string {
return ""
}
// eventLoop implements the functionality required for drivers where
// window event loops must run on a separate thread.
type eventLoop struct {
win *callbacks
// wakeup is the callback to wake up the event loop.
wakeup func()
// driverFuncs is a channel of functions to run the next
// time the window loop waits for events.
driverFuncs chan func()
// invalidates is notified when an invalidate is requested by the client.
invalidates chan struct{}
// immediateInvalidates is an optimistic invalidates that doesn't require a wakeup.
immediateInvalidates chan struct{}
// events is where the platform backend delivers events bound for the
// user program.
events chan event.Event
frames chan *op.Ops
frameAck chan struct{}
// delivering avoids re-entrant event delivery.
delivering bool
}
type frameEvent struct {
FrameEvent
system.FrameEvent
Sync bool
}
@@ -173,19 +147,9 @@ type context interface {
Unlock()
}
// basicDriver is the subset of [driver] that may be called even after
// a window is destroyed.
type basicDriver interface {
// Event blocks until an even is available and returns it.
Event() event.Event
// Invalidate requests a FrameEvent.
Invalidate()
}
// driver is the interface for the platform implementation
// Driver is the interface for the platform implementation
// of a window.
type driver interface {
basicDriver
// SetAnimating sets the animation flag. When the window is animating,
// FrameEvents are delivered as fast as the display can handle them.
SetAnimating(anim bool)
@@ -196,29 +160,23 @@ type driver interface {
// ReadClipboard requests the clipboard content.
ReadClipboard()
// WriteClipboard requests a clipboard write.
WriteClipboard(mime string, s []byte)
WriteClipboard(s string)
// Configure the window.
Configure([]Option)
// SetCursor updates the current cursor to name.
SetCursor(cursor pointer.Cursor)
// Wakeup wakes up the event loop and sends a WakeupEvent.
// Wakeup()
Wakeup()
// Perform actions on the window.
Perform(system.Action)
// EditorStateChanged notifies the driver that the editor state changed.
EditorStateChanged(old, new editorState)
// Run a function on the window thread.
Run(f func())
// Frame receives a frame.
Frame(frame *op.Ops)
// ProcessEvent processes an event.
ProcessEvent(e event.Event)
}
type windowRendezvous struct {
in chan windowAndConfig
out chan windowAndConfig
windows chan struct{}
in chan windowAndConfig
out chan windowAndConfig
errs chan error
}
type windowAndConfig struct {
@@ -228,137 +186,32 @@ type windowAndConfig struct {
func newWindowRendezvous() *windowRendezvous {
wr := &windowRendezvous{
in: make(chan windowAndConfig),
out: make(chan windowAndConfig),
windows: make(chan struct{}),
in: make(chan windowAndConfig),
out: make(chan windowAndConfig),
errs: make(chan error),
}
go func() {
in := wr.in
var window windowAndConfig
var main windowAndConfig
var out chan windowAndConfig
for {
select {
case w := <-in:
window = w
case w := <-wr.in:
var err error
if main.window != nil {
err = errors.New("multiple windows are not supported")
}
wr.errs <- err
main = w
out = wr.out
case out <- window:
case out <- main:
}
}
}()
return wr
}
func newEventLoop(w *callbacks, wakeup func()) *eventLoop {
return &eventLoop{
win: w,
wakeup: wakeup,
events: make(chan event.Event),
invalidates: make(chan struct{}, 1),
immediateInvalidates: make(chan struct{}),
frames: make(chan *op.Ops),
frameAck: make(chan struct{}),
driverFuncs: make(chan func(), 1),
}
}
// Frame receives a frame and waits for its processing. It is called by
// the client goroutine.
func (e *eventLoop) Frame(frame *op.Ops) {
e.frames <- frame
<-e.frameAck
}
// Event returns the next available event. It is called by the client
// goroutine.
func (e *eventLoop) Event() event.Event {
for {
evt := <-e.events
// Receiving a flushEvent indicates to the platform backend that
// all previous events have been processed by the user program.
if _, ok := evt.(flushEvent); ok {
continue
}
return evt
}
}
// Invalidate requests invalidation of the window. It is called by the client
// goroutine.
func (e *eventLoop) Invalidate() {
select {
case e.immediateInvalidates <- struct{}{}:
// The event loop was waiting, no need for a wakeup.
case e.invalidates <- struct{}{}:
// The event loop is sleeping, wake it up.
e.wakeup()
default:
// A redraw is pending.
}
}
// Run f in the window loop thread. It is called by the client goroutine.
func (e *eventLoop) Run(f func()) {
e.driverFuncs <- f
e.wakeup()
}
// FlushEvents delivers pending events to the client.
func (e *eventLoop) FlushEvents() {
if e.delivering {
return
}
e.delivering = true
defer func() { e.delivering = false }()
for {
evt, ok := e.win.nextEvent()
if !ok {
break
}
e.deliverEvent(evt)
}
}
func (e *eventLoop) deliverEvent(evt event.Event) {
var frames <-chan *op.Ops
for {
select {
case f := <-e.driverFuncs:
f()
case frame := <-frames:
// The client called FrameEvent.Frame.
frames = nil
e.win.ProcessFrame(frame, e.frameAck)
case e.events <- evt:
switch evt.(type) {
case flushEvent, DestroyEvent:
// DestroyEvents are not flushed.
return
case FrameEvent:
frames = e.frames
}
evt = theFlushEvent
case <-e.invalidates:
e.win.Invalidate()
case <-e.immediateInvalidates:
e.win.Invalidate()
}
}
}
func (e *eventLoop) Wakeup() {
for {
select {
case f := <-e.driverFuncs:
f()
case <-e.invalidates:
e.win.Invalidate()
case <-e.immediateInvalidates:
e.win.Invalidate()
default:
return
}
}
}
func (wakeupEvent) ImplementsEvent() {}
func (ConfigEvent) ImplementsEvent() {}
func walkActions(actions system.Action, do func(system.Action)) {
for a := system.Action(1); actions != 0; a <<= 1 {
@@ -368,6 +221,3 @@ func walkActions(actions system.Action, do func(system.Action)) {
}
}
}
func (wakeupEvent) ImplementsEvent() {}
func (ConfigEvent) ImplementsEvent() {}
+94 -134
View File
@@ -123,36 +123,31 @@ import (
"fmt"
"image"
"image/color"
"io"
"math"
"os"
"path/filepath"
"runtime"
"runtime/cgo"
"runtime/debug"
"strings"
"sync"
"time"
"unicode/utf16"
"unsafe"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/clipboard"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit"
)
type window struct {
callbacks *callbacks
loop *eventLoop
view C.jobject
handle cgo.Handle
@@ -161,19 +156,18 @@ type window struct {
fontScale float32
insets pixelInsets
visible bool
stage system.Stage
started bool
animating bool
win *C.ANativeWindow
config Config
inputHint key.InputHint
win *C.ANativeWindow
config Config
semantic struct {
hoverID input.SemanticID
rootID input.SemanticID
focusID input.SemanticID
diffs []input.SemanticID
hoverID router.SemanticID
rootID router.SemanticID
focusID router.SemanticID
diffs []router.SemanticID
}
}
@@ -205,9 +199,9 @@ type pixelInsets struct {
top, bottom, left, right int
}
// AndroidViewEvent is sent whenever the Window's underlying Android view
// ViewEvent is sent whenever the Window's underlying Android view
// changes.
type AndroidViewEvent struct {
type ViewEvent struct {
// View is a JNI global reference to the android.view.View
// instance backing the Window. The reference is valid until
// the next ViewEvent is received.
@@ -491,30 +485,24 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
})
view = C.jni_NewGlobalRef(env, view)
wopts := <-mainWindow.out
var cnf Config
w, ok := windows[wopts.window]
if !ok {
w = &window{
callbacks: wopts.window,
}
w.loop = newEventLoop(w.callbacks, w.wakeup)
w.callbacks.SetDriver(w)
cnf.apply(unit.Metric{}, wopts.options)
windows[wopts.window] = w
} else {
cnf = w.config
}
mainWindow.windows <- struct{}{}
if w.view != 0 {
w.detach(env)
}
w.view = view
w.visible = false
w.handle = cgo.NewHandle(w)
w.callbacks.SetDriver(w)
w.loadConfig(env, class)
w.setConfig(env, cnf)
w.SetInputHint(w.inputHint)
w.processEvent(AndroidViewEvent{View: uintptr(view)})
w.Configure(wopts.options)
w.SetInputHint(key.HintAny)
w.setStage(system.StagePaused)
w.callbacks.Event(ViewEvent{View: uintptr(view)})
return C.jlong(w.handle)
}
@@ -528,7 +516,7 @@ func Java_org_gioui_GioView_onDestroyView(env *C.JNIEnv, class C.jclass, handle
func Java_org_gioui_GioView_onStopView(env *C.JNIEnv, class C.jclass, handle C.jlong) {
w := cgo.Handle(handle).Value().(*window)
w.started = false
w.visible = false
w.setStage(system.StagePaused)
}
//export Java_org_gioui_GioView_onStartView
@@ -544,7 +532,7 @@ func Java_org_gioui_GioView_onStartView(env *C.JNIEnv, class C.jclass, handle C.
func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, handle C.jlong) {
w := cgo.Handle(handle).Value().(*window)
w.win = nil
w.visible = false
w.setStage(system.StagePaused)
}
//export Java_org_gioui_GioView_onSurfaceChanged
@@ -566,7 +554,9 @@ func Java_org_gioui_GioView_onLowMemory(env *C.JNIEnv, class C.jclass) {
func Java_org_gioui_GioView_onConfigurationChanged(env *C.JNIEnv, class C.jclass, view C.jlong) {
w := cgo.Handle(view).Value().(*window)
w.loadConfig(env, class)
w.draw(env, true)
if w.stage >= system.StageInactive {
w.draw(env, true)
}
}
//export Java_org_gioui_GioView_onFrameCallback
@@ -575,7 +565,10 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
if !exist {
return
}
if w.visible && w.animating {
if w.stage < system.StageInactive {
return
}
if w.animating {
w.draw(env, false)
callVoidMethod(env, w.view, gioView.postFrameCallback)
}
@@ -584,7 +577,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
//export Java_org_gioui_GioView_onBack
func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong) C.jboolean {
w := cgo.Handle(view).Value().(*window)
if w.processEvent(key.Event{Name: key.NameBack}) {
if w.callbacks.Event(key.Event{Name: key.NameBack}) {
return C.JNI_TRUE
}
return C.JNI_FALSE
@@ -593,8 +586,7 @@ func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong)
//export Java_org_gioui_GioView_onFocusChange
func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) {
w := cgo.Handle(view).Value().(*window)
w.config.Focused = focus == C.JNI_TRUE
w.processEvent(ConfigEvent{Config: w.config})
w.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE})
}
//export Java_org_gioui_GioView_onWindowInsets
@@ -606,7 +598,9 @@ func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C
left: int(left),
right: int(right),
}
w.draw(env, true)
if w.stage >= system.StageInactive {
w.draw(env, true)
}
}
//export Java_org_gioui_GioView_initializeAccessibilityNodeInfo
@@ -667,35 +661,7 @@ func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view
}
}
func (w *window) ProcessEvent(e event.Event) {
w.processEvent(e)
}
func (w *window) processEvent(e event.Event) bool {
if !w.callbacks.ProcessEvent(e) {
return false
}
w.loop.FlushEvents()
return true
}
func (w *window) Event() event.Event {
return w.loop.Event()
}
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem input.SemanticNode, off image.Point, info C.jobject) error {
func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNode, off image.Point, info C.jobject) error {
for _, ch := range sem.Children {
err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID)))
if err != nil {
@@ -738,7 +704,7 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem input.SemanticNode
panic(err)
}
}
if d.Gestures&input.ClickGesture != 0 {
if d.Gestures&router.ClickGesture != 0 {
addAction(ACTION_CLICK)
}
clsName := android.strings.androidViewView
@@ -783,23 +749,25 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem input.SemanticNode
return nil
}
func (w *window) virtualIDFor(id input.SemanticID) C.jint {
func (w *window) virtualIDFor(id router.SemanticID) C.jint {
// TODO: Android virtual IDs are 32-bit Java integers, but childID is a int64.
if id == w.semantic.rootID {
return HOST_VIEW_ID
}
return C.jint(id)
}
func (w *window) semIDFor(virtID C.jint) input.SemanticID {
func (w *window) semIDFor(virtID C.jint) router.SemanticID {
if virtID == HOST_VIEW_ID {
return w.semantic.rootID
}
return input.SemanticID(virtID)
return router.SemanticID(virtID)
}
func (w *window) detach(env *C.JNIEnv) {
callVoidMethod(env, w.view, gioView.unregister)
w.processEvent(AndroidViewEvent{})
w.callbacks.Event(ViewEvent{})
w.callbacks.SetDriver(nil)
w.handle.Delete()
C.jni_DeleteGlobalRef(env, w.view)
w.view = 0
@@ -810,10 +778,18 @@ func (w *window) setVisible(env *C.JNIEnv) {
if width == 0 || height == 0 {
return
}
w.visible = true
w.setStage(system.StageRunning)
w.draw(env, true)
}
func (w *window) setStage(stage system.Stage) {
if stage == w.stage {
return
}
w.stage = stage
w.callbacks.Event(system.StageEvent{stage})
}
func (w *window) setVisual(visID int) error {
if C.ANativeWindow_setBuffersGeometry(w.win, 0, 0, C.int32_t(visID)) != 0 {
return errors.New("ANativeWindow_setBuffersGeometry failed")
@@ -850,13 +826,10 @@ func (w *window) SetAnimating(anim bool) {
}
func (w *window) draw(env *C.JNIEnv, sync bool) {
if !w.visible {
return
}
size := image.Pt(int(C.ANativeWindow_getWidth(w.win)), int(C.ANativeWindow_getHeight(w.win)))
if size != w.config.Size {
w.config.Size = size
w.processEvent(ConfigEvent{Config: w.config})
w.callbacks.Event(ConfigEvent{Config: w.config})
}
if size.X == 0 || size.Y == 0 {
return
@@ -864,14 +837,14 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
const inchPrDp = 1.0 / 160
ppdp := float32(w.dpi) * inchPrDp
dppp := unit.Dp(1.0 / ppdp)
insets := Insets{
insets := system.Insets{
Top: unit.Dp(w.insets.top) * dppp,
Bottom: unit.Dp(w.insets.bottom) * dppp,
Left: unit.Dp(w.insets.left) * dppp,
Right: unit.Dp(w.insets.right) * dppp,
}
w.processEvent(frameEvent{
FrameEvent: FrameEvent{
w.callbacks.Event(frameEvent{
FrameEvent: system.FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Insets: insets,
@@ -925,8 +898,8 @@ func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) {
f(env)
}
func convertKeyCode(code C.jint) (key.Name, bool) {
var n key.Name
func convertKeyCode(code C.jint) (string, bool) {
var n string
switch code {
case C.AKEYCODE_FORWARD_DEL:
n = key.NameDeleteForward
@@ -970,7 +943,7 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
if pressed == C.JNI_TRUE {
state = key.Press
}
w.processEvent(key.Event{Name: n, State: state})
w.callbacks.Event(key.Event{Name: n, State: state})
}
if pressed == C.JNI_TRUE && r != 0 && r != '\n' { // Checking for "\n" to prevent duplication with key.NameEnter (gio#224).
w.callbacks.EditorInsert(string(rune(r)))
@@ -1020,7 +993,7 @@ func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C
default:
return
}
w.processEvent(pointer.Event{
w.callbacks.Event(pointer.Event{
Kind: kind,
Source: src,
Buttons: btns,
@@ -1172,8 +1145,6 @@ func (w *window) ShowTextInput(show bool) {
}
func (w *window) SetInputHint(mode key.InputHint) {
w.inputHint = mode
// Constants defined at https://developer.android.com/reference/android/text/InputType.
const (
TYPE_NULL = 0
@@ -1320,14 +1291,14 @@ func findClass(env *C.JNIEnv, name string) C.jclass {
func osMain() {
}
func newWindow(window *callbacks, options []Option) {
func newWindow(window *callbacks, options []Option) error {
mainWindow.in <- windowAndConfig{window, options}
<-mainWindow.windows
return <-mainWindow.errs
}
func (w *window) WriteClipboard(mime string, s []byte) {
func (w *window) WriteClipboard(s string) {
runInJVM(javaVM(), func(env *C.JNIEnv) {
jstr := javaString(env, string(s))
jstr := javaString(env, s)
callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard,
jvalue(android.appCtx), jvalue(jstr))
})
@@ -1341,54 +1312,45 @@ func (w *window) ReadClipboard() {
return
}
content := goString(env, C.jstring(c))
w.processEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
w.callbacks.Event(clipboard.Event{Text: content})
})
}
func (w *window) Configure(options []Option) {
cnf := w.config
cnf.apply(unit.Metric{}, options)
runInJVM(javaVM(), func(env *C.JNIEnv) {
w.setConfig(env, cnf)
})
}
prev := w.config
cnf := w.config
cnf.apply(unit.Metric{}, options)
// Decorations are never disabled.
cnf.Decorated = true
func (w *window) setConfig(env *C.JNIEnv, cnf Config) {
prev := w.config
// Decorations are never disabled.
cnf.Decorated = true
if prev.Orientation != cnf.Orientation {
w.config.Orientation = cnf.Orientation
setOrientation(env, w.view, cnf.Orientation)
}
if prev.NavigationColor != cnf.NavigationColor {
w.config.NavigationColor = cnf.NavigationColor
setNavigationColor(env, w.view, cnf.NavigationColor)
}
if prev.StatusColor != cnf.StatusColor {
w.config.StatusColor = cnf.StatusColor
setStatusColor(env, w.view, cnf.StatusColor)
}
if prev.Mode != cnf.Mode {
switch cnf.Mode {
case Fullscreen:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE)
w.config.Mode = Fullscreen
case Windowed:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE)
w.config.Mode = Windowed
if prev.Orientation != cnf.Orientation {
w.config.Orientation = cnf.Orientation
setOrientation(env, w.view, cnf.Orientation)
}
}
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.processEvent(ConfigEvent{Config: w.config})
if prev.NavigationColor != cnf.NavigationColor {
w.config.NavigationColor = cnf.NavigationColor
setNavigationColor(env, w.view, cnf.NavigationColor)
}
if prev.StatusColor != cnf.StatusColor {
w.config.StatusColor = cnf.StatusColor
setStatusColor(env, w.view, cnf.StatusColor)
}
if prev.Mode != cnf.Mode {
switch cnf.Mode {
case Fullscreen:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE)
w.config.Mode = Fullscreen
case Windowed:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE)
w.config.Mode = Windowed
}
}
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.callbacks.Event(ConfigEvent{Config: w.config})
})
}
func (w *window) Perform(system.Action) {}
@@ -1399,10 +1361,9 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
})
}
func (w *window) wakeup() {
func (w *window) Wakeup() {
runOnMain(func(env *C.JNIEnv) {
w.loop.Wakeup()
w.loop.FlushEvents()
w.callbacks.Event(wakeupEvent{})
})
}
@@ -1493,5 +1454,4 @@ func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {
}
}
func (AndroidViewEvent) implementsViewEvent() {}
func (AndroidViewEvent) ImplementsEvent() {}
func (_ ViewEvent) ImplementsEvent() {}
+2 -3
View File
@@ -260,9 +260,8 @@ func windowSetCursor(from, to pointer.Cursor) pointer.Cursor {
return to
}
func (w *window) wakeup() {
func (w *window) Wakeup() {
runOnMain(func() {
w.loop.Wakeup()
w.loop.FlushEvents()
w.w.Event(wakeupEvent{})
})
}
+64 -111
View File
@@ -12,8 +12,6 @@ package app
#include <UIKit/UIKit.h>
#include <stdint.h>
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
struct drawParams {
CGFloat dpi, sdpi;
CGFloat width, height;
@@ -21,7 +19,6 @@ struct drawParams {
};
static void writeClipboard(unichar *chars, NSUInteger length) {
#if !TARGET_OS_TV
@autoreleasepool {
NSString *s = [NSString string];
if (length > 0) {
@@ -30,18 +27,13 @@ static void writeClipboard(unichar *chars, NSUInteger length) {
UIPasteboard *p = UIPasteboard.generalPasteboard;
p.string = s;
}
#endif
}
static CFTypeRef readClipboard(void) {
#if !TARGET_OS_TV
@autoreleasepool {
UIPasteboard *p = UIPasteboard.generalPasteboard;
return (__bridge_retained CFTypeRef)p.string;
}
#else
return nil;
#endif
}
static void showTextInput(CFTypeRef viewRef) {
@@ -80,26 +72,21 @@ import "C"
import (
"image"
"io"
"runtime"
"runtime/cgo"
"runtime/debug"
"strings"
"time"
"unicode/utf16"
"unsafe"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/clipboard"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
)
type UIKitViewEvent struct {
type ViewEvent struct {
// ViewController is a CFTypeRef for the UIViewController backing a Window.
ViewController uintptr
}
@@ -108,17 +95,18 @@ type window struct {
view C.CFTypeRef
w *callbacks
displayLink *displayLink
loop *eventLoop
hidden bool
cursor pointer.Cursor
config Config
visible bool
cursor pointer.Cursor
config Config
pointerMap []C.CFTypeRef
}
var mainWindow = newWindowRendezvous()
var views = make(map[C.CFTypeRef]*window)
func init() {
// Darwin requires UI operations happen on the main thread only.
runtime.LockOSThread()
@@ -126,59 +114,55 @@ func init() {
//export onCreate
func onCreate(view, controller C.CFTypeRef) {
wopts := <-mainWindow.out
w := &window{
view: view,
w: wopts.window,
}
w.loop = newEventLoop(w.w, w.wakeup)
w.w.SetDriver(w)
mainWindow.windows <- struct{}{}
dl, err := newDisplayLink(func() {
w.draw(false)
})
if err != nil {
w.w.ProcessEvent(DestroyEvent{Err: err})
return
panic(err)
}
w.displayLink = dl
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
wopts := <-mainWindow.out
w.w = wopts.window
w.w.SetDriver(w)
views[view] = w
w.Configure(wopts.options)
w.ProcessEvent(UIKitViewEvent{ViewController: uintptr(controller)})
}
func viewFor(h C.uintptr_t) *window {
return cgo.Handle(h).Value().(*window)
w.w.Event(system.StageEvent{Stage: system.StagePaused})
w.w.Event(ViewEvent{ViewController: uintptr(controller)})
}
//export gio_onDraw
func gio_onDraw(h C.uintptr_t) {
w := viewFor(h)
func gio_onDraw(view C.CFTypeRef) {
w := views[view]
w.draw(true)
}
func (w *window) draw(sync bool) {
if w.hidden {
return
}
params := C.viewDrawParams(w.view)
if params.width == 0 || params.height == 0 {
return
}
wasVisible := w.visible
w.visible = true
if !wasVisible {
w.w.Event(system.StageEvent{Stage: system.StageRunning})
}
const inchPrDp = 1.0 / 163
m := unit.Metric{
PxPerDp: float32(params.dpi) * inchPrDp,
PxPerSp: float32(params.sdpi) * inchPrDp,
}
dppp := unit.Dp(1. / m.PxPerDp)
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
Now: time.Now(),
Size: image.Point{
X: int(params.width + .5),
Y: int(params.height + .5),
},
Insets: Insets{
Insets: system.Insets{
Top: unit.Dp(params.top) * dppp,
Bottom: unit.Dp(params.bottom) * dppp,
Left: unit.Dp(params.left) * dppp,
@@ -191,34 +175,26 @@ func (w *window) draw(sync bool) {
}
//export onStop
func onStop(h C.uintptr_t) {
w := viewFor(h)
w.hidden = true
}
//export onStart
func onStart(h C.uintptr_t) {
w := viewFor(h)
w.hidden = false
w.draw(true)
func onStop(view C.CFTypeRef) {
w := views[view]
w.visible = false
w.w.Event(system.StageEvent{Stage: system.StagePaused})
}
//export onDestroy
func onDestroy(h C.uintptr_t) {
w := viewFor(h)
w.ProcessEvent(UIKitViewEvent{})
w.ProcessEvent(DestroyEvent{})
func onDestroy(view C.CFTypeRef) {
w := views[view]
delete(views, view)
w.w.Event(ViewEvent{})
w.w.Event(system.DestroyEvent{})
w.displayLink.Close()
w.displayLink = nil
cgo.Handle(h).Delete()
w.view = 0
}
//export onFocus
func onFocus(h C.uintptr_t, focus int) {
w := viewFor(h)
w.config.Focused = focus != 0
w.ProcessEvent(ConfigEvent{Config: w.config})
func onFocus(view C.CFTypeRef, focus int) {
w := views[view]
w.w.Event(key.FocusEvent{Focus: focus != 0})
}
//export onLowMemory
@@ -228,38 +204,38 @@ func onLowMemory() {
}
//export onUpArrow
func onUpArrow(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameUpArrow)
func onUpArrow(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameUpArrow)
}
//export onDownArrow
func onDownArrow(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameDownArrow)
func onDownArrow(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameDownArrow)
}
//export onLeftArrow
func onLeftArrow(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameLeftArrow)
func onLeftArrow(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameLeftArrow)
}
//export onRightArrow
func onRightArrow(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameRightArrow)
func onRightArrow(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameRightArrow)
}
//export onDeleteBackward
func onDeleteBackward(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameDeleteBackward)
func onDeleteBackward(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameDeleteBackward)
}
//export onText
func onText(h C.uintptr_t, str C.CFTypeRef) {
w := viewFor(h)
func onText(view, str C.CFTypeRef) {
w := views[view]
w.w.EditorInsert(nsstringToString(str))
}
//export onTouch
func onTouch(h C.uintptr_t, last C.int, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) {
func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) {
var kind pointer.Kind
switch phase {
case C.UITouchPhaseBegan:
@@ -273,10 +249,10 @@ func onTouch(h C.uintptr_t, last C.int, touchRef C.CFTypeRef, phase C.NSInteger,
default:
return
}
w := viewFor(h)
w := views[view]
t := time.Duration(float64(ti) * float64(time.Second))
p := f32.Point{X: float32(x), Y: float32(y)}
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: kind,
Source: pointer.Touch,
PointerID: w.lookupTouch(last != 0, touchRef),
@@ -289,16 +265,11 @@ func (w *window) ReadClipboard() {
cstr := C.readClipboard()
defer C.CFRelease(cstr)
content := nsstringToString(cstr)
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
w.w.Event(clipboard.Event{Text: content})
}
func (w *window) WriteClipboard(mime string, s []byte) {
u16 := utf16.Encode([]rune(string(s)))
func (w *window) WriteClipboard(s string) {
u16 := utf16.Encode([]rune(s))
var chars *C.unichar
if len(u16) > 0 {
chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
@@ -309,7 +280,7 @@ func (w *window) WriteClipboard(mime string, s []byte) {
func (w *window) Configure([]Option) {
// Decorations are never disabled.
w.config.Decorated = true
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
func (w *window) EditorStateChanged(old, new editorState) {}
@@ -317,6 +288,10 @@ func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) Perform(system.Action) {}
func (w *window) SetAnimating(anim bool) {
v := w.view
if v == 0 {
return
}
if anim {
w.displayLink.Start()
} else {
@@ -328,8 +303,8 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
w.cursor = windowSetCursor(w.cursor, cursor)
}
func (w *window) onKeyCommand(name key.Name) {
w.ProcessEvent(key.Event{
func (w *window) onKeyCommand(name string) {
w.w.Event(key.Event{
Name: name,
})
}
@@ -368,30 +343,9 @@ func (w *window) ShowTextInput(show bool) {
func (w *window) SetInputHint(_ key.InputHint) {}
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
w.loop.FlushEvents()
}
func (w *window) Event() event.Event {
return w.loop.Event()
}
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
func newWindow(win *callbacks, options []Option) {
func newWindow(win *callbacks, options []Option) error {
mainWindow.in <- windowAndConfig{win, options}
<-mainWindow.windows
return <-mainWindow.errs
}
func osMain() {
@@ -402,5 +356,4 @@ func gio_runMain() {
runMain()
}
func (UIKitViewEvent) implementsViewEvent() {}
func (UIKitViewEvent) ImplementsEvent() {}
func (_ ViewEvent) ImplementsEvent() {}
+22 -31
View File
@@ -11,7 +11,6 @@
__attribute__ ((visibility ("hidden"))) Class gio_layerClass(void);
@interface GioView: UIView <UIKeyInput>
@property uintptr_t handle;
@end
@implementation GioViewController
@@ -26,13 +25,12 @@ CGFloat _keyboardHeight;
self.view.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
UIView *drawView = [[GioView alloc] initWithFrame:zeroFrame];
[self.view addSubview: drawView];
#if !TARGET_OS_TV
#ifndef TARGET_OS_TV
drawView.multipleTouchEnabled = YES;
#endif
drawView.preservesSuperviewLayoutMargins = YES;
drawView.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
onCreate((__bridge CFTypeRef)drawView, (__bridge CFTypeRef)self);
#if !TARGET_OS_TV
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillChange:)
name:UIKeyboardWillShowNotification
@@ -45,7 +43,6 @@ CGFloat _keyboardHeight;
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
#endif
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(applicationDidEnterBackground:)
name: UIApplicationDidEnterBackgroundNotification
@@ -57,33 +54,33 @@ CGFloat _keyboardHeight;
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
GioView *view = (GioView *)self.view.subviews[0];
if (view != nil) {
onStart(view.handle);
UIView *drawView = self.view.subviews[0];
if (drawView != nil) {
gio_onDraw((__bridge CFTypeRef)drawView);
}
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
GioView *view = (GioView *)self.view.subviews[0];
if (view != nil) {
onStop(view.handle);
UIView *drawView = self.view.subviews[0];
if (drawView != nil) {
onStop((__bridge CFTypeRef)drawView);
}
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
GioView *view = (GioView *)self.view.subviews[0];
onDestroy(view.handle);
CFTypeRef viewRef = (__bridge CFTypeRef)self.view.subviews[0];
onDestroy(viewRef);
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
GioView *view = (GioView *)self.view.subviews[0];
UIView *view = self.view.subviews[0];
CGRect frame = self.view.bounds;
// Adjust view bounds to make room for the keyboard.
frame.size.height -= _keyboardHeight;
view.frame = frame;
gio_onDraw(view.handle);
gio_onDraw((__bridge CFTypeRef)view);
}
- (void)didReceiveMemoryWarning {
@@ -91,7 +88,6 @@ CGFloat _keyboardHeight;
[super didReceiveMemoryWarning];
}
#if !TARGET_OS_TV
- (void)keyboardWillChange:(NSNotification *)note {
NSDictionary *userInfo = note.userInfo;
CGRect f = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
@@ -103,13 +99,13 @@ CGFloat _keyboardHeight;
_keyboardHeight = 0.0;
[self.view setNeedsLayout];
}
#endif
@end
static void handleTouches(int last, GioView *view, NSSet<UITouch *> *touches, UIEvent *event) {
static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIEvent *event) {
CGFloat scale = view.contentScaleFactor;
NSUInteger i = 0;
NSUInteger n = [touches count];
CFTypeRef viewRef = (__bridge CFTypeRef)view;
for (UITouch *touch in touches) {
CFTypeRef touchRef = (__bridge CFTypeRef)touch;
i++;
@@ -120,7 +116,7 @@ static void handleTouches(int last, GioView *view, NSSet<UITouch *> *touches, UI
CGPoint loc = [coalescedTouch locationInView:view];
j++;
int lastTouch = last && i == n && j == m;
onTouch(view.handle, lastTouch, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]);
onTouch(lastTouch, viewRef, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]);
}
}
}
@@ -155,13 +151,13 @@ NSArray<UIKeyCommand *> *_keyCommands;
- (void)onWindowDidBecomeKey:(NSNotification *)note {
if (self.isFirstResponder) {
onFocus(self.handle, YES);
onFocus((__bridge CFTypeRef)self, YES);
}
}
- (void)onWindowDidResignKey:(NSNotification *)note {
if (self.isFirstResponder) {
onFocus(self.handle, NO);
onFocus((__bridge CFTypeRef)self, NO);
}
}
@@ -182,7 +178,7 @@ NSArray<UIKeyCommand *> *_keyCommands;
}
- (void)insertText:(NSString *)text {
onText(self.handle, (__bridge CFTypeRef)text);
onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)text);
}
- (BOOL)canBecomeFirstResponder {
@@ -194,23 +190,23 @@ NSArray<UIKeyCommand *> *_keyCommands;
}
- (void)deleteBackward {
onDeleteBackward(self.handle);
onDeleteBackward((__bridge CFTypeRef)self);
}
- (void)onUpArrow {
onUpArrow(self.handle);
onUpArrow((__bridge CFTypeRef)self);
}
- (void)onDownArrow {
onDownArrow(self.handle);
onDownArrow((__bridge CFTypeRef)self);
}
- (void)onLeftArrow {
onLeftArrow(self.handle);
onLeftArrow((__bridge CFTypeRef)self);
}
- (void)onRightArrow {
onRightArrow(self.handle);
onRightArrow((__bridge CFTypeRef)self);
}
- (NSArray<UIKeyCommand *> *)keyCommands {
@@ -275,8 +271,3 @@ void gio_showCursor() {
void gio_setCursor(NSUInteger curID) {
// Not supported.
}
void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
GioView *v = (__bridge GioView *)viewRef;
v.handle = handle;
}
+96 -96
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"image"
"image/color"
"io"
"strings"
"syscall/js"
"time"
@@ -14,18 +13,16 @@ import (
"unicode/utf8"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/clipboard"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit"
)
type JSViewEvent struct {
type ViewEvent struct {
Element js.Value
}
@@ -56,6 +53,9 @@ type window struct {
composing bool
requestFocus bool
chanAnimation chan struct{}
chanRedraw chan struct{}
config Config
inset f32.Point
scale float32
@@ -68,7 +68,7 @@ type window struct {
contextStatus contextStatus
}
func newWindow(win *callbacks, options []Option) {
func newWindow(win *callbacks, options []Option) error {
doc := js.Global().Get("document")
cont := getContainer(doc)
cnv := createCanvas(doc)
@@ -83,9 +83,7 @@ func newWindow(win *callbacks, options []Option) {
head: doc.Get("head"),
clipboard: js.Global().Get("navigator").Get("clipboard"),
wakeups: make(chan struct{}, 1),
w: win,
}
w.w.SetDriver(w)
w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
w.browserHistory = w.window.Get("history")
w.visualViewport = w.window.Get("visualViewport")
@@ -95,28 +93,42 @@ func newWindow(win *callbacks, options []Option) {
if screen := w.window.Get("screen"); screen.Truthy() {
w.screenOrientation = screen.Get("orientation")
}
w.chanAnimation = make(chan struct{}, 1)
w.chanRedraw = make(chan struct{}, 1)
w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
w.draw(false)
w.chanAnimation <- struct{}{}
return nil
})
w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
content := args[0].String()
w.processEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
go win.Event(clipboard.Event{Text: content})
return nil
})
w.addEventListeners()
w.addHistory()
w.w = win
w.Configure(options)
w.blur()
w.processEvent(JSViewEvent{Element: cont})
w.resize()
w.draw(true)
go func() {
defer w.cleanup()
w.w.SetDriver(w)
w.Configure(options)
w.blur()
w.w.Event(ViewEvent{Element: cont})
w.w.Event(system.StageEvent{Stage: system.StageRunning})
w.resize()
w.draw(true)
for {
select {
case <-w.wakeups:
w.w.Event(wakeupEvent{})
case <-w.chanAnimation:
w.animCallback()
case <-w.chanRedraw:
w.draw(true)
}
}
}()
return nil
}
func getContainer(doc js.Value) js.Value {
@@ -176,12 +188,12 @@ func (w *window) addEventListeners() {
w.cnv.Set("width", 0)
w.cnv.Set("height", 0)
w.resize()
w.draw(true)
w.requestRedraw()
return nil
})
w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} {
w.resize()
w.draw(true)
w.requestRedraw()
return nil
})
w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} {
@@ -189,11 +201,22 @@ func (w *window) addEventListeners() {
return nil
})
w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} {
if w.processEvent(key.Event{Name: key.NameBack}) {
if w.w.Event(key.Event{Name: key.NameBack}) {
return w.browserHistory.Call("forward")
}
return w.browserHistory.Call("back")
})
w.addEventListener(w.document, "visibilitychange", func(this js.Value, args []js.Value) interface{} {
ev := system.StageEvent{}
switch w.document.Get("visibilityState").String() {
case "hidden", "prerender", "unloaded":
ev.Stage = system.StagePaused
default:
ev.Stage = system.StageRunning
}
w.w.Event(ev)
return nil
})
w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} {
w.pointerEvent(pointer.Move, 0, 0, args[0])
return nil
@@ -251,20 +274,18 @@ func (w *window) addEventListeners() {
w.touches[i] = js.Null()
}
w.touches = w.touches[:0]
w.processEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: pointer.Cancel,
Source: pointer.Touch,
})
return nil
})
w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} {
w.config.Focused = true
w.processEvent(ConfigEvent{Config: w.config})
w.w.Event(key.FocusEvent{Focus: true})
return nil
})
w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
w.config.Focused = false
w.processEvent(ConfigEvent{Config: w.config})
w.w.Event(key.FocusEvent{Focus: false})
w.blur()
return nil
})
@@ -353,50 +374,10 @@ func (w *window) keyEvent(e js.Value, ks key.State) {
Modifiers: modifiersFor(e),
State: ks,
}
w.processEvent(cmd)
w.w.Event(cmd)
}
}
func (w *window) ProcessEvent(e event.Event) {
w.processEvent(e)
}
func (w *window) processEvent(e event.Event) bool {
if !w.w.ProcessEvent(e) {
return false
}
select {
case w.wakeups <- struct{}{}:
default:
}
return true
}
func (w *window) Event() event.Event {
for {
evt, ok := w.w.nextEvent()
if ok {
if _, destroy := evt.(DestroyEvent); destroy {
w.cleanup()
}
return evt
}
<-w.wakeups
}
}
func (w *window) Invalidate() {
w.w.Invalidate()
}
func (w *window) Run(f func()) {
f()
}
func (w *window) Frame(frame *op.Ops) {
w.w.ProcessFrame(frame, nil)
}
// modifiersFor returns the modifier set for a DOM MouseEvent or
// KeyEvent.
func modifiersFor(e js.Value) key.Modifiers {
@@ -444,7 +425,7 @@ func (w *window) touchEvent(kind pointer.Kind, e js.Value) {
X: float32(x) * scale,
Y: float32(y) * scale,
}
w.processEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: kind,
Source: pointer.Touch,
Position: pos,
@@ -494,7 +475,7 @@ func (w *window) pointerEvent(kind pointer.Kind, dx, dy float32, e js.Value) {
if jbtns&4 != 0 {
btns |= pointer.ButtonTertiary
}
w.processEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: kind,
Source: pointer.Mouse,
Buttons: btns,
@@ -521,6 +502,17 @@ func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.F
return jsf
}
func (w *window) animCallback() {
anim := w.animating
w.animRequested = anim
if anim {
w.requestAnimationFrame.Invoke(w.redraw)
}
if anim {
w.draw(false)
}
}
func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) SetAnimating(anim bool) {
@@ -541,14 +533,14 @@ func (w *window) ReadClipboard() {
w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback)
}
func (w *window) WriteClipboard(mime string, s []byte) {
func (w *window) WriteClipboard(s string) {
if w.clipboard.IsUndefined() {
return
}
if w.clipboard.Get("writeText").IsUndefined() {
return
}
w.clipboard.Call("writeText", string(s))
w.clipboard.Call("writeText", s)
}
func (w *window) Configure(options []Option) {
@@ -576,7 +568,7 @@ func (w *window) Configure(options []Option) {
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.processEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
func (w *window) Perform(system.Action) {}
@@ -615,14 +607,23 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
style.Set("cursor", webCursor[cursor])
}
func (w *window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
}
func (w *window) ShowTextInput(show bool) {
// Run in a goroutine to avoid a deadlock if the
// focus change result in an event.
if show {
w.focus()
} else {
w.blur()
}
go func() {
if show {
w.focus()
} else {
w.blur()
}
}()
}
func (w *window) SetInputHint(mode key.InputHint) {
@@ -639,7 +640,7 @@ func (w *window) resize() {
}
if size != w.config.Size {
w.config.Size = size
w.processEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() {
@@ -659,20 +660,13 @@ func (w *window) draw(sync bool) {
if w.contextStatus == contextStatusLost {
return
}
anim := w.animating
w.animRequested = anim
if anim {
w.requestAnimationFrame.Invoke(w.redraw)
} else if !sync {
return
}
size, insets, metric := w.getConfig()
if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 {
return
}
w.processEvent(frameEvent{
FrameEvent: FrameEvent{
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
Now: time.Now(),
Size: size,
Insets: insets,
@@ -682,10 +676,10 @@ func (w *window) draw(sync bool) {
})
}
func (w *window) getConfig() (image.Point, Insets, unit.Metric) {
func (w *window) getConfig() (image.Point, system.Insets, unit.Metric) {
invscale := unit.Dp(1. / w.scale)
return image.Pt(w.config.Size.X, w.config.Size.Y),
Insets{
system.Insets{
Bottom: unit.Dp(w.inset.Y) * invscale,
Right: unit.Dp(w.inset.X) * invscale,
}, unit.Metric{
@@ -741,12 +735,19 @@ func (w *window) navigationColor(c color.NRGBA) {
theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B}))
}
func (w *window) requestRedraw() {
select {
case w.chanRedraw <- struct{}{}:
default:
}
}
func osMain() {
select {}
}
func translateKey(k string) (key.Name, bool) {
var n key.Name
func translateKey(k string) (string, bool) {
var n string
switch k {
case "ArrowUp":
@@ -813,12 +814,11 @@ func translateKey(k string) (key.Name, bool) {
r, s := utf8.DecodeRuneInString(k)
// If there is exactly one printable character, return that.
if s == len(k) && unicode.IsPrint(r) {
return key.Name(strings.ToUpper(k)), true
return strings.ToUpper(k), true
}
return "", false
}
return n, true
}
func (JSViewEvent) implementsViewEvent() {}
func (JSViewEvent) ImplementsEvent() {}
func (_ ViewEvent) ImplementsEvent() {}
+211 -278
View File
@@ -8,21 +8,16 @@ package app
import (
"errors"
"image"
"io"
"runtime"
"runtime/cgo"
"strings"
"time"
"unicode"
"unicode/utf8"
"gioui.org/internal/f32"
"gioui.org/io/event"
"gioui.org/io/clipboard"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
_ "gioui.org/internal/cocoainit"
@@ -42,7 +37,6 @@ import (
__attribute__ ((visibility ("hidden"))) void gio_main(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
static void writeClipboard(CFTypeRef str) {
@autoreleasepool {
@@ -62,212 +56,148 @@ static CFTypeRef readClipboard(void) {
}
static CGFloat viewHeight(CFTypeRef viewRef) {
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
return [view bounds].size.height;
}
NSView *view = (__bridge NSView *)viewRef;
return [view bounds].size.height;
}
static CGFloat viewWidth(CFTypeRef viewRef) {
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
return [view bounds].size.width;
}
NSView *view = (__bridge NSView *)viewRef;
return [view bounds].size.width;
}
static CGFloat getScreenBackingScale(void) {
@autoreleasepool {
return [NSScreen.mainScreen backingScaleFactor];
}
return [NSScreen.mainScreen backingScaleFactor];
}
static CGFloat getViewBackingScale(CFTypeRef viewRef) {
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
return [view.window backingScaleFactor];
}
NSView *view = (__bridge NSView *)viewRef;
return [view.window backingScaleFactor];
}
static void setNeedsDisplay(CFTypeRef viewRef) {
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
[view setNeedsDisplay:YES];
}
NSView *view = (__bridge NSView *)viewRef;
[view setNeedsDisplay:YES];
}
static NSPoint cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
return [window cascadeTopLeftFromPoint:topLeft];
}
NSWindow *window = (__bridge NSWindow *)windowRef;
return [window cascadeTopLeftFromPoint:topLeft];
}
static void makeKeyAndOrderFront(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window makeKeyAndOrderFront:nil];
}
NSWindow *window = (__bridge NSWindow *)windowRef;
[window makeKeyAndOrderFront:nil];
}
static void toggleFullScreen(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window toggleFullScreen:nil];
}
NSWindow *window = (__bridge NSWindow *)windowRef;
[window toggleFullScreen:nil];
}
static NSWindowStyleMask getWindowStyleMask(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
return [window styleMask];
}
NSWindow *window = (__bridge NSWindow *)windowRef;
return [window styleMask];
}
static void setWindowStyleMask(CFTypeRef windowRef, NSWindowStyleMask mask) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
window.styleMask = mask;
}
NSWindow *window = (__bridge NSWindow *)windowRef;
window.styleMask = mask;
}
static void setWindowTitleVisibility(CFTypeRef windowRef, NSWindowTitleVisibility state) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
window.titleVisibility = state;
}
NSWindow *window = (__bridge NSWindow *)windowRef;
window.titleVisibility = state;
}
static void setWindowTitlebarAppearsTransparent(CFTypeRef windowRef, int transparent) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
window.titlebarAppearsTransparent = (BOOL)transparent;
}
NSWindow *window = (__bridge NSWindow *)windowRef;
window.titlebarAppearsTransparent = (BOOL)transparent;
}
static void setWindowStandardButtonHidden(CFTypeRef windowRef, NSWindowButton btn, int hide) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window standardWindowButton:btn].hidden = (BOOL)hide;
}
NSWindow *window = (__bridge NSWindow *)windowRef;
[window standardWindowButton:btn].hidden = (BOOL)hide;
}
static void performWindowDragWithEvent(CFTypeRef windowRef, CFTypeRef evt) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window performWindowDragWithEvent:(__bridge NSEvent*)evt];
}
NSWindow *window = (__bridge NSWindow *)windowRef;
[window performWindowDragWithEvent:(__bridge NSEvent*)evt];
}
static void closeWindow(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
[window performClose:nil];
}
NSWindow* window = (__bridge NSWindow *)windowRef;
[window performClose:nil];
}
static void setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
NSSize size = NSMakeSize(width, height);
[window setContentSize:size];
}
NSWindow* window = (__bridge NSWindow *)windowRef;
NSSize size = NSMakeSize(width, height);
[window setContentSize:size];
}
static void setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
window.contentMinSize = NSMakeSize(width, height);
}
NSWindow* window = (__bridge NSWindow *)windowRef;
window.contentMinSize = NSMakeSize(width, height);
}
static void setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
window.contentMaxSize = NSMakeSize(width, height);
}
NSWindow* window = (__bridge NSWindow *)windowRef;
window.contentMaxSize = NSMakeSize(width, height);
}
static void setScreenFrame(CFTypeRef windowRef, CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
NSRect r = NSMakeRect(x, y, w, h);
[window setFrame:r display:YES];
}
}
static void resetLayerFrame(CFTypeRef viewRef) {
@autoreleasepool {
NSView* view = (__bridge NSView *)viewRef;
NSRect r = view.frame;
view.layer.frame = r;
}
NSWindow* window = (__bridge NSWindow *)windowRef;
NSRect r = NSMakeRect(x, y, w, h);
[window setFrame:r display:YES];
}
static void hideWindow(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
[window miniaturize:window];
}
NSWindow* window = (__bridge NSWindow *)windowRef;
[window miniaturize:window];
}
static void unhideWindow(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
[window deminiaturize:window];
}
NSWindow* window = (__bridge NSWindow *)windowRef;
[window deminiaturize:window];
}
static NSRect getScreenFrame(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
return [[window screen] frame];
}
NSWindow* window = (__bridge NSWindow *)windowRef;
return [[window screen] frame];
}
static void setTitle(CFTypeRef windowRef, CFTypeRef titleRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
window.title = (__bridge NSString *)titleRef;
}
NSWindow *window = (__bridge NSWindow *)windowRef;
window.title = (__bridge NSString *)titleRef;
}
static int isWindowZoomed(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
return window.zoomed ? 1 : 0;
}
NSWindow *window = (__bridge NSWindow *)windowRef;
return window.zoomed ? 1 : 0;
}
static void zoomWindow(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window zoom:nil];
}
NSWindow *window = (__bridge NSWindow *)windowRef;
[window zoom:nil];
}
static CFTypeRef layerForView(CFTypeRef viewRef) {
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
return (__bridge CFTypeRef)view.layer;
}
NSView *view = (__bridge NSView *)viewRef;
return (__bridge CFTypeRef)view.layer;
}
static CFTypeRef windowForView(CFTypeRef viewRef) {
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
return (__bridge CFTypeRef)view.window;
}
NSView *view = (__bridge NSView *)viewRef;
return (__bridge CFTypeRef)view.window;
}
static void raiseWindow(CFTypeRef windowRef) {
@autoreleasepool {
NSRunningApplication *currentApp = [NSRunningApplication currentApplication];
if (![currentApp isActive]) {
[currentApp activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
}
NSWindow* window = (__bridge NSWindow *)windowRef;
[window makeKeyAndOrderFront:nil];
NSRunningApplication *currentApp = [NSRunningApplication currentApplication];
if (![currentApp isActive]) {
[currentApp activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
}
NSWindow* window = (__bridge NSWindow *)windowRef;
[window makeKeyAndOrderFront:nil];
}
static CFTypeRef createInputContext(CFTypeRef clientRef) {
@@ -279,23 +209,23 @@ static CFTypeRef createInputContext(CFTypeRef clientRef) {
}
static void discardMarkedText(CFTypeRef viewRef) {
@autoreleasepool {
@autoreleasepool {
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
if (view == [ctx client]) {
[ctx discardMarkedText];
}
}
}
}
static void invalidateCharacterCoordinates(CFTypeRef viewRef) {
@autoreleasepool {
@autoreleasepool {
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
if (view == [ctx client]) {
[ctx invalidateCharacterCoordinates];
}
}
}
}
*/
import "C"
@@ -305,9 +235,9 @@ func init() {
runtime.LockOSThread()
}
// AppKitViewEvent notifies the client of changes to the window AppKit handles.
// The handles are retained until another AppKitViewEvent is sent.
type AppKitViewEvent struct {
// ViewEvent notified the client of changes to the window AppKit handles.
// The handles are retained until another ViewEvent is sent.
type ViewEvent struct {
// View is a CFTypeRef for the NSView for the window.
View uintptr
// Layer is a CFTypeRef of the CALayer of View.
@@ -317,20 +247,21 @@ type AppKitViewEvent struct {
type window struct {
view C.CFTypeRef
w *callbacks
anim bool
visible bool
stage system.Stage
displayLink *displayLink
// redraw is a single entry channel for making sure only one
// display link redraw request is in flight.
redraw chan struct{}
cursor pointer.Cursor
pointerBtns pointer.Buttons
loop *eventLoop
scale float32
config Config
}
// viewMap is the mapping from Cocoa NSViews to Go windows.
var viewMap = make(map[C.CFTypeRef]*window)
// launched is closed when applicationDidFinishLaunching is called.
var launched = make(chan struct{})
@@ -338,8 +269,30 @@ var launched = make(chan struct{})
// cascadeTopLeftFromPoint.
var nextTopLeft C.NSPoint
func windowFor(h C.uintptr_t) *window {
return cgo.Handle(h).Value().(*window)
// mustView is like lookupView, except that it panics
// if the view isn't mapped.
func mustView(view C.CFTypeRef) *window {
w, ok := lookupView(view)
if !ok {
panic("no window for view")
}
return w
}
func lookupView(view C.CFTypeRef) (*window, bool) {
w, exists := viewMap[view]
if !exists {
return nil, false
}
return w, true
}
func deleteView(view C.CFTypeRef) {
delete(viewMap, view)
}
func insertView(view C.CFTypeRef, w *window) {
viewMap[view] = w
}
func (w *window) contextView() C.CFTypeRef {
@@ -352,16 +305,11 @@ func (w *window) ReadClipboard() {
defer C.CFRelease(cstr)
}
content := nsstringToString(cstr)
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
w.w.Event(clipboard.Event{Text: content})
}
func (w *window) WriteClipboard(mime string, s []byte) {
cstr := stringToNSString(string(s))
func (w *window) WriteClipboard(s string) {
cstr := stringToNSString(s)
defer C.CFRelease(cstr)
C.writeClipboard(cstr)
}
@@ -464,11 +412,8 @@ func (w *window) Configure(options []Option) {
C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
// When toggling the titlebar, the layer doesn't update its frame
// until the next resize. Force it.
C.resetLayerFrame(w.view)
}
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
func (w *window) setTitle(prev, cnf Config) {
@@ -519,8 +464,7 @@ func (w *window) ShowTextInput(show bool) {}
func (w *window) SetInputHint(_ key.InputHint) {}
func (w *window) SetAnimating(anim bool) {
w.anim = anim
if w.anim && w.visible {
if anim {
w.displayLink.Start()
} else {
w.displayLink.Stop()
@@ -537,18 +481,26 @@ func (w *window) runOnMain(f func()) {
})
}
func (w *window) setStage(stage system.Stage) {
if stage == w.stage {
return
}
w.stage = stage
w.w.Event(system.StageEvent{Stage: stage})
}
//export gio_onKeys
func gio_onKeys(h C.uintptr_t, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) {
func gio_onKeys(view, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) {
str := nsstringToString(cstr)
kmods := convertMods(mods)
ks := key.Release
if keyDown {
ks = key.Press
}
w := windowFor(h)
w := mustView(view)
for _, k := range str {
if n, ok := convertKey(k); ok {
w.ProcessEvent(key.Event{
w.w.Event(key.Event{
Name: n,
Modifiers: kmods,
State: ks,
@@ -558,15 +510,15 @@ func gio_onKeys(h C.uintptr_t, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger,
}
//export gio_onText
func gio_onText(h C.uintptr_t, cstr C.CFTypeRef) {
func gio_onText(view, cstr C.CFTypeRef) {
str := nsstringToString(cstr)
w := windowFor(h)
w := mustView(view)
w.w.EditorInsert(str)
}
//export gio_onMouse
func gio_onMouse(h C.uintptr_t, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) {
w := windowFor(h)
func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) {
w := mustView(view)
t := time.Duration(float64(ti)*float64(time.Second) + .5)
xf, yf := float32(x)*w.scale, float32(y)*w.scale
dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale
@@ -603,7 +555,7 @@ func gio_onMouse(h C.uintptr_t, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x
default:
panic("invalid direction")
}
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: typ,
Source: pointer.Mouse,
Time: t,
@@ -615,29 +567,35 @@ func gio_onMouse(h C.uintptr_t, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x
}
//export gio_onDraw
func gio_onDraw(h C.uintptr_t) {
w := windowFor(h)
func gio_onDraw(view C.CFTypeRef) {
w := mustView(view)
w.draw()
}
//export gio_onFocus
func gio_onFocus(h C.uintptr_t, focus C.int) {
w := windowFor(h)
func gio_onFocus(view C.CFTypeRef, focus C.int) {
w := mustView(view)
w.w.Event(key.FocusEvent{Focus: focus == 1})
if w.stage >= system.StageInactive {
if focus == 0 {
w.setStage(system.StageInactive)
} else {
w.setStage(system.StageRunning)
}
}
w.SetCursor(w.cursor)
w.config.Focused = focus == 1
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onChangeScreen
func gio_onChangeScreen(h C.uintptr_t, did uint64) {
w := windowFor(h)
func gio_onChangeScreen(view C.CFTypeRef, did uint64) {
w := mustView(view)
w.displayLink.SetDisplayID(did)
C.setNeedsDisplay(w.view)
}
//export gio_hasMarkedText
func gio_hasMarkedText(h C.uintptr_t) C.int {
w := windowFor(h)
func gio_hasMarkedText(view C.CFTypeRef) C.int {
w := mustView(view)
state := w.w.EditorState()
if state.compose.Start != -1 {
return 1
@@ -646,8 +604,8 @@ func gio_hasMarkedText(h C.uintptr_t) C.int {
}
//export gio_markedRange
func gio_markedRange(h C.uintptr_t) C.NSRange {
w := windowFor(h)
func gio_markedRange(view C.CFTypeRef) C.NSRange {
w := mustView(view)
state := w.w.EditorState()
rng := state.compose
start, end := rng.Start, rng.End
@@ -662,8 +620,8 @@ func gio_markedRange(h C.uintptr_t) C.NSRange {
}
//export gio_selectedRange
func gio_selectedRange(h C.uintptr_t) C.NSRange {
w := windowFor(h)
func gio_selectedRange(view C.CFTypeRef) C.NSRange {
w := mustView(view)
state := w.w.EditorState()
rng := state.Selection
start, end := rng.Start, rng.End
@@ -678,14 +636,14 @@ func gio_selectedRange(h C.uintptr_t) C.NSRange {
}
//export gio_unmarkText
func gio_unmarkText(h C.uintptr_t) {
w := windowFor(h)
func gio_unmarkText(view C.CFTypeRef) {
w := mustView(view)
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
}
//export gio_setMarkedText
func gio_setMarkedText(h C.uintptr_t, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) {
w := windowFor(h)
func gio_setMarkedText(view, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) {
w := mustView(view)
str := nsstringToString(cstr)
state := w.w.EditorState()
rng := state.compose
@@ -724,8 +682,8 @@ func gio_setMarkedText(h C.uintptr_t, cstr C.CFTypeRef, selRange C.NSRange, repl
}
//export gio_substringForProposedRange
func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef {
w := windowFor(h)
func gio_substringForProposedRange(view C.CFTypeRef, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef {
w := mustView(view)
state := w.w.EditorState()
start, end := state.Snippet.Start, state.Snippet.End
if start > end {
@@ -745,8 +703,8 @@ func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRan
}
//export gio_insertText
func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) {
w := windowFor(h)
func gio_insertText(view, cstr C.CFTypeRef, crng C.NSRange) {
w := mustView(view)
state := w.w.EditorState()
rng := state.compose
if rng.Start == -1 {
@@ -770,13 +728,13 @@ func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) {
}
//export gio_characterIndexForPoint
func gio_characterIndexForPoint(h C.uintptr_t, p C.NSPoint) C.NSUInteger {
func gio_characterIndexForPoint(view C.CFTypeRef, p C.NSPoint) C.NSUInteger {
return C.NSNotFound
}
//export gio_firstRectForCharacterRange
func gio_firstRectForCharacterRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.NSRect {
w := windowFor(h)
func gio_firstRectForCharacterRange(view C.CFTypeRef, crng C.NSRange, actual C.NSRangePointer) C.NSRect {
w := mustView(view)
state := w.w.EditorState()
sel := state.Selection
u16start := state.UTF16Index(sel.Start)
@@ -803,10 +761,6 @@ func (w *window) draw() {
case <-w.redraw:
default:
}
w.visible = true
if w.anim {
w.SetAnimating(w.anim)
}
w.scale = float32(C.getViewBackingScale(w.view))
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
sz := image.Point{
@@ -815,14 +769,15 @@ func (w *window) draw() {
}
if sz != w.config.Size {
w.config.Size = sz
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
if sz.X == 0 || sz.Y == 0 {
return
}
cfg := configFor(w.scale)
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
w.setStage(system.StageRunning)
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: cfg,
@@ -831,27 +786,6 @@ func (w *window) draw() {
})
}
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
w.loop.FlushEvents()
}
func (w *window) Event() event.Event {
return w.loop.Event()
}
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
func configFor(scale float32) unit.Metric {
return unit.Metric{
PxPerDp: scale,
@@ -859,54 +793,56 @@ func configFor(scale float32) unit.Metric {
}
}
//export gio_onAttached
func gio_onAttached(h C.uintptr_t, attached C.int) {
w := windowFor(h)
if attached != 0 {
layer := C.layerForView(w.view)
w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
} else {
w.ProcessEvent(AppKitViewEvent{})
w.visible = false
w.SetAnimating(w.anim)
}
}
//export gio_onDestroy
func gio_onDestroy(h C.uintptr_t) {
w := windowFor(h)
w.ProcessEvent(DestroyEvent{})
//export gio_onClose
func gio_onClose(view C.CFTypeRef) {
w := mustView(view)
w.w.Event(ViewEvent{})
w.w.Event(system.DestroyEvent{})
w.displayLink.Close()
w.displayLink = nil
cgo.Handle(h).Delete()
deleteView(view)
C.CFRelease(w.view)
w.view = 0
}
//export gio_onHide
func gio_onHide(h C.uintptr_t) {
w := windowFor(h)
w.visible = false
w.SetAnimating(w.anim)
func gio_onHide(view C.CFTypeRef) {
w := mustView(view)
w.setStage(system.StagePaused)
}
//export gio_onShow
func gio_onShow(h C.uintptr_t) {
w := windowFor(h)
w.draw()
func gio_onShow(view C.CFTypeRef) {
w := mustView(view)
w.setStage(system.StageRunning)
}
//export gio_onFullscreen
func gio_onFullscreen(h C.uintptr_t) {
w := windowFor(h)
func gio_onFullscreen(view C.CFTypeRef) {
w := mustView(view)
w.config.Mode = Fullscreen
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
//export gio_onWindowed
func gio_onWindowed(h C.uintptr_t) {
w := windowFor(h)
func gio_onWindowed(view C.CFTypeRef) {
w := mustView(view)
w.config.Mode = Windowed
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
//export gio_onAppHide
func gio_onAppHide() {
for _, w := range viewMap {
w.setStage(system.StagePaused)
}
}
//export gio_onAppShow
func gio_onAppShow() {
for _, w := range viewMap {
w.setStage(system.StageRunning)
}
}
//export gio_onFinishLaunching
@@ -914,25 +850,20 @@ func gio_onFinishLaunching() {
close(launched)
}
func newWindow(win *callbacks, options []Option) {
func newWindow(win *callbacks, options []Option) error {
<-launched
res := make(chan struct{})
errch := make(chan error)
runOnMain(func() {
w := &window{
redraw: make(chan struct{}, 1),
w: win,
}
w.loop = newEventLoop(w.w, w.wakeup)
win.SetDriver(w)
res <- struct{}{}
if err := w.init(); err != nil {
w.ProcessEvent(DestroyEvent{Err: err})
w, err := newOSWindow()
if err != nil {
errch <- err
return
}
errch <- nil
w.w = win
window := C.gio_createWindow(w.view, 0, 0, 0, 0, 0, 0)
// Release our reference now that the NSWindow has it.
C.CFRelease(w.view)
w.updateWindowMode()
win.SetDriver(w)
w.Configure(options)
if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
// cascadeTopLeftFromPoint treats (0, 0) as a no-op,
@@ -942,17 +873,23 @@ func newWindow(win *callbacks, options []Option) {
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
// makeKeyAndOrderFront assumes ownership of our window reference.
C.makeKeyAndOrderFront(window)
layer := C.layerForView(w.view)
w.w.Event(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
})
<-res
return <-errch
}
func (w *window) init() error {
func newOSWindow() (*window, error) {
view := C.gio_createView()
if view == 0 {
return errors.New("newOSWindow: failed to create view")
return nil, errors.New("newOSWindows: failed to create view")
}
scale := float32(C.getViewBackingScale(view))
w.scale = scale
w := &window{
view: view,
scale: scale,
redraw: make(chan struct{}, 1),
}
dl, err := newDisplayLink(func() {
select {
case w.redraw <- struct{}{}:
@@ -960,27 +897,24 @@ func (w *window) init() error {
return
}
w.runOnMain(func() {
if w.visible {
C.setNeedsDisplay(w.view)
}
C.setNeedsDisplay(w.view)
})
})
w.displayLink = dl
if err != nil {
C.CFRelease(view)
return err
return nil, err
}
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
w.view = view
return nil
insertView(view, w)
return w, nil
}
func osMain() {
C.gio_main()
}
func convertKey(k rune) (key.Name, bool) {
var n key.Name
func convertKey(k rune) (string, bool) {
var n string
switch k {
case 0x1b:
n = key.NameEscape
@@ -1041,7 +975,7 @@ func convertKey(k rune) (key.Name, bool) {
if !unicode.IsPrint(k) {
return "", false
}
n = key.Name(k)
n = string(k)
}
return n, true
}
@@ -1063,5 +997,4 @@ func convertMods(mods C.NSUInteger) key.Modifiers {
return kmods
}
func (AppKitViewEvent) implementsViewEvent() {}
func (AppKitViewEvent) ImplementsEvent() {}
func (_ ViewEvent) ImplementsEvent() {}
+50 -72
View File
@@ -14,50 +14,40 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
@interface GioWindowDelegate : NSObject<NSWindowDelegate>
@end
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@property uintptr_t handle;
@end
@implementation GioWindowDelegate
- (void)windowWillMiniaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onHide(view.handle);
gio_onHide((__bridge CFTypeRef)window.contentView);
}
- (void)windowDidDeminiaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onShow(view.handle);
gio_onShow((__bridge CFTypeRef)window.contentView);
}
- (void)windowWillEnterFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onFullscreen(view.handle);
gio_onFullscreen((__bridge CFTypeRef)window.contentView);
}
- (void)windowWillExitFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onWindowed(view.handle);
gio_onWindowed((__bridge CFTypeRef)window.contentView);
}
- (void)windowDidChangeScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue];
GioView *view = (GioView *)window.contentView;
gio_onChangeScreen(view.handle, dispID);
CFTypeRef view = (__bridge CFTypeRef)window.contentView;
gio_onChangeScreen(view, dispID);
}
- (void)windowDidBecomeKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onFocus(view.handle, 1);
gio_onFocus((__bridge CFTypeRef)window.contentView, 1);
}
- (void)windowDidResignKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onFocus(view.handle, 0);
gio_onFocus((__bridge CFTypeRef)window.contentView, 0);
}
@end
static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil];
if (!event.hasPreciseScrollingDeltas) {
// dx and dy are in rows and columns.
@@ -66,9 +56,12 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
}
// Origin is in the lower left corner. Convert to upper left.
CGFloat height = view.bounds.size.height;
gio_onMouse(view.handle, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
gio_onMouse((__bridge CFTypeRef)view, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
}
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@end
@implementation GioView
- (void)setFrameSize:(NSSize)newSize {
[super setFrameSize:newSize];
@@ -77,11 +70,11 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
// drawRect is called when OpenGL is used, displayLayer otherwise.
// Don't know why.
- (void)drawRect:(NSRect)r {
gio_onDraw(self.handle);
gio_onDraw((__bridge CFTypeRef)self);
}
- (void)displayLayer:(CALayer *)layer {
layer.contentsScale = self.window.backingScaleFactor;
gio_onDraw(self.handle);
gio_onDraw((__bridge CFTypeRef)self);
}
- (CALayer *)makeBackingLayer {
CALayer *layer = gio_layerFactory();
@@ -89,7 +82,9 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
return layer;
}
- (void)viewDidMoveToWindow {
gio_onAttached(self.handle, self.window != nil ? 1 : 0);
if (self.window == nil) {
gio_onClose((__bridge CFTypeRef)self);
}
}
- (void)mouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
@@ -129,14 +124,14 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
- (void)keyDown:(NSEvent *)event {
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
gio_onKeys((__bridge CFTypeRef)self, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
}
- (void)keyUp:(NSEvent *)event {
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
gio_onKeys((__bridge CFTypeRef)self, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
}
- (void)insertText:(id)string {
gio_onText(self.handle, (__bridge CFTypeRef)string);
gio_onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)string);
}
- (void)doCommandBySelector:(SEL)sel {
// Don't pass commands up the responder chain.
@@ -144,17 +139,17 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
}
- (BOOL)hasMarkedText {
int res = gio_hasMarkedText(self.handle);
int res = gio_hasMarkedText((__bridge CFTypeRef)self);
return res ? YES : NO;
}
- (NSRange)markedRange {
return gio_markedRange(self.handle);
return gio_markedRange((__bridge CFTypeRef)self);
}
- (NSRange)selectedRange {
return gio_selectedRange(self.handle);
return gio_selectedRange((__bridge CFTypeRef)self);
}
- (void)unmarkText {
gio_unmarkText(self.handle);
gio_unmarkText((__bridge CFTypeRef)self);
}
- (void)setMarkedText:(id)string
selectedRange:(NSRange)selRange
@@ -166,14 +161,14 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
} else {
str = string;
}
gio_setMarkedText(self.handle, (__bridge CFTypeRef)str, selRange, replaceRange);
gio_setMarkedText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, selRange, replaceRange);
}
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
return nil;
}
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range
actualRange:(NSRangePointer)actualRange {
NSString *str = CFBridgingRelease(gio_substringForProposedRange(self.handle, range, actualRange));
NSString *str = CFBridgingRelease(gio_substringForProposedRange((__bridge CFTypeRef)self, range, actualRange));
return [[NSAttributedString alloc] initWithString:str attributes:nil];
}
- (void)insertText:(id)string
@@ -185,26 +180,17 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
} else {
str = string;
}
gio_insertText(self.handle, (__bridge CFTypeRef)str, replaceRange);
gio_insertText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, replaceRange);
}
- (NSUInteger)characterIndexForPoint:(NSPoint)p {
return gio_characterIndexForPoint(self.handle, p);
return gio_characterIndexForPoint((__bridge CFTypeRef)self, p);
}
- (NSRect)firstRectForCharacterRange:(NSRange)rng
actualRange:(NSRangePointer)actual {
NSRect r = gio_firstRectForCharacterRange(self.handle, rng, actual);
NSRect r = gio_firstRectForCharacterRange((__bridge CFTypeRef)self, rng, actual);
r = [self convertRect:r toView:nil];
return [[self window] convertRectToScreen:r];
}
- (void)applicationWillUnhide:(NSNotification *)notification {
gio_onShow(self.handle);
}
- (void)applicationDidHide:(NSNotification *)notification {
gio_onHide(self.handle);
}
- (void)dealloc {
gio_onDestroy(self.handle);
}
@end
// Delegates are weakly referenced from their peers. Nothing
@@ -254,7 +240,7 @@ void gio_showCursor() {
// some cursors are not public, this tries to use a private cursor
// and uses fallback when the use of private cursor fails.
static void trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
void gio_trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
if ([NSCursor respondsToSelector:cursorName]) {
id object = [NSCursor performSelector:cursorName];
if ([object isKindOfClass:[NSCursor class]]) {
@@ -286,7 +272,7 @@ void gio_setCursor(NSUInteger curID) {
break;
case 6: // pointer.CursorAllScroll
// For some reason, using _moveCursor fails on Monterey.
// trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
// gio_trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
[NSCursor.arrowCursor set];
break;
case 7: // pointer.CursorColResize
@@ -296,31 +282,33 @@ void gio_setCursor(NSUInteger curID) {
[NSCursor.resizeUpDownCursor set];
break;
case 9: // pointer.CursorGrab
[NSCursor.openHandCursor set];
// [NSCursor.openHandCursor set];
gio_trySetPrivateCursor(@selector(openHandCursor), NSCursor.arrowCursor);
break;
case 10: // pointer.CursorGrabbing
[NSCursor.closedHandCursor set];
// [NSCursor.closedHandCursor set];
gio_trySetPrivateCursor(@selector(closedHandCursor), NSCursor.arrowCursor);
break;
case 11: // pointer.CursorNotAllowed
[NSCursor.operationNotAllowedCursor set];
break;
case 12: // pointer.CursorWait
trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
break;
case 13: // pointer.CursorProgress
trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
break;
case 14: // pointer.CursorNorthWestResize
trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
break;
case 15: // pointer.CursorNorthEastResize
trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
break;
case 16: // pointer.CursorSouthWestResize
trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
gio_trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
break;
case 17: // pointer.CursorSouthEastResize
trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
gio_trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
break;
case 18: // pointer.CursorNorthSouthResize
[NSCursor.resizeUpDownCursor set];
@@ -341,10 +329,10 @@ void gio_setCursor(NSUInteger curID) {
[NSCursor.resizeDownCursor set];
break;
case 24: // pointer.CursorNorthEastSouthWestResize
trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
break;
case 25: // pointer.CursorNorthWestSouthEastResize
trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
break;
default:
[NSCursor.arrowCursor set];
@@ -386,32 +374,22 @@ CFTypeRef gio_createView(void) {
GioView* view = [[GioView alloc] initWithFrame:frame];
view.wantsLayer = YES;
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
[[NSNotificationCenter defaultCenter] addObserver:view
selector:@selector(applicationWillUnhide:)
name:NSApplicationWillUnhideNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:view
selector:@selector(applicationDidHide:)
name:NSApplicationDidHideNotification
object:nil];
return CFBridgingRetain(view);
}
}
void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
@autoreleasepool {
GioView *v = (__bridge GioView *)viewRef;
v.handle = handle;
}
}
@implementation GioAppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp activateIgnoringOtherApps:YES];
gio_onFinishLaunching();
}
- (void)applicationDidHide:(NSNotification *)aNotification {
gio_onAppHide();
}
- (void)applicationWillUnhide:(NSNotification *)notification {
gio_onAppShow();
}
@end
void gio_main() {
+12 -29
View File
@@ -9,10 +9,16 @@ import (
"errors"
"unsafe"
"gioui.org/io/event"
"gioui.org/io/pointer"
)
// ViewEvent provides handles to the underlying window objects for the
// current display protocol.
type ViewEvent interface {
implementsViewEvent()
ImplementsEvent()
}
type X11ViewEvent struct {
// Display is a pointer to the X11 Display created by XOpenDisplay.
Display unsafe.Pointer
@@ -43,7 +49,7 @@ type windowDriver func(*callbacks, []Option) error
// let each driver initialize these variables with their own version of createWindow.
var wlDriver, x11Driver windowDriver
func newWindow(window *callbacks, options []Option) {
func newWindow(window *callbacks, options []Option) error {
var errFirst error
for _, d := range []windowDriver{wlDriver, x11Driver} {
if d == nil {
@@ -51,39 +57,16 @@ func newWindow(window *callbacks, options []Option) {
}
err := d(window, options)
if err == nil {
return
return nil
}
if errFirst == nil {
errFirst = err
}
}
window.SetDriver(&dummyDriver{
win: window,
wakeups: make(chan event.Event, 1),
})
if errFirst == nil {
errFirst = errors.New("app: no window driver available")
}
window.ProcessEvent(DestroyEvent{Err: errFirst})
}
type dummyDriver struct {
win *callbacks
wakeups chan event.Event
}
func (d *dummyDriver) Event() event.Event {
if e, ok := d.win.nextEvent(); ok {
return e
}
return <-d.wakeups
}
func (d *dummyDriver) Invalidate() {
select {
case d.wakeups <- wakeupEvent{}:
default:
if errFirst != nil {
return errFirst
}
return errors.New("app: no window driver available")
}
// xCursor contains mapping from pointer.Cursor to XCursor.
+110 -173
View File
@@ -15,7 +15,6 @@ import (
"math"
"os"
"os/exec"
"runtime"
"strconv"
"sync"
"time"
@@ -26,12 +25,10 @@ import (
"gioui.org/app/internal/xkb"
"gioui.org/f32"
"gioui.org/internal/fling"
"gioui.org/io/event"
"gioui.org/io/clipboard"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
)
@@ -100,9 +97,7 @@ type wlDisplay struct {
read, write int
}
repeat repeatState
poller poller
readClipClose chan struct{}
repeat repeatState
}
type wlSeat struct {
@@ -142,7 +137,7 @@ type repeatState struct {
delay time.Duration
key uint32
win *window
win *callbacks
stopC chan struct{}
start time.Duration
@@ -199,10 +194,12 @@ type window struct {
dir f32.Point
}
configured bool
stage system.Stage
dead bool
lastFrameCallback *C.struct_wl_callback
animating bool
redraw bool
// The most recent configure serial waiting to be ack'ed.
serial C.uint32_t
scale int
@@ -212,13 +209,9 @@ type window struct {
wsize image.Point // window config size before going fullscreen or maximized
inCompositor bool // window is moving or being resized
clipReads chan transfer.DataEvent
clipReads chan clipboard.Event
wakeups chan struct{}
// invMu avoids the race between the destruction of disp and
// Invalidate waking it up.
invMu sync.Mutex
}
type poller struct {
@@ -267,17 +260,25 @@ func newWLWindow(callbacks *callbacks, options []Option) error {
return err
}
w.w = callbacks
w.w.SetDriver(w)
go func() {
defer d.destroy()
defer w.destroy()
// Finish and commit setup from createNativeWindow.
w.Configure(options)
w.draw(true)
C.wl_surface_commit(w.surf)
w.w.SetDriver(w)
w.ProcessEvent(WaylandViewEvent{
Display: unsafe.Pointer(w.display()),
Surface: unsafe.Pointer(w.surf),
})
// Finish and commit setup from createNativeWindow.
w.Configure(options)
C.wl_surface_commit(w.surf)
w.w.Event(WaylandViewEvent{
Display: unsafe.Pointer(w.display()),
Surface: unsafe.Pointer(w.surf),
})
err := w.loop()
w.w.Event(WaylandViewEvent{})
w.w.Event(system.DestroyEvent{Err: err})
}()
return nil
}
@@ -353,7 +354,7 @@ func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) {
ppdp: ppdp,
ppsp: ppdp,
wakeups: make(chan struct{}, 1),
clipReads: make(chan transfer.DataEvent, 1),
clipReads: make(chan clipboard.Event, 1),
}
w.surf = C.wl_compositor_create_surface(d.compositor)
if w.surf == nil {
@@ -548,15 +549,15 @@ func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) {
func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) {
w := callbackLoad(data).(*window)
w.serial = serial
w.redraw = true
C.xdg_surface_ack_configure(wmSurf, serial)
w.configured = true
w.draw(true)
w.setStage(system.StageRunning)
}
//export gio_onToplevelClose
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
w := callbackLoad(data).(*window)
w.close(nil)
w.dead = true
}
//export gio_onToplevelConfigure
@@ -585,8 +586,8 @@ func gio_onToplevelDecorationConfigure(data unsafe.Pointer, deco *C.struct_zxdg_
} else {
w.size.Y += int(w.config.decoHeight)
}
w.ProcessEvent(ConfigEvent{Config: w.config})
w.draw(true)
w.w.Event(ConfigEvent{Config: w.config})
w.redraw = true
}
}
@@ -644,7 +645,7 @@ func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output *
if w.config.Mode == Minimized {
// Minimized window got brought back up: it is no longer so.
w.config.Mode = Windowed
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
}
@@ -789,7 +790,7 @@ func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: pointer.Press,
Source: pointer.Touch,
Position: w.lastTouch,
@@ -805,7 +806,7 @@ func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.ui
s.serial = serial
w := s.touchFoci[id]
delete(s.touchFoci, id)
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: pointer.Release,
Source: pointer.Touch,
Position: w.lastTouch,
@@ -823,7 +824,7 @@ func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: pointer.Move,
Position: w.lastTouch,
Source: pointer.Touch,
@@ -842,7 +843,7 @@ func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) {
s := callbackLoad(data).(*wlSeat)
for id, w := range s.touchFoci {
delete(s.touchFoci, id)
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: pointer.Cancel,
Source: pointer.Touch,
})
@@ -868,7 +869,7 @@ func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.ui
s.serial = serial
if w.inCompositor {
w.inCompositor = false
w.ProcessEvent(pointer.Event{Kind: pointer.Cancel})
w.w.Event(pointer.Event{Kind: pointer.Cancel})
}
}
@@ -929,7 +930,7 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t,
}
w.flushScroll()
w.resetFling()
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: kind,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
@@ -1017,34 +1018,23 @@ func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, axis
}
func (w *window) ReadClipboard() {
if w.disp.readClipClose != nil {
return
}
w.disp.readClipClose = make(chan struct{})
r, err := w.disp.readClipboard()
// Send empty responses on unavailable clipboards or errors.
if r == nil || err != nil {
w.w.Event(clipboard.Event{})
return
}
// Don't let slow clipboard transfers block event loop.
go func() {
defer r.Close()
data, _ := io.ReadAll(r)
e := transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(bytes.NewReader(data))
},
}
select {
case w.clipReads <- e:
w.disp.wakeup()
case <-w.disp.readClipClose:
}
w.clipReads <- clipboard.Event{Text: string(data)}
w.Wakeup()
}()
}
func (w *window) WriteClipboard(mime string, s []byte) {
w.disp.writeClipboard(s)
func (w *window) WriteClipboard(s string) {
w.disp.writeClipboard([]byte(s))
}
func (w *window) Configure(options []Option) {
@@ -1102,7 +1092,8 @@ func (w *window) Configure(options []Option) {
w.config.MaxSize = cnf.MaxSize
w.setWindowConstraints()
}
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
w.redraw = true
}
func (w *window) setWindowConstraints() {
@@ -1139,7 +1130,7 @@ func (w *window) Perform(actions system.Action) {
walkActions(actions, func(action system.Action) {
switch action {
case system.ActionClose:
w.close(nil)
w.dead = true
}
})
}
@@ -1222,8 +1213,7 @@ func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
w := callbackLoad(unsafe.Pointer(surf)).(*window)
s.keyboardFocus = w
s.disp.repeat.Stop(0)
w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(key.FocusEvent{Focus: true})
}
//export gio_onKeyboardLeave
@@ -1232,8 +1222,7 @@ func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
s.serial = serial
s.disp.repeat.Stop(0)
w := s.keyboardFocus
w.config.Focused = false
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(key.FocusEvent{Focus: false})
}
//export gio_onKeyboardKey
@@ -1251,7 +1240,7 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri
// There's no support for IME yet.
w.w.EditorInsert(ee.Text)
} else {
w.ProcessEvent(e)
w.w.Event(e)
}
}
if state != C.WL_KEYBOARD_KEY_STATE_PRESSED {
@@ -1286,7 +1275,7 @@ func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) {
r.now = 0
r.stopC = stopC
r.key = keyCode
r.win = w
r.win = w.w
rate, delay := r.rate, r.delay
go func() {
timer := time.NewTimer(delay)
@@ -1344,9 +1333,9 @@ func (r *repeatState) Repeat(d *wlDisplay) {
for _, e := range d.xkb.DispatchKey(r.key, key.Press) {
if ee, ok := e.(key.EditEvent); ok {
// There's no support for IME yet.
r.win.w.EditorInsert(ee.Text)
r.win.EditorInsert(ee.Text)
} else {
r.win.ProcessEvent(e)
r.win.Event(e)
}
}
r.last += delay
@@ -1359,76 +1348,28 @@ func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.ui
w := callbackLoad(data).(*window)
if w.lastFrameCallback == callback {
w.lastFrameCallback = nil
w.draw(false)
}
}
func (w *window) close(err error) {
w.ProcessEvent(WaylandViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err})
}
func (w *window) dispatch() {
if w.disp == nil {
<-w.wakeups
w.w.Invalidate()
return
}
if err := w.disp.dispatch(); err != nil {
w.close(err)
return
}
select {
case e := <-w.clipReads:
w.disp.readClipClose = nil
w.ProcessEvent(e)
case <-w.wakeups:
w.w.Invalidate()
default:
}
}
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
}
func (w *window) Event() event.Event {
func (w *window) loop() error {
var p poller
for {
evt, ok := w.w.nextEvent()
if !ok {
w.dispatch()
continue
if err := w.disp.dispatch(&p); err != nil {
return err
}
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
w.invMu.Lock()
w.disp.destroy()
w.disp = nil
w.invMu.Unlock()
select {
case e := <-w.clipReads:
w.w.Event(e)
case <-w.wakeups:
w.w.Event(wakeupEvent{})
default:
}
return evt
if w.dead {
break
}
w.draw()
}
}
func (w *window) Invalidate() {
select {
case w.wakeups <- struct{}{}:
default:
return
}
w.invMu.Lock()
defer w.invMu.Unlock()
if w.disp != nil {
w.disp.wakeup()
}
}
func (w *window) Run(f func()) {
f()
}
func (w *window) Frame(frame *op.Ops) {
w.w.ProcessFrame(frame, nil)
return nil
}
// bindDataDevice initializes the dataDev field if and only if both
@@ -1444,21 +1385,13 @@ func (d *wlDisplay) bindDataDevice() {
}
}
func (d *wlDisplay) dispatch() error {
// wl_display_prepare_read records the current thread for
// use in wl_display_read_events or wl_display_cancel_events.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
func (d *wlDisplay) dispatch(p *poller) error {
dispfd := C.wl_display_get_fd(d.disp)
// Poll for events and notifications.
pollfds := append(d.poller.pollfds[:0],
pollfds := append(p.pollfds[:0],
syscall.PollFd{Fd: int32(dispfd), Events: syscall.POLLIN | syscall.POLLERR},
syscall.PollFd{Fd: int32(d.notify.read), Events: syscall.POLLIN | syscall.POLLERR},
)
for C.wl_display_prepare_read(d.disp) != 0 {
C.wl_display_dispatch_pending(d.disp)
}
dispFd := &pollfds[0]
if ret, err := C.wl_display_flush(d.disp); ret < 0 {
if err != syscall.EAGAIN {
@@ -1469,25 +1402,11 @@ func (d *wlDisplay) dispatch() error {
dispFd.Events |= syscall.POLLOUT
}
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
C.wl_display_cancel_read(d.disp)
return fmt.Errorf("wayland: poll failed: %v", err)
}
if dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0 {
C.wl_display_cancel_read(d.disp)
return errors.New("wayland: display file descriptor gone")
}
// Handle events.
if dispFd.Revents&syscall.POLLIN != 0 {
if ret, err := C.wl_display_read_events(d.disp); ret < 0 {
return fmt.Errorf("wayland: wl_display_read_events failed: %v", err)
}
C.wl_display_dispatch_pending(d.disp)
} else {
C.wl_display_cancel_read(d.disp)
}
// Clear notifications.
for {
_, err := syscall.Read(d.notify.read, d.poller.buf[:])
_, err := syscall.Read(d.notify.read, p.buf[:])
if err == syscall.EAGAIN {
break
}
@@ -1495,15 +1414,29 @@ func (d *wlDisplay) dispatch() error {
return fmt.Errorf("wayland: read from notify pipe failed: %v", err)
}
}
// Handle events
switch {
case dispFd.Revents&syscall.POLLIN != 0:
if ret, err := C.wl_display_dispatch(d.disp); ret < 0 {
return fmt.Errorf("wayland: wl_display_dispatch failed: %v", err)
}
case dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0:
return errors.New("wayland: display file descriptor gone")
}
d.repeat.Repeat(d)
return nil
}
func (w *window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
w.disp.wakeup()
}
func (w *window) SetAnimating(anim bool) {
w.animating = anim
if anim {
w.draw(false)
}
}
// Wakeup wakes up the event loop through the notification pipe.
@@ -1639,7 +1572,7 @@ func (w *window) flushScroll() {
if total == (f32.Point{}) {
return
}
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
@@ -1662,7 +1595,7 @@ func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: pointer.Move,
Position: w.lastPos,
Buttons: w.pointerBtns,
@@ -1683,8 +1616,7 @@ func (w *window) systemGesture() (*C.struct_wl_cursor, C.uint32_t) {
if w.config.Mode != Windowed || w.config.Decorated {
return nil, 0
}
_, cfg := w.getConfig()
border := cfg.Dp(3)
border := w.w.w.metric.Dp(3)
x, y, size := int(w.lastPos.X), int(w.lastPos.Y), w.config.Size
north := y <= border
south := y >= size.Y-border
@@ -1738,10 +1670,13 @@ func (w *window) updateOutputs() {
if found && scale != w.scale {
w.scale = scale
C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale))
w.draw(true)
w.redraw = true
}
if found {
w.draw(true)
if !found {
w.setStage(system.StagePaused)
} else {
w.setStage(system.StageRunning)
w.redraw = true
}
}
@@ -1753,10 +1688,7 @@ func (w *window) getConfig() (image.Point, unit.Metric) {
}
}
func (w *window) draw(sync bool) {
if !w.configured {
return
}
func (w *window) draw() {
w.flushScroll()
size, cfg := w.getConfig()
if cfg == (unit.Metric{}) {
@@ -1764,9 +1696,11 @@ func (w *window) draw(sync bool) {
}
if size != w.config.Size {
w.config.Size = size
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
anim := w.animating || w.fling.anim.Active()
sync := w.redraw
w.redraw = false
// Draw animation only when not waiting for frame callback.
redrawAnim := anim && w.lastFrameCallback == nil
if !redrawAnim && !sync {
@@ -1777,8 +1711,8 @@ func (w *window) draw(sync bool) {
// Use the surface as listener data for gio_onFrameDone.
C.wl_callback_add_listener(w.lastFrameCallback, &C.gio_callback_listener, unsafe.Pointer(w.surf))
}
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: cfg,
@@ -1787,6 +1721,14 @@ func (w *window) draw(sync bool) {
})
}
func (w *window) setStage(s system.Stage) {
if s == w.stage {
return
}
w.stage = s
w.w.Event(system.StageEvent{Stage: s})
}
func (w *window) display() *C.struct_wl_display {
return w.disp.disp
}
@@ -1878,10 +1820,6 @@ func newWLDisplay() (*wlDisplay, error) {
}
func (d *wlDisplay) destroy() {
if d.readClipClose != nil {
close(d.readClipClose)
d.readClipClose = nil
}
if d.notify.write != 0 {
syscall.Close(d.notify.write)
d.notify.write = 0
@@ -1923,7 +1861,6 @@ func (d *wlDisplay) destroy() {
if d.disp != nil {
C.wl_display_disconnect(d.disp)
callbackDelete(unsafe.Pointer(d.disp))
d.disp = nil
}
}
+71 -95
View File
@@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"image"
"io"
"runtime"
"sort"
"strings"
@@ -19,19 +18,17 @@ import (
syscall "golang.org/x/sys/windows"
"gioui.org/app/internal/windows"
"gioui.org/op"
"gioui.org/unit"
gowindows "golang.org/x/sys/windows"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/clipboard"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
)
type Win32ViewEvent struct {
type ViewEvent struct {
HWND uintptr
}
@@ -39,6 +36,7 @@ type window struct {
hwnd syscall.Handle
hdc syscall.Handle
w *callbacks
stage system.Stage
pointerBtns pointer.Buttons
// cursorIn tracks whether the cursor was inside the window according
@@ -50,13 +48,10 @@ type window struct {
placement *windows.WindowPlacement
animating bool
focused bool
borderSize image.Point
config Config
loop *eventLoop
// invMu avoids the race between destroying the window and Invalidate.
invMu sync.Mutex
}
const _WM_WAKEUP = windows.WM_USER + iota
@@ -89,38 +84,36 @@ func osMain() {
select {}
}
func newWindow(win *callbacks, options []Option) {
done := make(chan struct{})
func newWindow(window *callbacks, options []Option) error {
cerr := make(chan error)
go func() {
// GetMessage and PeekMessage can filter on a window HWND, but
// then thread-specific messages such as WM_QUIT are ignored.
// Instead lock the thread so window messages arrive through
// unfiltered GetMessage calls.
runtime.LockOSThread()
w := &window{
w: win,
}
w.loop = newEventLoop(w.w, w.wakeup)
w.w.SetDriver(w)
err := w.init()
done <- struct{}{}
w, err := createNativeWindow()
if err != nil {
w.ProcessEvent(DestroyEvent{Err: err})
cerr <- err
return
}
cerr <- nil
winMap.Store(w.hwnd, w)
defer winMap.Delete(w.hwnd)
w.ProcessEvent(Win32ViewEvent{HWND: uintptr(w.hwnd)})
w.w = window
w.w.SetDriver(w)
w.w.Event(ViewEvent{HWND: uintptr(w.hwnd)})
w.Configure(options)
windows.SetForegroundWindow(w.hwnd)
windows.SetFocus(w.hwnd)
// Since the window class for the cursor is null,
// set it here to show the cursor.
w.SetCursor(pointer.CursorDefault)
w.runLoop()
if err := w.loop(); err != nil {
panic(err)
}
}()
<-done
return <-cerr
}
// initResources initializes the resources global.
@@ -155,13 +148,13 @@ func initResources() error {
const dwExStyle = windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE
func (w *window) init() error {
func createNativeWindow() (*window, error) {
var resErr error
resources.once.Do(func() {
resErr = initResources()
})
if resErr != nil {
return resErr
return nil, resErr
}
const dwStyle = windows.WS_OVERLAPPEDWINDOW
@@ -177,15 +170,16 @@ func (w *window) init() error {
resources.handle,
0)
if err != nil {
return err
return nil, err
}
w := &window{
hwnd: hwnd,
}
w.hdc, err = windows.GetDC(hwnd)
if err != nil {
windows.DestroyWindow(hwnd)
return err
return nil, err
}
w.hwnd = hwnd
return nil
return w, nil
}
// update() handles changes done by the user, and updates the configuration.
@@ -202,7 +196,7 @@ func (w *window) update() {
windows.GetSystemMetrics(windows.SM_CXSIZEFRAME),
windows.GetSystemMetrics(windows.SM_CYSIZEFRAME),
)
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr {
@@ -243,7 +237,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
e.State = key.Release
}
w.ProcessEvent(e)
w.w.Event(e)
if (wParam == windows.VK_F10) && (msg == windows.WM_SYSKEYDOWN || msg == windows.WM_SYSKEYUP) {
// Reserve F10 for ourselves, and don't let it open the system menu. Other Windows programs
@@ -264,15 +258,23 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_MBUTTONUP:
w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers())
case windows.WM_CANCELMODE:
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: pointer.Cancel,
})
case windows.WM_SETFOCUS:
w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
w.focused = true
w.w.Event(key.FocusEvent{Focus: true})
case windows.WM_KILLFOCUS:
w.config.Focused = false
w.ProcessEvent(ConfigEvent{Config: w.config})
w.focused = false
w.w.Event(key.FocusEvent{Focus: false})
case windows.WM_NCACTIVATE:
if w.stage >= system.StageInactive {
if wParam == windows.TRUE {
w.setStage(system.StageRunning)
} else {
w.setStage(system.StageInactive)
}
}
case windows.WM_NCHITTEST:
if w.config.Decorated {
// Let the system handle it.
@@ -285,7 +287,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_MOUSEMOVE:
x, y := coordsFromlParam(lParam)
p := f32.Point{X: float32(x), Y: float32(y)}
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
Position: p,
@@ -298,16 +300,14 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_MOUSEHWHEEL:
w.scrollEvent(wParam, lParam, true, getModifiers())
case windows.WM_DESTROY:
w.ProcessEvent(Win32ViewEvent{})
w.ProcessEvent(DestroyEvent{})
w.w.Event(ViewEvent{})
w.w.Event(system.DestroyEvent{})
if w.hdc != 0 {
windows.ReleaseDC(w.hdc)
w.hdc = 0
}
w.invMu.Lock()
// The system destroys the HWND for us.
w.hwnd = 0
w.invMu.Unlock()
windows.PostQuitMessage(0)
case windows.WM_NCCALCSIZE:
if w.config.Decorated {
@@ -327,7 +327,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
// Adjust window position to avoid the extra padding in maximized
// state. See https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543.
// Note that trying to do the adjustment in WM_GETMINMAXINFO is ignored by Windows.
szp := (*windows.NCCalcSizeParams)(unsafe.Pointer(lParam))
szp := (*windows.NCCalcSizeParams)(unsafe.Pointer(uintptr(lParam)))
mi := windows.GetMonitorInfo(w.hwnd)
szp.Rgrc[0] = mi.WorkArea
return 0
@@ -338,15 +338,18 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
switch wParam {
case windows.SIZE_MINIMIZED:
w.config.Mode = Minimized
w.setStage(system.StagePaused)
case windows.SIZE_MAXIMIZED:
w.config.Mode = Maximized
w.setStage(system.StageRunning)
case windows.SIZE_RESTORED:
if w.config.Mode != Fullscreen {
w.config.Mode = Windowed
}
w.setStage(system.StageRunning)
}
case windows.WM_GETMINMAXINFO:
mm := (*windows.MinMaxInfo)(unsafe.Pointer(lParam))
mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam)))
var bw, bh int32
if w.config.Decorated {
r := windows.GetWindowRect(w.hwnd)
@@ -374,8 +377,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
return windows.TRUE
}
case _WM_WAKEUP:
w.loop.Wakeup()
w.loop.FlushEvents()
w.w.Event(wakeupEvent{})
case windows.WM_IME_STARTCOMPOSITION:
imc := windows.ImmGetContext(w.hwnd)
if imc == 0 {
@@ -495,7 +497,7 @@ func (w *window) hitTest(x, y int) uintptr {
}
func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, kmods key.Modifiers) {
if !w.config.Focused {
if !w.focused {
windows.SetFocus(w.hwnd)
}
@@ -515,7 +517,7 @@ func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr,
}
x, y := coordsFromlParam(lParam)
p := f32.Point{X: float32(x), Y: float32(y)}
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: kind,
Source: pointer.Mouse,
Position: p,
@@ -550,7 +552,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
sp.Y = -dist
}
}
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Mouse,
Position: p,
@@ -562,7 +564,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
}
// Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/
func (w *window) runLoop() {
func (w *window) loop() error {
msg := new(windows.Msg)
loop:
for {
@@ -573,7 +575,7 @@ loop:
}
switch ret := windows.GetMessage(msg, 0, 0, 0); ret {
case -1:
panic(errors.New("GetMessage failed"))
return errors.New("GetMessage failed")
case 0:
// WM_QUIT received.
break loop
@@ -581,6 +583,7 @@ loop:
windows.TranslateMessage(msg)
windows.DispatchMessage(msg)
}
return nil
}
func (w *window) EditorStateChanged(old, new editorState) {
@@ -598,48 +601,27 @@ func (w *window) SetAnimating(anim bool) {
w.animating = anim
}
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
w.loop.FlushEvents()
}
func (w *window) Event() event.Event {
return w.loop.Event()
}
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
func (w *window) wakeup() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.hwnd == 0 {
w.loop.Wakeup()
w.loop.FlushEvents()
return
}
func (w *window) Wakeup() {
if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil {
panic(err)
}
}
func (w *window) setStage(s system.Stage) {
if s != w.stage {
w.stage = s
w.w.Event(system.StageEvent{Stage: s})
}
}
func (w *window) draw(sync bool) {
if w.config.Size.X == 0 || w.config.Size.Y == 0 {
return
}
dpi := windows.GetWindowDPI(w.hwnd)
cfg := configForDPI(dpi)
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: cfg,
@@ -685,12 +667,7 @@ func (w *window) readClipboard() error {
}
defer windows.GlobalUnlock(mem)
content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr)))
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
w.w.Event(clipboard.Event{Text: content})
return nil
}
@@ -758,8 +735,8 @@ func (w *window) Configure(options []Option) {
w.update()
}
func (w *window) WriteClipboard(mime string, s []byte) {
w.writeClipboard(string(s))
func (w *window) WriteClipboard(s string) {
w.writeClipboard(s)
}
func (w *window) writeClipboard(s string) error {
@@ -887,11 +864,11 @@ func (w *window) raise() {
windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW)
}
func convertKeyCode(code uintptr) (key.Name, bool) {
func convertKeyCode(code uintptr) (string, bool) {
if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' {
return key.Name(rune(code)), true
return string(rune(code)), true
}
var r key.Name
var r string
switch code {
case windows.VK_ESCAPE:
@@ -991,5 +968,4 @@ func configForDPI(dpi int) unit.Metric {
}
}
func (Win32ViewEvent) implementsViewEvent() {}
func (Win32ViewEvent) ImplementsEvent() {}
func (_ ViewEvent) ImplementsEvent() {}
+84 -120
View File
@@ -30,20 +30,16 @@ import (
"errors"
"fmt"
"image"
"io"
"strconv"
"strings"
"sync"
"time"
"unsafe"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/clipboard"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
syscall "golang.org/x/sys/unix"
@@ -95,10 +91,12 @@ type x11Window struct {
// _NET_WM_STATE_MAXIMIZED_VERT
wmStateMaximizedVert C.Atom
}
stage system.Stage
metric unit.Metric
notify struct {
read, write int
}
dead bool
animating bool
@@ -111,11 +109,6 @@ type x11Window struct {
config Config
wakeups chan struct{}
handler x11EventHandler
buf [100]byte
// invMy avoids the race between destroy and Invalidate.
invMu sync.Mutex
}
var (
@@ -158,8 +151,8 @@ func (w *x11Window) ReadClipboard() {
C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime)
}
func (w *x11Window) WriteClipboard(mime string, s []byte) {
w.clipboard.content = s
func (w *x11Window) WriteClipboard(s string) {
w.clipboard.content = []byte(s)
C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime)
C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime)
}
@@ -239,7 +232,7 @@ func (w *x11Window) Configure(options []Option) {
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
func (w *x11Window) setTitle(prev, cnf Config) {
@@ -382,47 +375,11 @@ func (w *x11Window) sendWMStateEvent(action C.long, atom1, atom2 C.ulong) {
var x11OneByte = make([]byte, 1)
func (w *x11Window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
}
func (w *x11Window) shutdown(err error) {
w.ProcessEvent(X11ViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err})
}
func (w *x11Window) Event() event.Event {
for {
evt, ok := w.w.nextEvent()
if !ok {
w.dispatch()
continue
}
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
}
return evt
}
}
func (w *x11Window) Run(f func()) {
f()
}
func (w *x11Window) Frame(frame *op.Ops) {
w.w.ProcessFrame(frame, nil)
}
func (w *x11Window) Invalidate() {
func (w *x11Window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
w.invMu.Lock()
defer w.invMu.Unlock()
if w.x == nil {
return
}
if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
panic(fmt.Errorf("failed to write to pipe: %v", err))
}
@@ -436,20 +393,16 @@ func (w *x11Window) window() (C.Window, int, int) {
return w.xw, w.config.Size.X, w.config.Size.Y
}
func (w *x11Window) dispatch() {
if w.x == nil {
// Only Invalidate can wake us up.
<-w.wakeups
w.w.Invalidate()
func (w *x11Window) setStage(s system.Stage) {
if s == w.stage {
return
}
w.stage = s
w.w.Event(system.StageEvent{Stage: s})
}
select {
case <-w.wakeups:
w.w.Invalidate()
default:
}
func (w *x11Window) loop() {
h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
xfd := C.XConnectionNumber(w.x)
// Poll for events and notifications.
@@ -459,52 +412,64 @@ func (w *x11Window) dispatch() {
}
xEvents := &pollfds[0].Revents
// Plenty of room for a backlog of notifications.
buf := make([]byte, 100)
var syn, anim bool
// Check for pending draw events before checking animation or blocking.
// This fixes an issue on Xephyr where on startup XPending() > 0 but
// poll will still block. This also prevents no-op calls to poll.
if syn = w.handler.handleEvents(); !syn {
anim = w.animating
if !anim {
// Clear poll events.
*xEvents = 0
// Wait for X event or gio notification.
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
panic(fmt.Errorf("x11 loop: poll failed: %w", err))
}
switch {
case *xEvents&syscall.POLLIN != 0:
syn = w.handler.handleEvents()
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
loop:
for !w.dead {
var syn, anim bool
// Check for pending draw events before checking animation or blocking.
// This fixes an issue on Xephyr where on startup XPending() > 0 but
// poll will still block. This also prevents no-op calls to poll.
if syn = h.handleEvents(); !syn {
anim = w.animating
if !anim {
// Clear poll events.
*xEvents = 0
// Wait for X event or gio notification.
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
panic(fmt.Errorf("x11 loop: poll failed: %w", err))
}
switch {
case *xEvents&syscall.POLLIN != 0:
syn = h.handleEvents()
if w.dead {
break loop
}
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
break loop
}
}
}
}
// Clear notifications.
for {
_, err := syscall.Read(w.notify.read, w.buf[:])
if err == syscall.EAGAIN {
break
// Clear notifications.
for {
_, err := syscall.Read(w.notify.read, buf)
if err == syscall.EAGAIN {
break
}
if err != nil {
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
}
}
if err != nil {
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
select {
case <-w.wakeups:
w.w.Event(wakeupEvent{})
default:
}
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: w.metric,
},
Sync: syn,
})
}
}
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: w.metric,
},
Sync: syn,
})
}
}
func (w *x11Window) destroy() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.notify.write != 0 {
syscall.Close(w.notify.write)
w.notify.write = 0
@@ -519,7 +484,6 @@ func (w *x11Window) destroy() {
}
C.XDestroyWindow(w.x, w.xw)
C.XCloseDisplay(w.x)
w.x = nil
}
// atom is a wrapper around XInternAtom. Callers should cache the result
@@ -577,7 +541,7 @@ func (h *x11EventHandler) handleEvents() bool {
// There's no support for IME yet.
w.w.EditorInsert(ee.Text)
} else {
w.ProcessEvent(e)
w.w.Event(e)
}
}
case C.ButtonPress, C.ButtonRelease:
@@ -639,10 +603,10 @@ func (h *x11EventHandler) handleEvents() bool {
w.pointerBtns &^= btn
}
ev.Buttons = w.pointerBtns
w.ProcessEvent(ev)
w.w.Event(ev)
case C.MotionNotify:
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
w.ProcessEvent(pointer.Event{
w.w.Event(pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
@@ -657,16 +621,14 @@ func (h *x11EventHandler) handleEvents() bool {
// redraw only on the last expose event
redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0
case C.FocusIn:
w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(key.FocusEvent{Focus: true})
case C.FocusOut:
w.config.Focused = false
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(key.FocusEvent{Focus: false})
case C.ConfigureNotify: // window configuration change
cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev))
if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size {
w.config.Size = sz
w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
// redraw will be done by a later expose event
case C.SelectionNotify:
@@ -688,12 +650,7 @@ func (h *x11EventHandler) handleEvents() bool {
break
}
str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(str))
},
})
w.w.Event(clipboard.Event{Text: str})
case C.SelectionRequest:
cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None {
@@ -747,7 +704,7 @@ func (h *x11EventHandler) handleEvents() bool {
cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev))
switch *(*C.long)(unsafe.Pointer(&cevt.data)) {
case C.long(w.atoms.evDelWindow):
w.shutdown(nil)
w.dead = true
return false
}
}
@@ -829,10 +786,8 @@ func newX11Window(gioWin *callbacks, options []Option) error {
wakeups: make(chan struct{}, 1),
config: Config{Size: cnf.Size},
}
w.handler = x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
w.notify.read = pipe[0]
w.notify.write = pipe[1]
w.w.SetDriver(w)
if err := w.updateXkbKeymap(); err != nil {
w.destroy()
@@ -868,10 +823,19 @@ func newX11Window(gioWin *callbacks, options []Option) error {
// extensions
C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1)
// make the window visible on the screen
C.XMapWindow(dpy, win)
w.Configure(options)
w.ProcessEvent(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
go func() {
w.w.SetDriver(w)
// make the window visible on the screen
C.XMapWindow(dpy, win)
w.Configure(options)
w.w.Event(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
w.setStage(system.StageRunning)
w.loop()
w.w.Event(X11ViewEvent{})
w.w.Event(system.DestroyEvent{Err: nil})
w.destroy()
}()
return nil
}
-13
View File
@@ -1,13 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package app
// DestroyEvent is the last event sent through
// a window event channel.
type DestroyEvent struct {
// Err is nil for normal window closures. If a
// window is prematurely closed, Err is the cause.
Err error
}
func (DestroyEvent) ImplementsEvent() {}
+612 -345
View File
File diff suppressed because it is too large Load Diff
Generated
+17 -48
View File
@@ -9,11 +9,11 @@
]
},
"locked": {
"lastModified": 1701721028,
"narHash": "sha256-2z4YrdHPLoMZNWR1MPOjNZMqPg057i1eZXaYI6RTahQ=",
"lastModified": 1659298920,
"narHash": "sha256-LgRMge8BZUG15EN43iDJOlnEMX1dvRprB7SaoNqgibU=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "c923f9ec0f4dd0d7dc725dc5b73fbf03658e50dd",
"rev": "d4f20a3cd4ce961bb23b48447457f6810d69ae5e",
"type": "github"
},
"original": {
@@ -24,18 +24,21 @@
},
"devshell": {
"inputs": {
"nixpkgs": [
"flake-utils": [
"android",
"nixpkgs"
],
"systems": "systems"
"nixpkgs": [
"android",
"nixpkgs"
]
},
"locked": {
"lastModified": 1701697687,
"narHash": "sha256-dLLE5wQBVv+pIb4bWmKFSw2DvLVyuEk0F7ng6hpZPSU=",
"lastModified": 1658746384,
"narHash": "sha256-CCJcoMOcXyZFrV1ag4XMTpAPjLWb4Anbv+ktXFI1ry0=",
"owner": "numtide",
"repo": "devshell",
"rev": "c3bd77911391eb1638af6ce773de86da57ee6df5",
"rev": "0ffc7937bb5e8141af03d462b468bd071eb18e1b",
"type": "github"
},
"original": {
@@ -45,15 +48,12 @@
}
},
"flake-utils": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"lastModified": 1656928814,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"type": "github"
},
"original": {
@@ -64,16 +64,15 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1701282334,
"narHash": "sha256-MxCVrXY6v4QmfTwIysjjaX0XUhqBbxTWWB4HXtDYsdk=",
"lastModified": 1659305579,
"narHash": "sha256-SFeQTmh7hc9Y2fSkooHaoS8mDfPa04sfmUCtQ8MA6Pg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "057f9aecfb71c4437d2b27d3323df7f93c010b7e",
"rev": "5857574d45925585baffde730369414319228a84",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "23.11",
"repo": "nixpkgs",
"type": "github"
}
@@ -83,36 +82,6 @@
"android": "android",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+4 -4
View File
@@ -3,7 +3,7 @@
description = "Gio build environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/23.11";
nixpkgs.url = "github:NixOS/nixpkgs";
android.url = "github:tadfisher/android-nixpkgs";
android.inputs.nixpkgs.follows = "nixpkgs";
};
@@ -33,10 +33,10 @@
default = with pkgs; mkShell
({
ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk";
JAVA_HOME = jdk17.home;
JAVA_HOME = jdk8.home;
packages = [
android-sdk
jdk17
jdk8
clang
] ++ (if stdenv.isLinux then [
vulkan-headers
@@ -46,7 +46,7 @@
xorg.libXcursor
xorg.libXfixes
libGL
pkg-config
pkgconfig
] else if stdenv.isDarwin then [
darwin.apple_sdk_11_0.frameworks.Foundation
darwin.apple_sdk_11_0.frameworks.Metal
+61 -71
View File
@@ -18,7 +18,6 @@ import (
"gioui.org/f32"
"gioui.org/internal/fling"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/op"
@@ -38,19 +37,15 @@ type Hover struct {
// Add the gesture to detect hovering over the current pointer area.
func (h *Hover) Add(ops *op.Ops) {
event.Op(ops, h)
pointer.InputOp{
Tag: h,
Kinds: pointer.Enter | pointer.Leave,
}.Add(ops)
}
// Update state and report whether a pointer is inside the area.
func (h *Hover) Update(q input.Source) bool {
for {
ev, ok := q.Event(pointer.Filter{
Target: h,
Kinds: pointer.Enter | pointer.Leave | pointer.Cancel,
})
if !ok {
break
}
func (h *Hover) Update(q event.Queue) bool {
for _, ev := range q.Events(h) {
e, ok := ev.(pointer.Event)
if !ok {
continue
@@ -112,6 +107,7 @@ type Drag struct {
pressed bool
pid pointer.ID
start f32.Point
grab bool
}
// Scroll detects scroll gestures and reduces them to
@@ -119,9 +115,11 @@ type Drag struct {
// movements as well as drag and fling touch gestures.
type Scroll struct {
dragging bool
axis Axis
estimator fling.Extrapolation
flinger fling.Animation
pid pointer.ID
grab bool
last int
// Leftover scroll.
scroll float32
@@ -163,7 +161,10 @@ const touchSlop = unit.Dp(3)
// Add the handler to the operation list to receive click events.
func (c *Click) Add(ops *op.Ops) {
event.Op(ops, c)
pointer.InputOp{
Tag: c,
Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
}.Add(ops)
}
// Hovered returns whether a pointer is inside the area.
@@ -176,16 +177,10 @@ func (c *Click) Pressed() bool {
return c.pressed
}
// Update state and return the next click events, if any.
func (c *Click) Update(q input.Source) (ClickEvent, bool) {
for {
evt, ok := q.Event(pointer.Filter{
Target: c,
Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave | pointer.Cancel,
})
if !ok {
break
}
// Update state and return the click events.
func (c *Click) Update(q event.Queue) []ClickEvent {
var events []ClickEvent
for _, evt := range q.Events(c) {
e, ok := evt.(pointer.Event)
if !ok {
continue
@@ -197,15 +192,9 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
}
c.pressed = false
if !c.entered || c.hovered {
return ClickEvent{
Kind: KindClick,
Position: e.Position.Round(),
Source: e.Source,
Modifiers: e.Modifiers,
NumClicks: c.clicks,
}, true
events = append(events, ClickEvent{Kind: KindClick, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
} else {
return ClickEvent{Kind: KindCancel}, true
events = append(events, ClickEvent{Kind: KindCancel})
}
case pointer.Cancel:
wasPressed := c.pressed
@@ -213,7 +202,7 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
c.hovered = false
c.entered = false
if wasPressed {
return ClickEvent{Kind: KindCancel}, true
events = append(events, ClickEvent{Kind: KindCancel})
}
case pointer.Press:
if c.pressed {
@@ -235,7 +224,7 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
c.clicks = 1
}
c.clickedAt = e.Time
return ClickEvent{Kind: KindPress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks}, true
events = append(events, ClickEvent{Kind: KindPress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
case pointer.Leave:
if !c.pressed {
c.pid = e.PointerID
@@ -253,16 +242,25 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
}
}
}
return ClickEvent{}, false
return events
}
func (ClickEvent) ImplementsEvent() {}
// Add the handler to the operation list to receive scroll events.
// The bounds variable refers to the scrolling boundaries
// as defined in [pointer.Filter].
func (s *Scroll) Add(ops *op.Ops) {
event.Op(ops, s)
// as defined in io/pointer.InputOp.
func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) {
oph := pointer.InputOp{
Tag: s,
Grab: s.grab,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
ScrollBounds: bounds,
}
oph.Add(ops)
if s.flinger.Active() {
op.InvalidateOp{}.Add(ops)
}
}
// Stop any remaining fling movement.
@@ -271,18 +269,13 @@ func (s *Scroll) Stop() {
}
// Update state and report the scroll distance along axis.
func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, bounds image.Rectangle) int {
total := 0
f := pointer.Filter{
Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
ScrollBounds: bounds,
func (s *Scroll) Update(cfg unit.Metric, q event.Queue, t time.Time, axis Axis) int {
if s.axis != axis {
s.axis = axis
return 0
}
for {
evt, ok := q.Event(f)
if !ok {
break
}
total := 0
for _, evt := range q.Events(s) {
e, ok := evt.(pointer.Event)
if !ok {
continue
@@ -299,7 +292,7 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis,
}
s.Stop()
s.estimator = fling.Extrapolation{}
v := s.val(axis, e.Position)
v := s.val(e.Position)
s.last = int(math.Round(float64(v)))
s.estimator.Sample(e.Time, v)
s.dragging = true
@@ -315,8 +308,9 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis,
fallthrough
case pointer.Cancel:
s.dragging = false
s.grab = false
case pointer.Scroll:
switch axis {
switch s.axis {
case Horizontal:
s.scroll += e.Scroll.X
case Vertical:
@@ -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 {
continue
}
val := s.val(axis, e.Position)
val := s.val(e.Position)
s.estimator.Sample(e.Time, val)
v := int(math.Round(float64(val)))
dist := s.last - v
if e.Priority < pointer.Grabbed {
slop := cfg.Dp(touchSlop)
if dist := dist; dist >= slop || -slop >= dist {
q.Execute(pointer.GrabCmd{Tag: s, ID: e.PointerID})
s.grab = true
}
} else {
s.last = v
@@ -345,14 +339,11 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis,
}
}
total += s.flinger.Tick(t)
if s.flinger.Active() {
q.Execute(op.InvalidateCmd{})
}
return total
}
func (s *Scroll) val(axis Axis, p f32.Point) float32 {
if axis == Horizontal {
func (s *Scroll) val(p f32.Point) float32 {
if s.axis == Horizontal {
return p.X
} else {
return p.Y
@@ -373,20 +364,18 @@ func (s *Scroll) State() ScrollState {
// Add the handler to the operation list to receive drag events.
func (d *Drag) Add(ops *op.Ops) {
event.Op(ops, d)
pointer.InputOp{
Tag: d,
Grab: d.grab,
Kinds: pointer.Press | pointer.Drag | pointer.Release,
}.Add(ops)
}
// Update state and return the next drag event, if any.
func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event, bool) {
for {
ev, ok := q.Event(pointer.Filter{
Target: d,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel,
})
if !ok {
break
}
e, ok := ev.(pointer.Event)
// Update state and return the drag events.
func (d *Drag) Update(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event {
var events []pointer.Event
for _, e := range q.Events(d) {
e, ok := e.(pointer.Event)
if !ok {
continue
}
@@ -419,7 +408,7 @@ func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event
diff := e.Position.Sub(d.start)
slop := cfg.Dp(touchSlop)
if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
q.Execute(pointer.GrabCmd{Tag: d, ID: e.PointerID})
d.grab = true
}
}
case pointer.Release, pointer.Cancel:
@@ -428,12 +417,13 @@ func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event
continue
}
d.dragging = false
d.grab = false
}
return e, true
events = append(events, e)
}
return pointer.Event{}, false
return events
}
// Dragging reports whether it is currently in use.
+17 -17
View File
@@ -9,8 +9,8 @@ import (
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/op"
"gioui.org/op/clip"
)
@@ -22,21 +22,20 @@ func TestHover(t *testing.T) {
stack := clip.Rect(rect).Push(ops)
h.Add(ops)
stack.Pop()
r := new(input.Router)
h.Update(r.Source())
r := new(router.Router)
r.Frame(ops)
r.Queue(
pointer.Event{Kind: pointer.Move, Position: f32.Pt(30, 30)},
)
if !h.Update(r.Source()) {
if !h.Update(r) {
t.Fatal("expected hovered")
}
r.Queue(
pointer.Event{Kind: pointer.Move, Position: f32.Pt(50, 50)},
)
if h.Update(r.Source()) {
if h.Update(r) {
t.Fatal("expected not hovered")
}
}
@@ -72,21 +71,12 @@ func TestMouseClicks(t *testing.T) {
var ops op.Ops
click.Add(&ops)
var r input.Router
click.Update(r.Source())
var r router.Router
r.Frame(&ops)
r.Queue(tc.events...)
var clicks []ClickEvent
for {
ev, ok := click.Update(r.Source())
if !ok {
break
}
if ev.Kind == KindClick {
clicks = append(clicks, ev)
}
}
events := click.Update(&r)
clicks := filterMouseClicks(events)
if got, want := len(clicks), len(tc.clicks); got != want {
t.Fatalf("got %d mouse clicks, expected %d", got, want)
}
@@ -116,3 +106,13 @@ func mouseClickEvents(times ...time.Duration) []event.Event {
}
return events
}
func filterMouseClicks(events []ClickEvent) []ClickEvent {
var clicks []ClickEvent
for _, ev := range events {
if ev.Kind == KindClick {
clicks = append(clicks, ev)
}
}
return clicks
}
+3 -3
View File
@@ -6,11 +6,11 @@ require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
gioui.org/shader v1.0.8
github.com/go-text/typesetting v0.1.1
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
golang.org/x/image v0.5.0
golang.org/x/sys v0.5.0
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64
)
require golang.org/x/text v0.9.0
require golang.org/x/text v0.7.0
+6 -7
View File
@@ -5,9 +5,9 @@ gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJG
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -28,16 +28,15 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+10 -15
View File
@@ -8,13 +8,8 @@ import (
"gioui.org/internal/f32"
)
type textureCacheKey struct {
filter byte
handle any
}
type textureCache struct {
res map[textureCacheKey]resourceCacheValue
type resourceCache struct {
res map[interface{}]resourceCacheValue
}
type resourceCacheValue struct {
@@ -42,13 +37,13 @@ type opCacheValue struct {
keep bool
}
func newTextureCache() *textureCache {
return &textureCache{
res: make(map[textureCacheKey]resourceCacheValue),
func newResourceCache() *resourceCache {
return &resourceCache{
res: make(map[interface{}]resourceCacheValue),
}
}
func (r *textureCache) get(key textureCacheKey) (resource, bool) {
func (r *resourceCache) get(key interface{}) (resource, bool) {
v, exists := r.res[key]
if !exists {
return nil, false
@@ -60,17 +55,17 @@ func (r *textureCache) get(key textureCacheKey) (resource, bool) {
return v.resource, exists
}
func (r *textureCache) put(key textureCacheKey, val resource) {
func (r *resourceCache) put(key interface{}, val resource) {
v, exists := r.res[key]
if exists && v.used {
panic(fmt.Errorf("key exists, %v", key))
panic(fmt.Errorf("key exists, %p", key))
}
v.used = true
v.resource = val
r.res[key] = v
}
func (r *textureCache) frame() {
func (r *resourceCache) frame() {
for k, v := range r.res {
if v.used {
v.used = false
@@ -82,7 +77,7 @@ func (r *textureCache) frame() {
}
}
func (r *textureCache) release() {
func (r *resourceCache) release() {
for _, v := range r.res {
v.resource.release()
}
+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 {
profile string
t *timers
compact *timer
render *timer
@@ -175,6 +176,7 @@ type materialUniforms struct {
type collector struct {
hasher maphash.Hash
profile bool
reader ops.Reader
states []f32.Affine2D
clear bool
@@ -595,7 +597,7 @@ func (g *compute) frame(target RenderTarget) error {
defer g.ctx.EndFrame()
t := &g.timers
if false && t.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
if g.collector.profile && t.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
t.t = newTimers(g.ctx)
t.compact = t.t.newTimer()
t.render = t.t.newTimer()
@@ -629,13 +631,13 @@ func (g *compute) frame(target RenderTarget) error {
return err
}
t.compact.end()
if false && t.t.ready() {
if g.collector.profile && t.t.ready() {
com, ren, blit := t.compact.Elapsed, t.render.Elapsed, t.blit.Elapsed
ft := com + ren + blit
q := 100 * time.Microsecond
ft = ft.Round(q)
com, ren, blit = com.Round(q), ren.Round(q), blit.Round(q)
// t.profile = fmt.Sprintf("ft:%7s com: %7s ren:%7s blit:%7s", ft, com, ren, blit)
t.profile = fmt.Sprintf("ft:%7s com: %7s ren:%7s blit:%7s", ft, com, ren, blit)
}
return nil
}
@@ -659,6 +661,10 @@ func (g *compute) dumpAtlases() {
}
}
func (g *compute) Profile() string {
return g.timers.profile
}
func (g *compute) compactAllocs() error {
const (
maxAllocAge = 3
@@ -1650,6 +1656,7 @@ func (e *encoder) line(start, end f32.Point) {
func (c *collector) reset() {
c.prevFrame, c.frame = c.frame, c.prevFrame
c.profile = false
c.clipStates = c.clipStates[:0]
c.transStack = c.transStack[:0]
c.frame.reset()
@@ -1729,6 +1736,8 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur
c.addClip(&state, fview, fview, nil, ops.Key{}, 0, 0, false)
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
switch ops.OpType(encOp.Data[0]) {
case ops.TypeProfile:
c.profile = true
case ops.TypeTransform:
dop, push := ops.DecodeTransform(encOp.Data)
if push {
+26 -14
View File
@@ -44,10 +44,14 @@ type GPU interface {
Clear(color color.NRGBA)
// Frame draws the graphics operations from op into a viewport of target.
Frame(frame *op.Ops, target RenderTarget, viewport image.Point) error
// Profile returns the last available profiling information. Profiling
// information is requested when Frame sees an io/profile.Op, and the result
// is available through Profile at some later time.
Profile() string
}
type gpu struct {
cache *textureCache
cache *resourceCache
profile string
timers *timers
@@ -69,6 +73,7 @@ type renderer struct {
}
type drawOps struct {
profile bool
reader ops.Reader
states []f32.Affine2D
transStack []f32.Affine2D
@@ -354,7 +359,7 @@ func NewWithDevice(d driver.Device) (GPU, error) {
func newGPU(ctx driver.Device) (*gpu, error) {
g := &gpu{
cache: newTextureCache(),
cache: newResourceCache(),
}
g.drawOps.pathCache = newOpCache()
if err := g.init(ctx); err != nil {
@@ -394,7 +399,7 @@ func (g *gpu) collect(viewport image.Point, frameOps *op.Ops) {
g.renderer.pather.viewport = viewport
g.drawOps.reset(viewport)
g.drawOps.collect(frameOps, viewport)
if false && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
g.frameStart = time.Now()
g.timers = newTimers(g.ctx)
g.stencilTimer = g.timers.newTimer()
@@ -420,9 +425,9 @@ func (g *gpu) frame(target RenderTarget) error {
g.stencilTimer.end()
g.coverTimer.begin()
g.renderer.uploadImages(g.cache, g.drawOps.imageOps)
g.renderer.prepareDrawOps(g.drawOps.imageOps)
g.renderer.prepareDrawOps(g.cache, g.drawOps.imageOps)
g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers)
g.renderer.drawLayers(g.drawOps.layers, g.drawOps.imageOps)
g.renderer.drawLayers(g.cache, g.drawOps.layers, g.drawOps.imageOps)
d := driver.LoadDesc{
ClearColor: g.drawOps.clearColor,
}
@@ -432,14 +437,14 @@ func (g *gpu) frame(target RenderTarget) error {
}
g.ctx.BeginRenderPass(defFBO, d)
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
g.renderer.drawOps(false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps)
g.renderer.drawOps(g.cache, false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps)
g.coverTimer.end()
g.ctx.EndRenderPass()
g.cleanupTimer.begin()
g.cache.frame()
g.drawOps.pathCache.frame()
g.cleanupTimer.end()
if false && g.timers.ready() {
if g.drawOps.profile && g.timers.ready() {
st, covt, cleant := g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
ft := st + covt + cleant
q := 100 * time.Microsecond
@@ -455,8 +460,12 @@ func (g *gpu) Profile() string {
return g.profile
}
func (r *renderer) texHandle(cache *textureCache, data imageOpData) driver.Texture {
key := textureCacheKey{
func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture {
type cachekey struct {
filter byte
handle any
}
key := cachekey{
filter: data.filter,
handle: data.handle,
}
@@ -844,7 +853,7 @@ func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer {
return layers
}
func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
func (r *renderer) drawLayers(cache *resourceCache, layers []opacityLayer, ops []imageOp) {
if len(r.layers.sizes) == 0 {
return
}
@@ -867,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)
f := r.layerFBOs.fbos[fbo]
r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
r.drawOps(cache, true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
sr := f32.FRect(v)
uvScale, uvOffset := texSpaceTransform(sr, f.size)
uvTrans := f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset)
@@ -890,6 +899,7 @@ func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
}
func (d *drawOps) reset(viewport image.Point) {
d.profile = false
d.viewport = viewport
d.imageOps = d.imageOps[:0]
d.pathOps = d.pathOps[:0]
@@ -983,6 +993,8 @@ func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) {
loop:
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
switch ops.OpType(encOp.Data[0]) {
case ops.TypeProfile:
d.profile = true
case ops.TypeTransform:
dop, push := ops.DecodeTransform(encOp.Data)
if push {
@@ -1199,7 +1211,7 @@ func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32
return m
}
func (r *renderer) uploadImages(cache *textureCache, ops []imageOp) {
func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) {
for i := range ops {
img := &ops[i]
m := img.material
@@ -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 {
m := img.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
for i := 0; i < len(ops); i++ {
img := ops[i]
+1 -1
View File
@@ -665,7 +665,7 @@ func (f *Functions) load(forceES bool) error {
case runtime.GOOS == "android":
libNames = []string{"libGLESv2.so", "libGLESv3.so"}
default:
libNames = []string{"libGLESv2.so.2", "libGLESv2.so.3.0"}
libNames = []string{"libGLESv2.so.2"}
}
for _, lib := range libNames {
if h := dlopen(lib); h != nil {
+60 -10
View File
@@ -55,19 +55,28 @@ const (
TypePopTransform
TypePushOpacity
TypePopOpacity
TypeInvalidate
TypeImage
TypePaint
TypeColor
TypeLinearGradient
TypePass
TypePopPass
TypeInput
TypeKeyInputHint
TypePointerInput
TypeClipboardRead
TypeClipboardWrite
TypeSource
TypeTarget
TypeOffer
TypeKeyInput
TypeKeyFocus
TypeKeySoftKeyboard
TypeSave
TypeLoad
TypeAux
TypeClip
TypePopClip
TypeProfile
TypeCursor
TypePath
TypeStroke
@@ -76,6 +85,8 @@ const (
TypeSemanticClass
TypeSemanticSelected
TypeSemanticEnabled
TypeSnippet
TypeSelection
TypeActionInput
)
@@ -137,13 +148,21 @@ const (
TypeLinearGradientLen = 1 + 8*2 + 4*2
TypePassLen = 1
TypePopPassLen = 1
TypeInputLen = 1
TypeKeyInputHintLen = 1 + 1
TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4
TypeClipboardReadLen = 1
TypeClipboardWriteLen = 1
TypeSourceLen = 1
TypeTargetLen = 1
TypeOfferLen = 1
TypeKeyInputLen = 1 + 1
TypeKeyFocusLen = 1 + 1
TypeKeySoftKeyboardLen = 1 + 1
TypeSaveLen = 1 + 4
TypeLoadLen = 1 + 4
TypeAuxLen = 1
TypeClipLen = 1 + 4*4 + 1 + 1
TypePopClipLen = 1
TypeProfileLen = 1
TypeCursorLen = 2
TypePathLen = 8 + 1
TypeStrokeLen = 1 + 4
@@ -152,6 +171,8 @@ const (
TypeSemanticClassLen = 2
TypeSemanticSelectedLen = 2
TypeSemanticEnabledLen = 2
TypeSnippetLen = 1 + 4 + 4
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
TypeActionInputLen = 1 + 1
)
@@ -404,19 +425,28 @@ var opProps = [0x100]opProp{
TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0},
TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0},
TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0},
TypeInvalidate: {Size: TypeRedrawLen, NumRefs: 0},
TypeImage: {Size: TypeImageLen, NumRefs: 2},
TypePaint: {Size: TypePaintLen, NumRefs: 0},
TypeColor: {Size: TypeColorLen, NumRefs: 0},
TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0},
TypePass: {Size: TypePassLen, NumRefs: 0},
TypePopPass: {Size: TypePopPassLen, NumRefs: 0},
TypeInput: {Size: TypeInputLen, NumRefs: 1},
TypeKeyInputHint: {Size: TypeKeyInputHintLen, NumRefs: 1},
TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1},
TypeClipboardRead: {Size: TypeClipboardReadLen, NumRefs: 1},
TypeClipboardWrite: {Size: TypeClipboardWriteLen, NumRefs: 1},
TypeSource: {Size: TypeSourceLen, NumRefs: 2},
TypeTarget: {Size: TypeTargetLen, NumRefs: 2},
TypeOffer: {Size: TypeOfferLen, NumRefs: 3},
TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2},
TypeKeyFocus: {Size: TypeKeyFocusLen, NumRefs: 1},
TypeKeySoftKeyboard: {Size: TypeKeySoftKeyboardLen, NumRefs: 0},
TypeSave: {Size: TypeSaveLen, NumRefs: 0},
TypeLoad: {Size: TypeLoadLen, NumRefs: 0},
TypeAux: {Size: TypeAuxLen, NumRefs: 0},
TypeClip: {Size: TypeClipLen, NumRefs: 0},
TypePopClip: {Size: TypePopClipLen, NumRefs: 0},
TypeProfile: {Size: TypeProfileLen, NumRefs: 1},
TypeCursor: {Size: TypeCursorLen, NumRefs: 0},
TypePath: {Size: TypePathLen, NumRefs: 0},
TypeStroke: {Size: TypeStrokeLen, NumRefs: 0},
@@ -425,6 +455,8 @@ var opProps = [0x100]opProp{
TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0},
TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0},
TypeSemanticEnabled: {Size: TypeSemanticEnabledLen, NumRefs: 0},
TypeSnippet: {Size: TypeSnippetLen, NumRefs: 2},
TypeSelection: {Size: TypeSelectionLen, NumRefs: 1},
TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0},
}
@@ -457,6 +489,8 @@ func (t OpType) String() string {
return "PushOpacity"
case TypePopOpacity:
return "PopOpacity"
case TypeInvalidate:
return "Invalidate"
case TypeImage:
return "Image"
case TypePaint:
@@ -469,10 +503,24 @@ func (t OpType) String() string {
return "Pass"
case TypePopPass:
return "PopPass"
case TypeInput:
return "Input"
case TypeKeyInputHint:
return "KeyInputHint"
case TypePointerInput:
return "PointerInput"
case TypeClipboardRead:
return "ClipboardRead"
case TypeClipboardWrite:
return "ClipboardWrite"
case TypeSource:
return "Source"
case TypeTarget:
return "Target"
case TypeOffer:
return "Offer"
case TypeKeyInput:
return "KeyInput"
case TypeKeyFocus:
return "KeyFocus"
case TypeKeySoftKeyboard:
return "KeySoftKeyboard"
case TypeSave:
return "Save"
case TypeLoad:
@@ -483,6 +531,8 @@ func (t OpType) String() string {
return "Clip"
case TypePopClip:
return "PopClip"
case TypeProfile:
return "Profile"
case TypeCursor:
return "Cursor"
case TypePath:
+24 -11
View File
@@ -3,22 +3,35 @@
package clipboard
import (
"io"
"gioui.org/internal/ops"
"gioui.org/io/event"
"gioui.org/op"
)
// WriteCmd copies Text to the clipboard.
type WriteCmd struct {
Type string
Data io.ReadCloser
// Event is generated when the clipboard content is requested.
type Event struct {
Text string
}
// ReadCmd requests the text of the clipboard, delivered to
// the handler through an [io/transfer.DataEvent].
type ReadCmd struct {
// ReadOp requests the text of the clipboard, delivered to
// the current handler through an Event.
type ReadOp struct {
Tag event.Tag
}
func (WriteCmd) ImplementsCommand() {}
func (ReadCmd) ImplementsCommand() {}
// WriteOp copies Text to the clipboard.
type WriteOp struct {
Text string
}
func (h ReadOp) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeClipboardReadLen, h.Tag)
data[0] = byte(ops.TypeClipboardRead)
}
func (h WriteOp) Add(o *op.Ops) {
data := ops.Write1String(&o.Internal, ops.TypeClipboardWriteLen, h.Text)
data[0] = byte(ops.TypeClipboardWrite)
}
func (Event) ImplementsEvent() {}
+34 -20
View File
@@ -1,12 +1,41 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package event contains types for event handling.
/*
Package event contains the types for event handling.
The Queue interface is the protocol for receiving external events.
For example:
var queue event.Queue = ...
for _, e := range queue.Events(h) {
switch e.(type) {
...
}
}
In general, handlers must be declared before events become
available. Other packages such as pointer and key provide
the means for declaring handlers for specific event types.
The following example declares a handler ready for key input:
import gioui.org/io/key
ops := new(op.Ops)
var h *Handler = ...
key.InputOp{Tag: h, Filter: ...}.Add(ops)
*/
package event
import (
"gioui.org/internal/ops"
"gioui.org/op"
)
// Queue maps an event handler key to the events
// available to the handler.
type Queue interface {
// Events returns the available events for an
// event handler tag.
Events(t Tag) []Event
}
// Tag is the stable identifier for an event handler.
// For a handler h, the tag is typically &h.
@@ -16,18 +45,3 @@ type Tag interface{}
type Event interface {
ImplementsEvent()
}
// Filter represents a filter for [Event] types.
type Filter interface {
ImplementsFilter()
}
// Op declares a tag for input routing at the current transformation
// and clip area hierarchy. It panics if tag is nil.
func Op(o *op.Ops, tag Tag) {
if tag == nil {
panic("Tag must be non-nil")
}
data := ops.Write1(&o.Internal, ops.TypeInputLen, tag)
data[0] = byte(ops.TypeInput)
}
-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
// Package key implements key and text events and operations.
/*
Package key implements key and text events and operations.
The InputOp operations is used for declaring key input handlers. Use
an implementation of the Queue interface from package ui to receive
events.
*/
package key
import (
"encoding/binary"
"fmt"
"math"
"strings"
"gioui.org/f32"
@@ -12,40 +21,59 @@ import (
"gioui.org/op"
)
// Filter matches any [Event] that matches the parameters.
type Filter struct {
// Focus is the tag that must be focused for the filter to match. It has no effect
// if it is nil.
Focus event.Tag
// Required is the set of modifiers that must be included in events matched.
Required Modifiers
// Optional is the set of modifiers that may be included in events matched.
Optional Modifiers
// Name of the key to be matched. As a special case, the empty
// Name matches every key not matched by any other filter.
Name Name
}
// InputHintOp describes the type of text expected by a tag.
type InputHintOp struct {
Tag event.Tag
// InputOp declares a handler ready for key events.
// Key events are in general only delivered to the
// focused key handler.
type InputOp struct {
Tag event.Tag
// Hint describes the type of text expected by Tag.
Hint InputHint
// Keys is the set of keys Tag can handle. That is, Tag will only
// receive an Event if its key and modifiers are accepted by Keys.Contains.
// As a special case, the topmost (first added) InputOp handler receives all
// unhandled events.
Keys Set
}
// SoftKeyboardCmd shows or hides the on-screen keyboard, if available.
type SoftKeyboardCmd struct {
// Set is an expression that describes a set of key combinations, in the form
// "<modifiers>-<keyset>|...". Modifiers are separated by dashes, optional
// modifiers are enclosed by parentheses. A key set is either a literal key
// name or a list of key names separated by commas and enclosed in brackets.
//
// The "Short" modifier matches the shortcut modifier (ModShortcut) and
// "ShortAlt" matches the alternative modifier (ModShortcutAlt).
//
// Examples:
//
// - A|B matches the A and B keys
// - [A,B] also matches the A and B keys
// - Shift-A matches A key if shift is pressed, and no other modifier.
// - Shift-(Ctrl)-A matches A if shift is pressed, and optionally ctrl.
type Set string
// SoftKeyboardOp shows or hide the on-screen keyboard, if available.
// It replaces any previous SoftKeyboardOp.
type SoftKeyboardOp struct {
Show bool
}
// SelectionCmd updates the selection for an input handler.
type SelectionCmd struct {
// FocusOp sets or clears the keyboard focus. It replaces any previous
// FocusOp in the same frame.
type FocusOp struct {
// Tag is the new focus. The focus is cleared if Tag is nil, or if Tag
// has no InputOp in the same frame.
Tag event.Tag
}
// SelectionOp updates the selection for an input handler.
type SelectionOp struct {
Tag event.Tag
Range
Caret
}
// SnippetCmd updates the content snippet for an input handler.
type SnippetCmd struct {
// SnippetOp updates the content snippet for an input handler.
type SnippetOp struct {
Tag event.Tag
Snippet
}
@@ -90,8 +118,11 @@ type FocusEvent struct {
// An Event is generated when a key is pressed. For text input
// use EditEvent.
type Event struct {
// Name of the key.
Name Name
// Name of the key. For letters, the upper case form is used, via
// unicode.ToUpper. The shift modifier is taken into account, all other
// modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1"
// combinations both give the Name "!" with the US keyboard layout.
Name string
// Modifiers is the set of active modifiers when the key was pressed.
Modifiers Modifiers
// State is the state of the key when the event was fired.
@@ -105,13 +136,6 @@ type EditEvent struct {
Text string
}
// FocusFilter matches any [FocusEvent], [EditEvent], [SnippetEvent],
// or [SelectionEvent] with the specified target.
type FocusFilter struct {
// Target is a tag specified in a previous event.Op.
Target event.Tag
}
// InputHint changes the on-screen-keyboard type. That hints the
// type of data that might be entered by the user.
type InputHint uint8
@@ -165,60 +189,41 @@ const (
ModSuper
)
// Name is the identifier for a keyboard key.
//
// For letters, the upper case form is used, via unicode.ToUpper.
// The shift modifier is taken into account, all other
// modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1"
// combinations both give the Name "!" with the US keyboard layout.
type Name string
const (
// Names for special keys.
NameLeftArrow Name = "←"
NameRightArrow Name = "→"
NameUpArrow Name = "↑"
NameDownArrow Name = "↓"
NameReturn Name = "⏎"
NameEnter Name = "⌤"
NameEscape Name = "⎋"
NameHome Name = "⇱"
NameEnd Name = "⇲"
NameDeleteBackward Name = "⌫"
NameDeleteForward Name = "⌦"
NamePageUp Name = "⇞"
NamePageDown Name = "⇟"
NameTab Name = "Tab"
NameSpace Name = "Space"
NameCtrl Name = "Ctrl"
NameShift Name = "Shift"
NameAlt Name = "Alt"
NameSuper Name = "Super"
NameCommand Name = "⌘"
NameF1 Name = "F1"
NameF2 Name = "F2"
NameF3 Name = "F3"
NameF4 Name = "F4"
NameF5 Name = "F5"
NameF6 Name = "F6"
NameF7 Name = "F7"
NameF8 Name = "F8"
NameF9 Name = "F9"
NameF10 Name = "F10"
NameF11 Name = "F11"
NameF12 Name = "F12"
NameBack Name = "Back"
)
type FocusDirection int
const (
FocusRight FocusDirection = iota
FocusLeft
FocusUp
FocusDown
FocusForward
FocusBackward
NameLeftArrow = "←"
NameRightArrow = "→"
NameUpArrow = "↑"
NameDownArrow = "↓"
NameReturn = "⏎"
NameEnter = "⌤"
NameEscape = "⎋"
NameHome = "⇱"
NameEnd = "⇲"
NameDeleteBackward = "⌫"
NameDeleteForward = "⌦"
NamePageUp = "⇞"
NamePageDown = "⇟"
NameTab = "Tab"
NameSpace = "Space"
NameCtrl = "Ctrl"
NameShift = "Shift"
NameAlt = "Alt"
NameSuper = "Super"
NameCommand = "⌘"
NameF1 = "F1"
NameF2 = "F2"
NameF3 = "F3"
NameF4 = "F4"
NameF5 = "F5"
NameF6 = "F6"
NameF7 = "F7"
NameF8 = "F8"
NameF9 = "F9"
NameF10 = "F10"
NameF11 = "F11"
NameF12 = "F12"
NameBack = "Back"
)
// Contain reports whether m contains all modifiers
@@ -227,52 +232,161 @@ func (m Modifiers) Contain(m2 Modifiers) bool {
return m&m2 == m2
}
// FocusCmd requests to set or clear the keyboard focus.
type FocusCmd struct {
// Tag is the new focus. The focus is cleared if Tag is nil, or if Tag
// has no [event.Op] references.
Tag event.Tag
func (k Set) Contains(name string, mods Modifiers) bool {
ks := string(k)
for len(ks) > 0 {
// Cut next key expression.
chord, rest, _ := cut(ks, "|")
ks = rest
// Separate key set and modifier set.
var modSet, keySet string
sep := strings.LastIndex(chord, "-")
if sep != -1 {
modSet, keySet = chord[:sep], chord[sep+1:]
} else {
modSet, keySet = "", chord
}
if !keySetContains(keySet, name) {
continue
}
if modSetContains(modSet, mods) {
return true
}
}
return false
}
func (h InputHintOp) Add(o *op.Ops) {
func keySetContains(keySet, name string) bool {
// Check for single key match.
if keySet == name {
return true
}
// Check for set match.
if len(keySet) < 2 || keySet[0] != '[' || keySet[len(keySet)-1] != ']' {
return false
}
keySet = keySet[1 : len(keySet)-1]
for len(keySet) > 0 {
key, rest, _ := cut(keySet, ",")
keySet = rest
if key == name {
return true
}
}
return false
}
func modSetContains(modSet string, mods Modifiers) bool {
var smods Modifiers
for len(modSet) > 0 {
mod, rest, _ := cut(modSet, "-")
modSet = rest
if len(mod) >= 2 && mod[0] == '(' && mod[len(mod)-1] == ')' {
mods &^= modFor(mod[1 : len(mod)-1])
} else {
smods |= modFor(mod)
}
}
return mods == smods
}
// cut is a copy of the standard library strings.Cut.
// TODO: remove when Go 1.18 is our minimum.
func cut(s, sep string) (before, after string, found bool) {
if i := strings.Index(s, sep); i >= 0 {
return s[:i], s[i+len(sep):], true
}
return s, "", false
}
func modFor(name string) Modifiers {
switch name {
case NameCtrl:
return ModCtrl
case NameShift:
return ModShift
case NameAlt:
return ModAlt
case NameSuper:
return ModSuper
case NameCommand:
return ModCommand
case "Short":
return ModShortcut
case "ShortAlt":
return ModShortcutAlt
}
return 0
}
func (h InputOp) Add(o *op.Ops) {
if h.Tag == nil {
panic("Tag must be non-nil")
}
data := ops.Write1(&o.Internal, ops.TypeKeyInputHintLen, h.Tag)
data[0] = byte(ops.TypeKeyInputHint)
data := ops.Write2String(&o.Internal, ops.TypeKeyInputLen, h.Tag, string(h.Keys))
data[0] = byte(ops.TypeKeyInput)
data[1] = byte(h.Hint)
}
func (h SoftKeyboardOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeKeySoftKeyboardLen)
data[0] = byte(ops.TypeKeySoftKeyboard)
if h.Show {
data[1] = 1
}
}
func (h FocusOp) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeKeyFocusLen, h.Tag)
data[0] = byte(ops.TypeKeyFocus)
}
func (s SnippetOp) Add(o *op.Ops) {
data := ops.Write2String(&o.Internal, ops.TypeSnippetLen, s.Tag, s.Text)
data[0] = byte(ops.TypeSnippet)
bo := binary.LittleEndian
bo.PutUint32(data[1:], uint32(s.Range.Start))
bo.PutUint32(data[5:], uint32(s.Range.End))
}
func (s SelectionOp) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeSelectionLen, s.Tag)
data[0] = byte(ops.TypeSelection)
bo := binary.LittleEndian
bo.PutUint32(data[1:], uint32(s.Start))
bo.PutUint32(data[5:], uint32(s.End))
bo.PutUint32(data[9:], math.Float32bits(s.Pos.X))
bo.PutUint32(data[13:], math.Float32bits(s.Pos.Y))
bo.PutUint32(data[17:], math.Float32bits(s.Ascent))
bo.PutUint32(data[21:], math.Float32bits(s.Descent))
}
func (EditEvent) ImplementsEvent() {}
func (Event) ImplementsEvent() {}
func (FocusEvent) ImplementsEvent() {}
func (SnippetEvent) ImplementsEvent() {}
func (SelectionEvent) ImplementsEvent() {}
func (FocusCmd) ImplementsCommand() {}
func (SoftKeyboardCmd) ImplementsCommand() {}
func (SelectionCmd) ImplementsCommand() {}
func (SnippetCmd) ImplementsCommand() {}
func (Filter) ImplementsFilter() {}
func (FocusFilter) ImplementsFilter() {}
func (e Event) String() string {
return fmt.Sprintf("%v %v %v}", e.Name, e.Modifiers, e.State)
}
func (m Modifiers) String() string {
var strs []string
if m.Contain(ModCtrl) {
strs = append(strs, string(NameCtrl))
strs = append(strs, NameCtrl)
}
if m.Contain(ModCommand) {
strs = append(strs, string(NameCommand))
strs = append(strs, NameCommand)
}
if m.Contain(ModShift) {
strs = append(strs, string(NameShift))
strs = append(strs, NameShift)
}
if m.Contain(ModAlt) {
strs = append(strs, string(NameAlt))
strs = append(strs, NameAlt)
}
if m.Contain(ModSuper) {
strs = append(strs, string(NameSuper))
strs = append(strs, NameSuper)
}
return strings.Join(strs, "-")
}
+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
object such as a finger.
The [event.Op] operation is used to declare a handler ready for pointer
events.
The InputOp operation is used to declare a handler ready for pointer
events. Use an event.Queue to receive events.
# Kinds
Only events that match a specified list of types are delivered to a handler.
For example, to receive Press, Drag, and Release events (but not Move, Enter,
Leave, or Scroll):
var ops op.Ops
var h *Handler = ...
pointer.InputOp{
Tag: h,
Kinds: pointer.Press | pointer.Drag | pointer.Release,
}.Add(ops)
Cancel events are always delivered.
# Hit areas
Clip operations from package [op/clip] are used for specifying
hit areas where handlers may receive events.
Clip operations from package op/clip are used for specifying
hit areas where subsequent InputOps are active.
For example, to set up a handler with a rectangular hit area:
r := image.Rectangle{...}
area := clip.Rect(r).Push(ops)
event.Op{Tag: h}.Add(ops)
pointer.InputOp{Tag: h}.Add(ops)
area.Pop()
Note that hit areas behave similar to painting: the effective area of a stack
@@ -37,11 +54,11 @@ For example:
var h1, h2 *Handler
area := clip.Rect(...).Push(ops)
event.Op{Tag: h1}.Add(Ops)
pointer.InputOp{Tag: h1}.Add(Ops)
area.Pop()
area := clip.Rect(...).Push(ops)
event.Op{Tag: h2}.Add(ops)
pointer.InputOp{Tag: h2}.Add(ops)
area.Pop()
implies a tree of two inner nodes, each with one pointer handler attached.
+35 -17
View File
@@ -3,6 +3,8 @@
package pointer
import (
"encoding/binary"
"fmt"
"image"
"strings"
"time"
@@ -54,12 +56,14 @@ type PassStack struct {
macroID uint32
}
// Filter matches every [Event] that target the Tag and whose kind is
// included in Kinds. Note that only tags specified in [event.Op] can
// be targeted by pointer events.
type Filter struct {
Target event.Tag
// Kinds is a bitwise-or of event types to match.
// InputOp declares an input handler ready for pointer
// events.
type InputOp struct {
Tag event.Tag
// Grab, if set, request that the handler get
// Grabbed priority.
Grab bool
// Kinds is a bitwise-or of event types to receive.
Kinds Kind
// ScrollBounds describe the maximum scrollable distances in both
// axes. Specifically, any Event e delivered to Tag will satisfy
@@ -69,12 +73,6 @@ type Filter struct {
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
// Kind of an Event.
@@ -173,7 +171,7 @@ const (
const (
// A Cancel event is generated when the current gesture is
// interrupted by other handlers or the system.
Cancel Kind = 1 << iota
Cancel Kind = (1 << iota) >> 1
// Press of a pointer.
Press
// Release of a pointer.
@@ -239,6 +237,30 @@ func (op Cursor) Add(o *op.Ops) {
data[1] = byte(op)
}
// Add panics if the scroll range does not contain zero.
func (op InputOp) Add(o *op.Ops) {
if op.Tag == nil {
panic("Tag must be non-nil")
}
if b := op.ScrollBounds; b.Min.X > 0 || b.Max.X < 0 || b.Min.Y > 0 || b.Max.Y < 0 {
panic(fmt.Errorf("invalid scroll range value %v", b))
}
if op.Kinds>>16 > 0 {
panic(fmt.Errorf("value in Types overflows uint16"))
}
data := ops.Write1(&o.Internal, ops.TypePointerInputLen, op.Tag)
data[0] = byte(ops.TypePointerInput)
if op.Grab {
data[1] = 1
}
bo := binary.LittleEndian
bo.PutUint16(data[2:], uint16(op.Kinds))
bo.PutUint32(data[4:], uint32(op.ScrollBounds.Min.X))
bo.PutUint32(data[8:], uint32(op.ScrollBounds.Min.Y))
bo.PutUint32(data[12:], uint32(op.ScrollBounds.Max.X))
bo.PutUint32(data[16:], uint32(op.ScrollBounds.Max.Y))
}
func (t Kind) String() string {
if t == Cancel {
return "Cancel"
@@ -382,7 +404,3 @@ func (c Cursor) String() string {
}
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
package input
package router
import (
"image"
@@ -10,6 +10,7 @@ import (
f32internal "gioui.org/internal/f32"
"gioui.org/internal/ops"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/io/system"
@@ -17,8 +18,14 @@ import (
)
type pointerQueue struct {
hitTree []hitNode
areas []areaNode
hitTree []hitNode
areas []areaNode
cursor pointer.Cursor
handlers map[event.Tag]*pointerHandler
pointers []pointerInfo
transfers []io.ReadCloser // pending data transfers
scratch []event.Tag
semantic struct {
idsAssigned bool
@@ -36,15 +43,10 @@ type hitNode struct {
// For handler nodes.
tag event.Tag
ktag event.Tag
pass bool
}
// pointerState is the input state related to pointer events.
type pointerState struct {
cursor pointer.Cursor
pointers []pointerInfo
}
type pointerInfo struct {
id pointer.ID
pressed bool
@@ -61,21 +63,17 @@ type pointerInfo struct {
}
type pointerHandler struct {
// areaPlusOne is the index into the list of pointerQueue.areas, plus 1.
areaPlusOne int
// setup tracks whether the handler has received
// the pointer.Cancel event that resets its state.
setup bool
}
// pointerFilter represents the union of a set of pointer filters.
type pointerFilter struct {
kinds pointer.Kind
area int
active bool
wantsGrab bool
types pointer.Kind
// min and max horizontal/vertical scroll
scrollRange image.Rectangle
sourceMimes []string
targetMimes []string
offeredMime string
data io.ReadCloser
}
type areaOp struct {
@@ -231,18 +229,33 @@ func (c *pointerCollector) addHitNode(n hitNode) {
}
// newHandler returns the current handler or a new one for tag.
func (c *pointerCollector) newHandler(tag event.Tag, state *pointerHandler) {
func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *pointerHandler {
areaID := c.currentArea()
c.addHitNode(hitNode{
area: areaID,
tag: tag,
pass: c.state.pass > 0,
})
state.areaPlusOne = areaID + 1
h, ok := c.q.handlers[tag]
if !ok {
h = new(pointerHandler)
c.q.handlers[tag] = h
// Cancel handlers on (each) first appearance, but don't
// trigger redraw.
events.AddNoRedraw(tag, pointer.Event{Kind: pointer.Cancel})
}
h.active = true
h.area = areaID
return h
}
func (s *pointerHandler) Reset() {
s.areaPlusOne = 0
func (c *pointerCollector) keyInputOp(op key.InputOp) {
areaID := c.currentArea()
c.addHitNode(hitNode{
area: areaID,
ktag: op.Tag,
pass: true,
})
}
func (c *pointerCollector) actionInputOp(act system.Action) {
@@ -251,109 +264,21 @@ func (c *pointerCollector) actionInputOp(act system.Action) {
area.action = act
}
func (q *pointerQueue) grab(state pointerState, req pointer.GrabCmd) (pointerState, []taggedEvent) {
var evts []taggedEvent
for _, p := range state.pointers {
if !p.pressed || p.id != req.ID {
continue
}
// Drop other handlers that lost their grab.
for i := len(p.handlers) - 1; i >= 0; i-- {
if tag := p.handlers[i]; tag != req.Tag {
evts = append(evts, taggedEvent{
tag: tag,
event: pointer.Event{Kind: pointer.Cancel},
})
state = dropHandler(state, tag)
}
}
break
}
return state, evts
}
func (c *pointerCollector) inputOp(tag event.Tag, state *pointerHandler) {
func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
area.semantic.content.tag = tag
c.newHandler(tag, state)
}
func (p *pointerFilter) Add(f event.Filter) {
switch f := f.(type) {
case transfer.SourceFilter:
for _, m := range p.sourceMimes {
if m == f.Type {
return
}
}
p.sourceMimes = append(p.sourceMimes, f.Type)
case transfer.TargetFilter:
for _, m := range p.targetMimes {
if m == f.Type {
return
}
}
p.targetMimes = append(p.targetMimes, f.Type)
case pointer.Filter:
p.kinds = p.kinds | f.Kinds
p.scrollRange = p.scrollRange.Union(f.ScrollBounds)
area.semantic.content.tag = op.Tag
if op.Kinds&(pointer.Press|pointer.Release) != 0 {
area.semantic.content.gestures |= ClickGesture
}
}
func (p *pointerFilter) Matches(e event.Event) bool {
switch e := e.(type) {
case pointer.Event:
return e.Kind&p.kinds == e.Kind
case transfer.CancelEvent, transfer.InitiateEvent:
return len(p.sourceMimes) > 0 || len(p.targetMimes) > 0
case transfer.RequestEvent:
for _, t := range p.sourceMimes {
if t == e.Type {
return true
}
}
case transfer.DataEvent:
for _, t := range p.targetMimes {
if t == e.Type {
return true
}
}
if op.Kinds&pointer.Scroll != 0 {
area.semantic.content.gestures |= ScrollGesture
}
return false
}
func (p *pointerFilter) Merge(p2 pointerFilter) {
p.kinds = p.kinds | p2.kinds
p.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
area.semantic.valid = area.semantic.content.gestures != 0
h := c.newHandler(op.Tag, events)
h.wantsGrab = h.wantsGrab || op.Grab
h.types = h.types | op.Kinds
h.scrollRange = op.ScrollBounds
}
func (c *pointerCollector) semanticLabel(lbl string) {
@@ -397,28 +322,23 @@ func (c *pointerCollector) cursor(cursor pointer.Cursor) {
area.cursor = cursor
}
func (q *pointerQueue) offerData(handlers map[event.Tag]*handler, state pointerState, req transfer.OfferCmd) (pointerState, []taggedEvent) {
var evts []taggedEvent
for i, p := range state.pointers {
if p.dataSource != req.Tag {
continue
}
if p.dataTarget != nil {
evts = append(evts, taggedEvent{tag: p.dataTarget, event: transfer.DataEvent{
Type: req.Type,
Open: func() io.ReadCloser {
return req.Data
},
}})
}
state.pointers = append([]pointerInfo{}, state.pointers...)
state.pointers[i], evts = q.deliverTransferCancelEvent(handlers, p, evts)
break
}
return state, evts
func (c *pointerCollector) sourceOp(op transfer.SourceOp, events *handlerEvents) {
h := c.newHandler(op.Tag, events)
h.sourceMimes = append(h.sourceMimes, op.Type)
}
func (c *pointerCollector) Reset() {
func (c *pointerCollector) targetOp(op transfer.TargetOp, events *handlerEvents) {
h := c.newHandler(op.Tag, events)
h.targetMimes = append(h.targetMimes, op.Type)
}
func (c *pointerCollector) offerOp(op transfer.OfferOp, events *handlerEvents) {
h := c.newHandler(op.Tag, events)
h.offeredMime = op.Type
h.data = op.Data
}
func (c *pointerCollector) reset() {
c.q.reset()
c.resetState()
c.ensureRoot()
@@ -573,6 +493,20 @@ func (q *pointerQueue) hitTest(pos f32.Point, onNode func(*hitNode) bool) pointe
return cursor
}
func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
hits := q.scratch[:0]
cursor := q.hitTest(pos, func(n *hitNode) bool {
if n.tag != nil {
if _, exists := q.handlers[n.tag]; exists {
hits = addHandler(hits, n.tag)
}
}
return true
})
q.scratch = hits[:0]
return hits, cursor
}
func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point {
if areaIdx == -1 {
return p
@@ -597,6 +531,17 @@ func (q *pointerQueue) hit(areaIdx int, p f32.Point) (bool, pointer.Cursor) {
}
func (q *pointerQueue) reset() {
if q.handlers == nil {
q.handlers = make(map[event.Tag]*pointerHandler)
}
for _, h := range q.handlers {
// Reset handler.
h.active = false
h.wantsGrab = false
h.types = 0
h.sourceMimes = h.sourceMimes[:0]
h.targetMimes = h.targetMimes[:0]
}
q.hitTree = q.hitTree[:0]
q.areas = q.areas[:0]
q.semantic.idsAssigned = false
@@ -614,71 +559,80 @@ func (q *pointerQueue) reset() {
delete(q.semantic.contentIDs, k)
}
}
for _, rc := range q.transfers {
if rc != nil {
rc.Close()
}
}
q.transfers = nil
}
func (q *pointerQueue) Frame(handlers map[event.Tag]*handler, state pointerState) (pointerState, []taggedEvent) {
for _, h := range handlers {
if h.pointer.areaPlusOne != 0 {
area := &q.areas[h.pointer.areaPlusOne-1]
if h.filter.pointer.kinds&(pointer.Press|pointer.Release) != 0 {
area.semantic.content.gestures |= ClickGesture
func (q *pointerQueue) Frame(events *handlerEvents) {
for k, h := range q.handlers {
if !h.active {
q.dropHandler(nil, k)
delete(q.handlers, k)
}
if h.wantsGrab {
for _, p := range q.pointers {
if !p.pressed {
continue
}
for i, k2 := range p.handlers {
if k2 == k {
// Drop other handlers that lost their grab.
dropped := q.scratch[:0]
dropped = append(dropped, p.handlers[:i]...)
dropped = append(dropped, p.handlers[i+1:]...)
for _, tag := range dropped {
q.dropHandler(events, tag)
}
break
}
}
}
if h.filter.pointer.kinds&pointer.Scroll != 0 {
area.semantic.content.gestures |= ScrollGesture
}
area.semantic.valid = area.semantic.content.gestures != 0
}
}
var evts []taggedEvent
for i, p := range state.pointers {
changed := false
p, evts, state.cursor, changed = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, p.last)
if changed {
state.pointers = append([]pointerInfo{}, state.pointers...)
state.pointers[i] = p
}
for i := range q.pointers {
p := &q.pointers[i]
q.deliverEnterLeaveEvents(p, events, p.last)
q.deliverTransferDataEvent(p, events)
}
return state, evts
}
func dropHandler(state pointerState, tag event.Tag) pointerState {
pointers := state.pointers
state.pointers = nil
for _, p := range pointers {
handlers := p.handlers
p.handlers = nil
for _, h := range handlers {
if h != tag {
p.handlers = append(p.handlers, h)
}
}
entered := p.entered
p.entered = nil
for _, h := range entered {
if h != tag {
p.entered = append(p.entered, h)
}
}
state.pointers = append(state.pointers, p)
func (q *pointerQueue) dropHandler(events *handlerEvents, tag event.Tag) {
if events != nil {
events.Add(tag, pointer.Event{Kind: pointer.Cancel})
}
for i := range q.pointers {
p := &q.pointers[i]
for i := len(p.handlers) - 1; i >= 0; i-- {
if p.handlers[i] == tag {
p.handlers = append(p.handlers[:i], p.handlers[i+1:]...)
}
}
for i := len(p.entered) - 1; i >= 0; i-- {
if p.entered[i] == tag {
p.entered = append(p.entered[:i], p.entered[i+1:]...)
}
}
}
return state
}
// pointerOf returns the pointerInfo index corresponding to the pointer in e.
func (s pointerState) pointerOf(e pointer.Event) (pointerState, int) {
for i, p := range s.pointers {
func (q *pointerQueue) pointerOf(e pointer.Event) int {
for i, p := range q.pointers {
if p.id == e.PointerID {
return s, i
return i
}
}
n := len(s.pointers)
s.pointers = append(s.pointers[:n:n], pointerInfo{id: e.PointerID})
return s, len(s.pointers) - 1
q.pointers = append(q.pointers, pointerInfo{id: e.PointerID})
return len(q.pointers) - 1
}
// Deliver is like Push, but delivers an event to a particular area.
func (q *pointerQueue) Deliver(handlers map[event.Tag]*handler, areaIdx int, e pointer.Event) []taggedEvent {
scroll := e.Scroll
func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEvents) {
var sx, sy = e.Scroll.X, e.Scroll.Y
idx := len(q.hitTree) - 1
// Locate first potential receiver.
for idx != -1 {
@@ -688,28 +642,31 @@ func (q *pointerQueue) Deliver(handlers map[event.Tag]*handler, areaIdx int, e p
}
idx--
}
var evts []taggedEvent
for idx != -1 {
n := &q.hitTree[idx]
idx = n.next
h, ok := handlers[n.tag]
if !ok || !h.filter.pointer.Matches(e) {
if n.tag == nil {
continue
}
h := q.handlers[n.tag]
if e.Kind&h.types == 0 {
continue
}
e := e
if e.Kind == pointer.Scroll {
if scroll == (f32.Point{}) {
if sx == 0 && sy == 0 {
break
}
scroll, e.Scroll = h.filter.pointer.clampScroll(scroll)
// Distribute the scroll to the handler based on its ScrollRange.
sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X, h.scrollRange.Max.X)
sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y, h.scrollRange.Max.Y)
}
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
evts = append(evts, taggedEvent{tag: n.tag, event: e})
e.Position = q.invTransform(h.area, e.Position)
events.Add(n.tag, e)
if e.Kind != pointer.Scroll {
break
}
}
return evts
}
// SemanticArea returns the sematic content for area, and its parent area.
@@ -725,129 +682,106 @@ func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) {
return semanticContent{}, -1
}
func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState, e pointer.Event) (pointerState, []taggedEvent) {
var evts []taggedEvent
func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
if e.Kind == pointer.Cancel {
for k := range handlers {
evts = append(evts, taggedEvent{
event: pointer.Event{Kind: pointer.Cancel},
tag: k,
})
q.pointers = q.pointers[:0]
for k := range q.handlers {
q.dropHandler(events, k)
}
state.pointers = nil
return state, evts
return
}
state, pidx := state.pointerOf(e)
p := state.pointers[pidx]
pidx := q.pointerOf(e)
p := &q.pointers[pidx]
p.last = e
switch e.Kind {
case pointer.Press:
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
q.deliverEnterLeaveEvents(p, events, e)
p.pressed = true
evts = q.deliverEvent(handlers, p, evts, e)
q.deliverEvent(p, events, e)
case pointer.Move:
if p.pressed {
e.Kind = pointer.Drag
}
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
evts = q.deliverEvent(handlers, p, evts, e)
q.deliverEnterLeaveEvents(p, events, e)
q.deliverEvent(p, events, e)
if p.pressed {
p, evts = q.deliverDragEvent(handlers, p, evts)
q.deliverDragEvent(p, events)
}
case pointer.Release:
evts = q.deliverEvent(handlers, p, evts, e)
q.deliverEvent(p, events, e)
p.pressed = false
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
p, evts = q.deliverDropEvent(handlers, p, evts)
q.deliverEnterLeaveEvents(p, events, e)
q.deliverDropEvent(p, events)
case pointer.Scroll:
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
evts = q.deliverEvent(handlers, p, evts, e)
q.deliverEnterLeaveEvents(p, events, e)
q.deliverEvent(p, events, e)
default:
panic("unsupported pointer event type")
}
p.last = e
if !p.pressed && len(p.entered) == 0 {
// No longer need to track pointer.
state.pointers = append(state.pointers[:pidx:pidx], state.pointers[pidx+1:]...)
} else {
state.pointers = append([]pointerInfo{}, state.pointers...)
state.pointers[pidx] = p
q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...)
}
return state, evts
}
func (q *pointerQueue) deliverEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent, e pointer.Event) []taggedEvent {
func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e pointer.Event) {
foremost := true
if p.pressed && len(p.handlers) == 1 {
e.Priority = pointer.Grabbed
foremost = false
}
scroll := e.Scroll
var sx, sy = e.Scroll.X, e.Scroll.Y
for _, k := range p.handlers {
h, ok := handlers[k]
if !ok {
continue
}
f := h.filter.pointer
if !f.Matches(e) {
continue
}
h := q.handlers[k]
if e.Kind == pointer.Scroll {
if scroll == (f32.Point{}) {
return evts
if sx == 0 && sy == 0 {
return
}
scroll, e.Scroll = f.clampScroll(scroll)
// Distribute the scroll to the handler based on its ScrollRange.
sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X, h.scrollRange.Max.X)
sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y, h.scrollRange.Max.Y)
}
if e.Kind&h.types == 0 {
continue
}
e := e
if foremost {
foremost = false
e.Priority = pointer.Foremost
}
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
evts = append(evts, taggedEvent{event: e, tag: k})
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
}
return evts
}
func (q *pointerQueue) deliverEnterLeaveEvents(handlers map[event.Tag]*handler, cursor pointer.Cursor, p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent, pointer.Cursor, bool) {
changed := false
func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) {
var hits []event.Tag
if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press {
// Consider non-mouse pointers leaving when they're released.
} else {
var transSrc *pointerFilter
if p.dataSource != nil {
transSrc = &handlers[p.dataSource].filter.pointer
}
cursor = q.hitTest(e.Position, func(n *hitNode) bool {
h, ok := handlers[n.tag]
if !ok {
return true
}
add := true
if p.pressed {
add = false
// Filter out non-participating handlers,
// except potential transfer targets when a transfer has been initiated.
if _, found := searchTag(p.handlers, n.tag); found {
add = true
}
if transSrc != nil {
if _, ok := firstMimeMatch(transSrc, &h.filter.pointer); ok {
add = true
hits, q.cursor = q.opHit(e.Position)
if p.pressed {
// Filter out non-participating handlers,
// except potential transfer targets when a transfer has been initiated.
var hitsHaveTarget bool
if p.dataSource != nil {
transferSource := q.handlers[p.dataSource]
for _, hit := range hits {
if _, ok := firstMimeMatch(transferSource, q.handlers[hit]); ok {
hitsHaveTarget = true
break
}
}
}
if add {
hits = addHandler(hits, n.tag)
for i := len(hits) - 1; i >= 0; i-- {
if _, found := searchTag(p.handlers, hits[i]); !found && !hitsHaveTarget {
hits = append(hits[:i], hits[i+1:]...)
}
}
return true
})
if !p.pressed {
changed = true
p.handlers = hits
} else {
p.handlers = append(p.handlers[:0], hits...)
}
}
// Deliver Leave events.
@@ -855,94 +789,111 @@ func (q *pointerQueue) deliverEnterLeaveEvents(handlers map[event.Tag]*handler,
if _, found := searchTag(hits, k); found {
continue
}
h, ok := handlers[k]
if !ok {
continue
}
changed = true
e := e
h := q.handlers[k]
e.Kind = pointer.Leave
if h.filter.pointer.Matches(e) {
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
evts = append(evts, taggedEvent{tag: k, event: e})
if e.Kind&h.types != 0 {
e := e
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
}
}
// Deliver Enter events.
for _, k := range hits {
h := q.handlers[k]
if _, found := searchTag(p.entered, k); found {
continue
}
h, ok := handlers[k]
if !ok {
continue
}
changed = true
e := e
e.Kind = pointer.Enter
if h.filter.pointer.Matches(e) {
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
evts = append(evts, taggedEvent{tag: k, event: e})
if e.Kind&h.types != 0 {
e := e
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
}
}
p.entered = hits
return p, evts, cursor, changed
p.entered = append(p.entered[:0], hits...)
}
func (q *pointerQueue) deliverDragEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
func (q *pointerQueue) deliverDragEvent(p *pointerInfo, events *handlerEvents) {
if p.dataSource != nil {
return p, evts
return
}
// Identify the data source.
for _, k := range p.entered {
src := &handlers[k].filter.pointer
src := q.handlers[k]
if len(src.sourceMimes) == 0 {
continue
}
// One data source handler per pointer.
p.dataSource = k
// Notify all potential targets.
for k, tgt := range handlers {
if _, ok := firstMimeMatch(src, &tgt.filter.pointer); ok {
evts = append(evts, taggedEvent{tag: k, event: transfer.InitiateEvent{}})
for k, tgt := range q.handlers {
if _, ok := firstMimeMatch(src, tgt); ok {
events.Add(k, transfer.InitiateEvent{})
}
}
break
}
return p, evts
}
func (q *pointerQueue) deliverDropEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
func (q *pointerQueue) deliverDropEvent(p *pointerInfo, events *handlerEvents) {
if p.dataSource == nil {
return p, evts
return
}
// Request data from the source.
src := &handlers[p.dataSource].filter.pointer
src := q.handlers[p.dataSource]
for _, k := range p.entered {
h := handlers[k]
if m, ok := firstMimeMatch(src, &h.filter.pointer); ok {
h := q.handlers[k]
if m, ok := firstMimeMatch(src, h); ok {
p.dataTarget = k
evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.RequestEvent{Type: m}})
return p, evts
events.Add(p.dataSource, transfer.RequestEvent{Type: m})
return
}
}
// No valid target found, abort.
return q.deliverTransferCancelEvent(handlers, p, evts)
q.deliverTransferCancelEvent(p, events)
}
func (q *pointerQueue) deliverTransferCancelEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.CancelEvent{}})
func (q *pointerQueue) deliverTransferDataEvent(p *pointerInfo, events *handlerEvents) {
if p.dataSource == nil {
return
}
src := q.handlers[p.dataSource]
if src.data == nil {
// Data not received yet.
return
}
if p.dataTarget == nil {
q.deliverTransferCancelEvent(p, events)
return
}
// Send the offered data to the target.
transferIdx := len(q.transfers)
events.Add(p.dataTarget, transfer.DataEvent{
Type: src.offeredMime,
Open: func() io.ReadCloser {
q.transfers[transferIdx] = nil
return src.data
},
})
q.transfers = append(q.transfers, src.data)
p.dataTarget = nil
}
func (q *pointerQueue) deliverTransferCancelEvent(p *pointerInfo, events *handlerEvents) {
events.Add(p.dataSource, transfer.CancelEvent{})
// Cancel all potential targets.
src := &handlers[p.dataSource].filter.pointer
for k, h := range handlers {
if _, ok := firstMimeMatch(src, &h.filter.pointer); ok {
evts = append(evts, taggedEvent{tag: k, event: transfer.CancelEvent{}})
src := q.handlers[p.dataSource]
for k, h := range q.handlers {
if _, ok := firstMimeMatch(src, h); ok {
events.Add(k, transfer.CancelEvent{})
}
}
src.offeredMime = ""
src.data = nil
p.dataSource = nil
p.dataTarget = nil
return p, evts
}
// ClipFor clips r to the parents of area.
@@ -977,7 +928,7 @@ func addHandler(tags []event.Tag, tag event.Tag) []event.Tag {
}
// firstMimeMatch returns the first type match between src and tgt.
func firstMimeMatch(src, tgt *pointerFilter) (first string, matched bool) {
func firstMimeMatch(src, tgt *pointerHandler) (first string, matched bool) {
for _, m1 := range tgt.targetMimes {
for _, m2 := range src.sourceMimes {
if m1 == m2 {
@@ -1014,3 +965,13 @@ func (a *areaNode) bounds() image.Rectangle {
Max: a.trans.Transform(f32internal.FPt(a.area.rect.Max)),
}.Round()
}
func setScrollEvent(scroll float32, min, max int) (left, scrolled float32) {
if v := float32(max); scroll > v {
return scroll - v, v
}
if v := float32(min); scroll < v {
return scroll - v, v
}
return 0, scroll
}
File diff suppressed because it is too large Load Diff
+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
package input
package router
import (
"fmt"
@@ -9,7 +9,6 @@ import (
"testing"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/op"
@@ -75,19 +74,13 @@ func TestSemanticTree(t *testing.T) {
func TestSemanticDescription(t *testing.T) {
var ops op.Ops
h := new(int)
event.Op(&ops, h)
pointer.InputOp{Tag: new(int), Kinds: pointer.Press | pointer.Release}.Add(&ops)
semantic.DescriptionOp("description").Add(&ops)
semantic.LabelOp("label").Add(&ops)
semantic.Button.Add(&ops)
semantic.EnabledOp(false).Add(&ops)
semantic.SelectedOp(true).Add(&ops)
var r Router
events(&r, -1, pointer.Filter{
Target: h,
Kinds: pointer.Press | pointer.Release,
})
r.Frame(&ops)
tree := r.AppendSemantics(nil)
got := tree[0].Desc
+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:
//
// - Data sources use [SourceFilter] to receive an [InitiateEvent] when a drag
// is initiated, and an [RequestEvent] for each initiation of a data transfer.
// Sources respond to requests with [OfferCmd].
// - Data targets use [TargetFilter] to receive an [DataEvent] for receiving data.
// The target must close the data event after use.
// - Data sources are registered with SourceOps, data targets with TargetOps.
// - A data source receives a RequestEvent when a transfer is initiated.
// It must respond with an OfferOp.
// - The target receives a DataEvent when transferring to it. It must close
// the event data after use.
//
// When a user initiates a pointer-guided drag and drop transfer, the
// source as well as all potential targets receive an InitiateEvent.
@@ -20,11 +20,29 @@ package transfer
import (
"io"
"gioui.org/internal/ops"
"gioui.org/io/event"
"gioui.org/op"
)
// OfferCmd is used by data sources as a response to a RequestEvent.
type OfferCmd struct {
// SourceOp registers a tag as a data source for a MIME type.
// Use multiple SourceOps if a tag supports multiple types.
type SourceOp struct {
Tag event.Tag
// Type is the MIME type supported by this source.
Type string
}
// TargetOp registers a tag as a data target.
// Use multiple TargetOps if a tag supports multiple types.
type TargetOp struct {
Tag event.Tag
// Type is the MIME type accepted by this target.
Type string
}
// OfferOp is used by data sources as a response to a RequestEvent.
type OfferOp struct {
Tag event.Tag
// Type is the MIME type of Data.
// It must be the Type from the corresponding RequestEvent.
@@ -32,33 +50,32 @@ type OfferCmd struct {
// Data contains the offered data. It is closed when the
// transfer is complete or cancelled.
// Data must be kept valid until closed, and it may be used from
// a goroutine separate from the one processing the frame.
// a goroutine separate from the one processing the frame..
Data io.ReadCloser
}
func (OfferCmd) ImplementsCommand() {}
// SourceFilter filters for any [RequestEvent] that match a MIME type
// as well as [InitiateEvent] and [CancelEvent].
// Use multiple filters to offer multiple types.
type SourceFilter struct {
// Target is a tag included in a previous event.Op.
Target event.Tag
// Type is the MIME type supported by this source.
Type string
func (op SourceOp) Add(o *op.Ops) {
data := ops.Write2(&o.Internal, ops.TypeSourceLen, op.Tag, op.Type)
data[0] = byte(ops.TypeSource)
}
// TargetFilter filters for any [DataEvent] whose type matches a MIME type
// as well as [CancelEvent]. Use multiple filters to accept multiple types.
type TargetFilter struct {
// Target is a tag included in a previous event.Op.
Target event.Tag
// Type is the MIME type accepted by this target.
Type string
func (op TargetOp) Add(o *op.Ops) {
data := ops.Write2(&o.Internal, ops.TypeTargetLen, op.Tag, op.Type)
data[0] = byte(ops.TypeTarget)
}
// Add the offer to the list of operations.
// It panics if the Data field is not set.
func (op OfferOp) Add(o *op.Ops) {
if op.Data == nil {
panic("invalid nil data in OfferOp")
}
data := ops.Write3(&o.Internal, ops.TypeOfferLen, op.Tag, op.Type, op.Data)
data[0] = byte(ops.TypeOffer)
}
// RequestEvent requests data from a data source. The source must
// respond with an OfferCmd.
// respond with an OfferOp.
type RequestEvent struct {
// Type is the first matched type between the source and the target.
Type string
@@ -90,6 +107,3 @@ type DataEvent struct {
}
func (DataEvent) ImplementsEvent() {}
func (SourceFilter) ImplementsFilter() {}
func (TargetFilter) ImplementsFilter() {}
+57 -5
View File
@@ -3,9 +3,10 @@
package layout
import (
"image"
"time"
"gioui.org/io/input"
"gioui.org/io/event"
"gioui.org/io/system"
"gioui.org/op"
"gioui.org/unit"
@@ -20,6 +21,9 @@ type Context struct {
Constraints Constraints
Metric unit.Metric
// By convention, a nil Queue is a signal to widgets to draw themselves
// in a disabled state.
Queue event.Queue
// Now is the animation time.
Now time.Time
@@ -28,10 +32,46 @@ type Context struct {
// Interested users must look up and populate these values manually.
Locale system.Locale
input.Source
*op.Ops
}
// NewContext is a shorthand for
//
// Context{
// Ops: ops,
// Now: e.Now,
// Queue: e.Queue,
// Config: e.Config,
// Constraints: Exact(e.Size),
// }
//
// NewContext calls ops.Reset and adjusts ops for e.Insets.
func NewContext(ops *op.Ops, e system.FrameEvent) Context {
ops.Reset()
size := e.Size
if e.Insets != (system.Insets{}) {
left := e.Metric.Dp(e.Insets.Left)
top := e.Metric.Dp(e.Insets.Top)
op.Offset(image.Point{
X: left,
Y: top,
}).Add(ops)
size.X -= left + e.Metric.Dp(e.Insets.Right)
size.Y -= top + e.Metric.Dp(e.Insets.Bottom)
}
return Context{
Ops: ops,
Now: e.Now,
Queue: e.Queue,
Metric: e.Metric,
Constraints: Exact(size),
}
}
// Dp converts v to pixels.
func (c Context) Dp(v unit.Dp) int {
return c.Metric.Dp(v)
@@ -42,9 +82,21 @@ func (c Context) Sp(v unit.Sp) int {
return c.Metric.Sp(v)
}
// Disabled returns a copy of this context with a disabled Source,
// blocking widgets from changing its state and receiving events.
// Events returns the events available for the key. If no
// queue is configured, Events returns nil.
func (c Context) Events(k event.Tag) []event.Event {
if c.Queue == nil {
return nil
}
return c.Queue.Events(k)
}
// Disabled returns a copy of this context with a nil Queue,
// blocking events to widgets using it.
//
// By convention, a nil Queue is a signal to widgets to draw themselves
// in a disabled state.
func (c Context) Disabled() Context {
c.Source = input.Source{}
c.Queue = nil
return c
}
+20 -20
View File
@@ -144,25 +144,7 @@ func (l *List) Dragging() bool {
}
func (l *List) update(gtx Context) {
min, max := int(-inf), int(inf)
if l.Position.First == 0 {
// Use the size of the invisible part as scroll boundary.
min = -l.Position.Offset
if min > 0 {
min = 0
}
}
if l.Position.First+l.Position.Count == l.len {
max = -l.Position.OffsetLast
if max < 0 {
max = 0
}
}
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)
d := l.scroll.Update(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis))
l.scrollDelta = d
l.Position.Offset += d
}
@@ -350,7 +332,25 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
call := macro.Stop()
defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop()
l.scroll.Add(ops)
min, max := int(-inf), int(inf)
if l.Position.First == 0 {
// Use the size of the invisible part as scroll boundary.
min = -l.Position.Offset
if min > 0 {
min = 0
}
}
if l.Position.First+l.Position.Count == l.len {
max = -l.Position.OffsetLast
if max < 0 {
max = 0
}
}
scrollRange := image.Rectangle{
Min: l.Axis.Convert(image.Pt(min, 0)),
Max: l.Axis.Convert(image.Pt(max, 0)),
}
l.scroll.Add(ops, scrollRange)
call.Add(ops)
return Dimensions{Size: dims}
+3 -3
View File
@@ -8,8 +8,8 @@ import (
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/op"
)
@@ -64,13 +64,13 @@ func TestListScrollToEnd(t *testing.T) {
func TestListPosition(t *testing.T) {
_s := func(e ...event.Event) []event.Event { return e }
r := new(input.Router)
r := new(router.Router)
gtx := Context{
Ops: new(op.Ops),
Constraints: Constraints{
Max: image.Pt(20, 10),
},
Source: r.Source(),
Queue: r,
}
el := func(gtx Context, idx int) Dimensions {
return Dimensions{Size: image.Pt(10, 10)}
+16 -4
View File
@@ -53,6 +53,7 @@ The MacroOp records a list of operations to be executed later:
ops := new(op.Ops)
macro := op.Record(ops)
// Record operations by adding them.
op.InvalidateOp{}.Add(ops)
...
// End recording.
call := macro.Stop()
@@ -95,9 +96,9 @@ type CallOp struct {
end ops.PC
}
// InvalidateCmd requests a redraw at the given time. Use
// InvalidateOp requests a redraw at the given time. Use
// the zero value to request an immediate redraw.
type InvalidateCmd struct {
type InvalidateOp struct {
At time.Time
}
@@ -180,6 +181,19 @@ func (c CallOp) Add(o *Ops) {
ops.AddCall(&o.Internal, c.ops, c.start, c.end)
}
func (r InvalidateOp) Add(o *Ops) {
data := ops.Write(&o.Internal, ops.TypeRedrawLen)
data[0] = byte(ops.TypeInvalidate)
bo := binary.LittleEndian
// UnixNano cannot represent the zero time.
if t := r.At; !t.IsZero() {
nanos := t.UnixNano()
if nanos > 0 {
bo.PutUint64(data[1:], uint64(nanos))
}
}
}
// Offset converts an offset to a TransformOp.
func Offset(off image.Point) TransformOp {
offf := f32.Pt(float32(off.X), float32(off.Y))
@@ -226,5 +240,3 @@ func (t TransformStack) Pop() {
data := ops.Write(t.ops, ops.TypePopTransformLen)
data[0] = byte(ops.TypePopTransform)
}
func (InvalidateCmd) ImplementsCommand() {}
+1 -3
View File
@@ -4,7 +4,6 @@ package text
import (
"bytes"
"fmt"
"image"
"io"
"log"
@@ -277,8 +276,7 @@ func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
// It returns whether the face is now available for use. FontFaces are prioritized
// in the order in which they are loaded, with the first face being the default.
func (s *shaperImpl) Load(f FontFace) {
desc := opentype.FontToDescription(f.Font)
s.fontMap.AddFace(f.Face.Face(), fontscan.Location{File: fmt.Sprint(desc)}, desc)
s.fontMap.AddFace(f.Face.Face(), opentype.FontToDescription(f.Font))
s.addFace(f.Face.Face(), f.Font)
}
+4
View File
@@ -163,6 +163,10 @@ type layoutKey struct {
lineHeightScale float32
}
type pathKey struct {
gidHash uint64
}
const maxSize = 1000
func gidsEqual(a []glyphInfo, glyphs []Glyph) bool {
+8 -3
View File
@@ -16,7 +16,7 @@ type Bool struct {
// Update the widget state and report whether Value was changed.
func (b *Bool) Update(gtx layout.Context) bool {
changed := false
for b.clk.clicked(b, gtx) {
for b.clk.Clicked(gtx) {
b.Value = !b.Value
changed = true
}
@@ -33,15 +33,20 @@ func (b *Bool) Pressed() bool {
return b.clk.Pressed()
}
// Focused reports whether b has focus.
func (b *Bool) Focused() bool {
return b.clk.Focused()
}
func (b *Bool) History() []Press {
return b.clk.History()
}
func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
b.Update(gtx)
dims := b.clk.layout(b, gtx, func(gtx layout.Context) layout.Dimensions {
dims := b.clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.SelectedOp(b.Value).Add(gtx.Ops)
semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops)
semantic.EnabledOp(gtx.Queue != nil).Add(gtx.Ops)
return w(gtx)
})
return dims
+3
View File
@@ -11,6 +11,9 @@ import (
// editBuffer implements a gap buffer for text editing.
type editBuffer struct {
// pos is the byte position for Read and ReadRune.
pos int
// The gap start and end in bytes.
gapstart, gapend int
text []byte
+64 -53
View File
@@ -7,7 +7,6 @@ import (
"time"
"gioui.org/gesture"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
@@ -18,11 +17,16 @@ import (
// Clickable represents a clickable area.
type Clickable struct {
click gesture.Click
click gesture.Click
// clicks is for saved clicks to support Clicked.
clicks []Click
history []Press
keyTag struct{}
requestFocus bool
requestClicks int
pressedKey key.Name
focused bool
pressedKey string
}
// Click represents a click.
@@ -49,14 +53,19 @@ func (b *Clickable) Click() {
b.requestClicks++
}
// Clicked calls Update and reports whether a click was registered.
// Clicked reports whether there are pending clicks. If so, Clicked
// removes the earliest click.
func (b *Clickable) Clicked(gtx layout.Context) bool {
return b.clicked(b, gtx)
}
func (b *Clickable) clicked(t event.Tag, gtx layout.Context) bool {
_, clicked := b.update(t, gtx)
return clicked
if len(b.clicks) > 0 {
b.clicks = b.clicks[1:]
return true
}
b.clicks = b.Update(gtx)
if len(b.clicks) > 0 {
b.clicks = b.clicks[1:]
return true
}
return false
}
// Hovered reports whether a pointer is over the element.
@@ -69,6 +78,16 @@ func (b *Clickable) Pressed() bool {
return b.click.Pressed()
}
// Focus requests the input focus for the element.
func (b *Clickable) Focus() {
b.requestFocus = true
}
// Focused reports whether b has focus.
func (b *Clickable) Focused() bool {
return b.focused
}
// History is the past pointer presses useful for drawing markers.
// History is retained for a short duration (about a second).
func (b *Clickable) History() []Press {
@@ -77,34 +96,36 @@ func (b *Clickable) History() []Press {
// Layout and update the button state.
func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
return b.layout(b, gtx, w)
}
func (b *Clickable) layout(t event.Tag, gtx layout.Context, w layout.Widget) layout.Dimensions {
for {
_, ok := b.update(t, gtx)
if !ok {
break
}
}
b.Update(gtx)
m := op.Record(gtx.Ops)
dims := w(gtx)
c := m.Stop()
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops)
enabled := gtx.Queue != nil
semantic.EnabledOp(enabled).Add(gtx.Ops)
b.click.Add(gtx.Ops)
event.Op(gtx.Ops, t)
if enabled {
keys := key.Set("⏎|Space")
if !b.focused {
keys = ""
}
key.InputOp{Tag: &b.keyTag, Keys: keys}.Add(gtx.Ops)
}
c.Add(gtx.Ops)
return dims
}
// Update the button state by processing events, and return the next
// click, if any.
func (b *Clickable) Update(gtx layout.Context) (Click, bool) {
return b.update(b, gtx)
}
func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
// Update the button state by processing events, and return the resulting
// clicks, if any.
func (b *Clickable) Update(gtx layout.Context) []Click {
b.clicks = nil
if gtx.Queue == nil {
b.focused = false
}
if b.requestFocus {
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
b.requestFocus = false
}
for len(b.history) > 0 {
c := b.history[0]
if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second {
@@ -113,26 +134,23 @@ func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
n := copy(b.history, b.history[1:])
b.history = b.history[:n]
}
var clicks []Click
if c := b.requestClicks; c > 0 {
b.requestClicks = 0
return Click{
clicks = append(clicks, Click{
NumClicks: c,
}, true
})
}
for {
e, ok := b.click.Update(gtx.Source)
if !ok {
break
}
for _, e := range b.click.Update(gtx) {
switch e.Kind {
case gesture.KindClick:
if l := len(b.history); l > 0 {
b.history[l-1].End = gtx.Now
}
return Click{
clicks = append(clicks, Click{
Modifiers: e.Modifiers,
NumClicks: e.NumClicks,
}, true
})
case gesture.KindCancel:
for i := range b.history {
b.history[i].Cancelled = true
@@ -142,7 +160,7 @@ func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
}
case gesture.KindPress:
if e.Source == pointer.Mouse {
gtx.Execute(key.FocusCmd{Tag: t})
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
}
b.history = append(b.history, Press{
Position: e.Position,
@@ -150,22 +168,15 @@ func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
})
}
}
for {
e, ok := gtx.Event(
key.FocusFilter{Target: t},
key.Filter{Focus: t, Name: key.NameReturn},
key.Filter{Focus: t, Name: key.NameSpace},
)
if !ok {
break
}
for _, e := range gtx.Events(&b.keyTag) {
switch e := e.(type) {
case key.FocusEvent:
if e.Focus {
b.focused = e.Focus
if !b.focused {
b.pressedKey = ""
}
case key.Event:
if !gtx.Focused(t) {
if !b.focused {
break
}
if e.Name != key.NameReturn && e.Name != key.NameSpace {
@@ -180,12 +191,12 @@ func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
}
// only register a key as a click if the key was pressed and released while this button was focused
b.pressedKey = ""
return Click{
clicks = append(clicks, Click{
Modifiers: e.Modifiers,
NumClicks: 1,
}, true
})
}
}
}
return Click{}, false
return clicks
}
+25 -15
View File
@@ -6,8 +6,9 @@ import (
"image"
"testing"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/router"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/widget"
@@ -15,14 +16,12 @@ import (
func TestClickable(t *testing.T) {
var (
r input.Router
b1 widget.Clickable
b2 widget.Clickable
ops op.Ops
r router.Router
b1 widget.Clickable
b2 widget.Clickable
)
gtx := layout.Context{
Ops: new(op.Ops),
Source: r.Source(),
}
gtx := layout.NewContext(&ops, system.FrameEvent{Queue: &r})
layout := func() {
b1.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Dimensions{Size: image.Pt(100, 100)}
@@ -33,18 +32,23 @@ func TestClickable(t *testing.T) {
})
}
frame := func() {
gtx.Reset()
ops.Reset()
layout()
r.Frame(gtx.Ops)
}
gtx.Execute(key.FocusCmd{Tag: &b1})
// frame: request focus for button 1
b1.Focus()
frame()
if !gtx.Focused(&b1) {
// frame: gain focus for button 1
frame()
if !b1.Focused() {
t.Error("button 1 did not gain focus")
}
if gtx.Focused(&b2) {
if b2.Focused() {
t.Error("button 2 should not have focus")
}
// frame: press & release return
frame()
r.Queue(
key.Event{
Name: key.NameReturn,
@@ -61,30 +65,36 @@ func TestClickable(t *testing.T) {
if b2.Clicked(gtx) {
t.Error("button 2 got clicked when it did not have focus")
}
// frame: press return down
r.Queue(
key.Event{
Name: key.NameReturn,
State: key.Press,
},
)
frame()
if b1.Clicked(gtx) {
t.Error("button 1 got clicked, even if it only got return press")
}
// frame: request focus for button 2
b2.Focus()
frame()
gtx.Execute(key.FocusCmd{Tag: &b2})
// frame: gain focus for button 2
frame()
if gtx.Focused(&b1) {
if b1.Focused() {
t.Error("button 1 should not have focus")
}
if !gtx.Focused(&b2) {
if !b2.Focused() {
t.Error("button 2 did not gain focus")
}
// frame: release return
r.Queue(
key.Event{
Name: key.NameReturn,
State: key.Release,
},
)
frame()
if b1.Clicked(gtx) {
t.Error("button 1 got clicked, even if it had lost focus")
}
+6 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"math/bits"
"gioui.org/gesture"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op/clip"
@@ -11,7 +12,11 @@ import (
// Decorations handles the states of window decorations.
type Decorations struct {
clicks map[int]*Clickable
clicks map[int]*Clickable
resize [8]struct {
gesture.Hover
gesture.Drag
}
maximized bool
}
+18 -19
View File
@@ -5,7 +5,6 @@ import (
"gioui.org/f32"
"gioui.org/gesture"
"gioui.org/io/event"
"gioui.org/io/pointer"
"gioui.org/io/transfer"
"gioui.org/layout"
@@ -18,20 +17,24 @@ type Draggable struct {
// Type contains the MIME type and matches transfer.SourceOp.
Type string
drag gesture.Drag
click f32.Point
pos f32.Point
handle struct{}
drag gesture.Drag
click f32.Point
pos f32.Point
}
func (d *Draggable) Layout(gtx layout.Context, w, drag layout.Widget) layout.Dimensions {
if !gtx.Enabled() {
if gtx.Queue == nil {
return w(gtx)
}
dims := w(gtx)
stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops)
d.drag.Add(gtx.Ops)
event.Op(gtx.Ops, d)
transfer.SourceOp{
Tag: &d.handle,
Type: d.Type,
}.Add(gtx.Ops)
stack.Pop()
if drag != nil && d.drag.Pressed() {
@@ -53,11 +56,7 @@ func (d *Draggable) Dragging() bool {
// requested to offer data, if any
func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
pos := d.pos
for {
ev, ok := d.drag.Update(gtx.Metric, gtx.Source, gesture.Both)
if !ok {
break
}
for _, ev := range d.drag.Update(gtx.Metric, gtx.Queue, gesture.Both) {
switch ev.Kind {
case pointer.Press:
d.click = ev.Position
@@ -68,12 +67,8 @@ func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
}
d.pos = pos
for {
e, ok := gtx.Event(transfer.SourceFilter{Target: d, Type: d.Type})
if !ok {
break
}
if e, ok := e.(transfer.RequestEvent); ok {
for _, ev := range gtx.Queue.Events(&d.handle) {
if e, ok := ev.(transfer.RequestEvent); ok {
return e.Type, true
}
}
@@ -82,8 +77,12 @@ func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
// Offer the data ready for a drop. Must be called after being Requested.
// The mime must be one in the requested list.
func (d *Draggable) Offer(gtx layout.Context, mime string, data io.ReadCloser) {
gtx.Execute(transfer.OfferCmd{Tag: d, Type: mime, Data: data})
func (d *Draggable) Offer(ops *op.Ops, mime string, data io.ReadCloser) {
transfer.OfferOp{
Tag: &d.handle,
Type: mime,
Data: data,
}.Add(ops)
}
// Pos returns the drag position relative to its initial click position.
+14 -22
View File
@@ -5,9 +5,8 @@ import (
"testing"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/io/transfer"
"gioui.org/layout"
"gioui.org/op"
@@ -15,27 +14,27 @@ import (
)
func TestDraggable(t *testing.T) {
var r input.Router
var r router.Router
gtx := layout.Context{
Constraints: layout.Exact(image.Pt(100, 100)),
Source: r.Source(),
Queue: &r,
Ops: new(op.Ops),
}
drag := &Draggable{
Type: "file",
}
tgt := new(int)
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
dims := drag.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Dimensions{Size: gtx.Constraints.Min}
}, nil)
stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops)
event.Op(gtx.Ops, tgt)
transfer.TargetOp{
Tag: drag,
Type: drag.Type,
}.Add(gtx.Ops)
stack.Pop()
drag.Update(gtx)
r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type})
r.Frame(gtx.Ops)
r.Queue(
pointer.Event{
@@ -52,28 +51,21 @@ func TestDraggable(t *testing.T) {
},
)
ofr := &offer{data: "hello"}
drag.Update(gtx)
r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type})
drag.Offer(gtx, "file", ofr)
drag.Offer(gtx.Ops, "file", ofr)
r.Frame(gtx.Ops)
e, ok := r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type})
if !ok {
t.Fatalf("expected event")
evs := r.Events(drag)
if len(evs) != 1 {
t.Fatalf("expected 1 event, got %d", len(evs))
}
ev := e.(transfer.DataEvent)
ev := evs[0].(transfer.DataEvent)
ev.Open = nil
if got, want := ev.Type, "file"; got != want {
t.Errorf("expected %v; got %v", got, want)
}
if ofr.closed {
t.Error("offer closed prematurely")
}
e, ok = r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type})
if !ok {
t.Fatalf("expected event")
}
if _, ok := e.(transfer.CancelEvent); !ok {
t.Fatalf("expected transfer.CancelEvent event")
}
r.Frame(gtx.Ops)
if !ofr.closed {
t.Error("offer was not closed")
+1 -1
View File
@@ -2,5 +2,5 @@
// Package widget implements state tracking and event handling of
// common user interface controls. To draw widgets, use a theme
// packages such as package [gioui.org/widget/material].
// packages such as package gioui.org/widget/material.
package widget
+235 -284
View File
@@ -21,7 +21,6 @@ import (
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
@@ -70,8 +69,11 @@ type Editor struct {
buffer *editBuffer
// scratch is a byte buffer that is reused to efficiently read portions of text
// from the textView.
scratch []byte
blinkStart time.Time
scratch []byte
eventKey int
blinkStart time.Time
focused bool
requestFocus bool
// ime tracks the state relevant to input methods.
ime struct {
@@ -87,14 +89,16 @@ type Editor struct {
clicker gesture.Click
// events is the list of events not yet processed.
events []EditorEvent
// prevEvents is the number of events from the previous frame.
prevEvents int
// history contains undo history.
history []modification
// nextHistoryIdx is the index within the history of the next modification. This
// is only not len(history) immediately after undo operations occur. It is framed as the "next" value
// to make the zero value consistent.
nextHistoryIdx int
pending []EditorEvent
}
type offEntry struct {
@@ -187,37 +191,30 @@ const (
maxBlinkDuration = 10 * time.Second
)
func (e *Editor) processEvents(gtx layout.Context) (ev EditorEvent, ok bool) {
if len(e.pending) > 0 {
out := e.pending[0]
e.pending = e.pending[:copy(e.pending, e.pending[1:])]
return out, true
}
selStart, selEnd := e.Selection()
defer func() {
afterSelStart, afterSelEnd := e.Selection()
if selStart != afterSelStart || selEnd != afterSelEnd {
if ok {
e.pending = append(e.pending, SelectEvent{})
} else {
ev = SelectEvent{}
ok = true
}
}
}()
ev, ok = e.processPointer(gtx)
if ok {
return ev, ok
}
ev, ok = e.processKey(gtx)
if ok {
return ev, ok
}
return nil, false
// Events returns available editor events.
func (e *Editor) Events() []EditorEvent {
events := e.events
e.events = nil
e.prevEvents = 0
return events
}
func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
func (e *Editor) processEvents(gtx layout.Context) {
// Flush events from before the previous Layout.
n := copy(e.events, e.events[e.prevEvents:])
e.events = e.events[:n]
e.prevEvents = n
oldStart, oldLen := min(e.text.Selection()), e.text.SelectionLen()
e.processPointer(gtx)
e.processKey(gtx)
// Queue a SelectEvent if the selection changed, including if it went away.
if newStart, newLen := min(e.text.Selection()), e.text.SelectionLen(); oldStart != newStart || oldLen != newLen {
e.events = append(e.events, SelectEvent{})
}
}
func (e *Editor) processPointer(gtx layout.Context) {
sbounds := e.text.ScrollBounds()
var smin, smax int
var axis gesture.Axis
@@ -228,19 +225,7 @@ func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
axis = gesture.Vertical
smin, smax = sbounds.Min.Y, sbounds.Max.Y
}
var scrollRange image.Rectangle
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)
sdist := e.scroller.Update(gtx.Metric, gtx, gtx.Now, axis)
var soff int
if e.SingleLine {
e.text.ScrollRel(sdist, 0)
@@ -249,173 +234,115 @@ func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
e.text.ScrollRel(0, sdist)
soff = e.text.ScrollOff().Y
}
for {
evt, ok := e.clicker.Update(gtx.Source)
if !ok {
break
}
ev, ok := e.processPointerEvent(gtx, evt)
if ok {
return ev, ok
}
}
for {
evt, ok := e.dragger.Update(gtx.Metric, gtx.Source, gesture.Both)
if !ok {
break
}
ev, ok := e.processPointerEvent(gtx, evt)
if ok {
return ev, ok
for _, evt := range e.clickDragEvents(gtx) {
switch evt := evt.(type) {
case gesture.ClickEvent:
switch {
case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse,
evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse:
prevCaretPos, _ := e.text.Selection()
e.blinkStart = gtx.Now
e.text.MoveCoord(image.Point{
X: int(math.Round(float64(evt.Position.X))),
Y: int(math.Round(float64(evt.Position.Y))),
})
e.requestFocus = true
if e.scroller.State() != gesture.StateFlinging {
e.scrollCaret = true
}
if evt.Modifiers == key.ModShift {
start, end := e.text.Selection()
// If they clicked closer to the end, then change the end to
// where the caret used to be (effectively swapping start & end).
if abs(end-start) < abs(start-prevCaretPos) {
e.text.SetCaret(start, prevCaretPos)
}
} else {
e.text.ClearSelection()
}
e.dragging = true
// Process multi-clicks.
switch {
case evt.NumClicks == 2:
e.text.MoveWord(-1, selectionClear)
e.text.MoveWord(1, selectionExtend)
e.dragging = false
case evt.NumClicks >= 3:
e.text.MoveStart(selectionClear)
e.text.MoveEnd(selectionExtend)
e.dragging = false
}
}
case pointer.Event:
release := false
switch {
case evt.Kind == pointer.Release && evt.Source == pointer.Mouse:
release = true
fallthrough
case evt.Kind == pointer.Drag && evt.Source == pointer.Mouse:
if e.dragging {
e.blinkStart = gtx.Now
e.text.MoveCoord(image.Point{
X: int(math.Round(float64(evt.Position.X))),
Y: int(math.Round(float64(evt.Position.Y))),
})
e.scrollCaret = true
if release {
e.dragging = false
}
}
}
}
}
if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) {
e.scroller.Stop()
}
return nil, false
}
func (e *Editor) processPointerEvent(gtx layout.Context, ev event.Event) (EditorEvent, bool) {
switch evt := ev.(type) {
case gesture.ClickEvent:
switch {
case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse,
evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse:
prevCaretPos, _ := e.text.Selection()
e.blinkStart = gtx.Now
e.text.MoveCoord(image.Point{
X: int(math.Round(float64(evt.Position.X))),
Y: int(math.Round(float64(evt.Position.Y))),
})
gtx.Execute(key.FocusCmd{Tag: e})
if e.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
}
}
}
func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event {
var combinedEvents []event.Event
for _, evt := range e.clicker.Update(gtx) {
combinedEvents = append(combinedEvents, evt)
}
return nil, false
}
func condFilter(pred bool, f key.Filter) event.Filter {
if pred {
return f
} else {
return nil
for _, evt := range e.dragger.Update(gtx.Metric, gtx, gesture.Both) {
combinedEvents = append(combinedEvents, evt)
}
return combinedEvents
}
func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
func (e *Editor) processKey(gtx layout.Context) {
if e.text.Changed() {
return ChangeEvent{}, true
}
caret, _ := e.text.Selection()
atBeginning := caret == 0
atEnd := caret == e.text.Len()
if gtx.Locale.Direction.Progression() != system.FromOrigin {
atEnd, atBeginning = atBeginning, atEnd
}
filters := []event.Filter{
key.FocusFilter{Target: e},
transfer.TargetFilter{Target: e, Type: "application/text"},
key.Filter{Focus: e, Name: key.NameEnter, Optional: key.ModShift},
key.Filter{Focus: e, Name: key.NameReturn, Optional: key.ModShift},
key.Filter{Focus: e, Name: "Z", Required: key.ModShortcut, Optional: key.ModShift},
key.Filter{Focus: e, Name: "C", Required: key.ModShortcut},
key.Filter{Focus: e, Name: "V", Required: key.ModShortcut},
key.Filter{Focus: e, Name: "X", Required: key.ModShortcut},
key.Filter{Focus: e, Name: "A", Required: key.ModShortcut},
key.Filter{Focus: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NameHome, Optional: key.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}),
e.events = append(e.events, ChangeEvent{})
}
// adjust keeps track of runes dropped because of MaxLen.
var adjust int
for {
ke, ok := gtx.Event(filters...)
if !ok {
break
}
for _, ke := range gtx.Events(&e.eventKey) {
e.blinkStart = gtx.Now
switch ke := ke.(type) {
case key.FocusEvent:
e.focused = ke.Focus
// Reset IME state.
e.ime.imeState = imeState{}
if ke.Focus {
gtx.Execute(key.SoftKeyboardCmd{Show: true})
}
case key.Event:
if !gtx.Focused(e) || ke.State != key.Press {
if !e.focused || ke.State != key.Press {
break
}
if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
if !ke.Modifiers.Contain(key.ModShift) {
e.scratch = e.text.Text(e.scratch)
return SubmitEvent{
e.events = append(e.events, SubmitEvent{
Text: string(e.scratch),
}, true
})
continue
}
}
e.command(gtx, ke)
e.scrollCaret = true
e.scroller.Stop()
ev, ok := e.command(gtx, ke)
if ok {
return ev, ok
}
case key.SnippetEvent:
e.updateSnippet(gtx, ke.Start, ke.End)
case key.EditEvent:
@@ -442,26 +369,19 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
// Reset caret xoff.
e.text.MoveCaret(0, 0)
if submit {
e.scratch = e.text.Text(e.scratch)
submitEvent := SubmitEvent{
Text: string(e.scratch),
}
if e.text.Changed() {
e.pending = append(e.pending, submitEvent)
return ChangeEvent{}, true
e.events = append(e.events, ChangeEvent{})
}
return submitEvent, true
e.scratch = e.text.Text(e.scratch)
e.events = append(e.events, SubmitEvent{
Text: string(e.scratch),
})
}
// Complete a paste event, initiated by Shortcut-V in Editor.command().
case transfer.DataEvent:
case clipboard.Event:
e.scrollCaret = true
e.scroller.Stop()
content, err := io.ReadAll(ke.Open())
if err == nil {
if e.Insert(string(content)) != 0 {
return ChangeEvent{}, true
}
}
e.Insert(ke.Text)
case key.SelectionEvent:
e.scrollCaret = true
e.scroller.Stop()
@@ -472,12 +392,11 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
}
}
if e.text.Changed() {
return ChangeEvent{}, true
e.events = append(e.events, ChangeEvent{})
}
return nil, false
}
func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
func (e *Editor) command(gtx layout.Context, k key.Event) {
direction := 1
if gtx.Locale.Direction.Progression() == system.TowardOrigin {
direction = -1
@@ -493,17 +412,15 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
// half is in Editor.processKey() under clipboard.Event.
case "V":
if !e.ReadOnly {
gtx.Execute(clipboard.ReadCmd{Tag: e})
clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops)
}
// Copy or Cut selection -- ignored if nothing selected.
case "C", "X":
e.scratch = e.text.SelectedText(e.scratch)
if text := string(e.scratch); text != "" {
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))})
clipboard.WriteOp{Text: text}.Add(gtx.Ops)
if k.Name == "X" && !e.ReadOnly {
if e.Delete(1) != 0 {
return ChangeEvent{}, true
}
e.Delete(1)
}
}
// Select all
@@ -512,47 +429,33 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
case "Z":
if !e.ReadOnly {
if k.Modifiers.Contain(key.ModShift) {
if ev, ok := e.redo(); ok {
return ev, ok
}
e.redo()
} else {
if ev, ok := e.undo(); ok {
return ev, ok
}
e.undo()
}
}
}
return nil, false
return
}
switch k.Name {
case key.NameReturn, key.NameEnter:
if !e.ReadOnly {
if e.Insert("\n") != 0 {
return ChangeEvent{}, true
}
e.Insert("\n")
}
case key.NameDeleteBackward:
if !e.ReadOnly {
if moveByWord {
if e.deleteWord(-1) != 0 {
return ChangeEvent{}, true
}
e.deleteWord(-1)
} else {
if e.Delete(-1) != 0 {
return ChangeEvent{}, true
}
e.Delete(-1)
}
}
case key.NameDeleteForward:
if !e.ReadOnly {
if moveByWord {
if e.deleteWord(1) != 0 {
return ChangeEvent{}, true
}
e.deleteWord(1)
} else {
if e.Delete(1) != 0 {
return ChangeEvent{}, true
}
e.Delete(1)
}
}
case key.NameUpArrow:
@@ -586,7 +489,16 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
case key.NameEnd:
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
@@ -605,51 +517,48 @@ func (e *Editor) initBuffer() {
e.text.WrapPolicy = e.WrapPolicy
}
// Update the state of the editor in response to input events. Update consumes editor
// input events until there are no remaining events or an editor event is generated.
// To fully update the state of the editor, callers should call Update until it returns
// false.
func (e *Editor) Update(gtx layout.Context) (EditorEvent, bool) {
// Update the state of the editor in response to input events.
func (e *Editor) Update(gtx layout.Context) {
e.initBuffer()
event, ok := e.processEvents(gtx)
// Notify IME of selection if it changed.
newSel := e.ime.selection
start, end := e.text.Selection()
newSel.rng = key.Range{
Start: start,
End: end,
}
caretPos, carAsc, carDesc := e.text.CaretInfo()
newSel.caret = key.Caret{
Pos: layout.FPt(caretPos),
Ascent: float32(carAsc),
Descent: float32(carDesc),
}
if newSel != e.ime.selection {
e.ime.selection = newSel
gtx.Execute(key.SelectionCmd{Tag: e, Range: newSel.rng, Caret: newSel.caret})
}
e.processEvents(gtx)
if e.focused {
// Notify IME of selection if it changed.
newSel := e.ime.selection
start, end := e.text.Selection()
newSel.rng = key.Range{
Start: start,
End: end,
}
caretPos, carAsc, carDesc := e.text.CaretInfo()
newSel.caret = key.Caret{
Pos: layout.FPt(caretPos),
Ascent: float32(carAsc),
Descent: float32(carDesc),
}
if newSel != e.ime.selection {
e.ime.selection = newSel
key.SelectionOp{
Tag: &e.eventKey,
Range: newSel.rng,
Caret: newSel.caret,
}.Add(gtx.Ops)
}
e.updateSnippet(gtx, e.ime.start, e.ime.end)
return event, ok
e.updateSnippet(gtx, e.ime.start, e.ime.end)
}
}
// Layout lays out the editor using the provided textMaterial as the paint material
// for the text glyphs+caret and the selectMaterial as the paint material for the
// selection rectangle.
func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
for {
_, ok := e.Update(gtx)
if !ok {
break
}
}
e.Update(gtx)
e.text.Layout(gtx, lt, font, size)
return e.layout(gtx, textMaterial, selectMaterial)
}
// updateSnippet queues a key.SnippetCmd if the snippet content or position
// updateSnippet adds a key.SnippetOp if the snippet content or position
// have changed. off and len are in runes.
func (e *Editor) updateSnippet(gtx layout.Context, start, end int) {
if start > end {
@@ -689,7 +598,10 @@ func (e *Editor) updateSnippet(gtx layout.Context, start, end int) {
return
}
e.ime.snippet = newSnip
gtx.Execute(key.SnippetCmd{Tag: e, Snippet: newSnip})
key.SnippetOp{
Tag: &e.eventKey,
Snippet: newSnip,
}.Add(gtx.Ops)
}
func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
@@ -700,35 +612,79 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
e.scrollCaret = false
e.text.ScrollToCaret()
}
textDims := e.text.FullDimensions()
visibleDims := e.text.Dimensions()
defer clip.Rect(image.Rectangle{Max: visibleDims.Size}).Push(gtx.Ops).Pop()
pointer.CursorText.Add(gtx.Ops)
event.Op(gtx.Ops, e)
key.InputHintOp{Tag: e, Hint: e.InputHint}.Add(gtx.Ops)
var keys key.Set
if e.focused {
const keyFilterNoLeftUp = "(ShortAlt)-(Shift)-[→,↓]|(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z"
const keyFilterNoRightDown = "(ShortAlt)-(Shift)-[←,↑]|(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z"
const keyFilterNoArrows = "(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z"
const keyFilterAllArrows = "(ShortAlt)-(Shift)-[←,→,↑,↓]|(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z"
caret, _ := e.text.Selection()
switch {
case caret == 0 && caret == e.text.Len():
keys = keyFilterNoArrows
case caret == 0:
if gtx.Locale.Direction.Progression() == system.FromOrigin {
keys = keyFilterNoLeftUp
} else {
keys = keyFilterNoRightDown
}
case caret == e.text.Len():
if gtx.Locale.Direction.Progression() == system.FromOrigin {
keys = keyFilterNoRightDown
} else {
keys = keyFilterNoLeftUp
}
default:
keys = keyFilterAllArrows
}
}
key.InputOp{Tag: &e.eventKey, Hint: e.InputHint, Keys: keys}.Add(gtx.Ops)
if e.requestFocus {
key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops)
key.SoftKeyboardOp{Show: true}.Add(gtx.Ops)
}
e.requestFocus = false
e.scroller.Add(gtx.Ops)
var scrollRange image.Rectangle
if e.SingleLine {
scrollOffX := e.text.ScrollOff().X
scrollRange.Min.X = min(-scrollOffX, 0)
scrollRange.Max.X = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
} else {
scrollOffY := e.text.ScrollOff().Y
scrollRange.Min.Y = -scrollOffY
scrollRange.Max.Y = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y))
}
e.scroller.Add(gtx.Ops, scrollRange)
e.clicker.Add(gtx.Ops)
e.dragger.Add(gtx.Ops)
e.showCaret = false
if gtx.Focused(e) {
if e.focused {
now := gtx.Now
dt := now.Sub(e.blinkStart)
blinking := dt < maxBlinkDuration
const timePerBlink = time.Second / blinksPerSecond
nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2))
if blinking {
gtx.Execute(op.InvalidateCmd{At: nextBlink})
redraw := op.InvalidateOp{At: nextBlink}
redraw.Add(gtx.Ops)
}
e.showCaret = !blinking || dt%timePerBlink < timePerBlink/2
e.showCaret = e.focused && (!blinking || dt%timePerBlink < timePerBlink/2)
}
disabled := gtx.Queue == nil
semantic.Editor.Add(gtx.Ops)
if e.Len() > 0 {
e.paintSelection(gtx, selectMaterial)
e.paintText(gtx, textMaterial)
}
if gtx.Enabled() {
if !disabled {
e.paintCaret(gtx, textMaterial)
}
return visibleDims
@@ -738,7 +694,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
// material to set the painting material for the selection.
func (e *Editor) paintSelection(gtx layout.Context, material op.CallOp) {
e.initBuffer()
if !gtx.Focused(e) {
if !e.focused {
return
}
e.text.PaintSelection(gtx, material)
@@ -802,10 +758,10 @@ func (e *Editor) CaretCoords() f32.Point {
//
// If there is a selection, it is deleted and counts as a single grapheme
// cluster.
func (e *Editor) Delete(graphemeClusters int) (deletedRunes int) {
func (e *Editor) Delete(graphemeClusters int) {
e.initBuffer()
if graphemeClusters == 0 {
return 0
return
}
start, end := e.text.Selection()
@@ -821,10 +777,9 @@ func (e *Editor) Delete(graphemeClusters int) (deletedRunes int) {
// Reset xoff.
e.text.MoveCaret(0, 0)
e.ClearSelection()
return end - start
}
func (e *Editor) Insert(s string) (insertedRunes int) {
func (e *Editor) Insert(s string) {
e.initBuffer()
if e.SingleLine {
s = strings.ReplaceAll(s, "\n", " ")
@@ -838,7 +793,6 @@ func (e *Editor) Insert(s string) (insertedRunes int) {
e.text.MoveCaret(0, 0)
e.SetCaret(start+moves, start+moves)
e.scrollCaret = true
return moves
}
// modification represents a change to the contents of the editor buffer.
@@ -858,10 +812,10 @@ type modification struct {
// undo applies the modification at e.history[e.historyIdx] and decrements
// e.historyIdx.
func (e *Editor) undo() (EditorEvent, bool) {
func (e *Editor) undo() {
e.initBuffer()
if len(e.history) < 1 || e.nextHistoryIdx == 0 {
return nil, false
return
}
mod := e.history[e.nextHistoryIdx-1]
replaceEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent)
@@ -869,15 +823,14 @@ func (e *Editor) undo() (EditorEvent, bool) {
caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent)
e.SetCaret(caretEnd, mod.StartRune)
e.nextHistoryIdx--
return ChangeEvent{}, true
}
// redo applies the modification at e.history[e.historyIdx] and increments
// e.historyIdx.
func (e *Editor) redo() (EditorEvent, bool) {
func (e *Editor) redo() {
e.initBuffer()
if len(e.history) < 1 || e.nextHistoryIdx == len(e.history) {
return nil, false
return
}
mod := e.history[e.nextHistoryIdx]
end := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent)
@@ -885,7 +838,6 @@ func (e *Editor) redo() (EditorEvent, bool) {
caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent)
e.SetCaret(caretEnd, mod.StartRune)
e.nextHistoryIdx++
return ChangeEvent{}, true
}
// replace the text between start and end with s. Indices are in runes.
@@ -969,18 +921,18 @@ func (e *Editor) MoveCaret(startDelta, endDelta int) {
// Positive is forward, negative is backward.
// Absolute values greater than one will delete that many words.
// The selection counts as a single word.
func (e *Editor) deleteWord(distance int) (deletedRunes int) {
func (e *Editor) deleteWord(distance int) {
if distance == 0 {
return
}
start, end := e.text.Selection()
if start != end {
deletedRunes = e.Delete(1)
e.Delete(1)
distance -= sign(distance)
}
if distance == 0 {
return deletedRunes
return
}
// split the distance information into constituent parts to be
@@ -1020,8 +972,7 @@ func (e *Editor) deleteWord(distance int) (deletedRunes int) {
runes += 1
}
}
deletedRunes += e.Delete(runes * direction)
return deletedRunes
e.Delete(runes * direction)
}
// SelectionLen returns the length of the selection, in runes; it is
+80 -127
View File
@@ -20,7 +20,7 @@ import (
"gioui.org/font"
"gioui.org/font/gofont"
"gioui.org/font/opentype"
"gioui.org/io/input"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
@@ -96,14 +96,17 @@ func assertContents(t *testing.T, e *Editor, contents string, selectionStart, se
// TestEditorReadOnly ensures that mouse and keyboard interactions with readonly
// editors do nothing but manipulate the text selection.
func TestEditorReadOnly(t *testing.T) {
r := new(input.Router)
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Constraints{
Max: image.Pt(100, 100),
},
Locale: english,
Source: r.Source(),
}
gtx.Queue = &testQueue{
events: []event.Event{
key.FocusEvent{Focus: true},
},
}
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
@@ -115,23 +118,12 @@ func TestEditorReadOnly(t *testing.T) {
if cStart != cEnd {
t.Errorf("unexpected initial caret positions")
}
gtx.Execute(key.FocusCmd{Tag: e})
layoutEditor := func() layout.Dimensions {
return e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
}
layoutEditor()
r.Frame(gtx.Ops)
gtx.Ops.Reset()
layoutEditor()
r.Frame(gtx.Ops)
gtx.Ops.Reset()
layoutEditor()
r.Frame(gtx.Ops)
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
// Select everything.
gtx.Ops.Reset()
r.Queue(key.Event{Name: "A", Modifiers: key.ModShortcut})
layoutEditor()
gtx.Queue = &testQueue{events: []event.Event{key.Event{Name: "A", Modifiers: key.ModShortcut}}}
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
textContent := e.Text()
cStart2, cEnd2 := e.Selection()
if cStart2 > cEnd2 {
@@ -146,7 +138,7 @@ func TestEditorReadOnly(t *testing.T) {
// Type some new characters.
gtx.Ops.Reset()
r.Queue(key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"})
gtx.Queue = &testQueue{events: []event.Event{key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"}}}
e.Update(gtx)
textContent2 := e.Text()
if textContent2 != textContent {
@@ -155,8 +147,8 @@ func TestEditorReadOnly(t *testing.T) {
// Try to delete selection.
gtx.Ops.Reset()
r.Queue(key.Event{Name: key.NameDeleteBackward})
dims := layoutEditor()
gtx.Queue = &testQueue{events: []event.Event{key.Event{Name: key.NameDeleteBackward}}}
dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
textContent2 = e.Text()
if textContent2 != textContent {
t.Errorf("readonly editor modified by delete key.Event")
@@ -165,14 +157,14 @@ func TestEditorReadOnly(t *testing.T) {
// Click and drag from the middle of the first line
// to the center.
gtx.Ops.Reset()
r.Queue(
gtx.Queue = &testQueue{events: []event.Event{
pointer.Event{
Kind: pointer.Press,
Buttons: pointer.ButtonPrimary,
Position: f32.Pt(float32(dims.Size.X)*.5, 5),
},
pointer.Event{
Kind: pointer.Move,
Kind: pointer.Drag,
Buttons: pointer.ButtonPrimary,
Position: layout.FPt(dims.Size).Mul(.5),
},
@@ -181,7 +173,7 @@ func TestEditorReadOnly(t *testing.T) {
Buttons: pointer.ButtonPrimary,
Position: layout.FPt(dims.Size).Mul(.5),
},
)
}}
e.Update(gtx)
cStart3, cEnd3 := e.Selection()
if cStart3 == cStart2 || cEnd3 == cEnd2 {
@@ -504,22 +496,22 @@ func TestEditorLigature(t *testing.T) {
func TestEditorDimensions(t *testing.T) {
e := new(Editor)
r := new(input.Router)
tq := &testQueue{
events: []event.Event{
key.EditEvent{Text: "A"},
},
}
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Constraints{Max: image.Pt(100, 100)},
Source: r.Source(),
Queue: tq,
Locale: english,
}
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
gtx.Execute(key.FocusCmd{Tag: e})
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
r.Queue(key.EditEvent{Text: "A"})
dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
if dims.Size.X < 5 {
if dims.Size.X == 0 {
t.Errorf("EditEvent was not reflected in Editor width")
}
}
@@ -890,11 +882,9 @@ f 2 4 6 8 f
g 2 4 6 8 g
`)
r := new(input.Router)
gtx := layout.Context{
Ops: new(op.Ops),
Locale: english,
Source: r.Source(),
}
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
font := font.Font{}
@@ -902,39 +892,42 @@ g 2 4 6 8 g
var tim time.Duration
selected := func(start, end int) string {
gtx.Execute(key.FocusCmd{Tag: e})
// Layout once with no events; populate e.lines.
gtx.Queue = nil
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
_ = e.Events() // throw away any events from this layout
r.Frame(gtx.Ops)
gtx.Source = r.Source()
// Build the selection events
startPos := e.text.closestToRune(start)
endPos := e.text.closestToRune(end)
r.Queue(
pointer.Event{
Buttons: pointer.ButtonPrimary,
Kind: pointer.Press,
Source: pointer.Mouse,
Time: tim,
Position: f32.Pt(textWidth(e, startPos.lineCol.line, 0, startPos.lineCol.col), textBaseline(e, startPos.lineCol.line)),
tq := &testQueue{
events: []event.Event{
pointer.Event{
Buttons: pointer.ButtonPrimary,
Kind: pointer.Press,
Source: pointer.Mouse,
Time: tim,
Position: f32.Pt(textWidth(e, startPos.lineCol.line, 0, startPos.lineCol.col), textBaseline(e, startPos.lineCol.line)),
},
pointer.Event{
Kind: pointer.Release,
Source: pointer.Mouse,
Time: tim,
Position: f32.Pt(textWidth(e, endPos.lineCol.line, 0, endPos.lineCol.col), textBaseline(e, endPos.lineCol.line)),
},
},
pointer.Event{
Kind: pointer.Release,
Source: pointer.Mouse,
Time: tim,
Position: f32.Pt(textWidth(e, endPos.lineCol.line, 0, endPos.lineCol.col), textBaseline(e, endPos.lineCol.line)),
},
)
}
tim += time.Second // Avoid multi-clicks.
gtx.Queue = tq
for {
_, ok := e.Update(gtx) // throw away any events from this layout
if !ok {
break
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
for _, evt := range e.Events() {
switch evt.(type) {
case SelectEvent:
return e.SelectedText()
}
}
return e.SelectedText()
return ""
}
type screenPos image.Point
logicalPosMatch := func(t *testing.T, n int, label string, expected screenPos, actual combinedPos) {
@@ -972,7 +965,7 @@ g 2 4 6 8 g
// Constrain the editor to roughly 6 columns wide and redraw
gtx.Constraints = layout.Exact(image.Pt(36, 36))
// Keep existing selection
gtx = gtx.Disabled()
gtx.Queue = nil
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
caretStart := e.text.closestToRune(e.text.caret.start)
@@ -987,30 +980,19 @@ func TestSelectMove(t *testing.T) {
e := new(Editor)
e.SetText(`0123456789`)
r := new(input.Router)
gtx := layout.Context{
Ops: new(op.Ops),
Locale: english,
Source: r.Source(),
}
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
font := font.Font{}
fontSize := unit.Sp(10)
// Layout once to populate e.lines and get focus.
gtx.Execute(key.FocusCmd{Tag: e})
gtx.Queue = newQueue(key.FocusEvent{Focus: true})
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
// Set up selecton so the Editor key handler filters for all 4 directional keys.
e.SetCaret(3, 6)
gtx.Ops.Reset()
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
gtx.Ops.Reset()
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
for _, keyName := range []key.Name{key.NameLeftArrow, key.NameRightArrow, key.NameUpArrow, key.NameDownArrow} {
testKey := func(keyName string) {
// Select 345
e.SetCaret(3, 6)
if expected, got := "345", e.SelectedText(); expected != got {
@@ -1018,15 +1000,18 @@ func TestSelectMove(t *testing.T) {
}
// Press the key
r.Queue(key.Event{State: key.Press, Name: keyName})
gtx.Ops.Reset()
gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName})
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
if expected, got := "", e.SelectedText(); expected != got {
t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got)
}
}
testKey(key.NameLeftArrow)
testKey(key.NameRightArrow)
testKey(key.NameUpArrow)
testKey(key.NameDownArrow)
}
func TestEditor_Read(t *testing.T) {
@@ -1079,22 +1064,17 @@ func TestEditor_MaxLen(t *testing.T) {
}
e.SetText("2345678")
r := new(input.Router)
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)),
Source: r.Source(),
Queue: newQueue(
key.EditEvent{Range: key.Range{Start: 0, End: 2}, Text: "1234"},
key.SelectionEvent{Start: 4, End: 4},
),
}
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
gtx.Execute(key.FocusCmd{Tag: e})
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
r.Queue(
key.EditEvent{Range: key.Range{Start: 0, End: 2}, Text: "1234"},
key.SelectionEvent{Start: 4, End: 4},
)
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
if got, want := e.Text(), "12345678"; got != want {
@@ -1115,22 +1095,17 @@ func TestEditor_Filter(t *testing.T) {
}
e.SetText("2345678")
r := new(input.Router)
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)),
Source: r.Source(),
Queue: newQueue(
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1"},
key.SelectionEvent{Start: 4, End: 4},
),
}
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
gtx.Execute(key.FocusCmd{Tag: e})
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
r.Queue(
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1"},
key.SelectionEvent{Start: 4, End: 4},
)
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
if got, want := e.Text(), "12345678"; got != want {
@@ -1145,33 +1120,22 @@ func TestEditor_Submit(t *testing.T) {
e := new(Editor)
e.Submit = true
r := new(input.Router)
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)),
Source: r.Source(),
Queue: newQueue(
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"},
),
}
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
gtx.Execute(key.FocusCmd{Tag: e})
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
r.Queue(
key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"},
)
got := []EditorEvent{}
for {
ev, ok := e.Update(gtx)
if !ok {
break
}
got = append(got, ev)
}
if got, want := e.Text(), "ab1"; got != want {
t.Errorf("editor failed to filter newline")
}
got := e.Events()
want := []EditorEvent{
ChangeEvent{},
SubmitEvent{Text: e.Text()},
@@ -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.
// It assumes single-run lines, which isn't safe with non-test text
// data.
@@ -1223,3 +1164,15 @@ func textBaseline(e *Editor, lineNum int) float32 {
start := e.text.closestToLineCol(lineNum, 0)
return float32(start.y)
}
type testQueue struct {
events []event.Event
}
func newQueue(e ...event.Event) *testQueue {
return &testQueue{events: e}
}
func (q *testQueue) Events(_ event.Tag) []event.Event {
return q.events
}
+10 -20
View File
@@ -4,7 +4,6 @@ package widget
import (
"gioui.org/gesture"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
@@ -41,21 +40,17 @@ func (e *Enum) index(k string) *enumKey {
// Update the state and report whether Value has changed by user interaction.
func (e *Enum) Update(gtx layout.Context) bool {
if !gtx.Enabled() {
if gtx.Queue == nil {
e.focused = false
}
e.hovering = false
changed := false
for _, state := range e.keys {
for {
ev, ok := state.click.Update(gtx.Source)
if !ok {
break
}
for _, ev := range state.click.Update(gtx) {
switch ev.Kind {
case gesture.KindPress:
if ev.Source == pointer.Mouse {
gtx.Execute(key.FocusCmd{Tag: &state.tag})
key.FocusOp{Tag: &state.tag}.Add(gtx.Ops)
}
case gesture.KindClick:
if state.key != e.Value {
@@ -64,15 +59,7 @@ func (e *Enum) Update(gtx layout.Context) bool {
}
}
}
for {
ev, ok := gtx.Event(
key.FocusFilter{Target: &state.tag},
key.Filter{Focus: &state.tag, Name: key.NameReturn},
key.Filter{Focus: &state.tag, Name: key.NameSpace},
)
if !ok {
break
}
for _, ev := range gtx.Events(&state.tag) {
switch ev := ev.(type) {
case key.FocusEvent:
if ev.Focus {
@@ -82,7 +69,7 @@ func (e *Enum) Update(gtx layout.Context) bool {
e.focused = false
}
case key.Event:
if ev.State != key.Release {
if !e.focused || ev.State != key.Release {
break
}
if ev.Name != key.NameReturn && ev.Name != key.NameSpace {
@@ -130,9 +117,12 @@ func (e *Enum) Layout(gtx layout.Context, k string, content layout.Widget) layou
}
clk := &state.click
clk.Add(gtx.Ops)
event.Op(gtx.Ops, &state.tag)
enabled := gtx.Queue != nil
if enabled {
key.InputOp{Tag: &state.tag, Keys: "⏎|Space"}.Add(gtx.Ops)
}
semantic.SelectedOp(k == e.Value).Add(gtx.Ops)
semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops)
semantic.EnabledOp(enabled).Add(gtx.Ops)
c.Add(gtx.Ops)
return dims
+20 -20
View File
@@ -5,13 +5,10 @@ package widget_test
import (
"fmt"
"image"
"io"
"strings"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/io/transfer"
"gioui.org/layout"
"gioui.org/op"
@@ -24,11 +21,11 @@ func ExampleClickable_passthrough() {
// pointer events can be passed down for the underlying
// widgets to pick them up.
var button1, button2 widget.Clickable
var r input.Router
var r router.Router
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)),
Source: r.Source(),
Queue: &r,
}
// widget lays out two buttons on top of each other.
@@ -74,15 +71,15 @@ func ExampleClickable_passthrough() {
}
func ExampleDraggable_Layout() {
var r input.Router
var r router.Router
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Exact(image.Pt(100, 100)),
Source: r.Source(),
Queue: &r,
}
// mime is the type used to match drag and drop operations.
// It could be left empty in this example.
const mime = "MyMime"
mime := "MyMime"
drag := &widget.Draggable{Type: mime}
var drop int
// widget lays out the drag and drop handlers and processes
@@ -97,7 +94,7 @@ func ExampleDraggable_Layout() {
// drag must respond with an Offer event when requested.
// Use the drag method for this.
if m, ok := drag.Update(gtx); ok {
drag.Offer(gtx, m, io.NopCloser(strings.NewReader("hello world")))
drag.Offer(gtx.Ops, m, offer{Data: "hello world"})
}
// Setup the area for drops.
@@ -105,21 +102,17 @@ func ExampleDraggable_Layout() {
Min: image.Pt(20, 20),
Max: image.Pt(40, 40),
}.Push(gtx.Ops)
event.Op(gtx.Ops, &drop)
transfer.TargetOp{
Tag: &drop,
Type: mime, // this must match the drag Type for the drop to succeed
}.Add(gtx.Ops)
ds.Pop()
// Check for the received data.
for {
ev, ok := gtx.Event(transfer.TargetFilter{Target: &drop, Type: mime})
if !ok {
break
}
for _, ev := range gtx.Events(&drop) {
switch e := ev.(type) {
case transfer.DataEvent:
data := e.Open()
defer data.Close()
content, _ := io.ReadAll(data)
fmt.Println(string(content))
fmt.Println(data.(offer).Data)
}
}
}
@@ -152,3 +145,10 @@ func ExampleDraggable_Layout() {
// Output:
// hello world
}
type offer struct {
Data string
}
func (offer) Read([]byte) (int, error) { return 0, nil }
func (offer) Close() error { return nil }
+1 -5
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.
func (f *Float) Update(gtx layout.Context) bool {
changed := false
for {
e, ok := f.drag.Update(gtx.Metric, gtx.Source, gesture.Axis(f.axis))
if !ok {
break
}
for _, e := range f.drag.Update(gtx.Metric, gtx, gesture.Axis(f.axis)) {
if f.length > 0 && (e.Kind == pointer.Press || e.Kind == pointer.Drag) {
pos := e.Position.X
if f.axis == layout.Vertical {
+4
View File
@@ -347,6 +347,10 @@ type Region struct {
Baseline int
}
// region is identical to Region except that its coordinates are in document
// space instead of a widget coordinate space.
type region = Region
// locate returns highlight regions covering the glyphs that represent the runes in
// [startRune,endRune). If the rects parameter is non-nil, locate will use it to
// return results instead of allocating, provided that there is enough capacity.
+8 -8
View File
@@ -284,9 +284,9 @@ func TestIndexPositionBidi(t *testing.T) {
name: "bidi rtl",
glyphs: bidiRTLText,
expectedXs: []fixed.Int26_6{
2646, 3272, 3842, 4412, 4697, 5267, 5837, 6090, 6602, 7114, 2646, 2380, 1577, 985, 687, 266, // Positions on line 0.
2665, 3291, 3861, 4431, 4716, 5286, 5856, 6109, 6621, 7133, 2665, 2380, 1577, 985, 687, 266, // Positions on line 0.
7867, 7099, 6331, 5563, 4795, 4510, 4212, 3914, 3648, 2281, 2566, 3136, 3648, 2281, 2015, 1709, 1117, 266, // Positions on line 1.
7886, 7118, 6350, 5582, 4814, 4529, 4231, 3933, 3667, 2300, 2585, 3155, 3667, 2300, 2015, 1709, 1117, 266, // Positions on line 1.
8794, 8026, 7258, 6490, 5722, 5437, 4922, 4540, 4134, 3868, 0, 290, 860, 1430, 1715, 1989, 2559, 3071, 3583, // Positions on line 2.
@@ -402,7 +402,7 @@ func TestIndexPositionLines(t *testing.T) {
xOff: fixed.Int26_6(0),
yOff: 22,
glyphs: 15,
width: fixed.Int26_6(7114),
width: fixed.Int26_6(7133),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
@@ -410,7 +410,7 @@ func TestIndexPositionLines(t *testing.T) {
xOff: fixed.Int26_6(0),
yOff: 41,
glyphs: 15,
width: fixed.Int26_6(7867),
width: fixed.Int26_6(7886),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
@@ -477,18 +477,18 @@ func TestIndexPositionLines(t *testing.T) {
glyphs: bidiRTLTextOpp,
expectedLines: []lineInfo{
{
xOff: fixed.Int26_6(3126),
xOff: fixed.Int26_6(3107),
yOff: 22,
glyphs: 15,
width: fixed.Int26_6(7114),
width: fixed.Int26_6(7133),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(2373),
xOff: fixed.Int26_6(2354),
yOff: 41,
glyphs: 15,
width: fixed.Int26_6(7867),
width: fixed.Int26_6(7886),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
+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}
}
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.
type textIterator struct {
// viewport is the rectangle of document coordinates that the iterator is
+8 -18
View File
@@ -29,8 +29,8 @@ type Scrollbar struct {
oldDragPos float32
}
// Update updates the internal state of the scrollbar based on events
// since the previous call to Update. The provided axis will be used to
// Layout updates the internal state of the scrollbar based on events
// since the previous call to Layout. The provided axis will be used to
// normalize input event coordinates and constraints into an axis-
// independent format. viewportStart is the position of the beginning
// 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
// and viewportEnd is .5, the viewport described by the scrollbar is
// 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
// the length of the track within which pointer events occur, and is
// 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.
for {
event, ok := s.track.Update(gtx.Source)
if !ok {
break
}
for _, event := range s.track.Update(gtx) {
if event.Kind != gesture.KindClick ||
event.Modifiers != key.Modifiers(0) ||
event.NumClicks > 1 {
@@ -84,11 +80,7 @@ func (s *Scrollbar) Update(gtx layout.Context, axis layout.Axis, viewportStart,
}
// Offset to account for any drags.
for {
event, ok := s.drag.Update(gtx.Metric, gtx.Source, gesture.Axis(axis))
if !ok {
break
}
for _, event := range s.drag.Update(gtx.Metric, gtx, gesture.Axis(axis)) {
switch event.Kind {
case pointer.Drag:
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
// detected properly.
for {
if _, ok := s.indicator.Update(gtx.Source); !ok {
break
}
}
_ = s.indicator.Update(gtx)
return layout.Dimensions{}
}
// 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,
func(gtx layout.Context) layout.Dimensions {
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{}))
}
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()
background := b.Background
switch {
case !gtx.Enabled():
case gtx.Queue == nil:
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)
}
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()
background := b.Background
switch {
case !gtx.Enabled():
case gtx.Queue == nil:
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)
}
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.
if !c.End.IsZero() || sizet <= 1.0 {
gtx.Execute(op.InvalidateCmd{})
op.InvalidateOp{}.Add(gtx.Ops)
}
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 {
size := gtx.Dp(c.Size)
col := c.IconColor
if !gtx.Enabled() {
if gtx.Queue == nil {
col = f32color.Disabled(col)
}
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 {
return c.CheckBox.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
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(...)
//
// material.Button(theme, button, "Click me!").Layout(gtx)
// material.Button(theme, "Click me!").Layout(gtx, button)
//
// # Customization
//
@@ -36,22 +36,22 @@
// Theme-global parameters: For changing the look of all widgets drawn with a
// 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,
// 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.Layout(gtx)
// btn.Layout(gtx, button)
//
// 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
// 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
// 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)
hintColor := hintColorMacro.Stop()
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()
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.Max = gtx.Constraints.Min
s.Scrollbar.Update(gtx, axis, viewportStart, viewportEnd)
s.Scrollbar.Layout(gtx, axis, viewportStart, viewportEnd)
// Darken indicator if hovered.
if s.Scrollbar.IndicatorHovered() {
+8 -5
View File
@@ -3,7 +3,9 @@ package material_test
import (
"image"
"testing"
"time"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
@@ -12,17 +14,18 @@ import (
)
func TestListAnchorStrategies(t *testing.T) {
gtx := layout.Context{
Ops: new(op.Ops),
var ops op.Ops
gtx := layout.NewContext(&ops, system.FrameEvent{
Metric: unit.Metric{
PxPerDp: 1,
PxPerSp: 1,
},
Constraints: layout.Exact(image.Point{
Now: time.Now(),
Size: image.Point{
X: 500,
Y: 500,
}),
}
},
})
gtx.Constraints.Min = image.Point{}
var spaceConstraints layout.Constraints
+1 -1
View File
@@ -47,7 +47,7 @@ func (l LoaderStyle) Layout(gtx layout.Context) layout.Dimensions {
}.Add(gtx.Ops)
defer op.Offset(image.Pt(-radius, -radius)).Push(gtx.Ops).Pop()
paint.PaintOp{}.Add(gtx.Ops)
gtx.Execute(op.InvalidateCmd{})
op.InvalidateOp{}.Add(gtx.Ops)
return layout.Dimensions{
Size: sz,
}
+5 -7
View File
@@ -15,8 +15,6 @@ import (
type ProgressBarStyle struct {
Color color.NRGBA
Height unit.Dp
Radius unit.Dp
TrackColor color.NRGBA
Progress float32
}
@@ -24,8 +22,6 @@ type ProgressBarStyle struct {
func ProgressBar(th *Theme, progress float32) ProgressBarStyle {
return ProgressBarStyle{
Progress: progress,
Height: unit.Dp(4),
Radius: unit.Dp(2),
Color: th.Palette.ContrastBg,
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 {
shader := func(width int, color color.NRGBA) layout.Dimensions {
d := image.Point{X: width, Y: gtx.Dp(p.Height)}
rr := gtx.Dp(p.Radius)
const maxHeight = unit.Dp(4)
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()
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 {
fillWidth := int(float32(progressBarWidth) * clamp1(p.Progress))
fillColor := p.Color
if !gtx.Enabled() {
if gtx.Queue == nil {
fillColor = f32color.Disabled(fillColor)
}
return shader(fillWidth, fillColor)
+1 -1
View File
@@ -56,7 +56,7 @@ func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions {
trans.Pop()
color := s.Color
if !gtx.Enabled() {
if gtx.Queue == nil {
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 {
col = s.Color.Enabled
}
if !gtx.Enabled() {
if gtx.Queue == nil {
col = f32color.Disabled(col)
}
trackColor := s.Color.Track
@@ -98,7 +98,7 @@ func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions {
return clip.Ellipse(b).Op(gtx.Ops)
}
// Draw hover.
if s.Switch.Hovered() || gtx.Focused(s.Switch) {
if s.Switch.Hovered() || s.Switch.Focused() {
r := thumbRadius * 10 / 17
background := f32color.MulAlpha(s.Color.Enabled, 70)
paint.FillShape(gtx.Ops, background, circle(thumbRadius, thumbRadius, r))
+54 -51
View File
@@ -2,7 +2,6 @@ package widget
import (
"image"
"io"
"math"
"strings"
@@ -71,14 +70,21 @@ type Selectable struct {
source stringSource
// scratch is a buffer reused to efficiently read text out of the
// textView.
scratch []byte
lastValue string
text textView
focused bool
dragging bool
dragger gesture.Drag
scratch []byte
lastValue string
text textView
focused bool
requestFocus bool
dragging bool
dragger gesture.Drag
scroller gesture.Scroll
scrollOff image.Point
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
@@ -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.
func (l *Selectable) Focused() bool {
return l.focused
@@ -171,11 +182,10 @@ func (l *Selectable) Truncated() bool {
return l.text.Truncated()
}
// Update the state of the selectable in response to input events. It returns whether the
// text selection changed during event processing.
func (l *Selectable) Update(gtx layout.Context) bool {
// Update the state of the selectable in response to input events.
func (l *Selectable) Update(gtx layout.Context) {
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
@@ -193,7 +203,17 @@ func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font,
dims := l.text.Dimensions()
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
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.dragger.Add(gtx.Ops)
@@ -203,16 +223,18 @@ func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font,
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()
defer func() {
if newStart, newLen := min(l.text.Selection()), l.text.SelectionLen(); oldStart != newStart || oldLen != newLen {
selectionChanged = true
}
}()
l.processPointer(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) {
@@ -227,7 +249,7 @@ func (e *Selectable) processPointer(gtx layout.Context) {
X: int(math.Round(float64(evt.Position.X))),
Y: int(math.Round(float64(evt.Position.Y))),
})
gtx.Execute(key.FocusCmd{Tag: e})
e.requestFocus = true
if evt.Modifiers == key.ModShift {
start, end := e.text.Selection()
// 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 {
var combinedEvents []event.Event
for {
evt, ok := e.clicker.Update(gtx.Source)
if !ok {
break
}
for _, evt := range e.clicker.Update(gtx) {
combinedEvents = append(combinedEvents, evt)
}
for {
evt, ok := e.dragger.Update(gtx.Metric, gtx.Source, gesture.Both)
if !ok {
break
}
for _, evt := range e.dragger.Update(gtx.Metric, gtx, gesture.Both) {
combinedEvents = append(combinedEvents, evt)
}
return combinedEvents
}
func (e *Selectable) processKey(gtx layout.Context) {
for {
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
}
for _, ke := range gtx.Events(e) {
switch ke := ke.(type) {
case key.FocusEvent:
e.focused = ke.Focus
@@ -342,7 +337,7 @@ func (e *Selectable) command(gtx layout.Context, k key.Event) {
case "C", "X":
e.scratch = e.text.SelectedText(e.scratch)
if text := string(e.scratch); text != "" {
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))})
clipboard.WriteOp{Text: text}.Add(gtx.Ops)
}
// Select all
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).
func (l *Selectable) Regions(start, end int, regions []Region) []Region {
l.initialize()

Some files were not shown because too many files have changed in this diff Show More