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
| Block | Responsibility | Guide |
|---|---|---|
Application | Own content, registry, workers and capability managers; coordinate startup and shutdown | Application |
Windows::Manager | Own windows, handles, bridges, navigation registrations and disposal | Windows |
Windows::Window | Expose stable identity, live state and native window operations | Windows |
Routes / Handler | Declare exact-path routes; create handlers and mount views | Routes and handlers |
View | Hold Crystal state, provide a template and define lifecycle hooks | Views |
| HTML/ECR template | Describe ordinary HTML, reactive readers, callbacks and structural regions | Compiled templates |
| Document layout | Include CryKit::Layout and render a document shell containing the mounted fragment exactly once | Compiled templates |
Job / Stream(T) | Run worker operations and deliver results through mounted scopes | Background jobs |
Assets | Serve disk or embedded assets under /assets/ | Assets |
| Native capability managers | Coordinate dialogs, system access, menus and trays | Documentation index |
Framework internals
| Block | Responsibility |
|---|---|
Templates::Compiler and macro | Compile ECR into HTML, marker metadata and typed Crystal operations |
Templates::Template | Hold a compiled definition and its lexical readers, callbacks and child definitions |
Templates::Session / Instance | Retain instantiated parts and regions, allocate identities, stage updates and serialize HTML |
Reactive::Source / Consumer | Track reactive dependencies, invalidate readers and publish subscription edges |
Views::Mount | Connect the template session to callbacks, update delivery, DOM effects and view lifetimes |
Views::Registry | Publish mounts under owners and documents; release them on teardown |
Content::Navigation / Content::BufferedResponse | Associate resource requests with owners and prepare buffered documents |
Transport::Bridge / Adapter | Connect native handles, inject the runtime and handle document lifecycle notifications |
Transport::Resources / Runtime | Translate native resource requests and embed the shipped browser runtime |
Events::Dispatcher | Authenticate documents, enforce ordering and acknowledgements, and dispatch mounted work |
Events::Bus / Source / ScopedListeners | Deliver asynchronous channels and retain scoped synchronous subscriptions |
Execution::Scope / Queue | Own cleanup and queue UI blocks and coalesced invalidations |
Execution::Delivery | Cancel one source's queued deliveries independently of worker execution |
Execution::WorkerExecutor / CancellableWait | Schedule workers and provide cancellation-aware waits |
Assets::Changes / Native::FileEvents | Watch asset changes using platform file notifications |
| Browser connection | Capture subscribed events, serialize requests and acknowledgements, and report failures |
Browser interpreter / TemplateDOM | Register 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.