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