namespace XFESystem.Desktop;
/// Theme-aware small window affordances; animation time/state belongs to DesktopSession.
public static class DesktopWindowChromeRenderer
{
public const int AnimationSteps = 32;
private static readonly Dictionary CloseInkCache = [];
// Keep the animated band centered and clear of both caption buttons.
public static DesktopBounds TitleDamage(DesktopBounds bounds)
{
int width = bounds.Width * 2 / 3;
return new(bounds.X + (bounds.Width - width) / 2 - 1, bounds.Y, width + 2, 32);
}
public static DesktopBounds CaptionDamage(DesktopBounds bounds, bool close) =>
new(bounds.X + bounds.Width - (close ? 32 : 64), bounds.Y, 32, 32);
public static DesktopBounds ResizeDamage(DesktopBounds bounds) =>
new(bounds.X + bounds.Width - 20, bounds.Y + bounds.Height - 20, 20, 20);
///
/// Draw after restoring/redrawing the scene below this chrome. Progress is
/// reversible 0..1 input, quantized to a bounded family of native AA masks;
/// alpha is never composited repeatedly over the previous animation frame.
///
public static void Draw(IDesktopCanvas canvas, DesktopBounds bounds, DesktopTheme theme, bool isActive,
double minimizeProgress, double closeProgress, double titleHintProgress, double resizeProgress,
bool isMaximizedOrSnapped = false, bool minimizePressed = false, bool closePressed = false)
{
ArgumentNullException.ThrowIfNull(canvas); ArgumentNullException.ThrowIfNull(theme);
if (bounds.Width < 128 || bounds.Height < 64) return;
int minPhase = Phase(minimizeProgress), closePhase = Phase(closeProgress);
int titlePhase = Phase(titleHintProgress), resizePhase = Phase(resizeProgress);
double centerX = bounds.X + bounds.Width / 2d, centerY = bounds.Y + 16;
uint ink = isActive ? theme.AccentStroke : theme.Muted;
double titleWidth = bounds.Width * 2 / 3;
DesktopChromeRenderer.RoundedAlphaFill(canvas, 0xFFFFFF, centerX - titleWidth / 2, bounds.Y, titleWidth, 30,
0, 0, 10, 10, (byte)((titlePhase * 40 + AnimationSteps / 2) / AnimationSteps));
DesktopChromeRenderer.RoundedAlphaBorder(canvas, theme.AccentStroke, centerX - titleWidth / 2, bounds.Y, titleWidth, 30,
0, 0, 10, 10, .75, (byte)((titlePhase * 32 + AnimationSteps / 2) / AnimationSteps));
DesktopChromeRenderer.ThickLine(canvas, ink, centerX - 12, bounds.Y + 13, centerX + 12, bounds.Y + 13, 2.5);
DesktopChromeRenderer.ThickLine(canvas, ink, centerX - 12, bounds.Y + 19, centerX + 12, bounds.Y + 19, 2.5);
double minCenter = bounds.X + bounds.Width - 48;
double angle = minPhase * Math.PI / (2 * AnimationSteps);
double length = minimizePressed ? 5.0 : 5.5;
double dx = Math.Cos(angle) * length, dy = Math.Sin(angle) * length;
DesktopChromeRenderer.ThickLine(canvas, theme.AccentStroke, minCenter - dx, centerY - dy,
minCenter + dx, centerY + dy, minimizePressed ? 3.2 : 2.8);
double closeCenter = bounds.X + bounds.Width - 16, circle = closePressed ? 10.0 : 10.5;
DesktopChromeRenderer.RoundedAlphaFill(canvas, 0xFFFFFF, closeCenter - circle, centerY - circle,
circle * 2, circle * 2, circle, circle, circle, circle);
// An X repeats every quarter turn. Canonicalizing that symmetry avoids
// redundant cache entries while preserving the complete 180-degree spin.
angle = Math.PI / 4 + (closePhase % (AnimationSteps / 2)) * Math.PI / AnimationSteps;
length = closePressed ? 5.8 : 6.2;
dx = Math.Cos(angle) * length; dy = Math.Sin(angle) * length;
double thickness = closePressed ? 3.1 : 2.6;
uint closeInk = CloseInk(theme.AccentStroke);
DesktopChromeRenderer.ThickLine(canvas, closeInk, closeCenter - dx, centerY - dy,
closeCenter + dx, centerY + dy, thickness);
DesktopChromeRenderer.ThickLine(canvas, closeInk, closeCenter + dy, centerY - dx,
closeCenter - dy, centerY + dx, thickness);
if (isMaximizedOrSnapped) return;
double right = bounds.X + bounds.Width, bottom = bounds.Y + bounds.Height;
// The highlight occupies exactly the quarter-corner's footprint,
// rather than a separate square extending into the window content.
DesktopChromeRenderer.RoundedAlphaFill(canvas, 0xFFFFFF, right - 18, bottom - 18, 18, 18,
7, 0, 18, 0, (byte)((resizePhase * 44 + AnimationSteps / 2) / AnimationSteps));
DesktopChromeRenderer.RoundedAlphaBorder(canvas, theme.AccentStroke, right - 18, bottom - 18, 18, 18,
7, 0, 18, 0, .75, (byte)((resizePhase * 32 + AnimationSteps / 2) / AnimationSteps));
DesktopChromeRenderer.QuarterArc(canvas, ink, right - 18, bottom - 18, 15.5, 2.5);
}
private static int Phase(double value)
{
if (!double.IsFinite(value)) return 0;
return (int)Math.Round(Math.Clamp(value, 0, 1) * AnimationSteps);
}
private static uint CloseInk(uint accent)
{
lock (CloseInkCache)
{
if (CloseInkCache.TryGetValue(accent, out uint cached)) return cached;
uint result = accent;
for (int amount = 0; DesktopTheme.Contrast(result, 0xFFFFFF) < 3 && amount <= 100; amount += 10)
result = DesktopTheme.Mix(accent, 0, amount);
if (CloseInkCache.Count < 32) CloseInkCache.Add(accent, result);
return result;
}
}
}