From 5de6c6a21288ff0b177e257d797fbf28abf8bef6 Mon Sep 17 00:00:00 2001 From: Bobby Lucero Date: Sat, 12 Jul 2025 02:24:22 -0400 Subject: [PATCH] Basic Calculator with expression parsing --- MicroUI.cs/MicroUI.cs | 18 +- MicroUI.cs/Program.cs | 545 ++++++++++++++++++++++-------------------- 2 files changed, 293 insertions(+), 270 deletions(-) diff --git a/MicroUI.cs/MicroUI.cs b/MicroUI.cs/MicroUI.cs index b6592ba..aec2e98 100644 --- a/MicroUI.cs/MicroUI.cs +++ b/MicroUI.cs/MicroUI.cs @@ -10,15 +10,15 @@ namespace MicroUI { 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 const int RootListSize = 256; + public const int ContainerStackSize = 256; + public const int ClipStackSize = 256; + public const int IdStackSize = 256; + public const int LayoutStackSize = 256; + public const int ContainerPoolSize = 256; + public const int TreeNodePoolSize = 256; + public const int MaxWidths = 256; + public const int MaxFormatLength = 256; } public static class Format diff --git a/MicroUI.cs/Program.cs b/MicroUI.cs/Program.cs index 6501e01..da054a3 100644 --- a/MicroUI.cs/Program.cs +++ b/MicroUI.cs/Program.cs @@ -1,324 +1,347 @@ using Raylib_cs; using MicroUI; -using System.Numerics; +using System; +using System.Collections.Generic; +using System.Linq; namespace MicroUI; class Program { - // Key state tracking for key up events - private static HashSet pressedKeys = new HashSet(); - // Persistent textbox value - private static string textboxValue = "test"; - // Second number value for testing multiple number controls - private static float secondValue = 25.0f; - // Track focus changes - private static uint lastFocus = 0; - - // STAThread is required if you deploy using NativeAOT on Windows - See https://github.com/raylib-cs/raylib-cs/issues/301 + private static string display = "0"; + private static string expression = ""; + private static string lastOperation = ""; + private static double lastResult = 0; + private static bool evaluated = false; + private static string currentInput = ""; + + private static readonly string[,] keys = + { + { "7", "8", "9", "/" }, + { "4", "5", "6", "*" }, + { "1", "2", "3", "-" }, + { "0", "(", ")", "+" } + }; + [STAThread] public static void Main() { - 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) => - { - // Use Raylib's MeasureText to get actual text width - // If font is null, use default font, otherwise cast to Font - Font textFont = font as Font? ?? Raylib.GetFontDefault(); - return Raylib.MeasureText(str, 10); // 16 is font size - }; - - ctx.TextHeight = (object font) => - { - // Use Raylib's font height measurement - Font textFont = font as Font? ?? Raylib.GetFontDefault(); - return 10; // Return a fixed height for now, or use textFont.baseSize if available - }; - // Initialize microui - MicroUI.Init(ctx); - - bool isChecked = false; - float sliderValue = 50.0f; // Remove persistent slider value - + const int winW = 300; + const int winH = 450; + Raylib.SetConfigFlags(ConfigFlags.ResizableWindow); + Raylib.InitWindow(800, 600, "Calculator"); + var ctx = new MuContext(); + ctx.TextWidth = (object font, string str, int len) => Raylib.MeasureText(str, 20); + ctx.TextHeight = (object font) => 24; + MicroUI.Init(ctx); while (!Raylib.WindowShouldClose()) { - // Handle input var mousePos = Raylib.GetMousePosition(); MuInput.MouseMove(ctx, (int)mousePos.X, (int)mousePos.Y); - - // Track focus changes in main loop - if (ctx.Focus != lastFocus) - { - lastFocus = ctx.Focus; - } - if (Raylib.IsMouseButtonPressed(Raylib_cs.MouseButton.Left)) - { - MuInput.MouseDown(ctx, (int)mousePos.X, (int)mousePos.Y, 1); - } - else if (Raylib.IsMouseButtonDown(Raylib_cs.MouseButton.Left)) - { - // Keep MouseDown state active while button is held - ctx.MouseDown = 1; - } + MuInput.MouseDown(ctx, (int)mousePos.X, (int)mousePos.Y, (int)MouseButton.Left); if (Raylib.IsMouseButtonReleased(Raylib_cs.MouseButton.Left)) - { - MuInput.MouseUp(ctx, (int)mousePos.X, (int)mousePos.Y, 1); - } - - // Handle keyboard input - only for modifier keys - int key = Raylib.GetKeyPressed(); - if (key != 0) - { - // Only handle modifier keys here, not regular characters - // Regular characters are handled in the text input section below - if (key == (int)KeyboardKey.Enter || key == (int)KeyboardKey.Backspace) - { - // Convert to MicroUI key modifiers - int muKey = 0; - if (key == (int)KeyboardKey.Enter) muKey = (int)KeyModifiers.Return; - if (key == (int)KeyboardKey.Backspace) muKey = (int)KeyModifiers.Backspace; - - MuInput.KeyDown(ctx, muKey); - pressedKeys.Add(muKey); - } - } - - // Handle Shift key state (for Shift+Click functionality) - if (Raylib.IsKeyDown(KeyboardKey.LeftShift) || Raylib.IsKeyDown(KeyboardKey.RightShift)) - { - ctx.KeyDown |= (int)KeyModifiers.Shift; - } - else - { - ctx.KeyDown &= ~(int)KeyModifiers.Shift; - } - - // Handle key up events for modifier keys - var keysToRemove = new List(); - foreach (int pressedKey in pressedKeys) - { - // Convert back from MicroUI key modifiers to Raylib keys for checking - KeyboardKey raylibKey = KeyboardKey.Null; - if (pressedKey == (int)KeyModifiers.Return) raylibKey = KeyboardKey.Enter; - if (pressedKey == (int)KeyModifiers.Backspace) raylibKey = KeyboardKey.Backspace; - - if (raylibKey != KeyboardKey.Null && !Raylib.IsKeyDown(raylibKey)) - { - MuInput.KeyUp(ctx, pressedKey); - keysToRemove.Add(pressedKey); - } - } - foreach (int keyToRemove in keysToRemove) - { - pressedKeys.Remove(keyToRemove); - } - - // Handle text input - int keyChar; - while ((keyChar = Raylib.GetCharPressed()) != 0) - { - char typedChar = (char)keyChar; - MuInput.InputText(ctx, typedChar.ToString()); - } - // Debug print for KeyPressed (commented out to reduce noise) - // Console.WriteLine($"DEBUG: KeyPressed after input: {ctx.KeyPressed}"); - - Raylib.BeginDrawing(); - Raylib.ClearBackground(Color.Black); + MuInput.MouseUp(ctx, (int)mousePos.X, (int)mousePos.Y, (int)MouseButton.Left); + + Raylib.BeginDrawing(); + Raylib.ClearBackground(Color.DarkGray); - // Begin frame MicroUI.Begin(ctx); - - for(int i = 0; i < 1; i++){ - Window(ctx, i * 15, i * 15, i, ref isChecked, ref sliderValue); // Remove ref sliderValue + + var winX = (Raylib.GetScreenWidth() - winW) / 2; + var winY = (Raylib.GetScreenHeight() - winH) / 2; + if (MuControl.BeginWindowEx(ctx, "Calculator", new MuRect(winX, winY, winW, winH), (int)Options.NoClose)) + { + var container = MicroUI.GetCurrentContainer(ctx); + var availableWidth = container.Body.W - 32; + var buttonWidth = availableWidth / 4; + var buttonHeight = 48; + + MuLayoutUtil.LayoutRow(ctx, 1, new int[] { availableWidth }, 48); + string toShow = display == "Error" ? "Error" : (evaluated ? display : (expression.Length > 0 ? expression : "0")); + MuRect displayRect = MuLayoutUtil.LayoutNext(ctx); + MuControl.DrawControlText(ctx, toShow, displayRect, ColorType.Text, (int)Options.AlignRight); + + for (int row = 0; row < 4; row++) + { + MuLayoutUtil.LayoutRow(ctx, 4, new int[] { buttonWidth, buttonWidth, buttonWidth, buttonWidth }, buttonHeight); + for (int col = 0; col < 4; col++) + { + string key = keys[row, col]; + if (MuControl.ButtonEx(ctx, key, 0, (int)Options.AlignCenter).Flags.HasFlag(ResultFlags.Submit)) + HandleKey(key); + } + } + + MuLayoutUtil.LayoutRow(ctx, 4, new int[] { buttonWidth, buttonWidth, buttonWidth, buttonWidth }, buttonHeight); + if (MuControl.ButtonEx(ctx, "C", 0, (int)Options.AlignCenter).Flags.HasFlag(ResultFlags.Submit)) HandleKey("C"); + if (MuControl.ButtonEx(ctx, ".", 0, (int)Options.AlignCenter).Flags.HasFlag(ResultFlags.Submit)) HandleKey("."); + if (MuControl.ButtonEx(ctx, "<-", 0, (int)Options.AlignCenter).Flags.HasFlag(ResultFlags.Submit)) HandleKey("<-"); + if (MuControl.ButtonEx(ctx, "=", 0, (int)Options.AlignCenter).Flags.HasFlag(ResultFlags.Submit)) HandleKey("="); + + 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.WriteLine($" | 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})"); Raylib.BeginScissorMode(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: - Raylib.DrawText(textCmd.Text, textCmd.Position.X, textCmd.Position.Y, 10, new Color(textCmd.Color.R, textCmd.Color.G, textCmd.Color.B, textCmd.Color.A)); - //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: - if (iconCmd.IconId == (int)IconType.Check) - { - // Draw a check mark - int centerX = iconCmd.Rect.X + iconCmd.Rect.W / 2; - int centerY = iconCmd.Rect.Y + iconCmd.Rect.H / 2; - int size = Math.Min(iconCmd.Rect.W, iconCmd.Rect.H) / 3; - - // Draw check mark lines - Raylib.DrawLine( - centerX - size, centerY - size/2, - centerX - size/3, centerY + size/2, - new Color(iconCmd.Color.R, iconCmd.Color.G, iconCmd.Color.B, iconCmd.Color.A) - ); - Raylib.DrawLine( - centerX - size/3, centerY + size/2, - centerX + size, centerY - size, - new Color(iconCmd.Color.R, iconCmd.Color.G, iconCmd.Color.B, iconCmd.Color.A) - ); - } - else if (iconCmd.IconId == (int)IconType.Close) - { - // Draw an X for close - int centerX = iconCmd.Rect.X + iconCmd.Rect.W / 2; - int centerY = iconCmd.Rect.Y + iconCmd.Rect.H / 2; - int size = Math.Min(iconCmd.Rect.W, iconCmd.Rect.H) / 3; - - // Draw X lines - Raylib.DrawLine( - centerX - size, centerY - size, - centerX + size, centerY + size, - new Color(iconCmd.Color.R, iconCmd.Color.G, iconCmd.Color.B, iconCmd.Color.A) - ); - Raylib.DrawLine( - centerX - size, centerY + size, - centerX + size, centerY - size, - new Color(iconCmd.Color.R, iconCmd.Color.G, iconCmd.Color.B, iconCmd.Color.A) - ); - } - else if (iconCmd.IconId == (int)IconType.Collapsed) - { - // Draw a right-pointing triangle for collapsed - int centerX = iconCmd.Rect.X + iconCmd.Rect.W / 2; - int centerY = iconCmd.Rect.Y + iconCmd.Rect.H / 2; - int size = Math.Min(iconCmd.Rect.W, iconCmd.Rect.H) / 3; - - // Draw triangle pointing right - Raylib.DrawTriangle( - new Vector2(centerX - size/2, centerY - size), - new Vector2(centerX + size/2, centerY), - new Vector2(centerX - size/2, centerY + size), - new Color(iconCmd.Color.R, iconCmd.Color.G, iconCmd.Color.B, iconCmd.Color.A) - ); - } - else if (iconCmd.IconId == (int)IconType.Expanded) - { - // Draw a down-pointing triangle for expanded - int centerX = iconCmd.Rect.X + iconCmd.Rect.W / 2; - int centerY = iconCmd.Rect.Y + iconCmd.Rect.H / 2; - int size = Math.Min(iconCmd.Rect.W, iconCmd.Rect.H) / 3; - - // Draw triangle pointing down - Raylib.DrawTriangle( - new Vector2(centerX - size, centerY - size/2), - new Vector2(centerX + size, centerY - size/2), - new Vector2(centerX, centerY + size/2), - new Color(iconCmd.Color.R, iconCmd.Color.G, iconCmd.Color.B, iconCmd.Color.A) - ); - } - else - { - // For unknown icons, draw a colored rectangle - Raylib.DrawRectangle(iconCmd.Rect.X, iconCmd.Rect.Y, iconCmd.Rect.W, iconCmd.Rect.H, - new Color(iconCmd.Color.R, iconCmd.Color.G, iconCmd.Color.B, iconCmd.Color.A)); - } + Raylib.DrawText(textCmd.Text, textCmd.Position.X, textCmd.Position.Y, 20, new Color(textCmd.Color.R, textCmd.Color.G, textCmd.Color.B, textCmd.Color.A)); break; } - } Raylib.EndDrawing(); - //break; } - Raylib.CloseWindow(); } - - public static void Window(MuContext ctx, int x, int y, int index, ref bool isChecked, ref float sliderValue){ - if(MuControl.BeginWindowEx(ctx, $"Test Window", new MuRect(x, y, 300, 200), 0)){ - // Manual two-column layout - MuLayoutUtil.LayoutRow(ctx, 2, new int[] { 120, 160 }, 0); // Adjust widths as needed - - - // Left column (sliders/text) + private static void HandleKey(string key) + { + if (key == "<-") + { + if (evaluated) { - MuLayoutUtil.LayoutRow(ctx, 1, new int[] { 120 }, 0); - float test = 0; - //var sliderResult = MuControl.SliderEx(ctx, "main_slider", ref sliderValue, 0.0f, 100.0f, 1.0f, "F1", 0); - MuControl.SliderEx(ctx, "test_slider", ref test, 0.0f, 100.0f, 1.0f, "F1", 0); - // if ((sliderResult & ResultFlags.Change) != 0) - // { - // Console.WriteLine($"Slider value changed to: {sliderValue}"); - // } - MuControl.Text(ctx, "Other controls here..."); + display = "0"; + currentInput = ""; + expression = ""; + evaluated = false; + } + else if (expression.Length > 0) + { + expression = expression[..^1]; + if (currentInput.Length > 0) + currentInput = currentInput[..^1]; + if (expression.Length == 0) + display = "0"; + } + return; + } - // Test TextboxRaw - var textboxRect = MuLayoutUtil.LayoutNext(ctx); - var textboxResult = MuControl.TextboxRaw(ctx, ref textboxValue, 12345, textboxRect, 0); - if ((textboxResult & ResultFlags.Change) != 0) + if (char.IsDigit(key, 0) || key == ".") + { + if (evaluated) + { + expression = ""; + evaluated = false; + } + if (key == "." && currentInput.Contains(".")) return; + currentInput += key; + expression += key; + return; + } + + if (key == "C") + { + display = "0"; + currentInput = ""; + expression = ""; + evaluated = false; + return; + } + + if (key == "=") + { + if (expression.Length > 0 && !IsOperator(expression[^1]) && expression[^1] != '(' && !evaluated) + { + try { - Console.WriteLine($"Textbox changed: {textboxValue}"); + var result = EvaluateExpression(expression); + display = FormatResult(result); + lastResult = result; + lastOperation = expression; + currentInput = ""; + expression = ""; + evaluated = true; } - - // Test NumberEx (number textbox) - MuControl.Text(ctx, "Number control (Shift+Click to edit):"); - var numberResult = MuControl.NumberEx(ctx, "test_number", ref sliderValue, 1.0f, "F2", 0); - if ((numberResult & ResultFlags.Change) != 0) + catch { - Console.WriteLine($"Number changed to: {sliderValue}"); - } - - // Test second NumberEx (number textbox) - MuControl.Text(ctx, "Second number control (Shift+Click to edit):"); - var secondNumberResult = MuControl.NumberEx(ctx, "test_number2", ref secondValue, 0.5f, "F2", 0); - if ((secondNumberResult & ResultFlags.Change) != 0) - { - Console.WriteLine($"Second number changed to: {secondValue}"); + display = "Error"; } } - - // Right column (button grid) + else if (evaluated && lastOperation.Length > 0) { - MuLayoutUtil.LayoutRow(ctx, 1, new int[] { 160 }, 0); - MuControl.Text(ctx, "4x4 Grid of Buttons with 'x' labels:"); - for (int row = 0; row < 4; row++) + try { - MuLayoutUtil.LayoutRow(ctx, 4, new int[] { 25, 25, 25, 25 }, 0); - for (int col = 0; col < 4; col++) + var tokens = Tokenize(lastOperation); + if (tokens.Count == 3 && tokens[1] is "+" or "-" or "*" or "/") { - MicroUI.PushId(ctx, System.Text.Encoding.UTF8.GetBytes($"grid_{row}_{col}")); - if (MuControl.ButtonEx(ctx, "x", 0, (int)Options.AlignCenter)) + var op = tokens[1]; + var right = tokens[2]; + var newExpr = $"{lastResult} {op} {right}"; + var result = EvaluateExpression(newExpr); + display = FormatResult(result); + lastResult = result; + expression = newExpr; + } + else + { + string op = ""; + string right = ""; + for (int i = tokens.Count - 1; i >= 0; i--) { - Console.WriteLine($"Button at ({row},{col}) clicked!"); + if (tokens[i] is "+" or "-" or "*" or "/") + { + op = tokens[i]; + right = lastResult.ToString(); + break; + } + } + if (!string.IsNullOrEmpty(op)) + { + var newExpr = $"{lastResult} {op} {right}"; + var result = EvaluateExpression(newExpr); + display = FormatResult(result); + lastResult = result; + expression = newExpr; } - MicroUI.PopId(ctx); } } + catch + { + display = "Error"; + } } + return; + } - - MuControl.EndWindow(ctx); + if (key is "(" or ")") + { + if (evaluated) + { + expression = ""; + evaluated = false; + } + expression += key; + currentInput = ""; + return; + } + + if (expression.Length == 0 && key == "-") + { + currentInput = "-"; + expression = "-"; + return; + } + if (expression.Length > 0 && !IsOperator(expression[^1]) && expression[^1] != '(') + { + expression += " " + key + " "; + currentInput = ""; + evaluated = false; } } + + private static bool IsOperator(char c) => c is '+' or '-' or '*' or '/'; + + private static double EvaluateExpression(string expr) + { + var tokens = Tokenize(expr); + int idx = 0; + var result = ParseExpression(tokens, ref idx); + if (idx < tokens.Count) + throw new Exception("Unexpected token: " + tokens[idx]); + return result; + } + + private static List Tokenize(string expr) + { + var tokens = new List(); + var current = ""; + foreach (var c in expr) + { + if (char.IsDigit(c) || c == '.') + current += c; + else if (c is '+' or '-' or '*' or '/' or '(' or ')') + { + if (current.Length > 0) + { + tokens.Add(current); + current = ""; + } + tokens.Add(c.ToString()); + } + else if (c == ' ') + { + if (current.Length > 0) + { + tokens.Add(current); + current = ""; + } + } + } + if (current.Length > 0) + tokens.Add(current); + return tokens; + } + + private static double ParseExpression(List tokens, ref int idx) + { + var left = ParseTerm(tokens, ref idx); + while (idx < tokens.Count && (tokens[idx] == "+" || tokens[idx] == "-")) + { + var op = tokens[idx++]; + var right = ParseTerm(tokens, ref idx); + left = op == "+" ? left + right : left - right; + } + return left; + } + + private static double ParseTerm(List tokens, ref int idx) + { + var left = ParseFactor(tokens, ref idx); + while (idx < tokens.Count && (tokens[idx] == "*" || tokens[idx] == "/")) + { + var op = tokens[idx++]; + var right = ParseFactor(tokens, ref idx); + if (op == "*") left *= right; + else if (op == "/" && right != 0) left /= right; + else throw new DivideByZeroException(); + } + return left; + } + + private static double ParseFactor(List tokens, ref int idx) + { + if (idx >= tokens.Count) + throw new Exception("Unexpected end of expression"); + var token = tokens[idx++]; + if (token == "(") + { + var result = ParseExpression(tokens, ref idx); + if (idx >= tokens.Count || tokens[idx] != ")") + throw new Exception("Missing closing parenthesis"); + idx++; + return result; + } + if (double.TryParse(token, out double number)) + return number; + throw new Exception($"Invalid token: {token}"); + } + + private static string FormatResult(double value) + { + value = Math.Round(value, 10); + if (Math.Abs(value - Math.Round(value)) < 1e-10) + value = Math.Round(value); + var s = value.ToString("G15"); + if (s.Contains(".")) + { + s = s.TrimEnd('0'); + if (s.EndsWith(".")) + s = s.TrimEnd('.'); + } + return s; + } } \ No newline at end of file