github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/gio/app/internal/window/os_x11.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 // +build linux,!android,!nox11 freebsd 4 5 package window 6 7 /* 8 #cgo LDFLAGS: -lX11 -lxkbcommon -lxkbcommon-x11 -lX11-xcb 9 #include <stdlib.h> 10 #include <locale.h> 11 #include <X11/Xlib.h> 12 #include <X11/Xatom.h> 13 #include <X11/Xutil.h> 14 #include <X11/Xresource.h> 15 #include <X11/XKBlib.h> 16 #include <X11/Xlib-xcb.h> 17 #include <xkbcommon/xkbcommon-x11.h> 18 19 */ 20 import "C" 21 import ( 22 "errors" 23 "fmt" 24 "image" 25 "strconv" 26 "sync" 27 "time" 28 "unsafe" 29 30 "github.com/gop9/olt/gio/f32" 31 "github.com/gop9/olt/gio/io/key" 32 "github.com/gop9/olt/gio/io/pointer" 33 "github.com/gop9/olt/gio/io/system" 34 35 "github.com/gop9/olt/gio/app/internal/xkb" 36 syscall "golang.org/x/sys/unix" 37 ) 38 39 type x11Window struct { 40 w Callbacks 41 x *C.Display 42 xkb *xkb.Context 43 xkbEventBase C.int 44 xw C.Window 45 46 evDelWindow C.Atom 47 stage system.Stage 48 cfg config 49 width int 50 height int 51 notify struct { 52 read, write int 53 } 54 dead bool 55 56 mu sync.Mutex 57 animating bool 58 59 pointerBtns pointer.Buttons 60 } 61 62 func (w *x11Window) SetAnimating(anim bool) { 63 w.mu.Lock() 64 w.animating = anim 65 w.mu.Unlock() 66 if anim { 67 w.wakeup() 68 } 69 } 70 71 func (w *x11Window) ShowTextInput(show bool) {} 72 73 var x11OneByte = make([]byte, 1) 74 75 func (w *x11Window) wakeup() { 76 if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN { 77 panic(fmt.Errorf("failed to write to pipe: %v", err)) 78 } 79 } 80 81 func (w *x11Window) display() *C.Display { 82 return w.x 83 } 84 85 func (w *x11Window) window() (C.Window, int, int) { 86 return w.xw, w.width, w.height 87 } 88 89 func (w *x11Window) setStage(s system.Stage) { 90 if s == w.stage { 91 return 92 } 93 w.stage = s 94 w.w.Event(system.StageEvent{s}) 95 } 96 97 func (w *x11Window) loop() { 98 h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)} 99 xfd := C.XConnectionNumber(w.x) 100 101 // Poll for events and notifications. 102 pollfds := []syscall.PollFd{ 103 {Fd: int32(xfd), Events: syscall.POLLIN | syscall.POLLERR}, 104 {Fd: int32(w.notify.read), Events: syscall.POLLIN | syscall.POLLERR}, 105 } 106 xEvents := &pollfds[0].Revents 107 // Plenty of room for a backlog of notifications. 108 buf := make([]byte, 100) 109 110 loop: 111 for !w.dead { 112 var syn, redraw bool 113 // Check for pending draw events before checking animation or blocking. 114 // This fixes an issue on Xephyr where on startup XPending() > 0 but 115 // poll will still block. This also prevents no-op calls to poll. 116 if syn = h.handleEvents(); !syn { 117 w.mu.Lock() 118 animating := w.animating 119 w.mu.Unlock() 120 if animating { 121 redraw = true 122 } else { 123 // Clear poll events. 124 *xEvents = 0 125 // Wait for X event or gio notification. 126 if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR { 127 panic(fmt.Errorf("x11 loop: poll failed: %w", err)) 128 } 129 switch { 130 case *xEvents&syscall.POLLIN != 0: 131 syn = h.handleEvents() 132 if w.dead { 133 break loop 134 } 135 case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0: 136 break loop 137 } 138 } 139 } 140 // Clear notifications. 141 for { 142 _, err := syscall.Read(w.notify.read, buf) 143 if err == syscall.EAGAIN { 144 break 145 } 146 if err != nil { 147 panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err)) 148 } 149 redraw = true 150 } 151 152 if redraw || syn { 153 w.cfg.now = time.Now() 154 w.w.Event(FrameEvent{ 155 FrameEvent: system.FrameEvent{ 156 Size: image.Point{ 157 X: w.width, 158 Y: w.height, 159 }, 160 Config: &w.cfg, 161 }, 162 Sync: syn, 163 }) 164 } 165 } 166 w.w.Event(system.DestroyEvent{Err: nil}) 167 } 168 169 func (w *x11Window) destroy() { 170 if w.notify.write != 0 { 171 syscall.Close(w.notify.write) 172 w.notify.write = 0 173 } 174 if w.notify.read != 0 { 175 syscall.Close(w.notify.read) 176 w.notify.read = 0 177 } 178 if w.xkb != nil { 179 w.xkb.Destroy() 180 w.xkb = nil 181 } 182 C.XDestroyWindow(w.x, w.xw) 183 C.XCloseDisplay(w.x) 184 } 185 186 // atom is a wrapper around XInternAtom. Callers should cache the result 187 // in order to limit round-trips to the X server. 188 // 189 func (w *x11Window) atom(name string, onlyIfExists bool) C.Atom { 190 cname := C.CString(name) 191 defer C.free(unsafe.Pointer(cname)) 192 flag := C.Bool(C.False) 193 if onlyIfExists { 194 flag = C.True 195 } 196 return C.XInternAtom(w.x, cname, flag) 197 } 198 199 // x11EventHandler wraps static variables for the main event loop. 200 // Its sole purpose is to prevent heap allocation and reduce clutter 201 // in x11window.loop. 202 // 203 type x11EventHandler struct { 204 w *x11Window 205 text []byte 206 xev *C.XEvent 207 status C.Status 208 keysym C.KeySym 209 } 210 211 // handleEvents returns true if the window needs to be redrawn. 212 // 213 func (h *x11EventHandler) handleEvents() bool { 214 w := h.w 215 xev := h.xev 216 redraw := false 217 for C.XPending(w.x) != 0 { 218 C.XNextEvent(w.x, xev) 219 if C.XFilterEvent(xev, C.None) == C.True { 220 continue 221 } 222 switch _type := (*C.XAnyEvent)(unsafe.Pointer(xev))._type; _type { 223 case h.w.xkbEventBase: 224 xkbEvent := (*C.XkbAnyEvent)(unsafe.Pointer(xev)) 225 switch xkbEvent.xkb_type { 226 case C.XkbNewKeyboardNotify, C.XkbMapNotify: 227 if err := h.w.updateXkbKeymap(); err != nil { 228 panic(err) 229 } 230 case C.XkbStateNotify: 231 state := (*C.XkbStateNotifyEvent)(unsafe.Pointer(xev)) 232 h.w.xkb.UpdateMask(uint32(state.base_mods), uint32(state.latched_mods), uint32(state.locked_mods), 233 uint32(state.base_group), uint32(state.latched_group), uint32(state.locked_group)) 234 } 235 case C.KeyPress: 236 kevt := (*C.XKeyPressedEvent)(unsafe.Pointer(xev)) 237 for _, e := range h.w.xkb.DispatchKey(uint32(kevt.keycode)) { 238 w.w.Event(e) 239 } 240 case C.KeyRelease: 241 case C.ButtonPress, C.ButtonRelease: 242 bevt := (*C.XButtonEvent)(unsafe.Pointer(xev)) 243 ev := pointer.Event{ 244 Type: pointer.Press, 245 Source: pointer.Mouse, 246 Position: f32.Point{ 247 X: float32(bevt.x), 248 Y: float32(bevt.y), 249 }, 250 Time: time.Duration(bevt.time) * time.Millisecond, 251 } 252 if bevt._type == C.ButtonRelease { 253 ev.Type = pointer.Release 254 } 255 var btn pointer.Buttons 256 const scrollScale = 10 257 switch bevt.button { 258 case C.Button1: 259 btn = pointer.ButtonLeft 260 case C.Button2: 261 btn = pointer.ButtonMiddle 262 case C.Button3: 263 btn = pointer.ButtonRight 264 case C.Button4: 265 // scroll up 266 ev.Type = pointer.Move 267 ev.Scroll.Y = -scrollScale 268 case C.Button5: 269 // scroll down 270 ev.Type = pointer.Move 271 ev.Scroll.Y = +scrollScale 272 default: 273 continue 274 } 275 switch _type { 276 case C.ButtonPress: 277 w.pointerBtns |= btn 278 case C.ButtonRelease: 279 w.pointerBtns &^= btn 280 } 281 ev.Buttons = w.pointerBtns 282 w.w.Event(ev) 283 case C.MotionNotify: 284 mevt := (*C.XMotionEvent)(unsafe.Pointer(xev)) 285 w.w.Event(pointer.Event{ 286 Type: pointer.Move, 287 Source: pointer.Mouse, 288 Buttons: w.pointerBtns, 289 Position: f32.Point{ 290 X: float32(mevt.x), 291 Y: float32(mevt.y), 292 }, 293 Time: time.Duration(mevt.time) * time.Millisecond, 294 }) 295 case C.Expose: // update 296 // redraw only on the last expose event 297 redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0 298 case C.FocusIn: 299 w.w.Event(key.FocusEvent{Focus: true}) 300 case C.FocusOut: 301 w.w.Event(key.FocusEvent{Focus: false}) 302 case C.ConfigureNotify: // window configuration change 303 cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev)) 304 w.width = int(cevt.width) 305 w.height = int(cevt.height) 306 // redraw will be done by a later expose event 307 case C.ClientMessage: // extensions 308 cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev)) 309 switch *(*C.long)(unsafe.Pointer(&cevt.data)) { 310 case C.long(w.evDelWindow): 311 w.dead = true 312 return false 313 } 314 } 315 } 316 return redraw 317 } 318 319 var ( 320 x11Threads sync.Once 321 ) 322 323 func init() { 324 x11Driver = newX11Window 325 } 326 327 func newX11Window(gioWin Callbacks, opts *Options) error { 328 var err error 329 330 pipe := make([]int, 2) 331 if err := syscall.Pipe2(pipe, syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil { 332 return fmt.Errorf("NewX11Window: failed to create pipe: %w", err) 333 } 334 335 x11Threads.Do(func() { 336 if C.XInitThreads() == 0 { 337 err = errors.New("x11: threads init failed") 338 } 339 C.XrmInitialize() 340 }) 341 if err != nil { 342 return err 343 } 344 dpy := C.XOpenDisplay(nil) 345 if dpy == nil { 346 return errors.New("x11: cannot connect to the X server") 347 } 348 var major, minor C.int = C.XkbMajorVersion, C.XkbMinorVersion 349 var xkbEventBase C.int 350 if C.XkbQueryExtension(dpy, nil, &xkbEventBase, nil, &major, &minor) != C.True { 351 C.XCloseDisplay(dpy) 352 return errors.New("x11: XkbQueryExtension failed") 353 } 354 const bits = C.uint(C.XkbNewKeyboardNotifyMask | C.XkbMapNotifyMask | C.XkbStateNotifyMask) 355 if C.XkbSelectEvents(dpy, C.XkbUseCoreKbd, bits, bits) != C.True { 356 C.XCloseDisplay(dpy) 357 return errors.New("x11: XkbSelectEvents failed") 358 } 359 xkb, err := xkb.New() 360 if err != nil { 361 C.XCloseDisplay(dpy) 362 return fmt.Errorf("x11: %v", err) 363 } 364 365 ppsp := x11DetectUIScale(dpy) 366 cfg := config{pxPerDp: ppsp, pxPerSp: ppsp} 367 swa := C.XSetWindowAttributes{ 368 event_mask: C.ExposureMask | C.FocusChangeMask | // update 369 C.KeyPressMask | C.KeyReleaseMask | // keyboard 370 C.ButtonPressMask | C.ButtonReleaseMask | // mouse clicks 371 C.PointerMotionMask | // mouse movement 372 C.StructureNotifyMask, // resize 373 background_pixmap: C.None, 374 override_redirect: C.False, 375 } 376 win := C.XCreateWindow(dpy, C.XDefaultRootWindow(dpy), 377 0, 0, C.uint(cfg.Px(opts.Width)), C.uint(cfg.Px(opts.Height)), 378 0, C.CopyFromParent, C.InputOutput, nil, 379 C.CWEventMask|C.CWBackPixmap|C.CWOverrideRedirect, &swa) 380 381 w := &x11Window{ 382 w: gioWin, x: dpy, xw: win, 383 width: cfg.Px(opts.Width), 384 height: cfg.Px(opts.Height), 385 cfg: cfg, 386 xkb: xkb, 387 xkbEventBase: xkbEventBase, 388 } 389 w.notify.read = pipe[0] 390 w.notify.write = pipe[1] 391 392 if err := w.updateXkbKeymap(); err != nil { 393 w.destroy() 394 return err 395 } 396 397 var hints C.XWMHints 398 hints.input = C.True 399 hints.flags = C.InputHint 400 C.XSetWMHints(dpy, win, &hints) 401 402 // set the name 403 ctitle := C.CString(opts.Title) 404 defer C.free(unsafe.Pointer(ctitle)) 405 C.XStoreName(dpy, win, ctitle) 406 // set _NET_WM_NAME as well for UTF-8 support in window title. 407 C.XSetTextProperty(dpy, win, 408 &C.XTextProperty{ 409 value: (*C.uchar)(unsafe.Pointer(ctitle)), 410 encoding: w.atom("UTF8_STRING", false), 411 format: 8, 412 nitems: C.ulong(len(opts.Title)), 413 }, 414 w.atom("_NET_WM_NAME", false)) 415 416 // extensions 417 w.evDelWindow = w.atom("WM_DELETE_WINDOW", false) 418 C.XSetWMProtocols(dpy, win, &w.evDelWindow, 1) 419 420 // make the window visible on the screen 421 C.XMapWindow(dpy, win) 422 423 go func() { 424 w.w.SetDriver(w) 425 w.setStage(system.StageRunning) 426 w.loop() 427 w.destroy() 428 close(mainDone) 429 }() 430 return nil 431 } 432 433 // detectUIScale reports the system UI scale, or 1.0 if it fails. 434 func x11DetectUIScale(dpy *C.Display) float32 { 435 // default fixed DPI value used in most desktop UI toolkits 436 const defaultDesktopDPI = 96 437 var scale float32 = 1.0 438 439 // Get actual DPI from X resource Xft.dpi (set by GTK and Qt). 440 // This value is entirely based on user preferences and conflates both 441 // screen (UI) scaling and font scale. 442 rms := C.XResourceManagerString(dpy) 443 if rms != nil { 444 db := C.XrmGetStringDatabase(rms) 445 if db != nil { 446 var ( 447 t *C.char 448 v C.XrmValue 449 ) 450 if C.XrmGetResource(db, (*C.char)(unsafe.Pointer(&[]byte("Xft.dpi\x00")[0])), 451 (*C.char)(unsafe.Pointer(&[]byte("Xft.Dpi\x00")[0])), &t, &v) != C.False { 452 if t != nil && C.GoString(t) == "String" { 453 f, err := strconv.ParseFloat(C.GoString(v.addr), 32) 454 if err == nil { 455 scale = float32(f) / defaultDesktopDPI 456 } 457 } 458 } 459 C.XrmDestroyDatabase(db) 460 } 461 } 462 463 return scale 464 } 465 466 func (w *x11Window) updateXkbKeymap() error { 467 w.xkb.DestroyKeymapState() 468 ctx := (*C.struct_xkb_context)(unsafe.Pointer(w.xkb.Ctx)) 469 xcb := C.XGetXCBConnection(w.x) 470 if xcb == nil { 471 return errors.New("x11: XGetXCBConnection failed") 472 } 473 xkbDevID := C.xkb_x11_get_core_keyboard_device_id(xcb) 474 if xkbDevID == -1 { 475 return errors.New("x11: xkb_x11_get_core_keyboard_device_id failed") 476 } 477 keymap := C.xkb_x11_keymap_new_from_device(ctx, xcb, xkbDevID, C.XKB_KEYMAP_COMPILE_NO_FLAGS) 478 if keymap == nil { 479 return errors.New("x11: xkb_x11_keymap_new_from_device failed") 480 } 481 state := C.xkb_x11_state_new_from_device(keymap, xcb, xkbDevID) 482 if state == nil { 483 C.xkb_keymap_unref(keymap) 484 return errors.New("x11: xkb_x11_keymap_new_from_device failed") 485 } 486 w.xkb.SetKeymap(unsafe.Pointer(keymap), unsafe.Pointer(state)) 487 return nil 488 }