Parcourir la source

Anti-aliasing wip

flabbet il y a 10 mois
Parent
commit
874bd43d4d

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 63d856f0bf2240882a7a27cf256fc64ee2323a99
+Subproject commit 9cea0477f1cdd0a6ddd7f55e47d17fd90175cabd

+ 20 - 42
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -6,6 +6,7 @@ using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+
 internal class LineBasedPen_UpdateableChange : UpdateableChange
 {
     private readonly Guid memberGuid;
@@ -13,6 +14,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     private int strokeWidth;
     private readonly bool replacing;
     private readonly bool drawOnMask;
+    private readonly bool antiAliasing;
     private readonly Paint srcPaint = new Paint() { BlendMode = BlendMode.Src };
 
     private CommittedChunkStorage? storedChunks;
@@ -21,16 +23,18 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
 
     [GenerateUpdateableChangeActions]
     public LineBasedPen_UpdateableChange(Guid memberGuid, Color color, VecI pos, int strokeWidth, bool replacing,
+        bool antiAliasing,
         bool drawOnMask, int frame)
     {
         this.memberGuid = memberGuid;
         this.color = color;
         this.strokeWidth = strokeWidth;
         this.replacing = replacing;
+        this.antiAliasing = antiAliasing;
         this.drawOnMask = drawOnMask;
         points.Add(pos);
         this.frame = frame;
-}
+    }
 
     [UpdateChangeMethod]
     public void Update(VecI pos, int strokeWidth)
@@ -49,6 +53,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         if (!replacing)
             image.SetBlendMode(BlendMode.SrcOver);
         DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, image, memberGuid, drawOnMask);
+        srcPaint.IsAntiAliased = antiAliasing;
         return true;
     }
 
@@ -60,25 +65,13 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
 
         int opCount = image.QueueLength;
 
-        if (strokeWidth == 1)
-        {
-            image.EnqueueDrawBresenhamLine(from, to, color, BlendMode.Src);
-        }
-        else if (strokeWidth <= 10)
+        var bresenham = BresenhamLineHelper.GetBresenhamLine(from, to);
+        foreach (var point in bresenham)
         {
-            var bresenham = BresenhamLineHelper.GetBresenhamLine(from, to);
-            foreach (var point in bresenham)
-            {
-                var rect = new RectI(point - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-                image.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
-            }
-        }
-        else
-        {
-            var rect = new RectI(to - new VecI(strokeWidth / 2), new VecI(strokeWidth));
+            var rect = new RectI(point - new VecI(strokeWidth / 2), new VecI(strokeWidth));
             image.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
-            image.EnqueueDrawSkiaLine(from, to, StrokeCap.Butt, strokeWidth, color, BlendMode.Src);
         }
+
         var affChunks = image.FindAffectedArea(opCount);
 
         return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affChunks, drawOnMask);
@@ -88,37 +81,20 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     {
         if (points.Count == 1)
         {
-            if (strokeWidth == 1)
-            {
-                targetImage.EnqueueDrawBresenhamLine(points[0], points[0], color, BlendMode.Src);
-            }
-            else
-            {
-                var rect = new RectI(points[0] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-                targetImage.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
-            }
+            var rect = new RectI(points[0] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
+            targetImage.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
             return;
         }
 
-        var firstRect = new RectI(points[0] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-        targetImage.EnqueueDrawEllipse(firstRect, color, color, 1, 0, srcPaint);
-
-        for (int i = 1; i < points.Count; i++)
+        for (int i = 0; i < points.Count; i++)
         {
-            if (strokeWidth == 1)
-            {
-                targetImage.EnqueueDrawBresenhamLine(points[i - 1], points[i], color, BlendMode.Src);
-            }
-            else
-            {
-                var rect = new RectI(points[i] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
-                targetImage.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
-                targetImage.EnqueueDrawSkiaLine(points[i - 1], points[i], StrokeCap.Butt, strokeWidth, color, BlendMode.Src);
-            }
+            var rect = new RectI(points[i] - new VecI(strokeWidth / 2), new VecI(strokeWidth));
+            targetImage.EnqueueDrawEllipse(rect, color, color, 1, 0, srcPaint);
         }
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         if (storedChunks is not null)
             throw new InvalidOperationException("Trying to save chunks while there are saved chunks already");
@@ -150,7 +126,9 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
-        var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, frame, ref storedChunks);
+        var affected =
+            DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, memberGuid, drawOnMask, frame,
+                ref storedChunks);
         return DrawingChangeHelper.CreateAreaChangeInfo(memberGuid, affected, drawOnMask);
     }
 

+ 6 - 1
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -5,7 +5,12 @@
       "MoveViewport",
       "RotateViewport",
       "Move",
-      "Pen",
+      {
+        "ToolName": "Pen",
+        "Settings": {
+          "ExposePixelPerfectEnabled": true
+        }
+      },
       "Select",
       "MagicWand",
       "Lasso",

+ 2 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs

@@ -41,7 +41,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
 
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         IAction? action = new LineBasedPen_Action(guidValue, Colors.Transparent, controller!.LastPixelPosition, toolSize, true,
-            drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+            false, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
 
         return ExecutionState.Success;
@@ -49,7 +49,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
 
     public override void OnPixelPositionChange(VecI pos)
     {
-        IAction? action = new LineBasedPen_Action(guidValue, Colors.Transparent, pos, toolSize, true, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+        IAction? action = new LineBasedPen_Action(guidValue, Colors.Transparent, pos, toolSize, true, false, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
     }
 

+ 4 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs

@@ -19,6 +19,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     public int ToolSize => basicToolbar.ToolSize;
     private bool drawOnMask;
     private bool pixelPerfect;
+    private bool antiAliasing;
 
     private IBasicToolbar basicToolbar;
 
@@ -40,11 +41,12 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         guidValue = member.Id;
         color = colorsHandler.PrimaryColor;
         pixelPerfect = penTool.PixelPerfectEnabled;
+        antiAliasing = penTool.AntiAliasing;
 
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
         IAction? action = pixelPerfect switch
         {
-            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, ToolSize, false, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
+            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, ToolSize, false, antiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
             true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         internals!.ActionAccumulator.AddActions(action);
@@ -56,7 +58,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     {
         IAction? action = pixelPerfect switch
         {
-            false => new LineBasedPen_Action(guidValue, color, pos, ToolSize, false, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
+            false => new LineBasedPen_Action(guidValue, color, pos, ToolSize, false, antiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
             true => new PixelPerfectPen_Action(guidValue, pos, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         internals!.ActionAccumulator.AddActions(action);

+ 1 - 0
src/PixiEditor/Models/Handlers/IToolHandler.cs

@@ -62,4 +62,5 @@ internal interface IToolHandler : IHandler
 
     public void OnDeselecting();
     public void SetToolSetSettings(IToolSetHandler toolset, Dictionary<string, object>? settings);
+    public void ApplyToolSetSettings(IToolSetHandler toolset);
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/IToolSetHandler.cs

@@ -4,4 +4,5 @@ internal interface IToolSetHandler : IHandler
 {
     public string Name { get; }
     public ICollection<IToolHandler> Tools { get; }
+    public void ApplyToolSetSettings();
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/Tools/IPenToolHandler.cs

@@ -3,4 +3,5 @@
 internal interface IPenToolHandler : IToolHandler
 {
     public bool PixelPerfectEnabled { get; }
+    public bool AntiAliasing { get; }
 }

+ 15 - 5
src/PixiEditor/ViewModels/SubViewModels/ToolSetViewModel.cs

@@ -9,14 +9,24 @@ internal class ToolSetViewModel : PixiObservableObject, IToolSetHandler
 {
     public string Name { get; }
     ICollection<IToolHandler> IToolSetHandler.Tools => Tools;
+
     public ObservableCollection<IToolHandler> Tools { get; } = new();
-    
-    public ToolSetViewModel(string setName, List<IToolHandler> tools)
+
+    public ToolSetViewModel(string setName)
     {
         Name = setName;
-        foreach (var tool in tools)
+    }
+
+    public void AddTool(IToolHandler tool)
+    {
+        Tools.Add(tool);
+    }
+
+    public void ApplyToolSetSettings()
+    {
+        foreach (IToolHandler tool in Tools)
         {
-            Tools.Add(tool);
-        }    
+            tool.ApplyToolSetSettings(this);
+        }
     }
 }

+ 3 - 3
src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -131,6 +131,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     public void SetActiveToolSet(IToolSetHandler toolSetHandler)
     {
         ActiveToolSet = toolSetHandler;
+        ActiveToolSet.ApplyToolSetSettings();
         UpdateEnabledState();
     }
 
@@ -423,8 +424,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     {
         foreach (ToolSetConfig toolSet in toolSetConfig)
         {
-            List<IToolHandler> tools = new List<IToolHandler>();
-            var toolSetViewModel = new ToolSetViewModel(toolSet.Name, tools);
+            var toolSetViewModel = new ToolSetViewModel(toolSet.Name);
     
             foreach (var toolFromToolset in toolSet.Tools)
             {
@@ -440,7 +440,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
                     continue;
                 }
 
-                tools.Add(tool);
+                toolSetViewModel.AddTool(tool);
             }
 
             AllToolSets.Add(toolSetViewModel);

+ 41 - 1
src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/Setting.cs

@@ -47,6 +47,13 @@ internal abstract class Setting<T> : Setting
 internal abstract class Setting : ObservableObject
 {
     private object _value;
+    private bool isExposed = true;
+    
+    private bool overwrittenExposed;
+    private object overwrittenValue;
+
+    private bool hasOverwrittenValue;
+    private bool hasOverwrittenExposed;
     
     protected Setting(string name)
     {
@@ -57,7 +64,7 @@ internal abstract class Setting : ObservableObject
 
     public object Value
     {
-        get => _value;
+        get => hasOverwrittenValue ? overwrittenValue : _value;
         set
         {
             var old = _value;
@@ -68,6 +75,12 @@ internal abstract class Setting : ObservableObject
         }
     }
 
+    public bool IsExposed
+    {
+        get => hasOverwrittenExposed ? overwrittenExposed : isExposed;
+        set => SetProperty(ref isExposed, value);
+    }
+
     public string Name { get; }
 
     public LocalizedString Label { get; set; }
@@ -75,4 +88,31 @@ internal abstract class Setting : ObservableObject
     public bool HasLabel => !string.IsNullOrEmpty(Label);
 
     public abstract Type GetSettingType();
+    
+    public void SetOverwriteValue(object value)
+    {
+        overwrittenValue = value;
+        hasOverwrittenValue = true;
+        
+        OnPropertyChanged(nameof(Value));
+    }
+    
+    public void SetOverwriteExposed(bool value)
+    {
+        overwrittenExposed = value;
+        hasOverwrittenExposed = true;
+        
+        OnPropertyChanged(nameof(IsExposed));
+    }
+    
+    public void ResetOverwrite()
+    {
+        overwrittenValue = null;
+        overwrittenExposed = false;
+        hasOverwrittenValue = false;
+        hasOverwrittenExposed = false;
+        
+        OnPropertyChanged(nameof(Value));
+        OnPropertyChanged(nameof(IsExposed));
+    }
 }

+ 2 - 0
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/SettingAttributes.cs

@@ -73,6 +73,8 @@ public static class Settings
         public string Name { get; set; }
         
         public string Notify { get; set; }
+        
+        public bool ExposedByDefault { get; set; } = true;
 
         public SettingsAttribute() { }
         

+ 8 - 3
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/ToolbarFactory.cs

@@ -21,6 +21,7 @@ internal static class ToolbarFactory
 
             var name = attribute.Name ?? property.Name;
             var label = attribute.LabelKey ?? name;
+            bool exposedByDefault = attribute.ExposedByDefault;
 
             if (attribute is Settings.InheritedAttribute)
             {
@@ -28,7 +29,7 @@ internal static class ToolbarFactory
             }
             else
             {
-                var setting = CreateSetting(property.PropertyType, name, attribute, label);
+                var setting = CreateSetting(property.PropertyType, name, attribute, label, exposedByDefault);
                 AddValueChangedHandlerIfRequired(toolType, tool, setting, attribute);
                 toolbar.AddSetting(setting);
             }
@@ -51,9 +52,9 @@ internal static class ToolbarFactory
     }
 
     private static Setting CreateSetting(Type propertyType, string name, Settings.SettingsAttribute attribute,
-        string label)
+        string label, bool exposedByDefault)
     {
-        return attribute switch
+        var attr = attribute switch
         {
             Settings.BoolAttribute => new BoolSettingViewModel(name, (bool)(attribute.DefaultValue ?? false), label),
             Settings.ColorAttribute => new ColorSettingViewModel(name,
@@ -65,6 +66,10 @@ internal static class ToolbarFactory
             _ => throw new NotImplementedException(
                 $"SettingsAttribute of type '{attribute.GetType().FullName}' has not been implemented")
         };
+        
+        attr.IsExposed = exposedByDefault;
+        
+        return attr;
     }
 
     private static void AddValueChangedHandlerIfRequired(Type toolType, ToolViewModel tool, Setting setting,

+ 90 - 6
src/PixiEditor/ViewModels/Tools/ToolViewModel.cs

@@ -1,4 +1,5 @@
-using System.Reflection;
+using System.Diagnostics;
+using System.Reflection;
 using System.Runtime.CompilerServices;
 using Avalonia.Input;
 using CommunityToolkit.Mvvm.ComponentModel;
@@ -8,6 +9,7 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Input;
 using Drawie.Numerics;
+using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
 
@@ -91,7 +93,7 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
     public Cursor Cursor { get; set; } = new Cursor(StandardCursorType.Arrow);
 
     public IToolbar Toolbar { get; set; } = new EmptyToolbar();
-    
+
     public Dictionary<IToolSetHandler, Dictionary<string, object>> ToolSetSettings { get; } = new();
 
     internal ToolViewModel()
@@ -106,7 +108,7 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
             CanBeUsedOnActiveLayer = SupportedLayerTypes == null;
             return;
         }
-        
+
         var layer = layers[0];
 
         if (IsActive)
@@ -131,7 +133,7 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
 
         CanBeUsedOnActiveLayer = false;
     }
-    
+
     private bool IsFolderAndRasterSupported(IStructureMemberHandler layer)
     {
         return SupportedLayerTypes.Contains(typeof(IRasterLayerHandler)) && layer is IFolderHandler;
@@ -146,7 +148,7 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
 
     public virtual void UseTool(VecD pos) { }
     public virtual void OnSelected() { }
-    
+
     protected virtual void OnSelectedLayersChanged(IStructureMemberHandler[] layers) { }
 
     public virtual void OnDeselecting()
@@ -159,10 +161,71 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
         {
             return;
         }
-        
+
         ToolSetSettings[toolset] = settings;
     }
 
+    public void ApplyToolSetSettings(IToolSetHandler toolset)
+    {
+        if (!ToolSetSettings.TryGetValue(toolset, out var settings))
+        {
+            return;
+        }
+
+        foreach (var toolbarSetting in Toolbar.Settings)
+        {
+            toolbarSetting.ResetOverwrite();
+        }
+
+        foreach (var setting in settings)
+        {
+
+            if (IsExposeSetting(setting, out bool expose))
+            {
+                string settingName = setting.Key.Replace("Expose", string.Empty);
+                var foundSetting = TryGetSettingByName(settingName, setting);
+                if (foundSetting is null)
+                {
+                    continue;
+                }
+                
+                foundSetting.SetOverwriteExposed(expose);
+            }
+            else
+            {
+                try
+                {
+                    var foundSetting = TryGetSettingByName(setting.Key, setting);
+                    if (foundSetting is null)
+                    {
+                        continue;
+                    }
+                    
+                    foundSetting.SetOverwriteValue(setting.Value);
+                }
+                catch (InvalidCastException)
+                {
+#if DEBUG
+                    throw;
+#endif
+                }
+            }
+        }
+    }
+
+    private Setting? TryGetSettingByName(string settingName, KeyValuePair<string, object> setting)
+    {
+        var foundSetting = Toolbar.GetSetting(settingName);
+        if (foundSetting is null)
+        {
+#if DEBUG
+            Debug.WriteLine($"Setting {settingName} not found in toolbar {Toolbar.GetType().Name}");
+#endif
+        }
+
+        return foundSetting;
+    }
+
     protected T GetValue<T>([CallerMemberName] string name = null)
     {
         var setting = Toolbar.GetSetting(name);
@@ -176,4 +239,25 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
 
         return (T)setting.Value;
     }
+
+    private bool IsExposeSetting(KeyValuePair<string, object> settingConfig, out bool expose)
+    {
+        bool isExpose = settingConfig.Key.StartsWith("Expose", StringComparison.InvariantCultureIgnoreCase);
+        if (!isExpose)
+        {
+            expose = false;
+            return false;
+        }
+
+        var settingName = settingConfig.Key.Replace("Expose", string.Empty);
+
+        if (settingConfig.Value is bool value)
+        {
+            expose = value;
+            return true;
+        }
+
+        expose = false;
+        return false;
+    }
 }

+ 4 - 1
src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs

@@ -37,9 +37,12 @@ namespace PixiEditor.ViewModels.Tools.Tools
         [Settings.Inherited]
         public int ToolSize => GetValue<int>();
 
-        [Settings.Bool("PIXEL_PERFECT_SETTING", Notify = nameof(PixelPerfectChanged))]
+        [Settings.Bool("PIXEL_PERFECT_SETTING", Notify = nameof(PixelPerfectChanged), ExposedByDefault = false)]
         public bool PixelPerfectEnabled => GetValue<bool>();
 
+        [Settings.Bool("__antiAliasing", false, ExposedByDefault = false)]
+        public bool AntiAliasing => GetValue<bool>();
+        
         public override string Icon => PixiPerfectIcons.Pen;
 
         public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(ImageLayerNode);

+ 3 - 2
src/PixiEditor/Views/Main/Tools/Toolbar.axaml

@@ -5,6 +5,7 @@
              xmlns:cmds="clr-namespace:PixiEditor.Models.Commands.XAML"
              xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
              xmlns:decorators="clr-namespace:PixiEditor.Views.Decorators"
+             xmlns:settings="clr-namespace:PixiEditor.ViewModels.Tools.ToolSettings.Settings"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="PixiEditor.Views.Main.Tools.Toolbar">
     <Border CornerRadius="{DynamicResource ControlCornerRadius}"
@@ -56,8 +57,8 @@
                     </ItemsPanelTemplate>
                 </ItemsControl.ItemsPanel>
                 <ItemsControl.ItemTemplate>
-                    <DataTemplate>
-                        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="5,0,5,0">
+                    <DataTemplate x:DataType="settings:Setting">
+                        <StackPanel IsVisible="{Binding IsExposed}" Orientation="Horizontal" VerticalAlignment="Center" Margin="5,0,5,0">
                             <Label
                                 IsVisible="{Binding HasLabel}" VerticalAlignment="Center"
                                 Foreground="{DynamicResource ThemeForegroundBrush}" ui:Translator.Key="{Binding Label.Key}" />