diff --git a/core/src/main/java/com/volmit/iris/core/gui/EmptyState.java b/core/src/main/java/com/volmit/iris/core/gui/EmptyState.java new file mode 100644 index 000000000..13d411aed --- /dev/null +++ b/core/src/main/java/com/volmit/iris/core/gui/EmptyState.java @@ -0,0 +1,102 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.volmit.iris.core.gui; + +import javax.swing.Icon; +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.RoundRectangle2D; + +/** + * What an empty view says, drawn in the middle of it the way IntelliJ fills an empty tool window: a large icon, what + * is missing, and where to add it. + * + * @param location the pack folder or file the hint points to, drawn as code + */ +record EmptyState(Icon icon, String title, String hint, String location, String footer) { + private static final int ICON_GAP = 18; + private static final int TITLE_GAP = 8; + private static final int LOCATION_GAP = 10; + private static final int FOOTER_GAP = 14; + private static final int CHIP_PADDING_X = 8; + private static final int CHIP_PADDING_Y = 4; + private static final int CHIP_ARC = 8; + + /** + * Draws the empty state centred on the given point. + */ + void paint(Graphics2D graphics, Component owner, int centerX, int centerY) { + Graphics2D canvas = (Graphics2D) graphics.create(); + try { + canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + int top = centerY - height(canvas) / 2; + icon.paintIcon(owner, canvas, centerX - icon.getIconWidth() / 2, top); + top += icon.getIconHeight() + StudioTheme.scale(ICON_GAP); + top = drawCentered(canvas, title, StudioTheme.titleFont(), StudioTheme.TEXT, centerX, top); + top = drawCentered(canvas, hint, StudioTheme.font(), StudioTheme.SECONDARY_TEXT, centerX, + top + StudioTheme.scale(TITLE_GAP)); + top = drawLocationChip(canvas, centerX, top + StudioTheme.scale(LOCATION_GAP)); + drawCentered(canvas, footer, StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, centerX, top + StudioTheme.scale(FOOTER_GAP)); + } finally { + canvas.dispose(); + } + } + + private int height(Graphics2D canvas) { + return icon.getIconHeight() + StudioTheme.scale(ICON_GAP) + + canvas.getFontMetrics(StudioTheme.titleFont()).getHeight() + + StudioTheme.scale(TITLE_GAP) + canvas.getFontMetrics(StudioTheme.font()).getHeight() + + StudioTheme.scale(LOCATION_GAP) + chipHeight(canvas) + + StudioTheme.scale(FOOTER_GAP) + canvas.getFontMetrics(StudioTheme.smallFont()).getHeight(); + } + + private int chipHeight(Graphics2D canvas) { + return canvas.getFontMetrics(StudioTheme.monospacedFont()).getHeight() + StudioTheme.scale(CHIP_PADDING_Y) * 2; + } + + private int drawLocationChip(Graphics2D canvas, int centerX, int top) { + canvas.setFont(StudioTheme.monospacedFont()); + FontMetrics metrics = canvas.getFontMetrics(); + int width = metrics.stringWidth(location) + StudioTheme.scale(CHIP_PADDING_X) * 2; + int height = chipHeight(canvas); + int left = centerX - width / 2; + RoundRectangle2D chip = new RoundRectangle2D.Double(left + 0.5D, top + 0.5D, width - 1D, height - 1D, + StudioTheme.scale(CHIP_ARC), StudioTheme.scale(CHIP_ARC)); + canvas.setColor(StudioTheme.PANEL_BACKGROUND); + canvas.fill(chip); + canvas.setColor(StudioTheme.CONTROL_BORDER); + canvas.draw(chip); + canvas.setColor(StudioTheme.TEXT); + canvas.drawString(location, left + StudioTheme.scale(CHIP_PADDING_X), top + StudioTheme.scale(CHIP_PADDING_Y) + metrics.getAscent()); + return top + height; + } + + private static int drawCentered(Graphics2D canvas, String text, Font font, Color color, int centerX, int top) { + canvas.setFont(font); + canvas.setColor(color); + FontMetrics metrics = canvas.getFontMetrics(); + canvas.drawString(text, centerX - metrics.stringWidth(text) / 2, top + metrics.getAscent()); + return top + metrics.getHeight(); + } +} diff --git a/core/src/main/java/com/volmit/iris/core/gui/MapViewIcon.java b/core/src/main/java/com/volmit/iris/core/gui/MapViewIcon.java index 261657740..9a219c3b2 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/MapViewIcon.java +++ b/core/src/main/java/com/volmit/iris/core/gui/MapViewIcon.java @@ -37,11 +37,18 @@ final class MapViewIcon { } static Icon of(MapView view) { - return LOADED.computeIfAbsent(view, MapViewIcon::load); + return LOADED.computeIfAbsent(view, shown -> load(shown, SIZE)); } - private static Icon load(MapView view) { + /** + * The view's icon drawn larger, for an empty view. + */ + static Icon large(MapView view, int size) { + return load(view, size); + } + + private static Icon load(MapView view, int size) { String fileName = view.name().toLowerCase(Locale.ROOT).replace('_', '-') + ".svg"; - return new FlatSVGIcon(ICON_FOLDER + fileName, SIZE, SIZE, MapViewIcon.class.getClassLoader()); + return new FlatSVGIcon(ICON_FOLDER + fileName, size, size, MapViewIcon.class.getClassLoader()); } } diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java index 77093c4d9..717da7a27 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectExplorer.java @@ -190,7 +190,7 @@ final class ObjectExplorer { browser.showObjects(keys); String key = shownKey != null && keys.contains(shownKey) ? shownKey : browser.firstObject(); if (key == null) { - viewport.showMessage("This pack has no " + source.noun() + "s"); + viewport.showEmptyState(source.emptyState()); overview.clear(); layerCut.showHeight(1); if (comparingVariants) { diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectSource.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectSource.java index 1c1f60d88..b96b125a5 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/ObjectSource.java +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectSource.java @@ -31,6 +31,9 @@ interface ObjectSource { /** What one entry is called, such as "object", for the browser's texts. */ String noun(); + /** What the view shows when the pack has no entries. */ + EmptyState emptyState(); + /** The key of every entry, where a slash separates folders. */ List keys(IrisData data); diff --git a/core/src/main/java/com/volmit/iris/core/gui/ObjectViewport.java b/core/src/main/java/com/volmit/iris/core/gui/ObjectViewport.java index 7debd0f26..ff187ed21 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/ObjectViewport.java +++ b/core/src/main/java/com/volmit/iris/core/gui/ObjectViewport.java @@ -64,6 +64,7 @@ final class ObjectViewport extends JComponent { private ObjectCamera camera = ObjectCamera.START; private BufferedImage frame; private String message = "Choose an object"; + private EmptyState emptyState; private boolean rendering; private boolean renderAgain; private Point dragPoint; @@ -98,6 +99,7 @@ final class ObjectViewport extends JComponent { mesh = nextMesh; camera = ObjectCamera.START; message = nextMesh.isEmpty() ? "No visible blocks" : null; + emptyState = null; if (nextMesh.isEmpty()) { frame = null; finishLoading(); @@ -116,6 +118,7 @@ final class ObjectViewport extends JComponent { } loadingText = text; message = null; + emptyState = null; loadingTicker.start(); } @@ -130,6 +133,7 @@ final class ObjectViewport extends JComponent { void replaceMesh(ObjectMesh nextMesh) { mesh = nextMesh; message = nextMesh.isEmpty() ? "No visible blocks" : null; + emptyState = null; requestFrame(); repaint(); } @@ -147,6 +151,16 @@ final class ObjectViewport extends JComponent { void showMessage(String text) { mesh = null; message = text; + emptyState = null; + frame = null; + finishLoading(); + repaint(); + } + + void showEmptyState(EmptyState state) { + mesh = null; + message = null; + emptyState = state; frame = null; finishLoading(); repaint(); @@ -223,6 +237,9 @@ final class ObjectViewport extends JComponent { if (message != null) { drawMessage((Graphics2D) graphics); } + if (emptyState != null) { + emptyState.paint((Graphics2D) graphics, this, coveredLeftWidth + (getWidth() - coveredLeftWidth) / 2, getHeight() / 2); + } } private void drawLoading(Graphics2D graphics) { diff --git a/core/src/main/java/com/volmit/iris/core/gui/PackObjectSource.java b/core/src/main/java/com/volmit/iris/core/gui/PackObjectSource.java index e3f46b481..902aaf8f9 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PackObjectSource.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PackObjectSource.java @@ -27,11 +27,19 @@ import java.util.List; * The pack's object files, by their load keys. */ final class PackObjectSource implements ObjectSource { + private static final int EMPTY_ICON_SIZE = 40; + @Override public String noun() { return "object"; } + @Override + public EmptyState emptyState() { + return new EmptyState(MapViewIcon.large(MapView.OBJECTS, EMPTY_ICON_SIZE), "No objects yet", + "Put the pack's .iob objects in", "objects/", "They appear here once the pack reloads."); + } + @Override public List keys(IrisData data) { return Arrays.asList(data.getObjectLoader().getPossibleKeys()); diff --git a/core/src/main/java/com/volmit/iris/core/gui/RegionInspectorPanel.java b/core/src/main/java/com/volmit/iris/core/gui/PickInspectorPanel.java similarity index 100% rename from core/src/main/java/com/volmit/iris/core/gui/RegionInspectorPanel.java rename to core/src/main/java/com/volmit/iris/core/gui/PickInspectorPanel.java diff --git a/core/src/main/java/com/volmit/iris/core/gui/ProceduralObjectSource.java b/core/src/main/java/com/volmit/iris/core/gui/ProceduralObjectSource.java index 7aadcce39..b66c3a607 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/ProceduralObjectSource.java +++ b/core/src/main/java/com/volmit/iris/core/gui/ProceduralObjectSource.java @@ -39,6 +39,8 @@ final class ProceduralObjectSource implements ObjectSource { "fungi", "Fungi", "ruins", "Ruins"); + private static final int EMPTY_ICON_SIZE = 40; + private Map definitionsByKey = Map.of(); @Override @@ -46,6 +48,13 @@ final class ProceduralObjectSource implements ObjectSource { return "procedural object"; } + @Override + public EmptyState emptyState() { + return new EmptyState(MapViewIcon.large(MapView.PROCEDURAL, EMPTY_ICON_SIZE), "No procedural objects yet", + "Declare them in a biome or region, or save a settings file per object, like", + "snippet/" + ProceduralObjectCatalog.TREE_FOLDER + "/oak.json", "They appear here once the pack reloads."); + } + // The type folder gets its label; the rest of the path stays as the pack has it. @Override public List keys(IrisData data) {