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