Skip to content

Architecture and building blocks ​

CryKit combines a Crystal application with a native webview. Compiled HTML/ECR templates describe the interface; Crystal retains its state, callbacks and reactive computations. The browser parses the HTML and applies subsequent scalar and region mutations through the native bridge.

A native app://crykit resource handler supplies documents and assets directly from application-owned content. Native resource loading is required and is currently implemented on macOS. Backends without that capability fail at startup. Local dispatch uses Crystal's HTTP request/response types as in-memory adapters, without a listener or an HTTP hosting API.

Component map ​

flowchart TD
    Sources[HTML/ECR source] --> Compiler[Compile-time template compiler]
    Compiler --> Definition[Template definitions and Crystal closures]
    App[Application] --> Content[Content and Routes]
    App --> Registry[Views::Registry]
    App --> Windows[Windows::Manager]
    App --> Workers[Worker executor]
    App --> Capabilities[Dialogs, System, Menus and Trays]
    Windows --> Bridge[Transport::Bridge]
    Content --> Handler[Request handler]
    Handler --> Mount[Views::Mount and composed view lifetimes]
    Registry --> Mount
    Definition --> Session[Templates::Session]
    Mount --> Session
    Session --> Instances[Instances, parts and regions]
    Instances --> Consumers[Reactive consumers]
    Instances --> HTML[Initial HTML and document layout]
    HTML --> Browser[Native webview and HTML parser]
    Browser <--> Bridge
    Bridge --> Dispatcher[Events::Dispatcher]
    Dispatcher --> Mount
    Workers --> Updates[Mount update queue]
    Updates --> Bridge
    Instances --> Mutations[Scalar and range mutations]
    Mutations --> Bridge

The diagram shows responsibility and data flow; not every arrow is an object field. How it works follows a document from loading through its event and update cycles.

Compiled rendering ​

The template declaration compiles an ECR file during Crystal compilation. Relative template filenames resolve beside the declaring Crystal source file. The compiler preserves ordinary HTML while recording managed marker locations and generating typed Crystal readers, callbacks and structural definitions in the caller's lexical scope. Crystal checks those expressions; the browser owns HTML parsing. See Compiled templates for the source syntax.

A view's template method constructs a Templates::Template. A mounted Templates::Session instantiates that definition, assigning numeric identities and retaining parts, callbacks and child regions. Static composition is attached once. Conditional regions, keyed iterations and selected child views manage the portions of the interface whose structure can change.

Each text, attribute or property reader and structural selector has a reactive consumer. Reactive getter reads establish its dependencies; reactive writes invalidate the consumers that read them. A flush reevaluates dirty computations and compares their values with the committed values. Scalar changes produce SetText, SetAttribute or RemoveAttribute mutations. Structural changes insert, remove or move anchored HTML ranges. Unchanged conditional selections and retained keyed rows keep their existing instances.

The template method and ordinary Crystal calculations in it run when that definition is constructed. Live values belong in retained readers using reactive getters. Events run untracked in their owning view's scope. An event with no invalidated readers can return an empty mutation batch.

Evaluation stages new values, callbacks, region membership and subscription edges before publication. Views::Mount starts newly introduced view lifecycles before the session commits, then deactivates and cleans up removed views. Failed evaluation preserves the committed template graph. Application state mutations and arbitrary callback effects are not rolled back.

The browser runtime registers only explicit managed element and text targets, plus paired region anchors. Per-instance target manifests verify that browser parsing preserved each expected target's identity, kind and owning range. Unmanaged HTML, SVG and custom elements remain ordinary browser content. Inserted ranges use contextual HTML fragment parsing and validate their targets before attachment. Rendering describes these internals.

Native capabilities ​

Application capabilities live under src/crykit/capabilities/, grouped into CryKit::Windows, CryKit::Dialogs, CryKit::System, CryKit::Menus and CryKit::Trays. The folder adds no namespace segment. Their public entry points are app.windows, app.dialogs, app.system, app.menus and app.trays. Event protocol and dispatch live separately under CryKit::Events.

Capabilities share the same layers: models and public facades retain application state; managers coordinate ownership and lifecycle through injectable Backend and Handle contracts; native.cr selects the default implementation under native/. Engine calls, resource translation and platform sizing belong in that implementation. Contracts, models and explicitly injected managers compile without loading the concrete native implementation.

UIContext supplies shared UI execution, callback lifetime and deferred release. Menus and trays depend on this contract instead of a window backend. Transport::Adapter separately defines document transport and native gestures for Transport::Bridge. The bridge, backend contracts and injected managers compile without loading the WebView engine, checked by make capability-contracts-check.

Synchronous window, menu and tray events share on_event / events and an internal scoped listener collection. Each capability retains its filtering, error policy and dispatch decisions. Asynchronous channel delivery remains in Events::Bus.

Views declare channel dependencies with subscribe_to. Mounting registers each view with its own scope and delivery queue; unmount removes its registrations and suppresses pending delivery. Standalone rendering creates no listener. Application publishers can continue when no listening views are mounted.

Menus retain their model and independent action subscriptions. Tray status items have their own menu managers and popup backends, so repeated definitions retain independent state. Removing a tray invalidates its callbacks immediately and defers native retirement until menu tracking and nested callbacks finish.

Application-facing building blocks ​

BlockResponsibilityGuide
ApplicationOwn content, registry, workers and capability managers; coordinate startup and shutdownApplication
Windows::ManagerOwn windows, handles, bridges, navigation registrations and disposalWindows
Windows::WindowExpose stable identity, live state and native window operationsWindows
Routes / HandlerDeclare exact-path routes; create handlers and mount viewsRoutes and handlers
ViewHold Crystal state, provide a template and define lifecycle hooksViews
HTML/ECR templateDescribe ordinary HTML, reactive readers, callbacks and structural regionsCompiled templates
Document layoutInclude CryKit::Layout and render a document shell containing the mounted fragment exactly onceCompiled templates
Job / Stream(T)Run worker operations and deliver results through mounted scopesBackground jobs
AssetsServe disk or embedded assets under /assets/Assets
Native capability managersCoordinate dialogs, system access, menus and traysDocumentation index

Framework internals ​

BlockResponsibility
Templates::Compiler and macroCompile ECR into HTML, marker metadata and typed Crystal operations
Templates::TemplateHold a compiled definition and its lexical readers, callbacks and child definitions
Templates::Session / InstanceRetain instantiated parts and regions, allocate identities, stage updates and serialize HTML
Reactive::Source / ConsumerTrack reactive dependencies, invalidate readers and publish subscription edges
Views::MountConnect the template session to callbacks, update delivery, DOM effects and view lifetimes
Views::RegistryPublish mounts under owners and documents; release them on teardown
Content::Navigation / Content::BufferedResponseAssociate resource requests with owners and prepare buffered documents
Transport::Bridge / AdapterConnect native handles, inject the runtime and handle document lifecycle notifications
Transport::Resources / RuntimeTranslate native resource requests and embed the shipped browser runtime
Events::DispatcherAuthenticate documents, enforce ordering and acknowledgements, and dispatch mounted work
Events::Bus / Source / ScopedListenersDeliver asynchronous channels and retain scoped synchronous subscriptions
Execution::Scope / QueueOwn cleanup and queue UI blocks and coalesced invalidations
Execution::DeliveryCancel one source's queued deliveries independently of worker execution
Execution::WorkerExecutor / CancellableWaitSchedule workers and provide cancellation-aware waits
Assets::Changes / Native::FileEventsWatch asset changes using platform file notifications
Browser connectionCapture subscribed events, serialize requests and acknowledgements, and report failures
Browser interpreter / TemplateDOMRegister sparse targets, validate manifests and apply scalar and range mutations

The registry locks membership but constructs templates and runs hooks outside its lock. Mutable application state still belongs to its execution context. execution/ supplies scopes, delivery queues, worker scheduling and cooperative waits; job.cr adds mounted worker startup. The public JobExecutor contract remains in job_executor.cr. Channels and streams use delivery tokens without constructing worker jobs.

Loading Content brings in only the registry's owner and document identities, without loading mounted rendering or worker implementations. DOM operation guards use the lightweight Execution::Context to identify worker callbacks; they do not require the Job implementation.

A successful update acknowledges the invalidations it covered. Writes made after a reader evaluated, including writes from new mount hooks, remain pending for another update. This acknowledgement never consumes queued notification callbacks.

Execution and ownership ​

Hosted template evaluation, lifecycle hooks, publication, event dispatch and navigation cleanup run on the owning UI context. Callbacks must stay short and must not yield or reenter the dispatcher. Expensive work belongs in the worker executor; its results return through job.notify. Expiry scheduling posts work back to the UI context. Native asset readers handle bounded file chunks off that thread while delivery and cancellation remain on it.

Each bridge has a registry owner. That owner can have a document being prepared alongside its active document; each document owns its mounts. The dispatcher's event session authenticates the ready document and tracks revisions and acknowledgements across its mounts. It is separate from the Templates::Session that retains one mount's rendering graph.

Composed views share their root mount's session and update queue, while every view instance receives its own update scope. Removing a child expires that scope and suppresses old deliveries. A view instance cannot belong to two live mountings. A mount hook is not a browser-DOM-ready notification.

Route mount requires a navigation owner and document. Their teardown releases the retained views. render(view, layout: ...) uses a response-scoped template session without mount lifecycle hooks. Document layouts also render once per response. Internal Views::Registry#mount(view) callers explicitly release their mounts through unmount or registry closure.

Dev hotpatch changes ordinary text and existing safe presentation attribute values when the compiled shape, actions and bindings are unchanged. Session-local literal consumers publish those changes through the same scalar mutation path. Dev hotreload handles structural or executable source edits by compiling and restarting the application. Asset hotreload refreshes local CSS and tray icons referenced through asset_path, keeping the mounted document, tray menus and application state. The CLI coordinates source watching, hotpatching and rebuilding through crykit dev --watch. See Development updates.

Source orientation ​

Root files expose the application API and subsystem entry points. Internal implementations live under their owning namespaces: views/, execution/, events/, transport/, content/, assets/, native/, templates/ and reactive/. reactive_state.cr, job.cr, app_job.cr and log.cr match the public names they define. The capabilities/ folder deliberately groups the existing public capability namespaces without adding a Capabilities namespace. The separate Views namespace keeps implementation constants out of application classes that include View; view extension macros remain under view/.

Internal types and direct imports of implementation files are unsupported; applications should require crykit and use its documented entry points.

Start with the compiler, template definitions, retained instances, template sessions, and Views::Mount. Native document delivery runs through Application, Views::Registry and Transport::Bridge. The browser half lives in runtime/src, particularly template registration and ranges and the connection.

Built with Crystal. Documented with VitePress.