From b07228922ff6264f1a0c8164722398c2dbf3baf1 Mon Sep 17 00:00:00 2001 From: logikonline Date: Sat, 17 Jan 2026 01:43:42 +0000 Subject: [PATCH] Missing bindings defaults --- Views/SkiaButton.cs | 15 +- Views/SkiaEditor.cs | 51 ++++-- Views/SkiaEntry.cs | 60 +++++-- Views/SkiaLabel.cs | 28 ++- Views/SkiaSlider.cs | 54 ++++-- Views/SkiaStateTrigger.cs | 296 ++++++++++++++++++++++++++++++++ Views/SkiaView.cs | 6 +- Views/SkiaVisualState.cs | 44 +++++ Views/SkiaVisualStateManager.cs | 30 +++- Views/SkiaVisualStateSetter.cs | 79 ++++++++- 10 files changed, 590 insertions(+), 73 deletions(-) create mode 100644 Views/SkiaStateTrigger.cs diff --git a/Views/SkiaButton.cs b/Views/SkiaButton.cs index a717efd..96ac6c8 100644 --- a/Views/SkiaButton.cs +++ b/Views/SkiaButton.cs @@ -29,12 +29,13 @@ public class SkiaButton : SkiaView, IButtonController /// /// Bindable property for TextColor. + /// Default is null to match MAUI Button.TextColor (falls back to platform default). /// public static readonly BindableProperty TextColorProperty = BindableProperty.Create( nameof(TextColor), typeof(Color), typeof(SkiaButton), - Colors.White, + null, propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); /// @@ -109,12 +110,13 @@ public class SkiaButton : SkiaView, IButtonController /// /// Bindable property for BorderWidth. + /// Default is -1 to match MAUI Button.BorderWidth (unset/platform default). /// public static readonly BindableProperty BorderWidthProperty = BindableProperty.Create( nameof(BorderWidth), typeof(double), typeof(SkiaButton), - 0.0, + -1.0, propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); /// @@ -208,10 +210,11 @@ public class SkiaButton : SkiaView, IButtonController /// /// Gets or sets the color of the text. + /// Null means use platform default (white on buttons for Linux). /// - public Color TextColor + public Color? TextColor { - get => (Color)GetValue(TextColorProperty); + get => (Color?)GetValue(TextColorProperty); set => SetValue(TextColorProperty, value); } @@ -641,8 +644,8 @@ public class SkiaButton : SkiaView, IButtonController SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(fontFamily, fontStyle) ?? SKTypeface.Default, fontSize); - // Prepare text color - var textColor = ToSKColor(TextColor); + // Prepare text color (null means use platform default: white for buttons) + var textColor = TextColor != null ? ToSKColor(TextColor) : SKColors.White; if (!IsEnabled) { textColor = textColor.WithAlpha(128); diff --git a/Views/SkiaEditor.cs b/Views/SkiaEditor.cs index bc50103..5a36253 100644 --- a/Views/SkiaEditor.cs +++ b/Views/SkiaEditor.cs @@ -43,25 +43,27 @@ public class SkiaEditor : SkiaView /// /// Bindable property for TextColor. + /// Default is null to match MAUI Editor.TextColor (falls back to platform default). /// public static readonly BindableProperty TextColorProperty = BindableProperty.Create( nameof(TextColor), typeof(Color), typeof(SkiaEditor), - Colors.Black, + null, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); /// /// Bindable property for PlaceholderColor. + /// Default is null to match MAUI Editor.PlaceholderColor (falls back to platform default). /// public static readonly BindableProperty PlaceholderColorProperty = BindableProperty.Create( nameof(PlaceholderColor), typeof(Color), typeof(SkiaEditor), - Color.FromRgb(0x80, 0x80, 0x80), + null, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); @@ -103,13 +105,14 @@ public class SkiaEditor : SkiaView /// /// Bindable property for FontFamily. + /// Default is empty string to match MAUI Editor.FontFamily (falls back to platform default). /// public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create( nameof(FontFamily), typeof(string), typeof(SkiaEditor), - "Sans", + string.Empty, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); @@ -307,7 +310,7 @@ public class SkiaEditor : SkiaView /// /// Converts a MAUI Color to SkiaSharp SKColor. /// - private static SKColor ToSKColor(Color color) + private static SKColor ToSKColor(Color? color) { if (color == null) return SKColors.Transparent; return new SKColor( @@ -317,6 +320,30 @@ public class SkiaEditor : SkiaView (byte)(color.Alpha * 255)); } + /// + /// Gets the effective text color (platform default black if null). + /// + private SKColor GetEffectiveTextColor() + { + return TextColor != null ? ToSKColor(TextColor) : SKColors.Black; + } + + /// + /// Gets the effective placeholder color (platform default gray if null). + /// + private SKColor GetEffectivePlaceholderColor() + { + return PlaceholderColor != null ? ToSKColor(PlaceholderColor) : new SKColor(0x80, 0x80, 0x80); + } + + /// + /// Gets the effective font family (platform default "Sans" if empty). + /// + private string GetEffectiveFontFamily() + { + return string.IsNullOrEmpty(FontFamily) ? "Sans" : FontFamily; + } + #endregion #region Properties @@ -340,20 +367,20 @@ public class SkiaEditor : SkiaView } /// - /// Gets or sets the text color. + /// Gets or sets the text color. Null means platform default (black). /// - public Color TextColor + public Color? TextColor { - get => (Color)GetValue(TextColorProperty); + get => (Color?)GetValue(TextColorProperty); set => SetValue(TextColorProperty, value); } /// - /// Gets or sets the placeholder color. + /// Gets or sets the placeholder color. Null means platform default (gray). /// - public Color PlaceholderColor + public Color? PlaceholderColor { - get => (Color)GetValue(PlaceholderColorProperty); + get => (Color?)GetValue(PlaceholderColorProperty); set => SetValue(PlaceholderColorProperty, value); } @@ -780,14 +807,14 @@ public class SkiaEditor : SkiaView { using var placeholderPaint = new SKPaint(font) { - Color = ToSKColor(PlaceholderColor), + Color = GetEffectivePlaceholderColor(), IsAntialias = true }; canvas.DrawText(Placeholder, contentRect.Left, contentRect.Top + fontSize, placeholderPaint); } else { - var textColor = ToSKColor(TextColor); + var textColor = GetEffectiveTextColor(); using var textPaint = new SKPaint(font) { Color = IsEnabled ? textColor : textColor.WithAlpha(128), diff --git a/Views/SkiaEntry.cs b/Views/SkiaEntry.cs index bbcc6a8..a5c9231 100644 --- a/Views/SkiaEntry.cs +++ b/Views/SkiaEntry.cs @@ -43,24 +43,26 @@ public class SkiaEntry : SkiaView /// /// Bindable property for PlaceholderColor. + /// Default is null to match MAUI Entry.PlaceholderColor (falls back to platform default). /// public static readonly BindableProperty PlaceholderColorProperty = BindableProperty.Create( nameof(PlaceholderColor), typeof(Color), typeof(SkiaEntry), - Color.FromRgb(0x9E, 0x9E, 0x9E), + null, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// /// Bindable property for TextColor. + /// Default is null to match MAUI Entry.TextColor (falls back to platform default). /// public static readonly BindableProperty TextColorProperty = BindableProperty.Create( nameof(TextColor), typeof(Color), typeof(SkiaEntry), - Colors.Black, + null, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// @@ -120,13 +122,14 @@ public class SkiaEntry : SkiaView /// /// Bindable property for FontFamily. + /// Default is empty string to match MAUI Entry.FontFamily (falls back to platform default). /// public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create( nameof(FontFamily), typeof(string), typeof(SkiaEntry), - "Sans", + string.Empty, propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); /// @@ -229,13 +232,14 @@ public class SkiaEntry : SkiaView /// /// Bindable property for VerticalTextAlignment. + /// Default is Start to match MAUI Entry.VerticalTextAlignment. /// public static readonly BindableProperty VerticalTextAlignmentProperty = BindableProperty.Create( nameof(VerticalTextAlignment), typeof(TextAlignment), typeof(SkiaEntry), - TextAlignment.Center, + TextAlignment.Start, propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); /// @@ -365,20 +369,20 @@ public class SkiaEntry : SkiaView } /// - /// Gets or sets the placeholder color. + /// Gets or sets the placeholder color. Null means platform default (gray). /// - public Color PlaceholderColor + public Color? PlaceholderColor { - get => (Color)GetValue(PlaceholderColorProperty); + get => (Color?)GetValue(PlaceholderColorProperty); set => SetValue(PlaceholderColorProperty, value); } /// - /// Gets or sets the text color. + /// Gets or sets the text color. Null means platform default (black). /// - public Color TextColor + public Color? TextColor { - get => (Color)GetValue(TextColorProperty); + get => (Color?)GetValue(TextColorProperty); set => SetValue(TextColorProperty, value); } @@ -676,7 +680,7 @@ public class SkiaEntry : SkiaView /// /// Converts a MAUI Color to SkiaSharp SKColor for rendering. /// - private static SKColor ToSKColor(Color color) + private static SKColor ToSKColor(Color? color) { if (color == null) return SKColors.Transparent; return new SKColor( @@ -686,6 +690,30 @@ public class SkiaEntry : SkiaView (byte)(color.Alpha * 255)); } + /// + /// Gets the effective text color (platform default black if null). + /// + private SKColor GetEffectiveTextColor() + { + return TextColor != null ? ToSKColor(TextColor) : SKColors.Black; + } + + /// + /// Gets the effective placeholder color (platform default gray if null). + /// + private SKColor GetEffectivePlaceholderColor() + { + return PlaceholderColor != null ? ToSKColor(PlaceholderColor) : new SKColor(0x9E, 0x9E, 0x9E); + } + + /// + /// Gets the effective font family (platform default "Sans" if empty). + /// + private string GetEffectiveFontFamily() + { + return string.IsNullOrEmpty(FontFamily) ? "Sans" : FontFamily; + } + private void OnTextPropertyChanged(string oldText, string newText) { _cursorPosition = Math.Min(_cursorPosition, (newText ?? "").Length); @@ -742,7 +770,7 @@ public class SkiaEntry : SkiaView canvas.ClipRect(contentBounds); var fontStyle = GetFontStyle(); - var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) + var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(GetEffectiveFontFamily(), fontStyle) ?? SKTypeface.Default; using var font = new SKFont(typeface, (float)FontSize); @@ -753,7 +781,7 @@ public class SkiaEntry : SkiaView if (hasText) { - paint.Color = ToSKColor(TextColor); + paint.Color = GetEffectiveTextColor(); // Measure text to cursor position for scrolling var textToCursor = displayText.Substring(0, Math.Min(_cursorPosition, displayText.Length)); @@ -798,7 +826,7 @@ public class SkiaEntry : SkiaView else if (!string.IsNullOrEmpty(Placeholder)) { // Draw placeholder - paint.Color = ToSKColor(PlaceholderColor); + paint.Color = GetEffectivePlaceholderColor(); var textBounds = new SKRect(); paint.MeasureText(Placeholder, ref textBounds); @@ -1255,7 +1283,7 @@ public class SkiaEntry : SkiaView if (string.IsNullOrEmpty(Text)) return 0; var fontStyle = GetFontStyle(); - var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) + var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(GetEffectiveFontFamily(), fontStyle) ?? SKTypeface.Default; using var font = new SKFont(typeface, (float)FontSize); @@ -1428,7 +1456,7 @@ public class SkiaEntry : SkiaView protected override SKSize MeasureOverride(SKSize availableSize) { var fontStyle = GetFontStyle(); - var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) + var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(GetEffectiveFontFamily(), fontStyle) ?? SKTypeface.Default; using var font = new SKFont(typeface, (float)FontSize); diff --git a/Views/SkiaLabel.cs b/Views/SkiaLabel.cs index 26c7de5..e36cd92 100644 --- a/Views/SkiaLabel.cs +++ b/Views/SkiaLabel.cs @@ -30,12 +30,13 @@ public class SkiaLabel : SkiaView /// /// Bindable property for TextColor. + /// Default is null to match MAUI Label.TextColor (falls back to platform default). /// public static readonly BindableProperty TextColorProperty = BindableProperty.Create( nameof(TextColor), typeof(Color), typeof(SkiaLabel), - Colors.Black, + null, propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); /// @@ -110,12 +111,13 @@ public class SkiaLabel : SkiaView /// /// Bindable property for VerticalTextAlignment. + /// Default is Start to match MAUI Label.VerticalTextAlignment. /// public static readonly BindableProperty VerticalTextAlignmentProperty = BindableProperty.Create( nameof(VerticalTextAlignment), typeof(TextAlignment), typeof(SkiaLabel), - TextAlignment.Center, + TextAlignment.Start, propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); /// @@ -140,12 +142,13 @@ public class SkiaLabel : SkiaView /// /// Bindable property for LineHeight. + /// Default is -1 to match MAUI Label.LineHeight (platform default). /// public static readonly BindableProperty LineHeightProperty = BindableProperty.Create( nameof(LineHeight), typeof(double), typeof(SkiaLabel), - 1.2, + -1.0, propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); /// @@ -203,10 +206,11 @@ public class SkiaLabel : SkiaView /// /// Gets or sets the text color. + /// Null means use platform default (black on Linux). /// - public Color TextColor + public Color? TextColor { - get => (Color)GetValue(TextColorProperty); + get => (Color?)GetValue(TextColorProperty); set => SetValue(TextColorProperty, value); } @@ -778,7 +782,9 @@ public class SkiaLabel : SkiaView private void DrawMultiLineText(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds, string text) { - float lineHeight = (float)(FontSize * LineHeight); + // LineHeight -1 means platform default (use 1.0 multiplier) + double effectiveLineHeight = LineHeight < 0 ? 1.0 : LineHeight; + float lineHeight = (float)(FontSize * effectiveLineHeight); float y = bounds.Top; int lineCount = 0; @@ -869,7 +875,9 @@ public class SkiaLabel : SkiaView float x = bounds.Left; float y = bounds.Top; - float lineHeight = (float)(FontSize * LineHeight); + // LineHeight -1 means platform default (use 1.0 multiplier) + double effectiveLineHeight = LineHeight < 0 ? 1.0 : LineHeight; + float lineHeight = (float)(FontSize * effectiveLineHeight); float fontSize = FontSize > 0 ? (float)FontSize : 14f; // Calculate baseline for first line @@ -1092,12 +1100,14 @@ public class SkiaLabel : SkiaView using var paint = new SKPaint(font); float width, height; + // LineHeight -1 means platform default (use 1.0 multiplier) + double effectiveLineHeight = LineHeight < 0 ? 1.0 : LineHeight; if (FormattedText != null && FormattedText.Spans.Count > 0) { // Measure formatted text width = 0; - height = (float)(fontSize * LineHeight); + height = (float)(fontSize * effectiveLineHeight); foreach (var span in FormattedText.Spans) { if (!string.IsNullOrEmpty(span.Text)) @@ -1124,7 +1134,7 @@ public class SkiaLabel : SkiaView { var lines = displayText.Split('\n'); int lineCount = MaxLines > 0 ? Math.Min(lines.Length, MaxLines) : lines.Length; - height = (float)(lineCount * fontSize * LineHeight); + height = (float)(lineCount * fontSize * effectiveLineHeight); } } diff --git a/Views/SkiaSlider.cs b/Views/SkiaSlider.cs index 23ce616..bed5ae5 100644 --- a/Views/SkiaSlider.cs +++ b/Views/SkiaSlider.cs @@ -42,12 +42,15 @@ public class SkiaSlider : SkiaView BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); + /// + /// Maximum property - default is 1.0 to match MAUI Slider.Maximum. + /// public static readonly BindableProperty MaximumProperty = BindableProperty.Create( nameof(Maximum), typeof(double), typeof(SkiaSlider), - 100.0, + 1.0, // MAUI default is 1.0, not 100.0 BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); @@ -60,30 +63,39 @@ public class SkiaSlider : SkiaView BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).OnValuePropertyChanged((double)o, (double)n)); + /// + /// MinimumTrackColor - default is null to match MAUI (platform default). + /// public static readonly BindableProperty MinimumTrackColorProperty = BindableProperty.Create( nameof(MinimumTrackColor), typeof(Color), typeof(SkiaSlider), - Color.FromRgb(0x21, 0x96, 0xF3), // Material Blue - active track + null, // MAUI default is null (platform default) BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); + /// + /// MaximumTrackColor - default is null to match MAUI (platform default). + /// public static readonly BindableProperty MaximumTrackColorProperty = BindableProperty.Create( nameof(MaximumTrackColor), typeof(Color), typeof(SkiaSlider), - Color.FromRgb(0xE0, 0xE0, 0xE0), // Gray - inactive track + null, // MAUI default is null (platform default) BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); + /// + /// ThumbColor - default is null to match MAUI (platform default). + /// public static readonly BindableProperty ThumbColorProperty = BindableProperty.Create( nameof(ThumbColor), typeof(Color), typeof(SkiaSlider), - Color.FromRgb(0x21, 0x96, 0xF3), // Material Blue + null, // MAUI default is null (platform default) BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); @@ -147,33 +159,43 @@ public class SkiaSlider : SkiaView /// /// Gets or sets the color of the track from minimum to current value. - /// This is the "active" or "filled" portion of the track. + /// Null means platform default (Material Blue on Linux). /// - public Color MinimumTrackColor + public Color? MinimumTrackColor { - get => (Color)GetValue(MinimumTrackColorProperty); + get => (Color?)GetValue(MinimumTrackColorProperty); set => SetValue(MinimumTrackColorProperty, value); } /// /// Gets or sets the color of the track from current value to maximum. - /// This is the "inactive" or "unfilled" portion of the track. + /// Null means platform default (gray on Linux). /// - public Color MaximumTrackColor + public Color? MaximumTrackColor { - get => (Color)GetValue(MaximumTrackColorProperty); + get => (Color?)GetValue(MaximumTrackColorProperty); set => SetValue(MaximumTrackColorProperty, value); } /// /// Gets or sets the thumb color. + /// Null means platform default (Material Blue on Linux). /// - public Color ThumbColor + public Color? ThumbColor { - get => (Color)GetValue(ThumbColorProperty); + get => (Color?)GetValue(ThumbColorProperty); set => SetValue(ThumbColorProperty, value); } + // Platform defaults for colors when null + private static readonly SKColor DefaultMinimumTrackColor = new SKColor(0x21, 0x96, 0xF3); // Material Blue + private static readonly SKColor DefaultMaximumTrackColor = new SKColor(0xE0, 0xE0, 0xE0); // Gray + private static readonly SKColor DefaultThumbColor = new SKColor(0x21, 0x96, 0xF3); // Material Blue + + private SKColor GetEffectiveMinimumTrackColor() => MinimumTrackColor != null ? ToSKColor(MinimumTrackColor) : DefaultMinimumTrackColor; + private SKColor GetEffectiveMaximumTrackColor() => MaximumTrackColor != null ? ToSKColor(MaximumTrackColor) : DefaultMaximumTrackColor; + private SKColor GetEffectiveThumbColor() => ThumbColor != null ? ToSKColor(ThumbColor) : DefaultThumbColor; + /// /// Gets or sets the color used when disabled. /// @@ -272,10 +294,10 @@ public class SkiaSlider : SkiaView var percentage = Maximum > Minimum ? (Value - Minimum) / (Maximum - Minimum) : 0; var thumbX = trackLeft + (float)(percentage * trackWidth); - // Get colors - var minTrackColorSK = ToSKColor(MinimumTrackColor); - var maxTrackColorSK = ToSKColor(MaximumTrackColor); - var thumbColorSK = ToSKColor(ThumbColor); + // Get colors (using helper methods for platform defaults when null) + var minTrackColorSK = GetEffectiveMinimumTrackColor(); + var maxTrackColorSK = GetEffectiveMaximumTrackColor(); + var thumbColorSK = GetEffectiveThumbColor(); var disabledColorSK = ToSKColor(DisabledColor); // Draw inactive (maximum) track diff --git a/Views/SkiaStateTrigger.cs b/Views/SkiaStateTrigger.cs new file mode 100644 index 0000000..d4aa30a --- /dev/null +++ b/Views/SkiaStateTrigger.cs @@ -0,0 +1,296 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Maui.Controls; + +namespace Microsoft.Maui.Platform; + +/// +/// Base class for state triggers that automatically activate visual states. +/// +public abstract class SkiaStateTriggerBase +{ + private bool _isActive; + private SkiaVisualState? _ownerState; + private SkiaView? _ownerView; + + /// + /// Gets whether this trigger is currently active. + /// + public bool IsActive + { + get => _isActive; + protected set + { + if (_isActive != value) + { + _isActive = value; + OnIsActiveChanged(); + } + } + } + + /// + /// Gets or sets the visual state this trigger belongs to. + /// + internal SkiaVisualState? OwnerState + { + get => _ownerState; + set => _ownerState = value; + } + + /// + /// Gets or sets the view this trigger is attached to. + /// + internal SkiaView? OwnerView + { + get => _ownerView; + set + { + _ownerView = value; + OnAttached(); + } + } + + /// + /// Called when the trigger is attached to a view. + /// + protected virtual void OnAttached() + { + } + + /// + /// Called when IsActive changes. + /// + protected virtual void OnIsActiveChanged() + { + if (_isActive && _ownerState != null && _ownerView != null) + { + SkiaVisualStateManager.GoToState(_ownerView, _ownerState.Name); + } + } +} + +/// +/// A trigger that activates based on a boolean property. +/// Maps to MAUI StateTrigger. +/// +public class SkiaStateTrigger : SkiaStateTriggerBase +{ + private bool _isActiveValue; + + /// + /// Gets or sets whether this trigger should be active. + /// + public bool IsActiveValue + { + get => _isActiveValue; + set + { + _isActiveValue = value; + IsActive = value; + } + } +} + +/// +/// A trigger that activates based on window size thresholds. +/// Maps to MAUI AdaptiveTrigger. +/// +public class SkiaAdaptiveTrigger : SkiaStateTriggerBase +{ + private double _minWindowWidth = -1; + private double _minWindowHeight = -1; + + /// + /// Gets or sets the minimum window width for this trigger to activate. + /// + public double MinWindowWidth + { + get => _minWindowWidth; + set + { + _minWindowWidth = value; + UpdateIsActive(); + } + } + + /// + /// Gets or sets the minimum window height for this trigger to activate. + /// + public double MinWindowHeight + { + get => _minWindowHeight; + set + { + _minWindowHeight = value; + UpdateIsActive(); + } + } + + protected override void OnAttached() + { + base.OnAttached(); + // Subscribe to window size changes if needed + UpdateIsActive(); + } + + private void UpdateIsActive() + { + if (OwnerView == null) + { + IsActive = false; + return; + } + + // Get current window size from the view's bounds + var width = OwnerView.Bounds.Width; + var height = OwnerView.Bounds.Height; + + bool widthMet = _minWindowWidth < 0 || width >= _minWindowWidth; + bool heightMet = _minWindowHeight < 0 || height >= _minWindowHeight; + + IsActive = widthMet && heightMet; + } +} + +/// +/// A trigger that activates when a property equals a specific value. +/// Maps to MAUI CompareStateTrigger. +/// +public class SkiaCompareStateTrigger : SkiaStateTriggerBase +{ + private object? _property; + private object? _value; + + /// + /// Gets or sets the property value to compare. + /// + public object? Property + { + get => _property; + set + { + _property = value; + UpdateIsActive(); + } + } + + /// + /// Gets or sets the value to compare against. + /// + public object? Value + { + get => _value; + set + { + _value = value; + UpdateIsActive(); + } + } + + private void UpdateIsActive() + { + if (_property == null && _value == null) + { + IsActive = true; + return; + } + + if (_property == null || _value == null) + { + IsActive = _property == _value; + return; + } + + // Try to compare values + IsActive = _property.Equals(_value); + } +} + +/// +/// A trigger that activates based on device idiom (Desktop, Phone, Tablet, etc.). +/// +public class SkiaDeviceStateTrigger : SkiaStateTriggerBase +{ + private string _deviceType = ""; + + /// + /// Gets or sets the device type to match (Desktop, Phone, Tablet, Watch, TV). + /// + public string DeviceType + { + get => _deviceType; + set + { + _deviceType = value; + UpdateIsActive(); + } + } + + protected override void OnAttached() + { + base.OnAttached(); + UpdateIsActive(); + } + + private void UpdateIsActive() + { + // On Linux, we're always Desktop + IsActive = string.Equals(_deviceType, "Desktop", StringComparison.OrdinalIgnoreCase); + } +} + +/// +/// A trigger that activates based on orientation (Portrait or Landscape). +/// +public class SkiaOrientationStateTrigger : SkiaStateTriggerBase +{ + private SkiaDisplayOrientation _orientation = SkiaDisplayOrientation.Portrait; + + /// + /// Gets or sets the orientation to match. + /// + public SkiaDisplayOrientation Orientation + { + get => _orientation; + set + { + _orientation = value; + UpdateIsActive(); + } + } + + protected override void OnAttached() + { + base.OnAttached(); + UpdateIsActive(); + } + + private void UpdateIsActive() + { + if (OwnerView == null) + { + IsActive = false; + return; + } + + var width = OwnerView.Bounds.Width; + var height = OwnerView.Bounds.Height; + + var currentOrientation = width > height + ? SkiaDisplayOrientation.Landscape + : SkiaDisplayOrientation.Portrait; + + IsActive = currentOrientation == _orientation; + } +} + +/// +/// Display orientation values for state triggers. +/// +public enum SkiaDisplayOrientation +{ + Portrait, + Landscape +} diff --git a/Views/SkiaView.cs b/Views/SkiaView.cs index 0c46bfb..2665813 100644 --- a/Views/SkiaView.cs +++ b/Views/SkiaView.cs @@ -151,24 +151,26 @@ public abstract class SkiaView : BindableObject, IDisposable /// /// Bindable property for MinimumWidthRequest. + /// Default is -1 (unset) to match MAUI View.MinimumWidthRequest. /// public static readonly BindableProperty MinimumWidthRequestProperty = BindableProperty.Create( nameof(MinimumWidthRequest), typeof(double), typeof(SkiaView), - 0.0, + -1.0, propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); /// /// Bindable property for MinimumHeightRequest. + /// Default is -1 (unset) to match MAUI View.MinimumHeightRequest. /// public static readonly BindableProperty MinimumHeightRequestProperty = BindableProperty.Create( nameof(MinimumHeightRequest), typeof(double), typeof(SkiaView), - 0.0, + -1.0, propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); /// diff --git a/Views/SkiaVisualState.cs b/Views/SkiaVisualState.cs index a697070..bbe82e7 100644 --- a/Views/SkiaVisualState.cs +++ b/Views/SkiaVisualState.cs @@ -5,9 +5,53 @@ using System.Collections.Generic; namespace Microsoft.Maui.Platform; +/// +/// Represents a visual state with setters and optional triggers. +/// Maps to MAUI VisualState. +/// public class SkiaVisualState { + /// + /// Gets or sets the name of this visual state. + /// public string Name { get; set; } = ""; + /// + /// Gets the setters that define property changes for this state. + /// public List Setters { get; } = new List(); + + /// + /// Gets the state triggers that can automatically activate this state. + /// + public List StateTriggers { get; } = new List(); + + /// + /// Gets or sets the target type this state applies to. + /// + public Type? TargetType { get; set; } + + /// + /// Attaches triggers to the specified view. + /// + internal void AttachTriggers(SkiaView view) + { + foreach (var trigger in StateTriggers) + { + trigger.OwnerState = this; + trigger.OwnerView = view; + } + } + + /// + /// Detaches triggers from the view. + /// + internal void DetachTriggers() + { + foreach (var trigger in StateTriggers) + { + trigger.OwnerState = null; + trigger.OwnerView = null; + } + } } diff --git a/Views/SkiaVisualStateManager.cs b/Views/SkiaVisualStateManager.cs index 5b7a472..5bb17ce 100644 --- a/Views/SkiaVisualStateManager.cs +++ b/Views/SkiaVisualStateManager.cs @@ -55,10 +55,34 @@ public static class SkiaVisualStateManager private static void OnVisualStateGroupsChanged(BindableObject bindable, object? oldValue, object? newValue) { - if (bindable is SkiaView view && newValue is SkiaVisualStateGroupList groups) + if (bindable is SkiaView view) { - // Initialize to default state - GoToState(view, CommonStates.Normal); + // Detach old triggers + if (oldValue is SkiaVisualStateGroupList oldGroups) + { + foreach (var group in oldGroups) + { + foreach (var state in group.States) + { + state.DetachTriggers(); + } + } + } + + // Attach new triggers + if (newValue is SkiaVisualStateGroupList groups) + { + foreach (var group in groups) + { + foreach (var state in group.States) + { + state.AttachTriggers(view); + } + } + + // Initialize to default state + GoToState(view, CommonStates.Normal); + } } } diff --git a/Views/SkiaVisualStateSetter.cs b/Views/SkiaVisualStateSetter.cs index 68e0427..ada7ea0 100644 --- a/Views/SkiaVisualStateSetter.cs +++ b/Views/SkiaVisualStateSetter.cs @@ -5,33 +5,94 @@ using Microsoft.Maui.Controls; namespace Microsoft.Maui.Platform; +/// +/// Represents a property setter within a visual state. +/// Maps to MAUI Setter class. +/// public class SkiaVisualStateSetter { private object? _originalValue; private bool _hasOriginalValue; + private SkiaView? _targetView; + /// + /// Gets or sets the property to set. + /// public BindableProperty? Property { get; set; } + /// + /// Gets or sets the value to set. + /// public object? Value { get; set; } + /// + /// Gets or sets the name of the target element within a template. + /// If null, the setter applies to the root element. + /// + public string? TargetName { get; set; } + + /// + /// Applies the setter value to the view. + /// public void Apply(SkiaView view) { - if (Property != null) + var target = ResolveTarget(view); + if (target == null || Property == null) + return; + + if (!_hasOriginalValue) { - if (!_hasOriginalValue) - { - _originalValue = view.GetValue(Property); - _hasOriginalValue = true; - } - view.SetValue(Property, Value); + _originalValue = target.GetValue(Property); + _hasOriginalValue = true; + _targetView = target; } + target.SetValue(Property, Value); } + /// + /// Restores the original value on the view. + /// public void Unapply(SkiaView view) { - if (Property != null && _hasOriginalValue) + var target = _targetView ?? ResolveTarget(view); + if (target == null || Property == null || !_hasOriginalValue) + return; + + target.SetValue(Property, _originalValue); + } + + /// + /// Resolves the target view based on TargetName. + /// + private SkiaView? ResolveTarget(SkiaView view) + { + if (string.IsNullOrEmpty(TargetName)) + return view; + + // Find named element in visual tree + return FindNamedElement(view, TargetName); + } + + /// + /// Finds a named element in the visual tree. + /// + private static SkiaView? FindNamedElement(SkiaView root, string name) + { + // Check if root has the name (using Name property if available) + if (root.Name == name) + return root; + + // Search children if it's a layout + if (root is SkiaLayoutView layout) { - view.SetValue(Property, _originalValue); + foreach (var child in layout.Children) + { + var found = FindNamedElement(child, name); + if (found != null) + return found; + } } + + return null; } }