diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index b68becf..0000000 --- a/.editorconfig +++ /dev/null @@ -1,174 +0,0 @@ -root = true -# editorconfig.org - -# top-most EditorConfig file - -# Default settings: -# A newline ending every file -# Use 4 spaces as indentation -[*] -insert_final_newline = false -indent_style = space -indent_size = 4 - -# ReSharper properties -resharper_place_simple_initializer_on_single_line = true -resharper_wrap_before_arrow_with_expressions = false -resharper_xml_wrap_lines = false -dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -end_of_line = crlf -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_object_initializer = true:suggestion - -# C# files -[*.cs] -# New line preferences -csharp_new_line_before_open_brace = none -csharp_new_line_before_else = true -csharp_new_line_before_catch = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = false -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_between_query_expression_clauses = true -# trim_trailing_whitespace = true - -# Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents = true -csharp_indent_switch_labels = true -csharp_indent_labels = one_less_than_current - -# avoid this. unless absolutely necessary -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_property = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_event = false:suggestion - -# prefer var -csharp_style_var_for_built_in_types = true -csharp_style_var_when_type_is_apparent = true -csharp_style_var_elsewhere = true:suggestion - -# use language keywords instead of BCL types -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion - -# name all constant fields using PascalCase -dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style - -dotnet_naming_symbols.constant_fields.applicable_kinds = field -dotnet_naming_symbols.constant_fields.required_modifiers = const - -dotnet_naming_style.pascal_case_style.capitalization = pascal_case - -# internal and private fields should be _camelCase -dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion -dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields -dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style - -dotnet_naming_symbols.private_internal_fields.applicable_kinds = field -dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal - -dotnet_naming_style.camel_case_underscore_style.required_prefix = _ -dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case - -# use accessibility modifiers -dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion - -# Code style defaults -dotnet_sort_system_directives_first = true -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = false - -# Expression-level preferences -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion - -# Expression-bodied members -csharp_style_expression_bodied_methods = false:none -csharp_style_expression_bodied_constructors = false:none -csharp_style_expression_bodied_operators = false:none -csharp_style_expression_bodied_properties = true:none -csharp_style_expression_bodied_indexers = true:none -csharp_style_expression_bodied_accessors = true:none - -# Pattern matching -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion - -# Null checking preferences -csharp_style_throw_expression = true:suggestion -csharp_style_conditional_delegate_call = true:suggestion - -# Space preferences -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_between_square_brackets = false -space_within_single_line_array_initializer_braces = true -csharp_using_directive_placement = outside_namespace:silent -csharp_prefer_simple_using_statement = true:suggestion -csharp_prefer_braces = when_multiline:silent -csharp_style_namespace_declarations = file_scoped:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = false:silent - -# Xaml files -[*.{xaml,axaml}] -indent_size = 2 - -# Xml project files -[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] -indent_size = 2 - -# Xml build files -[*.builds] -indent_size = 2 - -# Xml files -[*.{xml,stylecop,resx,ruleset}] -indent_size = 2 - -# Xml config files -[*.{props,targets,config,nuspec}] -indent_size = 2 - -[*.json] -indent_size = 2 - -# Shell scripts -[*.sh] -end_of_line = lf - -[*.{cmd, bat}] -end_of_line = crlf diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/prbuild.yml b/.github/workflows/prbuild.yml deleted file mode 100644 index 59e291f..0000000 --- a/.github/workflows/prbuild.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: .NET Build on PR - -on: - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: windows-latest - - steps: - - uses: actions/checkout@v2 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 10 - - name: Setup Windows SDK 10.0.18362.0 - uses: GuillaumeFalourd/setup-windows10-sdk-action@v2 - with: - sdk-version: 18362 - - name: Build and Pack - run: | - dotnet build Material.Icons -c Release /p:Platform="Any CPU" - dotnet build Material.Icons.WPF -c Release /p:Platform="Any CPU" - dotnet build Material.Icons.Avalonia -c Release /p:Platform="Any CPU" - dotnet build Material.Icons.WinUI3 -c Release /p:Platform="Any CPU" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 6168f36..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: .NET Publish - -on: - push: - branches: [ master ] - -jobs: - build: - - runs-on: windows-latest - - steps: - - uses: actions/checkout@v2 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 10 - - name: Setup Windows SDK 10.0.18362.0 - uses: GuillaumeFalourd/setup-windows10-sdk-action@v2 - with: - sdk-version: 18362 - - name: Build and Pack - run: | - dotnet build Material.Icons -c Release /p:Platform="Any CPU" - dotnet build Material.Icons.WPF -c Release /p:Platform="Any CPU" - dotnet build Material.Icons.Avalonia -c Release /p:Platform="Any CPU" - dotnet build Material.Icons.WinUI3 -c Release /p:Platform="Any CPU" - - name: Publish to Nuget - run: | - dotnet nuget push **/*.nupkg --api-key ${{secrets.NUGET_KEY}} --source https://api.nuget.org/v3/index.json --skip-duplicate - - name: Publish to GitHub Packages - run: | - dotnet nuget push **/*.nupkg --api-key ${{secrets.GITHUB_TOKEN}} --source https://nuget.pkg.github.com/SKProCH/index.json --skip-duplicate diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml deleted file mode 100644 index 4df8687..0000000 --- a/.github/workflows/update.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Icons collection update - -on: - schedule: - - cron: '0 0,12 * * *' - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - with: - persist-credentials: false - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 10 - - name: Cache .nuke/temp, ~/.nuget/packages - uses: actions/cache@v4 - with: - path: | - .nuke/temp - ~/.nuget/packages - key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj') }} - - name: Run generator project - run: ./build.sh GhUpdateIconsWorkflow - - name: Push changes (if any) - uses: actions-js/push@master - with: - github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - message: 'Icons collection update ✨' - branch: 'master' diff --git a/.gitignore b/.gitignore index 60ad021..6d902c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,7 @@ -bin/ -obj/ -/packages/ -riderModule.iml -/_ReSharper.Caches/ -*.DotSettings - -# Created by https://www.toptal.com/developers/gitignore/api/csharp -# Edit at https://www.toptal.com/developers/gitignore?templates=csharp - -### Csharp ### ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore # User-specific files *.rsuser @@ -34,6 +23,7 @@ mono_crash.* [Rr]eleases/ x64/ x86/ +[Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ @@ -72,6 +62,9 @@ project.lock.json project.fragment.lock.json artifacts/ +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + # StyleCop StyleCopReport.xml @@ -81,7 +74,6 @@ StyleCopReport.xml *_h.h *.ilk *.meta -!*.Meta/ *.obj *.iobj *.pch @@ -98,6 +90,7 @@ StyleCopReport.xml *.tmp_proj *_wpftmp.csproj *.log +*.tlog *.vspscc *.vssscc .builds @@ -150,7 +143,9 @@ _TeamCity* !.axoCover/settings.json # Coverlet is a free, cross platform Code Coverage Tool -coverage*[.json, .xml, .info] +coverage*.json +coverage*.xml +coverage*.info # Visual Studio code coverage results *.coverage @@ -189,7 +184,7 @@ publish/ *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted -*.pubxml +#*.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to @@ -299,6 +294,17 @@ node_modules/ # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -355,6 +361,9 @@ ASALocalRun/ # Local History for Visual Studio .localhistory/ +# Visual Studio History (VSHistory) files +.vshistory/ + # BeatPulse healthcheck temp database healthchecksdb @@ -364,6 +373,36 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ -# End of https://www.toptal.com/developers/gitignore/api/csharp -.idea -*.DotSettings +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +/WateryTart.Platform.Linux/out +/WateryTart.Platform.Linux/staging_folder +WateryTart.Platform.Linux/WateryTart_Linux_amd64.deb +WateryTart.Platform.Linux/WateryTart_Linux_arm64.deb +*.deb +*.parcel +/.idea +src/WateryTart.Platform.Linux/staging_folder/ +src/WateryTart.Platform.Linux/out/ +output/ diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index b6aae97..4a7b90c 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -24,9 +24,8 @@ "ExecutableTarget": { "type": "string", "enum": [ + "Clean", "Compile", - "GhBumpVersionIfNeeded", - "GhUpdateIconsWorkflow", "Restore", "UpdateIcons" ] @@ -110,10 +109,6 @@ "Release" ] }, - "SkipWpfCompilation": { - "type": "boolean", - "description": "Should we skip compilation of WPF projects. In non windows systems this is enabled by default" - }, "Solution": { "type": "string", "description": "Path to a solution file that is automatically loaded" diff --git a/.nuke/parameters.json b/.nuke/parameters.json index 049f8bf..b7c89f2 100644 --- a/.nuke/parameters.json +++ b/.nuke/parameters.json @@ -1,4 +1,4 @@ { - "$schema": "./build.schema.json", - "Solution": "Material.Icons.sln" -} \ No newline at end of file + "$schema": "build.schema.json", + "Solution": "IconPackGenerators.slnx" +} diff --git a/AvaloniaApplication1/App.axaml b/AvaloniaApplication1/App.axaml new file mode 100644 index 0000000..716b997 --- /dev/null +++ b/AvaloniaApplication1/App.axaml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/AvaloniaApplication1/App.axaml.cs b/AvaloniaApplication1/App.axaml.cs new file mode 100644 index 0000000..7149fe1 --- /dev/null +++ b/AvaloniaApplication1/App.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace AvaloniaApplication1 +{ + public partial class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } + } +} \ No newline at end of file diff --git a/AvaloniaApplication1/AvaloniaDemo.csproj b/AvaloniaApplication1/AvaloniaDemo.csproj new file mode 100644 index 0000000..5daab8d --- /dev/null +++ b/AvaloniaApplication1/AvaloniaDemo.csproj @@ -0,0 +1,29 @@ + + + WinExe + net10.0 + enable + app.manifest + true + + + + + + + + + + None + All + + + + + + + + + + + \ No newline at end of file diff --git a/AvaloniaApplication1/MainWindow.axaml b/AvaloniaApplication1/MainWindow.axaml new file mode 100644 index 0000000..a0a5ccc --- /dev/null +++ b/AvaloniaApplication1/MainWindow.axaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + diff --git a/AvaloniaApplication1/MainWindow.axaml.cs b/AvaloniaApplication1/MainWindow.axaml.cs new file mode 100644 index 0000000..c018391 --- /dev/null +++ b/AvaloniaApplication1/MainWindow.axaml.cs @@ -0,0 +1,40 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Threading; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace AvaloniaApplication1 +{ + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + Loaded += MainWindow_Loaded; + } + + private void MainWindow_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + LoadIconsAsync(); + } + + private async Task LoadIconsAsync() + { + + //var allIcons = Enum.GetValues(typeof(FontAwesome.Icons.FontAwesomeIconKind)).Cast(); + //foreach (var icon in allIcons) + //{ + // // Ensure UI update happens on the UI thread + // await Dispatcher.UIThread.InvokeAsync(() => + // { + // faIcon.Kind = icon; + // }); + + // // Wait for 1 second + // await Task.Delay(200); + //} + } + } +} \ No newline at end of file diff --git a/Material.Icons.Avalonia.Demo/Program.cs b/AvaloniaApplication1/Program.cs similarity index 64% rename from Material.Icons.Avalonia.Demo/Program.cs rename to AvaloniaApplication1/Program.cs index 9532944..e2d6008 100644 --- a/Material.Icons.Avalonia.Demo/Program.cs +++ b/AvaloniaApplication1/Program.cs @@ -1,19 +1,22 @@ using Avalonia; -using Avalonia.ReactiveUI; +using System; -namespace Material.Icons.Avalonia.Demo { - class Program { +namespace AvaloniaApplication1 +{ + internal class Program + { // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. + [STAThread] public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + .StartWithClassicDesktopLifetime(args); // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace() - .UseReactiveUI(); + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); } -} \ No newline at end of file +} diff --git a/AvaloniaApplication1/app.manifest b/AvaloniaApplication1/app.manifest new file mode 100644 index 0000000..3943700 --- /dev/null +++ b/AvaloniaApplication1/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/IconGeneratorTemplate.Avalonia/DummyIcon.axaml b/IconGeneratorTemplate.Avalonia/DummyIcon.axaml new file mode 100644 index 0000000..381b192 --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/DummyIcon.axaml @@ -0,0 +1,139 @@ + + 0:0:2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IconGeneratorTemplate.Avalonia/DummyIcon.axaml.cs b/IconGeneratorTemplate.Avalonia/DummyIcon.axaml.cs new file mode 100644 index 0000000..9cef877 --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/DummyIcon.axaml.cs @@ -0,0 +1,137 @@ +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Media; +using IconGenerators; + +namespace Dummy.Icons.Avalonia; + +public class DummyIcon : TemplatedControl, IImage { + #region Properties + + public static readonly StyledProperty KindProperty + = AvaloniaProperty.Register(nameof(Kind)); + + /// + /// Gets or sets the icon to display. + /// + public DummyIconKind? Kind { + get => GetValue(KindProperty); + set => SetValue(KindProperty, value); + } + + public static readonly StyledProperty IconSizeProperty = + AvaloniaProperty.Register(nameof(IconSize), defaultValue: double.NaN); + + /// + /// Gets or sets the uniform size of the icon. + /// + public double IconSize { + get => GetValue(IconSizeProperty); + set => SetValue(IconSizeProperty, value); + } + + public static readonly StyledProperty AnimationProperty + = AvaloniaProperty.Register(nameof(Animation)); + + /// + /// Gets or sets the icon animation to play. + /// + public DummyIconAnimation Animation { + get => GetValue(AnimationProperty); + set => SetValue(AnimationProperty, value); + } + + public static readonly DirectProperty DrawingProperty = + AvaloniaProperty.RegisterDirect( + nameof(Drawing), + o => o.Drawing); + + /// + /// Gets the of the icon. + /// + public GeometryDrawing Drawing { get; } = new(); + + // Default size for FontAwesome Icons + private static readonly Rect DefaultIconBounds = new(0, 0, 24, 24); + + #endregion + + #region Constructor + + static DummyIcon() { + DummyIconsUtils.InitializeGeometryParser(); + } + + public DummyIcon() { + Drawing.Brush = Foreground; + } + + #endregion + + #region Overrides + + /// + protected override void OnLoaded(RoutedEventArgs e) { + if (Drawing.Geometry is null) + SetGeometry(); + base.OnLoaded(e); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { + base.OnPropertyChanged(e); + + if (e.Property == KindProperty) { + SetGeometry(); + } + else if (e.Property == ForegroundProperty) { + Drawing.Brush = Foreground; + } + } + + #endregion + + #region Methods + + /// + /// Sets the geometry for the drawing based on the specified FontAwesome icon kind. + /// + /// This method updates the Geometry property by parsing the + /// geometry data associated with the current value. + /// + private void SetGeometry() { + Drawing.Geometry = Kind is null ? null : DummyIconDataProvider.Get(Kind.Value); + } + + #endregion + + #region IImage Implementation + + /// + Size IImage.Size => DefaultIconBounds.Size; + + /// + void IImage.Draw(DrawingContext context, Rect sourceRect, Rect destRect) { + if (Drawing.Geometry is null) + SetGeometry(); + + var bounds = DefaultIconBounds; + var scale = Matrix.CreateScale( + destRect.Width / sourceRect.Width, + destRect.Height / sourceRect.Height + ); + var translate = Matrix.CreateTranslation( + -sourceRect.X + destRect.X - bounds.X, + -sourceRect.Y + destRect.Y - bounds.Y + ); + + using (context.PushClip(destRect)) + using (context.PushTransform(translate * scale)) { + Drawing.Draw(context); + } + } + + #endregion + +} diff --git a/IconGeneratorTemplate.Avalonia/DummyIconAnimation.cs b/IconGeneratorTemplate.Avalonia/DummyIconAnimation.cs new file mode 100644 index 0000000..2d6a4b0 --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/DummyIconAnimation.cs @@ -0,0 +1,12 @@ +namespace Dummy.Icons.Avalonia; + +public enum DummyIconAnimation +{ + None, + Spin, + SpinCcw, + Pulse, + PulseCcw, + FadeOutIn, + FadeInOut +} diff --git a/IconGeneratorTemplate.Avalonia/DummyIconDataProvider.cs b/IconGeneratorTemplate.Avalonia/DummyIconDataProvider.cs new file mode 100644 index 0000000..893cb15 --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/DummyIconDataProvider.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Text; +using IconGenerators.Avalonia; + +namespace IconGenerators; + + +/// +/// Provides access to the Material Design icons data and its geometry. +/// +public partial class DummyIconDataProvider : IIconProvider +{ + private static DummyIconDataProvider _instance = new(); + private static Func? _parser; + + /// + /// Gets the cache singleton for the icons. The cache is used to store the parsed icons to avoid parsing them multiple times. + /// + private static Dictionary? _cache = new(); + + /// + /// Gets the cache for the icons. The cache is used to store the parsed icons to avoid parsing them multiple times. + /// + public static IReadOnlyDictionary? Cache => _cache; + + /// + /// Gets or sets the singleton instance of this provider + /// + public static DummyIconDataProvider Instance + { + get => _instance; + set + { + _instance = value ?? throw new ArgumentNullException(nameof(value)); + ClearCache(); + } + } + + /// + /// Disables the cache for the icons. + /// + public static void DisableCache() + { + _cache = null; + } + + /// + /// Clears the cache for the icons. + /// + public static void ClearCache() + { + _cache?.Clear(); + } + + /// + /// Initializes the geometry parser with the specified parsing function. + /// + /// This method sets the parser function to be used for geometry parsing operations. If the + /// parser has already been initialized, subsequent calls to this method will have no effect. + /// A function that takes a string input and returns an object representing the parsed geometry. This parameter + /// cannot be null. + public static void InitializeGeometryParser(Func parser) => _parser ??= parser; + + /// + /// Gets the geometry for the specified icon using the + /// + /// The icon kind + /// SVG path for target icon kind + public static T Get(DummyIconKind kind) where T : class + { + if (_cache?.TryGetValue(kind, out var value) is true) + { + return value as T ?? throw new InvalidOperationException( + "Invalid type for icon kind. Check that you are requesting the correct geometry type."); + } + + if (_parser is null) + { + throw new InvalidOperationException( + "Geometry parser not initialized. Call InitializeGeometryParser first."); + } + + var result = _parser(GetData(kind)) as T ?? throw new InvalidOperationException( + "Parser returns a wrong type. Check that you are requesting the correct geometry type."); + + if (_cache != null) + _cache[kind] = result; + + return result; + } + + /// + /// Gets the data for the specified icon using the + /// + /// The icon kind + /// SVG path for target icon kind + public static string GetData(DummyIconKind kind) => Instance.ProvideData(kind); + + /// + /// Provides the data for the specified icon kind + /// + /// The icon kind + /// SVG path for target icon kind + public virtual partial string ProvideData(DummyIconKind kind); + + public string ProvideData(string kindstring) + { + // Using Enum.Parse (throws if invalid) + DummyIconKind kind = (DummyIconKind)Enum.Parse(typeof(DummyIconKind), kindstring); + return ProvideData(kind); + } +} diff --git a/IconGeneratorTemplate.Avalonia/DummyIconExt.cs b/IconGeneratorTemplate.Avalonia/DummyIconExt.cs new file mode 100644 index 0000000..e423dd5 --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/DummyIconExt.cs @@ -0,0 +1,125 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Layout; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using IconGenerators; + +namespace Dummy.Icons.Avalonia; +public class DummyIconExt : MarkupExtension { + public DummyIconExt() { } + public DummyIconExt(DummyIconKind kind) { + Kind = kind; + } + + public DummyIconExt(DummyIconKind kind, DummyIconAnimation animation) { + Kind = kind; + Animation = animation; + } + + public DummyIconExt(DummyIconKind kind, double iconSize, DummyIconAnimation animation = DummyIconAnimation.None) { + Kind = kind; + IconSize = iconSize; + Animation = animation; + } + + /// + /// Gets or sets the icon to display. Provides IntelliSense autocomplete. + /// + [ConstructorArgument("kind")] + public DummyIconKind Kind { get; set; } + + /// + /// Gets or sets a binding for the icon kind. Use this when data binding is required. + /// + [ConstructorArgument("kindBinding")] + public IBinding? KindBinding { get; set; } + + /// + /// Gets or sets the size of the icon to display.
+ /// Can be a double or a binding. + ///
+ [ConstructorArgument("iconSize")] + public object? IconSize { get; set; } + + /// + /// Gets or sets the icon foreground brush. + /// + [ConstructorArgument("iconForeground")] + public IBrush? IconForeground { get; set; } + + /// + /// Gets or sets a binding for the icon foreground. Use this when data binding is required. + /// + [ConstructorArgument("iconForegroundBinding")] + public IBinding? IconForegroundBinding { get; set; } + + /// + /// Gets or sets the animation to play. Provides IntelliSense autocomplete. + /// + [ConstructorArgument("animation")] + public DummyIconAnimation Animation { get; set; } + + /// + /// Gets or sets a binding for the animation. Use this when data binding is required. + /// + [ConstructorArgument("animationBinding")] + public IBinding? AnimationBinding { get; set; } + + /// + /// Gets or sets the vertical alignment of the content. + /// + [ConstructorArgument("verticalAlignment")] + public VerticalAlignment? VerticalAlignment { get; set; } + + /// + /// Gets or sets the horizontal alignment of the content. + /// + [ConstructorArgument("horizontalAlignment")] + public HorizontalAlignment? HorizontalAlignment { get; set; } + + /// + /// Gets or sets the class names to apply to the element. + /// + [ConstructorArgument("classes")] + public string? Classes { get; set; } + + public override object ProvideValue(IServiceProvider serviceProvider) { + var result = new DummyIcon(); + + // Kind: binding takes precedence + if (KindBinding is not null) result.Bind(DummyIcon.KindProperty, KindBinding); + else result.Kind = Kind; + + // Animation: binding takes precedence + if (AnimationBinding is not null) result.Bind(DummyIcon.AnimationProperty, AnimationBinding); + else result.Animation = Animation; + + if (IconSize is not null) { + switch (IconSize) { + case IBinding binding: + result.Bind(DummyIcon.IconSizeProperty, binding); + break; + case IConvertible conv: + result.IconSize = conv.ToDouble(System.Globalization.CultureInfo.InvariantCulture); + break; + default: + throw new InvalidOperationException($"IconSize must be of type IBinding or IConvertible. Actual type: {IconSize.GetType().FullName}"); + } + } + + // IconForeground: binding takes precedence + if (IconForegroundBinding is not null) result.Bind(TemplatedControl.ForegroundProperty, IconForegroundBinding); + else if (IconForeground is not null) result.Foreground = IconForeground; + + if (VerticalAlignment is not null) result.VerticalAlignment = VerticalAlignment.Value; + if (HorizontalAlignment is not null) result.HorizontalAlignment = HorizontalAlignment.Value; + + if (!string.IsNullOrWhiteSpace(Classes)) { + result.Classes.AddRange(global::Avalonia.Controls.Classes.Parse(Classes!)); + } + + return result; + } +} diff --git a/IconGeneratorTemplate.Avalonia/DummyIconStyles.axaml b/IconGeneratorTemplate.Avalonia/DummyIconStyles.axaml new file mode 100644 index 0000000..2da223b --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/DummyIconStyles.axaml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/IconGeneratorTemplate.Avalonia/DummyIconText.axaml b/IconGeneratorTemplate.Avalonia/DummyIconText.axaml new file mode 100644 index 0000000..bb0706b --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/DummyIconText.axaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/IconGeneratorTemplate.Avalonia/DummyIconText.axaml.cs b/IconGeneratorTemplate.Avalonia/DummyIconText.axaml.cs new file mode 100644 index 0000000..dc238c6 --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/DummyIconText.axaml.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; + +namespace Dummy.Icons.Avalonia; + +[TemplatePart("PART_Icon", typeof(DummyIcon))] +[TemplatePart("PART_TextBlock", typeof(TextBlock))] +[TemplatePart("PART_SelectableTextBlock", typeof(SelectableTextBlock))] +public class DummyIconText : DummyIcon { + public static readonly StyledProperty IconPlacementProperty = + DockPanel.DockProperty.AddOwner(); + + public static readonly StyledProperty SpacingProperty = + StackPanel.SpacingProperty.AddOwner(); + + public static readonly StyledProperty OrientationProperty = + StackPanel.OrientationProperty.AddOwner(); + + public static readonly StyledProperty TextProperty = + TextBlock.TextProperty.AddOwner(); + + public static readonly StyledProperty IsTextSelectableProperty = + AvaloniaProperty.Register(nameof(IsTextSelectable)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalContentAlignmentProperty = + ContentControl.HorizontalContentAlignmentProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalContentAlignmentProperty = + ContentControl.VerticalContentAlignmentProperty.AddOwner(); + + /// + /// Gets or sets the icon placement relative to the text. + /// + public Dock IconPlacement { + get => GetValue(IconPlacementProperty); + set => SetValue(IconPlacementProperty, value); + } + + /// + /// Gets or sets the spacing between the icon and the text. + /// + public double Spacing { + get => GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + /// + /// Gets or sets the orientation in which the icon and the text will be layed out. + /// + public Orientation Orientation { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + /// + /// Gets or sets the text to display + /// + public string? Text { + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + /// + /// Gets or sets whether the text should be selectable + /// + public bool IsTextSelectable { + get => GetValue(IsTextSelectableProperty); + set => SetValue(IsTextSelectableProperty, value); + } + + /// + /// Gets or sets the horizontal alignment of the content within the control. + /// + public HorizontalAlignment HorizontalContentAlignment { + get => GetValue(HorizontalContentAlignmentProperty); + set => SetValue(HorizontalContentAlignmentProperty, value); + } + + /// + /// Gets or sets the vertical alignment of the content within the control. + /// + public VerticalAlignment VerticalContentAlignment { + get => GetValue(VerticalContentAlignmentProperty); + set => SetValue(VerticalContentAlignmentProperty, value); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + base.OnApplyTemplate(e); + + + if (Classes.Count > 0) { + // Ignore pseudo-classes replication, otherwise crash with: The pseudoclass ':xxx' may only be added by the control itself. + var filteredClasses = Classes.Where(c => c.Length > 0 && c[0] != ':').ToArray(); + + if (filteredClasses.Length > 0) { + // Redirect classes to the template parts + var icon = e.NameScope.Get("PART_Icon"); + var textBlock = e.NameScope.Get("PART_TextBlock"); + var selectableTextBlock = e.NameScope.Get("PART_SelectableTextBlock"); + + icon.Classes.AddRange(filteredClasses); + textBlock.Classes.AddRange(filteredClasses); + selectableTextBlock.Classes.AddRange(filteredClasses); + } + } + } +} diff --git a/IconGeneratorTemplate.Avalonia/DummyIconTextExt.cs b/IconGeneratorTemplate.Avalonia/DummyIconTextExt.cs new file mode 100644 index 0000000..c95afac --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/DummyIconTextExt.cs @@ -0,0 +1,114 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Layout; +using Avalonia.Markup.Xaml; +using IconGenerators; + +namespace Dummy.Icons.Avalonia; + +public class DummyIconTextExt : DummyIconExt { + public DummyIconTextExt() { } + + public DummyIconTextExt(DummyIconKind kind) : base(kind) + { + } + + public DummyIconTextExt(DummyIconKind kind, DummyIconAnimation animation) : base(kind, animation) + { + } + + public DummyIconTextExt(DummyIconKind kind, double iconSize, DummyIconAnimation animation = DummyIconAnimation.None) : base(kind, iconSize, animation) + { + } + + public DummyIconTextExt(DummyIconKind kind, string text, DummyIconAnimation animation = DummyIconAnimation.None) + : base(kind, animation) + { + Text = text; + } + + public DummyIconTextExt(DummyIconKind kind, double iconSize, string text, DummyIconAnimation animation = DummyIconAnimation.None) + : base(kind, iconSize, animation) + { + Text = text; + } + + public DummyIconTextExt(DummyIconKind kind, double iconSize, Dock iconPlacement, string text, DummyIconAnimation animation = DummyIconAnimation.None) + : base(kind, iconSize, animation) { + IconPlacement = iconPlacement; + Text = text; + } + + [ConstructorArgument("iconPlacement")] + public Dock? IconPlacement { get; set; } + + [ConstructorArgument("spacing")] + public double? Spacing { get; set; } + + [ConstructorArgument("text")] + public object? Text { get; set; } + + [ConstructorArgument("isTextSelectable")] + public bool? IsTextSelectable { get; set; } + + [ConstructorArgument("verticalContentAlignment")] + public VerticalAlignment? VerticalContentAlignment { get; set; } + + [ConstructorArgument("horizontalContentAlignment")] + public HorizontalAlignment? HorizontalContentAlignment { get; set; } + + public override object ProvideValue(IServiceProvider serviceProvider) { + // If no text is provided and it's not a binding, fall back to base + if (Text is null || (Text is not IBinding && Text is string textString && string.IsNullOrWhiteSpace(textString))) + return base.ProvideValue(serviceProvider); + + var result = new DummyIconText(); + + // Kind: binding takes precedence + if (KindBinding is not null) result.Bind(DummyIcon.KindProperty, KindBinding); + else result.Kind = Kind; + + // Animation: binding takes precedence + if (AnimationBinding is not null) result.Bind(DummyIcon.AnimationProperty, AnimationBinding); + else result.Animation = Animation; + + // Apply Text (supports binding or direct value) + if (Text is IBinding textBinding) result.Bind(DummyIconText.TextProperty, textBinding); + else if (Text is string textValue) result.Text = textValue; + + if (IconSize is not null) { + switch (IconSize) { + case IBinding binding: + result.Bind(DummyIcon.IconSizeProperty, binding); + break; + case IConvertible conv: + result.IconSize = conv.ToDouble(System.Globalization.CultureInfo.InvariantCulture); + break; + default: + throw new InvalidOperationException($"IconSize must be of type IBinding or IConvertible. Actual type: {IconSize.GetType().FullName}"); + } + } + + // IconForeground: binding takes precedence + if (IconForegroundBinding is not null) result.Bind(TemplatedControl.ForegroundProperty, IconForegroundBinding); + else if (IconForeground is not null) result.Foreground = IconForeground; + + if (IconPlacement is not null) result.IconPlacement = IconPlacement.Value; + + if (Spacing is not null) result.Spacing = Spacing.Value; + if (IsTextSelectable is not null) result.IsTextSelectable = IsTextSelectable.Value; + + if (VerticalAlignment is not null) result.VerticalAlignment = VerticalAlignment.Value; + if (HorizontalAlignment is not null) result.HorizontalAlignment = HorizontalAlignment.Value; + if (VerticalContentAlignment is not null) result.VerticalContentAlignment = VerticalContentAlignment.Value; + if (HorizontalContentAlignment is not null) result.HorizontalContentAlignment = HorizontalContentAlignment.Value; + + if (!string.IsNullOrWhiteSpace(Classes)) { + result.Classes.AddRange(global::Avalonia.Controls.Classes.Parse(Classes!)); + } + + return result; + } +} diff --git a/IconGeneratorTemplate.Avalonia/DummyIconsUtils.cs b/IconGeneratorTemplate.Avalonia/DummyIconsUtils.cs new file mode 100644 index 0000000..e0bc0d5 --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/DummyIconsUtils.cs @@ -0,0 +1,7 @@ +using Avalonia.Media; +using IconGenerators; + +namespace Dummy.Icons.Avalonia; +internal static class DummyIconsUtils { + public static void InitializeGeometryParser() => DummyIconDataProvider.InitializeGeometryParser(Geometry.Parse); +} \ No newline at end of file diff --git a/IconGeneratorTemplate.Avalonia/DummyIconstyles.axaml.cs b/IconGeneratorTemplate.Avalonia/DummyIconstyles.axaml.cs new file mode 100644 index 0000000..cb03681 --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/DummyIconstyles.axaml.cs @@ -0,0 +1,18 @@ +using System; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; + +namespace Dummy.Icons.Avalonia; +/// +/// Styles for Dummy.Icons.Avalonia library +/// +public sealed class DummyIconStyles : Styles { + /// + /// Initializes a new instance of the class. + /// + /// The parent's service provider. + public DummyIconStyles(IServiceProvider? serviceProvider) { + DummyIconsUtils.InitializeGeometryParser(); + AvaloniaXamlLoader.Load(serviceProvider, this); + } +} diff --git a/IconGeneratorTemplate.Avalonia/IconGeneratorTemplate.Avalonia.csproj b/IconGeneratorTemplate.Avalonia/IconGeneratorTemplate.Avalonia.csproj new file mode 100644 index 0000000..3007759 --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/IconGeneratorTemplate.Avalonia.csproj @@ -0,0 +1,71 @@ + + + Library + net10.0 + enable + true + + + + + true + 1.0.0 + your_name + your_company + https://github.com/TemuWolverine/Material.Icons + https://github.com/TemuWolverine/Material.Icons + git + readme.md + True + snupkg + true + true + true + snupkg + .pdb + + + + + + + Always + + + + + + + + + + + + + + + None + All + + + + + + + + + + + True + \ + + + + + + + + + + + diff --git a/IconGeneratorTemplate.Avalonia/PulseEasing.cs b/IconGeneratorTemplate.Avalonia/PulseEasing.cs new file mode 100644 index 0000000..88edd3f --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/PulseEasing.cs @@ -0,0 +1,17 @@ +using Avalonia.Animation.Easings; +using System.Collections.Generic; +using System.Linq; + +namespace Dummy.Icons.Avalonia; +public class PulseEasing : Easing { + private const int Steps = 8; + + private static readonly IEnumerable _steps = Enumerable + .Range(0, Steps + 1) + .Select(index => 1.0 / Steps * index) + .ToArray(); + + public override double Ease(double progress) { + return _steps.Last(step => step <= progress); + } +} diff --git a/IconGeneratorTemplate.Avalonia/readme.md b/IconGeneratorTemplate.Avalonia/readme.md new file mode 100644 index 0000000..82ae7be --- /dev/null +++ b/IconGeneratorTemplate.Avalonia/readme.md @@ -0,0 +1 @@ +test readme \ No newline at end of file diff --git a/Material.Icons/MaterialIconAnimationEnum.cs b/IconGenerators.Avalonia/DummyIconAnimation.cs similarity index 57% rename from Material.Icons/MaterialIconAnimationEnum.cs rename to IconGenerators.Avalonia/DummyIconAnimation.cs index b9a880d..b8bce9b 100644 --- a/Material.Icons/MaterialIconAnimationEnum.cs +++ b/IconGenerators.Avalonia/DummyIconAnimation.cs @@ -1,6 +1,7 @@ -namespace Material.Icons; +namespace IconGenerators.Avalonia; -public enum MaterialIconAnimation { +public enum IconAnimation +{ None, Spin, SpinCcw, diff --git a/IconGenerators.Avalonia/DummyIconsUtils.cs b/IconGenerators.Avalonia/DummyIconsUtils.cs new file mode 100644 index 0000000..a9e54a3 --- /dev/null +++ b/IconGenerators.Avalonia/DummyIconsUtils.cs @@ -0,0 +1,7 @@ +using Avalonia.Media; +using IconGenerators; + +namespace Dummy.Icons.Avalonia; +internal static class DummyIconsUtils { +// public static void InitializeGeometryParser() => DummyIconDataProvider.InitializeGeometryParser(Geometry.Parse); +} \ No newline at end of file diff --git a/IconGenerators.Avalonia/IIconProvider.cs b/IconGenerators.Avalonia/IIconProvider.cs new file mode 100644 index 0000000..05744c1 --- /dev/null +++ b/IconGenerators.Avalonia/IIconProvider.cs @@ -0,0 +1,8 @@ +using Avalonia.Media; + +namespace IconGenerators.Avalonia; + +public interface IIconProvider +{ + string ProvideData(string kind); +} diff --git a/IconGenerators.Avalonia/IconGenerators.Avalonia.csproj b/IconGenerators.Avalonia/IconGenerators.Avalonia.csproj new file mode 100644 index 0000000..0f293d9 --- /dev/null +++ b/IconGenerators.Avalonia/IconGenerators.Avalonia.csproj @@ -0,0 +1,26 @@ + + + Library + net10.0 + enable + true + + + + + + + + + + None + All + + + + + + UniversalIconStyles.axaml + + + diff --git a/IconGenerators.Avalonia/PulseEasing.cs b/IconGenerators.Avalonia/PulseEasing.cs new file mode 100644 index 0000000..362f7e4 --- /dev/null +++ b/IconGenerators.Avalonia/PulseEasing.cs @@ -0,0 +1,18 @@ +using Avalonia.Animation.Easings; +using System.Collections.Generic; +using System.Linq; + +namespace IconGenerators.Avalonia; + +public class PulseEasing : Easing { + private const int Steps = 8; + + private static readonly IEnumerable _steps = Enumerable + .Range(0, Steps + 1) + .Select(index => 1.0 / Steps * index) + .ToArray(); + + public override double Ease(double progress) { + return _steps.Last(step => step <= progress); + } +} diff --git a/IconGenerators.Avalonia/UniverasalIcon.axaml b/IconGenerators.Avalonia/UniverasalIcon.axaml new file mode 100644 index 0000000..1ec27a1 --- /dev/null +++ b/IconGenerators.Avalonia/UniverasalIcon.axaml @@ -0,0 +1,136 @@ + + 0:0:2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IconGenerators.Avalonia/UniversalIcon.axaml.cs b/IconGenerators.Avalonia/UniversalIcon.axaml.cs new file mode 100644 index 0000000..2d7b20e --- /dev/null +++ b/IconGenerators.Avalonia/UniversalIcon.axaml.cs @@ -0,0 +1,140 @@ +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace IconGenerators.Avalonia; + +public class UniversalIcon : TemplatedControl, IImage +{ + #region Properties + + public static readonly StyledProperty ProviderProperty = + AvaloniaProperty.Register(nameof(Provider)); + + public IIconProvider? Provider + { + get => GetValue(ProviderProperty); + set => SetValue(ProviderProperty, value); + } + + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); + + public string? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public static readonly StyledProperty IconSizeProperty = + AvaloniaProperty.Register(nameof(IconSize), defaultValue: double.NaN); + + public double IconSize + { + get => GetValue(IconSizeProperty); + set => SetValue(IconSizeProperty, value); + } + + public static readonly StyledProperty AnimationProperty + = AvaloniaProperty.Register(nameof(Animation)); + + /// + /// Gets or sets the icon animation to play. + /// + public IconAnimation Animation + { + get => GetValue(AnimationProperty); + set => SetValue(AnimationProperty, value); + } + + public static readonly DirectProperty DrawingProperty = + AvaloniaProperty.RegisterDirect( + nameof(Drawing), + o => o.Drawing); + + public GeometryDrawing Drawing { get; } = new(); + + private static readonly Rect DefaultIconBounds = new(0, 0, 24, 24); + + #endregion + + #region Constructor + + public UniversalIcon() + { + Drawing.Brush = Foreground; + } + + #endregion + + #region Overrides + + protected override void OnLoaded(RoutedEventArgs e) + { + if (Drawing.Geometry is null) + SetGeometry(); + base.OnLoaded(e); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.Property == ProviderProperty || e.Property == IconProperty) + { + SetGeometry(); + } + else if (e.Property == ForegroundProperty) + { + Drawing.Brush = Foreground; + } + } + + #endregion + + #region Methods + + private void SetGeometry() + { + if (Provider is not null && !string.IsNullOrEmpty(Icon)) + { + var data = Provider.ProvideData(Icon); + Drawing.Geometry = !string.IsNullOrEmpty(data) ? Geometry.Parse(data) : null; + } + else + { + Drawing.Geometry = null; + } + } + + #endregion + + #region IImage Implementation + + Size IImage.Size => DefaultIconBounds.Size; + + void IImage.Draw(DrawingContext context, Rect sourceRect, Rect destRect) + { + if (Drawing.Geometry is null) + SetGeometry(); + + var bounds = DefaultIconBounds; + var scale = Matrix.CreateScale( + destRect.Width / sourceRect.Width, + destRect.Height / sourceRect.Height + ); + var translate = Matrix.CreateTranslation( + -sourceRect.X + destRect.X - bounds.X, + -sourceRect.Y + destRect.Y - bounds.Y + ); + + using (context.PushClip(destRect)) + using (context.PushTransform(translate * scale)) + { + Drawing.Draw(context); + } + } + + #endregion +} \ No newline at end of file diff --git a/IconGenerators.Avalonia/UniversalIconStyles.axaml b/IconGenerators.Avalonia/UniversalIconStyles.axaml new file mode 100644 index 0000000..b8c7b15 --- /dev/null +++ b/IconGenerators.Avalonia/UniversalIconStyles.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/IconGenerators.Avalonia/UniversalIconStyles.axaml.cs b/IconGenerators.Avalonia/UniversalIconStyles.axaml.cs new file mode 100644 index 0000000..ce3d011 --- /dev/null +++ b/IconGenerators.Avalonia/UniversalIconStyles.axaml.cs @@ -0,0 +1,19 @@ +using System; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; +using Dummy.Icons.Avalonia; + +namespace IconGenerators.Avalonia; +/// +/// Styles for Dummy.Icons.Avalonia library +/// +public sealed class UniversalIconStyles : Styles { + /// + /// Initializes a new instance of the class. + /// + /// The parent's service provider. + public UniversalIconStyles(IServiceProvider? serviceProvider) { + // DummyIconsUtils.InitializeGeometryParser(); + AvaloniaXamlLoader.Load(serviceProvider, this); + } +} diff --git a/build/Meta/ExtensionMethods.cs b/IconGenerators/Common/ExtensionMethods.cs similarity index 96% rename from build/Meta/ExtensionMethods.cs rename to IconGenerators/Common/ExtensionMethods.cs index ede8d66..4e85dce 100644 --- a/build/Meta/ExtensionMethods.cs +++ b/IconGenerators/Common/ExtensionMethods.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace Meta { +namespace IconGenerators.Common { internal static class ExtensionMethods { /// /// Separates the input words with underscore diff --git a/build/Meta/IconInfo.cs b/IconGenerators/Common/IconInfo.cs similarity index 84% rename from build/Meta/IconInfo.cs rename to IconGenerators/Common/IconInfo.cs index 5a10597..106c17c 100644 --- a/build/Meta/IconInfo.cs +++ b/IconGenerators/Common/IconInfo.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Meta; +namespace IconGenerators.Common; public class IconInfo { public string Name { get; set; } diff --git a/IconGenerators/DummyIconDataProvider.cs b/IconGenerators/DummyIconDataProvider.cs new file mode 100644 index 0000000..893cb15 --- /dev/null +++ b/IconGenerators/DummyIconDataProvider.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Text; +using IconGenerators.Avalonia; + +namespace IconGenerators; + + +/// +/// Provides access to the Material Design icons data and its geometry. +/// +public partial class DummyIconDataProvider : IIconProvider +{ + private static DummyIconDataProvider _instance = new(); + private static Func? _parser; + + /// + /// Gets the cache singleton for the icons. The cache is used to store the parsed icons to avoid parsing them multiple times. + /// + private static Dictionary? _cache = new(); + + /// + /// Gets the cache for the icons. The cache is used to store the parsed icons to avoid parsing them multiple times. + /// + public static IReadOnlyDictionary? Cache => _cache; + + /// + /// Gets or sets the singleton instance of this provider + /// + public static DummyIconDataProvider Instance + { + get => _instance; + set + { + _instance = value ?? throw new ArgumentNullException(nameof(value)); + ClearCache(); + } + } + + /// + /// Disables the cache for the icons. + /// + public static void DisableCache() + { + _cache = null; + } + + /// + /// Clears the cache for the icons. + /// + public static void ClearCache() + { + _cache?.Clear(); + } + + /// + /// Initializes the geometry parser with the specified parsing function. + /// + /// This method sets the parser function to be used for geometry parsing operations. If the + /// parser has already been initialized, subsequent calls to this method will have no effect. + /// A function that takes a string input and returns an object representing the parsed geometry. This parameter + /// cannot be null. + public static void InitializeGeometryParser(Func parser) => _parser ??= parser; + + /// + /// Gets the geometry for the specified icon using the + /// + /// The icon kind + /// SVG path for target icon kind + public static T Get(DummyIconKind kind) where T : class + { + if (_cache?.TryGetValue(kind, out var value) is true) + { + return value as T ?? throw new InvalidOperationException( + "Invalid type for icon kind. Check that you are requesting the correct geometry type."); + } + + if (_parser is null) + { + throw new InvalidOperationException( + "Geometry parser not initialized. Call InitializeGeometryParser first."); + } + + var result = _parser(GetData(kind)) as T ?? throw new InvalidOperationException( + "Parser returns a wrong type. Check that you are requesting the correct geometry type."); + + if (_cache != null) + _cache[kind] = result; + + return result; + } + + /// + /// Gets the data for the specified icon using the + /// + /// The icon kind + /// SVG path for target icon kind + public static string GetData(DummyIconKind kind) => Instance.ProvideData(kind); + + /// + /// Provides the data for the specified icon kind + /// + /// The icon kind + /// SVG path for target icon kind + public virtual partial string ProvideData(DummyIconKind kind); + + public string ProvideData(string kindstring) + { + // Using Enum.Parse (throws if invalid) + DummyIconKind kind = (DummyIconKind)Enum.Parse(typeof(DummyIconKind), kindstring); + return ProvideData(kind); + } +} diff --git a/IconGenerators/DummyIconKind.cs b/IconGenerators/DummyIconKind.cs new file mode 100644 index 0000000..cf842a0 --- /dev/null +++ b/IconGenerators/DummyIconKind.cs @@ -0,0 +1,6 @@ +namespace IconGenerators +{ + public enum DummyIconKind + { + } +} diff --git a/IconGenerators/Feather/FeatherDownloader.cs b/IconGenerators/Feather/FeatherDownloader.cs new file mode 100644 index 0000000..35a2e79 --- /dev/null +++ b/IconGenerators/Feather/FeatherDownloader.cs @@ -0,0 +1,133 @@ +using IconGenerators.Common; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml; + +namespace IconGenerators.Feather +{ + public class FeatherDownloader : IIconPackGenerator + { + public string Name => "Feather"; + public async Task> FetchIconData() + { + var tagsUrl = "https://raw.githubusercontent.com/feathericons/feather/refs/heads/main/src/tags.json"; + using var http = new HttpClient(); + var json = await http.GetStringAsync(tagsUrl); + + // tags.json is a dictionary: { "iconName": [ "tag1", "tag2", ... ], ... } + var icons = JsonSerializer.Deserialize>>(json); + if (icons == null) + { + Console.WriteLine("Failed to parse icon names."); + return null; + } + + var iconlist = new List(); + foreach (var iconName in icons.Keys) + { + var svgUrl = $"https://raw.githubusercontent.com/feathericons/feather/refs/heads/main/icons/{iconName}.svg"; + try + { + var svgData = await http.GetStringAsync(svgUrl); + var pathData = ExtractPathLikeData(svgData); + if (string.IsNullOrEmpty(pathData)) + continue; + + iconlist.Add(new IconInfo { Name = IconPackGenerator.ToPascalCase(iconName), Data = pathData }); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to fetch or parse SVG for {iconName}: {ex.Message}"); + } + } + + return iconlist; + } + private static string ExtractPathLikeData(string svg) + { + if (string.IsNullOrWhiteSpace(svg)) + return string.Empty; + + try + { + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(svg); + + // Try + var pathNodes = xmlDoc.GetElementsByTagName("path"); + if (pathNodes.Count > 0 && pathNodes[0].Attributes["d"] != null) + { + return pathNodes[0].Attributes["d"].Value; + } + + // Try + var polylineNodes = xmlDoc.GetElementsByTagName("polyline"); + if (polylineNodes.Count > 0 && polylineNodes[0].Attributes["points"] != null) + { + var points = polylineNodes[0].Attributes["points"].Value; + return PolylinePointsToPathData(points, close: false); + } + + // Try + var polygonNodes = xmlDoc.GetElementsByTagName("polygon"); + if (polygonNodes.Count > 0 && polygonNodes[0].Attributes["points"] != null) + { + var points = polygonNodes[0].Attributes["points"].Value; + return PolylinePointsToPathData(points, close: true); + } + + // Try + var circleNodes = xmlDoc.GetElementsByTagName("circle"); + if (circleNodes.Count > 0) + { + var cx = circleNodes[0].Attributes["cx"]?.Value; + var cy = circleNodes[0].Attributes["cy"]?.Value; + var r = circleNodes[0].Attributes["r"]?.Value; + if (cx != null && cy != null && r != null && double.TryParse(cx, out var cxVal) && double.TryParse(cy, out var cyVal) && double.TryParse(r, out var rVal)) + { + // XAML Path for a circle: M cx+r,cy A r,r 0 1 0 cx-r,cy A r,r 0 1 0 cx+r,cy + // This draws a full circle using two arcs + var startX = cxVal + rVal; + var startY = cyVal; + var endX = cxVal - rVal; + var endY = cyVal; + return $"M {startX},{startY} A {rVal},{rVal} 0 1 0 {endX},{endY} A {rVal},{rVal} 0 1 0 {startX},{startY}"; + } + } + } + catch + { + // Ignore malformed SVG + } + return string.Empty; + } + + private static string PolylinePointsToPathData(string points, bool close) + { + // points: "x1 y1 x2 y2 x3 y3 ..." + var matches = Regex.Matches(points, @"-?\d*\.?\d+"); + if (matches.Count < 2) return string.Empty; + + var coords = new List(); + foreach (Match m in matches) + coords.Add(m.Value); + + if (coords.Count < 4) return string.Empty; + + var sb = new System.Text.StringBuilder(); + sb.Append($"M {coords[0]},{coords[1]}"); + for (int i = 2; i < coords.Count; i += 2) + { + sb.Append($" L {coords[i]},{coords[i + 1]}"); + } + if (close) + sb.Append(" Z"); + return sb.ToString(); + } + } +} diff --git a/IconGenerators/FontAwesome/FontAwesomeDownloader.cs b/IconGenerators/FontAwesome/FontAwesomeDownloader.cs new file mode 100644 index 0000000..8fcca45 --- /dev/null +++ b/IconGenerators/FontAwesome/FontAwesomeDownloader.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using System.Xml; +using IconGenerators.Common; + +namespace IconGenerators.FontAwesome; + + +public class FontAwesomeDownloader : IIconPackGenerator +{ + public string Name => "FontAwesome"; + public async Task> FetchIconData() + { + var url = "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/refs/heads/master/metadata/icons.json"; + using var httpClient = new HttpClient(); + var json = await httpClient.GetStringAsync(url); + + using var doc = JsonDocument.Parse(json); + var icons = new List(); + + foreach (var iconProperty in doc.RootElement.EnumerateObject()) + { + var name = iconProperty.Name; + if (iconProperty.Value.TryGetProperty("svg", out var svgElement)) + { + foreach (var styleProperty in svgElement.EnumerateObject()) + { + if (styleProperty.Value.TryGetProperty("raw", out var rawSvgElement)) + { + var rawSvg = rawSvgElement.GetString(); + var xamlPath = ConvertSvgToXamlPath(rawSvg); + icons.Add(new IconInfo { Name = IconPackGenerator.ToPascalCase(name), Data = xamlPath }); + break; + } + } + } + } + + return icons; + } + + private static string ConvertSvgToXamlPath(string svg) + { + if (string.IsNullOrWhiteSpace(svg)) + return string.Empty; + + try + { + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(svg); + + var pathNode = xmlDoc.GetElementsByTagName("path"); + if (pathNode.Count > 0 && pathNode[0].Attributes["d"] != null) + { + return pathNode[0].Attributes["d"].Value; + } + } + catch + { + // Ignore malformed SVG + } + return string.Empty; + } +} \ No newline at end of file diff --git a/IconGenerators/IIconPackGenerator.cs b/IconGenerators/IIconPackGenerator.cs new file mode 100644 index 0000000..bb7cf13 --- /dev/null +++ b/IconGenerators/IIconPackGenerator.cs @@ -0,0 +1,13 @@ +using IconGenerators.Common; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace IconGenerators +{ + public interface IIconPackGenerator + { + public string Name { get; } + public Task> FetchIconData(); + } +} diff --git a/IconGenerators/IconGenerators.csproj b/IconGenerators/IconGenerators.csproj new file mode 100644 index 0000000..8c60107 --- /dev/null +++ b/IconGenerators/IconGenerators.csproj @@ -0,0 +1,30 @@ + + + + NET10.0 + 12 + + + + + + + + + + + + Always + + + + + + + + + + + + + diff --git a/IconGenerators/IconPackGenerator.cs b/IconGenerators/IconPackGenerator.cs new file mode 100644 index 0000000..fdd7345 --- /dev/null +++ b/IconGenerators/IconPackGenerator.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using IconGenerators.Common; +using Nuke.Common.IO; + +namespace IconGenerators; + +public static class IconPackGenerator +{ + public static void Generate(IEnumerable icons, AbsolutePath dir, string iconPackName) + { + var targetDir = dir / "out" / iconPackName + ".Icons"; + + // Copy and adapt IconGeneratorTemplate.Avalonia project + var templateDir = dir / "IconGeneratorTemplate.Avalonia"; + var targetProjectDir = dir / "out" / (iconPackName + ".Icons.Avalonia"); + if (!Directory.Exists(targetProjectDir)) + Directory.CreateDirectory(targetProjectDir); + + foreach (var file in Directory.GetFiles(templateDir, "*", SearchOption.TopDirectoryOnly)) + { + var fileName = Path.GetFileName(file); + if (fileName == "obj" || fileName.EndsWith(".user")) continue; + + var fileContent = File.ReadAllText(file); + // Replace Dummy, namespace, and using IconGenerators; + fileContent = fileContent.Replace("Dummy", iconPackName); + fileContent = Regex.Replace(fileContent, @"namespace\\s+Dummy\\.Icons\\.Avalonia", $"namespace {iconPackName}.Icons.Avalonia"); + fileContent = Regex.Replace(fileContent, @"using\s+IconGenerators;", $"using {iconPackName};"); + + // If this is the .csproj file, update the project reference + if (fileName.EndsWith(".csproj")) + { + // Update IconGenerators.Avalonia reference to go up two directories + fileContent = Regex.Replace( + fileContent, + @"", + ""); + + + // Update IconGenerators reference as before + fileContent = Regex.Replace( + fileContent, + @"", + $""); + + + // Ensure all remaining 'Dummy' are replaced + fileContent = fileContent.Replace("Dummy", iconPackName); + } + + var newFileName = fileName.Replace("Dummy", iconPackName).Replace("IconGeneratorTemplate.Avalonia", iconPackName + ".Icons.Avalonia"); + var newFilePath = Path.Combine(targetProjectDir, newFileName); + File.WriteAllText(newFilePath, fileContent); + } + + var enumTypeName = $"{iconPackName}IconKind"; + var dataProviderTypeName = $"{iconPackName}IconDataProvider"; + + if (!Directory.Exists(targetDir)) + Directory.CreateDirectory(targetDir); + // Copy and adapt DummyIconDataProvider + var assembly = typeof(IconPackGenerator).Assembly; + var resourceName = assembly.GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith("DummyIconDataProvider.cs")); + if (resourceName != null) + { + using var stream = assembly.GetManifestResourceStream(resourceName); + using var reader = new StreamReader(stream); + var dummyProviderContent = reader.ReadToEnd(); + var replacedContent = dummyProviderContent.Replace("Dummy", iconPackName); + // Replace the namespace line with the correct namespace + replacedContent = System.Text.RegularExpressions.Regex.Replace( + replacedContent, + @"^namespace\s+.*$", + $"namespace {iconPackName}.Icons;", + System.Text.RegularExpressions.RegexOptions.Multiline); + var outputProviderPath = targetDir / $"{iconPackName}IconDataProvider.cs"; + File.WriteAllText(outputProviderPath, replacedContent); + } + var enumOutputFile = targetDir / $"{enumTypeName}.cs"; + var pathDataOutputFile = targetDir / $"{dataProviderTypeName}.PathData.cs"; + var csprojFile = targetDir / $"{iconPackName}.Icons.csproj"; + + // Generate enum file + var enumSb = new StringBuilder(); + enumSb.AppendLine($"namespace {iconPackName}.Icons;"); + enumSb.AppendLine("/// ******************************************"); + enumSb.AppendLine("/// This code is auto generated. Do not amend."); + enumSb.AppendLine("/// ******************************************"); + enumSb.AppendLine("/// "); + enumSb.AppendLine("/// List of available icon kinds"); + enumSb.AppendLine("/// "); + enumSb.AppendLine($"public enum {enumTypeName}"); + enumSb.AppendLine("{"); + foreach (var icon in icons) + enumSb.AppendLine($" {icon.Name},"); + enumSb.AppendLine("}"); + File.WriteAllText(enumOutputFile, enumSb.ToString()); + + // Generate path data provider file + var pathSb = new StringBuilder(); + pathSb.AppendLine("using System.Runtime.CompilerServices;"); + pathSb.AppendLine(); + pathSb.AppendLine($"namespace {iconPackName}.Icons;"); + pathSb.AppendLine("/// ******************************************"); + pathSb.AppendLine("/// This code is auto generated. Do not amend."); + pathSb.AppendLine("/// ******************************************"); + pathSb.AppendLine($"public partial class {dataProviderTypeName}"); + pathSb.AppendLine("{"); + pathSb.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); + pathSb.AppendLine($" public virtual partial string ProvideData({enumTypeName} kind)"); + pathSb.AppendLine(" => kind switch"); + pathSb.AppendLine(" {"); + foreach (var icon in icons) + { + var escapedData = icon.Data.Replace("\"", "\"\""); + pathSb.AppendLine($" {enumTypeName}.{icon.Name} => @\"{escapedData}\","); + } + pathSb.AppendLine(" _ => string.Empty"); + pathSb.AppendLine(" };"); + pathSb.AppendLine("}"); + File.WriteAllText(pathDataOutputFile, pathSb.ToString()); + + // Generate .csproj file + var csprojSb = new StringBuilder(); + csprojSb.AppendLine(""); + csprojSb.AppendLine(" "); + csprojSb.AppendLine(" net10.0"); + csprojSb.AppendLine(" enable"); + csprojSb.AppendLine(" latest"); + csprojSb.AppendLine(" "); + csprojSb.AppendLine(""); + csprojSb.AppendLine($""); + csprojSb.AppendLine(""); + csprojSb.AppendLine(""); + + + + + + File.WriteAllText(csprojFile, csprojSb.ToString()); + } + + public static string ToPascalCase(this string name) + { + // Split on any non-alphanumeric character (including hyphens) to remove them + var parts = Regex.Split(name, @"[^a-zA-Z0-9]+"); + var pascal = new StringBuilder(); + foreach (var part in parts) + { + if (string.IsNullOrEmpty(part)) + continue; + pascal.Append(char.ToUpperInvariant(part[0])); + if (part.Length > 1) + pascal.Append(part.Substring(1)); + } + var result = pascal.ToString(); + if (!string.IsNullOrEmpty(result) && char.IsDigit(result[0])) + result = "_" + result; + return result; + } +} \ No newline at end of file diff --git a/IconGenerators/LineIcons/LineIconsDownloader.cs b/IconGenerators/LineIcons/LineIconsDownloader.cs new file mode 100644 index 0000000..7d0b623 --- /dev/null +++ b/IconGenerators/LineIcons/LineIconsDownloader.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using System.Xml; +using IconGenerators.Common; + +namespace IconGenerators.LineIcons +{ + public class LineIconsDownloader : IIconPackGenerator + { + public string Name => "LineIcon"; + + public async Task> FetchIconData() + { + var icons = new List(); + try + { + var url = "https://raw.githubusercontent.com/LineiconsHQ/Lineicons/main/assets/icon-fonts/unicodesMap.json"; + using var http = new HttpClient(); + var json = await http.GetStringAsync(url); + + // Parse the JSON as a dictionary + var icons2 = JsonSerializer.Deserialize>(json); + + if (icons2 == null) + { + Console.WriteLine("Failed to parse icon names."); + return icons; + } + + foreach (var iconName in icons2.Keys) + { + // Remove "lni-" prefix if present + var id = iconName.StartsWith("lni-") ? iconName.Substring(4) : iconName; + var svgUrl = $"https://raw.githubusercontent.com/LineiconsHQ/Lineicons/refs/heads/main/assets/svgs/regular/{id}.svg"; + + try + { + var svgData = await http.GetStringAsync(svgUrl); + var pathData = ExtractPathData(svgData); + var info = new IconInfo + { + Name = id.ToPascalCase(), + Data = pathData, + }; + if (info.Name != null) + icons.Add(info); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to fetch or parse SVG for {iconName}: {ex.Message}"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error parsing JSON: {ex.Message}"); + } + return icons; + } + + private static string ExtractPathData(string svg) + { + if (string.IsNullOrWhiteSpace(svg)) + return string.Empty; + + try + { + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(svg); + + var pathNode = xmlDoc.GetElementsByTagName("path"); + if (pathNode.Count > 0 && pathNode[0].Attributes["d"] != null) + { + return pathNode[0].Attributes["d"].Value; + } + } + catch + { + // Ignore malformed SVG + } + return string.Empty; + } + } +} diff --git a/IconGenerators/Lucide/LucideDownloader.cs b/IconGenerators/Lucide/LucideDownloader.cs new file mode 100644 index 0000000..62b35a8 --- /dev/null +++ b/IconGenerators/Lucide/LucideDownloader.cs @@ -0,0 +1,149 @@ +using IconGenerators.Common; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml; + +namespace IconGenerators.Lucide +{ + public class LucideDownloader : IIconPackGenerator + { + public string Name => "Lucide"; + + public async Task> FetchIconData() + { + var icons = new List(); + var apiUrl = "https://api.github.com/repos/lucide-icons/lucide/contents/icons"; + using var http = new HttpClient(); + http.DefaultRequestHeaders.UserAgent.ParseAdd("CSharpApp"); // GitHub API requires a User-Agent + + var json = await http.GetStringAsync(apiUrl); + var files = JsonSerializer.Deserialize>(json); + + if (files == null) + { + Console.WriteLine("Failed to get directory info from GitHub API."); + return null; + } + + Console.WriteLine("Lucide icon names and SVG path data:"); + foreach (var file in files) + { + if (file.type == "file" && file.name.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) + { + var iconName = file.name[..^4]; // Remove ".svg" + try + { + var svgData = await http.GetStringAsync(file.download_url); + var pathData = ExtractPathLikeData(svgData); + + icons.Add(new IconInfo + { + Name = iconName.ToPascalCase(), + Data = pathData, + }); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to fetch or parse SVG for {iconName}: {ex.Message}"); + } + } + } + + return icons; + } + + + public class GitHubFile + { + public string name { get; set; } + public string download_url { get; set; } + public string type { get; set; } + } + + private static string ExtractPathLikeData(string svg) + { + if (string.IsNullOrWhiteSpace(svg)) + return string.Empty; + + try + { + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(svg); + + // Try + var pathNodes = xmlDoc.GetElementsByTagName("path"); + if (pathNodes.Count > 0 && pathNodes[0].Attributes["d"] != null) + { + return pathNodes[0].Attributes["d"].Value; + } + + // Try + var polylineNodes = xmlDoc.GetElementsByTagName("polyline"); + if (polylineNodes.Count > 0 && polylineNodes[0].Attributes["points"] != null) + { + var points = polylineNodes[0].Attributes["points"].Value; + return PolylinePointsToPathData(points, close: false); + } + + // Try + var polygonNodes = xmlDoc.GetElementsByTagName("polygon"); + if (polygonNodes.Count > 0 && polygonNodes[0].Attributes["points"] != null) + { + var points = polygonNodes[0].Attributes["points"].Value; + return PolylinePointsToPathData(points, close: true); + } + + // Try + var circleNodes = xmlDoc.GetElementsByTagName("circle"); + if (circleNodes.Count > 0) + { + var cx = circleNodes[0].Attributes["cx"]?.Value; + var cy = circleNodes[0].Attributes["cy"]?.Value; + var r = circleNodes[0].Attributes["r"]?.Value; + if (cx != null && cy != null && r != null && double.TryParse(cx, out var cxVal) && double.TryParse(cy, out var cyVal) && double.TryParse(r, out var rVal)) + { + var startX = cxVal + rVal; + var startY = cyVal; + var endX = cxVal - rVal; + var endY = cyVal; + return $"M {startX},{startY} A {rVal},{rVal} 0 1 0 {endX},{endY} A {rVal},{rVal} 0 1 0 {startX},{startY}"; + } + } + } + catch + { + // Ignore malformed SVG + } + return string.Empty; + } + + private static string PolylinePointsToPathData(string points, bool close) + { + var matches = Regex.Matches(points, @"-?\d*\.?\d+"); + if (matches.Count < 2) return string.Empty; + + var coords = new List(); + foreach (Match m in matches) + coords.Add(m.Value); + + if (coords.Count < 4) return string.Empty; + + var sb = new System.Text.StringBuilder(); + sb.Append($"M {coords[0]},{coords[1]}"); + for (int i = 2; i < coords.Count; i += 2) + { + sb.Append($" L {coords[i]},{coords[i + 1]}"); + } + if (close) + sb.Append(" Z"); + return sb.ToString(); + } + + + } +} diff --git a/build/Generators/PathDataGenerators/DictionaryStringGenerator.cs b/IconGenerators/Material/DictionaryStringGenerator.cs similarity index 97% rename from build/Generators/PathDataGenerators/DictionaryStringGenerator.cs rename to IconGenerators/Material/DictionaryStringGenerator.cs index c6287fa..9dd667b 100644 --- a/build/Generators/PathDataGenerators/DictionaryStringGenerator.cs +++ b/IconGenerators/Material/DictionaryStringGenerator.cs @@ -1,11 +1,10 @@ using System.Collections.Generic; using System.IO; using System.Text; -using Meta; +using IconGenerators.Common; using Nuke.Common.IO; using Serilog; - -namespace Generators.PathDataGenerators; +namespace IconGenerators.Material; public class DictionaryStringGenerator { public static void Write(AbsolutePath destinationPath, IEnumerable iconInfos) { diff --git a/build/Meta/MaterialIconsMetaTools.cs b/IconGenerators/Material/MaterialDownloader.cs similarity index 65% rename from build/Meta/MaterialIconsMetaTools.cs rename to IconGenerators/Material/MaterialDownloader.cs index ed4f3ff..d664ea5 100644 --- a/build/Meta/MaterialIconsMetaTools.cs +++ b/IconGenerators/Material/MaterialDownloader.cs @@ -1,35 +1,46 @@ -using System; +using IconGenerators.Common; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; -namespace Meta; +namespace IconGenerators.Material; + +public class MaterialDownloader : IIconPackGenerator +{ -public static class MaterialIconsMetaTools { static readonly HttpClient HttpClient = new(); const string MaterialIconsFetchApi = "https://dev.materialdesignicons.com/api/package/38EF63D0-4744-11E4-B3CF-842B2B6CFE1B"; - public static async Task> GetIcons() { + public string Name => "Material"; + + public async Task> FetchIconData() + { var dataStream = await HttpClient.GetStreamAsync(MaterialIconsFetchApi); var dataNode = await JsonSerializer.DeserializeAsync(dataStream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); var icons = dataNode.Icons.Select(ToIconInfo); var iconsByName = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var icon in icons.Where(icon => !iconsByName.ContainsKey(icon.Name))) { + foreach (var icon in icons.Where(icon => !iconsByName.ContainsKey(icon.Name))) + { iconsByName.Add(icon.Name, icon); } //Clean up aliases to avoid naming collisions var seenAliases = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var icon in iconsByName.Values) { + foreach (var icon in iconsByName.Values) + { seenAliases.Add(icon.Name); } - foreach (var icon in iconsByName.Values) { - for (var i = icon.Aliases.Count - 1; i >= 0; i--) { + foreach (var icon in iconsByName.Values) + { + for (var i = icon.Aliases.Count - 1; i >= 0; i--) + { var alias = icon.Aliases[i]; - if (iconsByName.ContainsKey(alias) || !IsValidIdentifier(alias) || seenAliases.Add(alias) == false) { + if (iconsByName.ContainsKey(alias) || !IsValidIdentifier(alias) || seenAliases.Add(alias) == false) + { icon.Aliases.RemoveAt(i); } } @@ -37,19 +48,25 @@ public static async Task> GetIcons() { return iconsByName.Values.OrderBy(x => x.Name); - static bool IsValidIdentifier(string identifier) { + static bool IsValidIdentifier(string identifier) + { return identifier?.Length > 0 && (char.IsLetter(identifier[0]) || identifier[0] == '_'); } } - static IconInfo ToIconInfo(MaterialIconInfo info) { - return new IconInfo { - Aliases = info.Aliases.Select(alias => alias.Name.Underscore().Pascalize()).ToList(), - Name = info.Name.Underscore().Pascalize(), + static IconInfo ToIconInfo(MaterialIconInfo info) + { + return new IconInfo + { + Aliases = info.Aliases.Select(alias => alias.Name.Underscore().Pascalize()).ToList(), + Name = info.Name.Underscore().Pascalize(), Data = info.Data }; } - internal class MetaMaterialIcons { + + + internal class MetaMaterialIcons + { public List Icons { get; set; } } } diff --git a/build/Meta/MaterialIconInfo.cs b/IconGenerators/Material/MaterialIconInfo.cs similarity index 92% rename from build/Meta/MaterialIconInfo.cs rename to IconGenerators/Material/MaterialIconInfo.cs index c6faed1..eb78f91 100644 --- a/build/Meta/MaterialIconInfo.cs +++ b/IconGenerators/Material/MaterialIconInfo.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Text.Json.Serialization; -namespace Meta { +namespace IconGenerators.Material { public class MaterialIconInfo { public string Name { get; set; } diff --git a/build/Meta/MaterialIconInfoAlias.cs b/IconGenerators/Material/MaterialIconInfoAlias.cs similarity index 68% rename from build/Meta/MaterialIconInfoAlias.cs rename to IconGenerators/Material/MaterialIconInfoAlias.cs index ac297d1..7c649c8 100644 --- a/build/Meta/MaterialIconInfoAlias.cs +++ b/IconGenerators/Material/MaterialIconInfoAlias.cs @@ -1,4 +1,4 @@ -namespace Meta; +namespace IconGenerators.Material; public class MaterialIconInfoAlias { public string Name { get; set; } diff --git a/build/Generators/MaterialIconKindEnumGenerator.cs b/IconGenerators/Material/MaterialIconKindEnumGenerator.cs similarity index 96% rename from build/Generators/MaterialIconKindEnumGenerator.cs rename to IconGenerators/Material/MaterialIconKindEnumGenerator.cs index 201d6f5..1b54dad 100644 --- a/build/Generators/MaterialIconKindEnumGenerator.cs +++ b/IconGenerators/Material/MaterialIconKindEnumGenerator.cs @@ -2,11 +2,11 @@ using System.Collections.ObjectModel; using System.IO; using System.Text; -using Meta; +using IconGenerators.Common; using Nuke.Common.IO; using Serilog; -namespace Generators; +namespace IconGenerators.Material; public static class MaterialIconKindEnumGenerator { public static void Write(AbsolutePath destinationPath, IEnumerable iconInfos) { diff --git a/build/Meta/MaterialIconUser.cs b/IconGenerators/Material/MaterialIconUser.cs similarity index 76% rename from build/Meta/MaterialIconUser.cs rename to IconGenerators/Material/MaterialIconUser.cs index e76eb30..2dbd4e1 100644 --- a/build/Meta/MaterialIconUser.cs +++ b/IconGenerators/Material/MaterialIconUser.cs @@ -1,6 +1,4 @@ - - -namespace Meta { +namespace IconGenerators.Material { public class MaterialIconUser { public string Id { get; set; } diff --git a/build/Generators/PathDataGenerators/SwitchRegularStringGenerator.cs b/IconGenerators/Material/SwitchRegularStringGenerator.cs similarity index 96% rename from build/Generators/PathDataGenerators/SwitchRegularStringGenerator.cs rename to IconGenerators/Material/SwitchRegularStringGenerator.cs index c665122..5999789 100644 --- a/build/Generators/PathDataGenerators/SwitchRegularStringGenerator.cs +++ b/IconGenerators/Material/SwitchRegularStringGenerator.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; using System.IO; using System.Text; -using Meta; +using IconGenerators.Common; using Nuke.Common.IO; using Serilog; -namespace Generators.PathDataGenerators; + +namespace IconGenerators.Material; public class SwitchRegularStringGenerator { public static void Write(AbsolutePath destinationPath, IEnumerable iconInfos) { diff --git a/IconPackGenerators.slnx b/IconPackGenerators.slnx new file mode 100644 index 0000000..c80a2ad --- /dev/null +++ b/IconPackGenerators.slnx @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/LICENSE b/LICENSE deleted file mode 100644 index da9f97e..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 SKProCH - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Material.Icons.Avalonia.Demo/.gitignore b/Material.Icons.Avalonia.Demo/.gitignore deleted file mode 100644 index dbe7c8f..0000000 --- a/Material.Icons.Avalonia.Demo/.gitignore +++ /dev/null @@ -1,337 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -## Visual Studio Code specific files and folder -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.jsons - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -**/Properties/launchSettings.json - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ diff --git a/Material.Icons.Avalonia.Demo/App.axaml b/Material.Icons.Avalonia.Demo/App.axaml deleted file mode 100644 index 8efb736..0000000 --- a/Material.Icons.Avalonia.Demo/App.axaml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - diff --git a/Material.Icons.Avalonia.Demo/App.axaml.cs b/Material.Icons.Avalonia.Demo/App.axaml.cs deleted file mode 100644 index 97c2efc..0000000 --- a/Material.Icons.Avalonia.Demo/App.axaml.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Input.Platform; -using Avalonia.Markup.Xaml; -using Material.Icons.Avalonia.Demo.ViewModels; -using Material.Icons.Avalonia.Demo.Views; - -namespace Material.Icons.Avalonia.Demo { - public class App : Application { - public override void Initialize() { - AvaloniaXamlLoader.Load(this); - } - - public static IClipboard Clipboard { get; private set; } = null!; - - public override void OnFrameworkInitializationCompleted() { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - desktop.MainWindow = new MainWindow { - DataContext = new MainWindowViewModel(), - }; - - Clipboard = desktop.MainWindow.Clipboard!; - } - - base.OnFrameworkInitializationCompleted(); - } - } -} diff --git a/Material.Icons.Avalonia.Demo/Assets/avalonia-logo.ico b/Material.Icons.Avalonia.Demo/Assets/avalonia-logo.ico deleted file mode 100644 index da8d49f..0000000 Binary files a/Material.Icons.Avalonia.Demo/Assets/avalonia-logo.ico and /dev/null differ diff --git a/Material.Icons.Avalonia.Demo/Controls/SelectionWrapper.cs b/Material.Icons.Avalonia.Demo/Controls/SelectionWrapper.cs deleted file mode 100644 index 3032a26..0000000 --- a/Material.Icons.Avalonia.Demo/Controls/SelectionWrapper.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Metadata; - -namespace Material.Icons.Avalonia.Demo.Controls { - [PseudoClasses("selectednow")] - public class SelectionWrapper : ContentControl { - public static readonly StyledProperty DataSourceProperty = AvaloniaProperty.Register(nameof(DataSource)); - - public static readonly StyledProperty CurrentSelectedProperty = AvaloniaProperty.Register(nameof(CurrentSelected)); - - public static readonly DirectProperty SelectedNowProperty = AvaloniaProperty.RegisterDirect( - nameof(SelectedNow), - wrapper => wrapper.CurrentSelected == wrapper.DataSource); - - static SelectionWrapper() { - PointerPressedEvent.Raised.Subscribe(tuple => { - if (tuple.Item1 is SelectionWrapper selectionWrapper) { - selectionWrapper.CurrentSelected = selectionWrapper.DataSource; - } - }); - - CurrentSelectedProperty.Changed.Subscribe(args => { - if (args.Sender is SelectionWrapper selectionWrapper) { - selectionWrapper.UpdateSelectedNow(); - } - }); - - DataSourceProperty.Changed.Subscribe(args => { - if (args.Sender is SelectionWrapper selectionWrapper) selectionWrapper.UpdateSelectedNow(); - }); - - SelectedNowProperty.Changed.Subscribe(args => { - if (args.Sender is SelectionWrapper selectionWrapper) { - if (args.NewValue.Value) { - selectionWrapper.PseudoClasses.Add(":selectednow"); - } - else { - selectionWrapper.PseudoClasses.Remove(":selectednow"); - } - } - }); - } - - public object DataSource { - get => GetValue(DataSourceProperty); - set => SetValue(DataSourceProperty, value); - } - - public object CurrentSelected { - get => GetValue(CurrentSelectedProperty); - set => SetValue(CurrentSelectedProperty, value); - } - - public bool SelectedNow { - get; - private set => SetAndRaise(SelectedNowProperty, ref field, value); - } - - public void UpdateSelectedNow() { - SelectedNow = DataSource != null && DataSource == CurrentSelected; - } - } -} diff --git a/Material.Icons.Avalonia.Demo/Material.Icons.Avalonia.Demo.csproj b/Material.Icons.Avalonia.Demo/Material.Icons.Avalonia.Demo.csproj deleted file mode 100644 index 8e502e4..0000000 --- a/Material.Icons.Avalonia.Demo/Material.Icons.Avalonia.Demo.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - WinExe - net10.0 - enable - - - - - %(Filename) - - - Designer - - - - - - - - - - - - - - - - diff --git a/Material.Icons.Avalonia.Demo/Models/PackIconKindGroup.cs b/Material.Icons.Avalonia.Demo/Models/PackIconKindGroup.cs deleted file mode 100644 index f123dae..0000000 --- a/Material.Icons.Avalonia.Demo/Models/PackIconKindGroup.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Material.Icons.Avalonia.Demo.Models { - public class PackIconKindGroup { - public PackIconKindGroup(IEnumerable kinds) { - if (kinds is null) throw new ArgumentNullException(nameof(kinds)); - - var allValues = kinds as string[] ?? kinds.ToArray(); - if (allValues.Length == 0) throw new ArgumentException($"{nameof(kinds)} must contain at least one value"); - Kind = allValues[0]; - Aliases = allValues - .OrderBy(x => x, StringComparer.InvariantCultureIgnoreCase) - .ToArray(); - } - - public string Kind { get; } - public string[] Aliases { get; } - } -} diff --git a/Material.Icons.Avalonia.Demo/Models/StringJoinConverter.cs b/Material.Icons.Avalonia.Demo/Models/StringJoinConverter.cs deleted file mode 100644 index ef892c8..0000000 --- a/Material.Icons.Avalonia.Demo/Models/StringJoinConverter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections; -using System.Globalization; -using System.Linq; -using Avalonia.Data.Converters; - -namespace Material.Icons.Avalonia.Demo.Models { - public class StringJoinConverter : IValueConverter { - public string? Separator { get; set; } - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - IEnumerable values = value as IEnumerable ?? Array.Empty(); - return string.Join(Separator ?? "", values.OfType()); - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { - throw new NotSupportedException(); - } - } -} \ No newline at end of file diff --git a/Material.Icons.Avalonia.Demo/ViewLocator.cs b/Material.Icons.Avalonia.Demo/ViewLocator.cs deleted file mode 100644 index 135c123..0000000 --- a/Material.Icons.Avalonia.Demo/ViewLocator.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Avalonia.Controls; -using Avalonia.Controls.Templates; -using Material.Icons.Avalonia.Demo.ViewModels; - -#pragma warning disable 8600 -#pragma warning disable 8602 -#pragma warning disable 8603 - -namespace Material.Icons.Avalonia.Demo { - public class ViewLocator : IDataTemplate { - public bool SupportsRecycling => false; - - public Control Build(object? data) { - var name = data.GetType().FullName.Replace("ViewModel", "View"); - var type = Type.GetType(name); - - if (type != null) { - return (Control) Activator.CreateInstance(type); - } - else { - return new TextBlock {Text = "Not Found: " + name}; - } - } - - public bool Match(object? data) { - return data is ViewModelBase; - } - } -} diff --git a/Material.Icons.Avalonia.Demo/ViewModels/MainWindowViewModel.cs b/Material.Icons.Avalonia.Demo/ViewModels/MainWindowViewModel.cs deleted file mode 100644 index 1faabbb..0000000 --- a/Material.Icons.Avalonia.Demo/ViewModels/MainWindowViewModel.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Linq; -using System.Threading.Tasks; -using DynamicData.Binding; -using Material.Icons.Avalonia.Demo.Models; -using ReactiveUI; - -namespace Material.Icons.Avalonia.Demo.ViewModels { - public class MainWindowViewModel : ViewModelBase { - private readonly PackIconKindGroup[] _packIconKinds; - private PackIconKindGroup[] _kinds; - - public int IconCount => _packIconKinds.Length; - - public PackIconKindGroup[] Kinds { - get => _kinds; - set => this.RaiseAndSetIfChanged(ref _kinds, value); - } - - public PackIconKindGroup? Group { - get; - set => this.RaiseAndSetIfChanged(ref field, value); - } - - public string? SearchText { - get; - set => this.RaiseAndSetIfChanged(ref field, value); - } - - public IEnumerable Animations { get; set; } = Enum.GetValues(); - - public MaterialIconKind RandomIconKind { - get; - set => this.RaiseAndSetIfChanged(ref field, value); - } - - public MaterialIconAnimation Animation { - get; - set => this.RaiseAndSetIfChanged(ref field, value); - } = MaterialIconAnimation.None; - - public string DisabledIconText => "Disabled"; - - public MainWindowViewModel() { - _packIconKinds = Enum.GetNames() - .GroupBy(Enum.Parse) - .Select(g => new PackIconKindGroup(g)) - .OrderBy(x => x.Kind) - .ToArray(); - - _kinds = _packIconKinds; - - this.WhenValueChanged(model => model.SearchText) - .Throttle(TimeSpan.FromMilliseconds(500)) - .Subscribe(DoSearch); - } - - private void DoSearch(string? text) { - text = text?.Trim(); - if (string.IsNullOrWhiteSpace(text)) - // Reset to all icons - Kinds = _packIconKinds; - else { - // Search for given icon - Kinds = _packIconKinds.Where(x => - x.Aliases.Any(a => a.Contains(text, StringComparison.CurrentCultureIgnoreCase))) - .ToArray(); - } - } - - public void Unselect() { - Group = null; - } - - public async Task CopySelected() { - if (Group is null) return; - await App.Clipboard.SetTextAsync($""); - } - } -} diff --git a/Material.Icons.Avalonia.Demo/ViewModels/ViewModelBase.cs b/Material.Icons.Avalonia.Demo/ViewModels/ViewModelBase.cs deleted file mode 100644 index d461eeb..0000000 --- a/Material.Icons.Avalonia.Demo/ViewModels/ViewModelBase.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using ReactiveUI; - -namespace Material.Icons.Avalonia.Demo.ViewModels { - public class ViewModelBase : ReactiveObject { } -} \ No newline at end of file diff --git a/Material.Icons.Avalonia.Demo/Views/MainWindow.axaml b/Material.Icons.Avalonia.Demo/Views/MainWindow.axaml deleted file mode 100644 index bb83c49..0000000 --- a/Material.Icons.Avalonia.Demo/Views/MainWindow.axaml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0:0:2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Material.Icons.Avalonia/MaterialIcon.axaml.cs b/Material.Icons.Avalonia/MaterialIcon.axaml.cs deleted file mode 100644 index 857efbe..0000000 --- a/Material.Icons.Avalonia/MaterialIcon.axaml.cs +++ /dev/null @@ -1,137 +0,0 @@ -using Avalonia; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using Avalonia.Media; - -namespace Material.Icons.Avalonia { - - public class MaterialIcon : TemplatedControl, IImage { - #region Properties - - public static readonly StyledProperty KindProperty - = AvaloniaProperty.Register(nameof(Kind)); - - /// - /// Gets or sets the icon to display. - /// - public MaterialIconKind? Kind { - get => GetValue(KindProperty); - set => SetValue(KindProperty, value); - } - - public static readonly StyledProperty IconSizeProperty = - AvaloniaProperty.Register(nameof(IconSize), defaultValue: double.NaN); - - /// - /// Gets or sets the uniform size of the icon. - /// - public double IconSize { - get => GetValue(IconSizeProperty); - set => SetValue(IconSizeProperty, value); - } - - public static readonly StyledProperty AnimationProperty - = AvaloniaProperty.Register(nameof(Animation)); - - /// - /// Gets or sets the icon animation to play. - /// - public MaterialIconAnimation Animation { - get => GetValue(AnimationProperty); - set => SetValue(AnimationProperty, value); - } - - public static readonly DirectProperty DrawingProperty = - AvaloniaProperty.RegisterDirect( - nameof(Drawing), - o => o.Drawing); - - /// - /// Gets the of the icon. - /// - public GeometryDrawing Drawing { get; } = new(); - - // Default size for Material Icons - private static readonly Rect DefaultIconBounds = new(0, 0, 24, 24); - - #endregion - - #region Constructor - - static MaterialIcon() { - MaterialIconsUtils.InitializeGeometryParser(); - } - - public MaterialIcon() { - Drawing.Brush = Foreground; - } - - #endregion - - #region Overrides - - /// - protected override void OnLoaded(RoutedEventArgs e) { - if (Drawing.Geometry is null) - SetGeometry(); - base.OnLoaded(e); - } - - /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { - base.OnPropertyChanged(e); - - if (e.Property == KindProperty) { - SetGeometry(); - } - else if (e.Property == ForegroundProperty) { - Drawing.Brush = Foreground; - } - } - - #endregion - - #region Methods - - /// - /// Sets the geometry for the drawing based on the specified material icon kind. - /// - /// This method updates the Geometry property by parsing the - /// geometry data associated with the current value. - /// - private void SetGeometry() { - Drawing.Geometry = Kind is null ? null : MaterialIconDataProvider.Get(Kind.Value); - } - - #endregion - - #region IImage Implementation - - /// - Size IImage.Size => DefaultIconBounds.Size; - - /// - void IImage.Draw(DrawingContext context, Rect sourceRect, Rect destRect) { - if (Drawing.Geometry is null) - SetGeometry(); - - var bounds = DefaultIconBounds; - var scale = Matrix.CreateScale( - destRect.Width / sourceRect.Width, - destRect.Height / sourceRect.Height - ); - var translate = Matrix.CreateTranslation( - -sourceRect.X + destRect.X - bounds.X, - -sourceRect.Y + destRect.Y - bounds.Y - ); - - using (context.PushClip(destRect)) - using (context.PushTransform(translate * scale)) { - Drawing.Draw(context); - } - } - - #endregion - - } -} diff --git a/Material.Icons.Avalonia/MaterialIconExt.cs b/Material.Icons.Avalonia/MaterialIconExt.cs deleted file mode 100644 index b8a8c01..0000000 --- a/Material.Icons.Avalonia/MaterialIconExt.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using Avalonia.Controls.Primitives; -using Avalonia.Data; -using Avalonia.Layout; -using Avalonia.Markup.Xaml; -using Avalonia.Media; - -namespace Material.Icons.Avalonia { - public class MaterialIconExt : MarkupExtension { - public MaterialIconExt() { } - public MaterialIconExt(MaterialIconKind kind) { - Kind = kind; - } - - public MaterialIconExt(MaterialIconKind kind, MaterialIconAnimation animation) { - Kind = kind; - Animation = animation; - } - - public MaterialIconExt(MaterialIconKind kind, double iconSize, MaterialIconAnimation animation = MaterialIconAnimation.None) { - Kind = kind; - IconSize = iconSize; - Animation = animation; - } - - /// - /// Gets or sets the icon to display. Provides IntelliSense autocomplete. - /// - [ConstructorArgument("kind")] - public MaterialIconKind Kind { get; set; } - - /// - /// Gets or sets a binding for the icon kind. Use this when data binding is required. - /// - [ConstructorArgument("kindBinding")] - public IBinding? KindBinding { get; set; } - - /// - /// Gets or sets the size of the icon to display.
- /// Can be a double or a binding. - ///
- [ConstructorArgument("iconSize")] - public object? IconSize { get; set; } - - /// - /// Gets or sets the icon foreground brush. - /// - [ConstructorArgument("iconForeground")] - public IBrush? IconForeground { get; set; } - - /// - /// Gets or sets a binding for the icon foreground. Use this when data binding is required. - /// - [ConstructorArgument("iconForegroundBinding")] - public IBinding? IconForegroundBinding { get; set; } - - /// - /// Gets or sets the animation to play. Provides IntelliSense autocomplete. - /// - [ConstructorArgument("animation")] - public MaterialIconAnimation Animation { get; set; } - - /// - /// Gets or sets a binding for the animation. Use this when data binding is required. - /// - [ConstructorArgument("animationBinding")] - public IBinding? AnimationBinding { get; set; } - - /// - /// Gets or sets the vertical alignment of the content. - /// - [ConstructorArgument("verticalAlignment")] - public VerticalAlignment? VerticalAlignment { get; set; } - - /// - /// Gets or sets the horizontal alignment of the content. - /// - [ConstructorArgument("horizontalAlignment")] - public HorizontalAlignment? HorizontalAlignment { get; set; } - - /// - /// Gets or sets the class names to apply to the element. - /// - [ConstructorArgument("classes")] - public string? Classes { get; set; } - - public override object ProvideValue(IServiceProvider serviceProvider) { - var result = new MaterialIcon(); - - // Kind: binding takes precedence - if (KindBinding is not null) result.Bind(MaterialIcon.KindProperty, KindBinding); - else result.Kind = Kind; - - // Animation: binding takes precedence - if (AnimationBinding is not null) result.Bind(MaterialIcon.AnimationProperty, AnimationBinding); - else result.Animation = Animation; - - if (IconSize is not null) { - switch (IconSize) { - case IBinding binding: - result.Bind(MaterialIcon.IconSizeProperty, binding); - break; - case IConvertible conv: - result.IconSize = conv.ToDouble(System.Globalization.CultureInfo.InvariantCulture); - break; - default: - throw new InvalidOperationException($"IconSize must be of type IBinding or IConvertible. Actual type: {IconSize.GetType().FullName}"); - } - } - - // IconForeground: binding takes precedence - if (IconForegroundBinding is not null) result.Bind(TemplatedControl.ForegroundProperty, IconForegroundBinding); - else if (IconForeground is not null) result.Foreground = IconForeground; - - if (VerticalAlignment is not null) result.VerticalAlignment = VerticalAlignment.Value; - if (HorizontalAlignment is not null) result.HorizontalAlignment = HorizontalAlignment.Value; - - if (!string.IsNullOrWhiteSpace(Classes)) { - result.Classes.AddRange(global::Avalonia.Controls.Classes.Parse(Classes!)); - } - - return result; - } - } -} diff --git a/Material.Icons.Avalonia/MaterialIconStyles.axaml.cs b/Material.Icons.Avalonia/MaterialIconStyles.axaml.cs deleted file mode 100644 index bf42ad5..0000000 --- a/Material.Icons.Avalonia/MaterialIconStyles.axaml.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using Avalonia.Markup.Xaml; -using Avalonia.Styling; - -namespace Material.Icons.Avalonia { - /// - /// Styles for Material.Icons.Avalonia library - /// - public sealed class MaterialIconStyles : Styles { - /// - /// Initializes a new instance of the class. - /// - /// The parent's service provider. - public MaterialIconStyles(IServiceProvider? serviceProvider) { - MaterialIconsUtils.InitializeGeometryParser(); - AvaloniaXamlLoader.Load(serviceProvider, this); - } - } -} diff --git a/Material.Icons.Avalonia/MaterialIconText.axaml b/Material.Icons.Avalonia/MaterialIconText.axaml deleted file mode 100644 index 7a57b82..0000000 --- a/Material.Icons.Avalonia/MaterialIconText.axaml +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -