Browse Source

Initial implementation of control themes.

pull/8262/head
Steven Kirk 4 years ago
parent
commit
088d8cfc5c
  1. 27
      src/Avalonia.Base/Styling/ControlTheme.cs
  2. 2
      src/Avalonia.Base/Styling/IStyle.cs
  3. 13
      src/Avalonia.Base/Styling/IThemed.cs
  4. 4
      src/Avalonia.Base/Styling/NestingSelector.cs
  5. 8
      src/Avalonia.Base/Styling/Style.cs
  6. 20
      src/Avalonia.Base/Styling/StyleBase.cs
  7. 2
      src/Avalonia.Base/Styling/StyleCache.cs
  8. 14
      src/Avalonia.Base/Styling/Styler.cs
  9. 2
      src/Avalonia.Base/Styling/Styles.cs
  10. 25
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  11. 2
      src/Avalonia.Themes.Default/SimpleTheme.cs
  12. 2
      src/Avalonia.Themes.Fluent/FluentTheme.cs
  13. 2
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  14. 119
      tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs

27
src/Avalonia.Base/Styling/ControlTheme.cs

@ -0,0 +1,27 @@
using System;
namespace Avalonia.Styling
{
/// <summary>
/// Defines a switchable theme for a control.
/// </summary>
public class ControlTheme : StyleBase
{
/// <summary>
/// Gets or sets the type for which this control theme is intended.
/// </summary>
public Type? TargetType { get; set; }
internal override bool HasSelector => TargetType is not null;
internal override SelectorMatch Match(IStyleable control, object? host, bool subscribe)
{
if (TargetType is null)
throw new InvalidOperationException("ControlTheme has no TargetType.");
return control.StyleKey == TargetType ?
SelectorMatch.AlwaysThisType :
SelectorMatch.NeverThisType;
}
}
}

2
src/Avalonia.Base/Styling/IStyle.cs

@ -23,6 +23,6 @@ namespace Avalonia.Styling
/// <returns>
/// A <see cref="SelectorMatchResult"/> describing how the style matches the control.
/// </returns>
SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host);
SelectorMatchResult TryAttach(IStyleable target, object? host);
}
}

13
src/Avalonia.Base/Styling/IThemed.cs

@ -0,0 +1,13 @@
namespace Avalonia.Styling
{
/// <summary>
/// Represents a themed element.
/// </summary>
public interface IThemed
{
/// <summary>
/// Gets the theme style for the element.
/// </summary>
public ControlTheme? Theme { get; }
}
}

4
src/Avalonia.Base/Styling/NestingSelector.cs

@ -15,9 +15,9 @@ namespace Avalonia.Styling
protected override SelectorMatch Evaluate(IStyleable control, IStyle? parent, bool subscribe)
{
if (parent is Style s && s.Selector is Selector selector)
if (parent is StyleBase s && s.HasSelector)
{
return selector.Match(control, (parent as Style)?.Parent, subscribe);
return s.Match(control, null, subscribe);
}
throw new InvalidOperationException(

8
src/Avalonia.Base/Styling/Style.cs

@ -28,6 +28,8 @@ namespace Avalonia.Styling
/// </summary>
public Selector? Selector { get; set; }
internal override bool HasSelector => Selector is not null;
/// <summary>
/// Returns a string representation of the style.
/// </summary>
@ -44,10 +46,10 @@ namespace Avalonia.Styling
}
}
protected override SelectorMatch Matches(IStyleable target, IStyleHost? host)
internal override SelectorMatch Match(IStyleable control, object? host, bool subscribe)
{
return Selector?.Match(target, Parent) ??
(target == host ?
return Selector?.Match(control, Parent, subscribe) ??
(control == host ?
SelectorMatch.AlwaysThisInstance :
SelectorMatch.NeverThisInstance);
}

20
src/Avalonia.Base/Styling/StyleBase.cs

@ -64,12 +64,14 @@ namespace Avalonia.Styling
bool IResourceNode.HasResources => _resources?.Count > 0;
IReadOnlyList<IStyle> IStyle.Children => (IReadOnlyList<IStyle>?)_children ?? Array.Empty<IStyle>();
internal abstract bool HasSelector { get; }
public void Add(ISetter setter) => Setters.Add(setter);
public void Add(IStyle style) => Children.Add(style);
public event EventHandler? OwnerChanged;
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
public SelectorMatchResult TryAttach(IStyleable target, object? host)
{
target = target ?? throw new ArgumentNullException(nameof(target));
@ -77,7 +79,7 @@ namespace Avalonia.Styling
if (_setters?.Count > 0 || _animations?.Count > 0)
{
var match = Matches(target, host);
var match = Match(target, host, subscribe: true);
if (match.IsMatch)
{
@ -106,7 +108,19 @@ namespace Avalonia.Styling
return _resources?.TryGetResource(key, out result) ?? false;
}
protected abstract SelectorMatch Matches(IStyleable target, IStyleHost? host);
/// <summary>
/// Evaluates the style's selector against the specified target element.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="host">The element that hosts the style.</param>
/// <param name="subscribe">
/// Whether the match should subscribe to changes in order to track the match over time,
/// or simply return an immediate result.
/// </param>
/// <returns>
/// A <see cref="SelectorMatchResult"/> describing how the style matches the control.
/// </returns>
internal abstract SelectorMatch Match(IStyleable control, object? host, bool subscribe);
internal virtual void SetParent(StyleBase? parent) => Parent = parent;

2
src/Avalonia.Base/Styling/StyleCache.cs

@ -12,7 +12,7 @@ namespace Avalonia.Styling
/// </remarks>
internal class StyleCache : Dictionary<Type, List<IStyle>?>
{
public SelectorMatchResult TryAttach(IList<IStyle> styles, IStyleable target, IStyleHost? host)
public SelectorMatchResult TryAttach(IList<IStyle> styles, IStyleable target, object? host)
{
if (TryGetValue(target.StyleKey, out var cached))
{

14
src/Avalonia.Base/Styling/Styler.cs

@ -10,6 +10,20 @@ namespace Avalonia.Styling
{
target = target ?? throw new ArgumentNullException(nameof(target));
// If the control has a themed templated parent then first apply the styles from
// the templated parent theme.
if (target.TemplatedParent is IThemed themedTemplatedParent)
{
themedTemplatedParent.Theme?.TryAttach(target, themedTemplatedParent);
}
// If the control itself is themed, then next apply the control theme.
if (target is IThemed themed)
{
themed.Theme?.TryAttach(target, target);
}
// Apply styles from the rest of the tree.
if (target is IStyleHost styleHost)
{
ApplyStyles(target, styleHost);

2
src/Avalonia.Base/Styling/Styles.cs

@ -109,7 +109,7 @@ namespace Avalonia.Styling
set => _styles[index] = value;
}
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
public SelectorMatchResult TryAttach(IStyleable target, object? host)
{
_cache ??= new StyleCache();
return _cache.TryAttach(this, target, host);

25
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -12,7 +12,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// A lookless control whose visual appearance is defined by its <see cref="Template"/>.
/// </summary>
public class TemplatedControl : Control, ITemplatedControl
public class TemplatedControl : Control, IThemed, ITemplatedControl
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -86,6 +86,12 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<IControlTemplate?> TemplateProperty =
AvaloniaProperty.Register<TemplatedControl, IControlTemplate?>(nameof(Template));
/// <summary>
/// Defines the <see cref="Theme"/> property.
/// </summary>
public static readonly StyledProperty<ControlTheme?> ThemeProperty =
AvaloniaProperty.Register<TemplatedControl, ControlTheme?>(nameof(Theme));
/// <summary>
/// Defines the IsTemplateFocusTarget attached property.
/// </summary>
@ -228,6 +234,15 @@ namespace Avalonia.Controls.Primitives
set { SetValue(TemplateProperty, value); }
}
/// <summary>
/// Gets or sets the theme to be applied to the control.
/// </summary>
public ControlTheme? Theme
{
get { return GetValue(ThemeProperty); }
set { SetValue(ThemeProperty, value); }
}
/// <summary>
/// Gets the value of the IsTemplateFocusTargetProperty attached property on a control.
/// </summary>
@ -365,6 +380,14 @@ namespace Avalonia.Controls.Primitives
{
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ThemeProperty)
InvalidateStyles();
}
/// <summary>
/// Called when the control's template is applied.
/// </summary>

2
src/Avalonia.Themes.Default/SimpleTheme.cs

@ -103,7 +103,7 @@ namespace Avalonia.Themes.Default
void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner);
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host);
public bool TryGetResource(object key, out object? value)
{

2
src/Avalonia.Themes.Fluent/FluentTheme.cs

@ -164,7 +164,7 @@ namespace Avalonia.Themes.Fluent
}
}
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host);
public bool TryGetResource(object key, out object? value)
{

2
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@ -82,7 +82,7 @@ namespace Avalonia.Markup.Xaml.Styling
}
}
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host);
public bool TryGetResource(object key, out object? value)
{

119
tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests_Theming.cs

@ -0,0 +1,119 @@
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
#nullable enable
namespace Avalonia.Controls.UnitTests.Primitives
{
public class TemplatedControlTests_Theming
{
[Fact]
public void Theme_Is_Applied_When_Attached_To_Logical_Tree()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = CreateTarget();
Assert.Null(target.Template);
var root = CreateRoot(target);
Assert.NotNull(target.Template);
var border = Assert.IsType<Border>(target.VisualChild);
Assert.Equal(border.Background, Brushes.Red);
target.Classes.Add("foo");
Assert.Equal(border.Background, Brushes.Green);
}
[Fact]
public void Theme_Is_Detached_When_Theme_Property_Cleared()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = CreateTarget();
var root = CreateRoot(target);
Assert.NotNull(target.Template);
target.Theme = null;
Assert.Null(target.Template);
}
[Fact]
public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes()
{
using var app = UnitTestApplication.Start(TestServices.RealStyler);
var target = new ThemedControl();
var root = CreateRoot(target);
Assert.Null(target.Template);
target.Theme = CreateTheme();
Assert.Null(target.Template);
root.LayoutManager.ExecuteLayoutPass();
var border = Assert.IsType<Border>(target.VisualChild);
Assert.NotNull(target.Template);
Assert.Equal(border.Background, Brushes.Red);
}
private static ThemedControl CreateTarget()
{
return new ThemedControl
{
Theme = CreateTheme(),
};
}
private static ControlTheme CreateTheme()
{
var template = new FuncControlTemplate<ThemedControl>((o, n) =>
new Border { Name = "PART_Border" });
return new ControlTheme
{
TargetType = typeof(ThemedControl),
Setters =
{
new Setter(ThemedControl.TemplateProperty, template),
},
Children =
{
new Style(x => x.Nesting().Template().OfType<Border>())
{
Setters =
{
new Setter(Border.BackgroundProperty, Brushes.Red),
}
},
new Style(x => x.Nesting().Class("foo").Template().OfType<Border>())
{
Setters =
{
new Setter(Border.BackgroundProperty, Brushes.Green),
}
},
}
};
}
private static TestRoot CreateRoot(IControl child)
{
var result = new TestRoot(child);
result.LayoutManager.ExecuteInitialLayoutPass();
return result;
}
private class ThemedControl : TemplatedControl
{
public IVisual? VisualChild => VisualChildren?.SingleOrDefault();
}
}
}
Loading…
Cancel
Save