diff --git a/LanMountainDesktop.Tests/ComponentLibraryCrashRegressionTests.cs b/LanMountainDesktop.Tests/ComponentLibraryCrashRegressionTests.cs
new file mode 100644
index 0000000..167a9e6
--- /dev/null
+++ b/LanMountainDesktop.Tests/ComponentLibraryCrashRegressionTests.cs
@@ -0,0 +1,122 @@
+using System.Text.RegularExpressions;
+using LanMountainDesktop.Services;
+using Xunit;
+
+namespace LanMountainDesktop.Tests;
+
+public sealed class ComponentLibraryCrashRegressionTests
+{
+ [Fact]
+ public void BuiltInComponentXaml_DoesNotAnimateRenderTransformDirectly()
+ {
+ var componentsDirectory = FindRepositoryPath("LanMountainDesktop", "Views", "Components");
+ foreach (var file in Directory.EnumerateFiles(componentsDirectory, "*.axaml", SearchOption.AllDirectories))
+ {
+ var xaml = File.ReadAllText(file);
+ foreach (Match match in Regex.Matches(
+ xaml,
+ @"",
+ RegexOptions.CultureInvariant))
+ {
+ Assert.DoesNotContain(
+ "Property=\"RenderTransform\"",
+ match.Value,
+ StringComparison.Ordinal);
+ }
+ }
+ }
+
+ [Fact]
+ public void ComponentLibraryPreviewPages_FallBackWhenPreviewAttachFails()
+ {
+ var source = ReadRepositoryFile("LanMountainDesktop", "Views", "MainWindow.ComponentSystem.cs");
+ var buildSource = ExtractMethodSource(source, "BuildComponentLibraryComponentPages");
+ var pageSource = ExtractMethodSource(source, "CreateComponentLibraryComponentPage");
+ var contentSource = ExtractMethodSource(source, "CreateComponentLibraryComponentPageContent");
+
+ Assert.Contains("try", buildSource);
+ Assert.Contains("ComponentLibraryComponentPagesContainer.Children.Add(page)", buildSource);
+ Assert.Contains("forceFallback: true", buildSource);
+ Assert.Contains("UiExceptionGuard.IsFatalException", buildSource);
+
+ Assert.Contains("CreateStaticComponentPreviewFallback", contentSource);
+ Assert.Contains("forceFallback", pageSource);
+ }
+
+ [Fact]
+ public void SoftwareRenderRetry_IsDisabledAfterAvaloniaLifetimeStarts()
+ {
+ var shouldRetry = Program.ShouldRetryWithSoftwareRendering(
+ AppRenderingModeHelper.Default,
+ new InvalidOperationException("render failed"),
+ isAvaloniaLifetimeStarted: true);
+
+ Assert.False(shouldRetry);
+ }
+
+ private static string ReadRepositoryFile(params string[] segments)
+ {
+ var path = FindRepositoryPath(segments);
+ return File.ReadAllText(path);
+ }
+
+ private static string FindRepositoryPath(params string[] segments)
+ {
+ var directory = new DirectoryInfo(AppContext.BaseDirectory);
+ while (directory is not null)
+ {
+ var candidate = Path.Combine(new[] { directory.FullName }.Concat(segments).ToArray());
+ if (File.Exists(candidate) || Directory.Exists(candidate))
+ {
+ return candidate;
+ }
+
+ if (File.Exists(Path.Combine(directory.FullName, "LanMountainDesktop.slnx")))
+ {
+ break;
+ }
+
+ directory = directory.Parent;
+ }
+
+ throw new FileNotFoundException($"Could not locate repository path '{Path.Combine(segments)}'.");
+ }
+
+ private static string ExtractMethodSource(string source, string methodName)
+ {
+ var methodIndex = source.IndexOf($"private void {methodName}(", StringComparison.Ordinal);
+ if (methodIndex < 0)
+ {
+ methodIndex = source.IndexOf($"private Grid {methodName}(", StringComparison.Ordinal);
+ }
+
+ if (methodIndex < 0)
+ {
+ methodIndex = source.IndexOf($"private StackPanel {methodName}(", StringComparison.Ordinal);
+ }
+
+ Assert.True(methodIndex >= 0, $"Could not locate method '{methodName}'.");
+
+ var braceIndex = source.IndexOf('{', methodIndex);
+ Assert.True(braceIndex >= 0, $"Could not locate method body for '{methodName}'.");
+
+ var depth = 0;
+ for (var i = braceIndex; i < source.Length; i++)
+ {
+ if (source[i] == '{')
+ {
+ depth++;
+ }
+ else if (source[i] == '}')
+ {
+ depth--;
+ if (depth == 0)
+ {
+ return source.Substring(methodIndex, i - methodIndex + 1);
+ }
+ }
+ }
+
+ throw new InvalidOperationException($"Could not extract method '{methodName}'.");
+ }
+}
diff --git a/LanMountainDesktop/Program.cs b/LanMountainDesktop/Program.cs
index 63840d8..b0f5dcc 100644
--- a/LanMountainDesktop/Program.cs
+++ b/LanMountainDesktop/Program.cs
@@ -54,7 +54,7 @@ public sealed class Program
WriteCrashDump(ex, StartupRenderMode);
// 渲染模式安全降级:若失败且未禁用重试,且当前不是软件渲染,则用软件渲染重试一次
- if (ShouldRetryWithSoftwareRendering(StartupRenderMode, ex) &&
+ if (ShouldRetryWithSoftwareRendering(StartupRenderMode, ex, isAvaloniaLifetimeStarted: Application.Current is not null) &&
!attemptedRenderModes.Contains(AppRenderingModeHelper.Software))
{
AppLogger.Warn("Startup", $"Retrying startup with Software rendering mode (previous='{StartupRenderMode}').");
@@ -195,8 +195,16 @@ public sealed class Program
/// 判断是否应该用软件渲染重试。当异常看起来与渲染相关(GPU/驱动/平台初始化),
/// 且当前渲染模式不是软件渲染,且未通过环境变量禁用重试时返回 true。
///
- private static bool ShouldRetryWithSoftwareRendering(string currentRenderMode, Exception ex)
+ internal static bool ShouldRetryWithSoftwareRendering(
+ string currentRenderMode,
+ Exception ex,
+ bool isAvaloniaLifetimeStarted = false)
{
+ if (isAvaloniaLifetimeStarted)
+ {
+ return false;
+ }
+
if (string.Equals(currentRenderMode, AppRenderingModeHelper.Software, StringComparison.OrdinalIgnoreCase))
{
return false;
diff --git a/LanMountainDesktop/Styles/GlassModule.axaml b/LanMountainDesktop/Styles/GlassModule.axaml
index 81ad7c3..937daf9 100644
--- a/LanMountainDesktop/Styles/GlassModule.axaml
+++ b/LanMountainDesktop/Styles/GlassModule.axaml
@@ -26,6 +26,7 @@
+
diff --git a/LanMountainDesktop/Styles/NavigationStyles.axaml b/LanMountainDesktop/Styles/NavigationStyles.axaml
index 721bcc0..051f376 100644
--- a/LanMountainDesktop/Styles/NavigationStyles.axaml
+++ b/LanMountainDesktop/Styles/NavigationStyles.axaml
@@ -150,6 +150,7 @@
diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
index eaf6658..a6da705 100644
--- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
+++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
@@ -3655,90 +3655,40 @@ public partial class MainWindow : Window
for (var i = 0; i < componentCount; i++)
{
var component = _componentLibraryActiveComponents[i];
-
- var page = new Grid
- {
- Width = _componentLibraryComponentPageWidth,
- Height = viewportHeight,
- Background = Brushes.Transparent
- };
-
- // Fit the preview to the page while preserving component cell span proportions.
- var previewMaxWidth = _componentLibraryComponentPageWidth * 0.94;
- var previewMaxHeight = viewportHeight * 0.86;
var previewSpan = NormalizeComponentCellSpan(
component.ComponentId,
(component.MinWidthCells, component.MinHeightCells));
- var previewCellSize = Math.Min(
- previewMaxWidth / Math.Max(1, previewSpan.WidthCells),
- previewMaxHeight / Math.Max(1, previewSpan.HeightCells));
- previewCellSize = Math.Clamp(previewCellSize, 24, 96);
-
- var previewWidth = previewSpan.WidthCells * previewCellSize;
- var previewHeight = previewSpan.HeightCells * previewCellSize;
- var previewControl = CreateStaticComponentLibraryPreview(
- component.ComponentId,
- previewCellSize,
- previewWidth,
- previewHeight);
-
- var previewSurface = new Border
- {
- Width = previewWidth,
- Height = previewHeight,
- Background = Brushes.Transparent,
- ClipToBounds = false,
- Child = previewControl,
- IsHitTestVisible = false
- };
-
- var previewBorder = new Border
- {
- Width = previewWidth,
- Height = previewHeight,
- ClipToBounds = false,
- Background = Brushes.Transparent,
- BorderThickness = new Thickness(0),
- Child = previewSurface,
- Tag = component.ComponentId
- };
- previewBorder.PointerPressed += OnComponentLibraryComponentPreviewPointerPressed;
-
- var label = new TextBlock
- {
- Text = GetLocalizedComponentDisplayName(component),
- FontSize = 14,
- FontWeight = FontWeight.SemiBold,
- Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"),
- HorizontalAlignment = HorizontalAlignment.Center
- };
-
- var hint = new TextBlock
- {
- Text = L("component_library.drag_hint", "Drag to place"),
- FontSize = 12,
- Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"),
- HorizontalAlignment = HorizontalAlignment.Center
- };
-
- var stack = new StackPanel
- {
- Spacing = 8,
- HorizontalAlignment = HorizontalAlignment.Center,
- VerticalAlignment = VerticalAlignment.Center,
- Children =
- {
- previewBorder,
- label,
- hint
- }
- };
-
- page.Children.Add(stack);
+ var page = CreateComponentLibraryComponentPage(
+ component,
+ previewSpan,
+ _componentLibraryComponentPageWidth,
+ viewportHeight);
Grid.SetRow(page, 0);
Grid.SetColumn(page, i);
- ComponentLibraryComponentPagesContainer.Children.Add(page);
+
+ try
+ {
+ ComponentLibraryComponentPagesContainer.Children.Add(page);
+ }
+ catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
+ {
+ AppLogger.Warn(
+ "ComponentLibrary",
+ $"Failed to attach component library preview page for component '{component.ComponentId}'. Falling back to placeholder.",
+ ex);
+
+ DisposeStaticComponentLibraryPreviews([page]);
+ var fallbackPage = CreateComponentLibraryComponentPage(
+ component,
+ previewSpan,
+ _componentLibraryComponentPageWidth,
+ viewportHeight,
+ forceFallback: true);
+ Grid.SetRow(fallbackPage, 0);
+ Grid.SetColumn(fallbackPage, i);
+ ComponentLibraryComponentPagesContainer.Children.Add(fallbackPage);
+ }
}
_componentLibraryComponentHostTransform = ComponentLibraryComponentPagesHost.RenderTransform as TranslateTransform;
@@ -3752,6 +3702,128 @@ public partial class MainWindow : Window
UpdateComponentLibraryComponentNavigationButtons();
}
+ private Grid CreateComponentLibraryComponentPage(
+ ComponentLibraryComponentEntry component,
+ (int WidthCells, int HeightCells) previewSpan,
+ double pageWidth,
+ double pageHeight,
+ bool forceFallback = false)
+ {
+ var page = new Grid
+ {
+ Width = pageWidth,
+ Height = pageHeight,
+ Background = Brushes.Transparent
+ };
+
+ try
+ {
+ var stack = CreateComponentLibraryComponentPageContent(
+ component,
+ previewSpan,
+ pageWidth,
+ pageHeight,
+ forceFallback);
+ page.Children.Add(stack);
+ }
+ catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex) && !forceFallback)
+ {
+ AppLogger.Warn(
+ "ComponentLibrary",
+ $"Failed to build component library preview page for component '{component.ComponentId}'. Falling back to placeholder.",
+ ex);
+
+ page.Children.Clear();
+ var stack = CreateComponentLibraryComponentPageContent(
+ component,
+ previewSpan,
+ pageWidth,
+ pageHeight,
+ forceFallback: true);
+ page.Children.Add(stack);
+ }
+
+ return page;
+ }
+
+ private StackPanel CreateComponentLibraryComponentPageContent(
+ ComponentLibraryComponentEntry component,
+ (int WidthCells, int HeightCells) previewSpan,
+ double pageWidth,
+ double pageHeight,
+ bool forceFallback)
+ {
+ // Fit the preview to the page while preserving component cell span proportions.
+ var previewMaxWidth = pageWidth * 0.94;
+ var previewMaxHeight = pageHeight * 0.86;
+ var previewCellSize = Math.Min(
+ previewMaxWidth / Math.Max(1, previewSpan.WidthCells),
+ previewMaxHeight / Math.Max(1, previewSpan.HeightCells));
+ previewCellSize = Math.Clamp(previewCellSize, 24, 96);
+
+ var previewWidth = previewSpan.WidthCells * previewCellSize;
+ var previewHeight = previewSpan.HeightCells * previewCellSize;
+ var previewControl = forceFallback
+ ? CreateStaticComponentPreviewFallback(previewWidth, previewHeight)
+ : CreateStaticComponentLibraryPreview(
+ component.ComponentId,
+ previewCellSize,
+ previewWidth,
+ previewHeight);
+
+ var previewSurface = new Border
+ {
+ Width = previewWidth,
+ Height = previewHeight,
+ Background = Brushes.Transparent,
+ ClipToBounds = false,
+ Child = previewControl,
+ IsHitTestVisible = false
+ };
+
+ var previewBorder = new Border
+ {
+ Width = previewWidth,
+ Height = previewHeight,
+ ClipToBounds = false,
+ Background = Brushes.Transparent,
+ BorderThickness = new Thickness(0),
+ Child = previewSurface,
+ Tag = component.ComponentId
+ };
+ previewBorder.PointerPressed += OnComponentLibraryComponentPreviewPointerPressed;
+
+ var label = new TextBlock
+ {
+ Text = GetLocalizedComponentDisplayName(component),
+ FontSize = 14,
+ FontWeight = FontWeight.SemiBold,
+ Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"),
+ HorizontalAlignment = HorizontalAlignment.Center
+ };
+
+ var hint = new TextBlock
+ {
+ Text = L("component_library.drag_hint", "Drag to place"),
+ FontSize = 12,
+ Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"),
+ HorizontalAlignment = HorizontalAlignment.Center
+ };
+
+ return new StackPanel
+ {
+ Spacing = 8,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Children =
+ {
+ previewBorder,
+ label,
+ hint
+ }
+ };
+ }
+
private void ClearComponentLibraryPreviewControls()
{
if (ComponentLibraryComponentPagesContainer is null)
diff --git a/LanMountainDesktop/Views/MainWindow.axaml b/LanMountainDesktop/Views/MainWindow.axaml
index a30700c..54df62e 100644
--- a/LanMountainDesktop/Views/MainWindow.axaml
+++ b/LanMountainDesktop/Views/MainWindow.axaml
@@ -76,6 +76,7 @@
+