diff --git a/MicroUI.cs/MicroUI.cs b/MicroUI.cs/MicroUI.cs index 6ddf723..e8751a2 100644 --- a/MicroUI.cs/MicroUI.cs +++ b/MicroUI.cs/MicroUI.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using System.Security.Cryptography; namespace MicroUI { @@ -37,7 +38,7 @@ namespace MicroUI public static float Clamp(float x, float a, float b) => Min(b, Max(a, x)); } - public struct FixedStack + public class FixedStack { public int Index; public T[] Items; @@ -504,7 +505,7 @@ namespace MicroUI public static void DrawFrame(MuContext ctx, MuRect rect, ColorType colorId) { // Draw filled rectangle with given color - //DrawRect(ctx, rect, ctx.Style.Colors[(int)colorId]); // TODO: Implement DrawRect + MuCommandList.DrawRect(ctx, rect, ctx.Style.Colors[(int)colorId]); // Early return for certain color IDs (skip border) if (colorId == ColorType.ScrollBase || @@ -517,7 +518,7 @@ namespace MicroUI // 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]); // TODO: Implement DrawBox + MuCommandList.DrawBox(ctx, ExpandRect(rect, 1), ctx.Style.Colors[(int)ColorType.Border]); // TODO: Implement DrawBox } } @@ -530,6 +531,18 @@ namespace MicroUI // Copy the default style (make sure it's cloned, not shared!) ctx.Style = DefaultStyle; + + // Initialize containers array with actual MuContainer objects + for (int i = 0; i < ctx.Containers.Length; i++) + { + ctx.Containers[i] = new MuContainer(); + } + + // Initialize container pool items + for (int i = 0; i < ctx.ContainerPool.Length; i++) + { + ctx.ContainerPool[i] = new MuPoolItem(0, 0); + } } public static void Begin(MuContext ctx) @@ -579,7 +592,7 @@ namespace MicroUI ctx.NextHoverRoot.ZIndex < ctx.LastZIndex && ctx.NextHoverRoot.ZIndex >= 0) { - //BringToFront(ctx.NextHoverRoot); // TODO: Implement BringToFront + BringToFront(ctx, ctx.NextHoverRoot); // TODO: Implement BringToFront } ctx.KeyPressed = 0; @@ -651,8 +664,12 @@ namespace MicroUI } public static void PushClipRect(MuContext ctx, MuRect rect) { - MuRect last = GetClipRect(ctx); - ctx.ClipStack.Push( IntersectRects(rect, last)); + if (ctx.ClipStack.Index == 0) { + ctx.ClipStack.Push(rect); + } else { + MuRect last = GetClipRect(ctx); + ctx.ClipStack.Push(IntersectRects(rect, last)); + } } public static void PopClipRect(MuContext ctx) @@ -707,10 +724,10 @@ namespace MicroUI // Call mu_layout_row with count=1, widths pointer to width var, height=0 int[] widths = { 0 }; - LayoutRow(ctx, 1, widths, 0); + MuLayoutUtil.LayoutRow(ctx, 1, widths, 0); } - static ref MuLayout GetLayout(MuContext ctx) + public static ref MuLayout GetLayout(MuContext ctx) { if (ctx.LayoutStack.Index == 0) throw new InvalidOperationException("Layout stack is empty."); @@ -718,7 +735,7 @@ namespace MicroUI return ref ctx.LayoutStack.Items[ctx.LayoutStack.Index - 1]; } - static void PopContainer(MuContext ctx) + public static void PopContainer(MuContext ctx) { MuContainer cnt = GetCurrentContainer(ctx); MuLayout layout = GetLayout(ctx); @@ -734,7 +751,7 @@ namespace MicroUI PopId(ctx); } - static MuContainer GetCurrentContainer(MuContext ctx) + public static MuContainer GetCurrentContainer(MuContext ctx) { MuAssert.Expect(ctx.ContainerStack.Index > 0); return ctx.ContainerStack.Items[ctx.ContainerStack.Index - 1]; @@ -745,46 +762,49 @@ namespace MicroUI **============================================================================*/ - 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) + + public static MuContainer? GetContainer(MuContext ctx, uint id, int opt) { - //int idx = MuPool.Get(ctx, ctx.ContainerPool, ctx.Containers.Length, id); // TODO: Implement MuPool - int idx = MuPool.Get(ctx, ctx.ContainerPool, id); // TODO: Implement MuPool + // Try to get existing container from pool + int idx = MuPool.Get(ctx, ctx.ContainerPool, id); if (idx >= 0) { if (ctx.Containers[idx].Open || (opt & (int)Options.Closed) == 0) { - //MuPool.Update(ctx, ctx.ContainerPool, idx); // TODO: Implement MuPool - MuPool.Update(ctx, ctx.ContainerPool, idx); // TODO: Implement MuPool + MuPool.Update(ctx, ctx.ContainerPool, idx); } return ctx.Containers[idx]; } - - // Create a new container - //idx = MuPool.Init(ctx, ctx.ContainerPool, ctx.Containers.Length, id); // TODO: Implement MuPool - //ref MuContainer cnt = ref ctx.Containers[idx]; - //cnt = new MuContainer(); // Reset all fields - //cnt.Open = true; - //BringToFront(ctx, ref cnt); // TODO: Implement BringToFront - return null; + + // If we're looking for a closed container and it doesn't exist, return null + if ((opt & (int)Options.Closed) != 0) + { + return null; + } + + // Container not found in pool: init new container + idx = MuPool.Init(ctx, ctx.ContainerPool, id); + var cnt = ctx.Containers[idx]; + + // Reset all fields (equivalent to memset(cnt, 0, sizeof(*cnt))) + cnt.HeadIndex = -1; + cnt.TailIndex = -1; + cnt.Rect = new MuRect(0, 0, 0, 0); + cnt.Body = new MuRect(0, 0, 0, 0); + cnt.ContentSize = new MuVec2(0, 0); + cnt.Scroll = new MuVec2(0, 0); + cnt.ZIndex = 0; + cnt.Open = true; + + BringToFront(ctx, cnt); + return cnt; } - + public static void BringToFront(MuContext ctx, MuContainer cnt) + { + cnt.ZIndex = ++ctx.LastZIndex; + } } public static class MuPool @@ -875,4 +895,408 @@ namespace MicroUI } } } + + public static class MuCommandList + { + public static void Push(MuContext ctx, MuCommand cmd) + { + ctx.CommandList.Push(cmd); + } + + public static MuCommand? NextCommand(MuContext ctx, ref int index) + { + var commands = ctx.CommandList.Items; + int count = ctx.CommandList.Index; + + while (index < count) + { + var cmd = commands[index]; + if (cmd is MuJumpCommand jump) + { + // Jump to the destination index + if (jump.DestinationIndex < 0 || jump.DestinationIndex >= count) + return null; // Invalid jump, end iteration + index = jump.DestinationIndex; + continue; + } + // Return the current command and advance index + return commands[index++]; + } + return null; // End of command list + } + + public static MuJumpCommand PushJump(MuContext ctx, int destinationIndex = -1) + { + var jump = new MuJumpCommand { DestinationIndex = destinationIndex }; + ctx.CommandList.Push(jump); + return jump; + } + + public static void SetClip(MuContext ctx, MuRect rect) + { + MuCommandList.Push(ctx, new MuClipCommand(rect)); + } + + public static void DrawRect(MuContext ctx, MuRect rect, MuColor color) + { + // Intersect with current clip rect + rect = MicroUI.IntersectRects(rect, MicroUI.GetClipRect(ctx)); + if (rect.W > 0 && rect.H > 0) + { + MuCommandList.Push(ctx, new MuRectCommand(rect, color)); + } + } + + public static void DrawBox(MuContext ctx, MuRect rect, MuColor color) + { + DrawRect(ctx, new MuRect(rect.X + 1, rect.Y, rect.W - 2, 1), color); + DrawRect(ctx, new MuRect(rect.X + 1, rect.Y + rect.H - 1, rect.W - 2, 1), color); + DrawRect(ctx, new MuRect(rect.X, rect.Y, 1, rect.H), color); + DrawRect(ctx, new MuRect(rect.X + rect.W - 1, rect.Y, 1, rect.H), color); + } + + public static void DrawText(MuContext ctx, object? font, string str, MuVec2 pos, MuColor color) + { + int len = str.Length; + int w = ctx.TextWidth != null ? ctx.TextWidth(font, str, len) : 0; + int h = ctx.TextHeight != null ? ctx.TextHeight(font) : 0; + var rect = new MuRect(pos.X, pos.Y, w, h); + + var clipped = MicroUI.CheckClip(ctx, rect); + if (clipped == ClipMode.All) return; + if (clipped == ClipMode.Part) SetClip(ctx, MicroUI.GetClipRect(ctx)); + + // Add command + MuCommandList.Push(ctx, new MuTextCommand(font, pos, color, str)); + + // Reset clipping if it was set + if (clipped != 0) SetClip(ctx, MicroUI.UnclippedRect); + } + + public static void DrawIcon(MuContext ctx, int id, MuRect rect, MuColor color) + { + var clipped = MicroUI.CheckClip(ctx, rect); + if (clipped == ClipMode.All) return; + if (clipped == ClipMode.Part) SetClip(ctx, MicroUI.GetClipRect(ctx)); + + MuCommandList.Push(ctx, new MuIconCommand(rect, id, color)); + + if (clipped != 0) SetClip(ctx, MicroUI.UnclippedRect); + } + } + + // Layout next type enum (matches C enum { RELATIVE = 1, ABSOLUTE = 2 }) + enum LayoutNextType + { + Relative = 1, + Absolute = 2 + } + + public static class MuLayoutUtil + { + public static void LayoutBeginColumn(MuContext ctx) + { + var rect = LayoutNext(ctx); + MicroUI.PushLayout(ctx, rect, new MuVec2(0, 0)); + } + + public static void LayoutEndColumn(MuContext ctx) + { + // Get the child layout (top of stack) + ref var b = ref MicroUI.GetLayout(ctx); + ctx.LayoutStack.Pop(); + // Get the parent layout (now top of stack) + ref var a = ref MicroUI.GetLayout(ctx); + + a.Position = new MuVec2( + MathUtil.Max(a.Position.X, b.Position.X + b.Body.X - a.Body.X), + a.Position.Y + ); + a.NextRow = MathUtil.Max(a.NextRow, b.NextRow + b.Body.Y - a.Body.Y); + a.Max = new MuVec2( + MathUtil.Max(a.Max.X, b.Max.X), + MathUtil.Max(a.Max.Y, b.Max.Y) + ); + } + + public static void LayoutRow(MuContext ctx, int items, int[]? widths, int height) + { + ref var layout = ref MicroUI.GetLayout(ctx); + + if (widths != null) + { + MuAssert.Expect(items <= Constants.MaxWidths); + 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); // Only Y is updated + layout.ItemIndex = 0; + } + + public static void LayoutWidth(MuContext ctx, int width) + { + ref var layout = ref MicroUI.GetLayout(ctx); + layout.Size = new MuVec2(width, layout.Size.Y); + } + + public static void LayoutHeight(MuContext ctx, int height) + { + ref var layout = ref MicroUI.GetLayout(ctx); + layout.Size = new MuVec2(layout.Size.X, height); + } + + public static void LayoutSetNext(MuContext ctx, MuRect r, bool relative) + { + ref var layout = ref MicroUI.GetLayout(ctx); + layout.Next = r; + layout.NextType = relative ? (int)LayoutNextType.Relative : (int)LayoutNextType.Absolute; + } + + public static MuRect LayoutNext(MuContext ctx) + { + ref var layout = ref MicroUI.GetLayout(ctx); + var style = ctx.Style; + MuRect res; + + if (layout.NextType != 0) + { + int type = layout.NextType; + layout.NextType = 0; + res = layout.Next; + if (type == (int)LayoutNextType.Absolute) + { + ctx.LastRect = res; + return res; + } + } + else + { + // handle next row + if (layout.ItemIndex == layout.Items) + { + LayoutRow(ctx, layout.Items, null, layout.Size.Y); + } + + // position + res.X = layout.Position.X; + res.Y = layout.Position.Y; + + // size + res.W = layout.Items > 0 ? layout.Widths[layout.ItemIndex] : layout.Size.X; + res.H = layout.Size.Y; + if (res.W == 0) res.W = style.Size.X + style.Padding * 2; + if (res.H == 0) res.H = style.Size.Y + style.Padding * 2; + if (res.W < 0) res.W += layout.Body.W - res.X + 1; + if (res.H < 0) res.H += layout.Body.H - res.Y + 1; + + layout.ItemIndex++; + } + + // update position + layout.Position = new MuVec2( + layout.Position.X + res.W + style.Spacing, + layout.Position.Y + ); + layout.NextRow = MathUtil.Max(layout.NextRow, res.Y + res.H + style.Spacing); + + // apply body offset + res.X += layout.Body.X; + res.Y += layout.Body.Y; + + // update max position + layout.Max = new MuVec2( + MathUtil.Max(layout.Max.X, res.X + res.W), + MathUtil.Max(layout.Max.Y, res.Y + res.H) + ); + + ctx.LastRect = res; + return res; + } + } + + public static class MuControl + { + + public static void PushContainerBody(MuContext ctx, MuContainer cnt, MuRect body, int opt) + { + // If scrolling is enabled, handle scrollbars (stub for now) + if ((opt & (int)Options.NoScroll) == 0) + { + // TODO: Implement scrollbars(ctx, cnt, ref body); + } + + // Push layout for the container body, with padding and scroll offset + MicroUI.PushLayout(ctx, MicroUI.ExpandRect(body, -ctx.Style.Padding), cnt.Scroll); + + // Store the body rect in the container + cnt.Body = body; + } + + public static void BeginRootContainer(MuContext ctx, MuContainer cnt) + { + // Push container onto container stack + ctx.ContainerStack.Push(cnt); + + // Push container to roots list and push head command + ctx.RootList.Push(cnt); + cnt.HeadIndex = ctx.CommandList.Index; // Store the current command index as head + MuCommandList.PushJump(ctx); // Push a jump command (will be set up later) + + // Set as hover root if the mouse is overlapping this container and it has a + // higher zindex than the current hover root + if (MicroUI.RectOverlapsVec2(cnt.Rect, ctx.MousePos) && + (ctx.NextHoverRoot == null || cnt.ZIndex > ctx.NextHoverRoot.ZIndex)) + { + ctx.NextHoverRoot = cnt; + } + + // Clipping is reset here in case a root-container is made within + // another root-container's begin/end block; this prevents the inner + // root-container being clipped to the outer + MicroUI.PushClipRect(ctx, MicroUI.UnclippedRect); + + } + public static void EndRootContainer(MuContext ctx) + { + // Push tail 'goto' jump command and set head 'skip' command. The final steps + // on setting up these are done in MicroUI.End() + MuContainer cnt = MicroUI.GetCurrentContainer(ctx); + cnt.TailIndex = ctx.CommandList.Index; // Store the current command index as tail + MuCommandList.PushJump(ctx); // Push a jump command (will be set up later) + + // Set the head jump command to point to the current command index + // (This will be finalized in MicroUI.End()) + if (cnt.HeadIndex >= 0 && cnt.HeadIndex < ctx.CommandList.Index) + { + var headJump = ctx.CommandList.Items[cnt.HeadIndex] as MuJumpCommand; + if (headJump != null) + { + headJump.DestinationIndex = ctx.CommandList.Index; + } + } + + // Pop base clip rect and container + MicroUI.PopClipRect(ctx); + MicroUI.PopContainer(ctx); + } + public static bool BeginWindowEx(MuContext ctx, string title, MuRect rect, int opt) + { + // 1. Get/Init Container + uint id = MicroUI.GetId(ctx, System.Text.Encoding.UTF8.GetBytes(title)); + MuContainer? cnt = MicroUI.GetContainer(ctx, id, opt); + if (cnt == null || !cnt.Open) return false; + + // 2. Push ID + MicroUI.PushId(ctx, System.Text.Encoding.UTF8.GetBytes(title)); + + // 3. Set Initial Rect + if (cnt.Rect.W == 0) cnt.Rect = rect; + + // 4. Begin Root Container + MuControl.BeginRootContainer(ctx, cnt); + rect = cnt.Rect; + var body = rect; + + // 5. Draw Window Frame + if ((opt & (int)Options.NoFrame) == 0) + { + ctx.DrawFrame?.Invoke(ctx, rect, ColorType.WindowBg); + } + + // 6. Draw Title Bar + if ((opt & (int)Options.NoTitle) == 0) + { + var tr = rect; + tr.H = ctx.Style.TitleHeight; + ctx.DrawFrame?.Invoke(ctx, tr, ColorType.TitleBg); + + // Title text and drag + uint titleId = MicroUI.GetId(ctx, System.Text.Encoding.UTF8.GetBytes("!title")); + //MuControl.UpdateControl(ctx, titleId, tr, opt); // Implement or comment out + //MuControl.DrawControlText(ctx, title, tr, ColorType.TitleText, opt); // Implement or comment out + + // Move window if dragging title bar + if (titleId == ctx.Focus && (ctx.MouseDown & (int)MouseButton.Left) != 0) + { + cnt.Rect = new MuRect( + cnt.Rect.X + ctx.MouseDelta.X, + cnt.Rect.Y + ctx.MouseDelta.Y, + cnt.Rect.W, + cnt.Rect.H + ); + } + + // Adjust body for title bar + body.Y += tr.H; + body.H -= tr.H; + + // Close button + if ((opt & (int)Options.NoClose) == 0) + { + uint closeId = MicroUI.GetId(ctx, System.Text.Encoding.UTF8.GetBytes("!close")); + var r = new MuRect(tr.X + tr.W - tr.H, tr.Y, tr.H, tr.H); + tr.W -= r.W; + //MuControl.DrawIcon(ctx, (int)IconType.Close, r, ctx.Style.Colors[(int)ColorType.TitleText]); // Implement or comment out + //MuControl.UpdateControl(ctx, closeId, r, opt); // Implement or comment out + if ((ctx.MousePressed & (int)MouseButton.Left) != 0 && closeId == ctx.Focus) + { + cnt.Open = false; + } + } + } + + // 7. Push Container Body Layout + MuControl.PushContainerBody(ctx, cnt, body, opt); // Implement or comment out + + // 8. Resize handle + if ((opt & (int)Options.NoResize) == 0) + { + int sz = ctx.Style.TitleHeight; + uint resizeId = MicroUI.GetId(ctx, System.Text.Encoding.UTF8.GetBytes("!resize")); + var r = new MuRect(rect.X + rect.W - sz, rect.Y + rect.H - sz, sz, sz); + //MuControl.UpdateControl(ctx, resizeId, r, opt); // Implement or comment out + if (resizeId == ctx.Focus && (ctx.MouseDown & (int)MouseButton.Left) != 0) + { + cnt.Rect = new MuRect( + cnt.Rect.X, + cnt.Rect.Y, + MathUtil.Max(96, cnt.Rect.W + ctx.MouseDelta.X), + MathUtil.Max(64, cnt.Rect.H + ctx.MouseDelta.Y) + ); + } + } + + // 9. Auto-size + if ((opt & (int)Options.AutoSize) != 0) + { + var r = MicroUI.GetLayout(ctx).Body; + cnt.Rect = new MuRect( + cnt.Rect.X, + cnt.Rect.Y, + cnt.ContentSize.X + (cnt.Rect.W - r.W), + cnt.ContentSize.Y + (cnt.Rect.H - r.H) + ); + } + + // 10. Popup close + if ((opt & (int)Options.Popup) != 0 && ctx.MousePressed != 0 && ctx.HoverRoot != cnt) + { + cnt.Open = false; + } + + // 11. Push Clip Rect + MicroUI.PushClipRect(ctx, cnt.Body); + + // 12. Return Active + return true; + } + + public static void EndWindow(MuContext ctx) + { + MicroUI.PopClipRect(ctx); + MuControl.EndRootContainer(ctx); + } + + } } \ No newline at end of file diff --git a/MicroUI.cs/Program.cs b/MicroUI.cs/Program.cs index 9439d66..0423b90 100644 --- a/MicroUI.cs/Program.cs +++ b/MicroUI.cs/Program.cs @@ -1,6 +1,7 @@ using Raylib_cs; +using MicroUI; -namespace HelloWorld; +namespace MicroUI; class Program { @@ -10,14 +11,74 @@ class Program { Raylib.InitWindow(800, 480, "Hello World"); + // Create microui context + var ctx = new MuContext(); + + // Set up required callbacks + ctx.TextWidth = (object font, string str, int len) => + { + return str.Length * 8; // Simple approximation + }; + + ctx.TextHeight = (object font) => + { + return 16; // Simple approximation + }; + // Initialize microui + MicroUI.Init(ctx); + while (!Raylib.WindowShouldClose()) { Raylib.BeginDrawing(); Raylib.ClearBackground(Color.White); - Raylib.DrawText("Hello, world!", 12, 12, 20, Color.Black); + // Begin frame + MicroUI.Begin(ctx); + if(MuControl.BeginWindowEx(ctx, "Test Window", new MuRect(100, 100, 100, 100), 0)){ + + MuControl.EndWindow(ctx); + } + + if(MuControl.BeginWindowEx(ctx, "Test Window 2", new MuRect(120, 120, 300, 200), 0)){ + MuControl.EndWindow(ctx); + } + + // End frame + MicroUI.End(ctx); + + // Print all commands in the command list + int cmdIndex = 0; + MuCommand? cmd = null; + while ((cmd = MuCommandList.NextCommand(ctx, ref cmdIndex)) != null) + { + Console.Write($"Command: {cmd.Type}"); + switch (cmd) + { + case MuRectCommand rectCmd: + Console.Write($" | Rect: ({rectCmd.Rect.X},{rectCmd.Rect.Y},{rectCmd.Rect.W},{rectCmd.Rect.H}) Color: {rectCmd.Color.R},{rectCmd.Color.G},{rectCmd.Color.B},{rectCmd.Color.A}"); + Raylib.DrawRectangle(rectCmd.Rect.X, rectCmd.Rect.Y, rectCmd.Rect.W, rectCmd.Rect.H, new Color(rectCmd.Color.R, rectCmd.Color.G, rectCmd.Color.B, rectCmd.Color.A)); + break; + case MuClipCommand clipCmd: + Console.Write($" | Clip: ({clipCmd.Rect.X},{clipCmd.Rect.Y},{clipCmd.Rect.W},{clipCmd.Rect.H})"); + break; + case MuJumpCommand jumpCmd: + Console.Write($" | Jump to: {jumpCmd.DestinationIndex}"); + break; + case MuTextCommand textCmd: + Console.Write($" | Text: '{textCmd.Text}' at ({textCmd.Position.X},{textCmd.Position.Y}) Color: {textCmd.Color.R},{textCmd.Color.G},{textCmd.Color.B},{textCmd.Color.A}"); + break; + case MuIconCommand iconCmd: + Console.Write($" | Icon: {iconCmd.IconId} Rect: ({iconCmd.Rect.X},{iconCmd.Rect.Y},{iconCmd.Rect.W},{iconCmd.Rect.H}) Color: {iconCmd.Color.R},{iconCmd.Color.G},{iconCmd.Color.B},{iconCmd.Color.A}"); + break; + } + Console.WriteLine(""); + + } Raylib.EndDrawing(); + + //break; + } Raylib.CloseWindow();