From 5061e6b50daf522d01e4c9e8e12023b9b107dc10 Mon Sep 17 00:00:00 2001 From: treeform Date: Thu, 5 Mar 2026 16:58:45 -0800 Subject: [PATCH 1/4] fix full screen --- src/windy/platforms/macos/macdefs.nim | 2 +- src/windy/platforms/macos/platform.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/windy/platforms/macos/macdefs.nim b/src/windy/platforms/macos/macdefs.nim index fbb75c02..6ae30859 100644 --- a/src/windy/platforms/macos/macdefs.nim +++ b/src/windy/platforms/macos/macdefs.nim @@ -228,7 +228,7 @@ objc: proc makeFirstResponder*(self: NSWindow, x: NSView): bool proc styleMask*(self: NSWindow): NSWindowStyleMask proc setStyleMask*(self: NSWindow, x: NSWindowStyleMask) - proc toggleFullscreen*(self: NSWindow, x: ID) + proc toggleFullScreen*(self: NSWindow, x: ID) proc invalidateCursorRectsForView*(self: NSWindow, x: NSView) proc mouseLocationOutsideOfEventStream*(self: NSWindow): NSPoint proc level*(self: NSWindow): NSWindowLevel diff --git a/src/windy/platforms/macos/platform.nim b/src/windy/platforms/macos/platform.nim index 1b1557eb..70166574 100644 --- a/src/windy/platforms/macos/platform.nim +++ b/src/windy/platforms/macos/platform.nim @@ -174,7 +174,7 @@ proc `fullscreen=`*(window: Window, fullscreen: bool) = if window.fullscreen == fullscreen: return autoreleasepool: - window.inner.toggleFullscreen(0.ID) + window.inner.toggleFullScreen(0.ID) proc `floating=`*(window: Window, floating: bool) = if window.floating == floating: From ef073e6a2c48bb428d5d8bc49af7113dc03bf5d6 Mon Sep 17 00:00:00 2001 From: treeform Date: Thu, 5 Mar 2026 16:58:56 -0800 Subject: [PATCH 2/4] add runners --- .github/workflows/build.yml | 52 +++++++--- examples/property_changes.nim | 53 ++++++++-- nimby.lock | 12 +++ tests/run_all.nim | 75 +++++++++++++++ tests/run_all_emscripten.nim | 176 ++++++++++++++++++++++++++++++++++ 5 files changed, 348 insertions(+), 20 deletions(-) create mode 100644 nimby.lock create mode 100644 tests/run_all.nim create mode 100644 tests/run_all_emscripten.nim diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0da9fe47..2f801e2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,51 +12,79 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - uses: treeform/setup-nim-action@v2 - - uses: mymindstorm/setup-emsdk@v14 + - uses: actions/checkout@v5 + - uses: treeform/setup-nim-action@v6 + + - run: nimby sync -g nimby.lock - - run: nimble install -y - - run: nimble install boxy + # Run tests. + - run: nim c tests/test.nim + # Build native examples. - run: nim c examples/basic.nim - run: nim c examples/basic_boxy.nim - run: nim c examples/basic_textured_quad.nim - run: nim c examples/basic_triangle.nim - run: nim c examples/callbacks.nim - run: nim c examples/clipboard.nim + - run: nim c examples/content_scale.nim - run: nim c examples/cursor_position_test.nim - run: nim c examples/custom_cursor.nim + - run: nim c examples/dragdrop.nim - run: nim c examples/fixedsize.nim - run: nim c examples/fullscreen.nim - - run: nim c examples/content_scale.nim - # - run: nim c examples/httprequest.nim - run: nim c examples/icon.nim - run: nim c examples/opengl_version.nim + - run: nim c examples/openurl.nim - run: nim c examples/property_changes.nim - run: nim c examples/screens.nim + - run: nim c examples/scrollwheel.nim - run: nim c examples/system_cursors.nim - run: nim c examples/tray.nim - run: nim c examples/websocket.nim - - run: nim c examples/openurl.nim + # Install Emscripten only after native checks pass. + - uses: mymindstorm/setup-emsdk@v14 + if: matrix.os == 'ubuntu-latest' + + # Build Emscripten examples on Ubuntu only. - run: nim c -d:emscripten examples/basic.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/basic_boxy.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/basic_textured_quad.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/basic_triangle.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/callbacks.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/clipboard.nim + if: matrix.os == 'ubuntu-latest' + - run: nim c -d:emscripten examples/content_scale.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/cursor_position_test.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/custom_cursor.nim + if: matrix.os == 'ubuntu-latest' + - run: nim c -d:emscripten examples/dragdrop.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/fixedsize.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/fullscreen.nim - - run: nim c -d:emscripten examples/content_scale.nim - - run: nim c -d:emscripten examples/httprequest.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/icon.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/opengl_version.nim + if: matrix.os == 'ubuntu-latest' + - run: nim c -d:emscripten examples/openurl.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/property_changes.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/screens.nim + if: matrix.os == 'ubuntu-latest' + - run: nim c -d:emscripten examples/scrollwheel.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/system_cursors.nim + if: matrix.os == 'ubuntu-latest' - run: nim c -d:emscripten examples/tray.nim - # - run: nim c -d:emscripten examples/websocket.nim - - run: nim c -d:emscripten examples/openurl.nim + if: matrix.os == 'ubuntu-latest' diff --git a/examples/property_changes.nim b/examples/property_changes.nim index 56e9969e..89930990 100644 --- a/examples/property_changes.nim +++ b/examples/property_changes.nim @@ -61,17 +61,54 @@ while not window.closeRequested: doAssert not window.minimized window.minimized = true + when defined(macosx): + # AppKit updates miniaturized state asynchronously after miniaturize(). + var tries = 0 + while not window.minimized and tries < 30: + pollEvents() + sleep(16) + inc tries doAssert window.minimized - when not defined(macosx): - window.fullscreen = false - doAssert not window.fullscreen - - window.fullscreen = true - doAssert window.fullscreen + window.minimized = false + when defined(macosx): + # AppKit updates miniaturized state asynchronously after deminiaturize(). + var deminiaturizeTries = 0 + while window.minimized and deminiaturizeTries < 30: + pollEvents() + sleep(16) + inc deminiaturizeTries + doAssert not window.minimized - window.fullscreen = false - doAssert not window.fullscreen + window.fullscreen = false + when defined(macosx): + # Fullscreen transitions are asynchronous on AppKit. + var fullscreenOffTries0 = 0 + while window.fullscreen and fullscreenOffTries0 < 60: + pollEvents() + sleep(16) + inc fullscreenOffTries0 + doAssert not window.fullscreen + + window.fullscreen = true + when defined(macosx): + # Fullscreen transitions are asynchronous on AppKit. + var fullscreenOnTries = 0 + while not window.fullscreen and fullscreenOnTries < 600: + pollEvents() + sleep(16) + inc fullscreenOnTries + doAssert window.fullscreen + + window.fullscreen = false + when defined(macosx): + # Fullscreen transitions are asynchronous on AppKit. + var fullscreenOffTries1 = 0 + while window.fullscreen and fullscreenOffTries1 < 600: + pollEvents() + sleep(16) + inc fullscreenOffTries1 + doAssert not window.fullscreen echo "SUCCESS!" quit() diff --git a/nimby.lock b/nimby.lock new file mode 100644 index 00000000..6be0e523 --- /dev/null +++ b/nimby.lock @@ -0,0 +1,12 @@ +opengl 1.2.9 https://github.com/nim-lang/opengl 8e2e098f82dc5eefd874488c37b5830233cd18f4 +pixie 5.1.0 https://github.com/treeform/pixie 5eda4949a3c8bea318cfac8e42060a8f90f3f35d +vmath 2.0.1 https://github.com/treeform/vmath b1cb7ec85f6e7690cf1261227cd2d552275e3e53 +chroma 1.0.0 https://github.com/treeform/chroma 2381748f92e5ea16cb2403ff7e20c6dd5443a59d +zippy 0.10.16 https://github.com/guzba/zippy a99f6a7d8a8e3e0213b3cad0daf0ea974bf58e3f +flatty 0.3.4 https://github.com/treeform/flatty 05d878b397933331966a7f80dd0c55664559201f +nimsimd 1.3.2 https://github.com/guzba/nimsimd 3f6b2668ffb0867d0bf786a658b817763e611350 +bumpy 1.1.3 https://github.com/treeform/bumpy dc9a3d6d15680ec7f603959d65ba8ee4f1ddbc72 +crunchy 0.1.11 https://github.com/guzba/crunchy 98eb6526982bb8aae8eec6e8781f4539fa19e049 +urlly 1.1.1 https://github.com/treeform/urlly 99784779f05649df25fd9c33003d8ef6de027345 +ws 0.5.0 https://github.com/treeform/ws cbb8f763b436669392d10baec2a45778395395cc +boxy 0.7.0 https://github.com/treeform/boxy f064ead838dbf980a520c827bc0fb600b8ea2364 diff --git a/tests/run_all.nim b/tests/run_all.nim new file mode 100644 index 00000000..7f9d2925 --- /dev/null +++ b/tests/run_all.nim @@ -0,0 +1,75 @@ +## Compiles all examples first, then runs them sequentially. + +import std/[osproc, os, strformat] + +const Examples = [ + "basic", + "basic_boxy", + "basic_textured_quad", + "basic_triangle", + "callbacks", + "clipboard", + "content_scale", + "cursor_position_test", + "custom_cursor", + "dragdrop", + "fixedsize", + "fullscreen", + "icon", + "opengl_version", + "openurl", + "property_changes", + "screens", + "scrollwheel", + "system_cursors", + "tray", + "websocket", +] + +proc main() = + ## Compile all examples, then run all examples in sequence. + let + startDir = getCurrentDir() + rootDir = currentSourcePath().parentDir.parentDir + defer: + setCurrentDir(startDir) + + echo "=== Windy Examples Runner ===" + echo "Compiling all examples first." + echo "Running all examples after successful compilation." + echo "Close each window to proceed to the next example.\n" + + for i, name in Examples: + let nimFile = "examples" / (name & ".nim") + echo fmt"[{i + 1}/{Examples.len}] Compiling: {name}" + + setCurrentDir(rootDir) + let exitCode = execCmd(fmt"nim c {nimFile}") + if exitCode != 0: + echo fmt" ERROR: {name} failed to compile with exit code {exitCode}" + quit(exitCode) + echo "" + + echo "=== Compilation complete ===\n" + + for i, name in Examples: + when defined(macosx): + if name == "opengl_version": + echo fmt"[{i + 1}/{Examples.len}] Skipping on macOS: {name}" + echo "" + continue + + let binaryPath = "examples" / name + echo fmt"[{i + 1}/{Examples.len}] Running: {name}" + + setCurrentDir(rootDir) + let exitCode = execCmd(binaryPath) + if exitCode != 0: + echo fmt" ERROR: {name} failed with exit code {exitCode}" + quit(exitCode) + echo "" + + echo "=== All examples completed ===" + +when isMainModule: + main() diff --git a/tests/run_all_emscripten.nim b/tests/run_all_emscripten.nim new file mode 100644 index 00000000..8f051e51 --- /dev/null +++ b/tests/run_all_emscripten.nim @@ -0,0 +1,176 @@ +## Compiles all Emscripten examples, opens tabs, and serves assets over HTTP. + +import + std/[browsers, os, osproc, strformat, strutils], + mummy, mummy/routers + +when not declared(Thread): + import std/threads + +const Examples = [ + "basic", + "basic_boxy", + "basic_textured_quad", + "basic_triangle", + "callbacks", + "clipboard", + "content_scale", + "cursor_position_test", + "custom_cursor", + "dragdrop", + "fixedsize", + "fullscreen", + "icon", + "opengl_version", + "openurl", + "property_changes", + "screens", + "scrollwheel", + "system_cursors", + "tray", +] + +const + ServerHost = "127.0.0.1" + ServerPortNumber = 8080 + ServerPort = Port(ServerPortNumber) + +proc guessContentType(path: string): string = + ## Returns a basic content type for static files. + let ext = splitFile(path).ext.toLowerAscii() + case ext + of ".html": + "text/html; charset=utf-8" + of ".js": + "text/javascript; charset=utf-8" + of ".wasm": + "application/wasm" + of ".data": + "application/octet-stream" + of ".css": + "text/css; charset=utf-8" + of ".json": + "application/json; charset=utf-8" + of ".png": + "image/png" + of ".jpg", ".jpeg": + "image/jpeg" + of ".svg": + "image/svg+xml; charset=utf-8" + else: + "application/octet-stream" + +proc isSafeRelativePath(relativePath: string): bool = + ## Returns true when the relative path does not escape the root. + if relativePath.len == 0: + return true + if relativePath.startsWith('/'): + return false + if '\0' in relativePath: + return false + for part in relativePath.split('/'): + if part == "..": + return false + true + +proc openTabs(urls: seq[string]) = + ## Opens URLs in the default browser. + for url in urls: + openDefaultBrowser(url) + +proc serveThread(server: Server) = + ## Runs the server loop in a worker thread. + {.gcsafe.}: + server.serve(ServerPort, ServerHost) + +proc serveEmscriptenDir(emscriptenDir: string, urls: seq[string]) = + ## Serves the Emscripten output directory until Ctrl-C. + var router: Router + + router.get("/", proc(request: Request) {.gcsafe.} = + var body = "

Windy Emscripten Examples

") + + var headers: HttpHeaders + headers["Content-Type"] = "text/html; charset=utf-8" + request.respond(200, headers, body) + ) + + router.get("/**", proc(request: Request) {.gcsafe.} = + var relativePath = request.path + if relativePath.startsWith('/'): + relativePath = relativePath[1 .. ^1] + + if not isSafeRelativePath(relativePath): + request.respond(403, emptyHttpHeaders(), "Forbidden") + return + + var filePath = emscriptenDir / relativePath + if dirExists(filePath): + filePath = filePath / "index.html" + + if not fileExists(filePath): + request.respond(404, emptyHttpHeaders(), "Not found") + return + + let body = readFile(filePath) + var headers: HttpHeaders + headers["Content-Type"] = guessContentType(filePath) + request.respond(200, headers, body) + ) + + let server = newServer(router) + echo fmt"Serving {emscriptenDir} at http://{ServerHost}:{ServerPortNumber}/" + echo "Press Ctrl-C to stop." + + when compileOption("threads"): + var serverWorker: Thread[Server] + createThread(serverWorker, serveThread, server) + server.waitUntilReady() + openTabs(urls) + joinThread(serverWorker) + else: + echo "Warning: Build without threads, opening tabs before server starts." + openTabs(urls) + server.serve(ServerPort, ServerHost) + +proc main() = + ## Compiles all Emscripten examples, then serves and opens them in tabs. + let + startDir = getCurrentDir() + rootDir = currentSourcePath().parentDir.parentDir + emscriptenDir = rootDir / "examples" / "emscripten" + defer: + setCurrentDir(startDir) + + echo "=== Windy Emscripten Runner ===" + echo "Compiling all examples first with: nim c -d:emscripten" + echo "Serving generated files over HTTP after successful compilation.\n" + + for i, name in Examples: + let nimFile = "examples" / (name & ".nim") + + echo fmt"[{i + 1}/{Examples.len}] Compiling: {name}" + setCurrentDir(rootDir) + + let exitCode = execCmd(fmt"nim c -d:emscripten {nimFile}") + if exitCode != 0: + echo fmt" ERROR: {name} failed to compile with exit code {exitCode}" + quit(exitCode) + echo "" + + echo "=== Compilation complete ===\n" + + var urls: seq[string] + for i, name in Examples: + let url = fmt"http://{ServerHost}:{ServerPortNumber}/{name}.html" + echo fmt"[{i + 1}/{Examples.len}] Queueing tab: {url}" + urls.add(url) + + echo "" + serveEmscriptenDir(emscriptenDir, urls) + +when isMainModule: + main() From af6439aed0ee61bfb1e6643d5965541d996ce968 Mon Sep 17 00:00:00 2001 From: treeform Date: Thu, 5 Mar 2026 17:06:04 -0800 Subject: [PATCH 3/4] Fix full screen tracking on mac. --- examples/property_changes.nim | 49 +++++++---------------- src/windy/platforms/macos/platform.nim | 55 +++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 37 deletions(-) diff --git a/examples/property_changes.nim b/examples/property_changes.nim index 89930990..1705528b 100644 --- a/examples/property_changes.nim +++ b/examples/property_changes.nim @@ -10,6 +10,15 @@ window.onFrame = proc() = # Your OpenGL display code here window.swapBuffers() +template waitFor(condition: untyped, maxTries: int) = + ## Waits for an asynchronous window state transition. + when defined(macosx): + var tries = 0 + while not (condition) and tries < maxTries: + pollEvents() + sleep(16) + inc tries + while not window.closeRequested: sleep(100) @@ -61,53 +70,23 @@ while not window.closeRequested: doAssert not window.minimized window.minimized = true - when defined(macosx): - # AppKit updates miniaturized state asynchronously after miniaturize(). - var tries = 0 - while not window.minimized and tries < 30: - pollEvents() - sleep(16) - inc tries + waitFor(window.minimized, 30) doAssert window.minimized window.minimized = false - when defined(macosx): - # AppKit updates miniaturized state asynchronously after deminiaturize(). - var deminiaturizeTries = 0 - while window.minimized and deminiaturizeTries < 30: - pollEvents() - sleep(16) - inc deminiaturizeTries + waitFor(not window.minimized, 30) doAssert not window.minimized window.fullscreen = false - when defined(macosx): - # Fullscreen transitions are asynchronous on AppKit. - var fullscreenOffTries0 = 0 - while window.fullscreen and fullscreenOffTries0 < 60: - pollEvents() - sleep(16) - inc fullscreenOffTries0 + waitFor(not window.fullscreen, 60) doAssert not window.fullscreen window.fullscreen = true - when defined(macosx): - # Fullscreen transitions are asynchronous on AppKit. - var fullscreenOnTries = 0 - while not window.fullscreen and fullscreenOnTries < 600: - pollEvents() - sleep(16) - inc fullscreenOnTries + waitFor(window.fullscreen, 600) doAssert window.fullscreen window.fullscreen = false - when defined(macosx): - # Fullscreen transitions are asynchronous on AppKit. - var fullscreenOffTries1 = 0 - while window.fullscreen and fullscreenOffTries1 < 600: - pollEvents() - sleep(16) - inc fullscreenOffTries1 + waitFor(not window.fullscreen, 600) doAssert not window.fullscreen echo "SUCCESS!" diff --git a/src/windy/platforms/macos/platform.nim b/src/windy/platforms/macos/platform.nim index 70166574..e64953cf 100644 --- a/src/windy/platforms/macos/platform.nim +++ b/src/windy/platforms/macos/platform.nim @@ -28,6 +28,9 @@ type inner: NSWindow trackingArea: NSTrackingArea markedText: NSString + # AppKit applies this state asynchronously via delegate callbacks. + fullscreenState: bool + minimizedState: bool const decoratedResizableWindowMask = @@ -79,7 +82,7 @@ proc style*(window: Window): WindowStyle = Undecorated proc fullscreen*(window: Window): bool = - (window.inner.styleMask and NSWindowStyleMaskFullScreen) != 0 + window.fullscreenState proc floating*(window: Window): bool = window.inner.level == NSFloatingWindowLevel @@ -111,7 +114,7 @@ proc pos*(window: Window): IVec2 = ).ivec2 proc minimized*(window: Window): bool = - window.inner.isMiniaturized + window.minimizedState proc maximized*(window: Window): bool = window.inner.isZoomed @@ -326,6 +329,46 @@ proc windowDidMove( if window != nil and window.onMove != nil: window.onMove() +proc windowDidMiniaturize( + self: ID, + cmd: SEL, + notification: NSNotification +): ID {.cdecl.} = + let window = windows.forNSWindow(self.NSWindow) + if window == nil: + return + window.minimizedState = true + +proc windowDidDeminiaturize( + self: ID, + cmd: SEL, + notification: NSNotification +): ID {.cdecl.} = + let window = windows.forNSWindow(self.NSWindow) + if window == nil: + return + window.minimizedState = false + +proc windowDidEnterFullScreen( + self: ID, + cmd: SEL, + notification: NSNotification +): ID {.cdecl.} = + let window = windows.forNSWindow(self.NSWindow) + if window == nil: + return + window.fullscreenState = true + +proc windowDidExitFullScreen( + self: ID, + cmd: SEL, + notification: NSNotification +): ID {.cdecl.} = + let window = windows.forNSWindow(self.NSWindow) + if window == nil: + return + window.fullscreenState = false + proc canBecomeKeyWindow( self: ID, cmd: SEL, @@ -711,6 +754,10 @@ proc init() {.raises: [].} = addClass "WindyWindow", "NSWindow", WindyWindow: addMethod "windowDidResize:", windowDidResize addMethod "windowDidMove:", windowDidMove + addMethod "windowDidMiniaturize:", windowDidMiniaturize + addMethod "windowDidDeminiaturize:", windowDidDeminiaturize + addMethod "windowDidEnterFullScreen:", windowDidEnterFullScreen + addMethod "windowDidExitFullScreen:", windowDidExitFullScreen addMethod "canBecomeKeyWindow:", canBecomeKeyWindow addMethod "windowDidBecomeKey:", windowDidBecomeKey addMethod "windowDidResignKey:", windowDidResignKey @@ -951,6 +998,10 @@ proc newWindow*( result.style = style result.visible = visible + result.minimizedState = result.inner.isMiniaturized + result.fullscreenState = + (result.inner.styleMask and NSWindowStyleMaskFullScreen) != 0 + pollEvents() # This can cause lots of issues, potential workaround needed proc title*(window: Window): string = From 2435c4b757d92cc58553bddd7ebacf72400aa0c6 Mon Sep 17 00:00:00 2001 From: treeform Date: Thu, 5 Mar 2026 17:12:56 -0800 Subject: [PATCH 4/4] ci install boxy only in workflow Made-with: Cursor --- .github/workflows/build.yml | 1 + nimby.lock | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2f801e2b..1ad60e54 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,7 @@ jobs: - uses: treeform/setup-nim-action@v6 - run: nimby sync -g nimby.lock + - run: nimby install -g boxy # Run tests. - run: nim c tests/test.nim diff --git a/nimby.lock b/nimby.lock index 6be0e523..4d9232f9 100644 --- a/nimby.lock +++ b/nimby.lock @@ -9,4 +9,3 @@ bumpy 1.1.3 https://github.com/treeform/bumpy dc9a3d6d15680ec7f603959d65ba8ee4f1 crunchy 0.1.11 https://github.com/guzba/crunchy 98eb6526982bb8aae8eec6e8781f4539fa19e049 urlly 1.1.1 https://github.com/treeform/urlly 99784779f05649df25fd9c33003d8ef6de027345 ws 0.5.0 https://github.com/treeform/ws cbb8f763b436669392d10baec2a45778395395cc -boxy 0.7.0 https://github.com/treeform/boxy f064ead838dbf980a520c827bc0fb600b8ea2364