From 8aeb12413349778293cb47e37dde2abbd749339b Mon Sep 17 00:00:00 2001
From: Daniel Brooks <db48x@db48x.net>
Date: Fri, 18 Oct 2024 04:16:39 -0700
Subject: [PATCH] add a tab bar to the uilist showing categories, if there are
 any

Allows the user to select a category using the mouse, in addition to
the existing key bindings.

Also tweaks how the size of the uilist is computed a bit, to keep the
calculated_menu_size correct even when the caller specifies desired_bounds.

Fixes #75666
Fixes #76746
---
 src/magic.cpp |  2 +-
 src/ui.cpp    | 69 +++++++++++++++++++++++++++++++++++++--------------
 src/ui.h      |  3 ++-
 3 files changed, 53 insertions(+), 21 deletions(-)

diff --git a/src/magic.cpp b/src/magic.cpp
index 3453c804d1ba8..2330a82bf36db 100644
--- a/src/magic.cpp
+++ b/src/magic.cpp
@@ -2944,7 +2944,7 @@ spell &known_magic::select_spell( Character &guy )
         -1.0,
             -1.0,
             std::max( 80, TERMX * 3 / 8 ) *ImGui::CalcTextSize( "X" ).x,
-            clamp( static_cast<int>( known_spells_sorted.size() ), 24, TERMY * 9 / 10 ) *ImGui::GetTextLineHeightWithSpacing(),
+            clamp( static_cast<int>( known_spells_sorted.size() ), 24, TERMY * 9 / 10 ) *ImGui::GetTextLineHeight(),
         };
 
     spell_menu.title = _( "Choose a Spell" );
diff --git a/src/ui.cpp b/src/ui.cpp
index 33aacf9f4a5ff..0f09a3181944e 100644
--- a/src/ui.cpp
+++ b/src/ui.cpp
@@ -71,6 +71,28 @@ void uilist_impl::draw_controls()
         ImGui::Separator();
     }
 
+    if( !parent.categories.empty() ) {
+        if( ImGui::BeginTabBar( "##categories", ImGuiTabBarFlags_FittingPolicyScroll | ImGuiTabBarFlags_NoCloseWithMiddleMouseButton ) ) {
+            for( size_t i = 0; i < parent.categories.size(); i++ ) {
+                auto cat = parent.categories[ i ];
+                bool selected = i == parent.switch_to_category;
+                ImGuiTabItemFlags_ flags = ImGuiTabItemFlags_None;
+                if( selected ) {
+                    flags = ImGuiTabItemFlags_SetSelected;
+                    parent.switch_to_category = -1;
+                }
+                if( ImGui::BeginTabItem( cat.second.c_str(), nullptr, flags ) ) {
+                    if( parent.current_category != i ) {
+                        parent.current_category = i;
+                        parent.filterlist();
+                    }
+                    ImGui::EndTabItem();
+                }
+            }
+            ImGui::EndTabBar();
+        }
+    }
+
     // An invisible table with three columns. Used to create a sidebar effect.
     // Ideally we would use a layout engine for this, but ImGui does not natively support any.
     // TODO: Investigate using Stack Layout (https://github.com/thedmd/imgui/tree/feature/layout-external)
@@ -471,7 +493,7 @@ void uilist::init()
     max_column_len = 0;      // for calculating space for second column
 
     categories.clear();
-    current_category = 0;
+    switch_to_category = current_category = 0;
 
     input_category = "UILIST";
     additional_actions.clear();
@@ -583,12 +605,12 @@ void uilist::filterlist()
         if( fentries.empty() ) {
             selected = -1;
         } else {
-            selected = fentries [ 0 ];
+            hovered = selected = fentries [ 0 ];
         }
     } else if( fselected < static_cast<int>( fentries.size() ) ) {
-        selected = fentries[fselected];
+        hovered = selected = fentries[fselected];
     } else {
-        fselected = selected = -1;
+        hovered = fselected = selected = -1;
     }
     // scroll to top of screen if all remaining entries fit the screen.
     if( static_cast<int>( fentries.size() ) <= vmax ) {
@@ -681,6 +703,11 @@ void uilist::calc_data()
         text_size.y += ( s.ItemSpacing.y * expected_num_lines ) + ( s.ItemSpacing.y * 2.0 );
     }
 
+    ImVec2 tabs_size = {};
+    if( !categories.empty() ) {
+        tabs_size.y = ImGui::GetTextLineHeightWithSpacing() + ( 2.0 * s.FramePadding.y );
+    }
+
     ImVec2 desc_size = {};
     if( desc_enabled ) {
         desc_size = calc_size( footer_text );
@@ -695,30 +722,34 @@ void uilist::calc_data()
         float expected_num_lines = desc_size.y / ImGui::GetTextLineHeight();
         desc_size.y += ( s.ItemSpacing.y * expected_num_lines ) + ( s.ItemSpacing.y * 2.0 );
     }
-    float additional_height = title_size.y + text_size.y + desc_size.y + 2.0 *
+    float additional_height = title_size.y + text_size.y + desc_size.y + tabs_size.y + 2.0 *
                               ( s.FramePadding.y + s.WindowBorderSize );
 
     if( vmax * ImGui::GetTextLineHeightWithSpacing() + additional_height >
-        ImGui::GetMainViewport()->Size.y ) {
-        vmax = floorf( ( ImGui::GetMainViewport()->Size.y - additional_height ) /
+        0.9 * ImGui::GetMainViewport()->Size.y ) {
+        vmax = floorf( ( 0.9 * ImGui::GetMainViewport()->Size.y - additional_height + ( s.FramePadding.y * 2.0 ) ) /
                        ImGui::GetTextLineHeightWithSpacing() );
     }
 
     float padding = 2.0f * s.CellPadding.x;
-    calculated_hotkey_width = ImGui::CalcTextSize( "X" ).x;
+    calculated_hotkey_width = ImGui::CalcTextSize( "M" ).x;
     calculated_label_width = 0.0;
     calculated_secondary_width = 0.0;
-    for( int fentry : fentries ) {
-        calculated_label_width = std::max( calculated_label_width, calc_size( entries[fentry].txt ).x );
+    for( const uilist_entry &entry : entries ) {
+        calculated_label_width = std::max( calculated_label_width, calc_size( entry.txt ).x );
         calculated_secondary_width = std::max( calculated_secondary_width,
-                                               calc_size( entries[fentry].ctxt ).x );
+                                               calc_size( entry.ctxt ).x );
     }
     calculated_menu_size = { 0.0, 0.0 };
     calculated_menu_size.x += calculated_hotkey_width + padding;
     calculated_menu_size.x += calculated_label_width + padding;
     calculated_menu_size.x += calculated_secondary_width + padding;
-    calculated_menu_size.y = std::min( ImGui::GetMainViewport()->Size.y - additional_height,
-                                       vmax * ImGui::GetTextLineHeightWithSpacing() ) + ( s.FramePadding.y * 2.0 );
+    float max_avail_height = ImGui::GetMainViewport()->Size.y;
+    if( desired_bounds.has_value() ) {
+        max_avail_height = desired_bounds.value().h;
+    }
+    calculated_menu_size.y = std::min( max_avail_height - additional_height + ( s.FramePadding.y * 2.0 ),
+                                       vmax * ImGui::GetTextLineHeightWithSpacing() + ( s.FramePadding.y * 2.0 ) );
 
     extra_space_left = 0.0;
     extra_space_right = 0.0;
@@ -955,13 +986,13 @@ void uilist::query( bool loop, int timeout, bool allow_unfiltered_hotkeys )
         } else if( filtering && ret_act == "UILIST.FILTER" ) {
             inputfilter();
         } else if( !categories.empty() && ( ret_act == "UILIST.LEFT" || ret_act == "UILIST.RIGHT" ) ) {
-            current_category += ret_act == "UILIST.LEFT" ? -1 : 1;
-            if( current_category < 0 ) {
-                current_category = categories.size() - 1;
-            } else if( current_category >= static_cast<int>( categories.size() ) ) {
-                current_category = 0;
+            int tmp = current_category + (ret_act == "UILIST.LEFT" ? -1 : 1);
+            if( tmp < 0 ) {
+                tmp = categories.size() - 1;
+            } else if( tmp >= static_cast<int>( categories.size() ) ) {
+                tmp = 0;
             }
-            filterlist();
+            switch_to_category = static_cast<size_t>( tmp );
         } else if( iter != keymap.end() ) {
             if( allow_unfiltered_hotkeys ) {
                 const bool enabled = entries[iter->second].enabled;
diff --git a/src/ui.h b/src/ui.h
index 4a70de8fb671b..4f82df2cf38f6 100644
--- a/src/ui.h
+++ b/src/ui.h
@@ -486,7 +486,8 @@ class uilist // NOLINT(cata-xy)
         bool need_to_scroll = false;
         std::vector<std::pair<std::string, std::string>> categories;
         std::function<bool( const uilist_entry &, const std::string & )> category_filter;
-        int current_category = 0;
+        size_t current_category = 0;
+        size_t switch_to_category = 0;
 
     public:
         // Results