VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
MenuBuilder.hpp
Go to the documentation of this file.
1#pragma once
2
5#include <concepts>
6#include <functional>
7#include <memory>
8#include <optional>
9#include <string>
10#include <string_view>
11#include <type_traits>
12#include <utility>
13#include <vector>
14
15namespace VoltMod
16{
17
18/** Fixed or dynamically evaluated enabled state. */
20{
21public:
22 EnabledCondition() = default;
23 EnabledCondition(bool value) : _fixed(value) {}
24 template <std::predicate<int> Predicate>
25 requires(!std::same_as<std::remove_cvref_t<Predicate>, EnabledCondition>)
27 {}
28
29 bool operator()(int slot) const { return _predicate ? _predicate(slot) : _fixed; }
30
31private:
32 std::function<bool(int slot)> _predicate;
33 bool _fixed = true;
34};
35
36/** A row spec consumable as a @ref MenuItem. */
37template <class T>
38concept MenuRowSpec = requires(const T& row) {
39 { row.ToItem() } -> std::same_as<MenuItem>;
40};
41
42/** A plain action row. @ref Activate runs on E or a click. */
44{
45 std::string Label;
46 std::function<void(int slot)> Activate;
48
49 MenuItem ToItem() const;
50};
51
52/** A boolean row drawn as a switch. E and A/D run @ref Flip. */
54{
55 std::string Label;
56 std::function<bool(int slot)> Get;
57 std::function<void(int slot)> Flip;
59
60 MenuItem ToItem() const;
61};
62
63/** When a @ref ChoiceRow runs its `Commit`. */
64enum class ChoiceApply
65{
66 /** About 400 ms after the last step, so a burst of presses applies once. */
68 /** Only on E or a click; stepping just changes what the row shows. */
70};
71
72/** External storage for a @ref ChoiceRow selection. */
74{
75 std::function<int(int slot)> Get;
76 std::function<void(int slot, int index)> Set;
77};
78
79namespace Internal
80{
81/** @ref ChoiceRow's row with its values reduced to indices; build rows with ChoiceRow instead. */
82MenuItem ChoiceItem(std::string label, std::vector<std::string> choices,
83 std::function<void(int slot, int index)> commit, std::optional<ChoiceIndex> bind, int index,
85} // namespace Internal
86
87/** A labeled choice list. A/D wraps through the values. */
88template <class T>
90{
91 std::string Label;
92 std::vector<Labeled<T>> Choices;
93 /** Runs on E, or shortly after the last step unless @ref Apply says otherwise. */
94 std::function<void(int slot, const T& value)> Commit;
95 /** Optional external home for the selection; unset keeps it in the row. */
96 std::optional<ChoiceIndex> Bind;
97 /** Where an unbound row starts. */
98 int Index = 0;
101
103 {
104 std::vector<std::string> labels;
105 std::vector<T> values;
106 for (const auto& [label, value] : Choices)
107 {
108 labels.push_back(label);
109 values.push_back(value);
110 }
111
112 std::function<void(int slot, int index)> commitIndex;
113 if (Commit)
114 {
115 commitIndex = [values = std::move(values), commit = Commit](int slot, int index) {
116 commit(slot, values[static_cast<std::size_t>(index)]);
117 };
118 }
119 return Internal::ChoiceItem(Label, std::move(labels), std::move(commitIndex), Bind, Index, Enabled, Apply);
120 }
121};
122
123/**
124 * A free-text row. E pauses the menu and routes the player's next chat line to @ref Set; a false
125 * return re-prompts, and so does text longer than @ref MaxLength. R cancels.
126 */
128{
129 std::string Label;
130 std::string Prompt;
131 std::function<std::string(int slot)> Get;
132 std::function<bool(int slot, std::string_view text)> Set;
133 int MaxLength = 64;
135
136 MenuItem ToItem() const;
137};
138
139/** A link to another menu. @ref Build runs on E and its result is pushed onto the stack. */
141{
142 std::string Label;
143 std::function<std::shared_ptr<Menu>(int slot)> Build;
145 /** Reported as @ref MenuRow::Icon. */
146 std::string Icon;
147
148 MenuItem ToItem() const;
149};
150
151/** A heading or divider. The cursor skips it. */
153{
154 std::string Label;
155
156 MenuItem ToItem() const;
157};
158
159/** Fluent builder for menus and row specs. */
161{
162public:
163 explicit MenuBuilder(std::string title) { _menu.Title = std::move(title); }
164
165 /** Sets optional plain-text detail shown with the title. */
167 {
168 _menu.Subtitle = std::move(subtitle);
169 return *this;
170 }
171
172 /** Append a row built by hand. */
174 {
175 _menu.Items.push_back(std::move(item));
176 return *this;
177 }
178
179 /** Appends any compatible row spec. */
180 MenuBuilder& Add(const MenuRowSpec auto& row) { return Add(row.ToItem()); }
181
182 /** @ref ButtonRow with nothing but a label and a callback. */
183 MenuBuilder& Button(std::string label, std::function<void(int slot)> activate)
184 {
185 return Add(ButtonRow{.Label = std::move(label), .Activate = std::move(activate)});
186 }
187
188 /** @ref SubmenuRow with nothing but a label and a factory. */
189 MenuBuilder& Submenu(std::string label, std::function<std::shared_ptr<Menu>(int slot)> build)
190 {
191 return Add(SubmenuRow{.Label = std::move(label), .Build = std::move(build)});
192 }
193
194 /** @ref TextRow: a heading or divider. */
195 MenuBuilder& Text(std::string label) { return Add(TextRow{.Label = std::move(label)}); }
196
197 /** Sets a text row used only when no other rows were added. */
199 {
200 _emptyText = std::move(label);
201 return *this;
202 }
203
204 /** Returns the built menu. The builder must not be reused after this. */
205 std::shared_ptr<Menu> Build()
206 {
207 if (_menu.Items.empty() && !_emptyText.empty())
208 {
209 Text(std::move(_emptyText));
210 }
211
212 return std::make_shared<Menu>(std::move(_menu));
213 }
214
215private:
216 Menu _menu;
217 std::string _emptyText;
218};
219
220} // namespace VoltMod
bool operator()(int slot) const
EnabledCondition(Predicate predicate)
MenuBuilder & Add(const MenuRowSpec auto &row)
MenuBuilder & Add(MenuItem item)
MenuBuilder & Text(std::string label)
MenuBuilder & Button(std::string label, std::function< void(int slot)> activate)
MenuBuilder & Submenu(std::string label, std::function< std::shared_ptr< Menu >(int slot)> build)
MenuBuilder & Subtitle(std::string subtitle)
std::shared_ptr< Menu > Build()
MenuBuilder & EmptyText(std::string label)
MenuBuilder(std::string title)
MenuItem ChoiceItem(std::string label, std::vector< std::string > choices, std::function< void(int slot, int index)> commit, std::optional< ChoiceIndex > bind, int index, EnabledCondition enabled, ChoiceApply apply)
@ Text
Not selectable; a caption or a summary line.
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
std::function< void(int slot)> Activate
MenuItem ToItem() const
EnabledCondition Enabled
std::function< void(int slot, int index)> Set
std::function< int(int slot)> Get
EnabledCondition Enabled
std::vector< Labeled< T > > Choices
std::function< void(int slot, const T &value)> Commit
MenuItem ToItem() const
std::optional< ChoiceIndex > Bind
EnabledCondition Enabled
MenuItem ToItem() const
std::function< bool(int slot, std::string_view text)> Set
std::function< std::string(int slot)> Get
std::string Subtitle
std::string Title
std::vector< MenuItem > Items
EnabledCondition Enabled
MenuItem ToItem() const
std::function< std::shared_ptr< Menu >(int slot)> Build
std::string Label
MenuItem ToItem() const
MenuItem ToItem() const
std::function< bool(int slot)> Get
std::function< void(int slot)> Flip
EnabledCondition Enabled