diff --git a/core/src/main/java/com/volmit/iris/core/gui/MapCanvas.java b/core/src/main/java/com/volmit/iris/core/gui/MapCanvas.java
index d78dd54ab..b48d08968 100644
--- a/core/src/main/java/com/volmit/iris/core/gui/MapCanvas.java
+++ b/core/src/main/java/com/volmit/iris/core/gui/MapCanvas.java
@@ -112,6 +112,7 @@ final class MapCanvas extends JPanel {
private boolean entitiesVisible;
private boolean follow;
private boolean help;
+ private int coveredLeftWidth;
private boolean debug;
private boolean detailedHover;
private boolean active = true;
@@ -337,6 +338,17 @@ final class MapCanvas extends JPanel {
listener.onTogglesChanged();
}
+ /**
+ * Keeps the help, debug and notification cards clear of a panel floating over the map's left side.
+ */
+ void setCoveredLeftWidth(int width) {
+ if (width == coveredLeftWidth) {
+ return;
+ }
+ coveredLeftWidth = width;
+ repaint();
+ }
+
void toggleHelp() {
help = !help;
repaint();
@@ -814,15 +826,16 @@ final class MapCanvas extends JPanel {
int lineHeight = canvas.getFontMetrics().getHeight() + StudioTheme.scale(4);
int width = keyWidth + columnGap + descriptionWidth + padding * 2;
int height = keys.size() * lineHeight + padding * 2;
- StudioOverlay.drawCard(canvas, padding, padding, width, height);
+ int left = coveredLeftWidth + padding;
+ StudioOverlay.drawCard(canvas, left, padding, width, height);
for (int index = 0; index < keys.size(); index++) {
int y = padding * 2 + canvas.getFontMetrics().getAscent() + index * lineHeight;
canvas.setFont(StudioTheme.boldFont());
canvas.setColor(StudioTheme.LINK);
- canvas.drawString(keys.get(index), padding * 2, y);
+ canvas.drawString(keys.get(index), left + padding, y);
canvas.setFont(StudioTheme.font());
canvas.setColor(StudioTheme.SECONDARY_TEXT);
- canvas.drawString(descriptions.get(index), padding * 2 + keyWidth + columnGap, y);
+ canvas.drawString(descriptions.get(index), left + padding + keyWidth + columnGap, y);
}
}
@@ -835,7 +848,7 @@ final class MapCanvas extends JPanel {
lines.add("Tile scale " + (frame == null ? "-" : Form.f(frame.renderBlocksPerPixel(), 0)) + " blocks/px");
lines.add(paintCadenceFps + " paints per second");
int padding = StudioTheme.scale(CARD_PADDING);
- drawCard(canvas, padding, getHeight() - padding, 0D, 1D, lines);
+ drawCard(canvas, coveredLeftWidth + padding, getHeight() - padding, 0D, 1D, lines);
}
private void renderNotifications(Graphics2D canvas) {
@@ -848,7 +861,7 @@ final class MapCanvas extends JPanel {
int padding = StudioTheme.scale(CARD_PADDING);
int width = textWidth + padding * 2;
int height = canvas.getFontMetrics().getHeight() + padding;
- int x = (getWidth() - width) / 2;
+ int x = coveredLeftWidth + (getWidth() - coveredLeftWidth - width) / 2;
int y = getHeight() - height - StudioTheme.scale(16);
StudioOverlay.drawCard(canvas, x, y, width, height);
canvas.setColor(StudioTheme.TEXT);
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
new file mode 100644
index 000000000..59f812793
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/MapViewIcon.java
@@ -0,0 +1,190 @@
+/*
+ * 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 java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import java.util.List;
+
+/**
+ * A small outline picture of what a map view shows, in the style of IntelliJ's new UI icons: thin single-colour
+ * lines on a 16 by 16 grid, laid on half pixels so they stay crisp.
+ */
+final class MapViewIcon {
+ private static final double GRID = 16D;
+ private static final float LINE_WIDTH = 1F;
+ private static final Color LINE_COLOR = new Color(0xCED0D6);
+
+ private MapViewIcon() {
+ }
+
+ static void paint(Graphics2D graphics, MapView view, int x, int y, int size) {
+ Graphics2D canvas = (Graphics2D) graphics.create();
+ try {
+ canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ canvas.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
+ canvas.translate(x, y);
+ canvas.scale(size / GRID, size / GRID);
+ canvas.setColor(LINE_COLOR);
+ canvas.setStroke(new BasicStroke(LINE_WIDTH, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+ for (Shape line : glyphOf(view)) {
+ canvas.draw(line);
+ }
+ } finally {
+ canvas.dispose();
+ }
+ }
+
+ private static List glyphOf(MapView view) {
+ return switch (view) {
+ case BIOMES -> leaf();
+ case REGIONS -> foldedMap();
+ case HEIGHT -> mountains();
+ case CONTINENTS -> globe();
+ case LAND_BIOMES -> tree();
+ case SEA_BIOMES -> waves();
+ case CAVE_BIOMES -> caveMouth();
+ case PALETTES -> layers();
+ case OBJECTS -> cube();
+ case NOISE -> noiseWave();
+ };
+ }
+
+ private static List leaf() {
+ Path2D blade = new Path2D.Double();
+ blade.moveTo(3.5, 12.5);
+ blade.curveTo(2.5, 6, 6.5, 2.5, 13.5, 2.5);
+ blade.curveTo(13.5, 9.5, 10, 13.5, 3.5, 12.5);
+ return List.of(blade, new Line2D.Double(3.5, 12.5, 9.5, 6.5));
+ }
+
+ private static List foldedMap() {
+ Path2D outline = new Path2D.Double();
+ outline.moveTo(1.5, 3.5);
+ outline.lineTo(5.5, 2.5);
+ outline.lineTo(10.5, 3.5);
+ outline.lineTo(14.5, 2.5);
+ outline.lineTo(14.5, 12.5);
+ outline.lineTo(10.5, 13.5);
+ outline.lineTo(5.5, 12.5);
+ outline.lineTo(1.5, 13.5);
+ outline.closePath();
+ return List.of(outline, new Line2D.Double(5.5, 2.5, 5.5, 12.5), new Line2D.Double(10.5, 3.5, 10.5, 13.5));
+ }
+
+ private static List mountains() {
+ Path2D ridge = new Path2D.Double();
+ ridge.moveTo(1.5, 13.5);
+ ridge.lineTo(6.5, 4.5);
+ ridge.lineTo(9.5, 9.5);
+ ridge.lineTo(11, 7.5);
+ ridge.lineTo(14.5, 13.5);
+ ridge.closePath();
+ return List.of(ridge);
+ }
+
+ private static List globe() {
+ return List.of(
+ new Ellipse2D.Double(1.5, 1.5, 13, 13),
+ new Ellipse2D.Double(5, 1.5, 6, 13),
+ new Line2D.Double(1.5, 8, 14.5, 8));
+ }
+
+ private static List tree() {
+ Path2D crown = new Path2D.Double();
+ crown.moveTo(8, 1.5);
+ crown.lineTo(13.5, 11.5);
+ crown.lineTo(2.5, 11.5);
+ crown.closePath();
+ return List.of(crown, new Line2D.Double(8, 11.5, 8, 14.5));
+ }
+
+ private static List waves() {
+ return List.of(wave(5.5), wave(10.5));
+ }
+
+ private static Shape wave(double middleY) {
+ Path2D wave = new Path2D.Double();
+ wave.moveTo(1.5, middleY);
+ wave.curveTo(3.7, middleY - 2.5, 5.8, middleY - 2.5, 8, middleY);
+ wave.curveTo(10.2, middleY + 2.5, 12.3, middleY + 2.5, 14.5, middleY);
+ return wave;
+ }
+
+ private static List caveMouth() {
+ Path2D arch = new Path2D.Double();
+ arch.moveTo(3.5, 13.5);
+ arch.lineTo(3.5, 8);
+ arch.curveTo(3.5, 2, 12.5, 2, 12.5, 8);
+ arch.lineTo(12.5, 13.5);
+ return List.of(arch, new Line2D.Double(1.5, 13.5, 14.5, 13.5));
+ }
+
+ // Stacked sheets, like IntelliJ's layers icon.
+ private static List layers() {
+ Path2D top = new Path2D.Double();
+ top.moveTo(8, 1.5);
+ top.lineTo(14.5, 5);
+ top.lineTo(8, 8.5);
+ top.lineTo(1.5, 5);
+ top.closePath();
+ return List.of(top, chevron(8.5), chevron(11.5));
+ }
+
+ private static Shape chevron(double sideY) {
+ Path2D chevron = new Path2D.Double();
+ chevron.moveTo(1.5, sideY);
+ chevron.lineTo(8, sideY + 3.5);
+ chevron.lineTo(14.5, sideY);
+ return chevron;
+ }
+
+ private static List cube() {
+ Path2D outline = new Path2D.Double();
+ outline.moveTo(8, 1.5);
+ outline.lineTo(14, 4.5);
+ outline.lineTo(14, 11.5);
+ outline.lineTo(8, 14.5);
+ outline.lineTo(2, 11.5);
+ outline.lineTo(2, 4.5);
+ outline.closePath();
+ Path2D edges = new Path2D.Double();
+ edges.moveTo(2, 4.5);
+ edges.lineTo(8, 7.5);
+ edges.lineTo(14, 4.5);
+ edges.moveTo(8, 7.5);
+ edges.lineTo(8, 14.5);
+ return List.of(outline, edges);
+ }
+
+ private static List noiseWave() {
+ Path2D wave = new Path2D.Double();
+ wave.moveTo(1.5, 9);
+ wave.curveTo(2.8, 3, 4.6, 3, 5.8, 8);
+ wave.curveTo(7, 13, 8.8, 13.5, 10, 7.5);
+ wave.curveTo(11, 3.5, 13, 4.5, 14.5, 8.5);
+ return List.of(wave);
+ }
+}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/MapViewList.java b/core/src/main/java/com/volmit/iris/core/gui/MapViewList.java
index e70eea3e4..76d5cc33b 100644
--- a/core/src/main/java/com/volmit/iris/core/gui/MapViewList.java
+++ b/core/src/main/java/com/volmit/iris/core/gui/MapViewList.java
@@ -18,27 +18,35 @@
package com.volmit.iris.core.gui;
-import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
+import javax.swing.Icon;
import javax.swing.JList;
import javax.swing.ListSelectionModel;
+import javax.swing.UIManager;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
+import java.awt.Insets;
+import java.awt.Rectangle;
import java.awt.RenderingHints;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
import java.util.Locale;
import java.util.function.Consumer;
/**
- * The map window's list of views, grouped under headers, each row showing the key that opens it.
+ * The map window's list of views, grouped under headers, each row with a picture of the view and the key that opens
+ * it.
*/
final class MapViewList extends JList {
private static final long serialVersionUID = -4130872409914437415L;
- private static final int ROW_HEIGHT = 26;
+ private static final int ROW_HEIGHT = 28;
+ private static final int NO_ROW = -1;
private final DefaultListModel rows = new DefaultListModel<>();
private int lastViewRow;
+ private int hoveredRow = NO_ROW;
MapViewList(Consumer onViewChosen) {
fillRows();
@@ -58,6 +66,43 @@ final class MapViewList extends JList {
lastViewRow = getSelectedIndex();
onViewChosen.accept(selected.view());
});
+ MouseAdapter hover = new MouseAdapter() {
+ @Override
+ public void mouseMoved(MouseEvent event) {
+ hover(viewRowAt(event));
+ }
+
+ @Override
+ public void mouseExited(MouseEvent event) {
+ hover(NO_ROW);
+ }
+ };
+ addMouseListener(hover);
+ addMouseMotionListener(hover);
+ }
+
+ private int viewRowAt(MouseEvent event) {
+ int row = locationToIndex(event.getPoint());
+ if (row < 0 || rows.get(row).isHeader() || !getCellBounds(row, row).contains(event.getPoint())) {
+ return NO_ROW;
+ }
+ return row;
+ }
+
+ private void hover(int row) {
+ if (row == hoveredRow) {
+ return;
+ }
+ repaintRow(hoveredRow);
+ hoveredRow = row;
+ repaintRow(hoveredRow);
+ }
+
+ private void repaintRow(int row) {
+ Rectangle bounds = row == NO_ROW ? null : getCellBounds(row, row);
+ if (bounds != null) {
+ repaint(bounds);
+ }
}
void select(MapView view) {
@@ -123,7 +168,12 @@ final class MapViewList extends JList {
private static final class RowRenderer extends DefaultListCellRenderer {
private static final long serialVersionUID = 6302156810211578474L;
+ private static final int ICON_SIZE = 16;
+ private static final int ICON_TEXT_GAP = 8;
+ private static final int KEYCAP_SIZE = 17;
+ private static final int KEYCAP_ARC = 5;
private String shortcut;
+ private boolean hovered;
@Override
public Component getListCellRendererComponent(JList> list, Object value, int index, boolean selected, boolean focused) {
@@ -133,35 +183,83 @@ final class MapViewList extends JList {
setText(row.group().label().toUpperCase(Locale.ROOT));
setFont(StudioTheme.sectionFont());
setForeground(StudioTheme.MUTED_TEXT);
- setBorder(StudioTheme.emptyBorder(index == 0 ? 0 : 6, 6, 0, 6));
+ setBorder(StudioTheme.emptyBorder(index == 0 ? 2 : 10, 8, 0, 6));
+ setIcon(null);
setToolTipText(null);
shortcut = null;
+ hovered = false;
+ setOpaque(true);
return this;
}
setFont(StudioTheme.font());
- setBorder(StudioTheme.emptyBorder(0, 10, 0, 10));
+ setBorder(StudioTheme.emptyBorder(0, 8, 0, 8));
+ setIcon(new ViewIcon(row.view()));
+ setIconTextGap(StudioTheme.scale(ICON_TEXT_GAP));
setToolTipText(row.view().description());
shortcut = row.view().shortcutText();
+ hovered = !selected && list instanceof MapViewList viewList && viewList.hoveredRow == index;
+ setOpaque(!hovered);
return this;
}
@Override
protected void paintComponent(Graphics graphics) {
- super.paintComponent(graphics);
- if (shortcut == null) {
- return;
- }
Graphics2D canvas = (Graphics2D) graphics.create();
try {
+ canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
canvas.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
- canvas.setFont(StudioTheme.smallFont());
- canvas.setColor(StudioTheme.MUTED_TEXT);
- int textWidth = canvas.getFontMetrics().stringWidth(shortcut);
- int baseline = (getHeight() + canvas.getFontMetrics().getAscent() - canvas.getFontMetrics().getDescent()) / 2;
- canvas.drawString(shortcut, getWidth() - getInsets().right - textWidth, baseline);
+ if (hovered) {
+ paintHoverBackground(canvas);
+ }
+ super.paintComponent(graphics);
+ if (shortcut != null) {
+ paintKeycap(canvas);
+ }
} finally {
canvas.dispose();
}
}
+
+ // Shaped like the selection the look and feel paints, so moving between them doesn't jump.
+ private void paintHoverBackground(Graphics2D canvas) {
+ Insets inset = UIManager.getInsets("List.selectionInsets");
+ int left = inset == null ? 0 : StudioTheme.scale(inset.left);
+ int right = inset == null ? 0 : StudioTheme.scale(inset.right);
+ int arc = StudioTheme.scale(UIManager.getInt("List.selectionArc"));
+ canvas.setColor(StudioTheme.HOVER_BACKGROUND);
+ canvas.fillRoundRect(left, 0, getWidth() - left - right, getHeight(), arc, arc);
+ }
+
+ private void paintKeycap(Graphics2D canvas) {
+ canvas.setFont(StudioTheme.smallFont());
+ int textWidth = canvas.getFontMetrics().stringWidth(shortcut);
+ int height = StudioTheme.scale(KEYCAP_SIZE);
+ int width = Math.max(height, textWidth + StudioTheme.scale(8));
+ int x = getWidth() - getInsets().right - width;
+ int y = (getHeight() - height) / 2;
+ int arc = StudioTheme.scale(KEYCAP_ARC);
+ canvas.setColor(StudioTheme.CONTROL_BORDER);
+ canvas.drawRoundRect(x, y, width - 1, height - 1, arc, arc);
+ canvas.setColor(StudioTheme.SECONDARY_TEXT);
+ int baseline = y + (height + canvas.getFontMetrics().getAscent() - canvas.getFontMetrics().getDescent()) / 2;
+ canvas.drawString(shortcut, x + (width - textWidth) / 2, baseline);
+ }
+ }
+
+ private record ViewIcon(MapView view) implements Icon {
+ @Override
+ public void paintIcon(Component component, Graphics graphics, int x, int y) {
+ MapViewIcon.paint((Graphics2D) graphics, view, x, y, getIconWidth());
+ }
+
+ @Override
+ public int getIconWidth() {
+ return StudioTheme.scale(RowRenderer.ICON_SIZE);
+ }
+
+ @Override
+ public int getIconHeight() {
+ return getIconWidth();
+ }
}
}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/MapWindow.java b/core/src/main/java/com/volmit/iris/core/gui/MapWindow.java
index d7df45f07..bd3bb9ef6 100644
--- a/core/src/main/java/com/volmit/iris/core/gui/MapWindow.java
+++ b/core/src/main/java/com/volmit/iris/core/gui/MapWindow.java
@@ -47,6 +47,7 @@ import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;
+import java.util.function.IntConsumer;
/**
* The studio map: one window showing the world as tiles in several views, with the palette, object and noise explorers
@@ -141,6 +142,7 @@ public final class MapWindow implements MapCanvas.Listener {
frame.add(inspectorSlot, BorderLayout.EAST);
frame.add(statusBars, BorderLayout.SOUTH);
installShortcuts(frame.getRootPane());
+ keepClearOfIsland(canvas::setCoveredLeftWidth);
frame.setSize(GuiHost.fitToScreen(StudioTheme.scaledSize(1440, 880)));
frame.setMinimumSize(StudioTheme.scaledSize(820, 520));
frame.setLocationRelativeTo(null);
@@ -260,16 +262,19 @@ public final class MapWindow implements MapCanvas.Listener {
}
}
- private void createObjectExplorer() {
- objectExplorer = new ObjectExplorer(start.engine(), start.worldId());
- ObjectViewport viewport = objectExplorer.viewport();
- viewport.setCoveredLeftWidth(sidebarIsland.getWidth());
+ private void keepClearOfIsland(IntConsumer setCoveredLeftWidth) {
+ setCoveredLeftWidth.accept(sidebarIsland.getWidth());
sidebarIsland.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent event) {
- viewport.setCoveredLeftWidth(sidebarIsland.getWidth());
+ setCoveredLeftWidth.accept(sidebarIsland.getWidth());
}
});
+ }
+
+ private void createObjectExplorer() {
+ objectExplorer = new ObjectExplorer(start.engine(), start.worldId());
+ keepClearOfIsland(objectExplorer.viewport()::setCoveredLeftWidth);
content.add(objectExplorer.view(), OBJECTS_CARD);
statusBars.add(objectExplorer.statusBar(), OBJECTS_CARD);
}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/StudioTheme.java b/core/src/main/java/com/volmit/iris/core/gui/StudioTheme.java
index 7a71842e9..9d877bfd5 100644
--- a/core/src/main/java/com/volmit/iris/core/gui/StudioTheme.java
+++ b/core/src/main/java/com/volmit/iris/core/gui/StudioTheme.java
@@ -80,6 +80,7 @@ final class StudioTheme {
Map.entry("List.selectionInsets", "0,6,0,6"),
Map.entry("Tree.selectionArc", "6"),
Map.entry("Tree.selectionInsets", "0,6,0,6"),
+ Map.entry("Tree.border", "0,8,0,0"),
Map.entry("ScrollBar.width", "10"),
Map.entry("ScrollBar.thumbArc", "999"),
Map.entry("ScrollBar.thumbInsets", "2,2,2,2"),