gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/app/os_x11.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  //go:build ((linux && !android) || freebsd || openbsd) && !nox11
     4  // +build linux,!android freebsd openbsd
     5  // +build !nox11
     6  
     7  package app
     8  
     9  /*
    10  #cgo freebsd openbsd CFLAGS: -I/usr/X11R6/include -I/usr/local/include
    11  #cgo freebsd openbsd LDFLAGS: -L/usr/X11R6/lib -L/usr/local/lib
    12  #cgo freebsd openbsd LDFLAGS: -lX11 -lxkbcommon -lxkbcommon-x11 -lX11-xcb -lXcursor -lXfixes
    13  #cgo linux pkg-config: x11 xkbcommon xkbcommon-x11 x11-xcb xcursor xfixes
    14  
    15  #include <stdlib.h>
    16  #include <locale.h>
    17  #include <X11/Xlib.h>
    18  #include <X11/Xatom.h>
    19  #include <X11/Xutil.h>
    20  #include <X11/Xresource.h>
    21  #include <X11/XKBlib.h>
    22  #include <X11/Xlib-xcb.h>
    23  #include <X11/extensions/Xfixes.h>
    24  #include <X11/Xcursor/Xcursor.h>
    25  #include <xkbcommon/xkbcommon-x11.h>
    26  
    27  */
    28  import "C"
    29  import (
    30  	"errors"
    31  	"fmt"
    32  	"image"
    33  	"io"
    34  	"strconv"
    35  	"strings"
    36  	"sync"
    37  	"time"
    38  	"unsafe"
    39  
    40  	"gioui.org/f32"
    41  	"gioui.org/io/event"
    42  	"gioui.org/io/key"
    43  	"gioui.org/io/pointer"
    44  	"gioui.org/io/system"
    45  	"gioui.org/io/transfer"
    46  	"gioui.org/op"
    47  	"gioui.org/unit"
    48  
    49  	syscall "golang.org/x/sys/unix"
    50  
    51  	"gioui.org/app/internal/xkb"
    52  )
    53  
    54  const (
    55  	_NET_WM_STATE_REMOVE = 0
    56  	_NET_WM_STATE_ADD    = 1
    57  )
    58  
    59  type x11Window struct {
    60  	w            *callbacks
    61  	x            *C.Display
    62  	xkb          *xkb.Context
    63  	xkbEventBase C.int
    64  	xw           C.Window
    65  
    66  	atoms struct {
    67  		// "UTF8_STRING".
    68  		utf8string C.Atom
    69  		// "text/plain;charset=utf-8".
    70  		plaintext C.Atom
    71  		// "TARGETS"
    72  		targets C.Atom
    73  		// "CLIPBOARD".
    74  		clipboard C.Atom
    75  		// "PRIMARY".
    76  		primary C.Atom
    77  		// "CLIPBOARD_CONTENT", the clipboard destination property.
    78  		clipboardContent C.Atom
    79  		// "WM_DELETE_WINDOW"
    80  		evDelWindow C.Atom
    81  		// "ATOM"
    82  		atom C.Atom
    83  		// "GTK_TEXT_BUFFER_CONTENTS"
    84  		gtk_text_buffer_contents C.Atom
    85  		// "_NET_WM_NAME"
    86  		wmName C.Atom
    87  		// "_NET_WM_STATE"
    88  		wmState C.Atom
    89  		// "_NET_WM_STATE_FULLSCREEN"
    90  		wmStateFullscreen C.Atom
    91  		// "_NET_ACTIVE_WINDOW"
    92  		wmActiveWindow C.Atom
    93  		// _NET_WM_STATE_MAXIMIZED_HORZ
    94  		wmStateMaximizedHorz C.Atom
    95  		// _NET_WM_STATE_MAXIMIZED_VERT
    96  		wmStateMaximizedVert C.Atom
    97  	}
    98  	metric unit.Metric
    99  	notify struct {
   100  		read, write int
   101  	}
   102  
   103  	animating bool
   104  
   105  	pointerBtns pointer.Buttons
   106  
   107  	clipboard struct {
   108  		content []byte
   109  	}
   110  	cursor pointer.Cursor
   111  	config Config
   112  
   113  	wakeups chan struct{}
   114  	handler x11EventHandler
   115  	buf     [100]byte
   116  
   117  	// invMy avoids the race between destroy and Invalidate.
   118  	invMu sync.Mutex
   119  }
   120  
   121  var (
   122  	newX11EGLContext    func(w *x11Window) (context, error)
   123  	newX11VulkanContext func(w *x11Window) (context, error)
   124  )
   125  
   126  // X11 and Vulkan doesn't work reliably on NVIDIA systems.
   127  // See https://gioui.org/issue/347.
   128  const vulkanBuggy = true
   129  
   130  func (w *x11Window) NewContext() (context, error) {
   131  	var firstErr error
   132  	if f := newX11VulkanContext; f != nil && !vulkanBuggy {
   133  		c, err := f(w)
   134  		if err == nil {
   135  			return c, nil
   136  		}
   137  		firstErr = err
   138  	}
   139  	if f := newX11EGLContext; f != nil {
   140  		c, err := f(w)
   141  		if err == nil {
   142  			return c, nil
   143  		}
   144  		firstErr = err
   145  	}
   146  	if firstErr != nil {
   147  		return nil, firstErr
   148  	}
   149  	return nil, errors.New("x11: no available GPU backends")
   150  }
   151  
   152  func (w *x11Window) SetAnimating(anim bool) {
   153  	w.animating = anim
   154  }
   155  
   156  func (w *x11Window) ReadClipboard() {
   157  	C.XDeleteProperty(w.x, w.xw, w.atoms.clipboardContent)
   158  	C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime)
   159  }
   160  
   161  func (w *x11Window) WriteClipboard(mime string, s []byte) {
   162  	w.clipboard.content = s
   163  	C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime)
   164  	C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime)
   165  }
   166  
   167  func (w *x11Window) Configure(options []Option) {
   168  	var shints C.XSizeHints
   169  	prev := w.config
   170  	cnf := w.config
   171  	cnf.apply(w.metric, options)
   172  	// Decorations are never disabled.
   173  	cnf.Decorated = true
   174  
   175  	switch cnf.Mode {
   176  	case Fullscreen:
   177  		switch prev.Mode {
   178  		case Fullscreen:
   179  		case Minimized:
   180  			w.raise()
   181  			fallthrough
   182  		default:
   183  			w.config.Mode = Fullscreen
   184  			w.sendWMStateEvent(_NET_WM_STATE_ADD, w.atoms.wmStateFullscreen, 0)
   185  		}
   186  	case Minimized:
   187  		switch prev.Mode {
   188  		case Minimized, Fullscreen:
   189  		default:
   190  			w.config.Mode = Minimized
   191  			screen := C.XDefaultScreen(w.x)
   192  			C.XIconifyWindow(w.x, w.xw, screen)
   193  		}
   194  	case Maximized:
   195  		switch prev.Mode {
   196  		case Fullscreen:
   197  		case Minimized:
   198  			w.raise()
   199  			fallthrough
   200  		default:
   201  			w.config.Mode = Maximized
   202  			w.sendWMStateEvent(_NET_WM_STATE_ADD, w.atoms.wmStateMaximizedHorz, w.atoms.wmStateMaximizedVert)
   203  			w.setTitle(prev, cnf)
   204  		}
   205  	case Windowed:
   206  		switch prev.Mode {
   207  		case Fullscreen:
   208  			w.config.Mode = Windowed
   209  			w.sendWMStateEvent(_NET_WM_STATE_REMOVE, w.atoms.wmStateFullscreen, 0)
   210  			C.XResizeWindow(w.x, w.xw, C.uint(cnf.Size.X), C.uint(cnf.Size.Y))
   211  		case Minimized:
   212  			w.config.Mode = Windowed
   213  			w.raise()
   214  		case Maximized:
   215  			w.config.Mode = Windowed
   216  			w.sendWMStateEvent(_NET_WM_STATE_REMOVE, w.atoms.wmStateMaximizedHorz, w.atoms.wmStateMaximizedVert)
   217  		}
   218  		w.setTitle(prev, cnf)
   219  		if prev.Size != cnf.Size {
   220  			w.config.Size = cnf.Size
   221  			C.XResizeWindow(w.x, w.xw, C.uint(cnf.Size.X), C.uint(cnf.Size.Y))
   222  		}
   223  		if prev.MinSize != cnf.MinSize {
   224  			w.config.MinSize = cnf.MinSize
   225  			shints.min_width = C.int(cnf.MinSize.X)
   226  			shints.min_height = C.int(cnf.MinSize.Y)
   227  			shints.flags = C.PMinSize
   228  		}
   229  		if prev.MaxSize != cnf.MaxSize {
   230  			w.config.MaxSize = cnf.MaxSize
   231  			shints.max_width = C.int(cnf.MaxSize.X)
   232  			shints.max_height = C.int(cnf.MaxSize.Y)
   233  			shints.flags = shints.flags | C.PMaxSize
   234  		}
   235  		if shints.flags != 0 {
   236  			C.XSetWMNormalHints(w.x, w.xw, &shints)
   237  		}
   238  	}
   239  	if cnf.Decorated != prev.Decorated {
   240  		w.config.Decorated = cnf.Decorated
   241  	}
   242  	w.ProcessEvent(ConfigEvent{Config: w.config})
   243  }
   244  
   245  func (w *x11Window) setTitle(prev, cnf Config) {
   246  	if prev.Title != cnf.Title {
   247  		title := cnf.Title
   248  		ctitle := C.CString(title)
   249  		defer C.free(unsafe.Pointer(ctitle))
   250  		C.XStoreName(w.x, w.xw, ctitle)
   251  		// set _NET_WM_NAME as well for UTF-8 support in window title.
   252  		C.XSetTextProperty(w.x, w.xw,
   253  			&C.XTextProperty{
   254  				value:    (*C.uchar)(unsafe.Pointer(ctitle)),
   255  				encoding: w.atoms.utf8string,
   256  				format:   8,
   257  				nitems:   C.ulong(len(title)),
   258  			},
   259  			w.atoms.wmName)
   260  	}
   261  }
   262  
   263  func (w *x11Window) Perform(acts system.Action) {
   264  	walkActions(acts, func(a system.Action) {
   265  		switch a {
   266  		case system.ActionCenter:
   267  			w.center()
   268  		case system.ActionRaise:
   269  			w.raise()
   270  		}
   271  	})
   272  	if acts&system.ActionClose != 0 {
   273  		w.close()
   274  	}
   275  }
   276  
   277  func (w *x11Window) center() {
   278  	screen := C.XDefaultScreen(w.x)
   279  	width := C.XDisplayWidth(w.x, screen)
   280  	height := C.XDisplayHeight(w.x, screen)
   281  
   282  	var attrs C.XWindowAttributes
   283  	C.XGetWindowAttributes(w.x, w.xw, &attrs)
   284  	width -= attrs.border_width
   285  	height -= attrs.border_width
   286  
   287  	sz := w.config.Size
   288  	x := (int(width) - sz.X) / 2
   289  	y := (int(height) - sz.Y) / 2
   290  
   291  	C.XMoveResizeWindow(w.x, w.xw, C.int(x), C.int(y), C.uint(sz.X), C.uint(sz.Y))
   292  }
   293  
   294  func (w *x11Window) raise() {
   295  	var xev C.XEvent
   296  	ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev))
   297  	*ev = C.XClientMessageEvent{
   298  		_type:        C.ClientMessage,
   299  		display:      w.x,
   300  		window:       w.xw,
   301  		message_type: w.atoms.wmActiveWindow,
   302  		format:       32,
   303  	}
   304  	C.XSendEvent(
   305  		w.x,
   306  		C.XDefaultRootWindow(w.x), // MUST be the root window
   307  		C.False,
   308  		C.SubstructureNotifyMask|C.SubstructureRedirectMask,
   309  		&xev,
   310  	)
   311  	C.XMapRaised(w.display(), w.xw)
   312  }
   313  
   314  func (w *x11Window) SetCursor(cursor pointer.Cursor) {
   315  	if cursor == pointer.CursorNone {
   316  		w.cursor = cursor
   317  		C.XFixesHideCursor(w.x, w.xw)
   318  		return
   319  	}
   320  
   321  	xcursor := xCursor[cursor]
   322  	cname := C.CString(xcursor)
   323  	defer C.free(unsafe.Pointer(cname))
   324  	c := C.XcursorLibraryLoadCursor(w.x, cname)
   325  	if c == 0 {
   326  		cursor = pointer.CursorDefault
   327  	}
   328  	w.cursor = cursor
   329  	// If c if null (i.e. cursor was not found),
   330  	// XDefineCursor will use the default cursor.
   331  	C.XDefineCursor(w.x, w.xw, c)
   332  }
   333  
   334  func (w *x11Window) ShowTextInput(show bool) {}
   335  
   336  func (w *x11Window) SetInputHint(_ key.InputHint) {}
   337  
   338  func (w *x11Window) EditorStateChanged(old, new editorState) {}
   339  
   340  // close the window.
   341  func (w *x11Window) close() {
   342  	var xev C.XEvent
   343  	ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev))
   344  	*ev = C.XClientMessageEvent{
   345  		_type:        C.ClientMessage,
   346  		display:      w.x,
   347  		window:       w.xw,
   348  		message_type: w.atom("WM_PROTOCOLS", true),
   349  		format:       32,
   350  	}
   351  	arr := (*[5]C.long)(unsafe.Pointer(&ev.data))
   352  	arr[0] = C.long(w.atoms.evDelWindow)
   353  	arr[1] = C.CurrentTime
   354  	C.XSendEvent(w.x, w.xw, C.False, C.NoEventMask, &xev)
   355  }
   356  
   357  // action is one of _NET_WM_STATE_REMOVE, _NET_WM_STATE_ADD.
   358  func (w *x11Window) sendWMStateEvent(action C.long, atom1, atom2 C.ulong) {
   359  	var xev C.XEvent
   360  	ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev))
   361  	*ev = C.XClientMessageEvent{
   362  		_type:        C.ClientMessage,
   363  		display:      w.x,
   364  		window:       w.xw,
   365  		message_type: w.atoms.wmState,
   366  		format:       32,
   367  	}
   368  	data := (*[5]C.long)(unsafe.Pointer(&ev.data))
   369  	data[0] = C.long(action)
   370  	data[1] = C.long(atom1)
   371  	data[2] = C.long(atom2)
   372  	data[3] = 1 // application
   373  
   374  	C.XSendEvent(
   375  		w.x,
   376  		C.XDefaultRootWindow(w.x), // MUST be the root window
   377  		C.False,
   378  		C.SubstructureNotifyMask|C.SubstructureRedirectMask,
   379  		&xev,
   380  	)
   381  }
   382  
   383  var x11OneByte = make([]byte, 1)
   384  
   385  func (w *x11Window) ProcessEvent(e event.Event) {
   386  	w.w.ProcessEvent(e)
   387  }
   388  
   389  func (w *x11Window) shutdown(err error) {
   390  	w.ProcessEvent(X11ViewEvent{})
   391  	w.ProcessEvent(DestroyEvent{Err: err})
   392  	w.destroy()
   393  }
   394  
   395  func (w *x11Window) Event() event.Event {
   396  	for {
   397  		evt, ok := w.w.nextEvent()
   398  		if !ok {
   399  			w.dispatch()
   400  			continue
   401  		}
   402  		return evt
   403  	}
   404  }
   405  
   406  func (w *x11Window) Run(f func()) {
   407  	f()
   408  }
   409  
   410  func (w *x11Window) Frame(frame *op.Ops) {
   411  	w.w.ProcessFrame(frame, nil)
   412  }
   413  
   414  func (w *x11Window) Invalidate() {
   415  	select {
   416  	case w.wakeups <- struct{}{}:
   417  	default:
   418  	}
   419  	w.invMu.Lock()
   420  	defer w.invMu.Unlock()
   421  	if w.x == nil {
   422  		return
   423  	}
   424  	if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
   425  		panic(fmt.Errorf("failed to write to pipe: %v", err))
   426  	}
   427  }
   428  
   429  func (w *x11Window) display() *C.Display {
   430  	return w.x
   431  }
   432  
   433  func (w *x11Window) window() (C.Window, int, int) {
   434  	return w.xw, w.config.Size.X, w.config.Size.Y
   435  }
   436  
   437  func (w *x11Window) dispatch() {
   438  	if w.x == nil {
   439  		// Only Invalidate can wake us up.
   440  		<-w.wakeups
   441  		w.w.Invalidate()
   442  		return
   443  	}
   444  
   445  	select {
   446  	case <-w.wakeups:
   447  		w.w.Invalidate()
   448  	default:
   449  	}
   450  
   451  	xfd := C.XConnectionNumber(w.x)
   452  
   453  	// Poll for events and notifications.
   454  	pollfds := []syscall.PollFd{
   455  		{Fd: int32(xfd), Events: syscall.POLLIN | syscall.POLLERR},
   456  		{Fd: int32(w.notify.read), Events: syscall.POLLIN | syscall.POLLERR},
   457  	}
   458  	xEvents := &pollfds[0].Revents
   459  	// Plenty of room for a backlog of notifications.
   460  
   461  	var syn, anim bool
   462  	// Check for pending draw events before checking animation or blocking.
   463  	// This fixes an issue on Xephyr where on startup XPending() > 0 but
   464  	// poll will still block. This also prevents no-op calls to poll.
   465  	syn = w.handler.handleEvents()
   466  	if w.x == nil {
   467  		// handleEvents received a close request and destroyed the window.
   468  		return
   469  	}
   470  	if !syn {
   471  		anim = w.animating
   472  		if !anim {
   473  			// Clear poll events.
   474  			*xEvents = 0
   475  			// Wait for X event or gio notification.
   476  			if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
   477  				panic(fmt.Errorf("x11 loop: poll failed: %w", err))
   478  			}
   479  			switch {
   480  			case *xEvents&syscall.POLLIN != 0:
   481  				syn = w.handler.handleEvents()
   482  				if w.x == nil {
   483  					return
   484  				}
   485  			case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
   486  			}
   487  		}
   488  	}
   489  	// Clear notifications.
   490  	for {
   491  		_, err := syscall.Read(w.notify.read, w.buf[:])
   492  		if err == syscall.EAGAIN {
   493  			break
   494  		}
   495  		if err != nil {
   496  			panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
   497  		}
   498  	}
   499  	if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
   500  		w.ProcessEvent(frameEvent{
   501  			FrameEvent: FrameEvent{
   502  				Now:    time.Now(),
   503  				Size:   w.config.Size,
   504  				Metric: w.metric,
   505  			},
   506  			Sync: syn,
   507  		})
   508  	}
   509  }
   510  
   511  func (w *x11Window) destroy() {
   512  	w.invMu.Lock()
   513  	defer w.invMu.Unlock()
   514  	if w.notify.write != 0 {
   515  		syscall.Close(w.notify.write)
   516  		w.notify.write = 0
   517  	}
   518  	if w.notify.read != 0 {
   519  		syscall.Close(w.notify.read)
   520  		w.notify.read = 0
   521  	}
   522  	if w.xkb != nil {
   523  		w.xkb.Destroy()
   524  		w.xkb = nil
   525  	}
   526  	C.XDestroyWindow(w.x, w.xw)
   527  	C.XCloseDisplay(w.x)
   528  	w.x = nil
   529  }
   530  
   531  // atom is a wrapper around XInternAtom. Callers should cache the result
   532  // in order to limit round-trips to the X server.
   533  func (w *x11Window) atom(name string, onlyIfExists bool) C.Atom {
   534  	cname := C.CString(name)
   535  	defer C.free(unsafe.Pointer(cname))
   536  	flag := C.Bool(C.False)
   537  	if onlyIfExists {
   538  		flag = C.True
   539  	}
   540  	return C.XInternAtom(w.x, cname, flag)
   541  }
   542  
   543  // x11EventHandler wraps static variables for the main event loop.
   544  // Its sole purpose is to prevent heap allocation and reduce clutter
   545  // in x11window.loop.
   546  type x11EventHandler struct {
   547  	w    *x11Window
   548  	text []byte
   549  	xev  *C.XEvent
   550  }
   551  
   552  // handleEvents returns true if the window needs to be redrawn.
   553  func (h *x11EventHandler) handleEvents() bool {
   554  	w := h.w
   555  	xev := h.xev
   556  	redraw := false
   557  	for C.XPending(w.x) != 0 {
   558  		C.XNextEvent(w.x, xev)
   559  		if C.XFilterEvent(xev, C.None) == C.True {
   560  			continue
   561  		}
   562  		switch _type := (*C.XAnyEvent)(unsafe.Pointer(xev))._type; _type {
   563  		case h.w.xkbEventBase:
   564  			xkbEvent := (*C.XkbAnyEvent)(unsafe.Pointer(xev))
   565  			switch xkbEvent.xkb_type {
   566  			case C.XkbNewKeyboardNotify, C.XkbMapNotify:
   567  				if err := h.w.updateXkbKeymap(); err != nil {
   568  					panic(err)
   569  				}
   570  			case C.XkbStateNotify:
   571  				state := (*C.XkbStateNotifyEvent)(unsafe.Pointer(xev))
   572  				h.w.xkb.UpdateMask(uint32(state.base_mods), uint32(state.latched_mods), uint32(state.locked_mods),
   573  					uint32(state.base_group), uint32(state.latched_group), uint32(state.locked_group))
   574  			}
   575  		case C.KeyPress, C.KeyRelease:
   576  			ks := key.Press
   577  			if _type == C.KeyRelease {
   578  				ks = key.Release
   579  			}
   580  			kevt := (*C.XKeyPressedEvent)(unsafe.Pointer(xev))
   581  			for _, e := range h.w.xkb.DispatchKey(uint32(kevt.keycode), ks) {
   582  				if ee, ok := e.(key.EditEvent); ok {
   583  					// There's no support for IME yet.
   584  					w.w.EditorInsert(ee.Text)
   585  				} else {
   586  					w.ProcessEvent(e)
   587  				}
   588  			}
   589  		case C.ButtonPress, C.ButtonRelease:
   590  			bevt := (*C.XButtonEvent)(unsafe.Pointer(xev))
   591  			ev := pointer.Event{
   592  				Kind:   pointer.Press,
   593  				Source: pointer.Mouse,
   594  				Position: f32.Point{
   595  					X: float32(bevt.x),
   596  					Y: float32(bevt.y),
   597  				},
   598  				Time:      time.Duration(bevt.time) * time.Millisecond,
   599  				Modifiers: w.xkb.Modifiers(),
   600  			}
   601  			if bevt._type == C.ButtonRelease {
   602  				ev.Kind = pointer.Release
   603  			}
   604  			var btn pointer.Buttons
   605  			const scrollScale = 10
   606  			switch bevt.button {
   607  			case C.Button1:
   608  				btn = pointer.ButtonPrimary
   609  			case C.Button2:
   610  				btn = pointer.ButtonTertiary
   611  			case C.Button3:
   612  				btn = pointer.ButtonSecondary
   613  			case C.Button4:
   614  				ev.Kind = pointer.Scroll
   615  				// scroll up or left (if shift is pressed).
   616  				if ev.Modifiers == key.ModShift {
   617  					ev.Scroll.X = -scrollScale
   618  				} else {
   619  					ev.Scroll.Y = -scrollScale
   620  				}
   621  			case C.Button5:
   622  				// scroll down or right (if shift is pressed).
   623  				ev.Kind = pointer.Scroll
   624  				if ev.Modifiers == key.ModShift {
   625  					ev.Scroll.X = +scrollScale
   626  				} else {
   627  					ev.Scroll.Y = +scrollScale
   628  				}
   629  			case 6:
   630  				// http://xahlee.info/linux/linux_x11_mouse_button_number.html
   631  				// scroll left.
   632  				ev.Kind = pointer.Scroll
   633  				ev.Scroll.X = -scrollScale * 2
   634  			case 7:
   635  				// scroll right
   636  				ev.Kind = pointer.Scroll
   637  				ev.Scroll.X = +scrollScale * 2
   638  			default:
   639  				continue
   640  			}
   641  			switch _type {
   642  			case C.ButtonPress:
   643  				w.pointerBtns |= btn
   644  			case C.ButtonRelease:
   645  				w.pointerBtns &^= btn
   646  			}
   647  			ev.Buttons = w.pointerBtns
   648  			w.ProcessEvent(ev)
   649  		case C.MotionNotify:
   650  			mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
   651  			w.ProcessEvent(pointer.Event{
   652  				Kind:    pointer.Move,
   653  				Source:  pointer.Mouse,
   654  				Buttons: w.pointerBtns,
   655  				Position: f32.Point{
   656  					X: float32(mevt.x),
   657  					Y: float32(mevt.y),
   658  				},
   659  				Time:      time.Duration(mevt.time) * time.Millisecond,
   660  				Modifiers: w.xkb.Modifiers(),
   661  			})
   662  		case C.Expose: // update
   663  			// redraw only on the last expose event
   664  			redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0
   665  		case C.FocusIn:
   666  			w.config.Focused = true
   667  			w.ProcessEvent(ConfigEvent{Config: w.config})
   668  		case C.FocusOut:
   669  			w.config.Focused = false
   670  			w.ProcessEvent(ConfigEvent{Config: w.config})
   671  		case C.ConfigureNotify: // window configuration change
   672  			cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev))
   673  			if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size {
   674  				w.config.Size = sz
   675  				w.ProcessEvent(ConfigEvent{Config: w.config})
   676  			}
   677  			// redraw will be done by a later expose event
   678  		case C.SelectionNotify:
   679  			cevt := (*C.XSelectionEvent)(unsafe.Pointer(xev))
   680  			prop := w.atoms.clipboardContent
   681  			if cevt.property != prop {
   682  				break
   683  			}
   684  			if cevt.selection != w.atoms.clipboard {
   685  				break
   686  			}
   687  			var text C.XTextProperty
   688  			if st := C.XGetTextProperty(w.x, w.xw, &text, prop); st == 0 {
   689  				// Failed; ignore.
   690  				break
   691  			}
   692  			if text.format != 8 || text.encoding != w.atoms.utf8string {
   693  				// Ignore non-utf-8 encoded strings.
   694  				break
   695  			}
   696  			str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
   697  			w.ProcessEvent(transfer.DataEvent{
   698  				Type: "application/text",
   699  				Open: func() io.ReadCloser {
   700  					return io.NopCloser(strings.NewReader(str))
   701  				},
   702  			})
   703  		case C.SelectionRequest:
   704  			cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
   705  			if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None {
   706  				// Unsupported clipboard or obsolete requestor.
   707  				break
   708  			}
   709  			notify := func() {
   710  				var xev C.XEvent
   711  				ev := (*C.XSelectionEvent)(unsafe.Pointer(&xev))
   712  				*ev = C.XSelectionEvent{
   713  					_type:     C.SelectionNotify,
   714  					display:   cevt.display,
   715  					requestor: cevt.requestor,
   716  					selection: cevt.selection,
   717  					target:    cevt.target,
   718  					property:  cevt.property,
   719  					time:      cevt.time,
   720  				}
   721  				C.XSendEvent(w.x, cevt.requestor, 0, 0, &xev)
   722  			}
   723  			switch cevt.target {
   724  			case w.atoms.targets:
   725  				// The requestor wants the supported clipboard
   726  				// formats. First write the targets...
   727  				formats := [...]C.long{
   728  					C.long(w.atoms.targets),
   729  					C.long(w.atoms.utf8string),
   730  					C.long(w.atoms.plaintext),
   731  					// GTK clients need this.
   732  					C.long(w.atoms.gtk_text_buffer_contents),
   733  				}
   734  				C.XChangeProperty(w.x, cevt.requestor, cevt.property, w.atoms.atom,
   735  					32 /* bitwidth of formats */, C.PropModeReplace,
   736  					(*C.uchar)(unsafe.Pointer(&formats)), C.int(len(formats)),
   737  				)
   738  				// ...then notify the requestor.
   739  				notify()
   740  			case w.atoms.plaintext, w.atoms.utf8string, w.atoms.gtk_text_buffer_contents:
   741  				content := w.clipboard.content
   742  				var ptr *C.uchar
   743  				if len(content) > 0 {
   744  					ptr = (*C.uchar)(unsafe.Pointer(&content[0]))
   745  				}
   746  				C.XChangeProperty(w.x, cevt.requestor, cevt.property, cevt.target,
   747  					8 /* bitwidth */, C.PropModeReplace,
   748  					ptr, C.int(len(content)),
   749  				)
   750  				notify()
   751  			}
   752  		case C.ClientMessage: // extensions
   753  			cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev))
   754  			switch *(*C.long)(unsafe.Pointer(&cevt.data)) {
   755  			case C.long(w.atoms.evDelWindow):
   756  				w.shutdown(nil)
   757  				return false
   758  			}
   759  		}
   760  	}
   761  	return redraw
   762  }
   763  
   764  var (
   765  	x11Threads sync.Once
   766  )
   767  
   768  func init() {
   769  	x11Driver = newX11Window
   770  }
   771  
   772  func newX11Window(gioWin *callbacks, options []Option) error {
   773  	var err error
   774  
   775  	pipe := make([]int, 2)
   776  	if err := syscall.Pipe2(pipe, syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil {
   777  		return fmt.Errorf("NewX11Window: failed to create pipe: %w", err)
   778  	}
   779  
   780  	x11Threads.Do(func() {
   781  		if C.XInitThreads() == 0 {
   782  			err = errors.New("x11: threads init failed")
   783  		}
   784  		C.XrmInitialize()
   785  	})
   786  	if err != nil {
   787  		return err
   788  	}
   789  	dpy := C.XOpenDisplay(nil)
   790  	if dpy == nil {
   791  		return errors.New("x11: cannot connect to the X server")
   792  	}
   793  	var major, minor C.int = C.XkbMajorVersion, C.XkbMinorVersion
   794  	var xkbEventBase C.int
   795  	if C.XkbQueryExtension(dpy, nil, &xkbEventBase, nil, &major, &minor) != C.True {
   796  		C.XCloseDisplay(dpy)
   797  		return errors.New("x11: XkbQueryExtension failed")
   798  	}
   799  	const bits = C.uint(C.XkbNewKeyboardNotifyMask | C.XkbMapNotifyMask | C.XkbStateNotifyMask)
   800  	if C.XkbSelectEvents(dpy, C.XkbUseCoreKbd, bits, bits) != C.True {
   801  		C.XCloseDisplay(dpy)
   802  		return errors.New("x11: XkbSelectEvents failed")
   803  	}
   804  	xkb, err := xkb.New()
   805  	if err != nil {
   806  		C.XCloseDisplay(dpy)
   807  		return fmt.Errorf("x11: %v", err)
   808  	}
   809  
   810  	ppsp := x11DetectUIScale(dpy)
   811  	cfg := unit.Metric{PxPerDp: ppsp, PxPerSp: ppsp}
   812  	// Only use cnf for getting the window size.
   813  	var cnf Config
   814  	cnf.apply(cfg, options)
   815  
   816  	swa := C.XSetWindowAttributes{
   817  		event_mask: C.ExposureMask | C.FocusChangeMask | // update
   818  			C.KeyPressMask | C.KeyReleaseMask | // keyboard
   819  			C.ButtonPressMask | C.ButtonReleaseMask | // mouse clicks
   820  			C.PointerMotionMask | // mouse movement
   821  			C.StructureNotifyMask, // resize
   822  		background_pixmap: C.None,
   823  		override_redirect: C.False,
   824  	}
   825  	win := C.XCreateWindow(dpy, C.XDefaultRootWindow(dpy),
   826  		0, 0, C.uint(cnf.Size.X), C.uint(cnf.Size.Y),
   827  		0, C.CopyFromParent, C.InputOutput, nil,
   828  		C.CWEventMask|C.CWBackPixmap|C.CWOverrideRedirect, &swa)
   829  
   830  	w := &x11Window{
   831  		w: gioWin, x: dpy, xw: win,
   832  		metric:       cfg,
   833  		xkb:          xkb,
   834  		xkbEventBase: xkbEventBase,
   835  		wakeups:      make(chan struct{}, 1),
   836  		config:       Config{Size: cnf.Size},
   837  	}
   838  	w.handler = x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
   839  	w.notify.read = pipe[0]
   840  	w.notify.write = pipe[1]
   841  	w.w.SetDriver(w)
   842  
   843  	if err := w.updateXkbKeymap(); err != nil {
   844  		w.destroy()
   845  		return err
   846  	}
   847  
   848  	var hints C.XWMHints
   849  	hints.input = C.True
   850  	hints.flags = C.InputHint
   851  	C.XSetWMHints(dpy, win, &hints)
   852  
   853  	name := C.CString(ID)
   854  	defer C.free(unsafe.Pointer(name))
   855  	wmhints := C.XClassHint{name, name}
   856  	C.XSetClassHint(dpy, win, &wmhints)
   857  
   858  	w.atoms.utf8string = w.atom("UTF8_STRING", false)
   859  	w.atoms.plaintext = w.atom("text/plain;charset=utf-8", false)
   860  	w.atoms.gtk_text_buffer_contents = w.atom("GTK_TEXT_BUFFER_CONTENTS", false)
   861  	w.atoms.evDelWindow = w.atom("WM_DELETE_WINDOW", false)
   862  	w.atoms.clipboard = w.atom("CLIPBOARD", false)
   863  	w.atoms.primary = w.atom("PRIMARY", false)
   864  	w.atoms.clipboardContent = w.atom("CLIPBOARD_CONTENT", false)
   865  	w.atoms.atom = w.atom("ATOM", false)
   866  	w.atoms.targets = w.atom("TARGETS", false)
   867  	w.atoms.wmName = w.atom("_NET_WM_NAME", false)
   868  	w.atoms.wmState = w.atom("_NET_WM_STATE", false)
   869  	w.atoms.wmStateFullscreen = w.atom("_NET_WM_STATE_FULLSCREEN", false)
   870  	w.atoms.wmActiveWindow = w.atom("_NET_ACTIVE_WINDOW", false)
   871  	w.atoms.wmStateMaximizedHorz = w.atom("_NET_WM_STATE_MAXIMIZED_HORZ", false)
   872  	w.atoms.wmStateMaximizedVert = w.atom("_NET_WM_STATE_MAXIMIZED_VERT", false)
   873  
   874  	// extensions
   875  	C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1)
   876  
   877  	// make the window visible on the screen
   878  	C.XMapWindow(dpy, win)
   879  	w.Configure(options)
   880  	w.ProcessEvent(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
   881  	return nil
   882  }
   883  
   884  // detectUIScale reports the system UI scale, or 1.0 if it fails.
   885  func x11DetectUIScale(dpy *C.Display) float32 {
   886  	// default fixed DPI value used in most desktop UI toolkits
   887  	const defaultDesktopDPI = 96
   888  	var scale float32 = 1.0
   889  
   890  	// Get actual DPI from X resource Xft.dpi (set by GTK and Qt).
   891  	// This value is entirely based on user preferences and conflates both
   892  	// screen (UI) scaling and font scale.
   893  	rms := C.XResourceManagerString(dpy)
   894  	if rms != nil {
   895  		db := C.XrmGetStringDatabase(rms)
   896  		if db != nil {
   897  			var (
   898  				t *C.char
   899  				v C.XrmValue
   900  			)
   901  			if C.XrmGetResource(db, (*C.char)(unsafe.Pointer(&[]byte("Xft.dpi\x00")[0])),
   902  				(*C.char)(unsafe.Pointer(&[]byte("Xft.Dpi\x00")[0])), &t, &v) != C.False {
   903  				if t != nil && C.GoString(t) == "String" {
   904  					f, err := strconv.ParseFloat(C.GoString(v.addr), 32)
   905  					if err == nil {
   906  						scale = float32(f) / defaultDesktopDPI
   907  					}
   908  				}
   909  			}
   910  			C.XrmDestroyDatabase(db)
   911  		}
   912  	}
   913  
   914  	return scale
   915  }
   916  
   917  func (w *x11Window) updateXkbKeymap() error {
   918  	w.xkb.DestroyKeymapState()
   919  	ctx := (*C.struct_xkb_context)(unsafe.Pointer(w.xkb.Ctx))
   920  	xcb := C.XGetXCBConnection(w.x)
   921  	if xcb == nil {
   922  		return errors.New("x11: XGetXCBConnection failed")
   923  	}
   924  	xkbDevID := C.xkb_x11_get_core_keyboard_device_id(xcb)
   925  	if xkbDevID == -1 {
   926  		return errors.New("x11: xkb_x11_get_core_keyboard_device_id failed")
   927  	}
   928  	keymap := C.xkb_x11_keymap_new_from_device(ctx, xcb, xkbDevID, C.XKB_KEYMAP_COMPILE_NO_FLAGS)
   929  	if keymap == nil {
   930  		return errors.New("x11: xkb_x11_keymap_new_from_device failed")
   931  	}
   932  	state := C.xkb_x11_state_new_from_device(keymap, xcb, xkbDevID)
   933  	if state == nil {
   934  		C.xkb_keymap_unref(keymap)
   935  		return errors.New("x11: xkb_x11_keymap_new_from_device failed")
   936  	}
   937  	w.xkb.SetKeymap(unsafe.Pointer(keymap), unsafe.Pointer(state))
   938  	return nil
   939  }