From e80c6b65bb3182e4ac25add5d92a7935548ad1bd Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 31 May 2026 21:21:27 +0200 Subject: [PATCH] fix(io): fit the GUI map to the side panel; re-fit on resize MapPanel now scales the map to fit the side panel (min over width/height), with the Ctrl +/- zoom as a multiplier on top, so it no longer overflows and scrolls at normal zoom. The map re-fits whenever the window/side panel is resized; side panel grows a little more (28%, max 460px) before capping. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../thb/jeanluc/adventure/io/MapPanel.java | 156 +++++++++++------- .../thb/jeanluc/adventure/io/SwingIO.java | 11 +- 2 files changed, 102 insertions(+), 65 deletions(-) diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MapPanel.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MapPanel.java index 98db795..9171177 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MapPanel.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MapPanel.java @@ -6,22 +6,28 @@ import thb.jeanluc.adventure.io.text.MapView; import thb.jeanluc.adventure.io.text.RoomCell; import javax.swing.JComponent; +import javax.swing.JScrollPane; +import javax.swing.JViewport; import javax.swing.SwingUtilities; import java.awt.BasicStroke; import java.awt.Color; +import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; -import java.awt.Rectangle; +import java.awt.Insets; import java.awt.RenderingHints; import java.awt.Stroke; import java.util.List; /** * Draws a {@link MapView} as outlined rooms linked by corridors (Graphics2D). - * All dimensions and the label font are multiplied by {@link #setScale(float)} - * so the map tracks the window's HiDPI scale and the live zoom. + * + *

The map is scaled to fit the side panel, so it never needs + * scrollbars at normal zoom and re-fits whenever the window (and thus the + * panel) is resized. The {@link #setZoom(float) zoom} from {@code Ctrl +/-} + * multiplies on top of the fit scale (zoom > 1 enlarges and may scroll).

*/ public class MapPanel extends JComponent { @@ -39,7 +45,7 @@ public class MapPanel extends JComponent { private static final Color KNOWN = new Color(0x3a, 0x46, 0x55); private final Font baseFont; - private float scale = 1f; + private float zoom = 1f; private MapView view = new MapView(List.of(), List.of()); public MapPanel(Font font) { @@ -48,71 +54,81 @@ public class MapPanel extends JComponent { setOpaque(true); } - /** Sets the rendering scale (HiDPI × zoom) and re-lays-out the panel. */ - public void setScale(float scale) { + /** Sets the user zoom multiplier (applied on top of the fit-to-panel scale). */ + public void setZoom(float zoom) { SwingUtilities.invokeLater(() -> { - this.scale = Math.max(0.4f, scale); - recomputeSize(); + this.zoom = Math.max(0.4f, zoom); + revalidate(); repaint(); }); } - /** Replaces the rendered map and scrolls the current room into view. */ + /** Replaces the rendered map. */ public void show(MapView v) { SwingUtilities.invokeLater(() -> { this.view = v; - recomputeSize(); + revalidate(); repaint(); - v.cells().stream().filter(c -> c.state() == CellState.CURRENT).findFirst() - .ifPresent(c -> scrollRectToVisible(boundsExpanded(c))); }); } - private int cellW() { - return Math.round(BASE_CELL_W * scale); + /** The space the map should fit into: the enclosing scroll pane's viewport area. */ + private Dimension available() { + Container p = getParent(); + if (p instanceof JViewport && p.getParent() instanceof JScrollPane sp) { + Insets in = sp.getInsets(); + int w = sp.getWidth() - in.left - in.right; + int h = sp.getHeight() - in.top - in.bottom; + if (w > 0 && h > 0) { + return new Dimension(w, h); + } + } + Dimension s = getSize(); + return (s.width > 0 && s.height > 0) ? s : new Dimension(220, 320); } - private int cellH() { - return Math.round(BASE_CELL_H * scale); - } - - private int gapX() { - return Math.round(BASE_GAP_X * scale); - } - - private int gapY() { - return Math.round(BASE_GAP_Y * scale); - } - - private int pad() { - return Math.round(BASE_PAD * scale); - } - - private void recomputeSize() { + private int[] gridBounds() { int maxX = 0; int maxY = 0; for (RoomCell c : view.cells()) { maxX = Math.max(maxX, c.x()); maxY = Math.max(maxY, c.y()); } - setPreferredSize(new Dimension( - pad() * 2 + (maxX + 1) * cellW() + maxX * gapX(), - pad() * 2 + (maxY + 1) * cellH() + maxY * gapY())); - revalidate(); + return new int[]{maxX, maxY}; } - private Rectangle boundsExpanded(RoomCell c) { - int x = pad() + c.x() * (cellW() + gapX()); - int y = pad() + c.y() * (cellH() + gapY()); - return new Rectangle(x - gapX(), y - gapY(), cellW() + 2 * gapX(), cellH() + 2 * gapY()); + /** Fit-to-panel scale times the user zoom. */ + private float effScale(Dimension avail, int[] b) { + double natW = 2.0 * BASE_PAD + (b[0] + 1) * BASE_CELL_W + b[0] * BASE_GAP_X; + double natH = 2.0 * BASE_PAD + (b[1] + 1) * BASE_CELL_H + b[1] * BASE_GAP_Y; + if (natW <= 0 || natH <= 0) { + return zoom; + } + double fit = Math.min(avail.width / natW, avail.height / natH); + fit = Math.max(0.3, Math.min(3.0, fit)); + return (float) (fit * zoom); } - private int cx(RoomCell c) { - return pad() + c.x() * (cellW() + gapX()); + private Dimension content(float eff, int[] b) { + int pad = Math.round(BASE_PAD * eff); + int cw = Math.round(BASE_CELL_W * eff); + int ch = Math.round(BASE_CELL_H * eff); + int gx = Math.round(BASE_GAP_X * eff); + int gy = Math.round(BASE_GAP_Y * eff); + return new Dimension(pad * 2 + (b[0] + 1) * cw + b[0] * gx, + pad * 2 + (b[1] + 1) * ch + b[1] * gy); } - private int cy(RoomCell c) { - return pad() + c.y() * (cellH() + gapY()); + @Override + public Dimension getPreferredSize() { + if (view.cells().isEmpty()) { + return available(); + } + Dimension avail = available(); + int[] b = gridBounds(); + Dimension c = content(effScale(avail, b), b); + // Fill the viewport when the map is smaller; allow growth (scroll) when zoomed in. + return new Dimension(Math.max(c.width, avail.width), Math.max(c.height, avail.height)); } @Override @@ -121,48 +137,66 @@ public class MapPanel extends JComponent { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setColor(BG); g.fillRect(0, 0, getWidth(), getHeight()); - g.setFont(baseFont.deriveFont(BASE_FONT * scale)); + if (view.cells().isEmpty()) { + g.dispose(); + return; + } - int cellW = cellW(); - int cellH = cellH(); - int arc = Math.round(12 * scale); - Stroke solid = new BasicStroke(2f * scale); - Stroke dashed = new BasicStroke(1.5f * scale, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, - 10f, new float[]{4f * scale, 3f * scale}, 0f); + int[] b = gridBounds(); + float eff = effScale(available(), b); + g.setFont(baseFont.deriveFont(BASE_FONT * eff)); + + int cellW = Math.round(BASE_CELL_W * eff); + int cellH = Math.round(BASE_CELL_H * eff); + int gapX = Math.round(BASE_GAP_X * eff); + int gapY = Math.round(BASE_GAP_Y * eff); + int pad = Math.round(BASE_PAD * eff); + int arc = Math.round(12 * eff); + + Dimension c = content(eff, b); + int offX = Math.max(0, (getWidth() - c.width) / 2); + int offY = Math.max(0, (getHeight() - c.height) / 2); + + Stroke solid = new BasicStroke(2f * eff); + Stroke dashed = new BasicStroke(1.5f * eff, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, + 10f, new float[]{4f * eff, 3f * eff}, 0f); // corridors first for (Connection cn : view.connections()) { boolean known = cn.from().state() == CellState.KNOWN || cn.to().state() == CellState.KNOWN; g.setStroke(known ? dashed : solid); g.setColor(known ? KNOWN : CORRIDOR); - g.drawLine(cx(cn.from()) + cellW / 2, cy(cn.from()) + cellH / 2, - cx(cn.to()) + cellW / 2, cy(cn.to()) + cellH / 2); + int x1 = offX + pad + cn.from().x() * (cellW + gapX) + cellW / 2; + int y1 = offY + pad + cn.from().y() * (cellH + gapY) + cellH / 2; + int x2 = offX + pad + cn.to().x() * (cellW + gapX) + cellW / 2; + int y2 = offY + pad + cn.to().y() * (cellH + gapY) + cellH / 2; + g.drawLine(x1, y1, x2, y2); } // rooms on top - for (RoomCell c : view.cells()) { - int x = cx(c); - int y = cy(c); - Color stroke = switch (c.state()) { + for (RoomCell cell : view.cells()) { + int x = offX + pad + cell.x() * (cellW + gapX); + int y = offY + pad + cell.y() * (cellH + gapY); + Color stroke = switch (cell.state()) { case CURRENT -> CURRENT; case VISITED -> VISITED; case KNOWN -> KNOWN; }; - g.setColor(c.state() == CellState.CURRENT + g.setColor(cell.state() == CellState.CURRENT ? new Color(0x27, 0x21, 0x10) : new Color(0x11, 0x16, 0x1f)); g.fillRoundRect(x, y, cellW, cellH, arc, arc); - g.setStroke(c.state() == CellState.KNOWN + g.setStroke(cell.state() == CellState.KNOWN ? dashed - : new BasicStroke((c.state() == CellState.CURRENT ? 2.5f : 1.5f) * scale)); + : new BasicStroke((cell.state() == CellState.CURRENT ? 2.5f : 1.5f) * eff)); g.setColor(stroke); g.drawRoundRect(x, y, cellW, cellH, arc, arc); - String label = c.state() == CellState.KNOWN ? "?" : c.name(); + String label = cell.state() == CellState.KNOWN ? "?" : cell.name(); int tw = g.getFontMetrics().stringWidth(label); int ascent = g.getFontMetrics().getAscent(); - g.setColor(c.state() == CellState.KNOWN ? KNOWN.brighter() : stroke); + g.setColor(cell.state() == CellState.KNOWN ? KNOWN.brighter() : stroke); g.drawString(label, x + (cellW - tw) / 2, y + (cellH + ascent) / 2 - 2); } g.dispose(); diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/SwingIO.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/SwingIO.java index caf33ac..3fab999 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/SwingIO.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/SwingIO.java @@ -58,9 +58,9 @@ public class SwingIO implements GameIO { private static final float SMALL_SIZE = 12.5f; /** Side-panel width as a fraction of the window width, clamped by the bounds below. */ - private static final double SIDE_RATIO = 0.25; - private static final int SIDE_MIN = 160; - private static final int SIDE_MAX = 320; + private static final double SIDE_RATIO = 0.28; + private static final int SIDE_MIN = 180; + private static final int SIDE_MAX = 460; private final LinkedBlockingQueue inputs = new LinkedBlockingQueue<>(); private final JFrame frame; @@ -173,6 +173,9 @@ public class SwingIO implements GameIO { sideScroll.setPreferredSize(new Dimension(target, 0)); sideScroll.revalidate(); frame.validate(); + // The side panel changed size: let the map re-fit into it. + map.revalidate(); + map.repaint(); } /** Re-derives and applies all component fonts from the current scale and zoom. */ @@ -182,7 +185,7 @@ public class SwingIO implements GameIO { output.setFont(baseFont.deriveFont(main)); input.setFont(baseFont.deriveFont(main)); hud.setFont(baseFont.deriveFont(small)); - map.setScale(uiScale * zoom); + map.setZoom(zoom); frame.revalidate(); frame.repaint(); }