Files
gio/ui/app/os_wayland.go
T
Elias Naur 32aae18293 ui,ui/app: convert Config to an interface
To keep the interface slim, remove the helper methods and shorten
the essential method, Pixels, to Px.

Add and use unexported Config implementation in the app package.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2019-07-10 17:20:55 +02:00

1267 lines
33 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/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 struct {
x, y int
}
scroll f32.Point
lastPos f32.Point
lastTouch f32.Point
stage Stage
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(opts *WindowOptions) error {
connMu.Lock()
defer connMu.Unlock()
if len(winMap) > 0 {
panic("multiple windows are not supported")
}
if err := waylandConnect(); err != nil {
return err
}
w, err := createNativeWindow(opts)
if err != nil {
conn.destroy()
return err
}
go func() {
windows <- w.w
w.setStage(StageRunning)
w.loop()
w.destroy()
conn.destroy()
close(windows)
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 * monitorScale
if ppdp < minDensity {
ppdp = minDensity
}
w := &window{
disp: conn.disp,
scale: scale,
newScale: scale != 1,
ppdp: ppdp,
ppsp: ppdp * fontScale,
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)
ow := newWindow(w)
w.w = ow
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.draw(true)
}
//export gio_onToplevelClose
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
w := winMap[topLvl]
w.setStage(StageDead)
}
//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), Y: fromFixed(y)}
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), Y: fromFixed(y)}
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.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.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)
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()
}
//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, pointer *C.struct_wl_pointer, time, axis C.uint32_t) {
}
//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]
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)
}
}
//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]
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.stage == StageDead {
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
w.mu.Unlock()
if anim {
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.ChordEvent{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() {
if w.scroll == (f32.Point{}) {
return
}
// 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
}
w.w.event(pointer.Event{
Type: pointer.Move,
Source: pointer.Mouse,
Position: w.lastPos,
Scroll: w.scroll,
Time: w.scrollTime,
})
w.scroll = f32.Point{}
w.discScroll.x = 0
w.discScroll.y = 0
}
func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
w.flushScroll()
w.lastPos = f32.Point{X: fromFixed(x), Y: fromFixed(y)}
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) draw(sync bool) {
w.mu.Lock()
animating := w.animating
w.mu.Unlock()
if !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(DrawEvent{
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) setTextInput(s key.TextInputState) {}
// 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)
// Get globals.
C.wl_display_roundtrip(c.disp)
// Get output configurations.
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
}