StyleConf.java

package swingtree.style;

import com.google.errorprone.annotations.Immutable;
import swingtree.UI;
import swingtree.api.Configurator;
import swingtree.api.Painter;

import java.awt.*;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;

/**
 *  An immutable config container with cloning based update methods designed
 *  for functional {@link javax.swing.JComponent} styling.
 *  The styling in SwingTree is completely functional, meaning that changing a property
 *  of a {@link StyleConf} instance will always return a new {@link StyleConf} instance with the
 *  updated property.
 *  <p>
 *  Consider the following example demonstrating how a {@link javax.swing.JPanel} is styled through the SwingTree
 *  style API, which consists of a functional {@link swingtree.api.Styler} lambda that processes a
 *  {@link ComponentStyleDelegate} instance that internally assembles a {@link StyleConf} object:
 *  <pre>{@code
 *  panel(FILL)
 *  .withStyle( it -> it
 *      .foundationColor(new Color(26,191,230))
 *      .backgroundColor(new Color(255,255,255))
 *      .paddingTop(30)
 *      .paddingLeft(35)
 *      .paddingRight(35)
 *      .paddingBottom(30)
 *      .borderRadius(25, 25)
 *      .borderWidth(3)
 *      .borderColor(new Color(0,102,255))
 *      .shadowColor(new Color(64,64,64))
 *      .shadowBlurRadius(6)
 *      .shadowSpreadRadius(5)
 *      .shadowInset(false)
 *  )
 *  }</pre>
 *
 *  This design is inspired by the CSS styling language and the use of immutable objects
 *  is a key feature of the SwingTree API which makes it possible to safely compose
 *  {@link swingtree.api.Styler} lambdas into a complex style pipeline
 *  without having to worry about side effects.
 *  See {@link swingtree.style.StyleSheet} for more information about
 *  how this composition of styles is achieved in practice.
 */
@Immutable
@SuppressWarnings("ReferenceEquality")
public final class StyleConf
{
    private static final StyleConf _NONE = new StyleConf(
                                            LayoutConf.none(),
                                            BorderConf.none(),
                                            BaseConf.none(),
                                            FontConf.none(),
                                            DimensionalityConf.none(),
                                            StyleConfLayers.empty(),
                                            NamedConfigs.empty()
                                        );

    /**
     *  Exposes the "null object" pattern for {@link StyleConf} instances.
     *  So the constant returned by this method is the default instance
     *  that represents the absence of a style.
     *
     * @return The default style instance, representing the absence of a style.
     */
    public static StyleConf none() { return _NONE; }

    static StyleConf of(
        LayoutConf layout,
        BorderConf border,
        BaseConf base,
        FontConf font,
        DimensionalityConf dimensionality,
        StyleConfLayers layers,
        NamedConfigs<String> properties
    ) {
        if (
            layout         == _NONE._layout &&
            border         == _NONE._border &&
            base           == _NONE._base &&
            font           == _NONE._font &&
            dimensionality == _NONE._dimensionality &&
            layers         == _NONE._layers &&
            properties     == _NONE._properties
        )
            return _NONE;
        else
            return new StyleConf(layout, border, base, font, dimensionality, layers, properties);
    }


    private final LayoutConf           _layout;
    private final BorderConf           _border;
    private final BaseConf             _base;
    private final FontConf             _font;
    private final DimensionalityConf   _dimensionality;
    private final StyleConfLayers      _layers;
    private final NamedConfigs<String> _properties;


    private StyleConf(
        LayoutConf           layout,
        BorderConf           border,
        BaseConf             base,
        FontConf             font,
        DimensionalityConf   dimensionality,
        StyleConfLayers      layers,
        NamedConfigs<String> properties
    ) {
        _layout         = Objects.requireNonNull(layout);
        _border         = Objects.requireNonNull(border);
        _base           = Objects.requireNonNull(base);
        _font           = Objects.requireNonNull(font);
        _dimensionality = Objects.requireNonNull(dimensionality);
        _layers         = Objects.requireNonNull(layers);
        _properties     = Objects.requireNonNull(properties);
    }

    public Optional<Object> layoutConstraint() { return _layout.constraint(); }

    public LayoutConf layout() { return _layout; }

    Outline padding() { return _border.padding(); }

    Outline margin() { return _border.margin(); }

    BorderConf border() { return _border; }

    BaseConf base() { return _base; }

    DimensionalityConf dimensionality() { return _dimensionality; }

    StyleConfLayers layers() { return _layers; }

    StyleConfLayer layer(UI.Layer layer ) { return _layers.get(layer); }

    /**
     *  Exposes the default shadow style configuration object.
     * @return The default shadow style.
     */
    public ShadowConf shadow() {
        ShadowConf found =
                        _layers.get(ShadowConf.DEFAULT_LAYER)
                        .shadows()
                        .get(StyleUtil.DEFAULT_KEY);

        return found != null ? found : ShadowConf.none();
    }

    /**
     *  Internally, a style configuration consists of a set of layers defined by the {@link UI.Layer} enum.
     *  Using this method you can retrieve all shadow styles for a particular layer
     *  and with the provided name.
     *
     * @param layer The layer to retrieve the shadow style from.
     * @param shadowName The name of the shadow style to retrieve.
     * @return The shadow style with the provided name.
     */
    public ShadowConf shadow( UI.Layer layer, String shadowName ) {
        Objects.requireNonNull(shadowName);
        StyleConfLayer layerConf = _layers.get(layer);
        NamedConfigs<ShadowConf> shadows = layerConf.shadows();
        ShadowConf found = shadows.get(shadowName);
        return found != null ? found : ShadowConf.none();
    }

    /**
     *  Internally, a style configuration consists of a set of layers defined by the {@link UI.Layer} enum.
     *  You can retrieve all shadow styles for a specific layer by calling this method.
     *
     * @return An unmodifiable list of all shadow styles sorted by their names in ascending alphabetical order.
     */
    List<ShadowConf> shadows( UI.Layer layer ) {
        return Collections.unmodifiableList(
                        _layers.get(layer)
                        .shadows()
                        .namedStyles()
                        .stream()
                        .sorted(Comparator.comparing(NamedConf::name))
                        .map(NamedConf::style)
                        .collect(Collectors.toList())
                    );
    }

    NamedConfigs<ShadowConf> shadowsMap(UI.Layer layer) {
        return _layers.get(layer).shadows();
    }

    boolean hasVisibleShadows(UI.Layer layer) {
        return _layers.get(layer)
                .shadows()
                .stylesStream()
                .anyMatch(s -> s.color().isPresent() && s.color().get().getAlpha() > 0 );
    }

    public FontConf font() { return _font; }

    /**
     *  Returns a new {@link StyleConf} instance with the given layout constraint.
     * @return An unmodifiable list of painters sorted by their names in ascending alphabetical order.
     */
    List<PainterConf> painters( UI.Layer layer ) {
        return Collections.unmodifiableList(
                            new ArrayList<>(_layers.get(layer)
                                .painters()
                                .sortedByNames()
                            )
                        );
    }

    StyleConf painter(UI.Layer layer, UI.ComponentArea area, String painterName, Painter painter ) {
        Objects.requireNonNull(painterName);
        Objects.requireNonNull(painter);
        // We clone the painter map:
        NamedConfigs<PainterConf> newPainters = _layers.get(layer)
                                                    .painters()
                                                    .withNamedStyle(
                                                        painterName, // Existing painters are overwritten if they have the same name.
                                                        PainterConf.of(painter, area)
                                                    ); 
        return _withPainters(layer, newPainters);
    }

    boolean hasPaintersOnLayer(UI.Layer layer ) {
        return _layers.get(layer).painters().stylesStream().anyMatch(p -> !Painter.none().equals(p.painter()));
    }

    boolean hasImagesOnLayer(UI.Layer layer ) {
        return _layers.get(layer).images().stylesStream().anyMatch(i -> i.image().isPresent() || i.primer().isPresent());
    }

    List<GradientConf> gradients( UI.Layer layer ) {
        return _layers.get(layer).gradients().sortedByNames();
    }

    boolean hasCustomGradients() {
        boolean hasCustomGradients = false;
        for ( UI.Layer layer : UI.Layer.values() ) {
            if ( hasCustomGradients(layer) ) {
                hasCustomGradients = true;
                break;
            }
        }
        return hasCustomGradients;
    }

    boolean hasCustomGradients( UI.Layer layer ) {
        NamedConfigs<GradientConf> gradients = _layers.get(layer).gradients();
        return !( gradients.size() == 1 && GradientConf.none().equals(gradients.get(StyleUtil.DEFAULT_KEY)) );
    }

    boolean hasVisibleGradientsOnLayer( UI.Layer layer ) {
        List<GradientConf> gradients = gradients(layer);
        if ( gradients.isEmpty() ) return false;
        return gradients.stream().anyMatch( s -> s.colors().length > 0 );
    }

    List<NoiseConf> noises( UI.Layer layer ) {
        return _layers.get(layer).noises().sortedByNames();
    }

    boolean hasVisibleNoisesOnLayer( UI.Layer layer ) {
        List<NoiseConf> noises = noises(layer);
        if ( noises.isEmpty() ) return false;
        return noises.stream().anyMatch( s -> !s.equals(NoiseConf.none()) );
    }

    List<UI.ComponentArea> gradientCoveredAreas() {
        return gradientCoveredAreas(UI.Layer.values());
    }

    List<UI.ComponentArea> gradientCoveredAreas( UI.Layer... layers ) {
        return Arrays.stream(layers)
                .map(_layers::get)
                .map(StyleConfLayer::gradients)
                .flatMap( g -> g
                    .stylesStream()
                    .map( grad -> grad.isOpaque() ? grad.area() : null )
                    .filter(Objects::nonNull)
                )
                .distinct()
                .collect(Collectors.toList());
    }

    List<UI.ComponentArea> noiseCoveredAreas() {
        return noiseCoveredAreas(UI.Layer.values());
    }

    List<UI.ComponentArea> noiseCoveredAreas( UI.Layer... layers ) {
        return Arrays.stream(layers)
                .map(_layers::get)
                .map(StyleConfLayer::noises)
                .flatMap( n -> n
                    .stylesStream()
                    .map( noise -> noise.isOpaque() ? noise.area() : null )
                    .filter(Objects::nonNull)
                )
                .distinct()
                .collect(Collectors.toList());
    }

    List<UI.ComponentArea> noiseAndGradientCoveredAreas() {
        List<UI.ComponentArea> areas = new ArrayList<>(gradientCoveredAreas());
        areas.addAll(noiseCoveredAreas());
        return areas;
    }

    public StyleConf foundationColor( Color color ) { return _withBase(base().foundationColor(color)); }

    public StyleConf backgroundColor( Color color ) { return _withBase(base().backgroundColor(color)); }

    StyleConf _withLayout( LayoutConf layout ) {
        if ( layout == _layout )
            return this;

        return StyleConf.of(layout, _border, _base, _font, _dimensionality, _layers, _properties);
    }

    StyleConf _withBorder( BorderConf border ) {
        if ( border == _border )
            return this;

        return StyleConf.of(_layout, border, _base, _font, _dimensionality, _layers, _properties);
    }

    StyleConf _withBase( BaseConf background ) {
        if ( background == _base )
            return this;

        return StyleConf.of(_layout, _border, background, _font, _dimensionality, _layers, _properties);
    }

    StyleConf _withFont( FontConf font ) {
        if ( font == _font )
            return this;

        return StyleConf.of(_layout, _border, _base, font, _dimensionality, _layers, _properties);
    }

    StyleConf _withDimensionality( DimensionalityConf dimensionality ) {
        if ( dimensionality == _dimensionality )
            return this;

        return StyleConf.of(_layout, _border, _base, _font, dimensionality, _layers, _properties);
    }

    StyleConf _withShadow( UI.Layer layer, NamedConfigs<ShadowConf> shadows ) {
        return StyleConf.of(_layout, _border, _base, _font, _dimensionality, _layers.with(layer, _layers.get(layer).withShadows(shadows)), _properties);
    }

    StyleConf _withProperties( NamedConfigs<String> properties ) {
        if ( properties == _properties )
            return this;

        return StyleConf.of(_layout, _border, _base, _font, _dimensionality, _layers, properties);
    }

    StyleConf _withShadow( UI.Layer layer, Configurator<ShadowConf> styler ) {
        // A new map is created where all the styler is applied to all the values:
        NamedConfigs<ShadowConf> styledShadows = _layers.get(layer).shadows().mapStyles(styler);
        return _withShadow(layer, styledShadows);
    }

    StyleConf _withShadow( Configurator<ShadowConf> styler ) {
        return _withLayers(_layers.map( layer -> layer.withShadows(layer.shadows().mapStyles(styler)) ));
    }

    StyleConf _withGradients( UI.Layer layer, NamedConfigs<GradientConf> shades ) {
        Objects.requireNonNull(shades);
        return StyleConf.of(_layout, _border, _base, _font, _dimensionality, _layers.with(layer, _layers.get(layer).withGradients(shades)), _properties);
    }

    StyleConf _withNoises( UI.Layer layer, NamedConfigs<NoiseConf> noises ) {
        Objects.requireNonNull(noises);
        return StyleConf.of(_layout, _border, _base, _font, _dimensionality, _layers.with(layer, _layers.get(layer).withNoises(noises)), _properties);
    }

    StyleConf _withImages( UI.Layer layer, NamedConfigs<ImageConf> images ) {
        return StyleConf.of(_layout, _border, _base, _font, _dimensionality, _layers.with(layer, _layers.get(layer).withImages(images)), _properties);
    }

    StyleConf _withTexts( UI.Layer layer, NamedConfigs<TextConf> texts ) {
        return StyleConf.of(_layout, _border, _base, _font, _dimensionality, _layers.with(layer, _layers.get(layer).withTexts(texts)), _properties);
    }

    StyleConf _withLayers( StyleConfLayers layers ) {
        if ( layers == _layers )
            return this;

        return StyleConf.of(_layout, _border, _base, _font, _dimensionality, layers, _properties);
    }

    StyleConf _withPainters( UI.Layer layer, NamedConfigs<PainterConf> painters ) {
        Objects.requireNonNull(painters);
        return StyleConf.of(_layout, _border, _base, _font, _dimensionality, _layers.with(layer, _layers.get(layer).withPainters(painters)), _properties);
    }

    StyleConf property( String key, String value ) {
        Objects.requireNonNull(key);
        Objects.requireNonNull(value);
        return _withProperties(_properties.withNamedStyle(key, value));
    }

    List<NamedConf<String>> properties() {
        return _properties.namedStyles()
                            .stream()
                            .sorted(Comparator.comparing(NamedConf::name))
                            .collect(Collectors.toList());
    }

    StyleConf gradient( UI.Layer layer, String shadeName, Configurator<GradientConf> styler ) {
        Objects.requireNonNull(shadeName);
        Objects.requireNonNull(styler);
        GradientConf gradConf = _layers.get(layer).gradients().find(shadeName).orElse(GradientConf.none());
        // We clone the shadow map:
        NamedConfigs<GradientConf> newShadows = _layers.get(layer).gradients().withNamedStyle(shadeName, styler.configure(gradConf));
        return _withGradients(layer, newShadows);
    }

    GradientConf gradient( UI.Layer layer, String gradName ) {
        Objects.requireNonNull(gradName);
        StyleConfLayer layerConf = _layers.get(layer);
        NamedConfigs<GradientConf> gradients = layerConf.gradients();
        GradientConf found = gradients.get(gradName);
        return found != null ? found : GradientConf.none();
    }

    StyleConf noise( UI.Layer layer, String noiseName, Configurator<NoiseConf> styler ) {
        Objects.requireNonNull(noiseName);
        Objects.requireNonNull(styler);
        NoiseConf noise = _layers.get(layer).noises().find(noiseName).orElse(NoiseConf.none());
        // We clone the noise map:
        NamedConfigs<NoiseConf> newNoises = _layers.get(layer).noises().withNamedStyle(noiseName, styler.configure(noise));
        return _withNoises(layer, newNoises);
    }

    StyleConf images(UI.Layer layer, String imageName, Configurator<ImageConf> styler ) {
        Objects.requireNonNull(imageName);
        Objects.requireNonNull(styler);
        ImageConf ground = _layers.get(layer).images().find(imageName).orElse(ImageConf.none());
        // We clone the ground map:
        NamedConfigs<ImageConf> newImages = _layers.get(layer).images().withNamedStyle(imageName, styler.configure(ground));
        return _withImages( layer, newImages );
    }

    List<ImageConf> images( UI.Layer layer ) {
        return _layers.get(layer).images().sortedByNames();
    }

    StyleConf text(UI.Layer layer, String textName, Configurator<TextConf> styler ) {
        Objects.requireNonNull(textName);
        Objects.requireNonNull(styler);
        TextConf text = _layers.get(layer).texts().find(textName).orElse(TextConf.none());
        // We clone the text map:
        NamedConfigs<TextConf> newTexts = _layers.get(layer).texts().withNamedStyle(textName, styler.configure(text));
        return _withTexts( layer, newTexts );
    }

    StyleConf text( Configurator<TextConf> styler ) {
        return _withLayers(_layers.map( layer -> layer.withTexts(layer.texts().mapStyles(styler)) ));
    }

    List<TextConf> texts( UI.Layer layer ) {
        return _layers.get(layer).texts().sortedByNames();
    }

    StyleConf scale( double scale ) {
        return StyleConf.of(
                    _layout,
                    _border._scale(scale),
                    _base, // Just colors and the cursor
                    _font._scale(scale),
                    _dimensionality._scale(scale),
                    _layers._scale(scale),
                    _properties
                );
    }

    StyleConf simplified() {
        return _withBase(_base.simplified())
               ._withBorder(_border.simplified())
               ._withDimensionality(_dimensionality.simplified())
               ._withLayers(_layers.simplified());
    }

    StyleConf correctedForRounding() {
        return _withBorder(_border.correctedForRounding());
    }

    boolean hasEqualLayoutAs( StyleConf otherStyle ) {
        return Objects.equals(_layout, otherStyle._layout);
    }

    boolean hasEqualMarginAndPaddingAs( StyleConf otherStyle ) {
        return Objects.equals(_border.margin(), otherStyle._border.margin()) &&
               Objects.equals(_border.padding(), otherStyle._border.padding());
    }

    boolean hasEqualBorderAs( StyleConf otherStyle ) {
        return Objects.equals(_border, otherStyle._border);
    }

    boolean hasEqualBaseAs( StyleConf otherStyle ) {
        return Objects.equals(_base, otherStyle._base);
    }

    boolean hasEqualFontAs( StyleConf otherStyle ) {
        return Objects.equals(_font, otherStyle._font);
    }

    boolean hasEqualDimensionalityAs( StyleConf otherStyle ) {
        return Objects.equals(_dimensionality, otherStyle._dimensionality);
    }

    boolean hasEqualFilterAs( StyleConf otherStyle ) {
        return _layers.filter().equals(otherStyle._layers.filter());
    }

    boolean hasEqualShadowsAs( StyleConf otherStyle ) {
        boolean allLayersAreEqual = true;
        for ( UI.Layer layer : UI.Layer.values() ) {
            if ( !hasEqualShadowsAs(layer, otherStyle) ) {
                allLayersAreEqual = false;
                break;
            }
        }
        return allLayersAreEqual;
    }

    boolean hasEqualShadowsAs( UI.Layer layer, StyleConf otherStyle ) {
        StyleConfLayer thisLayer = _layers.get(layer);
        StyleConfLayer otherLayer = otherStyle._layers.get(layer);
        if ( thisLayer == null && otherLayer == null )
            return true;
        if ( thisLayer == null || otherLayer == null )
            return false;
        return thisLayer.hasEqualShadowsAs(otherLayer);
    }

    boolean hasEqualPaintersAs( StyleConf otherStyle ) {
        boolean allLayersAreEqual = true;
        for ( UI.Layer layer : UI.Layer.values() ) {
            if ( !hasEqualPaintersAs(layer, otherStyle) ) {
                allLayersAreEqual = false;
                break;
            }
        }
        return allLayersAreEqual;
    }

    boolean hasEqualPaintersAs( UI.Layer layer, StyleConf otherStyle ) {
        StyleConfLayer thisLayer = _layers.get(layer);
        StyleConfLayer otherLayer = otherStyle._layers.get(layer);
        if ( thisLayer == null && otherLayer == null )
            return true;
        if ( thisLayer == null || otherLayer == null )
            return false;
        return thisLayer.hasEqualPaintersAs(otherLayer);
    }

    boolean hasEqualGradientsAs( StyleConf otherStyle ) {
        boolean allLayersAreEqual = true;
        for ( UI.Layer layer : UI.Layer.values() ) {
            if ( !hasEqualGradientsAs(layer, otherStyle) ) {
                allLayersAreEqual = false;
                break;
            }
        }
        return allLayersAreEqual;
    }

    boolean hasEqualGradientsAs( UI.Layer layer, StyleConf otherStyle ) {
        StyleConfLayer thisLayer = _layers.get(layer);
        StyleConfLayer otherLayer = otherStyle._layers.get(layer);
        if ( thisLayer == null && otherLayer == null )
            return true;
        if ( thisLayer == null || otherLayer == null )
            return false;
        return thisLayer.hasEqualGradientsAs(otherLayer);
    }

    boolean hasEqualNoisesAs( StyleConf otherStyle ) {
        boolean allLayersAreEqual = true;
        for ( UI.Layer layer : UI.Layer.values() ) {
            if ( !hasEqualNoisesAs(layer, otherStyle) ) {
                allLayersAreEqual = false;
                break;
            }
        }
        return allLayersAreEqual;
    }

    boolean hasEqualNoisesAs( UI.Layer layer, StyleConf otherStyle ) {
        StyleConfLayer thisLayer = _layers.get(layer);
        StyleConfLayer otherLayer = otherStyle._layers.get(layer);
        if ( thisLayer == null && otherLayer == null )
            return true;
        if ( thisLayer == null || otherLayer == null )
            return false;
        return thisLayer.hasEqualNoisesAs(otherLayer);
    }

    boolean hasEqualImagesAs(StyleConf otherStyle ) {
        boolean allLayersAreEqual = true;
        for ( UI.Layer layer : UI.Layer.values() ) {
            if ( !hasEqualImagesAs(layer, otherStyle) ) {
                allLayersAreEqual = false;
                break;
            }
        }
        return allLayersAreEqual;
    }

    boolean hasEqualImagesAs( UI.Layer layer, StyleConf otherStyle ) {
        StyleConfLayer thisLayer = _layers.get(layer);
        StyleConfLayer otherLayer = otherStyle._layers.get(layer);
        if ( thisLayer == null && otherLayer == null )
            return true;
        if ( thisLayer == null || otherLayer == null )
            return false;
        return thisLayer.hasEqualImagesAs(otherLayer);
    }

    boolean hasEqualTextsAs( StyleConf otherStyle ) {
        boolean allLayersAreEqual = true;
        for ( UI.Layer layer : UI.Layer.values() ) {
            if ( !hasEqualTextsAs(layer, otherStyle) ) {
                allLayersAreEqual = false;
                break;
            }
        }
        return allLayersAreEqual;
    }

    boolean hasEqualTextsAs( UI.Layer layer, StyleConf otherStyle ) {
        StyleConfLayer thisLayer = _layers.get(layer);
        StyleConfLayer otherLayer = otherStyle._layers.get(layer);
        if ( thisLayer == null && otherLayer == null )
            return true;
        if ( thisLayer == null || otherLayer == null )
            return false;
        return thisLayer.hasEqualTextsAs(otherLayer);
    }

    boolean hasEqualPropertiesAs( StyleConf otherStyle ) {
        return Objects.equals(_properties, otherStyle._properties);
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                    _layout, _border, _base, _font, _dimensionality, _layers, _properties
                );
    }

    @Override
    public boolean equals( Object obj ) {
        if ( obj == this ) return true;
        if ( obj == null ) return false;
        if ( !(obj instanceof StyleConf) ) return false;
        StyleConf other = (StyleConf) obj;
        return hasEqualLayoutAs(other)         &&
               hasEqualBorderAs(other)         &&
               hasEqualBaseAs(other)           &&
               hasEqualFontAs(other)           &&
               hasEqualDimensionalityAs(other) &&
               hasEqualFilterAs(other)         &&
               hasEqualShadowsAs(other)        &&
               hasEqualPaintersAs(other)       &&
               hasEqualGradientsAs(other)      &&
               hasEqualNoisesAs(other)         &&
               hasEqualImagesAs(other)         &&
               hasEqualTextsAs(other)          &&
               hasEqualPropertiesAs(other);
    }

    @Override
    public String toString() {
        String propertiesString = _properties.toString(StyleUtil.DEFAULT_KEY, "properties");

        return this.getClass().getSimpleName() + "[" +
                    _layout          + ", " +
                    _border          + ", " +
                    _base            + ", " +
                    _font            + ", " +
                    _dimensionality  + ", " +
                    _layers          + ", " +
                    propertiesString +
                "]";
    }

}