Skip to content

Refactor window events - #5

Closed
elcritch wants to merge 14 commits into
masterfrom
refactor-window-events
Closed

elcritch wants to merge 14 commits into
masterfrom
refactor-window-events

Conversation

@elcritch

@elcritch elcritch commented Aug 4, 2026 •

Copy link
Copy Markdown
Owner

This adds an application-global, efficiently blocking event loop for
macOS, Windows, X11, and Wayland. It reduces CPU usage vs the previous
event polling design.

Applications can now wait once for all native window events, wake the
loop from worker threads or external queues, and service one or more
windows without fixed-delay polling.

I worked to make this API additive while keeping the previous tick behavior and semantics. The new APIs can be leveraged for applications to gain efficient blocking loop behavior.

A future a breaking release could change the default tick behavior.

New API

proc pollEvents*(globals: SiwinGlobals): bool
proc waitEvents*(globals: SiwinGlobals)
proc waitEvents*(
  globals: SiwinGlobals,
  timeout: Duration,
): EventWaitResult

proc eventLoopWaker*(globals: SiwinGlobals): EventLoopWaker
proc wake*(waker: EventLoopWaker) {.gcsafe, raises: [].}
proc wakeEventLoop*(globals: SiwinGlobals) {.gcsafe, raises: [].}

method serviceWindow*(window: Window)

EventLoopWaker is a narrow, copyable capability suitable for worker
threads. Producers enqueue their work first and then call wake;
notifications may be coalesced, while the application queue remains the
source of truth.

Timed waits can also use the next animation deadline instead of polling
every 16 ms.

## Platform implementations

### macOS

- Blocks with nextEventMatchingMask.
- Drains default, tracking/live-resize, and modal run-loop modes.
- Wakes through a coalesced application-defined NSEvent.
- Keeps AppKit dispatch and callbacks on the application thread.

### Windows

- Blocks with MsgWaitForMultipleObjectsEx.
- Waits on both the Win32 message queue and an auto-reset wake event.
- Uses SetEvent for thread-safe wakeups.

### X11

- Uses poll() on the X11 connection and a self-pipe.
- Routes global XPending/XNextEvent batches through a window registry.
- Preserves same-window key-repeat lookahead.
- Does not require calling Xlib from producer threads or enabling
  XInitThreads.

### Wayland

- Uses poll() on the Wayland display, libdecor, and a self-pipe.
- Implements the race-safe prepare_read / flush / poll / read-or-cancel
  sequence.

- Incorporates keyboard-repeat deadlines into timed waits.
- Loads the runtime libwayland-client.so.0 SONAME when the development
  symlink is unavailable.

## Compatibility

The existing run, runMultiple, and no-argument Window.step() APIs remain
available. step() is now a compatibility wrapper around the global wait
and nonblocking window service.

No wake callback was added to WindowEventsHandler, so its existing
layout remains unchanged.

Additive C API functions are also included:

- siwin_poll_events
- siwin_wait_events
- siwin_wake_event_loop
- siwin_window_service

## Examples and documentation

- Updates text_input_demo.nim to use deadline-driven waits for cursor
  blinking and scroll decay.

- Documents worker/queue wakeups and animation scheduling.
- Adds a README example for the global event-loop API.

## Testing

The cross-platform integration test covers:

- Nonblocking polling and zero-duration waits.
- Queued and coalesced wakeups.
- Wakeups from copied wakers on worker threads.
- Idle blocking without CPU spinning.
- Waker lifetime safety.
- Nonblocking serviceWindow.
- Existing step() compatibility.

Linux CI exercises both display backends:

- X11 under Xvfb.
- Wayland under headless Weston.

CI passes on Ubuntu, macOS, and Windows.

@elcritch elcritch closed this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant