Windows API
Access windows through app.windows : CryKit::Windows::Manager. Applications currently run on macOS; see platform support.
Manager API
| Call | Returns | Purpose |
|---|---|---|
app.windows.create(id: ..., path: ...) | Window | Create a window from a route, URL, or HTML; options below |
app.windows[id] | Window | Find an active window; raises KeyError if absent |
app.windows[id]? | Window? | Find an active window, or nil |
app.windows.each { |window| ... } | — | Iterate active windows in creation order |
app.windows.size | Int32 | Count active windows |
app.windows.empty? | Bool | Check whether any active windows remain |
main = app.windows.create(id: :main, path: "/", title: "My app",
width: 1100, height: 700)
app.runConfigure routes and assets before app.run. Use the OS main thread to start the application and the UI context for live window operations. From a view job, use job.notify to queue UI changes.
Creation and lookup
All arguments to app.windows.create are named.
| Argument | Type / default | Purpose |
|---|---|---|
id: | Symbol, required | Unique active window identity |
path: | String? = nil | Absolute local route, without query or fragment |
url: | String? = nil | Absolute URL |
html: | String? = nil | HTML source; an empty string is valid |
title: | String = "New Window" | Window title |
width:, height: | Int32 = 640, 480 | Initial content size in logical points |
min_width:, min_height: | Int32? = nil | Minimum content dimensions |
max_width:, max_height: | Int32? = nil | Maximum content dimensions |
resizable: | Bool = true | Allow user resizing |
hidden: | Bool = false | Load without initially presenting the window |
always_on_top: | Bool = false | Use the floating window level |
file_drop: | Bool = false | Enable receiving files on declared HTML targets |
macos: | MacOSOptions.new | Native appearance and website warnings |
Provide exactly one of path:, url:, or html:. Content and sizing bounds are fixed at creation. Dimensions and bounds must be positive, minima must not exceed maxima, and the initial size must satisfy the bounds. A non-resizable window can still be resized programmatically.
panel = app.windows.create(id: :panel, html: "<p>Status</p>",
width: 400, height: 300, resizable: false, hidden: true)
editor = app.windows.create(id: :editor, path: "/editor",
width: 1100, height: 700, min_width: 640, min_height: 480)Initial windows open during app.run; windows created while running open immediately. Local routes load through app://crykit. IDs can be reused after a window is removed from lookup during closure.
Window API
These calls are made on the returned Window, for example main.title = "Editor".
Properties and configuration
| Read | Write | Type / meaning |
|---|---|---|
id | — | Immutable Symbol identity |
title | title = value | String; last configured/requested title |
size | size(width, height) | Size; content dimensions |
width, height | width = value, height = value | Int32; content dimension, preserving the other axis |
min_width, min_height, max_width, max_height | — | Int32?; fixed content bounds |
resizable? | — | Bool; fixed user-resizing policy |
always_on_top? | always_on_top = value | Bool; floating window level |
file_drop? | file_drop = value | Bool; receiving files on declared targets |
document_ready? | — | Bool; current document is connected to CryKit |
closed? | — | Bool; logical window closure has completed |
macos | — | MacOSWindow; native appearance facade |
Title, content size, always-on-top, file-drop, and macOS settings can be configured before startup. Live content dimensions read native size. Closed windows retain identity, title, content dimensions, bounds, and resizability; mutations raise Window::ClosedError.
Native controls
These require an existing native window and the UI context, including queries.
| Call | Purpose |
|---|---|
show, hide, visible? | Present, hide, or query native visibility |
focus | Request key-window focus and application activation |
auto_hide_on_focus_loss?, auto_hide_on_focus_loss = value | Hide when the window loses key status; default false |
minimize, unminimize, minimized? | Minimize, restore, or query minimization |
maximize, unmaximize, toggle_maximize, maximized? | Control AppKit Zoom state |
center | Center using native placement |
position, position(x, y) | Read/write outer top-left coordinates |
frame, frame(x, y, width, height) | Read/write the outer frame, including decorations |
geometry | Read one Geometry(frame, content_size) snapshot |
Before native creation these raise Window::NotCreatedError; once closing begins, they raise Window::ClosedError. See geometry and visibility for coordinate and focus behavior.
Closing, events, and file sources
| Call | Returns / purpose |
|---|---|
close | Request closure; may be vetoed by :close_requested; repeated close is safe |
on_event(:event) { |event| ... } | Subscribe to one event type; returns an unsubscribe function |
events { |event| ... } | Subscribe to all window events; returns an unsubscribe function |
file_drag_source(name, paths) | Register files for an HTML drag source; returns an unsubscribe function |
Closing the last window exits the application by default. Set quit_on_last_window_closed: false for an application that remains running without windows.
Lifecycle events
| Event | Payload / action | When it fires |
|---|---|---|
:close_requested | reason; cancel | Before native close, close, or application quit |
:closing | window | Closure accepted; still in lookup and closed? is false |
:closed | window | Removed from lookup; document and jobs released |
:focus | window | Native window becomes key |
:lost_focus | window | Native window resigns key status |
:moved | geometry | Outer-frame position changes |
:resized | geometry | Outer frame or content size changes |
:document_ready | window | A document connects to CryKit |
:file_drop | file_drop | Local files land on an enabled HTML target |
:navigating | url; cancel | An application link requests document replacement |
:renderer_terminated | recovery_available?; cancel | WebKit renderer ends; veto automatic recovery |
Every event exposes window, type, and type predicates such as closed? or resized?. Read payloads only on their matching event type; otherwise they raise Window::EventRequest::InvalidStateError.
stop_logging = main.on_event(:closed) { |event| puts event.window.id }
stop_observing = main.events do |event|
puts event.geometry.content_size if event.resized?
end
stop_logging.call
stop_observing.callSubscriptions are independent and run synchronously in registration order. Unsubscribe functions are idempotent. Subscriptions created in a view's on_mount, UI callbacks, or job.notify blocks are removed automatically on unmount. Application/service subscriptions remain until explicitly removed or window closure.
Subscription and closure rules
- Registration and unsubscription are thread-safe; new subscriptions are rejected once closure begins.
- Removing a listener suppresses it if it has not been selected for delivery. Listeners added during dispatch wait for a later event.
- Callbacks run on the startup caller before
run, and the UI context while live. View-owned callbacks run in that view's scope; reactive writes update bindings through the UI queue. - Listener exceptions are logged. Observational events continue delivery; cancellable requests are vetoed and stop delivery.
- Closing seals the registry. Departing view listeners are removed during document cleanup, before
:closed; service listeners can observe:closed. :closingand:closedeach fire once for an accepted closure, including cleanup after failed startup. They describe logical lifetime, even if native disposal fails.- Native operations and mutations fail from
:closingonward. Recursivecloseis a no-op. Retaining the window cannot affect another window reusing its ID.
Close interception
main.on_event(:close_requested) do |event|
if draft.unsaved? && !app.dialogs.confirm("Unsaved changes", "Discard this draft?")
event.cancel
end
end| Member | Meaning |
|---|---|
reason | :native, :api, or :quit (Window::CloseRequest::Reason) |
cancel | Keep the window and its document open |
cancelled? | Whether a listener vetoed the request |
pending?, cancellable? | Whether the synchronous decision is still open |
Cancel during the callback. Native confirmation dialogs are supported while running; do not wait for asynchronous work or retain an event for a later decision. Cancellation preserves the document, views, and jobs and emits neither :closing nor :closed. A listener exception also vetoes closure.
Quit approval and request lifetime
app.quit asks every active window in creation order before closing any. A veto aborts quit; approval closes each window once without repeating the request. Native application quit uses the same path. New window creation is rejected while quit approval or committed shutdown is in progress.
Cancellation is irreversible. After dispatch the decision is sealed, and a later cancel raises Window::EventRequest::InvalidStateError. Reentrant navigation, close, and quit requests are coalesced to prevent nested prompts. Before run, listeners must not open native dialogs. Failed startup and final shutdown release resources unconditionally; cleanup cannot be vetoed.
Navigation, reload, and unmount release view subscriptions without asking for close approval. Use :navigating to guard application links.
Navigation interception
main.on_event(:navigating) do |event|
event.cancel if draft.unsaved? && !app.dialogs.confirm("Leave page?", "Discard changes?")
endevent.url is the destination. cancel, cancelled?, pending?, and cancellable? follow the close-request rules. Cancelling preserves the current document and its jobs; approval permits document replacement.
This covers ordinary, unmodified application links replacing the same window's connected document. Fragment-only links, modified clicks, downloads, other browsing targets, external links, JavaScript navigation, reload, and browser history retain their normal behavior.
Renderer recovery
On macOS, renderer termination releases the document and its work while leaving the native window open. CryKit offers one automatic reload per window lifetime. Register an application/service listener before app.run to control recovery:
main.on_event(:renderer_terminated) do |event|
event.cancel if shutting_down
puts event.recovery_available?
endThe event runs after document cleanup, when view-owned listeners have been removed. cancel vetoes recovery; closing the window or a listener exception also prevents it. A cancelled attempt does not consume the allowance. Later terminations are reported even after the allowance is exhausted. Reload preserves the current URL and navigation token; raw HTML restores its committed host source.
Focus observation
main.on_event(:focus) { |event| puts "Focused #{event.window.id}" }
main.on_event(:lost_focus) { |event| puts "Lost focus #{event.window.id}" }Focus means AppKit key-window status, including transfers to another window, application, or dialog. Moving between controls inside a WebView does not change native focus. focus requests a transition; events report actual transitions. There is no replay for late subscribers, and closure does not synthesize focus loss. Auto-hide runs before :lost_focus subscribers.
Geometry observation
snapshot = main.geometry
main.on_event(:resized) { |event| puts event.geometry.content_size }
main.on_event(:moved) { |event| puts event.geometry.frame }| Value type | Fields |
|---|---|
Windows::Size | width : Int32, height : Int32 |
Windows::Position | x : Int32, y : Int32 |
Windows::Frame | x : Int32, y : Int32, width : Int32, height : Int32 |
Windows::Geometry | frame : Frame, content_size : Size |
Snapshots are immutable and remain readable after further changes or closure. Measurements use logical points and the primary display's top-left origin.
Geometry event ordering
Native interactive and programmatic changes emit events when rounded geometry changes. There is no debounce, completion event, or initial replay. If one observation changes both size and position, :resized precedes :moved with the same snapshot. A native operation can produce multiple intermediate observations.
Callbacks run synchronously. Nested changes do not alter a snapshot already being delivered; closing the window suppresses remaining geometry delivery. Observation stops on closure or failed startup.
Document readiness
main.on_event(:document_ready) { |event| puts "Connected #{event.window.id}" }
puts main.document_ready?document_ready? reports the current document's authenticated CryKit connection. Each successfully connected document emits once; late subscribers use the query instead of receiving a replay. It is false before connection, once closure begins, and after connection expiry or failure. Query live windows on the UI context.
Readiness does not wait for images, paint, arbitrary application work, or the browser's separate crykit:ready JavaScript event. Raw HTML, external URLs, and plain local pages without mounted CryKit content remain unready. CSS hot reload preserves the connection and emits no new readiness event.
Darwin controls and geometry
main.size(900, 650)
main.position(120, 100)
main.frame(120, 100, 1000, 720)
main.center
main.minimize
main.unminimize
main.toggle_maximize| Measurement / operation | Behavior |
|---|---|
| Content size and bounds | Logical points, excluding default title-bar decorations |
| Frame | Outer window including decorations |
| Position | Primary display's top-left origin; x rightward, y downward |
| Native readback | Rounded to the nearest logical point, not physical Retina pixels |
size, width=, height= | Preserve outer top-left; enforce content bounds |
frame(...) | Convert outer to content size, then validate bounds before changing geometry |
position(...) | Preserve size; secondary displays may use negative coordinates |
center | Native placement, slightly above vertical center; does not show the window |
| Maximize operations | AppKit Zoom; no-op for resizable: false |
Native desktop constraints can adjust requested geometry. Invalid sizes raise ArgumentError without changing it. Native animations may finish after a call returns; queries report current state. Fullscreen configuration is not exposed.
Darwin visibility and focus
main.unminimize
main.show
main.focus
main.auto_hide_on_focus_loss = true| Call / state | Behavior |
|---|---|
show | Order the window front; does not explicitly restore or focus it |
hide | Hide without releasing its document, views, jobs, or geometry |
focus | Request key-window status and application activation; does not explicitly restore |
visible? | Read native visibility, independently of focus, obstruction, or minimization |
auto_hide_on_focus_loss = true | Hide when key status is lost, including to another window or dialog |
hidden: true | Load the initial document without first presenting the window |
Use unminimize, show, and focus together when all three are required. Disabling auto-hide does not reveal a hidden window; enabling it does not immediately hide an already unfocused one. Hiding every window keeps the application running, so retain a way to reveal one or call app.quit.
Darwin always-on-top
main.always_on_top = true
puts main.always_on_top?The setting can be configured before startup and changed live. It selects NSFloatingWindowLevel instead of NSNormalWindowLevel, preserving geometry, visibility, focus, and document lifetime. Higher-level system or modal windows can remain above it; this does not join every desktop Space or fullscreen Space.
macOS appearance
Pass CryKit::Windows::MacOSOptions.new(...) to create(macos: ...); read and change settings through window.macos. Native appearance requires macOS 11+.
| Creation argument | Read / write on window.macos | Default |
|---|---|---|
titlebar_transparent: | titlebar_transparent? / titlebar_transparent= | false |
full_size_content: | full_size_content? / full_size_content= | false |
transparent_background: | transparent_background? / transparent_background= | false |
title_visible: | title_visible? / title_visible= | true |
traffic_lights_visible: | traffic_lights_visible? / traffic_lights_visible= | true |
toolbar_style: | toolbar_style / toolbar_style= | nil |
appearance: | appearance / appearance= | :system |
fraudulent_website_warnings: | Creation only; website warnings | true |
| Enum | Values |
|---|---|
Windows::Appearance | :system, :light, :dark |
Windows::ToolbarStyle | :automatic, :expanded, :preference, :unified, :unified_compact |
main = app.windows.create(id: :main, path: "/",
macos: CryKit::Windows::MacOSOptions.new(
titlebar_transparent: true, full_size_content: true,
title_visible: false, appearance: :dark))
main.macos.appearance = :systemAppearance behavior and constraints
- Full-size content extends beneath native chrome; title-bar transparency and title visibility are separate settings. Reserve space for traffic lights in your HTML.
- A non-
niltoolbar style attaches an empty native toolbar;nilremoves it. Custom toolbar items are not exposed. - Structural changes preserve content size, outer top-left, bounds, and resizability subject to AppKit constraints. They do not show, focus, or recenter the window.
:systemclears the native override. WebKit inherits appearance; use CSScolor-schemeandprefers-color-schemefor page styling.- Transparent backgrounds use WebKit's private
drawsBackgroundkey. Page backgrounds must also be transparent. Failed private access raisesWindows::AppearanceErrorwith rollback attempted; vibrancy is not implemented. - Before startup, the facade reads/updates configuration. Live access uses the UI context and native state. Access after closure raises
Window::ClosedError. - Other backends accept default options, reject non-default options at native creation, and reject live facade operations with
Windows::UnsupportedError.
macOS website warnings
MacOSOptions.new(fraudulent_website_warnings: false) disables WebKit's fraudulent-website reputation checks before the first document loads. The default is true; there is no live setter. This setting does not change CSP, origin isolation, TLS validation, or WebKit's process sandbox.
File drops (macOS)
Enable receiving on the window, declare an HTML target, then subscribe:
main = app.windows.create(id: :main, path: "/", file_drop: true)
main.on_event(:file_drop) do |event|
drop = event.file_drop
puts "#{drop.target}: #{drop.paths} at (#{drop.x}, #{drop.y})"
end<div file_drop_target="imports">Drop files here</div>| API / attribute | Meaning |
|---|---|
main.file_drop?, main.file_drop = value | Receiving policy; defaults to false |
file_drop_target="name" | ECR target; accepts a static string or reactive String? |
data-file-drop-target="name" | Equivalent attribute for plain HTML |
crykit-file-drop-active | CSS class applied to the hovered target |
drop.paths | Snapshot of absolute local file/directory paths |
drop.target | Declared name on the nearest target ancestor at final drop position |
drop.x, drop.y | Float64 viewport coordinates in CSS pixels |
Receiving needs a connected CryKit document and a declared target. To receive across the page, put the target on a body covering the viewport. CryKit blocks WebView file navigation even when receiving is disabled. File promises and virtual files are not supported. Disabling receiving, navigation, and cleanup clear hover state and pending delivery.
Dragging files out (macOS)
Register trusted paths from Crystal and declare the matching source in HTML:
unsubscribe = main.file_drag_source("report", ["/absolute/path/summary.txt"])<div file_drag_source="report">Drag summary.txt to Finder</div>| Requirement | Rule |
|---|---|
| Source name | Nonblank, NUL-free, at most 128 bytes; duplicate active names raise ArgumentError |
| Paths | Nonempty array of absolute paths without NUL characters |
| Lifetime | Files must exist when dragging starts and remain until copying finishes |
| Cleanup | Returned function unregisters; view-owned registration is removed on unmount |
| HTML helper | Static string or reactive String?; plain HTML uses data-file-drag-source |
| Gesture | Trusted unmodified primary press, then at least four CSS pixels of movement |
| Operation | Native copy; originals remain in place |
Unsubscribe before replacing a source under the same name. Window closure removes all sources. Controls and editable content retain their behavior; source subtrees are excluded from window dragging. Paths supplied by the document are never used. File promises and on-demand generation during a drag are not supported.
Declarative window dragging (macOS)
<header window_drag="true">
<span>My workspace</span>
<div window_drag="false">
<button on_click=<%~ -> { save } %>>Save</button>
</div>
</header>| Declaration | Effect |
|---|---|
window_drag="true" | Enable native window dragging in this region |
window_drag="false" | Exclude the entire subtree |
window_drag=<%= reactive.draggable %> | Reactive Bool?; nil removes the local declaration |
--crykit-draggable: drag | Equivalent CSS declaration for plain HTML |
--crykit-draggable: no-drag | Equivalent CSS exclusion |
Drag regions activate after the CryKit document connects. A trusted, unmodified left mouse-down starts native dragging. Links, form controls, editable content, interactive/focusable widgets, HTML drag sources, and scrollbars keep their normal behavior. An excluded ancestor wins over any enabled descendant. Application mouse-down handlers can cancel dragging with preventDefault().
No window-creation option or Crystal callback is required. For content beneath the title bar, use full_size_content: true and reserve traffic-light clearance in CSS.
Native titlebar double-click actions
Drag regions use the macOS Desktop & Dock → Double-click a window’s title bar preference without another declaration.
| Preference | Action |
|---|---|
| Zoom, or unset | AppKit Zoom and normal restoration |
| Minimize | Minimize to Dock |
| Fill | Fill the screen's available work area; repeat to restore |
| No Action, or unknown | Leave the window unchanged |
Zoom/Fill require a resizable window; Minimize requires a minimizable window. Hidden, minimized, fullscreen, and closing windows ignore the gesture. The action runs on the second mouse release; movement cancels it. Controls, no-drag subtrees, modified clicks, and synthetic events retain their normal behavior. Handlers can cancel eligible mouse-down/up events with preventDefault(). Fill respects content bounds and Reduce Motion; subsequent geometry, chrome, or display changes discard its saved restore frame. It does not change maximized?, which reports AppKit Zoom state.
Ownership and disposal
After :closing listeners return, the window releases its document and cancels its jobs. :closed follows cleanup; native destruction waits for active callbacks to return. A retained closed window cannot keep its document alive or access a replacement using the same ID. See application shutdown for error precedence and application-wide ownership.
Platform support
| Platform | Application windows |
|---|---|
| macOS | AppKit and WKWebView; native loading, controls, appearance, and file dragging |
| Linux / Windows | Native resource loading is unavailable; application startup is rejected |
The startup requirement applies to path:, url:, and html: windows. Native features described as Darwin or macOS are implemented only on macOS.
Source: manager, window, events, macOS options.