github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/gio/app/internal/window/os_x11.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  // +build linux,!android,!nox11 freebsd
     4  
     5  package window
     6  
     7  /*
     8  #cgo LDFLAGS: -lX11 -lxkbcommon -lxkbcommon-x11 -lX11-xcb
     9  #include <stdlib.h>
    10  #include <locale.h>
    11  #include <X11/Xlib.h>
    12  #include <X11/Xatom.h>
    13  #include <X11/Xutil.h>
    14  #include <X11/Xresource.h>
    15  #include <X11/XKBlib.h>
    16  #include <X11/Xlib-xcb.h>
    17  #include <xkbcommon/xkbcommon-x11.h>
    18  
    19  */
    20  import "C"
    21  import (
    22  	"errors"
    23  	"fmt"
    24  	"image"
    25  	"strconv"
    26  	"sync"
    27  	"time"
    28  	"unsafe"
    29  
    30  	"github.com/gop9/olt/gio/f32"
    31  	"github.com/gop9/olt/gio/io/key"
    32  	"github.com/gop9/olt/gio/io/pointer"
    33  	"github.com/gop9/olt/gio/io/system"
    34  
    35  	"github.com/gop9/olt/gio/app/internal/xkb"
    36  	syscall "golang.org/x/sys/unix"
    37  )
    38  
    39  type x11Window struct {
    40  	w            Callbacks
    41  	x            *C.Display
    42  	xkb          *xkb.Context
    43  	xkbEventBase C.int
    44  	xw           C.Window
    45  
    46  	evDelWindow C.Atom
    47  	stage       system.Stage
    48  	cfg         config
    49  	width       int
    50  	height      int
    51  	notify      struct {
    52  		read, write int
    53  	}
    54  	dead bool
    55  
    56  	mu        sync.Mutex
    57  	animating bool
    58  
    59  	pointerBtns pointer.Buttons
    60  }
    61  
    62  func (w *x11Window) SetAnimating(anim bool) {
    63  	w.mu.Lock()
    64  	w.animating = anim
    65  	w.mu.Unlock()
    66  	if anim {
    67  		w.wakeup()
    68  	}
    69  }
    70  
    71  func (w *x11Window) ShowTextInput(show bool) {}
    72  
    73  var x11OneByte = make([]byte, 1)
    74  
    75  func (w *x11Window) wakeup() {
    76  	if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
    77  		panic(fmt.Errorf("failed to write to pipe: %v", err))
    78  	}
    79  }
    80  
    81  func (w *x11Window) display() *C.Display {
    82  	return w.x
    83  }
    84  
    85  func (w *x11Window) window() (C.Window, int, int) {
    86  	return w.xw, w.width, w.height
    87  }
    88  
    89  func (w *x11Window) setStage(s system.Stage) {
    90  	if s == w.stage {
    91  		return
    92  	}
    93  	w.stage = s
    94  	w.w.Event(system.StageEvent{s})
    95  }
    96  
    97  func (w *x11Window) loop() {
    98  	h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
    99  	xfd := C.XConnectionNumber(w.x)
   100  
   101  	// Poll for events and notifications.
   102  	pollfds := []syscall.PollFd{
   103  		{Fd: int32(xfd), Events: syscall.POLLIN | syscall.POLLERR},
   104  		{Fd: int32(w.notify.read), Events: syscall.POLLIN | syscall.POLLERR},
   105  	}
   106  	xEvents := &pollfds[0].Revents
   107  	// Plenty of room for a backlog of notifications.
   108  	buf := make([]byte, 100)
   109  
   110  loop:
   111  	for !w.dead {
   112  		var syn, redraw bool
   113  		// Check for pending draw events before checking animation or blocking.
   114  		// This fixes an issue on Xephyr where on startup XPending() > 0 but
   115  		// poll will still block. This also prevents no-op calls to poll.
   116  		if syn = h.handleEvents(); !syn {
   117  			w.mu.Lock()
   118  			animating := w.animating
   119  			w.mu.Unlock()
   120  			if animating {
   121  				redraw = true
   122  			} else {
   123  				// Clear poll events.
   124  				*xEvents = 0
   125  				// Wait for X event or gio notification.
   126  				if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
   127  					panic(fmt.Errorf("x11 loop: poll failed: %w", err))
   128  				}
   129  				switch {
   130  				case *xEvents&syscall.POLLIN != 0:
   131  					syn = h.handleEvents()
   132  					if w.dead {
   133  						break loop
   134  					}
   135  				case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
   136  					break loop
   137  				}
   138  			}
   139  		}
   140  		// Clear notifications.
   141  		for {
   142  			_, err := syscall.Read(w.notify.read, buf)
   143  			if err == syscall.EAGAIN {
   144  				break
   145  			}
   146  			if err != nil {
   147  				panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
   148  			}
   149  			redraw = true
   150  		}
   151  
   152  		if redraw || syn {
   153  			w.cfg.now = time.Now()
   154  			w.w.Event(FrameEvent{
   155  				FrameEvent: system.FrameEvent{
   156  					Size: image.Point{
   157  						X: w.width,
   158  						Y: w.height,
   159  					},
   160  					Config: &w.cfg,
   161  				},
   162  				Sync: syn,
   163  			})
   164  		}
   165  	}
   166  	w.w.Event(system.DestroyEvent{Err: nil})
   167  }
   168  
   169  func (w *x11Window) destroy() {
   170  	if w.notify.write != 0 {
   171  		syscall.Close(w.notify.write)
   172  		w.notify.write = 0
   173  	}
   174  	if w.notify.read != 0 {
   175  		syscall.Close(w.notify.read)
   176  		w.notify.read = 0
   177  	}
   178  	if w.xkb != nil {
   179  		w.xkb.Destroy()
   180  		w.xkb = nil
   181  	}
   182  	C.XDestroyWindow(w.x, w.xw)
   183  	C.XCloseDisplay(w.x)
   184  }
   185  
   186  // atom is a wrapper around XInternAtom. Callers should cache the result
   187  // in order to limit round-trips to the X server.
   188  //
   189  func (w *x11Window) atom(name string, onlyIfExists bool) C.Atom {
   190  	cname := C.CString(name)
   191  	defer C.free(unsafe.Pointer(cname))
   192  	flag := C.Bool(C.False)
   193  	if onlyIfExists {
   194  		flag = C.True
   195  	}
   196  	return C.XInternAtom(w.x, cname, flag)
   197  }
   198  
   199  // x11EventHandler wraps static variables for the main event loop.
   200  // Its sole purpose is to prevent heap allocation and reduce clutter
   201  // in x11window.loop.
   202  //
   203  type x11EventHandler struct {
   204  	w      *x11Window
   205  	text   []byte
   206  	xev    *C.XEvent
   207  	status C.Status
   208  	keysym C.KeySym
   209  }
   210  
   211  // handleEvents returns true if the window needs to be redrawn.
   212  //
   213  func (h *x11EventHandler) handleEvents() bool {
   214  	w := h.w
   215  	xev := h.xev
   216  	redraw := false
   217  	for C.XPending(w.x) != 0 {
   218  		C.XNextEvent(w.x, xev)
   219  		if C.XFilterEvent(xev, C.None) == C.True {
   220  			continue
   221  		}
   222  		switch _type := (*C.XAnyEvent)(unsafe.Pointer(xev))._type; _type {
   223  		case h.w.xkbEventBase:
   224  			xkbEvent := (*C.XkbAnyEvent)(unsafe.Pointer(xev))
   225  			switch xkbEvent.xkb_type {
   226  			case C.XkbNewKeyboardNotify, C.XkbMapNotify:
   227  				if err := h.w.updateXkbKeymap(); err != nil {
   228  					panic(err)
   229  				}
   230  			case C.XkbStateNotify:
   231  				state := (*C.XkbStateNotifyEvent)(unsafe.Pointer(xev))
   232  				h.w.xkb.UpdateMask(uint32(state.base_mods), uint32(state.latched_mods), uint32(state.locked_mods),
   233  					uint32(state.base_group), uint32(state.latched_group), uint32(state.locked_group))
   234  			}
   235  		case C.KeyPress:
   236  			kevt := (*C.XKeyPressedEvent)(unsafe.Pointer(xev))
   237  			for _, e := range h.w.xkb.DispatchKey(uint32(kevt.keycode)) {
   238  				w.w.Event(e)
   239  			}
   240  		case C.KeyRelease:
   241  		case C.ButtonPress, C.ButtonRelease:
   242  			bevt := (*C.XButtonEvent)(unsafe.Pointer(xev))
   243  			ev := pointer.Event{
   244  				Type:   pointer.Press,
   245  				Source: pointer.Mouse,
   246  				Position: f32.Point{
   247  					X: float32(bevt.x),
   248  					Y: float32(bevt.y),
   249  				},
   250  				Time: time.Duration(bevt.time) * time.Millisecond,
   251  			}
   252  			if bevt._type == C.ButtonRelease {
   253  				ev.Type = pointer.Release
   254  			}
   255  			var btn pointer.Buttons
   256  			const scrollScale = 10
   257  			switch bevt.button {
   258  			case C.Button1:
   259  				btn = pointer.ButtonLeft
   260  			case C.Button2:
   261  				btn = pointer.ButtonMiddle
   262  			case C.Button3:
   263  				btn = pointer.ButtonRight
   264  			case C.Button4:
   265  				// scroll up
   266  				ev.Type = pointer.Move
   267  				ev.Scroll.Y = -scrollScale
   268  			case C.Button5:
   269  				// scroll down
   270  				ev.Type = pointer.Move
   271  				ev.Scroll.Y = +scrollScale
   272  			default:
   273  				continue
   274  			}
   275  			switch _type {
   276  			case C.ButtonPress:
   277  				w.pointerBtns |= btn
   278  			case C.ButtonRelease:
   279  				w.pointerBtns &^= btn
   280  			}
   281  			ev.Buttons = w.pointerBtns
   282  			w.w.Event(ev)
   283  		case C.MotionNotify:
   284  			mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
   285  			w.w.Event(pointer.Event{
   286  				Type:    pointer.Move,
   287  				Source:  pointer.Mouse,
   288  				Buttons: w.pointerBtns,
   289  				Position: f32.Point{
   290  					X: float32(mevt.x),
   291  					Y: float32(mevt.y),
   292  				},
   293  				Time: time.Duration(mevt.time) * time.Millisecond,
   294  			})
   295  		case C.Expose: // update
   296  			// redraw only on the last expose event
   297  			redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0
   298  		case C.FocusIn:
   299  			w.w.Event(key.FocusEvent{Focus: true})
   300  		case C.FocusOut:
   301  			w.w.Event(key.FocusEvent{Focus: false})
   302  		case C.ConfigureNotify: // window configuration change
   303  			cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev))
   304  			w.width = int(cevt.width)
   305  			w.height = int(cevt.height)
   306  			// redraw will be done by a later expose event
   307  		case C.ClientMessage: // extensions
   308  			cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev))
   309  			switch *(*C.long)(unsafe.Pointer(&cevt.data)) {
   310  			case C.long(w.evDelWindow):
   311  				w.dead = true
   312  				return false
   313  			}
   314  		}
   315  	}
   316  	return redraw
   317  }
   318  
   319  var (
   320  	x11Threads sync.Once
   321  )
   322  
   323  func init() {
   324  	x11Driver = newX11Window
   325  }
   326  
   327  func newX11Window(gioWin Callbacks, opts *Options) error {
   328  	var err error
   329  
   330  	pipe := make([]int, 2)
   331  	if err := syscall.Pipe2(pipe, syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil {
   332  		return fmt.Errorf("NewX11Window: failed to create pipe: %w", err)
   333  	}
   334  
   335  	x11Threads.Do(func() {
   336  		if C.XInitThreads() == 0 {
   337  			err = errors.New("x11: threads init failed")
   338  		}
   339  		C.XrmInitialize()
   340  	})
   341  	if err != nil {
   342  		return err
   343  	}
   344  	dpy := C.XOpenDisplay(nil)
   345  	if dpy == nil {
   346  		return errors.New("x11: cannot connect to the X server")
   347  	}
   348  	var major, minor C.int = C.XkbMajorVersion, C.XkbMinorVersion
   349  	var xkbEventBase C.int
   350  	if C.XkbQueryExtension(dpy, nil, &xkbEventBase, nil, &major, &minor) != C.True {
   351  		C.XCloseDisplay(dpy)
   352  		return errors.New("x11: XkbQueryExtension failed")
   353  	}
   354  	const bits = C.uint(C.XkbNewKeyboardNotifyMask | C.XkbMapNotifyMask | C.XkbStateNotifyMask)
   355  	if C.XkbSelectEvents(dpy, C.XkbUseCoreKbd, bits, bits) != C.True {
   356  		C.XCloseDisplay(dpy)
   357  		return errors.New("x11: XkbSelectEvents failed")
   358  	}
   359  	xkb, err := xkb.New()
   360  	if err != nil {
   361  		C.XCloseDisplay(dpy)
   362  		return fmt.Errorf("x11: %v", err)
   363  	}
   364  
   365  	ppsp := x11DetectUIScale(dpy)
   366  	cfg := config{pxPerDp: ppsp, pxPerSp: ppsp}
   367  	swa := C.XSetWindowAttributes{
   368  		event_mask: C.ExposureMask | C.FocusChangeMask | // update
   369  			C.KeyPressMask | C.KeyReleaseMask | // keyboard
   370  			C.ButtonPressMask | C.ButtonReleaseMask | // mouse clicks
   371  			C.PointerMotionMask | // mouse movement
   372  			C.StructureNotifyMask, // resize
   373  		background_pixmap: C.None,
   374  		override_redirect: C.False,
   375  	}
   376  	win := C.XCreateWindow(dpy, C.XDefaultRootWindow(dpy),
   377  		0, 0, C.uint(cfg.Px(opts.Width)), C.uint(cfg.Px(opts.Height)),
   378  		0, C.CopyFromParent, C.InputOutput, nil,
   379  		C.CWEventMask|C.CWBackPixmap|C.CWOverrideRedirect, &swa)
   380  
   381  	w := &x11Window{
   382  		w: gioWin, x: dpy, xw: win,
   383  		width:        cfg.Px(opts.Width),
   384  		height:       cfg.Px(opts.Height),
   385  		cfg:          cfg,
   386  		xkb:          xkb,
   387  		xkbEventBase: xkbEventBase,
   388  	}
   389  	w.notify.read = pipe[0]
   390  	w.notify.write = pipe[1]
   391  
   392  	if err := w.updateXkbKeymap(); err != nil {
   393  		w.destroy()
   394  		return err
   395  	}
   396  
   397  	var hints C.XWMHints
   398  	hints.input = C.True
   399  	hints.flags = C.InputHint
   400  	C.XSetWMHints(dpy, win, &hints)
   401  
   402  	// set the name
   403  	ctitle := C.CString(opts.Title)
   404  	defer C.free(unsafe.Pointer(ctitle))
   405  	C.XStoreName(dpy, win, ctitle)
   406  	// set _NET_WM_NAME as well for UTF-8 support in window title.
   407  	C.XSetTextProperty(dpy, win,
   408  		&C.XTextProperty{
   409  			value:    (*C.uchar)(unsafe.Pointer(ctitle)),
   410  			encoding: w.atom("UTF8_STRING", false),
   411  			format:   8,
   412  			nitems:   C.ulong(len(opts.Title)),
   413  		},
   414  		w.atom("_NET_WM_NAME", false))
   415  
   416  	// extensions
   417  	w.evDelWindow = w.atom("WM_DELETE_WINDOW", false)
   418  	C.XSetWMProtocols(dpy, win, &w.evDelWindow, 1)
   419  
   420  	// make the window visible on the screen
   421  	C.XMapWindow(dpy, win)
   422  
   423  	go func() {
   424  		w.w.SetDriver(w)
   425  		w.setStage(system.StageRunning)
   426  		w.loop()
   427  		w.destroy()
   428  		close(mainDone)
   429  	}()
   430  	return nil
   431  }
   432  
   433  // detectUIScale reports the system UI scale, or 1.0 if it fails.
   434  func x11DetectUIScale(dpy *C.Display) float32 {
   435  	// default fixed DPI value used in most desktop UI toolkits
   436  	const defaultDesktopDPI = 96
   437  	var scale float32 = 1.0
   438  
   439  	// Get actual DPI from X resource Xft.dpi (set by GTK and Qt).
   440  	// This value is entirely based on user preferences and conflates both
   441  	// screen (UI) scaling and font scale.
   442  	rms := C.XResourceManagerString(dpy)
   443  	if rms != nil {
   444  		db := C.XrmGetStringDatabase(rms)
   445  		if db != nil {
   446  			var (
   447  				t *C.char
   448  				v C.XrmValue
   449  			)
   450  			if C.XrmGetResource(db, (*C.char)(unsafe.Pointer(&[]byte("Xft.dpi\x00")[0])),
   451  				(*C.char)(unsafe.Pointer(&[]byte("Xft.Dpi\x00")[0])), &t, &v) != C.False {
   452  				if t != nil && C.GoString(t) == "String" {
   453  					f, err := strconv.ParseFloat(C.GoString(v.addr), 32)
   454  					if err == nil {
   455  						scale = float32(f) / defaultDesktopDPI
   456  					}
   457  				}
   458  			}
   459  			C.XrmDestroyDatabase(db)
   460  		}
   461  	}
   462  
   463  	return scale
   464  }
   465  
   466  func (w *x11Window) updateXkbKeymap() error {
   467  	w.xkb.DestroyKeymapState()
   468  	ctx := (*C.struct_xkb_context)(unsafe.Pointer(w.xkb.Ctx))
   469  	xcb := C.XGetXCBConnection(w.x)
   470  	if xcb == nil {
   471  		return errors.New("x11: XGetXCBConnection failed")
   472  	}
   473  	xkbDevID := C.xkb_x11_get_core_keyboard_device_id(xcb)
   474  	if xkbDevID == -1 {
   475  		return errors.New("x11: xkb_x11_get_core_keyboard_device_id failed")
   476  	}
   477  	keymap := C.xkb_x11_keymap_new_from_device(ctx, xcb, xkbDevID, C.XKB_KEYMAP_COMPILE_NO_FLAGS)
   478  	if keymap == nil {
   479  		return errors.New("x11: xkb_x11_keymap_new_from_device failed")
   480  	}
   481  	state := C.xkb_x11_state_new_from_device(keymap, xcb, xkbDevID)
   482  	if state == nil {
   483  		C.xkb_keymap_unref(keymap)
   484  		return errors.New("x11: xkb_x11_keymap_new_from_device failed")
   485  	}
   486  	w.xkb.SetKeymap(unsafe.Pointer(keymap), unsafe.Pointer(state))
   487  	return nil
   488  }