Layout providers
A layout provider describes what a device or client surface is - its regions, their geometry and what they can render - never what is on it. Macro Deck uses a registered layout to constrain a profile to real hardware, so hardware support lives entirely in your plugin with no device-specific code in Macro Deck.
Quick start
Section titled “Quick start”using MacroDeck.Sdk;using MacroDeck.Sdk.Devices;using MacroDeck.Sdk.Layouts;
public sealed class MacroPadIntegration : IPluginIntegration, ILayoutProvider, IDeviceProvider{ private string? _layoutId;
public string ProviderName => "Macro Pad";
public async Task InitializeAsync(ILayoutProviderContext context, CancellationToken cancellationToken = default) { var registration = await context.RegisterLayoutAsync( new LayoutDescriptor( "pad-3x2", "Macro Pad", Regions: [ new LayoutRegion { Id = "keys", Kind = LayoutRegionKinds.Grid, Grid = new LayoutGrid { Rows = 2, Columns = 3, KeySize = new LayoutKeySize(72, 72) } } ], Capabilities: new LayoutCapabilities { Visuals = new LayoutVisualCapabilities { StaticIcons = true, BackgroundColors = true, TextLabels = true } }), cancellationToken);
_layoutId = registration.LayoutId; }
public async Task InitializeAsync(IDeviceProviderContext context, CancellationToken cancellationToken = default) { await context.RegisterDeviceAsync( new DeviceDescriptor("SERIAL-1", "Macro Pad", LayoutReference: _layoutId), cancellationToken); }
public Task ShutdownAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
// IPluginIntegration members omitted.}A profile that is the startup profile of this pad is now locked to 2x3 in the editor, and its spacing and corner-radius controls are disabled because the layout does not claim them.
Things to know:
- Register the layout, then use the id it returns.
LayoutRegistration.LayoutIdis the qualifiedyour.plugin.id::pad-3x2; that is whatDeviceDescriptor.LayoutReferencemust carry. - Layouts start before devices. In a plugin the host initializes
ILayoutProviderbeforeIDeviceProvider, both after the integration’s ownInitializeAsync, so_layoutIdis set in time. - There is no
ILayoutProvider.ShutdownAsync. Release what you acquired in the integration’s ownShutdownAsync; the host withdraws your layouts when the integration stops. - Every visual flag defaults to
false. Declare what the surface can actually render.
Registering a layout
Section titled “Registering a layout”ILayoutProviderContext member |
What it does |
|---|---|
RegisterLayoutAsync |
Registers a layout, or replaces the one under the same provider-local id. Devices referencing it pick up the new descriptor without re-registering. Returns LayoutRegistration(LayoutId, ProviderId). |
UnregisterLayoutAsync |
Withdraws a layout by provider-local id. Unknown ids are ignored. |
RegisterLayoutAsync throws ArgumentException when the id or name is empty, or a region id is empty or
repeated within the layout. The context is safe to keep while the provider runs.
Implement GetLayouts() to return what you currently offer; the host reads it to recover its view after a
reconnect. The default returns an empty list. ProviderName is optional and falls back to the
integration’s name - for a plugin, the manifest.json name.
Id grammar
Section titled “Id grammar”you register LayoutDescriptor.Id "pad-3x2"host returns LayoutRegistration.LayoutId "com.example.macropad::pad-3x2"device carries LayoutReference "com.example.macropad::pad-3x2"LayoutDescriptor.Id is provider-local, stable across restarts and unique within your provider - a model
name, not a per-device serial. Never build the qualified string yourself; read it back from the
registration. The prefix is always the authenticated provider’s own id, so no plugin can register a layout
into another owner’s namespace.
Declaring regions
Section titled “Declaring regions”+----+----+----+----+| k | k | k | k | "keys" grid 2x4+----+----+----+----+| k | k | k | k |+----+----+----+----+| touch strip | "strip" touch-strip+-------------------+ (o) (o) (o) (o) "dials" encoder x4Regions:[ new LayoutRegion { Id = "keys", Kind = LayoutRegionKinds.Grid, Grid = new LayoutGrid { Rows = 2, Columns = 4, KeySize = new LayoutKeySize(120, 120) } }, new LayoutRegion { Id = "strip", Kind = LayoutRegionKinds.TouchStrip, Count = 1 }, new LayoutRegion { Id = "dials", Kind = LayoutRegionKinds.Encoder, Count = 4, Name = "Dials" }]Kind is one of LayoutRegionKinds - grid, button, encoder, touch-strip, pedal - but it is an
open string: an unknown kind round-trips intact with its Id, Name, Count and Extra, so an older
host carries it without dropping the rest of the layout.
Only grid carries geometry, in LayoutRegion.Grid. Every other kind is a Count.
LayoutGrid member |
Meaning |
|---|---|
Rows, Columns |
The size right now. On a fixed grid, Macro Deck holds a profile edited for this layout to exactly these. |
IsConfigurable |
The user may pick another size. false (default) ignores the bounds below. |
MinRows, MaxRows, MinColumns, MaxColumns |
Per-axis bounds for a configurable grid. Default 1. |
SupportsRuntimeResize |
A resize applies live. When false, you may apply it when the device next connects. |
KeySize |
Pixel size of one key, where the hardware has a fixed one. |
RowsLocked, ColumnsLocked |
Derived: fixed, or configurable with min >= max on that axis. |
LayoutDescriptor.PrimaryGrid is the one grid Macro Deck renders a deck onto, computed for you. It is null
when the layout has no grid region (a pedal board) or more than one - Macro Deck does not guess.
Declaring what the surface renders
Section titled “Declaring what the surface renders”Capabilities: new LayoutCapabilities{ Visuals = new LayoutVisualCapabilities { StaticIcons = true, TextLabels = true, BackgroundColors = true, MaxUpdatesPerSecond = 10 }}LayoutVisualCapabilities |
Meaning |
|---|---|
StaticIcons, AnimatedIcons, Borders, BackgroundColors, TextLabels, Transparency |
What the surface can draw. |
WidgetSpacing, CornerRadius |
Whether the folder’s spacing and corner radius mean anything here. false disables those editor controls with a note that they have no effect on this device. |
CustomFolderViews |
Whether the surface can show a folder view at all. false means the choice is not offered for a profile its device claims. Set it on a software client or full display. |
MaxUpdatesPerSecond |
Advisory update rate. |
LayoutVisualCapabilities.Full sets every flag. A Visuals block that sets nothing reads as “renders
nothing”. No Visuals block at all keeps the spacing controls and the folder-view option - saying
nothing is not saying no. A region can override with its own LayoutRegion.Visuals; null inherits the
layout’s.
Associating a layout with a device provider
Section titled “Associating a layout with a device provider”new DeviceDescriptor(serial, "Macro Pad", LayoutReference: "com.other.plugin::pad-3x2")A device points at a layout through DeviceDescriptor.LayoutReference - see
device providers. The layout’s provider need not be the
device’s: a device may reference another plugin’s layout. A layout is descriptive metadata, and referencing
one grants nothing over its owner. An unresolvable reference is never an error - the device registers, and
its profile stays unconstrained until that layout appears.
What happens to profiles
Section titled “What happens to profiles”| Startup-profile devices resolve to | Profile |
|---|---|
| Exactly one distinct rows x columns pair (one device, or several sharing a layout) | Locked to that grid. |
| Different pairs | Editable, with a warning naming the disagreeing devices. |
A layout with no PrimaryGrid |
Not constrained. |
Macro Deck constrains a profile; it never rewrites one. No reflow, pagination, resizing or dropped widgets: a profile bigger than the device simply shows partially, as a device session does.
The host persists the last resolved layout with the device, not just the reference. UnregisterLayoutAsync
and a stopped provider behave the same: the reference is kept, the grid stays constrained, and it refreshes
when the layout is registered again.
In a plugin
Section titled “In a plugin”Declare the layout-provider capability and host:layouts in manifest.json.
MacroDeck.Plugin.Hosting starts the provider once the plugin is connected and its integration has
initialized, and stops it on shutdown. The provider is always the authenticated plugin, which keeps the id
grammar true across the wire. See Capability parity.
Testing
Section titled “Testing”using MacroDeck.Plugin.Testing.Fakes;using MacroDeck.Sdk.Layouts;
[Test]public async Task The_pad_declares_one_fixed_2x3_grid(){ var context = new FakeLayoutProviderContext();
await new MacroPadIntegration().InitializeAsync(context);
var grid = context.Layouts["pad-3x2"].PrimaryGrid!.Grid!; Assert.Multiple(() => { Assert.That((grid.Rows, grid.Columns), Is.EqualTo((2, 3))); Assert.That(grid.RowsLocked && grid.ColumnsLocked, Is.True); });}FakeLayoutProviderContext keeps the host’s rules: re-registering an id replaces it, unregistering an
unknown id is a no-op, and an empty id or name or an empty or repeated region id throws. Assert on
Layouts (keyed by local id) and Calls. The fake returns the local id as LayoutRegistration.LayoutId,
not a qualified one, so do not assert on the :: form.
Against a real plugin process, the harness’s LayoutProvider client invokes describe (DescribeAsync)
and layouts (GetLayoutsAsync). See Testing.
Over the plugin protocol
Section titled “Over the plugin protocol”The host-to-provider direction of the layout-provider capability only describes the provider and re-reads
its catalogue after a reconnect:
| Operation | Purpose |
|---|---|
describe |
The provider’s declared name. |
layouts |
The provider’s current layout catalogue, re-read after a reconnect. |
Registering and withdrawing travel the other way as the layouts host API:
| Operation | Purpose |
|---|---|
register |
Registers a layout, or replaces one already registered under the same provider-local id. |
unregister |
Withdraws a layout. Devices still referencing it keep their last-resolved geometry rather than losing their constraint. |
See also
Section titled “See also”- Device providers - registering the devices that reference a layout.
- Folder views - what
CustomFolderViewsgates. - Sizing - how widgets size within a grid.
- Capability parity - plugin-specific behaviour.