IconDeclaration.java

package swingtree.api;

import com.google.errorprone.annotations.Immutable;
import swingtree.UI;
import swingtree.layout.Size;
import swingtree.style.SvgIcon;

import javax.swing.*;
import java.util.Objects;
import java.util.Optional;

/**
 *  Primarily designed to be implemented by an {@link Enum} type
 *  that declares a set of icon paths so that the enum instances
 *  can be used to identify and load
 *  (cached) icons across your application.
 *  <p>
 *  Here an example of how to use this interface:
 *  <pre>{@code
 * public enum Icons implements IconDeclaration
 * {
 *     ADD("icons/add.png"),
 *     REMOVE("icons/remove.png"),
 *     EDIT("icons/edit.png"),
 *     SAVE("icons/save.png"),
 *     CANCEL("icons/cancel.png"),
 *     REFRESH("icons/refresh.png");
 *     // ...
 *
 *     private final String path;
 *
 *     Icons(String path) { this.path = path; }
 *
 *     {@literal @}Override public String source() {
 *         return path;
 *     }
 * }
 * }</pre>
 *
 *  You may then use the enum instances
 *  in the SwingTree API just like you would use the {@link ImageIcon} class:
 *  <pre>{@code
 *  UI.button(Icons.ADD)
 *  .onClick( it -> vm.add() )
 *  }</pre>
 *
 *  The reason why enums should be used instead of Strings is
 *  so that you have some more compile time safety in your application!
 *  When it comes to resource loading Strings are brittle because they
 *  are susceptible to typos and refactoring mistakes.
 *  <p>
 *  Instances of this class are intended to be used as part of a view model
 *  instead of using the {@link Icon} or {@link ImageIcon} classes directly.
 *  <p>
 *  The reason for this is the fact that traditional Swing icons
 *  are often heavyweight objects whose loading may or may not succeed, and so they are
 *  not suitable for direct use in a property as part of your view-model.
 *  Instead, you should use this {@link IconDeclaration} interface, which is a
 *  lightweight value object that merely models the resource location of the icon
 *  even if it is not yet loaded or even does not exist at all.
 *  <p>
 *  Not only does this make your view model more robust, but it also allows you
 *  to write unit tests much more easily. You can create icon declarations
 *  even if the targeted icon may not be available at all, yet you can still test the
 *  behavior of your view-model.
 *  <p>
 *  <strong>SVG Support:</strong>
 *  In addition to traditional image files, {@code IconDeclaration} supports
 *  programmatically defined SVG icons through the {@link #ofAutoScaledSvg(String)} factory method.
 *  This enables creation of resolution-independent vector icons directly in code:
 *  <pre>{@code
 *  // Create a custom SVG icon declaration
 *  IconDeclaration playButton = IconDeclaration.ofSvg(
 *      "<svg width='24' height='24' viewBox='0 0 24 24'>" +
 *      "  <path d='M8 5v14l11-7z' fill='currentColor'/>" +
 *      "</svg>"
 *  );
 *
 *  // Use it in UI declarations
 *  UI.button(playButton)
 *    .onClick(it -> mediaPlayer.play())
 *  }</pre>
 *
 *  SVG icons automatically scale to match DPI settings and maintain crisp edges
 *  at any resolution. They can be sized dynamically or set to a fixed size
 *  using {@link #withSize(Size)}.
 */
@Immutable
public interface IconDeclaration
{
    /**
     * Defines the format of the source string returned by {@link #source()}.
     * This enum determines how the source string should be interpreted
     * when loading or creating an icon.
     * <p>
     * For example, a source string could be a file path pointing to a
     * PNG or JPEG image file, or it could be a complete SVG document
     * in XML text form.
     */
    enum SourceFormat {
        /**
         * The source string is a path to an icon file.
         * This path can be either:
         * <ul>
         *     <li>A relative path resolved against the classpath</li>
         *     <li>An absolute file system path</li>
         * </ul>
         * The file format can be any image format supported by Java's
         * {@link ImageIcon} class (PNG, JPEG, etc.).
         */
        PATH_TO_ICON,

        /**
         * The source string is a complete SVG document in XML text form.
         * This allows for programmatic creation of vector icons without
         * needing external files.
         * <p>
         * Example:
         * <pre>{@code
         * String svg = "<svg width='16' height='16'><circle cx='8' cy='8' r='6' fill='red'/></svg>";
         * IconDeclaration.ofSvg(svg);
         * }</pre>
         */
        SVG_STRING
    }

    /**
     *  This method supplies a String which is a "source" for producing {@link ImageIcon},
     *  <b>this is typically a path to a file</b>, but may also be a full-blown SVG document in text form.<br>
     *  The exact meaning of the source String is defined by the {@link SourceFormat}
     *  returned by {@link #sourceFormat()} method. Together, these two properties form
     *  the most important part an icon declaration, since they constitute the minimum
     *  amount of information needed to resolve an actual icon...<br>
     *  Note that in case of the source String being a {@link SourceFormat#PATH_TO_ICON},
     *  it may either be a path relative to the classpath or may be an absolute path.
     *
     * @return The "source String" which is used together with the {@link #sourceFormat()}
     *          to resolve an icon. It is typically a path to the icon resource,
     *          which is relative to the classpath or may be an absolute path.
     */
    String source();

    /**
     * Returns the format of the source string returned by {@link #source()}.
     * This determines how the source string should be interpreted when
     * loading or creating the icon.
     * <p>
     * By default, this method returns {@link SourceFormat#PATH_TO_ICON},
     * meaning the source string is treated as a file path. Implementations
     * can override this method to return {@link SourceFormat#SVG_STRING}
     * if they provide SVG content directly.
     *
     * @return The format of the source string, never {@code null}.
     */
    default SourceFormat sourceFormat() {
        return SourceFormat.PATH_TO_ICON;
    }

    /**
     *  The preferred size of the icon, which is not necessarily the actual size
     *  of the icon that is being loaded but rather the size that the icon should
     *  be scaled to when it is being loaded.<br>
     *  If this method returns {@link Optional#empty()}, then the loaded icon will have
     *  the exact same size found at the image file found at {@link #source()}.
     *  A non-empty optional with a {@link Size#unknown()}, specifically indicates to
     *  {@link swingtree.style.SvgIcon}s that their size should be context dependent,
     *  meaning that their {@link SvgIcon#getIconWidth()} and {@link SvgIcon#getIconHeight()}
     *  are both returning {@code -1} to indicate flexible scaling.<br>
     *  In case of a regular png or jpeg on the other hand, a {@link Size#unknown()} will
     *  also cause the resulting {@link ImageIcon} to have the same dimensions found in the file.
     *
     * @return The preferred size of the icon or {@link Optional#empty()} if the size is unspecified,
     *          which means that the icon should be loaded in its original size.
     *          A non-empty optional with a {@link Size#unknown()}, specifically indicates to
     *          {@link swingtree.style.SvgIcon}s that their size should be context dependent.
     */
    default Optional<Size> size() {
        return Optional.of(Size.unknown());
    }

    /**
     *  This method is used to find the icon resource
     *  and load it as an {@link ImageIcon} instance
     *  wrapped in an {@link Optional},
     *  or return an empty {@link Optional} if the icon resource
     *  could not be found.
     *
     * @return An {@link Optional} that contains the {@link ImageIcon}
     *         if the icon resource was found, otherwise an empty {@link Optional}.
     */
    default Optional<ImageIcon> find() {
        return UI.findIcon(this);
    }

    /**
     *  Creates and returns an updated {@link IconDeclaration} instance
     *  with a new preferred size for the icon.
     *
     * @param size The preferred size of the icon in the form of a {@link Size} instance.
     * @return A new {@link IconDeclaration} instance with the same path
     *        but with the given size.
     */
    default IconDeclaration withSize( Size size ) {
        return IconDeclaration.of(size, sourceFormat(), source());
    }

    /**
     *  Creates and returns an updated {@link IconDeclaration} instance
     *  with a new preferred width and height for the icon.
     *
     * @param width The preferred width of the icon.
     * @param height The preferred height of the icon.
     * @return A new {@link IconDeclaration} instance with the same path
     *        but with the specified width and height as preferred size.
     */
    default IconDeclaration withSize( int width, int height ) {
        return IconDeclaration.of(Size.of(width, height), sourceFormat(), source());
    }

    /**
     *  Creates and returns an updated {@link IconDeclaration} instance
     *  with a new preferred width for the icon.
     *
     * @param width The preferred width of the icon.
     * @return A new {@link IconDeclaration} instance with the same path
     *        but with the specified width as preferred width.
     */
    default IconDeclaration withWidth( int width ) {
        return IconDeclaration.of(
                size().map(it->it.withWidth(width)).orElse(Size.of(width, -1)),
                sourceFormat(),
                source()
            );
    }

    /**
     *  Allows you to create an updated {@link IconDeclaration} instance
     *  with a new preferred height for the icon.
     *
     * @param height The preferred height of the icon.
     * @return A new {@link IconDeclaration} instance with the same path
     *        but with the specified height as preferred height.
     */
    default IconDeclaration withHeight( int height ) {
        return IconDeclaration.of(
                size().map(it->it.withHeight(height)).orElse(Size.of(-1, height)),
                sourceFormat(),
                source()
            );
    }

    /**
     *  This method is used to find an {@link ImageIcon} of a specific type
     *  and load and return it wrapped in an {@link Optional},
     *  or return an empty {@link Optional} if the icon resource could not be found.
     *
     * @param type The type of icon to find.
     * @return An {@link Optional} that contains the {@link ImageIcon} of the given type.
     * @param <T> The type of icon to find.
     */
    default <T extends ImageIcon> Optional<T> find( Class<T> type ) {
        return UI.findIcon(this).map(type::cast);
    }

    /**
     *  A factory method for creating an {@link IconDeclaration} instance
     *  from the provided path to the icon resource.
     *
     * @param path The path to the icon resource, which may be relative
     *             to the classpath or may be an absolute path.
     * @return A new {@link IconDeclaration} instance
     *        that represents the given icon resource as a constant.
     */
    static IconDeclaration of( String path ) {
        Objects.requireNonNull(path);
        return IconDeclaration.of(Size.unknown(), SourceFormat.PATH_TO_ICON, path);
    }

    /**
     *  A factory method for creating an {@link IconDeclaration} instance
     *  from the provided path to the icon resource and the preferred size.
     *
     * @param size The preferred size of the icon.
     * @param path The path to the icon resource, which may be relative
     *             to the classpath or may be an absolute path.
     * @return A new {@link IconDeclaration} instance
     *        that represents the given icon resource as a constant.
     */
    static IconDeclaration of( Size size, String path ) {
        Objects.requireNonNull(size);
        Objects.requireNonNull(path);
        return IconDeclaration.of(size, SourceFormat.PATH_TO_ICON, path);
    }

    /**
     * Creates an {@link IconDeclaration} instance from an SVG document string,
     * <b>which will be resolved to an {@link SvgIcon} with an unknown size,
     * effectively making it scale depending on its usage context.</b>
     * This factory method is specifically designed for creating vector-based
     * icons programmatically without requiring external files.
     * <p>
     * The provided SVG string must be a complete, well-formed SVG XML document.
     * The resulting icon declaration will have its {@link #sourceFormat()}
     * set to {@link SourceFormat#SVG_STRING}.
     * <p>
     * Icon declarations produced by this method will resolve to
     * {@link swingtree.style.SvgIcon} instances, which support dynamic
     * scaling while maintaining crisp edges at any scale.
     * <p>
     * Example:
     * <pre>{@code
     * String checkmarkSvg = "<svg width='16' height='16'>" +
     *                       "<path d='M2 8l4 4l8-8' stroke='green' stroke-width='2' fill='none'/>" +
     *                       "</svg>";
     * IconDeclaration checkmark = IconDeclaration.ofAutoScaledSvg(checkmarkSvg)
     *                                            .withSize(24, 24);
     * }</pre>
     * In the above example, an {@link SvgIcon} loaded using this declaration will report {@code -1}
     * for both {@link SvgIcon#getIconWidth()} and {@link SvgIcon#getIconHeight()} instead of 16!
     * This is to ensure that the svg is scalable when used as component icons...
     *
     * @param svg The complete SVG document as a string.
     *           Must not be {@code null}.
     * @return A new {@link IconDeclaration} instance representing the SVG icon.
     * @throws NullPointerException if {@code svg} is {@code null}.
     */
    static IconDeclaration ofAutoScaledSvg(String svg) {
        Objects.requireNonNull(svg);
        return IconDeclaration.of(Size.unknown(), SourceFormat.SVG_STRING, svg);
    }

    /**
     * Creates an {@link IconDeclaration} instance from an SVG document string,
     * <b>which will be resolved to an {@link SvgIcon} with the same size declared in the SVG text.</b>
     * This factory method is specifically designed for creating vector-based
     * icons programmatically without requiring external files.
     * <p>
     * The provided SVG string must be a complete, well-formed SVG XML document.
     * The resulting icon declaration will have its {@link #sourceFormat()}
     * set to {@link SourceFormat#SVG_STRING}.
     * <p>
     * Icon declarations produced by this method will resolve to
     * {@link swingtree.style.SvgIcon} instances, which support dynamic
     * scaling while maintaining crisp edges at any scale.
     * <p>
     * Example:
     * <pre>{@code
     * String checkmarkSvg = "<svg width='16' height='16'>" +
     *                       "<path d='M2 8l4 4l8-8' stroke='green' stroke-width='2' fill='none'/>" +
     *                       "</svg>";
     * IconDeclaration checkmark = IconDeclaration.ofSvg(checkmarkSvg)
     *                                            .withSize(24, 24);
     * }</pre>
     * In the above example, an {@link SvgIcon} loaded using this declaration will report {@code 16}
     * for both {@link SvgIcon#getIconWidth()} and {@link SvgIcon#getIconHeight()}.
     *
     * @param svg The complete SVG document as a string.
     *           Must not be {@code null}.
     * @return A new {@link IconDeclaration} instance representing the SVG icon.
     * @throws NullPointerException if {@code svg} is {@code null}.
     */
    static IconDeclaration ofSvg(String svg) {
        Objects.requireNonNull(svg);
        return PooledIconDeclaration.of(null, SourceFormat.SVG_STRING, svg);
    }

    /**
     *  Creates an {@link IconDeclaration} instance with full control over all
     *  declaration properties: size, source format, and source content.
     *  <p>
     *  This is the most comprehensive factory method, allowing precise specification
     *  of how the icon should be interpreted and displayed. It's particularly useful
     *  when creating icon declarations programmatically or when you need explicit
     *  control over the source format.
     *  <p>
     *  <b>Size Handling:</b>
     *  If the provided {@code size} parameter is {@link Size#unknown()}, it will
     *  indicate to raster based icons that they should use their natural dimensions
     *  (for raster images), whereas SVG based icons will be loaded without any size
     *  to make their size effectively context-dependent (automatic scaling).
     *  <p>
     *  <b>Example Usage:</b>
     *  <pre>{@code
     *  // Create a PNG icon declaration with specific size
     *  IconDeclaration icon1 = IconDeclaration.of(
     *      Size.of(32, 32),
     *      SourceFormat.PATH_TO_ICON,
     *      "icons/user.png"
     *  );
     *
     *  // Create an SVG icon declaration with unknown (flexible) size
     *  IconDeclaration icon2 = IconDeclaration.of(
     *      Size.unknown(),
     *      SourceFormat.SVG_STRING,
     *      "<svg ...>...</svg>"
     *  );
     *  }</pre>
     *
     *  @param size The preferred display size for the icon. Use {@link Size#unknown()}
     *              to indicate natural/context-dependent sizing. Must not be {@code null}.
     *  @param sourceFormat The format interpretation of the source string.
     *              Must not be {@code null}.
     *  @param source The icon source content (file path or SVG XML).
     *              Must not be {@code null}.
     *  @return A new {@link IconDeclaration} instance configured with the specified
     *          properties.
     *  @throws NullPointerException if any parameter is {@code null}.
     *  @see Size#unknown()
     *  @see SourceFormat
     */
    static IconDeclaration of( Size size, SourceFormat sourceFormat, String source ) {
        Objects.requireNonNull(size);
        Objects.requireNonNull(sourceFormat);
        Objects.requireNonNull(source);
        return PooledIconDeclaration.of( size, sourceFormat, source );
    }
}