feat(BorderBeam, Splash): update layout and introduce version tags in showcase components

This commit is contained in:
chinboy
2026-06-29 17:28:59 +08:00
parent f61cf166d4
commit ad5bea6429
15 changed files with 836 additions and 41 deletions
@@ -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;
@@ -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,
@@ -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:";
@@ -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 = "命名空间:";
@@ -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 = "命名空間:";
@@ -11,20 +11,27 @@
<StackPanel Margin="28,24,28,18"
Spacing="16">
<StackPanel Spacing="6">
<StackPanel Orientation="Horizontal"
Spacing="10"
VerticalAlignment="Center">
<atom:TextBlock Text="BorderBeam"
<Grid ColumnDefinitions="Auto,*"
VerticalAlignment="Center">
<atom:TextBlock Grid.Column="0"
Text="BorderBeam"
FontSize="26"
FontWeight="Bold"
VerticalAlignment="Center" />
<atom:Tag Text="{gallery:BorderBeamShowCaseLangResource ComponentCategory}"
TagColor="geekblue"
VerticalAlignment="Center" />
<atom:Tag Text="{gallery:BorderBeamShowCaseLangResource ComponentStatusPreview}"
TagColor="processing"
VerticalAlignment="Center" />
</StackPanel>
<WrapPanel Grid.Column="1"
Margin="10,0,0,0"
ItemSpacing="10"
LineSpacing="6"
VerticalAlignment="Center">
<atom:Tag Text="{gallery:BorderBeamShowCaseLangResource ComponentCategory}"
TagColor="blue"
VerticalAlignment="Center" />
<atom:Tag Text="{gallery:BorderBeamShowCaseLangResource ComponentIntroducedVersion}"
TagColor="blue"
IsBordered="False"
VerticalAlignment="Center" />
</WrapPanel>
</Grid>
<atom:TextBlock Text="{gallery:BorderBeamShowCaseLangResource PageSubtitle}"
FontSize="15"
Foreground="{atom:SharedTokenResource ColorTextSecondary}" />
@@ -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:";
@@ -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 = "命名空间:";
@@ -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 = "命名空間:";
@@ -11,20 +11,30 @@
<StackPanel Margin="28,24,28,18"
Spacing="16">
<StackPanel Spacing="6">
<StackPanel Orientation="Horizontal"
Spacing="10"
VerticalAlignment="Center">
<atom:TextBlock Text="Splash"
<Grid ColumnDefinitions="Auto,*"
VerticalAlignment="Center">
<atom:TextBlock Grid.Column="0"
Text="Splash"
FontSize="26"
FontWeight="Bold"
VerticalAlignment="Center" />
<atom:Tag Text="{gallery:SplashShowCaseLangResource ComponentCategory}"
TagColor="geekblue"
VerticalAlignment="Center" />
<atom:Tag Text="{gallery:SplashShowCaseLangResource ComponentStatusPreview}"
TagColor="processing"
VerticalAlignment="Center" />
</StackPanel>
<WrapPanel Grid.Column="1"
Margin="10,0,0,0"
ItemSpacing="10"
LineSpacing="6"
VerticalAlignment="Center">
<atom:Tag Text="{gallery:SplashShowCaseLangResource ComponentCategory}"
TagColor="blue"
VerticalAlignment="Center" />
<atom:Tag Text="{gallery:SplashShowCaseLangResource ComponentStatusPreview}"
TagColor="processing"
VerticalAlignment="Center" />
<atom:Tag Text="{gallery:SplashShowCaseLangResource ComponentIntroducedVersion}"
TagColor="blue"
IsBordered="False"
VerticalAlignment="Center" />
</WrapPanel>
</Grid>
<atom:TextBlock Text="{gallery:SplashShowCaseLangResource PageSubtitle}"
FontSize="15"
Foreground="{atom:SharedTokenResource ColorTextSecondary}" />
@@ -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<TextEditor>("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<Exception>? _exceptionHandler;
private GalleryTextEditorModel? _editorModel;
private IGrammar? _grammar;
private TMModel? _tmModel;
private ReadOnlyDictionary<string, string>? _themeColorsDictionary;
private bool _isDisposed;
public GalleryTextMateInstallation(TextEditor editor,
IRegistryOptions registryOptions,
Action<Exception>? 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<TextMateColoringTransformer>()
.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<Exception>? _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<Exception>? 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);
}
}
}
}
@@ -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<ScrollBar>()
.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<TextEditor>()
.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<TextMateColoringTransformer>()
.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(
" <atom:TextBlock Foreground=\"{atom:SharedTokenResource ColorTextTertiary}\" Text=\"{Binding Description}\" />",
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<TextEditor>()
.Single();
editor.ApplyTemplate();
Dispatcher.UIThread.RunJobs();
editor.TextArea.TextView.LineTransformers
.OfType<TextMateColoringTransformer>()
.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<TextMateColoringTransformer>()
.ShouldNotBeEmpty();
window.MouseUp(startPoint, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
editor.TextArea.TextView.LineTransformers
.OfType<TextMateColoringTransformer>()
.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(
" <atom:TextBlock Foreground=\"{atom:SharedTokenResource ColorTextTertiary}\" Text=\"{Binding Description}\" />",
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<TextEditor>()
.Single();
editor.ApplyTemplate();
Dispatcher.UIThread.RunJobs();
editor.TextArea.TextView.LineTransformers
.OfType<TextMateColoringTransformer>()
.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<TextMateColoringTransformer>()
.ShouldNotBeEmpty();
window.MouseUp(dragPoint, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
editor.TextArea.TextView.LineTransformers
.OfType<TextMateColoringTransformer>()
.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<ThemeName>();
}
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<Avalonia.Controls.ScrollViewer>();
}
private static string? GetAxamlTagNameColor(GalleryCodeViewer viewer)
{
return GetAxamlTokenColor(viewer, "StackPanel");
@@ -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);
@@ -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("<atom:DataGrid");
}
[Fact]
public void BorderBeam_ShowCase_Header_Centers_Title_Tags_And_Uses_Blue_Introduced_Version_Tag()
{
var source = ReadRepoFile("controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Views/BorderBeamShowCase.axaml");
var enSource = ReadRepoFile("controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/en_US.cs");
var zhCnSource = ReadRepoFile("controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/zh_CN.cs");
var zhTwSource = ReadRepoFile("controlgallery/AtomUIGallery/ShowCases/Other/BorderBeam/Localization/zh_TW.cs");
var header = ExtractHeaderTitleMarkup(source);
header.ShouldContain("<Grid ColumnDefinitions=\"Auto,*\"");
header.ShouldContain("<WrapPanel Grid.Column=\"1\"");
header.ShouldContain("VerticalAlignment=\"Center\"");
header.ShouldContain("Text=\"{gallery:BorderBeamShowCaseLangResource ComponentIntroducedVersion}\"");
header.ShouldContain("TagColor=\"blue\"");
header.ShouldContain("IsBordered=\"False\"");
header.ShouldNotContain("ComponentStatusPreview");
foreach (var localizationSource in new[] { enSource, zhCnSource, zhTwSource })
{
localizationSource.ShouldContain("public const string ComponentIntroducedVersion = \"v6.0.5\";");
localizationSource.ShouldNotContain("ComponentStatusPreview");
}
}
[Fact]
public void BorderBeam_ShowCase_Demos_Match_Ant_Design_Examples()
{
@@ -189,6 +213,21 @@ public class BorderBeamShowCasePageTests
return source[itemStart..(itemEnd + itemEndMarker.Length)];
}
private static string ExtractHeaderTitleMarkup(string source)
{
const string headerStartMarker = "Text=\"BorderBeam\"";
const string headerEndMarker = "BorderBeamShowCaseLangResource PageSubtitle";
var titleIndex = source.IndexOf(headerStartMarker, StringComparison.Ordinal);
titleIndex.ShouldBeGreaterThanOrEqualTo(0);
var headerStart = source.LastIndexOf("<Grid", titleIndex, StringComparison.Ordinal);
headerStart.ShouldBeGreaterThanOrEqualTo(0);
var headerEnd = source.IndexOf(headerEndMarker, headerStart, StringComparison.Ordinal);
headerEnd.ShouldBeGreaterThan(headerStart);
return source[headerStart..headerEnd];
}
private static IEnumerable<GalleryNavigationNode> Walk(IEnumerable<GalleryNavigationNode> nodes)
{
foreach (var node in nodes)
@@ -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("<atom:DataGrid");
}
[Fact]
public void Splash_ShowCase_Header_Centers_Title_Tags_And_Adds_Blue_Introduced_Version_Tag()
{
var source = ReadRepoFile("controlgallery/AtomUIGallery/ShowCases/Other/Splash/Views/SplashShowCase.axaml");
var enSource = ReadRepoFile("controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/en_US.cs");
var zhCnSource = ReadRepoFile("controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/zh_CN.cs");
var zhTwSource = ReadRepoFile("controlgallery/AtomUIGallery/ShowCases/Other/Splash/Localization/zh_TW.cs");
var header = ExtractHeaderTitleMarkup(source);
header.ShouldContain("<Grid ColumnDefinitions=\"Auto,*\"");
header.ShouldContain("<WrapPanel Grid.Column=\"1\"");
header.ShouldContain("VerticalAlignment=\"Center\"");
header.ShouldContain("Text=\"{gallery:SplashShowCaseLangResource ComponentStatusPreview}\"");
header.ShouldContain("Text=\"{gallery:SplashShowCaseLangResource ComponentIntroducedVersion}\"");
header.ShouldContain("TagColor=\"blue\"");
header.ShouldContain("IsBordered=\"False\"");
foreach (var localizationSource in new[] { enSource, zhCnSource, zhTwSource })
{
localizationSource.ShouldContain("public const string ComponentIntroducedVersion = \"v6.0.7\";");
}
}
[Fact]
public void Splash_ShowCase_Demos_Cover_Visual_States_Without_Showing_Window()
{
@@ -257,6 +281,21 @@ public class SplashShowCasePageTests
return source[itemStart..(itemEnd + itemEndMarker.Length)];
}
private static string ExtractHeaderTitleMarkup(string source)
{
const string headerStartMarker = "Text=\"Splash\"";
const string headerEndMarker = "SplashShowCaseLangResource PageSubtitle";
var titleIndex = source.IndexOf(headerStartMarker, StringComparison.Ordinal);
titleIndex.ShouldBeGreaterThanOrEqualTo(0);
var headerStart = source.LastIndexOf("<Grid", titleIndex, StringComparison.Ordinal);
headerStart.ShouldBeGreaterThanOrEqualTo(0);
var headerEnd = source.IndexOf(headerEndMarker, headerStart, StringComparison.Ordinal);
headerEnd.ShouldBeGreaterThan(headerStart);
return source[headerStart..headerEnd];
}
private static IEnumerable<GalleryNavigationNode> Walk(IEnumerable<GalleryNavigationNode> nodes)
{
foreach (var node in nodes)