Basic Calculator with expression parsing

This commit is contained in:
Bobby Lucero 2025-07-12 02:24:22 -04:00
parent cdc54751a3
commit 5de6c6a212
2 changed files with 293 additions and 270 deletions

View File

@ -10,15 +10,15 @@ namespace MicroUI
{ {
public const string Version = "1.0.0"; public const string Version = "1.0.0";
public const int CommandListSize = 256 * 1024; public const int CommandListSize = 256 * 1024;
public const int RootListSize = 32; public const int RootListSize = 256;
public const int ContainerStackSize = 32; public const int ContainerStackSize = 256;
public const int ClipStackSize = 32; public const int ClipStackSize = 256;
public const int IdStackSize = 32; public const int IdStackSize = 256;
public const int LayoutStackSize = 16; public const int LayoutStackSize = 256;
public const int ContainerPoolSize = 48; public const int ContainerPoolSize = 256;
public const int TreeNodePoolSize = 48; public const int TreeNodePoolSize = 256;
public const int MaxWidths = 16; public const int MaxWidths = 256;
public const int MaxFormatLength = 127; public const int MaxFormatLength = 256;
} }
public static class Format public static class Format

View File

@ -1,324 +1,347 @@
using Raylib_cs; using Raylib_cs;
using MicroUI; using MicroUI;
using System.Numerics; using System;
using System.Collections.Generic;
using System.Linq;
namespace MicroUI; namespace MicroUI;
class Program class Program
{ {
// Key state tracking for key up events private static string display = "0";
private static HashSet<int> pressedKeys = new HashSet<int>(); private static string expression = "";
// Persistent textbox value private static string lastOperation = "";
private static string textboxValue = "test"; private static double lastResult = 0;
// Second number value for testing multiple number controls private static bool evaluated = false;
private static float secondValue = 25.0f; private static string currentInput = "";
// Track focus changes
private static uint lastFocus = 0; private static readonly string[,] keys =
{
// STAThread is required if you deploy using NativeAOT on Windows - See https://github.com/raylib-cs/raylib-cs/issues/301 { "7", "8", "9", "/" },
{ "4", "5", "6", "*" },
{ "1", "2", "3", "-" },
{ "0", "(", ")", "+" }
};
[STAThread] [STAThread]
public static void Main() public static void Main()
{ {
Raylib.InitWindow(800, 480, "Hello World"); const int winW = 300;
const int winH = 450;
// Create microui context Raylib.SetConfigFlags(ConfigFlags.ResizableWindow);
var ctx = new MuContext(); Raylib.InitWindow(800, 600, "Calculator");
var ctx = new MuContext();
// Set up required callbacks ctx.TextWidth = (object font, string str, int len) => Raylib.MeasureText(str, 20);
ctx.TextWidth = (object font, string str, int len) => ctx.TextHeight = (object font) => 24;
{ MicroUI.Init(ctx);
// 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
while (!Raylib.WindowShouldClose()) while (!Raylib.WindowShouldClose())
{ {
// Handle input
var mousePos = Raylib.GetMousePosition(); var mousePos = Raylib.GetMousePosition();
MuInput.MouseMove(ctx, (int)mousePos.X, (int)mousePos.Y); 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)) if (Raylib.IsMouseButtonPressed(Raylib_cs.MouseButton.Left))
{ MuInput.MouseDown(ctx, (int)mousePos.X, (int)mousePos.Y, (int)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;
}
if (Raylib.IsMouseButtonReleased(Raylib_cs.MouseButton.Left)) if (Raylib.IsMouseButtonReleased(Raylib_cs.MouseButton.Left))
{ MuInput.MouseUp(ctx, (int)mousePos.X, (int)mousePos.Y, (int)MouseButton.Left);
MuInput.MouseUp(ctx, (int)mousePos.X, (int)mousePos.Y, 1);
} Raylib.BeginDrawing();
Raylib.ClearBackground(Color.DarkGray);
// 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<int>();
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);
// Begin frame
MicroUI.Begin(ctx); MicroUI.Begin(ctx);
for(int i = 0; i < 1; i++){ var winX = (Raylib.GetScreenWidth() - winW) / 2;
Window(ctx, i * 15, i * 15, i, ref isChecked, ref sliderValue); // Remove ref sliderValue 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); MicroUI.End(ctx);
// Print all commands in the command list
int cmdIndex = 0; int cmdIndex = 0;
MuCommand? cmd = null; MuCommand? cmd = null;
while ((cmd = MuCommandList.NextCommand(ctx, ref cmdIndex)) != null) while ((cmd = MuCommandList.NextCommand(ctx, ref cmdIndex)) != null)
{ {
//Console.Write($"Command: {cmd.Type}");
switch (cmd) switch (cmd)
{ {
case MuRectCommand rectCmd: 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)); 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; break;
case MuClipCommand clipCmd: 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); Raylib.BeginScissorMode(clipCmd.Rect.X, clipCmd.Rect.Y, clipCmd.Rect.W, clipCmd.Rect.H);
break; break;
case MuJumpCommand jumpCmd:
//Console.Write($" | Jump to: {jumpCmd.DestinationIndex}");
break;
case MuTextCommand textCmd: 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)); 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));
//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));
}
break; break;
} }
} }
Raylib.EndDrawing(); Raylib.EndDrawing();
//break;
} }
Raylib.CloseWindow(); Raylib.CloseWindow();
} }
private static void HandleKey(string key)
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)){ if (key == "<-")
// Manual two-column layout {
MuLayoutUtil.LayoutRow(ctx, 2, new int[] { 120, 160 }, 0); // Adjust widths as needed if (evaluated)
// Left column (sliders/text)
{ {
MuLayoutUtil.LayoutRow(ctx, 1, new int[] { 120 }, 0); display = "0";
float test = 0; currentInput = "";
//var sliderResult = MuControl.SliderEx(ctx, "main_slider", ref sliderValue, 0.0f, 100.0f, 1.0f, "F1", 0); expression = "";
MuControl.SliderEx(ctx, "test_slider", ref test, 0.0f, 100.0f, 1.0f, "F1", 0); evaluated = false;
// if ((sliderResult & ResultFlags.Change) != 0) }
// { else if (expression.Length > 0)
// Console.WriteLine($"Slider value changed to: {sliderValue}"); {
// } expression = expression[..^1];
MuControl.Text(ctx, "Other controls here..."); if (currentInput.Length > 0)
currentInput = currentInput[..^1];
if (expression.Length == 0)
display = "0";
}
return;
}
// Test TextboxRaw if (char.IsDigit(key, 0) || key == ".")
var textboxRect = MuLayoutUtil.LayoutNext(ctx); {
var textboxResult = MuControl.TextboxRaw(ctx, ref textboxValue, 12345, textboxRect, 0); if (evaluated)
if ((textboxResult & ResultFlags.Change) != 0) {
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;
} }
catch
// 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)
{ {
Console.WriteLine($"Number changed to: {sliderValue}"); display = "Error";
}
// 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}");
} }
} }
else if (evaluated && lastOperation.Length > 0)
// Right column (button grid)
{ {
MuLayoutUtil.LayoutRow(ctx, 1, new int[] { 160 }, 0); try
MuControl.Text(ctx, "4x4 Grid of Buttons with 'x' labels:");
for (int row = 0; row < 4; row++)
{ {
MuLayoutUtil.LayoutRow(ctx, 4, new int[] { 25, 25, 25, 25 }, 0); var tokens = Tokenize(lastOperation);
for (int col = 0; col < 4; col++) if (tokens.Count == 3 && tokens[1] is "+" or "-" or "*" or "/")
{ {
MicroUI.PushId(ctx, System.Text.Encoding.UTF8.GetBytes($"grid_{row}_{col}")); var op = tokens[1];
if (MuControl.ButtonEx(ctx, "x", 0, (int)Options.AlignCenter)) 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;
}
if (key is "(" or ")")
MuControl.EndWindow(ctx); {
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<string> Tokenize(string expr)
{
var tokens = new List<string>();
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<string> 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<string> 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<string> 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;
}
} }