Skip to content

Windows API ​

Access windows through app.windows : CryKit::Windows::Manager. Applications currently run on macOS; see platform support.

Manager API ​

CallReturnsPurpose
app.windows.create(id: ..., path: ...)WindowCreate a window from a route, URL, or HTML; options below
app.windows[id]WindowFind 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.sizeInt32Count active windows
app.windows.empty?BoolCheck whether any active windows remain
crystal
main = app.windows.create(id: :main, path: "/", title: "My app",
  width: 1100, height: 700)
app.run

Configure 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.

ArgumentType / defaultPurpose
id:Symbol, requiredUnique active window identity
path:String? = nilAbsolute local route, without query or fragment
url:String? = nilAbsolute URL
html:String? = nilHTML source; an empty string is valid
title:String = "New Window"Window title
width:, height:Int32 = 640, 480Initial content size in logical points
min_width:, min_height:Int32? = nilMinimum content dimensions
max_width:, max_height:Int32? = nilMaximum content dimensions
resizable:Bool = trueAllow user resizing
hidden:Bool = falseLoad without initially presenting the window
always_on_top:Bool = falseUse the floating window level
file_drop:Bool = falseEnable receiving files on declared HTML targets
macos:MacOSOptions.newNative 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.

crystal
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 ​

ReadWriteType / meaning
id—Immutable Symbol identity
titletitle = valueString; last configured/requested title
sizesize(width, height)Size; content dimensions
width, heightwidth = value, height = valueInt32; 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 = valueBool; floating window level
file_drop?file_drop = valueBool; 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.

CallPurpose
show, hide, visible?Present, hide, or query native visibility
focusRequest key-window focus and application activation
auto_hide_on_focus_loss?, auto_hide_on_focus_loss = valueHide 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
centerCenter 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
geometryRead 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 ​

CallReturns / purpose
closeRequest 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 ​

EventPayload / actionWhen it fires
:close_requestedreason; cancelBefore native close, close, or application quit
:closingwindowClosure accepted; still in lookup and closed? is false
:closedwindowRemoved from lookup; document and jobs released
:focuswindowNative window becomes key
:lost_focuswindowNative window resigns key status
:movedgeometryOuter-frame position changes
:resizedgeometryOuter frame or content size changes
:document_readywindowA document connects to CryKit
:file_dropfile_dropLocal files land on an enabled HTML target
:navigatingurl; cancelAn application link requests document replacement
:renderer_terminatedrecovery_available?; cancelWebKit 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.

crystal
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.call

Subscriptions 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.
  • :closing and :closed each 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 :closing onward. Recursive close is a no-op. Retaining the window cannot affect another window reusing its ID.

Close interception ​

crystal
main.on_event(:close_requested) do |event|
  if draft.unsaved? && !app.dialogs.confirm("Unsaved changes", "Discard this draft?")
    event.cancel
  end
end
MemberMeaning
reason:native, :api, or :quit (Window::CloseRequest::Reason)
cancelKeep 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.

crystal
main.on_event(:navigating) do |event|
  event.cancel if draft.unsaved? && !app.dialogs.confirm("Leave page?", "Discard changes?")
end

event.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:

crystal
main.on_event(:renderer_terminated) do |event|
  event.cancel if shutting_down
  puts event.recovery_available?
end

The 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 ​

crystal
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 ​

crystal
snapshot = main.geometry
main.on_event(:resized) { |event| puts event.geometry.content_size }
main.on_event(:moved) { |event| puts event.geometry.frame }
Value typeFields
Windows::Sizewidth : Int32, height : Int32
Windows::Positionx : Int32, y : Int32
Windows::Framex : Int32, y : Int32, width : Int32, height : Int32
Windows::Geometryframe : 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 ​

crystal
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 ​

crystal
main.size(900, 650)
main.position(120, 100)
main.frame(120, 100, 1000, 720)
main.center
main.minimize
main.unminimize
main.toggle_maximize
Measurement / operationBehavior
Content size and boundsLogical points, excluding default title-bar decorations
FrameOuter window including decorations
PositionPrimary display's top-left origin; x rightward, y downward
Native readbackRounded 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
centerNative placement, slightly above vertical center; does not show the window
Maximize operationsAppKit 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 ​

crystal
main.unminimize
main.show
main.focus
main.auto_hide_on_focus_loss = true
Call / stateBehavior
showOrder the window front; does not explicitly restore or focus it
hideHide without releasing its document, views, jobs, or geometry
focusRequest 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 = trueHide when key status is lost, including to another window or dialog
hidden: trueLoad 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 ​

crystal
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 argumentRead / write on window.macosDefault
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 warningstrue
EnumValues
Windows::Appearance:system, :light, :dark
Windows::ToolbarStyle:automatic, :expanded, :preference, :unified, :unified_compact
crystal
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 = :system
Appearance 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-nil toolbar style attaches an empty native toolbar; nil removes 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.
  • :system clears the native override. WebKit inherits appearance; use CSS color-scheme and prefers-color-scheme for page styling.
  • Transparent backgrounds use WebKit's private drawsBackground key. Page backgrounds must also be transparent. Failed private access raises Windows::AppearanceError with 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:

crystal
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
ecr
<div file_drop_target="imports">Drop files here</div>
API / attributeMeaning
main.file_drop?, main.file_drop = valueReceiving 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-activeCSS class applied to the hovered target
drop.pathsSnapshot of absolute local file/directory paths
drop.targetDeclared name on the nearest target ancestor at final drop position
drop.x, drop.yFloat64 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:

crystal
unsubscribe = main.file_drag_source("report", ["/absolute/path/summary.txt"])
ecr
<div file_drag_source="report">Drag summary.txt to Finder</div>
RequirementRule
Source nameNonblank, NUL-free, at most 128 bytes; duplicate active names raise ArgumentError
PathsNonempty array of absolute paths without NUL characters
LifetimeFiles must exist when dragging starts and remain until copying finishes
CleanupReturned function unregisters; view-owned registration is removed on unmount
HTML helperStatic string or reactive String?; plain HTML uses data-file-drag-source
GestureTrusted unmodified primary press, then at least four CSS pixels of movement
OperationNative 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) ​

ecr
<header window_drag="true">
  <span>My workspace</span>
  <div window_drag="false">
    <button on_click=<%~ -> { save } %>>Save</button>
  </div>
</header>
DeclarationEffect
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: dragEquivalent CSS declaration for plain HTML
--crykit-draggable: no-dragEquivalent 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.

PreferenceAction
Zoom, or unsetAppKit Zoom and normal restoration
MinimizeMinimize to Dock
FillFill the screen's available work area; repeat to restore
No Action, or unknownLeave 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 ​

PlatformApplication windows
macOSAppKit and WKWebView; native loading, controls, appearance, and file dragging
Linux / WindowsNative 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.

Built with Crystal. Documented with VitePress.