Logo FS-Skia-UI

Design Decisions

This document records the major architectural choices behind FS.Skia.UI. It is not a full ADR archive; it is the current decision log for the choices that shape subsystem design, testing, packaging, and template governance.

DD-001: Elmish Program Boundary

Decision: Applications use Model, Msg, init, update, view, and explicit effects through ViewerProgram<'model, 'msg>.

Rationale: UI state and workflow transitions must be testable without a window or GPU. Elmish gives contributors one consistent way to express stateful behavior across samples, runtime hosts, keyboard input, layout workflow, and build workflow governance.

Consequences: update paths should stay pure. Host work enters through messages and leaves through effects. New stateful features need model/message coverage and effect assertions before they are treated as complete. See src/Lib/Library.fsi for ViewerProgram and ViewerEffect, src/Lib/KeyboardInput.fsi for keyboard input's reducer surface, src/Layout/Layout.fsi for layout workflow functions, and build.fsx for the local build workflow effect algebra.

DD-002: Declarative Scene Model

Decision: Public UI output is an immutable Scene, not direct Skia drawing callbacks.

Rationale: Scene values can be inspected, tested, transformed, composed, and reused by charts, layout, keyboard displays, and samples. Direct drawing callbacks would couple product code to renderer lifetime and make non-visual testing much weaker.

Consequences: Renderer-specific details stay behind Viewer.run. Subsystems should return scenes and diagnostics, not Skia objects. The public Scene contract is in src/Lib/Library.fsi, and pure subsystem scene builders live in src/Charts and src/Layout.

DD-003: Vulkan-Only Desktop Renderer

Decision: The first renderer is Windows/Linux desktop with Vulkan, Silk.NET, and SkiaSharp. There is no OpenGL, CPU, browser, mobile, or fallback renderer.

Rationale: A narrow renderer target keeps the first public surface coherent and makes unsupported environment diagnostics explicit. Silent fallback would hide driver/platform failures and weaken evidence.

Consequences: Startup failures are reported through RenderDiagnostic. Documentation and tests must avoid implying broader platform support until a future feature adds and verifies it. The Vulkan/Silk.NET/SkiaSharp host code is implemented in src/Lib/Library.fs.

Roadmap boundary: Current support remains .NET desktop with the Vulkan renderer path and SkiaSharp preview dependency risk. macOS, mobile, browser, OpenGL, CPU/software rendering, and fallback renderer support are separate future platform-expansion features, not current support.

DD-004: Pure Widget Packages Over Host-Owned Widgets

Decision: Charts, DataGrid, layout, and graph features are pure scene builders and reducers. They do not own windows, renderer state, or host effects.

Rationale: This keeps FS.Skia.UI.Charts and FS.Skia.UI.Layout composable and usable from samples or products that own their own Elmish state.

Consequences: Interaction state such as selection, sorting, viewport, focus, and visibility belongs to the caller. Subsystems provide hit testing, validation, computed bounds, and scene output. Chart and table code lives in src/Charts; layout and graph code lives in src/Layout.

DD-005: Visibility Lives In Signature Files

Decision: Public runtime surface is governed by .fsi files and package surface baselines.

Rationale: F# signature files make the exported API explicit and reviewable. Baselines catch accidental public surface drift.

Consequences: Runtime public API changes require matching .fsi, semantic tests, package surface review, and evidence updates. The runtime signature files are in src/Lib, src/Charts, and src/Layout; package baselines are in readiness/surface-baselines.

DD-006: Non-Visual Contract Smoke As The Fast Path

Decision: Every current sample has a --contract-smoke path that exercises public APIs without opening a live window.

Rationale: Live Vulkan/window validation is valuable but environment-heavy. Contract smoke keeps the default Verify and Ci gates useful on developer and automation machines that can build and run non-visual tests.

Consequences: Contract smoke is not a substitute for explicit visual evidence when a feature requires pixel or live-renderer proof. Deferred visual scope must remain documented until a future target owns it. The sample hosts live in samples, with smoke coverage in tests/Smoke.Tests.

DD-007: Repository As Governed Template Source

Decision: The repository itself owns the fs-skia-ui template and validates both source-directory and local-package template installation.

Rationale: Generated products should inherit the current build, dependency, documentation, evidence, and Spec Kit governance without manual copying from historical feature directories.

Consequences: Template-owned source changes must stay aligned with template metadata, docs, dependency policy, generated guidance, build targets, or an explicit deferral. TemplateDrift enforces this boundary through scripts/template-drift.fsx, while template metadata lives in .template.config/template.json.

DD-008: Central Package Management

Decision: Direct external package versions are centralized in Directory.Packages.props.

Rationale: Runtime, test, sample, and generated-project dependency versions must be reviewable in one place. This is especially important while SkiaSharp 4 is still preview-risk.

Consequences: Repo-owned project files use versionless external PackageReference entries. Inline package versions are allowed only for documented validation-only local package checks. Versions are declared in Directory.Packages.props, and the report script is scripts/dependency-report.fsx.

Type something to start searching.