From 6c307a6ccc67a0e385a2da3e6a49be0a486d5d62 Mon Sep 17 00:00:00 2001
From: NaBian <836904362@qq.com>
Date: Sat, 13 Jun 2026 20:14:53 +0800
Subject: [PATCH] feat: Add Gravatar control and demo.
---
.../UserControl/Controls/GravatarDemo.axaml | 34 +++++
.../Controls/GravatarDemo.axaml.cs | 9 ++
.../Controls/HoneycombPanelDemo.axaml | 28 ++---
.../Controls/Base/IGravatarGenerator.cs | 6 +
.../Controls/Other/Gravatar.cs | 58 +++++++++
.../Themes/Styles/Gravatar.axaml | 57 +++++++++
.../Themes/Styles/ListBox.axaml | 34 ++++-
.../Themes/Styles/MergedStyles.axaml | 1 +
.../Generator/GithubGravatarGenerator.cs | 119 ++++++++++++++++++
9 files changed, 329 insertions(+), 17 deletions(-)
create mode 100644 src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GravatarDemo.axaml
create mode 100644 src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GravatarDemo.axaml.cs
create mode 100644 src/Avalonia/HandyControl_Avalonia/Controls/Base/IGravatarGenerator.cs
create mode 100644 src/Avalonia/HandyControl_Avalonia/Controls/Other/Gravatar.cs
create mode 100644 src/Avalonia/HandyControl_Avalonia/Themes/Styles/Gravatar.axaml
create mode 100644 src/Avalonia/HandyControl_Avalonia/Tools/Generator/GithubGravatarGenerator.cs
diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GravatarDemo.axaml b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GravatarDemo.axaml
new file mode 100644
index 00000000..635b6154
--- /dev/null
+++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GravatarDemo.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GravatarDemo.axaml.cs b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GravatarDemo.axaml.cs
new file mode 100644
index 00000000..fb905955
--- /dev/null
+++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/GravatarDemo.axaml.cs
@@ -0,0 +1,9 @@
+namespace HandyControlDemo.UserControl;
+
+public partial class GravatarDemo : Avalonia.Controls.UserControl
+{
+ public GravatarDemo()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/HoneycombPanelDemo.axaml b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/HoneycombPanelDemo.axaml
index 4f1dce0f..cb7bcaf8 100644
--- a/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/HoneycombPanelDemo.axaml
+++ b/src/Avalonia/HandyControlDemo_Avalonia/UserControl/Controls/HoneycombPanelDemo.axaml
@@ -23,27 +23,23 @@
-
-
+
+
-
-
+
+
-
-
-
+
-
-
+
+
diff --git a/src/Avalonia/HandyControl_Avalonia/Controls/Base/IGravatarGenerator.cs b/src/Avalonia/HandyControl_Avalonia/Controls/Base/IGravatarGenerator.cs
new file mode 100644
index 00000000..054505af
--- /dev/null
+++ b/src/Avalonia/HandyControl_Avalonia/Controls/Base/IGravatarGenerator.cs
@@ -0,0 +1,6 @@
+namespace HandyControl.Controls;
+
+public interface IGravatarGenerator
+{
+ object? GetGravatar(string id);
+}
diff --git a/src/Avalonia/HandyControl_Avalonia/Controls/Other/Gravatar.cs b/src/Avalonia/HandyControl_Avalonia/Controls/Other/Gravatar.cs
new file mode 100644
index 00000000..5c3f4296
--- /dev/null
+++ b/src/Avalonia/HandyControl_Avalonia/Controls/Other/Gravatar.cs
@@ -0,0 +1,58 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using HandyControl.Tools;
+
+namespace HandyControl.Controls;
+
+public class Gravatar : ContentControl
+{
+ public static readonly StyledProperty GeneratorProperty =
+ AvaloniaProperty.Register(
+ nameof(Generator), defaultValue: new GithubGravatarGenerator());
+
+ public IGravatarGenerator Generator
+ {
+ get => GetValue(GeneratorProperty);
+ set => SetValue(GeneratorProperty, value);
+ }
+
+ public static readonly StyledProperty IdProperty =
+ AvaloniaProperty.Register(nameof(Id));
+
+ public string? Id
+ {
+ get => GetValue(IdProperty);
+ set => SetValue(IdProperty, value);
+ }
+
+ public static readonly StyledProperty SourceProperty =
+ AvaloniaProperty.Register(nameof(Source));
+
+ public IImage? Source
+ {
+ get => GetValue(SourceProperty);
+ set => SetValue(SourceProperty, value);
+ }
+
+ static Gravatar()
+ {
+ IdProperty.Changed.AddClassHandler((o, e) => o.OnIdChanged(e));
+ SourceProperty.Changed.AddClassHandler((o, e) => o.OnSourceChanged(e));
+ }
+
+ private void OnIdChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (Source != null) return;
+ Content = Generator.GetGravatar((string?) e.NewValue ?? string.Empty);
+ }
+
+ private void OnSourceChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var v = (IImage?) e.NewValue;
+
+ Background = v is IImageBrushSource brushSource
+ ? new ImageBrush(brushSource) { Stretch = Stretch.UniformToFill }
+ : ResourceHelper.GetResource(Data.ResourceToken.SecondaryRegionBrush);
+ }
+}
diff --git a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/Gravatar.axaml b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/Gravatar.axaml
new file mode 100644
index 00000000..19438329
--- /dev/null
+++ b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/Gravatar.axaml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ListBox.axaml b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ListBox.axaml
index 492ebf11..6eec75bf 100644
--- a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ListBox.axaml
+++ b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/ListBox.axaml
@@ -1,4 +1,4 @@
-
@@ -118,4 +118,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml
index d0b13dde..ecd1b482 100644
--- a/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml
+++ b/src/Avalonia/HandyControl_Avalonia/Themes/Styles/MergedStyles.axaml
@@ -60,6 +60,7 @@
+
diff --git a/src/Avalonia/HandyControl_Avalonia/Tools/Generator/GithubGravatarGenerator.cs b/src/Avalonia/HandyControl_Avalonia/Tools/Generator/GithubGravatarGenerator.cs
new file mode 100644
index 00000000..b4fba9c5
--- /dev/null
+++ b/src/Avalonia/HandyControl_Avalonia/Tools/Generator/GithubGravatarGenerator.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Globalization;
+using System.Security.Cryptography;
+using System.Text;
+using Avalonia;
+using Avalonia.Controls.Shapes;
+using Avalonia.Layout;
+using Avalonia.Media;
+using HandyControl.Controls;
+
+namespace HandyControl.Tools;
+
+public class GithubGravatarGenerator : IGravatarGenerator
+{
+ private const int RenderDataMaxLength = 15;
+
+ public object? GetGravatar(string id)
+ {
+ if (string.IsNullOrEmpty(id)) return null;
+
+ var hashcode = GetHashCode(id);
+ var renderData = GetRenderData(hashcode);
+ var renderBrush = GetRenderBrush(hashcode);
+ var geometryGroup = new GeometryGroup();
+
+ void AddRec(int i, int j, bool hidden = false)
+ {
+ var rec = new RectangleGeometry(new Rect(new Point(i, j), hidden ? new Size() : new Size(1, 1)));
+ geometryGroup.Children.Add(rec);
+ }
+
+ var index = 0;
+ for (var i = 0; i < 2; i++)
+ {
+ for (var j = 0; j < 5; j++, index++)
+ {
+ AddRec(i, j, renderData[index] == 0);
+ }
+ }
+
+ for (var j = 0; j < 5; j++, index++)
+ {
+ AddRec(2, j, renderData[index] == 0);
+ }
+
+ index -= 10;
+ for (var i = 3; i < 5; i++)
+ {
+ for (var j = 0; j < 5; j++, index++)
+ {
+ AddRec(i, j, renderData[index] == 0);
+ }
+ index -= 10;
+ }
+
+ var path = new Path
+ {
+ Data = geometryGroup,
+ Fill = renderBrush,
+ VerticalAlignment = VerticalAlignment.Top,
+ Stretch = Stretch.Uniform
+ };
+
+ return path;
+ }
+
+ private string GetHashCode(string id)
+ {
+ var bytes = Encoding.ASCII.GetBytes(id);
+ var hash = MD5.HashData(bytes);
+ var stringBuilder = new StringBuilder();
+ foreach (var item in hash)
+ {
+ stringBuilder.Append(item.ToString("X2"));
+ }
+ return stringBuilder.ToString();
+ }
+
+ private int[] GetRenderData(string hashcode)
+ {
+ var arr = new int[RenderDataMaxLength];
+ for (var i = 0; i < RenderDataMaxLength; i++)
+ {
+ var c = hashcode[i];
+ var v = (int) c;
+ arr[i] = v % 2;
+ }
+
+ return arr;
+ }
+
+ private IBrush GetRenderBrush(string hashcode)
+ {
+ var v = (double) int.Parse(hashcode.Substring(hashcode.Length - 7), NumberStyles.HexNumber);
+ var scale = v / 0xfffffff;
+ var color = Hsl2Rgb(scale);
+ return new SolidColorBrush(color);
+ }
+
+ //adapted from https://github.com/stewartlord/identicon.js/blob/4fad7cafec1b7a4d896015e084e861625ef5d64f/identicon.js#L110
+ private Color Hsl2Rgb(double h, double s = 0.7, double b = 0.5)
+ {
+ h *= 6;
+ var arr = new[]
+ {
+ b += s *= b < .5 ? b : 1 - b,
+ b - h % 1 * s * 2,
+ b -= s *= 2,
+ b,
+ b + h % 1 * s,
+ b + s
+ };
+
+ var hValue = (int) Math.Floor(h);
+
+ return Color.FromRgb((byte) (arr[hValue % 6] * 255), (byte) (arr[(hValue | 16) % 6] * 255),
+ (byte) (arr[(hValue | 8) % 6] * 255));
+ }
+}