VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
PanoramaMenu.cpp
Go to the documentation of this file.
3#include <algorithm>
4#include <cstddef>
5#include <format>
6#include <utility>
7
8namespace VoltMod
9{
10
12 : _services(services),
13 _layout(layout),
14 _stack(*this, _services.Translations, services.Scheduler, services.Slots),
15 _sessions(services.Slots)
16{
17 _subs.Add(_services.Screens.Pressed += [this](const ButtonPress& press) { OnPress(press); });
18
19 // A held commit is applied by a timer, so redraw it here.
20 _subs.Add(_stack.Committed += [this](int slot) { Draw(slot); });
21}
22
24{
25 // A client still fetching the addon has no layout to draw.
26 return IsValidSlot(slot) && _services.Screens.Available() && _services.AddonManager.IsReady(slot);
27}
28
29bool PanoramaMenu::OpenSession(int slot, std::shared_ptr<Menu> menu, MenuOptions options)
30{
31 if (!menu || !CanShow(slot))
32 {
33 return false;
34 }
35
37 _stack.Push(slot, std::move(menu));
38 _sessions[slot].HomePage = options.HomePage;
39 ReadTabs(slot);
40
41 // Close the session when the layout cannot be shown.
42 Draw(slot);
43 if (!IsOpen(slot))
44 {
45 return false;
46 }
47
48 _services.Freeze.Open(slot, options.FreezeMovement);
49 return true;
50}
51
52void PanoramaMenu::Open(int slot, std::shared_ptr<Menu> menu)
53{
54 if (!menu)
55 {
56 return;
57 }
58 if (!IsOpen(slot))
59 {
60 OpenSession(slot, std::move(menu), {});
61 return;
62 }
63
64 _stack.Push(slot, std::move(menu));
65 _sessions[slot].Page = 0;
66 Draw(slot);
67}
68
70{
71 return _stack.IsOpen(slot);
72}
73
75{
76 if (!IsOpen(slot))
77 {
78 return;
79 }
80
81 _services.ChatInput.CancelCapture(slot);
82 _stack.Pop(slot);
83 if (!IsOpen(slot))
84 {
85 return Hide(slot);
86 }
87
88 Session& session = _sessions[slot];
89 session.Page = 0;
90 if (_stack.Depth(slot) == 1)
91 {
92 session.SelectedTab = -1;
93 }
94 Draw(slot);
95}
96
98{
99 if (!IsOpen(slot))
100 {
101 return;
102 }
103
104 _stack.Clear(slot);
105 Hide(slot);
106}
107
108void PanoramaMenu::CloseAll(int slot, std::string_view replyKey)
109{
110 // Reply before removing the player's menus.
111 if (auto& reply = _services.Policy.Reply; reply)
112 {
113 reply(slot, _services.Translations.Get(std::string(replyKey), slot));
114 }
115
116 CloseAll(slot);
117}
118
119void PanoramaMenu::Prompt(int slot, std::string prompt, std::function<bool(int, std::string_view)> callback)
120{
121 _services.ChatInput.BeginCapture(slot, std::move(prompt), std::move(callback));
122 DrawPrompt(slot);
123}
124
125std::string PanoramaMenu::Translate(int slot, std::string_view key, std::string_view fallback) const
126{
127 return _services.Translations.GetOr(key, slot, fallback);
128}
129
130int PanoramaMenu::ItemAt(int slot, int row) const
131{
132 return _sessions[slot].Page * _layout.RowCount() + row;
133}
134
135void PanoramaMenu::ReadTabs(int slot)
136{
137 // Cache the root's submenus for the session.
138 std::vector<Tab>& tabs = _sessions[slot].Tabs;
139 const int items = static_cast<int>(_stack.Root(slot)->Items.size());
140 for (int index = 0; index < items && static_cast<int>(tabs.size()) < _layout.TabCount(); ++index)
141 {
142 if (MenuRow described = _stack.Describe(slot, index); described.Kind == MenuRowKind::Submenu)
143 {
144 tabs.push_back(
145 {.RootIndex = index, .Label = std::move(described.Label), .Icon = std::move(described.Icon)});
146 }
147 }
148}
149
150void PanoramaMenu::Draw(int slot)
151{
152 const Menu* menu = _stack.Current(slot);
153 if (!menu)
154 {
155 return;
156 }
157
158 // This also restores the screen after a map change removes it.
159 if (!_layout.Show(slot))
160 {
161 return CloseAll(slot);
162 }
163
164 // The root subtitle already appears below the brand.
165 const Menu& root = *_stack.Root(slot);
166 const bool isRoot = menu == &root;
167 _layout.SetHeader(slot, {.Brand = root.Title,
168 .BrandSubtitle = root.Subtitle,
169 .Breadcrumb = _stack.Breadcrumb(slot),
170 .Title = menu->Title,
171 .Subtitle = isRoot ? std::string_view{} : std::string_view(menu->Subtitle)});
172 _layout.SetHomeVisible(slot, isRoot && _sessions[slot].HomePage);
173 DrawTabs(slot);
174 DrawRows(slot, *menu);
175 DrawPrompt(slot);
176
177 _layout.SetFooter(slot, isRoot ? std::string{} : Translate(slot, "nav.back", "Back"),
178 Translate(slot, "menu.cancel", "Cancel"));
179}
180
181void PanoramaMenu::DrawTabs(int slot)
182{
183 const Session& session = _sessions[slot];
184 const int shown = static_cast<int>(session.Tabs.size());
185
186 // Do not render a sidebar containing only the title.
187 _layout.SetSidebarVisible(slot, shown > 0);
188
189 for (int index = 0; index < _layout.TabCount(); ++index)
190 {
191 if (index >= shown)
192 {
193 _layout.SetTab(slot, index, nullptr);
194 continue;
195 }
196
197 const Tab& tab = session.Tabs[static_cast<std::size_t>(index)];
198 const MenuTab drawn{.Label = tab.Label, .Icon = tab.Icon, .Selected = index == session.SelectedTab};
199 _layout.SetTab(slot, index, &drawn);
200 }
201}
202
203void PanoramaMenu::DrawRows(int slot, const Menu& menu)
204{
205 Session& session = _sessions[slot];
206 const int count = static_cast<int>(menu.Items.size());
207 const int pages = PageCount(count, _layout.RowCount());
208 session.Page = std::clamp(session.Page, 0, pages - 1);
209
210 std::string pendingHint;
211 for (int row = 0; row < _layout.RowCount(); ++row)
212 {
213 const int item = ItemAt(slot, row);
214 if (item >= count)
215 {
216 _layout.SetRow(slot, row, nullptr, {});
217 continue;
218 }
219
220 const MenuRow described = _stack.Describe(slot, item);
221 if (described.Pending && pendingHint.empty())
222 {
223 pendingHint = Translate(slot, "menu.pending", "Applying...");
224 }
225 _layout.SetRow(slot, row, &described, pendingHint);
226 }
227
228 _layout.SetEmpty(slot, count == 0 ? Translate(slot, "menu.empty", "Nothing here") : std::string{});
229 _layout.SetPager(slot, pages > 1 ? std::format("{} / {}", session.Page + 1, pages) : std::string{});
230}
231
232void PanoramaMenu::DrawPrompt(int slot)
233{
234 const auto prompt = _services.ChatInput.GetPrompt(slot);
235 _layout.SetPrompt(slot, prompt.value_or(""),
236 prompt ? Translate(slot, "menu.promptHint", "Type your answer in chat") : std::string{});
237}
238
239void PanoramaMenu::OnPress(const ButtonPress& press)
240{
241 const int slot = press.Slot;
242 if (!IsOpen(slot))
243 {
244 return;
245 }
246
247 const auto button = _layout.ButtonFor(press.ButtonId);
248 if (!button)
249 {
250 return;
251 }
252
253 // A prompt handles presses except Cancel; its answer arrives as chat input.
254 if (_services.ChatInput.IsCapturing(slot) && button->Kind != MenuButtonKind::Cancel)
255 {
256 return;
257 }
258
259 switch (button->Kind)
260 {
262 _services.ChatInput.CancelCapture(slot);
263 return Draw(slot);
265 return Close(slot);
267 return CloseAll(slot);
269 return TurnPage(slot, -1);
271 return TurnPage(slot, +1);
273 return OpenTab(slot, button->Index);
275 return Activate(slot, ItemAt(slot, button->Index));
277 return StepRow(slot, button->Index, -1);
279 return StepRow(slot, button->Index, +1);
280 }
281}
282
283void PanoramaMenu::Activate(int slot, int index)
284{
285 // Record the branch's tab so it stays selected.
286 Session& session = _sessions[slot];
287 if (_stack.Depth(slot) == 1)
288 {
289 const auto found = std::ranges::find(session.Tabs, index, &Tab::RootIndex);
290 session.SelectedTab = found == session.Tabs.end() ? -1 : static_cast<int>(found - session.Tabs.begin());
291 }
292
293 // Activation may replace or close the session, so redraw only if it remains active.
294 _stack.Activate(slot, index);
295 Draw(slot);
296}
297
298void PanoramaMenu::StepRow(int slot, int row, int direction)
299{
300 if (_stack.Step(slot, ItemAt(slot, row), direction))
301 {
302 Draw(slot);
303 }
304}
305
306void PanoramaMenu::OpenTab(int slot, int tab)
307{
308 Session& session = _sessions[slot];
309 if (tab < 0 || tab >= static_cast<int>(session.Tabs.size()))
310 {
311 return;
312 }
313
314 // A tab jumps from the root instead of pushing another menu.
315 _stack.PopToRoot(slot);
316 session.Page = 0;
317 Activate(slot, session.Tabs[static_cast<std::size_t>(tab)].RootIndex);
318}
319
320void PanoramaMenu::TurnPage(int slot, int delta)
321{
322 const Menu* menu = _stack.Current(slot);
323 if (!menu)
324 {
325 return;
326 }
327
328 _stack.ApplyPending(slot);
329 Session& session = _sessions[slot];
330 session.Page = WrapIndex(session.Page + delta, PageCount(static_cast<int>(menu->Items.size()), _layout.RowCount()));
331 Draw(slot);
332}
333
334void PanoramaMenu::Hide(int slot)
335{
336 _sessions[slot] = {};
337 _services.ChatInput.CancelCapture(slot);
338 _services.Freeze.Close(slot);
339 _layout.Hide(slot);
340}
341
342} // namespace VoltMod
void CancelCapture(int slot)
Definition ChatInput.cpp:70
void BeginCapture(int slot, std::string prompt, Callback callback, int timeoutMs=60000)
Definition ChatInput.cpp:14
std::optional< std::string > GetPrompt(int slot) const
Definition ChatInput.cpp:80
bool IsCapturing(int slot) const
Definition ChatInput.cpp:32
void Close(int slot)
void Open(int slot, bool requested)
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)
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)
The screen built from the menu Panorama block, as PanoramaMenu draws on it.
void SetEmpty(int slot, std::string_view text)
std::optional< MenuButton > ButtonFor(std::string_view id) const
void SetHomeVisible(int slot, bool visible)
void SetFooter(int slot, std::string_view back, std::string_view cancel)
void SetPrompt(int slot, std::string_view text, std::string_view hint)
void SetHeader(int slot, const MenuHeader &header)
void SetRow(int slot, int index, const MenuRow *row, std::string_view pendingHint)
void SetPager(int slot, std::string_view text)
void SetTab(int slot, int index, const MenuTab *tab)
void SetSidebarVisible(int slot, bool visible)
void CloseAll(int slot) override
PanoramaMenu(const Services &services, PanoramaMenuLayout &layout)
void Prompt(int slot, std::string prompt, std::function< bool(int slot, std::string_view text)> callback) override
std::string Translate(int slot, std::string_view key, std::string_view fallback) const override
void Close(int slot) override
void Open(int slot, std::shared_ptr< Menu > menu) override
bool CanShow(int slot) const
bool OpenSession(int slot, std::shared_ptr< Menu > menu, MenuOptions options) override
bool IsOpen(int slot) const override
std::function< void(int slot, std::string_view message)> Reply
Definition Policy.hpp:63
One-shot delays and repeating timers, run on the game thread.
Definition Scheduler.hpp:18
Event< const ButtonPress & > Pressed
void Add(Subscription subscription)
Localization system. Loads one JSON file per language; nested objects flatten into dotted keys (categ...
std::string Get(std::string_view key) const
std::string GetOr(std::string_view key, int slot, std::string_view fallback) const
@ Submenu
Opens another menu.
constexpr int WrapIndex(int value, int count)
Definition MenuModel.hpp:20
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
constexpr int PageCount(int items, int perPage)
Definition MenuModel.hpp:14
constexpr bool IsValidSlot(int slot)
Definition Slot.hpp:13
A Button press inside a custom HUD layout.
MenuRowKind Kind
Definition MenuModel.hpp:42
std::vector< MenuItem > Items
MultiAddonManager & AddonManager
VoltMod::Translations & Translations
VoltMod::ChatInput & ChatInput
VoltMod::MenuFreeze & Freeze