From 7b1912579f2a129433007892a42f594a3a9fbfb7 Mon Sep 17 00:00:00 2001 From: Codex CLI Date: Tue, 26 Aug 2025 01:45:05 -0500 Subject: [PATCH] Implement migration plan: models, service, MVVM, XAML layout, icons\n\n- Add Models: HistoryItem, VariableItem, FunctionDefinitionItem, IconFont\n- Add Services: ICalculatorService, CalculatorService wrapping interpreter\n- Update MainViewModel: properties and Submit/ToggleFunctions commands\n- Rebuild Views/MainView.axaml with variables, history, input, functions panel\n- Add BoolToGridLengthConverter for panel toggle\n- Wire Material Design Icons font as Avalonia resource in App.axaml/csproj --- .../AdvancedCalculator.csproj | 4 + src/AdvancedCalculator/App.axaml | 5 + .../Converters/BoolToGridLengthConverter.cs | 27 +++++ .../Models/FunctionDefinitionItem.cs | 28 ++++++ src/AdvancedCalculator/Models/HistoryItem.cs | 8 ++ src/AdvancedCalculator/Models/IconFont.cs | 19 ++++ src/AdvancedCalculator/Models/VariableItem.cs | 15 +++ .../Services/CalculatorService.cs | 67 +++++++++++++ .../Services/ICalculatorService.cs | 17 ++++ .../ViewModels/MainViewModel.cs | 62 +++++++++++- src/AdvancedCalculator/Views/MainView.axaml | 99 +++++++++++++++++-- 11 files changed, 342 insertions(+), 9 deletions(-) create mode 100644 src/AdvancedCalculator/Converters/BoolToGridLengthConverter.cs create mode 100644 src/AdvancedCalculator/Models/FunctionDefinitionItem.cs create mode 100644 src/AdvancedCalculator/Models/HistoryItem.cs create mode 100644 src/AdvancedCalculator/Models/IconFont.cs create mode 100644 src/AdvancedCalculator/Models/VariableItem.cs create mode 100644 src/AdvancedCalculator/Services/CalculatorService.cs create mode 100644 src/AdvancedCalculator/Services/ICalculatorService.cs diff --git a/src/AdvancedCalculator/AdvancedCalculator.csproj b/src/AdvancedCalculator/AdvancedCalculator.csproj index 086d785..8375ef8 100644 --- a/src/AdvancedCalculator/AdvancedCalculator.csproj +++ b/src/AdvancedCalculator/AdvancedCalculator.csproj @@ -8,6 +8,10 @@ + + + Assets/Fonts/materialdesignicons-webfont.ttf + diff --git a/src/AdvancedCalculator/App.axaml b/src/AdvancedCalculator/App.axaml index ed5dc8d..5e686cd 100644 --- a/src/AdvancedCalculator/App.axaml +++ b/src/AdvancedCalculator/App.axaml @@ -7,4 +7,9 @@ + + + + avares://AdvancedCalculator/Assets/Fonts#Material Design Icons + diff --git a/src/AdvancedCalculator/Converters/BoolToGridLengthConverter.cs b/src/AdvancedCalculator/Converters/BoolToGridLengthConverter.cs new file mode 100644 index 0000000..d1de3fb --- /dev/null +++ b/src/AdvancedCalculator/Converters/BoolToGridLengthConverter.cs @@ -0,0 +1,27 @@ +using System; +using Avalonia.Data.Converters; +using Avalonia; + +namespace AdvancedCalculator.Converters; + +public class BoolToGridLengthConverter : IValueConverter +{ + public static readonly BoolToGridLengthConverter Instance = new(); + + public object? Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo? culture) + { + var isOpen = value is bool b && b; + // Return star when true, otherwise zero pixels + return isOpen ? new GridLength(1, GridUnitType.Star) : new GridLength(0, GridUnitType.Pixel); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo? culture) + { + if (value is GridLength gl) + { + return gl.Value > 0; + } + return false; + } +} + diff --git a/src/AdvancedCalculator/Models/FunctionDefinitionItem.cs b/src/AdvancedCalculator/Models/FunctionDefinitionItem.cs new file mode 100644 index 0000000..4cfbb1d --- /dev/null +++ b/src/AdvancedCalculator/Models/FunctionDefinitionItem.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace AdvancedCalculator.Models; + +public class FunctionDefinitionItem +{ + public string FunctionName { get; private set; } = string.Empty; + public string FunctionDescription { get; private set; } = string.Empty; + public string Icon { get; private set; } = string.Empty; + public IEnumerable> FunctionArguments { get; private set; } = new List>(); + + public static IEnumerable DefinedFunctions + { + get + { + yield return new FunctionDefinitionItem { FunctionName = "sin", Icon = IconFont.SineWave, FunctionDescription = "Returns the sine value of a given expression.", FunctionArguments = new[] { new KeyValuePair("expression", "An expression to compute.") } }; + yield return new FunctionDefinitionItem { FunctionName = "cos", Icon = IconFont.CosineWave, FunctionDescription = "Returns the cosine value of a given expression.", FunctionArguments = new[] { new KeyValuePair("expression", "An expression to compute.") } }; + yield return new FunctionDefinitionItem { FunctionName = "tan", Icon = IconFont.MathTan, FunctionDescription = "Returns the tangent value of a given expression.", FunctionArguments = new[] { new KeyValuePair("expression", "An expression to compute.") } }; + yield return new FunctionDefinitionItem { FunctionName = "round", Icon = IconFont.RoundedCorner, FunctionDescription = "Rounds an expression to the nearest whole number.", FunctionArguments = new[] { new KeyValuePair("expression", "An expression to compute.") } }; + yield return new FunctionDefinitionItem { FunctionName = "sqrt", Icon = IconFont.SquareRoot, FunctionDescription = "Returns the square root of a given expression.", FunctionArguments = new[] { new KeyValuePair("expression", "An expression to compute.") } }; + yield return new FunctionDefinitionItem { FunctionName = "abs", Icon = IconFont.PlusCircle, FunctionDescription = "Returns the absolute value of a given expression.", FunctionArguments = new[] { new KeyValuePair("expression", "An expression to compute.") } }; + yield return new FunctionDefinitionItem { FunctionName = "exp", Icon = IconFont.AlphaECircle, FunctionDescription = "Returns the constant e to a given power.", FunctionArguments = new[] { new KeyValuePair("power", "An expression to compute.") } }; + yield return new FunctionDefinitionItem { FunctionName = "log", Icon = IconFont.MathLog, FunctionDescription = "Returns the log of the first expression to the base of the second expression.", FunctionArguments = new[] { new KeyValuePair("value", "An expression to compute."), new KeyValuePair("base", "An expression to compute.") } }; + yield return new FunctionDefinitionItem { FunctionName = "precision", Icon = IconFont.DecimalIncrease, FunctionDescription = "Returns the value of expression1 to a given precision.", FunctionArguments = new[] { new KeyValuePair("value", "An expression to compute."), new KeyValuePair("precision", "An expression to compute.") } }; + } + } +} + diff --git a/src/AdvancedCalculator/Models/HistoryItem.cs b/src/AdvancedCalculator/Models/HistoryItem.cs new file mode 100644 index 0000000..1f6c0ed --- /dev/null +++ b/src/AdvancedCalculator/Models/HistoryItem.cs @@ -0,0 +1,8 @@ +namespace AdvancedCalculator.Models; + +public class HistoryItem +{ + public string Input { get; set; } = string.Empty; + public string Output { get; set; } = string.Empty; +} + diff --git a/src/AdvancedCalculator/Models/IconFont.cs b/src/AdvancedCalculator/Models/IconFont.cs new file mode 100644 index 0000000..07ffb75 --- /dev/null +++ b/src/AdvancedCalculator/Models/IconFont.cs @@ -0,0 +1,19 @@ +namespace AdvancedCalculator.Models; + +// Slimmed icon font mapping for required glyphs only +public static class IconFont +{ + public const string ArrowRightDropCircle = "\U000f0059"; + public const string DecimalIncrease = "\U000f01b5"; + public const string Function = "\U000f0295"; + public const string PlusCircle = "\U000f0417"; + public const string RoundedCorner = "\U000f0607"; + public const string SquareRoot = "\U000f0784"; + public const string SineWave = "\U000f095b"; + public const string Variable = "\U000f0ae7"; + public const string AlphaECircle = "\U000f0bf8"; + public const string MathTan = "\U000f0c98"; + public const string MathLog = "\U000f1085"; + public const string CosineWave = "\U000f1479"; +} + diff --git a/src/AdvancedCalculator/Models/VariableItem.cs b/src/AdvancedCalculator/Models/VariableItem.cs new file mode 100644 index 0000000..22a900c --- /dev/null +++ b/src/AdvancedCalculator/Models/VariableItem.cs @@ -0,0 +1,15 @@ +namespace AdvancedCalculator.Models; + +public class VariableItem +{ + public string VariableName { get; set; } = string.Empty; + public string Icon { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + + // True when this variable is defined by an equation/expression + public bool IsExpression { get; set; } + + // Computed value for an expression variable (pretty-printed) + public string? ExpressionComputation { get; set; } +} + diff --git a/src/AdvancedCalculator/Services/CalculatorService.cs b/src/AdvancedCalculator/Services/CalculatorService.cs new file mode 100644 index 0000000..aaf47c2 --- /dev/null +++ b/src/AdvancedCalculator/Services/CalculatorService.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AdvancedCalculator.Models; + +// Intentionally keep the csmic/CSMic dependency surface in this file only. +// This allows easy adaptation if the interpreter API changes. +namespace AdvancedCalculator.Services; + +public class CalculatorService : ICalculatorService +{ + private readonly object _lock = new(); + + // We attempt to use the legacy namespace to maximize compatibility. + // If using the newer CSMic.StandardLibrary with different namespaces, + // update the using directives and type names here. + private readonly csmic.InputInterpreter _interpreter = new(); + + public Task InterpretAsync(string input) + { + return Task.Run(() => + { + lock (_lock) + { + _interpreter.Interpret(input); + var output = _interpreter.Output ?? string.Empty; + + // Build variables list + var variables = new List(); + foreach (var kvp in _interpreter.Variables) + { + var name = kvp.Key; + var variable = kvp.Value; + var valueString = variable?.Value?.ToString() ?? string.Empty; + + var isExpression = variable?.Type == csmic.VariableType.Equation; + var item = new VariableItem + { + VariableName = name, + Value = valueString, + IsExpression = isExpression, + ExpressionComputation = null, + Icon = IconFont.Variable + }; + + if (isExpression && !string.IsNullOrWhiteSpace(valueString)) + { + // Compute the expression-based variable's current value + _interpreter.Interpret(valueString); + item.ExpressionComputation = _interpreter.Output ?? string.Empty; + item.Icon = IconFont.Function; + } + + variables.Add(item); + } + + return new InterpretResult + { + Output = output, + Variables = variables + }; + } + }); + } +} + diff --git a/src/AdvancedCalculator/Services/ICalculatorService.cs b/src/AdvancedCalculator/Services/ICalculatorService.cs new file mode 100644 index 0000000..bd0c0ec --- /dev/null +++ b/src/AdvancedCalculator/Services/ICalculatorService.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using AdvancedCalculator.Models; + +namespace AdvancedCalculator.Services; + +public interface ICalculatorService +{ + Task InterpretAsync(string input); +} + +public class InterpretResult +{ + public string Output { get; set; } = string.Empty; + public IReadOnlyList Variables { get; set; } = new List(); +} + diff --git a/src/AdvancedCalculator/ViewModels/MainViewModel.cs b/src/AdvancedCalculator/ViewModels/MainViewModel.cs index aba0c84..1611402 100644 --- a/src/AdvancedCalculator/ViewModels/MainViewModel.cs +++ b/src/AdvancedCalculator/ViewModels/MainViewModel.cs @@ -1,6 +1,64 @@ -namespace AdvancedCalculator.ViewModels; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using AdvancedCalculator.Models; +using AdvancedCalculator.Services; + +namespace AdvancedCalculator.ViewModels; public partial class MainViewModel : ViewModelBase { - public string Greeting => "Welcome to Avalonia!"; + private readonly ICalculatorService _calculatorService; + + public MainViewModel() + : this(new CalculatorService()) + { + } + + public MainViewModel(ICalculatorService calculatorService) + { + _calculatorService = calculatorService; + History = new ObservableCollection(); + Variables = new ObservableCollection(); + } + + [ObservableProperty] + private string _inputText = string.Empty; + + [ObservableProperty] + private bool _isFunctionsPanelOpen; + + [ObservableProperty] + private int _selectedHistoryIndex = -1; + + public ObservableCollection History { get; } + public ObservableCollection Variables { get; } + + [RelayCommand] + private void ToggleFunctions() + { + IsFunctionsPanelOpen = !IsFunctionsPanelOpen; + } + + [RelayCommand(AllowConcurrentExecutions = false)] + private async Task Submit() + { + var text = InputText?.Trim(); + if (string.IsNullOrEmpty(text)) + return; + + var result = await _calculatorService.InterpretAsync(text); + + History.Add(new HistoryItem { Input = text, Output = result.Output }); + + Variables.Clear(); + foreach (var v in result.Variables) + { + Variables.Add(v); + } + + InputText = string.Empty; + SelectedHistoryIndex = History.Count - 1; + } } diff --git a/src/AdvancedCalculator/Views/MainView.axaml b/src/AdvancedCalculator/Views/MainView.axaml index eef679f..71ffac9 100644 --- a/src/AdvancedCalculator/Views/MainView.axaml +++ b/src/AdvancedCalculator/Views/MainView.axaml @@ -3,14 +3,99 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:AdvancedCalculator.ViewModels" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + xmlns:m="clr-namespace:AdvancedCalculator.Models" + xmlns:conv="clr-namespace:AdvancedCalculator.Converters" + mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="600" x:Class="AdvancedCalculator.Views.MainView" x:DataType="vm:MainViewModel"> - - - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +