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)