Size the studio windows for high DPI screens

FlatLaf grows fonts and its own controls to the screen's DPI, but the
sizes the studio set itself stayed fixed, so on a 192 DPI screen text
was cut off and overlapped. Every padding, row height, column, card and
legend now grows with the same scale or follows the font, the windows
open at a scaled size that still fits the screen, and the theme is
installed before any window builds its controls. Long status text now
shortens with an ellipsis instead of pushing the rest of the bar away.

Model used: Claude Opus 5.5
This commit is contained in:
piexel committed 2026-10-19 11:05:55 +02:00
1 parent 80e7e37791
commit b1445430bb
11 files changed
+251 -137

No files matched your search

@@ -31,8 +31,10 @@ import org.bukkit.event.Listener;
import javax.swing.JFrame;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.desktop.QuitResponse;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
@@ -73,6 +75,12 @@ public final class GuiHost {
return frame;
}
// A scaled-up default size can be bigger than a small or low resolution screen.
static Dimension fitToScreen(Dimension preferred) {
Rectangle usable = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
return new Dimension(Math.min(preferred.width, usable.width), Math.min(preferred.height, usable.height));
}
public static Engine findActiveEngine() {
try {
for (World world : new ArrayList<>(Bukkit.getWorlds())) {
@@ -529,7 +529,7 @@ final class MapCanvas extends JPanel {
for (GuiMarker marker : entities) {
int screenX = (int) Math.round(worldToScreenX(marker.worldX()));
int screenY = (int) Math.round(worldToScreenZ(marker.worldZ()));
canvas.fillOval(screenX - 3, screenY - 3, 6, 6);
fillDot(canvas, screenX, screenY, 3);
}
}
@@ -538,18 +538,23 @@ final class MapCanvas extends JPanel {
int screenX = (int) Math.round(worldToScreenX(marker.worldX()));
int screenY = (int) Math.round(worldToScreenZ(marker.worldZ()));
canvas.setColor(PLAYER_HALO);
canvas.fillOval(screenX - 12, screenY - 12, 24, 24);
fillDot(canvas, screenX, screenY, 12);
canvas.setColor(PLAYER_COLOR);
canvas.fillOval(screenX - 5, screenY - 5, 10, 10);
fillDot(canvas, screenX, screenY, 5);
canvas.setColor(StudioTheme.TEXT);
int labelWidth = canvas.getFontMetrics().stringWidth(marker.label());
canvas.drawString(marker.label(), screenX - labelWidth / 2, screenY - 15);
canvas.drawString(marker.label(), screenX - labelWidth / 2, screenY - StudioTheme.scale(15));
}
if (entitiesVisible && detailedHover) {
renderNearestEntity(canvas);
}
}
private static void fillDot(Graphics2D canvas, int centerX, int centerY, int logicalRadius) {
int radius = StudioTheme.scale(logicalRadius);
canvas.fillOval(centerX - radius, centerY - radius, radius * 2, radius * 2);
}
private void renderNearestEntity(Graphics2D canvas) {
Point point = cursor;
if (point == null || entities.isEmpty()) {
@@ -577,34 +582,45 @@ final class MapCanvas extends JPanel {
if (nearest.maxHealth() > 0D) {
lines.add("Health " + Form.f(nearest.health(), 1) + " / " + Form.f(nearest.maxHealth(), 1));
}
drawCard(canvas, getWidth() - CARD_PADDING, CARD_PADDING, 1D, 0D, lines);
int padding = StudioTheme.scale(CARD_PADDING);
drawCard(canvas, getWidth() - padding, padding, 1D, 0D, lines);
}
private void renderLegend(Graphics2D canvas) {
if (currentType != RenderType.HEIGHT) {
return;
}
int width = 244;
int height = 54;
int x = getWidth() - width - CARD_PADDING;
int y = getHeight() - height - CARD_PADDING;
canvas.setFont(StudioTheme.smallFont());
int padding = StudioTheme.scale(CARD_PADDING);
int gradientHeight = StudioTheme.scale(14);
int labelGap = StudioTheme.scale(4);
int width = StudioTheme.scale(244);
int height = padding * 2 + gradientHeight + labelGap + canvas.getFontMetrics().getHeight();
int x = getWidth() - width - padding;
int y = getHeight() - height - padding;
StudioOverlay.drawCard(canvas, x, y, width, height);
int gradientX = x + CARD_PADDING;
int gradientY = y + CARD_PADDING;
int gradientWidth = width - CARD_PADDING * 2;
int gradientX = x + padding;
int gradientY = y + padding;
int gradientWidth = width - padding * 2;
for (int pixel = 0; pixel < gradientWidth; pixel++) {
double sampleHeight = heightMaximum * pixel / Math.max(1D, gradientWidth - 1D);
canvas.setColor(new Color(IrisRenderer.heightColor(sampleHeight, heightMaximum, fluidHeight)));
canvas.drawLine(gradientX + pixel, gradientY, gradientX + pixel, gradientY + 14);
canvas.drawLine(gradientX + pixel, gradientY, gradientX + pixel, gradientY + gradientHeight);
}
canvas.setFont(StudioTheme.smallFont());
canvas.setColor(StudioTheme.SECONDARY_TEXT);
canvas.drawString("0", gradientX, gradientY + 32);
int labelBaseline = gradientY + gradientHeight + labelGap + canvas.getFontMetrics().getAscent();
String minimum = "0";
String fluid = Integer.toString((int) Math.round(fluidHeight));
int fluidX = gradientX + (int) Math.round(gradientWidth * fluidHeight / Math.max(1D, heightMaximum));
canvas.drawString(fluid, Math.max(gradientX, fluidX - canvas.getFontMetrics().stringWidth(fluid) / 2), gradientY + 32);
String maximum = Integer.toString((int) Math.round(heightMaximum));
canvas.drawString(maximum, gradientX + gradientWidth - canvas.getFontMetrics().stringWidth(maximum), gradientY + 32);
int minimumRight = gradientX + canvas.getFontMetrics().stringWidth(minimum);
int maximumX = gradientX + gradientWidth - canvas.getFontMetrics().stringWidth(maximum);
int fluidWidth = canvas.getFontMetrics().stringWidth(fluid);
int fluidX = gradientX + (int) Math.round(gradientWidth * fluidHeight / Math.max(1D, heightMaximum)) - fluidWidth / 2;
canvas.drawString(minimum, gradientX, labelBaseline);
if (fluidX > minimumRight + labelGap && fluidX + fluidWidth < maximumX - labelGap) {
canvas.drawString(fluid, fluidX, labelBaseline);
}
canvas.drawString(maximum, maximumX, labelBaseline);
}
private void renderHover(Graphics2D canvas) {
@@ -624,7 +640,7 @@ final class MapCanvas extends JPanel {
} else {
lines.add("Hold Shift for more, double-click for palettes");
}
int screenX = (int) Math.round(worldToScreenX(info.worldX())) + 16;
int screenX = (int) Math.round(worldToScreenX(info.worldX())) + StudioTheme.scale(16);
int screenY = (int) Math.round(worldToScreenZ(info.worldZ()));
drawCard(canvas, screenX, screenY, 0D, 0D, lines);
}
@@ -659,18 +675,20 @@ final class MapCanvas extends JPanel {
for (String description : descriptions) {
descriptionWidth = Math.max(descriptionWidth, canvas.getFontMetrics().stringWidth(description));
}
int lineHeight = 22;
int width = keyWidth + descriptionWidth + CARD_PADDING * 3 + 8;
int height = keys.size() * lineHeight + CARD_PADDING * 2;
StudioOverlay.drawCard(canvas, CARD_PADDING, CARD_PADDING, width, height);
int padding = StudioTheme.scale(CARD_PADDING);
int columnGap = StudioTheme.scale(18);
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);
for (int index = 0; index < keys.size(); index++) {
int y = CARD_PADDING * 2 + 10 + index * lineHeight;
int y = padding * 2 + canvas.getFontMetrics().getAscent() + index * lineHeight;
canvas.setFont(StudioTheme.boldFont());
canvas.setColor(StudioTheme.LINK);
canvas.drawString(keys.get(index), CARD_PADDING * 2, y);
canvas.drawString(keys.get(index), padding * 2, y);
canvas.setFont(StudioTheme.font());
canvas.setColor(StudioTheme.SECONDARY_TEXT);
canvas.drawString(descriptions.get(index), CARD_PADDING * 2 + keyWidth + 18, y);
canvas.drawString(descriptions.get(index), padding * 2 + keyWidth + columnGap, y);
}
}
@@ -682,7 +700,8 @@ final class MapCanvas extends JPanel {
lines.add("Render threads busy " + progress.activeWorkers());
lines.add("Tile scale " + (frame == null ? "-" : Form.f(frame.renderBlocksPerPixel(), 0)) + " blocks/px");
lines.add(paintCadenceFps + " paints per second");
drawCard(canvas, CARD_PADDING, getHeight() - CARD_PADDING, 0D, 1D, lines);
int padding = StudioTheme.scale(CARD_PADDING);
drawCard(canvas, padding, getHeight() - padding, 0D, 1D, lines);
}
private void renderNotifications(Graphics2D canvas) {
@@ -692,13 +711,14 @@ final class MapCanvas extends JPanel {
String text = String.join(" · ", notifications.keySet());
canvas.setFont(StudioTheme.font());
int textWidth = canvas.getFontMetrics().stringWidth(text);
int width = textWidth + CARD_PADDING * 2;
int height = 34;
int padding = StudioTheme.scale(CARD_PADDING);
int width = textWidth + padding * 2;
int height = canvas.getFontMetrics().getHeight() + padding;
int x = (getWidth() - width) / 2;
int y = getHeight() - height - 16;
int y = getHeight() - height - StudioTheme.scale(16);
StudioOverlay.drawCard(canvas, x, y, width, height);
canvas.setColor(StudioTheme.TEXT);
canvas.drawString(text, x + CARD_PADDING, y + 22);
canvas.drawString(text, x + padding, y + padding / 2 + canvas.getFontMetrics().getAscent());
}
private void drawCard(Graphics2D canvas, int anchorX, int anchorY, double pushX, double pushY, List<String> lines) {
@@ -709,18 +729,19 @@ final class MapCanvas extends JPanel {
canvas.setFont(index == 0 ? StudioTheme.boldFont() : StudioTheme.font());
width = Math.max(width, canvas.getFontMetrics().stringWidth(lines.get(index)));
}
width += CARD_PADDING * 2;
int height = lines.size() * lineHeight + CARD_PADDING * 2 - 2;
int padding = StudioTheme.scale(CARD_PADDING);
width += padding * 2;
int height = lines.size() * lineHeight + padding * 2;
int x = (int) Math.round(anchorX - width * pushX);
int y = (int) Math.round(anchorY - height * pushY);
x = Math.max(CARD_PADDING, Math.min(getWidth() - width - CARD_PADDING, x));
y = Math.max(CARD_PADDING, Math.min(getHeight() - height - CARD_PADDING, y));
x = Math.max(padding, Math.min(getWidth() - width - padding, x));
y = Math.max(padding, Math.min(getHeight() - height - padding, y));
StudioOverlay.drawCard(canvas, x, y, width, height);
for (int index = 0; index < lines.size(); index++) {
boolean title = index == 0;
canvas.setFont(title ? StudioTheme.boldFont() : StudioTheme.font());
canvas.setColor(title ? StudioTheme.TEXT : StudioTheme.SECONDARY_TEXT);
canvas.drawString(lines.get(index), x + CARD_PADDING, y + CARD_PADDING + lineHeight - 3 + index * lineHeight);
canvas.drawString(lines.get(index), x + padding, y + padding + canvas.getFontMetrics().getAscent() + index * lineHeight);
}
}
@@ -44,9 +44,9 @@ final class MapViewList extends JList<MapViewList.Row> {
fillRows();
setModel(rows);
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
setFixedCellHeight(ROW_HEIGHT);
setFixedCellHeight(StudioTheme.scale(ROW_HEIGHT));
setCellRenderer(new RowRenderer());
setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6));
setBorder(StudioTheme.emptyBorder(4, 6, 4, 6));
addListSelectionListener(event -> {
if (event.getValueIsAdjusting() || skipHeaderRow()) {
return;
@@ -133,13 +133,13 @@ final class MapViewList extends JList<MapViewList.Row> {
setText(row.group().label().toUpperCase(Locale.ROOT));
setFont(StudioTheme.sectionFont());
setForeground(StudioTheme.MUTED_TEXT);
setBorder(BorderFactory.createEmptyBorder(index == 0 ? 0 : 6, 6, 0, 6));
setBorder(StudioTheme.emptyBorder(index == 0 ? 0 : 6, 6, 0, 6));
setToolTipText(null);
shortcut = null;
return this;
}
setFont(StudioTheme.font());
setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
setBorder(StudioTheme.emptyBorder(0, 10, 0, 10));
setToolTipText(row.view().description());
shortcut = row.view().shortcutText();
return this;
@@ -93,6 +93,7 @@ public final class MapWindow implements MapCanvas.Listener {
}
private static void open(PaletteExplorerStart start, MapView initialView) {
StudioTheme.install();
MapWindow window = new MapWindow(start);
window.frame.setVisible(true);
window.canvas.start();
@@ -102,7 +103,7 @@ public final class MapWindow implements MapCanvas.Listener {
private void buildFrame() {
content.add(canvas, MAP_CARD);
statusBars.add(canvas.statusBar(), MAP_CARD);
sidebar.setPreferredSize(new Dimension(SIDEBAR_WIDTH, 0));
sidebar.setPreferredSize(StudioTheme.scaledSize(SIDEBAR_WIDTH, 0));
sidebar.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, StudioTheme.SEPARATOR));
sidebar.add(viewListScroll, BorderLayout.CENTER);
@@ -113,8 +114,8 @@ public final class MapWindow implements MapCanvas.Listener {
frame.add(inspectorSlot, BorderLayout.EAST);
frame.add(statusBars, BorderLayout.SOUTH);
installShortcuts(frame.getRootPane());
frame.setSize(1440, 880);
frame.setMinimumSize(new Dimension(820, 520));
frame.setSize(GuiHost.fitToScreen(StudioTheme.scaledSize(1440, 880)));
frame.setMinimumSize(StudioTheme.scaledSize(820, 520));
frame.setLocationRelativeTo(null);
frame.addWindowListener(new WindowAdapter() {
@Override
@@ -127,7 +128,7 @@ public final class MapWindow implements MapCanvas.Listener {
private JToolBar buildToolbar() {
JToolBar toolbar = StudioToolbar.create();
viewTitle.setFont(StudioTheme.titleFont());
viewTitle.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 14));
viewTitle.setBorder(StudioTheme.emptyBorder(0, 6, 0, 14));
toolbar.add(viewTitle);
mapControls.add(StudioToolbar.button("Refresh", "Redraw the map with the latest pack files (R)", canvas::refreshContent));
@@ -88,7 +88,6 @@ public final class NoiseExplorerGUI extends JPanel {
private static final Color CROSSHAIR = new Color(255, 255, 255, 125);
private static final int SIDEBAR_WIDTH = 270;
private static final int LEGEND_WIDTH = 190;
private static final int LEGEND_HEIGHT = 40;
private static final int REFINEMENT_DELAY_MILLIS = 180;
private static final long DEFAULT_SEED = 12345L;
private static final double TEXTURE_PIXELS_MAX_BLOCKS_PER_PIXEL = 0.25D;
@@ -212,6 +211,7 @@ public final class NoiseExplorerGUI extends JPanel {
public static void launch() {
Engine engine = GuiHost.findActiveEngine();
EventQueue.invokeLater(() -> {
StudioTheme.install();
NoiseExplorerGUI explorer = new NoiseExplorerGUI(ExplorerMode.NOISE, engine, null, null, null, DEFAULT_SEED, null);
buildNoiseFrame("Noise Explorer", explorer);
});
@@ -221,6 +221,7 @@ public final class NoiseExplorerGUI extends JPanel {
Engine engine = GuiHost.findActiveEngine();
String displayName = generatorKey == null || generatorKey.isBlank() ? "Custom Generator" : generatorKey;
EventQueue.invokeLater(() -> {
StudioTheme.install();
NoiseExplorerGUI explorer = new NoiseExplorerGUI(
ExplorerMode.NOISE,
engine,
@@ -253,8 +254,8 @@ public final class NoiseExplorerGUI extends JPanel {
explorer.resetViewport();
}
});
frame.setSize(1440, 860);
frame.setMinimumSize(new Dimension(760, 500));
frame.setSize(GuiHost.fitToScreen(StudioTheme.scaledSize(1440, 860)));
frame.setMinimumSize(StudioTheme.scaledSize(760, 500));
frame.setLocationRelativeTo(null);
frame.addWindowListener(new WindowAdapter() {
@Override
@@ -295,7 +296,7 @@ public final class NoiseExplorerGUI extends JPanel {
private JToolBar buildNoiseToolbar() {
JToolBar toolbar = StudioToolbar.create();
selectedLabel.setFont(StudioTheme.boldFont());
selectedLabel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 10));
selectedLabel.setBorder(StudioTheme.emptyBorder(0, 4, 0, 10));
toolbar.add(selectedLabel);
toolbar.addSeparator();
toolbar.add(StudioToolbar.label("Seed"));
@@ -322,7 +323,7 @@ public final class NoiseExplorerGUI extends JPanel {
private JPanel buildNoiseSidebar() {
JPanel sidebar = new JPanel(new BorderLayout());
sidebar.setPreferredSize(new Dimension(SIDEBAR_WIDTH, 0));
sidebar.setPreferredSize(StudioTheme.scaledSize(SIDEBAR_WIDTH, 0));
sidebar.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, StudioTheme.SEPARATOR));
StudioToolbar.makeSearchField(searchField, "Search noise...");
searchField.getDocument().addDocumentListener(new DocumentListener() {
@@ -1186,33 +1187,42 @@ public final class NoiseExplorerGUI extends JPanel {
int centerZ = height / 2;
canvas.setColor(CROSSHAIR);
canvas.setStroke(new BasicStroke(1F));
canvas.drawLine(centerX - 8, centerZ, centerX + 8, centerZ);
canvas.drawLine(centerX, centerZ - 8, centerX, centerZ + 8);
int armLength = StudioTheme.scale(8);
canvas.drawLine(centerX - armLength, centerZ, centerX + armLength, centerZ);
canvas.drawLine(centerX, centerZ - armLength, centerX, centerZ + armLength);
}
private void drawLegend(Graphics2D canvas, int width) {
NoiseColorScale colorScale = activeColorScale();
int legendX = Math.max(10, width - LEGEND_WIDTH - 12);
int legendY = 12;
canvas.setFont(StudioTheme.smallFont());
String minimum = formatNumber(colorScale.minimum());
String maximum = formatNumber(colorScale.maximum());
String label = colorScale.label();
int padding = StudioTheme.scale(9);
int labelGap = StudioTheme.scale(12);
int gradientHeight = StudioTheme.scale(8);
int textWidth = canvas.getFontMetrics().stringWidth(minimum) + canvas.getFontMetrics().stringWidth(label)
+ canvas.getFontMetrics().stringWidth(maximum) + labelGap * 2;
int legendWidth = Math.max(StudioTheme.scale(LEGEND_WIDTH), textWidth + padding * 2);
int legendHeight = padding * 2 + gradientHeight + StudioTheme.scale(4) + canvas.getFontMetrics().getHeight();
int legendX = Math.max(StudioTheme.scale(10), width - legendWidth - StudioTheme.scale(12));
int legendY = StudioTheme.scale(12);
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
StudioOverlay.drawCard(canvas, legendX, legendY, LEGEND_WIDTH, LEGEND_HEIGHT);
int gradientX = legendX + 9;
int gradientY = legendY + 8;
int gradientWidth = LEGEND_WIDTH - 18;
StudioOverlay.drawCard(canvas, legendX, legendY, legendWidth, legendHeight);
int gradientX = legendX + padding;
int gradientY = legendY + padding;
int gradientWidth = legendWidth - padding * 2;
for (int x = 0; x < gradientWidth; x++) {
double normalized = x / (double) Math.max(1, gradientWidth - 1);
canvas.setColor(colorScale.displayColorNormalized(normalized));
canvas.drawLine(gradientX + x, gradientY, gradientX + x, gradientY + 8);
canvas.drawLine(gradientX + x, gradientY, gradientX + x, gradientY + gradientHeight);
}
canvas.setFont(StudioTheme.smallFont());
int baseline = gradientY + gradientHeight + StudioTheme.scale(4) + canvas.getFontMetrics().getAscent();
canvas.setColor(StudioTheme.TEXT);
canvas.drawString(formatNumber(colorScale.minimum()), gradientX, legendY + 31);
String maximum = formatNumber(colorScale.maximum());
int maximumWidth = canvas.getFontMetrics().stringWidth(maximum);
canvas.drawString(maximum, gradientX + gradientWidth - maximumWidth, legendY + 31);
String label = colorScale.label();
canvas.drawString(minimum, gradientX, baseline);
canvas.drawString(maximum, gradientX + gradientWidth - canvas.getFontMetrics().stringWidth(maximum), baseline);
int labelWidth = canvas.getFontMetrics().stringWidth(label);
canvas.drawString(label, legendX + ((LEGEND_WIDTH - labelWidth) / 2), legendY + 31);
canvas.drawString(label, legendX + (legendWidth - labelWidth) / 2, baseline);
}
private void refreshStatus() {
@@ -1513,10 +1523,10 @@ public final class NoiseExplorerGUI extends JPanel {
if (header) {
setFont(StudioTheme.sectionFont());
setForeground(StudioTheme.MUTED_TEXT);
setBorder(BorderFactory.createEmptyBorder(12, 12, 4, 12));
setBorder(StudioTheme.emptyBorder(12, 12, 4, 12));
} else {
setFont(StudioTheme.font());
setBorder(BorderFactory.createEmptyBorder(3, 20, 3, 12));
setBorder(StudioTheme.emptyBorder(3, 20, 3, 12));
}
setHorizontalAlignment(SwingConstants.LEFT);
return this;
@@ -51,7 +51,7 @@ final class PaletteInspectorPanel extends JPanel {
private final JLabel depthLabel = new JLabel("", SwingConstants.CENTER);
private final JButton shallowerButton = new JButton("‹");
private final JButton deeperButton = new JButton("›");
private final JPanel depthRow = new JPanel(new BorderLayout(6, 0));
private final JPanel depthRow = new JPanel(new BorderLayout(StudioTheme.scale(6), 0));
private final StatisticsView statisticsView = new StatisticsView();
private PaletteLayerScale.View selectedView = PaletteLayerScale.View.BLOCKS;
private int depth;
@@ -60,7 +60,7 @@ final class PaletteInspectorPanel extends JPanel {
PaletteInspectorPanel(Listener listener) {
super(new BorderLayout());
this.listener = listener;
setPreferredSize(new Dimension(PANEL_WIDTH, 0));
setPreferredSize(StudioTheme.scaledSize(PANEL_WIDTH, 0));
setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, StudioTheme.SEPARATOR));
add(buildControls(), BorderLayout.NORTH);
add(StudioToolbar.scroll(statisticsView), BorderLayout.CENTER);
@@ -91,10 +91,10 @@ final class PaletteInspectorPanel extends JPanel {
}
private JPanel buildControls() {
JPanel controls = new JPanel(new GridLayout(0, 1, 0, 6));
JPanel controls = new JPanel(new GridLayout(0, 1, 0, StudioTheme.scale(6)));
controls.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 0, StudioTheme.SEPARATOR),
BorderFactory.createEmptyBorder(12, 14, 12, 14)
StudioTheme.emptyBorder(12, 14, 12, 14)
));
titleLabel.setFont(StudioTheme.titleFont());
detailLabel.setForeground(StudioTheme.SECONDARY_TEXT);
@@ -110,7 +110,7 @@ final class PaletteInspectorPanel extends JPanel {
}
private JPanel buildViewButtons() {
JPanel row = new JPanel(new GridLayout(1, 0, 4, 0));
JPanel row = new JPanel(new GridLayout(1, 0, StudioTheme.scale(4), 0));
row.setOpaque(false);
ButtonGroup group = new ButtonGroup();
for (PaletteLayerScale.View view : PaletteLayerScale.View.values()) {
@@ -198,11 +198,11 @@ final class PaletteInspectorPanel extends JPanel {
private static final int MARGIN = 14;
private static final int HISTOGRAM_HEIGHT = 84;
private static final int BAND_HEIGHT = 12;
private static final int ROW_HEIGHT = 20;
private static final int SWATCH_SIZE = 14;
private static final int WEIGHT_COLUMN_WIDTH = 48;
private static final int ACTUAL_COLUMN_WIDTH = 70;
private static final int NOTE_LINE_HEIGHT = 15;
private static final int SECTION_GAP = 18;
private static final int TITLE_GAP = 6;
private static final int NOTE_GAP = 4;
private static final String WIDEST_WEIGHT = "100.0%";
private static final String WIDEST_ACTUAL = "▲ 100.0%";
private PaletteLayerPreview preview;
private String textureStatus;
@@ -226,19 +226,15 @@ final class PaletteInspectorPanel extends JPanel {
@Override
public Dimension getPreferredSize() {
int preferredWidth = StudioTheme.scale(PANEL_WIDTH - 20);
if (preview == null) {
return new Dimension(PANEL_WIDTH - 20, 0);
return new Dimension(preferredWidth, 0);
}
int width = Math.max(getWidth(), PANEL_WIDTH - 20) - MARGIN * 2;
FontMetrics noteMetrics = getFontMetrics(StudioTheme.smallFont());
int noteLines = 0;
for (String note : notes()) {
noteLines += wrap(note, noteMetrics, width).size() + 1;
}
int height = MARGIN + 18 + HISTOGRAM_HEIGHT + BAND_HEIGHT + 30
+ 22 + ROW_HEIGHT * (preview.entries().size() + 1)
+ 16 + noteLines * NOTE_LINE_HEIGHT + MARGIN;
return new Dimension(PANEL_WIDTH - 20, height);
int margin = StudioTheme.scale(MARGIN);
int width = Math.max(getWidth(), preferredWidth) - margin * 2;
int height = margin + valueSpreadHeight() + StudioTheme.scale(SECTION_GAP) + blockTableHeight()
+ StudioTheme.scale(SECTION_GAP) + notesHeight(width) + margin;
return new Dimension(preferredWidth, height);
}
@Override
@@ -252,18 +248,58 @@ final class PaletteInspectorPanel extends JPanel {
if (preview == null) {
return;
}
int width = getWidth() - MARGIN * 2;
int y = drawValueSpread(canvas, MARGIN, MARGIN, width);
y = drawBlockTable(canvas, MARGIN, y + 22, width);
drawNotes(canvas, MARGIN, y + 16, width);
int margin = StudioTheme.scale(MARGIN);
int width = getWidth() - margin * 2;
int top = margin;
drawValueSpread(canvas, margin, top, width);
top += valueSpreadHeight() + StudioTheme.scale(SECTION_GAP);
drawBlockTable(canvas, margin, top, width);
top += blockTableHeight() + StudioTheme.scale(SECTION_GAP);
drawNotes(canvas, margin, top, width);
} finally {
canvas.dispose();
}
}
private int drawValueSpread(Graphics2D canvas, int x, int y, int width) {
drawSectionTitle(canvas, "Noise values this layer produces", x, y + 10);
int histogramTop = y + 18;
private int valueSpreadHeight() {
return sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP) + StudioTheme.scale(HISTOGRAM_HEIGHT)
+ StudioTheme.scale(2) + StudioTheme.scale(BAND_HEIGHT) + StudioTheme.scale(3) + smallMetrics().getHeight();
}
private int blockTableHeight() {
return sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP) + rowHeight() * preview.entries().size();
}
private int notesHeight(int width) {
FontMetrics metrics = smallMetrics();
int height = 0;
for (String note : notes()) {
height += wrap(note, metrics, width).size() * metrics.getHeight() + StudioTheme.scale(NOTE_GAP);
}
return height;
}
private int rowHeight() {
return textMetrics().getHeight() + StudioTheme.scale(4);
}
private FontMetrics textMetrics() {
return getFontMetrics(StudioTheme.font());
}
private FontMetrics smallMetrics() {
return getFontMetrics(StudioTheme.smallFont());
}
private FontMetrics sectionMetrics() {
return getFontMetrics(StudioTheme.sectionFont());
}
private void drawValueSpread(Graphics2D canvas, int x, int top, int width) {
drawSectionTitle(canvas, "Noise values this layer produces", x, top + sectionMetrics().getAscent());
int histogramTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP);
int histogramHeight = StudioTheme.scale(HISTOGRAM_HEIGHT);
int bandHeight = StudioTheme.scale(BAND_HEIGHT);
long[] histogram = preview.histogram();
long peak = 1L;
for (long count : histogram) {
@@ -273,11 +309,11 @@ final class PaletteInspectorPanel extends JPanel {
for (int bin = 0; bin < histogram.length; bin++) {
int barLeft = x + (bin * width) / histogram.length;
int barRight = x + ((bin + 1) * width) / histogram.length;
int barHeight = (int) Math.round(HISTOGRAM_HEIGHT * (histogram[bin] / (double) peak));
canvas.fillRect(barLeft, histogramTop + HISTOGRAM_HEIGHT - barHeight, Math.max(1, barRight - barLeft - 1), barHeight);
int barHeight = (int) Math.round(histogramHeight * (histogram[bin] / (double) peak));
canvas.fillRect(barLeft, histogramTop + histogramHeight - barHeight, Math.max(1, barRight - barLeft - 1), barHeight);
}
int bandTop = histogramTop + HISTOGRAM_HEIGHT + 2;
int bandTop = histogramTop + histogramHeight + StudioTheme.scale(2);
for (PaletteLayerPreview.Entry entry : preview.entries()) {
if (Double.isNaN(entry.lowestNoise())) {
continue;
@@ -285,67 +321,73 @@ final class PaletteInspectorPanel extends JPanel {
int bandLeft = x + (int) Math.round(entry.lowestNoise() * width);
int bandRight = x + (int) Math.round(entry.highestNoise() * width);
canvas.setColor(new Color(entry.color()));
canvas.fillRect(bandLeft, bandTop, Math.max(1, bandRight - bandLeft), BAND_HEIGHT);
canvas.fillRect(bandLeft, bandTop, Math.max(1, bandRight - bandLeft), bandHeight);
if (entry.highestNoise() < 1D) {
canvas.setColor(BAND_DIVIDER);
canvas.drawLine(bandRight, histogramTop, bandRight, bandTop + BAND_HEIGHT - 1);
canvas.drawLine(bandRight, histogramTop, bandRight, bandTop + bandHeight - 1);
}
}
canvas.setFont(StudioTheme.smallFont());
canvas.setColor(StudioTheme.SECONDARY_TEXT);
FontMetrics metrics = canvas.getFontMetrics();
int labelY = bandTop + BAND_HEIGHT + 13;
int labelY = bandTop + bandHeight + StudioTheme.scale(3) + metrics.getAscent();
canvas.drawString("0", x, labelY);
canvas.drawString("0.5", x + (width - metrics.stringWidth("0.5")) / 2, labelY);
canvas.drawString("1", x + width - metrics.stringWidth("1"), labelY);
return labelY;
}
private int drawBlockTable(Graphics2D canvas, int x, int y, int width) {
private void drawBlockTable(Graphics2D canvas, int x, int top, int width) {
int columnGap = StudioTheme.scale(10);
int actualRight = x + width;
int weightRight = actualRight - ACTUAL_COLUMN_WIDTH;
drawSectionTitle(canvas, "Blocks", x, y);
drawRightAligned(canvas, "weight", weightRight, y);
drawRightAligned(canvas, "actual", actualRight, y);
int weightRight = actualRight - textMetrics().stringWidth(WIDEST_ACTUAL) - columnGap;
int nameRight = weightRight - textMetrics().stringWidth(WIDEST_WEIGHT) - columnGap;
int titleBaseline = top + sectionMetrics().getAscent();
drawSectionTitle(canvas, "Blocks", x, titleBaseline);
drawRightAligned(canvas, "weight", weightRight, titleBaseline);
drawRightAligned(canvas, "actual", actualRight, titleBaseline);
int rowY = y + ROW_HEIGHT;
int rowHeight = rowHeight();
FontMetrics text = textMetrics();
int swatchSize = text.getAscent() - text.getDescent();
int rowTop = top + sectionMetrics().getHeight() + StudioTheme.scale(TITLE_GAP);
for (PaletteLayerPreview.Entry entry : preview.entries()) {
drawSwatch(canvas, entry, x, rowY - SWATCH_SIZE + 2);
int baseline = rowTop + (rowHeight + text.getAscent() - text.getDescent()) / 2;
drawSwatch(canvas, entry, x, rowTop + (rowHeight - swatchSize) / 2, swatchSize);
canvas.setFont(StudioTheme.font());
canvas.setColor(entry.found() ? StudioTheme.TEXT : StudioTheme.ERROR);
String name = entry.found() ? shortBlockName(entry.block()) : shortBlockName(entry.block()) + " (not found)";
int nameLeft = x + SWATCH_SIZE + 7;
canvas.drawString(fit(canvas, name, weightRight - WEIGHT_COLUMN_WIDTH - nameLeft), nameLeft, rowY);
int nameLeft = x + swatchSize + StudioTheme.scale(7);
canvas.drawString(fit(canvas, name, nameRight - nameLeft), nameLeft, baseline);
canvas.setColor(StudioTheme.SECONDARY_TEXT);
drawRightAligned(canvas, percent(entry.weightShare()), weightRight, rowY);
drawRightAligned(canvas, percent(entry.weightShare()), weightRight, baseline);
canvas.setColor(actualColor(entry));
drawRightAligned(canvas, actualText(entry), actualRight, rowY);
rowY += ROW_HEIGHT;
drawRightAligned(canvas, actualText(entry), actualRight, baseline);
rowTop += rowHeight;
}
return rowY - ROW_HEIGHT;
}
private static void drawSwatch(Graphics2D canvas, PaletteLayerPreview.Entry entry, int x, int y) {
private static void drawSwatch(Graphics2D canvas, PaletteLayerPreview.Entry entry, int x, int y, int size) {
if (entry.texture() != null) {
canvas.drawImage(entry.texture().toImage(), x, y, SWATCH_SIZE, SWATCH_SIZE, null);
canvas.drawImage(entry.texture().toImage(), x, y, size, size, null);
return;
}
canvas.setColor(new Color(entry.color()));
canvas.fillRoundRect(x, y, SWATCH_SIZE, SWATCH_SIZE, 3, 3);
int arc = StudioTheme.scale(3);
canvas.fillRoundRect(x, y, size, size, arc, arc);
}
private void drawNotes(Graphics2D canvas, int x, int y, int width) {
private void drawNotes(Graphics2D canvas, int x, int top, int width) {
canvas.setFont(StudioTheme.smallFont());
canvas.setColor(StudioTheme.SECONDARY_TEXT);
FontMetrics metrics = canvas.getFontMetrics();
int lineY = y;
int lineY = top + metrics.getAscent();
for (String note : notes()) {
for (String line : wrap(note, metrics, width)) {
canvas.drawString(line, x, lineY);
lineY += NOTE_LINE_HEIGHT;
lineY += metrics.getHeight();
}
lineY += 4;
lineY += StudioTheme.scale(NOTE_GAP);
}
}
@@ -136,8 +136,8 @@ final class PaletteNavigator extends JPanel {
private JPanel buildHereCard() {
JPanel card = new JPanel(new BorderLayout());
JPanel location = new JPanel(new GridLayout(0, 1, 0, 3));
location.setBorder(BorderFactory.createEmptyBorder(9, 12, 5, 12));
JPanel location = new JPanel(new GridLayout(0, 1, 0, StudioTheme.scale(3)));
location.setBorder(StudioTheme.emptyBorder(9, 12, 5, 12));
location.add(regionLabel);
hereList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
@@ -175,7 +175,7 @@ final class PaletteNavigator extends JPanel {
overviewTree.setRootVisible(false);
overviewTree.setShowsRootHandles(true);
overviewTree.setRowHeight(24);
overviewTree.setRowHeight(StudioTheme.scale(24));
overviewTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
overviewTree.setCellRenderer(new OverviewRenderer());
overviewTree.addTreeSelectionListener(event -> {
@@ -220,7 +220,7 @@ final class PaletteNavigator extends JPanel {
}
private void showHereMessage(String message) {
regionLabel.setText("<html><body style='width:" + MESSAGE_WRAP_WIDTH_PIXELS + "px'>" + message + "</body></html>");
regionLabel.setText("<html><body style='width:" + StudioTheme.scale(MESSAGE_WRAP_WIDTH_PIXELS) + "px'>" + message + "</body></html>");
}
private void showHereRows(PlayerLocation here) {
@@ -423,7 +423,7 @@ final class PaletteNavigator extends JPanel {
setForeground(StudioTheme.MUTED_TEXT);
}
int indent = row.header() ? HEADER_INDENT : row.layer() >= 0 ? LAYER_INDENT : BIOME_INDENT;
setBorder(BorderFactory.createEmptyBorder(row.header() ? 11 : 4, indent, 4, 10));
setBorder(StudioTheme.emptyBorder(row.header() ? 11 : 4, indent, 4, 10));
return this;
}
}
@@ -33,7 +33,7 @@ final class StudioOverlay {
Object antialiasing = canvas.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
RoundRectangle2D card = new RoundRectangle2D.Double(x + 0.5D, y + 0.5D, width - 1D, height - 1D,
StudioTheme.OVERLAY_ARC, StudioTheme.OVERLAY_ARC);
StudioTheme.scale(StudioTheme.OVERLAY_ARC), StudioTheme.scale(StudioTheme.OVERLAY_ARC));
canvas.setColor(StudioTheme.OVERLAY_BACKGROUND);
canvas.fill(card);
canvas.setColor(StudioTheme.OVERLAY_BORDER);
@@ -45,18 +45,20 @@ final class StudioStatusBar extends JPanel {
setBackground(StudioTheme.PANEL_BACKGROUND);
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(1, 0, 0, 0, StudioTheme.SEPARATOR),
BorderFactory.createEmptyBorder(0, 10, 0, 10)
StudioTheme.emptyBorder(0, 10, 0, 10)
));
setPreferredSize(new Dimension(0, HEIGHT));
progress.setMaximumSize(new Dimension(120, 4));
progress.setPreferredSize(new Dimension(120, 4));
setPreferredSize(StudioTheme.scaledSize(0, HEIGHT));
progress.setMaximumSize(StudioTheme.scaledSize(120, 4));
progress.setPreferredSize(StudioTheme.scaledSize(120, 4));
progress.setVisible(false);
allowShrinking(summary);
allowShrinking(activity);
add(summary);
add(Box.createHorizontalGlue());
add(activity);
add(Box.createHorizontalStrut(8));
add(Box.createHorizontalStrut(StudioTheme.scale(8)));
add(progress);
add(Box.createHorizontalStrut(18));
add(Box.createHorizontalStrut(StudioTheme.scale(18)));
add(position);
}
@@ -86,6 +88,11 @@ final class StudioStatusBar extends JPanel {
setTextIfChanged(position, text);
}
// Long text gives way with an ellipsis instead of pushing the other parts of the bar out of view.
private static void allowShrinking(JLabel label) {
label.setMinimumSize(new Dimension(0, 0));
}
private static JLabel statusLabel() {
JLabel label = new JLabel();
label.setFont(StudioTheme.smallFont());
@@ -20,10 +20,14 @@ package com.volmit.iris.core.gui;
import com.formdev.flatlaf.FlatDarkLaf;
import com.formdev.flatlaf.FlatLaf;
import com.formdev.flatlaf.util.UIScale;
import com.volmit.iris.Iris;
import javax.swing.BorderFactory;
import javax.swing.UIManager;
import javax.swing.border.Border;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -53,6 +57,7 @@ final class StudioTheme {
static final int OVERLAY_ARC = 10;
private static final AtomicBoolean INSTALLED = new AtomicBoolean();
private static volatile float pixelScale = 1F;
private static final Map<String, String> INTELLIJ_NEW_UI_DEFAULTS = Map.ofEntries(
Map.entry("@background", "#2B2D30"),
Map.entry("@foreground", "#DFE1E5"),
@@ -100,12 +105,32 @@ final class StudioTheme {
try {
FlatLaf.setGlobalExtraDefaults(INTELLIJ_NEW_UI_DEFAULTS);
FlatDarkLaf.setup();
pixelScale = UIScale.getUserScaleFactor();
} catch (Throwable error) {
Iris.reportError(error);
Iris.warn("Could not apply the Iris studio theme, the studio windows will use Java's default look.");
}
}
/**
* FlatLaf grows fonts and its own controls to the screen's DPI; every size the studio sets itself has to grow with them.
*/
static int scale(int pixels) {
return Math.round(pixels * pixelScale);
}
static double scale(double pixels) {
return pixels * pixelScale;
}
static Dimension scaledSize(int width, int height) {
return new Dimension(scale(width), scale(height));
}
static Border emptyBorder(int top, int left, int bottom, int right) {
return BorderFactory.createEmptyBorder(scale(top), scale(left), scale(bottom), scale(right));
}
static Font font() {
return themeFont("defaultFont", Font.PLAIN, 13);
}
@@ -50,7 +50,7 @@ final class StudioToolbar {
toolbar.setBackground(StudioTheme.PANEL_BACKGROUND);
toolbar.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 0, StudioTheme.SEPARATOR),
BorderFactory.createEmptyBorder(TOOLBAR_HEIGHT_PADDING, 8, TOOLBAR_HEIGHT_PADDING, 8)
StudioTheme.emptyBorder(TOOLBAR_HEIGHT_PADDING, 8, TOOLBAR_HEIGHT_PADDING, 8)
));
return toolbar;
}
@@ -58,7 +58,7 @@ final class StudioToolbar {
static JLabel label(String text) {
JLabel label = new JLabel(text);
label.setForeground(StudioTheme.SECONDARY_TEXT);
label.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 6));
label.setBorder(StudioTheme.emptyBorder(0, 6, 0, 6));
return label;
}
@@ -99,7 +99,7 @@ final class StudioToolbar {
static JPanel padded(JComponent content, int top, int left, int bottom, int right) {
JPanel panel = new JPanel(new BorderLayout());
panel.setOpaque(false);
panel.setBorder(BorderFactory.createEmptyBorder(top, left, bottom, right));
panel.setBorder(StudioTheme.emptyBorder(top, left, bottom, right));
panel.add(content, BorderLayout.CENTER);
return panel;
}