diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index 7ad7b5df..ee76afa6 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/app/internal/window/GioView.java b/app/internal/window/GioView.java index a55346ac..822a3ed9 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; @@ -124,6 +124,11 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal return true; } + private void setCursor(Context ctx, int id) { + PointerIcon pointerIcon = PointerIcon.getSystemIcon(ctx, id); + GioView.this.setPointerIcon(pointerIcon); + } + private void dispatchMotionEvent(MotionEvent event) { for (int j = 0; j < event.getHistorySize(); j++) { long time = event.getHistoricalEventTime(j); diff --git a/app/internal/window/os_android.go b/app/internal/window/os_android.go index fc494e5c..a0eae136 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 = 1000 // TYPE_ARROW + case pointer.CursorText: + curID = 1008 // TYPE_TEXT + case pointer.CursorPointer: + curID = 1002 // TYPE_HAND + case pointer.CursorCrossHair: + curID = 1007 // TYPE_CROSSHAIR + case pointer.CursorColResize: + curID = 1014 // TYPE_HORIZONTAL_DOUBLE_ARROW + case pointer.CursorRowResize: + curID = 1015 // TYPE_VERTICAL_DOUBLE_ARROW + case pointer.CursorNone: + curID = 0 // TYPE_NULL + } + runOnMain(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_darwin.go b/app/internal/window/os_darwin.go index a3b77dc5..0c6b3555 100644 --- a/app/internal/window/os_darwin.go +++ b/app/internal/window/os_darwin.go @@ -13,6 +13,9 @@ __attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl __attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl); __attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl); __attribute__ ((visibility ("hidden"))) void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did); +__attribute__ ((visibility ("hidden"))) void gio_hideCursor(); +__attribute__ ((visibility ("hidden"))) void gio_showCursor(); +__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID); */ import "C" import ( @@ -22,6 +25,8 @@ import ( "time" "unicode/utf16" "unsafe" + + "gioui.org/io/pointer" ) // displayLink is the state for a display link (CVDisplayLinkRef on macOS, @@ -169,3 +174,41 @@ func gio_onFrameCallback(dl C.CFTypeRef) { } } } + +// windowSetCursor updates the cursor from the current one to a new one +// and returns the new one. +func windowSetCursor(from, to pointer.CursorName) pointer.CursorName { + if from == to { + return to + } + var curID int + switch to { + default: + to = 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: + runOnMain(func() { + C.gio_hideCursor() + }) + return to + } + runOnMain(func() { + if from == pointer.CursorNone { + C.gio_showCursor() + } + C.gio_setCursor(C.NSUInteger(curID)) + }) + return to +} diff --git a/app/internal/window/os_darwin.m b/app/internal/window/os_darwin.m index dc4f00b7..f2fcf67f 100644 --- a/app/internal/window/os_darwin.m +++ b/app/internal/window/os_darwin.m @@ -20,3 +20,43 @@ void gio_nsstringGetCharacters(CFTypeRef cstr, unichar *chars, NSUInteger loc, N NSString *str = (__bridge NSString *)cstr; [str getCharacters:chars range:NSMakeRange(loc, length)]; } + +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; + } + } +} diff --git a/app/internal/window/os_ios.go b/app/internal/window/os_ios.go index 1865454d..daf581a9 100644 --- a/app/internal/window/os_ios.go +++ b/app/internal/window/os_ios.go @@ -51,6 +51,7 @@ type window struct { layer C.CFTypeRef visible atomic.Value + cursor pointer.CursorName pointerMap []C.CFTypeRef } @@ -249,6 +250,10 @@ func (w *window) SetAnimating(anim bool) { } } +func (w *window) SetCursor(name pointer.CursorName) { + w.cursor = windowSetCursor(w.cursor, name) +} + func (w *window) onKeyCommand(name string) { w.w.Event(key.Event{ Name: name, diff --git a/app/internal/window/os_js.go b/app/internal/window/os_js.go index e4fb17cc..74a7efee 100644 --- a/app/internal/window/os_js.go +++ b/app/internal/window/os_js.go @@ -426,6 +426,11 @@ func (w *window) WriteClipboard(s string) { w.clipboard.Call("writeText", s) } +func (w *window) SetCursor(name pointer.CursorName) { + style := w.cnv.Get("style") + 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 1afec6f7..2d2d153e 100644 --- a/app/internal/window/os_macos.go +++ b/app/internal/window/os_macos.go @@ -58,6 +58,7 @@ type window struct { w Callbacks stage system.Stage displayLink *displayLink + cursor pointer.CursorName scale float32 } @@ -74,7 +75,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 +123,10 @@ func (w *window) WriteClipboard(s string) { }) } +func (w *window) SetCursor(name pointer.CursorName) { + w.cursor = windowSetCursor(w.cursor, name) +} + func (w *window) ShowTextInput(show bool) {} func (w *window) SetAnimating(anim bool) { @@ -227,6 +232,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_wayland.go b/app/internal/window/os_wayland.go index 60f59b15..59d10717 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 2a38d530..ae706f9c 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 413a9596..ae8fb51b 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 #include @@ -18,6 +18,8 @@ package window #include #include #include +#include +#include #include */ @@ -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 30ba3603..f1402140 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 21b18581..22435b49 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 ed469583..136350a7 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/io/pointer/pointer.go b/io/pointer/pointer.go index 2c0010a7..a5b6c2cc 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() {}