feat(io): themed full-window menu overlay in the GUI
Replace the plain Swing JDialog menu with a glass-pane overlay that fills the whole window: dark background matching the game palette, bundled font, lavender heading, flat hover-highlighted buttons, Escape = last option. The overlay hides the game chrome (HUD/map/quest panels) while in the menu and rebuilds at the current frameScale*zoom on window resize, so it scales correctly from startup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,6 @@ import javax.swing.Box;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
@@ -22,6 +21,7 @@ import javax.swing.JScrollPane;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.JTextPane;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.SwingConstants;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.SimpleAttributeSet;
|
||||
@@ -29,10 +29,13 @@ import javax.swing.text.StyleConstants;
|
||||
import javax.swing.text.StyledDocument;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Font;
|
||||
import java.awt.GraphicsConfiguration;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.event.ActionEvent;
|
||||
@@ -138,6 +141,7 @@ public class SwingIO implements GameIO {
|
||||
public void componentResized(ComponentEvent e) {
|
||||
updateSideWidth();
|
||||
applyFonts();
|
||||
rescaleMenuOverlayIfVisible();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -348,41 +352,12 @@ public class SwingIO implements GameIO {
|
||||
|
||||
@Override
|
||||
public int choose(String title, List<String> options) {
|
||||
// Same EDT↔worker handoff as readLine: the worker blocks on take(),
|
||||
// the EDT (button click / window close) offers the chosen index.
|
||||
// Full-window overlay (glass pane), not a floating dialog: it covers the game
|
||||
// chrome so no panels/outlines show behind the menu, and it fills + scales with
|
||||
// the window. EDT↔worker handoff as in readLine: the worker blocks on take(),
|
||||
// a button click / Escape offers the chosen index.
|
||||
LinkedBlockingQueue<Integer> picked = new LinkedBlockingQueue<>();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JDialog dialog = new JDialog(frame, title, true);
|
||||
JPanel panel = new JPanel();
|
||||
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
|
||||
panel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
|
||||
panel.add(new JLabel(title));
|
||||
panel.add(Box.createVerticalStrut(8));
|
||||
for (int i = 0; i < options.size(); i++) {
|
||||
int idx = i;
|
||||
JButton b = new JButton(options.get(i));
|
||||
b.setAlignmentX(0f);
|
||||
b.addActionListener(e -> {
|
||||
dialog.dispose();
|
||||
picked.offer(idx);
|
||||
});
|
||||
panel.add(b);
|
||||
panel.add(Box.createVerticalStrut(4));
|
||||
}
|
||||
// Closing the dialog counts as the last (safe/cancel) option.
|
||||
dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
|
||||
dialog.addWindowListener(new java.awt.event.WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(java.awt.event.WindowEvent e) {
|
||||
dialog.dispose();
|
||||
picked.offer(options.size() - 1);
|
||||
}
|
||||
});
|
||||
dialog.setContentPane(panel);
|
||||
dialog.pack();
|
||||
dialog.setLocationRelativeTo(frame);
|
||||
dialog.setVisible(true);
|
||||
});
|
||||
SwingUtilities.invokeLater(() -> showMenuOverlay(title, options, picked));
|
||||
try {
|
||||
return picked.take();
|
||||
} catch (InterruptedException e) {
|
||||
@@ -391,6 +366,123 @@ public class SwingIO implements GameIO {
|
||||
}
|
||||
}
|
||||
|
||||
/** Current menu being shown, so a window resize can rebuild it at the new scale. */
|
||||
private String menuTitle;
|
||||
private List<String> menuOptions;
|
||||
private LinkedBlockingQueue<Integer> menuPicked;
|
||||
|
||||
/** Rebuilds the visible menu overlay at the current window scale (called on resize). */
|
||||
private void rescaleMenuOverlayIfVisible() {
|
||||
if (menuOptions != null && frame.getGlassPane().isVisible()) {
|
||||
showMenuOverlay(menuTitle, menuOptions, menuPicked);
|
||||
}
|
||||
}
|
||||
|
||||
/** Installs the menu as the frame's glass pane (covers everything) and shows it. */
|
||||
private void showMenuOverlay(String title, List<String> options, LinkedBlockingQueue<Integer> picked) {
|
||||
menuTitle = title;
|
||||
menuOptions = options;
|
||||
menuPicked = picked;
|
||||
float scale = frameScale() * zoom; // same scaling the rest of the UI uses
|
||||
|
||||
JPanel glass = new JPanel(new GridBagLayout());
|
||||
glass.setOpaque(true);
|
||||
glass.setBackground(MENU_BG);
|
||||
glass.addMouseListener(new java.awt.event.MouseAdapter() { }); // swallow backdrop clicks
|
||||
|
||||
JPanel card = new JPanel();
|
||||
card.setOpaque(false);
|
||||
card.setLayout(new BoxLayout(card, BoxLayout.Y_AXIS));
|
||||
|
||||
JLabel heading = new JLabel(title.toUpperCase());
|
||||
heading.setForeground(MENU_TITLE);
|
||||
heading.setFont(baseFont.deriveFont(Font.BOLD, BASE_SIZE * scale * 1.6f));
|
||||
heading.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
heading.setBorder(BorderFactory.createEmptyBorder(0, 0, (int) (22 * scale), 0));
|
||||
card.add(heading);
|
||||
|
||||
Runnable cancel = () -> {
|
||||
hideMenuOverlay();
|
||||
picked.offer(options.size() - 1);
|
||||
};
|
||||
for (int i = 0; i < options.size(); i++) {
|
||||
int idx = i;
|
||||
JButton b = menuButton(options.get(i), scale);
|
||||
b.addActionListener(e -> {
|
||||
hideMenuOverlay();
|
||||
picked.offer(idx);
|
||||
});
|
||||
card.add(b);
|
||||
if (i < options.size() - 1) {
|
||||
card.add(Box.createVerticalStrut((int) (10 * scale)));
|
||||
}
|
||||
}
|
||||
|
||||
// Escape selects the last (safe/cancel) option — mirrors the console EOF convention.
|
||||
glass.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
|
||||
.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "menuCancel");
|
||||
glass.getActionMap().put("menuCancel", action(e -> cancel.run()));
|
||||
|
||||
glass.add(card); // single child → centered by GridBagLayout
|
||||
frame.setGlassPane(glass);
|
||||
glass.setVisible(true);
|
||||
glass.requestFocusInWindow();
|
||||
}
|
||||
|
||||
private void hideMenuOverlay() {
|
||||
menuOptions = null; // stop resize rebuilds once the menu is dismissed
|
||||
frame.getGlassPane().setVisible(false);
|
||||
}
|
||||
|
||||
/** Palette for the themed menu overlay, matching the game window's dark look. */
|
||||
private static final Color MENU_BG = new Color(0x0b, 0x0e, 0x13);
|
||||
private static final Color MENU_TITLE = new Color(0xc9, 0x8a, 0xe0);
|
||||
private static final Color BTN_BG = new Color(0x16, 0x1b, 0x24);
|
||||
private static final Color BTN_BG_HOVER = new Color(0x22, 0x2a, 0x37);
|
||||
private static final Color BTN_FG = new Color(0xcf, 0xd6, 0xe0);
|
||||
private static final Color BTN_LINE = new Color(0x2a, 0x2f, 0x3a);
|
||||
private static final Color BTN_LINE_HOVER = new Color(0x6f, 0xcf, 0x73);
|
||||
|
||||
/** A flat, dark, fixed-width menu button matching the game palette, with hover feedback. */
|
||||
private JButton menuButton(String text, float scale) {
|
||||
int padV = (int) (11 * scale);
|
||||
int padH = (int) (18 * scale);
|
||||
JButton b = new JButton(text);
|
||||
b.setFont(baseFont.deriveFont(BASE_SIZE * scale));
|
||||
b.setForeground(BTN_FG);
|
||||
b.setBackground(BTN_BG);
|
||||
b.setFocusPainted(false);
|
||||
b.setOpaque(true);
|
||||
b.setHorizontalAlignment(SwingConstants.LEFT);
|
||||
b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
b.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
b.setBorder(menuButtonBorder(BTN_LINE, padV, padH));
|
||||
Dimension size = new Dimension((int) (340 * scale), b.getPreferredSize().height);
|
||||
b.setPreferredSize(size);
|
||||
b.setMinimumSize(size);
|
||||
b.setMaximumSize(size);
|
||||
b.addMouseListener(new java.awt.event.MouseAdapter() {
|
||||
@Override
|
||||
public void mouseEntered(java.awt.event.MouseEvent e) {
|
||||
b.setBackground(BTN_BG_HOVER);
|
||||
b.setBorder(menuButtonBorder(BTN_LINE_HOVER, padV, padH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(java.awt.event.MouseEvent e) {
|
||||
b.setBackground(BTN_BG);
|
||||
b.setBorder(menuButtonBorder(BTN_LINE, padV, padH));
|
||||
}
|
||||
});
|
||||
return b;
|
||||
}
|
||||
|
||||
private static javax.swing.border.Border menuButtonBorder(Color line, int padV, int padH) {
|
||||
return BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(line, 1),
|
||||
BorderFactory.createEmptyBorder(padV, padH, padV, padH));
|
||||
}
|
||||
|
||||
private void addList(List<Span> spans, List<String> xs, Style st) {
|
||||
for (int i = 0; i < xs.size(); i++) {
|
||||
if (i > 0) {
|
||||
|
||||
Reference in New Issue
Block a user