File: src\Shared\ColorGenerator.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
 
namespace Aspire;
 
internal sealed class AccentColor
{
    internal AccentColor(string variableName)
    {
        VariableName = variableName;
        ReferencedVariableName = $"var({variableName})";
    }
 
    internal string VariableName { get; }
    internal string ReferencedVariableName { get; }
}
 
/// <summary>
/// Provides a stable color for a named element. When <see cref="GetColorVariableByKey(string)" />
/// is invoked a new color is returned if the key was not used previously. An instance of this class
/// is thread-safe and multiple threads can query colors concurrently without collisions.
/// The palette of CSS variable names is shared between the dashboard and the CLI so that a given
/// resource name always receives the same color regardless of where it is displayed.
/// </summary>
internal class ColorGenerator
{
    /// <summary>
    /// The ordered list of CSS accent variable names used as the color palette.
    /// </summary>
    internal static readonly string[] s_variableNames =
    [
        "--accent-teal",
        "--accent-marigold",
        "--accent-brass",
        "--accent-peach",
        "--accent-coral",
        "--accent-royal-blue",
        "--accent-orchid",
        "--accent-brand-blue",
        "--accent-seafoam",
        "--accent-mink",
        "--accent-cyan",
        "--accent-gold",
        "--accent-bronze",
        "--accent-orange",
        "--accent-rust",
        "--accent-navy",
        "--accent-berry",
        "--accent-ocean",
        "--accent-jade",
        "--accent-olive"
    ];
 
    public static readonly ColorGenerator Instance = new ColorGenerator();
 
    private readonly List<AccentColor> _colors;
    private readonly ConcurrentDictionary<string, Lazy<int>> _colorIndexByKey;
    private int _currentIndex;
 
    internal ColorGenerator()
    {
        _colors = new List<AccentColor>();
        _colorIndexByKey = new ConcurrentDictionary<string, Lazy<int>>(StringComparer.OrdinalIgnoreCase);
        _currentIndex = 0;
 
        foreach (var name in s_variableNames)
        {
            _colors.Add(new AccentColor(name));
        }
    }
 
    internal int GetColorIndex(string key)
    {
        return _colorIndexByKey.GetOrAdd(key, k =>
        {
            // GetOrAdd is run outside of the lock.
            // Use lazy to ensure that the index is only calculated once for an app.
            return new Lazy<int>(() =>
            {
                var i = _currentIndex;
                _currentIndex = ++_currentIndex % _colors.Count;
                return i;
            });
        }).Value;
    }
 
    public string GetColorVariableByKey(string key)
    {
        var i = GetColorIndex(key);
        return _colors[i].ReferencedVariableName;
    }
 
    /// <summary>
    /// Pre-resolves colors for all provided keys in sorted order so that
    /// color assignment is deterministic regardless of encounter order.
    /// </summary>
    public void ResolveAll(IEnumerable<string> keys)
    {
        foreach (var key in keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase))
        {
            GetColorIndex(key);
        }
    }
 
    public void Clear()
    {
        _colorIndexByKey.Clear();
        _currentIndex = 0;
    }
}