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) {