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

This commit is contained in:
Codex CLI 2025-08-26 01:45:05 -05:00
commit 7b1912579f
11 changed files with 342 additions and 9 deletions

View file

@ -8,6 +8,10 @@
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<!-- Link the icon font from the legacy project into this project as an Avalonia resource -->
<AvaloniaResource Include="..\src.4.7\materialdesignicons-webfont.ttf">
<Link>Assets/Fonts/materialdesignicons-webfont.ttf</Link>
</AvaloniaResource>
</ItemGroup>
<ItemGroup>

View file

@ -7,4 +7,9 @@
<Application.Styles>
<FluentTheme />
</Application.Styles>
<Application.Resources>
<!-- Material Design Icons font family embedded as an Avalonia resource -->
<FontFamily x:Key="MDI">avares://AdvancedCalculator/Assets/Fonts#Material Design Icons</FontFamily>
</Application.Resources>
</Application>

View file

@ -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;
}
}

View file

@ -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<KeyValuePair<string, string>> FunctionArguments { get; private set; } = new List<KeyValuePair<string, string>>();
public static IEnumerable<FunctionDefinitionItem> DefinedFunctions
{
get
{
yield return new FunctionDefinitionItem { FunctionName = "sin", Icon = IconFont.SineWave, FunctionDescription = "Returns the sine value of a given expression.", FunctionArguments = new[] { new KeyValuePair<string, string>("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<string, string>("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<string, string>("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<string, string>("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<string, string>("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<string, string>("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<string, string>("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<string, string>("value", "An expression to compute."), new KeyValuePair<string, string>("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<string, string>("value", "An expression to compute."), new KeyValuePair<string, string>("precision", "An expression to compute.") } };
}
}
}

View file

@ -0,0 +1,8 @@
namespace AdvancedCalculator.Models;
public class HistoryItem
{
public string Input { get; set; } = string.Empty;
public string Output { get; set; } = string.Empty;
}

View file

@ -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";
}

View file

@ -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; }
}

View file

@ -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<InterpretResult> InterpretAsync(string input)
{
return Task.Run(() =>
{
lock (_lock)
{
_interpreter.Interpret(input);
var output = _interpreter.Output ?? string.Empty;
// Build variables list
var variables = new List<VariableItem>();
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
};
}
});
}
}

View file

@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using AdvancedCalculator.Models;
namespace AdvancedCalculator.Services;
public interface ICalculatorService
{
Task<InterpretResult> InterpretAsync(string input);
}
public class InterpretResult
{
public string Output { get; set; } = string.Empty;
public IReadOnlyList<VariableItem> Variables { get; set; } = new List<VariableItem>();
}

View file

@ -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<HistoryItem>();
Variables = new ObservableCollection<VariableItem>();
}
[ObservableProperty]
private string _inputText = string.Empty;
[ObservableProperty]
private bool _isFunctionsPanelOpen;
[ObservableProperty]
private int _selectedHistoryIndex = -1;
public ObservableCollection<HistoryItem> History { get; }
public ObservableCollection<VariableItem> 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;
}
}

View file

@ -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">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainViewModel />
</Design.DataContext>
<UserControl.Resources>
<conv:BoolToGridLengthConverter x:Key="BoolToGridLengthConverter" />
</UserControl.Resources>
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Grid ColumnDefinitions="*,3*">
<!-- Left column: Variables + Functions -->
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="{Binding IsFunctionsPanelOpen, Converter={StaticResource BoolToGridLengthConverter}}" />
</Grid.RowDefinitions>
<!-- Variables list -->
<ListBox Grid.Row="0" ItemsSource="{Binding Variables}" SelectedIndex="-1">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="m:VariableItem">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="4,2">
<TextBlock Grid.Column="0" FontFamily="{StaticResource MDI}"
FontSize="22" Text="{Binding Icon}" VerticalAlignment="Center" Margin="0,0,8,0" />
<TextBlock Grid.Column="1" Text="{Binding VariableName}" FontWeight="Bold" VerticalAlignment="Center" />
<StackPanel Grid.Column="2" Spacing="2">
<TextBlock Text="{Binding Value}" />
<TextBlock IsVisible="{Binding IsExpression}" Text="{Binding ExpressionComputation}" FontStyle="Italic" />
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Function definitions -->
<ListBox Grid.Row="1" BorderThickness="0"
ItemsSource="{x:Static m:FunctionDefinitionItem.DefinedFunctions}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="m:FunctionDefinitionItem">
<StackPanel Spacing="2" Margin="4,6">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock FontFamily="{StaticResource MDI}" Text="{Binding Icon}" />
<TextBlock Text="{Binding FunctionName}" FontWeight="Bold" />
</StackPanel>
<TextBlock Text="{Binding FunctionDescription}" FontStyle="Italic" TextWrapping="Wrap" />
<ItemsControl ItemsSource="{Binding FunctionArguments}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Key}" />
<TextBlock Text="{Binding Value}" FontStyle="Italic" Margin="8,0,0,0" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<!-- GridSplitter between columns -->
<GridSplitter Grid.Column="0" HorizontalAlignment="Right" Width="5" Background="Transparent" />
<!-- Right column: History + Input -->
<Grid Grid.Column="1" RowDefinitions="*,Auto">
<!-- History -->
<ListBox Grid.Row="0" ItemsSource="{Binding History}" SelectedIndex="{Binding SelectedHistoryIndex}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="m:HistoryItem">
<Grid ColumnDefinitions="Auto,*" Margin="4,2">
<TextBlock FontFamily="{StaticResource MDI}" Text="{x:Static m:IconFont.ArrowRightDropCircle}"
FontSize="22" VerticalAlignment="Center" Margin="0,0,8,0" />
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding Input}" />
<TextBlock Text="{Binding Output}" FontWeight="Bold" />
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Input Row -->
<Grid Grid.Row="1" ColumnDefinitions="Auto,*" Margin="4">
<Button Command="{Binding ToggleFunctionsCommand}" Margin="0,0,6,0">
<TextBlock FontFamily="{StaticResource MDI}" Text="{x:Static m:IconFont.Function}" />
</Button>
<TextBox Grid.Column="1" Text="{Binding InputText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<InputElement.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding SubmitCommand}" />
</InputElement.KeyBindings>
</TextBox>
</Grid>
</Grid>
</Grid>
</UserControl>