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;
}
}