drawnui/drawnui.net/drawnui-fluent

Claude Code and agent skill by drawnui/drawnui.net

Trust tier 3
  • community
  • ext:[{"type":"hook","description":"Pre/post lifecycle hooks"}]
Source check pending

Checking that the SKILL.md file is still in the repository.

Use when writing DrawnUI C# code-behind with fluent extensions. Covers inline control construction, .Assign(out _field), .Initialize(), .OnTapped(), .OnHovered(), .OnTextChanged(), .ObserveProperty(), .ObserveProperties(), .ObservePropertyTwoWay() (two-way binding), .Adapt(), .WhenPaint(), .ObserveSelf(), layout aliases (SkiaStack/SkiaRow/SkiaLayer), one-shot and looping animations, gradients, colors, shadows, SkiaLottie, SkiaImageTiles, and SkiaBackdrop code-behind patterns. Load before any DrawnUI C# composition task.

Install

npx vskill@latest install drawnui/drawnui.net/drawnui-fluent

Semi-extensibleMentions customization, but needs skill-specific knowledge. Read the extensible skills standard

SKILL.md

DrawnUI Fluent Extensions — C# Code-Behind

Containers — use semantic aliases

Compose with alias containers, not raw SkiaLayout { Type = LayoutType.X }: SkiaStack (vertical, Fill), SkiaRow (horizontal), SkiaLayer (absolute overlay, Fill), SkiaWrap (Fill), SkiaMasonry (Fill, 2 columns), SkiaGrid (Fill). Aliases preset Type AND HorizontalOptions=Fill (except SkiaRow) — raw SkiaLayout defaults to Type=Absolute with NO fill, so it aligns differently. SkiaFrame = SkiaShape rectangle, not a layout.

Constructor shorthand is the documented idiom for text controls: new SkiaLabel("text"), new SkiaRichLabel("**md**"), new SkiaButton("Caption").

Row does NOT distribute Fill children (device-verified 2026-08-15): two HorizontalOptions=Fill children inside a SkiaRow each take full width and overflow the screen — a Row is not a star-grid. For proportional button rows use SkiaGrid + .WithColumnDefinitions("2*,3*") + .SetGrid(col, 0) per child. Same for HorizontalFillRatio inside a Row — not a substitute for grid columns. A child that is not Fill does get what is left, as in a SkiaStack vertically: a wrapping label after a title wraps in the remaining width, and an End child sits at the row's end. Pick containers in the order SkiaLayer (alignment + Margin), SkiaStack, SkiaRow, SkiaWrap, and SkiaGrid last (drawnui skill, "Choosing a container").

Composition Style — MANDATORY

Never declare a local variable for a control and then reference it in a children list. Always construct inline inside the collection initializer. Use .Assign(out _field) when a field reference is needed — it returns the control so chaining continues.

Do not spam `AddSubView(...)` while building a known static scene or container subtree. When the children are known at composition time, declare them together inside Children = new List<SkiaControl> { ... }. Reserve AddSubView() for dynamic/runtime operations such as pooled controls, generated rows, incremental updates, or add/remove flows that happen after the initial tree is built.

Critical: Static vs Runtime Children Mutation

Before layout is ready (static composition in constructor):

csharp
// ✅ CORRECT — initial tree building
Children = new List<SkiaControl>
{
new SkiaLabel { Text = "A" },
new SkiaLabel { Text = "B" },
};

After layout is ready (runtime mutation, e.g. in RefreshFriendsList):

csharp
// ✅ CORRECT — runtime mutation
layout.ClearChildren();
layout.AddSubView(new SkiaLabel { Text = "New" });
layout.RemoveSubView(existingChild);
// ✅ also works on the control's own (default) Children collection: it is observed,
// Add / Insert / Remove / Move / Replace / Clear change what is drawn
layout.Children.Add(child);
layout.Children.RemoveAt(0);
// ❌ WRONG — after `Children = new List<SkiaControl> { ... }` the collection is that plain list,
// which nobody observes: later Add / Clear on it are invisible to the engine
layout.Children.Add(child); // ignored when Children was assigned a List

Build the full list first and assign once (verified 2026-09-16: popup wrappers built as Children = new List<SkiaControl>() followed by Children.Add(x) never rendered and their ScaleToAsync never completed). The collection-initializer form Children = { a, b } (no new List) adds to the control's own observed collection.

Rule of thumb: Use Children = new List<...> only during initial construction. Once LayoutIsReady has fired or the control is in the visual tree, use ClearChildren(), AddSubView(), RemoveSubView(): they work whatever collection Children holds.

WRONG — never do this:

csharp
var label = new SkiaLabel { Text = "hi" };
Children = new List<SkiaControl> { label };

RIGHT — always inline:

csharp
Children = new List<SkiaControl>
{
new SkiaLabel { Text = "hi" }.Assign(out _label),
};

Nested layout:

csharp
Children = new List<SkiaControl>()
{
new SkiaStack()
{
Spacing = 8,
Children = new List<SkiaControl>()
{
new SkiaLabel() { Text = "hi" }.Assign(out _label),
}
},
};

Children = { ... } (bare collection initializer) and .WithChildren(a, b, c) are equivalent inline forms — fine too. .WithContent(child) sets the single child of IWithContent containers (e.g. SkiaScroll, ContentLayout). .AssignParent(parent) adds the control to a parent mid-chain.

WRONG for static composition:

csharp
AddSubView(new SkiaLabel { Text = "A" });
AddSubView(new SkiaLabel { Text = "B" });
AddSubView(new SkiaLabel { Text = "C" });

RIGHT for static composition:

csharp
Children = new List<SkiaControl>
{
new SkiaLabel { Text = "A" },
new SkiaLabel { Text = "B" },
new SkiaLabel { Text = "C" },
};

Chain after .Assign() — it returns the control:

csharp
new SkiaLabel() { ... }
.ObserveProperty(source, nameof(Score), me => { me.Text = $"{source.Score}"; })
.Assign(out LabelScore)
.OnTapped(me => { ... })

Omit redundant default assignments like Left = 0, Top = 0. Keep explicit zeros only in reset/pooling paths where prior state may still be present.

Layout shortcuts: .Center() / .CenterX() / .CenterY() (control inside parent), .Fill() / .FillX() / .FillY(), .StartX() / .StartY() / .EndX() / .EndY(). Text alignment inside a SkiaLabel (NOT the label's own position): .CenterText() / .CenterTextX() / .CenterTextY() — set HorizontalTextAlignment / VerticalTextAlignment to Center.

Property shortcuts (all chainable): .WithHeight(n) / .WithWidth(n), .WithMargin(all) / (h,v) / (l,t,r,b) / (Thickness), .WithPadding(...), .WithCache(SkiaCacheType.X), .WithBackgroundColor(c), .WithHorizontalOptions(...) / .WithVerticalOptions(...), .WithVisibility(bool), .WithTag("name"); shape: .Shape(ShapeType.Circle); image: .WithAspect(TransformAspect.X); label: .WithFontSize(n), .WithTextColor(c), .WithHorizontalTextAlignment(...).

Grid placement helpers

Definitions are set on the grid itself (string form, chainable), placement on each child:

csharp
new SkiaGrid()
{
DefaultRowDefinition = new RowDefinition(GridLength.Star), // avoids repeating N identical rows
Children = new List<SkiaControl>()
{
new SkiaSvg() { ... }.SetGrid(0, 0, 1, 2), // COLUMN first, then row, then colspan, rowspan
new SkiaLabel() { ... }.SetGrid(1, 0),
new SkiaLabel() { ... }.WithColumn(2).WithRow(0).WithRowSpan(2),
}
}
.WithColumnDefinitions("32,*,40")
.WithRowDefinitions("Auto,Auto")

SetGrid(column, row) / SetGrid(column, row, columnSpan, rowSpan) — column comes first, opposite of the usual "row, column" convention (the XML doc comment on the 4-arg overload in FluentExtensions.Maui.cs / FluentExtensions.NonMaui.cs lists them backwards — trust the signature). WithColumnDefinitions / WithRowDefinitions return SkiaLayout, so put them last in the chain (after .Assign(...)). Individual: .WithRow(n), .WithColumn(n), .WithRowSpan(n), .WithColumnSpan(n). Also DefaultColumnDefinition.

.Initialize vs .Adapt

.Adapt(me => ...) runs setup on the control itself mid-chain. Do NOT access OTHER .Assign'd references from Adapt — they may not exist yet. Post-build wiring that touches assigned refs goes in .Initialize(me => ...) on the OUTERMOST control — it runs after the whole chain is constructed.

.Initialize fires once, when the control gets its parent (added to Children/Content or to the canvas), independent of IsVisible, measuring or virtualization (since 1.10.6.16; before it fired at first measure, so an IsVisible=false control never ran it). Never attached → runs at first measure. Superview may still be null inside; tree-ready work goes in LayoutIsReady / Initialized. Trap: a child gets its parent INSIDE the parent's object initializer, before the parent's own .Assign(out ...) runs, so an .Initialize on a child that reads a sibling/parent field sees null. The lazy-target observers (ObserveProperty(() => x, ...), ObserveProperties(() => x, ...), Observe(() => x, ...)) therefore resolve their target at the control's FIRST MEASURE, not on attach (attach-time resolution left every SkiaSlider style thumb unsized, 2026-09-23).


Event / Gesture Wiring — MANDATORY

Always use fluent extension methods — never += events or commands wired outside the initializer.

TaskFluent method
Tap handler.OnTapped(me => { ... })
Tap with args.OnTapped((me, args) => { ... })
Long press.OnLongPressing(me => { ... })
Mouse hover.OnHovered((me, on) => { ... }) — turns ReceivesHover on; on = mouse over / left. A card and the button inside it are hovered together. Not tracked while content scrolls or slides (checked when it stops); touch never hovers
Context menu (right click / long press / Menu key on the web heads).OnContextMenu((me, e) => true) — true = handled (browser menu suppressed), false = browser menu shows
Editor text changed (SkiaEditor).OnTextChanged(text => { ... })
Editor focus (SkiaEditor).OnFocusChanged((me, focused) => { ... })
Label text + sender.OnTextChanged((lbl, text) => { ... })
Arbitrary setup.Adapt(me => { me.X = ...; })
Post-build wiring (touching Assign'd refs).Initialize(me => { ... })
Key pressed.OnKeyDown((me, key) => { ... })
Key released.OnKeyUp((me, key) => { ... })
Paint hook (before own background).WhenPaint((me, ctx) => { ... })
Overlay above content + children.WhenPainted((ctx, control) => { ...; return false; }) — true = keep repainting
Self-observe any property.ObserveSelf((me, propName) => { ... })
Templated layout applied a collection change.WhenItemsSourceChangesApplied(me => { ... }) — after every ItemsSource change (adds, removes, moves, LoadMore pages), once per frame, after the frame that shows it. Under MeasureVisible appended rows are measured later: read sizes in MeasurementApplied
Raw gesture interception (SkiaLayout-derived).WithGestures((me, args, apply) => { ... }) — return me = consumed, null = pass; never consume Up unless required

Keyboard: .OnKeyDown / .OnKeyUp both take (control, InputKey key) — InputKey.ArrowLeft/ArrowRight/ArrowUp/ArrowDown, Space, Enter, KeyD… Attach them to the ROOT control of the tree, not to the focused child. Verified on the Fiddle WASM build 2026-08-30.

OnKeyDown repeats at the OS key-repeat rate — with a delay before the first repeat and gaps between them — so driving a value directly from it produces stepped, laggy movement. For anything held (a game paddle, a camera pan), keep a direction flag instead and move per frame:

csharp
var keyDir = 0;
// in the per-frame animator: FPS-independent, no repeat delay
if (keyDir != 0) targetX = Math.Clamp(targetX + keyDir * speed * dt, min, max);
root.OnKeyDown((me, key) =>
{
if (key == InputKey.ArrowLeft) keyDir = -1;
else if (key == InputKey.ArrowRight) keyDir = 1;
})
.OnKeyUp((me, key) =>
{
if (key == InputKey.ArrowLeft && keyDir < 0) keyDir = 0;
else if (key == InputKey.ArrowRight && keyDir > 0) keyDir = 0;
});

On Blazor the key event is not preventDefaulted, so Space also scrolls the hosting page while the canvas has focus.

`.WhenPaint` draws BEFORE the control paints its own background (verified 2026-08-30, DrawnUI Fiddle WASM). A control that has BackgroundColor set will cover everything the hook drew — silently, no error, blank result. Put the background on a parent (e.g. a SkiaShape frame) and attach .WhenPaint to a transparent child filling it:

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Extension points

  • hookPre/post lifecycle hooks

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/drawnui/drawnui.net/drawnui-fluent/badge)](https://verified-skill.com/skills/drawnui/drawnui.net/drawnui-fluent)

Something wrong with this skill or its page? Tell us.