forked from joejulian/gio
c5d4e01e3d
The _NET_WM_STATE protocol description[0] states that to change the window mode for an X11, "To change the state of a mapped window, a Client MUST send a _NET_WM_STATE client message to the root window." and that the window manager in turn "The Window Manager MUST keep this property updated to reflect the current state of the window." However, our X11 implementation did both: send the message _and_ set or deleted the property. This change makes it so only the message is sent. It also replaces toggling the property by setting or clearing, to ensure our mode and the window manager's mode never gets out of sync. Maybe fixes gio#265 [0] https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm46515148826720 Signed-off-by: Elias Naur <mail@eliasnaur.com>
759 lines
20 KiB
Go
759 lines
20 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 openbsd CFLAGS: -I/usr/X11R6/include -I/usr/local/include
|
|
#cgo 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"
|
|
)
|
|
|
|
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
|
|
// "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
|
|
}
|
|
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{}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func (w *x11Window) Configure(options []Option) {
|
|
var shints C.XSizeHints
|
|
prev := w.config
|
|
cnf := w.config
|
|
cnf.apply(w.metric, options)
|
|
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 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.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)
|
|
}
|
|
|
|
if prev.Mode != cnf.Mode {
|
|
w.SetWindowMode(cnf.Mode)
|
|
}
|
|
}
|
|
|
|
func (w *x11Window) Config() Config {
|
|
return w.config
|
|
}
|
|
|
|
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) SetWindowMode(mode WindowMode) {
|
|
var action C.long
|
|
switch mode {
|
|
case Windowed:
|
|
action = 0 // _NET_WM_STATE_REMOVE
|
|
case Fullscreen:
|
|
action = 1 // _NET_WM_STATE_ADD
|
|
default:
|
|
return
|
|
}
|
|
w.config.Mode = mode
|
|
// "A Client wishing to change the state of a window MUST send
|
|
// a _NET_WM_STATE client message to the root window."
|
|
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,
|
|
}
|
|
arr := (*[5]C.long)(unsafe.Pointer(&ev.data))
|
|
arr[0] = action
|
|
arr[1] = C.long(w.atoms.wmStateFullscreen)
|
|
arr[2] = 0
|
|
arr[3] = 1 // application
|
|
arr[4] = 0
|
|
C.XSendEvent(
|
|
w.x,
|
|
C.XDefaultRootWindow(w.x), // MUST be the root window
|
|
C.False,
|
|
C.SubstructureNotifyMask|C.SubstructureRedirectMask,
|
|
&xev,
|
|
)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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: image.Point{
|
|
X: w.config.Size.X,
|
|
Y: w.config.Size.Y,
|
|
},
|
|
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))
|
|
w.config.Size = image.Pt(int(cevt.width), int(cevt.height))
|
|
// 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.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.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)
|
|
|
|
// extensions
|
|
C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1)
|
|
|
|
w.Configure(options)
|
|
|
|
// make the window visible on the screen
|
|
C.XMapWindow(dpy, win)
|
|
|
|
go func() {
|
|
w.w.SetDriver(w)
|
|
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
|
|
}
|