Implement CSS-like stylesheets into GUI.

Label now reads properties from the CSS system.
This commit is contained in:
Pieter-Jan Briers
2019-02-14 23:24:35 +01:00
parent 0f935fc925
commit d6625f9c6a
15 changed files with 304 additions and 48 deletions

View File

@@ -13,7 +13,8 @@ namespace SS14.Client.Interfaces.UserInterface
{
public interface IUserInterfaceManager
{
UITheme Theme { get; }
UITheme ThemeDefaults { get; }
Stylesheet Stylesheet { get; set; }
Control Focused { get; }

View File

@@ -21,7 +21,7 @@ namespace SS14.Client.ResourceManagement
{
if (!cache.ContentFileExists(path))
{
throw new FileNotFoundException("Content file does not exist for texture");
throw new FileNotFoundException("Content file does not exist for font");
}
switch (GameController.Mode)

View File

@@ -197,6 +197,7 @@
<Compile Include="ResourceManagement\ResourceTypes\RSIResource.cs" />
<Compile Include="ResourceManagement\ResourceTypes\TextureResource.cs" />
<Compile Include="SceneTreeHolder.cs" />
<Compile Include="UserInterface\Control.Styling.cs" />
<Compile Include="UserInterface\Controls\BaseButton.cs" />
<Compile Include="UserInterface\Controls\Button.cs" />
<Compile Include="UserInterface\Controls\CheckBox.cs" />

View File

@@ -0,0 +1,111 @@
using System.Collections.Generic;
namespace SS14.Client.UserInterface
{
// ReSharper disable once RequiredBaseTypesIsNotInherited
public partial class Control
{
private readonly Dictionary<string, object> _styleProperties = new Dictionary<string, object>();
private readonly HashSet<string> _styleClasses = new HashSet<string>();
public IReadOnlyCollection<string> StyleClasses => _styleClasses;
private string _styleIdentifier;
public string StyleIdentifier
{
get => _styleIdentifier;
set
{
_styleIdentifier = value;
Restyle();
}
}
public bool HasStyleClass(string className)
{
return _styleClasses.Contains(className);
}
public void AddStyleClass(string className)
{
_styleClasses.Add(className);
Restyle();
}
public void RemoveStyleClass(string className)
{
_styleClasses.Remove(className);
Restyle();
}
private void Restyle()
{
_styleProperties.Clear();
// TODO: probably gonna need support for multiple stylesheets.
var stylesheet = UserInterfaceManager.Stylesheet;
if (stylesheet == null)
{
return;
}
// Get all rules that apply to us, sort them and apply they params again.
var ruleList = new List<(StyleRule rule, int index)>();
var count = 0;
foreach (var rule in stylesheet.Rules)
{
if (!rule.Selector.Matches(this))
{
continue;
}
ruleList.Add((rule, count));
count += 1;
}
// Sort by specificity.
// The index is there to sort by if specificity is the same, in which case the last takes precedence.
ruleList.Sort((a, b) =>
{
var cmp = a.rule.Specificity.CompareTo(b.rule.Specificity);
// Reverse this sort so that high specificity is at the TOP.
return -(cmp != 0 ? cmp : a.index.CompareTo(b.index));
});
// Go over each rule.
foreach (var (rule, _) in ruleList)
{
foreach (var property in rule.Properties)
{
if (_styleProperties.ContainsKey(property.Name))
{
// Since we've sorted by priority in reverse,
// the first ones to get applied have highest priority.
// So if we have a duplicate it's always lower priority and we can discard it.
continue;
}
_styleProperties[property.Name] = property.Value;
}
}
StylePropertiesChanged();
}
protected virtual void StylePropertiesChanged()
{
MinimumSizeChanged();
}
public bool TryGetStyleProperty<T>(string param, out T value)
{
if (_styleProperties.TryGetValue(param, out var val))
{
value = (T) val;
return true;
}
value = default;
return false;
}
}
}

View File

@@ -466,31 +466,6 @@ namespace SS14.Client.UserInterface
}
}
private readonly HashSet<string> _styleClasses = new HashSet<string>();
public IReadOnlyCollection<string> StyleClasses => _styleClasses;
private string _styleIdentifier;
public string StyleIdentifier
{
get => _styleIdentifier;
set => _styleIdentifier = value;
}
public bool HasStyleClass(string className)
{
return _styleClasses.Contains(className);
}
public void AddStyleClass(string className)
{
_styleClasses.Add(className);
}
public void RemoveStyleClass(string className)
{
_styleClasses.Remove(className);
}
public Color Modulate
{
get => SceneControl.Modulate.Convert();
@@ -586,6 +561,7 @@ namespace SS14.Client.UserInterface
Name = GetType().Name;
Initialize();
_applyPropertyMap();
Restyle();
}
/// <param name="name">The name the component will have.</param>
@@ -610,6 +586,7 @@ namespace SS14.Client.UserInterface
Name = name;
Initialize();
_applyPropertyMap();
Restyle();
}
@@ -625,6 +602,7 @@ namespace SS14.Client.UserInterface
SetupSignalHooks();
//Logger.Debug($"Wrapping control {Name} ({GetType()} -> {control.GetType()})");
Initialize();
Restyle();
}
/// <summary>
@@ -1184,6 +1162,7 @@ namespace SS14.Client.UserInterface
protected virtual void Parented(Control newParent)
{
MinimumSizeChanged();
Restyle();
}
/// <summary>
@@ -1230,6 +1209,7 @@ namespace SS14.Client.UserInterface
/// </summary>
protected virtual void Deparented()
{
Restyle();
}
/// <summary>

View File

@@ -134,7 +134,7 @@ namespace SS14.Client.UserInterface.Controls
return;
}
var uiTheme = UserInterfaceManager.Theme;
var uiTheme = UserInterfaceManager.ThemeDefaults;
StyleBox style;
switch (DrawMode)
{
@@ -197,7 +197,7 @@ namespace SS14.Client.UserInterface.Controls
return base.CalculateMinimumSize();
}
var uiTheme = UserInterfaceManager.Theme;
var uiTheme = UserInterfaceManager.ThemeDefaults;
var style = uiTheme.ButtonStyleNormal;
var font = uiTheme.DefaultFont;
@@ -221,7 +221,7 @@ namespace SS14.Client.UserInterface.Controls
return 0;
}
var uiTheme = UserInterfaceManager.Theme;
var uiTheme = UserInterfaceManager.ThemeDefaults;
var font = uiTheme.DefaultFont;
var textWidth = 0;

View File

@@ -13,6 +13,9 @@ namespace SS14.Client.UserInterface.Controls
[ControlWrap(typeof(Godot.Label))]
public class Label : Control
{
public const string StylePropertyFontColor = "font-color";
public const string StylePropertyFont = "font";
private Vector2i? _textDimensionCache;
public Label(string name) : base(name)
@@ -105,6 +108,24 @@ namespace SS14.Client.UserInterface.Controls
set => SetFontOverride("font", _fontOverride = value);
}
private Font ActualFont
{
get
{
if (_fontOverride != null)
{
return _fontOverride;
}
if (TryGetStyleProperty<Font>(StylePropertyFont, out var font))
{
return font;
}
return UserInterfaceManager.ThemeDefaults.LabelFont;
}
}
private Color? _fontColorShadowOverride;
public Color? FontColorShadowOverride
@@ -113,6 +134,24 @@ namespace SS14.Client.UserInterface.Controls
set => SetColorOverride("font_color_shadow", _fontColorShadowOverride = value);
}
private Color ActualFontColor
{
get
{
if (_fontColorOverride.HasValue)
{
return _fontColorOverride.Value;
}
if (TryGetStyleProperty<Color>(StylePropertyFontColor, out var color))
{
return color;
}
return Color.White;
}
}
private Color? _fontColorOverride;
public Color? FontColorOverride
@@ -195,8 +234,9 @@ namespace SS14.Client.UserInterface.Controls
}
var newlines = 0;
var font = _fontOverride ?? UserInterfaceManager.Theme.LabelFont;
var font = ActualFont;
var baseLine = new Vector2(hOffset, font.Ascent + vOffset);
var actualFontColor = ActualFontColor;
foreach (var chr in _text)
{
if (chr == '\n')
@@ -205,7 +245,7 @@ namespace SS14.Client.UserInterface.Controls
baseLine = new Vector2(hOffset, font.Ascent + font.Height * newlines);
}
var advance = font.DrawChar(handle, chr, baseLine, FontColorOverride ?? Color.White);
var advance = font.DrawChar(handle, chr, baseLine, actualFontColor);
baseLine += new Vector2(advance, 0);
}
}
@@ -250,7 +290,7 @@ namespace SS14.Client.UserInterface.Controls
return;
}
var font = _fontOverride ?? UserInterfaceManager.Theme.LabelFont;
var font = ActualFont;
var height = font.Height;
var maxLineSize = 0;
var currentLineSize = 0;

View File

@@ -161,11 +161,11 @@ namespace SS14.Client.UserInterface.Controls
return;
}
var styleBox = UserInterfaceManager.Theme.LineEditBox;
var styleBox = UserInterfaceManager.ThemeDefaults.LineEditBox;
var drawBox = new UIBox2(Vector2.Zero, Size);
var contentBox = styleBox.GetContentBox(drawBox);
styleBox.Draw(handle, drawBox);
var font = UserInterfaceManager.Theme.DefaultFont;
var font = UserInterfaceManager.ThemeDefaults.DefaultFont;
var baseLine = new Vector2i(0, (int)(contentBox.Height + font.Ascent)/2) + contentBox.TopLeft;
@@ -200,8 +200,8 @@ namespace SS14.Client.UserInterface.Controls
return Vector2.Zero;
}
var font = UserInterfaceManager.Theme.DefaultFont;
return new Vector2(0, font.Height) + UserInterfaceManager.Theme.LineEditBox.MinimumSize;
var font = UserInterfaceManager.ThemeDefaults.DefaultFont;
return new Vector2(0, font.Height) + UserInterfaceManager.ThemeDefaults.LineEditBox.MinimumSize;
}
public enum AlignMode

View File

@@ -38,7 +38,7 @@ namespace SS14.Client.UserInterface.Controls
if (!GameController.OnGodot)
{
var panel = _panelOverride ?? UserInterfaceManager.Theme.PanelPanel;
var panel = _panelOverride ?? UserInterfaceManager.ThemeDefaults.PanelPanel;
panel.Draw(handle, UIBox2.FromDimensions(Vector2.Zero, Size));
}
}

View File

@@ -10,6 +10,8 @@ namespace SS14.Client.UserInterface.CustomControls
[ControlWrap("res://Engine/Scenes/SS14Window/SS14Window.tscn")]
public class SS14Window : Panel
{
public const string StyleClassWindowTitle = "windowTitle";
[Dependency] private readonly IDisplayManager _displayManager;
public SS14Window() : base()
@@ -87,6 +89,8 @@ namespace SS14.Client.UserInterface.CustomControls
CloseButton.OnPressed += CloseButtonPressed;
Contents = GetChild("Contents");
TitleLabel.AddStyleClass(StyleClassWindowTitle);
}
protected override void Dispose(bool disposing)

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using SS14.Shared.Utility;
namespace SS14.Client.UserInterface
{
@@ -9,7 +11,7 @@ namespace SS14.Client.UserInterface
/// </summary>
public sealed class Stylesheet
{
private IReadOnlyList<StyleRule> Rules { get; }
public IReadOnlyList<StyleRule> Rules { get; }
public Stylesheet(IReadOnlyList<StyleRule> rules)
{
@@ -26,12 +28,67 @@ namespace SS14.Client.UserInterface
{
Selector = selector;
Properties = properties;
Specificity = selector.CalculateSpecificity();
}
public StyleSpecificity Specificity { get; }
public Selector Selector { get; }
public IReadOnlyList<StyleProperty> Properties { get; }
}
// https://specifishity.com/
public struct StyleSpecificity : IComparable<StyleSpecificity>, 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 StyleSpecificity(
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})";
}
}
/// <summary>
/// A single property in a rule, with a name and an object value.
/// </summary>
@@ -50,6 +107,7 @@ namespace SS14.Client.UserInterface
public abstract class Selector
{
public abstract bool Matches(Control control);
public abstract StyleSpecificity CalculateSpecificity();
}
public sealed class SelectorElement : Selector
@@ -57,16 +115,19 @@ namespace SS14.Client.UserInterface
public SelectorElement(
string elementType,
IReadOnlyCollection<string> elementClasses,
string elementId)
string elementId,
string pseudoClass)
{
ElementType = elementType;
ElementClasses = elementClasses;
ElementId = elementId;
PseudoClass = pseudoClass;
}
public string ElementType { get; }
public IReadOnlyCollection<string> ElementClasses { get; }
public string ElementId { get; }
public string PseudoClass { get; }
public override bool Matches(Control control)
{
@@ -93,9 +154,19 @@ namespace SS14.Client.UserInterface
return true;
}
public override StyleSpecificity CalculateSpecificity()
{
var countId = ElementId == null ? 0 : 1;
var countClasses = (ElementClasses?.Count ?? 0) + (PseudoClass == null ? 0 : 1);
var countTypes = ElementType == null ? 0 : 1;
return new StyleSpecificity(countId, countClasses, countTypes);
}
}
public sealed class SelectorDescendant : Selector
// Temporarily hidden due to performance concerns.
// Like seriously this thing is O(n!)
internal sealed class SelectorDescendant : Selector
{
public SelectorDescendant([NotNull] Selector ascendant, [NotNull] Selector descendant)
{
@@ -128,8 +199,14 @@ namespace SS14.Client.UserInterface
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)
@@ -150,5 +227,10 @@ namespace SS14.Client.UserInterface
return Parent.Matches(control.Parent) && Child.Matches(control);
}
public override StyleSpecificity CalculateSpecificity()
{
return Parent.CalculateSpecificity() + Child.CalculateSpecificity();
}
}
}

View File

@@ -7,6 +7,9 @@ using SS14.Shared.Maths;
namespace SS14.Client.UserInterface
{
/// <summary>
/// Fallback theme system for GUI.
/// </summary>
public abstract class UITheme
{
public abstract Font DefaultFont { get; }

View File

@@ -31,8 +31,8 @@ namespace SS14.Client.UserInterface
[Dependency] private readonly IDisplayManager _displayManager;
[Dependency] private readonly IResourceCache _resourceCache;
public UITheme Theme { get; private set; }
public UITheme ThemeDefaults { get; private set; }
public Stylesheet Stylesheet { get; set; }
public Control Focused { get; private set; }
// When a control receives a mouse down it must also receive a mouse up and mouse moves, always.
@@ -59,7 +59,7 @@ namespace SS14.Client.UserInterface
public void Initialize()
{
Theme = new UIThemeDefault();
ThemeDefaults = new UIThemeDefault();
if (GameController.OnGodot)
{

View File

@@ -12,7 +12,8 @@ namespace SS14.UnitTesting.Client
{
internal class DummyUserInterfaceManager : IUserInterfaceManagerInternal
{
public UITheme Theme { get; } = new UIThemeDummy();
public UITheme ThemeDefaults { get; } = new UIThemeDummy();
public Stylesheet Stylesheet { get; set; }
public Control Focused => throw new System.NotImplementedException();

View File

@@ -1,6 +1,8 @@
using NUnit.Framework;
using SS14.Client.Interfaces.UserInterface;
using SS14.Client.UserInterface;
using SS14.Client.UserInterface.Controls;
using SS14.Shared.IoC;
using SS14.Shared.Utility;
namespace SS14.UnitTesting.Client.UserInterface
@@ -13,14 +15,14 @@ namespace SS14.UnitTesting.Client.UserInterface
[Test]
public void TestSelectors()
{
var selectorElementLabel = new SelectorElement("Label", null, null);
var selectorElementLabel = new SelectorElement("Label", null, null, null);
var label = new Label();
var panel = new Panel {StyleIdentifier = "bar"};
Assert.That(selectorElementLabel.Matches(label), Is.True);
Assert.That(selectorElementLabel.Matches(panel), Is.False);
selectorElementLabel = new SelectorElement("Label", new []{"foo"}, null);
selectorElementLabel = new SelectorElement("Label", new []{"foo"}, null, null);
Assert.That(selectorElementLabel.Matches(label), Is.False);
Assert.That(selectorElementLabel.Matches(panel), Is.False);
@@ -36,9 +38,40 @@ namespace SS14.UnitTesting.Client.UserInterface
// Make sure it doesn't throw.
label.RemoveStyleClass("foo");
selectorElementLabel = new SelectorElement(null, null, "bar");
selectorElementLabel = new SelectorElement(null, null, "bar", null);
Assert.That(selectorElementLabel.Matches(label), Is.False);
Assert.That(selectorElementLabel.Matches(panel), Is.True);
}
[Test]
public void TestStyleProperties()
{
var sheet = new Stylesheet(new []
{
new StyleRule(new SelectorElement("Label", null, "baz", null), new []
{
new StyleProperty("foo", "honk"),
}),
new StyleRule(new SelectorElement("Label", null, null, null), new []
{
new StyleProperty("foo", "heh"),
}),
new StyleRule(new SelectorElement("Label", null, null, null), new []
{
new StyleProperty("foo", "bar"),
}),
});
var uiMgr = IoCManager.Resolve<IUserInterfaceManager>();
uiMgr.Stylesheet = sheet;
var control = new Label();
control.TryGetStyleProperty("foo", out string value);
Assert.That(value, Is.EqualTo("bar"));
control.StyleIdentifier = "baz";
control.TryGetStyleProperty("foo", out value);
Assert.That(value, Is.EqualTo("honk"));
}
}
}