Namespace: Beep.Skia.Controls — Inherits: MaterialControl → SkiaComponent

Overview

The ComponentPalette control is a drag-and-drop toolbox that displays all registered SkiaComponent types as a categorized, searchable grid of icon+label tiles. It reads its inventory from the SkiaComponentRegistry — the centralized registry that maps component type metadata to factory delegates — enabling dynamic component discovery without hard-coded type lists.

Users can browse components by category, filter by keyword, select a display mode (icon grid or compact list), and drag items onto a canvas to instantiate new components. The palette renders entirely with SkiaSharp and follows Material Design 3.0 styling with elevation, state layers, and smooth drag-feedback animations.

Registry-Driven

Categories and items are populated automatically from SkiaComponentRegistry. No manual configuration required.

Search & Filter

A built-in search field filters components in real-time by name, description, or category tags.

Drag Ghost

Dragging a palette item creates a semi-transparent "ghost" preview that follows the pointer.

Custom Extensions

Register custom components and categories at runtime via the SkiaComponentRegistry API.

Properties

PropertyTypeDescription
Categories ObservableCollection<ComponentCategory> The grouped categories and their component items, built by LoadComponentsFromRegistry(). Each ComponentCategory exposes Name, Icon and its own Components collection.
SelectedCategory ComponentCategory The currently selected category, if any.
ItemHeight float Height of a component item row.
CategoryHeaderHeight float Height of a category header row.
DraggedComponent ComponentItem Read-only. The item currently being dragged, or null.
IsDragging bool Read-only. True while a component drag operation is in progress.
EventSignatureDescription
ComponentDragStarted EventHandler<ComponentDragEventArgs> Raised when the user begins dragging a component from the palette. The event carries the ComponentItem and pointer position.
ComponentDragging EventHandler<ComponentDragEventArgs> Raised continuously while a component is dragged, with the current pointer position.
ComponentDragEnded EventHandler<ComponentDragEventArgs> Raised when the user releases the drag. The event carries the ComponentItem, the final pointer position and the ComponentDragState.
ComponentSelected EventHandler<ComponentItem> Raised when a component is selected in the palette.

Component Discovery

The ComponentPalette discovers components exclusively through the SkiaComponentRegistry. This registry holds metadata for every component type available in the system, organized by category. The palette reads this data at initialization and builds its internal category/item structure.

Registry Entries

Each registry entry is an AssemblyClassDefinition (the shared BeepDM metadata type). The palette and the registry use these members:

MemberTypeDescription
className string The component class name; the palette converts it to a display label (for example ProcessNode becomes "Process Node").
dllname string Assembly that contains the component; used together with className as the registry's duplicate key.
GuidID string Stable identifier used by the registry lookups (GetComponentByGuid).
type Type Resolved CLR type used for instantiation and namespace grouping.
Methods List<MethodsClass> Optional command methods surfaced as child items in the palette and exposed through GetComponentsWithMethods().

How the Palette Reads the Registry

The palette builds its UI by calling LoadComponentsFromRegistry(), which reads SkiaComponentRegistry.GetComponentsOrdered() and groups the results into Categories — an ObservableCollection<ComponentCategory>. Each category exposes a Name, an Icon and an ObservableCollection<ComponentItem>; each ComponentItem wraps an AssemblyClassDefinition and derives its display name from the class name.

Call LoadComponentsFromRegistry() again after registering components at runtime to rebuild the category structure.

Drag-Drop Workflow

The drag-and-drop workflow is a multi-step interaction handled cooperatively by the palette and the target canvas:

  1. Pointer Down on Tile: The palette identifies which ComponentItem is under the pointer via hit testing.
  2. Drag Threshold: Once the pointer moves beyond the drag threshold, the palette raises ComponentDragStarted; a drag ghost follows the pointer while ComponentDragging fires continuously.
  3. Dragging Over Canvas: The canvas renders a drop indicator based on the pointer position carried in ComponentDragEventArgs.MousePosition.
  4. Pointer Up: ComponentDragEnded fires with the ComponentItem and the final pointer position. The host creates the instance through SkiaComponentRegistry.CreateComponentInstance(), positions it and adds it to the drawing manager.
  5. Cancel: Pressing Escape during a drag cancels the operation; the ghost is removed and ComponentDragEnded reports the cancelled state.

Canvas-Side Drop Handler

palette.ComponentDragEnded += (s, args) =>
{
    if (args.State != ComponentDragState.Ended) return;

    var component = SkiaComponentRegistry.CreateComponentInstance(args.Component.ClassDefinition);
    if (component == null) return;

    component.X = args.MousePosition.X;
    component.Y = args.MousePosition.Y;

    drawingManager.AddComponent(component);
    drawingManager.RequestRedraw();
};

Custom Category Registration

The supported way to add custom components is the extension SDK: an ISkiaExtension declares its component types, and the host registers them with an optional palette category and display name. See the Extension SDK guide for packaging and discovery.

Registering Custom Components

public sealed class WidgetsExtension : ISkiaExtension
{
    public string Id => "com.acme.widgets";
    public string Name => "Acme Widgets";
    public string Version => "1.0.0";
    public string Description => "Gauge and status indicator nodes";

    public IEnumerable<Type> GetComponentTypes() => new[]
    {
        typeof(CustomGaugeNode),
        typeof(StatusIndicator)
    };

    public void Initialize(ISkiaExtensionContext context)
    {
        context.RegisterComponent(typeof(CustomGaugeNode), "Custom Widgets", "Gauge");
        context.RegisterComponent(typeof(StatusIndicator), "Custom Widgets", "Status Indicator");
    }
}

Extending a Flowchart Node

public class PipelineStageNode : ProcessNode
{
    public PipelineStageNode()
    {
        Label = "Pipeline Stage";
        Width = 140;
        Height = 60;
    }

    protected override void DrawFlowchartContent(SKCanvas canvas, DrawingContext context)
    {
        // Draw the base node, then add family-specific decoration
        base.DrawFlowchartContent(canvas, context);
    }
}

Code Example

Basic ComponentPalette Setup

var palette = new ComponentPalette
{
    X = 10,
    Y = 50,
    Width = 320,
    Height = 600,
    ItemHeight = 28f,
    CategoryHeaderHeight = 32f
};

palette.LoadComponentsFromRegistry();

palette.ComponentSelected += (s, item) =>
    Console.WriteLine($"Selected: {item.DisplayName}");

palette.ComponentDragStarted += (s, args) =>
    Console.WriteLine($"Dragging: {args.Component.DisplayName}");

palette.ComponentDragEnded += (s, args) =>
{
    if (args.State != ComponentDragState.Ended)
    {
        Console.WriteLine("Drag cancelled");
        return;
    }

    var component = SkiaComponentRegistry.CreateComponentInstance(args.Component.ClassDefinition);
    if (component == null) return;

    component.X = args.MousePosition.X;
    component.Y = args.MousePosition.Y;

    drawingManager.AddComponent(component);
    drawingManager.RequestRedraw();
};

drawingManager.AddComponent(palette);
drawingManager.RequestRedraw();

Working with Categories Programmatically

// Iterate the grouped categories and their items
foreach (var category in palette.Categories)
{
    Console.WriteLine($"{category.Name} ({category.Components.Count})");

    foreach (var item in category.Components)
        Console.WriteLine($"  {item.DisplayName}");
}

// Select a category programmatically
palette.SelectedCategory = palette.Categories.FirstOrDefault();

// Track the active drag
if (palette.IsDragging)
    Console.WriteLine(palette.DraggedComponent?.DisplayName);

Refreshing After Runtime Registration

// Register through an extension, then rebuild the palette
palette.LoadComponentsFromRegistry();
drawingManager.RequestRedraw();

Note

The ComponentPalette is a read-only view of the registry: it never modifies entries. Register components through the extension SDK (or SkiaComponentRegistry.AddComponent) and call LoadComponentsFromRegistry() again to refresh the palette.

Tip

For large component libraries, keep the registry tidy by grouping related nodes into one extension package. The palette derives its categories from the registry, so a clean package structure produces a clean palette.