From ad5bea642909bc99ef42e7ddbfc0a2a930b7911d Mon Sep 17 00:00:00 2001 From: chinboy Date: Mon, 29 Jun 2026 17:28:59 +0800 Subject: [PATCH] feat(BorderBeam, Splash): update layout and introduce version tags in showcase components --- .../LanguageProviderPool.g.cs | 9 +- .../LanguageResourceConst.g.cs | 3 +- .../Other/BorderBeam/Localization/en_US.cs | 2 +- .../Other/BorderBeam/Localization/zh_CN.cs | 2 +- .../Other/BorderBeam/Localization/zh_TW.cs | 2 +- .../BorderBeam/Views/BorderBeamShowCase.axaml | 29 +- .../Other/Splash/Localization/en_US.cs | 1 + .../Other/Splash/Localization/zh_CN.cs | 1 + .../Other/Splash/Localization/zh_TW.cs | 1 + .../Other/Splash/Views/SplashShowCase.axaml | 32 +- .../Controls/GalleryCodeViewer.cs | 486 +++++++++++++++++- .../GalleryCodeViewerRuntimeTests.cs | 201 +++++++- .../GallerySourceCodeDisplayLayoutTests.cs | 28 + .../ShowCases/BorderBeamShowCasePageTests.cs | 41 +- .../ShowCases/SplashShowCasePageTests.cs | 39 ++ 15 files changed, 836 insertions(+), 41 deletions(-) diff --git a/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageProviderPool.g.cs b/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageProviderPool.g.cs index 209cfc293..8a3dcb361 100644 --- a/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageProviderPool.g.cs +++ b/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageProviderPool.g.cs @@ -1233,7 +1233,7 @@ namespace AtomUI.Theme.Language dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.BasicDescription] = global::AtomUIGallery.ShowCases.BorderBeam.en_US.BasicDescription; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.BasicTitle] = global::AtomUIGallery.ShowCases.BorderBeam.en_US.BasicTitle; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.ComponentCategory] = global::AtomUIGallery.ShowCases.BorderBeam.en_US.ComponentCategory; - dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.ComponentStatusPreview] = global::AtomUIGallery.ShowCases.BorderBeam.en_US.ComponentStatusPreview; + dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.ComponentIntroducedVersion] = global::AtomUIGallery.ShowCases.BorderBeam.en_US.ComponentIntroducedVersion; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.CustomizedColorCardDescription] = global::AtomUIGallery.ShowCases.BorderBeam.en_US.CustomizedColorCardDescription; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.CustomizedColorDescription] = global::AtomUIGallery.ShowCases.BorderBeam.en_US.CustomizedColorDescription; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.CustomizedColorTitle] = global::AtomUIGallery.ShowCases.BorderBeam.en_US.CustomizedColorTitle; @@ -1299,7 +1299,7 @@ namespace AtomUI.Theme.Language dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.BasicDescription] = global::AtomUIGallery.ShowCases.BorderBeam.zh_CN.BasicDescription; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.BasicTitle] = global::AtomUIGallery.ShowCases.BorderBeam.zh_CN.BasicTitle; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.ComponentCategory] = global::AtomUIGallery.ShowCases.BorderBeam.zh_CN.ComponentCategory; - dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.ComponentStatusPreview] = global::AtomUIGallery.ShowCases.BorderBeam.zh_CN.ComponentStatusPreview; + dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.ComponentIntroducedVersion] = global::AtomUIGallery.ShowCases.BorderBeam.zh_CN.ComponentIntroducedVersion; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.CustomizedColorCardDescription] = global::AtomUIGallery.ShowCases.BorderBeam.zh_CN.CustomizedColorCardDescription; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.CustomizedColorDescription] = global::AtomUIGallery.ShowCases.BorderBeam.zh_CN.CustomizedColorDescription; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.CustomizedColorTitle] = global::AtomUIGallery.ShowCases.BorderBeam.zh_CN.CustomizedColorTitle; @@ -1365,7 +1365,7 @@ namespace AtomUI.Theme.Language dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.BasicDescription] = global::AtomUIGallery.ShowCases.BorderBeam.zh_TW.BasicDescription; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.BasicTitle] = global::AtomUIGallery.ShowCases.BorderBeam.zh_TW.BasicTitle; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.ComponentCategory] = global::AtomUIGallery.ShowCases.BorderBeam.zh_TW.ComponentCategory; - dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.ComponentStatusPreview] = global::AtomUIGallery.ShowCases.BorderBeam.zh_TW.ComponentStatusPreview; + dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.ComponentIntroducedVersion] = global::AtomUIGallery.ShowCases.BorderBeam.zh_TW.ComponentIntroducedVersion; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.CustomizedColorCardDescription] = global::AtomUIGallery.ShowCases.BorderBeam.zh_TW.CustomizedColorCardDescription; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.CustomizedColorDescription] = global::AtomUIGallery.ShowCases.BorderBeam.zh_TW.CustomizedColorDescription; dictionary[global::AtomUIGallery.Localization.BorderBeamShowCaseLangResourceKind.CustomizedColorTitle] = global::AtomUIGallery.ShowCases.BorderBeam.zh_TW.CustomizedColorTitle; @@ -18568,6 +18568,7 @@ namespace AtomUI.Theme.Language dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.BasicDescription] = global::AtomUIGallery.ShowCases.Splash.en_US.BasicDescription; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.BasicTitle] = global::AtomUIGallery.ShowCases.Splash.en_US.BasicTitle; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComponentCategory] = global::AtomUIGallery.ShowCases.Splash.en_US.ComponentCategory; + dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComponentIntroducedVersion] = global::AtomUIGallery.ShowCases.Splash.en_US.ComponentIntroducedVersion; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComponentStatusPreview] = global::AtomUIGallery.ShowCases.Splash.en_US.ComponentStatusPreview; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComposedDescription] = global::AtomUIGallery.ShowCases.Splash.en_US.ComposedDescription; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComposedTitle] = global::AtomUIGallery.ShowCases.Splash.en_US.ComposedTitle; @@ -18681,6 +18682,7 @@ namespace AtomUI.Theme.Language dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.BasicDescription] = global::AtomUIGallery.ShowCases.Splash.zh_CN.BasicDescription; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.BasicTitle] = global::AtomUIGallery.ShowCases.Splash.zh_CN.BasicTitle; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComponentCategory] = global::AtomUIGallery.ShowCases.Splash.zh_CN.ComponentCategory; + dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComponentIntroducedVersion] = global::AtomUIGallery.ShowCases.Splash.zh_CN.ComponentIntroducedVersion; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComponentStatusPreview] = global::AtomUIGallery.ShowCases.Splash.zh_CN.ComponentStatusPreview; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComposedDescription] = global::AtomUIGallery.ShowCases.Splash.zh_CN.ComposedDescription; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComposedTitle] = global::AtomUIGallery.ShowCases.Splash.zh_CN.ComposedTitle; @@ -18794,6 +18796,7 @@ namespace AtomUI.Theme.Language dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.BasicDescription] = global::AtomUIGallery.ShowCases.Splash.zh_TW.BasicDescription; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.BasicTitle] = global::AtomUIGallery.ShowCases.Splash.zh_TW.BasicTitle; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComponentCategory] = global::AtomUIGallery.ShowCases.Splash.zh_TW.ComponentCategory; + dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComponentIntroducedVersion] = global::AtomUIGallery.ShowCases.Splash.zh_TW.ComponentIntroducedVersion; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComponentStatusPreview] = global::AtomUIGallery.ShowCases.Splash.zh_TW.ComponentStatusPreview; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComposedDescription] = global::AtomUIGallery.ShowCases.Splash.zh_TW.ComposedDescription; dictionary[global::AtomUIGallery.Localization.SplashShowCaseLangResourceKind.ComposedTitle] = global::AtomUIGallery.ShowCases.Splash.zh_TW.ComposedTitle; diff --git a/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageResourceConst.g.cs b/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageResourceConst.g.cs index f1ff8ea54..7321286dd 100644 --- a/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageResourceConst.g.cs +++ b/controlgallery/AtomUIGallery/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.LanguageGenerator/LanguageResourceConst.g.cs @@ -387,7 +387,7 @@ namespace AtomUIGallery.Localization BasicDescription, BasicTitle, ComponentCategory, - ComponentStatusPreview, + ComponentIntroducedVersion, CustomizedColorCardDescription, CustomizedColorDescription, CustomizedColorTitle, @@ -5841,6 +5841,7 @@ namespace AtomUIGallery.Localization BasicDescription, BasicTitle, ComponentCategory, + ComponentIntroducedVersion, ComponentStatusPreview, ComposedDescription, ComposedTitle, diff --git a/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/en_US.cs b/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/en_US.cs index 5847f2311..efd0eeadf 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/en_US.cs +++ b/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/en_US.cs @@ -10,7 +10,7 @@ internal partial class en_US : LanguageProvider public const string ScenarioApi = "API"; public const string ScenarioDesignToken = "Design Token"; public const string ComponentCategory = "Other"; - public const string ComponentStatusPreview = "v6.0.5"; + public const string ComponentIntroducedVersion = "v6.0.5"; public const string PageSubtitle = "Draw an animated highlight along a container boundary."; public const string PageDescription = "BorderBeam wraps a content control and renders a non-interactive beam on its border. It is decorative, follows theme motion settings, and can use a solid color or gradient stops."; public const string InfoNamespaceLabel = "Namespace:"; diff --git a/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/zh_CN.cs b/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/zh_CN.cs index a3868eec9..3cf45b54b 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/zh_CN.cs +++ b/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/zh_CN.cs @@ -10,7 +10,7 @@ internal partial class zh_CN : LanguageProvider public const string ScenarioApi = "API"; public const string ScenarioDesignToken = "设计变量"; public const string ComponentCategory = "其他"; - public const string ComponentStatusPreview = "v6.0.5"; + public const string ComponentIntroducedVersion = "v6.0.5"; public const string PageSubtitle = "沿容器边界绘制动态高光。"; public const string PageDescription = "BorderBeam 包裹一个内容控件,并在其边界上渲染不参与交互的流光层。它只承担装饰性强调,遵守主题动效设置,并支持单色或渐变停靠点。"; public const string InfoNamespaceLabel = "命名空间:"; diff --git a/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/zh_TW.cs b/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/zh_TW.cs index c531878c4..a3b1a597e 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/zh_TW.cs +++ b/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/zh_TW.cs @@ -10,7 +10,7 @@ internal partial class zh_TW : LanguageProvider public const string ScenarioApi = "API"; public const string ScenarioDesignToken = "設計變數"; public const string ComponentCategory = "其他"; - public const string ComponentStatusPreview = "v6.0.5"; + public const string ComponentIntroducedVersion = "v6.0.5"; public const string PageSubtitle = "沿容器邊界繪製動態高光。"; public const string PageDescription = "BorderBeam 包裹一個內容控制項,並在其邊界上渲染不參與互動的流光層。它只承擔裝飾性強調,遵守主題動效設定,並支援單色或漸變停靠點。"; public const string InfoNamespaceLabel = "命名空間:"; diff --git a/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Views/BorderBeamShowCase.axaml b/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Views/BorderBeamShowCase.axaml index 04bc2091c..536e285b6 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Views/BorderBeamShowCase.axaml +++ b/controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Views/BorderBeamShowCase.axaml @@ -11,20 +11,27 @@ - - + - - - + + + + + diff --git a/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/en_US.cs b/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/en_US.cs index 2d6ca171b..3b434565d 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/en_US.cs +++ b/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/en_US.cs @@ -11,6 +11,7 @@ internal partial class en_US : LanguageProvider public const string ScenarioDesignToken = "Design Token"; public const string ComponentCategory = "Other"; public const string ComponentStatusPreview = "Preview"; + public const string ComponentIntroducedVersion = "v6.0.7"; public const string PageSubtitle = "Present desktop startup progress before the main workspace is ready."; public const string PageDescription = "Splash combines a compact branded surface, loading indicator, determinate progress, status copy, and optional footer. The service API can host it in a borderless startup window, while the visual control can be previewed directly in Gallery."; public const string InfoNamespaceLabel = "Namespace:"; diff --git a/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/zh_CN.cs b/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/zh_CN.cs index 97ba80eca..95b552c40 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/zh_CN.cs +++ b/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/zh_CN.cs @@ -11,6 +11,7 @@ internal partial class zh_CN : LanguageProvider public const string ScenarioDesignToken = "Design Token"; public const string ComponentCategory = "其他"; public const string ComponentStatusPreview = "预览"; + public const string ComponentIntroducedVersion = "v6.0.7"; public const string PageSubtitle = "在主工作区准备完成前呈现桌面启动进度。"; public const string PageDescription = "Splash 组合紧凑的品牌面板、加载指示、确定进度、状态文案和可选页脚。服务 API 可将它托管在无边框启动窗口中,视觉控件也可以直接在 Gallery 中预览。"; public const string InfoNamespaceLabel = "命名空间:"; diff --git a/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/zh_TW.cs b/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/zh_TW.cs index 2818d0c67..99a5d5717 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/zh_TW.cs +++ b/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/zh_TW.cs @@ -11,6 +11,7 @@ internal partial class zh_TW : LanguageProvider public const string ScenarioDesignToken = "Design Token"; public const string ComponentCategory = "其他"; public const string ComponentStatusPreview = "預覽"; + public const string ComponentIntroducedVersion = "v6.0.7"; public const string PageSubtitle = "在主工作區準備完成前呈現桌面啟動進度。"; public const string PageDescription = "Splash 組合緊湊的品牌面板、載入指示、確定進度、狀態文案和可選頁腳。服務 API 可將它託管在無邊框啟動視窗中,視覺控件也可以直接在 Gallery 中預覽。"; public const string InfoNamespaceLabel = "命名空間:"; diff --git a/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Views/SplashShowCase.axaml b/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Views/SplashShowCase.axaml index d473c1c88..f85cfddd8 100644 --- a/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Views/SplashShowCase.axaml +++ b/controlgallery/AtomUIGallery/ShowCases/Other/Splash/Views/SplashShowCase.axaml @@ -11,20 +11,30 @@ - - + - - - + + + + + + diff --git a/src/AtomUI.Toolkits.GalleryBase/Controls/GalleryCodeViewer.cs b/src/AtomUI.Toolkits.GalleryBase/Controls/GalleryCodeViewer.cs index ceb2dbf0c..5c73a1faf 100644 --- a/src/AtomUI.Toolkits.GalleryBase/Controls/GalleryCodeViewer.cs +++ b/src/AtomUI.Toolkits.GalleryBase/Controls/GalleryCodeViewer.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Collections.ObjectModel; using AtomUI.Controls; using AtomUI.Theme; using Avalonia; @@ -8,10 +9,15 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Styling; +using Avalonia.Threading; using Avalonia.VisualTree; using AvaloniaEdit; +using AvaloniaEdit.Document; using AvaloniaEdit.TextMate; +using TextMateSharp.Model; using TextMateSharp.Grammars; +using TextMateSharp.Registry; +using TextMateSharp.Themes; using AtomUIContextMenu = AtomUI.Desktop.Controls.ContextMenu; using AtomUIMenuItem = AtomUI.Desktop.Controls.MenuItem; @@ -42,13 +48,14 @@ public sealed partial class GalleryCodeViewer : UserControl, IDisposable IsEnabled = false }; - private TextMate.Installation? _textMateInstallation; + private GalleryTextMateInstallation? _textMateInstallation; private RegistryOptions? _registryOptions; private ScrollBar? _horizontalScrollBar; private Application? _subscribedApplication; private IThemeManager? _subscribedThemeManager; private IDisposable? _themeVariantSubscription; private ThemeName? _currentSyntaxTheme; + private bool _isHorizontalScrollBarInsetUpdatePending; private bool _isUpdatingScrollBarInset; private bool _isDisposed; @@ -56,7 +63,6 @@ public sealed partial class GalleryCodeViewer : UserControl, IDisposable { InitializeComponent(); _editor = this.FindControl("PART_Editor")!; - _editor.LayoutUpdated += HandleEditorLayoutUpdated; _editor.TextArea.SelectionChanged += HandleEditorSelectionChanged; ActualThemeVariantChanged += HandleActualThemeVariantChanged; ConfigureEditorContextMenu(); @@ -107,7 +113,7 @@ public sealed partial class GalleryCodeViewer : UserControl, IDisposable _textMateInstallation = null; _registryOptions = null; ReleaseThemeVariantSubscriptions(); - _editor.LayoutUpdated -= HandleEditorLayoutUpdated; + CancelHorizontalScrollBarGutterInsetUpdate(); _editor.TextArea.SelectionChanged -= HandleEditorSelectionChanged; _editor.Document = null; _copyMenuItem.Click -= HandleCopyMenuItemClick; @@ -136,7 +142,7 @@ public sealed partial class GalleryCodeViewer : UserControl, IDisposable else if (change.Property == ShowLineNumbersProperty) { _editor.ShowLineNumbers = ShowLineNumbers; - UpdateHorizontalScrollBarGutterInset(); + RequestHorizontalScrollBarGutterInsetUpdate(); } else if (change.Property == LightSyntaxThemeProperty || change.Property == DarkSyntaxThemeProperty) @@ -150,16 +156,19 @@ public sealed partial class GalleryCodeViewer : UserControl, IDisposable base.OnAttachedToVisualTree(e); SubscribeThemeVariantChanges(); ApplySyntaxTheme(); + RequestHorizontalScrollBarGutterInsetUpdate(); } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { + CancelHorizontalScrollBarGutterInsetUpdate(); ReleaseThemeVariantSubscriptions(); base.OnDetachedFromVisualTree(e); } private void HandleEditorLayoutUpdated(object? sender, EventArgs e) { + CancelHorizontalScrollBarGutterInsetUpdate(); UpdateHorizontalScrollBarGutterInset(); } @@ -201,12 +210,33 @@ public sealed partial class GalleryCodeViewer : UserControl, IDisposable ApplySyntaxTheme(); } + private void RequestHorizontalScrollBarGutterInsetUpdate() + { + if (_isDisposed || _isHorizontalScrollBarInsetUpdatePending) + { + return; + } + + _isHorizontalScrollBarInsetUpdatePending = true; + _editor.LayoutUpdated += HandleEditorLayoutUpdated; + } + + private void CancelHorizontalScrollBarGutterInsetUpdate() + { + if (!_isHorizontalScrollBarInsetUpdatePending) + { + return; + } + + _isHorizontalScrollBarInsetUpdatePending = false; + _editor.LayoutUpdated -= HandleEditorLayoutUpdated; + } + private void UpdateHorizontalScrollBarGutterInset() { - // This runs from LayoutUpdated. Mutating the scroll bar Margin below triggers another - // layout pass, which re-enters here. Without this guard, dragging a selection past the - // right edge (which auto-scrolls and fires LayoutUpdated continuously) spins the layout - // system forever and hangs the UI thread. + // Mutating the scroll bar Margin triggers another layout pass, so this method must only + // run from the one-shot request path above. Drag selection auto-scroll produces repeated + // LayoutUpdated ticks, and a permanent handler can spin the UI thread. if (_isUpdatingScrollBarInset) { return; @@ -277,6 +307,7 @@ public sealed partial class GalleryCodeViewer : UserControl, IDisposable private void UpdateText() { _editor.Text = CodeText ?? string.Empty; + RequestHorizontalScrollBarGutterInsetUpdate(); } private void ConfigureEditorContextMenu() @@ -290,7 +321,7 @@ public sealed partial class GalleryCodeViewer : UserControl, IDisposable private void UpdateCopyMenuItemState() { - _copyMenuItem.IsEnabled = !string.IsNullOrEmpty(GetSelectedCodeText()); + _copyMenuItem.IsEnabled = !_editor.TextArea.Selection.IsEmpty; } private string GetSelectedCodeText() @@ -331,7 +362,7 @@ public sealed partial class GalleryCodeViewer : UserControl, IDisposable var syntaxTheme = ResolveSyntaxTheme(); _registryOptions = new RegistryOptions(syntaxTheme); - _textMateInstallation = _editor.InstallTextMate(_registryOptions); + _textMateInstallation = new GalleryTextMateInstallation(_editor, _registryOptions); _currentSyntaxTheme = syntaxTheme; } @@ -443,4 +474,439 @@ public sealed partial class GalleryCodeViewer : UserControl, IDisposable return _registryOptions?.GetScopeByExtension(extension); } + + private sealed class GalleryTextMateInstallation : IDisposable + { + private readonly object _lock = new(); + private readonly Registry _textMateRegistry; + private readonly TextEditor _editor; + private readonly TextMateColoringTransformer _transformer; + private readonly bool _ownsTransformer; + private Action? _exceptionHandler; + private GalleryTextEditorModel? _editorModel; + private IGrammar? _grammar; + private TMModel? _tmModel; + private ReadOnlyDictionary? _themeColorsDictionary; + private bool _isDisposed; + + public GalleryTextMateInstallation(TextEditor editor, + IRegistryOptions registryOptions, + Action? exceptionHandler = null) + { + RegistryOptions = registryOptions ?? throw new ArgumentNullException(nameof(registryOptions)); + _editor = editor ?? throw new ArgumentNullException(nameof(editor)); + _exceptionHandler = exceptionHandler; + _textMateRegistry = new Registry(registryOptions); + _transformer = _editor.TextArea.TextView.LineTransformers + .OfType() + .FirstOrDefault() ?? + new TextMateColoringTransformer(_editor.TextArea.TextView, _exceptionHandler); + + if (!_editor.TextArea.TextView.LineTransformers.Contains(_transformer)) + { + _editor.TextArea.TextView.LineTransformers.Add(_transformer); + _ownsTransformer = true; + } + + SetTheme(registryOptions.GetDefaultTheme()); + _editor.DocumentChanged += HandleEditorDocumentChanged; + HandleEditorDocumentChanged(_editor, EventArgs.Empty); + } + + public IRegistryOptions RegistryOptions { get; } + + public void SetGrammar(string scopeName) + { + ThrowIfDisposed(); + lock (_lock) + { + ThrowIfDisposed(); + SetGrammarInternal(_textMateRegistry.LoadGrammar(scopeName)); + ForceTokenizeDocument(); + } + _editor.TextArea.TextView.Redraw(); + } + + public bool TryGetThemeColor(string colorKey, out string? colorString) + { + ThrowIfDisposed(); + return (_themeColorsDictionary ?? throw new ObjectDisposedException(nameof(GalleryTextMateInstallation))) + .TryGetValue(colorKey, out colorString); + } + + public void SetTheme(IRawTheme theme) + { + ThrowIfDisposed(); + lock (_lock) + { + ThrowIfDisposed(); + _textMateRegistry.SetTheme(theme); + var textMateTheme = _textMateRegistry.GetTheme(); + _transformer.SetTheme(textMateTheme); + _tmModel?.InvalidateLine(0); + _editorModel?.InvalidateViewPortLines(); + _themeColorsDictionary = textMateTheme.GetGuiColorDictionary(); + } + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + GalleryTextEditorModel? editorModel; + TMModel? tmModel; + lock (_lock) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + editorModel = _editorModel; + _editorModel = null; + tmModel = _tmModel; + _tmModel = null; + _grammar = null; + _themeColorsDictionary = null; + _exceptionHandler = null; + } + + _editor.DocumentChanged -= HandleEditorDocumentChanged; + editorModel?.Dispose(); + DisposeTMModel(tmModel); + + if (_ownsTransformer) + { + _editor.TextArea.TextView.LineTransformers.Remove(_transformer); + _transformer.Dispose(); + } + else + { + _transformer.SetModel(null, null); + } + } + + private void HandleEditorDocumentChanged(object? sender, EventArgs e) + { + if (_isDisposed || _editor.Document is null) + { + return; + } + + lock (_lock) + { + if (_isDisposed || _editor.Document is null) + { + return; + } + + try + { + _editorModel?.Dispose(); + DisposeTMModel(_tmModel); + _editorModel = new GalleryTextEditorModel( + _editor.TextArea.TextView, + _editor.Document, + _exceptionHandler); + _tmModel = new TMModel(_editorModel); + _tmModel.SetGrammar(_grammar); + _transformer.SetModel(_editor.Document, _tmModel); + _tmModel.AddModelTokensChangedListener(_transformer); + ForceTokenizeDocument(); + } + catch (Exception ex) + { + _exceptionHandler?.Invoke(ex); + } + } + } + + private void SetGrammarInternal(IGrammar grammar) + { + _grammar = grammar; + _transformer.SetGrammar(_grammar); + } + + private void ForceTokenizeDocument() + { + _editorModel?.ForceTokenizeAllLines(); + } + + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(GalleryTextMateInstallation)); + } + } + + private void DisposeTMModel(TMModel? tmModel) + { + if (tmModel is null) + { + return; + } + + tmModel.RemoveModelTokensChangedListener(_transformer); + tmModel.Dispose(); + } + } + + private sealed class GalleryTextEditorModel : AbstractLineList, IDisposable + { + private readonly TextDocument _document; + private readonly AvaloniaEdit.Rendering.TextView _textView; + private readonly Action? _exceptionHandler; + private DocumentSnapshot _documentSnapshot; + private InvalidLineRange? _invalidRange; + private bool _isViewportTokenizationPending; + private int _pendingViewportStartLine = -1; + private int _pendingViewportEndLine = -1; + private bool _isDisposed; + + public GalleryTextEditorModel(AvaloniaEdit.Rendering.TextView textView, + TextDocument document, + Action? exceptionHandler) + { + _textView = textView; + _document = document; + _exceptionHandler = exceptionHandler; + _documentSnapshot = new DocumentSnapshot(_document); + for (var i = 0; i < _document.LineCount; i++) + { + AddLine(i); + } + + _document.Changing += HandleDocumentChanging; + _document.Changed += HandleDocumentChanged; + _document.UpdateFinished += HandleDocumentUpdateFinished; + _textView.ScrollOffsetChanged += HandleTextViewScrollOffsetChanged; + } + + public override void Dispose() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + _document.Changing -= HandleDocumentChanging; + _document.Changed -= HandleDocumentChanged; + _document.UpdateFinished -= HandleDocumentUpdateFinished; + _textView.ScrollOffsetChanged -= HandleTextViewScrollOffsetChanged; + } + + public override void UpdateLine(int lineIndex) + { + } + + public override int GetNumberOfLines() + { + return _documentSnapshot.LineCount; + } + + public override LineText GetLineTextIncludingTerminators(int lineIndex) + { + return _documentSnapshot.GetLineTextIncludingTerminatorAsMemory(lineIndex); + } + + public override int GetLineLength(int lineIndex) + { + return _documentSnapshot.GetLineLength(lineIndex); + } + + public void InvalidateViewPortLines() + { + if (_textView.VisualLinesValid && _textView.VisualLines.Count != 0) + { + InvalidateLineRange(GetFirstVisibleLineIndex(), GetLastVisibleLineIndex()); + } + } + + public void ForceTokenizeAllLines() + { + var lineCount = _documentSnapshot.LineCount; + if (lineCount > 0) + { + ForceTokenization(0, lineCount - 1); + } + } + + private void HandleTextViewScrollOffsetChanged(object? sender, EventArgs e) + { + try + { + RequestViewportTokenization(); + } + catch (Exception ex) + { + _exceptionHandler?.Invoke(ex); + } + } + + private void RequestViewportTokenization() + { + if (_isDisposed || !_textView.VisualLinesValid || _textView.VisualLines.Count == 0) + { + return; + } + + var startLine = GetFirstVisibleLineIndex(); + var endLine = GetLastVisibleLineIndex(); + if (_pendingViewportStartLine < 0) + { + _pendingViewportStartLine = startLine; + _pendingViewportEndLine = endLine; + } + else + { + _pendingViewportStartLine = Math.Min(_pendingViewportStartLine, startLine); + _pendingViewportEndLine = Math.Max(_pendingViewportEndLine, endLine); + } + + if (_isViewportTokenizationPending) + { + return; + } + + _isViewportTokenizationPending = true; + Avalonia.Threading.Dispatcher.UIThread.Post( + ProcessPendingViewportTokenization, + DispatcherPriority.Background); + } + + private void ProcessPendingViewportTokenization() + { + if (_isDisposed) + { + return; + } + + var startLine = _pendingViewportStartLine; + var endLine = _pendingViewportEndLine; + _pendingViewportStartLine = -1; + _pendingViewportEndLine = -1; + _isViewportTokenizationPending = false; + if (startLine >= 0 && endLine >= startLine) + { + ForceTokenization(startLine, endLine); + } + } + + private void HandleDocumentChanging(object? sender, DocumentChangeEventArgs e) + { + try + { + if (e.RemovalLength <= 0) + { + return; + } + + var startLine = _document.GetLineByOffset(e.Offset).LineNumber - 1; + var endLine = _document.GetLineByOffset(e.Offset + e.RemovalLength).LineNumber - 1; + for (var line = endLine; line > startLine; line--) + { + RemoveLine(line); + } + _documentSnapshot.RemoveLines(startLine, endLine); + } + catch (Exception ex) + { + _exceptionHandler?.Invoke(ex); + } + } + + private void HandleDocumentChanged(object? sender, DocumentChangeEventArgs e) + { + try + { + var startLine = _document.GetLineByOffset(e.Offset).LineNumber - 1; + var endLine = startLine; + if (e.InsertionLength > 0) + { + endLine = _document.GetLineByOffset(e.Offset + e.InsertionLength).LineNumber - 1; + for (var line = startLine; line < endLine; line++) + { + AddLine(line); + } + } + + _documentSnapshot.Update(e); + SetInvalidRange(startLine == 0 ? startLine : startLine - 1, endLine); + } + catch (Exception ex) + { + _exceptionHandler?.Invoke(ex); + } + } + + private void SetInvalidRange(int startLine, int endLine) + { + if (!_document.IsInUpdate) + { + InvalidateLineRange(startLine, endLine); + } + else if (_invalidRange is null) + { + _invalidRange = new InvalidLineRange(startLine, endLine); + } + else + { + _invalidRange.SetInvalidRange(startLine, endLine); + } + } + + private void HandleDocumentUpdateFinished(object? sender, EventArgs e) + { + if (_invalidRange is null) + { + return; + } + + try + { + var startLine = Math.Clamp(_invalidRange.StartLine, 0, _documentSnapshot.LineCount - 1); + var endLine = Math.Clamp(_invalidRange.EndLine, 0, _documentSnapshot.LineCount - 1); + InvalidateLineRange(startLine, endLine); + } + finally + { + _invalidRange = null; + } + } + + private int GetFirstVisibleLineIndex() + { + return _textView.VisualLines[0].FirstDocumentLine.LineNumber - 1; + } + + private int GetLastVisibleLineIndex() + { + return _textView.VisualLines[^1].LastDocumentLine.LineNumber - 1; + } + + private sealed class InvalidLineRange + { + public InvalidLineRange(int startLine, int endLine) + { + StartLine = startLine; + EndLine = endLine; + } + + public int StartLine { get; private set; } + + public int EndLine { get; private set; } + + public void SetInvalidRange(int startLine, int endLine) + { + StartLine = Math.Min(StartLine, startLine); + EndLine = Math.Max(EndLine, endLine); + } + } + } } diff --git a/tests/AtomUI.Toolkits.GalleryBase.Tests/SourceCode/GalleryCodeViewerRuntimeTests.cs b/tests/AtomUI.Toolkits.GalleryBase.Tests/SourceCode/GalleryCodeViewerRuntimeTests.cs index e3f95c284..b45ec7a25 100644 --- a/tests/AtomUI.Toolkits.GalleryBase.Tests/SourceCode/GalleryCodeViewerRuntimeTests.cs +++ b/tests/AtomUI.Toolkits.GalleryBase.Tests/SourceCode/GalleryCodeViewerRuntimeTests.cs @@ -8,6 +8,7 @@ using AtomUI.Toolkits.GalleryBase.SourceCode; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; @@ -16,6 +17,7 @@ using Avalonia.Threading; using Avalonia.VisualTree; using AvaloniaEdit; using AvaloniaEdit.Editing; +using AvaloniaEdit.TextMate; using Shouldly; using TextMateSharp.Registry; using TextMateSharp.Grammars; @@ -133,6 +135,7 @@ public class GalleryCodeViewerRuntimeTests var horizontalScrollBar = editor.GetVisualDescendants() .OfType() .Single(scrollBar => scrollBar.Orientation == Orientation.Horizontal); + var scrollViewer = GetEditorScrollViewer(editor); // Drive the editor through a range of horizontal scroll offsets, forcing a layout // pass at each step. This reproduces the auto-scroll-while-selecting feedback loop: @@ -140,7 +143,7 @@ public class GalleryCodeViewerRuntimeTests // previously re-triggered layout forever and hung the UI thread). double MeasureInsetAfterScroll(double offset) { - editor.ScrollToHorizontalOffset(offset); + scrollViewer.Offset = new Vector(offset, scrollViewer.Offset.Y); editor.InvalidateMeasure(); editor.InvalidateArrange(); Dispatcher.UIThread.RunJobs(); @@ -164,6 +167,192 @@ public class GalleryCodeViewerRuntimeTests }); } + [Fact] + public void GalleryCodeViewer_Selection_Drag_AutoScroll_Does_Not_Hang() + { + var viewer = new GalleryCodeViewer + { + CodeText = string.Join( + "\n", + Enumerable.Range(1, 40).Select(index => $"{index}: {new string('x', 260)}")), + Language = "csharp" + }; + var window = new Window + { + Width = 640, + Height = 480, + Content = viewer + }; + + try + { + window.Show(); + Dispatcher.UIThread.RunJobs(); + viewer.ApplyTemplate(); + + var editor = viewer.GetVisualDescendants() + .OfType() + .Single(); + editor.ApplyTemplate(); + Dispatcher.UIThread.RunJobs(); + + var textView = editor.TextArea.TextView; + var startPoint = textView.TranslatePoint(new Point(80, 22), window).ShouldNotBeNull(); + var dragPoint = textView.TranslatePoint(new Point(textView.Bounds.Width + 160, 82), window) + .ShouldNotBeNull(); + + window.MouseMove(startPoint); + window.MouseDown(startPoint, MouseButton.Left); + window.MouseMove(dragPoint); + + for (var i = 0; i < 8; i++) + { + Dispatcher.UIThread.RunJobs(); + } + + window.MouseUp(dragPoint, MouseButton.Left); + Dispatcher.UIThread.RunJobs(); + + editor.HorizontalOffset.ShouldBeGreaterThan(0); + editor.TextArea.Selection.IsEmpty.ShouldBeFalse(); + editor.TextArea.TextView.LineTransformers + .OfType() + .ShouldNotBeEmpty(); + } + finally + { + window.Close(); + Dispatcher.UIThread.RunJobs(); + viewer.Dispose(); + } + } + + [Fact] + public void GalleryCodeViewer_Keeps_TextMate_When_Click_Does_Not_Start_Selection_Drag() + { + var viewer = new GalleryCodeViewer + { + CodeText = string.Join( + "\n", + Enumerable.Repeat( + " ", + 40)), + Language = "axaml" + }; + var window = new Window + { + Width = 640, + Height = 480, + Content = viewer + }; + + try + { + window.Show(); + Dispatcher.UIThread.RunJobs(); + viewer.ApplyTemplate(); + + var editor = viewer.GetVisualDescendants() + .OfType() + .Single(); + editor.ApplyTemplate(); + Dispatcher.UIThread.RunJobs(); + + editor.TextArea.TextView.LineTransformers + .OfType() + .ShouldNotBeEmpty(); + + var startPoint = editor.TextArea.TextView.TranslatePoint(new Point(80, 22), window) + .ShouldNotBeNull(); + + window.MouseMove(startPoint); + window.MouseDown(startPoint, MouseButton.Left); + Dispatcher.UIThread.RunJobs(); + + editor.TextArea.TextView.LineTransformers + .OfType() + .ShouldNotBeEmpty(); + + window.MouseUp(startPoint, MouseButton.Left); + Dispatcher.UIThread.RunJobs(); + + editor.TextArea.TextView.LineTransformers + .OfType() + .ShouldNotBeEmpty(); + } + finally + { + window.Close(); + Dispatcher.UIThread.RunJobs(); + viewer.Dispose(); + } + } + + [Fact] + public void GalleryCodeViewer_Keeps_TextMate_During_Pointer_Selection_Drag() + { + var viewer = new GalleryCodeViewer + { + CodeText = string.Join( + "\n", + Enumerable.Repeat( + " ", + 40)), + Language = "axaml" + }; + var window = new Window + { + Width = 640, + Height = 480, + Content = viewer + }; + + try + { + window.Show(); + Dispatcher.UIThread.RunJobs(); + viewer.ApplyTemplate(); + + var editor = viewer.GetVisualDescendants() + .OfType() + .Single(); + editor.ApplyTemplate(); + Dispatcher.UIThread.RunJobs(); + + editor.TextArea.TextView.LineTransformers + .OfType() + .ShouldNotBeEmpty(); + + var textView = editor.TextArea.TextView; + var startPoint = textView.TranslatePoint(new Point(80, 22), window) + .ShouldNotBeNull(); + var dragPoint = textView.TranslatePoint(new Point(180, 52), window) + .ShouldNotBeNull(); + + window.MouseMove(startPoint); + window.MouseDown(startPoint, MouseButton.Left); + window.MouseMove(dragPoint); + Dispatcher.UIThread.RunJobs(); + + editor.TextArea.TextView.LineTransformers + .OfType() + .ShouldNotBeEmpty(); + + window.MouseUp(dragPoint, MouseButton.Left); + Dispatcher.UIThread.RunJobs(); + + editor.TextArea.TextView.LineTransformers + .OfType() + .ShouldNotBeEmpty(); + } + finally + { + window.Close(); + Dispatcher.UIThread.RunJobs(); + viewer.Dispose(); + } + } + [Fact] public void GalleryCodeViewer_Uses_Dark_Syntax_Theme_When_AtomUI_Dark_Mode_Is_Enabled() { @@ -607,6 +796,16 @@ public class GalleryCodeViewerRuntimeTests return syntaxThemeField.GetValue(viewer).ShouldBeOfType(); } + private static Avalonia.Controls.ScrollViewer GetEditorScrollViewer(TextEditor editor) + { + var scrollViewerProperty = typeof(TextEditor).GetProperty( + "ScrollViewer", + BindingFlags.Instance | BindingFlags.NonPublic); + scrollViewerProperty.ShouldNotBeNull(); + + return scrollViewerProperty.GetValue(editor).ShouldBeOfType(); + } + private static string? GetAxamlTagNameColor(GalleryCodeViewer viewer) { return GetAxamlTokenColor(viewer, "StackPanel"); diff --git a/tests/AtomUI.Toolkits.GalleryBase.Tests/SourceCode/GallerySourceCodeDisplayLayoutTests.cs b/tests/AtomUI.Toolkits.GalleryBase.Tests/SourceCode/GallerySourceCodeDisplayLayoutTests.cs index e03888465..56ffeafa7 100644 --- a/tests/AtomUI.Toolkits.GalleryBase.Tests/SourceCode/GallerySourceCodeDisplayLayoutTests.cs +++ b/tests/AtomUI.Toolkits.GalleryBase.Tests/SourceCode/GallerySourceCodeDisplayLayoutTests.cs @@ -54,6 +54,34 @@ public class GallerySourceCodeDisplayLayoutTests viewerMarkup.ShouldContain("x:Key=\"ThemeForegroundColor\""); } + [Fact] + public void Code_Viewer_Does_Not_Keep_A_Permanent_LayoutUpdated_Scrollbar_Adjuster() + { + var viewerSource = File.ReadAllText(GetRepoFile("src/AtomUI.Toolkits.GalleryBase/Controls/GalleryCodeViewer.cs")); + + var requestMethodIndex = viewerSource.IndexOf( + "private void RequestHorizontalScrollBarGutterInsetUpdate()", + StringComparison.Ordinal); + var subscriptionIndex = viewerSource.IndexOf( + "_editor.LayoutUpdated += HandleEditorLayoutUpdated", + StringComparison.Ordinal); + var handlerIndex = viewerSource.IndexOf( + "private void HandleEditorLayoutUpdated", + StringComparison.Ordinal); + var cancelInHandlerIndex = viewerSource.IndexOf( + "CancelHorizontalScrollBarGutterInsetUpdate();", + handlerIndex, + StringComparison.Ordinal); + var updateInHandlerIndex = viewerSource.IndexOf( + "UpdateHorizontalScrollBarGutterInset();", + handlerIndex, + StringComparison.Ordinal); + + requestMethodIndex.ShouldBeGreaterThanOrEqualTo(0); + subscriptionIndex.ShouldBeGreaterThan(requestMethodIndex); + cancelInHandlerIndex.ShouldBeLessThan(updateInHandlerIndex); + } + private static string GetRepoFile(string relativePath) { var directory = new DirectoryInfo(AppContext.BaseDirectory); diff --git a/tests/AtomUIGallery.Tests/ShowCases/BorderBeamShowCasePageTests.cs b/tests/AtomUIGallery.Tests/ShowCases/BorderBeamShowCasePageTests.cs index bb5671f98..1274997db 100644 --- a/tests/AtomUIGallery.Tests/ShowCases/BorderBeamShowCasePageTests.cs +++ b/tests/AtomUIGallery.Tests/ShowCases/BorderBeamShowCasePageTests.cs @@ -50,7 +50,7 @@ public class BorderBeamShowCasePageTests source.ShouldContain("BorderBeamShowCaseLangResource PageSubtitle"); source.ShouldContain("BorderBeamShowCaseLangResource PageDescription"); source.ShouldContain("BorderBeamShowCaseLangResource ComponentCategory"); - source.ShouldContain("BorderBeamShowCaseLangResource ComponentStatusPreview"); + source.ShouldContain("BorderBeamShowCaseLangResource ComponentIntroducedVersion"); source.ShouldContain("BorderBeamShowCaseLangResource ScenarioExamples"); source.ShouldContain("BorderBeamShowCaseLangResource ScenarioApi"); source.ShouldContain("BorderBeamShowCaseLangResource ScenarioDesignToken"); @@ -68,6 +68,30 @@ public class BorderBeamShowCasePageTests source.ShouldNotContain(" Walk(IEnumerable nodes) { foreach (var node in nodes) diff --git a/tests/AtomUIGallery.Tests/ShowCases/SplashShowCasePageTests.cs b/tests/AtomUIGallery.Tests/ShowCases/SplashShowCasePageTests.cs index d6fd97d7d..ed8137c35 100644 --- a/tests/AtomUIGallery.Tests/ShowCases/SplashShowCasePageTests.cs +++ b/tests/AtomUIGallery.Tests/ShowCases/SplashShowCasePageTests.cs @@ -50,6 +50,7 @@ public class SplashShowCasePageTests source.ShouldContain("SplashShowCaseLangResource PageDescription"); source.ShouldContain("SplashShowCaseLangResource ComponentCategory"); source.ShouldContain("SplashShowCaseLangResource ComponentStatusPreview"); + source.ShouldContain("SplashShowCaseLangResource ComponentIntroducedVersion"); source.ShouldContain("SplashShowCaseLangResource ScenarioExamples"); source.ShouldContain("SplashShowCaseLangResource ScenarioApi"); source.ShouldContain("SplashShowCaseLangResource ScenarioDesignToken"); @@ -67,6 +68,29 @@ public class SplashShowCasePageTests source.ShouldNotContain(" Walk(IEnumerable nodes) { foreach (var node in nodes)