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 b5b69e2..6109bd0 100644
--- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/SwingIO.java
+++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/SwingIO.java
@@ -6,13 +6,16 @@ import thb.jeanluc.adventure.io.text.Span;
import thb.jeanluc.adventure.io.text.Style;
import thb.jeanluc.adventure.io.text.StyledText;
+import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
+import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTextPane;
+import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
@@ -22,8 +25,12 @@ import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
+import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@@ -34,9 +41,20 @@ import java.util.concurrent.LinkedBlockingQueue;
* a styled output pane (centre), a side panel for exits / future map (right),
* and an input field (bottom). A bundled TrueType font is loaded if present so
* colours and glyphs work without the player installing anything.
+ *
+ *
Font sizes are multiplied by a detected display scale (HiDPI) and by a
+ * user-adjustable zoom ({@code Ctrl +} / {@code Ctrl -} / {@code Ctrl 0}), so
+ * text stays readable on high-DPI laptops even when the JVM does not pick up
+ * the desktop scaling factor.
*/
public class SwingIO implements GameIO {
+ /** Unscaled base size for the main output/input font, in points. */
+ private static final float BASE_SIZE = 15f;
+
+ /** Unscaled size for the HUD and side-panel font, in points. */
+ private static final float SMALL_SIZE = 12.5f;
+
private final LinkedBlockingQueue inputs = new LinkedBlockingQueue<>();
private final JFrame frame;
private final JTextPane output;
@@ -46,14 +64,20 @@ public class SwingIO implements GameIO {
private final JTextArea side;
private final Font baseFont;
+ /** Detected display scale (≥ 1.0); HiDPI panels report > 1.0 where supported. */
+ private final float uiScale;
+
+ /** User zoom multiplier, adjusted live via keyboard. */
+ private float zoom = 1f;
+
public SwingIO(String title) {
baseFont = loadFont();
+ uiScale = detectScale();
output = new JTextPane();
output.setEditable(false);
output.setBackground(new Color(0x0b, 0x0e, 0x13));
output.setForeground(new Color(0xcf, 0xd6, 0xe0));
- output.setFont(baseFont);
output.setMargin(new Insets(8, 8, 8, 8));
doc = output.getStyledDocument();
@@ -61,20 +85,17 @@ public class SwingIO implements GameIO {
hud.setOpaque(true);
hud.setBackground(new Color(0x11, 0x15, 0x1c));
hud.setForeground(new Color(0x8b, 0x94, 0xa3));
- hud.setFont(baseFont.deriveFont(12f));
hud.setBorder(BorderFactory.createEmptyBorder(6, 12, 6, 12));
side = new JTextArea();
side.setEditable(false);
side.setBackground(new Color(0x0e, 0x12, 0x18));
side.setForeground(new Color(0x6f, 0xcf, 0x73));
- side.setFont(baseFont.deriveFont(12f));
side.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
JScrollPane sideScroll = new JScrollPane(side);
- sideScroll.setPreferredSize(new Dimension(210, 0));
+ sideScroll.setPreferredSize(new Dimension((int) (210 * uiScale), 0));
input = new JTextField();
- input.setFont(baseFont);
input.addActionListener(e -> {
String line = input.getText();
input.setText("");
@@ -89,23 +110,93 @@ public class SwingIO implements GameIO {
frame.add(sideScroll, BorderLayout.EAST);
frame.add(input, BorderLayout.SOUTH);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
- frame.setSize(900, 600);
+ frame.setSize((int) (900 * uiScale), (int) (600 * uiScale));
frame.setLocationRelativeTo(null);
+
+ installZoomKeys();
+ applyFonts();
+
frame.setVisible(true);
input.requestFocusInWindow();
}
+ /** Loads the bundled font if present, else falls back to a logical monospaced font. */
private Font loadFont() {
try (InputStream in = getClass().getResourceAsStream("/fonts/game.ttf")) {
if (in != null) {
- Font f = Font.createFont(Font.TRUETYPE_FONT, in).deriveFont(14f);
+ Font f = Font.createFont(Font.TRUETYPE_FONT, in);
GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(f);
return f;
}
} catch (Exception e) {
// fall through to the logical monospaced fallback
}
- return new Font(Font.MONOSPACED, Font.PLAIN, 14);
+ return new Font(Font.MONOSPACED, Font.PLAIN, 12);
+ }
+
+ /** Detects the display scale; HiDPI-aware where the platform reports it, else uses DPI. */
+ private float detectScale() {
+ try {
+ GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment()
+ .getDefaultScreenDevice().getDefaultConfiguration();
+ double transform = gc.getDefaultTransform().getScaleX();
+ if (transform > 1.0) {
+ return (float) transform;
+ }
+ } catch (Exception ignored) {
+ // fall through to DPI heuristic
+ }
+ try {
+ int dpi = Toolkit.getDefaultToolkit().getScreenResolution();
+ return Math.max(1f, dpi / 96f);
+ } catch (Exception ignored) {
+ return 1f;
+ }
+ }
+
+ /** Re-derives and applies all component fonts from the current scale and zoom. */
+ private void applyFonts() {
+ float main = BASE_SIZE * uiScale * zoom;
+ float small = SMALL_SIZE * uiScale * zoom;
+ output.setFont(baseFont.deriveFont(main));
+ input.setFont(baseFont.deriveFont(main));
+ hud.setFont(baseFont.deriveFont(small));
+ side.setFont(baseFont.deriveFont(small));
+ frame.revalidate();
+ frame.repaint();
+ }
+
+ /** Binds Ctrl +, Ctrl - and Ctrl 0 to live font zoom. */
+ private void installZoomKeys() {
+ JComponent root = frame.getRootPane();
+ bind(root, KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, KeyEvent.CTRL_DOWN_MASK), "zoomIn");
+ bind(root, KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, KeyEvent.CTRL_DOWN_MASK), "zoomIn");
+ bind(root, KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_DOWN_MASK), "zoomIn");
+ bind(root, KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, KeyEvent.CTRL_DOWN_MASK), "zoomOut");
+ bind(root, KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, KeyEvent.CTRL_DOWN_MASK), "zoomOut");
+ bind(root, KeyStroke.getKeyStroke(KeyEvent.VK_0, KeyEvent.CTRL_DOWN_MASK), "zoomReset");
+
+ root.getActionMap().put("zoomIn", action(e -> setZoom(zoom + 0.1f)));
+ root.getActionMap().put("zoomOut", action(e -> setZoom(zoom - 0.1f)));
+ root.getActionMap().put("zoomReset", action(e -> setZoom(1f)));
+ }
+
+ private void bind(JComponent c, KeyStroke ks, String name) {
+ c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ks, name);
+ }
+
+ private AbstractAction action(java.util.function.Consumer body) {
+ return new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ body.accept(e);
+ }
+ };
+ }
+
+ private void setZoom(float z) {
+ zoom = Math.max(0.6f, Math.min(3.0f, z));
+ applyFonts();
}
private Color colorFor(Style s) {