+ Watermark;

This commit is contained in:
zeroone 2024-09-03 00:41:03 +08:00
parent 3521c3da11
commit 3036c31711
11 changed files with 466 additions and 0 deletions

View File

@ -31,6 +31,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{5EB2524C
build\Output.Local.props = build\Output.Local.props build\Output.Local.props = build\Output.Local.props
build\Output.props = build\Output.props build\Output.props = build\Output.props
build\Version.props = build\Version.props build\Version.props = build\Version.props
build\Directory.Build.targets = build\Directory.Build.targets
EndProjectSection EndProjectSection
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{279BC2C9-A818-4D6C-9274-678649932057}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{279BC2C9-A818-4D6C-9274-678649932057}"

View File

@ -0,0 +1,8 @@
<Project>
<!-- https://github.com/dotnet/sdk/issues/22515-->
<ItemGroup>
<None Remove="*.csproj.DotSettings" />
</ItemGroup>
</Project>

View File

@ -25,6 +25,7 @@
</showcase:ShowCaseItem> </showcase:ShowCaseItem>
<showcase:ShowCaseItem <showcase:ShowCaseItem
atom:Watermark.Glyph="{atom:TextGlyph 'Atom UI', Space=100, Angle=30, Opacity=0.5, UseMirror=False, UseCross=True}"
Title="Button Shape" Title="Button Shape"
Description="Supported button shape display, such as primary, default, dashed and Text, etc."> Description="Supported button shape display, such as primary, default, dashed and Text, etc.">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">

View File

@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermark/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermark_005Cglyphs/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -0,0 +1,75 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.VisualTree;
// ReSharper disable SuggestBaseTypeForParameter
namespace AtomUI.Controls.Primitives
{
public static class AtomLayerExtension
{
public static AtomLayer? GetAxLayer(this Visual? visual)
{
return visual == null ? null : AtomLayer.GetLayer(visual);
}
}
public class AtomLayer : Panel
{
private AtomLayer()
{
}
public static AtomLayer? GetLayer(Visual visual)
{
if (visual.GetVisualRoot() is not TopLevel topLevel)
{
return null;
}
var layer = topLevel.GetVisualChildren().FirstOrDefault(c => c is AtomLayer) as AtomLayer;
layer ??= TryInject(topLevel);
return layer;
}
private static AtomLayer TryInject(TopLevel topLevel)
{
var layer = new AtomLayer();
if (topLevel.IsLoaded == false)
{
topLevel.Loaded += (sender, args) =>
{
InjectCore(topLevel, layer);
};
}
else
{
InjectCore(topLevel, layer);
}
return layer;
}
private static void InjectCore(TopLevel topLevel, AtomLayer layer)
{
if (topLevel.GetVisualChildren() is not IList<Visual> visualChildren)
{
return;
}
if (visualChildren.Any(c => c is AtomLayer))
{
return;
}
layer.HorizontalAlignment = HorizontalAlignment.Stretch;
layer.VerticalAlignment = VerticalAlignment.Stretch;
layer.InheritanceParent = topLevel;
visualChildren.Add(layer);
topLevel.InvalidateMeasure();
}
}
}

View File

@ -0,0 +1,59 @@
using Avalonia;
namespace AtomUI.Controls.Utils
{
public static class MatrixUtil
{
public static void ScaleAt(ref this Matrix matrix, double scaleX, double scaleY, double centerX, double centerY)
{
matrix *= CreateScaling(scaleX, scaleY, centerX, centerY);
}
public static void Translate(ref this Matrix matrix, double offsetX, double offsetY)
{
matrix = new Matrix(matrix.M11, matrix.M12, matrix.M21, matrix.M22, matrix.M31 + offsetX, matrix.M32 + offsetY);
}
public static void RotateAt(ref this Matrix matrix, double angle, double centerX, double centerY)
{
matrix *= CreateRotationRadians(angle, centerX, centerY);
}
public static Matrix CreateRotationRadians(double angle, double centerX, double centerY)
{
var m12 = Math.Sin(angle);
var num = Math.Cos(angle);
var offsetX = centerX * (1.0 - num) + centerY * m12;
var offsetY = centerY * (1.0 - num) - centerX * m12;
return new Matrix(num, m12, -m12, num, offsetX, offsetY);
}
private static Matrix CreateScaling(double scaleX, double scaleY, double centerX, double centerY)
{
return new Matrix(scaleX, 0.0, 0.0, scaleY, centerX - scaleX * centerX, centerY - scaleY * centerY);
}
private static Matrix CreateScaling(double scaleX, double scaleY)
{
return new Matrix(scaleX, 0.0, 0.0, scaleY, 0.0, 0.0);
}
private static Matrix CreateSkewRadians(double skewX, double skewY)
{
return new Matrix(1.0, Math.Tan(skewY), Math.Tan(skewX), 1.0, 0.0, 0.0);
}
private static Matrix CreateTranslation(double offsetX, double offsetY)
{
return new Matrix(1.0, 0.0, 0.0, 1.0, offsetX, offsetY);
}
private static Matrix CreateIdentity()
{
return new Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
}
}
}

View File

@ -0,0 +1,53 @@
using System.Globalization;
using Avalonia;
using Avalonia.Media;
namespace AtomUI.Controls;
public class TextGlyph : WatermarkGlyph
{
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public static readonly StyledProperty<string?> TextProperty = AvaloniaProperty
.Register<TextGlyph, string?>(nameof(Text));
protected FormattedText? FormattedText { get; private set; }
static TextGlyph()
{
TextProperty.Changed.AddClassHandler<TextGlyph>((glyph, args) =>
{
glyph.RebuildFormatText();
});
}
private void RebuildFormatText()
{
if (Text == null)
{
FormattedText = null;
return;
}
// TODO Expose properties.
FormattedText = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Typeface.Default, 15, Brushes.Gray);
}
public override void Render(DrawingContext context)
{
if (FormattedText == null)
{
return;
}
context.DrawText(FormattedText, new Point());
}
public override Size GetDesiredSize()
{
return FormattedText == null ? new Size() : new Size(FormattedText.Width, FormattedText.Height);
}
}

View File

@ -0,0 +1,23 @@
namespace AtomUI.Controls;
public class TextGlyphExtension : WatermarkGlyphExtension
{
private string? Text { get; }
public TextGlyphExtension(string text)
{
Text = text;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
var glyph = new TextGlyph()
{
Text = Text,
};
SetProperties(glyph);
return glyph;
}
}

View File

@ -0,0 +1,25 @@
using Avalonia.Markup.Xaml;
namespace AtomUI.Controls;
public abstract class WatermarkGlyphExtension : MarkupExtension
{
public double Space { get; set; }
public double Angle { get; set; }
public double Opacity { get; set; }
public bool UseMirror { get; set; }
public bool UseCross { get; set; }
protected void SetProperties(WatermarkGlyph glyph)
{
glyph.Space = Space;
glyph.Angle = Angle;
glyph.Opacity = Opacity;
glyph.UseMirror = UseMirror;
glyph.UseCross = UseCross;
}
}

View File

@ -0,0 +1,167 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using AtomUI.Controls.Primitives;
using AtomUI.Controls.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace AtomUI.Controls;
public sealed class Watermark : Control
{
public static WatermarkGlyph? GetGlyph(Visual element)
{
return element.GetValue(GlyphProperty);
}
public static void SetGlyph(Visual element, WatermarkGlyph? value)
{
element.SetValue(GlyphProperty, value);
}
public static readonly AttachedProperty<WatermarkGlyph?> GlyphProperty = AvaloniaProperty
.RegisterAttached<Watermark, Visual, WatermarkGlyph?>("Glyph");
private Visual? Target { get; init; }
private WatermarkGlyph? Glyph { get; init; }
static Watermark()
{
GlyphProperty.Changed.AddClassHandler<Visual>(OnGlyphChanged);
}
private Watermark()
{
IsHitTestVisibleProperty.OverrideMetadata<Watermark>(new StyledPropertyMetadata<bool>(false));
}
private static void OnGlyphChanged(Visual target, AvaloniaPropertyChangedEventArgs arg)
{
if (target.IsAttachedToVisualTree())
{
InstallWatermark(target);
}
else
{
target.AttachedToVisualTree += TargetOnAttachedToVisualTree;
}
}
private static void TargetOnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
if (sender is not Visual target)
{
return;
}
target.AttachedToVisualTree -= TargetOnAttachedToVisualTree;
InstallWatermark(target);
}
private static void InstallWatermark(Visual target)
{
if (CheckLayer(target, out var layer) == false)
{
return;
}
var watermark = layer.Children.OfType<Watermark>().FirstOrDefault(w => w.Target == target);
if (watermark != null)
{
return;
}
watermark = new Watermark
{
Target = target,
Glyph = GetGlyph(target),
};
layer.Children.Add(watermark);
}
private static bool CheckLayer(Visual target, [NotNullWhen(true)] out AtomLayer? layer)
{
layer = target.GetAxLayer();
if (layer == null)
{
Trace.WriteLine($"Can not get AxLayer for {target} to show a watermark.");
}
return layer != null;
}
public override void Render(DrawingContext context)
{
base.Render(context);
if (Target == null || Glyph == null)
{
return;
}
if (CheckLayer(Target, out var layer) == false)
{
return;
}
var matrix = Target.TransformToVisual(layer);
if (matrix == null)
{
return;
}
var size = Glyph.GetDesiredSize();
if (size.Width == 0 || size.Height == 0)
{
return;
}
using (context.PushTransform(matrix.Value))
using (context.PushClip(new Rect(Target.Bounds.Size)))
using (context.PushOpacity(Glyph.Opacity))
{
var t = 0d;
var r = 0;
while (t < Target.Bounds.Height)
{
var pushState = new DrawingContext.PushedState();
if (r % 2 == 1 && Glyph.UseCross)
{
pushState = context.PushTransform(Matrix.CreateTranslation((Glyph.Space - size.Width) / 2 + size.Width, 0));
}
using (pushState)
{
var l = 0d;
var c = 0;
while (l < Target.Bounds.Width)
{
var angle = Glyph.Angle;
if (c % 2 == 1 && Glyph.UseMirror)
{
angle = -angle;
}
var m = MatrixUtil.CreateRotationRadians(angle * Math.PI / 180, size.Width / 2, size.Height / 2);
using (context.PushTransform(Matrix.CreateTranslation(l, t)))
using (context.PushTransform(m))
{
Glyph.Render(context);
}
l += size.Width + Glyph.Space;
c++;
}
t += size.Height + Glyph.Space;
r++;
}
}
}
}
}

View File

@ -0,0 +1,51 @@
using Avalonia;
using Avalonia.Media;
namespace AtomUI.Controls;
public abstract class WatermarkGlyph : AvaloniaObject
{
public double Space
{
get => GetValue(SpaceProperty);
set => SetValue(SpaceProperty, value);
}
public static readonly StyledProperty<double> SpaceProperty = AvaloniaProperty
.Register<WatermarkGlyph, double>(nameof(Space));
public double Angle
{
get => GetValue(AngleProperty);
set => SetValue(AngleProperty, value);
}
public static readonly StyledProperty<double> AngleProperty = AvaloniaProperty
.Register<WatermarkGlyph, double>(nameof(Angle));
public double Opacity
{
get => GetValue(OpacityProperty);
set => SetValue(OpacityProperty, value);
}
public static readonly StyledProperty<double> OpacityProperty = AvaloniaProperty
.Register<WatermarkGlyph, double>(nameof(Opacity));
public bool UseMirror
{
get => GetValue(UseMirrorProperty);
set => SetValue(UseMirrorProperty, value);
}
public static readonly StyledProperty<bool> UseMirrorProperty = AvaloniaProperty
.Register<WatermarkGlyph, bool>(nameof(UseMirror));
public bool UseCross
{
get => GetValue(UseCrossProperty);
set => SetValue(UseCrossProperty, value);
}
public static readonly StyledProperty<bool> UseCrossProperty = AvaloniaProperty
.Register<WatermarkGlyph, bool>(nameof(UseCross));
public abstract void Render(DrawingContext context);
public abstract Size GetDesiredSize();
}