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