Files
gio/ui/app/os_wayland.go
T
Elias Naur 64add13d28 ui/app: (wayland) wait for xdg_shell configure event before drawing
According to the xdg_shell documentation, the xdg_surface must be
configured before use.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2019-09-24 17:28:04 +02:00

1341 lines
35 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
// +build linux,!android
package app
import (
"bytes"
"errors"
"fmt"
"image"
"math"
"os"
"os/exec"
"strconv"
"sync"
"time"
"unicode"
"unicode/utf8"
"unsafe"
"gioui.org/ui/f32"
"gioui.org/ui/internal/fling"
"gioui.org/ui/key"
"gioui.org/ui/pointer"
syscall "golang.org/x/sys/unix"
)
// 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;^;// +build linux,!android\\n\\n;" wayland_xdg_shell.c
//go:generate sed -i "1s;^;// +build linux,!android\\n\\n;" wayland_xdg_decoration.c
//go:generate sed -i "1s;^;// +build linux,!android\\n\\n;" wayland_text_input.c
/*
#cgo LDFLAGS: -lwayland-client -lwayland-cursor -lxkbcommon
#include <stdlib.h>
#include <wayland-client.h>
#include <wayland-cursor.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-compose.h>
#include "wayland_text_input.h"
#include "wayland_xdg_shell.h"
#include "wayland_xdg_decoration.h"
#include "os_wayland.h"
*/
import "C"
type wlConn struct {
disp *C.struct_wl_display
compositor *C.struct_wl_compositor
wm *C.struct_xdg_wm_base
imm *C.struct_zwp_text_input_manager_v3
im *C.struct_zwp_text_input_v3
shm *C.struct_wl_shm
cursorTheme *C.struct_wl_cursor_theme
cursor *C.struct_wl_cursor
cursorSurf *C.struct_wl_surface
decor *C.struct_zxdg_decoration_manager_v1
seat *C.struct_wl_seat
seatName C.uint32_t
pointer *C.struct_wl_pointer
touch *C.struct_wl_touch
keyboard *C.struct_wl_keyboard
xkb *C.struct_xkb_context
xkbMap *C.struct_xkb_keymap
xkbState *C.struct_xkb_state
xkbCompTable *C.struct_xkb_compose_table
xkbCompState *C.struct_xkb_compose_state
utf8Buf []byte
repeat repeatState
}
type repeatState struct {
rate int
delay time.Duration
key C.uint32_t
win *window
stopC chan struct{}
start time.Duration
last time.Duration
mu sync.Mutex
now time.Duration
}
type window struct {
w *Window
disp *C.struct_wl_display
surf *C.struct_wl_surface
wmSurf *C.struct_xdg_surface
topLvl *C.struct_xdg_toplevel
decor *C.struct_zxdg_toplevel_decoration_v1
// Notification pipe fds.
notRead, notWrite int
ppdp, ppsp float32
scrollTime time.Duration
discScroll image.Point
scroll f32.Point
lastPos f32.Point
lastTouch f32.Point
// Flinging.
yExtrapolation fling.Extrapolation
xExtrapolation fling.Extrapolation
flinger fling.Animation
startFling bool
// Fling direction.
flingDir f32.Point
stage Stage
dead bool
lastFrameCallback *C.struct_wl_callback
mu sync.Mutex
animating bool
needAck bool
// The last configure serial waiting to be ack'ed.
serial C.uint32_t
width int
height int
newScale bool
scale int
}
type wlOutput struct {
width int
height int
physWidth int
physHeight int
transform C.int32_t
scale int
windows []*window
}
var connMu sync.Mutex
var conn *wlConn
var mainDone = make(chan struct{})
var (
winMap = make(map[interface{}]*window)
outputMap = make(map[C.uint32_t]*C.struct_wl_output)
outputConfig = make(map[*C.struct_wl_output]*wlOutput)
)
var (
_XKB_MOD_NAME_CTRL = []byte("Control\x00")
_XKB_MOD_NAME_SHIFT = []byte("Shift\x00")
)
func main() {
<-mainDone
}
func createWindow(window *Window, opts *windowOptions) error {
connMu.Lock()
defer connMu.Unlock()
if len(winMap) > 0 {
return errors.New("multiple windows are not supported")
}
if err := waylandConnect(); err != nil {
return err
}
w, err := createNativeWindow(opts)
if err != nil {
conn.destroy()
return err
}
w.w = window
go func() {
w.w.setDriver(w)
w.loop()
w.destroy()
conn.destroy()
close(mainDone)
}()
return nil
}
func createNativeWindow(opts *windowOptions) (*window, error) {
pipe := make([]int, 2)
if err := syscall.Pipe2(pipe, syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil {
return nil, fmt.Errorf("createNativeWindow: failed to create pipe: %v", err)
}
fontScale := detectFontScale()
var ppmm float32
var scale int
for _, conf := range outputConfig {
if d, err := conf.ppmm(); err == nil && d > ppmm {
ppmm = d
}
if s := conf.scale; s > scale {
scale = s
}
}
ppdp := ppmm * mmPrDp
ppsp := ppdp * fontScale
ppdp *= monitorScale
if ppdp < minDensity {
ppdp = minDensity
}
if ppsp < minDensity {
ppsp = minDensity
}
w := &window{
disp: conn.disp,
scale: scale,
newScale: scale != 1,
ppdp: ppdp,
ppsp: ppsp,
notRead: pipe[0],
notWrite: pipe[1],
}
w.surf = C.wl_compositor_create_surface(conn.compositor)
if w.surf == nil {
w.destroy()
return nil, errors.New("wayland: wl_compositor_create_surface failed")
}
w.wmSurf = C.xdg_wm_base_get_xdg_surface(conn.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")
}
C.gio_xdg_wm_base_add_listener(conn.wm)
C.gio_wl_surface_add_listener(w.surf)
C.gio_xdg_surface_add_listener(w.wmSurf)
C.gio_xdg_toplevel_add_listener(w.topLvl)
title := C.CString(opts.Title)
C.xdg_toplevel_set_title(w.topLvl, title)
C.free(unsafe.Pointer(title))
_, _, cfg := w.config()
w.width = cfg.Px(opts.Width)
w.height = cfg.Px(opts.Height)
if conn.decor != nil {
// Request server side decorations.
w.decor = C.zxdg_decoration_manager_v1_get_toplevel_decoration(conn.decor, w.topLvl)
C.zxdg_toplevel_decoration_v1_set_mode(w.decor, C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE)
}
w.updateOpaqueRegion()
C.wl_surface_commit(w.surf)
winMap[w.topLvl] = w
winMap[w.surf] = w
winMap[w.wmSurf] = w
return w, nil
}
//export gio_onSeatCapabilities
func gio_onSeatCapabilities(data unsafe.Pointer, seat *C.struct_wl_seat, caps C.uint32_t) {
if seat != conn.seat {
panic("unexpected seat")
}
if conn.im == nil && conn.imm != nil {
conn.im = C.zwp_text_input_manager_v3_get_text_input(conn.imm, conn.seat)
C.gio_zwp_text_input_v3_add_listener(conn.im)
}
switch {
case conn.pointer == nil && caps&C.WL_SEAT_CAPABILITY_POINTER != 0:
conn.pointer = C.wl_seat_get_pointer(seat)
C.gio_wl_pointer_add_listener(conn.pointer)
case conn.pointer != nil && caps&C.WL_SEAT_CAPABILITY_POINTER == 0:
C.wl_pointer_release(conn.pointer)
conn.pointer = nil
}
switch {
case conn.touch == nil && caps&C.WL_SEAT_CAPABILITY_TOUCH != 0:
conn.touch = C.wl_seat_get_touch(seat)
C.gio_wl_touch_add_listener(conn.touch)
case conn.touch != nil && caps&C.WL_SEAT_CAPABILITY_TOUCH == 0:
C.wl_touch_release(conn.touch)
conn.touch = nil
}
switch {
case conn.keyboard == nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD != 0:
conn.keyboard = C.wl_seat_get_keyboard(seat)
C.gio_wl_keyboard_add_listener(conn.keyboard)
case conn.keyboard != nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD == 0:
C.wl_keyboard_release(conn.keyboard)
conn.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 := winMap[wmSurf]
w.mu.Lock()
w.serial = serial
w.needAck = true
w.mu.Unlock()
w.setStage(StageRunning)
w.draw(true)
}
//export gio_onToplevelClose
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
w := winMap[topLvl]
w.dead = 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 := winMap[topLvl]
if width != 0 && height != 0 {
w.mu.Lock()
defer w.mu.Unlock()
w.width = int(width)
w.height = int(height)
w.updateOpaqueRegion()
}
}
//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
}
c := 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) {
c := 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) {
c := outputConfig[output]
c.scale = int(scale)
}
//export gio_onOutputDone
func gio_onOutputDone(data unsafe.Pointer, output *C.struct_wl_output) {
conf := outputConfig[output]
for _, w := range conf.windows {
w.draw(true)
}
}
//export gio_onSurfaceEnter
func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output *C.struct_wl_output) {
w := winMap[surf]
conf := 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()
}
//export gio_onSurfaceLeave
func gio_onSurfaceLeave(data unsafe.Pointer, surf *C.struct_wl_surface, output *C.struct_wl_output) {
w := winMap[surf]
conf := 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) {
switch C.GoString(cintf) {
case "wl_compositor":
conn.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.gio_wl_output_add_listener(output)
outputMap[name] = output
outputConfig[output] = new(wlOutput)
case "wl_seat":
if conn.seat == nil {
conn.seatName = name
conn.seat = (*C.struct_wl_seat)(C.wl_registry_bind(reg, name, &C.wl_seat_interface, 5))
C.gio_wl_seat_add_listener(conn.seat)
}
case "wl_shm":
conn.shm = (*C.struct_wl_shm)(C.wl_registry_bind(reg, name, &C.wl_shm_interface, 1))
case "xdg_wm_base":
conn.wm = (*C.struct_xdg_wm_base)(C.wl_registry_bind(reg, name, &C.xdg_wm_base_interface, 1))
case "zxdg_decoration_manager_v1":
conn.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":
conn.imm = (*C.struct_zwp_text_input_manager_v3)(C.wl_registry_bind(reg, name, &C.zwp_text_input_manager_v3_interface, 1))*/
}
}
//export gio_onRegistryGlobalRemove
func gio_onRegistryGlobalRemove(data unsafe.Pointer, reg *C.struct_wl_registry, name C.uint32_t) {
if conn.seat != nil && name == conn.seatName {
if conn.im != nil {
C.zwp_text_input_v3_destroy(conn.im)
conn.im = nil
}
if conn.pointer != nil {
delete(winMap, conn.pointer)
}
if conn.touch != nil {
delete(winMap, conn.touch)
}
if conn.keyboard != nil {
delete(winMap, conn.keyboard)
}
C.wl_seat_release(conn.seat)
conn.seat = nil
}
if output, exists := outputMap[name]; exists {
C.wl_output_destroy(output)
delete(outputMap, name)
delete(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) {
w := winMap[surf]
winMap[touch] = w
w.lastTouch = f32.Point{
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.w.event(pointer.Event{
Type: pointer.Press,
Source: pointer.Touch,
Position: w.lastTouch,
PointerID: pointer.ID(id),
Time: time.Duration(t) * time.Millisecond,
})
}
//export gio_onTouchUp
func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.uint32_t, id C.int32_t) {
w := winMap[touch]
w.w.event(pointer.Event{
Type: pointer.Release,
Source: pointer.Touch,
Position: w.lastTouch,
PointerID: pointer.ID(id),
Time: time.Duration(t) * time.Millisecond,
})
}
//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) {
w := winMap[touch]
w.lastTouch = f32.Point{
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.w.event(pointer.Event{
Type: pointer.Move,
Position: w.lastTouch,
Source: pointer.Touch,
PointerID: pointer.ID(id),
Time: time.Duration(t) * time.Millisecond,
})
}
//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) {
w := winMap[touch]
w.w.event(pointer.Event{
Type: 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) {
// Get images[0].
img := *conn.cursor.images
buf := C.wl_cursor_image_get_buffer(img)
if buf == nil {
return
}
C.wl_pointer_set_cursor(pointer, serial, conn.cursorSurf, C.int32_t(img.hotspot_x), C.int32_t(img.hotspot_y))
C.wl_surface_attach(conn.cursorSurf, buf, 0, 0)
C.wl_surface_damage(conn.cursorSurf, 0, 0, C.int32_t(img.width), C.int32_t(img.height))
C.wl_surface_commit(conn.cursorSurf)
w := winMap[surf]
winMap[pointer] = w
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, surface *C.struct_wl_surface) {
}
//export gio_onPointerMotion
func gio_onPointerMotion(data unsafe.Pointer, p *C.struct_wl_pointer, t C.uint32_t, x, y C.wl_fixed_t) {
w := winMap[p]
w.resetFling()
w.onPointerMotion(x, y, t)
}
//export gio_onPointerButton
func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t, button, state C.uint32_t) {
w := winMap[p]
// From linux-event-codes.h.
const BTN_LEFT = 0x110
if button != BTN_LEFT {
return
}
var typ pointer.Type
switch state {
case 0:
typ = pointer.Release
case 1:
typ = pointer.Press
}
w.flushScroll()
w.resetFling()
w.w.event(pointer.Event{
Type: typ,
Source: pointer.Mouse,
Position: w.lastPos,
Time: time.Duration(t) * time.Millisecond,
})
}
//export gio_onPointerAxis
func gio_onPointerAxis(data unsafe.Pointer, ptr *C.struct_wl_pointer, t, axis C.uint32_t, value C.wl_fixed_t) {
w := winMap[ptr]
v := fromFixed(value)
w.resetFling()
if w.scroll == (f32.Point{}) {
w.scrollTime = time.Duration(t) * time.Millisecond
}
switch axis {
case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL:
w.scroll.X += v
case C.WL_POINTER_AXIS_VERTICAL_SCROLL:
w.scroll.Y += v
}
}
//export gio_onPointerFrame
func gio_onPointerFrame(data unsafe.Pointer, pointer *C.struct_wl_pointer) {
w := winMap[pointer]
w.flushScroll()
w.flushFling()
}
func (w *window) flushFling() {
if !w.startFling {
return
}
w.startFling = false
estx, esty := w.xExtrapolation.Estimate(), w.yExtrapolation.Estimate()
w.xExtrapolation = fling.Extrapolation{}
w.yExtrapolation = fling.Extrapolation{}
vel := float32(math.Sqrt(float64(estx.Velocity*estx.Velocity + esty.Velocity*esty.Velocity)))
_, _, c := w.config()
c.now = time.Now()
if !w.flinger.Start(&c, vel) {
return
}
invDist := 1 / vel
w.flingDir.X = estx.Velocity * invDist
w.flingDir.Y = esty.Velocity * invDist
// Wake up the window loop.
w.notify()
}
//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, ptr *C.struct_wl_pointer, t, axis C.uint32_t) {
w := winMap[ptr]
w.startFling = true
}
//export gio_onPointerAxisDiscrete
func gio_onPointerAxisDiscrete(data unsafe.Pointer, pointer *C.struct_wl_pointer, axis C.uint32_t, discrete C.int32_t) {
w := winMap[pointer]
w.resetFling()
switch axis {
case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL:
w.discScroll.X += int(discrete)
case C.WL_POINTER_AXIS_VERTICAL_SCROLL:
w.discScroll.Y += int(discrete)
}
}
func (w *window) resetFling() {
w.startFling = false
w.flinger = 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) {
conn.repeat.Stop(0)
defer syscall.Close(int(fd))
if conn.xkbCompState != nil {
C.xkb_compose_state_unref(conn.xkbCompState)
conn.xkbCompState = nil
}
if conn.xkbCompTable != nil {
C.xkb_compose_table_unref(conn.xkbCompTable)
conn.xkbCompTable = nil
}
if conn.xkbState != nil {
C.xkb_state_unref(conn.xkbState)
conn.xkbState = nil
}
if conn.xkbMap != nil {
C.xkb_keymap_unref(conn.xkbMap)
conn.xkbMap = nil
}
if format != C.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 {
return
}
if conn.xkb == nil {
conn.xkb = C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS)
}
if conn.xkb == nil {
return
}
if conn.xkbCompTable == nil {
locale := os.Getenv("LC_ALL")
if locale == "" {
locale = os.Getenv("LC_CTYPE")
}
if locale == "" {
locale = os.Getenv("LANG")
}
if locale == "" {
locale = "C"
}
cloc := C.CString(locale)
defer C.free(unsafe.Pointer(cloc))
conn.xkbCompTable = C.xkb_compose_table_new_from_locale(conn.xkb, cloc, C.XKB_COMPOSE_COMPILE_NO_FLAGS)
if conn.xkbCompTable != nil {
conn.xkbCompState = C.xkb_compose_state_new(conn.xkbCompTable, C.XKB_COMPOSE_STATE_NO_FLAGS)
}
}
mapData, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED)
if err != nil {
return
}
defer syscall.Munmap(mapData)
conn.xkbMap = C.xkb_keymap_new_from_buffer(conn.xkb, (*C.char)(unsafe.Pointer(&mapData[0])), C.size_t(size-1), C.XKB_KEYMAP_FORMAT_TEXT_V1, C.XKB_KEYMAP_COMPILE_NO_FLAGS)
if conn.xkbMap == nil {
return
}
conn.xkbState = C.xkb_state_new(conn.xkbMap)
}
//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) {
conn.repeat.Stop(0)
w := winMap[surf]
winMap[keyboard] = w
w.w.event(key.FocusEvent{Focus: true})
}
//export gio_onKeyboardLeave
func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial C.uint32_t, surf *C.struct_wl_surface) {
conn.repeat.Stop(0)
w := winMap[keyboard]
w.w.event(key.FocusEvent{Focus: false})
}
//export gio_onKeyboardKey
func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial, timestamp, keyCode, state C.uint32_t) {
t := time.Duration(timestamp) * time.Millisecond
conn.repeat.Stop(t)
w := winMap[keyboard]
w.resetFling()
if state != C.WL_KEYBOARD_KEY_STATE_PRESSED || conn.xkbMap == nil || conn.xkbState == nil || conn.xkbCompState == nil {
return
}
// According to the xkb_v1 spec: "to determine the xkb keycode, clients must add 8 to the key event keycode."
keyCode += 8
w.dispatchKey(keyCode)
if C.xkb_keymap_key_repeats(conn.xkbMap, C.xkb_keycode_t(keyCode)) == 1 {
conn.repeat.Start(w, keyCode, t)
}
}
func (r *repeatState) Start(w *window, keyCode C.uint32_t, 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.notify()
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() {
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
}
r.win.dispatchKey(r.key)
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)
surf := (*C.struct_wl_surface)(data)
w := winMap[surf]
if w.lastFrameCallback == callback {
w.lastFrameCallback = nil
w.draw(false)
}
}
func (w *window) loop() {
dispfd := C.wl_display_get_fd(conn.disp)
// Poll for events and notifications.
pollfds := []syscall.PollFd{
{Fd: int32(dispfd), Events: syscall.POLLIN | syscall.POLLERR},
{Fd: int32(w.notRead), Events: syscall.POLLIN | syscall.POLLERR},
}
dispEvents := &pollfds[0].Revents
// Plenty of room for a backlog of notifications.
var buf = make([]byte, 100)
loop:
for {
C.wl_display_dispatch_pending(conn.disp)
if ret := C.wl_display_flush(conn.disp); ret < 0 {
break
}
if w.dead {
w.w.event(DestroyEvent{})
break
}
// Clear poll events.
*dispEvents = 0
if _, err := syscall.Ppoll(pollfds, nil, nil); err != nil && err != syscall.EINTR {
panic(fmt.Errorf("ppoll failed: %v", err))
}
redraw := false
// Clear notifications.
for {
_, err := syscall.Read(w.notRead, buf)
if err == syscall.EAGAIN {
break
}
if err != nil {
panic(fmt.Errorf("read from notify pipe failed: %v", err))
}
redraw = true
}
// Handle events
switch {
case *dispEvents&syscall.POLLIN != 0:
if ret := C.wl_display_dispatch(conn.disp); ret < 0 {
break loop
}
case *dispEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
break loop
}
conn.repeat.Repeat()
if redraw {
w.draw(false)
}
}
}
func (w *window) setAnimating(anim bool) {
w.mu.Lock()
w.animating = anim
animating := w.isAnimating()
w.mu.Unlock()
if animating {
w.notify()
}
}
// Wakeup wakes up the event loop through the notification pipe.
func (w *window) notify() {
oneByte := make([]byte, 1)
if _, err := syscall.Write(w.notWrite, oneByte); err != nil && err != syscall.EAGAIN {
panic(fmt.Errorf("failed to write to pipe: %v", err))
}
}
func (w *window) destroy() {
if w.notWrite != 0 {
syscall.Close(w.notWrite)
w.notWrite = 0
}
if w.notRead != 0 {
syscall.Close(w.notRead)
w.notRead = 0
}
if w.topLvl != nil {
delete(winMap, w.topLvl)
C.xdg_toplevel_destroy(w.topLvl)
}
if w.surf != nil {
delete(winMap, w.surf)
C.wl_surface_destroy(w.surf)
}
if w.wmSurf != nil {
delete(winMap, w.wmSurf)
C.xdg_surface_destroy(w.wmSurf)
}
if w.decor != nil {
C.zxdg_toplevel_decoration_v1_destroy(w.decor)
}
}
func (w *window) dispatchKey(keyCode C.uint32_t) {
if len(conn.utf8Buf) == 0 {
conn.utf8Buf = make([]byte, 1)
}
sym := C.xkb_state_key_get_one_sym(conn.xkbState, C.xkb_keycode_t(keyCode))
if n, ok := convertKeysym(sym); ok {
cmd := key.Event{Name: n}
if C.xkb_state_mod_name_is_active(conn.xkbState, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_CTRL[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 {
cmd.Modifiers |= key.ModCommand
}
if C.xkb_state_mod_name_is_active(conn.xkbState, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_SHIFT[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 {
cmd.Modifiers |= key.ModShift
}
w.w.event(cmd)
}
C.xkb_compose_state_feed(conn.xkbCompState, sym)
var size C.int
switch C.xkb_compose_state_get_status(conn.xkbCompState) {
case C.XKB_COMPOSE_CANCELLED, C.XKB_COMPOSE_COMPOSING:
return
case C.XKB_COMPOSE_COMPOSED:
size = C.xkb_compose_state_get_utf8(conn.xkbCompState, (*C.char)(unsafe.Pointer(&conn.utf8Buf[0])), C.size_t(len(conn.utf8Buf)))
if int(size) >= len(conn.utf8Buf) {
conn.utf8Buf = make([]byte, size+1)
size = C.xkb_compose_state_get_utf8(conn.xkbCompState, (*C.char)(unsafe.Pointer(&conn.utf8Buf[0])), C.size_t(len(conn.utf8Buf)))
}
C.xkb_compose_state_reset(conn.xkbCompState)
case C.XKB_COMPOSE_NOTHING:
size = C.xkb_state_key_get_utf8(conn.xkbState, C.xkb_keycode_t(keyCode), (*C.char)(unsafe.Pointer(&conn.utf8Buf[0])), C.size_t(len(conn.utf8Buf)))
if int(size) >= len(conn.utf8Buf) {
conn.utf8Buf = make([]byte, size+1)
size = C.xkb_state_key_get_utf8(conn.xkbState, C.xkb_keycode_t(keyCode), (*C.char)(unsafe.Pointer(&conn.utf8Buf[0])), C.size_t(len(conn.utf8Buf)))
}
}
// Report only printable runes.
str := conn.utf8Buf[:size]
var n int
for n < len(str) {
r, s := utf8.DecodeRune(str)
if unicode.IsPrint(r) {
n += s
} else {
copy(str[n:], str[n+s:])
str = str[:len(str)-s]
}
}
if len(str) > 0 {
w.w.event(key.EditEvent{Text: string(str)})
}
}
//export gio_onKeyboardModifiers
func gio_onKeyboardModifiers(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial, depressed, latched, locked, group C.uint32_t) {
conn.repeat.Stop(0)
if conn.xkbState == nil {
return
}
xkbGrp := C.xkb_layout_index_t(group)
C.xkb_state_update_mask(conn.xkbState, C.xkb_mod_mask_t(depressed), C.xkb_mod_mask_t(latched), C.xkb_mod_mask_t(locked), xkbGrp, xkbGrp, xkbGrp)
}
//export gio_onKeyboardRepeatInfo
func gio_onKeyboardRepeatInfo(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, rate, delay C.int32_t) {
conn.repeat.Stop(0)
conn.repeat.rate = int(rate)
conn.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) {
}
// ppmm returns the approximate pixels per millimeter for the output.
func (c *wlOutput) ppmm() (float32, error) {
if c.physWidth == 0 || c.physHeight == 0 {
return 0, errors.New("no physical size data for output")
}
// Because of https://gitlab.gnome.org/GNOME/mutter/issues/369, output dimensions might be undetectably swapped.
// Instead, compute and return sqrt(px²/mm²).
density := float32(math.Sqrt(float64(c.width*c.height) / float64(c.physWidth*c.physHeight)))
return density, nil
}
func (w *window) flushScroll() {
var fling f32.Point
if w.flinger.Active() {
dist := float32(w.flinger.Tick(time.Now()))
fling = w.flingDir.Mul(dist)
}
// The Wayland reported scroll distance for
// discrete scroll axis is only 10 pixels, where
// 100 seems more appropriate.
const discreteScale = 10
if w.discScroll.X != 0 {
w.scroll.X *= discreteScale
}
if w.discScroll.Y != 0 {
w.scroll.Y *= discreteScale
}
total := w.scroll.Add(fling)
if total == (f32.Point{}) {
return
}
w.w.event(pointer.Event{
Type: pointer.Move,
Source: pointer.Mouse,
Position: w.lastPos,
Scroll: total,
Time: w.scrollTime,
})
if w.discScroll == (image.Point{}) {
w.xExtrapolation.SampleDelta(w.scrollTime, -w.scroll.X)
w.yExtrapolation.SampleDelta(w.scrollTime, -w.scroll.Y)
}
w.scroll = f32.Point{}
w.discScroll = image.Point{}
}
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.w.event(pointer.Event{
Type: pointer.Move,
Position: w.lastPos,
Source: pointer.Mouse,
Time: time.Duration(t) * time.Millisecond,
})
}
func (w *window) updateOpaqueRegion() {
reg := C.wl_compositor_create_region(conn.compositor)
C.wl_region_add(reg, 0, 0, C.int32_t(w.width), C.int32_t(w.height))
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 outputConfig {
for _, w2 := range conf.windows {
if w2 == w {
found = true
if conf.scale > scale {
scale = conf.scale
}
}
}
}
w.mu.Lock()
if found && scale != w.scale {
w.scale = scale
w.newScale = true
}
w.mu.Unlock()
if !found {
w.setStage(StagePaused)
} else {
w.setStage(StageRunning)
w.draw(true)
}
}
func (w *window) config() (int, int, Config) {
width, height := w.width*w.scale, w.height*w.scale
return width, height, Config{
pxPerDp: w.ppdp * float32(w.scale),
pxPerSp: w.ppsp * float32(w.scale),
}
}
func (w *window) isAnimating() bool {
return w.animating || w.flinger.Active()
}
func (w *window) draw(sync bool) {
w.flushScroll()
w.mu.Lock()
animating := w.isAnimating()
dead := w.dead
w.mu.Unlock()
if dead || (!animating && !sync) {
return
}
width, height, cfg := w.config()
if cfg == (Config{}) {
return
}
if animating && w.lastFrameCallback == nil {
w.lastFrameCallback = C.wl_surface_frame(w.surf)
// Use the surface as listener data for gio_onFrameDone.
C.gio_wl_callback_add_listener(w.lastFrameCallback, unsafe.Pointer(w.surf))
}
cfg.now = time.Now()
w.w.event(UpdateEvent{
Size: image.Point{
X: width,
Y: height,
},
Config: cfg,
sync: sync,
})
}
func (w *window) setStage(s Stage) {
if s == w.stage {
return
}
w.stage = s
w.w.event(StageEvent{s})
}
func (w *window) display() unsafe.Pointer {
return unsafe.Pointer(w.disp)
}
func (w *window) nativeWindow(visID int) (unsafe.Pointer, int, int) {
w.mu.Lock()
defer w.mu.Unlock()
if w.needAck {
C.xdg_surface_ack_configure(w.wmSurf, w.serial)
w.needAck = false
}
width, height, scale := w.width, w.height, w.scale
if w.newScale {
C.wl_surface_set_buffer_scale(w.surf, C.int32_t(scale))
w.newScale = false
}
return unsafe.Pointer(w.surf), width * scale, height * scale
}
func (w *window) showTextInput(show bool) {}
// detectFontScale reports current font scale, or 1.0
// if it fails.
func detectFontScale() 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 waylandConnect() error {
c := new(wlConn)
conn = c
c.disp = C.wl_display_connect(nil)
if c.disp == nil {
c.destroy()
return errors.New("wayland: wl_display_connect failed")
}
reg := C.wl_display_get_registry(c.disp)
if reg == nil {
c.destroy()
return errors.New("wayland: wl_display_get_registry failed")
}
C.gio_wl_registry_add_listener(reg)
// Wait for the server to register all its globals to the
// registry listener (gio_onRegistryGlobal).
C.wl_display_roundtrip(c.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(c.disp)
if c.compositor == nil {
c.destroy()
return errors.New("wayland: no compositor available")
}
if c.wm == nil {
c.destroy()
return errors.New("wayland: no xdg_wm_base available")
}
if c.shm == nil {
c.destroy()
return errors.New("wayland: no wl_shm available")
}
if len(outputMap) == 0 {
c.destroy()
return errors.New("wayland: no outputs available")
}
c.cursorTheme = C.wl_cursor_theme_load(nil, 32, c.shm)
if c.cursorTheme == nil {
c.destroy()
return errors.New("wayland: wl_cursor_theme_load failed")
}
cname := C.CString("left_ptr")
defer C.free(unsafe.Pointer(cname))
c.cursor = C.wl_cursor_theme_get_cursor(c.cursorTheme, cname)
if c.cursor == nil {
c.destroy()
return errors.New("wayland: wl_cursor_theme_get_cursor failed")
}
c.cursorSurf = C.wl_compositor_create_surface(conn.compositor)
if c.cursorSurf == nil {
c.destroy()
return errors.New("wayland: wl_compositor_create_surface failed")
}
return nil
}
func (c *wlConn) destroy() {
c.repeat.Stop(0)
if c.xkbCompState != nil {
C.xkb_compose_state_unref(c.xkbCompState)
c.xkbCompState = nil
}
if c.xkbCompTable != nil {
C.xkb_compose_table_unref(c.xkbCompTable)
c.xkbCompTable = nil
}
if c.xkbState != nil {
C.xkb_state_unref(conn.xkbState)
}
if c.xkbMap != nil {
C.xkb_keymap_unref(c.xkbMap)
}
if c.xkb != nil {
C.xkb_context_unref(c.xkb)
}
if c.cursorSurf != nil {
C.wl_surface_destroy(c.cursorSurf)
}
if c.cursorTheme != nil {
C.wl_cursor_theme_destroy(c.cursorTheme)
}
if c.keyboard != nil {
C.wl_keyboard_release(c.keyboard)
}
if c.pointer != nil {
C.wl_pointer_release(c.pointer)
}
if c.touch != nil {
C.wl_touch_release(c.touch)
}
if c.im != nil {
C.zwp_text_input_v3_destroy(c.im)
}
if c.imm != nil {
C.zwp_text_input_manager_v3_destroy(c.imm)
}
if c.seat != nil {
C.wl_seat_release(c.seat)
}
if c.decor != nil {
C.zxdg_decoration_manager_v1_destroy(c.decor)
}
if c.shm != nil {
C.wl_shm_destroy(c.shm)
}
if c.compositor != nil {
C.wl_compositor_destroy(c.compositor)
}
if c.wm != nil {
C.xdg_wm_base_destroy(c.wm)
}
for _, output := range outputMap {
C.wl_output_destroy(output)
}
if c.disp != nil {
C.wl_display_disconnect(c.disp)
}
}
// 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)
}
func convertKeysym(s C.xkb_keysym_t) (rune, bool) {
if '0' <= s && s <= '9' || 'A' <= s && s <= 'Z' {
return rune(s), true
}
if 'a' <= s && s <= 'z' {
return rune(s - 0x20), true
}
var n rune
switch s {
case C.XKB_KEY_Escape:
n = key.NameEscape
case C.XKB_KEY_Left:
n = key.NameLeftArrow
case C.XKB_KEY_Right:
n = key.NameRightArrow
case C.XKB_KEY_Return:
n = key.NameReturn
case C.XKB_KEY_KP_Enter:
n = key.NameEnter
case C.XKB_KEY_Up:
n = key.NameUpArrow
case C.XKB_KEY_Down:
n = key.NameDownArrow
case C.XKB_KEY_Home:
n = key.NameHome
case C.XKB_KEY_End:
n = key.NameEnd
case C.XKB_KEY_BackSpace:
n = key.NameDeleteBackward
case C.XKB_KEY_Delete:
n = key.NameDeleteForward
case C.XKB_KEY_Page_Up:
n = key.NamePageUp
case C.XKB_KEY_Page_Down:
n = key.NamePageDown
default:
return 0, false
}
return n, true
}