#nullable enable 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.Toplevel)!, CreateToplevel ()), ] ); 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 CreateToplevel () { 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}"; } }