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 7dff9e6ff..d90259cce 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 @@ -839,11 +839,11 @@ final class MapCanvas extends JPanel { return; } MapHoverProbe.Details details = hoverDetails; - List lines = MapHoverCard.lines(quick.subject(), quick.region(), details, currentType, detailedHover, + MapHoverCard card = MapHoverCard.of(quick.subject(), quick.region(), details, currentType, detailedHover, quick.worldX(), quick.worldZ()); int screenX = (int) Math.round(worldToScreenX(quick.worldX())) + StudioTheme.scale(16); int screenY = (int) Math.round(worldToScreenZ(quick.worldZ())); - drawCard(canvas, screenX, screenY, 0D, 0D, lines); + card.paint(canvas, screenX, screenY, getWidth(), getHeight()); } private void renderHelp(Graphics2D canvas) { diff --git a/core/src/main/java/com/volmit/iris/core/gui/MapHoverCard.java b/core/src/main/java/com/volmit/iris/core/gui/MapHoverCard.java index 1b19d730f..d5d8ef01f 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/MapHoverCard.java +++ b/core/src/main/java/com/volmit/iris/core/gui/MapHoverCard.java @@ -24,49 +24,131 @@ import com.volmit.iris.core.loader.IrisRegistrant; import com.volmit.iris.engine.HybridBiomeSelector; import com.volmit.iris.engine.object.IrisBiome; +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** - * The text of the card that follows the cursor: what the drawn map shows there at once, and the rest once probed. + * The card that follows the cursor: what the drawn map shows there at once, and the rest once probed, in titled + * sections of labelled values. */ -final class MapHoverCard { - private MapHoverCard() { +record MapHoverCard(String title, String subtitle, List
sections, String footer) { + private static final int PADDING = 12; + private static final int SUBTITLE_GAP = 2; + private static final int SECTION_GAP = 10; + private static final int SECTION_TITLE_GAP = 4; + private static final int COLUMN_GAP = 14; + private static final int ROW_GAP = 2; + + /** + * @param subject the biome, or region in the region view, the drawn tile shows; null while that tile is drawing + * @param details the probed facts about the spot, or null until they arrive + * @param detailed whether Shift is held, which adds the pack files + */ + static MapHoverCard of(MapLabels.Label subject, MapLabels.Label region, MapHoverProbe.Details details, + RenderType type, boolean detailed, double worldX, double worldZ) { + List
sections = new ArrayList<>(); + if (details != null) { + sections.add(terrainSection(details, type)); + addPickSection(sections, details); + } + sections.add(locationSection(worldX, worldZ, detailed)); + if (detailed) { + addFilesSection(sections, subject, region); + } + return new MapHoverCard(title(subject, details), subtitle(region, details, type), sections, + detailed ? null : "Shift for pack files · H highlights"); } /** - * @param subject the biome, or region in the region view, the drawn tile shows; null while that tile is drawing - * @param details the probed facts about the spot, or null until they arrive - * @param detailed whether Shift is held, which adds keys and files + * Draws the card with its top left corner as close to the given point as fits inside the bounds. */ - static List lines(MapLabels.Label subject, MapLabels.Label region, MapHoverProbe.Details details, - RenderType type, boolean detailed, double worldX, double worldZ) { - List lines = new ArrayList<>(); - lines.add(title(subject, details)); - if (type != RenderType.REGION) { - String regionName = region != null ? region.name() : details != null ? details.region().getName() : null; - if (regionName != null) { - lines.add("Region " + regionName); + void paint(Graphics2D canvas, int anchorX, int anchorY, int boundsWidth, int boundsHeight) { + int padding = StudioTheme.scale(PADDING); + int labelWidth = labelColumnWidth(canvas); + int width = Math.max(headerWidth(canvas), labelWidth + StudioTheme.scale(COLUMN_GAP) + valueColumnWidth(canvas)) + padding * 2; + int height = contentHeight(canvas) + padding * 2; + int x = Math.max(padding, Math.min(boundsWidth - width - padding, anchorX)); + int y = Math.max(padding, Math.min(boundsHeight - height - padding, anchorY)); + StudioOverlay.drawCard(canvas, x, y, width, height); + + int left = x + padding; + int top = y + padding; + top = drawLine(canvas, title, StudioTheme.boldFont(), StudioTheme.TEXT, left, top); + if (subtitle != null) { + top = drawLine(canvas, subtitle, StudioTheme.smallFont(), StudioTheme.SECONDARY_TEXT, left, + top + StudioTheme.scale(SUBTITLE_GAP)); + } + for (Section section : sections) { + top += StudioTheme.scale(SECTION_GAP); + canvas.setColor(StudioTheme.OVERLAY_BORDER); + canvas.fillRect(left, top, width - padding * 2, 1); + top += StudioTheme.scale(SECTION_GAP) / 2; + top = drawLine(canvas, section.title().toUpperCase(Locale.ROOT), StudioTheme.sectionFont(), StudioTheme.MUTED_TEXT, + left, top) + StudioTheme.scale(SECTION_TITLE_GAP); + for (Row row : section.rows()) { + drawLine(canvas, row.label(), StudioTheme.font(), StudioTheme.SECONDARY_TEXT, left, top); + top = drawLine(canvas, row.value(), StudioTheme.font(), StudioTheme.TEXT, + left + labelWidth + StudioTheme.scale(COLUMN_GAP), top) + StudioTheme.scale(ROW_GAP); } } - if (details != null) { - addTerrain(lines, details, type); + if (footer != null) { + drawLine(canvas, footer, StudioTheme.smallFont(), StudioTheme.MUTED_TEXT, left, top + StudioTheme.scale(SECTION_GAP)); } - int blockX = MapCanvas.floorWorldCoordinate(worldX); - int blockZ = MapCanvas.floorWorldCoordinate(worldZ); - lines.add("Block " + blockX + ", " + blockZ + " · chunk " + (blockX >> 4) + ", " + (blockZ >> 4)); - if (!detailed) { - lines.add("Shift for keys and files · H highlights"); - return lines; + } + + private int labelColumnWidth(Graphics2D canvas) { + FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font()); + return sections.stream().flatMap(section -> section.rows().stream()) + .mapToInt(row -> metrics.stringWidth(row.label())).max().orElse(0); + } + + private int valueColumnWidth(Graphics2D canvas) { + FontMetrics metrics = canvas.getFontMetrics(StudioTheme.font()); + return sections.stream().flatMap(section -> section.rows().stream()) + .mapToInt(row -> metrics.stringWidth(row.value())).max().orElse(0); + } + + private int headerWidth(Graphics2D canvas) { + int width = canvas.getFontMetrics(StudioTheme.boldFont()).stringWidth(title); + FontMetrics small = canvas.getFontMetrics(StudioTheme.smallFont()); + if (subtitle != null) { + width = Math.max(width, small.stringWidth(subtitle)); } - addSource(lines, subject); - if (subject != null && subject.kind() != MapLabels.Kind.REGION) { - addSource(lines, region); + if (footer != null) { + width = Math.max(width, small.stringWidth(footer)); } - lines.add("Region file r." + (blockX >> 9) + "." + (blockZ >> 9) + ".mca"); - return lines; + return width; + } + + private int contentHeight(Graphics2D canvas) { + int small = canvas.getFontMetrics(StudioTheme.smallFont()).getHeight(); + int row = canvas.getFontMetrics(StudioTheme.font()).getHeight() + StudioTheme.scale(ROW_GAP); + int sectionTitle = canvas.getFontMetrics(StudioTheme.sectionFont()).getHeight() + StudioTheme.scale(SECTION_TITLE_GAP); + int height = canvas.getFontMetrics(StudioTheme.boldFont()).getHeight(); + if (subtitle != null) { + height += StudioTheme.scale(SUBTITLE_GAP) + small; + } + for (Section section : sections) { + height += StudioTheme.scale(SECTION_GAP) + StudioTheme.scale(SECTION_GAP) / 2 + sectionTitle + section.rows().size() * row; + } + if (footer != null) { + height += StudioTheme.scale(SECTION_GAP) + small; + } + return height; + } + + private static int drawLine(Graphics2D canvas, String text, Font font, Color color, int left, int top) { + canvas.setFont(font); + canvas.setColor(color); + FontMetrics metrics = canvas.getFontMetrics(); + canvas.drawString(text, left, top + metrics.getAscent()); + return top + metrics.getHeight(); } private static String title(MapLabels.Label subject, MapHoverProbe.Details details) { @@ -79,61 +161,115 @@ final class MapHoverCard { return "Drawing…"; } - private static void addTerrain(List lines, MapHoverProbe.Details details, RenderType type) { - lines.add("Height " + Math.round(details.height()) + " · sea level " + Math.round(details.fluidHeight())); - String slope = "slope " + Math.round(details.slopeDegrees()) + "°"; - lines.add(details.terrainKind() == null ? capitalize(slope) : details.terrainKind() + " · " + slope); + // The region view's title already is the region, so only the kind of ground is left to say. + private static String subtitle(MapLabels.Label region, MapHoverProbe.Details details, RenderType type) { + List parts = new ArrayList<>(); + if (type != RenderType.REGION) { + String regionName = region != null ? region.name() : details != null ? details.region().getName() : null; + if (regionName != null) { + parts.add(regionName + " region"); + } + } + if (details != null && details.terrainKind() != null) { + parts.add(details.terrainKind()); + } + return parts.isEmpty() ? null : String.join(" · ", parts); + } + + private static Section terrainSection(MapHoverProbe.Details details, RenderType type) { + List rows = new ArrayList<>(); + long height = Math.round(details.height()); + rows.add(new Row("Height", height + " · " + heightAboveSea(details.height() - details.fluidHeight()))); + rows.add(new Row("Slope", Math.round(details.slopeDegrees()) + "°")); IrisBiome surface = details.surfaceBiome(); IrisBiome cave = details.caveBiome(); if (type == RenderType.CAVE_LAND && surface != null) { - lines.add("Surface biome " + surface.getName()); + rows.add(new Row("Surface biome", surface.getName())); } else if (type != RenderType.CAVE_LAND && cave != null && cave != surface) { - lines.add("Cave biome " + cave.getName()); + rows.add(new Row("Cave biome", cave.getName())); } if (details.vanillaBiome() != null) { - lines.add("Minecraft biome " + details.vanillaBiome().replace('_', ' ')); + rows.add(new Row("Minecraft", details.vanillaBiome().replace('_', ' '))); + } + return new Section("Terrain", rows); + } + + private static void addPickSection(List
sections, MapHoverProbe.Details details) { + List rows = new ArrayList<>(); + MapHoverProbe.PickNoise noise = details.pickNoise(); + if (noise != null && noise.regionNoise() != null) { + rows.add(new Row("Region noise", noiseText(noise.regionNoise()))); + } + if (noise != null && noise.biomeType() != null) { + String label = capitalize(noise.biomeType().name().toLowerCase(Locale.ROOT)) + " noise"; + rows.add(new Row(label, biomeNoiseText(noise, details.surfaceBiome()))); } if (details.placement() != null) { - lines.add(placementText(details.placement())); + rows.add(new Row("Height", placementText(details.placement()))); } - if (details.pickNoise() != null) { - addPickNoise(lines, details.pickNoise(), surface); + if (!rows.isEmpty()) { + sections.add(new Section("Picked by", rows)); } } - private static void addPickNoise(List lines, MapHoverProbe.PickNoise noise, IrisBiome surface) { - if (noise.regionNoise() != null) { - lines.add("Region noise " + noiseText(noise.regionNoise())); - } - if (noise.biomeType() == null) { - return; - } - String biomeNoise = capitalize(noise.biomeType().name().toLowerCase(Locale.ROOT)) + " biome noise " - + noiseText(noise.biomeNoise()); + private static String biomeNoiseText(MapHoverProbe.PickNoise noise, IrisBiome surface) { + String value = noiseText(noise.biomeNoise()); IrisBiome picked = noise.noisePick(); if (picked == null) { - lines.add(biomeNoise); - } else if (surface == null || picked.getLoadKey().equals(surface.getLoadKey())) { - lines.add(biomeNoise + " picks " + picked.getName()); - } else { - lines.add(biomeNoise + " picks " + picked.getName() + ", then " + surface.getName() + " takes over"); + return value; } - } - - private static String noiseText(double noise) { - return String.format(Locale.ROOT, "%.3f", noise); + if (surface == null || picked.getLoadKey().equals(surface.getLoadKey())) { + return value + " → " + picked.getName(); + } + return value + " → " + picked.getName() + ", child " + surface.getName() + " took over"; } // How hybrid terrain chose the base biome; a child biome or a coast can still replace it afterwards. private static String placementText(HybridBiomeSelector.Choice choice) { - String height = "Picked at " + heightAboveSea(choice.placementHeight()) + " · "; + String height = heightAboveSea(choice.placementHeight()) + " · "; if (choice.fallback()) { - return height + "no range covers it, closest range wins"; + return height + "no range covers it, closest wins"; } if (choice.rivalCount() == 0) { - return height + "the only biome in range"; + return height + "only biome in range"; } - return height + "beat " + choice.rivalCount() + (choice.rivalCount() == 1 ? " other biome" : " other biomes") + " in range"; + return height + "beat " + choice.rivalCount() + (choice.rivalCount() == 1 ? " other" : " others") + " in range"; + } + + private static Section locationSection(double worldX, double worldZ, boolean detailed) { + int blockX = MapCanvas.floorWorldCoordinate(worldX); + int blockZ = MapCanvas.floorWorldCoordinate(worldZ); + List rows = new ArrayList<>(); + rows.add(new Row("Block", blockX + ", " + blockZ)); + rows.add(new Row("Chunk", (blockX >> 4) + ", " + (blockZ >> 4))); + if (detailed) { + rows.add(new Row("Region file", "r." + (blockX >> 9) + "." + (blockZ >> 9) + ".mca")); + } + return new Section("Location", rows); + } + + private static void addFilesSection(List
sections, MapLabels.Label subject, MapLabels.Label region) { + List rows = new ArrayList<>(); + addPackFile(rows, subject); + if (subject != null && subject.kind() != MapLabels.Kind.REGION) { + addPackFile(rows, region); + } + if (!rows.isEmpty()) { + sections.add(new Section("Pack files", rows)); + } + } + + // The key is the path under the type's folder, so folder, key and extension give the file within the pack. + private static void addPackFile(List rows, MapLabels.Label label) { + if (label == null || !(label.subject() instanceof IrisRegistrant registrant)) { + return; + } + String kind = label.kind() == MapLabels.Kind.REGION ? "Region" : "Biome"; + File file = registrant.getLoadFile(); + String fileName = file == null ? "" : file.getName(); + int extensionStart = fileName.lastIndexOf('.'); + String extension = extensionStart < 0 ? "" : fileName.substring(extensionStart); + rows.add(new Row(kind, registrant.getFolderName() + "/" + label.key() + extension)); } private static String heightAboveSea(double height) { @@ -141,20 +277,17 @@ final class MapHoverCard { return rounded < 0 ? -rounded + " below sea" : rounded + " above sea"; } - private static void addSource(List lines, MapLabels.Label label) { - if (label == null || !(label.subject() instanceof IrisRegistrant registrant)) { - return; - } - String kind = label.kind() == MapLabels.Kind.REGION ? "Region" : "Biome"; - lines.add(kind + " key " + label.key()); - File file = registrant.getLoadFile(); - if (file != null) { - File folder = file.getParentFile(); - lines.add(kind + " file " + (folder == null ? "" : folder.getName() + "/") + file.getName()); - } + private static String noiseText(double noise) { + return String.format(Locale.ROOT, "%.3f", noise); } private static String capitalize(String text) { return Character.toUpperCase(text.charAt(0)) + text.substring(1); } + + record Section(String title, List rows) { + } + + record Row(String label, String value) { + } }