From 151ffb1f5e3adafe1af99efe7a2fb548b6943d8f Mon Sep 17 00:00:00 2001 From: lincube Date: Tue, 23 Jun 2026 12:45:54 +0800 Subject: [PATCH] =?UTF-8?q?fix.=E5=A2=9E=E5=BC=BA=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E7=A8=B3=E5=AE=9A=E6=80=A7=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=BA=86?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E7=BB=84=E4=BB=B6=E5=8A=A0=E8=BD=BD=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E6=97=B6=E8=BF=9E=E5=B8=A6=E5=BA=94=E7=94=A8=E5=B4=A9?= =?UTF-8?q?=E6=BA=83=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ComponentLibraryCrashRegressionTests.cs | 122 ++++++++++ LanMountainDesktop/Program.cs | 12 +- LanMountainDesktop/Styles/GlassModule.axaml | 1 + .../Styles/NavigationStyles.axaml | 1 + .../Styles/SettingsAnimations.axaml | 3 + .../Views/Components/CnrDailyNewsWidget.axaml | 28 +-- .../Views/MainWindow.ComponentSystem.cs | 228 ++++++++++++------ LanMountainDesktop/Views/MainWindow.axaml | 1 + 8 files changed, 299 insertions(+), 97 deletions(-) create mode 100644 LanMountainDesktop.Tests/ComponentLibraryCrashRegressionTests.cs 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, + @"[\s\S]*?", + 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 @@ +