FontConf.java

package swingtree.style;

import com.google.errorprone.annotations.Immutable;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import sprouts.Val;
import swingtree.SwingTree;
import swingtree.UI;
import swingtree.api.Configurator;
import swingtree.api.Styler;

import javax.swing.*;
import java.awt.*;
import java.awt.font.TextAttribute;
import java.awt.geom.AffineTransform;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 *  An immutable, wither-like method based config API for font styles
 *  that is part of the full {@link StyleConf} configuration object.
 *  <p>
 *  The following properties with their respective purpose are available:
 *  <br>
 *  <ol>
 *      <li><b>Name</b>
 *          <p>
 *              The name of the font, which is essentially the font family.
 *              This will ultimately translate to {@link Font#getFamily()}.<br>
 *              You may specify the font family name through the {@link #family(String)} method.
 *          </p>
 *      </li>
 *      <li><b>Size</b>
 *          <p>
 *              The size of the font in points,
 *              which will ultimately translate to {@link Font#getSize()}.
 *              Use the {@link #size(int)} method to specify the size of the font.
 *          </p>
 *      </li>
 *      <li><b>Posture</b>
 *          <p>
 *              The posture of the font, which is a value between 0 and 1.
 *              <br>
 *              A value of 0 means that the font is not italic,
 *              while a value of 1 means that the font is "fully" italic.
 *              <br>
 *              You can use the {@link #posture(float)} method to specify the posture of the font.
 *          </p>
 *      </li>
 *      <li><b>Weight</b>
 *          <p>
 *              The weight of the font (boldness, see {@link Font#BOLD}),
 *              which is a value between 0 and 2.
 *          </p>
 *          <p>
 *              The weight of the font can be specified using the {@link #weight(double)} method.
 *          </p>
 *      </li>
 *      <li><b>Spacing (Tracking)</b>
 *          <p>
 *              This property controls the tracking which is a floating point number
 *              with the default value of
 *              {@code 0}, meaning no additional tracking is added to the font.
 *             
 *              <p>Useful constant values are the predefined {@link TextAttribute#TRACKING_TIGHT} and {@link
 *              TextAttribute#TRACKING_LOOSE} values, which represent values of {@code -0.04} and {@code 0.04},
 *             
 *              <p>The tracking value is multiplied by the font point size and
 *              passed through the font transform to determine an additional
 *              amount to add to the advance of each glyph cluster.  Positive
 *              tracking values will inhibit formation of optional ligatures.
 *              Tracking values are typically between {@code -0.1} and
 *              {@code 0.3}; values outside this range are generally not
 *              desirable.
 *          </p>
 *          <p>
 *              You can use the {@link #spacing(float)} method to specify the tracking of the font.
 *          </p>
 *      </li>
 *      <li><b>Color</b>
 *          <p>
 *              The color of the font, which translates to the text property
 *              {@link TextAttribute#FOREGROUND}.
 *          </p>
 *          <p>
 *              You can use the {@link #color(Color)} or {@link #color(String)} methods to specify the color of the font.
 *          </p>
 *      </li>
 *      <li><b>Background Color</b>
 *          <p>
 *              The background color of the font
 *              which translates to the text property {@link TextAttribute#BACKGROUND}.
 *          </p>
 *      </li>
 *      <li><b>Selection Color</b>
 *          <p>
 *              The selection color of the font, which translates to
 *              {@link javax.swing.text.JTextComponent#setSelectionColor(Color)}.
 *              <br>
 *              Note that this property is only relevant for text components,
 *              most components do not support text selection.
 *          </p>
 *      </li>
 *      <li><b>Underlined</b>
 *          <p>
 *              Whether or not the font is underlined.
 *              This will ultimately translate to {@link TextAttribute#UNDERLINE}.
 *          </p>
 *      </li>
 *      <li><b>Strike</b>
 *          <p>
 *              Whether or not the font is strike through.
 *              This will ultimately translate to {@link TextAttribute#STRIKETHROUGH}.
 *          </p>
 *      </li>
 *      <li><b>Transform</b>
 *          <p>
 *              The transform of the font, which is an {@link AffineTransform} instance.
 *          </p>
 *      </li>
 *      <li><b>Paint</b>
 *          <p>
 *              The paint of the font, which is a {@link Paint} instance.
 *              Note that specifying a custom paint will override the effects of the color property
 *              as the color property is in essence merely a convenience property for a
 *              paint painting across the entire font area homogeneously using the specified color.
 *          </p>
 *      </li>
 *      <li><b>Background Paint</b>
 *          <p>
 *              The background paint of the font, which is a {@link Paint} instance
 *              that is used to paint the background of the font.
 *          </p>
 *      </li>
 *      <li><b>Horizontal Alignment</b>
 *          <p>
 *              The horizontal alignment of the font.
 *              <br>
 *              Note that this property is not relevant for all components,
 *              It will usually only be relevant for {@link JLabel}, {@link AbstractButton} and {@link JTextField}
 *              types or maybe some custom components.
 *              Not all components support horizontal alignment.
 *          </p>
 *      </li>
 *      <li><b>Vertical Alignment</b>
 *          <p>
 *              The vertical alignment of the font.
 *              <br>
 *              Note that this property is not relevant for all components,
 *              It will usually only be relevant for {@link JLabel}, {@link AbstractButton} and {@link JTextField}
 *              types or maybe some custom components.
 *              Not all components support vertical alignment.
 *          </p>
 *      </li>
 *  </ol>
 *  <p>
 *  You can use the {@link #none()} method to specify that no font should be used,
 *  as the instance returned by that method is a font style with an empty string as its name,
 *  and other properties set to their default values,
 *  effectively making it a representation of the absence of a font style.
 *  <p>
 *  Also note that this class is immutable, which means that wither-like methods
 *  will always return new instances of this class, leaving the original instance untouched.
 *  <br>
 *  This means that you can not modify a font style instance directly, but you can
 *  easily create a modified copy of it by calling one of the wither-like methods.
 *
 *  <h2>Where the font color lives — read this before touching the color code!</h2>
 *  This config has a {@link #color(java.awt.Color) color}, yet the font it derives for a
 *  <b>styled component</b> deliberately does <b>not</b> carry that color — do not "fix" that!
 *  In AWT's model a {@link java.awt.Font} describes glyph geometry only; text color belongs
 *  to the component's <em>foreground</em> property, which is the channel every look-and-feel
 *  actually consults when painting text. A color <em>can</em> be embedded in a font (as
 *  {@code TextAttribute.FOREGROUND}), but doing so has two severe consequences:
 *  <ol>
 *      <li>it flips {@link java.awt.Font#hasLayoutAttributes()}, which makes the JDK build a
 *          {@code TextLayout} for <b>every</b> {@code stringWidth} and {@code drawString} —
 *          benchmarked as the dominant text cost of heavily styled UIs; and</li>
 *      <li>it silently overrides the look-and-feel's <em>state</em> colors — disabled or
 *          selected text keeps the embedded color instead of graying out properly.</li>
 *  </ol>
 *  Therefore: a <b>solid</b> font color is routed through {@code component.setForeground(..)}
 *  by the {@link StyleInstaller} (see {@link #createDerivedWithoutSolidColorFrom(Font, JComponent)}
 *  and {@link #solidColor()}), while gradient/noise/custom paints — which no foreground channel
 *  can express — stay in the font as attributes and knowingly pay the {@code TextLayout} price.
 *  The context-free conversions behave accordingly: {@link #toAwtFont()} is full-fidelity
 *  (color included, cost documented), {@link #toAwtFontWithoutColor()} is the
 *  component-installation variant.
 *
 * @author Daniel Nepp
 */
@Immutable
@SuppressWarnings("Immutable")
public final class FontConf
{
    private static final Logger log = org.slf4j.LoggerFactory.getLogger(FontConf.class);

    private static final FontConf _NONE = new FontConf(
                                                        "",    // Font name (family)
                                                        -1,     // size
                                                        -1,    // posture
                                                        -1,    // weight
                                                        0,     // spacing
                                                        null,  // selection color
                                                        null,  // is underlined
                                                        null,  // is strike through
                                                        null,  // transform
                                                        FontPaintConf.none(),  // paint
                                                        FontPaintConf.none(),  // background paint
                                                        UI.HorizontalAlignment.UNDEFINED,  // horizontal alignment
                                                        UI.VerticalAlignment.UNDEFINED   // vertical alignment
                                                    );

    /**
     * Returns a font style with all properties set to their no-op default values,
     * effectively making it a representation of the absence of a font style.
     *
     * @return A font style with all properties set to no-op default values.
     */
    public static FontConf none() { return _NONE; }

    static FontConf of(
        String                     name,
        int                        fontSize,
        float                      posture,
        float                      weight,
        float                      spacing,
        @Nullable Color            selectionColor,
        @Nullable Boolean          isUnderline,
        @Nullable Boolean          isStrike,
        @Nullable AffineTransform  transform,
        FontPaintConf              paint,
        FontPaintConf              backgroundPaint,
        UI.HorizontalAlignment     horizontalAlignment,
        UI.VerticalAlignment       verticalAlignment
    ) {
        if (
            name.isEmpty() &&
            fontSize == 0 &&
            posture == 0 &&
            weight == 0 &&
            spacing == 0 &&
            selectionColor == null &&
            isUnderline == null &&
            isStrike == null &&
            transform == null &&
            paint.equals(FontPaintConf.none()) &&
            backgroundPaint.equals(FontPaintConf.none()) &&
            horizontalAlignment == _NONE._horizontalAlignment &&
            verticalAlignment == _NONE._verticalAlignment
        )
            return _NONE;
        else
            return new FontConf(
                    name,
                    fontSize,
                    posture,
                    weight,
                    spacing,
                    selectionColor,
                    isUnderline,
                    isStrike,
                    transform,
                    paint,
                    backgroundPaint,
                    horizontalAlignment,
                    verticalAlignment
                );
    }

    private final String                    _familyName;
    private final int                       _size;
    private final float                     _posture;
    private final float                     _weight;
    private final float                     _spacing;
    private final @Nullable Color           _selectionColor; // Only relevant for text components with selection support.
    private final @Nullable Boolean         _isUnderlined;
    private final @Nullable Boolean         _isStrike;
    private final @Nullable AffineTransform _transform;
    private final FontPaintConf             _paint;
    private final FontPaintConf             _backgroundPaint;
    private final UI.HorizontalAlignment    _horizontalAlignment;
    private final UI.VerticalAlignment      _verticalAlignment;


    private FontConf(
        String                    name,
        int                       fontSize,
        float                     posture,
        float                     weight,
        float                     spacing,
        @Nullable Color           selectionColor,
        @Nullable Boolean         isUnderline,
        @Nullable Boolean         isStrike,
        @Nullable AffineTransform transform,
        FontPaintConf             paint,
        FontPaintConf             backgroundPaint,
        UI.HorizontalAlignment    horizontalAlignment,
        UI.VerticalAlignment      verticalAlignment
    ) {
        _familyName = Objects.requireNonNull(name);
        _size    = fontSize;
        _posture = posture < 0 ? -1 : posture;
        _weight  = weight < 0 ? -1 : weight;
        _spacing = spacing;
        _selectionColor  = selectionColor;
        _isUnderlined    = isUnderline;
        _isStrike        = isStrike;
        _transform       = transform;
        _paint           = paint;
        _backgroundPaint = backgroundPaint;
        _horizontalAlignment = horizontalAlignment;
        _verticalAlignment   = verticalAlignment;
    }

    /**
     * Returns the font family name for this font configuration.
     * This corresponds to the {@link Font#getFamily()} property in {@link java.awt.Font}
     * and determines the visual style category of the typeface.
     * <p>
     * The font family represents a group of related typefaces that share common design characteristics
     * but may vary in weight, style, or other attributes. Common examples include:
     * <ul>
     *   <li>"Arial" - A clean, sans-serif font</li>
     *   <li>"Times New Roman" - A classic serif font</li>
     *   <li>"Courier New" - A monospaced font</li>
     *   <li>"Verdana" - A sans-serif font designed for screen readability</li>
     * </ul>
     * <p>
     * From a user perspective, the font family defines the overall visual personality of the text.
     *
     * @return The font family name as a String. Returns an empty string if no specific family is defined,
     *         which typically means the system default font family should be used.
     * @see Font#getFamily()
     */
    public String family() { return _familyName; }

    /**
     * Returns the font size in points for this font configuration.
     * This corresponds to the {@link Font#getSize()} property in {@link java.awt.Font}
     * and determines the visual height of the text characters.
     * <p>
     * In typography, a "point" is a physical unit of measurement where 1 point equals 1/72 of an inch.
     * However, in digital typography, the actual rendered size may vary based on screen resolution,
     * display density (DPI), and the current UI scaling factor.
     * <p>
     * The perceived size may also be influenced by the specific font family, as some typefaces
     * appear larger than others at the same point size due to differences in x-height and character proportions.
     *
     * @return The font size in points. Returns 0 if no specific size is defined,
     *         which typically means the system default font size should be used.
     * @see Font#getSize()
     */
    public int size() { return _size; }

    /**
     * Returns the posture (italic) attribute of this font configuration.
     * This corresponds to the {@link TextAttribute#POSTURE} property and influences
     * whether the font appears in regular upright or italic (slanted) form.
     * <p>
     * The posture is represented as a floating-point value where:
     * <ul>
     *   <li><b>0.0</b> - Regular upright posture (non-italic)</li>
     *   <li><b>0.2</b> - Standard italic posture</li>
     *   <li>Values between 0.0 and 0.2 represent varying degrees of slant</li>
     * </ul>
     * <p>
     * From a user perspective, italic text serves several purposes:
     * <ul>
     *   <li><b>Emphasis</b> - Drawing attention to specific words or phrases within a body of text</li>
     *   <li><b>Distinction</b> - Differentiating certain types of content, such as book titles,
     *       foreign words, or technical terms</li>
     *   <li><b>Visual variety</b> - Adding stylistic variation to break visual monotony</li>
     *   <li><b>Hierarchy</b> - Creating subtle visual distinctions in complex layouts</li>
     * </ul>
     * <p>
     * It's important to note that true italic fonts are specifically designed with unique character shapes,
     * while oblique fonts are simply slanted versions of the regular font. The visual quality
     * depends on whether the font family includes dedicated italic variants.
     *
     * @return An {@link Optional} containing the posture value if defined, or empty if no specific
     *         posture is configured (system default should be used).
     * @see TextAttribute#POSTURE
     * @see Font#isItalic()
     */
    public Optional<Float> posture() { return Optional.ofNullable(_posture < 0 ? null : _posture); }

    /**
     * Returns the weight (boldness) attribute of this font configuration.
     * This corresponds to the {@link TextAttribute#WEIGHT} property and determines
     * the thickness of the character strokes.
     * <p>
     * The weight is represented as a floating-point value where:
     * <ul>
     *   <li><b>0.0</b> - Light weight (thin strokes)</li>
     *   <li><b>1.0</b> - Regular/standard weight (equivalent to {@link Font#PLAIN})</li>
     *   <li><b>2.0</b> - Bold weight (equivalent to {@link Font#BOLD})</li>
     *   <li>Values between these represent intermediate weights (semibold, etc.)</li>
     * </ul>
     * <p>
     * The actual availability of different weights depends on the font family.
     * Some families only support regular and bold, while professional typefaces
     * may offer multiple weights from thin to black.
     *
     * @return An {@link Optional} containing the weight value if defined, or empty if no specific
     *         weight is configured (system default should be used).
     * @see TextAttribute#WEIGHT
     * @see Font#isBold()
     */
    public Optional<Float> weight() { return Optional.ofNullable(_weight < 0 ? null : _weight); }

    /**
     * Returns the character spacing (tracking) attribute of this font configuration.
     * This corresponds to the {@link TextAttribute#TRACKING} property and controls
     * the uniform adjustment of space between characters.
     * <p>
     * The spacing value is a multiplier that affects the space between all characters:
     * <ul>
     *   <li><b>0.0</b> - Normal, font-default spacing</li>
     *   <li><b>Negative values</b> (-0.04 to 0.0) - Tighter spacing, characters are closer together</li>
     *   <li><b>Positive values</b> (0.0 to 0.04) - Looser spacing, characters are farther apart</li>
     * </ul>
     * <p>
     * Useful predefined constants include:
     * <ul>
     *   <li>{@link TextAttribute#TRACKING_TIGHT} (-0.04)</li>
     *   <li>{@link TextAttribute#TRACKING_LOOSE} (0.04)</li>
     * </ul>
     * <p>
     * Typical values range from -0.1 to 0.3, with values outside this range generally
     * producing undesirable results for continuous text. Tighter spacing can make text feel denser and more compact,
     * while looser spacing can improve legibility, especially at small sizes or for
     * users with visual impairments
     *
     * @return The character spacing value. Returns 0.0 if normal, font-default spacing should be used.
     * @see TextAttribute#TRACKING
     */
    public float spacing() { return _spacing; }

    /**
     * Returns the text selection color for this font configuration.
     * This property is specifically relevant for text components that support
     * user text selection, such as {@link JTextField}, {@link JTextArea}, and {@link JTextPane}.
     * <p>
     * The selection color determines the background color applied to text when users
     * highlight it with mouse dragging or keyboard navigation. This visual feedback
     * is crucial for indicating which text is currently selected for operations like
     * copy, cut, or formatting changes.
     * <p>
     * This property typically maps to component methods like
     * {@link javax.swing.text.JTextComponent#setSelectionColor(Color)}.
     * When not specified, components use their default selection colors, which are
     * often derived from the current look and feel or system preferences.
     *
     * @return An {@link Optional} containing the selection {@link Color} if defined,
     *         or empty if no specific selection color is configured (component default should be used).
     * @see javax.swing.text.JTextComponent#setSelectionColor(Color)
     * @see javax.swing.text.JTextComponent#getSelectionColor()
     */
    public Optional<Color> selectionColor() { return Optional.ofNullable(_selectionColor); }

    /**
     * Returns whether the font should be rendered with an underline.
     * This corresponds to the {@link TextAttribute#UNDERLINE} property and controls
     * the presence of a horizontal line beneath the text baseline.
     * <p>
     * The underline is a binary attribute - text is either underlined or not.
     * When enabled, a continuous line is drawn below the text characters, typically
     * positioned to avoid descending characters (like 'g', 'j', 'p', 'q', 'y').
     * <p>
     * It's important to use underlining judiciously, as excessive use can reduce
     * readability and create visual noise. In modern UI design, underlining is
     * primarily reserved for link identification.
     *
     * @return {@code true} if the font should be rendered with an underline,
     *         {@code false} if no underline should be applied. Returns the default
     *         value (false) if no specific underline preference is configured.
     * @see TextAttribute#UNDERLINE
     * @see TextAttribute#UNDERLINE_ON
     */
    public boolean isUnderlined() { return _isUnderlined != null ? _isUnderlined : false; }

    boolean isStrikeThrough() { return _isStrike != null ? _isStrike : false; }

    Optional<AffineTransform> transform() { // Deliberately package private to prevent leaking mutable state!
        return Optional.ofNullable(_transform);
    }

    Optional<Paint> paint() { // Deliberately package private to prevent leaking mutable state!
        if ( FontPaintConf.none().equals(_paint) )
            return Optional.empty();
        return Optional.ofNullable(_paint.getFor(BoxModelConf.none()));
    }

    Optional<Paint> backgroundPaint() { // Deliberately package private to prevent leaking mutable state!
        if ( FontPaintConf.none().equals(_backgroundPaint) )
            return Optional.empty();
        return Optional.ofNullable(_backgroundPaint.getFor(BoxModelConf.none()));
    }

    /**
     *  The horizontal alignment of a font configuration is an enum constant which
     *  express how text should be positioned within components that support
     *  text alignment in terms of left/right/center positioning along
     *  the x-axis.
     *
     * <p><b>Component Support Limitations:</b><br>
     * Note that horizontal alignment is only applicable to specific Swing components that
     * expose alignment methods or other means of styling, such as:
     * <ul>
     *   <li>{@link JLabel#setHorizontalAlignment(int)}</li>
     *   <li>{@link AbstractButton#setHorizontalAlignment(int)}</li>
     *   <li>{@link JTextField#setHorizontalAlignment(int)} (vertical alignment not supported)</li>
     *   <li>{@link JTextPane#getStyledDocument()} (vertical alignment not supported)</li>
     * </ul>
     * This property will have no effect on components that don't support text alignment,
     * except for when you use this property to render text through the SwingTree style API
     * (see {@link swingtree.UIForAnySwing#withStyle(Styler)}).
     *
     * @return The horizontal alignment constant of this conf, which
     *         may be NONE, LEFT, CENTER, RIGHT... among others.
     */
    public UI.HorizontalAlignment horizontalAlignment() { return _horizontalAlignment; }

    /**
     *  The vertical alignment of a font configuration is an enum constant which
     *  express how text should be positioned within components that support
     *  text alignment in terms of top/bottom/center positioning along the y-axis.
     *
     * <p><b>Component Support Limitations:</b><br>
     * Note that vertical alignment is only applicable to specific Swing components that
     * expose alignment methods or other means of styling, such as:
     * <ul>
     *   <li>{@link JLabel#setVerticalAlignment(int)}</li>
     *   <li>{@link AbstractButton#setVerticalAlignment(int)}</li>
     * </ul>
     * This property will have no effect on components that don't support text alignment,
     * except for when you use this property to render text through the SwingTree style API
     * (see {@link swingtree.UIForAnySwing#withStyle(Styler)}).
     *
     * @return The vertical alignment constant of this conf, which
     *         may be NONE, TOP, CENTER, BOTTOM... among others.
     */
    public UI.VerticalAlignment verticalAlignment() { return _verticalAlignment; }

    /**
     * Returns an updated font config with the specified font family name.
     *
     * @param fontFamily The font family name to use for the {@link Font#getFamily()} property.
     * @return A new font style with the specified font family name.
     */
    public FontConf family( String fontFamily ) {
        return FontConf.of(fontFamily, _size, _posture, _weight, _spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, _paint, _backgroundPaint, _horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified font size,
     * which will translate to a {@link Font} instance with the specified size 
     * (see {@link Font#getSize()}).
     *
     * @param fontSize The font size to use for the {@link Font#getSize()} property.
     * @return A new font style with the specified font size.
     */
    public FontConf size( int fontSize ) {
        return FontConf.of(_familyName, fontSize, _posture, _weight, _spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, _paint, _backgroundPaint, _horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified posture, defining the tilt of the font.
     * A {@link Font} with a higher posture value will be more italic.
     * (see {@link Font#isItalic()}).
     *
     * @param posture The posture to use for the {@link Font#isItalic()} property.
     * @return A new font style with the specified posture.
     */
    public FontConf posture( float posture ) {
        return FontConf.of(_familyName, _size, posture, _weight, _spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, _paint, _backgroundPaint, _horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified weight, defining the boldness of the font.
     * A {@link Font} with a higher weight value will be bolder.
     * (see {@link Font#isBold()}).
     *
     * @param fontWeight The weight to use for the {@link Font#isBold()} property.
     * @return A new font style with the specified weight.
     */
    public FontConf weight( double fontWeight ) {
        return FontConf.of(_familyName, _size, _posture, (float) fontWeight, _spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, _paint, _backgroundPaint, _horizontalAlignment, _verticalAlignment);
    }

    /**
     *  Determines if the font should be plain, bold, italic or bold and italic
     *  based on the provided {@link UI.FontStyle} parameter,
     *  which may be {@link UI.FontStyle#PLAIN}, {@link UI.FontStyle#BOLD},
     *  {@link UI.FontStyle#ITALIC} or {@link UI.FontStyle#BOLD_ITALIC}.<br>
     *  <b>
     *      Note that this will override any previous bold or italic settings.
     *  </b>
     * @param fontStyle The font style to use for the font in the {@link UI.FontStyle} enum.
     * @return An updated font config with the specified font style.
     */
    public FontConf style( UI.FontStyle fontStyle ) {
        switch (fontStyle) {
            case PLAIN:
                return weight(0).posture(0);
            case BOLD:
                return weight(2).posture(0);
            case ITALIC:
                return weight(0).posture(0.2f);
            case BOLD_ITALIC:
                return weight(2).posture(0.2f);
            default:
                return this;
        }
    }

    /**
     * Returns an updated font config with the specified spacing, defining the tracking of the font.
     * The tracking value is multiplied by the font point size and
     * passed through the font transform to determine an additional
     * amount to add to the advance of each glyph cluster.  Positive
     * tracking values will inhibit formation of optional ligatures.
     * Tracking values are typically between {@code -0.1} and
     * {@code 0.3}; values outside this range are generally not
     * desirable.
     *
     * @param spacing The spacing to use for the {@link TextAttribute#TRACKING} property.
     * @return A new font style with the specified spacing.
     */
    public FontConf spacing( float spacing ) {
        return FontConf.of(_familyName, _size, _posture, _weight, spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, _paint, _backgroundPaint, _horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified color,
     * which will be used for the {@link TextAttribute#FOREGROUND} property
     * of the resulting {@link Font} instance.
     *
     * @param color The color to use for the {@link TextAttribute#FOREGROUND} property.
     * @return A new font style with the specified color.
     */
    public FontConf color( Color color ) {
        Objects.requireNonNull(color);
        if ( StyleUtil.isUndefinedColor(color) )
            color = null;
        if ( _paint.representsColor(color) )
            return this;

        FontPaintConf paintConf = FontPaintConf.of(color, null, null, null);

        return FontConf.of(_familyName, _size, _posture, _weight, _spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, paintConf, _backgroundPaint, _horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified color string used to define the font color.
     * The color will be used for the {@link TextAttribute#FOREGROUND} property
     * of the resulting {@link Font} instance.
     *
     * @param colorString The color string to use for the {@link TextAttribute#FOREGROUND} property.
     * @return A new font style with the specified color.
     */
    public FontConf color( String colorString ) {
        Objects.requireNonNull(colorString);
        Color newColor;
        try {
            if ( colorString.isEmpty() )
                newColor = UI.Color.UNDEFINED;
            else
                newColor = UI.color(colorString);
        } catch ( Exception e ) {
            log.error(SwingTree.get().logMarker(), "Failed to parse color string: '"+colorString+"'", e);
            return this;
        }
        return color(newColor);
    }

    /**
     * Returns an updated font config with the specified background color.
     * The color value will be used for the {@link TextAttribute#BACKGROUND} property
     * of the resulting {@link Font} instance.
     *
     * @param backgroundColor The background color to use for the {@link TextAttribute#BACKGROUND} property.
     * @return A new font style with the specified background color.
     */
    public FontConf backgroundColor( Color backgroundColor ) {
        Objects.requireNonNull(backgroundColor);
        if ( StyleUtil.isUndefinedColor(backgroundColor) )
            backgroundColor = null;
        if ( _backgroundPaint.representsColor(backgroundColor) )
            return this;

        FontPaintConf backgroundPaintConf = FontPaintConf.of(backgroundColor, null, null, null);

        return FontConf.of(_familyName, _size, _posture, _weight, _spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, _paint, backgroundPaintConf, _horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified background color string used to define the background color.
     * The background color will be used for the {@link TextAttribute#BACKGROUND} property
     * of the resulting {@link Font} instance.
     *
     * @param colorString The color string to use for the {@link TextAttribute#BACKGROUND} property.
     * @return A new font style with the specified background color.
     */
    public FontConf backgroundColor( String colorString ) {
        Objects.requireNonNull(colorString);
        Color newColor;
        try {
            if ( colorString.isEmpty() )
                newColor = UI.Color.UNDEFINED;
            else
                newColor = UI.color(colorString);
        } catch ( Exception e ) {
            log.error(SwingTree.get().logMarker(), "Failed to parse color string: '{}'", colorString, e);
            return this;
        }
        return backgroundColor(newColor);
    }

    /**
     * Returns an updated font config with the specified selection color.
     * The selection color will be used for the selection color of the font.
     * Note that not all components support text selection, so this property may not
     * have an effect on all components.
     *
     * @param selectionColor The selection color to use for the selection color of the font.
     * @return A new font style with the specified selection color.
     */
    public FontConf selectionColor( Color selectionColor ) {
        Objects.requireNonNull(selectionColor);
        if ( StyleUtil.isUndefinedColor(selectionColor) )
            selectionColor = null;
        if ( Objects.equals(selectionColor, _selectionColor) )
            return this;
        return FontConf.of(_familyName, _size, _posture, _weight, _spacing, selectionColor, _isUnderlined, _isStrike, _transform, _paint, _backgroundPaint, _horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified selection color string used to define the selection color.
     * The selection color will be used for the selection color of the font.
     * Note that not all components support text selection, so this property may not
     * have an effect on all components.
     *
     * @param colorString The color string to use for the selection color of the font.
     * @return A new font style with the specified selection color.
     */
    public FontConf selectionColor( String colorString ) {
        Objects.requireNonNull(colorString);
        Color newColor;
        try {
            if ( colorString.isEmpty() )
                newColor = UI.Color.UNDEFINED;
            else
                newColor = UI.color(colorString);
        } catch ( Exception e ) {
            log.error(SwingTree.get().logMarker(), "Failed to parse color string: '"+colorString+"'", e);
            return this;
        }
        return selectionColor(newColor);
    }

    /**
     * Returns an updated font config with the specified underlined property.
     * This boolean will translate to the {@link TextAttribute#UNDERLINE} property
     * of the resulting {@link Font} instance.
     *
     * @param underlined Whether the font should be underlined.
     * @return A new font style with the specified underlined property.
     */
    public FontConf underlined( boolean underlined ) {
        return FontConf.of(_familyName, _size, _posture, _weight, _spacing, _selectionColor, underlined, _isStrike, _transform, _paint, _backgroundPaint, _horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified strike through property.
     * This boolean will translate to the {@link TextAttribute#STRIKETHROUGH} property
     * of the resulting {@link Font} instance.
     *
     * @param strike Whether the font should be strike through.
     * @return A new font style with the specified strike through property.
     */
    public FontConf strikeThrough( boolean strike ) {
        return FontConf.of(_familyName, _size, _posture, _weight, _spacing, _selectionColor, _isUnderlined, strike, _transform, _paint, _backgroundPaint, _horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified transform.
     * This transform will be used for the {@link TextAttribute#TRANSFORM} property
     * of the resulting {@link Font} instance.
     *
     * @param transform The transform to use for the {@link TextAttribute#TRANSFORM} property.
     * @return A new font style with the specified transform.
     */
    public FontConf transform( @Nullable AffineTransform transform ) {
        return FontConf.of(_familyName, _size, _posture, _weight, _spacing, _selectionColor, _isUnderlined, _isStrike, transform, _paint, _backgroundPaint, _horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified paint.
     * This paint will be used for the {@link TextAttribute#FOREGROUND} property
     * of the resulting {@link Font} instance.
     * Note that specifying a custom paint will override the effects of the color property
     * as the color property is in essence merely a convenience property for a
     * paint painting across the entire font area homogeneously using the specified color.
     * <br>
     * Note that this will override the effects of the {@link #color(Color)}, {@link #color(String)},
     * {@link #noise(Configurator)} or {@link #gradient(Configurator)} methods
     * as a font can only have one paint.
     *
     * @param paint The paint to use for the {@link TextAttribute#FOREGROUND} property.
     * @return A new font style with the specified paint.
     */
    public FontConf paint( @Nullable Paint paint ) {
        FontPaintConf paintConf = FontPaintConf.of(null, paint, null, null);
        return FontConf.of(_familyName, _size, _posture, _weight, _spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, paintConf, _backgroundPaint, _horizontalAlignment, _verticalAlignment);
    }

    /**
     *  Configures a noise function based {@link Paint} for the font appearance,
     *  using a configurator function that takes a {@link NoiseConf} instance
     *  and returns an updated {@link NoiseConf} instance with the desired properties.
     *  <br>
     *  Keep in mind that this will override the effects of the {@link #color(Color)},
     *  {@link #color(String)}, {@link #paint(Paint)} or {@link #gradient(Configurator)}
     *  methods as a font can only have one paint.
     *
     * @param configurator The configurator function that takes a {@link NoiseConf} instance
     *                     and returns an updated {@link NoiseConf} instance with the desired properties.
     * @return A new font style with the specified noise paint.
     */
    public FontConf noise( Configurator<NoiseConf> configurator ) {
        Objects.requireNonNull(configurator);
        FontPaintConf paintConf = _paint.noise(configurator);
        return _withPaintConf(paintConf);
    }

    /**
     *  Configures a gradient function based {@link Paint} for the font appearance,
     *  using a configurator function that takes a {@link GradientConf} instance
     *  and returns an updated {@link GradientConf} instance with the desired properties.
     *  <br>
     *  Keep in mind that this will override the effects of the {@link #color(Color)},
     *  {@link #color(String)}, {@link #paint(Paint)} or {@link #noise(Configurator)}
     *  methods as a font can only have one paint.
     *
     * @param configurator The configurator function that takes a {@link GradientConf} instance
     *                     and returns an updated {@link GradientConf} instance with the desired properties.
     * @return A new font style with the specified gradient paint.
     */
    public FontConf gradient( Configurator<GradientConf> configurator ) {
        Objects.requireNonNull(configurator);
        FontPaintConf paintConf = _paint.gradient(configurator);
        return _withPaintConf(paintConf);
    }

    private FontConf _withPaintConf( FontPaintConf paintConf ) {
        return FontConf.of(_familyName, _size, _posture, _weight, _spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, paintConf, _backgroundPaint, _horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified background paint.
     * This paint will be used for the {@link TextAttribute#BACKGROUND} property
     * of the resulting {@link Font} instance.
     *
     * @param backgroundPaint The background paint to use for the {@link TextAttribute#BACKGROUND} property.
     * @return A new font style with the specified background paint.
     */
    public FontConf backgroundPaint( @Nullable Paint backgroundPaint ) {
        FontPaintConf backgroundPaintConf = FontPaintConf.of(null, backgroundPaint, null, null);
        return FontConf.of(_familyName, _size, _posture, _weight, _spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, _paint, backgroundPaintConf, _horizontalAlignment, _verticalAlignment);
    }

    /**
     *  Configures a noise function based {@link Paint} for the background of the font appearance,
     *  using a configurator function that takes a {@link NoiseConf} instance
     *  and returns an updated {@link NoiseConf} instance with the desired properties.
     *  <br>
     *  Note that the background can only have one paint, so specifying a noise based paint
     *  will override the effects of the {@link #backgroundPaint(Paint)}, {@link #backgroundGradient(Configurator)},
     *  and {@link #backgroundColor(String)} methods.
     *
     * @param configurator The configurator function that takes a {@link NoiseConf} instance
     *                     and returns an updated {@link NoiseConf} instance with the desired properties.
     * @return A new font style with the specified noise background paint.
     */
    public FontConf backgroundNoise( Configurator<NoiseConf> configurator ) {
        Objects.requireNonNull(configurator);
        FontPaintConf backgroundPaintConf = _backgroundPaint.noise(configurator);
        return _withBackgroundPaintConf(backgroundPaintConf);
    }

    /**
     *  Configures a gradient function based {@link Paint} for the background of the font appearance,
     *  using a configurator function that takes a {@link GradientConf} instance
     *  and returns an updated {@link GradientConf} instance with the desired properties.
     *  <br>
     *  The background of a font can only have one paint, so specifying a gradient based paint
     *  will override the effects of the {@link #backgroundPaint(Paint)}, {@link #backgroundNoise(Configurator)},
     *  and {@link #backgroundColor(String)} methods.
     *
     * @param configurator The configurator function that takes a {@link GradientConf} instance
     *                     and returns an updated {@link GradientConf} instance with the desired properties.
     * @return A new font style with the specified gradient background paint.
     */
    public FontConf backgroundGradient( Configurator<GradientConf> configurator ) {
        Objects.requireNonNull(configurator);
        FontPaintConf backgroundPaintConf = _backgroundPaint.gradient(configurator);
        return _withBackgroundPaintConf(backgroundPaintConf);
    }

    private FontConf _withBackgroundPaintConf( FontPaintConf backgroundPaintConf ) {
        return FontConf.of(_familyName, _size, _posture, _weight, _spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, _paint, backgroundPaintConf, _horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified horizontal alignment policy, that
     * expresses text to be placed in certain positions alongside the horizontal axis.
     * This property is not relevant for all components, as it can only be applied to methods
     * like {@link JLabel#setHorizontalAlignment(int)} {@link AbstractButton#setHorizontalAlignment(int)} and
     * {@link JTextField#setHorizontalAlignment(int)}, which not all components have.
     * <b>
     *     You can specify this horizontal text alignment property effectively
     *     through the {@link ComponentStyleDelegate#fontAlignment(UI.VerticalAlignment)}
     *     or through the {@link FontConf} in a {@link UI.Font} passed to methods like
     *     {@link swingtree.UIForAnySwing#withFont(UI.Font)} or {@link swingtree.UIForAnySwing#withFont(Val)}.
     * </b>
     *
     * @param horizontalAlignment The horizontal alignment to use for the font.
     * @return A new font style with the specified horizontal alignment.
     * @throws NullPointerException if the supplied enum constant is null.
     */
    public FontConf horizontalAlignment( UI.HorizontalAlignment horizontalAlignment ) {
        Objects.requireNonNull(horizontalAlignment);
        return FontConf.of(_familyName, _size, _posture, _weight, _spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, _paint, _backgroundPaint, horizontalAlignment, _verticalAlignment);
    }

    /**
     * Returns an updated font config with the specified vertical alignment policy, that
     *      * expresses text to be placed in certain positions alongside the certical axis.
     * This property is not relevant for all components, as it can only be applied to methods
     * like {@link JLabel#setVerticalAlignment(int)} and {@link AbstractButton#setVerticalAlignment(int)},
     * which not all components have!
     * <b>
     *     You can specify this property effectively
     *     through the {@link ComponentStyleDelegate#fontAlignment(UI.VerticalAlignment)}
     *     or through the {@link FontConf} in a {@link UI.Font} passed to methods like
     *     {@link swingtree.UIForAnySwing#withFont(UI.Font)} or {@link swingtree.UIForAnySwing#withFont(Val)}.
     * </b>
     *
     * @param verticalAlignment The vertical alignment to use for the font.
     * @return A new font style with the specified vertical alignment.
     * @throws NullPointerException if the supplied enum constant is null.
     */
    public FontConf verticalAlignment( UI.VerticalAlignment verticalAlignment ) {
        Objects.requireNonNull(verticalAlignment);
        return FontConf.of(_familyName, _size, _posture, _weight, _spacing, _selectionColor, _isUnderlined, _isStrike,  _transform, _paint, _backgroundPaint, _horizontalAlignment, verticalAlignment);
    }

    /**
     * Returns a new font configuration with both horizontal and vertical alignment policies
     * specified through a unified alignment object. This method provides a convenient way
     * to set both alignment dimensions simultaneously while ensuring consistency between
     * horizontal and vertical alignment settings.
     *
     * <p>The alignment properties control how text is positioned within components that support
     * text alignment. The horizontal alignment determines left/right/center positioning along
     * the x-axis, while vertical alignment controls top/bottom/center positioning along the
     * y-axis.</p>
     *
     * <p><b>Component Support Limitations:</b><br>
     * Note that alignment properties are only applicable to specific Swing components that
     * expose alignment methods, such as:
     * <ul>
     *   <li>{@link JLabel#setHorizontalAlignment(int)} and {@link JLabel#setVerticalAlignment(int)}</li>
     *   <li>{@link AbstractButton#setHorizontalAlignment(int)} and {@link AbstractButton#setVerticalAlignment(int)}</li>
     *   <li>{@link JTextField#setHorizontalAlignment(int)} (vertical alignment not supported)</li>
     * </ul>
     * This property will have no effect on components that don't support text alignment,
     * except for when you use this property to render text through the SwingTree style API.
     * (see {@link ComponentStyleDelegate#fontAlignment(UI.Alignment)})
     *
     * <p><b>Recommended Usage:</b><br>
     * For effective alignment specification, use either:
     * <ul>
     *   <li>{@link ComponentStyleDelegate#fontAlignment(UI.VerticalAlignment)} for individual alignment</li>
     *   <li>{@link FontConf} within a {@link UI.Font} passed to:
     *     <ul>
     *       <li>see {@link ComponentStyleDelegate#fontAlignment(UI.Alignment)}</li>
     *       <li>{@link swingtree.UIForAnySwing#withFont(UI.Font)}</li>
     *       <li>{@link swingtree.UIForAnySwing#withFont(Val)}</li>
     *     </ul>
     *   </li>
     * </ul>
     *
     * @param alignment The unified alignment object containing both horizontal and vertical
     *                  alignment specifications. Must not be {@code null}.
     *
     * @return A new {@link FontConf} instance with the specified horizontal and vertical
     *         alignment properties applied, preserving all other font attributes from the
     *         current configuration.
     *
     * @throws NullPointerException if the provided {@code alignment} parameter is {@code null}.
     *
     * @see #horizontalAlignment(UI.HorizontalAlignment)
     * @see #verticalAlignment(UI.VerticalAlignment)
     * @see UI.Alignment
     * @see UI.HorizontalAlignment
     * @see UI.VerticalAlignment
     * @throws NullPointerException if the supplied enum constant is null.
     */
    public FontConf alignment( UI.Alignment alignment ) {
        Objects.requireNonNull(alignment);
        return verticalAlignment(alignment.getVertical()).horizontalAlignment(alignment.getHorizontal());
    }

    /**
     * Creates a new {@link FontConf} instance by extracting the font properties
     * from the provided {@link Font} instance.
     *
     * @param font The {@link Font} instance to extract the properties from.
     * @return A new {@link FontConf} instance with the extracted properties.
     * @throws NullPointerException if the provided font is null.
     */
    public FontConf withPropertiesFromFont( Font font )
    {
        Objects.requireNonNull(font);
        if ( StyleUtil.isUndefinedFont(font) )
            return this;

        Map<TextAttribute, ?> attributeMap = font.getAttributes();

        String family = font.getFamily();

        int size = font.getSize();

        float posture = font.isItalic() ? 0.2f : 0f;
        try {
            if (attributeMap.containsKey(TextAttribute.POSTURE)) {
                Object thing = attributeMap.get(TextAttribute.POSTURE);
                if ( thing instanceof Number )
                    posture = ((Number) thing).floatValue();
            }
        } catch (Exception e) {
            log.debug(SwingTree.get().logMarker(), "Failed to fetch TextAttribute.POSTURE in font attributes '{}' of font '{}'", attributeMap, font, e);
        }

        float weight = font.isBold() ? 2f : 0f;
        try {
            if (attributeMap.containsKey(TextAttribute.WEIGHT)) {
                Object thing = attributeMap.get(TextAttribute.WEIGHT);
                if ( thing instanceof Number )
                    weight = ((Number) thing).floatValue();
            }
        } catch (Exception e) {
            log.debug(SwingTree.get().logMarker(), "Failed to fetch TextAttribute.WEIGHT in font attributes '{}' of font '{}'", attributeMap, font, e);
        }

        float spacing = _spacing;
        try {
            if (attributeMap.containsKey(TextAttribute.TRACKING)) {
                Object thing = attributeMap.get(TextAttribute.TRACKING);
                if ( thing instanceof Number )
                    spacing = ((Number) thing).floatValue();
            }
        } catch (Exception e) {
            log.debug(SwingTree.get().logMarker(), "Failed to fetch TextAttribute.TRACKING in font attributes '{}' of font '{}'", attributeMap, font, e);
        }

        Color selectionColor = _selectionColor;
        // The selection color is not a text attribute, but a component property, like for text areas.

        boolean isUnderline = ( _isUnderlined != null ? _isUnderlined : false );
        try {
            if (attributeMap.containsKey(TextAttribute.UNDERLINE))
                isUnderline = Objects.equals(attributeMap.get(TextAttribute.UNDERLINE), TextAttribute.UNDERLINE_ON);
        } catch (Exception e) {
            log.debug(SwingTree.get().logMarker(), "Failed to fetch TextAttribute.UNDERLINE in font attributes '{}' of font '{}'", attributeMap, font, e);
        }

        boolean isStriked   = ( _isStrike != null ? _isStrike : false );
        try {
            if (attributeMap.containsKey(TextAttribute.STRIKETHROUGH))
                isStriked   = Objects.equals(attributeMap.get(TextAttribute.STRIKETHROUGH), TextAttribute.STRIKETHROUGH_ON);
        } catch (Exception e) {
            log.debug(SwingTree.get().logMarker(), "Failed to fetch TextAttribute.STRIKETHROUGH in font attributes '{}' of font '{}'", attributeMap, font, e);
        }

        AffineTransform transform = _transform;
        try {
            if (attributeMap.containsKey(TextAttribute.TRANSFORM))
                transform = (AffineTransform) attributeMap.get(TextAttribute.TRANSFORM);
        } catch (Exception e) {
            log.debug(SwingTree.get().logMarker(), "Failed to fetch TextAttribute.TRANSFORM in font attributes '{}' of font '{}'", attributeMap, font, e);
        }

        FontPaintConf paint = _paint;
        try {
            Paint found = null;
            if (attributeMap.containsKey(TextAttribute.FOREGROUND))
                found = (Paint) attributeMap.get(TextAttribute.FOREGROUND);
            if (found != null)
                paint = FontPaintConf.of(null, found, null, null);
        } catch (Exception e) {
            log.warn(SwingTree.get().logMarker(), "Failed to extract font attributes from font: {}", font, e);
        }

        FontPaintConf backgroundPaint = _backgroundPaint;
        try {
            Paint found = null;
            if (attributeMap.containsKey(TextAttribute.BACKGROUND))
                found = (Paint) attributeMap.get(TextAttribute.BACKGROUND);
            if (found != null)
                backgroundPaint = FontPaintConf.of(null, found, null, null);
        } catch (Exception e) {
            log.warn(SwingTree.get().logMarker(), "Failed to extract font attributes from font: {}", font, e);
        }

        Objects.requireNonNull(font);
        return FontConf.of(
                    family,
                    size,
                    posture,
                    weight,
                    spacing,
                    selectionColor,
                    isUnderline,
                    isStriked,
                    transform,
                    paint,
                    backgroundPaint,
                    _horizontalAlignment,
                    _verticalAlignment
                );
    }

    /**
     * Creates a new {@link Font} instance based on this font config,
     * using the default system font as the base font.
     *  <p>
     *  <b>This conversion is full-fidelity where possible:</b> a solid font color is included
     *  as a {@code TextAttribute.FOREGROUND} attribute, so the returned font really renders in
     *  that color. Be aware of what that means in AWT: any color attribute flips
     *  {@link java.awt.Font#hasLayoutAttributes()}, which makes the JDK build a
     *  {@code TextLayout} for <em>every</em> {@code stringWidth} and {@code drawString} of the
     *  font (expensive), and the attribute color overrides whatever color the look-and-feel
     *  chooses (including disabled/selected state colors). If you intend to install the font
     *  on a component, prefer {@link #toAwtFontWithoutColor()} plus
     *  {@code component.setForeground(color)} — or simply let the style engine do exactly that
     *  for you. Gradient/noise font paints depend on component geometry and cannot be included
     *  in a context-free conversion like this.
     *
     * @return A new {@link Font} instance based on this font config.
     */
    public java.awt.Font toAwtFont() {
        java.awt.Font defaultFont = UIManager.getDefaults().getFont("defaultFont");
        if ( defaultFont == null ) {
            Object obj = UIManager.getLookAndFeelDefaults().get("defaultFont");
            if ( obj instanceof java.awt.Font )
                defaultFont = (java.awt.Font) obj;
        }
        if ( defaultFont == null )
            defaultFont = new JLabel().getFont();
        return _createDerivedFrom(defaultFont, null)
                .orElse(defaultFont);
    }

    /**
     *  Converts this font config to an AWT font <b>without any color attributes</b> —
     *  the variant of {@link #toAwtFont()} designed for fonts that will be installed on
     *  components. Here is the quirk this exists for: in AWT's model a {@code Font}
     *  describes glyph geometry only, and text color belongs to the <em>foreground</em>
     *  property (that is why {@code setForeground} exists). Colors <em>can</em> be smuggled
     *  into a font as {@code TextAttribute.FOREGROUND}/{@code BACKGROUND}, but that flips
     *  {@link java.awt.Font#hasLayoutAttributes()}, putting every measure and draw of the
     *  font on the expensive {@code TextLayout} path and silently overriding the
     *  look-and-feel's state colors (disabled text stays colored). So: use this method to
     *  get the geometric font, and apply this config's {@link #color(java.awt.Color) color}
     *  via {@code component.setForeground(..)} — which is precisely what SwingTree's style
     *  engine does when it installs styled fonts.
     *
     * @return A new {@link Font} like {@link #toAwtFont()}, but guaranteed to carry no
     *         color attributes (its {@code hasLayoutAttributes()} is not flipped by color).
     */
    public java.awt.Font toAwtFontWithoutColor() {
        java.awt.Font full = toAwtFont();
        Map<TextAttribute, Object> strippedColors = new HashMap<>();
        strippedColors.put(TextAttribute.FOREGROUND, null);
        strippedColors.put(TextAttribute.BACKGROUND, null);
        return full.deriveFont(strippedColors);
    }

    /**
     *  Derives the font to be installed on a styled component — <b>deliberately without a
     *  solid font color</b>. A plain color placed into a font (as
     *  {@code TextAttribute.FOREGROUND}) flips {@link Font#hasLayoutAttributes()}, which makes
     *  the JDK route every {@code stringWidth} <em>and</em> every {@code drawString} of that
     *  component through a freshly built {@code TextLayout} — measured to be the dominant text
     *  cost of styled UIs — and it also overrides the look-and-feel's state colors (disabled
     *  text would keep the styled color). The solid color is therefore applied through the
     *  component's <em>foreground</em> property by the {@link StyleInstaller} instead (see
     *  {@link #solidColor()}), which every look-and-feel honours natively and cheaply.
     *  Gradient/noise/custom paints cannot travel that channel and stay in the font.
     */
    Optional<Font> createDerivedWithoutSolidColorFrom( Font existingFont, JComponent component ) {
        return _createDerivedFrom(existingFont, component);
    }

    /** The style's font color if (and only if) it is a plain, solid {@link java.awt.Color} —
     *  the case the {@link StyleInstaller} routes through the component foreground channel
     *  instead of the font (see {@link #createDerivedWithoutSolidColorFrom(Font, JComponent)}).
     *  {@code null} for no color or for gradient/noise/custom paints. */
    @Nullable Color solidColor() {
        return _paint.solidColor();
    }

    Optional<Font> createDerivedFrom( Font existingFont, BoxModelConf boxModel ) {
        return _createDerivedFrom(existingFont, boxModel);
    }

    private Optional<Font> _createDerivedFrom( Font existingFont, @Nullable Object boxModelOrComponent )
    {
        if ( this.equals(_NONE) )
            return Optional.empty();

        boolean isChange = false;

        if ( existingFont == null )
            existingFont = new JLabel().getFont();

        Map<TextAttribute, Object> currentAttributes = (Map<TextAttribute, Object>) existingFont.getAttributes();
        Map<TextAttribute, Object> attributes = new HashMap<>();

        if ( _size >= 0 ) {
            isChange = isChange || !Integer.valueOf(_size).equals(currentAttributes.get(TextAttribute.SIZE));
            attributes.put(TextAttribute.SIZE, _size);
        }
        if ( _posture > 0 ) {
            isChange = isChange || !Float.valueOf(_posture).equals(currentAttributes.get(TextAttribute.POSTURE));
            attributes.put(TextAttribute.POSTURE, _posture);
        } else if (_posture == 0) {
            isChange = isChange || !Objects.equals(0f, currentAttributes.get(TextAttribute.POSTURE));
            attributes.put(TextAttribute.POSTURE, null);
        }
        if ( _weight > 0 ) {
            isChange = isChange || !Float.valueOf(_weight).equals(currentAttributes.get(TextAttribute.WEIGHT));
            attributes.put(TextAttribute.WEIGHT, Math.min(_weight, 9)); // Valid if: weight > 0 && weight < 10
        } else if ( _weight == 0 ) {
            isChange = isChange || !Objects.equals(0f, currentAttributes.get(TextAttribute.WEIGHT));
            attributes.put(TextAttribute.WEIGHT, null);
        }
        if ( _spacing != 0 ) {
            isChange = isChange || !Float.valueOf(_spacing).equals(currentAttributes.get(TextAttribute.TRACKING));
            attributes.put(TextAttribute.TRACKING, Math.max(-1,Math.min(_spacing, 10))); // Valid if:  tracking >= -1 && tracking <= 10
        }
        if ( _isUnderlined != null ) {
            // AWT expects the Integer TextAttribute.UNDERLINE_ON here — a raw Boolean is
            // silently DROPPED by Font.deriveFont (a long-standing bug this used to have).
            Object underline = _isUnderlined ? TextAttribute.UNDERLINE_ON : null;
            isChange = isChange || !Objects.equals(underline, currentAttributes.get(TextAttribute.UNDERLINE));
            attributes.put(TextAttribute.UNDERLINE, underline);
        }
        if ( _isStrike != null ) {
            Object strike = _isStrike ? TextAttribute.STRIKETHROUGH_ON : null;
            isChange = isChange || !Objects.equals(strike, currentAttributes.get(TextAttribute.STRIKETHROUGH));
            attributes.put(TextAttribute.STRIKETHROUGH, strike);
        }
        if ( _transform != null ) {
            isChange = isChange || !Objects.equals(_transform, currentAttributes.get(TextAttribute.TRANSFORM));
            attributes.put(TextAttribute.TRANSFORM, _transform);
        }
        if ( !_familyName.isEmpty() ) {
            isChange = isChange || !Objects.equals(_familyName, currentAttributes.get(TextAttribute.FAMILY));
            attributes.put(TextAttribute.FAMILY, _familyName);
        }
        if ( !_paint.equals(FontPaintConf.none()) ) {
            try {
                Paint paint = null;
                if ( boxModelOrComponent instanceof BoxModelConf )
                    paint = _paint.getFor((BoxModelConf) boxModelOrComponent);
                else if ( boxModelOrComponent instanceof JComponent ) {
                    /*
                        Component fonts deliberately do NOT carry a solid color!
                        A plain color renders through the component's *foreground* channel
                        (applied by the StyleInstaller), which every look-and-feel honours
                        natively — see the "Where the font color lives" section of this
                        class' javadoc. Only paints that genuinely need the TextLayout
                        pipeline to render (gradients, noise, custom paints) stay in the
                        font, and only those pay its per-measure/per-draw cost.
                    */
                    if ( _paint.solidColor() == null )
                        paint = _paint.getFor((JComponent) boxModelOrComponent);
                }
                else
                    paint = _paint.solidColor(); // context-free conversion (toAwtFont): only a
                                                 // geometry-independent solid color is resolvable here

                isChange = isChange || !Objects.equals(paint, currentAttributes.get(TextAttribute.FOREGROUND));
                attributes.put(TextAttribute.FOREGROUND, paint);
            } catch ( Exception e ) {
                log.error(SwingTree.get().logMarker(), "Failed to create paint from paint config: "+_paint, e);
            }
        }
        if ( !_backgroundPaint.equals(FontPaintConf.none()) ) {
            try {
                Paint backgroundPaint = null;
                if ( boxModelOrComponent instanceof BoxModelConf )
                    backgroundPaint = _backgroundPaint.getFor((BoxModelConf) boxModelOrComponent);
                else if ( boxModelOrComponent instanceof JComponent )
                    backgroundPaint = _backgroundPaint.getFor((JComponent) boxModelOrComponent);
                else
                    backgroundPaint = _backgroundPaint.solidColor(); // context-free conversion: solid colors only
                // (Unlike the foreground, a text-run BACKGROUND has no component-level channel,
                // so on component fonts it stays an attribute and honestly pays the layout regime.)

                isChange = isChange || !Objects.equals(backgroundPaint, currentAttributes.get(TextAttribute.BACKGROUND));
                attributes.put(TextAttribute.BACKGROUND, backgroundPaint);
            } catch ( Exception e ) {
                log.error(SwingTree.get().logMarker(), "Failed to create paint from paint config: "+_backgroundPaint, e);
            }
        }
        if ( isChange )
            return Optional.of(existingFont.deriveFont(attributes));
        else
            return Optional.empty();
    }

    FontConf _scale(double scale ) {
        if ( scale == 1.0 )
            return this;
        else if ( this.equals(_NONE) )
            return this;
        else
            return FontConf.of(
                    _familyName,
                    (int) Math.round(_size * scale),
                    _posture,
                    _weight,
                    _spacing,
                    _selectionColor, 
                    _isUnderlined,
                    _isStrike,
                    _transform,
                    _paint._scale(scale),
                    _backgroundPaint._scale(scale),
                    _horizontalAlignment,
                    _verticalAlignment
                );
    }

    @Override
    public int hashCode()
    {
        int hash = 7;
        hash = 97 * hash + Objects.hashCode(_familyName);
        hash = 97 * hash + _size;
        hash = 97 * hash + Float.hashCode(_posture);
        hash = 97 * hash + Float.hashCode(_weight);
        hash = 97 * hash + Float.hashCode(_spacing);
        hash = 97 * hash + Objects.hashCode(_selectionColor);
        hash = 97 * hash + Objects.hashCode(_isUnderlined);
        hash = 97 * hash + Objects.hashCode(_isStrike);
        hash = 97 * hash + Objects.hashCode(_transform);
        hash = 97 * hash + Objects.hashCode(_paint);
        hash = 97 * hash + Objects.hashCode(_backgroundPaint);
        hash = 97 * hash + Objects.hashCode(_horizontalAlignment);
        hash = 97 * hash + Objects.hashCode(_verticalAlignment);
        return hash;
    }

    @Override
    public boolean equals( Object obj )
    {
        if ( obj == null )
            return false;
        if ( getClass() != obj.getClass() )
            return false;
        final FontConf other = (FontConf)obj;
        if ( !Objects.equals(_familyName, other._familyName) )
            return false;
        if ( _size != other._size )
            return false;
        if ( _posture != other._posture)
            return false;
        if ( _weight != other._weight )
            return false;
        if ( _spacing != other._spacing )
            return false;
        if ( !Objects.equals(_selectionColor, other._selectionColor) )
            return false;
        if ( !Objects.equals(_isUnderlined, other._isUnderlined) )
            return false;
        if ( !Objects.equals(_isStrike, other._isStrike) )
            return false;
        if ( !Objects.equals(_transform, other._transform) )
            return false;
        if ( !Objects.equals(_paint, other._paint) )
            return false;
        if ( !Objects.equals(_backgroundPaint, other._backgroundPaint) )
            return false;
        if ( _horizontalAlignment != other._horizontalAlignment )
            return false;
        if ( _verticalAlignment != other._verticalAlignment )
            return false;

        return true;
    }

    @Override
    public String toString()
    {
        if ( this.equals(_NONE) )
            return this.getClass().getSimpleName() + "[NONE]";
        String underline       = ( _isUnderlined        == null ? "?" : String.valueOf(_isUnderlined)   );
        String strike          = ( _isStrike            == null ? "?" : String.valueOf(_isStrike)       );
        String transform       = ( _transform           == null ? "?" : _transform.toString()           );
        String horizontalAlign = ( _horizontalAlignment == UI.HorizontalAlignment.UNDEFINED ? "?" : _horizontalAlignment.toString() );
        String verticalAlign   = ( _verticalAlignment   == UI.VerticalAlignment.UNDEFINED ? "?" : _verticalAlignment.toString()   );
        return this.getClass().getSimpleName() + "[" +
                    "family="              + _familyName + ", " +
                    "size="                + (_size < 0 ? "?" : _size)               + ", " +
                    "posture="             + (_posture < 0 ? "?" : _posture)         + ", " +
                    "weight="              + (_weight < 0 ? "?" : _weight)           + ", " +
                    "spacing="             + _spacing                                + ", " +
                    "underlined="          + underline                               + ", " +
                    "strikeThrough="       + strike                                  + ", " +
                    "selectionColor="      + StyleUtil.toString(_selectionColor)     + ", " +
                    "transform="           + transform                               + ", " +
                    "paint="               + _paint                                  + ", " +
                    "backgroundPaint="     + _backgroundPaint                        + ", " +
                    "horizontalAlignment=" + horizontalAlign                         + ", " +
                    "verticalAlignment="   + verticalAlign                           +
                "]";
    }
}