namespace XFESystem.Desktop;
public enum DesktopPointerShape { Arrow, Text, Resize }
/// <summary>
/// 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.
/// </summary>
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));
}
}
namespace XFESystem.Desktop;
public enum DesktopPointerShape { Arrow, Text, Resize }
/// <summary>
/// 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.
/// </summary>
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));
}
}