Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions examples/slow_window_start.nim
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,19 @@ echo "Slow window start repro."
echo "The window is visible before the event loop is allowed to run."
echo "After the delay, move the mouse and click the square."

echo "Focused right after newWindow: ", window.focused

drawFrame()
echo "Delaying for 3,500 ms."
sleep(SlowStartMs)
echo "Delay done."

pollEvents()
if window.focused:
echo "PASS: window is focused after the delay, no zombie state."
else:
echo "FAIL: window is not focused after the delay (zombie state), unless"
echo "another app was being used while this example started."

while not window.closeRequested:
pollEvents()
1 change: 1 addition & 0 deletions src/windy/platforms/macos/macdefs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ objc:
x: NSApplicationPresentationOptions
)
proc activateIgnoringOtherApps*(self: NSApplication, x: bool)
proc isActive*(self: NSApplication): bool
proc setDelegate*(self: NSApplication, x: ID)
proc setDelegate*(self: NSWindow, x: ID)
proc setMainMenu*(self: NSApplication, x: NSMenu)
Expand Down
114 changes: 75 additions & 39 deletions src/windy/platforms/macos/platform.nim
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ type
fullscreenState: bool
minimizedState: bool
cpuImage: NSImage
activationSettlePolls: int

const
ActivationSettlePolls = 60
ActivationSettleSeconds = 0.001
ActivationTurnSeconds = 0.001
ActivationPumpSeconds = 0.25
ActivationRescueSeconds = 2.0
decoratedResizableWindowMask =
NSWindowStyleMaskTitled or NSWindowStyleMaskClosable or
NSWindowStyleMaskMiniaturizable or NSWindowStyleMaskResizable
Expand All @@ -57,6 +57,12 @@ const
var
WindyAppDelegate, WindyWindow, WindyView: Class
windows: seq[Window]
# App activation is process-level, not per-window. While this deadline
# is in the future, pollEvents re-requests activation until it succeeds.
activationRescueDeadline: float64

proc drainEvents()
proc pumpActivation()

objc:
proc initWithFrame(self: NSView, x: NSRect): NSView
Expand Down Expand Up @@ -166,25 +172,28 @@ proc `title=`*(window: Window, title: string) =
proc `icon=`*(window: Window, icon: Image) =
window.state.icon = icon

proc requestActivation(window: Window) =
## Shows the window and asks AppKit to activate this app.
window.inner.makeKeyAndOrderFront(0.ID)
NSApp.activateIgnoringOtherApps(true)

proc settleRunLoop() =
## Gives AppKit a short turn to deliver activation notifications.
proc runLoopTurn() =
## Gives AppKit a short turn to exchange messages with the window server.
discard NSRunLoop.currentRunLoop.runMode(
NSDefaultRunLoopMode,
NSDate.dateWithTimeIntervalSinceNow(ActivationSettleSeconds)
NSDate.dateWithTimeIntervalSinceNow(ActivationTurnSeconds)
)

proc `visible=`*(window: Window, visible: bool) =
autoreleasepool:
if visible:
window.activationSettlePolls = ActivationSettlePolls
window.requestActivation()
window.inner.makeKeyAndOrderFront(0.ID)
NSApp.activateIgnoringOtherApps(true)
# The window server only honors this activation request while it is
# fresh, and completing it is a handshake that needs run loop turns.
# If we return without pumping and the app blocks (loading assets,
# etc.), the grant expires and the process is stuck as a background
# app: window front and clickable, but undecorated and never key.
# So finish the handshake now, before giving control back.
activationRescueDeadline = epochTime() + ActivationRescueSeconds
pumpActivation()
else:
window.activationSettlePolls = 0
activationRescueDeadline = 0
window.inner.orderOut(0.ID)

proc `style=`*(window: Window, windowStyle: WindowStyle) =
Expand Down Expand Up @@ -1070,6 +1079,16 @@ proc init() {.raises: [].} =

NSApp.finishLaunching()

# Give the window server a moment to finish promoting the process to a
# regular GUI app before any window is created. A window created while
# the process is still a background process comes up unmanaged: no
# traffic light buttons and it can never become key.
# No windows or user callbacks exist yet, so nothing here can raise.
{.cast(raises: []).}:
for _ in 0 ..< 10:
drainEvents()
runLoopTurn()

platformDoubleClickInterval = NSEvent.doubleClickInterval

initialized = true
Expand Down Expand Up @@ -1114,31 +1133,8 @@ proc processFlagsChanged(event: NSEvent) =
else:
window.handleButtonPress(button)

proc settleActivationRequests() =
## Replays activation while AppKit catches up after delayed startup.
var pending = false
for window in windows:
if window.activationSettlePolls == 0:
continue
dec window.activationSettlePolls
pending = true
window.requestActivation()
if pending:
settleRunLoop()

proc pollEvents*() =
autoreleasepool:
settleActivationRequests()

# Draw first (in case a message closes a window or similar)
for window in windows:
if window.onFrame != nil:
window.onFrame()

# Clear all per-frame data
for window in windows:
window.state.perFrame = PerFrame()

proc drainEvents() =
## Dequeues and dispatches all pending AppKit events.
autoreleasepool:
while true:
let event = NSApp.nextEventMatchingMask(
Expand Down Expand Up @@ -1168,6 +1164,46 @@ proc pollEvents*() =
# Forward event for app to handle.
NSApp.sendEvent(event)

proc pumpActivation() =
## Services the run loop until the app-activation handshake completes,
## bounded by ActivationPumpSeconds. Normally finishes in a couple of
## milliseconds; times out when macOS declines to activate us (e.g. the
## user is actively working in another app).
let deadline = epochTime() + ActivationPumpSeconds
while not NSApp.isActive and epochTime() < deadline:
drainEvents()
runLoopTurn()

proc rescueActivation() =
## Cooperative activation (macOS 14+) drops requests made while the user
## is interacting with another app; a fresh request right after they stop
## is granted. So for a short time after showing a window, re-request
## activation until it succeeds. Once we are active (or the deadline
## passes) this never fires again, so focus is not stolen back from the
## user later.
if activationRescueDeadline == 0:
return
if NSApp.isActive or epochTime() > activationRescueDeadline:
activationRescueDeadline = 0
return
NSApp.activateIgnoringOtherApps(true)
runLoopTurn()

proc pollEvents*() =
autoreleasepool:
rescueActivation()

# Draw first (in case a message closes a window or similar)
for window in windows:
if window.onFrame != nil:
window.onFrame()

# Clear all per-frame data
for window in windows:
window.state.perFrame = PerFrame()

drainEvents()

pollHttp()

proc centerWindow(window: Window) =
Expand Down
Loading