From aeae4be060f0434d9f3fd5b342b693f24225c64d Mon Sep 17 00:00:00 2001 From: lincube Date: Fri, 20 Mar 2026 10:22:40 +0800 Subject: [PATCH] 0.7.0.0 --- Directory.Build.props | 1 + .../ComponentTypographyLayoutModels.cs | 30 ++ .../ComponentTypographyLayoutService.cs | 314 ++++++++++++++ .../Services/IMusicControlService.cs | 72 +++- .../WindowsSmtcMusicControlService.cs | 382 +++++++++++++++++- .../Components/AnalogClockWidget.axaml.cs | 2 +- .../Components/BaiduHotSearchWidget.axaml.cs | 14 +- .../BilibiliHotSearchWidget.axaml.cs | 14 +- .../Views/Components/BrowserWidget.axaml.cs | 7 +- .../Components/ClassScheduleWidget.axaml.cs | 8 +- .../Components/CnrDailyNewsWidget.axaml.cs | 22 +- .../ComponentChromeCornerRadiusHelper.cs | 24 ++ .../Components/DailyArtworkWidget.axaml.cs | 29 +- .../Components/DailyPoetryWidget.axaml.cs | 30 +- .../Components/DailyWord2x2Widget.axaml.cs | 128 +++--- .../Views/Components/DailyWordWidget.axaml.cs | 176 ++++---- .../Views/Components/DateWidget.axaml.cs | 59 ++- .../ExchangeRateCalculatorWidget.axaml.cs | 2 +- .../Components/HolidayCalendarWidget.axaml | 15 +- .../Components/HolidayCalendarWidget.axaml.cs | 196 +++++---- .../Views/Components/IfengNewsWidget.axaml.cs | 14 +- .../Components/LunarCalendarWidget.axaml | 24 +- .../Components/LunarCalendarWidget.axaml.cs | 120 ++++-- .../Components/MonthCalendarWidget.axaml.cs | 59 ++- .../Views/Components/MusicControlWidget.axaml | 8 +- .../Components/MusicControlWidget.axaml.cs | 279 ++++++++----- .../OfficeRecentDocumentsWidget.axaml | 66 ++- .../OfficeRecentDocumentsWidget.axaml.cs | 51 ++- .../Views/Components/RecordingWidget.axaml.cs | 6 +- .../Components/Stcn24ForumWidget.axaml.cs | 22 +- .../StudyDeductionReasonsWidget.axaml.cs | 8 +- .../StudyEnvironmentWidget.axaml.cs | 4 +- .../StudyInterruptDensityWidget.axaml.cs | 8 +- .../Components/StudyNoiseCurveWidget.axaml.cs | 4 +- .../StudyNoiseDistributionWidget.axaml.cs | 4 +- .../StudyScoreOverviewWidget.axaml.cs | 8 +- .../StudySessionControlWidget.axaml.cs | 4 +- .../StudySessionHistoryWidget.axaml.cs | 18 +- .../Views/Components/TimerWidget.axaml.cs | 3 +- .../Components/WorldClockWidget.axaml.cs | 2 +- 40 files changed, 1666 insertions(+), 571 deletions(-) create mode 100644 LanMountainDesktop.DesktopComponents.Runtime/ComponentTypographyLayoutModels.cs create mode 100644 LanMountainDesktop.DesktopComponents.Runtime/ComponentTypographyLayoutService.cs diff --git a/Directory.Build.props b/Directory.Build.props index 327b321..3a012b7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,5 +4,6 @@ net10.0 enable enable + $(DefaultItemExcludes);**\obj_audit\** diff --git a/LanMountainDesktop.DesktopComponents.Runtime/ComponentTypographyLayoutModels.cs b/LanMountainDesktop.DesktopComponents.Runtime/ComponentTypographyLayoutModels.cs new file mode 100644 index 0000000..e9acce4 --- /dev/null +++ b/LanMountainDesktop.DesktopComponents.Runtime/ComponentTypographyLayoutModels.cs @@ -0,0 +1,30 @@ +using Avalonia; +using Avalonia.Media; + +namespace LanMountainDesktop.DesktopComponents.Runtime; + +public readonly record struct ComponentAdaptiveTextLayout( + double FontSize, + FontWeight Weight, + int MaxLines, + double LineHeight, + double OverflowScore, + bool FitsCompletely, + Size MeasuredSize) +{ + public double MeasuredWidth => MeasuredSize.Width; + + public double MeasuredHeight => MeasuredSize.Height; +} + +public readonly record struct ComponentBoxLayout( + double Width, + double Height, + Thickness Margin, + Thickness Padding) +{ + public double Size => Math.Max(Width, Height); + + public bool IsSquare => Math.Abs(Width - Height) <= 0.001d; +} + diff --git a/LanMountainDesktop.DesktopComponents.Runtime/ComponentTypographyLayoutService.cs b/LanMountainDesktop.DesktopComponents.Runtime/ComponentTypographyLayoutService.cs new file mode 100644 index 0000000..a8da07a --- /dev/null +++ b/LanMountainDesktop.DesktopComponents.Runtime/ComponentTypographyLayoutService.cs @@ -0,0 +1,314 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using System.Text; + +namespace LanMountainDesktop.DesktopComponents.Runtime; + +public static class ComponentTypographyLayoutService +{ + public static Size MeasureTextSize( + string? text, + double fontSize, + FontWeight weight, + double maxWidth, + double lineHeight, + FontFamily? fontFamily = null) + { + var probe = new TextBlock + { + Text = NormalizeText(text), + FontSize = Math.Max(1, fontSize), + FontWeight = weight, + TextWrapping = TextWrapping.Wrap, + LineHeight = Math.Max(1, lineHeight) + }; + + if (fontFamily is not null) + { + probe.FontFamily = fontFamily; + } + + probe.Measure(new Size(Math.Max(1, maxWidth), double.PositiveInfinity)); + return probe.DesiredSize; + } + + public static double FitFontSize( + string? text, + double maxWidth, + double maxHeight, + int maxLines, + double minFontSize, + double maxFontSize, + FontWeight weight, + double lineHeightFactor, + FontFamily? fontFamily = null) + { + var content = NormalizeText(text); + 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, fontFamily); + var lineCount = ResolveLineCount(size.Height, lineHeight); + var fits = size.Height <= maxHeight + 0.6d && lineCount <= Math.Max(1, maxLines); + + if (fits) + { + best = candidate; + low = candidate; + } + else + { + high = candidate; + } + } + + return best; + } + + public static ComponentAdaptiveTextLayout FitAdaptiveTextLayout( + string? text, + double maxWidth, + double maxHeight, + int minLines, + int maxLines, + double minFontSize, + double maxFontSize, + IEnumerable? weightCandidates = null, + double lineHeightFactor = 1.1d, + FontFamily? fontFamily = null) + { + var content = NormalizeText(text); + var safeMinLines = Math.Max(1, minLines); + var safeMaxLines = Math.Max(safeMinLines, maxLines); + var linesByHeight = ResolveMaxLinesByHeight(maxHeight, minFontSize, lineHeightFactor, safeMinLines, safeMaxLines); + + var candidates = weightCandidates?.ToArray(); + if (candidates is null || candidates.Length == 0) + { + candidates = new[] { FontWeight.Normal }; + } + + ComponentAdaptiveTextLayout? 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, + fontFamily); + + var lineHeight = fontSize * lineHeightFactor; + var measuredSize = MeasureTextSize(content, fontSize, weight, Math.Max(1, maxWidth), lineHeight, fontFamily); + 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 candidate = new ComponentAdaptiveTextLayout( + fontSize, + weight, + lineLimit, + lineHeight, + overflowScore, + overflowLines == 0 && overflowHeight <= 0.6d, + measuredSize); + + 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 ComponentAdaptiveTextLayout( + fallbackFontSize, + FontWeight.Normal, + safeMinLines, + fallbackFontSize * lineHeightFactor, + double.MaxValue, + false, + MeasureTextSize(content, fallbackFontSize, FontWeight.Normal, Math.Max(1, maxWidth), fallbackFontSize * lineHeightFactor, fontFamily)); + } + + public 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.6d); + var linesByHeight = (int)Math.Floor(maxHeightWithTolerance / lineHeight); + return Math.Clamp(linesByHeight, safeMinLines, safeMaxLines); + } + + public static int ResolveLineCount(double measuredHeight, double lineHeight) + { + return Math.Max(1, (int)Math.Ceiling(measuredHeight / Math.Max(1, lineHeight))); + } + + public static int EstimateDisplayUnits( + double availableWidth, + double unitWidth, + double gapWidth = 0, + double reservedWidth = 0, + int minUnits = 1, + int maxUnits = int.MaxValue) + { + var safeMinUnits = Math.Max(1, minUnits); + var safeMaxUnits = Math.Max(safeMinUnits, maxUnits); + var usableWidth = Math.Max(0, availableWidth - reservedWidth); + var safeGapWidth = Math.Max(0, gapWidth); + var raw = safeGapWidth > 0 + ? (usableWidth + safeGapWidth) / Math.Max(1, unitWidth + safeGapWidth) + : usableWidth / Math.Max(1, unitWidth); + + return Math.Clamp((int)Math.Floor(raw), safeMinUnits, safeMaxUnits); + } + + public static int CountTextDisplayUnits(string? text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return 0; + } + + var total = 0; + foreach (var rune in text.EnumerateRunes()) + { + if (Rune.IsWhiteSpace(rune)) + { + continue; + } + + total += IsCjkRune(rune) ? 2 : 1; + } + + return total; + } + + public static ComponentBoxLayout ResolveBadgeBox( + double availableWidth, + double availableHeight, + double preferredSizeScale = 0.42d, + double minSize = 10, + double maxSize = 24, + double insetScale = 0.2d) + { + var edge = Math.Min(Math.Max(1, availableWidth), Math.Max(1, availableHeight)); + var size = Math.Clamp(edge * preferredSizeScale, minSize, maxSize); + var inset = Math.Clamp(size * insetScale, 0, size * 0.35d); + return new ComponentBoxLayout(size, size, new Thickness(0, inset, 0, 0), new Thickness(inset)); + } + + public static ComponentBoxLayout ResolveGlyphBox( + double availableWidth, + double availableHeight, + double preferredSizeScale = 0.50d, + double minSize = 12, + double maxSize = 28, + double insetScale = 0.18d) + { + var edge = Math.Min(Math.Max(1, availableWidth), Math.Max(1, availableHeight)); + var size = Math.Clamp(edge * preferredSizeScale, minSize, maxSize); + var inset = Math.Clamp(size * insetScale, 0, size * 0.30d); + return new ComponentBoxLayout(size, size, new Thickness(inset), new Thickness(inset)); + } + + private static string NormalizeText(string? text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return " "; + } + + return string.Join(" ", text.Trim().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)); + } + + private static bool IsCjkRune(Rune rune) + { + var value = rune.Value; + return (value >= 0x4E00 && value <= 0x9FFF) || // CJK Unified Ideographs + (value >= 0x3400 && value <= 0x4DBF) || // CJK Unified Ideographs Extension A + (value >= 0x20000 && value <= 0x2A6DF) || // CJK Unified Ideographs Extension B + (value >= 0x2A700 && value <= 0x2B73F) || // CJK Unified Ideographs Extension C + (value >= 0x2B740 && value <= 0x2B81F) || // CJK Unified Ideographs Extension D + (value >= 0x2B820 && value <= 0x2CEAF) || // CJK Unified Ideographs Extension E/F + (value >= 0xF900 && value <= 0xFAFF) || // CJK Compatibility Ideographs + (value >= 0x2F800 && value <= 0x2FA1F) || // CJK Compatibility Ideographs Supplement + (value >= 0x3040 && value <= 0x309F) || // Hiragana + (value >= 0x30A0 && value <= 0x30FF) || // Katakana + (value >= 0xAC00 && value <= 0xD7AF); // Hangul Syllables + } + + private static bool IsBetterAdaptiveTextCandidate(ComponentAdaptiveTextLayout candidate, ComponentAdaptiveTextLayout 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.12d) + { + return true; + } + + if (Math.Abs(candidate.FontSize - best.FontSize) <= 0.12d && candidate.MaxLines < best.MaxLines) + { + return true; + } + + return false; + } + + if (candidate.OverflowScore < best.OverflowScore - 0.2d) + { + return true; + } + + if (Math.Abs(candidate.OverflowScore - best.OverflowScore) <= 0.2d && + candidate.FontSize > best.FontSize + 0.12d) + { + return true; + } + + if (Math.Abs(candidate.OverflowScore - best.OverflowScore) <= 0.2d && + Math.Abs(candidate.FontSize - best.FontSize) <= 0.12d && + candidate.MaxLines > best.MaxLines) + { + return true; + } + + return false; + } +} diff --git a/LanMountainDesktop/Services/IMusicControlService.cs b/LanMountainDesktop/Services/IMusicControlService.cs index 307642b..91cbc28 100644 --- a/LanMountainDesktop/Services/IMusicControlService.cs +++ b/LanMountainDesktop/Services/IMusicControlService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -28,7 +29,9 @@ public sealed record MusicPlaybackState( MusicPlaybackStatus PlaybackStatus, bool CanPlayPause, bool CanSkipPrevious, - bool CanSkipNext) + bool CanSkipNext, + bool CanToggleFavorite, + bool IsFavorite) { public static MusicPlaybackState Unsupported() { @@ -46,7 +49,9 @@ public sealed record MusicPlaybackState( PlaybackStatus: MusicPlaybackStatus.Unknown, CanPlayPause: false, CanSkipPrevious: false, - CanSkipNext: false); + CanSkipNext: false, + CanToggleFavorite: false, + IsFavorite: false); } public static MusicPlaybackState NoSession(bool isSupported = true) @@ -65,7 +70,35 @@ public sealed record MusicPlaybackState( PlaybackStatus: MusicPlaybackStatus.Unknown, CanPlayPause: false, CanSkipPrevious: false, - CanSkipNext: false); + CanSkipNext: false, + CanToggleFavorite: false, + IsFavorite: false); + } +} + +public sealed record MusicQueueItem( + string Id, + string Title, + string Artist, + string AlbumTitle, + byte[]? ThumbnailBytes, + TimeSpan Duration, + bool IsCurrentItem); + +public sealed record MusicQueueState( + bool IsSupported, + IReadOnlyList Items, + int CurrentIndex, + bool HasMoreItems) +{ + public static MusicQueueState Unsupported() + { + return new MusicQueueState(false, Array.Empty(), -1, false); + } + + public static MusicQueueState Empty() + { + return new MusicQueueState(true, Array.Empty(), -1, false); } } @@ -80,6 +113,18 @@ public interface IMusicControlService Task SkipPreviousAsync(CancellationToken cancellationToken = default); Task LaunchSourceAppAsync(CancellationToken cancellationToken = default); + + Task ToggleFavoriteAsync(CancellationToken cancellationToken = default); + + Task GetPlaybackQueueAsync(int maxItems = 20, CancellationToken cancellationToken = default); + + event EventHandler? PlaybackStateChanged; + + event EventHandler? QueueChanged; + + void StartListening(); + + void StopListening(); } public static class MusicControlServiceFactory @@ -118,4 +163,25 @@ internal sealed class NoOpMusicControlService : IMusicControlService { return Task.FromResult(false); } + + public Task ToggleFavoriteAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(false); + } + + public Task GetPlaybackQueueAsync(int maxItems = 20, CancellationToken cancellationToken = default) + { + return Task.FromResult(MusicQueueState.Unsupported()); + } + + public event EventHandler? PlaybackStateChanged; + public event EventHandler? QueueChanged; + + public void StartListening() + { + } + + public void StopListening() + { + } } diff --git a/LanMountainDesktop/Services/WindowsSmtcMusicControlService.cs b/LanMountainDesktop/Services/WindowsSmtcMusicControlService.cs index bdf5b54..88d5290 100644 --- a/LanMountainDesktop/Services/WindowsSmtcMusicControlService.cs +++ b/LanMountainDesktop/Services/WindowsSmtcMusicControlService.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -9,8 +10,9 @@ using System.Threading.Tasks; namespace LanMountainDesktop.Services; -public sealed class WindowsSmtcMusicControlService : IMusicControlService +public sealed class WindowsSmtcMusicControlService : IMusicControlService, IDisposable { + // WinRT Type Resolution private static readonly Type? SessionManagerType = ResolveWinRtType("Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager"); private static readonly Type? AppInfoType = ResolveWinRtType("Windows.ApplicationModel.AppInfo"); private static readonly MethodInfo? RequestSessionManagerAsyncMethod = @@ -18,15 +20,250 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService private static readonly MethodInfo? AsTaskGenericMethodDefinition = ResolveAsTaskGenericMethod(); private static readonly MethodInfo? AsStreamForReadMethod = ResolveAsStreamForReadMethod(); + // Synchronization private static readonly SemaphoreSlim ManagerLock = new(1, 1); private static object? _sessionManager; + // Instance State private readonly ConcurrentDictionary _sourceAppNameCache = new(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _stateGate = new(1, 1); + private readonly object _sessionLock = new(); + // Event State + private object? _currentSession; + private bool _isListening; + private readonly List _eventHandlers = new(); + + // Thumbnail Cache private string _thumbnailKey = string.Empty; private byte[]? _thumbnailBytesCache; + // Events + public event EventHandler? PlaybackStateChanged; + public event EventHandler? QueueChanged; + + public void StartListening() + { + if (_isListening || !IsRuntimeSupported()) + { + return; + } + + _isListening = true; + _ = InitializeSessionManagerAsync(); + } + + public void StopListening() + { + if (!_isListening) + { + return; + } + + _isListening = false; + UnsubscribeFromSessionEvents(); + } + + private async Task InitializeSessionManagerAsync() + { + try + { + var manager = await GetSessionManagerAsync(CancellationToken.None); + if (manager is null) + { + return; + } + + // Subscribe to CurrentSessionChanged event + var currentSessionChangedEvent = SessionManagerType?.GetEvent("CurrentSessionChanged"); + if (currentSessionChangedEvent is not null) + { + var handler = CreateTypedEventHandler( + currentSessionChangedEvent.EventHandlerType, + OnCurrentSessionChanged); + currentSessionChangedEvent.AddEventHandler(manager, handler); + _eventHandlers.Add(handler); + } + + // Get initial session and subscribe to its events + await UpdateCurrentSessionAsync(); + } + catch (Exception ex) + { + AppLogger.Warn("MusicControl", "Failed to initialize SMTC session manager", ex); + } + } + + private async Task OnCurrentSessionChanged(object? sender, object? args) + { + await UpdateCurrentSessionAsync(); + await RaisePlaybackStateChangedAsync(); + } + + private async Task UpdateCurrentSessionAsync() + { + lock (_sessionLock) + { + UnsubscribeFromSessionEvents(); + _currentSession = null; + } + + var session = await GetCurrentSessionAsync(CancellationToken.None); + if (session is null) + { + return; + } + + lock (_sessionLock) + { + _currentSession = session; + SubscribeToSessionEvents(session); + } + } + + private void SubscribeToSessionEvents(object session) + { + if (!_isListening) + { + return; + } + + try + { + // MediaPropertiesChanged event + var mediaPropertiesChanged = session.GetType().GetEvent("MediaPropertiesChanged"); + if (mediaPropertiesChanged is not null) + { + var handler = CreateTypedEventHandler( + mediaPropertiesChanged.EventHandlerType, + async (s, e) => await RaisePlaybackStateChangedAsync()); + mediaPropertiesChanged.AddEventHandler(session, handler); + _eventHandlers.Add(handler); + } + + // PlaybackInfoChanged event + var playbackInfoChanged = session.GetType().GetEvent("PlaybackInfoChanged"); + if (playbackInfoChanged is not null) + { + var handler = CreateTypedEventHandler( + playbackInfoChanged.EventHandlerType, + async (s, e) => await RaisePlaybackStateChangedAsync()); + playbackInfoChanged.AddEventHandler(session, handler); + _eventHandlers.Add(handler); + } + + // TimelinePropertiesChanged event + var timelinePropertiesChanged = session.GetType().GetEvent("TimelinePropertiesChanged"); + if (timelinePropertiesChanged is not null) + { + var handler = CreateTypedEventHandler( + timelinePropertiesChanged.EventHandlerType, + async (s, e) => await RaisePlaybackStateChangedAsync()); + timelinePropertiesChanged.AddEventHandler(session, handler); + _eventHandlers.Add(handler); + } + } + catch (Exception ex) + { + AppLogger.Warn("MusicControl", "Failed to subscribe to session events", ex); + } + } + + private void UnsubscribeFromSessionEvents() + { + if (_currentSession is null) + { + return; + } + + try + { + var sessionType = _currentSession.GetType(); + + // Remove MediaPropertiesChanged + var mediaPropertiesChanged = sessionType.GetEvent("MediaPropertiesChanged"); + if (mediaPropertiesChanged is not null) + { + foreach (var handler in _eventHandlers) + { + try + { + mediaPropertiesChanged.RemoveEventHandler(_currentSession, handler); + } + catch { } + } + } + + // Remove PlaybackInfoChanged + var playbackInfoChanged = sessionType.GetEvent("PlaybackInfoChanged"); + if (playbackInfoChanged is not null) + { + foreach (var handler in _eventHandlers) + { + try + { + playbackInfoChanged.RemoveEventHandler(_currentSession, handler); + } + catch { } + } + } + + // Remove TimelinePropertiesChanged + var timelinePropertiesChanged = sessionType.GetEvent("TimelinePropertiesChanged"); + if (timelinePropertiesChanged is not null) + { + foreach (var handler in _eventHandlers) + { + try + { + timelinePropertiesChanged.RemoveEventHandler(_currentSession, handler); + } + catch { } + } + } + } + catch { } + + _eventHandlers.Clear(); + } + + private Delegate CreateTypedEventHandler(Type eventHandlerType, Func asyncAction) + { + // Create a delegate that wraps the async action + var handler = new EventHandler((sender, args) => + { + _ = asyncAction(sender, args); + }); + + return handler; + } + + private async Task RaisePlaybackStateChangedAsync() + { + try + { + var state = await GetCurrentStateAsync(CancellationToken.None); + PlaybackStateChanged?.Invoke(this, state); + } + catch (Exception ex) + { + AppLogger.Warn("MusicControl", "Failed to raise playback state changed event", ex); + } + } + + private async Task RaiseQueueChangedAsync() + { + try + { + var queue = await GetPlaybackQueueAsync(20, CancellationToken.None); + QueueChanged?.Invoke(this, queue); + } + catch (Exception ex) + { + AppLogger.Warn("MusicControl", "Failed to raise queue changed event", ex); + } + } + public async Task GetCurrentStateAsync(CancellationToken cancellationToken = default) { if (!IsRuntimeSupported()) @@ -56,10 +293,17 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService var canSkipNext = ReadBoolProperty(controls, "IsNextEnabled"); var canSkipPrevious = ReadBoolProperty(controls, "IsPreviousEnabled"); + // Check for AutoRepeatModeChange and ShuffleEnabledChange support (indicates advanced SMTC) + var canToggleFavorite = ReadBoolProperty(controls, "IsChannelDownEnabled") || ReadBoolProperty(controls, "IsChannelUpEnabled"); + + // Try to get IsFavorite from mediaProperties (some apps support this) + var isFavorite = ReadBoolProperty(mediaProperties, "IsFavorite"); + var sourceAppId = ReadStringProperty(session, "SourceAppUserModelId"); var sourceAppName = await ResolveSourceAppDisplayNameAsync(sourceAppId, cancellationToken); - var timeline = InvokeMethod(session, "GetTimelineProperties"); + // Use async method to get timeline properties + var timeline = await TryGetTimelinePropertiesAsync(session, cancellationToken); var position = ReadTimeSpanProperty(timeline, "Position"); var start = ReadTimeSpanProperty(timeline, "StartTime"); var end = ReadTimeSpanProperty(timeline, "EndTime"); @@ -103,7 +347,9 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService PlaybackStatus: MapPlaybackStatus(playbackStatusRaw), CanPlayPause: canPlayPause, CanSkipPrevious: canSkipPrevious, - CanSkipNext: canSkipNext); + CanSkipNext: canSkipNext, + CanToggleFavorite: canToggleFavorite, + IsFavorite: isFavorite); } catch { @@ -199,6 +445,113 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService return await AwaitBooleanWinRtOperationAsync(InvokeMethod(session, "TrySkipPreviousAsync"), cancellationToken); } + public async Task ToggleFavoriteAsync(CancellationToken cancellationToken = default) + { + if (!IsRuntimeSupported()) + { + return false; + } + + var session = await GetCurrentSessionAsync(cancellationToken); + if (session is null) + { + return false; + } + + // Try to toggle favorite using RateAndReview (some apps support this) + try + { + var playbackInfo = GetPropertyValue(session, "PlaybackInfo") ?? InvokeMethod(session, "GetPlaybackInfo"); + var controls = GetPropertyValue(playbackInfo, "Controls"); + + // Check if RateAndReview is supported + if (ReadBoolProperty(controls, "IsRateEnabled")) + { + var operation = InvokeMethod(session, "TryRateAsync"); + return await AwaitBooleanWinRtOperationAsync(operation, cancellationToken); + } + + // Fallback: Try ChannelUp/ChannelDown as favorite toggle + if (ReadBoolProperty(controls, "IsChannelUpEnabled")) + { + var operation = InvokeMethod(session, "TryChannelUpAsync"); + return await AwaitBooleanWinRtOperationAsync(operation, cancellationToken); + } + } + catch { } + + return false; + } + + public async Task GetPlaybackQueueAsync(int maxItems = 20, CancellationToken cancellationToken = default) + { + if (!IsRuntimeSupported()) + { + return MusicQueueState.Unsupported(); + } + + var session = await GetCurrentSessionAsync(cancellationToken); + if (session is null) + { + return MusicQueueState.Empty(); + } + + try + { + // Try to get playback queue using GetPlaybackInfo + var playbackInfo = GetPropertyValue(session, "PlaybackInfo") ?? InvokeMethod(session, "GetPlaybackInfo"); + + // Check if shuffle/repeat controls exist (indicates queue support) + var controls = GetPropertyValue(playbackInfo, "Controls"); + var canShuffle = ReadBoolProperty(controls, "IsShuffleEnabled"); + var canRepeat = ReadBoolProperty(controls, "IsRepeatEnabled"); + + // Since SMTC doesn't expose the actual queue directly, we'll return a simplified state + // indicating whether queue navigation is supported + var items = new List(); + + // Try to get current media properties as the current item + var mediaProperties = await TryGetMediaPropertiesAsync(session, cancellationToken); + if (mediaProperties is not null) + { + var title = ReadStringProperty(mediaProperties, "Title"); + var artist = ReadStringProperty(mediaProperties, "Artist"); + var albumTitle = ReadStringProperty(mediaProperties, "AlbumTitle"); + var thumbnailBytes = await ResolveThumbnailBytesAsync( + mediaProperties, + ReadStringProperty(session, "SourceAppUserModelId"), + title, artist, albumTitle, + cancellationToken); + + // Get duration + var timeline = await TryGetTimelinePropertiesAsync(session, cancellationToken); + var duration = ReadTimeSpanProperty(timeline, "EndTime") - ReadTimeSpanProperty(timeline, "StartTime"); + + items.Add(new MusicQueueItem( + Id: "current", + Title: title, + Artist: artist, + AlbumTitle: albumTitle, + ThumbnailBytes: thumbnailBytes, + Duration: duration > TimeSpan.Zero ? duration : TimeSpan.Zero, + IsCurrentItem: true)); + } + + // If shuffle or repeat is supported, we assume there's a queue + var hasMoreItems = canShuffle || canRepeat || ReadBoolProperty(controls, "IsNextEnabled"); + + return new MusicQueueState( + IsSupported: true, + Items: items, + CurrentIndex: 0, + HasMoreItems: hasMoreItems); + } + catch + { + return MusicQueueState.Empty(); + } + } + public async Task LaunchSourceAppAsync(CancellationToken cancellationToken = default) { if (!IsRuntimeSupported()) @@ -259,6 +612,20 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService return await AwaitWinRtOperationAsync(operation, cancellationToken); } + private async Task TryGetTimelinePropertiesAsync(object session, CancellationToken cancellationToken) + { + // Use the async method TryGetTimelinePropertiesAsync if available + var tryGetTimelineMethod = session.GetType().GetMethod("TryGetTimelinePropertiesAsync"); + if (tryGetTimelineMethod is not null) + { + var operation = tryGetTimelineMethod.Invoke(session, null); + return await AwaitWinRtOperationAsync(operation, cancellationToken); + } + + // Fallback to synchronous method + return InvokeMethod(session, "GetTimelineProperties"); + } + private async Task ResolveThumbnailBytesAsync( object? mediaProperties, string sourceAppId, @@ -576,4 +943,11 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService _ => MusicPlaybackStatus.Unknown }; } + + public void Dispose() + { + StopListening(); + _stateGate.Dispose(); + ManagerLock.Dispose(); + } } diff --git a/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs index 27a2137..30e769b 100644 --- a/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs @@ -327,7 +327,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I var scale = ResolveScale(); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(42 * scale, 16, 56); - RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 26)); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(14 * scale, 14 * scale, null, 0.55d); ApplyModeVisualIfNeeded(); } diff --git a/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs b/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs index e9187e8..d667c91 100644 --- a/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs @@ -382,15 +382,21 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells; RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52); - RootBorder.Padding = new Thickness(0); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 10 * softScale, + 8 * softScale, + null, + 0.45d); var horizontalPadding = Math.Clamp(16 * softScale, 8, 24); var verticalPadding = Math.Clamp(14 * softScale, 7, 20); CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52); - CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); + CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d); - var innerWidth = Math.Max(120, totalWidth - (horizontalPadding * 2d)); - var innerHeight = Math.Max(72, totalHeight - (verticalPadding * 2d)); + var rootPadding = RootBorder.Padding; + var cardPadding = CardBorder.Padding; + var innerWidth = Math.Max(120, totalWidth - rootPadding.Left - rootPadding.Right - cardPadding.Left - cardPadding.Right); + var innerHeight = Math.Max(72, totalHeight - rootPadding.Top - rootPadding.Bottom - cardPadding.Top - cardPadding.Bottom); var rowSpacing = Math.Clamp(6 * softScale, 2, 9); ContentGrid.RowSpacing = rowSpacing; HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16); diff --git a/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs b/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs index c0f800f..1940596 100644 --- a/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs @@ -387,15 +387,21 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells; RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52); - RootBorder.Padding = new Thickness(0); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 10 * softScale, + 8 * softScale, + null, + 0.45d); var horizontalPadding = Math.Clamp(16 * softScale, 8, 24); var verticalPadding = Math.Clamp(14 * softScale, 7, 20); CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * softScale, 16, 52); - CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); + CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d); - var innerWidth = Math.Max(120, totalWidth - (horizontalPadding * 2d)); - var innerHeight = Math.Max(72, totalHeight - (verticalPadding * 2d)); + var rootPadding = RootBorder.Padding; + var cardPadding = CardBorder.Padding; + var innerWidth = Math.Max(120, totalWidth - rootPadding.Left - rootPadding.Right - cardPadding.Left - cardPadding.Right); + var innerHeight = Math.Max(72, totalHeight - rootPadding.Top - rootPadding.Bottom - cardPadding.Top - cardPadding.Bottom); var rowSpacing = Math.Clamp(6 * softScale, 2, 9); ContentGrid.RowSpacing = rowSpacing; HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16); diff --git a/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs b/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs index 591f2d9..b552955 100644 --- a/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs @@ -80,11 +80,14 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget, _currentCellSize = Math.Max(1, cellSize); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 12, 28); - RootBorder.Padding = new Thickness(Math.Clamp(_currentCellSize * 0.20, 8, 18)); + RootBorder.Padding = new Thickness( + ComponentChromeCornerRadiusHelper.SafeValue(_currentCellSize * 0.20, 8, 18)); WebViewHostBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.24, 10, 22); AddressBarBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.22, 10, 20); - AddressBarBorder.Padding = new Thickness(8, 6); + AddressBarBorder.Padding = new Thickness( + ComponentChromeCornerRadiusHelper.SafeValue(8, 6, 12), + ComponentChromeCornerRadiusHelper.SafeValue(6, 4, 10)); if (RootBorder.Child is Grid rootGrid) { diff --git a/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs b/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs index 06d24a3..0108e4e 100644 --- a/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs @@ -500,10 +500,10 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, var secondarySize = Math.Clamp(29 * scale, 10, 28); var lineSpacing = Math.Clamp(4 * scale, 1.5, 8); var itemPadding = new Thickness( - Math.Clamp(6 * scale, 3, 10), - Math.Clamp(4 * scale, 2, 8), - Math.Clamp(4 * scale, 2, 8), - Math.Clamp(4 * scale, 2, 8)); + 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); var primaryBrush = CreateBrush(_isNightVisual ? "#F9FBFF" : "#151821"); diff --git a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs index d16b1c1..678cc54 100644 --- a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs @@ -546,14 +546,24 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells; RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52); - RootBorder.Padding = new Thickness(0); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 10 * scale, + 8 * scale, + null, + 0.45d); CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52); - CardBorder.Padding = new Thickness( + CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( Math.Clamp(16 * scale, 8, 24), Math.Clamp(14 * scale, 7, 22), - Math.Clamp(16 * scale, 8, 24), - Math.Clamp(14 * scale, 7, 22)); + null, + 0.55d); + + var rootPadding = RootBorder.Padding; + var cardPadding = CardBorder.Padding; + var contentWidth = Math.Max( + 150, + totalWidth - rootPadding.Left - rootPadding.Right - cardPadding.Left - cardPadding.Right); var headlineFont = Math.Clamp(24 * scale, 12, 34); BrandPrimaryTextBlock.FontSize = headlineFont; @@ -567,7 +577,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, RefreshGlyphIcon.FontSize = Math.Clamp(19 * scale, 11, 24); RefreshLabelTextBlock.FontSize = Math.Clamp(22 * scale, 11, 29); - var imageWidth = Math.Clamp(totalWidth * 0.20, 60, 170); + var imageWidth = Math.Clamp(contentWidth * 0.20, 60, 170); var imageHeight = Math.Clamp(imageWidth * 0.56, 38, 94); News1ImageHost.Width = imageWidth; News1ImageHost.Height = imageHeight; @@ -584,7 +594,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget, var availableTextWidth = Math.Max( 84, - totalWidth - imageWidth - columnGap - Math.Clamp(20 * scale, 10, 32)); + contentWidth - imageWidth - columnGap - Math.Clamp(20 * scale, 10, 32)); News1TitleTextBlock.MaxWidth = availableTextWidth; News2TitleTextBlock.MaxWidth = availableTextWidth; diff --git a/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs b/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs index 355ab95..0a2b0c9 100644 --- a/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs +++ b/LanMountainDesktop/Views/Components/ComponentChromeCornerRadiusHelper.cs @@ -69,4 +69,28 @@ internal static class ComponentChromeCornerRadiusHelper var safetyScale = ResolveContentSafetyScale(chromeContext, responsiveness); return Math.Clamp(baseValue * safetyScale, min * safetyScale, max * safetyScale); } + + public static Thickness SafeThickness( + double left, + double top, + double right, + double bottom, + ComponentChromeContext? chromeContext = null, + double responsiveness = 0.45d) + { + return new Thickness( + SafeValue(left, 0, left, chromeContext, responsiveness), + SafeValue(top, 0, top, chromeContext, responsiveness), + SafeValue(right, 0, right, chromeContext, responsiveness), + SafeValue(bottom, 0, bottom, chromeContext, responsiveness)); + } + + public static Thickness SafeThickness( + double horizontal, + double vertical, + ComponentChromeContext? chromeContext = null, + double responsiveness = 0.45d) + { + return SafeThickness(horizontal, vertical, horizontal, vertical, chromeContext, responsiveness); + } } diff --git a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs index e1bb3d3..8e59121 100644 --- a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs @@ -102,18 +102,23 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, var scale = ResolveScale(); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 12 * scale, + 10 * scale, + null, + 0.45d); - InfoPanel.Padding = new Thickness( - Math.Clamp(18 * scale, 10, 28), - Math.Clamp(14 * scale, 8, 22), - Math.Clamp(18 * scale, 10, 28), - Math.Clamp(14 * scale, 8, 22)); + InfoPanel.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 18 * scale, + 14 * scale, + null, + 0.52d); DateInfoStack.Margin = new Thickness( - Math.Clamp(18 * scale, 8, 30), + ComponentChromeCornerRadiusHelper.SafeValue(18 * scale, 8, 30), 0, 0, - Math.Clamp(16 * scale, 8, 26)); + ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 8, 26)); DateInfoStack.Spacing = Math.Clamp(4 * scale, 2, 10); StatusTextBlock.FontSize = Math.Clamp(16 * scale, 10, 24); @@ -425,16 +430,18 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, var scale = ResolveScale(); var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells; var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells; + var rootPadding = RootBorder.Padding; var leftStar = totalWidth < _currentCellSize * 4.2 ? 2.0 : 2.08; MainLayoutGrid.ColumnDefinitions[0].Width = new GridLength(leftStar, GridUnitType.Star); MainLayoutGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star); - var rightPanelWidth = Math.Max(84, totalWidth / (leftStar + 1)); + var availableWidth = Math.Max(84, totalWidth - rootPadding.Left - rootPadding.Right); + var rightPanelWidth = Math.Max(84, availableWidth / (leftStar + 1)); var rightContentWidth = Math.Max(58, rightPanelWidth - InfoPanel.Padding.Left - InfoPanel.Padding.Right); - var leftPanelWidth = Math.Max(84, totalWidth - rightPanelWidth); + var leftPanelWidth = Math.Max(84, availableWidth - rightPanelWidth); var leftContentWidth = Math.Max(52, leftPanelWidth - DateInfoStack.Margin.Left - 10); - var leftContentHeight = Math.Max(30, totalHeight - DateInfoStack.Margin.Bottom - 10); + var leftContentHeight = Math.Max(30, totalHeight - rootPadding.Top - rootPadding.Bottom - DateInfoStack.Margin.Bottom - 10); var dateStackSpacing = Math.Clamp(4 * scale, 2, 10); DateInfoStack.Spacing = dateStackSpacing; @@ -464,7 +471,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, lineHeightFactor: 1.10); WeekdayTextBlock.LineHeight = WeekdayTextBlock.FontSize * 1.10; - var rightContentHeight = Math.Max(42, totalHeight - InfoPanel.Padding.Top - InfoPanel.Padding.Bottom); + var rightContentHeight = Math.Max(42, totalHeight - rootPadding.Top - rootPadding.Bottom - InfoPanel.Padding.Top - InfoPanel.Padding.Bottom); var titleBottomMargin = Math.Clamp(8 * scale, 4, 14); var separatorBottomMargin = Math.Clamp(10 * scale, 4, 14); var bottomStackSpacing = Math.Clamp(3 * scale, 2, 8); diff --git a/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs index b03930a..8537497 100644 --- a/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs @@ -93,25 +93,25 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I var scale = ResolveScale(); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52); - RootBorder.Padding = new Thickness( - Math.Clamp(20 * scale, 10, 34), - Math.Clamp(16 * scale, 8, 28), - Math.Clamp(20 * scale, 10, 34), - Math.Clamp(14 * scale, 7, 24)); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 20 * scale, + 16 * scale, + null, + 0.55d); QuoteMarkTextBlock.FontSize = Math.Clamp(80 * scale, 32, 120); QuoteMarkTextBlock.LineHeight = Math.Clamp(68 * scale, 26, 100); QuoteMarkTextBlock.Margin = new Thickness(Math.Clamp(1 * scale, 0, 3), 0, 0, 0); PoetryContentTextBlock.Margin = new Thickness( - Math.Clamp(8 * scale, 4, 16), - Math.Clamp(2 * scale, 0, 8), + ComponentChromeCornerRadiusHelper.SafeValue(8 * scale, 4, 16), + ComponentChromeCornerRadiusHelper.SafeValue(2 * scale, 0, 8), 0, 0); AuthorAccent.Width = Math.Clamp(6 * scale, 3.2, 9.5); AuthorAccent.Height = Math.Clamp(24 * scale, 12, 34); - AuthorAccent.Margin = new Thickness(0, 0, Math.Clamp(8 * scale, 4, 13), 0); + AuthorAccent.Margin = new Thickness(0, 0, ComponentChromeCornerRadiusHelper.SafeValue(8 * scale, 4, 13), 0); AuthorAccent.CornerRadius = new CornerRadius(Math.Clamp(3 * scale, 1.5, 4.5)); StatusTextBlock.FontSize = Math.Clamp(17 * scale, 9, 26); @@ -328,15 +328,13 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells; var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells; var scale = ResolveScale(); + var rootPadding = isNightMode + ? ComponentChromeCornerRadiusHelper.SafeThickness(20 * scale, 15 * scale, null, 0.55d) + : ComponentChromeCornerRadiusHelper.SafeThickness(20 * scale, 14 * scale, null, 0.55d); if (isNightMode) { RootBorder.Background = CreateBrush("#C5070D"); - RootBorder.Padding = new Thickness( - Math.Clamp(20 * scale, 10, 34), - Math.Clamp(15 * scale, 7, 24), - Math.Clamp(20 * scale, 10, 34), - Math.Clamp(14 * scale, 7, 24)); QuoteMarkTextBlock.IsVisible = true; QuoteMarkTextBlock.Foreground = CreateBrush("#4AF4C5A6"); @@ -360,11 +358,6 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I else { RootBorder.Background = CreateBrush("#F2F2F3"); - RootBorder.Padding = new Thickness( - Math.Clamp(20 * scale, 10, 34), - Math.Clamp(14 * scale, 6, 24), - Math.Clamp(20 * scale, 10, 34), - Math.Clamp(14 * scale, 7, 24)); QuoteMarkTextBlock.IsVisible = false; @@ -385,6 +378,7 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I StatusTextBlock.Foreground = CreateBrush("#8A8F98"); } + RootBorder.Padding = rootPadding; UpdateRefreshButtonState(); ApplyAdaptiveTextLayout(isNightMode, scale, totalWidth, totalHeight); } diff --git a/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs b/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs index ac45737..f3596ac 100644 --- a/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs +++ b/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs @@ -12,6 +12,7 @@ using Avalonia.Media; using Avalonia.Styling; using Avalonia.VisualTree; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -329,12 +330,17 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget, var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells; RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 14, 40); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 12 * scale, + 10 * scale, + null, + 0.45d); CardBorder.CornerRadius = RootBorder.CornerRadius; - CardBorder.Padding = new Thickness( - Math.Clamp(12 * scale, 8, 18), - Math.Clamp(11 * scale, 7, 16), - Math.Clamp(12 * scale, 8, 18), - Math.Clamp(11 * scale, 7, 16)); + CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 12 * scale, + 11 * scale, + null, + 0.55d); var refreshSize = Math.Clamp(30 * scale, 20, 38); RefreshButton.Width = refreshSize; @@ -342,44 +348,60 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget, RefreshButton.CornerRadius = new CornerRadius(refreshSize / 2d); RefreshIcon.FontSize = Math.Clamp(14 * scale, 10, 20); - var contentWidth = Math.Max(80, totalWidth - CardBorder.Padding.Left - CardBorder.Padding.Right); + var contentWidth = Math.Max( + 80, + totalWidth - RootBorder.Padding.Left - RootBorder.Padding.Right - CardBorder.Padding.Left - CardBorder.Padding.Right); var wordWidth = Math.Max(48, contentWidth - refreshSize - Math.Clamp(6 * scale, 4, 10)); WordTextBlock.MaxWidth = wordWidth; - var contentHeight = Math.Max(52, totalHeight - CardBorder.Padding.Top - CardBorder.Padding.Bottom); + var contentHeight = Math.Max( + 52, + totalHeight - RootBorder.Padding.Top - RootBorder.Padding.Bottom - CardBorder.Padding.Top - CardBorder.Padding.Bottom); var wordHeightBudget = Math.Max(18, contentHeight * 0.34); var detailHeightBudget = Math.Max(18, contentHeight - wordHeightBudget - Math.Clamp(8 * scale, 4, 14)); - WordTextBlock.FontSize = FitFontSize( + var wordLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( WordTextBlock.Text, wordWidth, wordHeightBudget, - maxLines: 1, - minFontSize: Math.Clamp(18 * scale, 12, 22), - maxFontSize: Math.Clamp(38 * scale, 20, 50), - weight: FontWeight.Bold, - lineHeightFactor: 1.02); - WordTextBlock.LineHeight = WordTextBlock.FontSize * 1.02; + 1, + 1, + Math.Clamp(18 * scale, 12, 22), + Math.Clamp(38 * scale, 20, 50), + [FontWeight.Bold], + 1.02, + MiSansFontFamily); + WordTextBlock.FontSize = wordLayout.FontSize; + WordTextBlock.FontWeight = wordLayout.Weight; + WordTextBlock.MaxLines = wordLayout.MaxLines; + WordTextBlock.TextWrapping = TextWrapping.NoWrap; + WordTextBlock.LineHeight = wordLayout.LineHeight; - var detailFont = FitFontSize( + var detailLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( MeaningTextBlock.IsVisible ? MeaningTextBlock.Text : HiddenHintTextBlock.Text, contentWidth, detailHeightBudget, - maxLines: MeaningTextBlock.IsVisible ? 5 : 4, - minFontSize: Math.Clamp(12 * scale, 9, 14), - maxFontSize: Math.Clamp(18 * scale, 12, 22), - weight: FontWeight.SemiBold, - lineHeightFactor: 1.10); + 1, + MeaningTextBlock.IsVisible ? 5 : 4, + Math.Clamp(12 * scale, 9, 14), + Math.Clamp(18 * scale, 12, 22), + [FontWeight.SemiBold, FontWeight.Medium], + 1.10, + MiSansFontFamily); MeaningTextBlock.MaxWidth = contentWidth; - MeaningTextBlock.FontSize = detailFont; - MeaningTextBlock.LineHeight = detailFont * 1.10; - MeaningTextBlock.MaxLines = totalHeight < _currentCellSize * 1.8 ? 4 : 5; + MeaningTextBlock.FontSize = detailLayout.FontSize; + MeaningTextBlock.FontWeight = detailLayout.Weight; + MeaningTextBlock.LineHeight = detailLayout.LineHeight; + MeaningTextBlock.MaxLines = Math.Min(detailLayout.MaxLines, totalHeight < _currentCellSize * 1.8 ? 4 : 5); + MeaningTextBlock.TextWrapping = MeaningTextBlock.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; HiddenHintTextBlock.MaxWidth = contentWidth; - HiddenHintTextBlock.FontSize = detailFont; - HiddenHintTextBlock.LineHeight = detailFont * 1.10; - HiddenHintTextBlock.MaxLines = totalHeight < _currentCellSize * 1.8 ? 3 : 4; + HiddenHintTextBlock.FontSize = detailLayout.FontSize; + HiddenHintTextBlock.FontWeight = detailLayout.Weight; + HiddenHintTextBlock.LineHeight = detailLayout.LineHeight; + HiddenHintTextBlock.MaxLines = Math.Min(detailLayout.MaxLines, totalHeight < _currentCellSize * 1.8 ? 3 : 4); + HiddenHintTextBlock.TextWrapping = HiddenHintTextBlock.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; StatusTextBlock.FontSize = Math.Clamp(14 * scale, 9, 18); } @@ -509,58 +531,4 @@ public partial class DailyWord2x2Widget : 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 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/DailyWordWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs index 9d6389d..ef466db 100644 --- a/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs @@ -10,6 +10,7 @@ using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -300,14 +301,18 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe var containerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52); RootBorder.CornerRadius = containerRadius; - RootBorder.Padding = new Thickness(0); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 10 * scale, + 8 * scale, + null, + 0.45d); CardBorder.CornerRadius = containerRadius; - CardBorder.Padding = new Thickness( - Math.Clamp(16 * scale, 8, 24), - Math.Clamp(14 * scale, 7, 22), - Math.Clamp(16 * scale, 8, 24), - Math.Clamp(14 * scale, 7, 22)); + CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 16 * scale, + 14 * scale, + null, + 0.55d); var refreshSize = Math.Clamp(38 * scale, 22, 48); RefreshButton.Width = refreshSize; @@ -346,65 +351,90 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe exampleHeightBudget += Math.Clamp(11 * scale, 5, 18); } - var wordBase = Math.Clamp(56 * scale, 18, 72); - WordTextBlock.FontSize = FitFontSize( + var wordLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( WordTextBlock.Text, wordWidth, wordHeightBudget, - maxLines: 1, - minFontSize: Math.Max(14, wordBase * 0.56), - maxFontSize: wordBase, - weight: FontWeight.Bold, - lineHeightFactor: 1.04); - WordTextBlock.LineHeight = WordTextBlock.FontSize * 1.04; + 1, + 1, + Math.Max(14, Math.Clamp(56 * scale, 18, 72) * 0.56), + Math.Clamp(56 * scale, 18, 72), + [FontWeight.Bold], + 1.04, + MiSansFontFamily); + WordTextBlock.FontSize = wordLayout.FontSize; + WordTextBlock.FontWeight = wordLayout.Weight; + WordTextBlock.MaxLines = wordLayout.MaxLines; + WordTextBlock.TextWrapping = TextWrapping.NoWrap; + WordTextBlock.LineHeight = wordLayout.LineHeight; - var pronunciationBase = Math.Clamp(27 * scale, 10, 36); - PronunciationTextBlock.FontSize = FitFontSize( + var pronunciationLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( PronunciationTextBlock.Text, contentWidth, pronunciationHeightBudget, - maxLines: 1, - minFontSize: Math.Max(8.6, pronunciationBase * 0.62), - maxFontSize: pronunciationBase, - weight: FontWeight.SemiBold, - lineHeightFactor: 1.08); - PronunciationTextBlock.LineHeight = PronunciationTextBlock.FontSize * 1.08; + 1, + 1, + Math.Max(8.6, Math.Clamp(27 * scale, 10, 36) * 0.62), + Math.Clamp(27 * scale, 10, 36), + [FontWeight.SemiBold, FontWeight.Medium], + 1.08, + MiSansFontFamily); + PronunciationTextBlock.FontSize = pronunciationLayout.FontSize; + PronunciationTextBlock.FontWeight = pronunciationLayout.Weight; + PronunciationTextBlock.MaxLines = pronunciationLayout.MaxLines; + PronunciationTextBlock.TextWrapping = TextWrapping.NoWrap; + PronunciationTextBlock.LineHeight = pronunciationLayout.LineHeight; - var meaningBase = Math.Clamp(25 * scale, 10, 34); - MeaningTextBlock.FontSize = FitFontSize( + var meaningLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( MeaningTextBlock.Text, contentWidth, meaningHeightBudget, - maxLines: Math.Max(1, MeaningTextBlock.MaxLines), - minFontSize: Math.Max(9.2, meaningBase * 0.60), - maxFontSize: meaningBase, - weight: FontWeight.SemiBold, - lineHeightFactor: 1.10); - MeaningTextBlock.LineHeight = MeaningTextBlock.FontSize * 1.10; + 1, + Math.Max(1, MeaningTextBlock.MaxLines), + Math.Max(9.2, Math.Clamp(25 * scale, 10, 34) * 0.60), + Math.Clamp(25 * scale, 10, 34), + [FontWeight.SemiBold, FontWeight.Medium], + 1.10, + MiSansFontFamily); + MeaningTextBlock.FontSize = meaningLayout.FontSize; + MeaningTextBlock.FontWeight = meaningLayout.Weight; + MeaningTextBlock.MaxLines = meaningLayout.MaxLines; + MeaningTextBlock.TextWrapping = meaningLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; + MeaningTextBlock.LineHeight = meaningLayout.LineHeight; - var exampleBase = Math.Clamp(22 * scale, 9, 30); - ExampleTextBlock.FontSize = FitFontSize( + var exampleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( ExampleTextBlock.Text, contentWidth, exampleHeightBudget, - maxLines: Math.Max(1, ExampleTextBlock.MaxLines), - minFontSize: Math.Max(8.8, exampleBase * 0.58), - maxFontSize: exampleBase, - weight: FontWeight.Medium, - lineHeightFactor: 1.08); - ExampleTextBlock.LineHeight = ExampleTextBlock.FontSize * 1.08; + 1, + Math.Max(1, ExampleTextBlock.MaxLines), + Math.Max(8.8, Math.Clamp(22 * scale, 9, 30) * 0.58), + Math.Clamp(22 * scale, 9, 30), + [FontWeight.Medium, FontWeight.SemiBold], + 1.08, + MiSansFontFamily); + ExampleTextBlock.FontSize = exampleLayout.FontSize; + ExampleTextBlock.FontWeight = exampleLayout.Weight; + ExampleTextBlock.MaxLines = exampleLayout.MaxLines; + ExampleTextBlock.TextWrapping = exampleLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; + ExampleTextBlock.LineHeight = exampleLayout.LineHeight; - var translationBase = Math.Clamp(20 * scale, 8, 28); - ExampleTranslationTextBlock.FontSize = FitFontSize( + var translationLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( ExampleTranslationTextBlock.Text, contentWidth, Math.Max(10, exampleHeightBudget * 0.44), - maxLines: 1, - minFontSize: Math.Max(7.8, translationBase * 0.62), - maxFontSize: translationBase, - weight: FontWeight.Medium, - lineHeightFactor: 1.06); - ExampleTranslationTextBlock.LineHeight = ExampleTranslationTextBlock.FontSize * 1.06; + 1, + 1, + Math.Max(7.8, Math.Clamp(20 * scale, 8, 28) * 0.62), + Math.Clamp(20 * scale, 8, 28), + [FontWeight.Medium, FontWeight.Normal], + 1.06, + MiSansFontFamily); + ExampleTranslationTextBlock.FontSize = translationLayout.FontSize; + ExampleTranslationTextBlock.FontWeight = translationLayout.Weight; + ExampleTranslationTextBlock.MaxLines = translationLayout.MaxLines; + ExampleTranslationTextBlock.TextWrapping = TextWrapping.NoWrap; + ExampleTranslationTextBlock.LineHeight = translationLayout.LineHeight; StatusTextBlock.FontSize = Math.Clamp(16 * scale, 9, 24); } @@ -587,58 +617,4 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe 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 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/DateWidget.axaml.cs b/LanMountainDesktop/Views/Components/DateWidget.axaml.cs index 1315699..e5614ec 100644 --- a/LanMountainDesktop/Views/Components/DateWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/DateWidget.axaml.cs @@ -6,6 +6,7 @@ using Avalonia.Controls; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Services; namespace LanMountainDesktop.Views.Components; @@ -253,8 +254,36 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon // 4x2 widget has less vertical space than 2x2. Compress only on 6-row months. var rowDensity = _calendarVisibleRows >= 6 ? 0.84 : 1.0; - var dayFontSize = Math.Clamp(_calendarDayFontSize * rowDensity, 8, 24); - var todayDotSize = Math.Clamp(_calendarTodayDotSize * rowDensity, 13.5, 32); + var availableCalendarHeight = Math.Max( + 1, + 220 + - RootBorder.Padding.Top + - RootBorder.Padding.Bottom + - (GregorianHeadlineTextBlock.FontSize * 1.16) + - LeftPanelGrid.RowSpacing + - (_weekdayFontSize * 1.10) + - WeekdayHeaderGrid.Margin.Top + - WeekdayHeaderGrid.Margin.Bottom + - CalendarGrid.Margin.Top + - CalendarGrid.Margin.Bottom); + var calendarCellHeight = availableCalendarHeight / Math.Max(1, _calendarVisibleRows); + var todayBadge = ComponentTypographyLayoutService.ResolveBadgeBox( + calendarCellHeight, + calendarCellHeight, + preferredSizeScale: 0.92d, + minSize: 14, + maxSize: 32, + insetScale: 0.14d); + var todayDotSize = Math.Min(todayBadge.Width, todayBadge.Height); + var todayGlyphBox = ComponentTypographyLayoutService.ResolveGlyphBox( + todayDotSize, + todayDotSize, + preferredSizeScale: 0.74d, + minSize: 8, + maxSize: 20, + insetScale: 0.12d); + var todayGlyphSize = Math.Min(todayGlyphBox.Width, todayGlyphBox.Height); + var dayFontSize = Math.Clamp(Math.Min(_calendarDayFontSize * rowDensity, calendarCellHeight * 0.46), 8, 22); for (var day = 1; day <= daysInMonth; day++) { @@ -286,6 +315,10 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon : Brushes.White; dayText.Foreground = onAccentBrush; + dayText.Width = todayGlyphBox.Width; + dayText.Height = todayGlyphBox.Height; + dayText.TextAlignment = TextAlignment.Center; + dayText.LineHeight = todayGlyphSize * 1.03; var dot = new Border { Width = todayDotSize, @@ -326,28 +359,28 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon var scale = ResolveScale(); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(28 * scale, 16, 40); - RootBorder.Padding = new Thickness(Math.Clamp(11 * scale, 7, 17)); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(13 * scale, 8 * scale, null, 0.55d); LayoutRoot.ColumnSpacing = Math.Clamp(10 * scale, 6, 16); - LeftPanelGrid.RowSpacing = Math.Clamp(5.2 * scale, 2.5, 10); + LeftPanelGrid.RowSpacing = Math.Clamp(6.2 * scale, 3, 11); WeekdayHeaderGrid.Margin = new Thickness( 0, - Math.Clamp(0.5 * scale, 0, 2), + Math.Clamp(0.8 * scale, 0, 2.5), 0, - Math.Clamp(2.4 * scale, 1, 4)); - CalendarGrid.Margin = new Thickness(0, 0, 0, Math.Clamp(0.8 * scale, 0, 2)); + Math.Clamp(3.0 * scale, 1.5, 4.5)); + CalendarGrid.Margin = new Thickness(0, 0, 0, Math.Clamp(1.2 * scale, 0.5, 2.5)); LunarCardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * scale, 14, 34); - LunarCardBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 20)); - RightPanelGrid.RowSpacing = Math.Clamp(7.5 * scale, 3.5, 11); + LunarCardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(15 * scale, 9 * scale, null, 0.55d); + RightPanelGrid.RowSpacing = Math.Clamp(8.2 * scale, 4, 12); DividerBorder.Margin = new Thickness(0, Math.Clamp(1 * scale, 0, 2), 0, Math.Clamp(1 * scale, 0, 2)); var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase); - var headerTextLength = Math.Max(1, GregorianHeadlineTextBlock.Text?.Length ?? (isZh ? 5 : 6)); + var headerTextLength = Math.Max(1, ComponentTypographyLayoutService.CountTextDisplayUnits(GregorianHeadlineTextBlock.Text)); var headerCompression = headerTextLength >= 8 ? 0.90 : headerTextLength >= 6 ? 0.95 : 1.0; var densityBoost = scale <= 0.74 ? 0.90 : scale <= 0.90 ? 0.95 : scale >= 1.45 ? 1.05 : 1.0; - GregorianHeadlineTextBlock.FontSize = Math.Clamp(29 * scale * headerCompression * densityBoost, 12.5, 42); + GregorianHeadlineTextBlock.FontSize = Math.Clamp(29 * scale * headerCompression * densityBoost, 12.5, 40); GregorianHeadlineTextBlock.FontWeight = ToVariableWeight(Lerp(560, 720, Math.Clamp((scale - 0.60) / 1.2, 0, 1))); GregorianHeadlineTextBlock.LineHeight = GregorianHeadlineTextBlock.FontSize * 1.03; @@ -360,9 +393,9 @@ public partial class DateWidget : UserControl, IDesktopComponentWidget, ITimeZon block.LineHeight = _weekdayFontSize * 1.02; } - _calendarDayFontSize = Math.Clamp(15.4 * scale * densityBoost, 8, 22); + _calendarDayFontSize = Math.Clamp(15.4 * scale * densityBoost, 8, 20); _calendarDayFontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.60) / 1.2, 0, 1))); - _calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.30, 13.5, 31); + _calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.42, 15, 30); var rightDensity = scale <= 0.72 ? 0.90 : scale <= 0.90 ? 0.95 : scale >= 1.38 ? 1.03 : 1.0; LunarDateTextBlock.FontSize = Math.Clamp(30 * scale * rightDensity, 14, 44); diff --git a/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs b/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs index b91e8a0..1ebad06 100644 --- a/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs @@ -81,7 +81,7 @@ public partial class ExchangeRateCalculatorWidget : UserControl, IDesktopCompone _currentCellSize = Math.Max(1, cellSize); var scale = ResolveScale(); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 14, 48); - RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 6, 18)); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(12 * scale, 12 * scale, null, 0.55d); } public void SetRecommendationInfoService(IRecommendationInfoService recommendationInfoService) diff --git a/LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml b/LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml index bdfd338..8b39cf0 100644 --- a/LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml +++ b/LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml @@ -13,7 +13,7 @@ ClipToBounds="True" Padding="14"> + MaxLines="2" + Margin="10,0,10,0" /> + MaxLines="2" + Margin="10,0,10,0" /> diff --git a/LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml.cs b/LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml.cs index b31e5ce..86c09a3 100644 --- a/LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml.cs @@ -6,6 +6,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Services; namespace LanMountainDesktop.Views.Components; @@ -211,46 +212,81 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge var scale = ResolveScale(width, height); var isCompact = width < 170 || height < 170; var isUltraCompact = width < 130 || height < 130; - var titleUnits = GetDisplayUnits(TitleTextBlock.Text); - var dateUnits = GetDisplayUnits(DateTextBlock.Text); + var titleUnits = ComponentTypographyLayoutService.CountTextDisplayUnits(TitleTextBlock.Text); + var dateUnits = ComponentTypographyLayoutService.CountTextDisplayUnits(DateTextBlock.Text); var titleNeedsTwoLines = isUltraCompact || titleUnits >= (isCompact ? 13 : 17); var dateNeedsTwoLines = isUltraCompact || dateUnits >= (isCompact ? 15 : 20); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(shortSide * 0.13, 10, 46); - var padding = ComponentChromeCornerRadiusHelper.SafeValue(shortSide * 0.05, 4.5, 21); - RootBorder.Padding = new Thickness(padding); - LayoutRoot.RowSpacing = Math.Clamp(shortSide * 0.028, 2.2, 12); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + shortSide * 0.055, + shortSide * 0.05, + null, + 0.55d); + LayoutRoot.RowSpacing = Math.Clamp(shortSide * 0.034, 3.2, 14); var rowWeights = ApplyAdaptiveRowHeights(isCompact, isUltraCompact, titleNeedsTwoLines, dateNeedsTwoLines); - var innerWidth = Math.Max(1, width - padding * 2); - var innerHeight = Math.Max(1, height - padding * 2); + var rootPadding = RootBorder.Padding; + var innerWidth = Math.Max(1, width - rootPadding.Left - rootPadding.Right); + var innerHeight = Math.Max(1, height - rootPadding.Top - rootPadding.Bottom); var totalWeight = Math.Max(0.001, rowWeights[0] + rowWeights[1] + rowWeights[2] + rowWeights[3] + rowWeights[4]); var row0Height = innerHeight * (rowWeights[0] / totalWeight); var row1Height = innerHeight * (rowWeights[1] / totalWeight); var row3Height = innerHeight * (rowWeights[3] / totalWeight); var row4Height = innerHeight * (rowWeights[4] / totalWeight); - var horizontalMargin = Math.Clamp(8 * scale, 4, 14); + var horizontalMargin = Math.Clamp(9 * scale, 5, 16); var titleMaxWidth = Math.Max(24, innerWidth - horizontalMargin * 2); var dateMaxWidth = titleMaxWidth; + var titleContentBox = ComponentTypographyLayoutService.ResolveGlyphBox( + titleMaxWidth, + row0Height, + preferredSizeScale: 0.84d, + minSize: 24, + maxSize: 170, + insetScale: 0.10d); + var countContentBox = ComponentTypographyLayoutService.ResolveGlyphBox( + titleMaxWidth, + row1Height, + preferredSizeScale: 0.80d, + minSize: 28, + maxSize: 170, + insetScale: 0.08d); + var unitContentBox = ComponentTypographyLayoutService.ResolveBadgeBox( + titleMaxWidth, + row3Height, + preferredSizeScale: 0.42d, + minSize: 10, + maxSize: 72, + insetScale: 0.12d); + var dateContentBox = ComponentTypographyLayoutService.ResolveGlyphBox( + dateMaxWidth, + row4Height, + preferredSizeScale: 0.78d, + minSize: 22, + maxSize: 92, + insetScale: 0.10d); - var titlePreferred = Math.Clamp(24 * scale, 8.8, 34); + var titlePreferred = Math.Clamp(24 * scale, 9.2, 34); var titleHeightCap = Math.Max(10, row0Height * 0.94); var titleLineCount = titleNeedsTwoLines ? 2 : 1; - TitleTextBlock.MaxLines = titleLineCount; - TitleTextBlock.TextWrapping = titleLineCount > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; TitleTextBlock.Margin = new Thickness(horizontalMargin, 0, horizontalMargin, 0); - TitleTextBlock.FontSize = FitTextSize( + var titleLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( TitleTextBlock.Text, - TitleTextBlock.FontWeight, - Math.Min(titlePreferred, Math.Max(8.8, row0Height * 0.62)), - 8.6, - titleMaxWidth, + Math.Max(24, titleContentBox.Width), titleHeightCap, + 1, titleLineCount, - lineHeightFactor: 1.10); - TitleTextBlock.LineHeight = TitleTextBlock.FontSize * 1.10; + 8.6, + Math.Min(titlePreferred, Math.Max(8.8, row0Height * 0.62)), + [TitleTextBlock.FontWeight], + 1.10); + TitleTextBlock.FontSize = titleLayout.FontSize; + TitleTextBlock.FontWeight = titleLayout.Weight; + TitleTextBlock.MaxLines = titleLayout.MaxLines; + TitleTextBlock.TextWrapping = titleLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; + TitleTextBlock.LineHeight = titleLayout.LineHeight; - var digitCount = Math.Max(1, CountTextBlock.Text?.Trim().Length ?? 1); + var digitCount = Math.Max(1, ComponentTypographyLayoutService.CountTextDisplayUnits(CountTextBlock.Text)); var digitCompression = digitCount switch { >= 5 => 0.68, @@ -261,39 +297,60 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge var countCompactFactor = isUltraCompact ? 0.86 : isCompact ? 0.93 : 1.0; var countPreferred = Math.Clamp(132 * scale * digitCompression * countCompactFactor, 28, 170); var countHeightCap = Math.Max(30, row1Height * 0.96); - CountTextBlock.FontSize = FitTextSize( + var countLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( CountTextBlock.Text, - CountTextBlock.FontWeight, - Math.Min(countPreferred, Math.Max(28, row1Height * 0.9)), - 24, - titleMaxWidth, + Math.Max(24, countContentBox.Width), countHeightCap, - maxLines: 1, - lineHeightFactor: 1.08); - CountTextBlock.LineHeight = CountTextBlock.FontSize * 1.08; + 1, + 1, + 24, + Math.Min(countPreferred, Math.Max(28, row1Height * 0.9)), + [CountTextBlock.FontWeight], + 1.08); + CountTextBlock.FontSize = countLayout.FontSize; + CountTextBlock.FontWeight = countLayout.Weight; + CountTextBlock.MaxLines = countLayout.MaxLines; + CountTextBlock.TextWrapping = TextWrapping.NoWrap; + CountTextBlock.LineHeight = countLayout.LineHeight; var unitCompactFactor = isUltraCompact ? 0.8 : isCompact ? 0.9 : 1.0; - DayUnitTextBlock.FontSize = Math.Clamp(52 * scale * unitCompactFactor, 10, 72); - DayUnitTextBlock.FontSize = Math.Min(DayUnitTextBlock.FontSize, Math.Max(10, row3Height * 0.64)); - DayUnitTextBlock.LineHeight = DayUnitTextBlock.FontSize * 1.02; + var unitPreferred = Math.Min(Math.Clamp(52 * scale * unitCompactFactor, 10, 72), Math.Max(10, row3Height * 0.64)); + var unitLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + DayUnitTextBlock.Text, + Math.Max(18, unitContentBox.Width), + Math.Max(10, row3Height * 0.64), + 1, + 1, + 10, + unitPreferred, + [DayUnitTextBlock.FontWeight], + 1.02); + DayUnitTextBlock.FontSize = unitLayout.FontSize; + DayUnitTextBlock.FontWeight = unitLayout.Weight; + DayUnitTextBlock.MaxLines = 1; + DayUnitTextBlock.TextWrapping = TextWrapping.NoWrap; + DayUnitTextBlock.LineHeight = unitLayout.LineHeight; var dateCompactFactor = isUltraCompact ? 0.84 : isCompact ? 0.92 : 1.0; var datePreferred = Math.Clamp(32 * scale * dateCompactFactor, 9, 46); var dateHeightCap = Math.Max(10, row4Height * 0.96); var dateLineCount = dateNeedsTwoLines ? 2 : 1; - DateTextBlock.MaxLines = dateLineCount; - DateTextBlock.TextWrapping = dateLineCount > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; DateTextBlock.Margin = new Thickness(horizontalMargin, 0, horizontalMargin, 0); - DateTextBlock.FontSize = FitTextSize( + var dateLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( DateTextBlock.Text, - DateTextBlock.FontWeight, - Math.Min(datePreferred, Math.Max(9, row4Height * 0.58)), - 8.5, - dateMaxWidth, + Math.Max(24, dateContentBox.Width), dateHeightCap, + 1, dateLineCount, - lineHeightFactor: 1.12); - DateTextBlock.LineHeight = DateTextBlock.FontSize * 1.12; + 8.5, + Math.Min(datePreferred, Math.Max(9, row4Height * 0.58)), + [DateTextBlock.FontWeight], + 1.12); + DateTextBlock.FontSize = dateLayout.FontSize; + DateTextBlock.FontWeight = dateLayout.Weight; + DateTextBlock.MaxLines = dateLayout.MaxLines; + DateTextBlock.TextWrapping = dateLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; + DateTextBlock.LineHeight = dateLayout.LineHeight; } private double[] ApplyAdaptiveRowHeights( @@ -343,66 +400,6 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge return weights; } - private static int GetDisplayUnits(string? text) - { - if (string.IsNullOrWhiteSpace(text)) - { - return 0; - } - - var units = 0; - foreach (var ch in text.Trim()) - { - if (char.IsWhiteSpace(ch)) - { - continue; - } - - units += ch > 0x7F ? 2 : 1; - } - - return units; - } - - private static double FitTextSize( - string? text, - FontWeight fontWeight, - double preferredSize, - double minSize, - double maxWidth, - double maxHeight, - int maxLines, - double lineHeightFactor) - { - var safeText = string.IsNullOrWhiteSpace(text) ? " " : text.Trim(); - var safeMaxWidth = Math.Max(1, maxWidth); - var safeMaxHeight = Math.Max(1, maxHeight); - var safeMaxLines = Math.Max(1, maxLines); - - var probe = new TextBlock - { - Text = safeText, - FontWeight = fontWeight, - MaxLines = safeMaxLines, - TextWrapping = safeMaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap - }; - - for (var size = preferredSize; size >= minSize; size -= 0.5) - { - probe.FontSize = size; - probe.LineHeight = size * lineHeightFactor; - probe.Measure(new Size(safeMaxWidth, double.PositiveInfinity)); - var desired = probe.DesiredSize; - if (desired.Width <= safeMaxWidth + 0.6 && - desired.Height <= safeMaxHeight + 0.6) - { - return size; - } - } - - return minSize; - } - private double ResolveScale(double width, double height) { var cellScale = Math.Clamp(_currentCellSize / 44d, 0.56, 2.0); @@ -410,4 +407,5 @@ public partial class HolidayCalendarWidget : UserControl, IDesktopComponentWidge var heightScale = Math.Clamp(height / 220d, 0.5, 2.0); return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale) * 1.02), 0.5, 2.0); } + } diff --git a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs index 46194a5..6836490 100644 --- a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs @@ -401,18 +401,26 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells; RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * softScale, 16, 46); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 12 * softScale, + 10 * softScale, + null, + 0.45d); CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * softScale, 16, 46); var horizontalPadding = Math.Clamp(14 * softScale, 8, 20); var verticalPadding = Math.Clamp(14 * softScale, 8, 20); - CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); + CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d); + + var rootPadding = RootBorder.Padding; + var cardPadding = CardBorder.Padding; var rowSpacing = Math.Clamp(8 * softScale, 4, 12); ContentGrid.RowSpacing = rowSpacing; HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16); - var innerWidth = Math.Max(150, totalWidth - horizontalPadding * 2d); - var innerHeight = Math.Max(160, totalHeight - verticalPadding * 2d); + var innerWidth = Math.Max(150, totalWidth - rootPadding.Left - rootPadding.Right - cardPadding.Left - cardPadding.Right); + var innerHeight = Math.Max(160, totalHeight - rootPadding.Top - rootPadding.Bottom - cardPadding.Top - cardPadding.Bottom); var availableRowsHeight = Math.Max(120, innerHeight - rowSpacing * 4d); var headerHeight = Math.Clamp(availableRowsHeight * 0.16, 24, 54); var itemHeight = Math.Max(32, (availableRowsHeight - headerHeight) / 4d); diff --git a/LanMountainDesktop/Views/Components/LunarCalendarWidget.axaml b/LanMountainDesktop/Views/Components/LunarCalendarWidget.axaml index d53e670..5d6b836 100644 --- a/LanMountainDesktop/Views/Components/LunarCalendarWidget.axaml +++ b/LanMountainDesktop/Views/Components/LunarCalendarWidget.axaml @@ -11,13 +11,13 @@ Background="#EFE6D9" CornerRadius="30" ClipToBounds="True" - Padding="16"> + Padding="18"> + RowSpacing="12"> + HorizontalAlignment="Center" + TextAlignment="Center" + TextWrapping="Wrap" + MaxLines="2" + Margin="4,0,4,0" /> + RowSpacing="14"> + ColumnSpacing="10"> + TextWrapping="Wrap" + MaxLines="2" /> + ColumnSpacing="10"> + TextWrapping="Wrap" + MaxLines="2" /> diff --git a/LanMountainDesktop/Views/Components/LunarCalendarWidget.axaml.cs b/LanMountainDesktop/Views/Components/LunarCalendarWidget.axaml.cs index b43b45e..ee7aa17 100644 --- a/LanMountainDesktop/Views/Components/LunarCalendarWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/LunarCalendarWidget.axaml.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Services; namespace LanMountainDesktop.Views.Components; @@ -181,36 +182,109 @@ public partial class LunarCalendarWidget : UserControl, IDesktopComponentWidget, private void ApplyAdaptiveTypography() { var scale = ResolveScale(); + var lunarUnits = ComponentTypographyLayoutService.CountTextDisplayUnits(LunarDateTextBlock.Text); + var itemUnits = Math.Max( + ComponentTypographyLayoutService.CountTextDisplayUnits(YiItemsTextBlock.Text), + ComponentTypographyLayoutService.CountTextDisplayUnits(JiItemsTextBlock.Text)); + var lunarNeedsTwoLines = lunarUnits >= (scale <= 0.82 ? 18 : 24); + var itemsNeedTwoLines = itemUnits >= (scale <= 0.82 ? 18 : 24); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 16, 44); - RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(16 * scale, 8, 24)); - LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 18); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(18 * scale, 18 * scale, null, 0.58d); + LayoutRoot.RowSpacing = Math.Clamp(12 * scale, 6, 20); DividerBorder.Margin = new Thickness( + Math.Clamp(10 * scale, 4, 16), Math.Clamp(8 * scale, 3, 14), - Math.Clamp(8 * scale, 3, 14), - Math.Clamp(8 * scale, 3, 14), - Math.Clamp(2 * scale, 1, 6)); - AuspiciousGrid.RowSpacing = Math.Clamp(12 * scale, 6, 20); + Math.Clamp(10 * scale, 4, 16), + Math.Clamp(3 * scale, 1, 7)); + AuspiciousGrid.RowSpacing = Math.Clamp(14 * scale, 7, 22); var densityBoost = scale <= 0.72 ? 0.90 : scale <= 0.88 ? 0.95 : scale >= 1.42 ? 1.04 : 1.0; - GregorianLineTextBlock.FontSize = Math.Clamp(24 * scale * densityBoost, 10, 38); - LunarDateTextBlock.FontSize = Math.Clamp(88 * scale * densityBoost, 28, 134); - YiLabelTextBlock.FontSize = Math.Clamp(30 * scale * densityBoost, 12, 46); - JiLabelTextBlock.FontSize = YiLabelTextBlock.FontSize; - YiItemsTextBlock.FontSize = Math.Clamp(24 * scale * densityBoost, 10, 36); - JiItemsTextBlock.FontSize = YiItemsTextBlock.FontSize; + var lunarTitleBox = ComponentTypographyLayoutService.ResolveGlyphBox( + Math.Max(1, Bounds.Width > 1 ? Bounds.Width : 300), + Math.Max(1, Bounds.Height > 1 ? Bounds.Height : 300), + preferredSizeScale: 0.50d, + minSize: 28, + maxSize: 134, + insetScale: 0.12d); + var lunarItemBox = ComponentTypographyLayoutService.ResolveBadgeBox( + Math.Max(1, Bounds.Width > 1 ? Bounds.Width : 300), + Math.Max(1, Bounds.Height > 1 ? Bounds.Height : 300), + preferredSizeScale: 0.32d, + minSize: 16, + maxSize: 84, + insetScale: 0.12d); - _gregorianLineWeight = ToVariableWeight(Lerp(500, 640, Math.Clamp((scale - 0.58) / 1.2, 0, 1))); - _lunarDateWeight = ToVariableWeight(Lerp(650, 780, Math.Clamp((scale - 0.58) / 1.2, 0, 1))); - _labelWeight = ToVariableWeight(Lerp(620, 760, Math.Clamp((scale - 0.58) / 1.2, 0, 1))); - _itemsWeight = ToVariableWeight(Lerp(520, 670, Math.Clamp((scale - 0.58) / 1.2, 0, 1))); + var gregorianLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + GregorianLineTextBlock.Text, + Math.Max(120, lunarTitleBox.Width * 0.9), + Math.Max(16, 44 * scale * densityBoost), + 1, + 1, + 10, + Math.Clamp(24 * scale * densityBoost, 10, 38), + [FontWeight.SemiBold, FontWeight.Medium], + 1.06); + GregorianLineTextBlock.MaxLines = gregorianLayout.MaxLines; + GregorianLineTextBlock.TextWrapping = TextWrapping.NoWrap; + GregorianLineTextBlock.FontSize = gregorianLayout.FontSize; + GregorianLineTextBlock.FontWeight = gregorianLayout.Weight; + GregorianLineTextBlock.Margin = new Thickness(4 * scale, 0, 4 * scale, 0); + _gregorianLineWeight = gregorianLayout.Weight; - GregorianLineTextBlock.FontWeight = _gregorianLineWeight; - LunarDateTextBlock.FontWeight = _lunarDateWeight; - YiLabelTextBlock.FontWeight = _labelWeight; - JiLabelTextBlock.FontWeight = _labelWeight; - YiItemsTextBlock.FontWeight = _itemsWeight; - JiItemsTextBlock.FontWeight = _itemsWeight; + var lunarLineCount = lunarNeedsTwoLines ? 2 : 1; + LunarDateTextBlock.Margin = new Thickness(2 * scale, 0, 2 * scale, 0); + var lunarLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + LunarDateTextBlock.Text, + Math.Max(120, lunarTitleBox.Width - (LunarDateTextBlock.Margin.Left + LunarDateTextBlock.Margin.Right)), + lunarLineCount > 1 + ? Math.Clamp(120 * scale * densityBoost, 44, 160) + : Math.Clamp(88 * scale * densityBoost, 28, 126), + 1, + lunarLineCount, + 24, + Math.Clamp(88 * scale * densityBoost, 28, 134), + [FontWeight.Bold, FontWeight.SemiBold], + 1.02); + LunarDateTextBlock.MaxLines = lunarLayout.MaxLines; + LunarDateTextBlock.TextWrapping = lunarLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; + LunarDateTextBlock.FontSize = lunarLayout.FontSize; + LunarDateTextBlock.FontWeight = lunarLayout.Weight; + + var labelSize = Math.Clamp(30 * scale * densityBoost, 12, 46); + YiLabelTextBlock.FontSize = labelSize; + JiLabelTextBlock.FontSize = labelSize; + + var itemMaxLines = itemsNeedTwoLines ? 2 : 1; + YiItemsTextBlock.Margin = new Thickness(0, 1 * scale, 0, 0); + var yiLayout = ComponentTypographyLayoutService.FitAdaptiveTextLayout( + YiItemsTextBlock.Text, + Math.Max(92, lunarItemBox.Width), + Math.Clamp(42 * scale * densityBoost, 16, 84), + 1, + itemMaxLines, + 9, + Math.Clamp(24 * scale * densityBoost, 10, 36), + [FontWeight.SemiBold, FontWeight.Medium], + 1.10); + YiItemsTextBlock.MaxLines = yiLayout.MaxLines; + YiItemsTextBlock.TextWrapping = yiLayout.MaxLines > 1 ? TextWrapping.Wrap : TextWrapping.NoWrap; + YiItemsTextBlock.FontSize = yiLayout.FontSize; + YiItemsTextBlock.FontWeight = yiLayout.Weight; + YiItemsTextBlock.LineHeight = yiLayout.LineHeight; + + JiItemsTextBlock.MaxLines = itemMaxLines; + JiItemsTextBlock.TextWrapping = YiItemsTextBlock.TextWrapping; + JiItemsTextBlock.Margin = new Thickness(0, 1 * scale, 0, 0); + JiItemsTextBlock.FontSize = yiLayout.FontSize; + JiItemsTextBlock.FontWeight = yiLayout.Weight; + JiItemsTextBlock.LineHeight = yiLayout.LineHeight; + + LunarDateTextBlock.FontWeight = lunarLayout.Weight; + YiLabelTextBlock.FontWeight = ToVariableWeight(Lerp(620, 740, Math.Clamp((scale - 0.60) / 1.2, 0, 1))); + JiLabelTextBlock.FontWeight = YiLabelTextBlock.FontWeight; + YiItemsTextBlock.FontWeight = yiLayout.Weight; + JiItemsTextBlock.FontWeight = yiLayout.Weight; _auspiciousItemCount = scale switch { diff --git a/LanMountainDesktop/Views/Components/MonthCalendarWidget.axaml.cs b/LanMountainDesktop/Views/Components/MonthCalendarWidget.axaml.cs index db854ea..792c575 100644 --- a/LanMountainDesktop/Views/Components/MonthCalendarWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/MonthCalendarWidget.axaml.cs @@ -5,6 +5,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.DesktopComponents.Runtime; using LanMountainDesktop.Services; namespace LanMountainDesktop.Views.Components; @@ -26,6 +27,7 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget, private double _calendarDayFontSize = 22; private FontWeight _calendarDayFontWeight = FontWeight.SemiBold; private double _calendarTodayDotSize = 44; + private int _calendarVisibleRows = 6; public MonthCalendarWidget() { @@ -148,6 +150,36 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget, var firstDayOfMonth = new DateTime(year, month, 1); var daysInMonth = DateTime.DaysInMonth(year, month); var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek; + var rowDensity = _calendarVisibleRows >= 6 ? 0.84 : 1.0; + var headerReserve = HeaderTextBlock.FontSize * 1.15; + var weekdayReserve = _weekdayFontSize * 1.12; + var gridReserve = LayoutRoot.RowSpacing * 2; + var availableCalendarHeight = Math.Max( + 1, + LayoutRoot.Height + - RootBorder.Padding.Top + - RootBorder.Padding.Bottom + - headerReserve + - weekdayReserve + - gridReserve); + var calendarCellHeight = availableCalendarHeight / Math.Max(1, _calendarVisibleRows); + var todayBadge = ComponentTypographyLayoutService.ResolveBadgeBox( + calendarCellHeight, + calendarCellHeight, + preferredSizeScale: 0.94d, + minSize: 15, + maxSize: 32, + insetScale: 0.14d); + var todayDotSize = Math.Min(todayBadge.Width, todayBadge.Height); + var todayGlyphBox = ComponentTypographyLayoutService.ResolveGlyphBox( + todayDotSize, + todayDotSize, + preferredSizeScale: 0.74d, + minSize: 8, + maxSize: 20, + insetScale: 0.12d); + var todayGlyphSize = Math.Min(todayGlyphBox.Width, todayGlyphBox.Height); + var dayFontSize = Math.Clamp(Math.Min(_calendarDayFontSize * rowDensity, calendarCellHeight * 0.46), 8, 24); for (var day = 1; day <= daysInMonth; day++) { @@ -163,7 +195,8 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget, Text = day.ToString(CultureInfo.CurrentCulture), HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, - FontSize = _calendarDayFontSize, + FontSize = dayFontSize, + LineHeight = dayFontSize * 1.04, FontWeight = _calendarDayFontWeight, Tag = "day" }; @@ -178,11 +211,15 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget, : Brushes.White; dayText.Foreground = onAccentBrush; + dayText.Width = todayGlyphBox.Width; + dayText.Height = todayGlyphBox.Height; + dayText.TextAlignment = TextAlignment.Center; + dayText.LineHeight = todayGlyphSize * 1.03; var dot = new Border { - Width = _calendarTodayDotSize, - Height = _calendarTodayDotSize, - CornerRadius = new CornerRadius(_calendarTodayDotSize * 0.5), + Width = todayDotSize, + Height = todayDotSize, + CornerRadius = new CornerRadius(todayDotSize * 0.5), Background = accentBrush, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, @@ -218,21 +255,21 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget, var scale = ResolveScale(); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(28 * scale, 14, 40); - RootBorder.Padding = new Thickness(ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 22)); - LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 16); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(15 * scale, 15 * scale, null, 0.55d); + LayoutRoot.RowSpacing = Math.Clamp(11 * scale, 5.5, 17); LayoutRoot.Width = Math.Clamp(280 * scale, 220, 420); LayoutRoot.Height = Math.Clamp(280 * scale, 220, 420); var isZh = CultureInfo.CurrentCulture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase); - var headerTextLength = Math.Max(1, HeaderTextBlock.Text?.Length ?? (isZh ? 5 : 6)); + var headerTextLength = Math.Max(1, ComponentTypographyLayoutService.CountTextDisplayUnits(HeaderTextBlock.Text)); var headerCompression = headerTextLength >= 8 ? 0.90 : headerTextLength >= 6 ? 0.95 : 1.0; var densityBoost = scale <= 0.74 ? 0.90 : scale <= 0.90 ? 0.95 : scale >= 1.45 ? 1.05 : 1.0; - HeaderTextBlock.FontSize = Math.Clamp(42 * scale * headerCompression * densityBoost, 13, 62); + HeaderTextBlock.FontSize = Math.Clamp(42 * scale * headerCompression * densityBoost, 13, 58); HeaderTextBlock.FontWeight = ToVariableWeight(Lerp(560, 720, Math.Clamp((scale - 0.62) / 1.2, 0, 1))); HeaderTextBlock.LineHeight = HeaderTextBlock.FontSize * 1.05; - _weekdayFontSize = Math.Clamp(20 * scale * densityBoost, 7.5, 27); + _weekdayFontSize = Math.Clamp(20 * scale * densityBoost, 7.5, 24); _weekdayFontWeight = ToVariableWeight(Lerp(500, 640, Math.Clamp((scale - 0.60) / 1.3, 0, 1))); foreach (var block in GetWeekdayHeaderBlocks()) { @@ -241,9 +278,9 @@ public partial class MonthCalendarWidget : UserControl, IDesktopComponentWidget, block.LineHeight = _weekdayFontSize * 1.06; } - _calendarDayFontSize = Math.Clamp(22 * scale * densityBoost, 8, 32); + _calendarDayFontSize = Math.Clamp(22 * scale * densityBoost, 8, 28); _calendarDayFontWeight = ToVariableWeight(Lerp(540, 680, Math.Clamp((scale - 0.60) / 1.3, 0, 1))); - _calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.95, 16, 62); + _calendarTodayDotSize = Math.Clamp(_calendarDayFontSize * 1.88, 16, 58); } private double ResolveScale() diff --git a/LanMountainDesktop/Views/Components/MusicControlWidget.axaml b/LanMountainDesktop/Views/Components/MusicControlWidget.axaml index 35cf009..e48b2a2 100644 --- a/LanMountainDesktop/Views/Components/MusicControlWidget.axaml +++ b/LanMountainDesktop/Views/Components/MusicControlWidget.axaml @@ -1,4 +1,4 @@ - + Height="31" + Click="OnQueueButtonClick"> + Height="31" + Click="OnFavoriteButtonClick"> + { + if (!_isAttached || !_isOnActivePage) + { + return; + } + + _currentState = state; + ApplyState(state); + }); + } + + private void OnQueueChanged(object? sender, MusicQueueState queue) + { + Dispatcher.UIThread.Post(() => + { + if (!_isAttached || !_isOnActivePage) + { + return; + } + + _currentQueue = queue; + UpdateQueueButtonState(); + }); + } + + private void UpdateListeningState() + { + var shouldListen = _isAttached && _isOnActivePage; + + if (shouldListen && !_isListening) + { + _musicControlService.StartListening(); + _isListening = true; + } + else if (!shouldListen && _isListening) + { + _musicControlService.StopListening(); + _isListening = false; + } + } + + private async Task RefreshStateAsync() + { + if (!_isAttached || !_isOnActivePage) + { + return; + } + + UpdateLanguageCode(); + + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var state = await _musicControlService.GetCurrentStateAsync(cts.Token); + + if (cts.IsCancellationRequested || !_isAttached) + { + return; + } + + _currentState = state; + ApplyState(state); + + // Also refresh queue + var queue = await _musicControlService.GetPlaybackQueueAsync(20, cts.Token); + _currentQueue = queue; + UpdateQueueButtonState(); + } + catch (OperationCanceledException) + { + // Ignore cancellation. + } + catch + { + var fallbackState = MusicPlaybackState.NoSession(isSupported: OperatingSystem.IsWindows()); + _currentState = fallbackState; + ApplyState(fallbackState); + } } private async void OnPlayPauseButtonClick(object? sender, RoutedEventArgs e) @@ -183,6 +258,25 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, await ExecuteCommandAsync(token => _musicControlService.SkipNextAsync(token)); } + private async void OnFavoriteButtonClick(object? sender, RoutedEventArgs e) + { + await ExecuteCommandAsync(token => _musicControlService.ToggleFavoriteAsync(token)); + } + + private async void OnQueueButtonClick(object? sender, RoutedEventArgs e) + { + // Show queue flyout or panel + // For now, just refresh the queue + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var queue = await _musicControlService.GetPlaybackQueueAsync(20, cts.Token); + _currentQueue = queue; + UpdateQueueButtonState(); + } + catch { } + } + private async void OnSourceAppButtonClick(object? sender, RoutedEventArgs e) { await ExecuteCommandAsync( @@ -208,85 +302,39 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, try { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(4)); - _ = await command(cts.Token); + CancelCommandRequest(); + _commandCts = new CancellationTokenSource(TimeSpan.FromSeconds(4)); + _ = await command(_commandCts.Token); } catch { - // Ignore command transport errors and recover on next poll. + // Ignore command transport errors and recover on next event. } finally { _isExecutingCommand = false; + CancelCommandRequest(); } if (refreshAfterCommand) { + // The event-driven system will update the UI automatically, + // but we also do a manual refresh to ensure consistency + await Task.Delay(100); await RefreshStateAsync(); } } - private async Task RefreshStateAsync() + private void CancelCommandRequest() { - if (!_isAttached || !_isOnActivePage || _isRefreshing) + var cts = Interlocked.Exchange(ref _commandCts, null); + if (cts is null) { return; } - _isRefreshing = true; - UpdateLanguageCode(); - - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - var previous = Interlocked.Exchange(ref _refreshCts, cts); - previous?.Cancel(); - previous?.Dispose(); - - try - { - var state = await _musicControlService.GetCurrentStateAsync(cts.Token); - if (cts.IsCancellationRequested || !_isAttached) - { - return; - } - - _currentState = state; - ApplyState(state); - } - catch (OperationCanceledException) - { - // Ignore cancellation. - } - catch - { - var fallbackState = MusicPlaybackState.NoSession(isSupported: OperatingSystem.IsWindows()); - _currentState = fallbackState; - ApplyState(fallbackState); - } - finally - { - if (ReferenceEquals(_refreshCts, cts)) - { - _refreshCts = null; - } - - cts.Dispose(); - _isRefreshing = false; - } - } - - private void UpdateRefreshTimerState() - { - if (_isAttached && _isOnActivePage) - { - if (!_refreshTimer.IsEnabled) - { - _refreshTimer.Start(); - } - - return; - } - - _refreshTimer.Stop(); + cts.Cancel(); + cts.Dispose(); } private void ApplyState(MusicPlaybackState state) @@ -364,6 +412,10 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, ? PauseSymbol : PlaySymbol; + // Update favorite button + FavoriteIcon.Symbol = state.IsFavorite ? HeartFilledSymbol : HeartSymbol; + FavoriteIcon.IconVariant = state.IsFavorite ? IconVariant.Filled : IconVariant.Regular; + SetCoverImage(state.ThumbnailBytes); ApplyActionButtonState(state); UpdateSourceAppButtonTooltip(); @@ -385,7 +437,16 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, : showNoSessionStyle; SourceAppButton.IsEnabled = !_isExecutingCommand && state.IsSupported; QueueButton.IsEnabled = canOperate || showNoSessionStyle; - FavoriteButton.IsEnabled = canOperate || showNoSessionStyle; + FavoriteButton.IsEnabled = canOperate + ? state.CanToggleFavorite + : showNoSessionStyle; + } + + private void UpdateQueueButtonState() + { + // Update queue button visual state based on queue availability + var hasQueue = _currentQueue.IsSupported && _currentQueue.HasMoreItems; + QueueIcon.Opacity = hasQueue ? 1.0 : 0.5; } private void ApplyNoMediaVisualTheme() @@ -441,6 +502,10 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, SourceAppGlyphBadge.BorderBrush = new SolidColorBrush(Color.Parse("#00FFFFFF")); SourceAppIcon.IconVariant = IconVariant.Filled; SourceAppIcon.Foreground = new SolidColorBrush(Color.Parse("#FBFFFFFF")); + + // Reset favorite icon + FavoriteIcon.Symbol = HeartSymbol; + FavoriteIcon.IconVariant = IconVariant.Regular; } private void ApplyActiveVisualTheme() @@ -474,18 +539,6 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, } } - private void CancelRefreshRequest() - { - var cts = Interlocked.Exchange(ref _refreshCts, null); - if (cts is null) - { - return; - } - - cts.Cancel(); - cts.Dispose(); - } - private string ResolveStatusText(MusicPlaybackStatus status) { return status switch @@ -696,4 +749,18 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, var safeIndex = Math.Clamp(index, 0, colors.Count - 1); return colors[safeIndex]; } + + public void Dispose() + { + _musicControlService.PlaybackStateChanged -= OnPlaybackStateChanged; + _musicControlService.QueueChanged -= OnQueueChanged; + _musicControlService.StopListening(); + if (_musicControlService is IDisposable disposableService) + { + disposableService.Dispose(); + } + + CancelCommandRequest(); + DisposeCoverBitmap(); + } } diff --git a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml index 59d50c7..e6632eb 100644 --- a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml +++ b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml @@ -9,37 +9,62 @@ d:DesignHeight="320" x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget"> + + 34 + 12,10,12,10 + 16,14,16,14 + 0,4,0,0 + 10 + 12 + 14 + 70 + 18 + 14 + 14 + 8 + 28 + 140 + 130 + 90 + 12 + 10 + 8 + + + Padding="{DynamicResource OfficeRecentDocumentsRootPadding}"> - + @@ -55,28 +80,29 @@ + Margin="{DynamicResource OfficeRecentDocumentsScrollMargin}"> - + @@ -100,7 +126,7 @@ IsVisible="False" Text="暂无最近文档" Foreground="#9AFFFFFF" - FontSize="14" + FontSize="{DynamicResource OfficeRecentDocumentsStatusFontSize}" HorizontalAlignment="Center" VerticalAlignment="Center" /> diff --git a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs index a1f0489..ba1782a 100644 --- a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using LanMountainDesktop.ComponentSystem; @@ -36,8 +37,54 @@ public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponen return; } - var scale = cellSize / 100.0; - RootBorder.CornerRadius = new Avalonia.CornerRadius(Math.Max(8, 34 * scale)); + var normalizedCellSize = Math.Max(1, cellSize); + var scale = Math.Clamp(normalizedCellSize / 48d, 0.72d, 1.65d); + var rootCornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 16, 52); + + RootBorder.CornerRadius = rootCornerRadius; + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 10 * scale, + 8 * scale, + null, + 0.45d); + + Resources["OfficeRecentDocumentsRootCornerRadius"] = rootCornerRadius; + Resources["OfficeRecentDocumentsRootPadding"] = RootBorder.Padding; + + var contentMargin = ComponentChromeCornerRadiusHelper.SafeThickness( + 16 * scale, + 14 * scale, + null, + 0.55d); + Resources["OfficeRecentDocumentsContentMargin"] = contentMargin; + Resources["OfficeRecentDocumentsScrollMargin"] = new Thickness( + 0, + ComponentChromeCornerRadiusHelper.SafeValue(4 * scale, 2, 8, null, 0.40d), + 0, + 0); + Resources["OfficeRecentDocumentsContentRowSpacing"] = ComponentChromeCornerRadiusHelper.SafeValue(8 * scale, 4, 12, null, 0.40d); + + var refreshButtonSize = Math.Clamp(28 * scale, 20, 40); + Resources["OfficeRecentDocumentsRefreshButtonSize"] = refreshButtonSize; + Resources["OfficeRecentDocumentsRefreshCornerRadius"] = new CornerRadius(refreshButtonSize / 2d); + Resources["OfficeRecentDocumentsRefreshIconFontSize"] = Math.Clamp(14 * scale, 10, 20); + + var accentSize = Math.Clamp(140 * scale, 88, 188); + 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); + var cardHeight = Math.Clamp(90 * scale, 68, 124); + Resources["OfficeRecentDocumentsDocumentCardWidth"] = cardWidth; + 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); } public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) diff --git a/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs b/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs index 26ebe58..879bc6c 100644 --- a/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs @@ -65,7 +65,11 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe var rootRadius = ComponentChromeCornerRadiusHelper.Scale(34 * chromeScale, 16, 56); RootBorder.CornerRadius = rootRadius; - RootBorder.Padding = new Thickness(0); + RootBorder.Padding = new Thickness( + ComponentChromeCornerRadiusHelper.SafeValue(14 * contentScale, 10, 22), + ComponentChromeCornerRadiusHelper.SafeValue(12 * contentScale, 8, 18), + ComponentChromeCornerRadiusHelper.SafeValue(14 * contentScale, 10, 22), + ComponentChromeCornerRadiusHelper.SafeValue(12 * contentScale, 8, 18)); RecorderCardBorder.CornerRadius = rootRadius; RecorderContentGrid.Margin = new Thickness( ComponentChromeCornerRadiusHelper.SafeValue(24 * contentScale, 14, 26), diff --git a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs index 2c5868b..585f3ef 100644 --- a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs @@ -603,12 +603,20 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells; RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * softScale, 14, 44); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( + 10 * softScale, + 8 * softScale, + null, + 0.45d); CardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * softScale, 14, 44); - CardBorder.Padding = new Thickness( + CardBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness( Math.Clamp(12 * softScale, 8, 18), Math.Clamp(12 * softScale, 8, 18), - Math.Clamp(12 * softScale, 8, 18), - Math.Clamp(12 * softScale, 8, 18)); + null, + 0.55d); + + var rootPadding = RootBorder.Padding; + var cardPadding = CardBorder.Padding; var rowSpacing = Math.Clamp(6 * softScale, 3, 10); ContentGrid.RowSpacing = rowSpacing; @@ -625,7 +633,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I RefreshButton.CornerRadius = new CornerRadius(refreshSize / 2d); RefreshGlyphIcon.FontSize = Math.Clamp(16 * softScale, 10, 20); - var innerWidth = Math.Max(100, totalWidth - CardBorder.Padding.Left - CardBorder.Padding.Right); + var innerWidth = Math.Max(100, totalWidth - rootPadding.Left - rootPadding.Right - cardPadding.Left - cardPadding.Right); var rowPaddingHorizontal = Math.Clamp(8 * softScale, 5, 14); var rowPaddingVertical = Math.Clamp(6 * softScale, 3, 10); var avatarSize = Math.Clamp(30 * softScale, 20, 40); @@ -640,8 +648,10 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I var availablePostsHeight = Math.Max( 0d, totalHeight - - CardBorder.Padding.Top - - CardBorder.Padding.Bottom - + rootPadding.Top - + rootPadding.Bottom - + cardPadding.Top - + cardPadding.Bottom - estimatedHeaderHeight - rowSpacing); var rowFootprint = Math.Max(1d, estimatedRowHeight + rowSpacing); diff --git a/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs index bd68a19..d034f24 100644 --- a/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs @@ -231,8 +231,8 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0; RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.46, 12, 34); RootBorder.Padding = new Thickness( - Math.Clamp(12 * scale * compactMultiplier, 6, 18), - Math.Clamp(10 * scale * compactMultiplier, 5, 16)); + ComponentChromeCornerRadiusHelper.SafeValue(12 * scale * compactMultiplier, 6, 18), + ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier, 5, 16)); ContentRootGrid.RowSpacing = _isUltraCompactMode ? Math.Clamp(4 * scale, 2, 6) @@ -271,8 +271,8 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(8 * scale, 4, 12)); var rowPadding = new Thickness( - Math.Clamp(10 * scale * compactMultiplier, 5, 14), - Math.Clamp(7 * scale * compactMultiplier, 3, 10)); + ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier, 5, 14), + ComponentChromeCornerRadiusHelper.SafeValue(7 * scale * compactMultiplier, 3, 10)); SustainedRowBorder.Padding = rowPadding; TimeRowBorder.Padding = rowPadding; SegmentRowBorder.Padding = rowPadding; diff --git a/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs index 173cb7f..26dee40 100644 --- a/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs @@ -54,8 +54,8 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 10, 28); RootBorder.Padding = new Thickness( - Math.Clamp(14 * scale, 8, 20), - Math.Clamp(10 * scale, 6, 16)); + ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 20), + ComponentChromeCornerRadiusHelper.SafeValue(10 * scale, 6, 16)); StatusTitleTextBlock.FontSize = Math.Clamp(11 * scale, 9, 18); StatusValueTextBlock.FontSize = Math.Clamp(20 * scale, 12, 34); diff --git a/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs index 568061f..d2f885c 100644 --- a/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs @@ -257,8 +257,8 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0; RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.46, 12, 34); RootBorder.Padding = new Thickness( - Math.Clamp(12 * scale * compactMultiplier, 6, 18), - Math.Clamp(9 * scale * compactMultiplier, 5, 16)); + ComponentChromeCornerRadiusHelper.SafeValue(12 * scale * compactMultiplier, 6, 18), + ComponentChromeCornerRadiusHelper.SafeValue(9 * scale * compactMultiplier, 5, 16)); ContentRootGrid.RowSpacing = _isUltraCompactMode ? Math.Clamp(3 * scale, 2, 5) @@ -297,8 +297,8 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(8 * scale, 4, 12)); var cardPadding = new Thickness( - Math.Clamp(10 * scale * compactMultiplier, 5, 14), - Math.Clamp(6 * scale * compactMultiplier, 3, 9)); + ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier, 5, 14), + ComponentChromeCornerRadiusHelper.SafeValue(6 * scale * compactMultiplier, 3, 9)); CountCardBorder.Padding = cardPadding; DurationCardBorder.Padding = cardPadding; diff --git a/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs index c85c4ef..c6cb04f 100644 --- a/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs @@ -107,8 +107,8 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 14, 42); RootBorder.Padding = new Thickness( - Math.Clamp(14 * scale, 8, 22), - Math.Clamp(10 * scale, 6, 16)); + ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 8, 22), + ComponentChromeCornerRadiusHelper.SafeValue(10 * scale, 6, 16)); StatusTextBlock.FontSize = Math.Clamp(16 * scale, 12, 30); RealtimeValueTextBlock.FontSize = Math.Clamp(18 * scale, 12, 34); diff --git a/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs index 717f0f5..fad3dd9 100644 --- a/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs @@ -325,8 +325,8 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone var compactMultiplier = _isUltraCompactMode ? 0.76 : _isCompactMode ? 0.88 : 1.0; RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 12, 34); RootBorder.Padding = new Thickness( - Math.Clamp(12 * scale * compactMultiplier, 6, 18), - Math.Clamp(9 * scale * compactMultiplier, 5, 16)); + ComponentChromeCornerRadiusHelper.SafeValue(12 * scale * compactMultiplier, 6, 18), + ComponentChromeCornerRadiusHelper.SafeValue(9 * scale * compactMultiplier, 5, 16)); ContentRootGrid.RowSpacing = _isUltraCompactMode ? Math.Clamp(4 * scale, 2, 5) diff --git a/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs index 74c3bcb..8c894ee 100644 --- a/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs @@ -260,8 +260,8 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi var expandedMultiplier = _isExpandedMode ? 1.12 : 1.0; RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.50, 14, 42); RootBorder.Padding = new Thickness( - Math.Clamp(16 * scale * compactMultiplier * expandedMultiplier, 8, 30), - Math.Clamp(14 * scale * compactMultiplier * expandedMultiplier, 6, 26)); + ComponentChromeCornerRadiusHelper.SafeValue(16 * scale * compactMultiplier * expandedMultiplier, 8, 30), + ComponentChromeCornerRadiusHelper.SafeValue(14 * scale * compactMultiplier * expandedMultiplier, 6, 26)); ContentRootGrid.RowSpacing = _isUltraCompactMode ? Math.Clamp(4 * scale, 2, 5) @@ -303,8 +303,8 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi ModeBadgeBorder.CornerRadius = new CornerRadius(Math.Clamp(8 * scale, 5, 14)); var cardPadding = new Thickness( - Math.Clamp(10 * scale * compactMultiplier * expandedMultiplier, 6, 20), - Math.Clamp(8 * scale * compactMultiplier * expandedMultiplier, 4, 16)); + ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier * expandedMultiplier, 6, 20), + ComponentChromeCornerRadiusHelper.SafeValue(8 * scale * compactMultiplier * expandedMultiplier, 4, 16)); var cardCornerRadius = ComponentChromeCornerRadiusHelper.Scale(10 * scale, 6, 18); AverageCardBorder.Padding = cardPadding; MinimumCardBorder.Padding = cardPadding; diff --git a/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs index 422af01..af204dd 100644 --- a/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs @@ -270,8 +270,8 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW var compactMultiplier = _isUltraCompactMode ? 0.78 : _isCompactMode ? 0.90 : 1.0; RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.34, 10, 28); RootBorder.Padding = new Thickness( - Math.Clamp(14 * scale * compactMultiplier, 7, 22), - Math.Clamp(10 * scale * compactMultiplier, 5, 16)); + ComponentChromeCornerRadiusHelper.SafeValue(14 * scale * compactMultiplier, 7, 22), + ComponentChromeCornerRadiusHelper.SafeValue(10 * scale * compactMultiplier, 5, 16)); LayoutGrid.ColumnSpacing = _isUltraCompactMode ? Math.Clamp(6 * scale, 3, 8) diff --git a/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs index db56fcf..b94dc72 100644 --- a/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs @@ -241,7 +241,9 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW Background = new SolidColorBrush(rowBackground), BorderBrush = new SolidColorBrush(rowBorderColor), BorderThickness = new Thickness(1), - Padding = new Thickness(Math.Clamp(8, 6, 12), Math.Clamp(6, 4, 10)) + Padding = new Thickness( + ComponentChromeCornerRadiusHelper.SafeValue(8, 6, 12), + ComponentChromeCornerRadiusHelper.SafeValue(6, 4, 10)) }; var panelComposite = ToOpaqueAgainst(panelColor, DarkSubstrate); @@ -355,7 +357,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW Width = _isUltraCompactMode ? 26 : 34, Height = Math.Clamp(26 * (_isCompactMode ? 0.90 : 1.0), 24, 30), Padding = new Thickness(0), - CornerRadius = new CornerRadius(10), + CornerRadius = ComponentChromeCornerRadiusHelper.Scale(10, 8, 12), Background = new SolidColorBrush(buttonBackground), BorderBrush = Brushes.Transparent, BorderThickness = new Thickness(0), @@ -590,8 +592,8 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(_currentCellSize * 0.44, 12, 36); RootBorder.Padding = new Thickness( - Math.Clamp(12 * scale, 7, 22), - Math.Clamp(9 * scale, 5, 16)); + ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 7, 22), + ComponentChromeCornerRadiusHelper.SafeValue(9 * scale, 5, 16)); ContentRootGrid.RowSpacing = _isUltraCompactMode ? Math.Clamp(4 * scale, 2, 6) @@ -604,12 +606,12 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW : Math.Clamp(6 * scale, 3, 8); DialogOverlayBorder.Padding = new Thickness( - Math.Clamp(12 * scale, 8, 20), - Math.Clamp(10 * scale, 8, 18)); + ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 8, 20), + ComponentChromeCornerRadiusHelper.SafeValue(10 * scale, 8, 18)); DialogCardBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(12 * scale, 10, 18); DialogCardBorder.Padding = new Thickness( - Math.Clamp(12 * scale, 9, 20), - Math.Clamp(11 * scale, 8, 18)); + 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); diff --git a/LanMountainDesktop/Views/Components/TimerWidget.axaml.cs b/LanMountainDesktop/Views/Components/TimerWidget.axaml.cs index 76b3220..73823f1 100644 --- a/LanMountainDesktop/Views/Components/TimerWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/TimerWidget.axaml.cs @@ -198,7 +198,8 @@ public partial class TimerWidget : UserControl, IDesktopComponentWidget var scale = ResolveScale(); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(34 * scale, 12, 48); - RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 7, 22)); + RootBorder.Padding = new Thickness( + ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 7, 22)); TimerPanelBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(32 * scale, 12, 42); PlayButtonBorder.Width = Math.Clamp(42 * scale, 28, 58); diff --git a/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs index 5621dac..f4d8c33 100644 --- a/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs @@ -169,7 +169,7 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT var horizontalPadding = Math.Clamp(10 * scale, 4, 26); var verticalPadding = Math.Clamp(8 * scale, 3, 22); - RootBorder.Padding = new Thickness(horizontalPadding, verticalPadding); + RootBorder.Padding = ComponentChromeCornerRadiusHelper.SafeThickness(horizontalPadding, verticalPadding, null, 0.55d); RootBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(24 * scale, 10, 46); var usableWidth = Math.Max(48, totalWidth - horizontalPadding * 2);