github.com/rajveermalviya/gamen@v0.1.2-0.20220930195403-9be15877c1aa/internal/xcb/window.go (about) 1 //go:build linux && !android 2 3 package xcb 4 5 /* 6 7 #include <stdlib.h> 8 #include <X11/Xlib-xcb.h> 9 #include <xcb/xinput.h> 10 #include <xcb/xcb_icccm.h> 11 #include <X11/Xcursor/Xcursor.h> 12 13 */ 14 import "C" 15 16 import ( 17 "errors" 18 "math" 19 "sync" 20 "unsafe" 21 22 "github.com/rajveermalviya/gamen/cursors" 23 "github.com/rajveermalviya/gamen/dpi" 24 "github.com/rajveermalviya/gamen/events" 25 "github.com/rajveermalviya/gamen/internal/common/atomicx" 26 "github.com/rajveermalviya/gamen/internal/common/mathx" 27 ) 28 29 type Window struct { 30 d *Display 31 // x window id 32 win C.xcb_window_t 33 // we allow destroy function to be called multiple 34 // times, but in reality we run it once 35 destroyOnce sync.Once 36 mu sync.Mutex 37 38 // state 39 previousCursorIcon cursors.Icon // shared mutex 40 currentCursorIcon cursors.Icon // shared mutex 41 42 size dpi.PhysicalSize[uint32] // non-shared 43 cursorPos dpi.PhysicalPosition[float64] // non-shared 44 45 // callbacks 46 resizedCb atomicx.Pointer[events.WindowResizedCallback] 47 closeRequestedCb atomicx.Pointer[events.WindowCloseRequestedCallback] 48 focusedCb atomicx.Pointer[events.WindowFocusedCallback] 49 unfocusedCb atomicx.Pointer[events.WindowUnfocusedCallback] 50 cursorEnteredCb atomicx.Pointer[events.WindowCursorEnteredCallback] 51 cursorLeftCb atomicx.Pointer[events.WindowCursorLeftCallback] 52 cursorMovedCb atomicx.Pointer[events.WindowCursorMovedCallback] 53 mouseWheelCb atomicx.Pointer[events.WindowMouseScrollCallback] 54 mouseInputCb atomicx.Pointer[events.WindowMouseInputCallback] 55 modifiersChangedCb atomicx.Pointer[events.WindowModifiersChangedCallback] 56 keyboardInputCb atomicx.Pointer[events.WindowKeyboardInputCallback] 57 receivedCharacterCb atomicx.Pointer[events.WindowReceivedCharacterCallback] 58 } 59 60 func NewWindow(d *Display) (*Window, error) { 61 win := d.l.xcb_generate_id(d.xcbConn) 62 63 // create window 64 { 65 var mask C.uint32_t = C.XCB_CW_BACK_PIXMAP | 66 C.XCB_CW_BORDER_PIXEL | 67 C.XCB_CW_BIT_GRAVITY | 68 C.XCB_CW_EVENT_MASK 69 values := [...]C.uint32_t{ 70 C.XCB_BACK_PIXMAP_NONE, // none background 71 d.screens[0].xcbScreen.black_pixel, // black border 72 C.XCB_GRAVITY_NORTH_WEST, // shift inner window from north west 73 C.XCB_EVENT_MASK_STRUCTURE_NOTIFY | // listen for resize, keypress, keyrelease 74 C.XCB_EVENT_MASK_KEY_PRESS | // (we listen for other input events via xinput) 75 C.XCB_EVENT_MASK_KEY_RELEASE, 76 } 77 78 cookie := d.l.xcb_create_window_checked(d.xcbConn, 79 0, 80 win, 81 d.screens[0].xcbScreen.root, 82 0, 0, 83 640, 480, 84 0, 85 C.XCB_WINDOW_CLASS_INPUT_OUTPUT, 86 d.screens[0].xcbScreen.root_visual, 87 mask, unsafe.Pointer(&values), 88 ) 89 err := d.l.xcb_request_check(d.xcbConn, cookie) 90 if err != nil { 91 defer C.free(unsafe.Pointer(err)) 92 return nil, errors.New("unable to create window") 93 } 94 } 95 96 // opt into window close event 97 { 98 wmDeleteWindow := d.wmDeleteWindow 99 d.l.xcb_change_property( 100 d.xcbConn, 101 C.XCB_PROP_MODE_REPLACE, 102 win, 103 d.wmProtocols, 104 C.XCB_ATOM_ATOM, 105 32, 106 1, 107 unsafe.Pointer(&wmDeleteWindow), 108 ) 109 } 110 111 // select xinput events 112 { 113 var mask struct { 114 head C.xcb_input_event_mask_t 115 mask C.xcb_input_xi_event_mask_t 116 } 117 mask.head.deviceid = C.XCB_INPUT_DEVICE_ALL_MASTER 118 mask.head.mask_len = C.uint16_t(unsafe.Sizeof(mask.mask) / unsafe.Sizeof(C.uint32_t(0))) 119 120 mask.mask = C.XCB_INPUT_XI_EVENT_MASK_MOTION | 121 C.XCB_INPUT_XI_EVENT_MASK_ENTER | 122 C.XCB_INPUT_XI_EVENT_MASK_LEAVE | 123 C.XCB_INPUT_XI_EVENT_MASK_FOCUS_IN | 124 C.XCB_INPUT_XI_EVENT_MASK_FOCUS_OUT | 125 C.XCB_INPUT_XI_EVENT_MASK_BUTTON_PRESS | 126 C.XCB_INPUT_XI_EVENT_MASK_BUTTON_RELEASE 127 128 d.l.xcb_input_xi_select_events(d.xcbConn, win, 1, &mask.head) 129 } 130 131 w := &Window{ 132 d: d, 133 win: win, 134 currentCursorIcon: cursors.Default, 135 } 136 137 w.setDecorations(d.motifWmHints, true) 138 139 // map window 140 { 141 cookie := d.l.xcb_map_window_checked(d.xcbConn, win) 142 err := d.l.xcb_request_check(d.xcbConn, cookie) 143 if err != nil { 144 defer C.free(unsafe.Pointer(err)) 145 return nil, errors.New("unable to map window") 146 } 147 } 148 149 d.l.xcb_flush(d.xcbConn) 150 151 d.windows[win] = w 152 return w, nil 153 } 154 155 func (w *Window) XcbConnection() unsafe.Pointer { 156 return unsafe.Pointer(w.d.xcbConn) 157 } 158 func (w *Window) XcbWindow() uint32 { 159 return uint32(w.win) 160 } 161 162 func (w *Window) XlibDisplay() unsafe.Pointer { 163 return unsafe.Pointer(w.d.xlibDisp) 164 } 165 func (w *Window) XlibWindow() uint32 { 166 return uint32(w.win) 167 } 168 169 func (w *Window) SetTitle(title string) { 170 titlePtr := C.CString(title) 171 defer C.free(unsafe.Pointer(titlePtr)) 172 173 w.d.l.xcb_change_property( 174 w.d.xcbConn, 175 C.XCB_PROP_MODE_REPLACE, 176 w.win, 177 C.XCB_ATOM_WM_NAME, 178 C.XCB_ATOM_STRING, 8, 179 C.uint32_t(len(title)+1), 180 unsafe.Pointer(titlePtr), 181 ) 182 } 183 184 func (w *Window) Destroy() { 185 w.destroyOnce.Do(func() { 186 w.resizedCb.Store(nil) 187 w.closeRequestedCb.Store(nil) 188 w.focusedCb.Store(nil) 189 w.unfocusedCb.Store(nil) 190 w.cursorEnteredCb.Store(nil) 191 w.cursorLeftCb.Store(nil) 192 w.cursorMovedCb.Store(nil) 193 w.mouseWheelCb.Store(nil) 194 w.mouseInputCb.Store(nil) 195 w.modifiersChangedCb.Store(nil) 196 w.keyboardInputCb.Store(nil) 197 w.receivedCharacterCb.Store(nil) 198 199 if _, ok := w.d.windows[w.win]; ok { 200 w.d.windows[w.win] = nil 201 delete(w.d.windows, w.win) 202 } 203 204 w.d.l.xcb_destroy_window(w.d.xcbConn, w.win) 205 w.d.l.xcb_flush(w.d.xcbConn) 206 }) 207 } 208 209 func (w *Window) InnerSize() dpi.PhysicalSize[uint32] { 210 r := w.d.l.xcb_get_geometry_reply(w.d.xcbConn, w.win) 211 if r == nil { 212 return dpi.PhysicalSize[uint32]{} 213 } 214 215 defer C.free(unsafe.Pointer(r)) 216 return dpi.PhysicalSize[uint32]{ 217 Width: uint32(r.width), 218 Height: uint32(r.height), 219 } 220 } 221 222 func (w *Window) SetInnerSize(size dpi.Size[uint32]) { 223 physicalSize := size.ToPhysical(1) 224 225 var mask C.uint16_t = C.XCB_CONFIG_WINDOW_WIDTH | C.XCB_CONFIG_WINDOW_HEIGHT 226 values := [...]uint32{ 227 mathx.Max(1, mathx.Min(physicalSize.Width, math.MaxInt16)), 228 mathx.Max(1, mathx.Min(physicalSize.Height, math.MaxInt16)), 229 } 230 231 w.d.l.xcb_configure_window(w.d.xcbConn, w.win, mask, unsafe.Pointer(&values)) 232 w.d.l.xcb_flush(w.d.xcbConn) 233 } 234 235 func (w *Window) SetMinInnerSize(size dpi.Size[uint32]) { 236 physicalSize := size.ToPhysical(1) 237 238 var hints C.xcb_size_hints_t 239 w.d.l.xcb_icccm_get_wm_normal_hints_reply( 240 w.d.xcbConn, 241 w.win, 242 &hints, 243 ) 244 245 w.d.l.xcb_icccm_size_hints_set_min_size( 246 &hints, 247 C.int32_t(mathx.Min(physicalSize.Width, math.MaxInt16)), 248 C.int32_t(mathx.Min(physicalSize.Height, math.MaxInt16)), 249 ) 250 251 w.d.l.xcb_icccm_set_wm_normal_hints(w.d.xcbConn, w.win, &hints) 252 } 253 254 func (w *Window) SetMaxInnerSize(size dpi.Size[uint32]) { 255 physicalSize := size.ToPhysical(1) 256 257 var hints C.xcb_size_hints_t 258 w.d.l.xcb_icccm_get_wm_normal_hints_reply( 259 w.d.xcbConn, 260 w.win, 261 &hints, 262 ) 263 264 w.d.l.xcb_icccm_size_hints_set_max_size( 265 &hints, 266 C.int32_t(mathx.Min(physicalSize.Width, math.MaxInt16)), 267 C.int32_t(mathx.Min(physicalSize.Height, math.MaxInt16)), 268 ) 269 270 w.d.l.xcb_icccm_set_wm_normal_hints(w.d.xcbConn, w.win, &hints) 271 } 272 273 func (w *Window) Maximized() bool { 274 r := w.d.l.xcb_get_property_reply( 275 w.d.xcbConn, 276 0, 277 w.win, 278 w.d.netWmState, 279 C.XCB_ATOM_ATOM, 280 0, 281 1024, 282 ) 283 defer C.free(unsafe.Pointer(r)) 284 285 dataSlice := unsafe.Slice( 286 (*C.xcb_atom_t)(w.d.l.xcb_get_property_value(r)), 287 uintptr(w.d.l.xcb_get_property_value_length(r))/unsafe.Sizeof(C.xcb_atom_t(0)), 288 ) 289 290 var hasMaximizedHorz, hasMaximizedVert bool 291 for _, atom := range dataSlice { 292 if !hasMaximizedHorz && atom == w.d.netWmStateMaximizedHorz { 293 hasMaximizedHorz = true 294 } 295 if !hasMaximizedVert && atom == w.d.netWmStateMaximizedVert { 296 hasMaximizedVert = true 297 } 298 if hasMaximizedHorz && hasMaximizedVert { 299 return true 300 } 301 } 302 303 return hasMaximizedHorz && hasMaximizedVert 304 } 305 306 func (w *Window) SetMinimized() { 307 event := C.xcb_client_message_event_t{ 308 response_type: C.XCB_CLIENT_MESSAGE, 309 format: 32, 310 sequence: 0, 311 window: w.win, 312 _type: w.d.wmChangeState, 313 } 314 315 data := (*[5]uint32)(unsafe.Pointer(&event.data)) 316 data[0] = C.XCB_ICCCM_WM_STATE_ICONIC 317 318 w.d.l.xcb_send_event( 319 w.d.xcbConn, 320 0, 321 w.d.screens[0].xcbScreen.root, 322 C.XCB_EVENT_MASK_STRUCTURE_NOTIFY|C.XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, 323 (*C.char)(unsafe.Pointer(&event)), 324 ) 325 326 var hints C.xcb_icccm_wm_hints_t 327 if w.d.l.xcb_icccm_get_wm_hints_reply( 328 w.d.xcbConn, 329 w.win, 330 &hints, 331 ) != 0 { 332 w.d.l.xcb_icccm_wm_hints_set_iconic(&hints) 333 w.d.l.xcb_icccm_set_wm_hints(w.d.xcbConn, w.win, &hints) 334 } 335 w.d.l.xcb_flush(w.d.xcbConn) 336 } 337 338 func (w *Window) SetMaximized(maximized bool) { 339 event := C.xcb_client_message_event_t{ 340 response_type: C.XCB_CLIENT_MESSAGE, 341 format: 32, 342 sequence: 0, 343 window: w.win, 344 _type: w.d.netWmState, 345 } 346 347 data := (*[5]uint32)(unsafe.Pointer(&event.data)) 348 if maximized { 349 data[0] = 1 350 } else { 351 data[0] = 0 352 } 353 data[1] = uint32(w.d.netWmStateMaximizedHorz) 354 data[2] = uint32(w.d.netWmStateMaximizedVert) 355 356 w.d.l.xcb_send_event( 357 w.d.xcbConn, 358 0, 359 w.d.screens[0].xcbScreen.root, 360 C.XCB_EVENT_MASK_STRUCTURE_NOTIFY|C.XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, 361 (*C.char)(unsafe.Pointer(&event)), 362 ) 363 w.d.l.xcb_flush(w.d.xcbConn) 364 } 365 366 func (w *Window) SetCursorIcon(icon cursors.Icon) { 367 if icon == 0 { 368 // 0 is internally used to hide cursor, 369 // users should instead use SetCursorVisible() 370 // so make this no-op 371 return 372 } 373 374 w.mu.Lock() 375 w.currentCursorIcon = icon 376 w.mu.Unlock() 377 378 cursor := w.d.loadCursorIcon(icon) 379 w.d.l.xcb_change_window_attributes( 380 w.d.xcbConn, 381 w.win, 382 C.XCB_CW_CURSOR, 383 unsafe.Pointer(&cursor), 384 ) 385 w.d.l.xcb_flush(w.d.xcbConn) 386 } 387 388 func (w *Window) SetCursorVisible(visible bool) { 389 if visible { 390 w.mu.Lock() 391 if w.currentCursorIcon == 0 { 392 w.currentCursorIcon = w.previousCursorIcon 393 currentCursor := w.currentCursorIcon 394 w.mu.Unlock() 395 396 cursor := w.d.loadCursorIcon(currentCursor) 397 w.d.l.xcb_change_window_attributes( 398 w.d.xcbConn, 399 w.win, 400 C.XCB_CW_CURSOR, 401 unsafe.Pointer(&cursor), 402 ) 403 w.d.l.xcb_flush(w.d.xcbConn) 404 } else { 405 w.mu.Unlock() 406 } 407 } else { 408 w.mu.Lock() 409 if w.currentCursorIcon != 0 { 410 w.previousCursorIcon = w.currentCursorIcon 411 w.currentCursorIcon = 0 412 w.mu.Unlock() 413 414 cursor := w.d.loadCursorIcon(0) 415 w.d.l.xcb_change_window_attributes( 416 w.d.xcbConn, 417 w.win, 418 C.XCB_CW_CURSOR, 419 unsafe.Pointer(&cursor), 420 ) 421 w.d.l.xcb_flush(w.d.xcbConn) 422 } else { 423 w.mu.Unlock() 424 } 425 } 426 } 427 428 func (w *Window) SetFullscreen(fullscreen bool) { 429 event := C.xcb_client_message_event_t{ 430 response_type: C.XCB_CLIENT_MESSAGE, 431 format: 32, 432 sequence: 0, 433 window: w.win, 434 _type: w.d.netWmState, 435 } 436 437 data := (*[5]uint32)(unsafe.Pointer(&event.data)) 438 if fullscreen { 439 data[0] = 1 440 } else { 441 data[0] = 0 442 } 443 data[1] = uint32(w.d.netWmStateFullscreen) 444 445 w.d.l.xcb_send_event( 446 w.d.xcbConn, 447 0, 448 w.d.screens[0].xcbScreen.root, 449 C.XCB_EVENT_MASK_STRUCTURE_NOTIFY|C.XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, 450 (*C.char)(unsafe.Pointer(&event)), 451 ) 452 w.d.l.xcb_flush(w.d.xcbConn) 453 } 454 func (w *Window) Fullscreen() bool { 455 r := w.d.l.xcb_get_property_reply( 456 w.d.xcbConn, 457 0, 458 w.win, 459 w.d.netWmState, 460 C.XCB_ATOM_ATOM, 461 0, 462 1024, 463 ) 464 defer C.free(unsafe.Pointer(r)) 465 466 dataSlice := unsafe.Slice( 467 (*C.xcb_atom_t)(w.d.l.xcb_get_property_value(r)), 468 uintptr(w.d.l.xcb_get_property_value_length(r))/unsafe.Sizeof(C.xcb_atom_t(0)), 469 ) 470 471 for _, atom := range dataSlice { 472 if atom == w.d.netWmStateFullscreen { 473 return true 474 } 475 } 476 return false 477 } 478 479 func (w *Window) DragWindow() { 480 const _NET_WM_MOVERESIZE_MOVE = 8 481 482 w.d.mu.Lock() 483 mousePosX := w.d.lastMousePositionX 484 mousePosY := w.d.lastMousePositionY 485 w.d.mu.Unlock() 486 487 r := w.d.l.xcb_translate_coordinates_reply( 488 w.d.xcbConn, 489 w.win, 490 w.d.screens[0].xcbScreen.root, 491 C.int16_t(fixed1616ToFloat64(mousePosX)), 492 C.int16_t(fixed1616ToFloat64(mousePosY)), 493 ) 494 495 var posX, posY C.int16_t 496 if r != nil { 497 defer C.free(unsafe.Pointer(r)) 498 499 posX = r.dst_x 500 posY = r.dst_y 501 } 502 503 event := C.xcb_client_message_event_t{ 504 response_type: C.XCB_CLIENT_MESSAGE, 505 format: 32, 506 sequence: 0, 507 window: w.win, 508 _type: w.d.netWmMoveResize, 509 } 510 511 data := (*[5]uint32)(unsafe.Pointer(&event.data)) 512 data[0] = uint32(posX) 513 data[1] = uint32(posY) 514 data[2] = _NET_WM_MOVERESIZE_MOVE 515 data[3] = C.XCB_BUTTON_INDEX_1 516 517 w.d.l.xcb_ungrab_pointer(w.d.xcbConn, C.XCB_CURRENT_TIME) 518 w.d.l.xcb_send_event( 519 w.d.xcbConn, 520 0, 521 w.d.screens[0].xcbScreen.root, 522 C.XCB_EVENT_MASK_STRUCTURE_NOTIFY|C.XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, 523 (*C.char)(unsafe.Pointer(&event)), 524 ) 525 } 526 527 func (w *Window) setDecorations(motifWmHints C.xcb_atom_t, decorate bool) { 528 var hints struct { 529 flags uint32 530 functions uint32 531 decorations uint32 532 inputMode int32 533 status uint32 534 } 535 536 hints.flags = 2 // MWM_HINTS_DECORATIONS 537 if decorate { 538 hints.decorations = 1 539 } else { 540 hints.decorations = 0 541 } 542 543 w.d.l.xcb_change_property( 544 w.d.xcbConn, 545 C.XCB_PROP_MODE_REPLACE, 546 w.win, 547 motifWmHints, 548 motifWmHints, 549 32, 550 5, 551 unsafe.Pointer(&hints), 552 ) 553 } 554 555 func (w *Window) SetDecorations(decorate bool) { 556 w.setDecorations(w.d.motifWmHints, decorate) 557 } 558 559 func (w *Window) Decorated() bool { 560 type wmHints struct { 561 flags uint32 562 functions uint32 563 decorations uint32 564 inputMode int32 565 status uint32 566 } 567 568 r := w.d.l.xcb_get_property_reply( 569 w.d.xcbConn, 570 0, 571 w.win, 572 w.d.motifWmHints, 573 w.d.motifWmHints, 574 0, 575 C.uint32_t(unsafe.Sizeof(wmHints{})), 576 ) 577 defer C.free(unsafe.Pointer(r)) 578 579 var hints wmHints 580 if w.d.l.xcb_get_property_value_length(r) == C.int(unsafe.Sizeof(wmHints{})) { 581 if v := (*wmHints)(w.d.l.xcb_get_property_value(r)); v != nil { 582 hints = *v 583 } 584 } 585 586 if hints.decorations == 0 { 587 return false 588 } 589 return true 590 } 591 592 func (w *Window) SetCloseRequestedCallback(cb events.WindowCloseRequestedCallback) { 593 w.closeRequestedCb.Store(&cb) 594 } 595 func (w *Window) SetResizedCallback(cb events.WindowResizedCallback) { 596 w.resizedCb.Store(&cb) 597 } 598 func (w *Window) SetFocusedCallback(cb events.WindowFocusedCallback) { 599 w.focusedCb.Store(&cb) 600 } 601 func (w *Window) SetUnfocusedCallback(cb events.WindowUnfocusedCallback) { 602 w.unfocusedCb.Store(&cb) 603 } 604 func (w *Window) SetCursorEnteredCallback(cb events.WindowCursorEnteredCallback) { 605 w.cursorEnteredCb.Store(&cb) 606 } 607 func (w *Window) SetCursorLeftCallback(cb events.WindowCursorLeftCallback) { 608 w.cursorLeftCb.Store(&cb) 609 } 610 func (w *Window) SetCursorMovedCallback(cb events.WindowCursorMovedCallback) { 611 w.cursorMovedCb.Store(&cb) 612 } 613 func (w *Window) SetMouseScrollCallback(cb events.WindowMouseScrollCallback) { 614 w.mouseWheelCb.Store(&cb) 615 } 616 func (w *Window) SetMouseInputCallback(cb events.WindowMouseInputCallback) { 617 w.mouseInputCb.Store(&cb) 618 } 619 func (w *Window) SetTouchInputCallback(cb events.WindowTouchInputCallback) { 620 // TODO: 621 } 622 func (w *Window) SetModifiersChangedCallback(cb events.WindowModifiersChangedCallback) { 623 w.modifiersChangedCb.Store(&cb) 624 } 625 func (w *Window) SetKeyboardInputCallback(cb events.WindowKeyboardInputCallback) { 626 w.keyboardInputCb.Store(&cb) 627 } 628 func (w *Window) SetReceivedCharacterCallback(cb events.WindowReceivedCharacterCallback) { 629 w.receivedCharacterCb.Store(&cb) 630 }