aboutsummaryrefslogtreecommitdiff
path: root/vendor/gioui.org/app/os_x11.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/gioui.org/app/os_x11.go')
-rw-r--r--vendor/gioui.org/app/os_x11.go849
1 files changed, 849 insertions, 0 deletions
diff --git a/vendor/gioui.org/app/os_x11.go b/vendor/gioui.org/app/os_x11.go
new file mode 100644
index 0000000..1c75247
--- /dev/null
+++ b/vendor/gioui.org/app/os_x11.go
@@ -0,0 +1,849 @@
+// 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)
+)
+
+func (w *x11Window) NewContext() (context, error) {
+ var firstErr error
+ if f := newX11VulkanContext; f != nil {
+ 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)
+ 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.Mode != Fullscreen && 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)
+ }
+ if w.config != prev {
+ w.w.Event(ConfigEvent{Config: w.config})
+ }
+}
+
+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) SetWindowMode(mode WindowMode) {
+ var action C.long
+ switch mode {
+ case Windowed:
+ action = _NET_WM_STATE_REMOVE
+ case Fullscreen:
+ action = _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."
+ w.sendWMStateEvent(action, w.atoms.wmStateFullscreen, 0)
+}
+
+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)
+}
+
+// Maximize the window.
+func (w *x11Window) Maximize() {
+ w.sendWMStateEvent(_NET_WM_STATE_ADD, w.atoms.wmStateMaximizedHorz, w.atoms.wmStateMaximizedVert)
+}
+
+// Center the window.
+func (w *x11Window) 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))
+}
+
+// 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)
+ w.Configure(options)
+
+ // make the window visible on the screen
+ C.XMapWindow(dpy, win)
+ 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
+}