using System.Collections.Immutable;
using System.Numerics;
using System.Text.Json.Serialization;
namespace Terminal.Gui.Drawing;
///
/// Represents a theme definition that maps each (such as ,
/// , etc.)
/// to an describing its foreground color, background color, and text style.
///
/// A enables consistent, semantic theming of UI elements by associating each visual state
/// with a specific style.
/// Each property (e.g., , , ) is an
/// .
/// If a property is not explicitly set, its value is derived from other roles (typically )
/// using well-defined inheritance rules.
///
///
/// objects are immutable. To update a scheme, create a new instance with the desired values.
/// Use to manage available schemes and apply them to views.
///
///
/// See for more information.
///
///
///
///
/// Immutability: Scheme objects are immutable. Once constructed, their properties cannot be changed. To
/// modify a Scheme,
/// create a new instance with the desired values (e.g., using the constructor).
///
///
/// Attribute Resolution Algorithm:
///
/// Each property corresponds to a and is an .
/// The attribute must always be set.
/// All other attributes are optional. If an attribute for a given is not explicitly set,
/// its value is derived using the following rules:
///
/// -
/// Normal: Must always be explicitly set.
///
/// -
///
/// Focus: If not set, derived from by swapping foreground and background
/// colors.
///
///
/// -
///
/// Active: If not set, derived from by:
///
/// -
///
/// Setting Foreground to 's foreground with
/// GetHighlightColor().
///
///
/// -
///
/// Setting Background to 's background with
/// GetDimColor().
///
///
/// -
/// Adding to the style.
///
///
///
///
/// -
///
/// Highlight: If not set, derived from by:
///
/// -
///
/// Setting Foreground to 's background with
/// GetHighlightColor().
///
///
/// -
/// Setting Background to 's background.
///
/// -
///
/// Setting Style to 's style with
/// added.
///
///
///
///
///
/// -
///
/// Editable: If not set, derived from by:
///
/// -
///
/// Setting Foreground to 's background with
/// GetHighlightColor().
///
///
/// -
///
/// Setting Background to 's background with
/// GetDimColor().
///
///
///
///
///
/// -
///
/// ReadOnly: If not set, derived from by adding
/// to the style.
///
///
/// -
///
/// Disabled: If not set, derived from by adding
/// to the style.
///
///
/// -
///
/// HotNormal: If not set, derived from by adding
/// to the style.
///
///
/// -
///
/// HotFocus: If not set, derived from by adding
/// to the style.
///
///
/// -
///
/// HotActive: If not set, derived from by adding
/// to the style.
///
///
///
/// This algorithm ensures that every always resolves to a valid ,
/// either explicitly set or derived.
///
///
[JsonConverter (typeof (SchemeJsonConverter))]
public record Scheme : IEqualityOperators
{
///
/// INTERNAL: Gets the hard-coded set of s. Used for generating the built-in config.json and for
/// unit tests that don't depend on ConfigurationManager.
///
///
internal static ImmutableSortedDictionary GetHardCodedSchemes ()
{
return ImmutableSortedDictionary.CreateRange (
StringComparer.InvariantCultureIgnoreCase,
[
new KeyValuePair (SchemeManager.SchemesToSchemeName (Schemes.Base)!, CreateBase ()),
new (SchemeManager.SchemesToSchemeName (Schemes.Dialog)!, CreateDialog ()),
new (SchemeManager.SchemesToSchemeName (Schemes.Error)!, CreateError ()),
new (SchemeManager.SchemesToSchemeName (Schemes.Menu)!, CreateMenu ()),
new (SchemeManager.SchemesToSchemeName (Schemes.Runnable)!, CreateRunnable ()),
]
);
Scheme CreateBase ()
{
return new ()
{
Normal = new (StandardColor.LightBlue, StandardColor.RaisinBlack)
};
}
Scheme CreateError ()
{
return new ()
{
Normal = new (StandardColor.IndianRed, StandardColor.RaisinBlack)
};
}
Scheme CreateDialog ()
{
return new ()
{
Normal = new (StandardColor.LightSkyBlue, StandardColor.OuterSpace)
};
}
Scheme CreateMenu ()
{
return new ()
{
Normal = new (StandardColor.Charcoal, StandardColor.LightBlue, TextStyle.Bold)
};
}
Scheme CreateRunnable ()
{
return new ()
{
Normal = new (StandardColor.CadetBlue, StandardColor.Charcoal)
};
}
}
/// Creates a new instance set to the default attributes (see ).
public Scheme () : this (Attribute.Default) { }
/// Creates a new instance, initialized with the values from .
/// The scheme to initialize the new instance with.
public Scheme (Scheme? scheme)
{
ArgumentNullException.ThrowIfNull (scheme);
Normal = scheme.Normal;
_hotNormal = scheme.TryGetExplicitlySetAttributeForRole (VisualRole.HotNormal, out Attribute? hotNormal) ? hotNormal : null;
_focus = scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Focus, out Attribute? focus) ? focus : null;
_hotFocus = scheme.TryGetExplicitlySetAttributeForRole (VisualRole.HotFocus, out Attribute? hotFocus) ? hotFocus : null;
_active = scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Active, out Attribute? active) ? active : null;
_hotActive = scheme.TryGetExplicitlySetAttributeForRole (VisualRole.HotActive, out Attribute? hotActive) ? hotActive : null;
_highlight = scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Highlight, out Attribute? highlight) ? highlight : null;
_editable = scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Editable, out Attribute? editable) ? editable : null;
_readOnly = scheme.TryGetExplicitlySetAttributeForRole (VisualRole.ReadOnly, out Attribute? readOnly) ? readOnly : null;
_disabled = scheme.TryGetExplicitlySetAttributeForRole (VisualRole.Disabled, out Attribute? disabled) ? disabled : null;
}
/// Creates a new instance, initialized with the values from .
/// The attribute to initialize the new instance with.
public Scheme (Attribute attribute)
{
// Only set Normal as explicitly set
Normal = attribute;
}
///
/// Gets the associated with a specified ,
/// applying inheritance rules for attributes not explicitly set.
///
/// The semantic describing the element being rendered.
///
/// The corresponding from the , possibly derived if not explicitly
/// set.
///
public Attribute GetAttributeForRole (VisualRole role)
{
// Use a HashSet to guard against recursion cycles
return GetAttributeForRoleCore (role, []);
}
///
/// Attempts to get the associated with a specified . If the
/// role is not explicitly set, it will return false and the out parameter will be null.
///
///
///
///
public bool TryGetExplicitlySetAttributeForRole (VisualRole role, out Attribute? attribute)
{
// Use a HashSet to guard against recursion cycles
attribute = role switch
{
VisualRole.Normal => _normal,
VisualRole.HotNormal => _hotNormal,
VisualRole.Focus => _focus,
VisualRole.HotFocus => _hotFocus,
VisualRole.Active => _active,
VisualRole.HotActive => _hotActive,
VisualRole.Highlight => _highlight,
VisualRole.Editable => _editable,
VisualRole.ReadOnly => _readOnly,
VisualRole.Disabled => _disabled,
_ => null
};
return attribute is { };
}
// TODO: Provide a CWP-based API that lets devs override this algo?
private Attribute GetAttributeForRoleCore (VisualRole role, HashSet stack)
{
// Prevent infinite recursion
if (!stack.Add (role))
{
return Normal; // fallback
}
Attribute? attr = Normal;
if (role == VisualRole.Normal || TryGetExplicitlySetAttributeForRole (role, out attr))
{
return attr!.Value;
}
// TODO: Provide an API that lets devs override this algo?
// Derivation algorithm as documented
Attribute result = role switch
{
VisualRole.Focus =>
GetAttributeForRoleCore (VisualRole.Normal, stack) with
{
Foreground = GetAttributeForRoleCore (VisualRole.Normal, stack).Background,
Background = GetAttributeForRoleCore (VisualRole.Normal, stack).Foreground
},
VisualRole.Active =>
GetAttributeForRoleCore (VisualRole.Focus, stack) with
{
Foreground = GetAttributeForRoleCore (VisualRole.Focus, stack).Foreground.GetBrighterColor (),
Background = GetAttributeForRoleCore (VisualRole.Focus, stack).Background.GetDimColor (),
Style = GetAttributeForRoleCore (VisualRole.Focus, stack).Style | TextStyle.Bold
},
VisualRole.Highlight =>
GetAttributeForRoleCore (VisualRole.Normal, stack) with
{
Foreground = GetAttributeForRoleCore (VisualRole.Normal, stack).Background.GetBrighterColor (),
Background = GetAttributeForRoleCore (VisualRole.Normal, stack).Background,
Style = GetAttributeForRoleCore (VisualRole.Editable, stack).Style | TextStyle.Italic
},
VisualRole.Editable =>
GetAttributeForRoleCore (VisualRole.Normal, stack) with
{
Foreground = GetAttributeForRoleCore (VisualRole.Normal, stack).Foreground,
Background = GetAttributeForRoleCore (VisualRole.Normal, stack).Foreground.GetDimColor (0.5)
},
VisualRole.ReadOnly =>
GetAttributeForRoleCore (VisualRole.Editable, stack) with
{
Foreground = GetAttributeForRoleCore (VisualRole.Editable, stack).Foreground.GetDimColor (0.05),
},
VisualRole.Disabled =>
GetAttributeForRoleCore (VisualRole.Normal, stack) with
{
Foreground = GetAttributeForRoleCore (VisualRole.Normal, stack).Foreground.GetDimColor (0.05),
},
VisualRole.HotNormal =>
GetAttributeForRoleCore (VisualRole.Normal, stack) with
{
Style = GetAttributeForRoleCore (VisualRole.Normal, stack).Style | TextStyle.Underline
},
VisualRole.HotFocus =>
GetAttributeForRoleCore (VisualRole.Focus, stack) with
{
Style = GetAttributeForRoleCore (VisualRole.Focus, stack).Style | TextStyle.Underline
},
VisualRole.HotActive =>
GetAttributeForRoleCore (VisualRole.Active, stack) with
{
Style = GetAttributeForRoleCore (VisualRole.Active, stack).Style | TextStyle.Underline
},
_ => GetAttributeForRoleCore (VisualRole.Normal, stack)
};
stack.Remove (role);
return result;
}
///
/// Gets the associated with a specified string.
///
/// The name of the describing the element being rendered.
/// The corresponding from the .
public Attribute GetAttributeForRole (string roleName)
{
if (Enum.TryParse (roleName, true, out VisualRole role))
{
return GetAttributeForRole (role);
}
// If the string does not match any VisualRole, return the default Normal attribute
return Normal;
}
// Helper method for property _get implementation
private Attribute GetAttributeForRoleProperty (Attribute? explicitValue, VisualRole role)
{
if (explicitValue is { })
{
return explicitValue.Value;
}
return GetAttributeForRoleCore (role, []);
}
// Helper method for property _set implementation
private Attribute? SetAttributeForRoleProperty (Attribute value, VisualRole role)
{
// If value is the same as the algorithm value, use null
if (GetAttributeForRoleCore (role, []) == value)
{
return null;
}
return value;
}
private readonly Attribute? _normal;
///
/// The default visual role for unfocused, unselected, enabled elements.
/// The Normal attribute must always be set. All other attributes are optional, and if not explicitly
/// set, will be automatically generated. See the description for for details on the
/// algorithm used.
///
public Attribute Normal
{
get => _normal!.Value;
init => _normal = value;
}
private readonly Attribute? _hotNormal;
///
/// The visual role for elements with a indicator.
/// If not explicitly set, will be a derived value. See the description for for details on the
/// algorithm used.
///
public Attribute HotNormal
{
get => GetAttributeForRoleProperty (_hotNormal, VisualRole.HotNormal);
init => _hotNormal = SetAttributeForRoleProperty (value, VisualRole.HotNormal);
}
private readonly Attribute? _focus;
///
/// The visual role when the element is focused.
/// If not explicitly set, will be a derived value. See the description for for details on the
/// algorithm used.
///
public Attribute Focus
{
get => GetAttributeForRoleProperty (_focus, VisualRole.Focus);
init => _focus = SetAttributeForRoleProperty (value, VisualRole.Focus);
}
private readonly Attribute? _hotFocus;
///
/// The visual role for elements with a indicator.
/// If not explicitly set, will be a derived value. See the description for for details on the
/// algorithm used.
///
public Attribute HotFocus
{
get => GetAttributeForRoleProperty (_hotFocus, VisualRole.HotFocus);
init => _hotFocus = SetAttributeForRoleProperty (value, VisualRole.HotFocus);
}
private readonly Attribute? _active;
///
/// The visual role for elements that are active or selected (e.g., selected item in a ). Also
/// used
/// for headers in, , and .
/// If not explicitly set, will be a derived value. See the description for for details on the
/// algorithm used.
///
public Attribute Active
{
get => GetAttributeForRoleProperty (_active, VisualRole.Active);
init => _active = SetAttributeForRoleProperty (value, VisualRole.Active);
}
private readonly Attribute? _hotActive;
///
/// The visual role for elements with a indicator.
/// If not explicitly set, will be a derived value. See the description for for details on the
/// algorithm used.
///
public Attribute HotActive
{
get => GetAttributeForRoleProperty (_hotActive, VisualRole.HotActive);
init => _hotActive = SetAttributeForRoleProperty (value, VisualRole.HotActive);
}
private readonly Attribute? _highlight;
///
/// The visual role for elements that are highlighted (e.g., when the mouse is inside a ).
/// If not explicitly set, will be a derived value. See the description for for details on the
/// algorithm used.
///
public Attribute Highlight
{
get => GetAttributeForRoleProperty (_highlight, VisualRole.Highlight);
init => _highlight = SetAttributeForRoleProperty (value, VisualRole.Highlight);
}
private readonly Attribute? _editable;
///
/// The visual role for elements that are editable (e.g., and ).
/// If not explicitly set, will be a derived value. See the description for for details on the
/// algorithm used.
///
public Attribute Editable
{
get => GetAttributeForRoleProperty (_editable, VisualRole.Editable);
init => _editable = SetAttributeForRoleProperty (value, VisualRole.Editable);
}
private readonly Attribute? _readOnly;
///
/// The visual role for elements that are normally editable but currently read-only.
/// If not explicitly set, will be a derived value. See the description for for details on the
/// algorithm used.
///
public Attribute ReadOnly
{
get => GetAttributeForRoleProperty (_readOnly, VisualRole.ReadOnly);
init => _readOnly = SetAttributeForRoleProperty (value, VisualRole.ReadOnly);
}
private readonly Attribute? _disabled;
///
/// The visual role for elements that are disabled and not interactable.
/// If not explicitly set, will be a derived value. See the description for for details on the
/// algorithm used.
///
public Attribute Disabled
{
get => GetAttributeForRoleProperty (_disabled, VisualRole.Disabled);
init => _disabled = SetAttributeForRoleProperty (value, VisualRole.Disabled);
}
///
public virtual bool Equals (Scheme? other)
{
return other is { }
&& EqualityComparer.Default.Equals (Normal, other.Normal)
&& EqualityComparer.Default.Equals (HotNormal, other.HotNormal)
&& EqualityComparer.Default.Equals (Focus, other.Focus)
&& EqualityComparer.Default.Equals (HotFocus, other.HotFocus)
&& EqualityComparer.Default.Equals (Active, other.Active)
&& EqualityComparer.Default.Equals (HotActive, other.HotActive)
&& EqualityComparer.Default.Equals (Highlight, other.Highlight)
&& EqualityComparer.Default.Equals (Editable, other.Editable)
&& EqualityComparer.Default.Equals (ReadOnly, other.ReadOnly)
&& EqualityComparer.Default.Equals (Disabled, other.Disabled);
}
///
public override int GetHashCode ()
{
return HashCode.Combine (
HashCode.Combine (Normal, HotNormal, Focus, HotFocus, Active, HotActive, Highlight, Editable),
HashCode.Combine (ReadOnly, Disabled)
);
}
///
public override string ToString ()
{
return $"Normal: {Normal}; HotNormal: {HotNormal}; Focus: {Focus}; HotFocus: {HotFocus}; "
+ $"Active: {Active}; HotActive: {HotActive}; Highlight: {Highlight}; Editable: {Editable}; "
+ $"ReadOnly: {ReadOnly}; Disabled: {Disabled}";
}
}