forked from joejulian/gio
5ce1e98282
This patch implements a mechanism for customizing window decorations. If a window is configured with app.Decorated(true), then the widget/material.Decorations are applied. On Wayland, the option is automatically set when the server does not provide window decorations. Server side decorations are no longer requested. The Decorated flag is set according to the server's requests. Wayland is now the default driver for UNIX platforms. References: https://todo.sr.ht/~eliasnaur/gio/318 Signed-off-by: Pierre Curto <pierre.curto@gmail.com>
881 lines
23 KiB
Go
881 lines
23 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
//go:build ((linux && !android) || freebsd || openbsd) && !nox11
|
|
// +build linux,!android freebsd openbsd
|
|
// +build !nox11
|
|
|
|
package app
|
|
|
|
/*
|
|
#cgo freebsd openbsd CFLAGS: -I/usr/X11R6/include -I/usr/local/include
|
|
#cgo freebsd openbsd LDFLAGS: -L/usr/X11R6/lib -L/usr/local/lib
|
|
#cgo freebsd openbsd LDFLAGS: -lX11 -lxkbcommon -lxkbcommon-x11 -lX11-xcb -lXcursor -lXfixes
|
|
#cgo linux pkg-config: x11 xkbcommon xkbcommon-x11 x11-xcb xcursor xfixes
|
|
|
|
#include <stdlib.h>
|
|
#include <locale.h>
|
|
#include <X11/Xlib.h>
|
|
#include <X11/Xatom.h>
|
|
#include <X11/Xutil.h>
|
|
#include <X11/Xresource.h>
|
|
#include <X11/XKBlib.h>
|
|
#include <X11/Xlib-xcb.h>
|
|
#include <X11/extensions/Xfixes.h>
|
|
#include <X11/Xcursor/Xcursor.h>
|
|
#include <xkbcommon/xkbcommon-x11.h>
|
|
|
|
*/
|
|
import "C"
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/io/clipboard"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/io/system"
|
|
"gioui.org/unit"
|
|
|
|
syscall "golang.org/x/sys/unix"
|
|
|
|
"gioui.org/app/internal/xkb"
|
|
)
|
|
|
|
const (
|
|
_NET_WM_STATE_REMOVE = 0
|
|
_NET_WM_STATE_ADD = 1
|
|
)
|
|
|
|
type x11Window struct {
|
|
w *callbacks
|
|
x *C.Display
|
|
xkb *xkb.Context
|
|
xkbEventBase C.int
|
|
xw C.Window
|
|
|
|
atoms struct {
|
|
// "UTF8_STRING".
|
|
utf8string C.Atom
|
|
// "text/plain;charset=utf-8".
|
|
plaintext C.Atom
|
|
// "TARGETS"
|
|
targets C.Atom
|
|
// "CLIPBOARD".
|
|
clipboard C.Atom
|
|
// "PRIMARY".
|
|
primary C.Atom
|
|
// "CLIPBOARD_CONTENT", the clipboard destination property.
|
|
clipboardContent C.Atom
|
|
// "WM_DELETE_WINDOW"
|
|
evDelWindow C.Atom
|
|
// "ATOM"
|
|
atom C.Atom
|
|
// "GTK_TEXT_BUFFER_CONTENTS"
|
|
gtk_text_buffer_contents C.Atom
|
|
// "_NET_WM_NAME"
|
|
wmName C.Atom
|
|
// "_NET_WM_STATE"
|
|
wmState C.Atom
|
|
// "_NET_WM_STATE_FULLSCREEN"
|
|
wmStateFullscreen C.Atom
|
|
// "_NET_ACTIVE_WINDOW"
|
|
wmActiveWindow C.Atom
|
|
// _NET_WM_STATE_MAXIMIZED_HORZ
|
|
wmStateMaximizedHorz C.Atom
|
|
// _NET_WM_STATE_MAXIMIZED_VERT
|
|
wmStateMaximizedVert C.Atom
|
|
}
|
|
stage system.Stage
|
|
metric unit.Metric
|
|
notify struct {
|
|
read, write int
|
|
}
|
|
dead bool
|
|
|
|
animating bool
|
|
|
|
pointerBtns pointer.Buttons
|
|
|
|
clipboard struct {
|
|
content []byte
|
|
}
|
|
cursor pointer.CursorName
|
|
config Config
|
|
|
|
wakeups chan struct{}
|
|
}
|
|
|
|
var (
|
|
newX11EGLContext func(w *x11Window) (context, error)
|
|
newX11VulkanContext func(w *x11Window) (context, error)
|
|
)
|
|
|
|
// X11 and Vulkan doesn't work reliably on NVIDIA systems.
|
|
// See https://gioui.org/issue/347.
|
|
const vulkanBuggy = true
|
|
|
|
func (w *x11Window) NewContext() (context, error) {
|
|
var firstErr error
|
|
if f := newX11VulkanContext; f != nil && !vulkanBuggy {
|
|
c, err := f(w)
|
|
if err == nil {
|
|
return c, nil
|
|
}
|
|
firstErr = err
|
|
}
|
|
if f := newX11EGLContext; f != nil {
|
|
c, err := f(w)
|
|
if err == nil {
|
|
return c, nil
|
|
}
|
|
firstErr = err
|
|
}
|
|
if firstErr != nil {
|
|
return nil, firstErr
|
|
}
|
|
return nil, errors.New("x11: no available GPU backends")
|
|
}
|
|
|
|
func (w *x11Window) SetAnimating(anim bool) {
|
|
w.animating = anim
|
|
}
|
|
|
|
func (w *x11Window) ReadClipboard() {
|
|
C.XDeleteProperty(w.x, w.xw, w.atoms.clipboardContent)
|
|
C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime)
|
|
}
|
|
|
|
func (w *x11Window) WriteClipboard(s string) {
|
|
w.clipboard.content = []byte(s)
|
|
C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime)
|
|
C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime)
|
|
}
|
|
|
|
func (w *x11Window) Configure(options []Option) {
|
|
var shints C.XSizeHints
|
|
prev := w.config
|
|
cnf := w.config
|
|
cnf.apply(w.metric, options)
|
|
// Decorations are never disabled.
|
|
cnf.Decorated = true
|
|
|
|
switch cnf.Mode {
|
|
case Fullscreen:
|
|
switch prev.Mode {
|
|
case Fullscreen:
|
|
case Minimized:
|
|
w.Raise()
|
|
fallthrough
|
|
default:
|
|
w.config.Mode = Fullscreen
|
|
w.sendWMStateEvent(_NET_WM_STATE_ADD, w.atoms.wmStateFullscreen, 0)
|
|
}
|
|
case Minimized:
|
|
switch prev.Mode {
|
|
case Minimized, Fullscreen:
|
|
default:
|
|
w.config.Mode = Minimized
|
|
screen := C.XDefaultScreen(w.x)
|
|
C.XIconifyWindow(w.x, w.xw, screen)
|
|
}
|
|
case Maximized:
|
|
switch prev.Mode {
|
|
case Fullscreen:
|
|
case Minimized:
|
|
w.Raise()
|
|
fallthrough
|
|
default:
|
|
w.config.Mode = Maximized
|
|
w.sendWMStateEvent(_NET_WM_STATE_ADD, w.atoms.wmStateMaximizedHorz, w.atoms.wmStateMaximizedVert)
|
|
w.setTitle(prev, cnf)
|
|
}
|
|
case Windowed:
|
|
switch prev.Mode {
|
|
case Fullscreen:
|
|
w.config.Mode = Windowed
|
|
w.sendWMStateEvent(_NET_WM_STATE_REMOVE, w.atoms.wmStateFullscreen, 0)
|
|
C.XResizeWindow(w.x, w.xw, C.uint(cnf.Size.X), C.uint(cnf.Size.Y))
|
|
case Minimized:
|
|
w.config.Mode = Windowed
|
|
w.Raise()
|
|
case Maximized:
|
|
w.config.Mode = Windowed
|
|
w.sendWMStateEvent(_NET_WM_STATE_REMOVE, w.atoms.wmStateMaximizedHorz, w.atoms.wmStateMaximizedVert)
|
|
}
|
|
w.setTitle(prev, cnf)
|
|
if prev.Size != cnf.Size {
|
|
w.config.Size = cnf.Size
|
|
C.XResizeWindow(w.x, w.xw, C.uint(cnf.Size.X), C.uint(cnf.Size.Y))
|
|
}
|
|
if prev.MinSize != cnf.MinSize {
|
|
w.config.MinSize = cnf.MinSize
|
|
shints.min_width = C.int(cnf.MinSize.X)
|
|
shints.min_height = C.int(cnf.MinSize.Y)
|
|
shints.flags = C.PMinSize
|
|
}
|
|
if prev.MaxSize != cnf.MaxSize {
|
|
w.config.MaxSize = cnf.MaxSize
|
|
shints.max_width = C.int(cnf.MaxSize.X)
|
|
shints.max_height = C.int(cnf.MaxSize.Y)
|
|
shints.flags = shints.flags | C.PMaxSize
|
|
}
|
|
if shints.flags != 0 {
|
|
C.XSetWMNormalHints(w.x, w.xw, &shints)
|
|
}
|
|
if cnf.center {
|
|
screen := C.XDefaultScreen(w.x)
|
|
width := C.XDisplayWidth(w.x, screen)
|
|
height := C.XDisplayHeight(w.x, screen)
|
|
|
|
var attrs C.XWindowAttributes
|
|
C.XGetWindowAttributes(w.x, w.xw, &attrs)
|
|
width -= attrs.border_width
|
|
height -= attrs.border_width
|
|
|
|
sz := w.config.Size
|
|
x := (int(width) - sz.X) / 2
|
|
y := (int(height) - sz.Y) / 2
|
|
|
|
C.XMoveResizeWindow(w.x, w.xw, C.int(x), C.int(y), C.uint(sz.X), C.uint(sz.Y))
|
|
}
|
|
}
|
|
if cnf.Decorated != prev.Decorated {
|
|
w.config.Decorated = cnf.Decorated
|
|
}
|
|
if w.config != prev {
|
|
w.w.Event(ConfigEvent{Config: w.config})
|
|
}
|
|
}
|
|
|
|
func (w *x11Window) setTitle(prev, cnf Config) {
|
|
if prev.Title != cnf.Title {
|
|
title := cnf.Title
|
|
ctitle := C.CString(title)
|
|
defer C.free(unsafe.Pointer(ctitle))
|
|
C.XStoreName(w.x, w.xw, ctitle)
|
|
// set _NET_WM_NAME as well for UTF-8 support in window title.
|
|
C.XSetTextProperty(w.x, w.xw,
|
|
&C.XTextProperty{
|
|
value: (*C.uchar)(unsafe.Pointer(ctitle)),
|
|
encoding: w.atoms.utf8string,
|
|
format: 8,
|
|
nitems: C.ulong(len(title)),
|
|
},
|
|
w.atoms.wmName)
|
|
}
|
|
}
|
|
|
|
func (w *x11Window) Perform(system.Action) {}
|
|
|
|
func (w *x11Window) Raise() {
|
|
var xev C.XEvent
|
|
ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev))
|
|
*ev = C.XClientMessageEvent{
|
|
_type: C.ClientMessage,
|
|
display: w.x,
|
|
window: w.xw,
|
|
message_type: w.atoms.wmActiveWindow,
|
|
format: 32,
|
|
}
|
|
C.XSendEvent(
|
|
w.x,
|
|
C.XDefaultRootWindow(w.x), // MUST be the root window
|
|
C.False,
|
|
C.SubstructureNotifyMask|C.SubstructureRedirectMask,
|
|
&xev,
|
|
)
|
|
C.XMapRaised(w.display(), w.xw)
|
|
}
|
|
|
|
func (w *x11Window) SetCursor(name pointer.CursorName) {
|
|
switch name {
|
|
case pointer.CursorNone:
|
|
w.cursor = name
|
|
C.XFixesHideCursor(w.x, w.xw)
|
|
return
|
|
case pointer.CursorGrab:
|
|
name = "hand1"
|
|
}
|
|
if w.cursor == pointer.CursorNone {
|
|
C.XFixesShowCursor(w.x, w.xw)
|
|
}
|
|
cname := C.CString(string(name))
|
|
defer C.free(unsafe.Pointer(cname))
|
|
c := C.XcursorLibraryLoadCursor(w.x, cname)
|
|
if c == 0 {
|
|
name = pointer.CursorDefault
|
|
}
|
|
w.cursor = name
|
|
// If c if null (i.e. name was not found),
|
|
// XDefineCursor will use the default cursor.
|
|
C.XDefineCursor(w.x, w.xw, c)
|
|
}
|
|
|
|
func (w *x11Window) ShowTextInput(show bool) {}
|
|
|
|
func (w *x11Window) SetInputHint(_ key.InputHint) {}
|
|
|
|
// Close the window.
|
|
func (w *x11Window) Close() {
|
|
var xev C.XEvent
|
|
ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev))
|
|
*ev = C.XClientMessageEvent{
|
|
_type: C.ClientMessage,
|
|
display: w.x,
|
|
window: w.xw,
|
|
message_type: w.atom("WM_PROTOCOLS", true),
|
|
format: 32,
|
|
}
|
|
arr := (*[5]C.long)(unsafe.Pointer(&ev.data))
|
|
arr[0] = C.long(w.atoms.evDelWindow)
|
|
arr[1] = C.CurrentTime
|
|
C.XSendEvent(w.x, w.xw, C.False, C.NoEventMask, &xev)
|
|
}
|
|
|
|
// action is one of _NET_WM_STATE_REMOVE, _NET_WM_STATE_ADD.
|
|
func (w *x11Window) sendWMStateEvent(action C.long, atom1, atom2 C.ulong) {
|
|
var xev C.XEvent
|
|
ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev))
|
|
*ev = C.XClientMessageEvent{
|
|
_type: C.ClientMessage,
|
|
display: w.x,
|
|
window: w.xw,
|
|
message_type: w.atoms.wmState,
|
|
format: 32,
|
|
}
|
|
data := (*[5]C.long)(unsafe.Pointer(&ev.data))
|
|
data[0] = C.long(action)
|
|
data[1] = C.long(atom1)
|
|
data[2] = C.long(atom2)
|
|
data[3] = 1 // application
|
|
|
|
C.XSendEvent(
|
|
w.x,
|
|
C.XDefaultRootWindow(w.x), // MUST be the root window
|
|
C.False,
|
|
C.SubstructureNotifyMask|C.SubstructureRedirectMask,
|
|
&xev,
|
|
)
|
|
}
|
|
|
|
var x11OneByte = make([]byte, 1)
|
|
|
|
func (w *x11Window) Wakeup() {
|
|
select {
|
|
case w.wakeups <- struct{}{}:
|
|
default:
|
|
}
|
|
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() *C.Display {
|
|
return w.x
|
|
}
|
|
|
|
func (w *x11Window) window() (C.Window, int, int) {
|
|
return w.xw, w.config.Size.X, w.config.Size.Y
|
|
}
|
|
|
|
func (w *x11Window) setStage(s system.Stage) {
|
|
if s == w.stage {
|
|
return
|
|
}
|
|
w.stage = s
|
|
w.w.Event(system.StageEvent{Stage: s})
|
|
}
|
|
|
|
func (w *x11Window) loop() {
|
|
h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
|
|
xfd := C.XConnectionNumber(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, anim bool
|
|
// Check for pending draw events before checking animation or blocking.
|
|
// This fixes an issue on Xephyr where on startup XPending() > 0 but
|
|
// poll will still block. This also prevents no-op calls to poll.
|
|
if syn = h.handleEvents(); !syn {
|
|
anim = w.animating
|
|
if !anim {
|
|
// Clear poll events.
|
|
*xEvents = 0
|
|
// Wait for X event or gio notification.
|
|
if _, err := syscall.Poll(pollfds, -1); 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))
|
|
}
|
|
}
|
|
select {
|
|
case <-w.wakeups:
|
|
w.w.Event(wakeupEvent{})
|
|
default:
|
|
}
|
|
|
|
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
|
|
w.w.Event(frameEvent{
|
|
FrameEvent: system.FrameEvent{
|
|
Now: time.Now(),
|
|
Size: w.config.Size,
|
|
Metric: w.metric,
|
|
},
|
|
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
|
|
}
|
|
if w.xkb != nil {
|
|
w.xkb.Destroy()
|
|
w.xkb = nil
|
|
}
|
|
C.XDestroyWindow(w.x, w.xw)
|
|
C.XCloseDisplay(w.x)
|
|
}
|
|
|
|
// atom is a wrapper around XInternAtom. Callers should cache the result
|
|
// in order to limit round-trips to the X server.
|
|
//
|
|
func (w *x11Window) atom(name string, onlyIfExists bool) C.Atom {
|
|
cname := C.CString(name)
|
|
defer C.free(unsafe.Pointer(cname))
|
|
flag := C.Bool(C.False)
|
|
if onlyIfExists {
|
|
flag = C.True
|
|
}
|
|
return C.XInternAtom(w.x, cname, flag)
|
|
}
|
|
|
|
// 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 *C.XEvent
|
|
}
|
|
|
|
// 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, xev)
|
|
if C.XFilterEvent(xev, C.None) == C.True {
|
|
continue
|
|
}
|
|
switch _type := (*C.XAnyEvent)(unsafe.Pointer(xev))._type; _type {
|
|
case h.w.xkbEventBase:
|
|
xkbEvent := (*C.XkbAnyEvent)(unsafe.Pointer(xev))
|
|
switch xkbEvent.xkb_type {
|
|
case C.XkbNewKeyboardNotify, C.XkbMapNotify:
|
|
if err := h.w.updateXkbKeymap(); err != nil {
|
|
panic(err)
|
|
}
|
|
case C.XkbStateNotify:
|
|
state := (*C.XkbStateNotifyEvent)(unsafe.Pointer(xev))
|
|
h.w.xkb.UpdateMask(uint32(state.base_mods), uint32(state.latched_mods), uint32(state.locked_mods),
|
|
uint32(state.base_group), uint32(state.latched_group), uint32(state.locked_group))
|
|
}
|
|
case C.KeyPress, C.KeyRelease:
|
|
ks := key.Press
|
|
if _type == C.KeyRelease {
|
|
ks = key.Release
|
|
}
|
|
kevt := (*C.XKeyPressedEvent)(unsafe.Pointer(xev))
|
|
for _, e := range h.w.xkb.DispatchKey(uint32(kevt.keycode), ks) {
|
|
w.w.Event(e)
|
|
}
|
|
case C.ButtonPress, C.ButtonRelease:
|
|
bevt := (*C.XButtonEvent)(unsafe.Pointer(xev))
|
|
ev := pointer.Event{
|
|
Type: pointer.Press,
|
|
Source: pointer.Mouse,
|
|
Position: f32.Point{
|
|
X: float32(bevt.x),
|
|
Y: float32(bevt.y),
|
|
},
|
|
Time: time.Duration(bevt.time) * time.Millisecond,
|
|
Modifiers: w.xkb.Modifiers(),
|
|
}
|
|
if bevt._type == C.ButtonRelease {
|
|
ev.Type = pointer.Release
|
|
}
|
|
var btn pointer.Buttons
|
|
const scrollScale = 10
|
|
switch bevt.button {
|
|
case C.Button1:
|
|
btn = pointer.ButtonPrimary
|
|
case C.Button2:
|
|
btn = pointer.ButtonTertiary
|
|
case C.Button3:
|
|
btn = pointer.ButtonSecondary
|
|
case C.Button4:
|
|
// scroll up
|
|
ev.Type = pointer.Scroll
|
|
ev.Scroll.Y = -scrollScale
|
|
case C.Button5:
|
|
// scroll down
|
|
ev.Type = pointer.Scroll
|
|
ev.Scroll.Y = +scrollScale
|
|
case 6:
|
|
// http://xahlee.info/linux/linux_x11_mouse_button_number.html
|
|
// scroll left
|
|
ev.Type = pointer.Scroll
|
|
ev.Scroll.X = -scrollScale * 2
|
|
case 7:
|
|
// scroll right
|
|
ev.Type = pointer.Scroll
|
|
ev.Scroll.X = +scrollScale * 2
|
|
default:
|
|
continue
|
|
}
|
|
switch _type {
|
|
case C.ButtonPress:
|
|
w.pointerBtns |= btn
|
|
case C.ButtonRelease:
|
|
w.pointerBtns &^= btn
|
|
}
|
|
ev.Buttons = w.pointerBtns
|
|
w.w.Event(ev)
|
|
case C.MotionNotify:
|
|
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
|
|
w.w.Event(pointer.Event{
|
|
Type: pointer.Move,
|
|
Source: pointer.Mouse,
|
|
Buttons: w.pointerBtns,
|
|
Position: f32.Point{
|
|
X: float32(mevt.x),
|
|
Y: float32(mevt.y),
|
|
},
|
|
Time: time.Duration(mevt.time) * time.Millisecond,
|
|
Modifiers: w.xkb.Modifiers(),
|
|
})
|
|
case C.Expose: // update
|
|
// redraw only on the last expose event
|
|
redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 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
|
|
cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev))
|
|
if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size {
|
|
w.config.Size = sz
|
|
w.w.Event(ConfigEvent{Config: w.config})
|
|
}
|
|
// redraw will be done by a later expose event
|
|
case C.SelectionNotify:
|
|
cevt := (*C.XSelectionEvent)(unsafe.Pointer(xev))
|
|
prop := w.atoms.clipboardContent
|
|
if cevt.property != prop {
|
|
break
|
|
}
|
|
if cevt.selection != w.atoms.clipboard {
|
|
break
|
|
}
|
|
var text C.XTextProperty
|
|
if st := C.XGetTextProperty(w.x, w.xw, &text, prop); st == 0 {
|
|
// Failed; ignore.
|
|
break
|
|
}
|
|
if text.format != 8 || text.encoding != w.atoms.utf8string {
|
|
// Ignore non-utf-8 encoded strings.
|
|
break
|
|
}
|
|
str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
|
|
w.w.Event(clipboard.Event{Text: str})
|
|
case C.SelectionRequest:
|
|
cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
|
|
if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None {
|
|
// Unsupported clipboard or obsolete requestor.
|
|
break
|
|
}
|
|
notify := func() {
|
|
var xev C.XEvent
|
|
ev := (*C.XSelectionEvent)(unsafe.Pointer(&xev))
|
|
*ev = C.XSelectionEvent{
|
|
_type: C.SelectionNotify,
|
|
display: cevt.display,
|
|
requestor: cevt.requestor,
|
|
selection: cevt.selection,
|
|
target: cevt.target,
|
|
property: cevt.property,
|
|
time: cevt.time,
|
|
}
|
|
C.XSendEvent(w.x, cevt.requestor, 0, 0, &xev)
|
|
}
|
|
switch cevt.target {
|
|
case w.atoms.targets:
|
|
// The requestor wants the supported clipboard
|
|
// formats. First write the targets...
|
|
formats := [...]C.long{
|
|
C.long(w.atoms.targets),
|
|
C.long(w.atoms.utf8string),
|
|
C.long(w.atoms.plaintext),
|
|
// GTK clients need this.
|
|
C.long(w.atoms.gtk_text_buffer_contents),
|
|
}
|
|
C.XChangeProperty(w.x, cevt.requestor, cevt.property, w.atoms.atom,
|
|
32 /* bitwidth of formats */, C.PropModeReplace,
|
|
(*C.uchar)(unsafe.Pointer(&formats)), C.int(len(formats)),
|
|
)
|
|
// ...then notify the requestor.
|
|
notify()
|
|
case w.atoms.plaintext, w.atoms.utf8string, w.atoms.gtk_text_buffer_contents:
|
|
content := w.clipboard.content
|
|
var ptr *C.uchar
|
|
if len(content) > 0 {
|
|
ptr = (*C.uchar)(unsafe.Pointer(&content[0]))
|
|
}
|
|
C.XChangeProperty(w.x, cevt.requestor, cevt.property, cevt.target,
|
|
8 /* bitwidth */, C.PropModeReplace,
|
|
ptr, C.int(len(content)),
|
|
)
|
|
notify()
|
|
}
|
|
case C.ClientMessage: // extensions
|
|
cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev))
|
|
switch *(*C.long)(unsafe.Pointer(&cevt.data)) {
|
|
case C.long(w.atoms.evDelWindow):
|
|
w.dead = true
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return redraw
|
|
}
|
|
|
|
var (
|
|
x11Threads sync.Once
|
|
)
|
|
|
|
func init() {
|
|
x11Driver = newX11Window
|
|
}
|
|
|
|
func newX11Window(gioWin *callbacks, options []Option) 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")
|
|
}
|
|
C.XrmInitialize()
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dpy := C.XOpenDisplay(nil)
|
|
if dpy == nil {
|
|
return errors.New("x11: cannot connect to the X server")
|
|
}
|
|
var major, minor C.int = C.XkbMajorVersion, C.XkbMinorVersion
|
|
var xkbEventBase C.int
|
|
if C.XkbQueryExtension(dpy, nil, &xkbEventBase, nil, &major, &minor) != C.True {
|
|
C.XCloseDisplay(dpy)
|
|
return errors.New("x11: XkbQueryExtension failed")
|
|
}
|
|
const bits = C.uint(C.XkbNewKeyboardNotifyMask | C.XkbMapNotifyMask | C.XkbStateNotifyMask)
|
|
if C.XkbSelectEvents(dpy, C.XkbUseCoreKbd, bits, bits) != C.True {
|
|
C.XCloseDisplay(dpy)
|
|
return errors.New("x11: XkbSelectEvents failed")
|
|
}
|
|
xkb, err := xkb.New()
|
|
if err != nil {
|
|
C.XCloseDisplay(dpy)
|
|
return fmt.Errorf("x11: %v", err)
|
|
}
|
|
|
|
ppsp := x11DetectUIScale(dpy)
|
|
cfg := unit.Metric{PxPerDp: ppsp, PxPerSp: ppsp}
|
|
// Only use cnf for getting the window size.
|
|
var cnf Config
|
|
cnf.apply(cfg, options)
|
|
|
|
swa := C.XSetWindowAttributes{
|
|
event_mask: C.ExposureMask | C.FocusChangeMask | // update
|
|
C.KeyPressMask | C.KeyReleaseMask | // keyboard
|
|
C.ButtonPressMask | C.ButtonReleaseMask | // mouse clicks
|
|
C.PointerMotionMask | // mouse movement
|
|
C.StructureNotifyMask, // resize
|
|
background_pixmap: C.None,
|
|
override_redirect: C.False,
|
|
}
|
|
win := C.XCreateWindow(dpy, C.XDefaultRootWindow(dpy),
|
|
0, 0, C.uint(cnf.Size.X), C.uint(cnf.Size.Y),
|
|
0, C.CopyFromParent, C.InputOutput, nil,
|
|
C.CWEventMask|C.CWBackPixmap|C.CWOverrideRedirect, &swa)
|
|
|
|
w := &x11Window{
|
|
w: gioWin, x: dpy, xw: win,
|
|
metric: cfg,
|
|
xkb: xkb,
|
|
xkbEventBase: xkbEventBase,
|
|
wakeups: make(chan struct{}, 1),
|
|
config: Config{Size: cnf.Size},
|
|
}
|
|
w.notify.read = pipe[0]
|
|
w.notify.write = pipe[1]
|
|
|
|
if err := w.updateXkbKeymap(); err != nil {
|
|
w.destroy()
|
|
return err
|
|
}
|
|
|
|
var hints C.XWMHints
|
|
hints.input = C.True
|
|
hints.flags = C.InputHint
|
|
C.XSetWMHints(dpy, win, &hints)
|
|
|
|
name := C.CString(filepath.Base(os.Args[0]))
|
|
defer C.free(unsafe.Pointer(name))
|
|
wmhints := C.XClassHint{name, name}
|
|
C.XSetClassHint(dpy, win, &wmhints)
|
|
|
|
w.atoms.utf8string = w.atom("UTF8_STRING", false)
|
|
w.atoms.plaintext = w.atom("text/plain;charset=utf-8", false)
|
|
w.atoms.gtk_text_buffer_contents = w.atom("GTK_TEXT_BUFFER_CONTENTS", false)
|
|
w.atoms.evDelWindow = w.atom("WM_DELETE_WINDOW", false)
|
|
w.atoms.clipboard = w.atom("CLIPBOARD", false)
|
|
w.atoms.primary = w.atom("PRIMARY", false)
|
|
w.atoms.clipboardContent = w.atom("CLIPBOARD_CONTENT", false)
|
|
w.atoms.atom = w.atom("ATOM", false)
|
|
w.atoms.targets = w.atom("TARGETS", false)
|
|
w.atoms.wmName = w.atom("_NET_WM_NAME", false)
|
|
w.atoms.wmState = w.atom("_NET_WM_STATE", false)
|
|
w.atoms.wmStateFullscreen = w.atom("_NET_WM_STATE_FULLSCREEN", false)
|
|
w.atoms.wmActiveWindow = w.atom("_NET_ACTIVE_WINDOW", false)
|
|
w.atoms.wmStateMaximizedHorz = w.atom("_NET_WM_STATE_MAXIMIZED_HORZ", false)
|
|
w.atoms.wmStateMaximizedVert = w.atom("_NET_WM_STATE_MAXIMIZED_VERT", false)
|
|
|
|
// extensions
|
|
C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1)
|
|
|
|
go func() {
|
|
w.w.SetDriver(w)
|
|
|
|
// make the window visible on the screen
|
|
C.XMapWindow(dpy, win)
|
|
w.Configure(options)
|
|
w.w.Event(ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
|
|
w.setStage(system.StageRunning)
|
|
w.loop()
|
|
w.w.Event(ViewEvent{})
|
|
w.destroy()
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// detectUIScale reports the system UI scale, or 1.0 if it fails.
|
|
func x11DetectUIScale(dpy *C.Display) float32 {
|
|
// default fixed DPI value used in most desktop UI toolkits
|
|
const defaultDesktopDPI = 96
|
|
var scale float32 = 1.0
|
|
|
|
// Get actual DPI from X resource Xft.dpi (set by GTK and Qt).
|
|
// This value is entirely based on user preferences and conflates both
|
|
// screen (UI) scaling and font scale.
|
|
rms := C.XResourceManagerString(dpy)
|
|
if rms != nil {
|
|
db := C.XrmGetStringDatabase(rms)
|
|
if db != nil {
|
|
var (
|
|
t *C.char
|
|
v C.XrmValue
|
|
)
|
|
if C.XrmGetResource(db, (*C.char)(unsafe.Pointer(&[]byte("Xft.dpi\x00")[0])),
|
|
(*C.char)(unsafe.Pointer(&[]byte("Xft.Dpi\x00")[0])), &t, &v) != C.False {
|
|
if t != nil && C.GoString(t) == "String" {
|
|
f, err := strconv.ParseFloat(C.GoString(v.addr), 32)
|
|
if err == nil {
|
|
scale = float32(f) / defaultDesktopDPI
|
|
}
|
|
}
|
|
}
|
|
C.XrmDestroyDatabase(db)
|
|
}
|
|
}
|
|
|
|
return scale
|
|
}
|
|
|
|
func (w *x11Window) updateXkbKeymap() error {
|
|
w.xkb.DestroyKeymapState()
|
|
ctx := (*C.struct_xkb_context)(unsafe.Pointer(w.xkb.Ctx))
|
|
xcb := C.XGetXCBConnection(w.x)
|
|
if xcb == nil {
|
|
return errors.New("x11: XGetXCBConnection failed")
|
|
}
|
|
xkbDevID := C.xkb_x11_get_core_keyboard_device_id(xcb)
|
|
if xkbDevID == -1 {
|
|
return errors.New("x11: xkb_x11_get_core_keyboard_device_id failed")
|
|
}
|
|
keymap := C.xkb_x11_keymap_new_from_device(ctx, xcb, xkbDevID, C.XKB_KEYMAP_COMPILE_NO_FLAGS)
|
|
if keymap == nil {
|
|
return errors.New("x11: xkb_x11_keymap_new_from_device failed")
|
|
}
|
|
state := C.xkb_x11_state_new_from_device(keymap, xcb, xkbDevID)
|
|
if state == nil {
|
|
C.xkb_keymap_unref(keymap)
|
|
return errors.New("x11: xkb_x11_keymap_new_from_device failed")
|
|
}
|
|
w.xkb.SetKeymap(unsafe.Pointer(keymap), unsafe.Pointer(state))
|
|
return nil
|
|
}
|