Files
gio/app/internal/window/os_x11.go
T
Denis Bernard e834a78ab2 app/internal/window: X11 fix black window on Xephyr
On Xephyr, when fitst entering x11Window.loop(), syscall.Ppoll would
block forever while XPending() would still return a non zero value.
This commit refactors the loop so that XPending() gets called first,
then fallback to waiting in ppoll if there are no pending draw events.

This has the added benefit of reducing the number of calls to ppoll when
receiving a lot of events.

Signed-off-by: Denis Bernard <db047h@gmail.com>
2019-10-29 14:04:20 +01:00

582 lines
14 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
// +build linux,!android,!nox11
package window
/*
#cgo LDFLAGS: -lX11
#include <stdlib.h>
#include <locale.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#define GIO_FIELD_OFFSET(typ, field) const int gio_##typ##_##field##_off = offsetof(typ, field)
GIO_FIELD_OFFSET(XClientMessageEvent, data);
GIO_FIELD_OFFSET(XExposeEvent, count);
GIO_FIELD_OFFSET(XConfigureEvent, width);
GIO_FIELD_OFFSET(XConfigureEvent, height);
GIO_FIELD_OFFSET(XButtonEvent, x);
GIO_FIELD_OFFSET(XButtonEvent, y);
GIO_FIELD_OFFSET(XButtonEvent, state);
GIO_FIELD_OFFSET(XButtonEvent, button);
GIO_FIELD_OFFSET(XButtonEvent, time);
GIO_FIELD_OFFSET(XMotionEvent, x);
GIO_FIELD_OFFSET(XMotionEvent, y);
GIO_FIELD_OFFSET(XMotionEvent, time);
GIO_FIELD_OFFSET(XKeyEvent, state);
void gio_x11_init_ime(Display *dpy, Window win, XIM *xim, XIC *xic) {
// adjust locale temporarily for XOpenIM
char *lc = setlocale(LC_CTYPE, NULL);
setlocale(LC_CTYPE, "");
XSetLocaleModifiers("");
*xim = XOpenIM(dpy, 0, 0, 0);
if (!*xim) {
// fallback to internal input method
XSetLocaleModifiers("@im=none");
*xim = XOpenIM(dpy, 0, 0, 0);
}
// revert locale to prevent any unexpected side effects
setlocale(LC_CTYPE, lc);
*xic = XCreateIC(*xim,
XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
XNClientWindow, win,
XNFocusWindow, win,
NULL);
XSetICFocus(*xic);
}
int gio_x11_connection_number(Display *dpy) {
return ConnectionNumber(dpy);
}
*/
import "C"
import (
"errors"
"fmt"
"image"
"sync"
"time"
"unsafe"
"gioui.org/f32"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
syscall "golang.org/x/sys/unix"
)
type x11Window struct {
w Callbacks
x *C.Display
xw C.Window
evDelWindow C.Atom
stage system.Stage
cfg config
width int
height int
xim C.XIM
xic C.XIC
notify struct {
read, write int
}
dead bool
mu sync.Mutex
animating bool
}
func (w *x11Window) SetAnimating(anim bool) {
w.mu.Lock()
w.animating = anim
w.mu.Unlock()
if anim {
w.wakeup()
}
}
func (w *x11Window) ShowTextInput(show bool) {}
var x11OneByte = make([]byte, 1)
func (w *x11Window) wakeup() {
if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
panic(fmt.Errorf("failed to write to pipe: %v", err))
}
}
func (w *x11Window) display() unsafe.Pointer {
return unsafe.Pointer(w.x)
}
func (w *x11Window) setStage(s system.Stage) {
if s == w.stage {
return
}
w.stage = s
w.w.Event(system.StageEvent{s})
}
func (w *x11Window) loop() {
h := x11EventHandler{w: w, xev: new(xEvent), text: make([]byte, 4)}
xfd := C.gio_x11_connection_number(w.x)
// Poll for events and notifications.
pollfds := []syscall.PollFd{
{Fd: int32(xfd), Events: syscall.POLLIN | syscall.POLLERR},
{Fd: int32(w.notify.read), Events: syscall.POLLIN | syscall.POLLERR},
}
xEvents := &pollfds[0].Revents
// Plenty of room for a backlog of notifications.
buf := make([]byte, 100)
loop:
for !w.dead {
var syn, redraw bool
// Check for pending draw events before checking animation or blocking.
// This fixes an issue on Xephyr where on startup XPending() > 0 but
// Ppoll will still block. This also prevents no-op calls to Ppoll.
if syn = h.handleEvents(); !syn {
w.mu.Lock()
animating := w.animating
w.mu.Unlock()
if animating {
redraw = true
} else {
// Clear poll events.
*xEvents = 0
// Wait for X event or gio notification.
if _, err := syscall.Ppoll(pollfds, nil, nil); err != nil && err != syscall.EINTR {
panic(fmt.Errorf("x11 loop: poll failed: %w", err))
}
switch {
case *xEvents&syscall.POLLIN != 0:
syn = h.handleEvents()
if w.dead {
break loop
}
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
break loop
}
}
}
// Clear notifications.
for {
_, err := syscall.Read(w.notify.read, buf)
if err == syscall.EAGAIN {
break
}
if err != nil {
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
}
redraw = true
}
if redraw || syn {
w.cfg.now = time.Now()
w.w.Event(FrameEvent{
FrameEvent: system.FrameEvent{
Size: image.Point{
X: w.width,
Y: w.height,
},
Config: &w.cfg,
},
Sync: syn,
})
}
}
w.w.Event(system.DestroyEvent{Err: nil})
}
func (w *x11Window) destroy() {
if w.notify.write != 0 {
syscall.Close(w.notify.write)
w.notify.write = 0
}
if w.notify.read != 0 {
syscall.Close(w.notify.read)
w.notify.read = 0
}
C.XDestroyIC(w.xic)
C.XCloseIM(w.xim)
C.XDestroyWindow(w.x, w.xw)
C.XCloseDisplay(w.x)
}
// x11EventHandler wraps static variables for the main event loop.
// Its sole purpose is to prevent heap allocation and reduce clutter
// in x11window.loop.
//
type x11EventHandler struct {
w *x11Window
text []byte
xev *xEvent
status C.Status
keysym C.KeySym
}
// handleEvents returns true if the window needs to be redrawn.
//
func (h *x11EventHandler) handleEvents() bool {
w := h.w
xev := h.xev
redraw := false
for C.XPending(w.x) != 0 {
C.XNextEvent(w.x, (*C.XEvent)(unsafe.Pointer(xev)))
if C.XFilterEvent((*C.XEvent)(unsafe.Pointer(xev)), C.None) == C.True {
continue
}
switch xev.Type {
case C.KeyPress:
lookup:
l := int(C.Xutf8LookupString(w.xic,
(*C.XKeyPressedEvent)(unsafe.Pointer(xev)),
(*C.char)(unsafe.Pointer(&h.text[0])), C.int(len(h.text)),
&h.keysym, &h.status))
switch h.status {
case C.XBufferOverflow:
h.text = make([]byte, l)
goto lookup
case C.XLookupChars:
w.w.Event(key.EditEvent{Text: string(h.text[:l])})
case C.XLookupKeySym:
if r, ok := x11KeySymToRune(h.keysym); ok {
w.w.Event(key.Event{
Name: r,
Modifiers: x11KeyStateToModifiers(xev.GetKeyState()),
})
}
case C.XLookupBoth:
// here we need to choose if we send a key.Event or key.EditEvent
mods := x11KeyStateToModifiers(xev.GetKeyState())
if mods&key.ModCommand != 0 {
r, ok := x11KeySymToRune(h.keysym)
if !ok {
// on AZERTY keyboards, CTRL-1, 2, etc do not have a consistent behavior.
// Since keysim as set by Xutf8LookupString is layout dependent, get its layout independent
// version and use that instead (i.e. send CTRL-1, CTRL-2, etc. instead of CTRL-&, CTRL-é, …)
r, ok = x11KeySymToRune(C.XLookupKeysym((*C.XKeyEvent)(unsafe.Pointer(xev)), 0))
}
if ok {
w.w.Event(key.Event{Name: r, Modifiers: mods})
}
} else if r, ok := x11SpecialKeySymToRune(h.keysym); ok {
w.w.Event(key.Event{Name: r, Modifiers: mods})
} else {
w.w.Event(key.EditEvent{Text: string(h.text[:l])})
}
}
case C.KeyRelease:
case C.ButtonPress, C.ButtonRelease:
ev := pointer.Event{
Type: pointer.Press,
Source: pointer.Mouse,
Position: f32.Point{
X: float32(xev.GetButtonX()),
Y: float32(xev.GetButtonY()),
},
Time: xev.GetButtonTime(),
}
if xev.Type == C.ButtonRelease {
ev.Type = pointer.Release
}
const scrollScale = 10
switch xev.GetButtonButton() {
case C.Button1:
// left click by default
case C.Button4:
// scroll up
ev.Type = pointer.Move
ev.Scroll.Y = -scrollScale
case C.Button5:
// scroll down
ev.Type = pointer.Move
ev.Scroll.Y = +scrollScale
default:
continue
}
w.w.Event(ev)
case C.MotionNotify:
w.w.Event(pointer.Event{
Type: pointer.Move,
Source: pointer.Mouse,
Position: f32.Point{
X: float32(xev.GetMotionX()),
Y: float32(xev.GetMotionY()),
},
Time: xev.GetMotionTime(),
})
case C.Expose: // update
// redraw only on the last expose event
redraw = xev.GetExposeCount() == 0
case C.FocusIn:
w.w.Event(key.FocusEvent{Focus: true})
case C.FocusOut:
w.w.Event(key.FocusEvent{Focus: false})
case C.ConfigureNotify: // window configuration change
w.width = int(xev.GetConfigureWidth())
w.height = int(xev.GetConfigureHeight())
// redraw will be done by a later expose event
case C.ClientMessage: // extensions
switch xev.GetClientDataLong()[0] {
case C.long(w.evDelWindow):
w.dead = true
return false
}
}
}
return redraw
}
func x11KeyStateToModifiers(s C.uint) key.Modifiers {
var m key.Modifiers
if s&C.ControlMask != 0 {
m |= key.ModCommand
}
if s&C.ShiftMask != 0 {
m |= key.ModShift
}
return m
}
func x11KeySymToRune(s C.KeySym) (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
}
return x11SpecialKeySymToRune(s)
}
func x11SpecialKeySymToRune(s C.KeySym) (rune, bool) {
var n rune
switch s {
case C.XK_Escape:
n = key.NameEscape
case C.XK_Left, C.XK_KP_Left:
n = key.NameLeftArrow
case C.XK_Right, C.XK_KP_Right:
n = key.NameRightArrow
case C.XK_Return:
n = key.NameReturn
case C.XK_KP_Enter:
n = key.NameEnter
case C.XK_Up, C.XK_KP_Up:
n = key.NameUpArrow
case C.XK_Down, C.XK_KP_Down:
n = key.NameDownArrow
case C.XK_Home, C.XK_KP_Home:
n = key.NameHome
case C.XK_End, C.XK_KP_End:
n = key.NameEnd
case C.XK_BackSpace:
n = key.NameDeleteBackward
case C.XK_Delete, C.XK_KP_Delete:
n = key.NameDeleteForward
case C.XK_Page_Up, C.XK_KP_Prior:
n = key.NamePageUp
case C.XK_Page_Down, C.XK_KP_Next:
n = key.NamePageDown
default:
return 0, false
}
return n, true
}
const xEventSize = unsafe.Sizeof(C.XEvent{})
// Make sure the Go struct has the same size.
// We can't use C.XEvent directly because it's a union.
var _ = [1]struct{}{}[unsafe.Sizeof(xEvent{})-xEventSize]
type xEvent struct {
Type C.int
Data [xEventSize - unsafe.Sizeof(C.int(0))]byte
}
func (e *xEvent) getInt(off int) C.int {
return *(*C.int)(unsafe.Pointer(uintptr(unsafe.Pointer(e)) + uintptr(off)))
}
func (e *xEvent) getUint(off int) C.uint {
return *(*C.uint)(unsafe.Pointer(uintptr(unsafe.Pointer(e)) + uintptr(off)))
}
func (e *xEvent) getUlong(off int) C.ulong {
return *(*C.ulong)(unsafe.Pointer(uintptr(unsafe.Pointer(e)) + uintptr(off)))
}
func (e *xEvent) getUlongMs(off int) time.Duration {
return time.Duration(e.getUlong(off)) * time.Millisecond
}
// GetExposeCount returns a XEvent.xexpose.count field.
func (e *xEvent) GetExposeCount() C.int {
return e.getInt(int(C.gio_XExposeEvent_count_off))
}
// GetConfigureWidth returns a XEvent.xconfigure.width field.
func (e *xEvent) GetConfigureWidth() C.int {
return e.getInt(int(C.gio_XConfigureEvent_width_off))
}
// GetConfigureWidth returns a XEvent.xconfigure.height field.
func (e *xEvent) GetConfigureHeight() C.int {
return e.getInt(int(C.gio_XConfigureEvent_height_off))
}
// GetButtonX returns a XEvent.xbutton.x field.
func (e *xEvent) GetButtonX() C.int {
return e.getInt(int(C.gio_XButtonEvent_x_off))
}
// GetButtonY returns a XEvent.xbutton.y field.
func (e *xEvent) GetButtonY() C.int {
return e.getInt(int(C.gio_XButtonEvent_y_off))
}
// GetButtonState returns a XEvent.xbutton.state field.
func (e *xEvent) GetButtonState() C.uint {
return e.getUint(int(C.gio_XButtonEvent_state_off))
}
// GetButtonButton returns a XEvent.xbutton.button field.
func (e *xEvent) GetButtonButton() C.uint {
return e.getUint(int(C.gio_XButtonEvent_button_off))
}
// GetButtonTime returns a XEvent.xbutton.time field.
func (e *xEvent) GetButtonTime() time.Duration {
return e.getUlongMs(int(C.gio_XButtonEvent_time_off))
}
// GetMotionX returns a XEvent.xmotion.x field.
func (e *xEvent) GetMotionX() C.int {
return e.getInt(int(C.gio_XMotionEvent_x_off))
}
// GetMotionY returns a XEvent.xmotion.y field.
func (e *xEvent) GetMotionY() C.int {
return e.getInt(int(C.gio_XMotionEvent_y_off))
}
// GetMotionTime returns a XEvent.xmotion.time field.
func (e *xEvent) GetMotionTime() time.Duration {
return e.getUlongMs(int(C.gio_XMotionEvent_time_off))
}
// GetClientDataLong returns a XEvent.xclient.data.l field.
func (e *xEvent) GetClientDataLong() [5]C.long {
ptr := (*[5]C.long)(unsafe.Pointer(uintptr(unsafe.Pointer(e)) + uintptr(C.gio_XClientMessageEvent_data_off)))
return *ptr
}
// GetKeyState returns a XKeyEvent.state field.
func (e *xEvent) GetKeyState() C.uint {
return e.getUint(int(C.gio_XKeyEvent_state_off))
}
var (
x11Threads sync.Once
)
func init() {
x11Driver = newX11Window
}
func newX11Window(gioWin Callbacks, opts *Options) error {
var err error
pipe := make([]int, 2)
if err := syscall.Pipe2(pipe, syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil {
return fmt.Errorf("NewX11Window: failed to create pipe: %w", err)
}
x11Threads.Do(func() {
if C.XInitThreads() == 0 {
err = errors.New("x11: threads init failed")
}
})
if err != nil {
return err
}
dpy := C.XOpenDisplay(nil)
if dpy == nil {
return errors.New("x11: cannot connect to the X server")
}
root := C.XDefaultRootWindow(dpy)
var (
swa C.XSetWindowAttributes
xim C.XIM
xic C.XIC
)
cfg := config{pxPerDp: 1, pxPerSp: 1} // TODO(dennwc): real config
swa.event_mask = C.ExposureMask | C.PointerMotionMask | C.KeyPressMask
win := C.XCreateWindow(dpy, root,
0, 0, C.uint(cfg.Px(opts.Width)), C.uint(cfg.Px(opts.Height)), 0,
C.CopyFromParent, C.InputOutput,
nil, C.CWEventMask|C.CWBackPixel,
&swa)
C.gio_x11_init_ime(dpy, win, &xim, &xic)
C.XSelectInput(dpy, win, 0|
C.ExposureMask|C.FocusChangeMask| // update
C.KeyPressMask|C.KeyReleaseMask| // keyboard
C.ButtonPressMask|C.ButtonReleaseMask| // mouse clicks
C.PointerMotionMask| // mouse movement
C.StructureNotifyMask, // resize
)
w := &x11Window{
w: gioWin, x: dpy, xw: win,
width: cfg.Px(opts.Width),
height: cfg.Px(opts.Height),
cfg: cfg,
xim: xim,
xic: xic,
}
w.notify.read = pipe[0]
w.notify.write = pipe[1]
var xattr C.XSetWindowAttributes
xattr.override_redirect = C.False
C.XChangeWindowAttributes(dpy, win, C.CWOverrideRedirect, &xattr)
var hints C.XWMHints
hints.input = C.True
hints.flags = C.InputHint
C.XSetWMHints(dpy, win, &hints)
// make the window visible on the screen
C.XMapWindow(dpy, win)
// set the name
ctitle := C.CString(opts.Title)
C.XStoreName(dpy, win, ctitle)
C.free(unsafe.Pointer(ctitle))
// extensions
ckey := C.CString("WM_DELETE_WINDOW")
w.evDelWindow = C.XInternAtom(dpy, ckey, C.False)
C.free(unsafe.Pointer(ckey))
C.XSetWMProtocols(dpy, win, &w.evDelWindow, 1)
go func() {
w.w.SetDriver(w)
w.setStage(system.StageRunning)
w.loop()
w.destroy()
close(mainDone)
}()
return nil
}