diff --git a/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs index 30e769b..dc41f3b 100644 --- a/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; @@ -65,6 +66,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I private readonly LocalizationService _localizationService = new(); private TimeZoneService? _timeZoneService; private double _currentCellSize = 48; + private double _layoutScale = 1d; private bool _dialInitialized; private bool _handsInitialized; private bool? _isNightModeApplied; @@ -185,17 +187,18 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I private void BuildTicks(bool isNightMode) { TickCanvas.Children.Clear(); + var scale = Math.Clamp(_layoutScale, 0.78d, 1.22d); var majorBrush = CreateBrush(isNightMode ? "#1A1A1A" : "#1E2430"); var minorBrush = CreateBrush(isNightMode ? "#D0D0D0" : "#D7DCE5"); - var majorThickness = isNightMode ? 3.0 : 2.8; - var minorThickness = isNightMode ? 1.4 : 1.2; + var majorThickness = (isNightMode ? 3.0 : 2.8) * scale; + var minorThickness = (isNightMode ? 1.4 : 1.2) * scale; for (var i = 0; i < 60; i++) { var angle = (i * 6 - 90) * Math.PI / 180d; var isHourTick = i % 5 == 0; - var outerRadius = Center - 7; - var innerRadius = outerRadius - (isHourTick ? 16 : 8); + var outerRadius = Center - (7 * scale); + var innerRadius = outerRadius - (isHourTick ? 16 * scale : 8 * scale); var x1 = Center + Math.Cos(angle) * innerRadius; var y1 = Center + Math.Sin(angle) * innerRadius; @@ -218,34 +221,50 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I private void BuildNumbers(bool isNightMode) { NumberCanvas.Children.Clear(); + var scale = Math.Clamp(_layoutScale, 0.78d, 1.22d); var foreground = CreateBrush(isNightMode ? "#101010" : "#0F131A"); var fontWeight = isNightMode ? FontWeight.Bold : FontWeight.SemiBold; for (var number = 1; number <= 12; number++) { var angle = (number * 30 - 90) * Math.PI / 180d; - var radius = 88; + var radius = 88 * scale; var x = Center + Math.Cos(angle) * radius; var y = Center + Math.Sin(angle) * radius; var isDoubleDigit = number >= 10; - var width = isDoubleDigit ? 44 : 28; - var height = 34; + var glyphBox = ComponentTypographyLayoutService.ResolveGlyphBox( + isDoubleDigit ? 44 * scale : 36 * scale, + 34 * scale, + preferredSizeScale: isDoubleDigit ? 0.92d : 0.88d, + minSize: 18, + maxSize: isDoubleDigit ? 36 : 30, + insetScale: 0d); + var text = number.ToString(CultureInfo.InvariantCulture); + var fontSize = ComponentTypographyLayoutService.FitFontSize( + text, + glyphBox.Width, + glyphBox.Height, + maxLines: 1, + minFontSize: 12 * scale, + maxFontSize: 18 * scale, + weight: fontWeight, + lineHeightFactor: 1d); - var text = new TextBlock + var numberText = new TextBlock { - Text = number.ToString(CultureInfo.InvariantCulture), - Width = width, - Height = height, + Text = text, + Width = glyphBox.Width, + Height = glyphBox.Height, TextAlignment = TextAlignment.Center, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, - FontSize = 18, + FontSize = fontSize, FontWeight = fontWeight, Foreground = foreground }; - Canvas.SetLeft(text, x - width / 2d); - Canvas.SetTop(text, y - height / 2d); - NumberCanvas.Children.Add(text); + Canvas.SetLeft(numberText, x - glyphBox.Width / 2d); + Canvas.SetTop(numberText, y - glyphBox.Height / 2d); + NumberCanvas.Children.Add(numberText); } } @@ -325,10 +344,14 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I { _currentCellSize = Math.Max(1, cellSize); var scale = ResolveScale(); + var chromeScale = ComponentChromeCornerRadiusHelper.ResolveScale(); + _layoutScale = Math.Clamp(scale * (0.9d + (chromeScale * 0.1d)), 0.58d, 2.0d); - RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(42 * scale, 16, 56); - RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(14 * scale, 14 * scale, null, 0.55d); + RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(42 * _layoutScale, 16, 56); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(14 * _layoutScale, 14 * _layoutScale, null, 0.55d); ApplyModeVisualIfNeeded(); + BuildTicks(_isNightModeApplied ?? ResolveIsNightMode()); + BuildNumbers(_isNightModeApplied ?? ResolveIsNightMode()); } private double ResolveScale() diff --git a/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs b/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs index d667c91..f96e86a 100644 --- a/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs @@ -13,6 +13,7 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -432,29 +433,61 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget RefreshGlyphIcon.FontSize = Math.Clamp(refreshButtonSize * 0.46, 10, 20); var lineColumnGap = Math.Clamp(lineRowHeight * 0.34, 5, 12); - var indexWidth = Math.Clamp(lineRowHeight * 1.02, 16, 28); - var indexFont = Math.Clamp(lineRowHeight * 0.50, 10, 16); - var itemFont = Math.Clamp(lineRowHeight * 0.62, 12, 24); + var indexBadge = ComponentTypographyLayoutService.ResolveBadgeBox( + lineRowHeight, + lineRowHeight, + preferredSizeScale: 0.62d, + minSize: 16, + maxSize: 28); + var indexFont = ComponentTypographyLayoutService.FitFontSize( + "88", + indexBadge.Width, + indexBadge.Height, + 1, + minFontSize: 10, + maxFontSize: Math.Clamp(indexBadge.Height * 0.82d, 10, 18), + weight: FontWeight.Bold, + lineHeightFactor: 1.0d, + fontFamily: MiSansFontFamily); + var itemFontMin = Math.Clamp(lineRowHeight * 0.42, 11, 16); + var itemFontMax = Math.Clamp(lineRowHeight * 0.68, 12, 24); var rowPadding = Math.Clamp(lineRowHeight * 0.08, 1, 4); - var itemTextWidth = Math.Max(56, innerWidth - indexWidth - lineColumnGap); + var itemTextWidth = Math.Max(56, innerWidth - indexBadge.Width - lineColumnGap); foreach (var visual in _hotItemVisuals) { visual.RowGrid.ColumnSpacing = lineColumnGap; if (visual.RowGrid.ColumnDefinitions.Count > 0) { - visual.RowGrid.ColumnDefinitions[0].Width = new GridLength(indexWidth, GridUnitType.Pixel); + visual.RowGrid.ColumnDefinitions[0].Width = new GridLength(indexBadge.Width, GridUnitType.Pixel); } visual.Host.Padding = new Thickness(0, rowPadding, 0, rowPadding); visual.IndexTextBlock.FontSize = indexFont; - visual.IndexTextBlock.MaxWidth = indexWidth; - visual.TitleTextBlock.FontSize = itemFont; + visual.IndexTextBlock.MaxWidth = indexBadge.Width; + visual.IndexTextBlock.MinWidth = indexBadge.Width; + visual.IndexTextBlock.Margin = indexBadge.Margin; + + var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + visual.TitleTextBlock.Text, + itemTextWidth, + lineRowHeight * 1.9d, + minLines: 1, + maxLines: ComponentTypographyLayoutService.CountTextDisplayUnits(visual.TitleTextBlock.Text) > 24 ? 2 : 1, + minFontSize: itemFontMin, + maxFontSize: itemFontMax, + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Bold }, + lineHeightFactor: 1.08d, + fontFamily: MiSansFontFamily); + visual.TitleTextBlock.FontSize = titleLayout.FontSize; + visual.TitleTextBlock.LineHeight = titleLayout.LineHeight; + visual.TitleTextBlock.MaxLines = titleLayout.MaxLines; + visual.TitleTextBlock.FontWeight = titleLayout.Weight; visual.TitleTextBlock.MaxWidth = itemTextWidth; visual.TitleTextBlock.TextAlignment = TextAlignment.Left; } - StatusTextBlock.FontSize = Math.Clamp(itemFont, 10, 20); + StatusTextBlock.FontSize = Math.Clamp(itemFontMax, 10, 20); ApplyNightModeVisual(); } diff --git a/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs b/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs index 1940596..1a9dd3b 100644 --- a/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs @@ -11,6 +11,7 @@ using Avalonia.Input; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -437,37 +438,93 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid Math.Clamp(searchBoxHeight * 0.24, 5, 10), 0); SearchGlyphIcon.FontSize = Math.Clamp(searchBoxHeight * 0.45, 10, 20); - SearchEntryTextBlock.FontSize = Math.Clamp(searchBoxHeight * 0.44, 10, 18); + + var searchLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + SearchEntryTextBlock.Text, + Math.Max(54, SearchBoxBorder.Width - Math.Clamp(searchBoxHeight * 0.48, 8, 16)), + searchBoxHeight, + minLines: 1, + maxLines: 1, + minFontSize: 10, + maxFontSize: Math.Clamp(searchBoxHeight * 0.44, 10, 18), + weightCandidates: new[] { FontWeight.Medium, FontWeight.SemiBold }, + lineHeightFactor: 1.0d, + fontFamily: MiSansFontFamily); + SearchEntryTextBlock.FontSize = searchLayout.FontSize; + SearchEntryTextBlock.LineHeight = searchLayout.LineHeight; TopRightTitleTextBlock.MaxWidth = Math.Max(80, innerWidth - SearchBoxBorder.Width - HeaderGrid.ColumnSpacing); - TopRightTitleTextBlock.FontSize = Math.Clamp(topRowHeight * 0.46, 11, 22); + var topRightLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + TopRightTitleTextBlock.Text, + TopRightTitleTextBlock.MaxWidth, + topRowHeight, + minLines: 1, + maxLines: 1, + minFontSize: 11, + maxFontSize: Math.Clamp(topRowHeight * 0.46, 11, 22), + weightCandidates: new[] { FontWeight.Medium, FontWeight.SemiBold }, + lineHeightFactor: 1.0d, + fontFamily: MiSansFontFamily); + TopRightTitleTextBlock.FontSize = topRightLayout.FontSize; + TopRightTitleTextBlock.LineHeight = topRightLayout.LineHeight; var lineColumnGap = Math.Clamp(lineRowHeight * 0.34, 5, 12); - var indexWidth = Math.Clamp(lineRowHeight * 1.02, 16, 28); - var indexFont = Math.Clamp(lineRowHeight * 0.50, 10, 16); - var itemFont = Math.Clamp(lineRowHeight * 0.62, 12, 24); + var indexBadge = ComponentTypographyLayoutService.ResolveBadgeBox( + lineRowHeight, + lineRowHeight, + preferredSizeScale: 0.62d, + minSize: 16, + maxSize: 28); + var indexFont = ComponentTypographyLayoutService.FitFontSize( + "88", + indexBadge.Width, + indexBadge.Height, + 1, + minFontSize: 10, + maxFontSize: Math.Clamp(indexBadge.Height * 0.82d, 10, 18), + weight: FontWeight.Bold, + lineHeightFactor: 1.0d, + fontFamily: MiSansFontFamily); + var itemFontMin = Math.Clamp(lineRowHeight * 0.42, 11, 16); + var itemFontMax = Math.Clamp(lineRowHeight * 0.68, 12, 24); var rowPadding = Math.Clamp(lineRowHeight * 0.08, 1, 4); - var itemTextWidth = Math.Max(56, innerWidth - indexWidth - lineColumnGap); + var itemTextWidth = Math.Max(56, innerWidth - indexBadge.Width - lineColumnGap); foreach (var visual in _hotItemVisuals) { visual.RowGrid.ColumnSpacing = lineColumnGap; if (visual.RowGrid.ColumnDefinitions.Count > 0) { - visual.RowGrid.ColumnDefinitions[0].Width = new GridLength(indexWidth, GridUnitType.Pixel); + visual.RowGrid.ColumnDefinitions[0].Width = new GridLength(indexBadge.Width, GridUnitType.Pixel); } visual.Host.Padding = new Thickness(0, rowPadding, 0, rowPadding); visual.IndexTextBlock.FontSize = indexFont; - visual.IndexTextBlock.MaxWidth = indexWidth; + visual.IndexTextBlock.MaxWidth = indexBadge.Width; + visual.IndexTextBlock.MinWidth = indexBadge.Width; + visual.IndexTextBlock.Margin = indexBadge.Margin; visual.IndexTextBlock.HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right; visual.IndexTextBlock.TextAlignment = TextAlignment.Right; - visual.TitleTextBlock.FontSize = itemFont; + var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + visual.TitleTextBlock.Text, + itemTextWidth, + lineRowHeight * 1.9d, + minLines: 1, + maxLines: ComponentTypographyLayoutService.CountTextDisplayUnits(visual.TitleTextBlock.Text) > 24 ? 2 : 1, + minFontSize: itemFontMin, + maxFontSize: itemFontMax, + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Bold }, + lineHeightFactor: 1.08d, + fontFamily: MiSansFontFamily); + visual.TitleTextBlock.FontSize = titleLayout.FontSize; + visual.TitleTextBlock.LineHeight = titleLayout.LineHeight; + visual.TitleTextBlock.MaxLines = titleLayout.MaxLines; + visual.TitleTextBlock.FontWeight = titleLayout.Weight; visual.TitleTextBlock.MaxWidth = itemTextWidth; visual.TitleTextBlock.TextAlignment = TextAlignment.Left; } - StatusTextBlock.FontSize = Math.Clamp(itemFont, 10, 20); + StatusTextBlock.FontSize = Math.Clamp(itemFontMax, 10, 20); ApplyNightModeVisual(); } diff --git a/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs b/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs index b552955..1c51347 100644 --- a/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs @@ -6,6 +6,7 @@ using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Styling; using AvaloniaWebView; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Services; using WebViewCore.Events; @@ -52,6 +53,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget, } AddressTextBox.Text = DefaultHomeUri.ToString(); + UpdateAddressTypography(); UpdateWebViewActiveState(); } @@ -116,6 +118,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget, AddressTextBox.FontSize = Math.Clamp(_currentCellSize * 0.30, 12, 15); AddressTextBox.Height = buttonSize; + UpdateAddressTypography(); } public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) @@ -283,6 +286,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget, { _lastKnownUri = uri; AddressTextBox.Text = uri.ToString(); + UpdateAddressTypography(); if (_isWebViewActive) { TryNavigate(uri, "NavigateTo"); @@ -298,6 +302,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget, _lastKnownUri = e.Url; AddressTextBox.Text = e.Url.ToString(); + UpdateAddressTypography(); } private void UpdateWebViewActiveState() @@ -407,6 +412,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget, GoButton.IsEnabled = false; AddressTextBox.IsEnabled = false; AddressTextBox.Text = _lastKnownUri.ToString(); + UpdateAddressTypography(); UnavailableMessageTextBlock.Text = _isWebViewFaulted ? "The browser component is temporarily unavailable. Restart the app to retry." @@ -451,4 +457,25 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget, ? uri : null; } + + private void UpdateAddressTypography() + { + var maxWidth = AddressTextBox.Bounds.Width > 1 + ? AddressTextBox.Bounds.Width + : Math.Max(120, (Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 7) - 120); + var maxHeight = AddressTextBox.Bounds.Height > 1 + ? AddressTextBox.Bounds.Height + : Math.Max(20, AddressTextBox.Height); + + AddressTextBox.FontSize = ComponentTypographyLayoutService.FitFontSize( + AddressTextBox.Text, + maxWidth, + maxHeight, + maxLines: 1, + minFontSize: 12, + maxFontSize: 16, + weight: FontWeight.Normal, + lineHeightFactor: 1.06d, + fontFamily: AddressTextBox.FontFamily); + } } diff --git a/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs b/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs index 0108e4e..42f94a0 100644 --- a/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs @@ -8,6 +8,7 @@ using Avalonia.Input; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; @@ -43,6 +44,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, private TimeZoneService? _timeZoneService; private double _currentCellSize = 48; + private double _layoutScale = 1d; private IReadOnlyList _courseItems = Array.Empty(); private bool _isNightVisual = true; private string _languageCode = "zh-CN"; @@ -493,18 +495,20 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor( _componentColorScheme, ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode()); + var chromeScale = ComponentChromeCornerRadiusHelper.ResolveScale(); + _layoutScale = Math.Clamp(ResolveScale() * (0.9d + (chromeScale * 0.1d)), 0.52d, 2.2d); - var scale = ResolveScale(); - var bulletSize = Math.Clamp(10 * scale, 5, 12); - var courseNameSize = Math.Clamp(42 * scale, 14, 42); - var secondarySize = Math.Clamp(29 * scale, 10, 28); - var lineSpacing = Math.Clamp(4 * scale, 1.5, 8); + var bulletSize = Math.Clamp(10 * _layoutScale, 5, 12); + var lineSpacing = Math.Clamp(4 * _layoutScale, 1.5, 8); var itemPadding = new Thickness( - ComponentChromeCornerRadiusHelper.SafeValue(6 * scale, 3, 10), - ComponentChromeCornerRadiusHelper.SafeValue(4 * scale, 2, 8), - ComponentChromeCornerRadiusHelper.SafeValue(4 * scale, 2, 8), - ComponentChromeCornerRadiusHelper.SafeValue(4 * scale, 2, 8)); - var maxVisibleItems = ResolveMaxVisibleItems(scale); + ComponentChromeCornerRadiusHelper.SafeValue(6 * _layoutScale, 3, 10), + ComponentChromeCornerRadiusHelper.SafeValue(4 * _layoutScale, 2, 8), + ComponentChromeCornerRadiusHelper.SafeValue(4 * _layoutScale, 2, 8), + ComponentChromeCornerRadiusHelper.SafeValue(4 * _layoutScale, 2, 8)); + var maxVisibleItems = ResolveMaxVisibleItems(_layoutScale); + var itemContentWidth = Math.Max(28, (Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 4) - itemPadding.Left - itemPadding.Right - bulletSize - Math.Clamp(10 * _layoutScale, 4, 14)); + var titleHeight = Math.Clamp(34 * _layoutScale, 16, 42); + var secondaryHeight = Math.Clamp(24 * _layoutScale, 12, 30); var primaryBrush = CreateBrush(_isNightVisual ? "#F9FBFF" : "#151821"); var secondaryBrush = CreateBrush(_isNightVisual ? "#848B99" : "#667084"); @@ -525,14 +529,32 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, CornerRadius = new CornerRadius(bulletSize * 0.5), Background = bulletBrush, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top, - Margin = new Thickness(0, Math.Clamp(8 * scale, 2, 12), 0, 0) + Margin = new Thickness(0, Math.Clamp(8 * _layoutScale, 2, 12), 0, 0) }; var titleText = new TextBlock { Text = item.Name, - FontSize = courseNameSize, - FontWeight = ToVariableWeight(Lerp(620, 780, Math.Clamp((scale - 0.60) / 1.2, 0, 1))), + FontSize = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + item.Name, + itemContentWidth, + titleHeight, + minLines: 1, + maxLines: 2, + minFontSize: 14, + maxFontSize: Math.Clamp(42 * _layoutScale, 14, 42), + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Medium }, + lineHeightFactor: 1.05d).FontSize, + FontWeight = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + item.Name, + itemContentWidth, + titleHeight, + minLines: 1, + maxLines: 2, + minFontSize: 14, + maxFontSize: Math.Clamp(42 * _layoutScale, 14, 42), + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Medium }, + lineHeightFactor: 1.05d).Weight, Foreground = primaryBrush, TextTrimming = TextTrimming.CharacterEllipsis, TextWrapping = TextWrapping.NoWrap @@ -541,8 +563,26 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, var timeText = new TextBlock { Text = item.TimeRange, - FontSize = secondarySize, - FontWeight = ToVariableWeight(Lerp(520, 680, Math.Clamp((scale - 0.60) / 1.2, 0, 1))), + FontSize = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + item.TimeRange, + itemContentWidth, + secondaryHeight, + minLines: 1, + maxLines: 1, + minFontSize: 10, + maxFontSize: Math.Clamp(28 * _layoutScale, 10, 28), + weightCandidates: new[] { FontWeight.Medium, FontWeight.Normal }, + lineHeightFactor: 1d).FontSize, + FontWeight = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + item.TimeRange, + itemContentWidth, + secondaryHeight, + minLines: 1, + maxLines: 1, + minFontSize: 10, + maxFontSize: Math.Clamp(28 * _layoutScale, 10, 28), + weightCandidates: new[] { FontWeight.Medium, FontWeight.Normal }, + lineHeightFactor: 1d).Weight, Foreground = secondaryBrush, TextTrimming = TextTrimming.CharacterEllipsis, TextWrapping = TextWrapping.NoWrap @@ -551,8 +591,26 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, var detailText = new TextBlock { Text = item.Detail, - FontSize = secondarySize, - FontWeight = ToVariableWeight(Lerp(500, 640, Math.Clamp((scale - 0.60) / 1.2, 0, 1))), + FontSize = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + item.Detail, + itemContentWidth, + secondaryHeight, + minLines: 1, + maxLines: 1, + minFontSize: 10, + maxFontSize: Math.Clamp(28 * _layoutScale, 10, 28), + weightCandidates: new[] { FontWeight.Normal, FontWeight.Medium }, + lineHeightFactor: 1d).FontSize, + FontWeight = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + item.Detail, + itemContentWidth, + secondaryHeight, + minLines: 1, + maxLines: 1, + minFontSize: 10, + maxFontSize: Math.Clamp(28 * _layoutScale, 10, 28), + weightCandidates: new[] { FontWeight.Normal, FontWeight.Medium }, + lineHeightFactor: 1d).Weight, Foreground = secondaryBrush, TextTrimming = TextTrimming.CharacterEllipsis, TextWrapping = TextWrapping.NoWrap @@ -567,7 +625,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, var itemGrid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,*"), - ColumnSpacing = Math.Clamp(10 * scale, 4, 14) + ColumnSpacing = Math.Clamp(10 * _layoutScale, 4, 14) }; itemGrid.Children.Add(bullet); itemGrid.Children.Add(textStack); @@ -603,6 +661,8 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, } var scale = ResolveScale(); + var chromeScale = ComponentChromeCornerRadiusHelper.ResolveScale(); + _layoutScale = Math.Clamp(scale * (0.9d + (chromeScale * 0.1d)), 0.52d, 2.2d); _isNightVisual = ResolveNightMode(); var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor( @@ -612,6 +672,66 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, var slashBrush = useMonetColor ? CreateBrush("#FF4FC3F7") : CreateBrush("#FF3250"); + var sampleNow = _timeZoneService?.GetCurrentTime() ?? DateTime.Now; + var headerWidth = Math.Max(42, Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 4); + var headerHeight = Math.Max(42, Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 4); + var headerPrimaryWidth = Math.Clamp(headerWidth * 0.34, 28, 92); + var headerSecondaryWidth = Math.Clamp(headerWidth * 0.52, 40, 148); + var dateHeight = Math.Clamp(headerHeight * 0.30, 26, 96); + var secondaryHeaderHeight = Math.Clamp(headerHeight * 0.12, 16, 42); + var weekdayLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + FormatWeekday(sampleNow.DayOfWeek), + headerSecondaryWidth, + secondaryHeaderHeight, + minLines: 1, + maxLines: 1, + minFontSize: 13, + maxFontSize: 32, + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Medium }, + lineHeightFactor: 1.02d); + var classCountLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + FormatClassCount(_courseItems.Count), + headerSecondaryWidth, + secondaryHeaderHeight, + minLines: 1, + maxLines: 1, + minFontSize: 14, + maxFontSize: 36, + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Medium }, + lineHeightFactor: 1.02d); + var statusText = string.IsNullOrWhiteSpace(StatusTextBlock.Text) + ? L("schedule.widget.no_class_today", "浠婂ぉ娌℃湁璇剧▼") + : StatusTextBlock.Text; + var statusLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + statusText, + headerSecondaryWidth, + secondaryHeaderHeight, + minLines: 1, + maxLines: 1, + minFontSize: 12, + maxFontSize: 30, + weightCandidates: new[] { FontWeight.Medium, FontWeight.Normal }, + lineHeightFactor: 1.02d); + var monthLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + sampleNow.Month.ToString(CultureInfo.InvariantCulture), + headerPrimaryWidth, + dateHeight, + minLines: 1, + maxLines: 1, + minFontSize: 26, + maxFontSize: 82, + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Medium }, + lineHeightFactor: 1d); + var dayLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + sampleNow.Day.ToString(CultureInfo.InvariantCulture), + headerPrimaryWidth, + dateHeight, + minLines: 1, + maxLines: 1, + minFontSize: 26, + maxFontSize: 82, + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Medium }, + lineHeightFactor: 1d); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.45, 24, 44); RootBorder.Background = _isNightVisual @@ -620,21 +740,21 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, RootBorder.BorderBrush = CreateBrush(_isNightVisual ? "#24FFFFFF" : "#15000000"); var rootPadding = new Thickness( - ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 10, 24), - ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 9, 20), - ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 10, 24), - ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 20)); + ComponentChromeCornerRadiusHelper.SafeValue(16 * _layoutScale, 10, 24), + ComponentChromeCornerRadiusHelper.SafeValue(14 * _layoutScale, 9, 20), + ComponentChromeCornerRadiusHelper.SafeValue(16 * _layoutScale, 10, 24), + ComponentChromeCornerRadiusHelper.SafeValue(14 * _layoutScale, 8, 20)); RootBorder.Padding = rootPadding; - LayoutGrid.RowSpacing = Math.Clamp(14 * scale, 6, 20); - HeaderGrid.ColumnSpacing = Math.Clamp(10 * scale, 4, 16); - DateGroup.Spacing = Math.Clamp(1.5 * scale, 0.5, 3); - MetaStack.Spacing = Math.Clamp(6 * scale, 3, 10); - CourseListPanel.Spacing = Math.Clamp(6 * scale, 3, 10); + LayoutGrid.RowSpacing = Math.Clamp(14 * _layoutScale, 6, 20); + HeaderGrid.ColumnSpacing = Math.Clamp(10 * _layoutScale, 4, 16); + DateGroup.Spacing = Math.Clamp(1.5 * _layoutScale, 0.5, 3); + MetaStack.Spacing = Math.Clamp(6 * _layoutScale, 3, 10); + CourseListPanel.Spacing = Math.Clamp(6 * _layoutScale, 3, 10); - var dateFont = Math.Clamp(66 * scale, 26, 82); - MonthTextBlock.FontSize = dateFont; - DayTextBlock.FontSize = dateFont; + var dateFont = Math.Clamp(66 * _layoutScale, 26, 82); + MonthTextBlock.FontSize = monthLayout.FontSize; + DayTextBlock.FontSize = dayLayout.FontSize; SlashTextBlock.FontSize = dateFont; MonthTextBlock.Foreground = CreateBrush(_isNightVisual ? "#F8FAFF" : "#131722"); @@ -644,12 +764,13 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, ClassCountTextBlock.Foreground = CreateBrush(_isNightVisual ? "#8D95A4" : "#738095"); StatusTextBlock.Foreground = CreateBrush(_isNightVisual ? "#9AA2B1" : "#4B5565"); - WeekdayTextBlock.FontSize = Math.Clamp(34 * scale, 13, 32); - ClassCountTextBlock.FontSize = Math.Clamp(40 * scale, 14, 36); - StatusTextBlock.FontSize = Math.Clamp(30 * scale, 12, 30); + WeekdayTextBlock.FontSize = weekdayLayout.FontSize; + ClassCountTextBlock.FontSize = classCountLayout.FontSize; + StatusTextBlock.FontSize = statusLayout.FontSize; - WeekdayTextBlock.FontWeight = ToVariableWeight(Lerp(560, 700, Math.Clamp((scale - 0.60) / 1.2, 0, 1))); - ClassCountTextBlock.FontWeight = ToVariableWeight(Lerp(560, 680, Math.Clamp((scale - 0.60) / 1.2, 0, 1))); + WeekdayTextBlock.FontWeight = weekdayLayout.Weight; + ClassCountTextBlock.FontWeight = classCountLayout.Weight; + StatusTextBlock.FontWeight = statusLayout.Weight; } private static string FormatTime(TimeSpan time) diff --git a/LanMountainDesktop/Views/Components/ClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/ClockWidget.axaml.cs index bbfb2e1..678fa67 100644 --- a/LanMountainDesktop/Views/Components/ClockWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/ClockWidget.axaml.cs @@ -4,6 +4,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Services; namespace LanMountainDesktop.Views.Components; @@ -120,11 +121,13 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo SecondsTextBlock.Text = now.ToString("ss", CultureInfo.CurrentCulture); SecondsTextBlock.IsVisible = _displayFormat == ClockDisplayFormat.HourMinuteSecond; + ApplyTypographyLayout(); } public void ApplyCellSize(double cellSize) { _lastAppliedCellSize = cellSize; + var layoutScale = Math.Clamp((cellSize / 44d) * (0.9d + (ComponentChromeCornerRadiusHelper.ResolveScale() * 0.1d)), 0.65d, 1.95d); // --- Class Island “满盈”风格算法 --- @@ -138,7 +141,7 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo // 3. 核心:满盈字阶 (Filled Typography) // 使主时间文字占据容器高度的 ~68%,产生饱满的视觉张力 - var mainFontSize = targetHeight * 0.68; + var mainFontSize = targetHeight * 0.68 * layoutScale; MainTimeTextBlock.FontSize = mainFontSize; MainTimeTextBlock.FontWeight = FontWeight.SemiBold; @@ -152,19 +155,74 @@ public partial class ClockWidget : UserControl, IDesktopComponentWidget, ITimeZo // 6. 间距微调 if (MainTimeTextBlock.Parent is StackPanel panel) { - panel.Spacing = Math.Clamp(cellSize * 0.06, 2, 8); + panel.Spacing = Math.Clamp(cellSize * 0.06 * layoutScale, 2, 8); } if (_transparentBackground) { RootBorder.MinWidth = 0; - RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 4, 10), 0); + RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.06 * layoutScale, 4, 10), 0); return; } // 确保清除可能存在的固定 Padding,由代码控制“紧密感” RootBorder.MinWidth = cellSize * 2.2; - RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.15, 12, 24), 0); + RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.15 * layoutScale, 12, 24), 0); + ApplyTypographyLayout(); + } + + private void ApplyTypographyLayout() + { + var layoutScale = Math.Clamp((_lastAppliedCellSize / 44d) * (0.9d + (ComponentChromeCornerRadiusHelper.ResolveScale() * 0.1d)), 0.65d, 1.95d); + var availableWidth = Math.Max(1, RootBorder.Bounds.Width > 1 ? RootBorder.Bounds.Width : Math.Max(1, _lastAppliedCellSize * 2.2)); + var availableHeight = Math.Max(1, RootBorder.Bounds.Height > 1 ? RootBorder.Bounds.Height : Math.Clamp(_lastAppliedCellSize * 0.74, 34, 74)); + var contentWidth = Math.Max(1, availableWidth - RootBorder.Padding.Left - RootBorder.Padding.Right); + var contentHeight = Math.Max(1, availableHeight - RootBorder.Padding.Top - RootBorder.Padding.Bottom); + + var mainLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + MainTimeTextBlock.Text, + contentWidth * (SecondsTextBlock.IsVisible ? 0.78d : 0.84d), + contentHeight * 0.80d, + minLines: 1, + maxLines: 1, + minFontSize: Math.Clamp(18 * layoutScale, 16, 28), + maxFontSize: Math.Clamp(44 * layoutScale, 28, 64), + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Medium }, + lineHeightFactor: 0.96d); + MainTimeTextBlock.FontSize = mainLayout.FontSize; + MainTimeTextBlock.FontWeight = mainLayout.Weight; + + if (SecondsTextBlock.IsVisible) + { + var secondsLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + SecondsTextBlock.Text, + contentWidth * 0.28d, + contentHeight * 0.46d, + minLines: 1, + maxLines: 1, + minFontSize: Math.Clamp(11 * layoutScale, 9, 18), + maxFontSize: Math.Clamp(28 * layoutScale, 14, 34), + weightCandidates: new[] { FontWeight.Medium, FontWeight.Normal }, + lineHeightFactor: 0.96d); + SecondsTextBlock.FontSize = secondsLayout.FontSize; + SecondsTextBlock.FontWeight = secondsLayout.Weight; + SecondsTextBlock.Opacity = 0.55; + } + + if (MainTimeTextBlock.Parent is StackPanel panel) + { + panel.Spacing = Math.Clamp(contentHeight * 0.06 * layoutScale, 2, 8); + } + + if (_transparentBackground) + { + RootBorder.MinWidth = 0; + RootBorder.Padding = new Thickness(Math.Clamp(_lastAppliedCellSize * 0.06 * layoutScale, 4, 10), 0); + return; + } + + RootBorder.MinWidth = _lastAppliedCellSize * 2.2; + RootBorder.Padding = new Thickness(Math.Clamp(_lastAppliedCellSize * 0.15 * layoutScale, 12, 24), 0); } private void ApplyChrome() diff --git a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs index 678cc54..6c3bf49 100644 --- a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs @@ -16,6 +16,7 @@ using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -599,17 +600,40 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, News2TitleTextBlock.MaxWidth = availableTextWidth; var newsFont = Math.Clamp(21 * scale, 10.5, 28); - News1TitleTextBlock.FontSize = newsFont; - News2TitleTextBlock.FontSize = newsFont; - var mainNewsLineHeight = newsFont * 1.14; - News1TitleTextBlock.LineHeight = mainNewsLineHeight; - News2TitleTextBlock.LineHeight = mainNewsLineHeight; - var mainNewsMinHeight = mainNewsLineHeight * 2; - News1TitleTextBlock.MinHeight = mainNewsMinHeight; - News2TitleTextBlock.MinHeight = mainNewsMinHeight; + var newsHeightBudget = Math.Max(28, imageHeight + columnGap * 2d); + var news1Layout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + News1TitleTextBlock.Text, + availableTextWidth, + newsHeightBudget, + minLines: 1, + maxLines: ComponentTypographyLayoutService.CountTextDisplayUnits(News1TitleTextBlock.Text) > 30 ? 2 : 1, + minFontSize: Math.Clamp(newsFont * 0.72, 10.5, 18), + maxFontSize: newsFont, + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Bold }, + lineHeightFactor: 1.14d, + fontFamily: MiSansFontFamily); + var news2Layout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + News2TitleTextBlock.Text, + availableTextWidth, + newsHeightBudget, + minLines: 1, + maxLines: ComponentTypographyLayoutService.CountTextDisplayUnits(News2TitleTextBlock.Text) > 30 ? 2 : 1, + minFontSize: Math.Clamp(newsFont * 0.72, 10.5, 18), + maxFontSize: newsFont, + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Bold }, + lineHeightFactor: 1.14d, + fontFamily: MiSansFontFamily); + News1TitleTextBlock.FontSize = news1Layout.FontSize; + News1TitleTextBlock.LineHeight = news1Layout.LineHeight; + News1TitleTextBlock.MinHeight = news1Layout.LineHeight * news1Layout.MaxLines; + News1TitleTextBlock.MaxLines = news1Layout.MaxLines; + News1TitleTextBlock.FontWeight = news1Layout.Weight; + News2TitleTextBlock.FontSize = news2Layout.FontSize; + News2TitleTextBlock.LineHeight = news2Layout.LineHeight; + News2TitleTextBlock.MinHeight = news2Layout.LineHeight * news2Layout.MaxLines; + News2TitleTextBlock.MaxLines = news2Layout.MaxLines; + News2TitleTextBlock.FontWeight = news2Layout.Weight; StatusTextBlock.FontSize = Math.Clamp(16 * scale, 9, 24); - News1TitleTextBlock.MaxLines = 2; - News2TitleTextBlock.MaxLines = 2; foreach (var row in _extraNewsRows) { @@ -623,11 +647,23 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, row.ImageHost.Height = imageHeight; row.ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22); + var rowTitleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + row.TitleTextBlock.Text, + availableTextWidth, + Math.Max(32, imageHeight + columnGap), + minLines: 1, + maxLines: ComponentTypographyLayoutService.CountTextDisplayUnits(row.TitleTextBlock.Text) > 28 ? 2 : 1, + minFontSize: Math.Clamp(19 * scale, 10, 16), + maxFontSize: Math.Clamp(19 * scale, 10, 25), + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Bold }, + lineHeightFactor: 1.12d, + fontFamily: MiSansFontFamily); row.TitleTextBlock.MaxWidth = availableTextWidth; - row.TitleTextBlock.FontSize = Math.Clamp(19 * scale, 10, 25); - row.TitleTextBlock.LineHeight = row.TitleTextBlock.FontSize * 1.12; - row.TitleTextBlock.MinHeight = row.TitleTextBlock.LineHeight * 2; - row.TitleTextBlock.MaxLines = 2; + row.TitleTextBlock.FontSize = rowTitleLayout.FontSize; + row.TitleTextBlock.LineHeight = rowTitleLayout.LineHeight; + row.TitleTextBlock.MinHeight = rowTitleLayout.LineHeight * rowTitleLayout.MaxLines; + row.TitleTextBlock.MaxLines = rowTitleLayout.MaxLines; + row.TitleTextBlock.FontWeight = rowTitleLayout.Weight; } ExtraNewsItemsPanel.Spacing = Math.Clamp(6 * scale, 3, 10); diff --git a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs index 8e59121..0447a8a 100644 --- a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -13,6 +13,7 @@ using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; @@ -26,13 +27,13 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, private static readonly IReadOnlyDictionary ZhWeekdays = new Dictionary { - [DayOfWeek.Monday] = "星期一", - [DayOfWeek.Tuesday] = "星期二", - [DayOfWeek.Wednesday] = "星期三", - [DayOfWeek.Thursday] = "星期四", - [DayOfWeek.Friday] = "星期五", - [DayOfWeek.Saturday] = "星期六", - [DayOfWeek.Sunday] = "星期日" + [DayOfWeek.Monday] = "һ", + [DayOfWeek.Tuesday] = "ڶ", + [DayOfWeek.Wednesday] = "", + [DayOfWeek.Thursday] = "", + [DayOfWeek.Friday] = "", + [DayOfWeek.Saturday] = "", + [DayOfWeek.Sunday] = "" }; private static readonly Regex MultiWhitespaceRegex = new(@"\s+", RegexOptions.Compiled); @@ -449,7 +450,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, var leftSingleLineHeight = Math.Max(12, (leftContentHeight - dateStackSpacing) / 2d); var dateBase = Math.Clamp(44 * scale, 16, 62); - DateTextBlock.FontSize = FitFontSize( + DateTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( DateTextBlock.Text, leftContentWidth, leftSingleLineHeight, @@ -457,10 +458,11 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, minFontSize: Math.Max(12, dateBase * 0.68), maxFontSize: dateBase, weight: FontWeight.Bold, - lineHeightFactor: 1.10); + lineHeightFactor: 1.10, + fontFamily: MiSansFontFamily); DateTextBlock.LineHeight = DateTextBlock.FontSize * 1.10; - WeekdayTextBlock.FontSize = FitFontSize( + WeekdayTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( WeekdayTextBlock.Text, leftContentWidth, leftSingleLineHeight, @@ -468,7 +470,8 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, minFontSize: Math.Max(12, dateBase * 0.68), maxFontSize: dateBase, weight: FontWeight.Bold, - lineHeightFactor: 1.10); + lineHeightFactor: 1.10, + fontFamily: MiSansFontFamily); WeekdayTextBlock.LineHeight = WeekdayTextBlock.FontSize * 1.10; var rightContentHeight = Math.Max(42, totalHeight - rootPadding.Top - rootPadding.Bottom - InfoPanel.Padding.Top - InfoPanel.Padding.Bottom); @@ -516,7 +519,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, yearHeightBudget = minYearHeight + extraHeight * (yearWeight / weightSum); } - var titleLayout = FitAdaptiveTextLayout( + var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( PaintingTitleTextBlock.Text, rightContentWidth, titleHeightBudget, @@ -525,7 +528,8 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, minFontSize: titleMin, maxFontSize: titleBase, weightCandidates: TitleWeightCandidates, - lineHeightFactor: 1.10); + lineHeightFactor: 1.10, + fontFamily: MiSansFontFamily); PaintingTitleTextBlock.MaxWidth = rightContentWidth; PaintingTitleTextBlock.Margin = new Thickness(0, 0, 0, titleBottomMargin); PaintingTitleTextBlock.MaxLines = titleLayout.MaxLines; @@ -538,7 +542,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, artistInfoStack.Spacing = bottomStackSpacing; } - var artistLayout = FitAdaptiveTextLayout( + var artistLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( ArtistTextBlock.Text, rightContentWidth, artistHeightBudget, @@ -547,14 +551,15 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, minFontSize: artistMin, maxFontSize: artistBase, weightCandidates: ArtistWeightCandidates, - lineHeightFactor: 1.14); + lineHeightFactor: 1.14, + fontFamily: MiSansFontFamily); ArtistTextBlock.MaxWidth = rightContentWidth; ArtistTextBlock.MaxLines = artistLayout.MaxLines; ArtistTextBlock.FontWeight = artistLayout.Weight; ArtistTextBlock.FontSize = artistLayout.FontSize; ArtistTextBlock.LineHeight = artistLayout.LineHeight; - var yearLayout = FitAdaptiveTextLayout( + var yearLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( YearTextBlock.Text, rightContentWidth, yearHeightBudget, @@ -563,7 +568,8 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, minFontSize: yearMin, maxFontSize: yearBase, weightCandidates: SecondaryWeightCandidates, - lineHeightFactor: 1.08); + lineHeightFactor: 1.08, + fontFamily: MiSansFontFamily); YearTextBlock.MaxWidth = rightContentWidth; YearTextBlock.MaxLines = yearLayout.MaxLines; YearTextBlock.FontWeight = yearLayout.Weight; @@ -717,7 +723,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, normalized = "Untitled"; } - return $"“{normalized}”"; + return $"{normalized}"; } private void CancelRefreshRequest() @@ -771,222 +777,4 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, return MultiWhitespaceRegex.Replace(text.Trim(), " "); } - private static double FitFontSize( - string? text, - double maxWidth, - double maxHeight, - int maxLines, - double minFontSize, - double maxFontSize, - FontWeight weight, - double lineHeightFactor) - { - var content = string.IsNullOrWhiteSpace(text) ? " " : text.Trim(); - var min = Math.Max(6, minFontSize); - var max = Math.Max(min, maxFontSize); - var low = min; - var high = max; - var best = min; - - for (var i = 0; i < 18; i++) - { - var candidate = (low + high) / 2d; - var lineHeight = candidate * lineHeightFactor; - var size = MeasureTextSize(content, candidate, weight, Math.Max(1, maxWidth), lineHeight); - var lineCount = Math.Max(1, (int)Math.Ceiling(size.Height / Math.Max(1, lineHeight))); - var fits = size.Height <= maxHeight + 0.6 && lineCount <= Math.Max(1, maxLines); - - if (fits) - { - best = candidate; - low = candidate; - } - else - { - high = candidate; - } - } - - return best; - } - - private static AdaptiveTextLayout FitAdaptiveTextLayout( - string? text, - double maxWidth, - double maxHeight, - int minLines, - int maxLines, - double minFontSize, - double maxFontSize, - FontWeight[] weightCandidates, - double lineHeightFactor) - { - var content = string.IsNullOrWhiteSpace(text) ? " " : text.Trim(); - var safeMinLines = Math.Max(1, minLines); - var safeMaxLines = Math.Max(safeMinLines, maxLines); - var linesByHeight = ResolveMaxLinesByHeight(maxHeight, minFontSize, lineHeightFactor, safeMinLines, safeMaxLines); - - var candidates = weightCandidates is { Length: > 0 } - ? weightCandidates - : new[] { FontWeight.Normal }; - - AdaptiveTextLayout? best = null; - foreach (var weight in candidates) - { - for (var lineLimit = linesByHeight; lineLimit >= safeMinLines; lineLimit--) - { - var fontSize = FitFontSize( - content, - maxWidth, - maxHeight, - lineLimit, - minFontSize, - maxFontSize, - weight, - lineHeightFactor); - var lineHeight = fontSize * lineHeightFactor; - var measuredSize = MeasureTextSize(content, fontSize, weight, Math.Max(1, maxWidth), lineHeight); - var measuredLineCount = ResolveLineCount(measuredSize.Height, lineHeight); - var overflowLines = Math.Max(0, measuredLineCount - lineLimit); - var overflowHeight = Math.Max(0, measuredSize.Height - maxHeight); - var overflowScore = overflowLines * 1000d + overflowHeight; - var fitsCompletely = overflowLines == 0 && overflowHeight <= 0.6; - var candidate = new AdaptiveTextLayout(fontSize, weight, lineLimit, lineHeight, overflowScore, fitsCompletely); - - if (best is null || IsBetterAdaptiveTextCandidate(candidate, best.Value)) - { - best = candidate; - } - } - } - - if (best is not null) - { - return best.Value; - } - - var fallbackFontSize = Math.Max(6, minFontSize); - return new AdaptiveTextLayout( - fallbackFontSize, - FontWeight.Normal, - safeMinLines, - fallbackFontSize * lineHeightFactor, - double.MaxValue, - fitsCompletely: false); - } - - private static bool IsBetterAdaptiveTextCandidate(AdaptiveTextLayout candidate, AdaptiveTextLayout best) - { - if (candidate.FitsCompletely && !best.FitsCompletely) - { - return true; - } - - if (!candidate.FitsCompletely && best.FitsCompletely) - { - return false; - } - - if (candidate.FitsCompletely && best.FitsCompletely) - { - if (candidate.FontSize > best.FontSize + 0.12) - { - return true; - } - - if (Math.Abs(candidate.FontSize - best.FontSize) <= 0.12 && candidate.MaxLines < best.MaxLines) - { - return true; - } - - return false; - } - - if (candidate.OverflowScore < best.OverflowScore - 0.2) - { - return true; - } - - if (Math.Abs(candidate.OverflowScore - best.OverflowScore) <= 0.2 && - candidate.FontSize > best.FontSize + 0.12) - { - return true; - } - - if (Math.Abs(candidate.OverflowScore - best.OverflowScore) <= 0.2 && - Math.Abs(candidate.FontSize - best.FontSize) <= 0.12 && - candidate.MaxLines > best.MaxLines) - { - return true; - } - - return false; - } - - private static int ResolveMaxLinesByHeight( - double maxHeight, - double minFontSize, - double lineHeightFactor, - int minLines, - int maxLines) - { - var safeMinLines = Math.Max(1, minLines); - var safeMaxLines = Math.Max(safeMinLines, maxLines); - var lineHeight = Math.Max(1, Math.Max(6, minFontSize) * lineHeightFactor); - var maxHeightWithTolerance = Math.Max(1, maxHeight + 0.6); - var linesByHeight = (int)Math.Floor(maxHeightWithTolerance / lineHeight); - return Math.Clamp(linesByHeight, safeMinLines, safeMaxLines); - } - - private static int ResolveLineCount(double measuredHeight, double lineHeight) - { - return Math.Max(1, (int)Math.Ceiling(measuredHeight / Math.Max(1, lineHeight))); - } - - private readonly struct AdaptiveTextLayout - { - public AdaptiveTextLayout( - double fontSize, - FontWeight weight, - int maxLines, - double lineHeight, - double overflowScore, - bool fitsCompletely) - { - FontSize = fontSize; - Weight = weight; - MaxLines = Math.Max(1, maxLines); - LineHeight = lineHeight; - OverflowScore = overflowScore; - FitsCompletely = fitsCompletely; - } - - public double FontSize { get; } - - public FontWeight Weight { get; } - - public int MaxLines { get; } - - public double LineHeight { get; } - - public double OverflowScore { get; } - - public bool FitsCompletely { get; } - } - - private static Size MeasureTextSize(string text, double fontSize, FontWeight weight, double maxWidth, double lineHeight) - { - var probe = new TextBlock - { - Text = text, - FontFamily = MiSansFontFamily, - FontSize = fontSize, - FontWeight = weight, - TextWrapping = TextWrapping.Wrap, - LineHeight = lineHeight - }; - - probe.Measure(new Size(Math.Max(1, maxWidth), double.PositiveInfinity)); - return probe.DesiredSize; - } } diff --git a/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs index 8537497..2a410be 100644 --- a/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -11,6 +11,7 @@ using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -48,8 +49,6 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I private const double MinPoetryFontSize = 8; private const double MinAuthorFontSize = 7; - private readonly record struct TextFitResult(double FontSize, FontWeight FontWeight, double LineHeight); - private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromHours(6) @@ -268,7 +267,7 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I { if (!string.IsNullOrWhiteSpace(snapshot.Origin)) { - return $"{snapshot.Origin.Trim()} \u00B7 {snapshot.Author.Trim()}"; + return $"{snapshot.Origin.Trim()} · {snapshot.Author.Trim()}"; } return snapshot.Author.Trim(); @@ -297,7 +296,7 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I private void ApplyLoadingState() { _poetryRawText = L("poetry.widget.loading_content", "Loading..."); - _authorRawText = L("poetry.widget.loading_author", "..."); + _authorRawText = L("poetry.widget.loading_author", "·"); StatusTextBlock.IsVisible = false; ApplyModeVisualIfNeeded(force: true); } @@ -484,23 +483,29 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I minFontWeight: ToVariableWeight(poemMinWeight), lineHeightFactor: 1.12); - var poemFit = FitTextStable( + var poemFit = ComponentTypographyLayoutService.FitAdaptiveTextLayout( poemPrepared, poemWidth, availablePoemHeight, minFontSize: poemMinFontSize, maxFontSize: Math.Clamp(poemPreferredFontSize * 1.20, poemMinFontSize, 62), + minLines: poemMaxLines, maxLines: poemMaxLines, + weightCandidates: new[] + { + ToVariableWeight(poemMinWeight), + ToVariableWeight((poemMinWeight + poemMaxWeight) / 2d), + ToVariableWeight(poemMaxWeight) + }, lineHeightFactor: 1.12, - minWeight: poemMinWeight, - maxWeight: poemMaxWeight); + fontFamily: MiSansFontFamily); PoetryContentTextBlock.Text = poemPrepared; PoetryContentTextBlock.MaxWidth = poemWidth; PoetryContentTextBlock.MaxLines = poemMaxLines; PoetryContentTextBlock.FontSize = poemFit.FontSize; PoetryContentTextBlock.LineHeight = poemFit.LineHeight; - PoetryContentTextBlock.FontWeight = poemFit.FontWeight; + PoetryContentTextBlock.FontWeight = poemFit.Weight; var authorWidth = Math.Max(72, Math.Min(innerWidth * (isNightMode ? 0.5 : 0.56), innerWidth - 8)); var authorUnitsTarget = 20; @@ -520,16 +525,22 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I minFontWeight: ToVariableWeight(authorMinWeight), lineHeightFactor: 1.12); - var authorFit = FitTextStable( + var authorFit = ComponentTypographyLayoutService.FitAdaptiveTextLayout( authorPrepared, authorWidth, AuthorAccent.Height, minFontSize: authorMinFontSize, maxFontSize: Math.Clamp(authorPreferredFontSize * 1.15, authorMinFontSize, 42), + minLines: 1, maxLines: 1, + weightCandidates: new[] + { + ToVariableWeight(authorMinWeight), + ToVariableWeight((authorMinWeight + authorMaxWeight) / 2d), + ToVariableWeight(authorMaxWeight) + }, lineHeightFactor: 1.12, - minWeight: authorMinWeight, - maxWeight: authorMaxWeight); + fontFamily: MiSansFontFamily); AuthorTextBlock.Text = authorPrepared; AuthorTextBlock.TextWrapping = TextWrapping.NoWrap; @@ -537,7 +548,7 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I AuthorTextBlock.MaxWidth = authorWidth; AuthorTextBlock.FontSize = authorFit.FontSize; AuthorTextBlock.LineHeight = authorFit.LineHeight; - AuthorTextBlock.FontWeight = authorFit.FontWeight; + AuthorTextBlock.FontWeight = authorFit.Weight; } private void UpdateRefreshButtonState() @@ -691,7 +702,7 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I if (current.Length == 0) { - if (EstimateDisplayUnits(remain) <= target || lines.Count == lineLimit - 1) + if (ComponentTypographyLayoutService.CountTextDisplayUnits(remain) <= target || lines.Count == lineLimit - 1) { current.Append(remain); remain = string.Empty; @@ -714,7 +725,7 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I } var merged = current + remain; - if (EstimateDisplayUnits(merged) <= target || lines.Count == lineLimit - 1) + if (ComponentTypographyLayoutService.CountTextDisplayUnits(merged) <= target || lines.Count == lineLimit - 1) { current.Append(remain); remain = string.Empty; @@ -848,7 +859,13 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I } var lineHeight = fontSize * lineHeightFactor; - var measured = MeasureTextSize(text, fontSize, fontWeight, maxWidth, lineHeight); + var measured = ComponentTypographyLayoutService.MeasureTextSize( + text, + fontSize, + fontWeight, + maxWidth, + lineHeight, + MiSansFontFamily); var lineCount = Math.Max(1, (int)Math.Ceiling(measured.Height / Math.Max(1, lineHeight))); return measured.Height <= maxHeight + 0.6 && lineCount <= Math.Max(1, maxLines); } @@ -891,86 +908,6 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I return text.Length; } - private static double EstimateDisplayUnits(string text) - { - var units = 0d; - foreach (var ch in text) - { - units += ch <= 127 ? 0.56 : 1d; - } - - return units; - } - - private static TextFitResult FitTextStable( - string? text, - double maxWidth, - double maxHeight, - double minFontSize, - double maxFontSize, - int maxLines, - double lineHeightFactor, - double minWeight, - double maxWeight) - { - var normalizedText = string.IsNullOrWhiteSpace(text) ? " " : text.Trim(); - var min = Math.Max(6, minFontSize); - var max = Math.Max(min, maxFontSize); - var low = min; - var high = max; - - var bestSize = min; - var bestWeight = ToVariableWeight(minWeight); - - for (var i = 0; i < 22; i++) - { - var candidate = (low + high) / 2d; - var progress = max <= min - ? 0 - : Math.Clamp((candidate - min) / (max - min), 0, 1); - var candidateWeight = ToVariableWeight(Lerp(minWeight, maxWeight, progress)); - var lineHeight = candidate * lineHeightFactor; - - var measured = MeasureTextSize(normalizedText, candidate, candidateWeight, Math.Max(1, maxWidth), lineHeight); - var lineCount = Math.Max(1, (int)Math.Ceiling(measured.Height / Math.Max(1, lineHeight))); - var fits = measured.Height <= maxHeight + 0.6 && lineCount <= Math.Max(1, maxLines); - - if (fits) - { - bestSize = candidate; - bestWeight = candidateWeight; - low = candidate; - } - else - { - high = candidate; - } - } - - var lineHeightResult = bestSize * lineHeightFactor; - return new TextFitResult(bestSize, bestWeight, lineHeightResult); - } - - private static Size MeasureTextSize( - string text, - double fontSize, - FontWeight fontWeight, - double maxWidth, - double lineHeight) - { - var probe = new TextBlock - { - Text = text, - FontFamily = MiSansFontFamily, - FontSize = fontSize, - FontWeight = fontWeight, - TextWrapping = TextWrapping.Wrap, - LineHeight = lineHeight - }; - - probe.Measure(new Size(Math.Max(1, maxWidth), double.PositiveInfinity)); - return probe.DesiredSize; - } private static FontWeight ToVariableWeight(double weight) { diff --git a/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs b/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs index 1ebad06..0d1187a 100644 --- a/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs @@ -9,6 +9,7 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Services; namespace LanMountainDesktop.Views.Components; @@ -74,6 +75,7 @@ public partial class ExchangeRateCalculatorWidget : UserControl, IDesktopCompone UpdateCurrencyLabels(); UpdateAmounts(); ApplyLoadingState(); + UpdateTypography(); } public void ApplyCellSize(double cellSize) @@ -82,6 +84,7 @@ public partial class ExchangeRateCalculatorWidget : UserControl, IDesktopCompone var scale = ResolveScale(); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 14, 48); RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(12 * scale, 12 * scale, null, 0.55d); + UpdateTypography(); } public void SetRecommendationInfoService(IRecommendationInfoService recommendationInfoService) @@ -243,6 +246,7 @@ public partial class ExchangeRateCalculatorWidget : UserControl, IDesktopCompone FromCurrencyNameTextBlock.Text = IsZh() ? from.ZhName : from.EnName; ToCurrencyCodeTextBlock.Text = to.Code; ToCurrencyNameTextBlock.Text = IsZh() ? to.ZhName : to.EnName; + UpdateTypography(); } private void UpdateAmounts() @@ -258,6 +262,7 @@ public partial class ExchangeRateCalculatorWidget : UserControl, IDesktopCompone _fromCurrency, _calculatorDataService.FormatAmount(_currentRate, maxFractionDigits: 6), _toCurrency); + UpdateTypography(); } private void ApplyLoadingState() @@ -333,6 +338,103 @@ public partial class ExchangeRateCalculatorWidget : UserControl, IDesktopCompone return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.72, 1.95); } + private void UpdateTypography() + { + var layoutWidth = LayoutRoot.Width > 1 ? LayoutRoot.Width : 304d; + var layoutHeight = LayoutRoot.Height > 1 ? LayoutRoot.Height : 304d; + + var fromStackWidth = Math.Max(72, (layoutWidth - 86) * 0.36); + var amountWidth = Math.Max(116, (layoutWidth - 86) * 0.50); + var rateWidth = Math.Max(120, layoutWidth - 24); + var statusWidth = Math.Max(120, layoutWidth - 24); + + FromCurrencyCodeTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + FromCurrencyCodeTextBlock.Text, + fromStackWidth, + 26, + 1, + 11, + 20, + FontWeight.SemiBold, + 1.06d, + MiSansFontFamily); + FromCurrencyNameTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + FromCurrencyNameTextBlock.Text, + fromStackWidth, + 18, + 1, + 9, + 14, + FontWeight.Normal, + 1.06d, + MiSansFontFamily); + ToCurrencyCodeTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + ToCurrencyCodeTextBlock.Text, + fromStackWidth, + 26, + 1, + 11, + 20, + FontWeight.SemiBold, + 1.06d, + MiSansFontFamily); + ToCurrencyNameTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + ToCurrencyNameTextBlock.Text, + fromStackWidth, + 18, + 1, + 9, + 14, + FontWeight.Normal, + 1.06d, + MiSansFontFamily); + + InputAmountTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + InputAmountTextBlock.Text, + amountWidth, + Math.Max(42, layoutHeight * 0.16), + 1, + 20, + 42, + FontWeight.Bold, + 1.02d, + MiSansFontFamily); + ConvertedAmountTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + ConvertedAmountTextBlock.Text, + amountWidth, + Math.Max(42, layoutHeight * 0.16), + 1, + 20, + 42, + FontWeight.Bold, + 1.02d, + MiSansFontFamily); + RateTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + RateTextBlock.Text, + rateWidth, + 20, + 1, + 10, + 15, + FontWeight.Normal, + 1.06d, + MiSansFontFamily); + + if (StatusTextBlock.IsVisible) + { + StatusTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + StatusTextBlock.Text, + statusWidth, + 22, + 1, + 10, + 16, + FontWeight.Normal, + 1.06d, + MiSansFontFamily); + } + } + private void CancelRefreshRequest() { var cts = Interlocked.Exchange(ref _refreshCts, null); diff --git a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs index 4a6e358..1ceef5b 100644 --- a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs @@ -9,6 +9,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.Models; @@ -544,34 +545,91 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge var temperatureSample = string.IsNullOrWhiteSpace(TemperatureTextBlock.Text) ? "00°" : TemperatureTextBlock.Text.Trim(); - var temperatureGlyphCount = Math.Clamp(temperatureSample.Length, 3, 6); var temperatureMaxWidth = Math.Max(30, innerWidth - iconSize - SummaryGrid.ColumnSpacing - 6); var rawTemperatureSize = Math.Clamp(Lerp(72, 102, iconGrowth) * topScale, 14, 340); - var fitTemperatureSize = temperatureMaxWidth / (temperatureGlyphCount * 0.62); - TemperatureTextBlock.FontSize = Math.Clamp(Math.Min(rawTemperatureSize, fitTemperatureSize), 10, 340); - TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 380, emphasis)); + var temperatureLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + TemperatureTextBlock.Text, + temperatureMaxWidth, + Math.Max(18, summaryHeight * 0.84), + 1, + 1, + Math.Max(10, rawTemperatureSize * 0.42), + rawTemperatureSize, + [ToVariableWeight(Lerp(300, 380, emphasis))], + 1.02); + TemperatureTextBlock.FontSize = temperatureLayout.FontSize; + TemperatureTextBlock.FontWeight = temperatureLayout.Weight; TemperatureTextBlock.MaxWidth = Math.Clamp(temperatureMaxWidth, 30, Math.Max(300, innerWidth * 0.66)); TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-2.2 * topScale, -12, 0), 0, 0); - var cityFontSize = Math.Clamp(18.5 * topScale, 7, 86); - var conditionFontSize = Math.Clamp(20 * topScale, 7, 90); - var rangeFontSize = Math.Clamp(20 * topScale, 7, 90); - CityTextBlock.FontSize = cityFontSize; - ConditionTextBlock.FontSize = conditionFontSize; - RangeTextBlock.FontSize = rangeFontSize; - CityTextBlock.FontWeight = ToVariableWeight(Lerp(530, 620, emphasis)); - ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(580, 660, emphasis)); - RangeTextBlock.FontWeight = ToVariableWeight(Lerp(600, 680, emphasis)); - CityTextBlock.LineHeight = cityFontSize * 1.08; - ConditionTextBlock.LineHeight = conditionFontSize * 1.06; - RangeTextBlock.LineHeight = rangeFontSize * 1.06; + var cityBadge = ComponentTypographyLayoutService.ResolveBadgeBox( + innerWidth * 0.36, + Math.Max(16, summaryHeight * 0.34), + preferredSizeScale: 0.28d, + minSize: 10, + maxSize: 24, + insetScale: 0.18d); + CityInfoBadge.Padding = cityBadge.Padding; + CityInfoBadge.CornerRadius = new CornerRadius(cityBadge.Size / 2d); + CityInfoBadge.MaxWidth = Math.Clamp(innerWidth * 0.36, 34, 420); + var cityLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + CityTextBlock.Text, + Math.Max(24, CityInfoBadge.MaxWidth - cityBadge.Padding.Left - cityBadge.Padding.Right), + Math.Max(12, summaryHeight * 0.36), + 1, + 1, + 6, + Math.Max(6, 18.5 * topScale), + [ToVariableWeight(Lerp(530, 620, emphasis))], + 1.08); + CityTextBlock.FontSize = cityLayout.FontSize; + CityTextBlock.FontWeight = cityLayout.Weight; + CityTextBlock.LineHeight = cityLayout.LineHeight; + CityTextBlock.MaxWidth = CityInfoBadge.MaxWidth; + + var conditionBadge = ComponentTypographyLayoutService.ResolveBadgeBox( + innerWidth * 0.25, + Math.Max(16, summaryHeight * 0.34), + preferredSizeScale: 0.26d, + minSize: 10, + maxSize: 24, + insetScale: 0.18d); + ConditionInfoBadge.Padding = conditionBadge.Padding; + ConditionInfoBadge.CornerRadius = new CornerRadius(conditionBadge.Size / 2d); + ConditionInfoBadge.MaxWidth = Math.Clamp(innerWidth * 0.25, 26, 340); + var conditionLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + ConditionTextBlock.Text, + Math.Max(24, ConditionInfoBadge.MaxWidth - conditionBadge.Padding.Left - conditionBadge.Padding.Right), + Math.Max(12, summaryHeight * 0.30), + 1, + 1, + 7, + Math.Max(6, 20 * topScale), + [ToVariableWeight(Lerp(580, 660, emphasis))], + 1.06); + var rangeLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + RangeTextBlock.Text, + Math.Max(24, ConditionInfoBadge.MaxWidth - conditionBadge.Padding.Left - conditionBadge.Padding.Right), + Math.Max(12, summaryHeight * 0.30), + 1, + 1, + 7, + Math.Max(6, 20 * topScale), + [ToVariableWeight(Lerp(600, 680, emphasis))], + 1.06); + ConditionTextBlock.FontSize = conditionLayout.FontSize; + ConditionTextBlock.FontWeight = conditionLayout.Weight; + ConditionTextBlock.LineHeight = conditionLayout.LineHeight; + RangeTextBlock.FontSize = rangeLayout.FontSize; + RangeTextBlock.FontWeight = rangeLayout.Weight; + RangeTextBlock.LineHeight = rangeLayout.LineHeight; + CityTextBlock.MaxWidth = CityInfoBadge.MaxWidth; + ConditionTextBlock.MaxWidth = ConditionInfoBadge.MaxWidth; + RangeTextBlock.MaxWidth = ConditionInfoBadge.MaxWidth; WeatherIconImage.Width = iconSize; WeatherIconImage.Height = iconSize; WeatherIconImage.Margin = new Thickness(0, Math.Clamp(-2.4 * topScale, -12, 0), 0, 0); - ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.25, 28, 340); - RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.31, 34, 380); - CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.36, 34, 420); HourlyPanelBorder.Padding = new Thickness(0); HourlyPanelBorder.CornerRadius = new CornerRadius(0); @@ -590,10 +648,30 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge var hourlyStackSpacing = Math.Clamp(2 * hourlyCellScale, 0.2, 10); for (var i = 0; i < _hourlyTempBlocks.Length; i++) { - _hourlyTempBlocks[i].FontSize = hourlyTempSize; - _hourlyTimeBlocks[i].FontSize = hourlyTimeSize; - _hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(540, 650, emphasis)); - _hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(450, 560, emphasis)); + var hourlyTempLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + _hourlyTempBlocks[i].Text, + Math.Clamp(hourlyCellWidth, 12, 260), + Math.Max(10, hourlyHeight * 0.42), + 1, + 1, + 6, + hourlyTempSize, + [ToVariableWeight(Lerp(540, 650, emphasis))], + 1.02); + var hourlyTimeLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + _hourlyTimeBlocks[i].Text, + Math.Clamp(hourlyCellWidth, 12, 260), + Math.Max(10, hourlyHeight * 0.34), + 1, + 1, + 6, + hourlyTimeSize, + [ToVariableWeight(Lerp(450, 560, emphasis))], + 1.02); + _hourlyTempBlocks[i].FontSize = hourlyTempLayout.FontSize; + _hourlyTimeBlocks[i].FontSize = hourlyTimeLayout.FontSize; + _hourlyTempBlocks[i].FontWeight = hourlyTempLayout.Weight; + _hourlyTimeBlocks[i].FontWeight = hourlyTimeLayout.Weight; _hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 260); _hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 260); _hourlyIconBlocks[i].Width = hourlyIconSize; @@ -620,12 +698,42 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge var dailyHighRightGap = Math.Clamp(innerWidth * 0.018, 1, 28); for (var i = 0; i < _dailyLabelBlocks.Length; i++) { - _dailyLabelBlocks[i].FontSize = dailyLabelSize; - _dailyHighBlocks[i].FontSize = dailyTempSize; - _dailyLowBlocks[i].FontSize = dailyTempSize; - _dailyLabelBlocks[i].FontWeight = ToVariableWeight(Lerp(520, 620, emphasis)); - _dailyHighBlocks[i].FontWeight = ToVariableWeight(Lerp(560, 680, emphasis)); - _dailyLowBlocks[i].FontWeight = ToVariableWeight(Lerp(470, 590, emphasis)); + var dailyLabelLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + _dailyLabelBlocks[i].Text, + dailyLabelMaxWidth, + Math.Max(10, dailyRowHeight * 0.50), + 1, + 1, + 6, + dailyLabelSize, + [ToVariableWeight(Lerp(520, 620, emphasis))], + 1.04); + var dailyHighLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + _dailyHighBlocks[i].Text, + dailyHighWidth, + Math.Max(10, dailyRowHeight * 0.42), + 1, + 1, + 6, + dailyTempSize, + [ToVariableWeight(Lerp(560, 680, emphasis))], + 1.02); + var dailyLowLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + _dailyLowBlocks[i].Text, + dailyLowWidth, + Math.Max(10, dailyRowHeight * 0.42), + 1, + 1, + 6, + dailyTempSize, + [ToVariableWeight(Lerp(470, 590, emphasis))], + 1.02); + _dailyLabelBlocks[i].FontSize = dailyLabelLayout.FontSize; + _dailyHighBlocks[i].FontSize = dailyHighLayout.FontSize; + _dailyLowBlocks[i].FontSize = dailyLowLayout.FontSize; + _dailyLabelBlocks[i].FontWeight = dailyLabelLayout.Weight; + _dailyHighBlocks[i].FontWeight = dailyHighLayout.Weight; + _dailyLowBlocks[i].FontWeight = dailyLowLayout.Weight; _dailyLabelBlocks[i].MaxWidth = dailyLabelMaxWidth; _dailyHighBlocks[i].Width = dailyHighWidth; _dailyLowBlocks[i].Width = dailyLowWidth; diff --git a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs index 2873a8b..16e7070 100644 --- a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs @@ -11,6 +11,7 @@ using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.Models; @@ -1271,31 +1272,88 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget, var temperatureSample = string.IsNullOrWhiteSpace(TemperatureTextBlock.Text) ? "00°" : TemperatureTextBlock.Text.Trim(); - var temperatureGlyphCount = Math.Clamp(temperatureSample.Length, 3, 6); var temperatureMaxWidth = Math.Max(28, innerWidth - iconSize - TopRowGrid.ColumnSpacing - 4); var rawTemperatureSize = Math.Clamp(Lerp(64, 92, iconGrowth) * topScale, 12, 320); - var fitTemperatureSize = temperatureMaxWidth / (temperatureGlyphCount * 0.62); - TemperatureTextBlock.FontSize = Math.Clamp(Math.Min(rawTemperatureSize, fitTemperatureSize), 9, 320); - TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 360, emphasis)); + var temperatureLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + TemperatureTextBlock.Text, + temperatureMaxWidth, + Math.Max(18, topZoneHeight * 0.84), + 1, + 1, + Math.Max(9, rawTemperatureSize * 0.42), + rawTemperatureSize, + [ToVariableWeight(Lerp(300, 360, emphasis))], + 1.02); + TemperatureTextBlock.FontSize = temperatureLayout.FontSize; + TemperatureTextBlock.FontWeight = temperatureLayout.Weight; TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-2.0 * topScale, -10, 0), 0, 0); TemperatureTextBlock.MaxWidth = Math.Clamp(temperatureMaxWidth, 28, Math.Max(280, innerWidth * 0.68)); - CityInfoBadge.Padding = new Thickness(0); - CityInfoBadge.CornerRadius = new CornerRadius(0); + var cityBadge = ComponentTypographyLayoutService.ResolveBadgeBox( + innerWidth * 0.37, + Math.Max(16, topZoneHeight * 0.34), + preferredSizeScale: 0.28d, + minSize: 10, + maxSize: 24, + insetScale: 0.18d); + CityInfoBadge.Padding = cityBadge.Padding; + CityInfoBadge.CornerRadius = new CornerRadius(cityBadge.Size / 2d); + CityInfoBadge.MaxWidth = Math.Clamp(innerWidth * 0.37, 34, 460); LocationIcon.FontSize = Math.Clamp(13 * topScale, 6, 52); - CityTextBlock.FontSize = Math.Clamp(18.5 * topScale, 7, 88); - CityTextBlock.FontWeight = ToVariableWeight(Lerp(530, 620, emphasis)); - CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.37, 34, 460); + var cityLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + CityTextBlock.Text, + Math.Max(24, CityInfoBadge.MaxWidth - cityBadge.Padding.Left - cityBadge.Padding.Right), + Math.Max(12, topZoneHeight * 0.36), + 1, + 1, + 6, + Math.Max(6, 18.5 * topScale), + [ToVariableWeight(Lerp(530, 620, emphasis))], + 1.08); + CityTextBlock.FontSize = cityLayout.FontSize; + CityTextBlock.FontWeight = cityLayout.Weight; + CityTextBlock.LineHeight = cityLayout.LineHeight; + CityTextBlock.MaxWidth = CityInfoBadge.MaxWidth; - ConditionInfoBadge.Padding = new Thickness(0); - ConditionInfoBadge.CornerRadius = new CornerRadius(0); + var conditionBadge = ComponentTypographyLayoutService.ResolveBadgeBox( + innerWidth * 0.24, + Math.Max(16, bottomZoneHeight * 0.34), + preferredSizeScale: 0.26d, + minSize: 10, + maxSize: 24, + insetScale: 0.18d); + ConditionInfoBadge.Padding = conditionBadge.Padding; + ConditionInfoBadge.CornerRadius = new CornerRadius(conditionBadge.Size / 2d); + ConditionInfoBadge.MaxWidth = Math.Clamp(innerWidth * 0.24, 26, 320); ConditionRangeStack.Spacing = Math.Clamp(8.5 * topScale, 1, 24); - ConditionTextBlock.FontSize = Math.Clamp(19 * topScale, 7, 78); - RangeTextBlock.FontSize = Math.Clamp(21 * topScale, 7, 84); - ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(580, 660, emphasis)); - RangeTextBlock.FontWeight = ToVariableWeight(Lerp(600, 680, emphasis)); - ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 26, 320); - RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.31, 32, 360); + var conditionLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + ConditionTextBlock.Text, + Math.Max(24, ConditionInfoBadge.MaxWidth - conditionBadge.Padding.Left - conditionBadge.Padding.Right), + Math.Max(12, bottomZoneHeight * 0.30), + 1, + 1, + 7, + Math.Max(6, 19 * topScale), + [ToVariableWeight(Lerp(580, 660, emphasis))], + 1.10); + var rangeLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + RangeTextBlock.Text, + Math.Max(24, ConditionInfoBadge.MaxWidth - conditionBadge.Padding.Left - conditionBadge.Padding.Right), + Math.Max(12, bottomZoneHeight * 0.30), + 1, + 1, + 7, + Math.Max(6, 21 * topScale), + [ToVariableWeight(Lerp(600, 680, emphasis))], + 1.10); + ConditionTextBlock.FontSize = conditionLayout.FontSize; + ConditionTextBlock.FontWeight = conditionLayout.Weight; + ConditionTextBlock.LineHeight = conditionLayout.LineHeight; + RangeTextBlock.FontSize = rangeLayout.FontSize; + RangeTextBlock.FontWeight = rangeLayout.Weight; + RangeTextBlock.LineHeight = rangeLayout.LineHeight; + ConditionTextBlock.MaxWidth = ConditionInfoBadge.MaxWidth; + RangeTextBlock.MaxWidth = ConditionInfoBadge.MaxWidth; BottomInfoStack.Spacing = Math.Clamp(2.0 * topScale, 0.4, 14); WeatherIconImage.Width = iconSize; @@ -1325,14 +1383,34 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget, for (var i = 0; i < _hourlyTimeBlocks.Length; i++) { - _hourlyTempBlocks[i].FontSize = hourlyTempSize; - _hourlyTimeBlocks[i].FontSize = hourlyTimeSize; + var hourlyTempLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + _hourlyTempBlocks[i].Text, + Math.Clamp(hourlyCellWidth, 12, 240), + Math.Max(10, hourlyCellScale * 28), + 1, + 1, + 6, + hourlyTempSize, + [ToVariableWeight(Lerp(580, 690, emphasis))], + 1.02); + var hourlyTimeLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + _hourlyTimeBlocks[i].Text, + Math.Clamp(hourlyCellWidth, 12, 240), + Math.Max(10, hourlyCellScale * 24), + 1, + 1, + 6, + hourlyTimeSize, + [ToVariableWeight(Lerp(500, 600, emphasis))], + 1.02); + _hourlyTempBlocks[i].FontSize = hourlyTempLayout.FontSize; + _hourlyTimeBlocks[i].FontSize = hourlyTimeLayout.FontSize; _hourlyIconBlocks[i].Width = hourlyIconSize; _hourlyIconBlocks[i].Height = hourlyIconSize; _hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 240); _hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 240); - _hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(500, 600, emphasis)); - _hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(580, 690, emphasis)); + _hourlyTimeBlocks[i].FontWeight = hourlyTimeLayout.Weight; + _hourlyTempBlocks[i].FontWeight = hourlyTempLayout.Weight; if (_hourlyTimeBlocks[i].Parent is StackPanel hourlyStack) { hourlyStack.Spacing = stackSpacing; diff --git a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs index 6836490..7f874ba 100644 --- a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs @@ -15,6 +15,7 @@ using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -462,11 +463,23 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe visual.ImageHost.Height = imageHeight; visual.ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(imageHeight * 0.15, 8, 16); + var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + visual.TitleTextBlock.Text, + textWidth, + itemHeight, + minLines: 1, + maxLines: ComponentTypographyLayoutService.CountTextDisplayUnits(visual.TitleTextBlock.Text) > 28 ? 2 : 1, + minFontSize: Math.Clamp(titleFont * 0.72, 10, 16), + maxFontSize: titleFont, + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Bold }, + lineHeightFactor: 1.12d, + fontFamily: MiSansFontFamily); visual.TitleTextBlock.MaxWidth = textWidth; - visual.TitleTextBlock.FontSize = titleFont; - visual.TitleTextBlock.LineHeight = titleFont * 1.12; - visual.TitleTextBlock.MinHeight = visual.TitleTextBlock.LineHeight * 2; - visual.TitleTextBlock.MaxLines = 2; + visual.TitleTextBlock.FontSize = titleLayout.FontSize; + visual.TitleTextBlock.LineHeight = titleLayout.LineHeight; + visual.TitleTextBlock.MinHeight = titleLayout.LineHeight * titleLayout.MaxLines; + visual.TitleTextBlock.MaxLines = titleLayout.MaxLines; + visual.TitleTextBlock.FontWeight = titleLayout.Weight; } StatusTextBlock.FontSize = Math.Clamp(titleFont, 10, 20); diff --git a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs index d8e8476..0e72031 100644 --- a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs @@ -9,6 +9,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.Models; @@ -1120,31 +1121,88 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge var temperatureSample = string.IsNullOrWhiteSpace(TemperatureTextBlock.Text) ? "00°" : TemperatureTextBlock.Text.Trim(); - var temperatureGlyphCount = Math.Clamp(temperatureSample.Length, 3, 6); var temperatureMaxWidth = Math.Max(28, innerWidth - iconSize - TopRowGrid.ColumnSpacing - 4); var rawTemperatureSize = Math.Clamp(Lerp(64, 92, iconGrowth) * topScale, 12, 320); - var fitTemperatureSize = temperatureMaxWidth / (temperatureGlyphCount * 0.62); - TemperatureTextBlock.FontSize = Math.Clamp(Math.Min(rawTemperatureSize, fitTemperatureSize), 9, 320); - TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 360, emphasis)); + var temperatureLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + TemperatureTextBlock.Text, + temperatureMaxWidth, + Math.Max(18, topZoneHeight * 0.84), + 1, + 1, + Math.Max(9, rawTemperatureSize * 0.42), + rawTemperatureSize, + [ToVariableWeight(Lerp(300, 360, emphasis))], + 1.02); + TemperatureTextBlock.FontSize = temperatureLayout.FontSize; + TemperatureTextBlock.FontWeight = temperatureLayout.Weight; TemperatureTextBlock.Margin = new Thickness(0, Math.Clamp(-2.0 * topScale, -10, 0), 0, 0); TemperatureTextBlock.MaxWidth = Math.Clamp(temperatureMaxWidth, 28, Math.Max(280, innerWidth * 0.68)); - CityInfoBadge.Padding = new Thickness(0); - CityInfoBadge.CornerRadius = new CornerRadius(0); + var cityBadge = ComponentTypographyLayoutService.ResolveBadgeBox( + innerWidth * 0.37, + Math.Max(16, topZoneHeight * 0.34), + preferredSizeScale: 0.28d, + minSize: 10, + maxSize: 24, + insetScale: 0.18d); + CityInfoBadge.Padding = cityBadge.Padding; + CityInfoBadge.CornerRadius = new CornerRadius(cityBadge.Size / 2d); + CityInfoBadge.MaxWidth = Math.Clamp(innerWidth * 0.37, 34, 460); LocationIcon.FontSize = Math.Clamp(13 * topScale, 6, 52); - CityTextBlock.FontSize = Math.Clamp(18.5 * topScale, 7, 88); - CityTextBlock.FontWeight = ToVariableWeight(Lerp(530, 620, emphasis)); - CityTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.37, 34, 460); + var cityLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + CityTextBlock.Text, + Math.Max(24, CityInfoBadge.MaxWidth - cityBadge.Padding.Left - cityBadge.Padding.Right), + Math.Max(12, topZoneHeight * 0.36), + 1, + 1, + 6, + Math.Max(6, 18.5 * topScale), + [ToVariableWeight(Lerp(530, 620, emphasis))], + 1.08); + CityTextBlock.FontSize = cityLayout.FontSize; + CityTextBlock.FontWeight = cityLayout.Weight; + CityTextBlock.LineHeight = cityLayout.LineHeight; + CityTextBlock.MaxWidth = CityInfoBadge.MaxWidth; - ConditionInfoBadge.Padding = new Thickness(0); - ConditionInfoBadge.CornerRadius = new CornerRadius(0); + var conditionBadge = ComponentTypographyLayoutService.ResolveBadgeBox( + innerWidth * 0.24, + Math.Max(16, topZoneHeight * 0.34), + preferredSizeScale: 0.26d, + minSize: 10, + maxSize: 24, + insetScale: 0.18d); + ConditionInfoBadge.Padding = conditionBadge.Padding; + ConditionInfoBadge.CornerRadius = new CornerRadius(conditionBadge.Size / 2d); + ConditionInfoBadge.MaxWidth = Math.Clamp(innerWidth * 0.24, 26, 320); ConditionIconStack.Spacing = Math.Clamp(8.5 * topScale, 1, 24); - ConditionTextBlock.FontSize = Math.Clamp(19 * topScale, 7, 78); - RangeTextBlock.FontSize = Math.Clamp(21 * topScale, 7, 84); - ConditionTextBlock.FontWeight = ToVariableWeight(Lerp(580, 660, emphasis)); - RangeTextBlock.FontWeight = ToVariableWeight(Lerp(600, 680, emphasis)); - ConditionTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.24, 26, 320); - RangeTextBlock.MaxWidth = Math.Clamp(innerWidth * 0.31, 32, 360); + var conditionLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + ConditionTextBlock.Text, + Math.Max(24, ConditionInfoBadge.MaxWidth - conditionBadge.Padding.Left - conditionBadge.Padding.Right), + Math.Max(12, topZoneHeight * 0.30), + 1, + 1, + 7, + Math.Max(6, 19 * topScale), + [ToVariableWeight(Lerp(580, 660, emphasis))], + 1.06); + var rangeLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + RangeTextBlock.Text, + Math.Max(24, ConditionInfoBadge.MaxWidth - conditionBadge.Padding.Left - conditionBadge.Padding.Right), + Math.Max(12, topZoneHeight * 0.30), + 1, + 1, + 7, + Math.Max(6, 21 * topScale), + [ToVariableWeight(Lerp(600, 680, emphasis))], + 1.06); + ConditionTextBlock.FontSize = conditionLayout.FontSize; + ConditionTextBlock.FontWeight = conditionLayout.Weight; + ConditionTextBlock.LineHeight = conditionLayout.LineHeight; + RangeTextBlock.FontSize = rangeLayout.FontSize; + RangeTextBlock.FontWeight = rangeLayout.Weight; + RangeTextBlock.LineHeight = rangeLayout.LineHeight; + ConditionTextBlock.MaxWidth = ConditionInfoBadge.MaxWidth; + RangeTextBlock.MaxWidth = ConditionInfoBadge.MaxWidth; BottomInfoStack.Spacing = Math.Clamp(2.0 * topScale, 0.4, 14); WeatherIconImage.Width = iconSize; @@ -1173,14 +1231,34 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge for (var i = 0; i < _hourlyTimeBlocks.Length; i++) { - _hourlyTimeBlocks[i].FontSize = forecastLabelSize; - _hourlyTempBlocks[i].FontSize = forecastRangeSize; + var hourlyTimeLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + _hourlyTimeBlocks[i].Text, + Math.Clamp(hourlyCellWidth, 12, 260), + Math.Max(10, bottomZoneHeight * 0.34), + 1, + 1, + 6, + forecastLabelSize, + [ToVariableWeight(Lerp(500, 600, emphasis))], + 1.02); + var hourlyTempLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + _hourlyTempBlocks[i].Text, + Math.Clamp(hourlyCellWidth, 12, 260), + Math.Max(10, bottomZoneHeight * 0.42), + 1, + 1, + 6, + forecastRangeSize, + [ToVariableWeight(Lerp(580, 690, emphasis))], + 1.02); + _hourlyTimeBlocks[i].FontSize = hourlyTimeLayout.FontSize; + _hourlyTempBlocks[i].FontSize = hourlyTempLayout.FontSize; _hourlyIconBlocks[i].Width = forecastIconSize; _hourlyIconBlocks[i].Height = forecastIconSize; _hourlyTimeBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 260); _hourlyTempBlocks[i].MaxWidth = Math.Clamp(hourlyCellWidth, 12, 260); - _hourlyTimeBlocks[i].FontWeight = ToVariableWeight(Lerp(500, 600, emphasis)); - _hourlyTempBlocks[i].FontWeight = ToVariableWeight(Lerp(580, 690, emphasis)); + _hourlyTimeBlocks[i].FontWeight = hourlyTimeLayout.Weight; + _hourlyTempBlocks[i].FontWeight = hourlyTempLayout.Weight; _hourlyTimeBlocks[i].TextAlignment = TextAlignment.Center; _hourlyTempBlocks[i].TextAlignment = TextAlignment.Center; if (_hourlyTimeBlocks[i].Parent is StackPanel hourlyStack) diff --git a/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs b/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs index 896ebe7..8e65b29 100644 --- a/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs @@ -12,6 +12,7 @@ using Avalonia.Media.Imaging; using Avalonia.Styling; using Avalonia.Threading; using FluentIcons.Common; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Services; using LanMountainDesktop.Theme; @@ -123,6 +124,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, NextIcon.FontSize = Math.Clamp(18 * scale, 13, 24); FavoriteIcon.FontSize = Math.Clamp(16 * scale, 11, 21); + UpdateTypography(); UpdateProgressVisual(_progressRatio, _isProgressIndeterminate); } @@ -418,6 +420,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, SetCoverImage(state.ThumbnailBytes); ApplyActionButtonState(state); + UpdateTypography(); UpdateSourceAppButtonTooltip(); } @@ -552,6 +555,67 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, }; } + private void UpdateTypography() + { + var scale = ResolveScale(); + var rootWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 10.5; + var rootHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 4.2; + var headerWidth = Math.Max(120, rootWidth - Math.Max(84, SourceAppButton.MinWidth) - 86); + var titleWidth = Math.Max(96, headerWidth); + var metaWidth = Math.Max(96, headerWidth); + var timelineWidth = Math.Max(52, rootWidth * 0.18); + var statusWidth = Math.Max(72, Math.Min(headerWidth, rootWidth * 0.26)); + + TitleTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + TitleTextBlock.Text, + titleWidth, + Math.Max(24, rootHeight * 0.12), + 1, + 12, + Math.Clamp(20 * scale, 12, 28), + FontWeight.SemiBold, + 1.06d); + + var artistMaxLines = ArtistTextBlock.MaxLines <= 0 ? 1 : ArtistTextBlock.MaxLines; + ArtistTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + ArtistTextBlock.Text, + metaWidth, + artistMaxLines > 1 ? Math.Max(32, rootHeight * 0.12) : Math.Max(20, rootHeight * 0.08), + artistMaxLines, + 9, + Math.Clamp(14 * scale, 9, 18), + FontWeight.SemiBold, + 1.06d); + + PositionTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + PositionTextBlock.Text, + timelineWidth, + 18, + 1, + 8, + Math.Clamp(13 * scale, 8, 15), + FontWeight.SemiBold, + 1.05d); + DurationTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + DurationTextBlock.Text, + timelineWidth, + 18, + 1, + 8, + Math.Clamp(13 * scale, 8, 15), + FontWeight.SemiBold, + 1.05d); + StatusTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + StatusTextBlock.Text, + statusWidth, + 18, + 1, + 8, + Math.Clamp(13 * scale, 8, 15), + FontWeight.Medium, + 1.05d); + } + private string L(string key, string fallback) { return _localizationService.GetString(_languageCode, key, fallback); diff --git a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs index ba1782a..4e8db7a 100644 --- a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs @@ -5,6 +5,8 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Media; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -73,8 +75,6 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen Resources["OfficeRecentDocumentsAccentSize"] = accentSize; Resources["OfficeRecentDocumentsAccentCornerRadius"] = new CornerRadius(accentSize / 2d); - Resources["OfficeRecentDocumentsHeaderFontSize"] = Math.Clamp(18 * scale, 12, 24); - Resources["OfficeRecentDocumentsStatusFontSize"] = Math.Clamp(14 * scale, 10, 18); Resources["OfficeRecentDocumentsDocumentSpacing"] = ComponentChromeCornerRadiusHelper.SafeValue(8 * scale, 4, 12, null, 0.40d); var cardWidth = Math.Clamp(130 * scale, 96, 180); @@ -83,8 +83,7 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen Resources["OfficeRecentDocumentsDocumentCardHeight"] = cardHeight; Resources["OfficeRecentDocumentsCardCornerRadius"] = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 10, 24); Resources["OfficeRecentDocumentsCardPadding"] = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(10 * scale, 6, 16, null, 0.50d)); - Resources["OfficeRecentDocumentsDocumentTitleFontSize"] = Math.Clamp(12 * scale, 10, 18); - Resources["OfficeRecentDocumentsDocumentTimeFontSize"] = Math.Clamp(10 * scale, 8, 14); + UpdateTypographyResources(); } public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) @@ -128,16 +127,19 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen { StatusTextBlock.Text = "\u6682\u65e0\u6700\u8fd1\u6587\u6863"; StatusTextBlock.IsVisible = true; + UpdateTypographyResources(); return; } UpdateDisplay(); + UpdateTypographyResources(); } catch (Exception ex) { AppLogger.Warn("OfficeRecentDocsWidget", "Failed to load recent Office documents.", ex); StatusTextBlock.Text = "\u52a0\u8f7d\u5931\u8d25"; StatusTextBlock.IsVisible = true; + UpdateTypographyResources(); } finally { @@ -163,6 +165,7 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen }).ToList(); DocumentsItemsControl.ItemsSource = displayItems; + UpdateTypographyResources(); } private static string GetTimeAgo(DateTime dateTime) @@ -215,4 +218,70 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen } } } + + private void UpdateTypographyResources() + { + var width = Bounds.Width > 1 ? Bounds.Width : 640d; + var cardWidth = (double?)Resources["OfficeRecentDocumentsDocumentCardWidth"] ?? 130d; + var cardHeight = (double?)Resources["OfficeRecentDocumentsDocumentCardHeight"] ?? 90d; + var cardPadding = (Thickness?)Resources["OfficeRecentDocumentsCardPadding"] ?? new Thickness(10); + var rootPadding = (Thickness?)Resources["OfficeRecentDocumentsRootPadding"] ?? new Thickness(12, 10, 12, 10); + var contentMargin = (Thickness?)Resources["OfficeRecentDocumentsContentMargin"] ?? new Thickness(16, 14, 16, 14); + + var innerWidth = Math.Max(180, width - rootPadding.Left - rootPadding.Right - contentMargin.Left - contentMargin.Right); + var headerWidth = Math.Max(120, innerWidth * 0.48); + var statusWidth = Math.Max(120, innerWidth * 0.40); + + Resources["OfficeRecentDocumentsHeaderFontSize"] = ComponentTypographyLayoutService.FitFontSize( + HeaderTextBlock.Text, + headerWidth, + 24, + 1, + 12, + 24, + FontWeight.SemiBold, + 1.05d); + + Resources["OfficeRecentDocumentsStatusFontSize"] = ComponentTypographyLayoutService.FitFontSize( + StatusTextBlock.Text, + statusWidth, + 22, + 1, + 10, + 18, + FontWeight.Normal, + 1.06d); + + var documentTexts = _documents.Count == 0 + ? new[] { "Sample Office Document" } + : _documents.Select(item => item.FileName).Where(text => !string.IsNullOrWhiteSpace(text)).ToArray(); + var longestDocumentText = documentTexts.Length == 0 + ? "Sample Office Document" + : documentTexts.OrderByDescending(ComponentTypographyLayoutService.CountTextDisplayUnits).First(); + var titleWidth = Math.Max(72, cardWidth - cardPadding.Left - cardPadding.Right); + var titleHeight = Math.Max(28, cardHeight - cardPadding.Top - cardPadding.Bottom - 18); + Resources["OfficeRecentDocumentsDocumentTitleFontSize"] = ComponentTypographyLayoutService.FitFontSize( + longestDocumentText, + titleWidth, + titleHeight, + 2, + 10, + 18, + FontWeight.Medium, + 1.08d); + + var timeSamples = _documents.Count == 0 + ? new[] { "00/00" } + : _documents.Select(item => GetTimeAgo(item.LastModifiedTime)).ToArray(); + var longestTimeText = timeSamples.OrderByDescending(ComponentTypographyLayoutService.CountTextDisplayUnits).First(); + Resources["OfficeRecentDocumentsDocumentTimeFontSize"] = ComponentTypographyLayoutService.FitFontSize( + longestTimeText, + Math.Max(56, titleWidth * 0.72), + 18, + 1, + 8, + 14, + FontWeight.Normal, + 1.06d); + } } diff --git a/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs b/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs index 879bc6c..bb297d9 100644 --- a/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs @@ -10,6 +10,7 @@ using Avalonia.Media; using Avalonia.Platform.Storage; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -108,10 +109,8 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe HintTextBlock.Margin = new Thickness(0, Math.Clamp(8 * contentScale, 4, 10), 0, 0); WaveformBarsPanel.Spacing = Math.Clamp(3 * contentScale, 1.6, 3.4); - TitleTextBlock.FontSize = Math.Clamp(19 * contentScale, 12, 20); - TimerTextBlock.FontSize = Math.Clamp(66 * contentScale, 34, 66); - HintTextBlock.FontSize = Math.Clamp(13 * contentScale, 9, 13); + UpdateTypography(); UpdateWaveformVisual(); } @@ -379,49 +378,43 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe PauseGlyphIcon.IsVisible = snapshot.State == AudioRecorderRuntimeState.Recording; PlayGlyphIcon.IsVisible = snapshot.State == AudioRecorderRuntimeState.Paused; + string hintText; if (!isSupported) { - HintTextBlock.Text = L("recording.widget.hint.unsupported", "Microphone is unavailable"); - return; + hintText = L("recording.widget.hint.unsupported", "Microphone is unavailable"); } - - if (snapshot.State == AudioRecorderRuntimeState.Recording) + else if (snapshot.State == AudioRecorderRuntimeState.Recording) { - HintTextBlock.Text = L("recording.widget.hint.recording", "Recording"); - return; + hintText = L("recording.widget.hint.recording", "Recording"); } - - if (snapshot.State == AudioRecorderRuntimeState.Paused) + else if (snapshot.State == AudioRecorderRuntimeState.Paused) { - HintTextBlock.Text = L("recording.widget.hint.paused", "Paused"); - return; + hintText = L("recording.widget.hint.paused", "Paused"); } - - if (snapshot.State == AudioRecorderRuntimeState.Error) + else if (snapshot.State == AudioRecorderRuntimeState.Error) { - HintTextBlock.Text = string.IsNullOrWhiteSpace(snapshot.LastError) + hintText = string.IsNullOrWhiteSpace(snapshot.LastError) ? L("recording.widget.hint.error", "Recording failed") : snapshot.LastError; - return; } - - if (!string.IsNullOrWhiteSpace(snapshot.LastSavedFilePath) && - !string.Equals(snapshot.LastSavedFilePath, _lastSavedFilePath, StringComparison.OrdinalIgnoreCase)) + else { - _lastSavedFilePath = snapshot.LastSavedFilePath; + if (!string.IsNullOrWhiteSpace(snapshot.LastSavedFilePath) && + !string.Equals(snapshot.LastSavedFilePath, _lastSavedFilePath, StringComparison.OrdinalIgnoreCase)) + { + _lastSavedFilePath = snapshot.LastSavedFilePath; + } + + hintText = !string.IsNullOrWhiteSpace(_lastSavedFilePath) + ? string.Format( + CultureInfo.InvariantCulture, + L("recording.widget.hint.saved_format", "Saved {0}"), + Path.GetFileName(_lastSavedFilePath)) + : L("recording.widget.hint.ready", "Tap red button to record"); } - if (!string.IsNullOrWhiteSpace(_lastSavedFilePath)) - { - var fileName = Path.GetFileName(_lastSavedFilePath); - HintTextBlock.Text = string.Format( - CultureInfo.InvariantCulture, - L("recording.widget.hint.saved_format", "Saved {0}"), - fileName); - return; - } - - HintTextBlock.Text = L("recording.widget.hint.ready", "Tap red button to record"); + HintTextBlock.Text = hintText; + UpdateTypography(); } private bool TryStartRecordingWithMonitoringHandoff() @@ -574,6 +567,51 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe return duration.ToString(@"mm\:ss", CultureInfo.InvariantCulture); } + private void UpdateTypography() + { + var contentWidth = RecorderContentGrid.Bounds.Width > 1 ? RecorderContentGrid.Bounds.Width : 252d; + var contentHeight = RecorderContentGrid.Bounds.Height > 1 ? RecorderContentGrid.Bounds.Height : 240d; + var timerWidth = Math.Max(88, contentWidth * 0.84); + var timerHeight = Math.Max(34, contentHeight * 0.24); + var hintWidth = Math.Max(120, contentWidth * 0.86); + var hintHeight = Math.Max(24, contentHeight * 0.12); + + TitleTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + TitleTextBlock.Text, + Math.Max(96, contentWidth * 0.62), + 20, + 1, + 12, + 20, + FontWeight.SemiBold, + 1.05d); + + TimerTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + TimerTextBlock.Text, + timerWidth, + timerHeight, + 1, + 34, + 66, + FontWeight.SemiBold, + 1.0d); + + var hintLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + HintTextBlock.Text, + hintWidth, + hintHeight, + 1, + 2, + 9, + 13, + [FontWeight.Medium, FontWeight.Normal], + 1.10d); + HintTextBlock.FontSize = hintLayout.FontSize; + HintTextBlock.FontWeight = hintLayout.Weight; + HintTextBlock.MaxLines = hintLayout.MaxLines; + HintTextBlock.TextWrapping = hintLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; + } + private static IBrush CreateBrush(string colorHex) { return new SolidColorBrush(Color.Parse(colorHex)); diff --git a/LanMountainDesktop/Views/Components/RemovableStorageWidget.axaml.cs b/LanMountainDesktop/Views/Components/RemovableStorageWidget.axaml.cs index 3660e17..e7bdc56 100644 --- a/LanMountainDesktop/Views/Components/RemovableStorageWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/RemovableStorageWidget.axaml.cs @@ -9,6 +9,7 @@ using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; using FluentIcons.Avalonia; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; @@ -364,11 +365,6 @@ public partial class RemovableStorageWidget : UserControl, IDesktopComponentWidg IconBadge.CornerRadius = new CornerRadius(badgeSize * 0.5); DriveIcon.FontSize = Math.Clamp(24 * scale, 20, 32); - DriveNameTextBlock.FontSize = Math.Clamp(16 * scale, 13, 24); - DriveDetailTextBlock.FontSize = Math.Clamp(11.5 * scale, 10, 16); - StatusTextBlock.FontSize = Math.Clamp(12 * scale, 10, 17); - StatusTextBlock.MaxWidth = Math.Max(96, width - (RootBorder.Padding.Left + RootBorder.Padding.Right)); - var buttonHeight = Math.Clamp(42 * scale, 38, 54); var buttonPadding = Math.Clamp(14 * scale, 10, 20); var buttonCornerRadius = Math.Clamp(buttonHeight * 0.5, 18, 999); @@ -383,14 +379,14 @@ public partial class RemovableStorageWidget : UserControl, IDesktopComponentWidg OpenButtonIcon.FontSize = Math.Clamp(16 * scale, 14, 20); EjectButtonIcon.FontSize = Math.Clamp(16 * scale, 14, 20); - OpenButtonTextBlock.FontSize = Math.Clamp(13 * scale, 11.5, 18); - EjectButtonTextBlock.FontSize = Math.Clamp(13 * scale, 11.5, 18); AccentOrb.Width = Math.Clamp(width * 0.44, 96, 176); AccentOrb.Height = AccentOrb.Width; AccentOrb.CornerRadius = new CornerRadius(AccentOrb.Width * 0.5); AccentGlow.Height = Math.Clamp(76 * scale, 52, 110); AccentGlow.CornerRadius = new CornerRadius(AccentGlow.Height * 0.5); + + UpdateTypography(); } private RemovableStorageDrive? GetSelectedDrive() @@ -522,6 +518,67 @@ public partial class RemovableStorageWidget : UserControl, IDesktopComponentWidg return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.72, 2.2); } + private void UpdateTypography() + { + var scale = ResolveScale(); + var width = Bounds.Width > 1 ? Bounds.Width : 220d; + var rootPadding = RootBorder.Padding; + var headerWidth = Math.Max(96, width - rootPadding.Left - rootPadding.Right - Math.Max(44, IconBadge.Width) - HeaderGrid.ColumnSpacing); + var statusWidth = Math.Max(120, width - rootPadding.Left - rootPadding.Right); + var buttonWidth = Math.Max(88, width - rootPadding.Left - rootPadding.Right); + + DriveNameTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + DriveNameTextBlock.Text, + headerWidth, + 22, + 1, + 13, + 24, + FontWeight.SemiBold, + 1.05d); + DriveDetailTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + DriveDetailTextBlock.Text, + headerWidth, + 18, + 1, + 10, + 16, + FontWeight.Normal, + 1.05d); + StatusTextBlock.FontSize = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + StatusTextBlock.Text, + statusWidth, + Math.Max(28, 40 * scale), + 1, + 3, + 10, + 17, + [FontWeight.Medium, FontWeight.Normal], + 1.10d).FontSize; + StatusTextBlock.MaxWidth = statusWidth; + StatusTextBlock.MaxLines = 3; + StatusTextBlock.TextWrapping = TextWrapping.Wrap; + + OpenButtonTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + OpenButtonTextBlock.Text, + buttonWidth - 32, + Math.Max(18, OpenButton.Height - 8), + 1, + 11, + 18, + FontWeight.SemiBold, + 1.04d); + EjectButtonTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + EjectButtonTextBlock.Text, + buttonWidth - 32, + Math.Max(18, EjectButton.Height - 8), + 1, + 11, + 18, + FontWeight.SemiBold, + 1.04d); + } + private string L(string key, string fallback) { return _localizationService.GetString(_languageCode, key, fallback); diff --git a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs index 585f3ef..0596178 100644 --- a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs @@ -15,6 +15,7 @@ using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -675,9 +676,32 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I visual.AvatarHost.Height = avatarSize; visual.AvatarHost.CornerRadius = new CornerRadius(avatarSize / 2d); - visual.AvatarFallbackText.FontSize = avatarFont; - visual.TitleTextBlock.FontSize = titleFont; + var avatarGlyphBox = ComponentTypographyLayoutService.ResolveGlyphBox( + avatarSize, + avatarSize, + preferredSizeScale: 0.60d, + minSize: 12, + maxSize: 18); + visual.AvatarFallbackText.Margin = avatarGlyphBox.Margin; + visual.AvatarFallbackText.FontSize = Math.Min(avatarFont, Math.Max(avatarGlyphBox.Width * 0.55d, 10)); + visual.AvatarFallbackText.MaxWidth = avatarGlyphBox.Width; + + var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + visual.TitleTextBlock.Text, + titleMaxWidth, + Math.Max(avatarSize, rowPaddingVertical * 2d + 18), + minLines: 1, + maxLines: ComponentTypographyLayoutService.CountTextDisplayUnits(visual.TitleTextBlock.Text) > 24 ? 2 : 1, + minFontSize: Math.Clamp(titleFont * 0.72, 10, 14), + maxFontSize: titleFont, + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Bold }, + lineHeightFactor: 1.08d, + fontFamily: MiSansFontFamily); + visual.TitleTextBlock.FontSize = titleLayout.FontSize; + visual.TitleTextBlock.LineHeight = titleLayout.LineHeight; visual.TitleTextBlock.MaxWidth = titleMaxWidth; + visual.TitleTextBlock.MaxLines = titleLayout.MaxLines; + visual.TitleTextBlock.FontWeight = titleLayout.Weight; } StatusTextBlock.FontSize = Math.Clamp(14 * softScale, 10, 18); diff --git a/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs index d034f24..04a9959 100644 --- a/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs @@ -6,6 +6,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; using LanMountainDesktop.Theme; @@ -284,6 +285,88 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen ApplyVariableWeights(scale); ApplyLocalizedLabels(); + + var contentWidth = Math.Max(120, (Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 8) - RootBorder.Padding.Left - RootBorder.Padding.Right); + var contentHeight = Math.Max(78, (Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 3) - RootBorder.Padding.Top - RootBorder.Padding.Bottom); + + var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + TitleTextBlock.Text, + Math.Max(120, contentWidth * 0.44), + Math.Max(18, contentHeight * 0.22), + 1, + 1, + 9, + Math.Clamp(20 * scale, 9, 20), + [TitleTextBlock.FontWeight], + 1.05); + TitleTextBlock.FontSize = titleLayout.FontSize; + TitleTextBlock.FontWeight = titleLayout.Weight; + TitleTextBlock.MaxLines = 1; + TitleTextBlock.TextWrapping = TextWrapping.NoWrap; + TitleTextBlock.LineHeight = titleLayout.LineHeight; + + var modeBadgeBox = ComponentTypographyLayoutService.ResolveBadgeBox( + Math.Max(64, contentWidth * 0.24), + Math.Max(20, contentHeight * 0.14), + preferredSizeScale: 0.48d, + minSize: 18, + maxSize: 42, + insetScale: 0.18d); + ModeBadgeBorder.Padding = modeBadgeBox.Padding; + ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(modeBadgeBox.Size * 0.36, 5, 12)); + var modeLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + ModeTextBlock.Text, + Math.Max(54, modeBadgeBox.Width), + Math.Max(18, modeBadgeBox.Height), + 1, + 1, + 8, + Math.Clamp(16 * scale, 8, 16), + [ModeTextBlock.FontWeight], + 1.02); + ModeTextBlock.FontSize = modeLayout.FontSize; + ModeTextBlock.FontWeight = modeLayout.Weight; + ModeTextBlock.MaxLines = 1; + ModeTextBlock.TextWrapping = TextWrapping.NoWrap; + ModeTextBlock.LineHeight = modeLayout.LineHeight; + + foreach (var block in new[] { SustainedReasonTextBlock, TimeReasonTextBlock, SegmentReasonTextBlock }) + { + var layout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + block.Text, + Math.Max(84, contentWidth * 0.34), + Math.Max(18, contentHeight * 0.14), + 1, + _isUltraCompactMode ? 1 : 2, + 9, + Math.Clamp(18 * scale, 9, 18), + [block.FontWeight], + 1.05); + block.FontSize = layout.FontSize; + block.FontWeight = layout.Weight; + block.MaxLines = layout.MaxLines; + block.TextWrapping = layout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; + block.LineHeight = layout.LineHeight; + } + + foreach (var block in new[] { SustainedMetricTextBlock, TimeMetricTextBlock, SegmentMetricTextBlock, TotalLossTextBlock, ScoreTextBlock }) + { + var layout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + block.Text, + Math.Max(72, contentWidth * 0.22), + Math.Max(16, contentHeight * 0.10), + 1, + 1, + 8, + Math.Clamp(16 * scale, 8, 16), + [block.FontWeight], + 1.02); + block.FontSize = layout.FontSize; + block.FontWeight = layout.Weight; + block.MaxLines = 1; + block.TextWrapping = TextWrapping.NoWrap; + block.LineHeight = layout.LineHeight; + } } private void ApplyTypographyByBackground(Color panelColor) diff --git a/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs index 26dee40..062f9cf 100644 --- a/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs @@ -5,6 +5,7 @@ using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; using LanMountainDesktop.ComponentSystem; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -251,6 +252,77 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg LayoutGrid.ColumnSpacing = hideStatusLabel ? Math.Clamp(6 * scale, 4, 10) : Math.Clamp(10 * scale, 7, 14); + + var availableWidth = Math.Max(72, (width > 0 ? width : _currentCellSize * 4) - RootBorder.Padding.Left - RootBorder.Padding.Right); + var availableHeight = Math.Max(34, (height > 0 ? height : _currentCellSize * 2) - RootBorder.Padding.Top - RootBorder.Padding.Bottom); + var statusTitleWidth = Math.Max(50, availableWidth * 0.32); + var statusValueWidth = Math.Max(78, availableWidth - statusTitleWidth - Math.Clamp(6 * scale, 3, 8)); + var valueHeight = Math.Max(18, availableHeight * 0.44); + + var statusTitleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + StatusTitleTextBlock.Text, + statusTitleWidth, + valueHeight, + 1, + 1, + 9, + Math.Clamp(18 * scale, 9, 18), + [StatusTitleTextBlock.FontWeight], + 1.04); + StatusTitleTextBlock.FontSize = statusTitleLayout.FontSize; + StatusTitleTextBlock.FontWeight = statusTitleLayout.Weight; + StatusTitleTextBlock.MaxLines = 1; + StatusTitleTextBlock.TextWrapping = TextWrapping.NoWrap; + StatusTitleTextBlock.LineHeight = statusTitleLayout.LineHeight; + + var statusValueLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + StatusValueTextBlock.Text, + statusValueWidth, + valueHeight, + 1, + 1, + 12, + Math.Clamp(34 * scale, 12, 34), + [StatusValueTextBlock.FontWeight], + 1.04); + StatusValueTextBlock.FontSize = statusValueLayout.FontSize; + StatusValueTextBlock.FontWeight = statusValueLayout.Weight; + StatusValueTextBlock.MaxLines = 1; + StatusValueTextBlock.TextWrapping = TextWrapping.NoWrap; + StatusValueTextBlock.LineHeight = statusValueLayout.LineHeight; + + var noiseValueLines = _showDisplayDb && _showDbfs ? 2 : 1; + var noiseValueLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + NoiseValueTextBlock.Text, + Math.Max(92, availableWidth * 0.48), + Math.Max(22, availableHeight * 0.56), + 1, + noiseValueLines, + 12, + Math.Clamp(38 * scale, 12, 38), + [NoiseValueTextBlock.FontWeight], + 1.06); + NoiseValueTextBlock.FontSize = noiseValueLayout.FontSize; + NoiseValueTextBlock.FontWeight = noiseValueLayout.Weight; + NoiseValueTextBlock.MaxLines = noiseValueLayout.MaxLines; + NoiseValueTextBlock.TextWrapping = noiseValueLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; + NoiseValueTextBlock.LineHeight = noiseValueLayout.LineHeight; + + var noiseSubValueLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + NoiseSubValueTextBlock.Text, + Math.Max(72, availableWidth * 0.34), + Math.Max(18, availableHeight * 0.24), + 1, + 1, + 9, + Math.Clamp(18 * scale, 9, 18), + [NoiseSubValueTextBlock.FontWeight], + 1.04); + NoiseSubValueTextBlock.FontSize = noiseSubValueLayout.FontSize; + NoiseSubValueTextBlock.FontWeight = noiseSubValueLayout.Weight; + NoiseSubValueTextBlock.MaxLines = 1; + NoiseSubValueTextBlock.TextWrapping = TextWrapping.NoWrap; + NoiseSubValueTextBlock.LineHeight = noiseSubValueLayout.LineHeight; } private string ResolveStatusText(StudyAnalyticsSnapshot snapshot) diff --git a/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs index d2f885c..6a02b8d 100644 --- a/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs @@ -5,6 +5,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; using LanMountainDesktop.Theme; @@ -310,6 +311,92 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen ApplyVariableWeights(scale); ApplyLocalizedLabels(); + + var contentWidth = Math.Max(120, (Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 8) - RootBorder.Padding.Left - RootBorder.Padding.Right); + var contentHeight = Math.Max(78, (Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 3) - RootBorder.Padding.Top - RootBorder.Padding.Bottom); + + var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + TitleTextBlock.Text, + Math.Max(120, contentWidth * 0.38), + Math.Max(18, contentHeight * 0.18), + 1, + 1, + 9, + Math.Clamp(20 * scale, 9, 20), + [TitleTextBlock.FontWeight], + 1.05); + TitleTextBlock.FontSize = titleLayout.FontSize; + TitleTextBlock.FontWeight = titleLayout.Weight; + TitleTextBlock.MaxLines = 1; + TitleTextBlock.TextWrapping = TextWrapping.NoWrap; + TitleTextBlock.LineHeight = titleLayout.LineHeight; + + var modeBadgeBox = ComponentTypographyLayoutService.ResolveBadgeBox( + Math.Max(64, contentWidth * 0.22), + Math.Max(20, contentHeight * 0.14), + preferredSizeScale: 0.46d, + minSize: 18, + maxSize: 42, + insetScale: 0.18d); + ModeBadgeBorder.Padding = modeBadgeBox.Padding; + ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(modeBadgeBox.Size * 0.36, 4, 12)); + var modeLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + ModeTextBlock.Text, + Math.Max(52, modeBadgeBox.Width), + Math.Max(18, modeBadgeBox.Height), + 1, + 1, + 8, + Math.Clamp(16 * scale, 8, 16), + [ModeTextBlock.FontWeight], + 1.02); + ModeTextBlock.FontSize = modeLayout.FontSize; + ModeTextBlock.FontWeight = modeLayout.Weight; + ModeTextBlock.MaxLines = 1; + ModeTextBlock.TextWrapping = TextWrapping.NoWrap; + ModeTextBlock.LineHeight = modeLayout.LineHeight; + + foreach (var block in new[] { DensityValueTextBlock, CountValueTextBlock, DurationValueTextBlock }) + { + var minFont = block == DensityValueTextBlock ? 18 : 10; + var maxFont = block == DensityValueTextBlock ? Math.Clamp(94 * scale, 18, 94) : Math.Clamp(36 * scale, 10, 36); + var maxWidth = block == DensityValueTextBlock ? Math.Max(86, contentWidth * 0.24) : Math.Max(64, contentWidth * 0.18); + var maxHeight = block == DensityValueTextBlock ? Math.Max(24, contentHeight * 0.26) : Math.Max(18, contentHeight * 0.18); + var layout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + block.Text, + maxWidth, + maxHeight, + 1, + 1, + minFont, + maxFont, + [block.FontWeight], + 1.02); + block.FontSize = layout.FontSize; + block.FontWeight = layout.Weight; + block.MaxLines = 1; + block.TextWrapping = TextWrapping.NoWrap; + block.LineHeight = layout.LineHeight; + } + + foreach (var block in new[] { DensityUnitTextBlock, DensityLevelTextBlock, CountLabelTextBlock, DurationLabelTextBlock, ThresholdTextBlock }) + { + var layout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + block.Text, + Math.Max(64, contentWidth * 0.18), + Math.Max(16, contentHeight * 0.14), + 1, + 1, + 8, + Math.Clamp(18 * scale, 8, 18), + [block.FontWeight], + 1.02); + block.FontSize = layout.FontSize; + block.FontWeight = layout.Weight; + block.MaxLines = 1; + block.TextWrapping = TextWrapping.NoWrap; + block.LineHeight = layout.LineHeight; + } } private void ApplyTypographyByBackground(Color panelColor) diff --git a/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs index c6cb04f..53651b3 100644 --- a/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs @@ -5,6 +5,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; using LanMountainDesktop.Theme; @@ -128,6 +129,50 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge XLeftTextBlock.FontSize = axisFontSize; XCenterTextBlock.FontSize = axisFontSize; XRightTextBlock.FontSize = axisFontSize; + + var contentWidth = Math.Max(110, (_currentCellSize * 4) - RootBorder.Padding.Left - RootBorder.Padding.Right); + var contentHeight = Math.Max(72, (_currentCellSize * 2) - RootBorder.Padding.Top - RootBorder.Padding.Bottom); + var statusLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + StatusTextBlock.Text, + Math.Max(72, contentWidth * 0.34), + Math.Max(18, contentHeight * 0.18), + 1, + 1, + 12, + Math.Clamp(30 * scale, 12, 30), + [StatusTextBlock.FontWeight], + 1.03); + StatusTextBlock.FontSize = statusLayout.FontSize; + StatusTextBlock.FontWeight = statusLayout.Weight; + StatusTextBlock.MaxLines = 1; + StatusTextBlock.TextWrapping = TextWrapping.NoWrap; + StatusTextBlock.LineHeight = statusLayout.LineHeight; + + var realtimeValueLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + RealtimeValueTextBlock.Text, + Math.Max(82, contentWidth * 0.40), + Math.Max(20, contentHeight * 0.22), + 1, + 1, + 12, + Math.Clamp(34 * scale, 12, 34), + [RealtimeValueTextBlock.FontWeight], + 1.03); + RealtimeValueTextBlock.FontSize = realtimeValueLayout.FontSize; + RealtimeValueTextBlock.FontWeight = realtimeValueLayout.Weight; + RealtimeValueTextBlock.MaxLines = 1; + RealtimeValueTextBlock.TextWrapping = TextWrapping.NoWrap; + RealtimeValueTextBlock.LineHeight = realtimeValueLayout.LineHeight; + + var badgeBox = ComponentTypographyLayoutService.ResolveBadgeBox( + Math.Max(64, contentWidth * 0.20), + Math.Max(20, contentHeight * 0.12), + preferredSizeScale: 0.46d, + minSize: 18, + maxSize: 40, + insetScale: 0.18d); + StatusBadgeBorder.Padding = badgeBox.Padding; + StatusBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(badgeBox.Size * 0.36, 5, 12)); } public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) diff --git a/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs index fad3dd9..eda01ad 100644 --- a/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs @@ -6,6 +6,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; using LanMountainDesktop.Theme; @@ -358,6 +359,85 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone SummaryTextBlock.IsVisible = true; ApplyVariableWeights(scale); + + var contentWidth = Math.Max(120, (Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 8) - RootBorder.Padding.Left - RootBorder.Padding.Right); + var contentHeight = Math.Max(78, (Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 3) - RootBorder.Padding.Top - RootBorder.Padding.Bottom); + + var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + TitleTextBlock.Text, + Math.Max(120, contentWidth * 0.38), + Math.Max(18, contentHeight * 0.18), + 1, + 1, + 9, + Math.Clamp(22 * scale, 9, 22), + [TitleTextBlock.FontWeight], + 1.05); + TitleTextBlock.FontSize = titleLayout.FontSize; + TitleTextBlock.FontWeight = titleLayout.Weight; + TitleTextBlock.MaxLines = 1; + TitleTextBlock.TextWrapping = TextWrapping.NoWrap; + TitleTextBlock.LineHeight = titleLayout.LineHeight; + + var summaryLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + SummaryTextBlock.Text, + Math.Max(104, contentWidth * 0.44), + Math.Max(18, contentHeight * 0.16), + 1, + _isUltraCompactMode ? 1 : 2, + 8, + Math.Clamp(20 * scale, 8, 20), + [SummaryTextBlock.FontWeight], + 1.06); + SummaryTextBlock.FontSize = summaryLayout.FontSize; + SummaryTextBlock.FontWeight = summaryLayout.Weight; + SummaryTextBlock.MaxLines = summaryLayout.MaxLines; + SummaryTextBlock.TextWrapping = summaryLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; + SummaryTextBlock.LineHeight = summaryLayout.LineHeight; + + var modeBadgeBox = ComponentTypographyLayoutService.ResolveBadgeBox( + Math.Max(64, contentWidth * 0.22), + Math.Max(20, contentHeight * 0.14), + preferredSizeScale: 0.46d, + minSize: 18, + maxSize: 42, + insetScale: 0.18d); + ModeBadgeBorder.Padding = modeBadgeBox.Padding; + ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(modeBadgeBox.Size * 0.36, 4, 12)); + var modeLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + ModeTextBlock.Text, + Math.Max(52, modeBadgeBox.Width), + Math.Max(18, modeBadgeBox.Height), + 1, + 1, + 8, + Math.Clamp(18 * scale, 8, 18), + [ModeTextBlock.FontWeight], + 1.02); + ModeTextBlock.FontSize = modeLayout.FontSize; + ModeTextBlock.FontWeight = modeLayout.Weight; + ModeTextBlock.MaxLines = 1; + ModeTextBlock.TextWrapping = TextWrapping.NoWrap; + ModeTextBlock.LineHeight = modeLayout.LineHeight; + + foreach (var block in new[] { YExtremeTextBlock, YNoisyTextBlock, YNormalTextBlock, YQuietTextBlock, XLeftTextBlock, XCenterTextBlock, XRightTextBlock }) + { + var layout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + block.Text, + Math.Max(36, contentWidth * 0.12), + Math.Max(14, contentHeight * 0.08), + 1, + 1, + 8, + Math.Clamp(16 * scale, 8, 16), + [block.FontWeight], + 1.02); + block.FontSize = layout.FontSize; + block.FontWeight = layout.Weight; + block.MaxLines = 1; + block.TextWrapping = TextWrapping.NoWrap; + block.LineHeight = layout.LineHeight; + } } private void ApplyTypographyByBackground(Color panelColor) diff --git a/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs index 8c894ee..fcbf154 100644 --- a/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs @@ -6,6 +6,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; using LanMountainDesktop.Theme; @@ -336,6 +337,92 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi ApplyVariableWeights(scale); ApplyLocalizedLabels(); + + var contentWidth = Math.Max(140, (Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 8) - RootBorder.Padding.Left - RootBorder.Padding.Right); + var contentHeight = Math.Max(96, (Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 4) - RootBorder.Padding.Top - RootBorder.Padding.Bottom); + + var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + TitleTextBlock.Text, + Math.Max(120, contentWidth * 0.42), + Math.Max(18, contentHeight * 0.14), + 1, + 1, + 9, + Math.Clamp(30 * scale * labelFactor, 9, 30), + [TitleTextBlock.FontWeight], + 1.05); + TitleTextBlock.FontSize = titleLayout.FontSize; + TitleTextBlock.FontWeight = titleLayout.Weight; + TitleTextBlock.MaxLines = 1; + TitleTextBlock.TextWrapping = TextWrapping.NoWrap; + TitleTextBlock.LineHeight = titleLayout.LineHeight; + + var modeBadgeBox = ComponentTypographyLayoutService.ResolveBadgeBox( + Math.Max(64, contentWidth * 0.22), + Math.Max(20, contentHeight * 0.12), + preferredSizeScale: 0.46d, + minSize: 18, + maxSize: 42, + insetScale: 0.18d); + ModeBadgeBorder.Padding = modeBadgeBox.Padding; + ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(modeBadgeBox.Size * 0.36, 5, 14)); + var modeLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + ModeTextBlock.Text, + Math.Max(52, modeBadgeBox.Width), + Math.Max(18, modeBadgeBox.Height), + 1, + 1, + 8, + Math.Clamp(22 * scale * labelFactor, 8, 22), + [ModeTextBlock.FontWeight], + 1.02); + ModeTextBlock.FontSize = modeLayout.FontSize; + ModeTextBlock.FontWeight = modeLayout.Weight; + ModeTextBlock.MaxLines = 1; + ModeTextBlock.TextWrapping = TextWrapping.NoWrap; + ModeTextBlock.LineHeight = modeLayout.LineHeight; + + foreach (var block in new[] { CurrentLabelTextBlock, AverageLabelTextBlock, MinimumLabelTextBlock, MaximumLabelTextBlock }) + { + var layout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + block.Text, + Math.Max(64, contentWidth * 0.16), + Math.Max(14, contentHeight * 0.10), + 1, + 1, + 8, + Math.Clamp(22 * scale * labelFactor, 8, 22), + [block.FontWeight], + 1.02); + block.FontSize = layout.FontSize; + block.FontWeight = layout.Weight; + block.MaxLines = 1; + block.TextWrapping = TextWrapping.NoWrap; + block.LineHeight = layout.LineHeight; + } + + foreach (var block in new[] { CurrentScoreTextBlock, AverageValueTextBlock, MinimumValueTextBlock, MaximumValueTextBlock }) + { + var minFont = block == CurrentScoreTextBlock ? 22 : 11; + var maxFont = block == CurrentScoreTextBlock ? Math.Clamp(190 * scale * headlineFactor, 22, 190) : Math.Clamp(64 * scale * statFactor, 11, 64); + var maxWidth = block == CurrentScoreTextBlock ? Math.Max(96, contentWidth * 0.30) : Math.Max(72, contentWidth * 0.18); + var maxHeight = block == CurrentScoreTextBlock ? Math.Max(30, contentHeight * 0.24) : Math.Max(20, contentHeight * 0.14); + var layout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + block.Text, + maxWidth, + maxHeight, + 1, + 1, + minFont, + maxFont, + [block.FontWeight], + 1.02); + block.FontSize = layout.FontSize; + block.FontWeight = layout.Weight; + block.MaxLines = 1; + block.TextWrapping = TextWrapping.NoWrap; + block.LineHeight = layout.LineHeight; + } } private void PushRealtimeScore(double score, DateTimeOffset now) diff --git a/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs index af204dd..c479103 100644 --- a/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs @@ -4,6 +4,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; using LanMountainDesktop.Theme; @@ -293,6 +294,40 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW ActionIcon.Height = Math.Clamp(buttonSize * 0.44, 14, 30); SecondaryTextBlock.IsVisible = !_isUltraCompactMode; + + var contentWidth = Math.Max(96, (Bounds.Width > 1 ? Bounds.Width : _currentCellSize * 4) - RootBorder.Padding.Left - RootBorder.Padding.Right); + var contentHeight = Math.Max(44, (Bounds.Height > 1 ? Bounds.Height : _currentCellSize * 2) - RootBorder.Padding.Top - RootBorder.Padding.Bottom); + var primaryLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + PrimaryTextBlock.Text, + Math.Max(72, contentWidth * 0.58), + Math.Max(18, contentHeight * 0.28), + 1, + 1, + 10, + Math.Clamp(30 * scale, 10, 30), + [PrimaryTextBlock.FontWeight], + 1.04); + PrimaryTextBlock.FontSize = primaryLayout.FontSize; + PrimaryTextBlock.FontWeight = primaryLayout.Weight; + PrimaryTextBlock.MaxLines = 1; + PrimaryTextBlock.TextWrapping = TextWrapping.NoWrap; + PrimaryTextBlock.LineHeight = primaryLayout.LineHeight; + + var secondaryLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + SecondaryTextBlock.Text, + Math.Max(64, contentWidth * 0.58), + Math.Max(16, contentHeight * 0.22), + 1, + _isUltraCompactMode ? 1 : 2, + 8, + Math.Clamp(18 * scale, 8, 18), + [SecondaryTextBlock.FontWeight], + 1.04); + SecondaryTextBlock.FontSize = secondaryLayout.FontSize; + SecondaryTextBlock.FontWeight = secondaryLayout.Weight; + SecondaryTextBlock.MaxLines = secondaryLayout.MaxLines; + SecondaryTextBlock.TextWrapping = secondaryLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; + SecondaryTextBlock.LineHeight = secondaryLayout.LineHeight; } private void ApplyTypographyByBackground(Color panelColor) diff --git a/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs index b94dc72..1e94826 100644 --- a/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs @@ -9,6 +9,7 @@ using Avalonia.Media; using Avalonia.Threading; using FluentIcons.Avalonia; using FluentIcons.Common; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; using LanMountainDesktop.Theme; @@ -195,6 +196,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW StatusTextBlock.Text = _transientStatus ?? L("study.session_history.empty", "No session history"); StatusTextBlock.Foreground = CreateAdaptiveBrush(panelSamples, SecondaryColorCandidates, MinTextContrast); + ApplyHistoryTypographyLayout(); UpdateDialogVisual(snapshot, panelColor); return; } @@ -219,6 +221,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW StatusTextBlock.Text = _transientStatus ?? string.Empty; StatusTextBlock.Foreground = CreateAdaptiveBrush(panelSamples, SecondaryColorCandidates, MinTextContrast); + ApplyHistoryTypographyLayout(); UpdateDialogVisual(snapshot, panelColor); } @@ -261,15 +264,25 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW { Spacing = _isUltraCompactMode ? 0 : 2 }; - textStack.Children.Add(new TextBlock + var rowTitleTextBlock = new TextBlock { Text = entry.Label, - FontSize = Math.Clamp(12 * (_isCompactMode ? 0.92 : 1.0), 10, 17), - FontWeight = FontWeight.SemiBold, - MaxLines = 1, - TextTrimming = TextTrimming.CharacterEllipsis, Foreground = rowPrimaryBrush - }); + }; + ApplyTextLayout( + rowTitleTextBlock, + ComponentTypographyLayoutService.FitAdaptiveTextLayout( + entry.Label, + ResolveHistoryContentWidth(), + _isUltraCompactMode ? 18 : 22, + 1, + 1, + 10, + 17, + new[] { FontWeight.SemiBold, FontWeight.Medium }, + 1.08d), + TextWrapping.NoWrap); + textStack.Children.Add(rowTitleTextBlock); if (!_isUltraCompactMode) { @@ -281,14 +294,25 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW FormatDuration(entry.Duration), entry.AverageScore); - textStack.Children.Add(new TextBlock + var metaTextBlock = new TextBlock { Text = metaText, - FontSize = Math.Clamp(10.5 * (_isCompactMode ? 0.94 : 1.0), 9, 14), - MaxLines = 1, - TextTrimming = TextTrimming.CharacterEllipsis, Foreground = rowSecondaryBrush - }); + }; + ApplyTextLayout( + metaTextBlock, + ComponentTypographyLayoutService.FitAdaptiveTextLayout( + metaText, + ResolveHistoryContentWidth(), + _isUltraCompactMode ? 16 : 20, + 1, + 1, + 9, + 14, + new[] { FontWeight.Normal, FontWeight.Medium }, + 1.08d), + TextWrapping.NoWrap); + textStack.Children.Add(metaTextBlock); } rowGrid.Children.Add(textStack); @@ -566,6 +590,8 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW DialogConfirmButton.Content = L("study.session_history.dialog.delete_confirm", "Delete"); DialogCancelButton.Content = L("study.session_history.rename_cancel", "Cancel rename"); } + + ApplyHistoryTypographyLayout(); } private void SetTransientStatus(string status, double seconds = 2.2) @@ -599,8 +625,6 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW ? Math.Clamp(4 * scale, 2, 6) : Math.Clamp(7 * scale, 4, 10); - TitleTextBlock.FontSize = Math.Clamp(13 * scale, 10, 22); - StatusTextBlock.FontSize = Math.Clamp(11 * scale, 9, 18); SessionListPanel.Spacing = _isUltraCompactMode ? Math.Clamp(4 * scale, 2, 5) : Math.Clamp(6 * scale, 3, 8); @@ -612,13 +636,108 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW DialogCardBorder.Padding = new Thickness( ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 9, 20), ComponentChromeCornerRadiusHelper.SafeValue(11 * scale, 8, 18)); - DialogTitleTextBlock.FontSize = Math.Clamp(14 * scale, 11, 20); - DialogMessageTextBlock.FontSize = Math.Clamp(12 * scale, 10, 17); - DialogRenameTextBox.FontSize = Math.Clamp(11.5 * scale, 10, 16); DialogCancelButton.FontSize = Math.Clamp(11 * scale, 10, 16); DialogConfirmButton.FontSize = Math.Clamp(11 * scale, 10, 16); DialogCancelButton.Height = Math.Clamp(30 * scale, 26, 38); DialogConfirmButton.Height = Math.Clamp(30 * scale, 26, 38); + + ApplyHistoryTypographyLayout(); + } + + private void ApplyHistoryTypographyLayout() + { + var contentWidth = ResolveHistoryContentWidth(); + + var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + TitleTextBlock.Text, + contentWidth, + _isUltraCompactMode ? 18 : 24, + 1, + 1, + 10, + 22, + new[] { FontWeight.SemiBold, FontWeight.Medium }, + 1.08d); + ApplyTextLayout(TitleTextBlock, titleLayout, TextWrapping.NoWrap); + + var statusLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + StatusTextBlock.Text, + contentWidth, + _isUltraCompactMode ? 18 : 28, + 1, + _isUltraCompactMode ? 1 : 2, + 9, + 18, + new[] { FontWeight.Normal, FontWeight.Medium }, + 1.10d); + ApplyTextLayout(StatusTextBlock, statusLayout, statusLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap); + + if (!DialogOverlayBorder.IsVisible) + { + return; + } + + var dialogWidth = DialogCardBorder.Bounds.Width > 1 + ? Math.Max(1, DialogCardBorder.Bounds.Width - DialogCardBorder.Padding.Left - DialogCardBorder.Padding.Right) + : Math.Max(180, contentWidth); + + var dialogTitleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + DialogTitleTextBlock.Text, + dialogWidth, + _isUltraCompactMode ? 18 : 24, + 1, + 1, + 11, + 20, + new[] { FontWeight.SemiBold, FontWeight.Medium }, + 1.08d); + ApplyTextLayout(DialogTitleTextBlock, dialogTitleLayout, TextWrapping.NoWrap); + + var dialogMessageLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + DialogMessageTextBlock.Text, + dialogWidth, + _isUltraCompactMode ? 42 : 56, + 1, + 3, + 10, + 17, + new[] { FontWeight.Normal, FontWeight.Medium }, + 1.12d); + ApplyTextLayout(DialogMessageTextBlock, dialogMessageLayout, dialogMessageLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap); + + var renameLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + DialogRenameTextBox.Text ?? DialogRenameTextBox.Watermark, + dialogWidth, + _isUltraCompactMode ? 30 : 36, + 1, + 1, + 10, + 16, + new[] { FontWeight.Normal }, + 1.08d); + DialogRenameTextBox.FontSize = renameLayout.FontSize; + DialogRenameTextBox.FontWeight = renameLayout.Weight; + } + + private double ResolveHistoryContentWidth() + { + if (Bounds.Width <= 1) + { + return Math.Max(90, _currentCellSize * 4.5); + } + + var reservedWidth = _isUltraCompactMode ? 126 : 150; + return Math.Max(90, Bounds.Width - reservedWidth); + } + + private static void ApplyTextLayout(TextBlock textBlock, ComponentAdaptiveTextLayout layout, TextWrapping wrapping) + { + textBlock.FontSize = layout.FontSize; + textBlock.FontWeight = layout.Weight; + textBlock.LineHeight = layout.LineHeight; + textBlock.MaxLines = layout.MaxLines; + textBlock.TextWrapping = wrapping; + textBlock.TextTrimming = TextTrimming.CharacterEllipsis; } private static StudySessionHistoryEntry? FindHistoryEntry(IReadOnlyList history, string? sessionId) diff --git a/LanMountainDesktop/Views/Components/TimerWidget.axaml.cs b/LanMountainDesktop/Views/Components/TimerWidget.axaml.cs index 73823f1..fd3d5f6 100644 --- a/LanMountainDesktop/Views/Components/TimerWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/TimerWidget.axaml.cs @@ -7,6 +7,7 @@ using Avalonia.Input; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; namespace LanMountainDesktop.Views.Components; @@ -122,6 +123,7 @@ public partial class TimerWidget : UserControl, IDesktopComponentWidget MainNumberTextBlock.Text = current.ToString(CultureInfo.InvariantCulture); NextNumberTextBlock.Text = next.ToString(CultureInfo.InvariantCulture); NextNextNumberTextBlock.Text = nextNext.ToString(CultureInfo.InvariantCulture); + UpdateTypography(); } private void UpdateHandGeometry() @@ -206,6 +208,7 @@ public partial class TimerWidget : UserControl, IDesktopComponentWidget PlayButtonBorder.Height = Math.Clamp(42 * scale, 28, 58); PlayButtonBorder.CornerRadius = new CornerRadius(PlayButtonBorder.Width / 2d); + UpdateTypography(); ApplyModeVisualIfNeeded(); } @@ -217,6 +220,51 @@ public partial class TimerWidget : UserControl, IDesktopComponentWidget return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.05), 0.58, 1.95); } + private void UpdateTypography() + { + var panelWidth = TimerPanelBorder.Bounds.Width > 1 ? TimerPanelBorder.Bounds.Width : 224d; + var panelHeight = TimerPanelBorder.Bounds.Height > 1 ? TimerPanelBorder.Bounds.Height : 224d; + var leftColumnWidth = Math.Max(72, panelWidth * 0.38); + var leftColumnHeight = Math.Max(120, panelHeight - 36); + + TopNumberTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + TopNumberTextBlock.Text, + leftColumnWidth, + Math.Max(20, leftColumnHeight * 0.20), + 1, + 18, + 38, + FontWeight.SemiBold, + 1.02d); + MainNumberTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + MainNumberTextBlock.Text, + leftColumnWidth, + Math.Max(28, leftColumnHeight * 0.30), + 1, + 28, + 64, + FontWeight.Bold, + 1.00d); + NextNumberTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + NextNumberTextBlock.Text, + leftColumnWidth, + Math.Max(18, leftColumnHeight * 0.16), + 1, + 14, + 34, + FontWeight.Medium, + 1.02d); + NextNextNumberTextBlock.FontSize = ComponentTypographyLayoutService.FitFontSize( + NextNextNumberTextBlock.Text, + leftColumnWidth, + Math.Max(16, leftColumnHeight * 0.12), + 1, + 12, + 26, + FontWeight.Medium, + 1.02d); + } + private bool ResolveIsNightMode() { if (ActualThemeVariant == ThemeVariant.Dark) diff --git a/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs index 8946da4..5cbfdb1 100644 --- a/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.Models; @@ -218,14 +219,36 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget, } var leftWidthFactor = Math.Clamp(leftContentWidth / 122d, 0.48, 1.35); - TimeTextBlock.FontSize = Math.Clamp((metrics.PrimaryTemperatureFont * 0.74) * scale * compactFactor * leftWidthFactor, 10, 62); - DateTextBlock.FontSize = Math.Clamp(metrics.SecondaryTextFont * scale * compactFactor * leftWidthFactor, 8, 30); + var timeLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + TimeTextBlock.Text, + leftContentWidth, + Math.Max(12, contentHeight * 0.42), + 1, + 1, + 10, + Math.Clamp((metrics.PrimaryTemperatureFont * 0.74) * scale * compactFactor * leftWidthFactor, 10, 62), + [ToVariableWeight(Lerp(620, 760, Math.Clamp((scale - 0.68) / 1.35, 0, 1)))], + 1.04); + var dateLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + DateTextBlock.Text, + Math.Max(12, leftContentWidth), + Math.Max(10, contentHeight * 0.24), + 1, + 1, + 8, + Math.Clamp(metrics.SecondaryTextFont * scale * compactFactor * leftWidthFactor, 8, 30), + [ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.68) / 1.35, 0, 1)))], + 1.04); + TimeTextBlock.FontSize = timeLayout.FontSize; + DateTextBlock.FontSize = dateLayout.FontSize; var weatherIconSize = Math.Clamp(metrics.IconFont * scale * compactFactor * leftWidthFactor, 9, 32); WeatherIconImage.Width = weatherIconSize; WeatherIconImage.Height = weatherIconSize; - TimeTextBlock.FontWeight = ToVariableWeight(Lerp(620, 760, Math.Clamp((scale - 0.68) / 1.35, 0, 1))); - DateTextBlock.FontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.68) / 1.35, 0, 1))); + TimeTextBlock.FontWeight = timeLayout.Weight; + DateTextBlock.FontWeight = dateLayout.Weight; + TimeTextBlock.LineHeight = timeLayout.LineHeight; + DateTextBlock.LineHeight = dateLayout.LineHeight; LeftStack.Width = leftContentWidth; LeftStack.MaxWidth = leftContentWidth; diff --git a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs index 156d497..a9af013 100644 --- a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs @@ -11,6 +11,7 @@ using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.Models; @@ -925,12 +926,21 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk var temperatureSample = string.IsNullOrWhiteSpace(TemperatureTextBlock.Text) ? "00°" : TemperatureTextBlock.Text.Trim(); - var temperatureGlyphCount = Math.Clamp(temperatureSample.Length, 3, 6); + var temperatureGlyphCount = Math.Clamp(ComponentTypographyLayoutService.CountTextDisplayUnits(temperatureSample), 3, 6); var temperatureMaxWidth = Math.Max(34, innerWidth - iconSize - TopRowGrid.ColumnSpacing - 2); var rawTemperatureSize = Math.Clamp(Lerp(94, 118, iconGrowth) * topScale, 22, 340); - var fitTemperatureSize = temperatureMaxWidth / (temperatureGlyphCount * 0.62); - TemperatureTextBlock.FontSize = Math.Clamp(Math.Min(rawTemperatureSize, fitTemperatureSize), 10, 340); - TemperatureTextBlock.FontWeight = ToVariableWeight(Lerp(300, 360, emphasis)); + var temperatureLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + TemperatureTextBlock.Text, + temperatureMaxWidth, + Math.Max(18, topZoneHeight * 0.84), + 1, + 1, + Math.Max(10, rawTemperatureSize * 0.42), + rawTemperatureSize, + [ToVariableWeight(Lerp(300, 360, emphasis))], + 1.02); + TemperatureTextBlock.FontSize = temperatureLayout.FontSize; + TemperatureTextBlock.FontWeight = temperatureLayout.Weight; TemperatureTextBlock.Margin = new Thickness(Math.Clamp(-1.4 * topScale, -6, 0), Math.Clamp(-7.6 * topScale, -16, -1), 0, 0); TemperatureTextBlock.MaxWidth = Math.Clamp(temperatureMaxWidth, 34, Math.Max(34, innerWidth * 0.76)); @@ -961,26 +971,76 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk } var infoFontWeight = ToVariableWeight(Lerp(580, 690, emphasis)); - ConditionTextBlock.FontSize = Math.Max(6, infoFontSize * 0.96); - ConditionTextBlock.FontWeight = infoFontWeight; - ConditionTextBlock.LineHeight = ConditionTextBlock.FontSize * infoLineHeightFactor; + var conditionBadge = ComponentTypographyLayoutService.ResolveBadgeBox( + bottomTextMaxWidth, + Math.Max(16, bottomZoneHeight * 0.34), + preferredSizeScale: 0.26d, + minSize: 10, + maxSize: 24, + insetScale: 0.18d); + ConditionStack.Margin = new Thickness( + conditionBadge.Padding.Left, + conditionBadge.Padding.Top, + conditionBadge.Padding.Right, + conditionBadge.Padding.Bottom); + var conditionContentMaxWidth = Math.Max(24, bottomTextMaxWidth - conditionBadge.Padding.Left - conditionBadge.Padding.Right); + var conditionLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + ConditionTextBlock.Text, + conditionContentMaxWidth, + Math.Max(12, bottomZoneHeight * 0.30), + 1, + 1, + 7, + Math.Max(6, infoFontSize * 0.96), + [infoFontWeight], + infoLineHeightFactor); + var rangeLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + RangeTextBlock.Text, + conditionContentMaxWidth, + Math.Max(12, bottomZoneHeight * 0.30), + 1, + 1, + 7, + Math.Max(6, infoFontSize * 1.03), + [infoFontWeight], + infoLineHeightFactor); + ConditionTextBlock.FontSize = conditionLayout.FontSize; + ConditionTextBlock.FontWeight = conditionLayout.Weight; + ConditionTextBlock.LineHeight = conditionLayout.LineHeight; ConditionTextBlock.MaxWidth = bottomTextMaxWidth; - RangeTextBlock.FontSize = Math.Max(6, infoFontSize * 1.03); - RangeTextBlock.FontWeight = infoFontWeight; - RangeTextBlock.LineHeight = RangeTextBlock.FontSize * infoLineHeightFactor; + RangeTextBlock.FontSize = rangeLayout.FontSize; + RangeTextBlock.FontWeight = rangeLayout.Weight; + RangeTextBlock.LineHeight = rangeLayout.LineHeight; RangeTextBlock.MaxWidth = bottomTextMaxWidth; - CityInfoBadge.Padding = new Thickness(0); - CityInfoBadge.CornerRadius = new CornerRadius(0); + var cityBadge = ComponentTypographyLayoutService.ResolveBadgeBox( + bottomTextMaxWidth, + Math.Max(16, bottomZoneHeight * 0.38), + preferredSizeScale: 0.28d, + minSize: 10, + maxSize: 24, + insetScale: 0.18d); + CityInfoBadge.Padding = cityBadge.Padding; + CityInfoBadge.CornerRadius = new CornerRadius(cityBadge.Size / 2d); CityInfoBadge.MaxWidth = bottomTextMaxWidth; LocationIcon.FontSize = Math.Clamp( 12 * bottomScale, 6, 34); LocationIcon.FontSize = Math.Min(LocationIcon.FontSize, infoFontSize * 0.72); - CityTextBlock.FontSize = Math.Max(6, infoFontSize * 0.84); - CityTextBlock.FontWeight = ToVariableWeight(Lerp(500, 620, emphasis)); - CityTextBlock.LineHeight = CityTextBlock.FontSize * infoLineHeightFactor; + var cityLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + CityTextBlock.Text, + Math.Max(24, bottomTextMaxWidth - cityBadge.Padding.Left - cityBadge.Padding.Right), + Math.Max(12, bottomZoneHeight * 0.36), + 1, + 1, + 6, + Math.Max(6, infoFontSize * 0.84), + [ToVariableWeight(Lerp(500, 620, emphasis))], + infoLineHeightFactor); + CityTextBlock.FontSize = cityLayout.FontSize; + CityTextBlock.FontWeight = cityLayout.Weight; + CityTextBlock.LineHeight = cityLayout.LineHeight; CityTextBlock.MaxWidth = bottomTextMaxWidth; } diff --git a/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs index f4d8c33..4a6f186 100644 --- a/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs @@ -8,6 +8,7 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Services; @@ -101,6 +102,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT private TimeZoneService? _timeZoneService; private string _languageCode = "zh-CN"; private double _currentCellSize = BaseCellSize; + private double _layoutScale = 1d; private DateTime _nextLanguageProbeUtc = DateTime.MinValue; private string _secondHandMode = ClockSecondHandMode.Tick; private bool _isNightVisual = true; @@ -163,14 +165,16 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT { _currentCellSize = Math.Max(1, cellSize); var scale = ResolveScale(); + var chromeScale = ComponentChromeCornerRadiusHelper.ResolveScale(); + _layoutScale = Math.Clamp(scale * (0.9d + (chromeScale * 0.1d)), 0.58d, 2.0d); var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells; var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells; - var horizontalPadding = Math.Clamp(10 * scale, 4, 26); - var verticalPadding = Math.Clamp(8 * scale, 3, 22); + var horizontalPadding = Math.Clamp(10 * _layoutScale, 4, 26); + var verticalPadding = Math.Clamp(8 * _layoutScale, 3, 22); RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d); - RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * scale, 10, 46); + RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * _layoutScale, 10, 46); var usableWidth = Math.Max(48, totalWidth - horizontalPadding * 2); var usableHeight = Math.Max(28, totalHeight - verticalPadding * 2); @@ -179,11 +183,8 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT ClockHostGrid.ColumnSpacing = columnSpacing; var widthPerClock = Math.Max(18, (usableWidth - columnSpacing * 3) / WorldClockTimeZoneCatalog.ClockCount); - var secondaryFont = Math.Clamp(10.5 * scale * (widthPerClock / 46d), 7, 18); - var cityFont = Math.Clamp(secondaryFont * 1.42, 9, 24); - var textSpacing = Math.Clamp(2.8 * scale, 1, 7); - - var estimatedTextHeight = cityFont * 1.2 + secondaryFont * 2.35 + textSpacing * 3; + var textSpacing = Math.Clamp(2.8 * _layoutScale, 1, 7); + var estimatedTextHeight = Math.Clamp(78 * _layoutScale, 42, 128); var dialSize = Math.Clamp(Math.Min(widthPerClock, usableHeight - estimatedTextHeight), 18, 108); if (dialSize < 18) { @@ -197,15 +198,13 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT entry.DialBorder.Height = dialSize; entry.DialBorder.CornerRadius = new CornerRadius(dialSize / 2d); - entry.CityTextBlock.FontSize = cityFont; - entry.DayTextBlock.FontSize = secondaryFont; - entry.OffsetTextBlock.FontSize = secondaryFont; - var maxTextWidth = Math.Max(16, widthPerClock + 10); entry.CityTextBlock.MaxWidth = maxTextWidth; entry.DayTextBlock.MaxWidth = maxTextWidth; entry.OffsetTextBlock.MaxWidth = maxTextWidth; } + + RefreshDialArtwork(_isNightVisual); } private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) @@ -476,6 +475,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT private static void BuildDialTicks(ClockEntryVisual entry, bool isNight) { entry.TickCanvas.Children.Clear(); + var scale = Math.Clamp(entry.Host.Spacing / 3d, 0.78d, 1.22d); var majorColor = isNight ? "#E3E7F2" : "#2D3341"; var minorColor = isNight ? "#9EA7B8" : "#9AA4B3"; @@ -483,8 +483,8 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT { var isMajor = i % 5 == 0; var angle = (i * 6 - 90) * Math.PI / 180d; - var outerRadius = DialCenter - 6.5; - var innerRadius = outerRadius - (isMajor ? 9 : 4.5); + var outerRadius = DialCenter - (6.5 * scale); + var innerRadius = outerRadius - (isMajor ? 9 * scale : 4.5 * scale); var x1 = DialCenter + Math.Cos(angle) * innerRadius; var y1 = DialCenter + Math.Sin(angle) * innerRadius; @@ -496,7 +496,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT StartPoint = new Point(x1, y1), EndPoint = new Point(x2, y2), Stroke = CreateBrush(isMajor ? majorColor : minorColor), - StrokeThickness = isMajor ? 1.9 : 0.8, + StrokeThickness = (isMajor ? 1.9 : 0.8) * scale, StrokeLineCap = PenLineCap.Round }); } @@ -505,8 +505,9 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT private static void BuildDialNumbers(ClockEntryVisual entry, bool isNight) { entry.NumberCanvas.Children.Clear(); + var scale = Math.Clamp(entry.Host.Spacing / 3d, 0.78d, 1.22d); var numberColor = isNight ? "#F2F5FB" : "#1B202A"; - var radius = 36; + var radius = 36 * scale; for (var number = 1; number <= 12; number++) { var angle = (number * 30 - 90) * Math.PI / 180d; @@ -514,22 +515,37 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT var y = DialCenter + Math.Sin(angle) * radius; var text = number.ToString(CultureInfo.InvariantCulture); var isDoubleDigit = number >= 10; - var width = isDoubleDigit ? 14 : 10; - var height = 12; + var glyphBox = ComponentTypographyLayoutService.ResolveGlyphBox( + isDoubleDigit ? 16 * scale : 12 * scale, + 12 * scale, + preferredSizeScale: 0.98d, + minSize: 8, + maxSize: 16, + insetScale: 0d); + var fontSize = ComponentTypographyLayoutService.FitFontSize( + text, + glyphBox.Width, + glyphBox.Height, + maxLines: 1, + minFontSize: 7 * scale, + maxFontSize: 11 * scale, + weight: FontWeight.SemiBold, + lineHeightFactor: 1d, + fontFamily: MiSansFontFamily); var numberText = new TextBlock { Text = text, - Width = width, - Height = height, + Width = glyphBox.Width, + Height = glyphBox.Height, FontFamily = MiSansFontFamily, - FontSize = 9, + FontSize = fontSize, FontWeight = FontWeight.SemiBold, Foreground = CreateBrush(numberColor), TextAlignment = TextAlignment.Center }; - Canvas.SetLeft(numberText, x - width / 2d); - Canvas.SetTop(numberText, y - height / 2d); + Canvas.SetLeft(numberText, x - glyphBox.Width / 2d); + Canvas.SetTop(numberText, y - glyphBox.Height / 2d); entry.NumberCanvas.Children.Add(numberText); } } @@ -584,18 +600,69 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT var minuteAngle = minuteValue * 6d; var secondAngle = secondValue * 6d; - SetHandGeometry(entry.HourHand, hourAngle, forwardLength: 24, backwardLength: 4.8); - SetHandGeometry(entry.MinuteHand, minuteAngle, forwardLength: 33, backwardLength: 6); - SetHandGeometry(entry.SecondHand, secondAngle, forwardLength: 37, backwardLength: 8.5); + SetHandGeometry(entry.HourHand, hourAngle, forwardLength: 24 * _layoutScale, backwardLength: 4.8 * _layoutScale); + SetHandGeometry(entry.MinuteHand, minuteAngle, forwardLength: 33 * _layoutScale, backwardLength: 6 * _layoutScale); + SetHandGeometry(entry.SecondHand, secondAngle, forwardLength: 37 * _layoutScale, backwardLength: 8.5 * _layoutScale); entry.CityTextBlock.Text = ResolveCityName(zone); entry.DayTextBlock.Text = ResolveRelativeDayLabel((zonedNow.Date - baseNow.Date).Days); var offsetDelta = zone.GetUtcOffset(utcNow) - baseOffset; entry.OffsetTextBlock.Text = ResolveOffsetLabel(offsetDelta); + ApplyEntryTypography(entry); } } + private void ApplyEntryTypography(ClockEntryVisual entry) + { + var hostWidth = entry.Host.Bounds.Width > 1 ? entry.Host.Bounds.Width : Math.Max(18, _currentCellSize * 0.74); + var hostHeight = entry.Host.Bounds.Height > 1 ? entry.Host.Bounds.Height : Math.Max(18, _currentCellSize * 1.7); + var textWidth = Math.Max(16, hostWidth); + var cityHeight = Math.Clamp(hostHeight * 0.18, 12, 28); + var secondaryHeight = Math.Clamp(hostHeight * 0.14, 10, 22); + + var cityLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + entry.CityTextBlock.Text, + textWidth, + cityHeight, + minLines: 1, + maxLines: 1, + minFontSize: 9, + maxFontSize: 24, + weightCandidates: new[] { FontWeight.SemiBold, FontWeight.Medium }, + lineHeightFactor: 1d, + fontFamily: MiSansFontFamily); + var dayLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + entry.DayTextBlock.Text, + textWidth, + secondaryHeight, + minLines: 1, + maxLines: 1, + minFontSize: 8, + maxFontSize: 18, + weightCandidates: new[] { FontWeight.Medium, FontWeight.Normal }, + lineHeightFactor: 1d, + fontFamily: MiSansFontFamily); + var offsetLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + entry.OffsetTextBlock.Text, + textWidth, + secondaryHeight, + minLines: 1, + maxLines: 1, + minFontSize: 8, + maxFontSize: 18, + weightCandidates: new[] { FontWeight.Medium, FontWeight.Normal }, + lineHeightFactor: 1d, + fontFamily: MiSansFontFamily); + + entry.CityTextBlock.FontSize = cityLayout.FontSize; + entry.CityTextBlock.FontWeight = cityLayout.Weight; + entry.DayTextBlock.FontSize = dayLayout.FontSize; + entry.DayTextBlock.FontWeight = dayLayout.Weight; + entry.OffsetTextBlock.FontSize = offsetLayout.FontSize; + entry.OffsetTextBlock.FontWeight = offsetLayout.Weight; + } + private static void ApplyDialTheme(ClockEntryVisual entry, bool isNight) { if (entry.IsNightApplied.HasValue && entry.IsNightApplied.Value == isNight) @@ -615,6 +682,21 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT BuildDialNumbers(entry, isNight); } + private void RefreshDialArtwork(bool isNight) + { + for (var index = 0; index < _entryVisuals.Length; index++) + { + var entry = _entryVisuals[index]; + if (entry is null) + { + continue; + } + + BuildDialTicks(entry, isNight); + BuildDialNumbers(entry, isNight); + } + } + private void ProbeLanguageCodeIfNeeded(DateTime utcNow) { if (utcNow < _nextLanguageProbeUtc)