diff options
Diffstat (limited to 'vendor/gioui.org/app/os_x11.go')
-rw-r--r-- | vendor/gioui.org/app/os_x11.go | 849 |
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 +} |