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)