os_x11.go raw

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