Pause Menu

a full-screen modal overlay with stacked buttons, toggled by a key.

▶ Run in browser

Tags: ui menu pause modal anchors

A classic pause screen layered over the running game: pressing P (or ESC) drops a dimmed full-rect scrim across the whole viewport and pops a centred panel with Resume / Restart / Quit buttons stacked vertically. The scrim fills the screen via AnchorPreset.FULL_RECT, the panel sits dead-centre via AnchorPreset.CENTER with symmetric margins, and both stay correctly placed at any window size. The background keeps a simple animated marker so it is obvious the overlay sits on top of live content.

What it demonstrates

  • A modal overlay built from a FULL_RECT scrim Panel plus a CENTER-anchored panel, both top-level Controls using anchors and margins, never absolute position.

  • Toggling the whole overlay’s visible flag from a key press to show / hide.

  • A vertical VBoxContainer of Buttons wired with Button.pressed.connect() (Resume hides the menu, Restart resets state, Quit closes the app).

  • The centred panel staying centred and the scrim staying full-screen on resize.

Controls: P / ESC - Toggle the pause overlay Mouse - Click Resume / Restart / Quit

Source

  1"""Pause Menu: a full-screen modal overlay with stacked buttons, toggled by a key.
  2
  3A classic pause screen layered over the running game: pressing P (or ESC) drops
  4a dimmed full-rect scrim across the whole viewport and pops a centred panel with
  5Resume / Restart / Quit buttons stacked vertically. The scrim fills the screen
  6via `AnchorPreset.FULL_RECT`, the panel sits dead-centre via `AnchorPreset.CENTER`
  7with symmetric margins, and both stay correctly placed at any window size. The
  8background keeps a simple animated marker so it is obvious the overlay sits on
  9top of live content.
 10
 11# /// simvx
 12# tags = ["ui", "menu", "pause", "modal", "anchors"]
 13# web = { root = "PauseMenuDemo", width = 800, height = 600, responsive = true }
 14# ///
 15
 16## What it demonstrates
 17
 18- A modal overlay built from a `FULL_RECT` scrim Panel plus a `CENTER`-anchored
 19  panel, both top-level Controls using anchors and margins, never absolute
 20  position.
 21- Toggling the whole overlay's `visible` flag from a key press to show / hide.
 22- A vertical `VBoxContainer` of `Button`s wired with `Button.pressed.connect()`
 23  (Resume hides the menu, Restart resets state, Quit closes the app).
 24- The centred panel staying centred and the scrim staying full-screen on resize.
 25
 26Controls:
 27  P / ESC - Toggle the pause overlay
 28  Mouse   - Click Resume / Restart / Quit
 29"""
 30
 31from simvx.core import (
 32    AnchorPreset,
 33    Button,
 34    Colour,
 35    Control,
 36    Input,
 37    InputMap,
 38    Key,
 39    Label,
 40    Node2D,
 41    Panel,
 42    VBoxContainer,
 43    Vec2,
 44)
 45from simvx.graphics import App
 46
 47WIDTH, HEIGHT = 800, 600
 48
 49
 50class PauseOverlay(Control):
 51    """Full-screen modal: dimmed scrim + centred button panel.
 52
 53    Itself a FULL_RECT top-level Control so it covers the viewport; toggling
 54    `self.visible` shows or hides the whole overlay at once.
 55    """
 56
 57    def __init__(self, on_resume, on_restart, on_quit, **kwargs):
 58        super().__init__(**kwargs)
 59        self.set_anchor_preset(AnchorPreset.FULL_RECT)
 60
 61        # Dimmed scrim across the entire viewport (semi-transparent black).
 62        scrim = Panel(name="Scrim")
 63        scrim.set_anchor_preset(AnchorPreset.FULL_RECT)
 64        scrim.bg_colour = Colour.rgba(0.0, 0.0, 0.0, 0.6)
 65        self.add_child(scrim)
 66
 67        # Centred panel: CENTER anchor + symmetric margins make a fixed-size
 68        # box that stays centred at any window size.
 69        panel = Panel(name="MenuPanel")
 70        panel.set_anchor_preset(AnchorPreset.CENTER)
 71        panel.margin_left = -160
 72        panel.margin_right = 160
 73        panel.margin_top = -150
 74        panel.margin_bottom = 150
 75        panel.bg_colour = Colour.hex("#1A1A2E")
 76        self.add_child(panel)
 77
 78        title = Label("Paused")
 79        title.set_anchor_preset(AnchorPreset.TOP_WIDE)
 80        title.margin_top = 16
 81        title.size_y = 36
 82        title.font_size = 24.0
 83        title.text_colour = Colour.WHITE
 84        title.alignment = "center"
 85        panel.add_child(title)
 86
 87        # Stacked buttons in a vertical container.
 88        buttons = VBoxContainer(name="Buttons")
 89        buttons.set_anchor_preset(AnchorPreset.CENTER)
 90        buttons.margin_left = -110
 91        buttons.margin_right = 110
 92        buttons.margin_top = -50
 93        buttons.margin_bottom = 90
 94        buttons.separation = 12.0
 95        panel.add_child(buttons)
 96
 97        for label, handler in (("Resume", on_resume), ("Restart", on_restart), ("Quit", on_quit)):
 98            btn = Button(label, name=f"{label}Button")
 99            btn.size = Vec2(220, 36)
100            btn.pressed.connect(handler)
101            buttons.add_child(btn)
102
103
104class PauseMenuDemo(Node2D):
105    """Root: animated background content with a toggleable pause overlay on top."""
106
107    def on_ready(self):
108        InputMap.add_action("pause", [Key.P, Key.ESCAPE])
109
110        self._t = 0.0
111        self._marker = Vec2(WIDTH / 2, HEIGHT / 2)
112
113        hint = Label("Press P or ESC to pause")
114        hint.set_anchor_preset(AnchorPreset.CENTER_TOP)
115        hint.margin_left = -160
116        hint.margin_right = 160
117        hint.margin_top = 20
118        hint.font_size = 16.0
119        hint.text_colour = Colour.LIGHT_GRAY
120        hint.alignment = "center"
121        self.add_child(hint)
122
123        self._overlay = PauseOverlay(
124            self._resume, self._restart, self._quit, name="PauseOverlay",
125        )
126        self._overlay.visible = False  # hidden until paused
127        self.add_child(self._overlay)
128
129    @property
130    def paused(self) -> bool:
131        return self._overlay.visible
132
133    def _resume(self):
134        self._overlay.visible = False
135
136    def _restart(self):
137        self._t = 0.0
138        self._marker = Vec2(WIDTH / 2, HEIGHT / 2)
139        self._overlay.visible = False
140
141    def _quit(self):
142        self.app.quit()
143
144    def on_update(self, dt: float):
145        if Input.is_action_just_pressed("pause"):
146            self._overlay.visible = not self._overlay.visible
147
148        # Background only advances while not paused, proving the overlay gates it.
149        if not self.paused:
150            self._t += dt
151
152    def on_draw(self, renderer):
153        # Simple live background: a marker orbiting the centre, so the dimmed
154        # overlay is visibly layered over moving content.
155        import math
156
157        cx, cy = WIDTH / 2, HEIGHT / 2
158        x = cx + math.cos(self._t * 1.5) * 160
159        y = cy + math.sin(self._t * 1.5) * 120
160        renderer.draw_rect((x - 18, y - 18), (36, 36), colour=Colour.hex("#4FC3F7"), filled=True)
161
162
163if __name__ == "__main__":
164    app = App(title="SimVX Pause Menu", width=WIDTH, height=HEIGHT)
165    app.run(PauseMenuDemo())