diff --git a/Demo/AutoSettingUI.Ursa.Demo/Models/ApplicationSettings.cs b/Demo/AutoSettingUI.Ursa.Demo/Models/ApplicationSettings.cs index 26057d2..8267b96 100644 --- a/Demo/AutoSettingUI.Ursa.Demo/Models/ApplicationSettings.cs +++ b/Demo/AutoSettingUI.Ursa.Demo/Models/ApplicationSettings.cs @@ -162,7 +162,7 @@ public string Email [Description("Your display name (3-20 characters)")] [Placeholder("Enter username")] [Validation(Required = true, MinLength = 3, MaxLength = 20, ErrorMessage = "Username must be 3-20 characters")] - [Layout(Width = 200, Height = 28, Margin = "0,2,0,2")] + [Layout(Width = 200, Height = 58, Margin = "0,2,0,2")] public string Username { get; set; } = ""; // Password: Custom mask character @@ -342,6 +342,7 @@ public partial class ExtendedControlsSettings : ObservableObject [Title("Settings.SelectionTags", UseResourceKey = true)] [TagInput] public ObservableCollection ProjectTags { get; set; } = ["Ursa", "Avalonia", "AutoSettingUI"]; + [Title("Settings.ReleaseDate", UseResourceKey = true)] [DatePicker] diff --git a/src/AutoSettingUI.Core/Models/PropertyDescriptor.cs b/src/AutoSettingUI.Core/Models/PropertyDescriptor.cs index 08fe28c..53d1385 100644 --- a/src/AutoSettingUI.Core/Models/PropertyDescriptor.cs +++ b/src/AutoSettingUI.Core/Models/PropertyDescriptor.cs @@ -221,6 +221,62 @@ public sealed class PropertyDescriptor /// public int DisplayOrder { get; } + #region Layout Properties + + /// + /// Gets the width for the control. double.NaN means Auto. + /// + public double LayoutWidth { get; } + + /// + /// Gets the height for the control. double.NaN means Auto. + /// + public double LayoutHeight { get; } + + /// + /// Gets the minimum width for the control. + /// + public double LayoutMinWidth { get; } + + /// + /// Gets the minimum height for the control. + /// + public double LayoutMinHeight { get; } + + /// + /// Gets the maximum width for the control. + /// + public double LayoutMaxWidth { get; } + + /// + /// Gets the maximum height for the control. + /// + public double LayoutMaxHeight { get; } + + /// + /// Gets the horizontal alignment for the control. + /// Values: "Left", "Center", "Right", "Stretch" + /// + public string? LayoutHorizontalAlignment { get; } + + /// + /// Gets the vertical alignment for the control. + /// Values: "Top", "Center", "Bottom", "Stretch" + /// + public string? LayoutVerticalAlignment { get; } + + /// + /// Gets the margin for the control. + /// + public string? LayoutMargin { get; } + + /// + /// Gets the padding for the control. + /// + public string? LayoutPadding { get; } + + #endregion + /// /// Initializes a new instance of the class. /// @@ -262,7 +318,17 @@ public PropertyDescriptor( int displayOrder = 0, string? displayNameKey = null, string? placeholderKey = null, - string? descriptionKey = null) + string? descriptionKey = null, + double layoutWidth = double.NaN, + double layoutHeight = double.NaN, + double layoutMinWidth = double.NaN, + double layoutMinHeight = double.NaN, + double layoutMaxWidth = double.NaN, + double layoutMaxHeight = double.NaN, + string? layoutHorizontalAlignment = null, + string? layoutVerticalAlignment = null, + string? layoutMargin = null, + string? layoutPadding = null) { PropertyName = propertyName; DisplayName = displayName; @@ -302,6 +368,16 @@ public PropertyDescriptor( NumericMaximum = numericMaximum; NumericIncrement = numericIncrement; DisplayOrder = displayOrder; + LayoutWidth = layoutWidth; + LayoutHeight = layoutHeight; + LayoutMinWidth = layoutMinWidth; + LayoutMinHeight = layoutMinHeight; + LayoutMaxWidth = layoutMaxWidth; + LayoutMaxHeight = layoutMaxHeight; + LayoutHorizontalAlignment = layoutHorizontalAlignment; + LayoutVerticalAlignment = layoutVerticalAlignment; + LayoutMargin = layoutMargin; + LayoutPadding = layoutPadding; } } diff --git a/src/AutoSettingUI.Core/Providers/ReflectionSettingDescriptorProvider.cs b/src/AutoSettingUI.Core/Providers/ReflectionSettingDescriptorProvider.cs index e64bc79..ad6a356 100644 --- a/src/AutoSettingUI.Core/Providers/ReflectionSettingDescriptorProvider.cs +++ b/src/AutoSettingUI.Core/Providers/ReflectionSettingDescriptorProvider.cs @@ -132,6 +132,7 @@ public bool HasDescriptor(string typeName) var descriptionAttr = prop.GetCustomAttribute(); var passwordAttr = prop.GetCustomAttribute(); var displayOrderAttr = prop.GetCustomAttribute(); + var layoutAttr = prop.GetCustomAttribute(); // Pre-compute enum values to avoid runtime Enum.GetValues in non-AOT path string[]? enumValues = null; @@ -191,7 +192,17 @@ public bool HasDescriptor(string typeName) displayOrderAttr?.Order ?? 0, titleAttr?.UseResourceKey == true ? titleAttr.Name : null, placeholderAttr?.UseResourceKey == true ? placeholderAttr.Text : null, - descriptionAttr?.UseResourceKey == true ? descriptionAttr.Text : (titleAttr?.UseDescriptionKey == true ? titleAttr.Description : null) + descriptionAttr?.UseResourceKey == true ? descriptionAttr.Text : (titleAttr?.UseDescriptionKey == true ? titleAttr.Description : null), + layoutAttr?.Width ?? double.NaN, + layoutAttr?.Height ?? double.NaN, + layoutAttr?.MinWidth ?? double.NaN, + layoutAttr?.MinHeight ?? double.NaN, + layoutAttr?.MaxWidth ?? double.NaN, + layoutAttr?.MaxHeight ?? double.NaN, + layoutAttr?.HorizontalAlignment, + layoutAttr?.VerticalAlignment, + layoutAttr?.Margin, + layoutAttr?.Padding ); if (currentSubProps is not null) diff --git a/src/AutoSettingUI.Generator/Incremental/AutoSettingGenerator.cs b/src/AutoSettingUI.Generator/Incremental/AutoSettingGenerator.cs index 7f3b8e8..3d82736 100644 --- a/src/AutoSettingUI.Generator/Incremental/AutoSettingGenerator.cs +++ b/src/AutoSettingUI.Generator/Incremental/AutoSettingGenerator.cs @@ -34,6 +34,7 @@ public class AutoSettingGenerator : IIncrementalGenerator private const string NumericUpDownAttributeName = "NumericUpDownAttribute"; // Special handling needed - type depends on property type private const string ControlBindingDefaultsAttributeName = "ControlBindingDefaultsAttribute"; private const string DisplayOrderAttributeName = "DisplayOrderAttribute"; + private const string LayoutAttributeName = "LayoutAttribute"; // Diagnostic descriptors private static readonly DiagnosticDescriptor NonPublicClassWarning = new( @@ -546,6 +547,32 @@ private static void AppendRegisterMethod(StringBuilder sb, INamedTypeSymbol cls, displayOrder = orderVal; } + // Layout + var layoutAttr = GetAttr(prop, LayoutAttributeName); + double layoutWidth = double.NaN; + double layoutHeight = double.NaN; + double layoutMinWidth = double.NaN; + double layoutMinHeight = double.NaN; + double layoutMaxWidth = double.NaN; + double layoutMaxHeight = double.NaN; + string? layoutHAlign = null; + string? layoutVAlign = null; + string? layoutMargin = null; + string? layoutPadding = null; + if (layoutAttr != null) + { + layoutWidth = TryGetNamedArgDouble(layoutAttr, "Width") ?? double.NaN; + layoutHeight = TryGetNamedArgDouble(layoutAttr, "Height") ?? double.NaN; + layoutMinWidth = TryGetNamedArgDouble(layoutAttr, "MinWidth") ?? double.NaN; + layoutMinHeight = TryGetNamedArgDouble(layoutAttr, "MinHeight") ?? double.NaN; + layoutMaxWidth = TryGetNamedArgDouble(layoutAttr, "MaxWidth") ?? double.NaN; + layoutMaxHeight = TryGetNamedArgDouble(layoutAttr, "MaxHeight") ?? double.NaN; + layoutHAlign = GetNamedArgString(layoutAttr, "HorizontalAlignment"); + layoutVAlign = GetNamedArgString(layoutAttr, "VerticalAlignment"); + layoutMargin = GetNamedArgString(layoutAttr, "Margin"); + layoutPadding = GetNamedArgString(layoutAttr, "Padding"); + } + // CollectionEditor var collEditorAttr = GetAttr(prop, CollectionEditorAttributeName); string? collEditorTypeName = null; @@ -651,7 +678,17 @@ private static void AppendRegisterMethod(StringBuilder sb, INamedTypeSymbol cls, sb.AppendLine($" {displayOrder},"); sb.AppendLine($" {displayNameKey},"); sb.AppendLine($" {placeholderKey},"); - sb.AppendLine($" {descriptionKey});"); + sb.AppendLine($" {descriptionKey},"); + sb.AppendLine($" {FormatDouble(layoutWidth)},"); + sb.AppendLine($" {FormatDouble(layoutHeight)},"); + sb.AppendLine($" {FormatDouble(layoutMinWidth)},"); + sb.AppendLine($" {FormatDouble(layoutMinHeight)},"); + sb.AppendLine($" {FormatDouble(layoutMaxWidth)},"); + sb.AppendLine($" {FormatDouble(layoutMaxHeight)},"); + sb.AppendLine($" {layoutHAlign ?? "null"},"); + sb.AppendLine($" {layoutVAlign ?? "null"},"); + sb.AppendLine($" {layoutMargin ?? "null"},"); + sb.AppendLine($" {layoutPadding ?? "null"});"); var varName = $"pd_{safeName}_{EscapeName(prop.Name)}"; sb.AppendLine($" if (currentSub != null) currentSub.Add({varName}); else directProps.Add({varName});"); @@ -976,6 +1013,14 @@ private static int GetDisplayOrder(PropertyInfo prop) return null; } + private static string FormatDouble(double value) + { + if (double.IsNaN(value)) return "double.NaN"; + if (double.IsPositiveInfinity(value)) return "double.PositiveInfinity"; + if (double.IsNegativeInfinity(value)) return "double.NegativeInfinity"; + return value.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + /// Gets a named argument value as a Type symbol. private static INamedTypeSymbol? GetNamedArgType(AttributeData? attr, string key) { diff --git a/src/Extensions/AutoSettingUI.Avalonia/Controls/AvaloniaAutoSettingPanel.cs b/src/Extensions/AutoSettingUI.Avalonia/Controls/AvaloniaAutoSettingPanel.cs index 9906ec9..fb18422 100644 --- a/src/Extensions/AutoSettingUI.Avalonia/Controls/AvaloniaAutoSettingPanel.cs +++ b/src/Extensions/AutoSettingUI.Avalonia/Controls/AvaloniaAutoSettingPanel.cs @@ -993,34 +993,92 @@ private bool CanExecuteDelegate(Core.Models.PropertyDescriptor prop, object targ /// private void ApplyControlAttributes(global::Avalonia.Controls.Control control, Core.Models.PropertyDescriptor prop, object target) { - var propertyInfo = GetPropertyInfo(target, prop.PropertyName); - if (propertyInfo != null) - { - // Apply layout attributes - control.ApplyLayout(propertyInfo); + // Apply layout from pre-generated PropertyDescriptor (AOT-safe) + ApplyLayoutFromDescriptor(control, prop); - // Apply placeholder to TextBox - if (control is TextBox textBox) - { - textBox.ApplyPlaceholder(propertyInfo); - } - - // Apply description (tooltip) - control.ApplyDescription(propertyInfo); - } - - // AOT-safe fallback using generated metadata - if (control is TextBox tb && !string.IsNullOrEmpty(prop.PlaceholderText)) + // Apply placeholder to TextBox + if (control is TextBox textBox && !string.IsNullOrEmpty(prop.PlaceholderText)) { - tb.Watermark = prop.PlaceholderText; + textBox.Watermark = prop.PlaceholderText; } + // Apply description (tooltip) if (!string.IsNullOrEmpty(prop.DescriptionText)) { ToolTip.SetTip(control, prop.DescriptionText); } } + /// + /// Applies layout properties from PropertyDescriptor (AOT-safe, no reflection). + /// + private static void ApplyLayoutFromDescriptor(global::Avalonia.Controls.Control control, Core.Models.PropertyDescriptor prop) + { + if (!double.IsNaN(prop.LayoutWidth)) control.Width = prop.LayoutWidth; + if (!double.IsNaN(prop.LayoutHeight)) control.Height = prop.LayoutHeight; + if (!double.IsNaN(prop.LayoutMinWidth)) control.MinWidth = prop.LayoutMinWidth; + if (!double.IsNaN(prop.LayoutMinHeight)) control.MinHeight = prop.LayoutMinHeight; + if (!double.IsNaN(prop.LayoutMaxWidth)) control.MaxWidth = prop.LayoutMaxWidth; + if (!double.IsNaN(prop.LayoutMaxHeight)) control.MaxHeight = prop.LayoutMaxHeight; + + if (!string.IsNullOrEmpty(prop.LayoutHorizontalAlignment)) + control.HorizontalAlignment = ParseHorizontalAlignment(prop.LayoutHorizontalAlignment); + + if (!string.IsNullOrEmpty(prop.LayoutVerticalAlignment)) + control.VerticalAlignment = ParseVerticalAlignment(prop.LayoutVerticalAlignment); + + if (!string.IsNullOrEmpty(prop.LayoutMargin)) + control.Margin = ParseThickness(prop.LayoutMargin); + + if (!string.IsNullOrEmpty(prop.LayoutPadding)) + { + var padding = ParseThickness(prop.LayoutPadding); + if (control is ContentControl contentControl) + contentControl.Padding = padding; + else if (control is Decorator decorator) + decorator.Padding = padding; + } + } + + private static HorizontalAlignment ParseHorizontalAlignment(string? value) + => value?.ToLowerInvariant() switch + { + "left" => HorizontalAlignment.Left, + "center" => HorizontalAlignment.Center, + "right" => HorizontalAlignment.Right, + "stretch" => HorizontalAlignment.Stretch, + _ => HorizontalAlignment.Stretch + }; + + private static VerticalAlignment ParseVerticalAlignment(string? value) + => value?.ToLowerInvariant() switch + { + "top" => VerticalAlignment.Top, + "center" => VerticalAlignment.Center, + "bottom" => VerticalAlignment.Bottom, + "stretch" => VerticalAlignment.Stretch, + _ => VerticalAlignment.Stretch + }; + + private static Thickness ParseThickness(string? value) + { + if (string.IsNullOrEmpty(value)) return new Thickness(0); + + var parts = value.Split(',').Select(p => p.Trim()).ToArray(); + + if (parts.Length == 1 && double.TryParse(parts[0], out var uniform)) + return new Thickness(uniform); + + if (parts.Length == 4 && + double.TryParse(parts[0], out var left) && + double.TryParse(parts[1], out var top) && + double.TryParse(parts[2], out var right) && + double.TryParse(parts[3], out var bottom)) + return new Thickness(left, top, right, bottom); + + return new Thickness(0); + } + /// /// Creates a TextBox with password support and validation. /// diff --git a/src/Extensions/AutoSettingUI.Ursa/Controls/UrsaAutoSettingPanel.cs b/src/Extensions/AutoSettingUI.Ursa/Controls/UrsaAutoSettingPanel.cs index 6dbd782..c881daa 100644 --- a/src/Extensions/AutoSettingUI.Ursa/Controls/UrsaAutoSettingPanel.cs +++ b/src/Extensions/AutoSettingUI.Ursa/Controls/UrsaAutoSettingPanel.cs @@ -1005,34 +1005,100 @@ private bool CanExecuteDelegate(Core.Models.PropertyDescriptor prop, object targ /// private void ApplyControlAttributes(global::Avalonia.Controls.Control control, Core.Models.PropertyDescriptor prop, object target) { - var propertyInfo = GetPropertyInfo(target, prop.PropertyName); - if (propertyInfo != null) - { - // Apply layout attributes - control.ApplyLayout(propertyInfo); + // Apply layout from pre-generated PropertyDescriptor (AOT-safe) + ApplyLayoutFromDescriptor(control, prop); - // Apply placeholder to TextBox - if (control is TextBox textBox) - { - textBox.ApplyPlaceholder(propertyInfo); - } + // Apply placeholder to TextBox + if (control is TextBox textBox && !string.IsNullOrEmpty(prop.PlaceholderText)) + { + textBox.Watermark = prop.PlaceholderText; + } - // Apply description (tooltip) - control.ApplyDescription(propertyInfo); + // Apply description (tooltip) + if (!string.IsNullOrEmpty(prop.DescriptionText)) + { + ToolTip.SetTip(control, prop.DescriptionText); } - // AOT-safe fallback using generated metadata - if (control is TextBox tb && !string.IsNullOrEmpty(prop.PlaceholderText)) + // Fix for TagInput: prevent infinite width constraint crash + // TagInputPanel.MeasureOverride() doesn't handle infinite width correctly + if (control is global::Ursa.Controls.TagInput tagInput) { - tb.Watermark = prop.PlaceholderText; + tagInput.MaxWidth = 800; + tagInput.HorizontalAlignment = HorizontalAlignment.Stretch; } + } - if (!string.IsNullOrEmpty(prop.DescriptionText)) + /// + /// Applies layout properties from PropertyDescriptor (AOT-safe, no reflection). + /// + private static void ApplyLayoutFromDescriptor(global::Avalonia.Controls.Control control, Core.Models.PropertyDescriptor prop) + { + if (!double.IsNaN(prop.LayoutWidth)) control.Width = prop.LayoutWidth; + if (!double.IsNaN(prop.LayoutHeight)) control.Height = prop.LayoutHeight; + if (!double.IsNaN(prop.LayoutMinWidth)) control.MinWidth = prop.LayoutMinWidth; + if (!double.IsNaN(prop.LayoutMinHeight)) control.MinHeight = prop.LayoutMinHeight; + if (!double.IsNaN(prop.LayoutMaxWidth)) control.MaxWidth = prop.LayoutMaxWidth; + if (!double.IsNaN(prop.LayoutMaxHeight)) control.MaxHeight = prop.LayoutMaxHeight; + + if (!string.IsNullOrEmpty(prop.LayoutHorizontalAlignment)) + control.HorizontalAlignment = ParseHorizontalAlignment(prop.LayoutHorizontalAlignment); + + if (!string.IsNullOrEmpty(prop.LayoutVerticalAlignment)) + control.VerticalAlignment = ParseVerticalAlignment(prop.LayoutVerticalAlignment); + + if (!string.IsNullOrEmpty(prop.LayoutMargin)) + control.Margin = ParseThickness(prop.LayoutMargin); + + if (!string.IsNullOrEmpty(prop.LayoutPadding)) { - ToolTip.SetTip(control, prop.DescriptionText); + var padding = ParseThickness(prop.LayoutPadding); + if (control is ContentControl contentControl) + contentControl.Padding = padding; + else if (control is Decorator decorator) + decorator.Padding = padding; } } + private static HorizontalAlignment ParseHorizontalAlignment(string? value) + => value?.ToLowerInvariant() switch + { + "left" => HorizontalAlignment.Left, + "center" => HorizontalAlignment.Center, + "right" => HorizontalAlignment.Right, + "stretch" => HorizontalAlignment.Stretch, + _ => HorizontalAlignment.Stretch + }; + + private static VerticalAlignment ParseVerticalAlignment(string? value) + => value?.ToLowerInvariant() switch + { + "top" => VerticalAlignment.Top, + "center" => VerticalAlignment.Center, + "bottom" => VerticalAlignment.Bottom, + "stretch" => VerticalAlignment.Stretch, + _ => VerticalAlignment.Stretch + }; + + private static Thickness ParseThickness(string? value) + { + if (string.IsNullOrEmpty(value)) return new Thickness(0); + + var parts = value.Split(',').Select(p => p.Trim()).ToArray(); + + if (parts.Length == 1 && double.TryParse(parts[0], out var uniform)) + return new Thickness(uniform); + + if (parts.Length == 4 && + double.TryParse(parts[0], out var left) && + double.TryParse(parts[1], out var top) && + double.TryParse(parts[2], out var right) && + double.TryParse(parts[3], out var bottom)) + return new Thickness(left, top, right, bottom); + + return new Thickness(0); + } + /// /// Creates a TextBox with password support and validation. ///