using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using Robust.Shared.Utility; namespace Robust.Client.UserInterface { /// /// As part of my evil plan to convince Monster to work on SS14, I'm porting web technologies to Robust. /// Here's CSS style sheets. /// /// public sealed class Stylesheet { public IReadOnlyList Rules { get; } // More organized forms of the above rules list, that is more efficient to query. internal List<(int order, StyleRule rule)> UnsortedRules { get; } internal Dictionary> TypeSortedRules { get; } public Stylesheet(IReadOnlyList rules) { Rules = rules; UnsortedRules = new List<(int order, StyleRule rule)>(); TypeSortedRules = new Dictionary>(); var order = 0; var typeControl = typeof(Control); foreach (var rule in rules) { if (rule.Selector is SelectorElement element && element.ElementType != null && typeControl.IsAssignableFrom(element.ElementType) && element.ElementType != typeControl) { if (!TypeSortedRules.TryGetValue(element.ElementType, out var typeList)) { typeList = new List<(int order, StyleRule rule)>(); TypeSortedRules.Add(element.ElementType, typeList); } typeList.Add((order, rule)); } else { UnsortedRules.Add((order, rule)); } order += 1; } } } /// /// Helper methods to make construction of style sheets easier. /// public static class StylesheetHelpers { /// /// Creates a blank mutable element selector. /// /// public static MutableSelectorElement Element() { return new(); } /// /// Creates a mutable element selector with as control type. /// /// The type of control that the selector binds to. /// public static MutableSelectorElement Element() where T : Control { return new() {Type = typeof(T)}; } /// /// Creates a new blank mutable child selector. /// /// public static MutableSelectorChild Child() { return new(); } } /// /// Mutable selectors are convenience wrappers to make constructing style selectors cleaner. /// /// /// Mutable selectors can store style properties that will get used when converted to a style rule. /// These can be added with /// public abstract class MutableSelector { private readonly List _props = new(); public MutableSelector Prop(string key, object value) { _props.Add(new StyleProperty(key, value)); return this; } /// /// Converts a mutable selector into a style rule, using the properties added via . /// public static implicit operator StyleRule(MutableSelector elem) { return new(elem.ToSelector(), elem._props); } public static implicit operator Selector(MutableSelector elem) { return elem.ToSelector(); } protected abstract Selector ToSelector(); } /// /// Mutable selector for . /// /// public sealed class MutableSelectorElement : MutableSelector { public Type? Type { get; set; } public List? StyleClasses { get; set; } public string? StyleIdentifier { get; set; } public List? PseudoClasses { get; set; } /// /// Adds a set of style classes to this selector. /// public MutableSelectorElement Class(params string[] classes) { StyleClasses ??= new List(); StyleClasses.AddRange(classes); return this; } /// /// Adds a single style class to this selector. /// public MutableSelectorElement Class(string @class) { StyleClasses ??= new List(); StyleClasses.Add(@class); return this; } /// /// Adds a set of pseudo classes to this selector. /// public MutableSelectorElement Pseudo(params string[] classes) { PseudoClasses ??= new List(); PseudoClasses.AddRange(classes); return this; } /// /// Adds a single pseudo class to this selector. /// public MutableSelectorElement Pseudo(string @class) { PseudoClasses ??= new List(); PseudoClasses.Add(@class); return this; } /// /// Sets the style identifier on this selector. /// public MutableSelectorElement Identifier(string identifier) { StyleIdentifier = identifier; return this; } protected override Selector ToSelector() { return new SelectorElement(Type, StyleClasses, StyleIdentifier, PseudoClasses); } } /// /// /// /// /// /// Both and must be set before converting this to a proper selector, /// or else an will be thrown. /// public sealed class MutableSelectorChild : MutableSelector { public Selector? ParentSelector { get; set; } public Selector? ChildSelector { get; set; } public MutableSelectorChild() { } public MutableSelectorChild Parent(Selector parent) { ParentSelector = parent; return this; } public MutableSelectorChild Child(Selector child) { ChildSelector = child; return this; } protected override Selector ToSelector() { if (ParentSelector == null || ChildSelector == null) { throw new InvalidOperationException("Must initialize both parent and child."); } return new SelectorChild(ParentSelector, ChildSelector); } } /// /// A single rule in a style sheet, containing a bunch of properties and a selector. /// public sealed class StyleRule { public StyleRule(Selector selector, IReadOnlyList properties) { Selector = selector; Properties = properties; Specificity = selector.CalculateSpecificity(); } public StyleSpecificity Specificity { get; } public Selector Selector { get; } public IReadOnlyList Properties { get; } } // https://specifishity.com/ public struct StyleSpecificity : IComparable, IComparable { public readonly int IdSelectors; public readonly int ClassSelectors; public readonly int TypeSelectors; public StyleSpecificity(int idSelectors, int classSelectors, int typeSelectors) { IdSelectors = idSelectors; ClassSelectors = classSelectors; TypeSelectors = typeSelectors; } public static StyleSpecificity operator +(StyleSpecificity a, StyleSpecificity b) { return new( a.IdSelectors + b.IdSelectors, a.ClassSelectors + b.ClassSelectors, a.TypeSelectors + b.TypeSelectors); } public int CompareTo(StyleSpecificity other) { var idSelectorsComparison = IdSelectors.CompareTo(other.IdSelectors); if (idSelectorsComparison != 0) { return idSelectorsComparison; } var classSelectorsComparison = ClassSelectors.CompareTo(other.ClassSelectors); if (classSelectorsComparison != 0) { return classSelectorsComparison; } return TypeSelectors.CompareTo(other.TypeSelectors); } public int CompareTo(object? obj) { if (ReferenceEquals(null, obj)) return 1; return obj is StyleSpecificity other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(StyleSpecificity)}"); } public override string ToString() { return $"({IdSelectors}-{ClassSelectors}-{TypeSelectors})"; } } /// /// A single property in a rule, with a name and an object value. /// public readonly struct StyleProperty { public StyleProperty(string name, object value) { Name = name; Value = value; } public string Name { get; } public object Value { get; } } public abstract class Selector { public abstract bool Matches(Control control); public abstract StyleSpecificity CalculateSpecificity(); } public sealed class SelectorElement : Selector { private readonly string[]? _elementClasses; private readonly string[]? _pseudoClasses; public SelectorElement( Type? elementType, IEnumerable? elementClasses, string? elementId, IEnumerable? pseudoClass) { if (elementType != null) { if (!typeof(Control).IsAssignableFrom(elementType)) { throw new ArgumentException("elementType must inherit Control.", nameof(elementType)); } if (elementType == typeof(Control)) { throw new ArgumentException("elementType may not be Control itself.", nameof(elementType)); } } ElementType = elementType; _elementClasses = elementClasses?.ToArray(); ElementId = elementId; _pseudoClasses = pseudoClass?.ToArray(); } public static SelectorElement Type(Type elementType) { return new(elementType, null, null, null); } public static SelectorElement Class(params string[] classes) { return new(null, classes, null, null); } public static SelectorElement Id(string id) { return new(null, null, id, null); } public Type? ElementType { get; } public IReadOnlyList? ElementClasses => _elementClasses; public string? ElementId { get; } public IReadOnlyList? PseudoClasses => _pseudoClasses; public override bool Matches(Control control) { if (ElementId != null && control.StyleIdentifier != ElementId) { return false; } if (ElementType != null && !ElementType.IsInstanceOfType(control)) { return false; } if (_elementClasses != null) { foreach (var elementClass in _elementClasses) { if (!control.HasStyleClass(elementClass)) { return false; } } } if (_pseudoClasses != null) { foreach (var elementClass in _pseudoClasses) { if (!control.HasStylePseudoClass(elementClass)) { return false; } } } return true; } public override StyleSpecificity CalculateSpecificity() { var countId = ElementId == null ? 0 : 1; var countClasses = (ElementClasses?.Count ?? 0) + (PseudoClasses?.Count ?? 0); var countTypes = 0; if (ElementType != null) { var type = ElementType; while (type != typeof(Control)) { DebugTools.AssertNotNull(type); type = type!.BaseType; countTypes += 1; } } return new StyleSpecificity(countId, countClasses, countTypes); } } // Temporarily hidden due to performance concerns. // Like seriously this thing is awful performance wise. // Also, you can't just enable it. The code is here but AddChild etc only do restyles one level deep. internal sealed class SelectorDescendant : Selector { public SelectorDescendant([NotNull] Selector ascendant, [NotNull] Selector descendant) { Ascendant = ascendant; Descendant = descendant; } public Selector Ascendant { get; } public Selector Descendant { get; } public override bool Matches(Control control) { if (!Descendant.Matches(control)) { return false; } var parent = control.Parent; var found = false; while (parent != null) { if (Ascendant.Matches(parent)) { found = true; break; } parent = parent.Parent; } return found; } public override StyleSpecificity CalculateSpecificity() { return Ascendant.CalculateSpecificity() + Descendant.CalculateSpecificity(); } } // Temporarily hidden due to performance concerns. public sealed class SelectorChild : Selector { public SelectorChild(Selector parent, Selector child) { Parent = parent; Child = child; } public Selector Parent { get; } public Selector Child { get; } public override bool Matches(Control control) { if (control.Parent == null) { return false; } return Parent.Matches(control.Parent) && Child.Matches(control); } public override StyleSpecificity CalculateSpecificity() { return Parent.CalculateSpecificity() + Child.CalculateSpecificity(); } } }