StyledElementTests_Theming.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. using System.Linq;
  2. using Avalonia.Controls;
  3. using Avalonia.Controls.Primitives;
  4. using Avalonia.Controls.Templates;
  5. using Avalonia.Media;
  6. using Avalonia.Styling;
  7. using Avalonia.UnitTests;
  8. using Avalonia.VisualTree;
  9. using Xunit;
  10. #nullable enable
  11. namespace Avalonia.Base.UnitTests.Styling;
  12. public class StyledElementTests_Theming
  13. {
  14. public class InlineTheme
  15. {
  16. [Fact]
  17. public void Theme_Is_Applied_When_Attached_To_Logical_Tree()
  18. {
  19. var target = CreateTarget();
  20. Assert.Null(target.Template);
  21. CreateRoot(target);
  22. Assert.NotNull(target.Template);
  23. var border = Assert.IsType<Border>(target.VisualChild);
  24. Assert.Equal(Brushes.Red, border.Background);
  25. target.Classes.Add("foo");
  26. Assert.Equal(Brushes.Green, border.Background);
  27. }
  28. [Fact]
  29. public void Theme_Is_Applied_To_Derived_Class_When_Attached_To_Logical_Tree()
  30. {
  31. var target = new DerivedThemedControl
  32. {
  33. Theme = CreateTheme(),
  34. };
  35. Assert.Null(target.Template);
  36. CreateRoot(target);
  37. Assert.NotNull(target.Template);
  38. var border = Assert.IsType<Border>(target.VisualChild);
  39. Assert.Equal(Brushes.Red, border.Background);
  40. target.Classes.Add("foo");
  41. Assert.Equal(Brushes.Green, border.Background);
  42. }
  43. [Fact]
  44. public void Theme_Is_Detached_When_Theme_Property_Cleared()
  45. {
  46. var target = CreateTarget();
  47. CreateRoot(target);
  48. Assert.NotNull(target.Template);
  49. target.Theme = null;
  50. Assert.Null(target.Template);
  51. }
  52. [Fact]
  53. public void Setting_Explicit_Theme_Detaches_Default_Theme()
  54. {
  55. var target = new ThemedControl();
  56. var root = new TestRoot
  57. {
  58. Resources = { { typeof(ThemedControl), CreateTheme() } },
  59. Child = target,
  60. };
  61. root.LayoutManager.ExecuteInitialLayoutPass();
  62. Assert.Equal("theme", target.Tag);
  63. target.Theme = new ControlTheme(typeof(ThemedControl))
  64. {
  65. Setters =
  66. {
  67. new Setter(ThemedControl.BackgroundProperty, Brushes.Yellow),
  68. }
  69. };
  70. root.LayoutManager.ExecuteLayoutPass();
  71. Assert.Null(target.Tag);
  72. Assert.Equal(Brushes.Yellow, target.Background);
  73. }
  74. [Fact]
  75. public void Unrelated_Styles_Are_Not_Detached_When_Theme_Property_Cleared()
  76. {
  77. var target = CreateTarget();
  78. CreateRoot(target, createAdditionalStyles: true);
  79. Assert.Equal("style", target.Tag);
  80. target.Theme = null;
  81. Assert.Equal("style", target.Tag);
  82. }
  83. [Fact]
  84. public void TemplatedParent_Theme_Is_Detached_From_Template_Controls_When_Theme_Property_Cleared()
  85. {
  86. var theme = new ControlTheme
  87. {
  88. TargetType = typeof(ThemedControl),
  89. Children =
  90. {
  91. new Style(x => x.Nesting().Template().OfType<Canvas>())
  92. {
  93. Setters =
  94. {
  95. new Setter(Panel.BackgroundProperty, Brushes.Red),
  96. }
  97. },
  98. }
  99. };
  100. var target = CreateTarget(theme);
  101. target.Template = new FuncControlTemplate<ThemedControl>((o, n) => new Canvas());
  102. var root = CreateRoot(target);
  103. var canvas = Assert.IsType<Canvas>(target.VisualChild);
  104. Assert.Equal(Brushes.Red, canvas.Background);
  105. target.Theme = null;
  106. Assert.Same(canvas, target.VisualChild);
  107. Assert.Null(canvas.Background);
  108. }
  109. [Fact]
  110. public void Primary_Theme_Is_Not_Detached_From_Template_Controls_When_Theme_Property_Cleared()
  111. {
  112. var templatedParentTheme = new ControlTheme
  113. {
  114. TargetType = typeof(ThemedControl),
  115. Children =
  116. {
  117. new Style(x => x.Nesting().Template().OfType<Button>())
  118. {
  119. Setters =
  120. {
  121. new Setter(Panel.BackgroundProperty, Brushes.Red),
  122. }
  123. },
  124. }
  125. };
  126. var childTheme = new ControlTheme
  127. {
  128. TargetType = typeof(Button),
  129. Setters =
  130. {
  131. new Setter(TemplatedControl.ForegroundProperty, Brushes.Green),
  132. }
  133. };
  134. var target = CreateTarget(templatedParentTheme);
  135. target.Template = new FuncControlTemplate<ThemedControl>((o, n) => new Button
  136. {
  137. Theme = childTheme,
  138. });
  139. var root = CreateRoot(target, createAdditionalStyles: true);
  140. var templateChild = Assert.IsType<Button>(target.VisualChild);
  141. Assert.Equal(Brushes.Red, templateChild.Background);
  142. Assert.Equal(Brushes.Green, templateChild.Foreground);
  143. target.Theme = null;
  144. Assert.Null(templateChild.Background);
  145. Assert.Equal(Brushes.Green, templateChild.Foreground);
  146. }
  147. [Fact]
  148. public void TemplatedParent_Theme_Is_Not_Detached_From_Template_Controls_When_Primary_Theme_Property_Cleared()
  149. {
  150. var templatedParentTheme = new ControlTheme
  151. {
  152. TargetType = typeof(ThemedControl),
  153. Children =
  154. {
  155. new Style(x => x.Nesting().Template().OfType<Button>())
  156. {
  157. Setters =
  158. {
  159. new Setter(Panel.BackgroundProperty, Brushes.Red),
  160. }
  161. },
  162. }
  163. };
  164. var childTheme = new ControlTheme
  165. {
  166. TargetType = typeof(Button),
  167. Setters =
  168. {
  169. new Setter(Button.TagProperty, "childTheme"),
  170. }
  171. };
  172. var target = CreateTarget(templatedParentTheme);
  173. target.Template = new FuncControlTemplate<ThemedControl>((o, n) => new Button
  174. {
  175. Theme = childTheme,
  176. });
  177. var root = CreateRoot(target, createAdditionalStyles: true);
  178. var templateChild = Assert.IsType<Button>(target.VisualChild);
  179. Assert.Equal(Brushes.Red, templateChild.Background);
  180. Assert.Equal("childTheme", templateChild.Tag);
  181. templateChild.Theme = null;
  182. Assert.Equal(Brushes.Red, templateChild.Background);
  183. Assert.Null(templateChild.Tag);
  184. }
  185. [Fact]
  186. public void Unrelated_Styles_Are_Not_Detached_From_Template_Controls_When_Theme_Property_Cleared()
  187. {
  188. var target = CreateTarget();
  189. var root = CreateRoot(target, createAdditionalStyles: true);
  190. var canvas = Assert.IsType<Border>(target.VisualChild);
  191. Assert.Equal("style", canvas.Tag);
  192. target.Theme = null;
  193. Assert.Same(canvas, target.VisualChild);
  194. Assert.Equal("style", canvas.Tag);
  195. }
  196. [Fact]
  197. public void Theme_Is_Applied_On_Layout_After_Theme_Property_Changes()
  198. {
  199. var target = new ThemedControl();
  200. var root = CreateRoot(target);
  201. Assert.Null(target.Template);
  202. target.Theme = CreateTheme();
  203. Assert.Null(target.Template);
  204. root.LayoutManager.ExecuteLayoutPass();
  205. var border = Assert.IsType<Border>(target.VisualChild);
  206. Assert.NotNull(target.Template);
  207. Assert.Equal(Brushes.Red, border.Background);
  208. }
  209. [Fact]
  210. public void BasedOn_Theme_Is_Applied_When_Attached_To_Logical_Tree()
  211. {
  212. var target = CreateTarget(CreateDerivedTheme());
  213. Assert.Null(target.Template);
  214. CreateRoot(target);
  215. Assert.NotNull(target.Template);
  216. Assert.Equal(Brushes.Blue, target.BorderBrush);
  217. var border = Assert.IsType<Border>(target.VisualChild);
  218. Assert.Equal(Brushes.Red, border.Background);
  219. Assert.Equal(Brushes.Yellow, border.BorderBrush);
  220. target.Classes.Add("foo");
  221. Assert.Equal(Brushes.Green, border.Background);
  222. Assert.Equal(Brushes.Cyan, border.BorderBrush);
  223. }
  224. [Fact]
  225. public void Theme_Has_Lower_Priority_Than_Style()
  226. {
  227. var target = CreateTarget();
  228. CreateRoot(target, createAdditionalStyles: true);
  229. Assert.Equal("style", target.Tag);
  230. }
  231. [Fact]
  232. public void Theme_Has_Lower_Priority_Than_Style_After_Change()
  233. {
  234. var target = CreateTarget();
  235. var theme = target.Theme;
  236. CreateRoot(target, createAdditionalStyles: true);
  237. target.Theme = null;
  238. target.Theme = theme;
  239. target.ApplyStyling();
  240. Assert.Equal("style", target.Tag);
  241. }
  242. private static ThemedControl CreateTarget(ControlTheme? theme = null)
  243. {
  244. return new ThemedControl
  245. {
  246. Theme = theme ?? CreateTheme(),
  247. };
  248. }
  249. private static TestRoot CreateRoot(
  250. Control child,
  251. bool createAdditionalStyles = false)
  252. {
  253. var result = new TestRoot();
  254. if (createAdditionalStyles)
  255. {
  256. result.Styles.Add(new Style(x => x.OfType<ThemedControl>())
  257. {
  258. Setters =
  259. {
  260. new Setter(Control.TagProperty, "style"),
  261. }
  262. });
  263. result.Styles.Add(new Style(x => x.OfType<Border>())
  264. {
  265. Setters =
  266. {
  267. new Setter(Control.TagProperty, "style"),
  268. }
  269. });
  270. }
  271. result.Child = child;
  272. result.LayoutManager.ExecuteInitialLayoutPass();
  273. return result;
  274. }
  275. }
  276. public class ImplicitTheme
  277. {
  278. [Fact]
  279. public void Implicit_Theme_Is_Applied_When_Attached_To_Logical_Tree()
  280. {
  281. var target = CreateTarget();
  282. CreateRoot(target);
  283. Assert.NotNull(target.Template);
  284. var border = Assert.IsType<Border>(target.VisualChild);
  285. Assert.Equal(Brushes.Red, border.Background);
  286. target.Classes.Add("foo");
  287. Assert.Equal(Brushes.Green, border.Background);
  288. }
  289. [Fact]
  290. public void Implicit_Theme_Is_Not_Detached_When_Removed_From_Logical_Tree()
  291. {
  292. var target = CreateTarget();
  293. var root = CreateRoot(target);
  294. Assert.Equal("theme", target.Tag);
  295. root.Child = null;
  296. var border = Assert.IsType<Border>(target.VisualChild);
  297. Assert.Equal("theme", target.Tag);
  298. Assert.Equal("theme", border.Tag);
  299. }
  300. [Fact]
  301. public void Can_Attach_Then_Reattach_To_Same_Logical_Tree()
  302. {
  303. var target = CreateTarget();
  304. var root = CreateRoot(target);
  305. Assert.Equal("theme", target.Tag);
  306. root.Child = null;
  307. root.Child = target;
  308. Assert.Equal("theme", target.Tag);
  309. }
  310. [Fact]
  311. public void Implicit_Theme_Is_Reevaluated_When_Removed_And_Added_To_Different_Logical_Tree()
  312. {
  313. var target = CreateTarget();
  314. var root1 = CreateRoot(target, "theme1");
  315. var root2 = CreateRoot(null, "theme2");
  316. Assert.Equal("theme1", target.Tag);
  317. root1.Child = null;
  318. root2.Child = target;
  319. var border = Assert.IsType<Border>(target.VisualChild);
  320. Assert.Equal("theme2", target.Tag);
  321. Assert.Equal("theme2", border.Tag);
  322. }
  323. [Fact]
  324. public void Nested_Style_Can_Override_Property_In_Inner_Templated_Control()
  325. {
  326. var target = new ThemedControl2
  327. {
  328. Theme = new ControlTheme(typeof(ThemedControl2))
  329. {
  330. Setters =
  331. {
  332. new Setter(
  333. Controls.Primitives.TemplatedControl.TemplateProperty,
  334. new FuncControlTemplate<ThemedControl2>((o, n) => new ThemedControl())),
  335. },
  336. Children =
  337. {
  338. new Style(x => x.Nesting().Template().OfType<ThemedControl>())
  339. {
  340. Setters = { new Setter(Controls.Primitives.TemplatedControl.CornerRadiusProperty, new CornerRadius(7)), }
  341. },
  342. }
  343. },
  344. };
  345. var root = CreateRoot(target);
  346. var inner = Assert.IsType<ThemedControl>(target.VisualChild);
  347. Assert.Equal(new CornerRadius(7), inner.CornerRadius);
  348. }
  349. private static ThemedControl CreateTarget() => new ThemedControl();
  350. private static TestRoot CreateRoot(Control? child, string themeTag = "theme")
  351. {
  352. var result = new TestRoot();
  353. result.Resources.Add(typeof(ThemedControl), CreateTheme(themeTag));
  354. result.Child = child;
  355. result.LayoutManager.ExecuteInitialLayoutPass();
  356. return result;
  357. }
  358. }
  359. public class ThemeFromStyle
  360. {
  361. [Fact]
  362. public void Theme_Is_Applied_When_Attached_To_Logical_Tree()
  363. {
  364. var target = CreateTarget();
  365. Assert.Null(target.Theme);
  366. Assert.Null(target.Template);
  367. CreateRoot(target);
  368. Assert.NotNull(target.Theme);
  369. Assert.NotNull(target.Template);
  370. var border = Assert.IsType<Border>(target.VisualChild);
  371. Assert.Equal(Brushes.Red, border.Background);
  372. target.Classes.Add("foo");
  373. Assert.Equal(Brushes.Green, border.Background);
  374. }
  375. [Fact]
  376. public void Theme_Can_Be_Changed_By_Style_Class()
  377. {
  378. var target = CreateTarget();
  379. var theme1 = CreateTheme();
  380. var theme2 = new ControlTheme(typeof(ThemedControl));
  381. var root = new TestRoot()
  382. {
  383. Styles =
  384. {
  385. new Style(x => x.OfType<ThemedControl>())
  386. {
  387. Setters = { new Setter(StyledElement.ThemeProperty, theme1) }
  388. },
  389. new Style(x => x.OfType<ThemedControl>().Class("bar"))
  390. {
  391. Setters = { new Setter(StyledElement.ThemeProperty, theme2) }
  392. },
  393. }
  394. };
  395. root.Child = target;
  396. root.LayoutManager.ExecuteInitialLayoutPass();
  397. Assert.Same(theme1, target.Theme);
  398. Assert.NotNull(target.Template);
  399. target.Classes.Add("bar");
  400. Assert.Same(theme2, target.Theme);
  401. Assert.Null(target.Template);
  402. }
  403. [Fact]
  404. public void Theme_Can_Be_Set_To_LocalValue_While_Updating_Due_To_Style_Class()
  405. {
  406. var target = CreateTarget();
  407. var theme1 = CreateTheme();
  408. var theme2 = new ControlTheme(typeof(ThemedControl));
  409. var theme3 = new ControlTheme(typeof(ThemedControl));
  410. var root = new TestRoot()
  411. {
  412. Styles =
  413. {
  414. new Style(x => x.OfType<ThemedControl>())
  415. {
  416. Setters = { new Setter(StyledElement.ThemeProperty, theme1) }
  417. },
  418. new Style(x => x.OfType<ThemedControl>().Class("bar"))
  419. {
  420. Setters = { new Setter(StyledElement.ThemeProperty, theme2) }
  421. },
  422. }
  423. };
  424. root.Child = target;
  425. root.LayoutManager.ExecuteInitialLayoutPass();
  426. Assert.Same(theme1, target.Theme);
  427. Assert.NotNull(target.Template);
  428. target.Classes.Add("bar");
  429. // At this point, theme2 has been promoted to a local value internally in StyledElement;
  430. // make sure that setting a new local value here doesn't cause it to be cleared when we
  431. // do a layout pass because StyledElement thinks its clearing the promoted theme.
  432. target.Theme = theme3;
  433. root.LayoutManager.ExecuteLayoutPass();
  434. Assert.Same(target.Theme, theme3);
  435. }
  436. [Fact]
  437. public void TemplatedParent_Theme_Change_Applies_To_Children()
  438. {
  439. var theme = CreateDerivedTheme();
  440. var target = CreateTarget();
  441. Assert.Null(target.Theme);
  442. Assert.Null(target.Template);
  443. var root = CreateRoot(target, theme.BasedOn);
  444. Assert.NotNull(target.Theme);
  445. Assert.NotNull(target.Template);
  446. root.Styles.Add(new Style(x => x.OfType<ThemedControl>().Class("foo"))
  447. {
  448. Setters = { new Setter(StyledElement.ThemeProperty, theme) }
  449. });
  450. root.LayoutManager.ExecuteLayoutPass();
  451. var border = Assert.IsType<Border>(target.VisualChild);
  452. Assert.Equal(Brushes.Red, border.Background);
  453. target.Classes.Add("foo");
  454. root.LayoutManager.ExecuteLayoutPass();
  455. Assert.Equal(Brushes.Green, border.Background);
  456. }
  457. [Fact]
  458. public void TemplatedParent_Theme_Change_Applies_Recursively_To_VisualChildren()
  459. {
  460. var theme = CreateDerivedTheme();
  461. var target = CreateTarget();
  462. Assert.Null(target.Theme);
  463. Assert.Null(target.Template);
  464. var root = CreateRoot(target, theme.BasedOn);
  465. Assert.NotNull(target.Theme);
  466. Assert.NotNull(target.Template);
  467. root.Styles.Add(new Style(x => x.OfType<ThemedControl>().Class("foo"))
  468. {
  469. Setters = { new Setter(StyledElement.ThemeProperty, theme) }
  470. });
  471. root.LayoutManager.ExecuteLayoutPass();
  472. var border = Assert.IsType<Border>(target.VisualChild);
  473. var inner = Assert.IsType<Border>(border.Child);
  474. Assert.Equal(Brushes.Red, border.Background);
  475. Assert.Equal(Brushes.Red, inner.Background);
  476. Assert.Equal(null, inner.BorderBrush);
  477. Assert.Equal(null, inner.BorderBrush);
  478. target.Classes.Add("foo");
  479. root.LayoutManager.ExecuteLayoutPass();
  480. Assert.Equal(Brushes.Green, border.Background);
  481. Assert.Equal(Brushes.Green, inner.Background);
  482. Assert.Equal(Brushes.Cyan, inner.BorderBrush);
  483. Assert.Equal(Brushes.Cyan, inner.BorderBrush);
  484. }
  485. private static ThemedControl CreateTarget()
  486. {
  487. return new ThemedControl();
  488. }
  489. private static TestRoot CreateRoot(Control child, ControlTheme? theme = null)
  490. {
  491. var result = new TestRoot()
  492. {
  493. Styles =
  494. {
  495. new Style(x => x.OfType<ThemedControl>())
  496. {
  497. Setters = { new Setter(StyledElement.ThemeProperty, theme ?? CreateTheme()) }
  498. }
  499. }
  500. };
  501. result.Child = child;
  502. result.LayoutManager.ExecuteInitialLayoutPass();
  503. return result;
  504. }
  505. }
  506. private static ControlTheme CreateTheme(string tag = "theme")
  507. {
  508. var template = new FuncControlTemplate<ThemedControl>(
  509. (o, n) => new Border() { Child = new Border() });
  510. return new ControlTheme
  511. {
  512. TargetType = typeof(ThemedControl),
  513. Setters =
  514. {
  515. new Setter(Control.TagProperty, tag),
  516. new Setter(TemplatedControl.TemplateProperty, template),
  517. new Setter(TemplatedControl.CornerRadiusProperty, new CornerRadius(5)),
  518. },
  519. Children =
  520. {
  521. new Style(x => x.Nesting().Template().OfType<Border>())
  522. {
  523. Setters =
  524. {
  525. new Setter(Border.BackgroundProperty, Brushes.Red),
  526. new Setter(Control.TagProperty, tag),
  527. }
  528. },
  529. new Style(x => x.Nesting().Class("foo").Template().OfType<Border>())
  530. {
  531. Setters = { new Setter(Border.BackgroundProperty, Brushes.Green) }
  532. },
  533. }
  534. };
  535. }
  536. private static ControlTheme CreateDerivedTheme()
  537. {
  538. return new ControlTheme
  539. {
  540. TargetType = typeof(ThemedControl),
  541. BasedOn = CreateTheme(),
  542. Setters =
  543. {
  544. new Setter(Border.BorderBrushProperty, Brushes.Blue),
  545. },
  546. Children =
  547. {
  548. new Style(x => x.Nesting().Template().OfType<Border>())
  549. {
  550. Setters = { new Setter(Border.BorderBrushProperty, Brushes.Yellow) }
  551. },
  552. new Style(x => x.Nesting().Class("foo").Template().OfType<Border>())
  553. {
  554. Setters = { new Setter(Border.BorderBrushProperty, Brushes.Cyan) }
  555. },
  556. }
  557. };
  558. }
  559. private class ThemedControl : Controls.Primitives.TemplatedControl
  560. {
  561. public Visual? VisualChild => VisualChildren?.SingleOrDefault();
  562. }
  563. private class ThemedControl2 : Controls.Primitives.TemplatedControl
  564. {
  565. public Visual? VisualChild => VisualChildren?.SingleOrDefault();
  566. }
  567. private class DerivedThemedControl : ThemedControl
  568. {
  569. }
  570. }