123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929 |
- // 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
- }
|