nodes/confirm_dialog.pyΒΆ

Part of Dungeon Explorer.

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