nodes/confirm_dialog.pyΒΆ

Part of Dungeon Explorer.

  1"""Reusable Yes/No confirmation modal."""
  2
  3from simvx.core import Anchor, Control, Property
  4from simvx.core.input import Key, KeyEvent, Pointer, PointerEvent, PointerPhase
  5
  6
  7class ConfirmDialog(Control):
  8    """Yes/No confirmation overlay. Calls on_confirm(True/False) when answered.
  9
 10    Opened as an overlay-over-overlay (e.g. on top of the pause menu). The
 11    player-pause is left to the PlayerPauseBridge (no ``inert``) so the
 12    underlying overlay keeps drawing.
 13    """
 14
 15    visible = Property(
 16        False,
 17        coerce=bool,
 18        hint="Whether this node and its subtree are drawn",
 19        on_change="_on_visible_changed",
 20    )
 21
 22    # Transient overlay: created and destroyed on demand. Never persisted so a
 23    # save taken while it is open can be re-applied cleanly.
 24    __save_persist__ = False
 25
 26    def __init__(self, message: str = "Are you sure?", on_confirm=None, **kwargs):
 27        super().__init__(name="ConfirmDialog", anchor=Anchor.FULL_RECT, **kwargs)
 28        # Inter-overlay stacking is decided by registry order, not z_index.
 29
 30        self._message = message
 31        self._on_confirm = on_confirm
 32        self._selected = 1  # Default to "No"
 33
 34        self.cancel_requested.connect(self._on_cancel)
 35
 36    # -- Lifecycle helpers --
 37
 38    def show(self):
 39        """Open the confirmation overlay."""
 40        self._selected = 1  # Default to "No" for safety
 41        self.show_overlay("blocking", dismiss=True)
 42
 43    def _on_cancel(self):
 44        # Outside-click / Esc dismissal: answer "No".
 45        if self._on_confirm:
 46            self._on_confirm(False)
 47        # close_overlay() already invoked by the router for the dismissal path.
 48
 49    # -- Input --
 50
 51    def on_gui_input(self, event):
 52        if not self.visible:
 53            return
 54        # Mouse press routing
 55        if isinstance(event, PointerEvent):
 56            if event.button in Pointer.PRIMARY and event.phase is PointerPhase.DOWN:
 57                mx, my = event.position
 58                sw, sh = self._screen_size()
 59                bw, bh = 400.0, 140.0
 60                bx = (sw - bw) / 2
 61                by = (sh - bh) / 2
 62                btn_y = by + bh - 50
 63                for i, offset in enumerate((80, 240)):
 64                    btn_x = bx + offset
 65                    if btn_x <= mx <= btn_x + 80 and btn_y <= my <= btn_y + 30:
 66                        self._selected = i
 67                        self._answer(i == 0)
 68                        return True
 69            return None
 70        # Keyboard
 71        if isinstance(event, KeyEvent) and event.pressed:
 72            if event.key in (Key.A, Key.LEFT):
 73                self._selected = 0
 74                return True
 75            elif event.key in (Key.D, Key.RIGHT):
 76                self._selected = 1
 77                return True
 78            elif event.key in (Key.SPACE, Key.ENTER, Key.E):
 79                self._answer(self._selected == 0)
 80                return True
 81            elif event.key is Key.ESCAPE:
 82                self._answer(False)
 83                return True
 84
 85    def _answer(self, value: bool):
 86        if self._on_confirm:
 87            self._on_confirm(value)
 88        self.close_overlay()
 89
 90    # -- Rendering --
 91
 92    def _screen_size(self) -> tuple[float, float]:
 93        if self.tree is not None:
 94            sw, sh = self.tree.screen_size
 95            return float(sw), float(sh)
 96        return 1280.0, 720.0
 97
 98    def on_draw(self, renderer):
 99        if not self.visible:
100            return
101        sw, sh = self._screen_size()
102
103        # Dim background
104        renderer.rect((0, 0), (sw, sh), fill=(0.0, 0.0, 0.0, 0.6))
105
106        bw, bh = 400.0, 140.0
107        bx = (sw - bw) / 2
108        by = (sh - bh) / 2
109        renderer.rect((bx, by), (bw, bh), fill=(0.12, 0.12, 0.15, 0.95))
110        renderer.rect((bx, by), (bw, 2), fill=(0.8, 0.6, 0.2, 1.0))
111
112        renderer.text(self._message, (bx + 20, by + 30), fill=(1.0, 1.0, 1.0, 1.0), scale=1.2)
113
114        btn_y = by + bh - 50
115        for i, label in enumerate(("Yes", "No")):
116            btn_x = bx + 80 + i * 160
117            if i == self._selected:
118                renderer.rect((btn_x, btn_y), (80, 30), fill=(0.3, 0.3, 0.45, 1.0))
119                colour = (1.0, 0.9, 0.3, 1.0)
120            else:
121                renderer.rect((btn_x, btn_y), (80, 30), fill=(0.2, 0.2, 0.25, 1.0))
122                colour = (0.6, 0.6, 0.6, 1.0)
123            renderer.text(label, (btn_x + 25, btn_y + 7), fill=colour, scale=1.2)