diff --git a/src/NexusMods.App.UI/Assets/Icons/add_circle_24px.svg b/src/NexusMods.App.UI/Assets/Icons/add_circle_24px.svg deleted file mode 100644 index 3e2ac81e8e..0000000000 --- a/src/NexusMods.App.UI/Assets/Icons/add_circle_24px.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/NexusMods.App.UI/Assets/Icons/bookmarks_24px.svg b/src/NexusMods.App.UI/Assets/Icons/bookmarks_24px.svg deleted file mode 100644 index 8cc8d1cb89..0000000000 --- a/src/NexusMods.App.UI/Assets/Icons/bookmarks_24px.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/NexusMods.App.UI/Assets/Icons/collections.svg b/src/NexusMods.App.UI/Assets/Icons/collections.svg deleted file mode 100644 index ea1788d5e5..0000000000 --- a/src/NexusMods.App.UI/Assets/Icons/collections.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/NexusMods.App.UI/Assets/Icons/discord.svg b/src/NexusMods.App.UI/Assets/Icons/discord.svg deleted file mode 100644 index 9f93feb056..0000000000 --- a/src/NexusMods.App.UI/Assets/Icons/discord.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/NexusMods.App.UI/Assets/Icons/disk_20px.svg b/src/NexusMods.App.UI/Assets/Icons/disk_20px.svg deleted file mode 100644 index ed52761e32..0000000000 --- a/src/NexusMods.App.UI/Assets/Icons/disk_20px.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/NexusMods.App.UI/Assets/Icons/forum.svg b/src/NexusMods.App.UI/Assets/Icons/forum.svg deleted file mode 100644 index d4640920b7..0000000000 --- a/src/NexusMods.App.UI/Assets/Icons/forum.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/src/NexusMods.App.UI/Assets/Icons/list_filled_24px.svg b/src/NexusMods.App.UI/Assets/Icons/list_filled_24px.svg deleted file mode 100644 index efe38351d8..0000000000 --- a/src/NexusMods.App.UI/Assets/Icons/list_filled_24px.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/NexusMods.App.UI/Assets/Icons/mod_library.svg b/src/NexusMods.App.UI/Assets/Icons/mod_library.svg deleted file mode 100644 index 39b525c20e..0000000000 --- a/src/NexusMods.App.UI/Assets/Icons/mod_library.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/NexusMods.App.UI/Assets/Icons/mods.svg b/src/NexusMods.App.UI/Assets/Icons/mods.svg deleted file mode 100644 index a725958563..0000000000 --- a/src/NexusMods.App.UI/Assets/Icons/mods.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/NexusMods.App.UI/Assets/Icons/newspaper-variant-outline-1.svg b/src/NexusMods.App.UI/Assets/Icons/newspaper-variant-outline-1.svg deleted file mode 100644 index e9ea35e4a4..0000000000 --- a/src/NexusMods.App.UI/Assets/Icons/newspaper-variant-outline-1.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/NexusMods.App.UI/Assets/Icons/stethoscope_24px.svg b/src/NexusMods.App.UI/Assets/Icons/stethoscope_24px.svg deleted file mode 100644 index 44c186a352..0000000000 --- a/src/NexusMods.App.UI/Assets/Icons/stethoscope_24px.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/NexusMods.App.UI/Assets/Icons/videogame_asset_24px.svg b/src/NexusMods.App.UI/Assets/Icons/videogame_asset_24px.svg deleted file mode 100644 index 1d94ad0af6..0000000000 --- a/src/NexusMods.App.UI/Assets/Icons/videogame_asset_24px.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/NexusMods.Icons/IconValue.cs b/src/NexusMods.Icons/IconValue.cs index e700a5b8f6..09414793cd 100644 --- a/src/NexusMods.Icons/IconValue.cs +++ b/src/NexusMods.Icons/IconValue.cs @@ -1,11 +1,13 @@ using Avalonia.Media; using JetBrains.Annotations; +using NexusMods.Icons.SimpleVector; using Union = OneOf.OneOf< NexusMods.Icons.Empty, NexusMods.Icons.ProjektankerIcon, NexusMods.Icons.AvaloniaImage, NexusMods.Icons.AvaloniaSvg, - NexusMods.Icons.AvaloniaPathIcon>; + NexusMods.Icons.AvaloniaPathIcon, + NexusMods.Icons.SimpleVectorIcon>; namespace NexusMods.Icons; @@ -42,6 +44,11 @@ public Geometry? GeometrySetter { set => Value = new AvaloniaPathIcon(value); } + + public SimpleVectorIconImage? SimpleVectorSetter + { + set => Value = new SimpleVectorIcon(value); + } public IconValue() { @@ -57,6 +64,7 @@ public IconValue(Union input) public static implicit operator IconValue(AvaloniaImage value) => new(value); public static implicit operator IconValue(AvaloniaSvg value) => new(value); public static implicit operator IconValue(AvaloniaPathIcon value) => new(value); + public static implicit operator IconValue(SimpleVectorIcon value) => new(value); } [PublicAPI] @@ -74,3 +82,5 @@ public record struct AvaloniaSvg(string? Path); [PublicAPI] public record struct AvaloniaPathIcon(Geometry? Geometry); +[PublicAPI] +public record struct SimpleVectorIcon(SimpleVectorIconImage Image); diff --git a/src/NexusMods.Icons/IconValues.cs b/src/NexusMods.Icons/IconValues.cs index 7dc4696c0d..23ffea9bc3 100644 --- a/src/NexusMods.Icons/IconValues.cs +++ b/src/NexusMods.Icons/IconValues.cs @@ -1,4 +1,6 @@ +using Avalonia; using Avalonia.Media; +using NexusMods.Icons.SimpleVector; namespace NexusMods.Icons; @@ -16,8 +18,11 @@ Important Notes!! - Sewer - Use Projektanker Icon if possible https://pictogrammers.com/library/mdi/icon/code-tags/ - Projectanker Icons are the raw SVGs, so it's okay ^-^ - - Export SVG from Figma if icon is custom. + - Create a SimpleVectorIconImage based on the contents of an SVG - This will give you the correct icon size, as padding etc. is preserved. + - We can't use the raw SVGs as they don't support recolouring. + - If you explicitly don't want recolouring for brand purposes, import an SVG like + - AvaloniaSvg("avares://NexusMods.App.UI/Assets/Icons/disk_20px.svg"); How to Import SVG: @@ -25,17 +30,19 @@ Exporting from Figma may give you an SVG like ```xml - + ``` - You have to remove the 'fill' info. So remove `fill="none"`, `fill="#F4F4F5"` - and `fill-rule="evenodd"`. This will allow recolouring of the icon. - - ```xml - - - + You have to extract the `d` attribute from the `path` tag, and the `viewBox` attribute from the `svg` tag. + + Then create a `SimpleVectorIconImage` with the `d` attribute as the `pathData` and the `viewBox` attribute as the `viewBox`. + + ```csharp + public static readonly IconValue Mods = new SimpleVectorIconImage( + "M12.46 17.9912L18.1722 13.5441L19.445 12.5584L12.46 7.12561L5.47498 12.5584L6.74004 13.5441L12.46 17.9912Z", + new Rect(0, 0, 25, 25) + ); ``` */ @@ -314,43 +321,65 @@ public static class IconValues public static readonly IconValue Alert = new ProjektankerIcon("mdi-alert-octagon"); // From Design System "Custom Icons" section on Figma - public static readonly IconValue Mods = new AvaloniaSvg("avares://NexusMods.App.UI/Assets/Icons/mods.svg"); + public static readonly IconValue Mods = new SimpleVectorIcon(new SimpleVectorIconImage( + "M12.46 17.9912L18.1722 13.5441L19.445 12.5584L12.46 7.12561L5.47498 12.5584L6.74004 13.5441L12.46 17.9912Z", + new Rect(0, 0, 25, 25) + )); // From Design System "Custom Icons" section on Figma - public static readonly IconValue Collections = new AvaloniaSvg("avares://NexusMods.App.UI/Assets/Icons/collections.svg"); + public static readonly IconValue Collections = new SimpleVectorIcon(new SimpleVectorIconImage( + "M12.1979 15.4946L6.68644 11.2096L5.47498 12.1518L12.2053 17.3866L18.9357 12.1518L17.7167 11.2021L12.1979 15.4946ZM12.1979 19.2336L6.68644 14.9486L5.47498 15.8908L12.2053 21.1255L18.9357 15.8908L17.7167 14.9411L12.1979 19.2336ZM12.2053 13.5951L17.7093 9.31006L18.9357 8.36033L12.2053 3.12561L5.47498 8.36033L6.69392 9.31006L12.2053 13.5951Z", + new Rect(0, 0, 25, 25) + )); // From Design System "Custom Icons" section on Figma - public static readonly IconValue ListFilled = new AvaloniaSvg("avares://NexusMods.App.UI/Assets/Icons/list_filled_24px.svg"); - + public static readonly IconValue ListFilled = new SimpleVectorIcon(new SimpleVectorIconImage( + "M9.39429 18.8744H20.3943V16.1994H9.39429V18.8744ZM4.39429 9.54939H7.39429V6.87439H4.39429V9.54939ZM4.39429 14.2244H7.39429V11.5494H4.39429V14.2244ZM4.39429 18.8744H7.39429V16.1994H4.39429V18.8744ZM9.39429 14.2244H20.3943V11.5494H9.39429V14.2244ZM9.39429 9.54939H20.3943V6.87439H9.39429V9.54939ZM4.39429 20.8744C3.84429 20.8744 3.37345 20.6786 2.98179 20.2869C2.59012 19.8952 2.39429 19.4244 2.39429 18.8744V6.87439C2.39429 6.32439 2.59012 5.85356 2.98179 5.46189C3.37345 5.07022 3.84429 4.87439 4.39429 4.87439H20.3943C20.9443 4.87439 21.4151 5.07022 21.8068 5.46189C22.1985 5.85356 22.3943 6.32439 22.3943 6.87439V18.8744C22.3943 19.4244 22.1985 19.8952 21.8068 20.2869C21.4151 20.6786 20.9443 20.8744 20.3943 20.8744H4.39429Z", + new Rect(0, 0, 25, 25) + )); + // https://pictogrammers.com/library/mdi/icon/progress-download/ public static readonly IconValue Downloading = new ProjektankerIcon("mdi-progress-download"); // From Design System "Custom Icons" section on Figma - public static readonly IconValue ModLibrary = new AvaloniaSvg("avares://NexusMods.App.UI/Assets/Icons/mod_library.svg"); + public static readonly IconValue ModLibrary = new SimpleVectorIcon(new SimpleVectorIconImage( + "M18.3721 4.87439H6.40772C6.40772 4.87439 6.1358 2.87439 8.03922 2.87439H17.2844C18.644 2.87439 18.3721 4.87439 18.3721 4.87439ZM22.3943 20.5411V11.2077C22.3943 9.92439 21.4943 8.87439 20.3943 8.87439H4.39429C3.29429 8.87439 2.39429 9.92439 2.39429 11.2077V20.5411C2.39429 21.8244 3.29429 22.8744 4.39429 22.8744H20.3943C21.4943 22.8744 22.3943 21.8244 22.3943 20.5411ZM4.41219 7.87439H20.3647C20.3647 7.87439 20.7272 5.87439 18.9145 5.87439H6.58753C4.04963 5.87439 4.41219 7.87439 4.41219 7.87439ZM12.3943 11.8744L18.3943 15.8805L12.3943 19.8744L6.39429 15.8805L12.3943 11.8744Z", + new Rect(0, 0, 25, 25) + )); // From Design System "Custom Icons" section on Figma - public static readonly IconValue Discord = new AvaloniaSvg("avares://NexusMods.App.UI/Assets/Icons/discord.svg"); + public static readonly IconValue Discord = new SimpleVectorIcon(new SimpleVectorIconImage( + "M19.4058 5.38929C18.1311 4.80439 16.7641 4.37346 15.3349 4.12665C15.3089 4.12188 15.2829 4.13379 15.2695 4.15759C15.0937 4.47027 14.8989 4.87819 14.7626 5.19881C13.2253 4.96867 11.696 4.96867 10.1902 5.19881C10.0538 4.87106 9.85205 4.47027 9.67546 4.15759C9.66205 4.13458 9.63605 4.12268 9.61002 4.12665C8.18157 4.37267 6.81461 4.8036 5.53909 5.38929C5.52805 5.39405 5.51858 5.40199 5.5123 5.4123C2.91947 9.28593 2.20918 13.0644 2.55763 16.7959C2.5592 16.8142 2.56945 16.8317 2.58364 16.8428C4.29432 18.099 5.9514 18.8617 7.57771 19.3672C7.60374 19.3752 7.63131 19.3657 7.64788 19.3442C8.03258 18.8189 8.37551 18.2649 8.66954 17.6824C8.68689 17.6483 8.67033 17.6078 8.63486 17.5943C8.09092 17.388 7.57298 17.1364 7.07475 16.8507C7.03534 16.8277 7.03219 16.7713 7.06844 16.7443C7.17329 16.6658 7.27816 16.584 7.37827 16.5015C7.39638 16.4864 7.42162 16.4832 7.44292 16.4928C10.716 17.9871 14.2596 17.9871 17.4941 16.4928C17.5154 16.4824 17.5406 16.4856 17.5595 16.5007C17.6597 16.5832 17.7645 16.6658 17.8702 16.7443C17.9064 16.7713 17.904 16.8277 17.8646 16.8507C17.3664 17.1419 16.8485 17.388 16.3037 17.5935C16.2683 17.607 16.2525 17.6483 16.2698 17.6824C16.5702 18.2641 16.9131 18.818 17.2907 19.3434C17.3065 19.3657 17.3349 19.3752 17.3609 19.3672C18.9951 18.8617 20.6522 18.099 22.3628 16.8428C22.3778 16.8317 22.3873 16.815 22.3889 16.7967C22.8059 12.4826 21.6904 8.73517 19.4318 5.41309C19.4263 5.40199 19.4169 5.39405 19.4058 5.38929ZM9.15833 14.5238C8.17289 14.5238 7.36092 13.6191 7.36092 12.508C7.36092 11.3969 8.15715 10.4922 9.15833 10.4922C10.1674 10.4922 10.9715 11.4049 10.9557 12.508C10.9557 13.6191 10.1595 14.5238 9.15833 14.5238ZM15.8039 14.5238C14.8185 14.5238 14.0066 13.6191 14.0066 12.508C14.0066 11.3969 14.8028 10.4922 15.8039 10.4922C16.813 10.4922 17.6171 11.4049 17.6013 12.508C17.6013 13.6191 16.813 14.5238 15.8039 14.5238Z", + new Rect(0, 0, 25, 25) + )); // From Design System "Custom Icons" section on Figma - public static readonly IconValue Forum = new AvaloniaSvg("avares://NexusMods.App.UI/Assets/Icons/forum.svg"); + public static readonly IconValue Forum = new SimpleVectorIcon(new SimpleVectorIconImage( + "M15.4751 4.12561V11.1256H5.6451L4.4751 12.2956V4.12561H15.4751ZM16.4751 2.12561H3.4751C2.9251 2.12561 2.4751 2.57561 2.4751 3.12561V17.1256L6.4751 13.1256H16.4751C17.0251 13.1256 17.4751 12.6756 17.4751 12.1256V3.12561C17.4751 2.57561 17.0251 2.12561 16.4751 2.12561ZM21.4751 6.12561H19.4751V15.1256H6.4751V17.1256C6.4751 17.6756 6.9251 18.1256 7.4751 18.1256H18.4751L22.4751 22.1256V7.12561C22.4751 6.57561 22.0251 6.12561 21.4751 6.12561Z", + new Rect(0, 0, 25, 25) + )); // Custom Icon from Figma. The source of this icon is currently unknown. // Need to ask. - Sewer - public static readonly IconValue HardDrive = new AvaloniaSvg("avares://NexusMods.App.UI/Assets/Icons/disk_20px.svg"); + public static readonly IconValue HardDrive = new SimpleVectorIcon(new SimpleVectorIconImage( + "M3.33317 14.1665H16.6665V9.1665H3.33317V14.1665ZM14.1665 12.9165C14.5137 12.9165 14.8089 12.795 15.0519 12.5519C15.295 12.3089 15.4165 12.0137 15.4165 11.6665C15.4165 11.3193 15.295 11.0241 15.0519 10.7811C14.8089 10.538 14.5137 10.4165 14.1665 10.4165C13.8193 10.4165 13.5241 10.538 13.2811 10.7811C13.038 11.0241 12.9165 11.3193 12.9165 11.6665C12.9165 12.0137 13.038 12.3089 13.2811 12.5519C13.5241 12.795 13.8193 12.9165 14.1665 12.9165ZM18.3332 7.49984H15.979L14.3123 5.83317H5.68734L4.02067 7.49984H1.6665L4.52067 4.64567C4.67345 4.49289 4.85053 4.37484 5.05192 4.2915C5.25331 4.20817 5.46512 4.1665 5.68734 4.1665H14.3123C14.5346 4.1665 14.7464 4.20817 14.9478 4.2915C15.1491 4.37484 15.3262 4.49289 15.479 4.64567L18.3332 7.49984ZM3.33317 15.8332C2.87484 15.8332 2.48248 15.67 2.15609 15.3436C1.8297 15.0172 1.6665 14.6248 1.6665 14.1665V7.49984H18.3332V14.1665C18.3332 14.6248 18.17 15.0172 17.8436 15.3436C17.5172 15.67 17.1248 15.8332 16.6665 15.8332H3.33317Z", + new Rect(0, 0, 20, 20) + )); // The Black and White Nexus 'Developer' Logo. // This is the variation of the Nexus logo used in the App, and on the Discord. public static readonly IconValue Nexus = new AvaloniaSvg("avares://NexusMods.App.UI/Assets/nexus-logo-white.svg"); // From Design System "Custom Icons" section on Figma - public static readonly IconValue Stethoscope = new AvaloniaSvg("avares://NexusMods.App.UI/Assets/Icons/stethoscope_24px.svg"); + public static readonly IconValue Stethoscope = new SimpleVectorIcon(new SimpleVectorIconImage( + "M13.8943 22.8744C12.0943 22.8744 10.561 22.2411 9.29429 20.9744C8.02762 19.7077 7.39429 18.1744 7.39429 16.3744V15.7994C5.96095 15.5661 4.76929 14.8952 3.81929 13.7869C2.86929 12.6786 2.39429 11.3744 2.39429 9.87439V3.87439H5.39429V2.87439H7.39429V6.87439H5.39429V5.87439H4.39429V9.87439C4.39429 10.9744 4.78595 11.9161 5.56929 12.6994C6.35262 13.4827 7.29429 13.8744 8.39429 13.8744C9.49429 13.8744 10.436 13.4827 11.2193 12.6994C12.0026 11.9161 12.3943 10.9744 12.3943 9.87439V5.87439H11.3943V6.87439H9.39429V2.87439H11.3943V3.87439H14.3943V9.87439C14.3943 11.3744 13.9193 12.6786 12.9693 13.7869C12.0193 14.8952 10.8276 15.5661 9.39429 15.7994V16.3744C9.39429 17.6244 9.83179 18.6869 10.7068 19.5619C11.5818 20.4369 12.6443 20.8744 13.8943 20.8744C15.1443 20.8744 16.2068 20.4369 17.0818 19.5619C17.9568 18.6869 18.3943 17.6244 18.3943 16.3744V14.6994C17.811 14.4994 17.3318 14.1411 16.9568 13.6244C16.5818 13.1077 16.3943 12.5244 16.3943 11.8744C16.3943 11.0411 16.686 10.3327 17.2693 9.74939C17.8526 9.16606 18.561 8.87439 19.3943 8.87439C20.2276 8.87439 20.936 9.16606 21.5193 9.74939C22.1026 10.3327 22.3943 11.0411 22.3943 11.8744C22.3943 12.5244 22.2068 13.1077 21.8318 13.6244C21.4568 14.1411 20.9776 14.4994 20.3943 14.6994V16.3744C20.3943 18.1744 19.761 19.7077 18.4943 20.9744C17.2276 22.2411 15.6943 22.8744 13.8943 22.8744ZM19.3943 12.8744C19.6776 12.8744 19.9151 12.7786 20.1068 12.5869C20.2985 12.3952 20.3943 12.1577 20.3943 11.8744C20.3943 11.5911 20.2985 11.3536 20.1068 11.1619C19.9151 10.9702 19.6776 10.8744 19.3943 10.8744C19.111 10.8744 18.8735 10.9702 18.6818 11.1619C18.4901 11.3536 18.3943 11.5911 18.3943 11.8744C18.3943 12.1577 18.4901 12.3952 18.6818 12.5869C18.8735 12.7786 19.111 12.8744 19.3943 12.8744Z", + new Rect(0, 0, 25, 25) + )); // From Design System "Custom Icons" section on Figma - public static readonly IconValue ShieldHalfFull = new AvaloniaPathIcon( - Geometry.Parse( - "M21.4751 11.1256C21.4751 16.6756 17.6351 21.8656 12.4751 23.1256C7.3151 21.8656 3.4751 16.6756 3.4751 11.1256V5.12561L12.4751 1.12561L21.4751 5.12561V11.1256ZM12.4751 21.1256C16.2251 20.1256 19.4751 15.6656 19.4751 11.3456V6.42561L12.4751 3.30561V21.1256Z" - ) - ); - + public static readonly IconValue ShieldHalfFull = new SimpleVectorIcon(new SimpleVectorIconImage( + "M21.4751 11.1256C21.4751 16.6756 17.6351 21.8656 12.4751 23.1256C7.3151 21.8656 3.4751 16.6756 3.4751 11.1256V5.12561L12.4751 1.12561L21.4751 5.12561V11.1256ZM12.4751 21.1256C16.2251 20.1256 19.4751 15.6656 19.4751 11.3456V6.42561L12.4751 3.30561V21.1256Z", + new Rect(0, 0, 25, 25) + )); #endregion } diff --git a/src/NexusMods.Icons/SimpleVector/Control/SimpleVectorIcon.cs b/src/NexusMods.Icons/SimpleVector/Control/SimpleVectorIcon.cs new file mode 100644 index 0000000000..e7b77da5a9 --- /dev/null +++ b/src/NexusMods.Icons/SimpleVector/Control/SimpleVectorIcon.cs @@ -0,0 +1,39 @@ +using Avalonia; +using Avalonia.Controls.Documents; +using Avalonia.Media; + +namespace NexusMods.Icons.SimpleVector.Control; + +/// +/// A wrapper for designed to work with . +/// +public class SimpleVectorIcon : Avalonia.Controls.Image +{ + /// + /// Defines the property. + /// + public static readonly StyledProperty ForegroundProperty = + TextElement.ForegroundProperty.AddOwner(); + + /// + /// Gets or sets the brush used to draw the control's text and other foreground elements. + /// + public IBrush? Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + /// + public SimpleVectorIcon(SimpleVectorIconImage? image) => Source = image; + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == ForegroundProperty) + { + ((SimpleVectorIconImage)Source!).Brush = Foreground; + } + } +} diff --git a/src/NexusMods.Icons/SimpleVector/SimpleVectorIconImage.cs b/src/NexusMods.Icons/SimpleVector/SimpleVectorIconImage.cs new file mode 100644 index 0000000000..fca9796de2 --- /dev/null +++ b/src/NexusMods.Icons/SimpleVector/SimpleVectorIconImage.cs @@ -0,0 +1,189 @@ +using Avalonia; +using Avalonia.Media; +using JetBrains.Annotations; + +namespace NexusMods.Icons.SimpleVector; + +/// +/// Represents a simple SVG icon image that can be used in Avalonia applications. +/// +[PublicAPI] +public class SimpleVectorIconImage : DrawingImage, IImage +{ + private readonly Rect _viewBox; + private readonly GeometryDrawing _drawing; + + /// + /// Defines the property. + /// + public static readonly StyledProperty BrushProperty = AvaloniaProperty.Register< + SimpleVectorIconImage, + IBrush? + >(nameof(Brush), null); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PenProperty = AvaloniaProperty.Register< + SimpleVectorIconImage, + IPen? + >(nameof(Pen), null); + + /// + /// Initializes a new instance of the class with specified path data, view box, brush, and pen. + /// + /// The SVG path data for the icon. + /// The view box of the SVG icon. + /// The brush used to fill the icon. Can be null for no fill. + /// The pen used to stroke the icon. Can be null for no stroke. + public SimpleVectorIconImage(string pathData, Rect viewBox, IBrush? brush = null, IPen? pen = null) + { + _viewBox = viewBox; + + /* + TODO(Sewer): Write a TinyVG parser that directly feeds into StreamGeometry. + And pass the path as a byte span. This way we can avoid slow string parsing + and save on binary size. Will pick this up in my own time. Shouldn't be that hard. + + Also considered parsing out the SVG and feeding StreamGeometry commands + directly via source generator. However that just bloats the code, as + .NET is unable to compile time generate the resulting StreamGeometry. + */ + _drawing = new GeometryDrawing + { + Geometry = StreamGeometry.Parse(pathData), + Brush = brush, + Pen = pen, + }; + + Drawing = _drawing; + Brush = brush; + Pen = pen; + } + + /// + /// Initializes a new instance of the class with specified geometry, view box, brush, and pen. + /// + /// The StreamGeometry for the icon. + /// The view box of the SVG icon. + /// The brush used to fill the icon. Can be null for no fill. + /// The pen used to stroke the icon. Can be null for no stroke. + public SimpleVectorIconImage(StreamGeometry geometry, Rect viewBox, IBrush? brush = null, IPen? pen = null) + { + _viewBox = viewBox; + + _drawing = new GeometryDrawing + { + Geometry = geometry, + Brush = brush, + Pen = pen, + }; + + Drawing = _drawing; + Brush = brush; + Pen = pen; + } + + /// + /// Initializes a new instance of the class with specified path data and view box, using a black brush for fill. + /// + /// The SVG path data for the icon. + /// The view box of the SVG icon. + public SimpleVectorIconImage(string pathData, Rect viewBox) + : this(pathData, viewBox, new SolidColorBrush(0xFFFFFFFF)) + { + } + + /// + /// Gets the view box of the SVG icon. + /// + public Rect ViewBox => _viewBox; + + /// + /// Gets or sets the brush used to fill the icon. Can be null for no fill. + /// + public IBrush? Brush + { + get => GetValue(BrushProperty); + set => SetValue(BrushProperty, value); + } + + /// + /// Gets or sets the pen used to stroke the icon. Can be null for no stroke. + /// + public IPen? Pen + { + get => GetValue(PenProperty); + set => SetValue(PenProperty, value); + } + + /// + /// Gets the size of the icon. + /// + public new Size Size => _viewBox.Size; + + /// + Size IImage.Size => _viewBox.Size; + + /// + /// Called when a property value changes. + /// + /// A description of the property change. + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == BrushProperty) + { + _drawing.Brush = Brush; + RaiseInvalidated(EventArgs.Empty); + } + else if (change.Property == PenProperty) + { + _drawing.Pen = Pen; + RaiseInvalidated(EventArgs.Empty); + } + } + + /// + void IImage.Draw(DrawingContext context, Rect sourceRect, Rect destRect) + { + var bounds = _viewBox; + 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 var clip = context.PushClip(destRect); + using var state = context.PushTransform(translate * scale); + _drawing.Draw(context); + } + + /// + /// Creates a new SimpleVectorIconImage that is a copy of the current instance. + /// + /// A new SimpleVectorIconImage with the same properties as this instance. + /// + /// The purpose of this method is to create a distinct instance of the + /// current object whose visual properties (such as colour) can be modified + /// without affecting the original object. + /// + /// (Assuming, that the properties, such as brushes are mutated by assigning + /// a new instance to the property, which is standard for XAML-like frameworks) + /// + /// The actual geometry of the icon is reused, thus we skip the parsing + /// overhead. + /// + public SimpleVectorIconImage Clone() + { + return new SimpleVectorIconImage( + (_drawing.Geometry as StreamGeometry)!, + _viewBox, + Brush, + Pen + ); + } +} diff --git a/src/NexusMods.Icons/UnifiedIcon.cs b/src/NexusMods.Icons/UnifiedIcon.cs index e3f8536795..2e436dc48a 100644 --- a/src/NexusMods.Icons/UnifiedIcon.cs +++ b/src/NexusMods.Icons/UnifiedIcon.cs @@ -122,7 +122,8 @@ private void UpdateControl(IconValue? value) [MaxHeightProperty] = this[MaxHeightProperty], [MaxWidthProperty] = this[MaxWidthProperty], [ForegroundProperty] = this[ForegroundProperty], - } + }, + f5: simpleVectorIcon => new NexusMods.Icons.SimpleVector.Control.SimpleVectorIcon(simpleVectorIcon.Image.Clone()) ); if (innerControl is null) return;