From b32d29330bc7fa3a67a727ade1dbefa816837fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Janiszewski?= Date: Fri, 15 Aug 2025 18:51:14 +0200 Subject: [PATCH] Fix #24888: Add search for ride list (#24890) * Fix #24888: Add search for ride list * Cache ride name * Hide unwanted ride list entries rather than rebuilding the list * Minor touchups * Display count of filtered rides/stalls in ride list --------- Co-authored-by: Aaron van Geffen --- distribution/changelog.txt | 1 + src/openrct2-ui/windows/RideList.cpp | 110 +++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 16 deletions(-) diff --git a/distribution/changelog.txt b/distribution/changelog.txt index 7682486377..36fb23f39b 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -2,6 +2,7 @@ ------------------------------------------------------------------------ - Feature: [#24949] Add an option to the command line screenshot function to draw debug bounding boxes. - Improved: [#24734] Save files now use Zstd compression for faster saving and smaller files. +- Improved: [#24890] The ride list is now searchable. - Improved: [#24893] The ride list now has headers, and can be sorted in both directions. - Improved: [#24921] The command line sprite build command now prints out the images table entry for the compiled sprite file. - Improved: [#24953] Opening the Scenario Editor, Track Designer or Track Designs Manager now display the progress bar. diff --git a/src/openrct2-ui/windows/RideList.cpp b/src/openrct2-ui/windows/RideList.cpp index 5672ecb23e..4de6833473 100644 --- a/src/openrct2-ui/windows/RideList.cpp +++ b/src/openrct2-ui/windows/RideList.cpp @@ -52,6 +52,8 @@ namespace OpenRCT2::Ui::Windows WIDX_TAB_1, WIDX_TAB_2, WIDX_TAB_3, + WIDX_SEARCH_TEXT_BOX, + WIDX_SEARCH_CLEAR_BUTTON, WIDX_HEADER_NAME, WIDX_HEADER_OTHER, WIDX_HEADER_CUSTOMISE, @@ -69,13 +71,15 @@ namespace OpenRCT2::Ui::Windows makeTab ({ 3, 17}, STR_LIST_RIDES_TIP ), // tab 1 makeTab ({ 34, 17}, STR_LIST_SHOPS_AND_STALLS_TIP ), // tab 2 makeTab ({ 65, 17}, STR_LIST_KIOSKS_AND_FACILITIES_TIP ), // tab 3 - makeWidget({ 3, 47}, {150, 14}, WidgetType::tableHeader, WindowColour::secondary ), // name header - makeWidget({153, 47}, {147, 14}, WidgetType::tableHeader, WindowColour::secondary ), // other header - makeWidget({300, 47}, { 14, 14}, WidgetType::button, WindowColour::secondary, STR_DROPDOWN_GLYPH ), // customise button - makeWidget({ 3, 60}, {334, 177}, WidgetType::scroll, WindowColour::secondary, SCROLL_VERTICAL ), // list - makeWidget({320, 62}, { 14, 14}, WidgetType::imgBtn, WindowColour::secondary, ImageId(SPR_G2_RCT1_CLOSE_BUTTON_0) ), - makeWidget({320, 76}, { 14, 14}, WidgetType::imgBtn, WindowColour::secondary, ImageId(SPR_G2_RCT1_OPEN_BUTTON_0) ), - makeWidget({315, 90}, { 24, 24}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_DEMOLISH), STR_QUICK_DEMOLISH_RIDE ) + makeWidget({ 3, 47}, {284, 14}, WidgetType::textBox, WindowColour::secondary ), // search text box + makeWidget({284, 47}, { 50, 14}, WidgetType::button, WindowColour::secondary, STR_OBJECT_SEARCH_CLEAR, STR_CLEAR_SCENERY_TIP ), // search clear button + makeWidget({ 3, 62}, {150, 14}, WidgetType::tableHeader, WindowColour::secondary ), // name header + makeWidget({153, 62}, {147, 14}, WidgetType::tableHeader, WindowColour::secondary ), // other header + makeWidget({300, 62}, { 14, 14}, WidgetType::button, WindowColour::secondary, STR_DROPDOWN_GLYPH ), // customise button + makeWidget({ 3, 75}, {334, 177}, WidgetType::scroll, WindowColour::secondary, SCROLL_VERTICAL ), // list + makeWidget({320, 77}, { 14, 14}, WidgetType::imgBtn, WindowColour::secondary, ImageId(SPR_G2_RCT1_CLOSE_BUTTON_0) ), + makeWidget({320, 91}, { 14, 14}, WidgetType::imgBtn, WindowColour::secondary, ImageId(SPR_G2_RCT1_OPEN_BUTTON_0) ), + makeWidget({315,105}, { 24, 24}, WidgetType::flatBtn, WindowColour::secondary, ImageId(SPR_DEMOLISH), STR_QUICK_DEMOLISH_RIDE ) ); // clang-format on @@ -165,11 +169,13 @@ namespace OpenRCT2::Ui::Windows bool _quickDemolishMode = false; InformationType _windowRideListInformationType = INFORMATION_TYPE_STATUS; bool _windowListSortDescending = false; + u8string _searchFilter; struct RideListEntry { RideId Id; u8string Name; + bool Visible; }; std::vector _rideList; @@ -188,6 +194,7 @@ namespace OpenRCT2::Ui::Windows _windowRideListInformationType = INFORMATION_TYPE_STATUS; _quickDemolishMode = false; + _searchFilter.clear(); } /** @@ -257,6 +264,14 @@ namespace OpenRCT2::Ui::Windows RefreshList(); } break; + case WIDX_SEARCH_TEXT_BOX: + WindowStartTextbox(*this, widgetIndex, _searchFilter, kTextInputSize); + break; + case WIDX_SEARCH_CLEAR_BUTTON: + _searchFilter.clear(); + ApplySearchQuery(); + InvalidateWidget(WIDX_SEARCH_TEXT_BOX); + break; case WIDX_CLOSE_LIGHT: CloseAllRides(); break; @@ -399,13 +414,25 @@ namespace OpenRCT2::Ui::Windows SortList(); } + void OnTextInput(WidgetIndex widgetIndex, std::string_view text) override + { + if (widgetIndex != WIDX_SEARCH_TEXT_BOX) + return; + + if (text == _searchFilter) + return; + + _searchFilter.assign(text); + ApplySearchQuery(); + } + /** * * rct2: 0x006B35A1 */ ScreenSize OnScrollGetSize(int32_t scrollIndex) override { - const auto newHeight = static_cast(_rideList.size() * kScrollableRowHeight); + const auto newHeight = static_cast(CountVisibleItems() * kScrollableRowHeight); auto top = std::max(0, newHeight - widgets[WIDX_LIST].bottom + widgets[WIDX_LIST].top + 21); if (top < scrolls[0].contentOffsetY) { @@ -416,14 +443,30 @@ namespace OpenRCT2::Ui::Windows return { 0, newHeight }; } + int32_t GetNthVisibleItemIndex(int32_t target) const + { + auto j = 0; + for (auto i = 0u; i < _rideList.size(); i++) + { + if (!_rideList[i].Visible) + continue; + + if (j == target) + return i; + + j++; + } + return -1; + } + /** * * rct2: 0x006B361F */ void OnScrollMouseDown(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override { - const auto index = screenCoords.y / kScrollableRowHeight; - if (index < 0 || static_cast(index) >= _rideList.size()) + const auto index = GetNthVisibleItemIndex(screenCoords.y / kScrollableRowHeight); + if (index < 0) return; // Open ride window @@ -448,8 +491,8 @@ namespace OpenRCT2::Ui::Windows */ void OnScrollMouseOver(int32_t scrollIndex, const ScreenCoordsXY& screenCoords) override { - const auto index = screenCoords.y / kScrollableRowHeight; - if (index < 0 || static_cast(index) >= _rideList.size()) + const auto index = GetNthVisibleItemIndex(screenCoords.y / kScrollableRowHeight); + if (index < 0) return; selected_list_item = index; @@ -480,6 +523,11 @@ namespace OpenRCT2::Ui::Windows widgets[WIDX_LIST].right = width - 26; widgets[WIDX_LIST].bottom = height - 15; + widgets[WIDX_SEARCH_CLEAR_BUTTON].right = widgets[WIDX_LIST].right - 1; + widgets[WIDX_SEARCH_CLEAR_BUTTON].left = widgets[WIDX_SEARCH_CLEAR_BUTTON].right - 40; + widgets[WIDX_SEARCH_TEXT_BOX].right = widgets[WIDX_SEARCH_CLEAR_BUTTON].left - 2; + widgets[WIDX_SEARCH_TEXT_BOX].string = _searchFilter.data(); + widgets[WIDX_HEADER_CUSTOMISE].right = widgets[WIDX_LIST].right - 1; widgets[WIDX_HEADER_CUSTOMISE].left = widgets[WIDX_HEADER_CUSTOMISE].right - 14; @@ -570,7 +618,7 @@ namespace OpenRCT2::Ui::Windows // Draw number of attractions on bottom auto ft = Formatter(); - ft.Add(static_cast(_rideList.size())); + ft.Add(static_cast(CountVisibleItems())); DrawTextBasic( rt, windowPos + ScreenCoordsXY{ 4, widgets[WIDX_LIST].bottom + 2 }, ride_list_statusbar_count_strings[page], ft); @@ -589,6 +637,9 @@ namespace OpenRCT2::Ui::Windows auto y = 0; for (size_t i = 0; i < _rideList.size(); i++) { + if (!_rideList[i].Visible) + continue; + StringId format = STR_BLACK_STRING; if (_quickDemolishMode) format = STR_RED_STRINGID; @@ -882,6 +933,28 @@ namespace OpenRCT2::Ui::Windows }); } + bool IsFiltered(const u8string& rideName) + { + if (_searchFilter.empty()) + return true; + + // Check if the filter matches the ride name + return String::contains(u8string_view(rideName), _searchFilter, true); + } + + void ApplySearchQuery() + { + for (auto& entry : _rideList) + { + entry.Visible = IsFiltered(entry.Name); + } + } + + size_t CountVisibleItems() const + { + return std::count_if(_rideList.begin(), _rideList.end(), [](const RideListEntry& entry) { return entry.Visible; }); + } + /** * * rct2: 0x006B39A8 @@ -897,14 +970,19 @@ namespace OpenRCT2::Ui::Windows continue; } + // Get the ride name once and use it for both filtering and storage + auto rideName = rideRef.getName(); + if (rideRef.windowInvalidateFlags & RIDE_INVALIDATE_RIDE_LIST) { rideRef.windowInvalidateFlags &= ~RIDE_INVALIDATE_RIDE_LIST; } - RideListEntry entry{}; - entry.Id = rideRef.id; - entry.Name = rideRef.getName(); + RideListEntry entry{ + .Id = rideRef.id, + .Name = std::move(rideName), + .Visible = IsFiltered(rideName), + }; _rideList.push_back(std::move(entry)); }