VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
CenterHtmlRender.cpp
Go to the documentation of this file.
2
6#include <algorithm>
7#include <sstream>
8#include <string>
9#include <string_view>
10
11namespace VoltMod
12{
13
14namespace Theme
15{
16constexpr std::string_view Gold = "#FFD700";
17constexpr std::string_view Amber = "#FF8C00";
18constexpr std::string_view WarmWhite = "#CCBBAA";
19constexpr std::string_view WarmGray = "#887755";
20constexpr std::string_view Disabled = "#665544";
21constexpr std::string_view NavGold = "#AA8833";
22constexpr std::string_view NavClose = "#AA4422";
23constexpr std::string_view NavBack = "#AA8833";
24} // namespace Theme
25
27{
28 std::ostringstream html;
29
30 // Show the path before the submenu title.
31 if (!header.Breadcrumb.empty())
32 {
33 html << "<font class='fontSize-s' color='" << Theme::WarmGray << "'>" << Strings::EscapeHtml(header.Breadcrumb)
34 << " › </font>";
35 }
36
37 // Titles may contain player input, so escape them before inserting markup.
38 html << "<font color='" << Theme::Gold << "'><b>" << Strings::EscapeHtml(header.Title) << "</b></font>";
39
40 if (!header.Subtitle.empty())
41 {
42 html << " <font class='fontSize-s' color='" << Theme::WarmGray << "'>" << Strings::EscapeHtml(header.Subtitle)
43 << "</font>";
44 }
45
46 if (header.Pages > 1)
47 {
48 html << " <font class='fontSize-s' color='" << Theme::WarmGray << "'>(" << (header.Page + 1) << "/"
49 << header.Pages << ")</font>";
50 }
51
52 html << "<br>";
53 return html.str();
54}
55
56static std::string FooterChunk(std::string_view keyColor, std::string_view keyText, const std::string& label)
57{
58 std::ostringstream html;
59 html << "<font color='" << keyColor << "'>" << keyText << "</font> "
60 << "<font color='" << Theme::WarmGray << "'>" << label << "</font>";
61 return html.str();
62}
63
65{
66 // Keep rendering when a consumer has no nav.* translation.
67 auto label = [&](std::string_view key, std::string_view fallback) {
68 return translations.GetOr(key, slot, fallback);
69 };
70
71 const std::string_view closeColor = isSubmenu ? Theme::NavBack : Theme::NavClose;
72 std::string closeLabel = isSubmenu ? label("nav.back", "Back") : label("nav.close", "Close");
73
74 // First row: W/S, the A/D hint for the current row (value-change or paging), and E.
75 std::ostringstream row1;
76 row1 << FooterChunk(Theme::NavGold, "[W/S]", label("nav.navigate", "Navigate"));
77
80 {
81 row1 << " · " << FooterChunk(Theme::NavGold, "[A/D]", label("nav.change", "Change"));
82 }
83 else if (isPaginated)
84 {
85 row1 << " · " << FooterChunk(Theme::NavGold, "[A/D]", label("nav.page", "Page"));
86 }
87
88 const std::string_view selectKey = selectedRowSteps ? "nav.confirm" : "nav.select";
89 const std::string_view selectFallback = selectedRowSteps ? "Confirm" : "Select";
91
92 std::string closeChunk = FooterChunk(closeColor, "[R]", closeLabel);
93
94 std::ostringstream html;
95 html << "<font class='fontSize-s'>" << row1.str();
96
97 // With an A/D hint there are four chunks - splitting onto two short rows is more reliable
98 // than relying on the HUD's word wrap, which sometimes pushes [R] past the visible area.
99 if (hasStepHint)
100 {
101 html << "<br>" << closeChunk;
102 }
103 else
104 {
105 html << " · " << closeChunk;
106 }
107
108 html << "</font>";
109 return html.str();
110}
111
112// One row as this renderer spells it: "Title", "Title: [ON]", "Title: &lt; 100 HP… &gt; *". The
113// model carries the two halves as plain text, so the decoration and the escaping are decided here.
114// The two markers mirror what the Panorama driver draws as classes: `…` while a stepped value is
115// still waiting to be applied, `*` for a moment after a value moved.
116static std::string RowText(const MenuRow& row)
117{
118 std::string text = Strings::EscapeHtml(row.Label);
119 if (row.Value.empty())
120 {
121 return text;
122 }
123
124 std::string value = Strings::EscapeHtml(row.Value);
125 if (row.Pending)
126 {
127 value += "…";
128 }
129
130 // The brackets say "this is a switch" and the arrows "A and D change this"; nothing else in a
131 // line of center HTML does.
132 if (row.Kind == MenuRowKind::Toggle)
133 {
134 value = "[" + value + "]";
135 }
136 else if (row.Kind == MenuRowKind::Choice)
137 {
138 value = "&lt; " + value + " &gt;";
139 }
140
141 text += ": " + value;
142 if (row.Changed)
143 {
144 text += " *";
145 }
146 return text;
147}
148
149// @p selected takes the cursor's row as it was described for the page, so the footer that needs it
150// does not describe the same row a second time.
151static std::string RenderItems(const CenterHtmlView& view, int pageStart, int pageEnd, MenuRow& selected)
152{
153 std::ostringstream html;
154
155 for (int i = pageStart; i < pageEnd; ++i)
156 {
157 const MenuRow row = view.Describe(i);
158 if (i == view.SelectedIndex)
159 {
160 selected = row;
161 }
162 std::string title = RowText(row);
163 bool selectable = row.Selectable;
164 bool enabled = row.Enabled;
165
166 if (!enabled)
167 {
168 html << "<font color='" << Theme::Disabled << "'>- " << title << "</font><br>";
169 }
170 else if (!selectable)
171 {
172 // Rendered without a cursor mark - the row is informational, not a target.
173 html << "<font color='" << Theme::WarmGray << "'>" << title << "</font><br>";
174 }
175 else if (i == view.SelectedIndex)
176 {
177 // No per-row [E]: the cursor signals selection, the footer carries the hint, and a
178 // shorter line avoids wrapping in long locales.
179 html << "<font color='" << Theme::Amber << "'><b>&gt; " << title << "</b></font><br>";
180 }
181 else
182 {
183 html << "<font color='" << Theme::WarmWhite << "'> " << title << "</font><br>";
184 }
185 }
186
187 return html.str();
188}
189
191{
192 if (!menu || !view.Describe)
193 {
194 return "";
195 }
196
197 int itemCount = static_cast<int>(menu->Items.size());
199 int currentPage = itemCount == 0 ? 0 : view.SelectedIndex / CenterHtmlRowsPerPage;
202
203 std::ostringstream html;
204
205 html << RenderHeader({.Title = menu->Title,
206 .Subtitle = menu->Subtitle,
207 .Breadcrumb = view.Breadcrumb,
208 .Page = currentPage,
209 .Pages = totalPages});
210
211 // Only the branch that draws it pays for it: the empty line is looked up here rather than by
212 // every caller on every frame, and the cursor's row comes back from the page that drew it.
213 MenuRow selected{.Enabled = false};
214 if (itemCount == 0)
215 {
216 const std::string empty = translations.GetOr("menu.empty", view.Slot, "Nothing here");
217 html << "<font color='" << Theme::WarmGray << "'>" << Strings::EscapeHtml(empty) << "</font><br>";
218 }
219 else
220 {
222 }
223
224 html << RenderFooter(view.IsSubmenu, totalPages > 1, selected.Enabled && selected.Steppable, view.Slot,
226
227 return html.str();
228}
229
230std::string RenderCaptureOverlay(const std::string& menuTitle, std::string_view prompt)
231{
232 std::ostringstream html;
233 // Escape the title and prompt before inserting caller-supplied text into markup.
234 html << "<font color='" << Theme::Gold << "'><b>" << Strings::EscapeHtml(menuTitle) << "</b></font><br>"
235 << "<font color='" << Theme::WarmWhite << "'>" << Strings::EscapeHtml(prompt) << "</font><br>"
236 << "<font class='fontSize-s' color='" << Theme::WarmGray << "'>Type your answer in chat</font><br>"
237 << "<font class='fontSize-s'>"
238 << "<font color='" << Theme::NavClose << "'>[R]</font> "
239 << "<font color='" << Theme::WarmGray << "'>Cancel</font>"
240 << "</font>";
241 return html.str();
242}
243
244} // namespace VoltMod
static std::string EscapeHtml(std::string_view text)
Definition Strings.cpp:185
Localization system. Loads one JSON file per language; nested objects flatten into dotted keys (categ...
constexpr std::string_view Gold
constexpr std::string_view NavGold
constexpr std::string_view WarmWhite
constexpr std::string_view Disabled
constexpr std::string_view Amber
constexpr std::string_view NavBack
constexpr std::string_view NavClose
constexpr std::string_view WarmGray
@ Choice
Carries one of several values, stepped left and right.
@ Toggle
Carries an on/off value.
std::string RenderMenuHtml(const Menu *menu, const CenterHtmlView &view, Translations &translations)
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
std::string RenderCaptureOverlay(const std::string &menuTitle, std::string_view prompt)
constexpr int PageCount(int items, int perPage)
Definition MenuModel.hpp:14
std::string RenderFooter(bool isSubmenu, bool isPaginated, bool selectedRowSteps, int slot, Translations &translations)
std::string RenderHeader(const CenterHtmlHeader &header)
static std::string RowText(const MenuRow &row)
constexpr int CenterHtmlRowsPerPage
static std::string FooterChunk(std::string_view keyColor, std::string_view keyText, const std::string &label)
static std::string RenderItems(const CenterHtmlView &view, int pageStart, int pageEnd, MenuRow &selected)