diff --git a/core/src/main/java/com/volmit/iris/core/gui/BiomeExplorer.java b/core/src/main/java/com/volmit/iris/core/gui/BiomeExplorer.java index 5137ffbcb..aa67497cc 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/BiomeExplorer.java +++ b/core/src/main/java/com/volmit/iris/core/gui/BiomeExplorer.java @@ -88,8 +88,11 @@ final class BiomeExplorer { for (RegionPicks region : measurement.regions()) { List charts = region.coverage() != null ? List.of(PickInspectorPanel.heightChart(region.coverage())) - : region.spreads().stream().map(spread -> (JComponent) new NoisePickChart(spread, "Biomes")).toList(); - sections.add(new PickInspectorPanel.Section(region.name(), region.color(), region.share(), charts, region.notes())); + : region.spreads().stream() + .map(spread -> (JComponent) inspector.noiseChart(spread, "Biomes", region.key() + "/" + spread.title())) + .toList(); + sections.add(new PickInspectorPanel.Section(region.key(), region.name(), region.color(), region.share(), + biomeCountText(region.biomeCount()), charts, region.notes())); } String detail = measurement.hybrid() ? "Hybrid terrain · each region picks biomes by height" @@ -97,6 +100,10 @@ final class BiomeExplorer { inspector.showRegionSections(detail, sections, measurement.notes()); } + private static String biomeCountText(int count) { + return count == 1 ? "1 biome" : count + " biomes"; + } + private Measurement measurePicks(MapCensus.ViewArea area) { Engine engine = engineSource.get(); IrisComplex complex = engine == null ? null : engine.getComplex(); @@ -165,27 +172,35 @@ final class BiomeExplorer { private RegionPicks picks(Engine engine, int totalCount) { double share = totalCount == 0 ? 0D : count / (double) totalCount; + String key = region.getLoadKey(); String name = region.getName(); int color = PickMeasuring.regionColor(engine, region); List notes = new ArrayList<>(); if (complex.isHybridTerrain()) { - RegionHeightCoverage coverage = RegionHeightCoverage.measure(PickMeasuring.biomeRanges(engine, region), + List ranges = PickMeasuring.biomeRanges(engine, region); + RegionHeightCoverage coverage = RegionHeightCoverage.measure(ranges, pickingHeights.stream().mapToInt(Integer::intValue).toArray()); notes.addAll(coverage.notes()); - return new RegionPicks(name, color, share, List.of(), coverage, notes); + return new RegionPicks(key, name, color, share, ranges.size(), List.of(), coverage, notes); } List inView = spreads.stream().filter(typed -> typed.spread().sampleCount() > 0).toList(); PickMeasuring.addBiomeSpreadNotes(notes, inView); PickMeasuring.addChildBiomeNote(notes, complex, region); - return new RegionPicks(name, color, share, inView.stream().map(TypedSpread::spread).toList(), null, notes); + int biomeCount = (int) inView.stream() + .flatMap(typed -> typed.spread().rows().stream()) + .map(row -> row.option().key()) + .distinct() + .count(); + return new RegionPicks(key, name, color, share, biomeCount, inView.stream().map(TypedSpread::spread).toList(), + null, notes); } } /** * @param coverage how the region's biomes cover the heights in view in hybrid terrain, or null in biome terrain */ - private record RegionPicks(String name, int color, double share, List spreads, - RegionHeightCoverage coverage, List notes) { + private record RegionPicks(String key, String name, int color, double share, int biomeCount, + List spreads, RegionHeightCoverage coverage, List notes) { } private record Measurement(boolean hybrid, List regions, List notes) { diff --git a/core/src/main/java/com/volmit/iris/core/gui/NoisePickChart.java b/core/src/main/java/com/volmit/iris/core/gui/NoisePickChart.java index c4d7f273a..cd49a3ab9 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/NoisePickChart.java +++ b/core/src/main/java/com/volmit/iris/core/gui/NoisePickChart.java @@ -20,17 +20,26 @@ package com.volmit.iris.core.gui; import javax.swing.JComponent; import java.awt.Color; +import java.awt.Cursor; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; +import java.awt.Rectangle; import java.awt.RenderingHints; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; /** * One noise that picks regions or biomes: the spread of its values in view above the bands that pick each option, - * and a table of how much of the noise each option is given and how much it gets in view. + * and a table of how much of the noise each option is given and how much it gets in view. A long table shows only + * the options with the most ground in view until asked for all; a noise with one option to pick is a single line. */ final class NoisePickChart extends JComponent { private static final long serialVersionUID = 1873365510472853316L; @@ -44,21 +53,36 @@ final class NoisePickChart extends JComponent { private static final Color HISTOGRAM_BAR = new Color(0x9D, 0xA0, 0xA8, 150); private static final Color BAND_DIVIDER = new Color(255, 255, 255, 70); private static final String WIDEST_SHARE = "▲ 100.0%"; + private static final int SHORT_TABLE_ROWS = 6; private final NoisePickSpread spread; + private final List rows; private final String optionNoun; + private final Consumer onShowAllRowsChanged; + private boolean showsAllRows; + private Rectangle showAllToggleBounds; /** - * @param optionNoun what the noise picks, as a table heading: "Regions" or "Biomes" + * @param optionNoun what the noise picks, as a table heading: "Regions" or "Biomes" + * @param showsAllRows whether a long table starts with all its rows + * @param onShowAllRowsChanged receives whether the table shows all its rows after the user asked for more or fewer */ - NoisePickChart(NoisePickSpread spread, String optionNoun) { + NoisePickChart(NoisePickSpread spread, String optionNoun, boolean showsAllRows, Consumer onShowAllRowsChanged) { this.spread = spread; + this.rows = spread.rows(); this.optionNoun = optionNoun; + this.showsAllRows = showsAllRows; + this.onShowAllRowsChanged = onShowAllRowsChanged; setAlignmentX(LEFT_ALIGNMENT); + listenForShowAllToggle(); } @Override public Dimension getPreferredSize() { + if (hasSingleOption()) { + return new Dimension(StudioTheme.scale(PREFERRED_WIDTH), StudioTheme.scale(SECTION_GAP) + tableHeight() + + StudioTheme.scale(BOTTOM_GAP)); + } int height = StudioTheme.scale(SECTION_GAP) + spreadHeight() + StudioTheme.scale(SECTION_GAP) + tableHeight() + StudioTheme.scale(BOTTOM_GAP); return new Dimension(StudioTheme.scale(PREFERRED_WIDTH), height); @@ -78,6 +102,10 @@ final class NoisePickChart extends JComponent { int margin = StudioTheme.scale(MARGIN); int width = getWidth() - margin * 2; int top = StudioTheme.scale(SECTION_GAP); + if (hasSingleOption()) { + drawSingleOption(canvas, margin, top, width); + return; + } drawSpread(canvas, margin, top, width); drawTable(canvas, margin, top + spreadHeight() + StudioTheme.scale(SECTION_GAP), width); } finally { @@ -90,8 +118,63 @@ final class NoisePickChart extends JComponent { + StudioTheme.scale(2) + StudioTheme.scale(BAND_HEIGHT) + StudioTheme.scale(3) + smallMetrics().getHeight(); } + private boolean hasSingleOption() { + return rows.size() == 1; + } + + private boolean isLongTable() { + return rows.size() > SHORT_TABLE_ROWS; + } + private int tableHeight() { - return sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP) + rowHeight() * spread.rows().size(); + int toggleRows = isLongTable() ? 1 : 0; + return sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP) + rowHeight() * (shownRows().size() + toggleRows); + } + + // A short table keeps the options with the most ground in view, still in the order the noise meets them. + private List shownRows() { + if (showsAllRows || !isLongTable()) { + return rows; + } + Set largest = rows.stream() + .sorted(Comparator.comparingDouble(NoisePickSpread.Row::actualShare).reversed()) + .limit(SHORT_TABLE_ROWS) + .collect(Collectors.toSet()); + return rows.stream().filter(largest::contains).toList(); + } + + private void listenForShowAllToggle() { + MouseAdapter toggle = new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + if (showAllToggleBounds == null || !showAllToggleBounds.contains(event.getPoint())) { + return; + } + showsAllRows = !showsAllRows; + onShowAllRowsChanged.accept(showsAllRows); + revalidate(); + repaint(); + } + + @Override + public void mouseMoved(MouseEvent event) { + boolean overToggle = showAllToggleBounds != null && showAllToggleBounds.contains(event.getPoint()); + setCursor(overToggle ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : null); + } + }; + addMouseListener(toggle); + addMouseMotionListener(toggle); + } + + private void drawSingleOption(Graphics2D canvas, int x, int top, int width) { + drawSectionTitle(canvas, spread.title(), x, top + sectionMetrics().getAscent()); + int rowTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP); + NoisePickSpread.Row row = rows.getFirst(); + String only = "picked everywhere"; + int nameRight = x + width - textMetrics().stringWidth(only) - StudioTheme.scale(10); + int baseline = drawOptionName(canvas, row, x, rowTop, nameRight); + canvas.setColor(StudioTheme.MUTED_TEXT); + drawRightAligned(canvas, only, x + width, baseline); } private void drawSpread(Graphics2D canvas, int x, int top, int width) { @@ -151,20 +234,10 @@ final class NoisePickChart extends JComponent { drawRightAligned(canvas, "WEIGHT", weightRight, titleBaseline); drawRightAligned(canvas, "IN VIEW", actualRight, titleBaseline); - FontMetrics text = textMetrics(); int rowHeight = rowHeight(); - int swatchSize = text.getAscent() - text.getDescent(); int rowTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP); - List rows = spread.rows(); - for (NoisePickSpread.Row row : rows) { - int baseline = rowTop + (rowHeight + text.getAscent() - text.getDescent()) / 2; - int arc = StudioTheme.scale(3); - canvas.setColor(new Color(row.option().color())); - canvas.fillRoundRect(x, rowTop + (rowHeight - swatchSize) / 2, swatchSize, swatchSize, arc, arc); - canvas.setFont(StudioTheme.font()); - canvas.setColor(StudioTheme.TEXT); - int nameLeft = x + swatchSize + StudioTheme.scale(7); - canvas.drawString(fit(canvas, distinctName(row, rows), nameRight - nameLeft), nameLeft, baseline); + for (NoisePickSpread.Row row : shownRows()) { + int baseline = drawOptionName(canvas, row, x, rowTop, nameRight); canvas.setColor(StudioTheme.SECONDARY_TEXT); drawRightAligned(canvas, percent(row.weightShare()), weightRight, baseline); boolean farFromWeight = row.isFarBelowWeight() || row.isFarAboveWeight(); @@ -172,6 +245,35 @@ final class NoisePickChart extends JComponent { drawRightAligned(canvas, actualText(row), actualRight, baseline); rowTop += rowHeight; } + showAllToggleBounds = isLongTable() ? drawShowAllToggle(canvas, x, rowTop) : null; + } + + // Returns the row's text baseline. + private int drawOptionName(Graphics2D canvas, NoisePickSpread.Row row, int x, int rowTop, int nameRight) { + FontMetrics text = textMetrics(); + int rowHeight = rowHeight(); + int swatchSize = text.getAscent() - text.getDescent(); + int baseline = rowTop + (rowHeight + text.getAscent() - text.getDescent()) / 2; + int arc = StudioTheme.scale(3); + canvas.setColor(new Color(row.option().color())); + canvas.fillRoundRect(x, rowTop + (rowHeight - swatchSize) / 2, swatchSize, swatchSize, arc, arc); + canvas.setFont(StudioTheme.font()); + canvas.setColor(StudioTheme.TEXT); + int nameLeft = x + swatchSize + StudioTheme.scale(7); + canvas.drawString(fit(canvas, distinctName(row, rows), nameRight - nameLeft), nameLeft, baseline); + return baseline; + } + + private Rectangle drawShowAllToggle(Graphics2D canvas, int x, int rowTop) { + String text = showsAllRows + ? "Show fewer" + : "Show all " + rows.size() + " " + optionNoun.toLowerCase(Locale.ROOT); + FontMetrics metrics = textMetrics(); + int baseline = rowTop + (rowHeight() + metrics.getAscent() - metrics.getDescent()) / 2; + canvas.setFont(StudioTheme.font()); + canvas.setColor(StudioTheme.LINK); + canvas.drawString(text, x, baseline); + return new Rectangle(x, rowTop, metrics.stringWidth(text), rowHeight()); } private int rowHeight() { diff --git a/core/src/main/java/com/volmit/iris/core/gui/PickInspectorPanel.java b/core/src/main/java/com/volmit/iris/core/gui/PickInspectorPanel.java index 2a21a0c48..f40b101bc 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PickInspectorPanel.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PickInspectorPanel.java @@ -25,15 +25,20 @@ import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JComponent; import javax.swing.JLabel; +import javax.swing.Icon; import javax.swing.JPanel; +import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JToggleButton; import javax.swing.Scrollable; import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; import javax.swing.text.DefaultCaret; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Color; +import java.awt.Cursor; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; @@ -41,16 +46,23 @@ import java.awt.Graphics2D; import java.awt.GridLayout; import java.awt.Rectangle; import java.awt.RenderingHints; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.EnumMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BooleanSupplier; import java.util.function.Consumer; /** * The inspector of the region and biome views: how noise picks the regions or biomes in view, measured against what - * is in view and put in words, with a switch for how the map draws the picks. + * is in view and put in words, with a switch for how the map draws the picks. What the user opens or closes stays so + * while the view is measured again. */ final class PickInspectorPanel extends JPanel { private static final long serialVersionUID = -2874105263188240317L; @@ -66,7 +78,12 @@ final class PickInspectorPanel extends JPanel { private final JPanel cardPanel = new JPanel(cards); private final JTextArea message = wrappingText(); private final JPanel details = new ViewportWidthPanel(); + private final JScrollPane detailsScroll = StudioToolbar.scroll(details); + private final Set expandedSectionKeys = new HashSet<>(); + private final Set fullTableKeys = new HashSet<>(); private final String picksName; + private boolean sectionsChosenByUser; + private String shownDetailsTitle; /** * @param picksName what the map shows when it draws the picks: "Regions" or "Biomes" @@ -82,7 +99,7 @@ final class PickInspectorPanel extends JPanel { cardPanel.add(buildMessageCard(), MESSAGE_CARD); details.setLayout(new BoxLayout(details, BoxLayout.Y_AXIS)); details.setBackground(StudioTheme.PANEL_BACKGROUND); - cardPanel.add(StudioToolbar.scroll(details), DETAILS_CARD); + cardPanel.add(detailsScroll, DETAILS_CARD); add(cardPanel, BorderLayout.CENTER); showMeasuring(picksName); } @@ -99,13 +116,13 @@ final class PickInspectorPanel extends JPanel { void showRegionPicks(NoisePickSpread spread, List notes) { showHeader("Regions", "Region noise, split by rarity"); nameFirstDisplay("Regions"); - showDetails(List.of(new NoisePickChart(spread, "Regions")), notes); + showDetails(List.of(noiseChart(spread, "Regions", "regions")), notes); } void showBiomePicks(String regionName, List spreads, List notes) { showHeader(regionName, "Biome terrain · biome noise, split by rarity"); nameFirstDisplay("Biomes"); - showDetails(spreads.stream().map(spread -> (JComponent) new NoisePickChart(spread, "Biomes")).toList(), notes); + showDetails(spreads.stream().map(spread -> (JComponent) noiseChart(spread, "Biomes", spread.title())).toList(), notes); } void showCoverage(String regionName, RegionHeightCoverage coverage, List notes) { @@ -115,22 +132,36 @@ final class PickInspectorPanel extends JPanel { } /** - * One section per region in view, each with how it picks its biomes, then notes about all of them. + * One collapsible section per region in view, each with how it picks its biomes, then notes about all of them. + * Until the user opens or closes one, only the first is open. */ void showRegionSections(String detailText, List
sections, List notes) { showHeader(picksName, detailText); nameFirstDisplay(picksName); + if (!sectionsChosenByUser) { + expandedSectionKeys.clear(); + sections.stream().findFirst().ifPresent(first -> expandedSectionKeys.add(first.key())); + } List components = new ArrayList<>(); for (Section section : sections) { - components.add(new SectionHeader(section)); - components.addAll(section.charts()); - if (!section.notes().isEmpty()) { - components.add(notesText(section.notes())); - } + components.add(new SectionPanel(section, expandedSectionKeys.contains(section.key()))); } showDetails(components, notes); } + /** + * A chart whose long table remembers, by this key, whether the user asked for all its rows. + */ + NoisePickChart noiseChart(NoisePickSpread spread, String optionNoun, String key) { + return new NoisePickChart(spread, optionNoun, fullTableKeys.contains(key), showsAll -> { + if (showsAll) { + fullTableKeys.add(key); + } else { + fullTableKeys.remove(key); + } + }); + } + static RegionHeightChart heightChart(RegionHeightCoverage coverage) { RegionHeightChart chart = new RegionHeightChart(); chart.setAlignmentX(LEFT_ALIGNMENT); @@ -195,12 +226,25 @@ final class PickInspectorPanel extends JPanel { detail.setText(detailText); } + private void rememberSectionExpanded(String key, boolean expanded) { + sectionsChosenByUser = true; + if (expanded) { + expandedSectionKeys.add(key); + } else { + expandedSectionKeys.remove(key); + } + } + private void showMessage(String text) { message.setText(text); cards.show(cardPanel, MESSAGE_CARD); } + // Measuring the same subject again keeps the scroll position, so panning the map doesn't jump the list. private void showDetails(List charts, List notes) { + boolean sameSubject = Objects.equals(title.getText(), shownDetailsTitle); + int scrollPosition = sameSubject ? detailsScroll.getVerticalScrollBar().getValue() : 0; + shownDetailsTitle = title.getText(); details.removeAll(); charts.forEach(details::add); if (!notes.isEmpty()) { @@ -208,8 +252,8 @@ final class PickInspectorPanel extends JPanel { } details.revalidate(); details.repaint(); - details.scrollRectToVisible(new Rectangle()); cards.show(cardPanel, DETAILS_CARD); + SwingUtilities.invokeLater(() -> detailsScroll.getVerticalScrollBar().setValue(scrollPosition)); } private static JTextArea notesText(List notes) { @@ -243,26 +287,73 @@ final class PickInspectorPanel extends JPanel { /** * One region of the biome view's inspector. * - * @param share how much of the view the region covers + * @param share how much of the view the region covers + * @param summary what the closed section says about its content, like "7 biomes" */ - record Section(String name, int color, double share, List charts, List notes) { + record Section(String key, String name, int color, double share, String summary, List charts, + List notes) { } /** - * A region's name in its colour above its charts, with how much of the view it covers. + * A region's header, which opens and closes the charts and notes below it. + */ + private final class SectionPanel extends JPanel { + private static final long serialVersionUID = 2318546098861530717L; + + private final JPanel body = new JPanel(); + + private SectionPanel(Section section, boolean expanded) { + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + setOpaque(false); + setAlignmentX(LEFT_ALIGNMENT); + body.setLayout(new BoxLayout(body, BoxLayout.Y_AXIS)); + body.setOpaque(false); + body.setAlignmentX(LEFT_ALIGNMENT); + section.charts().forEach(body::add); + if (!section.notes().isEmpty()) { + body.add(notesText(section.notes())); + } + body.setVisible(expanded); + add(new SectionHeader(section, body::isVisible, () -> toggle(section))); + add(body); + } + + private void toggle(Section section) { + body.setVisible(!body.isVisible()); + rememberSectionExpanded(section.key(), body.isVisible()); + revalidate(); + repaint(); + } + } + + /** + * A region's name in its colour behind an open or closed chevron, with what it holds and how much of the view it + * covers; clicking it opens or closes the region. */ private static final class SectionHeader extends JComponent { private static final long serialVersionUID = -4730226930154581123L; - private static final int HEIGHT = 34; + private static final int HEIGHT = 32; private static final int MARGIN = 16; + private static final int CHEVRON_WIDTH = 16; private static final int SWATCH_SIZE = 12; private static final int SWATCH_GAP = 8; + private static final int SUMMARY_GAP = 8; private final Section section; + private final BooleanSupplier expanded; - private SectionHeader(Section section) { + private SectionHeader(Section section, BooleanSupplier expanded, Runnable onToggle) { this.section = section; + this.expanded = expanded; setAlignmentX(LEFT_ALIGNMENT); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + onToggle.run(); + repaint(); + } + }); } @Override @@ -283,24 +374,46 @@ final class PickInspectorPanel extends JPanel { canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); canvas.setColor(StudioTheme.SEPARATOR); canvas.fillRect(0, 0, getWidth(), 1); - int margin = StudioTheme.scale(MARGIN); + int left = StudioTheme.scale(MARGIN) - StudioTheme.scale(4); + paintChevron(canvas, left); + left += StudioTheme.scale(CHEVRON_WIDTH); int swatch = StudioTheme.scale(SWATCH_SIZE); canvas.setColor(new Color(section.color())); int arc = StudioTheme.scale(4); - canvas.fillRoundRect(margin, (getHeight() - swatch) / 2, swatch, swatch, arc, arc); - canvas.setFont(StudioTheme.boldFont()); - FontMetrics metrics = canvas.getFontMetrics(); - int baseline = (getHeight() + metrics.getAscent() - metrics.getDescent()) / 2; - canvas.setColor(StudioTheme.TEXT); - canvas.drawString(section.name(), margin + swatch + StudioTheme.scale(SWATCH_GAP), baseline); - String share = NoisePickChart.percent(section.share()) + " of view"; + canvas.fillRoundRect(left, (getHeight() - swatch) / 2, swatch, swatch, arc, arc); + left += swatch + StudioTheme.scale(SWATCH_GAP); + canvas.setFont(StudioTheme.smallFont()); + FontMetrics small = canvas.getFontMetrics(); + String share = NoisePickChart.percent(section.share()); + int shareLeft = getWidth() - StudioTheme.scale(MARGIN) - small.stringWidth(share); + canvas.setFont(StudioTheme.boldFont()); + FontMetrics bold = canvas.getFontMetrics(); + int baseline = (getHeight() + bold.getAscent() - bold.getDescent()) / 2; + canvas.setColor(StudioTheme.TEXT); + canvas.drawString(section.name(), left, baseline); + left += bold.stringWidth(section.name()) + StudioTheme.scale(SUMMARY_GAP); + + canvas.setFont(StudioTheme.smallFont()); + canvas.setColor(StudioTheme.MUTED_TEXT); + if (section.summary() != null && left + small.stringWidth(section.summary()) < shareLeft - StudioTheme.scale(SUMMARY_GAP)) { + canvas.drawString(section.summary(), left, baseline); + } canvas.setColor(StudioTheme.SECONDARY_TEXT); - canvas.drawString(share, getWidth() - margin - canvas.getFontMetrics().stringWidth(share), baseline); + canvas.drawString(share, shareLeft, baseline); } finally { canvas.dispose(); } } + + private void paintChevron(Graphics2D canvas, int left) { + Icon chevron = UIManager.getIcon(expanded.getAsBoolean() ? "Tree.expandedIcon" : "Tree.collapsedIcon"); + if (chevron == null) { + return; + } + chevron.paintIcon(this, canvas, left + (StudioTheme.scale(CHEVRON_WIDTH) - chevron.getIconWidth()) / 2, + (getHeight() - chevron.getIconHeight()) / 2); + } } /** diff --git a/core/src/main/java/com/volmit/iris/core/gui/PickMeasuring.java b/core/src/main/java/com/volmit/iris/core/gui/PickMeasuring.java index 49c826236..3b62108f1 100644 --- a/core/src/main/java/com/volmit/iris/core/gui/PickMeasuring.java +++ b/core/src/main/java/com/volmit/iris/core/gui/PickMeasuring.java @@ -31,7 +31,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Locale; -import java.util.stream.Collectors; /** * Samples the map in view to measure how regions and biomes are picked, and puts what stands out in words; shared by @@ -40,6 +39,7 @@ import java.util.stream.Collectors; final class PickMeasuring { static final List PLACED_TYPES = List.of(InferredType.LAND, InferredType.SHORE, InferredType.SEA); private static final int SAMPLES_PER_SIDE = 96; + private static final int NAMED_IN_NOTES = 3; private PickMeasuring() { } @@ -106,7 +106,6 @@ final class PickMeasuring { return; } if (rows.size() == 1) { - notes.add(rows.getFirst().option().name() + " is the only " + noun + ", so the " + noun + " noise has nothing to pick."); return; } if (spread.belowZeroShare() > 0D) { @@ -120,7 +119,7 @@ final class PickMeasuring { List starved = rows.stream().filter(NoisePickSpread.Row::isFarBelowWeight).map(row -> row.option().name()).toList(); if (!starved.isEmpty()) { boolean single = starved.size() == 1; - notes.add(String.join(", ", starved) + (single ? " gets" : " get") + " under half " + (single ? "its" : "their") + notes.add(namedFew(starved) + (single ? " gets" : " get") + " under half " + (single ? "its" : "their") + " weight in view (▼): the " + noun + " noise rarely reaches " + (single ? "its band." : "their bands.")); } } @@ -132,14 +131,14 @@ final class PickMeasuring { } static void addChildBiomeNote(List notes, IrisComplex complex, IrisRegion region) { - String parents = PLACED_TYPES.stream() + List parents = PLACED_TYPES.stream() .flatMap(type -> complex.noisePickedBiomes(region, type).stream()) .filter(biome -> !biome.getChildren().isEmpty()) .map(IrisBiome::getName) .distinct() - .collect(Collectors.joining(", ")); + .toList(); if (!parents.isEmpty()) { - notes.add(parents + " then hand some spots to child biomes by a noise of their own."); + notes.add(namedFew(parents) + " then hand some spots to child biomes by a noise of their own."); } } @@ -152,6 +151,14 @@ final class PickMeasuring { : "Each weight is one over the rarity, so rarity 2 gets half the noise of rarity 1."); } + // A long list of names buries the point of a note, so only the first few are named. + private static String namedFew(List names) { + if (names.size() <= NAMED_IN_NOTES) { + return String.join(", ", names); + } + return String.join(", ", names.subList(0, NAMED_IN_NOTES)) + " and " + (names.size() - NAMED_IN_NOTES) + " more"; + } + static String measuredNote() { return "Measured on a " + SAMPLES_PER_SIDE + " × " + SAMPLES_PER_SIDE + " grid over the map in view."; }