diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 43d48be881..dbc495d933 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -26,6 +26,25 @@
+
+
+
+
+
@@ -34,6 +53,11 @@
+
+
+
+
+
diff --git a/src/Android/Avalonia.Android/Platform/AndroidMediaProvider.cs b/src/Android/Avalonia.Android/Platform/AndroidMediaProvider.cs
index fe59939340..958eea4b4d 100644
--- a/src/Android/Avalonia.Android/Platform/AndroidMediaProvider.cs
+++ b/src/Android/Avalonia.Android/Platform/AndroidMediaProvider.cs
@@ -51,5 +51,10 @@ namespace Avalonia.Android.Platform
_ => DeviceOrientation.Portrait,
};
}
+
+ public string GetPlatform()
+ {
+ return "android";
+ }
}
}
diff --git a/src/Avalonia.Base/Platform/IMediaProvider.cs b/src/Avalonia.Base/Platform/IMediaProvider.cs
index 7b53e919e8..ba087b5a4e 100644
--- a/src/Avalonia.Base/Platform/IMediaProvider.cs
+++ b/src/Avalonia.Base/Platform/IMediaProvider.cs
@@ -4,6 +4,7 @@ namespace Avalonia.Platform
{
public interface IMediaProvider
{
+ string GetPlatform();
double GetScreenWidth();
double GetScreenHeight();
diff --git a/src/Avalonia.Base/Platform/VisualMediaProvider.cs b/src/Avalonia.Base/Platform/VisualMediaProvider.cs
index 3bcf7c511a..8c387edef0 100644
--- a/src/Avalonia.Base/Platform/VisualMediaProvider.cs
+++ b/src/Avalonia.Base/Platform/VisualMediaProvider.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Reactive;
@@ -53,5 +54,17 @@ namespace Avalonia.Platform
return DeviceOrientation.Square;
}
+
+ public string GetPlatform()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return "windows";
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ return "linux";
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ return "osx";
+
+ return "";
+ }
}
}
diff --git a/src/Avalonia.Base/Styling/Activators/DeviceActivator.cs b/src/Avalonia.Base/Styling/Activators/DeviceActivator.cs
new file mode 100644
index 0000000000..236aa77f2b
--- /dev/null
+++ b/src/Avalonia.Base/Styling/Activators/DeviceActivator.cs
@@ -0,0 +1,14 @@
+namespace Avalonia.Styling.Activators
+{
+ internal sealed class IsOsActivator : MediaQueryActivatorBase
+ {
+ private readonly string _argument;
+
+ public IsOsActivator(Visual visual, string argument) : base(visual)
+ {
+ _argument = argument;
+ }
+
+ protected override bool EvaluateIsActive() => CurrentMediaInfoProvider != null && IsOsMediaSelector.Evaluate(CurrentMediaInfoProvider, _argument).IsMatch;
+ }
+}
diff --git a/src/Avalonia.Base/Styling/Activators/MediaQueryActivatorBase.cs b/src/Avalonia.Base/Styling/Activators/MediaQueryActivatorBase.cs
index ea5a9c6109..8699abba10 100644
--- a/src/Avalonia.Base/Styling/Activators/MediaQueryActivatorBase.cs
+++ b/src/Avalonia.Base/Styling/Activators/MediaQueryActivatorBase.cs
@@ -55,7 +55,7 @@ namespace Avalonia.Styling.Activators
ReevaluateIsActive();
}
- private void OrientationChanged(object sender, EventArgs e)
+ private void OrientationChanged(object? sender, EventArgs e)
{
ReevaluateIsActive();
}
diff --git a/src/Avalonia.Base/Styling/DeviceMediaSelector.cs b/src/Avalonia.Base/Styling/DeviceMediaSelector.cs
new file mode 100644
index 0000000000..cfeee24ea1
--- /dev/null
+++ b/src/Avalonia.Base/Styling/DeviceMediaSelector.cs
@@ -0,0 +1,45 @@
+using System;
+using Avalonia.Platform;
+using Avalonia.Styling.Activators;
+
+namespace Avalonia.Styling
+{
+ internal sealed class IsOsMediaSelector : MediaSelector
+ {
+ public IsOsMediaSelector(Selector? previous, string argument) : base(previous, argument)
+ {
+ }
+
+ private protected override SelectorMatch Evaluate(StyledElement control, IStyle? parent, bool subscribe)
+ {
+ if (!(control is Visual visual))
+ {
+ return SelectorMatch.NeverThisType;
+ }
+
+ if (subscribe)
+ {
+ return new SelectorMatch(new IsOsActivator(visual, Argument));
+ }
+
+ if (visual.VisualRoot is IMediaProviderHost mediaProviderHost && mediaProviderHost.MediaProvider is { } mediaProvider)
+ {
+ return Evaluate(mediaProvider, Argument);
+ }
+
+ return SelectorMatch.NeverThisInstance;
+ }
+
+ internal static SelectorMatch Evaluate(IMediaProvider mediaProvider, string argument)
+ {
+ return mediaProvider.GetPlatform() == argument ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
+ }
+
+ public override string ToString() => "is-os";
+
+ public override string ToString(Style? owner)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Styling/Selectors.cs b/src/Avalonia.Base/Styling/Selectors.cs
index 7ae9ec9f94..4b196e3515 100644
--- a/src/Avalonia.Base/Styling/Selectors.cs
+++ b/src/Avalonia.Base/Styling/Selectors.cs
@@ -221,6 +221,11 @@ namespace Avalonia.Styling
{
return new OrientationMediaSelector(previous, (DeviceOrientation)argument);
}
+
+ public static Selector IsOs(this Selector? previous, string argument)
+ {
+ return new IsOsMediaSelector(previous, argument);
+ }
///
/// Returns a selector which matches a control with the specified property value.
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
index b3c92622c8..3454b25c8c 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
@@ -162,6 +162,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
case SelectorGrammar.OrientationSyntax orientation:
result = new XamlIlOrientationSelector(result, orientation.Argument);
break;
+ case SelectorGrammar.IsOsSyntax isOs:
+ result = new XamlIlIsOsSelector(result, isOs.Argument);
+ break;
case SelectorGrammar.CommaSyntax comma:
if (results == null)
results = new XamlIlOrSelectorNode(node, selectorType);
@@ -490,6 +493,24 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
m => m.Name == "Orientation" && m.Parameters.Count == 2);
}
}
+
+ class XamlIlIsOsSelector : XamlIlSelectorNode
+ {
+ private string _argument;
+
+ public XamlIlIsOsSelector(XamlIlSelectorNode previous, string argument) : base(previous)
+ {
+ _argument = argument;
+ }
+
+ public override IXamlType TargetType => Previous?.TargetType;
+ protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen)
+ {
+ codeGen.Ldstr(_argument);
+ EmitCall(context, codeGen,
+ m => m.Name == "IsOs" && m.Parameters.Count == 2);
+ }
+ }
class XamlIlPropertyEqualsSelector : XamlIlSelectorNode
{
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
index 2e59a6767c..97d71525fa 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
@@ -183,6 +183,7 @@ namespace Avalonia.Markup.Parsers
const string NthChildKeyword = "nth-child";
const string NthLastChildKeyword = "nth-last-child";
const string OrientationKeyword = "orientation";
+ const string IsOsKeyword = "is-os";
if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('('))
{
@@ -253,6 +254,14 @@ namespace Avalonia.Markup.Parsers
var syntax = new OrientationSyntax { Argument = argument };
return (State.Middle, syntax);
}
+ if (identifier.SequenceEqual(IsOsKeyword.AsSpan()) && r.TakeIf('('))
+ {
+ var argument = ParseString(ref r);
+ Expect(ref r, ')');
+
+ var syntax = new IsOsSyntax { Argument = argument };
+ return (State.Middle, syntax);
+ }
else
{
return (
@@ -341,6 +350,11 @@ namespace Avalonia.Markup.Parsers
throw new ExpressionParseException(r.Position, $"Expected a {typeof(T)} after.");
}
+
+ private static string ParseString(ref CharacterReader r)
+ {
+ return r.ParseIdentifier().ToString();
+ }
private static (State, ISyntax) ParseTypeName(ref CharacterReader r)
{
@@ -763,6 +777,16 @@ namespace Avalonia.Markup.Parsers
}
}
+ public class IsOsSyntax : ISyntax
+ {
+ public string Argument { get; set; } = string.Empty;
+
+ public override bool Equals(object? obj)
+ {
+ return (obj is IsOsSyntax orientation) && orientation.Argument == Argument;
+ }
+ }
+
public class CommaSyntax : ISyntax
{
public override bool Equals(object? obj)
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
index 021f4bd4b6..b0f227005b 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
@@ -173,6 +173,9 @@ namespace Avalonia.Markup.Parsers
case SelectorGrammar.OrientationSyntax orientation:
result = result.Orientation(orientation.Argument);
break;
+ case SelectorGrammar.IsOsSyntax isOs:
+ result = result.IsOs(isOs.Argument);
+ break;
case SelectorGrammar.CommaSyntax comma:
if (results == null)
{