1 /*****************************************************************************
2  * Copyright (c) 2014-2020 OpenRCT2 developers
3  *
4  * For a complete list of all authors, please refer to contributors.md
5  * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
6  *
7  * OpenRCT2 is licensed under the GNU General Public License version 3.
8  *****************************************************************************/
9 
10 #include <algorithm>
11 #include <bitset>
12 #include <iterator>
13 #include <openrct2-ui/interface/Dropdown.h>
14 #include <openrct2-ui/interface/Widget.h>
15 #include <openrct2/Context.h>
16 #include <openrct2/Input.h>
17 #include <openrct2/drawing/Drawing.h>
18 #include <openrct2/localisation/Localisation.h>
19 #include <openrct2/sprites.h>
20 
21 // The maximum number of rows to list before items overflow into new columns
22 constexpr int32_t DROPDOWN_TEXT_MAX_ROWS = 32;
23 
24 constexpr int32_t DROPDOWN_ITEM_HEIGHT = 12;
25 
26 static constexpr const uint8_t _appropriateImageDropdownItemsPerRow[34] = {
27     1, 1, 1, 1, 2, 2, 3, 3, 4, 3, 5, 4, 4, 5, 5, 5, 4, 5, 6, 5, 5, 7, 4, 5, 6, 5, 6, 6, 6, 6, 6, 8, 8, 8,
28 };
29 
30 enum
31 {
32     WIDX_BACKGROUND,
33 };
34 
35 static rct_widget window_dropdown_widgets[] = {
36     MakeWidget({ 0, 0 }, { 1, 1 }, WindowWidgetType::ImgBtn, WindowColour::Primary),
37     WIDGETS_END,
38 };
39 
40 static int32_t _dropdown_num_columns;
41 static int32_t _dropdown_num_rows;
42 static int32_t _dropdown_item_width;
43 static int32_t _dropdown_item_height;
44 static bool _dropdown_list_vertically;
45 
46 int32_t gDropdownNumItems;
47 rct_string_id gDropdownItemsFormat[Dropdown::ItemsMaxSize];
48 int64_t gDropdownItemsArgs[Dropdown::ItemsMaxSize];
49 static std::bitset<Dropdown::ItemsMaxSize> _dropdownItemsChecked = {};
50 static std::bitset<Dropdown::ItemsMaxSize> _dropdownItemsDisabled = {};
51 bool gDropdownIsColour;
52 int32_t gDropdownLastColourHover;
53 int32_t gDropdownHighlightedIndex;
54 int32_t gDropdownDefaultIndex;
55 
IsChecked(int32_t index)56 bool Dropdown::IsChecked(int32_t index)
57 {
58     if (index < 0 || index >= static_cast<int32_t>(std::size(_dropdownItemsDisabled)))
59     {
60         return false;
61     }
62     return _dropdownItemsChecked[index];
63 }
64 
IsDisabled(int32_t index)65 bool Dropdown::IsDisabled(int32_t index)
66 {
67     if (index < 0 || index >= static_cast<int32_t>(std::size(_dropdownItemsDisabled)))
68     {
69         return true;
70     }
71     return _dropdownItemsDisabled[index];
72 }
73 
SetChecked(int32_t index,bool value)74 void Dropdown::SetChecked(int32_t index, bool value)
75 {
76     if (index < 0 || index >= static_cast<int32_t>(std::size(_dropdownItemsDisabled)))
77     {
78         return;
79     }
80     _dropdownItemsChecked[index] = value;
81 }
82 
SetDisabled(int32_t index,bool value)83 void Dropdown::SetDisabled(int32_t index, bool value)
84 {
85     if (index < 0 || index >= static_cast<int32_t>(std::size(_dropdownItemsDisabled)))
86     {
87         return;
88     }
89     _dropdownItemsDisabled[index] = value;
90 }
91 
92 static void window_dropdown_paint(rct_window* w, rct_drawpixelinfo* dpi);
93 
94 // clang-format off
95 static rct_window_event_list window_dropdown_events([](auto& events)
__anonf3b9f9f40202(auto& events) 96 {
97     events.paint = &window_dropdown_paint;
98 });
99 // clang-format on
100 
101 /**
102  * Shows a text dropdown menu.
103  *  rct2: 0x006ECFB9
104  *
105  * @param x (cx)
106  * @param y (dx)
107  * @param extray (di)
108  * @param flags (bh)
109  * @param num_items (bx)
110  * @param colour (al)
111  */
WindowDropdownShowText(const ScreenCoordsXY & screenPos,int32_t extray,uint8_t colour,uint8_t flags,size_t num_items)112 void WindowDropdownShowText(const ScreenCoordsXY& screenPos, int32_t extray, uint8_t colour, uint8_t flags, size_t num_items)
113 {
114     int32_t string_width, max_string_width;
115     char buffer[256];
116 
117     // Calculate the longest string width
118     max_string_width = 0;
119     for (size_t i = 0; i < num_items; i++)
120     {
121         format_string(buffer, 256, gDropdownItemsFormat[i], static_cast<void*>(&gDropdownItemsArgs[i]));
122         string_width = gfx_get_string_width(buffer, FontSpriteBase::MEDIUM);
123         max_string_width = std::max(string_width, max_string_width);
124     }
125 
126     WindowDropdownShowTextCustomWidth(screenPos, extray, colour, 0, flags, num_items, max_string_width + 3);
127 }
128 
129 /**
130  * Shows a text dropdown menu.
131  *  rct2: 0x006ECFB9, although 0x006ECE50 is real version
132  *
133  * @param x (cx)
134  * @param y (dx)
135  * @param extray (di)
136  * @param flags (bh)
137  * @param num_items (bx)
138  * @param colour (al)
139  * @param custom_height (ah) requires flag set as well
140  */
WindowDropdownShowTextCustomWidth(const ScreenCoordsXY & screenPos,int32_t extray,uint8_t colour,uint8_t custom_height,uint8_t flags,size_t num_items,int32_t width)141 void WindowDropdownShowTextCustomWidth(
142     const ScreenCoordsXY& screenPos, int32_t extray, uint8_t colour, uint8_t custom_height, uint8_t flags, size_t num_items,
143     int32_t width)
144 {
145     rct_window* w;
146 
147     input_set_flag(static_cast<INPUT_FLAGS>(INPUT_FLAG_DROPDOWN_STAY_OPEN | INPUT_FLAG_DROPDOWN_MOUSE_UP), false);
148     if (flags & Dropdown::Flag::StayOpen)
149         input_set_flag(INPUT_FLAG_DROPDOWN_STAY_OPEN, true);
150 
151     WindowDropdownClose();
152 
153     // Set and calculate num items, rows and columns
154     _dropdown_item_width = width;
155     _dropdown_item_height = (flags & Dropdown::Flag::CustomHeight) ? custom_height : DROPDOWN_ITEM_HEIGHT;
156     gDropdownNumItems = static_cast<int32_t>(num_items);
157     // There must always be at least one column to prevent dividing by zero
158     if (gDropdownNumItems == 0)
159     {
160         _dropdown_num_columns = 1;
161         _dropdown_num_rows = 1;
162     }
163     else
164     {
165         _dropdown_num_columns = (gDropdownNumItems + DROPDOWN_TEXT_MAX_ROWS - 1) / DROPDOWN_TEXT_MAX_ROWS;
166         _dropdown_num_rows = (gDropdownNumItems + _dropdown_num_columns - 1) / _dropdown_num_columns;
167     }
168 
169     // Text dropdowns are listed horizontally
170     _dropdown_list_vertically = true;
171 
172     width = _dropdown_item_width * _dropdown_num_columns + 3;
173     int32_t height = _dropdown_item_height * _dropdown_num_rows + 3;
174     int32_t screenWidth = context_get_width();
175     int32_t screenHeight = context_get_height();
176     auto boundedScreenPos = screenPos;
177     if (screenPos.x + width > screenWidth)
178         boundedScreenPos.x = std::max(0, screenWidth - width);
179     if (screenPos.y + height > screenHeight)
180         boundedScreenPos.y = std::max(0, screenHeight - height);
181 
182     window_dropdown_widgets[WIDX_BACKGROUND].right = width;
183     window_dropdown_widgets[WIDX_BACKGROUND].bottom = height;
184 
185     // Create the window
186     w = WindowCreate(
187         boundedScreenPos + ScreenCoordsXY{ 0, extray }, window_dropdown_widgets[WIDX_BACKGROUND].right + 1,
188         window_dropdown_widgets[WIDX_BACKGROUND].bottom + 1, &window_dropdown_events, WC_DROPDOWN, WF_STICK_TO_FRONT);
189     w->widgets = window_dropdown_widgets;
190     if (colour & COLOUR_FLAG_TRANSLUCENT)
191         w->flags |= WF_TRANSPARENT;
192     w->colours[0] = colour;
193 
194     // Input state
195     gDropdownHighlightedIndex = -1;
196     _dropdownItemsDisabled.reset();
197     _dropdownItemsChecked.reset();
198     gDropdownIsColour = false;
199     gDropdownDefaultIndex = -1;
200     input_set_state(InputState::DropdownActive);
201 }
202 
203 /**
204  * Shows an image dropdown menu.
205  *  rct2: 0x006ECFB9
206  *
207  * @param x (cx)
208  * @param y (dx)
209  * @param extray (di)
210  * @param flags (bh)
211  * @param numItems (bx)
212  * @param colour (al)
213  * @param itemWidth (bp)
214  * @param itemHeight (ah)
215  * @param numColumns (bl)
216  */
WindowDropdownShowImage(int32_t x,int32_t y,int32_t extray,uint8_t colour,uint8_t flags,int32_t numItems,int32_t itemWidth,int32_t itemHeight,int32_t numColumns)217 void WindowDropdownShowImage(
218     int32_t x, int32_t y, int32_t extray, uint8_t colour, uint8_t flags, int32_t numItems, int32_t itemWidth,
219     int32_t itemHeight, int32_t numColumns)
220 {
221     int32_t width, height;
222     rct_window* w;
223 
224     input_set_flag(static_cast<INPUT_FLAGS>(INPUT_FLAG_DROPDOWN_STAY_OPEN | INPUT_FLAG_DROPDOWN_MOUSE_UP), false);
225     if (flags & Dropdown::Flag::StayOpen)
226         input_set_flag(INPUT_FLAG_DROPDOWN_STAY_OPEN, true);
227 
228     // Close existing dropdown
229     WindowDropdownClose();
230 
231     // Set and calculate num items, rows and columns
232     _dropdown_item_width = itemWidth;
233     _dropdown_item_height = itemHeight;
234     gDropdownNumItems = numItems;
235     // There must always be at least one column and row to prevent dividing by zero
236     if (gDropdownNumItems == 0)
237     {
238         _dropdown_num_columns = 1;
239         _dropdown_num_rows = 1;
240     }
241     else
242     {
243         _dropdown_num_columns = std::max(1, numColumns);
244         _dropdown_num_rows = gDropdownNumItems / _dropdown_num_columns;
245         if (gDropdownNumItems % _dropdown_num_columns != 0)
246             _dropdown_num_rows++;
247     }
248 
249     // image dropdowns are listed horizontally
250     _dropdown_list_vertically = false;
251 
252     // Calculate position and size
253     width = _dropdown_item_width * _dropdown_num_columns + 3;
254     height = _dropdown_item_height * _dropdown_num_rows + 3;
255 
256     int32_t screenWidth = context_get_width();
257     int32_t screenHeight = context_get_height();
258     if (x + width > screenWidth)
259         x = std::max(0, screenWidth - width);
260     if (y + height > screenHeight)
261         y = std::max(0, screenHeight - height);
262     window_dropdown_widgets[WIDX_BACKGROUND].right = width;
263     window_dropdown_widgets[WIDX_BACKGROUND].bottom = height;
264 
265     // Create the window
266     w = WindowCreate(
267         ScreenCoordsXY(x, y + extray), window_dropdown_widgets[WIDX_BACKGROUND].right + 1,
268         window_dropdown_widgets[WIDX_BACKGROUND].bottom + 1, &window_dropdown_events, WC_DROPDOWN, WF_STICK_TO_FRONT);
269     w->widgets = window_dropdown_widgets;
270     if (colour & COLOUR_FLAG_TRANSLUCENT)
271         w->flags |= WF_TRANSPARENT;
272     w->colours[0] = colour;
273 
274     // Input state
275     gDropdownHighlightedIndex = -1;
276     _dropdownItemsDisabled.reset();
277     _dropdownItemsChecked.reset();
278     gDropdownIsColour = false;
279     gDropdownDefaultIndex = -1;
280     input_set_state(InputState::DropdownActive);
281 }
282 
WindowDropdownClose()283 void WindowDropdownClose()
284 {
285     window_close_by_class(WC_DROPDOWN);
286 }
287 
window_dropdown_paint(rct_window * w,rct_drawpixelinfo * dpi)288 static void window_dropdown_paint(rct_window* w, rct_drawpixelinfo* dpi)
289 {
290     WindowDrawWidgets(w, dpi);
291 
292     int32_t highlightedIndex = gDropdownHighlightedIndex;
293     for (int32_t i = 0; i < gDropdownNumItems; i++)
294     {
295         ScreenCoordsXY cellCoords;
296         if (_dropdown_list_vertically)
297             cellCoords = { i / _dropdown_num_rows, i % _dropdown_num_rows };
298         else
299             cellCoords = { i % _dropdown_num_columns, i / _dropdown_num_columns };
300 
301         ScreenCoordsXY screenCoords = w->windowPos
302             + ScreenCoordsXY{ 2 + (cellCoords.x * _dropdown_item_width), 2 + (cellCoords.y * _dropdown_item_height) };
303 
304         if (gDropdownItemsFormat[i] == Dropdown::SeparatorString)
305         {
306             const ScreenCoordsXY leftTop = screenCoords + ScreenCoordsXY{ 0, (_dropdown_item_height / 2) };
307             const ScreenCoordsXY rightBottom = leftTop + ScreenCoordsXY{ _dropdown_item_width - 1, 0 };
308             const ScreenCoordsXY shadowOffset{ 0, 1 };
309 
310             if (w->colours[0] & COLOUR_FLAG_TRANSLUCENT)
311             {
312                 translucent_window_palette palette = TranslucentWindowPalettes[BASE_COLOUR(w->colours[0])];
313                 gfx_filter_rect(dpi, { leftTop, rightBottom }, palette.highlight);
314                 gfx_filter_rect(dpi, { leftTop + shadowOffset, rightBottom + shadowOffset }, palette.shadow);
315             }
316             else
317             {
318                 gfx_fill_rect(dpi, { leftTop, rightBottom }, ColourMapA[w->colours[0]].mid_dark);
319                 gfx_fill_rect(dpi, { leftTop + shadowOffset, rightBottom + shadowOffset }, ColourMapA[w->colours[0]].lightest);
320             }
321         }
322         else
323         {
324             if (i == highlightedIndex)
325             {
326                 // Darken the cell's background slightly when highlighted
327                 const ScreenCoordsXY rightBottom = screenCoords
328                     + ScreenCoordsXY{ _dropdown_item_width - 1, _dropdown_item_height - 1 };
329                 gfx_filter_rect(dpi, { screenCoords, rightBottom }, FilterPaletteID::PaletteDarken3);
330             }
331 
332             rct_string_id item = gDropdownItemsFormat[i];
333             if (item == Dropdown::FormatLandPicker || item == Dropdown::FormatColourPicker)
334             {
335                 // Image item
336                 auto image = static_cast<uint32_t>(gDropdownItemsArgs[i]);
337                 if (item == Dropdown::FormatColourPicker && highlightedIndex == i)
338                     image++;
339 
340                 gfx_draw_sprite(dpi, ImageId::FromUInt32(image), screenCoords);
341             }
342             else
343             {
344                 // Text item
345                 if (i < Dropdown::ItemsMaxSize && Dropdown::IsChecked(i))
346                     item++;
347 
348                 // Calculate colour
349                 colour_t colour = NOT_TRANSLUCENT(w->colours[0]);
350                 if (i == highlightedIndex)
351                     colour = COLOUR_WHITE;
352                 if (i < Dropdown::ItemsMaxSize && Dropdown::IsDisabled(i))
353                     colour = NOT_TRANSLUCENT(w->colours[0]) | COLOUR_FLAG_INSET;
354 
355                 // Draw item string
356                 Formatter ft(reinterpret_cast<uint8_t*>(&gDropdownItemsArgs[i]));
357                 DrawTextEllipsised(dpi, screenCoords, w->width - 5, item, ft, { colour });
358             }
359         }
360     }
361 }
362 
363 /**
364  * New function based on 6e914e
365  * returns -1 if index is invalid
366  */
DropdownIndexFromPoint(const ScreenCoordsXY & loc,rct_window * w)367 int32_t DropdownIndexFromPoint(const ScreenCoordsXY& loc, rct_window* w)
368 {
369     int32_t top = loc.y - w->windowPos.y - 2;
370     if (top < 0)
371         return -1;
372 
373     int32_t left = loc.x - w->windowPos.x;
374     if (left >= w->width)
375         return -1;
376     left -= 2;
377     if (left < 0)
378         return -1;
379 
380     int32_t column_no = left / _dropdown_item_width;
381     if (column_no >= _dropdown_num_columns)
382         return -1;
383 
384     int32_t row_no = top / _dropdown_item_height;
385     if (row_no >= _dropdown_num_rows)
386         return -1;
387 
388     int32_t dropdown_index;
389     if (_dropdown_list_vertically)
390         dropdown_index = column_no * _dropdown_num_rows + row_no;
391     else
392         dropdown_index = row_no * _dropdown_num_columns + column_no;
393 
394     if (dropdown_index >= gDropdownNumItems)
395         return -1;
396 
397     return dropdown_index;
398 }
399 
400 /**
401  *  rct2: 0x006ED43D
402  */
WindowDropdownShowColour(rct_window * w,rct_widget * widget,uint8_t dropdownColour,uint8_t selectedColour)403 void WindowDropdownShowColour(rct_window* w, rct_widget* widget, uint8_t dropdownColour, uint8_t selectedColour)
404 {
405     int32_t defaultIndex = -1;
406     // Set items
407     for (uint64_t i = 0; i < COLOUR_COUNT; i++)
408     {
409         if (selectedColour == i)
410             defaultIndex = i;
411 
412         gDropdownItemsFormat[i] = Dropdown::FormatColourPicker;
413         gDropdownItemsArgs[i] = (i << 32) | (SPRITE_ID_PALETTE_COLOUR_1(i) | SPR_PALETTE_BTN);
414     }
415 
416     // Show dropdown
417     WindowDropdownShowImage(
418         w->windowPos.x + widget->left, w->windowPos.y + widget->top, widget->height() + 1, dropdownColour,
419         Dropdown::Flag::StayOpen, COLOUR_COUNT, 12, 12, _appropriateImageDropdownItemsPerRow[COLOUR_COUNT]);
420 
421     gDropdownIsColour = true;
422     gDropdownLastColourHover = -1;
423     gDropdownDefaultIndex = defaultIndex;
424 }
425 
DropdownGetAppropriateImageDropdownItemsPerRow(uint32_t numItems)426 uint32_t DropdownGetAppropriateImageDropdownItemsPerRow(uint32_t numItems)
427 {
428     return numItems < std::size(_appropriateImageDropdownItemsPerRow) ? _appropriateImageDropdownItemsPerRow[numItems] : 8;
429 }
430