From 963ddfb89e0bba3233886bb2496f109e3d7c5f1c Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 31 May 2026 20:21:23 +0200 Subject: [PATCH] feat(io): SwingIO 4-region layout, JTextPane styling, bundled font loader Co-Authored-By: Claude Opus 4.8 (1M context) --- .../thb/jeanluc/adventure/io/SwingIO.java | 172 ++++++++++++++---- .../src/main/resources/fonts/README.md | 12 ++ 2 files changed, 151 insertions(+), 33 deletions(-) create mode 100644 Semesterprojekt/src/main/resources/fonts/README.md 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 791e257..b5b69e2 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/SwingIO.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/SwingIO.java @@ -1,75 +1,143 @@ package thb.jeanluc.adventure.io; +import thb.jeanluc.adventure.io.text.Hud; +import thb.jeanluc.adventure.io.text.RoomView; +import thb.jeanluc.adventure.io.text.Span; +import thb.jeanluc.adventure.io.text.Style; +import thb.jeanluc.adventure.io.text.StyledText; + import javax.swing.BorderFactory; 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.SwingUtilities; +import javax.swing.text.BadLocationException; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; import java.awt.BorderLayout; import java.awt.Color; +import java.awt.Dimension; import java.awt.Font; +import java.awt.GraphicsEnvironment; +import java.awt.Insets; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.LinkedBlockingQueue; -import thb.jeanluc.adventure.io.text.StyledText; - /** - * {@link GameIO} backed by a Swing window. A read-only output area shows - * game text; a bottom text field collects player input. The input from - * the EDT is handed to the (worker-thread) game loop through a - * {@link LinkedBlockingQueue}. + * {@link GameIO} backed by a Swing window with four regions: a HUD bar (top), + * 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. */ public class SwingIO implements GameIO { - /** Bridge between the EDT and the game loop. One element = one line. */ private final LinkedBlockingQueue inputs = new LinkedBlockingQueue<>(); - - /** Main window. */ private final JFrame frame; - - /** Output text area; appended to from any thread via {@link SwingUtilities#invokeLater}. */ - private final JTextArea output; - - /** Input text field; ENTER pushes the line into {@link #inputs}. */ + private final JTextPane output; + private final StyledDocument doc; private final JTextField input; + private final JLabel hud; + private final JTextArea side; + private final Font baseFont; - /** - * Creates and shows the window with the given title. Must be called on - * the EDT (typical pattern: from {@code SwingUtilities.invokeLater}). - * - * @param title window title - */ public SwingIO(String title) { - frame = new JFrame(title); - output = new JTextArea(); - input = new JTextField(); + baseFont = loadFont(); + output = new JTextPane(); output.setEditable(false); - output.setLineWrap(true); - output.setWrapStyleWord(true); - output.setBackground(Color.BLACK); - output.setForeground(Color.LIGHT_GRAY); - output.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14)); - output.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + 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(); - input.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14)); + hud = new JLabel(" "); + 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)); + + input = new JTextField(); + input.setFont(baseFont); input.addActionListener(e -> { String line = input.getText(); input.setText(""); - output.append("> " + line + "\n"); + appendSpans(List.of(new Span("> " + line + "\n", Style.DIM))); inputs.offer(line); }); + frame = new JFrame(title); frame.setLayout(new BorderLayout()); + frame.add(hud, BorderLayout.NORTH); frame.add(new JScrollPane(output), BorderLayout.CENTER); + frame.add(sideScroll, BorderLayout.EAST); frame.add(input, BorderLayout.SOUTH); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.setSize(800, 600); + frame.setSize(900, 600); frame.setLocationRelativeTo(null); frame.setVisible(true); input.requestFocusInWindow(); } + private Font loadFont() { + try (InputStream in = getClass().getResourceAsStream("/fonts/game.ttf")) { + if (in != null) { + Font f = Font.createFont(Font.TRUETYPE_FONT, in).deriveFont(14f); + GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(f); + return f; + } + } catch (Exception e) { + // fall through to the logical monospaced fallback + } + return new Font(Font.MONOSPACED, Font.PLAIN, 14); + } + + private Color colorFor(Style s) { + return switch (s) { + case HEADING -> new Color(0xc9, 0x8a, 0xe0); + case ITEM -> new Color(0x46, 0xc8, 0xd8); + case NPC -> new Color(0xe6, 0xc3, 0x4a); + case EXIT -> new Color(0x6f, 0xcf, 0x73); + case DANGER -> new Color(0xe0, 0x6c, 0x6c); + case DIM -> new Color(0x6b, 0x72, 0x80); + case PLAIN -> new Color(0xcf, 0xd6, 0xe0); + }; + } + + private void appendSpans(List spans) { + SwingUtilities.invokeLater(() -> { + try { + for (Span sp : spans) { + SimpleAttributeSet as = new SimpleAttributeSet(); + StyleConstants.setForeground(as, colorFor(sp.style())); + if (sp.style() == Style.HEADING) { + StyleConstants.setBold(as, true); + } + doc.insertString(doc.getLength(), sp.text(), as); + } + output.setCaretPosition(doc.getLength()); + } catch (BadLocationException ignored) { + // append-only; cannot occur with getLength() + } + }); + } + @Override public String readLine() { try { @@ -82,6 +150,44 @@ public class SwingIO implements GameIO { @Override public void print(StyledText text) { - SwingUtilities.invokeLater(() -> output.append(text.plainText() + "\n")); + List spans = new ArrayList<>(text.spans()); + spans.add(new Span("\n", Style.PLAIN)); + appendSpans(spans); + } + + @Override + public void showRoom(RoomView room) { + List spans = new ArrayList<>(); + spans.add(new Span(room.name() + "\n", Style.HEADING)); + spans.add(new Span(room.description().stripTrailing() + "\n", Style.PLAIN)); + if (!room.items().isEmpty()) { + spans.add(new Span("You see ", Style.PLAIN)); + addList(spans, room.items(), Style.ITEM); + spans.add(new Span(".\n", Style.PLAIN)); + } + if (!room.npcs().isEmpty()) { + addList(spans, room.npcs(), Style.NPC); + spans.add(new Span(" is here.\n", Style.PLAIN)); + } + appendSpans(spans); + + String exits = room.exits().isEmpty() ? "(none)" : String.join("\n", room.exits()); + SwingUtilities.invokeLater(() -> side.setText("EXITS\n\n" + exits)); + } + + @Override + public void setHud(Hud h) { + String txt = " " + h.location() + " gold " + h.gold() + " turn " + h.turn() + + " light " + (h.lightOn() ? "on" : "off"); + SwingUtilities.invokeLater(() -> hud.setText(txt)); + } + + private void addList(List spans, List xs, Style st) { + for (int i = 0; i < xs.size(); i++) { + if (i > 0) { + spans.add(new Span(", ", Style.PLAIN)); + } + spans.add(new Span(xs.get(i), st)); + } } } diff --git a/Semesterprojekt/src/main/resources/fonts/README.md b/Semesterprojekt/src/main/resources/fonts/README.md new file mode 100644 index 0000000..3485750 --- /dev/null +++ b/Semesterprojekt/src/main/resources/fonts/README.md @@ -0,0 +1,12 @@ +# Bundled GUI font + +Drop a TrueType font here named `game.ttf` to use it in the Swing GUI +(`SwingIO`). It is loaded via `Font.createFont` and registered at runtime — +no installation on the player's machine required. + +- A monospaced font is recommended (the UI assumes fixed width for frames). +- A Nerd Font variant additionally provides icon glyphs. +- Check the font's licence before committing the `.ttf`. + +If no `game.ttf` is present, `SwingIO` falls back to the logical +`Font.MONOSPACED`, so the game still runs.