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