namespace XFESystem.Desktop; public enum DesktopPointerShape { Arrow, Text, Resize } /// /// Small tapered line cursors with native-pixel antialiasing and a translucent /// accent-colored halo. Cached masks contain coverage only, so the background /// always comes from the canvas and changing themes needs no raster rebuild. /// public static class DesktopPointerRenderer { private const double HaloRadius = 5.5; private const int HaloPeakAlpha = 38; private const int SamplesPerAxis = 4; private const int MaskCapacity = 24; private static readonly Dictionary<(DesktopPointerShape Shape, int ScalePercent), CoverageMask> Masks = new(MaskCapacity); private static readonly (DesktopPointerShape Shape, int ScalePercent)[] MaskOrder = new (DesktopPointerShape, int)[MaskCapacity]; private static int nextMaskSlot; private sealed record CoverageMask(int Width, int Height, byte[] Halo, byte[] Core); private readonly record struct Segment(double X1, double Y1, double X2, double Y2, double Radius1, double Radius2); // Coordinates are relative to the hot spot, in layout units. Extra padding // contains all halo and AA samples, including the outermost fractional edge. public static DesktopBounds GetBounds(int x, int y, DesktopPointerShape shape) { var relative = RelativeBounds(shape); return new(x + relative.X, y + relative.Y, relative.Width, relative.Height); } public static void Draw(IDesktopCanvas canvas, int x, int y, DesktopPointerShape shape, uint accent) { ArgumentNullException.ThrowIfNull(canvas); if (canvas is IDesktopCoverageCanvas coverage) { int percent = Math.Clamp(canvas.ScalePercent, 50, 400); double scale = percent / 100.0; var mask = Mask(shape, percent); var bounds = GetBounds(x, y, shape); int left = (int)Math.Floor(bounds.X * scale), top = (int)Math.Floor(bounds.Y * scale); coverage.BlendCoverage(accent & 0xFFFFFF, left, top, mask.Width, mask.Height, mask.Halo); coverage.BlendCoverage(accent & 0xFFFFFF, left, top, mask.Width, mask.Height, mask.Core); return; } // A legacy canvas cannot blend without knowing the background. Draw // only the narrow core instead of inventing an opaque glow background. var fallback = Mask(shape, 100); var logical = GetBounds(x, y, shape); for (int row = 0; row < fallback.Height; row++) for (int col = 0; col < fallback.Width;) { if (fallback.Core[row * fallback.Width + col] < 128) { col++; continue; } int start = col++; while (col < fallback.Width && fallback.Core[row * fallback.Width + col] >= 128) col++; canvas.Fill(accent, logical.X + start, logical.Y + row, col - start, 1); } } private static DesktopBounds RelativeBounds(DesktopPointerShape shape) => shape switch { DesktopPointerShape.Arrow => new(-7, -7, 23, 23), DesktopPointerShape.Text => new(-10, -13, 21, 29), DesktopPointerShape.Resize => new(-11, -11, 23, 23), _ => throw new ArgumentOutOfRangeException(nameof(shape)) }; private static CoverageMask Mask(DesktopPointerShape shape, int scalePercent) { lock (Masks) { var key = (shape, scalePercent); if (Masks.TryGetValue(key, out var cached)) return cached; var result = BuildMask(shape, scalePercent); // Always retain a new family. Evicted immutable masks remain valid // for a Draw already holding them; hits do not allocate or reorder. if (Masks.Count == MaskCapacity) Masks.Remove(MaskOrder[nextMaskSlot]); Masks.Add(key, result); MaskOrder[nextMaskSlot] = key; nextMaskSlot = (nextMaskSlot + 1) % MaskCapacity; return result; } } private static CoverageMask BuildMask(DesktopPointerShape shape, int scalePercent) { var bounds = RelativeBounds(shape); double scale = scalePercent / 100.0; int width = (int)Math.Ceiling(bounds.Width * scale), height = (int)Math.Ceiling(bounds.Height * scale); var core = new byte[width * height]; var halo = new byte[width * height]; Segment[] segments = shape switch { DesktopPointerShape.Arrow => [ new(0, 0, 8.3, 0.7, 0.95, 0.16), new(0, 0, 0.7, 8.3, 0.95, 0.16), new(0.2, 0.2, 5.8, 5.8, 0.80, 0.28) ], DesktopPointerShape.Text => [ new(0, -5.5, 0, 7.5, 0.78, 0.78), new(0, -5.5, -3.7, -5.5, 0.80, 0.16), new(0, -5.5, 3.7, -5.5, 0.80, 0.16), new(0, 7.5, -3.7, 7.5, 0.80, 0.16), new(0, 7.5, 3.7, 7.5, 0.80, 0.16) ], DesktopPointerShape.Resize => [ new(-4.6, -4.6, 4.6, 4.6, 0.64, 0.64), new(-4.6, -4.6, -4.3, 0, 0.88, 0.16), new(-4.6, -4.6, 0, -4.3, 0.88, 0.16), new(4.6, 4.6, 4.3, 0, 0.88, 0.16), new(4.6, 4.6, 0, 4.3, 0.88, 0.16) ], _ => throw new ArgumentOutOfRangeException(nameof(shape)) }; const int sampleCount = SamplesPerAxis * SamplesPerAxis; for (int row = 0; row < height; row++) for (int col = 0; col < width; col++) { int covered = 0; double glow = 0; for (int sy = 0; sy < SamplesPerAxis; sy++) for (int sx = 0; sx < SamplesPerAxis; sx++) { double x = bounds.X + (col + (sx + 0.5) / SamplesPerAxis) / scale; double y = bounds.Y + (row + (sy + 0.5) / SamplesPerAxis) / scale; double distance = double.MaxValue; foreach (var segment in segments) distance = Math.Min(distance, SignedDistance(segment, x, y)); if (distance <= 0) covered++; double strength = Math.Clamp(1 - Math.Max(0, distance) / HaloRadius, 0, 1); glow += strength * strength; } int at = row * width + col; core[at] = (byte)((covered * 255 + sampleCount / 2) / sampleCount); halo[at] = (byte)Math.Round(HaloPeakAlpha * glow / sampleCount); } return new CoverageMask(width, height, halo, core); } private static double SignedDistance(Segment segment, double x, double y) { double dx = segment.X2 - segment.X1, dy = segment.Y2 - segment.Y1; double lengthSquared = dx * dx + dy * dy; double t = lengthSquared == 0 ? 0 : Math.Clamp(((x - segment.X1) * dx + (y - segment.Y1) * dy) / lengthSquared, 0, 1); double px = x - (segment.X1 + t * dx), py = y - (segment.Y1 + t * dy); return Math.Sqrt(px * px + py * py) - (segment.Radius1 + t * (segment.Radius2 - segment.Radius1)); } }