VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
MenuStack.cpp
Go to the documentation of this file.
4#include <cstddef>
5#include <utility>
6
7namespace VoltMod
8{
9
10static constexpr std::string_view kBreadcrumbSeparator = " › ";
11
13{
14 return [&scheduler](int64_t ms, std::function<void()> run) { return scheduler.Delay(ms, std::move(run)); };
15}
16
20
24
26{
27 if (!IsValidSlot(slot) || _states[slot].Menus.empty())
28 {
29 return nullptr;
30 }
31 return _states[slot].Menus.back().get();
32}
33
35{
36 if (!IsValidSlot(slot) || _states[slot].Menus.empty())
37 {
38 return nullptr;
39 }
40 return _states[slot].Menus.front().get();
41}
42
43int MenuStack::Depth(int slot) const
44{
45 return IsValidSlot(slot) ? static_cast<int>(_states[slot].Menus.size()) : 0;
46}
47
48bool MenuStack::IsOpen(int slot) const
49{
50 return Depth(slot) > 0;
51}
52
54{
55 for (int slot = 0; slot < MaxPlayers; ++slot)
56 {
57 if (!_states[slot].Menus.empty())
58 {
59 return true;
60 }
61 }
62 return false;
63}
64
65std::string_view MenuStack::Breadcrumb(int slot) const
66{
67 return IsValidSlot(slot) ? std::string_view(_states[slot].Breadcrumb) : std::string_view{};
68}
69
70void MenuStack::Push(int slot, std::shared_ptr<Menu> menu)
71{
72 if (!IsValidSlot(slot) || !menu)
73 {
74 return;
75 }
76
77 _states[slot].Menus.push_back(std::move(menu));
78 Rebuild(slot);
79}
80
82{
83 if (!IsValidSlot(slot) || _states[slot].Menus.empty())
84 {
85 return;
86 }
87
88 // Apply a stepped value before changing the stack.
90
91 State& state = _states[slot];
92 state.Menus.pop_back();
93 if (state.Menus.empty())
94 {
95 state = {};
96 }
97 else
98 {
99 Rebuild(slot);
100 }
101}
102
104{
105 if (!IsValidSlot(slot))
106 {
107 return;
108 }
109
111 _states[slot] = {};
112}
113
115{
116 if (!IsValidSlot(slot) || _states[slot].Menus.empty())
117 {
118 return;
119 }
120
122 _states[slot].Menus.resize(1);
123 Rebuild(slot);
124}
125
126void MenuStack::Rebuild(int slot)
127{
128 State& state = _states[slot];
129 state.Rows.clear();
130
131 // The path leading to the current menu: every title below the top one.
132 state.Breadcrumb.clear();
133 for (std::size_t i = 0; i + 1 < state.Menus.size(); ++i)
134 {
135 if (!state.Breadcrumb.empty())
136 {
137 state.Breadcrumb += kBreadcrumbSeparator;
138 }
139 state.Breadcrumb += state.Menus[i]->Title;
140 }
141}
142
144{
145 Menu* menu = Current(slot);
146 if (!menu || index < 0 || index >= static_cast<int>(menu->Items.size()))
147 {
148 return MenuRow{.Enabled = false, .Selectable = false};
149 }
150
151 // A malformed item is shown as a disabled line.
152 const MenuItem& item = menu->Items[static_cast<std::size_t>(index)];
153 MenuRow row = item.Describe ? item.Describe(slot) : MenuRow{.Enabled = false, .Selectable = false};
154
155 // A toggle reports its state, not its words, so every switch reads the same and localizes here.
156 if (row.Kind == MenuRowKind::Toggle && row.Value.empty() && row.State.has_value())
157 {
158 row.Value =
159 *row.State ? _translations.GetOr("menu.on", slot, "ON") : _translations.GetOr("menu.off", slot, "OFF");
160 }
161
162 State& state = _states[slot];
163 if (state.Rows.size() != menu->Items.size())
164 {
165 state.Rows.assign(menu->Items.size(), RowMemory{});
166 }
167
168 const int64_t now = Time::MonotonicMs();
169 RowMemory& memory = state.Rows[static_cast<std::size_t>(index)];
170 if (memory.Value != row.Value)
171 {
172 // Arriving on screen is not a change: only a value that moves under a row already drawn
173 // is worth flashing.
174 if (memory.Drawn)
175 {
176 memory.ChangedAt = now;
177 }
178 memory.Value = row.Value;
179 memory.Drawn = true;
180 }
181
182 row.Changed = memory.ChangedAt != 0 && now - memory.ChangedAt < ChangedMs;
183 row.Pending = _pending.IsPending(slot, index);
184 return row;
185}
186
187void MenuStack::Activate(int slot, int index)
188{
189 if (!IsValidSlot(slot))
190 {
191 return;
192 }
193
194 // A row whose activation *is* its commit - a ChoiceRow's E - would apply the value twice if
195 // the held one ran as well, so pressing the pending row cancels the wait and lets the
196 // activation apply it. Any other row runs what is held first.
197 if (_pending.IsPending(slot, index))
198 {
199 _pending.Drop(slot);
200 }
201 else
202 {
203 _pending.Apply(slot);
204 }
205
206 // The callback may close or replace the menu, so read the stack again.
207 Menu* menu = Current(slot);
208 if (!menu || index < 0 || index >= static_cast<int>(menu->Items.size()))
209 {
210 return;
211 }
212
213 // Copy the item because its handler may close or reopen the menu.
214 const MenuItem item = menu->Items[static_cast<std::size_t>(index)];
215 if (item.Activate && IsRowActionable(item, slot))
216 {
217 item.Activate(slot, _surface);
218 }
219}
220
221bool MenuStack::Step(int slot, int index, int direction)
222{
223 Menu* menu = Current(slot);
224 if (!menu || index < 0 || index >= static_cast<int>(menu->Items.size()))
225 {
226 return false;
227 }
228
229 // Copy the item and keep the menu alive while its step handler runs.
230 const std::shared_ptr<Menu> held = _states[slot].Menus.back();
231 const MenuItem item = menu->Items[static_cast<std::size_t>(index)];
232 if (!item.Step || !item.Describe || !item.Describe(slot).Enabled)
233 {
234 return false;
235 }
236 if (!item.Step(slot, direction))
237 {
238 return false;
239 }
240
241 // A burst of presses is one commit. Do not hold by index if the step replaced the menu.
242 if (item.Commit && Current(slot) == held.get())
243 {
244 _pending.Hold(slot, index, [this, commit = item.Commit, slot] {
245 commit(slot);
247 });
248 }
249
250 return true;
251}
252
253void MenuStack::ApplyPending(int slot)
254{
255 _pending.Apply(slot);
256}
257
258bool MenuStack::IsPending(int slot, int index) const
259{
260 return _pending.IsPending(slot, index);
261}
262
263} // namespace VoltMod
void Raise(Args... args)
Definition Event.hpp:98
One player's open menus, and the row behaviour every surface shares.
Definition MenuStack.hpp:35
MenuStack(MenuSurface &surface, Translations &translations, Scheduler &scheduler, SlotEvents &slots)
Definition MenuStack.cpp:17
bool IsOpen(int slot) const
Definition MenuStack.cpp:48
void Pop(int slot)
Definition MenuStack.cpp:81
Menu * Current(int slot)
Definition MenuStack.cpp:25
void Activate(int slot, int index)
std::string_view Breadcrumb(int slot) const
Definition MenuStack.cpp:65
void Push(int slot, std::shared_ptr< Menu > menu)
Definition MenuStack.cpp:70
MenuRow Describe(int slot, int index)
bool AnyOpen() const
Definition MenuStack.cpp:53
void PopToRoot(int slot)
void ApplyPending(int slot)
Event< int > Committed
Definition MenuStack.hpp:97
void Clear(int slot)
int Depth(int slot) const
Definition MenuStack.cpp:43
Menu * Root(int slot)
Definition MenuStack.cpp:34
bool Step(int slot, int index, int direction)
bool IsPending(int slot, int index) const
void Hold(int slot, int index, std::function< void()> commit)
std::function< Subscription(int64_t delayMs, std::function< void()> callback)> Timer
One-shot delays and repeating timers, run on the game thread.
Definition Scheduler.hpp:18
"The occupant of this slot changed" - raised by PlayerManager, consumed below it.
static int64_t MonotonicMs()
Definition Durations.hpp:35
Localization system. Loads one JSON file per language; nested objects flatten into dotted keys (categ...
std::string GetOr(std::string_view key, int slot, std::string_view fallback) const
@ Toggle
Carries an on/off value.
static constexpr std::string_view kBreadcrumbSeparator
Definition MenuStack.cpp:10
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
constexpr bool IsValidSlot(int slot)
Definition Slot.hpp:13
bool IsRowActionable(const MenuItem &item, int slot)
static PendingCommit::Timer TimerOn(Scheduler &scheduler)
Definition MenuStack.cpp:12
constexpr int MaxPlayers
Definition Slot.hpp:7
std::function< void(int slot, MenuSurface &surface)> Activate