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.
 *
 * @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)
                                                        0,     // 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; }

    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))
                posture = ((Number) attributeMap.get(TextAttribute.POSTURE)).floatValue();
        } catch (Exception e) {
            log.debug(SwingTree.get().logMarker(), "Failed to fetch TextAttribute.POSTURE in font attributes '" + attributeMap + "' of font '" + font + "'", e);
        }

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

        float spacing = _spacing;
        try {
            if (attributeMap.containsKey(TextAttribute.TRACKING))
                spacing = ((Number) attributeMap.get(TextAttribute.TRACKING)).floatValue();
        } catch (Exception e) {
            log.debug(SwingTree.get().logMarker(), "Failed to fetch TextAttribute.TRACKING in font attributes '" + attributeMap + "' of font '" + 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 '" + attributeMap + "' of font '" + 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 '" + attributeMap + "' of font '" + 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 '" + attributeMap + "' of font '" + 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.
     *
     * @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);
    }

    Optional<Font> createDerivedFrom( Font existingFont, JComponent component ) {
        return _createDerivedFrom(existingFont, component);
    }

    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 ) {
            isChange = isChange || !Objects.equals(_isUnderlined, currentAttributes.get(TextAttribute.UNDERLINE));
            attributes.put(TextAttribute.UNDERLINE, _isUnderlined);
        }
        if ( _isStrike != null ) {
            isChange = isChange || !Objects.equals(_isStrike, currentAttributes.get(TextAttribute.STRIKETHROUGH));
            attributes.put(TextAttribute.STRIKETHROUGH, _isStrike);
        }
        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 )
                    paint = _paint.getFor((JComponent) boxModelOrComponent);

                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);

                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,
                    _backgroundPaint,
                    _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(_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(_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                                   + ", " +
                    "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                           +
                "]";
    }
}