Files
gio/app/os_wayland.go
T
2025-04-09 10:09:10 +02:00

1940 lines
53 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
//go:build ((linux && !android) || freebsd) && !nowayland
// +build linux,!android freebsd
// +build !nowayland
package app
import (
"bytes"
"errors"
"fmt"
"image"
"io"
"math"
"os"
"os/exec"
"runtime"
"strconv"
"sync"
"time"
"unsafe"
syscall "golang.org/x/sys/unix"
"gioui.org/app/internal/xkb"
"gioui.org/f32"
"gioui.org/internal/fling"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
)
// Use wayland-scanner to generate glue code for the xdg-shell and xdg-decoration extensions.
//go:generate wayland-scanner client-header /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml wayland_xdg_shell.h
//go:generate wayland-scanner private-code /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml wayland_xdg_shell.c
//go:generate wayland-scanner client-header /usr/share/wayland-protocols/unstable/text-input/text-input-unstable-v3.xml wayland_text_input.h
//go:generate wayland-scanner private-code /usr/share/wayland-protocols/unstable/text-input/text-input-unstable-v3.xml wayland_text_input.c
//go:generate wayland-scanner client-header /usr/share/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml wayland_xdg_decoration.h
//go:generate wayland-scanner private-code /usr/share/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml wayland_xdg_decoration.c
//go:generate sed -i "1s;^;//go:build ((linux \\&\\& !android) || freebsd) \\&\\& !nowayland\\n// +build linux,!android freebsd\\n// +build !nowayland\\n\\n;" wayland_xdg_shell.c
//go:generate sed -i "1s;^;//go:build ((linux \\&\\& !android) || freebsd) \\&\\& !nowayland\\n// +build linux,!android freebsd\\n// +build !nowayland\\n\\n;" wayland_xdg_decoration.c
//go:generate sed -i "1s;^;//go:build ((linux \\&\\& !android) || freebsd) \\&\\& !nowayland\\n// +build linux,!android freebsd\\n// +build !nowayland\\n\\n;" wayland_text_input.c
/*
#cgo linux pkg-config: wayland-client wayland-cursor
#cgo freebsd openbsd LDFLAGS: -lwayland-client -lwayland-cursor
#cgo freebsd CFLAGS: -I/usr/local/include
#cgo freebsd LDFLAGS: -L/usr/local/lib
#include <stdlib.h>
#include <wayland-client.h>
#include <wayland-cursor.h>
#include "wayland_text_input.h"
#include "wayland_xdg_shell.h"
#include "wayland_xdg_decoration.h"
extern const struct wl_registry_listener gio_registry_listener;
extern const struct wl_surface_listener gio_surface_listener;
extern const struct xdg_surface_listener gio_xdg_surface_listener;
extern const struct xdg_toplevel_listener gio_xdg_toplevel_listener;
extern const struct zxdg_toplevel_decoration_v1_listener gio_zxdg_toplevel_decoration_v1_listener;
extern const struct xdg_wm_base_listener gio_xdg_wm_base_listener;
extern const struct wl_callback_listener gio_callback_listener;
extern const struct wl_output_listener gio_output_listener;
extern const struct wl_seat_listener gio_seat_listener;
extern const struct wl_pointer_listener gio_pointer_listener;
extern const struct wl_touch_listener gio_touch_listener;
extern const struct wl_keyboard_listener gio_keyboard_listener;
extern const struct zwp_text_input_v3_listener gio_zwp_text_input_v3_listener;
extern const struct wl_data_device_listener gio_data_device_listener;
extern const struct wl_data_offer_listener gio_data_offer_listener;
extern const struct wl_data_source_listener gio_data_source_listener;
*/
import "C"
type wlDisplay struct {
disp *C.struct_wl_display
reg *C.struct_wl_registry
compositor *C.struct_wl_compositor
wm *C.struct_xdg_wm_base
imm *C.struct_zwp_text_input_manager_v3
shm *C.struct_wl_shm
dataDeviceManager *C.struct_wl_data_device_manager
decor *C.struct_zxdg_decoration_manager_v1
seat *wlSeat
xkb *xkb.Context
outputMap map[C.uint32_t]*C.struct_wl_output
outputConfig map[*C.struct_wl_output]*wlOutput
// Notification pipe fds.
notify struct {
read, write int
}
repeat repeatState
poller poller
readClipClose chan struct{}
}
type wlSeat struct {
disp *wlDisplay
seat *C.struct_wl_seat
name C.uint32_t
pointer *C.struct_wl_pointer
touch *C.struct_wl_touch
keyboard *C.struct_wl_keyboard
im *C.struct_zwp_text_input_v3
// The most recent input serial.
serial C.uint32_t
// The most recent pointer enter serial.
pointerSerial C.uint32_t
pointerFocus *window
keyboardFocus *window
touchFoci map[C.int32_t]*window
// Clipboard support.
dataDev *C.struct_wl_data_device
// offers is a map from active wl_data_offers to
// the list of mime types they support.
offers map[*C.struct_wl_data_offer][]string
// clipboard is the wl_data_offer for the clipboard.
clipboard *C.struct_wl_data_offer
// mimeType is the chosen mime type of clipboard.
mimeType string
// source represents the clipboard content of the most recent
// clipboard write, if any.
source *C.struct_wl_data_source
// content is the data belonging to source.
content []byte
}
type repeatState struct {
rate int
delay time.Duration
key uint32
win *window
stopC chan struct{}
start time.Duration
last time.Duration
mu sync.Mutex
now time.Duration
}
type window struct {
w *callbacks
disp *wlDisplay
surf *C.struct_wl_surface
wmSurf *C.struct_xdg_surface
topLvl *C.struct_xdg_toplevel
decor *C.struct_zxdg_toplevel_decoration_v1
ppdp, ppsp float32
scroll struct {
time time.Duration
steps image.Point
dist f32.Point
}
pointerBtns pointer.Buttons
lastPos f32.Point
lastTouch f32.Point
cursor struct {
theme *C.struct_wl_cursor_theme
cursor *C.struct_wl_cursor
// system is the active cursor for system gestures
// such as border resizes and window moves. It
// is nil if the pointer is not in a system gesture
// area.
system *C.struct_wl_cursor
surf *C.struct_wl_surface
cursors struct {
pointer *C.struct_wl_cursor
resizeNorth *C.struct_wl_cursor
resizeSouth *C.struct_wl_cursor
resizeWest *C.struct_wl_cursor
resizeEast *C.struct_wl_cursor
resizeNorthWest *C.struct_wl_cursor
resizeNorthEast *C.struct_wl_cursor
resizeSouthWest *C.struct_wl_cursor
resizeSouthEast *C.struct_wl_cursor
}
}
fling struct {
yExtrapolation fling.Extrapolation
xExtrapolation fling.Extrapolation
anim fling.Animation
start bool
dir f32.Point
}
configured bool
lastFrameCallback *C.struct_wl_callback
animating bool
// The most recent configure serial waiting to be ack'ed.
serial C.uint32_t
scale int
// size is the unscaled window size (unlike config.Size which is scaled).
size image.Point
config Config
wsize image.Point // window config size before going fullscreen or maximized
inCompositor bool // window is moving or being resized
clipReads chan transfer.DataEvent
wakeups chan struct{}
closing bool
}
type poller struct {
pollfds [2]syscall.PollFd
// buf is scratch space for draining the notification pipe.
buf [100]byte
}
type wlOutput struct {
width int
height int
physWidth int
physHeight int
transform C.int32_t
scale int
windows []*window
}
// callbackMap maps Wayland native handles to corresponding Go
// references. It is necessary because the Wayland client API
// forces the use of callbacks and storing pointers to Go values
// in C is forbidden.
var callbackMap sync.Map
// clipboardMimeTypes is a list of supported clipboard mime types, in
// order of preference.
var clipboardMimeTypes = []string{"text/plain;charset=utf8", "UTF8_STRING", "text/plain", "TEXT", "STRING"}
var (
newWaylandEGLContext func(w *window) (context, error)
newWaylandVulkanContext func(w *window) (context, error)
)
func init() {
wlDriver = newWLWindow
}
func newWLWindow(callbacks *callbacks, options []Option) error {
d, err := newWLDisplay()
if err != nil {
return err
}
w, err := d.createNativeWindow(options)
if err != nil {
d.destroy()
return err
}
w.w = callbacks
w.w.SetDriver(w)
// Finish and commit setup from createNativeWindow.
w.Configure(options)
w.draw(true)
C.wl_surface_commit(w.surf)
w.ProcessEvent(WaylandViewEvent{
Display: unsafe.Pointer(w.display()),
Surface: unsafe.Pointer(w.surf),
})
return nil
}
func (d *wlDisplay) writeClipboard(content []byte) error {
s := d.seat
if s == nil {
return nil
}
// Clear old offer.
if s.source != nil {
C.wl_data_source_destroy(s.source)
s.source = nil
s.content = nil
}
if d.dataDeviceManager == nil || s.dataDev == nil {
return nil
}
s.content = content
s.source = C.wl_data_device_manager_create_data_source(d.dataDeviceManager)
C.wl_data_source_add_listener(s.source, &C.gio_data_source_listener, unsafe.Pointer(s.seat))
for _, mime := range clipboardMimeTypes {
C.wl_data_source_offer(s.source, C.CString(mime))
}
C.wl_data_device_set_selection(s.dataDev, s.source, s.serial)
return nil
}
func (d *wlDisplay) readClipboard() (io.ReadCloser, error) {
s := d.seat
if s == nil {
return nil, nil
}
if s.clipboard == nil {
return nil, nil
}
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
// wl_data_offer_receive performs and implicit dup(2) of the write end
// of the pipe. Close our version.
defer w.Close()
cmimeType := C.CString(s.mimeType)
defer C.free(unsafe.Pointer(cmimeType))
C.wl_data_offer_receive(s.clipboard, cmimeType, C.int(w.Fd()))
return r, nil
}
func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) {
if d.compositor == nil {
return nil, errors.New("wayland: no compositor available")
}
if d.wm == nil {
return nil, errors.New("wayland: no xdg_wm_base available")
}
if d.shm == nil {
return nil, errors.New("wayland: no wl_shm available")
}
if len(d.outputMap) == 0 {
return nil, errors.New("wayland: no outputs available")
}
var scale int
for _, conf := range d.outputConfig {
if s := conf.scale; s > scale {
scale = s
}
}
ppdp := detectUIScale()
w := &window{
disp: d,
scale: scale,
ppdp: ppdp,
ppsp: ppdp,
wakeups: make(chan struct{}, 1),
clipReads: make(chan transfer.DataEvent, 1),
}
w.surf = C.wl_compositor_create_surface(d.compositor)
if w.surf == nil {
w.destroy()
return nil, errors.New("wayland: wl_compositor_create_surface failed")
}
C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale))
callbackStore(unsafe.Pointer(w.surf), w)
w.wmSurf = C.xdg_wm_base_get_xdg_surface(d.wm, w.surf)
if w.wmSurf == nil {
w.destroy()
return nil, errors.New("wayland: xdg_wm_base_get_xdg_surface failed")
}
w.topLvl = C.xdg_surface_get_toplevel(w.wmSurf)
if w.topLvl == nil {
w.destroy()
return nil, errors.New("wayland: xdg_surface_get_toplevel failed")
}
id := C.CString(ID)
defer C.free(unsafe.Pointer(id))
C.xdg_toplevel_set_app_id(w.topLvl, id)
cursorTheme := C.CString(os.Getenv("XCURSOR_THEME"))
defer C.free(unsafe.Pointer(cursorTheme))
cursorSize := 32
if envSize, ok := os.LookupEnv("XCURSOR_SIZE"); ok && envSize != "" {
size, err := strconv.Atoi(envSize)
if err == nil {
cursorSize = size
}
}
w.cursor.theme = C.wl_cursor_theme_load(cursorTheme, C.int(cursorSize*w.scale), d.shm)
if w.cursor.theme == nil {
w.destroy()
return nil, errors.New("wayland: wl_cursor_theme_load failed")
}
w.loadCursors()
w.cursor.cursor = w.cursor.cursors.pointer
if w.cursor.cursor == nil {
w.destroy()
return nil, errors.New("wayland: wl_cursor_theme_get_cursor failed")
}
w.cursor.surf = C.wl_compositor_create_surface(d.compositor)
if w.cursor.surf == nil {
w.destroy()
return nil, errors.New("wayland: wl_compositor_create_surface failed")
}
C.wl_surface_set_buffer_scale(w.cursor.surf, C.int32_t(w.scale))
C.xdg_wm_base_add_listener(d.wm, &C.gio_xdg_wm_base_listener, unsafe.Pointer(w.surf))
C.wl_surface_add_listener(w.surf, &C.gio_surface_listener, unsafe.Pointer(w.surf))
C.xdg_surface_add_listener(w.wmSurf, &C.gio_xdg_surface_listener, unsafe.Pointer(w.surf))
C.xdg_toplevel_add_listener(w.topLvl, &C.gio_xdg_toplevel_listener, unsafe.Pointer(w.surf))
if d.decor != nil {
w.decor = C.zxdg_decoration_manager_v1_get_toplevel_decoration(d.decor, w.topLvl)
C.zxdg_toplevel_decoration_v1_add_listener(w.decor, &C.gio_zxdg_toplevel_decoration_v1_listener, unsafe.Pointer(w.surf))
}
w.updateOpaqueRegion()
return w, nil
}
func (w *window) loadCursors() {
w.cursor.cursors.pointer = w.loadCursor(pointer.CursorDefault)
w.cursor.cursors.resizeNorth = w.loadCursor(pointer.CursorNorthResize)
w.cursor.cursors.resizeSouth = w.loadCursor(pointer.CursorSouthResize)
w.cursor.cursors.resizeWest = w.loadCursor(pointer.CursorWestResize)
w.cursor.cursors.resizeEast = w.loadCursor(pointer.CursorEastResize)
w.cursor.cursors.resizeSouthWest = w.loadCursor(pointer.CursorSouthWestResize)
w.cursor.cursors.resizeSouthEast = w.loadCursor(pointer.CursorSouthEastResize)
w.cursor.cursors.resizeNorthWest = w.loadCursor(pointer.CursorNorthWestResize)
w.cursor.cursors.resizeNorthEast = w.loadCursor(pointer.CursorNorthEastResize)
}
func (w *window) loadCursor(name pointer.Cursor) *C.struct_wl_cursor {
if name == pointer.CursorNone {
return nil
}
xcursor := xCursor[name]
cname := C.CString(xcursor)
defer C.free(unsafe.Pointer(cname))
c := C.wl_cursor_theme_get_cursor(w.cursor.theme, cname)
if c == nil {
// Fall back to default cursor.
c = w.cursor.cursors.pointer
}
return c
}
func callbackDelete(k unsafe.Pointer) {
callbackMap.Delete(k)
}
func callbackStore(k unsafe.Pointer, v interface{}) {
callbackMap.Store(k, v)
}
func callbackLoad(k unsafe.Pointer) interface{} {
v, exists := callbackMap.Load(k)
if !exists {
panic("missing callback entry")
}
return v
}
//export gio_onSeatCapabilities
func gio_onSeatCapabilities(data unsafe.Pointer, seat *C.struct_wl_seat, caps C.uint32_t) {
s := callbackLoad(data).(*wlSeat)
s.updateCaps(caps)
}
// flushOffers remove all wl_data_offers that isn't the clipboard
// content.
func (s *wlSeat) flushOffers() {
for o := range s.offers {
if o == s.clipboard {
continue
}
// We're only interested in clipboard offers.
delete(s.offers, o)
callbackDelete(unsafe.Pointer(o))
C.wl_data_offer_destroy(o)
}
}
func (s *wlSeat) destroy() {
if s.source != nil {
C.wl_data_source_destroy(s.source)
s.source = nil
}
if s.im != nil {
C.zwp_text_input_v3_destroy(s.im)
s.im = nil
}
if s.pointer != nil {
C.wl_pointer_release(s.pointer)
}
if s.touch != nil {
C.wl_touch_release(s.touch)
}
if s.keyboard != nil {
C.wl_keyboard_release(s.keyboard)
}
s.clipboard = nil
s.flushOffers()
if s.dataDev != nil {
C.wl_data_device_release(s.dataDev)
}
if s.seat != nil {
callbackDelete(unsafe.Pointer(s.seat))
C.wl_seat_release(s.seat)
}
}
func (s *wlSeat) updateCaps(caps C.uint32_t) {
if s.im == nil && s.disp.imm != nil {
s.im = C.zwp_text_input_manager_v3_get_text_input(s.disp.imm, s.seat)
C.zwp_text_input_v3_add_listener(s.im, &C.gio_zwp_text_input_v3_listener, unsafe.Pointer(s.seat))
}
switch {
case s.pointer == nil && caps&C.WL_SEAT_CAPABILITY_POINTER != 0:
s.pointer = C.wl_seat_get_pointer(s.seat)
C.wl_pointer_add_listener(s.pointer, &C.gio_pointer_listener, unsafe.Pointer(s.seat))
case s.pointer != nil && caps&C.WL_SEAT_CAPABILITY_POINTER == 0:
C.wl_pointer_release(s.pointer)
s.pointer = nil
}
switch {
case s.touch == nil && caps&C.WL_SEAT_CAPABILITY_TOUCH != 0:
s.touch = C.wl_seat_get_touch(s.seat)
C.wl_touch_add_listener(s.touch, &C.gio_touch_listener, unsafe.Pointer(s.seat))
case s.touch != nil && caps&C.WL_SEAT_CAPABILITY_TOUCH == 0:
C.wl_touch_release(s.touch)
s.touch = nil
}
switch {
case s.keyboard == nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD != 0:
s.keyboard = C.wl_seat_get_keyboard(s.seat)
C.wl_keyboard_add_listener(s.keyboard, &C.gio_keyboard_listener, unsafe.Pointer(s.seat))
case s.keyboard != nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD == 0:
C.wl_keyboard_release(s.keyboard)
s.keyboard = nil
}
}
//export gio_onSeatName
func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) {
}
//export gio_onXdgSurfaceConfigure
func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) {
w := callbackLoad(data).(*window)
w.serial = serial
C.xdg_surface_ack_configure(wmSurf, serial)
w.configured = true
w.draw(true)
}
//export gio_onToplevelClose
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
w := callbackLoad(data).(*window)
w.closing = true
}
//export gio_onToplevelConfigure
func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel, width, height C.int32_t, states *C.struct_wl_array) {
w := callbackLoad(data).(*window)
if width != 0 && height != 0 {
w.size = image.Pt(int(width), int(height))
w.updateOpaqueRegion()
}
}
//export gio_onToplevelDecorationConfigure
func gio_onToplevelDecorationConfigure(data unsafe.Pointer, deco *C.struct_zxdg_toplevel_decoration_v1, mode C.uint32_t) {
w := callbackLoad(data).(*window)
decorated := w.config.Decorated
switch mode {
case C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE:
w.config.Decorated = false
case C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE:
w.config.Decorated = true
}
if decorated != w.config.Decorated {
w.setWindowConstraints()
if w.config.Decorated {
w.size.Y -= int(w.config.decoHeight)
} else {
w.size.Y += int(w.config.decoHeight)
}
w.ProcessEvent(ConfigEvent{Config: w.config})
w.draw(true)
}
}
//export gio_onOutputMode
func gio_onOutputMode(data unsafe.Pointer, output *C.struct_wl_output, flags C.uint32_t, width, height, refresh C.int32_t) {
if flags&C.WL_OUTPUT_MODE_CURRENT == 0 {
return
}
d := callbackLoad(data).(*wlDisplay)
c := d.outputConfig[output]
c.width = int(width)
c.height = int(height)
}
//export gio_onOutputGeometry
func gio_onOutputGeometry(data unsafe.Pointer, output *C.struct_wl_output, x, y, physWidth, physHeight, subpixel C.int32_t, make, model *C.char, transform C.int32_t) {
d := callbackLoad(data).(*wlDisplay)
c := d.outputConfig[output]
c.transform = transform
c.physWidth = int(physWidth)
c.physHeight = int(physHeight)
}
//export gio_onOutputScale
func gio_onOutputScale(data unsafe.Pointer, output *C.struct_wl_output, scale C.int32_t) {
d := callbackLoad(data).(*wlDisplay)
c := d.outputConfig[output]
c.scale = int(scale)
}
//export gio_onOutputDone
func gio_onOutputDone(data unsafe.Pointer, output *C.struct_wl_output) {
d := callbackLoad(data).(*wlDisplay)
conf := d.outputConfig[output]
for _, w := range conf.windows {
w.updateOutputs()
}
}
//export gio_onSurfaceEnter
func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output *C.struct_wl_output) {
w := callbackLoad(data).(*window)
conf := w.disp.outputConfig[output]
var found bool
for _, w2 := range conf.windows {
if w2 == w {
found = true
break
}
}
if !found {
conf.windows = append(conf.windows, w)
}
w.updateOutputs()
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})
}
}
//export gio_onSurfaceLeave
func gio_onSurfaceLeave(data unsafe.Pointer, surf *C.struct_wl_surface, output *C.struct_wl_output) {
w := callbackLoad(data).(*window)
conf := w.disp.outputConfig[output]
for i, w2 := range conf.windows {
if w2 == w {
conf.windows = append(conf.windows[:i], conf.windows[i+1:]...)
break
}
}
w.updateOutputs()
}
//export gio_onRegistryGlobal
func gio_onRegistryGlobal(data unsafe.Pointer, reg *C.struct_wl_registry, name C.uint32_t, cintf *C.char, version C.uint32_t) {
d := callbackLoad(data).(*wlDisplay)
switch C.GoString(cintf) {
case "wl_compositor":
d.compositor = (*C.struct_wl_compositor)(C.wl_registry_bind(reg, name, &C.wl_compositor_interface, 3))
case "wl_output":
output := (*C.struct_wl_output)(C.wl_registry_bind(reg, name, &C.wl_output_interface, 2))
C.wl_output_add_listener(output, &C.gio_output_listener, unsafe.Pointer(d.disp))
d.outputMap[name] = output
d.outputConfig[output] = new(wlOutput)
case "wl_seat":
if d.seat != nil {
break
}
s := (*C.struct_wl_seat)(C.wl_registry_bind(reg, name, &C.wl_seat_interface, 5))
if s == nil {
// No support for v5 protocol.
break
}
d.seat = &wlSeat{
disp: d,
name: name,
seat: s,
offers: make(map[*C.struct_wl_data_offer][]string),
touchFoci: make(map[C.int32_t]*window),
}
callbackStore(unsafe.Pointer(s), d.seat)
C.wl_seat_add_listener(s, &C.gio_seat_listener, unsafe.Pointer(s))
d.bindDataDevice()
case "wl_shm":
d.shm = (*C.struct_wl_shm)(C.wl_registry_bind(reg, name, &C.wl_shm_interface, 1))
case "xdg_wm_base":
d.wm = (*C.struct_xdg_wm_base)(C.wl_registry_bind(reg, name, &C.xdg_wm_base_interface, 1))
case "zxdg_decoration_manager_v1":
d.decor = (*C.struct_zxdg_decoration_manager_v1)(C.wl_registry_bind(reg, name, &C.zxdg_decoration_manager_v1_interface, 1))
// TODO: Implement and test text-input support.
/*case "zwp_text_input_manager_v3":
d.imm = (*C.struct_zwp_text_input_manager_v3)(C.wl_registry_bind(reg, name, &C.zwp_text_input_manager_v3_interface, 1))*/
case "wl_data_device_manager":
d.dataDeviceManager = (*C.struct_wl_data_device_manager)(C.wl_registry_bind(reg, name, &C.wl_data_device_manager_interface, 3))
d.bindDataDevice()
}
}
//export gio_onDataOfferOffer
func gio_onDataOfferOffer(data unsafe.Pointer, offer *C.struct_wl_data_offer, mime *C.char) {
s := callbackLoad(data).(*wlSeat)
s.offers[offer] = append(s.offers[offer], C.GoString(mime))
}
//export gio_onDataOfferSourceActions
func gio_onDataOfferSourceActions(data unsafe.Pointer, offer *C.struct_wl_data_offer, acts C.uint32_t) {
}
//export gio_onDataOfferAction
func gio_onDataOfferAction(data unsafe.Pointer, offer *C.struct_wl_data_offer, act C.uint32_t) {
}
//export gio_onDataDeviceOffer
func gio_onDataDeviceOffer(data unsafe.Pointer, dataDev *C.struct_wl_data_device, id *C.struct_wl_data_offer) {
s := callbackLoad(data).(*wlSeat)
callbackStore(unsafe.Pointer(id), s)
C.wl_data_offer_add_listener(id, &C.gio_data_offer_listener, unsafe.Pointer(id))
s.offers[id] = nil
}
//export gio_onDataDeviceEnter
func gio_onDataDeviceEnter(data unsafe.Pointer, dataDev *C.struct_wl_data_device, serial C.uint32_t, surf *C.struct_wl_surface, x, y C.wl_fixed_t, id *C.struct_wl_data_offer) {
s := callbackLoad(data).(*wlSeat)
s.serial = serial
s.flushOffers()
}
//export gio_onDataDeviceLeave
func gio_onDataDeviceLeave(data unsafe.Pointer, dataDev *C.struct_wl_data_device) {
}
//export gio_onDataDeviceMotion
func gio_onDataDeviceMotion(data unsafe.Pointer, dataDev *C.struct_wl_data_device, t C.uint32_t, x, y C.wl_fixed_t) {
}
//export gio_onDataDeviceDrop
func gio_onDataDeviceDrop(data unsafe.Pointer, dataDev *C.struct_wl_data_device) {
}
//export gio_onDataDeviceSelection
func gio_onDataDeviceSelection(data unsafe.Pointer, dataDev *C.struct_wl_data_device, id *C.struct_wl_data_offer) {
s := callbackLoad(data).(*wlSeat)
defer s.flushOffers()
s.clipboard = nil
loop:
for _, want := range clipboardMimeTypes {
for _, got := range s.offers[id] {
if want != got {
continue
}
s.clipboard = id
s.mimeType = got
break loop
}
}
}
//export gio_onRegistryGlobalRemove
func gio_onRegistryGlobalRemove(data unsafe.Pointer, reg *C.struct_wl_registry, name C.uint32_t) {
d := callbackLoad(data).(*wlDisplay)
if s := d.seat; s != nil && name == s.name {
s.destroy()
d.seat = nil
}
if output, exists := d.outputMap[name]; exists {
C.wl_output_destroy(output)
delete(d.outputMap, name)
delete(d.outputConfig, output)
}
}
//export gio_onTouchDown
func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.uint32_t, surf *C.struct_wl_surface, id C.int32_t, x, y C.wl_fixed_t) {
s := callbackLoad(data).(*wlSeat)
s.serial = serial
w := callbackLoad(unsafe.Pointer(surf)).(*window)
s.touchFoci[id] = w
w.lastTouch = f32.Point{
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.ProcessEvent(pointer.Event{
Kind: pointer.Press,
Source: pointer.Touch,
Position: w.lastTouch,
PointerID: pointer.ID(id),
Time: time.Duration(t) * time.Millisecond,
Modifiers: w.disp.xkb.Modifiers(),
})
}
//export gio_onTouchUp
func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.uint32_t, id C.int32_t) {
s := callbackLoad(data).(*wlSeat)
s.serial = serial
w := s.touchFoci[id]
delete(s.touchFoci, id)
w.ProcessEvent(pointer.Event{
Kind: pointer.Release,
Source: pointer.Touch,
Position: w.lastTouch,
PointerID: pointer.ID(id),
Time: time.Duration(t) * time.Millisecond,
Modifiers: w.disp.xkb.Modifiers(),
})
}
//export gio_onTouchMotion
func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32_t, id C.int32_t, x, y C.wl_fixed_t) {
s := callbackLoad(data).(*wlSeat)
w := s.touchFoci[id]
w.lastTouch = f32.Point{
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Position: w.lastTouch,
Source: pointer.Touch,
PointerID: pointer.ID(id),
Time: time.Duration(t) * time.Millisecond,
Modifiers: w.disp.xkb.Modifiers(),
})
}
//export gio_onTouchFrame
func gio_onTouchFrame(data unsafe.Pointer, touch *C.struct_wl_touch) {
}
//export gio_onTouchCancel
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{
Kind: pointer.Cancel,
Source: pointer.Touch,
})
}
}
//export gio_onPointerEnter
func gio_onPointerEnter(data unsafe.Pointer, pointer *C.struct_wl_pointer, serial C.uint32_t, surf *C.struct_wl_surface, x, y C.wl_fixed_t) {
s := callbackLoad(data).(*wlSeat)
s.serial = serial
s.pointerSerial = serial
w := callbackLoad(unsafe.Pointer(surf)).(*window)
s.pointerFocus = w
w.setCursor(pointer, serial)
w.lastPos = f32.Point{X: fromFixed(x), Y: fromFixed(y)}
}
//export gio_onPointerLeave
func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.uint32_t, surf *C.struct_wl_surface) {
w := callbackLoad(unsafe.Pointer(surf)).(*window)
s := callbackLoad(data).(*wlSeat)
s.serial = serial
s.pointerFocus = nil
if w.inCompositor {
w.inCompositor = false
w.ProcessEvent(pointer.Event{Kind: pointer.Cancel})
}
}
//export gio_onPointerMotion
func gio_onPointerMotion(data unsafe.Pointer, p *C.struct_wl_pointer, t C.uint32_t, x, y C.wl_fixed_t) {
s := callbackLoad(data).(*wlSeat)
w := s.pointerFocus
w.resetFling()
w.onPointerMotion(x, y, t)
}
//export gio_onPointerButton
func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t, wbtn, state C.uint32_t) {
s := callbackLoad(data).(*wlSeat)
s.serial = serial
w := s.pointerFocus
// From linux-event-codes.h.
const (
BTN_LEFT = 0x110
BTN_RIGHT = 0x111
BTN_MIDDLE = 0x112
)
var btn pointer.Buttons
switch wbtn {
case BTN_LEFT:
btn = pointer.ButtonPrimary
case BTN_RIGHT:
btn = pointer.ButtonSecondary
case BTN_MIDDLE:
btn = pointer.ButtonTertiary
default:
return
}
if state == 1 && btn == pointer.ButtonPrimary {
if _, edge := w.systemGesture(); edge != 0 {
w.resize(serial, edge)
return
}
act, ok := w.w.ActionAt(w.lastPos)
if ok && w.config.Mode == Windowed {
switch act {
case system.ActionMove:
w.move(serial)
return
}
}
}
var kind pointer.Kind
switch state {
case 0:
w.pointerBtns &^= btn
kind = pointer.Release
// Move or resize gestures no longer applies.
w.inCompositor = false
case 1:
w.pointerBtns |= btn
kind = pointer.Press
}
w.flushScroll()
w.resetFling()
w.ProcessEvent(pointer.Event{
Kind: kind,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
Position: w.lastPos,
Time: time.Duration(t) * time.Millisecond,
Modifiers: w.disp.xkb.Modifiers(),
})
}
//export gio_onPointerAxis
func gio_onPointerAxis(data unsafe.Pointer, p *C.struct_wl_pointer, t, axis C.uint32_t, value C.wl_fixed_t) {
s := callbackLoad(data).(*wlSeat)
w := s.pointerFocus
v := fromFixed(value)
w.resetFling()
if w.scroll.dist == (f32.Point{}) {
w.scroll.time = time.Duration(t) * time.Millisecond
}
switch axis {
case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL:
w.scroll.dist.X += v
case C.WL_POINTER_AXIS_VERTICAL_SCROLL:
// horizontal scroll if shift + mousewheel(up/down) pressed.
if w.disp.xkb.Modifiers() == key.ModShift {
w.scroll.dist.X += v
} else {
w.scroll.dist.Y += v
}
}
}
//export gio_onPointerFrame
func gio_onPointerFrame(data unsafe.Pointer, p *C.struct_wl_pointer) {
s := callbackLoad(data).(*wlSeat)
w := s.pointerFocus
if w == nil {
return
}
w.flushScroll()
w.flushFling()
}
func (w *window) flushFling() {
if !w.fling.start {
return
}
w.fling.start = false
estx, esty := w.fling.xExtrapolation.Estimate(), w.fling.yExtrapolation.Estimate()
w.fling.xExtrapolation = fling.Extrapolation{}
w.fling.yExtrapolation = fling.Extrapolation{}
vel := float32(math.Sqrt(float64(estx.Velocity*estx.Velocity + esty.Velocity*esty.Velocity)))
_, c := w.getConfig()
if !w.fling.anim.Start(c, time.Now(), vel) {
return
}
invDist := 1 / vel
w.fling.dir.X = estx.Velocity * invDist
w.fling.dir.Y = esty.Velocity * invDist
}
//export gio_onPointerAxisSource
func gio_onPointerAxisSource(data unsafe.Pointer, pointer *C.struct_wl_pointer, source C.uint32_t) {
}
//export gio_onPointerAxisStop
func gio_onPointerAxisStop(data unsafe.Pointer, p *C.struct_wl_pointer, t, axis C.uint32_t) {
s := callbackLoad(data).(*wlSeat)
w := s.pointerFocus
w.fling.start = true
}
//export gio_onPointerAxisDiscrete
func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, axis C.uint32_t, discrete C.int32_t) {
s := callbackLoad(data).(*wlSeat)
w := s.pointerFocus
w.resetFling()
switch axis {
case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL:
w.scroll.steps.X += int(discrete)
case C.WL_POINTER_AXIS_VERTICAL_SCROLL:
// horizontal scroll if shift + mousewheel(up/down) pressed.
if w.disp.xkb.Modifiers() == key.ModShift {
w.scroll.steps.X += int(discrete)
} else {
w.scroll.steps.Y += int(discrete)
}
}
}
func (w *window) ReadClipboard() {
if w.disp.readClipClose != nil {
return
}
w.disp.readClipClose = make(chan struct{})
r, err := w.disp.readClipboard()
if r == nil || err != nil {
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:
}
}()
}
func (w *window) WriteClipboard(mime string, s []byte) {
w.disp.writeClipboard(s)
}
func (w *window) Configure(options []Option) {
_, cfg := w.getConfig()
prev := w.config
cnf := w.config
cnf.apply(cfg, options)
w.config.decoHeight = cnf.decoHeight
switch cnf.Mode {
case Fullscreen:
switch prev.Mode {
case Minimized, Fullscreen:
default:
w.config.Mode = Fullscreen
w.wsize = w.config.Size
C.xdg_toplevel_set_fullscreen(w.topLvl, nil)
}
case Minimized:
switch prev.Mode {
case Minimized, Fullscreen:
default:
w.config.Mode = Minimized
C.xdg_toplevel_set_minimized(w.topLvl)
}
case Maximized:
switch prev.Mode {
case Minimized, Fullscreen:
default:
w.config.Mode = Maximized
w.wsize = w.config.Size
C.xdg_toplevel_set_maximized(w.topLvl)
w.setTitle(prev, cnf)
}
case Windowed:
switch prev.Mode {
case Fullscreen:
w.config.Mode = Windowed
w.size = w.wsize.Div(w.scale)
C.xdg_toplevel_unset_fullscreen(w.topLvl)
case Minimized:
w.config.Mode = Windowed
case Maximized:
w.config.Mode = Windowed
w.size = w.wsize.Div(w.scale)
C.xdg_toplevel_unset_maximized(w.topLvl)
}
w.setTitle(prev, cnf)
if prev.Size != cnf.Size {
w.config.Size = cnf.Size
w.config.Size.Y += int(w.decoHeight()) * w.scale
w.size = w.config.Size.Div(w.scale)
}
w.config.MinSize = cnf.MinSize
w.config.MaxSize = cnf.MaxSize
w.setWindowConstraints()
}
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func (w *window) setWindowConstraints() {
decoHeight := w.decoHeight()
if scaled := w.config.MinSize.Div(w.scale); scaled != (image.Point{}) {
C.xdg_toplevel_set_min_size(w.topLvl, C.int32_t(scaled.X), C.int32_t(scaled.Y+decoHeight))
}
if scaled := w.config.MaxSize.Div(w.scale); scaled != (image.Point{}) {
C.xdg_toplevel_set_max_size(w.topLvl, C.int32_t(scaled.X), C.int32_t(scaled.Y+decoHeight))
}
}
// decoHeight returns the adjustment for client-side decorations, if applicable.
// The unit is in surface-local coordinates.
func (w *window) decoHeight() int {
if !w.config.Decorated {
return int(w.config.decoHeight)
}
return 0
}
func (w *window) setTitle(prev, cnf Config) {
if prev.Title != cnf.Title {
w.config.Title = cnf.Title
title := C.CString(cnf.Title)
C.xdg_toplevel_set_title(w.topLvl, title)
C.free(unsafe.Pointer(title))
}
}
func (w *window) Perform(actions system.Action) {
// NB. there is no way for a minimized window to be unminimized.
// https://wayland.app/protocols/xdg-shell#xdg_toplevel:request:set_minimized
walkActions(actions, func(action system.Action) {
switch action {
case system.ActionClose:
w.closing = true
}
})
}
func (w *window) move(serial C.uint32_t) {
s := w.disp.seat
if w.inCompositor || s.pointerFocus != w {
return
}
w.inCompositor = true
C.xdg_toplevel_move(w.topLvl, s.seat, serial)
}
func (w *window) resize(serial, edge C.uint32_t) {
s := w.disp.seat
if w.inCompositor || s.pointerFocus != w {
return
}
w.inCompositor = true
C.xdg_toplevel_resize(w.topLvl, s.seat, serial, edge)
}
func (w *window) SetCursor(cursor pointer.Cursor) {
w.cursor.cursor = w.loadCursor(cursor)
w.updateCursor()
}
func (w *window) updateCursor() {
s := w.disp.seat
ptr := s.pointer
if ptr == nil || s.pointerFocus != w {
return
}
w.setCursor(ptr, s.pointerSerial)
}
func (w *window) setCursor(pointer *C.struct_wl_pointer, serial C.uint32_t) {
c := w.cursor.system
if c == nil {
c = w.cursor.cursor
}
if c == nil {
C.wl_pointer_set_cursor(pointer, serial, nil, 0, 0)
return
}
// Get images[0].
img := *c.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.uint(w.scale)), C.int32_t(img.hotspot_y/C.uint(w.scale)))
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{}
}
//export gio_onKeyboardKeymap
func gio_onKeyboardKeymap(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, format C.uint32_t, fd C.int32_t, size C.uint32_t) {
defer syscall.Close(int(fd))
s := callbackLoad(data).(*wlSeat)
s.disp.repeat.Stop(0)
s.disp.xkb.DestroyKeymapState()
if format != C.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 {
return
}
if err := s.disp.xkb.LoadKeymap(int(format), int(fd), int(size)); err != nil {
// TODO: Do better.
panic(err)
}
}
//export gio_onKeyboardEnter
func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial C.uint32_t, surf *C.struct_wl_surface, keys *C.struct_wl_array) {
s := callbackLoad(data).(*wlSeat)
s.serial = serial
w := callbackLoad(unsafe.Pointer(surf)).(*window)
s.keyboardFocus = w
s.disp.repeat.Stop(0)
w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onKeyboardLeave
func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial C.uint32_t, surf *C.struct_wl_surface) {
s := callbackLoad(data).(*wlSeat)
s.serial = serial
s.disp.repeat.Stop(0)
w := s.keyboardFocus
w.config.Focused = false
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onKeyboardKey
func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial, timestamp, keyCode, state C.uint32_t) {
s := callbackLoad(data).(*wlSeat)
s.serial = serial
w := s.keyboardFocus
t := time.Duration(timestamp) * time.Millisecond
s.disp.repeat.Stop(t)
w.resetFling()
kc := mapXKBKeycode(uint32(keyCode))
ks := mapXKBKeyState(uint32(state))
for _, e := range w.disp.xkb.DispatchKey(kc, ks) {
if ee, ok := e.(key.EditEvent); ok {
// There's no support for IME yet.
w.w.EditorInsert(ee.Text)
} else {
w.ProcessEvent(e)
}
}
if state != C.WL_KEYBOARD_KEY_STATE_PRESSED {
return
}
if w.disp.xkb.IsRepeatKey(kc) {
w.disp.repeat.Start(w, kc, t)
}
}
func mapXKBKeycode(keyCode uint32) uint32 {
// According to the xkb_v1 spec: "to determine the xkb keycode, clients must add 8 to the key event keycode."
return keyCode + 8
}
func mapXKBKeyState(state uint32) key.State {
switch state {
case C.WL_KEYBOARD_KEY_STATE_RELEASED:
return key.Release
default:
return key.Press
}
}
func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) {
if r.rate <= 0 {
return
}
stopC := make(chan struct{})
r.start = t
r.last = 0
r.now = 0
r.stopC = stopC
r.key = keyCode
r.win = w
rate, delay := r.rate, r.delay
go func() {
timer := time.NewTimer(delay)
for {
select {
case <-timer.C:
case <-stopC:
close(stopC)
return
}
r.Advance(delay)
w.disp.wakeup()
delay = time.Second / time.Duration(rate)
timer.Reset(delay)
}
}()
}
func (r *repeatState) Stop(t time.Duration) {
if r.stopC == nil {
return
}
r.stopC <- struct{}{}
<-r.stopC
r.stopC = nil
t -= r.start
if r.now > t {
r.now = t
}
}
func (r *repeatState) Advance(dt time.Duration) {
r.mu.Lock()
defer r.mu.Unlock()
r.now += dt
}
func (r *repeatState) Repeat(d *wlDisplay) {
if r.rate <= 0 {
return
}
r.mu.Lock()
now := r.now
r.mu.Unlock()
for {
var delay time.Duration
if r.last < r.delay {
delay = r.delay
} else {
delay = time.Second / time.Duration(r.rate)
}
if r.last+delay > now {
break
}
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)
} else {
r.win.ProcessEvent(e)
}
}
r.last += delay
}
}
//export gio_onFrameDone
func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.uint32_t) {
C.wl_callback_destroy(callback)
w := callbackLoad(data).(*window)
if w.lastFrameCallback == callback {
w.lastFrameCallback = nil
w.draw(false)
}
}
func (w *window) close(err error) {
w.ProcessEvent(WaylandViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err})
w.destroy()
w.disp.destroy()
w.disp = nil
}
func (w *window) dispatch() {
if w.disp == nil {
<-w.wakeups
w.w.Invalidate()
return
}
if err := w.disp.dispatch(); err != nil || w.closing {
w.close(err)
return
}
select {
case e := <-w.clipReads:
w.disp.readClipClose = nil
w.ProcessEvent(e)
case <-w.wakeups:
w.w.Invalidate()
default:
}
}
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
}
func (w *window) Event() event.Event {
for {
evt, ok := w.w.nextEvent()
if !ok {
w.dispatch()
continue
}
return evt
}
}
func (w *window) Invalidate() {
select {
case w.wakeups <- struct{}{}:
default:
return
}
w.disp.wakeup()
}
func (w *window) Run(f func()) {
f()
}
func (w *window) Frame(frame *op.Ops) {
w.w.ProcessFrame(frame, nil)
}
// bindDataDevice initializes the dataDev field if and only if both
// the seat and dataDeviceManager fields are initialized.
func (d *wlDisplay) bindDataDevice() {
if d.seat != nil && d.dataDeviceManager != nil {
d.seat.dataDev = C.wl_data_device_manager_get_data_device(d.dataDeviceManager, d.seat.seat)
if d.seat.dataDev == nil {
return
}
callbackStore(unsafe.Pointer(d.seat.dataDev), d.seat)
C.wl_data_device_add_listener(d.seat.dataDev, &C.gio_data_device_listener, unsafe.Pointer(d.seat.dataDev))
}
}
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()
dispfd := C.wl_display_get_fd(d.disp)
// Poll for events and notifications.
pollfds := append(d.poller.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 {
return fmt.Errorf("wayland: wl_display_flush failed: %v", err)
}
// EAGAIN means the output buffer was full. Poll for
// POLLOUT to know when we can write again.
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[:])
if err == syscall.EAGAIN {
break
}
if err != nil {
return fmt.Errorf("wayland: read from notify pipe failed: %v", err)
}
}
d.repeat.Repeat(d)
return nil
}
func (w *window) SetAnimating(anim bool) {
w.animating = anim
if anim {
w.draw(false)
}
}
// Wakeup wakes up the event loop through the notification pipe.
func (d *wlDisplay) wakeup() {
oneByte := make([]byte, 1)
if _, err := syscall.Write(d.notify.write, oneByte); err != nil && err != syscall.EAGAIN {
panic(fmt.Errorf("failed to write to pipe: %v", err))
}
}
func (w *window) destroy() {
if w.lastFrameCallback != nil {
C.wl_callback_destroy(w.lastFrameCallback)
w.lastFrameCallback = nil
}
if w.cursor.surf != nil {
C.wl_surface_destroy(w.cursor.surf)
}
if w.cursor.theme != nil {
C.wl_cursor_theme_destroy(w.cursor.theme)
}
if w.topLvl != nil {
C.xdg_toplevel_destroy(w.topLvl)
}
if w.surf != nil {
C.wl_surface_destroy(w.surf)
}
if w.wmSurf != nil {
C.xdg_surface_destroy(w.wmSurf)
}
if w.decor != nil {
C.zxdg_toplevel_decoration_v1_destroy(w.decor)
}
callbackDelete(unsafe.Pointer(w.surf))
}
//export gio_onKeyboardModifiers
func gio_onKeyboardModifiers(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial, depressed, latched, locked, group C.uint32_t) {
s := callbackLoad(data).(*wlSeat)
s.serial = serial
d := s.disp
d.repeat.Stop(0)
if d.xkb == nil {
return
}
d.xkb.UpdateMask(uint32(depressed), uint32(latched), uint32(locked), uint32(group), uint32(group), uint32(group))
}
//export gio_onKeyboardRepeatInfo
func gio_onKeyboardRepeatInfo(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, rate, delay C.int32_t) {
s := callbackLoad(data).(*wlSeat)
d := s.disp
d.repeat.Stop(0)
d.repeat.rate = int(rate)
d.repeat.delay = time.Duration(delay) * time.Millisecond
}
//export gio_onTextInputEnter
func gio_onTextInputEnter(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, surf *C.struct_wl_surface) {
}
//export gio_onTextInputLeave
func gio_onTextInputLeave(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, surf *C.struct_wl_surface) {
}
//export gio_onTextInputPreeditString
func gio_onTextInputPreeditString(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, ctxt *C.char, begin, end C.int32_t) {
}
//export gio_onTextInputCommitString
func gio_onTextInputCommitString(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, ctxt *C.char) {
}
//export gio_onTextInputDeleteSurroundingText
func gio_onTextInputDeleteSurroundingText(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, before, after C.uint32_t) {
}
//export gio_onTextInputDone
func gio_onTextInputDone(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, serial C.uint32_t) {
s := callbackLoad(data).(*wlSeat)
s.serial = serial
}
//export gio_onDataSourceTarget
func gio_onDataSourceTarget(data unsafe.Pointer, source *C.struct_wl_data_source, mime *C.char) {
}
//export gio_onDataSourceSend
func gio_onDataSourceSend(data unsafe.Pointer, source *C.struct_wl_data_source, mime *C.char, fd C.int32_t) {
s := callbackLoad(data).(*wlSeat)
content := s.content
go func() {
defer syscall.Close(int(fd))
syscall.Write(int(fd), content)
}()
}
//export gio_onDataSourceCancelled
func gio_onDataSourceCancelled(data unsafe.Pointer, source *C.struct_wl_data_source) {
s := callbackLoad(data).(*wlSeat)
if s.source == source {
s.content = nil
s.source = nil
}
C.wl_data_source_destroy(source)
}
//export gio_onDataSourceDNDDropPerformed
func gio_onDataSourceDNDDropPerformed(data unsafe.Pointer, source *C.struct_wl_data_source) {
}
//export gio_onDataSourceDNDFinished
func gio_onDataSourceDNDFinished(data unsafe.Pointer, source *C.struct_wl_data_source) {
}
//export gio_onDataSourceAction
func gio_onDataSourceAction(data unsafe.Pointer, source *C.struct_wl_data_source, act C.uint32_t) {
}
func (w *window) flushScroll() {
var fling f32.Point
if w.fling.anim.Active() {
dist := float32(w.fling.anim.Tick(time.Now()))
fling = w.fling.dir.Mul(dist)
}
// The Wayland reported scroll distance for
// discrete scroll axes is only 10 pixels, where
// 100 seems more appropriate.
const discreteScale = 10
if w.scroll.steps.X != 0 {
w.scroll.dist.X *= discreteScale
}
if w.scroll.steps.Y != 0 {
w.scroll.dist.Y *= discreteScale
}
total := w.scroll.dist.Add(fling)
if total == (f32.Point{}) {
return
}
if w.scroll.steps == (image.Point{}) {
w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X)
w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y)
}
// Zero scroll distance prior to calling ProcessEvent, otherwise we may recursively
// re-process the scroll distance.
w.scroll.dist = f32.Point{}
w.scroll.steps = image.Point{}
w.ProcessEvent(pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
Position: w.lastPos,
Scroll: total,
Time: w.scroll.time,
Modifiers: w.disp.xkb.Modifiers(),
})
}
func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
w.flushScroll()
w.lastPos = f32.Point{
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Position: w.lastPos,
Buttons: w.pointerBtns,
Source: pointer.Mouse,
Time: time.Duration(t) * time.Millisecond,
Modifiers: w.disp.xkb.Modifiers(),
})
c, _ := w.systemGesture()
if c != w.cursor.system {
w.cursor.system = c
w.updateCursor()
}
}
// updateCursor updates the system gesture cursor according to the pointer
// position.
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)
x, y, size := int(w.lastPos.X), int(w.lastPos.Y), w.config.Size
north := y <= border
south := y >= size.Y-border
west := x <= border
east := x >= size.X-border
switch {
default:
fallthrough
case !north && !south && !west && !east:
return nil, 0
case north && west:
return w.cursor.cursors.resizeNorthWest, C.XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT
case north && east:
return w.cursor.cursors.resizeNorthEast, C.XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT
case south && west:
return w.cursor.cursors.resizeSouthWest, C.XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT
case south && east:
return w.cursor.cursors.resizeSouthEast, C.XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT
case north:
return w.cursor.cursors.resizeNorth, C.XDG_TOPLEVEL_RESIZE_EDGE_TOP
case south:
return w.cursor.cursors.resizeSouth, C.XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM
case west:
return w.cursor.cursors.resizeWest, C.XDG_TOPLEVEL_RESIZE_EDGE_LEFT
case east:
return w.cursor.cursors.resizeEast, C.XDG_TOPLEVEL_RESIZE_EDGE_RIGHT
}
}
func (w *window) updateOpaqueRegion() {
reg := C.wl_compositor_create_region(w.disp.compositor)
C.wl_region_add(reg, 0, 0, C.int32_t(w.size.X), C.int32_t(w.size.Y))
C.wl_surface_set_opaque_region(w.surf, reg)
C.wl_region_destroy(reg)
}
func (w *window) updateOutputs() {
scale := 1
var found bool
for _, conf := range w.disp.outputConfig {
for _, w2 := range conf.windows {
if w2 == w {
found = true
if conf.scale > scale {
scale = conf.scale
}
}
}
}
if found && scale != w.scale {
w.scale = scale
C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale))
w.draw(true)
}
if found {
w.draw(true)
}
}
func (w *window) getConfig() (image.Point, unit.Metric) {
size := w.size.Mul(w.scale)
return size, unit.Metric{
PxPerDp: w.ppdp * float32(w.scale),
PxPerSp: w.ppsp * float32(w.scale),
}
}
func (w *window) draw(sync bool) {
if !w.configured {
return
}
w.flushScroll()
size, cfg := w.getConfig()
if cfg == (unit.Metric{}) {
return
}
if size != w.config.Size {
w.config.Size = size
w.ProcessEvent(ConfigEvent{Config: w.config})
}
anim := w.animating || w.fling.anim.Active()
// Draw animation only when not waiting for frame callback.
redrawAnim := anim && w.lastFrameCallback == nil
if !redrawAnim && !sync {
return
}
if anim {
w.lastFrameCallback = C.wl_surface_frame(w.surf)
// 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{
Now: time.Now(),
Size: w.config.Size,
Metric: cfg,
},
Sync: sync,
})
}
func (w *window) display() *C.struct_wl_display {
return w.disp.disp
}
func (w *window) surface() (*C.struct_wl_surface, int, int) {
sz, _ := w.getConfig()
return w.surf, sz.X, sz.Y
}
func (w *window) ShowTextInput(show bool) {}
func (w *window) SetInputHint(_ key.InputHint) {}
func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) NewContext() (context, error) {
var firstErr error
if f := newWaylandEGLContext; f != nil {
c, err := f(w)
if err == nil {
return c, nil
}
firstErr = err
}
if f := newWaylandVulkanContext; f != nil {
c, err := f(w)
if err == nil {
return c, nil
}
firstErr = err
}
if firstErr != nil {
return nil, firstErr
}
return nil, errors.New("wayland: no available GPU backends")
}
// detectUIScale reports the system UI scale, or 1.0 if it fails.
func detectUIScale() float32 {
// TODO: What about other window environments?
out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "text-scaling-factor").Output()
if err != nil {
return 1.0
}
scale, err := strconv.ParseFloat(string(bytes.TrimSpace(out)), 32)
if err != nil {
return 1.0
}
return float32(scale)
}
func newWLDisplay() (*wlDisplay, error) {
d := &wlDisplay{
outputMap: make(map[C.uint32_t]*C.struct_wl_output),
outputConfig: make(map[*C.struct_wl_output]*wlOutput),
}
pipe := make([]int, 2)
if err := syscall.Pipe2(pipe, syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil {
return nil, fmt.Errorf("wayland: failed to create pipe: %v", err)
}
d.notify.read = pipe[0]
d.notify.write = pipe[1]
xkb, err := xkb.New()
if err != nil {
d.destroy()
return nil, fmt.Errorf("wayland: %v", err)
}
d.xkb = xkb
d.disp, err = C.wl_display_connect(nil)
if d.disp == nil {
d.destroy()
return nil, fmt.Errorf("wayland: wl_display_connect failed: %v", err)
}
callbackMap.Store(unsafe.Pointer(d.disp), d)
d.reg = C.wl_display_get_registry(d.disp)
if d.reg == nil {
d.destroy()
return nil, errors.New("wayland: wl_display_get_registry failed")
}
C.wl_registry_add_listener(d.reg, &C.gio_registry_listener, unsafe.Pointer(d.disp))
// Wait for the server to register all its globals to the
// registry listener (gio_onRegistryGlobal).
C.wl_display_roundtrip(d.disp)
// Configuration listeners are added to outputs by gio_onRegistryGlobal.
// We need another roundtrip to get the initial output configurations
// through the gio_onOutput* callbacks.
C.wl_display_roundtrip(d.disp)
return d, nil
}
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
}
if d.notify.read != 0 {
syscall.Close(d.notify.read)
d.notify.read = 0
}
d.repeat.Stop(0)
if d.xkb != nil {
d.xkb.Destroy()
d.xkb = nil
}
if d.seat != nil {
d.seat.destroy()
d.seat = nil
}
if d.imm != nil {
C.zwp_text_input_manager_v3_destroy(d.imm)
}
if d.decor != nil {
C.zxdg_decoration_manager_v1_destroy(d.decor)
}
if d.shm != nil {
C.wl_shm_destroy(d.shm)
}
if d.compositor != nil {
C.wl_compositor_destroy(d.compositor)
}
if d.wm != nil {
C.xdg_wm_base_destroy(d.wm)
}
for _, output := range d.outputMap {
C.wl_output_destroy(output)
}
if d.reg != nil {
C.wl_registry_destroy(d.reg)
}
if d.disp != nil {
C.wl_display_disconnect(d.disp)
callbackDelete(unsafe.Pointer(d.disp))
d.disp = nil
}
}
// fromFixed converts a Wayland wl_fixed_t 23.8 number to float32.
func fromFixed(v C.wl_fixed_t) float32 {
// Convert to float64 to avoid overflow.
// From wayland-util.h.
b := ((1023 + 44) << 52) + (1 << 51) + uint64(v)
f := math.Float64frombits(b) - (3 << 43)
return float32(f)
}