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