forked from joejulian/gio
8999747ad2
This change implements a Vulkan port for the two renderers, old and compute. Run with GIORENDERER=forcecompute to test the compute renderer. To shake out bugs faster, it is also made the default on systems that support it. To disable Vulkan and force the use of OpenGL, use the `novulkan` tag: $ go run -tags novulkan gioui.org/example/kitchen Don't forget to file an issue describing the issue that prompted the use of the tag. Signed-off-by: Elias Naur <mail@eliasnaur.com>
808 lines
21 KiB
Go
808 lines
21 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"
|
|
)
|
|
|
|
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
|
|
// "_NET_ACTIVE_WINDOW"
|
|
wmActiveWindow 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)
|
|
}
|
|
|
|
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 = 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: 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.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)
|
|
w.atoms.wmActiveWindow = w.atom("_NET_ACTIVE_WINDOW", 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
|
|
}
|