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