Missing bindings defaults

This commit is contained in:
2026-01-17 01:43:42 +00:00
parent 4c70118be6
commit b07228922f
10 changed files with 590 additions and 73 deletions

View File

@@ -29,12 +29,13 @@ public class SkiaButton : SkiaView, IButtonController
/// <summary>
/// Bindable property for TextColor.
/// Default is null to match MAUI Button.TextColor (falls back to platform default).
/// </summary>
public static readonly BindableProperty TextColorProperty = BindableProperty.Create(
nameof(TextColor),
typeof(Color),
typeof(SkiaButton),
Colors.White,
null,
propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate());
/// <summary>
@@ -109,12 +110,13 @@ public class SkiaButton : SkiaView, IButtonController
/// <summary>
/// Bindable property for BorderWidth.
/// Default is -1 to match MAUI Button.BorderWidth (unset/platform default).
/// </summary>
public static readonly BindableProperty BorderWidthProperty = BindableProperty.Create(
nameof(BorderWidth),
typeof(double),
typeof(SkiaButton),
0.0,
-1.0,
propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate());
/// <summary>
@@ -208,10 +210,11 @@ public class SkiaButton : SkiaView, IButtonController
/// <summary>
/// Gets or sets the color of the text.
/// Null means use platform default (white on buttons for Linux).
/// </summary>
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);

View File

@@ -43,25 +43,27 @@ public class SkiaEditor : SkiaView
/// <summary>
/// Bindable property for TextColor.
/// Default is null to match MAUI Editor.TextColor (falls back to platform default).
/// </summary>
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());
/// <summary>
/// Bindable property for PlaceholderColor.
/// Default is null to match MAUI Editor.PlaceholderColor (falls back to platform default).
/// </summary>
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
/// <summary>
/// Bindable property for FontFamily.
/// Default is empty string to match MAUI Editor.FontFamily (falls back to platform default).
/// </summary>
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
/// <summary>
/// Converts a MAUI Color to SkiaSharp SKColor.
/// </summary>
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));
}
/// <summary>
/// Gets the effective text color (platform default black if null).
/// </summary>
private SKColor GetEffectiveTextColor()
{
return TextColor != null ? ToSKColor(TextColor) : SKColors.Black;
}
/// <summary>
/// Gets the effective placeholder color (platform default gray if null).
/// </summary>
private SKColor GetEffectivePlaceholderColor()
{
return PlaceholderColor != null ? ToSKColor(PlaceholderColor) : new SKColor(0x80, 0x80, 0x80);
}
/// <summary>
/// Gets the effective font family (platform default "Sans" if empty).
/// </summary>
private string GetEffectiveFontFamily()
{
return string.IsNullOrEmpty(FontFamily) ? "Sans" : FontFamily;
}
#endregion
#region Properties
@@ -340,20 +367,20 @@ public class SkiaEditor : SkiaView
}
/// <summary>
/// Gets or sets the text color.
/// Gets or sets the text color. Null means platform default (black).
/// </summary>
public Color TextColor
public Color? TextColor
{
get => (Color)GetValue(TextColorProperty);
get => (Color?)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value);
}
/// <summary>
/// Gets or sets the placeholder color.
/// Gets or sets the placeholder color. Null means platform default (gray).
/// </summary>
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),

View File

@@ -43,24 +43,26 @@ public class SkiaEntry : SkiaView
/// <summary>
/// Bindable property for PlaceholderColor.
/// Default is null to match MAUI Entry.PlaceholderColor (falls back to platform default).
/// </summary>
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());
/// <summary>
/// Bindable property for TextColor.
/// Default is null to match MAUI Entry.TextColor (falls back to platform default).
/// </summary>
public static readonly BindableProperty TextColorProperty =
BindableProperty.Create(
nameof(TextColor),
typeof(Color),
typeof(SkiaEntry),
Colors.Black,
null,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
@@ -120,13 +122,14 @@ public class SkiaEntry : SkiaView
/// <summary>
/// Bindable property for FontFamily.
/// Default is empty string to match MAUI Entry.FontFamily (falls back to platform default).
/// </summary>
public static readonly BindableProperty FontFamilyProperty =
BindableProperty.Create(
nameof(FontFamily),
typeof(string),
typeof(SkiaEntry),
"Sans",
string.Empty,
propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure());
/// <summary>
@@ -229,13 +232,14 @@ public class SkiaEntry : SkiaView
/// <summary>
/// Bindable property for VerticalTextAlignment.
/// Default is Start to match MAUI Entry.VerticalTextAlignment.
/// </summary>
public static readonly BindableProperty VerticalTextAlignmentProperty =
BindableProperty.Create(
nameof(VerticalTextAlignment),
typeof(TextAlignment),
typeof(SkiaEntry),
TextAlignment.Center,
TextAlignment.Start,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary>
@@ -365,20 +369,20 @@ public class SkiaEntry : SkiaView
}
/// <summary>
/// Gets or sets the placeholder color.
/// Gets or sets the placeholder color. Null means platform default (gray).
/// </summary>
public Color PlaceholderColor
public Color? PlaceholderColor
{
get => (Color)GetValue(PlaceholderColorProperty);
get => (Color?)GetValue(PlaceholderColorProperty);
set => SetValue(PlaceholderColorProperty, value);
}
/// <summary>
/// Gets or sets the text color.
/// Gets or sets the text color. Null means platform default (black).
/// </summary>
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
/// <summary>
/// Converts a MAUI Color to SkiaSharp SKColor for rendering.
/// </summary>
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));
}
/// <summary>
/// Gets the effective text color (platform default black if null).
/// </summary>
private SKColor GetEffectiveTextColor()
{
return TextColor != null ? ToSKColor(TextColor) : SKColors.Black;
}
/// <summary>
/// Gets the effective placeholder color (platform default gray if null).
/// </summary>
private SKColor GetEffectivePlaceholderColor()
{
return PlaceholderColor != null ? ToSKColor(PlaceholderColor) : new SKColor(0x9E, 0x9E, 0x9E);
}
/// <summary>
/// Gets the effective font family (platform default "Sans" if empty).
/// </summary>
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);

View File

@@ -30,12 +30,13 @@ public class SkiaLabel : SkiaView
/// <summary>
/// Bindable property for TextColor.
/// Default is null to match MAUI Label.TextColor (falls back to platform default).
/// </summary>
public static readonly BindableProperty TextColorProperty = BindableProperty.Create(
nameof(TextColor),
typeof(Color),
typeof(SkiaLabel),
Colors.Black,
null,
propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());
/// <summary>
@@ -110,12 +111,13 @@ public class SkiaLabel : SkiaView
/// <summary>
/// Bindable property for VerticalTextAlignment.
/// Default is Start to match MAUI Label.VerticalTextAlignment.
/// </summary>
public static readonly BindableProperty VerticalTextAlignmentProperty = BindableProperty.Create(
nameof(VerticalTextAlignment),
typeof(TextAlignment),
typeof(SkiaLabel),
TextAlignment.Center,
TextAlignment.Start,
propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());
/// <summary>
@@ -140,12 +142,13 @@ public class SkiaLabel : SkiaView
/// <summary>
/// Bindable property for LineHeight.
/// Default is -1 to match MAUI Label.LineHeight (platform default).
/// </summary>
public static readonly BindableProperty LineHeightProperty = BindableProperty.Create(
nameof(LineHeight),
typeof(double),
typeof(SkiaLabel),
1.2,
-1.0,
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());
/// <summary>
@@ -203,10 +206,11 @@ public class SkiaLabel : SkiaView
/// <summary>
/// Gets or sets the text color.
/// Null means use platform default (black on Linux).
/// </summary>
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);
}
}

View File

@@ -42,12 +42,15 @@ public class SkiaSlider : SkiaView
BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged());
/// <summary>
/// Maximum property - default is 1.0 to match MAUI Slider.Maximum.
/// </summary>
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));
/// <summary>
/// MinimumTrackColor - default is null to match MAUI (platform default).
/// </summary>
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());
/// <summary>
/// MaximumTrackColor - default is null to match MAUI (platform default).
/// </summary>
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());
/// <summary>
/// ThumbColor - default is null to match MAUI (platform default).
/// </summary>
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
/// <summary>
/// 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).
/// </summary>
public Color MinimumTrackColor
public Color? MinimumTrackColor
{
get => (Color)GetValue(MinimumTrackColorProperty);
get => (Color?)GetValue(MinimumTrackColorProperty);
set => SetValue(MinimumTrackColorProperty, value);
}
/// <summary>
/// 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).
/// </summary>
public Color MaximumTrackColor
public Color? MaximumTrackColor
{
get => (Color)GetValue(MaximumTrackColorProperty);
get => (Color?)GetValue(MaximumTrackColorProperty);
set => SetValue(MaximumTrackColorProperty, value);
}
/// <summary>
/// Gets or sets the thumb color.
/// Null means platform default (Material Blue on Linux).
/// </summary>
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;
/// <summary>
/// Gets or sets the color used when disabled.
/// </summary>
@@ -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

296
Views/SkiaStateTrigger.cs Normal file
View File

@@ -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;
/// <summary>
/// Base class for state triggers that automatically activate visual states.
/// </summary>
public abstract class SkiaStateTriggerBase
{
private bool _isActive;
private SkiaVisualState? _ownerState;
private SkiaView? _ownerView;
/// <summary>
/// Gets whether this trigger is currently active.
/// </summary>
public bool IsActive
{
get => _isActive;
protected set
{
if (_isActive != value)
{
_isActive = value;
OnIsActiveChanged();
}
}
}
/// <summary>
/// Gets or sets the visual state this trigger belongs to.
/// </summary>
internal SkiaVisualState? OwnerState
{
get => _ownerState;
set => _ownerState = value;
}
/// <summary>
/// Gets or sets the view this trigger is attached to.
/// </summary>
internal SkiaView? OwnerView
{
get => _ownerView;
set
{
_ownerView = value;
OnAttached();
}
}
/// <summary>
/// Called when the trigger is attached to a view.
/// </summary>
protected virtual void OnAttached()
{
}
/// <summary>
/// Called when IsActive changes.
/// </summary>
protected virtual void OnIsActiveChanged()
{
if (_isActive && _ownerState != null && _ownerView != null)
{
SkiaVisualStateManager.GoToState(_ownerView, _ownerState.Name);
}
}
}
/// <summary>
/// A trigger that activates based on a boolean property.
/// Maps to MAUI StateTrigger.
/// </summary>
public class SkiaStateTrigger : SkiaStateTriggerBase
{
private bool _isActiveValue;
/// <summary>
/// Gets or sets whether this trigger should be active.
/// </summary>
public bool IsActiveValue
{
get => _isActiveValue;
set
{
_isActiveValue = value;
IsActive = value;
}
}
}
/// <summary>
/// A trigger that activates based on window size thresholds.
/// Maps to MAUI AdaptiveTrigger.
/// </summary>
public class SkiaAdaptiveTrigger : SkiaStateTriggerBase
{
private double _minWindowWidth = -1;
private double _minWindowHeight = -1;
/// <summary>
/// Gets or sets the minimum window width for this trigger to activate.
/// </summary>
public double MinWindowWidth
{
get => _minWindowWidth;
set
{
_minWindowWidth = value;
UpdateIsActive();
}
}
/// <summary>
/// Gets or sets the minimum window height for this trigger to activate.
/// </summary>
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;
}
}
/// <summary>
/// A trigger that activates when a property equals a specific value.
/// Maps to MAUI CompareStateTrigger.
/// </summary>
public class SkiaCompareStateTrigger : SkiaStateTriggerBase
{
private object? _property;
private object? _value;
/// <summary>
/// Gets or sets the property value to compare.
/// </summary>
public object? Property
{
get => _property;
set
{
_property = value;
UpdateIsActive();
}
}
/// <summary>
/// Gets or sets the value to compare against.
/// </summary>
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);
}
}
/// <summary>
/// A trigger that activates based on device idiom (Desktop, Phone, Tablet, etc.).
/// </summary>
public class SkiaDeviceStateTrigger : SkiaStateTriggerBase
{
private string _deviceType = "";
/// <summary>
/// Gets or sets the device type to match (Desktop, Phone, Tablet, Watch, TV).
/// </summary>
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);
}
}
/// <summary>
/// A trigger that activates based on orientation (Portrait or Landscape).
/// </summary>
public class SkiaOrientationStateTrigger : SkiaStateTriggerBase
{
private SkiaDisplayOrientation _orientation = SkiaDisplayOrientation.Portrait;
/// <summary>
/// Gets or sets the orientation to match.
/// </summary>
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;
}
}
/// <summary>
/// Display orientation values for state triggers.
/// </summary>
public enum SkiaDisplayOrientation
{
Portrait,
Landscape
}

View File

@@ -151,24 +151,26 @@ public abstract class SkiaView : BindableObject, IDisposable
/// <summary>
/// Bindable property for MinimumWidthRequest.
/// Default is -1 (unset) to match MAUI View.MinimumWidthRequest.
/// </summary>
public static readonly BindableProperty MinimumWidthRequestProperty =
BindableProperty.Create(
nameof(MinimumWidthRequest),
typeof(double),
typeof(SkiaView),
0.0,
-1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>
/// Bindable property for MinimumHeightRequest.
/// Default is -1 (unset) to match MAUI View.MinimumHeightRequest.
/// </summary>
public static readonly BindableProperty MinimumHeightRequestProperty =
BindableProperty.Create(
nameof(MinimumHeightRequest),
typeof(double),
typeof(SkiaView),
0.0,
-1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary>

View File

@@ -5,9 +5,53 @@ using System.Collections.Generic;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Represents a visual state with setters and optional triggers.
/// Maps to MAUI VisualState.
/// </summary>
public class SkiaVisualState
{
/// <summary>
/// Gets or sets the name of this visual state.
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// Gets the setters that define property changes for this state.
/// </summary>
public List<SkiaVisualStateSetter> Setters { get; } = new List<SkiaVisualStateSetter>();
/// <summary>
/// Gets the state triggers that can automatically activate this state.
/// </summary>
public List<SkiaStateTriggerBase> StateTriggers { get; } = new List<SkiaStateTriggerBase>();
/// <summary>
/// Gets or sets the target type this state applies to.
/// </summary>
public Type? TargetType { get; set; }
/// <summary>
/// Attaches triggers to the specified view.
/// </summary>
internal void AttachTriggers(SkiaView view)
{
foreach (var trigger in StateTriggers)
{
trigger.OwnerState = this;
trigger.OwnerView = view;
}
}
/// <summary>
/// Detaches triggers from the view.
/// </summary>
internal void DetachTriggers()
{
foreach (var trigger in StateTriggers)
{
trigger.OwnerState = null;
trigger.OwnerView = null;
}
}
}

View File

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

View File

@@ -5,33 +5,94 @@ using Microsoft.Maui.Controls;
namespace Microsoft.Maui.Platform;
/// <summary>
/// Represents a property setter within a visual state.
/// Maps to MAUI Setter class.
/// </summary>
public class SkiaVisualStateSetter
{
private object? _originalValue;
private bool _hasOriginalValue;
private SkiaView? _targetView;
/// <summary>
/// Gets or sets the property to set.
/// </summary>
public BindableProperty? Property { get; set; }
/// <summary>
/// Gets or sets the value to set.
/// </summary>
public object? Value { get; set; }
/// <summary>
/// Gets or sets the name of the target element within a template.
/// If null, the setter applies to the root element.
/// </summary>
public string? TargetName { get; set; }
/// <summary>
/// Applies the setter value to the view.
/// </summary>
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);
}
/// <summary>
/// Restores the original value on the view.
/// </summary>
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);
}
/// <summary>
/// Resolves the target view based on TargetName.
/// </summary>
private SkiaView? ResolveTarget(SkiaView view)
{
if (string.IsNullOrEmpty(TargetName))
return view;
// Find named element in visual tree
return FindNamedElement(view, TargetName);
}
/// <summary>
/// Finds a named element in the visual tree.
/// </summary>
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;
}
}