Logo FS-Skia-UI

Controls

FS.Skia.UI.Controls provides Skia-rendered declarative controls for Elmish-shaped view functions. Persistent values such as text, selected items, validation state, and committed values stay in the application model. Transient interaction state is held by a product-owned ControlRuntimeModel, keyboard state is held by FS.Skia.UI.KeyboardInput.KeyboardModel, and direct command or subscription wiring lives in FS.Skia.UI.Controls.Elmish.

The public surface is owned by src/Controls/*.fsi and the supported catalog is declared by src/Controls/catalog.yml plus Catalog.supportedControls. Charts and graphs are Controls-owned for generated products; the previous Charts capability remains compatibility source only and is not selected by the default app profile.

Validate changes with:

./fake.sh build -t ControlsCatalogCheck
./fake.sh build -t ControlsInteractionCheck
./fake.sh build -t ControlsRenderingCheck
./fake.sh build -t PackageSurfaceCheck

Supported Catalog

The catalog has 47 supported rows across display, input, selection, navigation, layout, feedback, data, chart, graph, and custom categories. Every row in src/Controls/catalog.yml records purpose, required attributes, common attributes, events where applicable, visual states, accessibility metadata, examples, tests, evidence paths, support status, and Controls ownership.

Authoring Pattern

Controls follow the same create : Attr<'msg> list -> Control<'msg> shape. Persistent state is read from the model and events emit messages:

let view model =
    Stack.create [
        Stack.children [
            TextBlock.create [ TextBlock.text model.Title ]
            TextBox.create [
                TextBox.value model.Name
                TextBox.validation model.NameValidation
                TextBox.onChanged NameChanged
            ]
            Button.create [
                Button.text "Save"
                Button.enabled model.CanSave
                Button.onClick SaveRequested
            ]
            RichText.create model.RichIntro []
        ]
    ]

Stateful interaction stays explicit:

let keyboard, keyboardEffects =
    Keyboard.init [ { Key = "S"; Command = "save" } ]

let runtime, _ = ControlRuntime.init ()

let commands =
    keyboardEffects
    |> List.collect (ControlsElmish.interpretKeyboardEffect (fun _ -> SaveRequested))

The maintained sample in samples/ControlsGallery combines stable records, rich text, chart and graph controls, a custom Skia escape hatch, ControlRuntime, KeyboardInput, and Controls.Elmish adapter wiring through the public package surfaces.

Controls And Lower-Level Paths

Controls is the supported high-level authoring path for ordinary controls, rich Skia rendering, chart controls, graph views, and DataGrid in generated product profiles. It is appropriate when product code wants declarative control records, generic product messages, catalog validation, and optional adapter wiring.

Lower-level packages remain supported for products that do not select Controls:

Path

Use when

FS.Skia.UI.Scene

Product code needs immutable scene primitives, paint data, diagnostics, or render-readback evidence without controls.

FS.Skia.UI.Layout

Product code needs layout evaluation or graph layout helpers while owning its own view/control layer.

FS.Skia.UI.KeyboardInput

Product code needs keyboard runtime state, YAML command configuration, diagnostics, or state display without Controls.

FS.Skia.UI.SkiaViewer

Product code needs the desktop Skia/Vulkan host boundary directly.

FS.Skia.UI.Elmish

Product code needs the general viewer Elmish integration without Controls-specific command interpretation.

FS.Skia.UI.Controls.Elmish

Product code uses Controls and wants command, subscription, or program helpers for Controls and KeyboardInput effects.

Charts, Graphs, And DataGrid

Charts and graph views are authored with LineChart, BarChart, PieChart, ScatterPlot, and GraphView from FS.Skia.UI.Controls. DataGrid is a data control, not a chart category; products provide columns, rows, selection/focus state, sort/filter metadata, and visible-range state through the Controls DataGrid APIs. Known controls, events, attributes, chart data, and DataGrid data use typed front doors such as LineChart.series, GraphView.nodes, DataGrid.columns, DataGrid.rows, DataGrid.visibleRange, DataGrid.selectedRows, and DataGrid.focusedCell. Deliberate extension points stay available through visibly custom APIs: Control.customControl, Attr.customAttribute, and Attr.customEvent.

Catalog rows for chart, graph, and DataGrid controls link to specs/011-controls-boundary-refactor/readiness/chart-datagrid-controls.md for implementation and validation evidence. New applications should not select the legacy Charts package or a chart-specific generated skill.

Existing chart users should migrate new authoring to FS.Skia.UI.Controls: LineChart, BarChart, PieChart, and ScatterPlot cover chart controls, GraphView covers graph-view authoring, and DataGrid covers table-like data controls. This is a replacement path, not a compatibility shim or automated external-app migration promise.

Validation Path

Use ./fake.sh build -t Dev for the default framework build and tests. Use ControlsCatalogCheck, ControlsInteractionCheck, and ControlsRenderingCheck before adding or changing catalog rows. Use FsiTranscripts, SampleContractSmoke, GeneratedProductCheck, and TemplateCheck before changing public surface, samples, or generated product support. Use TargetMetadataDrift when adding, renaming, or changing control validation targets so control docs, generated guidance, target metadata, and the native FAKE registry stay aligned.

For the form-and-dashboard walkthrough, build from the catalog docs with at least 10 controls, 3 nested layout regions, and 5 interactions. The maintained reference path is samples/ControlsGallery --contract-smoke; first-time human evaluator evidence is release-readiness work and is tracked separately from the in-repo command evidence.

Multiple items
module List from Microsoft.FSharp.Collections

--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
val collect: mapping: ('T -> 'U list) -> list: 'T list -> 'U list

Type something to start searching.