// 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

	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
	seat       *wlSeat
	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
	w := callbackLoad(unsafe.Pointer(surf)).(*window)
	w.seat = s
	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)
	w.seat = nil
	s := callbackLoad(data).(*wlSeat)
	s.serial = serial
	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
	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.seat
	if !w.inCompositor && s != nil {
		w.inCompositor = true
		C.xdg_toplevel_move(w.topLvl, s.seat, serial)
	}
}

func (w *window) resize(serial, edge C.uint32_t) {
	s := w.seat
	if w.inCompositor || s == nil {
		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() {
	ptr := w.disp.seat.pointer
	if ptr == nil {
		return
	}
	w.setCursor(ptr, w.serial)
}

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, w.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)
}
