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