Files
LanMountainDesktop/LanMountainDesktop/Services/MonetColorService.cs
lincube 9fb41378eb Migrate codebase to Avalonia 12 APIs
Apply Avalonia 12 migration changes: replace SystemDecorations with WindowDecorations and remove ExtendClientAreaChromeHints/ExtendClientAreaTitleBarHeightHint usages; update BindingPlugins removal logic (no-op); switch clipboard usage to ClipboardExtensions.SetTextAsync; update Bitmap.CopyPixels calls to the new signature. Replace TextBox.Watermark with PlaceholderText, convert NumberBox styles to FANumberBox and adjust templates, change Checked/Unchecked handlers to IsCheckedChanged, and adapt FluentIcons usages (SymbolIconSource -> FASymbol/FAFont/FluentIcon equivalents). Fix MainWindow partial classes to inherit Window and correct missing variables/fields/usings. Add migration docs/specs/tasks under .trae and include a small TestFluentIcons project for icon testing.
2026-04-29 08:50:28 +08:00

232 lines
7.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using LanMountainDesktop.Models;
using MaterialColorUtilities.Palettes;
using MaterialColorUtilities.Utils;
using Microsoft.Win32;
namespace LanMountainDesktop.Services;
public sealed class MonetColorService
{
private static readonly Color DefaultSeedColor = Color.Parse("#FF3B82F6");
public MonetPalette BuildPalette(Bitmap? wallpaper, bool nightMode, Color? preferredSeed = null)
{
var wallpaperCandidates = wallpaper is null
? []
: ExtractSeedCandidates(wallpaper);
return BuildPaletteCore(wallpaperCandidates, nightMode, preferredSeed);
}
public MonetPalette BuildPaletteFromSeedCandidates(
IReadOnlyList<Color>? seedCandidates,
bool nightMode,
Color? preferredSeed = null)
{
return BuildPaletteCore(seedCandidates ?? [], nightMode, preferredSeed);
}
public IReadOnlyList<Color> ExtractSeedCandidates(Bitmap wallpaper)
{
ArgumentNullException.ThrowIfNull(wallpaper);
return ExtractWallpaperSeedCandidates(wallpaper);
}
private static Color? ResolveSeedColor(
IReadOnlyList<Color> wallpaperCandidates,
Color? preferredSeed)
{
if (wallpaperCandidates.Count == 0)
{
return null;
}
if (preferredSeed is { } explicitSeed)
{
var exact = wallpaperCandidates.FirstOrDefault(candidate => candidate == explicitSeed);
if (exact != default)
{
return exact;
}
}
return wallpaperCandidates[0];
}
private static IReadOnlyList<Color> BuildFallbackSeedCandidates()
{
return
[
Color.Parse("#FF3B82F6"),
Color.Parse("#FF22C55E"),
Color.Parse("#FFF59E0B"),
Color.Parse("#FFF97316"),
Color.Parse("#FFA855F7")
];
}
private static IReadOnlyList<Color> ExtractWallpaperSeedCandidates(Bitmap wallpaper)
{
try
{
var width = Math.Clamp(wallpaper.PixelSize.Width, 1, 96);
var height = Math.Clamp(wallpaper.PixelSize.Height, 1, 96);
using var scaledBitmap = wallpaper.CreateScaledBitmap(
new PixelSize(width, height),
BitmapInterpolationMode.MediumQuality);
using var writeable = new WriteableBitmap(
scaledBitmap.PixelSize,
new Vector(96, 96),
PixelFormat.Bgra8888,
AlphaFormat.Premul);
using var framebuffer = writeable.Lock();
var byteCount = framebuffer.RowBytes * framebuffer.Size.Height;
if (byteCount <= 0 || framebuffer.Address == IntPtr.Zero)
{
return [];
}
var pixelBuffer = new byte[byteCount];
scaledBitmap.CopyPixels(
new PixelRect(scaledBitmap.PixelSize),
framebuffer.Address,
byteCount,
framebuffer.RowBytes);
Marshal.Copy(framebuffer.Address, pixelBuffer, 0, byteCount);
var argbPixels = new List<uint>(framebuffer.Size.Width * framebuffer.Size.Height);
for (var y = 0; y < framebuffer.Size.Height; y++)
{
var rowOffset = y * framebuffer.RowBytes;
for (var x = 0; x < framebuffer.Size.Width; x++)
{
var index = rowOffset + (x * 4);
var alpha = pixelBuffer[index + 3];
if (alpha <= 32)
{
continue;
}
var blue = pixelBuffer[index];
var green = pixelBuffer[index + 1];
var red = pixelBuffer[index + 2];
argbPixels.Add(
((uint)alpha << 24) |
((uint)red << 16) |
((uint)green << 8) |
blue);
}
}
if (argbPixels.Count == 0)
{
return [];
}
var extracted = ImageUtils.ColorsFromImage(argbPixels.ToArray());
return extracted
.Select(FromArgb)
.Distinct()
.Take(6)
.ToArray();
}
catch (Exception ex)
{
AppLogger.Warn("Appearance.WallpaperPalette", "Failed to extract wallpaper seed candidates.", ex);
return [];
}
}
private static Color? TryGetSystemMonetSeedColor()
{
if (!OperatingSystem.IsWindows())
{
return null;
}
try
{
var value = Registry.GetValue(
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM",
"AccentColor",
null);
if (value is not int accentDword)
{
return null;
}
var accentColor = unchecked((uint)accentDword);
var a = (byte)((accentColor >> 24) & 0xFF);
var b = (byte)((accentColor >> 16) & 0xFF);
var g = (byte)((accentColor >> 8) & 0xFF);
var r = (byte)(accentColor & 0xFF);
if (a == 0)
{
a = 0xFF;
}
return Color.FromArgb(a, r, g, b);
}
catch
{
return null;
}
}
private static uint ToArgb(Color color)
{
return
((uint)color.A << 24) |
((uint)color.R << 16) |
((uint)color.G << 8) |
color.B;
}
private static Color FromArgb(uint argb)
{
var a = (byte)((argb >> 24) & 0xFF);
var r = (byte)((argb >> 16) & 0xFF);
var g = (byte)((argb >> 8) & 0xFF);
var b = (byte)(argb & 0xFF);
return Color.FromArgb(a, r, g, b);
}
private static MonetPalette BuildPaletteCore(
IReadOnlyList<Color> wallpaperCandidates,
bool nightMode,
Color? preferredSeed)
{
var recommendedColors = wallpaperCandidates.Count > 0
? wallpaperCandidates
: BuildFallbackSeedCandidates();
var seed = ResolveSeedColor(wallpaperCandidates, preferredSeed)
?? preferredSeed
?? TryGetSystemMonetSeedColor()
?? DefaultSeedColor;
var corePalette = CorePalette.Of(ToArgb(seed), Style.TonalSpot);
var primary = FromArgb(corePalette.Primary.Tone(nightMode ? 80u : 40u));
var secondary = FromArgb(corePalette.Secondary.Tone(nightMode ? 80u : 40u));
var tertiary = FromArgb(corePalette.Tertiary.Tone(nightMode ? 80u : 40u));
var neutral = FromArgb(corePalette.Neutral.Tone(nightMode ? 20u : 94u));
var neutralVariant = FromArgb(corePalette.NeutralVariant.Tone(nightMode ? 30u : 90u));
return new MonetPalette(
recommendedColors,
seed,
primary,
secondary,
tertiary,
neutral,
neutralVariant);
}
}