OpenTTD Source 20250527-master-g808af15975
newgrf_badge_gui.cpp
1/*
2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6 */
7
10#include "stdafx.h"
11
12#include "core/flatset_type.hpp"
13#include "dropdown_type.h"
14#include "dropdown_func.h"
15#include "newgrf.h"
16#include "newgrf_badge.h"
17#include "newgrf_badge_config.h"
18#include "newgrf_badge_gui.h"
19#include "newgrf_badge_type.h"
20#include "settings_gui.h"
21#include "strings_func.h"
23#include "window_gui.h"
24#include "zoom_func.h"
25
26#include "table/strings.h"
27
29
30#include "safeguards.h"
31
32static constexpr uint MAX_BADGE_HEIGHT = 12;
33static constexpr uint MAX_BADGE_WIDTH = MAX_BADGE_HEIGHT * 2;
34
41static Dimension GetBadgeMaximalDimension(BadgeClassID class_index, GrfSpecFeature feature)
42{
43 Dimension d = {0, MAX_BADGE_HEIGHT};
44
45 for (const auto &badge : GetBadges()) {
46 if (badge.class_index != class_index) continue;
47
48 PalSpriteID ps = GetBadgeSprite(badge, feature, std::nullopt, PAL_NONE);
49 if (ps.sprite == 0) continue;
50
51 d.width = std::max(d.width, GetSpriteSize(ps.sprite, nullptr, ZoomLevel::Normal).width);
52 if (d.width > MAX_BADGE_WIDTH) break;
53 }
54
55 d.width = std::min(d.width, MAX_BADGE_WIDTH);
56 return d;
57}
58
59static bool operator<(const GUIBadgeClasses::Element &a, const GUIBadgeClasses::Element &b)
60{
61 if (a.column_group != b.column_group) return a.column_group < b.column_group;
62 if (a.sort_order != b.sort_order) return a.sort_order < b.sort_order;
63 return a.label < b.label;
64}
65
70GUIBadgeClasses::GUIBadgeClasses(GrfSpecFeature feature)
71{
72 /* Get list of classes used by feature. */
73 UsedBadgeClasses used(feature);
74
75 uint max_column = 0;
76 for (BadgeClassID class_index : used.Classes()) {
77 const Badge *class_badge = GetClassBadge(class_index);
78 if (class_badge->name == STR_NULL) continue;
79
80 Dimension size = GetBadgeMaximalDimension(class_index, feature);
81 if (size.width == 0) continue;
82
83 auto [config, sort_order] = GetBadgeClassConfigItem(feature, class_badge->label);
84
85 this->gui_classes.emplace_back(class_index, config.column, config.show_icon, sort_order, size, class_badge->label);
86 if (config.show_icon) max_column = std::max<uint>(max_column, config.column);
87 }
88
89 std::sort(std::begin(this->gui_classes), std::end(this->gui_classes));
90
91 /* Determine total width of visible badge columns. */
92 this->column_widths.resize(max_column + 1);
93 for (const auto &el : this->gui_classes) {
94 if (!el.visible) continue;
95 this->column_widths[el.column_group] += ScaleGUITrad(el.size.width) + WidgetDimensions::scaled.hsep_normal;
96 }
97
98 /* Replace trailing `hsep_normal` spacer with wider `hsep_wide` spacer. */
99 for (uint &badge_width : this->column_widths) {
100 if (badge_width == 0) continue;
102 }
103}
104
110{
111 return std::accumulate(std::begin(this->column_widths), std::end(this->column_widths), 0U);
112}
113
121int DrawBadgeNameList(Rect r, std::span<const BadgeID> badges, GrfSpecFeature)
122{
123 if (badges.empty()) return r.top;
124
125 FlatSet<BadgeClassID> class_indexes;
126 for (const BadgeID &index : badges) class_indexes.insert(GetBadge(index)->class_index);
127
128 std::string_view list_separator = GetListSeparator();
129 for (const BadgeClassID &class_index : class_indexes) {
130 const Badge *class_badge = GetClassBadge(class_index);
131 if (class_badge == nullptr || class_badge->name == STR_NULL) continue;
132
133 std::string s;
134 for (const BadgeID &index : badges) {
135 const Badge *badge = GetBadge(index);
136 if (badge == nullptr || badge->name == STR_NULL) continue;
137 if (badge->class_index != class_index) continue;
138
139 if (!s.empty()) {
140 if (badge->flags.Test(BadgeFlag::NameListFirstOnly)) continue;
141 s += list_separator;
142 }
143 AppendStringInPlace(s, badge->name);
144 if (badge->flags.Test(BadgeFlag::NameListStop)) break;
145 }
146
147 if (s.empty()) continue;
148
149 r.top = DrawStringMultiLine(r, GetString(STR_BADGE_NAME_LIST, class_badge->name, std::move(s)), TC_BLACK);
150 }
151
152 return r.top;
153}
154
165void DrawBadgeColumn(Rect r, int column_group, const GUIBadgeClasses &gui_classes, std::span<const BadgeID> badges, GrfSpecFeature feature, std::optional<TimerGameCalendar::Date> introduction_date, PaletteID remap)
166{
167 bool rtl = _current_text_dir == TD_RTL;
168 for (const auto &gc : gui_classes.GetClasses()) {
169 if (gc.column_group != column_group) continue;
170 if (!gc.visible) continue;
171
172 int width = ScaleGUITrad(gc.size.width);
173 for (const BadgeID &index : badges) {
174 const Badge &badge = *GetBadge(index);
175 if (badge.class_index != gc.class_index) continue;
176
177 PalSpriteID ps = GetBadgeSprite(badge, feature, introduction_date, remap);
178 if (ps.sprite == 0) continue;
179
180 DrawSpriteIgnorePadding(ps.sprite, ps.pal, r.WithWidth(width, rtl), SA_CENTER);
181 break;
182 }
183
184 r = r.Indent(width + WidgetDimensions::scaled.hsep_normal, rtl);
185 }
186}
187
189template <class TBase, bool TEnd = true, FontSize TFs = FS_NORMAL>
190class DropDownBadges : public TBase {
191public:
192 template <typename... Args>
193 explicit DropDownBadges(const std::shared_ptr<GUIBadgeClasses> &gui_classes, std::span<const BadgeID> badges, GrfSpecFeature feature, std::optional<TimerGameCalendar::Date> introduction_date, Args &&...args) :
194 TBase(std::forward<Args>(args)...), gui_classes(gui_classes), badges(badges), feature(feature), introduction_date(introduction_date)
195 {
196 for (const auto &gc : gui_classes->GetClasses()) {
197 if (gc.column_group != 0) continue;
198 dim.width += gc.size.width + WidgetDimensions::scaled.hsep_normal;
199 dim.height = std::max(dim.height, gc.size.height);
200 }
201 }
202
203 uint Height() const override
204 {
205 return std::max<uint>(this->dim.height, this->TBase::Height());
206 }
207
208 uint Width() const override
209 {
210 return this->dim.width + WidgetDimensions::scaled.hsep_wide + this->TBase::Width();
211 }
212
213 int OnClick(const Rect &r, const Point &pt) const override
214 {
215 bool rtl = TEnd ^ (_current_text_dir == TD_RTL);
216 return this->TBase::OnClick(r.Indent(this->dim.width + WidgetDimensions::scaled.hsep_wide, rtl), pt);
217 }
218
219 void Draw(const Rect &full, const Rect &r, bool sel, int click_result, Colours bg_colour) const override
220 {
221 bool rtl = TEnd ^ (_current_text_dir == TD_RTL);
222
223 DrawBadgeColumn(r.WithWidth(this->dim.width, rtl), 0, *this->gui_classes, this->badges, this->feature, this->introduction_date, PAL_NONE);
224
225 this->TBase::Draw(full, r.Indent(this->dim.width + WidgetDimensions::scaled.hsep_wide, rtl), sel, click_result, bg_colour);
226 }
227
228private:
229 std::shared_ptr<GUIBadgeClasses> gui_classes;
230
231 const std::span<const BadgeID> badges;
232 const GrfSpecFeature feature;
233 const std::optional<TimerGameCalendar::Date> introduction_date;
234
235 Dimension dim{};
236};
237
240
241std::unique_ptr<DropDownListItem> MakeDropDownListBadgeItem(const std::shared_ptr<GUIBadgeClasses> &gui_classes, std::span<const BadgeID> badges, GrfSpecFeature feature, std::optional<TimerGameCalendar::Date> introduction_date, std::string &&str, int value, bool masked, bool shaded)
242{
243 return std::make_unique<DropDownListBadgeItem>(gui_classes, badges, feature, introduction_date, std::move(str), value, masked, shaded);
244}
245
246std::unique_ptr<DropDownListItem> MakeDropDownListBadgeIconItem(const std::shared_ptr<GUIBadgeClasses> &gui_classes, std::span<const BadgeID> badges, GrfSpecFeature feature, std::optional<TimerGameCalendar::Date> introduction_date, const Dimension &dim, SpriteID sprite, PaletteID palette, std::string &&str, int value, bool masked, bool shaded)
247{
248 return std::make_unique<DropDownListBadgeIconItem>(gui_classes, badges, feature, introduction_date, dim, sprite, palette, std::move(str), value, masked, shaded);
249}
250
254template <class TBase, bool TEnd = true, FontSize TFs = FS_NORMAL>
255class DropDownMover : public TBase {
256public:
257 template <typename... Args>
258 explicit DropDownMover(int click_up, int click_down, Colours button_colour, Args &&...args)
259 : TBase(std::forward<Args>(args)...), click_up(click_up), click_down(click_down), button_colour(button_colour)
260 {
261 }
262
263 uint Height() const override
264 {
265 return std::max<uint>(SETTING_BUTTON_HEIGHT, this->TBase::Height());
266 }
267
268 uint Width() const override
269 {
270 return SETTING_BUTTON_WIDTH + WidgetDimensions::scaled.hsep_wide + this->TBase::Width();
271 }
272
273 int OnClick(const Rect &r, const Point &pt) const override
274 {
275 bool rtl = (_current_text_dir == TD_RTL);
276 int w = SETTING_BUTTON_WIDTH;
277
278 Rect br = r.WithWidth(w, TEnd ^ rtl).CentreTo(w, SETTING_BUTTON_HEIGHT);
279 if (br.WithWidth(w / 2, rtl).Contains(pt)) return this->click_up;
280 if (br.WithWidth(w / 2, !rtl).Contains(pt)) return this->click_down;
281
282 return this->TBase::OnClick(r.Indent(w + WidgetDimensions::scaled.hsep_wide, TEnd ^ rtl), pt);
283 }
284
285 void Draw(const Rect &full, const Rect &r, bool sel, int click_result, Colours bg_colour) const override
286 {
287 bool rtl = (_current_text_dir == TD_RTL);
288 int w = SETTING_BUTTON_WIDTH;
289
290 int state = 0;
291 if (sel && click_result != 0) {
292 if (click_result == this->click_up) state = 1;
293 if (click_result == this->click_down) state = 2;
294 }
295
296 Rect br = r.WithWidth(w, TEnd ^ rtl).CentreTo(w, SETTING_BUTTON_HEIGHT);
297 DrawUpDownButtons(br.left, br.top, this->button_colour, state, this->click_up != 0, this->click_down != 0);
298
299 this->TBase::Draw(full, r.Indent(w + WidgetDimensions::scaled.hsep_wide, TEnd ^ rtl), sel, click_result, bg_colour);
300 }
301
302private:
306};
307
309
310enum BadgeClick : int {
311 BADGE_CLICK_NONE,
312 BADGE_CLICK_MOVE_UP,
313 BADGE_CLICK_MOVE_DOWN,
314 BADGE_CLICK_TOGGLE_ICON,
315};
316
317DropDownList BuildBadgeClassConfigurationList(const GUIBadgeClasses &gui_classes, uint columns, std::span<const StringID> column_separators)
318{
319 DropDownList list;
320
321 list.push_back(MakeDropDownListStringItem(STR_BADGE_CONFIG_RESET, INT_MAX));
322 if (gui_classes.GetClasses().empty()) return list;
323 list.push_back(MakeDropDownListDividerItem());
324
325 const BadgeClassID front = gui_classes.GetClasses().front().class_index;
326 const BadgeClassID back = gui_classes.GetClasses().back().class_index;
327
328 for (uint i = 0; i < columns; ++i) {
329 for (const auto &gc : gui_classes.GetClasses()) {
330 if (gc.column_group != i) continue;
331
332 bool first = (i == 0 && gc.class_index == front);
333 bool last = (i == columns - 1 && gc.class_index == back);
334 list.push_back(std::make_unique<DropDownListToggleMoverItem>(first ? 0 : BADGE_CLICK_MOVE_UP, last ? 0 : BADGE_CLICK_MOVE_DOWN, COLOUR_YELLOW, gc.visible, BADGE_CLICK_TOGGLE_ICON, COLOUR_YELLOW, COLOUR_GREY, GetString(GetClassBadge(gc.class_index)->name), gc.class_index.base()));
335 }
336
337 if (i >= column_separators.size()) continue;
338
339 if (column_separators[i] == STR_NULL) {
340 list.push_back(MakeDropDownListDividerItem());
341 } else {
342 list.push_back(MakeDropDownListStringItem(column_separators[i], INT_MIN + i, false, true));
343 }
344 }
345
346 return list;
347}
348
355static void BadgeClassToggleVisibility(GrfSpecFeature feature, Badge &class_badge, int click_result)
356{
357 auto config = GetBadgeClassConfiguration(feature);
358 auto it = std::ranges::find(config, class_badge.label, &BadgeClassConfigItem::label);
359 if (it == std::end(config)) return;
360
361 if (click_result == BADGE_CLICK_TOGGLE_ICON) it->show_icon = !it->show_icon;
362}
363
369static void BadgeClassMovePrevious(GrfSpecFeature feature, Badge &class_badge)
370{
371 GUIBadgeClasses gui_classes(feature);
372 if (gui_classes.GetClasses().empty()) return;
373
374 auto config = GetBadgeClassConfiguration(feature);
375 auto it = std::ranges::find(config, class_badge.label, &BadgeClassConfigItem::label);
376 if (it == std::end(config)) return;
377
378 auto pos_cur = std::ranges::find(gui_classes.GetClasses(), class_badge.class_index, &GUIBadgeClasses::Element::class_index);
379 if (pos_cur == std::begin(gui_classes.GetClasses())) {
380 if (it->column > 0) --it->column;
381 return;
382 }
383
384 auto pos_prev = std::ranges::find(config, std::prev(pos_cur)->label, &BadgeClassConfigItem::label);
385 if (it->column > pos_prev->column) {
386 --it->column;
387 } else {
388 /* Rotate elements right so that it is placed before pos_prev, maintaining order of non-visible elements. */
389 std::rotate(pos_prev, it, std::next(it));
390 }
391}
392
399static void BadgeClassMoveNext(GrfSpecFeature feature, Badge &class_badge, uint columns)
400{
401 GUIBadgeClasses gui_classes(feature);
402 if (gui_classes.GetClasses().empty()) return;
403
404 auto config = GetBadgeClassConfiguration(feature);
405 auto it = std::ranges::find(config, class_badge.label, &BadgeClassConfigItem::label);
406 if (it == std::end(config)) return;
407
408 auto pos_cur = std::ranges::find(gui_classes.GetClasses(), class_badge.class_index, &GUIBadgeClasses::Element::class_index);
409 if (std::next(pos_cur) == std::end(gui_classes.GetClasses())) {
410 if (it->column < static_cast<int>(columns - 1)) ++it->column;
411 return;
412 }
413
414 auto pos_next = std::ranges::find(config, std::next(pos_cur)->label, &BadgeClassConfigItem::label);
415 if (it->column < pos_next->column) {
416 ++it->column;
417 } else {
418 /* Rotate elements left so that it is placed after pos_next, maintaining order of non-visible elements. */
419 std::rotate(it, std::next(it), std::next(pos_next));
420 }
421}
422
431bool HandleBadgeConfigurationDropDownClick(GrfSpecFeature feature, uint columns, int result, int click_result)
432{
433 if (result == INT_MAX) {
435 return true;
436 }
437
438 Badge *class_badge = GetClassBadge(static_cast<BadgeClassID>(result));
439 if (class_badge == nullptr) return false;
440
441 switch (click_result) {
442 case BADGE_CLICK_MOVE_DOWN: // Move down button.
443 BadgeClassMoveNext(feature, *class_badge, columns);
444 break;
445 case BADGE_CLICK_MOVE_UP: // Move up button.
446 BadgeClassMovePrevious(feature, *class_badge);
447 break;
448 case BADGE_CLICK_TOGGLE_ICON:
449 BadgeClassToggleVisibility(feature, *class_badge, click_result);
450 break;
451 default:
452 break;
453 }
454
455 return true;
456}
std::string label
Class label.
BadgeClassID class_index
Index of class this badge belongs to.
std::string label
Label of badge.
BadgeFlags flags
Display flags.
StringID name
Short name.
constexpr bool Test(Tvalue_type value) const
Test if the value-th bit is set.
Drop down element that draws a list of badges.
Drop down component that shows extra buttons to indicate that the item can be moved up or down.
int click_down
Click result for down button. Button is inactive if 0.
Colours button_colour
Colour of buttons.
int click_up
Click result for up button. Button is inactive if 0.
Flat set implementation that uses a sorted vector for storage.
uint GetTotalColumnsWidth() const
Get total width of all columns.
Utility class to create a list of badge classes used by a feature.
static WidgetDimensions scaled
Widget dimensions scaled for current zoom level.
Definition window_gui.h:30
int hsep_wide
Wide horizontal spacing.
Definition window_gui.h:62
int hsep_normal
Normal horizontal spacing.
Definition window_gui.h:61
Common drop down list components.
Functions related to the drop down widget.
Types related to the drop down widget.
std::vector< std::unique_ptr< const DropDownListItem > > DropDownList
A drop down list is a collection of drop down list items.
Flat set container implementation.
Dimension GetSpriteSize(SpriteID sprid, Point *offset, ZoomLevel zoom)
Get the size of a sprite.
Definition gfx.cpp:958
int DrawStringMultiLine(int left, int right, int top, int bottom, std::string_view str, TextColour colour, StringAlignment align, bool underline, FontSize fontsize)
Draw string, possibly over multiple lines.
Definition gfx.cpp:775
uint32_t SpriteID
The number of a sprite, without mapping bits and colourtables.
Definition gfx_type.h:17
@ SA_CENTER
Center both horizontally and vertically.
Definition gfx_type.h:393
uint32_t PaletteID
The number of the palette.
Definition gfx_type.h:18
Base for the NewGRF implementation.
GrfSpecFeature
Definition newgrf.h:69
Badge * GetBadge(BadgeID index)
Get a badge if it exists.
std::span< const Badge > GetBadges()
Get a read-only view of badges.
PalSpriteID GetBadgeSprite(const Badge &badge, GrfSpecFeature feature, std::optional< TimerGameCalendar::Date > introduction_date, PaletteID remap)
Get sprite for the given badge.
Badge * GetClassBadge(BadgeClassID class_index)
Get the badge class of a badge label.
Functions related to NewGRF badges.
void ResetBadgeClassConfiguration(GrfSpecFeature feature)
Reset badge class configuration for a feature.
std::pair< const BadgeClassConfigItem &, int > GetBadgeClassConfigItem(GrfSpecFeature feature, std::string_view label)
Get configuration for a badge class.
std::span< BadgeClassConfigItem > GetBadgeClassConfiguration(GrfSpecFeature feature)
Get the badge user configuration for a feature.
Functions related to NewGRF badge configuration.
Types related to NewGRF badges.
@ NameListStop
Stop adding names to the name list after this badge.
@ NameListFirstOnly
Don't add this name to the name list if not first.
A number of safeguards to prevent using unsafe methods.
void DrawUpDownButtons(int x, int y, Colours button_colour, uint8_t state, bool clickable_up, bool clickable_down)
Draw [^][v] buttons.
Functions for setting GUIs.
#define SETTING_BUTTON_WIDTH
Width of setting buttons.
#define SETTING_BUTTON_HEIGHT
Height of setting buttons.
Definition of base types and functions in a cross-platform compatible way.
std::string_view GetListSeparator()
Get the list separator string for the current language.
Definition strings.cpp:299
void AppendStringInPlace(std::string &result, StringID string)
Resolve the given StringID and append in place into an existing std::string with formatting but no pa...
Definition strings.cpp:425
std::string GetString(StringID string)
Resolve the given StringID into a std::string with formatting but no parameters.
Definition strings.cpp:415
TextDirection _current_text_dir
Text direction of the currently selected language.
Definition strings.cpp:57
Functions related to OTTD's strings.
@ TD_RTL
Text is written right-to-left by default.
Dimensions (a width and height) of a rectangle in 2D.
uint sort_order
Order of element.
std::string_view label
Class label (string owned by the class badge)
BadgeClassID class_index
Badge class index.
uint8_t column_group
Column group in UI. 0 = left, 1 = centre, 2 = right.
Combination of a palette sprite and a 'real' sprite.
Definition gfx_type.h:22
SpriteID sprite
The 'real' sprite.
Definition gfx_type.h:23
PaletteID pal
The palette (use PAL_NONE) if not needed)
Definition gfx_type.h:24
Coordinates of a point in 2D.
Specification of a rectangle with absolute coordinates of all edges.
Rect WithWidth(int width, bool end) const
Copy Rect and set its width.
Rect CentreTo(int width, int height) const
Centre a dimension within this Rect.
Rect Indent(int indent, bool end) const
Copy Rect and indent it from its position.
bool Contains(const Point &pt) const
Test if a point falls inside this Rect.
int16_t Height
Fixed point type for heights.
Definition tgp.cpp:153
Definition of the game-calendar-timer.
static RectPadding ScaleGUITrad(const RectPadding &r)
Scale a RectPadding to GUI zoom level.
Definition widget.cpp:49
Functions, definitions and such used only by the GUI.
Functions related to zooming.
@ Normal
The normal zoom level.