Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Controls/src/Core/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@
[assembly: StyleProperty("-maui-vertical-text-alignment", typeof(Label), nameof(TextAlignmentElement.VerticalTextAlignmentProperty))]
[assembly: StyleProperty("-maui-thumb-color", typeof(Switch), nameof(Switch.ThumbColorProperty))]

[assembly: StyleProperty("-maui-shadow", typeof(VisualElement), nameof(VisualElement.ShadowProperty))]

//shell
[assembly: StyleProperty("-maui-flyout-background", typeof(Shell), nameof(Shell.FlyoutBackgroundColorProperty))]
[assembly: StyleProperty("-maui-shell-background", typeof(Element), nameof(Shell.BackgroundColorProperty), PropertyOwnerType = typeof(Shell))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,4 @@ static readonly Microsoft.Maui.Controls.TitleBar.SubtitleProperty -> Microsoft.M
static readonly Microsoft.Maui.Controls.TitleBar.TitleProperty -> Microsoft.Maui.Controls.BindableProperty!
static readonly Microsoft.Maui.Controls.TitleBar.TrailingContentProperty -> Microsoft.Maui.Controls.BindableProperty!
static readonly Microsoft.Maui.Controls.Window.TitleBarProperty -> Microsoft.Maui.Controls.BindableProperty!
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -341,4 +341,4 @@ virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewDelegator2<TItemsView, TViewController>.GetVisibleItemsIndex() -> (bool VisibleItems, int First, int Center, int Last)
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.UpdateLayout() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
~Microsoft.Maui.Controls.Internals.TypedBindingBase.UpdateSourceEventName.set -> void
~Microsoft.Maui.Controls.Internals.TypedBindingBase.UpdateSourceEventName.set -> void
Original file line number Diff line number Diff line change
Expand Up @@ -341,4 +341,4 @@ virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>.UpdateVisibility() -> void
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewDelegator2<TItemsView, TViewController>.GetVisibleItemsIndex() -> (bool VisibleItems, int First, int Center, int Last)
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.UpdateLayout() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,4 @@ static readonly Microsoft.Maui.Controls.TitleBar.SubtitleProperty -> Microsoft.M
static readonly Microsoft.Maui.Controls.TitleBar.TitleProperty -> Microsoft.Maui.Controls.BindableProperty!
static readonly Microsoft.Maui.Controls.TitleBar.TrailingContentProperty -> Microsoft.Maui.Controls.BindableProperty!
static readonly Microsoft.Maui.Controls.Window.TitleBarProperty -> Microsoft.Maui.Controls.BindableProperty!
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,4 @@ static readonly Microsoft.Maui.Controls.TitleBar.SubtitleProperty -> Microsoft.M
static readonly Microsoft.Maui.Controls.TitleBar.TitleProperty -> Microsoft.Maui.Controls.BindableProperty!
static readonly Microsoft.Maui.Controls.TitleBar.TrailingContentProperty -> Microsoft.Maui.Controls.BindableProperty!
static readonly Microsoft.Maui.Controls.Window.TitleBarProperty -> Microsoft.Maui.Controls.BindableProperty!
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,4 @@ static readonly Microsoft.Maui.Controls.TitleBar.SubtitleProperty -> Microsoft.M
static readonly Microsoft.Maui.Controls.TitleBar.TitleProperty -> Microsoft.Maui.Controls.BindableProperty!
static readonly Microsoft.Maui.Controls.TitleBar.TrailingContentProperty -> Microsoft.Maui.Controls.BindableProperty!
static readonly Microsoft.Maui.Controls.Window.TitleBarProperty -> Microsoft.Maui.Controls.BindableProperty!
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,4 @@ static readonly Microsoft.Maui.Controls.TitleBar.SubtitleProperty -> Microsoft.M
static readonly Microsoft.Maui.Controls.TitleBar.TitleProperty -> Microsoft.Maui.Controls.BindableProperty!
static readonly Microsoft.Maui.Controls.TitleBar.TrailingContentProperty -> Microsoft.Maui.Controls.BindableProperty!
static readonly Microsoft.Maui.Controls.Window.TitleBarProperty -> Microsoft.Maui.Controls.BindableProperty!
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
186 changes: 186 additions & 0 deletions src/Controls/src/Core/ShadowTypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#nullable disable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be

Suggested change
#nullable disable
#nullable enable

?


using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Converters;

namespace Microsoft.Maui.Controls
{
/// <summary>
/// Type converter for converting a properly formatted string to a Shadow.
/// </summary>
internal class ShadowTypeConverter : TypeConverter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make ShadowTypeConverter public and enabled nullable references #nullable enable

{
readonly ColorTypeConverter _colorTypeConverter = new ColorTypeConverter();

/// <summary>
/// Checks whether the given <paramref name="sourceType" /> is a string.
/// </summary>
/// <param name="context">The context to use for conversion.</param>
/// <param name="sourceType">The type to convert from.</param>
/// <returns></returns>
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
=> sourceType == typeof(string);

/// <summary>
/// Checks whether the given <paramref name="destinationType" /> is a Shadow.
/// </summary>
/// <param name="context">The context to use for conversion.</param>
/// <param name="destinationType">The type to convert to.</param>
/// <returns></returns>
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
=> destinationType == typeof(Shadow);

/// <summary>
/// Converts <paramref name="value" /> to a Shadow.
/// </summary>
/// <param name="context">The context to use for conversion.</param>
/// <param name="culture">The culture to use for conversion.</param>
/// <param name="value">The value to convert.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="value" /> is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when <paramref name="value" /> is not a valid Shadow.</exception>
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var strValue = value?.ToString();

if (strValue == null)
{
throw new ArgumentNullException(nameof(strValue));
}

try
{
var regex = new Regex(@"
# Match colors
(
\#([0-9a-fA-F]{3,8}) # Hex colors (#RGB, #RRGGBB, #RRGGBBAA)
|rgb\(\s*\d+%\s*,\s*\d+%\s*,\s*\d+%\s*\) # rgb(percent, percent, percent)
|rgba\(\s*\d+%\s*,\s*\d+%\s*,\s*\d+%\s*,\s*\d+(?:\.\d+)?\s*\) # rgba(percent, percent, percent, alpha)
|rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\) # rgb(int, int, int)
|rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\.\d+)?\s*\) # rgba(int, int, int, alpha)
|hsl\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*\) # hsl(hue, saturation, lightness)
|hsla\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*,\s*\d+(?:\.\d+)?\s*\) # hsla(hue, saturation, lightness, alpha)
|hsv\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*\) # hsl(hue, saturation, value)
|hsva\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*,\s*\d+(?:\.\d+)?\s*\) # hsla(hue, saturation, value, alpha)
|[a-zA-Z]+ # X11 named colors (e.g., AliceBlue, limegreen)

)
| # Match numbers
(
-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)? # Floats or scientific notation
)
", RegexOptions.IgnorePatternWhitespace);

var matches = regex.Matches(strValue);
//var parts = matches.Select(m => m.Value).ToArray();

if (matches.Count == 3) // <color> | <float> | <float> e.g. #000000 4 4
{
var brush = ParseBrush(matches[0].Value);
var offsetX = float.Parse(matches[1].Value, CultureInfo.InvariantCulture);
var offsetY = float.Parse(matches[2].Value, CultureInfo.InvariantCulture);

return new Shadow
{
Brush = brush,
Offset = new Point(offsetX, offsetY)
};
}
else if (matches.Count == 4) // <float> | <float> | <float> | <color> e.g. 4 4 16 #000000
{
var offsetX = float.Parse(matches[0].Value, CultureInfo.InvariantCulture);
var offsetY = float.Parse(matches[1].Value, CultureInfo.InvariantCulture);
var radius = float.Parse(matches[2].Value, CultureInfo.InvariantCulture);
var brush = ParseBrush(matches[3].Value);

return new Shadow
{
Offset = new Point(offsetX, offsetY),
Radius = radius,
Brush = brush
};
}
else if (matches.Count == 5) // <float> | <float> | <float> | <color> | <float> e.g. 4 4 16 #000000 0.5
{
var offsetX = float.Parse(matches[0].Value, CultureInfo.InvariantCulture);
var offsetY = float.Parse(matches[1].Value, CultureInfo.InvariantCulture);
var radius = float.Parse(matches[2].Value, CultureInfo.InvariantCulture);
var brush = ParseBrush(matches[3].Value);
var opacity = float.Parse(matches[4].Value, CultureInfo.InvariantCulture);

return new Shadow
{
Offset = new Point(offsetX, offsetY),
Radius = radius,
Brush = brush,
Opacity = opacity
};
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"Cannot convert \"{strValue}\" into {typeof(Shadow)}.", ex);
}

throw new InvalidOperationException($"Cannot convert \"{strValue}\" into {typeof(IShadow)}.");
}

/// <summary>
/// Converts a Shadow to a string.
/// </summary>
/// <param name="context">The context to use for conversion.</param>
/// <param name="culture">The culture to use for conversion.</param>
/// <param name="value">The Shadow to convert.</param>
/// <param name="destinationType">The type to convert to.</param>
/// <returns>A string representation of the Shadow.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="value" /> is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when <paramref name="value" /> is not a Shadow or the Brush is not a SolidColorBrush.</exception>
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}

if (value is Shadow shadow)
{
var offsetX = shadow.Offset.X.ToString(CultureInfo.InvariantCulture);
var offsetY = shadow.Offset.Y.ToString(CultureInfo.InvariantCulture);
var radius = shadow.Radius.ToString(CultureInfo.InvariantCulture);
var color = (shadow.Brush as SolidColorBrush)?.Color.ToHex();
var opacity = shadow.Opacity.ToString(CultureInfo.InvariantCulture);

if (color == null)
{
throw new InvalidOperationException("Cannot convert Shadow to string: Brush is not a valid SolidColorBrush or has no Color.");
}

return $"{offsetX} {offsetY} {radius} {color} {opacity}";
}

throw new InvalidOperationException($"Cannot convert \"{value}\" into string.");
}

/// <summary>
/// Parses a string value into a SolidColorBrush.
/// </summary>
/// <param name="value">The value to parse.</param>
/// <returns>A SolidColorBrush.</returns>
/// <exception cref="InvalidOperationException">Thrown when the value is not a SolidColorBrush or has no Color.</exception>
SolidColorBrush ParseBrush(string value)
{
// If the value is a color, return a SolidColorBrush
if (_colorTypeConverter.ConvertFrom(value) is Color color)
{
return new SolidColorBrush(color);
}

throw new InvalidOperationException("Cannot convert Shadow to string: Brush is not a valid SolidColorBrush or has no Color.");
}
}
}
1 change: 1 addition & 0 deletions src/Controls/src/Core/VisualElement/VisualElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1851,6 +1851,7 @@ private protected override void OnHandlerChangedCore()
/// <summary>
/// Gets or sets the shadow effect cast by the element. This is a bindable property.
/// </summary>
[TypeConverter(typeof(ShadowTypeConverter))]
public Shadow Shadow
{
get { return (Shadow)GetValue(ShadowProperty); }
Expand Down
61 changes: 61 additions & 0 deletions src/Controls/tests/Core.UnitTests/ShadowTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Microsoft.Maui.Graphics;
using Xunit;

Expand Down Expand Up @@ -27,5 +28,65 @@ public void ShadowInitializesCorrectly()
Assert.Equal(expectedOpacity, shadow.Opacity);
Assert.Equal(expectedRadius, shadow.Radius);
}

[Theory]
[InlineData("#000000 4 4")]
[InlineData("rgb(6, 201, 198) 4 4")]
[InlineData("rgba(6, 201, 188, 0.2) 4 8")]
[InlineData("hsl(6, 20%, 45%) 1 5")]
[InlineData("hsla(6, 20%, 45%,0.75) 6 3")]
[InlineData("fuchsia 4 4")]
[InlineData("rgb(100%, 32%, 64%) 8 5")]
[InlineData("rgba(100%, 32%, 64%,0.27) 16 5")]
[InlineData("hsv(6, 20%, 45%) 1 5")]
[InlineData("hsva(6, 20%, 45%,0.75) 6 3")]
[InlineData("4 4 16 #FF00FF")]
[InlineData("4 4 16 AliceBlue")]
[InlineData("5 8 8 rgb(6, 201, 198)")]
[InlineData("7 5 4 rgba(6, 201, 188, 0.2)")]
[InlineData("9 4 6 hsl(6, 20%, 45%)")]
[InlineData("8 1 5 hsla(6, 20%, 45%,0.75)")]
[InlineData("5 2 8 rgb(100%, 32%, 64%)")]
[InlineData("1 5 3 rgba(100%, 32%, 64%,0.27)")]
[InlineData("4 4 16 #00FF00 0.5")]
[InlineData("4 4 16 limegreen 0.5")]
[InlineData("5 8 8 rgb(6, 201, 198) 0.5")]
[InlineData("7 5 4 rgba(6, 201, 188, 0.2) 0.5")]
[InlineData("9 4 6 hsl(6, 20%, 45%) 0.5")]
[InlineData("8 1 5 hsla(6, 20%, 45%,0.75) 0.5")]
[InlineData("9 4 6 hsv(6, 20%, 45%) 0.5")]
[InlineData("8 1 5 hsva(6, 20%, 45%,0.75) 0.5")]
[InlineData("5 2 8 rgb(100%, 32%, 64%) 0.5")]
[InlineData("1 5 3 rgba(100%, 32%, 64%,0.27) 0.5")]
public void ShadowTypeConverter_Valid(string value)
{
var converter = new ShadowTypeConverter();
Assert.True(converter.CanConvertFrom(typeof(string)));

bool actual = converter.IsValid(value);
Assert.True(actual);
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("invalid")]
[InlineData("#ZZZZZZ 4 4")]
[InlineData("4 4 #000000")]
[InlineData("4 4 dotnetpurple")]
[InlineData("rgb(6, 14.5, 198) 4 4")]
[InlineData("argb(0.2, 6, 201, 188) 4 8")]
[InlineData("hsl(6, 20%, 45.8%) 1 5")]
[InlineData("hsla(6.8, 20%, 45%,0.75) 6 3")]
[InlineData("hsv(6, 20%, 45.8%) 1 5")]
[InlineData("hsva(6.8, 20%, 45%,0.75) 6 3")]
[InlineData("rgb(100%, 32.9%, 64%) 8 5")]
[InlineData("argb(0.27, 100%, 32%, 64%) 16 5")]
public void ShadowTypeConverter_Invalid(string value)
{
ShadowTypeConverter converter = new ShadowTypeConverter();
bool actual = converter.IsValid(value);
Assert.False(actual);
}
}
}
Loading