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