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> /// <summary>
/// Bindable property for TextColor. /// Bindable property for TextColor.
/// Default is null to match MAUI Button.TextColor (falls back to platform default).
/// </summary> /// </summary>
public static readonly BindableProperty TextColorProperty = BindableProperty.Create( public static readonly BindableProperty TextColorProperty = BindableProperty.Create(
nameof(TextColor), nameof(TextColor),
typeof(Color), typeof(Color),
typeof(SkiaButton), typeof(SkiaButton),
Colors.White, null,
propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate());
/// <summary> /// <summary>
@@ -109,12 +110,13 @@ public class SkiaButton : SkiaView, IButtonController
/// <summary> /// <summary>
/// Bindable property for BorderWidth. /// Bindable property for BorderWidth.
/// Default is -1 to match MAUI Button.BorderWidth (unset/platform default).
/// </summary> /// </summary>
public static readonly BindableProperty BorderWidthProperty = BindableProperty.Create( public static readonly BindableProperty BorderWidthProperty = BindableProperty.Create(
nameof(BorderWidth), nameof(BorderWidth),
typeof(double), typeof(double),
typeof(SkiaButton), typeof(SkiaButton),
0.0, -1.0,
propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate());
/// <summary> /// <summary>
@@ -208,10 +210,11 @@ public class SkiaButton : SkiaView, IButtonController
/// <summary> /// <summary>
/// Gets or sets the color of the text. /// Gets or sets the color of the text.
/// Null means use platform default (white on buttons for Linux).
/// </summary> /// </summary>
public Color TextColor public Color? TextColor
{ {
get => (Color)GetValue(TextColorProperty); get => (Color?)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value); set => SetValue(TextColorProperty, value);
} }
@@ -641,8 +644,8 @@ public class SkiaButton : SkiaView, IButtonController
SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(fontFamily, fontStyle) ?? SKTypeface.Default, SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(fontFamily, fontStyle) ?? SKTypeface.Default,
fontSize); fontSize);
// Prepare text color // Prepare text color (null means use platform default: white for buttons)
var textColor = ToSKColor(TextColor); var textColor = TextColor != null ? ToSKColor(TextColor) : SKColors.White;
if (!IsEnabled) if (!IsEnabled)
{ {
textColor = textColor.WithAlpha(128); textColor = textColor.WithAlpha(128);

View File

@@ -43,25 +43,27 @@ public class SkiaEditor : SkiaView
/// <summary> /// <summary>
/// Bindable property for TextColor. /// Bindable property for TextColor.
/// Default is null to match MAUI Editor.TextColor (falls back to platform default).
/// </summary> /// </summary>
public static readonly BindableProperty TextColorProperty = public static readonly BindableProperty TextColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(TextColor), nameof(TextColor),
typeof(Color), typeof(Color),
typeof(SkiaEditor), typeof(SkiaEditor),
Colors.Black, null,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
/// <summary> /// <summary>
/// Bindable property for PlaceholderColor. /// Bindable property for PlaceholderColor.
/// Default is null to match MAUI Editor.PlaceholderColor (falls back to platform default).
/// </summary> /// </summary>
public static readonly BindableProperty PlaceholderColorProperty = public static readonly BindableProperty PlaceholderColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(PlaceholderColor), nameof(PlaceholderColor),
typeof(Color), typeof(Color),
typeof(SkiaEditor), typeof(SkiaEditor),
Color.FromRgb(0x80, 0x80, 0x80), null,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -103,13 +105,14 @@ public class SkiaEditor : SkiaView
/// <summary> /// <summary>
/// Bindable property for FontFamily. /// Bindable property for FontFamily.
/// Default is empty string to match MAUI Editor.FontFamily (falls back to platform default).
/// </summary> /// </summary>
public static readonly BindableProperty FontFamilyProperty = public static readonly BindableProperty FontFamilyProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(FontFamily), nameof(FontFamily),
typeof(string), typeof(string),
typeof(SkiaEditor), typeof(SkiaEditor),
"Sans", string.Empty,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure());
@@ -307,7 +310,7 @@ public class SkiaEditor : SkiaView
/// <summary> /// <summary>
/// Converts a MAUI Color to SkiaSharp SKColor. /// Converts a MAUI Color to SkiaSharp SKColor.
/// </summary> /// </summary>
private static SKColor ToSKColor(Color color) private static SKColor ToSKColor(Color? color)
{ {
if (color == null) return SKColors.Transparent; if (color == null) return SKColors.Transparent;
return new SKColor( return new SKColor(
@@ -317,6 +320,30 @@ public class SkiaEditor : SkiaView
(byte)(color.Alpha * 255)); (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 #endregion
#region Properties #region Properties
@@ -340,20 +367,20 @@ public class SkiaEditor : SkiaView
} }
/// <summary> /// <summary>
/// Gets or sets the text color. /// Gets or sets the text color. Null means platform default (black).
/// </summary> /// </summary>
public Color TextColor public Color? TextColor
{ {
get => (Color)GetValue(TextColorProperty); get => (Color?)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value); set => SetValue(TextColorProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the placeholder color. /// Gets or sets the placeholder color. Null means platform default (gray).
/// </summary> /// </summary>
public Color PlaceholderColor public Color? PlaceholderColor
{ {
get => (Color)GetValue(PlaceholderColorProperty); get => (Color?)GetValue(PlaceholderColorProperty);
set => SetValue(PlaceholderColorProperty, value); set => SetValue(PlaceholderColorProperty, value);
} }
@@ -780,14 +807,14 @@ public class SkiaEditor : SkiaView
{ {
using var placeholderPaint = new SKPaint(font) using var placeholderPaint = new SKPaint(font)
{ {
Color = ToSKColor(PlaceholderColor), Color = GetEffectivePlaceholderColor(),
IsAntialias = true IsAntialias = true
}; };
canvas.DrawText(Placeholder, contentRect.Left, contentRect.Top + fontSize, placeholderPaint); canvas.DrawText(Placeholder, contentRect.Left, contentRect.Top + fontSize, placeholderPaint);
} }
else else
{ {
var textColor = ToSKColor(TextColor); var textColor = GetEffectiveTextColor();
using var textPaint = new SKPaint(font) using var textPaint = new SKPaint(font)
{ {
Color = IsEnabled ? textColor : textColor.WithAlpha(128), Color = IsEnabled ? textColor : textColor.WithAlpha(128),

View File

@@ -43,24 +43,26 @@ public class SkiaEntry : SkiaView
/// <summary> /// <summary>
/// Bindable property for PlaceholderColor. /// Bindable property for PlaceholderColor.
/// Default is null to match MAUI Entry.PlaceholderColor (falls back to platform default).
/// </summary> /// </summary>
public static readonly BindableProperty PlaceholderColorProperty = public static readonly BindableProperty PlaceholderColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(PlaceholderColor), nameof(PlaceholderColor),
typeof(Color), typeof(Color),
typeof(SkiaEntry), typeof(SkiaEntry),
Color.FromRgb(0x9E, 0x9E, 0x9E), null,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary> /// <summary>
/// Bindable property for TextColor. /// Bindable property for TextColor.
/// Default is null to match MAUI Entry.TextColor (falls back to platform default).
/// </summary> /// </summary>
public static readonly BindableProperty TextColorProperty = public static readonly BindableProperty TextColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(TextColor), nameof(TextColor),
typeof(Color), typeof(Color),
typeof(SkiaEntry), typeof(SkiaEntry),
Colors.Black, null,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary> /// <summary>
@@ -120,13 +122,14 @@ public class SkiaEntry : SkiaView
/// <summary> /// <summary>
/// Bindable property for FontFamily. /// Bindable property for FontFamily.
/// Default is empty string to match MAUI Entry.FontFamily (falls back to platform default).
/// </summary> /// </summary>
public static readonly BindableProperty FontFamilyProperty = public static readonly BindableProperty FontFamilyProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(FontFamily), nameof(FontFamily),
typeof(string), typeof(string),
typeof(SkiaEntry), typeof(SkiaEntry),
"Sans", string.Empty,
propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure());
/// <summary> /// <summary>
@@ -229,13 +232,14 @@ public class SkiaEntry : SkiaView
/// <summary> /// <summary>
/// Bindable property for VerticalTextAlignment. /// Bindable property for VerticalTextAlignment.
/// Default is Start to match MAUI Entry.VerticalTextAlignment.
/// </summary> /// </summary>
public static readonly BindableProperty VerticalTextAlignmentProperty = public static readonly BindableProperty VerticalTextAlignmentProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(VerticalTextAlignment), nameof(VerticalTextAlignment),
typeof(TextAlignment), typeof(TextAlignment),
typeof(SkiaEntry), typeof(SkiaEntry),
TextAlignment.Center, TextAlignment.Start,
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
/// <summary> /// <summary>
@@ -365,20 +369,20 @@ public class SkiaEntry : SkiaView
} }
/// <summary> /// <summary>
/// Gets or sets the placeholder color. /// Gets or sets the placeholder color. Null means platform default (gray).
/// </summary> /// </summary>
public Color PlaceholderColor public Color? PlaceholderColor
{ {
get => (Color)GetValue(PlaceholderColorProperty); get => (Color?)GetValue(PlaceholderColorProperty);
set => SetValue(PlaceholderColorProperty, value); set => SetValue(PlaceholderColorProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the text color. /// Gets or sets the text color. Null means platform default (black).
/// </summary> /// </summary>
public Color TextColor public Color? TextColor
{ {
get => (Color)GetValue(TextColorProperty); get => (Color?)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value); set => SetValue(TextColorProperty, value);
} }
@@ -676,7 +680,7 @@ public class SkiaEntry : SkiaView
/// <summary> /// <summary>
/// Converts a MAUI Color to SkiaSharp SKColor for rendering. /// Converts a MAUI Color to SkiaSharp SKColor for rendering.
/// </summary> /// </summary>
private static SKColor ToSKColor(Color color) private static SKColor ToSKColor(Color? color)
{ {
if (color == null) return SKColors.Transparent; if (color == null) return SKColors.Transparent;
return new SKColor( return new SKColor(
@@ -686,6 +690,30 @@ public class SkiaEntry : SkiaView
(byte)(color.Alpha * 255)); (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) private void OnTextPropertyChanged(string oldText, string newText)
{ {
_cursorPosition = Math.Min(_cursorPosition, (newText ?? "").Length); _cursorPosition = Math.Min(_cursorPosition, (newText ?? "").Length);
@@ -742,7 +770,7 @@ public class SkiaEntry : SkiaView
canvas.ClipRect(contentBounds); canvas.ClipRect(contentBounds);
var fontStyle = GetFontStyle(); var fontStyle = GetFontStyle();
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(GetEffectiveFontFamily(), fontStyle)
?? SKTypeface.Default; ?? SKTypeface.Default;
using var font = new SKFont(typeface, (float)FontSize); using var font = new SKFont(typeface, (float)FontSize);
@@ -753,7 +781,7 @@ public class SkiaEntry : SkiaView
if (hasText) if (hasText)
{ {
paint.Color = ToSKColor(TextColor); paint.Color = GetEffectiveTextColor();
// Measure text to cursor position for scrolling // Measure text to cursor position for scrolling
var textToCursor = displayText.Substring(0, Math.Min(_cursorPosition, displayText.Length)); var textToCursor = displayText.Substring(0, Math.Min(_cursorPosition, displayText.Length));
@@ -798,7 +826,7 @@ public class SkiaEntry : SkiaView
else if (!string.IsNullOrEmpty(Placeholder)) else if (!string.IsNullOrEmpty(Placeholder))
{ {
// Draw placeholder // Draw placeholder
paint.Color = ToSKColor(PlaceholderColor); paint.Color = GetEffectivePlaceholderColor();
var textBounds = new SKRect(); var textBounds = new SKRect();
paint.MeasureText(Placeholder, ref textBounds); paint.MeasureText(Placeholder, ref textBounds);
@@ -1255,7 +1283,7 @@ public class SkiaEntry : SkiaView
if (string.IsNullOrEmpty(Text)) return 0; if (string.IsNullOrEmpty(Text)) return 0;
var fontStyle = GetFontStyle(); var fontStyle = GetFontStyle();
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(GetEffectiveFontFamily(), fontStyle)
?? SKTypeface.Default; ?? SKTypeface.Default;
using var font = new SKFont(typeface, (float)FontSize); using var font = new SKFont(typeface, (float)FontSize);
@@ -1428,7 +1456,7 @@ public class SkiaEntry : SkiaView
protected override SKSize MeasureOverride(SKSize availableSize) protected override SKSize MeasureOverride(SKSize availableSize)
{ {
var fontStyle = GetFontStyle(); var fontStyle = GetFontStyle();
var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(GetEffectiveFontFamily(), fontStyle)
?? SKTypeface.Default; ?? SKTypeface.Default;
using var font = new SKFont(typeface, (float)FontSize); using var font = new SKFont(typeface, (float)FontSize);

View File

@@ -30,12 +30,13 @@ public class SkiaLabel : SkiaView
/// <summary> /// <summary>
/// Bindable property for TextColor. /// Bindable property for TextColor.
/// Default is null to match MAUI Label.TextColor (falls back to platform default).
/// </summary> /// </summary>
public static readonly BindableProperty TextColorProperty = BindableProperty.Create( public static readonly BindableProperty TextColorProperty = BindableProperty.Create(
nameof(TextColor), nameof(TextColor),
typeof(Color), typeof(Color),
typeof(SkiaLabel), typeof(SkiaLabel),
Colors.Black, null,
propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());
/// <summary> /// <summary>
@@ -110,12 +111,13 @@ public class SkiaLabel : SkiaView
/// <summary> /// <summary>
/// Bindable property for VerticalTextAlignment. /// Bindable property for VerticalTextAlignment.
/// Default is Start to match MAUI Label.VerticalTextAlignment.
/// </summary> /// </summary>
public static readonly BindableProperty VerticalTextAlignmentProperty = BindableProperty.Create( public static readonly BindableProperty VerticalTextAlignmentProperty = BindableProperty.Create(
nameof(VerticalTextAlignment), nameof(VerticalTextAlignment),
typeof(TextAlignment), typeof(TextAlignment),
typeof(SkiaLabel), typeof(SkiaLabel),
TextAlignment.Center, TextAlignment.Start,
propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());
/// <summary> /// <summary>
@@ -140,12 +142,13 @@ public class SkiaLabel : SkiaView
/// <summary> /// <summary>
/// Bindable property for LineHeight. /// Bindable property for LineHeight.
/// Default is -1 to match MAUI Label.LineHeight (platform default).
/// </summary> /// </summary>
public static readonly BindableProperty LineHeightProperty = BindableProperty.Create( public static readonly BindableProperty LineHeightProperty = BindableProperty.Create(
nameof(LineHeight), nameof(LineHeight),
typeof(double), typeof(double),
typeof(SkiaLabel), typeof(SkiaLabel),
1.2, -1.0,
propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());
/// <summary> /// <summary>
@@ -203,10 +206,11 @@ public class SkiaLabel : SkiaView
/// <summary> /// <summary>
/// Gets or sets the text color. /// Gets or sets the text color.
/// Null means use platform default (black on Linux).
/// </summary> /// </summary>
public Color TextColor public Color? TextColor
{ {
get => (Color)GetValue(TextColorProperty); get => (Color?)GetValue(TextColorProperty);
set => SetValue(TextColorProperty, value); 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) 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; float y = bounds.Top;
int lineCount = 0; int lineCount = 0;
@@ -869,7 +875,9 @@ public class SkiaLabel : SkiaView
float x = bounds.Left; float x = bounds.Left;
float y = bounds.Top; 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; float fontSize = FontSize > 0 ? (float)FontSize : 14f;
// Calculate baseline for first line // Calculate baseline for first line
@@ -1092,12 +1100,14 @@ public class SkiaLabel : SkiaView
using var paint = new SKPaint(font); using var paint = new SKPaint(font);
float width, height; 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) if (FormattedText != null && FormattedText.Spans.Count > 0)
{ {
// Measure formatted text // Measure formatted text
width = 0; width = 0;
height = (float)(fontSize * LineHeight); height = (float)(fontSize * effectiveLineHeight);
foreach (var span in FormattedText.Spans) foreach (var span in FormattedText.Spans)
{ {
if (!string.IsNullOrEmpty(span.Text)) if (!string.IsNullOrEmpty(span.Text))
@@ -1124,7 +1134,7 @@ public class SkiaLabel : SkiaView
{ {
var lines = displayText.Split('\n'); var lines = displayText.Split('\n');
int lineCount = MaxLines > 0 ? Math.Min(lines.Length, MaxLines) : lines.Length; 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, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); 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 = public static readonly BindableProperty MaximumProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(Maximum), nameof(Maximum),
typeof(double), typeof(double),
typeof(SkiaSlider), typeof(SkiaSlider),
100.0, 1.0, // MAUI default is 1.0, not 100.0
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged());
@@ -60,30 +63,39 @@ public class SkiaSlider : SkiaView
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).OnValuePropertyChanged((double)o, (double)n)); 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 = public static readonly BindableProperty MinimumTrackColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(MinimumTrackColor), nameof(MinimumTrackColor),
typeof(Color), typeof(Color),
typeof(SkiaSlider), typeof(SkiaSlider),
Color.FromRgb(0x21, 0x96, 0xF3), // Material Blue - active track null, // MAUI default is null (platform default)
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate());
/// <summary>
/// MaximumTrackColor - default is null to match MAUI (platform default).
/// </summary>
public static readonly BindableProperty MaximumTrackColorProperty = public static readonly BindableProperty MaximumTrackColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(MaximumTrackColor), nameof(MaximumTrackColor),
typeof(Color), typeof(Color),
typeof(SkiaSlider), typeof(SkiaSlider),
Color.FromRgb(0xE0, 0xE0, 0xE0), // Gray - inactive track null, // MAUI default is null (platform default)
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate());
/// <summary>
/// ThumbColor - default is null to match MAUI (platform default).
/// </summary>
public static readonly BindableProperty ThumbColorProperty = public static readonly BindableProperty ThumbColorProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(ThumbColor), nameof(ThumbColor),
typeof(Color), typeof(Color),
typeof(SkiaSlider), typeof(SkiaSlider),
Color.FromRgb(0x21, 0x96, 0xF3), // Material Blue null, // MAUI default is null (platform default)
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate());
@@ -147,33 +159,43 @@ public class SkiaSlider : SkiaView
/// <summary> /// <summary>
/// Gets or sets the color of the track from minimum to current value. /// 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> /// </summary>
public Color MinimumTrackColor public Color? MinimumTrackColor
{ {
get => (Color)GetValue(MinimumTrackColorProperty); get => (Color?)GetValue(MinimumTrackColorProperty);
set => SetValue(MinimumTrackColorProperty, value); set => SetValue(MinimumTrackColorProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the color of the track from current value to maximum. /// 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> /// </summary>
public Color MaximumTrackColor public Color? MaximumTrackColor
{ {
get => (Color)GetValue(MaximumTrackColorProperty); get => (Color?)GetValue(MaximumTrackColorProperty);
set => SetValue(MaximumTrackColorProperty, value); set => SetValue(MaximumTrackColorProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the thumb color. /// Gets or sets the thumb color.
/// Null means platform default (Material Blue on Linux).
/// </summary> /// </summary>
public Color ThumbColor public Color? ThumbColor
{ {
get => (Color)GetValue(ThumbColorProperty); get => (Color?)GetValue(ThumbColorProperty);
set => SetValue(ThumbColorProperty, value); 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> /// <summary>
/// Gets or sets the color used when disabled. /// Gets or sets the color used when disabled.
/// </summary> /// </summary>
@@ -272,10 +294,10 @@ public class SkiaSlider : SkiaView
var percentage = Maximum > Minimum ? (Value - Minimum) / (Maximum - Minimum) : 0; var percentage = Maximum > Minimum ? (Value - Minimum) / (Maximum - Minimum) : 0;
var thumbX = trackLeft + (float)(percentage * trackWidth); var thumbX = trackLeft + (float)(percentage * trackWidth);
// Get colors // Get colors (using helper methods for platform defaults when null)
var minTrackColorSK = ToSKColor(MinimumTrackColor); var minTrackColorSK = GetEffectiveMinimumTrackColor();
var maxTrackColorSK = ToSKColor(MaximumTrackColor); var maxTrackColorSK = GetEffectiveMaximumTrackColor();
var thumbColorSK = ToSKColor(ThumbColor); var thumbColorSK = GetEffectiveThumbColor();
var disabledColorSK = ToSKColor(DisabledColor); var disabledColorSK = ToSKColor(DisabledColor);
// Draw inactive (maximum) track // 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> /// <summary>
/// Bindable property for MinimumWidthRequest. /// Bindable property for MinimumWidthRequest.
/// Default is -1 (unset) to match MAUI View.MinimumWidthRequest.
/// </summary> /// </summary>
public static readonly BindableProperty MinimumWidthRequestProperty = public static readonly BindableProperty MinimumWidthRequestProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(MinimumWidthRequest), nameof(MinimumWidthRequest),
typeof(double), typeof(double),
typeof(SkiaView), typeof(SkiaView),
0.0, -1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary> /// <summary>
/// Bindable property for MinimumHeightRequest. /// Bindable property for MinimumHeightRequest.
/// Default is -1 (unset) to match MAUI View.MinimumHeightRequest.
/// </summary> /// </summary>
public static readonly BindableProperty MinimumHeightRequestProperty = public static readonly BindableProperty MinimumHeightRequestProperty =
BindableProperty.Create( BindableProperty.Create(
nameof(MinimumHeightRequest), nameof(MinimumHeightRequest),
typeof(double), typeof(double),
typeof(SkiaView), typeof(SkiaView),
0.0, -1.0,
propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure());
/// <summary> /// <summary>

View File

@@ -5,9 +5,53 @@ using System.Collections.Generic;
namespace Microsoft.Maui.Platform; namespace Microsoft.Maui.Platform;
/// <summary>
/// Represents a visual state with setters and optional triggers.
/// Maps to MAUI VisualState.
/// </summary>
public class SkiaVisualState public class SkiaVisualState
{ {
/// <summary>
/// Gets or sets the name of this visual state.
/// </summary>
public string Name { get; set; } = ""; 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>(); 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) 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 // Detach old triggers
GoToState(view, CommonStates.Normal); 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; namespace Microsoft.Maui.Platform;
/// <summary>
/// Represents a property setter within a visual state.
/// Maps to MAUI Setter class.
/// </summary>
public class SkiaVisualStateSetter public class SkiaVisualStateSetter
{ {
private object? _originalValue; private object? _originalValue;
private bool _hasOriginalValue; private bool _hasOriginalValue;
private SkiaView? _targetView;
/// <summary>
/// Gets or sets the property to set.
/// </summary>
public BindableProperty? Property { get; set; } public BindableProperty? Property { get; set; }
/// <summary>
/// Gets or sets the value to set.
/// </summary>
public object? Value { get; set; } 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) public void Apply(SkiaView view)
{ {
if (Property != null) var target = ResolveTarget(view);
if (target == null || Property == null)
return;
if (!_hasOriginalValue)
{ {
if (!_hasOriginalValue) _originalValue = target.GetValue(Property);
{ _hasOriginalValue = true;
_originalValue = view.GetValue(Property); _targetView = target;
_hasOriginalValue = true;
}
view.SetValue(Property, Value);
} }
target.SetValue(Property, Value);
} }
/// <summary>
/// Restores the original value on the view.
/// </summary>
public void Unapply(SkiaView 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);
}
/// <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;
} }
} }