This commit is contained in:
lincube
2026-03-20 10:22:40 +08:00
parent 915739ff7b
commit aeae4be060
40 changed files with 1666 additions and 571 deletions

View File

@@ -4,5 +4,6 @@
<TargetFramework Condition="'$(TargetFramework)' == ''">net10.0</TargetFramework>
<Nullable Condition="'$(Nullable)' == ''">enable</Nullable>
<ImplicitUsings Condition="'$(ImplicitUsings)' == ''">enable</ImplicitUsings>
<DefaultItemExcludes>$(DefaultItemExcludes);**\obj_audit\**</DefaultItemExcludes>
</PropertyGroup>
</Project>

View File

@@ -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;
}

View File

@@ -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<FontWeight>? 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;
}
}

View File

@@ -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<MusicQueueItem> Items,
int CurrentIndex,
bool HasMoreItems)
{
public static MusicQueueState Unsupported()
{
return new MusicQueueState(false, Array.Empty<MusicQueueItem>(), -1, false);
}
public static MusicQueueState Empty()
{
return new MusicQueueState(true, Array.Empty<MusicQueueItem>(), -1, false);
}
}
@@ -80,6 +113,18 @@ public interface IMusicControlService
Task<bool> SkipPreviousAsync(CancellationToken cancellationToken = default);
Task<bool> LaunchSourceAppAsync(CancellationToken cancellationToken = default);
Task<bool> ToggleFavoriteAsync(CancellationToken cancellationToken = default);
Task<MusicQueueState> GetPlaybackQueueAsync(int maxItems = 20, CancellationToken cancellationToken = default);
event EventHandler<MusicPlaybackState>? PlaybackStateChanged;
event EventHandler<MusicQueueState>? QueueChanged;
void StartListening();
void StopListening();
}
public static class MusicControlServiceFactory
@@ -118,4 +163,25 @@ internal sealed class NoOpMusicControlService : IMusicControlService
{
return Task.FromResult(false);
}
public Task<bool> ToggleFavoriteAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult(false);
}
public Task<MusicQueueState> GetPlaybackQueueAsync(int maxItems = 20, CancellationToken cancellationToken = default)
{
return Task.FromResult(MusicQueueState.Unsupported());
}
public event EventHandler<MusicPlaybackState>? PlaybackStateChanged;
public event EventHandler<MusicQueueState>? QueueChanged;
public void StartListening()
{
}
public void StopListening()
{
}
}

View File

@@ -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<string, string> _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<Delegate> _eventHandlers = new();
// Thumbnail Cache
private string _thumbnailKey = string.Empty;
private byte[]? _thumbnailBytesCache;
// Events
public event EventHandler<MusicPlaybackState>? PlaybackStateChanged;
public event EventHandler<MusicQueueState>? 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<object?, object?, Task> asyncAction)
{
// Create a delegate that wraps the async action
var handler = new EventHandler<object>((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<MusicPlaybackState> 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<bool> 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<MusicQueueState> 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<MusicQueueItem>();
// 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<bool> LaunchSourceAppAsync(CancellationToken cancellationToken = default)
{
if (!IsRuntimeSupported())
@@ -259,6 +612,20 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService
return await AwaitWinRtOperationAsync(operation, cancellationToken);
}
private async Task<object?> 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<byte[]?> ResolveThumbnailBytesAsync(
object? mediaProperties,
string sourceAppId,
@@ -576,4 +943,11 @@ public sealed class WindowsSmtcMusicControlService : IMusicControlService
_ => MusicPlaybackStatus.Unknown
};
}
public void Dispose()
{
StopListening();
_stateGate.Dispose();
ManagerLock.Dispose();
}
}

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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)
{

View File

@@ -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");

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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)

View File

@@ -13,7 +13,7 @@
ClipToBounds="True"
Padding="14">
<Grid x:Name="LayoutRoot"
RowDefinitions="1.1*,2.3*,0.62*,0.78*,0.95*"
RowDefinitions="1.15*,2.45*,0.65*,0.82*,1.0*"
RowSpacing="8">
<TextBlock x:Name="TitleTextBlock"
Grid.Row="0"
@@ -22,11 +22,12 @@
FontWeight="SemiBold"
Foreground="#61697C"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
VerticalAlignment="Center"
TextAlignment="Center"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="1"
Margin="8,0,8,0" />
MaxLines="2"
Margin="10,0,10,0" />
<Viewbox Grid.Row="1"
Stretch="Uniform"
@@ -108,10 +109,10 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
TextAlignment="Center"
TextWrapping="NoWrap"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="1"
Margin="8,0,8,0" />
MaxLines="2"
Margin="10,0,10,0" />
</Grid>
</Border>
</UserControl>

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -11,13 +11,13 @@
Background="#EFE6D9"
CornerRadius="30"
ClipToBounds="True"
Padding="16">
Padding="18">
<Viewbox Stretch="Uniform">
<Grid x:Name="LayoutRoot"
Width="300"
Height="300"
RowDefinitions="Auto,Auto,Auto,*"
RowSpacing="10">
RowSpacing="12">
<TextBlock x:Name="GregorianLineTextBlock"
Grid.Row="0"
Text="10/9 Thu"
@@ -32,21 +32,25 @@
FontSize="88"
FontWeight="Bold"
Foreground="#6B4936"
HorizontalAlignment="Center" />
HorizontalAlignment="Center"
TextAlignment="Center"
TextWrapping="Wrap"
MaxLines="2"
Margin="4,0,4,0" />
<Border x:Name="DividerBorder"
Grid.Row="2"
Height="1"
Margin="8,8,8,2"
Margin="10,8,10,3"
Background="#D2C6B7" />
<Grid x:Name="AuspiciousGrid"
Grid.Row="3"
RowDefinitions="Auto,Auto"
RowSpacing="12">
RowSpacing="14">
<Grid Grid.Row="0"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
ColumnSpacing="10">
<TextBlock x:Name="YiLabelTextBlock"
Grid.Column="0"
Text="Yi"
@@ -59,12 +63,13 @@
FontWeight="SemiBold"
Foreground="#6B4936"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
TextWrapping="Wrap"
MaxLines="2" />
</Grid>
<Grid Grid.Row="1"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
ColumnSpacing="10">
<TextBlock x:Name="JiLabelTextBlock"
Grid.Column="0"
Text="Ji"
@@ -77,7 +82,8 @@
FontWeight="SemiBold"
Foreground="#6B4936"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
TextWrapping="Wrap"
MaxLines="2" />
</Grid>
</Grid>
</Grid>

View File

@@ -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
{

View File

@@ -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()

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -240,7 +240,8 @@
Grid.Column="0"
Classes="music-action"
Width="31"
Height="31">
Height="31"
Click="OnQueueButtonClick">
<fi:SymbolIcon x:Name="QueueIcon"
Symbol="List"
IconVariant="Regular"
@@ -291,7 +292,8 @@
Grid.Column="4"
Classes="music-action"
Width="31"
Height="31">
Height="31"
Click="OnFavoriteButtonClick">
<fi:SymbolIcon x:Name="FavoriteIcon"
Symbol="Heart"
IconVariant="Regular"

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -17,42 +17,43 @@ using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views.Components;
public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
public partial class MusicControlWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, IDisposable
{
private const Symbol PlaySymbol = Symbol.Play;
private const Symbol PauseSymbol = Symbol.Pause;
private readonly DispatcherTimer _refreshTimer = new()
{
Interval = TimeSpan.FromSeconds(2.4)
};
private const Symbol HeartSymbol = Symbol.Heart;
private const Symbol HeartFilledSymbol = Symbol.Heart;
private readonly IMusicControlService _musicControlService = MusicControlServiceFactory.CreateDefault();
private readonly MonetColorService _monetColorService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private CancellationTokenSource? _refreshCts;
private CancellationTokenSource? _commandCts;
private Bitmap? _coverBitmap;
private MusicPlaybackState _currentState = MusicPlaybackState.NoSession(isSupported: true);
private MusicQueueState _currentQueue = MusicQueueState.Empty();
private string _languageCode = "zh-CN";
private double _currentCellSize = 48;
private bool _isAttached;
private bool _isOnActivePage = true;
private bool _isRefreshing;
private bool _isExecutingCommand;
private double _progressRatio;
private bool _isProgressIndeterminate;
private bool _isListening;
public MusicControlWidget()
{
InitializeComponent();
_refreshTimer.Tick += OnRefreshTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
// Subscribe to service events
_musicControlService.PlaybackStateChanged += OnPlaybackStateChanged;
_musicControlService.QueueChanged += OnQueueChanged;
ApplyCellSize(_currentCellSize);
ApplyDynamicBackground(null);
ApplyState(MusicPlaybackState.NoSession(isSupported: OperatingSystem.IsWindows()));
@@ -63,21 +64,19 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
_currentCellSize = Math.Max(1, cellSize);
var scale = ResolveScale();
var rootRadius = Math.Clamp(30 * scale, 16, 44);
var rootCornerRadius = new CornerRadius(rootRadius);
var rootCornerRadius = ComponentChromeCornerRadiusHelper.Scale(30 * scale, 16, 44);
RootBorder.CornerRadius = rootCornerRadius;
ContentPaddingBorder.Padding = new Thickness(
Math.Clamp(14 * scale, 9, 22),
Math.Clamp(11 * scale, 7, 18),
Math.Clamp(14 * scale, 9, 22),
Math.Clamp(11 * scale, 7, 18));
LayoutGrid.RowSpacing = Math.Clamp(9 * scale, 6, 14);
HeaderRowGrid.ColumnSpacing = Math.Clamp(11 * scale, 8, 18);
MetaStackPanel.Spacing = Math.Clamp(3 * scale, 1, 6);
TimelineRowGrid.ColumnSpacing = Math.Clamp(9 * scale, 6, 14);
ActionRowGrid.ColumnSpacing = Math.Clamp(12 * scale, 8, 20);
ActionRowGrid.Margin = new Thickness(0, Math.Clamp(1 * scale, 0, 4), 0, 0);
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 9, 22),
ComponentChromeCornerRadiusHelper.SafeValue(11 * scale, 7, 18),
ComponentChromeCornerRadiusHelper.SafeValue(14 * scale, 9, 22),
ComponentChromeCornerRadiusHelper.SafeValue(11 * scale, 7, 18));
LayoutGrid.RowSpacing = ComponentChromeCornerRadiusHelper.SafeValue(9 * scale, 6, 14);
HeaderRowGrid.ColumnSpacing = ComponentChromeCornerRadiusHelper.SafeValue(11 * scale, 8, 18);
MetaStackPanel.Spacing = ComponentChromeCornerRadiusHelper.SafeValue(3 * scale, 1, 6);
TimelineRowGrid.ColumnSpacing = ComponentChromeCornerRadiusHelper.SafeValue(9 * scale, 6, 14);
ActionRowGrid.ColumnSpacing = ComponentChromeCornerRadiusHelper.SafeValue(12 * scale, 8, 20);
ActionRowGrid.Margin = new Thickness(0, ComponentChromeCornerRadiusHelper.SafeValue(1 * scale, 0, 4), 0, 0);
DynamicBackgroundBase.CornerRadius = rootCornerRadius;
BackdropCoverHost.CornerRadius = rootCornerRadius;
DynamicGradientOverlay.CornerRadius = rootCornerRadius;
@@ -85,7 +84,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
CoverBorder.Width = Math.Clamp(56 * scale, 38, 86);
CoverBorder.Height = Math.Clamp(56 * scale, 38, 86);
CoverBorder.CornerRadius = new CornerRadius(Math.Clamp(12 * scale, 8, 16));
CoverBorder.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(12 * scale, 8, 16);
TitleTextBlock.FontSize = Math.Clamp(20 * scale, 12, 28);
ArtistTextBlock.FontSize = Math.Clamp(14 * scale, 9, 18);
@@ -132,10 +131,11 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
_ = isEditMode;
var wasOnActivePage = _isOnActivePage;
_isOnActivePage = isOnActivePage;
UpdateRefreshTimerState();
UpdateListeningState();
if (!wasOnActivePage && _isOnActivePage && _isAttached)
{
// Refresh state when becoming visible again
_ = RefreshStateAsync();
}
}
@@ -143,18 +143,15 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = true;
UpdateRefreshTimerState();
if (_isOnActivePage)
{
_ = RefreshStateAsync();
}
UpdateListeningState();
_ = RefreshStateAsync();
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = false;
UpdateRefreshTimerState();
CancelRefreshRequest();
UpdateListeningState();
CancelCommandRequest();
DisposeCoverBitmap();
}
@@ -163,9 +160,87 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
ApplyCellSize(_currentCellSize);
}
private async void OnRefreshTimerTick(object? sender, EventArgs e)
private void OnPlaybackStateChanged(object? sender, MusicPlaybackState state)
{
await RefreshStateAsync();
Dispatcher.UIThread.Post(() =>
{
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();
}
}

View File

@@ -9,37 +9,62 @@
d:DesignHeight="320"
x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget">
<UserControl.Resources>
<CornerRadius x:Key="OfficeRecentDocumentsRootCornerRadius">34</CornerRadius>
<Thickness x:Key="OfficeRecentDocumentsRootPadding">12,10,12,10</Thickness>
<Thickness x:Key="OfficeRecentDocumentsContentMargin">16,14,16,14</Thickness>
<Thickness x:Key="OfficeRecentDocumentsScrollMargin">0,4,0,0</Thickness>
<Thickness x:Key="OfficeRecentDocumentsCardPadding">10</Thickness>
<CornerRadius x:Key="OfficeRecentDocumentsCardCornerRadius">12</CornerRadius>
<CornerRadius x:Key="OfficeRecentDocumentsRefreshCornerRadius">14</CornerRadius>
<CornerRadius x:Key="OfficeRecentDocumentsAccentCornerRadius">70</CornerRadius>
<x:Double x:Key="OfficeRecentDocumentsHeaderFontSize">18</x:Double>
<x:Double x:Key="OfficeRecentDocumentsStatusFontSize">14</x:Double>
<x:Double x:Key="OfficeRecentDocumentsRefreshIconFontSize">14</x:Double>
<x:Double x:Key="OfficeRecentDocumentsContentRowSpacing">8</x:Double>
<x:Double x:Key="OfficeRecentDocumentsRefreshButtonSize">28</x:Double>
<x:Double x:Key="OfficeRecentDocumentsAccentSize">140</x:Double>
<x:Double x:Key="OfficeRecentDocumentsDocumentCardWidth">130</x:Double>
<x:Double x:Key="OfficeRecentDocumentsDocumentCardHeight">90</x:Double>
<x:Double x:Key="OfficeRecentDocumentsDocumentTitleFontSize">12</x:Double>
<x:Double x:Key="OfficeRecentDocumentsDocumentTimeFontSize">10</x:Double>
<x:Double x:Key="OfficeRecentDocumentsDocumentSpacing">8</x:Double>
</UserControl.Resources>
<Border x:Name="RootBorder"
CornerRadius="34"
CornerRadius="{DynamicResource OfficeRecentDocumentsRootCornerRadius}"
Background="#2D5A8E"
ClipToBounds="True"
BorderThickness="0"
Padding="0">
Padding="{DynamicResource OfficeRecentDocumentsRootPadding}">
<Grid>
<Border x:Name="AccentCorner"
Width="140"
Height="140"
Width="{DynamicResource OfficeRecentDocumentsAccentSize}"
Height="{DynamicResource OfficeRecentDocumentsAccentSize}"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0,-40,-40,0"
CornerRadius="70"
CornerRadius="{DynamicResource OfficeRecentDocumentsAccentCornerRadius}"
Background="#4A90D9"
Opacity="0.3"
IsHitTestVisible="False" />
<Grid RowDefinitions="Auto,*" RowSpacing="8" Margin="16,14,16,14">
<Grid x:Name="ContentGrid"
RowDefinitions="Auto,*"
RowSpacing="{DynamicResource OfficeRecentDocumentsContentRowSpacing}"
Margin="{DynamicResource OfficeRecentDocumentsContentMargin}">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
<TextBlock x:Name="HeaderTextBlock"
Text="最近文档"
Foreground="#D8FFFFFF"
FontSize="18"
FontSize="{DynamicResource OfficeRecentDocumentsHeaderFontSize}"
FontWeight="SemiBold"
VerticalAlignment="Center" />
<Button x:Name="RefreshButton"
Grid.Column="1"
Width="28"
Height="28"
CornerRadius="14"
Width="{DynamicResource OfficeRecentDocumentsRefreshButtonSize}"
Height="{DynamicResource OfficeRecentDocumentsRefreshButtonSize}"
CornerRadius="{DynamicResource OfficeRecentDocumentsRefreshCornerRadius}"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0"
@@ -47,7 +72,7 @@
Focusable="False"
PointerPressed="OnRefreshPointerPressed">
<fi:SymbolIcon Symbol="ArrowSync"
FontSize="14"
FontSize="{DynamicResource OfficeRecentDocumentsRefreshIconFontSize}"
Foreground="#B8FFFFFF" />
</Button>
</Grid>
@@ -55,28 +80,29 @@
<ScrollViewer Grid.Row="1"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled"
Margin="0,4,0,0">
Margin="{DynamicResource OfficeRecentDocumentsScrollMargin}">
<ItemsControl x:Name="DocumentsItemsControl">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="8" />
<StackPanel Orientation="Horizontal"
Spacing="{DynamicResource OfficeRecentDocumentsDocumentSpacing}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:OfficeRecentDocumentViewModel">
<Border x:Name="DocumentCard"
Width="130"
Height="90"
CornerRadius="10"
Width="{DynamicResource OfficeRecentDocumentsDocumentCardWidth}"
Height="{DynamicResource OfficeRecentDocumentsDocumentCardHeight}"
CornerRadius="{DynamicResource OfficeRecentDocumentsCardCornerRadius}"
Background="#3AFFFFFF"
Padding="10"
Padding="{DynamicResource OfficeRecentDocumentsCardPadding}"
Cursor="Hand"
PointerPressed="OnDocumentCardPointerPressed">
<Grid RowDefinitions="Auto,*,Auto">
<TextBlock Grid.Row="0"
Text="{Binding FileName}"
Foreground="#D8FFFFFF"
FontSize="12"
FontSize="{DynamicResource OfficeRecentDocumentsDocumentTitleFontSize}"
FontWeight="Medium"
TextTrimming="CharacterEllipsis"
MaxLines="2"
@@ -85,7 +111,7 @@
<TextBlock Grid.Row="2"
Text="{Binding TimeAgo}"
Foreground="#9AFFFFFF"
FontSize="10"
FontSize="{DynamicResource OfficeRecentDocumentsDocumentTimeFontSize}"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</Grid>
@@ -100,7 +126,7 @@
IsVisible="False"
Text="暂无最近文档"
Foreground="#9AFFFFFF"
FontSize="14"
FontSize="{DynamicResource OfficeRecentDocumentsStatusFontSize}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>

View File

@@ -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)

View File

@@ -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),

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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)

View File

@@ -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;

View File

@@ -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)

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);