一键导入
fs-skia-ui-widgets
Build Skia-rendered FS.Skia.UI Controls, rich text, chart controls, graph controls, DataGrid, custom wrappers, and generated product examples.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Build Skia-rendered FS.Skia.UI Controls, rich text, chart controls, graph controls, DataGrid, custom wrappers, and generated product examples.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Consumer-facing guide to hosting an interactive FS.Skia.UI app — the keyboard/pointer input surface, the preview-vs-tree render distinction, and the windowed-fullscreen blur caveat.
Generated product guidance for Skia-rendered FS.Skia.UI Controls, rich text, chart controls, graph controls, DataGrid, and custom wrappers.
Wire a generated FS.Skia.UI product to the desktop viewer host.
Understand and work with the internal keyed VDOM diff over the lowered Control<'msg> IR (feature 067) — its key-first-then-positional matching, the NodePatch/ChildOp operation set, the totality/determinism/identity-at-rest/round-trip invariants, and the module's disposition (internal, property-tested, wired onto the live render path via RetainedRender in feature 091 and current through feature 103 — layout/bounds cache, injected-delta animation clock, visual-state cross-fade). Use when reading the diff invariants, extending the property tests, or working on the wired retained render path.
Maintainer-facing guide to the FS.Skia.UI.Controls.Elmish interactive-host seam — how runInteractiveApp drives the retained render structure each frame (RetainedRender.step over the keyed diff), advances per-identity animation clocks from an injected Tick delta, stamps runtime visual state pre-reconcile, routes keys focus-first through routeFocusedKey, and resolves pointer hits to a stable identity via retainedHitTest. Use when reading or extending the live controls host loop, the per-frame retained-state/clock/visual-state wiring, or the key/pointer routing seam.
Generated product guidance for Skia-rendered FS.Skia.UI Controls, rich text, chart controls, graph controls, DataGrid, and custom wrappers.
基于 SOC 职业分类
| name | fs-skia-ui-widgets |
| description | Build Skia-rendered FS.Skia.UI Controls, rich text, chart controls, graph controls, DataGrid, custom wrappers, and generated product examples. |
Use this skill for user-facing controls built with FS.Skia.UI.Controls:
forms, buttons, text input, lists, tables, rich text, layout containers, chart
controls, graph controls, DataGrid, custom control wrappers, catalog examples,
and generated product guidance.
The supported API lives in src/Controls/*.fsi. View functions should build
Control<'msg> values with module-per-control create functions and
declarative attributes such as TextBox.value, Button.onClick,
LineChart.series, DataGrid.columns, DataGrid.rows, and Stack.children.
Persistent values stay in the product model; controls may keep only keyed
transient interaction state through product-owned ControlRuntime.
CustomControl does NOT rasterize its content (feature 122)Control.renderTree (the production paint path the live host and every
screenshot/preview use) paints a labeled placeholder for a custom-control
— it does not invoke the CustomControlDefinition Render/Draw/Layout
fields, so authored Skia geometry does not appear in the window or in evidence.
CustomControl is a wrapper for product-owned events/attributes, not a
draw seam. When geometry must show in the rasterized/screenshot path, build it
from primitive controls (Border + TextBlock + Stack); reserve
CustomControl for non-visual extension points. (A null/blank Id or a null
effect string is guarded — validate/create return a diagnostic, never an NRE.)
Generated examples should keep product state and messages local:
type Msg =
| NameChanged of string
| SaveRequested
| GridSelectionChanged of string
type Model =
{ Name: string
Revenue: ChartSeries list
Columns: DataGridColumn list
Rows: DataGridRow list }
let view model : Control<Msg> =
Stack.create [
Stack.children [
TextBox.create [
TextBox.value model.Name
TextBox.onChanged NameChanged
]
Button.create [
Button.text "Save"
Button.onClick SaveRequested
]
LineChart.create [ LineChart.series model.Revenue ]
GraphView.create [ GraphView.nodes [ "form"; "chart"; "grid" ] ]
DataGrid.create model.Columns [
DataGrid.rows model.Rows
DataGrid.visibleRange {
FirstIndex = 0
Count = model.Rows.Length
Total = model.Rows.Length
}
]
]
]
When Elmish program integration is selected, use the
FS.Skia.UI.Controls.Elmish adapter for commands, subscriptions, and program
wiring at the product edge.
The Controls runtime is a declarative-retained MVU core: you write a single
view : 'model -> Control<'msg> (or build Widget<'msg> through the typed front
door FS.Skia.UI.Controls.Typed), and the framework supplies five composable,
all-shipped capabilities. None of them is a data binding, DataContext, or
lookless ControlTemplate — those remain permanent non-goals.
An authored event lowers to a binding keyed by the control's ControlId; the host
loop routes a ControlEvent to it through Control.dispatch, returning the 'msg
your update folds in.
open FS.Skia.UI.Controls
open FS.Skia.UI.Controls.Typed
let saveButton =
Button.view { Button.defaults with Id = Some "save"; Text = "Save"; OnClick = Some SaveRequested }
// host loop: Control.dispatch { Kind = "click"; ControlId = Some "save"; Origin = ControlEventOrigin.Pointer; Payload = None } tree
// => [ SaveRequested ]
Retained identity is a property of the keyed tree, not a binding. Give a control
a stable Id; the keyed reconciler matches it key-first across a sibling-shifting
re-render, so its focus / caret / text / animation state survive even when an
unrelated sibling is inserted above it. Omit the key and a positional shift resets
that transient state.
// the editor stays keyed, so inserting a banner above it does NOT reset its caret:
Stack.view
{ Stack.defaults with
Children =
[ banner
Button.view { Button.defaults with Id = Some "editor"; Text = "Edit" } ] }
Attach an ordered StyleClass list (typed Variant or free-form Custom); the
resolver folds base < classes-in-order < visual-state with fixed precedence. No
CSS selectors.
The runtime visual state has a public entry point (feature 096): deriveVisualState model controlId : VisualState is the pure, total projection from live interaction state
to a single VisualState under the fixed closed precedence tail Pressed > Selected > Focused > Hover > Normal (a control named by no interaction state yields Normal). That is
the state the resolver folds in. The host stamps it onto the lowered tree pre-reconcile
via the internal applyRuntimeVisualState model control (it preserves a consumer-set
non-Normal attribute and emits nothing at Normal, so a resting control is byte-identical);
consumers read state through the public deriveVisualState, not the internal bridge.
Button.view { Button.defaults with Text = "Delete"; Classes = [ Variant StyleVariant.Danger ] }
// the current visual state rides the control through the reconciler:
// ControlRuntime.deriveVisualState model "delete" // => Pressed when the pointer is down on it
Focus.order derives the deterministic tab order from accessibility metadata and
Focus.traverse moves Next/Previous (wrapping). A focusable control inside a
non-focusable container is its own tab stop.
Focus.route (as it ships after feature 100) takes role, keyboard, navRange,
key, isTab, shift and returns a closed KeyRouting = Activate | Navigate of NavIntent | Traverse of FocusMove | Fallthrough. A focused navigation key is
classified by role (and the declared navRange for a value role) into a closed
NavIntent = ValueStep of delta (a signed step the host applies to the live value and
clamps) | SelectionMove of Direction | GridMove of rowDelta * colDelta, carried by
KeyRouting.Navigate. Activation and navigation are tested before the Tab test, so a
control that lists a traversal key consumes it; only an unconsumed Tab/Shift+Tab yields
Traverse.
let order = Focus.order tree
let next = Focus.traverse order (Some "save") Next // FocusMove.Next
// route the focused control's key; a slider's Right-arrow becomes a Navigate (ValueStep):
match Focus.route role keyboard navRange "ArrowRight" false false with
| Navigate (ValueStep delta) -> () // host adds delta to the live value, clamps
| _ -> ()
Fill a control's declared, per-kind, typed slot regions with your OWN
Widget<'msg> to re-skin its shape — an icon before a button's label, a custom
panel header/footer. A slot fill is a static Control<'msg> your view already
computed — not a data-bound template, DataContext, or binding. The regions are
closed per kind: filling a region a kind does not declare is a compile error.
An unfilled slot renders the kind's existing chrome (byte-identical to before).
let icon = TextBlock.view { TextBlock.defaults with Text = "★" }
// Button declares Leading / Trailing; Panel declares Header / Footer:
let starred =
Button.view { Button.defaults with Text = "Save"; Leading = Some icon }
let framed =
Panel.view
{ Panel.defaults with
Header = Some(TextBlock.view { TextBlock.defaults with Text = "Settings" })
Children = [ body ] }
// Button.view { Button.defaults with Header = ... } // does NOT compile — Button declares no Header (closed per kind)
Slotted content is a first-class sub-tree: it composes with E1 dispatch, E3 styling,
and E4 focus, and keeps its E2 retained identity across a re-render — free,
because the fill lands in the control's Children, not a parallel channel.
Run ./fake.sh build -t Dev for normal development and
./fake.sh build -t VerifyPreflight before broad verification. Run
./fake.sh build -t Verify before readiness sign-off. Use ./fake.sh build -t PackLocal and ./fake.sh build -t PackageSurfaceCheck when changing .fsi
files. If Verify or Ci reports environment-failure, focused gates are
diagnostic only; final readiness needs a later healthy broad pass in a fresh
shell, fresh container, or CI runner.
Run dotnet test tests/Controls.Tests/Controls.Tests.fsproj for focused
coverage. The governed targets are ./fake.sh build -t ControlsCatalogCheck,
./fake.sh build -t ControlsInteractionCheck, and
./fake.sh build -t ControlsRenderingCheck.
Update the active feature readiness reports for control catalog, semantic
tests, interaction tests, layout/rendering, public surface, and generated
product evidence when behavior or public surface changes. Stable public
surface baselines live under readiness/surface-baselines/. Supported catalog
rows need purpose, attributes, events, visual states, accessibility metadata,
examples, tests, and evidence.
Controls owns ordinary controls, rich text, chart controls, graph controls, DataGrid, custom wrappers, the catalog, and generated controls guidance. Scene, SkiaViewer, Elmish, KeyboardInput, Layout, and Testing remain separate capabilities for lower-level or host-specific work. Layout remains a runtime package dependency; generated control authoring stays in Controls.
Generated products with Controls receive this skill. Product examples must be product-owned and must not copy framework galleries, samples, historical specs, readiness evidence, docs, or implementation projects.
Users moving from the legacy Charts package should replace chart declarations
with Controls LineChart, BarChart, PieChart, ScatterPlot, GraphView,
and DataGrid declarations. There is no compatibility shim; generated
products should use FS.Skia.UI.Controls directly.
When a problem outlasts reasonable in-repo attempts, extensive external research is
mandatory — consult official online docs first (the F#/.NET docs and the driven
library's own documentation/API reference), then community sources (forums, Reddit, Q&A
sites, issue trackers and changelogs). Record the findings and resolving links in the
feature's specs/<feature>/feedback/ folder and, for durable lessons, in this skill's
Sources line. Offline, the mandate degrades to recording "research blocked — "
rather than hard-failing the phase.