Skip to content

Commit b27ff1d

Browse files
authored
Adds CSS support for shadows and a simpler way of defining shadows in XAML (#27180)
* Add shadow support to CSS + simpler initialization through type converter * CanConvertTo should be a Shadow as destinationtype * Update AssemblyInfo.cs * Use InlineData for tests and change parsing for colors/brushes * Add more tests and color schemes * Remove the XML file * Somehow fat fingers got rid of a brace * Add APIs to definitions * Change to internal
1 parent 804a549 commit b27ff1d

File tree

11 files changed

+257
-7
lines changed

11 files changed

+257
-7
lines changed

src/Controls/src/Core/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@
163163
[assembly: StyleProperty("-maui-vertical-text-alignment", typeof(Label), nameof(TextAlignmentElement.VerticalTextAlignmentProperty))]
164164
[assembly: StyleProperty("-maui-thumb-color", typeof(Switch), nameof(Switch.ThumbColorProperty))]
165165

166+
[assembly: StyleProperty("-maui-shadow", typeof(VisualElement), nameof(VisualElement.ShadowProperty))]
167+
166168
//shell
167169
[assembly: StyleProperty("-maui-flyout-background", typeof(Shell), nameof(Shell.FlyoutBackgroundColorProperty))]
168170
[assembly: StyleProperty("-maui-shell-background", typeof(Element), nameof(Shell.BackgroundColorProperty), PropertyOwnerType = typeof(Shell))]

src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,4 @@ static readonly Microsoft.Maui.Controls.TitleBar.SubtitleProperty -> Microsoft.M
147147
static readonly Microsoft.Maui.Controls.TitleBar.TitleProperty -> Microsoft.Maui.Controls.BindableProperty!
148148
static readonly Microsoft.Maui.Controls.TitleBar.TrailingContentProperty -> Microsoft.Maui.Controls.BindableProperty!
149149
static readonly Microsoft.Maui.Controls.Window.TitleBarProperty -> Microsoft.Maui.Controls.BindableProperty!
150-
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
150+
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void

src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,4 +341,4 @@ virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>
341341
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewDelegator2<TItemsView, TViewController>.GetVisibleItemsIndex() -> (bool VisibleItems, int First, int Center, int Last)
342342
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.UpdateLayout() -> void
343343
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
344-
~Microsoft.Maui.Controls.Internals.TypedBindingBase.UpdateSourceEventName.set -> void
344+
~Microsoft.Maui.Controls.Internals.TypedBindingBase.UpdateSourceEventName.set -> void

src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,4 +341,4 @@ virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>
341341
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewController2<TItemsView>.UpdateVisibility() -> void
342342
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewDelegator2<TItemsView, TViewController>.GetVisibleItemsIndex() -> (bool VisibleItems, int First, int Center, int Last)
343343
virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.UpdateLayout() -> void
344-
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void
344+
override Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.MovedToWindow() -> void

src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,4 @@ static readonly Microsoft.Maui.Controls.TitleBar.SubtitleProperty -> Microsoft.M
142142
static readonly Microsoft.Maui.Controls.TitleBar.TitleProperty -> Microsoft.Maui.Controls.BindableProperty!
143143
static readonly Microsoft.Maui.Controls.TitleBar.TrailingContentProperty -> Microsoft.Maui.Controls.BindableProperty!
144144
static readonly Microsoft.Maui.Controls.Window.TitleBarProperty -> Microsoft.Maui.Controls.BindableProperty!
145-
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
145+
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void

src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,4 @@ static readonly Microsoft.Maui.Controls.TitleBar.SubtitleProperty -> Microsoft.M
148148
static readonly Microsoft.Maui.Controls.TitleBar.TitleProperty -> Microsoft.Maui.Controls.BindableProperty!
149149
static readonly Microsoft.Maui.Controls.TitleBar.TrailingContentProperty -> Microsoft.Maui.Controls.BindableProperty!
150150
static readonly Microsoft.Maui.Controls.Window.TitleBarProperty -> Microsoft.Maui.Controls.BindableProperty!
151-
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
151+
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void

src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,4 @@ static readonly Microsoft.Maui.Controls.TitleBar.SubtitleProperty -> Microsoft.M
142142
static readonly Microsoft.Maui.Controls.TitleBar.TitleProperty -> Microsoft.Maui.Controls.BindableProperty!
143143
static readonly Microsoft.Maui.Controls.TitleBar.TrailingContentProperty -> Microsoft.Maui.Controls.BindableProperty!
144144
static readonly Microsoft.Maui.Controls.Window.TitleBarProperty -> Microsoft.Maui.Controls.BindableProperty!
145-
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
145+
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void

src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,4 @@ static readonly Microsoft.Maui.Controls.TitleBar.SubtitleProperty -> Microsoft.M
142142
static readonly Microsoft.Maui.Controls.TitleBar.TitleProperty -> Microsoft.Maui.Controls.BindableProperty!
143143
static readonly Microsoft.Maui.Controls.TitleBar.TrailingContentProperty -> Microsoft.Maui.Controls.BindableProperty!
144144
static readonly Microsoft.Maui.Controls.Window.TitleBarProperty -> Microsoft.Maui.Controls.BindableProperty!
145-
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
145+
virtual Microsoft.Maui.Controls.Application.ActivateWindow(Microsoft.Maui.Controls.Window! window) -> void
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
#nullable disable
2+
3+
using System;
4+
using System.ComponentModel;
5+
using System.Globalization;
6+
using System.Linq;
7+
using System.Text.RegularExpressions;
8+
using Microsoft.Maui.Graphics;
9+
using Microsoft.Maui.Graphics.Converters;
10+
11+
namespace Microsoft.Maui.Controls
12+
{
13+
/// <summary>
14+
/// Type converter for converting a properly formatted string to a Shadow.
15+
/// </summary>
16+
internal class ShadowTypeConverter : TypeConverter
17+
{
18+
readonly ColorTypeConverter _colorTypeConverter = new ColorTypeConverter();
19+
20+
/// <summary>
21+
/// Checks whether the given <paramref name="sourceType" /> is a string.
22+
/// </summary>
23+
/// <param name="context">The context to use for conversion.</param>
24+
/// <param name="sourceType">The type to convert from.</param>
25+
/// <returns></returns>
26+
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
27+
=> sourceType == typeof(string);
28+
29+
/// <summary>
30+
/// Checks whether the given <paramref name="destinationType" /> is a Shadow.
31+
/// </summary>
32+
/// <param name="context">The context to use for conversion.</param>
33+
/// <param name="destinationType">The type to convert to.</param>
34+
/// <returns></returns>
35+
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
36+
=> destinationType == typeof(Shadow);
37+
38+
/// <summary>
39+
/// Converts <paramref name="value" /> to a Shadow.
40+
/// </summary>
41+
/// <param name="context">The context to use for conversion.</param>
42+
/// <param name="culture">The culture to use for conversion.</param>
43+
/// <param name="value">The value to convert.</param>
44+
/// <returns></returns>
45+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="value" /> is null.</exception>
46+
/// <exception cref="InvalidOperationException">Thrown when <paramref name="value" /> is not a valid Shadow.</exception>
47+
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
48+
{
49+
var strValue = value?.ToString();
50+
51+
if (strValue == null)
52+
{
53+
throw new ArgumentNullException(nameof(strValue));
54+
}
55+
56+
try
57+
{
58+
var regex = new Regex(@"
59+
# Match colors
60+
(
61+
\#([0-9a-fA-F]{3,8}) # Hex colors (#RGB, #RRGGBB, #RRGGBBAA)
62+
|rgb\(\s*\d+%\s*,\s*\d+%\s*,\s*\d+%\s*\) # rgb(percent, percent, percent)
63+
|rgba\(\s*\d+%\s*,\s*\d+%\s*,\s*\d+%\s*,\s*\d+(?:\.\d+)?\s*\) # rgba(percent, percent, percent, alpha)
64+
|rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\) # rgb(int, int, int)
65+
|rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\.\d+)?\s*\) # rgba(int, int, int, alpha)
66+
|hsl\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*\) # hsl(hue, saturation, lightness)
67+
|hsla\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*,\s*\d+(?:\.\d+)?\s*\) # hsla(hue, saturation, lightness, alpha)
68+
|hsv\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*\) # hsl(hue, saturation, value)
69+
|hsva\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*,\s*\d+(?:\.\d+)?\s*\) # hsla(hue, saturation, value, alpha)
70+
|[a-zA-Z]+ # X11 named colors (e.g., AliceBlue, limegreen)
71+
72+
)
73+
| # Match numbers
74+
(
75+
-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)? # Floats or scientific notation
76+
)
77+
", RegexOptions.IgnorePatternWhitespace);
78+
79+
var matches = regex.Matches(strValue);
80+
//var parts = matches.Select(m => m.Value).ToArray();
81+
82+
if (matches.Count == 3) // <color> | <float> | <float> e.g. #000000 4 4
83+
{
84+
var brush = ParseBrush(matches[0].Value);
85+
var offsetX = float.Parse(matches[1].Value, CultureInfo.InvariantCulture);
86+
var offsetY = float.Parse(matches[2].Value, CultureInfo.InvariantCulture);
87+
88+
return new Shadow
89+
{
90+
Brush = brush,
91+
Offset = new Point(offsetX, offsetY)
92+
};
93+
}
94+
else if (matches.Count == 4) // <float> | <float> | <float> | <color> e.g. 4 4 16 #000000
95+
{
96+
var offsetX = float.Parse(matches[0].Value, CultureInfo.InvariantCulture);
97+
var offsetY = float.Parse(matches[1].Value, CultureInfo.InvariantCulture);
98+
var radius = float.Parse(matches[2].Value, CultureInfo.InvariantCulture);
99+
var brush = ParseBrush(matches[3].Value);
100+
101+
return new Shadow
102+
{
103+
Offset = new Point(offsetX, offsetY),
104+
Radius = radius,
105+
Brush = brush
106+
};
107+
}
108+
else if (matches.Count == 5) // <float> | <float> | <float> | <color> | <float> e.g. 4 4 16 #000000 0.5
109+
{
110+
var offsetX = float.Parse(matches[0].Value, CultureInfo.InvariantCulture);
111+
var offsetY = float.Parse(matches[1].Value, CultureInfo.InvariantCulture);
112+
var radius = float.Parse(matches[2].Value, CultureInfo.InvariantCulture);
113+
var brush = ParseBrush(matches[3].Value);
114+
var opacity = float.Parse(matches[4].Value, CultureInfo.InvariantCulture);
115+
116+
return new Shadow
117+
{
118+
Offset = new Point(offsetX, offsetY),
119+
Radius = radius,
120+
Brush = brush,
121+
Opacity = opacity
122+
};
123+
}
124+
}
125+
catch (Exception ex)
126+
{
127+
throw new InvalidOperationException($"Cannot convert \"{strValue}\" into {typeof(Shadow)}.", ex);
128+
}
129+
130+
throw new InvalidOperationException($"Cannot convert \"{strValue}\" into {typeof(IShadow)}.");
131+
}
132+
133+
/// <summary>
134+
/// Converts a Shadow to a string.
135+
/// </summary>
136+
/// <param name="context">The context to use for conversion.</param>
137+
/// <param name="culture">The culture to use for conversion.</param>
138+
/// <param name="value">The Shadow to convert.</param>
139+
/// <param name="destinationType">The type to convert to.</param>
140+
/// <returns>A string representation of the Shadow.</returns>
141+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="value" /> is null.</exception>
142+
/// <exception cref="InvalidOperationException">Thrown when <paramref name="value" /> is not a Shadow or the Brush is not a SolidColorBrush.</exception>
143+
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
144+
{
145+
if (value is null)
146+
{
147+
throw new ArgumentNullException(nameof(value));
148+
}
149+
150+
if (value is Shadow shadow)
151+
{
152+
var offsetX = shadow.Offset.X.ToString(CultureInfo.InvariantCulture);
153+
var offsetY = shadow.Offset.Y.ToString(CultureInfo.InvariantCulture);
154+
var radius = shadow.Radius.ToString(CultureInfo.InvariantCulture);
155+
var color = (shadow.Brush as SolidColorBrush)?.Color.ToHex();
156+
var opacity = shadow.Opacity.ToString(CultureInfo.InvariantCulture);
157+
158+
if (color == null)
159+
{
160+
throw new InvalidOperationException("Cannot convert Shadow to string: Brush is not a valid SolidColorBrush or has no Color.");
161+
}
162+
163+
return $"{offsetX} {offsetY} {radius} {color} {opacity}";
164+
}
165+
166+
throw new InvalidOperationException($"Cannot convert \"{value}\" into string.");
167+
}
168+
169+
/// <summary>
170+
/// Parses a string value into a SolidColorBrush.
171+
/// </summary>
172+
/// <param name="value">The value to parse.</param>
173+
/// <returns>A SolidColorBrush.</returns>
174+
/// <exception cref="InvalidOperationException">Thrown when the value is not a SolidColorBrush or has no Color.</exception>
175+
SolidColorBrush ParseBrush(string value)
176+
{
177+
// If the value is a color, return a SolidColorBrush
178+
if (_colorTypeConverter.ConvertFrom(value) is Color color)
179+
{
180+
return new SolidColorBrush(color);
181+
}
182+
183+
throw new InvalidOperationException("Cannot convert Shadow to string: Brush is not a valid SolidColorBrush or has no Color.");
184+
}
185+
}
186+
}

src/Controls/src/Core/VisualElement/VisualElement.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,6 +1851,7 @@ private protected override void OnHandlerChangedCore()
18511851
/// <summary>
18521852
/// Gets or sets the shadow effect cast by the element. This is a bindable property.
18531853
/// </summary>
1854+
[TypeConverter(typeof(ShadowTypeConverter))]
18541855
public Shadow Shadow
18551856
{
18561857
get { return (Shadow)GetValue(ShadowProperty); }

0 commit comments

Comments
 (0)