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/>.
8 /** @file dropdown.cpp Implementation of the dropdown widget. */
11 #include "dropdown_type.h"
12 #include "dropdown_func.h"
13 #include "dropdown_common_type.h"
14 #include "strings_func.h"
15 #include "timer/timer.h"
16 #include "timer/timer_window.h"
17 #include "window_gui.h"
18 #include "window_func.h"
19 #include "zoom_func.h"
21 #include "widgets/dropdown_widget.h"
23 #include "safeguards.h"
25 std::unique_ptr
<DropDownListItem
> MakeDropDownListDividerItem()
27 return std::make_unique
<DropDownListDividerItem
>(-1);
30 std::unique_ptr
<DropDownListItem
> MakeDropDownListStringItem(StringID str
, int value
, bool masked
, bool shaded
)
32 return std::make_unique
<DropDownListStringItem
>(str
, value
, masked
, shaded
);
35 std::unique_ptr
<DropDownListItem
> MakeDropDownListStringItem(const std::string
&str
, int value
, bool masked
, bool shaded
)
37 return std::make_unique
<DropDownListStringItem
>(str
, value
, masked
, shaded
);
40 std::unique_ptr
<DropDownListItem
> MakeDropDownListIconItem(SpriteID sprite
, PaletteID palette
, StringID str
, int value
, bool masked
, bool shaded
)
42 return std::make_unique
<DropDownListIconItem
>(sprite
, palette
, str
, value
, masked
, shaded
);
45 std::unique_ptr
<DropDownListItem
> MakeDropDownListIconItem(const Dimension
&dim
, SpriteID sprite
, PaletteID palette
, StringID str
, int value
, bool masked
, bool shaded
)
47 return std::make_unique
<DropDownListIconItem
>(dim
, sprite
, palette
, str
, value
, masked
, shaded
);
50 std::unique_ptr
<DropDownListItem
> MakeDropDownListCheckedItem(bool checked
, StringID str
, int value
, bool masked
, bool shaded
)
52 return std::make_unique
<DropDownListCheckedItem
>(checked
, str
, value
, masked
, shaded
);
55 static constexpr NWidgetPart _nested_dropdown_menu_widgets
[] = {
56 NWidget(NWID_HORIZONTAL
),
57 NWidget(WWT_PANEL
, COLOUR_END
, WID_DM_ITEMS
), SetScrollbar(WID_DM_SCROLL
), EndContainer(),
58 NWidget(NWID_SELECTION
, INVALID_COLOUR
, WID_DM_SHOW_SCROLL
),
59 NWidget(NWID_VSCROLLBAR
, COLOUR_END
, WID_DM_SCROLL
),
64 static WindowDesc
_dropdown_desc(
65 WDP_MANUAL
, nullptr, 0, 0,
66 WC_DROPDOWN_MENU
, WC_NONE
,
68 _nested_dropdown_menu_widgets
71 /** Drop-down menu window */
72 struct DropdownWindow
: Window
{
73 WidgetID parent_button
; ///< Parent widget number where the window is dropped from.
74 Rect wi_rect
; ///< Rect of the button that opened the dropdown.
75 DropDownList list
; ///< List with dropdown menu items.
76 int selected_result
; ///< Result value of the selected item in the list.
77 uint8_t click_delay
= 0; ///< Timer to delay selection.
78 bool drag_mode
= true;
79 bool instant_close
; ///< Close the window when the mouse button is raised.
80 bool persist
; ///< Persist dropdown menu.
81 int scrolling
= 0; ///< If non-zero, auto-scroll the item list (one time).
82 Point position
; ///< Position of the topleft corner of the window.
85 Dimension items_dim
; ///< Calculated cropped and padded dimension for the items widget.
88 * Create a dropdown menu.
89 * @param parent Parent window.
90 * @param list Dropdown item list.
91 * @param selected Initial selected result of the list.
92 * @param button Widget of the parent window doing the dropdown.
93 * @param wi_rect Rect of the button that opened the dropdown.
94 * @param instant_close Close the window when the mouse button is raised.
95 * @param wi_colour Colour of the parent widget.
96 * @param persist Dropdown menu will persist.
98 DropdownWindow(Window
*parent
, DropDownList
&&list
, int selected
, WidgetID button
, const Rect wi_rect
, bool instant_close
, Colours wi_colour
, bool persist
)
99 : Window(_dropdown_desc
)
100 , parent_button(button
)
102 , list(std::move(list
))
103 , selected_result(selected
)
104 , instant_close(instant_close
)
107 assert(!this->list
.empty());
109 this->parent
= parent
;
111 this->CreateNestedTree();
113 this->GetWidget
<NWidgetCore
>(WID_DM_ITEMS
)->colour
= wi_colour
;
114 this->GetWidget
<NWidgetCore
>(WID_DM_SCROLL
)->colour
= wi_colour
;
115 this->vscroll
= this->GetScrollbar(WID_DM_SCROLL
);
116 this->UpdateSizeAndPosition();
118 this->FinishInitNested(0);
119 CLRBITS(this->flags
, WF_WHITE_BORDER
);
122 void Close([[maybe_unused
]] int data
= 0) override
124 /* Finish closing the dropdown, so it doesn't affect new window placement.
125 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
126 this->Window::Close();
128 Point pt
= _cursor
.pos
;
129 pt
.x
-= this->parent
->left
;
130 pt
.y
-= this->parent
->top
;
131 this->parent
->OnDropdownClose(pt
, this->parent_button
, this->selected_result
, this->instant_close
);
133 /* Set flag on parent widget to indicate that we have just closed. */
134 NWidgetCore
*nwc
= this->parent
->GetWidget
<NWidgetCore
>(this->parent_button
);
135 if (nwc
!= nullptr) SetBit(nwc
->disp_flags
, NDB_DROPDOWN_CLOSED
);
138 void OnFocusLost(bool closing
) override
141 this->instant_close
= false;
147 * Fit dropdown list into available height, rounding to average item size. Width is adjusted if scrollbar is present.
148 * @param[in,out] desired Desired dimensions of dropdown list.
149 * @param list Dimensions of the list itself, without padding or cropping.
150 * @param available_height Available height to fit list within.
152 void FitAvailableHeight(Dimension
&desired
, const Dimension
&list
, uint available_height
)
154 if (desired
.height
< available_height
) return;
156 /* If the dropdown doesn't fully fit, we a need a dropdown. */
157 uint avg_height
= list
.height
/ (uint
)this->list
.size();
158 uint rows
= std::max((available_height
- WidgetDimensions::scaled
.dropdownlist
.Vertical()) / avg_height
, 1U);
160 desired
.width
= std::max(list
.width
, desired
.width
- NWidgetScrollbar::GetVerticalDimension().width
);
161 desired
.height
= rows
* avg_height
+ WidgetDimensions::scaled
.dropdownlist
.Vertical();
165 * Update size and position of window to fit dropdown list into available space.
167 void UpdateSizeAndPosition()
169 Rect button_rect
= this->wi_rect
.Translate(this->parent
->left
, this->parent
->top
);
171 /* Get the dimensions required for the list. */
172 Dimension list_dim
= GetDropDownListDimension(this->list
);
174 /* Set up dimensions for the items widget. */
175 Dimension widget_dim
= list_dim
;
176 widget_dim
.width
+= WidgetDimensions::scaled
.dropdownlist
.Horizontal();
177 widget_dim
.height
+= WidgetDimensions::scaled
.dropdownlist
.Vertical();
179 /* Width should match at least the width of the parent widget. */
180 widget_dim
.width
= std::max
<uint
>(widget_dim
.width
, button_rect
.Width());
182 /* Available height below (or above, if the dropdown is placed above the widget). */
183 uint available_height_below
= std::max(GetMainViewBottom() - button_rect
.bottom
- 1, 0);
184 uint available_height_above
= std::max(button_rect
.top
- 1 - GetMainViewTop(), 0);
186 /* Is it better to place the dropdown above the widget? */
187 if (widget_dim
.height
> available_height_below
&& available_height_above
> available_height_below
) {
188 FitAvailableHeight(widget_dim
, list_dim
, available_height_above
);
189 this->position
.y
= button_rect
.top
- widget_dim
.height
;
191 FitAvailableHeight(widget_dim
, list_dim
, available_height_below
);
192 this->position
.y
= button_rect
.bottom
+ 1;
195 if (_current_text_dir
== TD_RTL
) {
196 /* In case the list is wider than the parent button, the list should be right aligned to the button and overflow to the left. */
197 this->position
.x
= button_rect
.right
+ 1 - (int)(widget_dim
.width
+ (list_dim
.height
> widget_dim
.height
? NWidgetScrollbar::GetVerticalDimension().width
: 0));
199 this->position
.x
= button_rect
.left
;
202 this->items_dim
= widget_dim
;
203 this->GetWidget
<NWidgetStacked
>(WID_DM_SHOW_SCROLL
)->SetDisplayedPlane(list_dim
.height
> widget_dim
.height
? 0 : SZSP_NONE
);
205 /* Capacity is the average number of items visible */
206 this->vscroll
->SetCapacity((widget_dim
.height
- WidgetDimensions::scaled
.dropdownlist
.Vertical()) * this->list
.size() / list_dim
.height
);
207 this->vscroll
->SetCount(this->list
.size());
209 /* If the dropdown is positioned above the parent widget, start selection at the bottom. */
210 if (this->position
.y
< button_rect
.top
&& list_dim
.height
> widget_dim
.height
) this->vscroll
->UpdatePosition(INT_MAX
);
213 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
215 if (widget
== WID_DM_ITEMS
) size
= this->items_dim
;
218 Point
OnInitialPosition([[maybe_unused
]] int16_t sm_width
, [[maybe_unused
]] int16_t sm_height
, [[maybe_unused
]] int window_number
) override
220 return this->position
;
224 * Find the dropdown item under the cursor.
225 * @param[out] value Selected item, if function returns \c true.
226 * @return Cursor points to a dropdown item.
228 bool GetDropDownItem(int &value
)
230 if (GetWidgetFromPos(this, _cursor
.pos
.x
- this->left
, _cursor
.pos
.y
- this->top
) < 0) return false;
232 const Rect
&r
= this->GetWidget
<NWidgetBase
>(WID_DM_ITEMS
)->GetCurrentRect().Shrink(WidgetDimensions::scaled
.dropdownlist
);
233 int y
= _cursor
.pos
.y
- this->top
- r
.top
;
234 int pos
= this->vscroll
->GetPosition();
236 for (const auto &item
: this->list
) {
237 /* Skip items that are scrolled up */
238 if (--pos
>= 0) continue;
240 int item_height
= item
->Height();
242 if (y
< item_height
) {
243 if (item
->masked
|| !item
->Selectable()) return false;
244 value
= item
->result
;
254 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
256 if (widget
!= WID_DM_ITEMS
) return;
258 Colours colour
= this->GetWidget
<NWidgetCore
>(widget
)->colour
;
260 Rect ir
= r
.Shrink(WidgetDimensions::scaled
.dropdownlist
);
262 int pos
= this->vscroll
->GetPosition();
263 for (const auto &item
: this->list
) {
264 int item_height
= item
->Height();
266 /* Skip items that are scrolled up */
267 if (--pos
>= 0) continue;
269 if (y
+ item_height
- 1 <= ir
.bottom
) {
270 Rect full
{ir
.left
, y
, ir
.right
, y
+ item_height
- 1};
272 bool selected
= (this->selected_result
== item
->result
) && item
->Selectable();
273 if (selected
) GfxFillRect(full
, PC_BLACK
);
275 item
->Draw(full
, full
.Shrink(WidgetDimensions::scaled
.dropdowntext
, RectPadding::zero
), selected
, colour
);
281 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
283 if (widget
!= WID_DM_ITEMS
) return;
285 if (this->GetDropDownItem(item
)) {
286 this->click_delay
= 4;
287 this->selected_result
= item
;
292 /** Rate limit how fast scrolling happens. */
293 IntervalTimer
<TimerWindow
> scroll_interval
= {std::chrono::milliseconds(30), [this](auto) {
294 if (this->scrolling
== 0) return;
296 if (this->vscroll
->UpdatePosition(this->scrolling
)) this->SetDirty();
301 void OnMouseLoop() override
303 if (this->click_delay
!= 0 && --this->click_delay
== 0) {
304 /* Close the dropdown, so it doesn't affect new window placement.
305 * Also mark it dirty in case the callback deals with the screen. (e.g. screenshots). */
306 if (!this->persist
) this->Close();
307 this->parent
->OnDropdownSelect(this->parent_button
, this->selected_result
);
311 if (this->drag_mode
) {
314 if (!_left_button_clicked
) {
315 this->drag_mode
= false;
316 if (!this->GetDropDownItem(item
)) {
317 if (this->instant_close
) this->Close();
320 this->click_delay
= 2;
322 if (_cursor
.pos
.y
<= this->top
+ WidgetDimensions::scaled
.dropdownlist
.top
) {
323 /* Cursor is above the list, set scroll up */
324 this->scrolling
= -1;
326 } else if (_cursor
.pos
.y
>= this->top
+ this->height
- WidgetDimensions::scaled
.dropdownlist
.bottom
) {
327 /* Cursor is below list, set scroll down */
332 if (!this->GetDropDownItem(item
)) return;
335 if (this->selected_result
!= item
) {
336 this->selected_result
= item
;
342 void ReplaceList(DropDownList
&&list
)
344 this->list
= std::move(list
);
345 this->UpdateSizeAndPosition();
347 this->InitializePositionSize(this->position
.x
, this->position
.y
, this->nested_root
->smallest_x
, this->nested_root
->smallest_y
);
352 void ReplaceDropDownList(Window
*parent
, DropDownList
&&list
)
354 DropdownWindow
*ddw
= dynamic_cast<DropdownWindow
*>(parent
->FindChildWindow(WC_DROPDOWN_MENU
));
355 if (ddw
!= nullptr) ddw
->ReplaceList(std::move(list
));
359 * Determine width and height required to fully display a DropDownList
360 * @param list The list.
361 * @return Dimension required to display the list.
363 Dimension
GetDropDownListDimension(const DropDownList
&list
)
366 for (const auto &item
: list
) {
367 dim
.height
+= item
->Height();
368 dim
.width
= std::max(dim
.width
, item
->Width());
370 dim
.width
+= WidgetDimensions::scaled
.dropdowntext
.Horizontal();
375 * Show a drop down list.
376 * @param w Parent window for the list.
377 * @param list Prepopulated DropDownList.
378 * @param selected The initially selected list item.
379 * @param button The widget which is passed to Window::OnDropdownSelect and OnDropdownClose.
380 * Unless you override those functions, this should be then widget index of the dropdown button.
381 * @param wi_rect Coord of the parent drop down button, used to position the dropdown menu.
382 * @param instant_close Set to true if releasing mouse button should close the
383 * list regardless of where the cursor is.
384 * @param persist Set if this dropdown should stay open after an option is selected.
386 void ShowDropDownListAt(Window
*w
, DropDownList
&&list
, int selected
, WidgetID button
, Rect wi_rect
, Colours wi_colour
, bool instant_close
, bool persist
)
388 CloseWindowByClass(WC_DROPDOWN_MENU
);
389 new DropdownWindow(w
, std::move(list
), selected
, button
, wi_rect
, instant_close
, wi_colour
, persist
);
393 * Show a drop down list.
394 * @param w Parent window for the list.
395 * @param list Prepopulated DropDownList.
396 * @param selected The initially selected list item.
397 * @param button The widget within the parent window that is used to determine
398 * the list's location.
399 * @param width Override the minimum width determined by the selected widget and list contents.
400 * @param instant_close Set to true if releasing mouse button should close the
401 * list regardless of where the cursor is.
402 * @param persist Set if this dropdown should stay open after an option is selected.
404 void ShowDropDownList(Window
*w
, DropDownList
&&list
, int selected
, WidgetID button
, uint width
, bool instant_close
, bool persist
)
406 /* Our parent's button widget is used to determine where to place the drop
407 * down list window. */
408 NWidgetCore
*nwi
= w
->GetWidget
<NWidgetCore
>(button
);
409 Rect wi_rect
= nwi
->GetCurrentRect();
410 Colours wi_colour
= nwi
->colour
;
412 if ((nwi
->type
& WWT_MASK
) == NWID_BUTTON_DROPDOWN
) {
413 nwi
->disp_flags
|= ND_DROPDOWN_ACTIVE
;
415 nwi
->SetLowered(true);
420 if (_current_text_dir
== TD_RTL
) {
421 wi_rect
.left
= wi_rect
.right
+ 1 - ScaleGUITrad(width
);
423 wi_rect
.right
= wi_rect
.left
+ ScaleGUITrad(width
) - 1;
427 ShowDropDownListAt(w
, std::move(list
), selected
, button
, wi_rect
, wi_colour
, instant_close
, persist
);
431 * Show a dropdown menu window near a widget of the parent window.
432 * The result code of the items is their index in the \a strings list.
433 * @param w Parent window that wants the dropdown menu.
434 * @param strings Menu list.
435 * @param selected Index of initial selected item.
436 * @param button Button widget number of the parent window \a w that wants the dropdown menu.
437 * @param disabled_mask Bitmask for disabled items (items with their bit set are displayed, but not selectable in the dropdown list).
438 * @param hidden_mask Bitmask for hidden items (items with their bit set are not copied to the dropdown list).
439 * @param width Minimum width of the dropdown menu.
441 void ShowDropDownMenu(Window
*w
, std::span
<const StringID
> strings
, int selected
, WidgetID button
, uint32_t disabled_mask
, uint32_t hidden_mask
, uint width
)
446 for (auto string
: strings
) {
447 if (!HasBit(hidden_mask
, i
)) {
448 list
.push_back(MakeDropDownListStringItem(string
, i
, HasBit(disabled_mask
, i
)));
453 if (!list
.empty()) ShowDropDownList(w
, std::move(list
), selected
, button
, width
);