diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e75fc90c..6be8f729 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,6 +36,18 @@ jobs: - run: nim r -d:useCpu tests/test_cpu_pixels.nim if: matrix.os == 'windows-latest' + - name: Test X11 contexts and mouse capture + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y xvfb xauth libgl1-mesa-dri libglx-mesa0 \ + libxcursor1 libxfixes3 libxtst6 + nim check tests/test_x11.nim + nim c tests/test_x11.nim + xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 ./tests/test_x11 + xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \ + MESA_GL_VERSION_OVERRIDE=3.3 ./tests/test_x11 --unsupported-version + # Build native examples. - run: nim c examples/basic.nim - run: nim c examples/basic_boxy.nim diff --git a/src/windy/platforms/linux/x11.nim b/src/windy/platforms/linux/x11.nim index 5b29e3cd..455e2ef2 100644 --- a/src/windy/platforms/linux/x11.nim +++ b/src/windy/platforms/linux/x11.nim @@ -36,6 +36,7 @@ type prevSize, prevPos: IVec2 handle: XWindow + colormap: Colormap ctx: GlxContext gc: GC ic: XIC @@ -47,6 +48,9 @@ type innerDecorated: bool innerFocused: bool mouseCaptured: bool + capturePos, restorePos, warpPos: IVec2 + warpSerial, mouseSerial: culong + warpPending: bool # XDnD state xdndSource: XWindow @@ -68,6 +72,7 @@ var multiClickRadius*: float = 4 initialized: bool + contextError: bool windows: seq[Window] display: Display @@ -138,7 +143,8 @@ proc initConstants(display: Display) = proc atomIfExist(name: string): Atom = display.XInternAtom(name, 1) -proc handleXError(d: Display, event: ptr XErrorEvent): bool {.cdecl.} = +proc handleXError(d: Display, event: ptr XErrorEvent): cint {.cdecl.} = + ## Reports X11 errors through Windy's exception type. raise WindyError.newException("Error dealing with X11: " & $event.errorCode.Status) @@ -381,7 +387,41 @@ proc queryKeyboardState(): set[0..255] = display.XQueryKeymap(r) return cast[ptr set[0..255]](r.addr)[] +proc warpMouse(window: Window, pos: IVec2) = + ## Tracks warps so their motion events do not become user input. + window.warpPos = pos + window.warpSerial = display.XNextRequest() + window.warpPending = true + display.XWarpPointer( + 0, + window.handle, + 0, + 0, + 0, + 0, + pos.x, + pos.y + ) + display.XFlush() + +proc releaseMouse*(window: Window) = + ## Shows the cursor and restores normal absolute mouse tracking. + if not window.mouseCaptured: + return + window.mouseCaptured = false + display.XUngrabPointer(CurrentTime) + window.warpMouse(window.restorePos) + window.mouseSerial = window.warpSerial + window.mousePos = window.restorePos + window.mousePrevPos = window.restorePos + proc destroy(window: Window) = + ## Releases native resources and any active pointer grab. + window.releaseMouse() + if window.ctx != nil: + if glXGetCurrentContext() == window.ctx: + display.glXMakeCurrent(0, nil) + display.glXDestroyContext(window.ctx) if window.ic != nil: XDestroyIC(window.ic) if window.im != nil: @@ -390,6 +430,8 @@ proc destroy(window: Window) = display.XFreeGC(window.gc) if window.handle != 0: display.XDestroyWindow(window.handle) + if window.colormap != 0: + display.XFreeColormap(window.colormap) if window.xSyncCounter.int != 0: display.XSyncDestroyCounter(window.xSyncCounter) wasMoved window[] @@ -431,6 +473,7 @@ proc `visible=`*(window: Window, v: bool) = if v: display.XMapWindow(window.handle) else: + window.releaseMouse() display.XUnmapWindow(window.handle) blockUntil(window.visible == v) @@ -725,6 +768,102 @@ proc `cursor=`*(window: Window, v: common.Cursor) = window.state.cursor = v window.applyCursor() +proc createContext( + style: WindowStyle, + openglVersion: OpenGLVersion, + msaa: MSAA, + depthBits, stencilBits: int +): tuple[visual: XVisualInfo, context: GlxContext] = + ## Chooses a matching visual and creates the requested OpenGL context. + let + extensions = strutils.splitWhitespace( + $display.glXQueryExtensionsString(display.defaultScreen) + ) + create = cast[GlxCreateContextAttribsProc]( + glXGetProcAddressARB("glXCreateContextAttribsARB") + ) + if "GLX_ARB_create_context" notin extensions or create == nil: + raise WindyError.newException("GLX_ARB_create_context is not supported") + if openglVersion >= OpenGL3Dot2 and + "GLX_ARB_create_context_profile" notin extensions: + raise WindyError.newException("OpenGL core profiles are not supported") + + var + attributes = [ + GlxXRenderable, 1, + GlxDrawableType, GlxWindowBit, + GlxRenderType, GlxRgbaBit, + GlxXVisualType, GlxTrueColor, + GlxDoublebuffer, 1, + GlxRedSize, 8, + GlxGreenSize, 8, + GlxBlueSize, 8, + GlxAlphaSize, 8, + GlxDepthSize, depthBits.cint, + GlxStencilSize, stencilBits.cint, + GlxSampleBuffers, cint(msaa != msaaDisabled), + GlxSamples, msaa.cint, + 0 + ] + count: cint + let configs = display.glXChooseFBConfig( + display.defaultScreen, + attributes[0].addr, + count.addr + ) + if configs == nil: + raise WindyError.newException("No matching OpenGL framebuffer configuration") + defer: + XFree(configs) + + var config: GlxFbConfig + for i in 0 ..< count: + let visual = display.glXGetVisualFromFBConfig(configs[i]) + if visual == nil: + continue + let suitable = style != Transparent or visual.depth == 32 + if suitable: + result.visual = visual[] + config = configs[i] + XFree(visual) + if suitable: + break + if config == nil: + raise WindyError.newException("No matching OpenGL window visual") + + var contextAttributes = @[ + GlxContextMajorVersion, openglVersion.major.cint, + GlxContextMinorVersion, openglVersion.minor.cint + ] + if openglVersion >= OpenGL3Dot2: + contextAttributes.add [GlxContextProfileMask, GlxContextCoreProfileBit] + contextAttributes.add 0 + + proc handleContextError(d: Display, event: ptr XErrorEvent): cint {.cdecl.} = + ## Records context creation errors without unwinding through Xlib. + contextError = true + + display.XSync() + let previousHandler = XSetErrorHandler(handleContextError) + defer: + XSetErrorHandler(previousHandler) + contextError = false + result.context = create( + display, + config, + nil, + 1, + contextAttributes[0].addr + ) + display.XSync() + if contextError or result.context == nil: + if result.context != nil: + display.glXDestroyContext(result.context) + raise WindyError.newException( + "Unable to create OpenGL " & $openglVersion.major & "." & + $openglVersion.minor & " context" + ) + proc newWindow*( title: string, size: IVec2, @@ -736,25 +875,30 @@ proc newWindow*( depthBits = 24, stencilBits = 8 ): Window = - ## Creates a new window. Intitializes Windy if needed. + ## Creates a new window and initializes Windy if needed. init() result = Window() result.innerDecorated = true result.state.runeInputEnabled = true - let root = display.defaultRootWindow - - var vi: XVisualInfo - if style == Transparent: - display.XMatchVisualInfo(display.defaultScreen, 32, TrueColor, vi.addr) - else: - display.XMatchVisualInfo(display.defaultScreen, 24, TrueColor, vi.addr) - # strangely, glx returns nil when -d:danger - # var attribList = [GlxRgba, GlxDepthSize, 24, GlxDoublebuffer] - # vi = display.glXChooseVisual(display.defaultScreen, attribList[0].addr)[] + let + root = display.defaultRootWindow + graphics = createContext( + style, + openglVersion, + msaa, + depthBits, + stencilBits + ) + vi = graphics.visual + result.ctx = graphics.context + var created = false + defer: + if not created: + result.destroy() - let cmap = display.XCreateColormap(root, vi.visual, AllocNone) - var swa = XSetWindowAttributes(colormap: cmap) + result.colormap = display.XCreateColormap(root, vi.visual, AllocNone) + var swa = XSetWindowAttributes(colormap: result.colormap) result.handle = display.XCreateWindow( root, @@ -802,11 +946,6 @@ proc newWindow*( var gcv: XGCValues result.gc = display.XCreateGC(result.handle, GCForeground or GCBackground, gcv.addr) - result.ctx = display.glXCreateContext(vi.addr, nil, 1) - - if result.ctx == nil: - raise WindyError.newException("Error creating OpenGL context") - result.title = title # Set a default cursor @@ -848,6 +987,7 @@ proc newWindow*( result.style = style windows.add result + created = true proc pollEvents(window: Window) = @@ -986,6 +1126,8 @@ proc pollEvents(window: Window) = display.XFlush() of xeFocusIn: + if ev.focus.mode in [NotifyGrab, NotifyUngrab]: + continue if window.innerFocused: return # was duplicated window.innerFocused = true @@ -1005,6 +1147,9 @@ proc pollEvents(window: Window) = window.onFocusChange() of xeFocusOut: + if ev.focus.mode in [NotifyGrab, NotifyUngrab]: + continue + window.releaseMouse() if not window.innerFocused: return # was duplicated window.innerFocused = false @@ -1035,9 +1180,26 @@ proc pollEvents(window: Window) = if window.onFrame != nil: window.onFrame() + of xeUnmap: + window.releaseMouse() + of xeMotion: + if ev.motion.serial < window.mouseSerial: + continue + if window.warpPending and ev.motion.serial >= window.warpSerial: + window.warpPending = false + window.capturePos = window.warpPos + if not window.mouseCaptured: + window.mousePos = window.warpPos + window.mousePrevPos = window.warpPos + if ev.motion.pos == window.warpPos: + continue window.mousePrevPos = window.mousePos - window.mousePos = ev.motion.pos + if window.mouseCaptured: + window.mousePos += ev.motion.pos - window.capturePos + window.capturePos = ev.motion.pos + else: + window.mousePos = ev.motion.pos window.state.mouseInside = true window.perFrame.mouseDelta += window.mousePos - window.mousePrevPos if (window.mousePos - window.lastClickPosition).vec2.length > multiClickRadius: @@ -1156,6 +1318,11 @@ proc pollEvents(window: Window) = else: discard + if window.mouseCaptured and not window.warpPending: + let center = window.size div 2 + if window.capturePos != center: + window.warpMouse(center) + proc mousePos*(window: Window): IVec2 = window.mousePos @@ -1174,17 +1341,70 @@ proc mouseCaptured*(window: Window): bool = window.mouseCaptured proc captureMouse*(window: Window) = - ## Hide the cursor and report unbounded relative mouse deltas. + ## Hides and confines the cursor and reports relative mouse deltas. if window.mouseCaptured: return + if window.closed: + raise newException(WindyError, "Cannot capture a closed window") + for other in windows: + if other.mouseCaptured: + raise newException(WindyError, "Another window has captured the mouse") + var + root, child: XWindow + rootPos, pos: IVec2 + mask: cuint + if display.XQueryPointer( + window.handle, + root.addr, + child.addr, + rootPos.x.addr, + rootPos.y.addr, + pos.x.addr, + pos.y.addr, + mask.addr + ) == 0: + raise newException(WindyError, "Mouse is on a different X11 screen") + let image = XcursorImageCreate(1, 1) + if image == nil: + raise newException(WindyError, "Unable to create an invisible cursor") + defer: + XcursorImageDestroy(image) + image.pixels[] = 0 + image.xhot = 0 + image.yhot = 0 + let cursor = XcursorImageLoadCursor(display, image) + if cursor == 0: + raise newException(WindyError, "Unable to load an invisible cursor") + defer: + display.XFreeCursor(cursor) + let status = display.XGrabPointer( + window.handle, + 0, + (PointerMotionMask or ButtonPressMask or ButtonReleaseMask).cuint, + GrabModeAsync, + GrabModeAsync, + window.handle, + cursor, + CurrentTime + ) + case status + of GrabSuccess: + discard + of AlreadyGrabbed: + raise newException(WindyError, "Mouse is grabbed by another application") + of GrabNotViewable: + raise newException(WindyError, "Cannot capture an unmapped window") + of GrabFrozen: + raise newException(WindyError, "Mouse is frozen by another application") + else: + raise newException(WindyError, "Unable to grab the X11 mouse pointer") window.mouseCaptured = true - warn "Mouse capture is not implemented on Linux yet" - -proc releaseMouse*(window: Window) = - ## Show the cursor and restore normal absolute mouse tracking. - if not window.mouseCaptured: - return - window.mouseCaptured = false + window.restorePos = pos + window.capturePos = pos + window.mousePos = pos + window.mousePrevPos = pos + window.warpMouse(window.size div 2) + window.mouseSerial = window.warpSerial proc scrollDelta*(window: Window): Vec2 = window.perFrame.scrollDelta diff --git a/src/windy/platforms/linux/x11/glx.nim b/src/windy/platforms/linux/x11/glx.nim index b626244f..e99aa8cf 100644 --- a/src/windy/platforms/linux/x11/glx.nim +++ b/src/windy/platforms/linux/x11/glx.nim @@ -1,20 +1,69 @@ import x, xlib -type - GlxContext* = ptr object - const GlxRgba* = 4'i32 GlxDoublebuffer* = 5'i32 + GlxRedSize* = 8'i32 + GlxGreenSize* = 9'i32 + GlxBlueSize* = 10'i32 + GlxAlphaSize* = 11'i32 GlxDepthSize* = 12'i32 + GlxStencilSize* = 13'i32 + GlxXVisualType* = 0x22'i32 + GlxTrueColor* = 0x8002'i32 + GlxDrawableType* = 0x8010'i32 + GlxRenderType* = 0x8011'i32 + GlxXRenderable* = 0x8012'i32 + GlxWindowBit* = 1'i32 + GlxRgbaBit* = 1'i32 + GlxSampleBuffers* = 100000'i32 + GlxSamples* = 100001'i32 + GlxContextMajorVersion* = 0x2091'i32 + GlxContextMinorVersion* = 0x2092'i32 + GlxContextProfileMask* = 0x9126'i32 + GlxContextCoreProfileBit* = 1'i32 + + LibGLX = + when defined(linux): "libGL.so.1" + elif defined(windows): "GL.dll" + elif defined(macosx): "/usr/X11R6/lib/libGL.dylib" + else: "libGL.so" + +type + GlxContext* = ptr object + GlxFbConfig* = ptr object + GlxCreateContextAttribsProc* = proc( + d: Display, + config: GlxFbConfig, + share: GlxContext, + direct: cint, + attributes: ptr cint + ): GlxContext {.cdecl.} + +{.pragma: libglx, cdecl, dynlib: LibGLX, importc.} + +proc glXChooseFBConfig*( + d: Display, + screen: cint, + attributes: ptr cint, + count: ptr cint +): ptr UncheckedArray[GlxFbConfig] {.libglx.} + ## Returns framebuffer configurations matching the supplied attributes. + +proc glXGetVisualFromFBConfig*( + d: Display, + config: GlxFbConfig +): ptr XVisualInfo {.libglx.} + ## Returns the X11 visual associated with a framebuffer configuration. -const libGLX = - when defined(linux): "libGL.so.1" - elif defined(windows): "GL.dll" - elif defined(macosx): "/usr/X11R6/lib/libGL.dylib" - else: "libGL.so" +proc glXQueryExtensionsString*( + d: Display, + screen: cint +): cstring {.libglx.} + ## Returns the GLX extensions supported by this screen. -{.pragma: libglx, cdecl, dynlib: libGLX, importc.} +proc glXGetProcAddressARB*(name: cstring): pointer {.libglx.} + ## Resolves an OpenGL or GLX extension entry point. proc glXChooseVisual*(d: Display, screen: cint, attribList: ptr int32): ptr XVisualInfo {.libglx.} diff --git a/src/windy/platforms/linux/x11/xevent.nim b/src/windy/platforms/linux/x11/xevent.nim index 1147b566..c897b929 100644 --- a/src/windy/platforms/linux/x11/xevent.nim +++ b/src/windy/platforms/linux/x11/xevent.nim @@ -6,7 +6,7 @@ type event: ptr XEvent, p: pointer ): bool {.cdecl.} - ErrorHandleProc* = proc (d: Display, event: ptr XErrorEvent): bool {.cdecl.} + ErrorHandleProc* = proc (d: Display, event: ptr XErrorEvent): cint {.cdecl.} XEventKind* {.size: cint.sizeof.} = enum xeKeyPress = 2 @@ -438,7 +438,8 @@ proc XSendEvent*(d: Display; window: Window, propogate: bool, mask: clong, e: pt proc Xutf8LookupString*(ic: XIC, e: ptr XKeyEvent, buffer: cstring, len: cint, ks: ptr KeySym, status: ptr cint): cint -proc XSetErrorHandler*(handler: ErrorHandleProc) +proc XSetErrorHandler*(handler: ErrorHandleProc): ErrorHandleProc {.discardable.} + ## Replaces the X11 error handler and returns the previous handler. proc XLookupKeysym*(e: ptr XKeyEvent, i: cint): KeySym diff --git a/src/windy/platforms/linux/x11/xlib.nim b/src/windy/platforms/linux/x11/xlib.nim index c84b559e..dd01d013 100644 --- a/src/windy/platforms/linux/x11/xlib.nim +++ b/src/windy/platforms/linux/x11/xlib.nim @@ -224,6 +224,9 @@ proc XGetAtomName*(d: Display, atom: Atom): cstring proc XCreateColormap*(d: Display, root: Window; visual: ptr Visual; flags: cint): Colormap +proc XFreeColormap*(d: Display, colormap: Colormap) + ## Frees a colormap created for a window. + proc XCreateWindow*( d: Display, root: Window; @@ -343,6 +346,42 @@ proc XDefineCursor*(d: Display, window: x.Window; cursor: x.Cursor) proc XUndefineCursor*(d: Display, window: x.Window) proc XFreeCursor*(d: Display, cursor: x.Cursor) +proc XGrabPointer*( + d: Display, + window: Window, + ownerEvents: cint, + eventMask: cuint, + pointerMode, keyboardMode: cint, + confineTo: Window, + cursor: x.Cursor, + time: Time +): cint + ## Grabs pointer events and optionally confines and changes the cursor. + +proc XUngrabPointer*(d: Display, time: Time) + ## Releases this client's pointer grab. + +proc XQueryPointer*( + d: Display, + window: Window, + root, child: ptr Window, + rootX, rootY, windowX, windowY: ptr cint, + mask: ptr cuint +): cint + ## Returns the pointer position relative to a window. + +proc XNextRequest*(d: Display): culong + ## Returns the serial number of the next X request. + +proc XWarpPointer*( + d: Display, + source, destination: Window, + sourceX, sourceY: cint, + sourceWidth, sourceHeight: cuint, + destinationX, destinationY: cint +) + ## Moves the pointer to the requested window coordinates. + {.pop.} {.push, cdecl, dynlib: libXExt, importc.} diff --git a/tests/test_x11.nim b/tests/test_x11.nim new file mode 100644 index 00000000..2ac92a04 --- /dev/null +++ b/tests/test_x11.nim @@ -0,0 +1,220 @@ +when defined(linux): + import opengl + include ../src/windy/platforms/linux/x11 + + type + CursorImage = object + x, y: cshort + width, height, xhot, yhot: cushort + serial: culong + pixels: ptr UncheckedArray[culong] + atom: Atom + name: cstring + + proc closeDisplay(d: Display): cint + {.cdecl, dynlib: libX11, importc: "XCloseDisplay".} + ## Closes the independent test connection. + + proc cursorImage(d: Display): ptr CursorImage + {.cdecl, dynlib: "libXfixes.so.3", importc: "XFixesGetCursorImage".} + ## Reads the cursor image currently displayed by the X server. + + proc movePointer(d: Display, x, y: cint, delay: culong): cint + {.cdecl, dynlib: "libXtst.so.6", + importc: "XTestFakeRelativeMotionEvent".} + ## Injects relative pointer movement through the X server. + + proc syncEvents() = + ## Processes queued events and finishes any resulting pointer warp. + display.XSync() + pollEvents() + display.XSync() + + proc pointerVisible(d: Display): bool = + ## Checks whether the server cursor has any opaque pixels. + let image = d.cursorImage() + doAssert image != nil + defer: + XFree(image) + for i in 0 ..< image.width.int * image.height.int: + if (image.pixels[i] shr 24 and 255) != 0: + return true + + proc tryGrab(d: Display): cint = + ## Tries to grab the pointer independently of Windy's state. + d.XGrabPointer( + d.defaultRootWindow, + 0, + PointerMotionMask.cuint, + GrabModeAsync, + GrabModeAsync, + 0, + 0, + CurrentTime + ) + + proc expectCaptureError(window: Window) = + ## Ensures an unsuccessful capture raises without claiming ownership. + var failed = false + try: + window.captureMouse() + except WindyError: + failed = true + doAssert failed + doAssert not window.mouseCaptured + + proc testContexts() = + ## Checks actual OpenGL version, profile, and framebuffer attributes. + let window = newWindow( + "X11 context", + ivec2(128, 128), + visible = false, + vsync = false, + openglVersion = OpenGL3Dot3, + depthBits = 24, + stencilBits = 8 + ) + defer: + window.close() + loadExtensions() + var major, minor, profile, depth, stencil: GLint + glGetIntegerv(GL_MAJOR_VERSION, major.addr) + glGetIntegerv(GL_MINOR_VERSION, minor.addr) + glGetIntegerv(GL_CONTEXT_PROFILE_MASK, profile.addr) + glGetFramebufferAttachmentParameteriv( + GL_FRAMEBUFFER, + GL_DEPTH, + GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, + depth.addr + ) + glGetFramebufferAttachmentParameteriv( + GL_FRAMEBUFFER, + GL_STENCIL, + GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, + stencil.addr + ) + doAssert major > 3 or (major == 3 and minor >= 3) + doAssert (profile and GL_CONTEXT_CORE_PROFILE_BIT.GLint) != 0 + doAssert depth >= 24 and stencil >= 8 + echo "OpenGL: ", cast[cstring](glGetString(GL_VERSION)) + + proc testUnsupportedVersion() = + ## Checks rejection above the Mesa version cap used by this test run. + var failed = false + try: + let window = newWindow( + "Unsupported context", + ivec2(32, 32), + visible = false, + vsync = false, + openglVersion = OpenGL4Dot6 + ) + window.close() + except WindyError: + failed = true + doAssert failed, "OpenGL 4.6 must fail with Mesa capped to 3.3" + testContexts() + + proc testCapture() = + ## Checks server capture, hidden cursor, relative movement, and cleanup. + let + window = newWindow( + "X11 capture", + ivec2(128, 128), + visible = false, + vsync = false, + openglVersion = OpenGL3Dot3 + ) + peer = XOpenDisplay(nil) + doAssert peer != nil + defer: + window.close() + discard peer.closeDisplay() + + window.expectCaptureError() + window.visible = true + window.focus() + syncEvents() + window.captureMouse() + syncEvents() + doAssert window.mouseCaptured + doAssert peer.tryGrab() == AlreadyGrabbed + doAssert not peer.pointerVisible() + window.cursor = common.Cursor(kind: CrosshairCursor) + display.XSync() + doAssert not peer.pointerVisible() + + var movement: IVec2 + for i in 0 ..< 12: + for j in 0 ..< 2: + doAssert peer.movePointer(13, -7, 0) != 0 + peer.XSync() + syncEvents() + doAssert window.mouseDelta == ivec2(26, -14) + movement += window.mouseDelta + syncEvents() + doAssert window.mouseDelta == ivec2(0, 0) + doAssert movement == ivec2(312, -168) + + window.releaseMouse() + syncEvents() + doAssert not window.mouseCaptured + doAssert peer.pointerVisible() + doAssert peer.tryGrab() == GrabSuccess + window.expectCaptureError() + peer.XUngrabPointer(CurrentTime) + peer.XSync() + + window.captureMouse() + syncEvents() + peer.XSetInputFocus(peer.defaultRootWindow, rtNone) + peer.XSync() + syncEvents() + doAssert not window.mouseCaptured + doAssert peer.tryGrab() == GrabSuccess + peer.XUngrabPointer(CurrentTime) + peer.XSync() + + window.focus() + syncEvents() + window.captureMouse() + window.visible = false + syncEvents() + doAssert not window.mouseCaptured + doAssert peer.tryGrab() == GrabSuccess + peer.XUngrabPointer(CurrentTime) + peer.XSync() + + window.visible = true + peer.XSetInputFocus(peer.defaultRootWindow, rtNone) + peer.XSync() + syncEvents() + window.captureMouse() + peer.XUnmapWindow(window.handle) + peer.XSync() + syncEvents() + doAssert not window.mouseCaptured + doAssert peer.tryGrab() == GrabSuccess + peer.XUngrabPointer(CurrentTime) + peer.XSync() + + window.visible = true + window.focus() + syncEvents() + window.captureMouse() + window.close() + display.XSync() + doAssert not window.mouseCaptured + doAssert peer.tryGrab() == GrabSuccess + peer.XUngrabPointer(CurrentTime) + peer.XSync() + window.expectCaptureError() + + if paramCount() > 0 and paramStr(1) == "--unsupported-version": + testUnsupportedVersion() + else: + testContexts() + testCapture() + echo "Windy X11 regression tests passed" +else: + echo "Windy X11 regression tests skipped"