diff --git a/include/TGUI/Widgets/ListView.hpp b/include/TGUI/Widgets/ListView.hpp index 065a4546d..f9adb9859 100644 --- a/include/TGUI/Widgets/ListView.hpp +++ b/include/TGUI/Widgets/ListView.hpp @@ -60,6 +60,7 @@ namespace tgui struct Item { std::vector texts; + Sprite icon; }; struct Column @@ -292,6 +293,25 @@ namespace tgui int getSelectedItemIndex() const; + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Sets a small icon in front of the item + /// + /// @param index Index of the item + /// @param texture Texture of the item icon + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void setItemIcon(std::size_t index, const Texture& texture); + + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief Gets the icon displayed in front of the item + /// + /// @param index Index of the item + /// + /// @return Texture of the item icon + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Texture getItemIcon(std::size_t index) const; + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Returns the amount of items in the list view /// @@ -661,6 +681,8 @@ namespace tgui unsigned int m_textSize = 0; unsigned int m_headerTextSize = 0; unsigned int m_separatorWidth = 1; + unsigned int m_iconCount = 0; + float m_maxIconWidth = 0; bool m_headerVisible = true; CopiedSharedPtr m_horizontalScrollbar; diff --git a/src/TGUI/Widgets/ListView.cpp b/src/TGUI/Widgets/ListView.cpp index 617fd6345..ef33826c2 100644 --- a/src/TGUI/Widgets/ListView.cpp +++ b/src/TGUI/Widgets/ListView.cpp @@ -269,6 +269,7 @@ namespace tgui { Item item; item.texts.push_back(createText(text)); + item.icon.setOpacity(m_opacityCached); m_items.push_back(std::move(item)); updateVerticalScrollbarMaximum(); @@ -289,6 +290,7 @@ namespace tgui for (const auto& text : itemTexts) item.texts.push_back(createText(text)); + item.icon.setOpacity(m_opacityCached); m_items.push_back(std::move(item)); updateVerticalScrollbarMaximum(); @@ -331,8 +333,30 @@ namespace tgui if (index >= m_items.size()) return false; + const bool wasIconSet = m_items[index].icon.isSet(); m_items.erase(m_items.begin() + index); + if (wasIconSet) + { + --m_iconCount; + + const float oldMaxIconWidth = m_maxIconWidth; + m_maxIconWidth = 0; + if (m_iconCount > 0) + { + // Rescan all items to find the largest icon + for (const auto& item : m_items) + { + if (!item.icon.isSet()) + continue; + + m_maxIconWidth = std::max(m_maxIconWidth, item.icon.getSize().x); + if (m_maxIconWidth == oldMaxIconWidth) + break; + } + } + } + updateVerticalScrollbarMaximum(); return true; } @@ -346,6 +370,9 @@ namespace tgui m_items.clear(); + m_iconCount = 0; + m_maxIconWidth = 0; + updateVerticalScrollbarMaximum(); } @@ -384,6 +411,60 @@ namespace tgui ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void ListView::setItemIcon(std::size_t index, const Texture& texture) + { + if (index >= m_items.size()) + { + TGUI_PRINT_WARNING("setItemIcon called with invalid index."); + return; + } + + const bool wasIconSet = m_items[index].icon.isSet(); + m_items[index].icon.setTexture(texture); + + if (m_items[index].icon.isSet()) + { + m_maxIconWidth = std::max(m_maxIconWidth, m_items[index].icon.getSize().x); + if (!wasIconSet) + ++m_iconCount; + } + else if (wasIconSet) + { + --m_iconCount; + + const float oldMaxIconWidth = m_maxIconWidth; + m_maxIconWidth = 0; + if (m_iconCount > 0) + { + // Rescan all items to find the largest icon + for (const auto& item : m_items) + { + if (!item.icon.isSet()) + continue; + + m_maxIconWidth = std::max(m_maxIconWidth, item.icon.getSize().x); + if (m_maxIconWidth == oldMaxIconWidth) + break; + } + } + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + Texture ListView::getItemIcon(std::size_t index) const + { + if (index < m_items.size()) + return m_items[index].icon.getTexture(); + else + { + TGUI_PRINT_WARNING("getItemIcon called with invalid index."); + return {}; + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + std::size_t ListView::getItemCount() const { return m_items.size(); @@ -914,6 +995,8 @@ namespace tgui { for (auto& text : item.texts) text.setOpacity(m_opacityCached); + + item.icon.setOpacity(m_opacityCached); } } else if (property == "font") @@ -1270,6 +1353,7 @@ namespace tgui void ListView::updateScrollbars() { + const bool verticalScrollbarAtBottom = (m_verticalScrollbar->getValue() + m_verticalScrollbar->getViewportSize() >= m_verticalScrollbar->getMaximum()); const float headerHeight = (m_headerVisible && !m_columns.empty()) ? getHeaderHeight() : 0.f; const Vector2f innerSize = {std::max(0.f, getInnerSize().x - m_paddingCached.getLeft() - m_paddingCached.getRight()), std::max(0.f, getInnerSize().y - m_paddingCached.getTop() - m_paddingCached.getBottom() - headerHeight)}; @@ -1295,6 +1379,10 @@ namespace tgui m_horizontalScrollbar->setSize({getInnerSize().x, m_horizontalScrollbar->getSize().y}); m_horizontalScrollbar->setViewportSize(static_cast(innerSize.x)); } + + // If the scrollbar was at the bottom then keep it at the bottom if it changes due to a different viewport size + if (verticalScrollbarAtBottom && (m_verticalScrollbar->getValue() + m_verticalScrollbar->getViewportSize() < m_verticalScrollbar->getMaximum())) + m_verticalScrollbar->setValue(m_verticalScrollbar->getMaximum() - m_verticalScrollbar->getViewportSize()); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1356,15 +1444,46 @@ namespace tgui if (firstItem == lastItem) return; + const float verticalTextOffset = (m_itemHeight - Text::getLineHeight(m_fontCached, m_textSize)) / 2.0f; const float headerHeight = (m_headerVisible && !m_columns.empty()) ? getHeaderHeight() : 0.f; const float textPadding = Text::getExtraHorizontalOffset(m_fontCached, m_textSize); const float columnHeight = getInnerSize().y - m_paddingCached.getTop() - m_paddingCached.getBottom() - headerHeight - (m_horizontalScrollbar->isShown() ? m_horizontalScrollbar->getSize().y : 0); - const Clipping clipping{target, states, {textPadding, 0}, {columnWidth - (2 * textPadding), columnHeight}}; - states.transform.translate({0, -static_cast(m_verticalScrollbar->getValue())}); - states.transform.translate({0, (m_itemHeight * firstItem) + (m_itemHeight - Text::getLineHeight(m_fontCached, m_textSize)) / 2.0f}); + // Draw the icons. + // If at least one icon is set then all items in the first column have to be shifted to make room for the icon. + if ((column == 0) && (m_iconCount > 0)) + { + const sf::Transform transformBeforeIcons = states.transform; + const Clipping clipping{target, states, {textPadding, 0}, {columnWidth - (2 * textPadding), columnHeight}}; + + states.transform.translate({0, (m_itemHeight * firstItem) - static_cast(m_verticalScrollbar->getValue())}); + + for (std::size_t i = firstItem; i < lastItem; ++i) + { + if (!m_items[i].icon.isSet()) + { + states.transform.translate({0, static_cast(m_itemHeight)}); + continue; + } + + const float verticalIconOffset = (m_itemHeight - m_items[i].icon.getSize().y) / 2.f; + + states.transform.translate({textPadding, verticalIconOffset}); + m_items[i].icon.draw(target, states); + states.transform.translate({-textPadding, static_cast(m_itemHeight) - verticalIconOffset}); + } + + states.transform = transformBeforeIcons; + + const float extraIconSpace = m_maxIconWidth + textPadding; + columnWidth -= extraIconSpace; + states.transform.translate({extraIconSpace, 0}); + } + + const Clipping clipping{target, states, {textPadding, 0}, {columnWidth - (2 * textPadding), columnHeight}}; + states.transform.translate({0, (m_itemHeight * firstItem) - static_cast(m_verticalScrollbar->getValue())}); for (std::size_t i = firstItem; i < lastItem; ++i) { if (column >= m_items[i].texts.size()) @@ -1374,16 +1493,16 @@ namespace tgui } float translateX; - if ((m_columns[column].alignment == ColumnAlignment::Left) || (column >= m_columns.size())) + if ((column >= m_columns.size()) || (m_columns[column].alignment == ColumnAlignment::Left)) translateX = textPadding; else if (m_columns[column].alignment == ColumnAlignment::Center) translateX = (columnWidth - m_items[i].texts[column].getSize().x) / 2.f; else // if (m_columns[column].alignment == ColumnAlignment::Right) translateX = columnWidth - textPadding - m_items[i].texts[column].getSize().x; - states.transform.translate({translateX, 0}); + states.transform.translate({translateX, verticalTextOffset}); m_items[i].texts[column].draw(target, states); - states.transform.translate({-translateX, static_cast(m_itemHeight)}); + states.transform.translate({-translateX, static_cast(m_itemHeight) - verticalTextOffset}); } } diff --git a/tests/Widgets/ListView.cpp b/tests/Widgets/ListView.cpp index 6a1a0c036..6b4ee3ca1 100644 --- a/tests/Widgets/ListView.cpp +++ b/tests/Widgets/ListView.cpp @@ -229,9 +229,14 @@ TEST_CASE("[ListView]") REQUIRE(listView->getTextSize() == 20); listView->setTextSize(0); - REQUIRE(listView->getTextSize() > 0); - REQUIRE(listView->getTextSize() != 20); - REQUIRE(listView->getTextSize() < 50); + const unsigned int textSize = listView->getTextSize(); + REQUIRE(textSize > 0); + REQUIRE(textSize != 20); + REQUIRE(textSize < 50); + + listView->setItemHeight(60); + REQUIRE(listView->getTextSize() > textSize); + REQUIRE(listView->getTextSize() < 60); } SECTION("HeaderTextSize") @@ -380,20 +385,20 @@ TEST_CASE("[ListView]") } } - SECTION("Header changes item positions") + SECTION("Click on header") { listView->setHeaderHeight(30); listView->addColumn("Col 1", 50); listView->addColumn("Col 2", 50); - mousePressed({40, 68}); - mouseReleased({40, 68}); - REQUIRE(listView->getSelectedItemIndex() == 0); + mousePressed({40, 35}); + mouseReleased({40, 35}); + REQUIRE(listView->getSelectedItemIndex() == -1); listView->setHeaderVisible(false); - mousePressed({40, 68}); - mouseReleased({40, 68}); - REQUIRE(listView->getSelectedItemIndex() == 2); + mousePressed({40, 35}); + mouseReleased({40, 35}); + REQUIRE(listView->getSelectedItemIndex() == 0); } SECTION("Vertical scrollbar interaction") @@ -623,10 +628,6 @@ TEST_CASE("[ListView]") renderer.setSelectedTextColorHover("#808080"); }; - listView->addColumn("C1", 40); - listView->addColumn("C2", 70); - listView->addColumn("C3", 70); - listView->addItem({"1", "1.2"}); listView->addItem("2"); listView->addItem({"3", "3.2"}); @@ -638,6 +639,15 @@ TEST_CASE("[ListView]") const sf::Vector2f mousePos2{30, 50}; const sf::Vector2f mousePos3{30, 45}; + SECTION("No columns") + { + TEST_DRAW("ListView_NoColumns.png") + } + + listView->addColumn("C1", 40); + listView->addColumn("C2", 70); + listView->addColumn("C3", 70); + SECTION("No selected item") { SECTION("No hover") @@ -782,5 +792,12 @@ TEST_CASE("[ListView]") } } } + + SECTION("Icons") + { + listView->setItemIcon(3, {"resources/Texture6.png", {0, 0, 20, 14}}); + listView->setItemIcon(4, {"resources/Texture7.png", {0, 0, 14, 14}}); + TEST_DRAW("ListView_Icons.png") + } } } diff --git a/tests/expected/ListView_Icons.png b/tests/expected/ListView_Icons.png new file mode 100644 index 000000000..d6184870d Binary files /dev/null and b/tests/expected/ListView_Icons.png differ diff --git a/tests/expected/ListView_NoColumns.png b/tests/expected/ListView_NoColumns.png new file mode 100644 index 000000000..4fa860454 Binary files /dev/null and b/tests/expected/ListView_NoColumns.png differ