Explain empty object views

Icon, where to add them, path chip.
This commit is contained in:
piexel committed 2026-10-21 21:06:00 +02:00
1 parent 0b5b75e936
commit 4ac268047e
8 files changed
+150 -4

No files matched your search

@@ -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 <https://www.gnu.org/licenses/>.
*/
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();
}
}
@@ -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());
}
}
@@ -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) {
@@ -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<String> keys(IrisData data);
@@ -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) {
@@ -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<String> keys(IrisData data) {
return Arrays.asList(data.getObjectLoader().getPossibleKeys());
@@ -39,6 +39,8 @@ final class ProceduralObjectSource implements ObjectSource {
"fungi", "Fungi",
"ruins", "Ruins");
private static final int EMPTY_ICON_SIZE = 40;
private Map<String, ResolvedProceduralPlacement> 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<String> keys(IrisData data) {