Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

87 Commits

Repository files navigation

Clay.NET

A pure .NET reimplementation of the Clay UI layout library. Clay.NET provides a declarative, immediate-mode UI framework with a flexbox-like layout system, zero per-frame allocations after warmup, and no native dependencies.

Features

  • Immediate-mode API - Describe your UI declaratively each frame using using scopes
  • Flexbox-like layout - Sizing (Fixed, Fit, Grow, Percent), padding, alignment, child gaps, and directional flow
  • Backend-agnostic rendering - Generates render commands for any graphics backend (Raylib, MonoGame, Unity, etc.)
  • Pointer input handling - Hover detection and pointer state tracking with O(1) element lookup
  • Scroll containers - Native support for scrollable content with automatic clipping
  • Floating/overlay elements - Z-indexed absolute positioning for tooltips, dropdowns, and modals
  • Zero dependencies - Pure managed .NET code, no interop required
  • High performance - Aggressive inlining, span-based access, pre-allocated buffers, zero per-frame GC pressure after warmup

Project Structure

Clay.NET/
├── Clay.slnx
└── src/
├── Clay/ # Core layout library
├── Clay.Example/ # Raylib-based example application
└── Clay.Test/ # xUnit test suite

Quick Start

usingClay;usingSystem.Numerics;// Initialize onceClay.Clay.Initialize(newDimensions(1920,1080),newSimpleTextMeasurer());// Each frameClay.Clay.SetPointerState(newVector2(mouseX,mouseY),mousePressed);Clay.Clay.BeginLayout();using(Clay.Clay.Element(newElementDeclaration{Id=Clay.Clay.Id("Root"),Layout=newLayoutConfig{Sizing=Sizing.Fill(),Direction=LayoutDirection.TopToBottom,Padding=Padding.All(16),ChildGap=8},BackgroundColor=Color.Rgba(30,30,30)})){Clay.Clay.Text("Hello, Clay!",newTextConfig{FontSize=24,TextColor=Color.White});using(Clay.Clay.Element(newElementDeclaration{Id=Clay.Clay.Id("Button"),Layout=newLayoutConfig{Padding=Padding.Symmetric(16,8)},BackgroundColor=Clay.Clay.PointerOver(Clay.Clay.Id("Button"))?Color.Rgba(80,80,80):Color.Rgba(60,60,60),CornerRadius=CornerRadius.All(4)})){Clay.Clay.Text("Click Me",newTextConfig{FontSize=16,TextColor=Color.White});}}ReadOnlySpan<RenderCommand>commands=Clay.Clay.EndLayout();// Pass commands to your renderer

ClayUI - Immediate Mode Widgets

Clay.NET includes ClayUI, a higher-level immediate-mode widget layer built on top of the core layout engine. It provides ready-to-use UI controls with built-in state management, hover/click handling, and theming.

Setup

Call ClayUI.BeginFrame() each frame before using widgets, between Clay.BeginLayout() and Clay.EndLayout():

Clay.Clay.BeginLayout();ClayUI.BeginFrame(mouseDown,newVector2(mouseX,mouseY),scrollDelta);// ... use ClayUI widgets here ...varcommands=Clay.Clay.EndLayout();

Buttons and Labels

ClayUI.Heading("Settings");ClayUI.Label("Configure your preferences below.");if(ClayUI.Button("Save")){SaveSettings();}if(ClayUI.Button("Cancel")){RevertChanges();}

Image and ImageButton

// Display an image (pass your texture/image object through to the renderer)ClayUI.Image(myTexture,width:128,height:128);// Rounded imageClayUI.Image(avatar,64,64,style:newImageStyle{CornerRadius=CornerRadius.All(32)// circular});// Clickable image button (like ImGui::ImageButton)if(ClayUI.ImageButton(icon,32,32)){DoAction();}

Checkbox and Toggle

booldarkMode=true;boolnotifications=false;ClayUI.Checkbox("Enable dark mode",refdarkMode);ClayUI.Toggle("Notifications",refnotifications);

Slider

floatvolume=0.75f;floatbrightness=1.0f;ClayUI.Slider("Volume",refvolume,0f,1f);ClayUI.Slider("Brightness",refbrightness,0f,2f);

Radio Group

intquality=1;// 0=Low, 1=Medium, 2=HighClayUI.RadioGroup("Quality",refquality,new[]{"Low","Medium","High"});

The generic overload RadioGroup<T> is also available for enum or object-based selection.

Progress Bar

ClayUI.ProgressBar(downloadProgress,0f,100f);

Panels

Panels are titled, styled containers for grouping related widgets:

ClayUI.BeginPanel("Player Info",scroll:true,maxHeight:300);ClayUI.Label($"Name: {player.Name}");ClayUI.Label($"Health: {player.Health}");ClayUI.Label($"Score: {player.Score}");ClayUI.Separator();ClayUI.Slider("Speed",refplayer.Speed,0f,10f);ClayUI.EndPanel();

Horizontal and Vertical Layouts

ClayUI.BeginHorizontal(gap:8);ClayUI.Button("Left");ClayUI.Button("Center");ClayUI.Button("Right");ClayUI.EndHorizontal();ClayUI.BeginVertical(gap:4);ClayUI.Label("Line 1");ClayUI.Label("Line 2");ClayUI.Label("Line 3");ClayUI.EndVertical();

Windows

Draggable, resizable, collapsible windows with automatic focus management:

boolshowInventory=true;if(ClayUI.BeginWindow("Inventory",refshowInventory,defaultPosition:newVector2(400,150),defaultSize:newVector2(300,200))){for(inti=0;i<items.Count;i++){ClayUI.BeginHorizontal();ClayUI.Label(items[i].Name);if(ClayUI.Button("Use"))UseItem(items[i]);ClayUI.EndHorizontal();}}ClayUI.EndWindow();

Popups and Context Menus

vartriggerId=Clay.Clay.Id("RightClickArea");if(ClayUI.BeginContextMenu("MyMenu",triggerId)){if(ClayUI.MenuItem("Cut"))DoCut();if(ClayUI.MenuItem("Copy"))DoCopy();if(ClayUI.MenuItem("Paste"))DoPaste();ClayUI.MenuSeparator();if(ClayUI.MenuItem("Delete"))DoDelete();ClayUI.EndContextMenu();}

Tree Nodes

if(ClayUI.BeginTreeNode("Root")){if(ClayUI.BeginTreeNode("Child A")){ClayUI.Label("Leaf 1");ClayUI.Label("Leaf 2");ClayUI.EndTreeNode();}if(ClayUI.BeginTreeNode("Child B")){ClayUI.Label("Leaf 3");ClayUI.EndTreeNode();}ClayUI.EndTreeNode();}

Theming

// Built-in themesClayUI.Style=ClayUIStyle.Dark;ClayUI.Style=ClayUIStyle.Light;// Or customize individual widget stylesClayUI.Button("Danger",style:newButtonStyle{BackgroundColor=Color.Rgba(180,40,40),HoverColor=Color.Rgba(200,60,60),PressedColor=Color.Rgba(140,30,30),CornerRadius=CornerRadius.All(8)});

Debug Window

// Toggle with a key pressif(keyPressed==Key.F12)ClayUI.ToggleDebugWindow();// Or show directlyClayUI.ShowDebugWindow();

Low-Level Layout Examples

Row Layout with Gap

using(Clay.Clay.Element(newElementDeclaration{Id=Clay.Clay.Id("Toolbar"),Layout=newLayoutConfig{Sizing=Sizing.FillWidth(),Direction=LayoutDirection.LeftToRight,ChildGap=8,Padding=Padding.All(12)},BackgroundColor=Color.Rgba(40,40,40)})){// Children are placed side by side with 8px gapButton("Save");Button("Load");Button("Settings");}

Sidebar + Content Split

using(Clay.Clay.Element(newElementDeclaration{Id=Clay.Clay.Id("App"),Layout=LayoutConfig.FillRow()})){// Fixed-width sidebarusing(Clay.Clay.Element(newElementDeclaration{Id=Clay.Clay.Id("Sidebar"),Layout=newLayoutConfig{Sizing=newSizing(SizingAxis.Fixed(250),SizingAxis.Grow()),Direction=LayoutDirection.TopToBottom,Padding=Padding.All(16),ChildGap=4},BackgroundColor=Color.Rgba(25,25,25)})){Clay.Clay.Text("Navigation",newTextConfig{FontSize=18,TextColor=Color.White});}// Main content grows to fill remaining spaceusing(Clay.Clay.Element(newElementDeclaration{Id=Clay.Clay.Id("Content"),Layout=newLayoutConfig{Sizing=Sizing.Fill(),Padding=Padding.All(24)}})){Clay.Clay.Text("Main content area",newTextConfig{FontSize=16,TextColor=Color.White});}}

Scroll Container

using(Clay.Clay.Element(newElementDeclaration{Id=Clay.Clay.Id("ScrollArea"),Layout=newLayoutConfig{Sizing=Sizing.FixedSize(300,400),Direction=LayoutDirection.TopToBottom,ChildGap=4},Scroll=ScrollConfig.VerticalScroll,BackgroundColor=Color.Rgba(20,20,20)})){for(inti=0;i<50;i++){using(Clay.Clay.Element(newElementDeclaration{Id=Clay.Clay.Id("Item",(uint)i),Layout=newLayoutConfig{Sizing=newSizing(SizingAxis.Grow(),SizingAxis.Fixed(40)),Padding=Padding.Symmetric(12,8)},BackgroundColor=Color.Rgba(50,50,50)})){Clay.Clay.Text($"Item {i}",newTextConfig{FontSize=14,TextColor=Color.White});}}}

Floating Tooltip

varbuttonId=Clay.Clay.Id("HoverButton");using(Clay.Clay.Element(newElementDeclaration{Id=buttonId,Layout=newLayoutConfig{Padding=Padding.Symmetric(16,8)},BackgroundColor=Color.Rgba(60,120,200),CornerRadius=CornerRadius.All(4)})){Clay.Clay.Text("Hover me",newTextConfig{FontSize=14,TextColor=Color.White});if(Clay.Clay.PointerOver(buttonId)){using(Clay.Clay.Element(newElementDeclaration{Id=Clay.Clay.Id("Tooltip"),Layout=newLayoutConfig{Padding=Padding.All(8)},BackgroundColor=Color.Rgba(0,0,0,220),CornerRadius=CornerRadius.All(4),Floating=newFloatingConfig{Offset=newVector2(0,4),AttachTo=FloatingAttachTo.Parent,AttachPoints=newFloatingAttachPoints{Element=FloatingAttachPoint.LeftTop,Parent=FloatingAttachPoint.LeftBottom},ZIndex=10}})){Clay.Clay.Text("This is a tooltip",newTextConfig{FontSize=12,TextColor=Color.White});}}}

Centering Content

using(Clay.Clay.Element(newElementDeclaration{Id=Clay.Clay.Id("CenterWrapper"),Layout=newLayoutConfig{Sizing=Sizing.Fill(),ChildAlignment=ChildAlignment.Center}})){using(Clay.Clay.Element(newElementDeclaration{Id=Clay.Clay.Id("Card"),Layout=newLayoutConfig{Sizing=Sizing.FixedSize(400,300),Direction=LayoutDirection.TopToBottom,Padding=Padding.All(24),ChildGap=12,ChildAlignment=ChildAlignment.Center},BackgroundColor=Color.Rgba(45,45,45),CornerRadius=CornerRadius.All(8),Border=BorderConfig.Uniform(1,Color.Rgba(80,80,80))})){Clay.Clay.Text("Centered Card",newTextConfig{FontSize=20,TextColor=Color.White});Clay.Clay.Text("This card is centered in the viewport.",newTextConfig{FontSize=14,TextColor=Color.Rgba(180,180,180)});}}

Unique IDs with

Like ImGui, Clay.NET supports ## in label strings to create unique IDs while sharing the same display text. The entire string (including the ## part) is hashed to produce the element ID, but only the portion before## is displayed.

This is useful when you have multiple widgets with the same visible label but need them to be distinct elements:

// Two buttons both display "Delete" but have different IDsif(ClayUI.Button("Delete##item_1"))RemoveItem(1);if(ClayUI.Button("Delete##item_2"))RemoveItem(2);// Works with any widget that takes a labelClayUI.Checkbox("Enable##audio",refaudioEnabled);ClayUI.Checkbox("Enable##video",refvideoEnabled);// Low-level ID creationvarid=Clay.Clay.Id("Save##primary");// Hashes "Save##primary", displays "Save"

Use ElementId.GetDisplayLabel("Save##id") to extract the visible portion ("Save") without allocations.

Sizing Types

TypeBehavior
SizingAxis.Fixed(size)Exact pixel size
SizingAxis.Fit(min, max)Shrink to content, respecting bounds
SizingAxis.Grow(min, max)Expand to fill available space
SizingAxis.PercentOf(pct)Percentage of parent size (0.0 - 1.0)

Rendering

Implement IClayRenderer for your graphics backend:

publicclassMyRenderer:IClayRenderer{publicvoidRender(ReadOnlySpan<RenderCommand>commands){foreach(varcmdincommands){switch(cmd.CommandType){caseRenderCommandType.Rectangle:DrawRect(cmd.BoundingBox,cmd.Rectangle.BackgroundColor);break;caseRenderCommandType.Text:DrawText(cmd.Text.Text,cmd.BoundingBox,cmd.Text.TextColor);break;// ... handle other command types}}}}

Building

Requires .NET 9.0 SDK.

dotnet build Clay.slnx
dotnet test src/Clay.Test/Clay.Test.csproj

Credits

Based on the original Clay C library by Nic Barker.

This project was developed with the assistance of AI (Claude by Anthropic).

About

A port of the fantastic clay library

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages