~eliasnaur/gio-patches

gio: app: add system cursor support v1 PROPOSED

~pierrec
~pierrec: 1
 app: add system cursor support

 17 files changed, 422 insertions(+), 30 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~eliasnaur/gio-patches/patches/15410/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH gio] app: add system cursor support Export this patch

~pierrec
From: pierre <pierre.curto@gmail.com>

Signed-off-by: pierre <pierre.curto@gmail.com>
---
 .builds/freebsd.yml               |  2 ++
 .builds/linux.yml                 |  2 +-
 app/internal/window/GioView.java  | 41 ++++++++++++++++++++++-
 app/internal/window/os_android.go | 28 ++++++++++++++++
 app/internal/window/os_ios.go     | 39 ++++++++++++++++++++++
 app/internal/window/os_ios.m      | 40 ++++++++++++++++++++++
 app/internal/window/os_js.go      | 17 ++++++++++
 app/internal/window/os_macos.go   | 42 ++++++++++++++++++++++-
 app/internal/window/os_macos.m    | 40 ++++++++++++++++++++++
 app/internal/window/os_wayland.go | 55 +++++++++++++++++++++++++------
 app/internal/window/os_windows.go | 42 +++++++++++++++++++++--
 app/internal/window/os_x11.go     | 28 ++++++++++++++--
 app/internal/window/window.go     |  4 +++
 app/internal/windows/windows.go   | 13 +++++++-
 app/window.go                     |  8 +++++
 example/kitchen/main_test.go      | 24 +++++++-------
 io/pointer/pointer.go             | 27 +++++++++++++++
 17 files changed, 422 insertions(+), 30 deletions(-)

diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml
index 7ad7b5d..ee76afa 100644
--- a/.builds/freebsd.yml
+++ b/.builds/freebsd.yml
@@ -3,6 +3,8 @@ image: freebsd/11.x
packages:
 - libX11
 - libxkbcommon
 - libXcursor
 - libXfixes
 - wayland
 - mesa-libs
 - xorg-vfbserver
diff --git a/.builds/linux.yml b/.builds/linux.yml
index cc45d7d..77b12ef 100644
--- a/.builds/linux.yml
+++ b/.builds/linux.yml
@@ -84,7 +84,7 @@ tasks:
     cd gio
     for hash in $(git log -n 20 --format="%H"); do
        message=$(git log -1 --format=%B $hash)
        if [[ ! "$message" =~ "Signed-off-by: " ]]; then
        if [[ ! "$message" =~ "Merge branch" ]] && [[ ! "$message" =~ "Signed-off-by: " ]]; then
            echo "unsigned commit $hash"
            exit 1
        fi
diff --git a/app/internal/window/GioView.java b/app/internal/window/GioView.java
index a55346a..fd8b7b9 100644
--- a/app/internal/window/GioView.java
+++ b/app/internal/window/GioView.java
@@ -22,6 +22,7 @@ import android.view.Choreographer;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowInsets;
@@ -36,7 +37,6 @@ import android.view.inputmethod.EditorInfo;
import java.io.UnsupportedEncodingException;

public final class GioView extends SurfaceView implements Choreographer.FrameCallback {
	private final static Object initLock = new Object();
	private static boolean jniLoaded;

	private final SurfaceHolder.Callback surfCallbacks;
@@ -45,6 +45,9 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
	private final float scrollXScale;
	private final float scrollYScale;

	private static final Object pointerIconLock = new Object();
	private PointerIcon pointerIcon;

	private long nhandle;

	public GioView(Context context) {
@@ -124,7 +127,43 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
		return true;
	}

    private void setCursor(Context ctx, int curID) {
	    int id;
	    switch (curID) {
	        case 0:
                id = PointerIcon.TYPE_NULL;
                break;
            case 1:
                id = PointerIcon.TYPE_ARROW;
                break;
            case 2:
                id = PointerIcon.TYPE_TEXT;
                break;
            case 3:
                id = PointerIcon.TYPE_HAND;
                break;
            case 4:
                id = PointerIcon.TYPE_CROSSHAIR;
                break;
            case 5:
                id = PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
                break;
            case 6:
                id = PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
                break;
            default:
                id = PointerIcon.TYPE_ARROW;
                break;
        }
        synchronized (pointerIconLock) {
	        pointerIcon = PointerIcon.getSystemIcon(ctx, id);
        }
	}

	private void dispatchMotionEvent(MotionEvent event) {
		synchronized (pointerIconLock) {
		    GioView.this.setPointerIcon(pointerIcon);
		}
		for (int j = 0; j < event.getHistorySize(); j++) {
			long time = event.getHistoricalEventTime(j);
			for (int i = 0; i < event.getPointerCount(); i++) {
diff --git a/app/internal/window/os_android.go b/app/internal/window/os_android.go
index fc494e5..f796d81 100644
--- a/app/internal/window/os_android.go
+++ b/app/internal/window/os_android.go
@@ -80,6 +80,7 @@ type window struct {
	mshowTextInput     C.jmethodID
	mhideTextInput     C.jmethodID
	mpostFrameCallback C.jmethodID
	msetCursor         C.jmethodID
}

// ViewEvent is sent whenever the Window's underlying Android view
@@ -202,6 +203,7 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
		mshowTextInput:     getMethodID(env, class, "showTextInput", "()V"),
		mhideTextInput:     getMethodID(env, class, "hideTextInput", "()V"),
		mpostFrameCallback: getMethodID(env, class, "postFrameCallback", "()V"),
		msetCursor:         getMethodID(env, class, "setCursor", "(Landroid/content/Context;I)V"),
	}
	wopts := <-mainWindow.out
	w.callbacks = wopts.window
@@ -649,6 +651,32 @@ func (w *window) ReadClipboard() {
	})
}

func (w *window) SetCursor(name pointer.CursorName) {
	var curID int
	switch name {
	default:
		fallthrough
	case pointer.CursorDefault:
		curID = 1
	case pointer.CursorText:
		curID = 2
	case pointer.CursorPointer:
		curID = 3
	case pointer.CursorCrossHair:
		curID = 4
	case pointer.CursorColResize:
		curID = 5
	case pointer.CursorRowResize:
		curID = 6
	case pointer.CursorNone:
		curID = 0
	}
	runInJVM(javaVM(), func(env *C.JNIEnv) {
		callVoidMethod(env, w.view, w.msetCursor,
			jvalue(android.appCtx), jvalue(curID))
	})
}

// Close the window. Not implemented for Android.
func (w *window) Close() {}

diff --git a/app/internal/window/os_ios.go b/app/internal/window/os_ios.go
index 1865454..cfd72ba 100644
--- a/app/internal/window/os_ios.go
+++ b/app/internal/window/os_ios.go
@@ -22,6 +22,9 @@ __attribute__ ((visibility ("hidden"))) void gio_hideTextInput(CFTypeRef viewRef
__attribute__ ((visibility ("hidden"))) void gio_addLayerToView(CFTypeRef viewRef, CFTypeRef layerRef);
__attribute__ ((visibility ("hidden"))) void gio_updateView(CFTypeRef viewRef, CFTypeRef layerRef);
__attribute__ ((visibility ("hidden"))) void gio_removeLayer(CFTypeRef layerRef);
__attribute__ ((visibility ("hidden"))) void gio_hideCursor();
__attribute__ ((visibility ("hidden"))) void gio_showCursor();
__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID);
__attribute__ ((visibility ("hidden"))) struct drawParams gio_viewDrawParams(CFTypeRef viewRef);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void);
__attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length);
@@ -51,6 +54,7 @@ type window struct {

	layer   C.CFTypeRef
	visible atomic.Value
	cursor  pointer.CursorName

	pointerMap []C.CFTypeRef
}
@@ -249,6 +253,41 @@ func (w *window) SetAnimating(anim bool) {
	}
}

func (w *window) SetCursor(name pointer.CursorName) {
	var curID int
	switch name {
	default:
		name = pointer.CursorDefault
		fallthrough
	case pointer.CursorDefault:
		curID = 1
	case pointer.CursorText:
		curID = 2
	case pointer.CursorPointer:
		curID = 3
	case pointer.CursorCrossHair:
		curID = 4
	case pointer.CursorColResize:
		curID = 5
	case pointer.CursorRowResize:
		curID = 6
	case pointer.CursorNone:
		w.cursor = name
		runOnMain(func() {
			C.gio_hideCursor()
		})
		return
	}
	hidden := w.cursor == pointer.CursorNone
	w.cursor = name
	runOnMain(func() {
		if hidden {
			C.gio_showCursor()
		}
		C.gio_setCursor(C.NSUInteger(curID))
	})
}

func (w *window) onKeyCommand(name string) {
	w.w.Event(key.Event{
		Name: name,
diff --git a/app/internal/window/os_ios.m b/app/internal/window/os_ios.m
index 66830e4..973842e 100644
--- a/app/internal/window/os_ios.m
+++ b/app/internal/window/os_ios.m
@@ -247,6 +247,46 @@ CFTypeRef gio_readClipboard(void) {
	}
}

void gio_hideCursor() {
    @autoreleasepool {
        [NSCursor hide];
    }
}

void gio_showCursor() {
    @autoreleasepool {
        [NSCursor unhide];
    }
}

void gio_setCursor(NSUInteger curID) {
    @autoreleasepool {
        switch (curID) {
        case 1:
            [NSCursor.arrowCursor set];
            break;
        case 2:
            [NSCursor.IBeamCursor set];
            break;
        case 3:
            [NSCursor.pointingHandCursor set];
            break;
        case 4:
            [NSCursor.crosshairCursor set];
            break;
        case 5:
            [NSCursor.resizeLeftRightCursor set];
            break;
        case 6:
            [NSCursor.resizeUpDownCursor set];
            break;
        default:
            [NSCursor.arrowCursor set];
            break;
        }
    }
}

void gio_showTextInput(CFTypeRef viewRef) {
	UIView *view = (__bridge UIView *)viewRef;
	[view becomeFirstResponder];
diff --git a/app/internal/window/os_js.go b/app/internal/window/os_js.go
index e4fb17c..246a55a 100644
--- a/app/internal/window/os_js.go
+++ b/app/internal/window/os_js.go
@@ -426,6 +426,23 @@ func (w *window) WriteClipboard(s string) {
	w.clipboard.Call("writeText", s)
}

func (w *window) SetCursor(name pointer.CursorName) {
	doc := js.Global().Get("document")
	cont := doc.Call("getElementsByTagName", "body")
	if cont.IsNull() {
		return
	}
	body := cont.Get("0")
	if body.IsNull() {
		return
	}
	style := body.Get("style")
	if style.IsNull() {
		return
	}
	style.Set("cursor", string(name))
}

func (w *window) ShowTextInput(show bool) {
	// Run in a goroutine to avoid a deadlock if the
	// focus change result in an event.
diff --git a/app/internal/window/os_macos.go b/app/internal/window/os_macos.go
index 1afec6f..72f2fdd 100644
--- a/app/internal/window/os_macos.go
+++ b/app/internal/window/os_macos.go
@@ -38,6 +38,9 @@ __attribute__ ((visibility ("hidden"))) CGFloat gio_viewHeight(CFTypeRef viewRef
__attribute__ ((visibility ("hidden"))) CGFloat gio_getViewBackingScale(CFTypeRef viewRef);
__attribute__ ((visibility ("hidden"))) CGFloat gio_getScreenBackingScale(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void);
__attribute__ ((visibility ("hidden"))) void gio_hideCursor();
__attribute__ ((visibility ("hidden"))) void gio_showCursor();
__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID);
__attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length);
__attribute__ ((visibility ("hidden"))) void gio_setNeedsDisplay(CFTypeRef viewRef);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
@@ -58,6 +61,7 @@ type window struct {
	w           Callbacks
	stage       system.Stage
	displayLink *displayLink
	cursor      pointer.CursorName

	scale float32
}
@@ -74,7 +78,7 @@ var launched = make(chan struct{})
// cascadeTopLeftFromPoint.
var nextTopLeft C.NSPoint

// mustView is like lookoupView, except that it panics
// mustView is like lookupView, except that it panics
// if the view isn't mapped.
func mustView(view C.CFTypeRef) *window {
	w, ok := lookupView(view)
@@ -122,6 +126,41 @@ func (w *window) WriteClipboard(s string) {
	})
}

func (w *window) SetCursor(name pointer.CursorName) {
	var curID int
	switch name {
	default:
		name = pointer.CursorDefault
		fallthrough
	case pointer.CursorDefault:
		curID = 1
	case pointer.CursorText:
		curID = 2
	case pointer.CursorPointer:
		curID = 3
	case pointer.CursorCrossHair:
		curID = 4
	case pointer.CursorColResize:
		curID = 5
	case pointer.CursorRowResize:
		curID = 6
	case pointer.CursorNone:
		w.cursor = name
		runOnMain(func() {
			C.gio_hideCursor()
		})
		return
	}
	hidden := w.cursor == pointer.CursorNone
	w.cursor = name
	runOnMain(func() {
		if hidden {
			C.gio_showCursor()
		}
		C.gio_setCursor(C.NSUInteger(curID))
	})
}

func (w *window) ShowTextInput(show bool) {}

func (w *window) SetAnimating(anim bool) {
@@ -227,6 +266,7 @@ func gio_onDraw(view C.CFTypeRef) {
func gio_onFocus(view C.CFTypeRef, focus C.BOOL) {
	w := mustView(view)
	w.w.Event(key.FocusEvent{Focus: focus == C.YES})
	w.SetCursor(w.cursor)
}

//export gio_onChangeScreen
diff --git a/app/internal/window/os_macos.m b/app/internal/window/os_macos.m
index b8c0dee..3dddcaa 100644
--- a/app/internal/window/os_macos.m
+++ b/app/internal/window/os_macos.m
@@ -67,6 +67,46 @@ CFTypeRef gio_readClipboard(void) {
	}
}

void gio_hideCursor() {
    @autoreleasepool {
        [NSCursor hide];
    }
}

void gio_showCursor() {
    @autoreleasepool {
        [NSCursor unhide];
    }
}

void gio_setCursor(NSUInteger curID) {
    @autoreleasepool {
        switch (curID) {
        case 1:
            [NSCursor.arrowCursor set];
            break;
        case 2:
            [NSCursor.IBeamCursor set];
            break;
        case 3:
            [NSCursor.pointingHandCursor set];
            break;
        case 4:
            [NSCursor.crosshairCursor set];
            break;
        case 5:
            [NSCursor.resizeLeftRightCursor set];
            break;
        case 6:
            [NSCursor.resizeUpDownCursor set];
            break;
        default:
            [NSCursor.arrowCursor set];
            break;
        }
    }
}

CGFloat gio_viewHeight(CFTypeRef viewRef) {
	NSView *view = (__bridge NSView *)viewRef;
	return [view bounds].size.height;
diff --git a/app/internal/window/os_wayland.go b/app/internal/window/os_wayland.go
index 60f59b1..59d1071 100644
--- a/app/internal/window/os_wayland.go
+++ b/app/internal/window/os_wayland.go
@@ -764,16 +764,7 @@ func gio_onPointerEnter(data unsafe.Pointer, pointer *C.struct_wl_pointer, seria
	s.serial = serial
	w := callbackLoad(unsafe.Pointer(surf)).(*window)
	s.pointerFocus = w
	// Get images[0].
	img := *w.cursor.cursor.images
	buf := C.wl_cursor_image_get_buffer(img)
	if buf == nil {
		return
	}
	C.wl_pointer_set_cursor(pointer, serial, w.cursor.surf, C.int32_t(img.hotspot_x), C.int32_t(img.hotspot_y))
	C.wl_surface_attach(w.cursor.surf, buf, 0, 0)
	C.wl_surface_damage(w.cursor.surf, 0, 0, C.int32_t(img.width), C.int32_t(img.height))
	C.wl_surface_commit(w.cursor.surf)
	w.setCursor(pointer, serial)
	w.lastPos = f32.Point{X: fromFixed(x), Y: fromFixed(y)}
}

@@ -917,6 +908,50 @@ func (w *window) WriteClipboard(s string) {
	w.disp.wakeup()
}

func (w *window) SetCursor(name pointer.CursorName) {
	if name == pointer.CursorNone {
		C.wl_pointer_set_cursor(w.disp.seat.pointer, w.serial, nil, 0, 0)
		return
	}
	switch name {
	default:
		fallthrough
	case pointer.CursorDefault:
		name = "left_ptr"
	case pointer.CursorText:
		name = "xterm"
	case pointer.CursorPointer:
		name = "hand1"
	case pointer.CursorCrossHair:
		name = "crosshair"
	case pointer.CursorRowResize:
		name = "top_side"
	case pointer.CursorColResize:
		name = "left_side"
	}
	cname := C.CString(string(name))
	defer C.free(unsafe.Pointer(cname))
	c := C.wl_cursor_theme_get_cursor(w.cursor.theme, cname)
	if c == nil {
		return
	}
	w.cursor.cursor = c
	w.setCursor(w.disp.seat.pointer, w.serial)
}

func (w *window) setCursor(pointer *C.struct_wl_pointer, serial C.uint32_t) {
	// Get images[0].
	img := *w.cursor.cursor.images
	buf := C.wl_cursor_image_get_buffer(img)
	if buf == nil {
		return
	}
	C.wl_pointer_set_cursor(pointer, serial, w.cursor.surf, C.int32_t(img.hotspot_x), C.int32_t(img.hotspot_y))
	C.wl_surface_attach(w.cursor.surf, buf, 0, 0)
	C.wl_surface_damage(w.cursor.surf, 0, 0, C.int32_t(img.width), C.int32_t(img.height))
	C.wl_surface_commit(w.cursor.surf)
}

func (w *window) resetFling() {
	w.fling.start = false
	w.fling.anim = fling.Animation{}
diff --git a/app/internal/window/os_windows.go b/app/internal/window/os_windows.go
index 2a38d53..ae706f9 100644
--- a/app/internal/window/os_windows.go
+++ b/app/internal/window/os_windows.go
@@ -45,6 +45,7 @@ type window struct {
	height      int
	stage       system.Stage
	pointerBtns pointer.Buttons
	cursor      syscall.Handle

	mu        sync.Mutex
	animating bool
@@ -105,6 +106,9 @@ func NewWindow(window Callbacks, opts *Options) error {
		windows.ShowWindow(w.hwnd, windows.SW_SHOWDEFAULT)
		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)
		if err := w.loop(); err != nil {
			panic(err)
		}
@@ -120,17 +124,16 @@ func initResources() error {
		return err
	}
	resources.handle = hInst
	curs, err := windows.LoadCursor(windows.IDC_ARROW)
	c, err := loadCursor(pointer.CursorDefault)
	if err != nil {
		return err
	}
	resources.cursor = curs
	resources.cursor = c
	wcls := windows.WndClassEx{
		CbSize:        uint32(unsafe.Sizeof(windows.WndClassEx{})),
		Style:         windows.CS_HREDRAW | windows.CS_VREDRAW | windows.CS_OWNDC,
		LpfnWndProc:   syscall.NewCallback(windowProc),
		HInstance:     hInst,
		HCursor:       curs,
		LpszClassName: syscall.StringToUTF16Ptr("GioWindow"),
	}
	cls, err := windows.RegisterClassEx(&wcls)
@@ -299,6 +302,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
				Y: w.minmax.maxHeight + w.deltas.height,
			}
		}
	case windows.WM_SETCURSOR:
		windows.SetCursor(w.cursor)
	}

	return windows.DefWindowProc(hwnd, msg, wParam, lParam)
@@ -552,6 +557,37 @@ func (w *window) writeClipboard(s string) error {
	return nil
}

func (w *window) SetCursor(name pointer.CursorName) {
	c, err := loadCursor(name)
	if err != nil {
		c = resources.cursor
	}
	w.cursor = c
}

func loadCursor(name pointer.CursorName) (syscall.Handle, error) {
	var curID uint16
	switch name {
	default:
		fallthrough
	case pointer.CursorDefault:
		return resources.cursor, nil
	case pointer.CursorText:
		curID = windows.IDC_IBEAM
	case pointer.CursorPointer:
		curID = windows.IDC_HAND
	case pointer.CursorCrossHair:
		curID = windows.IDC_CROSS
	case pointer.CursorColResize:
		curID = windows.IDC_SIZEWE
	case pointer.CursorRowResize:
		curID = windows.IDC_SIZENS
	case pointer.CursorNone:
		return 0, nil
	}
	return windows.LoadCursor(curID)
}

func (w *window) ShowTextInput(show bool) {}

func (w *window) HDC() syscall.Handle {
diff --git a/app/internal/window/os_x11.go b/app/internal/window/os_x11.go
index 413a959..ae8fb51 100644
--- a/app/internal/window/os_x11.go
+++ b/app/internal/window/os_x11.go
@@ -7,8 +7,8 @@ package window
/*
#cgo openbsd CFLAGS: -I/usr/X11R6/include -I/usr/local/include
#cgo openbsd LDFLAGS: -L/usr/X11R6/lib -L/usr/local/lib
#cgo freebsd openbsd LDFLAGS: -lX11 -lxkbcommon -lxkbcommon-x11 -lX11-xcb
#cgo linux pkg-config: x11 xkbcommon xkbcommon-x11 x11-xcb
#cgo freebsd openbsd LDFLAGS: -lX11 -lxkbcommon -lxkbcommon-x11 -lX11-xcb -lXcursor -lXfixes
#cgo linux pkg-config: x11 xkbcommon xkbcommon-x11 x11-xcb xcursor xfixes

#include <stdlib.h>
#include <locale.h>
@@ -18,6 +18,8 @@ package window
#include <X11/Xresource.h>
#include <X11/XKBlib.h>
#include <X11/Xlib-xcb.h>
#include <X11/extensions/Xfixes.h>
#include <X11/Xcursor/Xcursor.h>
#include <xkbcommon/xkbcommon-x11.h>

*/
@@ -87,6 +89,7 @@ type x11Window struct {
		write   *string
		content []byte
	}
	cursor pointer.CursorName
}

func (w *x11Window) SetAnimating(anim bool) {
@@ -112,6 +115,27 @@ func (w *x11Window) WriteClipboard(s string) {
	w.wakeup()
}

func (w *x11Window) SetCursor(name pointer.CursorName) {
	if name == pointer.CursorNone {
		w.cursor = name
		C.XFixesHideCursor(w.x, w.xw)
		return
	}
	if w.cursor == pointer.CursorNone {
		C.XFixesShowCursor(w.x, w.xw)
	}
	cname := C.CString(string(name))
	defer C.free(unsafe.Pointer(cname))
	c := C.XcursorLibraryLoadCursor(w.x, cname)
	if c == 0 {
		name = pointer.CursorDefault
	}
	w.cursor = name
	// If c if null (i.e. name was not found),
	// XDefineCursor will use the default cursor.
	C.XDefineCursor(w.x, w.xw, c)
}

func (w *x11Window) ShowTextInput(show bool) {}

// Close the window.
diff --git a/app/internal/window/window.go b/app/internal/window/window.go
index 30ba360..f140214 100644
--- a/app/internal/window/window.go
+++ b/app/internal/window/window.go
@@ -9,6 +9,7 @@ import (

	"gioui.org/gpu/backend"
	"gioui.org/io/event"
	"gioui.org/io/pointer"
	"gioui.org/io/system"
	"gioui.org/unit"
)
@@ -60,6 +61,9 @@ type Driver interface {
	// WriteClipboard requests a clipboard write.
	WriteClipboard(s string)

	// SetCursor updates the current cursor to name.
	SetCursor(name pointer.CursorName)

	// Close the window.
	Close()
}
diff --git a/app/internal/windows/windows.go b/app/internal/windows/windows.go
index 21b1858..22435b4 100644
--- a/app/internal/windows/windows.go
+++ b/app/internal/windows/windows.go
@@ -61,7 +61,12 @@ const (

	CW_USEDEFAULT = -2147483648

	IDC_ARROW = 32512
	IDC_ARROW  = 32512
	IDC_IBEAM  = 32513
	IDC_HAND   = 32649
	IDC_CROSS  = 32515
	IDC_SIZENS = 32645
	IDC_SIZEWE = 32644

	INFINITE = 0xFFFFFFFF

@@ -146,6 +151,7 @@ const (
	WM_PAINT         = 0x000F
	WM_CLOSE         = 0x0010
	WM_QUIT          = 0x0012
	WM_SETCURSOR     = 0x0020
	WM_SETFOCUS      = 0x0007
	WM_KILLFOCUS     = 0x0008
	WM_SHOWWINDOW    = 0x0018
@@ -227,6 +233,7 @@ var (
	_ScreenToClient              = user32.NewProc("ScreenToClient")
	_ShowWindow                  = user32.NewProc("ShowWindow")
	_SetCapture                  = user32.NewProc("SetCapture")
	_SetCursor                   = user32.NewProc("SetCursor")
	_SetClipboardData            = user32.NewProc("SetClipboardData")
	_SetForegroundWindow         = user32.NewProc("SetForegroundWindow")
	_SetFocus                    = user32.NewProc("SetFocus")
@@ -514,6 +521,10 @@ func SetClipboardData(format uint32, mem syscall.Handle) error {
	return nil
}

func SetCursor(h syscall.Handle) {
	_SetCursor.Call(uintptr(h))
}

func SetTimer(hwnd syscall.Handle, nIDEvent uintptr, uElapse uint32, timerProc uintptr) error {
	r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), uintptr(uElapse), timerProc)
	if r == 0 {
diff --git a/app/window.go b/app/window.go
index ed46958..136350a 100644
--- a/app/window.go
+++ b/app/window.go
@@ -10,6 +10,7 @@ import (

	"gioui.org/app/internal/window"
	"gioui.org/io/event"
	"gioui.org/io/pointer"
	"gioui.org/io/profile"
	"gioui.org/io/router"
	"gioui.org/io/system"
@@ -209,6 +210,13 @@ func (w *Window) WriteClipboard(s string) {
	})
}

// SetCursorName changes the current window cursor to name.
func (w *Window) SetCursorName(name pointer.CursorName) {
	go w.driverDo(func() {
		w.driver.SetCursor(name)
	})
}

// Close the window. The window's event loop should exit when it receives
// system.DestroyEvent.
//
diff --git a/example/kitchen/main_test.go b/example/kitchen/main_test.go
index 91a1130..c3c16e5 100644
--- a/example/kitchen/main_test.go
+++ b/example/kitchen/main_test.go
@@ -7,19 +7,21 @@ import (
	"testing"
	"time"

	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/app/headless"
	"gioui.org/f32"
	"gioui.org/font/gofont"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/widget/material"
)

func BenchmarkUI(b *testing.B) { benchmarkUI(b, transformation{}) }
func BenchmarkUI(b *testing.B)        { benchmarkUI(b, transformation{}) }
func BenchmarkUI_Offset(b *testing.B) { benchmarkUI(b, transformation{offset: true}) }
func BenchmarkUI_Scale(b *testing.B) { benchmarkUI(b, transformation{scale: true}) }
func BenchmarkUI_Scale(b *testing.B)  { benchmarkUI(b, transformation{scale: true}) }
func BenchmarkUI_Rotate(b *testing.B) { benchmarkUI(b, transformation{rotate: true}) }
func BenchmarkUI_All(b *testing.B) { benchmarkUI(b, transformation{offset: true, rotate: true, scale: true}) }
func BenchmarkUI_All(b *testing.B) {
	benchmarkUI(b, transformation{offset: true, rotate: true, scale: true})
}

func benchmarkUI(b *testing.B, transform transformation) {
	th := material.NewTheme(gofont.Collection())
@@ -42,19 +44,19 @@ func benchmarkUI(b *testing.B, transform transformation) {
			Constraints: layout.Exact(image.Pt(800, 600)),
		}
		addTransform(i, transform, gtx.Ops)
		layoutTime += measure(func(){ kitchen(gtx, th) })
		frameTime += measure(func(){ w.Frame(&ops) })
		layoutTime += measure(func() { kitchen(gtx, th) })
		frameTime += measure(func() { w.Frame(&ops) })
	}
	b.StopTimer()

	b.ReportMetric(float64(layoutTime.Nanoseconds()) / float64(b.N), "ns/layout")
	b.ReportMetric(float64(frameTime.Nanoseconds()) / float64(b.N), "ns/frame")
	b.ReportMetric(float64(layoutTime.Nanoseconds())/float64(b.N), "ns/layout")
	b.ReportMetric(float64(frameTime.Nanoseconds())/float64(b.N), "ns/frame")
}

type transformation struct {
	offset bool
	rotate bool
	scale bool
	scale  bool
}

func addTransform(i int, transform transformation, ops *op.Ops) {
@@ -88,4 +90,4 @@ func measure(fn func()) time.Duration {
	start := time.Now()
	fn()
	return time.Since(start)
}
\ No newline at end of file
}
diff --git a/io/pointer/pointer.go b/io/pointer/pointer.go
index 2c0010a..a5b6c2c 100644
--- a/io/pointer/pointer.go
+++ b/io/pointer/pointer.go
@@ -79,9 +79,29 @@ type Source uint8
// Buttons is a set of mouse buttons
type Buttons uint8

// CursorName is the name of a cursor.
type CursorName string

// Must match app/internal/input.areaKind
type areaKind uint8

const (
	// CursorDefault is the default cursor.
	CursorDefault CursorName = ""
	// CursorText is the cursor for text.
	CursorText CursorName = "text"
	// CursorPointer is the cursor for a link.
	CursorPointer CursorName = "pointer"
	// CursorCrossHair is the cursor for precise location.
	CursorCrossHair CursorName = "crosshair"
	// CursorColResize is the cursor for vertical resize.
	CursorColResize CursorName = "col-resize"
	// CursorRowResize is the cursor for horizontal resize.
	CursorRowResize CursorName = "row-resize"
	// CursorNone hides the cursor. To show it again, use any other cursor.
	CursorNone CursorName = "none"
)

const (
	// A Cancel event is generated when the current gesture is
	// interrupted by other handlers or the system.
@@ -242,4 +262,11 @@ func (b Buttons) String() string {
	return strings.Join(strs, "|")
}

func (c CursorName) String() string {
	if c == CursorDefault {
		return "default"
	}
	return string(c)
}

func (Event) ImplementsEvent() {}
-- 
2.26.2