chore(v2): vendor dependencies for offline/China builds
go mod vendor pins onnxruntime_go v1.12.1, Gio and the rest into v2/vendor so go run/build work without hitting proxy.golang.org (blocked/slow in China). Verified: CGO_ENABLED=1 go build -mod=vendor ./internal/spike and GOOS=windows go build -mod=vendor ./internal/ui both pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+928
@@ -0,0 +1,928 @@
|
||||
// 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"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/op"
|
||||
"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
|
||||
}
|
||||
metric unit.Metric
|
||||
notify struct {
|
||||
read, write int
|
||||
}
|
||||
|
||||
animating bool
|
||||
|
||||
pointerBtns pointer.Buttons
|
||||
|
||||
clipboard struct {
|
||||
content []byte
|
||||
}
|
||||
cursor pointer.Cursor
|
||||
config Config
|
||||
|
||||
wakeups chan struct{}
|
||||
handler x11EventHandler
|
||||
buf [100]byte
|
||||
}
|
||||
|
||||
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(mime string, s []byte) {
|
||||
w.clipboard.content = 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.Decorated != prev.Decorated {
|
||||
w.config.Decorated = cnf.Decorated
|
||||
}
|
||||
w.ProcessEvent(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(acts system.Action) {
|
||||
walkActions(acts, func(a system.Action) {
|
||||
switch a {
|
||||
case system.ActionCenter:
|
||||
w.center()
|
||||
case system.ActionRaise:
|
||||
w.raise()
|
||||
}
|
||||
})
|
||||
if acts&system.ActionClose != 0 {
|
||||
w.close()
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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(cursor pointer.Cursor) {
|
||||
if cursor == pointer.CursorNone {
|
||||
w.cursor = cursor
|
||||
C.XFixesHideCursor(w.x, w.xw)
|
||||
return
|
||||
}
|
||||
|
||||
xcursor := xCursor[cursor]
|
||||
cname := C.CString(xcursor)
|
||||
defer C.free(unsafe.Pointer(cname))
|
||||
c := C.XcursorLibraryLoadCursor(w.x, cname)
|
||||
if c == 0 {
|
||||
cursor = pointer.CursorDefault
|
||||
}
|
||||
w.cursor = cursor
|
||||
// If c if null (i.e. cursor 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) {}
|
||||
|
||||
func (w *x11Window) EditorStateChanged(old, new editorState) {}
|
||||
|
||||
// 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) ProcessEvent(e event.Event) {
|
||||
w.w.ProcessEvent(e)
|
||||
}
|
||||
|
||||
func (w *x11Window) shutdown(err error) {
|
||||
w.ProcessEvent(X11ViewEvent{})
|
||||
w.ProcessEvent(DestroyEvent{Err: err})
|
||||
w.destroy()
|
||||
}
|
||||
|
||||
func (w *x11Window) Event() event.Event {
|
||||
for {
|
||||
evt, ok := w.w.nextEvent()
|
||||
if !ok {
|
||||
w.dispatch()
|
||||
continue
|
||||
}
|
||||
return evt
|
||||
}
|
||||
}
|
||||
|
||||
func (w *x11Window) Run(f func()) {
|
||||
f()
|
||||
}
|
||||
|
||||
func (w *x11Window) Frame(frame *op.Ops) {
|
||||
w.w.ProcessFrame(frame, nil)
|
||||
}
|
||||
|
||||
func (w *x11Window) Invalidate() {
|
||||
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) dispatch() {
|
||||
if w.x == nil {
|
||||
// Only Invalidate can wake us up.
|
||||
<-w.wakeups
|
||||
w.w.Invalidate()
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-w.wakeups:
|
||||
w.w.Invalidate()
|
||||
default:
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
syn = w.handler.handleEvents()
|
||||
if w.x == nil {
|
||||
// handleEvents received a close request and destroyed the window.
|
||||
return
|
||||
}
|
||||
if !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 = w.handler.handleEvents()
|
||||
if w.x == nil {
|
||||
return
|
||||
}
|
||||
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear notifications.
|
||||
for {
|
||||
_, err := syscall.Read(w.notify.read, w.buf[:])
|
||||
if err == syscall.EAGAIN {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
|
||||
}
|
||||
}
|
||||
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
|
||||
w.ProcessEvent(frameEvent{
|
||||
FrameEvent: FrameEvent{
|
||||
Now: time.Now(),
|
||||
Size: w.config.Size,
|
||||
Metric: w.metric,
|
||||
},
|
||||
Sync: syn,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
w.x = nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if ee, ok := e.(key.EditEvent); ok {
|
||||
// There's no support for IME yet.
|
||||
w.w.EditorInsert(ee.Text)
|
||||
} else {
|
||||
w.ProcessEvent(e)
|
||||
}
|
||||
}
|
||||
case C.ButtonPress, C.ButtonRelease:
|
||||
bevt := (*C.XButtonEvent)(unsafe.Pointer(xev))
|
||||
ev := pointer.Event{
|
||||
Kind: 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.Kind = 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:
|
||||
ev.Kind = pointer.Scroll
|
||||
// scroll up or left (if shift is pressed).
|
||||
if ev.Modifiers == key.ModShift {
|
||||
ev.Scroll.X = -scrollScale
|
||||
} else {
|
||||
ev.Scroll.Y = -scrollScale
|
||||
}
|
||||
case C.Button5:
|
||||
// scroll down or right (if shift is pressed).
|
||||
ev.Kind = pointer.Scroll
|
||||
if ev.Modifiers == key.ModShift {
|
||||
ev.Scroll.X = +scrollScale
|
||||
} else {
|
||||
ev.Scroll.Y = +scrollScale
|
||||
}
|
||||
case 6:
|
||||
// http://xahlee.info/linux/linux_x11_mouse_button_number.html
|
||||
// scroll left.
|
||||
ev.Kind = pointer.Scroll
|
||||
ev.Scroll.X = -scrollScale * 2
|
||||
case 7:
|
||||
// scroll right
|
||||
ev.Kind = 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.ProcessEvent(ev)
|
||||
case C.MotionNotify:
|
||||
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
|
||||
w.ProcessEvent(pointer.Event{
|
||||
Kind: 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.config.Focused = true
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
case C.FocusOut:
|
||||
w.config.Focused = false
|
||||
w.ProcessEvent(ConfigEvent{Config: w.config})
|
||||
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.ProcessEvent(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.ProcessEvent(transfer.DataEvent{
|
||||
Type: "application/text",
|
||||
Open: func() io.ReadCloser {
|
||||
return io.NopCloser(strings.NewReader(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.shutdown(nil)
|
||||
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.handler = x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
|
||||
w.notify.read = pipe[0]
|
||||
w.notify.write = pipe[1]
|
||||
w.w.SetDriver(w)
|
||||
|
||||
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(ID)
|
||||
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)
|
||||
|
||||
// make the window visible on the screen
|
||||
C.XMapWindow(dpy, win)
|
||||
w.Configure(options)
|
||||
w.ProcessEvent(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user