Initial work in progress

This commit is contained in:
Bobby Lucero 2025-07-01 19:24:43 -04:00
commit a911d5310c
5 changed files with 866 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
MicroUI.cs/obj
MicroUI.cs/bin
.idea

16
MicroUI.cs.sln Normal file
View File

@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroUI.cs", "MicroUI.cs\MicroUI.cs.csproj", "{7668C69C-9286-4EB9-BA74-5F9CC2CF953C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7668C69C-9286-4EB9-BA74-5F9CC2CF953C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7668C69C-9286-4EB9-BA74-5F9CC2CF953C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7668C69C-9286-4EB9-BA74-5F9CC2CF953C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7668C69C-9286-4EB9-BA74-5F9CC2CF953C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

808
MicroUI.cs/MicroUI.cs Normal file
View File

@ -0,0 +1,808 @@
using System.Runtime.CompilerServices;
namespace MicroUI
{
using mu_Id = System.UInt32;
public static class Constants
{
public const string Version = "1.0.0";
public const int CommandListSize = 256 * 1024;
public const int RootListSize = 32;
public const int ContainerStackSize = 32;
public const int ClipStackSize = 32;
public const int IdStackSize = 32;
public const int LayoutStackSize = 16;
public const int ContainerPoolSize = 48;
public const int TreeNodePoolSize = 48;
public const int MaxWidths = 16;
public const int MaxFormatLength = 127;
}
public static class Format
{
public static string Real(float val) => val.ToString("G3");
public static string Slider(float val) => val.ToString("F2");
}
public static class MathUtil
{
public static int Min(int a, int b) => a < b ? a : b;
public static int Max(int a, int b) => a > b ? a : b;
public static int Clamp(int x, int a, int b) => Min(b, Max(a, x));
public static float Min(float a, float b) => a < b ? a : b;
public static float Max(float a, float b) => a > b ? a : b;
public static float Clamp(float x, float a, float b) => Min(b, Max(a, x));
}
public struct FixedStack<T>
{
public int Index;
public T[] Items;
public FixedStack(int capacity)
{
Items = new T[capacity];
Index = 0;
}
public void Push(T val)
{
if (Index >= Items.Length)
{
MuAssert.Expect(false);
return;
}
Items[Index] = val;
Index++;
}
public T Pop()
{
MuAssert.Expect(Index > 0);
Index--;
return Items[Index];
}
public T Peek()
{
MuAssert.Expect(Index > 0);
return Items[Index - 1];
}
public void Clear() => Index = 0;
}
public enum ClipMode
{
Part = 1,
All
}
public enum CommandType
{
Jump = 1,
Clip,
Rect,
Text,
Icon,
Max
}
public enum ColorType
{
Text,
Border,
WindowBg,
TitleBg,
TitleText,
PanelBg,
Button,
ButtonHover,
ButtonFocus,
Base,
BaseHover,
BaseFocus,
ScrollBase,
ScrollThumb,
Max
}
public enum IconType
{
Close = 1,
Check = 2,
Collapsed = 3,
Expanded = 4,
Max = 5
}
[Flags]
public enum ResultFlags
{
Active = 1 << 0, // 1
Submit = 1 << 1, // 2
Change = 1 << 2 // 4
}
[System.Flags]
public enum Options
{
AlignCenter = 1 << 0, // 1
AlignRight = 1 << 1, // 2
NoInteract = 1 << 2, // 4
NoFrame = 1 << 3, // 8
NoResize = 1 << 4, // 16
NoScroll = 1 << 5, // 32
NoClose = 1 << 6, // 64
NoTitle = 1 << 7, // 128
HoldFocus = 1 << 8, // 256
AutoSize = 1 << 9, // 512
Popup = 1 << 10, // 1024
Closed = 1 << 11, // 2048
Expanded = 1 << 12 // 4096
}
[System.Flags]
public enum MouseButton
{
Left = 1 << 0, // 1
Right = 1 << 1, // 2
Middle = 1 << 2 // 4
}
[System.Flags]
public enum KeyModifiers
{
Shift = 1 << 0, // 1
Ctrl = 1 << 1, // 2
Alt = 1 << 2, // 4
Backspace = 1 << 3, // 8
Return = 1 << 4 // 16
}
public struct MuVec2
{
public int X;
public int Y;
public MuVec2(int x, int y)
{
X = x;
Y = y;
}
}
public struct MuRect
{
public int X;
public int Y;
public int W;
public int H;
public MuRect(int x, int y, int w, int h)
{
X = x;
Y = y;
W = w;
H = h;
}
}
public struct MuColor
{
public byte R;
public byte G;
public byte B;
public byte A;
public MuColor(byte r, byte g, byte b, byte a)
{
R = r;
G = g;
B = b;
A = a;
}
}
public struct MuPoolItem
{
public mu_Id Id;
public int LastUpdate;
public MuPoolItem(uint id, int lastUpdate)
{
Id = id;
LastUpdate = lastUpdate;
}
}
public abstract class MuCommand
{
public abstract CommandType Type { get; }
}
public class MuJumpCommand : MuCommand
{
public int DestinationIndex { get; set; }
public override CommandType Type => CommandType.Jump;
}
public class MuClipCommand : MuCommand
{
public MuRect Rect { get; }
public MuClipCommand(MuRect rect)
{
Rect = rect;
}
public override CommandType Type => CommandType.Clip;
}
public class MuRectCommand : MuCommand
{
public MuRect Rect { get; }
public MuColor Color { get; }
public MuRectCommand(MuRect rect, MuColor color)
{
Rect = rect;
Color = color;
}
public override CommandType Type => CommandType.Rect;
}
public class MuTextCommand : MuCommand
{
public object? Font { get; } // TODO: Idk how this works yet
public MuVec2 Position { get; }
public MuColor Color { get; }
public string Text { get; }
public MuTextCommand(object? font, MuVec2 position, MuColor color, string text)
{
Font = font;
Position = position;
Color = color;
Text = text;
}
public override CommandType Type => CommandType.Text;
}
public class MuIconCommand : MuCommand
{
public MuRect Rect { get; }
public int IconId { get; }
public MuColor Color { get; }
public MuIconCommand(MuRect rect, int iconId, MuColor color)
{
Rect = rect;
IconId = iconId;
Color = color;
}
public override CommandType Type => CommandType.Icon;
}
public class MuCommandBuffer
{
public List<MuCommand> Commands { get; } = new();
public void Add(MuCommand command)
{
Commands.Add(command);
}
public void Clear()
{
Commands.Clear();
}
}
public struct MuLayout
{
public MuRect Body { get; set; }
public MuRect Next { get; set; }
public MuVec2 Position { get; set; }
public MuVec2 Size { get; set; }
public MuVec2 Max { get; set; }
public int[] Widths { get; }
public int Items { get; set; }
public int ItemIndex { get; set; }
public int NextRow { get; set; }
public int NextType { get; set; }
public int Indent { get; set; }
public MuLayout()
{
Widths = new int[Constants.MaxWidths];
}
}
public class MuContainer
{
public int HeadIndex { get; set; } = -1;
public int TailIndex { get; set; } = -1;
public MuRect Rect { get; set; }
public MuRect Body { get; set; }
public MuVec2 ContentSize { get; set; }
public MuVec2 Scroll { get; set; }
public int ZIndex { get; set; }
public bool Open { get; set; }
public MuContainer()
{
Open = false;
}
}
public class MuStyle
{
public object? Font { get; set; } // Replace 'object' with actual font type as needed
public MuVec2 Size { get; set; }
public int Padding { get; set; }
public int Spacing { get; set; }
public int Indent { get; set; }
public int TitleHeight { get; set; }
public int ScrollbarSize { get; set; }
public int ThumbSize { get; set; }
public MuColor[] Colors { get; set; }
public MuStyle()
{
Colors = new MuColor[(int)ColorType.Max];
}
}
public delegate int TextWidthDelegate(object font, string str, int len);
public delegate int TextHeightDelegate(object font);
public delegate void DrawFrameDelegate(MuContext ctx, MuRect rect, ColorType colorId);
public class MuContext
{
// Callbacks
public TextWidthDelegate? TextWidth;
public TextHeightDelegate? TextHeight;
public DrawFrameDelegate? DrawFrame;
// Styles
public MuStyle Style { get; set; } = new MuStyle();
// IDs and last widget info
public mu_Id Hover { get; set; } // assuming mu_Id is uint
public mu_Id Focus { get; set; }
public mu_Id LastId { get; set; }
public MuRect LastRect { get; set; }
public int LastZIndex { get; set; }
public int UpdatedFocus { get; set; }
public int Frame { get; set; }
// Containers
public MuContainer? HoverRoot { get; set; }
public MuContainer? NextHoverRoot { get; set; }
public MuContainer? ScrollTarget { get; set; }
// Number edit buffer
private char[] numberEditBuf = new char[Constants.MaxFormatLength]; // e.g. 127 or 128
public mu_Id NumberEdit { get; set; }
// Stacks
public FixedStack<MuCommand> CommandList { get; } = new FixedStack<MuCommand>(Constants.CommandListSize);
public FixedStack<MuContainer> RootList { get; } = new FixedStack<MuContainer>(Constants.RootListSize);
public FixedStack<MuContainer> ContainerStack { get; } = new FixedStack<MuContainer>(Constants.ContainerStackSize);
public FixedStack<MuRect> ClipStack { get; } = new FixedStack<MuRect>(Constants.ClipStackSize);
public FixedStack<uint> IdStack { get; } = new FixedStack<uint>(Constants.IdStackSize);
public FixedStack<MuLayout> LayoutStack { get; } = new FixedStack<MuLayout>(Constants.LayoutStackSize);
// Retained State Pools
public MuPoolItem[] ContainerPool { get; } = new MuPoolItem[Constants.ContainerPoolSize];
public MuContainer[] Containers { get; } = new MuContainer[Constants.ContainerPoolSize];
public MuPoolItem[] TreeNodePool { get; } = new MuPoolItem[Constants.TreeNodePoolSize];
// Input State
public MuVec2 MousePos { get; set; }
public MuVec2 LastMousePos { get; set; }
public MuVec2 MouseDelta { get; set; }
public MuVec2 ScrollDelta { get; set; }
public int MouseDown { get; set; }
public int MousePressed { get; set; }
public int KeyDown { get; set; }
public int KeyPressed { get; set; }
public char[] inputText = new char[32];
}
public static class MuAssert
{
public static void Expect(bool condition,
[CallerFilePath] string file = "",
[CallerLineNumber] int line = 0,
[CallerMemberName] string member = "")
{
if (!condition)
{
Console.Error.WriteLine($"Fatal error at {file}:{line} in {member}: assertion failed.");
Environment.FailFast("Expectation failed.");
}
}
}
public class MicroUI
{
public static readonly MuRect UnclippedRect = new MuRect(0,0,0x1000000,0x1000000);
public static readonly MuStyle DefaultStyle = new MuStyle
{
Font = null,
Size = new MuVec2(68, 10),
Padding = 5,
Spacing = 4,
Indent = 24,
TitleHeight = 24,
ScrollbarSize = 12,
ThumbSize = 8,
Colors = new MuColor[]
{
new MuColor(230, 230, 230, 255), // MU_COLOR_TEXT
new MuColor(25, 25, 25, 255), // MU_COLOR_BORDER
new MuColor(50, 50, 50, 255), // MU_COLOR_WINDOWBG
new MuColor(25, 25, 25, 255), // MU_COLOR_TITLEBG
new MuColor(240, 240, 240, 255), // MU_COLOR_TITLETEXT
new MuColor(0, 0, 0, 0), // MU_COLOR_PANELBG
new MuColor(75, 75, 75, 255), // MU_COLOR_BUTTON
new MuColor(95, 95, 95, 255), // MU_COLOR_BUTTONHOVER
new MuColor(115, 115, 115, 255), // MU_COLOR_BUTTONFOCUS
new MuColor(30, 30, 30, 255), // MU_COLOR_BASE
new MuColor(35, 35, 35, 255), // MU_COLOR_BASEHOVER
new MuColor(40, 40, 40, 255), // MU_COLOR_BASEFOCUS
new MuColor(43, 43, 43, 255), // MU_COLOR_SCROLLBASE
new MuColor(30, 30, 30, 255) // MU_COLOR_SCROLLTHUMB
}
};
public static MuRect ExpandRect(MuRect rect, int n)
{
return new MuRect(
rect.X - n,
rect.Y - n,
rect.W + 2 * n,
rect.H + 2 * n
);
}
public static MuRect IntersectRects(MuRect r1, MuRect r2)
{
int x1 = MathUtil.Max(r1.X, r2.X);
int y1 = MathUtil.Max(r1.Y, r2.Y);
int x2 = MathUtil.Min(r1.X + r1.W, r2.X + r2.W);
int y2 = MathUtil.Min(r1.Y + r1.H, r2.Y + r2.H);
if (x2 < x1) x2 = x1;
if (y2 < y1) y2 = y1;
return new MuRect(x1, y1, x2 - x1, y2 - y1);
}
public static bool RectOverlapsVec2(MuRect r, MuVec2 p)
{
return p.X >= r.X && p.X < r.X + r.W &&
p.Y >= r.Y && p.Y < r.Y + r.H;
}
public static void DrawFrame(MuContext ctx, MuRect rect, ColorType colorId)
{
// Draw filled rectangle with given color
DrawRect(ctx, rect, ctx.Style.Colors[(int)colorId]);
// Early return for certain color IDs (skip border)
if (colorId == ColorType.ScrollBase ||
colorId == ColorType.ScrollThumb ||
colorId == ColorType.TitleBg)
{
return;
}
// Draw border if alpha > 0
if (ctx.Style.Colors[(int)ColorType.Border].A > 0)
{
DrawBox(ctx, ExpandRect(rect, 1), ctx.Style.Colors[(int)ColorType.Border]);
}
}
public static void Init(MuContext ctx)
{
// Zeroing is not needed — C# classes are automatically zeroed/null-initialized.
// Assign the draw function delegate
ctx.DrawFrame = DrawFrame;
// Copy the default style (make sure it's cloned, not shared!)
ctx.Style = DefaultStyle;
}
public static void Begin(MuContext ctx)
{
MuAssert.Expect(ctx.TextWidth != null && ctx.TextHeight != null);
ctx.CommandList.Clear();
ctx.RootList.Clear();
ctx.ScrollTarget = null;
ctx.HoverRoot = ctx.NextHoverRoot;
ctx.NextHoverRoot = null;
ctx.MouseDelta = new MuVec2(ctx.MousePos.X - ctx.LastMousePos.X, ctx.MousePos.Y - ctx.LastMousePos.Y);
ctx.Frame++;
}
public static int CompareZIndex(MuContainer a, MuContainer b)
{
return a.ZIndex.CompareTo(b.ZIndex);
}
private static readonly IComparer<MuContainer> ZIndexComparerInstance = Comparer<MuContainer>.Create(CompareZIndex);
public static void End(MuContext ctx){
MuAssert.Expect(ctx.ContainerStack.Index == 0);
MuAssert.Expect(ctx.ClipStack.Index == 0);
MuAssert.Expect(ctx.IdStack.Index == 0);
MuAssert.Expect(ctx.LayoutStack.Index == 0);
if (ctx.ScrollTarget != null)
{
ctx.ScrollTarget.Scroll = new MuVec2(
ctx.ScrollTarget.Scroll.X + ctx.ScrollDelta.X,
ctx.ScrollTarget.Scroll.Y + ctx.ScrollDelta.Y
);
}
if (ctx.UpdatedFocus == 0)
{
ctx.Focus = 0;
}
ctx.UpdatedFocus = 0;
if (ctx.MousePressed != 0 && ctx.NextHoverRoot != null &&
ctx.NextHoverRoot.ZIndex < ctx.LastZIndex &&
ctx.NextHoverRoot.ZIndex >= 0)
{
BringToFront(ctx.NextHoverRoot);
}
ctx.KeyPressed = 0;
ctx.inputText = new char[32];
ctx.MousePressed = 0;
ctx.ScrollDelta = new MuVec2(0, 0);
ctx.LastMousePos = ctx.MousePos;
int n = ctx.RootList.Index;
Array.Sort(ctx.RootList.Items, 0, n, ZIndexComparerInstance);
for (int i = 0; i < n; i++)
{
var cnt = ctx.RootList.Items[i];
if (i == 0)
{
((MuJumpCommand)ctx.CommandList.Items[0]).DestinationIndex = cnt.HeadIndex + 1;
}
else
{
var prev = ctx.RootList.Items[i - 1];
// Set previous container's tail jump destination to current container's head + 1
((MuJumpCommand)ctx.CommandList.Items[prev.TailIndex]).DestinationIndex = cnt.HeadIndex + 1;
}
if (i == n - 1)
{
// Last container tail jump destination points to end of command list (beyond last command)
((MuJumpCommand)ctx.CommandList.Items[cnt.TailIndex]).DestinationIndex = cnt.HeadIndex + 1;
}
}
}
public static void SetFocus(MuContext ctx, mu_Id id)
{
ctx.Focus = id;
ctx.UpdatedFocus = 1;
}
public const uint HashInitial = 2166136261;
public static void Hash(ref uint hash, ReadOnlySpan<byte> data)
{
const uint fnvPrime = 16777619;
foreach (byte b in data)
{
hash = (hash ^ b) * fnvPrime;
}
}
public static uint GetId(MuContext ctx, ReadOnlySpan<byte> data)
{
uint seed = ctx.IdStack.Index > 0 ? ctx.IdStack.Items[ctx.IdStack.Index - 1] : HashInitial;
Hash(ref seed, data);
ctx.LastId = seed;
return seed;
}
public static void PushId(MuContext ctx, ReadOnlySpan<byte> data)
{
ctx.IdStack.Push(GetId(ctx, data));
}
public static void PopId(MuContext ctx)
{
ctx.IdStack.Pop();
}
public static void PushClipRect(MuContext ctx, MuRect rect) {
MuRect last = GetClipRect(ctx);
ctx.ClipStack.Push( IntersectRects(rect, last));
}
public static void PopClipRect(MuContext ctx)
{
ctx.ClipStack.Pop();
}
public static MuRect GetClipRect(MuContext ctx)
{
MuAssert.Expect(ctx.ClipStack.Index > 0);
return ctx.ClipStack.Items[ctx.ClipStack.Index - 1];
}
public static ClipMode CheckClip(MuContext ctx, MuRect r)
{
MuRect cr = GetClipRect(ctx);
// Completely outside clip rect
if (r.X > cr.X + cr.W || r.X + r.W < cr.X ||
r.Y > cr.Y + cr.H || r.Y + r.H < cr.Y)
{
return ClipMode.All; // MU_CLIP_ALL
}
// Completely inside clip rect
if (r.X >= cr.X && r.X + r.W <= cr.X + cr.W &&
r.Y >= cr.Y && r.Y + r.H <= cr.Y + cr.H)
{
return 0; // no clipping
}
// Partially clipped
return ClipMode.Part; // MU_CLIP_PART
}
public static void PushLayout(MuContext ctx, MuRect body, MuVec2 scroll)
{
MuLayout layout = new MuLayout();
// Adjust body position by subtracting scroll offset
layout.Body = new MuRect(
body.X - scroll.X,
body.Y - scroll.Y,
body.W,
body.H);
// Initialize max to very negative values (like C's -0x1000000)
layout.Max = new MuVec2(-0x1000000, -0x1000000);
// Push the layout struct onto the layout stack
ctx.LayoutStack.Push(layout);
// Call mu_layout_row with count=1, widths pointer to width var, height=0
int[] widths = { 0 };
LayoutRow(ctx, 1, widths, 0);
}
static ref MuLayout GetLayout(MuContext ctx)
{
if (ctx.LayoutStack.Index == 0)
throw new InvalidOperationException("Layout stack is empty.");
return ref ctx.LayoutStack.Items[ctx.LayoutStack.Index - 1];
}
static void PopContainer(MuContext ctx)
{
MuContainer cnt = GetCurrentContainer(ctx);
MuLayout layout = GetLayout(ctx);
cnt.ContentSize = new MuVec2(
layout.Max.X - layout.Body.X,
layout.Max.Y - layout.Body.Y
);
// Pop container, layout, and id
ctx.ContainerStack.Pop();
ctx.LayoutStack.Pop();
PopId(ctx);
}
static MuContainer GetCurrentContainer(MuContext ctx)
{
MuAssert.Expect(ctx.ContainerStack.Index > 0);
return ctx.ContainerStack.Items[ctx.ContainerStack.Index - 1];
}
/*============================================================================
** layout
**============================================================================*/
public static void LayoutRow(MuContext ctx, int items, int[] widths, int height)
{
var layout = GetLayout(ctx); // get current layout (implement accordingly)
if (items > Constants.MaxWidths)
throw new ArgumentOutOfRangeException(nameof(items), $"Items cannot be more than {Constants.MaxWidths}.");
// Copy widths into layout.Widths
Array.Copy(widths, layout.Widths, items);
layout.Items = items;
layout.Position = new MuVec2(layout.Indent, layout.NextRow);
layout.Size = new MuVec2(layout.Size.X, height); // assuming Size is MuVec2, updating only Y component
layout.ItemIndex = 0;
}
static MuContainer? GetContainer(MuContext ctx, uint id, int opt)
{
// Try to get an existing container from the pool
int idx = MuPool.Get(ctx, ctx.ContainerPool, ctx.Containers.Length, id);
if (idx >= 0)
{
if (ctx.Containers[idx].Open || (opt & (int)Option.Closed) == 0)
{
MuPool.Update(ctx, ctx.ContainerPool, idx);
}
return ctx.Containers[idx];
}
// If container is closed, return null
if ((opt & (int)Option.Closed) != 0)
{
return null;
}
// Create a new container
idx = MuPool.Init(ctx, ctx.ContainerPool, ctx.Containers.Length, id);
ref MuContainer cnt = ref ctx.Containers[idx];
cnt = new MuContainer(); // Reset all fields
cnt.Open = true;
BringToFront(ctx, ref cnt);
return cnt;
}
public static int Get(MuContext ctx, MuPoolItem[] items, int length, uint id)
{
for (int i = 0; i < length; i++)
{
if (items[i].Id == id)
{
return i;
}
}
return -1;
}
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Raylib-cs" Version="7.0.1" />
</ItemGroup>
</Project>

25
MicroUI.cs/Program.cs Normal file
View File

@ -0,0 +1,25 @@
using Raylib_cs;
namespace HelloWorld;
class Program
{
// STAThread is required if you deploy using NativeAOT on Windows - See https://github.com/raylib-cs/raylib-cs/issues/301
[STAThread]
public static void Main()
{
Raylib.InitWindow(800, 480, "Hello World");
while (!Raylib.WindowShouldClose())
{
Raylib.BeginDrawing();
Raylib.ClearBackground(Color.White);
Raylib.DrawText("Hello, world!", 12, 12, 20, Color.Black);
Raylib.EndDrawing();
}
Raylib.CloseWindow();
}
}