diff --git a/.nuspec/Xamarin.Forms.targets b/.nuspec/Xamarin.Forms.targets
index 034fce33ab8..7ec70f62538 100644
--- a/.nuspec/Xamarin.Forms.targets
+++ b/.nuspec/Xamarin.Forms.targets
@@ -1,5 +1,6 @@
+
@@ -33,7 +34,7 @@
- <_XFTasksExpectedAbi>3
+ <_XFTasksExpectedAbi>4
-
+
+
+
+
+ CssG;
+ $(CoreCompileDependsOn);
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/RDSourceTypeConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/RDSourceTypeConverter.cs
index b5417331e08..53621e6917f 100644
--- a/Xamarin.Forms.Build.Tasks/CompiledConverters/RDSourceTypeConverter.cs
+++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/RDSourceTypeConverter.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -13,6 +12,7 @@
namespace Xamarin.Forms.Core.XamlC
{
+
class RDSourceTypeConverter : ICompiledTypeConverter
{
public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node)
@@ -60,7 +60,7 @@ public IEnumerable ConvertFromString(string value, ILContext contex
yield return Create(Ldloc, uriVarDef);
}
- static string GetPathForType(ModuleDefinition module, TypeReference type)
+ internal static string GetPathForType(ModuleDefinition module, TypeReference type)
{
foreach (var ca in type.Module.GetCustomAttributes()) {
if (!TypeRefComparer.Default.Equals(ca.AttributeType, module.ImportReference(typeof(XamlResourceIdAttribute))))
@@ -72,7 +72,7 @@ static string GetPathForType(ModuleDefinition module, TypeReference type)
return null;
}
- static string GetResourceIdForPath(ModuleDefinition module, string path)
+ internal static string GetResourceIdForPath(ModuleDefinition module, string path)
{
foreach (var ca in module.GetCustomAttributes()) {
if (!TypeRefComparer.Default.Equals(ca.AttributeType, module.ImportReference(typeof(XamlResourceIdAttribute))))
diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/StyleSheetConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/StyleSheetConverter.cs
new file mode 100644
index 00000000000..3ea8ebddf74
--- /dev/null
+++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/StyleSheetConverter.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using Mono.Cecil.Cil;
+
+using static Mono.Cecil.Cil.Instruction;
+using static Mono.Cecil.Cil.OpCodes;
+
+using Xamarin.Forms.Build.Tasks;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Core.XamlC
+{
+ class StyleSheetConverter : ICompiledTypeConverter
+ {
+ public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node)
+ {
+ var module = context.Body.Method.Module;
+ var body = context.Body;
+
+ INode rootNode = node;
+ while (!(rootNode is ILRootNode))
+ rootNode = rootNode.Parent;
+
+ var rootTargetPath = RDSourceTypeConverter.GetPathForType(module, ((ILRootNode)rootNode).TypeReference);
+ var uri = new Uri(value, UriKind.Relative);
+
+ var resourceId = ResourceDictionary.RDSourceTypeConverter.GetResourceId(uri, rootTargetPath, s => RDSourceTypeConverter.GetResourceIdForPath(module, s));
+
+ //return StyleSheet.Parse(rootObjectType.GetTypeInfo().Assembly, resourceId, lineInfo);
+
+ var getTypeFromHandle = module.ImportReference(typeof(Type).GetMethod("GetTypeFromHandle", new[] { typeof(RuntimeTypeHandle) }));
+ var getAssembly = module.ImportReference(typeof(Type).GetProperty("Assembly").GetGetMethod());
+ yield return Create(Ldtoken, module.ImportReference(((ILRootNode)rootNode).TypeReference));
+ yield return Create(Call, module.ImportReference(getTypeFromHandle));
+ yield return Create(Callvirt, module.ImportReference(getAssembly)); //assembly
+
+ yield return Create(Ldstr, resourceId); //resourceId
+
+ foreach (var instruction in node.PushXmlLineInfo(context))
+ yield return instruction; //lineinfo
+
+ var styleSheetParse = module.ImportReference(typeof(StyleSheets.StyleSheet).GetMethods().FirstOrDefault(mi => mi.Name == "Parse" && mi.GetParameters().Length == 3));
+ yield return Create(Call, module.ImportReference(styleSheetParse));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Build.Tasks/CssGTask.cs b/Xamarin.Forms.Build.Tasks/CssGTask.cs
new file mode 100644
index 00000000000..872655d6fed
--- /dev/null
+++ b/Xamarin.Forms.Build.Tasks/CssGTask.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Xamarin.Forms.Build.Tasks
+{
+ public class CssGTask : Task
+ {
+ readonly List _generatedCodeFiles = new List();
+
+ [Required]
+ public ITaskItem[] XamlFiles { get; set; }
+
+ [Output]
+ public ITaskItem[] GeneratedCodeFiles => _generatedCodeFiles.ToArray();
+
+ public string Language { get; set; }
+ public string AssemblyName { get; set; }
+ public string OutputPath { get; set; }
+
+ public override bool Execute()
+ {
+ bool success = true;
+
+ if (XamlFiles == null) {
+ Log.LogMessage("Skipping CssG");
+ return true;
+ }
+
+ foreach (var xamlFile in XamlFiles) {
+ var outputFile = Path.Combine(OutputPath, $"{xamlFile.GetMetadata("TargetPath")}.g.cs");
+ var generator = new CssGenerator(xamlFile, Language, AssemblyName, outputFile, Log);
+ try {
+ if (generator.Execute())
+ _generatedCodeFiles.Add(new TaskItem(Microsoft.Build.Evaluation.ProjectCollection.Escape(outputFile)));
+ }
+ catch (XmlException xe) {
+ Log.LogError(null, null, null, xamlFile.ItemSpec, xe.LineNumber, xe.LinePosition, 0, 0, xe.Message, xe.HelpLink, xe.Source);
+
+ success = false;
+ }
+ catch (Exception e) {
+ Log.LogError(null, null, null, xamlFile.ItemSpec, 0, 0, 0, 0, e.Message, e.HelpLink, e.Source);
+ success = false;
+ }
+ }
+
+ return success;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Build.Tasks/CssGenerator.cs b/Xamarin.Forms.Build.Tasks/CssGenerator.cs
new file mode 100644
index 00000000000..85003f83398
--- /dev/null
+++ b/Xamarin.Forms.Build.Tasks/CssGenerator.cs
@@ -0,0 +1,97 @@
+using System.CodeDom;
+using System.CodeDom.Compiler;
+using System.IO;
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Microsoft.CSharp;
+
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Build.Tasks
+{
+ class CssGenerator
+ {
+ internal CssGenerator()
+ {
+ }
+
+ public CssGenerator(
+ ITaskItem taskItem,
+ string language,
+ string assemblyName,
+ string outputFile,
+ TaskLoggingHelper logger)
+ : this(
+ taskItem.ItemSpec,
+ language,
+ taskItem.GetMetadata("ManifestResourceName"),
+ taskItem.GetMetadata("TargetPath"),
+ assemblyName,
+ outputFile,
+ logger)
+ {
+ }
+
+ internal static CodeDomProvider Provider = new CSharpCodeProvider();
+
+ public string CssFile { get; }
+ public string Language { get; }
+ public string ResourceId { get; }
+ public string TargetPath { get; }
+ public string AssemblyName { get; }
+ public string OutputFile { get; }
+ public TaskLoggingHelper Logger { get; }
+
+ public CssGenerator(
+ string cssFile,
+ string language,
+ string resourceId,
+ string targetPath,
+ string assemblyName,
+ string outputFile,
+ TaskLoggingHelper logger = null)
+ {
+ CssFile = cssFile;
+ Language = language;
+ ResourceId = resourceId;
+ TargetPath = targetPath;
+ AssemblyName = assemblyName;
+ OutputFile = outputFile;
+ Logger = logger;
+ }
+
+ //returns true if a file is generated
+ public bool Execute()
+ {
+ Logger?.LogMessage("Source: {0}", CssFile);
+ Logger?.LogMessage(" Language: {0}", Language);
+ Logger?.LogMessage(" ResourceID: {0}", ResourceId);
+ Logger?.LogMessage(" TargetPath: {0}", TargetPath);
+ Logger?.LogMessage(" AssemblyName: {0}", AssemblyName);
+ Logger?.LogMessage(" OutputFile {0}", OutputFile);
+
+ GenerateCode();
+
+ return true;
+ }
+
+ void GenerateCode()
+ {
+ //Create the target directory if required
+ Directory.CreateDirectory(Path.GetDirectoryName(OutputFile));
+
+ var ccu = new CodeCompileUnit();
+ ccu.AssemblyCustomAttributes.Add(
+ new CodeAttributeDeclaration(new CodeTypeReference($"global::{typeof(XamlResourceIdAttribute).FullName}"),
+ new CodeAttributeArgument(new CodePrimitiveExpression(ResourceId)),
+ new CodeAttributeArgument(new CodePrimitiveExpression(TargetPath)),
+ new CodeAttributeArgument(new CodePrimitiveExpression(null))
+ ));
+
+ //write the result
+ using (var writer = new StreamWriter(OutputFile))
+ Provider.GenerateCodeFromCompileUnit(ccu, writer, new CodeGeneratorOptions());
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Build.Tasks/GetTasksAbi.cs b/Xamarin.Forms.Build.Tasks/GetTasksAbi.cs
index ff81018080e..ea7d14162cc 100644
--- a/Xamarin.Forms.Build.Tasks/GetTasksAbi.cs
+++ b/Xamarin.Forms.Build.Tasks/GetTasksAbi.cs
@@ -6,7 +6,7 @@ namespace Xamarin.Forms.Build.Tasks
public class GetTasksAbi : Task
{
[Output]
- public string AbiVersion { get; } = "3";
+ public string AbiVersion { get; } = "4";
public override bool Execute()
=> true;
diff --git a/Xamarin.Forms.Build.Tasks/TypeReferenceExtensions.cs b/Xamarin.Forms.Build.Tasks/TypeReferenceExtensions.cs
index f8a489b5e06..35d8888632a 100644
--- a/Xamarin.Forms.Build.Tasks/TypeReferenceExtensions.cs
+++ b/Xamarin.Forms.Build.Tasks/TypeReferenceExtensions.cs
@@ -21,6 +21,10 @@ static string GetAssembly(TypeReference typeRef)
public bool Equals(TypeReference x, TypeReference y)
{
+ if (x == null)
+ return y == null;
+ if (y == null)
+ return x == null;
if (x.FullName != y.FullName)
return false;
var xasm = GetAssembly(x);
diff --git a/Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj b/Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj
index 6d171f9371c..5472d52e9ae 100644
--- a/Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj
+++ b/Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj
@@ -102,6 +102,9 @@
+
+
+
diff --git a/Xamarin.Forms.Core.UnitTests/LabelTests.cs b/Xamarin.Forms.Core.UnitTests/LabelTests.cs
index 5e2ffb53f71..6fcaab9579d 100644
--- a/Xamarin.Forms.Core.UnitTests/LabelTests.cs
+++ b/Xamarin.Forms.Core.UnitTests/LabelTests.cs
@@ -189,8 +189,7 @@ public void FontSizeCanBeSetFromStyle ()
public void ManuallySetFontSizeNotOverridenByStyle ()
{
var label = new Label ();
-
- Assert.AreEqual (10.0, label.FontSize);
+ Assume.That (label.FontSize, Is.EqualTo(10.0));
label.SetValue (Label.FontSizeProperty, 2.0, false);
Assert.AreEqual (2.0, label.FontSize);
@@ -199,6 +198,19 @@ public void ManuallySetFontSizeNotOverridenByStyle ()
Assert.AreEqual (2.0, label.FontSize);
}
+ [Test]
+ public void ManuallySetFontSizeNotOverridenByFontSetInStyle()
+ {
+ var label = new Label();
+ Assume.That(label.FontSize, Is.EqualTo(10.0));
+
+ label.SetValue(Label.FontSizeProperty, 2.0);
+ Assert.AreEqual(2.0, label.FontSize);
+
+ label.SetValue(Label.FontProperty, Font.SystemFontOfSize(1.0), fromStyle: true);
+ Assert.AreEqual(2.0, label.FontSize);
+ }
+
[Test]
public void ChangingHorizontalTextAlignmentFiresXAlignChanged ()
{
diff --git a/Xamarin.Forms.Core.UnitTests/StyleSheets/IStylableTest.cs b/Xamarin.Forms.Core.UnitTests/StyleSheets/IStylableTest.cs
new file mode 100644
index 00000000000..f43be2e7977
--- /dev/null
+++ b/Xamarin.Forms.Core.UnitTests/StyleSheets/IStylableTest.cs
@@ -0,0 +1,51 @@
+using System;
+
+using NUnit.Framework;
+
+using Xamarin.Forms.Core.UnitTests;
+
+namespace Xamarin.Forms.StyleSheets.UnitTests
+{
+ [TestFixture]
+ public class IStylableTest
+ {
+ [SetUp]
+ public void SetUp()
+ {
+ Device.PlatformServices = new MockPlatformServices();
+ Internals.Registrar.RegisterAll(new Type[0]);
+ }
+
+ [TestCase]
+ public void GetBackgroundColor()
+ {
+ var label = new Label();
+ var bp = ((IStylable)label).GetProperty("background-color");
+ Assert.AreSame(VisualElement.BackgroundColorProperty, bp);
+ }
+
+ [TestCase]
+ public void GetLabelColor()
+ {
+ var label = new Label();
+ var bp = ((IStylable)label).GetProperty("color");
+ Assert.AreSame(Label.TextColorProperty, bp);
+ }
+
+ [TestCase]
+ public void GetEntryColor()
+ {
+ var entry = new Entry();
+ var bp = ((IStylable)entry).GetProperty("color");
+ Assert.AreSame(Entry.TextColorProperty, bp);
+ }
+
+ [TestCase]
+ public void GetGridColor()
+ {
+ var grid = new Grid();
+ var bp = ((IStylable)grid).GetProperty("color");
+ Assert.Null(bp);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core.UnitTests/StyleSheets/MockStylable.cs b/Xamarin.Forms.Core.UnitTests/StyleSheets/MockStylable.cs
new file mode 100644
index 00000000000..fa03ab44e39
--- /dev/null
+++ b/Xamarin.Forms.Core.UnitTests/StyleSheets/MockStylable.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using NUnit.Framework;
+
+namespace Xamarin.Forms.StyleSheets.UnitTests
+{
+ class MockStylable : IStyleSelectable
+ {
+ public IEnumerable Children { get; set; }
+ public IList Classes { get; set; }
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public IStyleSelectable Parent { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core.UnitTests/StyleSheets/SelectorTests.cs b/Xamarin.Forms.Core.UnitTests/StyleSheets/SelectorTests.cs
new file mode 100644
index 00000000000..49dd838d826
--- /dev/null
+++ b/Xamarin.Forms.Core.UnitTests/StyleSheets/SelectorTests.cs
@@ -0,0 +1,110 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using NUnit.Framework;
+
+namespace Xamarin.Forms.StyleSheets.UnitTests
+{
+ [TestFixture]
+ public class SelectorTests
+ {
+ IStyleSelectable Page;
+ IStyleSelectable StackLayout => Page.Children.First();
+ IStyleSelectable Label0 => StackLayout.Children.Skip(0).First();
+ IStyleSelectable Label1 => StackLayout.Children.Skip(1).First();
+ IStyleSelectable Label2 => ContentView0.Children.First();
+ IStyleSelectable Label3 => StackLayout.Children.Skip(3).First();
+ IStyleSelectable Label4 => StackLayout.Children.Skip(4).First();
+ IStyleSelectable ContentView0 => StackLayout.Children.Skip(2).First();
+
+ [SetUp]
+ public void SetUp()
+ {
+ Page = new MockStylable {
+ Name = "Page",
+ Children = new List {
+ new MockStylable {
+ Name = "StackLayout",
+ Children = new List {
+ new MockStylable {Name = "Label", Classes = new[]{"test"}}, //Label0
+ new MockStylable {Name = "Label"}, //Label1
+ new MockStylable { //ContentView0
+ Name = "ContentView",
+ Classes = new[]{"test"},
+ Children = new List {
+ new MockStylable {Name = "Label", Classes = new[]{"test"}}, //Label2
+ }
+ },
+ new MockStylable {Name = "Label", Id="foo"}, //Label3
+ new MockStylable {Name = "Label"}, //Label4
+ }
+ }
+ }
+ };
+ SetParents(Page);
+ }
+
+ void SetParents(IStyleSelectable stylable, IStyleSelectable parent = null)
+ {
+ ((MockStylable)stylable).Parent = parent;
+ if (stylable.Children == null)
+ return;
+ foreach (var s in stylable.Children)
+ SetParents(s, stylable);
+ }
+
+ [TestCase("label", true, true, true, true, true, false)]
+ [TestCase(" label", true, true, true, true, true, false)]
+ [TestCase("label ", true, true, true, true, true, false)]
+ [TestCase(".test", true, false, true, false, false, true)]
+ [TestCase("label.test", true, false, true, false, false, false)]
+ [TestCase("stacklayout>label.test", true, false, false, false, false, false)]
+ [TestCase("stacklayout >label.test", true, false, false, false, false, false)]
+ [TestCase("stacklayout> label.test", true, false, false, false, false, false)]
+ [TestCase("stacklayout label.test", true, false, true, false, false, false)]
+ [TestCase("stacklayout label.test", true, false, true, false, false, false)]
+ [TestCase("stacklayout .test", true, false, true, false, false, true)]
+ [TestCase("*", true, true, true, true, true, true)]
+ [TestCase("#foo", false, false, false, true, false, false)]
+ [TestCase("label#foo", false, false, false, true, false, false)]
+ [TestCase("div#foo", false, false, false, false, false, false)]
+ [TestCase(".test,#foo", true, false, true, true, false, true)]
+ [TestCase(".test ,#foo", true, false, true, true, false, true)]
+ [TestCase(".test, #foo", true, false, true, true, false, true)]
+ [TestCase("#foo,.test", true, false, true, true, false, true)]
+ [TestCase("#foo ,.test", true, false, true, true, false, true)]
+ [TestCase("#foo, .test", true, false, true, true, false, true)]
+ [TestCase("contentview+label", false, false, false, true, false, false)]
+ [TestCase("contentview +label", false, false, false, true, false, false)]
+ [TestCase("contentview+ label", false, false, false, true, false, false)]
+ [TestCase("contentview~label", false, false, false, true, true, false)]
+ [TestCase("contentview ~label", false, false, false, true, true, false)]
+ [TestCase("contentview\r\n~label", false, false, false, true, true, false)]
+ [TestCase("contentview~ label", false, false, false, true, true, false)]
+ [TestCase("label~*", false, true, false, true, true, true)]
+ [TestCase("label~.test", false, false, false, false, false, true)]
+ [TestCase("label~#foo", false, false, false, true, false, false)]
+ [TestCase("page contentview stacklayout label", false, false, false, false, false, false)]
+ [TestCase("page stacklayout contentview label", false, false, true, false, false, false)]
+ [TestCase("page contentview label", false, false, true, false, false, false)]
+ [TestCase("page contentview>label", false, false, true, false, false, false)]
+ [TestCase("page>stacklayout contentview label", false, false, true, false, false, false)]
+ [TestCase("page stacklayout>contentview label", false, false, true, false, false, false)]
+ [TestCase("page stacklayout contentview>label", false, false, true, false, false, false)]
+ [TestCase("page>stacklayout>contentview label", false, false, true, false, false, false)]
+ [TestCase("page>stack/* comment * */layout>contentview label", false, false, true, false, false, false)]
+ [TestCase("page>stacklayout contentview>label", false, false, true, false, false, false)]
+ [TestCase("page stacklayout>contentview>label", false, false, true, false, false, false)]
+ [TestCase("page>stacklayout>contentview>label", false, false, true, false, false, false)]
+ public void TestCase(string selectorString, bool label0match, bool label1match, bool label2match, bool label3match, bool label4match, bool content0match)
+ {
+ var selector = Selector.Parse(new CssReader(new StringReader(selectorString)));
+ Assert.AreEqual(label0match, selector.Matches(Label0));
+ Assert.AreEqual(label1match, selector.Matches(Label1));
+ Assert.AreEqual(label2match, selector.Matches(Label2));
+ Assert.AreEqual(label3match, selector.Matches(Label3));
+ Assert.AreEqual(label4match, selector.Matches(Label4));
+ Assert.AreEqual(content0match, selector.Matches(ContentView0));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core.UnitTests/StyleSheets/StyleTests.cs b/Xamarin.Forms.Core.UnitTests/StyleSheets/StyleTests.cs
new file mode 100644
index 00000000000..6680dddeff7
--- /dev/null
+++ b/Xamarin.Forms.Core.UnitTests/StyleSheets/StyleTests.cs
@@ -0,0 +1,82 @@
+using System;
+using System.IO;
+
+using NUnit.Framework;
+
+using Xamarin.Forms.Core.UnitTests;
+
+namespace Xamarin.Forms.StyleSheets.UnitTests
+{
+ [TestFixture]
+ public class StyleTests
+ {
+ [SetUp]
+ public void SetUp()
+ {
+ Device.PlatformServices = new MockPlatformServices();
+ Internals.Registrar.RegisterAll(new Type[0]);
+ }
+
+ [Test]
+ public void PropertiesAreApplied()
+ {
+ var styleString = @"background-color: #ff0000;";
+ var style = Style.Parse(new CssReader(new StringReader(styleString)), '}');
+ Assume.That(style, Is.Not.Null);
+
+ var ve = new VisualElement();
+ Assume.That(ve.BackgroundColor, Is.EqualTo(Color.Default));
+ style.Apply(ve);
+ Assert.That(ve.BackgroundColor, Is.EqualTo(Color.Red));
+ }
+
+ [Test]
+ public void PropertiesSetByStyleDoesNotOverrideManualOne()
+ {
+ var styleString = @"background-color: #ff0000;";
+ var style = Style.Parse(new CssReader(new StringReader(styleString)), '}');
+ Assume.That(style, Is.Not.Null);
+
+ var ve = new VisualElement() { BackgroundColor = Color.Pink };
+ Assume.That(ve.BackgroundColor, Is.EqualTo(Color.Pink));
+
+ style.Apply(ve);
+ Assert.That(ve.BackgroundColor, Is.EqualTo(Color.Pink));
+ }
+
+ [Test]
+ public void StylesAreCascading()
+ {
+ var styleString = @"background-color: #ff0000; color: #00ff00;";
+ var style = Style.Parse(new CssReader(new StringReader(styleString)), '}');
+ Assume.That(style, Is.Not.Null);
+
+ var label = new Label();
+ var layout = new StackLayout {
+ Children = {
+ label,
+ }
+ };
+
+ Assume.That(layout.BackgroundColor, Is.EqualTo(Color.Default));
+ Assume.That(label.BackgroundColor, Is.EqualTo(Color.Default));
+ Assume.That(label.TextColor, Is.EqualTo(Color.Default));
+
+ style.Apply(layout);
+ Assert.That(layout.BackgroundColor, Is.EqualTo(Color.Red));
+ Assert.That(label.BackgroundColor, Is.EqualTo(Color.Red));
+ Assert.That(label.TextColor, Is.EqualTo(Color.Lime));
+ }
+
+ [Test]
+ public void PropertiesAreOnlySetOnMatchingElements()
+ {
+ var styleString = @"background-color: #ff0000; color: #00ff00;";
+ var style = Style.Parse(new CssReader(new StringReader(styleString)), '}');
+ Assume.That(style, Is.Not.Null);
+
+ var layout = new StackLayout();
+ Assert.That(layout.GetValue(TextElement.TextColorProperty), Is.EqualTo(Color.Default));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
index 8cafd83edf1..daa584cd0eb 100644
--- a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
+++ b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
@@ -176,6 +176,10 @@
+
+
+
+
@@ -208,4 +212,7 @@
Images/crimson.jpg
+
+
+
diff --git a/Xamarin.Forms.Core/Element.cs b/Xamarin.Forms.Core/Element.cs
index 1f8725f2631..b185eb07516 100644
--- a/Xamarin.Forms.Core/Element.cs
+++ b/Xamarin.Forms.Core/Element.cs
@@ -9,7 +9,7 @@
namespace Xamarin.Forms
{
- public abstract class Element : BindableObject, IElement, INameScope, IElementController
+ public abstract partial class Element : BindableObject, IElement, INameScope, IElementController
{
public static readonly BindableProperty MenuProperty = BindableProperty.CreateAttached(nameof(Menu), typeof(Menu), typeof(Element), null);
diff --git a/Xamarin.Forms.Core/Element_StyleSheets.cs b/Xamarin.Forms.Core/Element_StyleSheets.cs
new file mode 100644
index 00000000000..908fc7e3192
--- /dev/null
+++ b/Xamarin.Forms.Core/Element_StyleSheets.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+
+using Xamarin.Forms.Internals;
+using Xamarin.Forms.StyleSheets;
+
+namespace Xamarin.Forms
+{
+ public partial class Element : IStyleSelectable
+ {
+ IEnumerable IStyleSelectable.Children => LogicalChildrenInternal;
+
+ IList IStyleSelectable.Classes => null;
+
+ string IStyleSelectable.Id => StyleId;
+
+ string _styleSelectableName;
+ string IStyleSelectable.Name => _styleSelectableName ?? (_styleSelectableName = GetType().Name);
+
+ IStyleSelectable IStyleSelectable.Parent => Parent;
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/IStyle.cs b/Xamarin.Forms.Core/IStyle.cs
index cdb998dc4bf..d9b3a04972e 100644
--- a/Xamarin.Forms.Core/IStyle.cs
+++ b/Xamarin.Forms.Core/IStyle.cs
@@ -2,7 +2,7 @@
namespace Xamarin.Forms
{
- internal interface IStyle
+ interface IStyle
{
Type TargetType { get; }
diff --git a/Xamarin.Forms.Core/Properties/AssemblyInfo.cs b/Xamarin.Forms.Core/Properties/AssemblyInfo.cs
index 429aca999d2..2f6ce234f6f 100644
--- a/Xamarin.Forms.Core/Properties/AssemblyInfo.cs
+++ b/Xamarin.Forms.Core/Properties/AssemblyInfo.cs
@@ -2,6 +2,7 @@
using System.Runtime.CompilerServices;
using Xamarin.Forms;
using Xamarin.Forms.Internals;
+using Xamarin.Forms.StyleSheets;
[assembly: AssemblyTitle("Xamarin.Forms.Core")]
[assembly: AssemblyDescription("")]
@@ -59,4 +60,7 @@
[assembly: InternalsVisibleTo("Xamarin.Forms.CarouselView")]
[assembly: Preserve]
-[assembly: XmlnsDefinition("http://xamarin.com/schemas/2014/forms", "Xamarin.Forms")]
\ No newline at end of file
+[assembly: XmlnsDefinition("http://xamarin.com/schemas/2014/forms", "Xamarin.Forms")]
+
+[assembly: StyleProperty("color", typeof(ITextElement), nameof(TextElement.TextColorProperty))]
+[assembly: StyleProperty("background-color", typeof(VisualElement), nameof(VisualElement.BackgroundColorProperty))]
diff --git a/Xamarin.Forms.Core/Registrar.cs b/Xamarin.Forms.Core/Registrar.cs
index 91b0375fc07..1c161b5b31d 100644
--- a/Xamarin.Forms.Core/Registrar.cs
+++ b/Xamarin.Forms.Core/Registrar.cs
@@ -97,14 +97,14 @@ public Type GetHandlerType(Type viewType)
type = attribute.Type;
- if (type.Name.StartsWith("_"))
+ if (type.Name.StartsWith("_", StringComparison.Ordinal))
{
// TODO: Remove attribute2 once renderer names have been unified across all platforms
var attribute2 = type.GetTypeInfo().GetCustomAttribute();
if (attribute2 != null)
type = attribute2.Type;
- if (type.Name.StartsWith("_"))
+ if (type.Name.StartsWith("_", StringComparison.Ordinal))
{
Register(viewType, null); // Cache this result so we don't work through this chain again
return null;
@@ -156,6 +156,7 @@ static Registrar()
}
internal static Dictionary Effects { get; } = new Dictionary();
+ internal static Dictionary StyleProperties { get; } = new Dictionary();
public static IEnumerable ExtraAssemblies { get; set; }
@@ -165,9 +166,7 @@ public static void RegisterAll(Type[] attrTypes)
{
Assembly[] assemblies = Device.GetAssemblies();
if (ExtraAssemblies != null)
- {
assemblies = assemblies.Union(ExtraAssemblies).ToArray();
- }
Assembly defaultRendererAssembly = Device.PlatformServices.GetType().GetTypeInfo().Assembly;
int indexOfExecuting = Array.IndexOf(assemblies, defaultRendererAssembly);
@@ -185,36 +184,38 @@ public static void RegisterAll(Type[] attrTypes)
foreach (Type attrType in attrTypes)
{
Attribute[] attributes = assembly.GetCustomAttributes(attrType).ToArray();
- if (attributes.Length == 0)
- continue;
-
- foreach (HandlerAttribute attribute in attributes)
+ var length = attributes.Length;
+ for (var i = 0; i < length;i++)
{
+ var attribute = (HandlerAttribute)attributes[i];
if (attribute.ShouldRegister())
Registered.Register(attribute.HandlerType, attribute.TargetType);
}
}
string resolutionName = assembly.FullName;
+ var resolutionNameAttribute = (ResolutionGroupNameAttribute)assembly.GetCustomAttribute(typeof(ResolutionGroupNameAttribute));
+ if (resolutionNameAttribute != null)
+ resolutionName = resolutionNameAttribute.ShortName;
Attribute[] effectAttributes = assembly.GetCustomAttributes(typeof(ExportEffectAttribute)).ToArray();
- if (effectAttributes.Length > 0)
+ var exportEffectsLength = effectAttributes.Length;
+ for (var i = 0; i < exportEffectsLength;i++)
{
- var resolutionNameAttribute = (ResolutionGroupNameAttribute)assembly.GetCustomAttribute(typeof(ResolutionGroupNameAttribute));
- if (resolutionNameAttribute != null)
- {
- resolutionName = resolutionNameAttribute.ShortName;
- }
+ var effect = (ExportEffectAttribute)effectAttributes[i];
+ Effects [resolutionName + "." + effect.Id] = effect.Type;
+ }
- foreach (Attribute attribute in effectAttributes)
- {
- var effect = (ExportEffectAttribute)attribute;
- Effects[resolutionName + "." + effect.Id] = effect.Type;
- }
+ Attribute[] styleAttributes = assembly.GetCustomAttributes(typeof(StyleSheets.StylePropertyAttribute)).ToArray();
+ var stylePropertiesLength = styleAttributes.Length;
+ for (var i = 0; i < stylePropertiesLength; i++)
+ {
+ var attribute = (StyleSheets.StylePropertyAttribute)styleAttributes[i];
+ StyleProperties[attribute.CssPropertyName] = attribute;
}
}
DependencyService.Initialize(assemblies);
}
}
-}
\ No newline at end of file
+}
diff --git a/Xamarin.Forms.Core/StyleSheets/CharExtensions.cs b/Xamarin.Forms.Core/StyleSheets/CharExtensions.cs
new file mode 100644
index 00000000000..a5a00a67714
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/CharExtensions.cs
@@ -0,0 +1,41 @@
+using System.Runtime.CompilerServices;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ static class CharExtensions
+ {
+ //w [ \t\r\n\f]*
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsW(this char c)
+ {
+ return c == ' '
+ || c == '\t'
+ || c == '\r'
+ || c == '\n'
+ || c == '\f';
+ }
+
+ //nmstart [_a-z]|{nonascii}|{escape}
+ //escape {unicode}|\\[^\n\r\f0-9a-f]
+ //nonascii [^\0-\237]
+ // TODO support escape and nonascii
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsNmStart(this char c)
+ {
+ return c == '_' || char.IsLetter(c);
+ }
+
+ //nmchar [_a-z0-9-]|{nonascii}|{escape}
+ //unicode \\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?
+ //escape {unicode}|\\[^\n\r\f0-9a-f]
+ //nonascii [^\0-\237]
+ //TODO support escape, nonascii and unicode
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsNmChar(this char c)
+ {
+ return c == '_'
+ || c == '-'
+ || char.IsLetterOrDigit(c);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/CssReader.cs b/Xamarin.Forms.Core/StyleSheets/CssReader.cs
new file mode 100644
index 00000000000..250fb6ccde6
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/CssReader.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ sealed class CssReader : TextReader
+ {
+ readonly TextReader _reader;
+
+ public CssReader(TextReader reader)
+ {
+ if (reader == null)
+ throw new ArgumentNullException(nameof(reader));
+
+ _reader = reader;
+ }
+
+ readonly Queue _cache = new Queue();
+
+ //skip comments
+ //TODO unescape escaped sequences
+ public override int Peek()
+ {
+ if (_cache.Count > 0)
+ return _cache.Peek();
+
+ int p = _reader.Peek();
+ if (p <= 0)
+ return p;
+ if (unchecked((char)p) != '/')
+ return p;
+
+ _cache.Enqueue(unchecked((char)_reader.Read()));
+ p = _reader.Peek();
+ if (p <= 0)
+ return _cache.Peek();
+ if (unchecked((char)p) != '*')
+ return _cache.Peek();
+
+ _cache.Clear();
+ _reader.Read(); //consume the '*'
+
+ bool hasStar = false;
+ while (true) {
+ var next = _reader.Read();
+ if (next <= 0)
+ return next;
+ if (unchecked((char)next) == '*')
+ hasStar = true;
+ else if (hasStar && unchecked((char)next) == '/')
+ return Peek(); //recursively call self for comments followign comments
+ else
+ hasStar = false;
+ }
+ }
+
+ //skip comments
+ //TODO unescape escaped sequences
+ public override int Read()
+ {
+ if (_cache.Count > 0)
+ return _cache.Dequeue();
+
+ int p = _reader.Read();
+ if (p <= 0)
+ return p;
+ var c = unchecked((char)p);
+ if (c != '/')
+ return p;
+
+ _cache.Enqueue(c);
+ p = _reader.Read();
+ if (p <= 0)
+ return _cache.Dequeue();
+ c = unchecked((char)p);
+ if (c != '*')
+ return _cache.Dequeue();
+
+ _cache.Clear();
+ _reader.Read(); //consume the '*'
+
+ bool hasStar = false;
+ while (true) {
+ var next = _reader.Read();
+ if (next <= 0)
+ return next;
+ if (unchecked((char)next) == '*')
+ hasStar = true;
+ else if (hasStar && unchecked((char)next) == '/')
+ return Read(); //recursively call self for comments followign comments
+ else
+ hasStar = false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/IStyleSelectable.cs b/Xamarin.Forms.Core/StyleSheets/IStyleSelectable.cs
new file mode 100644
index 00000000000..4dd337b7838
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/IStyleSelectable.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ interface IStyleSelectable
+ {
+ string Name { get; }
+ string Id { get; }
+ IStyleSelectable Parent { get; }
+ IList Classes { get; }
+ IEnumerable Children { get; }
+ }
+
+ interface IStylable
+ {
+ BindableProperty GetProperty(string key);
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/Selector.cs b/Xamarin.Forms.Core/StyleSheets/Selector.cs
new file mode 100644
index 00000000000..ae27e67a155
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/Selector.cs
@@ -0,0 +1,281 @@
+using System;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ abstract class Selector
+ {
+ Selector()
+ {
+ }
+
+ public static Selector Parse(CssReader reader, char stopChar = '\0')
+ {
+ Selector root = All, workingRoot = All;
+ Operator workingRootParent = null;
+ Action setCurrentSelector = (op, sel) => SetCurrentSelector(ref root, ref workingRoot, ref workingRootParent, op, sel);
+
+ int p;
+ reader.SkipWhiteSpaces();
+ while ((p = reader.Peek()) > 0) {
+ switch (unchecked((char)p)) {
+ case '*':
+ setCurrentSelector(new And(), All);
+ reader.Read();
+ break;
+ case '.':
+ reader.Read();
+ var className = reader.ReadIdent();
+ if (className == null)
+ return Invalid;
+ setCurrentSelector(new And(), new Class(className));
+ break;
+ case '#':
+ reader.Read();
+ var id = reader.ReadName();
+ if (id == null)
+ return Invalid;
+ setCurrentSelector(new And(), new Id(id));
+ break;
+ case '[':
+ throw new NotImplementedException("Attributes not implemented");
+ case ',':
+ reader.Read();
+ setCurrentSelector(new Or(), All);
+ reader.SkipWhiteSpaces();
+ break;
+ case '+':
+ reader.Read();
+ setCurrentSelector(new Adjacent(), All);
+ reader.SkipWhiteSpaces();
+ break;
+ case '~':
+ reader.Read();
+ setCurrentSelector(new Sibling(), All);
+ reader.SkipWhiteSpaces();
+ break;
+ case '>':
+ reader.Read();
+ setCurrentSelector(new Child(), All);
+ reader.SkipWhiteSpaces();
+ break;
+ case ' ':
+ case '\t':
+ case '\n':
+ case '\r':
+ case '\f':
+ reader.Read();
+ bool processWs = false;
+ while ((p = reader.Peek()) > 0) {
+ var c = unchecked((char)p);
+ if (char.IsWhiteSpace(c)) {
+ reader.Read();
+ continue;
+ }
+ processWs = (c != '+'
+ && c != '>'
+ && c != ','
+ && c != '~'
+ && c != stopChar);
+ break;
+ }
+ if (!processWs)
+ break;
+ setCurrentSelector(new Descendent(), All);
+ reader.SkipWhiteSpaces();
+ break;
+ default:
+ if (unchecked((char)p) == stopChar)
+ return root;
+
+ var elementName = reader.ReadIdent();
+ if (elementName == null)
+ return Invalid;
+ setCurrentSelector(new And(), new Element(elementName));
+ break;
+ }
+ }
+ return root;
+ }
+
+ static void SetCurrentSelector(ref Selector root, ref Selector workingRoot, ref Operator workingRootParent, Operator op, Selector sel)
+ {
+ var updateRoot = root == workingRoot;
+
+ op.Left = workingRoot;
+ op.Right = sel;
+ workingRoot = op;
+ if (workingRootParent != null)
+ workingRootParent.Right = workingRoot;
+
+ if (updateRoot)
+ root = workingRoot;
+
+ if (workingRoot is Or) {
+ workingRootParent = (Operator)workingRoot;
+ workingRoot = sel;
+ }
+ }
+
+ public abstract bool Matches(IStyleSelectable styleable);
+
+ internal static Selector Invalid = new Generic(s => false);
+ internal static Selector All = new Generic(s => true);
+
+ abstract class UnarySelector : Selector
+ {
+ }
+
+ abstract class Operator : Selector
+ {
+ public Selector Left { get; set; } = Invalid;
+ public Selector Right { get; set; } = Invalid;
+ }
+
+ sealed class Generic : UnarySelector
+ {
+ readonly Func func;
+ public Generic(Func func)
+ {
+ this.func = func;
+ }
+
+ public override bool Matches(IStyleSelectable styleable) => func(styleable);
+ }
+
+ sealed class Class : UnarySelector
+ {
+ public Class(string className)
+ {
+ ClassName = className;
+ }
+
+ public string ClassName { get; }
+ public override bool Matches(IStyleSelectable styleable)
+ => styleable.Classes != null && styleable.Classes.Contains(ClassName);
+ }
+
+ sealed class Id : UnarySelector
+ {
+ public Id(string id)
+ {
+ IdName = id;
+ }
+
+ public string IdName { get; }
+ public override bool Matches(IStyleSelectable styleable) => styleable.Id == IdName;
+ }
+
+ sealed class Or : Operator
+ {
+ public override bool Matches(IStyleSelectable styleable) => Right.Matches(styleable) || Left.Matches(styleable);
+ }
+
+ sealed class And : Operator
+ {
+ public override bool Matches(IStyleSelectable styleable) => Right.Matches(styleable) && Left.Matches(styleable);
+ }
+
+ sealed class Element : UnarySelector
+ {
+ public Element(string elementName)
+ {
+ ElementName = elementName;
+ }
+ public string ElementName { get; }
+ public override bool Matches(IStyleSelectable styleable) =>
+ string.Equals(styleable.Name, ElementName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ sealed class Child : Operator
+ {
+ public override bool Matches(IStyleSelectable styleable) =>
+ Right.Matches(styleable) && styleable.Parent != null && Left.Matches(styleable.Parent);
+ }
+
+ sealed class Descendent : Operator
+ {
+ public override bool Matches(IStyleSelectable styleable)
+ {
+ if (!Right.Matches(styleable))
+ return false;
+ var parent = styleable.Parent;
+ while (parent != null) {
+ if (Left.Matches(parent))
+ return true;
+ parent = parent.Parent;
+ }
+ return false;
+ }
+ }
+
+ sealed class Adjacent : Operator
+ {
+ public override bool Matches(IStyleSelectable styleable)
+ {
+ if (!Right.Matches(styleable))
+ return false;
+ if (styleable.Parent == null)
+ return false;
+
+ IStyleSelectable prev = null;
+ foreach (var elem in styleable.Parent.Children) {
+ if (elem == styleable && prev != null)
+ return Left.Matches(prev);
+ prev = elem;
+ }
+ return false;
+ //var index = styleable.Parent.Children.IndexOf(styleable);
+ //if (index == 0)
+ // return false;
+ //var adjacent = styleable.Parent.Children[index - 1];
+ //return Left.Matches(adjacent);
+ }
+ }
+
+ sealed class Sibling : Operator
+ {
+ public override bool Matches(IStyleSelectable styleable)
+ {
+ if (!Right.Matches(styleable))
+ return false;
+ if (styleable.Parent == null)
+ return false;
+
+ int selfIndex = 0;
+ bool foundSelfInParent = false;
+ foreach (var elem in styleable.Parent.Children) {
+ if (elem == styleable) {
+ foundSelfInParent = true;
+ break;
+ }
+ ++selfIndex;
+ }
+
+ if (!foundSelfInParent)
+ return false;
+
+ int index = 0;
+ foreach (var elem in styleable.Parent.Children) {
+ if (index >= selfIndex)
+ return false;
+ if (Left.Matches(elem))
+ return true;
+ ++index;
+ }
+
+ return false;
+
+ //var index = styleable.Parent.Children.IndexOf(styleable);
+ //if (index == 0)
+ // return false;
+ //int siblingIndex = -1;
+ //for (var i = 0; i < index; i++)
+ // if (Left.Matches(styleable.Parent.Children[i])) {
+ // siblingIndex = i;
+ // break;
+ // }
+ //return siblingIndex != -1;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/Style.cs b/Xamarin.Forms.Core/StyleSheets/Style.cs
new file mode 100644
index 00000000000..080a81a93d4
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/Style.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ sealed class Style
+ {
+ Style()
+ {
+ }
+
+ public IDictionary Declarations { get; set; } = new Dictionary();
+ Dictionary, object> convertedValues = new Dictionary, object>();
+
+ public static Style Parse(CssReader reader, char stopChar = '\0')
+ {
+ Style style = new Style();
+ string propertyName = null, propertyValue = null;
+
+ int p;
+ reader.SkipWhiteSpaces();
+ bool readingName = true;
+ while ((p = reader.Peek()) > 0) {
+ switch (unchecked((char)p)) {
+ case ':':
+ reader.Read();
+ readingName = false;
+ reader.SkipWhiteSpaces();
+ break;
+ case ';':
+ reader.Read();
+ if (!string.IsNullOrEmpty(propertyName) && !string.IsNullOrEmpty(propertyValue))
+ style.Declarations.Add(propertyName, propertyValue);
+ propertyName = propertyValue = null;
+ readingName = true;
+ reader.SkipWhiteSpaces();
+ break;
+ default:
+ if ((char)p == stopChar)
+ return style;
+
+ if (readingName) {
+ propertyName = reader.ReadIdent();
+ if (propertyName == null)
+ throw new Exception();
+ } else
+ propertyValue = reader.ReadUntil(stopChar, ';', ':');
+ break;
+ }
+ }
+ return style;
+ }
+
+ public void Apply(VisualElement styleable)
+ {
+ if (styleable == null)
+ throw new ArgumentNullException(nameof(styleable));
+
+ foreach (var decl in Declarations) {
+ var property = ((IStylable)styleable).GetProperty(decl.Key);
+ if (property == null)
+ continue;
+ object value;
+ if (!convertedValues.TryGetValue(decl, out value))
+ convertedValues[decl] = (value = Convert(decl.Value, property));
+ styleable.SetValue(property, value, fromStyle: true);
+ }
+
+ foreach (var child in styleable.LogicalChildrenInternal) {
+ var ve = child as VisualElement;
+ if (ve == null)
+ continue;
+ Apply(ve);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static object Convert(object value, BindableProperty property)
+ {
+ Func minforetriever = () => property.DeclaringType.GetRuntimeProperty(property.PropertyName) as MemberInfo
+ ?? property.DeclaringType.GetRuntimeMethod("Get" + property.PropertyName, new[] { typeof(BindableObject) }) as MemberInfo;
+ return value.ConvertTo(property.ReturnType, minforetriever, null);
+ }
+
+ public void UnApply(IStylable styleable)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/StylePropertyAttribute.cs b/Xamarin.Forms.Core/StyleSheets/StylePropertyAttribute.cs
new file mode 100644
index 00000000000..0dbacd0f44c
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/StylePropertyAttribute.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = true)]
+ sealed class StylePropertyAttribute : Attribute
+ {
+ public string CssPropertyName { get; }
+ public string BindablePropertyName { get; }
+ public Type TargetType { get; }
+ public BindableProperty BindableProperty { get; set; }
+
+
+ public StylePropertyAttribute(string cssPropertyName, Type targetType, string bindablePropertyName)
+ {
+ CssPropertyName = cssPropertyName;
+ BindablePropertyName = bindablePropertyName;
+ TargetType = targetType;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/StyleSheet.cs b/Xamarin.Forms.Core/StyleSheets/StyleSheet.cs
new file mode 100644
index 00000000000..43731bf659a
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/StyleSheet.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Xml;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ public sealed class StyleSheet : IStyle
+ {
+ StyleSheet()
+ {
+ }
+
+ internal IDictionary Styles { get; set; } = new Dictionary();
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static StyleSheet Parse(Assembly assembly, string resourceId, IXmlLineInfo lineInfo = null)
+ {
+ using (var stream = assembly.GetManifestResourceStream(resourceId)) {
+ if (stream == null)
+ throw new XamlParseException($"No resource found for '{resourceId}'.", lineInfo);
+ using (var reader = new StreamReader(stream)) {
+ return Parse(reader);
+ }
+ }
+ }
+
+ internal static StyleSheet Parse(TextReader reader)
+ {
+ if (reader == null)
+ throw new ArgumentNullException(nameof(reader));
+
+ return Parse(new CssReader(reader));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static StyleSheet Parse(CssReader reader)
+ {
+ var sheet = new StyleSheet();
+
+ Style style = null;
+ var selector = Selector.All;
+
+ int p;
+ bool inStyle = false;
+ reader.SkipWhiteSpaces();
+ while ((p = reader.Peek()) > 0) {
+ switch ((char)p) {
+ case '@':
+ throw new NotSupportedException("AT-rules not supported");
+ case '{':
+ reader.Read();
+ style = Style.Parse(reader, '}');
+ inStyle = true;
+ break;
+ case '}':
+ reader.Read();
+ if (!inStyle)
+ throw new Exception();
+ inStyle = false;
+ sheet.Styles.Add(selector, style);
+ style = null;
+ selector = Selector.All;
+ break;
+ default:
+ selector = Selector.Parse(reader, '{');
+ break;
+ }
+ }
+ return sheet;
+ }
+
+ Type IStyle.TargetType
+ => typeof(VisualElement);
+
+ void IStyle.Apply(BindableObject bindable)
+ {
+ var styleable = bindable as VisualElement;
+ if (styleable == null)
+ return;
+ Apply(styleable);
+ }
+
+ void Apply(VisualElement styleable)
+ {
+ ApplyCore(styleable);
+ foreach (var child in styleable.LogicalChildrenInternal)
+ ((IStyle)this).Apply(child);
+ }
+
+ void ApplyCore(VisualElement styleable)
+ {
+ foreach (var kvp in Styles) {
+ var selector = kvp.Key;
+ var style = kvp.Value;
+ if (!selector.Matches(styleable))
+ continue;
+ style.Apply(styleable);
+ }
+ }
+
+ void IStyle.UnApply(BindableObject bindable)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/TextReaderExtensions.cs b/Xamarin.Forms.Core/StyleSheets/TextReaderExtensions.cs
new file mode 100644
index 00000000000..6060bd6c5f3
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/TextReaderExtensions.cs
@@ -0,0 +1,76 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ static class TextReaderExtensions
+ {
+ //ident [-]?{nmstart}{nmchar}*
+ public static string ReadIdent(this TextReader reader)
+ {
+ var sb = new StringBuilder();
+ bool first = true;
+ bool hasLeadingDash = false;
+ int p;
+ while ((p = reader.Peek()) > 0) {
+ var c = unchecked((char)p);
+ if (first && !hasLeadingDash && c == '-') {
+ sb.Append((char)reader.Read());
+ hasLeadingDash = true;
+ } else if (first && c.IsNmStart()) {
+ sb.Append((char)reader.Read());
+ first = false;
+ } else if (first) { //a nmstart is expected
+ throw new Exception();
+ } else if (c.IsNmChar())
+ sb.Append((char)reader.Read());
+ else
+ break;
+ }
+ return sb.ToString();
+ }
+
+ //name {nmchar}+
+ public static string ReadName(this TextReader reader)
+ {
+ var sb = new StringBuilder();
+ int p;
+ while ((p = reader.Peek()) > 0) {
+ var c = unchecked((char)p);
+ if (c.IsNmChar())
+ sb.Append((char)reader.Read());
+ else
+ break;
+ }
+ return sb.ToString();
+ }
+
+ public static string ReadUntil(this TextReader reader, params char[] limit)
+ {
+ var sb = new StringBuilder();
+ int p;
+ while ((p = reader.Peek()) > 0) {
+ var c = unchecked((char)p);
+ if (limit != null && limit.Contains(c))
+ break;
+ reader.Read();
+ sb.Append(c);
+ }
+ return sb.ToString();
+ }
+
+ //w [ \t\r\n\f]*
+ public static void SkipWhiteSpaces(this TextReader reader)
+ {
+ int p;
+ while ((p = reader.Peek()) > 0) {
+ var c = unchecked((char)p);
+ if (!c.IsW())
+ break;
+ reader.Read();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/VisualElement.cs b/Xamarin.Forms.Core/VisualElement.cs
index 520b06b1787..959f750a792 100644
--- a/Xamarin.Forms.Core/VisualElement.cs
+++ b/Xamarin.Forms.Core/VisualElement.cs
@@ -616,6 +616,7 @@ protected override void OnParentSet()
NavigationProxy.Inner = null;
}
#pragma warning restore 0618
+ ApplyStyleSheetOnParentSet();
}
protected virtual void OnSizeAllocated(double width, double height)
diff --git a/Xamarin.Forms.Core/VisualElement_StyleSheet.cs b/Xamarin.Forms.Core/VisualElement_StyleSheet.cs
new file mode 100644
index 00000000000..0dfdccbd8f0
--- /dev/null
+++ b/Xamarin.Forms.Core/VisualElement_StyleSheet.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.IO;
+using System.Reflection;
+
+using Xamarin.Forms.Internals;
+using Xamarin.Forms.StyleSheets;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms
+{
+ public partial class VisualElement : IStyleSelectable, IStylable
+ {
+ IList IStyleSelectable.Classes
+ => StyleClass;
+
+ public static readonly BindableProperty StyleSheetProperty =
+ BindableProperty.Create("StyleSheet", typeof(StyleSheet), typeof(VisualElement), default(StyleSheet),
+ propertyChanged: (bp, o, n) => ((VisualElement)bp).OnStyleSheetChanged((StyleSheet)o, (StyleSheet)n));
+
+ [TypeConverter(typeof(StyleSheetConverter))]
+ public StyleSheet StyleSheet {
+ get { return (StyleSheet)GetValue(StyleSheetProperty); }
+ set { SetValue(StyleSheetProperty, value); }
+ }
+
+ void OnStyleSheetChanged(StyleSheet oldValue, StyleSheet newValue)
+ {
+ ((IStyle)oldValue)?.UnApply(this);
+ ((IStyle)newValue)?.Apply(this);
+ }
+
+ BindableProperty IStylable.GetProperty(string key)
+ {
+ StylePropertyAttribute styleAttribute;
+ if (!Internals.Registrar.StyleProperties.TryGetValue(key, out styleAttribute))
+ return null;
+
+ if (!styleAttribute.TargetType.GetTypeInfo().IsAssignableFrom(GetType().GetTypeInfo()))
+ return null;
+
+ if (styleAttribute.BindableProperty != null)
+ return styleAttribute.BindableProperty;
+
+ var bpField = GetType().GetField(styleAttribute.BindablePropertyName);
+ if (bpField == null || !bpField.IsStatic)
+ return null;
+
+ return (styleAttribute.BindableProperty = bpField.GetValue(null) as BindableProperty);
+ }
+
+ void ApplyStyleSheetOnParentSet()
+ {
+ var parent = Parent;
+ if (parent == null)
+ return;
+ var sheets = new List();
+ while (parent != null) {
+ var visualParent = parent as VisualElement;
+ var sheet = visualParent?.StyleSheet;
+ if (sheet != null)
+ sheets.Add(sheet);
+ parent = parent.Parent;
+ }
+ for (var i = sheets.Count - 1; i >= 0; i--)
+ ((IStyle)sheets[i]).Apply(this);
+ }
+
+ [Xaml.ProvideCompiled("Xamarin.Forms.Core.XamlC.StyleSheetConverter")]
+ public class StyleSheetConverter : TypeConverter, IExtendedTypeConverter
+ {
+ object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceProvider serviceProvider)
+ {
+ if (serviceProvider == null)
+ throw new ArgumentNullException(nameof(serviceProvider));
+
+ var rootObjectType = (serviceProvider.GetService(typeof(Xaml.IRootObjectProvider)) as Xaml.IRootObjectProvider)?.RootObject.GetType();
+ if (rootObjectType == null)
+ return null;
+
+ var lineInfo = (serviceProvider.GetService(typeof(Xaml.IXmlLineInfoProvider)) as Xaml.IXmlLineInfoProvider)?.XmlLineInfo;
+ var rootTargetPath = XamlResourceIdAttribute.GetPathForType(rootObjectType);
+ var uri = new Uri(value, UriKind.Relative); //we don't want file:// uris, even if they start with '/'
+ var resourceId = ResourceDictionary.RDSourceTypeConverter.GetResourceId(uri, rootTargetPath,
+ s => XamlResourceIdAttribute.GetResourceIdForPath(rootObjectType.GetTypeInfo().Assembly, s));
+
+ return StyleSheet.Parse(rootObjectType.GetTypeInfo().Assembly, resourceId, lineInfo);
+ }
+
+ object IExtendedTypeConverter.ConvertFrom(CultureInfo culture, object value, IServiceProvider serviceProvider)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override object ConvertFromInvariantString(string value)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj b/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj
index 52b33ca4aa3..01138968ffb 100644
--- a/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj
+++ b/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj
@@ -451,6 +451,15 @@
+
+
+
+
+
+
+
+
+
@@ -464,6 +473,7 @@
+
diff --git a/Xamarin.Forms.Xaml.UnitTests/StyleSheet.xaml b/Xamarin.Forms.Xaml.UnitTests/StyleSheet.xaml
new file mode 100644
index 00000000000..edcc52c93e7
--- /dev/null
+++ b/Xamarin.Forms.Xaml.UnitTests/StyleSheet.xaml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Xamarin.Forms.Xaml.UnitTests/StyleSheet.xaml.cs b/Xamarin.Forms.Xaml.UnitTests/StyleSheet.xaml.cs
new file mode 100644
index 00000000000..9162c8855c0
--- /dev/null
+++ b/Xamarin.Forms.Xaml.UnitTests/StyleSheet.xaml.cs
@@ -0,0 +1,47 @@
+using System;
+using NUnit.Framework;
+
+using Xamarin.Forms.Core.UnitTests;
+using NUnit.Framework.Constraints;
+
+namespace Xamarin.Forms.Xaml.UnitTests
+{
+ public partial class StyleSheet : ContentPage
+ {
+ public StyleSheet()
+ {
+ InitializeComponent();
+ }
+
+ public StyleSheet(bool useCompiledXaml)
+ {
+ //this stub will be replaced at compile time
+ }
+
+ [TestFixture]
+ public class Tests
+ {
+ [SetUp]
+ public void SetUp()
+ {
+ Device.PlatformServices = new MockPlatformServices();
+ Xamarin.Forms.Internals.Registrar.RegisterAll(new Type[0]);
+ }
+
+ [TestCase(false), TestCase(true)]
+ public void EmbeddedStyleSheetsAreLoaded(bool useCompiledXaml)
+ {
+ var layout = new StyleSheet(useCompiledXaml);
+ Assert.That(layout.StyleSheet.Styles.Count, Is.GreaterThanOrEqualTo(1));
+ }
+
+ [TestCase(false), TestCase(true)]
+ public void StyleSheetsAreApplied(bool useCompiledXaml)
+ {
+ var layout = new StyleSheet(useCompiledXaml);
+ Assert.That(layout.label0.TextColor, Is.EqualTo(Color.Azure));
+ Assert.That(layout.label0.BackgroundColor, Is.EqualTo(Color.AliceBlue));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj b/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj
index 60e744d28ec..62adc9edac0 100644
--- a/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj
+++ b/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj
@@ -432,7 +432,6 @@
Bz44216.xaml
- AcceptEmptyServiceProvider.xaml
AcceptEmptyServiceProvider.xaml
@@ -498,6 +497,9 @@
ResourceDictionaryWithSource.xaml
+
+ StyleSheet.xaml
+
@@ -527,15 +529,6 @@
Xamarin.Forms.Maps
-
-
-
-
-
-
-
-
-
MSBuild:UpdateDesignTimeXaml
@@ -924,6 +917,23 @@
MSBuild:UpdateDesignTimeXaml
+
+
+
+ MSBuild:UpdateDesignTimeXaml
+
+
+ MSBuild:UpdateDesignTimeXaml
+
+
+ MSBuild:UpdateDesignTimeXaml
+
+
+ MSBuild:UpdateDesignTimeXaml
+
+
+ MSBuild:UpdateDesignTimeXaml
+
@@ -940,23 +950,6 @@
-
- MSBuild:UpdateDesignTimeXaml
-
-
-
-
- MSBuild:UpdateDesignTimeXaml
-
-
-
-
- MSBuild:UpdateDesignTimeXaml
-
-
-
-
- MSBuild:UpdateDesignTimeXaml
-
+
diff --git a/Xamarin.Forms.Xaml.UnitTests/css/bar.css b/Xamarin.Forms.Xaml.UnitTests/css/bar.css
new file mode 100644
index 00000000000..5f282702bb0
--- /dev/null
+++ b/Xamarin.Forms.Xaml.UnitTests/css/bar.css
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Xamarin.Forms.Xaml.UnitTests/css/foo.css b/Xamarin.Forms.Xaml.UnitTests/css/foo.css
new file mode 100644
index 00000000000..9cca27b33d7
--- /dev/null
+++ b/Xamarin.Forms.Xaml.UnitTests/css/foo.css
@@ -0,0 +1,4 @@
+label {
+ color: azure;
+ background-color: aliceblue;
+}
\ No newline at end of file