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 }